extra-type-helpers 0.6.1__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.
- extra_type_helpers-0.6.1/LICENSE +21 -0
- extra_type_helpers-0.6.1/PKG-INFO +36 -0
- extra_type_helpers-0.6.1/README.md +5 -0
- extra_type_helpers-0.6.1/extra_type_helpers.egg-info/PKG-INFO +36 -0
- extra_type_helpers-0.6.1/extra_type_helpers.egg-info/SOURCES.txt +29 -0
- extra_type_helpers-0.6.1/extra_type_helpers.egg-info/dependency_links.txt +1 -0
- extra_type_helpers-0.6.1/extra_type_helpers.egg-info/top_level.txt +1 -0
- extra_type_helpers-0.6.1/extra_types/__init__.py +4 -0
- extra_type_helpers-0.6.1/extra_types/py.typed +0 -0
- extra_type_helpers-0.6.1/extra_types/type_utils.py +48 -0
- extra_type_helpers-0.6.1/extra_types/type_utils_test.py +36 -0
- extra_type_helpers-0.6.1/extra_types/type_utils_types.py +43 -0
- extra_type_helpers-0.6.1/extra_types/types/__init__.py +6 -0
- extra_type_helpers-0.6.1/extra_types/types/__init_test.py +15 -0
- extra_type_helpers-0.6.1/extra_types/types/_dynamic.py +41 -0
- extra_type_helpers-0.6.1/extra_types/types/char.py +18 -0
- extra_type_helpers-0.6.1/extra_types/types/char_test.py +59 -0
- extra_type_helpers-0.6.1/extra_types/types/char_types.py +20 -0
- extra_type_helpers-0.6.1/extra_types/types/conftest.py +20 -0
- extra_type_helpers-0.6.1/extra_types/types/modifiers.py +3 -0
- extra_type_helpers-0.6.1/extra_types/types/nat.py +18 -0
- extra_type_helpers-0.6.1/extra_types/types/nat_test.py +56 -0
- extra_type_helpers-0.6.1/extra_types/types/nat_types.py +27 -0
- extra_type_helpers-0.6.1/extra_types/types/pos_int.py +18 -0
- extra_type_helpers-0.6.1/extra_types/types/pos_int_test.py +58 -0
- extra_type_helpers-0.6.1/extra_types/types/pos_int_types.py +27 -0
- extra_type_helpers-0.6.1/extra_types/types/test_utils.py +28 -0
- extra_type_helpers-0.6.1/pyproject.toml +23 -0
- extra_type_helpers-0.6.1/requirements.txt +0 -0
- extra_type_helpers-0.6.1/setup.cfg +4 -0
- extra_type_helpers-0.6.1/version.txt +1 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 George Ogden
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: extra-type-helpers
|
|
3
|
+
Version: 0.6.1
|
|
4
|
+
Author: George Ogden
|
|
5
|
+
License: MIT License
|
|
6
|
+
|
|
7
|
+
Copyright (c) 2025 George Ogden
|
|
8
|
+
|
|
9
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
10
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
11
|
+
in the Software without restriction, including without limitation the rights
|
|
12
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
13
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
14
|
+
furnished to do so, subject to the following conditions:
|
|
15
|
+
|
|
16
|
+
The above copyright notice and this permission notice shall be included in all
|
|
17
|
+
copies or substantial portions of the Software.
|
|
18
|
+
|
|
19
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
20
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
21
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
22
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
23
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
24
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
25
|
+
SOFTWARE.
|
|
26
|
+
|
|
27
|
+
Requires-Python: <3.15,>=3.12
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
License-File: LICENSE
|
|
30
|
+
Dynamic: license-file
|
|
31
|
+
|
|
32
|
+
# Extra Types
|
|
33
|
+
|
|
34
|
+
Types and type utilities for Python.
|
|
35
|
+
|
|
36
|
+
:construction: Under construction (use at your own peril)
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: extra-type-helpers
|
|
3
|
+
Version: 0.6.1
|
|
4
|
+
Author: George Ogden
|
|
5
|
+
License: MIT License
|
|
6
|
+
|
|
7
|
+
Copyright (c) 2025 George Ogden
|
|
8
|
+
|
|
9
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
10
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
11
|
+
in the Software without restriction, including without limitation the rights
|
|
12
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
13
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
14
|
+
furnished to do so, subject to the following conditions:
|
|
15
|
+
|
|
16
|
+
The above copyright notice and this permission notice shall be included in all
|
|
17
|
+
copies or substantial portions of the Software.
|
|
18
|
+
|
|
19
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
20
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
21
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
22
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
23
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
24
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
25
|
+
SOFTWARE.
|
|
26
|
+
|
|
27
|
+
Requires-Python: <3.15,>=3.12
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
License-File: LICENSE
|
|
30
|
+
Dynamic: license-file
|
|
31
|
+
|
|
32
|
+
# Extra Types
|
|
33
|
+
|
|
34
|
+
Types and type utilities for Python.
|
|
35
|
+
|
|
36
|
+
:construction: Under construction (use at your own peril)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
requirements.txt
|
|
5
|
+
version.txt
|
|
6
|
+
extra_type_helpers.egg-info/PKG-INFO
|
|
7
|
+
extra_type_helpers.egg-info/SOURCES.txt
|
|
8
|
+
extra_type_helpers.egg-info/dependency_links.txt
|
|
9
|
+
extra_type_helpers.egg-info/top_level.txt
|
|
10
|
+
extra_types/__init__.py
|
|
11
|
+
extra_types/py.typed
|
|
12
|
+
extra_types/type_utils.py
|
|
13
|
+
extra_types/type_utils_test.py
|
|
14
|
+
extra_types/type_utils_types.py
|
|
15
|
+
extra_types/types/__init__.py
|
|
16
|
+
extra_types/types/__init_test.py
|
|
17
|
+
extra_types/types/_dynamic.py
|
|
18
|
+
extra_types/types/char.py
|
|
19
|
+
extra_types/types/char_test.py
|
|
20
|
+
extra_types/types/char_types.py
|
|
21
|
+
extra_types/types/conftest.py
|
|
22
|
+
extra_types/types/modifiers.py
|
|
23
|
+
extra_types/types/nat.py
|
|
24
|
+
extra_types/types/nat_test.py
|
|
25
|
+
extra_types/types/nat_types.py
|
|
26
|
+
extra_types/types/pos_int.py
|
|
27
|
+
extra_types/types/pos_int_test.py
|
|
28
|
+
extra_types/types/pos_int_types.py
|
|
29
|
+
extra_types/types/test_utils.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
extra_types
|
|
File without changes
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
from typing import Any, cast, overload
|
|
2
|
+
|
|
3
|
+
from .types import Unmodified
|
|
4
|
+
|
|
5
|
+
__all__ = ["strict_cast", "strict_not_none"]
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@overload
|
|
9
|
+
def strict_cast[T](typ: type[T], expr: Any, /) -> T: ...
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@overload
|
|
13
|
+
def strict_cast(typ: None, expr: Any, /) -> None: ...
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@overload
|
|
17
|
+
def strict_cast(typ: object, expr: Any, /) -> Any: ...
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def strict_cast[T](typ: object, expr: T, /) -> Unmodified[T]:
|
|
21
|
+
"""
|
|
22
|
+
Determine whether an object is of a given type at runtime.
|
|
23
|
+
This method is currently very limited in its ability to express types;
|
|
24
|
+
as a result, it will produce False positives, such as with Literal types.
|
|
25
|
+
If the type does not match, this raises a TypeError.
|
|
26
|
+
"""
|
|
27
|
+
if not _dynamic_type_check(typ, expr):
|
|
28
|
+
raise TypeError(f"{expr} is not an instance of {typ}")
|
|
29
|
+
return expr
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _dynamic_type_check(typ: object, expr: Any, /) -> bool:
|
|
33
|
+
if typ is None:
|
|
34
|
+
return expr is None
|
|
35
|
+
try:
|
|
36
|
+
return isinstance(expr, cast(type, typ))
|
|
37
|
+
except TypeError:
|
|
38
|
+
return True
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def strict_not_none[T](expr: T | None, /) -> Unmodified[T]:
|
|
42
|
+
"""
|
|
43
|
+
Check that an expression is not None.
|
|
44
|
+
If it is None, this raises a TypeError.
|
|
45
|
+
"""
|
|
46
|
+
if expr is None:
|
|
47
|
+
raise TypeError(f"{expr} is {None}")
|
|
48
|
+
return expr
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import contextlib
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from .type_utils import strict_cast, strict_not_none
|
|
6
|
+
from .types import Char
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@pytest.mark.parametrize(
|
|
10
|
+
"typ, expr, passes",
|
|
11
|
+
[
|
|
12
|
+
(int, 5, True),
|
|
13
|
+
(int | str, 5, True),
|
|
14
|
+
(str | None, 5, False),
|
|
15
|
+
(str, 5, False),
|
|
16
|
+
(None, 5, False),
|
|
17
|
+
(int | str, None, False),
|
|
18
|
+
(str | None, "abc", True),
|
|
19
|
+
(Char, "a", True),
|
|
20
|
+
(Char, "ab", False),
|
|
21
|
+
(None, "ab", False),
|
|
22
|
+
(None, None, True),
|
|
23
|
+
(Char | None, "a", True),
|
|
24
|
+
(Char | None, "ab", False),
|
|
25
|
+
(Char | None, None, True),
|
|
26
|
+
],
|
|
27
|
+
)
|
|
28
|
+
def test_strict_cast(typ: object, expr: object, passes: bool) -> None:
|
|
29
|
+
with contextlib.nullcontext() if passes else pytest.raises(TypeError):
|
|
30
|
+
assert strict_cast(typ, expr) is expr
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@pytest.mark.parametrize("obj", [5, None, "abc", True])
|
|
34
|
+
def test_strict_not_none(obj: object) -> None:
|
|
35
|
+
with pytest.raises(TypeError) if obj is None else contextlib.nullcontext():
|
|
36
|
+
assert strict_not_none(obj) is obj
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
from typing import Literal, cast
|
|
2
|
+
|
|
3
|
+
from .type_utils import strict_cast, strict_not_none
|
|
4
|
+
|
|
5
|
+
cast2 = cast
|
|
6
|
+
|
|
7
|
+
a: int | None = 5
|
|
8
|
+
b = strict_cast(int, a)
|
|
9
|
+
b + 2
|
|
10
|
+
b + "oops" # type: ignore [operator]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
c: int | float | None = 5
|
|
14
|
+
d = strict_cast(int | float, c)
|
|
15
|
+
d + 2
|
|
16
|
+
d + "oops" # type: ignore [operator]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
e = strict_cast(Literal[5], 10)
|
|
20
|
+
e + 4
|
|
21
|
+
|
|
22
|
+
f = strict_cast(None, 10)
|
|
23
|
+
f + "oops" # type: ignore [operator]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
g = strict_cast((int, str), 10)
|
|
27
|
+
g + () # noqa: RUF005
|
|
28
|
+
g + 5
|
|
29
|
+
|
|
30
|
+
h: int | None = 5
|
|
31
|
+
i = strict_not_none(h)
|
|
32
|
+
i + 4
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
j: list[str] | str | None = ["a"]
|
|
36
|
+
k = strict_not_none(j)
|
|
37
|
+
k + 1 # type: ignore [operator]
|
|
38
|
+
k[0] + 2 # type: ignore [operator]
|
|
39
|
+
k[0] + "3"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
l: None = None
|
|
43
|
+
m = strict_not_none(l) # type: ignore [var-annotated]
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import itertools
|
|
2
|
+
|
|
3
|
+
from .. import types
|
|
4
|
+
from ._dynamic import DynamicInstantiation
|
|
5
|
+
from .conftest import mcls_instances
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def test_init_all() -> None:
|
|
9
|
+
for instance in itertools.chain(mcls_instances, DynamicInstantiation.__subclasses__()):
|
|
10
|
+
assert instance.__name__ in types.__all__
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def test_init_instances() -> None:
|
|
14
|
+
for instance in itertools.chain(mcls_instances, DynamicInstantiation.__subclasses__()):
|
|
15
|
+
assert getattr(types, instance.__name__) is instance
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import abc
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class DynamicCheck(type, abc.ABC):
|
|
6
|
+
def __new__(
|
|
7
|
+
mcls: type[type],
|
|
8
|
+
name: str,
|
|
9
|
+
bases: tuple[type, ...],
|
|
10
|
+
namespace: dict[str, Any],
|
|
11
|
+
/,
|
|
12
|
+
**kwargs: Any,
|
|
13
|
+
) -> type:
|
|
14
|
+
assert "_is_instance" in namespace or any(hasattr(base, "_is_instance") for base in bases)
|
|
15
|
+
assert "_is_subclass" in namespace or any(hasattr(base, "_is_instance") for base in bases)
|
|
16
|
+
return type.__new__(mcls, name, bases, namespace, **kwargs)
|
|
17
|
+
|
|
18
|
+
def __instancecheck__(cls, instance: object) -> bool:
|
|
19
|
+
return cls._is_instance(instance)
|
|
20
|
+
|
|
21
|
+
@classmethod
|
|
22
|
+
@abc.abstractmethod
|
|
23
|
+
def _is_instance(cls, instance: object) -> bool: ...
|
|
24
|
+
|
|
25
|
+
def __subclasscheck__(cls, subclass: type) -> bool:
|
|
26
|
+
return cls._is_subclass(subclass)
|
|
27
|
+
|
|
28
|
+
@classmethod
|
|
29
|
+
@abc.abstractmethod
|
|
30
|
+
def _is_subclass(cls, instance: object) -> bool: ...
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class DynamicInstantiation:
|
|
34
|
+
def __new__(cls, *args: Any, **kwargs: Any) -> Any:
|
|
35
|
+
[base_cls, *_] = (
|
|
36
|
+
base_cls for base_cls in cls.__bases__ if not issubclass(base_cls, DynamicInstantiation)
|
|
37
|
+
)
|
|
38
|
+
obj = super().__new__(base_cls, *args, **kwargs)
|
|
39
|
+
if not isinstance(obj, cls):
|
|
40
|
+
raise TypeError(f"{obj} is not an instance of {cls.__name__}")
|
|
41
|
+
return obj
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from typing import TYPE_CHECKING, cast
|
|
2
|
+
|
|
3
|
+
from ._dynamic import DynamicCheck, DynamicInstantiation
|
|
4
|
+
|
|
5
|
+
if TYPE_CHECKING:
|
|
6
|
+
Char = str
|
|
7
|
+
else:
|
|
8
|
+
|
|
9
|
+
class Char(DynamicInstantiation, str, metaclass=DynamicCheck):
|
|
10
|
+
"""A character type representing strings of length one."""
|
|
11
|
+
|
|
12
|
+
@classmethod
|
|
13
|
+
def _is_instance(cls, instance: object) -> bool:
|
|
14
|
+
return issubclass(type(instance), cls) and len(cast(str, instance)) == 1
|
|
15
|
+
|
|
16
|
+
@classmethod
|
|
17
|
+
def _is_subclass(cls, sub_cls: type) -> bool:
|
|
18
|
+
return sub_cls is str
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
from collections.abc import Sequence
|
|
2
|
+
import enum
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
from . import Char
|
|
8
|
+
from .test_utils import instantiation_test_body, isinstance_test_body, issubclass_test_body
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Bull(enum.IntEnum):
|
|
12
|
+
FAWS = "0"
|
|
13
|
+
TWOO = "1"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@pytest.mark.parametrize(
|
|
17
|
+
"typ, expected", [(str, True), (list, False), (Sequence, False), (Bull, False)]
|
|
18
|
+
)
|
|
19
|
+
def test_issubclass(typ: type, expected: bool) -> None:
|
|
20
|
+
issubclass_test_body(Char, typ, expected)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@pytest.mark.parametrize(
|
|
24
|
+
"obj, expected",
|
|
25
|
+
[
|
|
26
|
+
("0", True),
|
|
27
|
+
("\0", True),
|
|
28
|
+
("", False),
|
|
29
|
+
("aa", False),
|
|
30
|
+
("ab", False),
|
|
31
|
+
(["a"], False),
|
|
32
|
+
(Bull.FAWS, False),
|
|
33
|
+
(Bull.TWOO, False),
|
|
34
|
+
],
|
|
35
|
+
)
|
|
36
|
+
def test_isinstance(obj: object, expected: bool) -> None:
|
|
37
|
+
isinstance_test_body(Char, obj, expected)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class Invisible:
|
|
41
|
+
def __str__(self) -> str:
|
|
42
|
+
return ""
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@pytest.mark.parametrize(
|
|
46
|
+
"arg, expected",
|
|
47
|
+
[
|
|
48
|
+
(1, "1"),
|
|
49
|
+
("a", "a"),
|
|
50
|
+
("aa", TypeError),
|
|
51
|
+
(10, TypeError),
|
|
52
|
+
(Invisible(), TypeError),
|
|
53
|
+
(b"ab", TypeError),
|
|
54
|
+
(Bull.FAWS, "0"),
|
|
55
|
+
(Bull.TWOO, "1"),
|
|
56
|
+
],
|
|
57
|
+
)
|
|
58
|
+
def test_instantiation(arg: Any, expected: object | type[BaseException]) -> None:
|
|
59
|
+
instantiation_test_body(Char, arg, expected)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from . import Char
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def increment(c: Char) -> Char:
|
|
5
|
+
return chr(ord(c) + 1)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
increment("A")
|
|
9
|
+
increment(Char(b"A"))
|
|
10
|
+
increment(1) # type: ignore [arg-type]
|
|
11
|
+
increment(b"A") # type: ignore [arg-type]
|
|
12
|
+
|
|
13
|
+
assert isinstance(Char("0"), Char)
|
|
14
|
+
assert isinstance("0", Char)
|
|
15
|
+
assert isinstance("00", Char)
|
|
16
|
+
assert isinstance("001", Char)
|
|
17
|
+
|
|
18
|
+
Char("a") + Char("b")
|
|
19
|
+
"abracadabr" + Char("a")
|
|
20
|
+
4 + Char("a") # type: ignore [operator]
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from debug import install
|
|
4
|
+
|
|
5
|
+
from ._dynamic import DynamicCheck
|
|
6
|
+
|
|
7
|
+
install()
|
|
8
|
+
|
|
9
|
+
original_new = DynamicCheck.__new__
|
|
10
|
+
|
|
11
|
+
mcls_instances = []
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def mock_new(*args: Any, **kwargs: Any) -> Any:
|
|
15
|
+
cls = original_new(*args, **kwargs)
|
|
16
|
+
mcls_instances.append(cls)
|
|
17
|
+
return cls
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
DynamicCheck.__new__ = mock_new # type: ignore [method-assign]
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from typing import TYPE_CHECKING
|
|
2
|
+
|
|
3
|
+
from ._dynamic import DynamicCheck, DynamicInstantiation
|
|
4
|
+
|
|
5
|
+
if TYPE_CHECKING:
|
|
6
|
+
Nat = int
|
|
7
|
+
else:
|
|
8
|
+
|
|
9
|
+
class Nat(DynamicInstantiation, int, metaclass=DynamicCheck):
|
|
10
|
+
"""A natural number type representing integers greater than or equal to zero."""
|
|
11
|
+
|
|
12
|
+
@classmethod
|
|
13
|
+
def _is_instance(cls, instance: object) -> bool:
|
|
14
|
+
return issubclass(type(instance), cls) and instance >= 0
|
|
15
|
+
|
|
16
|
+
@classmethod
|
|
17
|
+
def _is_subclass(cls, sub_cls: type) -> bool:
|
|
18
|
+
return sub_cls is int
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import enum
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
import pytest
|
|
5
|
+
|
|
6
|
+
from . import Nat
|
|
7
|
+
from .test_utils import Negated, instantiation_test_body, isinstance_test_body, issubclass_test_body
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Bull(enum.IntEnum):
|
|
11
|
+
FAWS = 0
|
|
12
|
+
TWOO = 1
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@pytest.mark.parametrize(
|
|
16
|
+
"typ, expected", [(int, True), (float, False), (bool, False), (Bull, False)]
|
|
17
|
+
)
|
|
18
|
+
def test_issubclass(typ: type, expected: bool) -> None:
|
|
19
|
+
issubclass_test_body(Nat, typ, expected)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@pytest.mark.parametrize(
|
|
23
|
+
"obj, expected",
|
|
24
|
+
[
|
|
25
|
+
(1, True),
|
|
26
|
+
(0, True),
|
|
27
|
+
(-1, False),
|
|
28
|
+
(1.5, False),
|
|
29
|
+
("1", False),
|
|
30
|
+
(Bull.FAWS, False),
|
|
31
|
+
(Bull.TWOO, False),
|
|
32
|
+
],
|
|
33
|
+
)
|
|
34
|
+
def test_isinstance(obj: object, expected: bool) -> None:
|
|
35
|
+
isinstance_test_body(Nat, obj, expected)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@pytest.mark.parametrize(
|
|
39
|
+
"arg, expected",
|
|
40
|
+
[
|
|
41
|
+
(1, 1),
|
|
42
|
+
("1", 1),
|
|
43
|
+
(-1, TypeError),
|
|
44
|
+
("-1", TypeError),
|
|
45
|
+
("0xff", ValueError),
|
|
46
|
+
(False, 0),
|
|
47
|
+
(None, TypeError),
|
|
48
|
+
(Bull.FAWS, 0),
|
|
49
|
+
(Bull.TWOO, 1),
|
|
50
|
+
(Negated(1), TypeError),
|
|
51
|
+
(Negated(-2), 2),
|
|
52
|
+
(Negated(0), 0),
|
|
53
|
+
],
|
|
54
|
+
)
|
|
55
|
+
def test_instantiation(arg: Any, expected: object | type[BaseException]) -> None:
|
|
56
|
+
instantiation_test_body(Nat, arg, expected)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from . import Nat
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def succ(x: Nat, /) -> Nat:
|
|
5
|
+
return x + 1
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def zero() -> Nat:
|
|
9
|
+
return 0
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
succ(succ(zero()))
|
|
13
|
+
|
|
14
|
+
succ(
|
|
15
|
+
succ(
|
|
16
|
+
0.0 # type: ignore [arg-type]
|
|
17
|
+
)
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
assert isinstance(Nat(0), Nat)
|
|
21
|
+
assert isinstance(0, Nat)
|
|
22
|
+
assert isinstance(1, Nat)
|
|
23
|
+
assert isinstance(-1, Nat)
|
|
24
|
+
|
|
25
|
+
Nat(5) % Nat(3)
|
|
26
|
+
succ(succ(Nat(3)) // Nat(2))
|
|
27
|
+
"value: " + Nat(5) # type: ignore [operator]
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from typing import TYPE_CHECKING
|
|
2
|
+
|
|
3
|
+
from ._dynamic import DynamicCheck, DynamicInstantiation
|
|
4
|
+
|
|
5
|
+
if TYPE_CHECKING:
|
|
6
|
+
PosInt = int
|
|
7
|
+
else:
|
|
8
|
+
|
|
9
|
+
class PosInt(DynamicInstantiation, int, metaclass=DynamicCheck):
|
|
10
|
+
"""A positive number type representing integers strictly greater than zero."""
|
|
11
|
+
|
|
12
|
+
@classmethod
|
|
13
|
+
def _is_instance(cls, instance: object) -> bool:
|
|
14
|
+
return issubclass(type(instance), cls) and instance > 0
|
|
15
|
+
|
|
16
|
+
@classmethod
|
|
17
|
+
def _is_subclass(cls, sub_cls: type) -> bool:
|
|
18
|
+
return sub_cls is int
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import enum
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
import pytest
|
|
5
|
+
|
|
6
|
+
from . import PosInt
|
|
7
|
+
from .test_utils import Negated, instantiation_test_body, isinstance_test_body, issubclass_test_body
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Bull(enum.IntEnum):
|
|
11
|
+
FAWS = 0
|
|
12
|
+
TWOO = 1
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@pytest.mark.parametrize(
|
|
16
|
+
"typ, expected", [(int, True), (float, False), (bool, False), (Bull, False)]
|
|
17
|
+
)
|
|
18
|
+
def test_issubclass(typ: type, expected: bool) -> None:
|
|
19
|
+
issubclass_test_body(PosInt, typ, expected)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@pytest.mark.parametrize(
|
|
23
|
+
"obj, expected",
|
|
24
|
+
[
|
|
25
|
+
(1, True),
|
|
26
|
+
(2, True),
|
|
27
|
+
(998244353, True),
|
|
28
|
+
(0, False),
|
|
29
|
+
(-1, False),
|
|
30
|
+
(1.5, False),
|
|
31
|
+
("1", False),
|
|
32
|
+
(Bull.FAWS, False),
|
|
33
|
+
(Bull.TWOO, False),
|
|
34
|
+
],
|
|
35
|
+
)
|
|
36
|
+
def test_isinstance(obj: object, expected: bool) -> None:
|
|
37
|
+
isinstance_test_body(PosInt, obj, expected)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@pytest.mark.parametrize(
|
|
41
|
+
"arg, expected",
|
|
42
|
+
[
|
|
43
|
+
(1, 1),
|
|
44
|
+
("1", 1),
|
|
45
|
+
(-1, TypeError),
|
|
46
|
+
("-1", TypeError),
|
|
47
|
+
("0xff", ValueError),
|
|
48
|
+
(False, TypeError),
|
|
49
|
+
(None, TypeError),
|
|
50
|
+
(Bull.FAWS, TypeError),
|
|
51
|
+
(Bull.TWOO, 1),
|
|
52
|
+
(Negated(1), TypeError),
|
|
53
|
+
(Negated(-2), 2),
|
|
54
|
+
(Negated(0), TypeError),
|
|
55
|
+
],
|
|
56
|
+
)
|
|
57
|
+
def test_instantiation(arg: Any, expected: object | type[BaseException]) -> None:
|
|
58
|
+
instantiation_test_body(PosInt, arg, expected)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from . import PosInt
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def succ(x: PosInt, /) -> PosInt:
|
|
5
|
+
return x + 1
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def one() -> PosInt:
|
|
9
|
+
return 1
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
succ(succ(one()))
|
|
13
|
+
|
|
14
|
+
succ(
|
|
15
|
+
succ(
|
|
16
|
+
0.0 # type: ignore [arg-type]
|
|
17
|
+
)
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
assert isinstance(PosInt(0), PosInt)
|
|
21
|
+
assert isinstance(0, PosInt)
|
|
22
|
+
assert isinstance(1, PosInt)
|
|
23
|
+
assert isinstance(-1, PosInt)
|
|
24
|
+
|
|
25
|
+
PosInt(5) % PosInt(3)
|
|
26
|
+
succ(succ(PosInt(3)) // PosInt(2))
|
|
27
|
+
"value: " + PosInt(5) # type: ignore [operator]
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import contextlib
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def issubclass_test_body(superclass: type, subclass: type, expected: bool) -> None:
|
|
7
|
+
assert issubclass(subclass, superclass) == expected
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def isinstance_test_body(typ: type, obj: object, expected: bool) -> None:
|
|
11
|
+
assert isinstance(obj, typ) == expected
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def instantiation_test_body(typ: type, arg: object, expected: object | type[BaseException]) -> None:
|
|
15
|
+
with (
|
|
16
|
+
pytest.raises(expected)
|
|
17
|
+
if isinstance(expected, type) and issubclass(expected, BaseException)
|
|
18
|
+
else contextlib.nullcontext()
|
|
19
|
+
):
|
|
20
|
+
assert typ(arg) == expected
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class Negated:
|
|
24
|
+
def __init__(self, x: int, /) -> None:
|
|
25
|
+
self.x = x
|
|
26
|
+
|
|
27
|
+
def __int__(self) -> int:
|
|
28
|
+
return -self.x
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "extra-type-helpers"
|
|
3
|
+
requires-python = ">=3.12,<3.15"
|
|
4
|
+
authors = [{name = "George Ogden"}]
|
|
5
|
+
license = {file = "LICENSE"}
|
|
6
|
+
readme = "README.md"
|
|
7
|
+
dynamic = ["dependencies", "version"]
|
|
8
|
+
|
|
9
|
+
[tool.setuptools.dynamic]
|
|
10
|
+
dependencies = {file = ["requirements.txt"]}
|
|
11
|
+
version = {file = ["version.txt"]}
|
|
12
|
+
|
|
13
|
+
[build-system]
|
|
14
|
+
requires = ["setuptools"]
|
|
15
|
+
build-backend = "setuptools.build_meta"
|
|
16
|
+
|
|
17
|
+
[tool.setuptools.packages.find]
|
|
18
|
+
where = ["."]
|
|
19
|
+
include = ["extra_types*"]
|
|
20
|
+
exclude = ["test*"]
|
|
21
|
+
|
|
22
|
+
[tool.setuptools.package-data]
|
|
23
|
+
"extra_types" = ["py.typed"]
|
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.6.1
|