This document explains in details the PCN (Python Config Notation) format. PCN files are organized in a hierarchy of sections and properties. PCN files can also contain normal Python code, such as imports and custom functions.
# Lines starting with a "#" are considered comments.
# Comments can also be added to the end of normal lines.
# Sections can contain indented properties, other sections or functions.
class Example:
# Properties are written as "name: type = value", examples:
integer_number: int = 5
decimal_number: float = 2.5
character_string: str = "Sample text"
boolean: bool = True # or False
string_list: List[str] = ["foo", "bar", "baz"]
# Property values can be any Python expression, e.g. math operations:
other_number: int = (5 * 4) / 2
# "self" points to the current section, Example, containing other_number.
above_10: bool = self.other_number > 10 # result: False
class Names:
# Property names with characters outside of a-z A-Z 0-9 _ need quoting:
"@alice:example.org": str = "Alice"
"@bob:example.org": str = "Bob"
# Section content can also be accessed with the "self[name]" syntax,
# which works with quoted properties like the ones above:
alice_name: str = self["@alice:example.org"] # result: Alice
# Child sections are also accessible from "self":
child_integer: int = self.Test.integer # result: 5
class Test:
# "parent" refers to the section parent of this one, here "Names".
alice_name: str = parent["@alice:example.org"] # result: "Alice"
integer: int = parent.parent.integer_number # Example.integer_number, which is 5
# Top-level sections can also be accessed directly by names:
alice_name_2: str = Example.Names["@alice:example.org"]
integer_2: int = Example.integer_number
Sections are defined like Python classes, and can contain properties,
other sections, or Python functions.
A section's name should be written as CamelCase
, and can only contain
letters, digits and underscores.
The content of a section must be indented by that section's indentation plus
four spaces:
class FirstSection:
content_spaces: int = 0 + 4
class SectionInsideFirst:
content_spaces: int = 4 + 4
class SecondSection:
content_spaces: int = 0 + 4
Empty sections can be created using the pass
keyword:
class Empty:
pass
A section, including the file's root (which is treated as a section)
can include files that are supplied by the application using the
self.include_builtin(path)
function.
path
is the relative path to a file in the application's source folder,
for example self.include_builtin("config/settings.py")
refers to
src/config/settings.py
.
The sections and properties from the included file will be recursively merged, see Including User Files for an example.
Similar to including built-in files, user-written
local files can be included with self.include_file(path)
, where path
is an absolute or relative (from the current file's directory) file path.
Example with two files, a.py
:
self.include_file("b.py")
class Shared:
text: str = "Sample"
gets_overriden: str = "A"
class FromA:
number: int = 1
and b.py
:
class Shared:
gets_overriden: str = "B"
class FromB:
number: int = 2
This results in a merged PCN looking like so:
class Shared:
text: str = "Sample"
gets_overriden: str = "B"
class FromA:
number: int = 1
class FromB:
number: int = 2
Include functions can also be used inside a section other than the root.
If a.py
had the include line inside Shared
, the result would be:
class Shared:
text: str = "Sample"
gets_overriden: str = "A"
class FromA:
number: int = 1
class Shared:
gets_overriden: str = "B"
class FromB:
number: int = 2
Like other Python classes, sections can inherit from other sections. Unlike including files, sections are not merged recursively.
This file:
class Mixin:
first: bool = True
second: bool = False
class First(Mixin):
pass
class Second(Mixin):
third: int = 100
Would be equivalent to:
class First(Mixin):
first: bool = True
second: bool = False
class Second(Mixin):
first: bool = True
second: bool = False
third: int = 100
Properties have a name, optional type annotation and value.
Standard property names should be written in snake_case
.
In most cases, it is recommended to include type annotations, to make clear
what a property's value should be:
with_type: int = 3
complex_type: Dict[str, int] = {"abc": 1, "def": 2, "ghi": 3}
any_type: Any = None
same_as_above = None
If the property's name starts with a digit or contains characters other than letters, digits or underscores, that name must be quoted:
"!alice:example.org" = "Alice"
Properties with these names can only be accessed by the brackets syntax.
int
: An integer number, e.g. 4
.
float
: Floating point number, e.g. 4.5
. Can also be an integer.
str
: String, a piece of text.
If the text contains quotes or backslashes, escape them with a backslash.
Other properties can be included by combining strings or using an f-string:
escaped: str = "C:\\Users\\Alice \"Foo\" Bar"
number: int = 1
combined: str = "foo " + self.number
fstring: str = f"foo {number}"
bool
: Boolean, a value that can be either True
or False
.
None
: A None
value, represents an absence of choice.
Any
: A value that can be of any type.
list
: List of values, e.g. [1, 2, 3]
.
The type can be written as list
or List[type]
to specify what type the
list's item should be.
tuple
: Similar to lists, but the length cannot be changed once created.
Can be written as tuple
, Tuple[type, type]
to specify for example that
the tuple must have two items of certain types, or Tuple[type, ...]
for a tuple with any number of items of a certain same type:
anything: tuple = (1, 2, 3, "foo")
many_ints: Tuple[int, ...] = (1, 2, 3, 4, 5)
three_values: Tuple[int, str, bool] = (1, "example", False)
dict
: Mapping of keys to values. Can be written as dict
or
Dict[key_type, value_type]
: anything: dict = {1: 2, "foo": "bar", True: 1.5}
account_order: Dict[str, int] = {"@a:example.com": 1, "@b:example.com": 2}
Optional[type]
: A value that can be either that type or None
Union[type1, type2]
: A value that can be one of the type in the Union
.
The number of types can be more than two.
A property's value can be any Python expression. Properties can also refer to other properties, no matter what section they belong to or what order they are defined in.
This PCN code:
class Section1:
other: int = self.text.lower() * 2 # "exampleexample"
text: str = "Example"
Is roughly equivalent to this in standard Python:
class Section1:
@property
def other(self) -> str:
return self.text.lower() * 2
@property
def text(self) -> str:
return "Example"
The current section and its properties are accessed via self
:
class Base:
number: int = 10
other: int = self.number * 2 # 20
The parent section is accessed via parent
:
class Base:
number: int = 10
class Inner:
number: int = parent.number * 2
Child sections can be accessed by self.SectionName
:
class Base:
number: int = self.Inner.number
class Inner:
number: int = 10
Any section (or property, or function) defined at the root/top-level of the file can be accessed by name:
class First:
class InsideFirst:
number: int = Second.number * 2 # 20
other: int = Second.InsideSecond.number # 50
class Second:
number: int = 10
class InsideSecond:
number: int = 50
The root (which behaves like a section) can also be explicitely accessed
with self.root
:
number: int = 10
class First:
root_num: int = self.root.number # Same as just saying "number"
class Second:
first_num: int = self.root.First.root_num # Same as "First.root_num"
Inner sections and properties can also be accessed by the
section[name]
syntax. This is the only way to access properties with
non-standard names (as described in Properties):
class Names:
"!alice:example.org": str = "alice"
class Capitalized:
alice: str = parent["!alice:example.org"].capitalize() # "Alice"
The syntax can also be used to access properties dynamically:
class Names:
alice: str = "Alice"
property_name: str = "alice"
first_person: str = self[property_name]
Top-level properties can only be accessed this way using self.root
:
"!alice:example.org": str = "Alice"
class Names:
alice: str = self.root["!alice:example.org"] = "Alice"
When properties for PCN files are edited from the user interface
(programmatically or due to user actions), a separate file with a .gui.json
extension is created in the same folder.
These files take priority and override properties from the equivalent user files. They should not be edited by hand. When a property in the user config file is edited, any equivalent property in the GUI file is automatically dropped, to let the user's setting apply again.