# PCN File Format 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. - [Overview](#overview) - [Sections](#sections) - [Including Built-in Files](#including-built-in-files) - [Including User Files](#including-user-files) - [Inheritance](#inheritance) - [Properties](#properties) - [Common Types](#common-types) - [Expressions](#expressions) - [Section Access](#section-access) - [Bracket Access](#bracket-access) - [GUI files](#gui-files) ## Overview ```python3 # 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 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: ```python3 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: ```python3 class Empty: pass ``` ### Including Built-in Files 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`][1]. The sections and properties from the included file will be recursively merged, see [Including User Files](#including-user-files) for an example. [1]: https://gitlab.com/mx-moment/moment/-/blob/main/src/config/settings.py ### Including User Files Similar to [including built-in files](#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`: ```python3 self.include_file("b.py") class Shared: text: str = "Sample" gets_overriden: str = "A" class FromA: number: int = 1 ``` and `b.py`: ```python3 class Shared: gets_overriden: str = "B" class FromB: number: int = 2 ``` This results in a merged PCN looking like so: ```python3 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: ```python3 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 ``` ### Inheritance Like other Python classes, sections can inherit from other sections. Unlike including files, sections are not merged recursively. This file: ```python3 class Mixin: first: bool = True second: bool = False class First(Mixin): pass class Second(Mixin): third: int = 100 ``` Would be equivalent to: ```python3 class First(Mixin): first: bool = True second: bool = False class Second(Mixin): first: bool = True second: bool = False third: int = 100 ``` ## Properties 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: ```python3 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: ```python3 "!alice:example.org" = "Alice" ``` Properties with these names can only be accessed by the [brackets syntax](#bracket-access). ### Common types - `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: ```python3 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: ```python3 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]`: ```python3 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. ### Expressions 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: ```python3 class Section1: other: int = self.text.lower() * 2 # "exampleexample" text: str = "Example" ``` Is roughly equivalent to this in standard Python: ```python3 class Section1: @property def other(self) -> str: return self.text.lower() * 2 @property def text(self) -> str: return "Example" ``` ### Section Access The current section and its properties are accessed via `self`: ```python3 class Base: number: int = 10 other: int = self.number * 2 # 20 ``` The parent section is accessed via `parent`: ```python3 class Base: number: int = 10 class Inner: number: int = parent.number * 2 ``` Child sections can be accessed by `self.SectionName`: ```python3 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: ```python3 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`: ```python3 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" ``` ### Bracket Access 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](#properties)): ```python3 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: ```python3 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`: ```python3 "!alice:example.org": str = "Alice" class Names: alice: str = self.root["!alice:example.org"] = "Alice" ``` # GUI Files 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.