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.
Files changed (31) hide show
  1. extra_type_helpers-0.6.1/LICENSE +21 -0
  2. extra_type_helpers-0.6.1/PKG-INFO +36 -0
  3. extra_type_helpers-0.6.1/README.md +5 -0
  4. extra_type_helpers-0.6.1/extra_type_helpers.egg-info/PKG-INFO +36 -0
  5. extra_type_helpers-0.6.1/extra_type_helpers.egg-info/SOURCES.txt +29 -0
  6. extra_type_helpers-0.6.1/extra_type_helpers.egg-info/dependency_links.txt +1 -0
  7. extra_type_helpers-0.6.1/extra_type_helpers.egg-info/top_level.txt +1 -0
  8. extra_type_helpers-0.6.1/extra_types/__init__.py +4 -0
  9. extra_type_helpers-0.6.1/extra_types/py.typed +0 -0
  10. extra_type_helpers-0.6.1/extra_types/type_utils.py +48 -0
  11. extra_type_helpers-0.6.1/extra_types/type_utils_test.py +36 -0
  12. extra_type_helpers-0.6.1/extra_types/type_utils_types.py +43 -0
  13. extra_type_helpers-0.6.1/extra_types/types/__init__.py +6 -0
  14. extra_type_helpers-0.6.1/extra_types/types/__init_test.py +15 -0
  15. extra_type_helpers-0.6.1/extra_types/types/_dynamic.py +41 -0
  16. extra_type_helpers-0.6.1/extra_types/types/char.py +18 -0
  17. extra_type_helpers-0.6.1/extra_types/types/char_test.py +59 -0
  18. extra_type_helpers-0.6.1/extra_types/types/char_types.py +20 -0
  19. extra_type_helpers-0.6.1/extra_types/types/conftest.py +20 -0
  20. extra_type_helpers-0.6.1/extra_types/types/modifiers.py +3 -0
  21. extra_type_helpers-0.6.1/extra_types/types/nat.py +18 -0
  22. extra_type_helpers-0.6.1/extra_types/types/nat_test.py +56 -0
  23. extra_type_helpers-0.6.1/extra_types/types/nat_types.py +27 -0
  24. extra_type_helpers-0.6.1/extra_types/types/pos_int.py +18 -0
  25. extra_type_helpers-0.6.1/extra_types/types/pos_int_test.py +58 -0
  26. extra_type_helpers-0.6.1/extra_types/types/pos_int_types.py +27 -0
  27. extra_type_helpers-0.6.1/extra_types/types/test_utils.py +28 -0
  28. extra_type_helpers-0.6.1/pyproject.toml +23 -0
  29. extra_type_helpers-0.6.1/requirements.txt +0 -0
  30. extra_type_helpers-0.6.1/setup.cfg +4 -0
  31. 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,5 @@
1
+ # Extra Types
2
+
3
+ Types and type utilities for Python.
4
+
5
+ :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,4 @@
1
+ from . import type_utils as type_utils
2
+ from . import types as types
3
+
4
+ __all__ = ["type_utils", "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,6 @@
1
+ from .char import Char
2
+ from .modifiers import Modified, New, Unmodified
3
+ from .nat import Nat
4
+ from .pos_int import PosInt
5
+
6
+ __all__ = ["Char", "Modified", "Nat", "New", "PosInt", "Unmodified"]
@@ -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,3 @@
1
+ type Modified[T] = T
2
+ type Unmodified[T] = T
3
+ type New[T] = T
@@ -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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1 @@
1
+ 0.6.1