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.
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.
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
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
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
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
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
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
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"
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.
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.
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.
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
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
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
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 )
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.
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.
The module(s) that contain your plugin's Minecraft resources to be rendered.
For example: your_plugin._export.generated
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
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
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.
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
.
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
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 )
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
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
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
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.
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.
The Minecraft mod version that this plugin represents.
This should generally return your_plugin.__gradle_version__.GRADLE_VERSION
.
For example: 0.11.1-7
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
Decorator for marking functions as hook implementations.