diff --git a/README.rst b/README.rst index 7063c12..1496a3c 100644 --- a/README.rst +++ b/README.rst @@ -21,32 +21,35 @@ 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: @@ -54,7 +57,7 @@ Next Goals: - ``CStruct``-Base class that operates on an ``_cvalue`` cffi struct. - Cython compatibility - +- Recursive ``TypeAliasType`` / ``ForwardRef`` Design Goal ------------- @@ -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 ***************************** @@ -367,7 +392,7 @@ 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 @@ -375,13 +400,6 @@ Callgraph Performance .. class:: no-web no-pdf -Release Process ------------------ - -:: - - $ rm -rf dist/* && python -m pytest tests/ && python setup.py sdist bdist_wheel && twine upload dist/* - Benchmark -------------- diff --git a/instruct/typedef.py b/instruct/typedef.py index b9c4793..24a48c4 100644 --- a/instruct/typedef.py +++ b/instruct/typedef.py @@ -33,6 +33,7 @@ Set, Dict, cast as cast_type, + TYPE_CHECKING, ) from typing_extensions import ( get_origin as _get_origin, @@ -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 @@ -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, ...] = () @@ -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." ) diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..ead9ac7 --- /dev/null +++ b/tests/conftest.py @@ -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 diff --git a/tests/test_typedef_312.py b/tests/test_typedef_312.py new file mode 100644 index 0000000..2a19fa8 --- /dev/null +++ b/tests/test_typedef_312.py @@ -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)