From 85f14deae3ccdc6d41ce7e5659fcb4f433769282 Mon Sep 17 00:00:00 2001 From: chaseastewart <153762074+chaseastewart@users.noreply.github.com> Date: Tue, 20 Feb 2024 02:15:45 -0600 Subject: [PATCH] 0.1.0 release. --- fhir_converter/loaders.py | 163 +++++++++++++++--------------------- fhir_converter/renderers.py | 21 +++-- pyproject.toml | 4 +- tests/test_loaders.py | 87 ++++--------------- tests/test_renderers.py | 18 ++-- 5 files changed, 103 insertions(+), 190 deletions(-) diff --git a/fhir_converter/loaders.py b/fhir_converter/loaders.py index 889367f..22dbe11 100644 --- a/fhir_converter/loaders.py +++ b/fhir_converter/loaders.py @@ -1,105 +1,15 @@ from __future__ import annotations from pathlib import Path -from typing import Callable, List +from typing import List, Optional, Sequence -from importlib_resources import Package, files -from liquid import BoundTemplate, ChoiceLoader, Context, Environment +from liquid import ChoiceLoader, Context, Environment from liquid.builtin.loaders.mixins import CachingLoaderMixin from liquid.exceptions import TemplateNotFound -from liquid.loaders import BaseLoader, TemplateNamespace, TemplateSource -from typing_extensions import deprecated +from liquid.loaders import BaseLoader, TemplateSource -@deprecated( - "ResourceLoader is deprecated and scheduled for removal " - "in a future version. Use PackageLoader instead" -) -class ResourceLoader(BaseLoader): - """A template loader that will load templates from the package resources - - Args: - search_package (Package): The package to load templates from - encoding (str, optional): The encoding to use loading the template source - Defaults to "utf-8". - ext (str, optional): The extension to use when one isn't provided - Defaults to ".liquid". - """ - - def __init__( - self, - search_package: Package, - encoding: str = "utf-8", - ext: str = ".liquid", - ) -> None: - super().__init__() - self.search_package = search_package - self.encoding = encoding - self.ext = ext - - def get_source(self, _: Environment, template_name: str) -> TemplateSource: - template_path = Path(template_name) - if ".." in template_path.parts: - raise TemplateNotFound(template_name) - - if not template_path.suffix: - template_path = template_path.with_suffix(self.ext) - try: - resource_path = files(self.search_package).joinpath(template_path) - return TemplateSource( - source=resource_path.read_text(self.encoding), - filename=str(resource_path), - uptodate=lambda: True, - ) - except (ModuleNotFoundError, FileNotFoundError): - raise TemplateNotFound(template_name) - - -@deprecated( - "CachedChoiceLoader is deprecated and scheduled for removal " - "in a future version. Use CachingChoiceLoader instead" -) -class CachedChoiceLoader(CachingLoaderMixin, ChoiceLoader): - """A choice loader that caches parsed templates in memory - Args: - loaders: A list of loaders implementing `liquid.loaders.BaseLoader` - auto_reload (bool, optional): If `True`, automatically reload a cached template - if it has been updated. Defaults to True - cache_size (int, optional): The maximum number of templates to hold in the cache - before removing the least recently used template. Defaults to 300 - namespace_key (str, optional): The name of a global render context variable or - loader keyword argument that resolves to the current loader "namespace" or - "scope". Defaults to "" - """ - - def __init__( - self, - loaders: List[BaseLoader], - auto_reload: bool = True, - cache_size: int = 300, - namespace_key: str = "", - ) -> None: - super().__init__( - auto_reload=auto_reload, - namespace_key=namespace_key, - cache_size=cache_size, - ) - ChoiceLoader.__init__(self, loaders) - self.is_caching = cache_size > 0 - - def _check_cache( - self, - env: Environment, # noqa: ARG002 - cache_key: str, - globals: TemplateNamespace, # noqa: A002 - load_func: Callable[[], BoundTemplate], - ) -> BoundTemplate: - if self.is_caching: - return super()._check_cache(env, cache_key, globals, load_func) - return load_func() - - -class TemplateSystemLoader(CachedChoiceLoader): +class TemplateSystemLoader(ChoiceLoader): """TemplateSystemLoader allows templates to be loaded from a primary and optionally secondary location(s). This allows templates to include / render templates from the other location(s) @@ -107,7 +17,7 @@ class TemplateSystemLoader(CachedChoiceLoader): Any template (non json file) that is in a subdirectory will have _ prepended to the name Ex: Section/Immunization -> Section/_Immunization - See ``CachedChoiceLoader`` for more information + See `ChoiceLoader` for more information """ def get_source( @@ -170,6 +80,69 @@ def _resolve_template_name(self, template_name: str) -> str: return str(template_path) +class CachingTemplateSystemLoader(CachingLoaderMixin, TemplateSystemLoader): + """TemplateSystemLoader that caches parsed templates in memory. + + See `TemplateSystemLoader` for more information + """ + + def __init__( + self, + loaders: List[BaseLoader], + *, + auto_reload: bool = True, + namespace_key: str = "", + cache_size: int = 300, + ) -> None: + super().__init__( + auto_reload=auto_reload, + namespace_key=namespace_key, + cache_size=cache_size, + ) + + TemplateSystemLoader.__init__(self, loaders) + + +def make_template_system_loader( + loader: BaseLoader, + *, + auto_reload: bool = True, + namespace_key: str = "", + cache_size: int = 300, + additional_loaders: Optional[Sequence[BaseLoader]] = None, +) -> BaseLoader: + """make_template_system_loader A `TemplateSystemLoader` factory + + Args: + loader (BaseLoader): The loader to use when loading the rendering temples + auto_reload (bool, optional): If `True`, loaders that have an `uptodate` + callable will reload template source data automatically. Defaults to False + namespace_key (str, optional): The name of a global render context variable or loader + keyword argument that resolves to the current loader "namespace" or + "scope". Defaults to "" + cache_size (int, optional): The capacity of the template cache in number of + templates. cache_size is None or less than 1 disables caching. + Defaults to 300 + additional_loaders (Optional[Sequence[BaseLoader]], optional): The additional + loaders to use when a template is not found by the loader. Defaults to None + + Returns: + BaseLoader: `CachingTemplateSystemLoader` if cache_size > 0 else `TemplateSystemLoader` + """ + loaders = [loader] + if additional_loaders: + loaders += additional_loaders + if cache_size > 0: + return CachingTemplateSystemLoader( + loaders=loaders, + auto_reload=auto_reload, + namespace_key=namespace_key, + cache_size=cache_size, + ) + + return TemplateSystemLoader(loaders=loaders) + + def read_text(env: Environment, filename: str) -> str: """read_text Reads the text from the given filename using the Environment's loader to retrieve the file's contents diff --git a/fhir_converter/renderers.py b/fhir_converter/renderers.py index 8196e49..4048163 100644 --- a/fhir_converter/renderers.py +++ b/fhir_converter/renderers.py @@ -22,13 +22,13 @@ from frozendict import frozendict from liquid import Environment -from liquid.loaders import BaseLoader +from liquid.loaders import BaseLoader, PackageLoader from pyjson5 import encode_io from pyjson5 import loads as json_loads from fhir_converter.filters import all_filters, register_filters from fhir_converter.hl7 import parse_fhir -from fhir_converter.loaders import ResourceLoader, TemplateSystemLoader, read_text +from fhir_converter.loaders import make_template_system_loader, read_text from fhir_converter.tags import all_tags, register_tags from fhir_converter.utils import ( del_empty_dirs_quietly, @@ -52,13 +52,13 @@ RenderErrorHandler = Callable[[Exception], None] """Callable[[Exception], None]: Rendering error handling function""" -ccda_default_loader: Final[ResourceLoader] = ResourceLoader( - search_package="fhir_converter.templates.ccda" +ccda_default_loader: Final[PackageLoader] = PackageLoader( + package="fhir_converter.templates", package_path="ccda" ) """ResourceLoader: The default loader for the ccda templates""" -stu3_default_loader: Final[ResourceLoader] = ResourceLoader( - search_package="fhir_converter.templates.stu3" +stu3_default_loader: Final[PackageLoader] = PackageLoader( + package="fhir_converter.templates", package_path="stu3" ) """ResourceLoader: The default loader for the stu3 templates""" @@ -292,15 +292,14 @@ def get_environment( Returns: Environment: The rendering environment """ - loaders = [loader] - if additional_loaders: - loaders += additional_loaders env = Environment( - loader=TemplateSystemLoader( - loaders, + loader=make_template_system_loader( + loader, auto_reload=auto_reload, cache_size=cache_size, + additional_loaders=additional_loaders, ), + cache_size=cache_size, **kwargs, ) register_filters(env, all_filters, replace=True) diff --git a/pyproject.toml b/pyproject.toml index b435eb2..874c089 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api" [tool.poetry] name = "python-fhir-converter" -version = "0.0.22" +version = "0.1.0" authors = ["Chase Stewart "] description = "Transformation utility to translate data formats into FHIR" readme = "README.md" @@ -38,7 +38,7 @@ packages = [ [tool.poetry.dependencies] python = "^3.8.1" -python-liquid = "1.11.0" +python-liquid = "1.12.1" xmltodict = "0.13.0" pyjson5 = "1.6.5" frozendict = "2.3.10" diff --git a/tests/test_loaders.py b/tests/test_loaders.py index df0bdbc..2524908 100644 --- a/tests/test_loaders.py +++ b/tests/test_loaders.py @@ -3,12 +3,15 @@ from liquid import BoundTemplate, Context, Environment, FileExtensionLoader from liquid.exceptions import TemplateNotFound -from liquid.loaders import BaseLoader +from liquid.loaders import BaseLoader, PackageLoader from pytest import raises -from fhir_converter.loaders import ResourceLoader, TemplateSystemLoader, read_text +from fhir_converter.loaders import make_template_system_loader, read_text from fhir_converter.tags import all_tags, register_tags +user_defined_ccda_loader = FileExtensionLoader(search_path="data/templates/ccda") +ccda_loader = PackageLoader(package="fhir_converter.templates", package_path="ccda") + def build_env(loader: BaseLoader, **kwargs) -> Environment: env = Environment(loader=loader, **kwargs) @@ -16,76 +19,16 @@ def build_env(loader: BaseLoader, **kwargs) -> Environment: return env -class ResourceLoaderTest(TestCase): - @staticmethod - def get_rendering_env( - search_package: str = "fhir_converter.templates.ccda", **kwargs - ) -> Environment: - return build_env(ResourceLoader(search_package), **kwargs) - - def test_load_template(self) -> None: - template = self.get_rendering_env().get_template(name="CCD.liquid") - self.assertIsInstance(template, BoundTemplate) - - def test_load_template_relative(self) -> None: - template = self.get_rendering_env().get_template(name="./CCD.liquid") - self.assertIsInstance(template, BoundTemplate) - - def test_load_template_without_suffix(self) -> None: - template = self.get_rendering_env().get_template(name="CCD") - self.assertIsInstance(template, BoundTemplate) - - def test_cached_template(self) -> None: - env = self.get_rendering_env() - template = env.get_template(name="CCD.liquid") - self.assertIsInstance(template, BoundTemplate) - - another = env.get_template(name="CCD.liquid") - self.assertIsInstance(template, BoundTemplate) - self.assertEqual(template.tree, another.tree) - - def test_disable_template_cache(self) -> None: - env = self.get_rendering_env(cache_size=0) - template = env.get_template(name="CCD.liquid") - self.assertTrue(template.is_up_to_date) - - another = env.get_template(name="CCD.liquid") - self.assertTrue(another.is_up_to_date) - self.assertIsNot(template.tree, another.tree) - - def test_template_not_found(self) -> None: - with raises(TemplateNotFound): - self.get_rendering_env().get_template(name="nosuchthing.liquid") - - def test_no_such_search_path(self) -> None: - with raises(TemplateNotFound): - self.get_rendering_env(search_package="nosuchthing").get_template( - name="nosuchthing.liquid" - ) - - def test_escape_search_path(self) -> None: - with raises(TemplateNotFound): - self.get_rendering_env().get_template(name="../CCD.liquid") - - def test_escape_search_path_nested(self) -> None: - with raises(TemplateNotFound): - self.get_rendering_env().get_template(name="./../CCD.liquid") - - class TemplateSystemLoaderTest(TestCase): - loaders = [ - FileExtensionLoader(search_path="data/templates/ccda"), - ResourceLoader(search_package="fhir_converter.templates.ccda"), - ] - def get_rendering_env( self, loader_cache_size: int = 1, **kwargs, ) -> Environment: return build_env( - TemplateSystemLoader( - loaders=self.loaders, + make_template_system_loader( + loader=user_defined_ccda_loader, + additional_loaders=[ccda_loader], cache_size=loader_cache_size, ), **kwargs, @@ -122,7 +65,7 @@ def test_load_internal_resource_template(self) -> None: template = self.get_rendering_env().get_template(name="Utils/GenerateId") self.assertIsInstance(template, BoundTemplate) - def test_cached_template(self) -> None: + def test_cached_user_defined_template(self) -> None: env = self.get_rendering_env() template = env.get_template(name="Pampi.liquid") self.assertIsInstance(template, BoundTemplate) @@ -140,7 +83,7 @@ def test_cached_template_with_globals(self) -> None: self.assertEqual({"test": "two"}, template.globals) self.assertEqual(template.tree, another.tree) - def test_cached_resource_template(self) -> None: + def test_cached_package_template(self) -> None: env = self.get_rendering_env() template = env.get_template(name="CCD.liquid") self.assertIsInstance(template, BoundTemplate) @@ -160,18 +103,16 @@ def test_disable_env_cache_template_cached(self) -> None: self.assertTrue(another.is_up_to_date) self.assertEqual(template.tree, another.tree) - def test_disable_loader_cache_template_not_cached(self) -> None: + def test_disable_loader_cache_template_cached(self) -> None: env = self.get_rendering_env(loader_cache_size=0) template = env.get_template(name="Pampi.liquid") self.assertTrue(template.is_up_to_date) - # The environment only caches when the loader is not a caching loader, - # if this changes, this will not be None - self.assertIsNone(env.cache) + self.assertIsNotNone(env.cache) another = env.get_template(name="Pampi.liquid") self.assertTrue(another.is_up_to_date) - self.assertIsNot(template.tree, another.tree) + self.assertEqual(template.tree, another.tree) def test_disable_caches_template_not_cached(self) -> None: env = self.get_rendering_env(loader_cache_size=0, cache_size=0) @@ -188,7 +129,7 @@ def test_template_not_found(self) -> None: class ReadTextTest(TestCase): - env = build_env(ResourceLoader(search_package="fhir_converter.templates.ccda")) + env = build_env(ccda_loader) def test_file_not_found(self) -> None: with raises(FileNotFoundError, match="File not found nosuchthing.json"): diff --git a/tests/test_renderers.py b/tests/test_renderers.py index 2f2cb6a..7a82694 100644 --- a/tests/test_renderers.py +++ b/tests/test_renderers.py @@ -2,6 +2,7 @@ from io import StringIO from json import loads from pathlib import Path +from typing import cast from unittest import TestCase from liquid import FileExtensionLoader @@ -10,7 +11,7 @@ from pyjson5 import Json5EOF from pytest import raises -from fhir_converter.loaders import TemplateSystemLoader +from fhir_converter.loaders import CachingTemplateSystemLoader, TemplateSystemLoader from fhir_converter.renderers import ( CcdaRenderer, RenderingError, @@ -27,10 +28,9 @@ def test_defaults(self) -> None: self.assertFalse(env.auto_reload) self.assertIsNone(env.cache) - self.assertIsInstance(env.loader, TemplateSystemLoader) - loader: TemplateSystemLoader = env.loader # type: ignore + self.assertIsInstance(env.loader, CachingTemplateSystemLoader) + loader = cast(CachingTemplateSystemLoader, env.loader) self.assertFalse(loader.auto_reload) - self.assertTrue(loader.is_caching) self.assertEqual([ccda_default_loader], loader.loaders) self.assertIsInstance(loader.cache, LRUCache) @@ -41,22 +41,22 @@ def test_auto_reload(self) -> None: self.assertFalse(env.auto_reload) self.assertIsNone(env.cache) - self.assertTrue(env.loader.auto_reload) # type: ignore + loader = cast(CachingTemplateSystemLoader, env.loader) + self.assertTrue(loader.auto_reload) def test_cache_size(self) -> None: env = get_environment(loader=ccda_default_loader, cache_size=1) self.assertFalse(env.auto_reload) self.assertIsNone(env.cache) - self.assertTrue(env.loader.is_caching) # type: ignore - self.assertEqual(env.loader.cache.capacity, 1) # type: ignore + loader = cast(CachingTemplateSystemLoader, env.loader) + self.assertEqual(loader.cache.capacity, 1) # type: ignore def test_cache_disabled(self) -> None: env = get_environment(loader=ccda_default_loader, cache_size=0) self.assertFalse(env.auto_reload) self.assertIsNone(env.cache) - self.assertFalse(env.loader.is_caching) # type: ignore - self.assertEqual(env.loader.cache.capacity, 0) # type: ignore + self.assertIsInstance(env.loader, TemplateSystemLoader) def test_additional_loaders(self) -> None: loader = FileExtensionLoader(search_path="data/templates/ccda")