Edit on GitHub

hexdoc.plugin

 1__all__ = [
 2    "HEXDOC_PROJECT_NAME",
 3    "BookPlugin",
 4    "BookPluginImpl",
 5    "DefaultRenderedTemplates",
 6    "HookReturn",
 7    "LoadTaggedUnionsImpl",
 8    "ModPlugin",
 9    "ModPluginImpl",
10    "ModPluginImplWithProps",
11    "ModPluginWithBook",
12    "PluginManager",
13    "PluginNotFoundError",
14    "UpdateContextImpl",
15    "UpdateTemplateArgsImpl",
16    "ValidateFormatTreeImpl",
17    "VersionedModPlugin",
18    "hookimpl",
19]
20
21import pluggy
22
23from .book_plugin import BookPlugin
24from .manager import (
25    PluginManager,
26    PluginNotFoundError,
27)
28from .mod_plugin import (
29    DefaultRenderedTemplates,
30    ModPlugin,
31    ModPluginWithBook,
32    VersionedModPlugin,
33)
34from .specs import (
35    HEXDOC_PROJECT_NAME,
36    BookPluginImpl,
37    LoadTaggedUnionsImpl,
38    ModPluginImpl,
39    ModPluginImplWithProps,
40    UpdateContextImpl,
41    UpdateTemplateArgsImpl,
42    ValidateFormatTreeImpl,
43)
44from .types import HookReturn
45
46hookimpl = pluggy.HookimplMarker(HEXDOC_PROJECT_NAME)
47"""Decorator for marking functions as hook implementations."""
HEXDOC_PROJECT_NAME = 'hexdoc'
class BookPlugin(abc.ABC, typing.Generic[~_Book]):
15class BookPlugin(ABC, Generic[_Book]):
16    @property
17    @abstractmethod
18    def modid(self) -> str:
19        """The modid of the mod whose book system this plugin implements."""
20
21    @abstractmethod
22    def load_book_data(
23        self,
24        book_id: ResourceLocation,
25        loader: ModResourceLoader,
26    ) -> tuple[ResourceLocation, JSONDict]:
27        """"""
28
29    @abstractmethod
30    def is_i18n_enabled(self, book_data: Mapping[str, Any]) -> bool:
31        """Given the raw book data, returns `True` if i18n is enabled for that book."""
32
33    @abstractmethod
34    def validate_book(
35        self,
36        book_data: Mapping[str, Any],
37        *,
38        context: ContextSource,
39    ) -> _Book:
40        """"""

Helper class that provides a standard way to create an ABC using inheritance.

modid: str
16    @property
17    @abstractmethod
18    def modid(self) -> str:
19        """The modid of the mod whose book system this plugin implements."""

The modid of the mod whose book system this plugin implements.

@abstractmethod
def load_book_data( self, book_id: hexdoc.core.ResourceLocation, loader: hexdoc.core.ModResourceLoader) -> tuple[hexdoc.core.ResourceLocation, dict[str, JsonValue]]:
21    @abstractmethod
22    def load_book_data(
23        self,
24        book_id: ResourceLocation,
25        loader: ModResourceLoader,
26    ) -> tuple[ResourceLocation, JSONDict]:
27        """"""
@abstractmethod
def is_i18n_enabled(self, book_data: Mapping[str, Any]) -> bool:
29    @abstractmethod
30    def is_i18n_enabled(self, book_data: Mapping[str, Any]) -> bool:
31        """Given the raw book data, returns `True` if i18n is enabled for that book."""

Given the raw book data, returns True if i18n is enabled for that book.

@abstractmethod
def validate_book( self, book_data: Mapping[str, Any], *, context: dict[str, typing.Any] | pydantic_core.core_schema.ValidationInfo | jinja2.runtime.Context) -> ~_Book:
33    @abstractmethod
34    def validate_book(
35        self,
36        book_data: Mapping[str, Any],
37        *,
38        context: ContextSource,
39    ) -> _Book:
40        """"""
class BookPluginImpl(hexdoc.plugin.specs.PluginImpl, typing.Protocol):
71class BookPluginImpl(PluginImpl, Protocol):
72    @staticmethod
73    def hexdoc_book_plugin() -> HookReturn[BookPlugin[Any]]:
74        """If your plugin represents a book system (like Patchouli), this must return an
75        instance of a subclass of `BookPlugin` with all abstract methods implemented."""
76        ...

Interface for an implementation of a hexdoc plugin hook.

These protocols are optional - they gives better type checking, but everything will work fine with a standard pluggy hook implementation.

@staticmethod
def hexdoc_book_plugin() -> Union[BookPlugin[Any], list[BookPlugin[Any]]]:
72    @staticmethod
73    def hexdoc_book_plugin() -> HookReturn[BookPlugin[Any]]:
74        """If your plugin represents a book system (like Patchouli), this must return an
75        instance of a subclass of `BookPlugin` with all abstract methods implemented."""
76        ...

If your plugin represents a book system (like Patchouli), this must return an instance of a subclass of BookPlugin with all abstract methods implemented.

DefaultRenderedTemplates = typing.Mapping[str | pathlib.Path, str | tuple[str, typing.Mapping[str, typing.Any]]]
HookReturn = typing.Union[~_T, list[~_T]]
class LoadTaggedUnionsImpl(hexdoc.plugin.specs.PluginImpl, typing.Protocol):
132class LoadTaggedUnionsImpl(PluginImpl, Protocol):
133    @staticmethod
134    def hexdoc_load_tagged_unions() -> HookReturn[Package]:
135        """Return the module(s) which contain your plugin's tagged union subtypes."""
136        ...

