Skip to content

Commit

Permalink
feature: implement simple type alias = hint (3.12+)
Browse files Browse the repository at this point in the history
  • Loading branch information
autumnjolitz committed Jul 27, 2024
1 parent 67d59fd commit e9e0ac7
Show file tree
Hide file tree
Showing 4 changed files with 136 additions and 31 deletions.
78 changes: 48 additions & 30 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -21,40 +21,43 @@ This girl asks for a lot but I like taking metaclassing as far as it can go with

Current Capabilities:

- Support multiple inheritance, chained fields and ``__slots__`` [Done]
- Support type coercions (via ``_coerce__``) [Done]
- Strictly-typed ability to define fixed data objects [Done]
- Ability to drop all of the above type checks [Done]
- Track changes made to the object as well as reset [Done]
- Fast ``__iter__`` [Done]
- Native support of pickle [Done]/json [Done]
- Support List[type] declarations and initializations [Done]
- optionally data class annotation-like behavior [Done]
- ``_asdict``, ``_astuple``, ``_aslist`` functions like in a NamedTuple [Done]
- Support multiple inheritance, chained fields and ``__slots__``
- Support type coercions (via ``_coerce__``)
- Strictly-typed ability to define fixed data objects
- Ability to drop all of the above type checks
- Track changes made to the object as well as reset
- Fast ``__iter__``
- Native support of pickle/json
- Support List[type] declarations and initializations
- optionally data class annotation-like behavior
- ``_asdict``, ``_astuple``, ``_aslist`` functions like in a NamedTuple
- ``get``, ``keys``, ``values``, ``item`` functions available in the module and in a mixin named ``mapping=True``
+ This effectively allows access like other packages e.g. ``attrs.keys(item_instance)``
- ``bytes``/``bytearray`` are urlsafe base64 encoded by default, can override per field via a class level ``BINARY_JSON_ENCODERS = {key: encoding_function}`` [Done]
- Allow ``__coerce__`` to have a tuple of field names to avoid repetition on ``__coerce__`` definitions [Done]
- Allow use of ``Literal`` in the type (exact match of a value to a vector of values) [Done]
- Allow subtraction of properties like ``(F - {"a", "b"}).keys() == F_without_a_b.keys()`` [Done]
- ``bytes``/``bytearray`` are urlsafe base64 encoded by default, can override per field via a class level ``BINARY_JSON_ENCODERS = {key: encoding_function}``
- Allow ``__coerce__`` to have a tuple of field names to avoid repetition on ``__coerce__`` definitions
- Allow use of ``Literal`` in the type (exact match of a value to a vector of values)
- Allow subtraction of properties like ``(F - {"a", "b"}).keys() == F_without_a_b.keys()``
+ This will allow one to slim down a class to a restricted subtype, like for use in a DAO system to load/hold less data.
- Allow subtraction of properties like ``(F - {"a": {"b"}).keys() == F_a_without_b.keys()`` [Done]
- Allow subtraction of properties like ``(F - {"a": {"b"}).keys() == F_a_without_b.keys()``
+ This allows for one to remove fields that are unused prior to class initialization.
- Allow subtraction of properties via an inclusive list like ``(F & {"a", "b"}).keys() == F_with_only_a_and_b.keys()`` [Done]
- Allow subtraction to propagate to embedded Instruct classes like ``(F - {"a.b", "a.c"}).a.keys() == (F_a.keys() - {"b", "c"))`` [Done]
- Allow subtraction of properties via an inclusive list like ``(F & {"a", "b"}).keys() == F_with_only_a_and_b.keys()``
- Allow subtraction to propagate to embedded Instruct classes like ``(F - {"a.b", "a.c"}).a.keys() == (F_a.keys() - {"b", "c"))``
+ This would really allow for complex trees of properties to be rendered down to thin SQL column selects, thus reducing data load.
- Replace references to an embedded class in a ``__coerce__`` function with the subtracted form in case of embedded property subtractions [Done]
- Allow use of Annotated i.e. ``field: Annotated[int, NoJSON, NoPickle]`` and have ``to_json`` and ``pickle.dumps(...)`` skip "field" [Done]
+ Would grant a more powerful interface to controlling code-gen'ed areas via ``cls._annotated_metadata`` (maps field -> what's inside the ``Annotation``) [Done]
- Allow Generics i.e. ``class F(instruct.Base, Generic[T]): ...`` -> ``F[str](...)``
- ✅ Replace references to an embedded class in a ``__coerce__`` function with the subtracted form in case of embedded property subtractions
- ✅ Allow use of Annotated i.e. ``field: Annotated[int, NoJSON, NoPickle]`` and have ``to_json`` and ``pickle.dumps(...)`` skip "field"
+ interface to controlling code-gen'ed areas via ``cls._annotated_metadata`` (maps field -> what's inside the ``Annotation``)
- 🚧 Allow Generics i.e. ``class F(instruct.Base, Generic[T]): ...`` -> ``F[str](...)``
- 🚧 ``TypeAliasType`` support (Python 3.12+)
+ ``type i = int | str`` is resolved to ``int | str``


