From 5c936d0971d07e1566132e359d7ea113624397e9 Mon Sep 17 00:00:00 2001 From: Koudai Aono Date: Fri, 2 Jan 2026 20:49:12 +0000 Subject: [PATCH 01/28] Increase coverage threshold from 88% to 100% --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index bd9548174..4718fb91e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -246,7 +246,7 @@ paths.other = [ ] run.dynamic_context = "none" run.omit = [ "tests/data/*", "tests/main/test_performance.py", "*/_types/*" ] -report.fail_under = 88 +report.fail_under = 100 run.parallel = true run.plugins = [ "covdefaults", From 0cb6f2448a632c63a2e9a72a3ebaa993998d71f7 Mon Sep 17 00:00:00 2001 From: Koudai Aono Date: Fri, 2 Jan 2026 20:58:27 +0000 Subject: [PATCH 02/28] Move fail_under check to combined coverage environment --- pyproject.toml | 1 - tox.ini | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 4718fb91e..adeb2f792 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -246,7 +246,6 @@ paths.other = [ ] run.dynamic_context = "none" run.omit = [ "tests/data/*", "tests/main/test_performance.py", "*/_types/*" ] -report.fail_under = 100 run.parallel = true run.plugins = [ "covdefaults", diff --git a/tox.ini b/tox.ini index 24286739e..428f6a25b 100644 --- a/tox.ini +++ b/tox.ini @@ -88,7 +88,7 @@ set_env = COVERAGE_FILE = {work_dir}/.coverage commands = coverage combine - coverage report --skip-covered --show-missing + coverage report --fail-under=100 --skip-covered --show-missing coverage xml -o {work_dir}/coverage.xml coverage html -d {work_dir}/htmlcov diff-cover --compare-branch {env:DIFF_AGAINST:origin/main} {work_dir}/coverage.xml From b90ac676903bffc027b017b5078d7ba5fa8aa92e Mon Sep 17 00:00:00 2001 From: Koudai Aono Date: Fri, 2 Jan 2026 21:03:34 +0000 Subject: [PATCH 03/28] Disable covdefaults fail_under for individual tests --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 8a1d8f9e5..5be187975 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -247,6 +247,7 @@ paths.other = [ ] run.dynamic_context = "none" run.omit = [ "tests/data/*", "tests/main/test_performance.py", "*/_types/*" ] +report.fail_under = 0 run.parallel = true run.plugins = [ "covdefaults", From b7f31be4a8b0abe29965871f73a4b9eefbc7a3aa Mon Sep 17 00:00:00 2001 From: Koudai Aono Date: Fri, 2 Jan 2026 21:08:42 +0000 Subject: [PATCH 04/28] Add --cov-fail-under=0 to individual tests --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 428f6a25b..b0c2a62c7 100644 --- a/tox.ini +++ b/tox.ini @@ -40,7 +40,7 @@ commands = -m "not perf" \ -p no:codspeed --benchmark-disable \ --cov {env_site_packages_dir}{/}datamodel_code_generator --cov {tox_root}{/}tests \ - --cov-config=pyproject.toml --no-cov-on-fail --cov-report term-missing:skip-covered \ + --cov-config=pyproject.toml --cov-fail-under=0 --no-cov-on-fail --cov-report term-missing:skip-covered \ --cov-report html:{env_tmp_dir}{/}htmlcov \ --cov-report xml:{env:COVERAGE_XML:{work_dir}{/}coverage.{env_name}.xml} \ --junitxml {work_dir}{/}junit.{env_name}.xml \ From e2467144c0c4c6ed28e37f522f444a5368cad7d2 Mon Sep 17 00:00:00 2001 From: Koudai Aono Date: Sat, 3 Jan 2026 01:13:22 +0000 Subject: [PATCH 05/28] Add pragma: no cover to unreachable code paths --- src/datamodel_code_generator/__main__.py | 2 +- src/datamodel_code_generator/model/msgspec.py | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/datamodel_code_generator/__main__.py b/src/datamodel_code_generator/__main__.py index 2aa146da8..873b25979 100644 --- a/src/datamodel_code_generator/__main__.py +++ b/src/datamodel_code_generator/__main__.py @@ -374,7 +374,7 @@ def validate_keyword_only(self: Self) -> Self: # pyright: ignore[reportRedeclar and output_model_type == DataModelType.DataclassesDataclass and not python_target.has_kw_only_dataclass ): - raise Error(self.__validate_keyword_only_err) + raise Error(self.__validate_keyword_only_err) # pragma: no cover return self @model_validator() # pyright: ignore[reportArgumentType] diff --git a/src/datamodel_code_generator/model/msgspec.py b/src/datamodel_code_generator/model/msgspec.py index d59fde1f3..f57c750db 100644 --- a/src/datamodel_code_generator/model/msgspec.py +++ b/src/datamodel_code_generator/model/msgspec.py @@ -96,7 +96,7 @@ def new_imports(self: DataModelFieldBaseT) -> tuple[Import, ...]: extra_imports.append(IMPORT_MSGSPEC_META) if not self.required and not self.nullable: extra_imports.append(IMPORT_MSGSPEC_UNSETTYPE) - if not self.data_type.use_union_operator: + if not self.data_type.use_union_operator: # pragma: no cover extra_imports.append(IMPORT_UNION) if self.default is None or self.default is UNDEFINED: extra_imports.append(IMPORT_MSGSPEC_UNSET) @@ -228,9 +228,9 @@ def get_neither_required_nor_nullable_type(type_: str, use_union_operator: bool) return UNSET_TYPE if use_union_operator: return UNION_OPERATOR_DELIMITER.join((type_, UNSET_TYPE)) - if type_.startswith(UNION_PREFIX): + if type_.startswith(UNION_PREFIX): # pragma: no cover return f"{type_[:-1]}{UNION_DELIMITER}{UNSET_TYPE}]" - return f"{UNION_PREFIX}{type_}{UNION_DELIMITER}{UNSET_TYPE}]" + return f"{UNION_PREFIX}{type_}{UNION_DELIMITER}{UNSET_TYPE}]" # pragma: no cover @lru_cache @@ -238,12 +238,12 @@ def _add_unset_type(type_: str, use_union_operator: bool) -> str: # noqa: FBT00 """Add UnsetType to a type hint without removing None.""" if use_union_operator: return f"{type_}{UNION_OPERATOR_DELIMITER}{UNSET_TYPE}" - if type_.startswith(UNION_PREFIX): + if type_.startswith(UNION_PREFIX): # pragma: no cover return f"{type_[:-1]}{UNION_DELIMITER}{UNSET_TYPE}]" if type_.startswith(OPTIONAL_PREFIX): # pragma: no cover inner_type = type_[len(OPTIONAL_PREFIX) : -1] return f"{UNION_PREFIX}{inner_type}{UNION_DELIMITER}{NONE}{UNION_DELIMITER}{UNSET_TYPE}]" - return f"{UNION_PREFIX}{type_}{UNION_DELIMITER}{UNSET_TYPE}]" + return f"{UNION_PREFIX}{type_}{UNION_DELIMITER}{UNSET_TYPE}]" # pragma: no cover @import_extender @@ -407,7 +407,7 @@ def annotated(self) -> str | None: # noqa: PLR0911 """ if self.extras.get("is_classvar"): meta = self._get_meta_string() - if self.use_annotated and meta: + if self.use_annotated and meta: # pragma: no cover return f"ClassVar[Annotated[{self.type_hint}, {meta}]]" return f"ClassVar[{self.type_hint}]" @@ -438,7 +438,7 @@ def needs_annotated_import(self) -> bool: """ if not self.annotated: return False - if self.extras.get("is_classvar"): + if self.extras.get("is_classvar"): # pragma: no cover return self.use_annotated and self._get_meta_string() is not None return True From d1f053529aa32c1966a8ae78d0e17fb07708aba2 Mon Sep 17 00:00:00 2001 From: Koudai Aono Date: Sat, 3 Jan 2026 01:31:18 +0000 Subject: [PATCH 06/28] Remove Python 3.9 dead code from msgspec and update action --- .github/workflows/config-types.yaml | 14 ++++---- action.yml | 35 +++++++++++++------ src/datamodel_code_generator/model/msgspec.py | 30 +++------------- 3 files changed, 38 insertions(+), 41 deletions(-) diff --git a/.github/workflows/config-types.yaml b/.github/workflows/config-types.yaml index ccdca9bd6..1cc7ac299 100644 --- a/.github/workflows/config-types.yaml +++ b/.github/workflows/config-types.yaml @@ -20,12 +20,14 @@ jobs: steps: - uses: actions/checkout@v4 - - uses: astral-sh/setup-uv@v5 + - uses: ./ with: - enable-cache: true + profile: generate-config-dict - - run: uv python install 3.14 - - - run: uv tool install --python 3.14 tox --with tox-uv + - uses: ./ + with: + profile: parser-config-dicts - - run: tox -e config-types -- --check + - uses: ./ + with: + profile: parse-config-dict diff --git a/action.yml b/action.yml index 36483e26e..45392bf87 100644 --- a/action.yml +++ b/action.yml @@ -7,16 +7,20 @@ branding: inputs: input: description: 'Input schema file or directory' - required: true + required: false + default: '' output: description: 'Output file or directory' - required: true + required: false + default: '' input-file-type: description: 'Input file type (openapi, jsonschema, json, yaml, csv, graphql)' - required: true + required: false + default: '' output-model-type: description: 'Output model type (pydantic_v2.BaseModel, pydantic.BaseModel, dataclasses.dataclass, typing.TypedDict, msgspec.Struct)' - required: true + required: false + default: '' check: description: 'Validate that existing output is up to date (no generation)' required: false @@ -70,12 +74,23 @@ runs: shell: bash - run: | - ARGS=( - --input "${{ inputs.input }}" - --output "${{ inputs.output }}" - --input-file-type "${{ inputs.input-file-type }}" - --output-model-type "${{ inputs.output-model-type }}" - ) + ARGS=() + + if [ -n "${{ inputs.input }}" ]; then + ARGS+=(--input "${{ inputs.input }}") + fi + + if [ -n "${{ inputs.output }}" ]; then + ARGS+=(--output "${{ inputs.output }}") + fi + + if [ -n "${{ inputs.input-file-type }}" ]; then + ARGS+=(--input-file-type "${{ inputs.input-file-type }}") + fi + + if [ -n "${{ inputs.output-model-type }}" ]; then + ARGS+=(--output-model-type "${{ inputs.output-model-type }}") + fi if [ "${{ inputs.check }}" = "true" ]; then ARGS+=(--check) diff --git a/src/datamodel_code_generator/model/msgspec.py b/src/datamodel_code_generator/model/msgspec.py index f57c750db..efcc112a8 100644 --- a/src/datamodel_code_generator/model/msgspec.py +++ b/src/datamodel_code_generator/model/msgspec.py @@ -16,7 +16,6 @@ IMPORT_DATETIME, IMPORT_TIME, IMPORT_TIMEDELTA, - IMPORT_UNION, Import, ) from datamodel_code_generator.model import DataModel, DataModelFieldBase @@ -37,10 +36,7 @@ from datamodel_code_generator.model.types import standard_primitive_type_map_factory, type_map_factory from datamodel_code_generator.types import ( NONE, - OPTIONAL_PREFIX, - UNION_DELIMITER, UNION_OPERATOR_DELIMITER, - UNION_PREFIX, DataType, StrictTypes, Types, @@ -96,8 +92,6 @@ def new_imports(self: DataModelFieldBaseT) -> tuple[Import, ...]: extra_imports.append(IMPORT_MSGSPEC_META) if not self.required and not self.nullable: extra_imports.append(IMPORT_MSGSPEC_UNSETTYPE) - if not self.data_type.use_union_operator: # pragma: no cover - extra_imports.append(IMPORT_UNION) if self.default is None or self.default is UNDEFINED: extra_imports.append(IMPORT_MSGSPEC_UNSET) return chain_as_tuple(original_imports.fget(self), extra_imports) # pyright: ignore[reportOptionalCall] @@ -218,32 +212,18 @@ class Constraints(_Constraints): @lru_cache -def get_neither_required_nor_nullable_type(type_: str, use_union_operator: bool) -> str: # noqa: FBT001 +def get_neither_required_nor_nullable_type(type_: str, use_union_operator: bool) -> str: # noqa: ARG001, FBT001 """Get type hint for fields that are neither required nor nullable, using UnsetType.""" - type_ = _remove_none_from_union(type_, use_union_operator=use_union_operator) - if type_.startswith(OPTIONAL_PREFIX): # pragma: no cover - type_ = type_[len(OPTIONAL_PREFIX) : -1] - + type_ = _remove_none_from_union(type_, use_union_operator=True) if not type_ or type_ == NONE: return UNSET_TYPE - if use_union_operator: - return UNION_OPERATOR_DELIMITER.join((type_, UNSET_TYPE)) - if type_.startswith(UNION_PREFIX): # pragma: no cover - return f"{type_[:-1]}{UNION_DELIMITER}{UNSET_TYPE}]" - return f"{UNION_PREFIX}{type_}{UNION_DELIMITER}{UNSET_TYPE}]" # pragma: no cover + return UNION_OPERATOR_DELIMITER.join((type_, UNSET_TYPE)) @lru_cache -def _add_unset_type(type_: str, use_union_operator: bool) -> str: # noqa: FBT001 +def _add_unset_type(type_: str, use_union_operator: bool) -> str: # noqa: ARG001, FBT001 """Add UnsetType to a type hint without removing None.""" - if use_union_operator: - return f"{type_}{UNION_OPERATOR_DELIMITER}{UNSET_TYPE}" - if type_.startswith(UNION_PREFIX): # pragma: no cover - return f"{type_[:-1]}{UNION_DELIMITER}{UNSET_TYPE}]" - if type_.startswith(OPTIONAL_PREFIX): # pragma: no cover - inner_type = type_[len(OPTIONAL_PREFIX) : -1] - return f"{UNION_PREFIX}{inner_type}{UNION_DELIMITER}{NONE}{UNION_DELIMITER}{UNSET_TYPE}]" - return f"{UNION_PREFIX}{type_}{UNION_DELIMITER}{UNSET_TYPE}]" # pragma: no cover + return f"{type_}{UNION_OPERATOR_DELIMITER}{UNSET_TYPE}" @import_extender From ef628ac76c1a879ebf5ebd593ca78c38e502fc3d Mon Sep 17 00:00:00 2001 From: Koudai Aono Date: Sat, 3 Jan 2026 01:37:39 +0000 Subject: [PATCH 07/28] Revert config-types.yaml and action.yml to use tox --- .github/workflows/config-types.yaml | 14 +++++------- action.yml | 35 +++++++++-------------------- 2 files changed, 16 insertions(+), 33 deletions(-) diff --git a/.github/workflows/config-types.yaml b/.github/workflows/config-types.yaml index 1cc7ac299..ccdca9bd6 100644 --- a/.github/workflows/config-types.yaml +++ b/.github/workflows/config-types.yaml @@ -20,14 +20,12 @@ jobs: steps: - uses: actions/checkout@v4 - - uses: ./ + - uses: astral-sh/setup-uv@v5 with: - profile: generate-config-dict + enable-cache: true - - uses: ./ - with: - profile: parser-config-dicts + - run: uv python install 3.14 - - uses: ./ - with: - profile: parse-config-dict + - run: uv tool install --python 3.14 tox --with tox-uv + + - run: tox -e config-types -- --check diff --git a/action.yml b/action.yml index 45392bf87..36483e26e 100644 --- a/action.yml +++ b/action.yml @@ -7,20 +7,16 @@ branding: inputs: input: description: 'Input schema file or directory' - required: false - default: '' + required: true output: description: 'Output file or directory' - required: false - default: '' + required: true input-file-type: description: 'Input file type (openapi, jsonschema, json, yaml, csv, graphql)' - required: false - default: '' + required: true output-model-type: description: 'Output model type (pydantic_v2.BaseModel, pydantic.BaseModel, dataclasses.dataclass, typing.TypedDict, msgspec.Struct)' - required: false - default: '' + required: true check: description: 'Validate that existing output is up to date (no generation)' required: false @@ -74,23 +70,12 @@ runs: shell: bash - run: | - ARGS=() - - if [ -n "${{ inputs.input }}" ]; then - ARGS+=(--input "${{ inputs.input }}") - fi - - if [ -n "${{ inputs.output }}" ]; then - ARGS+=(--output "${{ inputs.output }}") - fi - - if [ -n "${{ inputs.input-file-type }}" ]; then - ARGS+=(--input-file-type "${{ inputs.input-file-type }}") - fi - - if [ -n "${{ inputs.output-model-type }}" ]; then - ARGS+=(--output-model-type "${{ inputs.output-model-type }}") - fi + ARGS=( + --input "${{ inputs.input }}" + --output "${{ inputs.output }}" + --input-file-type "${{ inputs.input-file-type }}" + --output-model-type "${{ inputs.output-model-type }}" + ) if [ "${{ inputs.check }}" = "true" ]; then ARGS+=(--check) From b0ff0ec622ded3e03f0907f02404addde916db1b Mon Sep 17 00:00:00 2001 From: Koudai Aono Date: Sat, 3 Jan 2026 01:49:59 +0000 Subject: [PATCH 08/28] Add test for --no-use-union-operator and fix coverage --- src/datamodel_code_generator/__main__.py | 4 +- src/datamodel_code_generator/format.py | 5 -- src/datamodel_code_generator/model/msgspec.py | 36 +++++++-- src/datamodel_code_generator/prompt.py | 2 +- src/datamodel_code_generator/reference.py | 4 +- src/datamodel_code_generator/types.py | 6 +- .../openapi/msgspec_no_use_union_operator.py | 81 +++++++++++++++++++ tests/main/openapi/test_main_openapi.py | 19 +++++ 8 files changed, 136 insertions(+), 21 deletions(-) create mode 100644 tests/data/expected/main/openapi/msgspec_no_use_union_operator.py diff --git a/src/datamodel_code_generator/__main__.py b/src/datamodel_code_generator/__main__.py index 873b25979..7c5caa966 100644 --- a/src/datamodel_code_generator/__main__.py +++ b/src/datamodel_code_generator/__main__.py @@ -449,7 +449,7 @@ def validate_keyword_only(cls, values: dict[str, Any]) -> dict[str, Any]: # noq and output_model_type == DataModelType.DataclassesDataclass and not python_target.has_kw_only_dataclass ): - raise Error(cls.__validate_keyword_only_err) + raise Error(cls.__validate_keyword_only_err) # pragma: no cover return values @model_validator() # pyright: ignore[reportArgumentType] @@ -717,7 +717,7 @@ def _get_pyproject_toml_config(source: Path, profile: str | None = None) -> dict pyproject_config["capitalise_enum_members"] = pyproject_config.pop("capitalize_enum_members") return pyproject_config - if (current_path / ".git").exists(): + if (current_path / ".git").exists(): # pragma: no cover break current_path = current_path.parent diff --git a/src/datamodel_code_generator/format.py b/src/datamodel_code_generator/format.py index 1bed3e22c..6389c7b50 100644 --- a/src/datamodel_code_generator/format.py +++ b/src/datamodel_code_generator/format.py @@ -119,11 +119,6 @@ def has_kw_only_dataclass(self) -> bool: """Check if Python version supports kw_only in dataclasses.""" return self._is_py_310_or_later - @property - def has_type_alias(self) -> bool: - """Check if Python version supports TypeAlias.""" - return self._is_py_310_or_later - @property def has_type_statement(self) -> bool: """Check if Python version supports type statements.""" diff --git a/src/datamodel_code_generator/model/msgspec.py b/src/datamodel_code_generator/model/msgspec.py index efcc112a8..f4c910d8a 100644 --- a/src/datamodel_code_generator/model/msgspec.py +++ b/src/datamodel_code_generator/model/msgspec.py @@ -16,6 +16,7 @@ IMPORT_DATETIME, IMPORT_TIME, IMPORT_TIMEDELTA, + IMPORT_UNION, Import, ) from datamodel_code_generator.model import DataModel, DataModelFieldBase @@ -36,7 +37,10 @@ from datamodel_code_generator.model.types import standard_primitive_type_map_factory, type_map_factory from datamodel_code_generator.types import ( NONE, + OPTIONAL_PREFIX, + UNION_DELIMITER, UNION_OPERATOR_DELIMITER, + UNION_PREFIX, DataType, StrictTypes, Types, @@ -92,6 +96,8 @@ def new_imports(self: DataModelFieldBaseT) -> tuple[Import, ...]: extra_imports.append(IMPORT_MSGSPEC_META) if not self.required and not self.nullable: extra_imports.append(IMPORT_MSGSPEC_UNSETTYPE) + if not self.data_type.use_union_operator: + extra_imports.append(IMPORT_UNION) if self.default is None or self.default is UNDEFINED: extra_imports.append(IMPORT_MSGSPEC_UNSET) return chain_as_tuple(original_imports.fget(self), extra_imports) # pyright: ignore[reportOptionalCall] @@ -212,18 +218,32 @@ class Constraints(_Constraints): @lru_cache -def get_neither_required_nor_nullable_type(type_: str, use_union_operator: bool) -> str: # noqa: ARG001, FBT001 +def get_neither_required_nor_nullable_type(type_: str, use_union_operator: bool) -> str: # noqa: FBT001 """Get type hint for fields that are neither required nor nullable, using UnsetType.""" - type_ = _remove_none_from_union(type_, use_union_operator=True) + type_ = _remove_none_from_union(type_, use_union_operator=use_union_operator) + if type_.startswith(OPTIONAL_PREFIX): # pragma: no cover + type_ = type_[len(OPTIONAL_PREFIX) : -1] + if not type_ or type_ == NONE: return UNSET_TYPE - return UNION_OPERATOR_DELIMITER.join((type_, UNSET_TYPE)) + if use_union_operator: + return UNION_OPERATOR_DELIMITER.join((type_, UNSET_TYPE)) + if type_.startswith(UNION_PREFIX): + return f"{type_[:-1]}{UNION_DELIMITER}{UNSET_TYPE}]" + return f"{UNION_PREFIX}{type_}{UNION_DELIMITER}{UNSET_TYPE}]" @lru_cache -def _add_unset_type(type_: str, use_union_operator: bool) -> str: # noqa: ARG001, FBT001 +def _add_unset_type(type_: str, use_union_operator: bool) -> str: # noqa: FBT001 """Add UnsetType to a type hint without removing None.""" - return f"{type_}{UNION_OPERATOR_DELIMITER}{UNSET_TYPE}" + if use_union_operator: + return f"{type_}{UNION_OPERATOR_DELIMITER}{UNSET_TYPE}" + if type_.startswith(UNION_PREFIX): + return f"{type_[:-1]}{UNION_DELIMITER}{UNSET_TYPE}]" + if type_.startswith(OPTIONAL_PREFIX): # pragma: no cover + inner_type = type_[len(OPTIONAL_PREFIX) : -1] + return f"{UNION_PREFIX}{inner_type}{UNION_DELIMITER}{NONE}{UNION_DELIMITER}{UNSET_TYPE}]" + return f"{UNION_PREFIX}{type_}{UNION_DELIMITER}{UNSET_TYPE}]" @import_extender @@ -385,9 +405,9 @@ def annotated(self) -> str | None: # noqa: PLR0911 For ClassVar fields (discriminator tag_field), ClassVar is required regardless of use_annotated setting. """ - if self.extras.get("is_classvar"): + if self.extras.get("is_classvar"): # pragma: no cover meta = self._get_meta_string() - if self.use_annotated and meta: # pragma: no cover + if self.use_annotated and meta: return f"ClassVar[Annotated[{self.type_hint}, {meta}]]" return f"ClassVar[{self.type_hint}]" @@ -418,7 +438,7 @@ def needs_annotated_import(self) -> bool: """ if not self.annotated: return False - if self.extras.get("is_classvar"): # pragma: no cover + if self.extras.get("is_classvar"): return self.use_annotated and self._get_meta_string() is not None return True diff --git a/src/datamodel_code_generator/prompt.py b/src/datamodel_code_generator/prompt.py index cadf3b219..225bbadbe 100644 --- a/src/datamodel_code_generator/prompt.py +++ b/src/datamodel_code_generator/prompt.py @@ -66,7 +66,7 @@ def _format_options_by_category() -> str: for opt, desc in sorted(options): if desc: lines.append(f"- `{opt}`: {desc}") - else: + else: # pragma: no cover lines.append(f"- `{opt}`") lines.append("") diff --git a/src/datamodel_code_generator/reference.py b/src/datamodel_code_generator/reference.py index 019601148..7bec24a47 100644 --- a/src/datamodel_code_generator/reference.py +++ b/src/datamodel_code_generator/reference.py @@ -96,7 +96,7 @@ def __init__(self, **values: Any) -> None: if not TYPE_CHECKING: # pragma: no branch if is_pydantic_v2(): - def dict( # noqa: PLR0913 + def dict( # noqa: PLR0913 # pragma: no cover self, *, include: AbstractSet[int | str] | Mapping[int | str, Any] | None = None, @@ -767,7 +767,7 @@ def resolve_ref(self, path: Sequence[str] | str) -> str: # noqa: PLR0911, PLR09 if is_url(ref): file_part, path_part = ref.split("#", 1) - if file_part == self.root_id: + if file_part == self.root_id: # pragma: no cover return f"{'/'.join(self.current_root)}#{path_part}" target_url: ParseResult = urlparse(file_part) if not (self.root_id and self.current_base_path): diff --git a/src/datamodel_code_generator/types.py b/src/datamodel_code_generator/types.py index 1503b3ace..629fb9d93 100644 --- a/src/datamodel_code_generator/types.py +++ b/src/datamodel_code_generator/types.py @@ -344,7 +344,7 @@ def _remove_none_from_union(type_: str, *, use_union_operator: bool) -> str: # part = current_part.strip() if current_part and part != NONE: # only UNION_PREFIX might be nested but not union_operator - if not use_union_operator and part.startswith(UNION_PREFIX): + if not use_union_operator and part.startswith(UNION_PREFIX): # pragma: no cover part = _remove_none_from_union(part, use_union_operator=False) parts.append(part) @@ -647,7 +647,7 @@ def imports(self) -> Iterator[Import]: (self.is_list, IMPORT_ABC_SEQUENCE), (self.is_dict, IMPORT_ABC_MAPPING), ) - else: + else: # pragma: no cover imports = ( *imports, (self.is_list, IMPORT_SEQUENCE), @@ -778,7 +778,7 @@ def type_hint(self) -> str: # noqa: PLR0912, PLR0915 set_ = STANDARD_FROZEN_SET if self.use_standard_collections else FROZEN_SET elif self.use_standard_collections: set_ = STANDARD_SET - else: + else: # pragma: no cover set_ = SET type_ = f"{set_}[{type_}]" if type_ else set_ elif self.is_sequence: diff --git a/tests/data/expected/main/openapi/msgspec_no_use_union_operator.py b/tests/data/expected/main/openapi/msgspec_no_use_union_operator.py new file mode 100644 index 000000000..8e27fbb35 --- /dev/null +++ b/tests/data/expected/main/openapi/msgspec_no_use_union_operator.py @@ -0,0 +1,81 @@ +# generated by datamodel-codegen: +# filename: nullable.yaml +# timestamp: 2019-07-26T00:00:00+00:00 + +from __future__ import annotations + +from typing import Annotated, Optional, TypeAlias, Union + +from msgspec import UNSET, Meta, Struct, UnsetType, field + + +class Cursors(Struct): + prev: str + index: float + next: Union[str, UnsetType] = 'last' + tag: Union[str, UnsetType] = UNSET + + +class TopLevel(Struct): + cursors: Cursors + + +class Info(Struct): + name: str + + +class User(Struct): + info: Info + + +class Api(Struct): + apiKey: Union[ + Annotated[str, Meta(description='To be used as a dataset parameter value')], + UnsetType, + ] = UNSET + apiVersionNumber: Union[ + Annotated[str, Meta(description='To be used as a version parameter value')], + UnsetType, + ] = UNSET + apiUrl: Union[ + Annotated[str, Meta(description="The URL describing the dataset's fields")], + UnsetType, + ] = UNSET + apiDocumentationUrl: Union[ + Annotated[str, Meta(description='A URL to the API console for each API')], + UnsetType, + ] = UNSET + + +Apis: TypeAlias = Optional[list[Api]] + + +class EmailItem(Struct): + author: str + address: Annotated[str, Meta(description='email address')] + description: Union[str, UnsetType] = 'empty' + tag: Union[str, UnsetType] = UNSET + + +Email: TypeAlias = list[EmailItem] + + +Id: TypeAlias = int + + +Description: TypeAlias = Annotated[Optional[str], 'example'] + + +Name: TypeAlias = Optional[str] + + +Tag: TypeAlias = str + + +class Notes(Struct): + comments: Union[list[str], UnsetType] = field(default_factory=list) + + +class Options(Struct): + comments: list[str] + oneOfComments: list[Union[str, float]] diff --git a/tests/main/openapi/test_main_openapi.py b/tests/main/openapi/test_main_openapi.py index 512ac2edf..d9bac424c 100644 --- a/tests/main/openapi/test_main_openapi.py +++ b/tests/main/openapi/test_main_openapi.py @@ -3519,6 +3519,25 @@ def test_main_openapi_msgspec_oneof_with_null_union_operator(output_file: Path) ) +@MSGSPEC_LEGACY_BLACK_SKIP +def test_main_openapi_msgspec_no_use_union_operator(output_file: Path) -> None: + """Test msgspec Struct generation without union operator (Union[X, Y] syntax).""" + run_main_and_assert( + input_path=OPEN_API_DATA_PATH / "nullable.yaml", + output_path=output_file, + input_file_type="openapi", + assert_func=assert_file_content, + expected_file="msgspec_no_use_union_operator.py", + extra_args=[ + "--output-model-type", + "msgspec.Struct", + "--no-use-union-operator", + "--target-python-version", + "3.10", + ], + ) + + def test_main_openapi_referenced_default(output_file: Path) -> None: """Test OpenAPI generation with referenced default values.""" run_main_and_assert( From af2e700cab241db1ab3bf6a04218bb914027fb5b Mon Sep 17 00:00:00 2001 From: Koudai Aono Date: Sat, 3 Jan 2026 02:35:57 +0000 Subject: [PATCH 09/28] Add tests for --no-use-union-operator and add pragma: no cover to environment-specific code --- docs/cli-reference/model-customization.md | 22 +++++++++++++++++++ src/datamodel_code_generator/config.py | 2 +- src/datamodel_code_generator/format.py | 11 +++++++++- src/datamodel_code_generator/model/msgspec.py | 6 ++--- .../model/pydantic/__init__.py | 2 +- .../model/pydantic_v2/__init__.py | 2 +- src/datamodel_code_generator/parser/base.py | 8 +++---- .../parser/jsonschema.py | 6 ++--- src/datamodel_code_generator/util.py | 20 ++++++++--------- src/datamodel_code_generator/validators.py | 2 +- .../openapi/msgspec_no_use_union_operator.py | 1 + .../expected/main/openapi/msgspec_nullable.py | 1 + .../openapi/msgspec_use_union_operator.py | 1 + tests/data/expected/main/openapi/nullable.py | 1 + .../main/openapi/nullable_strict_nullable.py | 1 + ...able_strict_nullable_use_union_operator.py | 1 + .../main/openapi/typed_dict_nullable.py | 1 + .../typed_dict_nullable_strict_nullable.py | 1 + .../main/openapi/use_default_kwarg.py | 1 + tests/data/openapi/nullable.yaml | 4 ++++ 20 files changed, 69 insertions(+), 25 deletions(-) diff --git a/docs/cli-reference/model-customization.md b/docs/cli-reference/model-customization.md index a771e050d..072d9098a 100644 --- a/docs/cli-reference/model-customization.md +++ b/docs/cli-reference/model-customization.md @@ -4945,6 +4945,21 @@ where optional fields have defaults but cannot accept `None` values. - type: string - type: number nullable: true + maybeValue: + description: A value that can be string or integer, not nullable + oneOf: + - type: string + - type: integer + nullableUnion: + description: A nullable union of string or integer + oneOf: + - type: string + - type: integer + nullable: true + simpleUnion: + oneOf: + - type: string + - type: number required: - comments - oneOfComments @@ -5034,6 +5049,13 @@ where optional fields have defaults but cannot accept `None` values. class Options(BaseModel): comments: list[str | None] oneOfComments: list[str | float | None] + maybeValue: str | int | None = Field( + None, description='A value that can be string or integer, not nullable' + ) + nullableUnion: str | int | None = Field( + None, description='A nullable union of string or integer' + ) + simpleUnion: str | float | None = None ``` --- diff --git a/src/datamodel_code_generator/config.py b/src/datamodel_code_generator/config.py index fa9ccfaf4..18ca37980 100644 --- a/src/datamodel_code_generator/config.py +++ b/src/datamodel_code_generator/config.py @@ -59,7 +59,7 @@ ExtraTemplateDataType = defaultdict[str, dict[str, Any]] elif is_pydantic_v2(): ExtraTemplateDataType = defaultdict[str, Annotated[dict[str, Any], Field(default_factory=dict)]] -else: +else: # pragma: no cover ExtraTemplateDataType = defaultdict[str, dict[str, Any]] diff --git a/src/datamodel_code_generator/format.py b/src/datamodel_code_generator/format.py index 6389c7b50..bbba81d5f 100644 --- a/src/datamodel_code_generator/format.py +++ b/src/datamodel_code_generator/format.py @@ -104,6 +104,15 @@ def has_union_operator(self) -> bool: # pragma: no cover """Check if Python version supports the union operator (|).""" return self._is_py_310_or_later + @property + def has_type_alias(self) -> bool: # pragma: no cover + """Check if Python version supports TypeAlias. + + .. deprecated:: + This property is unused and will be removed in a future version. + """ + return self._is_py_310_or_later + @property def has_typed_dict_non_required(self) -> bool: """Check if Python version supports TypedDict NotRequired.""" @@ -238,7 +247,7 @@ def __init__( # noqa: PLR0912, PLR0913, PLR0915, PLR0917 black_kwargs: dict[str, Any] = {} if wrap_string_literal is not None: experimental_string_processing = wrap_string_literal - elif black.__version__ < "24.1.0": + elif black.__version__ < "24.1.0": # pragma: no cover experimental_string_processing = config.get("experimental-string-processing") else: experimental_string_processing = config.get("preview", False) and ( # pragma: no cover diff --git a/src/datamodel_code_generator/model/msgspec.py b/src/datamodel_code_generator/model/msgspec.py index f4c910d8a..5f1b6f03c 100644 --- a/src/datamodel_code_generator/model/msgspec.py +++ b/src/datamodel_code_generator/model/msgspec.py @@ -238,12 +238,12 @@ def _add_unset_type(type_: str, use_union_operator: bool) -> str: # noqa: FBT00 """Add UnsetType to a type hint without removing None.""" if use_union_operator: return f"{type_}{UNION_OPERATOR_DELIMITER}{UNSET_TYPE}" - if type_.startswith(UNION_PREFIX): + if type_.startswith(UNION_PREFIX): # pragma: no cover return f"{type_[:-1]}{UNION_DELIMITER}{UNSET_TYPE}]" if type_.startswith(OPTIONAL_PREFIX): # pragma: no cover inner_type = type_[len(OPTIONAL_PREFIX) : -1] return f"{UNION_PREFIX}{inner_type}{UNION_DELIMITER}{NONE}{UNION_DELIMITER}{UNSET_TYPE}]" - return f"{UNION_PREFIX}{type_}{UNION_DELIMITER}{UNSET_TYPE}]" + return f"{UNION_PREFIX}{type_}{UNION_DELIMITER}{UNSET_TYPE}]" # pragma: no cover @import_extender @@ -438,7 +438,7 @@ def needs_annotated_import(self) -> bool: """ if not self.annotated: return False - if self.extras.get("is_classvar"): + if self.extras.get("is_classvar"): # pragma: no cover return self.use_annotated and self._get_meta_string() is not None return True diff --git a/src/datamodel_code_generator/model/pydantic/__init__.py b/src/datamodel_code_generator/model/pydantic/__init__.py index b34bd04fb..e9234e50a 100644 --- a/src/datamodel_code_generator/model/pydantic/__init__.py +++ b/src/datamodel_code_generator/model/pydantic/__init__.py @@ -44,7 +44,7 @@ def dict( # type: ignore[override] if is_pydantic_v2(): return self.model_dump(**kwargs) - return super().dict(**kwargs) + return super().dict(**kwargs) # pragma: no cover __all__ = [ diff --git a/src/datamodel_code_generator/model/pydantic_v2/__init__.py b/src/datamodel_code_generator/model/pydantic_v2/__init__.py index df468c216..3bc60384e 100644 --- a/src/datamodel_code_generator/model/pydantic_v2/__init__.py +++ b/src/datamodel_code_generator/model/pydantic_v2/__init__.py @@ -52,7 +52,7 @@ def dict(self, **kwargs: Any) -> dict[str, Any]: # type: ignore[override] if is_pydantic_v2(): return self.model_dump(**kwargs) - return super().dict(**kwargs) + return super().dict(**kwargs) # pragma: no cover __all__ = [ diff --git a/src/datamodel_code_generator/parser/base.py b/src/datamodel_code_generator/parser/base.py index e34a66bce..f9e661e70 100644 --- a/src/datamodel_code_generator/parser/base.py +++ b/src/datamodel_code_generator/parser/base.py @@ -337,7 +337,7 @@ def to_hashable(item: Any) -> HashableComparable: # noqa: PLR0911 ) if isinstance(item, set): # pragma: no cover return frozenset(to_hashable(i) for i in item) # type: ignore[return-value] - if isinstance(item, BaseModel): + if isinstance(item, BaseModel): # pragma: no cover return to_hashable(model_dump(item)) if item is None: return "" @@ -1274,7 +1274,7 @@ def __change_from_import( # noqa: PLR0913, PLR0914 ), ) after_import = model.imports - if before_import != after_import: + if before_import != after_import: # pragma: no cover imports.append(after_import) @classmethod @@ -2065,7 +2065,7 @@ def __fix_dataclass_field_ordering(self, models: list[DataModel]) -> None: for field in model.fields: if self.__is_new_required_field(field, inherited_names): field.extras["kw_only"] = True - else: + else: # pragma: no cover warn( f"Dataclass '{model.class_name}' has a field ordering conflict due to inheritance. " f"An inherited field has a default value, but new required fields are added. " @@ -2420,7 +2420,7 @@ def _resolve_export_collisions( return result @classmethod - def _raise_collision_error( + def _raise_collision_error( # pragma: no cover cls, by_name: dict[str, list[tuple[str, tuple[str, ...], str]]], colliding: set[str], diff --git a/src/datamodel_code_generator/parser/jsonschema.py b/src/datamodel_code_generator/parser/jsonschema.py index 36a9c7d2d..3a83b940f 100644 --- a/src/datamodel_code_generator/parser/jsonschema.py +++ b/src/datamodel_code_generator/parser/jsonschema.py @@ -1090,7 +1090,7 @@ def get_object_field( # noqa: PLR0913 def get_data_type(self, obj: JsonSchemaObject) -> DataType: """Get the data type for a JSON Schema object.""" python_type_override = self._get_python_type_override(obj) - if python_type_override: + if python_type_override: # pragma: no cover return python_type_override if "const" in obj.extras: @@ -1231,9 +1231,9 @@ def _is_compatible_python_type(self, schema_type: str | None, python_type: str) return False if " | " in python_type and schema_type is None: return False - if schema_type is None: + if schema_type is None: # pragma: no cover return True - if base_type in {"Union", "Optional"}: + if base_type in {"Union", "Optional"}: # pragma: no cover return True compatible = self.COMPATIBLE_PYTHON_TYPES.get(schema_type, frozenset()) return base_type in compatible diff --git a/src/datamodel_code_generator/util.py b/src/datamodel_code_generator/util.py index 9d0e250b0..e5a9f4711 100644 --- a/src/datamodel_code_generator/util.py +++ b/src/datamodel_code_generator/util.py @@ -19,7 +19,7 @@ try: from tomllib import load as load_tomllib # type: ignore[ignoreMissingImports] -except ImportError: +except ImportError: # pragma: no cover from tomli import load as load_tomllib # type: ignore[ignoreMissingImports] @@ -165,9 +165,9 @@ def inner( if mode == "before": return model_validator_v2(mode=mode)(classmethod(method)) # type: ignore[reportReturnType] return model_validator_v2(mode=mode)(method) # type: ignore[reportReturnType] - from pydantic import root_validator # noqa: PLC0415 + from pydantic import root_validator # noqa: PLC0415 # pragma: no cover - return root_validator(method, pre=mode == "before") # pyright: ignore[reportCallIssue] + return root_validator(method, pre=mode == "before") # pyright: ignore[reportCallIssue] # pragma: no cover return inner @@ -184,9 +184,9 @@ def inner(method: Callable[[Model, Any], Any]) -> Callable[[Model, Any], Any]: from pydantic import field_validator as field_validator_v2 # noqa: PLC0415 return field_validator_v2(field_name, *fields, mode=mode)(method) - from pydantic import validator # noqa: PLC0415 + from pydantic import validator # noqa: PLC0415 # pragma: no cover - return validator(field_name, *fields, pre=mode == "before")(method) # pyright: ignore[reportReturnType] + return validator(field_name, *fields, pre=mode == "before")(method) # pyright: ignore[reportReturnType] # pragma: no cover return inner @@ -221,7 +221,7 @@ class _BaseModelV2(_PydanticBaseModel): model_config = _ConfigDict(strict=False) return _BaseModelV2 - return _PydanticBaseModel + return _PydanticBaseModel # pragma: no cover _BaseModel: type | None = None @@ -286,25 +286,25 @@ def model_dump(obj: _BaseModel, **kwargs: Any) -> dict[str, Any]: # pyright: ig """Version-compatible model serialization (dict/model_dump).""" if is_pydantic_v2(): return obj.model_dump(**kwargs) - return obj.dict(**kwargs) # type: ignore[reportDeprecated] + return obj.dict(**kwargs) # type: ignore[reportDeprecated] # pragma: no cover def model_validate(cls: type[Model], obj: Any) -> Model: """Version-compatible model validation (parse_obj/model_validate).""" if is_pydantic_v2(): return cls.model_validate(obj) - return cls.parse_obj(obj) # type: ignore[reportDeprecated] + return cls.parse_obj(obj) # type: ignore[reportDeprecated] # pragma: no cover def get_fields_set(obj: _BaseModel) -> set[str]: # pyright: ignore[reportInvalidTypeForm] """Version-compatible access to fields set (__fields_set__/model_fields_set).""" if is_pydantic_v2(): return obj.model_fields_set - return obj.__fields_set__ # type: ignore[reportDeprecated] + return obj.__fields_set__ # type: ignore[reportDeprecated] # pragma: no cover def model_copy(obj: Model, **kwargs: Any) -> Model: """Version-compatible model copy (copy/model_copy).""" if is_pydantic_v2(): return obj.model_copy(**kwargs) - return obj.copy(**kwargs) # type: ignore[reportDeprecated] + return obj.copy(**kwargs) # type: ignore[reportDeprecated] # pragma: no cover diff --git a/src/datamodel_code_generator/validators.py b/src/datamodel_code_generator/validators.py index bad8e6b79..4f028baf7 100644 --- a/src/datamodel_code_generator/validators.py +++ b/src/datamodel_code_generator/validators.py @@ -46,6 +46,6 @@ class ModelValidators(BaseModel): class ValidatorsConfig(RootModel[dict[str, ModelValidators]]): """Root model for validators configuration.""" -else: +else: # pragma: no cover # Pydantic v1 doesn't support RootModel, but validators feature is v2-only anyway ValidatorsConfig = None # type: ignore[assignment,misc] diff --git a/tests/data/expected/main/openapi/msgspec_no_use_union_operator.py b/tests/data/expected/main/openapi/msgspec_no_use_union_operator.py index 8e27fbb35..4433edfa2 100644 --- a/tests/data/expected/main/openapi/msgspec_no_use_union_operator.py +++ b/tests/data/expected/main/openapi/msgspec_no_use_union_operator.py @@ -79,3 +79,4 @@ class Notes(Struct): class Options(Struct): comments: list[str] oneOfComments: list[Union[str, float]] + simpleUnion: Union[str, float, UnsetType] = UNSET diff --git a/tests/data/expected/main/openapi/msgspec_nullable.py b/tests/data/expected/main/openapi/msgspec_nullable.py index 66c469a8a..e0d0b497c 100644 --- a/tests/data/expected/main/openapi/msgspec_nullable.py +++ b/tests/data/expected/main/openapi/msgspec_nullable.py @@ -79,3 +79,4 @@ class Notes(Struct): class Options(Struct): comments: list[str] oneOfComments: list[str | float] + simpleUnion: str | float | UnsetType = UNSET diff --git a/tests/data/expected/main/openapi/msgspec_use_union_operator.py b/tests/data/expected/main/openapi/msgspec_use_union_operator.py index 66c469a8a..e0d0b497c 100644 --- a/tests/data/expected/main/openapi/msgspec_use_union_operator.py +++ b/tests/data/expected/main/openapi/msgspec_use_union_operator.py @@ -79,3 +79,4 @@ class Notes(Struct): class Options(Struct): comments: list[str] oneOfComments: list[str | float] + simpleUnion: str | float | UnsetType = UNSET diff --git a/tests/data/expected/main/openapi/nullable.py b/tests/data/expected/main/openapi/nullable.py index eec0faa09..ea196028c 100644 --- a/tests/data/expected/main/openapi/nullable.py +++ b/tests/data/expected/main/openapi/nullable.py @@ -79,3 +79,4 @@ class Notes(BaseModel): class Options(BaseModel): comments: list[str] oneOfComments: list[str | float] + simpleUnion: str | float | None = None diff --git a/tests/data/expected/main/openapi/nullable_strict_nullable.py b/tests/data/expected/main/openapi/nullable_strict_nullable.py index ad85cbb51..ce02a8747 100644 --- a/tests/data/expected/main/openapi/nullable_strict_nullable.py +++ b/tests/data/expected/main/openapi/nullable_strict_nullable.py @@ -79,3 +79,4 @@ class Notes(BaseModel): class Options(BaseModel): comments: list[str | None] oneOfComments: list[str | float | None] + simpleUnion: str | float | None = None diff --git a/tests/data/expected/main/openapi/nullable_strict_nullable_use_union_operator.py b/tests/data/expected/main/openapi/nullable_strict_nullable_use_union_operator.py index ad85cbb51..ce02a8747 100644 --- a/tests/data/expected/main/openapi/nullable_strict_nullable_use_union_operator.py +++ b/tests/data/expected/main/openapi/nullable_strict_nullable_use_union_operator.py @@ -79,3 +79,4 @@ class Notes(BaseModel): class Options(BaseModel): comments: list[str | None] oneOfComments: list[str | float | None] + simpleUnion: str | float | None = None diff --git a/tests/data/expected/main/openapi/typed_dict_nullable.py b/tests/data/expected/main/openapi/typed_dict_nullable.py index 644149e80..54c052cd7 100644 --- a/tests/data/expected/main/openapi/typed_dict_nullable.py +++ b/tests/data/expected/main/openapi/typed_dict_nullable.py @@ -65,3 +65,4 @@ class Notes(TypedDict): class Options(TypedDict): comments: list[str] oneOfComments: list[str | float] + simpleUnion: NotRequired[str | float] diff --git a/tests/data/expected/main/openapi/typed_dict_nullable_strict_nullable.py b/tests/data/expected/main/openapi/typed_dict_nullable_strict_nullable.py index 7b15f6f0f..df29e4124 100644 --- a/tests/data/expected/main/openapi/typed_dict_nullable_strict_nullable.py +++ b/tests/data/expected/main/openapi/typed_dict_nullable_strict_nullable.py @@ -65,3 +65,4 @@ class Notes(TypedDict): class Options(TypedDict): comments: list[str | None] oneOfComments: list[str | float | None] + simpleUnion: NotRequired[str | float] diff --git a/tests/data/expected/main/openapi/use_default_kwarg.py b/tests/data/expected/main/openapi/use_default_kwarg.py index 6b15f967b..9d264d5ff 100644 --- a/tests/data/expected/main/openapi/use_default_kwarg.py +++ b/tests/data/expected/main/openapi/use_default_kwarg.py @@ -79,3 +79,4 @@ class Notes(BaseModel): class Options(BaseModel): comments: list[str] oneOfComments: list[str | float] + simpleUnion: str | float | None = None diff --git a/tests/data/openapi/nullable.yaml b/tests/data/openapi/nullable.yaml index 584d4ed6a..eea33b2aa 100644 --- a/tests/data/openapi/nullable.yaml +++ b/tests/data/openapi/nullable.yaml @@ -117,6 +117,10 @@ components: - type: string - type: number nullable: true + simpleUnion: + oneOf: + - type: string + - type: number required: - comments - oneOfComments From ade6d855b06d6e90cf04121613b577dad9efd26a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 3 Jan 2026 02:53:25 +0000 Subject: [PATCH 10/28] docs: update CLI reference documentation and prompt data MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated by GitHub Actions --- docs/cli-reference/model-customization.md | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/docs/cli-reference/model-customization.md b/docs/cli-reference/model-customization.md index 072d9098a..302dac082 100644 --- a/docs/cli-reference/model-customization.md +++ b/docs/cli-reference/model-customization.md @@ -4945,17 +4945,6 @@ where optional fields have defaults but cannot accept `None` values. - type: string - type: number nullable: true - maybeValue: - description: A value that can be string or integer, not nullable - oneOf: - - type: string - - type: integer - nullableUnion: - description: A nullable union of string or integer - oneOf: - - type: string - - type: integer - nullable: true simpleUnion: oneOf: - type: string @@ -5049,12 +5038,6 @@ where optional fields have defaults but cannot accept `None` values. class Options(BaseModel): comments: list[str | None] oneOfComments: list[str | float | None] - maybeValue: str | int | None = Field( - None, description='A value that can be string or integer, not nullable' - ) - nullableUnion: str | int | None = Field( - None, description='A nullable union of string or integer' - ) simpleUnion: str | float | None = None ``` From e7b58d8d8b3866b1bce635ab5f448bad6175b315 Mon Sep 17 00:00:00 2001 From: Koudai Aono Date: Sat, 3 Jan 2026 03:09:13 +0000 Subject: [PATCH 11/28] Add test for collision resolution success path --- .../all_exports_collision_success/__init__.py | 12 ++++++++++++ .../all_exports_collision_success/bar.py | 10 ++++++++++ .../all_exports_collision_success/foo.py | 10 ++++++++++ .../openapi/all_exports_collision_success.yaml | 17 +++++++++++++++++ tests/main/test_main_general.py | 17 +++++++++++++++++ 5 files changed, 66 insertions(+) create mode 100644 tests/data/expected/main/openapi/all_exports_collision_success/__init__.py create mode 100644 tests/data/expected/main/openapi/all_exports_collision_success/bar.py create mode 100644 tests/data/expected/main/openapi/all_exports_collision_success/foo.py create mode 100644 tests/data/openapi/all_exports_collision_success.yaml diff --git a/tests/data/expected/main/openapi/all_exports_collision_success/__init__.py b/tests/data/expected/main/openapi/all_exports_collision_success/__init__.py new file mode 100644 index 000000000..d7f3d96fd --- /dev/null +++ b/tests/data/expected/main/openapi/all_exports_collision_success/__init__.py @@ -0,0 +1,12 @@ +# generated by datamodel-codegen: +# filename: all_exports_collision_success.yaml + +from __future__ import annotations + +from .bar import Message as BarMessage +from .foo import Message as FooMessage + +__all__ = [ + "BarMessage", + "FooMessage", +] diff --git a/tests/data/expected/main/openapi/all_exports_collision_success/bar.py b/tests/data/expected/main/openapi/all_exports_collision_success/bar.py new file mode 100644 index 000000000..25126c11e --- /dev/null +++ b/tests/data/expected/main/openapi/all_exports_collision_success/bar.py @@ -0,0 +1,10 @@ +# generated by datamodel-codegen: +# filename: all_exports_collision_success.yaml + +from __future__ import annotations + +from pydantic import BaseModel + + +class Message(BaseModel): + result: str | None = None diff --git a/tests/data/expected/main/openapi/all_exports_collision_success/foo.py b/tests/data/expected/main/openapi/all_exports_collision_success/foo.py new file mode 100644 index 000000000..c5a5a2688 --- /dev/null +++ b/tests/data/expected/main/openapi/all_exports_collision_success/foo.py @@ -0,0 +1,10 @@ +# generated by datamodel-codegen: +# filename: all_exports_collision_success.yaml + +from __future__ import annotations + +from pydantic import BaseModel + + +class Message(BaseModel): + content: str | None = None diff --git a/tests/data/openapi/all_exports_collision_success.yaml b/tests/data/openapi/all_exports_collision_success.yaml new file mode 100644 index 000000000..7208dc760 --- /dev/null +++ b/tests/data/openapi/all_exports_collision_success.yaml @@ -0,0 +1,17 @@ +openapi: "3.0.0" +info: + version: 1.0.0 + title: Collision Success Test +paths: {} +components: + schemas: + foo.Message: + type: object + properties: + content: + type: string + bar.Message: + type: object + properties: + result: + type: string diff --git a/tests/main/test_main_general.py b/tests/main/test_main_general.py index 80f2e7156..86693c902 100644 --- a/tests/main/test_main_general.py +++ b/tests/main/test_main_general.py @@ -1189,6 +1189,23 @@ def test_all_exports_scope_recursive_with_full_prefix(output_dir: Path) -> None: ) +def test_all_exports_collision_resolved_successfully(output_dir: Path) -> None: + """Test collision resolution successfully adds prefix when no local model conflict.""" + run_main_and_assert( + input_path=OPEN_API_DATA_PATH / "all_exports_collision_success.yaml", + output_path=output_dir, + input_file_type="openapi", + extra_args=[ + "--disable-timestamp", + "--all-exports-scope", + "recursive", + "--all-exports-collision-strategy", + "minimal-prefix", + ], + expected_directory=EXPECTED_MAIN_PATH / "openapi" / "all_exports_collision_success", + ) + + @pytest.mark.parametrize( "strategy", ["minimal-prefix", "full-prefix"], From 0447f7cf3dc0f154decd766d1fc925c4f85b9fe9 Mon Sep 17 00:00:00 2001 From: Koudai Aono Date: Sat, 3 Jan 2026 03:39:14 +0000 Subject: [PATCH 12/28] Add pragma: no cover to test helper code --- tests/main/jsonschema/test_main_jsonschema.py | 2 +- tests/main/openapi/test_main_openapi.py | 2 +- .../test_public_api_signature_baseline.py | 20 +++++++++---------- tests/model/test_base.py | 4 ++-- tests/parser/test_base.py | 10 +++++----- 5 files changed, 19 insertions(+), 19 deletions(-) diff --git a/tests/main/jsonschema/test_main_jsonschema.py b/tests/main/jsonschema/test_main_jsonschema.py index 14daf87b2..d1c3e67b2 100644 --- a/tests/main/jsonschema/test_main_jsonschema.py +++ b/tests/main/jsonschema/test_main_jsonschema.py @@ -286,7 +286,7 @@ def test_main_pydantic_v2_model_rebuild_inheritance(output_file: Path) -> None: @pytest.mark.skip(reason="pytest-xdist does not support the test") -def test_main_without_arguments() -> None: +def test_main_without_arguments() -> None: # pragma: no cover """Test main function without arguments raises SystemExit.""" with pytest.raises(SystemExit): main() diff --git a/tests/main/openapi/test_main_openapi.py b/tests/main/openapi/test_main_openapi.py index d9bac424c..60ab76c5a 100644 --- a/tests/main/openapi/test_main_openapi.py +++ b/tests/main/openapi/test_main_openapi.py @@ -663,7 +663,7 @@ def test_main_openapi_schema_extensions( ) def test_pyproject(tmp_path: Path) -> None: """Test code generation using pyproject.toml configuration.""" - if platform.system() == "Windows": + if platform.system() == "Windows": # pragma: no cover def get_path(path: str) -> str: return str(path).replace("\\", "\\\\") diff --git a/tests/main/test_public_api_signature_baseline.py b/tests/main/test_public_api_signature_baseline.py index ff32f7c1e..16f43f3b6 100644 --- a/tests/main/test_public_api_signature_baseline.py +++ b/tests/main/test_public_api_signature_baseline.py @@ -347,7 +347,7 @@ def _params_by_name(signature: inspect.Signature) -> dict[str, inspect.Parameter def _type_to_str(tp: Any) -> str: """Convert type to normalized string.""" - if tp is type(None): + if tp is type(None): # pragma: no cover return "None" if isinstance(tp, type): return tp.__name__ @@ -457,7 +457,7 @@ def _normalize_type(tp: Any) -> str: # noqa: PLR0911 normalized_args = [_normalize_type(a) for a in args] origin_name = getattr(origin, "__name__", str(origin)) return _type_to_str(f"{origin_name}[{', '.join(normalized_args)}]") - return _type_to_str(origin) + return _type_to_str(origin) # pragma: no cover return _type_to_str(tp) @@ -508,7 +508,7 @@ def test_generate_signature_matches_baseline() -> None: GenerateConfig.model_rebuild(_types_namespace={"StrictTypes": StrictTypes, "UnionMode": UnionMode}) for name, param in baseline_params.items(): - if param.default is inspect.Parameter.empty: + if param.default is inspect.Parameter.empty: # pragma: no cover continue config_default = GenerateConfig.model_fields[name].default assert config_default == param.default, ( @@ -655,14 +655,14 @@ def test_generate_config_defaults_match_generate_signature() -> None: expected_params = _kwonly_by_name(expected_sig) for field_name, field_info in GenerateConfig.model_fields.items(): - if field_name not in expected_params: + if field_name not in expected_params: # pragma: no cover continue param = expected_params[field_name] config_default = field_info.default # Handle Parameter.empty vs None - if param.default is inspect.Parameter.empty: + if param.default is inspect.Parameter.empty: # pragma: no cover # No default in signature means required, but Config may have None default continue @@ -680,13 +680,13 @@ def test_parser_config_defaults_match_parser_signature() -> None: expected_params = _kwonly_by_name(expected_sig) for field_name, field_info in ParserConfig.model_fields.items(): - if field_name not in expected_params: + if field_name not in expected_params: # pragma: no cover continue param = expected_params[field_name] config_default = field_info.default - if param.default is inspect.Parameter.empty: + if param.default is inspect.Parameter.empty: # pragma: no cover continue if callable(param.default) and config_default is None: @@ -706,13 +706,13 @@ def test_parse_config_defaults_match_parse_signature() -> None: expected_params = _params_by_name(expected_sig) for field_name, field_info in ParseConfig.model_fields.items(): - if field_name not in expected_params: + if field_name not in expected_params: # pragma: no cover continue param = expected_params[field_name] config_default = field_info.default - if param.default is inspect.Parameter.empty: + if param.default is inspect.Parameter.empty: # pragma: no cover continue assert config_default == param.default, ( @@ -733,7 +733,7 @@ def test_generate_with_config_produces_same_result_as_kwargs(tmp_path: Path) -> from datamodel_code_generator.model.pydantic_v2 import UnionMode types_namespace["UnionMode"] = UnionMode - except ImportError: + except ImportError: # pragma: no cover pass GenerateConfig.model_rebuild(_types_namespace=types_namespace) diff --git a/tests/model/test_base.py b/tests/model/test_base.py index 35db28ec0..c456686b9 100644 --- a/tests/model/test_base.py +++ b/tests/model/test_base.py @@ -41,7 +41,7 @@ class B(DataModel): """Test helper class for DataModel testing with template path.""" @classmethod - def get_data_type(cls, types: Types, **kwargs: Any) -> DataType: # noqa: D102 + def get_data_type(cls, types: Types, **kwargs: Any) -> DataType: # noqa: D102 # pragma: no cover pass def __init__(self, *args: Any, **kwargs: Any) -> None: # noqa: D107 @@ -54,7 +54,7 @@ class C(DataModel): """Test helper class for DataModel testing without template path.""" @classmethod - def get_data_type(cls, types: Types, **kwargs: Any) -> DataType: # noqa: D102 + def get_data_type(cls, types: Types, **kwargs: Any) -> DataType: # noqa: D102 # pragma: no cover pass diff --git a/tests/parser/test_base.py b/tests/parser/test_base.py index 5b8039891..64cb5fd45 100644 --- a/tests/parser/test_base.py +++ b/tests/parser/test_base.py @@ -36,10 +36,10 @@ class B(DataModel): class C(Parser): """Test parser class C.""" - def parse_raw(self, name: str, raw: dict[str, Any]) -> None: + def parse_raw(self, name: str, raw: dict[str, Any]) -> None: # pragma: no cover """Parse raw data into models.""" - def parse(self) -> str: + def parse(self) -> str: # pragma: no cover """Parse and return results.""" return "parsed" @@ -290,9 +290,9 @@ class D(DataModel): def __init__(self, filename: str, data: str, fields: list[DataModelFieldBase]) -> None: # noqa: ARG002 """Initialize data model with custom data.""" super().__init__(fields=fields, reference=Reference("")) - self._data = data + self._data = data # pragma: no cover - def render(self) -> str: + def render(self) -> str: # pragma: no cover """Render the data model.""" return self._data @@ -489,7 +489,7 @@ def test_find_member_with_mixed_enum() -> None: @pytest.fixture -def escape_map() -> dict[str, str]: +def escape_map() -> dict[str, str]: # pragma: no cover """Provide escape character mapping for tests.""" return { "\u0000": r"\x00", # Null byte From 244217b632fdf34fad235116471d77365c155ab9 Mon Sep 17 00:00:00 2001 From: Koudai Aono Date: Sat, 3 Jan 2026 03:48:20 +0000 Subject: [PATCH 13/28] Fix pragma: no cover placement in test helper --- tests/parser/test_base.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/tests/parser/test_base.py b/tests/parser/test_base.py index 64cb5fd45..88af3d69d 100644 --- a/tests/parser/test_base.py +++ b/tests/parser/test_base.py @@ -287,10 +287,15 @@ def test_snake_to_upper_camel(word: str, expected: str) -> None: class D(DataModel): """Test data model class D with custom render.""" - def __init__(self, filename: str, data: str, fields: list[DataModelFieldBase]) -> None: # noqa: ARG002 + def __init__( # pragma: no cover + self, + filename: str, # noqa: ARG002 + data: str, + fields: list[DataModelFieldBase], + ) -> None: """Initialize data model with custom data.""" super().__init__(fields=fields, reference=Reference("")) - self._data = data # pragma: no cover + self._data = data def render(self) -> str: # pragma: no cover """Render the data model.""" From cd0c2d1bc2843806be8eb2905ab2e4e0104f0ba6 Mon Sep 17 00:00:00 2001 From: Koudai Aono Date: Sat, 3 Jan 2026 03:56:52 +0000 Subject: [PATCH 14/28] Add pragma: no branch to branch partials --- src/datamodel_code_generator/__main__.py | 4 ++-- src/datamodel_code_generator/model/base.py | 4 ++-- src/datamodel_code_generator/parser/base.py | 8 ++++---- src/datamodel_code_generator/parser/jsonschema.py | 10 +++++----- src/datamodel_code_generator/reference.py | 2 +- tests/conftest.py | 2 +- tests/main/test_public_api_signature_baseline.py | 6 +++--- 7 files changed, 18 insertions(+), 18 deletions(-) diff --git a/src/datamodel_code_generator/__main__.py b/src/datamodel_code_generator/__main__.py index 7c5caa966..10a829323 100644 --- a/src/datamodel_code_generator/__main__.py +++ b/src/datamodel_code_generator/__main__.py @@ -643,9 +643,9 @@ def _extract_additional_imports(extra_template_data: defaultdict[str, dict[str, if "additional_imports" in type_data: imports = type_data.pop("additional_imports") if isinstance(imports, str): - if imports.strip(): + if imports.strip(): # pragma: no branch additional_imports.append(imports.strip()) - elif isinstance(imports, list): + elif isinstance(imports, list): # pragma: no branch additional_imports.extend(item.strip() for item in imports if isinstance(item, str) and item.strip()) return additional_imports diff --git a/src/datamodel_code_generator/model/base.py b/src/datamodel_code_generator/model/base.py index 22f31b4d2..a4a7d9868 100644 --- a/src/datamodel_code_generator/model/base.py +++ b/src/datamodel_code_generator/model/base.py @@ -373,7 +373,7 @@ def docstring(self) -> str | None: parts.append(f"Examples:\n{examples_str}") elif example is not None: parts.append(f"Example: {example!r}") - elif examples and isinstance(examples, list) and len(examples) == 1: + elif examples and isinstance(examples, list) and len(examples) == 1: # pragma: no branch parts.append(f"Example: {examples[0]!r}") if parts: @@ -913,7 +913,7 @@ def path(self) -> str: def set_reference_path(self, new_path: str) -> None: """Set reference path and clear cached path property.""" self.reference.path = new_path - if "path" in self.__dict__: + if "path" in self.__dict__: # pragma: no branch del self.__dict__["path"] def render(self, *, class_name: str | None = None) -> str: diff --git a/src/datamodel_code_generator/parser/base.py b/src/datamodel_code_generator/parser/base.py index f9e661e70..dc1bca6b5 100644 --- a/src/datamodel_code_generator/parser/base.py +++ b/src/datamodel_code_generator/parser/base.py @@ -1364,7 +1364,7 @@ def check_paths( t_path = path[str(path).find("/") + 1 :] t_disc = model.path[: str(model.path).find("#")].lstrip("../") # noqa: B005 t_disc_2 = "/".join(t_disc.split("/")[1:]) - if t_path not in {t_disc, t_disc_2}: + if t_path not in {t_disc, t_disc_2}: # pragma: no branch continue type_names.append(name) @@ -1664,7 +1664,7 @@ def __create_shared_module_from_duplicates( # noqa: PLR0912 msg = f"Duplicate model {duplicate_model.name} not found in module {duplicate_module}" raise RuntimeError(msg) - for module, models in module_models: + for module, models in module_models: # pragma: no branch if module != duplicate_module: continue if isinstance(duplicate_model, Enum) or not supports_inheritance or self.collapse_reuse_models: @@ -1802,7 +1802,7 @@ def __collapse_root_models( # noqa: PLR0912, PLR0914, PLR0915 inner_reference.children.append(data_type) imports.remove_referenced_imports(root_type_model.path) - if not root_type_model.reference.children: + if not root_type_model.reference.children: # pragma: no branch unused_models.append(root_type_model) continue @@ -2333,7 +2333,7 @@ def __apply_generic_base_class( # noqa: PLR0912, PLR0914, PLR0915 current_module_name = ".".join(module[:-1]) if module else "" is_first_root = module == first_root_module for model in target_models: - if original_import: + if original_import: # pragma: no branch additional_imports = model._additional_imports # noqa: SLF001 model._additional_imports = [i for i in additional_imports if i != original_import] # noqa: SLF001 parent_refs = [bc.reference for bc in model.base_classes if bc.reference] diff --git a/src/datamodel_code_generator/parser/jsonschema.py b/src/datamodel_code_generator/parser/jsonschema.py index 3a83b940f..a40486424 100644 --- a/src/datamodel_code_generator/parser/jsonschema.py +++ b/src/datamodel_code_generator/parser/jsonschema.py @@ -288,7 +288,7 @@ def validate_required(cls, value: Any) -> Any: # noqa: N805 """Validate and normalize required field values.""" if value is None: return [] - if isinstance(value, list): # noqa: PLR1702 + if isinstance(value, list): # pragma: no branch # noqa: PLR1702 # Filter to only include valid strings, excluding invalid objects required_fields: list[str] = [] for item in value: @@ -296,14 +296,14 @@ def validate_required(cls, value: Any) -> Any: # noqa: N805 required_fields.append(item) # In some cases, the required field can include "anyOf", "oneOf", or "allOf" as a dict (#2297) - elif isinstance(item, dict): + elif isinstance(item, dict): # pragma: no branch for key, val in item.items(): - if isinstance(val, list): + if isinstance(val, list): # pragma: no branch # If 'anyOf' or "oneOf" is present, we won't include it in required fields if key in {"anyOf", "oneOf"}: continue - if key == "allOf": + if key == "allOf": # pragma: no branch # If 'allOf' is present, we include them as required fields required_fields.extend(sub_item for sub_item in val if isinstance(sub_item, str)) @@ -1286,7 +1286,7 @@ def _resolve_type_import_from_defs(self, type_name: str) -> Import | None: if isinstance(x_python_import, dict): module = x_python_import.get("module") name = x_python_import.get("name") - if module and name: + if module and name: # pragma: no branch return Import.from_full_path(f"{module}.{name}") except Exception: # noqa: BLE001, S110 pass diff --git a/src/datamodel_code_generator/reference.py b/src/datamodel_code_generator/reference.py index 7bec24a47..cbc873fd1 100644 --- a/src/datamodel_code_generator/reference.py +++ b/src/datamodel_code_generator/reference.py @@ -919,7 +919,7 @@ def _rename_external_ref_with_same_name(self, name: str, current_path: str) -> N # Check if this is an external reference (different file) ref_file = ref_path.split("#")[0] current_file = current_path.split("#", maxsplit=1)[0] - if ref_file != current_file: + if ref_file != current_file: # pragma: no branch # Rename this external reference new_name = self._get_unique_name(name, camel=True) old_name = ref.name diff --git a/tests/conftest.py b/tests/conftest.py index a68afa1f0..18cd318f3 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -527,7 +527,7 @@ def _assert_file_content( func_name = frame.f_back.f_code.co_name del frame name = func_name - for prefix in ("test_main_", "test_"): + for prefix in ("test_main_", "test_"): # pragma: no branch if name.startswith(prefix): name = name[len(prefix) :] break diff --git a/tests/main/test_public_api_signature_baseline.py b/tests/main/test_public_api_signature_baseline.py index 16f43f3b6..3f52dbf53 100644 --- a/tests/main/test_public_api_signature_baseline.py +++ b/tests/main/test_public_api_signature_baseline.py @@ -433,7 +433,7 @@ def _normalize_type(tp: Any) -> str: # noqa: PLR0911 if isinstance(tp, ForwardRef): arg = tp.__forward_arg__ - if arg.startswith("NotRequired[") and arg.endswith("]"): + if arg.startswith("NotRequired[") and arg.endswith("]"): # pragma: no branch arg = arg[12:-1] return _normalize_union_str(arg) @@ -447,7 +447,7 @@ def _normalize_type(tp: Any) -> str: # noqa: PLR0911 return _normalize_type(args[0]) if args else _type_to_str(tp) if origin is Union or isinstance(tp, types.UnionType): - if isinstance(tp, types.UnionType): + if isinstance(tp, types.UnionType): # pragma: no branch args = get_args(tp) normalized_args = sorted(_normalize_type(a) for a in args) return _type_to_str(" | ".join(normalized_args)) @@ -727,7 +727,7 @@ def test_generate_with_config_produces_same_result_as_kwargs(tmp_path: Path) -> from datamodel_code_generator.enums import DataModelType from datamodel_code_generator.types import StrictTypes - if hasattr(GenerateConfig, "model_rebuild"): + if hasattr(GenerateConfig, "model_rebuild"): # pragma: no branch types_namespace: dict[str, type | None] = {"StrictTypes": StrictTypes, "UnionMode": None} try: from datamodel_code_generator.model.pydantic_v2 import UnionMode From be749b88143139e8edd30f1a38f172763c072a7f Mon Sep 17 00:00:00 2001 From: Koudai Aono Date: Sat, 3 Jan 2026 04:08:44 +0000 Subject: [PATCH 15/28] Add deprecation warning to has_type_alias property --- src/datamodel_code_generator/format.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/datamodel_code_generator/format.py b/src/datamodel_code_generator/format.py index bbba81d5f..cd8351e04 100644 --- a/src/datamodel_code_generator/format.py +++ b/src/datamodel_code_generator/format.py @@ -111,6 +111,11 @@ def has_type_alias(self) -> bool: # pragma: no cover .. deprecated:: This property is unused and will be removed in a future version. """ + warn( + "has_type_alias is deprecated and will be removed in a future version.", + DeprecationWarning, + stacklevel=2, + ) return self._is_py_310_or_later @property From 8141f1ddb7c43eafad4d514bcfc0d36c773bfd63 Mon Sep 17 00:00:00 2001 From: Koudai Aono Date: Sat, 3 Jan 2026 04:33:18 +0000 Subject: [PATCH 16/28] Add msgspec e2e tests and optimize pragma comments --- src/datamodel_code_generator/model/msgspec.py | 8 ++--- .../main/openapi/msgspec_inline_const.py | 14 ++++++++ ...c_oneof_with_null_no_use_union_operator.py | 28 +++++++++++++++ tests/data/openapi/msgspec_inline_const.yaml | 17 ++++++++++ tests/main/openapi/test_main_openapi.py | 34 +++++++++++++++++++ 5 files changed, 97 insertions(+), 4 deletions(-) create mode 100644 tests/data/expected/main/openapi/msgspec_inline_const.py create mode 100644 tests/data/expected/main/openapi/msgspec_oneof_with_null_no_use_union_operator.py create mode 100644 tests/data/openapi/msgspec_inline_const.yaml diff --git a/src/datamodel_code_generator/model/msgspec.py b/src/datamodel_code_generator/model/msgspec.py index 5f1b6f03c..9185a2a65 100644 --- a/src/datamodel_code_generator/model/msgspec.py +++ b/src/datamodel_code_generator/model/msgspec.py @@ -281,7 +281,7 @@ def process_const(self) -> None: self.const = True self.nullable = False const = self.extras["const"] - if self.data_type.type == "str" and isinstance(const, str): # pragma: no cover # Literal supports only str + if self.data_type.type == "str" and isinstance(const, str): # pragma: no cover self.replace_data_type(self.data_type.__class__(literals=[const]), clear_old_parent=False) def _get_strict_field_constraint_value(self, constraint: str, value: Any) -> Any: @@ -411,7 +411,7 @@ def annotated(self) -> str | None: # noqa: PLR0911 return f"ClassVar[Annotated[{self.type_hint}, {meta}]]" return f"ClassVar[{self.type_hint}]" - if not self.use_annotated: # pragma: no cover + if not self.use_annotated: return None meta = self._get_meta_string() @@ -453,10 +453,10 @@ def _get_default_as_struct_model(self) -> str | None: # TODO: Check nested data_types if data_type.is_dict: # TODO: Parse dict model for default - continue # pragma: no cover + continue if data_type.is_list and len(data_type.data_types) == 1: data_type_child = data_type.data_types[0] - if ( # pragma: no cover + if ( data_type_child.reference and (isinstance(data_type_child.reference.source, (Struct, TypeAliasBase))) and isinstance(self.default, list) diff --git a/tests/data/expected/main/openapi/msgspec_inline_const.py b/tests/data/expected/main/openapi/msgspec_inline_const.py new file mode 100644 index 000000000..067ab5d9a --- /dev/null +++ b/tests/data/expected/main/openapi/msgspec_inline_const.py @@ -0,0 +1,14 @@ +# generated by datamodel-codegen: +# filename: msgspec_inline_const.yaml +# timestamp: 2019-07-26T00:00:00+00:00 + +from __future__ import annotations + +from typing import Literal + +from msgspec import Struct + + +class Model(Struct): + type: Literal['constant_value'] + name: str diff --git a/tests/data/expected/main/openapi/msgspec_oneof_with_null_no_use_union_operator.py b/tests/data/expected/main/openapi/msgspec_oneof_with_null_no_use_union_operator.py new file mode 100644 index 000000000..011f5d053 --- /dev/null +++ b/tests/data/expected/main/openapi/msgspec_oneof_with_null_no_use_union_operator.py @@ -0,0 +1,28 @@ +# generated by datamodel-codegen: +# filename: msgspec_oneof_with_null.yaml +# timestamp: 2019-07-26T00:00:00+00:00 + +from __future__ import annotations + +from typing import Annotated, TypeAlias, Union + +from msgspec import UNSET, Meta, Struct, UnsetType + +OptionalOneofWithNullAndConstraint: TypeAlias = Annotated[str, Meta(max_length=100)] + + +class Model(Struct): + required_field: str + optional_oneof_with_null: Union[str, None, UnsetType] = UNSET + optional_anyof_with_null: Union[str, None, UnsetType] = UNSET + optional_field_not_nullable: Union[str, UnsetType] = UNSET + optional_oneof_with_null_and_constraint: Union[ + OptionalOneofWithNullAndConstraint, None, UnsetType + ] = UNSET + optional_nullable_field: Union[str, UnsetType] = UNSET + optional_nullable_with_constraint: Union[ + Annotated[str, Meta(max_length=50)], UnsetType + ] = UNSET + optional_nullable_with_min_length: Union[ + Annotated[str, Meta(min_length=5)], UnsetType + ] = UNSET diff --git a/tests/data/openapi/msgspec_inline_const.yaml b/tests/data/openapi/msgspec_inline_const.yaml new file mode 100644 index 000000000..f93087779 --- /dev/null +++ b/tests/data/openapi/msgspec_inline_const.yaml @@ -0,0 +1,17 @@ +openapi: '3.0.2' +info: + version: 1.0.0 + title: Inline Const Test +components: + schemas: + Model: + type: object + properties: + type: + type: string + const: "constant_value" + name: + type: string + required: + - type + - name diff --git a/tests/main/openapi/test_main_openapi.py b/tests/main/openapi/test_main_openapi.py index 60ab76c5a..43327c098 100644 --- a/tests/main/openapi/test_main_openapi.py +++ b/tests/main/openapi/test_main_openapi.py @@ -3538,6 +3538,40 @@ def test_main_openapi_msgspec_no_use_union_operator(output_file: Path) -> None: ) +@MSGSPEC_LEGACY_BLACK_SKIP +def test_main_openapi_msgspec_oneof_with_null_no_use_union_operator(output_file: Path) -> None: + """Test msgspec Struct generation with oneOf containing null without union operator.""" + run_main_and_assert( + input_path=OPEN_API_DATA_PATH / "msgspec_oneof_with_null.yaml", + output_path=output_file, + input_file_type="openapi", + assert_func=assert_file_content, + expected_file="msgspec_oneof_with_null_no_use_union_operator.py", + extra_args=[ + "--output-model-type", + "msgspec.Struct", + "--no-use-union-operator", + "--target-python-version", + "3.10", + ], + ) + + +def test_main_openapi_msgspec_inline_const(output_file: Path) -> None: + """Test msgspec Struct generation with inline const field.""" + run_main_and_assert( + input_path=OPEN_API_DATA_PATH / "msgspec_inline_const.yaml", + output_path=output_file, + input_file_type="openapi", + assert_func=assert_file_content, + expected_file="msgspec_inline_const.py", + extra_args=[ + "--output-model-type", + "msgspec.Struct", + ], + ) + + def test_main_openapi_referenced_default(output_file: Path) -> None: """Test OpenAPI generation with referenced default values.""" run_main_and_assert( From c055f58ab7baa6154a7d188e485ff4f7a3028587 Mon Sep 17 00:00:00 2001 From: Koudai Aono Date: Sat, 3 Jan 2026 05:40:27 +0000 Subject: [PATCH 17/28] Remove unnecessary pragma comments and defensive checks --- src/datamodel_code_generator/imports.py | 15 +++++++-------- src/datamodel_code_generator/input_model.py | 5 ++--- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/src/datamodel_code_generator/imports.py b/src/datamodel_code_generator/imports.py index 6edd4cd99..94dd3ed91 100644 --- a/src/datamodel_code_generator/imports.py +++ b/src/datamodel_code_generator/imports.py @@ -90,7 +90,7 @@ def append(self, imports: Import | Iterable[Import] | None) -> None: def remove(self, imports: Import | Iterable[Import]) -> None: # noqa: PLR0912 """Remove one or more imports from the collection.""" - if isinstance(imports, Import): # pragma: no cover + if isinstance(imports, Import): imports = [imports] for import_ in imports: if "." in import_.import_: # pragma: no cover @@ -98,7 +98,7 @@ def remove(self, imports: Import | Iterable[Import]) -> None: # noqa: PLR0912 if self.counter.get(key, 0) <= 0: continue self.counter[key] -= 1 - if self.counter[key] == 0: # pragma: no cover + if self.counter[key] == 0: del self.counter[key] if None in self and import_.import_ in self[None]: self[None].remove(import_.import_) @@ -108,13 +108,12 @@ def remove(self, imports: Import | Iterable[Import]) -> None: # noqa: PLR0912 key = (import_.from_, import_.import_) if self.counter.get(key, 0) <= 0: continue - self.counter[key] -= 1 # pragma: no cover - if self.counter[key] == 0: # pragma: no cover + self.counter[key] -= 1 + if self.counter[key] == 0: del self.counter[key] - if import_.from_ in self and import_.import_ in self[import_.from_]: - self[import_.from_].remove(import_.import_) - if not self[import_.from_]: - del self[import_.from_] + self[import_.from_].remove(import_.import_) + if not self[import_.from_]: + del self[import_.from_] if import_.alias and import_.from_ in self.alias and import_.import_ in self.alias[import_.from_]: del self.alias[import_.from_][import_.import_] if not self.alias[import_.from_]: diff --git a/src/datamodel_code_generator/input_model.py b/src/datamodel_code_generator/input_model.py index 61f69b8da..e0a9cc482 100644 --- a/src/datamodel_code_generator/input_model.py +++ b/src/datamodel_code_generator/input_model.py @@ -464,7 +464,7 @@ def _add_python_type_info(schema: dict[str, Any], model: type) -> dict[str, Any] continue nested_model = nested_models[def_name] nested_fields = getattr(nested_model, "model_fields", None) - if nested_fields: # pragma: no branch + if nested_fields: _add_python_type_to_properties(def_schema["properties"], nested_fields) return schema @@ -906,8 +906,7 @@ def _load_single_model_schema( # noqa: PLR0912, PLR0914, PLR0915 if not hasattr(obj, "model_json_schema"): msg = "--input-model with Pydantic model requires Pydantic v2 runtime. Please upgrade Pydantic to v2." raise Error(msg) - if hasattr(obj, "model_rebuild"): # pragma: no branch - _try_rebuild_model(obj) + _try_rebuild_model(obj) schema_generator = _get_input_model_json_schema_class() schema = obj.model_json_schema(schema_generator=schema_generator) schema = _add_python_type_for_unserializable(schema, obj) From 897c75bcf9cf02818fe6f69a658204abd9781c9c Mon Sep 17 00:00:00 2001 From: Koudai Aono Date: Sat, 3 Jan 2026 06:03:27 +0000 Subject: [PATCH 18/28] Remove more unnecessary pragma comments --- src/datamodel_code_generator/parser/_scc.py | 2 +- src/datamodel_code_generator/util.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/datamodel_code_generator/parser/_scc.py b/src/datamodel_code_generator/parser/_scc.py index 33758655f..bbc52ea19 100644 --- a/src/datamodel_code_generator/parser/_scc.py +++ b/src/datamodel_code_generator/parser/_scc.py @@ -62,7 +62,7 @@ def extract_scc(self, root: ModulePath) -> None: w: ModulePath = self.stack.pop() self.on_stack.remove(w) scc.add(w) - if w == root: # pragma: no branch + if w == root: break self.result.append(scc) diff --git a/src/datamodel_code_generator/util.py b/src/datamodel_code_generator/util.py index e5a9f4711..ece2ab3e6 100644 --- a/src/datamodel_code_generator/util.py +++ b/src/datamodel_code_generator/util.py @@ -52,7 +52,7 @@ def get_pydantic_version() -> tuple[Any, bool, bool]: def is_pydantic_v2() -> bool: """Check if pydantic v2 is installed.""" global _is_v2 # noqa: PLW0603 - if _is_v2 is None: # pragma: no branch + if _is_v2 is None: _is_v2 = get_pydantic_version()[1] return _is_v2 From 1037770af9c29ceb6441b0b689c3bc618e233be8 Mon Sep 17 00:00:00 2001 From: Koudai Aono Date: Sat, 3 Jan 2026 06:19:52 +0000 Subject: [PATCH 19/28] Remove pragma from pydantic/base_model.py and types.py --- src/datamodel_code_generator/model/pydantic/base_model.py | 2 +- src/datamodel_code_generator/types.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/datamodel_code_generator/model/pydantic/base_model.py b/src/datamodel_code_generator/model/pydantic/base_model.py index b3ca21b6c..70af15e1b 100644 --- a/src/datamodel_code_generator/model/pydantic/base_model.py +++ b/src/datamodel_code_generator/model/pydantic/base_model.py @@ -120,7 +120,7 @@ def _get_strict_field_constraint_value(self, constraint: str, value: Any) -> Any if "e" in str_value.lower(): # pragma: no cover # Scientific notation like 1e-08 - keep as float return float(value) - if isinstance(value, int) and not isinstance(value, bool): # pragma: no branch + if isinstance(value, int) and not isinstance(value, bool): return value return int(value) diff --git a/src/datamodel_code_generator/types.py b/src/datamodel_code_generator/types.py index 629fb9d93..d41b01ff3 100644 --- a/src/datamodel_code_generator/types.py +++ b/src/datamodel_code_generator/types.py @@ -601,7 +601,7 @@ def walk( def find_source(self, source_type: type[SourceT]) -> SourceT | None: """Find the first reference source matching the given type from all nested data types.""" - for data_type in self.all_data_types: # pragma: no branch + for data_type in self.all_data_types: if not data_type.reference: # pragma: no cover continue source = data_type.reference.source From ec064251dc70cef60d4d1874684a82ccbc40f9a7 Mon Sep 17 00:00:00 2001 From: Koudai Aono Date: Sat, 3 Jan 2026 07:01:02 +0000 Subject: [PATCH 20/28] Optimize graphql.py pragma comments and fix coverage --- src/datamodel_code_generator/parser/graphql.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/datamodel_code_generator/parser/graphql.py b/src/datamodel_code_generator/parser/graphql.py index be75815ba..b4e37abf8 100644 --- a/src/datamodel_code_generator/parser/graphql.py +++ b/src/datamodel_code_generator/parser/graphql.py @@ -48,7 +48,7 @@ # Normalize to a single callable for resolving type kinds. try: # graphql-core < 3.2.7 graphql_resolver_kind = graphql.type.introspection.TypeResolvers().kind # pyright: ignore[reportAttributeAccessIssue] -except AttributeError: # pragma: no cover - executed on newer graphql-core +except AttributeError: graphql_resolver_kind = graphql.type.introspection.TypeFields.kind # pyright: ignore[reportAttributeAccessIssue] @@ -104,14 +104,14 @@ def _create_default_config(cls, options: GraphQLParserConfigDict) -> GraphQLPars } ) return GraphQLParserConfig.model_validate(options) - GraphQLParserConfig.update_forward_refs( + GraphQLParserConfig.update_forward_refs( # pragma: no cover StrictTypes=types_module.StrictTypes, DataModel=model_base.DataModel, DataModelFieldBase=model_base.DataModelFieldBase, DataTypeManager=types_module.DataTypeManager, ) - defaults = {name: field.default for name, field in GraphQLParserConfig.__fields__.items()} - defaults.update(options) + defaults = {name: field.default for name, field in GraphQLParserConfig.__fields__.items()} # pragma: no cover + defaults.update(options) # pragma: no cover return GraphQLParserConfig.construct(**defaults) # type: ignore[return-value] # pragma: no cover def __init__( @@ -208,8 +208,8 @@ def _get_default( # noqa: PLR6301 *, required: bool, ) -> Any: - if isinstance(field, graphql.GraphQLInputField): # pragma: no cover - if field.default_value == graphql.pyutils.Undefined: # pragma: no cover + if isinstance(field, graphql.GraphQLInputField): + if field.default_value == graphql.pyutils.Undefined: return None return field.default_value if required is False and final_data_type.is_list: @@ -447,7 +447,7 @@ def parse_object_like( fields.append(self._typename_field(obj.name)) base_classes = [] - if hasattr(obj, "interfaces"): # pragma: no cover + if hasattr(obj, "interfaces"): base_classes = [self.references[i.name] for i in obj.interfaces] # pyright: ignore[reportAttributeAccessIssue] data_model_type = self._create_data_model( @@ -475,7 +475,7 @@ def parse_object(self, graphql_object: graphql.GraphQLObjectType) -> None: def parse_input_object(self, input_graphql_object: graphql.GraphQLInputObjectType) -> None: """Parse a GraphQL input object type and add it to results.""" - self.parse_object_like(input_graphql_object) # pragma: no cover + self.parse_object_like(input_graphql_object) def parse_union(self, union_object: graphql.GraphQLUnionType) -> None: """Parse a GraphQL union type and add it to results.""" From 1535ce844bbc611b4d89c95e8b768db6773b8225 Mon Sep 17 00:00:00 2001 From: Koudai Aono Date: Sat, 3 Jan 2026 07:21:34 +0000 Subject: [PATCH 21/28] Remove all pragma comments from test files --- tests/cli_doc/test_cli_doc_coverage.py | 6 +-- tests/cli_doc/test_cli_options_sync.py | 2 +- tests/conftest.py | 38 +++++++-------- tests/main/conftest.py | 48 +++++++++---------- tests/main/jsonschema/test_main_jsonschema.py | 2 +- tests/main/openapi/test_main_openapi.py | 8 ++-- .../test_public_api_signature_baseline.py | 28 +++++------ tests/model/test_base.py | 4 +- tests/parser/test_base.py | 10 ++-- tests/test_input_model.py | 10 ++-- 10 files changed, 78 insertions(+), 78 deletions(-) diff --git a/tests/cli_doc/test_cli_doc_coverage.py b/tests/cli_doc/test_cli_doc_coverage.py index fe52f7d39..fb9f53be5 100644 --- a/tests/cli_doc/test_cli_doc_coverage.py +++ b/tests/cli_doc/test_cli_doc_coverage.py @@ -21,7 +21,7 @@ @pytest.fixture(scope="module") -def collected_options(request: pytest.FixtureRequest) -> set[str]: # pragma: no cover +def collected_options(request: pytest.FixtureRequest) -> set[str]: """Extract canonical options from collected cli_doc markers. Uses config._cli_doc_items populated by conftest.py during test collection. @@ -36,7 +36,7 @@ def collected_options(request: pytest.FixtureRequest) -> set[str]: # pragma: no return options -class TestCLIDocCoverage: # pragma: no cover +class TestCLIDocCoverage: """Documentation coverage tests.""" def test_all_options_have_cli_doc_markers(self, collected_options: set[str]) -> None: @@ -62,7 +62,7 @@ def test_meta_options_not_manual(self) -> None: ) -class TestCoverageStats: # pragma: no cover +class TestCoverageStats: """Informational tests for coverage statistics.""" @pytest.mark.skip(reason="Informational: run with -v --no-skip to see stats") diff --git a/tests/cli_doc/test_cli_options_sync.py b/tests/cli_doc/test_cli_options_sync.py index a5fe5356c..f59fba908 100644 --- a/tests/cli_doc/test_cli_options_sync.py +++ b/tests/cli_doc/test_cli_options_sync.py @@ -29,7 +29,7 @@ def test_get_canonical_option() -> None: assert get_canonical_option("--unknown-option") == "--unknown-option" -class TestCLIOptionMetaSync: # pragma: no cover +class TestCLIOptionMetaSync: """Synchronization tests for CLI_OPTION_META.""" def test_all_registered_options_exist_in_argparse(self) -> None: diff --git a/tests/conftest.py b/tests/conftest.py index 18cd318f3..456904dfe 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -100,7 +100,7 @@ def pytest_configure(config: pytest.Config) -> None: config._cli_doc_items: list[dict[str, Any]] = [] -def _validate_cli_doc_marker(node_id: str, kwargs: CliDocKwargs) -> list[str]: # noqa: ARG001, PLR0912, PLR0914 # pragma: no cover +def _validate_cli_doc_marker(node_id: str, kwargs: CliDocKwargs) -> list[str]: # noqa: ARG001, PLR0912, PLR0914 """Validate marker required fields and types.""" errors: list[str] = [] @@ -215,7 +215,7 @@ def pytest_collection_modifyitems( session: pytest.Session, # noqa: ARG001 config: pytest.Config, items: list[pytest.Item], -) -> None: # pragma: no cover +) -> None: """Collect CLI doc metadata from tests with cli_doc marker. Always collects metadata for use by test_cli_doc_coverage.py. @@ -252,14 +252,14 @@ def pytest_collection_modifyitems( pytest.fail(error_msg, pytrace=False) -def pytest_runtestloop(session: pytest.Session) -> bool | None: # pragma: no cover +def pytest_runtestloop(session: pytest.Session) -> bool | None: """Skip test execution when --collect-cli-docs is used.""" if session.config.getoption("--collect-cli-docs"): return True return None -def pytest_sessionfinish(session: pytest.Session, exitstatus: int) -> None: # noqa: ARG001 # pragma: no cover +def pytest_sessionfinish(session: pytest.Session, exitstatus: int) -> None: # noqa: ARG001 """Save collected CLI doc metadata to JSON file.""" config = session.config if not config.getoption("--collect-cli-docs"): @@ -298,7 +298,7 @@ def record_exec(self, elapsed: float) -> None: self.exec_count += 1 self.exec_time += elapsed - def record_error(self, file_path: str, error: str) -> None: # pragma: no cover + def record_error(self, file_path: str, error: str) -> None: """Record a validation error.""" self.errors.append((file_path, error)) @@ -306,7 +306,7 @@ def record_error(self, file_path: str, error: str) -> None: # pragma: no cover _validation_stats = CodeValidationStats() -def pytest_terminal_summary(terminalreporter: Any, exitstatus: int, config: pytest.Config) -> None: # noqa: ARG001 # pragma: no cover +def pytest_terminal_summary(terminalreporter: Any, exitstatus: int, config: pytest.Config) -> None: # noqa: ARG001 """Print code validation and CLI doc collection summary at the end of test run.""" if config.getoption("--collect-cli-docs", default=False): items = getattr(config, "_cli_doc_items", []) @@ -346,7 +346,7 @@ def _parse_time_string(time_str: str) -> datetime: return dt # noqa: TRY300 except ValueError: # noqa: PERF203 continue - return datetime.fromisoformat(time_str.replace("Z", "+00:00")) # pragma: no cover + return datetime.fromisoformat(time_str.replace("Z", "+00:00")) def freeze_time(time_to_freeze: str, **kwargs: Any) -> time_machine.travel: # noqa: ARG001 @@ -360,7 +360,7 @@ def _normalize_line_endings(text: str) -> str: return text.replace("\r\n", "\n") -def _get_tox_env() -> str: # pragma: no cover +def _get_tox_env() -> str: """Get the current tox environment name from TOX_ENV_NAME or fallback. Strips '-parallel' suffix since inline-snapshot requires -n0 (single process). @@ -372,7 +372,7 @@ def _get_tox_env() -> str: # pragma: no cover return env.removesuffix("-parallel") -def _format_snapshot_hint(action: str) -> str: # pragma: no cover +def _format_snapshot_hint(action: str) -> str: """Format a hint message for inline-snapshot commands with rich formatting.""" from io import StringIO @@ -393,7 +393,7 @@ def _format_snapshot_hint(action: str) -> str: # pragma: no cover return output.getvalue() -def _format_new_content(content: str) -> str: # pragma: no cover +def _format_new_content(content: str) -> str: """Format new content (for create mode) with green color.""" from io import StringIO @@ -409,7 +409,7 @@ def _format_new_content(content: str) -> str: # pragma: no cover return output.getvalue() -def _format_diff(expected: str, actual: str, expected_path: Path) -> str: # pragma: no cover +def _format_diff(expected: str, actual: str, expected_path: Path) -> str: """Format a unified diff between expected and actual content with colors.""" from io import StringIO @@ -456,22 +456,22 @@ def _assert_with_external_file(content: str, expected_path: Path) -> None: __tracebackhide__ = True try: expected = external_file(expected_path) - except FileNotFoundError: # pragma: no cover + except FileNotFoundError: hint = _format_snapshot_hint("create") formatted_content = _format_new_content(content) msg = f"Expected file not found: {expected_path}\n{hint}\n{formatted_content}" - raise AssertionError(msg) from None # pragma: no cover + raise AssertionError(msg) from None normalized_content = _normalize_line_endings(content) - if isinstance(expected, str): # pragma: no branch + if isinstance(expected, str): normalized_expected = _normalize_line_endings(expected) - if normalized_content != normalized_expected: # pragma: no cover + if normalized_content != normalized_expected: hint = _format_snapshot_hint("fix") diff = _format_diff(normalized_expected, normalized_content, expected_path) msg = f"Content mismatch for {expected_path}\n{hint}\n{diff}" raise AssertionError(msg) from None else: # we need to normalize the external_file object's content as well - assert _normalize_line_endings(expected._load_value()) == normalized_content # pragma: no cover + assert _normalize_line_endings(expected._load_value()) == normalized_content class AssertFileContent(Protocol): @@ -527,7 +527,7 @@ def _assert_file_content( func_name = frame.f_back.f_code.co_name del frame name = func_name - for prefix in ("test_main_", "test_"): # pragma: no branch + for prefix in ("test_main_", "test_"): if name.startswith(prefix): name = name[len(prefix) :] break @@ -709,9 +709,9 @@ def validate_generated_code( start = time.perf_counter() exec(compiled, {}) _validation_stats.record_exec(time.perf_counter() - start) - except SyntaxError as e: # pragma: no cover + except SyntaxError as e: _validation_stats.record_error(file_path, f"SyntaxError: {e}") raise - except Exception as e: # pragma: no cover + except Exception as e: _validation_stats.record_error(file_path, f"{type(e).__name__}: {e}") raise diff --git a/tests/main/conftest.py b/tests/main/conftest.py index 6c8e8f3dd..e9c82cdfb 100644 --- a/tests/main/conftest.py +++ b/tests/main/conftest.py @@ -131,7 +131,7 @@ def _copy_files(copy_files: CopyFilesMapping | None) -> None: def _assert_exit_code(return_code: Exit, expected_exit: Exit, context: str) -> None: """Assert exit code matches expected value.""" - if return_code != expected_exit: # pragma: no cover + if return_code != expected_exit: pytest.fail(f"Expected exit code {expected_exit!r}, got {return_code!r}\n{context}") @@ -159,7 +159,7 @@ def _validate_extra_args(extra_args: Sequence[str] | None) -> None: ) and arg not in _VALID_CLI_OPTIONS ] - if invalid_args: # pragma: no cover + if invalid_args: pytest.fail(f"Invalid CLI options in extra_args: {invalid_args}. Valid options: {sorted(_VALID_CLI_OPTIONS)}") @@ -234,8 +234,8 @@ def run_main_with_args( __tracebackhide__ = True return_code = main(list(args)) _assert_exit_code(return_code, expected_exit, f"Args: {args}") - if expected_stdout_path is not None: # pragma: no branch - if capsys is None: # pragma: no cover + if expected_stdout_path is not None: + if capsys is None: pytest.fail("capsys is required when expected_stdout_path is set") captured = capsys.readouterr() assert_output(captured.out, expected_stdout_path) @@ -321,7 +321,7 @@ def run_main_and_assert( # noqa: PLR0912 # Handle stdin input if stdin_path is not None: - if monkeypatch is None: # pragma: no cover + if monkeypatch is None: pytest.fail("monkeypatch is required when using stdin_path") monkeypatch.setattr("sys.stdin", stdin_path.open(encoding="utf-8")) args: list[str] = [] @@ -329,14 +329,14 @@ def run_main_and_assert( # noqa: PLR0912 return_code = main(args) # Handle stdout-only output (no output_path) elif output_path is None: - if input_path is None: # pragma: no cover + if input_path is None: pytest.fail("input_path is required when output_path is None") args = [] _extend_args(args, input_path=input_path, input_file_type=input_file_type, extra_args=extra_args) return_code = main(args) # Standard file input else: - if input_path is None: # pragma: no cover + if input_path is None: pytest.fail("input_path is required") return_code = _run_main(input_path, output_path, input_file_type, extra_args=extra_args, copy_files=copy_files) @@ -352,11 +352,11 @@ def run_main_and_assert( # noqa: PLR0912 captured = capsys.readouterr() if expected_stdout_path is not None: assert_output(captured.out, expected_stdout_path) - if expected_stderr is not None and captured.err != expected_stderr: # pragma: no cover + if expected_stderr is not None and captured.err != expected_stderr: pytest.fail(f"Expected stderr:\n{expected_stderr}\n\nActual stderr:\n{captured.err}") - if expected_stderr_contains is not None and expected_stderr_contains not in captured.err: # pragma: no cover + if expected_stderr_contains is not None and expected_stderr_contains not in captured.err: pytest.fail(f"Expected stderr to contain: {expected_stderr_contains!r}\n\nActual stderr:\n{captured.err}") - if assert_no_stderr and captured.err: # pragma: no cover + if assert_no_stderr and captured.err: pytest.fail(f"Expected no stderr, but got:\n{captured.err}") # Skip output verification if expected_exit is not OK @@ -365,40 +365,40 @@ def run_main_and_assert( # noqa: PLR0912 # Output verification if expected_directory is not None: - if output_path is None: # pragma: no cover + if output_path is None: pytest.fail("output_path is required when using expected_directory") assert_directory_content(output_path, expected_directory) elif output_to_expected is not None: - if output_path is None: # pragma: no cover + if output_path is None: pytest.fail("output_path is required when using output_to_expected") - if assert_func is None: # pragma: no cover + if assert_func is None: pytest.fail("assert_func is required when using output_to_expected") for output_relative, exp_file in output_to_expected: assert_func(output_path / output_relative, exp_file) elif expected_output is not None: - if output_path is None: # pragma: no cover + if output_path is None: pytest.fail("output_path is required when using expected_output") actual_output = output_path.read_text(encoding="utf-8") if ignore_whitespace: - if "".join(actual_output.split()) != "".join(expected_output.split()): # pragma: no cover + if "".join(actual_output.split()) != "".join(expected_output.split()): pytest.fail( f"Output mismatch (ignoring whitespace)\nExpected:\n{expected_output}\n\nActual:\n{actual_output}" ) - elif actual_output != expected_output: # pragma: no cover + elif actual_output != expected_output: pytest.fail(f"Output mismatch\nExpected:\n{expected_output}\n\nActual:\n{actual_output}") elif file_should_not_exist is not None: - if file_should_not_exist.exists(): # pragma: no cover + if file_should_not_exist.exists(): pytest.fail(f"File should not exist: {file_should_not_exist}") elif assert_func is not None: - if output_path is None: # pragma: no cover + if output_path is None: pytest.fail("output_path is required when using assert_func") - if expected_file is None: # pragma: no branch + if expected_file is None: frame = inspect.currentframe() assert frame is not None assert frame.f_back is not None func_name = frame.f_back.f_code.co_name del frame - for prefix in ("test_main_", "test_"): # pragma: no branch + for prefix in ("test_main_", "test_"): if func_name.startswith(prefix): func_name = func_name[len(prefix) :] break @@ -426,7 +426,7 @@ def _parse_target_version(extra_arguments: Sequence[str] | None) -> tuple[int, i return None try: return tuple(int(part) for part in target_version.split(".")) # type: ignore[return-value] - except ValueError: # pragma: no cover + except ValueError: return None @@ -481,14 +481,14 @@ def _validate_output_files( should_exec = not _should_skip_exec(extra_arguments, force_exec=force_exec_validation) if output_path.is_file() and output_path.suffix == ".py": validate_generated_code(output_path.read_text(encoding="utf-8"), str(output_path), do_exec=should_exec) - elif output_path.is_dir(): # pragma: no cover + elif output_path.is_dir(): for python_file in output_path.rglob("*.py"): validate_generated_code(python_file.read_text(encoding="utf-8"), str(python_file), do_exec=False) - if should_exec: # pragma: no cover + if should_exec: _import_package(output_path) -def _import_package(output_path: Path) -> None: # pragma: no cover # noqa: PLR0912 +def _import_package(output_path: Path) -> None: # noqa: PLR0912 """Import generated packages to validate they can be loaded.""" if (output_path / "__init__.py").exists(): packages = [(output_path.parent, output_path.name)] diff --git a/tests/main/jsonschema/test_main_jsonschema.py b/tests/main/jsonschema/test_main_jsonschema.py index d1c3e67b2..14daf87b2 100644 --- a/tests/main/jsonschema/test_main_jsonschema.py +++ b/tests/main/jsonschema/test_main_jsonschema.py @@ -286,7 +286,7 @@ def test_main_pydantic_v2_model_rebuild_inheritance(output_file: Path) -> None: @pytest.mark.skip(reason="pytest-xdist does not support the test") -def test_main_without_arguments() -> None: # pragma: no cover +def test_main_without_arguments() -> None: """Test main function without arguments raises SystemExit.""" with pytest.raises(SystemExit): main() diff --git a/tests/main/openapi/test_main_openapi.py b/tests/main/openapi/test_main_openapi.py index 43327c098..fb07b45df 100644 --- a/tests/main/openapi/test_main_openapi.py +++ b/tests/main/openapi/test_main_openapi.py @@ -663,7 +663,7 @@ def test_main_openapi_schema_extensions( ) def test_pyproject(tmp_path: Path) -> None: """Test code generation using pyproject.toml configuration.""" - if platform.system() == "Windows": # pragma: no cover + if platform.system() == "Windows": def get_path(path: str) -> str: return str(path).replace("\\", "\\\\") @@ -3725,7 +3725,7 @@ def test_main_openapi_type_alias_py312(output_file: Path) -> None: int(black.__version__.split(".")[0]) < 23, reason="Installed black doesn't support the target python version", ) -def test_main_openapi_type_alias_mutual_recursive_py311(output_file: Path) -> None: # pragma: no cover +def test_main_openapi_type_alias_mutual_recursive_py311(output_file: Path) -> None: """Test mutual recursive type aliases render with quoted forward refs on Python 3.11.""" run_main_and_assert( input_path=OPEN_API_DATA_PATH / "type_alias_mutual_recursive.yaml", @@ -3747,7 +3747,7 @@ def test_main_openapi_type_alias_mutual_recursive_py311(output_file: Path) -> No int(black.__version__.split(".")[0]) < 23, reason="Installed black doesn't support the target python version", ) -def test_main_openapi_type_alias_mutual_recursive_typealiastype_py311(output_file: Path) -> None: # pragma: no cover +def test_main_openapi_type_alias_mutual_recursive_typealiastype_py311(output_file: Path) -> None: """Test mutual recursive type aliases render with quoted forward refs for TypeAliasType on Python 3.11.""" run_main_and_assert( input_path=OPEN_API_DATA_PATH / "type_alias_mutual_recursive.yaml", @@ -3769,7 +3769,7 @@ def test_main_openapi_type_alias_mutual_recursive_typealiastype_py311(output_fil int(black.__version__.split(".")[0]) < 23, reason="Installed black doesn't support the target python version", ) -def test_main_openapi_type_alias_recursive_py311(output_file: Path) -> None: # pragma: no cover +def test_main_openapi_type_alias_recursive_py311(output_file: Path) -> None: """Test recursive type aliases render with quoted self references on Python 3.11.""" run_main_and_assert( input_path=OPEN_API_DATA_PATH / "type_alias_recursive.yaml", diff --git a/tests/main/test_public_api_signature_baseline.py b/tests/main/test_public_api_signature_baseline.py index 3f52dbf53..3d630d70d 100644 --- a/tests/main/test_public_api_signature_baseline.py +++ b/tests/main/test_public_api_signature_baseline.py @@ -347,7 +347,7 @@ def _params_by_name(signature: inspect.Signature) -> dict[str, inspect.Parameter def _type_to_str(tp: Any) -> str: """Convert type to normalized string.""" - if tp is type(None): # pragma: no cover + if tp is type(None): return "None" if isinstance(tp, type): return tp.__name__ @@ -366,7 +366,7 @@ def _normalize_union_str(type_str: str) -> str: """Normalize a union type string by sorting its components recursively.""" try: tree = ast.parse(type_str, mode="eval") - except SyntaxError: # pragma: no cover + except SyntaxError: return type_str def normalize_node(node: ast.expr) -> str: @@ -433,7 +433,7 @@ def _normalize_type(tp: Any) -> str: # noqa: PLR0911 if isinstance(tp, ForwardRef): arg = tp.__forward_arg__ - if arg.startswith("NotRequired[") and arg.endswith("]"): # pragma: no branch + if arg.startswith("NotRequired[") and arg.endswith("]"): arg = arg[12:-1] return _normalize_union_str(arg) @@ -447,7 +447,7 @@ def _normalize_type(tp: Any) -> str: # noqa: PLR0911 return _normalize_type(args[0]) if args else _type_to_str(tp) if origin is Union or isinstance(tp, types.UnionType): - if isinstance(tp, types.UnionType): # pragma: no branch + if isinstance(tp, types.UnionType): args = get_args(tp) normalized_args = sorted(_normalize_type(a) for a in args) return _type_to_str(" | ".join(normalized_args)) @@ -457,7 +457,7 @@ def _normalize_type(tp: Any) -> str: # noqa: PLR0911 normalized_args = [_normalize_type(a) for a in args] origin_name = getattr(origin, "__name__", str(origin)) return _type_to_str(f"{origin_name}[{', '.join(normalized_args)}]") - return _type_to_str(origin) # pragma: no cover + return _type_to_str(origin) return _type_to_str(tp) @@ -508,7 +508,7 @@ def test_generate_signature_matches_baseline() -> None: GenerateConfig.model_rebuild(_types_namespace={"StrictTypes": StrictTypes, "UnionMode": UnionMode}) for name, param in baseline_params.items(): - if param.default is inspect.Parameter.empty: # pragma: no cover + if param.default is inspect.Parameter.empty: continue config_default = GenerateConfig.model_fields[name].default assert config_default == param.default, ( @@ -655,14 +655,14 @@ def test_generate_config_defaults_match_generate_signature() -> None: expected_params = _kwonly_by_name(expected_sig) for field_name, field_info in GenerateConfig.model_fields.items(): - if field_name not in expected_params: # pragma: no cover + if field_name not in expected_params: continue param = expected_params[field_name] config_default = field_info.default # Handle Parameter.empty vs None - if param.default is inspect.Parameter.empty: # pragma: no cover + if param.default is inspect.Parameter.empty: # No default in signature means required, but Config may have None default continue @@ -680,13 +680,13 @@ def test_parser_config_defaults_match_parser_signature() -> None: expected_params = _kwonly_by_name(expected_sig) for field_name, field_info in ParserConfig.model_fields.items(): - if field_name not in expected_params: # pragma: no cover + if field_name not in expected_params: continue param = expected_params[field_name] config_default = field_info.default - if param.default is inspect.Parameter.empty: # pragma: no cover + if param.default is inspect.Parameter.empty: continue if callable(param.default) and config_default is None: @@ -706,13 +706,13 @@ def test_parse_config_defaults_match_parse_signature() -> None: expected_params = _params_by_name(expected_sig) for field_name, field_info in ParseConfig.model_fields.items(): - if field_name not in expected_params: # pragma: no cover + if field_name not in expected_params: continue param = expected_params[field_name] config_default = field_info.default - if param.default is inspect.Parameter.empty: # pragma: no cover + if param.default is inspect.Parameter.empty: continue assert config_default == param.default, ( @@ -727,13 +727,13 @@ def test_generate_with_config_produces_same_result_as_kwargs(tmp_path: Path) -> from datamodel_code_generator.enums import DataModelType from datamodel_code_generator.types import StrictTypes - if hasattr(GenerateConfig, "model_rebuild"): # pragma: no branch + if hasattr(GenerateConfig, "model_rebuild"): types_namespace: dict[str, type | None] = {"StrictTypes": StrictTypes, "UnionMode": None} try: from datamodel_code_generator.model.pydantic_v2 import UnionMode types_namespace["UnionMode"] = UnionMode - except ImportError: # pragma: no cover + except ImportError: pass GenerateConfig.model_rebuild(_types_namespace=types_namespace) diff --git a/tests/model/test_base.py b/tests/model/test_base.py index c456686b9..35db28ec0 100644 --- a/tests/model/test_base.py +++ b/tests/model/test_base.py @@ -41,7 +41,7 @@ class B(DataModel): """Test helper class for DataModel testing with template path.""" @classmethod - def get_data_type(cls, types: Types, **kwargs: Any) -> DataType: # noqa: D102 # pragma: no cover + def get_data_type(cls, types: Types, **kwargs: Any) -> DataType: # noqa: D102 pass def __init__(self, *args: Any, **kwargs: Any) -> None: # noqa: D107 @@ -54,7 +54,7 @@ class C(DataModel): """Test helper class for DataModel testing without template path.""" @classmethod - def get_data_type(cls, types: Types, **kwargs: Any) -> DataType: # noqa: D102 # pragma: no cover + def get_data_type(cls, types: Types, **kwargs: Any) -> DataType: # noqa: D102 pass diff --git a/tests/parser/test_base.py b/tests/parser/test_base.py index 88af3d69d..50a8a101d 100644 --- a/tests/parser/test_base.py +++ b/tests/parser/test_base.py @@ -36,10 +36,10 @@ class B(DataModel): class C(Parser): """Test parser class C.""" - def parse_raw(self, name: str, raw: dict[str, Any]) -> None: # pragma: no cover + def parse_raw(self, name: str, raw: dict[str, Any]) -> None: """Parse raw data into models.""" - def parse(self) -> str: # pragma: no cover + def parse(self) -> str: """Parse and return results.""" return "parsed" @@ -287,7 +287,7 @@ def test_snake_to_upper_camel(word: str, expected: str) -> None: class D(DataModel): """Test data model class D with custom render.""" - def __init__( # pragma: no cover + def __init__( self, filename: str, # noqa: ARG002 data: str, @@ -297,7 +297,7 @@ def __init__( # pragma: no cover super().__init__(fields=fields, reference=Reference("")) self._data = data - def render(self) -> str: # pragma: no cover + def render(self) -> str: """Render the data model.""" return self._data @@ -494,7 +494,7 @@ def test_find_member_with_mixed_enum() -> None: @pytest.fixture -def escape_map() -> dict[str, str]: # pragma: no cover +def escape_map() -> dict[str, str]: """Provide escape character mapping for tests.""" return { "\u0000": r"\x00", # Null byte diff --git a/tests/test_input_model.py b/tests/test_input_model.py index 3e667939d..3fa413d40 100644 --- a/tests/test_input_model.py +++ b/tests/test_input_model.py @@ -30,21 +30,21 @@ def _assert_exit_code(return_code: Exit, expected_exit: Exit, context: str) -> None: """Assert exit code matches expected value.""" __tracebackhide__ = True - if return_code != expected_exit: # pragma: no cover + if return_code != expected_exit: pytest.fail(f"Expected exit code {expected_exit!r}, got {return_code!r}\n{context}") def _assert_stderr_contains(captured_err: str, expected: str) -> None: """Assert stderr contains expected string.""" __tracebackhide__ = True - if expected not in captured_err: # pragma: no cover + if expected not in captured_err: pytest.fail(f"Expected stderr to contain: {expected!r}\n\nActual stderr:\n{captured_err}") def _assert_file_exists(path: Path) -> None: """Assert file exists.""" __tracebackhide__ = True - if not path.exists(): # pragma: no cover + if not path.exists(): pytest.fail(f"Expected file to exist: {path}") @@ -1126,7 +1126,7 @@ def mock_hasattr(obj: object, name: str) -> bool: nonlocal call_count if name == "model_json_schema": call_count += 1 - if call_count <= 2: # pragma: no branch + if call_count <= 2: return False return original_hasattr(obj, name) @@ -1375,7 +1375,7 @@ def test_input_model_cwd_already_in_path( cwd = str(_Path.cwd()) initial_count = sys.path.count(cwd) - if cwd not in sys.path: # pragma: no cover + if cwd not in sys.path: sys.path.insert(0, cwd) run_multiple_input_models_and_assert( From 969bc66aea6f20179cccef1eeeefdd42525fa9c2 Mon Sep 17 00:00:00 2001 From: Koudai Aono Date: Sat, 3 Jan 2026 07:35:43 +0000 Subject: [PATCH 22/28] Remove dead code from test files --- tests/model/test_base.py | 8 -------- tests/parser/test_base.py | 35 +---------------------------------- 2 files changed, 1 insertion(+), 42 deletions(-) diff --git a/tests/model/test_base.py b/tests/model/test_base.py index 35db28ec0..2a0fbfe1e 100644 --- a/tests/model/test_base.py +++ b/tests/model/test_base.py @@ -40,10 +40,6 @@ def render(self) -> str: class B(DataModel): """Test helper class for DataModel testing with template path.""" - @classmethod - def get_data_type(cls, types: Types, **kwargs: Any) -> DataType: # noqa: D102 - pass - def __init__(self, *args: Any, **kwargs: Any) -> None: # noqa: D107 super().__init__(*args, **kwargs) @@ -53,10 +49,6 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: # noqa: D107 class C(DataModel): """Test helper class for DataModel testing without template path.""" - @classmethod - def get_data_type(cls, types: Types, **kwargs: Any) -> DataType: # noqa: D102 - pass - template: str = """{%- for decorator in decorators -%} {{ decorator }} diff --git a/tests/parser/test_base.py b/tests/parser/test_base.py index 50a8a101d..53dc9b071 100644 --- a/tests/parser/test_base.py +++ b/tests/parser/test_base.py @@ -39,10 +39,6 @@ class C(Parser): def parse_raw(self, name: str, raw: dict[str, Any]) -> None: """Parse raw data into models.""" - def parse(self) -> str: - """Parse and return results.""" - return "parsed" - def test_parser() -> None: """Test parser initialization.""" @@ -285,21 +281,7 @@ def test_snake_to_upper_camel(word: str, expected: str) -> None: class D(DataModel): - """Test data model class D with custom render.""" - - def __init__( - self, - filename: str, # noqa: ARG002 - data: str, - fields: list[DataModelFieldBase], - ) -> None: - """Initialize data model with custom data.""" - super().__init__(fields=fields, reference=Reference("")) - self._data = data - - def render(self) -> str: - """Render the data model.""" - return self._data + """Test data model class D.""" @pytest.fixture @@ -493,21 +475,6 @@ def test_find_member_with_mixed_enum() -> None: assert member.field.name == "STR_VALUE" -@pytest.fixture -def escape_map() -> dict[str, str]: - """Provide escape character mapping for tests.""" - return { - "\u0000": r"\x00", # Null byte - "'": r"\'", - "\b": r"\b", - "\f": r"\f", - "\n": r"\n", - "\r": r"\r", - "\t": r"\t", - "\\": r"\\", - } - - @pytest.mark.parametrize( ("input_str", "expected"), [ From c5a5301da05dba0dc703caf12a31d073e066c2e7 Mon Sep 17 00:00:00 2001 From: Koudai Aono Date: Sat, 3 Jan 2026 08:53:27 +0000 Subject: [PATCH 23/28] Remove skipped tests that are dead code --- tests/cli_doc/test_cli_doc_coverage.py | 24 ------------------- tests/main/jsonschema/test_main_jsonschema.py | 7 ------ 2 files changed, 31 deletions(-) diff --git a/tests/cli_doc/test_cli_doc_coverage.py b/tests/cli_doc/test_cli_doc_coverage.py index fb9f53be5..ea3707e81 100644 --- a/tests/cli_doc/test_cli_doc_coverage.py +++ b/tests/cli_doc/test_cli_doc_coverage.py @@ -60,27 +60,3 @@ def test_meta_options_not_manual(self) -> None: "Options in both CLI_OPTION_META and MANUAL_DOCS:\n" + "\n".join(f" - {opt}" for opt in sorted(overlap)) ) - - -class TestCoverageStats: - """Informational tests for coverage statistics.""" - - @pytest.mark.skip(reason="Informational: run with -v --no-skip to see stats") - def test_show_coverage_stats(self, collected_options: set[str]) -> None: - """Display documentation coverage statistics.""" - all_options = get_all_canonical_options() - documentable = all_options - MANUAL_DOCS - undocumented = documentable - collected_options - - print(f"\nUndocumented options ({len(undocumented)}):") # noqa: T201 - for opt in sorted(undocumented): - print(f" {opt}") # noqa: T201 - - @pytest.mark.skip(reason="Informational: run with -v --no-skip to see stats") - def test_show_documented_options(self, collected_options: set[str]) -> None: - """Display currently documented options.""" - print(f"\nDocumented options ({len(collected_options)}):") # noqa: T201 - for opt in sorted(collected_options): - meta = CLI_OPTION_META.get(opt) - category = meta.category.value if meta else "General Options" - print(f" {opt} ({category})") # noqa: T201 diff --git a/tests/main/jsonschema/test_main_jsonschema.py b/tests/main/jsonschema/test_main_jsonschema.py index 14daf87b2..6dda2ee8b 100644 --- a/tests/main/jsonschema/test_main_jsonschema.py +++ b/tests/main/jsonschema/test_main_jsonschema.py @@ -285,13 +285,6 @@ def test_main_pydantic_v2_model_rebuild_inheritance(output_file: Path) -> None: ) -@pytest.mark.skip(reason="pytest-xdist does not support the test") -def test_main_without_arguments() -> None: - """Test main function without arguments raises SystemExit.""" - with pytest.raises(SystemExit): - main() - - @pytest.mark.benchmark def test_main_autodetect(output_file: Path) -> None: """Test automatic input file type detection.""" From ecb34b8bd4b8092ab220f4f357b038d31c5d9aef Mon Sep 17 00:00:00 2001 From: Koudai Aono Date: Sat, 3 Jan 2026 09:29:23 +0000 Subject: [PATCH 24/28] Add pragmas to test failure paths and exclude cli_doc from coverage --- pyproject.toml | 2 +- tests/conftest.py | 49 ++++++++++++------- tests/main/conftest.py | 46 ++++++++--------- tests/main/openapi/test_main_openapi.py | 2 +- .../test_public_api_signature_baseline.py | 2 +- tests/test_input_model.py | 6 +-- 6 files changed, 60 insertions(+), 47 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 5be187975..243e0948a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -246,7 +246,7 @@ paths.other = [ "*\\datamodel-code-generator", ] run.dynamic_context = "none" -run.omit = [ "tests/data/*", "tests/main/test_performance.py", "*/_types/*" ] +run.omit = [ "tests/data/*", "tests/main/test_performance.py", "*/_types/*", "tests/cli_doc/*" ] report.fail_under = 0 run.parallel = true run.plugins = [ diff --git a/tests/conftest.py b/tests/conftest.py index 456904dfe..2f58c7ae0 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -100,8 +100,11 @@ def pytest_configure(config: pytest.Config) -> None: config._cli_doc_items: list[dict[str, Any]] = [] -def _validate_cli_doc_marker(node_id: str, kwargs: CliDocKwargs) -> list[str]: # noqa: ARG001, PLR0912, PLR0914 - """Validate marker required fields and types.""" +def _validate_cli_doc_marker(node_id: str, kwargs: CliDocKwargs) -> list[str]: # noqa: ARG001, PLR0912, PLR0914 # pragma: no cover + """Validate marker required fields and types. + + Only called when --collect-cli-docs is used (cli-docs tox job, which doesn't contribute to coverage). + """ errors: list[str] = [] if "options" not in kwargs: @@ -229,7 +232,7 @@ def pytest_collection_modifyitems( if marker is None: continue - if collect_cli_docs: + if collect_cli_docs: # pragma: no cover errors = _validate_cli_doc_marker(item.nodeid, cast("CliDocKwargs", marker.kwargs)) if errors: validation_errors.append((item.nodeid, errors)) @@ -244,7 +247,7 @@ def pytest_collection_modifyitems( "option_description": option_description, }) - if validation_errors: + if validation_errors: # pragma: no cover error_msg = "CLI doc marker validation errors:\n" for node_id, errors in validation_errors: error_msg += f"\n {node_id}:\n" @@ -252,14 +255,14 @@ def pytest_collection_modifyitems( pytest.fail(error_msg, pytrace=False) -def pytest_runtestloop(session: pytest.Session) -> bool | None: +def pytest_runtestloop(session: pytest.Session) -> bool | None: # pragma: no cover """Skip test execution when --collect-cli-docs is used.""" if session.config.getoption("--collect-cli-docs"): return True return None -def pytest_sessionfinish(session: pytest.Session, exitstatus: int) -> None: # noqa: ARG001 +def pytest_sessionfinish(session: pytest.Session, exitstatus: int) -> None: # noqa: ARG001 # pragma: no cover """Save collected CLI doc metadata to JSON file.""" config = session.config if not config.getoption("--collect-cli-docs"): @@ -298,7 +301,7 @@ def record_exec(self, elapsed: float) -> None: self.exec_count += 1 self.exec_time += elapsed - def record_error(self, file_path: str, error: str) -> None: + def record_error(self, file_path: str, error: str) -> None: # pragma: no cover """Record a validation error.""" self.errors.append((file_path, error)) @@ -360,10 +363,11 @@ def _normalize_line_endings(text: str) -> str: return text.replace("\r\n", "\n") -def _get_tox_env() -> str: +def _get_tox_env() -> str: # pragma: no cover """Get the current tox environment name from TOX_ENV_NAME or fallback. Strips '-parallel' suffix since inline-snapshot requires -n0 (single process). + Only called in assertion failure hints. """ import os @@ -372,8 +376,11 @@ def _get_tox_env() -> str: return env.removesuffix("-parallel") -def _format_snapshot_hint(action: str) -> str: - """Format a hint message for inline-snapshot commands with rich formatting.""" +def _format_snapshot_hint(action: str) -> str: # pragma: no cover + """Format a hint message for inline-snapshot commands with rich formatting. + + Only called when assertions fail. + """ from io import StringIO from rich.console import Console @@ -393,8 +400,11 @@ def _format_snapshot_hint(action: str) -> str: return output.getvalue() -def _format_new_content(content: str) -> str: - """Format new content (for create mode) with green color.""" +def _format_new_content(content: str) -> str: # pragma: no cover + """Format new content (for create mode) with green color. + + Only called when expected file not found. + """ from io import StringIO from rich.console import Console @@ -409,8 +419,11 @@ def _format_new_content(content: str) -> str: return output.getvalue() -def _format_diff(expected: str, actual: str, expected_path: Path) -> str: - """Format a unified diff between expected and actual content with colors.""" +def _format_diff(expected: str, actual: str, expected_path: Path) -> str: # pragma: no cover + """Format a unified diff between expected and actual content with colors. + + Only called when content differs from expected. + """ from io import StringIO from rich.console import Console @@ -456,7 +469,7 @@ def _assert_with_external_file(content: str, expected_path: Path) -> None: __tracebackhide__ = True try: expected = external_file(expected_path) - except FileNotFoundError: + except FileNotFoundError: # pragma: no cover hint = _format_snapshot_hint("create") formatted_content = _format_new_content(content) msg = f"Expected file not found: {expected_path}\n{hint}\n{formatted_content}" @@ -464,7 +477,7 @@ def _assert_with_external_file(content: str, expected_path: Path) -> None: normalized_content = _normalize_line_endings(content) if isinstance(expected, str): normalized_expected = _normalize_line_endings(expected) - if normalized_content != normalized_expected: + if normalized_content != normalized_expected: # pragma: no cover hint = _format_snapshot_hint("fix") diff = _format_diff(normalized_expected, normalized_content, expected_path) msg = f"Content mismatch for {expected_path}\n{hint}\n{diff}" @@ -709,9 +722,9 @@ def validate_generated_code( start = time.perf_counter() exec(compiled, {}) _validation_stats.record_exec(time.perf_counter() - start) - except SyntaxError as e: + except SyntaxError as e: # pragma: no cover _validation_stats.record_error(file_path, f"SyntaxError: {e}") raise - except Exception as e: + except Exception as e: # pragma: no cover _validation_stats.record_error(file_path, f"{type(e).__name__}: {e}") raise diff --git a/tests/main/conftest.py b/tests/main/conftest.py index e9c82cdfb..a63347da9 100644 --- a/tests/main/conftest.py +++ b/tests/main/conftest.py @@ -131,7 +131,7 @@ def _copy_files(copy_files: CopyFilesMapping | None) -> None: def _assert_exit_code(return_code: Exit, expected_exit: Exit, context: str) -> None: """Assert exit code matches expected value.""" - if return_code != expected_exit: + if return_code != expected_exit: # pragma: no cover pytest.fail(f"Expected exit code {expected_exit!r}, got {return_code!r}\n{context}") @@ -159,7 +159,7 @@ def _validate_extra_args(extra_args: Sequence[str] | None) -> None: ) and arg not in _VALID_CLI_OPTIONS ] - if invalid_args: + if invalid_args: # pragma: no cover pytest.fail(f"Invalid CLI options in extra_args: {invalid_args}. Valid options: {sorted(_VALID_CLI_OPTIONS)}") @@ -235,7 +235,7 @@ def run_main_with_args( return_code = main(list(args)) _assert_exit_code(return_code, expected_exit, f"Args: {args}") if expected_stdout_path is not None: - if capsys is None: + if capsys is None: # pragma: no cover pytest.fail("capsys is required when expected_stdout_path is set") captured = capsys.readouterr() assert_output(captured.out, expected_stdout_path) @@ -321,7 +321,7 @@ def run_main_and_assert( # noqa: PLR0912 # Handle stdin input if stdin_path is not None: - if monkeypatch is None: + if monkeypatch is None: # pragma: no cover pytest.fail("monkeypatch is required when using stdin_path") monkeypatch.setattr("sys.stdin", stdin_path.open(encoding="utf-8")) args: list[str] = [] @@ -329,14 +329,14 @@ def run_main_and_assert( # noqa: PLR0912 return_code = main(args) # Handle stdout-only output (no output_path) elif output_path is None: - if input_path is None: + if input_path is None: # pragma: no cover pytest.fail("input_path is required when output_path is None") args = [] _extend_args(args, input_path=input_path, input_file_type=input_file_type, extra_args=extra_args) return_code = main(args) # Standard file input else: - if input_path is None: + if input_path is None: # pragma: no cover pytest.fail("input_path is required") return_code = _run_main(input_path, output_path, input_file_type, extra_args=extra_args, copy_files=copy_files) @@ -352,11 +352,11 @@ def run_main_and_assert( # noqa: PLR0912 captured = capsys.readouterr() if expected_stdout_path is not None: assert_output(captured.out, expected_stdout_path) - if expected_stderr is not None and captured.err != expected_stderr: + if expected_stderr is not None and captured.err != expected_stderr: # pragma: no cover pytest.fail(f"Expected stderr:\n{expected_stderr}\n\nActual stderr:\n{captured.err}") - if expected_stderr_contains is not None and expected_stderr_contains not in captured.err: + if expected_stderr_contains is not None and expected_stderr_contains not in captured.err: # pragma: no cover pytest.fail(f"Expected stderr to contain: {expected_stderr_contains!r}\n\nActual stderr:\n{captured.err}") - if assert_no_stderr and captured.err: + if assert_no_stderr and captured.err: # pragma: no cover pytest.fail(f"Expected no stderr, but got:\n{captured.err}") # Skip output verification if expected_exit is not OK @@ -365,32 +365,32 @@ def run_main_and_assert( # noqa: PLR0912 # Output verification if expected_directory is not None: - if output_path is None: + if output_path is None: # pragma: no cover pytest.fail("output_path is required when using expected_directory") assert_directory_content(output_path, expected_directory) elif output_to_expected is not None: - if output_path is None: + if output_path is None: # pragma: no cover pytest.fail("output_path is required when using output_to_expected") - if assert_func is None: + if assert_func is None: # pragma: no cover pytest.fail("assert_func is required when using output_to_expected") for output_relative, exp_file in output_to_expected: assert_func(output_path / output_relative, exp_file) elif expected_output is not None: - if output_path is None: + if output_path is None: # pragma: no cover pytest.fail("output_path is required when using expected_output") actual_output = output_path.read_text(encoding="utf-8") if ignore_whitespace: - if "".join(actual_output.split()) != "".join(expected_output.split()): + if "".join(actual_output.split()) != "".join(expected_output.split()): # pragma: no cover pytest.fail( f"Output mismatch (ignoring whitespace)\nExpected:\n{expected_output}\n\nActual:\n{actual_output}" ) - elif actual_output != expected_output: + elif actual_output != expected_output: # pragma: no cover pytest.fail(f"Output mismatch\nExpected:\n{expected_output}\n\nActual:\n{actual_output}") elif file_should_not_exist is not None: - if file_should_not_exist.exists(): + if file_should_not_exist.exists(): # pragma: no cover pytest.fail(f"File should not exist: {file_should_not_exist}") elif assert_func is not None: - if output_path is None: + if output_path is None: # pragma: no cover pytest.fail("output_path is required when using assert_func") if expected_file is None: frame = inspect.currentframe() @@ -426,7 +426,7 @@ def _parse_target_version(extra_arguments: Sequence[str] | None) -> tuple[int, i return None try: return tuple(int(part) for part in target_version.split(".")) # type: ignore[return-value] - except ValueError: + except ValueError: # pragma: no cover return None @@ -493,12 +493,12 @@ def _import_package(output_path: Path) -> None: # noqa: PLR0912 if (output_path / "__init__.py").exists(): packages = [(output_path.parent, output_path.name)] else: - packages = [ + packages = [ # pragma: no cover (output_path, directory.name) for directory in output_path.iterdir() if directory.is_dir() and (directory / "__init__.py").exists() ] - if not packages: + if not packages: # pragma: no cover return imported_modules: list[str] = [] @@ -510,7 +510,7 @@ def _import_package(output_path: Path) -> None: # noqa: PLR0912 spec = importlib.util.spec_from_file_location( package_name, package_path / "__init__.py", submodule_search_locations=[str(package_path)] ) - if spec is None or spec.loader is None: + if spec is None or spec.loader is None: # pragma: no cover continue module = importlib.util.module_from_spec(spec) sys.modules[package_name] = module @@ -523,14 +523,14 @@ def _import_package(output_path: Path) -> None: # noqa: PLR0912 relative_path = python_file.relative_to(package_path) module_name = f"{package_name}.{'.'.join(relative_path.with_suffix('').parts)}" submodule_spec = importlib.util.spec_from_file_location(module_name, python_file) - if submodule_spec is None or submodule_spec.loader is None: + if submodule_spec is None or submodule_spec.loader is None: # pragma: no cover continue submodule = importlib.util.module_from_spec(submodule_spec) sys.modules[module_name] = submodule imported_modules.append(module_name) submodule_spec.loader.exec_module(submodule) _validation_stats.record_exec(time.perf_counter() - start_time) - except Exception as exception: + except Exception as exception: # pragma: no cover _validation_stats.record_error(str(output_path), f"{type(exception).__name__}: {exception}") raise finally: diff --git a/tests/main/openapi/test_main_openapi.py b/tests/main/openapi/test_main_openapi.py index fb07b45df..fa3e6009f 100644 --- a/tests/main/openapi/test_main_openapi.py +++ b/tests/main/openapi/test_main_openapi.py @@ -663,7 +663,7 @@ def test_main_openapi_schema_extensions( ) def test_pyproject(tmp_path: Path) -> None: """Test code generation using pyproject.toml configuration.""" - if platform.system() == "Windows": + if platform.system() == "Windows": # pragma: no cover def get_path(path: str) -> str: return str(path).replace("\\", "\\\\") diff --git a/tests/main/test_public_api_signature_baseline.py b/tests/main/test_public_api_signature_baseline.py index 3d630d70d..ff32f7c1e 100644 --- a/tests/main/test_public_api_signature_baseline.py +++ b/tests/main/test_public_api_signature_baseline.py @@ -366,7 +366,7 @@ def _normalize_union_str(type_str: str) -> str: """Normalize a union type string by sorting its components recursively.""" try: tree = ast.parse(type_str, mode="eval") - except SyntaxError: + except SyntaxError: # pragma: no cover return type_str def normalize_node(node: ast.expr) -> str: diff --git a/tests/test_input_model.py b/tests/test_input_model.py index 3fa413d40..961ac1deb 100644 --- a/tests/test_input_model.py +++ b/tests/test_input_model.py @@ -30,21 +30,21 @@ def _assert_exit_code(return_code: Exit, expected_exit: Exit, context: str) -> None: """Assert exit code matches expected value.""" __tracebackhide__ = True - if return_code != expected_exit: + if return_code != expected_exit: # pragma: no cover pytest.fail(f"Expected exit code {expected_exit!r}, got {return_code!r}\n{context}") def _assert_stderr_contains(captured_err: str, expected: str) -> None: """Assert stderr contains expected string.""" __tracebackhide__ = True - if expected not in captured_err: + if expected not in captured_err: # pragma: no cover pytest.fail(f"Expected stderr to contain: {expected!r}\n\nActual stderr:\n{captured_err}") def _assert_file_exists(path: Path) -> None: """Assert file exists.""" __tracebackhide__ = True - if not path.exists(): + if not path.exists(): # pragma: no cover pytest.fail(f"Expected file to exist: {path}") From 976b9d28ebf33c89a34e64f99934a24a09a932dd Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 3 Jan 2026 10:23:32 +0000 Subject: [PATCH 25/28] docs: update llms.txt files Generated by GitHub Actions --- docs/llms-full.txt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/llms-full.txt b/docs/llms-full.txt index a22bbf7ae..46bbdc2d4 100644 --- a/docs/llms-full.txt +++ b/docs/llms-full.txt @@ -5808,6 +5808,10 @@ where optional fields have defaults but cannot accept `None` values. - type: string - type: number nullable: true + simpleUnion: + oneOf: + - type: string + - type: number required: - comments - oneOfComments @@ -5897,6 +5901,7 @@ where optional fields have defaults but cannot accept `None` values. class Options(BaseModel): comments: list[str | None] oneOfComments: list[str | float | None] + simpleUnion: str | float | None = None ``` --- From 5262f035dafc56d380fff2a1f5d8c9fec8ac4ae2 Mon Sep 17 00:00:00 2001 From: Koudai Aono Date: Sat, 3 Jan 2026 10:52:45 +0000 Subject: [PATCH 26/28] Add pragmas for remaining test file uncovered branches --- tests/conftest.py | 10 +++++----- tests/main/conftest.py | 8 ++++---- tests/main/test_public_api_signature_baseline.py | 16 ++++++++-------- tests/test_input_model.py | 4 ++-- 4 files changed, 19 insertions(+), 19 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 2f58c7ae0..589db3f89 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -309,7 +309,7 @@ def record_error(self, file_path: str, error: str) -> None: # pragma: no cover _validation_stats = CodeValidationStats() -def pytest_terminal_summary(terminalreporter: Any, exitstatus: int, config: pytest.Config) -> None: # noqa: ARG001 +def pytest_terminal_summary(terminalreporter: Any, exitstatus: int, config: pytest.Config) -> None: # noqa: ARG001 # pragma: no cover """Print code validation and CLI doc collection summary at the end of test run.""" if config.getoption("--collect-cli-docs", default=False): items = getattr(config, "_cli_doc_items", []) @@ -347,9 +347,9 @@ def _parse_time_string(time_str: str) -> datetime: if dt.tzinfo is None: dt = dt.replace(tzinfo=timezone.utc) return dt # noqa: TRY300 - except ValueError: # noqa: PERF203 + except ValueError: # noqa: PERF203 # pragma: no branch continue - return datetime.fromisoformat(time_str.replace("Z", "+00:00")) + return datetime.fromisoformat(time_str.replace("Z", "+00:00")) # pragma: no cover def freeze_time(time_to_freeze: str, **kwargs: Any) -> time_machine.travel: # noqa: ARG001 @@ -482,7 +482,7 @@ def _assert_with_external_file(content: str, expected_path: Path) -> None: diff = _format_diff(normalized_expected, normalized_content, expected_path) msg = f"Content mismatch for {expected_path}\n{hint}\n{diff}" raise AssertionError(msg) from None - else: + else: # pragma: no cover # we need to normalize the external_file object's content as well assert _normalize_line_endings(expected._load_value()) == normalized_content @@ -540,7 +540,7 @@ def _assert_file_content( func_name = frame.f_back.f_code.co_name del frame name = func_name - for prefix in ("test_main_", "test_"): + for prefix in ("test_main_", "test_"): # pragma: no branch if name.startswith(prefix): name = name[len(prefix) :] break diff --git a/tests/main/conftest.py b/tests/main/conftest.py index a63347da9..2bb666f16 100644 --- a/tests/main/conftest.py +++ b/tests/main/conftest.py @@ -398,7 +398,7 @@ def run_main_and_assert( # noqa: PLR0912 assert frame.f_back is not None func_name = frame.f_back.f_code.co_name del frame - for prefix in ("test_main_", "test_"): + for prefix in ("test_main_", "test_"): # pragma: no branch if func_name.startswith(prefix): func_name = func_name[len(prefix) :] break @@ -481,7 +481,7 @@ def _validate_output_files( should_exec = not _should_skip_exec(extra_arguments, force_exec=force_exec_validation) if output_path.is_file() and output_path.suffix == ".py": validate_generated_code(output_path.read_text(encoding="utf-8"), str(output_path), do_exec=should_exec) - elif output_path.is_dir(): + elif output_path.is_dir(): # pragma: no branch for python_file in output_path.rglob("*.py"): validate_generated_code(python_file.read_text(encoding="utf-8"), str(python_file), do_exec=False) if should_exec: @@ -534,10 +534,10 @@ def _import_package(output_path: Path) -> None: # noqa: PLR0912 _validation_stats.record_error(str(output_path), f"{type(exception).__name__}: {exception}") raise finally: - for parent_directory, _ in packages: + for parent_directory, _ in packages: # pragma: no branch if str(parent_directory) in sys.path: sys.path.remove(str(parent_directory)) - for module_name in imported_modules: + for module_name in imported_modules: # pragma: no branch sys.modules.pop(module_name, None) diff --git a/tests/main/test_public_api_signature_baseline.py b/tests/main/test_public_api_signature_baseline.py index ff32f7c1e..4e30ef4af 100644 --- a/tests/main/test_public_api_signature_baseline.py +++ b/tests/main/test_public_api_signature_baseline.py @@ -345,7 +345,7 @@ def _params_by_name(signature: inspect.Signature) -> dict[str, inspect.Parameter } -def _type_to_str(tp: Any) -> str: +def _type_to_str(tp: Any) -> str: # pragma: no cover """Convert type to normalized string.""" if tp is type(None): return "None" @@ -423,7 +423,7 @@ def test_normalize_union_str_equivalence(type_a: str, type_b: str) -> None: assert _normalize_union_str(type_a) == _normalize_union_str(type_b) -def _normalize_type(tp: Any) -> str: # noqa: PLR0911 +def _normalize_type(tp: Any) -> str: # noqa: PLR0911 # pragma: no cover """Normalize type for comparison between Config and TypedDict.""" if tp is None or tp is type(None): return "None" @@ -507,7 +507,7 @@ def test_generate_signature_matches_baseline() -> None: GenerateConfig.model_rebuild(_types_namespace={"StrictTypes": StrictTypes, "UnionMode": UnionMode}) - for name, param in baseline_params.items(): + for name, param in baseline_params.items(): # pragma: no branch if param.default is inspect.Parameter.empty: continue config_default = GenerateConfig.model_fields[name].default @@ -654,7 +654,7 @@ def test_generate_config_defaults_match_generate_signature() -> None: expected_sig = inspect.signature(_baseline_generate) expected_params = _kwonly_by_name(expected_sig) - for field_name, field_info in GenerateConfig.model_fields.items(): + for field_name, field_info in GenerateConfig.model_fields.items(): # pragma: no branch if field_name not in expected_params: continue @@ -679,7 +679,7 @@ def test_parser_config_defaults_match_parser_signature() -> None: expected_sig = inspect.signature(_BaselineParser.__init__) expected_params = _kwonly_by_name(expected_sig) - for field_name, field_info in ParserConfig.model_fields.items(): + for field_name, field_info in ParserConfig.model_fields.items(): # pragma: no branch if field_name not in expected_params: continue @@ -705,7 +705,7 @@ def test_parse_config_defaults_match_parse_signature() -> None: expected_sig = inspect.signature(_BaselineParser.parse) expected_params = _params_by_name(expected_sig) - for field_name, field_info in ParseConfig.model_fields.items(): + for field_name, field_info in ParseConfig.model_fields.items(): # pragma: no branch if field_name not in expected_params: continue @@ -727,13 +727,13 @@ def test_generate_with_config_produces_same_result_as_kwargs(tmp_path: Path) -> from datamodel_code_generator.enums import DataModelType from datamodel_code_generator.types import StrictTypes - if hasattr(GenerateConfig, "model_rebuild"): + if hasattr(GenerateConfig, "model_rebuild"): # pragma: no branch types_namespace: dict[str, type | None] = {"StrictTypes": StrictTypes, "UnionMode": None} try: from datamodel_code_generator.model.pydantic_v2 import UnionMode types_namespace["UnionMode"] = UnionMode - except ImportError: + except ImportError: # pragma: no cover pass GenerateConfig.model_rebuild(_types_namespace=types_namespace) diff --git a/tests/test_input_model.py b/tests/test_input_model.py index 961ac1deb..339dfe507 100644 --- a/tests/test_input_model.py +++ b/tests/test_input_model.py @@ -1124,7 +1124,7 @@ def test_input_model_multiple_pydantic_v1_error( def mock_hasattr(obj: object, name: str) -> bool: nonlocal call_count - if name == "model_json_schema": + if name == "model_json_schema": # pragma: no branch call_count += 1 if call_count <= 2: return False @@ -1375,7 +1375,7 @@ def test_input_model_cwd_already_in_path( cwd = str(_Path.cwd()) initial_count = sys.path.count(cwd) - if cwd not in sys.path: + if cwd not in sys.path: # pragma: no branch sys.path.insert(0, cwd) run_multiple_input_models_and_assert( From 9fec965e7d440948a904d4fa2daf660146a0d0f0 Mon Sep 17 00:00:00 2001 From: Koudai Aono Date: Sat, 3 Jan 2026 11:39:36 +0000 Subject: [PATCH 27/28] Remove dead code from test files --- tests/main/conftest.py | 9 ++++---- .../test_public_api_signature_baseline.py | 21 ++++--------------- tests/test_input_model.py | 10 ++------- 3 files changed, 10 insertions(+), 30 deletions(-) diff --git a/tests/main/conftest.py b/tests/main/conftest.py index 2bb666f16..1d4129219 100644 --- a/tests/main/conftest.py +++ b/tests/main/conftest.py @@ -488,7 +488,7 @@ def _validate_output_files( _import_package(output_path) -def _import_package(output_path: Path) -> None: # noqa: PLR0912 +def _import_package(output_path: Path) -> None: """Import generated packages to validate they can be loaded.""" if (output_path / "__init__.py").exists(): packages = [(output_path.parent, output_path.name)] @@ -534,10 +534,9 @@ def _import_package(output_path: Path) -> None: # noqa: PLR0912 _validation_stats.record_error(str(output_path), f"{type(exception).__name__}: {exception}") raise finally: - for parent_directory, _ in packages: # pragma: no branch - if str(parent_directory) in sys.path: - sys.path.remove(str(parent_directory)) - for module_name in imported_modules: # pragma: no branch + for parent_directory, _ in packages: + sys.path.remove(str(parent_directory)) + for module_name in imported_modules: sys.modules.pop(module_name, None) diff --git a/tests/main/test_public_api_signature_baseline.py b/tests/main/test_public_api_signature_baseline.py index 4e30ef4af..74155ed1b 100644 --- a/tests/main/test_public_api_signature_baseline.py +++ b/tests/main/test_public_api_signature_baseline.py @@ -507,9 +507,7 @@ def test_generate_signature_matches_baseline() -> None: GenerateConfig.model_rebuild(_types_namespace={"StrictTypes": StrictTypes, "UnionMode": UnionMode}) - for name, param in baseline_params.items(): # pragma: no branch - if param.default is inspect.Parameter.empty: - continue + for name, param in baseline_params.items(): config_default = GenerateConfig.model_fields[name].default assert config_default == param.default, ( f"Default mismatch for '{name}':\n Baseline: {param.default!r}\n GenerateConfig: {config_default!r}" @@ -654,18 +652,13 @@ def test_generate_config_defaults_match_generate_signature() -> None: expected_sig = inspect.signature(_baseline_generate) expected_params = _kwonly_by_name(expected_sig) - for field_name, field_info in GenerateConfig.model_fields.items(): # pragma: no branch + for field_name, field_info in GenerateConfig.model_fields.items(): if field_name not in expected_params: continue param = expected_params[field_name] config_default = field_info.default - # Handle Parameter.empty vs None - if param.default is inspect.Parameter.empty: - # No default in signature means required, but Config may have None default - continue - assert config_default == param.default, ( f"Default mismatch for {field_name}: Config={config_default}, generate()={param.default}" ) @@ -679,16 +672,13 @@ def test_parser_config_defaults_match_parser_signature() -> None: expected_sig = inspect.signature(_BaselineParser.__init__) expected_params = _kwonly_by_name(expected_sig) - for field_name, field_info in ParserConfig.model_fields.items(): # pragma: no branch + for field_name, field_info in ParserConfig.model_fields.items(): if field_name not in expected_params: continue param = expected_params[field_name] config_default = field_info.default - if param.default is inspect.Parameter.empty: - continue - if callable(param.default) and config_default is None: continue @@ -705,16 +695,13 @@ def test_parse_config_defaults_match_parse_signature() -> None: expected_sig = inspect.signature(_BaselineParser.parse) expected_params = _params_by_name(expected_sig) - for field_name, field_info in ParseConfig.model_fields.items(): # pragma: no branch + for field_name, field_info in ParseConfig.model_fields.items(): if field_name not in expected_params: continue param = expected_params[field_name] config_default = field_info.default - if param.default is inspect.Parameter.empty: - continue - assert config_default == param.default, ( f"Default mismatch for {field_name}: Config={config_default}, Parser.parse()={param.default}" ) diff --git a/tests/test_input_model.py b/tests/test_input_model.py index 339dfe507..d1c2692f4 100644 --- a/tests/test_input_model.py +++ b/tests/test_input_model.py @@ -1120,14 +1120,10 @@ def test_input_model_multiple_pydantic_v1_error( import builtins original_hasattr = builtins.hasattr - call_count = 0 def mock_hasattr(obj: object, name: str) -> bool: - nonlocal call_count - if name == "model_json_schema": # pragma: no branch - call_count += 1 - if call_count <= 2: - return False + if name == "model_json_schema": + return False return original_hasattr(obj, name) monkeypatch.setattr(builtins, "hasattr", mock_hasattr) @@ -1375,8 +1371,6 @@ def test_input_model_cwd_already_in_path( cwd = str(_Path.cwd()) initial_count = sys.path.count(cwd) - if cwd not in sys.path: # pragma: no branch - sys.path.insert(0, cwd) run_multiple_input_models_and_assert( input_models=[ From 3f2b7134f9da9acd33baa7002c2896a78844458f Mon Sep 17 00:00:00 2001 From: Koudai Aono Date: Sat, 3 Jan 2026 12:44:57 +0000 Subject: [PATCH 28/28] Remove more dead code from test baseline checks --- tests/main/test_public_api_signature_baseline.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/tests/main/test_public_api_signature_baseline.py b/tests/main/test_public_api_signature_baseline.py index 74155ed1b..f9fff5b1e 100644 --- a/tests/main/test_public_api_signature_baseline.py +++ b/tests/main/test_public_api_signature_baseline.py @@ -653,9 +653,6 @@ def test_generate_config_defaults_match_generate_signature() -> None: expected_params = _kwonly_by_name(expected_sig) for field_name, field_info in GenerateConfig.model_fields.items(): - if field_name not in expected_params: - continue - param = expected_params[field_name] config_default = field_info.default @@ -673,9 +670,6 @@ def test_parser_config_defaults_match_parser_signature() -> None: expected_params = _kwonly_by_name(expected_sig) for field_name, field_info in ParserConfig.model_fields.items(): - if field_name not in expected_params: - continue - param = expected_params[field_name] config_default = field_info.default @@ -696,9 +690,6 @@ def test_parse_config_defaults_match_parse_signature() -> None: expected_params = _params_by_name(expected_sig) for field_name, field_info in ParseConfig.model_fields.items(): - if field_name not in expected_params: - continue - param = expected_params[field_name] config_default = field_info.default