Interface for an implementation of a hexdoc plugin hook.

These protocols are optional - they gives better type checking, but everything will work fine with a standard pluggy hook implementation.

@staticmethod
def hexdoc_load_tagged_unions() -> Union[module, str, list[Union[module, str]]]:
133    @staticmethod
134    def hexdoc_load_tagged_unions() -> HookReturn[Package]:
135        """Return the module(s) which contain your plugin's tagged union subtypes."""
136        ...

Return the module(s) which contain your plugin's tagged union subtypes.

@dataclass(kw_only=True)
class ModPlugin(abc.ABC):
 28@dataclass(kw_only=True)
 29class ModPlugin(ABC):
 30    """Hexdoc plugin hooks that are tied to a specific Minecraft mod.
 31
 32    If you want to render a web book, subclass `ModPluginWithBook` instead.
 33
 34    Abstract methods are required. All other methods can optionally be implemented to
 35    override or add functionality to hexdoc.
 36
 37    Non-mod-specific hooks are implemented with normal Pluggy hooks instead.
 38    """
 39
 40    branch: str
 41    props: Properties | None = None
 42
 43    # required hooks
 44
 45    @property
 46    @abstractmethod
 47    def modid(self) -> str:
 48        """The modid of the Minecraft mod version that this plugin represents.
 49
 50        For example: `hexcasting`
 51        """
 52
 53    @property
 54    @abstractmethod
 55    def full_version(self) -> str:
 56        """The full PyPI version of this plugin.
 57
 58        This should generally return `your_plugin.__gradle_version__.FULL_VERSION`.
 59
 60        For example: `0.11.1.1.0rc7.dev20`
 61        """
 62
 63    @property
 64    @abstractmethod
 65    def plugin_version(self) -> str:
 66        """The hexdoc-specific component of this plugin's version number.
 67
 68        This should generally return `your_plugin.__version__.PY_VERSION`.
 69
 70        For example: `1.0.dev20`
 71        """
 72
 73    # optional hooks
 74
 75    @property
 76    def compat_minecraft_version(self) -> str | None:
 77        """The version of Minecraft supported by the mod that this plugin represents.
 78
 79        If no plugins implement this, models and validation for all Minecraft versions
 80        may be used. Currently, if two or more plugins provide different values, an
 81        error will be raised.
 82
 83        This should generally return `your_plugin.__gradle_version__.MINECRAFT_VERSION`.
 84
 85        For example: `1.20.1`
 86        """
 87        return None
 88
 89    @property
 90    def mod_version(self) -> str | None:
 91        """The Minecraft mod version that this plugin represents.
 92
 93        This should generally return `your_plugin.__gradle_version__.GRADLE_VERSION`.
 94
 95        For example: `0.11.1-7`
 96        """
 97        return None
 98
 99    def resource_dirs(self) -> HookReturn[Package]:
100        """The module(s) that contain your plugin's Minecraft resources to be rendered.
101
102        For example: `your_plugin._export.generated`
103        """
104        return []
105
106    def jinja_template_root(self) -> HookReturn[tuple[Package, str]] | None:
107        """The module that contains the folder with your plugin's Jinja templates, and
108        the name of that folder.
109
110        For example: `your_plugin, "_templates"`
111        """
112        return None
113
114    def default_rendered_templates(self) -> DefaultRenderedTemplates:
115        """Extra templates to be rendered by default when your plugin is active.
116
117        The key is the output path, and the value is the template to import and render.
118        It may also be a tuple where the first item is the template and the second is
119        a dict to be merged with the arguments for that template.
120
121        This hook is not called if `props.template.render` is set, since that option
122        overrides all default templates.
123        """
124        return {}
125
126    def default_rendered_templates_v2(
127        self,
128        book: Any,
129        context: ContextSource,
130    ) -> DefaultRenderedTemplates:
131        """Like `default_rendered_templates`, but gets access to the book and context.
132
133        This is useful for dynamically generating multi-file output structures.
134        """
135        return {}
136
137    def update_jinja_env(self, env: SandboxedEnvironment) -> None:
138        """Modify the Jinja environment/configuration.
139
140        This is called after hexdoc is done setting up the Jinja environment but before
141        rendering the book.
142        """
143
144    # utils
145
146    def site_path(self, versioned: bool):
147        if versioned:
148            return self.versioned_site_path
149        return self.latest_site_path
150
151    @property
152    def site_root(self) -> Path:
153        """Base path for all rendered web pages.
154
155        For example:
156        * URL: `https://gamma-delta.github.io/HexMod/v/0.11.1-7/1.0.dev20/en_us`
157        * value: `v`
158        """
159        return Path("v")
160
161    @property
162    def versioned_site_path(self) -> Path:
163        """Base path for the web pages for the current version.
164
165        For example:
166        * URL: `https://hexdoc.hexxy.media/book/v/1!0.1.0.dev0` (decoded)
167        * value: `book/v/1!0.1.0.dev0`
168        """
169        return self.site_root / self.full_version
170
171    @property
172    def latest_site_path(self) -> Path:
173        """Base path for the latest web pages for a given branch.
174
175        For example:
176        * URL: `https://gamma-delta.github.io/HexMod/v/latest/main/en_us`
177        * value: `v/latest/main`
178        """
179        return self.site_root / "latest" / self.branch
180
181    def asset_loader(
182        self,
183        loader: ModResourceLoader,
184        *,
185        site_url: URL,
186        asset_url: URL,
187        render_dir: Path,
188    ) -> HexdocAssetLoader:
189        # unfortunately, this is necessary to avoid some *real* ugly circular imports
190        from hexdoc.minecraft.assets import HexdocAssetLoader
191
192        return HexdocAssetLoader(
193            loader=loader,
194            site_url=site_url,
195            asset_url=asset_url,
196            render_dir=render_dir,
197        )

