From b8f1a008feb7be5b6148eba728810dc6d2981b34 Mon Sep 17 00:00:00 2001 From: Fabian Zills <46721498+PythonFZ@users.noreply.github.com> Date: Wed, 14 Dec 2022 14:23:49 +0100 Subject: [PATCH] allow for custom repr function + bump version (#13) * allow for custom repr function + bump version * remove '_get_descriptors' * make private classmethods functions * improve readability --- pyproject.toml | 2 +- tests/test_u_core.py | 25 ++++- tests/test_zninit.py | 2 +- zninit/core/__init__.py | 176 +++++++++++++++++----------------- zninit/descriptor/__init__.py | 4 + 5 files changed, 119 insertions(+), 90 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index ab784a8..83c83e6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "zninit" -version = "0.1.7" +version = "0.1.8" description = "Descriptor based dataclass implementation" authors = ["zincwarecode "] license = "Apache-2.0" diff --git a/tests/test_u_core.py b/tests/test_u_core.py index f883c09..59fda63 100644 --- a/tests/test_u_core.py +++ b/tests/test_u_core.py @@ -1,6 +1,6 @@ """'ZnInit' unit tests.""" from zninit import Descriptor, ZnInit -from zninit.core import get_args_type_error, get_init_type_error +from zninit.core import _get_auto_init_kwargs, get_args_type_error, get_init_type_error class ClsDefaultMixed(ZnInit): @@ -22,7 +22,7 @@ class DoNotUseRepr(ZnInit): def test_get_auto_init_kwargs(): """Test auto init kwargs.""" - kwargs_no_default, kwargs_with_default = ClsDefaultMixed._get_auto_init_kwargs() + kwargs_no_default, kwargs_with_default = _get_auto_init_kwargs(ClsDefaultMixed) assert kwargs_no_default == ["param1"] assert kwargs_with_default == {"param2": "World"} @@ -97,3 +97,24 @@ def test_repr(): assert repr(DoNotUseRepr(param1="Hello", param2="World")).startswith( "") + + +def test_custom_repr(): + """Test the __repr__.""" + + def do_something(): + pass + + instance = CustomRepr(param1="Hello", method=do_something) + assert ( + repr(instance) + == "CustomRepr(method=, param1='Custom:" + " Hello')" + ) diff --git a/tests/test_zninit.py b/tests/test_zninit.py index dda70c3..5f324d5 100644 --- a/tests/test_zninit.py +++ b/tests/test_zninit.py @@ -5,4 +5,4 @@ def test_version(): """Test the installed version.""" - assert zninit.__version__ == "0.1.7" + assert zninit.__version__ == "0.1.8" diff --git a/zninit/core/__init__.py b/zninit/core/__init__.py index 98d9d5f..81cf17e 100644 --- a/zninit/core/__init__.py +++ b/zninit/core/__init__.py @@ -114,6 +114,91 @@ def auto_init(self, *args, **kwargs): return auto_init +def _update_init(cls, super_init): + """Set the automatic __init__. + + Parameters + ---------- + cls: + the cls to be updated + super_init: + run a super call if required + + Returns + ------- + the updated cls instance + + """ + kwargs_no_default, kwargs_with_default = _get_auto_init_kwargs(cls) + signature_params = _get_auto_init_signature(cls) + + # Add new __init__ to the subclass + setattr( + cls, + "__init__", + get_auto_init(kwargs_no_default, kwargs_with_default, super_init=super_init), + ) + + # Add new __signature__ to the subclass + signature = Signature(parameters=signature_params) + setattr(cls, "__signature__", signature) + + return cls + + +def _get_auto_init_kwargs(cls) -> (list, dict): + """Get the keywords for the __init__. + + Collect keywords with and without default values for the init + """ + kwargs_no_default = [] + kwargs_with_default = {} + + for descriptor in get_descriptors( + descriptor=object.__new__(cls)._init_descriptors_, # pylint: disable=W0212 + cls=cls, + ): + # For the new __init__ + if descriptor.default is Empty: + kwargs_no_default.append(descriptor.name) + else: + kwargs_with_default[descriptor.name] = deepcopy(descriptor.default) + + return kwargs_no_default, kwargs_with_default + + +def _get_auto_init_signature(cls) -> (list, dict, list): + """Iterate over ZnTrackOptions in the __dict__ and save the option name. + + and create a signature Parameter + + Returns + ------- + kwargs_no_default: list + a list of names that will be converted to kwargs + kwargs_with_default: dict + a dict of {name: default} that will be converted to kwargs + signature_params: inspect.Parameter + """ + signature_params = [] + cls_annotations = cls.__annotations__ # pylint: disable=no-member + # fix for https://bugs.python.org/issue46930 + for descriptor in get_descriptors( + descriptor=object.__new__(cls)._init_descriptors_, # pylint: disable=W0212 + cls=cls, + ): + # For the new __signature__ + signature_params.append( + Parameter( + # default=... + name=descriptor.name, + kind=Parameter.POSITIONAL_OR_KEYWORD, + annotation=cls_annotations.get(descriptor.name), + ) + ) + return signature_params + + class ZnInit: # pylint: disable=R0903 """Parent class for automatic __init__ generation based on descriptors. @@ -177,90 +262,7 @@ def __init_subclass__(cls, **kwargs): log.debug( f"Found {_init_subclass_basecls_} instance - adding dataclass-like __init__" ) - return cls._update_init(super_init=_init_subclass_basecls_.__init__) - - @classmethod - def _get_descriptors(cls): - return get_descriptors(descriptor=object.__new__(cls)._init_descriptors_, cls=cls) - - @classmethod - def _update_init(cls, super_init): - """Set the automatic __init__. - - Parameters - ---------- - cls: - the cls to be updated - super_init: - run a super call if required - - Returns - ------- - the updated cls instance - - """ - kwargs_no_default, kwargs_with_default = cls._get_auto_init_kwargs() - signature_params = cls._get_auto_init_signature() - - # Add new __init__ to the subclass - setattr( - cls, - "__init__", - get_auto_init(kwargs_no_default, kwargs_with_default, super_init=super_init), - ) - - # Add new __signature__ to the subclass - signature = Signature(parameters=signature_params) - setattr(cls, "__signature__", signature) - - return cls - - @classmethod - def _get_auto_init_kwargs(cls) -> (list, dict): - """Get the keywords for the __init__. - - Collect keywords with and without default values for the init - """ - kwargs_no_default = [] - kwargs_with_default = {} - - for descriptor in cls._get_descriptors(): - # For the new __init__ - if descriptor.default is Empty: - kwargs_no_default.append(descriptor.name) - else: - kwargs_with_default[descriptor.name] = deepcopy(descriptor.default) - - return kwargs_no_default, kwargs_with_default - - @classmethod - def _get_auto_init_signature(cls) -> (list, dict, list): - """Iterate over ZnTrackOptions in the __dict__ and save the option name. - - and create a signature Parameter - - Returns - ------- - kwargs_no_default: list - a list of names that will be converted to kwargs - kwargs_with_default: dict - a dict of {name: default} that will be converted to kwargs - signature_params: inspect.Parameter - """ - signature_params = [] - cls_annotations = cls.__annotations__ # pylint: disable=no-member - # fix for https://bugs.python.org/issue46930 - for descriptor in cls._get_descriptors(): - # For the new __signature__ - signature_params.append( - Parameter( - # default=... - name=descriptor.name, - kind=Parameter.POSITIONAL_OR_KEYWORD, - annotation=cls_annotations.get(descriptor.name), - ) - ) - return signature_params + return _update_init(cls=cls, super_init=_init_subclass_basecls_.__init__) def __repr__(self): """Get a dataclass like representation of the ZnInit class.""" @@ -268,10 +270,12 @@ def __repr__(self): return super().__repr__() repr_str = f"{self.__class__.__name__}(" fields = [] - for descriptor in self._get_descriptors(): + for descriptor in get_descriptors(descriptor=self._init_descriptors_, self=self): if not descriptor.use_repr: continue - fields.append(f"{descriptor.name}={repr(getattr(self, descriptor.name))}") + representation = descriptor.get_repr(getattr(self, descriptor.name)) + + fields.append(f"{descriptor.name}={representation}") repr_str += ", ".join(fields) repr_str += ")" return repr_str diff --git a/zninit/descriptor/__init__.py b/zninit/descriptor/__init__.py index ab1b634..8d9bc3c 100644 --- a/zninit/descriptor/__init__.py +++ b/zninit/descriptor/__init__.py @@ -53,6 +53,7 @@ def __init__( instance=None, name="", use_repr: bool = True, + repr_func: typing.Callable = repr, check_types: bool = False, metadata: dict = None, frozen: bool = False, @@ -78,6 +79,8 @@ def __init__( Freeze the attribute after the first __set__ call. metadata: dict, default=None additional metadata for the descriptor. + repr_func: Callable, default=repr + A callable that will be used to compute the _repr_ of the descriptor. """ self._default = default self._owner = owner @@ -87,6 +90,7 @@ def __init__( self.check_types = check_types self.metadata = metadata or {} self.frozen = frozen + self.get_repr = repr_func self._frozen = weakref.WeakKeyDictionary() if check_types and ("typeguard" not in sys.modules): raise ImportError(