From cbe5d33fd91f5e794bc93e28f670d6194a64f3f8 Mon Sep 17 00:00:00 2001 From: sneakers-the-rat Date: Wed, 31 Jul 2024 13:06:10 -0700 Subject: [PATCH] initial vendoring, pre-fixing --- README.md | 9 +- licenses/nptyping.txt | 7 + pyproject.toml | 5 + src/numpydantic/vendor/__init__.py | 0 src/numpydantic/vendor/nptyping/__init__.py | 188 ++++++++++ .../vendor/nptyping/assert_isinstance.py | 52 +++ .../vendor/nptyping/base_meta_classes.py | 248 +++++++++++++ src/numpydantic/vendor/nptyping/error.py | 47 +++ src/numpydantic/vendor/nptyping/ndarray.py | 196 ++++++++++ .../vendor/nptyping/nptyping_type.py | 30 ++ .../vendor/nptyping/package_info.py | 37 ++ .../vendor/nptyping/pandas_/__init__.py | 0 .../vendor/nptyping/pandas_/dataframe.py | 139 +++++++ .../vendor/nptyping/pandas_/dataframe.pyi | 27 ++ .../vendor/nptyping/pandas_/typing_.py | 33 ++ src/numpydantic/vendor/nptyping/py.typed | 0 src/numpydantic/vendor/nptyping/recarray.py | 78 ++++ src/numpydantic/vendor/nptyping/recarray.pyi | 27 ++ src/numpydantic/vendor/nptyping/shape.py | 75 ++++ src/numpydantic/vendor/nptyping/shape.pyi | 36 ++ .../vendor/nptyping/shape_expression.py | 190 ++++++++++ src/numpydantic/vendor/nptyping/structure.py | 107 ++++++ src/numpydantic/vendor/nptyping/structure.pyi | 38 ++ .../vendor/nptyping/structure_expression.py | 339 ++++++++++++++++++ src/numpydantic/vendor/nptyping/typing_.py | 185 ++++++++++ src/numpydantic/vendor/nptyping/typing_.pyi | 114 ++++++ 26 files changed, 2206 insertions(+), 1 deletion(-) create mode 100644 licenses/nptyping.txt create mode 100644 src/numpydantic/vendor/__init__.py create mode 100644 src/numpydantic/vendor/nptyping/__init__.py create mode 100644 src/numpydantic/vendor/nptyping/assert_isinstance.py create mode 100644 src/numpydantic/vendor/nptyping/base_meta_classes.py create mode 100644 src/numpydantic/vendor/nptyping/error.py create mode 100644 src/numpydantic/vendor/nptyping/ndarray.py create mode 100644 src/numpydantic/vendor/nptyping/nptyping_type.py create mode 100644 src/numpydantic/vendor/nptyping/package_info.py create mode 100644 src/numpydantic/vendor/nptyping/pandas_/__init__.py create mode 100644 src/numpydantic/vendor/nptyping/pandas_/dataframe.py create mode 100644 src/numpydantic/vendor/nptyping/pandas_/dataframe.pyi create mode 100644 src/numpydantic/vendor/nptyping/pandas_/typing_.py create mode 100644 src/numpydantic/vendor/nptyping/py.typed create mode 100644 src/numpydantic/vendor/nptyping/recarray.py create mode 100644 src/numpydantic/vendor/nptyping/recarray.pyi create mode 100644 src/numpydantic/vendor/nptyping/shape.py create mode 100644 src/numpydantic/vendor/nptyping/shape.pyi create mode 100644 src/numpydantic/vendor/nptyping/shape_expression.py create mode 100644 src/numpydantic/vendor/nptyping/structure.py create mode 100644 src/numpydantic/vendor/nptyping/structure.pyi create mode 100644 src/numpydantic/vendor/nptyping/structure_expression.py create mode 100644 src/numpydantic/vendor/nptyping/typing_.py create mode 100644 src/numpydantic/vendor/nptyping/typing_.pyi diff --git a/README.md b/README.md index e69c8b2..b7ceeff 100644 --- a/README.md +++ b/README.md @@ -416,4 +416,11 @@ dumped = instance.model_dump_json(context={'zarr_dump_array': True}) "hexdigest": "c51604eace325fe42bbebf39146c0956bd2ed13c" } } -``` \ No newline at end of file +``` + +## Vendored Dependencies + +We have vendored dependencies in the `src/numpydantic/vendor` package, +and reproduced their licenses in the `licenses` directory. + +- [nptyping](https://github.com/ramonhagenaars/nptyping) - `numpydantic.vendor.nptyping` - `/licenses/nptyping.txt` \ No newline at end of file diff --git a/licenses/nptyping.txt b/licenses/nptyping.txt new file mode 100644 index 0000000..5a89c4e --- /dev/null +++ b/licenses/nptyping.txt @@ -0,0 +1,7 @@ +Copyright 2022, Ramon Hagenaars + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/pyproject.toml b/pyproject.toml index 68ef104..71d6d5e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -131,4 +131,9 @@ fixable = ["ALL"] [tool.mypy] plugins = [ "pydantic.mypy" +] + +[tool.coverage.run] +omit = [ + "src/numpydantic/vendor/*" ] \ No newline at end of file diff --git a/src/numpydantic/vendor/__init__.py b/src/numpydantic/vendor/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/numpydantic/vendor/nptyping/__init__.py b/src/numpydantic/vendor/nptyping/__init__.py new file mode 100644 index 0000000..5fd5b2c --- /dev/null +++ b/src/numpydantic/vendor/nptyping/__init__.py @@ -0,0 +1,188 @@ +""" +MIT License + +Copyright (c) 2023 Ramon Hagenaars + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" +from nptyping.assert_isinstance import assert_isinstance +from nptyping.error import ( + InvalidArgumentsError, + InvalidDTypeError, + InvalidShapeError, + InvalidStructureError, + NPTypingError, +) +from nptyping.ndarray import NDArray +from nptyping.package_info import __version__ +from nptyping.pandas_.dataframe import DataFrame +from nptyping.recarray import RecArray +from nptyping.shape import Shape +from nptyping.shape_expression import ( + normalize_shape_expression, + validate_shape_expression, +) +from nptyping.structure import Structure +from nptyping.typing_ import ( + Bool, + Bool8, + Byte, + Bytes, + Bytes0, + CDouble, + CFloat, + Character, + CLongDouble, + CLongFloat, + Complex, + Complex64, + Complex128, + ComplexFloating, + CSingle, + Datetime64, + Double, + DType, + Flexible, + Float, + Float16, + Float32, + Float64, + Floating, + Half, + Inexact, + Int, + Int0, + Int8, + Int16, + Int32, + Int64, + IntC, + Integer, + IntP, + LongComplex, + LongDouble, + LongFloat, + LongLong, + Number, + Object, + Object0, + Short, + SignedInteger, + Single, + SingleComplex, + Str0, + String, + Timedelta64, + UByte, + UInt, + UInt0, + UInt8, + UInt16, + UInt32, + UInt64, + UIntC, + UIntP, + ULongLong, + Unicode, + UnsignedInteger, + UShort, + Void, + Void0, +) + +__all__ = [ + "NDArray", + "RecArray", + "assert_isinstance", + "validate_shape_expression", + "normalize_shape_expression", + "NPTypingError", + "InvalidArgumentsError", + "InvalidShapeError", + "InvalidStructureError", + "InvalidDTypeError", + "Shape", + "Structure", + "__version__", + "DType", + "Number", + "Bool", + "Bool8", + "Object", + "Object0", + "Datetime64", + "Integer", + "SignedInteger", + "Int8", + "Int16", + "Int32", + "Int64", + "Byte", + "Short", + "IntC", + "IntP", + "Int0", + "Int", + "LongLong", + "Timedelta64", + "UnsignedInteger", + "UInt8", + "UInt16", + "UInt32", + "UInt64", + "UByte", + "UShort", + "UIntC", + "UIntP", + "UInt0", + "UInt", + "ULongLong", + "Inexact", + "Floating", + "Float16", + "Float32", + "Float64", + "Half", + "Single", + "Double", + "Float", + "LongDouble", + "LongFloat", + "ComplexFloating", + "Complex64", + "Complex128", + "CSingle", + "SingleComplex", + "CDouble", + "Complex", + "CFloat", + "CLongDouble", + "CLongFloat", + "LongComplex", + "Flexible", + "Void", + "Void0", + "Character", + "Bytes", + "String", + "Bytes0", + "Unicode", + "Str0", + "DataFrame", +] diff --git a/src/numpydantic/vendor/nptyping/assert_isinstance.py b/src/numpydantic/vendor/nptyping/assert_isinstance.py new file mode 100644 index 0000000..5ae4eb1 --- /dev/null +++ b/src/numpydantic/vendor/nptyping/assert_isinstance.py @@ -0,0 +1,52 @@ +""" +MIT License + +Copyright (c) 2023 Ramon Hagenaars + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" +from typing import ( + Any, + Optional, + Type, + TypeVar, +) + +try: + from typing import TypeGuard # type: ignore[attr-defined] +except ImportError: # pragma: no cover + from typing_extensions import TypeGuard # type: ignore[attr-defined] + +TYPE = TypeVar("TYPE") + + +def assert_isinstance( + instance: Any, cls: Type[TYPE], message: Optional[str] = None +) -> TypeGuard[TYPE]: + """ + A TypeGuard function that is equivalent to `assert instance, cls, message` + that hides nasty MyPy or IDE warnings. + :param instance: the instance that is checked against cls. + :param cls: the class + :param message: any message that is displayed when the assert check fails. + :return: the type of cls. + """ + message = message or f"instance={instance!r}, cls={cls!r}" + assert isinstance(instance, cls), message + return True diff --git a/src/numpydantic/vendor/nptyping/base_meta_classes.py b/src/numpydantic/vendor/nptyping/base_meta_classes.py new file mode 100644 index 0000000..b9ca13a --- /dev/null +++ b/src/numpydantic/vendor/nptyping/base_meta_classes.py @@ -0,0 +1,248 @@ +""" +MIT License + +Copyright (c) 2023 Ramon Hagenaars + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" +from abc import ABCMeta, abstractmethod +from inspect import FrameInfo +from typing import ( + Any, + Dict, + List, + Optional, + Set, + Tuple, + TypeVar, +) + +from nptyping.error import InvalidArgumentsError, NPTypingError + +_T = TypeVar("_T") + + +class InconstructableMeta(ABCMeta): + """ + Makes it impossible for a class to get instantiated. + """ + + def __call__(cls, *_: Any, **__: Any) -> None: + raise NPTypingError( + f"Cannot instantiate nptyping.{cls.__name__}. Did you mean to use [ ] ?" + ) + + +class ImmutableMeta(ABCMeta): + """ + Makes it impossible to changes values on a class. + """ + + def __setattr__(cls, key: str, value: Any) -> None: + if key not in ("_abc_impl", "__abstractmethods__"): + raise NPTypingError(f"Cannot set values to nptyping.{cls.__name__}.") + + +class FinalMeta(ABCMeta): + """ + Makes it impossible for classes to inherit from some class. + + An concrete inheriting meta class requires to define a name for its + implementation. The class with this name will be the only class that is + allowed to use that concrete meta class. + """ + + _name_per_meta_cls: Dict[type, Optional[str]] = {} + + def __init_subclass__(cls, implementation: Optional[str] = None) -> None: + # implementation is made Optional here, to allow other meta classes to + # inherit. + cls._name_per_meta_cls[cls] = implementation + + def __new__(cls, name: str, *args: Any, **kwargs: Any) -> type: + if name == cls._name_per_meta_cls[cls]: + assert name, "cls_name not set" + return type.__new__(cls, name, *args, **kwargs) + + raise NPTypingError(f"Cannot subclass nptyping.{cls._name_per_meta_cls[cls]}.") + + +class MaybeCheckableMeta(ABCMeta): + """ + Makes instance and subclass checks raise by default. + """ + + def __instancecheck__(cls, instance: Any) -> bool: + raise NPTypingError( + f"Instance checking is not supported for nptyping.{cls.__name__}." + ) + + def __subclasscheck__(cls, subclass: Any) -> bool: + raise NPTypingError( + f"Subclass checking is not supported for nptyping.{cls.__name__}." + ) + + +class PrintableMeta(ABCMeta): + """ + Ensures that a class can be printed nicely. + """ + + @abstractmethod + def __str__(cls) -> str: + ... # pragma: no cover + + def __repr__(cls) -> str: + return str(cls) + + +class SubscriptableMeta(ABCMeta): + """ + Makes a class subscriptable: it accepts arguments between brackets and a + new type is returned for every unique set of arguments. + """ + + _all_types: Dict[Tuple[type, Tuple[Any, ...]], type] = {} + _parameterized: bool = False + + @abstractmethod + def _get_item(cls, item: Any) -> Tuple[Any, ...]: + ... # pragma: no cover + + def _get_module(cls, stack: List[FrameInfo], module: str) -> str: + # The magic below makes Python's help function display a meaningful + # text with nptyping types. + return "typing" if stack[1][3] == "formatannotation" else module + + def _get_additional_values( + cls, item: Any # pylint: disable=unused-argument + ) -> Dict[str, Any]: + # This method is invoked after _get_item and right before returning + # the result of __getitem__. It can be overridden to provide extra + # values that are to be set as attributes on the new type. + return {} + + def __getitem__(cls, item: Any) -> type: + if getattr(cls, "_parameterized", False): + raise NPTypingError(f"Type nptyping.{cls} is already parameterized.") + + args = cls._get_item(item) + additional_values = cls._get_additional_values(item) + assert hasattr(cls, "__args__"), "A SubscriptableMeta must have __args__." + if args != cls.__args__: # type: ignore[attr-defined] + result = cls._create_type(args, additional_values) + else: + result = cls + + return result + + def _create_type( + cls, args: Tuple[Any, ...], additional_values: Dict[str, Any] + ) -> type: + key = (cls, args) + if key not in cls._all_types: + cls._all_types[key] = type( + cls.__name__, + (cls,), + {"__args__": args, "_parameterized": True, **additional_values}, + ) + return cls._all_types[key] + + +class ComparableByArgsMeta(ABCMeta): + """ + Makes a class comparable by means of its __args__. + """ + + __args__: Tuple[Any, ...] + + def __eq__(cls, other: Any) -> bool: + return ( + hasattr(cls, "__args__") + and hasattr(other, "__args__") + and cls.__args__ == other.__args__ + ) + + def __hash__(cls) -> int: + return hash(cls.__args__) + + +class ContainerMeta( + InconstructableMeta, + ImmutableMeta, + FinalMeta, + MaybeCheckableMeta, + PrintableMeta, + SubscriptableMeta, + ComparableByArgsMeta, + ABCMeta, +): + """ + Base meta class for "containers" such as Shape and Structure. + """ + + _known_expressions: Set[str] = set() + __args__: Tuple[str, ...] + + @abstractmethod + def _validate_expression(cls, item: str) -> None: + ... # pragma: no cover + + @abstractmethod + def _normalize_expression(cls, item: str) -> str: + ... # pragma: no cover + + def _get_item(cls, item: Any) -> Tuple[Any, ...]: + if not isinstance(item, str): + raise InvalidArgumentsError( + f"Unexpected argument of type {type(item)}, expecting a string." + ) + + if item in cls._known_expressions: + # No need to do costly validations and normalizations if it has been done + # before. + return (item,) + + cls._validate_expression(item) + norm_shape_expression = cls._normalize_expression(item) + cls._known_expressions.add(norm_shape_expression) + return (norm_shape_expression,) + + def __subclasscheck__(cls, subclass: Any) -> bool: + type_match = type(subclass) == type( # pylint: disable=unidiomatic-typecheck + cls + ) + return type_match and ( + subclass.__args__ == cls.__args__ or not cls._parameterized + ) + + def __str__(cls) -> str: + return f"{cls.__name__}['{cls.__args__[0]}']" + + def __eq__(cls, other: Any) -> bool: + result = cls is other + if not result and hasattr(cls, "__args__") and hasattr(other, "__args__"): + normalized_args = tuple( + cls._normalize_expression(str(arg)) for arg in other.__args__ + ) + result = cls.__args__ == normalized_args + return result + + def __hash__(cls) -> int: + return hash(cls.__args__) diff --git a/src/numpydantic/vendor/nptyping/error.py b/src/numpydantic/vendor/nptyping/error.py new file mode 100644 index 0000000..237864e --- /dev/null +++ b/src/numpydantic/vendor/nptyping/error.py @@ -0,0 +1,47 @@ +""" +MIT License + +Copyright (c) 2023 Ramon Hagenaars + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" + + +class NPTypingError(Exception): + """Base error for all NPTyping errors.""" + + +class InvalidArgumentsError(NPTypingError): + """Raised when a invalid arguments are provided to an nptyping type.""" + + +class InvalidShapeError(NPTypingError): + """Raised when a shape is considered not valid.""" + + +class InvalidStructureError(NPTypingError): + """Raised when a structure is considered not valid.""" + + +class InvalidDTypeError(NPTypingError): + """Raised when an argument is not a DType.""" + + +class DependencyError(NPTypingError): + """Raised when a dependency has not been installed.""" diff --git a/src/numpydantic/vendor/nptyping/ndarray.py b/src/numpydantic/vendor/nptyping/ndarray.py new file mode 100644 index 0000000..adeea12 --- /dev/null +++ b/src/numpydantic/vendor/nptyping/ndarray.py @@ -0,0 +1,196 @@ +""" +MIT License + +Copyright (c) 2023 Ramon Hagenaars + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" +import inspect +from abc import ABC +from typing import Any, Tuple + +import numpy as np + +from nptyping.base_meta_classes import ( + FinalMeta, + ImmutableMeta, + InconstructableMeta, + MaybeCheckableMeta, + PrintableMeta, + SubscriptableMeta, +) +from nptyping.error import InvalidArgumentsError +from nptyping.nptyping_type import NPTypingType +from nptyping.shape import Shape +from nptyping.shape_expression import check_shape +from nptyping.structure import Structure +from nptyping.structure_expression import check_structure, check_type_names +from nptyping.typing_ import ( + DType, + dtype_per_name, + name_per_dtype, +) + + +class NDArrayMeta( + SubscriptableMeta, + InconstructableMeta, + ImmutableMeta, + FinalMeta, + MaybeCheckableMeta, + PrintableMeta, + implementation="NDArray", +): + """ + Metaclass that is coupled to nptyping.NDArray. It contains all actual logic + such as instance checking. + """ + + __args__: Tuple[Shape, DType] + _parameterized: bool + + @property + def __module__(cls) -> str: + return cls._get_module(inspect.stack(), "nptyping.ndarray") + + def _get_item(cls, item: Any) -> Tuple[Any, ...]: + cls._check_item(item) + shape, dtype = cls._get_from_tuple(item) + return shape, dtype + + def __instancecheck__( # pylint: disable=bad-mcs-method-argument + self, instance: Any + ) -> bool: + shape, dtype = self.__args__ + dtype_is_structure = issubclass(dtype, Structure) + structure_is_ok = dtype_is_structure and check_structure( + instance.dtype, dtype, dtype_per_name + ) + return ( + isinstance(instance, np.ndarray) + and (shape is Any or check_shape(instance.shape, shape)) + and ( + dtype is Any + or structure_is_ok + or issubclass(instance.dtype.type, dtype) + ) + ) + + def __str__(cls) -> str: + shape, dtype = cls.__args__ + return ( + f"{cls.__name__}[{cls._shape_expression_to_str(shape)}, " + f"{cls._dtype_to_str(dtype)}]" + ) + + def _is_literal_like(cls, item: Any) -> bool: + # item is a Literal or "Literal enough" (ducktyping). + return hasattr(item, "__args__") + + def _check_item(cls, item: Any) -> None: + # Check if the item is what we expect and raise if it is not. + if not isinstance(item, tuple): + raise InvalidArgumentsError(f"Unexpected argument of type {type(item)}.") + if len(item) > 2: + raise InvalidArgumentsError(f"Unexpected argument {item[2]}.") + + def _get_from_tuple(cls, item: Tuple[Any, ...]) -> Tuple[Shape, DType]: + # Return the Shape Expression and DType from a tuple. + shape = cls._get_shape(item[0]) + dtype = cls._get_dtype(item[1]) + return shape, dtype + + def _get_shape(cls, dtype_candidate: Any) -> Shape: + if dtype_candidate is Any or dtype_candidate is Shape: + shape = Any + elif issubclass(dtype_candidate, Shape): + shape = dtype_candidate + elif cls._is_literal_like(dtype_candidate): + shape_expression = dtype_candidate.__args__[0] + shape = Shape[shape_expression] + else: + raise InvalidArgumentsError( + f"Unexpected argument '{dtype_candidate}', expecting" + " Shape[]" + " or Literal[]" + " or typing.Any." + ) + return shape + + def _get_dtype(cls, dtype_candidate: Any) -> DType: + is_dtype = isinstance(dtype_candidate, type) and issubclass( + dtype_candidate, np.generic + ) + if dtype_candidate is Any: + dtype = Any + elif is_dtype: + dtype = dtype_candidate + elif issubclass(dtype_candidate, Structure): + dtype = dtype_candidate + check_type_names(dtype, dtype_per_name) + elif cls._is_literal_like(dtype_candidate): + structure_expression = dtype_candidate.__args__[0] + dtype = Structure[structure_expression] + check_type_names(dtype, dtype_per_name) + else: + raise InvalidArgumentsError( + f"Unexpected argument '{dtype_candidate}', expecting" + " Structure[]" + " or Literal[]" + " or a dtype" + " or typing.Any." + ) + return dtype + + def _dtype_to_str(cls, dtype: Any) -> str: + if dtype is Any: + result = "Any" + elif issubclass(dtype, Structure): + result = str(dtype) + else: + result = name_per_dtype[dtype] + return result + + def _shape_expression_to_str(cls, shape_expression: Any) -> str: + return "Any" if shape_expression is Any else str(shape_expression) + + +class NDArray(NPTypingType, ABC, metaclass=NDArrayMeta): + """ + An nptyping equivalent of numpy ndarray. + + ## No arguments means an NDArray with any DType and any shape. + >>> NDArray + NDArray[Any, Any] + + ## You can provide a DType and a Shape Expression. + >>> from nptyping import Int32, Shape + >>> NDArray[Shape["2, 2"], Int32] + NDArray[Shape['2, 2'], Int32] + + ## Instance checking can be done and the shape is also checked. + >>> import numpy as np + >>> isinstance(np.array([[1, 2], [3, 4]]), NDArray[Shape['2, 2'], Int32]) + True + >>> isinstance(np.array([[1, 2], [3, 4], [5, 6]]), NDArray[Shape['2, 2'], Int32]) + False + + """ + + __args__ = (Any, Any) diff --git a/src/numpydantic/vendor/nptyping/nptyping_type.py b/src/numpydantic/vendor/nptyping/nptyping_type.py new file mode 100644 index 0000000..35d4897 --- /dev/null +++ b/src/numpydantic/vendor/nptyping/nptyping_type.py @@ -0,0 +1,30 @@ +""" +MIT License + +Copyright (c) 2023 Ramon Hagenaars + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" +from abc import ABC + + +class NPTypingType(ABC): + """ + Baseclass for all nptyping types. + """ diff --git a/src/numpydantic/vendor/nptyping/package_info.py b/src/numpydantic/vendor/nptyping/package_info.py new file mode 100644 index 0000000..ee15ac8 --- /dev/null +++ b/src/numpydantic/vendor/nptyping/package_info.py @@ -0,0 +1,37 @@ +""" +MIT License + +Copyright (c) 2023 Ramon Hagenaars + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" +__title__ = "nptyping" +__version__ = "2.5.0" +__author__ = "Ramon Hagenaars" +__author_email__ = "ramon.hagenaars@gmail.com" +__description__ = "Type hints for NumPy." +__url__ = "https://github.com/ramonhagenaars/nptyping" +__license__ = "MIT" +__python_versions__ = [ + "3.7", + "3.8", + "3.9", + "3.10", + "3.11", +] diff --git a/src/numpydantic/vendor/nptyping/pandas_/__init__.py b/src/numpydantic/vendor/nptyping/pandas_/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/numpydantic/vendor/nptyping/pandas_/dataframe.py b/src/numpydantic/vendor/nptyping/pandas_/dataframe.py new file mode 100644 index 0000000..e108b27 --- /dev/null +++ b/src/numpydantic/vendor/nptyping/pandas_/dataframe.py @@ -0,0 +1,139 @@ +""" +MIT License + +Copyright (c) 2023 Ramon Hagenaars + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" +import inspect +from abc import ABC +from typing import Any, Tuple + +import numpy as np + +from nptyping import InvalidArgumentsError +from nptyping.base_meta_classes import ( + FinalMeta, + ImmutableMeta, + InconstructableMeta, + MaybeCheckableMeta, + PrintableMeta, + SubscriptableMeta, +) +from nptyping.error import DependencyError +from nptyping.nptyping_type import NPTypingType +from nptyping.pandas_.typing_ import dtype_per_name +from nptyping.structure import Structure +from nptyping.structure_expression import check_structure + +try: + import pandas as pd +except ImportError: # pragma: no cover + pd = None # type: ignore[misc, assignment] + + +class DataFrameMeta( + SubscriptableMeta, + InconstructableMeta, + ImmutableMeta, + FinalMeta, + MaybeCheckableMeta, + PrintableMeta, + implementation="DataFrame", +): + """ + Metaclass that is coupled to nptyping.DataFrame. It contains all actual logic + such as instance checking. + """ + + __args__: Tuple[Structure] + _parameterized: bool + + def __instancecheck__( # pylint: disable=bad-mcs-method-argument + self, instance: Any + ) -> bool: + structure = self.__args__[0] + + if pd is None: + raise DependencyError( # pragma: no cover + "Pandas needs to be installed for instance checking. Use `pip " + "install nptyping[pandas]` or `pip install nptyping[complete]`" + ) + + if not isinstance(instance, pd.DataFrame): + return False + + if structure is Any: + return True + + structured_dtype = np.dtype( + [(column, dtype.str) for column, dtype in instance.dtypes.items()] + ) + return check_structure(structured_dtype, structure, dtype_per_name) + + def _get_item(cls, item: Any) -> Tuple[Structure]: + if item is Any: + return (Any,) + cls._check_item(item) + return (Structure[getattr(item, "__args__")[0]],) + + def __str__(cls) -> str: + structure = cls.__args__[0] + structure_str = "Any" if structure is Any else structure.__args__[0] + return f"{cls.__name__}[{structure_str}]" + + def __repr__(cls) -> str: + structure = cls.__args__[0] + structure_str = "Any" if structure is Any else structure + return f"{cls.__name__}[{structure_str}]" + + @property + def __module__(cls) -> str: + return cls._get_module(inspect.stack(), "nptyping.pandas_.dataframe") + + def _check_item(cls, item: Any) -> None: + # Check if the item is what we expect and raise if it is not. + if not hasattr(item, "__args__"): + raise InvalidArgumentsError(f"Unexpected argument of type {type(item)}.") + + +class DataFrame(NPTypingType, ABC, metaclass=DataFrameMeta): + """ + An nptyping equivalent of pandas DataFrame. + + ## No arguments means a DataFrame of any structure. + >>> DataFrame + DataFrame[Any] + + ## You can use Structure Expression. + >>> from nptyping import DataFrame, Structure + >>> DataFrame[Structure["x: Int, y: Int"]] + DataFrame[Structure['[x, y]: Int']] + + ## Instance checking can be done and the structure is also checked. + >>> import pandas as pd + >>> df = pd.DataFrame({'x': [1, 2, 3], 'y': [4., 5., 6.]}) + >>> isinstance(df, DataFrame[Structure['x: Int, y: Float']]) + True + >>> isinstance(df, DataFrame[Structure['x: Float, y: Int']]) + False + + """ + + __args__ = (Any,) diff --git a/src/numpydantic/vendor/nptyping/pandas_/dataframe.pyi b/src/numpydantic/vendor/nptyping/pandas_/dataframe.pyi new file mode 100644 index 0000000..edab03f --- /dev/null +++ b/src/numpydantic/vendor/nptyping/pandas_/dataframe.pyi @@ -0,0 +1,27 @@ +""" +MIT License + +Copyright (c) 2023 Ramon Hagenaars + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" + +import pandas as pd + +DataFrame = pd.DataFrame diff --git a/src/numpydantic/vendor/nptyping/pandas_/typing_.py b/src/numpydantic/vendor/nptyping/pandas_/typing_.py new file mode 100644 index 0000000..29f2188 --- /dev/null +++ b/src/numpydantic/vendor/nptyping/pandas_/typing_.py @@ -0,0 +1,33 @@ +""" +MIT License + +Copyright (c) 2023 Ramon Hagenaars + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" +from nptyping.typing_ import Object +from nptyping.typing_ import dtype_per_name as dtype_per_name_default + +dtype_per_name = { + **dtype_per_name_default, # type: ignore[arg-type] + # Override the `String` and `Str` to point to `Object`. Pandas uses Object + # for string types in Dataframes and Series. + "String": Object, + "Str": Object, +} diff --git a/src/numpydantic/vendor/nptyping/py.typed b/src/numpydantic/vendor/nptyping/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/src/numpydantic/vendor/nptyping/recarray.py b/src/numpydantic/vendor/nptyping/recarray.py new file mode 100644 index 0000000..b3bbbc5 --- /dev/null +++ b/src/numpydantic/vendor/nptyping/recarray.py @@ -0,0 +1,78 @@ +""" +MIT License + +Copyright (c) 2023 Ramon Hagenaars + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" +import inspect +from typing import Any, Tuple + +import numpy as np + +from nptyping.error import InvalidArgumentsError +from nptyping.ndarray import NDArray, NDArrayMeta +from nptyping.structure import Structure +from nptyping.typing_ import DType + + +class RecArrayMeta(NDArrayMeta, implementation="RecArray"): + """ + Metaclass that is coupled to nptyping.RecArray. It takes most of its logic + from NDArrayMeta. + """ + + def _get_item(cls, item: Any) -> Tuple[Any, ...]: + cls._check_item(item) + shape, dtype = cls._get_from_tuple(item) + return shape, dtype + + def _get_dtype(cls, dtype_candidate: Any) -> DType: + if not issubclass(dtype_candidate, Structure) and dtype_candidate is not Any: + raise InvalidArgumentsError( + f"Unexpected argument {dtype_candidate}. Expecting a Structure." + ) + return dtype_candidate + + @property + def __module__(cls) -> str: + return cls._get_module(inspect.stack(), "nptyping.recarray") + + def __instancecheck__( # pylint: disable=bad-mcs-method-argument + self, instance: Any + ) -> bool: + return isinstance(instance, np.recarray) and NDArrayMeta.__instancecheck__( + self, instance + ) + + +class RecArray(NDArray, metaclass=RecArrayMeta): + """ + An nptyping equivalent of numpy recarray. + + ## RecArrays can take a Shape and must take a Structure + >>> from nptyping import Shape, Structure + >>> RecArray[Shape["2, 2"], Structure["x: Float, y: Float"]] + RecArray[Shape['2, 2'], Structure['[x, y]: Float']] + + ## Or Any + >>> from typing import Any + >>> RecArray[Shape["2, 2"], Any] + RecArray[Shape['2, 2'], Any] + """ diff --git a/src/numpydantic/vendor/nptyping/recarray.pyi b/src/numpydantic/vendor/nptyping/recarray.pyi new file mode 100644 index 0000000..4e67f8a --- /dev/null +++ b/src/numpydantic/vendor/nptyping/recarray.pyi @@ -0,0 +1,27 @@ +""" +MIT License + +Copyright (c) 2023 Ramon Hagenaars + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" + +import numpy as np + +RecArray = np.recarray diff --git a/src/numpydantic/vendor/nptyping/shape.py b/src/numpydantic/vendor/nptyping/shape.py new file mode 100644 index 0000000..5dc05f5 --- /dev/null +++ b/src/numpydantic/vendor/nptyping/shape.py @@ -0,0 +1,75 @@ +""" +MIT License + +Copyright (c) 2023 Ramon Hagenaars + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" +from abc import ABC +from typing import Any, Dict + +from nptyping.base_meta_classes import ContainerMeta +from nptyping.nptyping_type import NPTypingType +from nptyping.shape_expression import ( + get_dimensions, + normalize_shape_expression, + remove_labels, + validate_shape_expression, +) + + +class ShapeMeta(ContainerMeta, implementation="Shape"): + """ + Metaclass that is coupled to nptyping.Shape. + """ + + def _validate_expression(cls, item: str) -> None: + validate_shape_expression(item) + + def _normalize_expression(cls, item: str) -> str: + return normalize_shape_expression(item) + + def _get_additional_values(cls, item: Any) -> Dict[str, Any]: + dim_strings = get_dimensions(item) + dim_string_without_labels = remove_labels(dim_strings) + return {"prepared_args": dim_string_without_labels} + + +class Shape(NPTypingType, ABC, metaclass=ShapeMeta): + """ + A container for shape expressions that describe the shape of an multi + dimensional array. + + Simple example: + + >>> Shape['2, 2'] + Shape['2, 2'] + + A Shape can be compared to a typing.Literal. You can use Literals in + NDArray as well. + + >>> from typing import Literal + + >>> Shape['2, 2'] == Literal['2, 2'] + True + + """ + + __args__ = ("*, ...",) + prepared_args = "*, ..." diff --git a/src/numpydantic/vendor/nptyping/shape.pyi b/src/numpydantic/vendor/nptyping/shape.pyi new file mode 100644 index 0000000..96d2eb6 --- /dev/null +++ b/src/numpydantic/vendor/nptyping/shape.pyi @@ -0,0 +1,36 @@ +""" +MIT License + +Copyright (c) 2023 Ramon Hagenaars + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" +try: + from typing import Literal # type: ignore[attr-defined] +except ImportError: + from typing_extensions import Literal # type: ignore[attr-defined,misc,assignment] + +from typing import Any, cast + +# For MyPy: +Shape = cast(Literal, Shape) # type: ignore[has-type,misc,valid-type] + +# For PyRight: +class Shape: # type: ignore[no-redef] + def __class_getitem__(cls, item: Any) -> Any: ... diff --git a/src/numpydantic/vendor/nptyping/shape_expression.py b/src/numpydantic/vendor/nptyping/shape_expression.py new file mode 100644 index 0000000..bcaf79b --- /dev/null +++ b/src/numpydantic/vendor/nptyping/shape_expression.py @@ -0,0 +1,190 @@ +""" +MIT License + +Copyright (c) 2023 Ramon Hagenaars + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" +import re +import string +from functools import lru_cache +from typing import ( + TYPE_CHECKING, + Any, + Dict, + List, + Union, +) + +from nptyping.error import InvalidShapeError +from nptyping.typing_ import ShapeExpression, ShapeTuple + +if TYPE_CHECKING: + from nptyping.shape import Shape # pragma: no cover + + +@lru_cache() +def check_shape(shape: ShapeTuple, target: "Shape") -> bool: + """ + Check whether the given shape corresponds to the given shape_expression. + :param shape: the shape in question. + :param target: the shape expression to which shape is tested. + :return: True if the given shape corresponds to shape_expression. + """ + target_shape = _handle_ellipsis(shape, target.prepared_args) + return _check_dimensions_against_shape(shape, target_shape) + + +def validate_shape_expression(shape_expression: Union[ShapeExpression, Any]) -> None: + """ + Validate shape_expression and raise an InvalidShapeError if it is not + considered valid. + :param shape_expression: the shape expression to validate. + :return: None. + """ + shape_expression_no_quotes = shape_expression.replace("'", "").replace('"', "") + if shape_expression is not Any and not re.match( + _REGEX_SHAPE_EXPRESSION, shape_expression_no_quotes + ): + raise InvalidShapeError( + f"'{shape_expression}' is not a valid shape expression." + ) + + +def normalize_shape_expression(shape_expression: ShapeExpression) -> ShapeExpression: + """ + Normalize the given shape expression, e.g. by removing whitespaces, making + similar expressions look the same. + :param shape_expression: the shape expression that is to be normalized. + :return: a normalized shape expression. + """ + shape_expression = shape_expression.replace("'", "").replace('"', "") + # Replace whitespaces right before labels with $. + shape_expression = re.sub(rf"\s*{_REGEX_LABEL}", r"$\1", shape_expression) + # Let all commas be followed by a $. + shape_expression = shape_expression.replace(",", ",$") + # Remove all whitespaces left. + shape_expression = re.sub(r"\s*", "", shape_expression) + # Remove $ right after a bracket. + shape_expression = re.sub(r"\[\$+", "[", shape_expression) + # Replace $ with a single space. + shape_expression = re.sub(r"\$+", " ", shape_expression) + return shape_expression + + +def get_dimensions(shape_expression: str) -> List[str]: + """ + Find all "break downs" (the parts between brackets) in a shape expressions + and replace them with mere dimension sizes. + + :param shape_expression: the shape expression that gets the break downs replaced. + :return: a list of dimensions without break downs. + """ + shape_expression_without_breakdowns = shape_expression + for dim_breakdown in re.findall( + r"(\[[^\]]+\])", shape_expression_without_breakdowns + ): + dim_size = len(dim_breakdown.split(",")) + shape_expression_without_breakdowns = ( + shape_expression_without_breakdowns.replace(dim_breakdown, str(dim_size)) + ) + return shape_expression_without_breakdowns.split(",") + + +def remove_labels(dimensions: List[str]) -> List[str]: + """ + Remove all labels (words that start with a lowercase). + + :param dimensions: a list of dimensions. + :return: a copy of the given list without labels. + """ + return [re.sub(r"\b[a-z]\w*", "", dim).strip() for dim in dimensions] + + +def _check_dimensions_against_shape(shape: ShapeTuple, target: List[str]) -> bool: + # Walk through the shape and test them against the given target, + # taking into consideration variables, wildcards, etc. + + if len(shape) != len(target): + return False + shape_as_strings = (str(dim) for dim in shape) + variables: Dict[str, str] = {} + for dim, target_dim in zip(shape_as_strings, target): + if _is_wildcard(target_dim) or _is_assignable_var(dim, target_dim, variables): + continue + if dim != target_dim: + return False + return True + + +def _handle_ellipsis(shape: ShapeTuple, target: List[str]) -> List[str]: + # Let the ellipsis allows for any number of dimensions by replacing the + # ellipsis with the dimension size repeated the number of times that + # corresponds to the shape of the instance. + if target[-1] == "...": + dim_to_repeat = target[-2] + target = target[0:-1] + if len(shape) > len(target): + difference = len(shape) - len(target) + target += difference * [dim_to_repeat] + return target + + +def _is_assignable_var(dim: str, target_dim: str, variables: Dict[str, str]) -> bool: + # Return whether target_dim is a variable and can be assigned with dim. + return _is_variable(target_dim) and _can_assign_variable(dim, target_dim, variables) + + +def _is_variable(dim: str) -> bool: + # Return whether dim is a variable. + return dim[0] in string.ascii_uppercase + + +def _can_assign_variable(dim: str, target_dim: str, variables: Dict[str, str]) -> bool: + # Check and assign a variable. + assignable = variables.get(target_dim) in (None, dim) + variables[target_dim] = dim + return assignable + + +def _is_wildcard(dim: str) -> bool: + # Return whether dim is a wildcard (i.e. the character that takes any + # dimension size). + return dim == "*" + + +_REGEX_SEPARATOR = r"(\s*,\s*)" +_REGEX_DIMENSION_SIZE = r"(\s*[0-9]+\s*)" +_REGEX_VARIABLE = r"(\s*\b[A-Z]\w*\s*)" +_REGEX_LABEL = r"(\s*\b[a-z]\w*\s*)" +_REGEX_LABELS = rf"({_REGEX_LABEL}({_REGEX_SEPARATOR}{_REGEX_LABEL})*)" +_REGEX_WILDCARD = r"(\s*\*\s*)" +_REGEX_DIMENSION_BREAKDOWN = rf"(\s*\[{_REGEX_LABELS}\]\s*)" +_REGEX_DIMENSION = ( + rf"({_REGEX_DIMENSION_SIZE}" + rf"|{_REGEX_VARIABLE}" + rf"|{_REGEX_WILDCARD}" + rf"|{_REGEX_DIMENSION_BREAKDOWN})" +) +_REGEX_DIMENSION_WITH_LABEL = rf"({_REGEX_DIMENSION}(\s+{_REGEX_LABEL})*)" +_REGEX_DIMENSIONS = ( + rf"{_REGEX_DIMENSION_WITH_LABEL}({_REGEX_SEPARATOR}{_REGEX_DIMENSION_WITH_LABEL})*" +) +_REGEX_DIMENSIONS_ELLIPSIS = rf"({_REGEX_DIMENSIONS}{_REGEX_SEPARATOR}\.\.\.\s*)" +_REGEX_SHAPE_EXPRESSION = rf"^({_REGEX_DIMENSIONS}|{_REGEX_DIMENSIONS_ELLIPSIS})$" diff --git a/src/numpydantic/vendor/nptyping/structure.py b/src/numpydantic/vendor/nptyping/structure.py new file mode 100644 index 0000000..0b95dfb --- /dev/null +++ b/src/numpydantic/vendor/nptyping/structure.py @@ -0,0 +1,107 @@ +""" +MIT License + +Copyright (c) 2023 Ramon Hagenaars + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" +from abc import ABC +from typing import ( + Any, + Dict, + List, +) + +from nptyping.base_meta_classes import ContainerMeta +from nptyping.nptyping_type import NPTypingType +from nptyping.structure_expression import ( + create_name_to_type_dict, + normalize_structure_expression, + validate_structure_expression, +) + + +class StructureMeta(ContainerMeta, implementation="Structure"): + """ + Metaclass that is coupled to nptyping.Structure. + """ + + __args__ = tuple() + + def _validate_expression(cls, item: str) -> None: + validate_structure_expression(item) + + def _normalize_expression(cls, item: str) -> str: + return normalize_structure_expression(item) + + def _get_additional_values(cls, item: Any) -> Dict[str, Any]: + return { + "_type_per_name": create_name_to_type_dict(item), + "_has_wildcard": item.replace(" ", "").endswith(",*"), + } + + +class Structure(NPTypingType, ABC, metaclass=StructureMeta): + """ + A container for structure expressions that describe the structured dtype of + an array. + + Simple example: + + >>> Structure["x: Float, y: Float"] + Structure['[x, y]: Float'] + + """ + + _type_per_name = {} + _has_wildcard = False + + @classmethod + def has_wildcard(cls) -> bool: + """ + Returns whether this Structure has a wildcard for any other columns. + :return: True if this Structure expresses "any other columns". + """ + return cls._has_wildcard + + @classmethod + def get_types(cls) -> List[str]: + """ + Return a list of all types (strings) in this Structure. + :return: a list of all types in this Structure. + """ + return list(set(cls._type_per_name.values())) + + @classmethod + def get_names(cls) -> List[str]: + """ + Return a list of all names in this Structure. + :return: a list of all names in this Structure. + """ + return list(cls._type_per_name.keys()) + + @classmethod + def get_type(cls, name: str) -> str: + """ + Get the type (str) that corresponds to the given name. For example for + Structure["x: Float"], get_type("x") would give "Float". + :param name: the name of which the type is to be returned. + :return: the type as a string that corresponds to that name. + """ + return cls._type_per_name[name] diff --git a/src/numpydantic/vendor/nptyping/structure.pyi b/src/numpydantic/vendor/nptyping/structure.pyi new file mode 100644 index 0000000..415cf4e --- /dev/null +++ b/src/numpydantic/vendor/nptyping/structure.pyi @@ -0,0 +1,38 @@ +""" +MIT License + +Copyright (c) 2023 Ramon Hagenaars + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" +try: + from typing import Literal # type: ignore[attr-defined] +except ImportError: + from typing_extensions import Literal # type: ignore[attr-defined,misc,assignment] + +from typing import Any, cast + +import numpy as np + +# For MyPy: +Structure = cast(Literal, Structure) # type: ignore[has-type,misc,valid-type] + +# For PyRight: +class Structure(np.dtype[Any]): # type: ignore[no-redef,misc] + def __class_getitem__(cls, item: Any) -> Any: ... diff --git a/src/numpydantic/vendor/nptyping/structure_expression.py b/src/numpydantic/vendor/nptyping/structure_expression.py new file mode 100644 index 0000000..ee1236c --- /dev/null +++ b/src/numpydantic/vendor/nptyping/structure_expression.py @@ -0,0 +1,339 @@ +""" +MIT License + +Copyright (c) 2023 Ramon Hagenaars + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" +import re +from collections import Counter, defaultdict +from difflib import get_close_matches +from typing import ( + TYPE_CHECKING, + Any, + Dict, + Generator, + List, + Mapping, + Tuple, + Type, + Union, +) + +import numpy as np + +from nptyping.error import InvalidShapeError, InvalidStructureError +from nptyping.shape import Shape +from nptyping.shape_expression import ( + check_shape, + normalize_shape_expression, + validate_shape_expression, +) +from nptyping.typing_ import StructureExpression + +if TYPE_CHECKING: + from nptyping.structure import Structure # pragma: no cover + + +def validate_structure_expression( + structure_expression: Union[StructureExpression, Any] +) -> None: + """ + Validate the given structure_expression and raise an InvalidStructureError + if it is deemed invalid. + :param structure_expression: the structure expression in question. + :return: None. + """ + if structure_expression is not Any: + if not re.match(_REGEX_STRUCTURE_EXPRESSION, structure_expression): + raise InvalidStructureError( + f"'{structure_expression}' is not a valid structure expression." + ) + _validate_structure_expression_contains_no_multiple_field_names( + structure_expression + ) + _validate_sub_array_expressions(structure_expression) + + +def check_structure( + structured_dtype: np.dtype, # type: ignore[type-arg] + target: "Structure", + type_per_name: Dict[str, type], +) -> bool: + """ + Check the given structured_dtype against the given target Structure and + return whether it corresponds (True) or not (False). The given dictionary + contains the vocabulary context for the check. + :param structured_dtype: the dtype in question. + :param target: the target Structure that is checked against. + :param type_per_name: a dict that holds the types by their names as they + occur in a structure expression. + :return: True if the given dtype is valid with the given target. + """ + fields: Mapping[str, Any] = structured_dtype.fields or {} # type: ignore[assignment] + + # Add the wildcard to the lexicon. We want to do this here to keep + # knowledge on wildcards in one place (this module). + type_per_name_with_wildcard: Dict[str, type] = { + **type_per_name, + "*": object, + } # type: ignore[arg-type] + + if target.has_wildcard(): + # Check from the Target's perspective. All fields in the Target should be + # in the subject. + def iterator() -> Generator[Tuple[str, Tuple[np.dtype, int]], None, None]: # type: ignore[type-arg] # pylint: disable=line-too-long + for name_ in target.get_names(): + yield name_, fields.get(name_) # type: ignore[misc] + + else: + # Check from the subject's perspective. All fields in the subject + # should be in the target. + if set(target.get_names()) != set(fields.keys()): + return False + + def iterator() -> Generator[Tuple[str, Tuple[np.dtype, int]], None, None]: # type: ignore[type-arg] # pylint: disable=line-too-long + for name_, dtype_tuple_ in fields.items(): + yield name_, dtype_tuple_ # type: ignore[misc] + + for name, dtype_tuple in iterator(): + field_in_target_not_in_subject = dtype_tuple is None + if field_in_target_not_in_subject or not _check_structure_field( + name, dtype_tuple, target, type_per_name_with_wildcard + ): + return False + return True + + +def _check_structure_field( + name: str, + dtype_tuple: Tuple[np.dtype, int], # type: ignore[type-arg] + target: "Structure", + type_per_name_with_wildcard: Dict[str, type], +) -> bool: + dtype = dtype_tuple[0] + target_type_name = target.get_type(name) + target_type_shape_match = re.search(_REGEX_FIELD_SHAPE, target_type_name) + actual_type = dtype.type + if target_type_shape_match: + if not dtype.subdtype: + # the dtype does not contain a shape. + return False + actual_type = dtype.subdtype[0].type + target_type_shape = target_type_shape_match.group(1) + shape_corresponds = check_shape(dtype.shape, Shape[target_type_shape]) + if not shape_corresponds: + return False + target_type_name = target_type_name.replace( + target_type_shape_match.group(0), "" + ) + check_type_name(target_type_name, type_per_name_with_wildcard) + target_type = type_per_name_with_wildcard[target_type_name] + return issubclass(actual_type, target_type) + + +def check_type_names( + structure: "Structure", type_per_name: Dict[str, Type[object]] +) -> None: + """ + Check the given structure for any invalid type names in the given context + of type_per_name. Raises an InvalidStructureError if a type name is + invalid. + :param structure: the Structure that is checked. + :param type_per_name: the context that determines which type names are valid. + :return: None. + """ + for type_ in structure.get_types(): + check_type_name(type_, type_per_name) + + +def check_type_name(type_name: str, type_per_name: Dict[str, Type[object]]) -> None: + """ + Check if the given type_name is in type_per_name and raise a meaningful + error if not. + :param type_name: the key that is checked to be in type_per_name. + :param type_per_name: a dict that is looked in for type_name. + :return: None. + """ + # Remove any subarray stuff here. + type_name = type_name.split("[")[0] + if type_name not in type_per_name: + close_matches = get_close_matches( + type_name, type_per_name.keys(), 3, cutoff=0.4 + ) + close_matches_str = ", ".join(f"'{match}'" for match in close_matches) + extra_help = "" + if len(close_matches) > 1: + extra_help = f" Did you mean one of {close_matches_str}?" + elif close_matches: + extra_help = f" Did you mean {close_matches_str}?" + raise InvalidStructureError( # pylint: disable=raise-missing-from + f"Type '{type_name}' is not valid in this context.{extra_help}" + ) + + +def normalize_structure_expression( + structure_expression: StructureExpression, +) -> StructureExpression: + """ + Normalize the given structure expression, e.g. by removing whitespaces, + making similar expressions look the same. + :param structure_expression: the structure expression that is to be normalized. + :return: a normalized structure expression. + """ + structure_expression = re.sub(r"\s*", "", structure_expression) + type_to_names_dict = _create_type_to_names_dict(structure_expression) + normalized_structure_expression = _type_to_names_dict_to_str(type_to_names_dict) + result = normalized_structure_expression.replace(",", ", ").replace(" ", " ") + has_wildcard_end = structure_expression.replace(" ", "").endswith(",*") + if has_wildcard_end: + result += ", *" + return result + + +def create_name_to_type_dict( + structure_expression: StructureExpression, +) -> Dict[str, str]: + """ + Create a dict with a name as key and a type (str) as value from the given + structure expression. Structure["x: Int, y: Float"] would yield + {"x: "Int", "y": "Float"}. + :param structure_expression: the structure expression from which the dict + is extracted. + :return: a dict with names and their types, both as strings. + """ + type_to_names_dict = _create_type_to_names_dict(structure_expression) + return { + name.strip(): type_.strip() + for type_, names in type_to_names_dict.items() + for name in names + } + + +def _validate_structure_expression_contains_no_multiple_field_names( + structure_expression: StructureExpression, +) -> None: + # Validate that there are not multiple occurrences of the same field names. + matches = re.findall(_REGEX_FIELD, re.sub(r"\s*", "", structure_expression)) + field_name_combinations = [match[0].split(":")[0] for match in matches] + field_names: List[str] = [] + for field_name_combination in field_name_combinations: + field_name_combination_match = re.match( + _REGEX_FIELD_NAMES_COMBINATION, field_name_combination + ) + if field_name_combination_match: + field_names += field_name_combination_match.group(2).split(_SEPARATOR) + else: + field_names.append(field_name_combination) + field_name_counter = Counter(field_names) + field_names_occurring_multiple_times = [ + field_name for field_name, amount in field_name_counter.items() if amount > 1 + ] + if field_names_occurring_multiple_times: + # If there are multiple, just raise about the first. Otherwise the + # error message gets bloated. + field_name_under_fire = field_names_occurring_multiple_times[0] + raise InvalidStructureError( + f"Field names may occur only once in a structure expression." + f" Field name '{field_name_under_fire}' occurs" + f" {field_name_counter[field_name_under_fire]} times in" + f" '{structure_expression}'." + ) + + +def _validate_sub_array_expressions(structure_expression: str) -> None: + # Validate that the given structure expression does not contain any shape + # expressions for sub arrays that are invalid. + for field_match in re.findall(_REGEX_FIELD, structure_expression): + field_type = field_match[0].split(_FIELD_TYPE_POINTER)[1] + type_shape_match = re.search(_REGEX_FIELD_SHAPE, field_type) + if type_shape_match: + type_shape = type_shape_match[1] + try: + validate_shape_expression(type_shape) + except InvalidShapeError as err: + raise InvalidStructureError( + f"'{structure_expression}' is not a valid structure" + f" expression; {str(err)}" + ) from err + + +def _create_type_to_names_dict( + structure_expression: StructureExpression, +) -> Dict[str, List[str]]: + # Create a dictionary with field names per type, sorted by type and then by + # name. + names_per_type: Dict[str, List[str]] = defaultdict(list) + for field_match in re.findall(_REGEX_FIELD, structure_expression): + field_name_combination, field_type = field_match[0].split(_FIELD_TYPE_POINTER) + field_name_combination_match = re.match( + _REGEX_FIELD_NAMES_COMBINATION, field_name_combination + ) + field_type_shape_match = re.search(_REGEX_FIELD_SHAPE, field_type) + if field_name_combination_match: + field_names = field_name_combination_match.group(2).split(_SEPARATOR) + else: + field_names = [field_name_combination] + if field_type_shape_match: + type_shape = field_type_shape_match.group(1) + normalized_type_shape = normalize_shape_expression(type_shape) + field_type = field_type.replace( + field_type_shape_match.group(0), f"[{normalized_type_shape}]" + ) + names_per_type[field_type] += field_names + return { + field_type: sorted(names_per_type[field_type]) + for field_type in sorted(names_per_type.keys()) + } + + +def _type_to_names_dict_to_str(type_to_names_dict: Dict[str, List[str]]) -> str: + # Turn the given dict into a structure expression. + field_strings = [] + for field_type, field_names in type_to_names_dict.items(): + field_names_joined = f"{_SEPARATOR}".join(field_names) + if len(field_names) > 1: + field_names_joined = f"[{field_names_joined}]" + field_strings.append(f"{field_names_joined}{_FIELD_TYPE_POINTER} {field_type}") + return f"{_SEPARATOR}".join(field_strings) + + +_SEPARATOR = "," +_FIELD_TYPE_POINTER = ":" +_REGEX_SEPARATOR = rf"(\s*{_SEPARATOR}\s*)" +_REGEX_FIELD_NAME = r"(\s*[a-zA-Z]\w*\s*)" +_REGEX_FIELD_NAMES = rf"({_REGEX_FIELD_NAME}({_REGEX_SEPARATOR}{_REGEX_FIELD_NAME})+)" +_REGEX_FIELD_NAMES_COMBINATION = rf"(\s*\[{_REGEX_FIELD_NAMES}\]\s*)" +_REGEX_FIELD_LEFT = rf"({_REGEX_FIELD_NAME}|{_REGEX_FIELD_NAMES_COMBINATION})" +_REGEX_FIELD_TYPE = r"(\s*[a-zA-Z]\w*\s*)" +_REGEX_FIELD_TYPE_WILDCARD = r"(\s*\*\s*)" +_REGEX_FIELD_SHAPE = r"\[([^\]]+)\]" +_REGEX_FIELD_SHAPE_MAYBE = rf"\s*({_REGEX_FIELD_SHAPE})?\s*" +_REGEX_FIELD_RIGHT = ( + rf"({_REGEX_FIELD_TYPE}|{_REGEX_FIELD_TYPE_WILDCARD}){_REGEX_FIELD_SHAPE_MAYBE}" +) +_REGEX_FIELD_TYPE_POINTER = rf"(\s*{_FIELD_TYPE_POINTER}\s*)" +_REGEX_FIELD = ( + rf"(\s*{_REGEX_FIELD_LEFT}{_REGEX_FIELD_TYPE_POINTER}{_REGEX_FIELD_RIGHT}\s*)" +) +_REGEX_STRUCTURE_EXPRESSION = ( + rf"^({_REGEX_FIELD}" + rf"({_REGEX_SEPARATOR}{_REGEX_FIELD})*" + rf"({_REGEX_SEPARATOR}{_REGEX_FIELD_TYPE_WILDCARD})?)$" +) diff --git a/src/numpydantic/vendor/nptyping/typing_.py b/src/numpydantic/vendor/nptyping/typing_.py new file mode 100644 index 0000000..2639e07 --- /dev/null +++ b/src/numpydantic/vendor/nptyping/typing_.py @@ -0,0 +1,185 @@ +""" +MIT License + +Copyright (c) 2023 Ramon Hagenaars + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" + +try: + from typing import ( # type: ignore[attr-defined,misc] # pylint: disable=unused-import + Literal, + TypeAlias, + TypeGuard, + final, + ) +except ImportError: # pragma: no cover + from typing_extensions import ( # type: ignore[attr-defined,misc] + Literal, + TypeAlias, + TypeGuard, + final, + ) + +from typing import Tuple, Union + +import numpy as np + +ShapeExpression: TypeAlias = str +StructureExpression: TypeAlias = str +DType: TypeAlias = Union[np.generic, StructureExpression] +ShapeTuple: TypeAlias = Tuple[int, ...] + +Number = np.number +Bool = np.bool_ +Bool8 = np.bool8 +Obj = np.object_ # Obj is a common abbreviation and should be usable. +Object = np.object_ +Object0 = np.object0 +Datetime64 = np.datetime64 +Integer = np.integer +SignedInteger = np.signedinteger +Int8 = np.int8 +Int16 = np.int16 +Int32 = np.int32 +Int64 = np.int64 +Byte = np.byte +Short = np.short +IntC = np.intc +IntP = np.intp +Int0 = np.int0 +Int = np.integer # Int should translate to the "generic" int type. +Int_ = np.int_ +LongLong = np.longlong +Timedelta64 = np.timedelta64 +UnsignedInteger = np.unsignedinteger +UInt8 = np.uint8 +UInt16 = np.uint16 +UInt32 = np.uint32 +UInt64 = np.uint64 +UByte = np.ubyte +UShort = np.ushort +UIntC = np.uintc +UIntP = np.uintp +UInt0 = np.uint0 +UInt = np.uint +ULongLong = np.ulonglong +Inexact = np.inexact +Floating = np.floating +Float16 = np.float16 +Float32 = np.float32 +Float64 = np.float64 +Half = np.half +Single = np.single +Double = np.double +Float = np.float_ +LongDouble = np.longdouble +LongFloat = np.longfloat +ComplexFloating = np.complexfloating +Complex64 = np.complex64 +Complex128 = np.complex128 +CSingle = np.csingle +SingleComplex = np.singlecomplex +CDouble = np.cdouble +Complex = np.complex_ +CFloat = np.cfloat +CLongDouble = np.clongdouble +CLongFloat = np.clongfloat +LongComplex = np.longcomplex +Flexible = np.flexible +Void = np.void +Void0 = np.void0 +Character = np.character +Bytes = np.bytes_ +Str = np.str_ +String = np.string_ +Bytes0 = np.bytes0 +Unicode = np.unicode_ +Str0 = np.str0 + +dtypes = [ + (Number, "Number"), + (Bool, "Bool"), + (Bool8, "Bool8"), + (Obj, "Obj"), + (Object, "Object"), + (Object0, "Object0"), + (Datetime64, "Datetime64"), + (Integer, "Integer"), + (SignedInteger, "SignedInteger"), + (Int8, "Int8"), + (Int16, "Int16"), + (Int32, "Int32"), + (Int64, "Int64"), + (Byte, "Byte"), + (Short, "Short"), + (IntC, "IntC"), + (IntP, "IntP"), + (Int0, "Int0"), + (Int, "Int"), + (LongLong, "LongLong"), + (Timedelta64, "Timedelta64"), + (UnsignedInteger, "UnsignedInteger"), + (UInt8, "UInt8"), + (UInt16, "UInt16"), + (UInt32, "UInt32"), + (UInt64, "UInt64"), + (UByte, "UByte"), + (UShort, "UShort"), + (UIntC, "UIntC"), + (UIntP, "UIntP"), + (UInt0, "UInt0"), + (UInt, "UInt"), + (ULongLong, "ULongLong"), + (Inexact, "Inexact"), + (Floating, "Floating"), + (Float16, "Float16"), + (Float32, "Float32"), + (Float64, "Float64"), + (Half, "Half"), + (Single, "Single"), + (Double, "Double"), + (Float, "Float"), + (LongDouble, "LongDouble"), + (LongFloat, "LongFloat"), + (ComplexFloating, "ComplexFloating"), + (Complex64, "Complex64"), + (Complex128, "Complex128"), + (CSingle, "CSingle"), + (SingleComplex, "SingleComplex"), + (CDouble, "CDouble"), + (Complex, "Complex"), + (CFloat, "CFloat"), + (CLongDouble, "CLongDouble"), + (CLongFloat, "CLongFloat"), + (LongComplex, "LongComplex"), + (Flexible, "Flexible"), + (Void, "Void"), + (Void0, "Void0"), + (Character, "Character"), + (Bytes, "Bytes"), + (String, "String"), + (Str, "Str"), + (Bytes0, "Bytes0"), + (Unicode, "Unicode"), + (Str0, "Str0"), +] + +name_per_dtype = dict(dtypes) +dtype_per_name = {name: dtype for dtype, name in dtypes} diff --git a/src/numpydantic/vendor/nptyping/typing_.pyi b/src/numpydantic/vendor/nptyping/typing_.pyi new file mode 100644 index 0000000..fcf83ce --- /dev/null +++ b/src/numpydantic/vendor/nptyping/typing_.pyi @@ -0,0 +1,114 @@ +""" +MIT License + +Copyright (c) 2023 Ramon Hagenaars + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" + +try: + from typing import ( # type: ignore[attr-defined] # pylint: disable=unused-import + Dict, + TypeAlias, + ) +except ImportError: # pragma: no cover + from typing_extensions import ( + TypeAlias, + ) + +from typing import ( + Any, + Tuple, + Union, +) + +import numpy as np + +ShapeExpression: TypeAlias = str +StructureExpression: TypeAlias = str +DType: TypeAlias = Union[np.generic, StructureExpression] +ShapeTuple: TypeAlias = Tuple[int, ...] + +Number: TypeAlias = np.dtype[np.number[Any]] +Bool: TypeAlias = np.dtype[np.bool_] +Bool8: TypeAlias = np.dtype[np.bool8] +Object: TypeAlias = np.dtype[np.object_] +Object0: TypeAlias = np.dtype[np.object0] +Datetime64: TypeAlias = np.dtype[np.datetime64] +Integer: TypeAlias = np.dtype[np.integer[Any]] +SignedInteger: TypeAlias = np.dtype[np.signedinteger[Any]] +Int8: TypeAlias = np.dtype[np.int8] +Int16: TypeAlias = np.dtype[np.int16] +Int32: TypeAlias = np.dtype[np.int32] +Int64: TypeAlias = np.dtype[np.int64] +Byte: TypeAlias = np.dtype[np.byte] +Short: TypeAlias = np.dtype[np.short] +IntC: TypeAlias = np.dtype[np.intc] +IntP: TypeAlias = np.dtype[np.intp] +Int0: TypeAlias = np.dtype[np.int0] +Int: TypeAlias = np.dtype[np.int_] +LongLong: TypeAlias = np.dtype[np.longlong] +Timedelta64: TypeAlias = np.dtype[np.timedelta64] +UnsignedInteger: TypeAlias = np.dtype[np.unsignedinteger[Any]] +UInt8: TypeAlias = np.dtype[np.uint8] +UInt16: TypeAlias = np.dtype[np.uint16] +UInt32: TypeAlias = np.dtype[np.uint32] +UInt64: TypeAlias = np.dtype[np.uint64] +UByte: TypeAlias = np.dtype[np.ubyte] +UShort: TypeAlias = np.dtype[np.ushort] +UIntC: TypeAlias = np.dtype[np.uintc] +UIntP: TypeAlias = np.dtype[np.uintp] +UInt0: TypeAlias = np.dtype[np.uint0] +UInt: TypeAlias = np.dtype[np.uint] +ULongLong: TypeAlias = np.dtype[np.ulonglong] +Inexact: TypeAlias = np.dtype[np.inexact[Any]] +Floating: TypeAlias = np.dtype[np.floating[Any]] +Float16: TypeAlias = np.dtype[np.float16] +Float32: TypeAlias = np.dtype[np.float32] +Float64: TypeAlias = np.dtype[np.float64] +Half: TypeAlias = np.dtype[np.half] +Single: TypeAlias = np.dtype[np.single] +Double: TypeAlias = np.dtype[np.double] +Float: TypeAlias = np.dtype[np.float_] +LongDouble: TypeAlias = np.dtype[np.longdouble] +LongFloat: TypeAlias = np.dtype[np.longfloat] +ComplexFloating: TypeAlias = np.dtype[np.complexfloating[Any, Any]] +Complex64: TypeAlias = np.dtype[np.complex64] +Complex128: TypeAlias = np.dtype[np.complex128] +CSingle: TypeAlias = np.dtype[np.csingle] +SingleComplex: TypeAlias = np.dtype[np.singlecomplex] +CDouble: TypeAlias = np.dtype[np.cdouble] +Complex: TypeAlias = np.dtype[np.complex_] +CFloat: TypeAlias = np.dtype[np.cfloat] +CLongDouble: TypeAlias = np.dtype[np.clongdouble] +CLongFloat: TypeAlias = np.dtype[np.clongfloat] +LongComplex: TypeAlias = np.dtype[np.longcomplex] +Flexible: TypeAlias = np.dtype[np.flexible] +Void: TypeAlias = np.dtype[np.void] +Void0: TypeAlias = np.dtype[np.void0] +Character: TypeAlias = np.dtype[np.character] +Bytes: TypeAlias = np.dtype[np.bytes_] +Str: TypeAlias = np.dtype[np.str_] +String: TypeAlias = np.dtype[np.string_] +Bytes0: TypeAlias = np.dtype[np.bytes0] +Unicode: TypeAlias = np.dtype[np.unicode_] +Str0: TypeAlias = np.dtype[np.str0] + +dtype_per_name: Dict[str, np.dtype[Any]] +name_per_dtype: Dict[np.dtype[Any], str]