Hexdoc plugin hooks that are tied to a specific Minecraft mod.

If you want to render a web book, subclass ModPluginWithBook instead.

Abstract methods are required. All other methods can optionally be implemented to override or add functionality to hexdoc.

Non-mod-specific hooks are implemented with normal Pluggy hooks instead.

branch: str
props: hexdoc.core.Properties | None = None
modid: str
45    @property
46    @abstractmethod
47    def modid(self) -> str:
48        """The modid of the Minecraft mod version that this plugin represents.
49
50        For example: `hexcasting`
51        """

The modid of the Minecraft mod version that this plugin represents.

For example: hexcasting

full_version: str
53    @property
54    @abstractmethod
55    def full_version(self) -> str:
56        """The full PyPI version of this plugin.
57
58        This should generally return `your_plugin.__gradle_version__.FULL_VERSION`.
59
60        For example: `0.11.1.1.0rc7.dev20`
61        """

The full PyPI version of this plugin.

This should generally return your_plugin.__gradle_version__.FULL_VERSION.

For example: 0.11.1.1.0rc7.dev20

plugin_version: str
63    @property
64    @abstractmethod
65    def plugin_version(self) -> str:
66        """The hexdoc-specific component of this plugin's version number.
67
68        This should generally return `your_plugin.__version__.PY_VERSION`.
69
70        For example: `1.0.dev20`
71        """

The hexdoc-specific component of this plugin's version number.

This should generally return your_plugin.__version__.PY_VERSION.

For example: 1.0.dev20

compat_minecraft_version: str | None
75    @property
76    def compat_minecraft_version(self) -> str | None:
77        """The version of Minecraft supported by the mod that this plugin represents.
78
79        If no plugins implement this, models and validation for all Minecraft versions
80        may be used. Currently, if two or more plugins provide different values, an
81        error will be raised.
82
83        This should generally return `your_plugin.__gradle_version__.MINECRAFT_VERSION`.
84
85        For example: `1.20.1`
86        """
87        return None

The version of Minecraft supported by the mod that this plugin represents.

If no plugins implement this, models and validation for all Minecraft versions may be used. Currently, if two or more plugins provide different values, an error will be raised.

This should generally return your_plugin.__gradle_version__.MINECRAFT_VERSION.

For example: 1.20.1

mod_version: str | None
89    @property
90    def mod_version(self) -> str | None:
91        """The Minecraft mod version that this plugin represents.
92
93        This should generally return `your_plugin.__gradle_version__.GRADLE_VERSION`.
94
95        For example: `0.11.1-7`
96        """
97        return None

The Minecraft mod version that this plugin represents.

This should generally return your_plugin.__gradle_version__.GRADLE_VERSION.

For example: 0.11.1-7

def resource_dirs(self) -> Union[module, str, list[Union[module, str]]]:
 99    def resource_dirs(self) -> HookReturn[Package]:
100        """The module(s) that contain your plugin's Minecraft resources to be rendered.
101
102        For example: `your_plugin._export.generated`
103        """
104        return []

The module(s) that contain your plugin's Minecraft resources to be rendered.

For example: your_plugin._export.generated

def jinja_template_root( self) -> Union[tuple[Union[module, str], str], list[tuple[Union[module, str], str]], NoneType]:
106    def jinja_template_root(self) -> HookReturn[tuple[Package, str]] | None:
107        """The module that contains the folder with your plugin's Jinja templates, and
108        the name of that folder.
109
110        For example: `your_plugin, "_templates"`
111        """
112        return None

The module that contains the folder with your plugin's Jinja templates, and the name of that folder.

For example: your_plugin, "_templates"

def default_rendered_templates(self) -> Mapping[str | pathlib.Path, str | tuple[str, Mapping[str, Any]]]:
114    def default_rendered_templates(self) -> DefaultRenderedTemplates:
115        """Extra templates to be rendered by default when your plugin is active.
116
117        The key is the output path, and the value is the template to import and render.
118        It may also be a tuple where the first item is the template and the second is
119        a dict to be merged with the arguments for that template.
120
121        This hook is not called if `props.template.render` is set, since that option
122        overrides all default templates.
123        """
124        return {}

Extra templates to be rendered by default when your plugin is active.

The key is the output path, and the value is the template to import and render. It may also be a tuple where the first item is the template and the second is a dict to be merged with the arguments for that template.

This hook is not called if props.template.render is set, since that option overrides all default templates.

def default_rendered_templates_v2( self, book: Any, context: dict[str, typing.Any] | pydantic_core.core_schema.ValidationInfo | jinja2.runtime.Context) -> Mapping[str | pathlib.Path, str | tuple[str, Mapping[str, Any]]]:
126    def default_rendered_templates_v2(
127        self,
128        book: Any,
129        context: ContextSource,
130    ) -> DefaultRenderedTemplates:
131        """Like `default_rendered_templates`, but gets access to the book and context.
132
133        This is useful for dynamically generating multi-file output structures.
134        """
135        return {}

Like default_rendered_templates, but gets access to the book and context.

This is useful for dynamically generating multi-file output structures.

def update_jinja_env(self, env: jinja2.sandbox.SandboxedEnvironment) -> None:
137    def update_jinja_env(self, env: SandboxedEnvironment) -> None:
138        """Modify the Jinja environment/configuration.
139
140        This is called after hexdoc is done setting up the Jinja environment but before
141        rendering the book.
142        """

