Uh oh!
There was an error while loading.Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork411
Implement pyright support via dataclass_transforms#796
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to ourterms of service andprivacy statement. We’ll occasionally send you account related emails.
Already on GitHub?Sign in to your account
Changes fromall commits
52d769c18913734f3787cbba9d7a70e428641b41498abde8a2a4f8df822722b55a06757c21153e8a4c0d6d10e305fa3b2d59714cf68e86bf693e71be63087b7876c40File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading.Please reload this page.
Jump to
Uh oh!
There was an error while loading.Please reload this page.
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| ``attrs`` has added provisional support for static typing in ``pyright`` version 1.1.135 via the `dataclass_transforms specification <https://github.com/microsoft/pyright/blob/1.1.135/specs/dataclass_transforms.md>`. | ||
| Both the ``pyright`` specification and ``attrs`` implementation may change in future versions of both projects. | ||
| Your constructive feedback is welcome in both `attrs#795 <https://github.com/python-attrs/attrs/issues/795>`_ and `pyright#1782 <https://github.com/microsoft/pyright/discussions/1782>`_. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -41,8 +41,11 @@ Also, starting in Python 3.10 (:pep:`526`) **all** annotations will be string li | ||
| When this happens, ``attrs`` will simply put these string literals into the ``type`` attributes. | ||
| If you need to resolve these to real types, you can call `attr.resolve_types` which will update the attribute in place. | ||
| In practice though, types show their biggest usefulness in combination with tools like mypy_, pytype_orpyright_ that have dedicated support for ``attrs`` classes. | ||
| The addition of static types is certainly one of the most exciting features in the Python ecosystem and helps you writing *correct* and *verified self-documenting* code. | ||
| If you don't know where to start, Carl Meyer gave a great talk on `Type-checked Python in the Real World <https://www.youtube.com/watch?v=pMgmKJyWKn8>`_ at PyCon US 2018 that will help you to get started in no time. | ||
| mypy | ||
| ---- | ||
| @@ -69,12 +72,38 @@ To mypy, this code is equivalent to the one above: | ||
| a_number = attr.ib(default=42) # type: int | ||
| list_of_numbers = attr.ib(factory=list, type=typing.List[int]) | ||
| pyright | ||
| ------- | ||
| ``attrs`` provides support for pyright_ though the dataclass_transform_ specification. | ||
| This provides static type inference for a subset of ``attrs`` equivalent to standard-library ``dataclasses``, | ||
| and requires explicit type annotations using the :ref:`next-gen` or ``@attr.s(auto_attribs=True)`` API. | ||
| Given the following definition, ``pyright`` will generate static type signatures for ``SomeClass`` attribute access, ``__init__``, ``__eq__``, and comparison methods:: | ||
| @attr.define | ||
| class SomeClass(object): | ||
| a_number: int = 42 | ||
| list_of_numbers: typing.List[int] = attr.field(factory=list) | ||
| .. note:: | ||
| ``dataclass_transform``-based types are supported provisionally as of ``pyright`` 1.1.135 and ``attrs`` 21.1. | ||
| Both the ``pyright`` dataclass_transform_ specification and ``attrs`` implementation may changed in future versions. | ||
| The ``pyright`` inferred types are a subset of those supported by ``mypy``, including: | ||
| - The generated ``__init__`` signature only includes the attribute type annotations, | ||
| and does not include attribute ``converter`` types. | ||
| - The ``attr.frozen`` decorator is not typed with frozen attributes, which are properly typed via ``attr.define(frozen=True)``. | ||
| Your constructive feedback is welcome in both `attrs#795 <https://github.com/python-attrs/attrs/issues/795>`_ and `pyright#1782 <https://github.com/microsoft/pyright/discussions/1782>`_. | ||
| ***** | ||
| .. _mypy: http://mypy-lang.org | ||
| .. _pytype: https://google.github.io/pytype/ | ||
| .. _pyright: https://github.com/microsoft/pyright | ||
| .. _dataclass_transform: https://github.com/microsoft/pyright/blob/1.1.135/specs/dataclass_transforms.md | ||
asford marked this conversation as resolved. Show resolvedHide resolvedUh oh!There was an error while loading.Please reload this page. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -86,6 +86,21 @@ else: | ||
| takes_self: bool = ..., | ||
| ) -> _T: ... | ||
| # Static type inference support via __dataclass_transform__ implemented as per: | ||
| # https://github.com/microsoft/pyright/blob/1.1.135/specs/dataclass_transforms.md | ||
| # This annotation must be applied to all overloads of "define" and "attrs" | ||
| # | ||
| # NOTE: This is a typing construct and does not exist at runtime. Extensions | ||
| # wrapping attrs decorators should declare a separate __dataclass_transform__ | ||
| # signature in the extension module using the specification linked above to | ||
| # provide pyright support. | ||
| def __dataclass_transform__( | ||
Contributor There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others.Learn more. There should probably be an implementation of this somewhere right? Otherwise this won't be useful outside of attrs. ContributorAuthor There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others.Learn more. This is a little strange, the decorator is just being used as a marker for Contributor There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others.Learn more. Hmm. I'm a little concerned (ok not too much but it definitely feels weird) about having a ContributorAuthor There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others.Learn more. 🤔 I've done a little test of this. Under the 1.1.135
Then a custom annotation of the form has types inferred properly. importattr@attr.__dataclass_transform__(fields=(attr.field,attr.attrib))defcustom_define(cls):returnattr.define(cls)@custom_defineclassCustom:a:int However, given that this is a very early and provisional specification and implementation in
Given how bleeding-edge this feature is I have a weakly held preference that we opt for 2 for this release to ensure that folks don't implictly rely on the Of course, happy to reconsider. Contributor There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others.Learn more. Since this is only for pyright and doesn't currently get mypy to do the right thing (i.e. it doesn't tell mypy that this is an attrs class creator) perhaps it's best if we leave this only inside the pyi file. I might add a comment on here that says that this method doesn't actually exist at runtime. ContributorAuthor There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others.Learn more. Roger, comment added. | ||
| *, | ||
| eq_default: bool = True, | ||
| order_default: bool = False, | ||
| kw_only_default: bool = False, | ||
| field_descriptors: Tuple[Union[type, Callable[..., Any]], ...] = (()), | ||
| ) -> Callable[[_T], _T]: ... | ||
| class Attribute(Generic[_T]): | ||
| name: str | ||
| @@ -276,6 +291,7 @@ def field( | ||
| on_setattr: Optional[_OnSetAttrArgType] = ..., | ||
| ) -> Any: ... | ||
| @overload | ||
| @__dataclass_transform__(order_default=True, field_descriptors=(attrib, field)) | ||
| def attrs( | ||
| maybe_cls: _C, | ||
| these: Optional[Dict[str, Any]] = ..., | ||
| @@ -301,6 +317,7 @@ def attrs( | ||
| field_transformer: Optional[_FieldTransformer] = ..., | ||
| ) -> _C: ... | ||
| @overload | ||
| @__dataclass_transform__(order_default=True, field_descriptors=(attrib, field)) | ||
| def attrs( | ||
| maybe_cls: None = ..., | ||
| these: Optional[Dict[str, Any]] = ..., | ||
| @@ -326,6 +343,7 @@ def attrs( | ||
| field_transformer: Optional[_FieldTransformer] = ..., | ||
| ) -> Callable[[_C], _C]: ... | ||
| @overload | ||
| @__dataclass_transform__(field_descriptors=(attrib, field)) | ||
| def define( | ||
| maybe_cls: _C, | ||
| *, | ||
| @@ -349,6 +367,7 @@ def define( | ||
| field_transformer: Optional[_FieldTransformer] = ..., | ||
| ) -> _C: ... | ||
| @overload | ||
| @__dataclass_transform__(field_descriptors=(attrib, field)) | ||
| def define( | ||
| maybe_cls: None = ..., | ||
| *, | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| import attr | ||
| @attr.define() | ||
| class Define: | ||
| a: str | ||
| b: int | ||
| reveal_type(Define.__init__) # noqa | ||
| @attr.define() | ||
| class DefineConverter: | ||
| # mypy plugin adapts the "int" method signature, pyright does not | ||
| with_converter: int = attr.field(converter=int) | ||
| reveal_type(DefineConverter.__init__) # noqa | ||
| # mypy plugin supports attr.frozen, pyright does not | ||
| @attr.frozen() | ||
| class Frozen: | ||
| a: str | ||
| d = Frozen("a") | ||
| d.a = "new" | ||
| reveal_type(d.a) # noqa | ||
| # but pyright supports attr.define(frozen) | ||
| @attr.define(frozen=True) | ||
| class FrozenDefine: | ||
| a: str | ||
| d2 = FrozenDefine("a") | ||
| d2.a = "new" | ||
| reveal_type(d2.a) # noqa |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,69 @@ | ||
| import json | ||
| import os.path | ||
| import shutil | ||
| import subprocess | ||
| import sys | ||
| import pytest | ||
| import attr | ||
| if sys.version_info < (3, 6): | ||
| _found_pyright = False | ||
| else: | ||
| _found_pyright = shutil.which("pyright") | ||
| @attr.s(frozen=True) | ||
| class PyrightDiagnostic(object): | ||
| severity = attr.ib() | ||
| message = attr.ib() | ||
| @pytest.mark.skipif(not _found_pyright, reason="Requires pyright.") | ||
| def test_pyright_baseline(): | ||
| """The __dataclass_transform__ decorator allows pyright to determine | ||
| attrs decorated class types. | ||
| """ | ||
| test_file = os.path.dirname(__file__) + "/dataclass_transform_example.py" | ||
| pyright = subprocess.run( | ||
| ["pyright", "--outputjson", str(test_file)], capture_output=True | ||
| ) | ||
| pyright_result = json.loads(pyright.stdout) | ||
| diagnostics = set( | ||
| PyrightDiagnostic(d["severity"], d["message"]) | ||
| for d in pyright_result["generalDiagnostics"] | ||
| ) | ||
| # Expected diagnostics as per pyright 1.1.135 | ||
| expected_diagnostics = { | ||
| PyrightDiagnostic( | ||
| severity="information", | ||
| message='Type of "Define.__init__" is' | ||
| ' "(self: Define, a: str, b: int) -> None"', | ||
| ), | ||
| PyrightDiagnostic( | ||
| severity="information", | ||
| message='Type of "DefineConverter.__init__" is ' | ||
| '"(self: DefineConverter, with_converter: int) -> None"', | ||
| ), | ||
| PyrightDiagnostic( | ||
| severity="information", | ||
| message='Type of "d.a" is "Literal[\'new\']"', | ||
| ), | ||
| PyrightDiagnostic( | ||
| severity="error", | ||
| message='Cannot assign member "a" for type ' | ||
| '"FrozenDefine"\n\xa0\xa0"FrozenDefine" is frozen', | ||
| ), | ||
| PyrightDiagnostic( | ||
| severity="information", | ||
| message='Type of "d2.a" is "Literal[\'new\']"', | ||
| ), | ||
| } | ||
| assert diagnostics == expected_diagnostics |