objectstate.lazy_factory

Generic lazy dataclass factory using flexible resolution.

Functions

auto_create_decorator(global_config_class)

Decorator that automatically creates: 1.

bind_lazy_resolution_to_class(cls)

Add lazy __getattribute__ to an existing class.

create_dataclass_for_editing(dataclass_type, ...)

Create dataclass for editing with configurable value preservation.

create_global_default_decorator(...)

Create a decorator factory for a specific global config class.

ensure_global_config_context(...)

Ensure proper thread-local storage setup for any global config type.

get_base_type_for_lazy(lazy_type)

Get the base type for a lazy dataclass type.

get_group_abbreviation(config_type)

Look up group abbreviation from GROUP_ABBREVIATIONS_REGISTRY.

get_inherited_field_names(cls)

Get names of fields inherited from parent dataclasses (not defined in cls itself).

get_lazy_type_for_base(base_type)

Get the lazy type for a base dataclass type.

get_registered_lazy_types()

Get all registered lazy types for constructor patching.

is_global_config_instance(config_instance)

Check if a config instance is an instance of a global config class.

is_global_config_type(config_type)

Check if a config type is a global config (marked by @auto_create_decorator).

is_lazy_dataclass(obj_or_type)

Check if an object or type is a lazy dataclass.

patch_lazy_constructors([types])

Context manager that patches lazy dataclass constructors to preserve None vs concrete distinction.

rebuild_lazy_config_with_new_global_reference(...)

Rebuild lazy config to reference new global config while preserving field states.

rebuild_with_none_defaults(cls[, ...])

Rebuild a dataclass via make_dataclass with None defaults for specified fields.

register_lazy_type(cls)

Register a lazy dataclass type for constructor patching.

register_lazy_type_mapping(lazy_type, base_type)

Register mapping between lazy dataclass type and its base type.

replace_raw(instance, **changes)

Replace dataclass fields while preserving raw None values.

resolve_lazy_configurations_for_serialization(data)

Recursively resolve lazy dataclass instances to concrete values for serialization.

Classes

GlobalConfigBase()

Virtual base class for all global config types.

GlobalConfigMeta

Metaclass that makes isinstance(obj, GlobalConfigBase) work by checking _is_global_config marker.

LazyDataclass()

Base class for all lazy dataclasses created by LazyDataclassFactory.

LazyDataclassFactory()

Generic factory for creating lazy dataclasses with flexible resolution.

LazyMethodBindings()

Declarative method bindings for lazy dataclasses.

abbreviation(name)

Decorator/marker for abbreviations in config classes.

objectstate.lazy_factory.get_inherited_field_names(cls: Type) set[source]

Get names of fields inherited from parent dataclasses (not defined in cls itself).

A field is “inherited” if it exists in a parent’s __dataclass_fields__ but is NOT in this class’s own __annotations__ (i.e., not redefined here).

objectstate.lazy_factory.rebuild_with_none_defaults(cls: Type, field_names_to_none: set | None = None, new_name: str | None = None) Type[source]

Rebuild a dataclass via make_dataclass with None defaults for specified fields.

This is the UNIFIED approach for both base classes (inherit_as_none) and lazy classes. Instead of patching Field objects after @dataclass, we rebuild with correct defaults.

Parameters:
  • cls – The dataclass to rebuild

  • field_names_to_none – Fields that should have default=None. If None, ALL fields get default=None (for lazy classes).

  • new_name – Optional new class name (for lazy classes)

Returns:

A new class with the same fields but modified defaults

objectstate.lazy_factory.replace_raw(instance, **changes)[source]

Replace dataclass fields while preserving raw None values.

Unlike dataclasses.replace(), this function uses object.__getattribute__ to get field values, preventing lazy resolution from being triggered. This is critical for lazy dataclasses where None means “inherit from parent” and must not be resolved during copy operations.

Parameters:
  • instance – The dataclass instance to copy

  • **changes – Field values to override

Returns:

A new instance with raw values preserved (not resolved)

objectstate.lazy_factory.register_lazy_type_mapping(lazy_type: Type, base_type: Type) None[source]

Register mapping between lazy dataclass type and its base type.

objectstate.lazy_factory.get_base_type_for_lazy(lazy_type: Type) Type | None[source]

Get the base type for a lazy dataclass type.

objectstate.lazy_factory.register_lazy_type(cls: Type) Type[source]

Register a lazy dataclass type for constructor patching.

This decorator/function marks a lazy dataclass as needing constructor patching when used in code execution contexts (e.g., exec()).

Parameters:

cls – The lazy dataclass type to register

Returns:

The class unchanged (allows use as decorator)

Example

register_lazy_type(LazyPipelineConfig) register_lazy_type(LazyZarrConfig)

# Or as decorator: @register_lazy_type class LazyCustomConfig:

objectstate.lazy_factory.get_registered_lazy_types() frozenset[source]