Modify the Jinja environment/configuration.

This is called after hexdoc is done setting up the Jinja environment but before rendering the book.

def site_path(self, versioned: bool):
146    def site_path(self, versioned: bool):
147        if versioned:
148            return self.versioned_site_path
149        return self.latest_site_path
site_root: pathlib.Path
151    @property
152    def site_root(self) -> Path:
153        """Base path for all rendered web pages.
154
155        For example:
156        * URL: `https://gamma-delta.github.io/HexMod/v/0.11.1-7/1.0.dev20/en_us`
157        * value: `v`
158        """
159        return Path("v")

Base path for all rendered web pages.

For example:

  • URL: https://gamma-delta.github.io/HexMod/v/0.11.1-7/1.0.dev20/en_us
  • value: v
versioned_site_path: pathlib.Path
161    @property
162    def versioned_site_path(self) -> Path:
163        """Base path for the web pages for the current version.
164
165        For example:
166        * URL: `https://hexdoc.hexxy.media/book/v/1!0.1.0.dev0` (decoded)
167        * value: `book/v/1!0.1.0.dev0`
168        """
169        return self.site_root / self.full_version

Base path for the web pages for the current version.

For example:

  • URL: https://hexdoc.hexxy.media/book/v/1!0.1.0.dev0 (decoded)
  • value: book/v/1!0.1.0.dev0
latest_site_path: pathlib.Path
171    @property
172    def latest_site_path(self) -> Path:
173        """Base path for the latest web pages for a given branch.
174
175        For example:
176        * URL: `https://gamma-delta.github.io/HexMod/v/latest/main/en_us`
177        * value: `v/latest/main`
178        """
179        return self.site_root / "latest" / self.branch

Base path for the latest web pages for a given branch.

For example:

  • URL: https://gamma-delta.github.io/HexMod/v/latest/main/en_us
  • value: v/latest/main
def asset_loader( self, loader: hexdoc.core.ModResourceLoader, *, site_url: yarl.URL, asset_url: yarl.URL, render_dir: pathlib.Path) -> hexdoc.minecraft.assets.HexdocAssetLoader:
181    def asset_loader(
182        self,
183        loader: ModResourceLoader,
184        *,
185        site_url: URL,
186        asset_url: URL,
187        render_dir: Path,
188    ) -> HexdocAssetLoader:
189        # unfortunately, this is necessary to avoid some *real* ugly circular imports
190        from hexdoc.minecraft.assets import HexdocAssetLoader
191
192        return HexdocAssetLoader(
193            loader=loader,
194            site_url=site_url,
195            asset_url=asset_url,
196            render_dir=render_dir,
197        )
class ModPluginImpl(hexdoc.plugin.specs.PluginImpl, typing.Protocol):
79class ModPluginImpl(PluginImpl, Protocol):
80    @staticmethod
81    def hexdoc_mod_plugin(branch: str) -> HookReturn[ModPlugin]:
82        """If your plugin represents a Minecraft mod, this must return an instance of a
83        subclass of `ModPlugin` or `ModPluginWithBook`, with all abstract methods
84        implemented."""
85        ...

Interface for an implementation of a hexdoc plugin hook.

These protocols are optional - they gives better type checking, but everything will work fine with a standard pluggy hook implementation.

@staticmethod
def hexdoc_mod_plugin( branch: str) -> Union[ModPlugin, list[ModPlugin]]:
80    @staticmethod
81    def hexdoc_mod_plugin(branch: str) -> HookReturn[ModPlugin]:
82        """If your plugin represents a Minecraft mod, this must return an instance of a
83        subclass of `ModPlugin` or `ModPluginWithBook`, with all abstract methods
84        implemented."""
85        ...

If your plugin represents a Minecraft mod, this must return an instance of a subclass of ModPlugin or ModPluginWithBook, with all abstract methods implemented.

class ModPluginImplWithProps(hexdoc.plugin.specs.PluginImpl, typing.Protocol):
88class ModPluginImplWithProps(PluginImpl, Protocol):
89    @staticmethod
90    def hexdoc_mod_plugin(branch: str, props: Properties) -> HookReturn[ModPlugin]:
91        """If your plugin represents a Minecraft mod, this must return an instance of a
92        subclass of `ModPlugin` or `ModPluginWithBook`, with all abstract methods
93        implemented."""
94        ...

Interface for an implementation of a hexdoc plugin hook.

These protocols are optional - they gives better type checking, but everything will work fine with a standard pluggy hook implementation.

@staticmethod
def hexdoc_mod_plugin( branch: str, props: hexdoc.core.Properties) -> Union[ModPlugin, list[ModPlugin]]:
89    @staticmethod
90    def hexdoc_mod_plugin(branch: str, props: Properties) -> HookReturn[ModPlugin]:
91        """If your plugin represents a Minecraft mod, this must return an instance of a
92        subclass of `ModPlugin` or `ModPluginWithBook`, with all abstract methods
93        implemented."""
94        ...

If your plugin represents a Minecraft mod, this must return an instance of a subclass of ModPlugin or ModPluginWithBook, with all abstract methods implemented.

class ModPluginWithBook(hexdoc.plugin.VersionedModPlugin):
220class ModPluginWithBook(VersionedModPlugin):
221    """Like `ModPlugin`, but with extra hooks to support rendering a web book."""
222
223    @abstractmethod
224    @override
225    def resource_dirs(self) -> HookReturn[Package]: ...
226
227    def site_book_path(self, lang: str, versioned: bool) -> Path:
228        if versioned:
229            return self.versioned_site_book_path(lang)
230        return self.latest_site_book_path(lang)
231
232    def versioned_site_book_path(self, lang: str) -> Path:
233        """Base path for the rendered web book for the current version.
234
235        For example:
236        * URL: `https://gamma-delta.github.io/HexMod/v/0.11.1-7/1.0.dev20/en_us`
237        * value: `v/0.11.1-7/1.0.dev20/en_us`
238        """
239        return self.versioned_site_path / lang
240
241    def latest_site_book_path(self, lang: str) -> Path:
242        """Base path for the latest rendered web book for a given branch.
243
244        For example:
245        * URL: `https://gamma-delta.github.io/HexMod/v/latest/main/en_us`
246        * value: `v/latest/main/en_us`
247        """
248        return self.latest_site_path / lang

Like ModPlugin, but with extra hooks to support rendering a web book.

@abstractmethod
@override
def resource_dirs(self) -> Union[module, str, list[Union[module, str]]]:
223    @abstractmethod
224    @override
225    def resource_dirs(self) -> HookReturn[Package]: ...

The module(s) that contain your plugin's Minecraft resources to be rendered.

For example: your_plugin._export.generated

def site_book_path(self, lang: str, versioned: bool) -> pathlib.Path:
227    def site_book_path(self, lang: str, versioned: bool) -> Path:
228        if versioned:
229            return self.versioned_site_book_path(lang)
230        return self.latest_site_book_path(lang)
def versioned_site_book_path(self, lang: str) -> pathlib.Path:
232    def versioned_site_book_path(self, lang: str) -> Path:
233        """Base path for the rendered web book for the current version.
234
235        For example:
236        * URL: `https://gamma-delta.github.io/HexMod/v/0.11.1-7/1.0.dev20/en_us`
237        * value: `v/0.11.1-7/1.0.dev20/en_us`
238        """
239        return self.versioned_site_path / lang

Base path for the rendered web book for the current version.

For example:

  • URL: https://gamma-delta.github.io/HexMod/v/0.11.1-7/1.0.dev20/en_us
  • value: v/0.11.1-7/1.0.dev20/en_us
def latest_site_book_path(self, lang: str) -> pathlib.Path:
241    def latest_site_book_path(self, lang: str) -> Path:
242        """Base path for the latest rendered web book for a given branch.
243
244        For example:
245        * URL: `https://gamma-delta.github.io/HexMod/v/latest/main/en_us`
246        * value: `v/latest/main/en_us`
247        """
248        return self.latest_site_path / lang

Base path for the latest rendered web book for a given branch.

For example:

  • URL: https://gamma-delta.github.io/HexMod/v/latest/main/en_us
  • value: v/latest/main/en_us
class PluginManager(hexdoc.utils.context.ValidationContext):
 92class PluginManager(ValidationContext):
 93    """Custom hexdoc plugin manager with helpers and stronger typing."""
 94
 95    def __init__(self, branch: str, props: Properties, load: bool = True) -> None:
 96        """Initialize the hexdoc plugin manager.
 97
 98        If `load` is true (the default), calls `init_entrypoints` and `init_mod_plugins`.
 99        """
