Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add support for type checkers using @dataclass_transform from PEP 681… #26

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
Open
3 changes: 2 additions & 1 deletion zninit/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@

from zninit.core import ZnInit
from zninit.descriptor import Descriptor, Empty, get_descriptors
from zninit.descriptor.desc import desc

__all__ = ("Descriptor", "ZnInit", "get_descriptors", "Empty")
__all__ = ("Descriptor", "ZnInit", "get_descriptors", "Empty", "desc")

__version__ = importlib.metadata.version("zninit")
15 changes: 15 additions & 0 deletions zninit/core/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,26 @@
from __future__ import annotations

import logging
import sys
import typing
from copy import deepcopy
from inspect import Parameter, Signature

from zninit.descriptor import Descriptor, Empty, get_descriptors

if sys.version_info >= (3, 11):
from typing import dataclass_transform
else:

def dataclass_transform(*args, **kwargs):
"""Empty decorator for Python < 3.11 support."""

def decorator(func):
return func

return decorator


log = logging.getLogger(__name__)


Expand Down Expand Up @@ -258,6 +272,7 @@ def _get_auto_init_signature(cls) -> typing.Tuple[list, dict, list]:
return signature_params


@dataclass_transform(field_specifiers=(Descriptor,))
class ZnInit: # pylint: disable=R0903
"""Parent class for automatic __init__ generation based on descriptors.

Expand Down
13 changes: 10 additions & 3 deletions zninit/descriptor/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from __future__ import annotations

import contextlib
import enum
import functools
import sys
import typing
Expand All @@ -12,13 +13,19 @@
import typeguard


class Empty: # pylint: disable=too-few-public-methods
# See https://github.com/python/cpython/blob/main/Lib/dataclasses.py#L181.
class _Empty_TYPE(enum.Enum): # pylint: disable=too-few-public-methods
"""ZnInit Version of None to distinguish default version from None.

When checking if something has a default we can not use 'value is None'
because 'None' could be the default. Therefore, we use 'value is zninit.Empty'
"""

Empty = enum.auto()


Empty = _Empty_TYPE.Empty


class Descriptor: # pylint: disable=too-many-instance-attributes
"""Simple Python Descriptor that allows adding.
Expand Down Expand Up @@ -56,9 +63,9 @@ def __init__(
use_repr: bool = True,
repr_func: typing.Callable = repr,
check_types: bool = False,
metadata: dict = None,
metadata: typing.Optional[dict] = None,
frozen: bool = False,
on_setattr: typing.Callable = None,
on_setattr: typing.Optional[typing.Callable] = None,
): # pylint: disable=too-many-arguments
"""Define a Descriptor object.

Expand Down
37 changes: 37 additions & 0 deletions zninit/descriptor/desc.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
"""Provide a typed factory function for descriptors."""

from typing import Callable, Optional

from zninit.descriptor import Descriptor, Empty


def desc(
default=Empty,
owner=None,
instance=None,
name="",
use_repr: bool = True,
repr_func: Callable = repr,
check_types: bool = False,
metadata: Optional[dict] = None,
frozen: bool = False,
on_setattr: Optional[Callable] = None,
):
"""Create a Descriptor object.

Forwards all arguments to the Descriptor.__init__ method.
The return type is annotated as the type of the managed attribute
to enable dataclass semantics, see stub file desc.pyi.
"""
return Descriptor(
default=default,
owner=owner,
instance=instance,
name=name,
use_repr=use_repr,
repr_func=repr_func,
check_types=check_types,
metadata=metadata,
frozen=frozen,
on_setattr=on_setattr,
)
43 changes: 43 additions & 0 deletions zninit/descriptor/desc.pyi
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
from typing import Any, Callable, Literal, Optional, TypeVar, overload

from zninit.descriptor import _Empty_TYPE

_T = TypeVar("_T")

# In reality, desc returns a Descriptor object. We lie about this
# and pretend it returns an object of the type of the managed attribute
# to enable dataclass semantics with type checkers, i.e.:
#
# class Human(ZnInit):
# age: int = desc(0)

# If no default value is given, we pretend the return type is Any.
# The actual type will be inferred from the annotation in the class.
@overload
def desc(
default: Literal[_Empty_TYPE.Empty] = ...,
owner=...,
instance=...,
name=...,
use_repr: bool = ...,
repr_func: Callable = ...,
check_types: bool = False,
metadata: Optional[dict] = ...,
frozen: bool = False,
on_setattr: Optional[Callable] = ...,
) -> Any: ...

# If a default value is given, we pretend its type is the return type.
@overload
def desc(
default: _T,
owner=...,
instance=...,
name=...,
use_repr: bool = ...,
repr_func: Callable = ...,
check_types: bool = False,
metadata: Optional[dict] = ...,
frozen: bool = False,
on_setattr: Optional[Callable] = ...,
) -> _T: ...
Empty file added zninit/py.typed
Empty file.