Skip to content

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
matthewwardrop committed Jan 5, 2024
1 parent 3c02d2f commit 1f53f5b
Show file tree
Hide file tree
Showing 8 changed files with 1,081 additions and 116 deletions.
878 changes: 878 additions & 0 deletions play.ipynb

Large diffs are not rendered by default.

6 changes: 5 additions & 1 deletion spec_classes/__init__.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
from ._version import __version__, __version_tuple__
from .errors import FrozenInstanceError
from .spec_class import spec_class
from .types import Alias, DeprecatedAlias, Attr, AttrProxy, MISSING, spec_property

try:
from ._version import __version__, __version_tuple__
except ImportError:
__version__ = __version_tuple__ = None

__author__ = "Matthew Wardrop"
__author_email__ = "mpwardrop@gmail.com"

Expand Down
142 changes: 74 additions & 68 deletions spec_classes/methods/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,19 @@

from spec_classes.errors import FrozenInstanceError
from spec_classes.methods.scalar import WithAttrMethod
from spec_classes.types import Attr, MISSING
from spec_classes.types import Attr, MISSING, UNSPECIFIED
from spec_classes.utils.method_builder import MethodBuilder
from spec_classes.utils.mutation import (
invalidate_attrs,
mutate_attr,
protect_via_deepcopy,
)

from .base import MethodDescriptor
from spec_classes.methods.base import MethodDescriptor


MISSING_FINAL = object()

class InitMethod(MethodDescriptor):
"""
The default implementation of `__init__` for spec-classes.
Expand All @@ -39,92 +41,96 @@ class InitMethod(MethodDescriptor):

method_name = "__init__"


@staticmethod
def init(spec_cls, self, **kwargs):
instance_metadata = self.__spec_class__
def build_init_method_body(spec_cls):
metadata = spec_cls.__spec_class__
body = [
"metadata = self.__spec_class__",
]

for attr, attr_spec in metadata.attrs.items():
body.append(f"""
if {attr} is UNSPECIFIED:
{attr} = metadata.attrs['{attr}'].get_value(self.__class__, {attr}, copy={not attr_spec.do_not_copy})
""")

# Unlock the class for mutation during initialization.
is_frozen = instance_metadata.frozen
if instance_metadata.owner is spec_cls and instance_metadata.frozen:
instance_metadata.frozen = False
is_frozen = metadata.frozen
if metadata.owner is spec_cls and metadata.frozen:
body.append("metadata.frozen = False")

# Initialise any non-local spec attributes via parent constructors
if instance_metadata.owner is spec_cls:
for parent in reversed(spec_cls.mro()[1:]):
if metadata.owner is spec_cls:
for parent_index, parent in enumerate(reversed(spec_cls.mro()[1:])):
parent_metadata = getattr(parent, "__spec_class__", None)
if parent_metadata:
parent_kwargs = {}
parent_args = []
for attr in parent_metadata.attrs:
instance_attr_spec = instance_metadata.attrs[attr]
if instance_attr_spec.owner is not parent:
attr_spec = metadata.attrs[attr]
if attr_spec.owner is not parent:
continue
if attr in kwargs:
parent_kwargs[attr] = kwargs.pop(attr)
else:
# Parent constructor may may be overridden, and not pick up
# subclass defaults. We pre-emptively solve this here.
# If the constructor was not overridden, then no harm is
# done (we just looked it up earlier than we had to).
# We don't pass missing values in case overridden constructor
# has defaults in the signature.
instance_default = instance_attr_spec.lookup_default_value(
self.__class__
)
if instance_default is not MISSING:
parent_kwargs[attr] = instance_default
if parent_metadata.key and parent_metadata.key not in parent_kwargs:
parent_kwargs[parent_metadata.key] = MISSING
parent.__init__( # pylint: disable=unnecessary-dunder-call
self, **parent_kwargs
)

# For each attribute owned by this spec_cls in `instance_metadata`,
parent_args.append(f""""{attr}": {attr},""")
if parent_metadata.key and parent_metadata.key not in parent_args:
parent_args.append(f""""{parent_metadata.key}": MISSING if {parent_metadata.key} is UNSPECIFIED else MISSING""")
parent_args = "".join(parent_args)
body.append(f"""
self.__class__.mro()[-{parent_index+1}].__init__(self, **{{k: v for k, v in {{{parent_args}}}.items() if v is not UNSPECIFIED }})
""")

# For each attribute owned by this spec_cls in `metadata`,
# initalize the attribute.
for attr, attr_spec in instance_metadata.attrs.items():
for attr, attr_spec in metadata.attrs.items():

if (
not attr_spec.init
or attr_spec.owner is not spec_cls
or attr == instance_metadata.init_overflow_attr
or attr == metadata.init_overflow_attr
):
continue

value = kwargs.get(attr, MISSING)
if value is not MISSING:
# If owner is not spec-class, we have already looked up and
# handled copying.
copy_required = (
instance_metadata.owner is spec_cls and not attr_spec.do_not_copy
)
else:
value = attr_spec.lookup_default_value(self.__class__)
copy_required = False

if value is not MISSING:
if copy_required:
value = protect_via_deepcopy(value)
setattr(self, attr, value)
copy_required = metadata.owner is spec_cls and not attr_spec.do_not_copy

body.append(f"""
# Assign `{attr}`
# {attr} = metadata.attrs['{attr}'].get_value(self.__class__, {attr}, copy={copy_required})
if {attr} not in (MISSING, UNSPECIFIED):
self.{attr} = {attr}
""")
# if copy_required:
# body.append(f"""
# if {attr} is not MISSING:
# {attr} = protect_via_deepcopy({attr})
# """)

