diff --git a/instruct/__init__.py b/instruct/__init__.py index 0864be6..d1b413a 100644 --- a/instruct/__init__.py +++ b/instruct/__init__.py @@ -102,12 +102,13 @@ def public_class( preserve_subtraction indicates that we want the public facing subtracted class as opposed to its root parent. """ + cls: Type[T] if not isinstance(instance_or_type, type): - cls: Type[T] = type(instance_or_type) + cls = type(instance_or_type) else: - cls: Type[T] = instance_or_type - if not isinstance(cls, Atomic): - raise TypeError(f"Can only call on Atomic-metaclassed types!, {cls}") + cls = instance_or_type + if not isinstance(cls, AtomicMeta): + raise TypeError(f"Can only call on AtomicMeta-metaclassed types!, {cls}") if property_path: key, *rest = property_path if key not in cls._slots: @@ -144,9 +145,9 @@ def public_class( if preserve_subtraction and any((cls._skipped_fields, cls._modified_fields)): return cls if any((cls._skipped_fields, cls._modified_fields)): - bases = tuple(x for x in cls.__bases__ if ismetasubclass(x, Atomic)) + bases = tuple(x for x in cls.__bases__ if ismetasubclass(x, AtomicMeta)) while len(bases) == 1 and any((bases[0]._skipped_fields, bases[0]._modified_fields)): - bases = tuple(x for x in cls.__bases__ if ismetasubclass(x, Atomic)) + bases = tuple(x for x in cls.__bases__ if ismetasubclass(x, AtomicMeta)) if len(bases) == 1: return bases[0] return cls @@ -157,9 +158,11 @@ def clear(instance: T, fields: Optional[Iterable[str]] = None) -> T: Clear all fields on an instruct class instance. """ if isinstance(instance, type): - raise TypeError("Can only call on an Atomic-metaclassed instance! You passed in a type!") - if not isinstance(instance.__class__, Atomic): - raise TypeError("Can only call on an Atomic-metaclassed type!") + raise TypeError( + "Can only call on an AtomicMeta-metaclassed instance! You passed in a type!" + ) + if not isinstance(instance.__class__, AtomicMeta): + raise TypeError("Can only call on an AtomicMeta-metaclassed type!") if fields: unrecognized_keys = frozenset(fields) - frozenset(keys(instance)) if unrecognized_keys: @@ -198,8 +201,8 @@ def keys( else: cls = instance_or_cls instance = None - if not isinstance(cls, Atomic): - raise TypeError(f"Can only call on Atomic-metaclassed types!, {cls}") + if not isinstance(cls, AtomicMeta): + raise TypeError(f"Can only call on AtomicMeta-metaclassed types!, {cls}") if not property_path: if instance is not None and not all: return AtomicKeysView(instance) @@ -225,8 +228,8 @@ def values(instance) -> AtomicValuesView: Analogous to dict.values(...) """ cls = type(instance) - if not isinstance(cls, Atomic): - raise TypeError("Can only call on Atomic-metaclassed types!") + if not isinstance(cls, AtomicMeta): + raise TypeError("Can only call on AtomicMeta-metaclassed types!") if instance is not None: return AtomicValuesView(instance) raise TypeError(f"values of a {cls} object needs to be called on an instance of {cls}") @@ -237,8 +240,8 @@ def items(instance: T) -> ItemsView: Analogous to dict.items(...) """ cls: Type[T] = type(instance) - if not isinstance(cls, Atomic): - raise TypeError("Can only call on Atomic-metaclassed types!") + if not isinstance(cls, AtomicMeta): + raise TypeError("Can only call on AtomicMeta-metaclassed types!") if instance is not None: return ItemsView(instance) raise TypeError(f"items of a {cls} object needs to be called on an instance of {cls}") @@ -250,8 +253,8 @@ def get(instance: T, key, default=None) -> Optional[Any]: exist on the type. """ cls: Type[T] = type(instance) - if not isinstance(cls, Atomic): - raise TypeError("Can only call on Atomic-metaclassed types!") + if not isinstance(cls, AtomicMeta): + raise TypeError("Can only call on AtomicMeta-metaclassed types!") if instance is None: raise TypeError(f"items of a {cls} object needs to be called on an instance of {cls}") try: @@ -265,8 +268,8 @@ def asdict(instance: T) -> Dict[str, Any]: Return a dictionary version of the instance """ cls: Type[T] = type(instance) - if not isinstance(cls, Atomic): - raise TypeError("Must be an Atomic-metaclassed type!") + if not isinstance(cls, AtomicMeta): + raise TypeError("Must be an AtomicMeta-metaclassed type!") return instance._asdict() @@ -275,8 +278,8 @@ def astuple(instance: T) -> Tuple[Any, ...]: Return a tuple of values from the instance """ cls: Type[T] = type(instance) - if not isinstance(cls, Atomic): - raise TypeError("Must be an Atomic-metaclassed type!") + if not isinstance(cls, AtomicMeta): + raise TypeError("Must be an AtomicMeta-metaclassed type!") return instance._astuple() @@ -285,8 +288,8 @@ def aslist(instance: T) -> List[Any]: Return a list of values from the instance """ cls: Type[T] = type(instance) - if not isinstance(cls, Atomic): - raise TypeError("Must be an Atomic-metaclassed type!") + if not isinstance(cls, AtomicMeta): + raise TypeError("Must be an AtomicMeta-metaclassed type!") return instance._aslist() @@ -304,8 +307,8 @@ def show_all_fields( cls = type(instance_or_cls) else: cls = instance_or_cls - if not isinstance(cls, Atomic): - raise TypeError("Must be an Atomic-metaclassed type!") + if not isinstance(cls, AtomicMeta): + raise TypeError("Must be an AtomicMeta-metaclassed type!") all_fields = {} for key, value in cls._slots.items(): all_fields[key] = {} @@ -316,7 +319,7 @@ def show_all_fields( if key in cls._nested_atomic_collection_keys: for item in cls._nested_atomic_collection_keys[key]: all_fields[key].update(show_all_fields(item, deep_traverse_on=next_deep_level)) - elif ismetasubclass(value, Atomic): + elif ismetasubclass(value, AtomicMeta): all_fields[key].update(show_all_fields(value, deep_traverse_on=next_deep_level)) if not all_fields[key]: all_fields[key] = None @@ -339,7 +342,7 @@ def _dump_skipped_fields(cls) -> Optional[FrozenMapping[str, Any]]: skipped_on_typedef_merged.update(skipped_on_typedef) if skipped_on_typedef_merged: skipped[key] = skipped_on_typedef_merged - elif ismetasubclass(typedef, Atomic): + elif ismetasubclass(typedef, AtomicMeta): skipped_on_typedef = _dump_skipped_fields(typedef) if skipped_on_typedef: skipped[key] = skipped_on_typedef @@ -676,7 +679,7 @@ def replace_class_references( def insert_class_closure( - klass: Atomic, function: Optional[Callable[..., Any]] + klass: AtomicMeta, function: Optional[Callable[..., Any]] ) -> Optional[Callable[..., Any]]: """ an implicit super() works by looking at __class__ to fill in the @@ -965,13 +968,13 @@ def transform_typing_to_coerce( type_hints = Union[type_hints] assert is_typing_definition(type_hints) or isinstance(type_hints, type) - return type_hints, wrapper_for_type(type_hints, class_mapping, Atomic) + return type_hints, wrapper_for_type(type_hints, class_mapping, AtomicMeta) class ModifiedSkipTypes(NamedTuple): - replacement_type_definition: Atomic + replacement_type_definition: AtomicMeta replacement_coerce_definition: Optional[Tuple[Any, Callable]] - mutant_classes: FrozenSet[Tuple[Atomic, Atomic]] + mutant_classes: FrozenSet[Tuple[AtomicMeta, AtomicMeta]] def create_union_coerce_function( @@ -1007,18 +1010,18 @@ def cast_values(value): def apply_skip_keys( skip_key_fields: Union[FrozenSet[str], Set[str], Dict[str, Any]], - current_definition: Atomic, + current_definition: AtomicMeta, current_coerce: Optional[Tuple[Any, Callable[[Any], Any]]], ) -> ModifiedSkipTypes: """ - If the current definition is Atomic, then Atomic - fields should be compatible with Atomic. + If the current definition is AtomicMeta, then AtomicMeta - fields should be compatible with AtomicMeta. So we will cast the base class to it's child class as the child should be compatible. Current issues: - - How do we unpack a Tuple[Union[Atomic1, Atomic2], ...] and transform to - Tuple[Union[Atomic1 - skip_fields, Atomic2 - skip_fields]] ? - - How do we handle Dict[str, Atomic1] -> Dict[str, Atomic1 - skip_fields] ? + - How do we unpack a Tuple[Union[AtomicMeta1, AtomicMeta2], ...] and transform to + Tuple[Union[AtomicMeta1 - skip_fields, AtomicMeta2 - skip_fields]] ? + - How do we handle Dict[str, AtomicMeta1] -> Dict[str, AtomicMeta1 - skip_fields] ? Basically this has to traverse the graph, branching out and replacing nodes. """ @@ -1041,13 +1044,13 @@ def apply_skip_keys( if ( isinstance(current_definition, type) - and ismetasubclass(current_definition, Atomic) + and ismetasubclass(current_definition, AtomicMeta) or is_typing_definition(current_definition) or isinstance(current_definition, tuple) ): new_coerce_definition = None original_definition = current_definition - gen = find_class_in_definition(current_definition, Atomic, metaclass=True) + gen = find_class_in_definition(current_definition, AtomicMeta, metaclass=True) replace_class_refs = [] try: result = None @@ -1133,14 +1136,14 @@ def __init_subclass__(cls, **kwargs): return __init_subclass__ -class Atomic(type): +class AtomicMeta(type): __slots__ = () REGISTRY = ReadOnly(set()) MIXINS = ReadOnly({}) - SKIPPED_FIELDS: Mapping[FrozenSet[str], Atomic] = WeakValueDictionary() + SKIPPED_FIELDS: Mapping[FrozenSet[str], AtomicMeta] = WeakValueDictionary() if TYPE_CHECKING: - _data_class: Atomic + _data_class: AtomicMeta _columns: Mapping[str, Any] _slots: Mapping[str, type] _column_types: Mapping[str, Union[Type, Tuple[Type, ...]]] @@ -1149,8 +1152,8 @@ class Atomic(type): _properties: typing.KeysView[str] _configuration: AttrsDict _all_accessible_fields: typing.KeysView[str] - # i.e. key -> List[Union[AtomicDerived, bool]] means key can hold an Atomic derived type. - _nested_atomic_collection_keys: Mapping[str, Tuple[Atomic, ...]] + # i.e. key -> List[Union[AtomicMetaDerived, bool]] means key can hold an AtomicMeta derived type. + _nested_atomic_collection_keys: Mapping[str, Tuple[AtomicMeta, ...]] def __public_class__(self): """ @@ -1174,9 +1177,9 @@ def __iter__(self): yield from self._columns.keys() def __and__( - self: Atomic, + self: AtomicMeta, include_fields: Union[Set[str], List[str], Tuple[str], FrozenSet[str], str, Dict[str, Any]], - ) -> Atomic: + ) -> AtomicMeta: assert isinstance(include_fields, (list, frozenset, set, tuple, dict, str, FrozenMapping)) include_fields: FrozenMapping = flatten_fields.collect(include_fields) include_fields -= self._skipped_fields @@ -1224,12 +1227,14 @@ def __getitem__(self, key): ) raise AttributeError(key) - def __sub__(self: Atomic, skip_fields: Union[Mapping[str, Any], Iterable[Any]]) -> Atomic: + def __sub__( + self: AtomicMeta, skip_fields: Union[Mapping[str, Any], Iterable[Any]] + ) -> AtomicMeta: assert isinstance(skip_fields, (list, frozenset, set, tuple, dict, str, FrozenMapping)) debug_mode = is_debug_mode("skip") root_class = type(self) - cls: Type[Atomic] = public_class(self) + cls: Type[AtomicMeta] = public_class(self) if not skip_fields: return self @@ -1442,7 +1447,7 @@ def __new__( combined_columns: Dict[Type, Type] = {} combined_slots = {} - nested_atomic_collections: Dict[str, Atomic] = {} + nested_atomic_collections: Dict[str, AtomicMeta] = {} # Mapping of public name -> custom type vector for `isinstance(...)` checks! column_types: Dict[str, Union[Type, Tuple[Type, ...]]] = {} base_class_has_subclass_init = False @@ -1464,7 +1469,7 @@ def __new__( if base_class_has_subclass_init: init_subclass_kwargs[mixin_name] = mixins[mixin_name] continue - raise ValueError(f"{mixin_name!r} is not a registered Mixin on Atomic!") + raise ValueError(f"{mixin_name!r} is not a registered Mixin on AtomicMeta!") if isinstance(mixins[mixin_name], type): mixin_cls = mixins[mixin_name] bases = (mixin_cls,) + bases @@ -1499,7 +1504,7 @@ def __new__( if ( hasattr(cls, "__slots__") and cls.__slots__ != () - and not ismetasubclass(cls, Atomic) + and not ismetasubclass(cls, AtomicMeta) ): # Because we cannot control the circumstances of a base class's construction # and it has __slots__, which will destroy our multiple inheritance support, @@ -1509,7 +1514,7 @@ def __new__( # subject to this limitation. raise TypeError( f"Multi-slot classes (like {cls.__name__}) must be defined " - "with `metaclass=Atomic`. Mixins with empty __slots__ are not subject to " + "with `metaclass=AtomicMeta`. Mixins with empty __slots__ are not subject to " "this restriction." ) if hasattr(cls, "_listener_funcs"): @@ -1521,8 +1526,8 @@ def __new__( if hasattr(cls, "__extra_slots__"): support_columns.extend(list(cls.__extra_slots__)) - if ismetasubclass(cls, Atomic): - # Only Atomic Descendants will merge in the helpers of + if ismetasubclass(cls, AtomicMeta): + # Only AtomicMeta Descendants will merge in the helpers of # _columns: Dict[str, Type] if cls._annotated_metadata: annotated_metadata.update(cls._annotated_metadata) @@ -1582,8 +1587,10 @@ def __new__( else: typehint = typehint_or_anonymous_struct_decl del typehint_or_anonymous_struct_decl - if not ismetasubclass(typehint, Atomic): - nested_atomics = tuple(find_class_in_definition(typehint, Atomic, metaclass=True)) + if not ismetasubclass(typehint, AtomicMeta): + nested_atomics = tuple( + find_class_in_definition(typehint, AtomicMeta, metaclass=True) + ) if nested_atomics: nested_atomic_collections[key] = nested_atomics del nested_atomics @@ -2010,7 +2017,7 @@ def __repr__(self): UNDEFINED = Undefined() -class History(metaclass=Atomic): +class History(metaclass=AtomicMeta): __slots__ = ("_changes", "_changed_keys", "_changed_index", "_suppress_history") setter_wrapper = "history-setter-wrapper.jinja" @@ -2094,7 +2101,7 @@ def list_changes(self): key_counter[key] += 1 -Atomic.register_mixin("history", History) +AtomicMeta.register_mixin("history", History) DEFAULTS = """{%- for field in fields %} @@ -2103,7 +2110,7 @@ def list_changes(self): """ -class IMapping(metaclass=Atomic): +class IMapping(metaclass=AtomicMeta): """ Allow an instruct class instance to have the `keys()` function which is mandatory to support **item unpacking. @@ -2148,7 +2155,7 @@ def get(self, key, default=None): return get(self, key, default) -Atomic.register_mixin("mapping", IMapping) +AtomicMeta.register_mixin("mapping", IMapping) def add_event_listener(*fields): @@ -2219,11 +2226,11 @@ def _encode_simple_nested_base(iterable, *, immutable=None): return iterable -class JSONSerializable(metaclass=Atomic): +class JSONSerializable(metaclass=AtomicMeta): __slots__ = () def to_json(self) -> Dict[str, Any]: - return Atomic.to_json(self)[0] + return AtomicMeta.to_json(self)[0] def __json__(self) -> Dict[str, Any]: return self.to_json() @@ -2246,10 +2253,10 @@ def wrapper(func): return wrapper -Atomic.register_mixin("json", JSONSerializable) +AtomicMeta.register_mixin("json", JSONSerializable) -class SimpleBase(metaclass=Atomic): +class SimpleBase(metaclass=AtomicMeta): __slots__ = ("_flags",) __setter_template__ = ReadOnly("self._{key}_ = val") __getter_template__ = ReadOnly("return self._{key}_") @@ -2417,7 +2424,7 @@ class Base(SimpleBase, mapping=True, json=True): # class instantiation status: "Flags", # metaclass - "Atomic", + "AtomicMeta", # history "History", "Delta", diff --git a/instruct/typedef.py b/instruct/typedef.py index 46febde..a438da5 100644 --- a/instruct/typedef.py +++ b/instruct/typedef.py @@ -319,7 +319,7 @@ def test_func(value) -> bool: return isinstance(value, types) elif origin_cls is Literal: - from . import Atomic, public_class + from . import AtomicMeta, public_class def test_func(value) -> bool: """ @@ -336,7 +336,7 @@ def test_func(value) -> bool: arg_type = arg else: arg_type = type(arg) - if isinstance(arg_type, Atomic): + if isinstance(arg_type, AtomicMeta): arg_type = public_class(arg_type, preserve_subtraction=True) if isinstance(value, arg_type): return True diff --git a/tests/test_atomic.py b/tests/test_atomic.py index de345cf..c375e3a 100644 --- a/tests/test_atomic.py +++ b/tests/test_atomic.py @@ -31,7 +31,7 @@ NoPickle, Range, RangeError, - Atomic, + AtomicMeta, NoHistory, clear, asdict, @@ -1408,7 +1408,7 @@ class Item(SimpleBase): field5: Annotated[str, NoIterable, NoJSON] i = Item("a", "b", "c", "d", "e") - assert Atomic.to_json(i)[0] == {"field1": "a", "field3": "c"} + assert AtomicMeta.to_json(i)[0] == {"field1": "a", "field3": "c"} assert i.__reduce__()[-1] == {"field2": "b", "field3": "c", "field5": "e"} assert dict(i) == {"field1": "a", "field2": "b", "field4": "d"} assert i._asdict() == { diff --git a/tests/test_subtype.py b/tests/test_subtype.py index e0ed792..641b8bb 100644 --- a/tests/test_subtype.py +++ b/tests/test_subtype.py @@ -22,7 +22,7 @@ class Foo(Base): from pytest import fixture from typing import Tuple, Mapping, Union, List, Any, Dict -from instruct import SimpleBase, keys, transform_typing_to_coerce, Atomic, asdict +from instruct import SimpleBase, keys, transform_typing_to_coerce, AtomicMeta, asdict from instruct.subtype import ( handle_instruct, handle_collection, @@ -69,7 +69,7 @@ def test_simple_function_composition(Item, SubItem): Item -> Item - {...} """ - cast_func = handle_instruct(Atomic, Item, SubItem) + cast_func = handle_instruct(AtomicMeta, Item, SubItem) from_value = Item(field="my value", value=123) to_value = SubItem(field="my value", value=123) assert cast_func(from_value) == to_value @@ -77,7 +77,7 @@ def test_simple_function_composition(Item, SubItem): # test currying - cast_func = handle_instruct(Atomic, Item)(SubItem) + cast_func = handle_instruct(AtomicMeta, Item)(SubItem) assert cast_func(from_value) == to_value assert isinstance(cast_func(from_value), SubItem) @@ -90,7 +90,7 @@ def test_collection_function_composition(Item, SubItem): Tuple[Item, ...] -> Tuple[SubItem, ...] Tuple[Item, str] -> Tuple[SubItem, str] """ - cast_func = handle_collection(list, handle_instruct(Atomic, Item, SubItem)) + cast_func = handle_collection(list, handle_instruct(AtomicMeta, Item, SubItem)) from_value = [Item(field="my value", value=123)] to_value = [SubItem(field="my value", value=123)] mutated_value = cast_func(from_value) @@ -98,7 +98,7 @@ def test_collection_function_composition(Item, SubItem): assert isinstance(mutated_value, list) assert all(isinstance(x, SubItem) for x in mutated_value) - cast_func = handle_collection(tuple, handle_instruct(Atomic, Item, SubItem)) + cast_func = handle_collection(tuple, handle_instruct(AtomicMeta, Item, SubItem)) from_value = (Item(field="my value", value=123),) to_value = (SubItem(field="my value", value=123),) mutated_value = cast_func(from_value) @@ -106,7 +106,9 @@ def test_collection_function_composition(Item, SubItem): assert isinstance(mutated_value, tuple) assert all(isinstance(x, SubItem) for x in mutated_value) - cast_func = handle_collection(tuple, handle_instruct(Atomic, Item, SubItem, handle_object(str))) + cast_func = handle_collection( + tuple, handle_instruct(AtomicMeta, Item, SubItem, handle_object(str)) + ) from_value = (Item(field="my value", value=123), "abcdef") to_value = (SubItem(field="my value", value=123), "abcdef") mutated_value = cast_func(from_value) @@ -126,7 +128,7 @@ def test_mapping_function_composition(Item, SubItem): # Mapping[str, Mapping[str, Item]] -> Mapping[str, Mapping[str, SubItem]] cast_func = handle_mapping( - dict, str, handle_mapping(dict, str, handle_instruct(Atomic, Item, SubItem)) + dict, str, handle_mapping(dict, str, handle_instruct(AtomicMeta, Item, SubItem)) ) from_value = {"a": {"b": Item(field="my value", value=1234)}} to_value = {"a": {"b": SubItem(field="my value", value=1234)}} @@ -137,7 +139,7 @@ def test_mapping_function_composition(Item, SubItem): cast_func = handle_mapping( dict, str, - handle_collection(tuple, handle_object(str), handle_instruct(Atomic, Item, SubItem)), + handle_collection(tuple, handle_object(str), handle_instruct(AtomicMeta, Item, SubItem)), ) from_value = {"a": ("b", Item(field="my value", value=1234))} to_value = {"a": ("b", SubItem(field="my value", value=1234))} @@ -150,7 +152,7 @@ def test_mapping_function_composition(Item, SubItem): handle_union(str, handle_object(str), int, handle_object(int)), handle_union( Tuple[Item, ...], - handle_collection(tuple, handle_instruct(Atomic, Item, SubItem)), + handle_collection(tuple, handle_instruct(AtomicMeta, Item, SubItem)), Tuple[str, ...], handle_collection(tuple, handle_object(str)), ), diff --git a/tests/test_typedef.py b/tests/test_typedef.py index c298480..0871dd2 100644 --- a/tests/test_typedef.py +++ b/tests/test_typedef.py @@ -9,7 +9,7 @@ get_args, issubormetasubclass, ) -from instruct import Base, Atomic +from instruct import Base, AtomicMeta from typing import List, Union, AnyStr, Any, Optional, Generic, TypeVar, Tuple, FrozenSet, Set, Dict try: @@ -147,22 +147,26 @@ class Item(Base): class Bar(Base): field: Item - assert (Item,) == tuple(find_class_in_definition(Item, Atomic, metaclass=True)) + assert (Item,) == tuple(find_class_in_definition(Item, AtomicMeta, metaclass=True)) # find_class_in_definition only goes one level and will always return the immediate atomic level - assert (Bar,) == tuple(find_class_in_definition(Bar, Atomic, metaclass=True)) + assert (Bar,) == tuple(find_class_in_definition(Bar, AtomicMeta, metaclass=True)) type_hints = Tuple[Item, ...] - items = tuple(find_class_in_definition(type_hints, Atomic, metaclass=True)) + items = tuple(find_class_in_definition(type_hints, AtomicMeta, metaclass=True)) assert items == (Item,) assert items[0] is Item - items = tuple(find_class_in_definition(Tuple[Tuple[Item, Item], ...], Atomic, metaclass=True)) + items = tuple( + find_class_in_definition(Tuple[Tuple[Item, Item], ...], AtomicMeta, metaclass=True) + ) assert items == (Item, Item) items = tuple( - find_class_in_definition(Tuple[Tuple[Dict[str, Item], int], ...], Atomic, metaclass=True) + find_class_in_definition( + Tuple[Tuple[Dict[str, Item], int], ...], AtomicMeta, metaclass=True + ) ) assert items == (Item,) type_hints = Optional[Bar] - items = tuple(find_class_in_definition(type_hints, Atomic, metaclass=True)) + items = tuple(find_class_in_definition(type_hints, AtomicMeta, metaclass=True)) assert items == (Bar,)