Get all registered lazy types for constructor patching.

Returns:

Immutable set of registered lazy dataclass types

objectstate.lazy_factory.is_lazy_dataclass(obj_or_type) bool[source]

Check if an object or type is a lazy dataclass.

ANTI-DUCK-TYPING: Uses isinstance() check against LazyDataclass base class instead of hasattr() attribute sniffing.

Works with both instances and types, and naturally handles Optional types without unwrapping.

Parameters:

obj_or_type – Either a dataclass instance or a dataclass type

Returns:

True if the object/type is a lazy dataclass

Examples

>>> is_lazy_dataclass(PipelineConfig)  # True (type check)
>>> is_lazy_dataclass(GlobalPipelineConfig)  # False
>>> is_lazy_dataclass(pipeline_config_instance)  # True (instance check)
>>> is_lazy_dataclass(LazyPathPlanningConfig)  # True
>>> is_lazy_dataclass(PathPlanningConfig)  # False

# Works with Optional without unwrapping! >>> config: Optional[PipelineConfig] = PipelineConfig() >>> is_lazy_dataclass(config) # True - checks the instance, not the type annotation

class objectstate.lazy_factory.GlobalConfigMeta[source]

Metaclass that makes isinstance(obj, GlobalConfigBase) work by checking _is_global_config marker.

This enables type-safe isinstance checks without inheritance:
if isinstance(config, GlobalConfigBase): # Returns True for GlobalPipelineConfig

# Returns False for PipelineConfig (lazy version)

class objectstate.lazy_factory.GlobalConfigBase[source]

Virtual base class for all global config types.

Uses custom metaclass to check _is_global_config marker instead of actual inheritance. This prevents lazy versions (PipelineConfig) from being considered global configs.

Usage:

if isinstance(config, GlobalConfigBase): # Generic, works for any global config

Instead of:

if isinstance(config, GlobalPipelineConfig): # Hardcoded, breaks extensibility

class objectstate.lazy_factory.LazyDataclass[source]

Base class for all lazy dataclasses created by LazyDataclassFactory.

This enables isinstance() checks without duck typing or unwrapping:

isinstance(config, LazyDataclass) # Works! isinstance(optional_config, LazyDataclass) # Works even for Optional!

All lazy dataclasses inherit from this, regardless of naming convention: - PipelineConfig (lazy version of GlobalPipelineConfig) - LazyPathPlanningConfig - LazyWellFilterConfig - etc.

ANTI-DUCK-TYPING: Use isinstance(obj, LazyDataclass) instead of hasattr() checks.

objectstate.lazy_factory.is_global_config_type(config_type: Type) bool[source]

Check if a config type is a global config (marked by @auto_create_decorator).

GENERIC SCOPE RULE: Use this instead of hardcoding class name checks like:

if config_class == GlobalPipelineConfig:

Instead use:

if is_global_config_type(config_class):

Parameters:

config_type – The config class to check

Returns:

True if the type is marked as a global config, False otherwise

objectstate.lazy_factory.is_global_config_instance(config_instance: Any) bool[source]

Check if a config instance is an instance of a global config class.

GENERIC SCOPE RULE: Use this instead of hardcoding isinstance checks like:

if isinstance(config, GlobalPipelineConfig):

Instead use:

if is_global_config_instance(config):

Or use the virtual base class:

if isinstance(config, GlobalConfigBase):

Parameters:

config_instance – The config instance to check

Returns:

True if the instance is of a global config type, False otherwise

objectstate.lazy_factory.get_lazy_type_for_base(base_type: Type) Type | None[source]

Get the lazy type for a base dataclass type.

objectstate.lazy_factory.bind_lazy_resolution_to_class(cls: Type) None[source]

Add lazy __getattribute__ to an existing class.

This enables concrete classes (like WellFilterConfig stored in GlobalPipelineConfig) to resolve None values via MRO without changing their static defaults.

Parameters:

cls – The class to add lazy resolution to

class objectstate.lazy_factory.LazyMethodBindings[source]

Declarative method bindings for lazy dataclasses.

static create_resolver() Callable[[Any, str], Any][source]

Create field resolver method using new pure function interface.

static create_getattribute() Callable[[Any, str], Any][source]

Create lazy __getattribute__ method using new context system.

static create_to_base_config(base_class: Type) Callable[[Any], Any][source]

Create base config converter method.

static create_class_methods() Dict[str, Any][source]

Create class-level utility methods.

__init__() None
class objectstate.lazy_factory.LazyDataclassFactory[source]

Generic factory for creating lazy dataclasses with flexible resolution.

static make_lazy_simple(base_class: Type, lazy_class_name: str = None) Type[source]

Create lazy dataclass using new contextvars system.

SIMPLIFIED: No complex hierarchy providers or field path detection needed. Uses new contextvars system for all resolution.