# body.append(f"""
# if {attr} is MISSING:
# {attr} = metadata.attrs['{attr}'].lookup_default_value(self.__class__)
# if {attr} is not MISSING:
# self.{attr} = {attr}
# """)

# Finalize initialisation by storing overflow attrs and restoring frozen
# status.
if instance_metadata.owner is spec_cls:
if instance_metadata.init_overflow_attr:
getattr(
self, f"with_{instance_metadata.init_overflow_attr}"
)( # TODO: avoid this
{
key: value
for key, value in kwargs.items()
if key not in instance_metadata.annotations
or key == instance_metadata.init_overflow_attr
},
_inplace=True,
)
if metadata.owner is spec_cls:
if metadata.init_overflow_attr:
body.append(f"""
self.with_{metadata.init_overflow_attr}({{ key: value for key, value in {metadata.init_overflow_attr}.items()
if key not in metadata.annotations
or key == metadata.init_overflow_attr }}, _inplace=True)
""")

if instance_metadata.post_init:
instance_metadata.post_init(self)
if metadata.post_init:
body.append("metadata.post_init(self)")

if is_frozen:
instance_metadata.frozen = True
body.append("metadata.frozen = True")

return "\n".join(
textwrap.dedent(s)
for s in body
)


def build_method(self) -> Callable:
spec_class_key = self.spec_cls.__spec_class__.key
Expand All @@ -136,11 +142,11 @@ def build_method(self) -> Callable:
# If the key has a default, don't require it to be set during
# construction.
key_default = (
MISSING if spec_class_key_spec.has_default else inspect.Parameter.empty
UNSPECIFIED if spec_class_key_spec.has_default else inspect.Parameter.empty
)

return (
MethodBuilder("__init__", functools.partial(self.init, self.spec_cls))
MethodBuilder("__init__", self.build_init_method_body(self.spec_cls), {"UNSPECIFIED": UNSPECIFIED, "MISSING": MISSING, "protect_via_deepcopy": protect_via_deepcopy})
.with_preamble(f"Initialise this `{self.spec_cls.__name__}` instance.")
.with_arg(
spec_class_key,
Expand Down
3 changes: 2 additions & 1 deletion spec_classes/types/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from .attr import Attr
from .attr_proxy import AttrProxy
from .keyed import KeyedList, KeyedSet
from .missing import MISSING
from .missing import MISSING, UNSPECIFIED
from .spec_property import spec_property
from .validated import ValidatedType, bounded, validated

Expand All @@ -14,6 +14,7 @@
"KeyedList",
"KeyedSet",
"MISSING",
"UNSPECIFIED",
"ValidatedType",
"bounded",
"spec_property",
Expand Down
17 changes: 16 additions & 1 deletion spec_classes/types/attr.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
type_match,
)

from .missing import MISSING
from .missing import MISSING, UNSPECIFIED

if TYPE_CHECKING: # pragma: no cover
from spec_classes.collections.base import CollectionAttrMutator
Expand Down Expand Up @@ -272,6 +272,21 @@ def item_constructor(self) -> Optional[Type]:
return self.item_spec_type_polymorphic or self.item_type

# Helpers

def get_value(self, spec_cls: Type, value: Any = UNSPECIFIED, copy: bool = True) -> Any:
from spec_classes.utils.mutation import protect_via_deepcopy

if value is MISSING:
return value
if value is UNSPECIFIED:
value = self.lookup_default_value(spec_cls)
if value is MISSING:
return UNSPECIFIED
return value
return protect_via_deepcopy(value) if copy else value



def lookup_default_value(self, spec_cls: Type) -> Any:
"""
Look up the correct default value for this attribute for `instance`.
Expand Down
16 changes: 10 additions & 6 deletions spec_classes/types/missing.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
# Sentinel for unset inputs to spec_class methods
class _MissingType:
__instance__ = None
__instances__ = {}

def __new__(cls):
if cls.__instance__ is None:
cls.__instance__ = super(_MissingType, cls).__new__(cls)
return cls.__instance__
def __new__(cls, name="MISSING"):
if name not in cls.__instances__:
cls.__instances__[name] = super(_MissingType, cls).__new__(cls)
return cls.__instances__[name]

def __init__(self, name="MISSING"):
self.name = name

def __bool__(self):
return False

def __repr__(self):
return "MISSING"
return self.name

def __copy__(self):
return self
Expand All @@ -21,3 +24,4 @@ def __deepcopy__(self, memo):


MISSING = _MissingType()
UNSPECIFIED = _MissingType("UNSPECIFIED")
Loading

0 comments on commit 1f53f5b

Please sign in to comment.