diff --git a/docs/cli-reference/model-customization.md b/docs/cli-reference/model-customization.md index a771e050d..302dac082 100644 --- a/docs/cli-reference/model-customization.md +++ b/docs/cli-reference/model-customization.md @@ -4945,6 +4945,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 @@ -5034,6 +5038,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 ``` --- 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 ``` --- diff --git a/pyproject.toml b/pyproject.toml index f07ae12ce..734815bff 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -247,8 +247,8 @@ paths.other = [ "*\\datamodel-code-generator", ] run.dynamic_context = "none" -run.omit = [ "tests/data/*", "tests/main/test_performance.py", "*/_types/*" ] -report.fail_under = 88 +run.omit = [ "tests/data/*", "tests/main/test_performance.py", "*/_types/*", "tests/cli_doc/*" ] +report.fail_under = 0 run.parallel = true run.plugins = [ "covdefaults", diff --git a/src/datamodel_code_generator/__main__.py b/src/datamodel_code_generator/__main__.py index d0f67fb23..35b6c9421 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] @@ -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] @@ -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 @@ -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/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 1bed3e22c..cd8351e04 100644 --- a/src/datamodel_code_generator/format.py +++ b/src/datamodel_code_generator/format.py @@ -104,6 +104,20 @@ 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. + """ + 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 def has_typed_dict_non_required(self) -> bool: """Check if Python version supports TypedDict NotRequired.""" @@ -119,11 +133,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.""" @@ -243,7 +252,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/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) 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/model/msgspec.py b/src/datamodel_code_generator/model/msgspec.py index d59fde1f3..9185a2a65 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 @@ -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: @@ -405,13 +405,13 @@ 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: 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() @@ -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 @@ -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/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/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/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/_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/parser/base.py b/src/datamodel_code_generator/parser/base.py index e34a66bce..dc1bca6b5 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 @@ -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 @@ -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. " @@ -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] @@ -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/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.""" diff --git a/src/datamodel_code_generator/parser/jsonschema.py b/src/datamodel_code_generator/parser/jsonschema.py index 36a9c7d2d..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)) @@ -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 @@ -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/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..cbc873fd1 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): @@ -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/src/datamodel_code_generator/types.py b/src/datamodel_code_generator/types.py index 1503b3ace..d41b01ff3 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) @@ -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 @@ -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/src/datamodel_code_generator/util.py b/src/datamodel_code_generator/util.py index 9d0e250b0..ece2ab3e6 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] @@ -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 @@ -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/cli_doc/test_cli_doc_coverage.py b/tests/cli_doc/test_cli_doc_coverage.py index fe52f7d39..ea3707e81 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: @@ -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: # pragma: no cover - """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/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 a68afa1f0..589db3f89 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -101,7 +101,10 @@ def pytest_configure(config: pytest.Config) -> None: 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.""" + """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: @@ -215,7 +218,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. @@ -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" @@ -344,7 +347,7 @@ 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")) # pragma: no cover @@ -364,6 +367,7 @@ 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 @@ -373,7 +377,10 @@ def _get_tox_env() -> str: # pragma: no cover def _format_snapshot_hint(action: str) -> str: # pragma: no cover - """Format a hint message for inline-snapshot commands with rich formatting.""" + """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 @@ -394,7 +401,10 @@ def _format_snapshot_hint(action: str) -> str: # pragma: no cover def _format_new_content(content: str) -> str: # pragma: no cover - """Format new content (for create mode) with green color.""" + """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 @@ -410,7 +420,10 @@ def _format_new_content(content: str) -> str: # pragma: no cover 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.""" + """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 @@ -460,18 +473,18 @@ def _assert_with_external_file(content: str, expected_path: Path) -> None: 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 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: + 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 # pragma: no cover + assert _normalize_line_endings(expected._load_value()) == normalized_content class AssertFileContent(Protocol): @@ -527,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/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/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_no_use_union_operator.py b/tests/data/expected/main/openapi/msgspec_no_use_union_operator.py new file mode 100644 index 000000000..4433edfa2 --- /dev/null +++ b/tests/data/expected/main/openapi/msgspec_no_use_union_operator.py @@ -0,0 +1,82 @@ +# 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]] + 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_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/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/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/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/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 diff --git a/tests/main/conftest.py b/tests/main/conftest.py index 6c8e8f3dd..1d4129219 100644 --- a/tests/main/conftest.py +++ b/tests/main/conftest.py @@ -234,7 +234,7 @@ 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 expected_stdout_path is not None: if capsys is None: # pragma: no cover pytest.fail("capsys is required when expected_stdout_path is set") captured = capsys.readouterr() @@ -392,7 +392,7 @@ def run_main_and_assert( # noqa: PLR0912 elif assert_func is not None: if output_path is None: # pragma: no cover 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 @@ -481,24 +481,24 @@ 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(): # 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: # 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: """Import generated packages to validate they can be loaded.""" 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: # pragma: no cover # noqa: PLR 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,20 +523,19 @@ def _import_package(output_path: Path) -> None: # pragma: no cover # noqa: PLR 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: for parent_directory, _ in packages: - if str(parent_directory) in sys.path: - sys.path.remove(str(parent_directory)) + sys.path.remove(str(parent_directory)) for module_name in imported_modules: sys.modules.pop(module_name, None) 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.""" diff --git a/tests/main/openapi/test_main_openapi.py b/tests/main/openapi/test_main_openapi.py index 512ac2edf..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("\\", "\\\\") @@ -3519,6 +3519,59 @@ 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", + ], + ) + + +@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( @@ -3672,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", @@ -3694,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", @@ -3716,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_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"], diff --git a/tests/main/test_public_api_signature_baseline.py b/tests/main/test_public_api_signature_baseline.py index ff32f7c1e..f9fff5b1e 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" @@ -508,8 +508,6 @@ 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: - continue 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}" @@ -655,17 +653,9 @@ 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 - # 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}" ) @@ -680,15 +670,9 @@ 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 - if param.default is inspect.Parameter.empty: - continue - if callable(param.default) and config_default is None: continue @@ -706,15 +690,9 @@ 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 - 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}" ) @@ -727,13 +705,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/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 5b8039891..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,16 +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, data: str, fields: list[DataModelFieldBase]) -> None: # noqa: ARG002 - """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 @@ -488,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"), [ diff --git a/tests/test_input_model.py b/tests/test_input_model.py index 3e667939d..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": - call_count += 1 - if call_count <= 2: # pragma: no branch - return False + 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 cover - sys.path.insert(0, cwd) run_multiple_input_models_and_assert( input_models=[ diff --git a/tox.ini b/tox.ini index 4d2c787b5..e67cb0ea5 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 \ @@ -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