Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion ccflow/__init__.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
__version__ = "0.6.10"

# Import exttypes early so modules that import `from ccflow import PyObjectPath` during
# initialization find it (avoids circular import issues with functions that import utilities
# which, in turn, import `ccflow`).
from .exttypes import * # noqa: I001

from .arrow import *
from .base import *
from .compose import *
from .callable import *
from .context import *
from .enums import Enum
from .exttypes import *
from .global_state import *
from .models import *
from .object_config import *
Expand Down
15 changes: 5 additions & 10 deletions ccflow/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@
log = logging.getLogger(__name__)

__all__ = (
"model_alias",
"BaseModel",
"ModelRegistry",
"ModelType",
Expand Down Expand Up @@ -324,17 +323,13 @@ def _is_config_subregistry(value):


def model_alias(model_name: str) -> BaseModel:
"""Function to alias a BaseModel by name in the root registry.
"""Alias a BaseModel by name (backward compatible).

Useful for configs in hydra where we want a config object to point directly to another config object.

Args:
model_name: The name of the underlying model to point to in the registry
Example:
_target_: ccflow.model_alias
model_name: foo
Delegates to `ccflow.compose.model_alias` and preserves the original signature.
"""
return BaseModel.model_validate(model_name)
from .compose import model_alias as _model_alias

return _model_alias(model_name)


ModelType = TypeVar("ModelType", bound=BaseModel)
Expand Down
101 changes: 101 additions & 0 deletions ccflow/compose.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
from typing import Any, Dict, Optional

from .base import BaseModel
from .exttypes.pyobjectpath import _TYPE_ADAPTER as PyObjectPathTA

__all__ = (
"model_alias",
"from_python",
"update_from_base",
)


def model_alias(model_name: str) -> BaseModel:
"""Return a model by alias from the registry.
Hydra-friendly: `_target_: ccflow.compose.model_alias` with `model_name`.
"""
return BaseModel.model_validate(model_name)


def from_python(py_object_path: str, indexer: Optional[list] = None) -> Any:
"""Hydra-friendly: resolve and return any Python object by import path.
Optionally accepts `indexer`, a list of keys that will be applied in order
to index into the resolved object. No safety checks are performed; indexing
errors will propagate.
Example YAML usage:
some_value:
_target_: ccflow.compose.from_python
py_object_path: mypkg.module.OBJECT
nested_value:
_target_: ccflow.compose.from_python
py_object_path: mypkg.module.NESTED
indexer: ["a", "b"]
"""
obj = PyObjectPathTA.validate_python(py_object_path).object
if indexer:
for key in indexer:
obj = obj[key]
return obj


def update_from_base(
base: Any = None,
*,
target_class: Optional[Any] = None,
update: Optional[Dict[str, Any]] = None,
) -> Any:
"""Generic update helper that constructs an instance from a base and updates.
Args:
base: Either a registry alias string, a dict, or a Pydantic BaseModel. If BaseModel, it is converted
to a shallow dict via ``dict(base)`` to preserve nested object identity.
target_class: Optional path to the target class to construct. May be a
string import path or the type itself. If None and ``base`` is a
BaseModel, returns an instance of ``base.__class__``. If None and
``base`` is a dict, returns the updated dict.
update: Optional dict of updates to apply.
Returns:
Instance of ``target_class`` if provided; otherwise an instance of the same
class as ``base`` when base is a BaseModel; or the updated dict when base
is a dict.
"""
# Determine base dict and default target
default_target = None
if isinstance(base, str):
# Allow passing alias name directly; resolve from registry
base = model_alias(base)
if isinstance(base, BaseModel):
base_dict = dict(base)
default_target = base.__class__
elif isinstance(base, dict):
base_dict = dict(base)
elif base is None:
base_dict = {}
else:
raise TypeError("base must be a dict, BaseModel, or None")

# Merge updates: explicit dict first, then kwargs
if update:
base_dict.update(update)

# Resolve target class if provided as string path
target = None
if target_class is not None:
if isinstance(target_class, str):
target = PyObjectPathTA.validate_python(target_class).object
else:
target = target_class
else:
target = default_target

if target is None:
# No target: return dict update for dict base
return base_dict

# Construct instance of target with updated fields
return target(**base_dict)
23 changes: 23 additions & 0 deletions ccflow/tests/config/conf_from_python.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
shared_model:
_target_: ccflow.compose.from_python
py_object_path: ccflow.tests.data.python_object_samples.SHARED_MODEL

consumer:
_target_: ccflow.tests.data.python_object_samples.Consumer
shared: shared_model
tag: consumer1

# Demonstrate from_python returning a dict (non-BaseModel)
holder:
_target_: ccflow.tests.data.python_object_samples.SharedHolder
name: holder1
cfg:
_target_: ccflow.compose.from_python
py_object_path: ccflow.tests.data.python_object_samples.SHARED_CFG

# Use update_from_base to update a field while preserving shared identity
consumer_updated:
_target_: ccflow.compose.update_from_base
base: consumer
update:
tag: consumer2
4 changes: 4 additions & 0 deletions ccflow/tests/data/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
"""Shared test data modules.

Import sample configs using module-level objects in `python_object_samples`.
"""
33 changes: 33 additions & 0 deletions ccflow/tests/data/python_object_samples.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
"""Sample python objects for testing from_python and identity preservation."""

from typing import Dict

from ccflow import BaseModel

# Module-level objects
SHARED_CFG: Dict[str, int] = {"x": 1, "y": 2}
OTHER_CFG: Dict[str, int] = {"x": 10, "y": 20}
"""Dict samples; identity for dicts is not guaranteed by Pydantic."""

NESTED_CFG = {
"db": {"host": "seed.local", "port": 7000, "name": "seed"},
"meta": {"env": "dev"},
}


class SharedHolder(BaseModel):
name: str
cfg: Dict[str, int]


class SharedModel(BaseModel):
val: int = 0


# Module-level instance to be resolved via from_python
SHARED_MODEL = SharedModel(val=42)


class Consumer(BaseModel):
shared: SharedModel
tag: str = ""
Loading