Skip to content

Commit e823a6b

Browse files
authored
Merge pull request #55 from selimb/mypy-plugin
Add mypy plugin for NewType
2 parents 520698f + c0184c8 commit e823a6b

File tree

6 files changed

+115
-5
lines changed

6 files changed

+115
-5
lines changed

.travis.yml

+2-2
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ dist: xenial
55
install:
66
- pip install --pre -e '.[dev]'
77
script:
8-
- python -m unittest
8+
- pytest
99

1010
stages:
1111
- pre-commit hooks
@@ -53,4 +53,4 @@ matrix:
5353
secure: "WhUEch0YOZRtqr+r6BAd2KfgMY2IctHdoXNbl60qbyURjX3RzmICxB6GFHlAPkzu+b7GkY/WxKrAEa9NjjkHZ69M+x6ozORW4+YzicNVIgqxQkE65Su6l8zOCSexncMJrtmYceoxaRcX2Bsjy9r5fAv0gO6+WugOCAV6sZHkANqIjySTN82vAnla9/htWcesvw3JFckhdsPH/Lnu+o/hHKeM33vQUsJThQ73fatNuObyzglaoNu2L1PSjitI3wBA3jOpAL2o5v7bD/P7Krpu+YsGlaJNMHfHWkcNxypikZitCKcocoI7dWgQvSTqWA6TPV/MboRRF/2ZhnMKuw7ZY84GBx8Dyyw/len9s/1+m8kgu9ES1qJaIf03fvC60Uend2n2yk3sx6+rG1CCAvf21CJgMW+6XAqKpLI2tj0nPdBHvLGSdeRtTO5Ubpw8hDModlVai0RLhtYNkjLQkl+QrEehm2x/zhrP7gCudXQsKx8jvHHF70/pCN/WlvUhR42HEqWsPg7ZahkWi7YkFVY7e846ScxbWKjFL1s6vyCZso5U9DdUBvVEinshLoSZSbXotF4oHqdyKf17Hk0P6dPn/ocxGRB5rWWn5irhtEqcxZFKgawO8N1GYnTF64YewvO3O5gNqXrbpwCSScOrvDKDRosAWQ0yB/71ZvUEEcaYiFw="
5454
on:
5555
tags: true
56-
all_branches: true
56+
all_branches: true

README.md

