From fb71b7b8b5e2a566bb383a4b0131a8e9afcca6e9 Mon Sep 17 00:00:00 2001 From: Aleksandr Penskoi Date: Thu, 2 Jul 2026 15:53:32 +0200 Subject: [PATCH 1/3] PY: make generated profile code type-check under strict mypy - profile-slices: wrap the slice input into its element model under a fresh variable instead of rebinding `merged` (dict -> model assignment) - profile-extensions: narrow the setter else branch with a runtime-validating assert -- is_record(value) for the dict-form complex/generic setters, not-Extension for single-value setters -- and type single-value setters as the value's own model type instead of `Any` (symmetric with the getter) - profile_helpers: is_record now returns TypeGuard[MutableMapping[str, Any]] - profile: import is_record when a dict-form extension setter is emitted --- .../python/profile_helpers.py | 6 +++-- .../python/profile-extensions.ts | 24 +++++++++++++++++-- .../writer-generator/python/profile-slices.ts | 10 +++++--- src/api/writer-generator/python/profile.ts | 6 +++++ 4 files changed, 39 insertions(+), 7 deletions(-) diff --git a/assets/api/writer-generator/python/profile_helpers.py b/assets/api/writer-generator/python/profile_helpers.py index dce9d91c..3be6d93a 100644 --- a/assets/api/writer-generator/python/profile_helpers.py +++ b/assets/api/writer-generator/python/profile_helpers.py @@ -26,6 +26,8 @@ import copy from typing import Any, Iterable, Mapping, MutableMapping, MutableSequence, Sequence, TypeVar +from typing_extensions import TypeGuard + T = TypeVar("T") # --------------------------------------------------------------------------- @@ -33,9 +35,9 @@ # --------------------------------------------------------------------------- -def is_record(value: Any) -> bool: +def is_record(value: Any) -> TypeGuard[MutableMapping[str, Any]]: """True when ``value`` is a non-None mapping (dict-like, not a list).""" - return isinstance(value, Mapping) + return isinstance(value, MutableMapping) def ensure_path(root: MutableMapping[str, Any], path: Sequence[str]) -> MutableMapping[str, Any]: diff --git a/src/api/writer-generator/python/profile-extensions.ts b/src/api/writer-generator/python/profile-extensions.ts index 6648e509..af08d8b0 100644 --- a/src/api/writer-generator/python/profile-extensions.ts +++ b/src/api/writer-generator/python/profile-extensions.ts @@ -74,7 +74,16 @@ export const generateExtensionMethods = ( const valueField = pyValueFieldName(valueType, w.nameFormatFunction); const pyType = pyTypeFromIdentifier(valueType); generateSingleValueExtensionGetter(w, ext, baseName, targetPath, valueField, pyType, extProfileInfo); - generateSingleValueExtensionSetter(w, ext, className, baseName, targetPath, valueField, extProfileInfo); + generateSingleValueExtensionSetter( + w, + ext, + className, + baseName, + targetPath, + valueField, + pyType, + extProfileInfo, + ); } else { generateGenericExtensionGetter(w, ext, baseName, targetPath, extProfileInfo); generateGenericExtensionSetter(w, ext, className, baseName, targetPath, extProfileInfo); @@ -269,6 +278,9 @@ const generateComplexExtensionSetter = ( extProfileInfo: ExtensionProfileInfo | undefined, ): void => { generateExtensionSetter(w, ext, className, baseName, "dict[str, Any]", targetPath, extProfileInfo, () => { + // In the else branch `value` is the flat dict form (models/profile classes were + // handled above); assert it so field access below type-checks and misuse fails loudly. + w.line("assert is_record(value)"); w.line("sub_extensions = []"); for (const sub of ext.subExtensions ?? []) { const valueField = sub.valueFieldType @@ -322,9 +334,14 @@ const generateSingleValueExtensionSetter = ( baseName: string, targetPath: string[], valueField: string, + pyType: string, extProfileInfo: ExtensionProfileInfo | undefined, ): void => { - generateExtensionSetter(w, ext, className, baseName, "Any", targetPath, extProfileInfo, () => { + // Type the input as the value's own type (symmetric with the getter) instead of `Any`. + generateExtensionSetter(w, ext, className, baseName, pyType, targetPath, extProfileInfo, () => { + // The profile-class arm is handled by isinstance above and the Extension arm by + // is_extension; assert the remainder so the value type-checks and misuse fails loudly. + w.line("assert not isinstance(value, Extension)"); // Wrap in an Extension model so the value field serializes with its FHIR alias. emitExtPush(w, targetPath, `Extension(url=${JSON.stringify(ext.url)}, ${valueField}=value)`); }); @@ -357,6 +374,9 @@ const generateGenericExtensionSetter = ( extProfileInfo: ExtensionProfileInfo | undefined, ): void => { generateExtensionSetter(w, ext, className, baseName, "dict[str, Any]", targetPath, extProfileInfo, () => { + // In the else branch `value` is the flat dict form (models/profile classes were + // handled above); assert it so `**value` type-checks and misuse fails loudly. + w.line("assert is_record(value)"); emitExtPush(w, targetPath, `{"url": ${JSON.stringify(ext.url)}, **value}`); }); }; diff --git a/src/api/writer-generator/python/profile-slices.ts b/src/api/writer-generator/python/profile-slices.ts index 1fde1711..b0d56f17 100644 --- a/src/api/writer-generator/python/profile-slices.ts +++ b/src/api/writer-generator/python/profile-slices.ts @@ -284,15 +284,19 @@ export const generateSliceSetters = ( } else { w.line(`merged = apply_slice_match(${inputExpr}, match)`); } + // Wrap into the element model under a fresh name: `merged` is typed + // `dict[str, Any]`, so rebinding it to a model would trip mypy's assignment check. + let elementExpr = "merged"; if (sliceDef.elementTypeName) { - w.line(`merged = ${sliceDef.elementTypeName}(**merged)`); + w.line(`element = ${sliceDef.elementTypeName}(**merged)`); + elementExpr = "element"; } if (sliceDef.array) { w.line(`items = getattr(self._resource, ${JSON.stringify(fieldName)}, None) or []`); - w.line("set_array_slice(items, match, merged)"); + w.line(`set_array_slice(items, match, ${elementExpr})`); w.line(`setattr(self._resource, ${JSON.stringify(fieldName)}, items)`); } else { - w.line(`setattr(self._resource, ${JSON.stringify(fieldName)}, merged)`); + w.line(`setattr(self._resource, ${JSON.stringify(fieldName)}, ${elementExpr})`); } w.line("return self"); }); diff --git a/src/api/writer-generator/python/profile.ts b/src/api/writer-generator/python/profile.ts index 72a71b98..8b4b943f 100644 --- a/src/api/writer-generator/python/profile.ts +++ b/src/api/writer-generator/python/profile.ts @@ -135,6 +135,12 @@ const collectHelperImports = ( imports.push("_get_key", "is_extension", "get_extension_value", "push_extension"); if (extensions.some((ext) => ext.isComplex && ext.subExtensions)) imports.push("extract_complex_extension"); if (extensions.some((ext) => ext.path.split(".").some((s) => s !== "extension"))) imports.push("ensure_path"); + // Complex/generic (non single-value) extension setters assert `is_record(value)` in the else branch. + const hasDictFormSetter = extensions.some( + (ext) => + (ext.isComplex && ext.subExtensions) || !(ext.valueFieldTypes?.length === 1 && ext.valueFieldTypes[0]), + ); + if (hasDictFormSetter) imports.push("is_record"); } imports.push(...validationHelpers); imports.sort(); From 89c775a9c867ba3c348aa12817c91f851be57354 Mon Sep 17 00:00:00 2001 From: Aleksandr Penskoi Date: Thu, 2 Jul 2026 15:53:41 +0200 Subject: [PATCH 2/3] PY: drop mypy suppressions from python-r4-us-core example, guard tests/demo - mypy.ini: remove disable_error_code and strict_optional = False; the example now type-checks under plain `strict = True` - add None-guards (capture-to-local + assert) at optional-access sites in the tests and demo, without changing any assertion's meaning - load.py: keep Row.gender a str and cast to the gender Literal at the Patient.gender assignment; type make_entry(resource: BaseModel) --- examples/python-r4-us-core/demo.py | 18 +++- examples/python-r4-us-core/mypy.ini | 6 -- examples/python-r4-us-core/test_bundle.py | 8 +- .../test_profile_bodyweight.py | 16 +++- examples/python-r4-us-core/test_profile_bp.py | 47 ++++++++-- .../python-r4-us-core/test_profile_patient.py | 89 +++++++++++++++---- .../test_profile_typed_bundle.py | 16 +++- .../python-r4-us-core/test_raw_extension.py | 14 ++- .../python-r4-us-core/us-core-demo/load.py | 10 ++- 9 files changed, 175 insertions(+), 49 deletions(-) diff --git a/examples/python-r4-us-core/demo.py b/examples/python-r4-us-core/demo.py index bc390305..f456d059 100644 --- a/examples/python-r4-us-core/demo.py +++ b/examples/python-r4-us-core/demo.py @@ -48,7 +48,11 @@ async def main() -> None: patients = await client.resources(Patient).fetch() print(f"\nFound {len(patients)} patients:") for pat in patients: - print(f" - {pat.name[0].family}, {pat.name[0].given[0]}") + name = pat.name + assert name is not None + given = name[0].given + assert given is not None + print(f" - {name[0].family}, {given[0]}") # Search with filters female_patients = await client.resources(Patient).search(gender="female").fetch() @@ -57,16 +61,22 @@ async def main() -> None: # Search and limit results first_patient = await client.resources(Patient).first() if first_patient: - print(f"\nFirst patient: {first_patient.name[0].family}") + first_patient_name = first_patient.name + assert first_patient_name is not None + print(f"\nFirst patient: {first_patient_name[0].family}") # Fetch a single patient by ID fetched_patient = await client.reference("Patient", created_patient.id).to_resource() - print(f"\nFetched patient by ID: {fetched_patient.name[0].family}") + fetched_patient_name = fetched_patient.name + assert fetched_patient_name is not None + print(f"\nFetched patient by ID: {fetched_patient_name[0].family}") # Update a patient created_patient.name = [HumanName(given=["Bob"], family="Updated")] updated_patient = await client.update(created_patient) - print(f"\nUpdated patient family name to: {updated_patient.name[0].family}") + updated_patient_name = updated_patient.name + assert updated_patient_name is not None + print(f"\nUpdated patient family name to: {updated_patient_name[0].family}") # Cleanup await client.delete(f"Patient/{created_patient.id}") diff --git a/examples/python-r4-us-core/mypy.ini b/examples/python-r4-us-core/mypy.ini index 3bed34b4..324b41d7 100644 --- a/examples/python-r4-us-core/mypy.ini +++ b/examples/python-r4-us-core/mypy.ini @@ -1,10 +1,4 @@ [mypy] python_version = 3.13 strict = True -# FHIR resources are modelled as Pydantic objects that also accept/return plain -# dicts, and the demo/test code freely accesses optional fields it knows are set. -# Optional-aware checks and these codes are impractical to satisfy without -# pervasive asserts, so they stay relaxed even under strict. -strict_optional = False -disable_error_code = assignment, return-value, union-attr, dict-item, index plugins = pydantic.mypy diff --git a/examples/python-r4-us-core/test_bundle.py b/examples/python-r4-us-core/test_bundle.py index faa35cff..aef2cb62 100644 --- a/examples/python-r4-us-core/test_bundle.py +++ b/examples/python-r4-us-core/test_bundle.py @@ -31,7 +31,9 @@ def test_bundle_generic_narrows_entry_resources() -> None: def test_bundle_entry_generic_narrows_resource() -> None: patient = Patient(id="p-1") entry: BundleEntry[Patient] = BundleEntry(resource=patient) - assert entry.resource.resourceType == "Patient" + resource = entry.resource + assert resource is not None + assert resource.resourceType == "Patient" def test_bundle_without_type_param_is_backwards_compatible() -> None: @@ -40,7 +42,9 @@ def test_bundle_without_type_param_is_backwards_compatible() -> None: type="collection", entry=[BundleEntry(resource=patient)], ) - assert len(bundle.entry) == 1 + entry = bundle.entry + assert entry is not None + assert len(entry) == 1 def test_bundle_from_json_raises_on_invalid_resource() -> None: diff --git a/examples/python-r4-us-core/test_profile_bodyweight.py b/examples/python-r4-us-core/test_profile_bodyweight.py index d94603e7..8d2253ec 100644 --- a/examples/python-r4-us-core/test_profile_bodyweight.py +++ b/examples/python-r4-us-core/test_profile_bodyweight.py @@ -39,9 +39,13 @@ def test_import_profiled_observation_from_api_and_read_values() -> None: profile = UscoreBodyWeightProfile.from_resource(api_response) assert profile.get_status() == "final" - assert profile.get_value_quantity().value == 75 + q = profile.get_value_quantity() + assert q is not None + assert q.value == 75 assert profile.get_effective_date_time() == "2024-06-15" - assert profile.get_subject().reference == "Patient/pt-1" + subject = profile.get_subject() + assert subject is not None + assert subject.reference == "Patient/pt-1" def test_apply_profile_to_bare_observation_and_populate_it() -> None: @@ -56,7 +60,10 @@ def test_apply_profile_to_bare_observation_and_populate_it() -> None: profile.set_value_quantity(Quantity(value=75, unit="kg", system="http://unitsofmeasure.org", code="kg")) assert profile.validate()["errors"] == [] - assert CANONICAL_URL in profile.to_resource().meta.profile + meta = profile.to_resource().meta + assert meta is not None + assert meta.profile is not None + assert CANONICAL_URL in meta.profile def test_create_builds_a_resource_with_fixed_code_and_required_slice_stubs() -> None: @@ -69,8 +76,11 @@ def test_create_builds_a_resource_with_fixed_code_and_required_slice_stubs() -> profile.set_effective_date_time("2024-01-15") obs = profile.to_resource() + assert obs.code.coding is not None assert obs.code.coding[0].code == "29463-7" + assert obs.valueQuantity is not None assert obs.valueQuantity.value == 70 + assert obs.category is not None assert len(obs.category) == 1 assert profile.validate()["errors"] == [] diff --git a/examples/python-r4-us-core/test_profile_bp.py b/examples/python-r4-us-core/test_profile_bp.py index a11f620d..4b4f337f 100644 --- a/examples/python-r4-us-core/test_profile_bp.py +++ b/examples/python-r4-us-core/test_profile_bp.py @@ -83,7 +83,10 @@ def test_apply_profile_to_bare_observation_and_populate_it() -> None: profile.set_diastolic({"value": 80, "unit": "mmHg"}) assert profile.validate()["errors"] == [] - assert CANONICAL_URL in profile.to_resource().meta.profile + meta = profile.to_resource().meta + assert meta is not None + assert meta.profile is not None + assert CANONICAL_URL in meta.profile # --------------------------------------------------------------------------- @@ -99,9 +102,13 @@ def test_create_auto_sets_code_and_meta_profile() -> None: profile = _make_bp() obs = profile.to_resource() assert obs.resourceType == "Observation" - assert obs.code.coding[0].code == "85354-9" - assert obs.code.coding[0].system == "http://loinc.org" - assert obs.meta.profile == [CANONICAL_URL] + coding = obs.code.coding + assert coding is not None + assert coding[0].code == "85354-9" + assert coding[0].system == "http://loinc.org" + meta = obs.meta + assert meta is not None + assert meta.profile == [CANONICAL_URL] def test_freshly_created_profile_is_not_yet_valid_missing_effective() -> None: @@ -115,6 +122,7 @@ def test_freshly_created_profile_is_not_yet_valid_missing_effective() -> None: def test_create_auto_populates_component_with_systolic_diastolic_stubs() -> None: profile = _make_bp() obs = profile.to_resource() + assert obs.component is not None assert len(obs.component) == 2 @@ -130,8 +138,12 @@ def test_set_systolic_get_systolic_get_systolic_raw() -> None: } raw = profile.get_systolic("raw") + assert raw is not None + assert raw.valueQuantity is not None assert raw.valueQuantity.value == 120 - assert raw.code.coding[0].code == "8480-6" + coding = raw.code.coding + assert coding is not None + assert coding[0].code == "8480-6" def test_set_diastolic_get_diastolic_get_diastolic_raw() -> None: @@ -146,13 +158,18 @@ def test_set_diastolic_get_diastolic_get_diastolic_raw() -> None: } raw = profile.get_diastolic("raw") + assert raw is not None + assert raw.valueQuantity is not None assert raw.valueQuantity.value == 80 - assert raw.code.coding[0].code == "8462-4" + coding = raw.code.coding + assert coding is not None + assert coding[0].code == "8462-4" def test_both_systolic_and_diastolic_are_in_the_component_array() -> None: profile = _make_bp() obs = profile.to_resource() + assert obs.component is not None assert len(obs.component) == 2 @@ -160,14 +177,19 @@ def test_set_systolic_replaces_an_existing_systolic_component() -> None: profile = _make_bp() profile.set_systolic({"value": 130, "unit": "mmHg"}) obs = profile.to_resource() + assert obs.component is not None assert len(obs.component) == 2 - assert profile.get_systolic("raw").valueQuantity.value == 130 + raw = profile.get_systolic("raw") + assert raw is not None + assert raw.valueQuantity is not None + assert raw.valueQuantity.value == 130 def test_set_vscat_adds_category_with_discriminator_values() -> None: profile = _make_bp() profile.set_vscat({"text": "Vital Signs"}) flat = profile.get_vscat() + assert flat is not None assert flat["text"] == "Vital Signs" @@ -188,9 +210,13 @@ def test_fluent_chaining_across_all_accessor_types() -> None: ) assert result is profile assert profile.get_status() == "final" - assert profile.get_vscat()["text"] == "Vital Signs" + vscat = profile.get_vscat() + assert vscat is not None + assert vscat["text"] == "Vital Signs" assert profile.get_effective_date_time() == "2024-06-15" - assert profile.get_subject().reference == "Patient/pt-2" + subject = profile.get_subject() + assert subject is not None + assert subject.reference == "Patient/pt-2" def test_set_systolic_with_no_args_inserts_discriminator_only_component() -> None: @@ -206,6 +232,7 @@ def test_create_with_custom_category_preserves_user_values_and_adds_required_vsc category=[CodeableConcept(text="My Category")], ) obs = custom.to_resource() + assert obs.category is not None assert len(obs.category) == 2 @@ -216,6 +243,7 @@ def test_create_with_empty_category_still_adds_required_vscat() -> None: category=[], ) obs = custom.to_resource() + assert obs.category is not None assert len(obs.category) == 1 @@ -226,4 +254,5 @@ def test_create_with_category_already_containing_vscat_does_not_duplicate_it() - category=[CodeableConcept(coding=[VSCAT_CODING])], ) obs = custom.to_resource() + assert obs.category is not None assert len(obs.category) == 1 diff --git a/examples/python-r4-us-core/test_profile_patient.py b/examples/python-r4-us-core/test_profile_patient.py index dd5eb83e..fb370662 100644 --- a/examples/python-r4-us-core/test_profile_patient.py +++ b/examples/python-r4-us-core/test_profile_patient.py @@ -59,9 +59,13 @@ def test_set_extension_via_flat_input() -> None: res = patient.to_resource() assert res.resourceType == "Patient" + assert res.identifier is not None assert res.identifier[0].value == "MRN-12345" + assert res.name is not None assert res.name[0].family == "Garcia" + assert res.meta is not None assert res.meta.profile == [CANONICAL_URL] + assert res.extension is not None assert len(res.extension) == 3 @@ -84,7 +88,9 @@ def test_set_extension_via_raw_extension() -> None: ) sex_extension = Extension(url=SEX_URL, valueCoding=Coding(code="female", display="Female")) patient.set_sex(sex_extension) - assert patient.get_sex().code == "female" + sex = patient.get_sex() + assert sex is not None + assert sex.code == "female" def test_import_profiled_resource_from_api_and_access_data_via_typed_getters() -> None: @@ -110,12 +116,16 @@ def test_import_profiled_resource_from_api_and_access_data_via_typed_getters() - patient = UscorePatientProfile.from_resource(api_response) + assert api_response.meta is not None + assert api_response.meta.profile is not None assert CANONICAL_URL in api_response.meta.profile names = patient.get_name() + assert names is not None assert names[0].family == "Smith" assert names[0].given == ["John"] race = patient.get_race() + assert race is not None # Pydantic parses the valueCoding sub-extension input into a Coding model, # so race["ombCategory"] is a Coding instance (not a dict like in TS). assert race["ombCategory"].code == "2054-5" @@ -123,6 +133,7 @@ def test_import_profiled_resource_from_api_and_access_data_via_typed_getters() - assert race["detailed"] == [] assert race["text"] == "Black or African American" sex = patient.get_sex() + assert sex is not None assert sex.code == "male" assert patient.get_ethnicity() is None @@ -138,9 +149,13 @@ def test_apply_profile_to_a_bare_resource_and_populate_it() -> None: assert patient.validate()["errors"] == [] res = patient.to_resource() + assert res.identifier is not None assert res.identifier[0].value == "MRN-00001" + assert res.name is not None assert res.name[0].family == "Chen" + assert res.meta is not None assert res.meta.profile == [CANONICAL_URL] + assert res.extension is not None assert len(res.extension) == 2 race_ext = next(e for e in res.extension if (e.get("url") if isinstance(e, dict) else e.url) == RACE_URL) eth_ext = next(e for e in res.extension if (e.get("url") if isinstance(e, dict) else e.url) == ETHNICITY_URL) @@ -161,7 +176,9 @@ def test_create_returns_a_profile_wrapping_the_resource() -> None: res = profile.to_resource() assert res.resourceType == "Patient" + assert res.identifier is not None assert res.identifier[0].value == "12345" + assert res.name is not None assert res.name[0].family == "Smith" @@ -172,6 +189,7 @@ def test_create_resource_returns_a_plain_patient() -> None: ) assert isinstance(res, Patient) assert res.resourceType == "Patient" + assert res.identifier is not None assert res.identifier[0].value == "12345" @@ -183,8 +201,12 @@ def test_apply_wraps_an_existing_patient() -> None: profile.set_name([HumanName(family="Smith", given=["John"])]) assert profile.to_resource() is patient - assert profile.get_identifier()[0].value == "12345" - assert profile.get_name()[0].family == "Smith" + identifier = profile.get_identifier() + assert identifier is not None + assert identifier[0].value == "12345" + name = profile.get_name() + assert name is not None + assert name[0].family == "Smith" def test_all_three_methods_produce_equivalent_resources() -> None: @@ -199,8 +221,11 @@ def test_all_three_methods_produce_equivalent_resources() -> None: from_apply = profile.to_resource() for res in (from_create, from_create_resource, from_apply): + assert res.identifier is not None assert res.identifier[0].value == "12345" + assert res.name is not None assert res.name[0].family == "Smith" + assert res.meta is not None assert res.meta.profile == [CANONICAL_URL] @@ -218,16 +243,24 @@ def _make_patient() -> UscorePatientProfile: def test_get_identifier_set_identifier() -> None: profile = _make_patient() - assert profile.get_identifier()[0].value == "12345" + identifier = profile.get_identifier() + assert identifier is not None + assert identifier[0].value == "12345" profile.set_identifier([Identifier(system="http://hospital.example.org", value="67890")]) - assert profile.get_identifier()[0].value == "67890" + identifier = profile.get_identifier() + assert identifier is not None + assert identifier[0].value == "67890" def test_get_name_set_name() -> None: profile = _make_patient() - assert profile.get_name()[0].family == "Smith" + name = profile.get_name() + assert name is not None + assert name[0].family == "Smith" profile.set_name([HumanName(family="Doe", given=["Jane"])]) - assert profile.get_name()[0].family == "Doe" + name = profile.get_name() + assert name is not None + assert name[0].family == "Doe" def test_fluent_chaining_across_field_accessors() -> None: @@ -237,8 +270,12 @@ def test_fluent_chaining_across_field_accessors() -> None: ).set_name([HumanName(family="Lee")]) assert result is profile - assert profile.get_identifier()[0].value == "AAA" - assert profile.get_name()[0].family == "Lee" + identifier = profile.get_identifier() + assert identifier is not None + assert identifier[0].value == "AAA" + name = profile.get_name() + assert name is not None + assert name[0].family == "Lee" # --------------------------------------------------------------------------- @@ -265,6 +302,7 @@ def test_set_race_get_race_round_trip_with_detailed_categories() -> None: ) race = profile.get_race() + assert race is not None assert race["ombCategory"].code == "2106-3" assert race["text"] == "White European" @@ -289,6 +327,7 @@ def test_set_sex_get_sex_round_trip() -> None: profile.set_sex(Coding(system="http://hl7.org/fhir/administrative-gender", code="male")) sex = profile.get_sex() + assert sex is not None assert sex.code == "male" @@ -300,7 +339,9 @@ def test_get_sex_raw_returns_raw_extension() -> None: profile.set_sex(Coding(system="http://hl7.org/fhir/administrative-gender", code="female")) raw = profile.get_sex("raw") + assert raw is not None assert raw.url == SEX_URL + assert raw.valueCoding is not None assert raw.valueCoding.code == "female" @@ -345,11 +386,21 @@ def test_fluent_chaining_across_extensions() -> None: ) assert result is profile - assert profile.get_race()["text"] == "White" - assert profile.get_ethnicity()["text"] == "Not Hispanic or Latino" - assert profile.get_sex().code == "male" - assert profile.get_tribal_affiliation()["tribalAffiliation"].text == "Navajo" - assert profile.get_interpreter_required().code == "no" + race = profile.get_race() + assert race is not None + assert race["text"] == "White" + ethnicity = profile.get_ethnicity() + assert ethnicity is not None + assert ethnicity["text"] == "Not Hispanic or Latino" + sex = profile.get_sex() + assert sex is not None + assert sex.code == "male" + tribal_affiliation = profile.get_tribal_affiliation() + assert tribal_affiliation is not None + assert tribal_affiliation["tribalAffiliation"].text == "Navajo" + interpreter_required = profile.get_interpreter_required() + assert interpreter_required is not None + assert interpreter_required.code == "no" def test_extensions_are_added_to_the_resource() -> None: @@ -417,7 +468,9 @@ def test_set_sex_accepts_extension_profile_instance() -> None: ) sex_profile = UscoreIndividualSexExtension.create(valueCoding=Coding(code="male")) profile.set_sex(sex_profile) - assert profile.get_sex().code == "male" + sex = profile.get_sex() + assert sex is not None + assert sex.code == "male" def test_set_sex_accepts_raw_extension() -> None: @@ -427,7 +480,9 @@ def test_set_sex_accepts_raw_extension() -> None: ) raw_extension = Extension(url=SEX_URL, valueCoding=Coding(code="female")) profile.set_sex(raw_extension) - assert profile.get_sex().code == "female" + sex = profile.get_sex() + assert sex is not None + assert sex.code == "female" # --------------------------------------------------------------------------- @@ -440,9 +495,11 @@ def test_profile_mutates_the_underlying_resource() -> None: profile = UscorePatientProfile.apply(patient) profile.set_identifier([Identifier(value="123")]) + assert patient.identifier is not None assert patient.identifier[0].value == "123" profile.set_name([HumanName(family="Doe")]) + assert patient.name is not None assert patient.name[0].family == "Doe" diff --git a/examples/python-r4-us-core/test_profile_typed_bundle.py b/examples/python-r4-us-core/test_profile_typed_bundle.py index 20924bc2..9a0e6a05 100644 --- a/examples/python-r4-us-core/test_profile_typed_bundle.py +++ b/examples/python-r4-us-core/test_profile_typed_bundle.py @@ -37,8 +37,12 @@ def test_get_patient_entry_returns_bundle_entry_instance() -> None: entry = bundle.get_patient_entry() assert isinstance(entry, BundleEntry) - assert entry.resource.resourceType == "Patient" - assert entry.resource.name[0].family == "Smith" + resource = entry.resource + assert resource is not None + assert resource.resourceType == "Patient" + name = resource.name + assert name is not None + assert name[0].family == "Smith" def test_get_patient_entry_raw_mode_returns_bundle_entry_instance() -> None: @@ -47,7 +51,9 @@ def test_get_patient_entry_raw_mode_returns_bundle_entry_instance() -> None: entry = bundle.get_patient_entry() assert isinstance(entry, BundleEntry) - assert entry.resource.resourceType == "Patient" + resource = entry.resource + assert resource is not None + assert resource.resourceType == "Patient" def test_get_patient_entry_returns_stored_entry_including_resource() -> None: @@ -67,7 +73,9 @@ def test_set_patient_entry_replaces_existing() -> None: bundle.set_patient_entry(BundleEntry(resource=active_patient)) # Only one patient entry — the second call replaced the first. - assert len(bundle.to_resource().entry) == 1 + entry = bundle.to_resource().entry + assert entry is not None + assert len(entry) == 1 # --------------------------------------------------------------------------- diff --git a/examples/python-r4-us-core/test_raw_extension.py b/examples/python-r4-us-core/test_raw_extension.py index da94d69a..abfe7a13 100644 --- a/examples/python-r4-us-core/test_raw_extension.py +++ b/examples/python-r4-us-core/test_raw_extension.py @@ -135,17 +135,26 @@ def test_read_element_level_extension() -> None: def test_read_primitive_extension() -> None: patient = create_patient_with_extensions() + assert patient.name is not None name = patient.name[0] assert isinstance(name.familyExtension, Element) + assert name.familyExtension.extension is not None assert name.familyExtension.extension[0].valueString == "van" assert isinstance(name.givenExtension, list) - assert name.givenExtension[0].extension[0].valueCode == "birth-certificate" + given0 = name.givenExtension[0] + assert given0 is not None + assert given0.extension is not None + assert given0.extension[0].valueCode == "birth-certificate" assert name.givenExtension[1] is None - assert name.givenExtension[2].extension[0].valueCode == "baptism-record" + given2 = name.givenExtension[2] + assert given2 is not None + assert given2.extension is not None + assert given2.extension[0].valueCode == "baptism-record" assert patient.birthDateExtension is not None assert isinstance(patient.birthDateExtension, Element) + assert patient.birthDateExtension.extension is not None assert patient.birthDateExtension.extension[0].valueDateTime == "1990-03-15T08:22:00-05:00" @@ -161,4 +170,5 @@ def test_primitive_extension_survives_round_trip() -> None: assert restored.birthDateExtension is not None assert isinstance(restored.birthDateExtension, Element) + assert restored.birthDateExtension.extension is not None assert restored.birthDateExtension.extension[0].valueDateTime == "1990-03-15T08:22:00-05:00" diff --git a/examples/python-r4-us-core/us-core-demo/load.py b/examples/python-r4-us-core/us-core-demo/load.py index 3a8bca05..5ec8fce3 100644 --- a/examples/python-r4-us-core/us-core-demo/load.py +++ b/examples/python-r4-us-core/us-core-demo/load.py @@ -7,7 +7,11 @@ import warnings from dataclasses import dataclass from pathlib import Path -from typing import Any, Iterator +from typing import Any, Iterator, Literal, cast + +from pydantic import BaseModel + +Gender = Literal["male", "female", "other", "unknown"] warnings.filterwarnings("ignore", category=UserWarning, module="pydantic") @@ -56,7 +60,7 @@ def row_to_patient(row: Row) -> Patient: identifier=[Identifier(system="http://hospital.example.org/mrn", value=row.mrn)], name=[HumanName(family=row.last, given=[row.first])], ) - resource.gender = row.gender + resource.gender = cast(Gender, row.gender) resource.birthDate = row.dob race = UscoreRaceExtension.create() @@ -96,7 +100,7 @@ def row_to_bp(row: Row, patient_uuid: str) -> Observation: return profile.to_resource() -def make_entry(full_url: str, resource: Any, method: str, url: str) -> dict[str, Any]: +def make_entry(full_url: str, resource: BaseModel, method: str, url: str) -> dict[str, Any]: return { "fullUrl": full_url, "resource": json.loads(resource.model_dump_json(exclude_none=True)), From f1d247435a4262c3aac2ed43e8c9ac87a065b90f Mon Sep 17 00:00:00 2001 From: Aleksandr Penskoi Date: Thu, 2 Jul 2026 15:53:41 +0200 Subject: [PATCH 3/3] PY: regenerate python-r4-us-core example and update generator snapshot --- .../profiles/extension_nationality.py | 6 +- .../observation_observation_bodyweight.py | 4 +- .../observation_observation_vitalsigns.py | 4 +- .../extension_uscore_ethnicity_extension.py | 19 +++--- .../extension_uscore_race_extension.py | 19 +++--- ...ion_uscore_tribal_affiliation_extension.py | 14 +++-- ...servation_uscore_blood_pressure_profile.py | 12 ++-- .../observation_uscore_body_weight_profile.py | 4 +- .../observation_uscore_vital_signs_profile.py | 4 +- .../patient_uscore_patient_profile.py | 11 +++- .../fhir_types/profile_helpers.py | 6 +- .../__snapshots__/python.test.ts.snap | 62 +++++++++++-------- 12 files changed, 95 insertions(+), 70 deletions(-) diff --git a/examples/python-r4-us-core/fhir_types/hl7_fhir_r4_core/profiles/extension_nationality.py b/examples/python-r4-us-core/fhir_types/hl7_fhir_r4_core/profiles/extension_nationality.py index 82158f2c..a218df20 100644 --- a/examples/python-r4-us-core/fhir_types/hl7_fhir_r4_core/profiles/extension_nationality.py +++ b/examples/python-r4-us-core/fhir_types/hl7_fhir_r4_core/profiles/extension_nationality.py @@ -69,12 +69,13 @@ def get_code(self, mode: Literal["raw"] | None = None) -> CodeableConcept | Exte return ext_obj return cast('CodeableConcept | None', get_extension_value(ext, "valueCodeableConcept")) - def set_code(self, value: "Extension | Any") -> "NationalityExtension": + def set_code(self, value: "Extension | CodeableConcept") -> "NationalityExtension": if is_extension(value): if _get_key(value, "url") != "code": raise ValueError(f"Expected extension url 'code', got {_get_key(value, 'url')!r}") push_extension(self._resource, value) else: + assert not isinstance(value, Extension) push_extension(self._resource, Extension(url="code", valueCodeableConcept=value)) return self @@ -92,12 +93,13 @@ def get_period(self, mode: Literal["raw"] | None = None) -> Period | Extension | return ext_obj return cast('Period | None', get_extension_value(ext, "valuePeriod")) - def set_period(self, value: "Extension | Any") -> "NationalityExtension": + def set_period(self, value: "Extension | Period") -> "NationalityExtension": if is_extension(value): if _get_key(value, "url") != "period": raise ValueError(f"Expected extension url 'period', got {_get_key(value, 'url')!r}") push_extension(self._resource, value) else: + assert not isinstance(value, Extension) push_extension(self._resource, Extension(url="period", valuePeriod=value)) return self diff --git a/examples/python-r4-us-core/fhir_types/hl7_fhir_r4_core/profiles/observation_observation_bodyweight.py b/examples/python-r4-us-core/fhir_types/hl7_fhir_r4_core/profiles/observation_observation_bodyweight.py index cb11db7d..05054783 100644 --- a/examples/python-r4-us-core/fhir_types/hl7_fhir_r4_core/profiles/observation_observation_bodyweight.py +++ b/examples/python-r4-us-core/fhir_types/hl7_fhir_r4_core/profiles/observation_observation_bodyweight.py @@ -132,9 +132,9 @@ def get_vscat(self, mode: Literal["raw"] | None = None) -> dict[str, Any] | Code def set_vscat(self, value: dict[str, Any] | None = None) -> "ObservationBodyweightProfile": match = self.__class__._vscat_slice_match merged = apply_slice_match((value or {}), match) - merged = CodeableConcept(**merged) + element = CodeableConcept(**merged) items = getattr(self._resource, "category", None) or [] - set_array_slice(items, match, merged) + set_array_slice(items, match, element) setattr(self._resource, "category", items) return self diff --git a/examples/python-r4-us-core/fhir_types/hl7_fhir_r4_core/profiles/observation_observation_vitalsigns.py b/examples/python-r4-us-core/fhir_types/hl7_fhir_r4_core/profiles/observation_observation_vitalsigns.py index 187f57df..b9c29cd0 100644 --- a/examples/python-r4-us-core/fhir_types/hl7_fhir_r4_core/profiles/observation_observation_vitalsigns.py +++ b/examples/python-r4-us-core/fhir_types/hl7_fhir_r4_core/profiles/observation_observation_vitalsigns.py @@ -125,9 +125,9 @@ def get_vscat(self, mode: Literal["raw"] | None = None) -> dict[str, Any] | Code def set_vscat(self, value: dict[str, Any] | None = None) -> "ObservationVitalsignsProfile": match = self.__class__._vscat_slice_match merged = apply_slice_match((value or {}), match) - merged = CodeableConcept(**merged) + element = CodeableConcept(**merged) items = getattr(self._resource, "category", None) or [] - set_array_slice(items, match, merged) + set_array_slice(items, match, element) setattr(self._resource, "category", items) return self diff --git a/examples/python-r4-us-core/fhir_types/hl7_fhir_us_core/profiles/extension_uscore_ethnicity_extension.py b/examples/python-r4-us-core/fhir_types/hl7_fhir_us_core/profiles/extension_uscore_ethnicity_extension.py index 22da17ea..45993ecb 100644 --- a/examples/python-r4-us-core/fhir_types/hl7_fhir_us_core/profiles/extension_uscore_ethnicity_extension.py +++ b/examples/python-r4-us-core/fhir_types/hl7_fhir_us_core/profiles/extension_uscore_ethnicity_extension.py @@ -10,8 +10,8 @@ from fhir_types.hl7_fhir_r4_core.base import Extension from fhir_types.profile_helpers import ( _get_key, apply_slice_match, build_resource, ensure_slice_defaults, get_array_slice, get_extension_value, \ - is_extension, matches_value, push_extension, set_array_slice, strip_match_keys, unwrap_slice_choice, validate_fixed_value, \ - validate_required, validate_slice_cardinality, wrap_slice_choice + is_extension, is_record, matches_value, push_extension, set_array_slice, strip_match_keys, unwrap_slice_choice, \ + validate_fixed_value, validate_required, validate_slice_cardinality, wrap_slice_choice ) @@ -92,6 +92,7 @@ def set_omb_category(self, value: "Extension | dict[str, Any]") -> "UscoreEthnic raise ValueError(f"Expected extension url 'ombCategory', got {_get_key(value, 'url')!r}") push_extension(self._resource, value) else: + assert is_record(value) push_extension(self._resource, {"url": "ombCategory", **value}) return self @@ -115,6 +116,7 @@ def set_detailed(self, value: "Extension | dict[str, Any]") -> "UscoreEthnicityE raise ValueError(f"Expected extension url 'detailed', got {_get_key(value, 'url')!r}") push_extension(self._resource, value) else: + assert is_record(value) push_extension(self._resource, {"url": "detailed", **value}) return self @@ -138,6 +140,7 @@ def set_text(self, value: "Extension | dict[str, Any]") -> "UscoreEthnicityExten raise ValueError(f"Expected extension url 'text', got {_get_key(value, 'url')!r}") push_extension(self._resource, value) else: + assert is_record(value) push_extension(self._resource, {"url": "text", **value}) return self @@ -181,9 +184,9 @@ def set_extension_omb_category(self, value: dict[str, Any] | None = None) -> "Us match = self.__class__._omb_category_slice_match wrapped = wrap_slice_choice((value or {}), "valueCoding") merged = apply_slice_match(wrapped, match) - merged = Extension(**merged) + element = Extension(**merged) items = getattr(self._resource, "extension", None) or [] - set_array_slice(items, match, merged) + set_array_slice(items, match, element) setattr(self._resource, "extension", items) return self @@ -191,18 +194,18 @@ def set_extension_detailed(self, value: dict[str, Any] | None = None) -> "Uscore match = self.__class__._detailed_slice_match wrapped = wrap_slice_choice((value or {}), "valueCoding") merged = apply_slice_match(wrapped, match) - merged = Extension(**merged) + element = Extension(**merged) items = getattr(self._resource, "extension", None) or [] - set_array_slice(items, match, merged) + set_array_slice(items, match, element) setattr(self._resource, "extension", items) return self def set_extension_text(self, value: dict[str, Any] | None = None) -> "UscoreEthnicityExtension": match = self.__class__._text_slice_match merged = apply_slice_match((value or {}), match) - merged = Extension(**merged) + element = Extension(**merged) items = getattr(self._resource, "extension", None) or [] - set_array_slice(items, match, merged) + set_array_slice(items, match, element) setattr(self._resource, "extension", items) return self diff --git a/examples/python-r4-us-core/fhir_types/hl7_fhir_us_core/profiles/extension_uscore_race_extension.py b/examples/python-r4-us-core/fhir_types/hl7_fhir_us_core/profiles/extension_uscore_race_extension.py index af969361..06ee4abe 100644 --- a/examples/python-r4-us-core/fhir_types/hl7_fhir_us_core/profiles/extension_uscore_race_extension.py +++ b/examples/python-r4-us-core/fhir_types/hl7_fhir_us_core/profiles/extension_uscore_race_extension.py @@ -10,8 +10,8 @@ from fhir_types.hl7_fhir_r4_core.base import Extension from fhir_types.profile_helpers import ( _get_key, apply_slice_match, build_resource, ensure_slice_defaults, get_array_slice, get_extension_value, \ - is_extension, matches_value, push_extension, set_array_slice, strip_match_keys, unwrap_slice_choice, validate_fixed_value, \ - validate_required, validate_slice_cardinality, wrap_slice_choice + is_extension, is_record, matches_value, push_extension, set_array_slice, strip_match_keys, unwrap_slice_choice, \ + validate_fixed_value, validate_required, validate_slice_cardinality, wrap_slice_choice ) @@ -95,6 +95,7 @@ def set_omb_category(self, value: "Extension | dict[str, Any]") -> "UscoreRaceEx raise ValueError(f"Expected extension url 'ombCategory', got {_get_key(value, 'url')!r}") push_extension(self._resource, value) else: + assert is_record(value) push_extension(self._resource, {"url": "ombCategory", **value}) return self @@ -118,6 +119,7 @@ def set_detailed(self, value: "Extension | dict[str, Any]") -> "UscoreRaceExtens raise ValueError(f"Expected extension url 'detailed', got {_get_key(value, 'url')!r}") push_extension(self._resource, value) else: + assert is_record(value) push_extension(self._resource, {"url": "detailed", **value}) return self @@ -141,6 +143,7 @@ def set_text(self, value: "Extension | dict[str, Any]") -> "UscoreRaceExtension" raise ValueError(f"Expected extension url 'text', got {_get_key(value, 'url')!r}") push_extension(self._resource, value) else: + assert is_record(value) push_extension(self._resource, {"url": "text", **value}) return self @@ -184,9 +187,9 @@ def set_extension_omb_category(self, value: dict[str, Any] | None = None) -> "Us match = self.__class__._omb_category_slice_match wrapped = wrap_slice_choice((value or {}), "valueCoding") merged = apply_slice_match(wrapped, match) - merged = Extension(**merged) + element = Extension(**merged) items = getattr(self._resource, "extension", None) or [] - set_array_slice(items, match, merged) + set_array_slice(items, match, element) setattr(self._resource, "extension", items) return self @@ -194,18 +197,18 @@ def set_extension_detailed(self, value: dict[str, Any] | None = None) -> "Uscore match = self.__class__._detailed_slice_match wrapped = wrap_slice_choice((value or {}), "valueCoding") merged = apply_slice_match(wrapped, match) - merged = Extension(**merged) + element = Extension(**merged) items = getattr(self._resource, "extension", None) or [] - set_array_slice(items, match, merged) + set_array_slice(items, match, element) setattr(self._resource, "extension", items) return self def set_extension_text(self, value: dict[str, Any] | None = None) -> "UscoreRaceExtension": match = self.__class__._text_slice_match merged = apply_slice_match((value or {}), match) - merged = Extension(**merged) + element = Extension(**merged) items = getattr(self._resource, "extension", None) or [] - set_array_slice(items, match, merged) + set_array_slice(items, match, element) setattr(self._resource, "extension", items) return self diff --git a/examples/python-r4-us-core/fhir_types/hl7_fhir_us_core/profiles/extension_uscore_tribal_affiliation_extension.py b/examples/python-r4-us-core/fhir_types/hl7_fhir_us_core/profiles/extension_uscore_tribal_affiliation_extension.py index af4174e3..fc418b50 100644 --- a/examples/python-r4-us-core/fhir_types/hl7_fhir_us_core/profiles/extension_uscore_tribal_affiliation_extension.py +++ b/examples/python-r4-us-core/fhir_types/hl7_fhir_us_core/profiles/extension_uscore_tribal_affiliation_extension.py @@ -10,8 +10,8 @@ from fhir_types.hl7_fhir_r4_core.base import Extension from fhir_types.profile_helpers import ( _get_key, apply_slice_match, build_resource, ensure_slice_defaults, get_array_slice, get_extension_value, \ - is_extension, matches_value, push_extension, set_array_slice, strip_match_keys, unwrap_slice_choice, validate_fixed_value, \ - validate_required, validate_slice_cardinality, wrap_slice_choice + is_extension, is_record, matches_value, push_extension, set_array_slice, strip_match_keys, unwrap_slice_choice, \ + validate_fixed_value, validate_required, validate_slice_cardinality, wrap_slice_choice ) @@ -88,6 +88,7 @@ def set_tribal_affiliation(self, value: "Extension | dict[str, Any]") -> "Uscore raise ValueError(f"Expected extension url 'tribalAffiliation', got {_get_key(value, 'url')!r}") push_extension(self._resource, value) else: + assert is_record(value) push_extension(self._resource, {"url": "tribalAffiliation", **value}) return self @@ -111,6 +112,7 @@ def set_is_enrolled(self, value: "Extension | dict[str, Any]") -> "UscoreTribalA raise ValueError(f"Expected extension url 'isEnrolled', got {_get_key(value, 'url')!r}") push_extension(self._resource, value) else: + assert is_record(value) push_extension(self._resource, {"url": "isEnrolled", **value}) return self @@ -142,18 +144,18 @@ def set_extension_tribal_affiliation(self, value: dict[str, Any] | None = None) match = self.__class__._tribal_affiliation_slice_match wrapped = wrap_slice_choice((value or {}), "valueCodeableConcept") merged = apply_slice_match(wrapped, match) - merged = Extension(**merged) + element = Extension(**merged) items = getattr(self._resource, "extension", None) or [] - set_array_slice(items, match, merged) + set_array_slice(items, match, element) setattr(self._resource, "extension", items) return self def set_extension_is_enrolled(self, value: dict[str, Any] | None = None) -> "UscoreTribalAffiliationExtension": match = self.__class__._is_enrolled_slice_match merged = apply_slice_match((value or {}), match) - merged = Extension(**merged) + element = Extension(**merged) items = getattr(self._resource, "extension", None) or [] - set_array_slice(items, match, merged) + set_array_slice(items, match, element) setattr(self._resource, "extension", items) return self diff --git a/examples/python-r4-us-core/fhir_types/hl7_fhir_us_core/profiles/observation_uscore_blood_pressure_profile.py b/examples/python-r4-us-core/fhir_types/hl7_fhir_us_core/profiles/observation_uscore_blood_pressure_profile.py index 6a3eb8e7..4f810582 100644 --- a/examples/python-r4-us-core/fhir_types/hl7_fhir_us_core/profiles/observation_uscore_blood_pressure_profile.py +++ b/examples/python-r4-us-core/fhir_types/hl7_fhir_us_core/profiles/observation_uscore_blood_pressure_profile.py @@ -354,9 +354,9 @@ def get_diastolic(self, mode: Literal["raw"] | None = None) -> dict[str, Any] | def set_vscat(self, value: dict[str, Any] | None = None) -> "UscoreBloodPressureProfile": match = self.__class__._vscat_slice_match merged = apply_slice_match((value or {}), match) - merged = CodeableConcept(**merged) + element = CodeableConcept(**merged) items = getattr(self._resource, "category", None) or [] - set_array_slice(items, match, merged) + set_array_slice(items, match, element) setattr(self._resource, "category", items) return self @@ -364,9 +364,9 @@ def set_systolic(self, value: dict[str, Any] | None = None) -> "UscoreBloodPress match = self.__class__._systolic_slice_match wrapped = wrap_slice_choice((value or {}), "valueQuantity") merged = apply_slice_match(wrapped, match) - merged = ObservationComponent(**merged) + element = ObservationComponent(**merged) items = getattr(self._resource, "component", None) or [] - set_array_slice(items, match, merged) + set_array_slice(items, match, element) setattr(self._resource, "component", items) return self @@ -374,9 +374,9 @@ def set_diastolic(self, value: dict[str, Any] | None = None) -> "UscoreBloodPres match = self.__class__._diastolic_slice_match wrapped = wrap_slice_choice((value or {}), "valueQuantity") merged = apply_slice_match(wrapped, match) - merged = ObservationComponent(**merged) + element = ObservationComponent(**merged) items = getattr(self._resource, "component", None) or [] - set_array_slice(items, match, merged) + set_array_slice(items, match, element) setattr(self._resource, "component", items) return self diff --git a/examples/python-r4-us-core/fhir_types/hl7_fhir_us_core/profiles/observation_uscore_body_weight_profile.py b/examples/python-r4-us-core/fhir_types/hl7_fhir_us_core/profiles/observation_uscore_body_weight_profile.py index 55014ad3..f67d63dc 100644 --- a/examples/python-r4-us-core/fhir_types/hl7_fhir_us_core/profiles/observation_uscore_body_weight_profile.py +++ b/examples/python-r4-us-core/fhir_types/hl7_fhir_us_core/profiles/observation_uscore_body_weight_profile.py @@ -204,9 +204,9 @@ def get_vscat(self, mode: Literal["raw"] | None = None) -> dict[str, Any] | Code def set_vscat(self, value: dict[str, Any] | None = None) -> "UscoreBodyWeightProfile": match = self.__class__._vscat_slice_match merged = apply_slice_match((value or {}), match) - merged = CodeableConcept(**merged) + element = CodeableConcept(**merged) items = getattr(self._resource, "category", None) or [] - set_array_slice(items, match, merged) + set_array_slice(items, match, element) setattr(self._resource, "category", items) return self diff --git a/examples/python-r4-us-core/fhir_types/hl7_fhir_us_core/profiles/observation_uscore_vital_signs_profile.py b/examples/python-r4-us-core/fhir_types/hl7_fhir_us_core/profiles/observation_uscore_vital_signs_profile.py index d3ca6b80..7702ff04 100644 --- a/examples/python-r4-us-core/fhir_types/hl7_fhir_us_core/profiles/observation_uscore_vital_signs_profile.py +++ b/examples/python-r4-us-core/fhir_types/hl7_fhir_us_core/profiles/observation_uscore_vital_signs_profile.py @@ -314,9 +314,9 @@ def get_vscat(self, mode: Literal["raw"] | None = None) -> dict[str, Any] | Code def set_vscat(self, value: dict[str, Any] | None = None) -> "UscoreVitalSignsProfile": match = self.__class__._vscat_slice_match merged = apply_slice_match((value or {}), match) - merged = CodeableConcept(**merged) + element = CodeableConcept(**merged) items = getattr(self._resource, "category", None) or [] - set_array_slice(items, match, merged) + set_array_slice(items, match, element) setattr(self._resource, "category", items) return self diff --git a/examples/python-r4-us-core/fhir_types/hl7_fhir_us_core/profiles/patient_uscore_patient_profile.py b/examples/python-r4-us-core/fhir_types/hl7_fhir_us_core/profiles/patient_uscore_patient_profile.py index 359b8b42..07dc97a7 100644 --- a/examples/python-r4-us-core/fhir_types/hl7_fhir_us_core/profiles/patient_uscore_patient_profile.py +++ b/examples/python-r4-us-core/fhir_types/hl7_fhir_us_core/profiles/patient_uscore_patient_profile.py @@ -16,7 +16,7 @@ from .extension_uscore_tribal_affiliation_extension import UscoreTribalAffiliationExtension from fhir_types.profile_helpers import ( _get_key, build_resource, ensure_profile, extract_complex_extension, get_extension_value, is_extension, \ - push_extension, validate_must_support, validate_required + is_record, push_extension, validate_must_support, validate_required ) @@ -106,6 +106,7 @@ def set_race(self, value: "UscoreRaceExtension | Extension | dict[str, Any]") -> raise ValueError(f"Expected extension url 'http://hl7.org/fhir/us/core/StructureDefinition/us-core-race', got {_get_key(value, 'url')!r}") push_extension(self._resource, value) else: + assert is_record(value) sub_extensions = [] if value.get("ombCategory") is not None: sub_extensions.append({"url": "ombCategory", "valueCoding": value["ombCategory"]}) @@ -143,6 +144,7 @@ def set_ethnicity(self, value: "UscoreEthnicityExtension | Extension | dict[str, raise ValueError(f"Expected extension url 'http://hl7.org/fhir/us/core/StructureDefinition/us-core-ethnicity', got {_get_key(value, 'url')!r}") push_extension(self._resource, value) else: + assert is_record(value) sub_extensions = [] if value.get("ombCategory") is not None: sub_extensions.append({"url": "ombCategory", "valueCoding": value["ombCategory"]}) @@ -180,6 +182,7 @@ def set_tribal_affiliation(self, value: "UscoreTribalAffiliationExtension | Exte raise ValueError(f"Expected extension url 'http://hl7.org/fhir/us/core/StructureDefinition/us-core-tribal-affiliation', got {_get_key(value, 'url')!r}") push_extension(self._resource, value) else: + assert is_record(value) sub_extensions = [] if value.get("tribalAffiliation") is not None: sub_extensions.append({"url": "tribalAffiliation", "valueCodeableConcept": value["tribalAffiliation"]}) @@ -206,7 +209,7 @@ def get_sex(self, mode: Literal["raw", "profile"] | None = None) -> Coding | Ext return UscoreIndividualSexExtension.apply(ext_obj) return cast('Coding | None', get_extension_value(ext, "valueCoding")) - def set_sex(self, value: "UscoreIndividualSexExtension | Extension | Any") -> "UscorePatientProfile": + def set_sex(self, value: "UscoreIndividualSexExtension | Extension | Coding") -> "UscorePatientProfile": if isinstance(value, UscoreIndividualSexExtension): push_extension(self._resource, value.to_resource()) elif is_extension(value): @@ -214,6 +217,7 @@ def set_sex(self, value: "UscoreIndividualSexExtension | Extension | Any") -> "U raise ValueError(f"Expected extension url 'http://hl7.org/fhir/us/core/StructureDefinition/us-core-individual-sex', got {_get_key(value, 'url')!r}") push_extension(self._resource, value) else: + assert not isinstance(value, Extension) push_extension(self._resource, Extension(url="http://hl7.org/fhir/us/core/StructureDefinition/us-core-individual-sex", valueCoding=value)) return self @@ -235,7 +239,7 @@ def get_interpreter_required(self, mode: Literal["raw", "profile"] | None = None return UscoreInterpreterNeededExtension.apply(ext_obj) return cast('Coding | None', get_extension_value(ext, "valueCoding")) - def set_interpreter_required(self, value: "UscoreInterpreterNeededExtension | Extension | Any") -> "UscorePatientProfile": + def set_interpreter_required(self, value: "UscoreInterpreterNeededExtension | Extension | Coding") -> "UscorePatientProfile": if isinstance(value, UscoreInterpreterNeededExtension): push_extension(self._resource, value.to_resource()) elif is_extension(value): @@ -243,6 +247,7 @@ def set_interpreter_required(self, value: "UscoreInterpreterNeededExtension | Ex raise ValueError(f"Expected extension url 'http://hl7.org/fhir/us/core/StructureDefinition/us-core-interpreter-needed', got {_get_key(value, 'url')!r}") push_extension(self._resource, value) else: + assert not isinstance(value, Extension) push_extension(self._resource, Extension(url="http://hl7.org/fhir/us/core/StructureDefinition/us-core-interpreter-needed", valueCoding=value)) return self diff --git a/examples/python-r4-us-core/fhir_types/profile_helpers.py b/examples/python-r4-us-core/fhir_types/profile_helpers.py index dce9d91c..3be6d93a 100644 --- a/examples/python-r4-us-core/fhir_types/profile_helpers.py +++ b/examples/python-r4-us-core/fhir_types/profile_helpers.py @@ -26,6 +26,8 @@ import copy from typing import Any, Iterable, Mapping, MutableMapping, MutableSequence, Sequence, TypeVar +from typing_extensions import TypeGuard + T = TypeVar("T") # --------------------------------------------------------------------------- @@ -33,9 +35,9 @@ # --------------------------------------------------------------------------- -def is_record(value: Any) -> bool: +def is_record(value: Any) -> TypeGuard[MutableMapping[str, Any]]: """True when ``value`` is a non-None mapping (dict-like, not a list).""" - return isinstance(value, Mapping) + return isinstance(value, MutableMapping) def ensure_path(root: MutableMapping[str, Any], path: Sequence[str]) -> MutableMapping[str, Any]: diff --git a/test/api/write-generator/__snapshots__/python.test.ts.snap b/test/api/write-generator/__snapshots__/python.test.ts.snap index 9f62ec61..06665095 100644 --- a/test/api/write-generator/__snapshots__/python.test.ts.snap +++ b/test/api/write-generator/__snapshots__/python.test.ts.snap @@ -545,9 +545,9 @@ class ObservationBodyweightProfile: def set_vscat(self, value: dict[str, Any] | None = None) -> "ObservationBodyweightProfile": match = self.__class__._vscat_slice_match merged = apply_slice_match((value or {}), match) - merged = CodeableConcept(**merged) + element = CodeableConcept(**merged) items = getattr(self._resource, "category", None) or [] - set_array_slice(items, match, merged) + set_array_slice(items, match, element) setattr(self._resource, "category", items) return self @@ -752,9 +752,9 @@ class ObservationBpProfile: def set_vscat(self, value: dict[str, Any] | None = None) -> "ObservationBpProfile": match = self.__class__._vscat_slice_match merged = apply_slice_match((value or {}), match) - merged = CodeableConcept(**merged) + element = CodeableConcept(**merged) items = getattr(self._resource, "category", None) or [] - set_array_slice(items, match, merged) + set_array_slice(items, match, element) setattr(self._resource, "category", items) return self @@ -762,9 +762,9 @@ class ObservationBpProfile: match = self.__class__._systolic_bp_slice_match wrapped = wrap_slice_choice((value or {}), "valueQuantity") merged = apply_slice_match(wrapped, match) - merged = ObservationComponent(**merged) + element = ObservationComponent(**merged) items = getattr(self._resource, "component", None) or [] - set_array_slice(items, match, merged) + set_array_slice(items, match, element) setattr(self._resource, "component", items) return self @@ -772,9 +772,9 @@ class ObservationBpProfile: match = self.__class__._diastolic_bp_slice_match wrapped = wrap_slice_choice((value or {}), "valueQuantity") merged = apply_slice_match(wrapped, match) - merged = ObservationComponent(**merged) + element = ObservationComponent(**merged) items = getattr(self._resource, "component", None) or [] - set_array_slice(items, match, merged) + set_array_slice(items, match, element) setattr(self._resource, "component", items) return self @@ -823,7 +823,7 @@ from .extension_uscore_race_extension import UscoreRaceExtension from .extension_uscore_tribal_affiliation_extension import UscoreTribalAffiliationExtension from fhir_types.profile_helpers import ( _get_key, build_resource, ensure_profile, extract_complex_extension, get_extension_value, is_extension, \\ - push_extension, validate_must_support, validate_required + is_record, push_extension, validate_must_support, validate_required ) @@ -913,6 +913,7 @@ class UscorePatientProfile: raise ValueError(f"Expected extension url 'http://hl7.org/fhir/us/core/StructureDefinition/us-core-race', got {_get_key(value, 'url')!r}") push_extension(self._resource, value) else: + assert is_record(value) sub_extensions = [] if value.get("ombCategory") is not None: sub_extensions.append({"url": "ombCategory", "valueCoding": value["ombCategory"]}) @@ -950,6 +951,7 @@ class UscorePatientProfile: raise ValueError(f"Expected extension url 'http://hl7.org/fhir/us/core/StructureDefinition/us-core-ethnicity', got {_get_key(value, 'url')!r}") push_extension(self._resource, value) else: + assert is_record(value) sub_extensions = [] if value.get("ombCategory") is not None: sub_extensions.append({"url": "ombCategory", "valueCoding": value["ombCategory"]}) @@ -987,6 +989,7 @@ class UscorePatientProfile: raise ValueError(f"Expected extension url 'http://hl7.org/fhir/us/core/StructureDefinition/us-core-tribal-affiliation', got {_get_key(value, 'url')!r}") push_extension(self._resource, value) else: + assert is_record(value) sub_extensions = [] if value.get("tribalAffiliation") is not None: sub_extensions.append({"url": "tribalAffiliation", "valueCodeableConcept": value["tribalAffiliation"]}) @@ -1013,7 +1016,7 @@ class UscorePatientProfile: return UscoreIndividualSexExtension.apply(ext_obj) return cast('Coding | None', get_extension_value(ext, "valueCoding")) - def set_sex(self, value: "UscoreIndividualSexExtension | Extension | Any") -> "UscorePatientProfile": + def set_sex(self, value: "UscoreIndividualSexExtension | Extension | Coding") -> "UscorePatientProfile": if isinstance(value, UscoreIndividualSexExtension): push_extension(self._resource, value.to_resource()) elif is_extension(value): @@ -1021,6 +1024,7 @@ class UscorePatientProfile: raise ValueError(f"Expected extension url 'http://hl7.org/fhir/us/core/StructureDefinition/us-core-individual-sex', got {_get_key(value, 'url')!r}") push_extension(self._resource, value) else: + assert not isinstance(value, Extension) push_extension(self._resource, Extension(url="http://hl7.org/fhir/us/core/StructureDefinition/us-core-individual-sex", valueCoding=value)) return self @@ -1042,7 +1046,7 @@ class UscorePatientProfile: return UscoreInterpreterNeededExtension.apply(ext_obj) return cast('Coding | None', get_extension_value(ext, "valueCoding")) - def set_interpreter_required(self, value: "UscoreInterpreterNeededExtension | Extension | Any") -> "UscorePatientProfile": + def set_interpreter_required(self, value: "UscoreInterpreterNeededExtension | Extension | Coding") -> "UscorePatientProfile": if isinstance(value, UscoreInterpreterNeededExtension): push_extension(self._resource, value.to_resource()) elif is_extension(value): @@ -1050,6 +1054,7 @@ class UscorePatientProfile: raise ValueError(f"Expected extension url 'http://hl7.org/fhir/us/core/StructureDefinition/us-core-interpreter-needed', got {_get_key(value, 'url')!r}") push_extension(self._resource, value) else: + assert not isinstance(value, Extension) push_extension(self._resource, Extension(url="http://hl7.org/fhir/us/core/StructureDefinition/us-core-interpreter-needed", valueCoding=value)) return self @@ -1423,9 +1428,9 @@ class UscoreBloodPressureProfile: def set_vscat(self, value: dict[str, Any] | None = None) -> "UscoreBloodPressureProfile": match = self.__class__._vscat_slice_match merged = apply_slice_match((value or {}), match) - merged = CodeableConcept(**merged) + element = CodeableConcept(**merged) items = getattr(self._resource, "category", None) or [] - set_array_slice(items, match, merged) + set_array_slice(items, match, element) setattr(self._resource, "category", items) return self @@ -1433,9 +1438,9 @@ class UscoreBloodPressureProfile: match = self.__class__._systolic_slice_match wrapped = wrap_slice_choice((value or {}), "valueQuantity") merged = apply_slice_match(wrapped, match) - merged = ObservationComponent(**merged) + element = ObservationComponent(**merged) items = getattr(self._resource, "component", None) or [] - set_array_slice(items, match, merged) + set_array_slice(items, match, element) setattr(self._resource, "component", items) return self @@ -1443,9 +1448,9 @@ class UscoreBloodPressureProfile: match = self.__class__._diastolic_slice_match wrapped = wrap_slice_choice((value or {}), "valueQuantity") merged = apply_slice_match(wrapped, match) - merged = ObservationComponent(**merged) + element = ObservationComponent(**merged) items = getattr(self._resource, "component", None) or [] - set_array_slice(items, match, merged) + set_array_slice(items, match, element) setattr(self._resource, "component", items) return self @@ -1684,9 +1689,9 @@ class UscoreBodyWeightProfile: def set_vscat(self, value: dict[str, Any] | None = None) -> "UscoreBodyWeightProfile": match = self.__class__._vscat_slice_match merged = apply_slice_match((value or {}), match) - merged = CodeableConcept(**merged) + element = CodeableConcept(**merged) items = getattr(self._resource, "category", None) or [] - set_array_slice(items, match, merged) + set_array_slice(items, match, element) setattr(self._resource, "category", items) return self @@ -1739,8 +1744,8 @@ from fhir_types.hl7_fhir_r4_core.base import Extension from fhir_types.hl7_fhir_r4_core.base import Extension from fhir_types.profile_helpers import ( _get_key, apply_slice_match, build_resource, ensure_slice_defaults, get_array_slice, get_extension_value, \\ - is_extension, matches_value, push_extension, set_array_slice, strip_match_keys, unwrap_slice_choice, validate_fixed_value, \\ - validate_required, validate_slice_cardinality, wrap_slice_choice + is_extension, is_record, matches_value, push_extension, set_array_slice, strip_match_keys, unwrap_slice_choice, \\ + validate_fixed_value, validate_required, validate_slice_cardinality, wrap_slice_choice ) @@ -1824,6 +1829,7 @@ class UscoreRaceExtension: raise ValueError(f"Expected extension url 'ombCategory', got {_get_key(value, 'url')!r}") push_extension(self._resource, value) else: + assert is_record(value) push_extension(self._resource, {"url": "ombCategory", **value}) return self @@ -1847,6 +1853,7 @@ class UscoreRaceExtension: raise ValueError(f"Expected extension url 'detailed', got {_get_key(value, 'url')!r}") push_extension(self._resource, value) else: + assert is_record(value) push_extension(self._resource, {"url": "detailed", **value}) return self @@ -1870,6 +1877,7 @@ class UscoreRaceExtension: raise ValueError(f"Expected extension url 'text', got {_get_key(value, 'url')!r}") push_extension(self._resource, value) else: + assert is_record(value) push_extension(self._resource, {"url": "text", **value}) return self @@ -1913,9 +1921,9 @@ class UscoreRaceExtension: match = self.__class__._omb_category_slice_match wrapped = wrap_slice_choice((value or {}), "valueCoding") merged = apply_slice_match(wrapped, match) - merged = Extension(**merged) + element = Extension(**merged) items = getattr(self._resource, "extension", None) or [] - set_array_slice(items, match, merged) + set_array_slice(items, match, element) setattr(self._resource, "extension", items) return self @@ -1923,18 +1931,18 @@ class UscoreRaceExtension: match = self.__class__._detailed_slice_match wrapped = wrap_slice_choice((value or {}), "valueCoding") merged = apply_slice_match(wrapped, match) - merged = Extension(**merged) + element = Extension(**merged) items = getattr(self._resource, "extension", None) or [] - set_array_slice(items, match, merged) + set_array_slice(items, match, element) setattr(self._resource, "extension", items) return self def set_extension_text(self, value: dict[str, Any] | None = None) -> "UscoreRaceExtension": match = self.__class__._text_slice_match merged = apply_slice_match((value or {}), match) - merged = Extension(**merged) + element = Extension(**merged) items = getattr(self._resource, "extension", None) or [] - set_array_slice(items, match, merged) + set_array_slice(items, match, element) setattr(self._resource, "extension", items) return self