Skip to content

Commit

Permalink
feat!: enhance custom code & remove cli options
Browse files Browse the repository at this point in the history
  • Loading branch information
mirkolenz committed Jan 14, 2023
1 parent 9404ecc commit a8b0b64
Show file tree
Hide file tree
Showing 8 changed files with 99 additions and 77 deletions.
6 changes: 6 additions & 0 deletions makejinja/__init__.py
Original file line number Diff line number Diff line change
@@ -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
58 changes: 33 additions & 25 deletions makejinja/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
from jinja2 import Environment, FileSystemLoader
from rich import print

from makejinja.types import ExportsTemplate

try:
import tomllib
except ModuleNotFoundError:
Expand All @@ -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]:
Expand All @@ -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,
}


Expand Down Expand Up @@ -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,
Expand All @@ -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")
Expand Down
32 changes: 4 additions & 28 deletions makejinja/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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/).
Expand Down
30 changes: 30 additions & 0 deletions makejinja/types.py
Original file line number Diff line number Diff line change
@@ -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
3 changes: 1 addition & 2 deletions tests/data/.makejinja.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 = "<%"
Expand Down
25 changes: 25 additions & 0 deletions tests/data/exports.py
Original file line number Diff line number Diff line change
@@ -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]
9 changes: 0 additions & 9 deletions tests/data/filters.py

This file was deleted.

13 changes: 0 additions & 13 deletions tests/data/globals.py

This file was deleted.

0 comments on commit a8b0b64

Please sign in to comment.