100        self.branch = branch
101        self.props = props
102        self.inner = pluggy.PluginManager(HEXDOC_PROJECT_NAME)
103        self.mod_plugins: dict[str, ModPlugin] = {}
104        self.book_plugins: dict[str, BookPlugin[Any]] = {}
105
106        self.inner.add_hookspecs(PluginSpec)
107        if load:
108            self.init_entrypoints()
109            self.init_plugins()
110
111    def init_entrypoints(self):
112        self.inner.load_setuptools_entrypoints(HEXDOC_PROJECT_NAME)
113        self.inner.check_pending()
114
115    def init_plugins(self):
116        self._init_book_plugins()
117        self._init_mod_plugins()
118
119    def _init_book_plugins(self):
120        caller = self._hook_caller(PluginSpec.hexdoc_book_plugin)
121        for plugin in flatten(caller.try_call()):
122            self.book_plugins[plugin.modid] = plugin
123
124    def _init_mod_plugins(self):
125        caller = self._hook_caller(PluginSpec.hexdoc_mod_plugin)
126        for plugin in flatten(
127            caller.try_call(
128                branch=self.branch,
129                props=self.props,
130            )
131        ):
132            self.mod_plugins[plugin.modid] = plugin
133
134    def register(self, plugin: Any, name: str | None = None):
135        self.inner.register(plugin, name)
136        self.init_plugins()
137
138    def book_plugin(self, modid: str):
139        plugin = self.book_plugins.get(modid)
140        if plugin is None:
141            raise ValueError(f"No BookPlugin registered for modid: {modid}")
142        return plugin
143
144    @overload
145    def mod_plugin(self, modid: str, book: Literal[True]) -> ModPluginWithBook: ...
146
147    @overload
148    def mod_plugin(self, modid: str, book: bool = False) -> ModPlugin: ...
149
150    def mod_plugin(self, modid: str, book: bool = False):
151        plugin = self.mod_plugins.get(modid)
152        if plugin is None:
153            raise ValueError(f"No ModPlugin registered for modid: {modid}")
154
155        if book and not isinstance(plugin, ModPluginWithBook):
156            raise ValueError(
157                f"ModPlugin registered for modid `{modid}`"
158                f" does not inherit from ModPluginWithBook: {plugin}"
159            )
160
161        return plugin
162
163    def mod_plugin_with_book(self, modid: str):
164        return self.mod_plugin(modid, book=True)
165
166    def minecraft_version(self) -> str | None:
167        versions = dict[str, str]()
168
169        for modid, plugin in self.mod_plugins.items():
170            version = plugin.compat_minecraft_version
171            if version is not None:
172                versions[modid] = version
173
174        match len(set(versions.values())):
175            case 0:
176                return None
177            case 1:
178                return versions.popitem()[1]
179            case n:
180                raise ValueError(
181                    f"Got {n} Minecraft versions, expected 1: "
182                    + ", ".join(
183                        f"{modid}={version}" for modid, version in versions.items()
184                    )
185                )
186
187    def validate_format_tree(
188        self,
189        tree: FormatTree,
190        macros: dict[str, str],
191        book_id: ResourceLocation,
192        i18n: I18n,
193        is_0_black: bool,
194        link_overrides: dict[str, str],
195    ):
196        caller = self._hook_caller(PluginSpec.hexdoc_validate_format_tree)
197        caller.try_call(
198            tree=tree,
199            macros=macros,
200            book_id=book_id,
201            i18n=i18n,
202            is_0_black=is_0_black,
203            link_overrides=link_overrides,
204        )
205        return tree
206
207    def update_context(self, context: dict[str, Any]) -> Iterator[ValidationContext]:
208        caller = self._hook_caller(PluginSpec.hexdoc_update_context)
209        if returns := caller.try_call(context=context):
210            yield from flatten(returns)
211
212    def update_jinja_env(self, env: SandboxedEnvironment, modids: Sequence[str]):
213        for modid in modids:
214            plugin = self.mod_plugin(modid)
215            plugin.update_jinja_env(env)
216        return env
217
218    def update_template_args(self, template_args: dict[str, Any]):
219        caller = self._hook_caller(PluginSpec.hexdoc_update_template_args)
220        caller.try_call(template_args=template_args)
221        return template_args
222
223    def load_resources(self, modid: str) -> Iterator[ModuleType]:
224        plugin = self.mod_plugin(modid)
225        for package in flatten([plugin.resource_dirs()]):
226            yield import_package(package)
227
228    def load_tagged_unions(self) -> Iterator[ModuleType]:
229        yield from self._import_from_hook(PluginSpec.hexdoc_load_tagged_unions)
230
231    def load_jinja_templates(self, modids: Sequence[str]):
232        """modid -> PackageLoader"""
233        extra_modids = set(self.mod_plugins.keys()) - set(modids)
234
235        included = self._package_loaders_for(modids)
236        extra = self._package_loaders_for(extra_modids)
237
238        return included, extra
239
240    def _package_loaders_for(self, modids: Iterable[str]):
241        loaders = dict[str, BaseLoader]()
242
243        for modid in modids:
244            plugin = self.mod_plugin(modid)
245
246            result = plugin.jinja_template_root()
247            if not result:
248                continue
249
250            loaders[modid] = ChoiceLoader(
251                [
252                    PackageLoader(
253                        package_name=import_package(package).__name__,
254                        package_path=package_path,
255                    )
256                    for package, package_path in flatten([result])
257                ]
258            )
259
260        return loaders
261
262    def default_rendered_templates(
263        self,
264        modids: Iterable[str],
265        book: Any,
266        context: ContextSource,
267    ):
268        templates: DefaultRenderedTemplates = {}
269        for modid in modids:
270            plugin = self.mod_plugin(modid)
271            templates |= plugin.default_rendered_templates()
272            templates |= plugin.default_rendered_templates_v2(book, context)
273
274        result = dict[Path, tuple[str, Mapping[str, Any]]]()
275        for path, value in templates.items():
276            match value:
277                case str(template):
278                    args = dict[str, Any]()
279                case (template, args):
280                    pass
281            result[Path(path)] = (template, args)
282
283        return result
284
285    def _import_from_hook(
286        self,
287        __spec: Callable[_P, HookReturns[Package]],
288        *args: _P.args,
289        **kwargs: _P.kwargs,
290    ) -> Iterator[ModuleType]:
291        packages = self._hook_caller(__spec)(*args, **kwargs)
292        for package in flatten(packages):
293            yield import_package(package)
294
295    @overload
296    def _hook_caller(self, spec: Callable[_P, None]) -> _NoCallTypedHookCaller[_P]: ...
297
298    @overload
299    def _hook_caller(
300        self, spec: Callable[_P, _R | None]
301    ) -> TypedHookCaller[_P, _R]: ...
302
303    def _hook_caller(self, spec: Callable[_P, _R | None]) -> TypedHookCaller[_P, _R]:
304        caller = self.inner.hook.__dict__[spec.__name__]
305        return TypedHookCaller(None, caller)

Custom hexdoc plugin manager with helpers and stronger typing.

PluginManager( branch: str, props: hexdoc.core.Properties, load: bool = True)
 95    def __init__(self, branch: str, props: Properties, load: bool = True) -> None:
 96        """Initialize the hexdoc plugin manager.
 97
 98        If `load` is true (the default), calls `init_entrypoints` and `init_mod_plugins`.
 99        """
100        self.branch = branch
101        self.props = props
102        self.inner = pluggy.PluginManager(HEXDOC_PROJECT_NAME)
103        self.mod_plugins: dict[str, ModPlugin] = {}
104        self.book_plugins: dict[str, BookPlugin[Any]] = {}
105
106        self.inner.add_hookspecs(PluginSpec)
107        if load:
108            self.init_entrypoints()
109            self.init_plugins()

Initialize the hexdoc plugin manager.

If load is true (the default), calls init_entrypoints and init_mod_plugins.