+10
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,16 @@ from marshmallow_dataclass import NewType
216216
Email = NewType("Email", str, field=marshmallow.fields.Email)
217217
```
218218

219+
Note: if you are using `mypy`, you will notice that `mypy` throws an error if a variable defined with
220+
`NewType` is used in a type annotation. To resolve this, add the `marshmallow_dataclass.mypy` plugin
221+
to your `mypy` configuration, e.g.:
222+
223+
```ini
224+
[mypy]
225+
plugins = marshmallow_dataclass.mypy
226+
# ...
227+
```
228+
219229
### `Meta` options
220230

221231
[`Meta` options](https://marshmallow.readthedocs.io/en/stable/api_reference.html#marshmallow.Schema.Meta) are set the same way as a marshmallow `Schema`.

marshmallow_dataclass/mypy.py

+65
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import inspect
2+
from typing import Callable, Optional, Type
3+
4+
from mypy import nodes
5+
from mypy.plugin import DynamicClassDefContext, Plugin
6+
7+
import marshmallow_dataclass
8+
9+
_NEW_TYPE_SIG = inspect.signature(marshmallow_dataclass.NewType)
10+
11+
12+
def plugin(version: str) -> Type[Plugin]:
13+
return MarshmallowDataclassPlugin
14+
15+
16+
class MarshmallowDataclassPlugin(Plugin):
17+
def get_dynamic_class_hook(
18+
self, fullname: str
19+
) -> Optional[Callable[[DynamicClassDefContext], None]]:
20+
if fullname == "marshmallow_dataclass.NewType":
21+
return new_type_hook
22+
return None
23+
24+
25+
def new_type_hook(ctx: DynamicClassDefContext) -> None:
26+
"""
27+
Dynamic class hook for :func:`marshmallow_dataclass.NewType`.
28+
29+
Uses the type of the ``typ`` argument.
30+
"""
31+
typ = _get_arg_by_name(ctx.call, "typ", _NEW_TYPE_SIG)
32+
if not isinstance(typ, nodes.RefExpr):
33+
return
34+
info = typ.node
35+
if not isinstance(info, nodes.TypeInfo):
36+
return
37+
ctx.api.add_symbol_table_node(ctx.name, nodes.SymbolTableNode(nodes.GDEF, info))
38+
39+
40+
def _get_arg_by_name(
41+
call: nodes.CallExpr, name: str, sig: inspect.Signature
42+
) -> Optional[nodes.Expression]:
43+
"""
44+
Get value of argument from a call.
45+
46+
:return: The argument value, or ``None`` if it cannot be found.
47+
48+
.. warning::
49+
This probably doesn't yet work for calls with ``*args`` and/or ``*kwargs``.
50+
"""
51+
args = []
52+
kwargs = {}
53+
for arg_name, arg_value in zip(call.arg_names, call.args):
54+
if arg_name is None:
55+
args.append(arg_value)
56+
else:
57+
kwargs[arg_name] = arg_value
58+
try:
59+
bound_args = sig.bind(*args, **kwargs)
60+
except TypeError:
61+
return None
62+
try:
63+
return bound_args.arguments[name]
64+
except KeyError:
65+
return None

setup.py

+9
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,21 @@
1818
':python_version == "3.6"': ["dataclasses"],
1919
"lint": ["pre-commit~=1.18"],
2020
"docs": ["sphinx"],
21+
"tests": [
22+
"pytest",
23+
# re: pypy: typed-ast (a dependency of mypy) fails to install on pypy
24+
# https://github.com/python/typed_ast/issues/111
25+
# re: win32: pytest-mypy-plugins depends on capturer, which isn't supported on
26+
# windows
27+
"pytest-mypy-plugins; implementation_name != 'pypy' and sys.platform != 'win32'",
28+
],
2129
}
2230
EXTRAS_REQUIRE["dev"] = (
2331
EXTRAS_REQUIRE["enum"]
2432
+ EXTRAS_REQUIRE["union"]
2533
+ EXTRAS_REQUIRE["lint"]
2634
+ EXTRAS_REQUIRE["docs"]
35+
+ EXTRAS_REQUIRE["tests"]
2736
)
2837

2938
setup(

tests/README.md

+3-3
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
## How to run
44
In the project root, install dependencies and then run:
5-
5+
66
```bash
7-
python3 -m unittest
8-
```
7+
pytest
8+
```

tests/test_mypy.yml

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# Tests for marshmallow_dataclass.mypy, using pytest-mypy-plugins
2+
# NOTE: Since pytest-mypy-plugins is not installed on pypy (see setup.py), these tests
3+
# will not even be collected by pytest
4+
- case: NewType_basic
5+
mypy_config: |
6+
follow_imports = silent
7+
plugins = marshmallow_dataclass.mypy
8+
main: |
9+
from dataclasses import dataclass
10+
import marshmallow as ma
11+
from marshmallow_dataclass import NewType
12+
13+
Email = NewType("Email", str, validate=ma.validate.Email)
14+
UserID = NewType("UserID", validate=ma.validate.Length(equal=32), typ=str)
15+
16+
@dataclass
17+
class User:
18+
id: UserID
19+
email: Email
20+
21+
user = User(id="a"*32, email="[email protected]")
22+
reveal_type(user.id) # N: Revealed type is 'builtins.str'
23+
reveal_type(user.email) # N: Revealed type is 'builtins.str'
24+
25+
User(id=42, email="[email protected]") # E: Argument "id" to "User" has incompatible type "int"; expected "str"
26+
User(id="a"*32, email=["not", "a", "string"]) # E: Argument "email" to "User" has incompatible type "List[str]"; expected "str"

0 commit comments

Comments
 (0)