├── .github
└── workflows
│ └── pythonapp.yml
├── CONTRIBUTING.md
├── HISTORY.md
├── LICENSE
├── MANIFEST.in
├── README.md
├── USERDOCS.md
├── constraints-3.10.txt
├── constraints-3.11.txt
├── constraints-3.7.txt
├── constraints-3.8.txt
├── constraints-3.9.txt
├── dependencies
├── build-requirements.txt
├── pandas-requirements.txt
├── qa-requirements.txt
└── requirements.txt
├── nptyping
├── __init__.py
├── assert_isinstance.py
├── base_meta_classes.py
├── error.py
├── ndarray.py
├── ndarray.pyi
├── nptyping_type.py
├── package_info.py
├── pandas_
│ ├── __init__.py
│ ├── dataframe.py
│ ├── dataframe.pyi
│ └── typing_.py
├── py.typed
├── recarray.py
├── recarray.pyi
├── shape.py
├── shape.pyi
├── shape_expression.py
├── structure.py
├── structure.pyi
├── structure_expression.py
├── typing_.py
└── typing_.pyi
├── resources
├── logo.pdn
└── logo.png
├── setup.cfg
├── setup.py
├── tasks.py
└── tests
├── __init__.py
├── pandas_
├── __init__.py
├── test_dataframe.py
├── test_fork_sync.py
└── test_mypy_dataframe.py
├── test_assert_isinstance.py
├── test_base_meta_classes.py
├── test_beartype.py
├── test_help_texts.py
├── test_helpers
├── __init__.py
├── check_mypy_on_code.py
└── temp_file.py
├── test_lib_export.py
├── test_mypy.py
├── test_ndarray.py
├── test_package_info.py
├── test_performance.py
├── test_pyright.py
├── test_recarray.py
├── test_shape.py
├── test_shape_expression.py
├── test_structure.py
├── test_structure_expression.py
├── test_typeguard.py
└── test_wheel.py
/.github/workflows/pythonapp.yml:
--------------------------------------------------------------------------------
1 | name: nptyping
2 |
3 | on: [push]
4 |
5 | jobs:
6 |
7 | test:
8 | runs-on: ${{ matrix.os }}
9 | strategy:
10 | matrix:
11 | python-version: [ '3.7', '3.8', '3.9', '3.10', '3.11' ]
12 | os: [ ubuntu-latest, macOS-latest, windows-latest ]
13 | name: Python ${{ matrix.python-version }} on ${{ matrix.os }}
14 | steps:
15 | - uses: actions/checkout@master
16 | - name: Setup python
17 | uses: actions/setup-python@v1
18 | with:
19 | python-version: ${{ matrix.python-version }}
20 | architecture: x64
21 | - name: Install Dependencies
22 | run: |
23 | python -m pip install --upgrade pip
24 | pip install invoke
25 | invoke venv install
26 | - name: Coverage
27 | run: |
28 | invoke coverage
29 | invoke run --command="-m codecov"
30 |
31 | qa:
32 | runs-on: windows-latest
33 | steps:
34 | - uses: actions/checkout@master
35 | - name: Setup python
36 | uses: actions/setup-python@v1
37 | with:
38 | python-version: '3.10'
39 | architecture: x64
40 | - name: Install Dependencies
41 | run: |
42 | python -m pip install --upgrade pip
43 | pip install invoke
44 | invoke venv install
45 | - name: Doctest
46 | run: |
47 | invoke doctest
48 | - name: Pylint
49 | run: |
50 | invoke pylint
51 | - name: Style
52 | run: |
53 | invoke format --check
54 |
55 | report:
56 | needs: test
57 | runs-on: windows-latest
58 | steps:
59 | - name: Coverage Report
60 | uses: codecov/codecov-action@v1
61 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | # *Contributing*
6 |
7 | * [Introduction](#Introduction)
8 | * [Bug reporting](#Bug-reporting)
9 | * [Pull requests](#Pull-requests)
10 | * [Development](#Development)
11 |
12 | ## Introduction
13 | Thank you for showing interest in contributing to this library. This document is intended to be a guideline to make any
14 | contribution as smooth as possible.
15 |
16 | ## Bug reporting
17 | When reporting a bug, please first check if it has been reported already in the list of
18 | [issues](https://github.com/ramonhagenaars/nptyping/issues). Also check the
19 | [closed ones](https://github.com/ramonhagenaars/nptyping/issues?q=is%3Aissue+is%3Aclosed).
20 |
21 | If your bug was not specified earlier, please [open a new issue](https://github.com/ramonhagenaars/nptyping/issues/new).
22 | When describing your bug, try to be as clear as possible. At least provide:
23 |
24 | * the Python version you used
25 | * the `nptyping` version you used
26 | * the Operating system you were on
27 |
28 | If applicable and possible, provide a complete stacktrace of the error.
29 |
30 | ## Pull requests
31 | You are free to open pull requests: this is highly appreciated! To avoid any waste of valuable developer time, it is
32 | recommended to first [open a new issue](https://github.com/ramonhagenaars/nptyping/issues/new) describing the
33 | feature/fix that you propose. This is not mandatory though.
34 |
35 | A pull request can be merged when:
36 | * all [checks](https://github.com/ramonhagenaars/nptyping/actions) are green
37 | * the content is deemed an improvement
38 | * the code is deemed acceptable
39 |
40 | ## Development
41 | Prerequisites:
42 | * A Python version within the `nptyping` [supported range](https://github.com/ramonhagenaars/nptyping/blob/master/nptyping/package_info.py)
43 | * An IDE to your liking
44 |
45 | ### Step 1: clone this repository
46 | Clone this repo in a space on your machine that you have sufficient rights on. For more info on cloning, please refer to
47 | the [Github Docs](https://docs.github.com/en/get-started/getting-started-with-git/about-remote-repositories#cloning-with-https-urls).
48 |
49 | ### Step 2: install invoke
50 | The build tool `invoke` is used in this repository. It is recommended to install it in your global python setup:
51 | ```
52 | pip install invoke
53 | ```
54 |
55 | ### Step 3: setup a ready-to-go virtual environment
56 | Make sure you cd to the directory of this repo that contains `tasks.py`. Then you can execute the following:
57 | ```
58 | invoke venv install
59 | ```
60 |
61 | When done, you can check all available build options by executing:
62 | ```
63 | invoke --list
64 | ```
65 |
66 | #### Optional: different Python versions
67 | Optionally, you can create multiple virtual environments for different Python versions. To do so, make sure you have
68 | `invoke` installed on that Python version, then use that specific Python interpreter to create a virtual environment.
69 | Here is an example of how that command would look like on a Windows machine:
70 | ```
71 | C:\Users\guidovanrossum\AppData\Local\Programs\Python\Python38\python -m invoke venv
72 | ```
73 | If you now invoke the tests, it will by default execute them using multiple virtual environments. this lets you check
74 | compatibility with different Python versions.
75 |
76 | ### Step 4: start developing
77 | You are now ready to go. You might want to point your IDEs interpreter to the Python executable in the created virtual
78 | environment.
79 |
80 | While you develop, it is a good idea to run the following tasks every now and then:
81 | ```
82 | invoke format qa coverage
83 | ```
84 |
85 | Happy coding!
86 |
--------------------------------------------------------------------------------
/HISTORY.md:
--------------------------------------------------------------------------------
1 | # History
2 |
3 | ## 2.5.0 (2023-02-20)
4 |
5 | - Added the column wildcard in structure expressions to allow expressing 'a structure with at least ...'.
6 | - Fixed the `help` text for functions that use `nptyping` types as hints.
7 | - Fixed the distribution of `dataframe.pyi` that was missing.
8 | - Fixed the sdist to include tests and dependencies.
9 |
10 | ## 2.4.1 (2022-11-16)
11 |
12 | - Fixed compatibility with `mypy==0.991`.
13 |
14 | ## 2.4.0 (2022-11-14)
15 |
16 | - Added hint for pandas DataFrame.
17 | - Fixed bug for checking against a Structure where a different number of fields did not fail the check.
18 | - Changed `nptyping.Int` pointing to the more generic `numpy.integer` rather than `numpy.int32`.
19 | - Added support for Python 3.11 with the exception of `pandas.DataFrame`.
20 |
21 | ## 2.3.1 (2022-08-30)
22 |
23 | - Fixed mypy error of inheriting final dtype as of numpy==1.23.1.
24 | - Allowed for quotes in shape expressions to appease PyCharm.
25 |
26 | ## 2.3.0 (2022-08-28)
27 |
28 | - Added support for subarrays with shape expressions inside structure expressions.
29 | - Added support for wildcards in structure expressions.
30 |
31 | ## 2.2.0 (2022-06-26)
32 |
33 | - Added support for expressing "at least N dimensions".
34 |
35 | ## 2.1.3 (2022-06-19)
36 |
37 | - Fixed typing issue with Pyright/Pylance that caused the message: "Literal" is not a class
38 | - Fixed wrong error message when an invalid `Structure` was provided to `NDArray`.
39 |
40 | ## 2.1.2 (2022-06-08)
41 |
42 | - Fixed bug that caused MyPy to fail with the message: Value of type variable "_DType_co" of "ndarray" cannot be "floating[Any]"
43 |
44 | ## 2.1.1 (2022-06-01)
45 |
46 | - Fixed bug that numpy ndarrays were incorrectly instance checked against `RecArray`.
47 |
48 | ## 2.1.0 (2022-06-01)
49 |
50 | - Added `Structure` and "structure expressions" to support structured arrays.
51 | - Added `RecArray`.
52 |
53 | ## 2.0.1 (2022-04-28)
54 |
55 | Thanks to [Jasha10](https://github.com/Jasha10) for this release.
56 | - Added an improved default message for `assert_isinstance`.
57 |
58 | Also some typos in README, in `test_mypy.py` and some style corrections.
59 |
60 | ## 2.0.0 (2022-04-07)
61 |
62 | Changes since `1.4.4`:
63 | - Changed the interface of `NDArray` into `NDArray[SHAPE, DTYPE]`
64 | - Added MyPy-acceptance (limited static type checking)
65 | - Added support for variables
66 | - Added support for labels and named dimensions
67 | - Added support for all numpy dtypes with `NDArray`
68 | - Added support for dynamic type checker: beartype
69 | - Added support for dynamic type checker: typeguard
70 | - Added autocompletion for all attributes of `ndarray`
71 | - Added CONTRIBUTING.md
72 | - Removed support for Python 3.5 and Python 3.6
73 |
74 | ## 2.0.0a2 (2022-03-27)
75 |
76 | - Changed the interface of `NDArray`: switched the order to `NDArray[SHAPE, DTYPE]` to be compatible to `numpy.ndarray.pyi`
77 | - Added autocompletion for all attributes of `ndarray` by changing the implementation of `NDArray`
78 | - Added CONTRIBUTING.md
79 | - Added support for dynamic type checker: beartype
80 | - Added support for dynamic type checker: typeguard
81 |
82 | ## 2.0.0a1 (2022-03-19)
83 |
84 | - Changed the interface of `NDArray`
85 | - Added MyPy-acceptance (limited static type checking)
86 | - Added support for variables
87 | - Added support for labels and named dimensions
88 | - Added support for all numpy dtypes with `NDArray`
89 | - Removed support for Python 3.5 and Python 3.6
90 |
91 | ## 1.4.4 (2021-09-10)
92 |
93 | - Fixed instance checks with 0d arrays.
94 |
95 | ## 1.4.3 (2021-08-05)
96 |
97 | - Fixed setup.py to exclude test(-resources) in the wheel.
98 |
99 | ## 1.4.2 (2021-05-08)
100 |
101 | - Fixed instance check that was incompatible with `typish==1.9.2`.
102 |
103 | ## 1.4.1 (2021-03-23)
104 |
105 | - Fixed instance checks of some types that did not properly respond to non-numpy types.
106 | - Fixed instance checks with ``nptyping.Object``.
107 | - Fixed identities of NPTyping instances: ``NDArray[(3,), int] is NDArray[(3,), int]``.
108 |
109 | ## 1.4.0 (2020-12-23)
110 |
111 | - Added ``SubArrayType``
112 | - Added ``StructuredType``
113 | - Added support for unsigned integers with ``py_type``.
114 |
115 | ## 1.3.0 (2020-07-21)
116 |
117 | - Added ``Complex128``
118 |
119 | ## 1.2.0 (2020-06-20)
120 |
121 | - Added ``Bool``
122 | - Added ``Datetime64``
123 | - Added ``Timedelta64``
124 |
125 | ## 1.1.0 (2020-05-30)
126 |
127 | - Removed ``Array``
128 | - Added ``get_type``
129 | - Added ``Int``
130 | - Added ``UInt``
131 | - Added ``Float``
132 | - Added ``Unicode``
133 | - Added ``Number``
134 | - Added ``NPType``
135 |
136 | ## 1.0.1 (2020-04-05)
137 |
138 | - Added a hash function to ``_NDArrayMeta``.
139 |
140 | ## 1.0.0 (2020-03-22)
141 |
142 | - Added ``NDArray``
143 | - Deprecated ``Array``
144 |
145 | ## 0.3.0 (2019-09-11)
146 |
147 | - Forbidden instantiation of ``Array``
148 | - Added support for hinting ndarray methods
149 |
150 | ## 0.2.0 (2019-02-09)
151 |
152 | - Added support for heterogeneous arrays
153 | - Added HISTORY.rst
154 |
155 | ## 0.1.0 (2019-02-05)
156 |
157 | - Initial release
158 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright 2022, Ramon Hagenaars
2 |
3 | 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:
4 |
5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6 |
7 | 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.
8 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | # See also:
2 | # https://packaging.python.org/guides/using-manifest-in/#how-files-are-included-in-an-sdist
3 | include CONTRIBUTING.md
4 | include HISTORY.md
5 | include USERDOCS.md
6 | include dependencies/*
7 | include dependencies/**/*
8 | include resources/*
9 | include resources/**/*
10 | include tests/*.py
11 | include tests/**/*.py
12 | recursive-exclude *.pyc
13 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://img.shields.io/pypi/pyversions/nptyping.svg)
2 | [](https://pepy.tech/project/nptyping)
3 | [](https://badge.fury.io/py/nptyping)
4 | [](https://codecov.io/gh/ramonhagenaars/nptyping)
5 | [](https://img.shields.io/badge/code%20style-black-black)
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 | 🧊 *Type hints for `NumPy`*
15 | 🐼 *Type hints for `pandas.DataFrame`*
16 | 💡 *Extensive dynamic type checks for dtypes shapes and structures*
17 | 🚀 *[Jump to the Quickstart](https://github.com/ramonhagenaars/nptyping/blob/master/USERDOCS.md#Quickstart)*
18 |
19 | Example of a hinted `numpy.ndarray`:
20 |
21 | ```python
22 | >>> from nptyping import NDArray, Int, Shape
23 |
24 | >>> arr: NDArray[Shape["2, 2"], Int]
25 |
26 | ```
27 |
28 | Example of a hinted `pandas.DataFrame`:
29 |
30 | ```python
31 | >>> from nptyping import DataFrame, Structure as S
32 |
33 | >>> df: DataFrame[S["name: Str, x: Float, y: Float"]]
34 |
35 | ```
36 |
37 | ### Installation
38 |
39 | | Command | Description |
40 | |:---------------------------------|-------------------------------|
41 | | `pip install nptyping` | Install the basics |
42 | | `pip install nptyping[pandas]` | Install with pandas extension |
43 | | `pip install nptyping[complete]` | Install with all extensions |
44 |
45 | ### Instance checking
46 |
47 | Example of instance checking:
48 | ```python
49 | >>> import numpy as np
50 |
51 | >>> isinstance(np.array([[1, 2], [3, 4]]), NDArray[Shape["2, 2"], Int])
52 | True
53 |
54 | >>> isinstance(np.array([[1., 2.], [3., 4.]]), NDArray[Shape["2, 2"], Int])
55 | False
56 |
57 | >>> isinstance(np.array([1, 2, 3, 4]), NDArray[Shape["2, 2"], Int])
58 | False
59 |
60 | ```
61 |
62 | `nptyping` also provides `assert_isinstance`. In contrast to `assert isinstance(...)`, this won't cause IDEs or MyPy
63 | complaints. Here is an example:
64 | ```python
65 | >>> from nptyping import assert_isinstance
66 |
67 | >>> assert_isinstance(np.array([1]), NDArray[Shape["1"], Int])
68 | True
69 |
70 | ```
71 |
72 | ### NumPy Structured arrays
73 |
74 | You can also express structured arrays using `nptyping.Structure`:
75 | ```python
76 | >>> from nptyping import Structure
77 |
78 | >>> Structure["name: Str, age: Int"]
79 | Structure['age: Int, name: Str']
80 |
81 | ```
82 |
83 | Here is an example to see it in action:
84 | ```python
85 | >>> from typing import Any
86 | >>> import numpy as np
87 | >>> from nptyping import NDArray, Structure
88 |
89 | >>> arr = np.array([("Peter", 34)], dtype=[("name", "U10"), ("age", "i4")])
90 | >>> isinstance(arr, NDArray[Any, Structure["name: Str, age: Int"]])
91 | True
92 |
93 | ```
94 |
95 | Subarrays can be expressed with a shape expression between square brackets:
96 | ```python
97 | >>> Structure["name: Int[3, 3]"]
98 | Structure['name: Int[3, 3]']
99 |
100 | ```
101 |
102 | ### NumPy Record arrays
103 | The recarray is a specialization of a structured array. You can use `RecArray`
104 | to express them.
105 |
106 | ```python
107 | >>> from nptyping import RecArray
108 |
109 | >>> arr = np.array([("Peter", 34)], dtype=[("name", "U10"), ("age", "i4")])
110 | >>> rec_arr = arr.view(np.recarray)
111 | >>> isinstance(rec_arr, RecArray[Any, Structure["name: Str, age: Int"]])
112 | True
113 |
114 | ```
115 |
116 | ### Pandas DataFrames
117 | Pandas DataFrames can be expressed with `Structure` also. To make it more concise, you may want to alias `Structure`.
118 | ```python
119 | >>> from nptyping import DataFrame, Structure as S
120 |
121 | >>> df: DataFrame[S["x: Float, y: Float"]]
122 |
123 | ```
124 |
125 | ### More examples
126 |
127 | Here is an example of a rich expression that can be done with `nptyping`:
128 | ```python
129 | def plan_route(
130 | locations: NDArray[Shape["[from, to], [x, y]"], Float]
131 | ) -> NDArray[Shape["* stops, [x, y]"], Float]:
132 | ...
133 |
134 | ```
135 |
136 | More examples can be found in the [documentation](https://github.com/ramonhagenaars/nptyping/blob/master/USERDOCS.md#Examples).
137 |
138 | ## Documentation
139 |
140 | * [User documentation](https://github.com/ramonhagenaars/nptyping/blob/master/USERDOCS.md)
141 | The place to go if you are using this library.
142 |
143 | * [Release notes](https://github.com/ramonhagenaars/nptyping/blob/master/HISTORY.md)
144 | To see what's new, check out the release notes.
145 |
146 | * [Contributing](https://github.com/ramonhagenaars/nptyping/blob/master/CONTRIBUTING.md)
147 | If you're interested in developing along, find the guidelines here.
148 |
149 | * [License](https://github.com/ramonhagenaars/nptyping/blob/master/LICENSE)
150 | If you want to check out how open source this library is.
151 |
--------------------------------------------------------------------------------
/USERDOCS.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | # *User documentation*
6 |
7 | * [Introduction](#Introduction)
8 | * [Quickstart](#Quickstart)
9 | * [Usage](#Usage)
10 | * [NDArray](#NDArray)
11 | * [Shape expressions](#Shape-expressions)
12 | * [Syntax](#Syntax-shape-expressions)
13 | * [Validation](#Validation)
14 | * [Normalization](#Normalization)
15 | * [Variables](#Variables)
16 | * [Wildcards](#Shape-Wildcards)
17 | * [N dimensions](#N-dimensions)
18 | * [Dimension breakdowns](#Dimension-breakdowns)
19 | * [Labels](#Labels)
20 | * [DTypes](#DTypes)
21 | * [Structure expressions](#Structure-expressions)
22 | * [Syntax](#Syntax-structure-expressions)
23 | * [Subarrays](#Subarrays)
24 | * [Wildcards](#Structure-Wildcards)
25 | * [RecArray](#RecArray)
26 | * [Pandas DataFrame](#Pandas-DataFrame)
27 | * [Examples](#Examples)
28 | * [Similar projects](#Similar-projects)
29 | * [FAQ](#FAQ)
30 | * [About](#About)
31 |
32 | ## Introduction
33 |
34 | Thank you for showing interest in this library.
35 |
36 | The intended audience of this document, are Pythoneers using `numpy`, that want to make their code more readable and
37 | secure with type hints.
38 |
39 | In this document, all features that `nptyping` has to offer can be found. If you think that something is missing or not
40 | clear enough, please check the [issue section](https://github.com/ramonhagenaars/nptyping/issues) to see if you can find
41 | your answer there. Don't forget to also check the
42 | [closed issues](https://github.com/ramonhagenaars/nptyping/issues?q=is%3Aissue+is%3Aclosed). Otherwise, feel free to
43 | raise your question [in a new issue](https://github.com/ramonhagenaars/nptyping/issues/new).
44 |
45 | You will find a lot of code blocks in this document. If you wonder why they are written the way they are (e.g. with the
46 | `>>>` and the `...`): all code blocks are tested using [doctest](https://docs.python.org/3/library/doctest.html).
47 |
48 | ## Quickstart
49 | Install `nptyping` for the type hints and the recommended `beartype` for dynamic type checks:
50 | ```shell
51 | pip install nptyping[complete], beartype
52 | ```
53 |
54 | Use the combination of these packages to add type safety and readability:
55 | ```python
56 | # File: myfile.py
57 |
58 | >>> from nptyping import DataFrame, Structure as S
59 | >>> from beartype import beartype
60 |
61 | >>> @beartype # The function signature is now type safe
62 | ... def fun(df: DataFrame[S["a: Int, b: Str"]]) -> DataFrame[S["a: Int, b: Str"]]:
63 | ... return df
64 |
65 | ```
66 |
67 | On your production environments, run Python in optimized mode. This disables the type checks done by beartype and any
68 | overhead it may cause:
69 | ```shell
70 | python -OO myfile.py
71 | ```
72 | You're now good to go. You can sleep tight knowing that today you made your codebase safer and more transparent.
73 |
74 | ## Usage
75 |
76 | ### NDArray
77 | The `NDArray` is the main character of this library and can be used to describe `numpy.ndarray`.
78 |
79 | ```python
80 | >>> from nptyping import NDArray
81 |
82 | ```
83 | The `NDArray` can take 2 arguments between brackets: the dtype and the shape of the array that is being described. This
84 | takes the form `NDArray[Shape[], ]`. For example:
85 |
86 | ```python
87 | >>> from nptyping import UInt16, Shape
88 | >>> NDArray[Shape["5, 3"], UInt16]
89 | NDArray[Shape['5, 3'], UShort]
90 |
91 | ```
92 | You can use `typing.Any` to denote any dtype or any shape:
93 | ```python
94 | >>> from typing import Any
95 | >>> NDArray[Any, Any]
96 | NDArray[Any, Any]
97 |
98 | ```
99 |
100 | ### Shape expressions
101 | You can denote the shape of an array using what we call a **shape expression**. This expression - a string - can be put
102 | into `Shape` and can then be used in an `NDArray`.
103 | ```python
104 | >>> from nptyping import Shape
105 |
106 | ```
107 | An example of a shape expression in an `NDArray`:
108 | ```python
109 | >>> from typing import Any
110 |
111 | >>> NDArray[Shape["3, 4"], Any]
112 | NDArray[Shape['3, 4'], Any]
113 |
114 | ```
115 | The above example shows an expression of a shape consisting of 2 dimensions of respectively size 3 and size 4. a fitting
116 | array would be: `np.array([[11, 12, 13, 14], [21, 22, 23, 24], [31, 32, 33, 34]])`.
117 |
118 | `Shape` is actually just a rich alias for `typing.Literal`:
119 | ```python
120 | >>> from typing import Literal
121 |
122 | >>> Shape["2, 2"] == Literal['2, 2']
123 | True
124 |
125 | ```
126 | This also means that you can use `typing.Literal` instead of `Shape` if you want.
127 |
128 | #### Syntax shape expressions
129 |
130 | A shape expression is just a comma separated list of dimensions. A dimension can be denoted by its size, like is done in
131 | the former examples. But you can also use variables, labels, wildcards and dimension breakdowns:
132 | ```python
133 | >>> Shape["3, 3 withLabel, *, Var, [entry1, entry2, entry3]"]
134 | Shape['3, 3 withLabel, *, Var, [entry1, entry2, entry3]']
135 |
136 | ```
137 | The shape expression above denotes a shape of size 3, 3, any, any, 3. For more details on the concepts of variables,
138 | labels, wildcards and dimension breakdowns, they are described in the following sections.
139 |
140 | The syntax of a shape expression can be formalized in BNF. Extra whitespacing is allowed (e.g. around commas), but this
141 | is not included in the schema below (to avoid extra complexity).
142 | ```
143 | shape-expression = |","
144 | dimensions = |","
145 | dimension = |
146 | labeled-dimension = " "
147 | unlabeled-dimension = |||
148 | wildcard = "*"
149 | dimension-breakdown = "[""]"
150 | labels = |","
151 | label = |
152 | variable = |
153 | word = ||
154 | letter = |
155 | uletter = "A"|"B"|"C"|"D"|"E"|"F"|"G"|"H"|"I"|"J"|"K"|"L"|"M"|"N"|"O"|"P"|"Q"|"R"|"S"|"T"|"U"|"V"|"W"|"X"|"Y"|"Z"
156 | lletter = "a"|"b"|"c"|"d"|"e"|"f"|"g"|"h"|"i"|"j"|"k"|"l"|"m"|"n"|"o"|"p"|"q"|"r"|"s"|"t"|"u"|"v"|"w"|"x"|"y"|"z"
157 | number = |
158 | digit = "0"|"1"|"2"|"3"|"4"|"5"|"6"|"7"|"8"|"9"
159 | underscore = "_"
160 | ellipsis = "..."
161 | ```
162 |
163 | #### Validation
164 | Shape expressions are validated and may raise an `InvalidShapeError`.
165 | ```python
166 | >>> from nptyping import Shape, InvalidShapeError
167 |
168 | >>> try:
169 | ... Shape["3, 3,"]
170 | ... except InvalidShapeError as err:
171 | ... print(err)
172 | '3, 3,' is not a valid shape expression.
173 |
174 | ```
175 |
176 | #### Normalization
177 | Shape expressions are normalized, so your "shape expression style" won't affect its working.
178 |
179 | ```python
180 | >>> from nptyping import Shape
181 |
182 | >>> Shape[" 3 , 3 "]
183 | Shape['3, 3']
184 |
185 | ```
186 |
187 | #### Variables
188 | Variables can be used to describe dimensions of variable size:
189 | ```python
190 | >>> from numpy import random
191 | >>> isinstance(random.randn(2, 2), NDArray[Shape["Size, Size"], Any])
192 | True
193 | >>> isinstance(random.randn(100, 100), NDArray[Shape["Size, Size"], Any])
194 | True
195 | >>> isinstance(random.randn(42, 43), NDArray[Shape["Size, Size"], Any])
196 | False
197 |
198 | ```
199 | They are interpreted from left to right. This means that in the last example, upon instance checking, `Size` becomes
200 | `42`, which is then checked against `43`, hence the `False`.
201 |
202 | A variable is a word that may contain underscores and digits as long as *it starts with an uppercase letter*.
203 |
204 | #### Shape Wildcards
205 | A wildcard accepts any dimension size. It is denoted by the asterisk (`*`). Example:
206 | ```python
207 | >>> isinstance(random.randn(42, 43), NDArray[Shape["*, *"], Any])
208 | True
209 |
210 | ```
211 |
212 | #### N dimensions
213 | The ellipsis (`...`) can be used to denote a variable number of dimensions. For example:
214 | ```python
215 | >>> isinstance(random.randn(2), NDArray[Shape["2, ..."], Any])
216 | True
217 | >>> isinstance(random.randn(2, 2, 2), NDArray[Shape["2, ..."], Any])
218 | True
219 | >>> isinstance(random.randn(2, 2, 3), NDArray[Shape["2, ..."], Any])
220 | False
221 |
222 | ```
223 | Combined with the wildcard, you could express the "any shape":
224 |
225 | ```python
226 | >>> isinstance(random.randn(2), NDArray[Shape["*, ..."], Any])
227 | True
228 | >>> isinstance(random.randn(2, 42, 100), NDArray[Shape["*, ..."], Any])
229 | True
230 |
231 | ```
232 | The shape in the above example can be replaced with `typing.Any` to have the same effect.
233 |
234 | You can also express "at least N dimensions":
235 | ```python
236 | >>> isinstance(random.randn(2, 2), NDArray[Shape["2, 2, ..."], Any])
237 | True
238 | >>> isinstance(random.randn(2, 2, 2, 2), NDArray[Shape["2, 2, ..."], Any])
239 | True
240 | >>> isinstance(random.randn(2), NDArray[Shape["2, 2, ..."], Any])
241 | False
242 |
243 | ```
244 |
245 | #### Dimension breakdowns
246 | A dimension can be broken down into more detail. We call this a **dimension breakdown**. This can be useful to clearly
247 | describe what a dimension means. Example:
248 |
249 | ```python
250 | >>> isinstance(random.randn(100, 2), NDArray[Shape["*, [x, y]"], Any])
251 | True
252 |
253 | ```
254 | The shape expression in the example above is synonymous to `Shape["*, 2"]`.
255 |
256 | Dimension breakdowns must consist of one or more labels, separated by commas. In contrast to variables, labels must
257 | start with a lowercase letter and may contain underscores and digits.
258 |
259 | #### Labels
260 | Labels can be used as extra clarification in a shape expression. They can be used in dimension breakdowns and right
261 | after dimensions. Example:
262 | ```python
263 | >>> isinstance(random.randn(5, 2), NDArray[Shape["5 coordinates, [x, y]"], Any])
264 | True
265 | >>> isinstance(random.randn(5, 2), NDArray[Shape["5 coordinates, [x, y] wgs84"], Any])
266 | True
267 |
268 | ```
269 |
270 | ### DTypes
271 | The second argument of `NDArray` can be `typing.Any` or any of the following dtypes:
272 | ```python
273 | >>> from nptyping.typing_ import dtypes
274 | >>> for _, dtype_name in dtypes:
275 | ... print(dtype_name)
276 | Number
277 | Bool
278 | Bool8
279 | Obj
280 | Object
281 | Object0
282 | Datetime64
283 | Integer
284 | SignedInteger
285 | Int8
286 | Int16
287 | Int32
288 | Int64
289 | Byte
290 | Short
291 | IntC
292 | IntP
293 | Int0
294 | Int
295 | LongLong
296 | Timedelta64
297 | UnsignedInteger
298 | UInt8
299 | UInt16
300 | UInt32
301 | UInt64
302 | UByte
303 | UShort
304 | UIntC
305 | UIntP
306 | UInt0
307 | UInt
308 | ULongLong
309 | Inexact
310 | Floating
311 | Float16
312 | Float32
313 | Float64
314 | Half
315 | Single
316 | Double
317 | Float
318 | LongDouble
319 | LongFloat
320 | ComplexFloating
321 | Complex64
322 | Complex128
323 | CSingle
324 | SingleComplex
325 | CDouble
326 | Complex
327 | CFloat
328 | CLongDouble
329 | CLongFloat
330 | LongComplex
331 | Flexible
332 | Void
333 | Void0
334 | Character
335 | Bytes
336 | String
337 | Str
338 | Bytes0
339 | Unicode
340 | Str0
341 |
342 | ```
343 | These are special aliases for `numpy` dtypes.
344 | ```python
345 | >>> from nptyping import Int
346 | >>> Int
347 |
348 |
349 | ```
350 | You may also provide `numpy` dtypes directly to an `NDArray`. This is not recommended though, because
351 | MyPy won't accept it.
352 | ```python
353 | >>> import numpy as np
354 |
355 | >>> NDArray[Any, np.floating]
356 | NDArray[Any, Floating]
357 |
358 | ```
359 |
360 | ### Structure expressions
361 | You can denote the structure of a structured array using what we call a **structure expression**. This expression
362 | (again a string) can be put into `Structure` and can then be used in an `NDArray`.
363 | ```python
364 | >>> from nptyping import Structure
365 |
366 | ```
367 | An example of a structure expression in an `NDArray`:
368 | ```python
369 | >>> from typing import Any
370 |
371 | >>> NDArray[Any, Structure["name: Str, age: Int"]]
372 | NDArray[Any, Structure['age: Int, name: Str']]
373 |
374 | ```
375 | The above example shows an expression for a structured array with 2 fields.
376 |
377 | Like with `Shape`, you can use `typing.Literal` in an `NDArray`:
378 | ```python
379 | >>> from typing import Literal
380 |
381 | >>> Structure["x: Float, y: Float"] == Literal["x: Float, y: Float"]
382 | True
383 |
384 | ```
385 | This also means that you can use `typing.Literal` instead of `Structure` if you want.
386 |
387 | #### Syntax structure expressions
388 |
389 | A structure expression is a comma separated list of fields, with each field consisting of a name and a type.
390 | ```python
391 | >>> Structure["a_name: AType, some_other_name: SomeOtherType"]
392 | Structure['a_name: AType, some_other_name: SomeOtherType']
393 |
394 | ```
395 |
396 | You can combine fields if you want to express multiple names with the same type. Here is an example of how that may
397 | look:
398 | ```python
399 | >>> from nptyping import Structure
400 |
401 | >>> Structure["[a, b, c]: Int, [d, e, f]: Float"]
402 | Structure['[d, e, f]: Float, [a, b, c]: Int']
403 |
404 | ```
405 |
406 | It can make your expression more concise, but it's just an alternative way of expressing the same thing:
407 | ```python
408 | >>> from nptyping import Structure
409 |
410 | >>> Structure["a: Int, b: Int, c: Int, d: Float, e: Float, f: Float"] \
411 | ... is \
412 | ... Structure["[a, b, c]: Int, [d, e, f]: Float"]
413 | True
414 |
415 | ```
416 |
417 | The syntax of a structure expression can be formalized in BNF. Extra whitespacing is allowed (e.g. around commas and
418 | colons), but this is not included in the schema below.
419 | ```
420 | structure-expression = |","
421 | fields = |","
422 | field = ":"|"[""]:"
423 | combined-field-names = ","|","
424 | field-type = ||
425 | wildcard = "*"
426 | field-subarray-shape = "[""]"
427 | field-name =
428 | word = ||
429 | letter = |
430 | uletter = "A"|"B"|"C"|"D"|"E"|"F"|"G"|"H"|"I"|"J"|"K"|"L"|"M"|"N"|"O"|"P"|"Q"|"R"|"S"|"T"|"U"|"V"|"W"|"X"|"Y"|"Z"
431 | lletter = "a"|"b"|"c"|"d"|"e"|"f"|"g"|"h"|"i"|"j"|"k"|"l"|"m"|"n"|"o"|"p"|"q"|"r"|"s"|"t"|"u"|"v"|"w"|"x"|"y"|"z"
432 | number = |
433 | digit = "0"|"1"|"2"|"3"|"4"|"5"|"6"|"7"|"8"|"9"
434 | underscore = "_"
435 | ```
436 |
437 | #### Subarrays
438 | You can express the shape of a subarray using brackets after a type. You can use the full power of shape expressions.
439 |
440 | ```python
441 | >>> from typing import Any
442 | >>> import numpy as np
443 | >>> from nptyping import NDArray, Structure
444 |
445 | >>> arr = np.array([("x")], np.dtype([("x", "U10", (2, 2))]))
446 | >>> isinstance(arr, NDArray[Any, Structure["x: Str[2, 2]"]])
447 | True
448 |
449 | ```
450 |
451 | #### Structure Wildcards
452 | You can use wildcards for field types or globally (for complete fields).
453 | Here is an example of a wildcard for a field type:
454 | ```python
455 | >>> Structure["anyType: *"]
456 | Structure['anyType: *']
457 |
458 | ```
459 |
460 | And here is an example with a global wildcard:
461 | ```python
462 | >>> Structure["someType: int, *"]
463 | Structure['someType: int, *']
464 |
465 | ```
466 | This expresses a structure that has *at least* a field `someType: int`. Any other fields are also accepted.
467 |
468 | ### RecArray
469 | The `RecArray` corresponds to [numpy.recarray](https://numpy.org/doc/stable/reference/generated/numpy.recarray.html).
470 | It is an extension of `NDArray` and behaves similarly. A key difference is that with `RecArray`, the `Structure` OR
471 | `typing.Any` are mandatory.
472 |
473 | ```python
474 | >>> from nptyping import RecArray
475 |
476 | >>> RecArray[Any, Structure["x: Float, y: Float"]]
477 | RecArray[Any, Structure['[x, y]: Float']]
478 |
479 | ```
480 |
481 | ### Pandas DataFrame
482 | The `nptyping.DataFrame` can be used for expressing structures of `pandas.DataFrame`. It takes a `Structure` and uses
483 | the same Structure Expression syntax.
484 |
485 | ```python
486 | >>> from nptyping import DataFrame, Structure as S
487 |
488 | >>> DataFrame[S["name: Str, x: Float, y: Float"]]
489 | DataFrame[Structure['[x, y]: Float, name: Str']]
490 |
491 | ```
492 |
493 | Check out the documentation on [Structure Expressions](#Structure-expressions) for more details.
494 |
495 | ### Examples
496 |
497 | Here is just a list of examples of how one can express arrays with `NDArray`.
498 |
499 | An Array with any dimensions of any size and any type:
500 | ```python
501 | >>> from nptyping import NDArray, Shape
502 | >>> from typing import Any
503 |
504 |
505 | >>> NDArray[Any, Any]
506 | NDArray[Any, Any]
507 |
508 | >>> NDArray[Shape["*, ..."], Any]
509 | NDArray[Any, Any]
510 |
511 | >>> NDArray # MyPy doesn't like this one though.
512 | NDArray[Any, Any]
513 |
514 | ```
515 |
516 | An array with 1 dimension of any size and any type:
517 | ```python
518 | >>> NDArray[Shape["*"], Any]
519 | NDArray[Shape['*'], Any]
520 |
521 | >>> NDArray[Shape["Var"], Any]
522 | NDArray[Shape['Var'], Any]
523 |
524 | ```
525 |
526 | An array with 1 dimension of size 3 and any type:
527 | ```python
528 | >>> NDArray[Shape["3"], Any]
529 | NDArray[Shape['3'], Any]
530 |
531 | >>> NDArray[Shape["[entry1, entry2, entry3]"], Any]
532 | NDArray[Shape['[entry1, entry2, entry3]'], Any]
533 |
534 | ```
535 |
536 | An array with 3 dimensions of size 3, 3 and any and any type:
537 | ```python
538 | >>> NDArray[Shape["3, 3, *"], Any]
539 | NDArray[Shape['3, 3, *'], Any]
540 |
541 | >>> NDArray[Shape["3, 3, Var"], Any]
542 | NDArray[Shape['3, 3, Var'], Any]
543 |
544 | >>> NDArray[Shape["3, [entry1, entry2, entry3], Var"], Any]
545 | NDArray[Shape['3, [entry1, entry2, entry3], Var'], Any]
546 |
547 | ```
548 |
549 | A square array with 2 dimensions that are of the same size:
550 | ```python
551 | >>> NDArray[Shape["Dim, Dim"], Any]
552 | NDArray[Shape['Dim, Dim'], Any]
553 |
554 | ```
555 |
556 | An array with multiple dimensions of the same size:
557 | ```python
558 | >>> NDArray[Shape["Dim, ..."], Any]
559 | NDArray[Shape['Dim, ...'], Any]
560 |
561 | ```
562 |
563 | An array with 2 dimensions of any size with type unsigned int.
564 | ```python
565 | >>> from nptyping import UInt
566 | >>> NDArray[Shape["*, *"], UInt]
567 | NDArray[Shape['*, *'], UInt]
568 |
569 | ```
570 |
571 | An array with 2 dimensions of size 3 and 3 with a structured type.
572 | ```python
573 | >>> NDArray[Shape["3, 3"], Structure["x: Float, y: Float"]]
574 | NDArray[Shape['3, 3'], Structure['[x, y]: Float']]
575 |
576 | ```
577 |
578 | Here are some examples of rich expressions that `nptyping` facilitates:
579 | ```python
580 | >>> from nptyping import NDArray, Shape, Float
581 |
582 | >>> def plan_route(
583 | ... locations: NDArray[Shape["[from, to], [x, y]"], Float]
584 | ... ) -> NDArray[Shape["* stops, [x, y]"], Float]:
585 | ... ...
586 |
587 | >>> AssetArray = NDArray[Shape["* assets, [id, type, age, state, x, y]"], Float]
588 |
589 | >>> def get_assets_within_range(
590 | ... x: float, y: float, range_km: float, assets: AssetArray
591 | ... ) -> AssetArray:
592 | ... ...
593 |
594 | ```
595 |
596 | Here is an example of how to get type safety to the max, by stacking `nptyping` up with
597 | [beartype](https://github.com/beartype/beartype):
598 | ```python
599 | >>> from beartype import beartype
600 |
601 | >>> @beartype
602 | ... def type_safety(assets: AssetArray) -> None:
603 | ... # assets is now guaranteed by beartype to be an AssetArray.
604 | ... ...
605 |
606 | ```
607 |
608 | ## Similar projects
609 |
610 | * [numpy.typing](https://numpy.org/devdocs/reference/typing.html)
611 | *First and foremost, `numpy`'s own typing. The pyi files are more complete and up to date than `nptyping`'s, so if code
612 | completion in an IDE is most important to you, this might be your go to. On the other hand, at the moment of writing, it
613 | does not offer instance checking with shapes as `nptptying` does.*
614 | * [dataenforce](https://github.com/CedricFR/dataenforce)
615 | *Although not for `numpy`, this library offers type hinting for `pandas.DataFrame`. Currently, there seems to be no
616 | `MyPy` integration, but apart from that it seems easy to use.*
617 | * [typing.annotated](https://peps.python.org/pep-0593/)
618 | *You could also create your own type hints using Python's builtin `typing` module. The `typing.Annotated` will take you
619 | quite far. `MyPy` will support it (to some extent), but you won't have any instance or shape checking.*
620 |
621 | ## FAQ
622 |
623 | * PyCharm complains about `Shape[]`, what should I do?
624 | *Unfortunately, some IDEs try to parse what's between quotes in a type hint sometimes. You are left with 3 options:*
625 | 1. *Use `typing.Literal` instead of `Shape`, `nptyping` can handle this perfectly fine*
626 | 2. *Use an extra pair of quotes: `Shape['""']`*, this appeases PyCharm and is accepted by `nptyping`
627 | 3. *Do nothing, accept the IDE complaints, wait and hope for the IDE to mature*
628 | * Can `MyPy` do the instance checking?
629 | *Because of the dynamic nature of `numpy` and `pandas`, this is currently not possible. The checking done by MyPy is*
630 | *limited to detecting whether or not a `numpy` or `pandas` type is provided when that is hinted. There are no static*
631 | *checks on shapes, structures or types.*
632 | * Will there ever be support for Tensorflow Tensors? Or for... ?
633 | *Maybe. Possibly. If there is enough demand for it and if I find the spare time.*
634 |
635 | ## About
636 |
637 | This project started in 2019 from a personal need to keep a `numpy` project maintainable. I prototyped a very small
638 | solution to the (then) missing type hint options for `numpy`. Then I put it online for others to use. I learned a lot
639 | since then and I feel that I owe a lot to everyone that has contributed to this project in any way.
640 |
641 | I wish to thank all contributors. It amazes me everytime when someone proposes an improvement in a near-perfect pull
642 | request. Also, the ideas and thoughts that some people put into the discussions are very valuable to this project, I
643 | consider these people contributors as well.
644 |
645 | Also thanks to all users. The best motivation for an open source fanatic like myself, is to see the software being used
646 | and to hear people being happy with it. This is what drives me to continue.
647 |
648 | Happy coding!
649 |
650 | ~ Ramon Hagenaars
651 |
--------------------------------------------------------------------------------
/constraints-3.10.txt:
--------------------------------------------------------------------------------
1 | #
2 | # This file is autogenerated by pip-compile with python 3.10
3 | # To update, run:
4 | #
5 | # pip-compile --output-file=constraints-3.10.0.txt './dependencies\build-requirements.txt' './dependencies\pandas-requirements.txt' './dependencies\qa-requirements.txt' './dependencies\requirements.txt'
6 | #
7 | astroid==2.12.9
8 | # via pylint
9 | autoflake==1.5.3
10 | # via -r ./dependencies\qa-requirements.txt
11 | beartype==0.10.4 ; python_version >= "3.10"
12 | # via -r ./dependencies\qa-requirements.txt
13 | black==22.8.0
14 | # via -r ./dependencies\qa-requirements.txt
15 | build==0.8.0
16 | # via pip-tools
17 | certifi==2022.6.15.1
18 | # via requests
19 | charset-normalizer==2.1.1
20 | # via requests
21 | click==8.1.3
22 | # via
23 | # black
24 | # pip-tools
25 | codecov==2.1.12
26 | # via -r ./dependencies\qa-requirements.txt
27 | colorama==0.4.5
28 | # via
29 | # build
30 | # click
31 | # pylint
32 | coverage==6.4.4
33 | # via
34 | # -r ./dependencies\qa-requirements.txt
35 | # codecov
36 | dill==0.3.5.1
37 | # via pylint
38 | feedparser==6.0.10
39 | # via -r ./dependencies\qa-requirements.txt
40 | idna==3.3
41 | # via requests
42 | invoke==1.7.1
43 | # via -r ./dependencies\build-requirements.txt
44 | isort==5.10.1
45 | # via
46 | # -r ./dependencies\qa-requirements.txt
47 | # pylint
48 | lazy-object-proxy==1.7.1
49 | # via astroid
50 | mccabe==0.7.0
51 | # via pylint
52 | mypy==1.0.0
53 | # via -r ./dependencies\qa-requirements.txt
54 | mypy-extensions==1.0.0
55 | # via
56 | # black
57 | # mypy
58 | nodeenv==1.7.0
59 | # via pyright
60 | numpy==1.23.3 ; python_version >= "3.8"
61 | # via
62 | # -r ./dependencies\requirements.txt
63 | # pandas
64 | packaging==21.3
65 | # via build
66 | pandas==1.4.4
67 | # via -r ./dependencies\pandas-requirements.txt
68 | pathspec==0.10.1
69 | # via black
70 | pep517==0.13.0
71 | # via build
72 | pip-tools==6.8.0
73 | # via -r ./dependencies\build-requirements.txt
74 | platformdirs==2.5.2
75 | # via
76 | # black
77 | # pylint
78 | pyflakes==2.5.0
79 | # via autoflake
80 | pylint==2.15.2
81 | # via -r ./dependencies\qa-requirements.txt
82 | pyparsing==3.0.9
83 | # via packaging
84 | pyright==1.1.294
85 | # via -r ./dependencies\qa-requirements.txt
86 | python-dateutil==2.8.2
87 | # via pandas
88 | pytz==2022.2.1
89 | # via pandas
90 | requests==2.28.1
91 | # via codecov
92 | sgmllib3k==1.0.0
93 | # via feedparser
94 | six==1.16.0
95 | # via python-dateutil
96 | toml==0.10.2
97 | # via autoflake
98 | tomli==2.0.1
99 | # via
100 | # black
101 | # build
102 | # mypy
103 | # pep517
104 | # pylint
105 | tomlkit==0.11.4
106 | # via pylint
107 | typeguard==2.13.3
108 | # via -r ./dependencies\qa-requirements.txt
109 | typing-extensions==4.3.0
110 | # via mypy
111 | urllib3==1.26.12
112 | # via requests
113 | wheel==0.37.1
114 | # via
115 | # -r ./dependencies\qa-requirements.txt
116 | # pip-tools
117 | wrapt==1.14.1
118 | # via astroid
119 |
120 | # The following packages are considered to be unsafe in a requirements file:
121 | # pip
122 | # setuptools
123 |
--------------------------------------------------------------------------------
/constraints-3.11.txt:
--------------------------------------------------------------------------------
1 | #
2 | # This file is autogenerated by pip-compile with python 3.11
3 | # To update, run:
4 | #
5 | # pip-compile --output-file=constraints-3.11.txt './dependencies\build-requirements.txt' './dependencies\pandas-requirements.txt' './dependencies\qa-requirements.txt' './dependencies\requirements.txt'
6 | #
7 | astroid==2.12.12
8 | # via pylint
9 | autoflake==1.7.7
10 | # via -r ./dependencies\qa-requirements.txt
11 | beartype==0.11.0 ; python_version >= "3.10"
12 | # via -r ./dependencies\qa-requirements.txt
13 | black==22.10.0
14 | # via -r ./dependencies\qa-requirements.txt
15 | build==0.9.0
16 | # via pip-tools
17 | certifi==2022.9.24
18 | # via requests
19 | charset-normalizer==2.1.1
20 | # via requests
21 | click==8.1.3
22 | # via
23 | # black
24 | # pip-tools
25 | codecov==2.1.12
26 | # via -r ./dependencies\qa-requirements.txt
27 | colorama==0.4.6
28 | # via
29 | # build
30 | # click
31 | # pylint
32 | coverage==6.5.0
33 | # via
34 | # -r ./dependencies\qa-requirements.txt
35 | # codecov
36 | dill==0.3.6
37 | # via pylint
38 | feedparser==6.0.10
39 | # via -r ./dependencies\qa-requirements.txt
40 | idna==3.4
41 | # via requests
42 | invoke==1.7.3
43 | # via -r ./dependencies\build-requirements.txt
44 | isort==5.10.1
45 | # via
46 | # -r ./dependencies\qa-requirements.txt
47 | # pylint
48 | lazy-object-proxy==1.8.0
49 | # via astroid
50 | mccabe==0.7.0
51 | # via pylint
52 | mypy==1.0.0
53 | # via -r ./dependencies\qa-requirements.txt
54 | mypy-extensions==1.0.0
55 | # via
56 | # black
57 | # mypy
58 | nodeenv==1.7.0
59 | # via pyright
60 | numpy==1.23.4 ; python_version >= "3.8"
61 | # via
62 | # -r ./dependencies\requirements.txt
63 | # pandas
64 | packaging==21.3
65 | # via build
66 | pandas==1.5.1
67 | # via -r ./dependencies\pandas-requirements.txt
68 | pathspec==0.10.1
69 | # via black
70 | pep517==0.13.0
71 | # via build
72 | pip-tools==6.9.0
73 | # via -r ./dependencies\build-requirements.txt
74 | platformdirs==2.5.3
75 | # via
76 | # black
77 | # pylint
78 | pyflakes==2.5.0
79 | # via autoflake
80 | pylint==2.15.5
81 | # via -r ./dependencies\qa-requirements.txt
82 | pyparsing==3.0.9
83 | # via packaging
84 | pyright==1.1.294
85 | # via -r ./dependencies\qa-requirements.txt
86 | python-dateutil==2.8.2
87 | # via pandas
88 | pytz==2022.6
89 | # via pandas
90 | requests==2.28.1
91 | # via codecov
92 | sgmllib3k==1.0.0
93 | # via feedparser
94 | six==1.16.0
95 | # via python-dateutil
96 | tomlkit==0.11.6
97 | # via pylint
98 | typeguard==2.13.3
99 | # via -r ./dependencies\qa-requirements.txt
100 | typing-extensions==4.4.0
101 | # via mypy
102 | urllib3==1.26.12
103 | # via requests
104 | wheel==0.38.3
105 | # via
106 | # -r ./dependencies\qa-requirements.txt
107 | # pip-tools
108 | wrapt==1.14.1
109 | # via astroid
110 |
111 | # The following packages are considered to be unsafe in a requirements file:
112 | # pip
113 | # setuptools
114 |
--------------------------------------------------------------------------------
/constraints-3.7.txt:
--------------------------------------------------------------------------------
1 | #
2 | # This file is autogenerated by pip-compile with python 3.7
3 | # To update, run:
4 | #
5 | # pip-compile --output-file=constraints-3.7.3.txt './dependencies\build-requirements.txt' './dependencies\pandas-requirements.txt' './dependencies\qa-requirements.txt' './dependencies\requirements.txt'
6 | #
7 | astroid==2.12.9
8 | # via pylint
9 | autoflake==1.5.3
10 | # via -r ./dependencies\qa-requirements.txt
11 | beartype==0.9.1 ; python_version < "3.10"
12 | # via -r ./dependencies\qa-requirements.txt
13 | black==22.8.0
14 | # via -r ./dependencies\qa-requirements.txt
15 | build==0.8.0
16 | # via pip-tools
17 | certifi==2022.6.15.1
18 | # via requests
19 | charset-normalizer==2.1.1
20 | # via requests
21 | click==8.1.3
22 | # via
23 | # black
24 | # pip-tools
25 | codecov==2.1.12
26 | # via -r ./dependencies\qa-requirements.txt
27 | colorama==0.4.5
28 | # via
29 | # build
30 | # click
31 | # pylint
32 | coverage==6.4.4
33 | # via
34 | # -r ./dependencies\qa-requirements.txt
35 | # codecov
36 | dill==0.3.5.1
37 | # via pylint
38 | feedparser==6.0.10
39 | # via -r ./dependencies\qa-requirements.txt
40 | idna==3.3
41 | # via requests
42 | importlib-metadata==4.12.0
43 | # via
44 | # build
45 | # click
46 | # pep517
47 | invoke==1.7.1
48 | # via -r ./dependencies\build-requirements.txt
49 | isort==5.10.1
50 | # via
51 | # -r ./dependencies\qa-requirements.txt
52 | # pylint
53 | lazy-object-proxy==1.7.1
54 | # via astroid
55 | mccabe==0.7.0
56 | # via pylint
57 | mypy==0.991
58 | # via -r ./dependencies\qa-requirements.txt
59 | mypy-extensions==1.0.0
60 | # via
61 | # black
62 | # mypy
63 | nodeenv==1.7.0
64 | # via pyright
65 | numpy==1.21.5 ; python_version < "3.8"
66 | # via
67 | # -r ./dependencies\requirements.txt
68 | # pandas
69 | packaging==21.3
70 | # via build
71 | pandas==1.3.5
72 | # via -r ./dependencies\pandas-requirements.txt
73 | pathspec==0.10.1
74 | # via black
75 | pep517==0.13.0
76 | # via build
77 | pip-tools==6.8.0
78 | # via -r ./dependencies\build-requirements.txt
79 | platformdirs==2.5.2
80 | # via
81 | # black
82 | # pylint
83 | pyflakes==2.5.0
84 | # via autoflake
85 | pylint==2.15.2
86 | # via -r ./dependencies\qa-requirements.txt
87 | pyparsing==3.0.9
88 | # via packaging
89 | pyright==1.1.294
90 | # via -r ./dependencies\qa-requirements.txt
91 | python-dateutil==2.8.2
92 | # via pandas
93 | pytz==2022.2.1
94 | # via pandas
95 | requests==2.28.1
96 | # via codecov
97 | sgmllib3k==1.0.0
98 | # via feedparser
99 | six==1.16.0
100 | # via python-dateutil
101 | toml==0.10.2
102 | # via autoflake
103 | tomli==2.0.1
104 | # via
105 | # black
106 | # build
107 | # mypy
108 | # pep517
109 | # pylint
110 | tomlkit==0.11.4
111 | # via pylint
112 | typed-ast==1.5.4
113 | # via
114 | # astroid
115 | # black
116 | # mypy
117 | typeguard==2.13.3
118 | # via -r ./dependencies\qa-requirements.txt
119 | typing-extensions==4.3.0 ; python_version < "3.10"
120 | # via
121 | # -r ./dependencies\requirements.txt
122 | # astroid
123 | # black
124 | # importlib-metadata
125 | # mypy
126 | # pylint
127 | # pyright
128 | urllib3==1.26.12
129 | # via requests
130 | wheel==0.37.1
131 | # via
132 | # -r ./dependencies\qa-requirements.txt
133 | # pip-tools
134 | wrapt==1.14.1
135 | # via astroid
136 | zipp==3.8.1
137 | # via
138 | # importlib-metadata
139 | # pep517
140 |
141 | # The following packages are considered to be unsafe in a requirements file:
142 | # pip
143 | # setuptools
144 |
--------------------------------------------------------------------------------
/constraints-3.8.txt:
--------------------------------------------------------------------------------
1 | #
2 | # This file is autogenerated by pip-compile with python 3.8
3 | # To update, run:
4 | #
5 | # pip-compile --output-file=constraints-3.8.3.txt './dependencies\build-requirements.txt' './dependencies\pandas-requirements.txt' './dependencies\qa-requirements.txt' './dependencies\requirements.txt'
6 | #
7 | astroid==2.12.9
8 | # via pylint
9 | autoflake==1.5.3
10 | # via -r ./dependencies\qa-requirements.txt
11 | beartype==0.9.1 ; python_version < "3.10"
12 | # via -r ./dependencies\qa-requirements.txt
13 | black==22.8.0
14 | # via -r ./dependencies\qa-requirements.txt
15 | build==0.8.0
16 | # via pip-tools
17 | certifi==2022.6.15.1
18 | # via requests
19 | charset-normalizer==2.1.1
20 | # via requests
21 | click==8.1.3
22 | # via
23 | # black
24 | # pip-tools
25 | codecov==2.1.12
26 | # via -r ./dependencies\qa-requirements.txt
27 | colorama==0.4.5
28 | # via
29 | # build
30 | # click
31 | # pylint
32 | coverage==6.4.4
33 | # via
34 | # -r ./dependencies\qa-requirements.txt
35 | # codecov
36 | dill==0.3.5.1
37 | # via pylint
38 | feedparser==6.0.10
39 | # via -r ./dependencies\qa-requirements.txt
40 | idna==3.3
41 | # via requests
42 | invoke==1.7.1
43 | # via -r ./dependencies\build-requirements.txt
44 | isort==5.10.1
45 | # via
46 | # -r ./dependencies\qa-requirements.txt
47 | # pylint
48 | lazy-object-proxy==1.7.1
49 | # via astroid
50 | mccabe==0.7.0
51 | # via pylint
52 | mypy==1.0.0
53 | # via -r ./dependencies\qa-requirements.txt
54 | mypy-extensions==1.0.0
55 | # via
56 | # black
57 | # mypy
58 | nodeenv==1.7.0
59 | # via pyright
60 | numpy==1.23.3 ; python_version >= "3.8"
61 | # via
62 | # -r ./dependencies\requirements.txt
63 | # pandas
64 | packaging==21.3
65 | # via build
66 | pandas==1.4.4
67 | # via -r ./dependencies\pandas-requirements.txt
68 | pathspec==0.10.1
69 | # via black
70 | pep517==0.13.0
71 | # via build
72 | pip-tools==6.8.0
73 | # via -r ./dependencies\build-requirements.txt
74 | platformdirs==2.5.2
75 | # via
76 | # black
77 | # pylint
78 | pyflakes==2.5.0
79 | # via autoflake
80 | pylint==2.15.2
81 | # via -r ./dependencies\qa-requirements.txt
82 | pyparsing==3.0.9
83 | # via packaging
84 | pyright==1.1.294
85 | # via -r ./dependencies\qa-requirements.txt
86 | python-dateutil==2.8.2
87 | # via pandas
88 | pytz==2022.2.1
89 | # via pandas
90 | requests==2.28.1
91 | # via codecov
92 | sgmllib3k==1.0.0
93 | # via feedparser
94 | six==1.16.0
95 | # via python-dateutil
96 | toml==0.10.2
97 | # via autoflake
98 | tomli==2.0.1
99 | # via
100 | # black
101 | # build
102 | # mypy
103 | # pep517
104 | # pylint
105 | tomlkit==0.11.4
106 | # via pylint
107 | typeguard==2.13.3
108 | # via -r ./dependencies\qa-requirements.txt
109 | typing-extensions==4.3.0 ; python_version < "3.10"
110 | # via
111 | # -r ./dependencies\requirements.txt
112 | # astroid
113 | # black
114 | # mypy
115 | # pylint
116 | urllib3==1.26.12
117 | # via requests
118 | wheel==0.37.1
119 | # via
120 | # -r ./dependencies\qa-requirements.txt
121 | # pip-tools
122 | wrapt==1.14.1
123 | # via astroid
124 |
125 | # The following packages are considered to be unsafe in a requirements file:
126 | # pip
127 | # setuptools
128 |
--------------------------------------------------------------------------------
/constraints-3.9.txt:
--------------------------------------------------------------------------------
1 | #
2 | # This file is autogenerated by pip-compile with python 3.9
3 | # To update, run:
4 | #
5 | # pip-compile --output-file=constraints-3.9.0.txt './dependencies\build-requirements.txt' './dependencies\pandas-requirements.txt' './dependencies\qa-requirements.txt' './dependencies\requirements.txt'
6 | #
7 | astroid==2.12.9
8 | # via pylint
9 | autoflake==1.5.3
10 | # via -r ./dependencies\qa-requirements.txt
11 | beartype==0.9.1 ; python_version < "3.10"
12 | # via -r ./dependencies\qa-requirements.txt
13 | black==22.8.0
14 | # via -r ./dependencies\qa-requirements.txt
15 | build==0.8.0
16 | # via pip-tools
17 | certifi==2022.6.15.1
18 | # via requests
19 | charset-normalizer==2.1.1
20 | # via requests
21 | click==8.1.3
22 | # via
23 | # black
24 | # pip-tools
25 | codecov==2.1.12
26 | # via -r ./dependencies\qa-requirements.txt
27 | colorama==0.4.5
28 | # via
29 | # build
30 | # click
31 | # pylint
32 | coverage==6.4.4
33 | # via
34 | # -r ./dependencies\qa-requirements.txt
35 | # codecov
36 | dill==0.3.5.1
37 | # via pylint
38 | feedparser==6.0.10
39 | # via -r ./dependencies\qa-requirements.txt
40 | idna==3.3
41 | # via requests
42 | invoke==1.7.1
43 | # via -r ./dependencies\build-requirements.txt
44 | isort==5.10.1
45 | # via
46 | # -r ./dependencies\qa-requirements.txt
47 | # pylint
48 | lazy-object-proxy==1.7.1
49 | # via astroid
50 | mccabe==0.7.0
51 | # via pylint
52 | mypy==1.0.0
53 | # via -r ./dependencies\qa-requirements.txt
54 | mypy-extensions==1.0.0
55 | # via
56 | # black
57 | # mypy
58 | nodeenv==1.7.0
59 | # via pyright
60 | numpy==1.23.3 ; python_version >= "3.8"
61 | # via
62 | # -r ./dependencies\requirements.txt
63 | # pandas
64 | packaging==21.3
65 | # via build
66 | pandas==1.4.4
67 | # via -r ./dependencies\pandas-requirements.txt
68 | pathspec==0.10.1
69 | # via black
70 | pep517==0.13.0
71 | # via build
72 | pip-tools==6.8.0
73 | # via -r ./dependencies\build-requirements.txt
74 | platformdirs==2.5.2
75 | # via
76 | # black
77 | # pylint
78 | pyflakes==2.5.0
79 | # via autoflake
80 | pylint==2.15.2
81 | # via -r ./dependencies\qa-requirements.txt
82 | pyparsing==3.0.9
83 | # via packaging
84 | pyright==1.1.294
85 | # via -r ./dependencies\qa-requirements.txt
86 | python-dateutil==2.8.2
87 | # via pandas
88 | pytz==2022.2.1
89 | # via pandas
90 | requests==2.28.1
91 | # via codecov
92 | sgmllib3k==1.0.0
93 | # via feedparser
94 | six==1.16.0
95 | # via python-dateutil
96 | toml==0.10.2
97 | # via autoflake
98 | tomli==2.0.1
99 | # via
100 | # black
101 | # build
102 | # mypy
103 | # pep517
104 | # pylint
105 | tomlkit==0.11.4
106 | # via pylint
107 | typeguard==2.13.3
108 | # via -r ./dependencies\qa-requirements.txt
109 | typing-extensions==4.3.0 ; python_version < "3.10"
110 | # via
111 | # -r ./dependencies\requirements.txt
112 | # astroid
113 | # black
114 | # mypy
115 | # pylint
116 | urllib3==1.26.12
117 | # via requests
118 | wheel==0.37.1
119 | # via
120 | # -r ./dependencies\qa-requirements.txt
121 | # pip-tools
122 | wrapt==1.14.1
123 | # via astroid
124 |
125 | # The following packages are considered to be unsafe in a requirements file:
126 | # pip
127 | # setuptools
128 |
--------------------------------------------------------------------------------
/dependencies/build-requirements.txt:
--------------------------------------------------------------------------------
1 | invoke>=1.6.0
2 | pip-tools>=6.5.0
3 |
--------------------------------------------------------------------------------
/dependencies/pandas-requirements.txt:
--------------------------------------------------------------------------------
1 | pandas
2 | pandas-stubs-fork ; python_version>="3.8"
3 |
--------------------------------------------------------------------------------
/dependencies/qa-requirements.txt:
--------------------------------------------------------------------------------
1 | autoflake
2 | beartype<0.10.0; python_version<'3.10'
3 | beartype>=0.10.0; python_version>='3.10'
4 | black
5 | coverage
6 | codecov>=2.1.0
7 | feedparser
8 | isort
9 | mypy
10 | pylint
11 | pyright
12 | setuptools
13 | typeguard
14 | wheel
15 |
--------------------------------------------------------------------------------
/dependencies/requirements.txt:
--------------------------------------------------------------------------------
1 | numpy==1.21.5; python_version<'3.8'
2 | numpy>=1.20.0,<2.0.0; python_version>='3.8'
3 | typing_extensions>=4.0.0,<5.0.0; python_version<'3.10'
4 |
--------------------------------------------------------------------------------
/nptyping/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | MIT License
3 |
4 | Copyright (c) 2023 Ramon Hagenaars
5 |
6 | Permission is hereby granted, free of charge, to any person obtaining a copy
7 | of this software and associated documentation files (the "Software"), to deal
8 | in the Software without restriction, including without limitation the rights
9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | copies of the Software, and to permit persons to whom the Software is
11 | furnished to do so, subject to the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be included in all
14 | copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | SOFTWARE.
23 | """
24 | from nptyping.assert_isinstance import assert_isinstance
25 | from nptyping.error import (
26 | InvalidArgumentsError,
27 | InvalidDTypeError,
28 | InvalidShapeError,
29 | InvalidStructureError,
30 | NPTypingError,
31 | )
32 | from nptyping.ndarray import NDArray
33 | from nptyping.package_info import __version__
34 | from nptyping.pandas_.dataframe import DataFrame
35 | from nptyping.recarray import RecArray
36 | from nptyping.shape import Shape
37 | from nptyping.shape_expression import (
38 | normalize_shape_expression,
39 | validate_shape_expression,
40 | )
41 | from nptyping.structure import Structure
42 | from nptyping.typing_ import (
43 | Bool,
44 | Bool8,
45 | Byte,
46 | Bytes,
47 | Bytes0,
48 | CDouble,
49 | CFloat,
50 | Character,
51 | CLongDouble,
52 | CLongFloat,
53 | Complex,
54 | Complex64,
55 | Complex128,
56 | ComplexFloating,
57 | CSingle,
58 | Datetime64,
59 | Double,
60 | DType,
61 | Flexible,
62 | Float,
63 | Float16,
64 | Float32,
65 | Float64,
66 | Floating,
67 | Half,
68 | Inexact,
69 | Int,
70 | Int0,
71 | Int8,
72 | Int16,
73 | Int32,
74 | Int64,
75 | IntC,
76 | Integer,
77 | IntP,
78 | LongComplex,
79 | LongDouble,
80 | LongFloat,
81 | LongLong,
82 | Number,
83 | Object,
84 | Object0,
85 | Short,
86 | SignedInteger,
87 | Single,
88 | SingleComplex,
89 | Str0,
90 | String,
91 | Timedelta64,
92 | UByte,
93 | UInt,
94 | UInt0,
95 | UInt8,
96 | UInt16,
97 | UInt32,
98 | UInt64,
99 | UIntC,
100 | UIntP,
101 | ULongLong,
102 | Unicode,
103 | UnsignedInteger,
104 | UShort,
105 | Void,
106 | Void0,
107 | )
108 |
109 | __all__ = [
110 | "NDArray",
111 | "RecArray",
112 | "assert_isinstance",
113 | "validate_shape_expression",
114 | "normalize_shape_expression",
115 | "NPTypingError",
116 | "InvalidArgumentsError",
117 | "InvalidShapeError",
118 | "InvalidStructureError",
119 | "InvalidDTypeError",
120 | "Shape",
121 | "Structure",
122 | "__version__",
123 | "DType",
124 | "Number",
125 | "Bool",
126 | "Bool8",
127 | "Object",
128 | "Object0",
129 | "Datetime64",
130 | "Integer",
131 | "SignedInteger",
132 | "Int8",
133 | "Int16",
134 | "Int32",
135 | "Int64",
136 | "Byte",
137 | "Short",
138 | "IntC",
139 | "IntP",
140 | "Int0",
141 | "Int",
142 | "LongLong",
143 | "Timedelta64",
144 | "UnsignedInteger",
145 | "UInt8",
146 | "UInt16",
147 | "UInt32",
148 | "UInt64",
149 | "UByte",
150 | "UShort",
151 | "UIntC",
152 | "UIntP",
153 | "UInt0",
154 | "UInt",
155 | "ULongLong",
156 | "Inexact",
157 | "Floating",
158 | "Float16",
159 | "Float32",
160 | "Float64",
161 | "Half",
162 | "Single",
163 | "Double",
164 | "Float",
165 | "LongDouble",
166 | "LongFloat",
167 | "ComplexFloating",
168 | "Complex64",
169 | "Complex128",
170 | "CSingle",
171 | "SingleComplex",
172 | "CDouble",
173 | "Complex",
174 | "CFloat",
175 | "CLongDouble",
176 | "CLongFloat",
177 | "LongComplex",
178 | "Flexible",
179 | "Void",
180 | "Void0",
181 | "Character",
182 | "Bytes",
183 | "String",
184 | "Bytes0",
185 | "Unicode",
186 | "Str0",
187 | "DataFrame",
188 | ]
189 |
--------------------------------------------------------------------------------
/nptyping/assert_isinstance.py:
--------------------------------------------------------------------------------
1 | """
2 | MIT License
3 |
4 | Copyright (c) 2023 Ramon Hagenaars
5 |
6 | Permission is hereby granted, free of charge, to any person obtaining a copy
7 | of this software and associated documentation files (the "Software"), to deal
8 | in the Software without restriction, including without limitation the rights
9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | copies of the Software, and to permit persons to whom the Software is
11 | furnished to do so, subject to the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be included in all
14 | copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | SOFTWARE.
23 | """
24 | from typing import (
25 | Any,
26 | Optional,
27 | Type,
28 | TypeVar,
29 | )
30 |
31 | try:
32 | from typing import TypeGuard # type: ignore[attr-defined]
33 | except ImportError: # pragma: no cover
34 | from typing_extensions import TypeGuard # type: ignore[attr-defined]
35 |
36 | TYPE = TypeVar("TYPE")
37 |
38 |
39 | def assert_isinstance(
40 | instance: Any, cls: Type[TYPE], message: Optional[str] = None
41 | ) -> TypeGuard[TYPE]:
42 | """
43 | A TypeGuard function that is equivalent to `assert instance, cls, message`
44 | that hides nasty MyPy or IDE warnings.
45 | :param instance: the instance that is checked against cls.
46 | :param cls: the class
47 | :param message: any message that is displayed when the assert check fails.
48 | :return: the type of cls.
49 | """
50 | message = message or f"instance={instance!r}, cls={cls!r}"
51 | assert isinstance(instance, cls), message
52 | return True
53 |
--------------------------------------------------------------------------------
/nptyping/base_meta_classes.py:
--------------------------------------------------------------------------------
1 | """
2 | MIT License
3 |
4 | Copyright (c) 2023 Ramon Hagenaars
5 |
6 | Permission is hereby granted, free of charge, to any person obtaining a copy
7 | of this software and associated documentation files (the "Software"), to deal
8 | in the Software without restriction, including without limitation the rights
9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | copies of the Software, and to permit persons to whom the Software is
11 | furnished to do so, subject to the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be included in all
14 | copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | SOFTWARE.
23 | """
24 | from abc import ABCMeta, abstractmethod
25 | from inspect import FrameInfo
26 | from typing import (
27 | Any,
28 | Dict,
29 | List,
30 | Optional,
31 | Set,
32 | Tuple,
33 | TypeVar,
34 | )
35 |
36 | from nptyping.error import InvalidArgumentsError, NPTypingError
37 |
38 | _T = TypeVar("_T")
39 |
40 |
41 | class InconstructableMeta(ABCMeta):
42 | """
43 | Makes it impossible for a class to get instantiated.
44 | """
45 |
46 | def __call__(cls, *_: Any, **__: Any) -> None:
47 | raise NPTypingError(
48 | f"Cannot instantiate nptyping.{cls.__name__}. Did you mean to use [ ] ?"
49 | )
50 |
51 |
52 | class ImmutableMeta(ABCMeta):
53 | """
54 | Makes it impossible to changes values on a class.
55 | """
56 |
57 | def __setattr__(cls, key: str, value: Any) -> None:
58 | if key not in ("_abc_impl", "__abstractmethods__"):
59 | raise NPTypingError(f"Cannot set values to nptyping.{cls.__name__}.")
60 |
61 |
62 | class FinalMeta(ABCMeta):
63 | """
64 | Makes it impossible for classes to inherit from some class.
65 |
66 | An concrete inheriting meta class requires to define a name for its
67 | implementation. The class with this name will be the only class that is
68 | allowed to use that concrete meta class.
69 | """
70 |
71 | _name_per_meta_cls: Dict[type, Optional[str]] = {}
72 |
73 | def __init_subclass__(cls, implementation: Optional[str] = None) -> None:
74 | # implementation is made Optional here, to allow other meta classes to
75 | # inherit.
76 | cls._name_per_meta_cls[cls] = implementation
77 |
78 | def __new__(cls, name: str, *args: Any, **kwargs: Any) -> type:
79 | if name == cls._name_per_meta_cls[cls]:
80 | assert name, "cls_name not set"
81 | return type.__new__(cls, name, *args, **kwargs)
82 |
83 | raise NPTypingError(f"Cannot subclass nptyping.{cls._name_per_meta_cls[cls]}.")
84 |
85 |
86 | class MaybeCheckableMeta(ABCMeta):
87 | """
88 | Makes instance and subclass checks raise by default.
89 | """
90 |
91 | def __instancecheck__(cls, instance: Any) -> bool:
92 | raise NPTypingError(
93 | f"Instance checking is not supported for nptyping.{cls.__name__}."
94 | )
95 |
96 | def __subclasscheck__(cls, subclass: Any) -> bool:
97 | raise NPTypingError(
98 | f"Subclass checking is not supported for nptyping.{cls.__name__}."
99 | )
100 |
101 |
102 | class PrintableMeta(ABCMeta):
103 | """
104 | Ensures that a class can be printed nicely.
105 | """
106 |
107 | @abstractmethod
108 | def __str__(cls) -> str:
109 | ... # pragma: no cover
110 |
111 | def __repr__(cls) -> str:
112 | return str(cls)
113 |
114 |
115 | class SubscriptableMeta(ABCMeta):
116 | """
117 | Makes a class subscriptable: it accepts arguments between brackets and a
118 | new type is returned for every unique set of arguments.
119 | """
120 |
121 | _all_types: Dict[Tuple[type, Tuple[Any, ...]], type] = {}
122 | _parameterized: bool = False
123 |
124 | @abstractmethod
125 | def _get_item(cls, item: Any) -> Tuple[Any, ...]:
126 | ... # pragma: no cover
127 |
128 | def _get_module(cls, stack: List[FrameInfo], module: str) -> str:
129 | # The magic below makes Python's help function display a meaningful
130 | # text with nptyping types.
131 | return "typing" if stack[1][3] == "formatannotation" else module
132 |
133 | def _get_additional_values(
134 | cls, item: Any # pylint: disable=unused-argument
135 | ) -> Dict[str, Any]:
136 | # This method is invoked after _get_item and right before returning
137 | # the result of __getitem__. It can be overridden to provide extra
138 | # values that are to be set as attributes on the new type.
139 | return {}
140 |
141 | def __getitem__(cls, item: Any) -> type:
142 | if getattr(cls, "_parameterized", False):
143 | raise NPTypingError(f"Type nptyping.{cls} is already parameterized.")
144 |
145 | args = cls._get_item(item)
146 | additional_values = cls._get_additional_values(item)
147 | assert hasattr(cls, "__args__"), "A SubscriptableMeta must have __args__."
148 | if args != cls.__args__: # type: ignore[attr-defined]
149 | result = cls._create_type(args, additional_values)
150 | else:
151 | result = cls
152 |
153 | return result
154 |
155 | def _create_type(
156 | cls, args: Tuple[Any, ...], additional_values: Dict[str, Any]
157 | ) -> type:
158 | key = (cls, args)
159 | if key not in cls._all_types:
160 | cls._all_types[key] = type(
161 | cls.__name__,
162 | (cls,),
163 | {"__args__": args, "_parameterized": True, **additional_values},
164 | )
165 | return cls._all_types[key]
166 |
167 |
168 | class ComparableByArgsMeta(ABCMeta):
169 | """
170 | Makes a class comparable by means of its __args__.
171 | """
172 |
173 | __args__: Tuple[Any, ...]
174 |
175 | def __eq__(cls, other: Any) -> bool:
176 | return (
177 | hasattr(cls, "__args__")
178 | and hasattr(other, "__args__")
179 | and cls.__args__ == other.__args__
180 | )
181 |
182 | def __hash__(cls) -> int:
183 | return hash(cls.__args__)
184 |
185 |
186 | class ContainerMeta(
187 | InconstructableMeta,
188 | ImmutableMeta,
189 | FinalMeta,
190 | MaybeCheckableMeta,
191 | PrintableMeta,
192 | SubscriptableMeta,
193 | ComparableByArgsMeta,
194 | ABCMeta,
195 | ):
196 | """
197 | Base meta class for "containers" such as Shape and Structure.
198 | """
199 |
200 | _known_expressions: Set[str] = set()
201 | __args__: Tuple[str, ...]
202 |
203 | @abstractmethod
204 | def _validate_expression(cls, item: str) -> None:
205 | ... # pragma: no cover
206 |
207 | @abstractmethod
208 | def _normalize_expression(cls, item: str) -> str:
209 | ... # pragma: no cover
210 |
211 | def _get_item(cls, item: Any) -> Tuple[Any, ...]:
212 | if not isinstance(item, str):
213 | raise InvalidArgumentsError(
214 | f"Unexpected argument of type {type(item)}, expecting a string."
215 | )
216 |
217 | if item in cls._known_expressions:
218 | # No need to do costly validations and normalizations if it has been done
219 | # before.
220 | return (item,)
221 |
222 | cls._validate_expression(item)
223 | norm_shape_expression = cls._normalize_expression(item)
224 | cls._known_expressions.add(norm_shape_expression)
225 | return (norm_shape_expression,)
226 |
227 | def __subclasscheck__(cls, subclass: Any) -> bool:
228 | type_match = type(subclass) == type( # pylint: disable=unidiomatic-typecheck
229 | cls
230 | )
231 | return type_match and (
232 | subclass.__args__ == cls.__args__ or not cls._parameterized
233 | )
234 |
235 | def __str__(cls) -> str:
236 | return f"{cls.__name__}['{cls.__args__[0]}']"
237 |
238 | def __eq__(cls, other: Any) -> bool:
239 | result = cls is other
240 | if not result and hasattr(cls, "__args__") and hasattr(other, "__args__"):
241 | normalized_args = tuple(
242 | cls._normalize_expression(str(arg)) for arg in other.__args__
243 | )
244 | result = cls.__args__ == normalized_args
245 | return result
246 |
247 | def __hash__(cls) -> int:
248 | return hash(cls.__args__)
249 |
--------------------------------------------------------------------------------
/nptyping/error.py:
--------------------------------------------------------------------------------
1 | """
2 | MIT License
3 |
4 | Copyright (c) 2023 Ramon Hagenaars
5 |
6 | Permission is hereby granted, free of charge, to any person obtaining a copy
7 | of this software and associated documentation files (the "Software"), to deal
8 | in the Software without restriction, including without limitation the rights
9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | copies of the Software, and to permit persons to whom the Software is
11 | furnished to do so, subject to the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be included in all
14 | copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | SOFTWARE.
23 | """
24 |
25 |
26 | class NPTypingError(Exception):
27 | """Base error for all NPTyping errors."""
28 |
29 |
30 | class InvalidArgumentsError(NPTypingError):
31 | """Raised when a invalid arguments are provided to an nptyping type."""
32 |
33 |
34 | class InvalidShapeError(NPTypingError):
35 | """Raised when a shape is considered not valid."""
36 |
37 |
38 | class InvalidStructureError(NPTypingError):
39 | """Raised when a structure is considered not valid."""
40 |
41 |
42 | class InvalidDTypeError(NPTypingError):
43 | """Raised when an argument is not a DType."""
44 |
45 |
46 | class DependencyError(NPTypingError):
47 | """Raised when a dependency has not been installed."""
48 |
--------------------------------------------------------------------------------
/nptyping/ndarray.py:
--------------------------------------------------------------------------------
1 | """
2 | MIT License
3 |
4 | Copyright (c) 2023 Ramon Hagenaars
5 |
6 | Permission is hereby granted, free of charge, to any person obtaining a copy
7 | of this software and associated documentation files (the "Software"), to deal
8 | in the Software without restriction, including without limitation the rights
9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | copies of the Software, and to permit persons to whom the Software is
11 | furnished to do so, subject to the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be included in all
14 | copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | SOFTWARE.
23 | """
24 | import inspect
25 | from abc import ABC
26 | from typing import Any, Tuple
27 |
28 | import numpy as np
29 |
30 | from nptyping.base_meta_classes import (
31 | FinalMeta,
32 | ImmutableMeta,
33 | InconstructableMeta,
34 | MaybeCheckableMeta,
35 | PrintableMeta,
36 | SubscriptableMeta,
37 | )
38 | from nptyping.error import InvalidArgumentsError
39 | from nptyping.nptyping_type import NPTypingType
40 | from nptyping.shape import Shape
41 | from nptyping.shape_expression import check_shape
42 | from nptyping.structure import Structure
43 | from nptyping.structure_expression import check_structure, check_type_names
44 | from nptyping.typing_ import (
45 | DType,
46 | dtype_per_name,
47 | name_per_dtype,
48 | )
49 |
50 |
51 | class NDArrayMeta(
52 | SubscriptableMeta,
53 | InconstructableMeta,
54 | ImmutableMeta,
55 | FinalMeta,
56 | MaybeCheckableMeta,
57 | PrintableMeta,
58 | implementation="NDArray",
59 | ):
60 | """
61 | Metaclass that is coupled to nptyping.NDArray. It contains all actual logic
62 | such as instance checking.
63 | """
64 |
65 | __args__: Tuple[Shape, DType]
66 | _parameterized: bool
67 |
68 | @property
69 | def __module__(cls) -> str:
70 | return cls._get_module(inspect.stack(), "nptyping.ndarray")
71 |
72 | def _get_item(cls, item: Any) -> Tuple[Any, ...]:
73 | cls._check_item(item)
74 | shape, dtype = cls._get_from_tuple(item)
75 | return shape, dtype
76 |
77 | def __instancecheck__( # pylint: disable=bad-mcs-method-argument
78 | self, instance: Any
79 | ) -> bool:
80 | shape, dtype = self.__args__
81 | dtype_is_structure = issubclass(dtype, Structure)
82 | structure_is_ok = dtype_is_structure and check_structure(
83 | instance.dtype, dtype, dtype_per_name
84 | )
85 | return (
86 | isinstance(instance, np.ndarray)
87 | and (shape is Any or check_shape(instance.shape, shape))
88 | and (
89 | dtype is Any
90 | or structure_is_ok
91 | or issubclass(instance.dtype.type, dtype)
92 | )
93 | )
94 |
95 | def __str__(cls) -> str:
96 | shape, dtype = cls.__args__
97 | return (
98 | f"{cls.__name__}[{cls._shape_expression_to_str(shape)}, "
99 | f"{cls._dtype_to_str(dtype)}]"
100 | )
101 |
102 | def _is_literal_like(cls, item: Any) -> bool:
103 | # item is a Literal or "Literal enough" (ducktyping).
104 | return hasattr(item, "__args__")
105 |
106 | def _check_item(cls, item: Any) -> None:
107 | # Check if the item is what we expect and raise if it is not.
108 | if not isinstance(item, tuple):
109 | raise InvalidArgumentsError(f"Unexpected argument of type {type(item)}.")
110 | if len(item) > 2:
111 | raise InvalidArgumentsError(f"Unexpected argument {item[2]}.")
112 |
113 | def _get_from_tuple(cls, item: Tuple[Any, ...]) -> Tuple[Shape, DType]:
114 | # Return the Shape Expression and DType from a tuple.
115 | shape = cls._get_shape(item[0])
116 | dtype = cls._get_dtype(item[1])
117 | return shape, dtype
118 |
119 | def _get_shape(cls, dtype_candidate: Any) -> Shape:
120 | if dtype_candidate is Any or dtype_candidate is Shape:
121 | shape = Any
122 | elif issubclass(dtype_candidate, Shape):
123 | shape = dtype_candidate
124 | elif cls._is_literal_like(dtype_candidate):
125 | shape_expression = dtype_candidate.__args__[0]
126 | shape = Shape[shape_expression]
127 | else:
128 | raise InvalidArgumentsError(
129 | f"Unexpected argument '{dtype_candidate}', expecting"
130 | " Shape[]"
131 | " or Literal[]"
132 | " or typing.Any."
133 | )
134 | return shape
135 |
136 | def _get_dtype(cls, dtype_candidate: Any) -> DType:
137 | is_dtype = isinstance(dtype_candidate, type) and issubclass(
138 | dtype_candidate, np.generic
139 | )
140 | if dtype_candidate is Any:
141 | dtype = Any
142 | elif is_dtype:
143 | dtype = dtype_candidate
144 | elif issubclass(dtype_candidate, Structure):
145 | dtype = dtype_candidate
146 | check_type_names(dtype, dtype_per_name)
147 | elif cls._is_literal_like(dtype_candidate):
148 | structure_expression = dtype_candidate.__args__[0]
149 | dtype = Structure[structure_expression]
150 | check_type_names(dtype, dtype_per_name)
151 | else:
152 | raise InvalidArgumentsError(
153 | f"Unexpected argument '{dtype_candidate}', expecting"
154 | " Structure[]"
155 | " or Literal[]"
156 | " or a dtype"
157 | " or typing.Any."
158 | )
159 | return dtype
160 |
161 | def _dtype_to_str(cls, dtype: Any) -> str:
162 | if dtype is Any:
163 | result = "Any"
164 | elif issubclass(dtype, Structure):
165 | result = str(dtype)
166 | else:
167 | result = name_per_dtype[dtype]
168 | return result
169 |
170 | def _shape_expression_to_str(cls, shape_expression: Any) -> str:
171 | return "Any" if shape_expression is Any else str(shape_expression)
172 |
173 |
174 | class NDArray(NPTypingType, ABC, metaclass=NDArrayMeta):
175 | """
176 | An nptyping equivalent of numpy ndarray.
177 |
178 | ## No arguments means an NDArray with any DType and any shape.
179 | >>> NDArray
180 | NDArray[Any, Any]
181 |
182 | ## You can provide a DType and a Shape Expression.
183 | >>> from nptyping import Int32, Shape
184 | >>> NDArray[Shape["2, 2"], Int32]
185 | NDArray[Shape['2, 2'], Int32]
186 |
187 | ## Instance checking can be done and the shape is also checked.
188 | >>> import numpy as np
189 | >>> isinstance(np.array([[1, 2], [3, 4]]), NDArray[Shape['2, 2'], Int32])
190 | True
191 | >>> isinstance(np.array([[1, 2], [3, 4], [5, 6]]), NDArray[Shape['2, 2'], Int32])
192 | False
193 |
194 | """
195 |
196 | __args__ = (Any, Any)
197 |
--------------------------------------------------------------------------------
/nptyping/ndarray.pyi:
--------------------------------------------------------------------------------
1 | """
2 | MIT License
3 |
4 | Copyright (c) 2023 Ramon Hagenaars
5 |
6 | Permission is hereby granted, free of charge, to any person obtaining a copy
7 | of this software and associated documentation files (the "Software"), to deal
8 | in the Software without restriction, including without limitation the rights
9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | copies of the Software, and to permit persons to whom the Software is
11 | furnished to do so, subject to the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be included in all
14 | copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | SOFTWARE.
23 | """
24 |
25 | import numpy as np
26 |
27 | NDArray = np.ndarray
28 |
--------------------------------------------------------------------------------
/nptyping/nptyping_type.py:
--------------------------------------------------------------------------------
1 | """
2 | MIT License
3 |
4 | Copyright (c) 2023 Ramon Hagenaars
5 |
6 | Permission is hereby granted, free of charge, to any person obtaining a copy
7 | of this software and associated documentation files (the "Software"), to deal
8 | in the Software without restriction, including without limitation the rights
9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | copies of the Software, and to permit persons to whom the Software is
11 | furnished to do so, subject to the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be included in all
14 | copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | SOFTWARE.
23 | """
24 | from abc import ABC
25 |
26 |
27 | class NPTypingType(ABC):
28 | """
29 | Baseclass for all nptyping types.
30 | """
31 |
--------------------------------------------------------------------------------
/nptyping/package_info.py:
--------------------------------------------------------------------------------
1 | """
2 | MIT License
3 |
4 | Copyright (c) 2023 Ramon Hagenaars
5 |
6 | Permission is hereby granted, free of charge, to any person obtaining a copy
7 | of this software and associated documentation files (the "Software"), to deal
8 | in the Software without restriction, including without limitation the rights
9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | copies of the Software, and to permit persons to whom the Software is
11 | furnished to do so, subject to the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be included in all
14 | copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | SOFTWARE.
23 | """
24 | __title__ = "nptyping"
25 | __version__ = "2.5.0"
26 | __author__ = "Ramon Hagenaars"
27 | __author_email__ = "ramon.hagenaars@gmail.com"
28 | __description__ = "Type hints for NumPy."
29 | __url__ = "https://github.com/ramonhagenaars/nptyping"
30 | __license__ = "MIT"
31 | __python_versions__ = [
32 | "3.7",
33 | "3.8",
34 | "3.9",
35 | "3.10",
36 | "3.11",
37 | ]
38 |
--------------------------------------------------------------------------------
/nptyping/pandas_/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ramonhagenaars/nptyping/785cd07e65f992f47256398fd01f62067928d29c/nptyping/pandas_/__init__.py
--------------------------------------------------------------------------------
/nptyping/pandas_/dataframe.py:
--------------------------------------------------------------------------------
1 | """
2 | MIT License
3 |
4 | Copyright (c) 2023 Ramon Hagenaars
5 |
6 | Permission is hereby granted, free of charge, to any person obtaining a copy
7 | of this software and associated documentation files (the "Software"), to deal
8 | in the Software without restriction, including without limitation the rights
9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | copies of the Software, and to permit persons to whom the Software is
11 | furnished to do so, subject to the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be included in all
14 | copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | SOFTWARE.
23 | """
24 | import inspect
25 | from abc import ABC
26 | from typing import Any, Tuple
27 |
28 | import numpy as np
29 |
30 | from nptyping import InvalidArgumentsError
31 | from nptyping.base_meta_classes import (
32 | FinalMeta,
33 | ImmutableMeta,
34 | InconstructableMeta,
35 | MaybeCheckableMeta,
36 | PrintableMeta,
37 | SubscriptableMeta,
38 | )
39 | from nptyping.error import DependencyError
40 | from nptyping.nptyping_type import NPTypingType
41 | from nptyping.pandas_.typing_ import dtype_per_name
42 | from nptyping.structure import Structure
43 | from nptyping.structure_expression import check_structure
44 |
45 | try:
46 | import pandas as pd
47 | except ImportError: # pragma: no cover
48 | pd = None # type: ignore[misc, assignment]
49 |
50 |
51 | class DataFrameMeta(
52 | SubscriptableMeta,
53 | InconstructableMeta,
54 | ImmutableMeta,
55 | FinalMeta,
56 | MaybeCheckableMeta,
57 | PrintableMeta,
58 | implementation="DataFrame",
59 | ):
60 | """
61 | Metaclass that is coupled to nptyping.DataFrame. It contains all actual logic
62 | such as instance checking.
63 | """
64 |
65 | __args__: Tuple[Structure]
66 | _parameterized: bool
67 |
68 | def __instancecheck__( # pylint: disable=bad-mcs-method-argument
69 | self, instance: Any
70 | ) -> bool:
71 | structure = self.__args__[0]
72 |
73 | if pd is None:
74 | raise DependencyError( # pragma: no cover
75 | "Pandas needs to be installed for instance checking. Use `pip "
76 | "install nptyping[pandas]` or `pip install nptyping[complete]`"
77 | )
78 |
79 | if not isinstance(instance, pd.DataFrame):
80 | return False
81 |
82 | if structure is Any:
83 | return True
84 |
85 | structured_dtype = np.dtype(
86 | [(column, dtype.str) for column, dtype in instance.dtypes.items()]
87 | )
88 | return check_structure(structured_dtype, structure, dtype_per_name)
89 |
90 | def _get_item(cls, item: Any) -> Tuple[Structure]:
91 | if item is Any:
92 | return (Any,)
93 | cls._check_item(item)
94 | return (Structure[getattr(item, "__args__")[0]],)
95 |
96 | def __str__(cls) -> str:
97 | structure = cls.__args__[0]
98 | structure_str = "Any" if structure is Any else structure.__args__[0]
99 | return f"{cls.__name__}[{structure_str}]"
100 |
101 | def __repr__(cls) -> str:
102 | structure = cls.__args__[0]
103 | structure_str = "Any" if structure is Any else structure
104 | return f"{cls.__name__}[{structure_str}]"
105 |
106 | @property
107 | def __module__(cls) -> str:
108 | return cls._get_module(inspect.stack(), "nptyping.pandas_.dataframe")
109 |
110 | def _check_item(cls, item: Any) -> None:
111 | # Check if the item is what we expect and raise if it is not.
112 | if not hasattr(item, "__args__"):
113 | raise InvalidArgumentsError(f"Unexpected argument of type {type(item)}.")
114 |
115 |
116 | class DataFrame(NPTypingType, ABC, metaclass=DataFrameMeta):
117 | """
118 | An nptyping equivalent of pandas DataFrame.
119 |
120 | ## No arguments means a DataFrame of any structure.
121 | >>> DataFrame
122 | DataFrame[Any]
123 |
124 | ## You can use Structure Expression.
125 | >>> from nptyping import DataFrame, Structure
126 | >>> DataFrame[Structure["x: Int, y: Int"]]
127 | DataFrame[Structure['[x, y]: Int']]
128 |
129 | ## Instance checking can be done and the structure is also checked.
130 | >>> import pandas as pd
131 | >>> df = pd.DataFrame({'x': [1, 2, 3], 'y': [4., 5., 6.]})
132 | >>> isinstance(df, DataFrame[Structure['x: Int, y: Float']])
133 | True
134 | >>> isinstance(df, DataFrame[Structure['x: Float, y: Int']])
135 | False
136 |
137 | """
138 |
139 | __args__ = (Any,)
140 |
--------------------------------------------------------------------------------
/nptyping/pandas_/dataframe.pyi:
--------------------------------------------------------------------------------
1 | """
2 | MIT License
3 |
4 | Copyright (c) 2023 Ramon Hagenaars
5 |
6 | Permission is hereby granted, free of charge, to any person obtaining a copy
7 | of this software and associated documentation files (the "Software"), to deal
8 | in the Software without restriction, including without limitation the rights
9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | copies of the Software, and to permit persons to whom the Software is
11 | furnished to do so, subject to the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be included in all
14 | copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | SOFTWARE.
23 | """
24 |
25 | import pandas as pd
26 |
27 | DataFrame = pd.DataFrame
28 |
--------------------------------------------------------------------------------
/nptyping/pandas_/typing_.py:
--------------------------------------------------------------------------------
1 | """
2 | MIT License
3 |
4 | Copyright (c) 2023 Ramon Hagenaars
5 |
6 | Permission is hereby granted, free of charge, to any person obtaining a copy
7 | of this software and associated documentation files (the "Software"), to deal
8 | in the Software without restriction, including without limitation the rights
9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | copies of the Software, and to permit persons to whom the Software is
11 | furnished to do so, subject to the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be included in all
14 | copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | SOFTWARE.
23 | """
24 | from nptyping.typing_ import Object
25 | from nptyping.typing_ import dtype_per_name as dtype_per_name_default
26 |
27 | dtype_per_name = {
28 | **dtype_per_name_default, # type: ignore[arg-type]
29 | # Override the `String` and `Str` to point to `Object`. Pandas uses Object
30 | # for string types in Dataframes and Series.
31 | "String": Object,
32 | "Str": Object,
33 | }
34 |
--------------------------------------------------------------------------------
/nptyping/py.typed:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ramonhagenaars/nptyping/785cd07e65f992f47256398fd01f62067928d29c/nptyping/py.typed
--------------------------------------------------------------------------------
/nptyping/recarray.py:
--------------------------------------------------------------------------------
1 | """
2 | MIT License
3 |
4 | Copyright (c) 2023 Ramon Hagenaars
5 |
6 | Permission is hereby granted, free of charge, to any person obtaining a copy
7 | of this software and associated documentation files (the "Software"), to deal
8 | in the Software without restriction, including without limitation the rights
9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | copies of the Software, and to permit persons to whom the Software is
11 | furnished to do so, subject to the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be included in all
14 | copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | SOFTWARE.
23 | """
24 | import inspect
25 | from typing import Any, Tuple
26 |
27 | import numpy as np
28 |
29 | from nptyping.error import InvalidArgumentsError
30 | from nptyping.ndarray import NDArray, NDArrayMeta
31 | from nptyping.structure import Structure
32 | from nptyping.typing_ import DType
33 |
34 |
35 | class RecArrayMeta(NDArrayMeta, implementation="RecArray"):
36 | """
37 | Metaclass that is coupled to nptyping.RecArray. It takes most of its logic
38 | from NDArrayMeta.
39 | """
40 |
41 | def _get_item(cls, item: Any) -> Tuple[Any, ...]:
42 | cls._check_item(item)
43 | shape, dtype = cls._get_from_tuple(item)
44 | return shape, dtype
45 |
46 | def _get_dtype(cls, dtype_candidate: Any) -> DType:
47 | if not issubclass(dtype_candidate, Structure) and dtype_candidate is not Any:
48 | raise InvalidArgumentsError(
49 | f"Unexpected argument {dtype_candidate}. Expecting a Structure."
50 | )
51 | return dtype_candidate
52 |
53 | @property
54 | def __module__(cls) -> str:
55 | return cls._get_module(inspect.stack(), "nptyping.recarray")
56 |
57 | def __instancecheck__( # pylint: disable=bad-mcs-method-argument
58 | self, instance: Any
59 | ) -> bool:
60 | return isinstance(instance, np.recarray) and NDArrayMeta.__instancecheck__(
61 | self, instance
62 | )
63 |
64 |
65 | class RecArray(NDArray, metaclass=RecArrayMeta):
66 | """
67 | An nptyping equivalent of numpy recarray.
68 |
69 | ## RecArrays can take a Shape and must take a Structure
70 | >>> from nptyping import Shape, Structure
71 | >>> RecArray[Shape["2, 2"], Structure["x: Float, y: Float"]]
72 | RecArray[Shape['2, 2'], Structure['[x, y]: Float']]
73 |
74 | ## Or Any
75 | >>> from typing import Any
76 | >>> RecArray[Shape["2, 2"], Any]
77 | RecArray[Shape['2, 2'], Any]
78 | """
79 |
--------------------------------------------------------------------------------
/nptyping/recarray.pyi:
--------------------------------------------------------------------------------
1 | """
2 | MIT License
3 |
4 | Copyright (c) 2023 Ramon Hagenaars
5 |
6 | Permission is hereby granted, free of charge, to any person obtaining a copy
7 | of this software and associated documentation files (the "Software"), to deal
8 | in the Software without restriction, including without limitation the rights
9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | copies of the Software, and to permit persons to whom the Software is
11 | furnished to do so, subject to the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be included in all
14 | copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | SOFTWARE.
23 | """
24 |
25 | import numpy as np
26 |
27 | RecArray = np.recarray
28 |
--------------------------------------------------------------------------------
/nptyping/shape.py:
--------------------------------------------------------------------------------
1 | """
2 | MIT License
3 |
4 | Copyright (c) 2023 Ramon Hagenaars
5 |
6 | Permission is hereby granted, free of charge, to any person obtaining a copy
7 | of this software and associated documentation files (the "Software"), to deal
8 | in the Software without restriction, including without limitation the rights
9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | copies of the Software, and to permit persons to whom the Software is
11 | furnished to do so, subject to the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be included in all
14 | copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | SOFTWARE.
23 | """
24 | from abc import ABC
25 | from typing import Any, Dict
26 |
27 | from nptyping.base_meta_classes import ContainerMeta
28 | from nptyping.nptyping_type import NPTypingType
29 | from nptyping.shape_expression import (
30 | get_dimensions,
31 | normalize_shape_expression,
32 | remove_labels,
33 | validate_shape_expression,
34 | )
35 |
36 |
37 | class ShapeMeta(ContainerMeta, implementation="Shape"):
38 | """
39 | Metaclass that is coupled to nptyping.Shape.
40 | """
41 |
42 | def _validate_expression(cls, item: str) -> None:
43 | validate_shape_expression(item)
44 |
45 | def _normalize_expression(cls, item: str) -> str:
46 | return normalize_shape_expression(item)
47 |
48 | def _get_additional_values(cls, item: Any) -> Dict[str, Any]:
49 | dim_strings = get_dimensions(item)
50 | dim_string_without_labels = remove_labels(dim_strings)
51 | return {"prepared_args": dim_string_without_labels}
52 |
53 |
54 | class Shape(NPTypingType, ABC, metaclass=ShapeMeta):
55 | """
56 | A container for shape expressions that describe the shape of an multi
57 | dimensional array.
58 |
59 | Simple example:
60 |
61 | >>> Shape['2, 2']
62 | Shape['2, 2']
63 |
64 | A Shape can be compared to a typing.Literal. You can use Literals in
65 | NDArray as well.
66 |
67 | >>> from typing import Literal
68 |
69 | >>> Shape['2, 2'] == Literal['2, 2']
70 | True
71 |
72 | """
73 |
74 | __args__ = ("*, ...",)
75 | prepared_args = "*, ..."
76 |
--------------------------------------------------------------------------------
/nptyping/shape.pyi:
--------------------------------------------------------------------------------
1 | """
2 | MIT License
3 |
4 | Copyright (c) 2023 Ramon Hagenaars
5 |
6 | Permission is hereby granted, free of charge, to any person obtaining a copy
7 | of this software and associated documentation files (the "Software"), to deal
8 | in the Software without restriction, including without limitation the rights
9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | copies of the Software, and to permit persons to whom the Software is
11 | furnished to do so, subject to the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be included in all
14 | copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | SOFTWARE.
23 | """
24 | try:
25 | from typing import Literal # type: ignore[attr-defined]
26 | except ImportError:
27 | from typing_extensions import Literal # type: ignore[attr-defined,misc,assignment]
28 |
29 | from typing import Any, cast
30 |
31 | # For MyPy:
32 | Shape = cast(Literal, Shape) # type: ignore[has-type,misc,valid-type]
33 |
34 | # For PyRight:
35 | class Shape: # type: ignore[no-redef]
36 | def __class_getitem__(cls, item: Any) -> Any: ...
37 |
--------------------------------------------------------------------------------
/nptyping/shape_expression.py:
--------------------------------------------------------------------------------
1 | """
2 | MIT License
3 |
4 | Copyright (c) 2023 Ramon Hagenaars
5 |
6 | Permission is hereby granted, free of charge, to any person obtaining a copy
7 | of this software and associated documentation files (the "Software"), to deal
8 | in the Software without restriction, including without limitation the rights
9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | copies of the Software, and to permit persons to whom the Software is
11 | furnished to do so, subject to the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be included in all
14 | copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | SOFTWARE.
23 | """
24 | import re
25 | import string
26 | from functools import lru_cache
27 | from typing import (
28 | TYPE_CHECKING,
29 | Any,
30 | Dict,
31 | List,
32 | Union,
33 | )
34 |
35 | from nptyping.error import InvalidShapeError
36 | from nptyping.typing_ import ShapeExpression, ShapeTuple
37 |
38 | if TYPE_CHECKING:
39 | from nptyping.shape import Shape # pragma: no cover
40 |
41 |
42 | @lru_cache()
43 | def check_shape(shape: ShapeTuple, target: "Shape") -> bool:
44 | """
45 | Check whether the given shape corresponds to the given shape_expression.
46 | :param shape: the shape in question.
47 | :param target: the shape expression to which shape is tested.
48 | :return: True if the given shape corresponds to shape_expression.
49 | """
50 | target_shape = _handle_ellipsis(shape, target.prepared_args)
51 | return _check_dimensions_against_shape(shape, target_shape)
52 |
53 |
54 | def validate_shape_expression(shape_expression: Union[ShapeExpression, Any]) -> None:
55 | """
56 | Validate shape_expression and raise an InvalidShapeError if it is not
57 | considered valid.
58 | :param shape_expression: the shape expression to validate.
59 | :return: None.
60 | """
61 | shape_expression_no_quotes = shape_expression.replace("'", "").replace('"', "")
62 | if shape_expression is not Any and not re.match(
63 | _REGEX_SHAPE_EXPRESSION, shape_expression_no_quotes
64 | ):
65 | raise InvalidShapeError(
66 | f"'{shape_expression}' is not a valid shape expression."
67 | )
68 |
69 |
70 | def normalize_shape_expression(shape_expression: ShapeExpression) -> ShapeExpression:
71 | """
72 | Normalize the given shape expression, e.g. by removing whitespaces, making
73 | similar expressions look the same.
74 | :param shape_expression: the shape expression that is to be normalized.
75 | :return: a normalized shape expression.
76 | """
77 | shape_expression = shape_expression.replace("'", "").replace('"', "")
78 | # Replace whitespaces right before labels with $.
79 | shape_expression = re.sub(rf"\s*{_REGEX_LABEL}", r"$\1", shape_expression)
80 | # Let all commas be followed by a $.
81 | shape_expression = shape_expression.replace(",", ",$")
82 | # Remove all whitespaces left.
83 | shape_expression = re.sub(r"\s*", "", shape_expression)
84 | # Remove $ right after a bracket.
85 | shape_expression = re.sub(r"\[\$+", "[", shape_expression)
86 | # Replace $ with a single space.
87 | shape_expression = re.sub(r"\$+", " ", shape_expression)
88 | return shape_expression
89 |
90 |
91 | def get_dimensions(shape_expression: str) -> List[str]:
92 | """
93 | Find all "break downs" (the parts between brackets) in a shape expressions
94 | and replace them with mere dimension sizes.
95 |
96 | :param shape_expression: the shape expression that gets the break downs replaced.
97 | :return: a list of dimensions without break downs.
98 | """
99 | shape_expression_without_breakdowns = shape_expression
100 | for dim_breakdown in re.findall(
101 | r"(\[[^\]]+\])", shape_expression_without_breakdowns
102 | ):
103 | dim_size = len(dim_breakdown.split(","))
104 | shape_expression_without_breakdowns = (
105 | shape_expression_without_breakdowns.replace(dim_breakdown, str(dim_size))
106 | )
107 | return shape_expression_without_breakdowns.split(",")
108 |
109 |
110 | def remove_labels(dimensions: List[str]) -> List[str]:
111 | """
112 | Remove all labels (words that start with a lowercase).
113 |
114 | :param dimensions: a list of dimensions.
115 | :return: a copy of the given list without labels.
116 | """
117 | return [re.sub(r"\b[a-z]\w*", "", dim).strip() for dim in dimensions]
118 |
119 |
120 | def _check_dimensions_against_shape(shape: ShapeTuple, target: List[str]) -> bool:
121 | # Walk through the shape and test them against the given target,
122 | # taking into consideration variables, wildcards, etc.
123 |
124 | if len(shape) != len(target):
125 | return False
126 | shape_as_strings = (str(dim) for dim in shape)
127 | variables: Dict[str, str] = {}
128 | for dim, target_dim in zip(shape_as_strings, target):
129 | if _is_wildcard(target_dim) or _is_assignable_var(dim, target_dim, variables):
130 | continue
131 | if dim != target_dim:
132 | return False
133 | return True
134 |
135 |
136 | def _handle_ellipsis(shape: ShapeTuple, target: List[str]) -> List[str]:
137 | # Let the ellipsis allows for any number of dimensions by replacing the
138 | # ellipsis with the dimension size repeated the number of times that
139 | # corresponds to the shape of the instance.
140 | if target[-1] == "...":
141 | dim_to_repeat = target[-2]
142 | target = target[0:-1]
143 | if len(shape) > len(target):
144 | difference = len(shape) - len(target)
145 | target += difference * [dim_to_repeat]
146 | return target
147 |
148 |
149 | def _is_assignable_var(dim: str, target_dim: str, variables: Dict[str, str]) -> bool:
150 | # Return whether target_dim is a variable and can be assigned with dim.
151 | return _is_variable(target_dim) and _can_assign_variable(dim, target_dim, variables)
152 |
153 |
154 | def _is_variable(dim: str) -> bool:
155 | # Return whether dim is a variable.
156 | return dim[0] in string.ascii_uppercase
157 |
158 |
159 | def _can_assign_variable(dim: str, target_dim: str, variables: Dict[str, str]) -> bool:
160 | # Check and assign a variable.
161 | assignable = variables.get(target_dim) in (None, dim)
162 | variables[target_dim] = dim
163 | return assignable
164 |
165 |
166 | def _is_wildcard(dim: str) -> bool:
167 | # Return whether dim is a wildcard (i.e. the character that takes any
168 | # dimension size).
169 | return dim == "*"
170 |
171 |
172 | _REGEX_SEPARATOR = r"(\s*,\s*)"
173 | _REGEX_DIMENSION_SIZE = r"(\s*[0-9]+\s*)"
174 | _REGEX_VARIABLE = r"(\s*\b[A-Z]\w*\s*)"
175 | _REGEX_LABEL = r"(\s*\b[a-z]\w*\s*)"
176 | _REGEX_LABELS = rf"({_REGEX_LABEL}({_REGEX_SEPARATOR}{_REGEX_LABEL})*)"
177 | _REGEX_WILDCARD = r"(\s*\*\s*)"
178 | _REGEX_DIMENSION_BREAKDOWN = rf"(\s*\[{_REGEX_LABELS}\]\s*)"
179 | _REGEX_DIMENSION = (
180 | rf"({_REGEX_DIMENSION_SIZE}"
181 | rf"|{_REGEX_VARIABLE}"
182 | rf"|{_REGEX_WILDCARD}"
183 | rf"|{_REGEX_DIMENSION_BREAKDOWN})"
184 | )
185 | _REGEX_DIMENSION_WITH_LABEL = rf"({_REGEX_DIMENSION}(\s+{_REGEX_LABEL})*)"
186 | _REGEX_DIMENSIONS = (
187 | rf"{_REGEX_DIMENSION_WITH_LABEL}({_REGEX_SEPARATOR}{_REGEX_DIMENSION_WITH_LABEL})*"
188 | )
189 | _REGEX_DIMENSIONS_ELLIPSIS = rf"({_REGEX_DIMENSIONS}{_REGEX_SEPARATOR}\.\.\.\s*)"
190 | _REGEX_SHAPE_EXPRESSION = rf"^({_REGEX_DIMENSIONS}|{_REGEX_DIMENSIONS_ELLIPSIS})$"
191 |
--------------------------------------------------------------------------------
/nptyping/structure.py:
--------------------------------------------------------------------------------
1 | """
2 | MIT License
3 |
4 | Copyright (c) 2023 Ramon Hagenaars
5 |
6 | Permission is hereby granted, free of charge, to any person obtaining a copy
7 | of this software and associated documentation files (the "Software"), to deal
8 | in the Software without restriction, including without limitation the rights
9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | copies of the Software, and to permit persons to whom the Software is
11 | furnished to do so, subject to the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be included in all
14 | copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | SOFTWARE.
23 | """
24 | from abc import ABC
25 | from typing import (
26 | Any,
27 | Dict,
28 | List,
29 | )
30 |
31 | from nptyping.base_meta_classes import ContainerMeta
32 | from nptyping.nptyping_type import NPTypingType
33 | from nptyping.structure_expression import (
34 | create_name_to_type_dict,
35 | normalize_structure_expression,
36 | validate_structure_expression,
37 | )
38 |
39 |
40 | class StructureMeta(ContainerMeta, implementation="Structure"):
41 | """
42 | Metaclass that is coupled to nptyping.Structure.
43 | """
44 |
45 | __args__ = tuple()
46 |
47 | def _validate_expression(cls, item: str) -> None:
48 | validate_structure_expression(item)
49 |
50 | def _normalize_expression(cls, item: str) -> str:
51 | return normalize_structure_expression(item)
52 |
53 | def _get_additional_values(cls, item: Any) -> Dict[str, Any]:
54 | return {
55 | "_type_per_name": create_name_to_type_dict(item),
56 | "_has_wildcard": item.replace(" ", "").endswith(",*"),
57 | }
58 |
59 |
60 | class Structure(NPTypingType, ABC, metaclass=StructureMeta):
61 | """
62 | A container for structure expressions that describe the structured dtype of
63 | an array.
64 |
65 | Simple example:
66 |
67 | >>> Structure["x: Float, y: Float"]
68 | Structure['[x, y]: Float']
69 |
70 | """
71 |
72 | _type_per_name = {}
73 | _has_wildcard = False
74 |
75 | @classmethod
76 | def has_wildcard(cls) -> bool:
77 | """
78 | Returns whether this Structure has a wildcard for any other columns.
79 | :return: True if this Structure expresses "any other columns".
80 | """
81 | return cls._has_wildcard
82 |
83 | @classmethod
84 | def get_types(cls) -> List[str]:
85 | """
86 | Return a list of all types (strings) in this Structure.
87 | :return: a list of all types in this Structure.
88 | """
89 | return list(set(cls._type_per_name.values()))
90 |
91 | @classmethod
92 | def get_names(cls) -> List[str]:
93 | """
94 | Return a list of all names in this Structure.
95 | :return: a list of all names in this Structure.
96 | """
97 | return list(cls._type_per_name.keys())
98 |
99 | @classmethod
100 | def get_type(cls, name: str) -> str:
101 | """
102 | Get the type (str) that corresponds to the given name. For example for
103 | Structure["x: Float"], get_type("x") would give "Float".
104 | :param name: the name of which the type is to be returned.
105 | :return: the type as a string that corresponds to that name.
106 | """
107 | return cls._type_per_name[name]
108 |
--------------------------------------------------------------------------------
/nptyping/structure.pyi:
--------------------------------------------------------------------------------
1 | """
2 | MIT License
3 |
4 | Copyright (c) 2023 Ramon Hagenaars
5 |
6 | Permission is hereby granted, free of charge, to any person obtaining a copy
7 | of this software and associated documentation files (the "Software"), to deal
8 | in the Software without restriction, including without limitation the rights
9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | copies of the Software, and to permit persons to whom the Software is
11 | furnished to do so, subject to the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be included in all
14 | copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | SOFTWARE.
23 | """
24 | try:
25 | from typing import Literal # type: ignore[attr-defined]
26 | except ImportError:
27 | from typing_extensions import Literal # type: ignore[attr-defined,misc,assignment]
28 |
29 | from typing import Any, cast
30 |
31 | import numpy as np
32 |
33 | # For MyPy:
34 | Structure = cast(Literal, Structure) # type: ignore[has-type,misc,valid-type]
35 |
36 | # For PyRight:
37 | class Structure(np.dtype[Any]): # type: ignore[no-redef,misc]
38 | def __class_getitem__(cls, item: Any) -> Any: ...
39 |
--------------------------------------------------------------------------------
/nptyping/structure_expression.py:
--------------------------------------------------------------------------------
1 | """
2 | MIT License
3 |
4 | Copyright (c) 2023 Ramon Hagenaars
5 |
6 | Permission is hereby granted, free of charge, to any person obtaining a copy
7 | of this software and associated documentation files (the "Software"), to deal
8 | in the Software without restriction, including without limitation the rights
9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | copies of the Software, and to permit persons to whom the Software is
11 | furnished to do so, subject to the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be included in all
14 | copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | SOFTWARE.
23 | """
24 | import re
25 | from collections import Counter, defaultdict
26 | from difflib import get_close_matches
27 | from typing import (
28 | TYPE_CHECKING,
29 | Any,
30 | Dict,
31 | Generator,
32 | List,
33 | Mapping,
34 | Tuple,
35 | Type,
36 | Union,
37 | )
38 |
39 | import numpy as np
40 |
41 | from nptyping.error import InvalidShapeError, InvalidStructureError
42 | from nptyping.shape import Shape
43 | from nptyping.shape_expression import (
44 | check_shape,
45 | normalize_shape_expression,
46 | validate_shape_expression,
47 | )
48 | from nptyping.typing_ import StructureExpression
49 |
50 | if TYPE_CHECKING:
51 | from nptyping.structure import Structure # pragma: no cover
52 |
53 |
54 | def validate_structure_expression(
55 | structure_expression: Union[StructureExpression, Any]
56 | ) -> None:
57 | """
58 | Validate the given structure_expression and raise an InvalidStructureError
59 | if it is deemed invalid.
60 | :param structure_expression: the structure expression in question.
61 | :return: None.
62 | """
63 | if structure_expression is not Any:
64 | if not re.match(_REGEX_STRUCTURE_EXPRESSION, structure_expression):
65 | raise InvalidStructureError(
66 | f"'{structure_expression}' is not a valid structure expression."
67 | )
68 | _validate_structure_expression_contains_no_multiple_field_names(
69 | structure_expression
70 | )
71 | _validate_sub_array_expressions(structure_expression)
72 |
73 |
74 | def check_structure(
75 | structured_dtype: np.dtype, # type: ignore[type-arg]
76 | target: "Structure",
77 | type_per_name: Dict[str, type],
78 | ) -> bool:
79 | """
80 | Check the given structured_dtype against the given target Structure and
81 | return whether it corresponds (True) or not (False). The given dictionary
82 | contains the vocabulary context for the check.
83 | :param structured_dtype: the dtype in question.
84 | :param target: the target Structure that is checked against.
85 | :param type_per_name: a dict that holds the types by their names as they
86 | occur in a structure expression.
87 | :return: True if the given dtype is valid with the given target.
88 | """
89 | fields: Mapping[str, Any] = structured_dtype.fields or {} # type: ignore[assignment]
90 |
91 | # Add the wildcard to the lexicon. We want to do this here to keep
92 | # knowledge on wildcards in one place (this module).
93 | type_per_name_with_wildcard: Dict[str, type] = {
94 | **type_per_name,
95 | "*": object,
96 | } # type: ignore[arg-type]
97 |
98 | if target.has_wildcard():
99 | # Check from the Target's perspective. All fields in the Target should be
100 | # in the subject.
101 | def iterator() -> Generator[Tuple[str, Tuple[np.dtype, int]], None, None]: # type: ignore[type-arg] # pylint: disable=line-too-long
102 | for name_ in target.get_names():
103 | yield name_, fields.get(name_) # type: ignore[misc]
104 |
105 | else:
106 | # Check from the subject's perspective. All fields in the subject
107 | # should be in the target.
108 | if set(target.get_names()) != set(fields.keys()):
109 | return False
110 |
111 | def iterator() -> Generator[Tuple[str, Tuple[np.dtype, int]], None, None]: # type: ignore[type-arg] # pylint: disable=line-too-long
112 | for name_, dtype_tuple_ in fields.items():
113 | yield name_, dtype_tuple_ # type: ignore[misc]
114 |
115 | for name, dtype_tuple in iterator():
116 | field_in_target_not_in_subject = dtype_tuple is None
117 | if field_in_target_not_in_subject or not _check_structure_field(
118 | name, dtype_tuple, target, type_per_name_with_wildcard
119 | ):
120 | return False
121 | return True
122 |
123 |
124 | def _check_structure_field(
125 | name: str,
126 | dtype_tuple: Tuple[np.dtype, int], # type: ignore[type-arg]
127 | target: "Structure",
128 | type_per_name_with_wildcard: Dict[str, type],
129 | ) -> bool:
130 | dtype = dtype_tuple[0]
131 | target_type_name = target.get_type(name)
132 | target_type_shape_match = re.search(_REGEX_FIELD_SHAPE, target_type_name)
133 | actual_type = dtype.type
134 | if target_type_shape_match:
135 | if not dtype.subdtype:
136 | # the dtype does not contain a shape.
137 | return False
138 | actual_type = dtype.subdtype[0].type
139 | target_type_shape = target_type_shape_match.group(1)
140 | shape_corresponds = check_shape(dtype.shape, Shape[target_type_shape])
141 | if not shape_corresponds:
142 | return False
143 | target_type_name = target_type_name.replace(
144 | target_type_shape_match.group(0), ""
145 | )
146 | check_type_name(target_type_name, type_per_name_with_wildcard)
147 | target_type = type_per_name_with_wildcard[target_type_name]
148 | return issubclass(actual_type, target_type)
149 |
150 |
151 | def check_type_names(
152 | structure: "Structure", type_per_name: Dict[str, Type[object]]
153 | ) -> None:
154 | """
155 | Check the given structure for any invalid type names in the given context
156 | of type_per_name. Raises an InvalidStructureError if a type name is
157 | invalid.
158 | :param structure: the Structure that is checked.
159 | :param type_per_name: the context that determines which type names are valid.
160 | :return: None.
161 | """
162 | for type_ in structure.get_types():
163 | check_type_name(type_, type_per_name)
164 |
165 |
166 | def check_type_name(type_name: str, type_per_name: Dict[str, Type[object]]) -> None:
167 | """
168 | Check if the given type_name is in type_per_name and raise a meaningful
169 | error if not.
170 | :param type_name: the key that is checked to be in type_per_name.
171 | :param type_per_name: a dict that is looked in for type_name.
172 | :return: None.
173 | """
174 | # Remove any subarray stuff here.
175 | type_name = type_name.split("[")[0]
176 | if type_name not in type_per_name:
177 | close_matches = get_close_matches(
178 | type_name, type_per_name.keys(), 3, cutoff=0.4
179 | )
180 | close_matches_str = ", ".join(f"'{match}'" for match in close_matches)
181 | extra_help = ""
182 | if len(close_matches) > 1:
183 | extra_help = f" Did you mean one of {close_matches_str}?"
184 | elif close_matches:
185 | extra_help = f" Did you mean {close_matches_str}?"
186 | raise InvalidStructureError( # pylint: disable=raise-missing-from
187 | f"Type '{type_name}' is not valid in this context.{extra_help}"
188 | )
189 |
190 |
191 | def normalize_structure_expression(
192 | structure_expression: StructureExpression,
193 | ) -> StructureExpression:
194 | """
195 | Normalize the given structure expression, e.g. by removing whitespaces,
196 | making similar expressions look the same.
197 | :param structure_expression: the structure expression that is to be normalized.
198 | :return: a normalized structure expression.
199 | """
200 | structure_expression = re.sub(r"\s*", "", structure_expression)
201 | type_to_names_dict = _create_type_to_names_dict(structure_expression)
202 | normalized_structure_expression = _type_to_names_dict_to_str(type_to_names_dict)
203 | result = normalized_structure_expression.replace(",", ", ").replace(" ", " ")
204 | has_wildcard_end = structure_expression.replace(" ", "").endswith(",*")
205 | if has_wildcard_end:
206 | result += ", *"
207 | return result
208 |
209 |
210 | def create_name_to_type_dict(
211 | structure_expression: StructureExpression,
212 | ) -> Dict[str, str]:
213 | """
214 | Create a dict with a name as key and a type (str) as value from the given
215 | structure expression. Structure["x: Int, y: Float"] would yield
216 | {"x: "Int", "y": "Float"}.
217 | :param structure_expression: the structure expression from which the dict
218 | is extracted.
219 | :return: a dict with names and their types, both as strings.
220 | """
221 | type_to_names_dict = _create_type_to_names_dict(structure_expression)
222 | return {
223 | name.strip(): type_.strip()
224 | for type_, names in type_to_names_dict.items()
225 | for name in names
226 | }
227 |
228 |
229 | def _validate_structure_expression_contains_no_multiple_field_names(
230 | structure_expression: StructureExpression,
231 | ) -> None:
232 | # Validate that there are not multiple occurrences of the same field names.
233 | matches = re.findall(_REGEX_FIELD, re.sub(r"\s*", "", structure_expression))
234 | field_name_combinations = [match[0].split(":")[0] for match in matches]
235 | field_names: List[str] = []
236 | for field_name_combination in field_name_combinations:
237 | field_name_combination_match = re.match(
238 | _REGEX_FIELD_NAMES_COMBINATION, field_name_combination
239 | )
240 | if field_name_combination_match:
241 | field_names += field_name_combination_match.group(2).split(_SEPARATOR)
242 | else:
243 | field_names.append(field_name_combination)
244 | field_name_counter = Counter(field_names)
245 | field_names_occurring_multiple_times = [
246 | field_name for field_name, amount in field_name_counter.items() if amount > 1
247 | ]
248 | if field_names_occurring_multiple_times:
249 | # If there are multiple, just raise about the first. Otherwise the
250 | # error message gets bloated.
251 | field_name_under_fire = field_names_occurring_multiple_times[0]
252 | raise InvalidStructureError(
253 | f"Field names may occur only once in a structure expression."
254 | f" Field name '{field_name_under_fire}' occurs"
255 | f" {field_name_counter[field_name_under_fire]} times in"
256 | f" '{structure_expression}'."
257 | )
258 |
259 |
260 | def _validate_sub_array_expressions(structure_expression: str) -> None:
261 | # Validate that the given structure expression does not contain any shape
262 | # expressions for sub arrays that are invalid.
263 | for field_match in re.findall(_REGEX_FIELD, structure_expression):
264 | field_type = field_match[0].split(_FIELD_TYPE_POINTER)[1]
265 | type_shape_match = re.search(_REGEX_FIELD_SHAPE, field_type)
266 | if type_shape_match:
267 | type_shape = type_shape_match[1]
268 | try:
269 | validate_shape_expression(type_shape)
270 | except InvalidShapeError as err:
271 | raise InvalidStructureError(
272 | f"'{structure_expression}' is not a valid structure"
273 | f" expression; {str(err)}"
274 | ) from err
275 |
276 |
277 | def _create_type_to_names_dict(
278 | structure_expression: StructureExpression,
279 | ) -> Dict[str, List[str]]:
280 | # Create a dictionary with field names per type, sorted by type and then by
281 | # name.
282 | names_per_type: Dict[str, List[str]] = defaultdict(list)
283 | for field_match in re.findall(_REGEX_FIELD, structure_expression):
284 | field_name_combination, field_type = field_match[0].split(_FIELD_TYPE_POINTER)
285 | field_name_combination_match = re.match(
286 | _REGEX_FIELD_NAMES_COMBINATION, field_name_combination
287 | )
288 | field_type_shape_match = re.search(_REGEX_FIELD_SHAPE, field_type)
289 | if field_name_combination_match:
290 | field_names = field_name_combination_match.group(2).split(_SEPARATOR)
291 | else:
292 | field_names = [field_name_combination]
293 | if field_type_shape_match:
294 | type_shape = field_type_shape_match.group(1)
295 | normalized_type_shape = normalize_shape_expression(type_shape)
296 | field_type = field_type.replace(
297 | field_type_shape_match.group(0), f"[{normalized_type_shape}]"
298 | )
299 | names_per_type[field_type] += field_names
300 | return {
301 | field_type: sorted(names_per_type[field_type])
302 | for field_type in sorted(names_per_type.keys())
303 | }
304 |
305 |
306 | def _type_to_names_dict_to_str(type_to_names_dict: Dict[str, List[str]]) -> str:
307 | # Turn the given dict into a structure expression.
308 | field_strings = []
309 | for field_type, field_names in type_to_names_dict.items():
310 | field_names_joined = f"{_SEPARATOR}".join(field_names)
311 | if len(field_names) > 1:
312 | field_names_joined = f"[{field_names_joined}]"
313 | field_strings.append(f"{field_names_joined}{_FIELD_TYPE_POINTER} {field_type}")
314 | return f"{_SEPARATOR}".join(field_strings)
315 |
316 |
317 | _SEPARATOR = ","
318 | _FIELD_TYPE_POINTER = ":"
319 | _REGEX_SEPARATOR = rf"(\s*{_SEPARATOR}\s*)"
320 | _REGEX_FIELD_NAME = r"(\s*[a-zA-Z]\w*\s*)"
321 | _REGEX_FIELD_NAMES = rf"({_REGEX_FIELD_NAME}({_REGEX_SEPARATOR}{_REGEX_FIELD_NAME})+)"
322 | _REGEX_FIELD_NAMES_COMBINATION = rf"(\s*\[{_REGEX_FIELD_NAMES}\]\s*)"
323 | _REGEX_FIELD_LEFT = rf"({_REGEX_FIELD_NAME}|{_REGEX_FIELD_NAMES_COMBINATION})"
324 | _REGEX_FIELD_TYPE = r"(\s*[a-zA-Z]\w*\s*)"
325 | _REGEX_FIELD_TYPE_WILDCARD = r"(\s*\*\s*)"
326 | _REGEX_FIELD_SHAPE = r"\[([^\]]+)\]"
327 | _REGEX_FIELD_SHAPE_MAYBE = rf"\s*({_REGEX_FIELD_SHAPE})?\s*"
328 | _REGEX_FIELD_RIGHT = (
329 | rf"({_REGEX_FIELD_TYPE}|{_REGEX_FIELD_TYPE_WILDCARD}){_REGEX_FIELD_SHAPE_MAYBE}"
330 | )
331 | _REGEX_FIELD_TYPE_POINTER = rf"(\s*{_FIELD_TYPE_POINTER}\s*)"
332 | _REGEX_FIELD = (
333 | rf"(\s*{_REGEX_FIELD_LEFT}{_REGEX_FIELD_TYPE_POINTER}{_REGEX_FIELD_RIGHT}\s*)"
334 | )
335 | _REGEX_STRUCTURE_EXPRESSION = (
336 | rf"^({_REGEX_FIELD}"
337 | rf"({_REGEX_SEPARATOR}{_REGEX_FIELD})*"
338 | rf"({_REGEX_SEPARATOR}{_REGEX_FIELD_TYPE_WILDCARD})?)$"
339 | )
340 |
--------------------------------------------------------------------------------
/nptyping/typing_.py:
--------------------------------------------------------------------------------
1 | """
2 | MIT License
3 |
4 | Copyright (c) 2023 Ramon Hagenaars
5 |
6 | Permission is hereby granted, free of charge, to any person obtaining a copy
7 | of this software and associated documentation files (the "Software"), to deal
8 | in the Software without restriction, including without limitation the rights
9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | copies of the Software, and to permit persons to whom the Software is
11 | furnished to do so, subject to the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be included in all
14 | copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | SOFTWARE.
23 | """
24 |
25 | try:
26 | from typing import ( # type: ignore[attr-defined,misc] # pylint: disable=unused-import
27 | Literal,
28 | TypeAlias,
29 | TypeGuard,
30 | final,
31 | )
32 | except ImportError: # pragma: no cover
33 | from typing_extensions import ( # type: ignore[attr-defined,misc]
34 | Literal,
35 | TypeAlias,
36 | TypeGuard,
37 | final,
38 | )
39 |
40 | from typing import Tuple, Union
41 |
42 | import numpy as np
43 |
44 | ShapeExpression: TypeAlias = str
45 | StructureExpression: TypeAlias = str
46 | DType: TypeAlias = Union[np.generic, StructureExpression]
47 | ShapeTuple: TypeAlias = Tuple[int, ...]
48 |
49 | Number = np.number
50 | Bool = np.bool_
51 | Bool8 = np.bool8
52 | Obj = np.object_ # Obj is a common abbreviation and should be usable.
53 | Object = np.object_
54 | Object0 = np.object0
55 | Datetime64 = np.datetime64
56 | Integer = np.integer
57 | SignedInteger = np.signedinteger
58 | Int8 = np.int8
59 | Int16 = np.int16
60 | Int32 = np.int32
61 | Int64 = np.int64
62 | Byte = np.byte
63 | Short = np.short
64 | IntC = np.intc
65 | IntP = np.intp
66 | Int0 = np.int0
67 | Int = np.integer # Int should translate to the "generic" int type.
68 | Int_ = np.int_
69 | LongLong = np.longlong
70 | Timedelta64 = np.timedelta64
71 | UnsignedInteger = np.unsignedinteger
72 | UInt8 = np.uint8
73 | UInt16 = np.uint16
74 | UInt32 = np.uint32
75 | UInt64 = np.uint64
76 | UByte = np.ubyte
77 | UShort = np.ushort
78 | UIntC = np.uintc
79 | UIntP = np.uintp
80 | UInt0 = np.uint0
81 | UInt = np.uint
82 | ULongLong = np.ulonglong
83 | Inexact = np.inexact
84 | Floating = np.floating
85 | Float16 = np.float16
86 | Float32 = np.float32
87 | Float64 = np.float64
88 | Half = np.half
89 | Single = np.single
90 | Double = np.double
91 | Float = np.float_
92 | LongDouble = np.longdouble
93 | LongFloat = np.longfloat
94 | ComplexFloating = np.complexfloating
95 | Complex64 = np.complex64
96 | Complex128 = np.complex128
97 | CSingle = np.csingle
98 | SingleComplex = np.singlecomplex
99 | CDouble = np.cdouble
100 | Complex = np.complex_
101 | CFloat = np.cfloat
102 | CLongDouble = np.clongdouble
103 | CLongFloat = np.clongfloat
104 | LongComplex = np.longcomplex
105 | Flexible = np.flexible
106 | Void = np.void
107 | Void0 = np.void0
108 | Character = np.character
109 | Bytes = np.bytes_
110 | Str = np.str_
111 | String = np.string_
112 | Bytes0 = np.bytes0
113 | Unicode = np.unicode_
114 | Str0 = np.str0
115 |
116 | dtypes = [
117 | (Number, "Number"),
118 | (Bool, "Bool"),
119 | (Bool8, "Bool8"),
120 | (Obj, "Obj"),
121 | (Object, "Object"),
122 | (Object0, "Object0"),
123 | (Datetime64, "Datetime64"),
124 | (Integer, "Integer"),
125 | (SignedInteger, "SignedInteger"),
126 | (Int8, "Int8"),
127 | (Int16, "Int16"),
128 | (Int32, "Int32"),
129 | (Int64, "Int64"),
130 | (Byte, "Byte"),
131 | (Short, "Short"),
132 | (IntC, "IntC"),
133 | (IntP, "IntP"),
134 | (Int0, "Int0"),
135 | (Int, "Int"),
136 | (LongLong, "LongLong"),
137 | (Timedelta64, "Timedelta64"),
138 | (UnsignedInteger, "UnsignedInteger"),
139 | (UInt8, "UInt8"),
140 | (UInt16, "UInt16"),
141 | (UInt32, "UInt32"),
142 | (UInt64, "UInt64"),
143 | (UByte, "UByte"),
144 | (UShort, "UShort"),
145 | (UIntC, "UIntC"),
146 | (UIntP, "UIntP"),
147 | (UInt0, "UInt0"),
148 | (UInt, "UInt"),
149 | (ULongLong, "ULongLong"),
150 | (Inexact, "Inexact"),
151 | (Floating, "Floating"),
152 | (Float16, "Float16"),
153 | (Float32, "Float32"),
154 | (Float64, "Float64"),
155 | (Half, "Half"),
156 | (Single, "Single"),
157 | (Double, "Double"),
158 | (Float, "Float"),
159 | (LongDouble, "LongDouble"),
160 | (LongFloat, "LongFloat"),
161 | (ComplexFloating, "ComplexFloating"),
162 | (Complex64, "Complex64"),
163 | (Complex128, "Complex128"),
164 | (CSingle, "CSingle"),
165 | (SingleComplex, "SingleComplex"),
166 | (CDouble, "CDouble"),
167 | (Complex, "Complex"),
168 | (CFloat, "CFloat"),
169 | (CLongDouble, "CLongDouble"),
170 | (CLongFloat, "CLongFloat"),
171 | (LongComplex, "LongComplex"),
172 | (Flexible, "Flexible"),
173 | (Void, "Void"),
174 | (Void0, "Void0"),
175 | (Character, "Character"),
176 | (Bytes, "Bytes"),
177 | (String, "String"),
178 | (Str, "Str"),
179 | (Bytes0, "Bytes0"),
180 | (Unicode, "Unicode"),
181 | (Str0, "Str0"),
182 | ]
183 |
184 | name_per_dtype = dict(dtypes)
185 | dtype_per_name = {name: dtype for dtype, name in dtypes}
186 |
--------------------------------------------------------------------------------
/nptyping/typing_.pyi:
--------------------------------------------------------------------------------
1 | """
2 | MIT License
3 |
4 | Copyright (c) 2023 Ramon Hagenaars
5 |
6 | Permission is hereby granted, free of charge, to any person obtaining a copy
7 | of this software and associated documentation files (the "Software"), to deal
8 | in the Software without restriction, including without limitation the rights
9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | copies of the Software, and to permit persons to whom the Software is
11 | furnished to do so, subject to the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be included in all
14 | copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | SOFTWARE.
23 | """
24 |
25 | try:
26 | from typing import ( # type: ignore[attr-defined] # pylint: disable=unused-import
27 | Dict,
28 | TypeAlias,
29 | )
30 | except ImportError: # pragma: no cover
31 | from typing_extensions import (
32 | TypeAlias,
33 | )
34 |
35 | from typing import (
36 | Any,
37 | Tuple,
38 | Union,
39 | )
40 |
41 | import numpy as np
42 |
43 | ShapeExpression: TypeAlias = str
44 | StructureExpression: TypeAlias = str
45 | DType: TypeAlias = Union[np.generic, StructureExpression]
46 | ShapeTuple: TypeAlias = Tuple[int, ...]
47 |
48 | Number: TypeAlias = np.dtype[np.number[Any]]
49 | Bool: TypeAlias = np.dtype[np.bool_]
50 | Bool8: TypeAlias = np.dtype[np.bool8]
51 | Object: TypeAlias = np.dtype[np.object_]
52 | Object0: TypeAlias = np.dtype[np.object0]
53 | Datetime64: TypeAlias = np.dtype[np.datetime64]
54 | Integer: TypeAlias = np.dtype[np.integer[Any]]
55 | SignedInteger: TypeAlias = np.dtype[np.signedinteger[Any]]
56 | Int8: TypeAlias = np.dtype[np.int8]
57 | Int16: TypeAlias = np.dtype[np.int16]
58 | Int32: TypeAlias = np.dtype[np.int32]
59 | Int64: TypeAlias = np.dtype[np.int64]
60 | Byte: TypeAlias = np.dtype[np.byte]
61 | Short: TypeAlias = np.dtype[np.short]
62 | IntC: TypeAlias = np.dtype[np.intc]
63 | IntP: TypeAlias = np.dtype[np.intp]
64 | Int0: TypeAlias = np.dtype[np.int0]
65 | Int: TypeAlias = np.dtype[np.int_]
66 | LongLong: TypeAlias = np.dtype[np.longlong]
67 | Timedelta64: TypeAlias = np.dtype[np.timedelta64]
68 | UnsignedInteger: TypeAlias = np.dtype[np.unsignedinteger[Any]]
69 | UInt8: TypeAlias = np.dtype[np.uint8]
70 | UInt16: TypeAlias = np.dtype[np.uint16]
71 | UInt32: TypeAlias = np.dtype[np.uint32]
72 | UInt64: TypeAlias = np.dtype[np.uint64]
73 | UByte: TypeAlias = np.dtype[np.ubyte]
74 | UShort: TypeAlias = np.dtype[np.ushort]
75 | UIntC: TypeAlias = np.dtype[np.uintc]
76 | UIntP: TypeAlias = np.dtype[np.uintp]
77 | UInt0: TypeAlias = np.dtype[np.uint0]
78 | UInt: TypeAlias = np.dtype[np.uint]
79 | ULongLong: TypeAlias = np.dtype[np.ulonglong]
80 | Inexact: TypeAlias = np.dtype[np.inexact[Any]]
81 | Floating: TypeAlias = np.dtype[np.floating[Any]]
82 | Float16: TypeAlias = np.dtype[np.float16]
83 | Float32: TypeAlias = np.dtype[np.float32]
84 | Float64: TypeAlias = np.dtype[np.float64]
85 | Half: TypeAlias = np.dtype[np.half]
86 | Single: TypeAlias = np.dtype[np.single]
87 | Double: TypeAlias = np.dtype[np.double]
88 | Float: TypeAlias = np.dtype[np.float_]
89 | LongDouble: TypeAlias = np.dtype[np.longdouble]
90 | LongFloat: TypeAlias = np.dtype[np.longfloat]
91 | ComplexFloating: TypeAlias = np.dtype[np.complexfloating[Any, Any]]
92 | Complex64: TypeAlias = np.dtype[np.complex64]
93 | Complex128: TypeAlias = np.dtype[np.complex128]
94 | CSingle: TypeAlias = np.dtype[np.csingle]
95 | SingleComplex: TypeAlias = np.dtype[np.singlecomplex]
96 | CDouble: TypeAlias = np.dtype[np.cdouble]
97 | Complex: TypeAlias = np.dtype[np.complex_]
98 | CFloat: TypeAlias = np.dtype[np.cfloat]
99 | CLongDouble: TypeAlias = np.dtype[np.clongdouble]
100 | CLongFloat: TypeAlias = np.dtype[np.clongfloat]
101 | LongComplex: TypeAlias = np.dtype[np.longcomplex]
102 | Flexible: TypeAlias = np.dtype[np.flexible]
103 | Void: TypeAlias = np.dtype[np.void]
104 | Void0: TypeAlias = np.dtype[np.void0]
105 | Character: TypeAlias = np.dtype[np.character]
106 | Bytes: TypeAlias = np.dtype[np.bytes_]
107 | Str: TypeAlias = np.dtype[np.str_]
108 | String: TypeAlias = np.dtype[np.string_]
109 | Bytes0: TypeAlias = np.dtype[np.bytes0]
110 | Unicode: TypeAlias = np.dtype[np.unicode_]
111 | Str0: TypeAlias = np.dtype[np.str0]
112 |
113 | dtype_per_name: Dict[str, np.dtype[Any]]
114 | name_per_dtype: Dict[np.dtype[Any], str]
115 |
--------------------------------------------------------------------------------
/resources/logo.pdn:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ramonhagenaars/nptyping/785cd07e65f992f47256398fd01f62067928d29c/resources/logo.pdn
--------------------------------------------------------------------------------
/resources/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ramonhagenaars/nptyping/785cd07e65f992f47256398fd01f62067928d29c/resources/logo.png
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 | [pylint]
2 | disable =
3 | too-few-public-methods,
4 | no-value-for-parameter,
5 | duplicate-code,
6 | too-many-ancestors,
7 | cyclic-import,
8 | invalid-name,
9 |
10 | [pycodestyle]
11 | max-line-length = 88
12 |
13 | [coverage:run]
14 | include = nptyping/*
15 | parallel = True
16 |
17 | [coverage:report]
18 | fail_under = 100
19 | show_missing = True
20 |
21 | [isort]
22 | multi_line_output = 3
23 | include_trailing_comma = True
24 | force_grid_wrap = 3
25 | use_parentheses = True
26 | line_length = 88
27 |
28 | [mypy]
29 | show_error_codes = True
30 | strict = True
31 | implicit_reexport = True
32 | warn_unused_ignores = False
33 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | import sys
2 | from pathlib import Path
3 |
4 | from setuptools import find_packages, setup
5 |
6 | project_slug = "nptyping"
7 | here = Path(__file__).parent.absolute()
8 |
9 |
10 | def _get_dependencies(dependency_file):
11 | with open(here / "dependencies" / dependency_file, mode="r", encoding="utf-8") as f:
12 | return f.read().strip().split("\n")
13 |
14 |
15 | # Read meta info from package_info.py.
16 | package_info = {}
17 | with open(here / project_slug / "package_info.py", mode="r", encoding="utf-8") as f:
18 | exec(f.read(), package_info)
19 | supp_versions = package_info["__python_versions__"]
20 |
21 | # The README.md provides the long description text.
22 | with open("README.md", mode="r", encoding="utf-8") as f:
23 | long_description = f.read()
24 |
25 | # Check the current version against the supported versions: older versions are not supported.
26 | u_major = sys.version_info.major
27 | u_minor = sys.version_info.minor
28 | versions_as_ints = [[int(v) for v in version.split(".")] for version in supp_versions]
29 | version_unsupported = not [
30 | 1 for major, minor in versions_as_ints if u_major == major and u_minor >= minor
31 | ]
32 | if version_unsupported:
33 | supported_versions_str = ", ".join(version for version in supp_versions)
34 | raise Exception(
35 | f"Unsupported Python version: {sys.version}. Supported versions: {supported_versions_str}"
36 | )
37 |
38 |
39 | extras = {
40 | "build": _get_dependencies("build-requirements.txt"),
41 | "qa": _get_dependencies("qa-requirements.txt"),
42 | "pandas": _get_dependencies("pandas-requirements.txt"),
43 | }
44 | # Complete: all extras for end users, excluding dev dependencies.
45 | extras["complete"] = [
46 | req for key, reqs in extras.items() for req in reqs if key not in ("build", "qa")
47 | ]
48 | # Dev: all extras for developers, including build and qa dependencies.
49 | extras["dev"] = [req for key, reqs in extras.items() for req in reqs]
50 |
51 |
52 | setup(
53 | name=package_info["__title__"],
54 | version=package_info["__version__"],
55 | author=package_info["__author__"],
56 | author_email=package_info["__author_email__"],
57 | description=package_info["__description__"],
58 | url=package_info["__url__"],
59 | long_description=long_description,
60 | long_description_content_type="text/markdown",
61 | license=package_info["__license__"],
62 | package_data={
63 | "": ["*.pyi", "py.typed"],
64 | },
65 | packages=find_packages(include=("nptyping", "nptyping.*")),
66 | install_requires=_get_dependencies("requirements.txt"),
67 | extras_require=extras,
68 | python_requires=f">={supp_versions[0]}",
69 | test_suite="tests",
70 | zip_safe=False,
71 | classifiers=[
72 | "Intended Audience :: Developers",
73 | "License :: OSI Approved :: MIT License",
74 | "Operating System :: OS Independent",
75 | "Natural Language :: English",
76 | "Programming Language :: Python",
77 | "Programming Language :: Python :: 3",
78 | *[f"Programming Language :: Python :: {version}" for version in supp_versions],
79 | ],
80 | )
81 |
--------------------------------------------------------------------------------
/tasks.py:
--------------------------------------------------------------------------------
1 | """
2 | MIT License
3 |
4 | Copyright (c) 2022 Ramon Hagenaars
5 |
6 | Permission is hereby granted, free of charge, to any person obtaining a copy
7 | of this software and associated documentation files (the "Software"), to deal
8 | in the Software without restriction, including without limitation the rights
9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | copies of the Software, and to permit persons to whom the Software is
11 | furnished to do so, subject to the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be included in all
14 | copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | SOFTWARE.
23 | """
24 | import os
25 | import shutil
26 | import sys
27 | import venv as venv_
28 | from glob import glob
29 | from pathlib import Path
30 |
31 | import invoke.tasks as invoke_tasks
32 | from invoke import task
33 |
34 | _ROOT = "nptyping"
35 | _PY_VERSION_STR = (
36 | f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}"
37 | )
38 | _DEFAULT_VENV = f".venv{_PY_VERSION_STR}"
39 |
40 | if sys.version_info.minor >= 11:
41 | # Patch invoke to replace a deprecated inspect function.
42 | # FIXME: https://github.com/pyinvoke/invoke/pull/877
43 | invoke_tasks.inspect.getargspec = invoke_tasks.inspect.getfullargspec
44 |
45 |
46 | if os.name == "nt":
47 | _PY_SUFFIX = "\\Scripts\\python.exe"
48 | _PIP_SUFFIX = "\\Scripts\\pip.exe"
49 | else:
50 | _PY_SUFFIX = "/bin/python"
51 | _PIP_SUFFIX = "/bin/pip"
52 |
53 |
54 | def get_venv(py=None):
55 | return f".venv{py}" if py else _DEFAULT_VENV
56 |
57 |
58 | def get_constraints(py=None):
59 | if py is not None:
60 | # Skip the patch version.
61 | py = ".".join(py.split(".")[:2])
62 |
63 | return f"constraints-{py}.txt" if py else "constraints.txt"
64 |
65 |
66 | def get_py(py=None):
67 | return f"{get_venv(py)}{_PY_SUFFIX}"
68 |
69 |
70 | def get_pip(py=None):
71 | return f"{get_venv(py)}{_PIP_SUFFIX}"
72 |
73 |
74 | def get_versions(py=None):
75 | if py:
76 | py_versions = [py]
77 | else:
78 | py_versions = sorted(
79 | venv_path.split(".venv")[1]
80 | for venv_path in glob(str(Path(__file__).parent / ".venv*"))
81 | )
82 | py_versions.sort(key=lambda version: int(version.replace(".", "")))
83 | return py_versions
84 |
85 |
86 | def print_header(version, function):
87 | print()
88 | print(f"[ {version} - {function.__name__} ]")
89 |
90 |
91 | # BUILD TOOLS
92 |
93 |
94 | @task
95 | def run(context, command, py=None):
96 | """Run the given command using the venv."""
97 | context.run(f"{get_py(py)} {command}")
98 |
99 |
100 | @task
101 | def destroy(context, py=None):
102 | """Destroy the generated virtual environment."""
103 | venv_to_destroy = get_venv(py)
104 | print(f"Destroying {venv_to_destroy}")
105 | shutil.rmtree(venv_to_destroy, ignore_errors=True)
106 |
107 |
108 | @task
109 | def clean(context, py=None):
110 | """Clean up all generated stuff."""
111 | print("Swiping clean the project")
112 | try:
113 | os.remove(".coverage")
114 | except FileNotFoundError:
115 | ... # No problem at all.
116 | shutil.rmtree(f"{_ROOT}.egg-info", ignore_errors=True)
117 | shutil.rmtree("dist", ignore_errors=True)
118 | shutil.rmtree("build", ignore_errors=True)
119 | shutil.rmtree(".mypy_cache", ignore_errors=True)
120 | shutil.rmtree(".pytest_cache", ignore_errors=True)
121 | shutil.rmtree("__pycache__", ignore_errors=True)
122 |
123 |
124 | @task
125 | def venv(context, py=None):
126 | """Create a new virtual environment and install all build dependencies in
127 | it."""
128 | print(f"Creating virtual environment: {_DEFAULT_VENV}")
129 | venv_.create(_DEFAULT_VENV, with_pip=True)
130 | print("Upgrading pip")
131 | context.run(f"{get_py(py)} -m pip install --upgrade pip")
132 | context.run(f"{get_pip(py)} install -r ./dependencies/build-requirements.txt")
133 |
134 |
135 | @task
136 | def lock(context, py=None):
137 | """Lock the project dependencies in a constraints file."""
138 | for version in get_versions(py):
139 | print_header(version, lock)
140 | context.run(
141 | f"{get_py(version)} -m piptools compile ./dependencies/* --output-file {get_constraints(version)} --quiet"
142 | )
143 |
144 |
145 | @task
146 | def install(context, py=None):
147 | """Install all dependencies (dev)."""
148 | for version in get_versions(py):
149 | print_header(version, install)
150 | print(f"Upgrading pip")
151 | context.run(f"{get_py(version)} -m pip install --upgrade pip")
152 | print(f"Installing dependencies into: {version}")
153 | print(
154 | f"{get_pip(version)} install .[dev] --constraint {get_constraints(version)}"
155 | )
156 | context.run(
157 | f"{get_pip(version)} install .[dev] --constraint {get_constraints(version)}"
158 | )
159 |
160 |
161 | @task(clean, venv, lock, install)
162 | def init(context, py=None):
163 | """Initialize a new dev setup."""
164 |
165 |
166 | @task
167 | def wheel(context, py=None):
168 | """Build a wheel."""
169 | print(f"Installing dependencies into: {_DEFAULT_VENV}")
170 | context.run(f"{get_py(py)} setup.py sdist")
171 | context.run(f"{get_pip(py)} wheel . --wheel-dir dist --no-deps")
172 |
173 |
174 | # QA TOOLS
175 |
176 |
177 | @task
178 | def test(context, py=None):
179 | """Run the tests."""
180 | for version in get_versions(py):
181 | print_header(version, test)
182 | context.run(f"{get_py(version)} -m unittest discover tests")
183 |
184 |
185 | @task
186 | def doctest(context, py=None, verbose=False):
187 | """Run the doctests."""
188 | # Check the README.
189 | context.run(f"{get_py(py)} -m doctest README.md")
190 | context.run(f"{get_py(py)} -m doctest USERDOCS.md")
191 |
192 | # And check all the modules.
193 | for filename in glob(f"{_ROOT}/**/*.py", recursive=True):
194 | if verbose:
195 | print(f"doctesting {filename}")
196 | context.run(f"{get_py(py)} -m doctest {filename}")
197 |
198 |
199 | @task
200 | def coverage(context, py=None):
201 | """Run the tests with coverage."""
202 | for version in get_versions(py):
203 | print_header(version, coverage)
204 | context.run(f"{get_py(version)} -m coverage run -m unittest discover tests")
205 | context.run(f"{get_py(py)} -m coverage combine")
206 | context.run(f"{get_py(py)} -m coverage report")
207 |
208 |
209 | @task
210 | def pylint(context, py=None):
211 | """Run pylint for various PEP-8 checks."""
212 | context.run(f"{get_py(py)} -m pylint --rcfile=setup.cfg {_ROOT}")
213 |
214 |
215 | @task
216 | def mypy(context, py=None):
217 | """Run mypy for static type checking."""
218 | context.run(f"{get_py(py)} -m mypy {_ROOT}")
219 |
220 |
221 | @task(doctest, pylint, mypy, coverage)
222 | @task
223 | def qa(context, py=None):
224 | """Run the linting tools."""
225 |
226 |
227 | # FORMATTERS
228 |
229 |
230 | @task
231 | def black(context, check=False, py=None):
232 | """Run Black for formatting."""
233 | cmd = f"{get_py(py)} -m black {_ROOT} setup.py tasks.py tests"
234 | if check:
235 | cmd += " --check"
236 | context.run(cmd)
237 |
238 |
239 | @task
240 | def isort(context, check=False, py=None):
241 | """Run isort for optimizing imports."""
242 | cmd = f"{get_py(py)} -m isort {_ROOT} setup.py tasks.py tests"
243 | if check:
244 | cmd += " --check"
245 | context.run(cmd)
246 |
247 |
248 | @task
249 | def autoflake(context, check=False, py=None):
250 | """Run autoflake to remove unused imports and variables."""
251 | cmd = (
252 | f"{get_py(py)} -m autoflake {_ROOT} setup.py tasks.py tests --recursive --in-place"
253 | f" --remove-unused-variables --remove-all-unused-imports --expand-star-imports"
254 | )
255 | if check:
256 | cmd += " --check"
257 | context.run(cmd)
258 |
259 |
260 | @task
261 | def format(context, check=False, py=None):
262 | """Run the formatters."""
263 | autoflake(context, check=check, py=py)
264 | isort(context, check=check, py=py)
265 | black(context, check=check, py=py)
266 |
--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ramonhagenaars/nptyping/785cd07e65f992f47256398fd01f62067928d29c/tests/__init__.py
--------------------------------------------------------------------------------
/tests/pandas_/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ramonhagenaars/nptyping/785cd07e65f992f47256398fd01f62067928d29c/tests/pandas_/__init__.py
--------------------------------------------------------------------------------
/tests/pandas_/test_dataframe.py:
--------------------------------------------------------------------------------
1 | from typing import Any
2 | from unittest import TestCase
3 |
4 | import pandas as pd
5 |
6 | from nptyping import DataFrame, InvalidArgumentsError
7 | from nptyping import Structure as S
8 | from nptyping.typing_ import Literal as L
9 |
10 |
11 | class DataframeTest(TestCase):
12 | def test_isinstance_success(self):
13 | df = pd.DataFrame(
14 | {
15 | "x": [1, 2, 3],
16 | "y": [2.0, 3.0, 4.0],
17 | "z": ["a", "b", "c"],
18 | }
19 | )
20 |
21 | self.assertIsInstance(df, DataFrame[S["x: Int, y: Float, z: Obj"]])
22 |
23 | def test_isinstance_any(self):
24 | df = pd.DataFrame(
25 | {
26 | "x": [1, 2, 3],
27 | "y": [2.0, 3.0, 4.0],
28 | "z": ["a", "b", "c"],
29 | }
30 | )
31 |
32 | self.assertIsInstance(df, DataFrame[Any])
33 |
34 | def test_isinstance_fail(self):
35 | df = pd.DataFrame(
36 | {
37 | "x": [1, 2, 3],
38 | "y": [2.0, 3.0, 4.0],
39 | "z": ["a", "b", "c"],
40 | }
41 | )
42 |
43 | self.assertNotIsInstance(df, DataFrame[S["x: Float, y: Int, z: Obj"]])
44 |
45 | def test_string_is_aliased(self):
46 | df = pd.DataFrame(
47 | {
48 | "x": ["a", "b", "c"],
49 | "y": ["d", "e", "f"],
50 | }
51 | )
52 |
53 | self.assertIsInstance(df, DataFrame[S["x: Str, y: String"]])
54 |
55 | def test_isinstance_fail_with_random_type(self):
56 | self.assertNotIsInstance(42, DataFrame[S["x: Float, y: Int, z: Obj"]])
57 |
58 | def test_literal_is_allowed(self):
59 | DataFrame[L["x: Int, y: Int"]]
60 |
61 | def test_string_is_not_allowed(self):
62 | with self.assertRaises(InvalidArgumentsError):
63 | DataFrame["x: Int, y: Int"]
64 |
65 | def test_repr(self):
66 | self.assertEqual(
67 | "DataFrame[Structure['[x, y]: Int']]", repr(DataFrame[S["x: Int, y: Int"]])
68 | )
69 | self.assertEqual("DataFrame[Any]", repr(DataFrame))
70 | self.assertEqual("DataFrame[Any]", repr(DataFrame[Any]))
71 |
72 | def test_str(self):
73 | self.assertEqual("DataFrame[[x, y]: Int]", str(DataFrame[S["x: Int, y: Int"]]))
74 | self.assertEqual("DataFrame[Any]", str(DataFrame))
75 | self.assertEqual("DataFrame[Any]", str(DataFrame[Any]))
76 |
--------------------------------------------------------------------------------
/tests/pandas_/test_fork_sync.py:
--------------------------------------------------------------------------------
1 | import os
2 | from unittest import TestCase, skipIf
3 | from urllib.request import Request, urlopen
4 |
5 |
6 | class ForkSyncTest(TestCase):
7 | @skipIf(os.environ.get("CI"), reason="Only run locally")
8 | def test_pandas_stubs_fork_is_synchronized(self):
9 | url = "https://github.com/ramonhagenaars/pandas-stubs/tree/main"
10 | httprequest = Request(url, headers={"Accept": "text/html"})
11 |
12 | with urlopen(httprequest) as response:
13 | payload = response.read().decode()
14 | out_of_sync = "commits behind" in payload
15 |
16 | self.assertFalse(out_of_sync, "The pandas-stubs fork needs to be synchronized")
17 |
--------------------------------------------------------------------------------
/tests/pandas_/test_mypy_dataframe.py:
--------------------------------------------------------------------------------
1 | import sys
2 | from unittest import TestCase, skipUnless
3 |
4 | from tests.test_helpers.check_mypy_on_code import check_mypy_on_code
5 |
6 |
7 | class MyPyDataFrameTest(TestCase):
8 | @skipUnless(7 < sys.version_info.minor, "MyPy does not work with DataFrame on 3.7")
9 | def test_mypy_accepts_dataframe(self):
10 | exit_code, stdout, stderr = check_mypy_on_code(
11 | """
12 | from nptyping import DataFrame, Structure as S
13 | import pandas as pd
14 |
15 |
16 | df: DataFrame[S["x: Int, y: Int"]] = pd.DataFrame({"x": [1], "y": [1]})
17 | """
18 | )
19 | self.assertEqual(0, exit_code, stdout)
20 |
21 | @skipUnless(7 < sys.version_info.minor, "MyPy does not work with DataFrame on 3.7")
22 | def test_mypy_disapproves_dataframe_with_wrong_function_arguments(self):
23 | exit_code, stdout, stderr = check_mypy_on_code(
24 | """
25 | from typing import Any
26 | import numpy as np
27 | from nptyping import DataFrame, Structure as S
28 |
29 |
30 | def func(_: DataFrame[S["x: Float, y: Float"]]) -> None:
31 | ...
32 |
33 |
34 | func("Not an array...")
35 | """
36 | )
37 |
38 | self.assertIn('Argument 1 to "func" has incompatible type "str"', stdout)
39 | self.assertIn('expected "DataFrame[Any]"', stdout)
40 | self.assertIn("Found 1 error in 1 file", stdout)
41 |
42 | @skipUnless(7 < sys.version_info.minor, "MyPy does not work with DataFrame on 3.7")
43 | def test_mypy_knows_of_dataframe_methods(self):
44 | # If MyPy knows of some arbitrary DataFrame methods, we can assume that
45 | # code completion works.
46 | exit_code, stdout, stderr = check_mypy_on_code(
47 | """
48 | from typing import Any
49 | from nptyping import DataFrame
50 |
51 |
52 | df: DataFrame[Any]
53 | df.shape
54 | df.dtypes
55 | df.values
56 | df.boxplot
57 | df.filter
58 | """
59 | )
60 |
61 | self.assertEqual(0, exit_code, stdout)
62 |
--------------------------------------------------------------------------------
/tests/test_assert_isinstance.py:
--------------------------------------------------------------------------------
1 | from unittest import TestCase
2 |
3 | from nptyping import assert_isinstance
4 |
5 |
6 | class AssertInstanceTest(TestCase):
7 | def test_assert_isinstance_true(self):
8 | assert_isinstance(1, int)
9 |
10 | def test_assert_isinstance_false(self):
11 | with self.assertRaises(AssertionError) as err:
12 | assert_isinstance(1, str)
13 | self.assertIn("instance=1, cls=", str(err.exception))
14 |
15 | def test_assert_isinstance_false_with_message(self):
16 | with self.assertRaises(AssertionError) as err:
17 | assert_isinstance(1, str, "That's no string")
18 | self.assertIn("That's no string", str(err.exception))
19 |
--------------------------------------------------------------------------------
/tests/test_base_meta_classes.py:
--------------------------------------------------------------------------------
1 | from typing import Any, Tuple
2 | from unittest import TestCase
3 |
4 | from nptyping.base_meta_classes import (
5 | ComparableByArgsMeta,
6 | ContainerMeta,
7 | FinalMeta,
8 | ImmutableMeta,
9 | InconstructableMeta,
10 | MaybeCheckableMeta,
11 | SubscriptableMeta,
12 | )
13 | from nptyping.error import NPTypingError
14 |
15 |
16 | class SubscriptableMetaTest(TestCase):
17 | def test_subscriptable_meta(self):
18 | class CMeta(SubscriptableMeta):
19 | def _get_item(cls, item: Any) -> Tuple[Any, ...]:
20 | return (item,)
21 |
22 | class C(metaclass=CMeta):
23 | __args__ = tuple()
24 |
25 | C42 = C[42]
26 |
27 | self.assertEqual((42,), C42.__args__)
28 | self.assertIs(C[42], C42)
29 |
30 | def test_final_meta(self):
31 | class CMeta(FinalMeta, implementation="C"):
32 | ...
33 |
34 | class C(metaclass=CMeta):
35 | __args__ = tuple()
36 |
37 | with self.assertRaises(NPTypingError) as err:
38 |
39 | class C2(C):
40 | ...
41 |
42 | self.assertEqual("Cannot subclass nptyping.C.", str(err.exception))
43 |
44 | def test_inconstructable(self):
45 | class CMeta(InconstructableMeta):
46 | ...
47 |
48 | class C(metaclass=CMeta):
49 | __args__ = tuple()
50 |
51 | with self.assertRaises(NPTypingError) as err:
52 | C()
53 |
54 | self.assertIn("Cannot instantiate nptyping.C.", str(err.exception))
55 |
56 | def test_immutable(self):
57 | class CMeta(ImmutableMeta):
58 | ...
59 |
60 | class C(metaclass=CMeta):
61 | __args__ = tuple()
62 |
63 | with self.assertRaises(NPTypingError) as err:
64 | C.some_attr = 42
65 |
66 | self.assertEqual("Cannot set values to nptyping.C.", str(err.exception))
67 |
68 | def test_subscriptable_cannot_parameterize_twice(self):
69 | class CMeta(SubscriptableMeta):
70 | def __str__(self) -> str:
71 | return "SomeName"
72 |
73 | class C(metaclass=CMeta):
74 | __args__ = tuple()
75 |
76 | with self.assertRaises(NPTypingError) as err:
77 | C[42][42]
78 |
79 | self.assertEqual(
80 | f"Type nptyping.SomeName is already parameterized.", str(err.exception)
81 | )
82 |
83 | def test_comparable_by_args_meta(self):
84 | class C1(metaclass=ComparableByArgsMeta):
85 | __args__ = (42, 42)
86 |
87 | class C2(metaclass=ComparableByArgsMeta):
88 | __args__ = (42, 42)
89 |
90 | class C3(metaclass=ComparableByArgsMeta):
91 | __args__ = (42, 42, 42)
92 |
93 | self.assertEqual(C1, C2)
94 | self.assertEqual(hash(C1), hash(C2))
95 | self.assertNotEqual(C1, C3)
96 | self.assertNotEqual(hash(C1), hash(C3))
97 |
98 | def test_maybe_checkable_instance_checking_is_disabled_by_default(self):
99 | class CMeta(MaybeCheckableMeta):
100 | ...
101 |
102 | class C(metaclass=CMeta):
103 | ...
104 |
105 | with self.assertRaises(NPTypingError) as err:
106 | isinstance(42, C)
107 |
108 | self.assertEqual(
109 | "Instance checking is not supported for nptyping.C.", str(err.exception)
110 | )
111 |
112 | def test_maybe_checkable_subclass_checking_is_disabled_by_default(self):
113 | class CMeta(MaybeCheckableMeta):
114 | ...
115 |
116 | class C(metaclass=CMeta):
117 | ...
118 |
119 | with self.assertRaises(NPTypingError) as err:
120 | issubclass(int, C)
121 |
122 | self.assertEqual(
123 | "Subclass checking is not supported for nptyping.C.", str(err.exception)
124 | )
125 |
126 | def test_container_meta(self):
127 | class TestContainerMeta(ContainerMeta, implementation="TestContainer"):
128 | def _normalize_expression(cls, item: str) -> str:
129 | return item.lower()
130 |
131 | def _validate_expression(cls, item: str) -> None:
132 | if item == "forbidden":
133 | raise NPTypingError("That item is forbidden.")
134 |
135 | class TestContainer(metaclass=TestContainerMeta):
136 | __args__ = (42,)
137 |
138 | self.assertEqual((42,), TestContainer.__args__)
139 | self.assertEqual(("test",), TestContainer["test"].__args__)
140 | self.assertIs(TestContainer["test"], TestContainer["test"])
141 | self.assertIs(TestContainer["test"], TestContainer["TeSt"])
142 |
143 | with self.assertRaises(NPTypingError):
144 | TestContainer["forbidden"]
145 |
146 | self.assertFalse(issubclass(int, TestContainer))
147 |
--------------------------------------------------------------------------------
/tests/test_beartype.py:
--------------------------------------------------------------------------------
1 | from unittest import TestCase
2 |
3 | import numpy as np
4 | from beartype import beartype
5 |
6 | from nptyping import (
7 | Float,
8 | NDArray,
9 | Shape,
10 | )
11 |
12 |
13 | @beartype
14 | def fun(_: NDArray[Shape["2, 2"], Float]) -> None:
15 | ...
16 |
17 |
18 | class BeartTypeTest(TestCase):
19 | def test_trivial_fail(self):
20 | with self.assertRaises(Exception):
21 | fun(42)
22 |
23 | def test_success(self):
24 | fun(np.random.randn(2, 2))
25 |
26 | def test_fail_shape(self):
27 | with self.assertRaises(Exception):
28 | fun(np.random.randn(3, 2))
29 |
30 | def test_fail_dtype(self):
31 | with self.assertRaises(Exception):
32 | fun(np.random.randn(2, 2).astype(int))
33 |
--------------------------------------------------------------------------------
/tests/test_help_texts.py:
--------------------------------------------------------------------------------
1 | import pydoc
2 | from unittest import TestCase
3 |
4 | from nptyping import (
5 | DataFrame,
6 | Int,
7 | NDArray,
8 | RecArray,
9 | Shape,
10 | Structure,
11 | )
12 |
13 |
14 | class HelpTextsTest(TestCase):
15 | def test_help_ndarray(self):
16 | def func(arr: NDArray[Shape["2, 2"], Int]):
17 | ...
18 |
19 | help_text = pydoc.render_doc(func)
20 |
21 | self.assertIn("arr: NDArray[Shape['2, 2'], Int]", help_text)
22 | self.assertEqual("nptyping.ndarray", NDArray.__module__)
23 |
24 | def test_help_recdarray(self):
25 | def func(arr: RecArray[Shape["2, 2"], Structure["[x, y]: Float"]]):
26 | ...
27 |
28 | help_text = pydoc.render_doc(func)
29 |
30 | self.assertIn(
31 | "arr: RecArray[Shape['2, 2'], Structure['[x, y]: Float']]", help_text
32 | )
33 | self.assertEqual("nptyping.recarray", RecArray.__module__)
34 |
35 | def test_help_dataframe(self):
36 | def func(df: DataFrame[Structure["[x, y]: Float"]]):
37 | ...
38 |
39 | help_text = pydoc.render_doc(func)
40 |
41 | self.assertIn("df: DataFrame[Structure['[x, y]: Float']]", help_text)
42 | self.assertEqual("nptyping.pandas_.dataframe", DataFrame.__module__)
43 |
--------------------------------------------------------------------------------
/tests/test_helpers/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ramonhagenaars/nptyping/785cd07e65f992f47256398fd01f62067928d29c/tests/test_helpers/__init__.py
--------------------------------------------------------------------------------
/tests/test_helpers/check_mypy_on_code.py:
--------------------------------------------------------------------------------
1 | from typing import Tuple
2 |
3 | from mypy import api
4 |
5 | from tests.test_helpers.temp_file import temp_file
6 |
7 |
8 | def check_mypy_on_code(python_code: str) -> Tuple[int, str, str]:
9 | with temp_file(python_code) as path_to_file:
10 | stdout, stderr, exit_code = api.run([str(path_to_file)])
11 | return exit_code, stdout, stderr
12 |
--------------------------------------------------------------------------------
/tests/test_helpers/temp_file.py:
--------------------------------------------------------------------------------
1 | import contextlib
2 | import os
3 | from pathlib import Path
4 | from tempfile import TemporaryDirectory
5 | from textwrap import dedent
6 |
7 |
8 | @contextlib.contextmanager
9 | def temp_file(python_code: str, file_name: str = "test_file.py"):
10 | file_content = dedent(python_code).strip() + os.linesep
11 | with TemporaryDirectory() as directory_name:
12 | path_to_file = Path(directory_name) / file_name
13 | with open(path_to_file, "w") as file:
14 | file.write(file_content)
15 | yield path_to_file
16 |
--------------------------------------------------------------------------------
/tests/test_lib_export.py:
--------------------------------------------------------------------------------
1 | from unittest import TestCase
2 |
3 | from nptyping import __all__
4 |
5 |
6 | class LibExportTest(TestCase):
7 | def test_all(self):
8 | expected_exports = {
9 | "NDArray",
10 | "RecArray",
11 | "assert_isinstance",
12 | "validate_shape_expression",
13 | "normalize_shape_expression",
14 | "NPTypingError",
15 | "InvalidDTypeError",
16 | "InvalidShapeError",
17 | "InvalidStructureError",
18 | "InvalidArgumentsError",
19 | "Shape",
20 | "Structure",
21 | "__version__",
22 | "DType",
23 | "Number",
24 | "Bool",
25 | "Bool8",
26 | "Object",
27 | "Object0",
28 | "Datetime64",
29 | "Integer",
30 | "SignedInteger",
31 | "Int8",
32 | "Int16",
33 | "Int32",
34 | "Int64",
35 | "Byte",
36 | "Short",
37 | "IntC",
38 | "IntP",
39 | "Int0",
40 | "Int",
41 | "LongLong",
42 | "Timedelta64",
43 | "UnsignedInteger",
44 | "UInt8",
45 | "UInt16",
46 | "UInt32",
47 | "UInt64",
48 | "UByte",
49 | "UShort",
50 | "UIntC",
51 | "UIntP",
52 | "UInt0",
53 | "UInt",
54 | "ULongLong",
55 | "Inexact",
56 | "Floating",
57 | "Float16",
58 | "Float32",
59 | "Float64",
60 | "Half",
61 | "Single",
62 | "Double",
63 | "Float",
64 | "LongDouble",
65 | "LongFloat",
66 | "ComplexFloating",
67 | "Complex64",
68 | "Complex128",
69 | "CSingle",
70 | "SingleComplex",
71 | "CDouble",
72 | "Complex",
73 | "CFloat",
74 | "CLongDouble",
75 | "CLongFloat",
76 | "LongComplex",
77 | "Flexible",
78 | "Void",
79 | "Void0",
80 | "Character",
81 | "Bytes",
82 | "String",
83 | "Bytes0",
84 | "Unicode",
85 | "Str0",
86 | "DataFrame",
87 | }
88 |
89 | self.assertSetEqual(expected_exports, set(__all__))
90 |
--------------------------------------------------------------------------------
/tests/test_mypy.py:
--------------------------------------------------------------------------------
1 | from unittest import TestCase
2 |
3 | from tests.test_helpers.check_mypy_on_code import check_mypy_on_code
4 |
5 |
6 | class MyPyTest(TestCase):
7 | def test_mypy_accepts_ndarray_with_any(self):
8 | exit_code, stdout, stderr = check_mypy_on_code(
9 | """
10 | from typing import Any
11 | from nptyping import NDArray
12 |
13 |
14 | NDArray[Any, Any]
15 | """
16 | )
17 | self.assertEqual(0, exit_code, stdout)
18 |
19 | def test_mypy_accepts_ndarray_with_shape(self):
20 | exit_code, stdout, stderr = check_mypy_on_code(
21 | """
22 | from typing import Any
23 | from nptyping import NDArray, Shape
24 |
25 |
26 | NDArray[Shape["3, 3"], Any]
27 | """
28 | )
29 |
30 | self.assertEqual(0, exit_code, stdout)
31 |
32 | def test_mypy_accepts_ndarray_with_structure(self):
33 | exit_code, stdout, stderr = check_mypy_on_code(
34 | """
35 | from typing import Any
36 | from nptyping import NDArray, RecArray, Structure
37 |
38 |
39 | NDArray[Any, Structure["x: Float, y: Int"]]
40 | """
41 | )
42 |
43 | self.assertEqual(0, exit_code, stdout)
44 |
45 | def test_mypy_disapproves_ndarray_with_wrong_function_arguments(self):
46 | exit_code, stdout, stderr = check_mypy_on_code(
47 | """
48 | from typing import Any
49 | import numpy as np
50 | from nptyping import NDArray, Shape
51 |
52 |
53 | def func(_: NDArray[Shape["2, 2"], Any]) -> None:
54 | ...
55 |
56 |
57 | func("Not an array...")
58 | """
59 | )
60 |
61 | self.assertIn('Argument 1 to "func" has incompatible type "str"', stdout)
62 | self.assertIn('expected "ndarray[Any, Any]"', stdout)
63 | self.assertIn("Found 1 error in 1 file", stdout)
64 |
65 | def test_mypy_accepts_ndarrays_as_function_arguments(self):
66 | exit_code, stdout, stderr = check_mypy_on_code(
67 | """
68 | from typing import Any
69 | import numpy as np
70 | from nptyping import NDArray, Shape
71 |
72 |
73 | def func(_: NDArray[Shape["2, 2"], Any]) -> None:
74 | ...
75 |
76 |
77 | func(np.array([1, 2])) # (Wrong shape though)
78 | """
79 | )
80 |
81 | self.assertEqual(0, exit_code, stdout)
82 |
83 | def test_mypy_accepts_ndarrays_as_variable_hints(self):
84 | exit_code, stdout, stderr = check_mypy_on_code(
85 | """
86 | from typing import Any
87 | import numpy as np
88 | from nptyping import NDArray
89 |
90 |
91 | arr: NDArray[Any, Any] = np.array([1, 2, 3])
92 | """
93 | )
94 |
95 | self.assertEqual(0, exit_code, stdout)
96 |
97 | def test_mypy_accepts_recarray_with_structure(self):
98 | exit_code, stdout, stderr = check_mypy_on_code(
99 | """
100 | from typing import Any
101 | from nptyping import RecArray, Structure
102 |
103 |
104 | RecArray[Any, Structure["x: Float, y: Int"]]
105 | """
106 | )
107 |
108 | self.assertEqual(0, exit_code, stdout)
109 |
110 | def test_mypy_accepts_numpy_types(self):
111 | exit_code, stdout, stderr = check_mypy_on_code(
112 | """
113 | from typing import Any
114 | from nptyping import NDArray
115 | import numpy as np
116 |
117 |
118 | NDArray[Any, np.dtype[np.int_]]
119 | NDArray[Any, np.dtype[np.float_]]
120 | NDArray[Any, np.dtype[np.uint8]]
121 | NDArray[Any, np.dtype[np.bool_]]
122 | """
123 | )
124 |
125 | self.assertEqual(0, exit_code, stdout)
126 |
127 | def test_mypy_wont_accept_numpy_types_without_dtype(self):
128 | exit_code, stdout, stderr = check_mypy_on_code(
129 | """
130 | from nptyping import NDArray
131 | from typing import Any
132 | import numpy as np
133 |
134 |
135 | NDArray[Any, np.int_]
136 | """
137 | )
138 |
139 | self.assertIn(
140 | 'Value of type variable "_DType_co" of "ndarray" cannot be "signedinteger[Any]"',
141 | stdout,
142 | )
143 |
144 | def test_mypy_knows_of_ndarray_methods(self):
145 | # If MyPy knows of some arbitrary ndarray methods, we can assume that
146 | # code completion works.
147 | exit_code, stdout, stderr = check_mypy_on_code(
148 | """
149 | from typing import Any
150 | from nptyping import NDArray
151 |
152 |
153 | arr: NDArray[Any, Any]
154 | arr.shape
155 | arr.size
156 | arr.sort
157 | arr.squeeze
158 | arr.transpose
159 | """
160 | )
161 |
162 | self.assertEqual(0, exit_code, stdout)
163 |
164 | def test_mypy_accepts_nptyping_types(self):
165 | exit_code, stdout, stderr = check_mypy_on_code(
166 | """
167 | from typing import Any
168 | import numpy as np
169 | import numpy.typing as npt
170 | from nptyping import (
171 | NDArray,
172 | Number,
173 | Bool,
174 | Bool8,
175 | Object,
176 | Object0,
177 | Datetime64,
178 | Integer,
179 | SignedInteger,
180 | Int8,
181 | Int16,
182 | Int32,
183 | Int64,
184 | Byte,
185 | Short,
186 | IntC,
187 | IntP,
188 | Int0,
189 | Int,
190 | LongLong,
191 | Timedelta64,
192 | UnsignedInteger,
193 | UInt8,
194 | UInt16,
195 | UInt32,
196 | UInt64,
197 | UByte,
198 | UShort,
199 | UIntC,
200 | UIntP,
201 | UInt0,
202 | UInt,
203 | ULongLong,
204 | Inexact,
205 | Floating,
206 | Float16,
207 | Float32,
208 | Float64,
209 | Half,
210 | Single,
211 | Double,
212 | Float,
213 | LongDouble,
214 | LongFloat,
215 | ComplexFloating,
216 | Complex64,
217 | Complex128,
218 | CSingle,
219 | SingleComplex,
220 | CDouble,
221 | Complex,
222 | CFloat,
223 | CLongDouble,
224 | CLongFloat,
225 | LongComplex,
226 | Flexible,
227 | Void,
228 | Void0,
229 | Character,
230 | Bytes,
231 | String,
232 | Bytes0,
233 | Unicode,
234 | Str0,
235 | )
236 |
237 | NDArray[Any, Number]
238 | NDArray[Any, Bool]
239 | NDArray[Any, Bool8]
240 | NDArray[Any, Object]
241 | NDArray[Any, Object0]
242 | NDArray[Any, Datetime64]
243 | NDArray[Any, Integer]
244 | NDArray[Any, SignedInteger]
245 | NDArray[Any, Int8]
246 | NDArray[Any, Int16]
247 | NDArray[Any, Int32]
248 | NDArray[Any, Int64]
249 | NDArray[Any, Byte]
250 | NDArray[Any, Short]
251 | NDArray[Any, IntC]
252 | NDArray[Any, IntP]
253 | NDArray[Any, Int0]
254 | NDArray[Any, Int]
255 | NDArray[Any, LongLong]
256 | NDArray[Any, Timedelta64]
257 | NDArray[Any, UnsignedInteger]
258 | NDArray[Any, UInt8]
259 | NDArray[Any, UInt16]
260 | NDArray[Any, UInt32]
261 | NDArray[Any, UInt64]
262 | NDArray[Any, UByte]
263 | NDArray[Any, UShort]
264 | NDArray[Any, UIntC]
265 | NDArray[Any, UIntP]
266 | NDArray[Any, UInt0]
267 | NDArray[Any, UInt]
268 | NDArray[Any, ULongLong]
269 | NDArray[Any, Inexact]
270 | NDArray[Any, Floating]
271 | NDArray[Any, Float16]
272 | NDArray[Any, Float32]
273 | NDArray[Any, Float64]
274 | NDArray[Any, Half]
275 | NDArray[Any, Single]
276 | NDArray[Any, Double]
277 | NDArray[Any, Float]
278 | NDArray[Any, LongDouble]
279 | NDArray[Any, LongFloat]
280 | NDArray[Any, ComplexFloating]
281 | NDArray[Any, Complex64]
282 | NDArray[Any, Complex128]
283 | NDArray[Any, CSingle]
284 | NDArray[Any, SingleComplex]
285 | NDArray[Any, CDouble]
286 | NDArray[Any, Complex]
287 | NDArray[Any, CFloat]
288 | NDArray[Any, CLongDouble]
289 | NDArray[Any, CLongFloat]
290 | NDArray[Any, LongComplex]
291 | NDArray[Any, Flexible]
292 | NDArray[Any, Void]
293 | NDArray[Any, Void0]
294 | NDArray[Any, Character]
295 | NDArray[Any, Bytes]
296 | NDArray[Any, String]
297 | NDArray[Any, Bytes0]
298 | NDArray[Any, Unicode]
299 | NDArray[Any, Str0]
300 | """
301 | )
302 |
303 | self.assertEqual(0, exit_code, stdout)
304 |
--------------------------------------------------------------------------------
/tests/test_ndarray.py:
--------------------------------------------------------------------------------
1 | from typing import Any
2 | from unittest import TestCase
3 |
4 | import numpy as np
5 |
6 | from nptyping import (
7 | Bool,
8 | Float,
9 | Int,
10 | InvalidArgumentsError,
11 | InvalidStructureError,
12 | NDArray,
13 | NPTypingError,
14 | Shape,
15 | Structure,
16 | UInt8,
17 | )
18 | from nptyping.typing_ import Literal
19 |
20 |
21 | class NDArrayTest(TestCase):
22 | def test_isinstance_succeeds_if_shapes_match_exactly(self):
23 |
24 | # Trivial identity checks.
25 | self.assertIs(NDArray, NDArray)
26 | self.assertIs(NDArray[Shape["1, 1"], Any], NDArray[Shape["1, 1"], Any])
27 |
28 | # Tuples should not make any difference.
29 | self.assertIs(NDArray[Shape["1, 1"], Any], NDArray[(Shape["1, 1"], Any)])
30 |
31 | # Whitespaces should not make any difference.
32 | self.assertIs(NDArray[Shape["1,1"], Any], NDArray[(Shape[" 1 , 1 "], Any)])
33 |
34 | # Arguments may point to the default NDArray (any type, any shape).
35 | self.assertIs(NDArray, NDArray[Any, Any])
36 | self.assertIs(NDArray, NDArray[Shape["*, ..."], Any])
37 |
38 | self.assertIsNot(NDArray[Shape["1, 1"], Any], NDArray[Shape["1, 2"], Any])
39 | self.assertIsNot(
40 | NDArray[Shape["1, 1"], np.floating], NDArray[Shape["1, 1"], Any]
41 | )
42 |
43 | def test_isinstance_fails_if_shape_size_dont_match(self):
44 | self.assertNotIsInstance(
45 | np.random.randn(2, 2),
46 | NDArray[Shape["2, 3"], Any],
47 | )
48 |
49 | def test_isinstance_fails_if_nr_of_shapes_dont_match(self):
50 | self.assertNotIsInstance(
51 | np.random.randn(2, 2),
52 | NDArray[Shape["2, 2, 2"], Any],
53 | )
54 | self.assertNotIsInstance(
55 | np.random.randn(2, 2),
56 | NDArray[Shape["2"], Any],
57 | )
58 |
59 | def test_isinstance_succeeds_if_variables_can_be_assigned(self):
60 | self.assertIsInstance(
61 | np.random.randn(3, 2),
62 | NDArray[Shape["Axis1, Axis2"], Any],
63 | )
64 | self.assertIsInstance(
65 | np.random.randn(3, 2),
66 | NDArray[Shape["Axis, 2"], Any],
67 | "Combinations of variables and values should work.",
68 | )
69 | self.assertIsInstance(
70 | np.random.randn(2),
71 | NDArray[Shape["VaR14bLe_"], Any],
72 | "Anything that starts with an uppercase letter is a variable.",
73 | )
74 |
75 | def test_isinstance_fails_if_variables_cannot_be_assigned(self):
76 | self.assertNotIsInstance(
77 | np.random.randn(3, 2),
78 | NDArray[Shape["Axis1, Axis1"], Any],
79 | )
80 |
81 | def test_isinstance_succeeds_with_wildcards(self):
82 | self.assertIsInstance(
83 | np.random.randn(4),
84 | NDArray[Shape["*"], Any],
85 | )
86 | self.assertIsInstance(
87 | np.random.randn(4, 4),
88 | NDArray[Shape["*, *"], Any],
89 | )
90 |
91 | def test_isinstance_succeeds_with_0d_arrays(self):
92 | self.assertIsInstance(
93 | np.array([]),
94 | NDArray[Shape["0"], Any],
95 | )
96 |
97 | def test_isinstance_succeeds_with_ellipsis(self):
98 | self.assertIsInstance(
99 | np.array([[[[[[0]]]]]]),
100 | NDArray[Shape["1, ..."], Any],
101 | "This should match with an array of any dimensions of size 1.",
102 | )
103 | self.assertIsInstance(
104 | np.array([[[[[[0, 0, 0]]]]]]),
105 | NDArray[Shape["*, ..."], Any],
106 | "This should match with an array of any dimensions of any size.",
107 | )
108 | self.assertIsInstance(
109 | np.array([[0]]),
110 | NDArray[Shape["1, 1, ..."], Any],
111 | "This should match with an array of shape (1, 1).",
112 | )
113 | self.assertIsInstance(
114 | np.array([[[[0]]]]),
115 | NDArray[Shape["1, 1, ..."], Any],
116 | "This should match with an array of shape (1, 1, 1, 1).",
117 | )
118 | self.assertIsInstance(
119 | np.array([[[[0, 0], [0, 0]], [[0, 0], [0, 0]]]]),
120 | NDArray[Shape["1, 2, ..."], Any],
121 | )
122 |
123 | def test_isinstance_fails_with_ellipsis(self):
124 | self.assertNotIsInstance(
125 | np.array([[[[[[0, 0]]]]]]),
126 | NDArray[Shape["1, ..."], Any],
127 | "This should match with an array of any dimensions of size 1.",
128 | )
129 | self.assertNotIsInstance(
130 | np.array([[[[[0], [0]], [[0], [0]]], [[[0], [0]], [[0], [0]]]]]),
131 | NDArray[Shape["1, 2, ..."], Any],
132 | )
133 |
134 | def test_isinstance_succeeds_with_dim_breakdown(self):
135 | self.assertIsInstance(
136 | np.random.randn(3, 2),
137 | NDArray[Shape["3, [x, y]"], Any],
138 | )
139 | self.assertIsInstance(
140 | np.random.randn(3, 2),
141 | NDArray[Shape["[obj1, obj2, obj3], [x, y]"], Any],
142 | )
143 |
144 | def test_isinstance_fails_with_dim_breakdown(self):
145 | self.assertNotIsInstance(
146 | np.random.randn(3, 2),
147 | NDArray[Shape["3, [x, y, z]"], Any],
148 | )
149 |
150 | def test_isinstance_succeeds_with_labels(self):
151 | self.assertIsInstance(
152 | np.random.randn(100, 5),
153 | NDArray[Shape["100 assets, [id, age, type, x, y]"], Any],
154 | )
155 | self.assertIsInstance(
156 | np.random.randn(100, 5),
157 | NDArray[Shape["* assets, [id, age, type, x, y]"], Any],
158 | )
159 | self.assertIsInstance(
160 | np.random.randn(100, 5),
161 | NDArray[Shape["N assets, [id, age, type, x, y]"], Any],
162 | )
163 |
164 | def test_isinstance_succeeds_if_structure_match_exactly(self):
165 | arr = np.array([("Pete", 34)], dtype=[("name", "U8"), ("age", "i4")])
166 | self.assertIsInstance(
167 | arr,
168 | NDArray[Any, Structure["name: Str, age: Int32"]],
169 | )
170 |
171 | def test_isinstance_fails_if_structure_doesnt_match(self):
172 | arr = np.array([("Johnny", 34)], dtype=[("name", "U8"), ("age", "i4")])
173 | self.assertNotIsInstance(
174 | arr,
175 | NDArray[Any, Structure["name: Str, age: Float"]],
176 | )
177 |
178 | arr = np.array([("Bill", 34)], dtype=[("name", "U8"), ("age", "i4")])
179 | self.assertNotIsInstance(
180 | arr,
181 | NDArray[Any, Structure["name: String, age: Int32"]],
182 | )
183 |
184 | arr = np.array([("Clair", 34)], dtype=[("name", "U8"), ("age", "i4")])
185 | self.assertNotIsInstance(
186 | arr,
187 | NDArray[Any, Structure["[name, age]: Str"]],
188 | )
189 |
190 | def test_isinstance_succeeds_if_structure_subarray_matches(self):
191 | arr = np.array([("x")], np.dtype([("x", "U10", (2, 2))]))
192 | self.assertIsInstance(arr, NDArray[Any, Structure["x: Str[2, 2]"]])
193 |
194 | def test_isinstance_fails_if_structure_contains_invalid_types(self):
195 | with self.assertRaises(InvalidStructureError) as err:
196 | NDArray[Any, Structure["name: Str, age: Float, address: Address"]]
197 | self.assertIn(
198 | "Type 'Address' is not valid in this context.", str(err.exception)
199 | )
200 |
201 | with self.assertRaises(InvalidStructureError) as err:
202 | NDArray[Any, Literal["x: Float, y: AlsoAFloat"]]
203 | self.assertIn(
204 | "Type 'AlsoAFloat' is not valid in this context.", str(err.exception)
205 | )
206 |
207 | def test_invalid_arguments_raise_errors(self):
208 | with self.assertRaises(InvalidArgumentsError) as err:
209 | NDArray[Shape["1"], Any, "Not good"]
210 | self.assertIn("Not good", str(err.exception))
211 |
212 | with self.assertRaises(InvalidArgumentsError) as err:
213 | NDArray["Not a Shape Expression", Any]
214 | self.assertIn("Not a Shape Expression", str(err.exception))
215 |
216 | with self.assertRaises(InvalidArgumentsError) as err:
217 | NDArray["Not a valid argument"]
218 | self.assertIn("str", str(err.exception))
219 |
220 | with self.assertRaises(InvalidArgumentsError):
221 | NDArray[Any]
222 |
223 | with self.assertRaises(InvalidArgumentsError):
224 | NDArray[Shape["1"]]
225 |
226 | with self.assertRaises(InvalidArgumentsError):
227 | NDArray[UInt8]
228 |
229 | with self.assertRaises(InvalidArgumentsError) as err:
230 | NDArray[Any, "Not a DType"]
231 | self.assertIn("Not a DType", str(err.exception))
232 |
233 | def test_valid_arguments_should_not_raise(self):
234 | NDArray
235 | NDArray[Any, Any]
236 | NDArray[Shape["1"], Any]
237 | NDArray[(Shape["1"], Any)]
238 | NDArray[Literal["1"], Any]
239 | NDArray[Any, Int]
240 | NDArray[Any, Structure["x: Float"]]
241 | NDArray[Any, Structure["x: Float, y: Int"]]
242 | NDArray[Any, Structure["[x, y]: Float, z: Int"]]
243 | NDArray[Any, Literal["[x, y]: Float, z: Int"]]
244 |
245 | def test_str(self):
246 | self.assertEqual("NDArray[Any, Any]", str(NDArray[Any, Any]))
247 | self.assertEqual("NDArray[Any, Any]", str(NDArray[Shape[" * , ... "], Any]))
248 | self.assertEqual(
249 | "NDArray[Shape['2, 2'], Any]", str(NDArray[Shape[" 2 , 2 "], Any])
250 | )
251 | self.assertEqual("NDArray[Any, UByte]", str(NDArray[Any, UInt8]))
252 | self.assertEqual("NDArray[Any, UByte]", str(NDArray[Any, np.uint8]))
253 | self.assertEqual(
254 | str(NDArray[Shape[" 2 , 2 "], Any]), repr(NDArray[Shape[" 2 , 2 "], Any])
255 | )
256 | self.assertEqual(
257 | "NDArray[Any, Structure['[x, y]: Float']]",
258 | str(NDArray[Any, Structure["x: Float, y: Float"]]),
259 | )
260 | self.assertEqual(
261 | "NDArray[Any, Structure['[x, y]: Float']]",
262 | repr(NDArray[Any, Structure["x: Float, y: Float"]]),
263 | )
264 |
265 | def test_types_with_numpy_dtypes(self):
266 | self.assertIsInstance(np.array([42]), NDArray[Any, np.int_])
267 | self.assertIsInstance(np.array([42.0]), NDArray[Any, np.float_])
268 | self.assertIsInstance(np.array([np.uint8(42)]), NDArray[Any, np.uint8])
269 | self.assertIsInstance(np.array([True]), NDArray[Any, np.bool_])
270 |
271 | def test_types_with_nptyping_aliases(self):
272 | self.assertIsInstance(np.array([42]), NDArray[Any, Int])
273 | self.assertIsInstance(np.array([42.0]), NDArray[Any, Float])
274 | self.assertIsInstance(np.array([np.uint8(42)]), NDArray[Any, UInt8])
275 | self.assertIsInstance(np.array([True]), NDArray[Any, Bool])
276 |
277 | def test_recursive_structure_is_forbidden(self):
278 | with self.assertRaises(NPTypingError) as err:
279 | NDArray[Any, Int][Any, Int]
280 | self.assertEqual(
281 | "Type nptyping.NDArray[Any, Int] is already parameterized.",
282 | str(err.exception),
283 | )
284 |
285 | def test_ndarray_is_hashable(self):
286 | hash(NDArray)
287 | hash(NDArray[Any, Any])
288 | hash(NDArray[Shape["2, 2"], Any])
289 |
290 | def test_instantiation_is_forbidden(self):
291 | with self.assertRaises(NPTypingError):
292 | NDArray[Shape["2, 2"], Any]()
293 |
294 | def test_subclassing_is_forbidden(self):
295 | with self.assertRaises(NPTypingError):
296 |
297 | class SomeSubclass(NDArray):
298 | ...
299 |
300 | def test_changing_attributes_is_forbidden(self):
301 | with self.assertRaises(NPTypingError):
302 | NDArray[Any, Any].__args__ = (1, 2)
303 |
304 | with self.assertRaises(NPTypingError):
305 | NDArray[Any, Any].some_attr = 42
306 |
--------------------------------------------------------------------------------
/tests/test_package_info.py:
--------------------------------------------------------------------------------
1 | import os
2 | from unittest import TestCase
3 | from unittest.case import skipIf
4 |
5 | import feedparser
6 |
7 | from nptyping import __version__
8 |
9 |
10 | class PackageInfoTest(TestCase):
11 | @skipIf(os.environ.get("CI"), reason="Only run locally")
12 | def test_version_bump(self):
13 | releases = feedparser.parse(
14 | "https://pypi.org/rss/project/nptyping/releases.xml"
15 | )
16 | release_versions = {entry.title for entry in releases.entries}
17 |
18 | self.assertNotIn(__version__, release_versions, "Version bump required")
19 |
--------------------------------------------------------------------------------
/tests/test_performance.py:
--------------------------------------------------------------------------------
1 | from timeit import Timer
2 | from unittest import TestCase
3 |
4 | import numpy as np
5 |
6 | from nptyping import (
7 | Float,
8 | NDArray,
9 | Shape,
10 | )
11 |
12 |
13 | class PerformanceTest(TestCase):
14 | def test_instance_check_performance(self):
15 |
16 | arr = np.random.randn(42, 42, 3, 5)
17 |
18 | def _check_inst():
19 | isinstance(arr, NDArray[Shape["A, *, [a, b, c], 5"], Float])
20 |
21 | first_time_sec = Timer(_check_inst).timeit(number=1)
22 | second_time_sec = Timer(_check_inst).timeit(number=1)
23 |
24 | self.assertLess(first_time_sec, 0.02)
25 | self.assertLess(second_time_sec, first_time_sec)
26 | self.assertLess(second_time_sec, 0.0004)
27 |
--------------------------------------------------------------------------------
/tests/test_pyright.py:
--------------------------------------------------------------------------------
1 | from functools import partial
2 | from subprocess import PIPE, run
3 | from typing import Tuple
4 | from unittest import TestCase
5 |
6 | import pyright
7 |
8 | from tests.test_helpers.temp_file import temp_file
9 |
10 |
11 | def _check_pyright_on_code(python_code: str) -> Tuple[int, str, str]:
12 | pyright.node.subprocess.run = partial(run, stdout=PIPE, stderr=PIPE)
13 | try:
14 | with temp_file(python_code) as path_to_file:
15 | result = pyright.run(str(path_to_file))
16 | return (
17 | result.returncode,
18 | bytes.decode(result.stdout),
19 | bytes.decode(result.stderr),
20 | )
21 | finally:
22 | pyright.node.subprocess.run = run
23 |
24 |
25 | class PyrightTest(TestCase):
26 | def test_pyright_accepts_array_with_shape(self):
27 | exit_code, stdout, sterr = _check_pyright_on_code(
28 | """
29 | from typing import Any
30 | from nptyping import NDArray, Shape
31 |
32 |
33 | NDArray[Shape["*, ..."], Any]
34 | """
35 | )
36 | self.assertEqual(0, exit_code, stdout)
37 |
38 | def test_pyright_accepts_array_with_structure(self):
39 | exit_code, stdout, sterr = _check_pyright_on_code(
40 | """
41 | from typing import Any
42 | from nptyping import NDArray, Structure
43 |
44 |
45 | NDArray[Any, Structure["x: Int, y: Float"]]
46 | """
47 | )
48 | self.assertEqual(0, exit_code, stdout)
49 |
--------------------------------------------------------------------------------
/tests/test_recarray.py:
--------------------------------------------------------------------------------
1 | from typing import Any
2 | from unittest import TestCase
3 |
4 | import numpy as np
5 |
6 | from nptyping import (
7 | Int32,
8 | NDArray,
9 | RecArray,
10 | Shape,
11 | Structure,
12 | )
13 | from nptyping.error import InvalidArgumentsError
14 |
15 |
16 | class RecArrayTest(TestCase):
17 | def test_isinstance_succeeds_if_shape_and_structure_match(self):
18 | arr = np.array([("William", 23)], dtype=[("name", "U8"), ("age", "i4")])
19 |
20 | self.assertNotIsInstance(arr, RecArray)
21 |
22 | rec_arr = arr.view(np.recarray)
23 |
24 | self.assertIsInstance(rec_arr, RecArray)
25 | self.assertIsInstance(
26 | rec_arr, RecArray[Shape["1"], Structure["name: Str, age: Int32"]]
27 | )
28 | self.assertIsInstance(
29 | rec_arr, NDArray[Shape["1"], Structure["name: Str, age: Int32"]]
30 | )
31 |
32 | def test_rec_array_enforces_structure(self):
33 | with self.assertRaises(InvalidArgumentsError) as err:
34 | RecArray[Any, Int32]
35 |
36 | self.assertEqual(
37 | "Unexpected argument . Expecting a Structure.",
38 | str(err.exception),
39 | )
40 |
41 | def test_rec_array_allows_any(self):
42 | arr = np.array([("Billy", 23)], dtype=[("name", "U8"), ("age", "i4")])
43 | rec_arr = arr.view(np.recarray)
44 |
45 | self.assertIsInstance(rec_arr, RecArray[Any, Any])
46 |
--------------------------------------------------------------------------------
/tests/test_shape.py:
--------------------------------------------------------------------------------
1 | from unittest import TestCase
2 |
3 | from nptyping import Shape
4 | from nptyping.error import InvalidArgumentsError
5 | from nptyping.typing_ import Literal
6 |
7 |
8 | class ShapeTest(TestCase):
9 | def test_happy_flow(self):
10 | self.assertIs(Shape["2, 2"], Shape["2, 2 "])
11 |
12 | def test_invalid_argument_is_detected(self):
13 | with self.assertRaises(InvalidArgumentsError) as err:
14 | Shape[42]
15 |
16 | self.assertIn("int", str(err.exception))
17 | self.assertIn("str", str(err.exception))
18 |
19 | def test_str(self):
20 | self.assertEqual("Shape['2, 2']", str(Shape[" 2 , 2 "]))
21 |
22 | def test_repr(self):
23 | self.assertEqual("Shape['2, 2']", repr(Shape[" 2 , 2 "]))
24 |
25 | def test_shape_can_be_compared_to_literal(self):
26 | self.assertEqual(Shape["2, 2"], Literal["2, 2"])
27 | self.assertEqual(Shape[" 2 , 2 "], Literal["2,2"])
28 |
29 | def test_quotes_are_allowed(self):
30 | self.assertEqual(Shape["2, 2"], Shape["'2, 2'"])
31 | self.assertEqual(Shape["2, 2"], Shape['"2, 2"'])
32 |
--------------------------------------------------------------------------------
/tests/test_shape_expression.py:
--------------------------------------------------------------------------------
1 | from unittest import TestCase
2 |
3 | from nptyping import (
4 | InvalidShapeError,
5 | normalize_shape_expression,
6 | validate_shape_expression,
7 | )
8 |
9 |
10 | class ShapeExpressionTest(TestCase):
11 | def test_validate_shape_expression_success(self):
12 | validate_shape_expression("1")
13 | validate_shape_expression("1, 2")
14 | validate_shape_expression("1, 2, 3")
15 | validate_shape_expression("1, ...")
16 | validate_shape_expression("*, ...")
17 | validate_shape_expression("*, *, ...")
18 | validate_shape_expression("VaRiAbLe123, ...")
19 | validate_shape_expression("[a, b], ...")
20 | validate_shape_expression("2, 3, ...")
21 | validate_shape_expression("*, *, *")
22 | validate_shape_expression("VaRiAbLe123, VaRiAbLe123, VaRiAbLe123")
23 | validate_shape_expression("[a]")
24 | validate_shape_expression("[a, b]")
25 | validate_shape_expression("[a, b], [c]")
26 | validate_shape_expression("[a, b], [c], 1")
27 | validate_shape_expression("1 stuff")
28 | validate_shape_expression("1 value of stuff")
29 | validate_shape_expression("1 stuff, 2 stuff")
30 | validate_shape_expression("[a, b, c] stuff")
31 | validate_shape_expression(" [ a , b , c ] stuff ")
32 | validate_shape_expression(
33 | " [ a , b , c ] stuff, *, VaRiAbLe123, 2 stuff"
34 | )
35 | validate_shape_expression("[a,b,c] stuff,*,VaRiAbLe123,2 stuff")
36 |
37 | def test_validate_shape_expression_fail(self):
38 | with self.assertRaises(InvalidShapeError):
39 | validate_shape_expression("")
40 | with self.assertRaises(InvalidShapeError):
41 | validate_shape_expression(" ")
42 | with self.assertRaises(InvalidShapeError):
43 | validate_shape_expression("just_a_label")
44 | with self.assertRaises(InvalidShapeError):
45 | validate_shape_expression("1number_with_a_label_attached")
46 | with self.assertRaises(InvalidShapeError):
47 | validate_shape_expression("_")
48 | with self.assertRaises(InvalidShapeError):
49 | validate_shape_expression("1, [2, 2]")
50 | with self.assertRaises(InvalidShapeError):
51 | validate_shape_expression("1, [NoVars, InDimBreakdowns]")
52 | with self.assertRaises(InvalidShapeError):
53 | validate_shape_expression("1, ..., 2")
54 | with self.assertRaises(InvalidShapeError):
55 | validate_shape_expression("**")
56 | with self.assertRaises(InvalidShapeError):
57 | validate_shape_expression("1,")
58 | with self.assertRaises(InvalidShapeError):
59 | validate_shape_expression("[a,]")
60 | with self.assertRaises(InvalidShapeError):
61 | validate_shape_expression("1 label with a number 2")
62 |
63 | def test_normalize_shape_expression(self):
64 | self.assertEqual("1, 1", normalize_shape_expression(" 1 , 1 "))
65 | self.assertEqual("X, Y", normalize_shape_expression(" X , Y "))
66 | self.assertEqual("1, ...", normalize_shape_expression(" 1 , ... "))
67 | self.assertEqual(
68 | "1, [a, b], C", normalize_shape_expression(" 1 , [ a , b ] , C ")
69 | )
70 | self.assertEqual(
71 | "1 label1 label2", normalize_shape_expression(" 1 label1 label2 ")
72 | )
73 | self.assertEqual(
74 | "1 label1 label2, [label3, label4]",
75 | normalize_shape_expression(" 1 label1 label2 , [ label3 , label4 ] "),
76 | )
77 |
--------------------------------------------------------------------------------
/tests/test_structure.py:
--------------------------------------------------------------------------------
1 | from unittest import TestCase
2 |
3 | from nptyping import Structure
4 | from nptyping.error import InvalidArgumentsError
5 | from nptyping.typing_ import Literal
6 |
7 |
8 | class StructureTest(TestCase):
9 | def test_happy_flow(self):
10 | self.assertIs(Structure["name: type"], Structure["name: type "])
11 |
12 | def test_invalid_argument_is_detected(self):
13 | with self.assertRaises(InvalidArgumentsError) as err:
14 | Structure[42]
15 |
16 | self.assertIn("int", str(err.exception))
17 | self.assertIn("str", str(err.exception))
18 |
19 | def test_str(self):
20 | self.assertEqual("Structure['name: type']", str(Structure[" name : type "]))
21 |
22 | def test_repr(self):
23 | self.assertEqual("Structure['name: type']", repr(Structure[" name : type "]))
24 |
25 | def test_shape_and_literal_are_interchangeable(self):
26 | self.assertEqual(Structure["name: type"], Literal["name: type"])
27 |
28 | def test_get_types(self):
29 | structure = Structure["a: Float, b: Int, [c, d, e]: Complex"]
30 | self.assertEqual({"Float", "Int", "Complex"}, set(structure.get_types()))
31 |
32 | def test_get_names(self):
33 | structure = Structure["a: Float, b: Int, [c, d, e]: Complex"]
34 | self.assertEqual({"a", "b", "c", "d", "e"}, set(structure.get_names()))
35 |
36 | def test_structure_can_be_compared_to_literal(self):
37 | self.assertEqual(Structure["a: Int, b: Float"], Literal["a: Int, b: Float"])
38 | self.assertEqual(
39 | Structure["b: Float, a: Int"], Literal[" a : Int , b : Float "]
40 | )
41 |
--------------------------------------------------------------------------------
/tests/test_structure_expression.py:
--------------------------------------------------------------------------------
1 | from unittest import TestCase
2 |
3 | import numpy as np
4 |
5 | from nptyping import Structure
6 | from nptyping.error import InvalidStructureError
7 | from nptyping.structure_expression import (
8 | check_structure,
9 | create_name_to_type_dict,
10 | normalize_structure_expression,
11 | validate_structure_expression,
12 | )
13 | from nptyping.typing_ import dtype_per_name
14 |
15 |
16 | class StructureExpressionTest(TestCase):
17 | def test_check_structure_true(self):
18 | dtype = np.dtype([("name", "U10"), ("age", "i4")])
19 | structure = Structure["name: Str, age: Int32"]
20 | self.assertTrue(check_structure(dtype, structure, dtype_per_name))
21 |
22 | dtype2 = np.dtype([("a", "i4"), ("b", "i4"), ("c", "i8")])
23 | structure2 = Structure["[a, b, c]: Integer"]
24 | self.assertTrue(check_structure(dtype2, structure2, dtype_per_name))
25 |
26 | dtype3 = np.dtype([("a", "i4"), ("b", "i4"), ("c", "i8")])
27 | structure3 = Structure["[a, b, c]: Int"]
28 | self.assertTrue(check_structure(dtype3, structure3, dtype_per_name))
29 |
30 | dtype4 = np.dtype([("name", "U10")])
31 | structure4 = Structure["name: *"]
32 | self.assertTrue(check_structure(dtype4, structure4, dtype_per_name))
33 |
34 | def test_check_structure_with_sub_array(self):
35 | dtype = np.dtype([("x", "U10", (2, 2))])
36 | structure = Structure["x: Str[2, 2]"]
37 | self.assertTrue(check_structure(dtype, structure, dtype_per_name))
38 |
39 | dtype2 = np.dtype([("x", "U10", (2, 2))])
40 | structure2 = Structure["x: Int[2, 2]"]
41 | self.assertFalse(
42 | check_structure(dtype2, structure2, dtype_per_name),
43 | "It should fail because of the type.",
44 | )
45 |
46 | dtype3 = np.dtype([("x", "U10", (3, 3))])
47 | structure3 = Structure["x: Str[2, 2]"]
48 | self.assertFalse(
49 | check_structure(dtype3, structure3, dtype_per_name),
50 | "It should fail because of the shape.",
51 | )
52 |
53 | dtype4 = np.dtype([("x", "U10", (2, 2)), ("y", "U10", (2, 2))])
54 | structure4 = Structure["x: Str[2, 2], y: Str[2, 2]"]
55 | self.assertTrue(check_structure(dtype4, structure4, dtype_per_name))
56 |
57 | dtype5 = np.dtype([("x", "U10", (2, 2)), ("y", "U10", (2, 2))])
58 | structure5 = Structure["[x, y]: Str[2, 2]"]
59 | self.assertTrue(check_structure(dtype5, structure5, dtype_per_name))
60 |
61 | dtype6 = np.dtype([("x", "U10")])
62 | structure6 = Structure["x: Str[2, 2]"]
63 | self.assertFalse(
64 | check_structure(dtype6, structure6, dtype_per_name),
65 | "It should fail because it doesn't contain a sub array at all.",
66 | )
67 |
68 | def test_check_structure_false(self):
69 | dtype = np.dtype([("name", "U10"), ("age", "i4")])
70 | structure = Structure["name: Str, age: UInt"]
71 | self.assertFalse(check_structure(dtype, structure, dtype_per_name))
72 |
73 | def test_check_structure_invalid_type(self):
74 | dtype = np.dtype([("name", "U10"), ("age", "i4")])
75 | structure = Structure["name: Str, age: Ui"]
76 | with self.assertRaises(InvalidStructureError) as err:
77 | check_structure(dtype, structure, dtype_per_name)
78 | self.assertEqual(
79 | "Type 'Ui' is not valid in this context. Did you mean 'Unicode'?",
80 | str(err.exception),
81 | )
82 |
83 | dtype2 = np.dtype([("name", "U10"), ("age", "i4")])
84 | structure2 = Structure["name: Str, age: uint"]
85 | with self.assertRaises(InvalidStructureError) as err:
86 | check_structure(dtype2, structure2, dtype_per_name)
87 | self.assertEqual(
88 | "Type 'uint' is not valid in this context. Did you mean one of"
89 | " 'Int', 'UInt', 'IntP'?",
90 | str(err.exception),
91 | )
92 |
93 | dtype3 = np.dtype([("name", "U10"), ("age", "i4")])
94 | structure3 = Structure["name: Str, age: not_even_close"]
95 | with self.assertRaises(InvalidStructureError) as err:
96 | check_structure(dtype3, structure3, dtype_per_name)
97 | self.assertEqual(
98 | "Type 'not_even_close' is not valid in this context.", str(err.exception)
99 | )
100 |
101 | def test_check_structure_that_is_subset_or_superset_of_dtype(self):
102 | dtype1 = np.dtype([("x", "i4"), ("y", "i4")])
103 | structure1 = Structure["x: Int"]
104 | self.assertFalse(check_structure(dtype1, structure1, dtype_per_name))
105 |
106 | dtype2 = np.dtype([("x", "i4"), ("y", "i4")])
107 | structure2 = Structure["x: Int, y: Int, z: Int"]
108 | self.assertFalse(check_structure(dtype2, structure2, dtype_per_name))
109 |
110 | def test_validate_structure_expression_success(self):
111 | # validate_structure_expression("_: t")
112 | validate_structure_expression("a: t")
113 | validate_structure_expression("a: type")
114 | validate_structure_expression("a: Type")
115 | validate_structure_expression("a: t_")
116 | validate_structure_expression("a: t_123")
117 | validate_structure_expression("a_123: t")
118 | validate_structure_expression("abc: type")
119 | validate_structure_expression("abc: *")
120 | validate_structure_expression("abc: type[2, 2]")
121 | validate_structure_expression("abc: type [*, ...]")
122 | validate_structure_expression("abc: type, def: type")
123 | validate_structure_expression("abc: type[*, 2, ...], def: type[2 ]")
124 | validate_structure_expression("[abc, def]: type")
125 | validate_structure_expression("[abc, def]: type, *")
126 | validate_structure_expression("[abc, def]: type[*, ...]")
127 | validate_structure_expression("[abc, def]: type1, ghi: type2")
128 | validate_structure_expression("[abc, def]: type1, [ghi, jkl]: type2")
129 | validate_structure_expression(
130 | "[abc, def]: type1, [ghi, jkl]: type2, mno: type3"
131 | )
132 | validate_structure_expression("[abc,def]:type1,[ghi,jkl]:type2,mno:type3")
133 | validate_structure_expression(
134 | " [ abc , def ] : type1 , [ ghi , jkl ] : type2 ,"
135 | " mno : type3 "
136 | )
137 |
138 | def test_validate_structure_expression_fail(self):
139 | with self.assertRaises(InvalidStructureError):
140 | validate_structure_expression("a: _")
141 | with self.assertRaises(InvalidStructureError):
142 | validate_structure_expression("a: 1")
143 | with self.assertRaises(InvalidStructureError):
144 | validate_structure_expression("1: t")
145 | with self.assertRaises(InvalidStructureError):
146 | validate_structure_expression("abc: type$")
147 | with self.assertRaises(InvalidStructureError):
148 | validate_structure_expression("a$bc: type$")
149 | with self.assertRaises(InvalidStructureError):
150 | validate_structure_expression("ab c: type")
151 | with self.assertRaises(InvalidStructureError):
152 | validate_structure_expression("abc: type,")
153 | with self.assertRaises(InvalidStructureError):
154 | validate_structure_expression("abc:: type")
155 | with self.assertRaises(InvalidStructureError):
156 | validate_structure_expression("[a]: type")
157 | with self.assertRaises(InvalidStructureError):
158 | validate_structure_expression("[a,]: type")
159 | with self.assertRaises(InvalidStructureError):
160 | validate_structure_expression("[a,b,]: type")
161 | with self.assertRaises(InvalidStructureError):
162 | validate_structure_expression("[,a,b]: type")
163 | with self.assertRaises(InvalidStructureError):
164 | validate_structure_expression("abc: type []")
165 | with self.assertRaises(InvalidStructureError):
166 | validate_structure_expression("a: t[]")
167 | with self.assertRaises(InvalidStructureError) as err:
168 | validate_structure_expression(
169 | "a: t[*, 2, ...], b: t[not-a-valid-shape-expression]"
170 | )
171 | self.assertIn("not-a-valid-shape-expression", str(err.exception))
172 | with self.assertRaises(InvalidStructureError) as err:
173 | validate_structure_expression("a: t1, b: t2, c: t3, a: t4")
174 | self.assertEqual(
175 | "Field names may occur only once in a structure expression. Field"
176 | " name 'a' occurs 2 times in 'a: t1, b: t2, c: t3, a: t4'.",
177 | str(err.exception),
178 | )
179 | with self.assertRaises(InvalidStructureError) as err:
180 | validate_structure_expression("[a, b, c]: t1, [d, e, b]: t2, b: t3")
181 | self.assertEqual(
182 | "Field names may occur only once in a structure expression. Field"
183 | " name 'b' occurs 3 times in '[a, b, c]: t1, [d, e, b]: t2, b: t3'.",
184 | str(err.exception),
185 | )
186 |
187 | def test_normalize_structure_expression(self):
188 | self.assertEqual("a: t", normalize_structure_expression(" a : t "))
189 | self.assertEqual("a: t", normalize_structure_expression("a:t"))
190 | self.assertEqual(
191 | "b: t1, [a, c]: t2", normalize_structure_expression("c: t2, b: t1, a: t2")
192 | )
193 | self.assertEqual(
194 | "[a, c]: *, b: t1", normalize_structure_expression("c: *, b: t1, a: *")
195 | )
196 | self.assertEqual(
197 | "b: t1, [a, c]: t2", normalize_structure_expression("[a, c]: t2, b: t1")
198 | )
199 | self.assertEqual(
200 | "[a, b, c]: t", normalize_structure_expression("[b, a]: t, c: t")
201 | )
202 | self.assertEqual(
203 | "[a, b, d, e]: t1, c: t2",
204 | normalize_structure_expression("[b, a]: t1, c: t2, [d, e]: t1"),
205 | )
206 | self.assertEqual(
207 | "a: t[*, ...]", normalize_structure_expression(" a : t [ * , ... ]")
208 | )
209 |
210 | def test_create_name_to_type_dict(self):
211 | output = create_name_to_type_dict("a: t1, b: t2, c: t1")
212 | expected = {"a": "t1", "b": "t2", "c": "t1"}
213 | self.assertDictEqual(expected, output)
214 |
215 | def test_structure_depicting_at_least(self):
216 | # Test that you can define a Structure that expresses a structure with
217 | # at least some columns of some type.
218 | dtype_true1 = np.dtype([("a", "U10"), ("b", "i4")])
219 | dtype_true2 = np.dtype([("a", "U10"), ("b", "i4"), ("c", "i4"), ("d", "i4")])
220 | dtype_false1 = np.dtype([("a", "U10"), ("b", "U10")])
221 | dtype_false2 = np.dtype([("b", "i4"), ("c", "i4"), ("d", "i4")])
222 |
223 | structure = Structure["a: Str, b: Int32, *"]
224 |
225 | self.assertTrue(check_structure(dtype_true1, structure, dtype_per_name))
226 | self.assertTrue(check_structure(dtype_true2, structure, dtype_per_name))
227 | self.assertFalse(check_structure(dtype_false1, structure, dtype_per_name))
228 | self.assertFalse(check_structure(dtype_false2, structure, dtype_per_name))
229 |
--------------------------------------------------------------------------------
/tests/test_typeguard.py:
--------------------------------------------------------------------------------
1 | from unittest import TestCase
2 |
3 | import numpy as np
4 | from typeguard import typechecked
5 |
6 | from nptyping import (
7 | Float,
8 | NDArray,
9 | Shape,
10 | )
11 |
12 |
13 | @typechecked
14 | def fun(_: NDArray[Shape["2, 2"], Float]) -> None:
15 | ...
16 |
17 |
18 | class TypeguardTest(TestCase):
19 | def test_trivial_fail(self):
20 | with self.assertRaises(Exception):
21 | fun(42)
22 |
23 | def test_success(self):
24 | fun(np.random.randn(2, 2))
25 |
26 | def test_fail_shape(self):
27 | with self.assertRaises(Exception):
28 | fun(np.random.randn(3, 2))
29 |
30 | def test_fail_dtype(self):
31 | with self.assertRaises(Exception):
32 | fun(np.random.randn(2, 2).astype(int))
33 |
--------------------------------------------------------------------------------
/tests/test_wheel.py:
--------------------------------------------------------------------------------
1 | import os
2 | import subprocess
3 | import sys
4 | import venv
5 | from contextlib import contextmanager
6 | from glob import glob
7 | from pathlib import Path
8 | from tempfile import TemporaryDirectory
9 | from typing import Any
10 | from unittest import (
11 | TestCase,
12 | TestLoader,
13 | skipIf,
14 | )
15 | from zipfile import ZipFile
16 |
17 | from nptyping.package_info import __version__
18 |
19 | _PATH_TO_SETUP = str(Path(__file__).parent.parent / "setup.py")
20 | _ROOT = Path(__file__).parent.parent.absolute()
21 | _VENV_NAME = "test_venv"
22 | _WHEEL_NAME = f"nptyping-{str(__version__)}-py3-none-any.whl"
23 | _VENV_PYTHON = "bin/python"
24 | _VENV_PIP = "bin/pip"
25 | if os.name == "nt":
26 | _VENV_PYTHON = "Scripts\\python.exe"
27 | _VENV_PIP = "Scripts\\pip.exe"
28 | _EXPECTED_FILES_IN_WHEEL = {
29 | "__init__.py",
30 | "assert_isinstance.py",
31 | "base_meta_classes.py",
32 | "error.py",
33 | "ndarray.py",
34 | "ndarray.pyi",
35 | "nptyping_type.py",
36 | "package_info.py",
37 | "py.typed",
38 | "recarray.py",
39 | "recarray.pyi",
40 | "shape.py",
41 | "shape.pyi",
42 | "shape_expression.py",
43 | "structure.py",
44 | "structure.pyi",
45 | "structure_expression.py",
46 | "typing_.py",
47 | "typing_.pyi",
48 | "pandas_/__init__.py",
49 | "pandas_/dataframe.py",
50 | "pandas_/dataframe.pyi",
51 | "pandas_/typing_.py",
52 | }
53 |
54 |
55 | def determine_order(_: Any, x: str, __: str) -> int:
56 | prio_tests = ("test_wheel_is_built_correctly", "test_wheel_can_be_installed")
57 | return -1 if x in prio_tests else 1
58 |
59 |
60 | TestLoader.sortTestMethodsUsing = determine_order
61 |
62 |
63 | @contextmanager
64 | def working_dir(path: Path):
65 | origin = Path().absolute()
66 | try:
67 | os.chdir(path)
68 | yield
69 | finally:
70 | os.chdir(origin)
71 |
72 |
73 | # No need to run these tests on all versions. They take a long time.
74 | @skipIf(sys.version_info.minor != 10, "Does not work on 3.11 due to invoke")
75 | class WheelTest(TestCase):
76 | temp_dir: TemporaryDirectory
77 | py: str
78 | pip: str
79 |
80 | @classmethod
81 | def setUpClass(cls) -> None:
82 | cls.temp_dir = TemporaryDirectory()
83 | venv_bin = Path(cls.temp_dir.name) / _VENV_NAME
84 | cls.py = str(venv_bin / _VENV_PYTHON)
85 | cls.pip = str(venv_bin / _VENV_PIP)
86 |
87 | @classmethod
88 | def tearDownClass(cls) -> None:
89 | cls.temp_dir.cleanup()
90 |
91 | def test_wheel_is_built_correctly(self):
92 | with working_dir(_ROOT):
93 | subprocess.check_output(f"{sys.executable} -m invoke wheel", shell=True)
94 | wheel_files = glob(f"dist/*{__version__}*.whl")
95 | src_files = glob(f"dist/*{__version__}*.tar.gz")
96 |
97 | self.assertEqual(1, len(wheel_files))
98 | self.assertEqual(1, len(src_files))
99 |
100 | with ZipFile(_ROOT / Path(wheel_files[0]), "r") as zip_:
101 | files_in_wheel = set(
102 | f.filename[len("nptyping/") :]
103 | for f in zip_.filelist
104 | if f.filename.startswith("nptyping/")
105 | )
106 |
107 | self.assertSetEqual(_EXPECTED_FILES_IN_WHEEL, files_in_wheel)
108 |
109 | def test_wheel_can_be_installed(self):
110 | with working_dir(Path(self.temp_dir.name)):
111 | venv.create(_VENV_NAME, with_pip=False)
112 | # For some reason, with_pip=True fails, so we do it separately.
113 | subprocess.check_output(
114 | f"{self.py} -m ensurepip --upgrade --default-pip", shell=True
115 | )
116 | subprocess.check_output(
117 | f"{self.py} -m pip install --upgrade pip", shell=True
118 | )
119 | subprocess.check_output(
120 | f"{self.pip} install {_ROOT / 'dist' / _WHEEL_NAME}", shell=True
121 | )
122 | # No errors raised? Then the install succeeded.
123 |
124 | def test_basic_nptyping_code(self):
125 | code = (
126 | "from nptyping import NDArray, Shape, Int; "
127 | "import numpy as np; "
128 | "print(isinstance(np.array([[1, 2], [3, 4]]), NDArray[Shape['2, 2'], Int]))"
129 | )
130 |
131 | output = subprocess.check_output(f'{self.py} -c "{code}"', shell=True)
132 |
133 | self.assertIn("True", str(output))
134 |
--------------------------------------------------------------------------------