Parameters:
  • base_class – Base dataclass to make lazy

  • lazy_class_name – Optional name for the lazy class

Returns:

Generated lazy dataclass with contextvars-based resolution

objectstate.lazy_factory.patch_lazy_constructors(types: List[Type] | None = None)[source]

Context manager that patches lazy dataclass constructors to preserve None vs concrete distinction.

This is critical for code editors that use exec() to create dataclass instances. Without patching, lazy dataclasses would resolve None values to concrete defaults during construction, making it impossible to distinguish between explicitly set values and inherited values.

The patched constructor sets fields provided in kwargs and otherwise uses the dataclass defaults/default_factory (or None if none exist). This preserves the None vs concrete distinction while still instantiating nested lazy configs.

Parameters:

types – Optional list of lazy types to patch. If None, uses all registered lazy types.

Usage:

# Register types at module level register_lazy_type(LazyPipelineConfig) register_lazy_type(LazyZarrConfig)

# Patch during code execution with patch_lazy_constructors():

exec(code_string, namespace) # Lazy dataclasses created during exec() will preserve None values

Example

# Without patching: LazyZarrConfig(compression=’gzip’) # All unspecified fields resolve to defaults

# With patching: with patch_lazy_constructors():

LazyZarrConfig(compression=’gzip’) # Only compression is set, rest are None

objectstate.lazy_factory.ensure_global_config_context(global_config_type: Type, global_config_instance: Any) None[source]

Ensure proper thread-local storage setup for any global config type.

objectstate.lazy_factory.resolve_lazy_configurations_for_serialization(data: Any) Any[source]

Recursively resolve lazy dataclass instances to concrete values for serialization.

CRITICAL: This function must be called WITHIN a config_context() block! The context provides the hierarchy for lazy resolution.

How it works: 1. For lazy dataclasses: Access fields with getattr() to trigger resolution 2. The lazy __getattribute__ uses the active config_context() to resolve None values 3. Convert resolved values to base config for pickling

Example (from README.md):
with config_context(orchestrator.pipeline_config):

# Lazy resolution happens here via context resolved_steps = resolve_lazy_configurations_for_serialization(steps)

objectstate.lazy_factory.create_dataclass_for_editing(dataclass_type: Type[T], source_config: Any, preserve_values: bool = False, context_provider: Callable[[Any], None] | None = None) T[source]

Create dataclass for editing with configurable value preservation.

objectstate.lazy_factory.rebuild_lazy_config_with_new_global_reference(existing_lazy_config: Any, new_global_config: Any, global_config_type: Type | None = None) Any[source]

Rebuild lazy config to reference new global config while preserving field states.

This function preserves the exact field state of the existing lazy config: - Fields that are None (using lazy resolution) remain None - Fields that have been explicitly set retain their concrete values - Nested dataclass fields are recursively rebuilt to reference new global config - The underlying global config reference is updated for None field resolution

Parameters:
  • existing_lazy_config – Current lazy config instance

  • new_global_config – New global config to reference for lazy resolution

  • global_config_type – Type of the global config (defaults to type of new_global_config)

Returns:

New lazy config instance with preserved field states and updated global reference

class objectstate.lazy_factory.abbreviation(name: str)[source]

Decorator/marker for abbreviations in config classes.

Can be used as: 1. Class decorator: @abbreviation(‘wfc’) to set class abbreviation 2. In Annotated types: Annotated[Type, abbreviation(‘wf’)] for field abbreviation

Examples

@abbreviation(‘wfc’) @global_pipeline_config @dataclass class WellFilterConfig:

well_filter: Annotated[Optional[int], abbreviation(‘wf’)] = None well_filter_mode: Annotated[WellFilterMode, abbreviation(‘wfm’)] = WellFilterMode.INCLUDE

Parameters:

name – The abbreviation string

Returns:

the class with _abbreviation attribute set When used in Annotated: a marker object storing the abbreviation

Return type:

When used as class decorator

__init__(name: str)[source]
__call__(cls: Type) Type[source]

Class decorator usage: @abbreviation(‘wfc’)

objectstate.lazy_factory.get_group_abbreviation(config_type: str | type) str[source]

Look up group abbreviation from GROUP_ABBREVIATIONS_REGISTRY.

Handles both type objects and type name strings.

Parameters:

config_type – Config class or type name to get abbreviation for

Returns:

Abbreviation string if found, otherwise falls back to class name prefix

objectstate.lazy_factory.create_global_default_decorator(target_config_class: Type)[source]

Create a decorator factory for a specific global config class.

The decorator accumulates all decorations, then injects all fields at once when the module finishes loading. Also creates lazy versions of all decorated configs.

objectstate.lazy_factory.auto_create_decorator(global_config_class)[source]

Decorator that automatically creates: 1. A field injection decorator for other configs to use 2. A lazy version of the global config itself

Global config classes must start with “Global” prefix.