diff --git a/docs/src/python_config.md b/docs/src/python_config.md index 20b68aeac..dc268b48f 100644 --- a/docs/src/python_config.md +++ b/docs/src/python_config.md @@ -78,3 +78,41 @@ proving validation. Choose `strict` to reject unmatched inputs. Dictionary/additional-property model preservation and multi-member `allOf` composition are separate concerns and are not redesigned by this option. + +## Additional properties + +An object with explicit `additionalProperties` gets typed mutable +`additional_properties` storage, including a pure dictionary with no named fields. +The storage is a keyword-only dataclass field with an independent empty-dict +default. `from_dict` separates known wire keys from extension keys; `to_dict` +flattens the entries back into the same JSON object, without a storage wrapper. + +```python +value = MixedMap( + known_field="fixed", + additional_properties={"keep_key": NestedValue(wire_value="nested")}, +) +value.to_dict() # {"known_field": "fixed", "keep_key": {"wire_value": "nested"}} +``` + +Extension keys remain verbatim, even when they resemble renamed Python fields. +Opaque values (`true` or `{}`) keep arbitrary JSON, including null entries and +large integers. Typed values use the declared model/container/alias converters; +recursive references and union policies use the same boundaries as ordinary +values. The storage does not make a pure dictionary a `dict` subtype: use its +`additional_properties` field and normal `from_dict` / `to_dict` entrypoints. + +If an authored field already uses the Python name `additional_properties`, the +storage name gains trailing underscores until it is distinct. Authored fields +are not renamed. Supplying an extension key equal to a declared wire key raises +`ValueError` on serialization, even when that named field is currently omitted; +extensions never overwrite or impersonate named fields. + +Objects without explicit additional properties and closed objects keep their +existing behavior. Ordinary named-field codecs and optional-null omission are +unchanged. This is not new JSON Schema validation or an `allOf`/tagged-wrapper +representation redesign. + +Value conversion follows the retained schema facts. General inline composed +additional-property schemas currently lowered as opaque values remain opaque; +this storage repair does not change that shared lowering behavior. diff --git a/just/golden-python-clients.just b/just/golden-python-clients.just index 5ea76845f..8a688aba9 100644 --- a/just/golden-python-clients.just +++ b/just/golden-python-clients.just @@ -10,10 +10,11 @@ set working-directory := '..' strategy="$policy" [ "$policy" != default ] || strategy=firstMatch cargo run --quiet --bin openapi-nexus -- generate --config tests/generated-clients/config.toml --input tests/fixtures/valid/python-untagged-union.yaml --output "tests/generated-clients/python/generated/{{backend}}/$policy" --generators 'python-{{backend}}' --generator-config "python-{{backend}}.package_name=union_$policy" --generator-config "python-{{backend}}.untagged_union_strategy=$strategy" + cargo run --quiet --bin openapi-nexus -- generate --config tests/generated-clients/config.toml --input tests/fixtures/valid/python-additional-properties.yaml --output "tests/generated-clients/python/generated/{{backend}}/additional/$policy" --generators 'python-{{backend}}' --generator-config "python-{{backend}}.package_name=additional_$policy" --generator-config "python-{{backend}}.untagged_union_strategy=$strategy" done @build backend: (prepare backend) - "${PYRIGHT:-pyright}" --pythonpath "$(command -v "${PYTHON:-python3}")" --project tests/generated-clients/python/pyrightconfig.{{backend}}.json tests/generated-clients/python/generated/{{backend}}/default/union_default tests/generated-clients/python/generated/{{backend}}/strict/union_strict tests/generated-clients/python/generated/{{backend}}/raw/union_raw tests/generated-clients/python/typing_contracts.py + "${PYRIGHT:-pyright}" --pythonpath "$(command -v "${PYTHON:-python3}")" --project tests/generated-clients/python/pyrightconfig.{{backend}}.json tests/generated-clients/python/generated/{{backend}}/default/union_default tests/generated-clients/python/generated/{{backend}}/strict/union_strict tests/generated-clients/python/generated/{{backend}}/raw/union_raw tests/generated-clients/python/generated/{{backend}}/additional/default/additional_default tests/generated-clients/python/generated/{{backend}}/additional/strict/additional_strict tests/generated-clients/python/generated/{{backend}}/additional/raw/additional_raw tests/generated-clients/python/typing_contracts.py tests/generated-clients/python/additional_typing_contracts.py @test backend: (build backend) - NEXUS_PYTHON_BACKEND={{backend}} PYTHONPATH="tests/generated-clients/python/generated/{{backend}}/default:tests/generated-clients/python/generated/{{backend}}/strict:tests/generated-clients/python/generated/{{backend}}/raw" "${PYTHON:-python3}" -m unittest discover -v -s tests/generated-clients/python/tests + NEXUS_PYTHON_BACKEND={{backend}} PYTHONPATH="tests/generated-clients/python/generated/{{backend}}/default:tests/generated-clients/python/generated/{{backend}}/strict:tests/generated-clients/python/generated/{{backend}}/raw:tests/generated-clients/python/generated/{{backend}}/additional/default:tests/generated-clients/python/generated/{{backend}}/additional/strict:tests/generated-clients/python/generated/{{backend}}/additional/raw" "${PYTHON:-python3}" -m unittest discover -v -s tests/generated-clients/python/tests diff --git a/src/generators/python/additional_properties.rs b/src/generators/python/additional_properties.rs new file mode 100644 index 000000000..dccbb30db --- /dev/null +++ b/src/generators/python/additional_properties.rs @@ -0,0 +1,124 @@ +//! Explicit extension storage and flat object conversion for Python models. + +use std::collections::BTreeSet; + +use sigil_stitch::prelude::*; + +use super::emit_models::python_field_name; +use super::value::{Direction, ValuePlan}; +use crate::generators::type_application; +use crate::ir::types::{IrObject, IrTypeExpr}; + +pub(super) struct AdditionalProperties<'a> { + name: String, + values: &'a IrTypeExpr, + known: Vec<&'a str>, +} + +impl<'a> AdditionalProperties<'a> { + pub fn new(object: &'a IrObject) -> Option { + let values = object.additional_properties.as_ref()?; + let used = object + .properties + .values() + .map(|property| python_field_name(&property.name)) + .collect::>(); + let mut name = "additional_properties".to_string(); + while used.contains(&name) { + name.push('_'); + } + Some(Self { + name, + values, + known: object.properties.keys().map(String::as_str).collect(), + }) + } + + pub fn field(&self, schema: &str, plan: &ValuePlan<'_>) -> FieldSpec { + let factory = TypeName::importable("dataclasses", "field").with_alias("_dataclass_field"); + let dictionary = TypeName::importable("builtins", "dict").with_alias("_dict"); + let key = TypeName::importable("builtins", "str").with_alias("_str"); + let container = + type_application(dictionary, vec![key, plan.model_type(self.values, schema)]); + // Specialize the factory for strict typing, but defer recursive model + // references until construction, after their modules finish importing. + FieldSpec::builder(&self.name, container.clone()) + .initializer( + sigil_quote!(Python { + $T(factory)(default_factory=lambda: $T(container)(), kw_only=True, metadata={"openapi_additional_properties": True, "openapi_dictionary": $if(self.known.is_empty()) { True } $else { False }}) + }) + .expect("Python extension storage initializer builds") + .__sigil_trim_trailing_newline(), + ) + .build() + .expect("Python extension storage field builds") + } + + pub fn write_entries(&self, plan: &ValuePlan<'_>) -> CodeBlock { + let value = plan.call( + self.values, + sigil_quote!(Python { item }).expect("Python extension value access builds"), + Direction::To, + ); + sigil_quote!(Python { + for key, item in self.$N(self.name.as_str()).items() { + $if(!self.known.is_empty()) { + if key in [$for(key in &self.known; separator = ", ") { $S(*key) }] { + raise ValueError("Additional property collides with declared wire field: " + key) + } + } + result[key] = $C(value) + } + }) + .expect("Python flat extension serialization builds") + } + + pub fn read_entries(&self, schema: &str, plan: &ValuePlan<'_>) -> CodeBlock { + let value = plan.call( + self.values, + sigil_quote!(Python { item }).expect("Python extension wire value builds"), + Direction::From, + ); + let value = plan.coerce(self.values, plan.model_type(self.values, schema), value); + sigil_quote!(Python { + $N(self.name.as_str()) = {key: $C(value) for key, item in data.items() $if(!self.known.is_empty()) { if key not in [$for(key in &self.known; separator = ", ") { $S(*key) }] }}, + }) + .expect("Python typed extension deserialization builds") + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::ir::types::IrSchemaKind; + use sigil_stitch::{assert_rendered, lang::python::Python}; + + #[test] + fn storage_factory_aliases_and_collision_name_render_exactly() { + let source = "openapi: 3.1.0\ninfo: {title: Storage, version: '1'}\npaths: {}\ncomponents:\n schemas:\n Model:\n type: object\n properties:\n additionalProperties: {type: string}\n additionalProperties: {type: string}\n"; + let ir = + crate::ir::lower::lower(crate::parser::parse_content_yaml(source).unwrap()).unwrap(); + let IrSchemaKind::Object(object) = &ir.schemas["Model"].kind else { + panic!("extension model retains object facts"); + }; + let plan = ValuePlan::new(&ir, super::super::value::UntaggedUnionStrategy::FirstMatch); + let storage = AdditionalProperties::new(object).unwrap(); + let declaration = TypeSpec::builder("Model", TypeKind::Class) + .annotate(AnnotationSpec::importable(TypeName::importable( + "dataclasses", + "dataclass", + ))) + .add_field(storage.field("Model", &plan)) + .build() + .unwrap() + .emit(&Python::new()) + .unwrap(); + let body = sigil_quote!(Python { $C_each(declaration) }).unwrap(); + assert_rendered!( + Python::new(), + width = 300, + body, + "from builtins import dict as _dict, str as _str\nfrom dataclasses import dataclass, field as _dataclass_field\n\n@dataclass\nclass Model:\n additional_properties_: _dict[_str, str] = _dataclass_field(default_factory = lambda: _dict[_str, str](), kw_only = True, metadata = {\"openapi_additional_properties\": True, \"openapi_dictionary\": False})\n\n", + ); + } +} diff --git a/src/generators/python/emit_api_values.rs b/src/generators/python/emit_api_values.rs index 0a8ae1ef2..74442edf0 100644 --- a/src/generators/python/emit_api_values.rs +++ b/src/generators/python/emit_api_values.rs @@ -25,10 +25,13 @@ pub(super) fn multipart_data( ir: &IrSpec, values: &ValuePlan<'_>, ) -> CodeBlock { - if parts.iter().any(|part| values.has_union(&part.type_expr)) { + if parts + .iter() + .any(|part| values.needs_conversion(&part.type_expr)) + { let assignments = parts.iter().map(|part| { let source = sigil_quote!(Python { $N(body_name).$N(part.field_name.as_str()) }).expect("Python multipart input access builds"); - let assignment = if part.value_encoding == MultipartValueEncoding::Json && values.has_union(&part.type_expr) { + let assignment = if part.value_encoding == MultipartValueEncoding::Json && values.needs_conversion(&part.type_expr) { let converted = values.call(&part.type_expr, source.clone(), Direction::To); sigil_quote!(Python { files[$S(part.wire_name.as_str())] = (None, json.dumps($C(converted)), $S(part.content_type.as_str())) diff --git a/src/generators/python/emit_models.rs b/src/generators/python/emit_models.rs index 2b33fa0bf..fd3e8d1ac 100644 --- a/src/generators/python/emit_models.rs +++ b/src/generators/python/emit_models.rs @@ -4,6 +4,7 @@ //! sigil_quote!) for structured code generation with automatic import tracking. //! Each schema produces one `.py` file via `FileSpec`. +use super::additional_properties::AdditionalProperties; use super::value::{Direction, UntaggedUnionStrategy, ValuePlan, optional_type}; use crate::codegen::traits::file_writer::FileInfo; use crate::generators::GeneratorError; @@ -73,7 +74,7 @@ fn request_input_model_file( if model .fields .iter() - .any(|field| values.has_union(&field.type_expr)) + .any(|field| values.needs_conversion(&field.type_expr)) { let mut declaration = TypeSpec::builder(&model.name.to_pascal_case(), TypeKind::Class).annotate( @@ -204,24 +205,31 @@ fn emit_object( values: &ValuePlan<'_>, ) -> Option { let name = schema.name.to_pascal_case(); + let additional = AdditionalProperties::new(obj); let mut file = FileSpec::builder_with("model.py", Python::new()).header(future_annotations_header()); - if needs_typing_literal_in_props(&obj.properties) { + if needs_typing_literal_in_props(&obj.properties) + || obj + .additional_properties + .as_ref() + .is_some_and(needs_typing_literal) + { file = file.add_import(ImportSpec::named("typing", "Literal")); } if obj .properties .values() - .any(|property| values.has_union(&property.type_expr)) + .any(|property| values.needs_conversion(&property.type_expr)) + || additional.is_some() { file = file.add_import(ImportSpec::named(".", "_union")); } // Import tagged-union helper functions for fields referencing TaggedUnion schemas for (_, prop) in &obj.properties { - if values.has_union(&prop.type_expr) { + if values.needs_conversion(&prop.type_expr) { continue; } for named_ref in collect_tagged_union_refs(&prop.type_expr, ir) { @@ -253,7 +261,7 @@ fn emit_object( let all_fields: Vec<(&String, &IrProperty)> = required.iter().chain(optional.iter()).copied().collect(); - if all_fields.is_empty() { + if all_fields.is_empty() && additional.is_none() { cls = cls.extra_member(CodeBlock::of("pass", ()).expect("pass")); } else { for (_json_name, prop) in &required { @@ -276,11 +284,16 @@ fn emit_object( ); } + if let Some(additional) = &additional { + cls = cls.add_field(additional.field(&schema.name, values)); + } + cls = cls.add_method(build_to_dict_method( &all_fields, ir, &obj.properties, values, + additional.as_ref(), )); cls = cls.add_method(build_from_dict_method( &name, @@ -289,6 +302,7 @@ fn emit_object( &obj.properties, values, &schema.name, + additional.as_ref(), )); } @@ -301,6 +315,7 @@ fn build_to_dict_method( ir: &IrSpec, properties: &indexmap::IndexMap, values: &ValuePlan<'_>, + additional: Option<&AdditionalProperties<'_>>, ) -> FunSpec { let self_param = ParameterSpec::of("self", TypeName::primitive("")); let return_type = type_application( @@ -312,13 +327,16 @@ fn build_to_dict_method( body.add_statement("result: dict[str, object] = {}", ()); for (json_name, prop) in all_fields { let field_name = python_field_name(&prop.name); - if values.has_union(&prop.type_expr) { + if values.needs_conversion(&prop.type_expr) { let access = sigil_quote!(Python { self.$N(field_name.as_str()) }) - .expect("Python union field access builds"); + .expect("Python planned field access builds"); let converted = values.call(&values.property_expr(prop), access, Direction::To); + // Newly planned object paths retain ordinary nullable-field omission. + let is_required = + prop.required && (values.has_union(&prop.type_expr) || !prop.nullable); body.add_code( sigil_quote!(Python { - $if(prop.required) { + $if(is_required) { result[$S(json_name.as_str())] = $C(converted) } $else { if self.$N(field_name) is not None { @@ -326,7 +344,7 @@ fn build_to_dict_method( } } }) - .expect("Python union field serialization builds"), + .expect("Python planned field serialization builds"), ); continue; } @@ -338,6 +356,9 @@ fn build_to_dict_method( body.add_statement(&format!("result[\"{json_name}\"] = {to_expr}%<"), ()); } } + if let Some(additional) = additional { + body.add_code(additional.write_entries(values)); + } body.add_statement("return result", ()); FunSpec::builder("to_dict") @@ -355,6 +376,7 @@ fn build_from_dict_method( properties: &indexmap::IndexMap, values: &ValuePlan<'_>, schema_name: &str, + additional: Option<&AdditionalProperties<'_>>, ) -> FunSpec { let cls_param = ParameterSpec::of("cls", TypeName::primitive("")); let data_param = ParameterSpec::of( @@ -370,22 +392,34 @@ fn build_from_dict_method( for (json_name, prop) in all_fields { let field_name = python_field_name(&prop.name); let is_required = prop.required && !prop.nullable; - if values.has_union(&prop.type_expr) { - let access = if prop.required { + if values.needs_conversion(&prop.type_expr) { + let has_union = values.has_union(&prop.type_expr); + let requires_key = if has_union { + prop.required + } else { + is_required + }; + let access = if requires_key { sigil_quote!(Python { data[$S(json_name.as_str())] }) } else { sigil_quote!(Python { data.get($S(json_name.as_str())) }) } - .expect("Python union field wire access builds"); + .expect("Python planned field wire access builds"); let expression = values.property_expr(prop); let converted = values.call(&expression, access.clone(), Direction::From); let ty = values.model_type(&prop.type_expr, schema_name); let ty = if is_required { ty } else { optional_type(ty) }; - let converted = sigil_quote!(Python { $if(prop.required) { $C(converted) } $else { None if $S(json_name.as_str()) not in data else $C(converted) } }).expect("Python optional union field projection builds"); + // Union fields keep their existing explicit-null policy. Ordinary + // optional/nullable objects never pass None into their converter. + let converted = sigil_quote!(Python { + $if(requires_key) { $C(converted) } + $else_if(has_union) { None if $S(json_name.as_str()) not in data else $C(converted) } + $else { None if $C(access) is None else $C(converted) } + }).expect("Python optional planned field projection builds"); let converted = values.coerce(&expression, ty, converted); body.add_code( sigil_quote!(Python { $N(field_name) = $C(converted), }) - .expect("Python union field deserialization builds"), + .expect("Python planned field deserialization builds"), ); continue; } @@ -401,6 +435,9 @@ fn build_from_dict_method( body.add_statement(&format!("{field_name}={expr},"), ()); } } + if let Some(additional) = additional { + body.add_code(additional.read_entries(schema_name, values)); + } body.add("%<", ()); body.add_statement(")", ()); @@ -674,7 +711,7 @@ fn emit_intersection_as_dataclass( } if all_props .values() - .any(|property| values.has_union(&property.type_expr)) + .any(|property| values.needs_conversion(&property.type_expr)) { file = file.add_import(ImportSpec::named(".", "_union")); } @@ -722,7 +759,13 @@ fn emit_intersection_as_dataclass( let all_fields: Vec<(&String, &IrProperty)> = required.iter().chain(optional.iter()).copied().collect(); - cls = cls.add_method(build_to_dict_method(&all_fields, ir, all_props, values)); + cls = cls.add_method(build_to_dict_method( + &all_fields, + ir, + all_props, + values, + None, + )); cls = cls.add_method(build_from_dict_method( &name, &all_fields, @@ -730,6 +773,7 @@ fn emit_intersection_as_dataclass( all_props, values, &schema.name, + None, )); } diff --git a/src/generators/python/httpx/emit_api.rs b/src/generators/python/httpx/emit_api.rs index 5be7a0067..b765bb897 100644 --- a/src/generators/python/httpx/emit_api.rs +++ b/src/generators/python/httpx/emit_api.rs @@ -146,9 +146,11 @@ fn emit_api_file( } if plans.iter().any(|plan| { plan.body.as_ref().is_some_and(|body| { - body.multipart_parts - .as_ref() - .is_some_and(|parts| parts.iter().any(|part| values.has_union(&part.type_expr))) + body.multipart_parts.as_ref().is_some_and(|parts| { + parts + .iter() + .any(|part| values.needs_conversion(&part.type_expr)) + }) }) }) || ops.iter().any(|op| { op.request_body @@ -159,7 +161,7 @@ fn emit_api_file( .iter() .flat_map(|response| response.content.values()), ) - .any(|expr| values.has_union(expr)) + .any(|expr| values.needs_conversion(expr)) }) { fb = fb.add_import(ImportSpec::named("..models", "_union")); fb = fb.add_import(ImportSpec::side_effect("json")); @@ -420,7 +422,7 @@ fn build_method_body( // Body serialization let body_expr = if let Some(b) = &plan.body { - if values.has_union(&b.type_expr) { + if values.needs_conversion(&b.type_expr) { let access = sigil_quote!(Python { $N(b.var_name.as_str()) }) .expect("Python HTTP union input builds"); let converted = values.call(&b.type_expr, access, Direction::To); @@ -481,7 +483,7 @@ fn build_method_body( } } else { match body.encoding { - BodyEncoding::Json if values.has_union(&body.type_expr) => { + BodyEncoding::Json if values.needs_conversion(&body.type_expr) => { union_json_argument = Some(sigil_quote!(Python { content = $if(body.required) { json.dumps(_union_body, allow_nan = False) } $else { None if $N(body.var_name.as_str()) is None else json.dumps(_union_body, allow_nan = False) } }).expect("Python httpx union JSON argument builds")); @@ -534,7 +536,7 @@ fn build_method_body( cb.add_code(emit_error_raise(plan, "response.reason_phrase")); let union_response = plan.typed_responses.first().filter(|response| { - response.decoding == ResponseDecoding::Json && values.has_union(&response.type_expr) + response.decoding == ResponseDecoding::Json && values.needs_conversion(&response.type_expr) }); if let Some(response) = union_response { let converted = values.call( @@ -755,7 +757,7 @@ fn error_body_property( ir: &IrSpec, values: &ValuePlan<'_>, ) -> CodeBlock { - if decoding == ResponseDecoding::Json && values.has_union(type_expr) { + if decoding == ResponseDecoding::Json && values.needs_conversion(type_expr) { return union_error_body_property(type_expr, ir, values); } let mut cb = CodeBlock::builder(); diff --git a/src/generators/python/mod.rs b/src/generators/python/mod.rs index 42925c0b5..8a65a8d6a 100644 --- a/src/generators/python/mod.rs +++ b/src/generators/python/mod.rs @@ -1,3 +1,4 @@ +mod additional_properties; mod emit_api_values; mod emit_models; pub mod httpx; diff --git a/src/generators/python/requests/emit_api.rs b/src/generators/python/requests/emit_api.rs index 9a1336318..38953f499 100644 --- a/src/generators/python/requests/emit_api.rs +++ b/src/generators/python/requests/emit_api.rs @@ -146,9 +146,11 @@ fn emit_api_file( } if plans.iter().any(|plan| { plan.body.as_ref().is_some_and(|body| { - body.multipart_parts - .as_ref() - .is_some_and(|parts| parts.iter().any(|part| values.has_union(&part.type_expr))) + body.multipart_parts.as_ref().is_some_and(|parts| { + parts + .iter() + .any(|part| values.needs_conversion(&part.type_expr)) + }) }) }) || ops.iter().any(|op| { op.request_body @@ -159,7 +161,7 @@ fn emit_api_file( .iter() .flat_map(|response| response.content.values()), ) - .any(|expr| values.has_union(expr)) + .any(|expr| values.needs_conversion(expr)) }) { fb = fb.add_import(ImportSpec::named("..models", "_union")); fb = fb.add_import(ImportSpec::side_effect("json")); @@ -420,7 +422,7 @@ fn build_method_body( // Body serialization let body_expr = if let Some(b) = &plan.body { - if values.has_union(&b.type_expr) { + if values.needs_conversion(&b.type_expr) { let access = sigil_quote!(Python { $N(b.var_name.as_str()) }) .expect("Python HTTP union input builds"); let converted = values.call(&b.type_expr, access, Direction::To); @@ -481,7 +483,7 @@ fn build_method_body( } } else { match body.encoding { - BodyEncoding::Json if values.has_union(&body.type_expr) => { + BodyEncoding::Json if values.needs_conversion(&body.type_expr) => { union_json_argument = Some(sigil_quote!(Python { data = $if(body.required) { json.dumps(_union_body, allow_nan = False) } $else { None if $N(body.var_name.as_str()) is None else json.dumps(_union_body, allow_nan = False) } }).expect("Python requests union JSON argument builds")); @@ -533,7 +535,7 @@ fn build_method_body( cb.add_code(emit_error_raise(plan, "response.reason")); let union_response = plan.typed_responses.first().filter(|response| { - response.decoding == ResponseDecoding::Json && values.has_union(&response.type_expr) + response.decoding == ResponseDecoding::Json && values.needs_conversion(&response.type_expr) }); if let Some(response) = union_response { let converted = values.call( @@ -754,7 +756,7 @@ fn error_body_property( ir: &IrSpec, values: &ValuePlan<'_>, ) -> CodeBlock { - if decoding == ResponseDecoding::Json && values.has_union(type_expr) { + if decoding == ResponseDecoding::Json && values.needs_conversion(type_expr) { return union_error_body_property(type_expr, ir, values); } let mut cb = CodeBlock::builder(); diff --git a/src/generators/python/runtime/union_support.py.txt b/src/generators/python/runtime/union_support.py.txt index bf1d91ea3..69acb5186 100644 --- a/src/generators/python/runtime/union_support.py.txt +++ b/src/generators/python/runtime/union_support.py.txt @@ -2,7 +2,7 @@ from __future__ import annotations -from dataclasses import is_dataclass +from dataclasses import fields, is_dataclass from enum import Enum from math import isfinite from typing import Protocol, TypeGuard, cast @@ -28,6 +28,24 @@ def is_list(value: object) -> TypeGuard[list[object]]: return isinstance(value, list) +def _additional_properties(value: object, dictionary_only: bool = False) -> dict[str, object] | None: + if not isinstance(value, type) and is_dataclass(value): + for field in fields(value): + if field.metadata.get("openapi_additional_properties") is True: + if not dictionary_only or field.metadata.get("openapi_dictionary") is True: + return as_dict(getattr(value, field.name)) + return None + + +def is_mapping(value: object) -> bool: + return is_dict(value) or _additional_properties(value, True) is not None + + +def mapping(value: object) -> dict[str, object]: + properties = _additional_properties(value, True) + return properties if properties is not None else as_dict(value) + + def scalar(value: object) -> object: return value.value if isinstance(value, Enum) else value @@ -53,9 +71,10 @@ def get(value: object, wire: str, field: str) -> object: def extras(value: object, known: list[str]) -> list[object]: - if not is_dict(value): - return [] - return [item for key, item in as_dict(value).items() if key not in known] + if is_dict(value): + return [item for key, item in as_dict(value).items() if key not in known] + properties = _additional_properties(value) + return list(properties.values()) if properties is not None else [] def number(value: object, integer: bool) -> bool: diff --git a/src/generators/python/value.rs b/src/generators/python/value.rs index e8840f1bd..d848077bb 100644 --- a/src/generators/python/value.rs +++ b/src/generators/python/value.rs @@ -1,4 +1,4 @@ -//! Python value conversion at untagged-union boundaries, shared by both transports. +//! Python union and explicit-extension value conversion, shared by both transports. //! //! Matching consumes the target-independent shape plan. Codecs keep original //! member boundaries: count candidates first, then invoke exactly one delegate. @@ -99,16 +99,19 @@ impl<'a> ValuePlan<'a> { for schema in ir.schemas.values() { if let IrSchemaKind::Object(object) = &schema.kind { for property in object.properties.values() { - if plan.has_union(&property.type_expr) { + if plan.needs_conversion(&property.type_expr) { plan.register(&plan.property_expr(property)); } } + if let Some(values) = &object.additional_properties { + plan.register(values); + } } - if plan.has_union(&IrTypeExpr::Named(schema.name.clone())) { + if plan.needs_conversion(&IrTypeExpr::Named(schema.name.clone())) { plan.register(&IrTypeExpr::Named(schema.name.clone())); } for expr in schema_expressions(&schema.kind) { - if plan.has_union(expr) { + if plan.needs_conversion(expr) { plan.register(expr); } } @@ -123,7 +126,7 @@ impl<'a> ValuePlan<'a> { .iter() .flat_map(|response| response.content.values()); for expr in request.chain(responses) { - if plan.has_union(expr) { + if plan.needs_conversion(expr) { plan.register(expr); } } @@ -131,29 +134,52 @@ impl<'a> ValuePlan<'a> { plan } + /// Boundaries requiring planned codecs, through aliases and containers. + pub fn needs_conversion(&self, expr: &IrTypeExpr) -> bool { + self.contains_conversion(expr, true) + } + + /// Preserve union-specific public helpers and field null semantics. pub fn has_union(&self, expr: &IrTypeExpr) -> bool { - fn visit(expr: &IrTypeExpr, ir: &IrSpec, seen: &mut BTreeSet) -> bool { + self.contains_conversion(expr, false) + } + + fn contains_conversion(&self, expr: &IrTypeExpr, include_extensions: bool) -> bool { + fn visit( + expr: &IrTypeExpr, + ir: &IrSpec, + seen: &mut BTreeSet, + include_extensions: bool, + ) -> bool { match expr { IrTypeExpr::Union(_) => true, - IrTypeExpr::ScalarEnum { representation, .. } => visit(representation, ir, seen), + IrTypeExpr::ScalarEnum { representation, .. } => { + visit(representation, ir, seen, include_extensions) + } IrTypeExpr::Array(inner) | IrTypeExpr::Map(inner) | IrTypeExpr::Nullable(inner) => { - visit(inner, ir, seen) + visit(inner, ir, seen, include_extensions) } IrTypeExpr::Named(name) if seen.insert(name.clone()) => { match ir.schemas.get(name).map(|schema| &schema.kind) { Some(IrSchemaKind::Union(_)) => true, - Some(IrSchemaKind::Alias(inner)) => visit(inner, ir, seen), - Some(IrSchemaKind::TaggedUnion(union)) => union - .variants - .iter() - .any(|variant| visit(&variant.content_type, ir, seen)), + Some(IrSchemaKind::Object(object)) => { + include_extensions && object.additional_properties.is_some() + } + Some(IrSchemaKind::Alias(inner)) => { + visit(inner, ir, seen, include_extensions) + } + Some(IrSchemaKind::TaggedUnion(union)) => { + union.variants.iter().any(|variant| { + visit(&variant.content_type, ir, seen, include_extensions) + }) + } _ => false, } } _ => false, } } - visit(expr, self.ir, &mut BTreeSet::new()) + visit(expr, self.ir, &mut BTreeSet::new(), include_extensions) } pub fn property_expr(&self, property: &crate::ir::types::IrProperty) -> IrTypeExpr { @@ -210,7 +236,7 @@ impl<'a> ValuePlan<'a> { .expressions .iter() .position(|candidate| candidate == expr) - .expect("union conversion expression belongs to the value plan"); + .expect("conversion expression belongs to the Python value plan"); format!("value_{index}_{}", direction.suffix()) } @@ -280,6 +306,8 @@ impl<'a> ValuePlan<'a> { fn type_name(&self, expr: &IrTypeExpr, current: Option<&str>) -> TypeName { match expr { + // Keep the runtime typing helper distinct from an authored Any model. + IrTypeExpr::Any => TypeName::importable("typing", "Any").with_alias("_Any"), IrTypeExpr::Union(_) if self.strategy == UntaggedUnionStrategy::Raw => { TypeName::primitive("object") } @@ -467,7 +495,7 @@ impl<'a> ValuePlan<'a> { let conversion = self.invoke(inner, value, direction)?; sigil_quote!(Python { return $C(conversion) }) } - Some(IrSchemaKind::TaggedUnion(union)) if self.has_union(expr) => { + Some(IrSchemaKind::TaggedUnion(union)) if self.needs_conversion(expr) => { self.tagged_body(union, direction, references) } Some(IrSchemaKind::TaggedUnion(_)) => { @@ -536,7 +564,7 @@ impl<'a> ValuePlan<'a> { } IrTypeExpr::Map(inner) => { let item = self.invoke(inner, sigil_quote!(Python { item })?, direction)?; - sigil_quote!(Python { return {key: $C(item) for key, item in _union_support.as_dict(value).items()} }) + sigil_quote!(Python { return {key: $C(item) for key, item in $if(direction == Direction::To) { _union_support.mapping(value) } $else { _union_support.as_dict(value) }.items()} }) } IrTypeExpr::Nullable(inner) => { let conversion = self.invoke(inner, value, direction)?; @@ -792,7 +820,13 @@ fn condition( direction, references, )?; - sigil_quote!(Python { (_union_support.is_dict($C(value.clone())) and all($C(inner) for _item in _union_support.as_dict($C(value)).values())) }) + sigil_quote!(Python { + $if(direction == Direction::To) { + (_union_support.is_mapping($C(value.clone())) and all($C(inner) for _item in _union_support.mapping($C(value)).values())) + } $else { + (_union_support.is_dict($C(value.clone())) and all($C(inner) for _item in _union_support.as_dict($C(value)).values())) + } + }) } JsonShape::MapWithConcreteValue { values, concrete } => { let all = condition( @@ -807,7 +841,7 @@ fn condition( direction, references, )?; - sigil_quote!(Python { ($C(all) and any($C(one) for _item in _union_support.as_dict($C(value)).values())) }) + sigil_quote!(Python { ($C(all) and any($C(one) for _item in $if(direction == Direction::To) { _union_support.mapping($C(value)) } $else { _union_support.as_dict($C(value)) }.values())) }) } JsonShape::Object(properties) | JsonShape::ObjectWithAdditional { properties, .. } => { let mut checks = diff --git a/tests/fixtures/valid/python-additional-properties.yaml b/tests/fixtures/valid/python-additional-properties.yaml new file mode 100644 index 000000000..083b3f85c --- /dev/null +++ b/tests/fixtures/valid/python-additional-properties.yaml @@ -0,0 +1,185 @@ +openapi: 3.1.0 +info: {title: Python additional properties, version: '1.0.0'} +paths: + /mixed: + post: + operationId: echoMixed + requestBody: + required: true + content: + application/json: + schema: {$ref: '#/components/schemas/MixedMap'} + responses: + '200': + description: Mixed object + content: + application/json: + schema: {$ref: '#/components/schemas/MixedMap'} + '400': + description: Typed dictionary error + content: + application/json: + schema: {$ref: '#/components/schemas/TypedMap'} + /dictionary: + post: + operationId: echoDictionary + requestBody: + required: true + content: + application/json: + schema: {$ref: '#/components/schemas/DictionaryAlias'} + responses: + '200': + description: Typed dictionary + content: + application/json: + schema: {$ref: '#/components/schemas/DictionaryAlias'} +components: + schemas: + NestedValue: + type: object + required: [wire_value] + properties: + wire_value: {type: string} + OpaqueMap: + type: object + required: [known_field] + properties: + known_field: {type: string} + additionalProperties: true + MixedMap: + type: object + required: [known_field] + properties: + known_field: {type: string} + additionalProperties: {$ref: '#/components/schemas/NestedValue'} + TypedMap: + type: object + additionalProperties: {$ref: '#/components/schemas/NestedValue'} + RawMap: + type: object + additionalProperties: {} + Any: + type: object + additionalProperties: true + Numbers: + type: object + additionalProperties: {type: integer, format: int64} + Status: + type: string + enum: [active, inactive] + StatusValues: + type: object + additionalProperties: {$ref: '#/components/schemas/Status'} + NullableValues: + type: object + additionalProperties: + anyOf: [{$ref: '#/components/schemas/NestedValue'}, {type: 'null'}] + DeepValues: + type: object + additionalProperties: + type: array + items: + type: object + additionalProperties: {$ref: '#/components/schemas/NestedValue'} + DictionaryAlias: {$ref: '#/components/schemas/TypedMap'} + ValueAlias: {$ref: '#/components/schemas/NestedValue'} + AliasValues: + type: object + additionalProperties: {$ref: '#/components/schemas/ValueAlias'} + Choices: + type: object + additionalProperties: {$ref: '#/components/schemas/ValueChoice'} + ValueChoice: + anyOf: [{$ref: '#/components/schemas/NestedValue'}, {type: integer}] + AmbiguousValues: + type: object + additionalProperties: {$ref: '#/components/schemas/AmbiguousNumber'} + AmbiguousNumber: + anyOf: [{type: integer}, {type: number}] + DictionaryChoice: + anyOf: [{$ref: '#/components/schemas/TypedMap'}, {type: integer}] + MixedChoice: + anyOf: [{$ref: '#/components/schemas/MixedMap'}, {type: string}] + Event: + oneOf: + - type: object + required: [type, sequence_number] + properties: + type: {type: string, enum: [searching]} + sequence_number: {type: integer} + additionalProperties: true + - type: object + required: [type] + properties: + type: {type: string, enum: [completed]} + additionalProperties: true + EventValues: + type: object + additionalProperties: {$ref: '#/components/schemas/Event'} + Recursive: + type: object + required: [wire_value] + properties: + wire_value: {type: string} + additionalProperties: {$ref: '#/components/schemas/Recursive'} + PureRecursive: + type: object + additionalProperties: {$ref: '#/components/schemas/PureRecursive'} + Node: + type: object + required: [wire_value, children] + properties: + wire_value: {type: string} + children: {$ref: '#/components/schemas/Nodes'} + Nodes: + type: object + additionalProperties: {$ref: '#/components/schemas/Node'} + StorageCollision: + type: object + required: [additionalProperties] + properties: + additionalProperties: {type: string} + additionalProperties: {$ref: '#/components/schemas/NestedValue'} + FactoryCollision: + type: object + properties: + field: {type: string} + dict: {type: string} + additionalProperties: true + OptionalKnown: + type: object + properties: + known_field: {type: string} + additionalProperties: true + OptionalHolder: + type: object + required: [nullable] + properties: + maybe: {$ref: '#/components/schemas/MixedMap'} + nullable: + anyOf: [{$ref: '#/components/schemas/MixedMap'}, {type: 'null'}] + LegacyOptionalHolder: + type: object + required: [nullable] + properties: + maybe: {$ref: '#/components/schemas/NestedValue'} + nullable: + anyOf: [{$ref: '#/components/schemas/NestedValue'}, {type: 'null'}] + Closed: + type: object + properties: + known_field: {type: string} + additionalProperties: false + Implicit: + type: object + properties: + known_field: {type: string} + Holder: + type: object + required: [mixed, opaque, dictionaries, aliases] + properties: + mixed: {$ref: '#/components/schemas/MixedMap'} + opaque: {$ref: '#/components/schemas/OpaqueMap'} + dictionaries: {type: array, items: {$ref: '#/components/schemas/TypedMap'}} + aliases: {type: object, additionalProperties: {$ref: '#/components/schemas/DictionaryAlias'}} diff --git a/tests/generated-clients/python/README.md b/tests/generated-clients/python/README.md index ff6b508b1..cc86f0503 100644 --- a/tests/generated-clients/python/README.md +++ b/tests/generated-clients/python/README.md @@ -1,10 +1,14 @@ -# Native Python union consumers +# Native Python generated-client consumers Generation and type checking are separate from the standard-library `unittest` runner. Tests exercise generated dataclasses, aliases, policy behavior, and each transport's actual JSON request encoder with in-process transports. No test launches a generator, compiler, shell, or another runner. +Additional-property consumers cover pure and mixed objects, verbatim keys, opaque +null/number preservation, typed containers and aliases, recursive dependencies, +storage-name collisions, union boundaries and HTTP response/error conversion. + Install `httpx`, `requests`, and `pyright` in the active Python environment, then: ```sh @@ -12,7 +16,7 @@ just golden-python-clients::test httpx just golden-python-clients::test requests ``` -`prepare` generates all three policies; `build` runs strict pyright against the +`prepare` generates both fixtures under all three policies; `build` runs strict pyright against the unchanged output; `test` uses `${PYTHON:-python3}` (locally Python 3.15.0). The declared SDK minimum remains Python 3.12, which is also the type-check target. Both backends run the same contracts. Tests never patch generated source. diff --git a/tests/generated-clients/python/additional_typing_contracts.py b/tests/generated-clients/python/additional_typing_contracts.py new file mode 100644 index 000000000..67d39b557 --- /dev/null +++ b/tests/generated-clients/python/additional_typing_contracts.py @@ -0,0 +1,46 @@ +"""Public extension types retain declared payloads on both transports.""" + +from typing import Any, assert_type + +from additional_default.apis.default_api import DefaultApi +from additional_default.models import DeepValues, Holder, MixedMap, NestedValue, OpaqueMap, StorageCollision, TypedMap +from additional_default.models.dictionary_choice import dictionary_choice_from_dict +from additional_default.models import DictionaryChoice +from additional_raw.models import Choices as RawChoices +from additional_strict.models import MixedMap as StrictMixedMap +from additional_default.models import Any as OpaqueNamedAny +from additional_default.models import Event, OptionalHolder +from additional_default.models.event import event_from_dict, event_to_dict + + +def typed_extensions(wire: dict[str, object], member: NestedValue, holder: Holder) -> MixedMap: + value = MixedMap(known_field="known", additional_properties={"keep_key": member}) + assert_type(value.additional_properties, dict[str, NestedValue]) + assert_type(TypedMap.from_dict(wire).additional_properties, dict[str, NestedValue]) + assert_type(DeepValues.from_dict(wire).additional_properties, dict[str, list[dict[str, NestedValue]]]) + assert_type(StorageCollision.from_dict(wire).additional_properties_, dict[str, NestedValue]) + assert_type(OpaqueMap.from_dict(wire).additional_properties, dict[str, Any]) + assert_type(OpaqueNamedAny.from_dict(wire).additional_properties, dict[str, Any]) + assert_type(holder.aliases, dict[str, TypedMap]) + assert_type(dictionary_choice_from_dict(wire), DictionaryChoice) + return value + + +def typed_http(api: DefaultApi, value: MixedMap, dictionary: TypedMap) -> MixedMap: + assert_type(api.echo_dictionary(body=dictionary), TypedMap) + assert_type(api.echo_mixed_with_http_info(body=value).data, MixedMap) + return api.echo_mixed(body=value) + + +def policy_types(wire: dict[str, object]) -> None: + assert_type(StrictMixedMap.from_dict(wire).additional_properties["key"].wire_value, str) + assert_type(RawChoices.from_dict(wire).additional_properties, dict[str, object]) + + +def compatible_helpers(wire: dict[str, object], value: Event) -> OptionalHolder: + assert_type(event_from_dict(data=wire), Event) + assert_type(event_to_dict(obj=value), dict[str, object]) + holder = OptionalHolder(maybe=None, nullable=None) + assert_type(holder.maybe, MixedMap | None) + assert_type(holder.nullable, MixedMap | None) + return holder diff --git a/tests/generated-clients/python/pyrightconfig.httpx.json b/tests/generated-clients/python/pyrightconfig.httpx.json index f2930d072..6411a05e9 100644 --- a/tests/generated-clients/python/pyrightconfig.httpx.json +++ b/tests/generated-clients/python/pyrightconfig.httpx.json @@ -1,4 +1,4 @@ { "extends": "./pyrightconfig.json", - "extraPaths": ["generated/httpx/default", "generated/httpx/strict", "generated/httpx/raw"] + "extraPaths": ["generated/httpx/default", "generated/httpx/strict", "generated/httpx/raw", "generated/httpx/additional/default", "generated/httpx/additional/strict", "generated/httpx/additional/raw"] } diff --git a/tests/generated-clients/python/pyrightconfig.requests.json b/tests/generated-clients/python/pyrightconfig.requests.json index e1e314ec8..ffda7f66f 100644 --- a/tests/generated-clients/python/pyrightconfig.requests.json +++ b/tests/generated-clients/python/pyrightconfig.requests.json @@ -1,4 +1,4 @@ { "extends": "./pyrightconfig.json", - "extraPaths": ["generated/requests/default", "generated/requests/strict", "generated/requests/raw"] + "extraPaths": ["generated/requests/default", "generated/requests/strict", "generated/requests/raw", "generated/requests/additional/default", "generated/requests/additional/strict", "generated/requests/additional/raw"] } diff --git a/tests/generated-clients/python/tests/test_additional_properties.py b/tests/generated-clients/python/tests/test_additional_properties.py new file mode 100644 index 000000000..2671bcde8 --- /dev/null +++ b/tests/generated-clients/python/tests/test_additional_properties.py @@ -0,0 +1,254 @@ +"""Native consumers of explicit dictionary and mixed-object extensions.""" + +import importlib +import json +import unittest +from unittest.mock import patch + +from test_union import sent_json, transport + + +class AdditionalPropertiesContracts(unittest.TestCase): + def models(self, policy="default"): + return importlib.import_module(f"additional_{policy}.models") + + def test_opaque_extensions_keep_verbatim_keys_and_values(self): + wire = { + "known_field": "fixed", + "knownField": "shadow", + "extra_key": {"wire_value": "opaque", "nil": None}, + "large": 9007199254740993, + } + self.assertEqual(self.models().OpaqueMap.from_dict(wire).to_dict(), wire) + + def test_typed_extensions_keep_declared_models_and_wire_names(self): + wire = {"known_field": "fixed", "extra_key": {"wire_value": "nested"}} + value = self.models().MixedMap.from_dict(wire) + self.assertEqual(value.to_dict(), wire) + self.assertIsInstance(value.additional_properties["extra_key"], self.models().NestedValue) + + def test_pure_dictionary_exposes_normal_conversion(self): + wire = {"keep_key": {"wire_value": "nested"}} + value = self.models().TypedMap.from_dict(wire) + self.assertEqual(value.to_dict(), wire) + self.assertIsInstance(value.additional_properties["keep_key"], self.models().NestedValue) + + def test_empty_defaults_are_mutable_and_independent(self): + for policy in ("default", "strict", "raw"): + models = self.models(policy) + first, second = models.TypedMap(), models.TypedMap() + first.additional_properties["new_key"] = models.NestedValue(wire_value="new") + self.assertEqual(first.to_dict(), {"new_key": {"wire_value": "new"}}) + self.assertEqual(second.to_dict(), {}) + self.assertEqual(models.MixedMap(known_field="fixed").to_dict(), {"known_field": "fixed"}) + + def test_scalar_and_enum_values_keep_native_types_and_errors(self): + for policy in ("default", "strict", "raw"): + models = self.models(policy) + numbers = {"large": 9007199254740993, "negative": -2} + self.assertEqual(models.Numbers.from_dict(numbers).to_dict(), numbers) + value = models.StatusValues.from_dict({"status": "active"}) + self.assertIsInstance(value.additional_properties["status"], models.Status) + self.assertEqual(value.to_dict(), {"status": "active"}) + with self.assertRaises(ValueError): + models.StatusValues.from_dict({"status": "invalid"}) + + def test_from_dict_does_not_mutate_input_and_does_not_hide_model_errors(self): + models = self.models() + wire = {"known_field": "fixed", "extra": {"wire_value": "nested"}} + before = json.loads(json.dumps(wire)) + value = models.MixedMap.from_dict(wire) + self.assertEqual(wire, before) + self.assertIsInstance(wire["extra"], dict) + value.additional_properties.clear() + self.assertEqual(wire, before) + with self.assertRaises(KeyError): + models.NestedValue.from_dict({}) + with self.assertRaises(KeyError): + models.MixedMap.from_dict({"known_field": "fixed", "extra": {}}) + with self.assertRaises((AttributeError, TypeError)): + models.TypedMap.from_dict([]) + + def test_raw_values_keep_nulls_large_numbers_and_storage_looking_keys(self): + wire = {"additional_properties": None, "": [True, None], "large": 9007199254740993, "quote'\\key": {"null": None}} + for policy in ("default", "strict", "raw"): + value = self.models(policy).RawMap.from_dict(wire) + self.assertEqual(json.loads(json.dumps(value.to_dict())), wire) + self.assertEqual(value.additional_properties, wire) + + def test_model_named_any_keeps_opaque_extension_values(self): + wire = {"nil": None, "nested": {"wire_value": "opaque"}, "large": 9007199254740993} + for policy in ("default", "strict", "raw"): + self.assertEqual(self.models(policy).Any.from_dict(wire).to_dict(), wire) + + def test_nullable_and_deep_container_values_keep_declared_types(self): + for policy in ("default", "strict", "raw"): + models = self.models(policy) + nullable = models.NullableValues.from_dict({"nil": None, "model": {"wire_value": "nested"}}) + self.assertIsNone(nullable.additional_properties["nil"]) + self.assertIsInstance(nullable.additional_properties["model"], models.NestedValue) + self.assertEqual(nullable.to_dict(), {"nil": None, "model": {"wire_value": "nested"}}) + wire = {"list": [{"keep_key": {"wire_value": "nested"}}]} + value = models.DeepValues.from_dict(wire) + self.assertIsInstance(value.additional_properties["list"][0]["keep_key"], models.NestedValue) + self.assertEqual(value.to_dict(), wire) + + def test_alias_and_containing_boundaries_use_the_same_codec(self): + dictionary = {"keep_key": {"wire_value": "nested"}} + wire = {"mixed": {"known_field": "fixed", **dictionary}, "opaque": {"known_field": "fixed", "nil": None}, "dictionaries": [dictionary], "aliases": {"alias": dictionary}} + for policy in ("default", "strict", "raw"): + models = self.models(policy) + value = models.Holder.from_dict(wire) + self.assertIsInstance(value.dictionaries[0], models.TypedMap) + self.assertIsInstance(value.aliases["alias"], models.TypedMap) + self.assertEqual(json.loads(json.dumps(value.to_dict())), wire) + alias_values = models.AliasValues.from_dict(dictionary) + self.assertIsInstance(alias_values.additional_properties["keep_key"], models.NestedValue) + self.assertEqual(alias_values.to_dict(), dictionary) + + def test_recursive_dictionary_and_model_dependencies_import_and_convert(self): + for policy in ("default", "strict", "raw"): + models = self.models(policy) + wire = {"wire_value": "root", "child": {"wire_value": "leaf"}} + value = models.Recursive.from_dict(wire) + self.assertIsInstance(value.additional_properties["child"], models.Recursive) + self.assertEqual(value.to_dict(), wire) + pure = {"one": {"two": {}}} + self.assertEqual(models.PureRecursive.from_dict(pure).to_dict(), pure) + nodes = {"root": {"wire_value": "root", "children": {"leaf": {"wire_value": "leaf", "children": {}}}}} + value = models.Nodes.from_dict(nodes) + self.assertIsInstance(value.additional_properties["root"].children, models.Nodes) + self.assertEqual(value.to_dict(), nodes) + + def test_storage_name_does_not_replace_an_authored_field(self): + wire = {"additionalProperties": "known", "additional_properties": {"wire_value": "extra"}} + for policy in ("default", "strict", "raw"): + value = self.models(policy).StorageCollision.from_dict(wire) + self.assertEqual(value.additional_properties, "known") + self.assertEqual(value.additional_properties_["additional_properties"].wire_value, "extra") + self.assertEqual(value.to_dict(), wire) + + def test_storage_factory_is_not_shadowed_by_named_fields(self): + value = self.models().FactoryCollision.from_dict({"field": "known", "dict": "named", "extra": None}) + self.assertEqual(value.to_dict(), {"field": "known", "dict": "named", "extra": None}) + + def test_extra_keys_cannot_overwrite_even_absent_declared_fields(self): + models = self.models() + for value in (models.OpaqueMap(known_field="fixed"), models.OptionalKnown()): + value.additional_properties["known_field"] = "collision" + with self.assertRaisesRegex(ValueError, "collides with declared wire field"): + value.to_dict() + self.assertEqual(models.OptionalKnown.from_dict({"known_field": None, "extra": None}).to_dict(), {"extra": None}) + + def test_closed_and_implicit_objects_keep_their_existing_behavior(self): + wire = {"known_field": "fixed", "extra": None} + for name in ("Closed", "Implicit"): + value = getattr(self.models(), name).from_dict(wire) + self.assertFalse(hasattr(value, "additional_properties")) + self.assertEqual(value.to_dict(), {"known_field": "fixed"}) + + def test_union_valued_extensions_keep_each_policy(self): + wire = {"model": {"wire_value": "nested"}, "scalar": 2} + for policy in ("default", "strict", "raw"): + models = self.models(policy) + value = models.Choices.from_dict(wire) + self.assertEqual(json.loads(json.dumps(value.to_dict())), wire) + if policy != "raw": + self.assertIsInstance(value.additional_properties["model"], models.NestedValue) + else: + self.assertIsInstance(value.additional_properties["model"], dict) + models = self.models("strict") + with self.assertRaisesRegex(ValueError, "Ambiguous"): + models.AmbiguousValues.from_dict({"value": 2}) + with self.assertRaisesRegex(ValueError, "Ambiguous"): + models.AmbiguousValues(additional_properties={"value": 2}).to_dict() + + def test_tagged_payload_extensions_survive_direct_and_dictionary_conversion(self): + searching = {"type": "searching", "sequence_number": 1, "sequenceNumber": "shadow", "extra": None} + completed = {"type": "completed", "extra": {"large": 9007199254740993}} + wire = {"search": searching, "done": completed} + for policy in ("default", "strict", "raw"): + models = self.models(policy) + module = importlib.import_module(f"additional_{policy}.models.event") + value = module.event_from_dict(data=searching) + self.assertIsInstance(value, models.EventSearching) + self.assertEqual(module.event_to_dict(obj=value), searching) + value = models.EventValues.from_dict(wire) + self.assertIsInstance(value.additional_properties["done"], models.EventCompleted) + self.assertEqual(value.to_dict(), wire) + + def test_containing_optional_and_nullable_fields_keep_the_ordinary_null_policy(self): + for policy in ("default", "strict", "raw"): + models = self.models(policy) + for wire in ({}, {"maybe": None}, {"nullable": None}, {"maybe": None, "nullable": None}): + ordinary = models.LegacyOptionalHolder.from_dict(wire) + value = models.OptionalHolder.from_dict(wire) + self.assertIsNone(value.maybe) + self.assertIsNone(value.nullable) + self.assertEqual(value.to_dict(), ordinary.to_dict()) + mixed = {"known_field": "fixed", "extra": {"wire_value": "nested"}} + wire = {"maybe": mixed, "nullable": mixed} + value = models.OptionalHolder.from_dict(wire) + self.assertIsInstance(value.maybe, models.MixedMap) + self.assertEqual(value.to_dict(), wire) + + def test_dictionary_and_mixed_models_match_outbound_unions_without_serializing_to_probe(self): + wire = {"keep_key": {"wire_value": "nested"}} + for policy in ("default", "strict"): + dictionary = importlib.import_module(f"additional_{policy}.models.dictionary_choice") + mixed = importlib.import_module(f"additional_{policy}.models.mixed_choice") + models = self.models(policy) + value = dictionary.dictionary_choice_from_dict(wire) + self.assertIsInstance(value, models.TypedMap) + with patch.object(value, "to_dict", wraps=value.to_dict) as encode: + self.assertEqual(dictionary.dictionary_choice_to_dict(value), wire) + encode.assert_called_once_with() + value = models.MixedMap.from_dict({"known_field": "fixed", **wire}) + with patch.object(value, "to_dict", wraps=value.to_dict) as encode: + self.assertEqual(mixed.mixed_choice_to_dict(value), {"known_field": "fixed", **wire}) + encode.assert_called_once_with() + invalid = self.models("strict").MixedMap(known_field="fixed", additional_properties={"bad": 42}) + with self.assertRaisesRegex(ValueError, "No matching"): + importlib.import_module("additional_strict.models.mixed_choice").mixed_choice_to_dict(invalid) + + def test_http_json_requests_responses_and_typed_errors_keep_extensions(self): + reply = {"known_field": "fixed", "extra": {"wire_value": "nested"}} + for policy in ("default", "strict", "raw"): + module = importlib.import_module(f"additional_{policy}.apis.default_api") + runtime = importlib.import_module(f"additional_{policy}.runtime") + models = self.models(policy) + http, sent = transport(reply) + with runtime.Client("https://additional.test", http_client=http) as client: + value = module.DefaultApi(client).echo_mixed(body=models.MixedMap.from_dict(reply)) + self.assertEqual(sent_json(sent[0]), reply) + self.assertEqual(value.to_dict(), reply) + dictionary = {"keep_key": {"wire_value": "nested"}} + http, sent = transport(dictionary) + with runtime.Client("https://additional.test", http_client=http) as client: + value = module.DefaultApi(client).echo_dictionary(body=models.TypedMap.from_dict(dictionary)) + self.assertEqual(sent_json(sent[0]), dictionary) + self.assertEqual(value.to_dict(), dictionary) + http, sent = transport(dictionary, 400) + with runtime.Client("https://additional.test", http_client=http) as client: + with self.assertRaises(module.EchoMixedError) as caught: + module.DefaultApi(client).echo_mixed(body=models.MixedMap.from_dict(reply)) + self.assertIsInstance(caught.exception.detail, module.EchoMixedBadRequest) + self.assertEqual(caught.exception.detail.body.to_dict(), dictionary) + self.assertIs(caught.exception.detail.body, caught.exception.detail.body) + + def test_typed_http_error_decode_failures_remain_lazy_and_cached(self): + models = self.models() + runtime = importlib.import_module("additional_default.runtime") + module = importlib.import_module("additional_default.apis.default_api") + http, _ = transport({"bad": {}}, 400) + with runtime.Client("https://additional.test", http_client=http) as client: + with patch.object(models.NestedValue, "from_dict", wraps=models.NestedValue.from_dict) as decode: + with self.assertRaises(module.EchoMixedError) as caught: + module.DefaultApi(client).echo_mixed(body=models.MixedMap(known_field="fixed")) + decode.assert_not_called() + with self.assertRaises(KeyError) as first: + _ = caught.exception.detail.body + with self.assertRaises(KeyError) as second: + _ = caught.exception.detail.body + self.assertIs(first.exception, second.exception) + decode.assert_called_once_with({}) diff --git a/tests/golden/python/python-httpx/additional-properties/additional_properties_api/models/leaf_value.py.golden b/tests/golden/python/python-httpx/additional-properties/additional_properties_api/models/leaf_value.py.golden index 309555cbe..d51fa5ab5 100644 --- a/tests/golden/python/python-httpx/additional-properties/additional_properties_api/models/leaf_value.py.golden +++ b/tests/golden/python/python-httpx/additional-properties/additional_properties_api/models/leaf_value.py.golden @@ -6,14 +6,14 @@ from __future__ import annotations from dataclasses import dataclass -from typing import Any +from typing import Any as _Any @dataclass class LeafValue: """Leaf level: fixed fields plus maps with string and integer value kinds..""" attributes: dict[str, str] value: int - extras: dict[str, Any] | None = None + extras: dict[str, _Any] | None = None label: str | None = None scores: dict[str, int] | None = None diff --git a/tests/golden/python/python-httpx/additional-properties/additional_properties_api/models/middle_level.py.golden b/tests/golden/python/python-httpx/additional-properties/additional_properties_api/models/middle_level.py.golden index 4a22d89bb..56e1a3f67 100644 --- a/tests/golden/python/python-httpx/additional-properties/additional_properties_api/models/middle_level.py.golden +++ b/tests/golden/python/python-httpx/additional-properties/additional_properties_api/models/middle_level.py.golden @@ -6,7 +6,7 @@ from __future__ import annotations from dataclasses import dataclass -from typing import Any +from typing import Any as _Any from .leaf_value import LeafValue @@ -16,7 +16,7 @@ class MiddleLevel: count: int key: str nested: dict[str, LeafValue] - extras: dict[str, Any] | None = None + extras: dict[str, _Any] | None = None flags: dict[str, bool] | None = None def to_dict(self) -> dict[str, object]: diff --git a/tests/golden/python/python-httpx/additional-properties/additional_properties_api/models/root_level.py.golden b/tests/golden/python/python-httpx/additional-properties/additional_properties_api/models/root_level.py.golden index 40b931333..8afc9ba17 100644 --- a/tests/golden/python/python-httpx/additional-properties/additional_properties_api/models/root_level.py.golden +++ b/tests/golden/python/python-httpx/additional-properties/additional_properties_api/models/root_level.py.golden @@ -6,7 +6,7 @@ from __future__ import annotations from dataclasses import dataclass -from typing import Any +from typing import Any as _Any from .middle_level import MiddleLevel @@ -16,7 +16,7 @@ class RootLevel: children: dict[str, MiddleLevel] id: int name: str - extras: dict[str, Any] | None = None + extras: dict[str, _Any] | None = None labels: dict[str, list[str]] | None = None def to_dict(self) -> dict[str, object]: diff --git a/tests/golden/python/python-httpx/comprehensive-schemas/comprehensive_schema_types_example/models/_union.py.golden b/tests/golden/python/python-httpx/comprehensive-schemas/comprehensive_schema_types_example/models/_union.py.golden index e458295c7..41bb3ec23 100644 --- a/tests/golden/python/python-httpx/comprehensive-schemas/comprehensive_schema_types_example/models/_union.py.golden +++ b/tests/golden/python/python-httpx/comprehensive-schemas/comprehensive_schema_types_example/models/_union.py.golden @@ -38,11 +38,31 @@ def value_2_to_dict(value: object) -> object: return _union_support.scalar(value) def value_3_from_dict(value: object) -> object: + return _union_support.scalar(value) + +def value_3_to_dict(value: object) -> object: + return _union_support.scalar(value) + +def value_4_from_dict(value: object) -> object: + from.import object_with_additional_properties_true + return object_with_additional_properties_true.ObjectWithAdditionalPropertiesTrue.from_dict(_union_support.as_dict(value)) + +def value_4_to_dict(value: object) -> object: + return _union_support.model_to_dict(value) + +def value_5_from_dict(value: object) -> object: + from.import object_with_typed_additional_properties + return object_with_typed_additional_properties.ObjectWithTypedAdditionalProperties.from_dict(_union_support.as_dict(value)) + +def value_5_to_dict(value: object) -> object: + return _union_support.model_to_dict(value) + +def value_6_from_dict(value: object) -> object: if (_union_support.is_object(value) and (not _union_support.has(value, 'active', 'active', False) or isinstance(_union_support.get(value, 'active', 'active'), bool)) and (_union_support.has(value, 'id', 'id', True) and _union_support.number(_union_support.get(value, 'id', 'id'), True)) and (_union_support.has(value, 'name', 'name', True) and isinstance(_union_support.get(value, 'name', 'name'), str))): - return value_4_from_dict(value) + return value_7_from_dict(value) if (_union_support.is_object(value) and (not _union_support.has(value, 'metadata', 'metadata', False) or (_union_support.is_object(_union_support.get(value, 'metadata', 'metadata')) @@ -50,16 +70,16 @@ def value_3_from_dict(value: object) -> object: and (not _union_support.has(value, 'user', 'user', False) or (_union_support.is_object(_union_support.get(value, 'user', 'user')) and (_union_support.has(_union_support.get(value, 'user', 'user'), 'id', 'id', True) and _union_support.number(_union_support.get(_union_support.get(value, 'user', 'user'), 'id', 'id'), True)) and (not _union_support.has(_union_support.get(value, 'user', 'user'), 'name', 'name', False) or isinstance(_union_support.get(_union_support.get(value, 'user', 'user'), 'name', 'name'), str))))): - return value_5_from_dict(value) + return value_8_from_dict(value) return value -def value_3_to_dict(value: object) -> object: +def value_6_to_dict(value: object) -> object: if (_union_support.is_object(value) and (not _union_support.has(value, 'active', 'active', False) or isinstance(_union_support.get(value, 'active', 'active'), bool)) and (_union_support.has(value, 'id', 'id', True) and _union_support.number(_union_support.get(value, 'id', 'id'), True)) and (_union_support.has(value, 'name', 'name', True) and isinstance(_union_support.get(value, 'name', 'name'), str))): - return value_4_to_dict(value) + return value_7_to_dict(value) if (_union_support.is_object(value) and (not _union_support.has(value, 'metadata', 'metadata', False) or (_union_support.is_object(_union_support.get(value, 'metadata', 'metadata')) @@ -67,25 +87,25 @@ def value_3_to_dict(value: object) -> object: and (not _union_support.has(value, 'user', 'user', False) or (_union_support.is_object(_union_support.get(value, 'user', 'user')) and (_union_support.has(_union_support.get(value, 'user', 'user'), 'id', 'id', True) and _union_support.number(_union_support.get(_union_support.get(value, 'user', 'user'), 'id', 'id'), True)) and (not _union_support.has(_union_support.get(value, 'user', 'user'), 'name', 'name', False) or isinstance(_union_support.get(_union_support.get(value, 'user', 'user'), 'name', 'name'), str))))): - return value_5_to_dict(value) + return value_8_to_dict(value) return value -def value_4_from_dict(value: object) -> object: +def value_7_from_dict(value: object) -> object: from.import basic_object return basic_object.BasicObject.from_dict(_union_support.as_dict(value)) -def value_4_to_dict(value: object) -> object: +def value_7_to_dict(value: object) -> object: return _union_support.model_to_dict(value) -def value_5_from_dict(value: object) -> object: +def value_8_from_dict(value: object) -> object: from.import nested_object return nested_object.NestedObject.from_dict(_union_support.as_dict(value)) -def value_5_to_dict(value: object) -> object: +def value_8_to_dict(value: object) -> object: return _union_support.model_to_dict(value) -def value_6_from_dict(value: object) -> object: +def value_9_from_dict(value: object) -> object: if isinstance(value, str): return value_1_from_dict(value) @@ -93,11 +113,11 @@ def value_6_from_dict(value: object) -> object: return value_2_from_dict(value) if isinstance(value, bool): - return value_7_from_dict(value) + return value_10_from_dict(value) return value -def value_6_to_dict(value: object) -> object: +def value_9_to_dict(value: object) -> object: if isinstance(value, str): return value_1_to_dict(value) @@ -105,17 +125,17 @@ def value_6_to_dict(value: object) -> object: return value_2_to_dict(value) if isinstance(value, bool): - return value_7_to_dict(value) + return value_10_to_dict(value) return value -def value_7_from_dict(value: object) -> object: +def value_10_from_dict(value: object) -> object: return _union_support.scalar(value) -def value_7_to_dict(value: object) -> object: +def value_10_to_dict(value: object) -> object: return _union_support.scalar(value) -def value_8_from_dict(value: object) -> object: +def value_11_from_dict(value: object) -> object: if isinstance(value, str): return value_1_from_dict(value) @@ -123,14 +143,14 @@ def value_8_from_dict(value: object) -> object: and (not _union_support.has(value, 'active', 'active', False) or isinstance(_union_support.get(value, 'active', 'active'), bool)) and (_union_support.has(value, 'id', 'id', True) and _union_support.number(_union_support.get(value, 'id', 'id'), True)) and (_union_support.has(value, 'name', 'name', True) and isinstance(_union_support.get(value, 'name', 'name'), str))): - return value_4_from_dict(value) + return value_7_from_dict(value) if value is None: return None return value -def value_8_to_dict(value: object) -> object: +def value_11_to_dict(value: object) -> object: if isinstance(value, str): return value_1_to_dict(value) @@ -138,7 +158,7 @@ def value_8_to_dict(value: object) -> object: and (not _union_support.has(value, 'active', 'active', False) or isinstance(_union_support.get(value, 'active', 'active'), bool)) and (_union_support.has(value, 'id', 'id', True) and _union_support.number(_union_support.get(value, 'id', 'id'), True)) and (_union_support.has(value, 'name', 'name', True) and isinstance(_union_support.get(value, 'name', 'name'), str))): - return value_4_to_dict(value) + return value_7_to_dict(value) if value is None: return None diff --git a/tests/golden/python/python-httpx/comprehensive-schemas/comprehensive_schema_types_example/models/empty_object.py.golden b/tests/golden/python/python-httpx/comprehensive-schemas/comprehensive_schema_types_example/models/empty_object.py.golden index eff1a187d..4b9af30a1 100644 --- a/tests/golden/python/python-httpx/comprehensive-schemas/comprehensive_schema_types_example/models/empty_object.py.golden +++ b/tests/golden/python/python-httpx/comprehensive-schemas/comprehensive_schema_types_example/models/empty_object.py.golden @@ -5,8 +5,8 @@ from __future__ import annotations -from typing import Any +from typing import Any as _Any # Object without properties -type EmptyObject = (Any) +type EmptyObject = (_Any) diff --git a/tests/golden/python/python-httpx/comprehensive-schemas/comprehensive_schema_types_example/models/object_with_additional_properties_true.py.golden b/tests/golden/python/python-httpx/comprehensive-schemas/comprehensive_schema_types_example/models/object_with_additional_properties_true.py.golden index 67585b90c..2ba291623 100644 --- a/tests/golden/python/python-httpx/comprehensive-schemas/comprehensive_schema_types_example/models/object_with_additional_properties_true.py.golden +++ b/tests/golden/python/python-httpx/comprehensive-schemas/comprehensive_schema_types_example/models/object_with_additional_properties_true.py.golden @@ -5,20 +5,34 @@ from __future__ import annotations -from dataclasses import dataclass +from builtins import dict as _dict, str as _str +from dataclasses import dataclass, field as _dataclass_field +from typing import Any as _Any + +from . import _union @dataclass class ObjectWithAdditionalPropertiesTrue: """Object allowing any additional properties.""" name: str + additional_properties: _dict[_str, + _Any] = _dataclass_field(default_factory = lambda: _dict[_str, + _Any](), kw_only = True, metadata = {"openapi_additional_properties": True, "openapi_dictionary": False}) def to_dict(self) -> dict[str, object]: result: dict[str, object] = {} result["name"] = self.name + for key, item in self.additional_properties.items(): + if key in ['name']: + raise ValueError("Additional property collides with declared wire field: " + key) + + result[key] = _union.value_3_to_dict(item) + return result @classmethod def from_dict(cls, data: dict[str, object]) -> ObjectWithAdditionalPropertiesTrue: return cls( name=data["name"], # type: ignore[assignment] + additional_properties = {key: _union.value_3_from_dict(item) for key, item in data.items() if key not in ['name']}, ) diff --git a/tests/golden/python/python-httpx/comprehensive-schemas/comprehensive_schema_types_example/models/object_with_typed_additional_properties.py.golden b/tests/golden/python/python-httpx/comprehensive-schemas/comprehensive_schema_types_example/models/object_with_typed_additional_properties.py.golden index 3298f1c7b..0f6a17212 100644 --- a/tests/golden/python/python-httpx/comprehensive-schemas/comprehensive_schema_types_example/models/object_with_typed_additional_properties.py.golden +++ b/tests/golden/python/python-httpx/comprehensive-schemas/comprehensive_schema_types_example/models/object_with_typed_additional_properties.py.golden @@ -5,20 +5,34 @@ from __future__ import annotations -from dataclasses import dataclass +from builtins import dict as _dict, str as _str +from dataclasses import dataclass, field as _dataclass_field +from typing import cast + +from . import _union @dataclass class ObjectWithTypedAdditionalProperties: """Object with typed additional properties.""" name: str + additional_properties: _dict[_str, + float] = _dataclass_field(default_factory = lambda: _dict[_str, + float](), kw_only = True, metadata = {"openapi_additional_properties": True, "openapi_dictionary": False}) def to_dict(self) -> dict[str, object]: result: dict[str, object] = {} result["name"] = self.name + for key, item in self.additional_properties.items(): + if key in ['name']: + raise ValueError("Additional property collides with declared wire field: " + key) + + result[key] = _union.value_2_to_dict(item) + return result @classmethod def from_dict(cls, data: dict[str, object]) -> ObjectWithTypedAdditionalProperties: return cls( name=data["name"], # type: ignore[assignment] + additional_properties = {key: cast(float, _union.value_2_from_dict(item)) for key, item in data.items() if key not in ['name']}, ) diff --git a/tests/golden/python/python-httpx/comprehensive-schemas/comprehensive_schema_types_example/models/one_of_objects.py.golden b/tests/golden/python/python-httpx/comprehensive-schemas/comprehensive_schema_types_example/models/one_of_objects.py.golden index 198397c80..a7d76e497 100644 --- a/tests/golden/python/python-httpx/comprehensive-schemas/comprehensive_schema_types_example/models/one_of_objects.py.golden +++ b/tests/golden/python/python-httpx/comprehensive-schemas/comprehensive_schema_types_example/models/one_of_objects.py.golden @@ -19,7 +19,7 @@ type OneOfObjects = ( ) def one_of_objects_from_dict(value: object) -> OneOfObjects: - return cast(OneOfObjects, _union.value_3_from_dict(value)) + return cast(OneOfObjects, _union.value_6_from_dict(value)) def one_of_objects_to_dict(value: OneOfObjects) -> object: - return _union.value_3_to_dict(value) + return _union.value_6_to_dict(value) diff --git a/tests/golden/python/python-httpx/comprehensive-schemas/comprehensive_schema_types_example/models/one_of_types.py.golden b/tests/golden/python/python-httpx/comprehensive-schemas/comprehensive_schema_types_example/models/one_of_types.py.golden index 5e7628744..142c041cb 100644 --- a/tests/golden/python/python-httpx/comprehensive-schemas/comprehensive_schema_types_example/models/one_of_types.py.golden +++ b/tests/golden/python/python-httpx/comprehensive-schemas/comprehensive_schema_types_example/models/one_of_types.py.golden @@ -18,7 +18,7 @@ type OneOfTypes = ( ) def one_of_types_from_dict(value: object) -> OneOfTypes: - return cast(OneOfTypes, _union.value_6_from_dict(value)) + return cast(OneOfTypes, _union.value_9_from_dict(value)) def one_of_types_to_dict(value: OneOfTypes) -> object: - return _union.value_6_to_dict(value) + return _union.value_9_to_dict(value) diff --git a/tests/golden/python/python-httpx/comprehensive-schemas/comprehensive_schema_types_example/models/one_of_with_null.py.golden b/tests/golden/python/python-httpx/comprehensive-schemas/comprehensive_schema_types_example/models/one_of_with_null.py.golden index fb08327fc..0ef510275 100644 --- a/tests/golden/python/python-httpx/comprehensive-schemas/comprehensive_schema_types_example/models/one_of_with_null.py.golden +++ b/tests/golden/python/python-httpx/comprehensive-schemas/comprehensive_schema_types_example/models/one_of_with_null.py.golden @@ -19,7 +19,7 @@ type OneOfWithNull = ( ) def one_of_with_null_from_dict(value: object) -> OneOfWithNull: - return cast(OneOfWithNull, _union.value_8_from_dict(value)) + return cast(OneOfWithNull, _union.value_11_from_dict(value)) def one_of_with_null_to_dict(value: OneOfWithNull) -> object: - return _union.value_8_to_dict(value) + return _union.value_11_to_dict(value) diff --git a/tests/golden/python/python-httpx/comprehensive-schemas/comprehensive_schema_types_example/runtime/_union_support.py.golden b/tests/golden/python/python-httpx/comprehensive-schemas/comprehensive_schema_types_example/runtime/_union_support.py.golden index c24a70570..f618e5fa9 100644 --- a/tests/golden/python/python-httpx/comprehensive-schemas/comprehensive_schema_types_example/runtime/_union_support.py.golden +++ b/tests/golden/python/python-httpx/comprehensive-schemas/comprehensive_schema_types_example/runtime/_union_support.py.golden @@ -7,7 +7,7 @@ from __future__ import annotations -from dataclasses import is_dataclass +from dataclasses import fields, is_dataclass from enum import Enum from math import isfinite from typing import Protocol, TypeGuard, cast @@ -33,6 +33,24 @@ def is_list(value: object) -> TypeGuard[list[object]]: return isinstance(value, list) +def _additional_properties(value: object, dictionary_only: bool = False) -> dict[str, object] | None: + if not isinstance(value, type) and is_dataclass(value): + for field in fields(value): + if field.metadata.get("openapi_additional_properties") is True: + if not dictionary_only or field.metadata.get("openapi_dictionary") is True: + return as_dict(getattr(value, field.name)) + return None + + +def is_mapping(value: object) -> bool: + return is_dict(value) or _additional_properties(value, True) is not None + + +def mapping(value: object) -> dict[str, object]: + properties = _additional_properties(value, True) + return properties if properties is not None else as_dict(value) + + def scalar(value: object) -> object: return value.value if isinstance(value, Enum) else value @@ -58,9 +76,10 @@ def get(value: object, wire: str, field: str) -> object: def extras(value: object, known: list[str]) -> list[object]: - if not is_dict(value): - return [] - return [item for key, item in as_dict(value).items() if key not in known] + if is_dict(value): + return [item for key, item in as_dict(value).items() if key not in known] + properties = _additional_properties(value) + return list(properties.values()) if properties is not None else [] def number(value: object, integer: bool) -> bool: diff --git a/tests/golden/python/python-httpx/delete-with-response-schema/test_api_with_delete_response_schema/models/create_test_resource_request.py.golden b/tests/golden/python/python-httpx/delete-with-response-schema/test_api_with_delete_response_schema/models/create_test_resource_request.py.golden index 9efe100fd..d7d75113f 100644 --- a/tests/golden/python/python-httpx/delete-with-response-schema/test_api_with_delete_response_schema/models/create_test_resource_request.py.golden +++ b/tests/golden/python/python-httpx/delete-with-response-schema/test_api_with_delete_response_schema/models/create_test_resource_request.py.golden @@ -5,8 +5,8 @@ from __future__ import annotations -from typing import Any +from typing import Any as _Any # Request to create a new test resource (type alias - no properties) -type CreateTestResourceRequest = (Any) +type CreateTestResourceRequest = (_Any) diff --git a/tests/golden/python/python-httpx/enum-repr/enum_representation_api/runtime/_union_support.py.golden b/tests/golden/python/python-httpx/enum-repr/enum_representation_api/runtime/_union_support.py.golden index ee0abbd31..a1a48005b 100644 --- a/tests/golden/python/python-httpx/enum-repr/enum_representation_api/runtime/_union_support.py.golden +++ b/tests/golden/python/python-httpx/enum-repr/enum_representation_api/runtime/_union_support.py.golden @@ -7,7 +7,7 @@ from __future__ import annotations -from dataclasses import is_dataclass +from dataclasses import fields, is_dataclass from enum import Enum from math import isfinite from typing import Protocol, TypeGuard, cast @@ -33,6 +33,24 @@ def is_list(value: object) -> TypeGuard[list[object]]: return isinstance(value, list) +def _additional_properties(value: object, dictionary_only: bool = False) -> dict[str, object] | None: + if not isinstance(value, type) and is_dataclass(value): + for field in fields(value): + if field.metadata.get("openapi_additional_properties") is True: + if not dictionary_only or field.metadata.get("openapi_dictionary") is True: + return as_dict(getattr(value, field.name)) + return None + + +def is_mapping(value: object) -> bool: + return is_dict(value) or _additional_properties(value, True) is not None + + +def mapping(value: object) -> dict[str, object]: + properties = _additional_properties(value, True) + return properties if properties is not None else as_dict(value) + + def scalar(value: object) -> object: return value.value if isinstance(value, Enum) else value @@ -58,9 +76,10 @@ def get(value: object, wire: str, field: str) -> object: def extras(value: object, known: list[str]) -> list[object]: - if not is_dict(value): - return [] - return [item for key, item in as_dict(value).items() if key not in known] + if is_dict(value): + return [item for key, item in as_dict(value).items() if key not in known] + properties = _additional_properties(value) + return list(properties.values()) if properties is not None else [] def number(value: object, integer: bool) -> bool: diff --git a/tests/golden/python/python-httpx/object-extensions/README.md.golden b/tests/golden/python/python-httpx/object-extensions/README.md.golden new file mode 100644 index 000000000..a71356902 --- /dev/null +++ b/tests/golden/python/python-httpx/object-extensions/README.md.golden @@ -0,0 +1,7 @@ +# Python additional properties + +Generated Python SDK. + +Version: `1.0.0` + +Generated by [openapi-nexus](https://github.com/rust-codegen-group/openapi-nexus) for `python_additional_properties`. diff --git a/tests/golden/python/python-httpx/object-extensions/pyproject.toml.golden b/tests/golden/python/python-httpx/object-extensions/pyproject.toml.golden new file mode 100644 index 000000000..6b6e93b74 --- /dev/null +++ b/tests/golden/python/python-httpx/object-extensions/pyproject.toml.golden @@ -0,0 +1,10 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "python_additional_properties" +version = "1.0.0" +description = "Generated Python SDK." +requires-python = ">=3.12" +dependencies = ["httpx>=0.27"] diff --git a/tests/golden/python/python-httpx/object-extensions/python_additional_properties/__init__.py.golden b/tests/golden/python/python-httpx/object-extensions/python_additional_properties/__init__.py.golden new file mode 100644 index 000000000..c89e3c783 --- /dev/null +++ b/tests/golden/python/python-httpx/object-extensions/python_additional_properties/__init__.py.golden @@ -0,0 +1,10 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python additional properties — 1.0.0 + +from .runtime import ApiKeyAuth as ApiKeyAuth +from .runtime import Authenticator as Authenticator +from .runtime import BearerAuth as BearerAuth +from .runtime import ApiResponse as ApiResponse +from .runtime import Client as Client +from .runtime import ApiError as ApiError diff --git a/tests/golden/python/python-httpx/object-extensions/python_additional_properties/apis/__init__.py.golden b/tests/golden/python/python-httpx/object-extensions/python_additional_properties/apis/__init__.py.golden new file mode 100644 index 000000000..565d953ad --- /dev/null +++ b/tests/golden/python/python-httpx/object-extensions/python_additional_properties/apis/__init__.py.golden @@ -0,0 +1,5 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python additional properties — 1.0.0 + +from .default_api import DefaultApi as DefaultApi diff --git a/tests/golden/python/python-httpx/object-extensions/python_additional_properties/apis/default_api.py.golden b/tests/golden/python/python-httpx/object-extensions/python_additional_properties/apis/default_api.py.golden new file mode 100644 index 000000000..e29d3eaf4 --- /dev/null +++ b/tests/golden/python/python-httpx/object-extensions/python_additional_properties/apis/default_api.py.golden @@ -0,0 +1,118 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python additional properties — 1.0.0 + +from __future__ import annotations + +import json + +from collections.abc import Mapping +from typing import cast + +from ..models import _union +from ..models.dictionary_alias import DictionaryAlias +from ..models.mixed_map import MixedMap +from ..models.typed_map import TypedMap +from ..runtime.client import ApiResponse, Client +from ..runtime.errors import ApiError + +class EchoDictionaryUnexpected: + def __init__(self, status_code: int, headers: Mapping[str, str], raw_body: bytes) -> None: + self.status_code: int = status_code + self.headers: Mapping[str, str] = headers + self.raw_body: bytes = raw_body + @property + def body(self) -> bytes: + return self.raw_body + +type EchoDictionaryErrorDetail = ( + EchoDictionaryUnexpected +) + +class EchoDictionaryError(ApiError): + def __init__(self, status_code: int, status: str, body: bytes, detail: EchoDictionaryErrorDetail, headers: Mapping[str, str] | None = None, response: object | None = None) -> None: + self.detail: EchoDictionaryErrorDetail = detail + super().__init__(status_code, status, body, headers=headers, response=response) + +class EchoMixedBadRequest: + def __init__(self, status_code: int, headers: Mapping[str, str], raw_body: bytes) -> None: + self.status_code: int = status_code + self.headers: Mapping[str, str] = headers + self.raw_body: bytes = raw_body + self._body_loaded: bool = False + self._body_value: TypedMap | None = None + self._body_error: Exception | None = None + @property + def body(self) -> TypedMap: + if not self._body_loaded: + try: + self._body_value = cast(TypedMap, _union.value_15_from_dict(json.loads(self.raw_body.decode("utf-8")))) + + except Exception as exc: + self._body_error = exc + + self._body_loaded = True + + if self._body_error is not None: + raise self._body_error + + return cast(TypedMap, self._body_value) + +class EchoMixedUnexpected: + def __init__(self, status_code: int, headers: Mapping[str, str], raw_body: bytes) -> None: + self.status_code: int = status_code + self.headers: Mapping[str, str] = headers + self.raw_body: bytes = raw_body + @property + def body(self) -> bytes: + return self.raw_body + +type EchoMixedErrorDetail = ( + EchoMixedBadRequest + | EchoMixedUnexpected +) + +class EchoMixedError(ApiError): + def __init__(self, status_code: int, status: str, body: bytes, detail: EchoMixedErrorDetail, headers: Mapping[str, str] | None = None, response: object | None = None) -> None: + self.detail: EchoMixedErrorDetail = detail + super().__init__(status_code, status, body, headers=headers, response=response) + + +class DefaultApi: + def __init__(self, client: Client) -> None: + self._client = client + + def echo_dictionary_with_http_info(self, *, body: DictionaryAlias) -> ApiResponse[DictionaryAlias]: + path = "/dictionary" + headers: dict[str, str] = {} + headers["Content-Type"] = "application/json" + _union_body = _union.value_14_to_dict(body) + response = self._client.request("POST", path, headers=headers, content = json.dumps(_union_body, allow_nan = False)) + if not (200 <= response.status_code < 300): + detail = EchoDictionaryUnexpected(response.status_code, response.headers, response.content) + raise EchoDictionaryError(response.status_code, response.reason_phrase, response.content, detail, headers=response.headers, response=response) + _union_data = cast(DictionaryAlias, _union.value_14_from_dict(response.json())) + data = _union_data + return ApiResponse(data=data, status_code=response.status_code, headers=response.headers, raw=response) + + def echo_dictionary(self, *, body: DictionaryAlias) -> DictionaryAlias: + return self.echo_dictionary_with_http_info(body=body).data + + def echo_mixed_with_http_info(self, *, body: MixedMap) -> ApiResponse[MixedMap]: + path = "/mixed" + headers: dict[str, str] = {} + headers["Content-Type"] = "application/json" + _union_body = _union.value_24_to_dict(body) + response = self._client.request("POST", path, headers=headers, content = json.dumps(_union_body, allow_nan = False)) + if not (200 <= response.status_code < 300): + if response.status_code == 400: + detail = EchoMixedBadRequest(response.status_code, response.headers, response.content) + else: + detail = EchoMixedUnexpected(response.status_code, response.headers, response.content) + raise EchoMixedError(response.status_code, response.reason_phrase, response.content, detail, headers=response.headers, response=response) + _union_data = cast(MixedMap, _union.value_24_from_dict(response.json())) + data = _union_data + return ApiResponse(data=data, status_code=response.status_code, headers=response.headers, raw=response) + + def echo_mixed(self, *, body: MixedMap) -> MixedMap: + return self.echo_mixed_with_http_info(body=body).data diff --git a/tests/golden/python/python-httpx/object-extensions/python_additional_properties/models/__init__.py.golden b/tests/golden/python/python-httpx/object-extensions/python_additional_properties/models/__init__.py.golden new file mode 100644 index 000000000..6f97d0328 --- /dev/null +++ b/tests/golden/python/python-httpx/object-extensions/python_additional_properties/models/__init__.py.golden @@ -0,0 +1,40 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python additional properties — 1.0.0 + +from .alias_values import AliasValues as AliasValues +from .ambiguous_number import AmbiguousNumber as AmbiguousNumber +from .ambiguous_values import AmbiguousValues as AmbiguousValues +from .any import Any as Any +from .choices import Choices as Choices +from .closed import Closed as Closed +from .deep_values import DeepValues as DeepValues +from .dictionary_alias import DictionaryAlias as DictionaryAlias +from .dictionary_choice import DictionaryChoice as DictionaryChoice +from .event import Event as Event +from .event_completed import EventCompleted as EventCompleted +from .event_searching import EventSearching as EventSearching +from .event_values import EventValues as EventValues +from .factory_collision import FactoryCollision as FactoryCollision +from .holder import Holder as Holder +from .implicit import Implicit as Implicit +from .legacy_optional_holder import LegacyOptionalHolder as LegacyOptionalHolder +from .mixed_choice import MixedChoice as MixedChoice +from .mixed_map import MixedMap as MixedMap +from .nested_value import NestedValue as NestedValue +from .node import Node as Node +from .nodes import Nodes as Nodes +from .nullable_values import NullableValues as NullableValues +from .numbers import Numbers as Numbers +from .opaque_map import OpaqueMap as OpaqueMap +from .optional_holder import OptionalHolder as OptionalHolder +from .optional_known import OptionalKnown as OptionalKnown +from .pure_recursive import PureRecursive as PureRecursive +from .raw_map import RawMap as RawMap +from .recursive import Recursive as Recursive +from .status import Status as Status +from .status_values import StatusValues as StatusValues +from .storage_collision import StorageCollision as StorageCollision +from .typed_map import TypedMap as TypedMap +from .value_alias import ValueAlias as ValueAlias +from .value_choice import ValueChoice as ValueChoice diff --git a/tests/golden/python/python-httpx/object-extensions/python_additional_properties/models/_union.py.golden b/tests/golden/python/python-httpx/object-extensions/python_additional_properties/models/_union.py.golden new file mode 100644 index 000000000..b8d329f16 --- /dev/null +++ b/tests/golden/python/python-httpx/object-extensions/python_additional_properties/models/_union.py.golden @@ -0,0 +1,363 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python additional properties — 1.0.0 + +from __future__ import annotations + +from ..runtime import _union_support + +def value_0_from_dict(value: object) -> object: + return value_1_from_dict(value) + +def value_0_to_dict(value: object) -> object: + return value_1_to_dict(value) + +def value_1_from_dict(value: object) -> object: + from.import nested_value + return nested_value.NestedValue.from_dict(_union_support.as_dict(value)) + +def value_1_to_dict(value: object) -> object: + return _union_support.model_to_dict(value) + +def value_2_from_dict(value: object) -> object: + from.import alias_values + return alias_values.AliasValues.from_dict(_union_support.as_dict(value)) + +def value_2_to_dict(value: object) -> object: + return _union_support.model_to_dict(value) + +def value_3_from_dict(value: object) -> object: + if _union_support.number(value, True): + return value_4_from_dict(value) + + if _union_support.number(value, False): + return value_5_from_dict(value) + + return value + +def value_3_to_dict(value: object) -> object: + if _union_support.number(value, True): + return value_4_to_dict(value) + + if _union_support.number(value, False): + return value_5_to_dict(value) + + return value + +def value_4_from_dict(value: object) -> object: + return _union_support.scalar(value) + +def value_4_to_dict(value: object) -> object: + return _union_support.scalar(value) + +def value_5_from_dict(value: object) -> object: + return _union_support.scalar(value) + +def value_5_to_dict(value: object) -> object: + return _union_support.scalar(value) + +def value_6_from_dict(value: object) -> object: + from.import ambiguous_values + return ambiguous_values.AmbiguousValues.from_dict(_union_support.as_dict(value)) + +def value_6_to_dict(value: object) -> object: + return _union_support.model_to_dict(value) + +def value_7_from_dict(value: object) -> object: + return _union_support.scalar(value) + +def value_7_to_dict(value: object) -> object: + return _union_support.scalar(value) + +def value_8_from_dict(value: object) -> object: + from.import any + return any.Any.from_dict(_union_support.as_dict(value)) + +def value_8_to_dict(value: object) -> object: + return _union_support.model_to_dict(value) + +def value_9_from_dict(value: object) -> object: + if (_union_support.is_object(value) + and (_union_support.has(value, 'wire_value', 'wire_value', True) and isinstance(_union_support.get(value, 'wire_value', 'wire_value'), str))): + return value_1_from_dict(value) + + if _union_support.number(value, True): + return value_4_from_dict(value) + + return value + +def value_9_to_dict(value: object) -> object: + if (_union_support.is_object(value) + and (_union_support.has(value, 'wire_value', 'wire_value', True) and isinstance(_union_support.get(value, 'wire_value', 'wire_value'), str))): + return value_1_to_dict(value) + + if _union_support.number(value, True): + return value_4_to_dict(value) + + return value + +def value_10_from_dict(value: object) -> object: + from.import choices + return choices.Choices.from_dict(_union_support.as_dict(value)) + +def value_10_to_dict(value: object) -> object: + return _union_support.model_to_dict(value) + +def value_11_from_dict(value: object) -> object: + return [value_12_from_dict(item) for item in _union_support.as_list(value)] + +def value_11_to_dict(value: object) -> object: + return [value_12_to_dict(item) for item in _union_support.as_list(value)] + +def value_12_from_dict(value: object) -> object: + return {key: value_1_from_dict(item) for key, item in _union_support.as_dict(value).items()} + +def value_12_to_dict(value: object) -> object: + return {key: value_1_to_dict(item) for key, item in _union_support.mapping(value).items()} + +def value_13_from_dict(value: object) -> object: + from.import deep_values + return deep_values.DeepValues.from_dict(_union_support.as_dict(value)) + +def value_13_to_dict(value: object) -> object: + return _union_support.model_to_dict(value) + +def value_14_from_dict(value: object) -> object: + return value_15_from_dict(value) + +def value_14_to_dict(value: object) -> object: + return value_15_to_dict(value) + +def value_15_from_dict(value: object) -> object: + from.import typed_map + return typed_map.TypedMap.from_dict(_union_support.as_dict(value)) + +def value_15_to_dict(value: object) -> object: + return _union_support.model_to_dict(value) + +def value_16_from_dict(value: object) -> object: + if (_union_support.is_dict(value) and all((_union_support.is_object(_item) + and (_union_support.has(_item, 'wire_value', 'wire_value', True) and isinstance(_union_support.get(_item, 'wire_value', 'wire_value'), str))) for _item in _union_support.as_dict(value).values())): + return value_15_from_dict(value) + + if _union_support.number(value, True): + return value_4_from_dict(value) + + return value + +def value_16_to_dict(value: object) -> object: + if (_union_support.is_mapping(value) and all((_union_support.is_object(_item) + and (_union_support.has(_item, 'wire_value', 'wire_value', True) and isinstance(_union_support.get(_item, 'wire_value', 'wire_value'), str))) for _item in _union_support.mapping(value).values())): + return value_15_to_dict(value) + + if _union_support.number(value, True): + return value_4_to_dict(value) + + return value + +def value_17_from_dict(value: object) -> object: + from.import event_searching + return event_searching.EventSearching.from_dict(_union_support.as_dict(value)) + +def value_17_to_dict(value: object) -> object: + return _union_support.model_to_dict(value) + +def value_18_from_dict(value: object) -> object: + from.import event_completed + return event_completed.EventCompleted.from_dict(_union_support.as_dict(value)) + +def value_18_to_dict(value: object) -> object: + return _union_support.model_to_dict(value) + +def value_19_from_dict(value: object) -> object: + data = _union_support.as_dict(value) + if data.get('type') == 'searching': + return value_17_from_dict(value) + + if data.get('type') == 'completed': + return value_18_from_dict(value) + + raise ValueError("No matching tagged union variant") + +def value_19_to_dict(value: object) -> object: + if ((_union_support.is_object(value) + and (_union_support.has(value, 'sequence_number', 'sequence_number', True) and _union_support.number(_union_support.get(value, 'sequence_number', 'sequence_number'), True)) + and (_union_support.has(value, 'type', 'type_', True) and _union_support.equal(_union_support.get(value, 'type', 'type_'), 'searching')) + and all(True for _item in _union_support.extras(value, ['sequence_number', 'type'])))): + result = dict(_union_support.as_dict(value_17_to_dict(value))) + result['type'] = 'searching' + return result + + if ((_union_support.is_object(value) + and (_union_support.has(value, 'type', 'type_', True) and _union_support.equal(_union_support.get(value, 'type', 'type_'), 'completed')) + and all(True for _item in _union_support.extras(value, ['type'])))): + result = dict(_union_support.as_dict(value_18_to_dict(value))) + result['type'] = 'completed' + return result + + raise ValueError("No matching tagged union variant") + +def value_20_from_dict(value: object) -> object: + from.import event_values + return event_values.EventValues.from_dict(_union_support.as_dict(value)) + +def value_20_to_dict(value: object) -> object: + return _union_support.model_to_dict(value) + +def value_21_from_dict(value: object) -> object: + from.import factory_collision + return factory_collision.FactoryCollision.from_dict(_union_support.as_dict(value)) + +def value_21_to_dict(value: object) -> object: + return _union_support.model_to_dict(value) + +def value_22_from_dict(value: object) -> object: + return {key: value_14_from_dict(item) for key, item in _union_support.as_dict(value).items()} + +def value_22_to_dict(value: object) -> object: + return {key: value_14_to_dict(item) for key, item in _union_support.mapping(value).items()} + +def value_23_from_dict(value: object) -> object: + return [value_15_from_dict(item) for item in _union_support.as_list(value)] + +def value_23_to_dict(value: object) -> object: + return [value_15_to_dict(item) for item in _union_support.as_list(value)] + +def value_24_from_dict(value: object) -> object: + from.import mixed_map + return mixed_map.MixedMap.from_dict(_union_support.as_dict(value)) + +def value_24_to_dict(value: object) -> object: + return _union_support.model_to_dict(value) + +def value_25_from_dict(value: object) -> object: + from.import opaque_map + return opaque_map.OpaqueMap.from_dict(_union_support.as_dict(value)) + +def value_25_to_dict(value: object) -> object: + return _union_support.model_to_dict(value) + +def value_26_from_dict(value: object) -> object: + if (_union_support.is_object(value) + and (_union_support.has(value, 'known_field', 'known_field', True) and isinstance(_union_support.get(value, 'known_field', 'known_field'), str)) + and all((_union_support.is_object(_item) + and (_union_support.has(_item, 'wire_value', 'wire_value', True) and isinstance(_union_support.get(_item, 'wire_value', 'wire_value'), str))) for _item in _union_support.extras(value, ['known_field']))): + return value_24_from_dict(value) + + if isinstance(value, str): + return value_27_from_dict(value) + + return value + +def value_26_to_dict(value: object) -> object: + if (_union_support.is_object(value) + and (_union_support.has(value, 'known_field', 'known_field', True) and isinstance(_union_support.get(value, 'known_field', 'known_field'), str)) + and all((_union_support.is_object(_item) + and (_union_support.has(_item, 'wire_value', 'wire_value', True) and isinstance(_union_support.get(_item, 'wire_value', 'wire_value'), str))) for _item in _union_support.extras(value, ['known_field']))): + return value_24_to_dict(value) + + if isinstance(value, str): + return value_27_to_dict(value) + + return value + +def value_27_from_dict(value: object) -> object: + return _union_support.scalar(value) + +def value_27_to_dict(value: object) -> object: + return _union_support.scalar(value) + +def value_28_from_dict(value: object) -> object: + from.import nodes + return nodes.Nodes.from_dict(_union_support.as_dict(value)) + +def value_28_to_dict(value: object) -> object: + return _union_support.model_to_dict(value) + +def value_29_from_dict(value: object) -> object: + from.import node + return node.Node.from_dict(_union_support.as_dict(value)) + +def value_29_to_dict(value: object) -> object: + return _union_support.model_to_dict(value) + +def value_30_from_dict(value: object) -> object: + return None if value is None else value_1_from_dict(value) + +def value_30_to_dict(value: object) -> object: + return None if value is None else value_1_to_dict(value) + +def value_31_from_dict(value: object) -> object: + from.import nullable_values + return nullable_values.NullableValues.from_dict(_union_support.as_dict(value)) + +def value_31_to_dict(value: object) -> object: + return _union_support.model_to_dict(value) + +def value_32_from_dict(value: object) -> object: + return _union_support.scalar(value) + +def value_32_to_dict(value: object) -> object: + return _union_support.scalar(value) + +def value_33_from_dict(value: object) -> object: + from.import numbers + return numbers.Numbers.from_dict(_union_support.as_dict(value)) + +def value_33_to_dict(value: object) -> object: + return _union_support.model_to_dict(value) + +def value_34_from_dict(value: object) -> object: + return None if value is None else value_24_from_dict(value) + +def value_34_to_dict(value: object) -> object: + return None if value is None else value_24_to_dict(value) + +def value_35_from_dict(value: object) -> object: + from.import optional_known + return optional_known.OptionalKnown.from_dict(_union_support.as_dict(value)) + +def value_35_to_dict(value: object) -> object: + return _union_support.model_to_dict(value) + +def value_36_from_dict(value: object) -> object: + from.import pure_recursive + return pure_recursive.PureRecursive.from_dict(_union_support.as_dict(value)) + +def value_36_to_dict(value: object) -> object: + return _union_support.model_to_dict(value) + +def value_37_from_dict(value: object) -> object: + from.import raw_map + return raw_map.RawMap.from_dict(_union_support.as_dict(value)) + +def value_37_to_dict(value: object) -> object: + return _union_support.model_to_dict(value) + +def value_38_from_dict(value: object) -> object: + from.import recursive + return recursive.Recursive.from_dict(_union_support.as_dict(value)) + +def value_38_to_dict(value: object) -> object: + return _union_support.model_to_dict(value) + +def value_39_from_dict(value: object) -> object: + from.import status + return status.Status(value) + +def value_39_to_dict(value: object) -> object: + return _union_support.scalar(value) + +def value_40_from_dict(value: object) -> object: + from.import status_values + return status_values.StatusValues.from_dict(_union_support.as_dict(value)) + +def value_40_to_dict(value: object) -> object: + return _union_support.model_to_dict(value) + +def value_41_from_dict(value: object) -> object: + from.import storage_collision + return storage_collision.StorageCollision.from_dict(_union_support.as_dict(value)) + +def value_41_to_dict(value: object) -> object: + return _union_support.model_to_dict(value) diff --git a/tests/golden/python/python-httpx/object-extensions/python_additional_properties/models/alias_values.py.golden b/tests/golden/python/python-httpx/object-extensions/python_additional_properties/models/alias_values.py.golden new file mode 100644 index 000000000..c3442b63d --- /dev/null +++ b/tests/golden/python/python-httpx/object-extensions/python_additional_properties/models/alias_values.py.golden @@ -0,0 +1,31 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python additional properties — 1.0.0 + +from __future__ import annotations + +from builtins import dict as _dict, str as _str +from dataclasses import dataclass, field as _dataclass_field +from typing import cast + +from . import _union +from .value_alias import ValueAlias + +@dataclass +class AliasValues: + additional_properties: _dict[_str, + ValueAlias] = _dataclass_field(default_factory = lambda: _dict[_str, + ValueAlias](), kw_only = True, metadata = {"openapi_additional_properties": True, "openapi_dictionary": True}) + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + for key, item in self.additional_properties.items(): + result[key] = _union.value_0_to_dict(item) + + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> AliasValues: + return cls( + additional_properties = {key: cast(ValueAlias, _union.value_0_from_dict(item)) for key, item in data.items() }, + ) diff --git a/tests/golden/python/python-httpx/object-extensions/python_additional_properties/models/ambiguous_number.py.golden b/tests/golden/python/python-httpx/object-extensions/python_additional_properties/models/ambiguous_number.py.golden new file mode 100644 index 000000000..f45cff901 --- /dev/null +++ b/tests/golden/python/python-httpx/object-extensions/python_additional_properties/models/ambiguous_number.py.golden @@ -0,0 +1,20 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python additional properties — 1.0.0 + +from __future__ import annotations + +from typing import cast + +from . import _union + +type AmbiguousNumber = ( + int + | float +) + +def ambiguous_number_from_dict(value: object) -> AmbiguousNumber: + return cast(AmbiguousNumber, _union.value_3_from_dict(value)) + +def ambiguous_number_to_dict(value: AmbiguousNumber) -> object: + return _union.value_3_to_dict(value) diff --git a/tests/golden/python/python-httpx/object-extensions/python_additional_properties/models/ambiguous_values.py.golden b/tests/golden/python/python-httpx/object-extensions/python_additional_properties/models/ambiguous_values.py.golden new file mode 100644 index 000000000..39e6a7bc5 --- /dev/null +++ b/tests/golden/python/python-httpx/object-extensions/python_additional_properties/models/ambiguous_values.py.golden @@ -0,0 +1,31 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python additional properties — 1.0.0 + +from __future__ import annotations + +from builtins import dict as _dict, str as _str +from dataclasses import dataclass, field as _dataclass_field +from typing import cast + +from . import _union +from .ambiguous_number import AmbiguousNumber + +@dataclass +class AmbiguousValues: + additional_properties: _dict[_str, + AmbiguousNumber] = _dataclass_field(default_factory = lambda: _dict[_str, + AmbiguousNumber](), kw_only = True, metadata = {"openapi_additional_properties": True, "openapi_dictionary": True}) + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + for key, item in self.additional_properties.items(): + result[key] = _union.value_3_to_dict(item) + + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> AmbiguousValues: + return cls( + additional_properties = {key: cast(AmbiguousNumber, _union.value_3_from_dict(item)) for key, item in data.items() }, + ) diff --git a/tests/golden/python/python-httpx/object-extensions/python_additional_properties/models/any.py.golden b/tests/golden/python/python-httpx/object-extensions/python_additional_properties/models/any.py.golden new file mode 100644 index 000000000..a4bbc57c0 --- /dev/null +++ b/tests/golden/python/python-httpx/object-extensions/python_additional_properties/models/any.py.golden @@ -0,0 +1,30 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python additional properties — 1.0.0 + +from __future__ import annotations + +from builtins import dict as _dict, str as _str +from dataclasses import dataclass, field as _dataclass_field +from typing import Any as _Any + +from . import _union + +@dataclass +class Any: + additional_properties: _dict[_str, + _Any] = _dataclass_field(default_factory = lambda: _dict[_str, + _Any](), kw_only = True, metadata = {"openapi_additional_properties": True, "openapi_dictionary": True}) + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + for key, item in self.additional_properties.items(): + result[key] = _union.value_7_to_dict(item) + + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> Any: + return cls( + additional_properties = {key: _union.value_7_from_dict(item) for key, item in data.items() }, + ) diff --git a/tests/golden/python/python-httpx/object-extensions/python_additional_properties/models/choices.py.golden b/tests/golden/python/python-httpx/object-extensions/python_additional_properties/models/choices.py.golden new file mode 100644 index 000000000..258771e5b --- /dev/null +++ b/tests/golden/python/python-httpx/object-extensions/python_additional_properties/models/choices.py.golden @@ -0,0 +1,31 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python additional properties — 1.0.0 + +from __future__ import annotations + +from builtins import dict as _dict, str as _str +from dataclasses import dataclass, field as _dataclass_field +from typing import cast + +from . import _union +from .value_choice import ValueChoice + +@dataclass +class Choices: + additional_properties: _dict[_str, + ValueChoice] = _dataclass_field(default_factory = lambda: _dict[_str, + ValueChoice](), kw_only = True, metadata = {"openapi_additional_properties": True, "openapi_dictionary": True}) + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + for key, item in self.additional_properties.items(): + result[key] = _union.value_9_to_dict(item) + + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> Choices: + return cls( + additional_properties = {key: cast(ValueChoice, _union.value_9_from_dict(item)) for key, item in data.items() }, + ) diff --git a/tests/golden/python/python-httpx/object-extensions/python_additional_properties/models/closed.py.golden b/tests/golden/python/python-httpx/object-extensions/python_additional_properties/models/closed.py.golden new file mode 100644 index 000000000..270c76041 --- /dev/null +++ b/tests/golden/python/python-httpx/object-extensions/python_additional_properties/models/closed.py.golden @@ -0,0 +1,23 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python additional properties — 1.0.0 + +from __future__ import annotations + +from dataclasses import dataclass + +@dataclass +class Closed: + known_field: str | None = None + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + if self.known_field is not None: + result["known_field"] = self.known_field + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> Closed: + return cls( + known_field=data.get("known_field"), # type: ignore[assignment] + ) diff --git a/tests/golden/python/python-httpx/object-extensions/python_additional_properties/models/deep_values.py.golden b/tests/golden/python/python-httpx/object-extensions/python_additional_properties/models/deep_values.py.golden new file mode 100644 index 000000000..c8ab8b5db --- /dev/null +++ b/tests/golden/python/python-httpx/object-extensions/python_additional_properties/models/deep_values.py.golden @@ -0,0 +1,33 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python additional properties — 1.0.0 + +from __future__ import annotations + +from builtins import dict as _dict, str as _str +from dataclasses import dataclass, field as _dataclass_field +from typing import cast + +from . import _union +from .nested_value import NestedValue + +@dataclass +class DeepValues: + additional_properties: _dict[_str, + list[dict[str, NestedValue]]] = _dataclass_field(default_factory = lambda: _dict[_str, + list[dict[str, + NestedValue]]](), kw_only = True, metadata = {"openapi_additional_properties": True, "openapi_dictionary": True}) + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + for key, item in self.additional_properties.items(): + result[key] = _union.value_11_to_dict(item) + + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> DeepValues: + return cls( + additional_properties = {key: cast(list[dict[str, + NestedValue]], _union.value_11_from_dict(item)) for key, item in data.items() }, + ) diff --git a/tests/golden/python/python-httpx/object-extensions/python_additional_properties/models/dictionary_alias.py.golden b/tests/golden/python/python-httpx/object-extensions/python_additional_properties/models/dictionary_alias.py.golden new file mode 100644 index 000000000..e915ac108 --- /dev/null +++ b/tests/golden/python/python-httpx/object-extensions/python_additional_properties/models/dictionary_alias.py.golden @@ -0,0 +1,9 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python additional properties — 1.0.0 + +from __future__ import annotations + +from .typed_map import TypedMap + +type DictionaryAlias = (TypedMap) diff --git a/tests/golden/python/python-httpx/object-extensions/python_additional_properties/models/dictionary_choice.py.golden b/tests/golden/python/python-httpx/object-extensions/python_additional_properties/models/dictionary_choice.py.golden new file mode 100644 index 000000000..6cbe6b270 --- /dev/null +++ b/tests/golden/python/python-httpx/object-extensions/python_additional_properties/models/dictionary_choice.py.golden @@ -0,0 +1,21 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python additional properties — 1.0.0 + +from __future__ import annotations + +from typing import cast + +from . import _union +from .typed_map import TypedMap + +type DictionaryChoice = ( + TypedMap + | int +) + +def dictionary_choice_from_dict(value: object) -> DictionaryChoice: + return cast(DictionaryChoice, _union.value_16_from_dict(value)) + +def dictionary_choice_to_dict(value: DictionaryChoice) -> object: + return _union.value_16_to_dict(value) diff --git a/tests/golden/python/python-httpx/object-extensions/python_additional_properties/models/event.py.golden b/tests/golden/python/python-httpx/object-extensions/python_additional_properties/models/event.py.golden new file mode 100644 index 000000000..babf58ca2 --- /dev/null +++ b/tests/golden/python/python-httpx/object-extensions/python_additional_properties/models/event.py.golden @@ -0,0 +1,36 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python additional properties — 1.0.0 + +from __future__ import annotations + +from .event_completed import EventCompleted +from .event_searching import EventSearching + +# Discriminator: type (internal). + +type Event = ( + EventSearching + | EventCompleted +) + + +def event_from_dict(data: dict[str, object]) -> Event: + _tag = data["type"] + if _tag == "searching": + return EventSearching.from_dict(data) + elif _tag == "completed": + return EventCompleted.from_dict(data) + raise ValueError(f"Unknown discriminator value for Event: {data}") + + +def event_to_dict(obj: Event) -> dict[str, object]: + if isinstance(obj, EventSearching): + result = obj.to_dict() + result["type"] = "searching" + return result + elif isinstance(obj, EventCompleted): + result = obj.to_dict() + result["type"] = "completed" + return result + raise ValueError(f"Unknown variant for Event: {type(obj)}") diff --git a/tests/golden/python/python-httpx/object-extensions/python_additional_properties/models/event_completed.py.golden b/tests/golden/python/python-httpx/object-extensions/python_additional_properties/models/event_completed.py.golden new file mode 100644 index 000000000..2b27f9a2a --- /dev/null +++ b/tests/golden/python/python-httpx/object-extensions/python_additional_properties/models/event_completed.py.golden @@ -0,0 +1,36 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python additional properties — 1.0.0 + +from __future__ import annotations + +from builtins import dict as _dict, str as _str +from dataclasses import dataclass, field as _dataclass_field +from typing import Any as _Any, Literal + +from . import _union + +@dataclass +class EventCompleted: + type_: Literal["completed"] + additional_properties: _dict[_str, + _Any] = _dataclass_field(default_factory = lambda: _dict[_str, + _Any](), kw_only = True, metadata = {"openapi_additional_properties": True, "openapi_dictionary": False}) + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + result["type"] = self.type_ + for key, item in self.additional_properties.items(): + if key in ['type']: + raise ValueError("Additional property collides with declared wire field: " + key) + + result[key] = _union.value_7_to_dict(item) + + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> EventCompleted: + return cls( + type_=data["type"], # type: ignore[assignment] + additional_properties = {key: _union.value_7_from_dict(item) for key, item in data.items() if key not in ['type']}, + ) diff --git a/tests/golden/python/python-httpx/object-extensions/python_additional_properties/models/event_searching.py.golden b/tests/golden/python/python-httpx/object-extensions/python_additional_properties/models/event_searching.py.golden new file mode 100644 index 000000000..0a47ac9c3 --- /dev/null +++ b/tests/golden/python/python-httpx/object-extensions/python_additional_properties/models/event_searching.py.golden @@ -0,0 +1,39 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python additional properties — 1.0.0 + +from __future__ import annotations + +from builtins import dict as _dict, str as _str +from dataclasses import dataclass, field as _dataclass_field +from typing import Any as _Any, Literal + +from . import _union + +@dataclass +class EventSearching: + sequence_number: int + type_: Literal["searching"] + additional_properties: _dict[_str, + _Any] = _dataclass_field(default_factory = lambda: _dict[_str, + _Any](), kw_only = True, metadata = {"openapi_additional_properties": True, "openapi_dictionary": False}) + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + result["sequence_number"] = self.sequence_number + result["type"] = self.type_ + for key, item in self.additional_properties.items(): + if key in ['sequence_number', 'type']: + raise ValueError("Additional property collides with declared wire field: " + key) + + result[key] = _union.value_7_to_dict(item) + + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> EventSearching: + return cls( + sequence_number=data["sequence_number"], # type: ignore[assignment] + type_=data["type"], # type: ignore[assignment] + additional_properties = {key: _union.value_7_from_dict(item) for key, item in data.items() if key not in ['sequence_number', 'type']}, + ) diff --git a/tests/golden/python/python-httpx/object-extensions/python_additional_properties/models/event_values.py.golden b/tests/golden/python/python-httpx/object-extensions/python_additional_properties/models/event_values.py.golden new file mode 100644 index 000000000..e1e450460 --- /dev/null +++ b/tests/golden/python/python-httpx/object-extensions/python_additional_properties/models/event_values.py.golden @@ -0,0 +1,31 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python additional properties — 1.0.0 + +from __future__ import annotations + +from builtins import dict as _dict, str as _str +from dataclasses import dataclass, field as _dataclass_field +from typing import cast + +from . import _union +from .event import Event + +@dataclass +class EventValues: + additional_properties: _dict[_str, + Event] = _dataclass_field(default_factory = lambda: _dict[_str, + Event](), kw_only = True, metadata = {"openapi_additional_properties": True, "openapi_dictionary": True}) + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + for key, item in self.additional_properties.items(): + result[key] = _union.value_19_to_dict(item) + + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> EventValues: + return cls( + additional_properties = {key: cast(Event, _union.value_19_from_dict(item)) for key, item in data.items() }, + ) diff --git a/tests/golden/python/python-httpx/object-extensions/python_additional_properties/models/factory_collision.py.golden b/tests/golden/python/python-httpx/object-extensions/python_additional_properties/models/factory_collision.py.golden new file mode 100644 index 000000000..a6ec404f1 --- /dev/null +++ b/tests/golden/python/python-httpx/object-extensions/python_additional_properties/models/factory_collision.py.golden @@ -0,0 +1,41 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python additional properties — 1.0.0 + +from __future__ import annotations + +from builtins import dict as _dict, str as _str +from dataclasses import dataclass, field as _dataclass_field +from typing import Any as _Any + +from . import _union + +@dataclass +class FactoryCollision: + dict: str | None = None + field: str | None = None + additional_properties: _dict[_str, + _Any] = _dataclass_field(default_factory = lambda: _dict[_str, + _Any](), kw_only = True, metadata = {"openapi_additional_properties": True, "openapi_dictionary": False}) + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + if self.dict is not None: + result["dict"] = self.dict + if self.field is not None: + result["field"] = self.field + for key, item in self.additional_properties.items(): + if key in ['dict', 'field']: + raise ValueError("Additional property collides with declared wire field: " + key) + + result[key] = _union.value_7_to_dict(item) + + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> FactoryCollision: + return cls( + dict=data.get("dict"), # type: ignore[assignment] + field=data.get("field"), # type: ignore[assignment] + additional_properties = {key: _union.value_7_from_dict(item) for key, item in data.items() if key not in ['dict', 'field']}, + ) diff --git a/tests/golden/python/python-httpx/object-extensions/python_additional_properties/models/holder.py.golden b/tests/golden/python/python-httpx/object-extensions/python_additional_properties/models/holder.py.golden new file mode 100644 index 000000000..1924f47a2 --- /dev/null +++ b/tests/golden/python/python-httpx/object-extensions/python_additional_properties/models/holder.py.golden @@ -0,0 +1,38 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python additional properties — 1.0.0 + +from __future__ import annotations + +from dataclasses import dataclass +from typing import cast + +from . import _union +from .dictionary_alias import DictionaryAlias +from .mixed_map import MixedMap +from .opaque_map import OpaqueMap +from .typed_map import TypedMap + +@dataclass +class Holder: + aliases: dict[str, DictionaryAlias] + dictionaries: list[TypedMap] + mixed: MixedMap + opaque: OpaqueMap + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + result['aliases'] = _union.value_22_to_dict(self.aliases) + result['dictionaries'] = _union.value_23_to_dict(self.dictionaries) + result['mixed'] = _union.value_24_to_dict(self.mixed) + result['opaque'] = _union.value_25_to_dict(self.opaque) + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> Holder: + return cls( + aliases = cast(dict[str, DictionaryAlias], _union.value_22_from_dict(data['aliases'])), + dictionaries = cast(list[TypedMap], _union.value_23_from_dict(data['dictionaries'])), + mixed = cast(MixedMap, _union.value_24_from_dict(data['mixed'])), + opaque = cast(OpaqueMap, _union.value_25_from_dict(data['opaque'])), + ) diff --git a/tests/golden/python/python-httpx/object-extensions/python_additional_properties/models/implicit.py.golden b/tests/golden/python/python-httpx/object-extensions/python_additional_properties/models/implicit.py.golden new file mode 100644 index 000000000..6807c209d --- /dev/null +++ b/tests/golden/python/python-httpx/object-extensions/python_additional_properties/models/implicit.py.golden @@ -0,0 +1,23 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python additional properties — 1.0.0 + +from __future__ import annotations + +from dataclasses import dataclass + +@dataclass +class Implicit: + known_field: str | None = None + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + if self.known_field is not None: + result["known_field"] = self.known_field + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> Implicit: + return cls( + known_field=data.get("known_field"), # type: ignore[assignment] + ) diff --git a/tests/golden/python/python-httpx/object-extensions/python_additional_properties/models/legacy_optional_holder.py.golden b/tests/golden/python/python-httpx/object-extensions/python_additional_properties/models/legacy_optional_holder.py.golden new file mode 100644 index 000000000..497c49921 --- /dev/null +++ b/tests/golden/python/python-httpx/object-extensions/python_additional_properties/models/legacy_optional_holder.py.golden @@ -0,0 +1,29 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python additional properties — 1.0.0 + +from __future__ import annotations + +from dataclasses import dataclass + +from .nested_value import NestedValue + +@dataclass +class LegacyOptionalHolder: + maybe: NestedValue | None = None + nullable: NestedValue | None = None + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + if self.maybe is not None: + result["maybe"] = self.maybe.to_dict() + if self.nullable is not None: + result["nullable"] = self.nullable.to_dict() + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> LegacyOptionalHolder: + return cls( + maybe=NestedValue.from_dict(data.get("maybe")) if data.get("maybe") is not None else None, # type: ignore[arg-type] + nullable=NestedValue.from_dict(data.get("nullable")) if data.get("nullable") is not None else None, # type: ignore[arg-type] + ) diff --git a/tests/golden/python/python-httpx/object-extensions/python_additional_properties/models/mixed_choice.py.golden b/tests/golden/python/python-httpx/object-extensions/python_additional_properties/models/mixed_choice.py.golden new file mode 100644 index 000000000..192de30e8 --- /dev/null +++ b/tests/golden/python/python-httpx/object-extensions/python_additional_properties/models/mixed_choice.py.golden @@ -0,0 +1,21 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python additional properties — 1.0.0 + +from __future__ import annotations + +from typing import cast + +from . import _union +from .mixed_map import MixedMap + +type MixedChoice = ( + MixedMap + | str +) + +def mixed_choice_from_dict(value: object) -> MixedChoice: + return cast(MixedChoice, _union.value_26_from_dict(value)) + +def mixed_choice_to_dict(value: MixedChoice) -> object: + return _union.value_26_to_dict(value) diff --git a/tests/golden/python/python-httpx/object-extensions/python_additional_properties/models/mixed_map.py.golden b/tests/golden/python/python-httpx/object-extensions/python_additional_properties/models/mixed_map.py.golden new file mode 100644 index 000000000..67210e724 --- /dev/null +++ b/tests/golden/python/python-httpx/object-extensions/python_additional_properties/models/mixed_map.py.golden @@ -0,0 +1,37 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python additional properties — 1.0.0 + +from __future__ import annotations + +from builtins import dict as _dict, str as _str +from dataclasses import dataclass, field as _dataclass_field +from typing import cast + +from . import _union +from .nested_value import NestedValue + +@dataclass +class MixedMap: + known_field: str + additional_properties: _dict[_str, + NestedValue] = _dataclass_field(default_factory = lambda: _dict[_str, + NestedValue](), kw_only = True, metadata = {"openapi_additional_properties": True, "openapi_dictionary": False}) + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + result["known_field"] = self.known_field + for key, item in self.additional_properties.items(): + if key in ['known_field']: + raise ValueError("Additional property collides with declared wire field: " + key) + + result[key] = _union.value_1_to_dict(item) + + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> MixedMap: + return cls( + known_field=data["known_field"], # type: ignore[assignment] + additional_properties = {key: cast(NestedValue, _union.value_1_from_dict(item)) for key, item in data.items() if key not in ['known_field']}, + ) diff --git a/tests/golden/python/python-httpx/object-extensions/python_additional_properties/models/nested_value.py.golden b/tests/golden/python/python-httpx/object-extensions/python_additional_properties/models/nested_value.py.golden new file mode 100644 index 000000000..69524d09c --- /dev/null +++ b/tests/golden/python/python-httpx/object-extensions/python_additional_properties/models/nested_value.py.golden @@ -0,0 +1,22 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python additional properties — 1.0.0 + +from __future__ import annotations + +from dataclasses import dataclass + +@dataclass +class NestedValue: + wire_value: str + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + result["wire_value"] = self.wire_value + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> NestedValue: + return cls( + wire_value=data["wire_value"], # type: ignore[assignment] + ) diff --git a/tests/golden/python/python-httpx/object-extensions/python_additional_properties/models/node.py.golden b/tests/golden/python/python-httpx/object-extensions/python_additional_properties/models/node.py.golden new file mode 100644 index 000000000..18535f708 --- /dev/null +++ b/tests/golden/python/python-httpx/object-extensions/python_additional_properties/models/node.py.golden @@ -0,0 +1,28 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python additional properties — 1.0.0 + +from __future__ import annotations + +from dataclasses import dataclass +from typing import cast + +from . import _union, nodes + +@dataclass +class Node: + children: nodes.Nodes + wire_value: str + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + result['children'] = _union.value_28_to_dict(self.children) + result["wire_value"] = self.wire_value + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> Node: + return cls( + children = cast(nodes.Nodes, _union.value_28_from_dict(data['children'])), + wire_value=data["wire_value"], # type: ignore[assignment] + ) diff --git a/tests/golden/python/python-httpx/object-extensions/python_additional_properties/models/nodes.py.golden b/tests/golden/python/python-httpx/object-extensions/python_additional_properties/models/nodes.py.golden new file mode 100644 index 000000000..704a8c69b --- /dev/null +++ b/tests/golden/python/python-httpx/object-extensions/python_additional_properties/models/nodes.py.golden @@ -0,0 +1,30 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python additional properties — 1.0.0 + +from __future__ import annotations + +from builtins import dict as _dict, str as _str +from dataclasses import dataclass, field as _dataclass_field +from typing import cast + +from . import _union, node + +@dataclass +class Nodes: + additional_properties: _dict[_str, + node.Node] = _dataclass_field(default_factory = lambda: _dict[_str, + node.Node](), kw_only = True, metadata = {"openapi_additional_properties": True, "openapi_dictionary": True}) + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + for key, item in self.additional_properties.items(): + result[key] = _union.value_29_to_dict(item) + + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> Nodes: + return cls( + additional_properties = {key: cast(node.Node, _union.value_29_from_dict(item)) for key, item in data.items() }, + ) diff --git a/tests/golden/python/python-httpx/object-extensions/python_additional_properties/models/nullable_values.py.golden b/tests/golden/python/python-httpx/object-extensions/python_additional_properties/models/nullable_values.py.golden new file mode 100644 index 000000000..48fd21e77 --- /dev/null +++ b/tests/golden/python/python-httpx/object-extensions/python_additional_properties/models/nullable_values.py.golden @@ -0,0 +1,33 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python additional properties — 1.0.0 + +from __future__ import annotations + +from builtins import dict as _dict, str as _str +from dataclasses import dataclass, field as _dataclass_field +from typing import cast + +from . import _union +from .nested_value import NestedValue + +@dataclass +class NullableValues: + additional_properties: _dict[_str, + NestedValue | None] = _dataclass_field(default_factory = lambda: _dict[_str, + NestedValue + | None](), kw_only = True, metadata = {"openapi_additional_properties": True, "openapi_dictionary": True}) + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + for key, item in self.additional_properties.items(): + result[key] = _union.value_30_to_dict(item) + + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> NullableValues: + return cls( + additional_properties = {key: cast(NestedValue + | None, _union.value_30_from_dict(item)) for key, item in data.items() }, + ) diff --git a/tests/golden/python/python-httpx/object-extensions/python_additional_properties/models/numbers.py.golden b/tests/golden/python/python-httpx/object-extensions/python_additional_properties/models/numbers.py.golden new file mode 100644 index 000000000..f1f590794 --- /dev/null +++ b/tests/golden/python/python-httpx/object-extensions/python_additional_properties/models/numbers.py.golden @@ -0,0 +1,29 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python additional properties — 1.0.0 + +from __future__ import annotations + +from builtins import dict as _dict, str as _str +from dataclasses import dataclass, field as _dataclass_field +from typing import cast + +from . import _union + +@dataclass +class Numbers: + additional_properties: _dict[_str, int] = _dataclass_field(default_factory = lambda: _dict[_str, + int](), kw_only = True, metadata = {"openapi_additional_properties": True, "openapi_dictionary": True}) + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + for key, item in self.additional_properties.items(): + result[key] = _union.value_32_to_dict(item) + + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> Numbers: + return cls( + additional_properties = {key: cast(int, _union.value_32_from_dict(item)) for key, item in data.items() }, + ) diff --git a/tests/golden/python/python-httpx/object-extensions/python_additional_properties/models/opaque_map.py.golden b/tests/golden/python/python-httpx/object-extensions/python_additional_properties/models/opaque_map.py.golden new file mode 100644 index 000000000..7a7a7f5e2 --- /dev/null +++ b/tests/golden/python/python-httpx/object-extensions/python_additional_properties/models/opaque_map.py.golden @@ -0,0 +1,36 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python additional properties — 1.0.0 + +from __future__ import annotations + +from builtins import dict as _dict, str as _str +from dataclasses import dataclass, field as _dataclass_field +from typing import Any as _Any + +from . import _union + +@dataclass +class OpaqueMap: + known_field: str + additional_properties: _dict[_str, + _Any] = _dataclass_field(default_factory = lambda: _dict[_str, + _Any](), kw_only = True, metadata = {"openapi_additional_properties": True, "openapi_dictionary": False}) + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + result["known_field"] = self.known_field + for key, item in self.additional_properties.items(): + if key in ['known_field']: + raise ValueError("Additional property collides with declared wire field: " + key) + + result[key] = _union.value_7_to_dict(item) + + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> OpaqueMap: + return cls( + known_field=data["known_field"], # type: ignore[assignment] + additional_properties = {key: _union.value_7_from_dict(item) for key, item in data.items() if key not in ['known_field']}, + ) diff --git a/tests/golden/python/python-httpx/object-extensions/python_additional_properties/models/optional_holder.py.golden b/tests/golden/python/python-httpx/object-extensions/python_additional_properties/models/optional_holder.py.golden new file mode 100644 index 000000000..7abf38903 --- /dev/null +++ b/tests/golden/python/python-httpx/object-extensions/python_additional_properties/models/optional_holder.py.golden @@ -0,0 +1,35 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python additional properties — 1.0.0 + +from __future__ import annotations + +from dataclasses import dataclass +from typing import cast + +from . import _union +from .mixed_map import MixedMap + +@dataclass +class OptionalHolder: + maybe: MixedMap | None = None + nullable: MixedMap | None = None + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + if self.maybe is not None: + result['maybe'] = _union.value_24_to_dict(self.maybe) + + if self.nullable is not None: + result['nullable'] = _union.value_34_to_dict(self.nullable) + + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> OptionalHolder: + return cls( + maybe = cast(MixedMap + | None, None if data.get('maybe') is None else _union.value_24_from_dict(data.get('maybe'))), + nullable = cast(MixedMap + | None, None if data.get('nullable') is None else _union.value_34_from_dict(data.get('nullable'))), + ) diff --git a/tests/golden/python/python-httpx/object-extensions/python_additional_properties/models/optional_known.py.golden b/tests/golden/python/python-httpx/object-extensions/python_additional_properties/models/optional_known.py.golden new file mode 100644 index 000000000..5ecacf926 --- /dev/null +++ b/tests/golden/python/python-httpx/object-extensions/python_additional_properties/models/optional_known.py.golden @@ -0,0 +1,37 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python additional properties — 1.0.0 + +from __future__ import annotations + +from builtins import dict as _dict, str as _str +from dataclasses import dataclass, field as _dataclass_field +from typing import Any as _Any + +from . import _union + +@dataclass +class OptionalKnown: + known_field: str | None = None + additional_properties: _dict[_str, + _Any] = _dataclass_field(default_factory = lambda: _dict[_str, + _Any](), kw_only = True, metadata = {"openapi_additional_properties": True, "openapi_dictionary": False}) + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + if self.known_field is not None: + result["known_field"] = self.known_field + for key, item in self.additional_properties.items(): + if key in ['known_field']: + raise ValueError("Additional property collides with declared wire field: " + key) + + result[key] = _union.value_7_to_dict(item) + + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> OptionalKnown: + return cls( + known_field=data.get("known_field"), # type: ignore[assignment] + additional_properties = {key: _union.value_7_from_dict(item) for key, item in data.items() if key not in ['known_field']}, + ) diff --git a/tests/golden/python/python-httpx/object-extensions/python_additional_properties/models/pure_recursive.py.golden b/tests/golden/python/python-httpx/object-extensions/python_additional_properties/models/pure_recursive.py.golden new file mode 100644 index 000000000..c0c657dd8 --- /dev/null +++ b/tests/golden/python/python-httpx/object-extensions/python_additional_properties/models/pure_recursive.py.golden @@ -0,0 +1,30 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python additional properties — 1.0.0 + +from __future__ import annotations + +from builtins import dict as _dict, str as _str +from dataclasses import dataclass, field as _dataclass_field +from typing import cast + +from . import _union + +@dataclass +class PureRecursive: + additional_properties: _dict[_str, + PureRecursive] = _dataclass_field(default_factory = lambda: _dict[_str, + PureRecursive](), kw_only = True, metadata = {"openapi_additional_properties": True, "openapi_dictionary": True}) + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + for key, item in self.additional_properties.items(): + result[key] = _union.value_36_to_dict(item) + + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> PureRecursive: + return cls( + additional_properties = {key: cast(PureRecursive, _union.value_36_from_dict(item)) for key, item in data.items() }, + ) diff --git a/tests/golden/python/python-httpx/object-extensions/python_additional_properties/models/raw_map.py.golden b/tests/golden/python/python-httpx/object-extensions/python_additional_properties/models/raw_map.py.golden new file mode 100644 index 000000000..d2c84165f --- /dev/null +++ b/tests/golden/python/python-httpx/object-extensions/python_additional_properties/models/raw_map.py.golden @@ -0,0 +1,30 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python additional properties — 1.0.0 + +from __future__ import annotations + +from builtins import dict as _dict, str as _str +from dataclasses import dataclass, field as _dataclass_field +from typing import Any as _Any + +from . import _union + +@dataclass +class RawMap: + additional_properties: _dict[_str, + _Any] = _dataclass_field(default_factory = lambda: _dict[_str, + _Any](), kw_only = True, metadata = {"openapi_additional_properties": True, "openapi_dictionary": True}) + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + for key, item in self.additional_properties.items(): + result[key] = _union.value_7_to_dict(item) + + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> RawMap: + return cls( + additional_properties = {key: _union.value_7_from_dict(item) for key, item in data.items() }, + ) diff --git a/tests/golden/python/python-httpx/object-extensions/python_additional_properties/models/recursive.py.golden b/tests/golden/python/python-httpx/object-extensions/python_additional_properties/models/recursive.py.golden new file mode 100644 index 000000000..dd0064d12 --- /dev/null +++ b/tests/golden/python/python-httpx/object-extensions/python_additional_properties/models/recursive.py.golden @@ -0,0 +1,36 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python additional properties — 1.0.0 + +from __future__ import annotations + +from builtins import dict as _dict, str as _str +from dataclasses import dataclass, field as _dataclass_field +from typing import cast + +from . import _union + +@dataclass +class Recursive: + wire_value: str + additional_properties: _dict[_str, + Recursive] = _dataclass_field(default_factory = lambda: _dict[_str, + Recursive](), kw_only = True, metadata = {"openapi_additional_properties": True, "openapi_dictionary": False}) + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + result["wire_value"] = self.wire_value + for key, item in self.additional_properties.items(): + if key in ['wire_value']: + raise ValueError("Additional property collides with declared wire field: " + key) + + result[key] = _union.value_38_to_dict(item) + + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> Recursive: + return cls( + wire_value=data["wire_value"], # type: ignore[assignment] + additional_properties = {key: cast(Recursive, _union.value_38_from_dict(item)) for key, item in data.items() if key not in ['wire_value']}, + ) diff --git a/tests/golden/python/python-httpx/object-extensions/python_additional_properties/models/status.py.golden b/tests/golden/python/python-httpx/object-extensions/python_additional_properties/models/status.py.golden new file mode 100644 index 000000000..e3a664307 --- /dev/null +++ b/tests/golden/python/python-httpx/object-extensions/python_additional_properties/models/status.py.golden @@ -0,0 +1,11 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python additional properties — 1.0.0 + +from __future__ import annotations + +from enum import Enum + +class Status(str, Enum): + ACTIVE = "active" + INACTIVE = "inactive" diff --git a/tests/golden/python/python-httpx/object-extensions/python_additional_properties/models/status_values.py.golden b/tests/golden/python/python-httpx/object-extensions/python_additional_properties/models/status_values.py.golden new file mode 100644 index 000000000..c93068e18 --- /dev/null +++ b/tests/golden/python/python-httpx/object-extensions/python_additional_properties/models/status_values.py.golden @@ -0,0 +1,31 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python additional properties — 1.0.0 + +from __future__ import annotations + +from builtins import dict as _dict, str as _str +from dataclasses import dataclass, field as _dataclass_field +from typing import cast + +from . import _union +from .status import Status + +@dataclass +class StatusValues: + additional_properties: _dict[_str, + Status] = _dataclass_field(default_factory = lambda: _dict[_str, + Status](), kw_only = True, metadata = {"openapi_additional_properties": True, "openapi_dictionary": True}) + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + for key, item in self.additional_properties.items(): + result[key] = _union.value_39_to_dict(item) + + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> StatusValues: + return cls( + additional_properties = {key: cast(Status, _union.value_39_from_dict(item)) for key, item in data.items() }, + ) diff --git a/tests/golden/python/python-httpx/object-extensions/python_additional_properties/models/storage_collision.py.golden b/tests/golden/python/python-httpx/object-extensions/python_additional_properties/models/storage_collision.py.golden new file mode 100644 index 000000000..e88f39f38 --- /dev/null +++ b/tests/golden/python/python-httpx/object-extensions/python_additional_properties/models/storage_collision.py.golden @@ -0,0 +1,37 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python additional properties — 1.0.0 + +from __future__ import annotations + +from builtins import dict as _dict, str as _str +from dataclasses import dataclass, field as _dataclass_field +from typing import cast + +from . import _union +from .nested_value import NestedValue + +@dataclass +class StorageCollision: + additional_properties: str + additional_properties_: _dict[_str, + NestedValue] = _dataclass_field(default_factory = lambda: _dict[_str, + NestedValue](), kw_only = True, metadata = {"openapi_additional_properties": True, "openapi_dictionary": False}) + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + result["additionalProperties"] = self.additional_properties + for key, item in self.additional_properties_.items(): + if key in ['additionalProperties']: + raise ValueError("Additional property collides with declared wire field: " + key) + + result[key] = _union.value_1_to_dict(item) + + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> StorageCollision: + return cls( + additional_properties=data["additionalProperties"], # type: ignore[assignment] + additional_properties_ = {key: cast(NestedValue, _union.value_1_from_dict(item)) for key, item in data.items() if key not in ['additionalProperties']}, + ) diff --git a/tests/golden/python/python-httpx/object-extensions/python_additional_properties/models/typed_map.py.golden b/tests/golden/python/python-httpx/object-extensions/python_additional_properties/models/typed_map.py.golden new file mode 100644 index 000000000..5e0de15ac --- /dev/null +++ b/tests/golden/python/python-httpx/object-extensions/python_additional_properties/models/typed_map.py.golden @@ -0,0 +1,31 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python additional properties — 1.0.0 + +from __future__ import annotations + +from builtins import dict as _dict, str as _str +from dataclasses import dataclass, field as _dataclass_field +from typing import cast + +from . import _union +from .nested_value import NestedValue + +@dataclass +class TypedMap: + additional_properties: _dict[_str, + NestedValue] = _dataclass_field(default_factory = lambda: _dict[_str, + NestedValue](), kw_only = True, metadata = {"openapi_additional_properties": True, "openapi_dictionary": True}) + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + for key, item in self.additional_properties.items(): + result[key] = _union.value_1_to_dict(item) + + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> TypedMap: + return cls( + additional_properties = {key: cast(NestedValue, _union.value_1_from_dict(item)) for key, item in data.items() }, + ) diff --git a/tests/golden/python/python-httpx/object-extensions/python_additional_properties/models/value_alias.py.golden b/tests/golden/python/python-httpx/object-extensions/python_additional_properties/models/value_alias.py.golden new file mode 100644 index 000000000..b28b10747 --- /dev/null +++ b/tests/golden/python/python-httpx/object-extensions/python_additional_properties/models/value_alias.py.golden @@ -0,0 +1,9 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python additional properties — 1.0.0 + +from __future__ import annotations + +from .nested_value import NestedValue + +type ValueAlias = (NestedValue) diff --git a/tests/golden/python/python-httpx/object-extensions/python_additional_properties/models/value_choice.py.golden b/tests/golden/python/python-httpx/object-extensions/python_additional_properties/models/value_choice.py.golden new file mode 100644 index 000000000..1b4cf75ee --- /dev/null +++ b/tests/golden/python/python-httpx/object-extensions/python_additional_properties/models/value_choice.py.golden @@ -0,0 +1,21 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python additional properties — 1.0.0 + +from __future__ import annotations + +from typing import cast + +from . import _union +from .nested_value import NestedValue + +type ValueChoice = ( + NestedValue + | int +) + +def value_choice_from_dict(value: object) -> ValueChoice: + return cast(ValueChoice, _union.value_9_from_dict(value)) + +def value_choice_to_dict(value: ValueChoice) -> object: + return _union.value_9_to_dict(value) diff --git a/tests/golden/python/python-httpx/object-extensions/python_additional_properties/py.typed.golden b/tests/golden/python/python-httpx/object-extensions/python_additional_properties/py.typed.golden new file mode 100644 index 000000000..e69de29bb diff --git a/tests/golden/python/python-httpx/object-extensions/python_additional_properties/runtime/__init__.py.golden b/tests/golden/python/python-httpx/object-extensions/python_additional_properties/runtime/__init__.py.golden new file mode 100644 index 000000000..31f0adc37 --- /dev/null +++ b/tests/golden/python/python-httpx/object-extensions/python_additional_properties/runtime/__init__.py.golden @@ -0,0 +1,10 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python additional properties — 1.0.0 + +from .auth import ApiKeyAuth as ApiKeyAuth +from .auth import Authenticator as Authenticator +from .auth import BearerAuth as BearerAuth +from .client import ApiResponse as ApiResponse +from .client import Client as Client +from .errors import ApiError as ApiError diff --git a/tests/golden/python/python-httpx/object-extensions/python_additional_properties/runtime/_union_support.py.golden b/tests/golden/python/python-httpx/object-extensions/python_additional_properties/runtime/_union_support.py.golden new file mode 100644 index 000000000..33b10f50e --- /dev/null +++ b/tests/golden/python/python-httpx/object-extensions/python_additional_properties/runtime/_union_support.py.golden @@ -0,0 +1,109 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python additional properties — 1.0.0 + +"""Internal structural selection support; deliberately not a schema validator.""" + +from __future__ import annotations + +from dataclasses import fields, is_dataclass +from enum import Enum +from math import isfinite +from typing import Protocol, TypeGuard, cast + + +class _Model(Protocol): + def to_dict(self) -> dict[str, object]: ... + + +def as_dict(value: object) -> dict[str, object]: + return cast(dict[str, object], value) + + +def as_list(value: object) -> list[object]: + return cast(list[object], value) + + +def is_dict(value: object) -> TypeGuard[dict[str, object]]: + return isinstance(value, dict) and all(isinstance(key, str) for key in cast(dict[object, object], value)) + + +def is_list(value: object) -> TypeGuard[list[object]]: + return isinstance(value, list) + + +def _additional_properties(value: object, dictionary_only: bool = False) -> dict[str, object] | None: + if not isinstance(value, type) and is_dataclass(value): + for field in fields(value): + if field.metadata.get("openapi_additional_properties") is True: + if not dictionary_only or field.metadata.get("openapi_dictionary") is True: + return as_dict(getattr(value, field.name)) + return None + + +def is_mapping(value: object) -> bool: + return is_dict(value) or _additional_properties(value, True) is not None + + +def mapping(value: object) -> dict[str, object]: + properties = _additional_properties(value, True) + return properties if properties is not None else as_dict(value) + + +def scalar(value: object) -> object: + return value.value if isinstance(value, Enum) else value + + +def model_to_dict(value: object) -> object: + return value if is_dict(value) else cast(_Model, value).to_dict() + + +def is_object(value: object) -> bool: + return is_dict(value) or (not isinstance(value, type) and is_dataclass(value)) + + +def has(value: object, wire: str, field: str, required: bool) -> bool: + if is_dict(value): + return wire in as_dict(value) + # Optional dataclass fields use None for absence. Required fields still + # undergo their shape check; a nullable required field accepts that None. + return hasattr(value, field) and (required or getattr(value, field) is not None) + + +def get(value: object, wire: str, field: str) -> object: + return value.get(wire) if is_dict(value) else getattr(value, field, None) + + +def extras(value: object, known: list[str]) -> list[object]: + if is_dict(value): + return [item for key, item in as_dict(value).items() if key not in known] + properties = _additional_properties(value) + return list(properties.values()) if properties is not None else [] + + +def number(value: object, integer: bool) -> bool: + value = scalar(value) + if isinstance(value, bool): + return False + if isinstance(value, int): + return True + return isinstance(value, float) and isfinite(value) and (not integer or value.is_integer()) + + +def equal(value: object, expected: object) -> bool: + value = scalar(value) + if isinstance(expected, bool): + return isinstance(value, bool) and value == expected + if isinstance(expected, (int, float)): + return number(value, False) and value == expected + if is_list(expected): + if not is_list(value): + return False + left, right = as_list(value), as_list(expected) + return len(left) == len(right) and all(equal(a, b) for a, b in zip(left, right)) + if is_dict(expected): + if not is_dict(value): + return False + left, right = as_dict(value), as_dict(expected) + return left.keys() == right.keys() and all(equal(left[key], item) for key, item in right.items()) + return type(value) is type(expected) and value == expected diff --git a/tests/golden/python/python-httpx/object-extensions/python_additional_properties/runtime/auth.py.golden b/tests/golden/python/python-httpx/object-extensions/python_additional_properties/runtime/auth.py.golden new file mode 100644 index 000000000..6799e51bd --- /dev/null +++ b/tests/golden/python/python-httpx/object-extensions/python_additional_properties/runtime/auth.py.golden @@ -0,0 +1,50 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python additional properties — 1.0.0 + +"""Authentication helpers.""" + +from __future__ import annotations + +from abc import ABC, abstractmethod +from collections.abc import Callable + + +class Authenticator(ABC): + """Base class for request authenticators.""" + + @abstractmethod + def auth_headers(self) -> dict[str, str]: + """Return headers to attach to every request.""" + ... + + +class BearerAuth(Authenticator): + """Bearer token authentication. + + Accepts a static token string or a callable that returns the + current token (evaluated on every request). + """ + + def __init__(self, token: str | Callable[[], str]) -> None: + self._token = token + + def auth_headers(self) -> dict[str, str]: + token = self._token() if callable(self._token) else self._token + return {"Authorization": f"Bearer {token}"} + + +class ApiKeyAuth(Authenticator): + """API key authentication via a custom header. + + Accepts a static key string or a callable that returns the + current key (evaluated on every request). + """ + + def __init__(self, header_name: str, api_key: str | Callable[[], str]) -> None: + self._header_name = header_name + self._api_key = api_key + + def auth_headers(self) -> dict[str, str]: + key = self._api_key() if callable(self._api_key) else self._api_key + return {self._header_name: key} diff --git a/tests/golden/python/python-httpx/object-extensions/python_additional_properties/runtime/client.py.golden b/tests/golden/python/python-httpx/object-extensions/python_additional_properties/runtime/client.py.golden new file mode 100644 index 000000000..9ee71a710 --- /dev/null +++ b/tests/golden/python/python-httpx/object-extensions/python_additional_properties/runtime/client.py.golden @@ -0,0 +1,84 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python additional properties — 1.0.0 + +"""HTTP client wrapping httpx.""" + +from __future__ import annotations + +from collections.abc import Mapping +from dataclasses import dataclass +from typing import Any, Generic, TypeVar + +import httpx + +from .auth import Authenticator + +ResponseData = TypeVar("ResponseData") + + +@dataclass(frozen=True) +class ApiResponse(Generic[ResponseData]): + """Decoded response data together with its HTTP metadata.""" + + data: ResponseData + status_code: int + headers: Mapping[str, str] + raw: httpx.Response + + +class Client: + """Synchronous HTTP client for the generated SDK.""" + + def __init__( + self, + base_url: str, + *, + http_client: httpx.Client | None = None, + authenticator: Authenticator | None = None, + ) -> None: + self._base_url = base_url.rstrip("/") + self._http_client = http_client or httpx.Client() + self._authenticator = authenticator + + def request( + self, + method: str, + path: str, + *, + params: dict[str, str] | None = None, + json: Any = None, + content: Any = None, + data: Any = None, + files: Any = None, + headers: dict[str, str] | None = None, + ) -> httpx.Response: + """Send an HTTP request and return the raw response.""" + url = f"{self._base_url}{path}" + req_headers: dict[str, str] = {"Accept": "application/json"} + if json is not None: + req_headers["Content-Type"] = "application/json" + if headers: + req_headers.update(headers) + if self._authenticator is not None: + req_headers.update(self._authenticator.auth_headers()) + return self._http_client.request( + method, + url, + params=params, + json=json, + content=content, + data=data, + files=files, + headers=req_headers, + ) + + def close(self) -> None: + """Close the underlying HTTP client.""" + self._http_client.close() + + def __enter__(self) -> Client: + return self + + def __exit__(self, *args: object) -> None: + self.close() diff --git a/tests/golden/python/python-httpx/object-extensions/python_additional_properties/runtime/errors.py.golden b/tests/golden/python/python-httpx/object-extensions/python_additional_properties/runtime/errors.py.golden new file mode 100644 index 000000000..aa9e4987a --- /dev/null +++ b/tests/golden/python/python-httpx/object-extensions/python_additional_properties/runtime/errors.py.golden @@ -0,0 +1,34 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python additional properties — 1.0.0 + +"""API error type.""" + +from __future__ import annotations + +from collections.abc import Mapping + + +class ApiError(Exception): + """Raised when the server returns a non-success status code.""" + + def __init__( + self, + status_code: int, + status: str, + body: bytes, + *, + headers: Mapping[str, str] | None = None, + response: object | None = None, + ) -> None: + self.status_code: int = status_code + self.status: str = status + self.raw_body: bytes = body + self.body: bytes = body + self.headers: Mapping[str, str] | None = headers + self.response: object | None = response + super().__init__(f"{status_code} {status}") + + @property + def text(self) -> str: + return self.raw_body.decode("utf-8", errors="replace") diff --git a/tests/golden/python/python-httpx/open-tagged-additional-properties/open_tagged_additional_properties/models/_union.py.golden b/tests/golden/python/python-httpx/open-tagged-additional-properties/open_tagged_additional_properties/models/_union.py.golden new file mode 100644 index 000000000..f10744f31 --- /dev/null +++ b/tests/golden/python/python-httpx/open-tagged-additional-properties/open_tagged_additional_properties/models/_union.py.golden @@ -0,0 +1,55 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Open tagged additional properties — 1.0.0 + +from __future__ import annotations + +from ..runtime import _union_support + +def value_0_from_dict(value: object) -> object: + return _union_support.scalar(value) + +def value_0_to_dict(value: object) -> object: + return _union_support.scalar(value) + +def value_1_from_dict(value: object) -> object: + from.import event_searching + return event_searching.EventSearching.from_dict(_union_support.as_dict(value)) + +def value_1_to_dict(value: object) -> object: + return _union_support.model_to_dict(value) + +def value_2_from_dict(value: object) -> object: + from.import event_completed + return event_completed.EventCompleted.from_dict(_union_support.as_dict(value)) + +def value_2_to_dict(value: object) -> object: + return _union_support.model_to_dict(value) + +def value_3_from_dict(value: object) -> object: + data = _union_support.as_dict(value) + if data.get('type') == 'searching': + return value_1_from_dict(value) + + if data.get('type') == 'completed': + return value_2_from_dict(value) + + raise ValueError("No matching tagged union variant") + +def value_3_to_dict(value: object) -> object: + if ((_union_support.is_object(value) + and (_union_support.has(value, 'sequence_number', 'sequence_number', True) and _union_support.number(_union_support.get(value, 'sequence_number', 'sequence_number'), True)) + and (_union_support.has(value, 'type', 'type_', True) and _union_support.equal(_union_support.get(value, 'type', 'type_'), 'searching')) + and all(True for _item in _union_support.extras(value, ['sequence_number', 'type'])))): + result = dict(_union_support.as_dict(value_1_to_dict(value))) + result['type'] = 'searching' + return result + + if ((_union_support.is_object(value) + and (_union_support.has(value, 'type', 'type_', True) and _union_support.equal(_union_support.get(value, 'type', 'type_'), 'completed')) + and all(True for _item in _union_support.extras(value, ['type'])))): + result = dict(_union_support.as_dict(value_2_to_dict(value))) + result['type'] = 'completed' + return result + + raise ValueError("No matching tagged union variant") diff --git a/tests/golden/python/python-httpx/open-tagged-additional-properties/open_tagged_additional_properties/models/event_completed.py.golden b/tests/golden/python/python-httpx/open-tagged-additional-properties/open_tagged_additional_properties/models/event_completed.py.golden index c8bab9e90..89829522e 100644 --- a/tests/golden/python/python-httpx/open-tagged-additional-properties/open_tagged_additional_properties/models/event_completed.py.golden +++ b/tests/golden/python/python-httpx/open-tagged-additional-properties/open_tagged_additional_properties/models/event_completed.py.golden @@ -4,20 +4,33 @@ from __future__ import annotations -from dataclasses import dataclass -from typing import Literal +from builtins import dict as _dict, str as _str +from dataclasses import dataclass, field as _dataclass_field +from typing import Any as _Any, Literal + +from . import _union @dataclass class EventCompleted: type_: Literal["completed"] + additional_properties: _dict[_str, + _Any] = _dataclass_field(default_factory = lambda: _dict[_str, + _Any](), kw_only = True, metadata = {"openapi_additional_properties": True, "openapi_dictionary": False}) def to_dict(self) -> dict[str, object]: result: dict[str, object] = {} result["type"] = self.type_ + for key, item in self.additional_properties.items(): + if key in ['type']: + raise ValueError("Additional property collides with declared wire field: " + key) + + result[key] = _union.value_0_to_dict(item) + return result @classmethod def from_dict(cls, data: dict[str, object]) -> EventCompleted: return cls( type_=data["type"], # type: ignore[assignment] + additional_properties = {key: _union.value_0_from_dict(item) for key, item in data.items() if key not in ['type']}, ) diff --git a/tests/golden/python/python-httpx/open-tagged-additional-properties/open_tagged_additional_properties/models/event_searching.py.golden b/tests/golden/python/python-httpx/open-tagged-additional-properties/open_tagged_additional_properties/models/event_searching.py.golden index 266ef40cf..77e5ab866 100644 --- a/tests/golden/python/python-httpx/open-tagged-additional-properties/open_tagged_additional_properties/models/event_searching.py.golden +++ b/tests/golden/python/python-httpx/open-tagged-additional-properties/open_tagged_additional_properties/models/event_searching.py.golden @@ -4,18 +4,30 @@ from __future__ import annotations -from dataclasses import dataclass -from typing import Literal +from builtins import dict as _dict, str as _str +from dataclasses import dataclass, field as _dataclass_field +from typing import Any as _Any, Literal + +from . import _union @dataclass class EventSearching: sequence_number: int type_: Literal["searching"] + additional_properties: _dict[_str, + _Any] = _dataclass_field(default_factory = lambda: _dict[_str, + _Any](), kw_only = True, metadata = {"openapi_additional_properties": True, "openapi_dictionary": False}) def to_dict(self) -> dict[str, object]: result: dict[str, object] = {} result["sequence_number"] = self.sequence_number result["type"] = self.type_ + for key, item in self.additional_properties.items(): + if key in ['sequence_number', 'type']: + raise ValueError("Additional property collides with declared wire field: " + key) + + result[key] = _union.value_0_to_dict(item) + return result @classmethod @@ -23,4 +35,5 @@ class EventSearching: return cls( sequence_number=data["sequence_number"], # type: ignore[assignment] type_=data["type"], # type: ignore[assignment] + additional_properties = {key: _union.value_0_from_dict(item) for key, item in data.items() if key not in ['sequence_number', 'type']}, ) diff --git a/tests/golden/python/python-httpx/open-tagged-additional-properties/open_tagged_additional_properties/runtime/_union_support.py.golden b/tests/golden/python/python-httpx/open-tagged-additional-properties/open_tagged_additional_properties/runtime/_union_support.py.golden new file mode 100644 index 000000000..6b104cbd9 --- /dev/null +++ b/tests/golden/python/python-httpx/open-tagged-additional-properties/open_tagged_additional_properties/runtime/_union_support.py.golden @@ -0,0 +1,109 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Open tagged additional properties — 1.0.0 + +"""Internal structural selection support; deliberately not a schema validator.""" + +from __future__ import annotations + +from dataclasses import fields, is_dataclass +from enum import Enum +from math import isfinite +from typing import Protocol, TypeGuard, cast + + +class _Model(Protocol): + def to_dict(self) -> dict[str, object]: ... + + +def as_dict(value: object) -> dict[str, object]: + return cast(dict[str, object], value) + + +def as_list(value: object) -> list[object]: + return cast(list[object], value) + + +def is_dict(value: object) -> TypeGuard[dict[str, object]]: + return isinstance(value, dict) and all(isinstance(key, str) for key in cast(dict[object, object], value)) + + +def is_list(value: object) -> TypeGuard[list[object]]: + return isinstance(value, list) + + +def _additional_properties(value: object, dictionary_only: bool = False) -> dict[str, object] | None: + if not isinstance(value, type) and is_dataclass(value): + for field in fields(value): + if field.metadata.get("openapi_additional_properties") is True: + if not dictionary_only or field.metadata.get("openapi_dictionary") is True: + return as_dict(getattr(value, field.name)) + return None + + +def is_mapping(value: object) -> bool: + return is_dict(value) or _additional_properties(value, True) is not None + + +def mapping(value: object) -> dict[str, object]: + properties = _additional_properties(value, True) + return properties if properties is not None else as_dict(value) + + +def scalar(value: object) -> object: + return value.value if isinstance(value, Enum) else value + + +def model_to_dict(value: object) -> object: + return value if is_dict(value) else cast(_Model, value).to_dict() + + +def is_object(value: object) -> bool: + return is_dict(value) or (not isinstance(value, type) and is_dataclass(value)) + + +def has(value: object, wire: str, field: str, required: bool) -> bool: + if is_dict(value): + return wire in as_dict(value) + # Optional dataclass fields use None for absence. Required fields still + # undergo their shape check; a nullable required field accepts that None. + return hasattr(value, field) and (required or getattr(value, field) is not None) + + +def get(value: object, wire: str, field: str) -> object: + return value.get(wire) if is_dict(value) else getattr(value, field, None) + + +def extras(value: object, known: list[str]) -> list[object]: + if is_dict(value): + return [item for key, item in as_dict(value).items() if key not in known] + properties = _additional_properties(value) + return list(properties.values()) if properties is not None else [] + + +def number(value: object, integer: bool) -> bool: + value = scalar(value) + if isinstance(value, bool): + return False + if isinstance(value, int): + return True + return isinstance(value, float) and isfinite(value) and (not integer or value.is_integer()) + + +def equal(value: object, expected: object) -> bool: + value = scalar(value) + if isinstance(expected, bool): + return isinstance(value, bool) and value == expected + if isinstance(expected, (int, float)): + return number(value, False) and value == expected + if is_list(expected): + if not is_list(value): + return False + left, right = as_list(value), as_list(expected) + return len(left) == len(right) and all(equal(a, b) for a, b in zip(left, right)) + if is_dict(expected): + if not is_dict(value): + return False + left, right = as_dict(value), as_dict(expected) + return left.keys() == right.keys() and all(equal(left[key], item) for key, item in right.items()) + return type(value) is type(expected) and value == expected diff --git a/tests/golden/python/python-httpx/type-aliases-complex-union/complex_union_type_test/runtime/_union_support.py.golden b/tests/golden/python/python-httpx/type-aliases-complex-union/complex_union_type_test/runtime/_union_support.py.golden index 1138cbb7b..fec92c4df 100644 --- a/tests/golden/python/python-httpx/type-aliases-complex-union/complex_union_type_test/runtime/_union_support.py.golden +++ b/tests/golden/python/python-httpx/type-aliases-complex-union/complex_union_type_test/runtime/_union_support.py.golden @@ -7,7 +7,7 @@ from __future__ import annotations -from dataclasses import is_dataclass +from dataclasses import fields, is_dataclass from enum import Enum from math import isfinite from typing import Protocol, TypeGuard, cast @@ -33,6 +33,24 @@ def is_list(value: object) -> TypeGuard[list[object]]: return isinstance(value, list) +def _additional_properties(value: object, dictionary_only: bool = False) -> dict[str, object] | None: + if not isinstance(value, type) and is_dataclass(value): + for field in fields(value): + if field.metadata.get("openapi_additional_properties") is True: + if not dictionary_only or field.metadata.get("openapi_dictionary") is True: + return as_dict(getattr(value, field.name)) + return None + + +def is_mapping(value: object) -> bool: + return is_dict(value) or _additional_properties(value, True) is not None + + +def mapping(value: object) -> dict[str, object]: + properties = _additional_properties(value, True) + return properties if properties is not None else as_dict(value) + + def scalar(value: object) -> object: return value.value if isinstance(value, Enum) else value @@ -58,9 +76,10 @@ def get(value: object, wire: str, field: str) -> object: def extras(value: object, known: list[str]) -> list[object]: - if not is_dict(value): - return [] - return [item for key, item in as_dict(value).items() if key not in known] + if is_dict(value): + return [item for key, item in as_dict(value).items() if key not in known] + properties = _additional_properties(value) + return list(properties.values()) if properties is not None else [] def number(value: object, integer: bool) -> bool: diff --git a/tests/golden/python/python-httpx/type-aliases-nested-union/nested_union_type_test/runtime/_union_support.py.golden b/tests/golden/python/python-httpx/type-aliases-nested-union/nested_union_type_test/runtime/_union_support.py.golden index 386b49b07..6ef782e37 100644 --- a/tests/golden/python/python-httpx/type-aliases-nested-union/nested_union_type_test/runtime/_union_support.py.golden +++ b/tests/golden/python/python-httpx/type-aliases-nested-union/nested_union_type_test/runtime/_union_support.py.golden @@ -7,7 +7,7 @@ from __future__ import annotations -from dataclasses import is_dataclass +from dataclasses import fields, is_dataclass from enum import Enum from math import isfinite from typing import Protocol, TypeGuard, cast @@ -33,6 +33,24 @@ def is_list(value: object) -> TypeGuard[list[object]]: return isinstance(value, list) +def _additional_properties(value: object, dictionary_only: bool = False) -> dict[str, object] | None: + if not isinstance(value, type) and is_dataclass(value): + for field in fields(value): + if field.metadata.get("openapi_additional_properties") is True: + if not dictionary_only or field.metadata.get("openapi_dictionary") is True: + return as_dict(getattr(value, field.name)) + return None + + +def is_mapping(value: object) -> bool: + return is_dict(value) or _additional_properties(value, True) is not None + + +def mapping(value: object) -> dict[str, object]: + properties = _additional_properties(value, True) + return properties if properties is not None else as_dict(value) + + def scalar(value: object) -> object: return value.value if isinstance(value, Enum) else value @@ -58,9 +76,10 @@ def get(value: object, wire: str, field: str) -> object: def extras(value: object, known: list[str]) -> list[object]: - if not is_dict(value): - return [] - return [item for key, item in as_dict(value).items() if key not in known] + if is_dict(value): + return [item for key, item in as_dict(value).items() if key not in known] + properties = _additional_properties(value) + return list(properties.values()) if properties is not None else [] def number(value: object, integer: bool) -> bool: diff --git a/tests/golden/python/python-httpx/type-aliases-union-mixed/union_type_with_mixed_members_test/runtime/_union_support.py.golden b/tests/golden/python/python-httpx/type-aliases-union-mixed/union_type_with_mixed_members_test/runtime/_union_support.py.golden index 8a0a49a69..8c966fcc9 100644 --- a/tests/golden/python/python-httpx/type-aliases-union-mixed/union_type_with_mixed_members_test/runtime/_union_support.py.golden +++ b/tests/golden/python/python-httpx/type-aliases-union-mixed/union_type_with_mixed_members_test/runtime/_union_support.py.golden @@ -7,7 +7,7 @@ from __future__ import annotations -from dataclasses import is_dataclass +from dataclasses import fields, is_dataclass from enum import Enum from math import isfinite from typing import Protocol, TypeGuard, cast @@ -33,6 +33,24 @@ def is_list(value: object) -> TypeGuard[list[object]]: return isinstance(value, list) +def _additional_properties(value: object, dictionary_only: bool = False) -> dict[str, object] | None: + if not isinstance(value, type) and is_dataclass(value): + for field in fields(value): + if field.metadata.get("openapi_additional_properties") is True: + if not dictionary_only or field.metadata.get("openapi_dictionary") is True: + return as_dict(getattr(value, field.name)) + return None + + +def is_mapping(value: object) -> bool: + return is_dict(value) or _additional_properties(value, True) is not None + + +def mapping(value: object) -> dict[str, object]: + properties = _additional_properties(value, True) + return properties if properties is not None else as_dict(value) + + def scalar(value: object) -> object: return value.value if isinstance(value, Enum) else value @@ -58,9 +76,10 @@ def get(value: object, wire: str, field: str) -> object: def extras(value: object, known: list[str]) -> list[object]: - if not is_dict(value): - return [] - return [item for key, item in as_dict(value).items() if key not in known] + if is_dict(value): + return [item for key, item in as_dict(value).items() if key not in known] + properties = _additional_properties(value) + return list(properties.values()) if properties is not None else [] def number(value: object, integer: bool) -> bool: diff --git a/tests/golden/python/python-httpx/type-aliases-union-with-any/union_type_with_any_test/models/config.py.golden b/tests/golden/python/python-httpx/type-aliases-union-with-any/union_type_with_any_test/models/config.py.golden index 4a36e52d5..1b88e980f 100644 --- a/tests/golden/python/python-httpx/type-aliases-union-with-any/union_type_with_any_test/models/config.py.golden +++ b/tests/golden/python/python-httpx/type-aliases-union-with-any/union_type_with_any_test/models/config.py.golden @@ -5,14 +5,14 @@ from __future__ import annotations -from typing import Any +from typing import Any as _Any from . import _union type Config = ( str | float - | Any + | _Any ) def config_from_dict(value: object) -> Config: diff --git a/tests/golden/python/python-httpx/type-aliases-union-with-any/union_type_with_any_test/runtime/_union_support.py.golden b/tests/golden/python/python-httpx/type-aliases-union-with-any/union_type_with_any_test/runtime/_union_support.py.golden index aa7278d60..78babf760 100644 --- a/tests/golden/python/python-httpx/type-aliases-union-with-any/union_type_with_any_test/runtime/_union_support.py.golden +++ b/tests/golden/python/python-httpx/type-aliases-union-with-any/union_type_with_any_test/runtime/_union_support.py.golden @@ -7,7 +7,7 @@ from __future__ import annotations -from dataclasses import is_dataclass +from dataclasses import fields, is_dataclass from enum import Enum from math import isfinite from typing import Protocol, TypeGuard, cast @@ -33,6 +33,24 @@ def is_list(value: object) -> TypeGuard[list[object]]: return isinstance(value, list) +def _additional_properties(value: object, dictionary_only: bool = False) -> dict[str, object] | None: + if not isinstance(value, type) and is_dataclass(value): + for field in fields(value): + if field.metadata.get("openapi_additional_properties") is True: + if not dictionary_only or field.metadata.get("openapi_dictionary") is True: + return as_dict(getattr(value, field.name)) + return None + + +def is_mapping(value: object) -> bool: + return is_dict(value) or _additional_properties(value, True) is not None + + +def mapping(value: object) -> dict[str, object]: + properties = _additional_properties(value, True) + return properties if properties is not None else as_dict(value) + + def scalar(value: object) -> object: return value.value if isinstance(value, Enum) else value @@ -58,9 +76,10 @@ def get(value: object, wire: str, field: str) -> object: def extras(value: object, known: list[str]) -> list[object]: - if not is_dict(value): - return [] - return [item for key, item in as_dict(value).items() if key not in known] + if is_dict(value): + return [item for key, item in as_dict(value).items() if key not in known] + properties = _additional_properties(value) + return list(properties.values()) if properties is not None else [] def number(value: object, integer: bool) -> bool: diff --git a/tests/golden/python/python-httpx/type-aliases-union-with-inline-objects/union_type_with_inline_objects_test/runtime/_union_support.py.golden b/tests/golden/python/python-httpx/type-aliases-union-with-inline-objects/union_type_with_inline_objects_test/runtime/_union_support.py.golden index 20fb2628d..4b95f0b66 100644 --- a/tests/golden/python/python-httpx/type-aliases-union-with-inline-objects/union_type_with_inline_objects_test/runtime/_union_support.py.golden +++ b/tests/golden/python/python-httpx/type-aliases-union-with-inline-objects/union_type_with_inline_objects_test/runtime/_union_support.py.golden @@ -7,7 +7,7 @@ from __future__ import annotations -from dataclasses import is_dataclass +from dataclasses import fields, is_dataclass from enum import Enum from math import isfinite from typing import Protocol, TypeGuard, cast @@ -33,6 +33,24 @@ def is_list(value: object) -> TypeGuard[list[object]]: return isinstance(value, list) +def _additional_properties(value: object, dictionary_only: bool = False) -> dict[str, object] | None: + if not isinstance(value, type) and is_dataclass(value): + for field in fields(value): + if field.metadata.get("openapi_additional_properties") is True: + if not dictionary_only or field.metadata.get("openapi_dictionary") is True: + return as_dict(getattr(value, field.name)) + return None + + +def is_mapping(value: object) -> bool: + return is_dict(value) or _additional_properties(value, True) is not None + + +def mapping(value: object) -> dict[str, object]: + properties = _additional_properties(value, True) + return properties if properties is not None else as_dict(value) + + def scalar(value: object) -> object: return value.value if isinstance(value, Enum) else value @@ -58,9 +76,10 @@ def get(value: object, wire: str, field: str) -> object: def extras(value: object, known: list[str]) -> list[object]: - if not is_dict(value): - return [] - return [item for key, item in as_dict(value).items() if key not in known] + if is_dict(value): + return [item for key, item in as_dict(value).items() if key not in known] + properties = _additional_properties(value) + return list(properties.values()) if properties is not None else [] def number(value: object, integer: bool) -> bool: diff --git a/tests/golden/python/python-httpx/type-aliases-union-with-interfaces/union_type_with_interfaces_test/runtime/_union_support.py.golden b/tests/golden/python/python-httpx/type-aliases-union-with-interfaces/union_type_with_interfaces_test/runtime/_union_support.py.golden index b207b3392..1b6ea2483 100644 --- a/tests/golden/python/python-httpx/type-aliases-union-with-interfaces/union_type_with_interfaces_test/runtime/_union_support.py.golden +++ b/tests/golden/python/python-httpx/type-aliases-union-with-interfaces/union_type_with_interfaces_test/runtime/_union_support.py.golden @@ -7,7 +7,7 @@ from __future__ import annotations -from dataclasses import is_dataclass +from dataclasses import fields, is_dataclass from enum import Enum from math import isfinite from typing import Protocol, TypeGuard, cast @@ -33,6 +33,24 @@ def is_list(value: object) -> TypeGuard[list[object]]: return isinstance(value, list) +def _additional_properties(value: object, dictionary_only: bool = False) -> dict[str, object] | None: + if not isinstance(value, type) and is_dataclass(value): + for field in fields(value): + if field.metadata.get("openapi_additional_properties") is True: + if not dictionary_only or field.metadata.get("openapi_dictionary") is True: + return as_dict(getattr(value, field.name)) + return None + + +def is_mapping(value: object) -> bool: + return is_dict(value) or _additional_properties(value, True) is not None + + +def mapping(value: object) -> dict[str, object]: + properties = _additional_properties(value, True) + return properties if properties is not None else as_dict(value) + + def scalar(value: object) -> object: return value.value if isinstance(value, Enum) else value @@ -58,9 +76,10 @@ def get(value: object, wire: str, field: str) -> object: def extras(value: object, known: list[str]) -> list[object]: - if not is_dict(value): - return [] - return [item for key, item in as_dict(value).items() if key not in known] + if is_dict(value): + return [item for key, item in as_dict(value).items() if key not in known] + properties = _additional_properties(value) + return list(properties.values()) if properties is not None else [] def number(value: object, integer: bool) -> bool: diff --git a/tests/golden/python/python-httpx/type-aliases-union-with-primitives/union_type_with_primitives_test/runtime/_union_support.py.golden b/tests/golden/python/python-httpx/type-aliases-union-with-primitives/union_type_with_primitives_test/runtime/_union_support.py.golden index 30f44c9b4..6ebe5e3e6 100644 --- a/tests/golden/python/python-httpx/type-aliases-union-with-primitives/union_type_with_primitives_test/runtime/_union_support.py.golden +++ b/tests/golden/python/python-httpx/type-aliases-union-with-primitives/union_type_with_primitives_test/runtime/_union_support.py.golden @@ -7,7 +7,7 @@ from __future__ import annotations -from dataclasses import is_dataclass +from dataclasses import fields, is_dataclass from enum import Enum from math import isfinite from typing import Protocol, TypeGuard, cast @@ -33,6 +33,24 @@ def is_list(value: object) -> TypeGuard[list[object]]: return isinstance(value, list) +def _additional_properties(value: object, dictionary_only: bool = False) -> dict[str, object] | None: + if not isinstance(value, type) and is_dataclass(value): + for field in fields(value): + if field.metadata.get("openapi_additional_properties") is True: + if not dictionary_only or field.metadata.get("openapi_dictionary") is True: + return as_dict(getattr(value, field.name)) + return None + + +def is_mapping(value: object) -> bool: + return is_dict(value) or _additional_properties(value, True) is not None + + +def mapping(value: object) -> dict[str, object]: + properties = _additional_properties(value, True) + return properties if properties is not None else as_dict(value) + + def scalar(value: object) -> object: return value.value if isinstance(value, Enum) else value @@ -58,9 +76,10 @@ def get(value: object, wire: str, field: str) -> object: def extras(value: object, known: list[str]) -> list[object]: - if not is_dict(value): - return [] - return [item for key, item in as_dict(value).items() if key not in known] + if is_dict(value): + return [item for key, item in as_dict(value).items() if key not in known] + properties = _additional_properties(value) + return list(properties.values()) if properties is not None else [] def number(value: object, integer: bool) -> bool: diff --git a/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/models/_union.py.golden b/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/models/_union.py.golden index 3836a8a0c..726e80770 100644 --- a/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/models/_union.py.golden +++ b/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/models/_union.py.golden @@ -127,7 +127,7 @@ def value_18_from_dict(value: object) -> object: return {key: value_11_from_dict(item) for key, item in _union_support.as_dict(value).items()} def value_18_to_dict(value: object) -> object: - return {key: value_11_to_dict(item) for key, item in _union_support.as_dict(value).items()} + return {key: value_11_to_dict(item) for key, item in _union_support.mapping(value).items()} def value_19_from_dict(value: object) -> object: return [value_20_from_dict(item) for item in _union_support.as_list(value)] @@ -139,7 +139,7 @@ def value_20_from_dict(value: object) -> object: return {key: value_17_from_dict(item) for key, item in _union_support.as_dict(value).items()} def value_20_to_dict(value: object) -> object: - return {key: value_17_to_dict(item) for key, item in _union_support.as_dict(value).items()} + return {key: value_17_to_dict(item) for key, item in _union_support.mapping(value).items()} def value_21_from_dict(value: object) -> object: return value_22_from_dict(value) @@ -333,7 +333,7 @@ def value_46_from_dict(value: object) -> object: return {key: value_45_from_dict(item) for key, item in _union_support.as_dict(value).items()} def value_46_to_dict(value: object) -> object: - return {key: value_45_to_dict(item) for key, item in _union_support.as_dict(value).items()} + return {key: value_45_to_dict(item) for key, item in _union_support.mapping(value).items()} def value_47_from_dict(value: object) -> object: return [value_44_from_dict(item) for item in _union_support.as_list(value)] diff --git a/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/models/client_tool.py.golden b/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/models/client_tool.py.golden index b65bef982..d34e4696b 100644 --- a/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/models/client_tool.py.golden +++ b/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/models/client_tool.py.golden @@ -5,11 +5,11 @@ from __future__ import annotations from dataclasses import dataclass -from typing import Any +from typing import Any as _Any @dataclass class ClientTool: - input_schema: dict[str, Any] + input_schema: dict[str, _Any] name: str def to_dict(self) -> dict[str, object]: diff --git a/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/runtime/_union_support.py.golden b/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/runtime/_union_support.py.golden index a4c9e6107..ea1b63caa 100644 --- a/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/runtime/_union_support.py.golden +++ b/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/runtime/_union_support.py.golden @@ -6,7 +6,7 @@ from __future__ import annotations -from dataclasses import is_dataclass +from dataclasses import fields, is_dataclass from enum import Enum from math import isfinite from typing import Protocol, TypeGuard, cast @@ -32,6 +32,24 @@ def is_list(value: object) -> TypeGuard[list[object]]: return isinstance(value, list) +def _additional_properties(value: object, dictionary_only: bool = False) -> dict[str, object] | None: + if not isinstance(value, type) and is_dataclass(value): + for field in fields(value): + if field.metadata.get("openapi_additional_properties") is True: + if not dictionary_only or field.metadata.get("openapi_dictionary") is True: + return as_dict(getattr(value, field.name)) + return None + + +def is_mapping(value: object) -> bool: + return is_dict(value) or _additional_properties(value, True) is not None + + +def mapping(value: object) -> dict[str, object]: + properties = _additional_properties(value, True) + return properties if properties is not None else as_dict(value) + + def scalar(value: object) -> object: return value.value if isinstance(value, Enum) else value @@ -57,9 +75,10 @@ def get(value: object, wire: str, field: str) -> object: def extras(value: object, known: list[str]) -> list[object]: - if not is_dict(value): - return [] - return [item for key, item in as_dict(value).items() if key not in known] + if is_dict(value): + return [item for key, item in as_dict(value).items() if key not in known] + properties = _additional_properties(value) + return list(properties.values()) if properties is not None else [] def number(value: object, integer: bool) -> bool: diff --git a/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/models/_union.py.golden b/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/models/_union.py.golden index da2f4f470..776324e37 100644 --- a/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/models/_union.py.golden +++ b/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/models/_union.py.golden @@ -307,7 +307,7 @@ def value_18_from_dict(value: object) -> object: return {key: value_11_from_dict(item) for key, item in _union_support.as_dict(value).items()} def value_18_to_dict(value: object) -> object: - return {key: value_11_to_dict(item) for key, item in _union_support.as_dict(value).items()} + return {key: value_11_to_dict(item) for key, item in _union_support.mapping(value).items()} def value_19_from_dict(value: object) -> object: return [value_20_from_dict(item) for item in _union_support.as_list(value)] @@ -319,7 +319,7 @@ def value_20_from_dict(value: object) -> object: return {key: value_17_from_dict(item) for key, item in _union_support.as_dict(value).items()} def value_20_to_dict(value: object) -> object: - return {key: value_17_to_dict(item) for key, item in _union_support.as_dict(value).items()} + return {key: value_17_to_dict(item) for key, item in _union_support.mapping(value).items()} def value_21_from_dict(value: object) -> object: return value_22_from_dict(value) @@ -591,7 +591,7 @@ def value_34_from_dict(value: object) -> object: def value_34_to_dict(value: object) -> object: matches = [(_union_support.is_object(value) - and (_union_support.has(value, 'input_schema', 'input_schema', True) and (_union_support.is_dict(_union_support.get(value, 'input_schema', 'input_schema')) and all(True for _item in _union_support.as_dict(_union_support.get(value, 'input_schema', 'input_schema')).values()))) + and (_union_support.has(value, 'input_schema', 'input_schema', True) and (_union_support.is_mapping(_union_support.get(value, 'input_schema', 'input_schema')) and all(True for _item in _union_support.mapping(_union_support.get(value, 'input_schema', 'input_schema')).values()))) and (_union_support.has(value, 'name', 'name', True) and isinstance(_union_support.get(value, 'name', 'name'), str))), (_union_support.is_object(value) and (not _union_support.has(value, 'max_uses', 'max_uses', False) or _union_support.number(_union_support.get(value, 'max_uses', 'max_uses'), True)) and (_union_support.has(value, 'name', 'name', True) and _union_support.equal(_union_support.get(value, 'name', 'name'), 'web_search')) @@ -819,7 +819,7 @@ def value_46_from_dict(value: object) -> object: return {key: value_45_from_dict(item) for key, item in _union_support.as_dict(value).items()} def value_46_to_dict(value: object) -> object: - return {key: value_45_to_dict(item) for key, item in _union_support.as_dict(value).items()} + return {key: value_45_to_dict(item) for key, item in _union_support.mapping(value).items()} def value_47_from_dict(value: object) -> object: return [value_44_from_dict(item) for item in _union_support.as_list(value)] @@ -875,13 +875,13 @@ def value_48_from_dict(value: object) -> object: def value_48_to_dict(value: object) -> object: matches = [(_union_support.is_object(value) - and (_union_support.has(value, 'mapping', 'mapping', True) and (_union_support.is_dict(_union_support.get(value, 'mapping', 'mapping')) and all(((_union_support.is_object(_item) + and (_union_support.has(value, 'mapping', 'mapping', True) and (_union_support.is_mapping(_union_support.get(value, 'mapping', 'mapping')) and all(((_union_support.is_object(_item) and (_union_support.has(_item, 'name', 'name', True) and isinstance(_union_support.get(_item, 'name', 'name'), str))) or (_union_support.is_object(_item) and (_union_support.has(_item, 'revision', 'revision', True) and (_union_support.number(_union_support.get(_item, 'revision', 'revision'), True) and _union_support.equal(_union_support.get(_item, 'revision', 'revision'), 2))) and (not _union_support.has(_item, 'second_value', 'second_value', False) or isinstance(_union_support.get(_item, 'second_value', 'second_value'), str))) - or _union_support.number(_item, True)) for _item in _union_support.as_dict(_union_support.get(value, 'mapping', 'mapping')).values()))) + or _union_support.number(_item, True)) for _item in _union_support.mapping(_union_support.get(value, 'mapping', 'mapping')).values()))) and (_union_support.has(value, 'value', 'value', True) and ((_union_support.is_object(_union_support.get(value, 'value', 'value')) and (_union_support.has(_union_support.get(value, 'value', 'value'), 'name', 'name', True) and isinstance(_union_support.get(_union_support.get(value, 'value', 'value'), 'name', 'name'), str))) or (_union_support.is_object(_union_support.get(value, 'value', 'value')) diff --git a/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/models/client_tool.py.golden b/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/models/client_tool.py.golden index b65bef982..d34e4696b 100644 --- a/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/models/client_tool.py.golden +++ b/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/models/client_tool.py.golden @@ -5,11 +5,11 @@ from __future__ import annotations from dataclasses import dataclass -from typing import Any +from typing import Any as _Any @dataclass class ClientTool: - input_schema: dict[str, Any] + input_schema: dict[str, _Any] name: str def to_dict(self) -> dict[str, object]: diff --git a/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/models/open_choice.py.golden b/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/models/open_choice.py.golden index 3bcd71d81..5b3fd2874 100644 --- a/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/models/open_choice.py.golden +++ b/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/models/open_choice.py.golden @@ -4,13 +4,13 @@ from __future__ import annotations -from typing import Any +from typing import Any as _Any from . import _union from .numeric_two import NumericTwo type OpenChoice = ( - Any + _Any | NumericTwo ) diff --git a/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/runtime/_union_support.py.golden b/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/runtime/_union_support.py.golden index a4c9e6107..ea1b63caa 100644 --- a/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/runtime/_union_support.py.golden +++ b/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/runtime/_union_support.py.golden @@ -6,7 +6,7 @@ from __future__ import annotations -from dataclasses import is_dataclass +from dataclasses import fields, is_dataclass from enum import Enum from math import isfinite from typing import Protocol, TypeGuard, cast @@ -32,6 +32,24 @@ def is_list(value: object) -> TypeGuard[list[object]]: return isinstance(value, list) +def _additional_properties(value: object, dictionary_only: bool = False) -> dict[str, object] | None: + if not isinstance(value, type) and is_dataclass(value): + for field in fields(value): + if field.metadata.get("openapi_additional_properties") is True: + if not dictionary_only or field.metadata.get("openapi_dictionary") is True: + return as_dict(getattr(value, field.name)) + return None + + +def is_mapping(value: object) -> bool: + return is_dict(value) or _additional_properties(value, True) is not None + + +def mapping(value: object) -> dict[str, object]: + properties = _additional_properties(value, True) + return properties if properties is not None else as_dict(value) + + def scalar(value: object) -> object: return value.value if isinstance(value, Enum) else value @@ -57,9 +75,10 @@ def get(value: object, wire: str, field: str) -> object: def extras(value: object, known: list[str]) -> list[object]: - if not is_dict(value): - return [] - return [item for key, item in as_dict(value).items() if key not in known] + if is_dict(value): + return [item for key, item in as_dict(value).items() if key not in known] + properties = _additional_properties(value) + return list(properties.values()) if properties is not None else [] def number(value: object, integer: bool) -> bool: diff --git a/tests/golden/python/python-httpx/untagged-union/python_union_conversion/models/_union.py.golden b/tests/golden/python/python-httpx/untagged-union/python_union_conversion/models/_union.py.golden index b537ed00a..c7d820872 100644 --- a/tests/golden/python/python-httpx/untagged-union/python_union_conversion/models/_union.py.golden +++ b/tests/golden/python/python-httpx/untagged-union/python_union_conversion/models/_union.py.golden @@ -259,7 +259,7 @@ def value_18_from_dict(value: object) -> object: return {key: value_11_from_dict(item) for key, item in _union_support.as_dict(value).items()} def value_18_to_dict(value: object) -> object: - return {key: value_11_to_dict(item) for key, item in _union_support.as_dict(value).items()} + return {key: value_11_to_dict(item) for key, item in _union_support.mapping(value).items()} def value_19_from_dict(value: object) -> object: return [value_20_from_dict(item) for item in _union_support.as_list(value)] @@ -271,7 +271,7 @@ def value_20_from_dict(value: object) -> object: return {key: value_17_from_dict(item) for key, item in _union_support.as_dict(value).items()} def value_20_to_dict(value: object) -> object: - return {key: value_17_to_dict(item) for key, item in _union_support.as_dict(value).items()} + return {key: value_17_to_dict(item) for key, item in _union_support.mapping(value).items()} def value_21_from_dict(value: object) -> object: return value_22_from_dict(value) @@ -491,7 +491,7 @@ def value_34_from_dict(value: object) -> object: def value_34_to_dict(value: object) -> object: if (_union_support.is_object(value) - and (_union_support.has(value, 'input_schema', 'input_schema', True) and (_union_support.is_dict(_union_support.get(value, 'input_schema', 'input_schema')) and all(True for _item in _union_support.as_dict(_union_support.get(value, 'input_schema', 'input_schema')).values()))) + and (_union_support.has(value, 'input_schema', 'input_schema', True) and (_union_support.is_mapping(_union_support.get(value, 'input_schema', 'input_schema')) and all(True for _item in _union_support.mapping(_union_support.get(value, 'input_schema', 'input_schema')).values()))) and (_union_support.has(value, 'name', 'name', True) and isinstance(_union_support.get(value, 'name', 'name'), str))): return value_35_to_dict(value) @@ -691,7 +691,7 @@ def value_46_from_dict(value: object) -> object: return {key: value_45_from_dict(item) for key, item in _union_support.as_dict(value).items()} def value_46_to_dict(value: object) -> object: - return {key: value_45_to_dict(item) for key, item in _union_support.as_dict(value).items()} + return {key: value_45_to_dict(item) for key, item in _union_support.mapping(value).items()} def value_47_from_dict(value: object) -> object: return [value_44_from_dict(item) for item in _union_support.as_list(value)] @@ -743,13 +743,13 @@ def value_48_from_dict(value: object) -> object: def value_48_to_dict(value: object) -> object: if (_union_support.is_object(value) - and (_union_support.has(value, 'mapping', 'mapping', True) and (_union_support.is_dict(_union_support.get(value, 'mapping', 'mapping')) and all(((_union_support.is_object(_item) + and (_union_support.has(value, 'mapping', 'mapping', True) and (_union_support.is_mapping(_union_support.get(value, 'mapping', 'mapping')) and all(((_union_support.is_object(_item) and (_union_support.has(_item, 'name', 'name', True) and isinstance(_union_support.get(_item, 'name', 'name'), str))) or (_union_support.is_object(_item) and (_union_support.has(_item, 'revision', 'revision', True) and (_union_support.number(_union_support.get(_item, 'revision', 'revision'), True) and _union_support.equal(_union_support.get(_item, 'revision', 'revision'), 2))) and (not _union_support.has(_item, 'second_value', 'second_value', False) or isinstance(_union_support.get(_item, 'second_value', 'second_value'), str))) - or _union_support.number(_item, True)) for _item in _union_support.as_dict(_union_support.get(value, 'mapping', 'mapping')).values()))) + or _union_support.number(_item, True)) for _item in _union_support.mapping(_union_support.get(value, 'mapping', 'mapping')).values()))) and (_union_support.has(value, 'value', 'value', True) and ((_union_support.is_object(_union_support.get(value, 'value', 'value')) and (_union_support.has(_union_support.get(value, 'value', 'value'), 'name', 'name', True) and isinstance(_union_support.get(_union_support.get(value, 'value', 'value'), 'name', 'name'), str))) or (_union_support.is_object(_union_support.get(value, 'value', 'value')) diff --git a/tests/golden/python/python-httpx/untagged-union/python_union_conversion/models/client_tool.py.golden b/tests/golden/python/python-httpx/untagged-union/python_union_conversion/models/client_tool.py.golden index b65bef982..d34e4696b 100644 --- a/tests/golden/python/python-httpx/untagged-union/python_union_conversion/models/client_tool.py.golden +++ b/tests/golden/python/python-httpx/untagged-union/python_union_conversion/models/client_tool.py.golden @@ -5,11 +5,11 @@ from __future__ import annotations from dataclasses import dataclass -from typing import Any +from typing import Any as _Any @dataclass class ClientTool: - input_schema: dict[str, Any] + input_schema: dict[str, _Any] name: str def to_dict(self) -> dict[str, object]: diff --git a/tests/golden/python/python-httpx/untagged-union/python_union_conversion/models/open_choice.py.golden b/tests/golden/python/python-httpx/untagged-union/python_union_conversion/models/open_choice.py.golden index 3bcd71d81..5b3fd2874 100644 --- a/tests/golden/python/python-httpx/untagged-union/python_union_conversion/models/open_choice.py.golden +++ b/tests/golden/python/python-httpx/untagged-union/python_union_conversion/models/open_choice.py.golden @@ -4,13 +4,13 @@ from __future__ import annotations -from typing import Any +from typing import Any as _Any from . import _union from .numeric_two import NumericTwo type OpenChoice = ( - Any + _Any | NumericTwo ) diff --git a/tests/golden/python/python-httpx/untagged-union/python_union_conversion/runtime/_union_support.py.golden b/tests/golden/python/python-httpx/untagged-union/python_union_conversion/runtime/_union_support.py.golden index a4c9e6107..ea1b63caa 100644 --- a/tests/golden/python/python-httpx/untagged-union/python_union_conversion/runtime/_union_support.py.golden +++ b/tests/golden/python/python-httpx/untagged-union/python_union_conversion/runtime/_union_support.py.golden @@ -6,7 +6,7 @@ from __future__ import annotations -from dataclasses import is_dataclass +from dataclasses import fields, is_dataclass from enum import Enum from math import isfinite from typing import Protocol, TypeGuard, cast @@ -32,6 +32,24 @@ def is_list(value: object) -> TypeGuard[list[object]]: return isinstance(value, list) +def _additional_properties(value: object, dictionary_only: bool = False) -> dict[str, object] | None: + if not isinstance(value, type) and is_dataclass(value): + for field in fields(value): + if field.metadata.get("openapi_additional_properties") is True: + if not dictionary_only or field.metadata.get("openapi_dictionary") is True: + return as_dict(getattr(value, field.name)) + return None + + +def is_mapping(value: object) -> bool: + return is_dict(value) or _additional_properties(value, True) is not None + + +def mapping(value: object) -> dict[str, object]: + properties = _additional_properties(value, True) + return properties if properties is not None else as_dict(value) + + def scalar(value: object) -> object: return value.value if isinstance(value, Enum) else value @@ -57,9 +75,10 @@ def get(value: object, wire: str, field: str) -> object: def extras(value: object, known: list[str]) -> list[object]: - if not is_dict(value): - return [] - return [item for key, item in as_dict(value).items() if key not in known] + if is_dict(value): + return [item for key, item in as_dict(value).items() if key not in known] + properties = _additional_properties(value) + return list(properties.values()) if properties is not None else [] def number(value: object, integer: bool) -> bool: diff --git a/tests/golden/python/python-requests/additional-properties/additional_properties_api/models/leaf_value.py.golden b/tests/golden/python/python-requests/additional-properties/additional_properties_api/models/leaf_value.py.golden index 309555cbe..d51fa5ab5 100644 --- a/tests/golden/python/python-requests/additional-properties/additional_properties_api/models/leaf_value.py.golden +++ b/tests/golden/python/python-requests/additional-properties/additional_properties_api/models/leaf_value.py.golden @@ -6,14 +6,14 @@ from __future__ import annotations from dataclasses import dataclass -from typing import Any +from typing import Any as _Any @dataclass class LeafValue: """Leaf level: fixed fields plus maps with string and integer value kinds..""" attributes: dict[str, str] value: int - extras: dict[str, Any] | None = None + extras: dict[str, _Any] | None = None label: str | None = None scores: dict[str, int] | None = None diff --git a/tests/golden/python/python-requests/additional-properties/additional_properties_api/models/middle_level.py.golden b/tests/golden/python/python-requests/additional-properties/additional_properties_api/models/middle_level.py.golden index 4a22d89bb..56e1a3f67 100644 --- a/tests/golden/python/python-requests/additional-properties/additional_properties_api/models/middle_level.py.golden +++ b/tests/golden/python/python-requests/additional-properties/additional_properties_api/models/middle_level.py.golden @@ -6,7 +6,7 @@ from __future__ import annotations from dataclasses import dataclass -from typing import Any +from typing import Any as _Any from .leaf_value import LeafValue @@ -16,7 +16,7 @@ class MiddleLevel: count: int key: str nested: dict[str, LeafValue] - extras: dict[str, Any] | None = None + extras: dict[str, _Any] | None = None flags: dict[str, bool] | None = None def to_dict(self) -> dict[str, object]: diff --git a/tests/golden/python/python-requests/additional-properties/additional_properties_api/models/root_level.py.golden b/tests/golden/python/python-requests/additional-properties/additional_properties_api/models/root_level.py.golden index 40b931333..8afc9ba17 100644 --- a/tests/golden/python/python-requests/additional-properties/additional_properties_api/models/root_level.py.golden +++ b/tests/golden/python/python-requests/additional-properties/additional_properties_api/models/root_level.py.golden @@ -6,7 +6,7 @@ from __future__ import annotations from dataclasses import dataclass -from typing import Any +from typing import Any as _Any from .middle_level import MiddleLevel @@ -16,7 +16,7 @@ class RootLevel: children: dict[str, MiddleLevel] id: int name: str - extras: dict[str, Any] | None = None + extras: dict[str, _Any] | None = None labels: dict[str, list[str]] | None = None def to_dict(self) -> dict[str, object]: diff --git a/tests/golden/python/python-requests/comprehensive-schemas/comprehensive_schema_types_example/models/_union.py.golden b/tests/golden/python/python-requests/comprehensive-schemas/comprehensive_schema_types_example/models/_union.py.golden index e458295c7..41bb3ec23 100644 --- a/tests/golden/python/python-requests/comprehensive-schemas/comprehensive_schema_types_example/models/_union.py.golden +++ b/tests/golden/python/python-requests/comprehensive-schemas/comprehensive_schema_types_example/models/_union.py.golden @@ -38,11 +38,31 @@ def value_2_to_dict(value: object) -> object: return _union_support.scalar(value) def value_3_from_dict(value: object) -> object: + return _union_support.scalar(value) + +def value_3_to_dict(value: object) -> object: + return _union_support.scalar(value) + +def value_4_from_dict(value: object) -> object: + from.import object_with_additional_properties_true + return object_with_additional_properties_true.ObjectWithAdditionalPropertiesTrue.from_dict(_union_support.as_dict(value)) + +def value_4_to_dict(value: object) -> object: + return _union_support.model_to_dict(value) + +def value_5_from_dict(value: object) -> object: + from.import object_with_typed_additional_properties + return object_with_typed_additional_properties.ObjectWithTypedAdditionalProperties.from_dict(_union_support.as_dict(value)) + +def value_5_to_dict(value: object) -> object: + return _union_support.model_to_dict(value) + +def value_6_from_dict(value: object) -> object: if (_union_support.is_object(value) and (not _union_support.has(value, 'active', 'active', False) or isinstance(_union_support.get(value, 'active', 'active'), bool)) and (_union_support.has(value, 'id', 'id', True) and _union_support.number(_union_support.get(value, 'id', 'id'), True)) and (_union_support.has(value, 'name', 'name', True) and isinstance(_union_support.get(value, 'name', 'name'), str))): - return value_4_from_dict(value) + return value_7_from_dict(value) if (_union_support.is_object(value) and (not _union_support.has(value, 'metadata', 'metadata', False) or (_union_support.is_object(_union_support.get(value, 'metadata', 'metadata')) @@ -50,16 +70,16 @@ def value_3_from_dict(value: object) -> object: and (not _union_support.has(value, 'user', 'user', False) or (_union_support.is_object(_union_support.get(value, 'user', 'user')) and (_union_support.has(_union_support.get(value, 'user', 'user'), 'id', 'id', True) and _union_support.number(_union_support.get(_union_support.get(value, 'user', 'user'), 'id', 'id'), True)) and (not _union_support.has(_union_support.get(value, 'user', 'user'), 'name', 'name', False) or isinstance(_union_support.get(_union_support.get(value, 'user', 'user'), 'name', 'name'), str))))): - return value_5_from_dict(value) + return value_8_from_dict(value) return value -def value_3_to_dict(value: object) -> object: +def value_6_to_dict(value: object) -> object: if (_union_support.is_object(value) and (not _union_support.has(value, 'active', 'active', False) or isinstance(_union_support.get(value, 'active', 'active'), bool)) and (_union_support.has(value, 'id', 'id', True) and _union_support.number(_union_support.get(value, 'id', 'id'), True)) and (_union_support.has(value, 'name', 'name', True) and isinstance(_union_support.get(value, 'name', 'name'), str))): - return value_4_to_dict(value) + return value_7_to_dict(value) if (_union_support.is_object(value) and (not _union_support.has(value, 'metadata', 'metadata', False) or (_union_support.is_object(_union_support.get(value, 'metadata', 'metadata')) @@ -67,25 +87,25 @@ def value_3_to_dict(value: object) -> object: and (not _union_support.has(value, 'user', 'user', False) or (_union_support.is_object(_union_support.get(value, 'user', 'user')) and (_union_support.has(_union_support.get(value, 'user', 'user'), 'id', 'id', True) and _union_support.number(_union_support.get(_union_support.get(value, 'user', 'user'), 'id', 'id'), True)) and (not _union_support.has(_union_support.get(value, 'user', 'user'), 'name', 'name', False) or isinstance(_union_support.get(_union_support.get(value, 'user', 'user'), 'name', 'name'), str))))): - return value_5_to_dict(value) + return value_8_to_dict(value) return value -def value_4_from_dict(value: object) -> object: +def value_7_from_dict(value: object) -> object: from.import basic_object return basic_object.BasicObject.from_dict(_union_support.as_dict(value)) -def value_4_to_dict(value: object) -> object: +def value_7_to_dict(value: object) -> object: return _union_support.model_to_dict(value) -def value_5_from_dict(value: object) -> object: +def value_8_from_dict(value: object) -> object: from.import nested_object return nested_object.NestedObject.from_dict(_union_support.as_dict(value)) -def value_5_to_dict(value: object) -> object: +def value_8_to_dict(value: object) -> object: return _union_support.model_to_dict(value) -def value_6_from_dict(value: object) -> object: +def value_9_from_dict(value: object) -> object: if isinstance(value, str): return value_1_from_dict(value) @@ -93,11 +113,11 @@ def value_6_from_dict(value: object) -> object: return value_2_from_dict(value) if isinstance(value, bool): - return value_7_from_dict(value) + return value_10_from_dict(value) return value -def value_6_to_dict(value: object) -> object: +def value_9_to_dict(value: object) -> object: if isinstance(value, str): return value_1_to_dict(value) @@ -105,17 +125,17 @@ def value_6_to_dict(value: object) -> object: return value_2_to_dict(value) if isinstance(value, bool): - return value_7_to_dict(value) + return value_10_to_dict(value) return value -def value_7_from_dict(value: object) -> object: +def value_10_from_dict(value: object) -> object: return _union_support.scalar(value) -def value_7_to_dict(value: object) -> object: +def value_10_to_dict(value: object) -> object: return _union_support.scalar(value) -def value_8_from_dict(value: object) -> object: +def value_11_from_dict(value: object) -> object: if isinstance(value, str): return value_1_from_dict(value) @@ -123,14 +143,14 @@ def value_8_from_dict(value: object) -> object: and (not _union_support.has(value, 'active', 'active', False) or isinstance(_union_support.get(value, 'active', 'active'), bool)) and (_union_support.has(value, 'id', 'id', True) and _union_support.number(_union_support.get(value, 'id', 'id'), True)) and (_union_support.has(value, 'name', 'name', True) and isinstance(_union_support.get(value, 'name', 'name'), str))): - return value_4_from_dict(value) + return value_7_from_dict(value) if value is None: return None return value -def value_8_to_dict(value: object) -> object: +def value_11_to_dict(value: object) -> object: if isinstance(value, str): return value_1_to_dict(value) @@ -138,7 +158,7 @@ def value_8_to_dict(value: object) -> object: and (not _union_support.has(value, 'active', 'active', False) or isinstance(_union_support.get(value, 'active', 'active'), bool)) and (_union_support.has(value, 'id', 'id', True) and _union_support.number(_union_support.get(value, 'id', 'id'), True)) and (_union_support.has(value, 'name', 'name', True) and isinstance(_union_support.get(value, 'name', 'name'), str))): - return value_4_to_dict(value) + return value_7_to_dict(value) if value is None: return None diff --git a/tests/golden/python/python-requests/comprehensive-schemas/comprehensive_schema_types_example/models/empty_object.py.golden b/tests/golden/python/python-requests/comprehensive-schemas/comprehensive_schema_types_example/models/empty_object.py.golden index eff1a187d..4b9af30a1 100644 --- a/tests/golden/python/python-requests/comprehensive-schemas/comprehensive_schema_types_example/models/empty_object.py.golden +++ b/tests/golden/python/python-requests/comprehensive-schemas/comprehensive_schema_types_example/models/empty_object.py.golden @@ -5,8 +5,8 @@ from __future__ import annotations -from typing import Any +from typing import Any as _Any # Object without properties -type EmptyObject = (Any) +type EmptyObject = (_Any) diff --git a/tests/golden/python/python-requests/comprehensive-schemas/comprehensive_schema_types_example/models/object_with_additional_properties_true.py.golden b/tests/golden/python/python-requests/comprehensive-schemas/comprehensive_schema_types_example/models/object_with_additional_properties_true.py.golden index 67585b90c..2ba291623 100644 --- a/tests/golden/python/python-requests/comprehensive-schemas/comprehensive_schema_types_example/models/object_with_additional_properties_true.py.golden +++ b/tests/golden/python/python-requests/comprehensive-schemas/comprehensive_schema_types_example/models/object_with_additional_properties_true.py.golden @@ -5,20 +5,34 @@ from __future__ import annotations -from dataclasses import dataclass +from builtins import dict as _dict, str as _str +from dataclasses import dataclass, field as _dataclass_field +from typing import Any as _Any + +from . import _union @dataclass class ObjectWithAdditionalPropertiesTrue: """Object allowing any additional properties.""" name: str + additional_properties: _dict[_str, + _Any] = _dataclass_field(default_factory = lambda: _dict[_str, + _Any](), kw_only = True, metadata = {"openapi_additional_properties": True, "openapi_dictionary": False}) def to_dict(self) -> dict[str, object]: result: dict[str, object] = {} result["name"] = self.name + for key, item in self.additional_properties.items(): + if key in ['name']: + raise ValueError("Additional property collides with declared wire field: " + key) + + result[key] = _union.value_3_to_dict(item) + return result @classmethod def from_dict(cls, data: dict[str, object]) -> ObjectWithAdditionalPropertiesTrue: return cls( name=data["name"], # type: ignore[assignment] + additional_properties = {key: _union.value_3_from_dict(item) for key, item in data.items() if key not in ['name']}, ) diff --git a/tests/golden/python/python-requests/comprehensive-schemas/comprehensive_schema_types_example/models/object_with_typed_additional_properties.py.golden b/tests/golden/python/python-requests/comprehensive-schemas/comprehensive_schema_types_example/models/object_with_typed_additional_properties.py.golden index 3298f1c7b..0f6a17212 100644 --- a/tests/golden/python/python-requests/comprehensive-schemas/comprehensive_schema_types_example/models/object_with_typed_additional_properties.py.golden +++ b/tests/golden/python/python-requests/comprehensive-schemas/comprehensive_schema_types_example/models/object_with_typed_additional_properties.py.golden @@ -5,20 +5,34 @@ from __future__ import annotations -from dataclasses import dataclass +from builtins import dict as _dict, str as _str +from dataclasses import dataclass, field as _dataclass_field +from typing import cast + +from . import _union @dataclass class ObjectWithTypedAdditionalProperties: """Object with typed additional properties.""" name: str + additional_properties: _dict[_str, + float] = _dataclass_field(default_factory = lambda: _dict[_str, + float](), kw_only = True, metadata = {"openapi_additional_properties": True, "openapi_dictionary": False}) def to_dict(self) -> dict[str, object]: result: dict[str, object] = {} result["name"] = self.name + for key, item in self.additional_properties.items(): + if key in ['name']: + raise ValueError("Additional property collides with declared wire field: " + key) + + result[key] = _union.value_2_to_dict(item) + return result @classmethod def from_dict(cls, data: dict[str, object]) -> ObjectWithTypedAdditionalProperties: return cls( name=data["name"], # type: ignore[assignment] + additional_properties = {key: cast(float, _union.value_2_from_dict(item)) for key, item in data.items() if key not in ['name']}, ) diff --git a/tests/golden/python/python-requests/comprehensive-schemas/comprehensive_schema_types_example/models/one_of_objects.py.golden b/tests/golden/python/python-requests/comprehensive-schemas/comprehensive_schema_types_example/models/one_of_objects.py.golden index 198397c80..a7d76e497 100644 --- a/tests/golden/python/python-requests/comprehensive-schemas/comprehensive_schema_types_example/models/one_of_objects.py.golden +++ b/tests/golden/python/python-requests/comprehensive-schemas/comprehensive_schema_types_example/models/one_of_objects.py.golden @@ -19,7 +19,7 @@ type OneOfObjects = ( ) def one_of_objects_from_dict(value: object) -> OneOfObjects: - return cast(OneOfObjects, _union.value_3_from_dict(value)) + return cast(OneOfObjects, _union.value_6_from_dict(value)) def one_of_objects_to_dict(value: OneOfObjects) -> object: - return _union.value_3_to_dict(value) + return _union.value_6_to_dict(value) diff --git a/tests/golden/python/python-requests/comprehensive-schemas/comprehensive_schema_types_example/models/one_of_types.py.golden b/tests/golden/python/python-requests/comprehensive-schemas/comprehensive_schema_types_example/models/one_of_types.py.golden index 5e7628744..142c041cb 100644 --- a/tests/golden/python/python-requests/comprehensive-schemas/comprehensive_schema_types_example/models/one_of_types.py.golden +++ b/tests/golden/python/python-requests/comprehensive-schemas/comprehensive_schema_types_example/models/one_of_types.py.golden @@ -18,7 +18,7 @@ type OneOfTypes = ( ) def one_of_types_from_dict(value: object) -> OneOfTypes: - return cast(OneOfTypes, _union.value_6_from_dict(value)) + return cast(OneOfTypes, _union.value_9_from_dict(value)) def one_of_types_to_dict(value: OneOfTypes) -> object: - return _union.value_6_to_dict(value) + return _union.value_9_to_dict(value) diff --git a/tests/golden/python/python-requests/comprehensive-schemas/comprehensive_schema_types_example/models/one_of_with_null.py.golden b/tests/golden/python/python-requests/comprehensive-schemas/comprehensive_schema_types_example/models/one_of_with_null.py.golden index fb08327fc..0ef510275 100644 --- a/tests/golden/python/python-requests/comprehensive-schemas/comprehensive_schema_types_example/models/one_of_with_null.py.golden +++ b/tests/golden/python/python-requests/comprehensive-schemas/comprehensive_schema_types_example/models/one_of_with_null.py.golden @@ -19,7 +19,7 @@ type OneOfWithNull = ( ) def one_of_with_null_from_dict(value: object) -> OneOfWithNull: - return cast(OneOfWithNull, _union.value_8_from_dict(value)) + return cast(OneOfWithNull, _union.value_11_from_dict(value)) def one_of_with_null_to_dict(value: OneOfWithNull) -> object: - return _union.value_8_to_dict(value) + return _union.value_11_to_dict(value) diff --git a/tests/golden/python/python-requests/comprehensive-schemas/comprehensive_schema_types_example/runtime/_union_support.py.golden b/tests/golden/python/python-requests/comprehensive-schemas/comprehensive_schema_types_example/runtime/_union_support.py.golden index c24a70570..f618e5fa9 100644 --- a/tests/golden/python/python-requests/comprehensive-schemas/comprehensive_schema_types_example/runtime/_union_support.py.golden +++ b/tests/golden/python/python-requests/comprehensive-schemas/comprehensive_schema_types_example/runtime/_union_support.py.golden @@ -7,7 +7,7 @@ from __future__ import annotations -from dataclasses import is_dataclass +from dataclasses import fields, is_dataclass from enum import Enum from math import isfinite from typing import Protocol, TypeGuard, cast @@ -33,6 +33,24 @@ def is_list(value: object) -> TypeGuard[list[object]]: return isinstance(value, list) +def _additional_properties(value: object, dictionary_only: bool = False) -> dict[str, object] | None: + if not isinstance(value, type) and is_dataclass(value): + for field in fields(value): + if field.metadata.get("openapi_additional_properties") is True: + if not dictionary_only or field.metadata.get("openapi_dictionary") is True: + return as_dict(getattr(value, field.name)) + return None + + +def is_mapping(value: object) -> bool: + return is_dict(value) or _additional_properties(value, True) is not None + + +def mapping(value: object) -> dict[str, object]: + properties = _additional_properties(value, True) + return properties if properties is not None else as_dict(value) + + def scalar(value: object) -> object: return value.value if isinstance(value, Enum) else value @@ -58,9 +76,10 @@ def get(value: object, wire: str, field: str) -> object: def extras(value: object, known: list[str]) -> list[object]: - if not is_dict(value): - return [] - return [item for key, item in as_dict(value).items() if key not in known] + if is_dict(value): + return [item for key, item in as_dict(value).items() if key not in known] + properties = _additional_properties(value) + return list(properties.values()) if properties is not None else [] def number(value: object, integer: bool) -> bool: diff --git a/tests/golden/python/python-requests/delete-with-response-schema/test_api_with_delete_response_schema/models/create_test_resource_request.py.golden b/tests/golden/python/python-requests/delete-with-response-schema/test_api_with_delete_response_schema/models/create_test_resource_request.py.golden index 9efe100fd..d7d75113f 100644 --- a/tests/golden/python/python-requests/delete-with-response-schema/test_api_with_delete_response_schema/models/create_test_resource_request.py.golden +++ b/tests/golden/python/python-requests/delete-with-response-schema/test_api_with_delete_response_schema/models/create_test_resource_request.py.golden @@ -5,8 +5,8 @@ from __future__ import annotations -from typing import Any +from typing import Any as _Any # Request to create a new test resource (type alias - no properties) -type CreateTestResourceRequest = (Any) +type CreateTestResourceRequest = (_Any) diff --git a/tests/golden/python/python-requests/enum-repr/enum_representation_api/runtime/_union_support.py.golden b/tests/golden/python/python-requests/enum-repr/enum_representation_api/runtime/_union_support.py.golden index ee0abbd31..a1a48005b 100644 --- a/tests/golden/python/python-requests/enum-repr/enum_representation_api/runtime/_union_support.py.golden +++ b/tests/golden/python/python-requests/enum-repr/enum_representation_api/runtime/_union_support.py.golden @@ -7,7 +7,7 @@ from __future__ import annotations -from dataclasses import is_dataclass +from dataclasses import fields, is_dataclass from enum import Enum from math import isfinite from typing import Protocol, TypeGuard, cast @@ -33,6 +33,24 @@ def is_list(value: object) -> TypeGuard[list[object]]: return isinstance(value, list) +def _additional_properties(value: object, dictionary_only: bool = False) -> dict[str, object] | None: + if not isinstance(value, type) and is_dataclass(value): + for field in fields(value): + if field.metadata.get("openapi_additional_properties") is True: + if not dictionary_only or field.metadata.get("openapi_dictionary") is True: + return as_dict(getattr(value, field.name)) + return None + + +def is_mapping(value: object) -> bool: + return is_dict(value) or _additional_properties(value, True) is not None + + +def mapping(value: object) -> dict[str, object]: + properties = _additional_properties(value, True) + return properties if properties is not None else as_dict(value) + + def scalar(value: object) -> object: return value.value if isinstance(value, Enum) else value @@ -58,9 +76,10 @@ def get(value: object, wire: str, field: str) -> object: def extras(value: object, known: list[str]) -> list[object]: - if not is_dict(value): - return [] - return [item for key, item in as_dict(value).items() if key not in known] + if is_dict(value): + return [item for key, item in as_dict(value).items() if key not in known] + properties = _additional_properties(value) + return list(properties.values()) if properties is not None else [] def number(value: object, integer: bool) -> bool: diff --git a/tests/golden/python/python-requests/object-extensions/README.md.golden b/tests/golden/python/python-requests/object-extensions/README.md.golden new file mode 100644 index 000000000..a71356902 --- /dev/null +++ b/tests/golden/python/python-requests/object-extensions/README.md.golden @@ -0,0 +1,7 @@ +# Python additional properties + +Generated Python SDK. + +Version: `1.0.0` + +Generated by [openapi-nexus](https://github.com/rust-codegen-group/openapi-nexus) for `python_additional_properties`. diff --git a/tests/golden/python/python-requests/object-extensions/pyproject.toml.golden b/tests/golden/python/python-requests/object-extensions/pyproject.toml.golden new file mode 100644 index 000000000..17fef54d6 --- /dev/null +++ b/tests/golden/python/python-requests/object-extensions/pyproject.toml.golden @@ -0,0 +1,10 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "python_additional_properties" +version = "1.0.0" +description = "Generated Python SDK." +requires-python = ">=3.12" +dependencies = ["requests>=2.32"] diff --git a/tests/golden/python/python-requests/object-extensions/python_additional_properties/__init__.py.golden b/tests/golden/python/python-requests/object-extensions/python_additional_properties/__init__.py.golden new file mode 100644 index 000000000..c89e3c783 --- /dev/null +++ b/tests/golden/python/python-requests/object-extensions/python_additional_properties/__init__.py.golden @@ -0,0 +1,10 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python additional properties — 1.0.0 + +from .runtime import ApiKeyAuth as ApiKeyAuth +from .runtime import Authenticator as Authenticator +from .runtime import BearerAuth as BearerAuth +from .runtime import ApiResponse as ApiResponse +from .runtime import Client as Client +from .runtime import ApiError as ApiError diff --git a/tests/golden/python/python-requests/object-extensions/python_additional_properties/apis/__init__.py.golden b/tests/golden/python/python-requests/object-extensions/python_additional_properties/apis/__init__.py.golden new file mode 100644 index 000000000..565d953ad --- /dev/null +++ b/tests/golden/python/python-requests/object-extensions/python_additional_properties/apis/__init__.py.golden @@ -0,0 +1,5 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python additional properties — 1.0.0 + +from .default_api import DefaultApi as DefaultApi diff --git a/tests/golden/python/python-requests/object-extensions/python_additional_properties/apis/default_api.py.golden b/tests/golden/python/python-requests/object-extensions/python_additional_properties/apis/default_api.py.golden new file mode 100644 index 000000000..e2a0a6188 --- /dev/null +++ b/tests/golden/python/python-requests/object-extensions/python_additional_properties/apis/default_api.py.golden @@ -0,0 +1,118 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python additional properties — 1.0.0 + +from __future__ import annotations + +import json + +from collections.abc import Mapping +from typing import cast + +from ..models import _union +from ..models.dictionary_alias import DictionaryAlias +from ..models.mixed_map import MixedMap +from ..models.typed_map import TypedMap +from ..runtime.client import ApiResponse, Client +from ..runtime.errors import ApiError + +class EchoDictionaryUnexpected: + def __init__(self, status_code: int, headers: Mapping[str, str], raw_body: bytes) -> None: + self.status_code: int = status_code + self.headers: Mapping[str, str] = headers + self.raw_body: bytes = raw_body + @property + def body(self) -> bytes: + return self.raw_body + +type EchoDictionaryErrorDetail = ( + EchoDictionaryUnexpected +) + +class EchoDictionaryError(ApiError): + def __init__(self, status_code: int, status: str, body: bytes, detail: EchoDictionaryErrorDetail, headers: Mapping[str, str] | None = None, response: object | None = None) -> None: + self.detail: EchoDictionaryErrorDetail = detail + super().__init__(status_code, status, body, headers=headers, response=response) + +class EchoMixedBadRequest: + def __init__(self, status_code: int, headers: Mapping[str, str], raw_body: bytes) -> None: + self.status_code: int = status_code + self.headers: Mapping[str, str] = headers + self.raw_body: bytes = raw_body + self._body_loaded: bool = False + self._body_value: TypedMap | None = None + self._body_error: Exception | None = None + @property + def body(self) -> TypedMap: + if not self._body_loaded: + try: + self._body_value = cast(TypedMap, _union.value_15_from_dict(json.loads(self.raw_body.decode("utf-8")))) + + except Exception as exc: + self._body_error = exc + + self._body_loaded = True + + if self._body_error is not None: + raise self._body_error + + return cast(TypedMap, self._body_value) + +class EchoMixedUnexpected: + def __init__(self, status_code: int, headers: Mapping[str, str], raw_body: bytes) -> None: + self.status_code: int = status_code + self.headers: Mapping[str, str] = headers + self.raw_body: bytes = raw_body + @property + def body(self) -> bytes: + return self.raw_body + +type EchoMixedErrorDetail = ( + EchoMixedBadRequest + | EchoMixedUnexpected +) + +class EchoMixedError(ApiError): + def __init__(self, status_code: int, status: str, body: bytes, detail: EchoMixedErrorDetail, headers: Mapping[str, str] | None = None, response: object | None = None) -> None: + self.detail: EchoMixedErrorDetail = detail + super().__init__(status_code, status, body, headers=headers, response=response) + + +class DefaultApi: + def __init__(self, client: Client) -> None: + self._client = client + + def echo_dictionary_with_http_info(self, *, body: DictionaryAlias) -> ApiResponse[DictionaryAlias]: + path = "/dictionary" + headers: dict[str, str] = {} + headers["Content-Type"] = "application/json" + _union_body = _union.value_14_to_dict(body) + response = self._client.request("POST", path, headers=headers, data = json.dumps(_union_body, allow_nan = False)) + if not (200 <= response.status_code < 300): + detail = EchoDictionaryUnexpected(response.status_code, response.headers, response.content) + raise EchoDictionaryError(response.status_code, response.reason, response.content, detail, headers=response.headers, response=response) + _union_data = cast(DictionaryAlias, _union.value_14_from_dict(response.json())) + data = _union_data + return ApiResponse(data=data, status_code=response.status_code, headers=response.headers, raw=response) + + def echo_dictionary(self, *, body: DictionaryAlias) -> DictionaryAlias: + return self.echo_dictionary_with_http_info(body=body).data + + def echo_mixed_with_http_info(self, *, body: MixedMap) -> ApiResponse[MixedMap]: + path = "/mixed" + headers: dict[str, str] = {} + headers["Content-Type"] = "application/json" + _union_body = _union.value_24_to_dict(body) + response = self._client.request("POST", path, headers=headers, data = json.dumps(_union_body, allow_nan = False)) + if not (200 <= response.status_code < 300): + if response.status_code == 400: + detail = EchoMixedBadRequest(response.status_code, response.headers, response.content) + else: + detail = EchoMixedUnexpected(response.status_code, response.headers, response.content) + raise EchoMixedError(response.status_code, response.reason, response.content, detail, headers=response.headers, response=response) + _union_data = cast(MixedMap, _union.value_24_from_dict(response.json())) + data = _union_data + return ApiResponse(data=data, status_code=response.status_code, headers=response.headers, raw=response) + + def echo_mixed(self, *, body: MixedMap) -> MixedMap: + return self.echo_mixed_with_http_info(body=body).data diff --git a/tests/golden/python/python-requests/object-extensions/python_additional_properties/models/__init__.py.golden b/tests/golden/python/python-requests/object-extensions/python_additional_properties/models/__init__.py.golden new file mode 100644 index 000000000..6f97d0328 --- /dev/null +++ b/tests/golden/python/python-requests/object-extensions/python_additional_properties/models/__init__.py.golden @@ -0,0 +1,40 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python additional properties — 1.0.0 + +from .alias_values import AliasValues as AliasValues +from .ambiguous_number import AmbiguousNumber as AmbiguousNumber +from .ambiguous_values import AmbiguousValues as AmbiguousValues +from .any import Any as Any +from .choices import Choices as Choices +from .closed import Closed as Closed +from .deep_values import DeepValues as DeepValues +from .dictionary_alias import DictionaryAlias as DictionaryAlias +from .dictionary_choice import DictionaryChoice as DictionaryChoice +from .event import Event as Event +from .event_completed import EventCompleted as EventCompleted +from .event_searching import EventSearching as EventSearching +from .event_values import EventValues as EventValues +from .factory_collision import FactoryCollision as FactoryCollision +from .holder import Holder as Holder +from .implicit import Implicit as Implicit +from .legacy_optional_holder import LegacyOptionalHolder as LegacyOptionalHolder +from .mixed_choice import MixedChoice as MixedChoice +from .mixed_map import MixedMap as MixedMap +from .nested_value import NestedValue as NestedValue +from .node import Node as Node +from .nodes import Nodes as Nodes +from .nullable_values import NullableValues as NullableValues +from .numbers import Numbers as Numbers +from .opaque_map import OpaqueMap as OpaqueMap +from .optional_holder import OptionalHolder as OptionalHolder +from .optional_known import OptionalKnown as OptionalKnown +from .pure_recursive import PureRecursive as PureRecursive +from .raw_map import RawMap as RawMap +from .recursive import Recursive as Recursive +from .status import Status as Status +from .status_values import StatusValues as StatusValues +from .storage_collision import StorageCollision as StorageCollision +from .typed_map import TypedMap as TypedMap +from .value_alias import ValueAlias as ValueAlias +from .value_choice import ValueChoice as ValueChoice diff --git a/tests/golden/python/python-requests/object-extensions/python_additional_properties/models/_union.py.golden b/tests/golden/python/python-requests/object-extensions/python_additional_properties/models/_union.py.golden new file mode 100644 index 000000000..b8d329f16 --- /dev/null +++ b/tests/golden/python/python-requests/object-extensions/python_additional_properties/models/_union.py.golden @@ -0,0 +1,363 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python additional properties — 1.0.0 + +from __future__ import annotations + +from ..runtime import _union_support + +def value_0_from_dict(value: object) -> object: + return value_1_from_dict(value) + +def value_0_to_dict(value: object) -> object: + return value_1_to_dict(value) + +def value_1_from_dict(value: object) -> object: + from.import nested_value + return nested_value.NestedValue.from_dict(_union_support.as_dict(value)) + +def value_1_to_dict(value: object) -> object: + return _union_support.model_to_dict(value) + +def value_2_from_dict(value: object) -> object: + from.import alias_values + return alias_values.AliasValues.from_dict(_union_support.as_dict(value)) + +def value_2_to_dict(value: object) -> object: + return _union_support.model_to_dict(value) + +def value_3_from_dict(value: object) -> object: + if _union_support.number(value, True): + return value_4_from_dict(value) + + if _union_support.number(value, False): + return value_5_from_dict(value) + + return value + +def value_3_to_dict(value: object) -> object: + if _union_support.number(value, True): + return value_4_to_dict(value) + + if _union_support.number(value, False): + return value_5_to_dict(value) + + return value + +def value_4_from_dict(value: object) -> object: + return _union_support.scalar(value) + +def value_4_to_dict(value: object) -> object: + return _union_support.scalar(value) + +def value_5_from_dict(value: object) -> object: + return _union_support.scalar(value) + +def value_5_to_dict(value: object) -> object: + return _union_support.scalar(value) + +def value_6_from_dict(value: object) -> object: + from.import ambiguous_values + return ambiguous_values.AmbiguousValues.from_dict(_union_support.as_dict(value)) + +def value_6_to_dict(value: object) -> object: + return _union_support.model_to_dict(value) + +def value_7_from_dict(value: object) -> object: + return _union_support.scalar(value) + +def value_7_to_dict(value: object) -> object: + return _union_support.scalar(value) + +def value_8_from_dict(value: object) -> object: + from.import any + return any.Any.from_dict(_union_support.as_dict(value)) + +def value_8_to_dict(value: object) -> object: + return _union_support.model_to_dict(value) + +def value_9_from_dict(value: object) -> object: + if (_union_support.is_object(value) + and (_union_support.has(value, 'wire_value', 'wire_value', True) and isinstance(_union_support.get(value, 'wire_value', 'wire_value'), str))): + return value_1_from_dict(value) + + if _union_support.number(value, True): + return value_4_from_dict(value) + + return value + +def value_9_to_dict(value: object) -> object: + if (_union_support.is_object(value) + and (_union_support.has(value, 'wire_value', 'wire_value', True) and isinstance(_union_support.get(value, 'wire_value', 'wire_value'), str))): + return value_1_to_dict(value) + + if _union_support.number(value, True): + return value_4_to_dict(value) + + return value + +def value_10_from_dict(value: object) -> object: + from.import choices + return choices.Choices.from_dict(_union_support.as_dict(value)) + +def value_10_to_dict(value: object) -> object: + return _union_support.model_to_dict(value) + +def value_11_from_dict(value: object) -> object: + return [value_12_from_dict(item) for item in _union_support.as_list(value)] + +def value_11_to_dict(value: object) -> object: + return [value_12_to_dict(item) for item in _union_support.as_list(value)] + +def value_12_from_dict(value: object) -> object: + return {key: value_1_from_dict(item) for key, item in _union_support.as_dict(value).items()} + +def value_12_to_dict(value: object) -> object: + return {key: value_1_to_dict(item) for key, item in _union_support.mapping(value).items()} + +def value_13_from_dict(value: object) -> object: + from.import deep_values + return deep_values.DeepValues.from_dict(_union_support.as_dict(value)) + +def value_13_to_dict(value: object) -> object: + return _union_support.model_to_dict(value) + +def value_14_from_dict(value: object) -> object: + return value_15_from_dict(value) + +def value_14_to_dict(value: object) -> object: + return value_15_to_dict(value) + +def value_15_from_dict(value: object) -> object: + from.import typed_map + return typed_map.TypedMap.from_dict(_union_support.as_dict(value)) + +def value_15_to_dict(value: object) -> object: + return _union_support.model_to_dict(value) + +def value_16_from_dict(value: object) -> object: + if (_union_support.is_dict(value) and all((_union_support.is_object(_item) + and (_union_support.has(_item, 'wire_value', 'wire_value', True) and isinstance(_union_support.get(_item, 'wire_value', 'wire_value'), str))) for _item in _union_support.as_dict(value).values())): + return value_15_from_dict(value) + + if _union_support.number(value, True): + return value_4_from_dict(value) + + return value + +def value_16_to_dict(value: object) -> object: + if (_union_support.is_mapping(value) and all((_union_support.is_object(_item) + and (_union_support.has(_item, 'wire_value', 'wire_value', True) and isinstance(_union_support.get(_item, 'wire_value', 'wire_value'), str))) for _item in _union_support.mapping(value).values())): + return value_15_to_dict(value) + + if _union_support.number(value, True): + return value_4_to_dict(value) + + return value + +def value_17_from_dict(value: object) -> object: + from.import event_searching + return event_searching.EventSearching.from_dict(_union_support.as_dict(value)) + +def value_17_to_dict(value: object) -> object: + return _union_support.model_to_dict(value) + +def value_18_from_dict(value: object) -> object: + from.import event_completed + return event_completed.EventCompleted.from_dict(_union_support.as_dict(value)) + +def value_18_to_dict(value: object) -> object: + return _union_support.model_to_dict(value) + +def value_19_from_dict(value: object) -> object: + data = _union_support.as_dict(value) + if data.get('type') == 'searching': + return value_17_from_dict(value) + + if data.get('type') == 'completed': + return value_18_from_dict(value) + + raise ValueError("No matching tagged union variant") + +def value_19_to_dict(value: object) -> object: + if ((_union_support.is_object(value) + and (_union_support.has(value, 'sequence_number', 'sequence_number', True) and _union_support.number(_union_support.get(value, 'sequence_number', 'sequence_number'), True)) + and (_union_support.has(value, 'type', 'type_', True) and _union_support.equal(_union_support.get(value, 'type', 'type_'), 'searching')) + and all(True for _item in _union_support.extras(value, ['sequence_number', 'type'])))): + result = dict(_union_support.as_dict(value_17_to_dict(value))) + result['type'] = 'searching' + return result + + if ((_union_support.is_object(value) + and (_union_support.has(value, 'type', 'type_', True) and _union_support.equal(_union_support.get(value, 'type', 'type_'), 'completed')) + and all(True for _item in _union_support.extras(value, ['type'])))): + result = dict(_union_support.as_dict(value_18_to_dict(value))) + result['type'] = 'completed' + return result + + raise ValueError("No matching tagged union variant") + +def value_20_from_dict(value: object) -> object: + from.import event_values + return event_values.EventValues.from_dict(_union_support.as_dict(value)) + +def value_20_to_dict(value: object) -> object: + return _union_support.model_to_dict(value) + +def value_21_from_dict(value: object) -> object: + from.import factory_collision + return factory_collision.FactoryCollision.from_dict(_union_support.as_dict(value)) + +def value_21_to_dict(value: object) -> object: + return _union_support.model_to_dict(value) + +def value_22_from_dict(value: object) -> object: + return {key: value_14_from_dict(item) for key, item in _union_support.as_dict(value).items()} + +def value_22_to_dict(value: object) -> object: + return {key: value_14_to_dict(item) for key, item in _union_support.mapping(value).items()} + +def value_23_from_dict(value: object) -> object: + return [value_15_from_dict(item) for item in _union_support.as_list(value)] + +def value_23_to_dict(value: object) -> object: + return [value_15_to_dict(item) for item in _union_support.as_list(value)] + +def value_24_from_dict(value: object) -> object: + from.import mixed_map + return mixed_map.MixedMap.from_dict(_union_support.as_dict(value)) + +def value_24_to_dict(value: object) -> object: + return _union_support.model_to_dict(value) + +def value_25_from_dict(value: object) -> object: + from.import opaque_map + return opaque_map.OpaqueMap.from_dict(_union_support.as_dict(value)) + +def value_25_to_dict(value: object) -> object: + return _union_support.model_to_dict(value) + +def value_26_from_dict(value: object) -> object: + if (_union_support.is_object(value) + and (_union_support.has(value, 'known_field', 'known_field', True) and isinstance(_union_support.get(value, 'known_field', 'known_field'), str)) + and all((_union_support.is_object(_item) + and (_union_support.has(_item, 'wire_value', 'wire_value', True) and isinstance(_union_support.get(_item, 'wire_value', 'wire_value'), str))) for _item in _union_support.extras(value, ['known_field']))): + return value_24_from_dict(value) + + if isinstance(value, str): + return value_27_from_dict(value) + + return value + +def value_26_to_dict(value: object) -> object: + if (_union_support.is_object(value) + and (_union_support.has(value, 'known_field', 'known_field', True) and isinstance(_union_support.get(value, 'known_field', 'known_field'), str)) + and all((_union_support.is_object(_item) + and (_union_support.has(_item, 'wire_value', 'wire_value', True) and isinstance(_union_support.get(_item, 'wire_value', 'wire_value'), str))) for _item in _union_support.extras(value, ['known_field']))): + return value_24_to_dict(value) + + if isinstance(value, str): + return value_27_to_dict(value) + + return value + +def value_27_from_dict(value: object) -> object: + return _union_support.scalar(value) + +def value_27_to_dict(value: object) -> object: + return _union_support.scalar(value) + +def value_28_from_dict(value: object) -> object: + from.import nodes + return nodes.Nodes.from_dict(_union_support.as_dict(value)) + +def value_28_to_dict(value: object) -> object: + return _union_support.model_to_dict(value) + +def value_29_from_dict(value: object) -> object: + from.import node + return node.Node.from_dict(_union_support.as_dict(value)) + +def value_29_to_dict(value: object) -> object: + return _union_support.model_to_dict(value) + +def value_30_from_dict(value: object) -> object: + return None if value is None else value_1_from_dict(value) + +def value_30_to_dict(value: object) -> object: + return None if value is None else value_1_to_dict(value) + +def value_31_from_dict(value: object) -> object: + from.import nullable_values + return nullable_values.NullableValues.from_dict(_union_support.as_dict(value)) + +def value_31_to_dict(value: object) -> object: + return _union_support.model_to_dict(value) + +def value_32_from_dict(value: object) -> object: + return _union_support.scalar(value) + +def value_32_to_dict(value: object) -> object: + return _union_support.scalar(value) + +def value_33_from_dict(value: object) -> object: + from.import numbers + return numbers.Numbers.from_dict(_union_support.as_dict(value)) + +def value_33_to_dict(value: object) -> object: + return _union_support.model_to_dict(value) + +def value_34_from_dict(value: object) -> object: + return None if value is None else value_24_from_dict(value) + +def value_34_to_dict(value: object) -> object: + return None if value is None else value_24_to_dict(value) + +def value_35_from_dict(value: object) -> object: + from.import optional_known + return optional_known.OptionalKnown.from_dict(_union_support.as_dict(value)) + +def value_35_to_dict(value: object) -> object: + return _union_support.model_to_dict(value) + +def value_36_from_dict(value: object) -> object: + from.import pure_recursive + return pure_recursive.PureRecursive.from_dict(_union_support.as_dict(value)) + +def value_36_to_dict(value: object) -> object: + return _union_support.model_to_dict(value) + +def value_37_from_dict(value: object) -> object: + from.import raw_map + return raw_map.RawMap.from_dict(_union_support.as_dict(value)) + +def value_37_to_dict(value: object) -> object: + return _union_support.model_to_dict(value) + +def value_38_from_dict(value: object) -> object: + from.import recursive + return recursive.Recursive.from_dict(_union_support.as_dict(value)) + +def value_38_to_dict(value: object) -> object: + return _union_support.model_to_dict(value) + +def value_39_from_dict(value: object) -> object: + from.import status + return status.Status(value) + +def value_39_to_dict(value: object) -> object: + return _union_support.scalar(value) + +def value_40_from_dict(value: object) -> object: + from.import status_values + return status_values.StatusValues.from_dict(_union_support.as_dict(value)) + +def value_40_to_dict(value: object) -> object: + return _union_support.model_to_dict(value) + +def value_41_from_dict(value: object) -> object: + from.import storage_collision + return storage_collision.StorageCollision.from_dict(_union_support.as_dict(value)) + +def value_41_to_dict(value: object) -> object: + return _union_support.model_to_dict(value) diff --git a/tests/golden/python/python-requests/object-extensions/python_additional_properties/models/alias_values.py.golden b/tests/golden/python/python-requests/object-extensions/python_additional_properties/models/alias_values.py.golden new file mode 100644 index 000000000..c3442b63d --- /dev/null +++ b/tests/golden/python/python-requests/object-extensions/python_additional_properties/models/alias_values.py.golden @@ -0,0 +1,31 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python additional properties — 1.0.0 + +from __future__ import annotations + +from builtins import dict as _dict, str as _str +from dataclasses import dataclass, field as _dataclass_field +from typing import cast + +from . import _union +from .value_alias import ValueAlias + +@dataclass +class AliasValues: + additional_properties: _dict[_str, + ValueAlias] = _dataclass_field(default_factory = lambda: _dict[_str, + ValueAlias](), kw_only = True, metadata = {"openapi_additional_properties": True, "openapi_dictionary": True}) + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + for key, item in self.additional_properties.items(): + result[key] = _union.value_0_to_dict(item) + + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> AliasValues: + return cls( + additional_properties = {key: cast(ValueAlias, _union.value_0_from_dict(item)) for key, item in data.items() }, + ) diff --git a/tests/golden/python/python-requests/object-extensions/python_additional_properties/models/ambiguous_number.py.golden b/tests/golden/python/python-requests/object-extensions/python_additional_properties/models/ambiguous_number.py.golden new file mode 100644 index 000000000..f45cff901 --- /dev/null +++ b/tests/golden/python/python-requests/object-extensions/python_additional_properties/models/ambiguous_number.py.golden @@ -0,0 +1,20 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python additional properties — 1.0.0 + +from __future__ import annotations + +from typing import cast + +from . import _union + +type AmbiguousNumber = ( + int + | float +) + +def ambiguous_number_from_dict(value: object) -> AmbiguousNumber: + return cast(AmbiguousNumber, _union.value_3_from_dict(value)) + +def ambiguous_number_to_dict(value: AmbiguousNumber) -> object: + return _union.value_3_to_dict(value) diff --git a/tests/golden/python/python-requests/object-extensions/python_additional_properties/models/ambiguous_values.py.golden b/tests/golden/python/python-requests/object-extensions/python_additional_properties/models/ambiguous_values.py.golden new file mode 100644 index 000000000..39e6a7bc5 --- /dev/null +++ b/tests/golden/python/python-requests/object-extensions/python_additional_properties/models/ambiguous_values.py.golden @@ -0,0 +1,31 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python additional properties — 1.0.0 + +from __future__ import annotations + +from builtins import dict as _dict, str as _str +from dataclasses import dataclass, field as _dataclass_field +from typing import cast + +from . import _union +from .ambiguous_number import AmbiguousNumber + +@dataclass +class AmbiguousValues: + additional_properties: _dict[_str, + AmbiguousNumber] = _dataclass_field(default_factory = lambda: _dict[_str, + AmbiguousNumber](), kw_only = True, metadata = {"openapi_additional_properties": True, "openapi_dictionary": True}) + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + for key, item in self.additional_properties.items(): + result[key] = _union.value_3_to_dict(item) + + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> AmbiguousValues: + return cls( + additional_properties = {key: cast(AmbiguousNumber, _union.value_3_from_dict(item)) for key, item in data.items() }, + ) diff --git a/tests/golden/python/python-requests/object-extensions/python_additional_properties/models/any.py.golden b/tests/golden/python/python-requests/object-extensions/python_additional_properties/models/any.py.golden new file mode 100644 index 000000000..a4bbc57c0 --- /dev/null +++ b/tests/golden/python/python-requests/object-extensions/python_additional_properties/models/any.py.golden @@ -0,0 +1,30 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python additional properties — 1.0.0 + +from __future__ import annotations + +from builtins import dict as _dict, str as _str +from dataclasses import dataclass, field as _dataclass_field +from typing import Any as _Any + +from . import _union + +@dataclass +class Any: + additional_properties: _dict[_str, + _Any] = _dataclass_field(default_factory = lambda: _dict[_str, + _Any](), kw_only = True, metadata = {"openapi_additional_properties": True, "openapi_dictionary": True}) + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + for key, item in self.additional_properties.items(): + result[key] = _union.value_7_to_dict(item) + + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> Any: + return cls( + additional_properties = {key: _union.value_7_from_dict(item) for key, item in data.items() }, + ) diff --git a/tests/golden/python/python-requests/object-extensions/python_additional_properties/models/choices.py.golden b/tests/golden/python/python-requests/object-extensions/python_additional_properties/models/choices.py.golden new file mode 100644 index 000000000..258771e5b --- /dev/null +++ b/tests/golden/python/python-requests/object-extensions/python_additional_properties/models/choices.py.golden @@ -0,0 +1,31 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python additional properties — 1.0.0 + +from __future__ import annotations + +from builtins import dict as _dict, str as _str +from dataclasses import dataclass, field as _dataclass_field +from typing import cast + +from . import _union +from .value_choice import ValueChoice + +@dataclass +class Choices: + additional_properties: _dict[_str, + ValueChoice] = _dataclass_field(default_factory = lambda: _dict[_str, + ValueChoice](), kw_only = True, metadata = {"openapi_additional_properties": True, "openapi_dictionary": True}) + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + for key, item in self.additional_properties.items(): + result[key] = _union.value_9_to_dict(item) + + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> Choices: + return cls( + additional_properties = {key: cast(ValueChoice, _union.value_9_from_dict(item)) for key, item in data.items() }, + ) diff --git a/tests/golden/python/python-requests/object-extensions/python_additional_properties/models/closed.py.golden b/tests/golden/python/python-requests/object-extensions/python_additional_properties/models/closed.py.golden new file mode 100644 index 000000000..270c76041 --- /dev/null +++ b/tests/golden/python/python-requests/object-extensions/python_additional_properties/models/closed.py.golden @@ -0,0 +1,23 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python additional properties — 1.0.0 + +from __future__ import annotations + +from dataclasses import dataclass + +@dataclass +class Closed: + known_field: str | None = None + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + if self.known_field is not None: + result["known_field"] = self.known_field + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> Closed: + return cls( + known_field=data.get("known_field"), # type: ignore[assignment] + ) diff --git a/tests/golden/python/python-requests/object-extensions/python_additional_properties/models/deep_values.py.golden b/tests/golden/python/python-requests/object-extensions/python_additional_properties/models/deep_values.py.golden new file mode 100644 index 000000000..c8ab8b5db --- /dev/null +++ b/tests/golden/python/python-requests/object-extensions/python_additional_properties/models/deep_values.py.golden @@ -0,0 +1,33 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python additional properties — 1.0.0 + +from __future__ import annotations + +from builtins import dict as _dict, str as _str +from dataclasses import dataclass, field as _dataclass_field +from typing import cast + +from . import _union +from .nested_value import NestedValue + +@dataclass +class DeepValues: + additional_properties: _dict[_str, + list[dict[str, NestedValue]]] = _dataclass_field(default_factory = lambda: _dict[_str, + list[dict[str, + NestedValue]]](), kw_only = True, metadata = {"openapi_additional_properties": True, "openapi_dictionary": True}) + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + for key, item in self.additional_properties.items(): + result[key] = _union.value_11_to_dict(item) + + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> DeepValues: + return cls( + additional_properties = {key: cast(list[dict[str, + NestedValue]], _union.value_11_from_dict(item)) for key, item in data.items() }, + ) diff --git a/tests/golden/python/python-requests/object-extensions/python_additional_properties/models/dictionary_alias.py.golden b/tests/golden/python/python-requests/object-extensions/python_additional_properties/models/dictionary_alias.py.golden new file mode 100644 index 000000000..e915ac108 --- /dev/null +++ b/tests/golden/python/python-requests/object-extensions/python_additional_properties/models/dictionary_alias.py.golden @@ -0,0 +1,9 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python additional properties — 1.0.0 + +from __future__ import annotations + +from .typed_map import TypedMap + +type DictionaryAlias = (TypedMap) diff --git a/tests/golden/python/python-requests/object-extensions/python_additional_properties/models/dictionary_choice.py.golden b/tests/golden/python/python-requests/object-extensions/python_additional_properties/models/dictionary_choice.py.golden new file mode 100644 index 000000000..6cbe6b270 --- /dev/null +++ b/tests/golden/python/python-requests/object-extensions/python_additional_properties/models/dictionary_choice.py.golden @@ -0,0 +1,21 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python additional properties — 1.0.0 + +from __future__ import annotations + +from typing import cast + +from . import _union +from .typed_map import TypedMap + +type DictionaryChoice = ( + TypedMap + | int +) + +def dictionary_choice_from_dict(value: object) -> DictionaryChoice: + return cast(DictionaryChoice, _union.value_16_from_dict(value)) + +def dictionary_choice_to_dict(value: DictionaryChoice) -> object: + return _union.value_16_to_dict(value) diff --git a/tests/golden/python/python-requests/object-extensions/python_additional_properties/models/event.py.golden b/tests/golden/python/python-requests/object-extensions/python_additional_properties/models/event.py.golden new file mode 100644 index 000000000..babf58ca2 --- /dev/null +++ b/tests/golden/python/python-requests/object-extensions/python_additional_properties/models/event.py.golden @@ -0,0 +1,36 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python additional properties — 1.0.0 + +from __future__ import annotations + +from .event_completed import EventCompleted +from .event_searching import EventSearching + +# Discriminator: type (internal). + +type Event = ( + EventSearching + | EventCompleted +) + + +def event_from_dict(data: dict[str, object]) -> Event: + _tag = data["type"] + if _tag == "searching": + return EventSearching.from_dict(data) + elif _tag == "completed": + return EventCompleted.from_dict(data) + raise ValueError(f"Unknown discriminator value for Event: {data}") + + +def event_to_dict(obj: Event) -> dict[str, object]: + if isinstance(obj, EventSearching): + result = obj.to_dict() + result["type"] = "searching" + return result + elif isinstance(obj, EventCompleted): + result = obj.to_dict() + result["type"] = "completed" + return result + raise ValueError(f"Unknown variant for Event: {type(obj)}") diff --git a/tests/golden/python/python-requests/object-extensions/python_additional_properties/models/event_completed.py.golden b/tests/golden/python/python-requests/object-extensions/python_additional_properties/models/event_completed.py.golden new file mode 100644 index 000000000..2b27f9a2a --- /dev/null +++ b/tests/golden/python/python-requests/object-extensions/python_additional_properties/models/event_completed.py.golden @@ -0,0 +1,36 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python additional properties — 1.0.0 + +from __future__ import annotations + +from builtins import dict as _dict, str as _str +from dataclasses import dataclass, field as _dataclass_field +from typing import Any as _Any, Literal + +from . import _union + +@dataclass +class EventCompleted: + type_: Literal["completed"] + additional_properties: _dict[_str, + _Any] = _dataclass_field(default_factory = lambda: _dict[_str, + _Any](), kw_only = True, metadata = {"openapi_additional_properties": True, "openapi_dictionary": False}) + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + result["type"] = self.type_ + for key, item in self.additional_properties.items(): + if key in ['type']: + raise ValueError("Additional property collides with declared wire field: " + key) + + result[key] = _union.value_7_to_dict(item) + + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> EventCompleted: + return cls( + type_=data["type"], # type: ignore[assignment] + additional_properties = {key: _union.value_7_from_dict(item) for key, item in data.items() if key not in ['type']}, + ) diff --git a/tests/golden/python/python-requests/object-extensions/python_additional_properties/models/event_searching.py.golden b/tests/golden/python/python-requests/object-extensions/python_additional_properties/models/event_searching.py.golden new file mode 100644 index 000000000..0a47ac9c3 --- /dev/null +++ b/tests/golden/python/python-requests/object-extensions/python_additional_properties/models/event_searching.py.golden @@ -0,0 +1,39 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python additional properties — 1.0.0 + +from __future__ import annotations + +from builtins import dict as _dict, str as _str +from dataclasses import dataclass, field as _dataclass_field +from typing import Any as _Any, Literal + +from . import _union + +@dataclass +class EventSearching: + sequence_number: int + type_: Literal["searching"] + additional_properties: _dict[_str, + _Any] = _dataclass_field(default_factory = lambda: _dict[_str, + _Any](), kw_only = True, metadata = {"openapi_additional_properties": True, "openapi_dictionary": False}) + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + result["sequence_number"] = self.sequence_number + result["type"] = self.type_ + for key, item in self.additional_properties.items(): + if key in ['sequence_number', 'type']: + raise ValueError("Additional property collides with declared wire field: " + key) + + result[key] = _union.value_7_to_dict(item) + + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> EventSearching: + return cls( + sequence_number=data["sequence_number"], # type: ignore[assignment] + type_=data["type"], # type: ignore[assignment] + additional_properties = {key: _union.value_7_from_dict(item) for key, item in data.items() if key not in ['sequence_number', 'type']}, + ) diff --git a/tests/golden/python/python-requests/object-extensions/python_additional_properties/models/event_values.py.golden b/tests/golden/python/python-requests/object-extensions/python_additional_properties/models/event_values.py.golden new file mode 100644 index 000000000..e1e450460 --- /dev/null +++ b/tests/golden/python/python-requests/object-extensions/python_additional_properties/models/event_values.py.golden @@ -0,0 +1,31 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python additional properties — 1.0.0 + +from __future__ import annotations + +from builtins import dict as _dict, str as _str +from dataclasses import dataclass, field as _dataclass_field +from typing import cast + +from . import _union +from .event import Event + +@dataclass +class EventValues: + additional_properties: _dict[_str, + Event] = _dataclass_field(default_factory = lambda: _dict[_str, + Event](), kw_only = True, metadata = {"openapi_additional_properties": True, "openapi_dictionary": True}) + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + for key, item in self.additional_properties.items(): + result[key] = _union.value_19_to_dict(item) + + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> EventValues: + return cls( + additional_properties = {key: cast(Event, _union.value_19_from_dict(item)) for key, item in data.items() }, + ) diff --git a/tests/golden/python/python-requests/object-extensions/python_additional_properties/models/factory_collision.py.golden b/tests/golden/python/python-requests/object-extensions/python_additional_properties/models/factory_collision.py.golden new file mode 100644 index 000000000..a6ec404f1 --- /dev/null +++ b/tests/golden/python/python-requests/object-extensions/python_additional_properties/models/factory_collision.py.golden @@ -0,0 +1,41 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python additional properties — 1.0.0 + +from __future__ import annotations + +from builtins import dict as _dict, str as _str +from dataclasses import dataclass, field as _dataclass_field +from typing import Any as _Any + +from . import _union + +@dataclass +class FactoryCollision: + dict: str | None = None + field: str | None = None + additional_properties: _dict[_str, + _Any] = _dataclass_field(default_factory = lambda: _dict[_str, + _Any](), kw_only = True, metadata = {"openapi_additional_properties": True, "openapi_dictionary": False}) + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + if self.dict is not None: + result["dict"] = self.dict + if self.field is not None: + result["field"] = self.field + for key, item in self.additional_properties.items(): + if key in ['dict', 'field']: + raise ValueError("Additional property collides with declared wire field: " + key) + + result[key] = _union.value_7_to_dict(item) + + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> FactoryCollision: + return cls( + dict=data.get("dict"), # type: ignore[assignment] + field=data.get("field"), # type: ignore[assignment] + additional_properties = {key: _union.value_7_from_dict(item) for key, item in data.items() if key not in ['dict', 'field']}, + ) diff --git a/tests/golden/python/python-requests/object-extensions/python_additional_properties/models/holder.py.golden b/tests/golden/python/python-requests/object-extensions/python_additional_properties/models/holder.py.golden new file mode 100644 index 000000000..1924f47a2 --- /dev/null +++ b/tests/golden/python/python-requests/object-extensions/python_additional_properties/models/holder.py.golden @@ -0,0 +1,38 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python additional properties — 1.0.0 + +from __future__ import annotations + +from dataclasses import dataclass +from typing import cast + +from . import _union +from .dictionary_alias import DictionaryAlias +from .mixed_map import MixedMap +from .opaque_map import OpaqueMap +from .typed_map import TypedMap + +@dataclass +class Holder: + aliases: dict[str, DictionaryAlias] + dictionaries: list[TypedMap] + mixed: MixedMap + opaque: OpaqueMap + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + result['aliases'] = _union.value_22_to_dict(self.aliases) + result['dictionaries'] = _union.value_23_to_dict(self.dictionaries) + result['mixed'] = _union.value_24_to_dict(self.mixed) + result['opaque'] = _union.value_25_to_dict(self.opaque) + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> Holder: + return cls( + aliases = cast(dict[str, DictionaryAlias], _union.value_22_from_dict(data['aliases'])), + dictionaries = cast(list[TypedMap], _union.value_23_from_dict(data['dictionaries'])), + mixed = cast(MixedMap, _union.value_24_from_dict(data['mixed'])), + opaque = cast(OpaqueMap, _union.value_25_from_dict(data['opaque'])), + ) diff --git a/tests/golden/python/python-requests/object-extensions/python_additional_properties/models/implicit.py.golden b/tests/golden/python/python-requests/object-extensions/python_additional_properties/models/implicit.py.golden new file mode 100644 index 000000000..6807c209d --- /dev/null +++ b/tests/golden/python/python-requests/object-extensions/python_additional_properties/models/implicit.py.golden @@ -0,0 +1,23 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python additional properties — 1.0.0 + +from __future__ import annotations + +from dataclasses import dataclass + +@dataclass +class Implicit: + known_field: str | None = None + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + if self.known_field is not None: + result["known_field"] = self.known_field + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> Implicit: + return cls( + known_field=data.get("known_field"), # type: ignore[assignment] + ) diff --git a/tests/golden/python/python-requests/object-extensions/python_additional_properties/models/legacy_optional_holder.py.golden b/tests/golden/python/python-requests/object-extensions/python_additional_properties/models/legacy_optional_holder.py.golden new file mode 100644 index 000000000..497c49921 --- /dev/null +++ b/tests/golden/python/python-requests/object-extensions/python_additional_properties/models/legacy_optional_holder.py.golden @@ -0,0 +1,29 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python additional properties — 1.0.0 + +from __future__ import annotations + +from dataclasses import dataclass + +from .nested_value import NestedValue + +@dataclass +class LegacyOptionalHolder: + maybe: NestedValue | None = None + nullable: NestedValue | None = None + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + if self.maybe is not None: + result["maybe"] = self.maybe.to_dict() + if self.nullable is not None: + result["nullable"] = self.nullable.to_dict() + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> LegacyOptionalHolder: + return cls( + maybe=NestedValue.from_dict(data.get("maybe")) if data.get("maybe") is not None else None, # type: ignore[arg-type] + nullable=NestedValue.from_dict(data.get("nullable")) if data.get("nullable") is not None else None, # type: ignore[arg-type] + ) diff --git a/tests/golden/python/python-requests/object-extensions/python_additional_properties/models/mixed_choice.py.golden b/tests/golden/python/python-requests/object-extensions/python_additional_properties/models/mixed_choice.py.golden new file mode 100644 index 000000000..192de30e8 --- /dev/null +++ b/tests/golden/python/python-requests/object-extensions/python_additional_properties/models/mixed_choice.py.golden @@ -0,0 +1,21 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python additional properties — 1.0.0 + +from __future__ import annotations + +from typing import cast + +from . import _union +from .mixed_map import MixedMap + +type MixedChoice = ( + MixedMap + | str +) + +def mixed_choice_from_dict(value: object) -> MixedChoice: + return cast(MixedChoice, _union.value_26_from_dict(value)) + +def mixed_choice_to_dict(value: MixedChoice) -> object: + return _union.value_26_to_dict(value) diff --git a/tests/golden/python/python-requests/object-extensions/python_additional_properties/models/mixed_map.py.golden b/tests/golden/python/python-requests/object-extensions/python_additional_properties/models/mixed_map.py.golden new file mode 100644 index 000000000..67210e724 --- /dev/null +++ b/tests/golden/python/python-requests/object-extensions/python_additional_properties/models/mixed_map.py.golden @@ -0,0 +1,37 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python additional properties — 1.0.0 + +from __future__ import annotations + +from builtins import dict as _dict, str as _str +from dataclasses import dataclass, field as _dataclass_field +from typing import cast + +from . import _union +from .nested_value import NestedValue + +@dataclass +class MixedMap: + known_field: str + additional_properties: _dict[_str, + NestedValue] = _dataclass_field(default_factory = lambda: _dict[_str, + NestedValue](), kw_only = True, metadata = {"openapi_additional_properties": True, "openapi_dictionary": False}) + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + result["known_field"] = self.known_field + for key, item in self.additional_properties.items(): + if key in ['known_field']: + raise ValueError("Additional property collides with declared wire field: " + key) + + result[key] = _union.value_1_to_dict(item) + + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> MixedMap: + return cls( + known_field=data["known_field"], # type: ignore[assignment] + additional_properties = {key: cast(NestedValue, _union.value_1_from_dict(item)) for key, item in data.items() if key not in ['known_field']}, + ) diff --git a/tests/golden/python/python-requests/object-extensions/python_additional_properties/models/nested_value.py.golden b/tests/golden/python/python-requests/object-extensions/python_additional_properties/models/nested_value.py.golden new file mode 100644 index 000000000..69524d09c --- /dev/null +++ b/tests/golden/python/python-requests/object-extensions/python_additional_properties/models/nested_value.py.golden @@ -0,0 +1,22 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python additional properties — 1.0.0 + +from __future__ import annotations + +from dataclasses import dataclass + +@dataclass +class NestedValue: + wire_value: str + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + result["wire_value"] = self.wire_value + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> NestedValue: + return cls( + wire_value=data["wire_value"], # type: ignore[assignment] + ) diff --git a/tests/golden/python/python-requests/object-extensions/python_additional_properties/models/node.py.golden b/tests/golden/python/python-requests/object-extensions/python_additional_properties/models/node.py.golden new file mode 100644 index 000000000..18535f708 --- /dev/null +++ b/tests/golden/python/python-requests/object-extensions/python_additional_properties/models/node.py.golden @@ -0,0 +1,28 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python additional properties — 1.0.0 + +from __future__ import annotations + +from dataclasses import dataclass +from typing import cast + +from . import _union, nodes + +@dataclass +class Node: + children: nodes.Nodes + wire_value: str + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + result['children'] = _union.value_28_to_dict(self.children) + result["wire_value"] = self.wire_value + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> Node: + return cls( + children = cast(nodes.Nodes, _union.value_28_from_dict(data['children'])), + wire_value=data["wire_value"], # type: ignore[assignment] + ) diff --git a/tests/golden/python/python-requests/object-extensions/python_additional_properties/models/nodes.py.golden b/tests/golden/python/python-requests/object-extensions/python_additional_properties/models/nodes.py.golden new file mode 100644 index 000000000..704a8c69b --- /dev/null +++ b/tests/golden/python/python-requests/object-extensions/python_additional_properties/models/nodes.py.golden @@ -0,0 +1,30 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python additional properties — 1.0.0 + +from __future__ import annotations + +from builtins import dict as _dict, str as _str +from dataclasses import dataclass, field as _dataclass_field +from typing import cast + +from . import _union, node + +@dataclass +class Nodes: + additional_properties: _dict[_str, + node.Node] = _dataclass_field(default_factory = lambda: _dict[_str, + node.Node](), kw_only = True, metadata = {"openapi_additional_properties": True, "openapi_dictionary": True}) + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + for key, item in self.additional_properties.items(): + result[key] = _union.value_29_to_dict(item) + + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> Nodes: + return cls( + additional_properties = {key: cast(node.Node, _union.value_29_from_dict(item)) for key, item in data.items() }, + ) diff --git a/tests/golden/python/python-requests/object-extensions/python_additional_properties/models/nullable_values.py.golden b/tests/golden/python/python-requests/object-extensions/python_additional_properties/models/nullable_values.py.golden new file mode 100644 index 000000000..48fd21e77 --- /dev/null +++ b/tests/golden/python/python-requests/object-extensions/python_additional_properties/models/nullable_values.py.golden @@ -0,0 +1,33 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python additional properties — 1.0.0 + +from __future__ import annotations + +from builtins import dict as _dict, str as _str +from dataclasses import dataclass, field as _dataclass_field +from typing import cast + +from . import _union +from .nested_value import NestedValue + +@dataclass +class NullableValues: + additional_properties: _dict[_str, + NestedValue | None] = _dataclass_field(default_factory = lambda: _dict[_str, + NestedValue + | None](), kw_only = True, metadata = {"openapi_additional_properties": True, "openapi_dictionary": True}) + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + for key, item in self.additional_properties.items(): + result[key] = _union.value_30_to_dict(item) + + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> NullableValues: + return cls( + additional_properties = {key: cast(NestedValue + | None, _union.value_30_from_dict(item)) for key, item in data.items() }, + ) diff --git a/tests/golden/python/python-requests/object-extensions/python_additional_properties/models/numbers.py.golden b/tests/golden/python/python-requests/object-extensions/python_additional_properties/models/numbers.py.golden new file mode 100644 index 000000000..f1f590794 --- /dev/null +++ b/tests/golden/python/python-requests/object-extensions/python_additional_properties/models/numbers.py.golden @@ -0,0 +1,29 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python additional properties — 1.0.0 + +from __future__ import annotations + +from builtins import dict as _dict, str as _str +from dataclasses import dataclass, field as _dataclass_field +from typing import cast + +from . import _union + +@dataclass +class Numbers: + additional_properties: _dict[_str, int] = _dataclass_field(default_factory = lambda: _dict[_str, + int](), kw_only = True, metadata = {"openapi_additional_properties": True, "openapi_dictionary": True}) + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + for key, item in self.additional_properties.items(): + result[key] = _union.value_32_to_dict(item) + + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> Numbers: + return cls( + additional_properties = {key: cast(int, _union.value_32_from_dict(item)) for key, item in data.items() }, + ) diff --git a/tests/golden/python/python-requests/object-extensions/python_additional_properties/models/opaque_map.py.golden b/tests/golden/python/python-requests/object-extensions/python_additional_properties/models/opaque_map.py.golden new file mode 100644 index 000000000..7a7a7f5e2 --- /dev/null +++ b/tests/golden/python/python-requests/object-extensions/python_additional_properties/models/opaque_map.py.golden @@ -0,0 +1,36 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python additional properties — 1.0.0 + +from __future__ import annotations + +from builtins import dict as _dict, str as _str +from dataclasses import dataclass, field as _dataclass_field +from typing import Any as _Any + +from . import _union + +@dataclass +class OpaqueMap: + known_field: str + additional_properties: _dict[_str, + _Any] = _dataclass_field(default_factory = lambda: _dict[_str, + _Any](), kw_only = True, metadata = {"openapi_additional_properties": True, "openapi_dictionary": False}) + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + result["known_field"] = self.known_field + for key, item in self.additional_properties.items(): + if key in ['known_field']: + raise ValueError("Additional property collides with declared wire field: " + key) + + result[key] = _union.value_7_to_dict(item) + + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> OpaqueMap: + return cls( + known_field=data["known_field"], # type: ignore[assignment] + additional_properties = {key: _union.value_7_from_dict(item) for key, item in data.items() if key not in ['known_field']}, + ) diff --git a/tests/golden/python/python-requests/object-extensions/python_additional_properties/models/optional_holder.py.golden b/tests/golden/python/python-requests/object-extensions/python_additional_properties/models/optional_holder.py.golden new file mode 100644 index 000000000..7abf38903 --- /dev/null +++ b/tests/golden/python/python-requests/object-extensions/python_additional_properties/models/optional_holder.py.golden @@ -0,0 +1,35 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python additional properties — 1.0.0 + +from __future__ import annotations + +from dataclasses import dataclass +from typing import cast + +from . import _union +from .mixed_map import MixedMap + +@dataclass +class OptionalHolder: + maybe: MixedMap | None = None + nullable: MixedMap | None = None + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + if self.maybe is not None: + result['maybe'] = _union.value_24_to_dict(self.maybe) + + if self.nullable is not None: + result['nullable'] = _union.value_34_to_dict(self.nullable) + + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> OptionalHolder: + return cls( + maybe = cast(MixedMap + | None, None if data.get('maybe') is None else _union.value_24_from_dict(data.get('maybe'))), + nullable = cast(MixedMap + | None, None if data.get('nullable') is None else _union.value_34_from_dict(data.get('nullable'))), + ) diff --git a/tests/golden/python/python-requests/object-extensions/python_additional_properties/models/optional_known.py.golden b/tests/golden/python/python-requests/object-extensions/python_additional_properties/models/optional_known.py.golden new file mode 100644 index 000000000..5ecacf926 --- /dev/null +++ b/tests/golden/python/python-requests/object-extensions/python_additional_properties/models/optional_known.py.golden @@ -0,0 +1,37 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python additional properties — 1.0.0 + +from __future__ import annotations + +from builtins import dict as _dict, str as _str +from dataclasses import dataclass, field as _dataclass_field +from typing import Any as _Any + +from . import _union + +@dataclass +class OptionalKnown: + known_field: str | None = None + additional_properties: _dict[_str, + _Any] = _dataclass_field(default_factory = lambda: _dict[_str, + _Any](), kw_only = True, metadata = {"openapi_additional_properties": True, "openapi_dictionary": False}) + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + if self.known_field is not None: + result["known_field"] = self.known_field + for key, item in self.additional_properties.items(): + if key in ['known_field']: + raise ValueError("Additional property collides with declared wire field: " + key) + + result[key] = _union.value_7_to_dict(item) + + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> OptionalKnown: + return cls( + known_field=data.get("known_field"), # type: ignore[assignment] + additional_properties = {key: _union.value_7_from_dict(item) for key, item in data.items() if key not in ['known_field']}, + ) diff --git a/tests/golden/python/python-requests/object-extensions/python_additional_properties/models/pure_recursive.py.golden b/tests/golden/python/python-requests/object-extensions/python_additional_properties/models/pure_recursive.py.golden new file mode 100644 index 000000000..c0c657dd8 --- /dev/null +++ b/tests/golden/python/python-requests/object-extensions/python_additional_properties/models/pure_recursive.py.golden @@ -0,0 +1,30 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python additional properties — 1.0.0 + +from __future__ import annotations + +from builtins import dict as _dict, str as _str +from dataclasses import dataclass, field as _dataclass_field +from typing import cast + +from . import _union + +@dataclass +class PureRecursive: + additional_properties: _dict[_str, + PureRecursive] = _dataclass_field(default_factory = lambda: _dict[_str, + PureRecursive](), kw_only = True, metadata = {"openapi_additional_properties": True, "openapi_dictionary": True}) + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + for key, item in self.additional_properties.items(): + result[key] = _union.value_36_to_dict(item) + + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> PureRecursive: + return cls( + additional_properties = {key: cast(PureRecursive, _union.value_36_from_dict(item)) for key, item in data.items() }, + ) diff --git a/tests/golden/python/python-requests/object-extensions/python_additional_properties/models/raw_map.py.golden b/tests/golden/python/python-requests/object-extensions/python_additional_properties/models/raw_map.py.golden new file mode 100644 index 000000000..d2c84165f --- /dev/null +++ b/tests/golden/python/python-requests/object-extensions/python_additional_properties/models/raw_map.py.golden @@ -0,0 +1,30 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python additional properties — 1.0.0 + +from __future__ import annotations + +from builtins import dict as _dict, str as _str +from dataclasses import dataclass, field as _dataclass_field +from typing import Any as _Any + +from . import _union + +@dataclass +class RawMap: + additional_properties: _dict[_str, + _Any] = _dataclass_field(default_factory = lambda: _dict[_str, + _Any](), kw_only = True, metadata = {"openapi_additional_properties": True, "openapi_dictionary": True}) + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + for key, item in self.additional_properties.items(): + result[key] = _union.value_7_to_dict(item) + + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> RawMap: + return cls( + additional_properties = {key: _union.value_7_from_dict(item) for key, item in data.items() }, + ) diff --git a/tests/golden/python/python-requests/object-extensions/python_additional_properties/models/recursive.py.golden b/tests/golden/python/python-requests/object-extensions/python_additional_properties/models/recursive.py.golden new file mode 100644 index 000000000..dd0064d12 --- /dev/null +++ b/tests/golden/python/python-requests/object-extensions/python_additional_properties/models/recursive.py.golden @@ -0,0 +1,36 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python additional properties — 1.0.0 + +from __future__ import annotations + +from builtins import dict as _dict, str as _str +from dataclasses import dataclass, field as _dataclass_field +from typing import cast + +from . import _union + +@dataclass +class Recursive: + wire_value: str + additional_properties: _dict[_str, + Recursive] = _dataclass_field(default_factory = lambda: _dict[_str, + Recursive](), kw_only = True, metadata = {"openapi_additional_properties": True, "openapi_dictionary": False}) + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + result["wire_value"] = self.wire_value + for key, item in self.additional_properties.items(): + if key in ['wire_value']: + raise ValueError("Additional property collides with declared wire field: " + key) + + result[key] = _union.value_38_to_dict(item) + + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> Recursive: + return cls( + wire_value=data["wire_value"], # type: ignore[assignment] + additional_properties = {key: cast(Recursive, _union.value_38_from_dict(item)) for key, item in data.items() if key not in ['wire_value']}, + ) diff --git a/tests/golden/python/python-requests/object-extensions/python_additional_properties/models/status.py.golden b/tests/golden/python/python-requests/object-extensions/python_additional_properties/models/status.py.golden new file mode 100644 index 000000000..e3a664307 --- /dev/null +++ b/tests/golden/python/python-requests/object-extensions/python_additional_properties/models/status.py.golden @@ -0,0 +1,11 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python additional properties — 1.0.0 + +from __future__ import annotations + +from enum import Enum + +class Status(str, Enum): + ACTIVE = "active" + INACTIVE = "inactive" diff --git a/tests/golden/python/python-requests/object-extensions/python_additional_properties/models/status_values.py.golden b/tests/golden/python/python-requests/object-extensions/python_additional_properties/models/status_values.py.golden new file mode 100644 index 000000000..c93068e18 --- /dev/null +++ b/tests/golden/python/python-requests/object-extensions/python_additional_properties/models/status_values.py.golden @@ -0,0 +1,31 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python additional properties — 1.0.0 + +from __future__ import annotations + +from builtins import dict as _dict, str as _str +from dataclasses import dataclass, field as _dataclass_field +from typing import cast + +from . import _union +from .status import Status + +@dataclass +class StatusValues: + additional_properties: _dict[_str, + Status] = _dataclass_field(default_factory = lambda: _dict[_str, + Status](), kw_only = True, metadata = {"openapi_additional_properties": True, "openapi_dictionary": True}) + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + for key, item in self.additional_properties.items(): + result[key] = _union.value_39_to_dict(item) + + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> StatusValues: + return cls( + additional_properties = {key: cast(Status, _union.value_39_from_dict(item)) for key, item in data.items() }, + ) diff --git a/tests/golden/python/python-requests/object-extensions/python_additional_properties/models/storage_collision.py.golden b/tests/golden/python/python-requests/object-extensions/python_additional_properties/models/storage_collision.py.golden new file mode 100644 index 000000000..e88f39f38 --- /dev/null +++ b/tests/golden/python/python-requests/object-extensions/python_additional_properties/models/storage_collision.py.golden @@ -0,0 +1,37 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python additional properties — 1.0.0 + +from __future__ import annotations + +from builtins import dict as _dict, str as _str +from dataclasses import dataclass, field as _dataclass_field +from typing import cast + +from . import _union +from .nested_value import NestedValue + +@dataclass +class StorageCollision: + additional_properties: str + additional_properties_: _dict[_str, + NestedValue] = _dataclass_field(default_factory = lambda: _dict[_str, + NestedValue](), kw_only = True, metadata = {"openapi_additional_properties": True, "openapi_dictionary": False}) + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + result["additionalProperties"] = self.additional_properties + for key, item in self.additional_properties_.items(): + if key in ['additionalProperties']: + raise ValueError("Additional property collides with declared wire field: " + key) + + result[key] = _union.value_1_to_dict(item) + + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> StorageCollision: + return cls( + additional_properties=data["additionalProperties"], # type: ignore[assignment] + additional_properties_ = {key: cast(NestedValue, _union.value_1_from_dict(item)) for key, item in data.items() if key not in ['additionalProperties']}, + ) diff --git a/tests/golden/python/python-requests/object-extensions/python_additional_properties/models/typed_map.py.golden b/tests/golden/python/python-requests/object-extensions/python_additional_properties/models/typed_map.py.golden new file mode 100644 index 000000000..5e0de15ac --- /dev/null +++ b/tests/golden/python/python-requests/object-extensions/python_additional_properties/models/typed_map.py.golden @@ -0,0 +1,31 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python additional properties — 1.0.0 + +from __future__ import annotations + +from builtins import dict as _dict, str as _str +from dataclasses import dataclass, field as _dataclass_field +from typing import cast + +from . import _union +from .nested_value import NestedValue + +@dataclass +class TypedMap: + additional_properties: _dict[_str, + NestedValue] = _dataclass_field(default_factory = lambda: _dict[_str, + NestedValue](), kw_only = True, metadata = {"openapi_additional_properties": True, "openapi_dictionary": True}) + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + for key, item in self.additional_properties.items(): + result[key] = _union.value_1_to_dict(item) + + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> TypedMap: + return cls( + additional_properties = {key: cast(NestedValue, _union.value_1_from_dict(item)) for key, item in data.items() }, + ) diff --git a/tests/golden/python/python-requests/object-extensions/python_additional_properties/models/value_alias.py.golden b/tests/golden/python/python-requests/object-extensions/python_additional_properties/models/value_alias.py.golden new file mode 100644 index 000000000..b28b10747 --- /dev/null +++ b/tests/golden/python/python-requests/object-extensions/python_additional_properties/models/value_alias.py.golden @@ -0,0 +1,9 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python additional properties — 1.0.0 + +from __future__ import annotations + +from .nested_value import NestedValue + +type ValueAlias = (NestedValue) diff --git a/tests/golden/python/python-requests/object-extensions/python_additional_properties/models/value_choice.py.golden b/tests/golden/python/python-requests/object-extensions/python_additional_properties/models/value_choice.py.golden new file mode 100644 index 000000000..1b4cf75ee --- /dev/null +++ b/tests/golden/python/python-requests/object-extensions/python_additional_properties/models/value_choice.py.golden @@ -0,0 +1,21 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python additional properties — 1.0.0 + +from __future__ import annotations + +from typing import cast + +from . import _union +from .nested_value import NestedValue + +type ValueChoice = ( + NestedValue + | int +) + +def value_choice_from_dict(value: object) -> ValueChoice: + return cast(ValueChoice, _union.value_9_from_dict(value)) + +def value_choice_to_dict(value: ValueChoice) -> object: + return _union.value_9_to_dict(value) diff --git a/tests/golden/python/python-requests/object-extensions/python_additional_properties/py.typed.golden b/tests/golden/python/python-requests/object-extensions/python_additional_properties/py.typed.golden new file mode 100644 index 000000000..e69de29bb diff --git a/tests/golden/python/python-requests/object-extensions/python_additional_properties/runtime/__init__.py.golden b/tests/golden/python/python-requests/object-extensions/python_additional_properties/runtime/__init__.py.golden new file mode 100644 index 000000000..31f0adc37 --- /dev/null +++ b/tests/golden/python/python-requests/object-extensions/python_additional_properties/runtime/__init__.py.golden @@ -0,0 +1,10 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python additional properties — 1.0.0 + +from .auth import ApiKeyAuth as ApiKeyAuth +from .auth import Authenticator as Authenticator +from .auth import BearerAuth as BearerAuth +from .client import ApiResponse as ApiResponse +from .client import Client as Client +from .errors import ApiError as ApiError diff --git a/tests/golden/python/python-requests/object-extensions/python_additional_properties/runtime/_union_support.py.golden b/tests/golden/python/python-requests/object-extensions/python_additional_properties/runtime/_union_support.py.golden new file mode 100644 index 000000000..33b10f50e --- /dev/null +++ b/tests/golden/python/python-requests/object-extensions/python_additional_properties/runtime/_union_support.py.golden @@ -0,0 +1,109 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python additional properties — 1.0.0 + +"""Internal structural selection support; deliberately not a schema validator.""" + +from __future__ import annotations + +from dataclasses import fields, is_dataclass +from enum import Enum +from math import isfinite +from typing import Protocol, TypeGuard, cast + + +class _Model(Protocol): + def to_dict(self) -> dict[str, object]: ... + + +def as_dict(value: object) -> dict[str, object]: + return cast(dict[str, object], value) + + +def as_list(value: object) -> list[object]: + return cast(list[object], value) + + +def is_dict(value: object) -> TypeGuard[dict[str, object]]: + return isinstance(value, dict) and all(isinstance(key, str) for key in cast(dict[object, object], value)) + + +def is_list(value: object) -> TypeGuard[list[object]]: + return isinstance(value, list) + + +def _additional_properties(value: object, dictionary_only: bool = False) -> dict[str, object] | None: + if not isinstance(value, type) and is_dataclass(value): + for field in fields(value): + if field.metadata.get("openapi_additional_properties") is True: + if not dictionary_only or field.metadata.get("openapi_dictionary") is True: + return as_dict(getattr(value, field.name)) + return None + + +def is_mapping(value: object) -> bool: + return is_dict(value) or _additional_properties(value, True) is not None + + +def mapping(value: object) -> dict[str, object]: + properties = _additional_properties(value, True) + return properties if properties is not None else as_dict(value) + + +def scalar(value: object) -> object: + return value.value if isinstance(value, Enum) else value + + +def model_to_dict(value: object) -> object: + return value if is_dict(value) else cast(_Model, value).to_dict() + + +def is_object(value: object) -> bool: + return is_dict(value) or (not isinstance(value, type) and is_dataclass(value)) + + +def has(value: object, wire: str, field: str, required: bool) -> bool: + if is_dict(value): + return wire in as_dict(value) + # Optional dataclass fields use None for absence. Required fields still + # undergo their shape check; a nullable required field accepts that None. + return hasattr(value, field) and (required or getattr(value, field) is not None) + + +def get(value: object, wire: str, field: str) -> object: + return value.get(wire) if is_dict(value) else getattr(value, field, None) + + +def extras(value: object, known: list[str]) -> list[object]: + if is_dict(value): + return [item for key, item in as_dict(value).items() if key not in known] + properties = _additional_properties(value) + return list(properties.values()) if properties is not None else [] + + +def number(value: object, integer: bool) -> bool: + value = scalar(value) + if isinstance(value, bool): + return False + if isinstance(value, int): + return True + return isinstance(value, float) and isfinite(value) and (not integer or value.is_integer()) + + +def equal(value: object, expected: object) -> bool: + value = scalar(value) + if isinstance(expected, bool): + return isinstance(value, bool) and value == expected + if isinstance(expected, (int, float)): + return number(value, False) and value == expected + if is_list(expected): + if not is_list(value): + return False + left, right = as_list(value), as_list(expected) + return len(left) == len(right) and all(equal(a, b) for a, b in zip(left, right)) + if is_dict(expected): + if not is_dict(value): + return False + left, right = as_dict(value), as_dict(expected) + return left.keys() == right.keys() and all(equal(left[key], item) for key, item in right.items()) + return type(value) is type(expected) and value == expected diff --git a/tests/golden/python/python-requests/object-extensions/python_additional_properties/runtime/auth.py.golden b/tests/golden/python/python-requests/object-extensions/python_additional_properties/runtime/auth.py.golden new file mode 100644 index 000000000..6799e51bd --- /dev/null +++ b/tests/golden/python/python-requests/object-extensions/python_additional_properties/runtime/auth.py.golden @@ -0,0 +1,50 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python additional properties — 1.0.0 + +"""Authentication helpers.""" + +from __future__ import annotations + +from abc import ABC, abstractmethod +from collections.abc import Callable + + +class Authenticator(ABC): + """Base class for request authenticators.""" + + @abstractmethod + def auth_headers(self) -> dict[str, str]: + """Return headers to attach to every request.""" + ... + + +class BearerAuth(Authenticator): + """Bearer token authentication. + + Accepts a static token string or a callable that returns the + current token (evaluated on every request). + """ + + def __init__(self, token: str | Callable[[], str]) -> None: + self._token = token + + def auth_headers(self) -> dict[str, str]: + token = self._token() if callable(self._token) else self._token + return {"Authorization": f"Bearer {token}"} + + +class ApiKeyAuth(Authenticator): + """API key authentication via a custom header. + + Accepts a static key string or a callable that returns the + current key (evaluated on every request). + """ + + def __init__(self, header_name: str, api_key: str | Callable[[], str]) -> None: + self._header_name = header_name + self._api_key = api_key + + def auth_headers(self) -> dict[str, str]: + key = self._api_key() if callable(self._api_key) else self._api_key + return {self._header_name: key} diff --git a/tests/golden/python/python-requests/object-extensions/python_additional_properties/runtime/client.py.golden b/tests/golden/python/python-requests/object-extensions/python_additional_properties/runtime/client.py.golden new file mode 100644 index 000000000..512cad917 --- /dev/null +++ b/tests/golden/python/python-requests/object-extensions/python_additional_properties/runtime/client.py.golden @@ -0,0 +1,116 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python additional properties — 1.0.0 + +"""HTTP client wrapping requests.""" + +from __future__ import annotations + +from collections.abc import Mapping +from dataclasses import dataclass +from typing import Any, Generic, TypeVar, cast + +import requests + +from .auth import Authenticator + +ResponseData = TypeVar("ResponseData") + + +class Response: + """Thin wrapper over requests.Response with strict types.""" + + def __init__(self, raw: requests.Response) -> None: + self._raw = raw + + @property + def status_code(self) -> int: + return self._raw.status_code # type: ignore[return-value] + + @property + def reason(self) -> str: + return self._raw.reason or "" + + @property + def content(self) -> bytes: + return self._raw.content # type: ignore[return-value] + + @property + def headers(self) -> Mapping[str, str]: + return cast(Mapping[str, str], self._raw.headers) + + @property + def text(self) -> str: + return self._raw.text + + def json(self) -> Any: # type: ignore[explicit-override] + return self._raw.json() # type: ignore[reportUnknownMemberType] + + +@dataclass(frozen=True) +class ApiResponse(Generic[ResponseData]): + """Decoded response data together with its HTTP metadata.""" + + data: ResponseData + status_code: int + headers: Mapping[str, str] + raw: Response + + +class Client: + """Synchronous HTTP client for the generated SDK.""" + + def __init__( + self, + base_url: str, + *, + http_client: requests.Session | None = None, + authenticator: Authenticator | None = None, + ) -> None: + self._base_url = base_url.rstrip("/") + self._http_client = http_client or requests.Session() + self._authenticator = authenticator + + def request( + self, + method: str, + path: str, + *, + params: dict[str, str] | None = None, + json: Any = None, + data: Any = None, + files: Any = None, + headers: dict[str, str] | None = None, + ) -> Response: + """Send an HTTP request and return the raw response.""" + url = f"{self._base_url}{path}" + req_headers: dict[str, str] = {"Accept": "application/json"} + if json is not None: + req_headers["Content-Type"] = "application/json" + if headers: + req_headers.update(headers) + if self._authenticator is not None: + req_headers.update(self._authenticator.auth_headers()) + raw = cast( + requests.Response, + self._http_client.request( # type: ignore[reportUnknownMemberType] + method, + url, + params=params, + json=json, + data=data, + files=files, + headers=req_headers, + ), + ) + return Response(raw) + + def close(self) -> None: + """Close the underlying HTTP client.""" + self._http_client.close() + + def __enter__(self) -> Client: + return self + + def __exit__(self, *args: object) -> None: + self.close() diff --git a/tests/golden/python/python-requests/object-extensions/python_additional_properties/runtime/errors.py.golden b/tests/golden/python/python-requests/object-extensions/python_additional_properties/runtime/errors.py.golden new file mode 100644 index 000000000..aa9e4987a --- /dev/null +++ b/tests/golden/python/python-requests/object-extensions/python_additional_properties/runtime/errors.py.golden @@ -0,0 +1,34 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python additional properties — 1.0.0 + +"""API error type.""" + +from __future__ import annotations + +from collections.abc import Mapping + + +class ApiError(Exception): + """Raised when the server returns a non-success status code.""" + + def __init__( + self, + status_code: int, + status: str, + body: bytes, + *, + headers: Mapping[str, str] | None = None, + response: object | None = None, + ) -> None: + self.status_code: int = status_code + self.status: str = status + self.raw_body: bytes = body + self.body: bytes = body + self.headers: Mapping[str, str] | None = headers + self.response: object | None = response + super().__init__(f"{status_code} {status}") + + @property + def text(self) -> str: + return self.raw_body.decode("utf-8", errors="replace") diff --git a/tests/golden/python/python-requests/open-tagged-additional-properties/open_tagged_additional_properties/models/_union.py.golden b/tests/golden/python/python-requests/open-tagged-additional-properties/open_tagged_additional_properties/models/_union.py.golden new file mode 100644 index 000000000..f10744f31 --- /dev/null +++ b/tests/golden/python/python-requests/open-tagged-additional-properties/open_tagged_additional_properties/models/_union.py.golden @@ -0,0 +1,55 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Open tagged additional properties — 1.0.0 + +from __future__ import annotations + +from ..runtime import _union_support + +def value_0_from_dict(value: object) -> object: + return _union_support.scalar(value) + +def value_0_to_dict(value: object) -> object: + return _union_support.scalar(value) + +def value_1_from_dict(value: object) -> object: + from.import event_searching + return event_searching.EventSearching.from_dict(_union_support.as_dict(value)) + +def value_1_to_dict(value: object) -> object: + return _union_support.model_to_dict(value) + +def value_2_from_dict(value: object) -> object: + from.import event_completed + return event_completed.EventCompleted.from_dict(_union_support.as_dict(value)) + +def value_2_to_dict(value: object) -> object: + return _union_support.model_to_dict(value) + +def value_3_from_dict(value: object) -> object: + data = _union_support.as_dict(value) + if data.get('type') == 'searching': + return value_1_from_dict(value) + + if data.get('type') == 'completed': + return value_2_from_dict(value) + + raise ValueError("No matching tagged union variant") + +def value_3_to_dict(value: object) -> object: + if ((_union_support.is_object(value) + and (_union_support.has(value, 'sequence_number', 'sequence_number', True) and _union_support.number(_union_support.get(value, 'sequence_number', 'sequence_number'), True)) + and (_union_support.has(value, 'type', 'type_', True) and _union_support.equal(_union_support.get(value, 'type', 'type_'), 'searching')) + and all(True for _item in _union_support.extras(value, ['sequence_number', 'type'])))): + result = dict(_union_support.as_dict(value_1_to_dict(value))) + result['type'] = 'searching' + return result + + if ((_union_support.is_object(value) + and (_union_support.has(value, 'type', 'type_', True) and _union_support.equal(_union_support.get(value, 'type', 'type_'), 'completed')) + and all(True for _item in _union_support.extras(value, ['type'])))): + result = dict(_union_support.as_dict(value_2_to_dict(value))) + result['type'] = 'completed' + return result + + raise ValueError("No matching tagged union variant") diff --git a/tests/golden/python/python-requests/open-tagged-additional-properties/open_tagged_additional_properties/models/event_completed.py.golden b/tests/golden/python/python-requests/open-tagged-additional-properties/open_tagged_additional_properties/models/event_completed.py.golden index c8bab9e90..89829522e 100644 --- a/tests/golden/python/python-requests/open-tagged-additional-properties/open_tagged_additional_properties/models/event_completed.py.golden +++ b/tests/golden/python/python-requests/open-tagged-additional-properties/open_tagged_additional_properties/models/event_completed.py.golden @@ -4,20 +4,33 @@ from __future__ import annotations -from dataclasses import dataclass -from typing import Literal +from builtins import dict as _dict, str as _str +from dataclasses import dataclass, field as _dataclass_field +from typing import Any as _Any, Literal + +from . import _union @dataclass class EventCompleted: type_: Literal["completed"] + additional_properties: _dict[_str, + _Any] = _dataclass_field(default_factory = lambda: _dict[_str, + _Any](), kw_only = True, metadata = {"openapi_additional_properties": True, "openapi_dictionary": False}) def to_dict(self) -> dict[str, object]: result: dict[str, object] = {} result["type"] = self.type_ + for key, item in self.additional_properties.items(): + if key in ['type']: + raise ValueError("Additional property collides with declared wire field: " + key) + + result[key] = _union.value_0_to_dict(item) + return result @classmethod def from_dict(cls, data: dict[str, object]) -> EventCompleted: return cls( type_=data["type"], # type: ignore[assignment] + additional_properties = {key: _union.value_0_from_dict(item) for key, item in data.items() if key not in ['type']}, ) diff --git a/tests/golden/python/python-requests/open-tagged-additional-properties/open_tagged_additional_properties/models/event_searching.py.golden b/tests/golden/python/python-requests/open-tagged-additional-properties/open_tagged_additional_properties/models/event_searching.py.golden index 266ef40cf..77e5ab866 100644 --- a/tests/golden/python/python-requests/open-tagged-additional-properties/open_tagged_additional_properties/models/event_searching.py.golden +++ b/tests/golden/python/python-requests/open-tagged-additional-properties/open_tagged_additional_properties/models/event_searching.py.golden @@ -4,18 +4,30 @@ from __future__ import annotations -from dataclasses import dataclass -from typing import Literal +from builtins import dict as _dict, str as _str +from dataclasses import dataclass, field as _dataclass_field +from typing import Any as _Any, Literal + +from . import _union @dataclass class EventSearching: sequence_number: int type_: Literal["searching"] + additional_properties: _dict[_str, + _Any] = _dataclass_field(default_factory = lambda: _dict[_str, + _Any](), kw_only = True, metadata = {"openapi_additional_properties": True, "openapi_dictionary": False}) def to_dict(self) -> dict[str, object]: result: dict[str, object] = {} result["sequence_number"] = self.sequence_number result["type"] = self.type_ + for key, item in self.additional_properties.items(): + if key in ['sequence_number', 'type']: + raise ValueError("Additional property collides with declared wire field: " + key) + + result[key] = _union.value_0_to_dict(item) + return result @classmethod @@ -23,4 +35,5 @@ class EventSearching: return cls( sequence_number=data["sequence_number"], # type: ignore[assignment] type_=data["type"], # type: ignore[assignment] + additional_properties = {key: _union.value_0_from_dict(item) for key, item in data.items() if key not in ['sequence_number', 'type']}, ) diff --git a/tests/golden/python/python-requests/open-tagged-additional-properties/open_tagged_additional_properties/runtime/_union_support.py.golden b/tests/golden/python/python-requests/open-tagged-additional-properties/open_tagged_additional_properties/runtime/_union_support.py.golden new file mode 100644 index 000000000..6b104cbd9 --- /dev/null +++ b/tests/golden/python/python-requests/open-tagged-additional-properties/open_tagged_additional_properties/runtime/_union_support.py.golden @@ -0,0 +1,109 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Open tagged additional properties — 1.0.0 + +"""Internal structural selection support; deliberately not a schema validator.""" + +from __future__ import annotations + +from dataclasses import fields, is_dataclass +from enum import Enum +from math import isfinite +from typing import Protocol, TypeGuard, cast + + +class _Model(Protocol): + def to_dict(self) -> dict[str, object]: ... + + +def as_dict(value: object) -> dict[str, object]: + return cast(dict[str, object], value) + + +def as_list(value: object) -> list[object]: + return cast(list[object], value) + + +def is_dict(value: object) -> TypeGuard[dict[str, object]]: + return isinstance(value, dict) and all(isinstance(key, str) for key in cast(dict[object, object], value)) + + +def is_list(value: object) -> TypeGuard[list[object]]: + return isinstance(value, list) + + +def _additional_properties(value: object, dictionary_only: bool = False) -> dict[str, object] | None: + if not isinstance(value, type) and is_dataclass(value): + for field in fields(value): + if field.metadata.get("openapi_additional_properties") is True: + if not dictionary_only or field.metadata.get("openapi_dictionary") is True: + return as_dict(getattr(value, field.name)) + return None + + +def is_mapping(value: object) -> bool: + return is_dict(value) or _additional_properties(value, True) is not None + + +def mapping(value: object) -> dict[str, object]: + properties = _additional_properties(value, True) + return properties if properties is not None else as_dict(value) + + +def scalar(value: object) -> object: + return value.value if isinstance(value, Enum) else value + + +def model_to_dict(value: object) -> object: + return value if is_dict(value) else cast(_Model, value).to_dict() + + +def is_object(value: object) -> bool: + return is_dict(value) or (not isinstance(value, type) and is_dataclass(value)) + + +def has(value: object, wire: str, field: str, required: bool) -> bool: + if is_dict(value): + return wire in as_dict(value) + # Optional dataclass fields use None for absence. Required fields still + # undergo their shape check; a nullable required field accepts that None. + return hasattr(value, field) and (required or getattr(value, field) is not None) + + +def get(value: object, wire: str, field: str) -> object: + return value.get(wire) if is_dict(value) else getattr(value, field, None) + + +def extras(value: object, known: list[str]) -> list[object]: + if is_dict(value): + return [item for key, item in as_dict(value).items() if key not in known] + properties = _additional_properties(value) + return list(properties.values()) if properties is not None else [] + + +def number(value: object, integer: bool) -> bool: + value = scalar(value) + if isinstance(value, bool): + return False + if isinstance(value, int): + return True + return isinstance(value, float) and isfinite(value) and (not integer or value.is_integer()) + + +def equal(value: object, expected: object) -> bool: + value = scalar(value) + if isinstance(expected, bool): + return isinstance(value, bool) and value == expected + if isinstance(expected, (int, float)): + return number(value, False) and value == expected + if is_list(expected): + if not is_list(value): + return False + left, right = as_list(value), as_list(expected) + return len(left) == len(right) and all(equal(a, b) for a, b in zip(left, right)) + if is_dict(expected): + if not is_dict(value): + return False + left, right = as_dict(value), as_dict(expected) + return left.keys() == right.keys() and all(equal(left[key], item) for key, item in right.items()) + return type(value) is type(expected) and value == expected diff --git a/tests/golden/python/python-requests/type-aliases-complex-union/complex_union_type_test/runtime/_union_support.py.golden b/tests/golden/python/python-requests/type-aliases-complex-union/complex_union_type_test/runtime/_union_support.py.golden index 1138cbb7b..fec92c4df 100644 --- a/tests/golden/python/python-requests/type-aliases-complex-union/complex_union_type_test/runtime/_union_support.py.golden +++ b/tests/golden/python/python-requests/type-aliases-complex-union/complex_union_type_test/runtime/_union_support.py.golden @@ -7,7 +7,7 @@ from __future__ import annotations -from dataclasses import is_dataclass +from dataclasses import fields, is_dataclass from enum import Enum from math import isfinite from typing import Protocol, TypeGuard, cast @@ -33,6 +33,24 @@ def is_list(value: object) -> TypeGuard[list[object]]: return isinstance(value, list) +def _additional_properties(value: object, dictionary_only: bool = False) -> dict[str, object] | None: + if not isinstance(value, type) and is_dataclass(value): + for field in fields(value): + if field.metadata.get("openapi_additional_properties") is True: + if not dictionary_only or field.metadata.get("openapi_dictionary") is True: + return as_dict(getattr(value, field.name)) + return None + + +def is_mapping(value: object) -> bool: + return is_dict(value) or _additional_properties(value, True) is not None + + +def mapping(value: object) -> dict[str, object]: + properties = _additional_properties(value, True) + return properties if properties is not None else as_dict(value) + + def scalar(value: object) -> object: return value.value if isinstance(value, Enum) else value @@ -58,9 +76,10 @@ def get(value: object, wire: str, field: str) -> object: def extras(value: object, known: list[str]) -> list[object]: - if not is_dict(value): - return [] - return [item for key, item in as_dict(value).items() if key not in known] + if is_dict(value): + return [item for key, item in as_dict(value).items() if key not in known] + properties = _additional_properties(value) + return list(properties.values()) if properties is not None else [] def number(value: object, integer: bool) -> bool: diff --git a/tests/golden/python/python-requests/type-aliases-nested-union/nested_union_type_test/runtime/_union_support.py.golden b/tests/golden/python/python-requests/type-aliases-nested-union/nested_union_type_test/runtime/_union_support.py.golden index 386b49b07..6ef782e37 100644 --- a/tests/golden/python/python-requests/type-aliases-nested-union/nested_union_type_test/runtime/_union_support.py.golden +++ b/tests/golden/python/python-requests/type-aliases-nested-union/nested_union_type_test/runtime/_union_support.py.golden @@ -7,7 +7,7 @@ from __future__ import annotations -from dataclasses import is_dataclass +from dataclasses import fields, is_dataclass from enum import Enum from math import isfinite from typing import Protocol, TypeGuard, cast @@ -33,6 +33,24 @@ def is_list(value: object) -> TypeGuard[list[object]]: return isinstance(value, list) +def _additional_properties(value: object, dictionary_only: bool = False) -> dict[str, object] | None: + if not isinstance(value, type) and is_dataclass(value): + for field in fields(value): + if field.metadata.get("openapi_additional_properties") is True: + if not dictionary_only or field.metadata.get("openapi_dictionary") is True: + return as_dict(getattr(value, field.name)) + return None + + +def is_mapping(value: object) -> bool: + return is_dict(value) or _additional_properties(value, True) is not None + + +def mapping(value: object) -> dict[str, object]: + properties = _additional_properties(value, True) + return properties if properties is not None else as_dict(value) + + def scalar(value: object) -> object: return value.value if isinstance(value, Enum) else value @@ -58,9 +76,10 @@ def get(value: object, wire: str, field: str) -> object: def extras(value: object, known: list[str]) -> list[object]: - if not is_dict(value): - return [] - return [item for key, item in as_dict(value).items() if key not in known] + if is_dict(value): + return [item for key, item in as_dict(value).items() if key not in known] + properties = _additional_properties(value) + return list(properties.values()) if properties is not None else [] def number(value: object, integer: bool) -> bool: diff --git a/tests/golden/python/python-requests/type-aliases-union-mixed/union_type_with_mixed_members_test/runtime/_union_support.py.golden b/tests/golden/python/python-requests/type-aliases-union-mixed/union_type_with_mixed_members_test/runtime/_union_support.py.golden index 8a0a49a69..8c966fcc9 100644 --- a/tests/golden/python/python-requests/type-aliases-union-mixed/union_type_with_mixed_members_test/runtime/_union_support.py.golden +++ b/tests/golden/python/python-requests/type-aliases-union-mixed/union_type_with_mixed_members_test/runtime/_union_support.py.golden @@ -7,7 +7,7 @@ from __future__ import annotations -from dataclasses import is_dataclass +from dataclasses import fields, is_dataclass from enum import Enum from math import isfinite from typing import Protocol, TypeGuard, cast @@ -33,6 +33,24 @@ def is_list(value: object) -> TypeGuard[list[object]]: return isinstance(value, list) +def _additional_properties(value: object, dictionary_only: bool = False) -> dict[str, object] | None: + if not isinstance(value, type) and is_dataclass(value): + for field in fields(value): + if field.metadata.get("openapi_additional_properties") is True: + if not dictionary_only or field.metadata.get("openapi_dictionary") is True: + return as_dict(getattr(value, field.name)) + return None + + +def is_mapping(value: object) -> bool: + return is_dict(value) or _additional_properties(value, True) is not None + + +def mapping(value: object) -> dict[str, object]: + properties = _additional_properties(value, True) + return properties if properties is not None else as_dict(value) + + def scalar(value: object) -> object: return value.value if isinstance(value, Enum) else value @@ -58,9 +76,10 @@ def get(value: object, wire: str, field: str) -> object: def extras(value: object, known: list[str]) -> list[object]: - if not is_dict(value): - return [] - return [item for key, item in as_dict(value).items() if key not in known] + if is_dict(value): + return [item for key, item in as_dict(value).items() if key not in known] + properties = _additional_properties(value) + return list(properties.values()) if properties is not None else [] def number(value: object, integer: bool) -> bool: diff --git a/tests/golden/python/python-requests/type-aliases-union-with-any/union_type_with_any_test/models/config.py.golden b/tests/golden/python/python-requests/type-aliases-union-with-any/union_type_with_any_test/models/config.py.golden index 4a36e52d5..1b88e980f 100644 --- a/tests/golden/python/python-requests/type-aliases-union-with-any/union_type_with_any_test/models/config.py.golden +++ b/tests/golden/python/python-requests/type-aliases-union-with-any/union_type_with_any_test/models/config.py.golden @@ -5,14 +5,14 @@ from __future__ import annotations -from typing import Any +from typing import Any as _Any from . import _union type Config = ( str | float - | Any + | _Any ) def config_from_dict(value: object) -> Config: diff --git a/tests/golden/python/python-requests/type-aliases-union-with-any/union_type_with_any_test/runtime/_union_support.py.golden b/tests/golden/python/python-requests/type-aliases-union-with-any/union_type_with_any_test/runtime/_union_support.py.golden index aa7278d60..78babf760 100644 --- a/tests/golden/python/python-requests/type-aliases-union-with-any/union_type_with_any_test/runtime/_union_support.py.golden +++ b/tests/golden/python/python-requests/type-aliases-union-with-any/union_type_with_any_test/runtime/_union_support.py.golden @@ -7,7 +7,7 @@ from __future__ import annotations -from dataclasses import is_dataclass +from dataclasses import fields, is_dataclass from enum import Enum from math import isfinite from typing import Protocol, TypeGuard, cast @@ -33,6 +33,24 @@ def is_list(value: object) -> TypeGuard[list[object]]: return isinstance(value, list) +def _additional_properties(value: object, dictionary_only: bool = False) -> dict[str, object] | None: + if not isinstance(value, type) and is_dataclass(value): + for field in fields(value): + if field.metadata.get("openapi_additional_properties") is True: + if not dictionary_only or field.metadata.get("openapi_dictionary") is True: + return as_dict(getattr(value, field.name)) + return None + + +def is_mapping(value: object) -> bool: + return is_dict(value) or _additional_properties(value, True) is not None + + +def mapping(value: object) -> dict[str, object]: + properties = _additional_properties(value, True) + return properties if properties is not None else as_dict(value) + + def scalar(value: object) -> object: return value.value if isinstance(value, Enum) else value @@ -58,9 +76,10 @@ def get(value: object, wire: str, field: str) -> object: def extras(value: object, known: list[str]) -> list[object]: - if not is_dict(value): - return [] - return [item for key, item in as_dict(value).items() if key not in known] + if is_dict(value): + return [item for key, item in as_dict(value).items() if key not in known] + properties = _additional_properties(value) + return list(properties.values()) if properties is not None else [] def number(value: object, integer: bool) -> bool: diff --git a/tests/golden/python/python-requests/type-aliases-union-with-inline-objects/union_type_with_inline_objects_test/runtime/_union_support.py.golden b/tests/golden/python/python-requests/type-aliases-union-with-inline-objects/union_type_with_inline_objects_test/runtime/_union_support.py.golden index 20fb2628d..4b95f0b66 100644 --- a/tests/golden/python/python-requests/type-aliases-union-with-inline-objects/union_type_with_inline_objects_test/runtime/_union_support.py.golden +++ b/tests/golden/python/python-requests/type-aliases-union-with-inline-objects/union_type_with_inline_objects_test/runtime/_union_support.py.golden @@ -7,7 +7,7 @@ from __future__ import annotations -from dataclasses import is_dataclass +from dataclasses import fields, is_dataclass from enum import Enum from math import isfinite from typing import Protocol, TypeGuard, cast @@ -33,6 +33,24 @@ def is_list(value: object) -> TypeGuard[list[object]]: return isinstance(value, list) +def _additional_properties(value: object, dictionary_only: bool = False) -> dict[str, object] | None: + if not isinstance(value, type) and is_dataclass(value): + for field in fields(value): + if field.metadata.get("openapi_additional_properties") is True: + if not dictionary_only or field.metadata.get("openapi_dictionary") is True: + return as_dict(getattr(value, field.name)) + return None + + +def is_mapping(value: object) -> bool: + return is_dict(value) or _additional_properties(value, True) is not None + + +def mapping(value: object) -> dict[str, object]: + properties = _additional_properties(value, True) + return properties if properties is not None else as_dict(value) + + def scalar(value: object) -> object: return value.value if isinstance(value, Enum) else value @@ -58,9 +76,10 @@ def get(value: object, wire: str, field: str) -> object: def extras(value: object, known: list[str]) -> list[object]: - if not is_dict(value): - return [] - return [item for key, item in as_dict(value).items() if key not in known] + if is_dict(value): + return [item for key, item in as_dict(value).items() if key not in known] + properties = _additional_properties(value) + return list(properties.values()) if properties is not None else [] def number(value: object, integer: bool) -> bool: diff --git a/tests/golden/python/python-requests/type-aliases-union-with-interfaces/union_type_with_interfaces_test/runtime/_union_support.py.golden b/tests/golden/python/python-requests/type-aliases-union-with-interfaces/union_type_with_interfaces_test/runtime/_union_support.py.golden index b207b3392..1b6ea2483 100644 --- a/tests/golden/python/python-requests/type-aliases-union-with-interfaces/union_type_with_interfaces_test/runtime/_union_support.py.golden +++ b/tests/golden/python/python-requests/type-aliases-union-with-interfaces/union_type_with_interfaces_test/runtime/_union_support.py.golden @@ -7,7 +7,7 @@ from __future__ import annotations -from dataclasses import is_dataclass +from dataclasses import fields, is_dataclass from enum import Enum from math import isfinite from typing import Protocol, TypeGuard, cast @@ -33,6 +33,24 @@ def is_list(value: object) -> TypeGuard[list[object]]: return isinstance(value, list) +def _additional_properties(value: object, dictionary_only: bool = False) -> dict[str, object] | None: + if not isinstance(value, type) and is_dataclass(value): + for field in fields(value): + if field.metadata.get("openapi_additional_properties") is True: + if not dictionary_only or field.metadata.get("openapi_dictionary") is True: + return as_dict(getattr(value, field.name)) + return None + + +def is_mapping(value: object) -> bool: + return is_dict(value) or _additional_properties(value, True) is not None + + +def mapping(value: object) -> dict[str, object]: + properties = _additional_properties(value, True) + return properties if properties is not None else as_dict(value) + + def scalar(value: object) -> object: return value.value if isinstance(value, Enum) else value @@ -58,9 +76,10 @@ def get(value: object, wire: str, field: str) -> object: def extras(value: object, known: list[str]) -> list[object]: - if not is_dict(value): - return [] - return [item for key, item in as_dict(value).items() if key not in known] + if is_dict(value): + return [item for key, item in as_dict(value).items() if key not in known] + properties = _additional_properties(value) + return list(properties.values()) if properties is not None else [] def number(value: object, integer: bool) -> bool: diff --git a/tests/golden/python/python-requests/type-aliases-union-with-primitives/union_type_with_primitives_test/runtime/_union_support.py.golden b/tests/golden/python/python-requests/type-aliases-union-with-primitives/union_type_with_primitives_test/runtime/_union_support.py.golden index 30f44c9b4..6ebe5e3e6 100644 --- a/tests/golden/python/python-requests/type-aliases-union-with-primitives/union_type_with_primitives_test/runtime/_union_support.py.golden +++ b/tests/golden/python/python-requests/type-aliases-union-with-primitives/union_type_with_primitives_test/runtime/_union_support.py.golden @@ -7,7 +7,7 @@ from __future__ import annotations -from dataclasses import is_dataclass +from dataclasses import fields, is_dataclass from enum import Enum from math import isfinite from typing import Protocol, TypeGuard, cast @@ -33,6 +33,24 @@ def is_list(value: object) -> TypeGuard[list[object]]: return isinstance(value, list) +def _additional_properties(value: object, dictionary_only: bool = False) -> dict[str, object] | None: + if not isinstance(value, type) and is_dataclass(value): + for field in fields(value): + if field.metadata.get("openapi_additional_properties") is True: + if not dictionary_only or field.metadata.get("openapi_dictionary") is True: + return as_dict(getattr(value, field.name)) + return None + + +def is_mapping(value: object) -> bool: + return is_dict(value) or _additional_properties(value, True) is not None + + +def mapping(value: object) -> dict[str, object]: + properties = _additional_properties(value, True) + return properties if properties is not None else as_dict(value) + + def scalar(value: object) -> object: return value.value if isinstance(value, Enum) else value @@ -58,9 +76,10 @@ def get(value: object, wire: str, field: str) -> object: def extras(value: object, known: list[str]) -> list[object]: - if not is_dict(value): - return [] - return [item for key, item in as_dict(value).items() if key not in known] + if is_dict(value): + return [item for key, item in as_dict(value).items() if key not in known] + properties = _additional_properties(value) + return list(properties.values()) if properties is not None else [] def number(value: object, integer: bool) -> bool: diff --git a/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/models/_union.py.golden b/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/models/_union.py.golden index 3836a8a0c..726e80770 100644 --- a/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/models/_union.py.golden +++ b/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/models/_union.py.golden @@ -127,7 +127,7 @@ def value_18_from_dict(value: object) -> object: return {key: value_11_from_dict(item) for key, item in _union_support.as_dict(value).items()} def value_18_to_dict(value: object) -> object: - return {key: value_11_to_dict(item) for key, item in _union_support.as_dict(value).items()} + return {key: value_11_to_dict(item) for key, item in _union_support.mapping(value).items()} def value_19_from_dict(value: object) -> object: return [value_20_from_dict(item) for item in _union_support.as_list(value)] @@ -139,7 +139,7 @@ def value_20_from_dict(value: object) -> object: return {key: value_17_from_dict(item) for key, item in _union_support.as_dict(value).items()} def value_20_to_dict(value: object) -> object: - return {key: value_17_to_dict(item) for key, item in _union_support.as_dict(value).items()} + return {key: value_17_to_dict(item) for key, item in _union_support.mapping(value).items()} def value_21_from_dict(value: object) -> object: return value_22_from_dict(value) @@ -333,7 +333,7 @@ def value_46_from_dict(value: object) -> object: return {key: value_45_from_dict(item) for key, item in _union_support.as_dict(value).items()} def value_46_to_dict(value: object) -> object: - return {key: value_45_to_dict(item) for key, item in _union_support.as_dict(value).items()} + return {key: value_45_to_dict(item) for key, item in _union_support.mapping(value).items()} def value_47_from_dict(value: object) -> object: return [value_44_from_dict(item) for item in _union_support.as_list(value)] diff --git a/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/models/client_tool.py.golden b/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/models/client_tool.py.golden index b65bef982..d34e4696b 100644 --- a/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/models/client_tool.py.golden +++ b/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/models/client_tool.py.golden @@ -5,11 +5,11 @@ from __future__ import annotations from dataclasses import dataclass -from typing import Any +from typing import Any as _Any @dataclass class ClientTool: - input_schema: dict[str, Any] + input_schema: dict[str, _Any] name: str def to_dict(self) -> dict[str, object]: diff --git a/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/runtime/_union_support.py.golden b/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/runtime/_union_support.py.golden index a4c9e6107..ea1b63caa 100644 --- a/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/runtime/_union_support.py.golden +++ b/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/runtime/_union_support.py.golden @@ -6,7 +6,7 @@ from __future__ import annotations -from dataclasses import is_dataclass +from dataclasses import fields, is_dataclass from enum import Enum from math import isfinite from typing import Protocol, TypeGuard, cast @@ -32,6 +32,24 @@ def is_list(value: object) -> TypeGuard[list[object]]: return isinstance(value, list) +def _additional_properties(value: object, dictionary_only: bool = False) -> dict[str, object] | None: + if not isinstance(value, type) and is_dataclass(value): + for field in fields(value): + if field.metadata.get("openapi_additional_properties") is True: + if not dictionary_only or field.metadata.get("openapi_dictionary") is True: + return as_dict(getattr(value, field.name)) + return None + + +def is_mapping(value: object) -> bool: + return is_dict(value) or _additional_properties(value, True) is not None + + +def mapping(value: object) -> dict[str, object]: + properties = _additional_properties(value, True) + return properties if properties is not None else as_dict(value) + + def scalar(value: object) -> object: return value.value if isinstance(value, Enum) else value @@ -57,9 +75,10 @@ def get(value: object, wire: str, field: str) -> object: def extras(value: object, known: list[str]) -> list[object]: - if not is_dict(value): - return [] - return [item for key, item in as_dict(value).items() if key not in known] + if is_dict(value): + return [item for key, item in as_dict(value).items() if key not in known] + properties = _additional_properties(value) + return list(properties.values()) if properties is not None else [] def number(value: object, integer: bool) -> bool: diff --git a/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/models/_union.py.golden b/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/models/_union.py.golden index da2f4f470..776324e37 100644 --- a/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/models/_union.py.golden +++ b/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/models/_union.py.golden @@ -307,7 +307,7 @@ def value_18_from_dict(value: object) -> object: return {key: value_11_from_dict(item) for key, item in _union_support.as_dict(value).items()} def value_18_to_dict(value: object) -> object: - return {key: value_11_to_dict(item) for key, item in _union_support.as_dict(value).items()} + return {key: value_11_to_dict(item) for key, item in _union_support.mapping(value).items()} def value_19_from_dict(value: object) -> object: return [value_20_from_dict(item) for item in _union_support.as_list(value)] @@ -319,7 +319,7 @@ def value_20_from_dict(value: object) -> object: return {key: value_17_from_dict(item) for key, item in _union_support.as_dict(value).items()} def value_20_to_dict(value: object) -> object: - return {key: value_17_to_dict(item) for key, item in _union_support.as_dict(value).items()} + return {key: value_17_to_dict(item) for key, item in _union_support.mapping(value).items()} def value_21_from_dict(value: object) -> object: return value_22_from_dict(value) @@ -591,7 +591,7 @@ def value_34_from_dict(value: object) -> object: def value_34_to_dict(value: object) -> object: matches = [(_union_support.is_object(value) - and (_union_support.has(value, 'input_schema', 'input_schema', True) and (_union_support.is_dict(_union_support.get(value, 'input_schema', 'input_schema')) and all(True for _item in _union_support.as_dict(_union_support.get(value, 'input_schema', 'input_schema')).values()))) + and (_union_support.has(value, 'input_schema', 'input_schema', True) and (_union_support.is_mapping(_union_support.get(value, 'input_schema', 'input_schema')) and all(True for _item in _union_support.mapping(_union_support.get(value, 'input_schema', 'input_schema')).values()))) and (_union_support.has(value, 'name', 'name', True) and isinstance(_union_support.get(value, 'name', 'name'), str))), (_union_support.is_object(value) and (not _union_support.has(value, 'max_uses', 'max_uses', False) or _union_support.number(_union_support.get(value, 'max_uses', 'max_uses'), True)) and (_union_support.has(value, 'name', 'name', True) and _union_support.equal(_union_support.get(value, 'name', 'name'), 'web_search')) @@ -819,7 +819,7 @@ def value_46_from_dict(value: object) -> object: return {key: value_45_from_dict(item) for key, item in _union_support.as_dict(value).items()} def value_46_to_dict(value: object) -> object: - return {key: value_45_to_dict(item) for key, item in _union_support.as_dict(value).items()} + return {key: value_45_to_dict(item) for key, item in _union_support.mapping(value).items()} def value_47_from_dict(value: object) -> object: return [value_44_from_dict(item) for item in _union_support.as_list(value)] @@ -875,13 +875,13 @@ def value_48_from_dict(value: object) -> object: def value_48_to_dict(value: object) -> object: matches = [(_union_support.is_object(value) - and (_union_support.has(value, 'mapping', 'mapping', True) and (_union_support.is_dict(_union_support.get(value, 'mapping', 'mapping')) and all(((_union_support.is_object(_item) + and (_union_support.has(value, 'mapping', 'mapping', True) and (_union_support.is_mapping(_union_support.get(value, 'mapping', 'mapping')) and all(((_union_support.is_object(_item) and (_union_support.has(_item, 'name', 'name', True) and isinstance(_union_support.get(_item, 'name', 'name'), str))) or (_union_support.is_object(_item) and (_union_support.has(_item, 'revision', 'revision', True) and (_union_support.number(_union_support.get(_item, 'revision', 'revision'), True) and _union_support.equal(_union_support.get(_item, 'revision', 'revision'), 2))) and (not _union_support.has(_item, 'second_value', 'second_value', False) or isinstance(_union_support.get(_item, 'second_value', 'second_value'), str))) - or _union_support.number(_item, True)) for _item in _union_support.as_dict(_union_support.get(value, 'mapping', 'mapping')).values()))) + or _union_support.number(_item, True)) for _item in _union_support.mapping(_union_support.get(value, 'mapping', 'mapping')).values()))) and (_union_support.has(value, 'value', 'value', True) and ((_union_support.is_object(_union_support.get(value, 'value', 'value')) and (_union_support.has(_union_support.get(value, 'value', 'value'), 'name', 'name', True) and isinstance(_union_support.get(_union_support.get(value, 'value', 'value'), 'name', 'name'), str))) or (_union_support.is_object(_union_support.get(value, 'value', 'value')) diff --git a/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/models/client_tool.py.golden b/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/models/client_tool.py.golden index b65bef982..d34e4696b 100644 --- a/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/models/client_tool.py.golden +++ b/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/models/client_tool.py.golden @@ -5,11 +5,11 @@ from __future__ import annotations from dataclasses import dataclass -from typing import Any +from typing import Any as _Any @dataclass class ClientTool: - input_schema: dict[str, Any] + input_schema: dict[str, _Any] name: str def to_dict(self) -> dict[str, object]: diff --git a/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/models/open_choice.py.golden b/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/models/open_choice.py.golden index 3bcd71d81..5b3fd2874 100644 --- a/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/models/open_choice.py.golden +++ b/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/models/open_choice.py.golden @@ -4,13 +4,13 @@ from __future__ import annotations -from typing import Any +from typing import Any as _Any from . import _union from .numeric_two import NumericTwo type OpenChoice = ( - Any + _Any | NumericTwo ) diff --git a/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/runtime/_union_support.py.golden b/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/runtime/_union_support.py.golden index a4c9e6107..ea1b63caa 100644 --- a/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/runtime/_union_support.py.golden +++ b/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/runtime/_union_support.py.golden @@ -6,7 +6,7 @@ from __future__ import annotations -from dataclasses import is_dataclass +from dataclasses import fields, is_dataclass from enum import Enum from math import isfinite from typing import Protocol, TypeGuard, cast @@ -32,6 +32,24 @@ def is_list(value: object) -> TypeGuard[list[object]]: return isinstance(value, list) +def _additional_properties(value: object, dictionary_only: bool = False) -> dict[str, object] | None: + if not isinstance(value, type) and is_dataclass(value): + for field in fields(value): + if field.metadata.get("openapi_additional_properties") is True: + if not dictionary_only or field.metadata.get("openapi_dictionary") is True: + return as_dict(getattr(value, field.name)) + return None + + +def is_mapping(value: object) -> bool: + return is_dict(value) or _additional_properties(value, True) is not None + + +def mapping(value: object) -> dict[str, object]: + properties = _additional_properties(value, True) + return properties if properties is not None else as_dict(value) + + def scalar(value: object) -> object: return value.value if isinstance(value, Enum) else value @@ -57,9 +75,10 @@ def get(value: object, wire: str, field: str) -> object: def extras(value: object, known: list[str]) -> list[object]: - if not is_dict(value): - return [] - return [item for key, item in as_dict(value).items() if key not in known] + if is_dict(value): + return [item for key, item in as_dict(value).items() if key not in known] + properties = _additional_properties(value) + return list(properties.values()) if properties is not None else [] def number(value: object, integer: bool) -> bool: diff --git a/tests/golden/python/python-requests/untagged-union/python_union_conversion/models/_union.py.golden b/tests/golden/python/python-requests/untagged-union/python_union_conversion/models/_union.py.golden index b537ed00a..c7d820872 100644 --- a/tests/golden/python/python-requests/untagged-union/python_union_conversion/models/_union.py.golden +++ b/tests/golden/python/python-requests/untagged-union/python_union_conversion/models/_union.py.golden @@ -259,7 +259,7 @@ def value_18_from_dict(value: object) -> object: return {key: value_11_from_dict(item) for key, item in _union_support.as_dict(value).items()} def value_18_to_dict(value: object) -> object: - return {key: value_11_to_dict(item) for key, item in _union_support.as_dict(value).items()} + return {key: value_11_to_dict(item) for key, item in _union_support.mapping(value).items()} def value_19_from_dict(value: object) -> object: return [value_20_from_dict(item) for item in _union_support.as_list(value)] @@ -271,7 +271,7 @@ def value_20_from_dict(value: object) -> object: return {key: value_17_from_dict(item) for key, item in _union_support.as_dict(value).items()} def value_20_to_dict(value: object) -> object: - return {key: value_17_to_dict(item) for key, item in _union_support.as_dict(value).items()} + return {key: value_17_to_dict(item) for key, item in _union_support.mapping(value).items()} def value_21_from_dict(value: object) -> object: return value_22_from_dict(value) @@ -491,7 +491,7 @@ def value_34_from_dict(value: object) -> object: def value_34_to_dict(value: object) -> object: if (_union_support.is_object(value) - and (_union_support.has(value, 'input_schema', 'input_schema', True) and (_union_support.is_dict(_union_support.get(value, 'input_schema', 'input_schema')) and all(True for _item in _union_support.as_dict(_union_support.get(value, 'input_schema', 'input_schema')).values()))) + and (_union_support.has(value, 'input_schema', 'input_schema', True) and (_union_support.is_mapping(_union_support.get(value, 'input_schema', 'input_schema')) and all(True for _item in _union_support.mapping(_union_support.get(value, 'input_schema', 'input_schema')).values()))) and (_union_support.has(value, 'name', 'name', True) and isinstance(_union_support.get(value, 'name', 'name'), str))): return value_35_to_dict(value) @@ -691,7 +691,7 @@ def value_46_from_dict(value: object) -> object: return {key: value_45_from_dict(item) for key, item in _union_support.as_dict(value).items()} def value_46_to_dict(value: object) -> object: - return {key: value_45_to_dict(item) for key, item in _union_support.as_dict(value).items()} + return {key: value_45_to_dict(item) for key, item in _union_support.mapping(value).items()} def value_47_from_dict(value: object) -> object: return [value_44_from_dict(item) for item in _union_support.as_list(value)] @@ -743,13 +743,13 @@ def value_48_from_dict(value: object) -> object: def value_48_to_dict(value: object) -> object: if (_union_support.is_object(value) - and (_union_support.has(value, 'mapping', 'mapping', True) and (_union_support.is_dict(_union_support.get(value, 'mapping', 'mapping')) and all(((_union_support.is_object(_item) + and (_union_support.has(value, 'mapping', 'mapping', True) and (_union_support.is_mapping(_union_support.get(value, 'mapping', 'mapping')) and all(((_union_support.is_object(_item) and (_union_support.has(_item, 'name', 'name', True) and isinstance(_union_support.get(_item, 'name', 'name'), str))) or (_union_support.is_object(_item) and (_union_support.has(_item, 'revision', 'revision', True) and (_union_support.number(_union_support.get(_item, 'revision', 'revision'), True) and _union_support.equal(_union_support.get(_item, 'revision', 'revision'), 2))) and (not _union_support.has(_item, 'second_value', 'second_value', False) or isinstance(_union_support.get(_item, 'second_value', 'second_value'), str))) - or _union_support.number(_item, True)) for _item in _union_support.as_dict(_union_support.get(value, 'mapping', 'mapping')).values()))) + or _union_support.number(_item, True)) for _item in _union_support.mapping(_union_support.get(value, 'mapping', 'mapping')).values()))) and (_union_support.has(value, 'value', 'value', True) and ((_union_support.is_object(_union_support.get(value, 'value', 'value')) and (_union_support.has(_union_support.get(value, 'value', 'value'), 'name', 'name', True) and isinstance(_union_support.get(_union_support.get(value, 'value', 'value'), 'name', 'name'), str))) or (_union_support.is_object(_union_support.get(value, 'value', 'value')) diff --git a/tests/golden/python/python-requests/untagged-union/python_union_conversion/models/client_tool.py.golden b/tests/golden/python/python-requests/untagged-union/python_union_conversion/models/client_tool.py.golden index b65bef982..d34e4696b 100644 --- a/tests/golden/python/python-requests/untagged-union/python_union_conversion/models/client_tool.py.golden +++ b/tests/golden/python/python-requests/untagged-union/python_union_conversion/models/client_tool.py.golden @@ -5,11 +5,11 @@ from __future__ import annotations from dataclasses import dataclass -from typing import Any +from typing import Any as _Any @dataclass class ClientTool: - input_schema: dict[str, Any] + input_schema: dict[str, _Any] name: str def to_dict(self) -> dict[str, object]: diff --git a/tests/golden/python/python-requests/untagged-union/python_union_conversion/models/open_choice.py.golden b/tests/golden/python/python-requests/untagged-union/python_union_conversion/models/open_choice.py.golden index 3bcd71d81..5b3fd2874 100644 --- a/tests/golden/python/python-requests/untagged-union/python_union_conversion/models/open_choice.py.golden +++ b/tests/golden/python/python-requests/untagged-union/python_union_conversion/models/open_choice.py.golden @@ -4,13 +4,13 @@ from __future__ import annotations -from typing import Any +from typing import Any as _Any from . import _union from .numeric_two import NumericTwo type OpenChoice = ( - Any + _Any | NumericTwo ) diff --git a/tests/golden/python/python-requests/untagged-union/python_union_conversion/runtime/_union_support.py.golden b/tests/golden/python/python-requests/untagged-union/python_union_conversion/runtime/_union_support.py.golden index a4c9e6107..ea1b63caa 100644 --- a/tests/golden/python/python-requests/untagged-union/python_union_conversion/runtime/_union_support.py.golden +++ b/tests/golden/python/python-requests/untagged-union/python_union_conversion/runtime/_union_support.py.golden @@ -6,7 +6,7 @@ from __future__ import annotations -from dataclasses import is_dataclass +from dataclasses import fields, is_dataclass from enum import Enum from math import isfinite from typing import Protocol, TypeGuard, cast @@ -32,6 +32,24 @@ def is_list(value: object) -> TypeGuard[list[object]]: return isinstance(value, list) +def _additional_properties(value: object, dictionary_only: bool = False) -> dict[str, object] | None: + if not isinstance(value, type) and is_dataclass(value): + for field in fields(value): + if field.metadata.get("openapi_additional_properties") is True: + if not dictionary_only or field.metadata.get("openapi_dictionary") is True: + return as_dict(getattr(value, field.name)) + return None + + +def is_mapping(value: object) -> bool: + return is_dict(value) or _additional_properties(value, True) is not None + + +def mapping(value: object) -> dict[str, object]: + properties = _additional_properties(value, True) + return properties if properties is not None else as_dict(value) + + def scalar(value: object) -> object: return value.value if isinstance(value, Enum) else value @@ -57,9 +75,10 @@ def get(value: object, wire: str, field: str) -> object: def extras(value: object, known: list[str]) -> list[object]: - if not is_dict(value): - return [] - return [item for key, item in as_dict(value).items() if key not in known] + if is_dict(value): + return [item for key, item in as_dict(value).items() if key not in known] + properties = _additional_properties(value) + return list(properties.values()) if properties is not None else [] def number(value: object, integer: bool) -> bool: diff --git a/tests/golden_tests_python_httpx.rs b/tests/golden_tests_python_httpx.rs index ba3a41c6b..193d5bc6b 100644 --- a/tests/golden_tests_python_httpx.rs +++ b/tests/golden_tests_python_httpx.rs @@ -38,6 +38,7 @@ fn get_golden_test_cases() -> HashMap<&'static str, &'static str> { test_cases_from_slice(&[ ("petstore", "valid/petstore.yaml"), ("untagged-union", "valid/python-untagged-union.yaml"), + ("object-extensions", "valid/python-additional-properties.yaml"), ("comprehensive-schemas", "valid/comprehensive-schemas.yaml"), ("delete-with-response-schema", "valid/delete-with-response-schema.yaml"), @@ -128,6 +129,7 @@ macro_rules! generate_golden_tests { generate_golden_tests! { test_untagged_union_golden: "untagged-union", + test_object_extensions_golden: "object-extensions", test_nested_nullable_golden: "nested-nullable", test_petstore_golden: "petstore", diff --git a/tests/golden_tests_python_requests.rs b/tests/golden_tests_python_requests.rs index d93a8731d..d52ecbffe 100644 --- a/tests/golden_tests_python_requests.rs +++ b/tests/golden_tests_python_requests.rs @@ -38,6 +38,7 @@ fn get_golden_test_cases() -> HashMap<&'static str, &'static str> { test_cases_from_slice(&[ ("petstore", "valid/petstore.yaml"), ("untagged-union", "valid/python-untagged-union.yaml"), + ("object-extensions", "valid/python-additional-properties.yaml"), ("comprehensive-schemas", "valid/comprehensive-schemas.yaml"), ("delete-with-response-schema", "valid/delete-with-response-schema.yaml"), @@ -128,6 +129,7 @@ macro_rules! generate_golden_tests { generate_golden_tests! { test_untagged_union_golden: "untagged-union", + test_object_extensions_golden: "object-extensions", test_nested_nullable_golden: "nested-nullable", test_petstore_golden: "petstore", diff --git a/tests/python_additional_properties.rs b/tests/python_additional_properties.rs new file mode 100644 index 000000000..6a71fabf2 --- /dev/null +++ b/tests/python_additional_properties.rs @@ -0,0 +1,32 @@ +use openapi_nexus::codegen::traits::code_generator::CodeGenerator; +use openapi_nexus::generators::python::{ + httpx::PythonHttpxCodeGenerator, requests::PythonRequestsCodeGenerator, +}; +use openapi_nexus::ir::{lower::lower, types::IrSchemaKind}; +use openapi_nexus::parser::parse_content_yaml; + +#[test] +fn explicit_extensions_retain_value_facts_and_usable_model_converters() { + let source = "openapi: 3.1.0\ninfo: {title: Python dictionaries, version: '1'}\npaths: {}\ncomponents:\n schemas:\n OpaqueMap: {type: object, additionalProperties: true}\n MixedMap:\n type: object\n properties:\n known_field: {type: string}\n additionalProperties: {type: string}\n TypedMap: {type: object, additionalProperties: {type: string}}\n"; + for version in ["3.0.3", "3.1.0", "3.2.0"] { + let ir = lower(parse_content_yaml(&source.replacen("3.1.0", version, 1)).unwrap()).unwrap(); + for name in ["OpaqueMap", "MixedMap", "TypedMap"] { + let IrSchemaKind::Object(object) = &ir.schemas[name].kind else { + panic!("explicit additional properties retain object facts"); + }; + assert!(object.additional_properties.is_some()); + } + for generator in [ + Box::new(PythonHttpxCodeGenerator::new(Default::default())) as Box, + Box::new(PythonRequestsCodeGenerator::new(Default::default())), + ] { + let files = generator.generate(&ir).unwrap(); + for name in ["opaque_map.py", "mixed_map.py", "typed_map.py"] { + let file = files.iter().find(|file| file.filename == name).unwrap(); + assert!(file.content.contains("additional_properties:"), "{name}"); + assert!(file.content.contains("def from_dict("), "{name}"); + assert!(file.content.contains("def to_dict("), "{name}"); + } + } + } +}