diff --git a/basedtyping/__init__.py b/basedtyping/__init__.py index 47b2964..209e093 100644 --- a/basedtyping/__init__.py +++ b/basedtyping/__init__.py @@ -11,6 +11,7 @@ Sequence, TypeVar, _GenericAlias, + _SpecialForm, cast, ) @@ -36,8 +37,12 @@ Fn = TypeVar("Fn", bound=Function) +Never = NoReturn +"""A value that can never exist. This is the narrowest possible form.""" + + class _ReifiedGenericAlias(_GenericAlias, _root=True): - def __call__(self, *args: NoReturn, **kwargs: NoReturn) -> _ReifiedGenericMetaclass: + def __call__(self, *args: object, **kwargs: object) -> _ReifiedGenericMetaclass: """Copied from ``super().__call__`` but modified to call ``type.__call__`` instead of ``__origin__.__call__``, and throw an error if there are any TypeVars """ @@ -149,7 +154,7 @@ class NotReifiedParameterError(ReifiedGenericError): class _ReifiedGenericMetaclass(type, OrigClass): - def __call__(cls, *args: NoReturn, **kwargs: NoReturn) -> object: + def __call__(cls, *args: object, **kwargs: object) -> object: """A placeholder ``__call__`` method that gets called when the class is instantiated directly, instead of first supplying the type parameters. """ @@ -213,8 +218,10 @@ def __class_getitem__(cls, item) -> type[ReifiedGeneric[T]]: # TODO: make this work with any "form", not just unions # should be (form: TypeForm, forminfo: TypeForm) -def issubform(form: type | UnionType, forminfo: type | UnionType) -> bool: - """EXPERIMENTAL: Warning, this function currently only supports unions. +def issubform( + form: type | UnionType | _SpecialForm, forminfo: type | UnionType | _SpecialForm +) -> bool: + """EXPERIMENTAL: Warning, this function currently only supports unions and ``Never``. Returns ``True`` if ``form`` is a subform (specialform or subclass) of ``forminfo``. @@ -231,9 +238,14 @@ def issubform(form: type | UnionType, forminfo: type | UnionType) -> bool: >>> issubform(int | str, object) True """ + # type ignores because issubclass doesn't support _SpecialForm, but we do if isinstance(form, UnionType | OldUnionType): for t in cast(Sequence[type], cast(UnionType, form).__args__): - if not issubclass(t, forminfo): + if not issubform(t, forminfo): return False return True - return issubclass(form, forminfo) + if form is Never: + return True + if forminfo is Never: + return False + return issubclass(form, forminfo) # type: ignore[arg-type] diff --git a/pyproject.toml b/pyproject.toml index 5e344a1..b8d130d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,14 +39,16 @@ disable = [ "missing-function-docstring", "missing-module-docstring", "missing-class-docstring", - "invalid-name", # https://github.com/PyCQA/pylint/issues/3401 + "invalid-name", # https://github.com/PyCQA/pylint/issues/3401 "fixme", - "useless-import-alias", # required when using no-implicit-reexport in mypy - "no-member", # handled by mypy - "inherit-non-class", # handled by mypy, false positive on ReifiedGeneric - "too-few-public-methods", - "unused-import", # false negatives + handled by flake8 - "duplicate-code", # https://github.com/PyCQA/pylint/issues/214 + "useless-import-alias", # required when using no-implicit-reexport in mypy + "no-member", # handled by mypy + "inherit-non-class", # handled by mypy, false positive on ReifiedGeneric + "too-few-public-methods", # didn't ask + ratio + "unused-import", # false negatives + handled by flake8 + "duplicate-code", # https://github.com/PyCQA/pylint/issues/214 + "isinstance-second-argument-not-valid-type", # false positive on Never type, handled by mypy + "no-value-for-parameter", # handled by mypy ] enable = ["useless-suppression", "deprecated-pragma"] diff --git a/tests/test_never_type/test_runtime.py b/tests/test_never_type/test_runtime.py new file mode 100644 index 0000000..8cb7d0b --- /dev/null +++ b/tests/test_never_type/test_runtime.py @@ -0,0 +1,44 @@ +from pytest import mark, raises + +from basedtyping import Never, issubform + +# type ignores due to # https://github.com/KotlinIsland/basedmypy/issues/136 + + +@mark.xfail # https://github.com/KotlinIsland/basedtyping/issues/22 +def test_isinstance() -> None: + assert not isinstance( # type: ignore[misc] + 1, + Never, # type: ignore[arg-type] + ) + + +def test_issubform_true() -> None: + assert issubform(Never, int) # type: ignore[arg-type] + + +def test_issubform_false() -> None: + assert not issubform(str, Never) # type: ignore[arg-type] + + +def test_issubform_never_is_never() -> None: + assert issubform(Never, Never) # type: ignore[arg-type] + + +def test_issubclass() -> None: + with raises(TypeError): + assert issubclass( # type: ignore[misc] + int, Never # type: ignore[arg-type] + ) + + +def test_cant_instantiate() -> None: + with raises(TypeError): + Never() # type: ignore[operator] + + +def test_cant_subtype() -> None: + with raises(TypeError): + + class _SubNever(Never): # type: ignore[misc] + pass diff --git a/tests/test_never_type/test_typetime.py b/tests/test_never_type/test_typetime.py new file mode 100644 index 0000000..2fa6be5 --- /dev/null +++ b/tests/test_never_type/test_typetime.py @@ -0,0 +1,33 @@ +from typing import TYPE_CHECKING, NoReturn, cast + +if TYPE_CHECKING: + # these are just type-time tests, not real life pytest tests. they are only run by mypy + + from basedtyping import Never + from basedtyping.typetime_only import assert_type + + def test_never_equals_noreturn() -> None: + # TODO: better way to check if types are equal + assert_type[NoReturn](cast(Never, 1)) + assert_type[Never](cast(NoReturn, 1)) + + def test_valid_type_hint() -> None: + _never: Never + + def test_cant_assign_to_never() -> None: + _never: Never = 1 # type: ignore[assignment] + + def test_cant_subtype() -> None: + class _A(Never): # type: ignore[misc] + ... + + def test_type_never() -> None: + """``type[Never]`` is invalid as ``Never`` is not a ``type``. + + Should actually be: + ``_t: type[Never] # type: ignore[type-var]`` + due to https://github.com/python/mypy/issues/11291 + + So current implementation resembles an xfail. + """ + _t: type[Never] diff --git a/tests/test_reified_generics/test_type_of_types.py b/tests/test_reified_generics/test_type_of_types.py index b2e5a59..b3d6391 100644 --- a/tests/test_reified_generics/test_type_of_types.py +++ b/tests/test_reified_generics/test_type_of_types.py @@ -18,11 +18,11 @@ class Reified(ReifiedGeneric[tuple[T, U]]): def test_instance() -> None: """may be possible once https://github.com/KotlinIsland/basedmypy/issues/24 is resolved""" assert_type[tuple[type[int], type[str]]]( - Reified[int, str]().__orig_class__.__args__ # type:ignore[arg-type] + Reified[int, str]().__orig_class__.__args__ # type: ignore[arg-type] ) def from_class() -> None: """may be possible once https://github.com/python/mypy/issues/11672 is resolved""" assert_type[tuple[type[int], type[str]]]( - Reified[int, str].__args__ # type:ignore[arg-type] + Reified[int, str].__args__ # type: ignore[arg-type] )