diff --git a/changelog.d/1358.change.md b/changelog.d/1358.change.md new file mode 100644 index 000000000..6d9a009ff --- /dev/null +++ b/changelog.d/1358.change.md @@ -0,0 +1 @@ +Introduce `attrs.NothingType`, for annotating types consistent with `attrs.NOTHING`. diff --git a/src/attr/__init__.py b/src/attr/__init__.py index d2cb9207a..2e3b7c005 100644 --- a/src/attr/__init__.py +++ b/src/attr/__init__.py @@ -5,7 +5,7 @@ """ from functools import partial -from typing import Callable, Protocol +from typing import Callable, Literal, Protocol from . import converters, exceptions, filters, setters, validators from ._cmp import cmp_using @@ -16,6 +16,7 @@ Attribute, Converter, Factory, + _Nothing, attrib, attrs, fields, @@ -36,12 +37,15 @@ class AttrsInstance(Protocol): pass +NothingType = Literal[_Nothing.NOTHING] + __all__ = [ "NOTHING", "Attribute", "AttrsInstance", "Converter", "Factory", + "NothingType", "asdict", "assoc", "astuple", diff --git a/src/attr/__init__.pyi b/src/attr/__init__.pyi index 47951eebc..133e50105 100644 --- a/src/attr/__init__.pyi +++ b/src/attr/__init__.pyi @@ -5,6 +5,7 @@ from typing import ( Any, Callable, Generic, + Literal, Mapping, Protocol, Sequence, @@ -37,9 +38,9 @@ from attrs import ( ) if sys.version_info >= (3, 10): - from typing import TypeGuard + from typing import TypeGuard, TypeAlias else: - from typing_extensions import TypeGuard + from typing_extensions import TypeGuard, TypeAlias if sys.version_info >= (3, 11): from typing import dataclass_transform @@ -72,11 +73,11 @@ class _Nothing(enum.Enum): NOTHING = enum.auto() NOTHING = _Nothing.NOTHING +NothingType: TypeAlias = Literal[_Nothing.NOTHING] # NOTE: Factory lies about its return type to make this possible: # `x: List[int] # = Factory(list)` # Work around mypy issue #4554 in the common case by using an overload. -from typing import Literal @overload def Factory(factory: Callable[[], _T]) -> _T: ... diff --git a/src/attr/_make.py b/src/attr/_make.py index 530ce39d1..eac5888a5 100644 --- a/src/attr/_make.py +++ b/src/attr/_make.py @@ -79,6 +79,8 @@ def __bool__(self): NOTHING = _Nothing.NOTHING """ Sentinel to indicate the lack of a value when `None` is ambiguous. + +When using in 3rd party code, use `attrs.NothingType` for type annotations. """ diff --git a/src/attrs/__init__.py b/src/attrs/__init__.py index 3b5ab8428..e8023ff6c 100644 --- a/src/attrs/__init__.py +++ b/src/attrs/__init__.py @@ -6,6 +6,7 @@ AttrsInstance, Converter, Factory, + NothingType, _make_getattr, assoc, cmp_using, @@ -32,6 +33,7 @@ "AttrsInstance", "Converter", "Factory", + "NothingType", "__author__", "__copyright__", "__description__", diff --git a/src/attrs/__init__.pyi b/src/attrs/__init__.pyi index 05e5a0c53..648fa7a34 100644 --- a/src/attrs/__init__.pyi +++ b/src/attrs/__init__.pyi @@ -40,6 +40,7 @@ from attr import setters as setters from attr import validate as validate from attr import validators as validators from attr import attrib, asdict as asdict, astuple as astuple +from attr import NothingType as NothingType if sys.version_info >= (3, 11): from typing import dataclass_transform diff --git a/tests/test_mypy.yml b/tests/test_mypy.yml index 8042ebb11..41c5029f3 100644 --- a/tests/test_mypy.yml +++ b/tests/test_mypy.yml @@ -1472,3 +1472,16 @@ reveal_type(A) # N: Revealed type is "def () -> main.A" if has(A): reveal_type(A) # N: Revealed type is "type[attr.AttrsInstance]" + +- case: testNothingType + regex: true + main: | + from typing import Optional + from attrs import NOTHING, NothingType + + def takes_nothing(arg: Optional[NothingType]) -> None: + return None + + takes_nothing(NOTHING) + takes_nothing(None) + takes_nothing(1) # E: Argument 1 to "takes_nothing" has incompatible type "Literal\[1\]"; expected "(Optional\[Literal\[_Nothing.NOTHING\]\]|Literal\[_Nothing.NOTHING\] \| None)" \[arg-type\]