From 4cf24ff93bcffab37d821ad8366adf8abc4d56db Mon Sep 17 00:00:00 2001 From: Michael Terry Date: Fri, 12 Jul 2024 12:51:23 -0400 Subject: [PATCH] Add more CI testing and fix R4 generation - Add CI generation test for R5 - Add CI unit test for R4 - Fix a generation bug that wrote out bogus enum classes like AccountStatus.str instead of just str - Fix an R5 generation bug by adding enum mappings for + and - - Change the default unit test filename pattern from _tests.py to _test.py so that pytest automatically finds the files - Fix a unit-test generation bug that didn't properly escape backslashes in strings - Generate models/__init__.py automatically so that all the relative importing the models do works out of the box R5 unit tests still don't pass with this commit, but that can be left to a future effort. R4 is still the default generation mode. --- .github/workflows/ci.yaml | 41 +++++++++++++++++++++++++++++++++++-- Default/mappings.py | 2 ++ Default/settings.py | 2 +- README.md | 4 ++-- Sample/template-unittest.py | 2 +- fhirspec.py | 8 ++++++-- 6 files changed, 51 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 5d0ae1ba..3e5e2455 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -31,8 +31,45 @@ jobs: run: | python -m pip install --upgrade pip pip install -r requirements.txt + pip install pytest - - name: Test generation + - name: Cache R5 download + uses: actions/cache@v4 + with: + path: downloads-r5 + key: downloads-r5 + + - name: Generate R5 + run: | + cp ./Default/settings.py . + sed -i "s|'../models|'models|g" settings.py + sed -i "s|'downloads'|'downloads-r5'|g" settings.py + sed -i "s|\(^specification_url = \)'.*'|\1'http://hl7.org/fhir/R5'|g" settings.py + rm -rf models + ./generate.py + grep 'Generated from FHIR 5.0.0' models/account.py # sanity check + +# FIXME: The R5 tests fail to pass currently (due to some wrong types and missing properties) +# - name: Test R5 +# run: | +# FHIR_UNITTEST_DATADIR=downloads-r5 pytest + + - name: Cache R4 download + uses: actions/cache@v4 + with: + path: downloads-r4 + key: downloads-r4 + + - name: Generate R4 run: | - echo "from Default.settings import *" > settings.py + cp ./Default/settings.py . + sed -i "s|'../models|'models|g" settings.py + sed -i "s|'downloads'|'downloads-r4'|g" settings.py + sed -i "s|\(^specification_url = \)'.*'|\1'http://hl7.org/fhir/R4'|g" settings.py + rm -rf models ./generate.py + grep 'Generated from FHIR 4.0.1' models/account.py # sanity check + + - name: Test R4 + run: | + FHIR_UNITTEST_DATADIR=downloads-r4 pytest diff --git a/Default/mappings.py b/Default/mappings.py index dcbac0ae..7cc91582 100644 --- a/Default/mappings.py +++ b/Default/mappings.py @@ -69,6 +69,8 @@ '>': 'gt', '>=': 'gte', '*': 'max', + '+': 'pos', + '-': 'neg', } # If you want to give specific names to enums based on their URI diff --git a/Default/settings.py b/Default/settings.py index 252e8655..2c1fcda6 100644 --- a/Default/settings.py +++ b/Default/settings.py @@ -36,7 +36,7 @@ write_unittests = True tpl_unittest_source = 'template-unittest.py' # the template to use for unit test generation tpl_unittest_target = '../models' # target directory to write the generated unit test files to -tpl_unittest_target_ptrn = '{}_tests.py' # target file name pattern for unit tests; the one placeholder (`{}`) will be the class name +tpl_unittest_target_ptrn = '{}_test.py' # target file name pattern for unit tests; the one placeholder (`{}`) will be the class name unittest_copyfiles = [] # array of file names to copy to the test directory `tpl_unittest_target` (e.g. unit test base classes) unittest_format_path_prepare = '{}' # used to format `path` before appending another path element - one placeholder for `path` diff --git a/README.md b/README.md index 8f6615a9..85b8bc49 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,8 @@ If you've come here because you want _Swift_ or _Python_ classes for FHIR data m - [Swift-FHIR][] and [Swift-SMART][] - Python [client-py][] -The `main` branch is currently capable of parsing _R5_. +The `main` branch is currently capable of parsing _R4_ +and has preliminary support for _R5_. This work is licensed under the [APACHE license][license]. FHIR® is the registered trademark of [HL7][] and is used with the permission of HL7. @@ -75,7 +76,6 @@ If an element itself defines a class, e.g. `Patient.animal`, calling the instanc The class of this property is derived from `element.type`, which is expected to only contain one entry, in this matter: - If _type_ is `BackboneElement`, a class name is constructed from the parent element (in this case _Patient_) and the property name (in this case _animal_), camel-cased (in this case _PatientAnimal_). -- If _type_ is `*`, a class for all classes found in settings` `star_expand_types` is created - Otherwise, the type is taken as-is (e.g. _CodeableConcept_) and mapped according to mappings' `classmap`, which is expected to be a valid FHIR class. > TODO: should `http://hl7.org/fhir/StructureDefinition/structuredefinition-explicit-type-name` be respected? diff --git a/Sample/template-unittest.py b/Sample/template-unittest.py index 86c0c78e..cc21a689 100644 --- a/Sample/template-unittest.py +++ b/Sample/template-unittest.py @@ -39,7 +39,7 @@ def test{{ class.name }}{{ loop.index }}(self): def impl{{ class.name }}{{ loop.index }}(self, inst): {%- for onetest in tcase.tests %} {%- if "str" == onetest.klass.name %} - self.assertEqual(inst.{{ onetest.path }}, "{{ onetest.value|replace('"', '\\"') }}") + self.assertEqual(inst.{{ onetest.path }}, "{{ onetest.value|replace('\\', '\\\\')|replace('"', '\\"') }}") {%- else %}{% if "int" == onetest.klass.name or "float" == onetest.klass.name or "NSDecimalNumber" == onetest.klass.name %} self.assertEqual(inst.{{ onetest.path }}, {{ onetest.value }}) {%- else %}{% if "bool" == onetest.klass.name %} diff --git a/fhirspec.py b/fhirspec.py index a7ec0d66..a0ad9d59 100644 --- a/fhirspec.py +++ b/fhirspec.py @@ -8,6 +8,7 @@ import glob import json import datetime +import pathlib from logger import logger import fhirclass @@ -267,6 +268,9 @@ def write(self): vsrenderer = fhirrenderer.FHIRValueSetRenderer(self, self.settings) vsrenderer.render() + + # Create init file so that our relative imports work out of the box + pathlib.Path(self.settings.tpl_resource_target, "__init__.py").touch() if self.settings.write_factory: renderer = fhirrenderer.FHIRFactoryRenderer(self, self.settings) @@ -609,11 +613,11 @@ def needed_external_classes(self): # look at all properties' classes and assign their modules for prop in klass.properties: prop_cls_name = prop.class_name - if prop.enum is not None: + if prop.enum is not None and not self.spec.class_name_is_native(prop_cls_name): enum_cls, did_create = fhirclass.FHIRClass.for_element(prop.enum) enum_cls.module = prop.enum.name prop.module_name = enum_cls.module - if not enum_cls.name in needed: + if enum_cls.name not in needed: needed.add(enum_cls.name) needs.append(enum_cls)