From c24d33e5f13334a7131965983973a095059dfc2f Mon Sep 17 00:00:00 2001 From: Arseny Boykov <36469655+Bobronium@users.noreply.github.com> Date: Sun, 14 Aug 2022 20:21:53 +0400 Subject: [PATCH] Generate docs exampels for Python 3.10 and above (#4339) * Generate docs exampels for Python 3.10 and above Code quality is not great and main intent here is to show the result. * Fix docs build on 3.9 * Build docs on 3.10 * What's Python 3.1? * Create temp dir if not exists * Refactor and improve imlementetion * Keep runtime typing in examples * Revert unrelated formatting changes * Add changes file * Allow specifying requirements in examples * Pin autoflake and pyupgrade * Add docs/build to Makefile lint/format/mypy * ignore_missing_imports for ansi2html and devtools * Add .tmp-projections to .gitignore * Remove dont-upgrade now when Pattern is supported * Update postponed evaluation examples Co-authored-by: Samuel Colvin --- .github/workflows/ci.yml | 2 +- .gitignore | 2 + Makefile | 11 +- changes/4339-Bobronium.md | 1 + docs/build/exec_examples.py | 364 +++++++++++----- docs/build/main.py | 2 +- docs/build/schema_mapping.py | 392 +++++++++--------- docs/datamodel_code_generator.md | 4 +- docs/examples/generate_models_person_model.py | 1 + docs/examples/index_error.py | 1 + docs/examples/postponed_annotations_broken.py | 20 +- docs/examples/postponed_annotations_main.py | 5 +- docs/examples/postponed_annotations_works.py | 9 +- docs/examples/settings_disable_source.py | 1 + docs/examples/types_bare_type.py | 1 + ...types_infinite_generator_validate_first.py | 3 +- docs/examples/validation_decorator_async.py | 1 + docs/hypothesis_plugin.md | 5 +- docs/index.md | 14 +- docs/mypy_plugin.md | 8 +- docs/requirements.txt | 3 + docs/usage/dataclasses.md | 58 +-- docs/usage/devtools.md | 4 +- docs/usage/exporting_models.md | 75 +--- docs/usage/model_config.md | 45 +- docs/usage/models.md | 166 ++------ docs/usage/mypy.md | 4 +- docs/usage/postponed_annotations.md | 28 +- docs/usage/schema.md | 73 +--- docs/usage/settings.md | 28 +- docs/usage/types.md | 159 ++----- docs/usage/validation_decorator.md | 45 +- docs/usage/validators.md | 35 +- mkdocs.yml | 5 +- setup.cfg | 8 + 35 files changed, 664 insertions(+), 919 deletions(-) create mode 100644 changes/4339-Bobronium.md diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 95454048ee..f3fb083748 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -70,7 +70,7 @@ jobs: - name: set up python uses: actions/setup-python@v4 with: - python-version: 3.9 + python-version: "3.10" - uses: actions/cache@v3 id: cache diff --git a/.gitignore b/.gitignore index 653f3d51f1..4a04924a7e 100644 --- a/.gitignore +++ b/.gitignore @@ -21,6 +21,8 @@ test.py /docs/.version.md /docs/.tmp_schema_mappings.html /docs/.tmp_examples/ +/docs/.tmp-projections/ +/docs/usage/.tmp-projections/ /site/ /site.zip .pytest_cache/ diff --git a/Makefile b/Makefile index e4eed75b7a..5b3dc1e4d1 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,7 @@ .DEFAULT_GOAL := all -isort = isort pydantic tests -black = black -S -l 120 --target-version py38 pydantic tests +sources = pydantic tests docs/build +isort = isort $(sources) +black = black -S -l 120 --target-version py38 $(sources) .PHONY: install-linting install-linting: @@ -35,13 +36,13 @@ build: .PHONY: format format: - pyupgrade --py37-plus --exit-zero-even-if-changed `find pydantic tests -name "*.py" -type f` + pyupgrade --py37-plus --exit-zero-even-if-changed `find $(sources) -name "*.py" -type f` $(isort) $(black) .PHONY: lint lint: - flake8 pydantic/ tests/ + flake8 $(sources) $(isort) --check-only --df $(black) --check --diff @@ -53,7 +54,7 @@ check-dist: .PHONY: mypy mypy: - mypy pydantic + mypy pydantic docs/build .PHONY: pyupgrade pyupgrade: diff --git a/changes/4339-Bobronium.md b/changes/4339-Bobronium.md new file mode 100644 index 0000000000..b64eb4680c --- /dev/null +++ b/changes/4339-Bobronium.md @@ -0,0 +1 @@ +Add Python 3.9 and 3.10 examples to docs diff --git a/docs/build/exec_examples.py b/docs/build/exec_examples.py index 70b41425db..34a4a4f7f1 100755 --- a/docs/build/exec_examples.py +++ b/docs/build/exec_examples.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 +from __future__ import annotations import importlib -import inspect import json import os import re @@ -9,8 +9,8 @@ import sys import textwrap import traceback -from pathlib import Path, PosixPath -from typing import Any, List, Tuple +from pathlib import Path +from typing import Any, Callable from unittest.mock import patch from ansi2html import Ansi2HTMLConverter @@ -20,9 +20,31 @@ DOCS_DIR = (THIS_DIR / '..').resolve() EXAMPLES_DIR = DOCS_DIR / 'examples' TMP_EXAMPLES_DIR = DOCS_DIR / '.tmp_examples' -MAX_LINE_LENGTH = int(re.search(r'max_line_length = (\d+)', (EXAMPLES_DIR / '.editorconfig').read_text()).group(1)) +UPGRADED_TMP_EXAMPLES_DIR = TMP_EXAMPLES_DIR / 'upgraded' + +MAX_LINE_LENGTH = int( + re.search(r'max_line_length = (\d+)', (EXAMPLES_DIR / '.editorconfig').read_text()).group(1) # type: ignore +) LONG_LINE = 50 +LOWEST_VERSION = (3, 7) +HIGHEST_VERSION = (3, 10) pformat = PrettyFormat(simple_cutoff=LONG_LINE) +Error = Callable[..., None] +Version = tuple[int, int] + +PYTHON_CODE_MD_TMPL = """ +=== "Python {version} and above" + + ```py +{code} + ``` +""".strip() +JSON_OUTPUT_MD_TMPL = """ +Outputs: +```json +{output} +``` +""".strip() def to_string(value: Any) -> str: @@ -46,34 +68,27 @@ def to_string(value: Any) -> str: class MockPrint: - def __init__(self, file: Path): + def __init__(self, file: Path) -> None: self.file = file - self.statements = [] + self.statements: list[tuple[int, str]] = [] + + def __call__(self, *args: Any, sep: str = ' ', **kwargs: Any) -> None: + frame = sys._getframe(4) if sys.version_info >= (3, 8) else sys._getframe(3) - def __call__(self, *args, file=None, flush=None): - frame = inspect.currentframe().f_back.f_back.f_back - if sys.version_info >= (3, 8): - frame = frame.f_back if not self.file.samefile(frame.f_code.co_filename): # happens when index_error.py imports index_main.py return - s = ' '.join(map(to_string, args)) + s = sep.join(map(to_string, args)) self.statements.append((frame.f_lineno, s)) -class MockPath(PosixPath): - def __new__(cls, name, *args, **kwargs): - if name == 'config.json': - return cls._from_parts(name, *args, **kwargs) - else: - return Path.__new__(cls, name, *args, **kwargs) - - def read_text(self, *args, **kwargs) -> str: +class MockPath: + def read_text(self, *args: Any, **kwargs: Any) -> str: return '{"foobar": "spam"}' -def build_print_lines(s: str, max_len_reduction: int = 0): +def build_print_lines(s: str, max_len_reduction: int = 0) -> list[str]: print_lines = [] max_len = MAX_LINE_LENGTH - 3 - max_len_reduction for line in s.split('\n'): @@ -84,7 +99,7 @@ def build_print_lines(s: str, max_len_reduction: int = 0): return print_lines -def build_print_statement(line_no: int, s: str, lines: List[str]) -> None: +def build_print_statement(line_no: int, s: str, lines: list[str]) -> None: indent = '' for back in range(1, 100): m = re.search(r'^( *)print\(', lines[line_no - back]) @@ -107,7 +122,7 @@ def all_md_contents() -> str: return '\n\n\n'.join(file_contents) -def gen_ansi_output(): +def gen_ansi_output() -> None: conv = Ansi2HTMLConverter() @@ -122,45 +137,196 @@ def gen_ansi_output(): dont_execute_re = re.compile(r'^# dont-execute\n', flags=re.M | re.I) +dont_upgrade_re = re.compile(r'^# dont-upgrade\n', flags=re.M | re.I) +requires_re = re.compile(r'^# requires: *(.+)\n', flags=re.M | re.I) required_py_re = re.compile(r'^# *requires *python *(\d+).(\d+)', flags=re.M) -def should_execute(file_name: str, file_text: str) -> Tuple[str, bool]: - if dont_execute_re.search(file_text): - return dont_execute_re.sub('', file_text), False +def should_execute(file_name: str, file_text: str) -> tuple[str, bool, Version]: m = required_py_re.search(file_text) if m: - if sys.version_info >= tuple(int(v) for v in m.groups()): - return required_py_re.sub('', file_text), True + lowest_version = (int(m.groups()[0]), int(m.groups()[1])) + if sys.version_info >= lowest_version: + return required_py_re.sub('', file_text), True, lowest_version else: v = '.'.join(m.groups()) print(f'WARNING: {file_name} requires python {v}, not running') - return required_py_re.sub(f'# requires python {v}, NOT EXECUTED!', file_text), False + return ( + required_py_re.sub(f'# requires python {v}, NOT EXECUTED!', file_text), + False, + lowest_version, + ) + elif dont_execute_re.search(file_text): + return dont_execute_re.sub('', file_text), False, LOWEST_VERSION + return file_text, True, LOWEST_VERSION + + +def should_upgrade(file_text: str) -> tuple[str, bool]: + if dont_upgrade_re.search(file_text): + return dont_upgrade_re.sub('', file_text), False + return file_text, True + + +def get_requirements(file_text: str) -> tuple[str, str | None]: + m = requires_re.search(file_text) + if m: + return requires_re.sub('', file_text), m.groups()[0] + return file_text, None + + +def exec_file(file: Path, file_text: str, error: Error) -> tuple[list[str], str | None]: + no_print_intercept_re = re.compile(r'^# no-print-intercept\n', flags=re.M) + print_intercept = not bool(no_print_intercept_re.search(file_text)) + if not print_intercept: + file_text = no_print_intercept_re.sub('', file_text) + + if file.stem in sys.modules: + del sys.modules[file.stem] + mp = MockPrint(file) + mod = None + + with patch.object(Path, 'read_text', MockPath.read_text), patch('builtins.print') as patch_print: + if print_intercept: + patch_print.side_effect = mp + try: + mod = importlib.import_module(file.stem) + except Exception: + tb = traceback.format_exception(*sys.exc_info()) + error(''.join(e for e in tb if '/pydantic/docs/examples/' in e or not e.startswith(' File '))) + + if mod and mod.__file__ != str(file): + error(f'module path "{mod.__file__}" is not same as "{file}", name may shadow another module?') + + lines = file_text.split('\n') + + to_json_line = '# output-json' + if to_json_line in lines: + lines = [line for line in lines if line != to_json_line] + if len(mp.statements) != 1: + error('should have exactly one print statement') + print_lines = build_print_lines(mp.statements[0][1]) + return lines, '\n'.join(print_lines) + '\n' else: - return file_text, True + for line_no, print_string in reversed(mp.statements): + build_print_statement(line_no, print_string, lines) + return lines, None + +def filter_lines(lines: list[str], error: Any) -> tuple[list[str], bool]: + ignored_above = False + try: + ignore_above = lines.index('# ignore-above') + except ValueError: + pass + else: + ignored_above = True + lines = lines[ignore_above + 1 :] -def exec_examples(): + try: + ignore_below = lines.index('# ignore-below') + except ValueError: + pass + else: + lines = lines[:ignore_below] + + lines = '\n'.join(lines).split('\n') + if any(len(line) > MAX_LINE_LENGTH for line in lines): + error(f'lines longer than {MAX_LINE_LENGTH} characters') + return lines, ignored_above + + +def upgrade_code(content: str, min_version: Version = HIGHEST_VERSION) -> str: + import pyupgrade._main # type: ignore + import autoflake # type: ignore + + upgraded = pyupgrade._main._fix_plugins( + content, + settings=pyupgrade._main.Settings( + min_version=min_version, + keep_percent_format=True, + keep_mock=False, + keep_runtime_typing=True, + ), + ) + upgraded = autoflake.fix_code(upgraded, remove_all_unused_imports=True) + return upgraded + + +def ensure_used(file: Path, all_md: str, error: Error) -> None: + """Ensures that example is used appropriately""" + file_tmpl = '{{!.tmp_examples/{}!}}' + md_name = file.stem + '.md' + if file_tmpl.format(md_name) not in all_md: + if file_tmpl.format(file.name) in all_md: + error( + f'incorrect usage, change filename to {md_name!r} in docs.' + "make sure you don't specify ```py code blocks around examples," + 'they are automatically generated now.' + ) + else: + error( + 'file not used anywhere. correct usage:', + file_tmpl.format(md_name), + ) + + +def check_style(file_text: str, error: Error) -> None: + if '\n\n\n\n' in file_text: + error('too many new lines') + if not file_text.endswith('\n'): + error('no trailing new line') + if re.search('^ *# *>', file_text, flags=re.M): + error('contains comments with print output, please remove') + + +def populate_upgraded_versions(file: Path, file_text: str, lowest_version: Version) -> list[tuple[Path, str, Version]]: + versions = [] + major, minor = lowest_version + assert major == HIGHEST_VERSION[0], 'Wow, Python 4 is out? Congrats!' + upgraded_file_text = file_text + while minor < HIGHEST_VERSION[1]: + minor += 1 + new_file_text = upgrade_code(file_text, min_version=(major, minor)) + if upgraded_file_text != new_file_text: + upgraded_file_text = new_file_text + new_file = UPGRADED_TMP_EXAMPLES_DIR / (file.stem + f'_{major}_{minor}' + file.suffix) + new_file.write_text(upgraded_file_text) + versions.append((new_file, upgraded_file_text, (major, minor))) + return versions + + +def exec_examples() -> int: # noqa: C901 (I really don't want to decompose it any further) errors = [] all_md = all_md_contents() new_files = {} - os.environ.update({ - 'my_auth_key': 'xxx', - 'my_api_key': 'xxx', - 'database_dsn': 'postgres://postgres@localhost:5432/env_db', - 'v0': '0', - 'sub_model': '{"v1": "json-1", "v2": "json-2"}', - 'sub_model__v2': 'nested-2', - 'sub_model__v3': '3', - 'sub_model__deep__v4': 'v4', - }) - + os.environ.update( + { + 'my_auth_key': 'xxx', + 'my_api_key': 'xxx', + 'database_dsn': 'postgres://postgres@localhost:5432/env_db', + 'v0': '0', + 'sub_model': '{"v1": "json-1", "v2": "json-2"}', + 'sub_model__v2': 'nested-2', + 'sub_model__v3': '3', + 'sub_model__deep__v4': 'v4', + } + ) sys.path.append(str(EXAMPLES_DIR)) + if sys.version_info < HIGHEST_VERSION: + print("WARNING: examples for 3.10+ requires python 3.10. They won't be executed") + else: + UPGRADED_TMP_EXAMPLES_DIR.mkdir(parents=True, exist_ok=True) + sys.path.append(str(UPGRADED_TMP_EXAMPLES_DIR)) + for file in sorted(EXAMPLES_DIR.iterdir()): + markdown_name = file.stem + '.md' - def error(desc: str): + def error(*desc: str) -> None: errors.append((file, desc)) - sys.stderr.write(f'error in {file.name}: {desc}\n') + previous_frame = sys._getframe(1) + filename = Path(previous_frame.f_globals['__file__']).relative_to(Path.cwd()) + location = f'{filename}:{previous_frame.f_lineno}' + sys.stderr.write(f'{location}: error in {file.relative_to(Path.cwd())}:\n{" ".join(desc)}\n') if not file.is_file(): # __pycache__, maybe others @@ -171,75 +337,60 @@ def error(desc: str): new_files[file.name] = file.read_text() continue - if f'{{!.tmp_examples/{file.name}!}}' not in all_md: - error('file not used anywhere') - file_text = file.read_text('utf-8') - if '\n\n\n\n' in file_text: - error('too many new lines') - if not file_text.endswith('\n'): - error('no trailing new line') - if re.search('^ *# *>', file_text, flags=re.M): - error('contains comments with print output, please remove') - - file_text, execute = should_execute(file.name, file_text) - if execute: - no_print_intercept_re = re.compile(r'^# no-print-intercept\n', flags=re.M) - print_intercept = not bool(no_print_intercept_re.search(file_text)) - if not print_intercept: - file_text = no_print_intercept_re.sub('', file_text) - - if file.stem in sys.modules: - del sys.modules[file.stem] - mp = MockPrint(file) - mod = None - with patch('pathlib.Path', MockPath): - with patch('builtins.print') as patch_print: - if print_intercept: - patch_print.side_effect = mp - try: - mod = importlib.import_module(file.stem) - except Exception: - tb = traceback.format_exception(*sys.exc_info()) - error(''.join(e for e in tb if '/pydantic/docs/examples/' in e or not e.startswith(' File '))) - - if mod and not mod.__file__.startswith(str(EXAMPLES_DIR)): - error(f'module path "{mod.__file__}" not inside "{EXAMPLES_DIR}", name may shadow another module?') - - lines = file_text.split('\n') - - to_json_line = '# output-json' - if to_json_line in lines: - lines = [line for line in lines if line != to_json_line] - if len(mp.statements) != 1: - error('should have exactly one print statement') - print_lines = build_print_lines(mp.statements[0][1]) - new_files[file.stem + '.json'] = '\n'.join(print_lines) + '\n' + ensure_used(file, all_md, error) + check_style(file_text, error) + + file_text, execute, lowest_version = should_execute(file.name, file_text) + file_text, upgrade = should_upgrade(file_text) + file_text, requirements = get_requirements(file_text) + + if upgrade and upgrade_code(file_text, min_version=lowest_version) != file_text: + error("pyupgrade would upgrade file. If it's not desired, add '# dont-upgrade' line at the top of the file") + + versions: list[tuple[Path, str, Version]] = [(file, file_text, lowest_version)] + if upgrade: + versions.extend(populate_upgraded_versions(file, file_text, lowest_version)) + + json_outputs: set[str | None] = set() + should_run_as_is = not requirements + final_content: list[str] = [] + for file, file_text, lowest_version in versions: + if execute and sys.version_info >= lowest_version: + lines, json_output = exec_file(file, file_text, error) + json_outputs.add(json_output) else: - for line_no, print_string in reversed(mp.statements): - build_print_statement(line_no, print_string, lines) + lines = file_text.split('\n') + + lines, ignored_lines_before_script = filter_lines(lines, error) + should_run_as_is = should_run_as_is and not ignored_lines_before_script + + final_content.append( + PYTHON_CODE_MD_TMPL.format( + version='.'.join(map(str, lowest_version)), + code=textwrap.indent('\n'.join(lines), ' '), + ) + ) + + if should_run_as_is: + final_content.append('_(This script is complete, it should run "as is")_') + elif requirements: + final_content.append(f'_(This script requires {requirements})_') else: - lines = file_text.split('\n') + error( + 'script may not run as is, but requirements were not specified.', + 'specify `# requires: ` in the end of the script', + ) - try: - ignore_above = lines.index('# ignore-above') - except ValueError: - pass - else: - lines = lines[ignore_above + 1 :] - - try: - ignore_below = lines.index('# ignore-below') - except ValueError: - pass - else: - lines = lines[:ignore_below] + if len(json_outputs) > 1: + error('json output should not differ between versions') - lines = '\n'.join(lines).split('\n') - if any(len(l) > MAX_LINE_LENGTH for l in lines): - error(f'lines longer than {MAX_LINE_LENGTH} characters') + if json_outputs: + json_output, *_ = json_outputs + if json_output: + final_content.append(JSON_OUTPUT_MD_TMPL.format(output=json_output)) - new_files[file.name] = '\n'.join(lines) + new_files[markdown_name] = '\n'.join(final_content) if errors: print(f'\n{len(errors)} errors, not writing files\n') @@ -253,6 +404,7 @@ def error(desc: str): for file_name, content in new_files.items(): (TMP_EXAMPLES_DIR / file_name).write_text(content, 'utf-8') gen_ansi_output() + return 0 diff --git a/docs/build/main.py b/docs/build/main.py index 1ef51b1dc4..f55237867d 100755 --- a/docs/build/main.py +++ b/docs/build/main.py @@ -8,7 +8,7 @@ PROJECT_ROOT = THIS_DIR / '..' / '..' -def main(): +def main() -> int: history = (PROJECT_ROOT / 'HISTORY.md').read_text() history = re.sub(r'#(\d+)', r'[#\1](https://github.com/pydantic/pydantic/issues/\1)', history) history = re.sub(r'(\s)@([\w\-]+)', r'\1[@\2](https://github.com/\2)', history, flags=re.I) diff --git a/docs/build/schema_mapping.py b/docs/build/schema_mapping.py index 96c7b74a01..60d385b060 100755 --- a/docs/build/schema_mapping.py +++ b/docs/build/schema_mapping.py @@ -6,107 +6,109 @@ Please edit this file directly not .tmp_schema_mappings.html """ +from __future__ import annotations import json import re from pathlib import Path +from typing import Any -table = [ - [ +table: list[tuple[str, str, str | dict[str, Any], str, str]] = [ + ( 'None', 'null', '', 'JSON Schema Core', - 'Same for `type(None)` or `Literal[None]`' - ], - [ + 'Same for `type(None)` or `Literal[None]`', + ), + ( 'bool', 'boolean', '', 'JSON Schema Core', - '' - ], - [ + '', + ), + ( 'str', 'string', '', 'JSON Schema Core', - '' - ], - [ + '', + ), + ( 'float', 'number', '', 'JSON Schema Core', - '' - ], - [ + '', + ), + ( 'int', 'integer', '', 'JSON Schema Validation', - '' - ], - [ + '', + ), + ( 'dict', 'object', '', 'JSON Schema Core', - '' - ], - [ + '', + ), + ( 'list', 'array', {'items': {}}, 'JSON Schema Core', - '' - ], - [ + '', + ), + ( 'tuple', 'array', {'items': {}}, 'JSON Schema Core', - '' - ], - [ + '', + ), + ( 'set', 'array', {'items': {}, 'uniqueItems': True}, 'JSON Schema Validation', - '' - ], - [ + '', + ), + ( 'frozenset', 'array', {'items': {}, 'uniqueItems': True}, 'JSON Schema Validation', - '' - ], - [ + '', + ), + ( 'List[str]', 'array', {'items': {'type': 'string'}}, 'JSON Schema Validation', - 'And equivalently for any other sub type, e.g. `List[int]`.' - ], - [ + 'And equivalently for any other sub type, e.g. `List[int]`.', + ), + ( 'Tuple[str, ...]', 'array', {'items': {'type': 'string'}}, 'JSON Schema Validation', - 'And equivalently for any other sub type, e.g. `Tuple[int, ...]`.' - ], - [ + 'And equivalently for any other sub type, e.g. `Tuple[int, ...]`.', + ), + ( 'Tuple[str, int]', 'array', {'items': [{'type': 'string'}, {'type': 'integer'}], 'minItems': 2, 'maxItems': 2}, 'JSON Schema Validation', ( 'And equivalently for any other set of subtypes. Note: If using schemas for OpenAPI, ' - 'you shouldn\'t use this declaration, as it would not be valid in OpenAPI (although it is ' + "you shouldn't use this declaration, as it would not be valid in OpenAPI (although it is " 'valid in JSON Schema).' - ) - ], - [ + ), + ), + ( 'Dict[str, int]', 'object', {'additionalProperties': {'type': 'integer'}}, @@ -115,247 +117,247 @@ 'And equivalently for any other subfields for dicts. Have in mind that although you can use other types as ' 'keys for dicts with Pydantic, only strings are valid keys for JSON, and so, only str is valid as ' 'JSON Schema key types.' - ) - ], - [ + ), + ), + ( 'Union[str, int]', 'anyOf', {'anyOf': [{'type': 'string'}, {'type': 'integer'}]}, 'JSON Schema Validation', - 'And equivalently for any other subfields for unions.' - ], - [ + 'And equivalently for any other subfields for unions.', + ), + ( 'Enum', 'enum', '{"enum": [...]}', 'JSON Schema Validation', - 'All the literal values in the enum are included in the definition.' - ], - [ + 'All the literal values in the enum are included in the definition.', + ), + ( 'SecretStr', 'string', {'writeOnly': True}, 'JSON Schema Validation', - '' - ], - [ + '', + ), + ( 'SecretBytes', 'string', {'writeOnly': True}, 'JSON Schema Validation', - '' - ], - [ + '', + ), + ( 'EmailStr', 'string', {'format': 'email'}, 'JSON Schema Validation', - '' - ], - [ + '', + ), + ( 'NameEmail', 'string', {'format': 'name-email'}, 'Pydantic standard "format" extension', - '' - ], - [ + '', + ), + ( 'AnyUrl', 'string', {'format': 'uri'}, 'JSON Schema Validation', - '' - ], - [ + '', + ), + ( 'Pattern', 'string', {'format': 'regex'}, 'JSON Schema Validation', - '' - ], - [ + '', + ), + ( 'bytes', 'string', {'format': 'binary'}, 'OpenAPI', - '' - ], - [ + '', + ), + ( 'Decimal', 'number', '', 'JSON Schema Core', - '' - ], - [ + '', + ), + ( 'UUID1', 'string', {'format': 'uuid1'}, 'Pydantic standard "format" extension', - '' - ], - [ + '', + ), + ( 'UUID3', 'string', {'format': 'uuid3'}, 'Pydantic standard "format" extension', - '' - ], - [ + '', + ), + ( 'UUID4', 'string', {'format': 'uuid4'}, 'Pydantic standard "format" extension', - '' - ], - [ + '', + ), + ( 'UUID5', 'string', {'format': 'uuid5'}, 'Pydantic standard "format" extension', - '' - ], - [ + '', + ), + ( 'UUID', 'string', {'format': 'uuid'}, 'Pydantic standard "format" extension', - 'Suggested in OpenAPI.' - ], - [ + 'Suggested in OpenAPI.', + ), + ( 'FilePath', 'string', {'format': 'file-path'}, 'Pydantic standard "format" extension', - '' - ], - [ + '', + ), + ( 'DirectoryPath', 'string', {'format': 'directory-path'}, 'Pydantic standard "format" extension', - '' - ], - [ + '', + ), + ( 'Path', 'string', {'format': 'path'}, 'Pydantic standard "format" extension', - '' - ], - [ + '', + ), + ( 'datetime', 'string', {'format': 'date-time'}, 'JSON Schema Validation', - '' - ], - [ + '', + ), + ( 'date', 'string', {'format': 'date'}, 'JSON Schema Validation', - '' - ], - [ + '', + ), + ( 'time', 'string', {'format': 'time'}, 'JSON Schema Validation', - '' - ], - [ + '', + ), + ( 'timedelta', 'number', {'format': 'time-delta'}, 'Difference in seconds (a `float`), with Pydantic standard "format" extension', - 'Suggested in JSON Schema repository\'s issues by maintainer.' - ], - [ + "Suggested in JSON Schema repository's issues by maintainer.", + ), + ( 'Json', 'string', {'format': 'json-string'}, 'Pydantic standard "format" extension', - '' - ], - [ + '', + ), + ( 'IPv4Address', 'string', {'format': 'ipv4'}, 'JSON Schema Validation', - '' - ], - [ + '', + ), + ( 'IPv6Address', 'string', {'format': 'ipv6'}, 'JSON Schema Validation', - '' - ], - [ + '', + ), + ( 'IPvAnyAddress', 'string', {'format': 'ipvanyaddress'}, 'Pydantic standard "format" extension', 'IPv4 or IPv6 address as used in `ipaddress` module', - ], - [ + ), + ( 'IPv4Interface', 'string', {'format': 'ipv4interface'}, 'Pydantic standard "format" extension', 'IPv4 interface as used in `ipaddress` module', - ], - [ + ), + ( 'IPv6Interface', 'string', {'format': 'ipv6interface'}, 'Pydantic standard "format" extension', 'IPv6 interface as used in `ipaddress` module', - ], - [ + ), + ( 'IPvAnyInterface', 'string', {'format': 'ipvanyinterface'}, 'Pydantic standard "format" extension', 'IPv4 or IPv6 interface as used in `ipaddress` module', - ], - [ + ), + ( 'IPv4Network', 'string', {'format': 'ipv4network'}, 'Pydantic standard "format" extension', 'IPv4 network as used in `ipaddress` module', - ], - [ + ), + ( 'IPv6Network', 'string', {'format': 'ipv6network'}, 'Pydantic standard "format" extension', 'IPv6 network as used in `ipaddress` module', - ], - [ + ), + ( 'IPvAnyNetwork', 'string', {'format': 'ipvanynetwork'}, 'Pydantic standard "format" extension', 'IPv4 or IPv6 network as used in `ipaddress` module', - ], - [ + ), + ( 'StrictBool', 'boolean', '', 'JSON Schema Core', - '' - ], - [ + '', + ), + ( 'StrictStr', 'string', '', 'JSON Schema Core', - '' - ], - [ + '', + ), + ( 'ConstrainedStr', 'string', '', @@ -363,16 +365,16 @@ ( 'If the type has values declared for the constraints, they are included as validations. ' 'See the mapping for `constr` below.' - ) - ], - [ - 'constr(regex=\'^text$\', min_length=2, max_length=10)', + ), + ), + ( + "constr(regex='^text$', min_length=2, max_length=10)", 'string', {'pattern': '^text$', 'minLength': 2, 'maxLength': 10}, 'JSON Schema Validation', - 'Any argument not passed to the function (not defined) will not be included in the schema.' - ], - [ + 'Any argument not passed to the function (not defined) will not be included in the schema.', + ), + ( 'ConstrainedInt', 'integer', '', @@ -380,44 +382,44 @@ ( 'If the type has values declared for the constraints, they are included as validations. ' 'See the mapping for `conint` below.' - ) - ], - [ + ), + ), + ( 'conint(gt=1, ge=2, lt=6, le=5, multiple_of=2)', 'integer', {'maximum': 5, 'exclusiveMaximum': 6, 'minimum': 2, 'exclusiveMinimum': 1, 'multipleOf': 2}, '', - 'Any argument not passed to the function (not defined) will not be included in the schema.' - ], - [ + 'Any argument not passed to the function (not defined) will not be included in the schema.', + ), + ( 'PositiveInt', 'integer', {'exclusiveMinimum': 0}, 'JSON Schema Validation', - '' - ], - [ + '', + ), + ( 'NegativeInt', 'integer', {'exclusiveMaximum': 0}, 'JSON Schema Validation', - '' - ], - [ + '', + ), + ( 'NonNegativeInt', 'integer', {'minimum': 0}, 'JSON Schema Validation', - '' - ], - [ + '', + ), + ( 'NonPositiveInt', 'integer', {'maximum': 0}, 'JSON Schema Validation', - '' - ], - [ + '', + ), + ( 'ConstrainedFloat', 'number', '', @@ -425,44 +427,44 @@ ( 'If the type has values declared for the constraints, they are included as validations. ' 'See the mapping for `confloat` below.' - ) - ], - [ + ), + ), + ( 'confloat(gt=1, ge=2, lt=6, le=5, multiple_of=2)', 'number', {'maximum': 5, 'exclusiveMaximum': 6, 'minimum': 2, 'exclusiveMinimum': 1, 'multipleOf': 2}, 'JSON Schema Validation', - 'Any argument not passed to the function (not defined) will not be included in the schema.' - ], - [ + 'Any argument not passed to the function (not defined) will not be included in the schema.', + ), + ( 'PositiveFloat', 'number', {'exclusiveMinimum': 0}, 'JSON Schema Validation', - '' - ], - [ + '', + ), + ( 'NegativeFloat', 'number', {'exclusiveMaximum': 0}, 'JSON Schema Validation', - '' - ], - [ + '', + ), + ( 'NonNegativeFloat', 'number', {'minimum': 0}, 'JSON Schema Validation', - '' - ], - [ + '', + ), + ( 'NonPositiveFloat', 'number', {'maximum': 0}, 'JSON Schema Validation', - '' - ], - [ + '', + ), + ( 'ConstrainedDecimal', 'number', '', @@ -470,29 +472,29 @@ ( 'If the type has values declared for the constraints, they are included as validations. ' 'See the mapping for `condecimal` below.' - ) - ], - [ + ), + ), + ( 'condecimal(gt=1, ge=2, lt=6, le=5, multiple_of=2)', 'number', {'maximum': 5, 'exclusiveMaximum': 6, 'minimum': 2, 'exclusiveMinimum': 1, 'multipleOf': 2}, 'JSON Schema Validation', - 'Any argument not passed to the function (not defined) will not be included in the schema.' - ], - [ + 'Any argument not passed to the function (not defined) will not be included in the schema.', + ), + ( 'BaseModel', 'object', '', 'JSON Schema Core', - 'All the properties defined will be defined with standard JSON Schema, including submodels.' - ], - [ + 'All the properties defined will be defined with standard JSON Schema, including submodels.', + ), + ( 'Color', 'string', {'format': 'color'}, 'Pydantic standard "format" extension', '', - ], + ), ] headings = [ @@ -503,11 +505,11 @@ ] -def md2html(s): +def md2html(s: str) -> str: return re.sub(r'`(.+?)`', r'\1', s) -def build_schema_mappings(): +def build_schema_mappings() -> None: rows = [] for py_type, json_type, additional, defined_in, notes in table: diff --git a/docs/datamodel_code_generator.md b/docs/datamodel_code_generator.md index dec9cbcb55..90daa8c3c9 100644 --- a/docs/datamodel_code_generator.md +++ b/docs/datamodel_code_generator.md @@ -70,9 +70,7 @@ person.json: ``` model.py: -```py -{!.tmp_examples/generate_models_person_model.py!} -``` +{!.tmp_examples/generate_models_person_model.md!} More information can be found on the [official documentation](https://koxudaxi.github.io/datamodel-code-generator/) diff --git a/docs/examples/generate_models_person_model.py b/docs/examples/generate_models_person_model.py index a846938a82..a1961ba243 100644 --- a/docs/examples/generate_models_person_model.py +++ b/docs/examples/generate_models_person_model.py @@ -1,3 +1,4 @@ +# dont-upgrade # generated by datamodel-codegen: # filename: person.json # timestamp: 2020-05-19T15:07:31+00:00 diff --git a/docs/examples/index_error.py b/docs/examples/index_error.py index 7f495308f6..16887d04bf 100644 --- a/docs/examples/index_error.py +++ b/docs/examples/index_error.py @@ -8,3 +8,4 @@ User(signup_ts='broken', friends=[1, 2, 'not number']) except ValidationError as e: print(e.json()) +# requires: User from previous example diff --git a/docs/examples/postponed_annotations_broken.py b/docs/examples/postponed_annotations_broken.py index 082182a30b..9dec4f5553 100644 --- a/docs/examples/postponed_annotations_broken.py +++ b/docs/examples/postponed_annotations_broken.py @@ -1,13 +1,23 @@ from __future__ import annotations from pydantic import BaseModel +from pydantic.errors import ConfigError def this_is_broken(): - # List is defined inside the function so is not in the module's - # global scope! - from typing import List + from pydantic import HttpUrl # HttpUrl is defined in functuon local scope class Model(BaseModel): - a: List[int] + a: HttpUrl - print(Model(a=(1, 2))) + try: + Model(a='https://example.com') + except ConfigError as e: + print(e) + + try: + Model.update_forward_refs() + except NameError as e: + print(e) + + +this_is_broken() diff --git a/docs/examples/postponed_annotations_main.py b/docs/examples/postponed_annotations_main.py index a755f07951..50d524c11a 100644 --- a/docs/examples/postponed_annotations_main.py +++ b/docs/examples/postponed_annotations_main.py @@ -1,10 +1,11 @@ from __future__ import annotations -from typing import List +from typing import Any, List from pydantic import BaseModel class Model(BaseModel): a: List[int] + b: Any -print(Model(a=('1', 2, 3))) +print(Model(a=('1', 2, 3), b='ok')) diff --git a/docs/examples/postponed_annotations_works.py b/docs/examples/postponed_annotations_works.py index eb289a5193..7a88631450 100644 --- a/docs/examples/postponed_annotations_works.py +++ b/docs/examples/postponed_annotations_works.py @@ -1,10 +1,13 @@ from __future__ import annotations -from typing import List # <-- List is defined in the module's global scope from pydantic import BaseModel +from pydantic import HttpUrl # HttpUrl is defined in the module's global scope def this_works(): class Model(BaseModel): - a: List[int] + a: HttpUrl - print(Model(a=(1, 2))) + print(Model(a='https://example.com')) + + +this_works() diff --git a/docs/examples/settings_disable_source.py b/docs/examples/settings_disable_source.py index 61b40feb65..fe5185d55e 100644 --- a/docs/examples/settings_disable_source.py +++ b/docs/examples/settings_disable_source.py @@ -20,3 +20,4 @@ def customise_sources( print(Settings(my_api_key='this is ignored')) +# requires: `MY_API_KEY` env variable to be set, e.g. `export MY_API_KEY=xxx` diff --git a/docs/examples/types_bare_type.py b/docs/examples/types_bare_type.py index b3dec3b31a..aea9fef533 100644 --- a/docs/examples/types_bare_type.py +++ b/docs/examples/types_bare_type.py @@ -1,3 +1,4 @@ +# dont-upgrade from typing import Type from pydantic import BaseModel, ValidationError diff --git a/docs/examples/types_infinite_generator_validate_first.py b/docs/examples/types_infinite_generator_validate_first.py index 1325d3629a..a0bd5633d0 100644 --- a/docs/examples/types_infinite_generator_validate_first.py +++ b/docs/examples/types_infinite_generator_validate_first.py @@ -37,8 +37,7 @@ def infinite_ints(): def infinite_strs(): while True: - for letter in 'allthesingleladies': - yield letter + yield from 'allthesingleladies' try: diff --git a/docs/examples/validation_decorator_async.py b/docs/examples/validation_decorator_async.py index 41bbe6cd33..009043b34d 100644 --- a/docs/examples/validation_decorator_async.py +++ b/docs/examples/validation_decorator_async.py @@ -29,3 +29,4 @@ async def main(): asyncio.run(main()) +# requires: `conn.execute()` that will return `'testing@example.com'` diff --git a/docs/hypothesis_plugin.md b/docs/hypothesis_plugin.md index 1593e36eb1..16d2a79b92 100644 --- a/docs/hypothesis_plugin.md +++ b/docs/hypothesis_plugin.md @@ -19,10 +19,7 @@ strategies support them without any user configuration. ### Example tests -```py -{!.tmp_examples/hypothesis_property_based_test.py!} -``` -_(This script is complete, it should run "as is")_ +{!.tmp_examples/hypothesis_property_based_test.md!} ### Use with JSON Schemas diff --git a/docs/index.md b/docs/index.md index 45e7f8180a..c5da5cbbb0 100644 --- a/docs/index.md +++ b/docs/index.md @@ -68,10 +68,7 @@ And many more who kindly sponsor Samuel Colvin on [GitHub Sponsors](https://gith ## Example -```py -{!.tmp_examples/index_main.py!} -``` -_(This script is complete, it should run "as is")_ +{!.tmp_examples/index_main.md!} What's going on here: @@ -85,13 +82,8 @@ What's going on here: If validation fails pydantic will raise an error with a breakdown of what was wrong: -```py -{!.tmp_examples/index_error.py!} -``` -outputs: -```json -{!.tmp_examples/index_error.json!} -``` +{!.tmp_examples/index_error.md!} + ## Rationale diff --git a/docs/mypy_plugin.md b/docs/mypy_plugin.md index 12e0a4f734..7172a509da 100644 --- a/docs/mypy_plugin.md +++ b/docs/mypy_plugin.md @@ -4,9 +4,8 @@ However, Pydantic also ships with a mypy plugin that adds a number of important features to mypy that improve its ability to type-check your code. For example, consider the following script: -```py -{!.tmp_examples/mypy_main.py!} -``` +{!.tmp_examples/mypy_main.md!} + Without any special configuration, mypy catches one of the errors (see [here](usage/mypy.md) for usage instructions): ``` @@ -64,6 +63,7 @@ There are other benefits too! See below for more details. ### Optional Capabilities: #### Prevent the use of required dynamic aliases + * If the [`warn_required_dynamic_aliases` **plugin setting**](#plugin-settings) is set to `True`, you'll get a mypy error any time you use a dynamically-determined alias or alias generator on a model with `Config.allow_population_by_field_name=False`. @@ -168,4 +168,4 @@ init_forbid_extra = true init_typed = true warn_required_dynamic_aliases = true warn_untyped_fields = true -``` +``` \ No newline at end of file diff --git a/docs/requirements.txt b/docs/requirements.txt index 6df8b4ee69..8a67597c32 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,3 +1,4 @@ +autoflake==1.4 ansi2html==1.8.0 flake8==5.0.4 flake8-quotes==3.3.1 @@ -7,6 +8,8 @@ mdx-truly-sane-lists==1.3 mkdocs==1.3.1 mkdocs-exclude==1.0.2 mkdocs-material==8.3.9 +pyupgrade==2.37.3 sqlalchemy orjson ujson + diff --git a/docs/usage/dataclasses.md b/docs/usage/dataclasses.md index af9bcfa9d2..09e5c5098d 100644 --- a/docs/usage/dataclasses.md +++ b/docs/usage/dataclasses.md @@ -1,10 +1,7 @@ If you don't want to use _pydantic_'s `BaseModel` you can instead get the same data validation on standard [dataclasses](https://docs.python.org/3/library/dataclasses.html) (introduced in Python 3.7). -```py -{!.tmp_examples/dataclasses_main.py!} -``` -_(This script is complete, it should run "as is")_ +{!.tmp_examples/dataclasses_main.md!} !!! note Keep in mind that `pydantic.dataclasses.dataclass` is a drop-in replacement for `dataclasses.dataclass` @@ -20,10 +17,7 @@ created by the standard library `dataclass` decorator. The underlying model and its schema can be accessed through `__pydantic_model__`. Also, fields that require a `default_factory` can be specified by either a `pydantic.Field` or a `dataclasses.field`. -```py -{!.tmp_examples/dataclasses_default_schema.py!} -``` -_(This script is complete, it should run "as is")_ +{!.tmp_examples/dataclasses_default_schema.md!} `pydantic.dataclasses.dataclass`'s arguments are the same as the standard decorator, except one extra keyword argument `config` which has the same meaning as [Config](model_config.md). @@ -38,9 +32,7 @@ For more information about combining validators with dataclasses, see If you want to modify the `Config` like you would with a `BaseModel`, you have three options: -```py -{!.tmp_examples/dataclasses_config.py!} -``` +{!.tmp_examples/dataclasses_config.md!} !!! warning After v1.10, _pydantic_ dataclasses support `Config.extra` but some default behaviour of stdlib dataclasses @@ -52,10 +44,7 @@ If you want to modify the `Config` like you would with a `BaseModel`, you have t Nested dataclasses are supported both in dataclasses and normal models. -```py -{!.tmp_examples/dataclasses_nested.py!} -``` -_(This script is complete, it should run "as is")_ +{!.tmp_examples/dataclasses_nested.md!} Dataclasses attributes can be populated by tuples, dictionaries or instances of the dataclass itself. @@ -69,30 +58,21 @@ _Pydantic_ will enhance the given stdlib dataclass but won't alter the default b It will instead create a wrapper around it to trigger validation that will act like a plain proxy. The stdlib dataclass can still be accessed via the `__dataclass__` attribute (see example below). -```py -{!.tmp_examples/dataclasses_stdlib_to_pydantic.py!} -``` -_(This script is complete, it should run "as is")_ +{!.tmp_examples/dataclasses_stdlib_to_pydantic.md!} ### Choose when to trigger validation As soon as your stdlib dataclass has been decorated with _pydantic_ dataclass decorator, magic methods have been added to validate input data. If you want, you can still keep using your dataclass and choose when to trigger it. -```py -{!.tmp_examples/dataclasses_stdlib_run_validation.py!} -``` -_(This script is complete, it should run "as is")_ +{!.tmp_examples/dataclasses_stdlib_run_validation.md!} ### Inherit from stdlib dataclasses Stdlib dataclasses (nested or not) can also be inherited and _pydantic_ will automatically validate all the inherited fields. -```py -{!.tmp_examples/dataclasses_stdlib_inheritance.py!} -``` -_(This script is complete, it should run "as is")_ +{!.tmp_examples/dataclasses_stdlib_inheritance.md!} ### Use of stdlib dataclasses with `BaseModel` @@ -100,10 +80,7 @@ Bear in mind that stdlib dataclasses (nested or not) are **automatically convert dataclasses when mixed with `BaseModel`! Furthermore the generated _pydantic_ dataclass will have the **exact same configuration** (`order`, `frozen`, ...) as the original one. -```py -{!.tmp_examples/dataclasses_stdlib_with_basemodel.py!} -``` -_(This script is complete, it should run "as is")_ +{!.tmp_examples/dataclasses_stdlib_with_basemodel.md!} ### Use custom types @@ -111,10 +88,7 @@ Since stdlib dataclasses are automatically converted to add validation using custom types may cause some unexpected behaviour. In this case you can simply add `arbitrary_types_allowed` in the config! -```py -{!.tmp_examples/dataclasses_arbitrary_types_allowed.py!} -``` -_(This script is complete, it should run "as is")_ +{!.tmp_examples/dataclasses_arbitrary_types_allowed.md!} ## Initialize hooks @@ -127,18 +101,12 @@ code *before* validation. be done before. In this case you can set `Config.post_init_call = 'after_validation'` -```py -{!.tmp_examples/dataclasses_post_init_post_parse.py!} -``` -_(This script is complete, it should run "as is")_ +{!.tmp_examples/dataclasses_post_init_post_parse.md!} Since version **v1.0**, any fields annotated with `dataclasses.InitVar` are passed to both `__post_init__` *and* `__post_init_post_parse__`. -```py -{!.tmp_examples/dataclasses_initvars.py!} -``` -_(This script is complete, it should run "as is")_ +{!.tmp_examples/dataclasses_initvars.md!} ### Difference with stdlib dataclasses @@ -150,6 +118,4 @@ When substituting usage of `dataclasses.dataclass` with `pydantic.dataclasses.da _Pydantic_ dataclasses do not feature a `.json()` function. To dump them as JSON, you will need to make use of the `pydantic_encoder` as follows: -```py -{!.tmp_examples/dataclasses_json_dumps.py!} -``` +{!.tmp_examples/dataclasses_json_dumps.md!} diff --git a/docs/usage/devtools.md b/docs/usage/devtools.md index 02583a35a7..8dd0c7ff5c 100644 --- a/docs/usage/devtools.md +++ b/docs/usage/devtools.md @@ -11,9 +11,7 @@ is on and what value was printed. In particular `debug()` is useful when inspecting models: -```py -{!.tmp_examples/devtools_main.py!} -``` +{!.tmp_examples/devtools_main.md!} Will output in your terminal: diff --git a/docs/usage/exporting_models.md b/docs/usage/exporting_models.md index 62aca5b025..6ab9013a1f 100644 --- a/docs/usage/exporting_models.md +++ b/docs/usage/exporting_models.md @@ -20,10 +20,7 @@ Arguments: Example: -```py -{!.tmp_examples/exporting_models_dict.py!} -``` -_(This script is complete, it should run "as is")_ +{!.tmp_examples/exporting_models_dict.md!} ## `dict(model)` and iteration @@ -33,10 +30,7 @@ returned, so sub-models will not be converted to dictionaries. Example: -```py -{!.tmp_examples/exporting_models_iterate.py!} -``` -_(This script is complete, it should run "as is")_ +{!.tmp_examples/exporting_models_iterate.md!} ## `model.copy(...)` @@ -51,10 +45,7 @@ Arguments: Example: -```py -{!.tmp_examples/exporting_models_copy.py!} -``` -_(This script is complete, it should run "as is")_ +{!.tmp_examples/exporting_models_copy.md!} ## `model.json(...)` @@ -81,20 +72,14 @@ Arguments: *pydantic* can serialise many commonly used types to JSON (e.g. `datetime`, `date` or `UUID`) which would normally fail with a simple `json.dumps(foobar)`. -```py -{!.tmp_examples/exporting_models_json.py!} -``` -_(This script is complete, it should run "as is")_ +{!.tmp_examples/exporting_models_json.md!} ### `json_encoders` Serialisation can be customised on a model using the `json_encoders` config property; the keys should be types (or names of types for forward references), and the values should be functions which serialise that type (see the example below): -```py -{!.tmp_examples/exporting_models_json_encoders.py!} -``` -_(This script is complete, it should run "as is")_ +{!.tmp_examples/exporting_models_json_encoders.md!} By default, `timedelta` is encoded as a simple float of total seconds. The `timedelta_isoformat` is provided as an optional alternative which implements [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) time diff encoding. @@ -102,10 +87,7 @@ as an optional alternative which implements [ISO 8601](https://en.wikipedia.org/ The `json_encoders` are also merged during the models inheritance with the child encoders taking precedence over the parent one. -```py -{!.tmp_examples/exporting_models_json_encoders_merge.py!} -``` -_(This script is complete, it should run "as is")_ +{!.tmp_examples/exporting_models_json_encoders_merge.md!} ### Serialising self-reference or other models @@ -113,10 +95,7 @@ By default, models are serialised as dictionaries. If you want to serialise them differently, you can add `models_as_dict=False` when calling `json()` method and add the classes of the model in `json_encoders`. In case of forward references, you can use a string with the class name instead of the class itself -```py -{!.tmp_examples/exporting_models_json_forward_ref.py!} -``` -_(This script is complete, it should run "as is")_ +{!.tmp_examples/exporting_models_json_forward_ref.md!} ### Serialising subclasses @@ -127,10 +106,7 @@ _(This script is complete, it should run "as is")_ Subclasses of common types are automatically encoded like their super-classes: -```py -{!.tmp_examples/exporting_models_json_subclass.py!} -``` -_(This script is complete, it should run "as is")_ +{!.tmp_examples/exporting_models_json_subclass.md!} ### Custom JSON (de)serialisation @@ -138,19 +114,13 @@ To improve the performance of encoding and decoding JSON, alternative JSON imple (e.g. [ujson](https://pypi.python.org/pypi/ujson)) can be used via the `json_loads` and `json_dumps` properties of `Config`. -```py -{!.tmp_examples/exporting_models_ujson.py!} -``` -_(This script is complete, it should run "as is")_ +{!.tmp_examples/exporting_models_ujson.md!} `ujson` generally cannot be used to dump JSON since it doesn't support encoding of objects like datetimes and does not accept a `default` fallback function argument. To do this, you may use another library like [orjson](https://github.com/ijl/orjson). -```py -{!.tmp_examples/exporting_models_orjson.py!} -``` -_(This script is complete, it should run "as is")_ +{!.tmp_examples/exporting_models_orjson.md!} Note that `orjson` takes care of `datetime` encoding natively, making it faster than `json.dumps` but meaning you cannot always customise the encoding using `Config.json_encoders`. @@ -159,19 +129,14 @@ meaning you cannot always customise the encoding using `Config.json_encoders`. Using the same plumbing as `copy()`, *pydantic* models support efficient pickling and unpickling. -```py -{!.tmp_examples/exporting_models_pickle.py!} -``` -_(This script is complete, it should run "as is")_ +{!.tmp_examples/exporting_models_pickle.md!} ## Advanced include and exclude The `dict`, `json`, and `copy` methods support `include` and `exclude` arguments which can either be sets or dictionaries. This allows nested selection of which fields to export: -```py -{!.tmp_examples/exporting_models_exclude1.py!} -``` +{!.tmp_examples/exporting_models_exclude1.md!} The `True` indicates that we want to exclude or include an entire key, just as if we included it in a set. Of course, the same can be done at any depth level. @@ -180,9 +145,7 @@ Special care must be taken when including or excluding fields from a list or tup `dict` and related methods expect integer keys for element-wise inclusion or exclusion. To exclude a field from **every** member of a list or tuple, the dictionary key `'__all__'` can be used as follows: -```py -{!.tmp_examples/exporting_models_exclude2.py!} -``` +{!.tmp_examples/exporting_models_exclude2.md!} The same holds for the `json` and `copy` methods. @@ -190,9 +153,7 @@ The same holds for the `json` and `copy` methods. In addition to the explicit arguments `exclude` and `include` passed to `dict`, `json` and `copy` methods, we can also pass the `include`/`exclude` arguments directly to the `Field` constructor or the equivalent `field` entry in the models `Config` class: -```py -{!.tmp_examples/exporting_models_exclude3.py!} -``` +{!.tmp_examples/exporting_models_exclude3.md!} In the case where multiple strategies are used, `exclude`/`include` fields are merged according to the following rules: @@ -203,12 +164,8 @@ Note that while merging settings, `exclude` entries are merged by computing the The resulting merged exclude settings: -```py -{!.tmp_examples/exporting_models_exclude4.py!} -``` +{!.tmp_examples/exporting_models_exclude4.md!} are the same as using merged include settings as follows: -```py -{!.tmp_examples/exporting_models_exclude5.py!} -``` +{!.tmp_examples/exporting_models_exclude5.md!} diff --git a/docs/usage/model_config.md b/docs/usage/model_config.md index d25204a3b2..749f894362 100644 --- a/docs/usage/model_config.md +++ b/docs/usage/model_config.md @@ -1,21 +1,12 @@ Behaviour of _pydantic_ can be controlled via the `Config` class on a model or a _pydantic_ dataclass. -```py -{!.tmp_examples/model_config_main.py!} -``` -_(This script is complete, it should run "as is")_ +{!.tmp_examples/model_config_main.md!} Also, you can specify config options as model class kwargs: -```py -{!.tmp_examples/model_config_class_kwargs.py!} -``` -_(This script is complete, it should run "as is")_ +{!.tmp_examples/model_config_class_kwargs.md!} Similarly, if using the `@dataclass` decorator: -```py -{!.tmp_examples/model_config_dataclass.py!} -``` -_(This script is complete, it should run "as is")_ +{!.tmp_examples/model_config_dataclass.md!} ## Options @@ -134,20 +125,14 @@ not be included in the model schemas. **Note**: this means that attributes on th If you wish to change the behaviour of _pydantic_ globally, you can create your own custom `BaseModel` with custom `Config` since the config is inherited -```py -{!.tmp_examples/model_config_change_globally_custom.py!} -``` -_(This script is complete, it should run "as is")_ +{!.tmp_examples/model_config_change_globally_custom.md!} ## Alias Generator If data source field names do not match your code style (e. g. CamelCase fields), you can automatically generate aliases using `alias_generator`: -```py -{!.tmp_examples/model_config_alias_generator.py!} -``` -_(This script is complete, it should run "as is")_ +{!.tmp_examples/model_config_alias_generator.md!} Here camel case refers to ["upper camel case"](https://en.wikipedia.org/wiki/Camel_case) aka pascal case e.g. `CamelCase`. If you'd like instead to use lower camel case e.g. `camelCase`, @@ -175,34 +160,22 @@ the selected value is determined as follows (in descending order of priority): For example: -```py -{!.tmp_examples/model_config_alias_precedence.py!} -``` -_(This script is complete, it should run "as is")_ +{!.tmp_examples/model_config_alias_precedence.md!} ## Smart Union By default, as explained [here](types.md#unions), _pydantic_ tries to validate (and coerce if it can) in the order of the `Union`. So sometimes you may have unexpected coerced data. -```py -{!.tmp_examples/model_config_smart_union_off.py!} -``` -_(This script is complete, it should run "as is")_ +{!.tmp_examples/model_config_smart_union_off.md!} To prevent this, you can enable `Config.smart_union`. _Pydantic_ will then check all allowed types before even trying to coerce. Know that this is of course slower, especially if your `Union` is quite big. -```py -{!.tmp_examples/model_config_smart_union_on.py!} -``` -_(This script is complete, it should run "as is")_ +{!.tmp_examples/model_config_smart_union_on.md!} !!! warning Note that this option **does not support compound types yet** (e.g. differentiate `List[int]` and `List[str]`). This option will be improved further once a strict mode is added in _pydantic_ and will probably be the default behaviour in v2! -```py -{!.tmp_examples/model_config_smart_union_on_edge_case.py!} -``` -_(This script is complete, it should run "as is")_ +{!.tmp_examples/model_config_smart_union_on_edge_case.md!} diff --git a/docs/usage/models.md b/docs/usage/models.md index 7cf7c9dbf9..c6665d7da5 100644 --- a/docs/usage/models.md +++ b/docs/usage/models.md @@ -115,10 +115,7 @@ Models possess the following methods and attributes: More complex hierarchical data structures can be defined using models themselves as types in annotations. -```py -{!.tmp_examples/models_recursive.py!} -``` -_(This script is complete, it should run "as is")_ +{!.tmp_examples/models_recursive.md!} For self-referencing models, see [postponed annotations](postponed_annotations.md#self-referencing-models). @@ -133,20 +130,14 @@ To do this: The example here uses SQLAlchemy, but the same approach should work for any ORM. -```py -{!.tmp_examples/models_orm_mode.py!} -``` -_(This script is complete, it should run "as is")_ +{!.tmp_examples/models_orm_mode.md!} ### Reserved names You may want to name a Column after a reserved SQLAlchemy field. In that case, Field aliases will be convenient: -```py -{!.tmp_examples/models_orm_mode_reserved_name.py!} -``` -_(This script is complete, it should run "as is")_ +{!.tmp_examples/models_orm_mode_reserved_name.md!} !!! note The example above works because aliases have priority over field names for @@ -158,10 +149,7 @@ ORM instances will be parsed with `from_orm` recursively as well as at the top l Here a vanilla class is used to demonstrate the principle, but any ORM class could be used instead. -```py -{!.tmp_examples/models_orm_mode_recursive.py!} -``` -_(This script is complete, it should run "as is")_ +{!.tmp_examples/models_orm_mode_recursive.md!} ### Data binding @@ -178,10 +166,7 @@ The `GetterDict` instance will be called for each field with a sentinel as a fal value is set). Returning this sentinel means that the field is missing. Any other value will be interpreted as the value of the field. -```py -{!.tmp_examples/models_orm_mode_data_binding.py!} -``` -_(This script is complete, it should run "as is")_ +{!.tmp_examples/models_orm_mode_data_binding.md!} ## Error Handling @@ -225,11 +210,7 @@ Each error object contains: As a demonstration: -```py -{!.tmp_examples/models_errors1.py!} -``` -_(This script is complete, it should run "as is". `json()` has `indent=2` set by default, but I've tweaked the -JSON here and below to make it slightly more concise.)_ +{!.tmp_examples/models_errors1.md!} ### Custom Errors @@ -237,17 +218,11 @@ In your custom data types or validators you should use `ValueError`, `TypeError` See [validators](validators.md) for more details on use of the `@validator` decorator. -```py -{!.tmp_examples/models_errors2.py!} -``` -_(This script is complete, it should run "as is")_ +{!.tmp_examples/models_errors2.md!} You can also define your own error classes, which can specify a custom error code, message template, and context: -```py -{!.tmp_examples/models_errors3.py!} -``` -_(This script is complete, it should run "as is")_ +{!.tmp_examples/models_errors3.md!} ## Helper Functions @@ -260,10 +235,7 @@ _(This script is complete, it should run "as is")_ * **`parse_file`**: this takes in a file path, reads the file and passes the contents to `parse_raw`. If `content_type` is omitted, it is inferred from the file's extension. -```py -{!.tmp_examples/models_parse.py!} -``` -_(This script is complete, it should run "as is")_ +{!.tmp_examples/models_parse.md!} !!! warning To quote the [official `pickle` docs](https://docs.python.org/3/library/pickle.html), @@ -284,10 +256,7 @@ as efficiently as possible (`construct()` is generally around 30x faster than cr `construct()` does not do any validation, meaning it can create models which are invalid. **You should only ever use the `construct()` method with data which has already been validated, or you trust.** -```py -{!.tmp_examples/models_construct.py!} -``` -_(This script is complete, it should run "as is")_ +{!.tmp_examples/models_construct.md!} The `_fields_set` keyword argument to `construct()` is optional, but allows you to be more precise about which fields were originally set and which weren't. If it's omitted `__fields_set__` will just be the keys @@ -310,10 +279,7 @@ In order to declare a generic model, you perform the following steps: Here is an example using `GenericModel` to create an easily-reused HTTP response payload wrapper: -```py -{!.tmp_examples/models_generics.py!} -``` -_(This script is complete, it should run "as is")_ +{!.tmp_examples/models_generics.md!} If you set `Config` or make use of `validator` in your generic model definition, it is applied to concrete subclasses in the same way as when inheriting from `BaseModel`. Any methods defined on @@ -329,32 +295,20 @@ you would expect mypy to provide if you were to declare the type without using ` To inherit from a GenericModel without replacing the `TypeVar` instance, a class must also inherit from `typing.Generic`: -```py -{!.tmp_examples/models_generics_inheritance.py!} -``` -_(This script is complete, it should run "as is")_ +{!.tmp_examples/models_generics_inheritance.md!} You can also create a generic subclass of a `GenericModel` that partially or fully replaces the type parameters in the superclass. -```py -{!.tmp_examples/models_generics_inheritance_extend.py!} -``` -_(This script is complete, it should run "as is")_ +{!.tmp_examples/models_generics_inheritance_extend.md!} If the name of the concrete subclasses is important, you can also override the default behavior: -```py -{!.tmp_examples/models_generics_naming.py!} -``` -_(This script is complete, it should run "as is")_ +{!.tmp_examples/models_generics_naming.md!} Using the same TypeVar in nested models allows you to enforce typing relationships at different points in your model: -```py -{!.tmp_examples/models_generics_nested.py!} -``` -_(This script is complete, it should run "as is")_ +{!.tmp_examples/models_generics_nested.md!} Pydantic also treats `GenericModel` similarly to how it treats built-in generic types like `List` and `Dict` when it comes to leaving them unparameterized, or using bounded `TypeVar` instances: @@ -364,19 +318,14 @@ comes to leaving them unparameterized, or using bounded `TypeVar` instances: Also, like `List` and `Dict`, any parameters specified using a `TypeVar` can later be substituted with concrete types. -```py -{!.tmp_examples/models_generics_typevars.py!} -``` -_(This script is complete, it should run "as is")_ +{!.tmp_examples/models_generics_typevars.md!} ## Dynamic model creation There are some occasions where the shape of a model is not known until runtime. For this *pydantic* provides the `create_model` method to allow models to be created on the fly. -```py -{!.tmp_examples/models_dynamic_creation.py!} -``` +{!.tmp_examples/models_dynamic_creation.md!} Here `StaticFoobarModel` and `DynamicFoobarModel` are identical. @@ -389,15 +338,11 @@ Fields are defined by either a tuple of the form `(, )` or special key word arguments `__config__` and `__base__` can be used to customise the new model. This includes extending a base model with extra fields. -```py -{!.tmp_examples/models_dynamic_inheritance.py!} -``` +{!.tmp_examples/models_dynamic_inheritance.md!} You can also add validators by passing a dict to the `__validators__` argument. -```py -{!.tmp_examples/models_dynamic_validators.py!} -``` +{!.tmp_examples/models_dynamic_validators.md!} ## Model creation from `NamedTuple` or `TypedDict` @@ -407,9 +352,7 @@ For this _pydantic_ provides `create_model_from_namedtuple` and `create_model_fr Those methods have the exact same keyword arguments as `create_model`. -```py -{!.tmp_examples/models_from_typeddict.py!} -``` +{!.tmp_examples/models_from_typeddict.md!} ## Custom Root Types @@ -419,9 +362,7 @@ The root type can be any type supported by pydantic, and is specified by the typ The root value can be passed to the model `__init__` via the `__root__` keyword argument, or as the first and only argument to `parse_obj`. -```py -{!.tmp_examples/models_custom_root_field.py!} -``` +{!.tmp_examples/models_custom_root_field.md!} If you call the `parse_obj` method for a model with a custom root type with a *dict* as the first argument, the following logic is used: @@ -434,9 +375,7 @@ the following logic is used: This is demonstrated in the following example: -```py -{!.tmp_examples/models_custom_root_field_parse_obj.py!} -``` +{!.tmp_examples/models_custom_root_field_parse_obj.md!} !!! warning Calling the `parse_obj` method on a dict with the single key `"__root__"` for non-mapping custom root types @@ -444,9 +383,7 @@ This is demonstrated in the following example: If you want to access items in the `__root__` field directly or to iterate over the items, you can implement custom `__iter__` and `__getitem__` functions, as shown in the following example. -```py -{!.tmp_examples/models_custom_root_access.py!} -``` +{!.tmp_examples/models_custom_root_access.md!} ## Faux Immutability @@ -457,9 +394,7 @@ values of instance attributes will raise errors. See [model config](model_config Immutability in Python is never strict. If developers are determined/stupid they can always modify a so-called "immutable" object. -```py -{!.tmp_examples/models_mutation.py!} -``` +{!.tmp_examples/models_mutation.md!} Trying to change `a` caused an error, and `a` remains unchanged. However, the dict `b` is mutable, and the immutability of `foobar` doesn't stop `b` from being changed. @@ -469,10 +404,7 @@ immutability of `foobar` doesn't stop `b` from being changed. Pydantic models can be used alongside Python's [Abstract Base Classes](https://docs.python.org/3/library/abc.html) (ABCs). -```py -{!.tmp_examples/models_abc.py!} -``` -_(This script is complete, it should run "as is")_ +{!.tmp_examples/models_abc.md!} ## Field Ordering @@ -487,10 +419,7 @@ Field order is important in models for the following reasons: As of **v1.0** all fields with annotations (whether annotation-only or with a default value) will precede all fields without an annotation. Within their respective groups, fields remain in the order they were defined. -```py -{!.tmp_examples/models_field_order.py!} -``` -_(This script is complete, it should run "as is")_ +{!.tmp_examples/models_field_order.md!} !!! warning As demonstrated by the example above, combining the use of annotated and non-annotated fields @@ -504,10 +433,7 @@ _(This script is complete, it should run "as is")_ To declare a field as required, you may declare it using just an annotation, or you may use an ellipsis (`...`) as the value: -```py -{!.tmp_examples/models_required_fields.py!} -``` -_(This script is complete, it should run "as is")_ +{!.tmp_examples/models_required_fields.md!} Where `Field` refers to the [field function](schema.md#field-customisation). @@ -525,10 +451,7 @@ with [mypy](mypy.md), and as of **v1.0** should be avoided in most cases. If you want to specify a field that can take a `None` value while still being required, you can use `Optional` with `...`: -```py -{!.tmp_examples/models_required_field_optional.py!} -``` -_(This script is complete, it should run "as is")_ +{!.tmp_examples/models_required_field_optional.md!} In this model, `a`, `b`, and `c` can take `None` as a value. But `a` is optional, while `b` and `c` are required. `b` and `c` require a value, even if the value is `None`. @@ -546,10 +469,7 @@ To do this, you may want to use a `default_factory`. Example of usage: -```py -{!.tmp_examples/models_default_factory.py!} -``` -_(This script is complete, it should run "as is")_ +{!.tmp_examples/models_default_factory.md!} Where `Field` refers to the [field function](schema.md#field-customisation). @@ -566,19 +486,13 @@ automatically excluded from the model. If you need to vary or manipulate internal attributes on instances of the model, you can declare them using `PrivateAttr`: -```py -{!.tmp_examples/private_attributes.py!} -``` -_(This script is complete, it should run "as is")_ +{!.tmp_examples/private_attributes.md!} Private attribute names must start with underscore to prevent conflicts with model fields: both `_attr` and `__attr__` are supported. If `Config.underscore_attrs_are_private` is `True`, any non-ClassVar underscore attribute will be treated as private: -```py -{!.tmp_examples/private_attributes_underscore_attrs_are_private.py!} -``` -_(This script is complete, it should run "as is")_ +{!.tmp_examples/private_attributes_underscore_attrs_are_private.md!} Upon class creation pydantic constructs `__slots__` filled with private attributes. @@ -591,10 +505,7 @@ logic used to populate pydantic models in a more ad-hoc way. This function behav This is especially useful when you want to parse results into a type that is not a direct subclass of `BaseModel`. For example: -```py -{!.tmp_examples/parse_obj_as.py!} -``` -_(This script is complete, it should run "as is")_ +{!.tmp_examples/parse_obj_as.md!} This function is capable of parsing data into any of the types pydantic can handle as fields of a `BaseModel`. @@ -607,10 +518,7 @@ which are analogous to `BaseModel.parse_file` and `BaseModel.parse_raw`. and in some cases this may result in a loss of information. For example: -```py -{!.tmp_examples/models_data_conversion.py!} -``` -_(This script is complete, it should run "as is")_ +{!.tmp_examples/models_data_conversion.md!} This is a deliberate decision of *pydantic*, and in general it's the most useful approach. See [here](https://github.com/pydantic/pydantic/issues/578) for a longer discussion on the subject. @@ -621,17 +529,13 @@ Nevertheless, [strict type checking](types.md#strict-types) is partially support All *pydantic* models will have their signature generated based on their fields: -```py -{!.tmp_examples/models_signature.py!} -``` +{!.tmp_examples/models_signature.md!} An accurate signature is useful for introspection purposes and libraries like `FastAPI` or `hypothesis`. The generated signature will also respect custom `__init__` functions: -```py -{!.tmp_examples/models_signature_custom_init.py!} -``` +{!.tmp_examples/models_signature_custom_init.md!} To be included in the signature, a field's alias or name must be a valid Python identifier. *pydantic* prefers aliases over names, but may use field names if the alias is not a valid Python identifier. diff --git a/docs/usage/mypy.md b/docs/usage/mypy.md index 3c62eb58c9..4d047c57bb 100644 --- a/docs/usage/mypy.md +++ b/docs/usage/mypy.md @@ -1,9 +1,7 @@ *pydantic* models work with [mypy](http://mypy-lang.org/) provided you use the annotation-only version of required fields: -```py -{!.tmp_examples/mypy_main.py!} -``` +{!.tmp_examples/mypy_main.md!} You can run your code through mypy with: diff --git a/docs/usage/postponed_annotations.md b/docs/usage/postponed_annotations.md index 72b25dcddf..8fbf805f0c 100644 --- a/docs/usage/postponed_annotations.md +++ b/docs/usage/postponed_annotations.md @@ -4,10 +4,7 @@ Postponed annotations (as described in [PEP563](https://www.python.org/dev/peps/pep-0563/)) "just work". -```py -{!.tmp_examples/postponed_annotations_main.py!} -``` -_(This script is complete, it should run "as is")_ +{!.tmp_examples/postponed_annotations_main.md!} Internally, *pydantic* will call a method similar to `typing.get_type_hints` to resolve annotations. @@ -19,10 +16,7 @@ In some cases, a `ForwardRef` won't be able to be resolved during model creation For example, this happens whenever a model references itself as a field type. When this happens, you'll need to call `update_forward_refs` after the model has been created before it can be used: -```py -{!.tmp_examples/postponed_annotations_forward_ref.py!} -``` -_(This script is complete, it should run "as is")_ +{!.tmp_examples/postponed_annotations_forward_ref.md!} !!! warning To resolve strings (type names) into annotations (types), *pydantic* needs a namespace dict in which to @@ -31,15 +25,11 @@ _(This script is complete, it should run "as is")_ For example, this works fine: -```py -{!.tmp_examples/postponed_annotations_works.py!} -``` +{!.tmp_examples/postponed_annotations_works.md!} While this will break: -```py -{!.tmp_examples/postponed_annotations_broken.py!} -``` +{!.tmp_examples/postponed_annotations_broken.md!} Resolving this is beyond the call for *pydantic*: either remove the future import or declare the types globally. @@ -50,16 +40,10 @@ resolved after model creation. Within the model, you can refer to the not-yet-constructed model using a string: -```py -{!.tmp_examples/postponed_annotations_self_referencing_string.py!} -``` -_(This script is complete, it should run "as is")_ +{!.tmp_examples/postponed_annotations_self_referencing_string.md!} Since Python 3.7, you can also refer it by its type, provided you import `annotations` (see [above](postponed_annotations.md) for support depending on Python and *pydantic* versions). -```py -{!.tmp_examples/postponed_annotations_self_referencing_annotations.py!} -``` -_(This script is complete, it should run "as is")_ +{!.tmp_examples/postponed_annotations_self_referencing_annotations.md!} diff --git a/docs/usage/schema.md b/docs/usage/schema.md index da652f2c48..57b22fa1ef 100644 --- a/docs/usage/schema.md +++ b/docs/usage/schema.md @@ -1,15 +1,7 @@ *Pydantic* allows auto creation of JSON Schemas from models: -```py -{!.tmp_examples/schema_main.py!} -``` -_(This script is complete, it should run "as is")_ - -Outputs: +{!.tmp_examples/schema_main.md!} -```json -{!.tmp_examples/schema_main.json!} -``` The generated schemas are compliant with the specifications: [JSON Schema Core](https://json-schema.org/latest/json-schema-core.html), @@ -44,10 +36,7 @@ apply the schema generation logic used for _pydantic_ models in a more ad-hoc wa These functions behave similarly to `BaseModel.schema` and `BaseModel.schema_json`, but work with arbitrary pydantic-compatible types. -```py -{!.tmp_examples/schema_ad_hoc.py!} -``` -_(This script is complete, it should run "as is")_ +{!.tmp_examples/schema_ad_hoc.md!} ## Field customization @@ -125,19 +114,13 @@ If *pydantic* finds constraints which are not being enforced, an error will be r constraint to appear in the schema, even though it's not being checked upon parsing, you can use variadic arguments to `Field()` with the raw schema attribute name: -```py -{!.tmp_examples/schema_unenforced_constraints.py!} -``` -_(This script is complete, it should run "as is")_ +{!.tmp_examples/schema_unenforced_constraints.md!} ### typing.Annotated Fields Rather than assigning a `Field` value, it can be specified in the type hint with `typing.Annotated`: -```py -{!.tmp_examples/schema_annotated.py!} -``` -_(This script is complete, it should run "as is")_ +{!.tmp_examples/schema_annotated.md!} `Field` can only be supplied once per field - an error will be raised if used in `Annotated` and as the assigned value. Defaults can be set outside `Annotated` as the assigned value or with `Field.default_factory` inside `Annotated` - the @@ -154,16 +137,8 @@ see [Custom Data Types](types.md#custom-data-types) for more details. *pydantic* will inspect the signature of `__modify_schema__` to determine whether the `field` argument should be included. -```py -{!.tmp_examples/schema_with_field.py!} -``` -_(This script is complete, it should run "as is")_ +{!.tmp_examples/schema_with_field.md!} -Outputs: - -```json -{!.tmp_examples/schema_with_field.json!} -``` ## JSON Schema Types @@ -184,16 +159,8 @@ The field schema mapping from Python / *pydantic* to JSON Schema is done as foll You can also generate a top-level JSON Schema that only includes a list of models and related sub-models in its `definitions`: -```py -{!.tmp_examples/schema_top_level.py!} -``` -_(This script is complete, it should run "as is")_ +{!.tmp_examples/schema_top_level.md!} -Outputs: - -```json -{!.tmp_examples/schema_top_level.json!} -``` ## Schema customization @@ -202,16 +169,8 @@ You can customize the generated `$ref` JSON location: the definitions are always This is useful if you need to extend or modify the JSON Schema default definitions location. E.g. with OpenAPI: -```py -{!.tmp_examples/schema_custom.py!} -``` -_(This script is complete, it should run "as is")_ +{!.tmp_examples/schema_custom.md!} -Outputs: - -```json -{!.tmp_examples/schema_custom.json!} -``` It's also possible to extend/override the generated JSON schema in a model. @@ -219,16 +178,8 @@ To do it, use the `Config` sub-class attribute `schema_extra`. For example, you could add `examples` to the JSON Schema: -```py -{!.tmp_examples/schema_with_example.py!} -``` -_(This script is complete, it should run "as is")_ - -Outputs: +{!.tmp_examples/schema_with_example.md!} -```json -{!.tmp_examples/schema_with_example.json!} -``` For more fine-grained control, you can alternatively set `schema_extra` to a callable and post-process the generated schema. The callable can have one or two positional arguments. @@ -238,14 +189,8 @@ The callable is expected to mutate the schema dictionary *in-place*; the return For example, the `title` key can be removed from the model's `properties`: -```py -{!.tmp_examples/schema_extra_callable.py!} -``` +{!.tmp_examples/schema_extra_callable.md!} -_(This script is complete, it should run "as is")_ -Outputs: -```json -{!.tmp_examples/schema_extra_callable.json!} ``` diff --git a/docs/usage/settings.md b/docs/usage/settings.md index 2a9c4c39c6..1ba926f6eb 100644 --- a/docs/usage/settings.md +++ b/docs/usage/settings.md @@ -12,10 +12,7 @@ This makes it easy to: For example: -```py -{!.tmp_examples/settings_main.py!} -``` -_(This script is complete, it should run "as is")_ +{!.tmp_examples/settings_main.md!} ## Environment variable names @@ -46,9 +43,7 @@ The following rules are used to determine which environment variable(s) are read Case-sensitivity can be turned on through the `Config`: -```py -{!.tmp_examples/settings_case_sensitive.py!} -``` +{!.tmp_examples/settings_case_sensitive.md!} When `case_sensitive` is `True`, the environment variable names must match field names (optionally with a prefix), so in this example @@ -90,9 +85,7 @@ export SUB_MODEL__DEEP__V4=v4 ``` You could load a settings module thus: -```py -{!.tmp_examples/settings_nested_env.py!} -``` +{!.tmp_examples/settings_nested_env.md!} `env_nested_delimiter` can be configured via the `Config` class as shown above, or via the `_env_nested_delimiter` keyword argument on instantiation. @@ -276,10 +269,7 @@ Each callable should take an instance of the settings class as its sole argument The order of the returned callables decides the priority of inputs; first item is the highest priority. -```py -{!.tmp_examples/settings_env_priority.py!} -``` -_(This script is complete, it should run "as is")_ +{!.tmp_examples/settings_env_priority.md!} By flipping `env_settings` and `init_settings`, environment variables now have precedence over `__init__` kwargs. @@ -288,16 +278,10 @@ By flipping `env_settings` and `init_settings`, environment variables now have p As explained earlier, *pydantic* ships with multiples built-in settings sources. However, you may occasionally need to add your own custom sources, `customise_sources` makes this very easy: -```py -{!.tmp_examples/settings_add_custom_source.py!} -``` -_(This script is complete, it should run "as is")_ +{!.tmp_examples/settings_add_custom_source.md!} ### Removing sources You might also want to disable a source: -```py -{!.tmp_examples/settings_disable_source.py!} -``` -_(This script is complete, it should run "as is", here you might need to set the `my_api_key` environment variable)_ +{!.tmp_examples/settings_disable_source.md!} diff --git a/docs/usage/types.md b/docs/usage/types.md index 2bcad159d7..0fe38d1901 100644 --- a/docs/usage/types.md +++ b/docs/usage/types.md @@ -187,10 +187,7 @@ with custom properties and validation. *pydantic* uses standard library `typing` types as defined in PEP 484 to define complex objects. -```py -{!.tmp_examples/types_iterables.py!} -``` -_(This script is complete, it should run "as is")_ +{!.tmp_examples/types_iterables.md!} ### Infinite Generators @@ -201,10 +198,7 @@ validated with the sub-type of `Sequence` (e.g. `int` in `Sequence[int]`). But if you have a generator that you don't want to be consumed, e.g. an infinite generator or a remote data loader, you can define its type with `Iterable`: -```py -{!.tmp_examples/types_infinite_generator.py!} -``` -_(This script is complete, it should run "as is")_ +{!.tmp_examples/types_infinite_generator.md!} !!! warning `Iterable` fields only perform a simple check that the argument is iterable and @@ -225,10 +219,7 @@ _(This script is complete, it should run "as is")_ You can create a [validator](validators.md) to validate the first value in an infinite generator and still not consume it entirely. -```py -{!.tmp_examples/types_infinite_generator_validate_first.py!} -``` -_(This script is complete, it should run "as is")_ +{!.tmp_examples/types_infinite_generator_validate_first.md!} ### Unions @@ -238,10 +229,7 @@ The `Union` type allows a model attribute to accept different types, e.g.: You may get unexpected coercion with `Union`; see below.
Know that you can also make the check slower but stricter by using [Smart Union](model_config.md#smart-union) -```py -{!.tmp_examples/types_union_incorrect.py!} -``` -_(This script is complete, it should run "as is")_ +{!.tmp_examples/types_union_incorrect.md!} However, as can be seen above, *pydantic* will attempt to 'match' any of the types defined under `Union` and will use the first one that matches. In the above example the `id` of `user_03` was defined as a `uuid.UUID` class (which @@ -262,10 +250,7 @@ followed by less specific types. In the above example, the `UUID` class should precede the `int` and `str` classes to preclude the unexpected representation as such: -```py -{!.tmp_examples/types_union_correct.py!} -``` -_(This script is complete, it should run "as is")_ +{!.tmp_examples/types_union_correct.md!} !!! tip The type `Optional[x]` is a shorthand for `Union[x, None]`. @@ -288,10 +273,7 @@ Setting a discriminated union has many benefits: - only one explicit error is raised in case of failure - the generated JSON schema implements the [associated OpenAPI specification](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#discriminatorObject) -```py -{!.tmp_examples/types_union_discriminated.py!} -``` -_(This script is complete, it should run "as is")_ +{!.tmp_examples/types_union_discriminated.md!} !!! note Using the [Annotated Fields syntax](../schema/#typingannotated-fields) can be handy to regroup @@ -308,19 +290,13 @@ _(This script is complete, it should run "as is")_ Only one discriminator can be set for a field but sometimes you want to combine multiple discriminators. In this case you can always create "intermediate" models with `__root__` and add your discriminator. -```py -{!.tmp_examples/types_union_discriminated_nested.py!} -``` -_(This script is complete, it should run "as is")_ +{!.tmp_examples/types_union_discriminated_nested.md!} ### Enums and Choices *pydantic* uses Python's standard `enum` classes to define choices. -```py -{!.tmp_examples/types_choices.py!} -``` -_(This script is complete, it should run "as is")_ +{!.tmp_examples/types_choices.md!} ### Datetime Types @@ -362,9 +338,7 @@ types: * `[-][DD ][HH:MM]SS[.ffffff]` * `[±]P[DD]DT[HH]H[MM]M[SS]S` ([ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) format for timedelta) -```py -{!.tmp_examples/types_dt.py!} -``` +{!.tmp_examples/types_dt.md!} ### Booleans @@ -388,19 +362,13 @@ A standard `bool` field will raise a `ValidationError` if the value is not one o Here is a script demonstrating some of these behaviors: -```py -{!.tmp_examples/types_boolean.py!} -``` -_(This script is complete, it should run "as is")_ +{!.tmp_examples/types_boolean.md!} ### Callable Fields can also be of type `Callable`: -```py -{!.tmp_examples/types_callable.py!} -``` -_(This script is complete, it should run "as is")_ +{!.tmp_examples/types_callable.md!} !!! warning Callable fields only perform a simple check that the argument is @@ -412,26 +380,17 @@ _(This script is complete, it should run "as is")_ *pydantic* supports the use of `Type[T]` to specify that a field may only accept classes (not instances) that are subclasses of `T`. -```py -{!.tmp_examples/types_type.py!} -``` -_(This script is complete, it should run "as is")_ +{!.tmp_examples/types_type.md!} You may also use `Type` to specify that any class is allowed. -```py -{!.tmp_examples/types_bare_type.py!} -``` -_(This script is complete, it should run "as is")_ +{!.tmp_examples/types_bare_type.md!} ### TypeVar `TypeVar` is supported either unconstrained, constrained or with a bound. -```py -{!.tmp_examples/types_typevar.py!} -``` -_(This script is complete, it should run "as is")_ +{!.tmp_examples/types_typevar.md!} ## Literal Type @@ -442,34 +401,22 @@ _(This script is complete, it should run "as is")_ *pydantic* supports the use of `typing.Literal` (or `typing_extensions.Literal` prior to Python 3.8) as a lightweight way to specify that a field may accept only specific literal values: -```py -{!.tmp_examples/types_literal1.py!} -``` -_(This script is complete, it should run "as is")_ +{!.tmp_examples/types_literal1.md!} One benefit of this field type is that it can be used to check for equality with one or more specific values without needing to declare custom validators: -```py -{!.tmp_examples/types_literal2.py!} -``` -_(This script is complete, it should run "as is")_ +{!.tmp_examples/types_literal2.md!} With proper ordering in an annotated `Union`, you can use this to parse types of decreasing specificity: -```py -{!.tmp_examples/types_literal3.py!} -``` -_(This script is complete, it should run "as is")_ +{!.tmp_examples/types_literal3.md!} ## Annotated Types ### NamedTuple -```py -{!.tmp_examples/annotated_types_named_tuple.py!} -``` -_(This script is complete, it should run "as is")_ +{!.tmp_examples/annotated_types_named_tuple.md!} ### TypedDict @@ -480,10 +427,7 @@ _(This script is complete, it should run "as is")_ We therefore recommend using [typing-extensions](https://pypi.org/project/typing-extensions/) with Python 3.8 as well. -```py -{!.tmp_examples/annotated_types_typed_dict.py!} -``` -_(This script is complete, it should run "as is")_ +{!.tmp_examples/annotated_types_typed_dict.md!} ## Pydantic Types @@ -676,10 +620,7 @@ For URI/URL validation the following types are available: The above types (which all inherit from `AnyUrl`) will attempt to give descriptive errors when invalid URLs are provided: -```py -{!.tmp_examples/types_urls.py!} -``` -_(This script is complete, it should run "as is")_ +{!.tmp_examples/types_urls.md!} If you require a custom URI/URL type, it can be created in a similar way to the types defined above. @@ -709,10 +650,7 @@ the above types export the following properties: If further validation is required, these properties can be used by validators to enforce specific behaviour: -```py -{!.tmp_examples/types_url_properties.py!} -``` -_(This script is complete, it should run "as is")_ +{!.tmp_examples/types_url_properties.md!} #### International Domains @@ -720,10 +658,7 @@ _(This script is complete, it should run "as is")_ [punycode](https://en.wikipedia.org/wiki/Punycode) (see [this article](https://www.xudongz.com/blog/2017/idn-phishing/) for a good description of why this is important): -```py -{!.tmp_examples/types_url_punycode.py!} -``` -_(This script is complete, it should run "as is")_ +{!.tmp_examples/types_url_punycode.md!} !!! warning @@ -757,10 +692,7 @@ You can use the `Color` data type for storing colors as per - [HSL strings](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value#HSL_colors) (e.g. `"hsl(270, 60%, 70%)"`, `"hsl(270, 60%, 70%, .5)"`) -```py -{!.tmp_examples/types_color.py!} -``` -_(This script is complete, it should run "as is")_ +{!.tmp_examples/types_color.md!} `Color` has the following methods: @@ -806,10 +738,7 @@ that you do not want to be visible in logging or tracebacks. `SecretStr` and `SecretBytes` can be initialized idempotently or by using `str` or `bytes` literals respectively. The `SecretStr` and `SecretBytes` will be formatted as either `'**********'` or `''` on conversion to json. -```py -{!.tmp_examples/types_secret_types.py!} -``` -_(This script is complete, it should run "as is")_ +{!.tmp_examples/types_secret_types.md!} ### Json Type @@ -817,20 +746,14 @@ You can use `Json` data type to make *pydantic* first load a raw JSON string. It can also optionally be used to parse the loaded object into another type base on the type `Json` is parameterised with: -```py -{!.tmp_examples/types_json_type.py!} -``` -_(This script is complete, it should run "as is")_ +{!.tmp_examples/types_json_type.md!} ### Payment Card Numbers The `PaymentCardNumber` type validates [payment cards](https://en.wikipedia.org/wiki/Payment_card) (such as a debit or credit card). -```py -{!.tmp_examples/types_payment_card_number.py!} -``` -_(This script is complete, it should run "as is")_ +{!.tmp_examples/types_payment_card_number.md!} `PaymentCardBrand` can be one of the following based on the BIN: @@ -850,10 +773,7 @@ The actual validation verifies the card number is: The value of numerous common types can be restricted using `con*` type functions: -```py -{!.tmp_examples/types_constrained.py!} -``` -_(This script is complete, it should run "as is")_ +{!.tmp_examples/types_constrained.md!} Where `Field` refers to the [field function](schema.md#field-customisation). @@ -957,10 +877,7 @@ The following caveats apply: even though `bool` is a subclass of `int` in Python. Other subclasses will work. - `StrictFloat` (and the `strict` option of `ConstrainedFloat`) will not accept `int`. -```py -{!.tmp_examples/types_strict.py!} -``` -_(This script is complete, it should run "as is")_ +{!.tmp_examples/types_strict.md!} ## ByteSize @@ -970,10 +887,7 @@ raw bytes and print out human readable versions of the bytes as well. !!! info Note that `1b` will be parsed as "1 byte" and not "1 bit". -```py -{!.tmp_examples/types_bytesize.py!} -``` -_(This script is complete, it should run "as is")_ +{!.tmp_examples/types_bytesize.md!} ## Custom Data Types @@ -988,10 +902,7 @@ to get validators to parse and validate the input data. These validators have the same semantics as in [Validators](validators.md), you can declare a parameter `config`, `field`, etc. -```py -{!.tmp_examples/types_custom_type.py!} -``` -_(This script is complete, it should run "as is")_ +{!.tmp_examples/types_custom_type.md!} Similar validation could be achieved using [`constr(regex=...)`](#constrained-types) except the value won't be formatted with a space, the schema would just include the full pattern and the returned value would be a vanilla string. @@ -1003,10 +914,7 @@ See [schema](schema.md) for more details on how the model's schema is generated. You can allow arbitrary types using the `arbitrary_types_allowed` config in the [Model Config](model_config.md). -```py -{!.tmp_examples/types_arbitrary_allowed.py!} -``` -_(This script is complete, it should run "as is")_ +{!.tmp_examples/types_arbitrary_allowed.md!} ### Generic Classes as Types @@ -1025,7 +933,4 @@ If the Generic class that you are using as a sub-type has a classmethod Because you can declare validators that receive the current `field`, you can extract the `sub_fields` (from the generic class type parameters) and validate data with them. -```py -{!.tmp_examples/types_generics.py!} -``` -_(This script is complete, it should run "as is")_ +{!.tmp_examples/types_generics.md!} diff --git a/docs/usage/validation_decorator.md b/docs/usage/validation_decorator.md index c01f6dce72..fad7a5f5f3 100644 --- a/docs/usage/validation_decorator.md +++ b/docs/usage/validation_decorator.md @@ -11,10 +11,7 @@ boilerplate. Example of usage: -```py -{!.tmp_examples/validation_decorator_main.py!} -``` -_(This script is complete, it should run "as is")_ +{!.tmp_examples/validation_decorator_main.md!} ## Argument Types @@ -23,10 +20,7 @@ as `Any`. Since `validate_arguments` internally uses a standard `BaseModel`, all [types](types.md) can be validated, including *pydantic* models and [custom types](types.md#custom-data-types). As with the rest of *pydantic*, types can be coerced by the decorator before they're passed to the actual function: -```py -{!.tmp_examples/validation_decorator_types.py!} -``` -_(This script is complete, it should run "as is")_ +{!.tmp_examples/validation_decorator_types.md!} A few notes: @@ -50,10 +44,7 @@ combinations of these: To demonstrate all the above parameter types: -```py -{!.tmp_examples/validation_decorator_parameter_types.py!} -``` -_(This script is complete, it should run "as is")_ +{!.tmp_examples/validation_decorator_parameter_types.md!} ## Using Field to describe function arguments @@ -62,17 +53,12 @@ the field and validations. In general it should be used in a type hint with [Annotated](schema.md#typingannotated-fields), unless `default_factory` is specified, in which case it should be used as the default value of the field: -```py -{!.tmp_examples/validation_decorator_field.py!} -``` -_(This script is complete, it should run "as is")_ +{!.tmp_examples/validation_decorator_field.md!} The [alias](model_config#alias-precedence) can be used with the decorator as normal. -```py -{!.tmp_examples/validation_decorator_field_alias.py!} -``` -_(This script is complete, it should run "as is")_ +{!.tmp_examples/validation_decorator_field_alias.md!} + ## Usage with mypy @@ -87,28 +73,20 @@ By default, arguments validation is done by directly calling the decorated funct But what if you wanted to validate them without *actually* calling the function? To do that you can call the `validate` method bound to the decorated function. -```py -{!.tmp_examples/validation_decorator_validate.py!} -``` -_(This script is complete, it should run "as is")_ +{!.tmp_examples/validation_decorator_validate.md!} ## Raw function The raw function which was decorated is accessible, this is useful if in some scenarios you trust your input arguments and want to call the function in the most performant way (see [notes on performance](#performance) below): -```py -{!.tmp_examples/validation_decorator_raw_function.py!} -``` -_(This script is complete, it should run "as is")_ +{!.tmp_examples/validation_decorator_raw_function.md!} ## Async Functions `validate_arguments` can also be used on async functions: -```py -{!.tmp_examples/validation_decorator_async.py!} -``` +{!.tmp_examples/validation_decorator_async.md!} ## Custom Config @@ -122,10 +100,7 @@ setting the `Config` sub-class in normal models. Configuration is set using the `config` keyword argument to the decorator, it may be either a config class or a dict of properties which are converted to a class later. -```py -{!.tmp_examples/validation_decorator_config.py!} -``` -_(This script is complete, it should run "as is")_ +{!.tmp_examples/validation_decorator_config.md!} ## Limitations diff --git a/docs/usage/validators.md b/docs/usage/validators.md index f720615177..2fc75f5c7e 100644 --- a/docs/usage/validators.md +++ b/docs/usage/validators.md @@ -1,9 +1,6 @@ Custom validation and complex relationships between objects can be achieved using the `validator` decorator. -```py -{!.tmp_examples/validators_simple.py!} -``` -_(This script is complete, it should run "as is")_ +{!.tmp_examples/validators_simple.md!} A few things to note on validators: @@ -37,10 +34,7 @@ A few things to note on validators: Validators can do a few more complex things: -```py -{!.tmp_examples/validators_pre_item.py!} -``` -_(This script is complete, it should run "as is")_ +{!.tmp_examples/validators_pre_item.md!} A few more things to note: @@ -55,10 +49,7 @@ A few more things to note: If using a validator with a subclass that references a `List` type field on a parent class, using `each_item=True` will cause the validator not to run; instead, the list must be iterated over programmatically. -```py -{!.tmp_examples/validators_subclass_each_item.py!} -``` -_(This script is complete, it should run "as is")_ +{!.tmp_examples/validators_subclass_each_item.md!} ## Validate Always @@ -66,10 +57,7 @@ For performance reasons, by default validators are not called for fields when a However there are situations where it may be useful or required to always call the validator, e.g. to set a dynamic default value. -```py -{!.tmp_examples/validators_always.py!} -``` -_(This script is complete, it should run "as is")_ +{!.tmp_examples/validators_always.md!} You'll often want to use this together with `pre`, since otherwise with `always=True` *pydantic* would try to validate the default `None` which would cause an error. @@ -82,10 +70,7 @@ then call it from multiple decorators. Obviously, this entails a lot of repetit boiler plate code. To circumvent this, the `allow_reuse` parameter has been added to `pydantic.validator` in **v1.2** (`False` by default): -```py -{!.tmp_examples/validators_allow_reuse.py!} -``` -_(This script is complete, it should run "as is")_ +{!.tmp_examples/validators_allow_reuse.md!} As it is obvious, repetition has been reduced and the models become again almost declarative. @@ -99,10 +84,7 @@ declarative. Validation can also be performed on the entire model's data. -```py -{!.tmp_examples/validators_root.py!} -``` -_(This script is complete, it should run "as is")_ +{!.tmp_examples/validators_root.md!} As with field validators, root validators can have `pre=True`, in which case they're called before field validation occurs (and are provided with the raw input data), or `pre=False` (the default), in which case @@ -125,7 +107,4 @@ In this case you should set `check_fields=False` on the validator. Validators also work with *pydantic* dataclasses. -```py -{!.tmp_examples/validators_dataclass.py!} -``` -_(This script is complete, it should run "as is")_ +{!.tmp_examples/validators_dataclass.md!} diff --git a/mkdocs.yml b/mkdocs.yml index df481b7936..a19e761948 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -21,7 +21,8 @@ theme: toggle: icon: material/lightbulb name: "Switch to light mode" - + features: + - content.tabs.link logo: 'logo-white.svg' favicon: 'favicon.png' @@ -79,6 +80,8 @@ markdown_extensions: - pymdownx.emoji: emoji_index: !!python/name:materialx.emoji.twemoji emoji_generator: !!python/name:materialx.emoji.to_svg +- pymdownx.tabbed: + alternate_style: true plugins: - search diff --git a/setup.cfg b/setup.cfg index a3fb54b9e8..505479bbe7 100644 --- a/setup.cfg +++ b/setup.cfg @@ -82,3 +82,11 @@ ignore_missing_imports = true [mypy-toml] ignore_missing_imports = true + +# ansi2html and devtools are required to avoid the need to install these packages when running linting, +# they're used in the docs build script +[mypy-ansi2html] +ignore_missing_imports = true + +[mypy-devtools] +ignore_missing_imports = true