diff --git a/changelog.d/999.change.rst b/changelog.d/999.change.rst new file mode 100644 index 000000000..977483386 --- /dev/null +++ b/changelog.d/999.change.rst @@ -0,0 +1,2 @@ +Made ``attrs.AttrsInstance`` stub available at runtime and fixed type errors +related to the usage of ``attrs.AttrsInstance`` in Pyright. diff --git a/src/attr/__init__.py b/src/attr/__init__.py index c3f0937e7..33b663e5e 100644 --- a/src/attr/__init__.py +++ b/src/attr/__init__.py @@ -52,8 +52,14 @@ ib = attr = attrib dataclass = partial(attrs, auto_attribs=True) # happy Easter ;) + +class AttrsInstance: + pass + + __all__ = [ "Attribute", + "AttrsInstance", "Factory", "NOTHING", "asdict", diff --git a/src/attr/__init__.pyi b/src/attr/__init__.pyi index c21fec7a1..dfa69578b 100644 --- a/src/attr/__init__.pyi +++ b/src/attr/__init__.pyi @@ -4,7 +4,6 @@ import sys from typing import ( Any, Callable, - ClassVar, Dict, Generic, List, @@ -26,6 +25,7 @@ from . import filters as filters from . import setters as setters from . import validators as validators from ._cmp import cmp_using as cmp_using +from ._typing_compat import AttrsInstance_ from ._version_info import VersionInfo if sys.version_info >= (3, 10): @@ -65,9 +65,9 @@ _FieldTransformer = Callable[ # _ValidatorType from working when passed in a list or tuple. _ValidatorArgType = Union[_ValidatorType[_T], Sequence[_ValidatorType[_T]]] -# A protocol to be able to statically accept an attrs class. -class AttrsInstance(Protocol): - __attrs_attrs__: ClassVar[Any] +# We subclass this here to keep the protocol's qualified name clean. +class AttrsInstance(AttrsInstance_, Protocol): + pass # _make -- diff --git a/src/attr/_typing_compat.pyi b/src/attr/_typing_compat.pyi new file mode 100644 index 000000000..ca7b71e90 --- /dev/null +++ b/src/attr/_typing_compat.pyi @@ -0,0 +1,15 @@ +from typing import Any, ClassVar, Protocol + +# MYPY is a special constant in mypy which works the same way as `TYPE_CHECKING`. +MYPY = False + +if MYPY: + # A protocol to be able to statically accept an attrs class. + class AttrsInstance_(Protocol): + __attrs_attrs__: ClassVar[Any] + +else: + # For type checkers without plug-in support use an empty protocol that + # will (hopefully) be combined into a union. + class AttrsInstance_(Protocol): + pass diff --git a/src/attrs/__init__.py b/src/attrs/__init__.py index a704b8b56..81dd6b2f0 100644 --- a/src/attrs/__init__.py +++ b/src/attrs/__init__.py @@ -3,6 +3,7 @@ from attr import ( NOTHING, Attribute, + AttrsInstance, Factory, __author__, __copyright__, @@ -48,6 +49,7 @@ "assoc", "astuple", "Attribute", + "AttrsInstance", "cmp_using", "converters", "define", diff --git a/tests/test_pyright.py b/tests/test_pyright.py index 3cc1fa79b..44b308906 100644 --- a/tests/test_pyright.py +++ b/tests/test_pyright.py @@ -78,3 +78,31 @@ def test_pyright_baseline(): } assert diagnostics == expected_diagnostics + + +def test_pyright_attrsinstance_compat(tmp_path): + """ + Test that `AttrsInstance` is compatible with Pyright. + """ + test_pyright_attrsinstance_compat_path = ( + tmp_path / "test_pyright_attrsinstance_compat.py" + ) + test_pyright_attrsinstance_compat_path.write_text( + """\ +import attrs + +# We can assign any old object to `AttrsInstance`. +foo: attrs.AttrsInstance = object() + +reveal_type(attrs.AttrsInstance) +""" + ) + + diagnostics = parse_pyright_output(test_pyright_attrsinstance_compat_path) + expected_diagnostics = { + PyrightDiagnostic( + severity="information", + message='Type of "attrs.AttrsInstance" is "Type[AttrsInstance]"', + ), + } + assert diagnostics == expected_diagnostics diff --git a/tox.ini b/tox.ini index 5c9f7cd85..2be420dcb 100644 --- a/tox.ini +++ b/tox.ini @@ -92,7 +92,7 @@ commands = towncrier build --version UNRELEASED --draft [testenv:mypy] deps = mypy>=0.902 commands = - mypy src/attrs/__init__.pyi src/attr/__init__.pyi src/attr/_version_info.pyi src/attr/converters.pyi src/attr/exceptions.pyi src/attr/filters.pyi src/attr/setters.pyi src/attr/validators.pyi + mypy src/attrs/__init__.pyi src/attr/__init__.pyi src/attr/_typing_compat.pyi src/attr/_version_info.pyi src/attr/converters.pyi src/attr/exceptions.pyi src/attr/filters.pyi src/attr/setters.pyi src/attr/validators.pyi mypy tests/typing_example.py