diff --git a/pyproject.toml b/pyproject.toml index d483313..3951594 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,10 +11,23 @@ write_to = "src/validataclass/_version.py" version_scheme = "post-release" [tool.mypy] -files = "src/" +files = ["src/", "tests/"] +mypy_path = "src/" +explicit_package_bases = true # Enable strict type checking strict = true # Ignore errors like `Module "validataclass.exceptions" does not explicitly export attribute "..."` no_implicit_reexport = false + +[[tool.mypy.overrides]] +module = 'tests.*' + +# Don't enforce typed definitions in tests, this is a lot of unnecessary work (most parameters would be Any anyway). +allow_untyped_defs = true + +# TODO: This is the main issue with mypy and validataclass right now. +# Defining dataclasses with validators using the @validataclass decorator, like `some_field: str = StringValidator()`, +# will cause "Incompatible types in assignment" errors. Until we find a way to solve this, ignore this error for now. +disable_error_code = "assignment" diff --git a/setup.cfg b/setup.cfg index 70668f8..ae8085e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -46,3 +46,4 @@ testing = coverage-conditional-plugin ~= 0.5 flake8 ~= 7.0 mypy ~= 1.9 + types-python-dateutil diff --git a/tests/dataclasses/_helpers.py b/tests/dataclasses/_helpers.py index fe7f4be..7aa1e13 100644 --- a/tests/dataclasses/_helpers.py +++ b/tests/dataclasses/_helpers.py @@ -6,24 +6,35 @@ import dataclasses import sys -from typing import Any +from typing import Any, Dict, Type, TYPE_CHECKING import pytest from validataclass.dataclasses import Default +from validataclass.validators import T_Dataclass + +# TODO: Replace type alias with dataclasses.Field[Any] when removing Python 3.9 support. (#15) +if TYPE_CHECKING: + T_DataclassField = dataclasses.Field[Any] +else: + T_DataclassField = dataclasses.Field # Test helpers for dataclass tests -def assert_field_default(field: dataclasses.Field, default_value: Any): +def assert_field_default(field: T_DataclassField, default_value: Any) -> None: """ Asserts that a given (vali-)dataclass field has a specified default value. """ - # Check regular dataclass defaults - assert ( - (field.default == default_value and field.default_factory is dataclasses.MISSING) - or (field.default is dataclasses.MISSING and field.default_factory() == default_value) - ) + # Check that the field has a regular dataclass default VALUE or default FACTORY, but not both + assert field.default is not dataclasses.MISSING or field.default_factory is not dataclasses.MISSING + assert field.default is dataclasses.MISSING or field.default_factory is dataclasses.MISSING + + # Check regular dataclass default + if field.default_factory is not dataclasses.MISSING: + assert field.default_factory() == default_value + else: + assert field.default == default_value # Check defaults in dataclass metadata metadata_default = field.metadata.get('validator_default') @@ -31,7 +42,7 @@ def assert_field_default(field: dataclasses.Field, default_value: Any): assert metadata_default.get_value() == default_value -def assert_field_no_default(field: dataclasses.Field): +def assert_field_no_default(field: T_DataclassField) -> None: """ Asserts that a given (vali-)dataclass field has no default value. """ @@ -43,15 +54,20 @@ def assert_field_no_default(field: dataclasses.Field): # For Python under 3.10, check that an exception raising default_factory is set if sys.version_info < (3, 10): + assert field.default_factory is not dataclasses.MISSING with pytest.raises(TypeError, match="required keyword-only argument"): field.default_factory() else: assert field.default_factory is dataclasses.MISSING -def get_dataclass_fields(cls) -> dict: +def get_dataclass_fields(cls: Type[T_Dataclass]) -> Dict[str, T_DataclassField]: """ Returns a dictionary containing all fields of a given dataclass. """ + # Make sure the class is really a dataclass + assert dataclasses.is_dataclass(cls) and isinstance(cls, type) + + # Get fields and return them as a dictionary fields_tuple = dataclasses.fields(cls) return {field.name: field for field in fields_tuple} diff --git a/tests/dataclasses/defaults_test.py b/tests/dataclasses/defaults_test.py index 88c225b..0416994 100644 --- a/tests/dataclasses/defaults_test.py +++ b/tests/dataclasses/defaults_test.py @@ -5,6 +5,7 @@ """ from copy import copy +from typing import Any, List import pytest @@ -42,7 +43,7 @@ def test_default_immutable_values(value, expected_repr): @staticmethod def test_default_list_deepcopied(): """ Test Default object with a list, make sure that it is deepcopied. """ - default_list = [] + default_list: List[Any] = [] default = Default(default_list) # Check string representation and value diff --git a/tests/dataclasses/validataclass_test.py b/tests/dataclasses/validataclass_test.py index 6af6036..61aad5f 100644 --- a/tests/dataclasses/validataclass_test.py +++ b/tests/dataclasses/validataclass_test.py @@ -5,7 +5,7 @@ """ import dataclasses -from typing import Dict, List, Optional +from typing import Dict, List, Optional, Union import pytest @@ -19,7 +19,7 @@ validataclass_field, ) from validataclass.exceptions import DataclassValidatorFieldException -from validataclass.helpers import OptionalUnset, UnsetValue +from validataclass.helpers import OptionalUnset, UnsetValue, UnsetValueType from validataclass.validators import ( DictValidator, IntegerValidator, @@ -279,7 +279,7 @@ class SubClass(BaseClass): # Check type annotations assert all(fields[field].type is int for field in ['required1', 'required2', 'optional2', 'optional4']) assert all(fields[field].type is Optional[int] for field in ['required3', 'optional1']) - assert all(fields[field].type is OptionalUnset[int] for field in ['required4', 'optional3']) + assert all(fields[field].type is Union[int, UnsetValueType] for field in ['required4', 'optional3']) # Check validators assert all(type(field.metadata.get('validator')) is IntegerValidator for field in fields.values()) @@ -391,7 +391,7 @@ class BaseB: field_both: str = StringValidator() @validataclass - class SubClass(BaseB, BaseA): + class SubClass(BaseB, BaseA): # type: ignore[misc] # Override the defaults to test that the decorator recognizes all fields of both base classes. # If it does not, a "no validator for field X" error would be raised. field_a: int = Default(42) diff --git a/tests/test_utils.py b/tests/test_utils.py index 712ae31..e20fd8c 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -5,12 +5,12 @@ """ from decimal import Decimal -from typing import Any, List, Union +from typing import Any, List, Tuple, Union from validataclass.validators import Validator -def unpack_params(*args) -> List[tuple]: +def unpack_params(*args: Any) -> List[Tuple[Any, ...]]: """ Returns a list containing tuples build from the arguments. @@ -64,7 +64,7 @@ def unpack_params(*args) -> List[tuple]: ] ``` """ - unpacked = [tuple()] + unpacked: List[Tuple[Any, ...]] = [tuple()] for arg in args: if type(arg) is list: diff --git a/tests/validators/dataclass_validator_test.py b/tests/validators/dataclass_validator_test.py index a337298..2409528 100644 --- a/tests/validators/dataclass_validator_test.py +++ b/tests/validators/dataclass_validator_test.py @@ -6,7 +6,7 @@ from dataclasses import dataclass, field from decimal import Decimal -from typing import Optional, List +from typing import Any, Dict, List, Optional import pytest @@ -94,7 +94,7 @@ class UnitTestPostValidationDataclass: start: int = IntegerValidator() end: int = IntegerValidator() - def __post_validate__(self): + def __post_validate__(self) -> None: if self.start > self.end: raise ValidationError(code='invalid_range', reason='"start" must be smaller than or equal to "end".') @@ -110,7 +110,7 @@ class UnitTestContextSensitiveDataclass: name: str = UnitTestContextValidator() value: Optional[int] = IntegerValidator(), Default(None) - def __post_validate__(self, *, value_required: bool = False): + def __post_validate__(self, *, value_required: bool = False) -> None: if value_required and self.value is None: raise DataclassPostValidationError(field_errors={ 'value': RequiredValueError(reason='Value is required in this context.'), @@ -124,7 +124,7 @@ class UnitTestContextSensitiveDataclassWithPosArgs(UnitTestContextSensitiveDatac """ # Same as UnitTestContextSensitiveDataclass, but with positional arguments - def __post_validate__(self, value_required: bool = False): + def __post_validate__(self, value_required: bool = False) -> None: super().__post_validate__(value_required=value_required) @@ -149,7 +149,7 @@ class UnitTestContextSensitiveDataclassWithVarKwargs: ctx_b = None extra_kwargs = None - def __post_validate__(self, *, ctx_a: str = '', ctx_b: str = '', **kwargs): + def __post_validate__(self, *, ctx_a: str = '', ctx_b: str = '', **kwargs: Any) -> None: self.ctx_a = ctx_a self.ctx_b = ctx_b self.extra_kwargs = kwargs @@ -166,7 +166,7 @@ class UnitTestPreValidateStaticMethodDataclass: example_int: int = IntegerValidator() @staticmethod - def __pre_validate__(input_data: dict) -> dict: + def __pre_validate__(input_data: Dict[Any, Any]) -> Dict[Any, Any]: mapping = { 'exampleStr': 'example_str', 'exampleInt': 'example_int', @@ -193,7 +193,7 @@ class UnitTestPreValidateClassMethodDataclass: example_int: int = IntegerValidator() @classmethod - def __pre_validate__(cls, input_data: dict) -> dict: + def __pre_validate__(cls, input_data: Dict[Any, Any]) -> Dict[Any, Any]: for from_key, to_key in cls.__key_mapping.items(): if from_key in input_data: input_data[to_key] = input_data.pop(from_key) @@ -212,7 +212,7 @@ class UnitTestPreValidateContextSensitiveDataclass: target_field: int = IntegerValidator() @classmethod - def __pre_validate__(cls, input_data: dict, *, source_field_name: str) -> dict: + def __pre_validate__(cls, input_data: Dict[Any, Any], *, source_field_name: str) -> Dict[Any, Any]: if source_field_name in input_data: return {'target_field': input_data[source_field_name]} else: @@ -236,7 +236,7 @@ class UnitTestPreValidateContextSensitiveVarKwargsDataclass: example_int: int = IntegerValidator() @classmethod - def __pre_validate__(cls, input_data: dict, **kwargs) -> dict: + def __pre_validate__(cls, input_data: Dict[Any, Any], **kwargs: Any) -> Dict[Any, Any]: # Fill input_data with default values based on kwargs for key, default_value in kwargs.items(): if key not in input_data: @@ -252,7 +252,7 @@ class UnitTestInvalidPreValidateDataclass1: """ Dataclass with invalid __pre_validate__ class method: Not enough arguments. """ @classmethod - def __pre_validate__(cls) -> dict: + def __pre_validate__(cls) -> Dict[Any, Any]: return {} @@ -261,7 +261,7 @@ class UnitTestInvalidPreValidateDataclass2: """ Dataclass with invalid __pre_validate__ static method: Not enough arguments. """ @staticmethod - def __pre_validate__() -> dict: + def __pre_validate__() -> Dict[Any, Any]: return {} @@ -270,7 +270,7 @@ class UnitTestInvalidPreValidateDataclass3: """ Dataclass with invalid __pre_validate__ class method: Too many positional arguments. """ @classmethod - def __pre_validate__(cls, input_data: dict, _extra_pos_argument) -> dict: + def __pre_validate__(cls, input_data: Dict[Any, Any], _extra_pos_argument: Any) -> Dict[Any, Any]: return input_data @@ -279,7 +279,7 @@ class UnitTestInvalidPreValidateDataclass4: """ Dataclass with invalid __pre_validate__ static method: Too many positional arguments. """ @staticmethod - def __pre_validate__(input_data: dict, _extra_pos_argument) -> dict: + def __pre_validate__(input_data: Dict[Any, Any], _extra_pos_argument: Any) -> Dict[Any, Any]: return input_data @@ -288,7 +288,7 @@ class UnitTestInvalidPreValidateDataclass5: """ Dataclass with invalid __pre_validate__ class method: Too many (variable) positional arguments. """ @classmethod - def __pre_validate__(cls, input_data: dict, *_args) -> dict: + def __pre_validate__(cls, input_data: Dict[Any, Any], *_args: Any) -> Dict[Any, Any]: return input_data @@ -297,7 +297,7 @@ class UnitTestInvalidPreValidateDataclass6: """ Dataclass with invalid __pre_validate__ static method: Too many (variable) positional arguments. """ @staticmethod - def __pre_validate__(input_data: dict, *_args) -> dict: + def __pre_validate__(input_data: Dict[Any, Any], *_args: Any) -> Dict[Any, Any]: return input_data @@ -839,7 +839,8 @@ def test_dataclass_with_pre_validate_methods( dataclass_cls, ): """ Validate dataclasses with different __pre_validate__() methods (static and class methods). """ - validator = DataclassValidator(dataclass_cls) + validator: DataclassValidator[Any] = DataclassValidator(dataclass_cls) + validated_data = validator.validate(input_data) assert validated_data.example_str == expected_example_str @@ -897,7 +898,7 @@ def test_dataclass_with_pre_validate_methods_invalid( dataclass_cls, ): """ Validate dataclasses with different __pre_validate__() methods and invalid input. """ - validator = DataclassValidator(dataclass_cls) + validator: DataclassValidator[Any] = DataclassValidator(dataclass_cls) with pytest.raises(DictFieldsValidationError) as exception_info: validator.validate(input_data) @@ -1048,7 +1049,7 @@ def test_dataclass_with_context_sensitive_pre_validate_with_var_kwargs_invalid( ) def test_dataclass_with_invalid_forms_of_pre_validate(dataclass_cls): """ Test error handling for dataclasses with __pre_validate__() methods with an invalid method signature. """ - validator = DataclassValidator(dataclass_cls) + validator: DataclassValidator[Any] = DataclassValidator(dataclass_cls) with pytest.raises(DataclassInvalidPreValidateSignatureException, match=PRE_VALIDATE_INVALID_SIGNATURE_ERROR): validator.validate({}) diff --git a/tests/validators/datetime_validator_test.py b/tests/validators/datetime_validator_test.py index 7810ef9..6e5ad00 100644 --- a/tests/validators/datetime_validator_test.py +++ b/tests/validators/datetime_validator_test.py @@ -506,10 +506,14 @@ def test_with_local_timezone_valid(input_string, local_timezone, expected_dateti assert validated_dt == expected_datetime # Check timezone of datetimes by comparing their offset to UTC - assert ( - validated_dt.tzinfo == expected_datetime.tzinfo - or validated_dt.tzinfo.utcoffset(validated_dt) == expected_datetime.tzinfo.utcoffset(expected_datetime) - ) + if expected_datetime.tzinfo is None: + assert validated_dt.tzinfo is None + else: + assert validated_dt.tzinfo is not None + assert ( + validated_dt.tzinfo == expected_datetime.tzinfo + or validated_dt.tzinfo.utcoffset(validated_dt) == expected_datetime.tzinfo.utcoffset(expected_datetime) + ) # Test DateTimeValidator with target_timezone parameter diff --git a/tests/validators/dict_validator_test.py b/tests/validators/dict_validator_test.py index 739fd68..716ad06 100644 --- a/tests/validators/dict_validator_test.py +++ b/tests/validators/dict_validator_test.py @@ -563,7 +563,7 @@ class UnitTestDictValidator(DictValidator): 'value': DecimalValidator(), 'optional_value': DecimalValidator(), } - required_fields = ['name', 'value'] + required_fields = {'name', 'value'} validator = UnitTestDictValidator() assert validator.validate(input_dict) == expected_output @@ -608,7 +608,7 @@ class UnitTestDictValidator(DictValidator): 'value': DecimalValidator(), 'optional_value': DecimalValidator(), } - required_fields = ['name', 'value'] + required_fields = {'name', 'value'} validator = UnitTestDictValidator() diff --git a/tests/validators/discard_validator_test.py b/tests/validators/discard_validator_test.py index 2b5a6c3..9949bf0 100644 --- a/tests/validators/discard_validator_test.py +++ b/tests/validators/discard_validator_test.py @@ -4,6 +4,8 @@ Use of this source code is governed by an MIT-style license that can be found in the LICENSE file. """ +from typing import Any, List + import pytest from validataclass.helpers import UnsetValue @@ -15,7 +17,7 @@ class DiscardValidatorTest: Unit tests for the DiscardValidator. """ - example_input_data = [ + example_input_data: List[Any] = [ None, True, False, diff --git a/tests/validators/float_validator_test.py b/tests/validators/float_validator_test.py index a8b05fa..3d423f7 100644 --- a/tests/validators/float_validator_test.py +++ b/tests/validators/float_validator_test.py @@ -4,6 +4,8 @@ Use of this source code is governed by an MIT-style license that can be found in the LICENSE file. """ +from typing import Any, Dict + import pytest from validataclass.exceptions import ( @@ -131,7 +133,7 @@ def test_float_value_range_invalid(min_value, max_value, input_data_list): validator = FloatValidator(min_value=min_value, max_value=max_value) # Construct error dict with min_value and/or max_value, depending on which is specified - expected_error_dict = {'code': 'number_range_error'} + expected_error_dict: Dict[str, Any] = {'code': 'number_range_error'} expected_error_dict.update({'min_value': float(min_value)} if min_value is not None else {}) expected_error_dict.update({'max_value': float(max_value)} if max_value is not None else {}) diff --git a/tests/validators/list_validator_test.py b/tests/validators/list_validator_test.py index 01ed1d8..4506c30 100644 --- a/tests/validators/list_validator_test.py +++ b/tests/validators/list_validator_test.py @@ -29,21 +29,21 @@ class ListValidatorTest: @staticmethod def test_valid_integer_list(): """ Test ListValidator with IntegerValidator as item validator with valid integers. """ - validator = ListValidator(item_validator=IntegerValidator()) + validator: ListValidator[int] = ListValidator(item_validator=IntegerValidator()) assert validator.validate([123, 0, -42, 123]) == [123, 0, -42, 123] @staticmethod def test_valid_integer_list_empty(): """ Test ListValidator with IntegerValidator as item validator with an empty list. """ - validator = ListValidator(item_validator=IntegerValidator()) + validator: ListValidator[int] = ListValidator(item_validator=IntegerValidator()) assert validator.validate([]) == [] @staticmethod def test_valid_decimal_list(): """ Test ListValidator with DecimalValidator as item validator with valid decimal strings. """ - validator = ListValidator(item_validator=DecimalValidator()) + validator: ListValidator[Decimal] = ListValidator(item_validator=DecimalValidator()) output_list = validator.validate(['3.1415', '-0.42', '0']) assert all(isinstance(item, Decimal) for item in output_list) @@ -86,7 +86,7 @@ def test_valid_decimal_list(): ) def test_valid_nested_list(input_list, expected_output): """ Test nested ListValidator to validate lists of lists of decimals. """ - validator = ListValidator(ListValidator(DecimalValidator())) + validator: ListValidator[Decimal] = ListValidator(ListValidator(DecimalValidator())) output_list = validator.validate(input_list) for sublist in output_list: @@ -100,7 +100,7 @@ def test_valid_nested_list(input_list, expected_output): @staticmethod def test_invalid_none(): """ Check that ListValidator raises exceptions for None as value. """ - validator = ListValidator(item_validator=IntegerValidator()) + validator: ListValidator[int] = ListValidator(item_validator=IntegerValidator()) with pytest.raises(RequiredValueError) as exception_info: validator.validate(None) @@ -119,7 +119,7 @@ def test_invalid_none(): ) def test_invalid_not_a_list(input_data): """ Check that ListValidator raises exceptions for values that are not of type 'list'. """ - validator = ListValidator(item_validator=StringValidator()) + validator: ListValidator[str] = ListValidator(item_validator=StringValidator()) with pytest.raises(InvalidTypeError) as exception_info: validator.validate(input_data) @@ -132,7 +132,7 @@ def test_invalid_not_a_list(input_data): @staticmethod def test_invalid_decimal_list_items(): """ Test ListValidator with DecimalValidator as item validator with invalid list items. """ - validator = ListValidator(item_validator=DecimalValidator()) + validator: ListValidator[Decimal] = ListValidator(item_validator=DecimalValidator()) with pytest.raises(ListItemsValidationError) as exception_info: # Indices 1 and 4 are valid; indices 0, 2, 3 raise errors @@ -158,7 +158,7 @@ def test_invalid_decimal_list_items(): @staticmethod def test_with_context_arguments(): """ Test that ListValidator passes context arguments down to the item validator. """ - validator = ListValidator(item_validator=UnitTestContextValidator()) + validator: ListValidator[str] = ListValidator(item_validator=UnitTestContextValidator()) assert validator.validate(['unit', 'test']) == [ "unit / {}", @@ -201,7 +201,7 @@ def test_with_context_arguments(): ) def test_list_length_valid(min_length, max_length, input_data): """ Test ListValidator with length requirements with lists of the correct length. """ - validator = ListValidator( + validator: ListValidator[int] = ListValidator( IntegerValidator(), min_length=min_length, max_length=max_length, @@ -239,7 +239,7 @@ def test_list_length_valid(min_length, max_length, input_data): ) def test_list_length_invalid(min_length, max_length, input_data): """ Test ListValidator with length requirements with lists of the wrong length. """ - validator = ListValidator( + validator: ListValidator[int] = ListValidator( IntegerValidator(), min_length=min_length, max_length=max_length, @@ -276,7 +276,7 @@ def test_list_length_invalid(min_length, max_length, input_data): ) def test_discarding_invalid_items(input_data, expected_output): """ Test that ListValidator with discard_invalid=True discards invalid items. """ - validator = ListValidator( + validator: ListValidator[int] = ListValidator( item_validator=IntegerValidator(), discard_invalid=True, ) @@ -305,7 +305,7 @@ def test_discard_invalid_with_length_requirements_valid(input_data, expected_out """ Test that ListValidator with discard_invalid=True handles length requirements correctly, with valid input. """ - validator = ListValidator( + validator: ListValidator[int] = ListValidator( IntegerValidator(), min_length=2, max_length=4, @@ -339,7 +339,7 @@ def test_discard_invalid_with_length_requirements_invalid(input_data): Before item validation, minimum and maximum length must be checked. After item validation (and potential discarding), the minimum length needs to be checked again. """ - validator = ListValidator( + validator: ListValidator[int] = ListValidator( IntegerValidator(), min_length=2, max_length=4, diff --git a/tests/validators/regex_validator_test.py b/tests/validators/regex_validator_test.py index d2d3d4b..7299616 100644 --- a/tests/validators/regex_validator_test.py +++ b/tests/validators/regex_validator_test.py @@ -438,7 +438,7 @@ def test_custom_error_class_invalid_type(): Test that RegexValidator raises an error on init if the custom error class is not a ValidatonError subclass. """ with pytest.raises(TypeError, match='Custom error class must be a subclass of ValidationError'): - RegexValidator('[0-9]', custom_error_class=Exception) # noqa + RegexValidator('[0-9]', custom_error_class=Exception) # type: ignore[arg-type] # noqa # Tests with length requirements diff --git a/tests/validators/reject_validator_test.py b/tests/validators/reject_validator_test.py index d93a622..86cfad4 100644 --- a/tests/validators/reject_validator_test.py +++ b/tests/validators/reject_validator_test.py @@ -80,7 +80,7 @@ def test_reject_none(allow_none): def test_allow_none(): """ Test that RejectValidator allows None if allow_none is True. """ validator = RejectValidator(allow_none=True) - assert validator.validate(None) is None + assert validator.validate(None) is None # type: ignore[func-returns-value] # Tests with custom errors @@ -180,4 +180,4 @@ def test_custom_error_class_invalid_type(): Test that RejectValidator raises an error on construction if the error class is not a ValidatonError subclass. """ with pytest.raises(TypeError, match='Error class must be a subclass of ValidationError'): - RejectValidator(error_class=Exception) # noqa + RejectValidator(error_class=Exception) # type: ignore[arg-type] # noqa diff --git a/tests/validators/validator_test.py b/tests/validators/validator_test.py index 775c229..b81ab07 100644 --- a/tests/validators/validator_test.py +++ b/tests/validators/validator_test.py @@ -24,7 +24,7 @@ def test_validate_without_kwargs_deprecation(): # Ensure that Validator creation causes a DeprecationWarning with pytest.deprecated_call(): class ValidatorWithoutKwargs(Validator): - def validate(self, input_data: Any) -> Any: # noqa (missing parameter) + def validate(self, input_data: Any) -> Any: # type: ignore[override] # noqa return input_data # Check that validate_with_context() calls validate() without errors diff --git a/tox.ini b/tox.ini index 90371bd..298ef83 100644 --- a/tox.ini +++ b/tox.ini @@ -24,8 +24,7 @@ deps = flake8 commands = flake8 src/ tests/ [testenv:mypy,py{312,311,310,39,38}-mypy] -skip_install = true -deps = mypy +extras = testing commands = mypy [testenv:clean]