From 21b5ae81cab293fc3d004ee55e85c2e3eb6f8a21 Mon Sep 17 00:00:00 2001 From: Maxim Avanov <601955+avanov@users.noreply.github.com> Date: Wed, 8 Mar 2023 19:46:28 +0000 Subject: [PATCH] Release 3.11.0.0 (#80) * Release 3.11.0.0 --- .github/workflows/ci.yml | 4 ++- CHANGELOG.rst | 6 ++++ default.nix | 58 ++++++++++++++++++++++++++++++++++++++ nixpkgs/default.nix | 19 +++++++++++++ nixpkgs/overlays.nix | 13 +++++++++ requirements/minimal.txt | 8 +++--- requirements/test.txt | 6 ++-- setup.py | 2 +- shell.nix | 58 ++------------------------------------ tests/test_pyrsistent.py | 6 ++-- tests/test_utils.py | 16 ++++++++++- typeit/parser/__init__.py | 46 +++++++++++++----------------- typeit/parser/type_info.py | 15 ++++++++++ typeit/utils.py | 29 +++++++++++++++++-- 14 files changed, 190 insertions(+), 96 deletions(-) create mode 100644 default.nix create mode 100644 nixpkgs/default.nix create mode 100644 nixpkgs/overlays.nix create mode 100644 typeit/parser/type_info.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d98ad1f..0dc20ac 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,7 +9,9 @@ jobs: tests: strategy: matrix: - python-version: [ 310 ] + python-version: + - 310 + - 311 runs-on: ubuntu-latest steps: - uses: actions/checkout@v2.4.0 diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 04ac17f..d0ab758 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -2,6 +2,12 @@ CHANGELOG ========= +3.11.0.0 +======== + +* Support for Python 3.11 + + 3.9.1.9 ======= diff --git a/default.nix b/default.nix new file mode 100644 index 0000000..5ebd826 --- /dev/null +++ b/default.nix @@ -0,0 +1,58 @@ +{ pyVersion ? "311" +, isDevEnv ? true +}: + +let + commonEnv = import ./nixpkgs {}; + pkgs = commonEnv.pkgs; + + python = pkgs."python${pyVersion}"; + pythonPkgs = pkgs."python${pyVersion}Packages"; + devLibs = if isDevEnv then [ pythonPkgs.twine pythonPkgs.wheel ] else [ pythonPkgs.coveralls ]; + + # Make a new "derivation" that represents our shell + devEnv = pkgs.mkShellNoCC { + name = "typeit"; + + # The packages in the `buildInputs` list will be added to the PATH in our shell + # Python-specific guide: + # https://github.com/NixOS/nixpkgs/blob/master/doc/languages-frameworks/python.section.md + nativeBuildInputs = with pkgs; [ + # see https://nixos.org/nixos/packages.html + # Python distribution + python + pythonPkgs.virtualenv + ncurses + libxml2 + libxslt + libzip + zlib + which + ] ++ devLibs; + shellHook = '' + # set SOURCE_DATE_EPOCH so that we can use python wheels + export SOURCE_DATE_EPOCH=$(date +%s) + + export VENV_DIR="$PWD/.venv${pyVersion}" + + export PATH=$VENV_DIR/bin:$PATH + export PYTHONPATH="" + export LANG=en_US.UTF-8 + + # https://python-poetry.org/docs/configuration/ + export PIP_CACHE_DIR="$PWD/.local/pip-cache${pyVersion}" + + # Setup virtualenv + if [ ! -d $VENV_DIR ]; then + virtualenv $VENV_DIR + pip install -r requirements/minimal.txt + pip install -r requirements/test.txt + pip install -r requirements/extras/third_party.txt + fi + ''; + }; +in + +{ + inherit devEnv; +} diff --git a/nixpkgs/default.nix b/nixpkgs/default.nix new file mode 100644 index 0000000..ae554c3 --- /dev/null +++ b/nixpkgs/default.nix @@ -0,0 +1,19 @@ +{}: + +let + +common-src = builtins.fetchTarball { + name = "common-2023-03-03"; + url = https://github.com/avanov/nix-common/archive/a30f466f3ac73842d111e80f40f287d8aa13e929.tar.gz; + # Hash obtained using `nix-prefetch-url --unpack ` + sha256 = "sha256:1dimd334ay4jx4n81n5ms8p4i9kpyn0z7mm8xa0kcy2cpdlbq798"; +}; + +overlays = import ./overlays.nix {}; +pkgs = (import common-src { projectOverlays = [ overlays.globalPackageOverlay ]; }).pkgs; + +in + +{ + inherit pkgs; +} diff --git a/nixpkgs/overlays.nix b/nixpkgs/overlays.nix new file mode 100644 index 0000000..e2c358f --- /dev/null +++ b/nixpkgs/overlays.nix @@ -0,0 +1,13 @@ +{}: + +let + +globalPackageOverlay = (self: original: rec { + # Place overrides here as described in https://nixos.wiki/wiki/Overlays#Examples_of_overlays +}); + +in + +{ + inherit globalPackageOverlay; +} diff --git a/requirements/minimal.txt b/requirements/minimal.txt index 8d8039c..94855bc 100644 --- a/requirements/minimal.txt +++ b/requirements/minimal.txt @@ -1,5 +1,5 @@ inflection>=0.4.0,<0.6.0 -colander>=1.7.0,<1.9.0 -pyrsistent>=0.18.1,<0.19 -typing-inspect>=0.7.1,<0.8.0 -typing-extensions>=4.2,<4.3 \ No newline at end of file +colander>=1.7.0,<2.1 +pyrsistent>=0.19.3 +typing-inspect>=0.7.1,<0.9 +typing-extensions>=4.2,<4.6 \ No newline at end of file diff --git a/requirements/test.txt b/requirements/test.txt index 507081d..44927e5 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -1,8 +1,8 @@ -r ./minimal.txt -pytest>=7.1,<7.2 -coverage>=6.4,<6.5 +pytest>=7.1,<7.3 +coverage>=6.4,<7.3 coveralls>=3.3,<3.4 -pytest-cov>=3.0,<3.1 +pytest-cov>=3.0,<4.1 mypy==0.961 py-money==0.5.0 requests>=2.28 diff --git a/setup.py b/setup.py index 6d1f4a5..2b5dc43 100644 --- a/setup.py +++ b/setup.py @@ -38,7 +38,7 @@ def requirements(at_path: Path): # ---------------------------- setup(name='typeit', - version='3.10.0.0', + version='3.11.0.0', description='typeit brings typed data into your project', long_description=README, classifiers=[ diff --git a/shell.nix b/shell.nix index 660dae5..11c0d2e 100644 --- a/shell.nix +++ b/shell.nix @@ -1,58 +1,4 @@ -{ - pkgs ? (import (builtins.fetchGit { - url = "https://github.com/avanov/nix-common.git"; - ref = "master"; - rev = "be2dc05bf6beac92fc12da9a2adae6994c9f2ee6"; - }) {}).pkgs -, pyVersion ? "310" -, isDevEnv ? true -}: - let - - python = pkgs."python${pyVersion}"; - pythonPkgs = pkgs."python${pyVersion}Packages"; - devLibs = if isDevEnv then [ pythonPkgs.twine pythonPkgs.wheel ] else [ pythonPkgs.coveralls ]; + repository = import ./default.nix {}; in - -# Make a new "derivation" that represents our shell -pkgs.mkShellNoCC { - name = "typeit"; - - # The packages in the `buildInputs` list will be added to the PATH in our shell - # Python-specific guide: - # https://github.com/NixOS/nixpkgs/blob/master/doc/languages-frameworks/python.section.md - nativeBuildInputs = with pkgs; [ - # see https://nixos.org/nixos/packages.html - # Python distribution - python - pythonPkgs.virtualenv - ncurses - libxml2 - libxslt - libzip - zlib - which - ] ++ devLibs; - shellHook = '' - # set SOURCE_DATE_EPOCH so that we can use python wheels - export SOURCE_DATE_EPOCH=$(date +%s) - - export VENV_DIR="$PWD/.venv${pyVersion}" - - export PATH=$VENV_DIR/bin:$PATH - export PYTHONPATH="" - export LANG=en_US.UTF-8 - - # https://python-poetry.org/docs/configuration/ - export PIP_CACHE_DIR="$PWD/.local/pip-cache${pyVersion}" - - # Setup virtualenv - if [ ! -d $VENV_DIR ]; then - virtualenv $VENV_DIR - pip install -r requirements/minimal.txt - pip install -r requirements/test.txt - pip install -r requirements/extras/third_party.txt - fi - ''; -} \ No newline at end of file + repository.devEnv \ No newline at end of file diff --git a/tests/test_pyrsistent.py b/tests/test_pyrsistent.py index a7fbc38..2cb908f 100644 --- a/tests/test_pyrsistent.py +++ b/tests/test_pyrsistent.py @@ -11,13 +11,15 @@ class X(NamedTuple): a: ps.typing.PMap[str, Any] b: ps.typing.PVector[ps.typing.PMap[str, Any]] c: Optional[ps.typing.PMap[str, Any]] + d: Optional[ps.typing.PMap] - mk_x, serialize_x = ty.TypeConstructor ^ X + mk_x, serialize_x = ty.type_constructor ^ X data = { 'a': {'x': 'x', 'y': 'y'}, 'b': [{'x': 'x', 'y': 'y'}], - 'c': None + 'c': None, + 'd': {'a': 1} } x = mk_x(data) assert isinstance(x.a, ps.PMap) diff --git a/tests/test_utils.py b/tests/test_utils.py index f68b737..bb6d0d8 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -75,4 +75,18 @@ class X(NamedTuple): try: serialize_x(x) except typeit.Error as e: - x = list(e) \ No newline at end of file + x = list(e) + + +def test_new_init(): + class X(NamedTuple): + a: int + b: bool + + a = 1 + b = True + res = utils.new(X) + assert res.a == a + assert res.b == b + + res1 = utils.new(X, res) \ No newline at end of file diff --git a/typeit/parser/__init__.py b/typeit/parser/__init__.py index 50956f9..61f84ef 100644 --- a/typeit/parser/__init__.py +++ b/typeit/parser/__init__.py @@ -1,8 +1,7 @@ from types import UnionType from typing import ( Type, Tuple, Optional, Any, Union, List, Set, - Dict, Sequence, get_type_hints, - MutableSet, TypeVar, FrozenSet, Mapping, NamedTuple, ForwardRef, NewType, + Dict, Sequence, MutableSet, TypeVar, FrozenSet, Mapping, ForwardRef, NewType, ) import inspect @@ -12,6 +11,8 @@ from pyrsistent import pmap, pvector from pyrsistent import typing as pyt +from .type_info import get_type_attribute_info, AttrInfo, NoneType + from ..compat import Literal from ..definitions import OverridesT from ..utils import is_named_tuple, clone_schema_node, get_global_name_overrider @@ -25,9 +26,6 @@ T = TypeVar('T') MemoType = TypeVar('MemoType') -NoneType = type(None) - - OverrideT = ( flags._Flag # flag override | TypeExtension # new type extension | Union[ @@ -442,15 +440,23 @@ def _maybe_node_for_dict( if typ in supported_type or get_origin_39(typ) in supported_origin or are_generic_bases_match(generic_bases, supported_origin): schema_node_type = schema.nodes.PMapSchema if is_pmap(typ) else schema.nodes.SchemaNode - if generic_bases: - # python 3.9 args - key_type, value_type = typ.__args__ - else: + def maybe_non_specified_arguments(mappingType): try: - key_type, value_type = insp.get_args(typ) + kt, kv = insp.get_args(mappingType) except ValueError: # Mapping doesn't provide key/value types - key_type, value_type = Any, Any + kt, kv = Any, Any + return kt, kv + + if generic_bases: + # python 3.9 args + try: + key_type, value_type = typ.__args__ + except AttributeError: + # PMap without clarifying type arguments will cause this branch + key_type, value_type = maybe_non_specified_arguments(typ) + else: + key_type, value_type = maybe_non_specified_arguments(typ) key_node, memo, forward_refs = decide_node_type(key_type, overrides, memo, forward_refs) value_node, memo, forward_refs = decide_node_type(value_type, overrides, memo, forward_refs) @@ -459,18 +465,6 @@ def _maybe_node_for_dict( return rv, memo, forward_refs -class AttrInfo(NamedTuple): - name: str - resolved_type: Type - raw_type: Union[Type, ForwardRef] - - -def _type_hints_getter(typ: Type) -> Sequence[AttrInfo]: - raw = getattr(typ, '__annotations__', {}) - existing_only = lambda x: x[1] is not NoneType - return [AttrInfo(name, t, raw.get(name, t)) for name, t in filter(existing_only, get_type_hints(typ).items())] - - def _maybe_node_for_user_type( typ: Type[iface.IType], overrides: OverridesT, @@ -495,7 +489,7 @@ def _maybe_node_for_user_type( type_var_to_type = pmap(zip(generic_vars_ordered, bound_type_args)) # resolve type hints attribute_hints = [(field_name, type_var_to_type[type_var]) - for field_name, type_var in ((x, raw_type) for x, _resolved_type, raw_type in _type_hints_getter(hints_source))] + for field_name, type_var in ((x, raw_type) for x, _resolved_type, raw_type in get_type_attribute_info(hints_source))] # Generic types should not have default values defaults_source = lambda: () # Overrides should be the same as class-based ones, as Generics are not NamedTuple classes, @@ -516,7 +510,7 @@ def _maybe_node_for_user_type( elif is_named_tuple(typ): hints_source = typ - attribute_hints = [(x, raw_type) for x, y, raw_type in _type_hints_getter(hints_source)] + attribute_hints = [(x, raw_type) for x, y, raw_type in get_type_attribute_info(hints_source)] get_override_identifier = lambda x: getattr(typ, x) defaults_source = typ.__new__ @@ -537,7 +531,7 @@ def _maybe_node_for_user_type( else: # use init-based types hints_source = typ.__init__ - attribute_hints = [(x, raw_type) for x, y, raw_type in _type_hints_getter(hints_source)] + attribute_hints = [(x, raw_type) for x, y, raw_type in get_type_attribute_info(hints_source)] get_override_identifier = lambda x: (typ, x) defaults_source = typ.__init__ diff --git a/typeit/parser/type_info.py b/typeit/parser/type_info.py new file mode 100644 index 0000000..aea1a10 --- /dev/null +++ b/typeit/parser/type_info.py @@ -0,0 +1,15 @@ +from typing import Type, get_type_hints, NamedTuple, Union, ForwardRef, Any, Generator + +NoneType = type(None) + + +class AttrInfo(NamedTuple): + name: str + resolved_type: Type + raw_type: Union[Type, ForwardRef] + + +def get_type_attribute_info(typ: Type) -> Generator[AttrInfo, Any, None]: + raw = getattr(typ, '__annotations__', {}) + existing_only = lambda x: x[1] is not NoneType + return (AttrInfo(name, t, raw.get(name, t)) for name, t in filter(existing_only, get_type_hints(typ).items())) diff --git a/typeit/utils.py b/typeit/utils.py index 25942a4..46e456d 100644 --- a/typeit/utils.py +++ b/typeit/utils.py @@ -1,12 +1,14 @@ import re import keyword import string -from typing import Any, Type, TypeVar, Callable +import inspect as ins +from typing import Any, Type, TypeVar, Callable, Optional from colander import TupleSchema, SequenceSchema from typeit import flags from typeit.definitions import OverridesT +from typeit.parser import get_type_attribute_info from typeit.schema.nodes import SchemaNode NORMALIZATION_PREFIX = 'overridden__' @@ -14,7 +16,8 @@ T = TypeVar('T', SchemaNode, TupleSchema, SequenceSchema) - +A = TypeVar('A') +B = TypeVar('B') def normalize_name(name: str, pattern=re.compile('^([_0-9]+).*$')) -> str: @@ -53,3 +56,25 @@ def clone_schema_node(node: T) -> T: def get_global_name_overrider(overrides: OverridesT) -> Callable[[str], str]: return overrides.get(flags.GlobalNameOverride, flags.Identity) + + +def new(t: Type[A], scope: Optional[B] = None) -> A: + """Experimental: Init a type instance from the values of the provided scope, as long as the scope variables + have the same names and their types match the types of the attributes being initialised. + """ + if scope is None: + f = ins.currentframe().f_back.f_locals + else: + f_ = scope.__annotations__ + f = {x: getattr(scope, x) for x in f_} + + tattrs = get_type_attribute_info(t) + constr = {} + for attr in tattrs: + if attr.name not in f: + raise AttributeError(f"Could not find attribute {attr.name} for type {t.__class__} in the provided context") + ctxval = f[attr.name] + if not isinstance(ctxval, attr.resolved_type): + raise AttributeError(f"Types do not match: '{attr.name}' has to be {attr.resolved_type} but got {type(ctxval)} instead.") + constr[attr.name] = ctxval + return t(**constr)