diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index d496680..33ba755 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -27,22 +27,20 @@ jobs: - name: run isort run: | isort --check-only --quiet . - flake8: + ruff: runs-on: ubuntu-latest steps: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - name: Install Python - uses: actions/setup-python@v4 - with: - python-version: "3.10" - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install ruff - - name: Run Ruff - run: ruff . + - uses: actions/checkout@v3 + - name: Install Python + uses: actions/setup-python@v4 + with: + python-version: "3.10" + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install ruff + - name: Run Ruff + run: ruff . pylint: runs-on: ubuntu-latest diff --git a/README.md b/README.md index 4094988..e66f3f6 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ class Metric(Descriptor): class Human(ZnInit): - init_descriptors = [Input] # only add Input descriptors to the __init__ + _init_descriptors_ = [Input] # only add Input descriptors to the __init__ name: str = Input() language: str = Input("DE") date: str = Metric() # will not appear in the __init__ diff --git a/pyproject.toml b/pyproject.toml index 145de8a..1872654 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "zninit" -version = "0.1.3" +version = "0.1.4" description = "Descriptor based dataclass implementation" authors = ["zincwarecode "] license = "Apache-2.0" diff --git a/tests/test_AutomaticInit.py b/tests/test_AutomaticInit.py index be624d0..da9437d 100644 --- a/tests/test_AutomaticInit.py +++ b/tests/test_AutomaticInit.py @@ -65,6 +65,15 @@ class ChildCls(ParentCls): class OnlyParamsInInit(ZnInit): """ZnInit child.""" + _init_descriptors_ = [Params] + + parameter: int = Params() + output = Outs() + + +class OnlyParamsInInitOld(ZnInit): + """ZnInit child.""" + init_descriptors = [Params] parameter: int = Params() @@ -180,9 +189,13 @@ def test_DefaultIsNone(): assert instance.parameter == 42 -def test_OnlyParamsInInit(): +@pytest.mark.parametrize("cls", (OnlyParamsInInit, OnlyParamsInInitOld)) +def test_OnlyParamsInInit(cls): """ZnInit Test.""" - instance = OnlyParamsInInit(parameter=10) + instance = cls(parameter=10) assert instance.parameter == 10 with pytest.raises(AttributeError): _ = instance.output + + with pytest.raises(TypeError): + cls(parameter=10, output=25) diff --git a/tests/test_subclass_automatic.py b/tests/test_subclass_automatic.py index 77a8ae5..d725e2d 100644 --- a/tests/test_subclass_automatic.py +++ b/tests/test_subclass_automatic.py @@ -17,13 +17,21 @@ def __init__(self, **kwargs): class Child(Parent): """Child class.""" + _init_subclass_basecls_ = Parent + text = Descriptor() + + +class ChildOld(Parent): + """Child class.""" + init_subclass_basecls = Parent text = Descriptor() -def test_subclass_init(): +@pytest.mark.parametrize("cls", (Child, ChildOld)) +def test_subclass_init(cls): """Test subclass init.""" - instance = Child(name="Test", text="Hello World") + instance = cls(name="Test", text="Hello World") assert instance.name == "Test" assert instance.text == "Hello World" @@ -31,11 +39,11 @@ def test_subclass_init(): assert instance.name == "Test" with pytest.raises(TypeError): - _ = Child(name="Test", text="Hello World", data="Lorem Ipsum") + _ = cls(name="Test", text="Hello World", data="Lorem Ipsum") with pytest.raises(TypeError): - _ = Child(name="Test") + _ = cls(name="Test") - instance = Child(text="Hello World") + instance = cls(text="Hello World") assert instance.name is None assert instance.text == "Hello World" diff --git a/tests/test_zninit.py b/tests/test_zninit.py index 341b62a..c127c47 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.3" + assert zninit.__version__ == "0.1.4" diff --git a/zninit/core/__init__.py b/zninit/core/__init__.py index b112c02..73b01e8 100644 --- a/zninit/core/__init__.py +++ b/zninit/core/__init__.py @@ -88,7 +88,7 @@ def auto_init(self, *args, **kwargs): raise get_args_type_error(args, cls_name, uses_auto_init) log.debug(f"The '__init__' uses auto_init: {uses_auto_init}") for kwarg_name in kwargs_no_default: - try: # pylint: disable=loop-try-except-usage + try: # pylint: disable=R8203 init_kwargs[kwarg_name] = kwargs.pop(kwarg_name) except KeyError: required_keys.append(kwarg_name) @@ -114,25 +114,53 @@ def auto_init(self, *args, **kwargs): return auto_init -class ZnInit: +def update_attribute_names(cls): + """Update changed attribute names. + + E.g. 'init_descriptors' was renamed to '_init_descriptors_' but should be + backwards compatible. This was done according to PEP8 style guide where + '_single_leading_underscore' are meant for weak internal usage. + """ + if cls.init_descriptors is not None: + cls._init_descriptors_ = cls.init_descriptors # pylint: disable=W0212 + if cls.use_repr is not None: + cls._use_repr_ = cls.use_repr # pylint: disable=W0212 + if cls.init_subclass_basecls is not None: + cls._init_subclass_basecls_ = cls.init_subclass_basecls # pylint: disable=W0212 + + +class Meta(type): + """Metaclass to 'update_attribute_names'.""" + + def __new__(cls, *args, **kwargs): + meta_cls = super().__new__(cls, *args, **kwargs) + update_attribute_names(meta_cls) + return meta_cls + + +class ZnInit(metaclass=Meta): """Parent class for automatic __init__ generation based on descriptors. Attributes ---------- - init_descriptors: list + _init_descriptors_: list A list of the descriptor classes to be added to the init. This also supports subclasses of Descriptor. - use_repr: bool + _use_repr_: bool Generate an automatic, dataclass like representation string. - init_subclass_basecls: object + _init_subclass_basecls_: object Any class (not an instance) that acts as the lower bound for searching an __init__ method. If the __init__ of this class is reached when iterating over the mro, an automatic __init__ method will be generated and the __init__ of the basecls will be called via super. """ - init_descriptors: typing.List[Descriptor] = [Descriptor] - use_repr: bool = True + _init_descriptors_: typing.List[Descriptor] = [Descriptor] + _use_repr_: bool = True + _init_subclass_basecls_ = None + + init_descriptors: typing.List[Descriptor] = None + use_repr: bool = None init_subclass_basecls = None def __init__(self): @@ -147,13 +175,14 @@ def __init__(self): def __init_subclass__(cls, **kwargs): super().__init_subclass__(**kwargs) - if cls.init_subclass_basecls is None: - cls.init_subclass_basecls = ZnInit + update_attribute_names(cls) + if cls._init_subclass_basecls_ is None: + cls._init_subclass_basecls_ = ZnInit for inherited in cls.__mro__: # Go through the mro until you find the init_subclass_basecls. # If found an init before that class it will implement super # if not add the fields to the __init__ automatically. - if inherited == cls.init_subclass_basecls: + if inherited == cls._init_subclass_basecls_: break if inherited.__dict__.get("__init__") is not None: @@ -161,13 +190,15 @@ def __init_subclass__(cls, **kwargs): return cls log.debug( - f"Found {cls.init_subclass_basecls} instance - adding dataclass-like __init__" + f"Found {cls._init_subclass_basecls_} instance - adding dataclass-like" + " __init__" ) - return cls._update_init(super_init=cls.init_subclass_basecls.__init__) + return cls._update_init(super_init=cls._init_subclass_basecls_.__init__) @classmethod def _get_descriptors(cls): - return get_descriptors(descriptor=cls.init_descriptors, cls=cls) + update_attribute_names(cls) + return get_descriptors(descriptor=cls._init_descriptors_, cls=cls) @classmethod def _update_init(cls, super_init): @@ -250,7 +281,7 @@ def _get_auto_init_signature(cls) -> (list, dict, list): def __repr__(self): """Get a dataclass like representation of the ZnInit class.""" - if not self.use_repr: + if not self._use_repr_: return super().__repr__() repr_str = f"{self.__class__.__name__}(" fields = [] diff --git a/zninit/descriptor/__init__.py b/zninit/descriptor/__init__.py index 187d05e..64e9868 100644 --- a/zninit/descriptor/__init__.py +++ b/zninit/descriptor/__init__.py @@ -188,6 +188,8 @@ def get_descriptors( a list of the found descriptor objects """ + if descriptor is None: + return [] if self is None and cls is None: raise ValueError("Either self or cls must not be None") if self is not None and cls is not None: