Skip to content

Commit 32cd528

Browse files
author
Laurent Franceschetti
committed
Test the hooks for external registration (#237)
- added the register_macros test case - reimplemented the .variables, .macros and .filters properties for MacrosDocProject - updated documentation, to document hooks scripts (including comparison with MkDocs-Macros modules)
1 parent 16be58d commit 32cd528

File tree

12 files changed

+350
-48
lines changed

12 files changed

+350
-48
lines changed

mkdocs_macros/plugin.py

Lines changed: 27 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,7 @@
4343
# The default name of the Python module:
4444
DEFAULT_MODULE_NAME = 'main' # main.py
4545

46-
# the directory where the rendered macros must go
47-
RENDERED_MACROS_DIRNAME = '__docs_macros_rendered'
46+
4847

4948

5049

@@ -295,20 +294,7 @@ def reverse(x):
295294

296295

297296

298-
@property
299-
def rendered_macros_dir(self):
300-
"""
301-
The directory, beside the docs_dir, that contains
302-
the rendered pages from the macros.
303-
"""
304-
try:
305-
r = self._rendered_macros_dir
306-
except AttributeError:
307-
raise AttributeError("Rendered macros directory is undefined")
308-
if not os.path.isdir(self._rendered_macros_dir):
309-
raise FileNotFoundError("Rendered macros directory is defined "
310-
"but does not exists")
311-
return r
297+
312298

313299

314300
# ------------------------------------------------
@@ -378,6 +364,7 @@ def register_macros(self, items:dict):
378364
Register macros (hook for other plugins).
379365
These will be added last, and raise an exception if already present.
380366
"""
367+
trace(f"Registering external macros: {list(items)}")
381368
try:
382369
# after on_config
383370
self._macros
@@ -393,6 +380,7 @@ def register_filters(self, items:dict):
393380
Register filters (hook for other plugins).
394381
These will be added last, and raise an exception if already present.
395382
"""
383+
trace(f"Registering external filters: {list(items)}")
396384
try:
397385
self._filters
398386
register_items('filter', self.filters, items)
@@ -407,6 +395,7 @@ def register_variables(self, items:dict):
407395
Register variables (hook for other plugins).
408396
These will be added last, and raise an exception if already present.
409397
"""
398+
trace(f"Registering external variables: {list(items)}")
410399
try:
411400
# after on_config
412401
self._variables
@@ -723,6 +712,7 @@ def on_config(self, config):
723712
From the configuration file, builds a Jinj2 environment
724713
with variables, functions and filters.
725714
"""
715+
trace("Configuring the macros environment...")
726716
# WARNING: this is not the config argument:
727717
trace("Macros arguments\n", self.config)
728718
# define the variables and macros as dictionaries
@@ -770,18 +760,7 @@ def on_config(self, config):
770760
register_items('macro' , self.macros , self._add_macros )
771761
register_items('filter' , self.filters , self._add_filters )
772762

773-
# Provide information:
774-
trace("Config variables:", list(self.variables.keys()))
775-
debug("Config variables:\n", payload=json.dumps(self.variables,
776-
cls=CustomEncoder))
777-
if self.macros:
778-
trace("Config macros:", list(self.macros.keys()))
779-
debug("Config macros:", payload=json.dumps(self.macros,
780-
cls=CustomEncoder))
781-
if self.filters:
782-
trace("Config filters:", list(self.filters.keys()))
783-
debug("Config filters:", payload=json.dumps(self.filters,
784-
cls=CustomEncoder))
763+
785764
# if len(extra):
786765
# trace("Extra variables (config file):", list(extra.keys()))
787766
# debug("Content of extra variables (config file):\n", dict(extra))
@@ -856,16 +835,26 @@ def on_config(self, config):
856835
# update environment with the custom filters:
857836
self.env.filters.update(self.filters)
858837

859-
# -------------------
860-
# Setup the markdown (rendered) directory
861-
# -------------------
862-
docs_dir = config['docs_dir']
863-
abs_docs_dir = os.path.abspath(docs_dir)
864-
# recreate only if debug (otherewise delete):
865-
recreate = get_log_level('DEBUG')
866-
self._rendered_macros_dir = setup_directory(abs_docs_dir,
867-
RENDERED_MACROS_DIRNAME,
868-
recreate=recreate)
838+
trace("End of environment config")
839+
840+
def on_pre_build(self, *, config):
841+
"""
842+
Provide information on the variables.
843+
It is put here, in case some plugin hooks into the config,
844+
after the execution of the `on_config()` of this plugin.
845+
"""
846+
trace("Config variables:", list(self.variables.keys()))
847+
debug("Config variables:\n", payload=json.dumps(self.variables,
848+
cls=CustomEncoder))
849+
if self.macros:
850+
trace("Config macros:", list(self.macros.keys()))
851+
debug("Config macros:", payload=json.dumps(self.macros,
852+
cls=CustomEncoder))
853+
if self.filters:
854+
trace("Config filters:", list(self.filters.keys()))
855+
debug("Config filters:", payload=json.dumps(self.filters,
856+
cls=CustomEncoder))
857+
869858

870859
def on_nav(self, nav, config, files):
871860
"""

mkdocs_macros/util.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ def default(self, obj: Any) -> Any:
136136
try:
137137
return super().default(obj)
138138
except TypeError:
139-
print(f"CANNOT INTERPRET {obj.__class__}")
139+
debug(f"json: cannot encode {obj.__class__}")
140140
return str(obj)
141141

142142

test/fixture.py

Lines changed: 66 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@
55
"""
66

77
import warnings
8+
import json
9+
import subprocess
10+
11+
12+
from super_collections import SuperDict
813
from mkdocs_test import DocProject, MkDocsPage
914

1015

@@ -25,18 +30,75 @@ def is_rendered(self):
2530
class MacrosDocProject(DocProject):
2631
"Specific for MkDocs-Macros"
2732

33+
def build(self, strict:bool=False) -> subprocess.CompletedProcess:
34+
"""
35+
Build the documentation, to perform the tests
36+
Verbose is forced to True, to get the variables, functions and filters
37+
"""
38+
super().build(strict=strict, verbose=True)
39+
2840
@property
2941
def pages(self) -> dict[MacrosPage]:
3042
"List of pages"
3143
pages = super().pages
3244
return {key: MacrosPage(value) for key, value in pages.items()}
3345

34-
@property
35-
def variables(self):
36-
"Alias for config.extra"
37-
return self.config.extra
46+
3847

3948
@property
4049
def macros_plugin(self):
4150
"Information on the plugin"
4251
return self.get_plugin('macros')
52+
53+
# ------------------------------------
54+
# Get information through the payload
55+
# ------------------------------------
56+
@property
57+
def variables(self):
58+
"Return the variables"
59+
try:
60+
return self._variables
61+
except AttributeError:
62+
entry = self.find_entry("config variables",
63+
source='macros',
64+
severity='debug')
65+
if entry and entry.payload:
66+
self._variables = SuperDict(json.loads(entry.payload))
67+
else:
68+
print(entry)
69+
raise ValueError("Cannot find variables")
70+
return self._variables
71+
72+
73+
@property
74+
def macros(self):
75+
"Return the macros"
76+
try:
77+
return self._macros
78+
except AttributeError:
79+
entry = self.find_entry("config macros",
80+
source='macros',
81+
severity='debug')
82+
if entry and entry.payload:
83+
self._macros = SuperDict(json.loads(entry.payload))
84+
else:
85+
print(entry)
86+
raise ValueError("Cannot find macros")
87+
return self._macros
88+
89+
90+
@property
91+
def filters(self):
92+
"Return the filters"
93+
try:
94+
return self._filters
95+
except AttributeError:
96+
entry = self.find_entry("config filters",
97+
source='macros',
98+
severity='debug')
99+
if entry and entry.payload:
100+
self._filters = SuperDict(json.loads(entry.payload))
101+
else:
102+
print(entry)
103+
raise ValueError("Cannot find filters")
104+
return self._filters

test/register_macros/__init__.py

Whitespace-only changes.

test/register_macros/docs/index.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
---
2+
message: Vive Zorglub
3+
---
4+
5+
# Main Page
6+
7+
This project contains a registered macro
8+
9+
## Variables
10+
I want to write 'foo': {{ foo }}
11+
12+
x2 is also present: {{ x2 }}
13+
14+
## Macros
15+
16+
Calculation: {{ bar(2, 5) }}
17+
18+
## Filters
19+
20+
I want to scramble '{{ message }}': {{ message | scramble }}

test/register_macros/docs/second.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# Second page
2+
3+
This page shows the information on variables,
4+
to understand, how the functions, variables and filters
5+
have been registered.
6+
7+
8+
{{ macros_info() }}

test/register_macros/hooks.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
def foo(x:int, y:str):
2+
"First macro"
3+
return f"{x} and {y}"
4+
5+
def bar(x:int, y:int):
6+
"Second macro"
7+
return x + y
8+
9+
def scramble(s:str, length:int=None):
10+
"""
11+
Dummy filter to reverse the string and swap the case of each character.
12+
13+
Usage in Markdown page:
14+
15+
{{ "Hello world" | scramble }} -> Dlrow Olleh
16+
{{ "Hello world" | scramble(6) }} -> Dlrow
17+
"""
18+
# Split the phrase into words
19+
words = s.split()
20+
# Reverse each word and then reverse the order of the words
21+
reversed_words = [word[::-1].capitalize() for word in words][::-1]
22+
# Join the reversed words to form the new phrase
23+
new_phrase = ' '.join(reversed_words)
24+
if length:
25+
new_phrase = new_phrase[length]
26+
return new_phrase
27+
28+
29+
MY_FUNCTIONS = {"foo": foo, "bar": bar}
30+
MY_VARIABLES = {"x1": 5, "x2": 'hello world'}
31+
MY_FILTERS = {"scramble": scramble}
32+
33+
34+
def on_config(config, **kwargs):
35+
"Add the functions variables and filters to the mix"
36+
# get MkdocsMacros plugin, but only if present
37+
macros_plugin = config.plugins.get("macros")
38+
if macros_plugin:
39+
macros_plugin.register_macros(MY_FUNCTIONS)
40+
macros_plugin.register_variables(MY_VARIABLES)
41+
macros_plugin.register_filters(MY_FILTERS)
42+
else:
43+
raise SystemError("Cannot find macros plugin!")
44+

test/register_macros/mkdocs.yml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
site_name: Testing the hooks
2+
theme: readthedocs
3+
4+
nav:
5+
- Home: index.md
6+
- Next page: second.md
7+
8+
hooks:
9+
# Mkdocs hook for testing the Mkdocs-Macros hook
10+
- hooks.py
11+
12+
plugins:
13+
- search
14+
- macros
15+
- test
16+
17+
extra:
18+
greeting: Hello World!
19+

test/register_macros/test_doc.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
"""
2+
Testing the project
3+
4+
(C) Laurent Franceschetti 2024
5+
"""
6+
7+
8+
import pytest
9+
10+
from test.fixture import MacrosDocProject
11+
12+
13+
from .hooks import MY_VARIABLES, MY_FUNCTIONS, MY_FILTERS, bar, scramble
14+
15+
16+
def test_pages():
17+
project = MacrosDocProject(".")
18+
build_result = project.build(strict=True)
19+
# did not fail
20+
return_code = project.build_result.returncode
21+
assert not return_code, f"Build returned with {return_code} {build_result.args})"
22+
23+
# check the presence of variables in the environment
24+
print("Variables:", list(project.variables.keys()))
25+
for variable in MY_VARIABLES:
26+
assert variable in project.variables
27+
print(f"{variable}: {project.variables[variable]}")
28+
29+
print("Macros:", list(project.macros.keys()))
30+
for macro in MY_FUNCTIONS:
31+
assert macro in project.macros
32+
print(f"{macro}: {project.macros[macro]}")
33+
34+
print("Filters:", list(project.filters.keys()))
35+
for filter in MY_FILTERS:
36+
assert filter in project.filters
37+
print(f"{filter}: {project.filters[filter]}")
38+
39+
# ----------------
40+
# First page
41+
# ----------------
42+
43+
44+
page = project.get_page('index')
45+
assert page.is_markdown_rendered()
46+
# variable
47+
value = MY_VARIABLES['x2']
48+
print(f"Check if x2 ('{value}') is present")
49+
assert page.find(value, header="Variables")
50+
# macro
51+
print("Check macro: bar")
52+
assert page.find(bar(2, 5), header="Macros")
53+
# filter
54+
message = page.meta.message
55+
result = scramble(message)
56+
print(f"Check filter: scramble('{message}') --> '{result}'")
57+
assert page.find(result, header="Filters")
58+
59+
60+
61+
62+
# ----------------
63+
# Second page
64+
# ----------------
65+
# there is intentionally an error (`foo` does not exist)
66+
page = project.get_page('second')
67+
assert 'foo' not in project.config.extra
68+
assert page.is_markdown_rendered()
69+
assert not page.has_error()
70+

0 commit comments

Comments
 (0)