Declarative notes: Notes as code#
One of the goals of this project is to enable building, maintaining, and sharing complex note hierarchies using Python. This approach is declarative in nature, inspired by SQLAlchemy’s declarative mapping approach.
The general idea of declarative programming is that you specify the desired end state, not the steps needed to reach it.
For a fully-featured example of a note hierarchy designed using this approach, see Event tracker.
Note subclasses#
The basic technique is to subclass Note
:
class MyNote(Note): pass
Mixin subclasses#
Sometimes you want to logically group attributes or children together in a reusable way, but don’t need a fully-featured Note
. In those cases you can use a Mixin
.
The basic technique is to subclass Mixin
:
class MyMixin(Mixin): pass
Setting fields#
You can set the following fields by setting attribute values:
class MyNote(Note):
title = "My title"
note_type = "text"
mime = "text/html"
content = "<p>Hello, world!</p>"
Setting content from file#
Set note content from a file by setting Note.content_file
or Mixin.content_file
:
class MyFrontendScript(Note):
note_type = "code"
mime = "application/javascript;env=frontend"
content_file = "assets/myFrontendScript.js"
The filename is relative to the package the class is defined in. Currently accessing parent paths (".."
) is not supported.
Adding labels#
Use the decorator label
to add a label:
@label("sorted")
class Sorted(Mixin): pass
Now you can simply subclass this mixin if you want a note’s children to be sorted:
@label("iconClass", "bx bx-group")
class Contacts(Note, Sorted): pass
The above is equivalent to the following imperative approach:
contacts = Note(title="Contacts")
contacts += [Label("iconClass", "bx bx-group"), Label("sorted")]
Promoted attributes#
A special type of label is one which defines a promoted attribute. Decorators label_def
and relation_def
are provided for convenience.
@label("person")
@label_def("altName", multi=True)
@label_def("birthday", value_type="date")
@relation_def("livesAt")
@relation_def("livedAt", multi=True)
class Person(WorkspaceTemplate):
icon = "bx bxs-user-circle"
Singleton notes#
In some cases it’s important to generate the same Note.note_id
every time the class is instantiated. Templates, for example, should have only one instance and be automatically updated as changes are made to the code. This behavior can be accomplished in a number of ways.
Warning
Without setting Note.leaf
or Mixin.leaf
, TriliumAlchemy assumes that you want to explicitly specify the note’s children. Therefore it will delete any existing children which aren’t declaratively added. See Leaf notes to learn more.
Setting singleton
#
When Note.singleton
or Mixin.singleton
is set, the note’s Note.note_id
is generated based on the fully qualified class name, i.e. the class name including its modpath.
The following creates a template note for a task:
@label("template")
@label("iconClass", "bx bx-task")
class Task(Note):
singleton = True
Setting note_id_seed
#
When Note.note_id_seed
or Mixin.note_id_seed
is set, the provided value is hashed to generate Note.note_id
.
It uses the same hash algorithm used by Mixin.singleton
.
# now note_id won't change if we move the class to a different module
@label("template")
@label("iconClass", "bx bx-task")
class Task(Note):
note_id_seed = "Task"
Todo
Add a flag to set Mixin.note_id_seed
from class name (user guarantees uniqueness of class names)
Setting note_id
#
When Note.note_id
or Mixin.note_id
is set, the provided value is used as Note.note_id
directly.
class MyNote(Note):
note_id = "my_note_id"
Passing note_id
#
When note_id
is passed in the constructor of a Note
subclass, it’s similarly considered a singleton.
Child of singleton#
Every child of a singleton note is required to also have a deterministic Note.note_id
. Therefore a note_id
is generated for children of singletons, even if they don’t satisfy any of the above criteria.
This is recursive, so an entire note tree specified by Note
subclasses will be instantiated with a deterministic note_id
if the root satisfies any of the above criteria.
Adding relations#
You can declaratively add a relation to another note, as long as the target note is a singleton.
For example, to create a ~template
relation:
@relation("template", Task)
class TaskInstance(Note): pass
Now you can create a task by simply instantiating TaskInstance
, and it will automatically have ~template=Task
.
Adding children#
Use children
or child
to add children:
class Child1(Note): pass
class Child2(Note): pass
class Child3(Note): pass
@children(Child1, Child2) # add children with no branch prefix
@child(Child3, prefix="My prefix") # add child with branch prefix
class Parent(Note): pass
Custom initializer to add attributes, children#
Define Note.init
or Mixin.init
to add attributes and children dynamically. Use the following APIs to add attributes and children:
These APIs are required for singleton notes to generate a deterministic id for attributes and children, generating the same subtree every time the Note
subclass is instantiated.
For example, this mechanism is used internally to add an iconClass
label for certain Note
subclasses like Workspace
:
class IconMixin(Mixin):
icon: str = None
"""
If provided, defines value of `#iconClass` label.
"""
def init(self, attributes: list[Attribute], children: list[Branch]):
"""
Set `#iconClass` value by defining {obj}`IconMixin.icon`.
"""
if self.icon:
attributes += [
self.create_declarative_label("iconClass", self.icon)
]
Leaf notes#
If you design a note hierarchy using this approach, you might want to designate some “folder” notes to hold user-maintained notes. Set Note.leaf
or Mixin.leaf
to indicate this, in which case using children
or child
will raise an exception.
For example, this would be necessary for a list of contacts:
@label("sorted")
@label("iconClass", "bx bx-group")
class Contacts(Note):
singleton = True
leaf = True
Now, assuming it’s been placed in your hierarchy, you can access your contact list by simply instantiating Contacts
.