atom

repository·main·Indexed 18 days ago

https://github.com/nucleic/atom

A framework for creating memory-efficient Python objects with built-in validation, dynamic initialization, and change notification. It serves as the model binding layer for the Enaml UI framework and provides reactive state management via the Atom class, specialized containers like atomlist and atomdict, and high-performance implementations through CAtom.

Tokens
24.6K
Snippets
92
Records
132
Agent score
58%

What's inside atom

  1. Overview of the Atom framework

    main

    Atom is a framework designed for creating memory-efficient Python objects. It provides enhanced features for object management, including:

    • Dynamic Initialization: Support for default values and initialization logic.
    • Validation: Ensuring attribute types and constraints (e.g., ranges) are respected.
    • Change Notification: The ability to observe and react to attribute changes via decorators.

    Atom serves as the default model binding layer for the Enaml UI framework.

  2. Define Atom members and type validation

    main

    In an Atom class, you define attributes as members using descriptors. These members define the available attributes on instances and provide type validation.

    • Value(): A member that performs no type validation (can store any type).
    • Int(default): A member that validates the assigned value is an integer.
    • List(default): A member that validates the assigned value is a list.

    Note on Attribute Restriction: By default, Atom objects are compact and do not allow defining new attributes on instances that were not declared as members. Attempting to set an undefined attribute will cause a crash. To re-enable dynamic attribute assignment, add __slots__ = ('__dict__',) to your class definition.

    Metadata/Tags: You can attach metadata to descriptors using the .tag() method. While Atom does not use these tags internally, they can be used to filter members.

    from atom.api import Atom, Value, Int, List
    
    class CompactObject(Atom):
        untyped_value = Value()
        int_value = Int(10)
        list_value = List().tag(pref=True)
  3. Customize pickling behavior in Atom subclasses

    main
    In version 0.9.0 and later, you can customize which member values are pickled by implementing a method prefixed with _getstate_ on your Atom subclass. This is useful for preventing non-restorable states (like Constant members) from being pickled.
  4. Understand Atom's Python C++ bindings architecture

    main

    Atom's Python bindings are hand-written using cppy. The implementation follows modern C API practices to support sub-interpreters, which results in the following architectural constraints:

    • Multi-phase Initialization: Modules use the multi-phase extension module initialization mechanism as defined in PEP 489.
    • Namespace Isolation: All non-exported symbols are enclosed in anonymous namespaces.
    • Dynamic Typing: Atom avoids static types in favor of dynamical types (though type slots and related structures are stored in a static variable).
    • Static Variable Usage: The use of static variables is limited to cases that do not cause state leakage between multiple sub-interpreters.
  5. Customize Atom members using specially named methods

    main

    Atom allows you to customize the behavior of class members by defining methods with specific prefixes. The method name must include the name of the member being customized.

    Available prefixes for customization:

    • _default_<member>: Define default values.
    • _observe_<member>: Define a static observer.
    • _validate_<member>: Define a custom validation algorithm.
    • _post_getattr_<member>: Customize the post-getattr step.
    • _post_setattr_<member>: Customize the post-setattr step.
    • _post_validate_<member>: Customize the post-validate step.
    • _getstate_<member>: Determine if a member should be pickled.
  6. Understand Property notification behavior

    main

    It is critical to understand that Property members do not behave like Value members regarding notifications:

    • No notifications are emitted by default when getting or setting a Property value.
    • Notifications are always emitted when a Property is deleted or when its cache is reset.

    If you need to manually trigger notifications during a property access, you must follow the manual notification patterns.

  7. Use framework-called methods for lifecycle and observation

    main

    Atom provides specially named methods that the framework calls automatically. These are not intended to be called directly by user code.

    Post-setattr methods

    To perform an action immediately after a member's value is set, define a method following the pattern _post_setattr_<member_name>(self, old, new). This method receives the previous value (old) and the newly assigned value (new).

    Observer methods

    There are two ways to observe changes to a member:

    1. Mangled method name: Define a method named _observe_<member_name>(self, change). This method receives a dictionary containing information about the modification.
    2. @observe decorator: Decorate a method with @observe('<member_name>').

    Warning: For container members like List, the observer is only triggered when the container itself is replaced. Changes to the container's contents (e.g., via .append()) will not trigger the observer.

    from atom.api import Atom, Int, List, observe
    
    class CompactObject(Atom):
        int_value = Int(10)
        list_value = List()
    
        # Called after int_value is set
        def _post_setattr_int_value(self, old, new):
            print(f'Changed from {old} to {new}')
    
        # Called when int_value changes
        def _observe_int_value(self, change):
            print(change)
    
        # Alternative observer using decorator
        @observe('list_value')
        def notify_change(self, change):
            print(change)
  8. How members work in Atom

    main

    Members are descriptors used in an Atom class definition to define the fields (attributes) that exist on each instance. They handle the logic for accessing, setting, and validating data.

    Member Reading (Accessing a value)

    When you access a member (e.g., obj.value):

    1. Check for existing value: If a value was previously set, it is retrieved directly.
    2. Retrieve default: If no value exists, the framework fetches the default value.
    3. Validate: The default value is validated against the member's rules.
    4. Post-validation: A post-validation method is run (defaults to no-op).
    5. Store & Notify: The value is stored in the instance, and observers are notified of the 'create' event.
    6. Post-getattr: A post-getattr step is run before the value is returned.

    Member Writing (Setting a value)

    When you set a member (e.g., obj.value = 10):

    1. Validate: The new value is validated.
    2. Post-validation: A post-validation method is run.
    3. Store: The value is stored in the instance.
    4. Post-setattr: A post-setattr step is run.
    5. Notify: Observers are notified of the change.
    class Custom(Atom):
        value = Int()
    
    obj = Custom()
    obj.value  # Triggers default retrieval, validation, and storage
  9. Avoid manual instantiation of Atom's custom containers

    main

    Atom implements custom list and dictionary subclasses to handle type validation and notifications.

    Warning: Users should generally avoid instantiating these containers manually. These containers require internal references to both the specific member and the instance they are tied to, which is managed by the Atom framework. Manual instantiation may bypass necessary validation or notification logic.

    Note on Typed Dictionaries: The current implementation of the type-validated dictionary is not a subclass of the Python builtin dict. This can cause issues when assigning a value from a Dict member to another Dict. To avoid errors, wrap the content in a standard dictionary call:

    # Workaround for assigning Dict members
    new_dict = dict(old_dict_member)
  10. How observers and notifications work in Atom

    main

    Atom implements the observer pattern to notify listeners when member values change. An observer is a callable triggered by specific events:

    • 'create': When a member is assigned a value for the first time or accessed for the first time using its default value.
    • 'update': When a different value is assigned to an existing member.
    • 'delete': When a member is deleted (via del or delattr).
    • 'container': Specific to ContainerList members, emitted during in-place modifications (e.g., adding or removing elements).

    Observers are categorized into Static Observers (defined at the class level and affecting all instances) and Dynamic Observers (bound to a specific instance at runtime).

  11. Understand Atom's dual-type Member[T, S] model

    main

    Atom members are Python descriptors that often perform type conversion during validation (e.g., using Coerced). Because of this, a member has two distinct types:

    1. Getter/Read type (T): The type returned when accessing the member.
    2. Setter/Write type (S): The type that the member can accept when being set.

    Most Atom members are automatically typed by the library, but understanding this Member[T, S] relationship is crucial when defining custom members or handling complex coercions.