Skip to content

Commit

Permalink
Merge pull request #10 from lmignon/master-refactor
Browse files Browse the repository at this point in the history
Master refactor
  • Loading branch information
lmignon authored Jul 14, 2023
2 parents de590cf + 0a1aca2 commit 12a8aa1
Show file tree
Hide file tree
Showing 6 changed files with 50 additions and 25 deletions.
3 changes: 3 additions & 0 deletions news/.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Access to the context variable used to store the current extended Classes
returns None if no context is available. Previously the access to the context
throws an exception if no context was available.
2 changes: 2 additions & 0 deletions news/exceptions.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Calls to the classmethod "_get_assembled_cls" now raises RegistryNotInitializedError
if the registry is not initialized.
3 changes: 3 additions & 0 deletions news/refactor.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
The metadaclass now provides the method `_wrap_class_method`. This method
can be used to wrap class methods in a way that when the method is called
the logic is delegated to the aggregated class instance if it exists.
6 changes: 3 additions & 3 deletions src/extendable/context.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
# define context vars to hold the extendable registry

from contextvars import ContextVar
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Optional

if TYPE_CHECKING:
from .registry import ExtendableClassesRegistry

extendable_registry: ContextVar["ExtendableClassesRegistry"] = ContextVar(
"extendable_registry"
extendable_registry: ContextVar[Optional["ExtendableClassesRegistry"]] = ContextVar(
"extendable_registry", default=None
)
2 changes: 2 additions & 0 deletions src/extendable/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
class RegistryNotInitializedError(Exception):
pass
59 changes: 37 additions & 22 deletions src/extendable/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,14 @@
from abc import ABCMeta

from .context import extendable_registry
from .exceptions import RegistryNotInitializedError

_registry_build_mode = False
if TYPE_CHECKING:
from .registry import ExtendableClassesRegistry

AnyClassMethod = classmethod[Any, Any, Any]


class ExtendableClassDef:
name: str
Expand Down Expand Up @@ -190,28 +193,7 @@ def _wrap_class_methods(metacls, namespace: Dict[str, Any]) -> Dict[str, Any]:
new_namespace: Dict[str, Any] = {}
for key, value in namespace.items():
if isinstance(value, classmethod):
func = value.__func__

@no_type_check
def new_method(
cls, *args, _method_name=None, _initial_func=None, **kwargs
):
# ensure that arggs and kwargs are conform to the
# initial signature
inspect.signature(_initial_func).bind(cls, *args, **kwargs)
try:
return getattr(cls._get_assembled_cls(), _method_name)(
*args, **kwargs
)
except KeyError:
return _initial_func(cls, *args, **kwargs)

new_method_def = functools.partial(
new_method, _method_name=key, _initial_func=func
)
# preserve signature for IDE
functools.update_wrapper(new_method_def, func)
new_namespace[key] = classmethod(new_method_def)
new_namespace[key] = metacls._wrap_class_method(value, key)
else:
new_namespace[key] = value
return new_namespace
Expand All @@ -220,6 +202,35 @@ def new_method(
def _is_extendable(metacls, cls: Type[Any]) -> bool:
return issubclass(type(cls), ExtendableMeta)

@classmethod
def _wrap_class_method(
metacls, method: "AnyClassMethod", method_name: str
) -> "AnyClassMethod":
"""Wrap a class method to delegate the call to the final class.
In addition to preserve the signature and the docstring, this
method will also preserve the validation of args and kwargs
against the signature of the initial method at method call.
"""
func = method.__func__

@no_type_check
def new_method(cls, *args, _method_name=None, _initial_func=None, **kwargs):
# ensure that args and kwargs are conform to the
# initial signature
inspect.signature(_initial_func).bind(cls, *args, **kwargs)
try:
return getattr(cls._get_assembled_cls(), _method_name)(*args, **kwargs)
except (RegistryNotInitializedError, KeyError):
return _initial_func(cls, *args, **kwargs)

new_method_def = functools.partial(
new_method, _method_name=method_name, _initial_func=func
)
# preserve signature for IDE
functools.update_wrapper(new_method_def, func)
return classmethod(new_method_def)

@no_type_check
def __call__(cls, *args, **kwargs) -> "ExtendableMeta":
"""Create the aggregated class in place of the original class definition.
Expand Down Expand Up @@ -249,4 +260,8 @@ def _get_assembled_cls(
"""An helper method to get the final class (the aggregated one) for the current
class."""
registry = registry if registry else extendable_registry.get()
if not registry:
raise RegistryNotInitializedError(
"Extendable classes registry is not initialized"
)
return registry[cls.__xreg_name__]

0 comments on commit 12a8aa1

Please sign in to comment.