From c7a21dc6d4460ce95cf465dfa3b4fff54a099131 Mon Sep 17 00:00:00 2001 From: DetachHead Date: Fri, 7 Jan 2022 00:27:14 +1000 Subject: [PATCH 1/2] add `Never` type --- basedtyping/__init__.py | 24 ++++++++++++----- pyproject.toml | 14 +++++----- tests/test_never_type/test_runtime.py | 37 ++++++++++++++++++++++++++ tests/test_never_type/test_typetime.py | 22 +++++++++++++++ 4 files changed, 85 insertions(+), 12 deletions(-) create mode 100644 tests/test_never_type/test_runtime.py create mode 100644 tests/test_never_type/test_typetime.py diff --git a/basedtyping/__init__.py b/basedtyping/__init__.py index 47b2964..36fc669 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..7b511f0 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 + "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 + "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..0aee782 --- /dev/null +++ b/tests/test_never_type/test_runtime.py @@ -0,0 +1,37 @@ +from pytest import mark, raises + +from basedtyping import Never, issubform + +# type ignore's 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_instanciate() -> None: + with raises(TypeError): + Never() # type:ignore[operator] diff --git a/tests/test_never_type/test_typetime.py b/tests/test_never_type/test_typetime.py new file mode 100644 index 0000000..ef0e981 --- /dev/null +++ b/tests/test_never_type/test_typetime.py @@ -0,0 +1,22 @@ +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] + ... From 691447696e9815aff7636f87efabcdedad29c512 Mon Sep 17 00:00:00 2001 From: KotlinIsland Date: Tue, 11 Jan 2022 09:29:36 +1000 Subject: [PATCH 2/2] WIP --- basedtyping/__init__.py | 6 ++--- pyproject.toml | 2 +- tests/test_never_type/test_runtime.py | 27 ++++++++++++------- tests/test_never_type/test_typetime.py | 15 +++++++++-- .../test_type_of_types.py | 4 +-- 5 files changed, 36 insertions(+), 18 deletions(-) diff --git a/basedtyping/__init__.py b/basedtyping/__init__.py index 36fc669..209e093 100644 --- a/basedtyping/__init__.py +++ b/basedtyping/__init__.py @@ -38,7 +38,7 @@ Never = NoReturn -"""a value that can never exist. this is the narrowest possible form""" +"""A value that can never exist. This is the narrowest possible form.""" class _ReifiedGenericAlias(_GenericAlias, _root=True): @@ -238,7 +238,7 @@ def issubform( >>> issubform(int | str, object) True """ - # type ignores because issubclass doesn't support _SpecialForm but we do + # 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 issubform(t, forminfo): @@ -248,4 +248,4 @@ def issubform( return True if forminfo is Never: return False - return issubclass(form, forminfo) # type:ignore[arg-type] + return issubclass(form, forminfo) # type: ignore[arg-type] diff --git a/pyproject.toml b/pyproject.toml index 7b511f0..b8d130d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,7 +44,7 @@ disable = [ "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", + "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 diff --git a/tests/test_never_type/test_runtime.py b/tests/test_never_type/test_runtime.py index 0aee782..8cb7d0b 100644 --- a/tests/test_never_type/test_runtime.py +++ b/tests/test_never_type/test_runtime.py @@ -2,36 +2,43 @@ from basedtyping import Never, issubform -# type ignore's due to # https://github.com/KotlinIsland/basedmypy/issues/136 +# 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] + assert not isinstance( # type: ignore[misc] 1, - Never, # type:ignore[arg-type] + Never, # type: ignore[arg-type] ) def test_issubform_true() -> None: - assert issubform(Never, int) # type:ignore[arg-type] + assert issubform(Never, int) # type: ignore[arg-type] def test_issubform_false() -> None: - assert not issubform(str, Never) # type:ignore[arg-type] + assert not issubform(str, Never) # type: ignore[arg-type] def test_issubform_never_is_never() -> None: - assert issubform(Never, Never) # type:ignore[arg-type] + 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] + assert issubclass( # type: ignore[misc] + int, Never # type: ignore[arg-type] ) -def test_cant_instanciate() -> None: +def test_cant_instantiate() -> None: with raises(TypeError): - Never() # type:ignore[operator] + 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 index ef0e981..2fa6be5 100644 --- a/tests/test_never_type/test_typetime.py +++ b/tests/test_never_type/test_typetime.py @@ -15,8 +15,19 @@ def test_valid_type_hint() -> None: _never: Never def test_cant_assign_to_never() -> None: - _never: Never = 1 # type:ignore[assignment] + _never: Never = 1 # type: ignore[assignment] def test_cant_subtype() -> None: - class _A(Never): # type:ignore[misc] + 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] )