branch
props
inner
mod_plugins: dict[str, ModPlugin]
book_plugins: dict[str, BookPlugin[typing.Any]]
def init_entrypoints(self):
111    def init_entrypoints(self):
112        self.inner.load_setuptools_entrypoints(HEXDOC_PROJECT_NAME)
113        self.inner.check_pending()
def init_plugins(self):
115    def init_plugins(self):
116        self._init_book_plugins()
117        self._init_mod_plugins()
def register(self, plugin: Any, name: str | None = None):
134    def register(self, plugin: Any, name: str | None = None):
135        self.inner.register(plugin, name)
136        self.init_plugins()
def book_plugin(self, modid: str):
138    def book_plugin(self, modid: str):
139        plugin = self.book_plugins.get(modid)
140        if plugin is None:
141            raise ValueError(f"No BookPlugin registered for modid: {modid}")
142        return plugin
def mod_plugin(self, modid: str, book: bool = False):
150    def mod_plugin(self, modid: str, book: bool = False):
151        plugin = self.mod_plugins.get(modid)
152        if plugin is None:
153            raise ValueError(f"No ModPlugin registered for modid: {modid}")
154
155        if book and not isinstance(plugin, ModPluginWithBook):
156            raise ValueError(
157                f"ModPlugin registered for modid `{modid}`"
158                f" does not inherit from ModPluginWithBook: {plugin}"
159            )
160
161        return plugin
def mod_plugin_with_book(self, modid: str):
163    def mod_plugin_with_book(self, modid: str):
164        return self.mod_plugin(modid, book=True)
def minecraft_version(self) -> str | None:
166    def minecraft_version(self) -> str | None:
167        versions = dict[str, str]()
168
169        for modid, plugin in self.mod_plugins.items():
170            version = plugin.compat_minecraft_version
171            if version is not None:
172                versions[modid] = version
173
174        match len(set(versions.values())):
175            case 0:
176                return None
177            case 1:
178                return versions.popitem()[1]
179            case n:
180                raise ValueError(
181                    f"Got {n} Minecraft versions, expected 1: "
182                    + ", ".join(
183                        f"{modid}={version}" for modid, version in versions.items()
184                    )
185                )
def validate_format_tree( self, tree: hexdoc.patchouli.FormatTree, macros: dict[str, str], book_id: hexdoc.core.ResourceLocation, i18n: hexdoc.minecraft.I18n, is_0_black: bool, link_overrides: dict[str, str]):
187    def validate_format_tree(
188        self,
189        tree: FormatTree,
190        macros: dict[str, str],
191        book_id: ResourceLocation,
192        i18n: I18n,
193        is_0_black: bool,
194        link_overrides: dict[str, str],
195    ):
196        caller = self._hook_caller(PluginSpec.hexdoc_validate_format_tree)
197        caller.try_call(
198            tree=tree,
199            macros=macros,
200            book_id=book_id,
201            i18n=i18n,
202            is_0_black=is_0_black,
203            link_overrides=link_overrides,
204        )
205        return tree
def update_context( self, context: dict[str, typing.Any]) -> Iterator[hexdoc.utils.ValidationContext]:
207    def update_context(self, context: dict[str, Any]) -> Iterator[ValidationContext]:
208        caller = self._hook_caller(PluginSpec.hexdoc_update_context)
209        if returns := caller.try_call(context=context):
210            yield from flatten(returns)
def update_jinja_env( self, env: jinja2.sandbox.SandboxedEnvironment, modids: Sequence[str]):
212    def update_jinja_env(self, env: SandboxedEnvironment, modids: Sequence[str]):
213        for modid in modids:
214            plugin = self.mod_plugin(modid)
215            plugin.update_jinja_env(env)
216        return env
def update_template_args(self, template_args: dict[str, typing.Any]):
218    def update_template_args(self, template_args: dict[str, Any]):
219        caller = self._hook_caller(PluginSpec.hexdoc_update_template_args)
220        caller.try_call(template_args=template_args)
221        return template_args
def load_resources(self, modid: str) -> Iterator[module]:
223    def load_resources(self, modid: str) -> Iterator[ModuleType]:
224        plugin = self.mod_plugin(modid)
225        for package in flatten([plugin.resource_dirs()]):
226            yield import_package(package)
def load_tagged_unions(self) -> Iterator[module]:
228    def load_tagged_unions(self) -> Iterator[ModuleType]:
229        yield from self._import_from_hook(PluginSpec.hexdoc_load_tagged_unions)
def load_jinja_templates(self, modids: Sequence[str]):
231    def load_jinja_templates(self, modids: Sequence[str]):
232        """modid -> PackageLoader"""
233        extra_modids = set(self.mod_plugins.keys()) - set(modids)
234
235        included = self._package_loaders_for(modids)
236        extra = self._package_loaders_for(extra_modids)
237
238        return included, extra

modid -> PackageLoader

def default_rendered_templates( self, modids: Iterable[str], book: Any, context: dict[str, typing.Any] | pydantic_core.core_schema.ValidationInfo | jinja2.runtime.Context):
262    def default_rendered_templates(
263        self,
264        modids: Iterable[str],
265        book: Any,
266        context: ContextSource,
267    ):
268        templates: DefaultRenderedTemplates = {}
269        for modid in modids:
270            plugin = self.mod_plugin(modid)
271            templates |= plugin.default_rendered_templates()
272            templates |= plugin.default_rendered_templates_v2(book, context)
273
274        result = dict[Path, tuple[str, Mapping[str, Any]]]()
275        for path, value in templates.items():
276            match value:
277                case str(template):
278                    args = dict[str, Any]()
279                case (template, args):
280                    pass
281            result[Path(path)] = (template, args)
282
283        return result
class PluginNotFoundError(builtins.RuntimeError):
47class PluginNotFoundError(RuntimeError):
48    pass

Unspecified run-time error.

