Skip to content

Commit

Permalink
rename cls attributes and keep backwards compatibility (#7)
Browse files Browse the repository at this point in the history
* rename cls attributes and keep backwards compatibility

- `init_descriptors` to `_init_descriptors_`
- `use_repr` to `_use_repr_`
- `init_subclass_basecls` to `_init_subclass_basecls_`

* fix lint.yaml
  • Loading branch information
PythonFZ committed Dec 7, 2022
1 parent a3d16ff commit 850e56e
Show file tree
Hide file tree
Showing 8 changed files with 90 additions and 38 deletions.
26 changes: 12 additions & 14 deletions .github/workflows/lint.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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__
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "zninit"
version = "0.1.3"
version = "0.1.4"
description = "Descriptor based dataclass implementation"
authors = ["zincwarecode <zincwarecode@gmail.com>"]
license = "Apache-2.0"
Expand Down
17 changes: 15 additions & 2 deletions tests/test_AutomaticInit.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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)
18 changes: 13 additions & 5 deletions tests/test_subclass_automatic.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,25 +17,33 @@ 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"

instance = Parent(name="Test")
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"
2 changes: 1 addition & 1 deletion tests/test_zninit.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@

def test_version():
"""Test the installed version."""
assert zninit.__version__ == "0.1.3"
assert zninit.__version__ == "0.1.4"
59 changes: 45 additions & 14 deletions zninit/core/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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):
Expand All @@ -147,27 +175,30 @@ 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:
if not getattr(inherited.__init__, "uses_auto_init", False):
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):
Expand Down Expand Up @@ -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 = []
Expand Down
2 changes: 2 additions & 0 deletions zninit/descriptor/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down

0 comments on commit 850e56e

Please sign in to comment.