diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index b694913..ebbbda7 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -12,10 +12,10 @@ jobs: - name: Set up Python uses: actions/setup-python@v5 with: - python-version: 3.12 + python-version: 3.13 - name: Install dependencies run: | - poetry install --only main + poetry install --only main,docs - name: Build Documentation run: | poetry run python -m docs diff --git a/.gitignore b/.gitignore index e313c48..089af00 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ .vscode docs/public/ +examples/*.md # Byte-compiled / optimized / DLL files __pycache__/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 239ba82..1ba8c3a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,3 +1,8 @@ +default_stages: + - pre-commit +default_install_hook_types: + - pre-commit + - pre-push repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v5.0.0 @@ -11,7 +16,7 @@ repos: hooks: - id: add-trailing-comma - repo: https://github.com/tox-dev/pyproject-fmt - rev: v2.4.3 + rev: v2.5.0 hooks: - id: pyproject-fmt - repo: https://github.com/tox-dev/tox-ini-fmt @@ -41,7 +46,7 @@ repos: - id: poetry-check - id: poetry-lock - repo: https://github.com/google/yamlfmt - rev: v0.13.0 + rev: v0.14.0 hooks: - id: yamlfmt - repo: https://github.com/PyCQA/flake8 @@ -49,3 +54,11 @@ repos: hooks: - id: flake8 args: ["--max-line-length", "88"] + - repo: local + hooks: + - id: readme + name: rendering readme + entry: poetry run python docs/readme.py + pass_filenames: false + always_run: true + language: system diff --git a/.pre-commit-sample.yaml b/.pre-commit-sample.yaml new file mode 100644 index 0000000..89f3e3a --- /dev/null +++ b/.pre-commit-sample.yaml @@ -0,0 +1,6 @@ +repos: + - repo: https://github.com/d-chris/jinja2_pdoc/ + rev: v1.1.0 + hooks: + - id: jinja2pdoc + files: docs/.*\.jinja2$ diff --git a/README.md b/README.md index d4be9fd..2eee408 100644 --- a/README.md +++ b/README.md @@ -33,25 +33,25 @@ from jinja2_pdoc import Environment env = Environment() -s = """ - # jinja2-pdoc +template = """\ +# jinja2-pdoc - embedd python code directly from pathlib using a jinja2 extension based on pdoc +embedd python code directly from pathlib using a jinja2 extension based on pdoc - ## docstring from pathlib.Path +## docstring from pathlib.Path - {% pdoc pathlib:Path:docstring %} +{% pdoc pathlib:Path:docstring %} - ## source from pathlib.Path.open +## source from pathlib.Path.open - ```python - {% pdoc pathlib:Path.open:source.indent -%} - ``` - """ +```python +{% pdoc pathlib:Path.open:source.indent -%} +``` +""" -code = env.from_string(textwrap.dedent(s)).render() +code = env.from_string(template).render() -Path("example.md").write_text(code) +print(code) ```` ### Markdown @@ -59,7 +59,6 @@ Path("example.md").write_text(code) output of the [python code](#python) above. ````markdown - # jinja2-pdoc embedd python code directly from pathlib using a jinja2 extension based on pdoc @@ -149,50 +148,77 @@ Example: {% pdoc pathlib:Path.open:code.dedent %} ``` -## Command Line Interface +## Filter -```console ->>> jinja2pdoc --help - -Usage: jinja2pdoc [OPTIONS] [FILES]... - - Render jinja2 one or multiple template files, wildcards in filenames are - allowed, e.g. `examples/*.jinja2`. - - If no 'filename' is provided in the frontmatter section of your file, e.g. - ''. All files are written to `output` directory - and `suffixes` will be removed. - - To ignore the frontmatter section use the `--no-meta` flag. - -Options: - -o, --output PATH output directory for files, if no 'filename' - is provided in the frontmatter. [default: - cwd] - -e, --encoding TEXT encoding of the files [default: utf-8] - -s, --suffixes TEXT suffixes which will be removed from templates, - if no 'filename' is provided in the - frontmatter [default: .jinja2, .j2] - --fail-fast exit on first error when rendering multiple - file - --meta / --no-meta parse frontmatter from the template, to search - for 'filename' [default: meta] - --rerender / --no-rerender Each file is rendered only once. [default: - no-rerender] - --silent suppress console output - --load-path / --no-load-path add the current working directory to path - [default: load-path] - --help Show this message and exit. +Filter to use in `jinja2` template. + +### include + +`Environment.include` - returns the content of the file. + +```jinja +{{ "path/to/file" | include(enc="utf-8") }} ``` -```console ->>> jinja2pdoc .\examples\*.jinja2 -rendered examples\example.md.jinja2...................... .\example.md +### shell + +`Environment.shell` - run shell command and return the selected result from `subprocess.CompletedProcess`. + +```jinja +{{ "python --version" | shell(promt=">>> %s\n") }} ``` -## pre-commit hook +### strip -**Per default the hook is not registered to `files`!** +`Environment.strip` - remove leading and trailing whitespace and newlines from a string. + +```jinja +{{ "path/to/file" | include | strip }} +``` + +## Command Line Interface + +```cmd +$ jinja2pdoc --help + + Usage: jinja2pdoc [OPTIONS] [FILES]... + + Render jinja2 one or multiple template files, wildcards in filenames are + allowed, e.g. `examples/*.jinja2`. + + If no 'filename' is provided in the frontmatter section of your file, e.g. + ''. All files are written to `output` directory + and `suffixes` will be removed. + + To ignore the frontmatter section use the `--no-meta` flag. + + Options: + -o, --output PATH output directory for files, if no 'filename' + is provided in the frontmatter. [default: + cwd] + -e, --encoding TEXT encoding of the files [default: utf-8] + -s, --suffixes TEXT suffixes which will be removed from templates, + if no 'filename' is provided in the + frontmatter [default: .jinja2, .j2] + --fail-fast exit on first error when rendering multiple + file + --meta / --no-meta parse frontmatter from the template, to search + for 'filename' [default: meta] + --rerender / --no-rerender Each file is rendered only once. [default: + no-rerender] + --silent suppress console output + --load-path / --no-load-path add the current working directory to path + [default: load-path] + --help Show this message and exit. +``` + +```cmd +$ jinja2pdoc .\examples\*.jinja2 + + rendering examples\example.md.jinja2...................... examples\example.md +``` + +## pre-commit-config To render all template files from `docs` using `.pre-commit-config.yaml` add the following. @@ -207,18 +233,22 @@ repos: files: docs/.*\.jinja2$ ``` -Use `additional_dependencies` to add extra dependencies to the pre-commit environment. Example see below. +Use [`additional_dependencies`](https://pre-commit.com/#config-additional_dependencies) to add extra dependencies to the pre-commit environment. > This is necessary when a module or source code rendered into your template contains modules that are not part of the standard library. +## pre-commit-hooks + +**Per default the hook is not registered to `files`!** + ```yaml -repos: - - repo: https://github.com/d-chris/jinja2_pdoc/ - rev: v1.1.0 - hooks: - - id: jinja2pdoc - files: docs/.*\.jinja2$ - additional_dependencies: [pathlibutil] +- id: jinja2pdoc + name: render jinja2pdoc + description: render jinja2 templates to embedd python code directly from module using pdoc. + entry: jinja2pdoc + language: python + types: [jinja] + files: ^$ ``` ## Dependencies diff --git a/docs/README.md.jinja2 b/docs/README.md.jinja2 new file mode 100644 index 0000000..4330cf5 --- /dev/null +++ b/docs/README.md.jinja2 @@ -0,0 +1,91 @@ +# jinja2-pdoc + +[![PyPI - Python Version](https://img.shields.io/pypi/pyversions/jinja2_pdoc)](https://pypi.org/project/jinja2_pdoc/) +[![PyPI - jinja2_pdoc](https://img.shields.io/pypi/v/jinja2_pdoc)](https://pypi.org/project/jinja2_pdoc/) +[![PyPI - Downloads](https://img.shields.io/pypi/dm/jinja2_pdoc)](https://pypi.org/project/jinja2_pdoc/) +[![PyPI - License](https://img.shields.io/pypi/l/jinja2_pdoc)](https://raw.githubusercontent.com/d-chris/jinja2_pdoc/main/LICENSE) +[![GitHub - pytest](https://img.shields.io/github/actions/workflow/status/d-chris/jinja2_pdoc/pytest.yml?logo=github&label=pytest)](https://github.com/d-chris/jinja2_pdoc/actions/workflows/pytest.yml) +[![GitHub - Page](https://img.shields.io/website?url=https%3A%2F%2Fd-chris.github.io%2Fjinja2_pdoc%2F&up_message=pdoc&logo=github&label=documentation)](https://d-chris.github.io/jinja2_pdoc) +[![GitHub tag (with filter)](https://img.shields.io/github/v/tag/d-chris/jinja2_pdoc?logo=github&label=github)](https://github.com/d-chris/jinja2_pdoc) +[![codecov](https://codecov.io/gh/d-chris/jinja2_pdoc/graph/badge.svg?token=19YB50ZL63)](https://codecov.io/gh/d-chris/jinja2_pdoc) +[![pre-commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit)](https://github.com/pre-commit/pre-commit) + +--- + +[`jinja2`](https://www.pypi.org/project/jinja2) extension based on [`pdoc`](https://pypi.org/project/pdoc/) to embedd python code directly from modules or files into your `jinja` template. + +Lazy loading of `docstrings`, `code` and `functions` directly from python modules into your `jinja2 template`. + +## Installation + +```cmd +pip install jinja2_pdoc +``` + +## Example + +Create a markdown file with `docstrings` and `source code` from `pathlib.Path` using `jinja2` with `jinja2_pdoc` extension. + +### Python + +````python +{{ "examples/example.py" | include }} +```` + +### Markdown + +output of the [python code](#python) above. + +````markdown +{{ "python examples/example.py" | shell }} +```` + +## Syntax + +{{ "docs/syntax.md" | include }} + +## Filter + +{{ "docs/filter.md" | include }} + +## Command Line Interface + +```cmd +{{ "jinja2pdoc --help" | shell(promt="$ ") | indent(2) }} +``` + +```cmd +{{ "jinja2pdoc .\examples\*.jinja2" | shell(promt="$ ") | indent(2) }} +``` + +## pre-commit-config + +To render all template files from `docs` using `.pre-commit-config.yaml` add the following. + +You may add a `frontmatter` section at the beginning of in your templates to specify output directory and filename, e.g. ``. If no metadata are at the beginning of the template, the rendered file is written to the `output` directory which is default the current working direktory. + +```yaml +{{ ".pre-commit-sample.yaml" | include }} +``` + +Use [`additional_dependencies`](https://pre-commit.com/#config-additional_dependencies) to add extra dependencies to the pre-commit environment. + +> This is necessary when a module or source code rendered into your template contains modules that are not part of the standard library. + +## pre-commit-hooks + +**Per default the hook is not registered to `files`!** + +```yaml +{{ ".pre-commit-hooks.yaml" | include }} +``` + +## Dependencies + +[![PyPI - autopep8](https://img.shields.io/pypi/v/autopep8?logo=pypi&logoColor=white&label=autopep8)](https://pypi.org/project/autopep8/) +[![PyPI - click](https://img.shields.io/pypi/v/click?logo=pypi&logoColor=white&label=click)](https://pypi.org/project/click/) +[![PyPI - jinja2](https://img.shields.io/pypi/v/jinja2?logo=jinja&logoColor=white&label=jinja2)](https://pypi.org/project/jinja2/) +[![PyPI - pdoc](https://img.shields.io/pypi/v/pdoc?logo=pypi&logoColor=white&label=pdoc)](https://pypi.org/project/pdoc/) +[![Pypi - PyYAML](https://img.shields.io/pypi/v/PyYAML?logo=pypi&logoColor=white&label=PyYAML)](https://pypi.org/project/PyYAML/) + +--- diff --git a/docs/__init__.py b/docs/__init__.py new file mode 100644 index 0000000..3b69224 --- /dev/null +++ b/docs/__init__.py @@ -0,0 +1,7 @@ +from .documentation import main as documentation +from .readme import main as readme + +__all__ = [ + "documentation", + "readme", +] diff --git a/docs/__main__.py b/docs/__main__.py index e1153a6..829040c 100644 --- a/docs/__main__.py +++ b/docs/__main__.py @@ -1,106 +1,15 @@ -import json -import os -import shutil -import warnings -from contextlib import contextmanager -from pathlib import Path +from . import documentation, readme -@contextmanager -def pushd(path): - cwd = os.getcwd() - try: - os.chdir(path) - yield Path(path) - finally: - os.chdir(cwd) - - -class WarningMessageEncoder(json.JSONEncoder): - def default(self, obj): - - if isinstance(obj, warnings.WarningMessage): - return { - k: v - for k, v in obj.__dict__.items() - if v is not None and not k.startswith("_") - } - - if isinstance(obj, type): - return obj.__name__ - - if isinstance(obj, Exception): - return str(obj).strip().split("\n") - - return super().default(obj) - - -def main() -> int: +def main(): """ - Create a static html documentation for the project in the 'public' directory. - - Requires follwing PyPi packages: - - 'pdoc' - - 'pathlibutil' + Render the README.md file and create a static html documentation for the project + in the 'public' directory. Returns non-zero on failure. """ - try: - from pdoc import pdoc, render - - with pushd(Path(__file__).parent) as cwd: - print(f"\nrunning docs in {cwd=}...") - - documentation = cwd / "public" - - try: - print("cleaning output directory.") - shutil.rmtree(documentation) - except FileNotFoundError: - pass - - documentation.mkdir(parents=True) - - config = { - "template_directory": cwd / "dark-mode", - "show_source": False, - "search": False, - } - - modules = [ - "jinja2_pdoc", - "pdoc", - "jinja2", - ] - - with warnings.catch_warnings(record=True) as messages: - print(f"rendering {modules=}.") - - render.configure(**config) - pdoc(*modules, output_directory=documentation) - - log = documentation / "warnings.json" - - with log.open("w", encoding="utf-8") as f: - json.dump( - messages, - f, - cls=WarningMessageEncoder, - ) - - if messages: - print(f"{log=} generated with {len(messages)} warnings.") - except ModuleNotFoundError as e: - print(f"Creation failed, due missing dependency!\n\tpip install {e.name}") - return 2 - except Exception as e: - print(f"{documentation=} creation failed!\n\t{e}") - return 1 - - print(f"{documentation=} generated successfully.\n") - - return 0 + return readme() or documentation() if __name__ == "__main__": diff --git a/docs/documentation.py b/docs/documentation.py new file mode 100644 index 0000000..ea6aec9 --- /dev/null +++ b/docs/documentation.py @@ -0,0 +1,97 @@ +import json +import warnings + + +class WarningMessageEncoder(json.JSONEncoder): + def default(self, obj): + + if isinstance(obj, warnings.WarningMessage): + return { + k: v + for k, v in obj.__dict__.items() + if v is not None and not k.startswith("_") + } + + if isinstance(obj, type): + return obj.__name__ + + if isinstance(obj, Exception): + return str(obj).strip().split("\n") + + return super().default(obj) + + +def main() -> int: + """ + Create a static html documentation for the project in the 'public' directory. + + Requires follwing PyPi packages: + - 'pdoc' + - 'pathlibutil' + + Returns non-zero on failure. + """ + + try: + from pathlibutil import Path + from pdoc import pdoc, render + + with Path(__file__).parent as cwd: + print(f"\nrunning docs in {cwd=}...") + + documentation: Path = cwd / "public" + + try: + print(f"cleaning output directory {documentation.size()}.") + documentation.delete(recursive=True, missing_ok=True) + except FileNotFoundError: + pass + + documentation.mkdir(parents=True) + + config = { + "template_directory": cwd / "dark-mode", + "show_source": False, + "search": False, + "docformat": "google", + } + + modules = [ + "jinja2_pdoc", + "pdoc", + "jinja2", + "pathlib", + "subprocess", + ] + + with warnings.catch_warnings(record=True) as messages: + print(f"rendering {modules=}.") + + render.configure(**config) + pdoc(*modules, output_directory=documentation) + + log = documentation / "warnings.json" + + with log.open("w", encoding="utf-8") as f: + json.dump( + messages, + f, + cls=WarningMessageEncoder, + ) + + if messages: + print(f"{log=} generated with {len(messages)} warnings.") + except ModuleNotFoundError as e: + print(f"Creation failed, due missing dependency!\n\tpip install {e.name}") + return 2 + except Exception as e: + print(f"{documentation=} creation failed!\n\t{e}") + return 1 + + print(f"{documentation=} generated successfully.\n") + + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/docs/filter.md b/docs/filter.md new file mode 100644 index 0000000..0e9cb2b --- /dev/null +++ b/docs/filter.md @@ -0,0 +1,25 @@ +Filter to use in `jinja2` template. + +### include + +`Environment.include` - returns the content of the file. + +```jinja +{{ "path/to/file" | include(enc="utf-8") }} +``` + +### shell + +`Environment.shell` - run shell command and return the selected result from `subprocess.CompletedProcess`. + +```jinja +{{ "python --version" | shell(promt=">>> %s\n") }} +``` + +### strip + +`Environment.strip` - remove leading and trailing whitespace and newlines from a string. + +```jinja +{{ "path/to/file" | include | strip }} +``` diff --git a/docs/readme.py b/docs/readme.py new file mode 100644 index 0000000..761294b --- /dev/null +++ b/docs/readme.py @@ -0,0 +1,37 @@ +from jinja2_pdoc import Environment + + +def main(): + """ + Render the README.md file using the README.md.jinja2 template. + + Requires following PyPi packages: + - 'pathlibutil' + + Returns non-zero on failure. + """ + + try: + from pathlibutil import Path + + template = Path(__file__).with_name("README.md.jinja2") + readme = Path("README.md") + + with template.open("r") as file: + template = Environment( + keep_trailing_newline=True, + ).from_string(file.read()) + + with readme.open("w") as file: + file.write(template.render()) + + except Exception as e: + print(f"{readme=} creation failed!\n\t{e}") + return 1 + + print(f"{readme=} created successfully!") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/docs/syntax.md b/docs/syntax.md new file mode 100644 index 0000000..546b6ce --- /dev/null +++ b/docs/syntax.md @@ -0,0 +1,58 @@ + +`{% pdoc`[``](#module)`:`[``](#object)`:`[``](#pdoc_attr)`%}` + +### `` + +module name or path to python file, e.g.: + +- `pathlib` +- `examples/example.py` + +Example: + +```jinja2 +{% pdoc pathlib %} +``` + +### `` + +class and/or function names, eg. from `pathlib`: + +- `Path` +- `Path.open` + +Example: + +```jinja2 +{% pdoc pathlib:Path %} +``` + +### `` + +`pdoc` attributes: + +- `docstring` - docstring of the object +- `source` - source code of the object +- `code` - plane code from functions, without def and docstring + +Example: + +```jinja2 +{% pdoc pathlib:Path:docstring %} +``` + +### `[.str_attr]` + +optional `str` functions can be added to `` with a dot + +- `dedent` - removes common leading whitespace, see `textwrap.dedent` +- `indent` - format code with 2 spaces for indentation, see `autopep8.fix_code` +- `upper` - converts to upper case +- `lower` - converts to lower case +- `nodoc` - removes shebang and docstring + +Example: + +```jinja2 +{% pdoc pathlib:Path.open:code.dedent %} +``` diff --git a/examples/example.md.jinja2 b/examples/example.md.jinja2 index ea5e53e..2310d9b 100644 --- a/examples/example.md.jinja2 +++ b/examples/example.md.jinja2 @@ -1,3 +1,4 @@ + # jinja2-pdoc embedd python code directly from pathlib using a jinja2 extension based on pdoc diff --git a/examples/example.py b/examples/example.py new file mode 100644 index 0000000..c80ba1f --- /dev/null +++ b/examples/example.py @@ -0,0 +1,23 @@ +from jinja2_pdoc import Environment + +env = Environment() + +template = """\ +# jinja2-pdoc + +embedd python code directly from pathlib using a jinja2 extension based on pdoc + +## docstring from pathlib.Path + +{% pdoc pathlib:Path:docstring %} + +## source from pathlib.Path.open + +```python +{% pdoc pathlib:Path.open:source.indent -%} +``` +""" + +code = env.from_string(template).render() + +print(code) diff --git a/jinja2_pdoc/cli.py b/jinja2_pdoc/cli.py index c4c82cd..b4712f2 100644 --- a/jinja2_pdoc/cli.py +++ b/jinja2_pdoc/cli.py @@ -53,6 +53,8 @@ def echo(tag, file, out, silent=False): if silent: return + color = "yellow" + if isinstance(tag, Exception): out = str(tag)[:48] tag = type(tag).__name__ @@ -63,9 +65,7 @@ def echo(tag, file, out, silent=False): except ValueError: out = str(out)[-48:] - if tag == "skip": - color = "yellow" - else: + if tag != "skip": color = "green" tag = click.style(f"{tag[:16]:<16}", fg=color) @@ -220,4 +220,4 @@ def cli(**kwargs): if __name__ == "__main__": - cli() + cli() # pragma: no cover diff --git a/jinja2_pdoc/environment.py b/jinja2_pdoc/environment.py index 71b0462..98746c8 100644 --- a/jinja2_pdoc/environment.py +++ b/jinja2_pdoc/environment.py @@ -1,3 +1,7 @@ +import subprocess +from pathlib import Path +from typing import Callable + import jinja2 from jinja2_pdoc.extension import Jinja2Pdoc @@ -22,3 +26,123 @@ def __init__(self, *args, **kwargs): """ super().__init__(*args, **kwargs) self.add_extension(Jinja2Pdoc) + + self.add_filter("shell", self.shell) + self.add_filter("include", self.include) + self.add_filter("strip", self.strip) + + def add_filter(self, name: str, func: Callable) -> None: + """ + Add a new filter to the environment. + + Args: + name (str): The name of the filter. + func (Callable): The filter function. + + Example: + >>> env = jinja2_pdoc.Environment() + >>> env.add_filter('upper', lambda s: s.upper()) + >>> env.from_string('{{ "hello world." | upper }}').render() + 'HELLO WORLD.' + """ + if not callable(func): + raise TypeError(f"{func=} is not a callable") + + self.filters[name] = func + + @staticmethod + def shell( + cmd: str, + result: str = "stdout", + *, + promt: str = None, + ) -> str: + """ + Filter to run a shell command and return the output. + + Args: + cmd (str): The command to run with `subprocess.run`. + result (str, optional): The attribute to return from the + `subprocess.CompletedProcess` instance. Defaults to "stdout". + promt (str, optional): Format string to include the command before the + output, e.g. `">>> %s \\n"`. Defaults to None. + + Returns: + str: The result of the command. + + Example: + ```jinja2 + {{ "python --version" | shell(promt="$ ") }} + ``` + """ + + process = subprocess.run( + cmd, + shell=True, + capture_output=True, + text=True, + ) + + output = str(getattr(process, result)) + + if promt is not None: + try: + prefix = promt % cmd + except TypeError: + prefix = f"{promt}{cmd}\n" + + output = f"{prefix}\n{output}" + + return Environment.strip(output) + + @staticmethod + def include( + file: str, + enc=None, + *, + attr: str = None, + ) -> str: + """ + Filter for jinja2 Template to include the content of a file. + + Args: + file (str): The file to include. + enc (str, optional): The encoding to use with `pathlib.Path.read_text()`. + attr (str, optional): The string method to call on the file + content, e.g. `include(attr="upper")`. + + Returns: + str: The content of the file. + + Example: + ```jinja2 + {{ ".pre-commit-hool.yaml" | include }} + ``` + """ + + content = Path(file).read_text(encoding=enc) + + if attr is not None: + content = getattr(content, attr)() + + return Environment.strip(content) + + @staticmethod + def strip(text: str, chars: str = "\n ") -> str: + """ + Strips the specified characters from the beginning and end of the given text. + + Args: + text (str): The text to be stripped. + chars (str, optional): The characters to be stripped from the text. + Defaults to `"\\n "`. + + Returns: + str: The stripped text. + + Example: + ```jinja2 + {{ " hello world. \\n" | strip }} + ``` + """ + return text.strip(chars) diff --git a/jinja2_pdoc/wrapper.py b/jinja2_pdoc/wrapper.py index 4748960..2368cce 100644 --- a/jinja2_pdoc/wrapper.py +++ b/jinja2_pdoc/wrapper.py @@ -101,7 +101,7 @@ class PdocStr(str): re.MULTILINE | re.DOTALL, ) - def dedent(self) -> str: + def dedent(self) -> "PdocStr": """ remove common whitespace from the left of every line in the string, see `textwrap.dedent` for more information. @@ -109,14 +109,14 @@ def dedent(self) -> str: s = textwrap.dedent(self) return self.__class__(s) - def indent(self) -> str: + def indent(self) -> "PdocStr": """ remove leading spaces and change indent size to 2 spaces, instead of 4. """ s = autopep8.fix_code(self.dedent(), options={"indent_size": 2}) return self.__class__(s) - def nodoc(self) -> str: + def nodoc(self) -> "PdocStr": """ remove shebang and docstring and from the string """ diff --git a/poetry.lock b/poetry.lock index 05f7e97..e2478eb 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.4 and should not be changed by hand. [[package]] name = "astunparse" @@ -313,6 +313,20 @@ files = [ {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, ] +[[package]] +name = "pathlibutil" +version = "0.3.4" +description = "inherits from pathlib.Path with methods for hashing, copying, deleting and more" +optional = false +python-versions = "<4.0.0,>=3.8.1" +files = [ + {file = "pathlibutil-0.3.4-py3-none-any.whl", hash = "sha256:7f5b9575ca785dc4e61bacf3d707c3eac70df9b8e05103c00fe1727b0f0a777f"}, + {file = "pathlibutil-0.3.4.tar.gz", hash = "sha256:e866c106af6034924f2e7cb4a16d0e543100009381a943c85d20c899af8982ad"}, +] + +[package.extras] +7z = ["py7zr (>=0.20.2)"] + [[package]] name = "pdoc" version = "14.7.0" @@ -333,6 +347,22 @@ pygments = ">=2.12.0" [package.extras] dev = ["hypothesis", "mypy", "pdoc-pyo3-sample-library (==1.0.11)", "pygments (>=2.14.0)", "pytest", "pytest-cov", "pytest-timeout", "ruff", "tox", "types-pygments"] +[[package]] +name = "pdoc" +version = "15.0.0" +description = "API Documentation for Python Projects" +optional = false +python-versions = ">=3.9" +files = [ + {file = "pdoc-15.0.0-py3-none-any.whl", hash = "sha256:151b0187a25eaf827099e981d6dbe3a4f68aeb18d0d637c24edcab788d5540f1"}, + {file = "pdoc-15.0.0.tar.gz", hash = "sha256:b761220d3ba129cd87e6da1bb7b62c8e799973ab9c595de7ba1a514850d86da5"}, +] + +[package.dependencies] +Jinja2 = ">=2.11.0" +MarkupSafe = ">=1.1.1" +pygments = ">=2.12.0" + [[package]] name = "platformdirs" version = "4.3.6" @@ -432,13 +462,13 @@ dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments [[package]] name = "pytest-cov" -version = "4.1.0" +version = "5.0.0" description = "Pytest plugin for measuring coverage." optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "pytest-cov-4.1.0.tar.gz", hash = "sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6"}, - {file = "pytest_cov-4.1.0-py3-none-any.whl", hash = "sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a"}, + {file = "pytest-cov-5.0.0.tar.gz", hash = "sha256:5837b58e9f6ebd335b0f8060eecce69b662415b16dc503883a02f45dfeb14857"}, + {file = "pytest_cov-5.0.0-py3-none-any.whl", hash = "sha256:4f0764a1219df53214206bf1feea4633c3b558a2925c8b59f144f682861ce652"}, ] [package.dependencies] @@ -446,7 +476,7 @@ coverage = {version = ">=5.2.1", extras = ["toml"]} pytest = ">=4.6" [package.extras] -testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] +testing = ["fields", "hunter", "process-tests", "pytest-xdist", "virtualenv"] [[package]] name = "pytest-mock" @@ -603,13 +633,13 @@ files = [ [[package]] name = "virtualenv" -version = "20.27.0" +version = "20.27.1" description = "Virtual Python Environment builder" optional = false python-versions = ">=3.8" files = [ - {file = "virtualenv-20.27.0-py3-none-any.whl", hash = "sha256:44a72c29cceb0ee08f300b314848c86e57bf8d1f13107a5e671fb9274138d655"}, - {file = "virtualenv-20.27.0.tar.gz", hash = "sha256:2ca56a68ed615b8fe4326d11a0dca5dfbe8fd68510fb6c6349163bed3c15f2b2"}, + {file = "virtualenv-20.27.1-py3-none-any.whl", hash = "sha256:f11f1b8a29525562925f745563bfd48b189450f61fb34c4f9cc79dd5aa32a1f4"}, + {file = "virtualenv-20.27.1.tar.gz", hash = "sha256:142c6be10212543b32c6c45d3d3893dff89112cc588b7d0879ae5a1ec03a47ba"}, ] [package.dependencies] @@ -638,4 +668,4 @@ test = ["pytest (>=6.0.0)", "setuptools (>=65)"] [metadata] lock-version = "2.0" python-versions = "^3.8.1" -content-hash = "5220afe3eeeb780a742b1bfe3a7e40fcfda1e3c70ed31fbcab7c0195353d3ec6" +content-hash = "a10ccf0ec34aadaae32d99e08869fcab391d18270ad960354fe0d87e5bd23bc5" diff --git a/pyproject.toml b/pyproject.toml index 688c942..df5aeaa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,7 +29,10 @@ documentation = "https://d-chris.github.io/jinja2_pdoc" [tool.poetry.dependencies] python = "^3.8.1" Jinja2 = "^3.1.2" -pdoc = "^14.3.0" +pdoc = [ + { version = "^14.3.0", python = "<3.9" }, + { version = ">=14.3.0", python = "^3.9" }, +] click = "^8.1.7" autopep8 = "^2.0.4" pyyaml = "^6.0.1" @@ -38,14 +41,17 @@ pyyaml = "^6.0.1" jinja2pdoc = "jinja2_pdoc.cli:cli" [tool.poetry.group.test.dependencies] -pytest = "^8.3.3" +pytest = "^8.0.0" pytest-random-order = "^1.1.0" -pytest-cov = "^4.1.0" +pytest-cov = ">=4.1.0" pytest-mock = "^3.14.0" [tool.poetry.group.dev.dependencies] tox = "^4.11.4" +[tool.poetry.group.docs.dependencies] +pathlibutil = "^0.3.0" + [[tool.poetry.source]] name = "testpypi" url = "https://test.pypi.org/legacy/" @@ -68,3 +74,10 @@ addopts = [ "--cov-report=term-missing:skip-covered", "--cov-report=xml", ] + +[tool.coverage.run] +omit = [ + "*/tests/*", + "*/docs/*", + "*/examples/*", +] diff --git a/tests/test_cli.py b/tests/test_cli.py index ce1cf98..b2b552a 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1,3 +1,5 @@ +import warnings + import pytest from click.testing import CliRunner @@ -6,17 +8,22 @@ def test_cli_folder(tmp_path): runner = CliRunner() - result = runner.invoke( - cli, - [ - "examples/*.jinja2", - "--output", - str(tmp_path), - ], - ) + + with warnings.catch_warnings(record=True): + warnings.simplefilter("always", DeprecationWarning) + + # Your code that may trigger a DeprecationWarning + result = runner.invoke( + cli, + [ + "docs/*.jinja2", + "--output", + str(tmp_path), + ], + ) assert result.exit_code == 0 assert "rendering" in result.output - assert tmp_path.joinpath("example.md").is_file() + assert tmp_path.joinpath("README.md").is_file() def test_cli_nofile(tmp_path): diff --git a/tests/test_filter.py b/tests/test_filter.py new file mode 100644 index 0000000..5b4931a --- /dev/null +++ b/tests/test_filter.py @@ -0,0 +1,121 @@ +import subprocess +from pathlib import Path + +import pytest + +from jinja2_pdoc import Environment + + +@pytest.fixture +def mock_shell(mocker): + return mocker.patch( + "subprocess.run", + return_value=subprocess.CompletedProcess( + args="echo testing..", + stdout="testing..", + stderr="", + returncode=0, + ), + ) + + +@pytest.fixture +def mock_include(mocker): + return mocker.patch.object(Path, "read_text", return_value="testing..") + + +@pytest.mark.parametrize( + "filter", + [ + "shell", + "include", + "strip", + ], +) +def test_filter(filter): + + assert filter in Environment().filters + + +def test_add_filter(): + + env = Environment() + env.add_filter("upper", lambda s: s.upper()) + s = env.from_string('{{ "hello world." | upper }}').render() + + assert s == "HELLO WORLD." + + +def test_add_filter_raises(): + + with pytest.raises(TypeError): + Environment().add_filter("none", None) + + +def test_render_include(mock_include): + + template = '{{ "LICENSE" | include }}' + + code = Environment().from_string(template).render() + + assert code == "testing.." + + +def test_render_shell(mock_shell): + + template = '{{ "echo testing.." | shell }}' + + code = Environment().from_string(template).render() + + assert code == "testing.." + + +def test_render_strip(): + + template = '{{ " testing.. \n" | strip }}' + + code = Environment().from_string(template).render() + + assert code == "testing.." + + +@pytest.mark.parametrize( + "attr", + [ + "stdout", + "stderr", + "returncode", + "args", + ], +) +def test_shell_result(mock_shell, attr): + assert isinstance(Environment.shell("echo testing..", result=attr), str) + + +@pytest.mark.parametrize( + "promt,expected", + [ + (None, "testing.."), + ("> ", "> echo testing..\n\ntesting.."), + ("$ %s", "$ echo testing..\ntesting.."), + ], +) +def test_shell_promt(mock_shell, promt, expected): + + stdout = Environment.shell("echo testing..", promt=promt) + + assert stdout == expected + + +def test_include_attr(mock_include): + + content = Environment.include("", attr="upper") + + assert content == "TESTING.." + + +def test_strip_chars(): + + content = Environment.strip("testing..", chars=".") + + assert content == "testing"