Skip to content

Commit 252360e

Browse files
committed
Add mgration script
Signed-off-by: Leandro Lucarella <[email protected]>
1 parent 4df0254 commit 252360e

File tree

3 files changed

+297
-2
lines changed

3 files changed

+297
-2
lines changed

cookiecutter/migrate.py

Lines changed: 109 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
And remember to follow any manual instructions for each run.
2121
""" # noqa: E501
2222

23+
import hashlib
2324
import os
2425
import subprocess
2526
import tempfile
@@ -30,8 +31,8 @@
3031
def main() -> None:
3132
"""Run the migration steps."""
3233
add_default_pytest_options()
33-
34-
# Add a separation line like this one after each migration step.
34+
print("=" * 72)
35+
migrate_mkdocs_macros()
3536
print("=" * 72)
3637

3738

@@ -64,6 +65,92 @@ def add_default_pytest_options() -> None:
6465
)
6566

6667

68+
def migrate_mkdocs_macros() -> None:
69+
"""Migrate from custom macros.py to standard module."""
70+
macros_file = Path("docs/_scripts/macros.py")
71+
mkdocs_yaml = Path("mkdocs.yaml")
72+
if not mkdocs_yaml.exists():
73+
mkdocs_yaml = Path("mkdocs.yml")
74+
75+
known_hashes = {
76+
"47a991286132471b6cb666577beb89e78c0f5d4975c53f0dcb319c4338a2c3cb",
77+
"6bb960c72b370ac77918f49d7a35f39c0ddb58fe52cf2d12caa2577098fd8469",
78+
"7351276ac314955a343bab09d1602e50300887291f841643e9fb79c94acc923c",
79+
"8fa5f9f3fd928e17f590e3ab056434474633259d615971404db0d2f3034adb62",
80+
"ba3ff5f1612b3dd22372a8ca95394b8ea468f18dcefc494c73811c8433fcb880",
81+
"dd32e8759abc43232bb3db5b33c0a7cf8d8442db6135c594968c499d8bae0ce5",
82+
}
83+
84+
print("Checking if docs/_scripts/macros.py can be migrated...")
85+
86+
file_hash = calculate_file_sha256_skip_lines(macros_file, 2)
87+
if not file_hash:
88+
return
89+
90+
if file_hash not in known_hashes:
91+
manual_step("The macros.py file seems to be customized. You have two options:")
92+
manual_step("")
93+
manual_step(
94+
"1. Switch to the standard module (if you don't have custom macros):"
95+
)
96+
manual_step(" a. Update mkdocs.yaml to use the standard module:")
97+
manual_step(
98+
' module_name: docs/_scripts/macros -> modules: ["frequenz.repo.config.mkdocs.mkdocstrings_macros"]' # noqa: E501
99+
)
100+
manual_step(" b. Remove docs/_scripts/macros.py")
101+
manual_step("")
102+
manual_step("2. Keep your custom macros but use the standard functionality:")
103+
manual_step(" a. Update mkdocs.yaml:")
104+
manual_step(" - Keep using module_name: docs/_scripts/macros")
105+
manual_step(" b. Update your macros.py to be minimal:")
106+
manual_step(" ```python")
107+
manual_step(
108+
" from frequenz.repo.config.mkdocs.mkdocstrings_macros import hook_env_with_everything" # noqa: E501
109+
)
110+
manual_step("")
111+
manual_step(" def define_env(env):")
112+
manual_step(" # Add your custom variables, filters, and macros here")
113+
manual_step(" env.variables.my_var = 'Example'")
114+
manual_step(" env.filters.my_filter = lambda x: x.upper()")
115+
manual_step("")
116+
manual_step(
117+
" # This must be at the end to enable all standard features"
118+
)
119+
manual_step(" hook_env_with_everything(env)")
120+
manual_step(" ```")
121+
manual_step("")
122+
manual_step("See the docs for more details:")
123+
manual_step(
124+
"https://frequenz-floss.github.io/frequenz-repo-config-python/v0.12/reference/frequenz/repo/config/mkdocs/mkdocstrings_macros/" # noqa: E501
125+
)
126+
return
127+
128+
if not mkdocs_yaml.exists():
129+
print("mkdocs.yaml/yml not found, skipping macros migration")
130+
return
131+
132+
content = mkdocs_yaml.read_text(encoding="utf-8")
133+
if "module_name: docs/_scripts/macros" not in content:
134+
print("Custom macros configuration not found in mkdocs.yaml")
135+
return
136+
137+
print("Updating mkdocs.yaml to use standard module...")
138+
new_content = content.replace(
139+
"module_name: docs/_scripts/macros",
140+
'modules: ["frequenz.repo.config.mkdocs.mkdocstrings_macros"]',
141+
)
142+
new_content = new_content.replace(
143+
"# inside docstrings. See the comment in `docs/_scripts/macros.py` for more\n"
144+
" # details\n",
145+
"# inside docstrings.\n",
146+
)
147+
148+
replace_file_contents_atomically(mkdocs_yaml, content, new_content)
149+
150+
print("Removing docs/_scripts/macros.py...")
151+
macros_file.unlink()
152+
153+
67154
def apply_patch(patch_content: str) -> None:
68155
"""Apply a patch using the patch utility."""
69156
subprocess.run(["patch", "-p1"], input=patch_content.encode(), check=True)
@@ -134,5 +221,25 @@ def manual_step(message: str) -> None:
134221
print(f"\033[0;33m>>> {message}\033[0m")
135222

136223

224+
def calculate_file_sha256_skip_lines(filepath: Path, skip_lines: int) -> str | None:
225+
"""Calculate SHA256 of file contents excluding the first N lines.
226+
227+
Args:
228+
filepath: Path to the file to hash
229+
skip_lines: Number of lines to skip at the beginning
230+
231+
Returns:
232+
The SHA256 hex digest, or None if the file doesn't exist
233+
"""
234+
if not filepath.exists():
235+
return None
236+
237+
# Read file and normalize line endings to LF
238+
content = filepath.read_text(encoding="utf-8").replace("\r\n", "\n")
239+
# Skip first N lines and ensure there's a trailing newline
240+
remaining_content = "\n".join(content.splitlines()[skip_lines:]) + "\n"
241+
return hashlib.sha256(remaining_content.encode()).hexdigest()
242+
243+
137244
if __name__ == "__main__":
138245
main()
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
# License: {{cookiecutter.license}}
2+
# Copyright © {{copyright_year}} {{cookiecutter.author_name}}
3+
4+
"""This module defines macros for use in Markdown files."""
5+
6+
from typing import Any
7+
8+
import markdown as md
9+
from markdown.extensions import toc
10+
from mkdocs_macros import plugin as macros
11+
12+
_CODE_ANNOTATION_MARKER: str = (
13+
r'<span class="md-annotation">'
14+
r'<span class="md-annotation__index" tabindex="-1">'
15+
r'<span data-md-annotation-id="1"></span>'
16+
r"</span>"
17+
r"</span>"
18+
)
19+
20+
21+
def _slugify(text: str) -> str:
22+
"""Slugify a text.
23+
24+
Args:
25+
text: The text to slugify.
26+
27+
Returns:
28+
The slugified text.
29+
"""
30+
return toc.slugify_unicode(text, "-")
31+
32+
33+
def _hook_macros_plugin(env: macros.MacrosPlugin) -> None:
34+
"""Integrate the `mkdocs-macros` plugin into `mkdocstrings`.
35+
36+
This is a temporary workaround to make `mkdocs-macros` work with
37+
`mkdocstrings` until a proper `mkdocs-macros` *pluglet* is available. See
38+
https://github.com/mkdocstrings/mkdocstrings/issues/615 for details.
39+
40+
Args:
41+
env: The environment to hook the plugin into.
42+
"""
43+
# get mkdocstrings' Python handler
44+
python_handler = env.conf["plugins"]["mkdocstrings"].get_handler("python")
45+
46+
# get the `update_env` method of the Python handler
47+
update_env = python_handler.update_env
48+
49+
# override the `update_env` method of the Python handler
50+
def patched_update_env(markdown: md.Markdown, config: dict[str, Any]) -> None:
51+
update_env(markdown, config)
52+
53+
# get the `convert_markdown` filter of the env
54+
convert_markdown = python_handler.env.filters["convert_markdown"]
55+
56+
# build a chimera made of macros+mkdocstrings
57+
def render_convert(markdown: str, *args: Any, **kwargs: Any) -> Any:
58+
return convert_markdown(env.render(markdown), *args, **kwargs)
59+
60+
# patch the filter
61+
python_handler.env.filters["convert_markdown"] = render_convert
62+
63+
# patch the method
64+
python_handler.update_env = patched_update_env
65+
66+
67+
def define_env(env: macros.MacrosPlugin) -> None:
68+
"""Define the hook to create macro functions for use in Markdown.
69+
70+
Args:
71+
env: The environment to define the macro functions in.
72+
"""
73+
# A variable to easily show an example code annotation from mkdocs-material.
74+
# https://squidfunk.github.io/mkdocs-material/reference/code-blocks/#adding-annotations
75+
env.variables["code_annotation_marker"] = _CODE_ANNOTATION_MARKER
76+
77+
# TODO(cookiecutter): Add any other macros, variables and filters here.
78+
79+
# This hook needs to be done at the end of the `define_env` function.
80+
_hook_macros_plugin(env)

docs/_scripts/macros.py

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
# License: MIT
2+
# Copyright © 2023 Frequenz Energy-as-a-Service GmbH
3+
4+
"""This module defines macros for use in Markdown files."""
5+
6+
import logging
7+
from typing import Any
8+
9+
import markdown as md
10+
from markdown.extensions import toc
11+
from mkdocs_macros import plugin as macros
12+
13+
from frequenz.repo.config import github
14+
15+
_logger = logging.getLogger(__name__)
16+
17+
_CODE_ANNOTATION_MARKER: str = (
18+
r'<span class="md-annotation">'
19+
r'<span class="md-annotation__index" tabindex="-1">'
20+
r'<span data-md-annotation-id="1"></span>'
21+
r"</span>"
22+
r"</span>"
23+
)
24+
25+
26+
def _slugify(text: str) -> str:
27+
"""Slugify a text.
28+
29+
Args:
30+
text: The text to slugify.
31+
32+
Returns:
33+
The slugified text.
34+
"""
35+
return toc.slugify_unicode(text, "-")
36+
37+
38+
def _add_version_variables(env: macros.MacrosPlugin) -> None:
39+
"""Add variables with git information to the environment.
40+
41+
Args:
42+
env: The environment to add the variables to.
43+
"""
44+
env.variables["version"] = None
45+
env.variables["version_requirement"] = ""
46+
try:
47+
version_info = github.get_repo_version_info()
48+
except Exception as exc: # pylint: disable=broad-except
49+
_logger.warning("Failed to get version info: %s", exc)
50+
else:
51+
env.variables["version"] = version_info
52+
if version_info.current_tag:
53+
env.variables["version_requirement"] = f" == {version_info.current_tag}"
54+
elif version_info.current_branch:
55+
env.variables["version_requirement"] = (
56+
" @ git+https://github.com/frequenz-floss/frequenz-repo-config-python"
57+
f"@{version_info.current_branch}"
58+
)
59+
60+
61+
def _hook_macros_plugin(env: macros.MacrosPlugin) -> None:
62+
"""Integrate the `mkdocs-macros` plugin into `mkdocstrings`.
63+
64+
This is a temporary workaround to make `mkdocs-macros` work with
65+
`mkdocstrings` until a proper `mkdocs-macros` *pluglet* is available. See
66+
https://github.com/mkdocstrings/mkdocstrings/issues/615 for details.
67+
68+
Args:
69+
env: The environment to hook the plugin into.
70+
"""
71+
# get mkdocstrings' Python handler
72+
python_handler = env.conf["plugins"]["mkdocstrings"].get_handler("python")
73+
74+
# get the `update_env` method of the Python handler
75+
update_env = python_handler.update_env
76+
77+
# override the `update_env` method of the Python handler
78+
def patched_update_env(markdown: md.Markdown, config: dict[str, Any]) -> None:
79+
update_env(markdown, config)
80+
81+
# get the `convert_markdown` filter of the env
82+
convert_markdown = python_handler.env.filters["convert_markdown"]
83+
84+
# build a chimera made of macros+mkdocstrings
85+
def render_convert(markdown: str, *args: Any, **kwargs: Any) -> Any:
86+
return convert_markdown(env.render(markdown), *args, **kwargs)
87+
88+
# patch the filter
89+
python_handler.env.filters["convert_markdown"] = render_convert
90+
91+
# patch the method
92+
python_handler.update_env = patched_update_env
93+
94+
95+
def define_env(env: macros.MacrosPlugin) -> None:
96+
"""Define the hook to create macro functions for use in Markdown.
97+
98+
Args:
99+
env: The environment to define the macro functions in.
100+
"""
101+
# A variable to easily show an example code annotation from mkdocs-material.
102+
# https://squidfunk.github.io/mkdocs-material/reference/code-blocks/#adding-annotations
103+
env.variables["code_annotation_marker"] = _CODE_ANNOTATION_MARKER
104+
105+
_add_version_variables(env)
106+
107+
# This hook needs to be done at the end of the `define_env` function.
108+
_hook_macros_plugin(env)

0 commit comments

Comments
 (0)