mirror of
https://github.com/kennethreitz/pydantic.git
synced 2026-06-05 23:00:18 +00:00
594effa279
* working on core schema generation * adapting main.py * getting tests to run * fix tests * disable pyright, fix mypy * moving to class-based model generation * working on validators * change how models are created * start fixing test_main.py * fixing mypy * SelfType * recursive models working, more tests fixed * fix tests on <3.10 * get docs build to pass * starting to cleanup types.py * starting works on custom types * working on using annotated-types * using annoated types for constraints * lots of cleanup, fixing network tests * network tests passing 🎉 * working on types * working on types and cleanup * fixing UUID type, restructing again * more types and newer pydantic-core * working on Iterable * more test_types tests * support newer pydantic-core, fixing more test_types.py * working through more test_types.py * test_types.py at last passing locally 🎉 * fixing more tests in test_types.py * fix datetime_parse tests and linting * get tests running again, rename to test_datetime.py * renaming internal modules * working through mypy errors * fixing mypy * refactoring _generate_schema.py * test_main.py passing * uprev deps * fix conftest and linting? * importing Annotated * ltining * import Annotated from typing_extensions * fixing 3.7 compatibility * fixing tests on 3.9 * fix linting * fixing SecretField and 3.9 tests * customising get_type_hints * ignore warnings on 3.11 * spliting repr out of utils * removing unused bits of _repr, fix tests for 3.7 * more cleanup, removing many type aliases * clean up repr * support namedtuples and typeddicts * test is_union * removing errors, uprev pydantic-core * fix tests on 3.8 * fixing private attributes and model_post_init * renaming and cleanup * remove unnecessary PydanticMetadata inheritance * fixing forward refs and mypy tests * fix signatures, change how xfail works * revert mypy tests to 3.7 syntax * correct model title * try to fix tests * fixing ClassVar forward refs * uprev pydantic-core, new error format * add "force" argument to model_rebuild * Apply suggestions from code review Suggestions from @tiangolo and @hramezani 🙏 Co-authored-by: Hasan Ramezani <hasan.r67@gmail.com> Co-authored-by: Sebastián Ramírez <tiangolo@gmail.com> * more suggestions from @tiangolo * extra -> json_schema_extra on Field Co-authored-by: Hasan Ramezani <hasan.r67@gmail.com> Co-authored-by: Sebastián Ramírez <tiangolo@gmail.com>
153 lines
6.7 KiB
Python
153 lines
6.7 KiB
Python
import importlib
|
|
import os
|
|
import re
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
try:
|
|
from mypy import api as mypy_api
|
|
from mypy.version import __version__ as mypy_version
|
|
|
|
from pydantic.mypy import parse_mypy_version
|
|
except ImportError:
|
|
mypy_api = None
|
|
mypy_version = None
|
|
parse_mypy_version = lambda _: (0,) # noqa: E731
|
|
|
|
|
|
pytestmark = pytest.mark.skipif(sys.platform != 'linux' and 'CI' in os.environ, reason='only run on linux when on CI')
|
|
|
|
# This ensures mypy can find the test files, no matter where tests are run from:
|
|
os.chdir(Path(__file__).parent.parent.parent)
|
|
|
|
cases = [
|
|
('mypy-plugin.ini', 'plugin_success.py', None),
|
|
('mypy-plugin.ini', 'plugin_fail.py', 'plugin-fail.txt'),
|
|
('mypy-plugin.ini', 'custom_constructor.py', 'custom_constructor.txt'),
|
|
('mypy-plugin-strict.ini', 'plugin_success.py', 'plugin-success-strict.txt'),
|
|
('mypy-plugin-strict.ini', 'plugin_fail.py', 'plugin-fail-strict.txt'),
|
|
('mypy-plugin-strict.ini', 'fail_defaults.py', 'fail_defaults.txt'),
|
|
('mypy-default.ini', 'success.py', None),
|
|
('mypy-default.ini', 'fail1.py', 'fail1.txt'),
|
|
('mypy-default.ini', 'fail2.py', 'fail2.txt'),
|
|
('mypy-default.ini', 'fail3.py', 'fail3.txt'),
|
|
('mypy-default.ini', 'fail4.py', 'fail4.txt'),
|
|
('mypy-default.ini', 'plugin_success.py', 'plugin_success.txt'),
|
|
pytest.param(
|
|
'mypy-plugin-strict-no-any.ini', 'dataclass_no_any.py', None, marks=pytest.mark.xfail(reason='TODO dataclasses')
|
|
),
|
|
('pyproject-default.toml', 'success.py', None),
|
|
('pyproject-default.toml', 'fail1.py', 'fail1.txt'),
|
|
('pyproject-default.toml', 'fail2.py', 'fail2.txt'),
|
|
('pyproject-default.toml', 'fail3.py', 'fail3.txt'),
|
|
('pyproject-default.toml', 'fail4.py', 'fail4.txt'),
|
|
('pyproject-plugin.toml', 'plugin_success.py', None),
|
|
('pyproject-plugin.toml', 'plugin_fail.py', 'plugin-fail.txt'),
|
|
('pyproject-plugin-strict.toml', 'plugin_success.py', 'plugin-success-strict.txt'),
|
|
('pyproject-plugin-strict.toml', 'plugin_fail.py', 'plugin-fail-strict.txt'),
|
|
('pyproject-plugin-strict.toml', 'fail_defaults.py', 'fail_defaults.txt'),
|
|
('mypy-plugin-strict.ini', 'plugin_default_factory.py', 'plugin_default_factory.txt'),
|
|
]
|
|
executable_modules = list({fname[:-3] for _, fname, out_fname in cases if out_fname is None})
|
|
|
|
|
|
@pytest.mark.skipif(not mypy_api, reason='mypy is not installed')
|
|
@pytest.mark.parametrize('config_filename,python_filename,output_filename', cases)
|
|
def test_mypy_results(config_filename: str, python_filename: str, output_filename: str) -> None:
|
|
full_config_filename = f'tests/mypy/configs/{config_filename}'
|
|
full_filename = f'tests/mypy/modules/{python_filename}'
|
|
output_path = None if output_filename is None else Path(f'tests/mypy/outputs/{output_filename}')
|
|
|
|
# Specifying a different cache dir for each configuration dramatically speeds up subsequent execution
|
|
# It also prevents cache-invalidation-related bugs in the tests
|
|
cache_dir = f'.mypy_cache/test-{os.path.splitext(config_filename)[0]}'
|
|
command = [full_filename, '--config-file', full_config_filename, '--cache-dir', cache_dir, '--show-error-codes']
|
|
print(f"\nExecuting: mypy {' '.join(command)}") # makes it easier to debug as necessary
|
|
actual_result = mypy_api.run(command)
|
|
actual_out, actual_err, actual_returncode = actual_result
|
|
# Need to strip filenames due to differences in formatting by OS
|
|
actual_out = '\n'.join(['.py:'.join(line.split('.py:')[1:]) for line in actual_out.split('\n') if line]).strip()
|
|
actual_out = re.sub(r'\n\s*\n', r'\n', actual_out)
|
|
|
|
if actual_out:
|
|
print('{0}\n{1:^100}\n{0}\n{2}\n{0}'.format('=' * 100, 'mypy output', actual_out))
|
|
|
|
assert actual_err == ''
|
|
expected_returncode = 0 if output_filename is None else 1
|
|
assert actual_returncode == expected_returncode
|
|
|
|
if output_path and not output_path.exists():
|
|
output_path.write_text(actual_out)
|
|
raise RuntimeError(f'wrote actual output to {output_path} since file did not exist')
|
|
|
|
expected_out = Path(output_path).read_text().rstrip('\n') if output_path else ''
|
|
|
|
# fix for compatibility between mypy versions: (this can be dropped once we drop support for mypy<0.930)
|
|
if actual_out and parse_mypy_version(mypy_version) < (0, 930):
|
|
actual_out = actual_out.lower()
|
|
expected_out = expected_out.lower()
|
|
actual_out = actual_out.replace('variant:', 'variants:')
|
|
actual_out = re.sub(r'^(\d+: note: {4}).*', r'\1...', actual_out, flags=re.M)
|
|
expected_out = re.sub(r'^(\d+: note: {4}).*', r'\1...', expected_out, flags=re.M)
|
|
|
|
assert actual_out == expected_out, actual_out
|
|
|
|
|
|
@pytest.mark.skipif(not mypy_api, reason='mypy is not installed')
|
|
def test_bad_toml_config() -> None:
|
|
full_config_filename = 'tests/mypy/configs/pyproject-plugin-bad-param.toml'
|
|
full_filename = 'tests/mypy/modules/success.py'
|
|
|
|
# Specifying a different cache dir for each configuration dramatically speeds up subsequent execution
|
|
# It also prevents cache-invalidation-related bugs in the tests
|
|
cache_dir = '.mypy_cache/test-pyproject-plugin-bad-param'
|
|
command = [full_filename, '--config-file', full_config_filename, '--cache-dir', cache_dir, '--show-error-codes']
|
|
print(f"\nExecuting: mypy {' '.join(command)}") # makes it easier to debug as necessary
|
|
with pytest.raises(ValueError) as e:
|
|
mypy_api.run(command)
|
|
|
|
assert str(e.value) == 'Configuration value must be a boolean for key: init_forbid_extra'
|
|
|
|
|
|
@pytest.mark.xfail(reason='todo')
|
|
@pytest.mark.parametrize('module', executable_modules)
|
|
def test_success_cases_run(module: str) -> None:
|
|
"""
|
|
Ensure the "success" files can actually be executed
|
|
"""
|
|
importlib.import_module(f'tests.mypy.modules.{module}')
|
|
|
|
|
|
def test_explicit_reexports():
|
|
from pydantic import __all__ as root_all
|
|
from pydantic.main import __all__ as main
|
|
from pydantic.networks import __all__ as networks
|
|
from pydantic.tools import __all__ as tools
|
|
from pydantic.types import __all__ as types
|
|
|
|
for name, export_all in [('main', main), ('network', networks), ('tools', tools), ('types', types)]:
|
|
for export in export_all:
|
|
assert export in root_all, f'{export} is in {name}.__all__ but missing from re-export in __init__.py'
|
|
|
|
|
|
def test_explicit_reexports_exist():
|
|
import pydantic
|
|
|
|
for name in pydantic.__all__:
|
|
assert hasattr(pydantic, name), f'{name} is in pydantic.__all__ but missing from pydantic'
|
|
|
|
|
|
@pytest.mark.skipif(mypy_version is None, reason='mypy is not installed')
|
|
@pytest.mark.parametrize(
|
|
'v_str,v_tuple',
|
|
[
|
|
('0', (0,)),
|
|
('0.930', (0, 930)),
|
|
('0.940+dev.04cac4b5d911c4f9529e6ce86a27b44f28846f5d.dirty', (0, 940)),
|
|
],
|
|
)
|
|
def test_parse_mypy_version(v_str, v_tuple):
|
|
assert parse_mypy_version(v_str) == v_tuple
|