Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit7e372c5

Browse files
asfordhynek
andauthored
Implement pyright support via dataclass_transforms (#796)
* Add __dataclass_transform__ decorator.* Add doc notes for pyright dataclass_transform support.* Fix docs build error.* Expand docs on dataclass_transform* Add changelog* Fix docs build* Fix lint* Add note on __dataclass_transform__ in .pyi* Add baseline test of pyright support via tox* Add pyright tests to tox run configuration* Fix test errors, enable in tox.* Fixup lint* Move pyright to py39* Add test docstring.* Fixup docs.Co-authored-by: Hynek Schlawack <hs@ox.cx>
1 parentfc0d60e commit7e372c5

File tree

7 files changed

+220
-7
lines changed

7 files changed

+220
-7
lines changed

‎changelog.d/796.change.rst‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
``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>`.
2+
Both the ``pyright`` specification and ``attrs`` implementation may change in future versions of both projects.
3+
4+
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>`_.

‎docs/extending.rst‎

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,10 @@ An example for that is the package `environ-config <https://github.com/hynek/env
4646

4747
Another common use case is to overwrite ``attrs``'s defaults.
4848

49-
Unfortunately, this currently `confuses<https://github.com/python/mypy/issues/5406>`_ mypy's ``attrs`` plugin.
49+
Mypy
50+
************
51+
52+
Unfortunately, decorator wrapping currently `confuses<https://github.com/python/mypy/issues/5406>`_ mypy's ``attrs`` plugin.
5053
At the moment, the best workaround is to hold your nose, write a fake mypy plugin, and mutate a bunch of global variables::
5154

5255
from mypy.plugin import Plugin
@@ -86,6 +89,34 @@ Then tell mypy about your plugin using your project's ``mypy.ini``:
8689
Please note that it is currently *impossible* to let mypy know that you've changed defaults like *eq* or *order*.
8790
You can only use this trick to tell mypy that a class is actually an ``attrs`` class.
8891

92+
Pyright
93+
*************
94+
95+
Generic decorator wrapping is supported in ``pyright`` via the provisionaldataclass_transform_ specification.
96+
97+
For a custom wrapping of the form::
98+
99+
def custom_define(f):
100+
return attr.define(f)
101+
102+
This is implemented via a ``__dataclass_transform__`` type decorator in the custom extension's ``.pyi`` of the form::
103+
104+
def __dataclass_transform__(
105+
*,
106+
eq_default: bool = True,
107+
order_default: bool = False,
108+
kw_only_default: bool = False,
109+
field_descriptors: Tuple[Union[type, Callable[..., Any]], ...] = (()),
110+
) -> Callable[[_T], _T]: ...
111+
112+
@__dataclass_transform__(field_descriptors=(attr.attrib, attr.field))
113+
def custom_define(f): ...
114+
115+
..note::
116+
117+
``dataclass_transform`` is supported provisionally as of ``pyright`` 1.1.135.
118+
119+
Both the ``pyright``dataclass_transform_ specification and ``attrs`` implementation may changed in future versions.
89120

90121
Types
91122
-----
@@ -272,3 +303,7 @@ It has the signature
272303
{'dt': '2020-05-04T13:37:00'}
273304
>>>json.dumps(data)
274305
'{"dt": "2020-05-04T13:37:00"}'
306+
307+
*****
308+
309+
.. _dataclass_transform:https://github.com/microsoft/pyright/blob/1.1.135/specs/dataclass_transforms.md

‎docs/types.rst‎

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,11 @@ Also, starting in Python 3.10 (:pep:`526`) **all** annotations will be string li
4141
When this happens, ``attrs`` will simply put these string literals into the ``type`` attributes.
4242
If you need to resolve these to real types, you can call `attr.resolve_types` which will update the attribute in place.
4343

44-
In practice though, types show their biggest usefulness in combination with tools likemypy_orpytype_ that both have dedicated support for ``attrs`` classes.
44+
In practice though, types show their biggest usefulness in combination with tools likemypy_,pytype_orpyright_ that have dedicated support for ``attrs`` classes.
4545

46+
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.
47+
48+
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.
4649

4750
mypy
4851
----
@@ -69,12 +72,38 @@ To mypy, this code is equivalent to the one above:
6972
a_number= attr.ib(default=42)#type:int
7073
list_of_numbers= attr.ib(factory=list,type=typing.List[int])
7174
72-
*****
7375
74-
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.
76+
pyright
77+
-------
7578

76-
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.
79+
``attrs`` provides support forpyright_ though thedataclass_transform_ specification.
80+
This provides static type inference for a subset of ``attrs`` equivalent to standard-library ``dataclasses``,
81+
and requires explicit type annotations using the:ref:`next-gen` or ``@attr.s(auto_attribs=True)`` API.
82+
83+
Given the following definition, ``pyright`` will generate static type signatures for ``SomeClass`` attribute access, ``__init__``, ``__eq__``, and comparison methods::
84+
85+
@attr.define
86+
class SomeClass(object):
87+
a_number: int = 42
88+
list_of_numbers: typing.List[int] = attr.field(factory=list)
89+
90+
..note::
91+
92+
``dataclass_transform``-based types are supported provisionally as of ``pyright`` 1.1.135 and ``attrs`` 21.1.
93+
Both the ``pyright``dataclass_transform_ specification and ``attrs`` implementation may changed in future versions.
94+
95+
The ``pyright`` inferred types are a subset of those supported by ``mypy``, including:
7796

97+
- The generated ``__init__`` signature only includes the attribute type annotations,
98+
and does not include attribute ``converter`` types.
99+
100+
- The ``attr.frozen`` decorator is not typed with frozen attributes, which are properly typed via ``attr.define(frozen=True)``.
101+
102+
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>`_.
103+
104+
*****
78105

79106
.. _mypy:http://mypy-lang.org
80107
.. _pytype:https://google.github.io/pytype/
108+
.. _pyright:https://github.com/microsoft/pyright
109+
.. _dataclass_transform:https://github.com/microsoft/pyright/blob/1.1.135/specs/dataclass_transforms.md

‎src/attr/__init__.pyi‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,21 @@ else:
8686
takes_self:bool= ...,
8787
)->_T: ...
8888

89+
# Static type inference support via __dataclass_transform__ implemented as per:
90+
# https://github.com/microsoft/pyright/blob/1.1.135/specs/dataclass_transforms.md
91+
# This annotation must be applied to all overloads of "define" and "attrs"
92+
#
93+
# NOTE: This is a typing construct and does not exist at runtime. Extensions
94+
# wrapping attrs decorators should declare a separate __dataclass_transform__
95+
# signature in the extension module using the specification linked above to
96+
# provide pyright support.
97+
def__dataclass_transform__(
98+
*,
99+
eq_default:bool=True,
100+
order_default:bool=False,
101+
kw_only_default:bool=False,
102+
field_descriptors:Tuple[Union[type,Callable[...,Any]], ...]= (()),
103+
)->Callable[[_T],_T]: ...
89104

90105
classAttribute(Generic[_T]):
91106
name:str
@@ -276,6 +291,7 @@ def field(
276291
on_setattr:Optional[_OnSetAttrArgType]= ...,
277292
)->Any: ...
278293
@overload
294+
@__dataclass_transform__(order_default=True,field_descriptors=(attrib,field))
279295
defattrs(
280296
maybe_cls:_C,
281297
these:Optional[Dict[str,Any]]= ...,
@@ -301,6 +317,7 @@ def attrs(
301317
field_transformer:Optional[_FieldTransformer]= ...,
302318
)->_C: ...
303319
@overload
320+
@__dataclass_transform__(order_default=True,field_descriptors=(attrib,field))
304321
defattrs(
305322
maybe_cls:None= ...,
306323
these:Optional[Dict[str,Any]]= ...,
@@ -326,6 +343,7 @@ def attrs(
326343
field_transformer:Optional[_FieldTransformer]= ...,
327344
)->Callable[[_C],_C]: ...
328345
@overload
346+
@__dataclass_transform__(field_descriptors=(attrib,field))
329347
defdefine(
330348
maybe_cls:_C,
331349
*,
@@ -349,6 +367,7 @@ def define(
349367
field_transformer:Optional[_FieldTransformer]= ...,
350368
)->_C: ...
351369
@overload
370+
@__dataclass_transform__(field_descriptors=(attrib,field))
352371
defdefine(
353372
maybe_cls:None= ...,
354373
*,
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
importattr
2+
3+
4+
@attr.define()
5+
classDefine:
6+
a:str
7+
b:int
8+
9+
10+
reveal_type(Define.__init__)# noqa
11+
12+
13+
@attr.define()
14+
classDefineConverter:
15+
# mypy plugin adapts the "int" method signature, pyright does not
16+
with_converter:int=attr.field(converter=int)
17+
18+
19+
reveal_type(DefineConverter.__init__)# noqa
20+
21+
22+
# mypy plugin supports attr.frozen, pyright does not
23+
@attr.frozen()
24+
classFrozen:
25+
a:str
26+
27+
28+
d=Frozen("a")
29+
d.a="new"
30+
31+
reveal_type(d.a)# noqa
32+
33+
34+
# but pyright supports attr.define(frozen)
35+
@attr.define(frozen=True)
36+
classFrozenDefine:
37+
a:str
38+
39+
40+
d2=FrozenDefine("a")
41+
d2.a="new"
42+
43+
reveal_type(d2.a)# noqa

‎tests/test_pyright.py‎

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
importjson
2+
importos.path
3+
importshutil
4+
importsubprocess
5+
importsys
6+
7+
importpytest
8+
9+
importattr
10+
11+
12+
ifsys.version_info< (3,6):
13+
_found_pyright=False
14+
else:
15+
_found_pyright=shutil.which("pyright")
16+
17+
18+
@attr.s(frozen=True)
19+
classPyrightDiagnostic(object):
20+
severity=attr.ib()
21+
message=attr.ib()
22+
23+
24+
@pytest.mark.skipif(not_found_pyright,reason="Requires pyright.")
25+
deftest_pyright_baseline():
26+
"""The __dataclass_transform__ decorator allows pyright to determine
27+
attrs decorated class types.
28+
"""
29+
30+
test_file=os.path.dirname(__file__)+"/dataclass_transform_example.py"
31+
32+
pyright=subprocess.run(
33+
["pyright","--outputjson",str(test_file)],capture_output=True
34+
)
35+
pyright_result=json.loads(pyright.stdout)
36+
37+
diagnostics=set(
38+
PyrightDiagnostic(d["severity"],d["message"])
39+
fordinpyright_result["generalDiagnostics"]
40+
)
41+
42+
# Expected diagnostics as per pyright 1.1.135
43+
expected_diagnostics= {
44+
PyrightDiagnostic(
45+
severity="information",
46+
message='Type of "Define.__init__" is'
47+
' "(self: Define, a: str, b: int) -> None"',
48+
),
49+
PyrightDiagnostic(
50+
severity="information",
51+
message='Type of "DefineConverter.__init__" is '
52+
'"(self: DefineConverter, with_converter: int) -> None"',
53+
),
54+
PyrightDiagnostic(
55+
severity="information",
56+
message='Type of "d.a" is "Literal[\'new\']"',
57+
),
58+
PyrightDiagnostic(
59+
severity="error",
60+
message='Cannot assign member "a" for type '
61+
'"FrozenDefine"\n\xa0\xa0"FrozenDefine" is frozen',
62+
),
63+
PyrightDiagnostic(
64+
severity="information",
65+
message='Type of "d2.a" is "Literal[\'new\']"',
66+
),
67+
}
68+
69+
assertdiagnostics==expected_diagnostics

‎tox.ini‎

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,14 @@ python =
1515
3.6: py36
1616
3.7: py37, docs
1717
3.8: py38, lint, manifest, typing, changelog
18-
3.9: py39
18+
3.9: py39, pyright
1919
3.10: py310
2020
pypy2: pypy2
2121
pypy3: pypy3
2222

2323

2424
[tox]
25-
envlist = typing,lint,py27,py35,py36,py37,py38,py39,py310,pypy,pypy3,manifest,docs,pypi-description,changelog,coverage-report
25+
envlist = typing,lint,py27,py35,py36,py37,py38,py39,py310,pypy,pypy3,pyright,manifest,docs,pypi-description,changelog,coverage-report
2626
isolated_build = True
2727

2828

@@ -121,3 +121,17 @@ deps = mypy>=0.800
121121
commands =
122122
mypy 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
123123
mypy tests/typing_example.py
124+
125+
126+
[testenv:pyright]
127+
# Install and configure node and pyright
128+
# This *could* be folded into a custom install_command
129+
# Use nodeenv to configure node in the running tox virtual environment
130+
# Seeing errors using "nodeenv -p"
131+
# Use npm install -g to install "globally" into the virtual environment
132+
basepython = python3.9
133+
deps = nodeenv
134+
commands =
135+
nodeenv --prebuilt --node=lts --force {envdir}
136+
npm install -g --no-package-lock --no-save pyright@1.1.135
137+
pytest tests/test_pyright.py -vv

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2026 Movatter.jp