typing-arguments 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,16 @@
1
+ version: 2
2
+ updates:
3
+ - package-ecosystem: "github-actions"
4
+ directory: "/"
5
+ schedule:
6
+ interval: "weekly"
7
+ assignees:
8
+ - "ddanier"
9
+ - package-ecosystem: "pip"
10
+ directory: "/"
11
+ schedule:
12
+ interval: "weekly"
13
+ ignore:
14
+ - dependency-name: "pydantic"
15
+ assignees:
16
+ - "ddanier"
@@ -0,0 +1,24 @@
1
+ name: "LINT: Run ruff & pyright"
2
+ on:
3
+ push:
4
+ pull_request:
5
+ schedule:
6
+ - cron: '0 7 * * 1'
7
+ jobs:
8
+ lint:
9
+ runs-on: ubuntu-latest
10
+ steps:
11
+ - uses: actions/checkout@v4
12
+ - name: Install uv
13
+ uses: astral-sh/setup-uv@v4
14
+ - name: Set up Python
15
+ uses: actions/setup-python@v5
16
+ with:
17
+ python-version: "3.x"
18
+ - name: Install dependencies
19
+ run: |
20
+ uv sync --group dev
21
+ - name: Lint with ruff & pyright
22
+ run: |
23
+ uv run ruff check typing_arguments tests
24
+ uv run pyright typing_arguments
@@ -0,0 +1,26 @@
1
+ name: "RELEASE: Upload Python Package to PyPI"
2
+ on:
3
+ release:
4
+ types: [published]
5
+ jobs:
6
+ release:
7
+ runs-on: ubuntu-latest
8
+ permissions:
9
+ id-token: write
10
+ steps:
11
+ - uses: actions/checkout@v4
12
+ - name: Install uv
13
+ uses: astral-sh/setup-uv@v4
14
+ - name: Set up Python
15
+ uses: actions/setup-python@v5
16
+ with:
17
+ python-version: '3.x'
18
+ - name: Install dependencies
19
+ run: |
20
+ uv sync
21
+ - name: Build package
22
+ run: uv build
23
+ - name: Publish package
24
+ uses: pypa/gh-action-pypi-publish@release/v1
25
+ with:
26
+ password: ${{ secrets.PYPI_API_TOKEN }}
@@ -0,0 +1,26 @@
1
+ name: "TEST: Run pytest using tox"
2
+ on:
3
+ push:
4
+ pull_request:
5
+ schedule:
6
+ - cron: '0 7 * * 1'
7
+ jobs:
8
+ test:
9
+ runs-on: ubuntu-latest
10
+ strategy:
11
+ matrix:
12
+ python-version: ["3.9", "3.9", "3.10", "3.11", "3.12", "3.13"]
13
+ steps:
14
+ - uses: actions/checkout@v4
15
+ - name: Install uv
16
+ uses: astral-sh/setup-uv@v4
17
+ - name: Set up Python ${{ matrix.python-version }}
18
+ uses: actions/setup-python@v5
19
+ with:
20
+ python-version: ${{ matrix.python-version }}
21
+ - name: Install dependencies
22
+ run: |
23
+ uv sync --group dev
24
+ - name: Test with pytest
25
+ run: |
26
+ uv run tox -e 'py'
@@ -0,0 +1,13 @@
1
+ # Python-generated files
2
+ __pycache__/
3
+ *.py[oc]
4
+ /build/
5
+ /dist/
6
+ /wheels/
7
+ /*.egg-info
8
+ /uv.lock
9
+ /.coverage
10
+ /.tox
11
+
12
+ # Virtual environments
13
+ /.venv
@@ -0,0 +1,26 @@
1
+ repos:
2
+ - repo: https://github.com/pre-commit/pre-commit-hooks
3
+ rev: v5.0.0
4
+ hooks:
5
+ - id: end-of-file-fixer
6
+ - id: check-added-large-files
7
+ - id: check-merge-conflict
8
+ - id: check-docstring-first
9
+ - id: debug-statements
10
+ - repo: https://github.com/astral-sh/ruff-pre-commit
11
+ rev: v0.8.0
12
+ hooks:
13
+ - id: ruff
14
+ args: [--fix, --exit-non-zero-on-fix]
15
+ - repo: https://github.com/alessandrojcm/commitlint-pre-commit-hook
16
+ rev: v9.18.0
17
+ hooks:
18
+ - id: commitlint
19
+ stages: [commit-msg]
20
+ additional_dependencies:
21
+ - "@commitlint/config-conventional"
22
+ default_stages:
23
+ - pre-commit
24
+ default_install_hook_types:
25
+ - pre-commit
26
+ - commit-msg
@@ -0,0 +1 @@
1
+ 3.10
@@ -0,0 +1,20 @@
1
+ The MIT License (MIT)
2
+ Copyright (c) 2024 TEAM23 GmbH
3
+
4
+ Permission is hereby granted, free of charge, to any person obtaining a copy
5
+ of this software and associated documentation files (the "Software"), to deal
6
+ in the Software without restriction, including without limitation the rights
7
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ copies of the Software, and to permit persons to whom the Software is
9
+ furnished to do so, subject to the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be included in all
12
+ copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
17
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
18
+ DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
19
+ OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
20
+ OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,158 @@
1
+ Metadata-Version: 2.3
2
+ Name: typing-arguments
3
+ Version: 0.1.0
4
+ Summary: Store references of your typing arguments to be available at runtime.
5
+ Project-URL: Repository, https://github.com/team23/typing-arguments
6
+ Author-email: TEAM23 GmbH <info@team23.de>
7
+ License: The MIT License (MIT)
8
+ Copyright (c) 2024 TEAM23 GmbH
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
21
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
22
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
23
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
24
+ DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
25
+ OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
26
+ OR OTHER DEALINGS IN THE SOFTWARE.
27
+ Requires-Python: >=3.10
28
+ Provides-Extra: pydantic
29
+ Requires-Dist: pydantic<3.0.0,>=2.0.0; extra == 'pydantic'
30
+ Description-Content-Type: text/markdown
31
+
32
+ # `typing-arguments`
33
+
34
+ Typing arguments using the `Generic` base class in python are great, but they lack the ability to
35
+ easily access the type arguments at runtime. This library provides a mixin class that can be used
36
+ to make type arguments available in the class and its instances.
37
+
38
+ This is also true for classes based on `pydantic.BaseModel` for pydantic > 2.x.
39
+
40
+ ## Installation
41
+
42
+ Just use `pip install typing-arguments` to install the library.
43
+
44
+ **Note:** `typing-arguments` is tested on Python `3.10`, `3.11`, `3.12` and `3.13`. This is also ensured running
45
+ all tests on all those versions using `tox`.
46
+
47
+ ## Usage
48
+
49
+ ### Quick Example
50
+
51
+ ```python
52
+ T1 = TypeVar("T1")
53
+ T2 = TypeVar("T2", bound="SomeBaseClass")
54
+
55
+
56
+ class Something(
57
+ GenericArgumentsMixin,
58
+ Generic[T1, T2],
59
+ ):
60
+ t1 = typing_arg(T1)
61
+ t2 = typing_arg(T2)
62
+
63
+
64
+ ConcreteClass = Something[str, SomeBaseClassChild]
65
+ ConcreteClass.t1 # str
66
+ ConcreteClass.t2 # SomeBaseClassChild
67
+ ```
68
+
69
+ **Hint:** You may also use this with pydantic models:
70
+
71
+ ```python
72
+ T1 = TypeVar("T1")
73
+ T2 = TypeVar("T2", bound="SomeBaseClass")
74
+
75
+
76
+ class SomethingModel(
77
+ GenericArgumentsMixin,
78
+ BaseModel,
79
+ Generic[T1, T2],
80
+ ):
81
+ t1: ClassVar = typing_arg(T1)
82
+ t2: ClassVar = typing_arg(T2)
83
+
84
+
85
+ ConcreteClassModel = SomethingModel[str, SomeBaseClassChild]
86
+ ConcreteClassModel.t1 # str
87
+ ConcreteClassModel.t2 # SomeBaseClassChild
88
+ ```
89
+
90
+ Internally `GenericArgumentsMixin` will create a new attribute `__typing_arguments__`
91
+ inside the class and its instances. This attribute is a dictionary mapping the type
92
+ variables to their concrete types. This is useful if you want to access the type
93
+ arguments in a generic way.
94
+
95
+ The `typing_arg` function is a helper function to make the type arguments available
96
+ in the class and its instances using a nicely named attribute. This is just a
97
+ convenience function, as you can also access the type arguments directly from the
98
+ `__typing_arguments__` attribute.
99
+
100
+ **Note:** If you are using pydantic models you should use the `ClassVar` annotation
101
+ to ensure pydantic will not try to catch and validate the type arguments as normal
102
+ model fields.
103
+
104
+ You may also mix different generic base classes like so:
105
+
106
+ ```python
107
+ T1 = TypeVar("T1")
108
+ T2 = TypeVar("T2", bound="SomeBaseClass")
109
+
110
+
111
+ class Base1(
112
+ GenericArgumentsMixin,
113
+ Generic[T1],
114
+ ):
115
+ pass
116
+
117
+
118
+ class Base2(
119
+ GenericArgumentsMixin,
120
+ Generic[T2],
121
+ ):
122
+ t2 = typing_arg(T2)
123
+
124
+
125
+ class Something(
126
+ Base1[str],
127
+ Base2[SomeBaseClassChild],
128
+ ):
129
+ t1 = typing_arg(T1)
130
+
131
+
132
+ Something.t1 # str
133
+ Something.t2 # SomeBaseClassChild
134
+ ```
135
+
136
+ In this example `Base1` and `Base2` are both generic base classes. `Base1` only
137
+ defines a type argument `T1` and `Base2` only defines a type argument `T2`. The
138
+ `Something` class inherits from both `Base1` and `Base2`. Note that `Base1` does
139
+ not define a simple accessor like `t1` using `typing_arg`, while `Base2` does. This
140
+ is not a problem and can be later added by `Something` using `typing_arg` as well.
141
+
142
+ You may encounter issues using the `typing_arg` function when using type validator
143
+ like mypy or your IDE. If so you might need to use `cast` to tell the type checker
144
+ you are sure about what you are doing. For example:
145
+
146
+ ```python
147
+ T1 = TypeVar("T1", bound="SomeBaseClass")
148
+
149
+
150
+ class Something(
151
+ GenericArgumentsMixin,
152
+ Generic[T1],
153
+ ):
154
+ t1 = cast(type[SomeBaseClass], typing_arg(T1))
155
+ ```
156
+
157
+ **Note:** You will still need to use `ClassVar` when using pydantic models. This
158
+ might result in using the same type twice (inside `ClassVar` and `cast`).
@@ -0,0 +1,127 @@
1
+ # `typing-arguments`
2
+
3
+ Typing arguments using the `Generic` base class in python are great, but they lack the ability to
4
+ easily access the type arguments at runtime. This library provides a mixin class that can be used
5
+ to make type arguments available in the class and its instances.
6
+
7
+ This is also true for classes based on `pydantic.BaseModel` for pydantic > 2.x.
8
+
9
+ ## Installation
10
+
11
+ Just use `pip install typing-arguments` to install the library.
12
+
13
+ **Note:** `typing-arguments` is tested on Python `3.10`, `3.11`, `3.12` and `3.13`. This is also ensured running
14
+ all tests on all those versions using `tox`.
15
+
16
+ ## Usage
17
+
18
+ ### Quick Example
19
+
20
+ ```python
21
+ T1 = TypeVar("T1")
22
+ T2 = TypeVar("T2", bound="SomeBaseClass")
23
+
24
+
25
+ class Something(
26
+ GenericArgumentsMixin,
27
+ Generic[T1, T2],
28
+ ):
29
+ t1 = typing_arg(T1)
30
+ t2 = typing_arg(T2)
31
+
32
+
33
+ ConcreteClass = Something[str, SomeBaseClassChild]
34
+ ConcreteClass.t1 # str
35
+ ConcreteClass.t2 # SomeBaseClassChild
36
+ ```
37
+
38
+ **Hint:** You may also use this with pydantic models:
39
+
40
+ ```python
41
+ T1 = TypeVar("T1")
42
+ T2 = TypeVar("T2", bound="SomeBaseClass")
43
+
44
+
45
+ class SomethingModel(
46
+ GenericArgumentsMixin,
47
+ BaseModel,
48
+ Generic[T1, T2],
49
+ ):
50
+ t1: ClassVar = typing_arg(T1)
51
+ t2: ClassVar = typing_arg(T2)
52
+
53
+
54
+ ConcreteClassModel = SomethingModel[str, SomeBaseClassChild]
55
+ ConcreteClassModel.t1 # str
56
+ ConcreteClassModel.t2 # SomeBaseClassChild
57
+ ```
58
+
59
+ Internally `GenericArgumentsMixin` will create a new attribute `__typing_arguments__`
60
+ inside the class and its instances. This attribute is a dictionary mapping the type
61
+ variables to their concrete types. This is useful if you want to access the type
62
+ arguments in a generic way.
63
+
64
+ The `typing_arg` function is a helper function to make the type arguments available
65
+ in the class and its instances using a nicely named attribute. This is just a
66
+ convenience function, as you can also access the type arguments directly from the
67
+ `__typing_arguments__` attribute.
68
+
69
+ **Note:** If you are using pydantic models you should use the `ClassVar` annotation
70
+ to ensure pydantic will not try to catch and validate the type arguments as normal
71
+ model fields.
72
+
73
+ You may also mix different generic base classes like so:
74
+
75
+ ```python
76
+ T1 = TypeVar("T1")
77
+ T2 = TypeVar("T2", bound="SomeBaseClass")
78
+
79
+
80
+ class Base1(
81
+ GenericArgumentsMixin,
82
+ Generic[T1],
83
+ ):
84
+ pass
85
+
86
+
87
+ class Base2(
88
+ GenericArgumentsMixin,
89
+ Generic[T2],
90
+ ):
91
+ t2 = typing_arg(T2)
92
+
93
+
94
+ class Something(
95
+ Base1[str],
96
+ Base2[SomeBaseClassChild],
97
+ ):
98
+ t1 = typing_arg(T1)
99
+
100
+
101
+ Something.t1 # str
102
+ Something.t2 # SomeBaseClassChild
103
+ ```
104
+
105
+ In this example `Base1` and `Base2` are both generic base classes. `Base1` only
106
+ defines a type argument `T1` and `Base2` only defines a type argument `T2`. The
107
+ `Something` class inherits from both `Base1` and `Base2`. Note that `Base1` does
108
+ not define a simple accessor like `t1` using `typing_arg`, while `Base2` does. This
109
+ is not a problem and can be later added by `Something` using `typing_arg` as well.
110
+
111
+ You may encounter issues using the `typing_arg` function when using type validator
112
+ like mypy or your IDE. If so you might need to use `cast` to tell the type checker
113
+ you are sure about what you are doing. For example:
114
+
115
+ ```python
116
+ T1 = TypeVar("T1", bound="SomeBaseClass")
117
+
118
+
119
+ class Something(
120
+ GenericArgumentsMixin,
121
+ Generic[T1],
122
+ ):
123
+ t1 = cast(type[SomeBaseClass], typing_arg(T1))
124
+ ```
125
+
126
+ **Note:** You will still need to use `ClassVar` when using pydantic models. This
127
+ might result in using the same type twice (inside `ClassVar` and `cast`).
@@ -0,0 +1,12 @@
1
+ module.exports = {
2
+ // See https://github.com/conventional-changelog/commitlint/blob/master/%40commitlint/config-conventional/index.js
3
+ extends: ['@commitlint/config-conventional'],
4
+ // Own rules
5
+ rules: {
6
+ 'subject-case': [
7
+ 2,
8
+ 'never',
9
+ ['start-case', 'pascal-case', 'upper-case'],
10
+ ],
11
+ },
12
+ }
@@ -0,0 +1,46 @@
1
+ default:
2
+ just --list
3
+
4
+ [unix]
5
+ _install-pre-commit:
6
+ #!/usr/bin/env bash
7
+ if ( which pre-commit > /dev/null 2>&1 )
8
+ then
9
+ pre-commit install --install-hooks
10
+ else
11
+ echo "-----------------------------------------------------------------"
12
+ echo "pre-commit is not installed - cannot enable pre-commit hooks!"
13
+ echo "Recommendation: Install pre-commit ('brew install pre-commit')."
14
+ echo "-----------------------------------------------------------------"
15
+ fi
16
+
17
+ [windows]
18
+ _install-pre-commit:
19
+ #!powershell.exe
20
+ Write-Host "Please ensure pre-commit hooks are installed using 'pre-commit install --install-hooks'"
21
+
22
+ install: (uv "sync") && _install-pre-commit
23
+
24
+ update: (uv "sync")
25
+
26
+ uv *args:
27
+ uv {{args}}
28
+
29
+ test *args: (uv "run" "pytest" "--cov=typing_arguments" "--cov-report" "term-missing:skip-covered" args)
30
+
31
+ test-all: (uv "run" "tox")
32
+
33
+ ruff *args: (uv "run" "ruff" "check" "typing_arguments" "tests" args)
34
+
35
+ pyright *args: (uv "run" "pyright" "typing_arguments" args)
36
+
37
+ lint: ruff pyright
38
+
39
+ publish: (uv "publish" "--build")
40
+
41
+ release version: (uv "run" "pkg-version.py" version)
42
+ git add pyproject.toml
43
+ git commit -m "release: 🔖 v$(uv run --quiet pkg-version.py)" --no-verify
44
+ git tag "v$(uv run --quiet pkg-version.py)"
45
+ git push
46
+ git push --tags
@@ -0,0 +1,50 @@
1
+ # FILE GENERATED BY nurify COMMAND
2
+
3
+ def --wrapped "nur _install-pre-commit" [...args] {
4
+ ^just "_install-pre-commit" ...$args
5
+ }
6
+
7
+ def --wrapped "nur default" [...args] {
8
+ ^just "default" ...$args
9
+ }
10
+
11
+ def --wrapped "nur install" [...args] {
12
+ ^just "install" ...$args
13
+ }
14
+
15
+ def --wrapped "nur lint" [...args] {
16
+ ^just "lint" ...$args
17
+ }
18
+
19
+ def --wrapped "nur publish" [...args] {
20
+ ^just "publish" ...$args
21
+ }
22
+
23
+ def --wrapped "nur pyright" [...args] {
24
+ ^just "pyright" ...$args
25
+ }
26
+
27
+ def --wrapped "nur release" [...args] {
28
+ ^just "release" ...$args
29
+ }
30
+
31
+ def --wrapped "nur ruff" [...args] {
32
+ ^just "ruff" ...$args
33
+ }
34
+
35
+ def --wrapped "nur test" [...args] {
36
+ ^just "test" ...$args
37
+ }
38
+
39
+ def --wrapped "nur test-all" [...args] {
40
+ ^just "test-all" ...$args
41
+ }
42
+
43
+ def --wrapped "nur update" [...args] {
44
+ ^just "update" ...$args
45
+ }
46
+
47
+ def --wrapped "nur uv" [...args] {
48
+ ^just "uv" ...$args
49
+ }
50
+
@@ -0,0 +1,24 @@
1
+ import sys
2
+
3
+ import tomlkit
4
+
5
+ if __name__ == "__main__":
6
+ if len(sys.argv) not in (1, 2):
7
+ raise RuntimeError("Usage: python pkg-version.py [version]")
8
+
9
+ if len(sys.argv) == 2:
10
+ version = sys.argv[1]
11
+
12
+ with open("pyproject.toml", "rb") as f:
13
+ pyproject_toml = tomlkit.parse(f.read())
14
+
15
+ pyproject_toml["project"]["version"] = version
16
+
17
+ with open("pyproject.toml", "w") as f:
18
+ f.write(tomlkit.dumps(pyproject_toml))
19
+ else:
20
+ with open("pyproject.toml", "rb") as f:
21
+ pyproject_toml = tomlkit.parse(f.read())
22
+
23
+ version = pyproject_toml["project"]["version"]
24
+ print(version)
@@ -0,0 +1,33 @@
1
+ [project]
2
+ name = "typing-arguments"
3
+ version = "0.1.0"
4
+ description = "Store references of your typing arguments to be available at runtime."
5
+ readme = "README.md"
6
+ authors = [
7
+ {name = "TEAM23 GmbH", email = "info@team23.de"},
8
+ ]
9
+ license = {file = "LICENSE"}
10
+ requires-python = ">=3.10"
11
+ dependencies = []
12
+
13
+ [project.urls]
14
+ Repository = "https://github.com/team23/typing-arguments"
15
+
16
+ [project.optional-dependencies]
17
+ pydantic = [
18
+ "pydantic>=2.0.0,<3.0.0",
19
+ ]
20
+
21
+ [dependency-groups]
22
+ dev = [
23
+ "pydantic>=2.10.1",
24
+ "pyright>=1.1.389",
25
+ "pytest>=8.3.3",
26
+ "pytest-cov>=6.0.0",
27
+ "ruff>=0.8.0",
28
+ "tox>=4.23.2",
29
+ ]
30
+
31
+ [build-system]
32
+ requires = ["hatchling"]
33
+ build-backend = "hatchling.build"
File without changes
@@ -0,0 +1,161 @@
1
+ from typing import ClassVar, Generic, TypeVar
2
+
3
+ import pytest
4
+ from pydantic import BaseModel
5
+
6
+ from typing_arguments import GenericArgumentsMixin, typing_arg
7
+
8
+ T1 = TypeVar("T1")
9
+ T2 = TypeVar("T2")
10
+
11
+
12
+ class PlainGeneric(GenericArgumentsMixin, Generic[T1, T2]):
13
+ t1 = typing_arg(T1)
14
+ t2 = typing_arg(T2)
15
+
16
+
17
+ class PlainGenericChild(PlainGeneric[str, int]):
18
+ pass
19
+
20
+
21
+ class PlainGenericGrandChild(PlainGenericChild):
22
+ pass
23
+
24
+
25
+ def test_plain_generic():
26
+ type_alias = PlainGeneric[str, int]
27
+
28
+ assert type_alias.__typing_arguments__ == {T1: str, T2: int}
29
+ assert type_alias.t1 is str
30
+ assert type_alias.t2 is int
31
+ assert type_alias().t1 is str
32
+ assert type_alias().t2 is int
33
+
34
+
35
+ def test_plain_generic_raises_exception_if_not_typed():
36
+ assert PlainGeneric.__typing_arguments__ == {}
37
+ with pytest.raises(TypeError):
38
+ PlainGeneric.t1
39
+ with pytest.raises(TypeError):
40
+ PlainGeneric.t2
41
+ with pytest.raises(TypeError):
42
+ PlainGeneric().t1
43
+ with pytest.raises(TypeError):
44
+ PlainGeneric().t2
45
+
46
+
47
+ def test_plain_generic_raises_exception_if_base_class_is_not_generic():
48
+ class NotGeneric(GenericArgumentsMixin):
49
+ pass
50
+
51
+ with pytest.raises(TypeError):
52
+ NotGeneric[str, int]
53
+
54
+
55
+ def test_plain_generic_raises_exception_if_you_try_to_pass_typevars_to_mixin():
56
+ with pytest.raises(TypeError):
57
+ class TypeVarsPassedToMixin(
58
+ GenericArgumentsMixin[T1, T2],
59
+ Generic[T1, T2],
60
+ ):
61
+ pass
62
+
63
+
64
+ def test_plain_generic_raises_exception_if_you_miss_generic_base_class():
65
+ class GenericWithoutBase(
66
+ GenericArgumentsMixin,
67
+ ):
68
+ pass
69
+
70
+ with pytest.raises(TypeError):
71
+ GenericWithoutBase[str, int]
72
+
73
+
74
+ def test_plain_generic_raises_exception_if_wrong_arguments_count():
75
+ with pytest.raises(TypeError):
76
+ PlainGeneric[str]
77
+ with pytest.raises(TypeError):
78
+ PlainGeneric[str, int, str]
79
+
80
+
81
+ def test_plain_generic_raises_exception_if_using_typing_arg_on_non_mixin_class():
82
+ class Something(Generic[T1]):
83
+ t1 = typing_arg(T1)
84
+
85
+ with pytest.raises(TypeError):
86
+ Something[str].t1
87
+
88
+
89
+ def test_plain_generic_child():
90
+ assert PlainGenericChild.__typing_arguments__ == {T1: str, T2: int}
91
+ assert PlainGenericChild.t1 is str
92
+ assert PlainGenericChild.t2 is int
93
+ assert PlainGenericChild().t1 is str
94
+ assert PlainGenericChild().t2 is int
95
+
96
+
97
+ def test_plain_generic__grand_child():
98
+ assert PlainGenericGrandChild.__typing_arguments__ == {T1: str, T2: int}
99
+ assert PlainGenericGrandChild.t1 is str
100
+ assert PlainGenericGrandChild.t2 is int
101
+ assert PlainGenericGrandChild().t1 is str
102
+ assert PlainGenericGrandChild().t2 is int
103
+
104
+
105
+ class PydanticModel(GenericArgumentsMixin, BaseModel, Generic[T1, T2]):
106
+ t1: ClassVar = typing_arg(T1)
107
+ t2: ClassVar = typing_arg(T2)
108
+
109
+
110
+ class PydanticModelChild(PydanticModel[str, int]):
111
+ pass
112
+
113
+
114
+ def test_pydantic_model():
115
+ type_alias = PydanticModel[str, int]
116
+
117
+ assert type_alias.__typing_arguments__ == {T1: str, T2: int}
118
+ assert type_alias.t1 is str
119
+ assert type_alias.t2 is int
120
+ assert type_alias().t1 is str
121
+ assert type_alias().t2 is int
122
+
123
+
124
+ def test_pydantic_model_child():
125
+ assert PydanticModelChild.__typing_arguments__ == {T1: str, T2: int}
126
+ assert PydanticModelChild.t1 is str
127
+ assert PydanticModelChild.t2 is int
128
+ assert PydanticModelChild().t1 is str
129
+ assert PydanticModelChild().t2 is int
130
+
131
+
132
+ class Base1(GenericArgumentsMixin, Generic[T1]):
133
+ t1 = typing_arg(T1)
134
+
135
+
136
+ class Base2(GenericArgumentsMixin, Generic[T2]):
137
+ pass
138
+
139
+
140
+ class MultiBaseGeneric(Base1[str], Base2[int]):
141
+ t2 = typing_arg(T2)
142
+
143
+
144
+ class MultiBaseGenericChild(MultiBaseGeneric):
145
+ pass
146
+
147
+
148
+ def test_multi_base():
149
+ assert MultiBaseGeneric.__typing_arguments__ == {T1: str, T2: int}
150
+ assert MultiBaseGeneric.t1 is str
151
+ assert MultiBaseGeneric.t2 is int
152
+ assert MultiBaseGeneric().t1 is str
153
+ assert MultiBaseGeneric().t2 is int
154
+
155
+
156
+ def test_multi_base_child():
157
+ assert MultiBaseGenericChild.__typing_arguments__ == {T1: str, T2: int}
158
+ assert MultiBaseGenericChild.t1 is str
159
+ assert MultiBaseGenericChild.t2 is int
160
+ assert MultiBaseGenericChild().t1 is str
161
+ assert MultiBaseGenericChild().t2 is int
@@ -0,0 +1,13 @@
1
+ [tox]
2
+ isolated_build = True
3
+ envlist =
4
+ py310,
5
+ py311,
6
+ py312,
7
+ py313
8
+
9
+ [testenv]
10
+ deps =
11
+ pytest
12
+ pydantic
13
+ commands = pytest
@@ -0,0 +1,9 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <module type="PYTHON_MODULE" version="4">
3
+ <component name="NewModuleRootManager" inherit-compiler-output="true">
4
+ <exclude-output />
5
+ <content url="file://$MODULE_DIR$" />
6
+ <orderEntry type="jdk" jdkName="Python 3.13 (python-libs:typing-arguments)" jdkType="Python SDK" />
7
+ <orderEntry type="sourceFolder" forTests="false" />
8
+ </component>
9
+ </module>
@@ -0,0 +1,4 @@
1
+ from typing_arguments.generic_arguments import (
2
+ GenericArgumentsMixin as GenericArgumentsMixin,
3
+ typing_arg as typing_arg,
4
+ )
@@ -0,0 +1,235 @@
1
+ """
2
+ Generic method to make type arguments of generic models available in the class.
3
+
4
+ Example:
5
+ -------
6
+ ```python
7
+ T1 = TypeVar("T1")
8
+ T2 = TypeVar("T2", bound="SomeBaseClass")
9
+
10
+
11
+ class Something(
12
+ GenericArgumentsMixin,
13
+ Generic[T1, T2],
14
+ ):
15
+ t1 = typing_arg(T1)
16
+ t2 = typing_arg(T2)
17
+
18
+
19
+ ConcreteClass = Something[str, SomeBaseClassChild]
20
+ ConcreteClass.t1 # str
21
+ ConcreteClass.t2 # SomeBaseClassChild
22
+ ```
23
+
24
+ You may also use this with pydantic models:
25
+ ```python
26
+ T1 = TypeVar("T1")
27
+ T2 = TypeVar("T2", bound="SomeBaseClass")
28
+
29
+
30
+ class SomethingModel(
31
+ GenericArgumentsMixin,
32
+ BaseModel,
33
+ Generic[T1, T2],
34
+ ):
35
+ t1: ClassVar = typing_arg(T1)
36
+ t2: ClassVar = typing_arg(T2)
37
+
38
+
39
+ ConcreteClassModel = SomethingModel[str, SomeBaseClassChild]
40
+ ConcreteClassModel.t1 # str
41
+ ConcreteClassModel.t2 # SomeBaseClassChild
42
+ ```
43
+
44
+ Internally `GenericArgumentsMixin` will create a new attribute `__typing_arguments__`
45
+ inside the class and its instances. This attribute is a dictionary mapping the type
46
+ variables to their concrete types. This is useful if you want to access the type
47
+ arguments in a generic way.
48
+
49
+ The `typing_arg` function is a helper function to make the type arguments available
50
+ in the class and its instances using a nicely named attribute. This is just a
51
+ convenience function, as you can also access the type arguments directly from the
52
+ `__typing_arguments__` attribute.
53
+
54
+ **Note:** If you are using pydantic models you should use the `ClassVar` annotation
55
+ to ensure pydantic will not try to catch and validate the type arguments as normal
56
+ model fields.
57
+
58
+ You may also mix different generic base classes like so:
59
+ ```python
60
+ T1 = TypeVar("T1")
61
+ T2 = TypeVar("T2", bound="SomeBaseClass")
62
+
63
+
64
+ class Base1(
65
+ GenericArgumentsMixin,
66
+ Generic[T1],
67
+ ):
68
+ pass
69
+
70
+
71
+ class Base2(
72
+ GenericArgumentsMixin,
73
+ Generic[T2],
74
+ ):
75
+ t2 = typing_arg(T2)
76
+
77
+
78
+ class Something(
79
+ Base1[str],
80
+ Base2[SomeBaseClassChild],
81
+ ):
82
+ t1 = typing_arg(T1)
83
+
84
+
85
+ Something.t1 # str
86
+ Something.t2 # SomeBaseClassChild
87
+ ```
88
+
89
+ In this example `Base1` and `Base2` are both generic base classes. `Base1` only
90
+ defines a type argument `T1` and `Base2` only defines a type argument `T2`. The
91
+ `Something` class inherits from both `Base1` and `Base2`. Note that `Base1` does
92
+ not define a simple accessor like `t1` using `typing_arg`, while `Base2` does. This
93
+ is not a problem and can be later added by `Something` using `typing_arg` as well.
94
+
95
+ You may encounter issues using the `typing_arg` function when using type validator
96
+ like mypy or your IDE. If so you might need to use `cast` to tell the type checker
97
+ you are sure about what you are doing. For example:
98
+ ```python
99
+ T1 = TypeVar("T1", bound="SomeBaseClass")
100
+
101
+
102
+ class Something(
103
+ GenericArgumentsMixin,
104
+ Generic[T1],
105
+ ):
106
+ t1 = cast(type[SomeBaseClass], typing_arg(T1))
107
+ ```
108
+
109
+ **Note:** You will still need to use `ClassVar` when using pydantic models. This
110
+ might result in using the same type twice (inside `ClassVar` and `cast`).
111
+ """
112
+
113
+ import functools
114
+ import types
115
+ from typing import TYPE_CHECKING, Any, Generic, TypeVar, cast
116
+ from typing import _GenericAlias as TypingGenericAlias # pyright: ignore[reportAttributeAccessIssue]
117
+
118
+ try:
119
+ from pydantic import BaseModel as _PydanticBaseModel # pyright: ignore[reportAssignmentType]
120
+ except ImportError: # pragma: no cover
121
+ # Provide fake pydantic base model
122
+ class _PydanticBaseModel:
123
+ pass
124
+
125
+ GenericTrackerMixinT = TypeVar("GenericTrackerMixinT")
126
+
127
+ TYPING_ATTRIBUTE_NAME = "__typing_arguments__"
128
+
129
+
130
+ class GenericArgumentsMixin:
131
+ if TYPE_CHECKING: # pragma: no cover
132
+ # Same as TYPING_ATTRIBUTE_NAME
133
+ __typing_arguments__: dict[TypeVar, type[Any]]
134
+
135
+ @classmethod
136
+ @functools.lru_cache(maxsize=None, typed=True)
137
+ def __class_getitem__(
138
+ cls: type[GenericTrackerMixinT],
139
+ params: type[Any] | tuple[type[Any], ...],
140
+ ) -> type[Any]:
141
+ if (
142
+ cls is GenericArgumentsMixin
143
+ and params
144
+ ):
145
+ raise TypeError(
146
+ 'Type parameters should be placed on typing.Generic, '
147
+ 'not GenericArgumentsMixin',
148
+ )
149
+ if Generic not in cls.__bases__:
150
+ raise TypeError(
151
+ 'Cannot provide type arguments to a non-generic class, must '
152
+ 'inherit from typing.Generic first',
153
+ )
154
+ if not isinstance(params, tuple):
155
+ params = (params,)
156
+
157
+ base_cls = cls
158
+ if issubclass(cls, _PydanticBaseModel):
159
+ base_cls = super().__class_getitem__(params) # pyright: ignore[reportAttributeAccessIssue]
160
+
161
+ if len(cls.__parameters__) != len(params): # pyright: ignore[reportAttributeAccessIssue]
162
+ raise TypeError(
163
+ f'Type {cls.__name__} expects {len(cls.__parameters__)} ' # pyright: ignore[reportAttributeAccessIssue]
164
+ f'parameters, got {len(params)}',
165
+ )
166
+
167
+ typing_args = dict(zip(cls.__parameters__, params, strict=True)) # pyright: ignore[reportAttributeAccessIssue]
168
+ if hasattr(cls, TYPING_ATTRIBUTE_NAME):
169
+ typing_args = {
170
+ **getattr(cls, TYPING_ATTRIBUTE_NAME),
171
+ **typing_args,
172
+ }
173
+
174
+ typed_cls = cast(
175
+ type[GenericArgumentsMixin],
176
+ types.new_class(
177
+ f"Typed{cls.__name__}",
178
+ (base_cls,),
179
+ {},
180
+ lambda ns: ns.update({TYPING_ATTRIBUTE_NAME: typing_args}),
181
+ ),
182
+ )
183
+
184
+ if issubclass(cls, _PydanticBaseModel):
185
+ return typed_cls
186
+
187
+ typed_alias = TypingGenericAlias(typed_cls, params)
188
+ setattr(typed_alias, TYPING_ATTRIBUTE_NAME, typing_args)
189
+ return typed_alias
190
+
191
+ def __init_subclass__(cls, *args: Any, **kwargs: Any) -> None:
192
+ super().__init_subclass__(*args, **kwargs)
193
+
194
+ if not hasattr(cls, TYPING_ATTRIBUTE_NAME):
195
+ setattr(cls, TYPING_ATTRIBUTE_NAME, {})
196
+
197
+ base_typing_args = {}
198
+ typing_args = getattr(cls, TYPING_ATTRIBUTE_NAME)
199
+ for base in reversed(cls.__bases__):
200
+ if hasattr(base, TYPING_ATTRIBUTE_NAME):
201
+ base_typing_args.update(getattr(base, TYPING_ATTRIBUTE_NAME))
202
+
203
+ setattr(
204
+ cls, TYPING_ATTRIBUTE_NAME, {
205
+ **base_typing_args,
206
+ **typing_args,
207
+ },
208
+ )
209
+
210
+
211
+ class typing_arg: # noqa
212
+ __slots__ = ("type_argument",)
213
+
214
+ def __init__(self, type_argument: TypeVar, /) -> None:
215
+ self.type_argument = type_argument
216
+
217
+ def __get__(
218
+ self,
219
+ obj: GenericArgumentsMixin,
220
+ obj_class: type[GenericArgumentsMixin] | None = None,
221
+ ) -> type[Any]:
222
+ if obj_class is None: # pragma: no cover
223
+ obj_class = obj.__class__
224
+
225
+ if not hasattr(obj_class, TYPING_ATTRIBUTE_NAME):
226
+ raise TypeError(
227
+ f"{obj_class} seems not be be using GenericArgumentsMixin or "
228
+ f"no arguments were provided",
229
+ )
230
+ if self.type_argument not in getattr(obj_class, TYPING_ATTRIBUTE_NAME):
231
+ raise TypeError(
232
+ f"Type argument {self.type_argument} not found in {obj_class}",
233
+ )
234
+
235
+ return getattr(obj_class, TYPING_ATTRIBUTE_NAME)[self.type_argument]
File without changes