Next Goals:

=======

- ``CStruct``-Base class that operates on an ``_cvalue`` cffi struct.
- Cython compatibility

- Recursive ``TypeAliasType`` / ``ForwardRef``

Design Goal
-------------
Expand Down Expand Up @@ -357,6 +360,28 @@ Such a graph may look like this::
Now it is possible for any valid multiple-inheritance chain to proceed, provided it respects the above constraints - there are either support classes or data classes (denoted with an underscore in front of their class name). Support classes may be inherited from, data classes cannot.
Development
-------------
Tests
^^^^^^^
::
$ invoke test
Release Process
^^^^^^^^^^^^^^^^^
::
$ invoke create-release
$ invoke create-release [--version x.y.z]
$ invoke create-release [--version x.y.z] [--next-version x.y.z+1]
Solving the Slowness issue
*****************************
Expand All @@ -367,21 +392,14 @@ Callgraph Performance
.. class:: no-web
.. image:: https://raw.githubusercontent.com/benjolitz/Instruct/master/callgraph.png
.. image:: https://raw.githubusercontent.com/autumnjolitz/Instruct/master/callgraph.png
:alt: Callgraph of project
:width: 100%
:align: center
.. class:: no-web no-pdf
Release Process
-----------------
::
$ rm -rf dist/* && python -m pytest tests/ && python setup.py sdist bdist_wheel && twine upload dist/*
Benchmark
--------------
Expand Down
31 changes: 30 additions & 1 deletion instruct/typedef.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
Set,
Dict,
cast as cast_type,
TYPE_CHECKING,
)
from typing_extensions import (
get_origin as _get_origin,
Expand All @@ -59,6 +60,21 @@
T = TypeVar("T")
U = TypeVar("U")

_has_typealiastype: bool = False

if sys.version_info >= (3, 12):
from typing import TypeAliasType

_has_typealiastype = True
else:

class TypeAliasType:
__slots__ = ("__value__",)
__value__: TypeHint

def __new__(cls):
raise NotImplementedError


if sys.version_info >= (3, 11):
from typing import TypeVarTuple, Unpack
Expand Down Expand Up @@ -1101,6 +1117,17 @@ def wrapped(*args, **kwargs):
return wrapped


if _has_typealiastype:

def is_typealiastype(o: Any) -> TypeGuard[TypeAliasType]:
return isinstance(o, TypeAliasType)

else:

def is_typealiastype(o: Any) -> TypeGuard[TypeAliasType]:
return False


@assert_never_null
def parse_typedef(
typedef: Union[Tuple[Type, ...], List[Type]], *, check_ranges: Tuple[Range, ...] = ()
Expand Down Expand Up @@ -1213,7 +1240,9 @@ def parse_typedef(
metaclass.set_type(cls, typehint)
return cls
return as_origin_cls

if _has_typealiastype:
if is_typealiastype(typehint):
return parse_typedef(typehint.__value__)
raise NotImplementedError(
f"The type definition for {typehint!r} ({type(typehint)}) is not supported yet, report as an issue."
)
41 changes: 41 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import sys
import builtins
import inspect
from contextlib import suppress
from pathlib import Path
from functools import wraps

import pytest

repository = Path(__file__).resolve(True).parent.parent


@wraps(builtins.print)
def print(*args, **kwargs) -> None:
s, *rest = args
if not isinstance(s, str):
rest = (s, *rest)
s = ""
stack = inspect.stack()[1:]
caller, *stack = stack
module_name = caller.frame.f_globals["__name__"]
prefix = f"[{module_name}.{caller.function}]"
with suppress(KeyError):
module_name = caller.frame.f_locals["__name__"]
if s:
s = f"{prefix} {s}"
return builtins.print(s or prefix, *rest, **kwargs)


def pytest_ignore_collect(collection_path: Path, path, config: pytest.Config) -> bool:
with suppress(ValueError):
_, version = collection_path.stem.rsplit("_", 1)
major, minor = int(version[:1], 10), int(version[1:], 10)
if sys.version_info[:2] < (major, minor):
print(
f"Skipping {collection_path.relative_to(repository)!s} "
f"(python {sys.version_info[:2]} < ({major}, {minor}))",
file=sys.stderr,
)
return True
return False
17 changes: 17 additions & 0 deletions tests/test_typedef_312.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from instruct.typedef import parse_typedef

import pytest


def test_parse_typealiastype():
type i = int
assert parse_typedef(i) is int
type b = str | int
assert {*parse_typedef(b)} == {str, int}
type c = i | b
assert {*parse_typedef(c)} == {str, int}

type d = dict[str, d]

with pytest.raises(RecursionError):
parse_typedef(d)

0 comments on commit e9e0ac7

Please sign in to comment.