Add namespaced template imports
This commit is contained in:
parent
5c152a4c30
commit
a5b905cb9f
2
.vscode/extensions.json
vendored
2
.vscode/extensions.json
vendored
|
@ -4,5 +4,7 @@
|
|||
"ms-python.vscode-pylance",
|
||||
"ms-python.black-formatter",
|
||||
"ms-python.isort",
|
||||
"samuelcolvin.jinjahtml",
|
||||
"noxiz.jinja-snippets",
|
||||
],
|
||||
}
|
|
@ -107,7 +107,10 @@ missing = []
|
|||
|
||||
[template]
|
||||
static_dir = "static"
|
||||
packages = ["hexdoc"]
|
||||
include = [
|
||||
"hexcasting",
|
||||
"patchouli",
|
||||
]
|
||||
|
||||
[template.args]
|
||||
mod_name = "Hex Casting"
|
||||
|
|
|
@ -88,7 +88,7 @@ def render(
|
|||
logger.info(f"update_latest={update_latest}, release={release}")
|
||||
|
||||
# set up Jinja
|
||||
env = create_jinja_env(props)
|
||||
env = create_jinja_env(props, pm)
|
||||
|
||||
templates = {
|
||||
"index.html": env.get_template(props.template.main),
|
||||
|
|
|
@ -6,13 +6,7 @@ import shutil
|
|||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from jinja2 import (
|
||||
ChoiceLoader,
|
||||
FileSystemLoader,
|
||||
PackageLoader,
|
||||
StrictUndefined,
|
||||
Template,
|
||||
)
|
||||
from jinja2 import ChoiceLoader, PrefixLoader, StrictUndefined, Template
|
||||
from jinja2.sandbox import SandboxedEnvironment
|
||||
|
||||
from hexdoc.core.metadata import HexdocMetadata
|
||||
|
@ -21,6 +15,7 @@ from hexdoc.core.resource import ResourceLocation
|
|||
from hexdoc.minecraft import I18n
|
||||
from hexdoc.minecraft.assets.textures import AnimatedTexture, Texture
|
||||
from hexdoc.patchouli import Book
|
||||
from hexdoc.plugin import PluginManager
|
||||
from hexdoc.utils.jinja.extensions import IncludeRawExtension
|
||||
from hexdoc.utils.jinja.filters import (
|
||||
hexdoc_block,
|
||||
|
@ -33,12 +28,15 @@ from hexdoc.utils.path import write_to_path
|
|||
from .sitemap import MARKER_NAME, SitemapMarker
|
||||
|
||||
|
||||
def create_jinja_env(props: Properties):
|
||||
def create_jinja_env(props: Properties, pm: PluginManager):
|
||||
prefix_loaders = pm.load_jinja_templates(props.template.include)
|
||||
|
||||
env = SandboxedEnvironment(
|
||||
# search order: template_dirs, template_packages
|
||||
loader=ChoiceLoader(
|
||||
[FileSystemLoader(props.template.dirs)]
|
||||
+ [PackageLoader(name, str(path)) for name, path in props.template.packages]
|
||||
[
|
||||
PrefixLoader(prefix_loaders, ":"),
|
||||
*prefix_loaders.values(),
|
||||
]
|
||||
),
|
||||
undefined=StrictUndefined,
|
||||
lstrip_blocks=True,
|
||||
|
|
|
@ -5,7 +5,7 @@ import re
|
|||
from pathlib import Path
|
||||
from typing import Annotated, Any, Self
|
||||
|
||||
from pydantic import AfterValidator, Field, HttpUrl, field_validator
|
||||
from pydantic import AfterValidator, Field, HttpUrl
|
||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||
|
||||
from hexdoc.core.resource import ResourceDir, ResourceLocation
|
||||
|
@ -72,20 +72,9 @@ class TemplateProps(StripHiddenModel):
|
|||
style: str = "main.css.jinja"
|
||||
|
||||
static_dir: RelativePath | None = None
|
||||
dirs: list[RelativePath] = Field(default_factory=list)
|
||||
packages: list[tuple[str, Path]]
|
||||
include: list[str]
|
||||
args: dict[str, Any]
|
||||
|
||||
@field_validator("packages", mode="before")
|
||||
def _check_packages(cls, values: Any | list[Any]):
|
||||
if not isinstance(values, list):
|
||||
return values
|
||||
|
||||
for i, value in enumerate(values):
|
||||
if isinstance(value, str):
|
||||
values[i] = (value, Path("_templates"))
|
||||
return values
|
||||
|
||||
|
||||
class MinecraftAssetsProps(StripHiddenModel):
|
||||
ref: str
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
from importlib.resources import Package
|
||||
|
||||
import hexdoc
|
||||
from hexdoc.__gradle_version__ import GRADLE_VERSION
|
||||
from hexdoc.plugin import (
|
||||
HookReturn,
|
||||
LoadJinjaTemplatesImpl,
|
||||
LoadResourceDirsImpl,
|
||||
LoadTaggedUnionsImpl,
|
||||
ModVersionImpl,
|
||||
|
@ -12,7 +15,12 @@ from . import hex_recipes
|
|||
from .page import hex_pages
|
||||
|
||||
|
||||
class HexcastingPlugin(LoadResourceDirsImpl, LoadTaggedUnionsImpl, ModVersionImpl):
|
||||
class HexcastingPlugin(
|
||||
LoadResourceDirsImpl,
|
||||
LoadTaggedUnionsImpl,
|
||||
ModVersionImpl,
|
||||
LoadJinjaTemplatesImpl,
|
||||
):
|
||||
@staticmethod
|
||||
@hookimpl
|
||||
def hexdoc_mod_version() -> str:
|
||||
|
@ -20,14 +28,20 @@ class HexcastingPlugin(LoadResourceDirsImpl, LoadTaggedUnionsImpl, ModVersionImp
|
|||
|
||||
@staticmethod
|
||||
@hookimpl
|
||||
def hexdoc_load_resource_dirs() -> Package | list[Package]:
|
||||
# lazy import because this won't exist when we're initially generating it
|
||||
# so we only want to import it when addons need the data
|
||||
def hexdoc_load_resource_dirs() -> HookReturn[Package]:
|
||||
# lazy import because generated may not exist when this file is loaded
|
||||
# eg. when generating the contents of generated
|
||||
# so we only want to import it if we actually need it
|
||||
from hexdoc._export import generated
|
||||
|
||||
return generated
|
||||
|
||||
@staticmethod
|
||||
@hookimpl
|
||||
def hexdoc_load_tagged_unions() -> Package | list[Package]:
|
||||
def hexdoc_load_tagged_unions() -> HookReturn[Package]:
|
||||
return [hex_recipes, hex_pages]
|
||||
|
||||
@staticmethod
|
||||
@hookimpl
|
||||
def hexdoc_load_jinja_templates() -> HookReturn[tuple[Package, str]]:
|
||||
return hexdoc, "_templates"
|
||||
|
|
|
@ -1,12 +1,23 @@
|
|||
from importlib.resources import Package
|
||||
|
||||
from hexdoc.plugin import LoadTaggedUnionsImpl, hookimpl
|
||||
import hexdoc
|
||||
from hexdoc.plugin import (
|
||||
HookReturn,
|
||||
LoadJinjaTemplatesImpl,
|
||||
LoadTaggedUnionsImpl,
|
||||
hookimpl,
|
||||
)
|
||||
|
||||
from .page import pages
|
||||
|
||||
|
||||
class PatchouliPlugin(LoadTaggedUnionsImpl):
|
||||
class PatchouliPlugin(LoadTaggedUnionsImpl, LoadJinjaTemplatesImpl):
|
||||
@staticmethod
|
||||
@hookimpl
|
||||
def hexdoc_load_tagged_unions() -> Package | list[Package]:
|
||||
return pages
|
||||
|
||||
@staticmethod
|
||||
@hookimpl
|
||||
def hexdoc_load_jinja_templates() -> HookReturn[tuple[Package, str]]:
|
||||
return hexdoc, "_templates"
|
||||
|
|
|
@ -3,7 +3,9 @@ __all__ = [
|
|||
"ModVersionImpl",
|
||||
"LoadResourceDirsImpl",
|
||||
"LoadTaggedUnionsImpl",
|
||||
"LoadJinjaTemplatesImpl",
|
||||
"PluginManager",
|
||||
"HookReturn",
|
||||
]
|
||||
|
||||
import pluggy
|
||||
|
@ -11,6 +13,8 @@ import pluggy
|
|||
from .manager import PluginManager
|
||||
from .specs import (
|
||||
HEXDOC_PROJECT_NAME,
|
||||
HookReturn,
|
||||
LoadJinjaTemplatesImpl,
|
||||
LoadResourceDirsImpl,
|
||||
LoadTaggedUnionsImpl,
|
||||
ModVersionImpl,
|
||||
|
|
|
@ -2,13 +2,14 @@ import importlib
|
|||
from dataclasses import dataclass
|
||||
from importlib.resources import Package
|
||||
from types import ModuleType
|
||||
from typing import Callable, Generic, Iterator, ParamSpec, Sequence, TypeVar
|
||||
from typing import Callable, Generic, Iterable, Iterator, ParamSpec, TypeVar
|
||||
|
||||
import pluggy
|
||||
from jinja2 import PackageLoader
|
||||
|
||||
from hexdoc.model import ValidationContext
|
||||
|
||||
from .specs import HEXDOC_PROJECT_NAME, HookPackages, PluginSpec
|
||||
from .specs import HEXDOC_PROJECT_NAME, HookReturns, PluginSpec
|
||||
|
||||
_T = TypeVar("_T")
|
||||
|
||||
|
@ -16,6 +17,10 @@ _P = ParamSpec("_P")
|
|||
_R = TypeVar("_R", covariant=True)
|
||||
|
||||
|
||||
class PluginNotFoundError(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
@dataclass
|
||||
class TypedHookCaller(Generic[_P, _R]):
|
||||
plugin_name: str | None
|
||||
|
@ -32,16 +37,22 @@ class TypedHookCaller(Generic[_P, _R]):
|
|||
|
||||
return f"Plugin {HEXDOC_PROJECT_NAME}-{self.plugin_name}"
|
||||
|
||||
def __call__(self, *args: _P.args, **kwargs: _P.kwargs) -> _R:
|
||||
def try_call(self, *args: _P.args, **kwargs: _P.kwargs) -> _R | None:
|
||||
result = self.caller(*args, **kwargs)
|
||||
match result:
|
||||
case None | []:
|
||||
raise RuntimeError(
|
||||
f"{self.plugin_display_name} does not implement hook {self.name}"
|
||||
)
|
||||
return None
|
||||
case _:
|
||||
return result
|
||||
|
||||
def __call__(self, *args: _P.args, **kwargs: _P.kwargs) -> _R:
|
||||
result = self.try_call(*args, **kwargs)
|
||||
if result is None:
|
||||
raise PluginNotFoundError(
|
||||
f"{self.plugin_display_name} does not implement hook {self.name}"
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
class PluginManager:
|
||||
"""Custom hexdoc plugin manager with helpers and stronger typing."""
|
||||
|
@ -61,10 +72,20 @@ class PluginManager:
|
|||
def load_tagged_unions(self, modid: str | None = None) -> Iterator[ModuleType]:
|
||||
yield from self._import_from_hook(modid, PluginSpec.hexdoc_load_tagged_unions)
|
||||
|
||||
def load_jinja_templates(self, modids: Iterable[str]):
|
||||
"""modid -> PackageLoader"""
|
||||
loaders = dict[str, PackageLoader]()
|
||||
for modid in modids:
|
||||
caller = self._hook_caller(modid, PluginSpec.hexdoc_load_jinja_templates)
|
||||
for package, package_path in flatten(caller()):
|
||||
module = import_package(package)
|
||||
loaders[modid] = PackageLoader(module.__name__, package_path)
|
||||
return loaders
|
||||
|
||||
def _import_from_hook(
|
||||
self,
|
||||
__modid: str | None,
|
||||
__spec: Callable[_P, HookPackages],
|
||||
__spec: Callable[_P, HookReturns[Package]],
|
||||
*args: _P.args,
|
||||
**kwargs: _P.kwargs,
|
||||
) -> Iterator[ModuleType]:
|
||||
|
@ -72,6 +93,14 @@ class PluginManager:
|
|||
for package in flatten(packages):
|
||||
yield import_package(package)
|
||||
|
||||
def _all_hook_callers(
|
||||
self,
|
||||
spec: Callable[_P, _R | None],
|
||||
) -> Iterator[tuple[str, TypedHookCaller[_P, _R]]]:
|
||||
for modid, plugin in self.inner.list_name_plugin():
|
||||
caller = self.inner.subset_hook_caller(spec.__name__, [plugin])
|
||||
yield modid, TypedHookCaller(modid, caller)
|
||||
|
||||
def _hook_caller(
|
||||
self,
|
||||
modid: str | None,
|
||||
|
@ -100,7 +129,7 @@ class PluginManagerContext(ValidationContext, arbitrary_types_allowed=True):
|
|||
|
||||
def flatten(values: list[list[_T] | _T]) -> Iterator[_T]:
|
||||
for value in values:
|
||||
if isinstance(value, Sequence) and not isinstance(value, (str, bytes)):
|
||||
if isinstance(value, list):
|
||||
yield from value
|
||||
else:
|
||||
yield value
|
||||
|
|
|
@ -1,16 +1,21 @@
|
|||
from importlib.resources import Package
|
||||
from typing import Protocol
|
||||
from typing import Protocol, TypeVar
|
||||
|
||||
import pluggy
|
||||
|
||||
HEXDOC_PROJECT_NAME = "hexdoc"
|
||||
|
||||
|
||||
_T = TypeVar("_T")
|
||||
|
||||
HookReturn = _T | list[_T]
|
||||
|
||||
HookReturns = list[HookReturn[_T]]
|
||||
|
||||
|
||||
hookspec = pluggy.HookspecMarker(HEXDOC_PROJECT_NAME)
|
||||
|
||||
|
||||
HookPackages = list[Package | list[Package]]
|
||||
|
||||
|
||||
class PluginSpec(Protocol):
|
||||
@staticmethod
|
||||
@hookspec(firstresult=True)
|
||||
|
@ -19,12 +24,17 @@ class PluginSpec(Protocol):
|
|||
|
||||
@staticmethod
|
||||
@hookspec
|
||||
def hexdoc_load_resource_dirs() -> HookPackages:
|
||||
def hexdoc_load_resource_dirs() -> HookReturns[Package]:
|
||||
...
|
||||
|
||||
@staticmethod
|
||||
@hookspec
|
||||
def hexdoc_load_tagged_unions() -> HookPackages:
|
||||
def hexdoc_load_tagged_unions() -> HookReturns[Package]:
|
||||
...
|
||||
|
||||
@staticmethod
|
||||
@hookspec
|
||||
def hexdoc_load_jinja_templates() -> HookReturns[tuple[Package, str]]:
|
||||
...
|
||||
|
||||
|
||||
|
@ -52,7 +62,7 @@ class LoadResourceDirsImpl(Protocol):
|
|||
"""
|
||||
|
||||
@staticmethod
|
||||
def hexdoc_load_resource_dirs() -> Package | list[Package]:
|
||||
def hexdoc_load_resource_dirs() -> HookReturn[Package]:
|
||||
"""Return the module(s) which contain your plugin's exported book resources."""
|
||||
...
|
||||
|
||||
|
@ -65,6 +75,18 @@ class LoadTaggedUnionsImpl(Protocol):
|
|||
"""
|
||||
|
||||
@staticmethod
|
||||
def hexdoc_load_tagged_unions() -> Package | list[Package]:
|
||||
def hexdoc_load_tagged_unions() -> HookReturn[Package]:
|
||||
"""Return the module(s) which contain your plugin's tagged union subtypes."""
|
||||
...
|
||||
|
||||
|
||||
class LoadJinjaTemplatesImpl(Protocol):
|
||||
"""Interface for a plugin implementing `hexdoc_load_jinja_templates`.
|
||||
|
||||
These protocols are optional - they gives better type checking, but everything will
|
||||
work fine with a standard pluggy hook implementation.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def hexdoc_load_jinja_templates() -> HookReturn[tuple[Package, str]]:
|
||||
...
|
||||
|
|
|
@ -37,9 +37,10 @@ regex = "{^_pattern_regex}"
|
|||
|
||||
[template]
|
||||
static_dir = "static"
|
||||
packages = [
|
||||
"{{ cookiecutter.__project_slug }}",
|
||||
"hexdoc",
|
||||
include = [
|
||||
"{{ cookiecutter.modid }}",
|
||||
"hexcasting",
|
||||
"patchouli",
|
||||
]
|
||||
|
||||
[template.args]
|
||||
|
|
|
@ -1,11 +1,23 @@
|
|||
from importlib.resources import Package
|
||||
|
||||
from hexdoc.plugin import LoadResourceDirsImpl, ModVersionImpl, hookimpl
|
||||
from hexdoc.plugin import (
|
||||
HookReturn,
|
||||
LoadJinjaTemplatesImpl,
|
||||
LoadResourceDirsImpl,
|
||||
ModVersionImpl,
|
||||
hookimpl,
|
||||
)
|
||||
|
||||
import {{ cookiecutter.__project_slug }}
|
||||
|
||||
from .__gradle_version__ import GRADLE_VERSION
|
||||
|
||||
|
||||
class {{ cookiecutter.plugin_classname }}(LoadResourceDirsImpl, ModVersionImpl):
|
||||
class {{ cookiecutter.plugin_classname }}(
|
||||
LoadJinjaTemplatesImpl,
|
||||
LoadResourceDirsImpl,
|
||||
ModVersionImpl,
|
||||
):
|
||||
@staticmethod
|
||||
@hookimpl
|
||||
def hexdoc_mod_version() -> str:
|
||||
|
@ -13,9 +25,14 @@ class {{ cookiecutter.plugin_classname }}(LoadResourceDirsImpl, ModVersionImpl):
|
|||
|
||||
@staticmethod
|
||||
@hookimpl
|
||||
def hexdoc_load_resource_dirs() -> Package | list[Package]:
|
||||
def hexdoc_load_resource_dirs() -> HookReturn[Package]:
|
||||
# This needs to be a lazy import because they may not exist when this file is
|
||||
# first loaded, eg. when generating the contents of generated.
|
||||
from ._export import generated, resources
|
||||
|
||||
return [generated, resources]
|
||||
|
||||
@staticmethod
|
||||
@hookimpl
|
||||
def hexdoc_load_jinja_templates() -> HookReturn[tuple[Package, str]]:
|
||||
return {{ cookiecutter.__project_slug }}, "_templates"
|
||||
|
|
Loading…
Reference in a new issue