From a8b0b641304583377975d9960d0677596ad88709 Mon Sep 17 00:00:00 2001 From: Mirko Lenz Date: Sun, 15 Jan 2023 00:55:55 +0100 Subject: [PATCH] feat!: enhance custom code & remove cli options --- makejinja/__init__.py | 6 ++++ makejinja/cli.py | 58 ++++++++++++++++++++++---------------- makejinja/config.py | 32 +++------------------ makejinja/types.py | 30 ++++++++++++++++++++ tests/data/.makejinja.toml | 3 +- tests/data/exports.py | 25 ++++++++++++++++ tests/data/filters.py | 9 ------ tests/data/globals.py | 13 --------- 8 files changed, 99 insertions(+), 77 deletions(-) create mode 100644 makejinja/types.py create mode 100644 tests/data/exports.py delete mode 100644 tests/data/filters.py delete mode 100644 tests/data/globals.py diff --git a/makejinja/__init__.py b/makejinja/__init__.py index e69de29..6f0e6b0 100644 --- a/makejinja/__init__.py +++ b/makejinja/__init__.py @@ -0,0 +1,6 @@ +from .cli import main as main +from .types import Data as Data +from .types import ExportsTemplate as ExportsTemplate +from .types import Extensions as Extensions +from .types import Filters as Filters +from .types import Globals as Globals diff --git a/makejinja/cli.py b/makejinja/cli.py index 8adcafe..92741ae 100644 --- a/makejinja/cli.py +++ b/makejinja/cli.py @@ -12,6 +12,8 @@ from jinja2 import Environment, FileSystemLoader from rich import print +from makejinja.types import ExportsTemplate + try: import tomllib except ModuleNotFoundError: @@ -23,20 +25,26 @@ click.rich_click.OPTION_GROUPS = OPTION_GROUPS -def _import_module(path: Path) -> ModuleType: +def _import_module(path: Path) -> ExportsTemplate: # https://stackoverflow.com/a/41595552 # https://docs.python.org/3.11/library/importlib.html#importing-a-source-file-directly name = str(uuid()).lower().replace("-", "") + + if path.is_dir(): + path /= "__init__.py" + spec = importlib.util.spec_from_file_location(name, path) - assert spec is not None + assert ( + spec is not None + ), f"The module has not been found. Please verify the given path '{path}'." - module = importlib.util.module_from_spec(spec) + module: ModuleType = importlib.util.module_from_spec(spec) sys.modules[name] = module assert spec.loader is not None spec.loader.exec_module(module) - return module + return module.Exports() def _from_yaml(path: Path) -> dict[str, t.Any]: @@ -54,16 +62,10 @@ def _from_toml(path: Path) -> dict[str, t.Any]: return tomllib.load(fp) -def _from_py(path: Path) -> dict[str, t.Any]: - mod = _import_module(path) - return mod.data - - DATA_LOADERS: dict[str, t.Callable[[Path], dict[str, t.Any]]] = { ".yaml": _from_yaml, ".yml": _from_yaml, ".toml": _from_toml, - ".py": _from_py, } @@ -98,9 +100,17 @@ def main(config: Config): Please refer to the file [`makejinja/config.py`](https://github.com/mirkolenz/makejinja/blob/main/makejinja/config.py) to see their actual names. You will also find an example here: [`makejinja/tests/data/.makejinja.toml`](https://github.com/mirkolenz/makejinja/blob/main/tests/data/.makejinja.toml). """ + modules = [_import_module(mod) for mod in config.modules] + + extensions: list[t.Any] = [*config.extensions] + + for mod in modules: + if hasattr(mod, "extensions"): + extensions.extend(mod.extensions()) + env = Environment( loader=FileSystemLoader(config.input_path), - extensions=config.extension_names, + extensions=extensions, keep_trailing_newline=config.keep_trailing_newline, trim_blocks=config.trim_blocks, lstrip_blocks=config.lstrip_blocks, @@ -112,26 +122,24 @@ def main(config: Config): variable_end_string=config.delimiter.variable_end, ) - for _global in _collect_files(config.global_paths, "**/*.py"): - mod = _import_module(_global) - env.globals.update(mod.globals) - - for _filter in _collect_files(config.filter_paths, "**/*.py"): - mod = _import_module(_filter) - env.filters.update(mod.filters) - data: dict[str, t.Any] = {} for path in _collect_files(config.data_paths): if loader := DATA_LOADERS.get(path.suffix): data.update(loader(path)) - # TODO: Maybe remove `collect_files` and import as real module instead - for file in _collect_files(config.custom_code, "**/*.py"): - mod = _import_module(file) - env.globals.update(mod.globals) - env.filters.update(mod.filters) - data.update(mod.data) + for mod in modules: + if hasattr(mod, "globals"): + env.globals.update({func.__name__: func for func in mod.globals()}) + + if hasattr(mod, "filters"): + env.filters.update({func.__name__: func for func in mod.filters()}) + + if hasattr(mod, "data"): + data.update(mod.data()) + + if hasattr(mod, "setup_env"): + mod.setup_env(env) if config.output_path.is_dir(): print(f"Remove '{config.output_path}' from previous run") diff --git a/makejinja/config.py b/makejinja/config.py index 740a331..dd9301c 100644 --- a/makejinja/config.py +++ b/makejinja/config.py @@ -80,43 +80,19 @@ class Config: If multiple files are supplied, beware that previous declarations will be overwritten by newer ones. """, ) - global_paths: list[Path] = ts.option( + modules: list[Path] = ts.option( factory=list, click={ "type": click.Path(exists=True, path_type=Path), - "param_decls": "--global-path", + "param_decls": "--module", }, - help=""" - You can import functions/varibales defined in `.py` files to use them in your Jinja templates. - Can either be a file or a folder containg files. - **Note:** This option may be passed multiple times to pass a list of files/folders. - If multiple files are supplied, beware that previous declarations will be overwritten by newer ones. - """, - ) - filter_paths: list[Path] = ts.option( - factory=list, - click={ - "type": click.Path(exists=True, path_type=Path), - "param_decls": "--filter-path", - }, - help=""" - Jinja has support for filters (e.g., `[1, 2, 3] | length`) to easily call functions. - This option allows you to define [custom filters](https://jinja.palletsprojects.com/en/3.1.x/api/#custom-filters) in `.py` files. - Can either be a file or a folder containg files. - **Note:** This option may be passed multiple times to pass a list of files/folders. - If multiple files are supplied, beware that previous declarations will be overwritten by newer ones. - """, - ) - custom_code: list[Path] = ts.option( - factory=list, - click={"type": click.Path(exists=True, path_type=Path)}, help=""" Load custom code into the program. TODO: More details """, ) - extension_names: list[str] = ts.option( + extensions: list[str] = ts.option( factory=list, - click={"param_decls": "--extension-name"}, + click={"param_decls": "--extension"}, help=""" Extend Jinja's parser by loading the specified extensions. An overview of the built-in ones can be found on the [project website](https://jinja.palletsprojects.com/en/3.1.x/extensions/). diff --git a/makejinja/types.py b/makejinja/types.py new file mode 100644 index 0000000..8db27d9 --- /dev/null +++ b/makejinja/types.py @@ -0,0 +1,30 @@ +import typing as t +from abc import ABC, abstractmethod + +from jinja2 import Environment +from jinja2.ext import Extension + +ExtensionType = t.Union[Extension, str] +Extensions = t.Sequence[ExtensionType] +Filter = t.Callable[[t.Any], t.Any] +Filters = t.Sequence[Filter] +Global = t.Callable[..., t.Any] +Globals = t.Sequence[Global] +Data = t.Mapping[str, t.Any] + + +class ExportsTemplate(ABC): + def filters(self) -> Filters: + return [] + + def globals(self) -> Globals: + return [] + + def data(self) -> Data: + return {} + + def extensions(self) -> Extensions: + return [] + + def setup_env(self, env: Environment) -> None: + pass diff --git a/tests/data/.makejinja.toml b/tests/data/.makejinja.toml index 999628f..7a29d73 100644 --- a/tests/data/.makejinja.toml +++ b/tests/data/.makejinja.toml @@ -2,8 +2,7 @@ input_path = "./input" output_path = "./output" data_paths = ["./config"] -filter_paths = ["./filters.py"] -global_paths = ["./globals.py"] +modules = ["./exports.py"] [makejinja.delimiter] block_start = "<%" diff --git a/tests/data/exports.py b/tests/data/exports.py new file mode 100644 index 0000000..54f58c3 --- /dev/null +++ b/tests/data/exports.py @@ -0,0 +1,25 @@ +import typing as t +from urllib.parse import quote as urlparse + +from makejinja import Filters, Globals + + +def hassurl(value: str) -> str: + return urlparse(value).replace("_", "-") + + +def getlang( + value: t.Union[str, t.Mapping[str, t.Any]], lang: str, default_lang: str = "en" +): + if isinstance(value, str): + return value + else: + return value.get(lang, value.get(default_lang, "")) + + +class Exports: + def filters(self) -> Filters: + return [hassurl] + + def globals(self) -> Globals: + return [getlang] diff --git a/tests/data/filters.py b/tests/data/filters.py deleted file mode 100644 index 6ea15f7..0000000 --- a/tests/data/filters.py +++ /dev/null @@ -1,9 +0,0 @@ -import typing as t -from urllib.parse import quote as urlparse - - -def hassurl(value: str) -> str: - return urlparse(value).replace("_", "-") - - -filters: t.Dict[str, t.Callable[[t.Any], t.Any]] = {"hassurl": hassurl} diff --git a/tests/data/globals.py b/tests/data/globals.py deleted file mode 100644 index e4ad456..0000000 --- a/tests/data/globals.py +++ /dev/null @@ -1,13 +0,0 @@ -import typing as t - - -def getlang( - value: t.Union[str, t.Mapping[str, t.Any]], lang: str, default_lang: str = "en" -): - if isinstance(value, str): - return value - else: - return value.get(lang, value.get(default_lang, "")) - - -globals: t.Dict[str, t.Callable] = {"getlang": getlang}