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."""
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.
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.
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.
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.
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.
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.
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.
29@dataclass(kw_only=True) 30class ModPlugin(ABC): 31 """Hexdoc plugin hooks that are tied to a specific Minecraft mod. 32 33 If you want to render a web book, subclass `ModPluginWithBook` instead. 34 35 Abstract methods are required. All other methods can optionally be implemented to 36 override or add functionality to hexdoc. 37 38 Non-mod-specific hooks are implemented with normal Pluggy hooks instead. 39 """ 40 41 branch: str 42 props: Properties | None = None 43 44 # required hooks 45 46 @property 47 @abstractmethod 48 def modid(self) -> str: 49 """The modid of the Minecraft mod version that this plugin represents. 50 51 For example: `hexcasting` 52 """ 53 54 @property 55 @abstractmethod 56 def full_version(self) -> str: 57 """The full PyPI version of this plugin. 58 59 This should generally return `your_plugin.__gradle_version__.FULL_VERSION`. 60 61 For example: `0.11.1.1.0rc7.dev20` 62 """ 63 64 @property 65 @abstractmethod 66 def plugin_version(self) -> str: 67 """The hexdoc-specific component of this plugin's version number. 68 69 This should generally return `your_plugin.__version__.PY_VERSION`. 70 71 For example: `1.0.dev20` 72 """ 73 74 # optional hooks 75 76 @property 77 def compat_minecraft_version(self) -> str | None: 78 """The version of Minecraft supported by the mod that this plugin represents. 79 80 If no plugins implement this, models and validation for all Minecraft versions 81 may be used. Currently, if two or more plugins provide different values, an 82 error will be raised. 83 84 This should generally return `your_plugin.__gradle_version__.MINECRAFT_VERSION`. 85 86 For example: `1.20.1` 87 """ 88 return None 89 90 @property 91 def mod_version(self) -> str | None: 92 """The Minecraft mod version that this plugin represents. 93 94 This should generally return `your_plugin.__gradle_version__.GRADLE_VERSION`. 95 96 For example: `0.11.1-7` 97 """ 98 return None 99 100 def resource_dirs(self) -> HookReturn[Package]: 101 """The module(s) that contain your plugin's Minecraft resources to be rendered. 102 103 For example: `your_plugin._export.generated` 104 """ 105 return [] 106 107 def jinja_template_root(self) -> HookReturn[tuple[Package, str]] | None: 108 """The module that contains the folder with your plugin's Jinja templates, and 109 the name of that folder. 110 111 For example: `your_plugin, "_templates"` 112 """ 113 return None 114 115 def default_rendered_templates(self) -> DefaultRenderedTemplates: 116 """Extra templates to be rendered by default when your plugin is active. 117 118 The key is the output path, and the value is the template to import and render. 119 It may also be a tuple where the first item is the template and the second is 120 a dict to be merged with the arguments for that template. 121 122 This hook is not called if `props.template.render` is set, since that option 123 overrides all default templates. 124 """ 125 return {} 126 127 def default_rendered_templates_v2( 128 self, 129 book: Any, 130 context: ContextSource, 131 ) -> DefaultRenderedTemplates: 132 """Like `default_rendered_templates`, but gets access to the book and context. 133 134 This is useful for dynamically generating multi-file output structures. 135 """ 136 return {} 137 138 def update_jinja_env(self, env: SandboxedEnvironment) -> None: 139 """Modify the Jinja environment/configuration. 140 141 This is called after hexdoc is done setting up the Jinja environment but before 142 rendering the book. 143 """ 144 145 def pre_render_site( 146 self, 147 props: Properties, 148 books: list[LoadedBookInfo], 149 env: SandboxedEnvironment, 150 output_dir: Path, 151 ) -> None: 152 """Called once, after hexdoc is done setting up the Jinja environment but before 153 rendering the book.""" 154 155 def post_render_site( 156 self, 157 props: Properties, 158 books: list[LoadedBookInfo], 159 env: SandboxedEnvironment, 160 output_dir: Path, 161 ) -> None: 162 """Called once, at the end of `hexdoc build`.""" 163 164 def pre_render_book(self, template_args: dict[str, Any], output_dir: Path) -> None: 165 """Called once per language, just before rendering the book for that 166 language.""" 167 168 def post_render_book(self, template_args: dict[str, Any], output_dir: Path) -> None: 169 """Called once per language, after all book files for that language are 170 rendered.""" 171 172 # utils 173 174 def site_path(self, versioned: bool): 175 if versioned: 176 return self.versioned_site_path 177 return self.latest_site_path 178 179 @property 180 def site_root(self) -> Path: 181 """Base path for all rendered web pages. 182 183 For example: 184 * URL: `https://gamma-delta.github.io/HexMod/v/0.11.1-7/1.0.dev20/en_us` 185 * value: `v` 186 """ 187 return Path("v") 188 189 @property 190 def versioned_site_path(self) -> Path: 191 """Base path for the web pages for the current version. 192 193 For example: 194 * URL: `https://hexdoc.hexxy.media/book/v/1!0.1.0.dev0` (decoded) 195 * value: `book/v/1!0.1.0.dev0` 196 """ 197 return self.site_root / self.full_version 198 199 @property 200 def latest_site_path(self) -> Path: 201 """Base path for the latest web pages for a given branch. 202 203 For example: 204 * URL: `https://gamma-delta.github.io/HexMod/v/latest/main/en_us` 205 * value: `v/latest/main` 206 """ 207 return self.site_root / "latest" / self.branch 208 209 def asset_loader( 210 self, 211 loader: ModResourceLoader, 212 *, 213 site_url: URL, 214 asset_url: URL, 215 render_dir: Path, 216 ) -> HexdocAssetLoader: 217 # unfortunately, this is necessary to avoid some *real* ugly circular imports 218 from hexdoc.minecraft.assets import HexdocAssetLoader 219 220 return HexdocAssetLoader( 221 loader=loader, 222 site_url=site_url, 223 asset_url=asset_url, 224 render_dir=render_dir, 225 ) 226 227 @property 228 def flags(self) -> dict[str, bool]: 229 """Set default values for Patchouli config flags. 230 231 Unlike the table in `hexdoc.toml`, this is available for use in dependents. 232 """ 233 return {}
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.
46 @property 47 @abstractmethod 48 def modid(self) -> str: 49 """The modid of the Minecraft mod version that this plugin represents. 50 51 For example: `hexcasting` 52 """
The modid of the Minecraft mod version that this plugin represents.
For example: hexcasting
54 @property 55 @abstractmethod 56 def full_version(self) -> str: 57 """The full PyPI version of this plugin. 58 59 This should generally return `your_plugin.__gradle_version__.FULL_VERSION`. 60 61 For example: `0.11.1.1.0rc7.dev20` 62 """
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
64 @property 65 @abstractmethod 66 def plugin_version(self) -> str: 67 """The hexdoc-specific component of this plugin's version number. 68 69 This should generally return `your_plugin.__version__.PY_VERSION`. 70 71 For example: `1.0.dev20` 72 """
The hexdoc-specific component of this plugin's version number.
This should generally return your_plugin.__version__.PY_VERSION.
For example: 1.0.dev20
76 @property 77 def compat_minecraft_version(self) -> str | None: 78 """The version of Minecraft supported by the mod that this plugin represents. 79 80 If no plugins implement this, models and validation for all Minecraft versions 81 may be used. Currently, if two or more plugins provide different values, an 82 error will be raised. 83 84 This should generally return `your_plugin.__gradle_version__.MINECRAFT_VERSION`. 85 86 For example: `1.20.1` 87 """ 88 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
90 @property 91 def mod_version(self) -> str | None: 92 """The Minecraft mod version that this plugin represents. 93 94 This should generally return `your_plugin.__gradle_version__.GRADLE_VERSION`. 95 96 For example: `0.11.1-7` 97 """ 98 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
100 def resource_dirs(self) -> HookReturn[Package]: 101 """The module(s) that contain your plugin's Minecraft resources to be rendered. 102 103 For example: `your_plugin._export.generated` 104 """ 105 return []
The module(s) that contain your plugin's Minecraft resources to be rendered.
For example: your_plugin._export.generated
107 def jinja_template_root(self) -> HookReturn[tuple[Package, str]] | None: 108 """The module that contains the folder with your plugin's Jinja templates, and 109 the name of that folder. 110 111 For example: `your_plugin, "_templates"` 112 """ 113 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"
115 def default_rendered_templates(self) -> DefaultRenderedTemplates: 116 """Extra templates to be rendered by default when your plugin is active. 117 118 The key is the output path, and the value is the template to import and render. 119 It may also be a tuple where the first item is the template and the second is 120 a dict to be merged with the arguments for that template. 121 122 This hook is not called if `props.template.render` is set, since that option 123 overrides all default templates. 124 """ 125 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.
127 def default_rendered_templates_v2( 128 self, 129 book: Any, 130 context: ContextSource, 131 ) -> DefaultRenderedTemplates: 132 """Like `default_rendered_templates`, but gets access to the book and context. 133 134 This is useful for dynamically generating multi-file output structures. 135 """ 136 return {}
Like default_rendered_templates, but gets access to the book and context.
This is useful for dynamically generating multi-file output structures.
138 def update_jinja_env(self, env: SandboxedEnvironment) -> None: 139 """Modify the Jinja environment/configuration. 140 141 This is called after hexdoc is done setting up the Jinja environment but before 142 rendering the book. 143 """
Modify the Jinja environment/configuration.
This is called after hexdoc is done setting up the Jinja environment but before rendering the book.
145 def pre_render_site( 146 self, 147 props: Properties, 148 books: list[LoadedBookInfo], 149 env: SandboxedEnvironment, 150 output_dir: Path, 151 ) -> None: 152 """Called once, after hexdoc is done setting up the Jinja environment but before 153 rendering the book."""
Called once, after hexdoc is done setting up the Jinja environment but before rendering the book.
155 def post_render_site( 156 self, 157 props: Properties, 158 books: list[LoadedBookInfo], 159 env: SandboxedEnvironment, 160 output_dir: Path, 161 ) -> None: 162 """Called once, at the end of `hexdoc build`."""
Called once, at the end of hexdoc build.
164 def pre_render_book(self, template_args: dict[str, Any], output_dir: Path) -> None: 165 """Called once per language, just before rendering the book for that 166 language."""
Called once per language, just before rendering the book for that language.
168 def post_render_book(self, template_args: dict[str, Any], output_dir: Path) -> None: 169 """Called once per language, after all book files for that language are 170 rendered."""
Called once per language, after all book files for that language are rendered.
179 @property 180 def site_root(self) -> Path: 181 """Base path for all rendered web pages. 182 183 For example: 184 * URL: `https://gamma-delta.github.io/HexMod/v/0.11.1-7/1.0.dev20/en_us` 185 * value: `v` 186 """ 187 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
189 @property 190 def versioned_site_path(self) -> Path: 191 """Base path for the web pages for the current version. 192 193 For example: 194 * URL: `https://hexdoc.hexxy.media/book/v/1!0.1.0.dev0` (decoded) 195 * value: `book/v/1!0.1.0.dev0` 196 """ 197 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
199 @property 200 def latest_site_path(self) -> Path: 201 """Base path for the latest web pages for a given branch. 202 203 For example: 204 * URL: `https://gamma-delta.github.io/HexMod/v/latest/main/en_us` 205 * value: `v/latest/main` 206 """ 207 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
209 def asset_loader( 210 self, 211 loader: ModResourceLoader, 212 *, 213 site_url: URL, 214 asset_url: URL, 215 render_dir: Path, 216 ) -> HexdocAssetLoader: 217 # unfortunately, this is necessary to avoid some *real* ugly circular imports 218 from hexdoc.minecraft.assets import HexdocAssetLoader 219 220 return HexdocAssetLoader( 221 loader=loader, 222 site_url=site_url, 223 asset_url=asset_url, 224 render_dir=render_dir, 225 )
227 @property 228 def flags(self) -> dict[str, bool]: 229 """Set default values for Patchouli config flags. 230 231 Unlike the table in `hexdoc.toml`, this is available for use in dependents. 232 """ 233 return {}
Set default values for Patchouli config flags.
Unlike the table in hexdoc.toml, this is available for use in dependents.
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.
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.
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.
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.
256class ModPluginWithBook(VersionedModPlugin): 257 """Like `ModPlugin`, but with extra hooks to support rendering a web book.""" 258 259 @abstractmethod 260 @override 261 def resource_dirs(self) -> HookReturn[Package]: ... 262 263 def site_book_path(self, lang: str, versioned: bool) -> Path: 264 if versioned: 265 return self.versioned_site_book_path(lang) 266 return self.latest_site_book_path(lang) 267 268 def versioned_site_book_path(self, lang: str) -> Path: 269 """Base path for the rendered web book for the current version. 270 271 For example: 272 * URL: `https://gamma-delta.github.io/HexMod/v/0.11.1-7/1.0.dev20/en_us` 273 * value: `v/0.11.1-7/1.0.dev20/en_us` 274 """ 275 return self.versioned_site_path / lang 276 277 def latest_site_book_path(self, lang: str) -> Path: 278 """Base path for the latest rendered web book for a given branch. 279 280 For example: 281 * URL: `https://gamma-delta.github.io/HexMod/v/latest/main/en_us` 282 * value: `v/latest/main/en_us` 283 """ 284 return self.latest_site_path / lang
Like ModPlugin, but with extra hooks to support rendering a web book.
The module(s) that contain your plugin's Minecraft resources to be rendered.
For example: your_plugin._export.generated
268 def versioned_site_book_path(self, lang: str) -> Path: 269 """Base path for the rendered web book for the current version. 270 271 For example: 272 * URL: `https://gamma-delta.github.io/HexMod/v/0.11.1-7/1.0.dev20/en_us` 273 * value: `v/0.11.1-7/1.0.dev20/en_us` 274 """ 275 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
277 def latest_site_book_path(self, lang: str) -> Path: 278 """Base path for the latest rendered web book for a given branch. 279 280 For example: 281 * URL: `https://gamma-delta.github.io/HexMod/v/latest/main/en_us` 282 * value: `v/latest/main/en_us` 283 """ 284 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
97class PluginManager(ValidationContext): 98 """Custom hexdoc plugin manager with helpers and stronger typing.""" 99 100 def __init__(self, branch: str, props: Properties, load: bool = True) -> None: 101 """Initialize the hexdoc plugin manager. 102 103 If `load` is true (the default), calls `init_entrypoints` and `init_mod_plugins`. 104 """ 105 self.branch = branch 106 self.props = props 107 self.inner = pluggy.PluginManager(HEXDOC_PROJECT_NAME) 108 self.mod_plugins: dict[str, ModPlugin] = {} 109 self.book_plugins: dict[str, BookPlugin[Any]] = {} 110 111 self.inner.add_hookspecs(PluginSpec) 112 if load: 113 self.init_entrypoints() 114 self.init_plugins() 115 116 def init_entrypoints(self): 117 self.inner.load_setuptools_entrypoints(HEXDOC_PROJECT_NAME) 118 self.inner.check_pending() 119 120 def init_plugins(self): 121 self._init_book_plugins() 122 self._init_mod_plugins() 123 124 def _init_book_plugins(self): 125 caller = self._hook_caller(PluginSpec.hexdoc_book_plugin) 126 for plugin in flatten(caller.try_call()): 127 self.book_plugins[plugin.modid] = plugin 128 129 def _init_mod_plugins(self): 130 caller = self._hook_caller(PluginSpec.hexdoc_mod_plugin) 131 for plugin in flatten( 132 caller.try_call( 133 branch=self.branch, 134 props=self.props, 135 ) 136 ): 137 self.mod_plugins[plugin.modid] = plugin 138 139 def register(self, plugin: Any, name: str | None = None): 140 self.inner.register(plugin, name) 141 self.init_plugins() 142 143 def book_plugin(self, modid: str): 144 plugin = self.book_plugins.get(modid) 145 if plugin is None: 146 raise ValueError(f"No BookPlugin registered for modid: {modid}") 147 return plugin 148 149 @overload 150 def mod_plugin(self, modid: str, book: Literal[True]) -> ModPluginWithBook: ... 151 152 @overload 153 def mod_plugin(self, modid: str, book: bool = False) -> ModPlugin: ... 154 155 def mod_plugin(self, modid: str, book: bool = False): 156 plugin = self.mod_plugins.get(modid) 157 if plugin is None: 158 raise ValueError(f"No ModPlugin registered for modid: {modid}") 159 160 if book and not isinstance(plugin, ModPluginWithBook): 161 raise ValueError( 162 f"ModPlugin registered for modid `{modid}`" 163 f" does not inherit from ModPluginWithBook: {plugin}" 164 ) 165 166 return plugin 167 168 def mod_plugin_with_book(self, modid: str): 169 return self.mod_plugin(modid, book=True) 170 171 def minecraft_version(self) -> str | None: 172 versions = dict[str, str]() 173 174 for modid, plugin in self.mod_plugins.items(): 175 version = plugin.compat_minecraft_version 176 if version is not None: 177 versions[modid] = version 178 179 match len(set(versions.values())): 180 case 0: 181 return None 182 case 1: 183 return versions.popitem()[1] 184 case n: 185 raise ValueError( 186 f"Got {n} Minecraft versions, expected 1: " 187 + ", ".join( 188 f"{modid}={version}" for modid, version in versions.items() 189 ) 190 ) 191 192 def validate_format_tree( 193 self, 194 tree: FormatTree, 195 macros: dict[str, str], 196 book_id: ResourceLocation, 197 i18n: I18n, 198 is_0_black: bool, 199 link_overrides: dict[str, str], 200 ): 201 caller = self._hook_caller(PluginSpec.hexdoc_validate_format_tree) 202 caller.try_call( 203 tree=tree, 204 macros=macros, 205 book_id=book_id, 206 i18n=i18n, 207 is_0_black=is_0_black, 208 link_overrides=link_overrides, 209 ) 210 return tree 211 212 def update_context(self, context: dict[str, Any]) -> Iterator[ValidationContext]: 213 caller = self._hook_caller(PluginSpec.hexdoc_update_context) 214 if returns := caller.try_call(context=context): 215 yield from flatten(returns) 216 217 def update_jinja_env(self, modids: Sequence[str], env: SandboxedEnvironment): 218 for modid in modids: 219 plugin = self.mod_plugin(modid) 220 plugin.update_jinja_env(env) 221 return env 222 223 def pre_render_site( 224 self, 225 books: list[LoadedBookInfo], 226 env: SandboxedEnvironment, 227 output_dir: Path, 228 modids: Sequence[str], 229 ): 230 for modid in modids: 231 self.mod_plugin(modid).pre_render_site(self.props, books, env, output_dir) 232 233 def post_render_site( 234 self, 235 books: list[LoadedBookInfo], 236 env: SandboxedEnvironment, 237 output_dir: Path, 238 modids: Sequence[str], 239 ): 240 for modid in modids: 241 self.mod_plugin(modid).post_render_site(self.props, books, env, output_dir) 242 243 def update_template_args(self, template_args: dict[str, Any]): 244 caller = self._hook_caller(PluginSpec.hexdoc_update_template_args) 245 caller.try_call(template_args=template_args) 246 return template_args 247 248 def pre_render_book( 249 self, 250 template_args: dict[str, Any], 251 output_dir: Path, 252 modids: Sequence[str], 253 ): 254 for modid in modids: 255 self.mod_plugin(modid).pre_render_book(template_args, output_dir) 256 257 def post_render_book( 258 self, 259 template_args: dict[str, Any], 260 output_dir: Path, 261 modids: Sequence[str], 262 ): 263 for modid in modids: 264 self.mod_plugin(modid).post_render_book(template_args, output_dir) 265 266 def load_resources(self, modid: str) -> Iterator[ModuleType]: 267 plugin = self.mod_plugin(modid) 268 for package in flatten([plugin.resource_dirs()]): 269 yield import_package(package) 270 271 def load_tagged_unions(self) -> Iterator[ModuleType]: 272 yield from self._import_from_hook(PluginSpec.hexdoc_load_tagged_unions) 273 274 def load_jinja_templates(self, modids: Sequence[str]): 275 """modid -> PackageLoader""" 276 extra_modids = set(self.mod_plugins.keys()) - set(modids) 277 278 included = self._package_loaders_for(modids) 279 extra = self._package_loaders_for(extra_modids) 280 281 return included, extra 282 283 def _package_loaders_for(self, modids: Iterable[str]): 284 loaders = dict[str, BaseLoader]() 285 286 for modid in modids: 287 plugin = self.mod_plugin(modid) 288 289 result = plugin.jinja_template_root() 290 if not result: 291 continue 292 293 loaders[modid] = ChoiceLoader( 294 [ 295 PackageLoader( 296 package_name=import_package(package).__name__, 297 package_path=package_path, 298 ) 299 for package, package_path in flatten([result]) 300 ] 301 ) 302 303 return loaders 304 305 def default_rendered_templates( 306 self, 307 modids: Iterable[str], 308 book: Any, 309 context: ContextSource, 310 ): 311 templates: DefaultRenderedTemplates = {} 312 for modid in modids: 313 plugin = self.mod_plugin(modid) 314 templates |= plugin.default_rendered_templates() 315 templates |= plugin.default_rendered_templates_v2(book, context) 316 317 result = dict[Path, tuple[str, Mapping[str, Any]]]() 318 for path, value in templates.items(): 319 match value: 320 case str(template): 321 args = dict[str, Any]() 322 case (template, args): 323 pass 324 result[Path(path)] = (template, args) 325 326 return result 327 328 def _import_from_hook( 329 self, 330 __spec: Callable[_P, HookReturns[Package]], 331 *args: _P.args, 332 **kwargs: _P.kwargs, 333 ) -> Iterator[ModuleType]: 334 packages = self._hook_caller(__spec)(*args, **kwargs) 335 for package in flatten(packages): 336 yield import_package(package) 337 338 def load_flags(self) -> dict[str, bool]: 339 # https://vazkiimods.github.io/Patchouli/docs/patchouli-basics/config-gating 340 flags = { 341 "debug": False, 342 "advancements_disabled": False, 343 "testing_mode": False, 344 } 345 346 # first, resolve exported flags by OR 347 for modid, plugin in self.mod_plugins.items(): 348 for flag, value in plugin.flags.items(): 349 if flag in flags: 350 resolved = flags[flag] = flags[flag] or value 351 logger.debug( 352 f"{modid} exports flag {flag}={value}, resolving to {resolved}" 353 ) 354 else: 355 flags[flag] = value 356 logger.debug(f"{modid} exports flag {flag}={value}") 357 358 # then, apply local overrides 359 flags |= self.props.flags 360 361 # any missing flags will default to True 362 363 logger.debug(f"Resolved flags: {flags}") 364 return flags 365 366 @overload 367 def _hook_caller(self, spec: Callable[_P, None]) -> _NoCallTypedHookCaller[_P]: ... 368 369 @overload 370 def _hook_caller( 371 self, spec: Callable[_P, _R | None] 372 ) -> TypedHookCaller[_P, _R]: ... 373 374 def _hook_caller(self, spec: Callable[_P, _R | None]) -> TypedHookCaller[_P, _R]: 375 caller = self.inner.hook.__dict__[spec.__name__] 376 return TypedHookCaller(None, caller)
Custom hexdoc plugin manager with helpers and stronger typing.
100 def __init__(self, branch: str, props: Properties, load: bool = True) -> None: 101 """Initialize the hexdoc plugin manager. 102 103 If `load` is true (the default), calls `init_entrypoints` and `init_mod_plugins`. 104 """ 105 self.branch = branch 106 self.props = props 107 self.inner = pluggy.PluginManager(HEXDOC_PROJECT_NAME) 108 self.mod_plugins: dict[str, ModPlugin] = {} 109 self.book_plugins: dict[str, BookPlugin[Any]] = {} 110 111 self.inner.add_hookspecs(PluginSpec) 112 if load: 113 self.init_entrypoints() 114 self.init_plugins()
Initialize the hexdoc plugin manager.
If load is true (the default), calls init_entrypoints and init_mod_plugins.
155 def mod_plugin(self, modid: str, book: bool = False): 156 plugin = self.mod_plugins.get(modid) 157 if plugin is None: 158 raise ValueError(f"No ModPlugin registered for modid: {modid}") 159 160 if book and not isinstance(plugin, ModPluginWithBook): 161 raise ValueError( 162 f"ModPlugin registered for modid `{modid}`" 163 f" does not inherit from ModPluginWithBook: {plugin}" 164 ) 165 166 return plugin
171 def minecraft_version(self) -> str | None: 172 versions = dict[str, str]() 173 174 for modid, plugin in self.mod_plugins.items(): 175 version = plugin.compat_minecraft_version 176 if version is not None: 177 versions[modid] = version 178 179 match len(set(versions.values())): 180 case 0: 181 return None 182 case 1: 183 return versions.popitem()[1] 184 case n: 185 raise ValueError( 186 f"Got {n} Minecraft versions, expected 1: " 187 + ", ".join( 188 f"{modid}={version}" for modid, version in versions.items() 189 ) 190 )
192 def validate_format_tree( 193 self, 194 tree: FormatTree, 195 macros: dict[str, str], 196 book_id: ResourceLocation, 197 i18n: I18n, 198 is_0_black: bool, 199 link_overrides: dict[str, str], 200 ): 201 caller = self._hook_caller(PluginSpec.hexdoc_validate_format_tree) 202 caller.try_call( 203 tree=tree, 204 macros=macros, 205 book_id=book_id, 206 i18n=i18n, 207 is_0_black=is_0_black, 208 link_overrides=link_overrides, 209 ) 210 return tree
274 def load_jinja_templates(self, modids: Sequence[str]): 275 """modid -> PackageLoader""" 276 extra_modids = set(self.mod_plugins.keys()) - set(modids) 277 278 included = self._package_loaders_for(modids) 279 extra = self._package_loaders_for(extra_modids) 280 281 return included, extra
modid -> PackageLoader
305 def default_rendered_templates( 306 self, 307 modids: Iterable[str], 308 book: Any, 309 context: ContextSource, 310 ): 311 templates: DefaultRenderedTemplates = {} 312 for modid in modids: 313 plugin = self.mod_plugin(modid) 314 templates |= plugin.default_rendered_templates() 315 templates |= plugin.default_rendered_templates_v2(book, context) 316 317 result = dict[Path, tuple[str, Mapping[str, Any]]]() 318 for path, value in templates.items(): 319 match value: 320 case str(template): 321 args = dict[str, Any]() 322 case (template, args): 323 pass 324 result[Path(path)] = (template, args) 325 326 return result
338 def load_flags(self) -> dict[str, bool]: 339 # https://vazkiimods.github.io/Patchouli/docs/patchouli-basics/config-gating 340 flags = { 341 "debug": False, 342 "advancements_disabled": False, 343 "testing_mode": False, 344 } 345 346 # first, resolve exported flags by OR 347 for modid, plugin in self.mod_plugins.items(): 348 for flag, value in plugin.flags.items(): 349 if flag in flags: 350 resolved = flags[flag] = flags[flag] or value 351 logger.debug( 352 f"{modid} exports flag {flag}={value}, resolving to {resolved}" 353 ) 354 else: 355 flags[flag] = value 356 logger.debug(f"{modid} exports flag {flag}={value}") 357 358 # then, apply local overrides 359 flags |= self.props.flags 360 361 # any missing flags will default to True 362 363 logger.debug(f"Resolved flags: {flags}") 364 return flags
Unspecified run-time error.
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.
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.
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.
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).
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.
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.
236class VersionedModPlugin(ModPlugin): 237 """Like `ModPlugin`, but the versioned site path uses the plugin and mod version.""" 238 239 @property 240 @abstractmethod 241 @override 242 def mod_version(self) -> str: ... 243 244 @property 245 @override 246 def versioned_site_path(self) -> Path: 247 """Base path for the web pages for the current version. 248 249 For example: 250 * URL: `https://gamma-delta.github.io/HexMod/v/0.11.1-7/1.0.dev20/en_us` 251 * value: `v/0.11.1-7/1.0.dev20` 252 """ 253 return self.site_root / self.mod_version / self.plugin_version
Like ModPlugin, but the versioned site path uses the plugin and mod version.
The Minecraft mod version that this plugin represents.
This should generally return your_plugin.__gradle_version__.GRADLE_VERSION.
For example: 0.11.1-7
244 @property 245 @override 246 def versioned_site_path(self) -> Path: 247 """Base path for the web pages for the current version. 248 249 For example: 250 * URL: `https://gamma-delta.github.io/HexMod/v/0.11.1-7/1.0.dev20/en_us` 251 * value: `v/0.11.1-7/1.0.dev20` 252 """ 253 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
Decorator for marking functions as hook implementations.