class UpdateContextImpl(hexdoc.plugin.specs.PluginImpl, typing.Protocol):
115class UpdateContextImpl(PluginImpl, Protocol):
116    @staticmethod
117    def hexdoc_update_context(context: dict[str, Any]) -> HookReturn[ValidationContext]:
118        """Modify the book validation context.
119
120        For example, Hex Casting uses this to add pattern data needed by pattern pages.
121        """
122        ...

Interface for an implementation of a hexdoc plugin hook.

These protocols are optional - they gives better type checking, but everything will work fine with a standard pluggy hook implementation.

@staticmethod
def hexdoc_update_context( context: dict[str, typing.Any]) -> Union[hexdoc.utils.ValidationContext, list[hexdoc.utils.ValidationContext]]:
116    @staticmethod
117    def hexdoc_update_context(context: dict[str, Any]) -> HookReturn[ValidationContext]:
118        """Modify the book validation context.
119
120        For example, Hex Casting uses this to add pattern data needed by pattern pages.
121        """
122        ...

Modify the book validation context.

For example, Hex Casting uses this to add pattern data needed by pattern pages.

class UpdateTemplateArgsImpl(hexdoc.plugin.specs.PluginImpl, typing.Protocol):
125class UpdateTemplateArgsImpl(PluginImpl, Protocol):
126    @staticmethod
127    def hexdoc_update_template_args(template_args: dict[str, Any]) -> None:
128        """Add extra template args (global variables for the Jinja templates)."""
129        ...

Interface for an implementation of a hexdoc plugin hook.

These protocols are optional - they gives better type checking, but everything will work fine with a standard pluggy hook implementation.

@staticmethod
def hexdoc_update_template_args(template_args: dict[str, typing.Any]) -> None:
126    @staticmethod
127    def hexdoc_update_template_args(template_args: dict[str, Any]) -> None:
128        """Add extra template args (global variables for the Jinja templates)."""
129        ...

Add extra template args (global variables for the Jinja templates).

class ValidateFormatTreeImpl(hexdoc.plugin.specs.PluginImpl, typing.Protocol):
 97class ValidateFormatTreeImpl(PluginImpl, Protocol):
 98    @staticmethod
 99    def hexdoc_validate_format_tree(
100        tree: FormatTree,
101        macros: dict[str, str],
102        book_id: ResourceLocation,
103        i18n: I18n,
104        is_0_black: bool,
105        link_overrides: dict[str, str],
106    ) -> None:
107        """This is called as the last step when a FormatTree (styled Patchouli text) is
108        generated. You can use this to modify or validate the text and styles.
109
110        For example, Hex Casting uses this to ensure all $(action) styles are in a link.
111        """
112        ...

Interface for an implementation of a hexdoc plugin hook.

These protocols are optional - they gives better type checking, but everything will work fine with a standard pluggy hook implementation.

@staticmethod
def hexdoc_validate_format_tree( tree: hexdoc.patchouli.FormatTree, macros: dict[str, str], book_id: hexdoc.core.ResourceLocation, i18n: hexdoc.minecraft.I18n, is_0_black: bool, link_overrides: dict[str, str]) -> None:
 98    @staticmethod
 99    def hexdoc_validate_format_tree(
100        tree: FormatTree,
101        macros: dict[str, str],
102        book_id: ResourceLocation,
103        i18n: I18n,
104        is_0_black: bool,
105        link_overrides: dict[str, str],
106    ) -> None:
107        """This is called as the last step when a FormatTree (styled Patchouli text) is
108        generated. You can use this to modify or validate the text and styles.
109
110        For example, Hex Casting uses this to ensure all $(action) styles are in a link.
111        """
112        ...

This is called as the last step when a FormatTree (styled Patchouli text) is generated. You can use this to modify or validate the text and styles.

For example, Hex Casting uses this to ensure all $(action) styles are in a link.

class VersionedModPlugin(hexdoc.plugin.ModPlugin):
200class VersionedModPlugin(ModPlugin):
201    """Like `ModPlugin`, but the versioned site path uses the plugin and mod version."""
202
203    @property
204    @abstractmethod
205    @override
206    def mod_version(self) -> str: ...
207
208    @property
209    @override
210    def versioned_site_path(self) -> Path:
211        """Base path for the web pages for the current version.
212
213        For example:
214        * URL: `https://gamma-delta.github.io/HexMod/v/0.11.1-7/1.0.dev20/en_us`
215        * value: `v/0.11.1-7/1.0.dev20`
216        """
217        return self.site_root / self.mod_version / self.plugin_version

Like ModPlugin, but the versioned site path uses the plugin and mod version.

mod_version: str
203    @property
204    @abstractmethod
205    @override
206    def mod_version(self) -> str: ...

The Minecraft mod version that this plugin represents.

This should generally return your_plugin.__gradle_version__.GRADLE_VERSION.

For example: 0.11.1-7

versioned_site_path: pathlib.Path
208    @property
209    @override
210    def versioned_site_path(self) -> Path:
211        """Base path for the web pages for the current version.
212
213        For example:
214        * URL: `https://gamma-delta.github.io/HexMod/v/0.11.1-7/1.0.dev20/en_us`
215        * value: `v/0.11.1-7/1.0.dev20`
216        """
217        return self.site_root / self.mod_version / self.plugin_version

Base path for the web pages for the current version.

For example:

  • URL: https://gamma-delta.github.io/HexMod/v/0.11.1-7/1.0.dev20/en_us
  • value: v/0.11.1-7/1.0.dev20
hookimpl = <pluggy._hooks.HookimplMarker object>

Decorator for marking functions as hook implementations.