Skip to content

pygx.symbolic

Symbolic Object Model.

symbolic

Symbolic Object Model.

This module enables Symbolic Object Model, which defines and implements the symbolic interfaces for common Python types (e.g. symbolic class, symbolic function and symbolic container types). Based on symbolic types, symbolic objects can be created, which can be then inspected, manipulated symbolically.

ContextualObject

ContextualObject(
    *,
    allow_partial: bool = False,
    sealed: bool | None = None,
    root_path: KeyPath | None = None,
    explicit_init: bool = False,
    **kwargs: Any
)

Bases: Object

Base class for contextual objects.

Contextual objects are objects whose attributes can be dynamically overridden using pg.contextual.override or resolved through pg.contextual.Placeholder, allowing them to inherit values from their containing objects.

Usages:

# Define a contextual object.
class A(pg.ContextualObject):
  x: int
  y: Any = pg.contextual.Placeholder()

# Create an instance of A
a = A(1)
print(a.x)  # Outputs: 1
print(a.y)  # Raises an error, as `a` has no containing object.

# Define another contextual object containing an instance of A
class B(pg.ContextualObject):
  y: int
  a: A

# Create an instance of B, containing "a"
b = B(y=2, a=a)
print(a.y)  # Outputs: 2, as "y" is resolved from the containing object (B).

# Contextual overrides are thread-specific
with pg.contextual.override(x=2):
  print(a.x)  # Outputs: 2

# Thread-specific behavior of `pg.contextual.override`
def foo(a):
  print(a.x)

with pg.contextual.override(x=3):
  t = threading.Thread(target=foo, args=(a,))
  t.start()
  t.join()
  # Outputs: 1, because `pg.contextual.override` is limited to the current
  # thread to avoid clashes in multi-threaded environments.

# To propagate the override to a new thread, use `pg.contextual.with_override`
with pg.contextual.override(x=3):
  t = threading.Thread(target=pg.contextual.with_override(foo), args=(a,))
  t.start()
  t.join()
  # Outputs: 3, as the override is explicitly propagated.
Source code in pygx/symbolic/_object.py
@_filter_typing_traceback_on_raise
def __init__(
    self,
    *,
    allow_partial: bool = False,
    sealed: bool | None = None,
    root_path: topology.KeyPath | None = None,
    explicit_init: bool = False,
    **kwargs: Any,
):
    """Create an Object instance.

    ``pg.Object`` synthesizes a keyword-only ``__init__``. Subclasses
    that need positional arguments must override ``__init__``
    explicitly and translate to keyword arguments before forwarding
    to ``super().__init__(**kwargs)``.

    Args:
      allow_partial: If True, the object can be partial.
      sealed: If True, seal the object from future modification (unless under
        a `pg.as_sealed(False)` context manager). If False, treat the object as
        unsealed. If None, it's determined by `cls.__sym_options__.frozen`.
      root_path: The symbolic path for current object. By default it's None,
        which indicates that newly constructed object does not have a parent.
      explicit_init: Should set to `True` when `__init__` is called via
        `pg.Object.__init__` instead of `super().__init__`.
      **kwargs: key/value arguments that align with the schema. All required
        keys in the schema must be specified, and values should be acceptable
        according to their value spec.

    Raises:
      KeyError: When required key(s) are missing.
      ValueError: When value(s) are not acceptable by their value spec.
    """
    # Placeholder for Google-internal usage instrumentation.

    cls = self.__class__
    # The ever-instantiated latch `apply_schema`'s post-instance guard
    # reads (#432: `update_schema` is only legal before instances exist).
    # Own-`__dict__`, latched on the construction ATTEMPT — mirroring the
    # native core, whose `__native_ctor_meta__` (the equivalent marker) is
    # created on the first instance op. One dict read per construct.
    meta = cls.__sym_meta__
    if not meta.instantiated:
        meta.instantiated = True
    if sealed is None:
        sealed = cls.__sym_options__.frozen

    if not isinstance(allow_partial, bool):
        raise TypeError(
            f"Expect bool type for argument 'allow_partial' in "
            f'symbolic.Object.__init__ but encountered {allow_partial}.'
        )

    # Fast path: const-keyed schema.
    if cls._sym_init_fields is not None and self._init_fast_path(
        kwargs,
        allow_partial,
        sealed,
        root_path,
        explicit_init,
    ):
        return

    # Slow path: handles varargs and schemas with non-const keys.

    # Model before-hook (#516) — the fast path was not taken
    # (`_sym_init_fields is None`), so the hook has not run yet.
    if cls._sym_preinit_overridden:
        kwargs = _apply_preinit(cls, kwargs)

    # We delay the seal attempt until members are all set.
    super().__init__(
        allow_partial=allow_partial,
        accessor_writable=cls.__sym_options__.attr_write,
        sealed=sealed,
        root_path=root_path,
        init_super=not explicit_init,
    )

    # Fill field_args and init_args from **kwargs.
    _, unmatched_keys = cls.__schema__.resolve(list(kwargs.keys()))
    # Fields opted out of `__init__` via `enable_init=False` resolve
    # against the schema but must still be rejected as kwargs.
    get_field = cls.__schema__.get_field
    unmatched_keys = list(unmatched_keys) + [
        k
        for k in kwargs
        if (f := get_field(k)) is not None and not f.enable_init
    ]
    if unmatched_keys:
        _screen_unexpected_init_keys(cls, kwargs, unmatched_keys)

    # Declared aliases are RESERVED wire spellings (#517, owner ruling):
    # an open (varkw) schema must not accept an extra kwarg equal to a
    # field's alias — the wire would become ambiguous. (Const-keyed
    # schemas never reach here with an alias-spelled kwarg; their
    # unknown-key screen already raised — or, under `varkw='ignore'`,
    # raised for alias keys before dropping the rest.)
    if cls._sym_alias_maps is not None:
        alias_to_name = cls._sym_alias_maps[0]
        for k in kwargs:
            if k in alias_to_name:
                raise TypeError(
                    f'{cls.__name__}.__init__() got keyword argument '
                    f'{k!r}, which is reserved as the alias of field '
                    f'{alias_to_name[k]!r}.'
                )

    # All field values come from kwargs — `pg.Object.__init__` is
    # keyword-only.
    field_args = dict(kwargs)

    # Check missing arguments when partial binding is disallowed.
    if not base.accepts_partial(self):
        missing_args = []
        for field in self.__class__.__schema__.fields.values():
            if (
                not field.value.has_default
                and isinstance(field.key, pg_typing.ConstStrKey)
                and field.key not in field_args
            ):
                missing_args.append(str(field.key))
        if missing_args:
            arg_phrase = formatting.auto_plural(
                len(missing_args), 'argument'
            )
            keys_str = formatting.comma_delimited_str(missing_args)
            raise TypeError(
                f'{self.__class__.__name__}.__init__() missing {len(missing_args)} '
                f'required {arg_phrase}: {keys_str}.'
            )

    # NOTE(daiyip): Accessor writable is honored by `Object.__setattr__` so
    # we can always make `_sym_attributes` accessor writable. This prevents
    # a child object's attribute access from being changed when it's
    # attached to a parent whose symbolic attributes are not writable.
    # Per-field validation is gated by `field.enable_validation` inside
    # `Schema.apply` / `Dict._formalized_value` — so a class-wide
    # `validate=False` (resolved into each field) and per-field overrides
    # both flow through naturally here.
    vars(self)['_sym_attributes'] = pg_dict.Dict(
        field_args,  # pyright: ignore[reportArgumentType]
        value_spec=self.__class__.sym_fields,
        allow_partial=allow_partial,
        sealed=sealed,
        accessor_writable=True,
        root_path=root_path,
        as_object_attributes_container=True,
        under_sym_mode=cls.__sym_options__.sym,
    )
    self._sym_attributes.sym_setparent(self)
    # Seed `init=False` fields from their defaults before `on_sym_post_init`
    # runs so `on_sym_ready`/`on_sym_bound` overrides start from a known baseline.
    _seed_non_symbolic_fields(self, cls._non_symbolic_fields)
    # Model after-validator (#516): fires before any lifecycle hook can
    # observe the object, gated like `on_sym_ready` (concrete only).
    if cls._sym_validate_overridden:
        abstract = (
            self._allow_partial and self.sym_partial
        ) or self.sym_puresymbolic
        if not abstract:
            self.on_sym_validate()
    self.on_sym_post_init()
    self.sym_seal(sealed)

sym_inferred

sym_inferred(key: str, default: Any = RAISE_IF_NOT_FOUND, **kwargs: Any) -> Any

Override to allow attribute to access scoped value.

Parameters:

Name Type Description Default
key str

attribute name.

required
default Any

Value to return if inference fails (else the error propagates).

RAISE_IF_NOT_FOUND
**kwargs Any

Optional keyword arguments for value inference.

{}

Returns:

Type Description
Any

The value of the symbolic attribute. If not available, returns the default value.

Raises:

Type Description
AttributeError

If the attribute does not exist or contextual attribute is not ready (and no default was given).

Source code in pygx/contextual/_contextual_object.py
@override
def sym_inferred(  # pyright: ignore[reportIncompatibleMethodOverride]
    self, key: str, default: Any = base.RAISE_IF_NOT_FOUND, **kwargs: Any
) -> Any:
    """Override to allow attribute to access scoped value.

    Args:
      key: attribute name.
      default: Value to return if inference fails (else the error propagates).
      **kwargs: Optional keyword arguments for value inference.

    Returns:
      The value of the symbolic attribute. If not available, returns the
        default value.

    Raises:
      AttributeError: If the attribute does not exist or contextual attribute
        is not ready (and no ``default`` was given).
    """
    if key not in self._sym_attributes:
        # Honor the `default` fallback for a missing key, matching the base
        # `sym_inferred` (whose `sym_getattr` would raise here).
        if default is base.RAISE_IF_NOT_FOUND:
            raise AttributeError(key)
        return default

    # Step 1: Try use value from `self.override`.
    # The reason is that `self.override` is short-lived and explicitly specified
    # by the user in scenarios like `LangFunc.render`, which should not be
    # affected by `pg.contextual.override`.
    v = contextual.get_scoped_value(self._contextual_overrides, key)
    if v is not None:
        return v.value

    # Step 2: Try use value from `pg.contextual.override` with `override_attrs`.
    # This gives users a chance to override the bound attributes of components
    # from the top, allowing change of bindings without modifying the code
    # that produces the components.
    override = contextual.get_override(key)  # pylint: disable=redefined-outer-name
    if override and override.override_attrs:
        return override.value

    # Step 3: Try use value from the symbolic tree, starting from self to
    # the root of the tree.
    # Step 4: If the value is not present, use the value from `context()` (
    # override_attrs=False).
    # Step 5: Otherwise use the default value from `Placeholder`.
    return super().sym_inferred(
        key, default=default, context_override=override, **kwargs
    )

override

override(**kwargs) -> ContextManager[dict[str, Override]]

Context manager to override the attributes of this component.

Source code in pygx/contextual/_contextual_object.py
def override(
    self, **kwargs
) -> ContextManager[dict[str, contextual.Override]]:
    """Context manager to override the attributes of this component."""
    vs = {k: contextual.Override(v) for k, v in kwargs.items()}
    return contextual.contextual_scope(self._contextual_overrides, **vs)

DescendantQueryOption

Bases: Enum

Options for querying descendants through sym_descendant.

FieldUpdate

FieldUpdate(
    path: KeyPath,
    target: Symbolic,
    field: Field | None,
    old_value: Any,
    new_value: Any,
)

Bases: Formattable

Class that describes an update to a field in an object tree.

Constructor.

Parameters:

Name Type Description Default
path KeyPath

KeyPath of the field that is updated.

required
target Symbolic

Parent of updated field.

required
field Field | None

Specification of the updated field.

required
old_value Any

Old value of the field.

required
new_value Any

New value of the field.

required
Source code in pygx/symbolic/_base.py
def __init__(
    self,
    path: topology.KeyPath,
    target: 'Symbolic',
    field: pg_typing.Field | None,
    old_value: Any,
    new_value: Any,
):
    """Constructor.

    Args:
      path: KeyPath of the field that is updated.
      target: Parent of updated field.
      field: Specification of the updated field.
      old_value: Old value of the field.
      new_value: New value of the field.
    """
    self.path = path
    self.target = target
    self.field = field
    self.old_value = old_value
    self.new_value = new_value

format

format(
    compact: bool = False, verbose: bool = True, root_indent: int = 0, **kwargs
) -> str

Formats this object.

Source code in pygx/symbolic/_base.py
def format(
    self,
    compact: bool = False,
    verbose: bool = True,
    root_indent: int = 0,
    **kwargs,
) -> str:
    """Formats this object."""
    return formatting.kvlist_str(
        [
            ('parent_path', self.target.sym_path, None),
            ('path', self.path, None),
            ('old_value', self.old_value, topology.MISSING_VALUE),
            ('new_value', self.new_value, topology.MISSING_VALUE),
        ],
        label=self.__class__.__name__,
        compact=compact,
        verbose=verbose,
        root_indent=root_indent,
        **kwargs,
    )

Inferential

Bases: TopologyAware, CustomTyping

Interface for values that could be dynamically inferred upon read.

Inferential values are objects whose values are not determined directly but are instead derived from other sources, such as references (pg.Ref) to other objects or computed based on their context (pg.symbolic.ValueFromParentChain) such as the symbolic tree they reside in.

When inferential values are utilized as symbolic attributes, we can obtain their original definition by invoking pg.Symbolic.sym_getattr, and their inferred values can be retrieved by calling pg.Symbolic.sym_inferred. The values retrieved from pg.Dict, pg.List and pg.Object through __getitem__ or __getattribute__ are all inferred values.

infer abstractmethod

infer(**kwargs: Any) -> Any

Returns the inferred value.

Parameters:

Name Type Description Default
**kwargs Any

Optional keyword arguments for inference, which are usually inferential subclass specific.

{}

Returns:

Type Description
Any

Inferred value.

Raises:

Type Description
AttributeError

If the value cannot be inferred.

Source code in pygx/symbolic/_base.py
@abc.abstractmethod
def infer(self, **kwargs: Any) -> Any:
    """Returns the inferred value.

    Args:
      **kwargs: Optional keyword arguments for inference, which are usually
        inferential subclass specific.

    Returns:
      Inferred value.

    Raises:
      AttributeError: If the value cannot be inferred.
    """

Symbolic

Symbolic(
    *,
    allow_partial: bool,
    accessor_writable: bool,
    sealed: bool,
    root_path: KeyPath | None,
    init_super: bool = True
)

Bases: TopologyAware, Formattable, WireConvertible, MaybePartial, HtmlConvertible

Base for all symbolic types.

Symbolic types are types that provide interfaces for symbolic programming, based on which symbolic objects can be created. In PyGX, there are three categories of symbolic types:

Constructor.

Parameters:

Name Type Description Default
allow_partial bool

Whether to allow required fields to be MISSING_VALUE or partial.

required
accessor_writable bool

Whether to allow write access via attributes. This flag is useful when we want to enforce update of fields using sym_rebind method, which leads to better trackability and batched field update notification.

required
sealed bool

Whether object is sealed that cannot be changed. This flag is useful when we don't want downstream to modify the object.

required
root_path KeyPath | None

KeyPath of current object in its context (object tree).

required
init_super bool

If True, call super.init, otherwise short-circuit. This flag is useful when user want to explicitly implement __init__ for multi-inheritance, which is needed to pass different arguments to different bases. Please see symbolic_test.py#testMultiInheritance for more details.

True
Source code in pygx/symbolic/_base.py
def __init__(
    self,
    *,
    allow_partial: bool,
    accessor_writable: bool,
    sealed: bool,
    root_path: topology.KeyPath | None,
    init_super: bool = True,
):
    """Constructor.

    Args:
      allow_partial: Whether to allow required fields to be MISSING_VALUE or
        partial.
      accessor_writable: Whether to allow write access via attributes. This flag
        is useful when we want to enforce update of fields using `sym_rebind`
        method, which leads to better trackability and batched field update
        notification.
      sealed: Whether object is sealed that cannot be changed. This flag is
        useful when we don't want downstream to modify the object.
      root_path: KeyPath of current object in its context (object tree).
      init_super: If True, call super.__init__, otherwise short-circuit. This
        flag is useful when user want to explicitly implement `__init__` for
        multi-inheritance, which is needed to pass different arguments to
        different bases. Please see `symbolic_test.py#testMultiInheritance`
        for more details.
    """
    # Set all symbolic state in one C-level dict update. `__dict__` here is
    # the *instance* attribute dict — distinct from the underlying dict
    # storage for `pg.Dict` (populated by `dict.__init__`) and from the list
    # storage for `pg.List`. A literal dict beats `dict.update(proxy,
    # **kwargs)` here: the proxy iteration costs more than enumerating the
    # keys inline. This is hot (every Symbolic construction).
    vars(self).update(
        {
            '_allow_partial': allow_partial,
            '_accessor_writable': accessor_writable,
            '_sealed': sealed,
            '_sym_parent': None,
            '_sym_path': root_path
            if root_path is not None
            else _EMPTY_KEY_PATH,
            '_sym_puresymbolic': None,
            '_sym_missing_values': None,
            '_sym_nondefault_values': None,
            '_sym_origin': None,
        }
    )
    if flags.is_tracking_origin():
        vars(self)['_sym_origin'] = Origin(None, '__init__')

    # super.__init__ may enter into next base class's __init__ when
    # multi-inheritance is used. Since we have override `__setattr__` for
    # symbolic.Object, which depends on `_accessor_writable` and so on,
    # we want to call make `__setattr__` ready to call before entering
    # other base's `__init__`.
    if init_super:
        super().__init__()
    else:
        object.__init__(self)

sym_partial property

sym_partial: bool

Returns True if current value is partial.

sym_puresymbolic property

sym_puresymbolic: bool

Returns True if current value is or contains subnodes of PureSymbolic.

sym_abstract property

sym_abstract: bool

Returns True if current value is abstract (partial or pure symbolic).

sym_sealed property

sym_sealed: bool

Returns True if current object is sealed.

sym_field property

sym_field: Field | None

Returns the symbolic field for current object.

sym_root property

sym_root: Symbolic

Returns the root of the symbolic tree.

sym_parent property

sym_parent: Symbolic

Returns the containing symbolic object.

For a sym=False object (which has no symbolic tree, and is held by reference rather than adopted) this is always None.

sym_path property

sym_path: KeyPath

Returns the path of current object from the root of its symbolic tree.

For a sym=False object (which has no symbolic tree) this is always the empty pg.KeyPath().

sym_origin property

sym_origin: Origin | None

Returns the symbolic origin of current object.

allow_partial property

allow_partial: bool

Returns True if partial binding is allowed.

accessor_writable property

accessor_writable: bool

Returns True if mutation can be made by attribute assignment.

is_deterministic property

is_deterministic: bool

Returns if current object is deterministic.

sym_seal

sym_seal(is_seal: bool = True) -> Self

Seals or unseals current object from further modification.

Source code in pygx/symbolic/_base.py
def sym_seal(self, is_seal: bool = True) -> Self:
    """Seals or unseals current object from further modification."""
    vars(self)['_sealed'] = is_seal
    return self

sym_missing abstractmethod

sym_missing(flatten: bool = True) -> dict[str | int, Any]

Returns missing values.

The public override point (there is no protected _sym_missing hook): concrete subclasses (Object / Dict / List / Functor) override this directly and route their computation through self._sym_cached('_sym_missing_values', compute, flatten). A user subclass overrides it too and may call super().sym_missing() to get the (cached) parent result.

Parameters:

Name Type Description Default
flatten bool

If True, flatten nested structures into a key-path dict.

True
Source code in pygx/symbolic/_base.py
@abc.abstractmethod
def sym_missing(self, flatten: bool = True) -> dict[str | int, Any]:
    """Returns missing values.

    The public override point (there is no protected ``_sym_missing`` hook):
    concrete subclasses (``Object`` / ``Dict`` / ``List`` / ``Functor``)
    override this directly and route their computation through
    ``self._sym_cached('_sym_missing_values', compute, flatten)``. A user
    subclass overrides it too and may call ``super().sym_missing()`` to get
    the (cached) parent result.

    Args:
      flatten: If True, flatten nested structures into a key-path dict.
    """

sym_nondefault abstractmethod

sym_nondefault(flatten: bool = True) -> dict[int | str, Any]

Returns non-default values.

The public override point (there is no protected _sym_nondefault hook); see sym_missing for the override / caching contract.

Parameters:

Name Type Description Default
flatten bool

If True, flatten nested structures into a key-path dict.

True
Source code in pygx/symbolic/_base.py
@abc.abstractmethod
def sym_nondefault(self, flatten: bool = True) -> dict[int | str, Any]:
    """Returns non-default values.

    The public override point (there is no protected ``_sym_nondefault``
    hook); see ``sym_missing`` for the override / caching contract.

    Args:
      flatten: If True, flatten nested structures into a key-path dict.
    """

sym_ancestor

sym_ancestor() -> Optional[Symbolic]
sym_ancestor(where: type[_T]) -> Optional[_T]
sym_ancestor(where: Callable[[Any], bool]) -> Optional[Symbolic]
sym_ancestor(where: type[_T] | Callable[[Any], bool] | None = None) -> Any

Returns the nearest ancestor matching where.

Parameters:

Name Type Description Default
where type[_T] | Callable[[Any], bool] | None

One of: - None (default): returns the immediate parent. - A class: returns the nearest ancestor that is an instance of the class. Static type-checkers narrow the result to that class. - A callable: returns the nearest ancestor for which the callable returns True.

None
Source code in pygx/symbolic/_base.py
def sym_ancestor(
    self,
    where: type[_T] | Callable[[Any], bool] | None = None,
) -> Any:
    """Returns the nearest ancestor matching `where`.

    Args:
      where: One of:
        - None (default): returns the immediate parent.
        - A class: returns the nearest ancestor that is an instance of the
          class. Static type-checkers narrow the result to that class.
        - A callable: returns the nearest ancestor for which the callable
          returns True.
    """
    ancestor = self.sym_parent
    if where is None:
        # No filter: nearest ancestor is the immediate parent.
        return ancestor
    if isinstance(where, type):
        cls = where
        while ancestor is not None and not isinstance(ancestor, cls):
            ancestor = ancestor.sym_parent
        return ancestor
    while ancestor is not None and not where(ancestor):
        ancestor = ancestor.sym_parent
    return ancestor

sym_descendants

sym_descendants(
    where: None = None,
    option: DescendantQueryOption = ...,
    include_self: bool = ...,
) -> list[Any]
sym_descendants(
    where: type[_T],
    option: DescendantQueryOption = ...,
    include_self: bool = ...,
) -> list[_T]
sym_descendants(
    where: Callable[[Any], bool],
    option: DescendantQueryOption = ...,
    include_self: bool = ...,
) -> list[Any]
sym_descendants(
    where: type[_T] | Callable[[Any], bool] | None = None,
    option: DescendantQueryOption = ALL,
    include_self: bool = False,
) -> list[Any]

Returns all descendants matching where.

Parameters:

Name Type Description Default
where type[_T] | Callable[[Any], bool] | None

One of: - None (default): all descendants are returned. - A class: descendants that are instances of the class are returned. Static type-checkers narrow the result to list[cls]. - A callable: descendants for which the callable returns True are returned.

None
option DescendantQueryOption

Descendant query options, indicating whether all matched, immediate matched or only the matched leaf nodes will be returned.

ALL
include_self bool

If True, self will be included in the query, otherwise only strict descendants are included.

False

Returns:

Type Description
list[Any]

A list of objects that match where.

Source code in pygx/symbolic/_base.py
def sym_descendants(
    self,
    where: type[_T] | Callable[[Any], bool] | None = None,
    option: DescendantQueryOption = DescendantQueryOption.ALL,
    include_self: bool = False,
) -> list[Any]:
    """Returns all descendants matching `where`.

    Args:
      where: One of:
        - None (default): all descendants are returned.
        - A class: descendants that are instances of the class are returned.
          Static type-checkers narrow the result to `list[cls]`.
        - A callable: descendants for which the callable returns True are
          returned.
      option: Descendant query options, indicating whether all matched,
        immediate matched or only the matched leaf nodes will be returned.
      include_self: If True, `self` will be included in the query, otherwise
        only strict descendants are included.

    Returns:
      A list of objects that match `where`.
    """
    if isinstance(where, type):
        cls = where
        where = lambda x: isinstance(x, cls)
    # Direct recursive walk via `_collect_descendants` — skips `traverse`'s
    # per-node `KeyPath` allocation and visitor-fn dispatch.
    descendants: list[Any] = []
    if include_self and (where is None or where(self)):
        if option is DescendantQueryOption.IMMEDIATE:
            return [self]
        # ALL or LEAF: collect descendants first, then decide if `self` is
        # included (LEAF excludes `self` when it has matched descendants).
        _collect_descendants(self, descendants, where, option)
        if option is DescendantQueryOption.ALL or not descendants:
            descendants.insert(0, self)
        return descendants
    _collect_descendants(self, descendants, where, option)
    return descendants

sym_attr_field abstractmethod

sym_attr_field(key: str | int) -> Field | None

Returns the field definition for a symbolic attribute.

Source code in pygx/symbolic/_base.py
@abc.abstractmethod
def sym_attr_field(self, key: str | int) -> pg_typing.Field | None:
    """Returns the field definition for a symbolic attribute."""

sym_has

sym_has(path: KeyPath | str | int) -> bool

Returns True if a path exists in the sub-tree.

Parameters:

Name Type Description Default
path KeyPath | str | int

A KeyPath object or equivalence.

required

Returns:

Type Description
bool

True if the path exists in current sub-tree, otherwise False.

Source code in pygx/symbolic/_base.py
def sym_has(self, path: topology.KeyPath | str | int) -> bool:
    """Returns True if a path exists in the sub-tree.

    Args:
      path: A KeyPath object or equivalence.

    Returns:
      True if the path exists in current sub-tree, otherwise False.
    """
    # Depth-1 simple keys go directly to `sym_hasattr` — no `KeyPath` build,
    # no `_query` recursion.
    if _is_simple_path_key(path):
        return self.sym_hasattr(path)
    return topology.KeyPath.from_value(path).exists(self)

sym_get

sym_get(
    path: KeyPath | str | int,
    default: Any = RAISE_IF_NOT_FOUND,
    use_inferred: bool = False,
) -> Any

Returns a sub-node by path.

NOTE: there is no sym_set, use sym_rebind.

Parameters:

Name Type Description Default
path KeyPath | str | int

A KeyPath object or equivalence.

required
default Any

Default value if path does not exists. If absent, KeyError will be thrown.

RAISE_IF_NOT_FOUND
use_inferred bool

If True, return inferred value instead of the symbolic form of pg.Inferential objects.

False

Returns:

Type Description
Any

Value of symbolic attribute specified by path if found, otherwise the

Any

default value if it's specified.

Raises:

Type Description
KeyError

If path does not exist and default is not specified.

Source code in pygx/symbolic/_base.py
def sym_get(
    self,
    path: topology.KeyPath | str | int,
    default: Any = RAISE_IF_NOT_FOUND,
    use_inferred: bool = False,
) -> Any:
    """Returns a sub-node by path.

    NOTE: there is no `sym_set`, use `sym_rebind`.

    Args:
      path: A KeyPath object or equivalence.
      default: Default value if path does not exists. If absent, `KeyError` will
        be thrown.
      use_inferred: If True, return inferred value instead of the symbolic form
        of `pg.Inferential` objects.

    Returns:
      Value of symbolic attribute specified by path if found, otherwise the
      default value if it's specified.

    Raises:
      KeyError: If `path` does not exist and `default` is not specified.
    """
    # Depth-1 simple keys can route directly to `sym_getattr` / `sym_inferred`
    # without building a `KeyPath` and walking `_query`.
    if _is_simple_path_key(path):
        if not self.sym_hasattr(path):
            if default is RAISE_IF_NOT_FOUND:
                # Match the message format `KeyPath._query` raises.
                raise KeyError(
                    f'Path {topology.KeyPath(path)!r} does not exist: '
                    f'key {path!r} is absent from innermost value {self!r}.'
                )
            return default
        return (
            self.sym_inferred(path)
            if use_inferred
            else self._sym_getattr(path)
        )
    path = topology.KeyPath.from_value(path)
    if default is RAISE_IF_NOT_FOUND:
        return path.query(self, use_inferred=use_inferred)
    return path.get(self, default, use_inferred=use_inferred)

sym_hasattr abstractmethod

sym_hasattr(key: str | int) -> bool

Returns if a symbolic attribute exists.

Source code in pygx/symbolic/_base.py
@abc.abstractmethod
def sym_hasattr(self, key: str | int) -> bool:
    """Returns if a symbolic attribute exists."""

sym_getattr

sym_getattr(key: str | int, default: Any = RAISE_IF_NOT_FOUND) -> Any

Gets a symbolic attribute.

Parameters:

Name Type Description Default
key str | int

Key of symbolic attribute.

required
default Any

Default value if attribute does not exist. If absent,

RAISE_IF_NOT_FOUND

Returns:

Type Description
Any

Value of symbolic attribute if found, otherwise the default value

Any

if it's specified.

Raises:

Type Description
AttributeError

If key does not exist and default is not provided.

Source code in pygx/symbolic/_base.py
def sym_getattr(
    self, key: str | int, default: Any = RAISE_IF_NOT_FOUND
) -> Any:
    """Gets a symbolic attribute.

    Args:
      key: Key of symbolic attribute.
      default: Default value if attribute does not exist. If absent,

    Returns:
      Value of symbolic attribute if found, otherwise the default value
      if it's specified.

    Raises:
      AttributeError: If `key` does not exist and `default` is not provided.
    """
    if not self.sym_hasattr(key):
        if default is RAISE_IF_NOT_FOUND:
            raise AttributeError(
                self._error_message(
                    f'{self.__class__!r} object has no symbolic attribute {key!r}.'
                )
            )
        return default
    return self._sym_getattr(key)

sym_inferrable

sym_inferrable(key: str | int, **kwargs) -> bool

Returns True if the attribute under key can be inferred.

Source code in pygx/symbolic/_base.py
def sym_inferrable(self, key: str | int, **kwargs) -> bool:
    """Returns True if the attribute under key can be inferred."""
    # Only wrap `infer()` in try/except — the common no-Inferential case
    # doesn't need it (`sym_inferred` would otherwise wrap unconditionally).
    missing = pg_typing.MISSING_VALUE
    if not self.sym_hasattr(key):
        return False
    v = self.sym_getattr(key)
    if v == missing:
        return False
    if isinstance(v, Inferential):
        try:
            v = v.infer(**kwargs)
        except Exception:  # pylint: disable=broad-exception-caught
            return False
        return v != missing
    return True

sym_inferred

sym_inferred(
    key: str | int, default: Any = RAISE_IF_NOT_FOUND, **kwargs
) -> Any

Returns the inferred value of the attribute under key.

The public override point (there is no protected _sym_inferred hook): a subclass overrides this directly and calls super().sym_inferred(...) for the base infer-unwrap (read the attribute; if it is Inferential, return infer(**kwargs)). The default / RAISE_IF_NOT_FOUND fallback — return default instead of propagating an inference error — is applied here, so a subclass override that defers to super() for the not-found path inherits it.

Source code in pygx/symbolic/_base.py
def sym_inferred(
    self,
    key: str | int,
    default: Any = RAISE_IF_NOT_FOUND,
    **kwargs,
) -> Any:
    """Returns the inferred value of the attribute under key.

    The public override point (there is no protected ``_sym_inferred`` hook):
    a subclass overrides this directly and calls ``super().sym_inferred(...)``
    for the base infer-unwrap (read the attribute; if it is ``Inferential``,
    return ``infer(**kwargs)``). The ``default`` / ``RAISE_IF_NOT_FOUND``
    fallback — return ``default`` instead of propagating an inference error —
    is applied here, so a subclass override that defers to ``super()`` for the
    not-found path inherits it.
    """
    try:
        v = self.sym_getattr(key)
        if isinstance(v, Inferential):
            return v.infer(**kwargs)
        return v
    except Exception:  # pylint: disable=broad-exception-caught
        if default is RAISE_IF_NOT_FOUND:
            raise
        return default

sym_keys abstractmethod

sym_keys() -> Iterator[str | int]

Iterates the keys of symbolic attributes.

Source code in pygx/symbolic/_base.py
@abc.abstractmethod
def sym_keys(self) -> Iterator[str | int]:
    """Iterates the keys of symbolic attributes."""

sym_values abstractmethod

sym_values() -> Iterator[Any]

Iterates the values of symbolic attributes.

Source code in pygx/symbolic/_base.py
@abc.abstractmethod
def sym_values(self) -> Iterator[Any]:
    """Iterates the values of symbolic attributes."""

sym_items abstractmethod

sym_items() -> Iterator[tuple[str | int, Any]]

Iterates the (key, value) pairs of symbolic attributes.

Source code in pygx/symbolic/_base.py
@abc.abstractmethod
def sym_items(self) -> Iterator[tuple[str | int, Any]]:
    """Iterates the (key, value) pairs of symbolic attributes."""

sym_setparent

sym_setparent(parent: Optional[TopologyAware])

Sets the parent of current node in the symbolic tree.

Source code in pygx/symbolic/_base.py
def sym_setparent(self, parent: Optional['TopologyAware']):
    """Sets the parent of current node in the symbolic tree."""
    vars(self)['_sym_parent'] = parent

sym_contains

sym_contains(
    value: Any = None, type: None | type[Any] | tuple[type[Any], ...] = None
) -> bool

Returns True if the object contains sub-nodes of given value or type.

Source code in pygx/symbolic/_base.py
def sym_contains(
    self,
    value: Any = None,
    type: None | type[Any] | tuple[type[Any], ...] = None,  # pylint: disable=redefined-builtin
) -> bool:
    """Returns True if the object contains sub-nodes of given value or type."""
    # Direct recursive walk avoids `traverse`'s per-node `KeyPath`
    # allocations + visitor-fn dispatch when we only need to test membership.
    if type is not None:
        return _walk_contains_type(self, type)
    return _walk_contains_value(self, value)

sym_setpath

sym_setpath(path: str | KeyPath | None) -> None

Sets the path of current node in its symbolic tree.

Source code in pygx/symbolic/_base.py
def sym_setpath(self, path: str | topology.KeyPath | None) -> None:
    """Sets the path of current node in its symbolic tree."""
    # Cache `sym_path` once — called on every Object construction.
    old_path = self.sym_path
    if old_path != path:
        vars(self)['_sym_path'] = path
        self._update_children_paths(old_path, path)

sym_rebind

sym_rebind(
    path_value_pairs: None | (Mapping[Any, Any] | Callable[..., Any]) = None,
    *,
    raise_on_no_change: bool = True,
    notify_parents: bool = True,
    skip_notification: bool | None = None,
    **kwargs: Any
) -> Self

Mutates the sub-nodes of current object.

sym_rebind is the recommended way for mutating symbolic objects in PyGX:

  • It allows mutations not only on immediate child nodes, but on the entire sub-tree.
  • It allows mutations by rules via passing a callable object as the value for path_value_pairs.
  • It batches the updates from multiple sub-nodes, which triggers the on_sym_change or on_sym_bound event once for recomputing the parent object's internal states.
  • It respects the "sealed" flag of the object or the pg.as_sealed context manager to trigger permission error.

Example::

# # Rebind on pg.Object subclasses. #

class A(pg.Object): x: pg.typing.Dict([ ('y', pg.typing.Int(default=0)) ]) z: int = 1

a = A() # Rebind using path-value pairs. a.sym_rebind({ 'x.y': 1, 'z': 0 })

# Rebind using **kwargs. a.sym_rebind(x={y: 1}, z=0)

# Rebind using rebinders. # Rebind based on path. a.sym_rebind(lambda k, v: 1 if k == 'x.y' else v) # Rebind based on key. a.sym_rebind(lambda k, v: 1 if k and k.key == 'y' else v) # Rebind based on value. a.sym_rebind(lambda k, v: 0 if v == 1 else v) # Rebind baesd on value and parent. a.sym_rebind(lambda k, v, p: (0 if isinstance(p, A) and isinstance(v, int) else v))

# Rebind on pg.Dict. # d = pg.Dict(value_spec=pg.typing.Dict([ ('a', pg.typing.Dict([ ('b', pg.typing.Int()), ])), ('c', pg.typing.Float()) ])

# Rebind using **kwargs. d.sym_rebind(a={b: 1}, c=1.0)

# Rebind using key path to value dict. d.sym_rebind({ 'a.b': 2, 'c': 2.0 })

# NOT OKAY: **kwargs and dict/rebinder cannot be used at the same time. d.sym_rebind({'a.b': 2}, c=2)

# Rebind with rebinder by path (on subtree). d.sym_rebind(lambda k, v: 1 if k.key == 'b' else v)

# Rebind with rebinder by value (on subtree). d.sym_rebind(lambda k, v: 0 if isinstance(v, int) else v)

# # Rebind on pg.List. # l = pg.List([{ 'a': 'foo', 'b': 0, } ], value_spec = pg.typing.List(pg.typing.Dict([ ('a', pg.typing.Str()), ('b', pg.typing.Int()) ]), max_size=10))

# Rebind using integer as list index: update semantics on list[0]. l.sym_rebind({ 0: { 'a': 'bar', 'b': 1 } })

# Rebind: trigger append semantics when index is larger than list length. l.sym_rebind({ 999: { 'a': 'fun', 'b': 2 } })

# Rebind using key path. l.sym_rebind({ '[0].a': 'bar2' '[1].b': 3 })

# Rebind using function (rebinder). # Change all integers to 0 in sub-tree. l.sym_rebind(lambda k, v: v if not isinstance(v, int) else 0)

Parameters:

Name Type Description Default
path_value_pairs None | (Mapping[Any, Any] | Callable[..., Any])

A dictionary of key/or key path to new field value, or a function that generate updates based on the key path, value and parent of each node under current object. We use terminology 'rebinder' for this type of functions. The signature of a rebinder is:

`(key_path: pg.KeyPath, value: Any)` or
`(key_path: pg.KeyPath, value: Any, parent: pg.Symbolic)`
None
raise_on_no_change bool

If True, raises ValueError when there are no values to change. This is useful when rebinder is used, which may or may not generate any updates.

True
notify_parents bool

If True (default), parents will be notified upon change. Otherwisee only the current object and the impacted children will be notified. A most common use case for setting this flag to False is when users want to rebind a child within the parent on_sym_bound method.

True
skip_notification bool | None

If True, there will be no on_sym_change event triggered from current sym_rebind. If None, the default value will be inferred from the pygx.notify_on_change context manager. Use it only when you are certain that current rebind does not invalidate internal states of its object tree.

None
**kwargs Any

For pg.Dict and pg.Object subclasses, user can use keyword arguments (in format of <field_name>=<field_value>) to directly modify immediate child nodes.

{}

Returns:

Type Description
Self

Self.

Raises:

Type Description
WritePermissionError

If object is sealed.

KeyError

If update location specified by key or key path is not aligned with the schema of the object tree.

TypeError

If updated field value type does not conform to field spec.

ValueError

If updated field value is not acceptable according to field spec, or nothing is updated and raise_on_no_change is set to True.

SymbolicModeError

If the object was declared with sym=False and the rebind needs the tree — a rebinder callable, or a path key that descends below the immediate fields. (A dotted key smuggled through **kwargs — CPython allows non-identifier strings via ** — takes the always-simple kwargs fast path instead and surfaces as the schema's KeyError, in both sym modes.)

Note

On a sym=False (the default) object, sym_rebind is the batch mutation surface for the IMMEDIATE fields: plain field-name keys (dict or **kwargs) are validated and applied through the same write path as attribute assignment, with one batched on_sym_change on the object itself (there is no tree to propagate through). Path keys ('a.b', 'x[0]') and rebinder callables need the symbolic tree and raise SymbolicModeError. Sealing follows frozen alone — attr_write gates only the dotted surface.

Source code in pygx/symbolic/_base.py
def sym_rebind(
    self,
    # `Mapping[Any, Any]` rather than the narrower `Mapping[KeyPath | str |
    # int, Any]` because Mapping (and dict) are invariant in K, so the
    # narrow form rejects ``dict[KeyPath, Any]``. The runtime check below
    # enforces the actual constraint that keys are ``KeyPath | str | int``.
    path_value_pairs: None
    | (Mapping[Any, Any] | Callable[..., Any]) = None,  # pylint: disable=g-bare-generic
    *,
    raise_on_no_change: bool = True,
    notify_parents: bool = True,
    skip_notification: bool | None = None,
    **kwargs: Any,
) -> Self:
    """Mutates the sub-nodes of current object.

    `sym_rebind` is the recommended way for mutating symbolic objects in
    PyGX:

      * It allows mutations not only on immediate child nodes, but on the
        entire sub-tree.
      * It allows mutations by rules via passing a callable object as the
        value for `path_value_pairs`.
      * It batches the updates from multiple sub-nodes, which triggers the
        `on_sym_change` or `on_sym_bound` event once for recomputing the parent
        object's internal states.
      * It respects the "sealed" flag of the object or the `pg.as_sealed`
        context manager to trigger permission error.

    Example::

      #
      # Rebind on pg.Object subclasses.
      #

      class A(pg.Object):
        x: pg.typing.Dict([
            ('y', pg.typing.Int(default=0))
        ])
        z: int = 1

      a = A()
      # Rebind using path-value pairs.
      a.sym_rebind({
        'x.y': 1,
        'z': 0
      })

      # Rebind using **kwargs.
      a.sym_rebind(x={y: 1}, z=0)

      # Rebind using rebinders.
      # Rebind based on path.
      a.sym_rebind(lambda k, v: 1 if k == 'x.y' else v)
      # Rebind based on key.
      a.sym_rebind(lambda k, v: 1 if k and k.key == 'y' else v)
      # Rebind based on value.
      a.sym_rebind(lambda k, v: 0 if v == 1 else v)
      # Rebind baesd on value and parent.
      a.sym_rebind(lambda k, v, p: (0 if isinstance(p, A) and isinstance(v, int)
                                else v))

      # Rebind on pg.Dict.
      #
      d = pg.Dict(value_spec=pg.typing.Dict([
        ('a', pg.typing.Dict([
          ('b', pg.typing.Int()),
        ])),
        ('c', pg.typing.Float())
      ])

      # Rebind using **kwargs.
      d.sym_rebind(a={b: 1}, c=1.0)

      # Rebind using key path to value dict.
      d.sym_rebind({
        'a.b': 2,
        'c': 2.0
      })

      # NOT OKAY: **kwargs and dict/rebinder cannot be used at the same time.
      d.sym_rebind({'a.b': 2}, c=2)

      # Rebind with rebinder by path (on subtree).
      d.sym_rebind(lambda k, v: 1 if k.key == 'b' else v)

      # Rebind with rebinder by value (on subtree).
      d.sym_rebind(lambda k, v: 0 if isinstance(v, int) else v)

      #
      # Rebind on pg.List.
      #
      l = pg.List([{
            'a': 'foo',
            'b': 0,
          }
        ],
        value_spec = pg.typing.List(pg.typing.Dict([
            ('a', pg.typing.Str()),
            ('b', pg.typing.Int())
        ]), max_size=10))

      # Rebind using integer as list index: update semantics on list[0].
      l.sym_rebind({
        0: {
          'a': 'bar',
          'b': 1
        }
      })

      # Rebind: trigger append semantics when index is larger than list length.
      l.sym_rebind({
        999: {
          'a': 'fun',
          'b': 2
        }
      })

      # Rebind using key path.
      l.sym_rebind({
        '[0].a': 'bar2'
        '[1].b': 3
      })

      # Rebind using function (rebinder).
      # Change all integers to 0 in sub-tree.
      l.sym_rebind(lambda k, v: v if not isinstance(v, int) else 0)

    Args:
      path_value_pairs: A dictionary of key/or key path to new field value, or
        a function that generate updates based on the key path, value and
        parent of each node under current object. We use terminology 'rebinder'
        for this type of functions. The signature of a rebinder is:

            `(key_path: pg.KeyPath, value: Any)` or
            `(key_path: pg.KeyPath, value: Any, parent: pg.Symbolic)`

      raise_on_no_change: If True, raises ``ValueError`` when there are no
        values to change. This is useful when rebinder is used, which may or
        may not generate any updates.
      notify_parents: If True (default), parents will be notified upon change.
        Otherwisee only the current object and the impacted children will
        be notified. A most common use case for setting this flag to False
        is when users want to rebind a child within the parent `on_sym_bound`
        method.
      skip_notification: If True, there will be no ``on_sym_change`` event
        triggered from current `sym_rebind`. If None, the default value will be
        inferred from the [`pygx.notify_on_change`][pygx.symbolic.notify_on_change] context manager.
        Use it only when you are certain that current rebind does not
        invalidate internal states of its object tree.
      **kwargs: For ``pg.Dict`` and ``pg.Object`` subclasses, user can use
        keyword arguments (in format of `<field_name>=<field_value>`) to
        directly modify immediate child nodes.

    Returns:
      Self.

    Raises:
      WritePermissionError: If object is sealed.
      KeyError: If update location specified by key or key path is not aligned
        with the schema of the object tree.
      TypeError: If updated field value type does not conform to field spec.
      ValueError: If updated field value is not acceptable according to field
        spec, or nothing is updated and `raise_on_no_change` is set to
        True.
      SymbolicModeError: If the object was declared with ``sym=False`` and
        the rebind needs the tree — a rebinder callable, or a path key
        that descends below the immediate fields. (A dotted key smuggled
        through ``**kwargs`` — CPython allows non-identifier strings via
        ``**`` — takes the always-simple kwargs fast path instead and
        surfaces as the schema's ``KeyError``, in both ``sym`` modes.)

    Note:
      On a ``sym=False`` (the default) object, ``sym_rebind`` is the
      batch mutation surface for the IMMEDIATE fields: plain field-name
      keys (dict or ``**kwargs``) are validated and applied through the
      same write path as attribute assignment, with one batched
      ``on_sym_change`` on the object itself (there is no tree to
      propagate through). Path keys (``'a.b'``, ``'x[0]'``) and rebinder
      callables need the symbolic tree and raise ``SymbolicModeError``.
      Sealing follows ``frozen`` alone — ``attr_write`` gates only the
      dotted surface.
    """
    flat = not self.sym_mode
    if flat and callable(path_value_pairs):
        raise SymbolicModeError(
            f'`sym_rebind` with a rebinder callable requires a symbolic '
            f'tree, but {self.__class__.__name__} was declared with '
            f'`sym=False`. Pass field-name keys (or **kwargs) to mutate '
            f'the immediate fields, or declare the class with `sym=True`.'
        )
    # Hot path: pure-kwargs call (`rebind(x=1, ...)`). kwargs is a fresh
    # dict and its keys are Python identifiers (always simple), so we skip
    # detection and dispatch straight to the `simple` apply fast path.
    if path_value_pairs is None:
        if kwargs:
            updates = self._sym_rebind(kwargs, simple=True)
        elif raise_on_no_change:
            raise ValueError(
                self._error_message('There are no values to rebind.')
            )
        else:
            updates = []
    else:
        resolved: dict[Any, Any]
        if callable(path_value_pairs):
            resolved = get_rebind_dict(path_value_pairs, self)
        elif Symbolic.DictType is not None and isinstance(  # pylint: disable=isinstance-second-argument-not-valid-type
            path_value_pairs, Symbolic.DictType
        ):
            # Iterate symbolic items so `pg.Inferential` values aren't evaluated.
            sd = typing.cast(Any, path_value_pairs)
            resolved = {k: v for k, v in sd.sym_items()}
        elif isinstance(path_value_pairs, dict):
            resolved = path_value_pairs
        elif isinstance(path_value_pairs, Mapping):
            # Signature accepts any Mapping (covariant in K) but downstream
            # code does `.update(kwargs)` on this value, so a non-dict
            # Mapping must be copied into a dict first.
            resolved = dict(path_value_pairs)
        else:
            raise ValueError(
                self._error_message(
                    f"Argument 'path_value_pairs' should be a dict. "
                    f'Encountered {path_value_pairs}'
                )
            )
        if kwargs:
            resolved.update(kwargs)
        if not resolved:
            if raise_on_no_change:
                raise ValueError(
                    self._error_message('There are no values to rebind.')
                )
            updates = []
        else:
            # If every key addresses a depth-1 field (per `_is_simple_path_key`),
            # skip `KeyPath` construction and use the `simple` apply fast path.
            # Inlined for the hot path — call overhead matters on small dicts.
            simple = True
            for k in resolved:
                if isinstance(k, str):
                    if not k or '.' in k or '[' in k or ']' in k:
                        simple = False
                        break
                elif not isinstance(k, int):
                    simple = False
                    break
            if simple:
                updates = self._sym_rebind(resolved, simple=True)
            elif flat:
                # A flat object's rebind surface is its IMMEDIATE fields
                # only — a path key descends below the flat boundary,
                # which is exactly the write crossing #547's guard
                # rejects. Refuse here, at the surface, with the mode
                # error naming the offending key. `_is_simple_path_key`
                # is the ONE notion of "depth-1 key", shared with
                # `sym_clone(override=)`'s flat gate (int keys are
                # depth-1: alone they'd take the simple path and get the
                # schema's own KeyError).
                offending = next(
                    k for k in resolved if not _is_simple_path_key(k)
                )
                raise SymbolicModeError(
                    f'`sym_rebind` with a path key ({offending!r}) '
                    f'requires a symbolic tree, but '
                    f'{self.__class__.__name__} was declared with '
                    f'`sym=False`: a flat object rebinds its immediate '
                    f'fields only. Use plain field-name keys, or declare '
                    f'the class with `sym=True`.'
                )
            else:
                # Mixed/nested paths: build `KeyPath` per key. `from_value` itself
                # skips `parse` for the simple keys in this dict.
                from_value = topology.KeyPath.from_value
                updates = self._sym_rebind(
                    {from_value(k): v for k, v in resolved.items()}
                )
    if skip_notification is None:
        skip_notification = not flags.is_change_notification_enabled()
    # Always called: model validation must run even when notification
    # is skipped (the `notify` flag gates only the observer half).
    self._notify_field_updates(
        updates,
        notify_parents=notify_parents,
        notify=not skip_notification,
    )
    return self

sym_jsonify abstractmethod

sym_jsonify(*, exclude_defaults: bool = False, **kwargs) -> WireValue

Converts representation of current object to a plain Python object.

Source code in pygx/symbolic/_base.py
@abc.abstractmethod
def sym_jsonify(
    self, *, exclude_defaults: bool = False, **kwargs
) -> wire.WireValue:
    """Converts representation of current object to a plain Python object."""

sym_ne

sym_ne(other: Any) -> bool

Returns if this object does not equal to another object symbolically.

Source code in pygx/symbolic/_base.py
def sym_ne(self, other: Any) -> bool:
    """Returns if this object does not equal to another object symbolically."""
    return ne(self, other)

sym_eq

sym_eq(other: Any) -> bool

Returns if this object equals to another object symbolically.

Source code in pygx/symbolic/_base.py
def sym_eq(self, other: Any) -> bool:
    """Returns if this object equals to another object symbolically."""
    return eq(self, other)

sym_gt

sym_gt(other: Any) -> bool

Returns if this object is symbolically greater than another object.

Source code in pygx/symbolic/_base.py
def sym_gt(self, other: Any) -> bool:
    """Returns if this object is symbolically greater than another object."""
    return gt(self, other)

sym_lt

sym_lt(other: Any) -> bool

Returns True if this object is symbolically less than other object.

Source code in pygx/symbolic/_base.py
def sym_lt(self, other: Any) -> bool:
    """Returns True if this object is symbolically less than other object."""
    return lt(self, other)

sym_hash abstractmethod

sym_hash() -> int

Computes the symbolic hash of current object.

Source code in pygx/symbolic/_base.py
@abc.abstractmethod
def sym_hash(self) -> int:
    """Computes the symbolic hash of current object."""

sym_setorigin

sym_setorigin(
    source: Any,
    tag: str,
    stacktrace: bool | None = None,
    stacklimit: int | None = None,
    stacktop: int = -1,
)

Sets the symbolic origin of current object.

Parameters:

Name Type Description Default
source Any

Source value for current object.

required
tag str

A descriptive tag of the origin. Built-in tags are: __init__, clone, deepclone, return. Users can manually call sym_setorigin with custom tag value.

required
stacktrace bool | None

If True, enable stack trace for the origin. If None, enable stack trace if pg.tracek_origin() is called. Otherwise stack trace is disabled.

None
stacklimit int | None

An optional integer to limit the stack depth. If None, it's determined by the value passed to pg.set_origin_stacktrace_limit, which is 10 by default.

None
stacktop int

A negative or zero-value integer indicating the stack top among the stack frames that we want to present to user, by default it's 1-level up from the stack within current sym_setorigin call.

-1

Example::

def foo(): return bar()

def bar(): s = MyObject() t = s.build() t.sym_setorigin(s, 'builder', stacktrace=True, stacklimit=5, stacktop=-1)

This example sets the origin of t using s as its source with tag 'builder'. We also record the callstack where the sym_setorigin is called, so users can call t.sym_origin.stacktrace to get the call stack later. The stacktop -1 indicates that we do not need the stack frame within sym_setorigin, so users will see the stack top within the function bar. We also set the max number of stack frames to display to 5, not including the stack frame inside sym_setorigin.

Source code in pygx/symbolic/_base.py
def sym_setorigin(
    self,
    source: Any,
    tag: str,
    stacktrace: bool | None = None,
    stacklimit: int | None = None,
    stacktop: int = -1,
):
    """Sets the symbolic origin of current object.

    Args:
      source: Source value for current object.
      tag: A descriptive tag of the origin. Built-in tags are:
        `__init__`, `clone`, `deepclone`, `return`. Users can manually
        call `sym_setorigin` with custom tag value.
      stacktrace: If True, enable stack trace for the origin. If None, enable
        stack trace if `pg.tracek_origin()` is called. Otherwise stack trace is
        disabled.
      stacklimit: An optional integer to limit the stack depth. If None, it's
        determined by the value passed to `pg.set_origin_stacktrace_limit`,
        which is 10 by default.
      stacktop: A negative or zero-value integer indicating the stack top among
        the stack frames that we want to present to user, by default it's
        1-level up from the stack within current `sym_setorigin` call.

    Example::

      def foo():
        return bar()

      def bar():
        s = MyObject()
        t = s.build()
        t.sym_setorigin(s, 'builder',
            stacktrace=True, stacklimit=5, stacktop=-1)

    This example sets the origin of `t` using `s` as its source with tag
    'builder'. We also record the callstack where the `sym_setorigin` is
    called, so users can call `t.sym_origin.stacktrace` to get the call stack
    later. The `stacktop` -1 indicates that we do not need the stack frame
    within ``sym_setorigin``, so users will see the stack top within the
    function `bar`. We also set the max number of stack frames to display to 5,
    not including the stack frame inside ``sym_setorigin``.
    """
    if self.sym_origin is not None:
        current_source = self.sym_origin.source  # pytype: disable=attribute-error  # always-use-property-annotation
        if current_source is not None and current_source is not source:
            raise ValueError(
                f'Cannot set the origin with a different source value. '
                f'Origin source: {current_source!r}, New source: {source!r}.'
            )
    # NOTE(daiyip): We decrement the stacktop by 1 as the physical stack top
    # is within Origin.
    vars(self)['_sym_origin'] = Origin(
        source, tag, stacktrace, stacklimit, stacktop - 1
    )

set_accessor_writable

set_accessor_writable(writable: bool = True) -> Self

Sets accessor writable.

Source code in pygx/symbolic/_base.py
def set_accessor_writable(self, writable: bool = True) -> Self:
    """Sets accessor writable."""
    vars(self)['_accessor_writable'] = writable
    return self

sym_clone

sym_clone(
    deep: bool = False,
    memo: Any | None = None,
    override: Mapping[Any, Any] | None = None,
) -> Self

Clones current object symbolically (the formalized public entry).

Layers override-rebind + origin-tracking on top of the per-subclass _sym_clone core, so copy protocols (__copy__ / __deepcopy__), pg.clone, and child walks compose those exactly once.

Parameters:

Name Type Description Default
deep bool

If True, perform deep copy (equivalent to copy.deepcopy). Otherwise shallow copy (equivalent to copy.copy).

False
memo Any | None

Memo object for deep clone.

None
override Mapping[Any, Any] | None

An optional dict of key path to new values to override cloned value. Under sym=False (#524) only TOP-LEVEL field names are accepted — the dataclasses.replace() analog: the clone is re-constructed with the overridden values (full validation runs); path-based keys ('a.b', 'x[0]', KeyPath) require the tree and raise SymbolicModeError.

None

Returns:

Type Description
Self

A copy of self.

Source code in pygx/symbolic/_base.py
def sym_clone(
    self,
    deep: bool = False,
    memo: Any | None = None,
    override: Mapping[Any, Any] | None = None,
) -> Self:
    """Clones current object symbolically (the formalized public entry).

    Layers ``override``-rebind + origin-tracking on top of the per-subclass
    ``_sym_clone`` core, so copy protocols (``__copy__`` / ``__deepcopy__``),
    ``pg.clone``, and child walks compose those exactly once.

    Args:
      deep: If True, perform deep copy (equivalent to copy.deepcopy). Otherwise
        shallow copy (equivalent to copy.copy).
      memo: Memo object for deep clone.
      override: An optional dict of key path to new values to override cloned
        value. Under ``sym=False`` (#524) only TOP-LEVEL field names are
        accepted — the ``dataclasses.replace()`` analog: the clone is
        re-constructed with the overridden values (full validation runs);
        path-based keys (``'a.b'``, ``'x[0]'``, ``KeyPath``) require the
        tree and raise ``SymbolicModeError``.

    Returns:
      A copy of self.
    """
    assert deep or not memo
    if override and not self.sym_mode:
        # Flat replace analog (#524): construct ONCE from merged init
        # args — no phantom intermediate clone whose hooks would fire
        # with the pre-override values (#539 review M3).
        new_value = self._replace_via_construct(deep, memo, override)
    else:
        new_value = self._sym_clone(deep, memo)
        if override:
            # #566: sealing protects SHARED state, not the clone's own
            # birth — a frozen source yields a sealed clone whose
            # override-rebind would raise, making the
            # `dataclasses.replace()` analog fail exactly where a
            # non-mutating replace is the only way to derive a modified
            # value. Unseal the fresh clone for the application and
            # re-seal after — mirroring the flat replace analog, where
            # overrides land during construction and the seal applies at
            # the end.
            # Root-only by design: the unseal principle covers the fresh
            # clone the caller receives; sealed CHILD nodes keep their
            # own state (a path override into a frozen-class child still
            # raises — file demand against #567's availability law if it
            # arises). The re-seal runs in `finally` so a mid-override
            # validation raise cannot leak an unsealed "frozen" clone to
            # a hook that stashed it.
            was_sealed = new_value.sym_sealed
            if was_sealed:
                new_value.sym_seal(False)
            try:
                new_value.sym_rebind(override, raise_on_no_change=False)
            finally:
                if was_sealed:
                    new_value.sym_seal(True)
    if flags.is_tracking_origin():
        new_value.sym_setorigin(self, 'deepclone' if deep else 'clone')
    return new_value

to_json

to_json(**kwargs) -> WireValue

Alias for sym_jsonify.

Source code in pygx/symbolic/_base.py
def to_json(self, **kwargs) -> wire.WireValue:
    """Alias for `sym_jsonify`."""
    return wire.to_json(self, **kwargs)

to_json_str

to_json_str(json_indent: int | None = None, **kwargs) -> str

Serializes current object into a JSON string.

Source code in pygx/symbolic/_base.py
def to_json_str(self, json_indent: int | None = None, **kwargs) -> str:
    """Serializes current object into a JSON string."""
    return to_json_str(self, json_indent=json_indent, **kwargs)

load classmethod

load(*args: Any, **kwargs: Any) -> Self

Loads an instance of this type using the global load handler.

Source code in pygx/symbolic/_base.py
@classmethod
def load(cls, *args: Any, **kwargs: Any) -> Self:
    """Loads an instance of this type using the global load handler."""
    # Lazy: file IO lives in `_io.py` (which imports this module); the
    # call-time import keeps the two modules order-proof. Cold path.
    from pygx.symbolic import _io  # pylint: disable=import-outside-toplevel

    value = _io.load(*args, **kwargs)
    if not isinstance(value, cls):
        raise TypeError(f'Value is not of type {cls!r}: {value!r}.')
    return value

save

save(*args: Any, **kwargs: Any) -> Any

Saves current object using the global save handler.

Source code in pygx/symbolic/_base.py
def save(self, *args: Any, **kwargs: Any) -> Any:
    """Saves current object using the global save handler."""
    from pygx.symbolic import _io  # pylint: disable=import-outside-toplevel

    return _io.save(self, *args, **kwargs)

inspect

inspect(
    path_regex: str | None = None,
    where: None | (Callable[[Any], bool] | Callable[[Any, Any], bool]) = None,
    custom_selector: None | (
        Callable[[KeyPath, Any], bool] | Callable[[KeyPath, Any, Any], bool]
    ) = None,
    file: IO[str] = stdout,
    **kwargs: Any
) -> None

Inspects current object by printing out selected values.

Example::

class A(pg.Object): x: int = 0 y: str

value = { 'a1': A(x=0, y=0), 'a2': [A(x=1, y=1), A(x=1, y=2)], 'a3': { 'p': A(x=2, y=1), 'q': A(x=2, y=2) } }

# Inspect without constraint, # which is equivalent as print(value.format(exclude_defaults=True)) # Shall print: # { # a1 = A(y=0) # a2 = [ # 0: A(x=1, y=1) # 1: A(x=1, y=2) # a3 = { # p = A(x=2, y=1) # q = A(x=2, y=2) # } # } value.inspect(exclude_defaults=True)

# Inspect by path regex. # Shall print: # {'a3.p': A(x=2, y=1)} value.inspect(r'.*p')

# Inspect by value. # Shall print: # { # 'a3.p.x': 2, # 'a3.q.x': 2, # 'a3.q.y': 2, # } value.inspect(where=lambda v: v==2)

# Inspect by path, value and parent. # Shall print: # { # 'a2[1].y': 2 # } value.inspect( r'.*y', where=lambda v, p: v > 1 and isinstance(p, A) and p.x == 1))

# Inspect by custom_selector. # Shall print: # { # 'a2[0].x': 1, # 'a2[0].y': 1, # 'a3.q.x': 2, # 'a3.q.y': 2 # } value.inspect( custom_selector=lambda k, v, p: ( len(k) == 3 and isinstance(p, A) and p.x == v))

Parameters:

Name Type Description Default
path_regex str | None

Optional regex expression to constrain path.

None
where None | (Callable[[Any], bool] | Callable[[Any, Any], bool])

Optional callable to constrain value and parent when path matches path_regex or path_regex is not provided. The signature is: (value) -> should_select, or (value, parent) -> should_select.

None
custom_selector None | (Callable[[KeyPath, Any], bool] | Callable[[KeyPath, Any, Any], bool])

Optional callable object as custom selector. When custom_selector is provided, path_regex and where must be None. The signature of custom_selector is: (key_path, value) -> should_select or (key_path, value, parent) -> should_select.

None
file IO[str]

Output file stream. This can be any object with a write(str) method.

stdout
**kwargs Any

Wildcard keyword arguments to pass to format.

{}
Source code in pygx/symbolic/_base.py
def inspect(
    self,
    path_regex: str | None = None,
    where: None
    | (Callable[[Any], bool] | Callable[[Any, Any], bool]) = None,
    custom_selector: None
    | (
        Callable[[topology.KeyPath, Any], bool]
        | Callable[[topology.KeyPath, Any, Any], bool]
    ) = None,
    file: typing.IO[str] = sys.stdout,  # pylint: disable=redefined-builtin
    **kwargs: Any,
) -> None:
    """Inspects current object by printing out selected values.

    Example::

      class A(pg.Object):
        x: int = 0
        y: str

      value = {
        'a1': A(x=0, y=0),
        'a2': [A(x=1, y=1), A(x=1, y=2)],
        'a3': {
          'p': A(x=2, y=1),
          'q': A(x=2, y=2)
        }
      }

      # Inspect without constraint,
      # which is equivalent as `print(value.format(exclude_defaults=True))`
      # Shall print:
      # {
      #   a1 = A(y=0)
      #   a2 = [
      #     0: A(x=1, y=1)
      #     1: A(x=1, y=2)
      #   a3 = {
      #     p = A(x=2, y=1)
      #     q = A(x=2, y=2)
      #   }
      # }
      value.inspect(exclude_defaults=True)

      # Inspect by path regex.
      # Shall print:
      # {'a3.p': A(x=2, y=1)}
      value.inspect(r'.*p')

      # Inspect by value.
      # Shall print:
      # {
      #    'a3.p.x': 2,
      #    'a3.q.x': 2,
      #    'a3.q.y': 2,
      # }
      value.inspect(where=lambda v: v==2)

      # Inspect by path, value and parent.
      # Shall print:
      # {
      #    'a2[1].y': 2
      # }
      value.inspect(
        r'.*y', where=lambda v, p: v > 1 and isinstance(p, A) and p.x == 1))

      # Inspect by custom_selector.
      # Shall print:
      # {
      #   'a2[0].x': 1,
      #   'a2[0].y': 1,
      #   'a3.q.x': 2,
      #   'a3.q.y': 2
      # }
      value.inspect(
        custom_selector=lambda k, v, p: (
          len(k) == 3 and isinstance(p, A) and p.x == v))

    Args:
      path_regex: Optional regex expression to constrain path.
      where: Optional callable to constrain value and parent when path matches
        `path_regex` or `path_regex` is not provided. The signature is:
        `(value) -> should_select`, or `(value, parent) -> should_select`.
      custom_selector: Optional callable object as custom selector. When
        `custom_selector` is provided, `path_regex` and `where` must be None.
        The signature of `custom_selector` is:
        `(key_path, value) -> should_select`
        or `(key_path, value, parent) -> should_select`.
      file: Output file stream. This can be any object with a `write(str)`
        method.
      **kwargs: Wildcard keyword arguments to pass to `format`.
    """
    if path_regex is None and where is None and custom_selector is None:
        v = self
    else:
        v = query(self, path_regex, where, False, custom_selector)
    formatting.printv(v, file=file, **kwargs)

on_sym_change abstractmethod

on_sym_change(field_updates: dict[KeyPath, FieldUpdate])

Event that is triggered when field values in the subtree are updated.

This event will be called * On per-field basis when object is modified via attribute. * In batch when multiple fields are modified via sym_rebind.

When a field in an object tree is updated, all ancestors' on_sym_change event will be triggered in order, from the nearest one to furthest one.

Parameters:

Name Type Description Default
field_updates dict[KeyPath, FieldUpdate]

Updates made to the subtree. Key path is relative to current object.

required
Source code in pygx/symbolic/_base.py
@abc.abstractmethod
def on_sym_change(self, field_updates: dict[topology.KeyPath, FieldUpdate]):
    """Event that is triggered when field values in the subtree are updated.

    This event will be called
      * On per-field basis when object is modified via attribute.
      * In batch when multiple fields are modified via `sym_rebind`.

    When a field in an object tree is updated, all ancestors' `on_sym_change` event
    will be triggered in order, from the nearest one to furthest one.

    Args:
      field_updates: Updates made to the subtree. Key path is relative to
        current object.
    """

SymbolicModeError

Bases: Exception

Raised when a tree-dependent operation is used on a sym=False object.

A pg.Object subclass declared with sym=False is a flat, reference-semantics object with no symbolic tree. Operations that require a tree position or tree-propagated mutation — sym_rebind, sym_path, sym_parent, sym_root, sym_ancestor, sym_setparent / sym_setpath, and contextual resolution — raise this error. Declare the class with sym=True to use those features (the default is sym=False — a flat, validated dataclass; #524).

Subclasses Exception directly (not AttributeError) so contextual resolution's error handling cannot silently swallow it.

TraverseAction

Bases: Enum

Enum for the next action after a symbolic node is visited.

See also: pygx.traverse.

WritePermissionError

Bases: Exception

Exception raisen when write access to object fields is not allowed.

ClassWrapper

ClassWrapper(
    *,
    allow_partial: bool = False,
    sealed: bool | None = None,
    root_path: KeyPath | None = None,
    explicit_init: bool = False,
    **kwargs: Any
)

Bases: Object

Base class for symbolic class wrapper.

Please see pygx.wrap for details.

Source code in pygx/symbolic/_object.py
@_filter_typing_traceback_on_raise
def __init__(
    self,
    *,
    allow_partial: bool = False,
    sealed: bool | None = None,
    root_path: topology.KeyPath | None = None,
    explicit_init: bool = False,
    **kwargs: Any,
):
    """Create an Object instance.

    ``pg.Object`` synthesizes a keyword-only ``__init__``. Subclasses
    that need positional arguments must override ``__init__``
    explicitly and translate to keyword arguments before forwarding
    to ``super().__init__(**kwargs)``.

    Args:
      allow_partial: If True, the object can be partial.
      sealed: If True, seal the object from future modification (unless under
        a `pg.as_sealed(False)` context manager). If False, treat the object as
        unsealed. If None, it's determined by `cls.__sym_options__.frozen`.
      root_path: The symbolic path for current object. By default it's None,
        which indicates that newly constructed object does not have a parent.
      explicit_init: Should set to `True` when `__init__` is called via
        `pg.Object.__init__` instead of `super().__init__`.
      **kwargs: key/value arguments that align with the schema. All required
        keys in the schema must be specified, and values should be acceptable
        according to their value spec.

    Raises:
      KeyError: When required key(s) are missing.
      ValueError: When value(s) are not acceptable by their value spec.
    """
    # Placeholder for Google-internal usage instrumentation.

    cls = self.__class__
    # The ever-instantiated latch `apply_schema`'s post-instance guard
    # reads (#432: `update_schema` is only legal before instances exist).
    # Own-`__dict__`, latched on the construction ATTEMPT — mirroring the
    # native core, whose `__native_ctor_meta__` (the equivalent marker) is
    # created on the first instance op. One dict read per construct.
    meta = cls.__sym_meta__
    if not meta.instantiated:
        meta.instantiated = True
    if sealed is None:
        sealed = cls.__sym_options__.frozen

    if not isinstance(allow_partial, bool):
        raise TypeError(
            f"Expect bool type for argument 'allow_partial' in "
            f'symbolic.Object.__init__ but encountered {allow_partial}.'
        )

    # Fast path: const-keyed schema.
    if cls._sym_init_fields is not None and self._init_fast_path(
        kwargs,
        allow_partial,
        sealed,
        root_path,
        explicit_init,
    ):
        return

    # Slow path: handles varargs and schemas with non-const keys.

    # Model before-hook (#516) — the fast path was not taken
    # (`_sym_init_fields is None`), so the hook has not run yet.
    if cls._sym_preinit_overridden:
        kwargs = _apply_preinit(cls, kwargs)

    # We delay the seal attempt until members are all set.
    super().__init__(
        allow_partial=allow_partial,
        accessor_writable=cls.__sym_options__.attr_write,
        sealed=sealed,
        root_path=root_path,
        init_super=not explicit_init,
    )

    # Fill field_args and init_args from **kwargs.
    _, unmatched_keys = cls.__schema__.resolve(list(kwargs.keys()))
    # Fields opted out of `__init__` via `enable_init=False` resolve
    # against the schema but must still be rejected as kwargs.
    get_field = cls.__schema__.get_field
    unmatched_keys = list(unmatched_keys) + [
        k
        for k in kwargs
        if (f := get_field(k)) is not None and not f.enable_init
    ]
    if unmatched_keys:
        _screen_unexpected_init_keys(cls, kwargs, unmatched_keys)

    # Declared aliases are RESERVED wire spellings (#517, owner ruling):
    # an open (varkw) schema must not accept an extra kwarg equal to a
    # field's alias — the wire would become ambiguous. (Const-keyed
    # schemas never reach here with an alias-spelled kwarg; their
    # unknown-key screen already raised — or, under `varkw='ignore'`,
    # raised for alias keys before dropping the rest.)
    if cls._sym_alias_maps is not None:
        alias_to_name = cls._sym_alias_maps[0]
        for k in kwargs:
            if k in alias_to_name:
                raise TypeError(
                    f'{cls.__name__}.__init__() got keyword argument '
                    f'{k!r}, which is reserved as the alias of field '
                    f'{alias_to_name[k]!r}.'
                )

    # All field values come from kwargs — `pg.Object.__init__` is
    # keyword-only.
    field_args = dict(kwargs)

    # Check missing arguments when partial binding is disallowed.
    if not base.accepts_partial(self):
        missing_args = []
        for field in self.__class__.__schema__.fields.values():
            if (
                not field.value.has_default
                and isinstance(field.key, pg_typing.ConstStrKey)
                and field.key not in field_args
            ):
                missing_args.append(str(field.key))
        if missing_args:
            arg_phrase = formatting.auto_plural(
                len(missing_args), 'argument'
            )
            keys_str = formatting.comma_delimited_str(missing_args)
            raise TypeError(
                f'{self.__class__.__name__}.__init__() missing {len(missing_args)} '
                f'required {arg_phrase}: {keys_str}.'
            )

    # NOTE(daiyip): Accessor writable is honored by `Object.__setattr__` so
    # we can always make `_sym_attributes` accessor writable. This prevents
    # a child object's attribute access from being changed when it's
    # attached to a parent whose symbolic attributes are not writable.
    # Per-field validation is gated by `field.enable_validation` inside
    # `Schema.apply` / `Dict._formalized_value` — so a class-wide
    # `validate=False` (resolved into each field) and per-field overrides
    # both flow through naturally here.
    vars(self)['_sym_attributes'] = pg_dict.Dict(
        field_args,  # pyright: ignore[reportArgumentType]
        value_spec=self.__class__.sym_fields,
        allow_partial=allow_partial,
        sealed=sealed,
        accessor_writable=True,
        root_path=root_path,
        as_object_attributes_container=True,
        under_sym_mode=cls.__sym_options__.sym,
    )
    self._sym_attributes.sym_setparent(self)
    # Seed `init=False` fields from their defaults before `on_sym_post_init`
    # runs so `on_sym_ready`/`on_sym_bound` overrides start from a known baseline.
    _seed_non_symbolic_fields(self, cls._non_symbolic_fields)
    # Model after-validator (#516): fires before any lifecycle hook can
    # observe the object, gated like `on_sym_ready` (concrete only).
    if cls._sym_validate_overridden:
        abstract = (
            self._allow_partial and self.sym_partial
        ) or self.sym_puresymbolic
        if not abstract:
            self.on_sym_validate()
    self.on_sym_post_init()
    self.sym_seal(sealed)

sym_wrapped abstractmethod property

sym_wrapped

Returns symbolically wrapped object.

Dict

Dict(
    dict_obj: (
        None | Iterable[tuple[str | int, Any]] | dict[str | int, Any]
    ) = None,
    *,
    value_spec: Dict | None = None,
    wrap: bool = True,
    onchange_callback: (
        None | Callable[[dict[KeyPath, FieldUpdate]], None]
    ) = None,
    allow_partial: bool = False,
    accessor_writable: bool = True,
    sealed: bool = False,
    pass_through: bool = False,
    root_path: KeyPath | None = None,
    as_object_attributes_container: bool = False,
    under_sym_mode: bool = True,
    **kwargs: Any
)

Bases: dict[K, V], Symbolic, CustomTyping

Symbolic dict.

pg.Dict implements a dict type whose instances are symbolically programmable, which is a subclass of the built-in Python dict and a subclass of pygx.Symbolic.

pg.Dict provides the following features:

  • It a symbolic programmable dict with string keys.
  • It enables attribute access on dict keys.
  • It supports symbolic validation and value completitions based on schema.
  • It provides events to handle sub-nodes changes.

pg.Dict can be used as a regular dict with string keys::

# Construct a symbolic dict from key value pairs. d = pg.Dict(x=1, y=2)

or::

# Construct a symbolic dict from a mapping object. d = pg.Dict({'x': 1, 'y': 2})

Besides regular items access using [], it allows attribute access to its keys::

# Read access to key x. assert d.x == 1

# Write access to key 'y'. d.y = 1

pg.Dict supports symbolic validation when the value_spec argument is provided::

d = pg.Dict(x=1, y=2, value_spec=pg.typing.Dict([ ('x', pg.typing.Int(min_value=1)), ('y', pg.typing.Int(min_value=1)), (pg.typing.StrKey('foo.*'), pg.typing.Str()) ])

# Okay: all keys started with 'foo' is acceptable and are strings. d.foo1 = 'abc'

# Raises: 'bar' is not acceptable as keys in the dict. d.bar = 'abc'

Users can mutate the values contained in it::

d = pg.Dict(x=pg.Dict(y=1), p=pg.List([0])) d.sym_rebind({ 'x.y': 2, 'p[0]': 1 })

It also allows the users to subscribe subtree updates::

def on_change(updates): print(updates)

d = pg.Dict(x=1, onchange_callaback=on_change)

# on_change will be triggered on item insertion. d['y'] = {'z': 1}

# on_change will be triggered on item removal. del d.x

# on_change will also be triggered on subtree change. d.sym_rebind({'y.z': 2})

Constructor.

Parameters:

Name Type Description Default
dict_obj None | Iterable[tuple[str | int, Any]] | dict[str | int, Any]

A dict as initial value for this Dict.

None
value_spec Dict | None

Value spec that applies to this Dict.

None
wrap bool

Container-level default for whether nested raw dict / list items are wrapped into pg.Dict / pg.List. When True (default), items wrap on construction and on every subsequent __setitem__ / update / rebind. When False, items stay raw — both at init time and for later mutations, so the container's children type stays consistent. Acts as the default for schema fields whose enable_wrap is None; per-field explicit settings in value_spec win. (See base.resolve_wrap for the full wrap / validate / sym axis reference.)

True
onchange_callback None | Callable[[dict[KeyPath, FieldUpdate]], None]

Callback when sub-tree has been modified.

None
allow_partial bool

Whether to allow unbound or partial fields. This takes effect only when value_spec is not None.

False
accessor_writable bool

Whether to allow modification of this Dict using accessors (operator[] / attributes).

True
sealed bool

Whether to seal this Dict after creation.

False
pass_through bool

When True (and value_spec is provided), the caller asserts items are already formalized/validated against the spec, so per-field validation is skipped in the populate phase — only symbolic children are relocated (parent/path bookkeeping).

False
root_path KeyPath | None

KeyPath of this Dict in its object tree.

None
as_object_attributes_container bool

Internal — when True, child item parents propagate to self.sym_parent instead of self. Used for the inner _sym_attributes of pg.Object.

False
under_sym_mode bool

Internal — the inner _sym_attributes of a sym=False pg.Object passes False so this container neither clones nor reparents its children (reference semantics); _relocate_if_symbolic reads it during the populate phase.

True
**kwargs Any

Key value pairs that will be inserted into the dict as initial value, which provides a syntax sugar for usage as below: d = pg.Dict(a=1, b=2). The parameter names above are reserved: they configure the container and cannot be inserted as literal keys through this sugar — pass such keys via dict_obj (e.g. pg.Dict({'sealed': True})).

{}
Source code in pygx/symbolic/_dict.py
def __init__(
    self,
    dict_obj: (
        None | Iterable[tuple[str | int, Any]] | dict[str | int, Any]
    ) = None,
    *,
    value_spec: pg_typing.Dict | None = None,
    wrap: bool = True,
    onchange_callback: None
    | (Callable[[dict[topology.KeyPath, base.FieldUpdate]], None]) = None,  # pylint: disable=bad-continuation
    allow_partial: bool = False,
    accessor_writable: bool = True,
    sealed: bool = False,
    pass_through: bool = False,
    root_path: topology.KeyPath | None = None,
    as_object_attributes_container: bool = False,
    under_sym_mode: bool = True,
    **kwargs: Any,
):
    """Constructor.

    Args:
      dict_obj: A dict as initial value for this Dict.
      value_spec: Value spec that applies to this Dict.
      wrap: Container-level default for whether nested raw
        ``dict`` / ``list`` items are wrapped into ``pg.Dict`` /
        ``pg.List``. When True (default), items wrap on
        construction and on every subsequent
        ``__setitem__`` / ``update`` / rebind. When False, items
        stay raw — both at init time *and* for later mutations,
        so the container's children type stays consistent. Acts as
        the default for schema fields whose ``enable_wrap`` is
        ``None``; per-field explicit settings in ``value_spec`` win.
        (See ``base.resolve_wrap`` for the full ``wrap`` / ``validate`` /
        ``sym`` axis reference.)
      onchange_callback: Callback when sub-tree has been modified.
      allow_partial: Whether to allow unbound or partial fields. This takes
        effect only when value_spec is not None.
      accessor_writable: Whether to allow modification of this Dict using
        accessors (operator[] / attributes).
      sealed: Whether to seal this Dict after creation.
      pass_through: When True (and ``value_spec`` is provided), the caller
        asserts items are already formalized/validated against the spec,
        so per-field validation is skipped in the populate phase — only
        symbolic children are relocated (parent/path bookkeeping).
      root_path: KeyPath of this Dict in its object tree.
      as_object_attributes_container: Internal — when True, child item
        parents propagate to ``self.sym_parent`` instead of ``self``. Used
        for the inner ``_sym_attributes`` of `pg.Object`.
      under_sym_mode: Internal — the inner ``_sym_attributes`` of a
        ``sym=False`` `pg.Object` passes False so this container neither
        clones nor reparents its children (reference semantics);
        `_relocate_if_symbolic` reads it during the populate phase.
      **kwargs: Key value pairs that will be inserted into the dict as
        initial value, which provides a syntax sugar for usage as below:
        ``d = pg.Dict(a=1, b=2)``. The parameter names above are reserved:
        they configure the container and cannot be inserted as literal
        keys through this sugar — pass such keys via ``dict_obj`` (e.g.
        ``pg.Dict({'sealed': True})``).
    """
    if value_spec and not isinstance(value_spec, pg_typing.Dict):
        raise TypeError(
            f"Argument 'value_spec' must be a `pg.typing.Dict` object. "
            f'Encountered {value_spec}'
        )

    # --- 1. Normalize the (dict_obj, kwargs) overlay into a single
    #        iteration source. Avoid copying when possible: a plain dict
    #        with no kwargs overlay is iterated in place.
    normalized: dict[Any, Any] | types.MappingProxyType[Any, Any]
    if isinstance(dict_obj, Dict):
        # pg.Dict input is read via `sym_items` so Inferential placeholders
        # survive (the public `items()` would `infer()` them away).
        normalized = {k: v for k, v in dict_obj.sym_items()}
        if kwargs:
            normalized.update(kwargs)
    elif dict_obj is None:
        normalized = kwargs if kwargs else _EMPTY_DICT
    elif kwargs or not isinstance(dict_obj, dict):
        normalized = dict(dict_obj)
        if kwargs:
            normalized.update(kwargs)
    else:
        normalized = dict_obj
    dict_obj = normalized

    # --- 2. Initialize Symbolic + Dict state. We call the base inits
    #        directly (not via `super().__init__()`) because `dict.__init__`
    #        doesn't cooperate with `super(...)__init__`, which would skip
    #        `Symbolic.__init__`. Symbolic-state starts accessor-writable
    #        so we can populate members; user's `accessor_writable` is
    #        applied at the end.
    base.Symbolic.__init__(
        self,
        allow_partial=allow_partial,
        accessor_writable=True,
        sealed=False,  # delay sealing until items are populated.
        root_path=root_path,
    )
    dict.__init__(self)
    # Dict-specific state. Direct `__dict__` writes bypass the overridden
    # `__setattr__` (~80 ns each). `_onchange_callback` is set to its final
    # value at the end so notifications don't fire during population.
    self_dict = vars(self)
    self_dict['_value_spec'] = None
    self_dict['_onchange_callback'] = None
    self_dict['_as_object_attributes_container'] = (
        as_object_attributes_container
    )
    # Only shadow the class default (True) when False, to keep the common
    # tree-node case free of an extra instance-dict entry.
    if not under_sym_mode:
        self_dict['sym_mode'] = False
    # Container-level default for child-wrapping. Consulted by
    # `_formalized_value` (init + every later setitem / rebind), and
    # by `use_value_spec` when a schemaful container's schema field
    # leaves `enable_wrap` unset (`None`). Per-field explicit
    # settings on the value_spec still win.
    self_dict['_wrap'] = wrap

    # --- 3. Populate items. Three branches:
    #        a. `value_spec + pass_through`: trust caller's values, only
    #           relocate symbolic children (parent/path bookkeeping).
    #        b. `value_spec` (no pass_through): formalize per item, then
    #           run full schema validation via `use_value_spec`.
    #        c. no `value_spec` (schema-less): inline setter that skips the
    #           `FieldUpdate` build done by `_set_item_without_permission_check`
    #           (the update would be discarded — nothing subscribes yet).
    iter_items = dict_obj.items
    # Identity set for catching the same parentless symbolic object placed
    # under two keys. Only the object-attributes container needs it: it is
    # populated before being attached to its owner, so its children carry a
    # deferred `sym_parent=None` and the parent-based clone check can't see a
    # collision. Plain dicts parent children eagerly, so the existing check
    # already clones duplicates — leave `seen=None` to avoid the overhead.
    seen = set() if as_object_attributes_container else None
    if value_spec:
        if pass_through:
            for k, v in iter_items():
                super().__setitem__(
                    k, self._relocate_if_symbolic(k, v, seen)
                )
            self_dict['_value_spec'] = value_spec
        else:
            for k, v in iter_items():
                super().__setitem__(
                    k, self._formalized_value(k, None, v, seen)
                )
            self.use_value_spec(value_spec, allow_partial)
    else:
        _primitives = _PRIMITIVE_TYPES
        _missing = pg_typing.MISSING_VALUE
        for k, v in iter_items():
            if type(k) is not str and not isinstance(k, (str, int)):  # pylint: disable=unidiomatic-typecheck
                raise KeyError(
                    self._error_message(
                        f'Key must be string or int type. Encountered {k!r}.'
                    )
                )
            if v.__class__ in _primitives:
                super().__setitem__(k, v)
            elif _missing == v:
                # MISSING_VALUE on a schema-less Dict has no field default, so
                # it would just disappear — skip the insert.
                continue
            else:
                # `_formalized_value` consults `self._wrap` to decide
                # whether to wrap raw dict/list children — same logic at
                # init time and on later mutations, so the container's
                # children-type stays consistent.
                super().__setitem__(k, self._formalized_value(k, None, v))

    # --- 4. Finalize: apply user's accessor/seal/onchange settings. ----
    self_dict['_onchange_callback'] = onchange_callback
    if not accessor_writable:
        self_dict['_accessor_writable'] = False
    if sealed:
        self.sym_seal(True)

value_spec property

value_spec: Dict | None

Returns value spec of this dict.

NOTE(daiyip): If this dict is schema-less, value_spec will be None.

partial classmethod

partial(
    dict_obj: dict[str | int, Any] | None = None,
    value_spec: Dict | None = None,
    *,
    onchange_callback: (
        None | Callable[[dict[KeyPath, FieldUpdate]], None]
    ) = None,
    **kwargs
) -> Self

Class method that creates a partial Dict object.

Source code in pygx/symbolic/_dict.py
@classmethod
def partial(
    cls,
    dict_obj: dict[str | int, Any] | None = None,
    value_spec: pg_typing.Dict | None = None,
    *,
    onchange_callback: None
    | (Callable[[dict[topology.KeyPath, base.FieldUpdate]], None]) = None,  # pylint: disable=bad-continuation
    **kwargs,
) -> Self:
    """Class method that creates a partial Dict object."""
    return cls(
        dict_obj,
        value_spec=value_spec,
        onchange_callback=onchange_callback,
        allow_partial=True,
        **kwargs,
    )

from_json classmethod

from_json(
    json_value: Any,
    *,
    value_spec: Dict | None = None,
    allow_partial: bool = False,
    root_path: KeyPath | None = None,
    **kwargs: Any
) -> Self

Class method that load an symbolic Dict from a JSON value.

Parameters:

Name Type Description Default
json_value Any

Input JSON value, only JSON dict is acceptable.

required
value_spec Dict | None

An optional value spec to apply.

None
allow_partial bool

Whether to allow members of the dict to be partial.

False
root_path KeyPath | None

KeyPath of loaded object in its object tree.

None
**kwargs Any

Allow passing through keyword arguments that are not applicable.

{}

Returns:

Type Description
Self

A schemaless symbolic dict. For example::

d = Dict.from_json({ 'a': { 'type': '__main_.Foo', 'f1': 1, 'f2': { 'f21': True } } })

assert d.value_spec is None

Okay:

d.b = 1

a.f2 is bound by class Foo's field 'f2' definition (assume it defines
a schema for the Dict field).

assert d.a.f2.value_spec is not None

Not okay:

d.a.f2.abc = 1

Source code in pygx/symbolic/_dict.py
@classmethod
def from_json(
    cls,
    json_value: Any,
    *,
    value_spec: pg_typing.Dict | None = None,
    allow_partial: bool = False,
    root_path: topology.KeyPath | None = None,
    **kwargs: Any,
) -> Self:
    """Class method that load an symbolic Dict from a JSON value.

    Args:
      json_value: Input JSON value, only JSON dict is acceptable.
      value_spec: An optional value spec to apply.
      allow_partial: Whether to allow members of the dict to be partial.
      root_path: KeyPath of loaded object in its object tree.
      **kwargs: Allow passing through keyword arguments that are not applicable.

    Returns:
      A schemaless symbolic dict. For example::

        d = Dict.from_json({
          'a': {
            '_type': '__main__.Foo',
            'f1': 1,
            'f2': {
              'f21': True
            }
          }
        })

        assert d.value_spec is None
        # Okay:
        d.b = 1

        # a.f2 is bound by class Foo's field 'f2' definition (assume it defines
        # a schema for the Dict field).
        assert d.a.f2.value_spec is not None

        # Not okay:
        d.a.f2.abc = 1
    """
    # Drop the symbolic marker if present, then deserialize children in
    # place. `cls()` below adopts the dict via its own copy, so we don't
    # need to build a fresh one via comprehension.
    json_value.pop(wire.WireConvertible.SYMBOLIC_MARKER, None)
    _from_json = base.from_json
    _KeyPath = topology.KeyPath
    for k in json_value:
        json_value[k] = _from_json(
            json_value[k],
            root_path=_KeyPath(k, root_path),
            allow_partial=allow_partial,
            **kwargs,
        )
    return cls(
        json_value,
        value_spec=value_spec,
        root_path=root_path,
        allow_partial=allow_partial,
    )

use_value_spec

use_value_spec(value_spec: Dict | None, allow_partial: bool = False) -> Self

Applies a pg.typing.Dict as the value spec for current dict.

Parameters:

Name Type Description Default
value_spec Dict | None

A Dict ValueSpec to apply to this Dict. If current Dict is schema-less (whose immediate members are not validated against schema), and value_spec is not None, the value spec will be applied to the Dict. Or else if current Dict is already symbolic (whose immediate members are under the constraint of a Dict value spec), and value_spec is None, current Dict will become schema-less. However, the schema constraints for non-immediate members will remain.

required
allow_partial bool

Whether allow partial dict based on the schema. This flag will override allow_partial flag in init for spec-less Dict.

False

Returns:

Type Description
Self

Self.

Raises:

Type Description
ValueError

validation failed due to value error.

RuntimeError

Dict is already bound with another spec.

TypeError

type errors during validation.

KeyError

key errors during validation.

Source code in pygx/symbolic/_dict.py
def use_value_spec(
    self, value_spec: pg_typing.Dict | None, allow_partial: bool = False
) -> Self:
    """Applies a ``pg.typing.Dict`` as the value spec for current dict.

    Args:
      value_spec: A Dict ValueSpec to apply to this Dict.
        If current Dict is schema-less (whose immediate members are not
        validated against schema), and `value_spec` is not None, the value spec
        will be applied to the Dict.
        Or else if current Dict is already symbolic (whose immediate members
        are under the constraint of a Dict value spec), and `value_spec` is
        None, current Dict will become schema-less. However, the schema
        constraints for non-immediate members will remain.
      allow_partial: Whether allow partial dict based on the schema. This flag
        will override allow_partial flag in __init__ for spec-less Dict.

    Returns:
      Self.

    Raises:
      ValueError: validation failed due to value error.
      RuntimeError: Dict is already bound with another spec.
      TypeError: type errors during validation.
      KeyError: key errors during validation.
    """
    return _container_use_value_spec(
        self, value_spec, allow_partial, pg_typing.Dict
    )

sym_missing

sym_missing(flatten: bool = True) -> dict[str | int, Any]

Returns missing values.

Returns:

Type Description
dict[str | int, Any]

A dict of key to MISSING_VALUE.

Source code in pygx/symbolic/_dict.py
def sym_missing(self, flatten: bool = True) -> dict[str | int, Any]:
    """Returns missing values.

    Returns:
      A dict of key to MISSING_VALUE.
    """

    def compute() -> dict[str | int, Any]:
        missing = dict()
        if self._value_spec and self._value_spec.schema:
            matched_keys, _ = self._value_spec.schema.resolve(self.keys())
            for key_spec, keys in matched_keys.items():
                field = self._value_spec.schema[key_spec]
                assert keys or isinstance(
                    key_spec, pg_typing.NonConstKey
                ), key_spec
                if keys:
                    for key in keys:
                        v = self.sym_getattr(key)
                        if topology.MISSING_VALUE == v:
                            missing[key] = field.value.default
                        else:
                            if isinstance(v, base.Symbolic):
                                missing_child = v.sym_missing(flatten=False)
                                if missing_child:
                                    missing[key] = missing_child
        else:
            for k, v in self.sym_items():
                if isinstance(v, base.Symbolic):
                    missing_child = v.sym_missing(flatten=False)
                    if missing_child:
                        missing[k] = missing_child
        return missing

    return self._sym_cached('_sym_missing_values', compute, flatten)

sym_nondefault

sym_nondefault(flatten: bool = True) -> dict[str | int, Any]

Returns non-default values as key/value pairs in a dict.

Source code in pygx/symbolic/_dict.py
def sym_nondefault(self, flatten: bool = True) -> dict[str | int, Any]:
    """Returns non-default values as key/value pairs in a dict."""

    def compute() -> dict[str | int, Any]:
        non_defaults = dict()
        if self._value_spec is not None and self._value_spec.schema:
            dict_schema = self._value_spec.schema
            matched_keys, _ = dict_schema.resolve(self.keys())
            for key_spec, keys in matched_keys.items():
                value_spec = dict_schema[key_spec].value
                for key in keys:
                    diff = base.diff_base(
                        self.sym_getattr(key), value_spec.default
                    )
                    if pg_typing.MISSING_VALUE != diff:
                        non_defaults[key] = diff
        else:
            for k, v in self.sym_items():
                if isinstance(v, base.Symbolic):
                    non_defaults_child = v.sym_nondefault(flatten=False)
                    if non_defaults_child:
                        non_defaults[k] = non_defaults_child
                else:
                    non_defaults[k] = v
        return non_defaults

    return self._sym_cached('_sym_nondefault_values', compute, flatten)

sym_seal

sym_seal(is_seal: bool = True) -> Self

Seals or unseals current object (recursing into children).

Source code in pygx/symbolic/_dict.py
def sym_seal(self, is_seal: bool = True) -> Self:
    """Seals or unseals current object (recursing into children)."""
    if self.sym_sealed == is_seal:
        return self
    _Symbolic = base.Symbolic
    for v in dict.values(self):
        # Seal stops at the flat boundary (#542 ruling): a `sym=False`
        # child is a foreign, reference-held leaf — sealing the tree must
        # not toggle write permissions on an object other trees (and
        # plain holders) share. Its own `sym_seal` stays a direct op —
        # and RULED deep: a flat OWNER's direct seal still recurses into
        # its symbolic members (this loop, via `_sym_attributes`), even
        # though those members are reference-held too. The asymmetry is
        # deliberate: a direct seal is an explicit act on that object;
        # a tree-wide seal is not.
        if isinstance(v, _Symbolic) and v.sym_mode:
            v.sym_seal(is_seal)
    super().sym_seal(is_seal)
    return self

sym_attr_field

sym_attr_field(key: str | int) -> Field | None

Returns the field definition for a symbolic attribute.

Source code in pygx/symbolic/_dict.py
def sym_attr_field(self, key: str | int) -> pg_typing.Field | None:
    """Returns the field definition for a symbolic attribute."""
    if self._value_spec is None:
        return None
    schema = self._value_spec.schema
    if schema is None:
        return None
    return schema.get_field(key)

sym_hasattr

sym_hasattr(key: str | int) -> bool

Tests if a symbolic attribute exists.

Source code in pygx/symbolic/_dict.py
def sym_hasattr(self, key: str | int) -> bool:
    """Tests if a symbolic attribute exists."""
    return key in self

sym_keys

sym_keys() -> Iterator[str | int]

Iterates the keys of symbolic attributes.

Source code in pygx/symbolic/_dict.py
def sym_keys(self) -> Iterator[str | int]:
    """Iterates the keys of symbolic attributes."""
    if self._value_spec is None or self._value_spec.schema is None:
        # Schema-less: insertion order is iteration order. Use the C-level
        # dict iterator directly to avoid a per-key Python-level yield.
        return iter(dict.keys(self))
    return self._sym_keys_schema()

sym_values

sym_values() -> Iterator[Any]

Iterates the values of symbolic attributes.

Source code in pygx/symbolic/_dict.py
def sym_values(self) -> Iterator[Any]:
    """Iterates the values of symbolic attributes."""
    if self._value_spec is None or self._value_spec.schema is None:
        return iter(dict.values(self))
    return (dict.__getitem__(self, k) for k in self._sym_keys_schema())

sym_items

sym_items() -> Iterator[tuple[str | int, Any]]

Iterates the (key, value) pairs of symbolic attributes.

Source code in pygx/symbolic/_dict.py
def sym_items(self) -> Iterator[tuple[str | int, Any]]:
    """Iterates the (key, value) pairs of symbolic attributes."""
    if self._value_spec is None or self._value_spec.schema is None:
        return iter(dict.items(self))
    return ((k, dict.__getitem__(self, k)) for k in self._sym_keys_schema())

sym_setparent

sym_setparent(parent: TopologyAware | None)

Override set parent of Dict to handle the passing through scenario.

Source code in pygx/symbolic/_dict.py
def sym_setparent(self, parent: base.TopologyAware | None):
    """Override set parent of Dict to handle the passing through scenario."""
    # Set our own parent directly (skip the `super().sym_setparent` call so
    # we don't allocate a super proxy per call — this is the hot path on
    # every Object construction).
    vars(self)['_sym_parent'] = parent
    # When `as_object_attributes_container` is on, propagate to children.
    # Iteration order doesn't matter here; raw dict iteration avoids the
    # schema generator path. Skipped entirely for the inner container of a
    # `sym=False` object (it does not reparent its children); individual
    # `sym=False` children (opaque leaves) are skipped in either case.
    if vars(self)['_as_object_attributes_container'] and self.sym_mode:
        _TA = base.TopologyAware
        _prims = _PRIMITIVE_TYPES
        for v in dict.values(self):
            # Primitives can't be TopologyAware — skip the ABCMeta isinstance
            # for the common case; only tree-node children are reparented.
            if (
                v.__class__ not in _prims
                and isinstance(v, _TA)
                and getattr(v, 'sym_mode', True)
            ):
                v.sym_setparent(parent)

sym_hash

sym_hash() -> int

Symbolic hashing.

Source code in pygx/symbolic/_dict.py
def sym_hash(self) -> int:
    """Symbolic hashing."""
    # `base.sym_hash` on this tuple is just `hash(tuple)` — inline it.
    # Schema-less Dicts use `dict.items` directly (no `sym_items` generator).
    #
    # The item pairs hash as a FROZENSET (#493, owner-ruled): `sym_eq` is
    # key-set based, so the hash must be insertion-order-independent or
    # eq-equal Dicts hash unequal (`pg.Dict(a=1, b=2)` vs
    # `pg.Dict(b=2, a=1)`). A frozenset — not a key-sorted tuple — because
    # a schema-less Dict admits MIXED str/int keys, which don't totally
    # order; pairs are unique (unique keys), so no dedup can occur.
    #
    # The tag is the CONSTANT `dict` — not `self.__class__` — because
    # `eq`'s dict rule is class-blind (`pg.Dict(a=1)` equals `{'a': 1}`
    # and any pg.Dict subclass of equal content), so the hash must be
    # too; it matches `base.sym_hash`'s raw-dict arm exactly, holding the
    # contract across the pg.Dict/raw boundary. (A raw dict HOLDING a
    # `MISSING_VALUE` still diverges — the raw arm has no MISSING filter;
    # partial-artifact raw dicts are out of the contract.) The
    # `!=`-based MISSING filter is equivalent to the generated bodies'
    # `__class__ is _MissingType` test because `MissingValue` is SEALED.
    _sym_hash = base.sym_hash
    _MISSING = pg_typing.MISSING_VALUE
    if self._value_spec is None or self._value_spec.schema is None:
        items = dict.items(self)
    else:
        items = self.sym_items()
    return hash(
        (
            dict,
            frozenset((k, _sym_hash(v)) for k, v in items if v != _MISSING),
        )
    )

on_sym_change

on_sym_change(field_updates: dict[KeyPath, FieldUpdate])

On change event of Dict.

Source code in pygx/symbolic/_dict.py
def on_sym_change(
    self, field_updates: dict[topology.KeyPath, base.FieldUpdate]
):
    """On change event of Dict."""
    if self._onchange_callback:
        self._onchange_callback(field_updates)

get

get(key: str | int, default: Any = None) -> Any

Get item in this Dict.

Source code in pygx/symbolic/_dict.py
def get(self, key: str | int, default: Any = None) -> Any:
    """Get item in this Dict."""
    v = dict.get(self, key, _MISSING_SENTINEL)
    if v is _MISSING_SENTINEL:
        return default
    if isinstance(v, base.Inferential):
        try:
            return v.infer()
        except AttributeError:
            return default
    return v

keys

keys() -> Iterator[K]

Returns an iterator of keys in current dict.

Source code in pygx/symbolic/_dict.py
@override
def keys(  # pyright: ignore[reportIncompatibleMethodOverride]
    self,
) -> Iterator[K]:
    """Returns an iterator of keys in current dict."""
    return self.sym_keys()

items

items() -> Iterator[tuple[K, V]]

Returns an iterator of (key, value) items in current dict.

Source code in pygx/symbolic/_dict.py
@override
def items(  # pyright: ignore[reportIncompatibleMethodOverride]
    self,
) -> Iterator[tuple[K, V]]:
    """Returns an iterator of (key, value) items in current dict."""
    _Inferential = base.Inferential
    for k, v in self.sym_items():
        yield k, (v.infer() if isinstance(v, _Inferential) else v)

values

values() -> Iterator[V]

Returns an iterator of values in current dict..

Source code in pygx/symbolic/_dict.py
@override
def values(self) -> Iterator[V]:  # pyright: ignore[reportIncompatibleMethodOverride]
    """Returns an iterator of values in current dict.."""
    _Inferential = base.Inferential
    for v in self.sym_values():
        yield v.infer() if isinstance(v, _Inferential) else v

copy

copy() -> Self

Overridden copy using symbolic copy.

Source code in pygx/symbolic/_dict.py
def copy(self) -> Self:
    """Overridden copy using symbolic copy."""
    cloned = self.sym_clone(deep=False)
    assert isinstance(cloned, type(self))
    return cloned

pop

pop(key: str | int, default: Any = RAISE_IF_NOT_FOUND) -> Any

Pops a key from current dict.

Source code in pygx/symbolic/_dict.py
def pop(
    self,
    key: str | int,
    default: Any = base.RAISE_IF_NOT_FOUND,  # pylint: disable=protected-access
) -> Any:
    """Pops a key from current dict."""
    if key in self:
        value = self[key]
        with flags.allow_writable_accessors(True):
            del self[key]  # pyright: ignore[reportArgumentType]
        return value if value != pg_typing.MISSING_VALUE else default
    if default is base.RAISE_IF_NOT_FOUND:
        raise KeyError(key)
    return default

clear

clear() -> None

Removes all the keys in current dict.

Source code in pygx/symbolic/_dict.py
def clear(self) -> None:
    """Removes all the keys in current dict."""
    if base.treats_as_sealed(self):
        raise base.WritePermissionError('Cannot clear a sealed Dict.')
    # (`_value_spec` is defined by `_container_use_value_spec` /
    # `_container_custom_apply`, the module-level shared bodies, which
    # astroid does not track as attribute definitions.)
    # pylint: disable-next=access-member-before-definition
    value_spec = self._value_spec
    self._value_spec = None
    super().clear()

    if value_spec:
        self.use_value_spec(value_spec, self._allow_partial)

setdefault

setdefault(key: str | int, default: Any = None) -> Any

Sets default as the value to key if not present.

Source code in pygx/symbolic/_dict.py
def setdefault(self, key: str | int, default: Any = None) -> Any:
    """Sets default as the value to key if not present."""
    value = pg_typing.MISSING_VALUE
    if key in self:
        value = self.sym_getattr(key)
    if value == pg_typing.MISSING_VALUE:
        self[key] = default
        value = default
    return value

update

update(
    other: None | dict[str | int, Any] | Iterable[tuple[str | int, Any]] = None,
    **kwargs
) -> None

Update Dict with the same semantic as update on standard dict.

Source code in pygx/symbolic/_dict.py
@override
def update(  # pyright: ignore[reportIncompatibleMethodOverride]
    self,
    other: (
        None | dict[str | int, Any] | Iterable[tuple[str | int, Any]]
    ) = None,
    **kwargs,
) -> None:
    """Update Dict with the same semantic as update on standard dict."""
    updates = dict(other) if other else {}
    updates.update(kwargs)
    self.sym_rebind(
        updates, raise_on_no_change=False, skip_notification=True
    )  # pyright: ignore[reportArgumentType]

sym_jsonify

sym_jsonify(
    *,
    exclude_frozen: bool = True,
    exclude_defaults: bool = False,
    exclude_keys: Sequence[str | int] | None = None,
    use_inferred: bool = False,
    exclude_none: bool = False,
    omit_symbolic_marker: bool = True,
    **kwargs
) -> WireValue

Converts current object to a dict with plain Python objects.

exclude_none=True (#519) drops None-valued entries at emission — uniformly for pg.Object fields and pg.Dict entries (any mapping key whose value is None; list items are never dropped); exclude_defaults=True drops entries equal to their schema default (the pydantic spelling — formerly hide_default_values).

Source code in pygx/symbolic/_dict.py
def sym_jsonify(
    self,
    *,
    exclude_frozen: bool = True,
    exclude_defaults: bool = False,
    exclude_keys: Sequence[str | int] | None = None,
    use_inferred: bool = False,
    exclude_none: bool = False,
    omit_symbolic_marker: bool = True,
    **kwargs,
) -> wire.WireValue:
    """Converts current object to a dict with plain Python objects.

    `exclude_none=True` (#519) drops `None`-valued entries at emission —
    uniformly for `pg.Object` fields and `pg.Dict` entries (any mapping
    key whose value is `None`; list items are never dropped);
    `exclude_defaults=True` drops entries equal to their schema default
    (the pydantic spelling — formerly `hide_default_values`).
    """
    json_repr = {}
    if not omit_symbolic_marker:
        json_repr[wire.WireConvertible.SYMBOLIC_MARKER] = True
    # Stash the public flags into kwargs so child `to_json` calls see
    # them without us re-spreading the locals per field.
    kwargs['exclude_frozen'] = exclude_frozen
    kwargs['exclude_defaults'] = exclude_defaults
    kwargs['use_inferred'] = use_inferred
    kwargs['exclude_none'] = exclude_none
    kwargs['omit_symbolic_marker'] = omit_symbolic_marker

    spec = self._value_spec
    schema = spec.schema if spec is not None else None
    if schema is None:
        self._sym_jsonify_no_schema(
            json_repr, exclude_keys, use_inferred, exclude_none, kwargs
        )
    elif (
        not schema._has_nonconst_keys  # pylint: disable=protected-access
        and not exclude_keys
        and not exclude_defaults
        and not use_inferred
        and not exclude_none
    ):
        # Hot path: `Object._sym_attributes`-shaped dict — all-const-keys,
        # default flags. Walks `schema._const_apply_fields` directly,
        # skipping `schema.resolve()` / `_sym_keys_schema` / per-key
        # `key_spec.text`.
        self._sym_jsonify_const_fast(
            schema, exclude_frozen, json_repr, kwargs
        )
    else:
        self._sym_jsonify_schema_slow(
            schema,
            exclude_frozen,
            exclude_defaults,
            exclude_keys,
            use_inferred,
            exclude_none,
            json_repr,
            kwargs,
        )
    return json_repr

custom_apply

custom_apply(
    path: KeyPath,
    value_spec: ValueSpec,
    allow_partial: bool,
    child_transform: None | Callable[[KeyPath, Field, Any], Any] = None,
) -> tuple[bool, Dict]

Implement pg.typing.CustomTyping interface.

Parameters:

Name Type Description Default
path KeyPath

KeyPath of current object.

required
value_spec ValueSpec

Origin value spec of the field.

required
allow_partial bool

Whether allow partial object to be created.

required
child_transform None | Callable[[KeyPath, Field, Any], Any]

Function to transform child node values in dict_obj into their final values. Transform function is called on leaf nodes first, then on their containers, recursively.

None

Returns:

Type Description
tuple[bool, Dict]

A tuple (proceed_with_standard_apply, transformed value)

Source code in pygx/symbolic/_dict.py
def custom_apply(
    self,
    path: topology.KeyPath,
    value_spec: pg_typing.ValueSpec,
    allow_partial: bool,
    child_transform: None
    | (Callable[[topology.KeyPath, pg_typing.Field, Any], Any]) = None,
) -> tuple[bool, 'Dict']:
    """Implement pg.typing.CustomTyping interface.

    Args:
      path: KeyPath of current object.
      value_spec: Origin value spec of the field.
      allow_partial: Whether allow partial object to be created.
      child_transform: Function to transform child node values in dict_obj into
        their final values. Transform function is called on leaf nodes first,
        then on their containers, recursively.

    Returns:
      A tuple (proceed_with_standard_apply, transformed value)
    """
    return _container_custom_apply(
        self, path, value_spec, allow_partial, pg_typing.Dict
    )

format

format(
    compact: bool = False,
    verbose: bool = True,
    root_indent: int = 0,
    *,
    python_format: bool = False,
    exclude_frozen: bool = True,
    exclude_defaults: bool = False,
    exclude_missing: bool = False,
    include_keys: set[str | int] | None = None,
    exclude_keys: set[str | int] | None = None,
    use_inferred: bool = False,
    cls_name: str | None = None,
    bracket_type: BracketType = CURLY,
    key_as_attribute: bool = False,
    extra_blankline_for_field_docstr: bool = False,
    extra_fields: list[tuple[Field | None, str | int, Any]] | None = None,
    **kwargs
) -> str

Formats this Dict.

extra_fields lets callers (notably Object.format) inject rendered entries that aren't part of the underlying Dict — e.g. enable_init=False fields stored on the parent __dict__. Entries are formatted with the same per-field codepath as normal entries (description, indentation, exclude_missing).

Source code in pygx/symbolic/_dict.py
def format(
    self,
    compact: bool = False,
    verbose: bool = True,
    root_indent: int = 0,
    *,
    python_format: bool = False,
    exclude_frozen: bool = True,
    exclude_defaults: bool = False,
    exclude_missing: bool = False,
    include_keys: set[str | int] | None = None,
    exclude_keys: set[str | int] | None = None,
    use_inferred: bool = False,
    cls_name: str | None = None,
    bracket_type: formatting.BracketType = formatting.BracketType.CURLY,
    key_as_attribute: bool = False,
    extra_blankline_for_field_docstr: bool = False,
    extra_fields: (
        list[tuple[pg_typing.Field | None, str | int, Any]] | None
    ) = None,
    **kwargs,
) -> str:
    """Formats this Dict.

    ``extra_fields`` lets callers (notably ``Object.format``) inject
    rendered entries that aren't part of the underlying Dict — e.g.
    ``enable_init=False`` fields stored on the parent ``__dict__``.
    Entries are formatted with the same per-field codepath as
    normal entries (description, indentation, exclude_missing).
    """
    # Render via the shared `_format_symbolic_fields`, driven from this Dict's
    # own storage. (The inline `Object.format` calls the same helper with the
    # object's `cls.__schema__` + inline accessors, so a `sym=True` object
    # formats without materializing a `__sym_dict__` — v2 P4.)
    schema = self._value_spec.schema if self._value_spec else None
    return _format_symbolic_fields(
        schema,
        self.keys(),
        self.sym_getattr,
        self.sym_items,
        self.sym_inferred,
        compact,
        verbose,
        root_indent,
        python_format=python_format,
        exclude_frozen=exclude_frozen,
        exclude_defaults=exclude_defaults,
        exclude_missing=exclude_missing,
        include_keys=include_keys,
        exclude_keys=exclude_keys,
        use_inferred=use_inferred,
        cls_name=cls_name,
        bracket_type=bracket_type,
        key_as_attribute=key_as_attribute,
        extra_blankline_for_field_docstr=extra_blankline_for_field_docstr,
        extra_fields=extra_fields,
        **kwargs,
    )

Diff

Diff(
    left: Any = _DIFF_MISSING,
    right: Any = _DIFF_MISSING,
    children: dict[str, Diff] | None = None,
    **kwargs: Any
)

Bases: PureSymbolic, Object, Extension

A value diff between two objects: a 'left' object and a 'right' object.

If one of them is missing, it may be represented by pg.Diff.MISSING

For example::

pg.Diff(3.14, 1.618) Diff(left=3.14, right=1.618) pg.Diff('hello world', pg.Diff.MISSING) Diff(left='hello world', right=MISSING)

Source code in pygx/symbolic/_diff.py
def __init__(
    self,
    left: Any = _DIFF_MISSING,
    right: Any = _DIFF_MISSING,
    children: dict[str, 'Diff'] | None = None,
    **kwargs: Any,
):
    super().__init__(
        left=left,
        right=right,
        children=children if children is not None else {},
        **kwargs,
    )

is_leaf property

is_leaf: bool

Returns True if current Diff does not contain inner Diff object.

value property

value

Returns the value if left and right are the same.

sym_eq

sym_eq(other: Any) -> bool

Override symbolic equality.

Source code in pygx/symbolic/_diff.py
def sym_eq(self, other: Any) -> bool:
    """Override symbolic equality."""
    if super().sym_eq(other):
        return True
    if not bool(self):
        return base.eq(self.left, other)
    return False

format

format(
    compact: bool = False, verbose: bool = True, root_indent: int = 0, **kwargs
)

Override format to conditionally print the shared value or the diff.

Source code in pygx/symbolic/_diff.py
def format(
    self,
    compact: bool = False,
    verbose: bool = True,
    root_indent: int = 0,
    **kwargs,
):
    """Override format to conditionally print the shared value or the diff."""
    if not bool(self):
        if self.value == Diff.MISSING:
            return 'No diff'
        # When there is no diff, but the same value needs to be displayed
        # we simply return the value.
        return formatting.format(
            self.value, compact, verbose, root_indent, **kwargs
        )
    if self.is_leaf:
        exclude_keys = kwargs.pop('exclude_keys', None)
        exclude_keys = exclude_keys or set()
        exclude_keys.add('children')
        return super().format(
            compact,
            verbose,
            root_indent,
            exclude_keys=exclude_keys,
            **kwargs,
        )
    else:
        assert isinstance(self.left, type)
        assert isinstance(self.right, type)
        # `self.children` is materialized as a `pg.Dict` at __init__ time
        # by `value_spec=Dict[...]`, so `.format` is available at runtime.
        children: Any = self.children
        if self.left is self.right and issubclass(self.left, list):
            return children.format(
                compact=compact,
                verbose=verbose,
                root_indent=root_indent,
                cls_name='',
                bracket_type=formatting.BracketType.SQUARE,
                **kwargs,
            )
        if self.left is self.right:
            cls_name = self.left.__name__
        else:
            cls_name = f'{self.left.__name__}|{self.right.__name__}'
        return children.format(
            compact=compact,
            verbose=verbose,
            root_indent=root_indent,
            cls_name=cls_name,
            bracket_type=formatting.BracketType.ROUND,
            **kwargs,
        )

Functor

Functor(
    *args: Any,
    root_path: KeyPath | None = None,
    override_args: bool = False,
    ignore_extra_args: bool = False,
    **kwargs: Any
)

Bases: Object, Functor

Symbolic functions (Functors).

A symbolic function is a symbolic class with a __call__ method, whose arguments can be bound partially, incrementally bound by attribute assignment, or provided at call time.

Another useful trait is that a symbolic function is serializable, when its definition is imported by the target program and its arguments are also serializable. Therefore, it is very handy to move a symbolic function around in distributed scenarios.

Symbolic functions can be created from regular function via pygx.functor::

# Create a functor class using @pg.functor decorator. @pg.functor([ ('a', pg.typing.Int(), 'Argument a'), # No field specification for 'b', which will be treated as any type. ]) def sum(a, b=1, args, *kwargs): return a + b + sum(args + kwargs.values())

sum(1)() # returns 2: prebind a=1, invoke with b=1 (default) sum(a=1)() # returns 2: same as above. sum()(1) # returns 2: bind a=1 at call time, b=1(default)

sum(b=2)(1) # returns 3: prebind b=2, invoke with a=1. sum(b=2)() # wrong: a is not provided.

sum(1)(2) # wrong: 'a' is provided multiple times. sum(1)(2, override_args=True) # ok: override a value with 2.

sum()(1, 2, 3, 4) # returns 10: a=1, b=2, args=[3, 4] sum(c=4)(1, 2, 3) # returns 10: a=1, b=2, *args=[3], *kwargs={'c': 4}

Or created by subclassing pg.Functor::

class Sum(pg.Functor): a: int b: int = 1

def _call(self) -> int:
  return self.a + self.b

Usage on subclassed functors is the same as functors created from functions.

Constructor.

Parameters:

Name Type Description Default
*args Any

prebound positional arguments.

()
root_path KeyPath | None

The symbolic path for current object.

None
override_args bool

If True, allows arguments provided during __call__ to override existing bound arguments.

False
ignore_extra_args bool

If True, unsupported arguments can be passed in during __call__ without using them. Otherwise, calling with unsupported arguments will raise error.

False
**kwargs Any

prebound keyword arguments.

{}

Raises:

Type Description
KeyError

constructor got unexpected arguments.

Source code in pygx/symbolic/_functor.py
def __init__(
    self,
    *args: Any,
    root_path: topology.KeyPath | None = None,
    override_args: bool = False,
    ignore_extra_args: bool = False,
    **kwargs: Any,
):
    """Constructor.

    Args:
      *args: prebound positional arguments.
      root_path: The symbolic path for current object.
      override_args: If True, allows arguments provided during `__call__` to
        override existing bound arguments.
      ignore_extra_args: If True, unsupported arguments can be passed in
        during `__call__` without using them. Otherwise, calling with
        unsupported arguments will raise error.
      **kwargs: prebound keyword arguments.

    Raises:
      KeyError: constructor got unexpected arguments.
    """
    # NOTE(daiyip): Since Functor is usually late bound (until call time),
    # we pass `allow_partial=True` during functor construction.
    _ = kwargs.pop('allow_partial', None)

    varargs = None
    signature = self.__signature__
    if len(args) > len(signature.args):
        if signature.varargs:
            varargs = list(args[len(signature.args) :])
            args = args[: len(signature.args)]
        else:
            arg_phrase = formatting.auto_plural(
                len(signature.args), 'argument'
            )
            was_phrase = formatting.auto_plural(len(args), 'was', 'were')
            raise TypeError(
                f'{signature.id}() takes {len(signature.args)} '
                f'positional {arg_phrase} but {len(args)} {was_phrase} given.'
            )

    bound_kwargs = dict()
    for i, v in enumerate(args):
        if pg_typing.MISSING_VALUE != v:
            bound_kwargs[signature.args[i].name] = v

    if varargs is not None:
        bound_kwargs[signature.varargs.name] = varargs

    for k, v in kwargs.items():
        if pg_typing.MISSING_VALUE != v:
            if k in bound_kwargs:
                raise TypeError(
                    f'{signature.id}() got multiple values for keyword '
                    f'argument {k!r}.'
                )
            bound_kwargs[k] = v

    default_args = set()
    non_default_args = set(bound_kwargs)

    for arg_spec in signature.named_args:
        if not arg_spec.value_spec.has_default:
            continue
        arg_name = arg_spec.name
        if arg_name not in non_default_args:
            default_args.add(arg_name)
        elif bound_kwargs[arg_name] == arg_spec.value_spec.default:
            default_args.add(arg_name)
            non_default_args.discard(arg_name)

    if signature.varargs and not varargs:
        default_args.add(signature.varargs.name)

    super().__init__(
        allow_partial=True, root_path=root_path, **bound_kwargs
    )

    self._non_default_args = non_default_args
    self._default_args = default_args
    self._specified_args = set(bound_kwargs)
    self._override_args = override_args
    self._ignore_extra_args = ignore_extra_args

    # For subclassed Functor, we use thread-local storage for storing temporary
    # member overrides from the arguments during functor call.
    self._tls = threading.local() if self.is_subclassed_functor() else None

specified_args property

specified_args: set[str]

Returns user specified argument names.

non_default_args property

non_default_args: set[str]

Returns the names of bound arguments whose values are not the default.

default_args property

default_args: set[str]

Returns the names of bound argument whose values are the default.

bound_args property

bound_args: set[str]

Returns bound argument names.

unbound_args property

unbound_args: set[str]

Returns unbound argument names.

is_fully_bound property

is_fully_bound: bool

Returns if all arguments of functor is bound.

is_subclassed_functor classmethod

is_subclassed_functor() -> bool

Returns True if this class is a subclassed Functor.

Source code in pygx/symbolic/_functor.py
@classmethod
def is_subclassed_functor(cls) -> bool:
    """Returns True if this class is a subclassed Functor."""
    return cls.__auto_schema__

partial classmethod

partial(*args, **kwargs) -> Self

Creates a partial functor with positional/keyword args prebound.

Source code in pygx/symbolic/_functor.py
@classmethod
def partial(cls, *args, **kwargs) -> Self:
    """Creates a partial functor with positional/keyword args prebound."""
    return cls(*args, allow_partial=True, **kwargs)

sym_inferred

sym_inferred(key: str, default: Any = RAISE_IF_NOT_FOUND, **kwargs: Any) -> Any

Overrides method to allow member overrides during call.

Source code in pygx/symbolic/_functor.py
@override
def sym_inferred(
    self, key: str, default: Any = base.RAISE_IF_NOT_FOUND, **kwargs: Any
) -> Any:  # pyright: ignore[reportIncompatibleMethodOverride]
    """Overrides method to allow member overrides during call."""
    tls = self._tls
    if tls is not None:
        overrides = tls.__dict__.get(Functor._TLS_OVERRIDE_MEMBERS_KEY)
        if overrides is not None and key in overrides:
            return overrides[key]
    return super().sym_inferred(key, default=default, **kwargs)

on_sym_change

on_sym_change(field_updates: dict[KeyPath, FieldUpdate])

Custom handling field change to update bound args.

Source code in pygx/symbolic/_functor.py
def on_sym_change(
    self, field_updates: dict[topology.KeyPath, base.FieldUpdate]
):
    """Custom handling field change to update bound args."""
    for relative_path, update in field_updates.items():
        assert relative_path
        if len(relative_path) != 1:
            continue
        arg_name = str(relative_path)
        assert update.field is not None
        if update.field.default_value == update.new_value:
            if update.field.value.has_default:
                self._default_args.add(arg_name)
            self._non_default_args.discard(arg_name)
        else:
            self._default_args.discard(arg_name)
            self._non_default_args.add(arg_name)

        if update.new_value == pg_typing.MISSING_VALUE:
            self._specified_args.discard(arg_name)
        else:
            self._specified_args.add(arg_name)

sym_missing

sym_missing(flatten: bool = True) -> dict[str, Any]

Returns missing values for Functor.

Semantically unbound arguments are not missing, thus we only return partial bound arguments in sym_missing. As a result, a functor is partial only when any of its bound arguments is partial.

Returns:

Type Description
dict[str, Any]

A dict of missing key (or path) to missing value.

Source code in pygx/symbolic/_functor.py
def sym_missing(self, flatten: bool = True) -> dict[str, Any]:
    """Returns missing values for Functor.

    Semantically unbound arguments are not missing, thus we only return partial
    bound arguments in `sym_missing`. As a result, a functor is partial only
    when any of its bound arguments is partial.

    Returns:
      A dict of missing key (or path) to missing value.
    """

    def compute() -> dict[str, Any]:
        missing = dict()
        for k, v in self._sym_attributes.items():
            if pg_typing.MISSING_VALUE != v and isinstance(
                v, base.Symbolic
            ):
                missing_child = v.sym_missing(flatten=False)
                if missing_child:
                    missing[k] = missing_child
        return missing

    return self._sym_cached('_sym_missing_values', compute, flatten)

InferredValue

InferredValue(
    *,
    allow_partial: bool = False,
    sealed: bool | None = None,
    root_path: KeyPath | None = None,
    explicit_init: bool = False,
    **kwargs: Any
)

Bases: Object, Inferential

Base class for inferred values.

Source code in pygx/symbolic/_object.py
@_filter_typing_traceback_on_raise
def __init__(
    self,
    *,
    allow_partial: bool = False,
    sealed: bool | None = None,
    root_path: topology.KeyPath | None = None,
    explicit_init: bool = False,
    **kwargs: Any,
):
    """Create an Object instance.

    ``pg.Object`` synthesizes a keyword-only ``__init__``. Subclasses
    that need positional arguments must override ``__init__``
    explicitly and translate to keyword arguments before forwarding
    to ``super().__init__(**kwargs)``.

    Args:
      allow_partial: If True, the object can be partial.
      sealed: If True, seal the object from future modification (unless under
        a `pg.as_sealed(False)` context manager). If False, treat the object as
        unsealed. If None, it's determined by `cls.__sym_options__.frozen`.
      root_path: The symbolic path for current object. By default it's None,
        which indicates that newly constructed object does not have a parent.
      explicit_init: Should set to `True` when `__init__` is called via
        `pg.Object.__init__` instead of `super().__init__`.
      **kwargs: key/value arguments that align with the schema. All required
        keys in the schema must be specified, and values should be acceptable
        according to their value spec.

    Raises:
      KeyError: When required key(s) are missing.
      ValueError: When value(s) are not acceptable by their value spec.
    """
    # Placeholder for Google-internal usage instrumentation.

    cls = self.__class__
    # The ever-instantiated latch `apply_schema`'s post-instance guard
    # reads (#432: `update_schema` is only legal before instances exist).
    # Own-`__dict__`, latched on the construction ATTEMPT — mirroring the
    # native core, whose `__native_ctor_meta__` (the equivalent marker) is
    # created on the first instance op. One dict read per construct.
    meta = cls.__sym_meta__
    if not meta.instantiated:
        meta.instantiated = True
    if sealed is None:
        sealed = cls.__sym_options__.frozen

    if not isinstance(allow_partial, bool):
        raise TypeError(
            f"Expect bool type for argument 'allow_partial' in "
            f'symbolic.Object.__init__ but encountered {allow_partial}.'
        )

    # Fast path: const-keyed schema.
    if cls._sym_init_fields is not None and self._init_fast_path(
        kwargs,
        allow_partial,
        sealed,
        root_path,
        explicit_init,
    ):
        return

    # Slow path: handles varargs and schemas with non-const keys.

    # Model before-hook (#516) — the fast path was not taken
    # (`_sym_init_fields is None`), so the hook has not run yet.
    if cls._sym_preinit_overridden:
        kwargs = _apply_preinit(cls, kwargs)

    # We delay the seal attempt until members are all set.
    super().__init__(
        allow_partial=allow_partial,
        accessor_writable=cls.__sym_options__.attr_write,
        sealed=sealed,
        root_path=root_path,
        init_super=not explicit_init,
    )

    # Fill field_args and init_args from **kwargs.
    _, unmatched_keys = cls.__schema__.resolve(list(kwargs.keys()))
    # Fields opted out of `__init__` via `enable_init=False` resolve
    # against the schema but must still be rejected as kwargs.
    get_field = cls.__schema__.get_field
    unmatched_keys = list(unmatched_keys) + [
        k
        for k in kwargs
        if (f := get_field(k)) is not None and not f.enable_init
    ]
    if unmatched_keys:
        _screen_unexpected_init_keys(cls, kwargs, unmatched_keys)

    # Declared aliases are RESERVED wire spellings (#517, owner ruling):
    # an open (varkw) schema must not accept an extra kwarg equal to a
    # field's alias — the wire would become ambiguous. (Const-keyed
    # schemas never reach here with an alias-spelled kwarg; their
    # unknown-key screen already raised — or, under `varkw='ignore'`,
    # raised for alias keys before dropping the rest.)
    if cls._sym_alias_maps is not None:
        alias_to_name = cls._sym_alias_maps[0]
        for k in kwargs:
            if k in alias_to_name:
                raise TypeError(
                    f'{cls.__name__}.__init__() got keyword argument '
                    f'{k!r}, which is reserved as the alias of field '
                    f'{alias_to_name[k]!r}.'
                )

    # All field values come from kwargs — `pg.Object.__init__` is
    # keyword-only.
    field_args = dict(kwargs)

    # Check missing arguments when partial binding is disallowed.
    if not base.accepts_partial(self):
        missing_args = []
        for field in self.__class__.__schema__.fields.values():
            if (
                not field.value.has_default
                and isinstance(field.key, pg_typing.ConstStrKey)
                and field.key not in field_args
            ):
                missing_args.append(str(field.key))
        if missing_args:
            arg_phrase = formatting.auto_plural(
                len(missing_args), 'argument'
            )
            keys_str = formatting.comma_delimited_str(missing_args)
            raise TypeError(
                f'{self.__class__.__name__}.__init__() missing {len(missing_args)} '
                f'required {arg_phrase}: {keys_str}.'
            )

    # NOTE(daiyip): Accessor writable is honored by `Object.__setattr__` so
    # we can always make `_sym_attributes` accessor writable. This prevents
    # a child object's attribute access from being changed when it's
    # attached to a parent whose symbolic attributes are not writable.
    # Per-field validation is gated by `field.enable_validation` inside
    # `Schema.apply` / `Dict._formalized_value` — so a class-wide
    # `validate=False` (resolved into each field) and per-field overrides
    # both flow through naturally here.
    vars(self)['_sym_attributes'] = pg_dict.Dict(
        field_args,  # pyright: ignore[reportArgumentType]
        value_spec=self.__class__.sym_fields,
        allow_partial=allow_partial,
        sealed=sealed,
        accessor_writable=True,
        root_path=root_path,
        as_object_attributes_container=True,
        under_sym_mode=cls.__sym_options__.sym,
    )
    self._sym_attributes.sym_setparent(self)
    # Seed `init=False` fields from their defaults before `on_sym_post_init`
    # runs so `on_sym_ready`/`on_sym_bound` overrides start from a known baseline.
    _seed_non_symbolic_fields(self, cls._non_symbolic_fields)
    # Model after-validator (#516): fires before any lifecycle hook can
    # observe the object, gated like `on_sym_ready` (concrete only).
    if cls._sym_validate_overridden:
        abstract = (
            self._allow_partial and self.sym_partial
        ) or self.sym_puresymbolic
        if not abstract:
            self.on_sym_validate()
    self.on_sym_post_init()
    self.sym_seal(sealed)

ValueFromParentChain

ValueFromParentChain(
    *,
    allow_partial: bool = False,
    sealed: bool | None = None,
    root_path: KeyPath | None = None,
    explicit_init: bool = False,
    **kwargs: Any
)

Bases: InferredValue

A value that could inferred from the parent chain.

For example::

class A(pg.Object): x: int y: int = pg.symbolic.ValueFromParentChain()

# Not okay: x is not inferential and is not specified. A()

# Okay: both x and y are specified. A(x=1, y=2)

# Okay: y is inferential, hence optional. a = A(x=1)

# Raises: y is neither specified during init # nor provided from the context. a.y

d = pg.Dict(y=2, z=pg.Dict(a=a))

# a.y now refers to d.a since d is in its symbolic parent chain, # aka. context. assert a.y == 2

Source code in pygx/symbolic/_object.py
@_filter_typing_traceback_on_raise
def __init__(
    self,
    *,
    allow_partial: bool = False,
    sealed: bool | None = None,
    root_path: topology.KeyPath | None = None,
    explicit_init: bool = False,
    **kwargs: Any,
):
    """Create an Object instance.

    ``pg.Object`` synthesizes a keyword-only ``__init__``. Subclasses
    that need positional arguments must override ``__init__``
    explicitly and translate to keyword arguments before forwarding
    to ``super().__init__(**kwargs)``.

    Args:
      allow_partial: If True, the object can be partial.
      sealed: If True, seal the object from future modification (unless under
        a `pg.as_sealed(False)` context manager). If False, treat the object as
        unsealed. If None, it's determined by `cls.__sym_options__.frozen`.
      root_path: The symbolic path for current object. By default it's None,
        which indicates that newly constructed object does not have a parent.
      explicit_init: Should set to `True` when `__init__` is called via
        `pg.Object.__init__` instead of `super().__init__`.
      **kwargs: key/value arguments that align with the schema. All required
        keys in the schema must be specified, and values should be acceptable
        according to their value spec.

    Raises:
      KeyError: When required key(s) are missing.
      ValueError: When value(s) are not acceptable by their value spec.
    """
    # Placeholder for Google-internal usage instrumentation.

    cls = self.__class__
    # The ever-instantiated latch `apply_schema`'s post-instance guard
    # reads (#432: `update_schema` is only legal before instances exist).
    # Own-`__dict__`, latched on the construction ATTEMPT — mirroring the
    # native core, whose `__native_ctor_meta__` (the equivalent marker) is
    # created on the first instance op. One dict read per construct.
    meta = cls.__sym_meta__
    if not meta.instantiated:
        meta.instantiated = True
    if sealed is None:
        sealed = cls.__sym_options__.frozen

    if not isinstance(allow_partial, bool):
        raise TypeError(
            f"Expect bool type for argument 'allow_partial' in "
            f'symbolic.Object.__init__ but encountered {allow_partial}.'
        )

    # Fast path: const-keyed schema.
    if cls._sym_init_fields is not None and self._init_fast_path(
        kwargs,
        allow_partial,
        sealed,
        root_path,
        explicit_init,
    ):
        return

    # Slow path: handles varargs and schemas with non-const keys.

    # Model before-hook (#516) — the fast path was not taken
    # (`_sym_init_fields is None`), so the hook has not run yet.
    if cls._sym_preinit_overridden:
        kwargs = _apply_preinit(cls, kwargs)

    # We delay the seal attempt until members are all set.
    super().__init__(
        allow_partial=allow_partial,
        accessor_writable=cls.__sym_options__.attr_write,
        sealed=sealed,
        root_path=root_path,
        init_super=not explicit_init,
    )

    # Fill field_args and init_args from **kwargs.
    _, unmatched_keys = cls.__schema__.resolve(list(kwargs.keys()))
    # Fields opted out of `__init__` via `enable_init=False` resolve
    # against the schema but must still be rejected as kwargs.
    get_field = cls.__schema__.get_field
    unmatched_keys = list(unmatched_keys) + [
        k
        for k in kwargs
        if (f := get_field(k)) is not None and not f.enable_init
    ]
    if unmatched_keys:
        _screen_unexpected_init_keys(cls, kwargs, unmatched_keys)

    # Declared aliases are RESERVED wire spellings (#517, owner ruling):
    # an open (varkw) schema must not accept an extra kwarg equal to a
    # field's alias — the wire would become ambiguous. (Const-keyed
    # schemas never reach here with an alias-spelled kwarg; their
    # unknown-key screen already raised — or, under `varkw='ignore'`,
    # raised for alias keys before dropping the rest.)
    if cls._sym_alias_maps is not None:
        alias_to_name = cls._sym_alias_maps[0]
        for k in kwargs:
            if k in alias_to_name:
                raise TypeError(
                    f'{cls.__name__}.__init__() got keyword argument '
                    f'{k!r}, which is reserved as the alias of field '
                    f'{alias_to_name[k]!r}.'
                )

    # All field values come from kwargs — `pg.Object.__init__` is
    # keyword-only.
    field_args = dict(kwargs)

    # Check missing arguments when partial binding is disallowed.
    if not base.accepts_partial(self):
        missing_args = []
        for field in self.__class__.__schema__.fields.values():
            if (
                not field.value.has_default
                and isinstance(field.key, pg_typing.ConstStrKey)
                and field.key not in field_args
            ):
                missing_args.append(str(field.key))
        if missing_args:
            arg_phrase = formatting.auto_plural(
                len(missing_args), 'argument'
            )
            keys_str = formatting.comma_delimited_str(missing_args)
            raise TypeError(
                f'{self.__class__.__name__}.__init__() missing {len(missing_args)} '
                f'required {arg_phrase}: {keys_str}.'
            )

    # NOTE(daiyip): Accessor writable is honored by `Object.__setattr__` so
    # we can always make `_sym_attributes` accessor writable. This prevents
    # a child object's attribute access from being changed when it's
    # attached to a parent whose symbolic attributes are not writable.
    # Per-field validation is gated by `field.enable_validation` inside
    # `Schema.apply` / `Dict._formalized_value` — so a class-wide
    # `validate=False` (resolved into each field) and per-field overrides
    # both flow through naturally here.
    vars(self)['_sym_attributes'] = pg_dict.Dict(
        field_args,  # pyright: ignore[reportArgumentType]
        value_spec=self.__class__.sym_fields,
        allow_partial=allow_partial,
        sealed=sealed,
        accessor_writable=True,
        root_path=root_path,
        as_object_attributes_container=True,
        under_sym_mode=cls.__sym_options__.sym,
    )
    self._sym_attributes.sym_setparent(self)
    # Seed `init=False` fields from their defaults before `on_sym_post_init`
    # runs so `on_sym_ready`/`on_sym_bound` overrides start from a known baseline.
    _seed_non_symbolic_fields(self, cls._non_symbolic_fields)
    # Model after-validator (#516): fires before any lifecycle hook can
    # observe the object, gated like `on_sym_ready` (concrete only).
    if cls._sym_validate_overridden:
        abstract = (
            self._allow_partial and self.sym_partial
        ) or self.sym_puresymbolic
        if not abstract:
            self.on_sym_validate()
    self.on_sym_post_init()
    self.sym_seal(sealed)

inference_key property

inference_key: str

Returns the key for attribute inference from parents.

Insertion dataclass

Insertion(value: Any)

Class that marks a value to insert into a list.

Example::

l = pg.List([0, 1]) l.sym_rebind({ 0: pg.Insertion(2) }) assert l == [2, 0, 1]

List

List(
    items: Iterable[Any] | None = None,
    *,
    value_spec: List | None = None,
    wrap: bool = True,
    onchange_callback: (
        None | Callable[[dict[KeyPath, FieldUpdate]], None]
    ) = None,
    allow_partial: bool = False,
    accessor_writable: bool = True,
    sealed: bool = False,
    pass_through: bool = False,
    root_path: KeyPath | None = None
)

Bases: list, Symbolic, CustomTyping

Symbolic list.

pg.List implements a list type whose instances are symbolically programmable, which is a subclass of the built-in Python list, and the subclass of pg.Symbolic.

pg.List can be used as a regular list::

# Construct a symbolic list from an iterable object. l = pg.List(range(10))

It also supports symbolic validation through the value_spec argument::

l = pg.List([1, 2, 3], value_spec=pg.typing.List( pg.typing.Int(min_value=1), max_size=10 ))

# Raises: 0 is not in acceptable range. l.append(0)

And can be symbolically manipulated::

l = pg.List([{'foo': 1}]) l.sym_rebind({ '[0].foo': 2 })

pg.query(l, where=lambda x: isinstance(x, int))

The user call also subscribe changes to its sub-nodes::

def on_change(updates): print(updates)

l = pg.List([{'foo': 1}], onchange_callaback=on_change)

# on_change will be triggered on item insertion. l.append({'bar': 2})

# on_change will be triggered on item removal. l.pop(0)

# on_change will also be triggered on subtree change. l.sym_rebind({'[0].bar': 3})

Constructor.

Parameters:

Name Type Description Default
items Iterable[Any] | None

A optional iterable object as initial value for this list.

None
value_spec List | None

Value spec that applies to this List.

None
wrap bool

Container-level default for whether nested raw dict / list items are wrapped into pg.Dict / pg.List. When True (default), items wrap on construction and on every subsequent __setitem__ / append / insert. When False, items stay raw — both at init time and for later mutations, so the container's children type stays consistent. Acts as the default for the schema's element.enable_wrap when it is None; an explicit per-element setting in value_spec wins. (See base.resolve_wrap for the full wrap / validate / sym axis reference.)

True
onchange_callback None | Callable[[dict[KeyPath, FieldUpdate]], None]

Callback when sub-tree has been modified.

None
allow_partial bool

Whether to allow unbound or partial fields. This takes effect only when value_spec is not None.

False
accessor_writable bool

Whether to allow modification of this List using accessors (operator[]).

True
sealed bool

Whether to seal this List after creation.

False
pass_through bool

When True (and value_spec is provided), the caller asserts items are already formalized/validated against the spec, so this skips per-item validation and the full use_value_spec re-apply — only symbolic children are relocated (parent/path bookkeeping). Mirrors pg.Dict's pass_through; used by the symbolic transform that wraps an already-validated raw list into a pg.List during construct (avoids validating every element a second time).

False
root_path KeyPath | None

KeyPath of this List in its object tree.

None
Source code in pygx/symbolic/_list.py
def __init__(
    self,
    items: Iterable[Any] | None = None,
    *,
    value_spec: pg_typing.List | None = None,
    wrap: bool = True,
    onchange_callback: None
    | (Callable[[dict[topology.KeyPath, base.FieldUpdate]], None]) = None,
    allow_partial: bool = False,
    accessor_writable: bool = True,
    sealed: bool = False,
    pass_through: bool = False,
    root_path: topology.KeyPath | None = None,
):
    """Constructor.

    Args:
      items: A optional iterable object as initial value for this list.
      value_spec: Value spec that applies to this List.
      wrap: Container-level default for whether nested raw
        ``dict`` / ``list`` items are wrapped into ``pg.Dict`` /
        ``pg.List``. When True (default), items wrap on
        construction and on every subsequent ``__setitem__`` /
        ``append`` / ``insert``. When False, items stay raw — both
        at init time *and* for later mutations, so the
        container's children type stays consistent. Acts as the
        default for the schema's ``element.enable_wrap`` when it is
        ``None``; an explicit per-element setting in ``value_spec`` wins.
        (See ``base.resolve_wrap`` for the full ``wrap`` / ``validate`` /
        ``sym`` axis reference.)
      onchange_callback: Callback when sub-tree has been modified.
      allow_partial: Whether to allow unbound or partial fields. This takes
        effect only when value_spec is not None.
      accessor_writable: Whether to allow modification of this List using
        accessors (operator[]).
      sealed: Whether to seal this List after creation.
      pass_through: When True (and ``value_spec`` is provided), the caller
        asserts ``items`` are already formalized/validated against the spec,
        so this skips per-item validation and the full ``use_value_spec``
        re-apply — only symbolic children are relocated (parent/path
        bookkeeping). Mirrors ``pg.Dict``'s ``pass_through``; used by the
        symbolic transform that wraps an already-validated raw ``list`` into
        a ``pg.List`` during construct (avoids validating every element a
        second time).
      root_path: KeyPath of this List in its object tree.
    """
    if value_spec and not isinstance(value_spec, pg_typing.List):
        raise TypeError(
            f"Argument 'value_spec' must be a `pg.typing.List` object. "
            f'Encountered {value_spec}.'
        )

    # We delay seal operation until items are filled.
    base.Symbolic.__init__(
        self,
        allow_partial=allow_partial,
        accessor_writable=accessor_writable,
        sealed=False,
        root_path=root_path,
    )

    self._value_spec = None
    self._onchange_callback = None
    # Container-level default for child-wrapping. Consulted by
    # `_formalized_value` (init + every later setitem / append / insert)
    # so the container's children type stays consistent across mutations.
    # Per-element explicit `enable_wrap` on the value_spec wins.
    self._wrap = wrap

    list.__init__(self)
    if items:
        # Iterate the symbolic form of input pg.List so Inferential / other
        # symbolic placeholders survive the copy.
        if isinstance(items, List):
            items = items.sym_values()

        if value_spec is None:
            # Schema-less fast path: inline the minimal setter. Skipping
            # `_set_item_without_permission_check` saves the FieldUpdate
            # allocation per item — and there are no subscribers yet to read
            # it. Insertion / MISSING_VALUE special cases preserve the
            # semantics of the slow path. `_formalize` reads
            # `self._wrap` to decide whether to wrap raw children, so
            # the wrap-off path uses the same call as on.
            _primitives = _PRIMITIVE_TYPES
            _missing = pg_typing.MISSING_VALUE
            _formalize = self._formalized_value
            _append = list.append
            for item in items:
                if item.__class__ in _primitives:
                    _append(self, item)
                elif isinstance(item, Insertion):
                    self._set_item_without_permission_check(len(self), item)
                elif _missing == item:
                    continue  # MISSING_VALUE appends are no-ops.
                else:
                    _append(self, _formalize(len(self), item))
        elif pass_through:
            # Trust the caller's already-formalized items: only relocate
            # symbolic children for parent/path bookkeeping, skipping the
            # per-item `_formalized_value` validation AND the full
            # `use_value_spec` re-apply below. Mirrors `pg.Dict`'s
            # `pass_through` (without it the list path validates every
            # element twice). Items parent eagerly, so `seen=None` (the
            # parent-based clone check in `_relocate_if_symbolic` suffices).
            # Primitives are never symbolic, so they skip `_relocate` (and
            # its ABCMeta `isinstance` checks) entirely — the common
            # `list[int]` / `list[str]` case appends straight through.
            _primitives = _PRIMITIVE_TYPES
            _relocate = self._relocate_if_symbolic
            _append = list.append
            for item in items:
                if item.__class__ in _primitives:
                    _append(self, item)
                else:
                    _append(self, _relocate(len(self), item, None))
        else:
            for item in items:
                self._set_item_without_permission_check(len(self), item)

    if value_spec:
        if pass_through:
            # `_value_spec` is normally set as a side effect of
            # `use_value_spec` -> `value_spec.apply`; set it directly since
            # the pass-through path skips that re-apply.
            self._value_spec = value_spec
        else:
            self.use_value_spec(value_spec, allow_partial)

    # Set onchange callback last so notifications don't fire during init.
    self._onchange_callback = onchange_callback
    self.sym_seal(sealed)

max_size property

max_size: int | None

Returns max size of this list.

value_spec property

value_spec: List | None

Returns value spec of this List.

partial classmethod

partial(
    items: Iterable[Any] | None = None,
    *,
    value_spec: List | None = None,
    onchange_callback: (
        None | Callable[[dict[KeyPath, FieldUpdate]], None]
    ) = None,
    **kwargs
) -> Self

Class method that creates a partial List object.

Source code in pygx/symbolic/_list.py
@classmethod
def partial(
    cls,
    items: Iterable[Any] | None = None,
    *,
    value_spec: pg_typing.List | None = None,
    onchange_callback: None
    | (Callable[[dict[topology.KeyPath, base.FieldUpdate]], None]) = None,
    **kwargs,
) -> Self:
    """Class method that creates a partial List object."""
    return cls(
        items,
        value_spec=value_spec,
        onchange_callback=onchange_callback,
        allow_partial=True,
        **kwargs,
    )

from_json classmethod

from_json(
    json_value: Any,
    *,
    value_spec: List | None = None,
    allow_partial: bool = False,
    root_path: KeyPath | None = None,
    **kwargs: Any
) -> Self

Class method that load an symbolic List from a JSON value.

Example::

l = List.from_json([{
    '_type': '__main__.Foo',
    'f1': 1,
    'f2': {
      'f21': True
    }
  },
  1
])

assert l.value_spec is None
# Okay:
l.append('abc')

# [0].f2 is bound by class Foo's field 'f2' definition
# (assuming it defines a schema for the Dict field).
assert l[0].f2.value_spec is not None

# Not okay:
l[0].f2.abc = 1

Parameters:

Name Type Description Default
json_value Any

Input JSON value, only JSON list is acceptable.

required
value_spec List | None

An optional pg.typing.List object as the schema for the list.

None
allow_partial bool

Whether to allow elements of the list to be partial.

False
root_path KeyPath | None

KeyPath of loaded object in its object tree.

None
**kwargs Any

Allow passing through keyword arguments that are not applicable.

{}

Returns:

Type Description
Self

A schema-less symbolic list, but its items maybe symbolic.

Source code in pygx/symbolic/_list.py
@classmethod
def from_json(
    cls,
    json_value: Any,
    *,
    value_spec: pg_typing.List | None = None,
    allow_partial: bool = False,
    root_path: topology.KeyPath | None = None,
    **kwargs: Any,
) -> Self:
    """Class method that load an symbolic List from a JSON value.

    Example::

        l = List.from_json([{
            '_type': '__main__.Foo',
            'f1': 1,
            'f2': {
              'f21': True
            }
          },
          1
        ])

        assert l.value_spec is None
        # Okay:
        l.append('abc')

        # [0].f2 is bound by class Foo's field 'f2' definition
        # (assuming it defines a schema for the Dict field).
        assert l[0].f2.value_spec is not None

        # Not okay:
        l[0].f2.abc = 1

    Args:
      json_value: Input JSON value, only JSON list is acceptable.
      value_spec: An optional `pg.typing.List` object as the schema for the
        list.
      allow_partial: Whether to allow elements of the list to be partial.
      root_path: KeyPath of loaded object in its object tree.
      **kwargs: Allow passing through keyword arguments that are not applicable.

    Returns:
      A schema-less symbolic list, but its items maybe symbolic.
    """
    # Drop the symbolic marker if present, then deserialize children in
    # place. `cls()` below adopts the list via its own copy, so we don't
    # need to materialize a fresh comprehension.
    if json_value and json_value[0] == wire.WireConvertible.SYMBOLIC_MARKER:
        json_value.pop(0)
    _from_json = base.from_json
    _KeyPath = topology.KeyPath
    for i, item in enumerate(json_value):
        json_value[i] = _from_json(
            item,
            root_path=_KeyPath(i, root_path),
            allow_partial=allow_partial,
            **kwargs,
        )
    return cls(
        json_value,
        value_spec=value_spec,
        root_path=root_path,
        allow_partial=allow_partial,
    )

use_value_spec

use_value_spec(value_spec: List | None, allow_partial: bool = False) -> Self

Applies a pg.List as the value spec for current list.

Parameters:

Name Type Description Default
value_spec List | None

A List ValueSpec to apply to this List. If current List is schema-less (whose immediate members are not validated against schema), and value_spec is not None, the value spec will be applied to the List. Or else if current List is already symbolic (whose immediate members are under the constraint of a List value spec), and value_spec is None, current List will become schema-less. However, the schema constraints for non-immediate members will remain.

required
allow_partial bool

Whether allow partial dict based on the schema. This flag will override allow_partial flag in init for spec-less List.

False

Returns:

Type Description
Self

Self.

Raises:

Type Description
ValueError

schema validation failed due to value error.

RuntimeError

List is already bound with another value_spec.

TypeError

type errors during validation.

KeyError

key errors during validation.

Source code in pygx/symbolic/_list.py
def use_value_spec(
    self, value_spec: pg_typing.List | None, allow_partial: bool = False
) -> Self:
    """Applies a ``pg.List`` as the value spec for current list.

    Args:
      value_spec: A List ValueSpec to apply to this List.
        If current List is schema-less (whose immediate members are not
        validated against schema), and `value_spec` is not None, the value spec
        will be applied to the List.
        Or else if current List is already symbolic (whose immediate members
        are under the constraint of a List value spec), and `value_spec` is
        None, current List will become schema-less. However, the schema
        constraints for non-immediate members will remain.
      allow_partial: Whether allow partial dict based on the schema. This flag
        will override allow_partial flag in __init__ for spec-less List.

    Returns:
      Self.

    Raises:
      ValueError: schema validation failed due to value error.
      RuntimeError: List is already bound with another value_spec.
      TypeError: type errors during validation.
      KeyError: key errors during validation.
    """
    return _container_use_value_spec(
        self, value_spec, allow_partial, pg_typing.List
    )

sym_attr_field

sym_attr_field(key: str | int) -> Field | None

Returns the field definition for a symbolic attribute.

Source code in pygx/symbolic/_list.py
def sym_attr_field(self, key: str | int) -> pg_typing.Field | None:
    """Returns the field definition for a symbolic attribute."""
    del key
    if self._value_spec is None:
        return None
    return self._value_spec.element

sym_hasattr

sym_hasattr(key: str | int) -> bool

Tests if a symbolic attribute exists.

Source code in pygx/symbolic/_list.py
def sym_hasattr(self, key: str | int) -> bool:
    """Tests if a symbolic attribute exists."""
    return isinstance(key, int) and key >= -len(self) and key < len(self)

sym_keys

sym_keys() -> Iterator[int]

Symbolically iterates indices.

Source code in pygx/symbolic/_list.py
def sym_keys(self) -> Iterator[int]:
    """Symbolically iterates indices."""
    return iter(range(len(self)))

sym_values

sym_values() -> Iterator[Any]

Iterates the values of symbolic attributes.

Source code in pygx/symbolic/_list.py
def sym_values(self) -> Iterator[Any]:
    """Iterates the values of symbolic attributes."""
    return list.__iter__(self)

sym_items

sym_items() -> Iterator[tuple[int, Any]]

Iterates the (key, value) pairs of symbolic attributes.

Source code in pygx/symbolic/_list.py
def sym_items(self) -> Iterator[tuple[int, Any]]:
    """Iterates the (key, value) pairs of symbolic attributes."""
    return enumerate(list.__iter__(self))

sym_hash

sym_hash() -> int

Symbolically hashing.

Source code in pygx/symbolic/_list.py
def sym_hash(self) -> int:
    """Symbolically hashing."""
    # `base.sym_hash` on this tuple is just `hash(tuple)` — inline it.
    # `list.__iter__` skips the `sym_values` indirection.
    _sym_hash = base.sym_hash
    return hash(
        (
            self.__class__,
            tuple(_sym_hash(e) for e in list.__iter__(self)),
        )
    )

sym_missing

sym_missing(flatten: bool = True) -> dict[Any, Any]

Returns missing fields.

Source code in pygx/symbolic/_list.py
def sym_missing(self, flatten: bool = True) -> dict[Any, Any]:
    """Returns missing fields."""

    def compute() -> dict[Any, Any]:
        missing = dict()
        for idx, elem in self.sym_items():
            if isinstance(elem, base.Symbolic):
                missing_child = elem.sym_missing(flatten=False)
                if missing_child:
                    missing[idx] = missing_child
        return missing

    return self._sym_cached('_sym_missing_values', compute, flatten)

sym_nondefault

sym_nondefault(flatten: bool = True) -> dict[int, Any]

Returns non-default values.

Source code in pygx/symbolic/_list.py
@override
def sym_nondefault(self, flatten: bool = True) -> dict[int, Any]:  # pyright: ignore[reportIncompatibleMethodOverride]
    """Returns non-default values."""

    def compute() -> dict[int, Any]:
        non_defaults = dict()
        for idx, elem in self.sym_items():
            if isinstance(elem, base.Symbolic):
                non_defaults_child = elem.sym_nondefault(flatten=False)
                if non_defaults_child:
                    non_defaults[idx] = non_defaults_child
            else:
                non_defaults[idx] = elem
        return non_defaults

    return self._sym_cached('_sym_nondefault_values', compute, flatten)

sym_seal

sym_seal(is_seal: bool = True) -> Self

Seal or unseal current object (recursing into children).

Source code in pygx/symbolic/_list.py
def sym_seal(self, is_seal: bool = True) -> Self:
    """Seal or unseal current object (recursing into children)."""
    if self.sym_sealed == is_seal:
        return self
    for elem in self.sym_values():
        # Seal stops at the flat boundary (#542 ruling) — see
        # `Dict.sym_seal`.
        if isinstance(elem, base.Symbolic) and elem.sym_mode:
            elem.sym_seal(is_seal)
    super().sym_seal(is_seal)
    return self

on_sym_change

on_sym_change(field_updates: dict[KeyPath, FieldUpdate])

On change event of List.

Source code in pygx/symbolic/_list.py
def on_sym_change(
    self, field_updates: dict[topology.KeyPath, base.FieldUpdate]
):
    """On change event of List."""
    # Do nothing for now to handle changes of List.

    # NOTE(daiyip): Remove items that are MISSING_VALUES.
    keys_to_remove = []
    for i, item in self.sym_items():
        if pg_typing.MISSING_VALUE == item:
            keys_to_remove.append(i)
    if keys_to_remove:
        for i in reversed(keys_to_remove):
            list.__delitem__(self, i)

    # Update paths for children (skipping `sym=False` opaque leaves).
    for idx, item in self.sym_items():
        if (
            isinstance(item, base.TopologyAware)
            and getattr(item, 'sym_mode', True)
            and item.sym_path.key != idx
        ):
            item.sym_setpath(topology.KeyPath(idx, self.sym_path))

    if self._onchange_callback is not None:
        self._onchange_callback(field_updates)

copy

copy() -> List

Shallow current list.

Source code in pygx/symbolic/_list.py
def copy(self) -> 'List':
    """Shallow current list."""
    return List(super().copy(), value_spec=self._value_spec)

append

append(value: Any) -> None

Appends an item.

Source code in pygx/symbolic/_list.py
def append(self, value: Any) -> None:
    """Appends an item."""
    if base.treats_as_sealed(self):
        raise base.WritePermissionError(
            'Cannot append element on a sealed List.'
        )
    if self.max_size is not None and len(self) >= self.max_size:
        raise ValueError(f'List reached its max size {self.max_size}.')

    update = self._set_item_without_permission_check(len(self), value)
    if flags.is_change_notification_enabled() and update:
        self._notify_field_updates([update])

insert

insert(index: SupportsIndex, value: Any) -> None

Inserts an item at a given position.

Source code in pygx/symbolic/_list.py
@override
def insert(self, index: SupportsIndex, value: Any) -> None:
    """Inserts an item at a given position."""
    if base.treats_as_sealed(self):
        raise base.WritePermissionError(
            'Cannot insert element into a sealed List.'
        )
    if self.max_size is not None and len(self) >= self.max_size:
        raise ValueError(f'List reached its max size {self.max_size}.')

    update = self._set_item_without_permission_check(
        index.__index__(), mark_as_insertion(value)
    )
    if flags.is_change_notification_enabled() and update:
        self._notify_field_updates([update])

pop

pop(index: SupportsIndex = -1) -> Any

Pop an item and return its value.

Source code in pygx/symbolic/_list.py
@override
def pop(self, index: SupportsIndex = -1) -> Any:
    """Pop an item and return its value."""
    i = index.__index__()
    if i < -len(self) or i >= len(self):
        raise IndexError('pop index out of range')
    i = (i + len(self)) % len(self)
    value = self[i]
    with flags.allow_writable_accessors(True):
        del self[i]
    return value

remove

remove(value: Any) -> None

Removes the first occurrence of the value.

Source code in pygx/symbolic/_list.py
def remove(self, value: Any) -> None:
    """Removes the first occurrence of the value."""
    for i, item in self.sym_items():
        if item == value:
            if self._value_spec and self._value_spec.min_size == len(self):
                raise ValueError(
                    f'Cannot remove item: min size ({self._value_spec.min_size}) '
                    f'is reached.'
                )
            del self[i]
            return
    raise ValueError(f'{value!r} not in list.')

clear

clear() -> None

Clears the list.

Source code in pygx/symbolic/_list.py
def clear(self) -> None:
    """Clears the list."""
    if base.treats_as_sealed(self):
        raise base.WritePermissionError('Cannot clear a sealed List.')
    if self._value_spec and self._value_spec.min_size > 0:
        raise ValueError(
            f'List cannot be cleared: min size is {self._value_spec.min_size}.'
        )
    super().clear()

sort

sort(*, key=None, reverse=False) -> None

Sorts the items of the list in place..

Source code in pygx/symbolic/_list.py
def sort(self, *, key=None, reverse=False) -> None:
    """Sorts the items of the list in place.."""
    if base.treats_as_sealed(self):
        raise base.WritePermissionError('Cannot sort a sealed List.')
    super().sort(key=key, reverse=reverse)

reverse

reverse() -> None

Reverse the elements of the list in place.

Source code in pygx/symbolic/_list.py
def reverse(self) -> None:
    """Reverse the elements of the list in place."""
    if base.treats_as_sealed(self):
        raise base.WritePermissionError('Cannot reverse a sealed List.')
    super().reverse()

custom_apply

custom_apply(
    path: KeyPath,
    value_spec: ValueSpec,
    allow_partial: bool,
    child_transform: None | Callable[[KeyPath, Field, Any], Any] = None,
) -> tuple[bool, List]

Implement pg.typing.CustomTyping interface.

Parameters:

Name Type Description Default
path KeyPath

KeyPath of current object.

required
value_spec ValueSpec

Origin value spec of the field.

required
allow_partial bool

Whether allow partial object to be created.

required
child_transform None | Callable[[KeyPath, Field, Any], Any]

Function to transform child node values in dict_obj into their final values. Transform function is called on leaf nodes first, then on their containers, recursively.

None

Returns:

Type Description
tuple[bool, List]

A tuple (proceed_with_standard_apply, transformed value)

Source code in pygx/symbolic/_list.py
def custom_apply(
    self,
    path: topology.KeyPath,
    value_spec: pg_typing.ValueSpec,
    allow_partial: bool,
    child_transform: None
    | (Callable[[topology.KeyPath, pg_typing.Field, Any], Any]) = None,
) -> tuple[bool, 'List']:
    """Implement pg.typing.CustomTyping interface.

    Args:
      path: KeyPath of current object.
      value_spec: Origin value spec of the field.
      allow_partial: Whether allow partial object to be created.
      child_transform: Function to transform child node values in dict_obj into
        their final values. Transform function is called on leaf nodes first,
        then on their containers, recursively.

    Returns:
      A tuple (proceed_with_standard_apply, transformed value)
    """
    return _container_custom_apply(
        self, path, value_spec, allow_partial, pg_typing.List
    )

sym_jsonify

sym_jsonify(
    use_inferred: bool = False, omit_symbolic_marker: bool = True, **kwargs
) -> WireValue

Converts current list to a list of plain Python objects.

Source code in pygx/symbolic/_list.py
def sym_jsonify(
    self,
    use_inferred: bool = False,
    omit_symbolic_marker: bool = True,
    **kwargs,
) -> wire.WireValue:
    """Converts current list to a list of plain Python objects."""
    # Notes on the implementation choices:
    #   * `list.__iter__(self)` skips `sym_getattr(idx)` per element
    #     (which would dispatch through `sym_hasattr` + `_sym_getattr`).
    #   * The primitive short-circuit (`v.__class__ in _PRIM`) inlines
    #     what `wire.to_json` would do anyway, saving a Python call
    #     and a `**kwargs` spread per leaf.
    #   * `use_inferred` is rare; keep it in its own branch so the
    #     common path stays a tight comprehension.
    _to_json = wire.to_json
    _PRIM = _PRIMITIVE_WIRE_TYPES
    kwargs['use_inferred'] = use_inferred
    kwargs['omit_symbolic_marker'] = omit_symbolic_marker
    if use_inferred:
        Inferential = base.Inferential
        sym_inferred = self.sym_inferred
        json_value = []
        for i, v in enumerate(list.__iter__(self)):
            if isinstance(v, Inferential):
                v = sym_inferred(i, default=v)
            json_value.append(
                v if v.__class__ in _PRIM else _to_json(v, **kwargs)
            )
    else:
        json_value = [
            v if v.__class__ in _PRIM else _to_json(v, **kwargs)
            for v in list.__iter__(self)
        ]
    if not omit_symbolic_marker:
        json_value.insert(0, wire.WireConvertible.SYMBOLIC_MARKER)
    return json_value

format

format(
    compact: bool = False,
    verbose: bool = True,
    root_indent: int = 0,
    *,
    python_format: bool = False,
    use_inferred: bool = False,
    cls_name: str | None = None,
    bracket_type: BracketType = SQUARE,
    **kwargs
) -> str

Formats this List.

Source code in pygx/symbolic/_list.py
def format(
    self,
    compact: bool = False,
    verbose: bool = True,
    root_indent: int = 0,
    *,
    python_format: bool = False,
    use_inferred: bool = False,
    cls_name: str | None = None,
    bracket_type: formatting.BracketType = formatting.BracketType.SQUARE,
    **kwargs,
) -> str:
    """Formats this List."""

    def _indent(text, indent):
        return ' ' * 2 * indent + text

    cls_name = cls_name or ''
    open_bracket, close_bracket = formatting.bracket_chars(bracket_type)
    s = [f'{cls_name}{open_bracket}']
    # Hoist per-child kwargs out of the loop — they don't vary per element.
    child_kwargs = {
        'python_format': python_format,
        'use_inferred': use_inferred,
        **kwargs,
    }
    child_root_indent = root_indent + 1
    if compact:
        kv_strs = []
        for idx, elem in self.sym_items():
            if use_inferred and isinstance(elem, base.Inferential):
                elem = self.sym_inferred(idx, default=elem)
            v_str = formatting.format(
                elem,
                compact,
                verbose,
                child_root_indent,
                **child_kwargs,
            )
            if python_format:
                kv_strs.append(v_str)
            else:
                kv_strs.append(f'{idx}: {v_str}')
        s.append(', '.join(kv_strs))
        s.append(close_bracket)
    else:
        if self:
            for idx, elem in self.sym_items():
                if use_inferred and isinstance(elem, base.Inferential):
                    elem = self.sym_inferred(idx, default=elem)
                if idx == 0:
                    s.append('\n')
                else:
                    s.append(',\n')
                v_str = formatting.format(
                    elem,
                    compact,
                    verbose,
                    child_root_indent,
                    **child_kwargs,
                )
                if python_format:
                    s.append(_indent(v_str, child_root_indent))
                else:
                    s.append(_indent(f'{idx} : {v_str}', child_root_indent))
            s.append('\n')
            s.append(_indent(close_bracket, root_indent))
        else:
            s.append(close_bracket)
    return ''.join(s)

Object

Object(
    *,
    allow_partial: bool = False,
    sealed: bool | None = None,
    root_path: KeyPath | None = None,
    explicit_init: bool = False,
    **kwargs: Any
)

Bases: Symbolic

Base class for symbolic user classes.

PyGX allow symbolic programming interfaces to be easily added to most Python classes in two ways:

  • Developing a dataclass-like symbolic class by subclassing pg.Object.
  • Developing a class as usual and decorate it using pygx.symbolize. This also work with existing classes.

By directly subclassing pg.Object, programmers can create new symbolic classes with the least effort. For example::

class Greeting(pg.Object): # Each annotated class attribute defines a symbolic field for __init__. name: str | None # Name to greet time_of_day: pg.typing.Enum( # Time of the day. ['morning', 'afternoon', 'evening'], 'morning')

def __call__(self):
  # Values for symbolic fields can be accessed
  # as public data members of the symbolic object.
  print(f'Good {self.time_of_day}, {self.name}')

# Create an object of Greeting and invoke it, # which shall print 'Good morning, Bob'. Greeting('Bob')()

Symbolic fields can be inherited from the base symbolic class: the fields from the base class will be copied to the subclass in their declaration order, while the subclass can override the inherited fields with more restricted validation rules or different default values. For example::

class Foo(pg.Object): x: pg.typing.Int(max_value=10) y: pg.typing.Float(min_value=0)

class Bar(Foo): x: pg.typing.Int(min_value=1, default=1) z: str | None

# Printing Bar's schema will show that there are 3 parameters defined: # x : pg.typing.Int(min_value=1, max_value=10, default=1)) # y : pg.typing.Float(min_value=0) # z : pg.typing.Str().noneable() print(Bar.schema)

Overriding only a default on a subclass. When a subclass overrides an inherited field's default, always re-annotate the name so static type checkers stay in sync::

class Message(pg.Object): text: str sender: str = pg.MISSING_VALUE # required at construction time

class UserMessage(Message): sender: str = 'User' # re-annotate — pyright sees this

A bare assignment (sender = 'User') works at runtime — pygx still rewrites the default — but PEP 681 dataclass_transform synthesizes __init__ from class-level annotations only, so pyright would still see sender as required. pygx emits a UserWarning at class creation when it detects this pattern; silence it via pg.warn_on_field_override_no_annotation(False).

Create an Object instance.

pg.Object synthesizes a keyword-only __init__. Subclasses that need positional arguments must override __init__ explicitly and translate to keyword arguments before forwarding to super().__init__(**kwargs).

Parameters:

Name Type Description Default
allow_partial bool

If True, the object can be partial.

False
sealed bool | None

If True, seal the object from future modification (unless under a pg.as_sealed(False) context manager). If False, treat the object as unsealed. If None, it's determined by cls.__sym_options__.frozen.

None
root_path KeyPath | None

The symbolic path for current object. By default it's None, which indicates that newly constructed object does not have a parent.

None
explicit_init bool

Should set to True when __init__ is called via pg.Object.__init__ instead of super().__init__.

False
**kwargs Any

key/value arguments that align with the schema. All required keys in the schema must be specified, and values should be acceptable according to their value spec.

{}

Raises:

Type Description
KeyError

When required key(s) are missing.

ValueError

When value(s) are not acceptable by their value spec.

Source code in pygx/symbolic/_object.py
@_filter_typing_traceback_on_raise
def __init__(
    self,
    *,
    allow_partial: bool = False,
    sealed: bool | None = None,
    root_path: topology.KeyPath | None = None,
    explicit_init: bool = False,
    **kwargs: Any,
):
    """Create an Object instance.

    ``pg.Object`` synthesizes a keyword-only ``__init__``. Subclasses
    that need positional arguments must override ``__init__``
    explicitly and translate to keyword arguments before forwarding
    to ``super().__init__(**kwargs)``.

    Args:
      allow_partial: If True, the object can be partial.
      sealed: If True, seal the object from future modification (unless under
        a `pg.as_sealed(False)` context manager). If False, treat the object as
        unsealed. If None, it's determined by `cls.__sym_options__.frozen`.
      root_path: The symbolic path for current object. By default it's None,
        which indicates that newly constructed object does not have a parent.
      explicit_init: Should set to `True` when `__init__` is called via
        `pg.Object.__init__` instead of `super().__init__`.
      **kwargs: key/value arguments that align with the schema. All required
        keys in the schema must be specified, and values should be acceptable
        according to their value spec.

    Raises:
      KeyError: When required key(s) are missing.
      ValueError: When value(s) are not acceptable by their value spec.
    """
    # Placeholder for Google-internal usage instrumentation.

    cls = self.__class__
    # The ever-instantiated latch `apply_schema`'s post-instance guard
    # reads (#432: `update_schema` is only legal before instances exist).
    # Own-`__dict__`, latched on the construction ATTEMPT — mirroring the
    # native core, whose `__native_ctor_meta__` (the equivalent marker) is
    # created on the first instance op. One dict read per construct.
    meta = cls.__sym_meta__
    if not meta.instantiated:
        meta.instantiated = True
    if sealed is None:
        sealed = cls.__sym_options__.frozen

    if not isinstance(allow_partial, bool):
        raise TypeError(
            f"Expect bool type for argument 'allow_partial' in "
            f'symbolic.Object.__init__ but encountered {allow_partial}.'
        )

    # Fast path: const-keyed schema.
    if cls._sym_init_fields is not None and self._init_fast_path(
        kwargs,
        allow_partial,
        sealed,
        root_path,
        explicit_init,
    ):
        return

    # Slow path: handles varargs and schemas with non-const keys.

    # Model before-hook (#516) — the fast path was not taken
    # (`_sym_init_fields is None`), so the hook has not run yet.
    if cls._sym_preinit_overridden:
        kwargs = _apply_preinit(cls, kwargs)

    # We delay the seal attempt until members are all set.
    super().__init__(
        allow_partial=allow_partial,
        accessor_writable=cls.__sym_options__.attr_write,
        sealed=sealed,
        root_path=root_path,
        init_super=not explicit_init,
    )

    # Fill field_args and init_args from **kwargs.
    _, unmatched_keys = cls.__schema__.resolve(list(kwargs.keys()))
    # Fields opted out of `__init__` via `enable_init=False` resolve
    # against the schema but must still be rejected as kwargs.
    get_field = cls.__schema__.get_field
    unmatched_keys = list(unmatched_keys) + [
        k
        for k in kwargs
        if (f := get_field(k)) is not None and not f.enable_init
    ]
    if unmatched_keys:
        _screen_unexpected_init_keys(cls, kwargs, unmatched_keys)

    # Declared aliases are RESERVED wire spellings (#517, owner ruling):
    # an open (varkw) schema must not accept an extra kwarg equal to a
    # field's alias — the wire would become ambiguous. (Const-keyed
    # schemas never reach here with an alias-spelled kwarg; their
    # unknown-key screen already raised — or, under `varkw='ignore'`,
    # raised for alias keys before dropping the rest.)
    if cls._sym_alias_maps is not None:
        alias_to_name = cls._sym_alias_maps[0]
        for k in kwargs:
            if k in alias_to_name:
                raise TypeError(
                    f'{cls.__name__}.__init__() got keyword argument '
                    f'{k!r}, which is reserved as the alias of field '
                    f'{alias_to_name[k]!r}.'
                )

    # All field values come from kwargs — `pg.Object.__init__` is
    # keyword-only.
    field_args = dict(kwargs)

    # Check missing arguments when partial binding is disallowed.
    if not base.accepts_partial(self):
        missing_args = []
        for field in self.__class__.__schema__.fields.values():
            if (
                not field.value.has_default
                and isinstance(field.key, pg_typing.ConstStrKey)
                and field.key not in field_args
            ):
                missing_args.append(str(field.key))
        if missing_args:
            arg_phrase = formatting.auto_plural(
                len(missing_args), 'argument'
            )
            keys_str = formatting.comma_delimited_str(missing_args)
            raise TypeError(
                f'{self.__class__.__name__}.__init__() missing {len(missing_args)} '
                f'required {arg_phrase}: {keys_str}.'
            )

    # NOTE(daiyip): Accessor writable is honored by `Object.__setattr__` so
    # we can always make `_sym_attributes` accessor writable. This prevents
    # a child object's attribute access from being changed when it's
    # attached to a parent whose symbolic attributes are not writable.
    # Per-field validation is gated by `field.enable_validation` inside
    # `Schema.apply` / `Dict._formalized_value` — so a class-wide
    # `validate=False` (resolved into each field) and per-field overrides
    # both flow through naturally here.
    vars(self)['_sym_attributes'] = pg_dict.Dict(
        field_args,  # pyright: ignore[reportArgumentType]
        value_spec=self.__class__.sym_fields,
        allow_partial=allow_partial,
        sealed=sealed,
        accessor_writable=True,
        root_path=root_path,
        as_object_attributes_container=True,
        under_sym_mode=cls.__sym_options__.sym,
    )
    self._sym_attributes.sym_setparent(self)
    # Seed `init=False` fields from their defaults before `on_sym_post_init`
    # runs so `on_sym_ready`/`on_sym_bound` overrides start from a known baseline.
    _seed_non_symbolic_fields(self, cls._non_symbolic_fields)
    # Model after-validator (#516): fires before any lifecycle hook can
    # observe the object, gated like `on_sym_ready` (concrete only).
    if cls._sym_validate_overridden:
        abstract = (
            self._allow_partial and self.sym_partial
        ) or self.sym_puresymbolic
        if not abstract:
            self.on_sym_validate()
    self.on_sym_post_init()
    self.sym_seal(sealed)

sym_init_args property

sym_init_args: dict[str, Any]

Returns the symbolic attributes which are also the __init__ args.

Returns:

Type Description
dict[str, Any]

A plain dict of the evaluated symbolic attributes, in schema order. Every inferential value (e.g. pg.symbolic.ValueFromParentChain) that CAN be resolved from the object's context is resolved; an unresolvable one is included as the raw inferential (so the property never raises on a context-partial object — resolution errors stay at value-access time, exactly as they did on the former live view). A missing value is included as MISSING_VALUE. The dict is a SHALLOW snapshot: adding/replacing top-level keys does not rebind the object (use sym_rebind), while nested symbolic values are the live children. (This property used to return the live _sym_attributes pg.Dict view; attribute-style access such as obj.sym_init_args.x becomes obj.sym_init_args['x'].)

sym_partial property

sym_partial: bool

Whether any field (here or in a child) is unbound.

Inline sym=False fast path: read the native inline fields and decide directly, WITHOUT materializing into _sym_attributes. The base Symbolic.sym_partial is bool(self.sym_missing(flatten=False)), and Object.sym_missing reads _sym_attributes — which materializes the object. The Object value-spec's apply reads sym_partial on every Object-typed field value, so on the construct hot path that materialized every nested sym=False child (the dominant nested-ctor cost). An inline object stores every field (an unset one as a present MISSING_VALUE), so sym_partial is: any field is MISSING_VALUE, or any symbolic child is itself partial (matching sym_missing(flatten=False)'s recursion — a child's sym_partial is bool(child.sym_missing(flatten=False))). The child's sym_partial recurses through this same inline path, so a nested clean tree never materializes.

partial classmethod

partial(**kwargs) -> Self

Class method that creates a partial object of current class.

Source code in pygx/symbolic/_object.py
@classmethod
def partial(cls, **kwargs) -> Self:
    """Class method that creates a partial object of current class."""
    return cls(allow_partial=True, **kwargs)

from_json classmethod

from_json(
    json_value: Any,
    *,
    allow_partial: bool = False,
    root_path: KeyPath | None = None,
    **kwargs: Any
) -> Self

Class method that load an symbolic Object from a JSON value.

Example::

class Foo(pg.Object):
  f1: int
  f2: pg.typing.Dict([
      ('f21', pg.typing.Bool())
  ])

foo = Foo.from_json({
    'f1': 1,
    'f2': {
      'f21': True
    }
  })

# or

foo2 = symbolic.from_json({
    '_type': '__main__.Foo',
    'f1': 1,
    'f2': {
      'f21': True
    }
})

assert foo == foo2

Parameters:

Name Type Description Default
json_value Any

Input JSON value, only JSON dict is acceptable.

required
allow_partial bool

Whether to allow elements of the list to be partial.

False
root_path KeyPath | None

KeyPath of loaded object in its object tree.

None
**kwargs Any

Additional keyword arguments to pass through.

{}

Returns:

Type Description
Self

A symbolic Object instance.

Source code in pygx/symbolic/_object.py
@classmethod
def from_json(
    cls,
    json_value: Any,
    *,
    allow_partial: bool = False,
    root_path: topology.KeyPath | None = None,
    **kwargs: Any,
) -> Self:
    """Class method that load an symbolic Object from a JSON value.

    Example::

        class Foo(pg.Object):
          f1: int
          f2: pg.typing.Dict([
              ('f21', pg.typing.Bool())
          ])

        foo = Foo.from_json({
            'f1': 1,
            'f2': {
              'f21': True
            }
          })

        # or

        foo2 = symbolic.from_json({
            '_type': '__main__.Foo',
            'f1': 1,
            'f2': {
              'f21': True
            }
        })

        assert foo == foo2

    Args:
      json_value: Input JSON value, only JSON dict is acceptable.
      allow_partial: Whether to allow elements of the list to be partial.
      root_path: KeyPath of loaded object in its object tree.
      **kwargs: Additional keyword arguments to pass through.

    Returns:
      A symbolic Object instance.
    """
    # Convert child values in place (rather than building a fresh dict
    # via comprehension and unpacking it as kwargs). `json_value` will
    # be consumed by `cls(...)` below.
    #
    # Raw-passthrough fast path: a field whose value spec is
    # `from_json`-trivial (a primitive scalar, or a `List` / `Dict`
    # recursively of primitives) has plain children that deserialize to
    # themselves AND that the constructor wraps + validates in one pass.
    # Pre-converting them via `base.from_json` builds a schemaless
    # container the constructor would then re-wrap — a full double build.
    # So hand a plain child straight to `cls(...)`.
    #
    # Two guards keep this sound: the child must pass `_is_plain_json_child`
    # (a symbolic substitute — a contextual `Placeholder`, a hyper-value,
    # a `{"_type": ...}` object — can stand in for ANY typed field and must
    # still be resolved), and there must be no `context` (a shared
    # `{"__ref__": idx}` child is dereferenced only by `base.from_json`).
    # Wire-layer aliases (#517): remap alias keys to field names BEFORE
    # the raw-safe scan, so remapped keys still take the fast paths.
    # Both spellings for the same field is ambiguous input — raise.
    alias_maps = cls._sym_alias_maps
    if alias_maps is not None:
        alias_to_name = alias_maps[0]
        for k in list(json_value):
            name = alias_to_name.get(k)
            if name is not None and name != k:
                if name in json_value:
                    raise ValueError(
                        f'Duplicate key for field {name!r} of '
                        f'{cls.__name__!r}: both the field name and its '
                        f'alias {k!r} are present.'
                    )
                json_value[name] = json_value.pop(k)

    # `varkw='ignore'` (#518): drop unknown wire keys BEFORE converting
    # child values — a junk payload may carry an unknown `_type` (which
    # `base.from_json` would raise on) or a known one (which would be
    # fully constructed, hooks and all, then discarded). Runs after the
    # alias remap so remapped keys classify by field name; the `__init__`
    # screen stays the backstop for the keys this pass keeps. An
    # `'ignore'` class is always const-keyed (class creation rejects the
    # posture on an open schema), so `_fields` membership is exactly the
    # `__init__` screen's test.
    if cls.__varkw_ignore__:
        schema_fields = (
            cls.__schema__._fields  # pylint: disable=protected-access
        )
        unmatched = [
            k
            for k in json_value
            if (f := schema_fields.get(k)) is None or not f.enable_init
        ]
        if unmatched:
            _screen_unexpected_init_keys(cls, json_value, unmatched)

    _from_json = base.from_json
    _prim = base.PRIMITIVE_TYPES
    has_context = kwargs.get('context') is not None
    if not has_context:
        # Inline the per-class cache read (skip the `_cached_...` call frame on
        # the hot hit path); compute + cache only on the first-seen miss. The
        # sentinel MUST be `_UNSET` (not `None`): the base `Object` declares the
        # attribute as `= _UNSET`, so a `None` default would mis-read it as
        # already-computed and pass `_UNSET` to the scan.
        raw_safe = cls.__dict__.get('_from_json_raw_safe_fields', _UNSET)
        if raw_safe is _UNSET:
            raw_safe = _cached_from_json_raw_safe_fields(cls)
    else:
        # A shared `{"__ref__": idx}` child is dereferenced only by
        # `base.from_json`, so the raw-passthrough is unsafe under a context.
        raw_safe = frozenset()
    # Native whole-object fast path: when EVERY child is a raw-safe or
    # no-wrap field's plain JSON value (no marker substitute / ref / nested
    # typed object), the object deserializes by `cls(**json_value)` directly
    # — one native scan replaces the Python per-key loop + the per-field
    # `_is_plain_json_child` walk (the dominant container-from_json cost).
    # A plain child of a no-wrap field passes through identically (the
    # constructor keeps it raw), so the scan set is the CACHED per-class
    # union (a per-call `raw_safe | no_wrap` costs ~+25% on a flat
    # from-dict — the 2026-07-15 perf-report catch) — but only without a
    # `context` (a shared `__ref__` child must dereference through
    # `base.from_json`, mirroring the raw-safe gate above, so the context
    # arm falls back to the bare `raw_safe`, empty there by construction).
    # `None`/`False` ⇒ the general loop below (which converts the specific
    # non-plain children). The scan owns its own eligibility (seam
    # decline): a non-exact-`dict` input returns `False` there, not a
    # pre-check here.
    native_all_plain = _native.from_json_all_plain
    if native_all_plain is not None:  # pragma: no cover  (native core only)
        if not has_context:
            scan_fields = cls.__dict__.get('_from_json_scan_fields', _UNSET)
            if scan_fields is _UNSET:
                scan_fields = raw_safe | _cached_from_json_no_wrap_fields(
                    cls
                )
                cls._from_json_scan_fields = scan_fields
        else:
            scan_fields = raw_safe
        if scan_fields and native_all_plain(json_value, scan_fields):
            return cls(
                allow_partial=allow_partial,
                root_path=root_path,
                **json_value,
            )
    # No-wrap fields (#541): a field whose resolved `enable_wrap` is False
    # (every init field of a `sym=False` class) keeps raw containers
    # verbatim under direct construction — the wire path must match, so
    # its children skip the `base.from_json` pre-conversion (which would
    # build a symbolic container the constructor then keeps as-is) and
    # take the raw-shell walk instead. Read AFTER the native scan — the
    # scan-hit path never consults it.
    no_wrap = cls.__dict__.get('_from_json_no_wrap_fields', _UNSET)
    if no_wrap is _UNSET:
        no_wrap = _cached_from_json_no_wrap_fields(cls)
    for k in json_value:
        v = json_value[k]
        # A scalar child of a raw-safe field is the common case — check it
        # inline (no `_is_plain_json_child` call) before the deeper walk a
        # container child needs.
        if k in raw_safe and (
            v.__class__ in _prim or _is_plain_json_child(v)
        ):
            continue
        if k in no_wrap:
            # The field's containers land verbatim (#541): resolve wire
            # markers with raw shells; primitives pass through untouched.
            if v.__class__ not in _prim:
                json_value[k] = _from_json_no_wrap_child(
                    v, allow_partial=allow_partial, **kwargs
                )
            continue
        json_value[k] = _from_json(v, allow_partial=allow_partial, **kwargs)
    return cls(
        allow_partial=allow_partial, root_path=root_path, **json_value
    )

on_sym_post_init

on_sym_post_init()

Event triggered at the end of __init__.

This is the construction-time entry point for the bind sequence: the default implementation triggers on_sym_bound (and then on_sym_ready once the object is concrete). An override that does not call super().on_sym_post_init() therefore suppresses on_sym_bound and on_sym_ready at construction — call super() unless that is intended. For most needs, prefer overriding on_sym_ready (or on_sym_bound) directly rather than this funnel.

Source code in pygx/symbolic/_object.py
def on_sym_post_init(self):
    """Event triggered at the end of ``__init__``.

    This is the construction-time entry point for the bind sequence: the
    default implementation triggers `on_sym_bound` (and then `on_sym_ready`
    once the object is concrete). An override that does not call
    ``super().on_sym_post_init()`` therefore suppresses `on_sym_bound` and
    `on_sym_ready` at construction — call ``super()`` unless that is
    intended. For most needs, prefer overriding `on_sym_ready` (or
    `on_sym_bound`) directly rather than this funnel.
    """
    self._trigger_bound()

on_sym_bound

on_sym_bound() -> None

Event triggered on each (re)bind, even when the subtree is partial.

Fires at the end of __init__ and at most once per rebind (no matter how many fields changed), regardless of whether every field is present. Use this only for logic that must run even on a partial or pure-symbolic object.

For the common case — (re)computing derived members from schema fields — override on_sym_ready instead: it fires right after on_sym_bound but only once the object is concrete, so all fields are guaranteed present.

When derived members are expensive to recompute, override on_sym_change instead: it receives the exact field_updates (both per-field mutations and batched rebinds, anywhere in the subtree) so you can refresh only the members impacted by the change.

Source code in pygx/symbolic/_object.py
def on_sym_bound(self) -> None:
    """Event triggered on each (re)bind, *even when the subtree is partial*.

    Fires at the end of `__init__` and at most once per rebind (no matter how
    many fields changed), regardless of whether every field is present. Use
    this only for logic that must run even on a partial or pure-symbolic
    object.

    For the common case — (re)computing derived members from schema fields —
    override `on_sym_ready` instead: it fires right after `on_sym_bound` but only
    once the object is concrete, so all fields are guaranteed present.

    When derived members are expensive to recompute, override `on_sym_change`
    instead: it receives the exact `field_updates` (both per-field mutations
    and batched rebinds, anywhere in the subtree) so you can refresh only
    the members impacted by the change.
    """

on_sym_ready

on_sym_ready() -> None

Event triggered after on_sym_bound once the object is concrete.

Fires at the end of __init__ and after each rebind, but only when the object is not abstract (not sym_abstract — neither partial nor pure-symbolic). Unlike on_sym_bound, every schema field is guaranteed present, making this the natural place to set derived members.

It is level-triggered: it fires on every (re)bind that leaves the object concrete, not only on the abstract -> concrete transition. For one-shot setup, guard with your own flag.

Source code in pygx/symbolic/_object.py
def on_sym_ready(self) -> None:
    """Event triggered after `on_sym_bound` once the object is concrete.

    Fires at the end of `__init__` and after each rebind, but only when the
    object is not abstract (``not sym_abstract`` — neither partial nor
    pure-symbolic). Unlike `on_sym_bound`, every schema field is guaranteed
    present, making this the natural place to set derived members.

    It is level-triggered: it fires on every (re)bind that leaves the object
    concrete, not only on the abstract -> concrete transition. For one-shot
    setup, guard with your own flag.
    """

on_sym_preinit classmethod

on_sym_preinit(kwargs: dict[str, Any]) -> dict[str, Any]

Model-level before hook: transform raw init kwargs (#516).

Called on every fresh construct (including from_json) with the raw field kwargs — before unknown-key/missing-required resolution — and must return the mapping to construct from (mutating and returning kwargs is fine). The reserved init args (allow_partial / sealed / root_path / explicit_init) are never included. Not called on clones (their values come formalized from a validated source) nor on rebinds (per-field transform is the write-path before-hook).

The default implementation returns kwargs unchanged.

Source code in pygx/symbolic/_object.py
@classmethod
def on_sym_preinit(cls, kwargs: dict[str, Any]) -> dict[str, Any]:
    """Model-level *before* hook: transform raw init kwargs (#516).

    Called on every fresh construct (including ``from_json``) with the
    raw field kwargs — before unknown-key/missing-required resolution —
    and must return the mapping to construct from (mutating and
    returning ``kwargs`` is fine). The reserved init args
    (``allow_partial`` / ``sealed`` / ``root_path`` / ``explicit_init``)
    are never included. Not called on clones (their values come
    formalized from a validated source) nor on rebinds (per-field
    ``transform`` is the write-path before-hook).

    The default implementation returns ``kwargs`` unchanged.
    """
    return kwargs

on_sym_validate

on_sym_validate() -> None

Model-level after validator: check cross-field invariants (#516).

Called with all fields validated and committed — raise to reject. Unlike on_sym_ready (a lifecycle notification), a raise here is part of validation:

  • Construct: fires after fields are stored, before on_sym_post_init / on_sym_bound / on_sym_ready, and only when the object is concrete (the on_sym_ready gate) — a failing check aborts the construct.
  • Writes (self.x = ... / sym_rebind): fires after the update commits and before any on_sym_change notification; a raise rolls the update back (observers never see the invalid state) and propagates to the writer. It also fires when notifications are suppressed (pg.notify_on_change(False) / skip_notification=True) — those flags mute observers, never validation.

Validate only — do not mutate. A write from inside this hook re-enters validation (an unconditionally-writing hook recurses without bound) and escapes the failing write's rollback. Normalize values with the field-level transform, and compute derived state in on_sym_ready / on_sym_change.

The default implementation is a no-op.

Source code in pygx/symbolic/_object.py
def on_sym_validate(self) -> None:
    """Model-level *after* validator: check cross-field invariants (#516).

    Called with all fields validated and committed — raise to reject.
    Unlike `on_sym_ready` (a lifecycle notification), a raise here is
    part of validation:

    * **Construct:** fires after fields are stored, before
      `on_sym_post_init` / `on_sym_bound` / `on_sym_ready`, and only
      when the object is concrete (the `on_sym_ready` gate) — a
      failing check aborts the construct.
    * **Writes** (``self.x = ...`` / ``sym_rebind``): fires after the
      update commits and *before* any `on_sym_change` notification;
      a raise rolls the update back (observers never see the invalid
      state) and propagates to the writer. It also fires when
      notifications are suppressed (``pg.notify_on_change(False)`` /
      ``skip_notification=True``) — those flags mute observers, never
      validation.

    **Validate only — do not mutate.** A write from inside this hook
    re-enters validation (an unconditionally-writing hook recurses
    without bound) and escapes the failing write's rollback. Normalize
    values with the field-level ``transform``, and compute derived
    state in `on_sym_ready` / `on_sym_change`.

    The default implementation is a no-op.
    """

on_sym_change

on_sym_change(field_updates: dict[KeyPath, FieldUpdate]) -> None

Event that is triggered when field values in the subtree are updated.

This event will be called * On per-field basis when object is modified via attribute. * In batch when multiple fields are modified via sym_rebind method.

When a field in an object tree is updated, all ancestors' on_sym_change event will be triggered in order, from the nearest one to furthest one.

The default implementation triggers on_sym_bound (and then on_sym_ready when the object is concrete).

Any functools.cached_property (including the pg.typing.cached_property shim) declared on the class is invalidated automatically just before this event fires, so a cached value can never outlive a change to the fields it derives from. The invalidation is a framework guarantee — it runs whether or not this method is overridden, and whether or not an override calls super().on_sym_change(...) — so subclasses no longer need to pop their cached values by hand.

Parameters:

Name Type Description Default
field_updates dict[KeyPath, FieldUpdate]

Updates made to the subtree. Key path is relative to current object.

required
Source code in pygx/symbolic/_object.py
def on_sym_change(
    self, field_updates: dict[topology.KeyPath, base.FieldUpdate]
) -> None:
    """Event that is triggered when field values in the subtree are updated.

    This event will be called
      * On per-field basis when object is modified via attribute.
      * In batch when multiple fields are modified via `sym_rebind` method.

    When a field in an object tree is updated, all ancestors' `on_sym_change` event
    will be triggered in order, from the nearest one to furthest one.

    The default implementation triggers `on_sym_bound` (and then `on_sym_ready`
    when the object is concrete).

    Any `functools.cached_property` (including the `pg.typing.cached_property`
    shim) declared on the class is invalidated automatically just before this
    event fires, so a cached value can never outlive a change to the fields it
    derives from. The invalidation is a framework guarantee — it runs whether or
    not this method is overridden, and whether or not an override calls
    `super().on_sym_change(...)` — so subclasses no longer need to pop their
    cached values by hand.

    Args:
      field_updates: Updates made to the subtree. Key path is relative to
        current object.
    """
    del field_updates
    self._trigger_bound()

on_sym_path_change

on_sym_path_change(
    old_path: str | KeyPath | None, new_path: str | KeyPath | None
)

Event that is triggered after the symbolic path changes.

Source code in pygx/symbolic/_object.py
def on_sym_path_change(
    self,
    old_path: str | topology.KeyPath | None,
    new_path: str | topology.KeyPath | None,
):
    """Event that is triggered after the symbolic path changes."""
    del old_path, new_path

on_sym_parent_change

on_sym_parent_change(
    old_parent: TopologyAware | None, new_parent: TopologyAware | None
)

Event that is triggered after the symbolic parent changes.

Source code in pygx/symbolic/_object.py
def on_sym_parent_change(
    self,
    old_parent: base.TopologyAware | None,
    new_parent: base.TopologyAware | None,
):
    """Event that is triggered after the symbolic parent changes."""
    del old_parent, new_parent

sym_hasattr

sym_hasattr(key: str | int) -> bool

Tests if a symbolic attribute exists.

Source code in pygx/symbolic/_object.py
def sym_hasattr(self, key: str | int) -> bool:
    """Tests if a symbolic attribute exists."""
    if key == '_sym_attributes':
        raise ValueError(
            f'{self.__class__.__name__}.__init__ should call `super().__init__`.'
        )
    # Inline (v2 P4): test membership against the inline `fields` directly — no
    # materialization (mirrors `sym_getattr`). This keeps a nested `sym_rebind`'s
    # `KeyPath.query` descent (`sym_hasattr` → `sym_getattr` per level) inline.
    # `None` ⇒ a materialized object / python core → `_sym_attributes`.
    attrs = _inline_object_fields(self)
    if attrs is None:
        attrs = self._sym_attributes
    return isinstance(key, str) and not key.startswith('_') and key in attrs

sym_getattr

sym_getattr(key: str | int, default: Any = RAISE_IF_NOT_FOUND) -> Any

Gets a symbolic attribute (Object-specific fast path).

Symbolic.sym_getattr would dispatch sym_hasattr then _sym_getattr and round-trip through the attrs Dict's own dispatchers. Inline that here: one _sym_attributes lookup, one membership check, one dict.__getitem__.

Source code in pygx/symbolic/_object.py
def sym_getattr(
    self, key: str | int, default: Any = base.RAISE_IF_NOT_FOUND
) -> Any:
    """Gets a symbolic attribute (Object-specific fast path).

    `Symbolic.sym_getattr` would dispatch `sym_hasattr` then `_sym_getattr`
    and round-trip through the attrs Dict's own dispatchers. Inline that
    here: one `_sym_attributes` lookup, one membership check, one
    `dict.__getitem__`.
    """
    if key == '_sym_attributes':
        raise ValueError(
            f'{self.__class__.__name__}.__init__ should call `super().__init__`.'
        )
    # Inline (v2 P4): read the raw value straight from the inline `fields`
    # (same key → raw-value mapping the `__sym_dict__` exposes) — no
    # materialization. `None` ⇒ a materialized object / python core →
    # `_sym_attributes`.
    attrs = _inline_object_fields(self)
    if attrs is None:
        attrs = self._sym_attributes
    if isinstance(key, str) and not key.startswith('_') and key in attrs:
        return dict.__getitem__(attrs, key)
    if default is base.RAISE_IF_NOT_FOUND:
        raise AttributeError(
            self._error_message(
                f'{self.__class__!r} object has no symbolic attribute {key!r}.'
            )
        )
    return default

sym_attr_field

sym_attr_field(key: str | int) -> Field | None

Returns the field definition for a symbolic attribute.

Source code in pygx/symbolic/_object.py
def sym_attr_field(self, key: str | int) -> pg_typing.Field | None:
    """Returns the field definition for a symbolic attribute."""
    # Read the field straight from the class's symbolic-field schema — the SAME
    # `cls.sym_fields` the `__sym_dict__`'s value spec carries — so this never
    # materializes (v2: the inner Dict's `sym_attr_field` is just
    # `_value_spec.schema.get_field`, and `_value_spec is cls.sym_fields`).
    sym_fields = type(self).sym_fields
    schema = sym_fields.schema if sym_fields is not None else None
    return schema.get_field(key) if schema is not None else None

sym_keys

sym_keys() -> Iterator[str]

Iterates the keys of symbolic attributes.

Source code in pygx/symbolic/_object.py
def sym_keys(self) -> Iterator[str]:
    """Iterates the keys of symbolic attributes."""
    fields = _inline_object_fields(self)
    if fields is not None:
        return iter(fields.keys())
    return self._sym_attributes.sym_keys()

sym_values

sym_values()

Iterates the values of symbolic attributes.

Source code in pygx/symbolic/_object.py
def sym_values(self):
    """Iterates the values of symbolic attributes."""
    fields = _inline_object_fields(self)
    if fields is not None:
        return iter(fields.values())
    return self._sym_attributes.sym_values()

sym_items

sym_items()

Iterates the (key, value) pairs of symbolic attributes.

Source code in pygx/symbolic/_object.py
def sym_items(self):
    """Iterates the (key, value) pairs of symbolic attributes."""
    fields = _inline_object_fields(self)
    if fields is not None:
        return iter(fields.items())
    return self._sym_attributes.sym_items()

sym_eq

sym_eq(other: Any) -> bool

Tests symbolic equality.

Source code in pygx/symbolic/_object.py
def sym_eq(self, other: Any) -> bool:
    """Tests symbolic equality."""
    return self is other or (
        type(self) is type(other)
        and base.eq(self._sym_attributes, other._sym_attributes)
    )  # pylint: disable=protected-access

sym_lt

sym_lt(other: Any) -> bool

Tests symbolic less-than.

Same-type comparison is lexicographic over the enable_compare fields in declaration order — the same fields that feed sym_eq. This keeps sym_eq / sym_lt / sym_gt mutually coherent: for two same-type values exactly one of == / < / > holds (trichotomy), and a compare=False field is ignored by all three. Cross-type comparison defers to base.lt, which orders by type bucket (see pygx.lt).

Source code in pygx/symbolic/_object.py
def sym_lt(self, other: Any) -> bool:
    """Tests symbolic less-than.

    Same-type comparison is lexicographic over the `enable_compare`
    fields in declaration order — the *same* fields that feed `sym_eq`.
    This keeps `sym_eq` / `sym_lt` / `sym_gt` mutually coherent: for two
    same-type values exactly one of `==` / `<` / `>` holds (trichotomy),
    and a `compare=False` field is ignored by all three. Cross-type
    comparison defers to `base.lt`, which orders by type bucket (see
    [`pygx.lt`][pygx.symbolic.lt]).
    """
    if type(self) is not type(other):
        return base.lt(self, other)
    return _seq_lt(self._sym_order_values(), other._sym_order_values())  # pylint: disable=protected-access

sym_hash

sym_hash() -> int

Symbolically hashing.

Source code in pygx/symbolic/_object.py
def sym_hash(self) -> int:
    """Symbolically hashing."""
    # `base.sym_hash` on this tuple would just call `hash(tuple)` — inline
    # it. The inner `sym_hash` call stays since the attrs Dict is Symbolic.
    return hash((self.__class__, self._sym_attributes.sym_hash()))

sym_setparent

sym_setparent(parent: TopologyAware | None)

Sets the parent of current node in the symbolic tree.

Available only when sym=True; a sym=False object is a flat, reference-held leaf with no tree to attach to and raises SymbolicModeError.

Source code in pygx/symbolic/_object.py
def sym_setparent(self, parent: base.TopologyAware | None):
    """Sets the parent of current node in the symbolic tree.

    Available only when ``sym=True``; a ``sym=False`` object is a flat,
    reference-held leaf with no tree to attach to and raises
    ``SymbolicModeError``.
    """
    self._require_sym_mode('sym_setparent')
    # Skip the `super().sym_setparent` indirection — this is hit on every
    # adoption of this object as a child.
    d = vars(self)
    old_parent = d.get('_sym_parent')
    d['_sym_parent'] = parent
    if old_parent is not parent:
        self.on_sym_parent_change(old_parent, parent)

sym_setpath

sym_setpath(path: str | KeyPath | None) -> None

Sets the path of current node in its symbolic tree.

Available only when sym=True; a sym=False object has no tree position (sym_path is always the empty KeyPath()) and raises SymbolicModeError.

Source code in pygx/symbolic/_object.py
def sym_setpath(self, path: str | topology.KeyPath | None) -> None:
    """Sets the path of current node in its symbolic tree.

    Available only when ``sym=True``; a ``sym=False`` object has no tree
    position (``sym_path`` is always the empty ``KeyPath()``) and raises
    ``SymbolicModeError``.
    """
    self._require_sym_mode('sym_setpath')
    super().sym_setpath(path)

sym_missing

sym_missing(flatten: bool = True) -> dict[str | int, Any]

Returns missing values.

Source code in pygx/symbolic/_object.py
@override
def sym_missing(self, flatten: bool = True) -> dict[str | int, Any]:
    """Returns missing values."""

    def compute() -> dict[str | int, Any]:
        # Inline fast path: an inline object (a `slots=True` record in either
        # sym mode, or a dict-layout object in either sym mode whose storage is
        # still `Inline`) computes its missing structure from the native inline
        # `fields` WITHOUT materializing a `_sym_attributes` SymDict — the
        # record / non-symbolic-object memory win, via the same
        # `_inline_object_fields` reader the `sym_partial` property uses
        # (the caching here is `sym_missing`'s own, not shared). `None` ⇒ an
        # already-materialized object / python core → the general path below.
        fields = _inline_object_fields(self)
        if fields is not None:
            # `fields` is non-None only on the native core; the scan logic is
            # unit-tested directly via `_inline_sym_missing`.
            return _inline_sym_missing(  # pragma: no cover
                fields, type(self).__schema__
            )
        # Invalidate the cache of child attributes' missing values before
        # calling `Dict.sym_missing`. The cache lives in `__dict__` on both
        # cores (see `base.Symbolic._sym_cached`), so clear it via `vars`.
        vars(self._sym_attributes)['_sym_missing_values'] = None
        return self._sym_attributes.sym_missing(flatten=False)

    return self._sym_cached('_sym_missing_values', compute, flatten)

sym_nondefault

sym_nondefault(flatten: bool = True) -> dict[str, Any]

Returns non-default values.

Source code in pygx/symbolic/_object.py
@override
def sym_nondefault(self, flatten: bool = True) -> dict[str, Any]:  # pyright: ignore[reportIncompatibleMethodOverride]
    """Returns non-default values."""

    def compute() -> dict[str, Any]:
        # Inline fast path: an inline object (a `slots=True` record in either
        # sym mode, or a dict-layout object in either sym mode whose storage is
        # still `Inline`) computes its non-default structure from the native
        # inline `fields` WITHOUT materializing a `_sym_attributes` SymDict —
        # the record / non-symbolic-object memory win, via the same
        # `_inline_object_fields` reader `sym_missing` uses. `None` ⇒ an
        # already-materialized object / python core → the general path below.
        fields = _inline_object_fields(self)
        if fields is not None:
            # `fields` is non-None only on the native core; the scan logic is
            # unit-tested directly via `_inline_sym_nondefault`.
            return _inline_sym_nondefault(  # pragma: no cover
                fields, type(self).__schema__
            )
        # Invalidate the cache of child attributes' non-default values before
        # calling `Dict.sym_nondefault`. The cache lives in `__dict__` on both
        # cores (see `base.Symbolic._sym_cached`), so clear it via `vars`.
        vars(self._sym_attributes)['_sym_nondefault_values'] = None
        return self._sym_attributes.sym_nondefault(flatten=False)

    return self._sym_cached('_sym_nondefault_values', compute, flatten)

sym_seal

sym_seal(is_seal: bool = True) -> Self

Seal or unseal current object (recursing into fields).

Source code in pygx/symbolic/_object.py
def sym_seal(self, is_seal: bool = True) -> Self:
    """Seal or unseal current object (recursing into fields)."""
    # Inline (v2 P4): delegate entirely to the native `super().sym_seal`
    # (`SymObject.sym_seal` → `topo_seal`), which seals the inline symbolic
    # children directly + sets `node.sealed`, WITHOUT materializing. The
    # redundant `_sym_attributes.sym_seal` below (which the oracle needs,
    # because there `super().sym_seal` only sets the flag) would materialize,
    # so skip it for an inline object. `None` ⇒ a materialized object / python
    # core → the `_sym_attributes` recursion + flag.
    if (
        _inline_object_fields(self) is not None
    ):  # pragma: no cover  (rust only)
        super().sym_seal(is_seal)
        return self
    self._sym_attributes.sym_seal(is_seal)
    super().sym_seal(is_seal)
    return self

to_json

to_json(**kwargs) -> WireValue

Serializes to a plain-Python JSON value.

Fast path: a sym=False object whose field values are all plain — JSON primitives, or list / dict trees recursively of primitives with no container shared across fields / positions and no cycle — needs none of the wire.to_json shared-object context (allocation + per-object bookkeeping + the deref post-pass). Emit {_type, **fields} directly, with a deep plain-copy of each value — byte-identical to what wire.to_json would build, since with nothing aliased no __ref__ ever materializes. _plain_value_to_json shares one seen id-set across the fields so a container reached twice (or a cycle) bails the WHOLE object to wire.to_json, which emits the correct __ref__ / __context__ form.

Any genuinely symbolic value (a nested Object, a pg.List / pg.Dict, a pg.Ref, a hyper-value, a tuple), a shared / cyclic plain container, a sym=True object, or a shape-changing kwarg falls through to the general wire.to_json (which honors shared/cyclic-reference dedup, frozen / non-const fields, and the serialization options).

Dump options (#519, threaded through the recursion like by_alias): exclude_none=True drops None-valued mapping entries — object fields, pg.Dict entries, and RAW dict values alike (list items are never dropped); exclude_defaults=True drops fields equal to their schema default; exclude_frozen (default True) drops frozen fields; type_info=False omits the _type class marker everywhere — the plain-dict export (dataclasses.asdict analog; one-way — from_json of the result yields plain containers, not the original classes). Under type_info=False, markers that ARE the payload still emit: structural wire forms (__tuple__, __ref__), UnknownTypedObject / opaque-pickle _types, the pg.Ref wrapper, and classes with a custom sym_jsonify; an unresolved Inferential dumps to {}, so pair with use_inferred=True when dumping contextual trees. Pydantic's exclude_unset has no pygx equivalent — defaults materialize at construction, so provided-at-default and defaulted are indistinguishable afterwards.

Source code in pygx/symbolic/_object.py
def to_json(self, **kwargs) -> wire.WireValue:
    """Serializes to a plain-Python JSON value.

    Fast path: a `sym=False` object whose field values are all plain —
    JSON primitives, or `list` / `dict` trees recursively of primitives
    with no container shared across fields / positions and no cycle — needs
    none of the `wire.to_json` shared-object context (allocation +
    per-object bookkeeping + the deref post-pass). Emit `{_type, **fields}`
    directly, with a deep plain-copy of each value — byte-identical to what
    `wire.to_json` would build, since with nothing aliased no `__ref__` ever
    materializes. `_plain_value_to_json` shares one `seen` id-set across the
    fields so a container reached twice (or a cycle) bails the WHOLE object
    to `wire.to_json`, which emits the correct `__ref__` / `__context__`
    form.

    Any genuinely symbolic value (a nested `Object`, a `pg.List` / `pg.Dict`,
    a `pg.Ref`, a hyper-value, a tuple), a shared / cyclic plain container,
    a `sym=True` object, or a shape-changing kwarg falls through to the
    general `wire.to_json` (which honors shared/cyclic-reference dedup,
    frozen / non-const fields, and the serialization options).

    Dump options (#519, threaded through the recursion like `by_alias`):
    `exclude_none=True` drops `None`-valued mapping entries — object
    fields, `pg.Dict` entries, and RAW dict values alike (list items are
    never dropped); `exclude_defaults=True` drops fields equal to their
    schema default; `exclude_frozen` (default True) drops frozen fields;
    `type_info=False` omits the `_type` class marker
    everywhere — the plain-dict export (`dataclasses.asdict` analog;
    one-way — `from_json` of the result yields plain containers, not the
    original classes). Under `type_info=False`, markers that ARE the
    payload still emit: structural wire forms (`__tuple__`, `__ref__`),
    `UnknownTypedObject` / opaque-pickle `_type`s, the `pg.Ref` wrapper,
    and classes with a custom `sym_jsonify`; an unresolved `Inferential`
    dumps to `{}`, so pair with `use_inferred=True` when dumping
    contextual trees. Pydantic's `exclude_unset` has
    no pygx equivalent — defaults materialize at construction, so
    provided-at-default and defaulted are indistinguishable afterwards.
    """
    inline_reader = _native.object_inline_fields
    if inline_reader is not None and not self.sym_mode and not kwargs:
        # Native inline fast path: for an all-primitive, override-free,
        # const-no-frozen `sym=False` object, build `{_type, **fields}` in Rust
        # (the `sym=False` mirror of the `sym=True` native walk below) — closes
        # the anomaly where `sym=False` serialized slower than `sym=True`. `None`
        # ⇒ a non-primitive field / materialized object → the Python path.
        cls = type(self)
        native_inline = _native.inline_object_to_json
        # The native serializer reads a dict-layout `SymObject`'s storage; a
        # `slots=True` record uses the Python `_inline_object_to_json` below,
        # which reads its inline slots (SymDict-free) via `_record_json_fields`.
        if cls.__slot_storage__:  # pragma: no cover  (record: Python path)
            native_inline = None
        if (
            native_inline is not None
            and cls.sym_jsonify is Object.sym_jsonify
        ):
            plan = _cached_jsonify_inline_plan(cls)
            if plan is not None:
                # A closed plan is the key order itself; an open
                # (`varkw=True`) plan's order is instance-dependent, so its
                # serializer reads the native schema's open plan directly
                # (and declines on any drift, falling through below).
                # Exact-type: `_OpenJsonifyPlan` IS a tuple subclass.
                if type(plan) is tuple:  # pylint: disable=unidiomatic-typecheck
                    native = native_inline(self, plan)
                else:
                    native = _native.inline_object_to_json_open(self)
                if native is not None:
                    return typing.cast(wire.WireValue, native)
        # `_inline_object_to_json` serializes this object (and recursively any
        # nested `sym=False` child) to `{_type, **fields}`, sharing one `seen`
        # id-set so a container / child aliased across fields (or referenced
        # cyclically) bails the WHOLE object to `wire.to_json`'s `__ref__`
        # form. Returns `_NOT_PLAIN` to fall through to the general path.
        result = _inline_object_to_json(self, set())
        if result is not _NOT_PLAIN:
            return typing.cast(wire.WireValue, result)
    # `sym=True` fast path: a symbolic tree with default flags is serialized
    # by the native walk (Object/Dict/List/primitives recursed in Rust),
    # collapsing the per-node `wire.to_json` / `serialize_maybe_shared` /
    # `sym_jsonify` frames. `None` ⇒ the tree has a sharing-capable leaf or a
    # shape the walk doesn't replicate byte-identically → fall back to the
    # general path (which honors shared/cyclic-ref dedup).
    walk = _native.to_json_walk
    if walk is not None and self.sym_mode and not kwargs:
        walked = walk(self)
        if walked is not None:
            return walked
    return wire.to_json(self, **kwargs)

sym_jsonify

sym_jsonify(**kwargs) -> WireValue

Converts current object to a dict of plain Python objects.

Calls the inner Dict's sym_jsonify directly instead of routing through _sym_attributes.to_json(). That round-trip would otherwise re-enter wire.to_json and serialize_maybe_shared just to wrap and immediately unwrap the same content into this outer dict.

type(self) bypasses Object.__getattribute__'s try/except, which shows up on the hot serialization path. _sym_attributes is read via the attribute (not vars(self)[...]) so it is storage-agnostic: a vars() entry under the python core, a Rust struct field under the native core.

Source code in pygx/symbolic/_object.py
def sym_jsonify(self, **kwargs) -> wire.WireValue:
    """Converts current object to a dict of plain Python objects.

    Calls the inner Dict's `sym_jsonify` directly instead of routing
    through `_sym_attributes.to_json()`. That round-trip would otherwise
    re-enter `wire.to_json` and `serialize_maybe_shared` just to wrap
    and immediately unwrap the same content into this outer dict.

    `type(self)` bypasses `Object.__getattribute__`'s try/except, which shows
    up on the hot serialization path. `_sym_attributes` is read via the
    attribute (not `vars(self)[...]`) so it is storage-agnostic: a `vars()`
    entry under the python core, a Rust struct field under the native core.
    """
    # `type_info=False` (#519) omits the `_type` class marker — the
    # plain-dict dump (`dataclasses.asdict` analog). One-way by design:
    # the result is not `from_json`-loadable. Only the CLASS marker is
    # gated; structural wire markers (`__tuple__` etc.) still emit. The
    # kwarg stays in `kwargs`, so nested objects drop theirs too.
    json_dict: dict[str, Any] = (
        {_TYPE_NAME_KEY: type(self).__serialization_key__}
        if kwargs.get('type_info', True)
        else {}
    )
    # Inline fast path: a `sym=False` object holds its (non-symbolic) values
    # inline — serialize them directly, with NO `__sym_dict__`
    # materialization. The per-class `_jsonify_inline_plan` is the ordered
    # field-key tuple when the schema is const-keyed with NO frozen field
    # (so every field is emitted and `exclude_frozen` is moot), or an
    # `_OpenJsonifyPlan` for the same no-frozen discipline on an open
    # (`varkw=True`) schema — `_plan_emit_keys` interleaves the instance's
    # residue at the wildcard's position (#442 resolve order); `None` (the
    # general-shape sentinel) routes to the materialize + walk path, which
    # honors frozen / non-const / the non-default flags. The per-value
    # primitive check keeps `validate=False` fields correct (a non-primitive
    # value still goes through `wire.to_json`). Gated to `sym=False` and the
    # default flags; `_native_object_inline_fields` is absent on the python
    # core.
    inline_reader = _native.object_inline_fields
    if (
        inline_reader is not None
        and not self.sym_mode
        and not kwargs.get('exclude_defaults', False)
        and not kwargs.get('exclude_keys')
        and not kwargs.get('use_inferred', False)
        and not kwargs.get('by_alias', False)
        and not kwargs.get('exclude_none', False)
    ):
        cls = type(self)
        plan = _cached_jsonify_inline_plan(cls)
        if plan is not None:  # pragma: no cover  (native core only)
            # A `slots=True` record reads its inline slots (SymDict-free); a
            # dict-layout object its native inline `fields`. The dict-layout
            # path stays a single native read (no record dispatch); `None` (a
            # materialized dict-layout object) → the general path below.
            if cls.__slot_storage__:
                fields = _record_json_fields(self)
            else:
                fields = inline_reader(self)
            if fields is not None:
                _dgi = dict.__getitem__
                _PRIM = pg_dict._PRIMITIVE_WIRE_TYPES  # pylint: disable=protected-access
                child_kwargs = None
                for key in _plan_emit_keys(plan, fields):
                    value = _dgi(fields, key)
                    if value.__class__ is _MISSING_TYPE:
                        # A partial object's unbound field is DROPPED (matching the
                        # materialized `SymDict.sym_jsonify`); inline `fields` holds
                        # it as a present `MISSING_VALUE` (a fresh instance, so a
                        # `__class__` check — not `is` — per `_MISSING_TYPE`), skip it.
                        continue
                    if value.__class__ in _PRIM:
                        json_dict[key] = value
                    else:
                        if child_kwargs is None:
                            child_kwargs = {
                                **kwargs,
                                'omit_symbolic_marker': True,
                            }
                        json_dict[key] = wire.to_json(value, **child_kwargs)
                return json_dict
    kwargs['omit_symbolic_marker'] = True
    json_dict.update(self._sym_attributes.sym_jsonify(**kwargs))
    # Wire-layer aliases (#517): `by_alias=True` renames THIS object's
    # const field keys post-emission (order preserved; `_type` never
    # aliases); the kwarg stays in `kwargs` above so nested objects
    # rename their own keys in recursion.
    if kwargs.get('by_alias', False):
        alias_maps = type(self)._sym_alias_maps
        if alias_maps is not None:
            name_to_alias = alias_maps[1]
            renamed = {
                name_to_alias.get(k, k): v for k, v in json_dict.items()
            }
            if len(renamed) != len(json_dict):
                # A post-construct write snuck an extra key that equals
                # a field alias past the construct-time reservation —
                # refuse to silently drop data.
                raise ValueError(
                    f'Cannot serialize {type(self).__name__!r} with '
                    f'`by_alias=True`: an extra key collides with a '
                    f'field alias.'
                )
            json_dict = renamed
    return json_dict

format

format(
    compact: bool = False, verbose: bool = False, root_indent: int = 0, **kwargs
) -> str

Formats this object.

Source code in pygx/symbolic/_object.py
def format(
    self,
    compact: bool = False,
    verbose: bool = False,
    root_indent: int = 0,
    **kwargs,
) -> str:
    """Formats this object."""
    # Fast path for a bare-args call (`obj.format()` / `format(compact=True)`
    # with no formatting kwargs): render directly via the native builder.
    # `verbose` is intentionally not gated — for a clean class it is inert
    # (it only affects spec-bearing renders: `MISSING` / partials, which are
    # non-primitive and decline the builder). Any keyword argument — even
    # one spelled at its default — routes to the general formatter, so the
    # full kwargs surface keeps its exact semantics. `compact`/`root_indent`
    # pass through UNINSPECTED: declining on a non-standard argument type
    # (this method consumes both lazily/truthily) is the renderer's own
    # input contract, not this layer's concern (#413 review).
    if not kwargs:
        s = _maybe_fast_object_repr_str(self, compact, root_indent)
        if s is not None:
            return s  # pragma: no cover  (fast path: native core only)
    cls = self.__class__
    # Honor `enable_repr=False` by excluding the opted-out symbolic
    # keys from the inner Dict's format. Merge with any user-supplied
    # `exclude_keys`.
    exclude_repr_keys = cls._exclude_from_repr  # pylint: disable=protected-access
    if exclude_repr_keys:
        user_exclude = kwargs.get('exclude_keys')
        if user_exclude:
            kwargs['exclude_keys'] = set(user_exclude) | exclude_repr_keys
        else:
            kwargs['exclude_keys'] = exclude_repr_keys
    # Inject `enable_init=False` fields (instance-stored, outside
    # `_sym_attributes`) as extras so they appear alongside symbolic
    # fields. Their `enable_repr` flag is honored here; unset attrs
    # surface as MISSING and are suppressed by `exclude_missing`.
    # `_get_non_symbolic` reads a record's inline slots (C3a) — a
    # record has no `self.__dict__` to read at all (C3c).
    non_sym = cls._non_symbolic_fields  # pylint: disable=protected-access
    if non_sym:
        extras = [
            (
                field,
                name,
                _get_non_symbolic(self, name, pg_typing.MISSING_VALUE),
            )
            for name, field in non_sym
            if field.enable_repr
        ]
        if extras:
            user_extras = kwargs.get('extra_fields')
            kwargs['extra_fields'] = (
                list(user_extras) + extras if user_extras else extras
            )
    # Inline (v2 P4): render directly from the inline fields + `cls.__schema__`
    # via the shared `_format_symbolic_fields`, WITHOUT materializing a
    # `__sym_dict__`. The object's `sym_getattr` / `sym_items` read inline; the
    # schema is the object's own. `None` ⇒ a materialized object / python core →
    # the `_sym_attributes.format` delegation (same kwargs).
    if (
        _inline_object_fields(self) is not None
    ):  # pragma: no cover (rust only)
        # Use the SYMBOLIC-fields schema (`cls.sym_fields.schema`, the
        # `__sym_dict__`'s `_value_spec.schema`), NOT `cls.__schema__` — the
        # latter also carries the `enable_init=False` non-symbolic fields, which
        # are rendered separately as `extra_fields` (above) and must not appear
        # in the schema-driven loop.
        return pg_dict._format_symbolic_fields(  # pylint: disable=protected-access
            cls.sym_fields.schema,
            self.sym_keys(),
            self.sym_getattr,
            self.sym_items,
            self.sym_inferred,
            compact,
            verbose,
            root_indent,
            cls_name=cls.__name__,
            key_as_attribute=True,
            bracket_type=formatting.BracketType.ROUND,
            **kwargs,
        )
    return self._sym_attributes.format(
        compact,
        verbose,
        root_indent,
        cls_name=cls.__name__,
        key_as_attribute=True,
        bracket_type=formatting.BracketType.ROUND,
        **kwargs,
    )

ObjectMeta

Bases: ABCMeta

Meta class for pg.Object.

sym_fields property

sym_fields: Dict

Gets symbolic field.

init_arg_list property

init_arg_list: list[str]

Gets init positional argument list.

apply_schema

apply_schema(schema: Schema | None = None) -> None

Applies a schema to a symbolic class.

Parameters:

Name Type Description Default
schema Schema | None

The schema that will be applied to class. If cls was attached with an existing schema. The old schema will be dropped. If None, the cls will update its signature and getters according to the (maybe updated) old schema.

None

Raises:

Type Description
TypeError

When REPLACING the schema (schema is not None) after instances of cls (or a subclass) have been created — schema mutation is only legal before instances exist (#432): a live instance's storage was validated and filled against the OLD schema, so it would go stale (missing new fields, holding removed ones) with no way to heal it consistently. Also when replacing a slots=True class's schema after class creation (the inline-slot layout is baked from the creation-time schema). The schema=None refresh path is exempt (it replaces nothing).

Source code in pygx/symbolic/_object_meta.py
def apply_schema(cls, schema: pg_typing.Schema | None = None) -> None:
    """Applies a schema to a symbolic class.

    Args:
      schema: The schema that will be applied to class. If `cls` was attached
        with an existing schema. The old schema will be dropped. If None, the
        cls will update its signature and getters according to the (maybe
        updated) old schema.

    Raises:
      TypeError: When REPLACING the schema (`schema is not None`) after
        instances of `cls` (or a subclass) have been created — schema
        mutation is only legal before instances exist (#432): a live
        instance's storage was validated and filled against the OLD
        schema, so it would go stale (missing new fields, holding removed
        ones) with no way to heal it consistently. Also when replacing a
        `slots=True` class's schema after class creation (the inline-slot
        layout is baked from the creation-time schema). The `schema=None`
        refresh path is exempt (it replaces nothing).
    """
    # Formalize schema first.
    if schema is not None:
        # #432: a `slots=True` class's schema is frozen at class creation
        # — the record's C layout (one inline slot per field, offsets
        # baked into getsets) cannot be re-derived, and a post-creation
        # field previously validated at construct then silently VANISHED
        # (no slot to store it). Guarded HERE (the funnel every mutation
        # route delegates to — #435 review: an `update_schema`-only guard
        # left direct `apply_schema` open); the class-creation FIRST apply
        # (no own `__schema__` yet) is the one legal application.
        if cls.__slot_storage__ and '__schema__' in cls.__dict__:
            raise TypeError(
                f'Cannot update the schema of {cls.__qualname__!r}: a '
                f'`slots=True` class generates its inline-slot layout '
                f'from the schema at class creation. Define a new class '
                f'instead.'
            )
        # #432 (owner decision): `update_schema` (and any schema
        # REPLACEMENT) is only legal before instances exist — enforce
        # loudly instead of defining degraded stale-instance semantics.
        # Class-creation calls (`__init_subclass__`, wrappers, functors,
        # `pg.members`) always run on a fresh, instance-less class.
        witness = ObjectMeta._first_instantiated_class(cls)
        if witness is not None:
            of = (
                ''
                if witness is cls
                else f' (instances of subclass {witness.__qualname__!r} exist)'
            )
            raise TypeError(
                f'Cannot update the schema of {cls.__qualname__!r}: '
                f'instances have already been created{of}. Schema '
                f'mutation is only allowed before a class (or any of its '
                f'subclasses) is instantiated — define a new class, or '
                f'update the schema before constructing instances.'
            )
        # Class-wide opt-outs (`validate=False` / `sym=False` → per-field
        # flags, `strict=True`) materialize HERE — the funnel every schema
        # application routes through — so fields added post-creation via
        # `pg.members` / `update_schema` receive them exactly like
        # class-creation fields (#559: an update-added container field on
        # a flat class previously wrapped, violating the flat law).
        # Idempotent: only unset (`None`) flags are touched, so the
        # re-application over inherited fields is a no-op.
        cls._materialize_field_flags(schema)
        schema = cls._normalize_schema(schema)
        assert isinstance(schema, pg_typing.Schema)
        setattr(cls, '__schema__', schema)
        # The inner `_sym_attributes` Dict only carries symbolic
        # (i.e. ``enable_init=True``) fields. Fields with
        # ``enable_init=False`` live on ``self.__dict__`` instead and
        # therefore must be excluded from the Dict's value_spec so
        # apply/validation paths don't expect them as keys.
        init_fields = [f for f in schema.fields.values() if f.enable_init]
        if len(init_fields) == len(schema.fields):
            sym_dict_spec = pg_typing.Dict(schema)
        else:
            # Build a sibling schema with only the symbolic fields.
            # Skip `_normalize_schema` (fields are already normalized
            # in `schema`) and reuse the originals (no deep-copy):
            # the derived schema is read-only state used as the
            # inner Dict's value_spec.
            derived = pg_typing.Schema(
                init_fields,
                name=schema.name,
                description=schema.description,
                allow_nonconst_keys=True,
                metadata=dict(schema.metadata),
            )
            sym_dict_spec = pg_typing.Dict(derived)
        setattr(cls, '__sym_fields', sym_dict_spec)

    cls._on_schema_update()

update_schema

update_schema(
    fields: list[Field | list[FieldDef] | dict[FieldKeyDef, FieldValueDef]],
    extend: bool = True,
    *,
    init_arg_list: Sequence[str] | None = None,
    metadata: dict[str, Any] | None = None
) -> None

Updates the schema of the class.

Only legal BEFORE any instance of the class (or a subclass) exists, and never for a slots=True class, whose inline-slot layout is generated from the schema at class creation — both enforced by apply_schema (#432).

Source code in pygx/symbolic/_object_meta.py
def update_schema(
    cls,
    fields: list[
        (
            pg_typing.Field
            | list[pg_typing.FieldDef]
            | dict[pg_typing.FieldKeyDef, pg_typing.FieldValueDef]
        )
    ],
    extend: bool = True,
    *,
    init_arg_list: Sequence[str] | None = None,
    metadata: dict[str, Any] | None = None,
) -> None:
    """Updates the schema of the class.

    Only legal BEFORE any instance of the class (or a subclass) exists,
    and never for a `slots=True` class, whose inline-slot layout is
    generated from the schema at class creation — both enforced by
    `apply_schema` (#432).
    """
    metadata = metadata or {}
    if init_arg_list is None:
        init_arg_list = metadata.pop('init_arg_list', None)

    metadata['init_arg_list'] = init_arg_list
    schema = pg_typing.create_schema(
        fields=fields,  # pyright: ignore[reportArgumentType]
        base_schema_list=[cls.__schema__] if extend else [],
        allow_nonconst_keys=True,
        metadata=metadata,
        for_cls=cls,
    )
    cls.apply_schema(schema)

    # Update abstract methods.
    abc.update_abstractmethods(cls)

register_for_deserialization

register_for_deserialization(
    serialization_key: str | None = None,
    additional_keys: list[str] | None = None,
) -> None

Register current symbolic class for deserialization.

Also mirrors the resolved keys onto cls.__json_options__ so callers that later read the class's JSON conversion options see the updated state.

Source code in pygx/symbolic/_object_meta.py
def register_for_deserialization(
    cls,
    serialization_key: str | None = None,
    additional_keys: list[str] | None = None,
) -> None:
    """Register current symbolic class for deserialization.

    Also mirrors the resolved keys onto ``cls.__json_options__`` so
    callers that later read the class's JSON conversion options see
    the updated state.
    """
    serialization_key = serialization_key or cls.__type_name__
    setattr(cls, '__serialization_key__', serialization_key)

    keys = [serialization_key]
    keys.extend(additional_keys or [])
    if cls.__type_name__ not in keys:
        keys.append(cls.__type_name__)

    # Replace the per-class JSON conversion options with the freshly
    # resolved keys so any later reader sees the canonical state.
    cls.__json_options__ = _json_conversion_mod._WireConversionOptions(  # pylint: disable=protected-access
        to_json_key=serialization_key,
        from_json_keys=tuple(k for k in keys if k != serialization_key),
    )

    for key in keys:
        wire.WireConvertible.register(
            key,
            cls,  # pyright: ignore[reportArgumentType]
            flags.is_repeated_class_registration_allowed(),
        )

Origin

Origin(
    source: Any,
    tag: str,
    stacktrace: bool | None = None,
    stacklimit: int | None = None,
    stacktop: int = -1,
)

Bases: Formattable

Class that represents the origin of a symbolic value.

Origin is used for debugging the creation chain of a symbolic value, as well as keeping track of the factory or builder in creational design patterns. An Origin object records the source value, a string tag, and optional stack information on where a symbolic value is created.

Built-in tags are 'init', 'clone', 'deepclone' and 'return'. Users can pass custom tags to the sym_setorigin method of a symbolic value for tracking its source in their own scenarios.

When origin tracking is enabled by calling pg.track_origin(True), the sym_setorigin method of symbolic values will be automatically called during object creation, cloning or being returned from a functor. The stack information can be obtained by origin.stack or origin.stacktrace.

Constructor.

Parameters:

Name Type Description Default
source Any

Source value for the origin.

required
tag str

A descriptive tag of the origin. Built-in tags are: 'init', 'clone', 'deepclone', 'return'. Users can manually call sym_setorigin with custom tag value.

required
stacktrace bool | None

If True, enable stack trace for the origin. If None, enable stack trace if pg.tracek_origin() is called. Otherwise stack trace is disabled.

None
stacklimit int | None

An optional integer to limit the stack depth. If None, it's determined by the value passed to pg.set_origin_stacktrace_limit, which is 10 by default.

None
stacktop int

A negative integer to indicate the stack top among the stack frames that we want to present to user, by default it's 2-level up from the stack within current sym_setorigin call.

-1
Source code in pygx/symbolic/_origin.py
def __init__(
    self,
    source: Any,
    tag: str,
    stacktrace: bool | None = None,
    stacklimit: int | None = None,
    stacktop: int = -1,
):
    """Constructor.

    Args:
      source: Source value for the origin.
      tag: A descriptive tag of the origin. Built-in tags are:
        '__init__', 'clone', 'deepclone', 'return'. Users can manually
        call `sym_setorigin` with custom tag value.
      stacktrace: If True, enable stack trace for the origin. If None, enable
        stack trace if `pg.tracek_origin()` is called. Otherwise stack trace is
        disabled.
      stacklimit: An optional integer to limit the stack depth. If None, it's
        determined by the value passed to `pg.set_origin_stacktrace_limit`,
        which is 10 by default.
      stacktop: A negative integer to indicate the stack top among the stack
        frames that we want to present to user, by default it's 2-level up from
        the stack within current `sym_setorigin` call.
    """
    if not isinstance(tag, str):
        raise ValueError(f'`tag` must be a string. Encountered: {tag!r}.')

    self._source = source
    self._tag = tag
    self._stack = None
    self._stacktrace = None

    if stacktrace is None:
        stacktrace = flags.is_tracking_origin()

    if stacklimit is None:
        stacklimit = flags.get_origin_stacktrace_limit()

    if stacktrace:
        assert stacktop < 0, stacktop
        self._stack = traceback.extract_stack(limit=stacklimit - stacktop)[
            :stacktop
        ]

source property

source: Any

Returns the source object.

root property

root: Origin

Returns the root source of the origin.

tag property

tag: str

Returns tag.

stack property

stack: list[FrameSummary] | None

Returns the frame summary of original stack.

stacktrace property

stacktrace: str | None

Returns stack trace string.

history

history(condition: Callable[[Origin], bool] | None = None) -> list[Origin]

Returns a history of origins with an optional filter.

Parameters:

Name Type Description Default
condition Callable[[Origin], bool] | None

An optional callable object with signature (origin) -> should_list. If None, all origins will be listed.

None

Returns:

Type Description
list[Origin]

A list of filtered origin from the earliest (root) to the most recent.

Source code in pygx/symbolic/_origin.py
def history(
    self, condition: Callable[['Origin'], bool] | None = None
) -> list['Origin']:
    """Returns a history of origins with an optional filter.

    Args:
      condition: An optional callable object with signature
        (origin) -> should_list. If None, all origins will be listed.

    Returns:
      A list of filtered origin from the earliest (root) to the most recent.
    """
    condition = condition or (lambda o: True)
    current = self
    history = []
    while current is not None:
        if condition(current):
            history.append(current)
        current = getattr(current.source, 'sym_origin', None)
    history.reverse()
    return history

chain

chain(tag: str | None = None) -> list[Origin]

Get the origin list from the neareast to the farthest filtered by tag.

Source code in pygx/symbolic/_origin.py
def chain(self, tag: str | None = None) -> list['Origin']:
    """Get the origin list from the neareast to the farthest filtered by tag."""
    origins = []
    o = self
    while o is not None:
        if tag is None or tag == o.tag:
            origins.append(o)
        o = getattr(o.source, 'sym_origin', None)
    return origins

format

format(
    compact: bool = False, verbose: bool = True, root_indent: int = 0, **kwargs
) -> str

Formats this object.

Source code in pygx/symbolic/_origin.py
def format(
    self,
    compact: bool = False,
    verbose: bool = True,
    root_indent: int = 0,
    **kwargs,
) -> str:
    """Formats this object."""
    if isinstance(self._source, (str, type(None))):
        source_str = self._source
    else:
        source_info = formatting.format(
            self._source, compact, verbose, root_indent + 1, **kwargs
        )
        source_str = formatting.RawText(
            f'{source_info} at 0x{id(self._source):8x}'
        )

    return formatting.kvlist_str(
        [
            ('tag', self._tag, None),
            ('source', source_str, None),
        ],
        label=self.__class__.__name__,
        compact=compact,
        verbose=verbose,
        root_indent=root_indent,
    )

NonDeterministic

Bases: PureSymbolic

Base class to mark a class whose objects are considered non-deterministic.

A non-deterministic value represents a value that will be decided later. In PyGX system, pg.one_of, pg.sublist_of, pg.float_value are non-deterministic values. Please search NonDeterministic subclasses for more details.

PureSymbolic

Bases: CustomTyping

Base class to classes whose objects are considered pure symbolic.

Pure symbolic objects can be used for representing abstract concepts - for example, a search space of objects - which cannot be executed but soely representational.

Having pure symbolic object is a key differentiator of symbolic OOP from regular OOP, which can be used to placehold values in an object as a high-level expression of ideas. Later, with symbolic manipulation, the pure symbolic objects are replaced with material values so the object can be evaluated. This effectively decouples the expression of ideas from the implementation of ideas. For example: pg.oneof(['a', 'b', 'c'] will be manipulated into 'a', 'b' or 'c' based on the decision of a search algorithm, letting the program evolve itself.

custom_apply

custom_apply(
    path: KeyPath,
    value_spec: ValueSpec,
    allow_partial: bool,
    child_transform: None | Callable[[KeyPath, Field, Any], Any] = None,
) -> tuple[bool, Any]

Custom apply on a value based on its original value spec.

This implements pg.pg_typing.CustomTyping, allowing a pure symbolic value to be assigned to any field. To customize this behavior, override this method in subclasses.

Parameters:

Name Type Description Default
path KeyPath

KeyPath of current object under its object tree.

required
value_spec ValueSpec

Original value spec for this field.

required
allow_partial bool

Whether allow partial object to be created.

required
child_transform None | Callable[[KeyPath, Field, Any], Any]

Function to transform child node values into their final values. Transform function is called on leaf nodes first, then on their parents, recursively.

None

Returns:

Type Description
tuple[bool, Any]

A tuple (proceed_with_standard_apply, value_to_proceed). If proceed_with_standard_apply is set to False, value_to_proceed will be used as final value.

Raises:

Type Description
TypeError

If the value is not compatible with the value spec.

Source code in pygx/symbolic/_pure_symbolic.py
def custom_apply(
    self,
    path: topology.KeyPath,
    value_spec: pg_typing.ValueSpec,
    allow_partial: bool,
    child_transform: None
    | (Callable[[topology.KeyPath, pg_typing.Field, Any], Any]) = None,
) -> tuple[bool, Any]:
    """Custom apply on a value based on its original value spec.

    This implements ``pg.pg_typing.CustomTyping``, allowing a pure symbolic
    value to be assigned to any field. To customize this behavior, override
    this method in subclasses.

    Args:
      path: KeyPath of current object under its object tree.
      value_spec: Original value spec for this field.
      allow_partial: Whether allow partial object to be created.
      child_transform: Function to transform child node values into their final
        values. Transform function is called on leaf nodes first, then on their
        parents, recursively.

    Returns:
      A tuple (proceed_with_standard_apply, value_to_proceed).
        If proceed_with_standard_apply is set to False, value_to_proceed
        will be used as final value.

    Raises:
      TypeError: If the value is not compatible with the value spec.
    """
    del path, value_spec, allow_partial, child_transform
    return (False, self)

Ref

Ref(value: T, **kwargs: Any)

Bases: Object, Inferential, Extension

Symbolic reference.

When adding a symbolic node to a symbolic tree, it undergoes a copy operation if it already has a parent, ensuring that all symbolic objects have a single parent. Additionally, list and dict objects are automatically converted to pg.List and pg.Dict, respectively, to enable symbolic operability.

However, these two conventions come with certain costs. The act of making copies incurs a runtime cost, and it also introduces challenges in sharing states across different symbolic objects. To address this issue, symbolic reference is introduced. This feature allows a symbolic node to refer to value objects without the need for transformation or copying, even when the symbolic node itself is copied. For example::

class A(pg.Object): x: int

a = pg.Ref(A(1)) b = pg.Dict(x=a) c = pg.Dict(y=a)

assert b.x is a assert c.y is a assert b.sym_clone().x is a assert c.sym_clone(deep=True).y is a

In this example, pg.Ref is used to create a symbolic reference to the object A(1), and the pg.Dict objects b and c can then reference a without creating additional copies. This mechanism not only mitigates the runtime cost but also facilitates seamless sharing of states among various symbolic objects.

Another useful scenario arises when we wish to utilize regular Python list and dict objects. In this case, pg.Ref enables us to access the list/dict object as fields in the symbolic tree without requiring them to be transformed into pg.List and pg.Dict. This allows for seamless integration of standard Python containers within the symbolic structure::

d = pg.Dict(x=pg.Ref({1: 2})) assert isinstance(d.x, dict) assert not isinstance(d.x, pg.Dict)

e = pg.Dict(x=pg.Ref([0, 1, 2]])) assert isinstance(e.x, list) assert not isinstance(e.x, pg.List)

Please be aware that pg.Ref objects are treated as leaf nodes in the symbolic tree, even when they reference other symbolic objects. As a result, the sym_rebind() method cannot modify the value they are pointing to.

For primitive types, pg.Ref() returns their values directly without creating a reference. For example, pg.Ref(1) and pg.Ref('abc') will simply return the values 1 and 'abc', respectively, without any additional referencing.

Source code in pygx/symbolic/_ref.py
def __init__(self, value: T, **kwargs: Any) -> None:
    super().__init__(**kwargs)
    # Unwrap a passed-in `Ref` to avoid nesting; the cast tells pyright
    # the inner type matches our `T` (which it must, since the public
    # `Ref(ref)` overload only makes sense when the inner type is the
    # same).
    if isinstance(value, Ref):
        value = cast(T, value.value)
    self._value: T = value

value property

value: T

Returns the referenced value.

infer

infer(**kwargs: Any) -> T

Returns the referenced value.

Source code in pygx/symbolic/_ref.py
def infer(self, **kwargs: Any) -> T:
    """Returns the referenced value."""
    return self._value

custom_apply

custom_apply(
    path: KeyPath,
    value_spec: ValueSpec,
    allow_partial: bool = False,
    child_transform: None | Callable[[KeyPath, Field, Any], Any] = None,
) -> tuple[bool, Any]

Validate candidates during value_spec binding time.

Source code in pygx/symbolic/_ref.py
def custom_apply(
    self,
    path: topology.KeyPath,
    value_spec: pg_typing.ValueSpec,
    allow_partial: bool = False,
    child_transform: None
    | (Callable[[topology.KeyPath, pg_typing.Field, Any], Any]) = None,
) -> tuple[bool, Any]:
    """Validate candidates during value_spec binding time."""
    del child_transform
    # Check if the field being assigned could accept the referenced value.
    # We do not do any transformation, thus not passing the child transform.
    value_spec.apply(self._value, allow_partial=allow_partial)
    return (False, self)

UnknownFunction

UnknownFunction(
    *,
    allow_partial: bool = False,
    sealed: bool | None = None,
    root_path: KeyPath | None = None,
    explicit_init: bool = False,
    **kwargs: Any
)

Bases: UnknownCallable

Symbolic objject for representing unknown functions.

Source code in pygx/symbolic/_object.py
@_filter_typing_traceback_on_raise
def __init__(
    self,
    *,
    allow_partial: bool = False,
    sealed: bool | None = None,
    root_path: topology.KeyPath | None = None,
    explicit_init: bool = False,
    **kwargs: Any,
):
    """Create an Object instance.

    ``pg.Object`` synthesizes a keyword-only ``__init__``. Subclasses
    that need positional arguments must override ``__init__``
    explicitly and translate to keyword arguments before forwarding
    to ``super().__init__(**kwargs)``.

    Args:
      allow_partial: If True, the object can be partial.
      sealed: If True, seal the object from future modification (unless under
        a `pg.as_sealed(False)` context manager). If False, treat the object as
        unsealed. If None, it's determined by `cls.__sym_options__.frozen`.
      root_path: The symbolic path for current object. By default it's None,
        which indicates that newly constructed object does not have a parent.
      explicit_init: Should set to `True` when `__init__` is called via
        `pg.Object.__init__` instead of `super().__init__`.
      **kwargs: key/value arguments that align with the schema. All required
        keys in the schema must be specified, and values should be acceptable
        according to their value spec.

    Raises:
      KeyError: When required key(s) are missing.
      ValueError: When value(s) are not acceptable by their value spec.
    """
    # Placeholder for Google-internal usage instrumentation.

    cls = self.__class__
    # The ever-instantiated latch `apply_schema`'s post-instance guard
    # reads (#432: `update_schema` is only legal before instances exist).
    # Own-`__dict__`, latched on the construction ATTEMPT — mirroring the
    # native core, whose `__native_ctor_meta__` (the equivalent marker) is
    # created on the first instance op. One dict read per construct.
    meta = cls.__sym_meta__
    if not meta.instantiated:
        meta.instantiated = True
    if sealed is None:
        sealed = cls.__sym_options__.frozen

    if not isinstance(allow_partial, bool):
        raise TypeError(
            f"Expect bool type for argument 'allow_partial' in "
            f'symbolic.Object.__init__ but encountered {allow_partial}.'
        )

    # Fast path: const-keyed schema.
    if cls._sym_init_fields is not None and self._init_fast_path(
        kwargs,
        allow_partial,
        sealed,
        root_path,
        explicit_init,
    ):
        return

    # Slow path: handles varargs and schemas with non-const keys.

    # Model before-hook (#516) — the fast path was not taken
    # (`_sym_init_fields is None`), so the hook has not run yet.
    if cls._sym_preinit_overridden:
        kwargs = _apply_preinit(cls, kwargs)

    # We delay the seal attempt until members are all set.
    super().__init__(
        allow_partial=allow_partial,
        accessor_writable=cls.__sym_options__.attr_write,
        sealed=sealed,
        root_path=root_path,
        init_super=not explicit_init,
    )

    # Fill field_args and init_args from **kwargs.
    _, unmatched_keys = cls.__schema__.resolve(list(kwargs.keys()))
    # Fields opted out of `__init__` via `enable_init=False` resolve
    # against the schema but must still be rejected as kwargs.
    get_field = cls.__schema__.get_field
    unmatched_keys = list(unmatched_keys) + [
        k
        for k in kwargs
        if (f := get_field(k)) is not None and not f.enable_init
    ]
    if unmatched_keys:
        _screen_unexpected_init_keys(cls, kwargs, unmatched_keys)

    # Declared aliases are RESERVED wire spellings (#517, owner ruling):
    # an open (varkw) schema must not accept an extra kwarg equal to a
    # field's alias — the wire would become ambiguous. (Const-keyed
    # schemas never reach here with an alias-spelled kwarg; their
    # unknown-key screen already raised — or, under `varkw='ignore'`,
    # raised for alias keys before dropping the rest.)
    if cls._sym_alias_maps is not None:
        alias_to_name = cls._sym_alias_maps[0]
        for k in kwargs:
            if k in alias_to_name:
                raise TypeError(
                    f'{cls.__name__}.__init__() got keyword argument '
                    f'{k!r}, which is reserved as the alias of field '
                    f'{alias_to_name[k]!r}.'
                )

    # All field values come from kwargs — `pg.Object.__init__` is
    # keyword-only.
    field_args = dict(kwargs)

    # Check missing arguments when partial binding is disallowed.
    if not base.accepts_partial(self):
        missing_args = []
        for field in self.__class__.__schema__.fields.values():
            if (
                not field.value.has_default
                and isinstance(field.key, pg_typing.ConstStrKey)
                and field.key not in field_args
            ):
                missing_args.append(str(field.key))
        if missing_args:
            arg_phrase = formatting.auto_plural(
                len(missing_args), 'argument'
            )
            keys_str = formatting.comma_delimited_str(missing_args)
            raise TypeError(
                f'{self.__class__.__name__}.__init__() missing {len(missing_args)} '
                f'required {arg_phrase}: {keys_str}.'
            )

    # NOTE(daiyip): Accessor writable is honored by `Object.__setattr__` so
    # we can always make `_sym_attributes` accessor writable. This prevents
    # a child object's attribute access from being changed when it's
    # attached to a parent whose symbolic attributes are not writable.
    # Per-field validation is gated by `field.enable_validation` inside
    # `Schema.apply` / `Dict._formalized_value` — so a class-wide
    # `validate=False` (resolved into each field) and per-field overrides
    # both flow through naturally here.
    vars(self)['_sym_attributes'] = pg_dict.Dict(
        field_args,  # pyright: ignore[reportArgumentType]
        value_spec=self.__class__.sym_fields,
        allow_partial=allow_partial,
        sealed=sealed,
        accessor_writable=True,
        root_path=root_path,
        as_object_attributes_container=True,
        under_sym_mode=cls.__sym_options__.sym,
    )
    self._sym_attributes.sym_setparent(self)
    # Seed `init=False` fields from their defaults before `on_sym_post_init`
    # runs so `on_sym_ready`/`on_sym_bound` overrides start from a known baseline.
    _seed_non_symbolic_fields(self, cls._non_symbolic_fields)
    # Model after-validator (#516): fires before any lifecycle hook can
    # observe the object, gated like `on_sym_ready` (concrete only).
    if cls._sym_validate_overridden:
        abstract = (
            self._allow_partial and self.sym_partial
        ) or self.sym_puresymbolic
        if not abstract:
            self.on_sym_validate()
    self.on_sym_post_init()
    self.sym_seal(sealed)

UnknownMethod

UnknownMethod(
    *,
    allow_partial: bool = False,
    sealed: bool | None = None,
    root_path: KeyPath | None = None,
    explicit_init: bool = False,
    **kwargs: Any
)

Bases: UnknownCallable

Symbolic object for representing unknown methods.

Source code in pygx/symbolic/_object.py
@_filter_typing_traceback_on_raise
def __init__(
    self,
    *,
    allow_partial: bool = False,
    sealed: bool | None = None,
    root_path: topology.KeyPath | None = None,
    explicit_init: bool = False,
    **kwargs: Any,
):
    """Create an Object instance.

    ``pg.Object`` synthesizes a keyword-only ``__init__``. Subclasses
    that need positional arguments must override ``__init__``
    explicitly and translate to keyword arguments before forwarding
    to ``super().__init__(**kwargs)``.

    Args:
      allow_partial: If True, the object can be partial.
      sealed: If True, seal the object from future modification (unless under
        a `pg.as_sealed(False)` context manager). If False, treat the object as
        unsealed. If None, it's determined by `cls.__sym_options__.frozen`.
      root_path: The symbolic path for current object. By default it's None,
        which indicates that newly constructed object does not have a parent.
      explicit_init: Should set to `True` when `__init__` is called via
        `pg.Object.__init__` instead of `super().__init__`.
      **kwargs: key/value arguments that align with the schema. All required
        keys in the schema must be specified, and values should be acceptable
        according to their value spec.

    Raises:
      KeyError: When required key(s) are missing.
      ValueError: When value(s) are not acceptable by their value spec.
    """
    # Placeholder for Google-internal usage instrumentation.

    cls = self.__class__
    # The ever-instantiated latch `apply_schema`'s post-instance guard
    # reads (#432: `update_schema` is only legal before instances exist).
    # Own-`__dict__`, latched on the construction ATTEMPT — mirroring the
    # native core, whose `__native_ctor_meta__` (the equivalent marker) is
    # created on the first instance op. One dict read per construct.
    meta = cls.__sym_meta__
    if not meta.instantiated:
        meta.instantiated = True
    if sealed is None:
        sealed = cls.__sym_options__.frozen

    if not isinstance(allow_partial, bool):
        raise TypeError(
            f"Expect bool type for argument 'allow_partial' in "
            f'symbolic.Object.__init__ but encountered {allow_partial}.'
        )

    # Fast path: const-keyed schema.
    if cls._sym_init_fields is not None and self._init_fast_path(
        kwargs,
        allow_partial,
        sealed,
        root_path,
        explicit_init,
    ):
        return

    # Slow path: handles varargs and schemas with non-const keys.

    # Model before-hook (#516) — the fast path was not taken
    # (`_sym_init_fields is None`), so the hook has not run yet.
    if cls._sym_preinit_overridden:
        kwargs = _apply_preinit(cls, kwargs)

    # We delay the seal attempt until members are all set.
    super().__init__(
        allow_partial=allow_partial,
        accessor_writable=cls.__sym_options__.attr_write,
        sealed=sealed,
        root_path=root_path,
        init_super=not explicit_init,
    )

    # Fill field_args and init_args from **kwargs.
    _, unmatched_keys = cls.__schema__.resolve(list(kwargs.keys()))
    # Fields opted out of `__init__` via `enable_init=False` resolve
    # against the schema but must still be rejected as kwargs.
    get_field = cls.__schema__.get_field
    unmatched_keys = list(unmatched_keys) + [
        k
        for k in kwargs
        if (f := get_field(k)) is not None and not f.enable_init
    ]
    if unmatched_keys:
        _screen_unexpected_init_keys(cls, kwargs, unmatched_keys)

    # Declared aliases are RESERVED wire spellings (#517, owner ruling):
    # an open (varkw) schema must not accept an extra kwarg equal to a
    # field's alias — the wire would become ambiguous. (Const-keyed
    # schemas never reach here with an alias-spelled kwarg; their
    # unknown-key screen already raised — or, under `varkw='ignore'`,
    # raised for alias keys before dropping the rest.)
    if cls._sym_alias_maps is not None:
        alias_to_name = cls._sym_alias_maps[0]
        for k in kwargs:
            if k in alias_to_name:
                raise TypeError(
                    f'{cls.__name__}.__init__() got keyword argument '
                    f'{k!r}, which is reserved as the alias of field '
                    f'{alias_to_name[k]!r}.'
                )

    # All field values come from kwargs — `pg.Object.__init__` is
    # keyword-only.
    field_args = dict(kwargs)

    # Check missing arguments when partial binding is disallowed.
    if not base.accepts_partial(self):
        missing_args = []
        for field in self.__class__.__schema__.fields.values():
            if (
                not field.value.has_default
                and isinstance(field.key, pg_typing.ConstStrKey)
                and field.key not in field_args
            ):
                missing_args.append(str(field.key))
        if missing_args:
            arg_phrase = formatting.auto_plural(
                len(missing_args), 'argument'
            )
            keys_str = formatting.comma_delimited_str(missing_args)
            raise TypeError(
                f'{self.__class__.__name__}.__init__() missing {len(missing_args)} '
                f'required {arg_phrase}: {keys_str}.'
            )

    # NOTE(daiyip): Accessor writable is honored by `Object.__setattr__` so
    # we can always make `_sym_attributes` accessor writable. This prevents
    # a child object's attribute access from being changed when it's
    # attached to a parent whose symbolic attributes are not writable.
    # Per-field validation is gated by `field.enable_validation` inside
    # `Schema.apply` / `Dict._formalized_value` — so a class-wide
    # `validate=False` (resolved into each field) and per-field overrides
    # both flow through naturally here.
    vars(self)['_sym_attributes'] = pg_dict.Dict(
        field_args,  # pyright: ignore[reportArgumentType]
        value_spec=self.__class__.sym_fields,
        allow_partial=allow_partial,
        sealed=sealed,
        accessor_writable=True,
        root_path=root_path,
        as_object_attributes_container=True,
        under_sym_mode=cls.__sym_options__.sym,
    )
    self._sym_attributes.sym_setparent(self)
    # Seed `init=False` fields from their defaults before `on_sym_post_init`
    # runs so `on_sym_ready`/`on_sym_bound` overrides start from a known baseline.
    _seed_non_symbolic_fields(self, cls._non_symbolic_fields)
    # Model after-validator (#516): fires before any lifecycle hook can
    # observe the object, gated like `on_sym_ready` (concrete only).
    if cls._sym_validate_overridden:
        abstract = (
            self._allow_partial and self.sym_partial
        ) or self.sym_puresymbolic
        if not abstract:
            self.on_sym_validate()
    self.on_sym_post_init()
    self.sym_seal(sealed)

UnknownSymbol

UnknownSymbol(
    *,
    allow_partial: bool = False,
    sealed: bool | None = None,
    root_path: KeyPath | None = None,
    explicit_init: bool = False,
    **kwargs: Any
)

Bases: Object, CustomTyping

Interface for symbolic representation of unknown symbols.

Source code in pygx/symbolic/_object.py
@_filter_typing_traceback_on_raise
def __init__(
    self,
    *,
    allow_partial: bool = False,
    sealed: bool | None = None,
    root_path: topology.KeyPath | None = None,
    explicit_init: bool = False,
    **kwargs: Any,
):
    """Create an Object instance.

    ``pg.Object`` synthesizes a keyword-only ``__init__``. Subclasses
    that need positional arguments must override ``__init__``
    explicitly and translate to keyword arguments before forwarding
    to ``super().__init__(**kwargs)``.

    Args:
      allow_partial: If True, the object can be partial.
      sealed: If True, seal the object from future modification (unless under
        a `pg.as_sealed(False)` context manager). If False, treat the object as
        unsealed. If None, it's determined by `cls.__sym_options__.frozen`.
      root_path: The symbolic path for current object. By default it's None,
        which indicates that newly constructed object does not have a parent.
      explicit_init: Should set to `True` when `__init__` is called via
        `pg.Object.__init__` instead of `super().__init__`.
      **kwargs: key/value arguments that align with the schema. All required
        keys in the schema must be specified, and values should be acceptable
        according to their value spec.

    Raises:
      KeyError: When required key(s) are missing.
      ValueError: When value(s) are not acceptable by their value spec.
    """
    # Placeholder for Google-internal usage instrumentation.

    cls = self.__class__
    # The ever-instantiated latch `apply_schema`'s post-instance guard
    # reads (#432: `update_schema` is only legal before instances exist).
    # Own-`__dict__`, latched on the construction ATTEMPT — mirroring the
    # native core, whose `__native_ctor_meta__` (the equivalent marker) is
    # created on the first instance op. One dict read per construct.
    meta = cls.__sym_meta__
    if not meta.instantiated:
        meta.instantiated = True
    if sealed is None:
        sealed = cls.__sym_options__.frozen

    if not isinstance(allow_partial, bool):
        raise TypeError(
            f"Expect bool type for argument 'allow_partial' in "
            f'symbolic.Object.__init__ but encountered {allow_partial}.'
        )

    # Fast path: const-keyed schema.
    if cls._sym_init_fields is not None and self._init_fast_path(
        kwargs,
        allow_partial,
        sealed,
        root_path,
        explicit_init,
    ):
        return

    # Slow path: handles varargs and schemas with non-const keys.

    # Model before-hook (#516) — the fast path was not taken
    # (`_sym_init_fields is None`), so the hook has not run yet.
    if cls._sym_preinit_overridden:
        kwargs = _apply_preinit(cls, kwargs)

    # We delay the seal attempt until members are all set.
    super().__init__(
        allow_partial=allow_partial,
        accessor_writable=cls.__sym_options__.attr_write,
        sealed=sealed,
        root_path=root_path,
        init_super=not explicit_init,
    )

    # Fill field_args and init_args from **kwargs.
    _, unmatched_keys = cls.__schema__.resolve(list(kwargs.keys()))
    # Fields opted out of `__init__` via `enable_init=False` resolve
    # against the schema but must still be rejected as kwargs.
    get_field = cls.__schema__.get_field
    unmatched_keys = list(unmatched_keys) + [
        k
        for k in kwargs
        if (f := get_field(k)) is not None and not f.enable_init
    ]
    if unmatched_keys:
        _screen_unexpected_init_keys(cls, kwargs, unmatched_keys)

    # Declared aliases are RESERVED wire spellings (#517, owner ruling):
    # an open (varkw) schema must not accept an extra kwarg equal to a
    # field's alias — the wire would become ambiguous. (Const-keyed
    # schemas never reach here with an alias-spelled kwarg; their
    # unknown-key screen already raised — or, under `varkw='ignore'`,
    # raised for alias keys before dropping the rest.)
    if cls._sym_alias_maps is not None:
        alias_to_name = cls._sym_alias_maps[0]
        for k in kwargs:
            if k in alias_to_name:
                raise TypeError(
                    f'{cls.__name__}.__init__() got keyword argument '
                    f'{k!r}, which is reserved as the alias of field '
                    f'{alias_to_name[k]!r}.'
                )

    # All field values come from kwargs — `pg.Object.__init__` is
    # keyword-only.
    field_args = dict(kwargs)

    # Check missing arguments when partial binding is disallowed.
    if not base.accepts_partial(self):
        missing_args = []
        for field in self.__class__.__schema__.fields.values():
            if (
                not field.value.has_default
                and isinstance(field.key, pg_typing.ConstStrKey)
                and field.key not in field_args
            ):
                missing_args.append(str(field.key))
        if missing_args:
            arg_phrase = formatting.auto_plural(
                len(missing_args), 'argument'
            )
            keys_str = formatting.comma_delimited_str(missing_args)
            raise TypeError(
                f'{self.__class__.__name__}.__init__() missing {len(missing_args)} '
                f'required {arg_phrase}: {keys_str}.'
            )

    # NOTE(daiyip): Accessor writable is honored by `Object.__setattr__` so
    # we can always make `_sym_attributes` accessor writable. This prevents
    # a child object's attribute access from being changed when it's
    # attached to a parent whose symbolic attributes are not writable.
    # Per-field validation is gated by `field.enable_validation` inside
    # `Schema.apply` / `Dict._formalized_value` — so a class-wide
    # `validate=False` (resolved into each field) and per-field overrides
    # both flow through naturally here.
    vars(self)['_sym_attributes'] = pg_dict.Dict(
        field_args,  # pyright: ignore[reportArgumentType]
        value_spec=self.__class__.sym_fields,
        allow_partial=allow_partial,
        sealed=sealed,
        accessor_writable=True,
        root_path=root_path,
        as_object_attributes_container=True,
        under_sym_mode=cls.__sym_options__.sym,
    )
    self._sym_attributes.sym_setparent(self)
    # Seed `init=False` fields from their defaults before `on_sym_post_init`
    # runs so `on_sym_ready`/`on_sym_bound` overrides start from a known baseline.
    _seed_non_symbolic_fields(self, cls._non_symbolic_fields)
    # Model after-validator (#516): fires before any lifecycle hook can
    # observe the object, gated like `on_sym_ready` (concrete only).
    if cls._sym_validate_overridden:
        abstract = (
            self._allow_partial and self.sym_partial
        ) or self.sym_puresymbolic
        if not abstract:
            self.on_sym_validate()
    self.on_sym_post_init()
    self.sym_seal(sealed)

custom_apply

custom_apply(*args, **kwargs) -> tuple[bool, Any]

Bypass PyGX type check.

Source code in pygx/symbolic/_unknown_symbols.py
def custom_apply(self, *args, **kwargs) -> tuple[bool, Any]:
    """Bypass PyGX type check."""
    return (False, self)

UnknownType

UnknownType(
    *,
    allow_partial: bool = False,
    sealed: bool | None = None,
    root_path: KeyPath | None = None,
    explicit_init: bool = False,
    **kwargs: Any
)

Bases: UnknownSymbol

Symbolic object for representing unknown types.

Source code in pygx/symbolic/_object.py
@_filter_typing_traceback_on_raise
def __init__(
    self,
    *,
    allow_partial: bool = False,
    sealed: bool | None = None,
    root_path: topology.KeyPath | None = None,
    explicit_init: bool = False,
    **kwargs: Any,
):
    """Create an Object instance.

    ``pg.Object`` synthesizes a keyword-only ``__init__``. Subclasses
    that need positional arguments must override ``__init__``
    explicitly and translate to keyword arguments before forwarding
    to ``super().__init__(**kwargs)``.

    Args:
      allow_partial: If True, the object can be partial.
      sealed: If True, seal the object from future modification (unless under
        a `pg.as_sealed(False)` context manager). If False, treat the object as
        unsealed. If None, it's determined by `cls.__sym_options__.frozen`.
      root_path: The symbolic path for current object. By default it's None,
        which indicates that newly constructed object does not have a parent.
      explicit_init: Should set to `True` when `__init__` is called via
        `pg.Object.__init__` instead of `super().__init__`.
      **kwargs: key/value arguments that align with the schema. All required
        keys in the schema must be specified, and values should be acceptable
        according to their value spec.

    Raises:
      KeyError: When required key(s) are missing.
      ValueError: When value(s) are not acceptable by their value spec.
    """
    # Placeholder for Google-internal usage instrumentation.

    cls = self.__class__
    # The ever-instantiated latch `apply_schema`'s post-instance guard
    # reads (#432: `update_schema` is only legal before instances exist).
    # Own-`__dict__`, latched on the construction ATTEMPT — mirroring the
    # native core, whose `__native_ctor_meta__` (the equivalent marker) is
    # created on the first instance op. One dict read per construct.
    meta = cls.__sym_meta__
    if not meta.instantiated:
        meta.instantiated = True
    if sealed is None:
        sealed = cls.__sym_options__.frozen

    if not isinstance(allow_partial, bool):
        raise TypeError(
            f"Expect bool type for argument 'allow_partial' in "
            f'symbolic.Object.__init__ but encountered {allow_partial}.'
        )

    # Fast path: const-keyed schema.
    if cls._sym_init_fields is not None and self._init_fast_path(
        kwargs,
        allow_partial,
        sealed,
        root_path,
        explicit_init,
    ):
        return

    # Slow path: handles varargs and schemas with non-const keys.

    # Model before-hook (#516) — the fast path was not taken
    # (`_sym_init_fields is None`), so the hook has not run yet.
    if cls._sym_preinit_overridden:
        kwargs = _apply_preinit(cls, kwargs)

    # We delay the seal attempt until members are all set.
    super().__init__(
        allow_partial=allow_partial,
        accessor_writable=cls.__sym_options__.attr_write,
        sealed=sealed,
        root_path=root_path,
        init_super=not explicit_init,
    )

    # Fill field_args and init_args from **kwargs.
    _, unmatched_keys = cls.__schema__.resolve(list(kwargs.keys()))
    # Fields opted out of `__init__` via `enable_init=False` resolve
    # against the schema but must still be rejected as kwargs.
    get_field = cls.__schema__.get_field
    unmatched_keys = list(unmatched_keys) + [
        k
        for k in kwargs
        if (f := get_field(k)) is not None and not f.enable_init
    ]
    if unmatched_keys:
        _screen_unexpected_init_keys(cls, kwargs, unmatched_keys)

    # Declared aliases are RESERVED wire spellings (#517, owner ruling):
    # an open (varkw) schema must not accept an extra kwarg equal to a
    # field's alias — the wire would become ambiguous. (Const-keyed
    # schemas never reach here with an alias-spelled kwarg; their
    # unknown-key screen already raised — or, under `varkw='ignore'`,
    # raised for alias keys before dropping the rest.)
    if cls._sym_alias_maps is not None:
        alias_to_name = cls._sym_alias_maps[0]
        for k in kwargs:
            if k in alias_to_name:
                raise TypeError(
                    f'{cls.__name__}.__init__() got keyword argument '
                    f'{k!r}, which is reserved as the alias of field '
                    f'{alias_to_name[k]!r}.'
                )

    # All field values come from kwargs — `pg.Object.__init__` is
    # keyword-only.
    field_args = dict(kwargs)

    # Check missing arguments when partial binding is disallowed.
    if not base.accepts_partial(self):
        missing_args = []
        for field in self.__class__.__schema__.fields.values():
            if (
                not field.value.has_default
                and isinstance(field.key, pg_typing.ConstStrKey)
                and field.key not in field_args
            ):
                missing_args.append(str(field.key))
        if missing_args:
            arg_phrase = formatting.auto_plural(
                len(missing_args), 'argument'
            )
            keys_str = formatting.comma_delimited_str(missing_args)
            raise TypeError(
                f'{self.__class__.__name__}.__init__() missing {len(missing_args)} '
                f'required {arg_phrase}: {keys_str}.'
            )

    # NOTE(daiyip): Accessor writable is honored by `Object.__setattr__` so
    # we can always make `_sym_attributes` accessor writable. This prevents
    # a child object's attribute access from being changed when it's
    # attached to a parent whose symbolic attributes are not writable.
    # Per-field validation is gated by `field.enable_validation` inside
    # `Schema.apply` / `Dict._formalized_value` — so a class-wide
    # `validate=False` (resolved into each field) and per-field overrides
    # both flow through naturally here.
    vars(self)['_sym_attributes'] = pg_dict.Dict(
        field_args,  # pyright: ignore[reportArgumentType]
        value_spec=self.__class__.sym_fields,
        allow_partial=allow_partial,
        sealed=sealed,
        accessor_writable=True,
        root_path=root_path,
        as_object_attributes_container=True,
        under_sym_mode=cls.__sym_options__.sym,
    )
    self._sym_attributes.sym_setparent(self)
    # Seed `init=False` fields from their defaults before `on_sym_post_init`
    # runs so `on_sym_ready`/`on_sym_bound` overrides start from a known baseline.
    _seed_non_symbolic_fields(self, cls._non_symbolic_fields)
    # Model after-validator (#516): fires before any lifecycle hook can
    # observe the object, gated like `on_sym_ready` (concrete only).
    if cls._sym_validate_overridden:
        abstract = (
            self._allow_partial and self.sym_partial
        ) or self.sym_puresymbolic
        if not abstract:
            self.on_sym_validate()
    self.on_sym_post_init()
    self.sym_seal(sealed)

UnknownTypedObject

UnknownTypedObject(
    *,
    allow_partial: bool = False,
    sealed: bool | None = None,
    root_path: KeyPath | None = None,
    explicit_init: bool = False,
    **kwargs: Any
)

Bases: UnknownSymbol

Symbolic object for representing objects of unknown-type.

Source code in pygx/symbolic/_object.py
@_filter_typing_traceback_on_raise
def __init__(
    self,
    *,
    allow_partial: bool = False,
    sealed: bool | None = None,
    root_path: topology.KeyPath | None = None,
    explicit_init: bool = False,
    **kwargs: Any,
):
    """Create an Object instance.

    ``pg.Object`` synthesizes a keyword-only ``__init__``. Subclasses
    that need positional arguments must override ``__init__``
    explicitly and translate to keyword arguments before forwarding
    to ``super().__init__(**kwargs)``.

    Args:
      allow_partial: If True, the object can be partial.
      sealed: If True, seal the object from future modification (unless under
        a `pg.as_sealed(False)` context manager). If False, treat the object as
        unsealed. If None, it's determined by `cls.__sym_options__.frozen`.
      root_path: The symbolic path for current object. By default it's None,
        which indicates that newly constructed object does not have a parent.
      explicit_init: Should set to `True` when `__init__` is called via
        `pg.Object.__init__` instead of `super().__init__`.
      **kwargs: key/value arguments that align with the schema. All required
        keys in the schema must be specified, and values should be acceptable
        according to their value spec.

    Raises:
      KeyError: When required key(s) are missing.
      ValueError: When value(s) are not acceptable by their value spec.
    """
    # Placeholder for Google-internal usage instrumentation.

    cls = self.__class__
    # The ever-instantiated latch `apply_schema`'s post-instance guard
    # reads (#432: `update_schema` is only legal before instances exist).
    # Own-`__dict__`, latched on the construction ATTEMPT — mirroring the
    # native core, whose `__native_ctor_meta__` (the equivalent marker) is
    # created on the first instance op. One dict read per construct.
    meta = cls.__sym_meta__
    if not meta.instantiated:
        meta.instantiated = True
    if sealed is None:
        sealed = cls.__sym_options__.frozen

    if not isinstance(allow_partial, bool):
        raise TypeError(
            f"Expect bool type for argument 'allow_partial' in "
            f'symbolic.Object.__init__ but encountered {allow_partial}.'
        )

    # Fast path: const-keyed schema.
    if cls._sym_init_fields is not None and self._init_fast_path(
        kwargs,
        allow_partial,
        sealed,
        root_path,
        explicit_init,
    ):
        return

    # Slow path: handles varargs and schemas with non-const keys.

    # Model before-hook (#516) — the fast path was not taken
    # (`_sym_init_fields is None`), so the hook has not run yet.
    if cls._sym_preinit_overridden:
        kwargs = _apply_preinit(cls, kwargs)

    # We delay the seal attempt until members are all set.
    super().__init__(
        allow_partial=allow_partial,
        accessor_writable=cls.__sym_options__.attr_write,
        sealed=sealed,
        root_path=root_path,
        init_super=not explicit_init,
    )

    # Fill field_args and init_args from **kwargs.
    _, unmatched_keys = cls.__schema__.resolve(list(kwargs.keys()))
    # Fields opted out of `__init__` via `enable_init=False` resolve
    # against the schema but must still be rejected as kwargs.
    get_field = cls.__schema__.get_field
    unmatched_keys = list(unmatched_keys) + [
        k
        for k in kwargs
        if (f := get_field(k)) is not None and not f.enable_init
    ]
    if unmatched_keys:
        _screen_unexpected_init_keys(cls, kwargs, unmatched_keys)

    # Declared aliases are RESERVED wire spellings (#517, owner ruling):
    # an open (varkw) schema must not accept an extra kwarg equal to a
    # field's alias — the wire would become ambiguous. (Const-keyed
    # schemas never reach here with an alias-spelled kwarg; their
    # unknown-key screen already raised — or, under `varkw='ignore'`,
    # raised for alias keys before dropping the rest.)
    if cls._sym_alias_maps is not None:
        alias_to_name = cls._sym_alias_maps[0]
        for k in kwargs:
            if k in alias_to_name:
                raise TypeError(
                    f'{cls.__name__}.__init__() got keyword argument '
                    f'{k!r}, which is reserved as the alias of field '
                    f'{alias_to_name[k]!r}.'
                )

    # All field values come from kwargs — `pg.Object.__init__` is
    # keyword-only.
    field_args = dict(kwargs)

    # Check missing arguments when partial binding is disallowed.
    if not base.accepts_partial(self):
        missing_args = []
        for field in self.__class__.__schema__.fields.values():
            if (
                not field.value.has_default
                and isinstance(field.key, pg_typing.ConstStrKey)
                and field.key not in field_args
            ):
                missing_args.append(str(field.key))
        if missing_args:
            arg_phrase = formatting.auto_plural(
                len(missing_args), 'argument'
            )
            keys_str = formatting.comma_delimited_str(missing_args)
            raise TypeError(
                f'{self.__class__.__name__}.__init__() missing {len(missing_args)} '
                f'required {arg_phrase}: {keys_str}.'
            )

    # NOTE(daiyip): Accessor writable is honored by `Object.__setattr__` so
    # we can always make `_sym_attributes` accessor writable. This prevents
    # a child object's attribute access from being changed when it's
    # attached to a parent whose symbolic attributes are not writable.
    # Per-field validation is gated by `field.enable_validation` inside
    # `Schema.apply` / `Dict._formalized_value` — so a class-wide
    # `validate=False` (resolved into each field) and per-field overrides
    # both flow through naturally here.
    vars(self)['_sym_attributes'] = pg_dict.Dict(
        field_args,  # pyright: ignore[reportArgumentType]
        value_spec=self.__class__.sym_fields,
        allow_partial=allow_partial,
        sealed=sealed,
        accessor_writable=True,
        root_path=root_path,
        as_object_attributes_container=True,
        under_sym_mode=cls.__sym_options__.sym,
    )
    self._sym_attributes.sym_setparent(self)
    # Seed `init=False` fields from their defaults before `on_sym_post_init`
    # runs so `on_sym_ready`/`on_sym_bound` overrides start from a known baseline.
    _seed_non_symbolic_fields(self, cls._non_symbolic_fields)
    # Model after-validator (#516): fires before any lifecycle hook can
    # observe the object, gated like `on_sym_ready` (concrete only).
    if cls._sym_validate_overridden:
        abstract = (
            self._allow_partial and self.sym_partial
        ) or self.sym_puresymbolic
        if not abstract:
            self.on_sym_validate()
    self.on_sym_post_init()
    self.sym_seal(sealed)

sym_jsonify

sym_jsonify(**kwargs) -> WireValue

Converts current object to a dict of plain Python objects.

Source code in pygx/symbolic/_unknown_symbols.py
def sym_jsonify(self, **kwargs) -> wire.WireValue:
    """Converts current object to a dict of plain Python objects."""
    json_dict = self._sym_attributes.to_json(
        exclude_keys={'type_name'}, **kwargs
    )
    assert isinstance(json_dict, dict)
    json_dict[wire.WireConvertible.TYPE_NAME_KEY] = self.type_name
    return json_dict

clone

clone(
    x: _CloneT,
    deep: bool = False,
    memo: Any | None = None,
    override: Mapping[Any, Any] | None = None,
) -> _CloneT

Clones a value. Use symbolic clone if possible.

Example::

class A(pg.Object): x: int y: Any

# B is not a symbolic object. class B: pass

# Shallow copy on non-symbolic values (by reference). a = A(1, B()) b = pg.clone(a) assert pg.eq(a, b) assert a.y is b.y

# Deepcopy on non-symbolic values. c = pg.clone(a, deep=True) assert pg.ne(a, c) assert a.y is not c.y

# Copy with override d = pg.clone(a, override={'x': 2}) assert d.x == 2 assert d.y is a.y

Parameters:

Name Type Description Default
x _CloneT

value to clone.

required
deep bool

If True, use deep clone, otherwise use shallow clone.

False
memo Any | None

Optional memo object for deep clone.

None
override Mapping[Any, Any] | None

Value to override if value is symbolic.

None

Returns:

Type Description
_CloneT

Cloned instance.

Source code in pygx/symbolic/_base.py
def clone(
    x: _CloneT,
    deep: bool = False,
    memo: Any | None = None,
    override: Mapping[Any, Any] | None = None,
) -> _CloneT:
    """Clones a value. Use symbolic clone if possible.

    Example::

      class A(pg.Object):
        x: int
        y: Any

      # B is not a symbolic object.
      class B:
        pass

      # Shallow copy on non-symbolic values (by reference).
      a = A(1, B())
      b = pg.clone(a)
      assert pg.eq(a, b)
      assert a.y is b.y

      # Deepcopy on non-symbolic values.
      c = pg.clone(a, deep=True)
      assert pg.ne(a, c)
      assert a.y is not c.y

      # Copy with override
      d = pg.clone(a, override={'x': 2})
      assert d.x == 2
      assert d.y is a.y

    Args:
      x: value to clone.
      deep: If True, use deep clone, otherwise use shallow clone.
      memo: Optional memo object for deep clone.
      override: Value to override if value is symbolic.

    Returns:
      Cloned instance.
    """
    if isinstance(x, Symbolic):
        # `sym_clone` (the public entry) layers override-rebind + origin-tracking
        # on top of the `_sym_clone` core.
        return cast(_CloneT, x.sym_clone(deep, memo, override))
    elif isinstance(x, list):
        assert not override, override
        return cast(_CloneT, [clone(v, deep, memo) for v in x])
    elif isinstance(x, tuple):
        assert not override, override
        return cast(_CloneT, tuple([clone(v, deep, memo) for v in x]))
    elif isinstance(x, dict):
        assert not override, override
        return cast(_CloneT, {k: clone(v, deep, memo) for k, v in x.items()})
    else:
        assert not override, override
        return copy.deepcopy(x, memo) if deep else copy.copy(x)

contains

contains(
    x: Any,
    value: Any = None,
    type: type[Any] | tuple[type[Any], ...] | None = None,
) -> bool

Returns if a value contains values of specific type.

Example::

class A(pg.Object): x: Any y: Any

# Test if a symbolic tree contains a value. assert pg.contains(A('a', 'b'), 'a') assert not pg.contains(A('a', 'b'), A)

# Test if a symbolic tree contains a type. assert pg.contains({'x': A(1, 2)}, type=A) assert pg.contains({'x': A(1, 2)}, type=int) assert pg.contains({'x': A(1, 2)}, type=(int, float))

Parameters:

Name Type Description Default
x Any

The source value to query against.

required
value Any

Value of sub-node to contain. Applicable when type is None.

None
type type[Any] | tuple[type[Any], ...] | None

A type or a tuple of types for the sub-nodes. Applicable if not None.

None

Returns:

Type Description
bool

True if x itself or any of its sub-nodes equal to value or

bool

is an instance of value_type.

Source code in pygx/symbolic/_base.py
def contains(
    x: Any,
    value: Any = None,
    type: (  # pylint: disable=redefined-builtin
        type[Any] | tuple[type[Any], ...] | None
    ) = None,
) -> bool:
    """Returns if a value contains values of specific type.

    Example::

      class A(pg.Object):
        x: Any
        y: Any

      # Test if a symbolic tree contains a value.
      assert pg.contains(A('a', 'b'), 'a')
      assert not pg.contains(A('a', 'b'), A)

      # Test if a symbolic tree contains a type.
      assert pg.contains({'x': A(1, 2)}, type=A)
      assert pg.contains({'x': A(1, 2)}, type=int)
      assert pg.contains({'x': A(1, 2)}, type=(int, float))

    Args:
      x: The source value to query against.
      value: Value of sub-node to contain. Applicable when `type` is None.
      type: A type or a tuple of types for the sub-nodes. Applicable if
        not None.

    Returns:
      True if `x` itself or any of its sub-nodes equal to `value` or
      is an instance of `value_type`.
    """
    # Share walkers with `Symbolic.sym_contains` (avoids `traverse`'s `KeyPath`
    # allocation per node, which is unused here).
    if type is not None:
        return _walk_contains_type(x, type)
    return _walk_contains_value(x, value)

eq

eq(left: Any, right: Any) -> bool

Compares if two values are equal. Use symbolic equality if possible.

Example::

class A(pg.Object): x: Any

def sym_eq(self, right):
  if super().sym_eq(right):
    return True
  return pg.eq(self.x, right)

class B: pass

assert pg.eq(1, 1) assert pg.eq(A(1), A(1)) # This is True since A has override sym_eq. assert pg.eq(A(1), 1) # Objects of B are compared by references. assert not pg.eq(A(B()), A(B()))

Parameters:

Name Type Description Default
left Any

The left-hand value to compare.

required
right Any

The right-hand value to compare.

required

Returns:

Type Description
bool

True if left and right is equal or symbolically equal. Otherwise False.

Source code in pygx/symbolic/_base.py
def eq(left: Any, right: Any) -> bool:
    """Compares if two values are equal. Use symbolic equality if possible.

    Example::

      class A(pg.Object):
        x: Any

        def sym_eq(self, right):
          if super().sym_eq(right):
            return True
          return pg.eq(self.x, right)

      class B:
        pass

      assert pg.eq(1, 1)
      assert pg.eq(A(1), A(1))
      # This is True since A has override `sym_eq`.
      assert pg.eq(A(1), 1)
      # Objects of B are compared by references.
      assert not pg.eq(A(B()), A(B()))

    Args:
      left: The left-hand value to compare.
      right: The right-hand value to compare.

    Returns:
      True if left and right is equal or symbolically equal. Otherwise False.
    """
    # NOTE(daiyip): the default behavior for dict/list/tuple comparison is that
    # it compares the elements using __eq__, __ne__. For symbolic comparison on
    # these container types, we need to change the behavior by using symbolic
    # comparison on their items.
    if left is right:
        return True
    # Primitive leaves: short-circuit to `==`. Dominant case when comparing
    # items inside `Dict`/`List`/`Object`.
    if type(left) in PRIMITIVE_TYPES:
        return left == right
    if (isinstance(left, list) and isinstance(right, list)) or (
        isinstance(left, tuple) and isinstance(right, tuple)
    ):
        if len(left) != len(right):
            return False
        for x, y in zip(left, right):
            if ne(x, y):
                return False
        return True
    elif isinstance(left, dict):
        if (
            not isinstance(right, dict)
            or len(left) != len(right)
            or set(left.keys()) != set(right.keys())
        ):
            return False
        # NOTE(daiyip): pg.Dict.__getitem__ will trigger inferred value
        # evaluation, therefore we always get its symbolic form during traversal.
        left_items = (
            left.sym_items if isinstance(left, Symbolic) else left.items
        )
        right_item = (
            right.sym_getattr
            if isinstance(right, Symbolic)
            else right.__getitem__
        )
        for k, v in left_items():
            if ne(v, right_item(k)):
                return False
        return True
    # We compare sym_eq with Symbolic.sym_eq to avoid endless recursion.
    left_any: Any = left
    right_any: Any = right
    if (
        hasattr(left, 'sym_eq')
        and not inspect.isclass(left)
        and left_any.sym_eq.__code__ is not Symbolic.sym_eq.__code__
    ):
        return left_any.sym_eq(right)
    elif (
        hasattr(right, 'sym_eq')
        and not inspect.isclass(right)
        and right_any.sym_eq.__code__ is not Symbolic.sym_eq.__code__
    ):
        return right_any.sym_eq(left)
    # Compare two maybe callable objects.
    return pg_typing.callable_eq(left, right)

from_json

from_json(
    json_value: Any,
    *,
    expected_type: type[T],
    context: WireConversionContext | None = ...,
    wrap: bool = ...,
    auto_import: bool = ...,
    convert_unknown: bool = ...,
    allow_partial: bool = ...,
    root_path: KeyPath | None = ...,
    value_spec: ValueSpec | None = ...,
    **kwargs: Any
) -> T
from_json(
    json_value: Any,
    *,
    expected_type: Any = ...,
    context: WireConversionContext | None = ...,
    wrap: bool = ...,
    auto_import: bool = ...,
    convert_unknown: bool = ...,
    allow_partial: bool = ...,
    root_path: KeyPath | None = ...,
    value_spec: ValueSpec | None = ...,
    **kwargs: Any
) -> Any
from_json(
    json_value: Any,
    *,
    expected_type: Any = Any,
    context: WireConversionContext | None = None,
    wrap: bool = True,
    auto_import: bool = True,
    convert_unknown: bool = False,
    allow_partial: bool = False,
    root_path: KeyPath | None = None,
    value_spec: ValueSpec | None = None,
    **kwargs: Any
) -> Any

Deserializes a (maybe) symbolic value from JSON value.

Example::

class A(pg.Object): x: Any

a1 = A(1) json = a1.to_json() a2 = pg.from_json(json) assert pg.eq(a1, a2)

# Narrow the static return type and assert at runtime: a3 = pg.from_json(json, expected_type=A) # statically typed as A.

Parameters:

Name Type Description Default
json_value Any

Input JSON value.

required
expected_type Any

When provided (anything other than the default Any), the deserialized result must be an instance of this type — pyright narrows the return to it, and a TypeError is raised at runtime if the actual value does not match. Defaults to Any (no check, return widened to Any).

Any
context WireConversionContext | None

JSON conversion context.

None
wrap bool

If True (default), schemaless JSON list / dict nodes are wrapped into pg.List / pg.Dict and the flag propagates through every nested schemaless layer. If False, they stay raw. The flag does NOT reach into typed Object subtrees ({"_type": "Foo", ...}); those defer to Foo's field-level wrap setting. Embedded __symbolic__: true markers in JSON force wrapping for a specific subtree regardless of this flag.

True
auto_import bool

If True, when a '_type' is not registered, PyGX will identify its parent module and automatically import it. For example, if the type is 'foo.bar.A', PyGX will try to import 'foo.bar' and find the class 'A' within the imported module.

True
convert_unknown bool

If True, when a '_type' is not registered and cannot be imported, PyGX will create objects of: - pg.symbolic.UnknownType for unknown types; - pg.symbolic.UnknownTypedObject for objects of unknown types; - pg.symbolic.UnknownFunction for unknown functions; - pg.symbolic.UnknownMethod for unknown methods. If False, TypeError will be raised.

False
allow_partial bool

Whether to allow elements of the list to be partial.

False
root_path KeyPath | None

KeyPath of loaded object in its object tree.

None
value_spec ValueSpec | None

The value spec for the symbolic list or dict.

None
**kwargs Any

Allow passing through keyword arguments to from_json of specific types.

{}

Returns:

Type Description
Any

Deserialized value, which is

Any
  • pg.Dict for dict.
Any
  • pg.List for list.
Any
  • symbolic.Object for dict with '_type' property.
Any
  • value itself.

Raises:

Type Description
TypeError

If expected_type is provided and the deserialized value is not an instance of it.

Source code in pygx/symbolic/_base.py
def from_json(
    json_value: Any,
    *,
    expected_type: Any = Any,
    context: wire.WireConversionContext | None = None,
    wrap: bool = True,
    auto_import: bool = True,
    convert_unknown: bool = False,
    allow_partial: bool = False,
    root_path: topology.KeyPath | None = None,
    value_spec: pg_typing.ValueSpec | None = None,
    **kwargs: Any,
) -> Any:
    """Deserializes a (maybe) symbolic value from JSON value.

    Example::

      class A(pg.Object):
        x: Any

      a1 = A(1)
      json = a1.to_json()
      a2 = pg.from_json(json)
      assert pg.eq(a1, a2)

      # Narrow the static return type and assert at runtime:
      a3 = pg.from_json(json, expected_type=A)  # statically typed as ``A``.

    Args:
      json_value: Input JSON value.
      expected_type: When provided (anything other than the default ``Any``),
        the deserialized result must be an instance of this type — pyright
        narrows the return to it, and a ``TypeError`` is raised at runtime if
        the actual value does not match. Defaults to ``Any`` (no check, return
        widened to ``Any``).
      context: JSON conversion context.
      wrap: If True (default), schemaless JSON ``list`` / ``dict``
        nodes are wrapped into ``pg.List`` / ``pg.Dict`` and the flag
        propagates through every nested schemaless layer. If False,
        they stay raw. The flag does NOT reach into typed Object
        subtrees (``{"_type": "Foo", ...}``); those defer to ``Foo``'s
        field-level ``wrap`` setting. Embedded
        ``__symbolic__: true`` markers in JSON force wrapping for a
        specific subtree regardless of this flag.
      auto_import: If True, when a '_type' is not registered, PyGX will
        identify its parent module and automatically import it. For example,
        if the type is 'foo.bar.A', PyGX will try to import 'foo.bar' and
        find the class 'A' within the imported module.
      convert_unknown: If True, when a '_type' is not registered and cannot
        be imported, PyGX will create objects of:
          - `pg.symbolic.UnknownType` for unknown types;
          - `pg.symbolic.UnknownTypedObject` for objects of unknown types;
          - `pg.symbolic.UnknownFunction` for unknown functions;
          - `pg.symbolic.UnknownMethod` for unknown methods.
        If False, TypeError will be raised.
      allow_partial: Whether to allow elements of the list to be partial.
      root_path: KeyPath of loaded object in its object tree.
      value_spec: The value spec for the symbolic list or dict.
      **kwargs: Allow passing through keyword arguments to from_json of specific
        types.

    Returns:
      Deserialized value, which is
      * pg.Dict for dict.
      * pg.List for list.
      * symbolic.Object for dict with '_type' property.
      * value itself.

    Raises:
      TypeError: If ``expected_type`` is provided and the deserialized value
        is not an instance of it.
    """
    value = _from_json_impl(
        json_value,
        context=context,
        wrap=wrap,
        auto_import=auto_import,
        convert_unknown=convert_unknown,
        allow_partial=allow_partial,
        root_path=root_path,
        value_spec=value_spec,
        **kwargs,
    )
    return _enforce_expected_type(value, expected_type)

from_json_str

from_json_str(
    json_str: bytes | str,
    *,
    expected_type: type[T],
    context: WireConversionContext | None = ...,
    auto_import: bool = ...,
    convert_unknown: bool = ...,
    allow_partial: bool = ...,
    root_path: KeyPath | None = ...,
    value_spec: ValueSpec | None = ...,
    format: str | WireFormat = ...,
    **kwargs: Any
) -> T
from_json_str(
    json_str: bytes | str,
    *,
    expected_type: Any = ...,
    context: WireConversionContext | None = ...,
    auto_import: bool = ...,
    convert_unknown: bool = ...,
    allow_partial: bool = ...,
    root_path: KeyPath | None = ...,
    value_spec: ValueSpec | None = ...,
    format: str | WireFormat = ...,
    **kwargs: Any
) -> Any
from_json_str(
    json_str: bytes | str,
    *,
    expected_type: Any = Any,
    context: WireConversionContext | None = None,
    auto_import: bool = True,
    convert_unknown: bool = False,
    allow_partial: bool = False,
    root_path: KeyPath | None = None,
    value_spec: ValueSpec | None = None,
    format: str | WireFormat = "json",
    **kwargs: Any
) -> Any

Deserialize (maybe) symbolic object from JSON string.

Example::

class A(pg.Object): x: Any

a1 = A(1) json_str = a1.to_json_str() a2 = pg.from_json_str(json_str) assert pg.eq(a1, a2)

# Narrow the static return type and assert at runtime: a3 = pg.from_json_str(json_str, expected_type=A)

Parameters:

Name Type Description Default
json_str bytes | str

JSON string.

required
expected_type Any

When provided (anything other than the default Any), the deserialized result must be an instance of this type — pyright narrows the return to it, and a TypeError is raised at runtime if the actual value does not match. Defaults to Any (no check).

Any
context WireConversionContext | None

JSON conversion context.

None
auto_import bool

If True, when a '_type' is not registered, PyGX will identify its parent module and automatically import it. For example, if the type is 'foo.bar.A', PyGX will try to import 'foo.bar' and find the class 'A' within the imported module.

True
convert_unknown bool

If True, when a '_type' is not registered and cannot be imported, PyGX will create objects of: - pg.symbolic.UnknownType for unknown types; - pg.symbolic.UnknownTypedObject for objects of unknown types; - pg.symbolic.UnknownFunction for unknown functions; - pg.symbolic.UnknownMethod for unknown methods. If False, TypeError will be raised.

False
allow_partial bool

If True, allow a partial symbolic object to be created. Otherwise error will be raised on partial value.

False
root_path KeyPath | None

The symbolic path used for the deserialized root object.

None
value_spec ValueSpec | None

The value spec for the symbolic list or dict.

None
format str | WireFormat

The wire format to decode with -- a registered format name (e.g. 'json', 'yaml') or a pg.WireFormat instance. Defaults to 'json'.

'json'
**kwargs Any

Additional keyword arguments that will be passed to pg.from_json.

{}

Returns:

Type Description
Any

A deserialized value.

Raises:

Type Description
TypeError

If expected_type is provided and the deserialized value is not an instance of it.

Source code in pygx/symbolic/_base.py
def from_json_str(
    json_str: bytes | str,
    *,
    expected_type: Any = Any,
    context: wire.WireConversionContext | None = None,
    auto_import: bool = True,
    convert_unknown: bool = False,
    allow_partial: bool = False,
    root_path: topology.KeyPath | None = None,
    value_spec: pg_typing.ValueSpec | None = None,
    format: str | wire.WireFormat = 'json',  # pylint: disable=redefined-builtin
    **kwargs: Any,
) -> Any:
    """Deserialize (maybe) symbolic object from JSON string.

    Example::

      class A(pg.Object):
        x: Any

      a1 = A(1)
      json_str = a1.to_json_str()
      a2 = pg.from_json_str(json_str)
      assert pg.eq(a1, a2)

      # Narrow the static return type and assert at runtime:
      a3 = pg.from_json_str(json_str, expected_type=A)

    Args:
      json_str: JSON string.
      expected_type: When provided (anything other than the default ``Any``),
        the deserialized result must be an instance of this type — pyright
        narrows the return to it, and a ``TypeError`` is raised at runtime if
        the actual value does not match. Defaults to ``Any`` (no check).
      context: JSON conversion context.
      auto_import: If True, when a '_type' is not registered, PyGX will
        identify its parent module and automatically import it. For example,
        if the type is 'foo.bar.A', PyGX will try to import 'foo.bar' and
        find the class 'A' within the imported module.
      convert_unknown: If True, when a '_type' is not registered and cannot
        be imported, PyGX will create objects of:
          - `pg.symbolic.UnknownType` for unknown types;
          - `pg.symbolic.UnknownTypedObject` for objects of unknown types;
          - `pg.symbolic.UnknownFunction` for unknown functions;
          - `pg.symbolic.UnknownMethod` for unknown methods.
        If False, TypeError will be raised.
      allow_partial: If True, allow a partial symbolic object to be created.
        Otherwise error will be raised on partial value.
      root_path: The symbolic path used for the deserialized root object.
      value_spec: The value spec for the symbolic list or dict.
      format: The wire format to decode with -- a registered format name
        (e.g. ``'json'``, ``'yaml'``) or a ``pg.WireFormat`` instance.
        Defaults to ``'json'``.
      **kwargs: Additional keyword arguments that will be passed to
        ``pg.from_json``.

    Returns:
      A deserialized value.

    Raises:
      TypeError: If ``expected_type`` is provided and the deserialized value
        is not an instance of it.
    """
    return from_json(
        wire.get_format(format).decode(json_str),
        expected_type=expected_type,
        context=context,
        auto_import=auto_import,
        convert_unknown=convert_unknown,
        allow_partial=allow_partial,
        root_path=root_path,
        value_spec=value_spec,
        **kwargs,
    )

gt

gt(left: Any, right: Any) -> bool

Returns True if a value is symbolically greater than the other value.

Refer to pygx.lt for the definition of symbolic comparison.

Parameters:

Name Type Description Default
left Any

The left-hand value to compare.

required
right Any

The right-hand value to compare.

required

Returns:

Type Description
bool

True if the left value is symbolically greater than the right value.

Source code in pygx/symbolic/_base.py
def gt(left: Any, right: Any) -> bool:
    """Returns True if a value is symbolically greater than the other value.

    Refer to [`pygx.lt`][pygx.symbolic.lt] for the definition of symbolic comparison.

    Args:
      left: The left-hand value to compare.
      right: The right-hand value to compare.

    Returns:
      True if the left value is symbolically greater than the right value.
    """
    return lt(right, left)  # pylint: disable=arguments-out-of-order

is_abstract

is_abstract(x: Any) -> bool

Returns if the input value is abstract.

Example::

@pg.symbolize class Foo: def init(self, x): pass

class Bar(pg.PureSymbolic): pass

assert not pg.is_abstract(1) assert not pg.is_abstract(Foo(1)) assert pg.is_abstract(Foo.partial()) assert pg.is_abstract(Bar()) assert pg.is_abstract(Foo(Bar())) assert pg.is_abstract(Foo(pg.oneof([1, 2])))

Parameters:

Name Type Description Default
x Any

Value to query against.

required

Returns:

Type Description
bool

True if value itself is partial/PureSymbolic or its child and nested

bool

child fields contain partial/PureSymbolic values.

Source code in pygx/symbolic/_base.py
def is_abstract(x: Any) -> bool:
    """Returns if the input value is abstract.

    Example::

      @pg.symbolize
      class Foo:
        def __init__(self, x):
          pass

      class Bar(pg.PureSymbolic):
        pass

      assert not pg.is_abstract(1)
      assert not pg.is_abstract(Foo(1))
      assert pg.is_abstract(Foo.partial())
      assert pg.is_abstract(Bar())
      assert pg.is_abstract(Foo(Bar()))
      assert pg.is_abstract(Foo(pg.oneof([1, 2])))

    Args:
      x: Value to query against.

    Returns:
      True if value itself is partial/PureSymbolic or its child and nested
      child fields contain partial/PureSymbolic values.
    """
    return topology.is_partial(x) or is_pure_symbolic(x)

is_deterministic

is_deterministic(x: Any) -> bool

Returns if the input value is deterministic.

Example::

@pg.symbolize def foo(x, y): pass

assert pg.is_deterministic(1) assert pg.is_deterministic(foo(1, 2)) assert not pg.is_deterministic(pg.oneof([1, 2])) assert not pg.is_deterministic(foo(pg.oneof([1, 2]), 3))

Parameters:

Name Type Description Default
x Any

Value to query against.

required

Returns:

Type Description
bool

True if value itself is not NonDeterministic and its child and nested

bool

child fields do not contain NonDeterministic values.

Source code in pygx/symbolic/_base.py
def is_deterministic(x: Any) -> bool:
    """Returns if the input value is deterministic.

    Example::

      @pg.symbolize
      def foo(x, y):
        pass

      assert pg.is_deterministic(1)
      assert pg.is_deterministic(foo(1, 2))
      assert not pg.is_deterministic(pg.oneof([1, 2]))
      assert not pg.is_deterministic(foo(pg.oneof([1, 2]), 3))

    Args:
      x: Value to query against.

    Returns:
      True if value itself is not NonDeterministic and its child and nested
      child fields do not contain NonDeterministic values.
    """
    return not contains(x, type=NonDeterministic)

is_pure_symbolic

is_pure_symbolic(x: Any) -> bool

Returns if the input value is pure symbolic.

Example::

class Bar(pg.PureSymbolic): pass

@pg.symbolize def foo(x, y): pass

assert not pg.is_pure_symbolic(1) assert not pg.is_pure_symbolic(foo(1, 2)) assert pg.is_pure_symbolic(Bar()) assert pg.is_pure_symbolic(foo(Bar(), 1)) assert pg.is_pure_symbolic(foo(pg.oneof([1, 2]), 1))

Parameters:

Name Type Description Default
x Any

Value to query against.

required

Returns:

Type Description
bool

True if value itself is PureSymbolic or its child and nested

bool

child fields contain PureSymbolic values.

Source code in pygx/symbolic/_base.py
def is_pure_symbolic(x: Any) -> bool:
    """Returns if the input value is pure symbolic.

    Example::

      class Bar(pg.PureSymbolic):
        pass

      @pg.symbolize
      def foo(x, y):
        pass

      assert not pg.is_pure_symbolic(1)
      assert not pg.is_pure_symbolic(foo(1, 2))
      assert pg.is_pure_symbolic(Bar())
      assert pg.is_pure_symbolic(foo(Bar(), 1))
      assert pg.is_pure_symbolic(foo(pg.oneof([1, 2]), 1))

    Args:
      x: Value to query against.

    Returns:
      True if value itself is PureSymbolic or its child and nested
      child fields contain PureSymbolic values.
    """

    def _check_pure_symbolic(k, v, p):
        del k, p
        if isinstance(v, PureSymbolic) or (
            isinstance(v, Symbolic) and v.sym_puresymbolic
        ):
            return TraverseAction.STOP
        else:
            return TraverseAction.ENTER

    return not traverse(x, _check_pure_symbolic)

lt

lt(left: Any, right: Any) -> bool

Returns True if a value is symbolically less than the other value.

Symbolic values are comparable by their symbolic representations. For common types such as numbers and string, symbolic comparison returns the same value as value comparisons. For example::

assert pg.lt(False, True) == Flase < True assert pg.lt(0.1, 1) == 0.1 < 1 assert pg.lt('a', 'ab') == 'a' < 'ab'

However, symbolic comparison can be applied on hierarchical values, for example::

assert pg.lt(['a'], ['a', 'b']) assert pg.lt(['a', 'b', 'c'], ['b']) assert pg.lt({'x': 1}, {'x': 2}) assert pg.lt({'x': 1}, {'y': 1}) assert pg.lt(A(x=1), A(x=2))

Also, symbolic values of different types can be compared, for example::

assert pg.lt(pg.MISSING_VALUE, None) assert pg.lt(None, 1) assert pg.lt(1, 'abc') assert pg.lt('abc', []) assert pg.lt([], {}) assert pg.lt([], A(x=1))

The high-level idea is that a value with lower information entropy is less than a value with higher information entropy. As a result, we know that pg.MISSING_VALUE is the smallest among all values.

The order of symbolic representation are defined by the following rules:

1) If x and y are comparable by their values, they will be compared using operator <. (e.g. bool, int, float, str) 2) If x and y are not directly comparable and are different in their types, they will be compared based on their types. The order of different types are: pg.MISSING_VALUE, NoneType, bool, int, float, str, list, tuple, set, dict, functions/classes. When different functions/classes compare, their order is determined by their qualified name. 3) If x and y are of the same type, which are symbolic containers (e.g. list, dict, pg.Symbolic objects), their order will be determined by the order of their first sub-nodes which are different. Therefore ['b'] is greater than ['a', 'b'], though the later have 2 elements. 4) Non-symbolic classes can define method sym_lt to enable symbolic comparison. 5) Two pg.Object values of the same type are ordered over their compare=True fields only (the same fields that feed pg.eq), so a compare=False field never affects pg.lt / pg.gt — keeping equality and ordering consistent (exactly one of < / == / > holds).

Parameters:

Name Type Description Default
left Any

The left-hand value to compare.

required
right Any

The right-hand value to compare.

required

Returns:

Type Description
bool

True if the left value is symbolically less than the right value.

Source code in pygx/symbolic/_base.py
def lt(left: Any, right: Any) -> bool:
    """Returns True if a value is symbolically less than the other value.

    Symbolic values are comparable by their symbolic representations. For common
    types such as numbers and string, symbolic comparison returns the same value
    as value comparisons. For example::

      assert pg.lt(False, True) == Flase < True
      assert pg.lt(0.1, 1) == 0.1 < 1
      assert pg.lt('a', 'ab') == 'a' < 'ab'

    However, symbolic comparison can be applied on hierarchical values, for
    example::

      assert pg.lt(['a'], ['a', 'b'])
      assert pg.lt(['a', 'b', 'c'], ['b'])
      assert pg.lt({'x': 1}, {'x': 2})
      assert pg.lt({'x': 1}, {'y': 1})
      assert pg.lt(A(x=1), A(x=2))

    Also, symbolic values of different types can be compared, for example::

      assert pg.lt(pg.MISSING_VALUE, None)
      assert pg.lt(None, 1)
      assert pg.lt(1, 'abc')
      assert pg.lt('abc', [])
      assert pg.lt([], {})
      assert pg.lt([], A(x=1))

    The high-level idea is that a value with lower information entropy is less
    than a value with higher information entropy. As a result, we know that
    `pg.MISSING_VALUE` is the smallest among all values.

    The order of symbolic representation are defined by the following rules:

    1) If x and y are comparable by their values, they will be compared using
       operator <. (e.g. bool, int, float, str)
    2) If x and y are not directly comparable and are different in their types,
       they will be compared based on their types. The order of different types
       are: pg.MISSING_VALUE, NoneType, bool, int, float, str, list, tuple, set,
       dict, functions/classes. When different functions/classes compare, their
       order is determined by their qualified name.
    3) If x and y are of the same type, which are symbolic containers (e.g. list,
       dict, pg.Symbolic objects), their order will be determined by the order of
       their first sub-nodes which are different. Therefore ['b'] is greater than
       ['a', 'b'], though the later have 2 elements.
    4) Non-symbolic classes can define method `sym_lt` to enable symbolic
       comparison.
    5) Two `pg.Object` values of the same type are ordered over their
       `compare=True` fields only (the same fields that feed `pg.eq`), so a
       `compare=False` field never affects `pg.lt` / `pg.gt` — keeping
       equality and ordering consistent (exactly one of `<` / `==` / `>`
       holds).

    Args:
      left: The left-hand value to compare.
      right: The right-hand value to compare.

    Returns:
      True if the left value is symbolically less than the right value.
    """
    # A fast type check can eliminate most
    if type(left) is not type(right):
        tol = _type_order(left)
        tor = _type_order(right)
        # When tol == tor, this means different types are treated as same symbols.
        # E.g. list and pg.List.
        if tol != tor:
            return tol < tor

    # Primitive leaves: short-circuit to `<`. Exact-type check is faster than
    # `isinstance` and avoids hitting types where `<` is undefined (`None`).
    if type(left) in _LT_PRIMITIVE_TYPES:
        return left < right
    elif isinstance(left, list):
        min_len = min(len(left), len(right))
        for i in range(min_len):
            l, r = left[i], right[i]
            if not eq(l, r):
                return lt(l, r)
        # `left` and `right` are equal so far, so `left` is less than `right`
        # only when left has a smaller length.
        return len(left) < len(right)
    elif isinstance(left, dict):
        lkeys = list(left.keys())
        rkeys = list(right.keys())
        min_len = min(len(lkeys), len(rkeys))
        for i in range(min_len):
            kl, kr = lkeys[i], rkeys[i]
            if kl == kr:
                if not eq(left[kl], right[kr]):
                    return lt(left[kl], right[kr])
            else:
                return kl < kr
        # `left` and `right` are equal so far, so `left is less than `right`
        # only when left has fewer keys.
        return len(lkeys) < len(rkeys)
    elif hasattr(left, 'sym_lt'):
        return left.sym_lt(right)
    return left < right

ne

ne(left: Any, right: Any) -> bool

Compares if two values are not equal. Use symbolic equality if possible.

Example::

class A(pg.Object): x: Any

def sym_eq(self, right):
  if super().sym_eq(right):
    return True
  return pg.eq(self.x, right)

class B: pass

assert pg.ne(1, 2) assert pg.ne(A(1), A(2)) # A has override sym_eq. assert not pg.ne(A(1), 1) # Objects of B are compared by references. assert pg.ne(A(B()), A(B()))

Parameters:

Name Type Description Default
left Any

The left-hand value to compare.

required
right Any

The right-hand value to compare.

required

Returns:

Type Description
bool

True if left and right is not equal or symbolically equal. Otherwise False.

Source code in pygx/symbolic/_base.py
def ne(left: Any, right: Any) -> bool:
    """Compares if two values are not equal. Use symbolic equality if possible.

    Example::

      class A(pg.Object):
        x: Any

        def sym_eq(self, right):
          if super().sym_eq(right):
            return True
          return pg.eq(self.x, right)

      class B:
        pass

      assert pg.ne(1, 2)
      assert pg.ne(A(1), A(2))
      # A has override `sym_eq`.
      assert not pg.ne(A(1), 1)
      # Objects of B are compared by references.
      assert pg.ne(A(B()), A(B()))

    Args:
      left: The left-hand value to compare.
      right: The right-hand value to compare.

    Returns:
      True if left and right is not equal or symbolically equal. Otherwise False.
    """
    return not eq(left, right)

query

query(
    x: Any,
    path_regex: str | None = None,
    where: None | (Callable[[Any], bool] | Callable[[Any, Any], bool]) = None,
    enter_selected: bool = False,
    custom_selector: None | (
        Callable[[KeyPath, Any], bool] | Callable[[KeyPath, Any, Any], bool]
    ) = None,
) -> dict[str, Any]

Queries a (maybe) symbolic value.

Example::

class A(pg.Object):
  x: int
  y: int

value = {
  'a1': A(x=0, y=1),
  'a2': [A(x=1, y=1), A(x=1, y=2)],
  'a3': {
    'p': A(x=2, y=1),
    'q': A(x=2, y=2)
  }
}

# Query by path regex.
# Shall print:
# {'a3.p': A(x=2, y=1)}
print(pg.query(value, r'.*p'))

# Query by value.
# Shall print:
# {
#    'a2[1].y': 2,
#    'a3.p.x': 2,
#    'a3.q.x': 2,
#    'a3.q.y': 2,
# }
print(pg.query(value, where=lambda v: v==2))

# Query by path, value and parent.
# Shall print:
# {
#    'a2[1].y': 2,
# }
print(pg.query(
    value, r'.*y',
    where=lambda v, p: v > 1 and isinstance(p, A) and p.x == 1))

Parameters:

Name Type Description Default
x Any

A nested structure that may contains symbolic value.

required
path_regex str | None

Optional regex expression to constrain path.

None
where None | (Callable[[Any], bool] | Callable[[Any, Any], bool])

Optional callable to constrain value and parent when path matches with path_regex or path_regex is not provided. The signature is:

(value) -> should_select or (value, parent) -> should_select

None
enter_selected bool

If True, if a node is selected, enter the node and query its sub-nodes.

False
custom_selector None | (Callable[[KeyPath, Any], bool] | Callable[[KeyPath, Any, Any], bool])

Optional callable object as custom selector. When custom_selector is provided, path_regex and where must be None. The signature of custom_selector is:

(key_path, value) -> should_select or (key_path, value, parent) -> should_select

None

Returns:

Type Description
dict[str, Any]

A dict of key path to value as results for selected values.

Source code in pygx/symbolic/_base.py
def query(
    x: Any,
    path_regex: str | None = None,
    where: None | (Callable[[Any], bool] | Callable[[Any, Any], bool]) = None,
    enter_selected: bool = False,
    custom_selector: None
    | (
        Callable[[topology.KeyPath, Any], bool]
        | Callable[[topology.KeyPath, Any, Any], bool]
    ) = None,
) -> dict[str, Any]:
    """Queries a (maybe) symbolic value.

    Example::

        class A(pg.Object):
          x: int
          y: int

        value = {
          'a1': A(x=0, y=1),
          'a2': [A(x=1, y=1), A(x=1, y=2)],
          'a3': {
            'p': A(x=2, y=1),
            'q': A(x=2, y=2)
          }
        }

        # Query by path regex.
        # Shall print:
        # {'a3.p': A(x=2, y=1)}
        print(pg.query(value, r'.*p'))

        # Query by value.
        # Shall print:
        # {
        #    'a2[1].y': 2,
        #    'a3.p.x': 2,
        #    'a3.q.x': 2,
        #    'a3.q.y': 2,
        # }
        print(pg.query(value, where=lambda v: v==2))

        # Query by path, value and parent.
        # Shall print:
        # {
        #    'a2[1].y': 2,
        # }
        print(pg.query(
            value, r'.*y',
            where=lambda v, p: v > 1 and isinstance(p, A) and p.x == 1))

    Args:
      x: A nested structure that may contains symbolic value.
      path_regex: Optional regex expression to constrain path.
      where: Optional callable to constrain value and parent when path matches
        with `path_regex` or `path_regex` is not provided. The signature is:

          `(value) -> should_select` or `(value, parent) -> should_select`

      enter_selected: If True, if a node is selected, enter the node and query
        its sub-nodes.
      custom_selector: Optional callable object as custom selector. When
        `custom_selector` is provided, `path_regex` and `where` must be None.
        The signature of `custom_selector` is:

          `(key_path, value) -> should_select`
          or `(key_path, value, parent) -> should_select`

    Returns:
      A dict of key path to value as results for selected values.
    """
    regex = re.compile(path_regex) if path_regex else None
    select_fn: Callable[..., Any]
    if custom_selector is not None:
        if path_regex is not None or where is not None:
            raise ValueError(
                "'path_regex' and 'where' must be None when "
                "'custom_selector' is provided."
            )
        signature = pg_typing.signature(
            custom_selector, auto_typing=False, auto_doc=False
        )
        if len(signature.args) == 2:
            select_fn = lambda k, v, p: custom_selector(k, v)  # pyright: ignore[reportCallIssue]
        elif len(signature.args) == 3:
            select_fn = custom_selector
        else:
            raise TypeError(
                f"Custom selector '{signature.id}' should accept 2 or 3 arguments. "
                f'(key_path, value, [parent]). Encountered: {signature.args}'
            )
    else:
        where_fn: Callable[..., Any]
        if where is not None:
            signature = pg_typing.signature(where)
            if len(signature.args) == 1:
                where_fn = lambda v, p: where(v)  # pyright: ignore[reportCallIssue]
            elif len(signature.args) == 2:
                where_fn = where
            else:
                raise TypeError(
                    f"Where function '{signature.id}' should accept 1 or 2 "
                    f'arguments: (value, [parent]). Encountered: {signature.args}.'
                )
        else:
            where_fn = lambda v, p: True

        def _select_fn(k, v, p):
            if regex is not None and not regex.match(str(k)):
                return False
            return where_fn(v, p)

        select_fn = _select_fn

    results = {}

    def _preorder_visitor(
        path: topology.KeyPath, v: Any, parent: Any
    ) -> TraverseAction:
        if select_fn(path, v, parent):
            results[str(path)] = v
            return (
                TraverseAction.ENTER
                if enter_selected
                else TraverseAction.CONTINUE
            )
        return TraverseAction.ENTER

    traverse(x, preorder_visitor_fn=_preorder_visitor)
    return results

hash

hash(x: Any) -> int

Returns hash of value. Use symbolic hashing function if possible.

Example::

@pg.symbolize class A: def init(self, x): self.x = x

assert hash(A(1)) != hash(A(1)) assert pg.hash(A(1)) == pg.hash(A(1)) assert pg.hash(pg.Dict(x=[A(1)])) == pg.hash(pg.Dict(x=[A(1)]))

Parameters:

Name Type Description Default
x Any

Value for computing hash.

required

Returns:

Type Description
int

The hash value for x.

Source code in pygx/symbolic/_base.py
def sym_hash(x: Any) -> int:
    """Returns hash of value. Use symbolic hashing function if possible.

    Example::

      @pg.symbolize
      class A:
        def __init__(self, x):
          self.x = x

      assert hash(A(1)) != hash(A(1))
      assert pg.hash(A(1)) == pg.hash(A(1))
      assert pg.hash(pg.Dict(x=[A(1)])) == pg.hash(pg.Dict(x=[A(1)]))

    Args:
      x: Value for computing hash.

    Returns:
      The hash value for `x`.
    """
    # Primitive leaves: short-circuit to `hash`. Dominant case when hashing a
    # `Dict`/`List` of primitives.
    if type(x) in PRIMITIVE_TYPES:
        return hash(x)
    if isinstance(x, Symbolic):
        return x.sym_hash()
    if inspect.isfunction(x):
        return hash(x.__code__.co_code)
    if inspect.ismethod(x):
        return hash(
            (sym_hash(x.__self__), x.__code__.co_code)
        )  # pytype: disable=attribute-error
    # Raw containers reach here under `sym=False` (their members are not
    # wrapped into `pg.List` / `pg.Dict`). `hash(x)` would raise
    # `TypeError: unhashable type`, so hash them structurally — mirroring the
    # element-wise comparison `eq` performs on `list` / `tuple` / `dict`, so the
    # `sym_eq` ⇒ `sym_hash` contract holds. The type tag keeps `list` and
    # `tuple` distinct (as `eq` does); the `dict` hash is order-independent to
    # match `eq`'s order-independent dict comparison.
    if isinstance(x, list):
        return hash((list, tuple(sym_hash(e) for e in x)))
    if isinstance(x, tuple):
        return hash((tuple, tuple(sym_hash(e) for e in x)))
    if isinstance(x, dict):
        return hash((dict, frozenset((k, sym_hash(v)) for k, v in x.items())))
    return hash(x)

to_json

to_json(value: Any, **kwargs: Any) -> Any

Serializes a (maybe) symbolic value into a plain Python object.

Example::

class A(pg.Object): x: Any

a1 = A(1) json = a1.to_json() a2 = pg.from_json(json) assert pg.eq(a1, a2)

Parameters:

Name Type Description Default
value Any

value to serialize. Applicable value types are:

  • Builtin python types: None, bool, int, float, string;
  • WireConvertible types;
  • List types;
  • Tuple types;
  • Dict types.
required
**kwargs Any

Keyword arguments to pass to value.to_json if value is WireConvertible.

{}

Returns:

Type Description
Any

JSON value.

Source code in pygx/symbolic/_base.py
def to_json(value: Any, **kwargs: Any) -> Any:
    """Serializes a (maybe) symbolic value into a plain Python object.

    Example::

      class A(pg.Object):
        x: Any

      a1 = A(1)
      json = a1.to_json()
      a2 = pg.from_json(json)
      assert pg.eq(a1, a2)

    Args:
      value: value to serialize. Applicable value types are:

        * Builtin python types: None, bool, int, float, string;
        * WireConvertible types;
        * List types;
        * Tuple types;
        * Dict types.

      **kwargs: Keyword arguments to pass to value.to_json if value is
        WireConvertible.

    Returns:
      JSON value.
    """
    # Route symbolic values through their own `to_json` so per-type fast paths
    # (e.g. `Object.to_json`'s inline `{_type, **fields}` builder) apply —
    # byte-identical to `wire.to_json`, which serializes symbolic nodes via
    # `sym_jsonify` anyway. `sym_jsonify` is the cheap type-marker for a
    # symbolic `WireConvertible`; non-symbolic values keep `wire.to_json` (their
    # `value.to_json` is a node serializer, a different contract).
    if getattr(value, 'sym_jsonify', None) is not None:
        return value.to_json(**kwargs)
    return wire.to_json(value, **kwargs)

to_json_str

to_json_str(
    value: Any,
    *,
    json_indent: int | None = None,
    format: str | WireFormat = "json",
    **kwargs: Any
) -> str

Serializes a (maybe) symbolic value into a wire-format string.

Example::

class A(pg.Object): x: Any

a1 = A(1) json_str = a1.to_json_str() a2 = pg.from_json_str(json_str) assert pg.eq(a1, a2)

Parameters:

Name Type Description Default
value Any

Value to serialize.

required
json_indent int | None

The size of indentation passed to the format's encoder.

None
format str | WireFormat

The wire format to encode with -- a registered format name (e.g. 'json', 'yaml') or a pg.WireFormat instance. Defaults to 'json'.

'json'
**kwargs Any

Additional keyword arguments that are passed to pg.to_json.

{}

Returns:

Type Description
str

A serialized string.

Source code in pygx/symbolic/_base.py
def to_json_str(
    value: Any,
    *,
    json_indent: int | None = None,
    format: str | wire.WireFormat = 'json',  # pylint: disable=redefined-builtin
    **kwargs: Any,
) -> str:
    """Serializes a (maybe) symbolic value into a wire-format string.

    Example::

      class A(pg.Object):
        x: Any

      a1 = A(1)
      json_str = a1.to_json_str()
      a2 = pg.from_json_str(json_str)
      assert pg.eq(a1, a2)

    Args:
      value: Value to serialize.
      json_indent: The size of indentation passed to the format's encoder.
      format: The wire format to encode with -- a registered format name
        (e.g. ``'json'``, ``'yaml'``) or a ``pg.WireFormat`` instance.
        Defaults to ``'json'``.
      **kwargs: Additional keyword arguments that are passed to ``pg.to_json``.

    Returns:
      A serialized string.
    """
    fmt = wire.get_format(format)
    # Only thread `supports_refs` for formats that can't represent shared
    # references (where a shared value must raise). JSON's `supports_refs`
    # matches `to_json`'s default, so omitting it keeps `kwargs` empty — which
    # lets the symbolic `to_json` fast paths (gated on no kwargs) fire.
    if not fmt.supports_refs:
        kwargs['supports_refs'] = False
    tree = to_json(value, **kwargs)
    encoded = fmt.encode(tree, indent=json_indent)
    if isinstance(encoded, bytes):
        return encoded.decode('utf-8')
    return encoded

traverse

traverse(
    x: Any,
    preorder_visitor_fn: (
        None | Callable[[KeyPath, Any, Any], TraverseAction | None]
    ) = None,
    postorder_visitor_fn: (
        None | Callable[[KeyPath, Any, Any], TraverseAction | None]
    ) = None,
    root_path: KeyPath | None = None,
    parent: Any | None = None,
) -> bool

Traverse a (maybe) symbolic value using visitor functions.

Example::

class A(pg.Object): x: int

v = [{'a': A(1)}, A(2)] integers = [] def track_integers(k, v, p): if isinstance(v, int): integers.append((k, v)) return pg.TraverseAction.ENTER

pg.traverse(v, track_integers) assert integers == [('[0].a.x', 1), ('[1].x', 2)]

Parameters:

Name Type Description Default
x Any

Maybe symbolic value.

required
preorder_visitor_fn None | Callable[[KeyPath, Any, Any], TraverseAction | None]

preorder visitor function. Function signature is (path, value, parent) -> should_continue.

None
postorder_visitor_fn None | Callable[[KeyPath, Any, Any], TraverseAction | None]

postorder visitor function. Function signature is (path, value, parent) -> should_continue.

None
root_path KeyPath | None

KeyPath of root value.

None
parent Any | None

Optional parent of the root node.

None

Returns:

Type Description
bool

True if both preorder_visitor_fn and postorder_visitor_fn return either TraverseAction.ENTER or TraverseAction.CONTINUE for all nodes. Otherwise False.

Source code in pygx/symbolic/_base.py
def traverse(
    x: Any,
    preorder_visitor_fn: None
    | (Callable[[topology.KeyPath, Any, Any], TraverseAction | None]) = None,
    postorder_visitor_fn: None
    | (Callable[[topology.KeyPath, Any, Any], TraverseAction | None]) = None,
    root_path: topology.KeyPath | None = None,
    parent: Any | None = None,
) -> bool:
    """Traverse a (maybe) symbolic value using visitor functions.

    Example::

      class A(pg.Object):
        x: int

      v = [{'a': A(1)}, A(2)]
      integers = []
      def track_integers(k, v, p):
        if isinstance(v, int):
          integers.append((k, v))
        return pg.TraverseAction.ENTER

      pg.traverse(v, track_integers)
      assert integers == [('[0].a.x', 1), ('[1].x', 2)]

    Args:
      x: Maybe symbolic value.
      preorder_visitor_fn: preorder visitor function. Function signature is
        `(path, value, parent) -> should_continue`.
      postorder_visitor_fn: postorder visitor function. Function signature is
        `(path, value, parent) -> should_continue`.
      root_path: KeyPath of root value.
      parent: Optional parent of the root node.

    Returns:
      True if both `preorder_visitor_fn` and `postorder_visitor_fn` return
        either `TraverseAction.ENTER` or `TraverseAction.CONTINUE` for all nodes.
        Otherwise False.
    """
    root_path = root_path or topology.KeyPath()

    def no_op_visitor(path, value, parent):
        del path, value, parent
        return TraverseAction.ENTER

    if preorder_visitor_fn is None:
        preorder_visitor_fn = no_op_visitor
    if postorder_visitor_fn is None:
        postorder_visitor_fn = no_op_visitor

    preorder_action = preorder_visitor_fn(root_path, x, parent)
    if preorder_action is None or preorder_action == TraverseAction.ENTER:
        items = _iter_items(x)
        if items is not None:
            for k, v in items:
                if not traverse(
                    v,
                    preorder_visitor_fn,
                    postorder_visitor_fn,
                    topology.KeyPath(k, root_path),
                    x,
                ):
                    preorder_action = TraverseAction.STOP
                    break
    postorder_action = postorder_visitor_fn(root_path, x, parent)
    if (
        preorder_action == TraverseAction.STOP
        or postorder_action == TraverseAction.STOP
    ):
        return False
    return True

boilerplate_class

boilerplate_class(
    cls_name: str,
    value: Object,
    init_arg_list: list[str] | None = None,
    **kwargs: Any
) -> type[Object]

Create a boilerplate class using a symbolic object.

As the name indicates, a boilerplate class is a class that can be used as a boilerplate to create object.

Implementation-wise it's a class that extends the type of input value, while setting the default values of its (inherited) schema using the value from input.

An analogy to boilerplate class is prebound function.

For example::

# A regular function: correspond to a pg.Object subclass.
def f(a, b, c)
  return a + b + c

# A partially bound function: correspond to a boilerplate class created
# from a partially bound object.
def g(c):
  return f(1, 2, c)

# A fully bound function: correspond to a boilerplate class created from
# a fully bound object.
def h():
  return f(1, 2, 3)

Boilerplate class can be created with a value that is fully bound (like function h above), or partially bound (like function g above). Since boilerplate class extends the type of the input, we can rebind members of its instances as we modify the input.

Here are a few examples::

class A(pg.Object): a: str # Field A. b: int # Field B.

A1 = pg.boilerplate_class('A1', A.partial(a='foo')) assert A1(b=1) == A(a='foo', b=1)

A2 = pg.boilerplate_class('A2', A(a='bar', b=2)) assert A2() == A(a='bar', b=2)

Parameters:

Name Type Description Default
cls_name str

Name of the boilerplate class.

required
value Object

Value that is used as the default value of the boilerplate class.

required
init_arg_list list[str] | None

An optional list of strings as init positional arguments names.

None
**kwargs Any

Keyword arguments for infrequently used options. Acceptable keywords are: * serialization_key: An optional string to be used as the serialization key for the class during sym_jsonify. If None, cls.__type_name__ will be used. This is introduced for scenarios when we want to relocate a class, before the downstream can recognize the new location, we need the class to serialize it using previous key. * additional_keys: An optional list of strings as additional keys to deserialize an object of the registered class. This can be useful when we need to relocate or rename the registered class while being able to load existing serialized JSON values.

{}

Returns:

Type Description
type[Object]

A class which extends the input value's type, with its schema's default values set from the input value.

Raises:

Type Description
TypeError

Keyword argumment provided is not supported.

Source code in pygx/symbolic/_boilerplate.py
def boilerplate_class(
    cls_name: str,
    value: pg_object.Object,
    init_arg_list: list[str] | None = None,
    **kwargs: Any,
) -> type[pg_object.Object]:
    """Create a boilerplate class using a symbolic object.

    As the name indicates, a boilerplate class is a class that can be used
    as a boilerplate to create object.

    Implementation-wise it's a class that extends the type of input value, while
    setting the default values of its (inherited) schema using the value from
    input.

    An analogy to boilerplate class is prebound function.

      For example::

        # A regular function: correspond to a pg.Object subclass.
        def f(a, b, c)
          return a + b + c

        # A partially bound function: correspond to a boilerplate class created
        # from a partially bound object.
        def g(c):
          return f(1, 2, c)

        # A fully bound function: correspond to a boilerplate class created from
        # a fully bound object.
        def h():
          return f(1, 2, 3)

    Boilerplate class can be created with a value that is fully bound
    (like function `h` above), or partially bound (like function `g` above).
    Since boilerplate class extends the type of the input, we can rebind members
    of its instances as we modify the input.

    Here are a few examples::

      class A(pg.Object):
        a: str  # Field A.
        b: int  # Field B.

      A1 = pg.boilerplate_class('A1', A.partial(a='foo'))
      assert A1(b=1) == A(a='foo', b=1)

      A2 = pg.boilerplate_class('A2', A(a='bar', b=2))
      assert A2() == A(a='bar', b=2)

    Args:
      cls_name: Name of the boilerplate class.
      value: Value that is used as the default value of the boilerplate class.
      init_arg_list: An optional list of strings as __init__ positional arguments
        names.
      **kwargs: Keyword arguments for infrequently used options. Acceptable
        keywords are:  * `serialization_key`: An optional string to be used as the
        serialization key for the class during `sym_jsonify`. If None,
        `cls.__type_name__` will be used. This is introduced for scenarios when we
        want to relocate a class, before the downstream can recognize the new
        location, we need the class to serialize it using previous key. *
        `additional_keys`: An optional list of strings as additional keys to
        deserialize an object of the registered class. This can be useful when we
        need to relocate or rename the registered class while being able to load
        existing serialized JSON values.

    Returns:
      A class which extends the input value's type, with its schema's default
        values set from the input value.

    Raises:
      TypeError: Keyword argumment provided is not supported.
    """
    if not isinstance(value, pg_object.Object):
        raise ValueError(
            "Argument 'value' must be an instance of symbolic.Object subclass."
        )

    serialization_key = kwargs.pop('serialization_key', None)
    additional_keys = kwargs.pop('additional_keys', None)
    if kwargs:
        raise TypeError(
            f'Unsupported keyword arguments: {list(kwargs.keys())!r}.'
        )

    base_cls = value.__class__

    class _BoilerplateClass(base_cls, to_json_key=False):
        """Boilerplate class."""

        is_boilerplate = True

        def __init__(self, *args, **kwargs):
            if args:
                init_arg_names = self.__class__.init_arg_list
                if init_arg_names and init_arg_names[-1].startswith('*'):
                    vararg_name = init_arg_names[-1][1:]
                    named = init_arg_names[:-1]
                    n = len(named)
                    kwargs.update(zip(named, args[:n]))
                    kwargs[vararg_name] = list(args[n:])
                else:
                    kwargs.update(zip(init_arg_names, args))  # pyright: ignore[reportArgumentType,reportCallIssue]
            super().__init__(**kwargs)

    caller_module = inspect.getmodule(inspect.stack()[1][0])
    cls_module = caller_module.__name__ if caller_module else '__main__'
    cls = _BoilerplateClass
    cls.__name__ = cls_name
    cls.__qualname__ = cls.__qualname__.replace(
        'boilerplate_class.<locals>._BoilerplateClass', cls_name
    )
    cls.__module__ = cls_module

    allow_partial = value.allow_partial

    def _freeze_field(
        path: topology.KeyPath, field: pg_typing.Field, value: Any
    ) -> Any:
        # We do not do validation since Object is already in valid form.
        del path
        # Recursively freeze dict field.
        if isinstance(field.value, pg_typing.Dict) and field.value.schema:
            field.value.schema.apply(
                value,
                allow_partial=allow_partial,
                child_transform=_freeze_field,
            )
            field.value.set_default(value)
            field.value.freeze()
        elif value != pg_typing.MISSING_VALUE:
            field.value.freeze(copy.deepcopy(value), apply_before_use=False)
        else:
            field.value.set_default(
                pg_typing.MISSING_VALUE, use_default_apply=False
            )
        return value

    # NOTE(daiyip): we call `cls.__schema__.apply` to freeze fields that have
    # default values.
    with flags.allow_writable_accessors():
        cls.__schema__.apply(
            value._sym_attributes,  # pylint: disable=protected-access
            allow_partial=allow_partial,
            child_transform=_freeze_field,
        )
    cls.__schema__.metadata['init_arg_list'] = init_arg_list
    cls.apply_schema(cls.__schema__)
    cls.register_for_deserialization(serialization_key, additional_keys)
    return cls

apply_wrappers

apply_wrappers(
    wrapper_classes: Sequence[type[ClassWrapper]] | None = None,
    where: Callable[[type[ClassWrapper]], bool] | None = None,
) -> AbstractContextManager[Any]

Context manager for swapping user classes with their class wrappers.

This helper method is a handy tool to swap user classes with their wrappers within a code block, without modifying exisiting code.

For example::

def foo(): return A()

APrime = pg.wrap(A)

with pg.apply_wrappers([APrime]): # Direct creation of an instance of A will be detoured to APrime. assert isinstance(A(), APrime)

Indirect creation of an instance of `A` will be detoured too.
assert isinstance(foo(), APrime)

# Out of the scope, direct/indirect creation of A will be restored. assert not isinstance(A(), APrime) assert not isinstance(foo(), APrime)

pg.apply_wrappers can be nested, under which the inner context will apply the wrappers from the outter context. pg.apply_wrappers is NOT thread-safe.

Parameters:

Name Type Description Default
wrapper_classes Sequence[type[ClassWrapper]] | None

Wrapper classes to use. If None, sets it to all registered wrapper classes.

None
where Callable[[type[ClassWrapper]], bool] | None

An optional filter function in signature (wrapper_class) -> bool. If not None, only filtered wrapper_class will be swapped.

None

Returns:

Type Description
AbstractContextManager[Any]

A context manager that detours the original classes to the wrapper classes.

Source code in pygx/symbolic/_class_wrapper.py
def apply_wrappers(
    wrapper_classes: Sequence[type['ClassWrapper']] | None = None,
    where: Callable[[type['ClassWrapper']], bool] | None = None,
) -> contextlib.AbstractContextManager[Any]:
    """Context manager for swapping user classes with their class wrappers.

    This helper method is a handy tool to swap user classes with their wrappers
    within a code block, without modifying exisiting code.

    For example::

      def foo():
        return A()

      APrime = pg.wrap(A)

      with pg.apply_wrappers([APrime]):
        # Direct creation of an instance of `A` will be detoured to `APrime`.
        assert isinstance(A(), APrime)

        Indirect creation of an instance of `A` will be detoured too.
        assert isinstance(foo(), APrime)

      # Out of the scope, direct/indirect creation `of` A will be restored.
      assert not isinstance(A(), APrime)
      assert not isinstance(foo(), APrime)

    ``pg.apply_wrappers`` can be nested, under which the inner context will apply
    the wrappers from the outter context. ``pg.apply_wrappers`` is NOT
    thread-safe.

    Args:
      wrapper_classes: Wrapper classes to use. If None, sets it to all registered
        wrapper classes.
      where: An optional filter function in signature (wrapper_class) -> bool.
        If not None, only filtered `wrapper_class` will be swapped.

    Returns:
      A context manager that detours the original classes to the wrapper classes.
    """
    if not wrapper_classes:
        wrapper_classes = []
        for _, c in wire.WireConvertible.registered_types():
            if (
                issubclass(c, ClassWrapper)
                and c not in (ClassWrapper, _SubclassedWrapperBase)
                and (not where or where(c))
                and c.sym_wrapped_cls is not None
            ):
                wrapper_classes.append(c)
    return detouring.detour([(c.sym_wrapped_cls, c) for c in wrapper_classes])

wrap

wrap(
    cls: type[Any],
    init_args: (
        list[Field | FieldDef] | dict[FieldKeyDef, FieldValueDef] | None
    ) = None,
    *,
    reset_state_fn: Callable[[Any], None] | None = None,
    repr: bool = True,
    eq: bool = False,
    class_name: str | None = None,
    module_name: str | None = None,
    auto_doc: bool = False,
    auto_typing: bool = False,
    serialization_key: str | None = None,
    additional_keys: list[str] | None = None,
    override: dict[str, Any] | None = None
) -> type[ClassWrapper]

Makes a symbolic class wrapper from a regular Python class.

pg.wrap is called by pygx.symbolize for symbolizing existing Python classes. For example::

class A: def init(self, x): self.x = x

# The following two lines are equivalent. A1 = pg.symbolize(A) A2 = pg.wrap(A)

Besides passing the source class, pg.wrap allows the user to pass symbolic field definitions for the init arguments. For example::

A3 = pg.wrap(A, [ ('x', pg.typing.Int()) ])

Moreover, multiple flags are provided to determine whether or not to use the symbolic operations as the default behaviors. For example::

A4 = pg.wrap( A, [], # Instead clearing out all internal states (default), # do not reset internal state. reset_state_fn=lambda self: None, # Use symbolic representation for repr and str. repr=True, # use symbolic equality for eq, ne and hash. eq=True, # Customize the class name obtained (the default behaivor # is to use the source class name). class_name='A4' # Customize the module name for created class (the default # behavior is to use the source module name). module_name='my_module')

Parameters:

Name Type Description Default
cls type[Any]

Class to wrap.

required
init_args list[Field | FieldDef] | dict[FieldKeyDef, FieldValueDef] | None

An optional list of field definitions for the arguments of init. It can be a sparse value specifications for argument in the init method of cls.

None
reset_state_fn Callable[[Any], None] | None

An optional callable object to reset the internal state of the user class when rebind happens.

None
repr bool

Options for generating __repr__ and __str__. If True (default), use symbolic representation if the user class does not define its own. Otherwise use the user class' definition. If False, always use non-symbolic representations, which falls back to object.__repr__ and object.__str__ if the user class does not define them.

True
eq bool

Options for generating __eq__, __ne__ and __hash__. If True and the user_cls defines __eq__, __ne__ and __hash__, use the definitions from the user_cls. If True and the user_cls does not define __eq__, __ne__ and __hash__, use symbolic eq/hash. If False (default), use user_cls's definition if present, or the definitions from the object class.

False
class_name str | None

An optional string used as class name for the wrapper class. If None, the wrapper class will use the class name of the wrapped class.

None
module_name str | None

An optional string used as module name for the wrapper class. If None, the wrapper class will use the module name of the wrapped class.

None
auto_doc bool

If True, the descriptions for init argument fields will be extracted from docstring if present.

False
auto_typing bool

If True, PyGX typing (runtime-typing) will be enabled based on type annotations inspected from the __init__ method.

False
serialization_key str | None

An optional string to be used as the serialization key for the class during sym_jsonify. If None, cls.__type_name__ will be used. This is introduced for scenarios when we want to relocate a class, before the downstream can recognize the new location, we need the class to serialize it using previous key.

None
additional_keys list[str] | None

An optional list of strings as additional keys to deserialize an object of the registered class. This can be useful when we need to relocate or rename the registered class while being able to load existing serialized JSON values.

None
override dict[str, Any] | None

Additional class attributes to override.

None

Returns:

Type Description
type[ClassWrapper]

A subclass of cls and ClassWrapper.

Raises:

Type Description
TypeError

input cls is not a class.

Source code in pygx/symbolic/_class_wrapper.py
def wrap(
    cls: type[Any],
    init_args: (
        list[pg_typing.Field | pg_typing.FieldDef]
        | dict[pg_typing.FieldKeyDef, pg_typing.FieldValueDef]
        | None
    ) = None,
    *,
    reset_state_fn: Callable[[Any], None] | None = None,
    repr: bool = True,  # pylint: disable=redefined-builtin
    eq: bool = False,
    class_name: str | None = None,
    module_name: str | None = None,
    auto_doc: bool = False,
    auto_typing: bool = False,
    serialization_key: str | None = None,
    additional_keys: list[str] | None = None,
    override: dict[str, Any] | None = None,  # pylint: disable=redefined-outer-name
) -> type['ClassWrapper']:
    """Makes a symbolic class wrapper from a regular Python class.

    ``pg.wrap`` is called by [`pygx.symbolize`][pygx.symbolic.symbolize] for symbolizing existing
    Python classes. For example::

      class A:
        def __init__(self, x):
          self.x = x

      # The following two lines are equivalent.
      A1 = pg.symbolize(A)
      A2 = pg.wrap(A)

    Besides passing the source class, ``pg.wrap`` allows the user to pass symbolic
    field definitions for the init arguments. For example::

      A3 = pg.wrap(A, [
        ('x', pg.typing.Int())
      ])

    Moreover, multiple flags are provided to determine whether or not to use the
    symbolic operations as the default behaviors. For example::

      A4 = pg.wrap(
        A,
        [],
        # Instead clearing out all internal states (default),
        # do not reset internal state.
        reset_state_fn=lambda self: None,
        # Use symbolic representation for __repr__ and __str__.
        repr=True,
        # use symbolic equality for __eq__, __ne__ and __hash__.
        eq=True,
        # Customize the class name obtained (the default behaivor
        # is to use the source class name).
        class_name='A4'
        # Customize the module name for created class (the default
        # behavior is to use the source module name).
        module_name='my_module')

    Args:
      cls: Class to wrap.
      init_args: An optional list of field definitions for the arguments of
        __init__. It can be a sparse value specifications for argument in the
        __init__ method of `cls`.
      reset_state_fn: An optional callable object to reset the internal state of
        the user class when rebind happens.
      repr: Options for generating `__repr__` and `__str__`. If True (default),
        use symbolic representation if the user class does not define its own.
        Otherwise use the user class' definition. If False, always use
        non-symbolic representations, which falls back to `object.__repr__` and
        `object.__str__` if the user class does not define them.
      eq: Options for generating `__eq__`, `__ne__` and `__hash__`. If True and
        the `user_cls` defines `__eq__`, `__ne__` and `__hash__`, use the
        definitions from the `user_cls`. If True and the `user_cls` does not
        define `__eq__`, `__ne__` and `__hash__`, use symbolic eq/hash. If False
        (default), use `user_cls`'s definition if present, or the definitions from
        the `object` class.
      class_name: An optional string used as class name for the wrapper class. If
        None, the wrapper class will use the class name of the wrapped class.
      module_name: An optional string used as module name for the wrapper class.
        If None, the wrapper class will use the module name of the wrapped class.
      auto_doc: If True, the descriptions for init argument fields will be
        extracted from docstring if present.
      auto_typing: If True, PyGX typing (runtime-typing) will be enabled based
        on type annotations inspected from the `__init__` method.
      serialization_key: An optional string to be used as the serialization key
        for the class during `sym_jsonify`. If None, `cls.__type_name__` will be
        used. This is introduced for scenarios when we want to relocate a class,
        before the downstream can recognize the new location, we need the class to
        serialize it using previous key.
      additional_keys: An optional list of strings as additional keys to
        deserialize an object of the registered class. This can be useful when we
        need to relocate or rename the registered class while being able to load
        existing serialized JSON values.
      override: Additional class attributes to override.

    Returns:
      A subclass of `cls` and `ClassWrapper`.

    Raises:
      TypeError: input `cls` is not a class.
    """
    if not inspect.isclass(cls):
        raise TypeError(
            f'Class wrapper can only be created from classes. '
            f'Encountered: {cls!r}.'
        )

    if not issubclass(cls, ClassWrapper):
        cls = _subclassed_wrapper(
            cls,
            use_symbolic_repr=repr,
            use_symbolic_comp=eq,
            reset_state_fn=reset_state_fn,
            class_name=class_name,
            module_name=module_name,
            use_auto_doc=auto_doc,
            use_auto_typing=auto_typing,
        )

    # Update init argument specifications according to user specified specs.
    # Replace schema instead of extending it.
    init_arg_list, arg_fields = _extract_init_signature(
        cls, init_args, auto_doc=auto_doc, auto_typing=auto_typing
    )
    cls.update_schema(arg_fields, init_arg_list=init_arg_list, extend=False)  # pyright: ignore[reportArgumentType]
    cls.register_for_deserialization(serialization_key, additional_keys)

    if override:
        for k, v in override.items():
            setattr(cls, k, v)
    return cls

wrap_module

wrap_module(
    module: ModuleType,
    names: Sequence[str] | None = None,
    where: Callable[[type[ClassWrapper]], bool] | None = None,
    export_to: ModuleType | None = None,
    **kwargs: Any
) -> list[type[ClassWrapper]]

Wrap classes from a module.

For example, users can wrap all subclasses of xxx.Base under module xxx::

import xxx

pg.wrap_module( xxx, where=lambda c: isinstance(c, xxx.Base))

Parameters:

Name Type Description Default
module ModuleType

A container that contains classes to wrap.

required
names Sequence[str] | None

An optional list of class names. If not provided, all classes under module will be considered candidates.

None
where Callable[[type[ClassWrapper]], bool] | None

An optional filter function in signature (user_class) -> bool. Only the classes under module with True return value will be wrapped.

None
export_to ModuleType | None

An optional module to export the wrapper classes.

None
**kwargs Any

Keyword arguments passed to wrap

{}

Returns:

Type Description
list[type[ClassWrapper]]

Wrapper classes.

Source code in pygx/symbolic/_class_wrapper.py
def wrap_module(
    module: types.ModuleType,
    names: Sequence[str] | None = None,
    where: Callable[[type['ClassWrapper']], bool] | None = None,
    export_to: types.ModuleType | None = None,
    **kwargs: Any,
) -> list[type['ClassWrapper']]:
    """Wrap classes from a module.

    For example, users can wrap all subclasses of `xxx.Base` under module `xxx`::

      import xxx

      pg.wrap_module(
        xxx, where=lambda c: isinstance(c, xxx.Base))

    Args:
      module: A container that contains classes to wrap.
      names: An optional list of class names. If not provided, all classes under
        `module` will be considered candidates.
      where: An optional filter function in signature (user_class) -> bool.
        Only the classes under `module` with True return value will be wrapped.
      export_to: An optional module to export the wrapper classes.
      **kwargs: Keyword arguments passed to `wrap`

    Returns:
      Wrapper classes.
    """
    wrapper_classes = []
    module_name = export_to.__name__ if export_to else None
    origin_cls_to_wrap_cls = {}
    for symbol_name in names or dir(module):
        s = getattr(module, symbol_name)
        if inspect.isclass(s) and (not where or where(s)):
            # NOTE(daiyip): It's possible that a name under a module is an alias for
            # another class. In such case, we do not create duplicated wrappers but
            # shares the same wrapper classes with different names.
            if s in origin_cls_to_wrap_cls:
                wrapper_class = origin_cls_to_wrap_cls[s]
            else:
                wrapper_class = wrap(s, module_name=module_name, **kwargs)
                origin_cls_to_wrap_cls[s] = wrapper_class
                wrapper_classes.append(wrapper_class)
            if export_to:
                setattr(export_to, symbol_name, wrapper_class)
    return wrapper_classes

compound

compound(
    base_class: type[Object] | None = None,
    args: (
        None
        | list[
            tuple[tuple[str, KeySpec], ValueSpec, str]
            | tuple[tuple[str, KeySpec], ValueSpec, str, Any]
        ]
    ) = None,
    **kwargs: Any
) -> Callable[[Callable[..., Any]], type[Compound]]

Function decorator to create compound class.

Example::

@dataclasses.dataclass class Foo: x: int y: int

def sum(self):
  return self.x + self.y

@pg.compound def foo_with_equal_x_y(v: int) -> Foo: return Foo(v, v)

f = foo_with_equal_x_y(1)

# First of all, the objects of compound classes can be used as an in-place # replacement for the objects of regular classes, as they are subclasses of # the regular classes. assert issubclass(foo_with_equal_x_y, Foo) assert isinstance(f, Foo)

# We can access symbolic attributes of the compound object. assert f.v == 1

# We can also access the public APIs of the decomposed object. assert f.x == 1 assert f.y == 1 assert f.sum() == 2

# Or explicit access the decomposed object. assert f.decomposed == Foo(1, 1)

# Moreover, symbolic power is fully unleashed to the compound class. f.sym_rebind(v=2) assert f.x == 2 assert f.y == 2 assert f.sum() == 4

# Err with runtime type check: 2.5 is not an integer. f.sym_rebind(v=2.5)

Parameters:

Name Type Description Default
base_class type[Object] | None

The base class of the compond class, which should be a pg.Object type. If None, it will be infererenced from the return annotation of factory_fn. If the annotation is not present or auto_typing is set to False, base_class must be present.

None
args None | list[tuple[tuple[str, KeySpec], ValueSpec, str] | tuple[tuple[str, KeySpec], ValueSpec, str, Any]]

Symbolic args specification. See pg.compound_class for details.

None
**kwargs Any

Keyword arguments. See pg.compound_class for details.

{}

Returns:

A symbolic compound class that subclasses base_class.

Source code in pygx/symbolic/_compounding.py
def compound(
    base_class: type[Object] | None = None,
    args: None
    | (
        list[
            (
                tuple[tuple[str, pg_typing.KeySpec], pg_typing.ValueSpec, str]
                | tuple[
                    tuple[str, pg_typing.KeySpec], pg_typing.ValueSpec, str, Any
                ]
            )
        ]
    ) = None,  # pylint: disable=bad-continuation
    **kwargs: Any,
) -> Callable[[Callable[..., Any]], type['Compound']]:
    """Function decorator to create compound class.

    Example::

      @dataclasses.dataclass
      class Foo:
        x: int
        y: int

        def sum(self):
          return self.x + self.y

      @pg.compound
      def foo_with_equal_x_y(v: int) -> Foo:
        return Foo(v, v)

      f = foo_with_equal_x_y(1)

      # First of all, the objects of compound classes can be used as an in-place
      # replacement for the objects of regular classes, as they are subclasses of
      # the regular classes.
      assert issubclass(foo_with_equal_x_y, Foo)
      assert isinstance(f, Foo)

      # We can access symbolic attributes of the compound object.
      assert f.v == 1

      # We can also access the public APIs of the decomposed object.
      assert f.x == 1
      assert f.y == 1
      assert f.sum() == 2

      # Or explicit access the decomposed object.
      assert f.decomposed == Foo(1, 1)

      # Moreover, symbolic power is fully unleashed to the compound class.
      f.sym_rebind(v=2)
      assert f.x == 2
      assert f.y == 2
      assert f.sum() == 4

      # Err with runtime type check: 2.5 is not an integer.
      f.sym_rebind(v=2.5)

    Args:
      base_class: The base class of the compond class, which should be a
        ``pg.Object`` type. If None, it will be infererenced from the return
        annotation of `factory_fn`. If the annotation is not present or
        `auto_typing` is set to False, `base_class` must be present.
      args: Symbolic args specification. See [`pg.compound_class`][pygx.symbolic.compound_class] for
        details.
      **kwargs: Keyword arguments. See [`pg.compound_class`][pygx.symbolic.compound_class] for details.

    Returns:

      A symbolic compound class that subclasses `base_class`.
    """
    if inspect.isfunction(base_class):
        assert args is None
        return compound_class(base_class, add_to_registry=True, **kwargs)
    return lambda fn: compound_class(  # pylint: disable=g-long-lambda
        fn,
        base_class,
        args,  # pyright: ignore[reportArgumentType]
        add_to_registry=True,
        **kwargs,
    )

compound_class

compound_class(
    factory_fn: FunctionType,
    base_class: type[Object] | None = None,
    args: (
        list[Field | FieldDef] | dict[FieldKeyDef, FieldValueDef] | None
    ) = None,
    *,
    lazy_build: bool = True,
    auto_doc: bool = True,
    auto_typing: bool = True,
    serialization_key: str | None = None,
    additional_keys: list[str] | None = None,
    add_to_registry: bool = False
) -> type[Compound]

Creates a compound class from a factory function.

Parameters:

Name Type Description Default
factory_fn FunctionType

A function that produces a compound object.

required
base_class type[Object] | None

The base class of the compond class, which should be a pg.Object type. If None, it will be infererenced from the return annotation of factory_fn. If the annotation is not present or auto_typing is set to False, base_class must be present.

None
args list[Field | FieldDef] | dict[FieldKeyDef, FieldValueDef] | None

Symbolic args specification. args is a list of tuples, each describes an argument from the input function. Each tuple is the format of (, , [description], [metadata-objects]). argument-name - a str or pg_typing.StrKey object. When pg_typing.StrKey is used, it describes the wildcard keyword argument. value-spec - a pg_typing.ValueSpec object or equivalent, e.g. primitive values which will be converted to ValueSpec implementation according to its type and used as its default value. description - a string to describe the agument. metadata-objects - an optional list of any type, which can be used to generate code according to the schema. There are notable rules in filling the args: 1) When args is None or arguments from the function signature are missing from it, schema.Field for these fields will be automatically generated and inserted into args. That being said, every arguments in input function will have a schema.Field counterpart in Functor.schema.fields sorted by the declaration order of each argument in the function signature ( other than the order in args). 2) Default argument values are specified along with function definition as regular python functions, instead of being set at schema.Field level. But validation rules can be set using args and apply to argument values.

None
lazy_build bool

If True, factory_fn will be called upon first use. Otherwise, it will be called at construction.

True
auto_doc bool

If True, the descriptions of argument fields will be inherited from factory_fn docstr if they are not explicitly specified through args.

True
auto_typing bool

If True, the value spec for constraining each argument will be inferred from its annotation. Otherwise the value specs for all arguments will be pg.typing.Any().

True
serialization_key str | None

An optional string to be used as the serialization key for the class during sym_jsonify. If None, cls.__type_name__ will be used. This is introduced for scenarios when we want to relocate a class, before the downstream can recognize the new location, we need the class to serialize it using previous key.

None
additional_keys list[str] | None

An optional list of strings as additional keys to deserialize an object of the registered class. This can be useful when we need to relocate or rename the registered class while being able to load existing serialized JSON values.

None
add_to_registry bool

If True, the newly created functor class will be added to the registry for deserialization.

False

Returns:

Type Description
type[Compound]

A callable that converts a factory function into a subclass of the base class.

Source code in pygx/symbolic/_compounding.py
def compound_class(
    factory_fn: types.FunctionType,
    base_class: type[Object] | None = None,
    args: (
        list[pg_typing.Field | pg_typing.FieldDef]
        | dict[pg_typing.FieldKeyDef, pg_typing.FieldValueDef]
        | None
    ) = None,
    *,
    lazy_build: bool = True,
    auto_doc: bool = True,
    auto_typing: bool = True,
    serialization_key: str | None = None,
    additional_keys: list[str] | None = None,
    add_to_registry: bool = False,
) -> type[Compound]:
    """Creates a compound class from a factory function.

    Args:
      factory_fn: A function that produces a compound object.
      base_class: The base class of the compond class, which should be a
        ``pg.Object`` type. If None, it will be infererenced from the return
        annotation of `factory_fn`. If the annotation is not present or
        `auto_typing` is set to False, `base_class` must be present.
      args: Symbolic args specification. `args` is a list of tuples, each
        describes an argument from the input function. Each tuple is the format of
        (<argumment-name>, <value-spec>, [description], [metadata-objects]).
        `argument-name` - a `str` or `pg_typing.StrKey` object. When
        `pg_typing.StrKey` is used, it describes the wildcard keyword argument.
        `value-spec` - a `pg_typing.ValueSpec` object or equivalent, e.g.
        primitive values which will be converted to ValueSpec implementation
        according to its type and used as its default value. `description` - a
        string to describe the agument. `metadata-objects` - an optional list of
        any type, which can be used to generate code according to the schema.
        There are notable rules in filling the `args`: 1) When `args` is None or
        arguments from the function signature are missing from it, `schema.Field`
        for these fields will be automatically generated and inserted into `args`.
        That being said, every arguments in input function will have a
        `schema.Field` counterpart in `Functor.schema.fields` sorted by the
        declaration order of each argument in the function signature ( other than
        the order in `args`).  2) Default argument values are specified along with
        function definition as regular python functions, instead of being set at
        `schema.Field` level. But validation rules can be set using `args` and
        apply to argument values.
      lazy_build: If True, `factory_fn` will be called upon first use. Otherwise,
        it will be called at construction.
      auto_doc: If True, the descriptions of argument fields will be inherited
        from `factory_fn` docstr if they are not explicitly specified through
        ``args``.
      auto_typing: If True, the value spec for constraining each argument will be
        inferred from its annotation. Otherwise the value specs for all arguments
        will be ``pg.typing.Any()``.
      serialization_key: An optional string to be used as the serialization key
        for the class during `sym_jsonify`. If None, `cls.__type_name__` will be
        used. This is introduced for scenarios when we want to relocate a class,
        before the downstream can recognize the new location, we need the class to
        serialize it using previous key.
      additional_keys: An optional list of strings as additional keys to
        deserialize an object of the registered class. This can be useful when we
        need to relocate or rename the registered class while being able to load
        existing serialized JSON values.
      add_to_registry: If True, the newly created functor class will be added to
        the registry for deserialization.

    Returns:
      A callable that converts a factory function into a subclass of the base
        class.
    """

    if not inspect.isfunction(factory_fn):
        raise TypeError('Decorator `compound` is only applicable to functions.')

    schema = pg_typing.schema(
        factory_fn,
        args=args,
        returns=pg_typing.Object(base_class) if base_class else None,
        auto_doc=auto_doc,
        auto_typing=auto_typing,
    )

    # Inference the base_class from schema.
    return_spec = schema.metadata.get('returns', None)
    if isinstance(return_spec, pg_typing.Object):
        base_class = return_spec.cls
    else:
        raise ValueError(
            'Cannot inference the base class from return value annotation. '
            'Please either add an annotation for the return value or provide the '
            'value for the `base_class` argument.'
        )

    class _Compound(Compound, base_class, to_json_key=False):
        """The compound class bound to a factory function.

        ``to_json_key=False`` skips auto-registration during
        ``__init_subclass__``; the explicit call to
        ``cls.register_for_deserialization`` below registers under the
        factory function's module/name.
        """

        # The compound class uses the function signature to decide its
        # schema, thus we do not infer its schema from the class annotations.
        __auto_schema__ = False

        def on_sym_bound(self):
            # NOTE(daiyip): Do not call `super().on_sym_bound()` to avoid side effect.
            # This is okay since all states are delegated to `self.decomposed`.
            Compound.on_sym_bound(self)  # pylint: disable=protected-access

            self._sym_decomposed = None

            if not lazy_build:
                # Trigger build.
                _ = self.decomposed

        @property
        def decomposed(self):
            if self._sym_decomposed is None:
                # Build the compound object.
                self._sym_decomposed = factory_fn(**self.sym_init_args)  # pyright: ignore[reportCallIssue]

                # This allows the decomposed symbolic object to access the parent chain
                # for value retrieval of inferred attributes.
                if isinstance(self._sym_decomposed, Symbolic):
                    self._sym_decomposed.sym_setparent(self.sym_parent)
            return self._sym_decomposed

        def on_sym_parent_change(self, old_parent, new_parent):
            """Override to allow decomposed object to follow parent chain change."""
            super().on_sym_parent_change(old_parent, new_parent)
            if isinstance(self._sym_decomposed, Symbolic):
                self._sym_decomposed.sym_setparent(new_parent)

        @override
        def sym_inferred(
            self, key: str, default=RAISE_IF_NOT_FOUND, **kwargs
        ) -> Any:  # pyright: ignore[reportIncompatibleMethodOverride]
            # Bypass the user base' `sym_inferred` if it's overriden.
            return Compound.sym_inferred(self, key, default=default, **kwargs)

        def __getattribute__(self, name: str):
            # Fast path: underscore-prefixed names and owned attrs go straight to the
            # base implementation. Symbolic init args also go through base; everything
            # else is redirected to the decomposed object.
            if (
                name[:1] == '_'
                or name in _COMPOUND_OWNED_ATTR_NAMES
                or name in Compound.__getattribute__(self, '_sym_attributes')
            ):
                return Compound.__getattribute__(self, name)
            return getattr(Compound.__getattribute__(self, 'decomposed'), name)

    cls = _Compound
    cls.__name__ = factory_fn.__name__
    cls.__qualname__ = factory_fn.__qualname__
    cls.__module__ = factory_fn.__module__
    cls.__doc__ = factory_fn.__doc__

    cls.apply_schema(schema)

    # NOTE(daiyip): Override abstract methods as non-ops, so `cls` could
    # have an abstract class as its base. We don't need to worry about the
    # implementation of the abstract method, since it will be detoured to the
    # decomposed object at runtime via `__getattribute__`.
    for key in dir(cls):
        attr = getattr(cls, key)
        if getattr(attr, '__isabstractmethod__', False):
            # pylint: disable-next=unnecessary-lambda-assignment
            noop = lambda self, *args, **kwargs: None
            if isinstance(attr, property):
                noop = property(noop)
            else:
                assert inspect.isfunction(attr), (key, attr)
            setattr(cls, key, noop)
    abc.update_abstractmethods(cls)

    if add_to_registry:
        cls.register_for_deserialization(serialization_key, additional_keys)
    return cls

field

field(
    *,
    value_spec: ValueSpec | None = None,
    default: Any = MISSING_VALUE,
    default_factory: Callable[[], Any] | None = None,
    doc: str | None = None,
    metadata: dict[str, Any] | None = None,
    transform: Callable[[Any], Any] | None = None,
    validator: Callable[[Any], None] | None = None,
    alias: str | None = None,
    repr: bool | None = None,
    compare: bool | None = None,
    hash: bool | None = None,
    init: bool | None = None,
    clone: bool | None = None,
    validate: bool | None = None,
    wrap: bool | None = None
) -> Any

Field descriptor for pg.Object dataclass-like fields.

Registered as a field_specifier on pg.ObjectMeta so that type checkers honoring :pep:681 (pyright/Pylance/mypy) read the field's static properties from the keyword arguments here.

pg.Object.__init__ is keyword-only by default (dataclass_transform(kw_only_default=True)), so this helper exists primarily to attach doc or metadata to an annotation-style field — matching the third/fourth tuple slots in pg.members::

class A(pg.Object):
  x: int = pg.field(
      default=1,
      doc='Initial count.',
      metadata={'serialize_as': 'count'},
  )

A value_spec may be supplied to apply richer runtime constraints than the bare annotation can express (e.g. regex / numeric bounds)::

class A(pg.Object):
  x: str = pg.field(value_spec=pg.typing.Str(regex='a.*'))

Parameters:

Name Type Description Default
value_spec ValueSpec | None

Optional explicit pg.typing.ValueSpec overriding the spec inferred from the field's type annotation. Use when the annotation cannot express the desired runtime constraint (e.g. pg.typing.Str(regex=...), pg.typing.Int(min_value=0)). The type annotation still drives static type checking.

None
default Any

The field's default value. If omitted (and no default_factory), the field is required.

MISSING_VALUE
default_factory Callable[[], Any] | None

Zero-argument callable producing the default value. Stored on the field's value spec and invoked once per instance (stdlib-dataclasses semantics) — supports factories like lambda: time.time() that should produce a fresh value each time. Mutually exclusive with default.

None
doc str | None

Field docstring — equivalent to the third tuple slot in pg.members([(name, spec, doc, metadata)]).

None
metadata dict[str, Any] | None

Field metadata dict — equivalent to the fourth tuple slot in pg.members.

None
transform Callable[[Any], Any] | None

Optional (value) -> value callable attached to the field's value spec. Runs after type validation inside ValueSpec.apply and is the same hook as the transform= constructor kwarg on pg.typing.* specs — exposed here so annotation-style fields can wire up a transform without re-instantiating the spec.

None
validator Callable[[Any], None] | None

Optional (value) -> None callable — the field's after check. Called with the final value once the value spec accepted (and possibly coerced/wrapped) it; raise to reject (re-raised path-suffixed as the same exception class). Runs everywhere the field validates: construct, self.x = ... assignment, sym_rebind. Its return value is ignored — value transformation belongs to transform (the before hook), keeping validation and coercion separate. Not called for missing values of a partial construct, nor when validate=False skips the apply pipeline.

None
alias str | None

Optional wire-layer alias for this field's key. Accepted by from_json (either spelling; both at once is an error), emitted by to_json(by_alias=True) and JSON Schema. Python attribute access and __init__ kwargs always use the field name. May not collide with _type, a sibling field name, or another alias.

None
repr bool | None

Whether the field appears in repr / format. None (default) resolves at read time to the field's storage mode: symbolic (init=True) → True, non-symbolic (init=False) → False. Mirrors dataclasses.field(repr=...).

None
compare bool | None

Whether the field participates in symbolic equality (sym_eq) and symbolic ordering (sym_lt / sym_gt and the order=True operators) — the two stay consistent, as in dataclasses.field(compare=...). Same None resolution as repr.

None
hash bool | None

Whether the field participates in symbolic hashing. Same None resolution as repr. Mirrors dataclasses.field(hash=...).

None
init bool | None

If False, drop this field from the generated __init__. The field is then routed outside the symbolic state (stored on self.__dict__ rather than _sym_attributes) — i.e. it behaves like a plain Python attribute managed by the class, suitable for derived/computed state that on_sym_ready fills in. If a default or default_factory is given it seeds the initial value in __init__; otherwise the attribute is unset until something assigns to it (mirrors dataclasses.field(init=False) + __post_init__). None (default) resolves from the field's name: _xinit=False (non-symbolic by convention), xinit=True. Set an explicit value to override.

None
clone bool | None

Only meaningful when init=False (no-op for symbolic fields). Same None resolution as repr: None → don't clone (cloned instance reseeds from default / leaves unset and on_sym_ready recomputes). Set True to preserve the source value during sym_clone (the clone's deep flag is honored).

None
validate bool | None

Whether values are validated against the field's value spec. Same None resolution: symbolic fields validate (ValueSpec.apply runs at __init__ time as before), non-symbolic fields don't (values stored verbatim). Set True on a non-symbolic field to revalidate the default and every self.x = ... assignment; set False on a symbolic field to skip apply at __init__.

None
wrap bool | None

Whether raw dict / list values supplied to this field are wrapped into pg.Dict / pg.List at the field boundary. None (default) resolves to the owning class's sym axis — wrap under sym=True, raw under sym=False. Strictly narrower than validate: only gates the symbolic transform callback — type-validation and __pg_accept__-based coercions still run. Set False to preserve raw containers (and type(x) is dict / object identity) at the cost of the framework's tree-walk features on this field's subtree. Moot when validate=False — the whole apply pipeline is skipped.

None

Returns:

Type Description
Any

A FieldDescriptor carrying the resolved default plus

Any

doc/metadata/flags. The return is typed as Any so the field's

Any

declared annotation is what type checkers surface.

Source code in pygx/symbolic/_dataclass_field.py
def field(
    *,
    value_spec: pg_typing.ValueSpec | None = None,
    default: Any = pg_typing.MISSING_VALUE,
    default_factory: Callable[[], Any] | None = None,
    doc: str | None = None,
    metadata: dict[str, Any] | None = None,
    transform: Callable[[Any], Any] | None = None,
    validator: Callable[[Any], None] | None = None,
    alias: str | None = None,
    repr: bool | None = None,  # pylint: disable=redefined-builtin
    compare: bool | None = None,
    hash: bool | None = None,  # pylint: disable=redefined-builtin
    init: bool | None = None,
    clone: bool | None = None,
    validate: bool | None = None,
    wrap: bool | None = None,
) -> Any:
    """Field descriptor for ``pg.Object`` dataclass-like fields.

    Registered as a ``field_specifier`` on ``pg.ObjectMeta`` so that type
    checkers honoring :pep:`681` (pyright/Pylance/mypy) read the field's
    static properties from the keyword arguments here.

    ``pg.Object.__init__`` is keyword-only by default
    (``dataclass_transform(kw_only_default=True)``), so this helper exists
    primarily to attach ``doc`` or ``metadata`` to an annotation-style
    field — matching the third/fourth tuple slots in ``pg.members``::

        class A(pg.Object):
          x: int = pg.field(
              default=1,
              doc='Initial count.',
              metadata={'serialize_as': 'count'},
          )

    A ``value_spec`` may be supplied to apply richer runtime constraints
    than the bare annotation can express (e.g. regex / numeric bounds)::

        class A(pg.Object):
          x: str = pg.field(value_spec=pg.typing.Str(regex='a.*'))

    Args:
      value_spec: Optional explicit ``pg.typing.ValueSpec`` overriding
        the spec inferred from the field's type annotation. Use when
        the annotation cannot express the desired runtime constraint
        (e.g. ``pg.typing.Str(regex=...)``, ``pg.typing.Int(min_value=0)``).
        The type annotation still drives static type checking.
      default: The field's default value. If omitted (and no
        ``default_factory``), the field is required.
      default_factory: Zero-argument callable producing the default
        value. Stored on the field's value spec and invoked once per
        instance (stdlib-``dataclasses`` semantics) — supports factories
        like ``lambda: time.time()`` that should produce a fresh value
        each time. Mutually exclusive with ``default``.
      doc: Field docstring — equivalent to the third tuple slot in
        ``pg.members([(name, spec, doc, metadata)])``.
      metadata: Field metadata dict — equivalent to the fourth tuple
        slot in ``pg.members``.
      transform: Optional ``(value) -> value`` callable attached to the
        field's value spec. Runs after type validation inside
        ``ValueSpec.apply`` and is the same hook as the ``transform=``
        constructor kwarg on ``pg.typing.*`` specs — exposed here so
        annotation-style fields can wire up a transform without
        re-instantiating the spec.
      validator: Optional ``(value) -> None`` callable — the field's
        *after* check. Called with the final value once the value spec
        accepted (and possibly coerced/wrapped) it; raise to reject
        (re-raised path-suffixed as the same exception class). Runs
        everywhere the field validates: construct,
        ``self.x = ...`` assignment, ``sym_rebind``. Its return value is
        ignored — value *transformation* belongs to ``transform`` (the
        *before* hook), keeping validation and coercion separate. Not
        called for missing values of a partial construct, nor when
        ``validate=False`` skips the apply pipeline.
      alias: Optional wire-layer alias for this field's key. Accepted by
        ``from_json`` (either spelling; both at once is an error), emitted
        by ``to_json(by_alias=True)`` and JSON Schema. Python attribute
        access and ``__init__`` kwargs always use the field name. May not
        collide with ``_type``, a sibling field name, or another alias.
      repr: Whether the field appears in ``repr`` / ``format``. ``None``
        (default) resolves at read time to the field's storage mode:
        symbolic (``init=True``) → ``True``, non-symbolic
        (``init=False``) → ``False``. Mirrors
        ``dataclasses.field(repr=...)``.
      compare: Whether the field participates in symbolic equality
        (``sym_eq``) *and* symbolic ordering (``sym_lt`` / ``sym_gt`` and the
        ``order=True`` operators) — the two stay consistent, as in
        ``dataclasses.field(compare=...)``. Same ``None`` resolution as
        ``repr``.
      hash: Whether the field participates in symbolic hashing. Same
        ``None`` resolution as ``repr``. Mirrors
        ``dataclasses.field(hash=...)``.
      init: If False, drop this field from the generated ``__init__``.
        The field is then routed *outside* the symbolic state (stored on
        ``self.__dict__`` rather than ``_sym_attributes``) — i.e. it
        behaves like a plain Python attribute managed by the class,
        suitable for derived/computed state that ``on_sym_ready`` fills in.
        If a ``default`` or ``default_factory`` is given it seeds the
        initial value in ``__init__``; otherwise the attribute is unset
        until something assigns to it (mirrors
        ``dataclasses.field(init=False)`` + ``__post_init__``). ``None``
        (default) resolves from the field's name: ``_x`` →
        ``init=False`` (non-symbolic by convention),
        ``x`` → ``init=True``. Set an explicit value to override.
      clone: Only meaningful when ``init=False`` (no-op for symbolic
        fields). Same ``None`` resolution as ``repr``: ``None`` → don't
        clone (cloned instance reseeds from default / leaves unset and
        ``on_sym_ready`` recomputes). Set ``True`` to preserve the source
        value during ``sym_clone`` (the clone's ``deep`` flag is
        honored).
      validate: Whether values are validated against the field's value
        spec. Same ``None`` resolution: symbolic fields validate
        (``ValueSpec.apply`` runs at ``__init__`` time as before),
        non-symbolic fields don't (values stored verbatim). Set
        ``True`` on a non-symbolic field to revalidate the default
        and every ``self.x = ...`` assignment; set ``False`` on a
        symbolic field to skip ``apply`` at ``__init__``.
      wrap: Whether raw ``dict`` / ``list`` values supplied to
        this field are wrapped into ``pg.Dict`` / ``pg.List`` at the
        field boundary. ``None`` (default) resolves to the owning
        class's ``sym`` axis — wrap under ``sym=True``, raw under
        ``sym=False``. Strictly narrower than ``validate``: only gates
        the symbolic transform callback — type-validation and
        ``__pg_accept__``-based coercions still run. Set
        ``False`` to preserve raw containers (and ``type(x) is dict``
        / object identity) at the cost of the framework's tree-walk
        features on this field's subtree. Moot when
        ``validate=False`` — the whole apply pipeline is skipped.

    Returns:
      A `FieldDescriptor` carrying the resolved default plus
      doc/metadata/flags. The return is typed as ``Any`` so the field's
      declared annotation is what type checkers surface.
    """
    if default is not pg_typing.MISSING_VALUE and default_factory is not None:
        raise ValueError('Cannot specify both `default` and `default_factory`.')
    if value_spec is not None and not isinstance(
        value_spec, pg_typing.ValueSpec
    ):
        raise TypeError(
            f'`value_spec` must be a `pg.typing.ValueSpec`. '
            f'Encountered: {value_spec!r}.'
        )
    return FieldDescriptor(
        value_spec=value_spec,
        default=default,
        default_factory=default_factory,
        doc=doc,
        metadata=metadata,
        transform=transform,
        validator=validator,
        alias=alias,
        enable_repr=repr,
        enable_compare=compare,
        enable_hash=hash,
        enable_init=init,
        enable_clone=clone,
        enable_validation=validate,
        enable_wrap=wrap,
    )

diff

diff(
    left: Any,
    right: Any,
    flatten: bool = False,
    collapse: bool | str | Callable[[Any, Any], bool] = "same_type",
    mode: str = "diff",
) -> Nestable[Diff]

Inspect the symbolic diff between two objects.

For example::

class A(pg.Object): x: Any y: Any

class B(A): z: Any | None = None

# Diff the same object. pg.diff(A(1, 2), A(1, 2))

No diff

# Diff the same object with mode 'same'. pg.diff(A(1, 2), A(1, 2), mode='same')

A(1, 2)

# Diff different objects of the same type. pg.diff(A(1, 2), A(1, 3))

A( y = Diff( left=2, right=3 ) )

# Diff objects of different type. pg.diff(A(1, 2), B(1, 3))

Diff( left = A( x = 1, y = 2 ), right = B( x = 1, y = 3, z = None )

# Diff objects of different type with collapse. pg.diff(A(1, 2), B(1, 3), collapse=True)

A|B ( y = Diff( left = 2, right = 3, ), z = Diff( left = MISSING, right = None ) )

# Diff objects of different type with collapse and flatten. # Object type is included in key '_type'. pg.diff(A(1, pg.Dict(a=1)), B(1, pg.Dict(a=2)), collapse=True, flatten=True)

{ 'y.a': Diff(1, 2), 'z', Diff(MISSING, None), '_type': Diff(A, B) }

Parameters:

Name Type Description Default
left Any

The left object to compare.

required
right Any

The right object to compare.

required
flatten bool

If True, returns a level-1 dict with diff keys flattened. Otherwise preserve the hierarchy of the diff result.

False
collapse bool | str | Callable[[Any, Any], bool]

One of a boolean value, string or a callable object that indicates whether to collapse two different values. The default value 'same_type' means only collapse when the two values are of the same type.

'same_type'
mode str

Diff mode, should be one of ['diff', 'same', 'both']. For 'diff' mode (the default), the return value contains only different values. For 'same' mode, the return value contains only same values. For 'both', the return value contains both different and same values.

'diff'

Returns:

Type Description
Nestable[Diff]

A Diff object when flatten is False. Otherwise a dict of string (key path)

Nestable[Diff]

to Diff.

Source code in pygx/symbolic/_diff.py
def diff(
    left: Any,
    right: Any,
    flatten: bool = False,
    collapse: bool | str | Callable[[Any, Any], bool] = 'same_type',
    mode: str = 'diff',
) -> wire.Nestable[Diff]:
    """Inspect the symbolic diff between two objects.

    For example::

      class A(pg.Object):
        x: Any
        y: Any

      class B(A):
        z: Any | None = None


      # Diff the same object.
      pg.diff(A(1, 2), A(1, 2))

      >> No diff

      # Diff the same object with mode 'same'.
      pg.diff(A(1, 2), A(1, 2), mode='same')

      >> A(1, 2)

      # Diff different objects of the same type.
      pg.diff(A(1, 2), A(1, 3))

      >> A(
      >>   y = Diff(
      >>     left=2,
      >>     right=3
      >>   )
      >>  )

      # Diff objects of different type.
      pg.diff(A(1, 2), B(1, 3))

      >> Diff(
      >>    left = A(
      >>      x = 1,
      >>      y = 2
      >>    ),
      >>    right = B(
      >>      x = 1,
      >>      y = 3,
      >>      z = None
      >>    )

      # Diff objects of different type with collapse.
      pg.diff(A(1, 2), B(1, 3), collapse=True)

      >> A|B (
      >>   y = Diff(
      >>     left = 2,
      >>     right = 3,
      >>   ),
      >>   z = Diff(
      >>     left = MISSING,
      >>     right = None
      >>   )
      >> )

      # Diff objects of different type with collapse and flatten.
      # Object type is included in key '_type'.
      pg.diff(A(1, pg.Dict(a=1)), B(1, pg.Dict(a=2)), collapse=True, flatten=True)

      >> {
      >>    'y.a': Diff(1, 2),
      >>    'z', Diff(MISSING, None),
      >>    '_type': Diff(A, B)
      >> }

    Args:
      left: The left object to compare.
      right: The right object to compare.
      flatten: If True, returns a level-1 dict with diff keys flattened. Otherwise
        preserve the hierarchy of the diff result.
      collapse: One of a boolean value, string or a callable object that indicates
        whether to collapse two different values. The default value 'same_type'
        means only collapse when the two values are of the same type.
      mode: Diff mode, should be one of ['diff', 'same', 'both']. For 'diff' mode
        (the default), the return value contains only different values. For 'same'
        mode, the return value contains only same values. For 'both', the return
        value contains both different and same values.

    Returns:
      A `Diff` object when flatten is False. Otherwise a dict of string (key path)
      to `Diff`.
    """

    def _should_collapse(left, right):
        if isinstance(left, dict):
            if isinstance(right, dict):
                return True
        elif isinstance(left, list):
            return isinstance(right, list)

        if isinstance(left, (dict, base.Symbolic)) and isinstance(
            right, (dict, base.Symbolic)
        ):
            if collapse == 'same_type':
                return type(left) is type(right)
            elif callable(collapse):
                return collapse(left, right)
            elif isinstance(collapse, bool):
                return collapse
            else:
                raise ValueError(f'Unsupported `collapse` value: {collapse!r}')
        else:
            return False

    def _add_child_diff(diff_container, key, value, child_has_diff):
        if (mode != 'same' and child_has_diff) or (
            mode != 'diff' and not child_has_diff
        ):
            diff_container[key] = value

    def _get_container_ops(container):
        if isinstance(container, dict):
            return (
                container.__contains__,
                container.__getitem__,
                container.items,
            )
        else:
            assert isinstance(container, base.Symbolic)
            return (
                container.sym_hasattr,
                container.sym_getattr,
                container.sym_items,
            )

    def _diff(x, y) -> tuple[wire.Nestable[Diff], bool]:
        if x is y or x == y:
            return (Diff(left=x, right=y), False)
        if not _should_collapse(x, y):
            return (Diff(left=x, right=y), True)

        diff_value, has_diff = {}, False
        if isinstance(x, list):
            assert isinstance(y, list)

            def _child(l, index):
                return l[i] if index < len(l) else Diff.MISSING

            for i in range(max(len(x), len(y))):
                child_diff, child_has_diff = _diff(_child(x, i), _child(y, i))
                has_diff = has_diff or child_has_diff
                _add_child_diff(diff_value, str(i), child_diff, child_has_diff)
            diff_value = Diff(
                left=pg_list.List, right=pg_list.List, children=diff_value
            )
        else:
            assert isinstance(x, (dict, base.Symbolic))
            assert isinstance(y, (dict, base.Symbolic))

            x_haskey, _, x_items = _get_container_ops(x)
            y_haskey, y_getitem, y_items = _get_container_ops(y)

            for k, xv in x_items():
                yv = y_getitem(k) if y_haskey(k) else Diff.MISSING
                child_diff, child_has_diff = _diff(xv, yv)
                has_diff = has_diff or child_has_diff
                _add_child_diff(diff_value, k, child_diff, child_has_diff)

            for k, yv in y_items():
                if not x_haskey(k):
                    child_diff, _ = _diff(Diff.MISSING, yv)
                    has_diff = True
                    _add_child_diff(diff_value, k, child_diff, True)

            xt, yt = type(x), type(y)
            same_type = xt is yt
            if not same_type:
                has_diff = True

            if flatten:
                # Put type difference with key '_type'. Since symbolic
                # fields will not start with underscore, so there should be
                # no clash.
                if not same_type or mode != 'diff':
                    diff_value['_type'] = Diff(left=xt, right=yt)
            else:
                diff_value = Diff(left=xt, right=yt, children=diff_value)
        return diff_value, has_diff

    diff_value, has_diff = _diff(left, right)
    if not has_diff and mode == 'diff':
        diff_value = Diff()
    if flatten:
        diff_value = topology.flatten(diff_value)
    return diff_value

allow_empty_field_description

allow_empty_field_description(allow: bool = True) -> None

Allow empty field description, which is useful for testing purposes.

Source code in pygx/symbolic/_flags.py
def allow_empty_field_description(allow: bool = True) -> None:
    """Allow empty field description, which is useful for testing purposes."""
    global _ALLOW_EMPTY_FIELD_DESCRIPTION
    _ALLOW_EMPTY_FIELD_DESCRIPTION = allow

allow_partial

allow_partial(allow: bool | None = True) -> ContextManager[None]

Returns a context manager that allows partial values in scope.

This function is thread-safe and can be nested. In the nested use case, the allow flag of immediate parent context is effective.

Example::

class A(pg.Object): x: int y: int

with pg.allow_partial(True): a = A(x=1) # Missing y, but OK with pg.allow_partial(False): a.sym_rebind(x=pg.MISSING_VALUE) # NOT OK a.sym_rebind(x=pg.MISSING_VALUE) # OK

Parameters:

Name Type Description Default
allow bool | None

If True, allow partial symbolic values in scope. If False, do not allow partial symbolic values in scope even if individual objects allow so. If None, honor object-level allow_partial property.

True

Returns:

Type Description
ContextManager[None]

A context manager that allows/disallow partial symbolic values in scope. After leaving the scope, the allow_partial state of individual objects will remain intact.

Source code in pygx/symbolic/_flags.py
def allow_partial(allow: bool | None = True) -> ContextManager[None]:
    """Returns a context manager that allows partial values in scope.

    This function is thread-safe and can be nested. In the nested use case, the
    allow flag of immediate parent context is effective.

    Example::

      class A(pg.Object):
        x: int
        y: int

      with pg.allow_partial(True):
        a = A(x=1)  # Missing `y`, but OK
        with pg.allow_partial(False):
          a.sym_rebind(x=pg.MISSING_VALUE)  # NOT OK
        a.sym_rebind(x=pg.MISSING_VALUE)  # OK

    Args:
      allow: If True, allow partial symbolic values in scope.
        If False, do not allow partial symbolic values in scope even if
        individual objects allow so. If None, honor object-level
        `allow_partial` property.

    Returns:
      A context manager that allows/disallow partial symbolic values in scope.
        After leaving the scope, the `allow_partial` state of individual objects
        will remain intact.
    """
    return thread_local.thread_local_value_scope(
        _TLS_ALLOW_PARTIAL, allow, None
    )

allow_repeated_class_registration

allow_repeated_class_registration(allow: bool = True) -> None

Allow repeated class registration, which is useful for testing purposes.

Source code in pygx/symbolic/_flags.py
def allow_repeated_class_registration(allow: bool = True) -> None:
    """Allow repeated class registration, which is useful for testing purposes."""
    global _ALLOW_REPEATED_CLASS_REGISTRATION
    _ALLOW_REPEATED_CLASS_REGISTRATION = allow

allow_writable_accessors

allow_writable_accessors(writable: bool | None = True) -> ContextManager[None]

Returns a context manager that makes accessor writable in scope.

This function is thread-safe and can be nested. In the nested use case, the writable flag of immediate parent context is effective.

Example::

sd1 = pg.Dict() sd2 = pg.Dict(accessor_writable=False) with pg.allow_writable_accessors(False): sd1.a = 2 # NOT OK sd2.a = 2 # NOT OK with pg.allow_writable_accessors(True): sd1.a = 2 # OK sd2.a = 2 # OK with pg.allow_writable_accessors(None): sd1.a = 1 # OK sd2.a = 1 # NOT OK

Parameters:

Name Type Description Default
writable bool | None

If True, allow write access with accessors (setattr, setitem) for all symbolic values in scope. If False, disallow write access via accessors for all symbolic values in scope, even if individual objects allow so. If None, honor object-level accessor_writable flag.

True

Returns:

Type Description
ContextManager[None]

A context manager that allows/disallows writable accessors of all symbolic values in scope. After leaving the scope, the accessor_writable flag of individual objects will remain intact.

Source code in pygx/symbolic/_flags.py
def allow_writable_accessors(
    writable: bool | None = True,
) -> ContextManager[None]:
    """Returns a context manager that makes accessor writable in scope.

    This function is thread-safe and can be nested. In the nested use case, the
    writable flag of immediate parent context is effective.

    Example::

      sd1 = pg.Dict()
      sd2 = pg.Dict(accessor_writable=False)
      with pg.allow_writable_accessors(False):
        sd1.a = 2  # NOT OK
        sd2.a = 2  # NOT OK
        with pg.allow_writable_accessors(True):
          sd1.a = 2   # OK
          sd2.a = 2  # OK
          with pg.allow_writable_accessors(None):
            sd1.a = 1  # OK
            sd2.a = 1  # NOT OK

    Args:
      writable: If True, allow write access with accessors (__setattr__,
        __setitem__) for all symbolic values in scope.
        If False, disallow write access via accessors for all symbolic values
        in scope, even if individual objects allow so.
        If None, honor object-level `accessor_writable` flag.

    Returns:
      A context manager that allows/disallows writable accessors of all
        symbolic values in scope. After leaving the scope, the
        `accessor_writable` flag of individual objects will remain intact.
    """
    return thread_local.thread_local_value_scope(
        _TLS_ACCESSOR_WRITABLE, writable, None
    )

as_sealed

as_sealed(sealed: bool | None = True) -> ContextManager[None]

Returns a context manager to treat symbolic values as sealed/unsealed.

While the user can use Symbolic.sym_seal to seal or unseal an individual object. This context manager is useful to create a readonly zone for operations on all existing symbolic objects.

This function is thread-safe and can be nested. In the nested use case, the sealed flag of immediate parent context is effective.

Example::

sd1 = pg.Dict() sd2 = pg.Dict().sym_seal()

with pg.as_sealed(True): sd1.a = 2 # NOT OK sd2.a = 2 # NOT OK with pg.as_sealed(False): sd1.a = 2 # OK sd2.a = 2 # OK with pg.as_sealed(None): sd1.a = 1 # OK sd2.a = 1 # NOT OK

Parameters:

Name Type Description Default
sealed bool | None

If True, treats all symbolic values as sealed in scope. If False, treats all as unsealed. If None, honor object-level sealed state.

True

Returns:

Type Description
ContextManager[None]

A context manager that treats all symbolic values as sealed/unsealed in scope. After leaving the scope, the sealed state of individual objects will remain intact.

Source code in pygx/symbolic/_flags.py
def as_sealed(sealed: bool | None = True) -> ContextManager[None]:
    """Returns a context manager to treat symbolic values as sealed/unsealed.

    While the user can use `Symbolic.sym_seal` to seal or unseal an individual object.
    This context manager is useful to create a readonly zone for operations on
    all existing symbolic objects.

    This function is thread-safe and can be nested. In the nested use case, the
    sealed flag of immediate parent context is effective.

    Example::

      sd1 = pg.Dict()
      sd2 = pg.Dict().sym_seal()

      with pg.as_sealed(True):
        sd1.a = 2  # NOT OK
        sd2.a = 2  # NOT OK
        with pg.as_sealed(False):
          sd1.a = 2   # OK
          sd2.a = 2  # OK
          with pg.as_sealed(None):
            sd1.a = 1  # OK
            sd2.a = 1  # NOT OK

    Args:
      sealed: If True, treats all symbolic values as sealed in scope.
        If False, treats all as unsealed.
        If None, honor object-level `sealed` state.

    Returns:
      A context manager that treats all symbolic values as sealed/unsealed
        in scope. After leaving the scope, the sealed state of individual objects
        will remain intact.
    """
    return thread_local.thread_local_value_scope(_TLS_SEALED, sealed, None)

auto_call_functors

auto_call_functors(enabled: bool = True) -> ContextManager[None]

Returns a context manager to enable or disable auto call for functors.

auto_call_functors is thread-safe and can be nested. For example::

@pg.symbolize def foo(x, y): return x + y

with pg.auto_call_functors(True): a = foo(1, 2) assert a == 3 with pg.auto_call_functors(False): b = foo(1, 2) assert isinstance(b, foo)

Parameters:

Name Type Description Default
enabled bool

If True, enable auto call for functors. Otherwise, auto call will be disabled.

True

Returns:

Type Description
ContextManager[None]

A context manager for enabling/disabling auto call for functors.

Source code in pygx/symbolic/_flags.py
def auto_call_functors(enabled: bool = True) -> ContextManager[None]:
    """Returns a context manager to enable or disable auto call for functors.

    `auto_call_functors` is thread-safe and can be nested. For example::

      @pg.symbolize
      def foo(x, y):
        return x + y

      with pg.auto_call_functors(True):
        a = foo(1, 2)
        assert a == 3
        with pg.auto_call_functors(False):
          b = foo(1, 2)
          assert isinstance(b, foo)

    Args:
      enabled: If True, enable auto call for functors.
        Otherwise, auto call will be disabled.

    Returns:
      A context manager for enabling/disabling auto call for functors.
    """
    return thread_local.thread_local_value_scope(
        _TLS_AUTO_CALL_FUNCTORS, enabled, False
    )

enable_type_debug

enable_type_debug(enabled: bool = True) -> None

Toggle inclusion of pygx-internal frames in type-error tracebacks.

When disabled (the default), tracebacks for type-related errors raised during pg.Object construction (and similar entry points) are rewritten to drop frames that live inside the pygx package, leaving only the user's call sites visible. When enabled, the full traceback (including every pygx-internal frame) is preserved — useful when debugging the typing machinery itself.

Parameters:

Name Type Description Default
enabled bool

If True, keep pygx-internal frames; if False, hide them.

True
Source code in pygx/symbolic/_flags.py
def enable_type_debug(enabled: bool = True) -> None:
    """Toggle inclusion of pygx-internal frames in type-error tracebacks.

    When disabled (the default), tracebacks for type-related errors raised
    during ``pg.Object`` construction (and similar entry points) are
    rewritten to drop frames that live inside the ``pygx`` package, leaving
    only the user's call sites visible. When enabled, the full traceback
    (including every pygx-internal frame) is preserved — useful when
    debugging the typing machinery itself.

    Args:
      enabled: If True, keep pygx-internal frames; if False, hide them.
    """
    global _TYPE_DEBUG_ENABLED
    _TYPE_DEBUG_ENABLED = enabled

get_load_handler

get_load_handler() -> Callable[..., Any] | None

Returns global load handler.

Source code in pygx/symbolic/_flags.py
def get_load_handler() -> Callable[..., Any] | None:
    """Returns global load handler."""
    return _LOAD_HANDLER

get_origin_stacktrace_limit

get_origin_stacktrace_limit() -> int

Returns the limited depth of stacktrace for tracking.

Source code in pygx/symbolic/_flags.py
def get_origin_stacktrace_limit() -> int:
    """Returns the limited depth of stacktrace for tracking."""
    return _ORIGIN_STACKTRACE_LIMIT

get_save_handler

get_save_handler() -> Callable[..., Any] | None

Returns global save handler.

Source code in pygx/symbolic/_flags.py
def get_save_handler() -> Callable[..., Any] | None:
    """Returns global save handler."""
    return _SAVE_HANDLER

is_change_notification_enabled

is_change_notification_enabled() -> bool

Returns True if change notification is enabled.

Source code in pygx/symbolic/_flags.py
def is_change_notification_enabled() -> bool:
    """Returns True if change notification is enabled."""
    return thread_local.thread_local_get(_TLS_ENABLE_CHANGE_NOTIFICATION, True)

is_empty_field_description_allowed

is_empty_field_description_allowed() -> bool

Returns True if empty field description is allowed.

Source code in pygx/symbolic/_flags.py
def is_empty_field_description_allowed() -> bool:
    """Returns True if empty field description is allowed."""
    return _ALLOW_EMPTY_FIELD_DESCRIPTION

is_repeated_class_registration_allowed

is_repeated_class_registration_allowed() -> bool

Returns True if repeated class registration is allowed.

Source code in pygx/symbolic/_flags.py
def is_repeated_class_registration_allowed() -> bool:
    """Returns True if repeated class registration is allowed."""
    return _ALLOW_REPEATED_CLASS_REGISTRATION

is_tracking_origin

is_tracking_origin() -> bool

Returns if origin of symbolic object are being tracked.

Source code in pygx/symbolic/_flags.py
def is_tracking_origin() -> bool:
    """Returns if origin of symbolic object are being tracked."""
    return thread_local.thread_local_get(_TLS_ENABLE_ORIGIN_TRACKING, False)

is_type_debug_enabled

is_type_debug_enabled() -> bool

Returns True if type-error tracebacks include pygx-internal frames.

Source code in pygx/symbolic/_flags.py
def is_type_debug_enabled() -> bool:
    """Returns True if type-error tracebacks include pygx-internal frames."""
    return _TYPE_DEBUG_ENABLED

is_under_accessor_writable_scope

is_under_accessor_writable_scope() -> bool | None

Return True if symbolic values are treated as sealed in current context.

Source code in pygx/symbolic/_flags.py
def is_under_accessor_writable_scope() -> bool | None:
    """Return True if symbolic values are treated as sealed in current context."""
    return thread_local.thread_local_get(_TLS_ACCESSOR_WRITABLE, None)

is_under_partial_scope

is_under_partial_scope() -> bool | None

Return True if partial value is allowed in current context.

Source code in pygx/symbolic/_flags.py
def is_under_partial_scope() -> bool | None:
    """Return True if partial value is allowed in current context."""
    return thread_local.thread_local_get(_TLS_ALLOW_PARTIAL, None)

is_under_sealed_scope

is_under_sealed_scope() -> bool | None

Return True if symbolic values are treated as sealed in current context.

Source code in pygx/symbolic/_flags.py
def is_under_sealed_scope() -> bool | None:
    """Return True if symbolic values are treated as sealed in current context."""
    return thread_local.thread_local_get(_TLS_SEALED, None)

is_warn_on_capability_narrowing_enabled

is_warn_on_capability_narrowing_enabled() -> bool

Returns True if capability-narrowing warnings are emitted.

Source code in pygx/symbolic/_flags.py
def is_warn_on_capability_narrowing_enabled() -> bool:
    """Returns True if capability-narrowing warnings are emitted."""
    return _WARN_ON_CAPABILITY_NARROWING

is_warn_on_field_override_no_annotation_enabled

is_warn_on_field_override_no_annotation_enabled() -> bool

Returns True if bare-assignment field-override warnings are emitted.

Source code in pygx/symbolic/_flags.py
def is_warn_on_field_override_no_annotation_enabled() -> bool:
    """Returns True if bare-assignment field-override warnings are emitted."""
    return _WARN_ON_FIELD_OVERRIDE_NO_ANNOTATION

notify_on_change

notify_on_change(enabled: bool = True) -> ContextManager[None]

Returns a context manager to enable or disable notification upon change.

notify_on_change is thread-safe and can be nested. For example, in the following code, on_sym_change (thus on_sym_bound) method of a will be triggered due to the rebind in the inner with statement, and those of b will not be triggered as the outer with statement disables the notification::

with pg.notify_on_change(False): with pg.notify_on_change(True): a.sym_rebind(b=1) b.sym_rebind(x=2)

Parameters:

Name Type Description Default
enabled bool

If True, enable change notification in current scope. Otherwise, disable notification.

True

Returns:

Type Description
ContextManager[None]

A context manager for allowing/disallowing change notification in scope.

Source code in pygx/symbolic/_flags.py
def notify_on_change(enabled: bool = True) -> ContextManager[None]:
    """Returns a context manager to enable or disable notification upon change.

    `notify_on_change` is thread-safe and can be nested. For example, in the
    following code, `on_sym_change` (thus `on_sym_bound`) method of `a` will be
    triggered due to the rebind in the inner `with` statement, and those of `b`
    will not be triggered as the outer `with` statement disables the
    notification::

      with pg.notify_on_change(False):
        with pg.notify_on_change(True):
          a.sym_rebind(b=1)
        b.sym_rebind(x=2)

    Args:
      enabled: If True, enable change notification in current scope.
        Otherwise, disable notification.

    Returns:
      A context manager for allowing/disallowing change notification in scope.
    """
    return thread_local.thread_local_value_scope(
        _TLS_ENABLE_CHANGE_NOTIFICATION, enabled, True
    )

notify_once

notify_once(coalesce_nested: bool = False) -> Iterator[None]

Returns a context manager that coalesces change notifications.

Within the scope, every object touched by sym_rebind (or accessor writes such as obj.x = ...) receives its on_sym_change event when the scope exits, instead of once per mutation. Repeated updates to the same field collapse into a single FieldUpdate that keeps the original old_value and the final new_value. This is useful when applying a batch of related edits that should be observed as one change.

For example, x and y below are each notified a single time::

with pg.notify_once(): x.sym_rebind(a=2, c=3) y.sym_rebind(p=1) x.sym_rebind(a=1) # x.on_sym_change fires once (with a: ->1, c: ->3); # y.on_sym_change fires once (with p: ->1).

Nesting. notify_once is thread-safe and nestable. Each object is flushed by the scope it was rebound in — the scope where sym_rebind (or an accessor write) was called on that object. So a helper that wraps its own edits in notify_once always observes them settled when it returns, regardless of how it was called::

def configure(x): with pg.notify_once(): x.sym_rebind(a=1) x.sym_rebind(b=2) return x # x is fully notified here, even if called within a scope

with pg.notify_once(): configure(x) # x flushed when configure returns y = Y(p=x.derived) # reads x's settled state y.sym_rebind(q=1) # y flushed here

A parent reached only by walking up from a rebound child is not owned by the inner scope; it is notified by whichever scope rebinds the parent, or by the outermost scope::

with pg.notify_once(): p.sym_rebind(w=1) # p rebound here -> owned by this scope with pg.notify_once(): p.child.sym_rebind(v=2) # p.child rebound here -> owned by inner # inner exit: p.child is notified; p is not (it is the outer's) # outer exit: p is notified once (with w and the child change)

Children are therefore always notified before their parents, and an object is notified once per scope that directly rebinds it (each event reports a correct, coalesced transition — no event ever carries a stale value).

Coalescing nested scopes. Pass coalesce_nested=True to absorb any scopes nested inside this one: they no longer flush on their own exit, and every accumulated change is flushed once when this scope exits. Use it to coalesce edits made by nested scopes, e.g. in a loop::

with pg.notify_once(coalesce_nested=True): for item in items: with pg.notify_once(): # absorbed; does not flush here y.children.append(item) # y is notified once here, for all appended children.

Deferred side effects. Because notifications are deferred to scope exit, side effects driven by on_sym_change (e.g. pg.Object.on_sym_bound recomputation, or pg.List removal of deleted items) are also deferred. Caches derived from content (including functools.cached_property) are still invalidated eagerly, so most reads within the scope stay consistent; only state recomputed as a side effect of on_sym_change lags until the flush. When change notification is disabled via pygx.notify_on_change(False), there is nothing to batch and notify_once has no effect.

Parameters:

Name Type Description Default
coalesce_nested bool

If True, notify_once scopes nested within this one do not flush on their own exit; their changes are coalesced and flushed together when this scope exits.

False

Yields:

Type Description
None

None, within the coalescing scope.

Source code in pygx/symbolic/_flags.py
@contextlib.contextmanager
def notify_once(coalesce_nested: bool = False) -> Iterator[None]:
    """Returns a context manager that coalesces change notifications.

    Within the scope, every object touched by ``sym_rebind`` (or accessor writes
    such as ``obj.x = ...``) receives its ``on_sym_change`` event when the
    scope exits, instead of once per mutation. Repeated updates to the same
    field collapse into a single ``FieldUpdate`` that keeps the original
    ``old_value`` and the final ``new_value``. This is useful when applying a
    batch of related edits that should be observed as one change.

    For example, ``x`` and ``y`` below are each notified a single time::

      with pg.notify_once():
        x.sym_rebind(a=2, c=3)
        y.sym_rebind(p=1)
        x.sym_rebind(a=1)
      # x.on_sym_change fires once (with a: <orig>->1, c: <orig>->3);
      # y.on_sym_change fires once (with p: <orig>->1).

    **Nesting.** `notify_once` is thread-safe and nestable. Each object is
    flushed by the scope it was *rebound in* — the scope where ``sym_rebind`` (or
    an accessor write) was called on that object. So a helper that wraps its
    own edits in `notify_once` always observes them settled when it returns,
    regardless of how it was called::

      def configure(x):
        with pg.notify_once():
          x.sym_rebind(a=1)
          x.sym_rebind(b=2)
        return x  # `x` is fully notified here, even if called within a scope

      with pg.notify_once():
        configure(x)            # `x` flushed when `configure` returns
        y = Y(p=x.derived)      # reads `x`'s settled state
        y.sym_rebind(q=1)
      # `y` flushed here

    A parent reached only by walking up from a rebound child is *not* owned by
    the inner scope; it is notified by whichever scope rebinds the parent, or
    by the outermost scope::

      with pg.notify_once():
        p.sym_rebind(w=1)             # `p` rebound here -> owned by this scope
        with pg.notify_once():
          p.child.sym_rebind(v=2)     # `p.child` rebound here -> owned by inner
        # inner exit: `p.child` is notified; `p` is not (it is the outer's)
      # outer exit: `p` is notified once (with w and the child change)

    Children are therefore always notified before their parents, and an object
    is notified once per scope that *directly rebinds* it (each event reports a
    correct, coalesced transition — no event ever carries a stale value).

    **Coalescing nested scopes.** Pass ``coalesce_nested=True`` to absorb any
    scopes nested inside this one: they no longer flush on their own exit, and
    every accumulated change is flushed once when this scope exits. Use it to
    coalesce edits made by nested scopes, e.g. in a loop::

      with pg.notify_once(coalesce_nested=True):
        for item in items:
          with pg.notify_once():   # absorbed; does not flush here
            y.children.append(item)
      # `y` is notified once here, for all appended children.

    **Deferred side effects.** Because notifications are deferred to scope
    exit, side effects driven by ``on_sym_change`` (e.g.
    ``pg.Object.on_sym_bound`` recomputation, or ``pg.List`` removal of deleted
    items) are also deferred. Caches derived from content (including
    ``functools.cached_property``) are still invalidated eagerly, so most reads
    within the scope stay consistent; only state recomputed as a side effect of
    ``on_sym_change`` lags until the flush. When change notification is disabled
    via [`pygx.notify_on_change`][pygx.symbolic.notify_on_change]``(False)``,
    there is nothing to batch and `notify_once` has no effect.

    Args:
      coalesce_nested: If True, ``notify_once`` scopes nested within this one
        do not flush on their own exit; their changes are coalesced and
        flushed together when this scope exits.

    Yields:
      None, within the coalescing scope.
    """
    batch = thread_local.thread_local_get(_TLS_NOTIFICATION_BATCH, None)
    if batch is None:
        batch = _NotificationBatch()
        thread_local.thread_local_set(_TLS_NOTIFICATION_BATCH, batch)
    batch.push_scope(coalesce_nested)
    try:
        yield
    finally:
        # Drain even when an exception is unwinding: the tree was already
        # mutated, and `on_sym_change` carries structural fixups (e.g. `List`
        # cleanup).
        depth = batch.pop_scope()
        if not batch.has_open_scope():
            # Outermost scope: tear down the thread-local before draining so
            # that rebinds triggered from within `on_sym_change` notify
            # immediately instead of re-entering this (now-draining) batch.
            # Flush everything that remains, including deferred ancestors.
            thread_local.thread_local_del(_TLS_NOTIFICATION_BATCH)
            batch.drain(None)
        elif not batch.is_absorbed():
            # A nested scope with no coalescing ancestor: flush the objects it
            # owns (rebound at this depth or, if it coalesces nested scopes,
            # deeper), leaving ancestors and outer-owned objects for the parent.
            batch.drain(depth)

set_load_handler

set_load_handler(
    load_handler: Callable[..., Any] | None,
) -> Callable[..., Any] | None

Sets global load handler.

Parameters:

Name Type Description Default
load_handler Callable[..., Any] | None

A callable object that takes arbitrary arguments and returns a value. symbolic.load method will pass through all arguments to this handler and return its return value.

required

Returns:

Type Description
Callable[..., Any] | None

Previous global load handler.

Source code in pygx/symbolic/_flags.py
def set_load_handler(
    load_handler: Callable[..., Any] | None,
) -> Callable[..., Any] | None:
    """Sets global load handler.

    Args:
      load_handler: A callable object that takes arbitrary arguments and returns
        a value. `symbolic.load` method will pass through all arguments to this
        handler and return its return value.

    Returns:
      Previous global load handler.
    """
    if load_handler and not callable(load_handler):
        raise ValueError('`load_handler` must be callable.')
    global _LOAD_HANDLER
    old_handler = _LOAD_HANDLER
    _LOAD_HANDLER = load_handler
    return old_handler

set_origin_stacktrace_limit

set_origin_stacktrace_limit(limit: int) -> None

Set stack trace limit for origin tracking.

Source code in pygx/symbolic/_flags.py
def set_origin_stacktrace_limit(limit: int) -> None:
    """Set stack trace limit for origin tracking."""
    global _ORIGIN_STACKTRACE_LIMIT
    _ORIGIN_STACKTRACE_LIMIT = limit

set_save_handler

set_save_handler(
    save_handler: Callable[..., Any] | None,
) -> Callable[..., Any] | None

Sets global save handler.

Parameters:

Name Type Description Default
save_handler Callable[..., Any] | None

A callable object that takes at least one argument as value to save. symbolic.save method will pass through all arguments to this handler and return its return value.

required

Returns:

Type Description
Callable[..., Any] | None

Previous global save handler.

Source code in pygx/symbolic/_flags.py
def set_save_handler(
    save_handler: Callable[..., Any] | None,
) -> Callable[..., Any] | None:
    """Sets global save handler.

    Args:
      save_handler: A callable object that takes at least one argument as value to
        save. `symbolic.save` method will pass through all arguments to this
        handler and return its return value.

    Returns:
      Previous global save handler.
    """
    if save_handler and not callable(save_handler):
        raise ValueError('`save_handler` must be callable.')
    global _SAVE_HANDLER
    old_handler = _SAVE_HANDLER
    _SAVE_HANDLER = save_handler
    return old_handler

should_call_functors_during_init

should_call_functors_during_init() -> bool | None

Return True functors should be automatically called during init.

Source code in pygx/symbolic/_flags.py
def should_call_functors_during_init() -> bool | None:
    """Return True functors should be automatically called during __init__."""
    return thread_local.thread_local_get(_TLS_AUTO_CALL_FUNCTORS, None)

track_origin

track_origin(enabled: bool = True) -> ContextManager[None]

Returns a context manager to enable or disable origin tracking.

track_origin is thread-safe and can be nested. For example::

a = pg.Dict(x=1) with pg.track_origin(False): with pg.track_origin(True): # b's origin will be tracked, which can be accessed by b.sym_origin. b = a.sym_clone() # c's origin will not be tracked, c.sym_origin returns None. c = a.sym_clone()

Parameters:

Name Type Description Default
enabled bool

If True, the origin of symbolic values will be tracked during object cloning and retuning from functors under current scope.

True

Returns:

Type Description
ContextManager[None]

A context manager for enable or disable origin tracking.

Source code in pygx/symbolic/_flags.py
def track_origin(enabled: bool = True) -> ContextManager[None]:
    """Returns a context manager to enable or disable origin tracking.

    `track_origin` is thread-safe and can be nested. For example::

      a = pg.Dict(x=1)
      with pg.track_origin(False):
        with pg.track_origin(True):
          # b's origin will be tracked, which can be accessed by `b.sym_origin`.
          b = a.sym_clone()
        # c's origin will not be tracked, `c.sym_origin` returns None.
        c = a.sym_clone()

    Args:
      enabled: If True, the origin of symbolic values will be tracked during
        object cloning and retuning from functors under current scope.

    Returns:
      A context manager for enable or disable origin tracking.
    """
    return thread_local.thread_local_value_scope(
        _TLS_ENABLE_ORIGIN_TRACKING, enabled, False
    )

warn_on_capability_narrowing

warn_on_capability_narrowing(enabled: bool = True) -> None

Toggle the warning emitted when a subclass narrows a class capability.

A subclass that explicitly removes a class-level capability its base granted — attr_read / attr_write / sym TrueFalse, or frozen FalseTrue — breaks substitutability: code that uses an instance through its base type may then fail (e.g. reading obj.x, or calling sym_rebind). When enabled (the default), pygx emits a one-shot UserWarning at class creation. See the flag-combination matrix in pg_object_semantics.md for the rationale.

Parameters:

Name Type Description Default
enabled bool

If True, emit the warning; if False, silence it.

True
Source code in pygx/symbolic/_flags.py
def warn_on_capability_narrowing(enabled: bool = True) -> None:
    """Toggle the warning emitted when a subclass narrows a class capability.

    A subclass that *explicitly* removes a class-level capability its base
    granted — ``attr_read`` / ``attr_write`` / ``sym`` ``True`` → ``False``, or
    ``frozen`` ``False`` → ``True`` — breaks substitutability: code that uses an
    instance through its base type may then fail (e.g. reading ``obj.x``, or
    calling ``sym_rebind``). When enabled (the default), pygx emits a one-shot
    ``UserWarning`` at class creation. See the flag-combination matrix in
    ``pg_object_semantics.md`` for the rationale.

    Args:
      enabled: If True, emit the warning; if False, silence it.
    """
    global _WARN_ON_CAPABILITY_NARROWING
    _WARN_ON_CAPABILITY_NARROWING = enabled

warn_on_field_override_no_annotation

warn_on_field_override_no_annotation(enabled: bool = True) -> None

Toggle the warning emitted for bare-assignment field overrides.

A subclass that shadows an inherited pg.Object field via bare class-level assignment (e.g. sender = 'User' instead of sender: str = 'User') is handled correctly at runtime, but pyright/PEP 681 dataclass_transform synthesizes __init__ from class-level annotations only — the override is invisible to static type checkers. When enabled (the default), pygx emits a one-shot UserWarning at class creation pointing at the offending name and suggesting the re-annotated form.

Parameters:

Name Type Description Default
enabled bool

If True, emit the warning; if False, silence it.

True
Source code in pygx/symbolic/_flags.py
def warn_on_field_override_no_annotation(enabled: bool = True) -> None:
    """Toggle the warning emitted for bare-assignment field overrides.

    A subclass that shadows an inherited ``pg.Object`` field via bare class-level
    assignment (e.g. ``sender = 'User'`` instead of ``sender: str = 'User'``) is
    handled correctly at runtime, but pyright/PEP 681 ``dataclass_transform``
    synthesizes ``__init__`` from class-level *annotations* only — the override
    is invisible to static type checkers. When enabled (the default), pygx emits
    a one-shot ``UserWarning`` at class creation pointing at the offending name
    and suggesting the re-annotated form.

    Args:
      enabled: If True, emit the warning; if False, silence it.
    """
    global _WARN_ON_FIELD_OVERRIDE_NO_ANNOTATION
    _WARN_ON_FIELD_OVERRIDE_NO_ANNOTATION = enabled

as_functor

as_functor(func: Callable, ignore_extra_args: bool = False) -> Functor

Make a functor object from a regular python function.

NOTE(daiyip): This method is designed to create on-the-go functor object, usually for lambdas. To create a reusable functor class, please use functor_class method.

Parameters:

Name Type Description Default
func Callable

A regular python function.

required
ignore_extra_args bool

If True, extra argument which is not acceptable by func will be ignored.

False

Returns:

Type Description
Functor

Functor object from input function.

Source code in pygx/symbolic/_functor.py
def as_functor(
    func: Callable,  # pylint: disable=g-bare-generic
    ignore_extra_args: bool = False,
) -> Functor:
    """Make a functor object from a regular python function.

    NOTE(daiyip): This method is designed to create on-the-go functor object,
    usually for lambdas. To create a reusable functor class, please use
    `functor_class` method.

    Args:
      func: A regular python function.
      ignore_extra_args: If True, extra argument which is not acceptable by `func`
        will be ignored.

    Returns:
      Functor object from input function.
    """
    return functor_class(func)(
        ignore_extra_args=ignore_extra_args
    )  # pytype: disable=not-instantiable

functor

functor(
    args: (
        None
        | list[
            tuple[str | KeySpec, ValueSpec, str]
            | tuple[str | KeySpec, ValueSpec, str, Any]
        ]
    ) = None,
    returns: ValueSpec | None = None,
    base_class: type[Functor] | None = None,
    **kwargs: Any
) -> Callable[..., type[Functor]] | type[Functor]

Function/Decorator for creating symbolic function from regular function.

Example::

# Create a symbolic function without specifying the # validation rules for arguments. @pg.functor def foo(x, y): return x + y

f = foo(1, 2) assert f() == 3

# Create a symbolic function with specifying the # the validation rules for argument 'a', 'args', and 'kwargs'. @pg.functor([ ('a', pg.typing.Int()), ('b', pg.typing.Float()), ('args', pg.List(pg.typing.Int())), (pg.typing.StrKey(), pg.typing.Int()) ]) def bar(a, b, c, args, *kwargs): return a * b / c + sum(args) + sum(kwargs.values())

See pygx.Functor for more details on symbolic function.

Parameters:

Name Type Description Default
args None | list[tuple[str | KeySpec, ValueSpec, str] | tuple[str | KeySpec, ValueSpec, str, Any]]

A list of tuples that defines the schema for function arguments. Please see functor_class for detailed explanation of args.

None
returns ValueSpec | None

Optional value spec for return value.

None
base_class type[Functor] | None

Optional base class derived from symbolic.Functor. If None, returning functor will inherit from symbolic.Functor.

None
**kwargs Any

Keyword arguments for infrequently used options: Acceptable keywords are: * serialization_key: An optional string to be used as the serialization key for the class during sym_jsonify. If None, cls.__type_name__ will be used. This is introduced for scenarios when we want to relocate a class, before the downstream can recognize the new location, we need the class to serialize it using previous key. * additional_keys: An optional list of strings as additional keys to deserialize an object of the registered class. This can be useful when we need to relocate or rename the registered class while being able to load existing serialized JSON values.

{}

Returns:

Type Description
Callable[..., type[Functor]] | type[Functor]

A function that converts a regular function into a symbolic function.

Source code in pygx/symbolic/_functor.py
def functor(
    args: None
    | (
        list[
            (
                tuple[str | pg_typing.KeySpec, pg_typing.ValueSpec, str]
                | tuple[str | pg_typing.KeySpec, pg_typing.ValueSpec, str, Any]
            )
        ]
    ) = None,  # pylint: disable=bad-continuation
    returns: pg_typing.ValueSpec | None = None,
    base_class: type[Functor] | None = None,
    **kwargs: Any,
) -> Callable[..., type[Functor]] | type[Functor]:
    """Function/Decorator for creating symbolic function from regular function.

    Example::

      # Create a symbolic function without specifying the
      # validation rules for arguments.
      @pg.functor
      def foo(x, y):
        return x + y

      f = foo(1, 2)
      assert f() == 3

      # Create a symbolic function with specifying the
      # the validation rules for argument 'a', 'args', and 'kwargs'.
      @pg.functor([
        ('a', pg.typing.Int()),
        ('b', pg.typing.Float()),
        ('args', pg.List(pg.typing.Int())),
        (pg.typing.StrKey(), pg.typing.Int())
      ])
      def bar(a, b, c, *args, **kwargs):
        return a * b / c + sum(args) + sum(kwargs.values())

    See [`pygx.Functor`][pygx.symbolic.Functor] for more details on symbolic function.

    Args:
      args: A list of tuples that defines the schema for function arguments.
        Please see `functor_class` for detailed explanation of `args`.
      returns: Optional value spec for return value.
      base_class: Optional base class derived from `symbolic.Functor`. If None,
        returning functor will inherit from `symbolic.Functor`.
      **kwargs: Keyword arguments for infrequently used options: Acceptable
        keywords are:  * `serialization_key`: An optional string to be used as the
        serialization key for the class during `sym_jsonify`. If None,
        `cls.__type_name__` will be used. This is introduced for scenarios when we
        want to relocate a class, before the downstream can recognize the new
        location, we need the class to serialize it using previous key. *
        `additional_keys`: An optional list of strings as additional keys to
        deserialize an object of the registered class. This can be useful when we
        need to relocate or rename the registered class while being able to load
        existing serialized JSON values.

    Returns:
      A function that converts a regular function into a symbolic function.
    """
    if inspect.isfunction(args):
        assert returns is None
        return functor_class(
            typing.cast(Callable[..., Any], args),
            base_class=base_class,
            add_to_registry=True,
            **kwargs,
        )
    return lambda fn: functor_class(  # pylint: disable=g-long-lambda
        fn,
        args,  # pyright: ignore[reportArgumentType]
        returns,
        base_class=base_class,
        add_to_registry=True,
        **kwargs,
    )

functor_class

functor_class(
    func: FunctionType,
    args: (
        list[Field | FieldDef] | dict[FieldKeyDef, FieldValueDef] | None
    ) = None,
    returns: ValueSpec | None = None,
    base_class: type[Functor] | None = None,
    *,
    auto_doc: bool = False,
    auto_typing: bool = False,
    serialization_key: str | None = None,
    additional_keys: list[str] | None = None,
    add_to_registry: bool = False
) -> type[Functor]

Returns a functor class from a function.

Parameters:

Name Type Description Default
func FunctionType

Function to be wrapped into a functor.

required
args list[Field | FieldDef] | dict[FieldKeyDef, FieldValueDef] | None

Symbolic args specification. args is a list of tuples, each describes an argument from the input function. Each tuple is the format of (, , [description], [metadata-objects]). argument-name - a str or pg_typing.StrKey object. When pg_typing.StrKey is used, it describes the wildcard keyword argument. value-spec - a pg_typing.ValueSpec object or equivalent, e.g. primitive values which will be converted to ValueSpec implementation according to its type and used as its default value. description - a string to describe the agument. metadata-objects - an optional list of any type, which can be used to generate code according to the schema. There are notable rules in filling the args: 1) When args is None or arguments from the function signature are missing from it, schema.Field for these fields will be automatically generated and inserted into args. That being said, every arguments in input function will have a schema.Field counterpart in Functor.__schema__.fields sorted by the declaration order of each argument in the function signature ( other than the order in args). 2) Default argument values are specified along with function definition as regular python functions, instead of being set at schema.Field level. But validation rules can be set using args and apply to argument values. For example::

@pg.functor([('c', pg.typing.Int(min_value=0), 'Arg c')]) def foo(a, b, c=1, **kwargs): return a + b + c + sum(kwargs.values())

assert foo.schema.fields() == [
    pg.typing.Field('a', pg.Any(), 'Argument a'.),
    pg.typing.Field('b', pg.Any(), 'Argument b'.),
    pg.typing.Field('c', pg.typing.Int(), 'Arg c.),
    pg.typing.Filed(
        pg.typing.StrKey(), pg.Any(), 'Other arguments.')
]
# Prebind a=1, b=2, with default value c=1.
assert foo(1, 2)() == 4
None
returns ValueSpec | None

Optional schema specification for the return value.

None
base_class type[Functor] | None

Optional base class (derived from symbolic.Functor). If None, returned type will inherit from Functor directly.

None
auto_doc bool

If True, the descriptions of argument fields will be inherited from funciton docstr if they are not explicitly specified through args.

False
auto_typing bool

If True, the value spec for constraining each argument will be inferred from its annotation. Otherwise the value specs for all arguments will be pg.typing.Any().

False
serialization_key str | None

An optional string to be used as the serialization key for the class during sym_jsonify. If None, cls.__type_name__ will be used. This is introduced for scenarios when we want to relocate a class, before the downstream can recognize the new location, we need the class to serialize it using previous key.

None
additional_keys list[str] | None

An optional list of strings as additional keys to deserialize an object of the registered class. This can be useful when we need to relocate or rename the registered class while being able to load existing serialized JSON values.

None
add_to_registry bool

If True, the newly created functor class will be added to the registry for deserialization.

False

Returns:

Type Description
type[Functor]

symbolic.Functor subclass that wraps input function.

Raises:

Type Description
KeyError

names of symbolic arguments are not compatible with function signature.

TypeError

types of symbolic arguments are not compatible with function signature.

ValueError

default values of symbolic arguments are not compatible with function signature.

Source code in pygx/symbolic/_functor.py
def functor_class(
    func: types.FunctionType,
    args: (
        list[pg_typing.Field | pg_typing.FieldDef]
        | dict[pg_typing.FieldKeyDef, pg_typing.FieldValueDef]
        | None
    ) = None,
    returns: pg_typing.ValueSpec | None = None,
    base_class: type[Functor] | None = None,
    *,
    auto_doc: bool = False,
    auto_typing: bool = False,
    serialization_key: str | None = None,
    additional_keys: list[str] | None = None,
    add_to_registry: bool = False,
) -> type[Functor]:
    """Returns a functor class from a function.

    Args:
      func: Function to be wrapped into a functor.
      args: Symbolic args specification. `args` is a list of tuples, each
        describes an argument from the input function. Each tuple is the format of
        (<argumment-name>, <value-spec>, [description], [metadata-objects]).
        `argument-name` - a `str` or `pg_typing.StrKey` object. When
        `pg_typing.StrKey` is used, it describes the wildcard keyword argument.
        `value-spec` - a `pg_typing.ValueSpec` object or equivalent, e.g.
        primitive values which will be converted to ValueSpec implementation
        according to its type and used as its default value. `description` - a
        string to describe the agument. `metadata-objects` - an optional list of
        any type, which can be used to generate code according to the schema.
        There are notable rules in filling the `args`: 1) When `args` is None or
        arguments from the function signature are missing from it, `schema.Field`
        for these fields will be automatically generated and inserted into `args`.
        That being said, every arguments in input function will have a
        `schema.Field` counterpart in `Functor.__schema__.fields` sorted by the
        declaration order of each argument in the function signature ( other than
        the order in `args`). 2) Default argument values are specified along with
        function definition as regular python functions, instead of being set at
        `schema.Field` level. But validation rules can be set using `args` and
        apply to argument values.
        For example::

          @pg.functor([('c', pg.typing.Int(min_value=0), 'Arg c')])
            def foo(a, b, c=1, **kwargs):
              return a + b + c + sum(kwargs.values())

            assert foo.schema.fields() == [
                pg.typing.Field('a', pg.Any(), 'Argument a'.),
                pg.typing.Field('b', pg.Any(), 'Argument b'.),
                pg.typing.Field('c', pg.typing.Int(), 'Arg c.),
                pg.typing.Filed(
                    pg.typing.StrKey(), pg.Any(), 'Other arguments.')
            ]
            # Prebind a=1, b=2, with default value c=1.
            assert foo(1, 2)() == 4
      returns: Optional schema specification for the return value.
      base_class: Optional base class (derived from `symbolic.Functor`). If None,
        returned type will inherit from `Functor` directly.
      auto_doc: If True, the descriptions of argument fields will be inherited
        from funciton docstr if they are not explicitly specified through
        ``args``.
      auto_typing: If True, the value spec for constraining each argument will be
        inferred from its annotation. Otherwise the value specs for all arguments
        will be ``pg.typing.Any()``.
      serialization_key: An optional string to be used as the serialization key
        for the class during `sym_jsonify`. If None, `cls.__type_name__` will be
        used. This is introduced for scenarios when we want to relocate a class,
        before the downstream can recognize the new location, we need the class to
        serialize it using previous key.
      additional_keys: An optional list of strings as additional keys to
        deserialize an object of the registered class. This can be useful when we
        need to relocate or rename the registered class while being able to load
        existing serialized JSON values.
      add_to_registry: If True, the newly created functor class will be added to
        the registry for deserialization.

    Returns:
      `symbolic.Functor` subclass that wraps input function.

    Raises:
      KeyError: names of symbolic arguments are not compatible with
        function signature.
      TypeError: types of symbolic arguments are not compatible with
        function signature.
      ValueError: default values of symbolic arguments are not compatible
        with  function signature.
    """
    if not inspect.isfunction(func):
        raise TypeError(f'{func!r} is not a function.')

    class _Functor(base_class or Functor, to_json_key=False):
        """Functor wrapper for input function.

        ``to_json_key=False`` skips auto-registration during
        ``__init_subclass__``: at that point `_Functor` still has its
        local qualname (``functor_class.<locals>._Functor``) and
        registering under that would leak a stale entry. The explicit
        call to ``cls.register_for_deserialization`` below registers
        under the renamed ``__module__``/``__qualname__`` derived from
        the wrapped function.
        """

        # The schema for function-based Functor will be inferred from the
        # function signature, so skip auto-schema generation at class
        # creation time. Annotation-based field inference stays enabled
        # (the default) because this functor is built from a function
        # rather than class-level attributes.
        __auto_schema__ = False

        def _call(self, *args, **kwargs):
            return func(*args, **kwargs)

    cls = typing.cast(type[Functor], _Functor)
    cls.__name__ = func.__name__
    cls.__qualname__ = func.__qualname__
    cls.__module__ = getattr(func, '__module__', 'wrapper')
    cls.__doc__ = func.__doc__

    # Apply function schema.
    cls.apply_schema(
        pg_typing.schema(
            func, args, returns, auto_doc=auto_doc, auto_typing=auto_typing
        )
    )

    # Register functor class for deserialization if needed.
    if add_to_registry:
        cls.register_for_deserialization(serialization_key, additional_keys)
    return cls

default_load_handler

default_load_handler(
    path: str, file_format: str | WireFormat = "json", **kwargs: Any
) -> Any

Default load handler from file.

Parameters:

Name Type Description Default
path str

File path to read.

required
file_format str | WireFormat

The literal 'txt' (read raw text), or any registered wire format -- a name (e.g. 'json', 'yaml') or a pg.WireFormat instance.

'json'
**kwargs Any

Forwarded to pg.from_json_str for wire formats.

{}
Source code in pygx/symbolic/_io.py
def default_load_handler(
    path: str,
    file_format: str | wire.WireFormat = 'json',
    **kwargs: Any,
) -> Any:
    """Default load handler from file.

    Args:
      path: File path to read.
      file_format: The literal ``'txt'`` (read raw text), or any registered
        wire format -- a name (e.g. ``'json'``, ``'yaml'``) or a
        ``pg.WireFormat`` instance.
      **kwargs: Forwarded to ``pg.from_json_str`` for wire formats.
    """
    content = pg_io.readfile(path)
    assert content is not None
    if file_format == 'txt':
        return content
    return from_json_str(
        content,
        format=_resolve_file_format(file_format),
        allow_partial=True,
        **kwargs,
    )

default_save_handler

default_save_handler(
    value: Any,
    path: str,
    *,
    indent: int | None = None,
    file_format: str | WireFormat = "json",
    **kwargs: Any
) -> None

Default save handler to file.

Parameters:

Name Type Description Default
value Any

Value to save.

required
path str

File path to write.

required
indent int | None

Indentation passed to the format's encoder.

None
file_format str | WireFormat

The literal 'txt' (write the value's text form), or any registered wire format -- a name (e.g. 'json', 'yaml') or a pg.WireFormat instance.

'json'
**kwargs Any

Forwarded to pg.to_json_str for wire formats.

{}
Source code in pygx/symbolic/_io.py
def default_save_handler(
    value: Any,
    path: str,
    *,
    indent: int | None = None,
    file_format: str | wire.WireFormat = 'json',
    **kwargs: Any,
) -> None:
    """Default save handler to file.

    Args:
      value: Value to save.
      path: File path to write.
      indent: Indentation passed to the format's encoder.
      file_format: The literal ``'txt'`` (write the value's text form), or any
        registered wire format -- a name (e.g. ``'json'``, ``'yaml'``) or a
        ``pg.WireFormat`` instance.
      **kwargs: Forwarded to ``pg.to_json_str`` for wire formats.
    """
    if file_format == 'txt':
        content = (
            value
            if isinstance(value, str)
            else formatting.format(value, compact=False, verbose=True)
        )
    else:
        content = to_json_str(
            value,
            json_indent=indent,
            format=_resolve_file_format(file_format),
            **kwargs,
        )

    pg_io.mkdirs(os.path.dirname(path), exist_ok=True)
    pg_io.writefile(path, content)

load

load(path: str, *args: Any, expected_type: type[T], **kwargs: Any) -> T
load(path: str, *args: Any, expected_type: Any = ..., **kwargs: Any) -> Any
load(path: str, *args: Any, expected_type: Any = Any, **kwargs: Any) -> Any

Load a symbolic value using the global load handler.

Example::

class A(pg.Object):
  x: Any

a1 = A(1)
file = 'my_file.json'
a1.save(file)
a2 = pg.load(file)
assert pg.eq(a1, a2)

# Narrow the static return type and assert at runtime:
a3 = pg.load(file, expected_type=A)

Parameters:

Name Type Description Default
path str

A path string for loading an object.

required
*args Any

Positional arguments that will be passed through to the global load handler.

()
expected_type Any

When provided (anything other than the default Any), the loaded value must be an instance of this type — pyright narrows the return to it, and a TypeError is raised at runtime if the actual value does not match. Defaults to Any (no check).

Any
**kwargs Any

Keyword arguments that will be passed through to the global load handler.

{}

Returns:

Type Description
Any

Return value from the global load handler.

Raises:

Type Description
TypeError

If expected_type is provided and the loaded value is not an instance of it.

Source code in pygx/symbolic/_io.py
def load(path: str, *args: Any, expected_type: Any = Any, **kwargs: Any) -> Any:
    """Load a symbolic value using the global load handler.

    Example::

        class A(pg.Object):
          x: Any

        a1 = A(1)
        file = 'my_file.json'
        a1.save(file)
        a2 = pg.load(file)
        assert pg.eq(a1, a2)

        # Narrow the static return type and assert at runtime:
        a3 = pg.load(file, expected_type=A)

    Args:
      path: A path string for loading an object.
      *args: Positional arguments that will be passed through to the global
        load handler.
      expected_type: When provided (anything other than the default ``Any``),
        the loaded value must be an instance of this type — pyright narrows
        the return to it, and a ``TypeError`` is raised at runtime if the
        actual value does not match. Defaults to ``Any`` (no check).
      **kwargs: Keyword arguments that will be passed through to the global
        load handler.

    Returns:
      Return value from the global load handler.

    Raises:
      TypeError: If ``expected_type`` is provided and the loaded value is not
        an instance of it.
    """
    load_handler = flags.get_load_handler() or default_load_handler
    value = load_handler(path, *args, **kwargs)
    if flags.is_tracking_origin() and isinstance(value, Symbolic):
        value.sym_setorigin(path, 'load')
    return _enforce_expected_type(value, expected_type)

open_jsonl

open_jsonl(path: str, mode: str = 'r', **kwargs: Any) -> Sequence

Open a JSONL file for reading or writing.

Example::

with pg.open_jsonl('my_file.jsonl', 'w') as f: f.add(1) f.add('foo') f.add(dict(x=1))

with pg.open_jsonl('my_file.jsonl', 'r') as f: for value in f: print(value)

Parameters:

Name Type Description Default
path str

The path to the file.

required
mode str

The mode of the file.

'r'
**kwargs Any

Additional keyword arguments that will be passed to pg_io.open_sequence.

{}

Returns:

Type Description
Sequence

A sequence for PyGX objects.

Source code in pygx/symbolic/_io.py
def open_jsonl(path: str, mode: str = 'r', **kwargs: Any) -> pg_io.Sequence:
    """Open a JSONL file for reading or writing.

    Example::

      with pg.open_jsonl('my_file.jsonl', 'w') as f:
        f.add(1)
        f.add('foo')
        f.add(dict(x=1))

      with pg.open_jsonl('my_file.jsonl', 'r') as f:
        for value in f:
          print(value)

    Args:
      path: The path to the file.
      mode: The mode of the file.
      **kwargs: Additional keyword arguments that will be passed to
        ``pg_io.open_sequence``.

    Returns:
      A sequence for PyGX objects.
    """
    return pg_io.open_sequence(
        path, mode, serializer=to_json_str, deserializer=from_json_str, **kwargs
    )

save

save(value: Any, path: str, *args: Any, **kwargs: Any) -> Any

Save a symbolic value using the global save handler.

Example::

class A(pg.Object):
  x: Any

a1 = A(1)
file = 'my_file.json'
a1.save(file)
a2 = pg.load(file)
assert pg.eq(a1, a2)

Parameters:

Name Type Description Default
value Any

value to save.

required
path str

A path string for saving value.

required
*args Any

Positional arguments that will be passed through to the global save handler.

()
**kwargs Any

Keyword arguments that will be passed through to the global save handler.

{}

Returns:

Type Description
Any

Return value from the global save handler.

Raises:

Type Description
RuntimeError

if global save handler is not set.

Source code in pygx/symbolic/_io.py
def save(value: Any, path: str, *args: Any, **kwargs: Any) -> Any:
    """Save a symbolic value using the global save handler.

    Example::

        class A(pg.Object):
          x: Any

        a1 = A(1)
        file = 'my_file.json'
        a1.save(file)
        a2 = pg.load(file)
        assert pg.eq(a1, a2)

    Args:
      value: value to save.
      path: A path string for saving `value`.
      *args: Positional arguments that will be passed through to the global
        save handler.
      **kwargs: Keyword arguments that will be passed through to the global
        save handler.

    Returns:
      Return value from the global save handler.

    Raises:
      RuntimeError: if global save handler is not set.
    """
    save_handler = flags.get_save_handler() or default_save_handler
    return save_handler(value, path, *args, **kwargs)

mark_as_insertion

mark_as_insertion(value: Any) -> Insertion

Mark a value as an insertion to a List.

Source code in pygx/symbolic/_list.py
def mark_as_insertion(value: Any) -> Insertion:
    """Mark a value as an insertion to a List."""
    return Insertion(value=value)

members

members(
    fields: list[Field | FieldDef] | dict[FieldKeyDef, FieldValueDef],
    metadata: dict[str, Any] | None = None,
    serialization_key: str | None = None,
    additional_keys: list[str] | None = None,
    add_to_registry: bool = True,
) -> Decorator

Function/Decorator for declaring symbolic fields for pg.Object.

Example::

@pg.members([ # Declare symbolic fields. Each field produces a symbolic attribute # for its object, which can be accessed by self.<field_name>. # Description is optional. ('x', pg.typing.Int(min_value=0, default=0), 'Description for x.'), ('y', pg.typing.Str(), 'Description for y.') ]) class A(pg.Object): def sum(self): return self.x + self.y

@pg.members([ # Override field 'x' inherited from class A and make it more restrictive. ('x', pg.typing.Int(max_value=10, default=5)), # Add field 'z'. ('z', pg.typing.Bool().noneable()) ]) class B(A): pass

@pg.members([ # Declare dynamic fields: any keyword can be acceptable during __init__ # and can be accessed using self.<field_name>. (pg.typing.StrKey(), pg.typing.Int()) ]) class D(B): pass

@pg.members([ # Declare dynamic fields: keywords started with 'foo' is acceptable. (pg.typing.StrKey('foo.*'), pg.typing.Int()) ]) class E(pg.Object): pass

See pygx.typing.ValueSpec for supported value specifications.

Parameters:

Name Type Description Default
fields list[Field | FieldDef] | dict[FieldKeyDef, FieldValueDef]

A list of pg.typing.Field or equivalent tuple representation as (, , [description], [metadata-objects]). key should be a string. value-spec should be pg_typing.ValueSpec classes or equivalent, e.g. primitive values which will be converted to ValueSpec implementation according to its type and used as its default value. description is optional only when field overrides a field from its parent class. metadata-objects is an optional list of any type, which can be used to generate code according to the schema.

required
metadata dict[str, Any] | None

Optional dict of user objects as class-level metadata which will be attached to class schema.

None
serialization_key str | None

An optional string to be used as the serialization key for the class during sym_jsonify. If None, cls.__type_name__ will be used. This is introduced for scenarios when we want to relocate a class, before the downstream can recognize the new location, we need the class to serialize it using previous key.

None
additional_keys list[str] | None

An optional list of strings as additional keys to deserialize an object of the registered class. This can be useful when we need to relocate or rename the registered class while being able to load existing serialized JSON values.

None
add_to_registry bool

If True, register serialization keys and additional keys with the class.

True

Returns:

Type Description
Decorator

a decorator function that register the class or function with schema created from the fields.

Raises:

Type Description
TypeError

Decorator cannot be applied on target class or keyword argument provided is not supported.

KeyError

If type has already been registered in the registry.

ValueError

schema cannot be created from fields.

Source code in pygx/symbolic/_object.py
def members(
    fields: (
        list[pg_typing.Field | pg_typing.FieldDef]
        | dict[pg_typing.FieldKeyDef, pg_typing.FieldValueDef]
    ),
    metadata: dict[str, Any] | None = None,
    serialization_key: str | None = None,
    additional_keys: list[str] | None = None,
    add_to_registry: bool = True,
) -> pg_typing.Decorator:
    """Function/Decorator for declaring symbolic fields for ``pg.Object``.

    Example::

      @pg.members([
        # Declare symbolic fields. Each field produces a symbolic attribute
        # for its object, which can be accessed by `self.<field_name>`.
        # Description is optional.
        ('x', pg.typing.Int(min_value=0, default=0), 'Description for `x`.'),
        ('y', pg.typing.Str(), 'Description for `y`.')
      ])
      class A(pg.Object):
        def sum(self):
          return self.x + self.y

      @pg.members([
        # Override field 'x' inherited from class A and make it more restrictive.
        ('x', pg.typing.Int(max_value=10, default=5)),
        # Add field 'z'.
        ('z', pg.typing.Bool().noneable())
      ])
      class B(A):
        pass

      @pg.members([
        # Declare dynamic fields: any keyword can be acceptable during `__init__`
        # and can be accessed using `self.<field_name>`.
        (pg.typing.StrKey(), pg.typing.Int())
      ])
      class D(B):
        pass

      @pg.members([
        # Declare dynamic fields: keywords started with 'foo' is acceptable.
        (pg.typing.StrKey('foo.*'), pg.typing.Int())
      ])
      class E(pg.Object):
        pass

    See [`pygx.typing.ValueSpec`][pygx.typing.ValueSpec] for supported value specifications.

    Args:
      fields: A list of pg.typing.Field or equivalent tuple representation as
        (<key>, <value-spec>, [description], [metadata-objects]). `key` should be
        a string. `value-spec` should be pg_typing.ValueSpec classes or
        equivalent, e.g. primitive values which will be converted to ValueSpec
        implementation according to its type and used as its default value.
        `description` is optional only when field overrides a field from its
        parent class. `metadata-objects` is an optional list of any type, which
        can be used to generate code according to the schema.
      metadata: Optional dict of user objects as class-level metadata which will
        be attached to class schema.
      serialization_key: An optional string to be used as the
        serialization key for the class during `sym_jsonify`. If None,
        `cls.__type_name__` will be used. This is introduced for scenarios when we
        want to relocate a class, before the downstream can recognize the new
        location, we need the class to serialize it using previous key.
      additional_keys: An optional list of strings as additional keys to
        deserialize an object of the registered class. This can be useful when we
        need to relocate or rename the registered class while being able to load
        existing serialized JSON values.
      add_to_registry: If True, register serialization keys and additional keys
        with the class.

    Returns:
      a decorator function that register the class or function with schema
        created from the fields.

    Raises:
      TypeError: Decorator cannot be applied on target class or keyword argument
        provided is not supported.
      KeyError: If type has already been registered in the registry.
      ValueError: schema cannot be created from fields.
    """

    def _decorator(cls):
        """Decorator function that registers schema with an Object class."""
        cls.update_schema(
            fields,
            extend=True,
            metadata=metadata,
        )
        if add_to_registry:
            cls.register_for_deserialization(serialization_key, additional_keys)
        return cls

    return typing.cast(pg_typing.Decorator, _decorator)

deref

deref(value: Symbolic, recursive: bool = False) -> Any

Dereferences a symbolic value that may contain pg.Ref.

Parameters:

Name Type Description Default
value Symbolic

The input symbolic value.

required
recursive bool

If True, dereference pg.Ref in the entire tree. Otherwise Only dereference the root node.

False

Returns:

Type Description
Any

The dereferenced root, or dereferenced tree if recursive is True.

Source code in pygx/symbolic/_ref.py
def deref(value: base.Symbolic, recursive: bool = False) -> Any:
    """Dereferences a symbolic value that may contain pg.Ref.

    Args:
      value: The input symbolic value.
      recursive: If True, dereference `pg.Ref` in the entire tree. Otherwise
        Only dereference the root node.

    Returns:
      The dereferenced root, or dereferenced tree if recursive is True.
    """
    if isinstance(value, Ref):
        value = value.value

    if recursive:

        def _deref(k, v, p):
            del k, p
            if isinstance(v, Ref):
                return deref(v.value, recursive=True)
            return v

        return value.sym_rebind(_deref, raise_on_no_change=False)
    return value

maybe_ref

maybe_ref(value: Any) -> Any

Returns a reference if a value is not symbolic or already has a parent.

Source code in pygx/symbolic/_ref.py
def maybe_ref(value: Any) -> Any:
    """Returns a reference if a value is not symbolic or already has a parent."""
    if isinstance(value, base.Symbolic):
        if value.sym_parent is None:
            return value
    return Ref(value)

symbolize

symbolize(*args: Any, **kwargs: Any) -> Any

Make a symbolic class/function out of a regular Python class/function.

pg.symbolize is introduced for the purpose of making existing classes/functions symbolically programmable. For use cases that build symbolic classes from scratch (native PyGX classes), extending pg.Object with dataclass-like class annotations that declare the symbolic properties is the recommended way, which automatically generates the __init__ method and allow symbolic attributes to be accessed via self.<member>.

pg.symbolize can be invoked as a class/function decorator, or as a function. When it is used as a decorator, the decorated class or function will be converted to a symbolic type (via pygx.wrap and pygx.functor_class). This is preferred when user can modify the files of existing classes/functions. For example::

@pg.symbolize def foo(a, b): return a + b

f = foo(1, 2) f.sym_rebind(a=2) f() # Returns 4

@pg.symbolize([ # (Optional) add symbolic constraint for init argument 'a'. ('a', pg.typing.Int(min_value=0), 'Description for a.') ]) class Foo: def init(self, a, b): self._a = a self._b = b

def result(self):
  return self._a + self._b

f = Foo(1, 2) f.sym_rebind(a=2, b=3) f.result() # Returns 5

When it used as a function, the input class or function will not be modified. Instead, a new symbolic type will be created and returned. This is helpful when users want to create new symbolic types from existing classes/functions without modifying their original source code. For example::

def foo(a, b): return a + b

# Create a new symbolic type with constraint on 'a'. symbolic_foo = pg.symbolize(foo, [ ('a', pg.typing.Int(min_value=0)) ], returns=pg.typing.Int()) foo(1, 2) # Returns 3 (foo is kept intact).

f = symbolic_foo(1, 2) f.sym_rebind(a=2) f() # Returns 4.

class Foo: def init(self, a, b): self._a = a self._b = b

def result(self):
  return self._a + self._b

SymbolicFoo = pg.symbolize(Foo) f = SymbolicFoo(2, 2) f.sym_rebind(a=3) f.result() # Returns 5.

Parameters:

Name Type Description Default
*args Any

The positional arguments for symbolize are:

  • class_or_fn: applicable when symbolize is called in function mode.
  • constraints: an optional list of tuples that allows users to specify the constraints for arguments from the __init__ method (for class) or the arguments from the function signature (for function). Each tuple should be in format:

    (<arg_name>, <value_spec>, [description], [arg_metadata])

Where arg_name is an argument name that is acceptable to the __init__ method of the class, or the function signature; 'value_spec' is a pg.ValueSpec object that validates the value of the argument. description and arg_metadata are optional, for documentation and meta-programming purposes.

()
**kwargs Any

Keyword arguments will be passsed through to pygx.wrap (for symbolizing classes) and pygx.functor_class (for symbolizing functions).

{}

Returns:

Type Description
Any

A Symbolic subclass for the decorated/input type.

Raises:

Type Description
TypeError

input type cannot be symbolized, or it's not a type.

Source code in pygx/symbolic/_symbolize.py
def symbolize(*args: Any, **kwargs: Any) -> Any:
    """Make a symbolic class/function out of a regular Python class/function.

    ``pg.symbolize`` is introduced for the purpose of making existing
    classes/functions symbolically programmable. For use cases that build
    symbolic classes from scratch (native PyGX classes), extending `pg.Object`
    with dataclass-like class annotations that declare the symbolic properties
    is the recommended way, which automatically generates the ``__init__``
    method and allow symbolic attributes to be accessed via `self.<member>`.

    ``pg.symbolize`` can be invoked as a class/function decorator, or as a
    function. When it is used as a decorator, the decorated class or function
    will be converted to a symbolic type (via [`pygx.wrap`][pygx.symbolic.wrap] and
    [`pygx.functor_class`][pygx.symbolic.functor_class]). This is preferred when user can modify the
    files of existing classes/functions. For example::

      @pg.symbolize
      def foo(a, b):
        return a + b

      f = foo(1, 2)
      f.sym_rebind(a=2)
      f()           # Returns 4

      @pg.symbolize([
        # (Optional) add symbolic constraint for __init__ argument 'a'.
        ('a', pg.typing.Int(min_value=0), 'Description for `a`.')
      ])
      class Foo:
        def __init__(self, a, b):
          self._a = a
          self._b = b

        def result(self):
          return self._a + self._b

      f = Foo(1, 2)
      f.sym_rebind(a=2, b=3)
      f.result()   # Returns 5

    When it used as a function, the input class or function will not be modified.
    Instead, a new symbolic type will be created and returned. This is helpful
    when users want to create new symbolic types from existing classes/functions
    without modifying their original source code. For example::

      def foo(a, b):
        return a + b

      # Create a new symbolic type with constraint on 'a'.
      symbolic_foo = pg.symbolize(foo, [
          ('a', pg.typing.Int(min_value=0))
      ], returns=pg.typing.Int())
      foo(1, 2)    # Returns 3 (foo is kept intact).

      f = symbolic_foo(1, 2)
      f.sym_rebind(a=2)
      f()          # Returns 4.

      class Foo:
        def __init__(self, a, b):
          self._a = a
          self._b = b

        def result(self):
          return self._a + self._b

      SymbolicFoo = pg.symbolize(Foo)
      f = SymbolicFoo(2, 2)
      f.sym_rebind(a=3)
      f.result()   # Returns 5.

    Args:
      *args:  The positional arguments for `symbolize` are:

        * `class_or_fn`: applicable when `symbolize` is called in function mode.
        * `constraints`: an optional list of tuples that allows users to specify
          the constraints for arguments from the `__init__` method (for class)
          or the arguments from the function signature (for function).
          Each tuple should be in format:

             `(<arg_name>, <value_spec>, [description], [arg_metadata])`

          Where `arg_name` is an argument name that is acceptable to the
          `__init__` method of the class, or the function signature;
          'value_spec' is a `pg.ValueSpec` object that validates the value of
          the argument.
          `description` and `arg_metadata` are optional, for documentation and
          meta-programming purposes.
      **kwargs: Keyword arguments will be passsed through to [`pygx.wrap`][pygx.symbolic.wrap]
        (for symbolizing classes) and [`pygx.functor_class`][pygx.symbolic.functor_class] (for
        symbolizing functions).

    Returns:
      A Symbolic subclass for the decorated/input type.

    Raises:
      TypeError: input type cannot be symbolized, or it's not a type.
    """
    cls_or_fn = None
    if args:
        if inspect.isclass(args[0]) or inspect.isfunction(
            args[0]
        ):  # pytype: disable=not-supported-yet
            cls_or_fn = args[0]
            if cls_or_fn is dict or cls_or_fn is list:
                if len(args) != 1 or kwargs:
                    raise ValueError(
                        f'Constraints are not supported in symbolic {cls_or_fn!r}. '
                        f'Encountered: constraints={args[1]!r}.'  # pyright: ignore[reportGeneralTypeIssues]
                    )
                return pg_dict.Dict if cls_or_fn is dict else pg_list.List
            args = args[1:]
            if len(args) > 1:
                raise ValueError(
                    f'Only `constraint` is supported as positional arguments. '
                    f'Encountered {args!r}.'
                )
        elif not isinstance(args[0], list):
            raise TypeError(f'{args[0]!r} cannot be symbolized.')

    def _symbolize(cls_or_fn):
        if inspect.isclass(cls_or_fn):
            if issubclass(cls_or_fn, base.Symbolic) and not issubclass(
                cls_or_fn, class_wrapper.ClassWrapper
            ):
                raise ValueError(
                    f'Cannot symbolize {cls_or_fn!r}: {cls_or_fn.__name__} is already '
                    f'a dataclass-like symbolic class derived from `pg.Object`. '
                    f'Consider to use `pg.members` to add new symbolic attributes.'
                )
            return class_wrapper.wrap(cls_or_fn, *args, **kwargs)
        assert inspect.isfunction(cls_or_fn), (
            f'Unexpected: {cls_or_fn!r} should be a class or function.'
        )
        return functor_class(cls_or_fn, add_to_registry=True, *args, **kwargs)

    if cls_or_fn is not None:
        # When `cls_or_fn` is provided, `symbolize` is called under function mode
        # such as `SymbolicFoo = pg.symbolize(Foo)` or being used as a decorator
        # with no arguments, e.g:
        # ```
        #   @symbolize
        #   class Foo:
        #     pass
        # ```
        # In both case, we return the symbolic type of `cls_or_fn`.
        return _symbolize(cls_or_fn)
    else:
        # Otherwise a factory method is returned to create the symbolic type from
        # a late-bound `cls_or_fn` input, which is the case when `symbolize` is used
        # as a decorator with provided arguments.
        return _symbolize

from_uri

from_uri(
    uri: str,
    class_resolver: ClassSource[T],
    *,
    case_sensitive: bool = True,
    allow_scheme_names: bool = False
) -> T

Build a symbolic object from a URI-like string.

Two equivalent top-level forms are accepted:

  • URI form: <ClassName>?<arg1>=<v1>&<arg2>=<v2>
  • Call form: <ClassName>(<arg1>=<v1>, <arg2>=<v2>)

Use whichever reads better at the call site. The forms can also be mixed: nested values inside a URI-form call always use call form, since & is the URI's arg separator (see Special characters below).

Positional arguments are accepted (mapped to the class's init_arg_list) and may appear before any keyword arguments.

Suppose we have::

class Optimizer(pg.Object):
    lr: float
    momentum: float = 0.0

class Schedule(pg.Object):
    name: str
    steps: list[int] = []

class Trainer(pg.Object):
    optimizer: Optimizer
    epochs: int = 10
    schedules: list[Schedule] = []
    tags: dict[str, str] = {}
    notes: str | None = None

The class_resolver argument can be a dict, a list, or a callable::

# 1. Mapping: explicit name -> class lookup.
registry = {
    'Optimizer': Optimizer,
    'Schedule': Schedule,
    'Trainer': Trainer,
}

# 2. Sequence: each class is keyed by ``cls.__name__``.
registry = [Optimizer, Schedule, Trainer]

# 3. Callable: resolve a name on demand (e.g. import lazily).
def registry(name):
    return {'Optimizer': Optimizer, 'Trainer': Trainer}.get(name)

All three are interchangeable in the examples below.

Primitives — ints, floats, bools (True/False, yes/no), None, and strings (quoted or unquoted)::

pg.from_uri('Optimizer?lr=0.1', registry)
# Optimizer(lr=0.1, momentum=0.0)

pg.from_uri('Optimizer?lr=0.1&momentum=0.9', registry)
# Optimizer(lr=0.1, momentum=0.9)

pg.from_uri('Schedule?name=cosine', registry)
pg.from_uri('Schedule?name="cosine annealing"', registry)
# Schedule(name='cosine annealing', steps=[])

Positional arguments map through the class's init_arg_list::

pg.from_uri('Optimizer?0.1&0.9', registry)
pg.from_uri('Optimizer(0.1, 0.9)', registry)
# Optimizer(lr=0.1, momentum=0.9)

Lists use [...]; dicts use {...} with : or =::

pg.from_uri('Schedule?name=warmup&steps=[1, 2, 3]', registry)
# Schedule(name='warmup', steps=[1, 2, 3])

pg.from_uri('Trainer?optimizer=Optimizer(lr=0.1)&'
            'tags={env: "prod", team: "ml"}', registry)
# Trainer(optimizer=Optimizer(lr=0.1), tags={'env': 'prod', 'team': 'ml'})

Nested objects are spelled with the call form anywhere a value is expected; arbitrary depth is supported::

pg.from_uri(
    'Trainer?epochs=5&optimizer=Optimizer(lr=0.1, momentum=0.9)&'
    'schedules=[Schedule(name="warmup", steps=[1, 2]),'
    ' Schedule(name="cosine", steps=[10, 20])]',
    registry,
)

Equivalently, in call form::

pg.from_uri(
    'Trainer(epochs=5,'
    ' optimizer=Optimizer(lr=0.1, momentum=0.9),'
    ' schedules=[Schedule(name="warmup", steps=[1, 2])])',
    registry,
)

None and noneable fields::

pg.from_uri('Trainer?optimizer=Optimizer(lr=0.1)&notes=None',
            registry)
# Trainer(..., notes=None)

Case-insensitive matching for both class and field names::

pg.from_uri('TRAINER?optimizer=OPTIMIZER(LR=0.1, MoMentum=0.9)',
            registry, case_sensitive=False)
# Trainer(optimizer=Optimizer(lr=0.1, momentum=0.9))

Special characters in string values:

  • Inside double or single quotes, content is preserved verbatim — spaces, real newlines, parens, commas, and & are all fine::

    pg.from_uri('Schedule(name="my favorite schedule")', registry)

    pg.from_uri('Schedule(name="line1\nline2")', registry)

    Note: \n here is a real newline in the Python source. The

    parser does NOT interpret escape sequences — only \" (an

    escaped quote) and \\ (an escaped backslash) are useful.

    To embed a newline, pass a real newline character.

  • Unquoted top-level URI-form values may contain spaces (the URI form splits only on &)::

    pg.from_uri('Schedule?name=cosine annealing', registry)

    Schedule(name='cosine annealing')

However, inside any structured context ([...], {...}, Name(...)), whitespace is a token separator — quote any value that contains spaces.

  • & is the URI form's arg separator and is not quote-aware. If a value contains &, use the call form (or wrap the value in [...] / Name(...) so the surrounding context isn't URI-style)::

    Doesn't work — URI form splits on & before quotes are seen:

    pg.from_uri('Note?text="a&b"', registry) # ValueError

    Works — call form splits on ,:

    pg.from_uri('Note(text="a&b")', registry)

Scheme-style names for registry-style factories — pass allow_scheme_names=True and a callable resolver::

def resolve(model_id):
    factory = lookup(model_id)  # exact + regex registry, etc.
    def instantiate(**kw):
        return factory(model=model_id, **kw)
    return instantiate

pg.from_uri('mock://test?temperature=0.1', resolve,
            allow_scheme_names=True)
pg.from_uri('claude-opus-4-6@latest(temperature=0.1)', resolve,
            allow_scheme_names=True)

The leading name extends to the first ? or ( and is passed verbatim to the resolver. When the resolver returns a plain callable (no __schema__), value-spec validation is skipped — kwargs are forwarded with best-effort coercion so the callable can accept **kwargs for fields it routes itself.

Note

pg.patching.from_uri is a different function that resolves a URI to a registered pygx.patching.Patcher; this function builds a symbolic object instead.

Parameters:

Name Type Description Default
uri str

URI-like string. Either Name?args (URI form) or Name(args) (Python-call form).

required
class_resolver ClassSource[T]

Source used to resolve both the top-level name and any nested object calls. The return type T is inferred from this argument — from_uri(...) returns the element type of the resolver, not the generic Object base. One of:

  • Mapping[str, type[T]] — explicit name -> class table.
  • Sequence[type[T]] — list of classes; each is keyed by cls.__name__.
  • Callable[[str], type[T] | Callable[..., T] | None] — lookup function. The callable may return None or raise KeyError / LookupError to signal "unknown class". The factory-callable shape is used by allow_scheme_names registries that wrap the target class in a closure (see Scheme-style names above).
required
case_sensitive bool

When False, both class-name lookups and kwarg-to-field resolution on the resolved class (and any nested classes) are matched case-insensitively. Defaults to True for strict matching. Ignored for class lookups when class_resolver is a callable (the callable owns its own lookup policy).

True
allow_scheme_names bool

When True, the leading name may contain characters that are not valid in a Python identifier — anything except ? and (. This unlocks scheme-style identifiers like mock://test, claude-opus-4-6@latest, or groq://gemma-7b, which are typically resolved by a callable class_resolver acting as a registry. The name is passed verbatim to the resolver.

The resolver may return either a type (with or without a symbolic __schema__) or any plain callable. When the resolved target has no __schema__, positional arguments are forwarded as *args and keyword arguments are forwarded verbatim with best-effort primitive coercion — no value-spec validation.

Only the top-level name is affected; nested Name(...) calls inside structured values still use Python-identifier names.

False

Returns:

Type Description
T

A new instance of T (the element type of class_resolver)

T

constructed from the parsed arguments.

Raises:

Type Description
KeyError

If the top-level class name is not resolvable via the resolver.

ValueError

If the URI cannot be parsed or violates the class schema.

Source code in pygx/symbolic/_uri.py
def from_uri[T](  # pylint: disable=redefined-outer-name
    uri: str,
    class_resolver: ClassSource[T],
    *,
    case_sensitive: bool = True,
    allow_scheme_names: bool = False,
) -> T:
    """Build a symbolic object from a URI-like string.

    Two equivalent top-level forms are accepted:

    * URI form: ``<ClassName>?<arg1>=<v1>&<arg2>=<v2>``
    * Call form: ``<ClassName>(<arg1>=<v1>, <arg2>=<v2>)``

    Use whichever reads better at the call site. The forms can also be
    mixed: nested values inside a URI-form call always use call form, since
    ``&`` is the URI's arg separator (see *Special characters* below).

    Positional arguments are accepted (mapped to the class's
    ``init_arg_list``) and may appear before any keyword arguments.

    Suppose we have::

        class Optimizer(pg.Object):
            lr: float
            momentum: float = 0.0

        class Schedule(pg.Object):
            name: str
            steps: list[int] = []

        class Trainer(pg.Object):
            optimizer: Optimizer
            epochs: int = 10
            schedules: list[Schedule] = []
            tags: dict[str, str] = {}
            notes: str | None = None

    The ``class_resolver`` argument can be a dict, a list, or a callable::

        # 1. Mapping: explicit name -> class lookup.
        registry = {
            'Optimizer': Optimizer,
            'Schedule': Schedule,
            'Trainer': Trainer,
        }

        # 2. Sequence: each class is keyed by ``cls.__name__``.
        registry = [Optimizer, Schedule, Trainer]

        # 3. Callable: resolve a name on demand (e.g. import lazily).
        def registry(name):
            return {'Optimizer': Optimizer, 'Trainer': Trainer}.get(name)

    All three are interchangeable in the examples below.

    **Primitives** — ints, floats, bools (``True``/``False``, ``yes``/``no``),
    ``None``, and strings (quoted or unquoted)::

        pg.from_uri('Optimizer?lr=0.1', registry)
        # Optimizer(lr=0.1, momentum=0.0)

        pg.from_uri('Optimizer?lr=0.1&momentum=0.9', registry)
        # Optimizer(lr=0.1, momentum=0.9)

        pg.from_uri('Schedule?name=cosine', registry)
        pg.from_uri('Schedule?name="cosine annealing"', registry)
        # Schedule(name='cosine annealing', steps=[])

    **Positional arguments** map through the class's ``init_arg_list``::

        pg.from_uri('Optimizer?0.1&0.9', registry)
        pg.from_uri('Optimizer(0.1, 0.9)', registry)
        # Optimizer(lr=0.1, momentum=0.9)

    **Lists** use ``[...]``; **dicts** use ``{...}`` with ``:`` or ``=``::

        pg.from_uri('Schedule?name=warmup&steps=[1, 2, 3]', registry)
        # Schedule(name='warmup', steps=[1, 2, 3])

        pg.from_uri('Trainer?optimizer=Optimizer(lr=0.1)&'
                    'tags={env: "prod", team: "ml"}', registry)
        # Trainer(optimizer=Optimizer(lr=0.1), tags={'env': 'prod', 'team': 'ml'})

    **Nested objects** are spelled with the call form anywhere a value is
    expected; arbitrary depth is supported::

        pg.from_uri(
            'Trainer?epochs=5&optimizer=Optimizer(lr=0.1, momentum=0.9)&'
            'schedules=[Schedule(name="warmup", steps=[1, 2]),'
            ' Schedule(name="cosine", steps=[10, 20])]',
            registry,
        )

    Equivalently, in call form::

        pg.from_uri(
            'Trainer(epochs=5,'
            ' optimizer=Optimizer(lr=0.1, momentum=0.9),'
            ' schedules=[Schedule(name="warmup", steps=[1, 2])])',
            registry,
        )

    **``None`` and noneable fields**::

        pg.from_uri('Trainer?optimizer=Optimizer(lr=0.1)&notes=None',
                    registry)
        # Trainer(..., notes=None)

    **Case-insensitive matching** for both class and field names::

        pg.from_uri('TRAINER?optimizer=OPTIMIZER(LR=0.1, MoMentum=0.9)',
                    registry, case_sensitive=False)
        # Trainer(optimizer=Optimizer(lr=0.1, momentum=0.9))

    **Special characters in string values:**

    * Inside double or single quotes, content is preserved verbatim —
      spaces, real newlines, parens, commas, and ``&`` are all fine::

        pg.from_uri('Schedule(name="my favorite schedule")', registry)

        pg.from_uri('Schedule(name="line1\\nline2")', registry)
        # Note: ``\\n`` here is a real newline in the Python source. The
        # parser does NOT interpret escape sequences — only ``\\"`` (an
        # escaped quote) and ``\\\\`` (an escaped backslash) are useful.
        # To embed a newline, pass a real newline character.

    * Unquoted top-level URI-form values may contain spaces (the URI form
      splits only on ``&``)::

        pg.from_uri('Schedule?name=cosine annealing', registry)
        # Schedule(name='cosine annealing')

      However, inside any structured context (``[...]``, ``{...}``,
      ``Name(...)``), whitespace is a token separator — quote any value
      that contains spaces.

    * ``&`` is the URI form's arg separator and is **not** quote-aware.
      If a value contains ``&``, use the call form (or wrap the value in
      ``[...]`` / ``Name(...)`` so the surrounding context isn't URI-style)::

        # Doesn't work — URI form splits on ``&`` before quotes are seen:
        pg.from_uri('Note?text="a&b"', registry)   # ValueError

        # Works — call form splits on ``,``:
        pg.from_uri('Note(text="a&b")', registry)

    **Scheme-style names** for registry-style factories — pass
    ``allow_scheme_names=True`` and a callable resolver::

        def resolve(model_id):
            factory = lookup(model_id)  # exact + regex registry, etc.
            def instantiate(**kw):
                return factory(model=model_id, **kw)
            return instantiate

        pg.from_uri('mock://test?temperature=0.1', resolve,
                    allow_scheme_names=True)
        pg.from_uri('claude-opus-4-6@latest(temperature=0.1)', resolve,
                    allow_scheme_names=True)

      The leading name extends to the first ``?`` or ``(`` and is passed
      verbatim to the resolver. When the resolver returns a plain callable
      (no ``__schema__``), value-spec validation is skipped — kwargs are
      forwarded with best-effort coercion so the callable can accept
      ``**kwargs`` for fields it routes itself.

    Note:
      ``pg.patching.from_uri`` is a different function that resolves a URI
      to a registered [`pygx.patching.Patcher`][pygx.patching.Patcher]; this
      function builds a symbolic object instead.

    Args:
      uri: URI-like string. Either ``Name?args`` (URI form) or ``Name(args)``
        (Python-call form).
      class_resolver: Source used to resolve both the top-level name and
        any nested object calls. The return type ``T`` is inferred from this
        argument — ``from_uri(...)`` returns the element type of the
        resolver, not the generic ``Object`` base. One of:

        * ``Mapping[str, type[T]]`` — explicit name -> class table.
        * ``Sequence[type[T]]`` — list of classes; each is keyed by
          ``cls.__name__``.
        * ``Callable[[str], type[T] | Callable[..., T] | None]`` — lookup
          function. The callable may return ``None`` or raise ``KeyError`` /
          ``LookupError`` to signal "unknown class". The factory-callable
          shape is used by ``allow_scheme_names`` registries that wrap the
          target class in a closure (see *Scheme-style names* above).

      case_sensitive: When ``False``, both class-name lookups and
        kwarg-to-field resolution on the resolved class (and any nested
        classes) are matched case-insensitively. Defaults to ``True`` for
        strict matching. Ignored for class lookups when ``class_resolver``
        is a callable (the callable owns its own lookup policy).
      allow_scheme_names: When ``True``, the leading name may contain
        characters that are not valid in a Python identifier — anything
        except ``?`` and ``(``. This unlocks scheme-style identifiers like
        ``mock://test``, ``claude-opus-4-6@latest``, or ``groq://gemma-7b``,
        which are typically resolved by a callable ``class_resolver``
        acting as a registry. The name is passed verbatim to the resolver.

        The resolver may return either a ``type`` (with or without a
        symbolic ``__schema__``) or any plain callable. When the resolved
        target has no ``__schema__``, positional arguments are forwarded
        as ``*args`` and keyword arguments are forwarded verbatim with
        best-effort primitive coercion — no value-spec validation.

        Only the *top-level* name is affected; nested ``Name(...)`` calls
        inside structured values still use Python-identifier names.

    Returns:
      A new instance of ``T`` (the element type of ``class_resolver``)
      constructed from the parsed arguments.

    Raises:
      KeyError: If the top-level class name is not resolvable via the
        resolver.
      ValueError: If the URI cannot be parsed or violates the class schema.
    """
    resolver = _as_resolver(class_resolver, case_sensitive)
    if allow_scheme_names:
        return _from_uri_scheme(uri, resolver)
    if _looks_like_call_form(uri):
        return _from_uri_call_form(uri, resolver)

    name, pos_args, kw_args = parse_uri(uri)
    cls = resolver.resolve(name)
    if cls is None:
        raise KeyError(f'Unknown class name {name!r}. {resolver.describe()}')
    args, kwargs = _from_uri_args(cls, pos_args, kw_args, resolver)
    return _from_uri_instantiate(cls, name, args, kwargs)

parse_uri_value

parse_uri_value(
    raw: str,
    value_spec: ValueSpec | None = None,
    class_resolver: ClassSource[Any] | _ClassResolver | None = None,
    *,
    source_kind: str = "Source",
    source_id: str = "",
    arg_name: str = "",
    case_sensitive: bool = True
) -> Any

Parse one value string into a typed Python value.

Parameters:

Name Type Description Default
raw str

The raw value string.

required
value_spec ValueSpec | None

If provided, the parsed value is validated/coerced through value_spec.apply. Also drives legacy behavior for plain tokens (e.g. unquoted strings, colon-separated lists).

None
class_resolver ClassSource[Any] | _ClassResolver | None

Optional source for resolving nested object calls ClassName(...). Accepts a Mapping[str, type], a Sequence[type] (each class keyed by its __name__), or a Callable[[str], type | None] lookup. None disables nested object construction.

None
source_kind str

Label used in error messages (e.g. 'Patcher', 'Class').

'Source'
source_id str

Identifier (patcher/class name) used in error messages.

''
arg_name str

Argument name used in error messages.

''
case_sensitive bool

When False, both class-name lookups and kwarg-to-field resolution on pg.Object subclasses are matched case-insensitively. Ignored for class lookups when class_resolver is a callable (the callable owns its own lookup policy).

True

Returns:

Type Description
Any

The parsed value (after optional value_spec.apply).

Raises:

Type Description
ValueError

If parsing fails or the value violates value_spec.

Source code in pygx/symbolic/_uri.py
def parse_uri_value(
    raw: str,
    value_spec: pg_typing.ValueSpec | None = None,
    class_resolver: 'ClassSource[Any] | _ClassResolver | None' = None,
    *,
    source_kind: str = 'Source',
    source_id: str = '',
    arg_name: str = '',
    case_sensitive: bool = True,
) -> Any:
    """Parse one value string into a typed Python value.

    Args:
      raw: The raw value string.
      value_spec: If provided, the parsed value is validated/coerced through
        ``value_spec.apply``. Also drives legacy behavior for plain tokens
        (e.g. unquoted strings, colon-separated lists).
      class_resolver: Optional source for resolving nested object calls
        ``ClassName(...)``. Accepts a ``Mapping[str, type]``, a
        ``Sequence[type]`` (each class keyed by its ``__name__``), or a
        ``Callable[[str], type | None]`` lookup. ``None`` disables nested
        object construction.
      source_kind: Label used in error messages (e.g. ``'Patcher'``,
        ``'Class'``).
      source_id: Identifier (patcher/class name) used in error messages.
      arg_name: Argument name used in error messages.
      case_sensitive: When ``False``, both class-name lookups and
        kwarg-to-field resolution on ``pg.Object`` subclasses are matched
        case-insensitively. Ignored for class lookups when
        ``class_resolver`` is a callable (the callable owns its own
        lookup policy).

    Returns:
      The parsed value (after optional ``value_spec.apply``).

    Raises:
      ValueError: If parsing fails or the value violates ``value_spec``.
    """
    ctx = _Context(
        source_kind=source_kind, source_id=source_id, arg_name=arg_name
    )
    resolver = _as_resolver(class_resolver, case_sensitive)

    # ``Str``/``Any`` fields always use legacy literal-string coercion so that
    # quoted-string handling (e.g. unmatched-quote diagnostics) and bare
    # identifier tokens like ``hello`` keep their historical meaning.
    use_legacy = isinstance(value_spec, (pg_typing.Str, pg_typing.Any))
    if not use_legacy and _starts_structured(raw):
        parser = _Parser(raw, resolver, ctx)
        value = parser.parse_root()
    else:
        value = _legacy_coerce(raw.strip(), value_spec, ctx)

    if value_spec is not None:
        root_path = topology.KeyPath.parse(
            f'{source_id}.{arg_name}' if source_id else arg_name
        )
        value = value_spec.apply(value, root_path=root_path)
    return value

options: show_root_heading: false show_root_toc_entry: false members_order: source heading_level: 2