Skip to content

Commit 6f26a1c

Browse files
authored
Support mypy 0.910 to 0.930 including CI tests (#3594)
* cleanup bumping mypy to 0.930, #3573 * add tests for old mypy * tweak test-old-mypy job * alter mypy plugin to work with older versions * mypy.py compatibility with multiple versions * fix mypy tests to allow for varied output * toml parsing, fix #3579 * formatting :-( * ignore missing types for toml package * remove unused ignore_missing_imports * undo removal of ignore_missing_imports for dotenv * tweak coverage ignore * fully uninstall mypy and toml/tomli
1 parent 8ef492b commit 6f26a1c

File tree

10 files changed

+129
-43
lines changed

10 files changed

+129
-43
lines changed

.github/workflows/ci.yml

+45-4
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ jobs:
138138
COMPILED: no
139139
DEPS: yes
140140

141-
runs-on: ${{ format('{0}-latest', matrix.os) }}
141+
runs-on: ${{ matrix.os }}-latest
142142
steps:
143143
- uses: actions/checkout@v2
144144

@@ -164,8 +164,49 @@ jobs:
164164
name: coverage
165165
path: coverage
166166

167+
test-old-mypy:
168+
name: test mypy v${{ matrix.mypy-version }}
169+
runs-on: ubuntu-latest
170+
strategy:
171+
fail-fast: false
172+
matrix:
173+
mypy-version: ['0.910', '0.920', '0.921']
174+
175+
steps:
176+
- uses: actions/checkout@v2
177+
178+
- name: set up python
179+
uses: actions/setup-python@v2
180+
with:
181+
python-version: '3.10'
182+
183+
- name: install
184+
run: |
185+
make install-testing
186+
pip freeze
187+
188+
- name: uninstall deps
189+
run: pip uninstall -y mypy tomli toml
190+
191+
- name: install specific mypy version
192+
run: pip install mypy==${{ matrix.mypy-version }}
193+
194+
- run: mkdir coverage
195+
196+
- name: run tests
197+
run: pytest --cov=pydantic tests/mypy
198+
env:
199+
COVERAGE_FILE: coverage/.coverage.linux-py3.10-mypy${{ matrix.mypy-version }}
200+
CONTEXT: linux-py3.10-mypy${{ matrix.mypy-version }}
201+
202+
- name: store coverage files
203+
uses: actions/upload-artifact@v2
204+
with:
205+
name: coverage
206+
path: coverage
207+
167208
coverage-combine:
168-
needs: [test-linux, test-windows-mac]
209+
needs: [test-linux, test-windows-mac, test-old-mypy]
169210
runs-on: ubuntu-latest
170211

171212
steps:
@@ -236,7 +277,7 @@ jobs:
236277

237278
build:
238279
name: build py3.${{ matrix.python-version }} on ${{ matrix.platform || matrix.os }}
239-
needs: [lint, test-linux, test-windows-mac, test-fastapi, benchmark]
280+
needs: [lint, test-linux, test-windows-mac, test-old-mypy, test-fastapi, benchmark]
240281
if: "success() && (startsWith(github.ref, 'refs/tags/') || github.ref == 'refs/heads/master')"
241282
strategy:
242283
fail-fast: false
@@ -249,7 +290,7 @@ jobs:
249290
- os: windows
250291
ls: dir
251292

252-
runs-on: ${{ format('{0}-latest', matrix.os) }}
293+
runs-on: ${{ matrix.os }}-latest
253294
steps:
254295
- uses: actions/checkout@v2
255296

docs/mypy_plugin.md

+2
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,8 @@ To get started, all you need to do is create a `mypy.ini` file with following co
8686
plugins = pydantic.mypy
8787
```
8888

89+
The plugin is compatible with mypy versions 0.910, 0.920, 0.921 and 0.930.
90+
8991
See the [mypy usage](usage/mypy.md) and [plugin configuration](#configuring-the-plugin) docs for more details.
9092

9193
### Plugin Settings

pydantic/class_validators.py

+11-8
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@
99
from .typing import AnyCallable
1010
from .utils import ROOT_KEY, in_ipython
1111

12+
if TYPE_CHECKING:
13+
from .typing import AnyClassMethod
14+
1215

1316
class Validator:
1417
__slots__ = 'func', 'pre', 'each_item', 'always', 'check_fields', 'skip_on_failure'
@@ -54,7 +57,7 @@ def validator(
5457
check_fields: bool = True,
5558
whole: bool = None,
5659
allow_reuse: bool = False,
57-
) -> Callable[[AnyCallable], classmethod]: # type: ignore[type-arg]
60+
) -> Callable[[AnyCallable], 'AnyClassMethod']:
5861
"""
5962
Decorate methods on the class indicating that they should be used to validate fields
6063
:param fields: which field(s) the method should be called on
@@ -81,7 +84,7 @@ def validator(
8184
assert each_item is False, '"each_item" and "whole" conflict, remove "whole"'
8285
each_item = not whole
8386

84-
def dec(f: AnyCallable) -> classmethod: # type: ignore[type-arg]
87+
def dec(f: AnyCallable) -> 'AnyClassMethod':
8588
f_cls = _prepare_validator(f, allow_reuse)
8689
setattr(
8790
f_cls,
@@ -97,20 +100,20 @@ def dec(f: AnyCallable) -> classmethod: # type: ignore[type-arg]
97100

98101

99102
@overload
100-
def root_validator(_func: AnyCallable) -> classmethod: # type: ignore[type-arg]
103+
def root_validator(_func: AnyCallable) -> 'AnyClassMethod':
101104
...
102105

103106

104107
@overload
105108
def root_validator(
106109
*, pre: bool = False, allow_reuse: bool = False, skip_on_failure: bool = False
107-
) -> Callable[[AnyCallable], classmethod]: # type: ignore[type-arg]
110+
) -> Callable[[AnyCallable], 'AnyClassMethod']:
108111
...
109112

110113

111114
def root_validator(
112115
_func: Optional[AnyCallable] = None, *, pre: bool = False, allow_reuse: bool = False, skip_on_failure: bool = False
113-
) -> Union[classmethod, Callable[[AnyCallable], classmethod]]: # type: ignore[type-arg]
116+
) -> Union['AnyClassMethod', Callable[[AnyCallable], 'AnyClassMethod']]:
114117
"""
115118
Decorate methods on a model indicating that they should be used to validate (and perhaps modify) data either
116119
before or after standard model parsing/validation is performed.
@@ -122,7 +125,7 @@ def root_validator(
122125
)
123126
return f_cls
124127

125-
def dec(f: AnyCallable) -> classmethod: # type: ignore[type-arg]
128+
def dec(f: AnyCallable) -> 'AnyClassMethod':
126129
f_cls = _prepare_validator(f, allow_reuse)
127130
setattr(
128131
f_cls, ROOT_VALIDATOR_CONFIG_KEY, Validator(func=f_cls.__func__, pre=pre, skip_on_failure=skip_on_failure)
@@ -132,7 +135,7 @@ def dec(f: AnyCallable) -> classmethod: # type: ignore[type-arg]
132135
return dec
133136

134137

135-
def _prepare_validator(function: AnyCallable, allow_reuse: bool) -> classmethod: # type: ignore[type-arg]
138+
def _prepare_validator(function: AnyCallable, allow_reuse: bool) -> 'AnyClassMethod':
136139
"""
137140
Avoid validators with duplicated names since without this, validators can be overwritten silently
138141
which generally isn't the intended behaviour, don't run in ipython (see #312) or if allow_reuse is False.
@@ -325,7 +328,7 @@ def _generic_validator_basic(validator: AnyCallable, sig: 'Signature', args: Set
325328
return lambda cls, v, values, field, config: validator(v, values=values, field=field, config=config)
326329

327330

328-
def gather_all_validators(type_: 'ModelOrDc') -> Dict[str, classmethod]: # type: ignore[type-arg]
331+
def gather_all_validators(type_: 'ModelOrDc') -> Dict[str, 'AnyClassMethod']:
329332
all_attributes = ChainMap(*[cls.__dict__ for cls in type_.__mro__])
330333
return {
331334
k: v

pydantic/main.py

+4-3
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@
6666
from .types import ModelOrDc
6767
from .typing import (
6868
AbstractSetIntStr,
69+
AnyClassMethod,
6970
CallableGenerator,
7071
DictAny,
7172
DictStrAny,
@@ -890,7 +891,7 @@ def create_model(
890891
__config__: Optional[Type[BaseConfig]] = None,
891892
__base__: None = None,
892893
__module__: str = __name__,
893-
__validators__: Dict[str, classmethod] = None, # type: ignore[type-arg]
894+
__validators__: Dict[str, 'AnyClassMethod'] = None,
894895
**field_definitions: Any,
895896
) -> Type['BaseModel']:
896897
...
@@ -903,7 +904,7 @@ def create_model(
903904
__config__: Optional[Type[BaseConfig]] = None,
904905
__base__: Union[Type['Model'], Tuple[Type['Model'], ...]],
905906
__module__: str = __name__,
906-
__validators__: Dict[str, classmethod] = None, # type: ignore[type-arg]
907+
__validators__: Dict[str, 'AnyClassMethod'] = None,
907908
**field_definitions: Any,
908909
) -> Type['Model']:
909910
...
@@ -915,7 +916,7 @@ def create_model(
915916
__config__: Optional[Type[BaseConfig]] = None,
916917
__base__: Union[None, Type['Model'], Tuple[Type['Model'], ...]] = None,
917918
__module__: str = __name__,
918-
__validators__: Dict[str, classmethod] = None, # type: ignore[type-arg]
919+
__validators__: Dict[str, 'AnyClassMethod'] = None,
919920
**field_definitions: Any,
920921
) -> Type['Model']:
921922
"""

pydantic/mypy.py

+49-26
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,6 @@
11
from configparser import ConfigParser
22
from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Type as TypingType, Union
33

4-
from pydantic.utils import is_valid_field
5-
6-
try:
7-
import toml
8-
except ImportError: # pragma: no cover
9-
# future-proofing for upcoming `mypy` releases which will switch dependencies
10-
try:
11-
import tomli as toml # type: ignore
12-
except ImportError:
13-
import warnings
14-
15-
warnings.warn('No TOML parser installed, cannot read configuration from `pyproject.toml`.')
16-
toml = None # type: ignore
17-
184
from mypy.errorcodes import ErrorCode
195
from mypy.nodes import (
206
ARG_NAMED,
@@ -60,20 +46,29 @@
6046
Type,
6147
TypeOfAny,
6248
TypeType,
63-
TypeVarLikeType,
6449
TypeVarType,
6550
UnionType,
6651
get_proper_type,
6752
)
6853
from mypy.typevars import fill_typevars
6954
from mypy.util import get_unique_redefinition_name
55+
from mypy.version import __version__ as mypy_version
56+
57+
from pydantic.utils import is_valid_field
58+
59+
try:
60+
from mypy.types import TypeVarDef # type: ignore[attr-defined]
61+
except ImportError: # pragma: no cover
62+
# Backward-compatible with TypeVarDef from Mypy 0.910.
63+
from mypy.types import TypeVarType as TypeVarDef
7064

7165
CONFIGFILE_KEY = 'pydantic-mypy'
7266
METADATA_KEY = 'pydantic-mypy-metadata'
7367
BASEMODEL_FULLNAME = 'pydantic.main.BaseModel'
7468
BASESETTINGS_FULLNAME = 'pydantic.env_settings.BaseSettings'
7569
FIELD_FULLNAME = 'pydantic.fields.Field'
7670
DATACLASS_FULLNAME = 'pydantic.dataclasses.dataclass'
71+
BUILTINS_NAME = 'builtins' if float(mypy_version) >= 0.930 else '__builtins__'
7772

7873

7974
def plugin(version: str) -> 'TypingType[Plugin]':
@@ -125,9 +120,9 @@ def __init__(self, options: Options) -> None:
125120
if options.config_file is None: # pragma: no cover
126121
return
127122

128-
if toml and options.config_file.endswith('.toml'):
129-
with open(options.config_file, 'r') as rf:
130-
config = toml.load(rf).get('tool', {}).get('pydantic-mypy', {})
123+
toml_config = parse_toml(options.config_file)
124+
if toml_config is not None:
125+
config = toml_config.get('tool', {}).get('pydantic-mypy', {})
131126
for key in self.__slots__:
132127
setting = config.get(key, False)
133128
if not isinstance(setting, bool):
@@ -348,26 +343,32 @@ def add_construct_method(self, fields: List['PydanticModelField']) -> None:
348343
and does not treat settings fields as optional.
349344
"""
350345
ctx = self._ctx
351-
set_str = ctx.api.named_type('builtins.set', [ctx.api.named_type('builtins.str')])
346+
set_str = ctx.api.named_type(f'{BUILTINS_NAME}.set', [ctx.api.named_type(f'{BUILTINS_NAME}.str')])
352347
optional_set_str = UnionType([set_str, NoneType()])
353348
fields_set_argument = Argument(Var('_fields_set', optional_set_str), optional_set_str, None, ARG_OPT)
354349
construct_arguments = self.get_field_arguments(fields, typed=True, force_all_optional=False, use_alias=False)
355350
construct_arguments = [fields_set_argument] + construct_arguments
356351

357-
obj_type = ctx.api.named_type('builtins.object')
352+
obj_type = ctx.api.named_type(f'{BUILTINS_NAME}.object')
358353
self_tvar_name = '_PydanticBaseModel' # Make sure it does not conflict with other names in the class
359354
tvar_fullname = ctx.cls.fullname + '.' + self_tvar_name
360-
self_type = TypeVarType(self_tvar_name, tvar_fullname, -1, [], obj_type)
355+
tvd = TypeVarDef(self_tvar_name, tvar_fullname, -1, [], obj_type)
361356
self_tvar_expr = TypeVarExpr(self_tvar_name, tvar_fullname, [], obj_type)
362357
ctx.cls.info.names[self_tvar_name] = SymbolTableNode(MDEF, self_tvar_expr)
363358

359+
# Backward-compatible with TypeVarDef from Mypy 0.910.
360+
if isinstance(tvd, TypeVarType):
361+
self_type = tvd
362+
else:
363+
self_type = TypeVarType(tvd) # type: ignore[call-arg]
364+
364365
add_method(
365366
ctx,
366367
'construct',
367368
construct_arguments,
368369
return_type=self_type,
369370
self_type=self_type,
370-
tvar_like_type=self_type,
371+
tvar_def=tvd,
371372
is_classmethod=True,
372373
)
373374

@@ -619,7 +620,7 @@ def add_method(
619620
args: List[Argument],
620621
return_type: Type,
621622
self_type: Optional[Type] = None,
622-
tvar_like_type: Optional[TypeVarLikeType] = None,
623+
tvar_def: Optional[TypeVarDef] = None,
623624
is_classmethod: bool = False,
624625
is_new: bool = False,
625626
# is_staticmethod: bool = False,
@@ -654,10 +655,10 @@ def add_method(
654655
arg_names.append(get_name(arg.variable))
655656
arg_kinds.append(arg.kind)
656657

657-
function_type = ctx.api.named_type('builtins.function')
658+
function_type = ctx.api.named_type(f'{BUILTINS_NAME}.function')
658659
signature = CallableType(arg_types, arg_kinds, arg_names, return_type, function_type)
659-
if tvar_like_type:
660-
signature.variables = [tvar_like_type]
660+
if tvar_def:
661+
signature.variables = [tvar_def]
661662

662663
func = FuncDef(name, args, Block([PassStmt()]))
663664
func.info = info
@@ -714,3 +715,25 @@ def get_name(x: Union[FuncBase, SymbolNode]) -> str:
714715
if callable(fn): # pragma: no cover
715716
return fn()
716717
return fn
718+
719+
720+
def parse_toml(config_file: str) -> Optional[Dict[str, Any]]:
721+
if not config_file.endswith('.toml'):
722+
return None
723+
724+
read_mode = 'rb'
725+
try:
726+
import tomli as toml_
727+
except ImportError:
728+
# older versions of mypy have toml as a dependency, not tomli
729+
read_mode = 'r'
730+
try:
731+
import toml as toml_ # type: ignore[no-redef]
732+
except ImportError: # pragma: no cover
733+
import warnings
734+
735+
warnings.warn('No TOML parser installed, cannot read configuration from `pyproject.toml`.')
736+
return None
737+
738+
with open(config_file, read_mode) as rf:
739+
return toml_.load(rf) # type: ignore[arg-type]

pydantic/typing.py

+2
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,7 @@ def is_union(tp: Optional[Type[Any]]) -> bool:
228228
MappingIntStrAny = Mapping[IntStr, Any]
229229
CallableGenerator = Generator[AnyCallable, None, None]
230230
ReprArgs = Sequence[Tuple[Optional[str], Any]]
231+
AnyClassMethod = classmethod[Any]
231232

232233
__all__ = (
233234
'ForwardRef',
@@ -258,6 +259,7 @@ def is_union(tp: Optional[Type[Any]]) -> bool:
258259
'DictIntStrAny',
259260
'CallableGenerator',
260261
'ReprArgs',
262+
'AnyClassMethod',
261263
'CallableGenerator',
262264
'WithArgsTypes',
263265
'get_args',

pydantic/utils.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -574,7 +574,8 @@ def _coerce_items(items: Union['AbstractSetIntStr', 'MappingIntStrAny']) -> 'Map
574574
elif isinstance(items, AbstractSet):
575575
items = dict.fromkeys(items, ...)
576576
else:
577-
raise TypeError(f'Unexpected type of exclude value {items.__class__}') # type: ignore[attr-defined]
577+
class_name = getattr(items, '__class__', '???')
578+
raise TypeError(f'Unexpected type of exclude value {class_name}')
578579
return items
579580

580581
@classmethod

setup.cfg

+3
Original file line numberDiff line numberDiff line change
@@ -76,3 +76,6 @@ ignore_missing_imports = true
7676

7777
[mypy-dotenv]
7878
ignore_missing_imports = true
79+
80+
[mypy-toml]
81+
ignore_missing_imports = true

0 commit comments

Comments
 (0)