From 53deb7b901fb17c9deb1f3b857c8faed29fa6006 Mon Sep 17 00:00:00 2001 From: EGJ-Moorington Date: Thu, 15 Jan 2026 22:29:24 +0100 Subject: [PATCH] Created a `pre-commit` hook to update examples in `README` Created a `pre-commit` hook which synchronises examples in `README.rst` with examples in `examples`. --- .pre-commit-config.yaml | 11 +++ README.rst | 9 ++ ruff.toml | 2 +- scripts/update_readme_examples.py | 139 ++++++++++++++++++++++++++++++ 4 files changed, 160 insertions(+), 1 deletion(-) create mode 100644 scripts/update_readme_examples.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index bd072de..739e477 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,4 +1,5 @@ # SPDX-FileCopyrightText: 2024 Justin Myers for Adafruit Industries +# SPDX-FileCopyrightText: Copyright (c) 2026 EGJ Moorington # # SPDX-License-Identifier: Unlicense @@ -19,3 +20,13 @@ repos: rev: v6.2.0 hooks: - id: reuse + + - repo: local + hooks: + - id: readme-example-updater + name: update readme examples + language: python + entry: python scripts/update_readme_examples.py + files: | + (^README\.rst$)| + (^examples/.*\.py$) diff --git a/README.rst b/README.rst index 6f45685..46cf665 100644 --- a/README.rst +++ b/README.rst @@ -103,8 +103,16 @@ multiple buttons. | D9 | +---------------+ +.. include-example::: examples/button_handler_singlebutton.py + :language: python + .. code-block:: python + # SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries + # SPDX-FileCopyrightText: Copyright (c) 2024 EGJ Moorington + # + # SPDX-License-Identifier: Unlicense + import time import board @@ -144,6 +152,7 @@ multiple buttons. button_handler.update() time.sleep(0.0025) +.. /include-example::: Documentation ============= diff --git a/ruff.toml b/ruff.toml index 2beaad8..cfb26ce 100644 --- a/ruff.toml +++ b/ruff.toml @@ -7,7 +7,7 @@ target-version = "py38" line-length = 100 [per-file-target-version] -"{docs,tests}/**/*.py" = "py313" +"{docs,scripts,tests}/**/*.py" = "py313" [lint] select = ["I", "PL", "UP"] diff --git a/scripts/update_readme_examples.py b/scripts/update_readme_examples.py new file mode 100644 index 0000000..b8baed9 --- /dev/null +++ b/scripts/update_readme_examples.py @@ -0,0 +1,139 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 EGJ Moorington +# +# SPDX-License-Identifier: MIT + +import re +import warnings +from pathlib import Path + +START_DIRECTIVE_RE = re.compile(r"([\t ]*)(\.\. include-example:::)\s*(.*)") +END_DIRECTIVE_RE = re.compile(".. /include-example:::") +CODE_BLOCK_RE = re.compile(".. code-block::") +SETTING_RE = re.compile(r"\s*:(\S+):\s*(.*)") + +README_PATH = Path("README.rst") + + +def indent(code: str, spaces: int | str = 4) -> str: + prefix = spaces if isinstance(spaces, str) else " " * spaces + return "".join( + prefix + line if line.strip() else line for line in code.splitlines(True) + ) # Only indent if a line, when removing all whitespace and linebreaks, is not empty + + +def find_used_examples(readme: str) -> set[Path]: + used_examples = set() + for start_directive in re.finditer(START_DIRECTIVE_RE, readme): + used_examples.add(Path(start_directive.group(3))) + + return used_examples + + +def load_examples(paths: set[Path]) -> dict[str, str]: + examples = {} + + for path in paths: + try: + examples[str(path)] = path.read_text().rstrip() + except FileNotFoundError: + warnings.warn(f"Tried to load example {path}, which does not exist.") + + return examples + + +def read_settings( + settings_string: str, default_language: str = "python" +) -> tuple[dict[str, str], str]: + settings = {} + language = default_language + for setting in re.finditer(SETTING_RE, settings_string): + setting_name = setting.group(1) + + if setting_name == "language": + language = setting.group(2) + continue + + settings[setting_name] = setting.group(2) + + return settings, language + + +def update_readme(readme: str, examples_to_update: dict[str, str]) -> str: + for start_directive in reversed( + list(re.finditer(START_DIRECTIVE_RE, readme)) + ): # Update readme from the bottom, so that Match indices do not change on each iteration + example_to_insert = str(Path(start_directive.group(3))) # Normalise the path string + + if example_to_insert not in examples_to_update.keys(): + continue + + remaining_readme = readme[start_directive.end() :] + + next_start_directive = re.search(START_DIRECTIVE_RE, remaining_readme) + end_directive = re.search( + END_DIRECTIVE_RE, + remaining_readme[: next_start_directive.start() if next_start_directive else None], + ) + + if end_directive == None: + raise SyntaxError( + "An include-example directive was not closed:\n" + f"{ + readme[ + max(start_directive.start() - 100, 0) : min( + start_directive.end() + 100, len(readme) + ) + ] + }" + ) + + code_block = re.search(CODE_BLOCK_RE, remaining_readme[: end_directive.start()]) + settings_string = remaining_readme[ + : code_block.start() if code_block else end_directive.start() + ] # Read include-example settings, ignoring code-block settings + + settings, language = read_settings(settings_string) + + settings_block = "" + for setting, value in settings.items(): + settings_block += f":{setting}: {value}\n" + + modified_block = " ".join(start_directive.group(2, 3)) + "\n" + modified_block += indent(f":language: {language}\n") + modified_block += indent(settings_block) + modified_block += "\n" + modified_block += f".. code-block:: {language}\n" + modified_block += indent(settings_block) + modified_block += "\n" + modified_block += indent(examples_to_update[example_to_insert]) + modified_block += "\n\n" + modified_block += f"{end_directive.group()}" + + readme = ( + readme[: start_directive.start()] + + indent(modified_block, start_directive.group(1)) + + readme[start_directive.end() + end_directive.end() :] + ) + + return readme + + +def main(*args): + readme = README_PATH.read_text() + updated_files = {Path(path_str) for path_str in args[0]} + used_examples = find_used_examples(readme) + + required_examples = ( + used_examples if README_PATH in updated_files else used_examples & updated_files + ) + + examples = load_examples(required_examples) + updated = update_readme(readme, examples) + + README_PATH.write_text(updated, newline="\n") + + +if __name__ == "__main__": + import sys + + main(sys.argv[1:])