From 807d40361cad2055e65f9ed82773922a11b958fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20D=C3=B6rrer?= Date: Fri, 25 Oct 2024 18:45:06 +0200 Subject: [PATCH] Enhance PdocStr functionality - Keep trailing newline of templates while rendering. - Add 'nodoc' method to PdocStr for removing shebang and docstring - Update tests to cover new PdocStr methods and improve naming conventions - Update pre-commit configuration --- .pre-commit-config.yaml | 4 +- README.md | 15 +++++ docs/__main__.py | 2 +- jinja2_pdoc/cli.py | 4 +- jinja2_pdoc/wrapper.py | 32 ++++++++++ poetry.lock | 9 ++- tests/test_wrapper.py | 133 ++++++++++++++++++++++++++++++---------- 7 files changed, 159 insertions(+), 40 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f59e5d9..239ba82 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -11,7 +11,7 @@ repos: hooks: - id: add-trailing-comma - repo: https://github.com/tox-dev/pyproject-fmt - rev: 2.4.3 + rev: v2.4.3 hooks: - id: pyproject-fmt - repo: https://github.com/tox-dev/tox-ini-fmt @@ -27,7 +27,7 @@ repos: hooks: - id: black - repo: https://github.com/adamchainz/blacken-docs - rev: "1.19.0" + rev: "1.19.1" hooks: - id: blacken-docs files: pathlibutil/ diff --git a/README.md b/README.md index 9c461bc..d4be9fd 100644 --- a/README.md +++ b/README.md @@ -141,6 +141,7 @@ optional `str` functions can be added to `` with a dot - `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: @@ -206,6 +207,20 @@ repos: files: docs/.*\.jinja2$ ``` +Use `additional_dependencies` to add extra dependencies to the pre-commit environment. Example see below. + +> This is necessary when a module or source code rendered into your template contains modules that are not part of the standard library. + +```yaml +repos: + - repo: https://github.com/d-chris/jinja2_pdoc/ + rev: v1.1.0 + hooks: + - id: jinja2pdoc + files: docs/.*\.jinja2$ + additional_dependencies: [pathlibutil] +``` + ## Dependencies [![PyPI - autopep8](https://img.shields.io/pypi/v/autopep8?logo=pypi&logoColor=white&label=autopep8)](https://pypi.org/project/autopep8/) diff --git a/docs/__main__.py b/docs/__main__.py index 204e6ea..e1153a6 100644 --- a/docs/__main__.py +++ b/docs/__main__.py @@ -104,4 +104,4 @@ def main() -> int: if __name__ == "__main__": - SystemExit(main()) + raise SystemExit(main()) diff --git a/jinja2_pdoc/cli.py b/jinja2_pdoc/cli.py index fee5030..c4c82cd 100644 --- a/jinja2_pdoc/cli.py +++ b/jinja2_pdoc/cli.py @@ -102,7 +102,9 @@ def jinja2pdoc( root = Path(output) if output else cwd - env = Environment() + env = Environment( + keep_trailing_newline=True, + ) def render_file(file): template = file.read_text(encoding) diff --git a/jinja2_pdoc/wrapper.py b/jinja2_pdoc/wrapper.py index 2d571f1..4748960 100644 --- a/jinja2_pdoc/wrapper.py +++ b/jinja2_pdoc/wrapper.py @@ -96,6 +96,11 @@ class PdocStr(str): inhertits from `str` with a `dedent` method """ + _regex_doc = re.compile( + r"^(?:#!.*?)?(?P\"{3}|\'{3}).*?(?P=doc)\s*$", + re.MULTILINE | re.DOTALL, + ) + def dedent(self) -> str: """ remove common whitespace from the left of every line in the string, @@ -110,3 +115,30 @@ def indent(self) -> str: """ s = autopep8.fix_code(self.dedent(), options={"indent_size": 2}) return self.__class__(s) + + def nodoc(self) -> str: + """ + remove shebang and docstring and from the string + """ + s = self._regex_doc.sub("", self.dedent(), 1) + + return self.__class__(s.strip("\n")) + + def __getattribute__(self, name: str) -> Any: + """ + get all known attributes and cast `str` to `PdocStr` + """ + attr = super().__getattribute__(name) + + if callable(attr): + + def wrapper(*args, **kwargs): + result = attr(*args, **kwargs) + if isinstance(result, str): + cls = object.__getattribute__(self, "__class__") + return cls(result) + return result + + return wrapper + + return attr diff --git a/poetry.lock b/poetry.lock index 0d17caf..05f7e97 100644 --- a/poetry.lock +++ b/poetry.lock @@ -565,13 +565,13 @@ files = [ [[package]] name = "tox" -version = "4.23.0" +version = "4.23.2" description = "tox is a generic virtualenv management and test command line tool" optional = false python-versions = ">=3.8" files = [ - {file = "tox-4.23.0-py3-none-any.whl", hash = "sha256:46da40afb660e46238c251280eb910bdaf00b390c7557c8e4bb611f422e9db12"}, - {file = "tox-4.23.0.tar.gz", hash = "sha256:a6bd7d54231d755348d3c3a7b450b5bf6563833716d1299a1619587a1b77a3bf"}, + {file = "tox-4.23.2-py3-none-any.whl", hash = "sha256:452bc32bb031f2282881a2118923176445bac783ab97c874b8770ab4c3b76c38"}, + {file = "tox-4.23.2.tar.gz", hash = "sha256:86075e00e555df6e82e74cfc333917f91ecb47ffbc868dcafbd2672e332f4a2c"}, ] [package.dependencies] @@ -587,6 +587,9 @@ tomli = {version = ">=2.0.1", markers = "python_version < \"3.11\""} typing-extensions = {version = ">=4.12.2", markers = "python_version < \"3.11\""} virtualenv = ">=20.26.6" +[package.extras] +test = ["devpi-process (>=1.0.2)", "pytest (>=8.3.3)", "pytest-mock (>=3.14)"] + [[package]] name = "typing-extensions" version = "4.12.2" diff --git a/tests/test_wrapper.py b/tests/test_wrapper.py index 9a410f3..7143786 100644 --- a/tests/test_wrapper.py +++ b/tests/test_wrapper.py @@ -4,66 +4,133 @@ @pytest.fixture -def doc() -> Module: +def module() -> Module: return Module.from_name("pathlib") @pytest.fixture -def open(doc: Module) -> Function: - return doc.get("Path.open") +def function(module: Module) -> Function: + return module.get("Path.open") @pytest.fixture -def funcstr() -> PdocStr: +def pdocstr() -> PdocStr: return PdocStr("\n".join([" def dummy():", " pass"])) -def test_module(doc: Module): - assert isinstance(doc, Module) +@pytest.fixture(params=["indent", "dedent", "nodoc", "lower", "upper"]) +def pdocstr_attr(request): + return request.param -def test_class(doc: Module): - cls = doc.get("Path") +@pytest.fixture(params=["source", "code", "docstring"]) +def function_prop(request): + return request.param - assert cls.name == "Path" - assert isinstance(cls, Function) - cls = doc.get("NotAClass") - assert cls is None +def test_module(): + m = Module.from_name("pathlib") - func = doc.get("Path.notafunction") - assert func is None + assert isinstance(m, Module) -def test_func(open: Function): - assert open.name == "open" - assert isinstance(open, Function) - assert hasattr(open, "code") +def test_module_raises(): + with pytest.raises(RuntimeError): + Module.from_name("not_a_module") -def test_str(open: Function): - sourcecode = open.code +@pytest.mark.parametrize( + "name, returntype", + [ + ("Path", Function), + ("Path.open", Function), + ("NotAClass", type(None)), + ("Path.notafunction", type(None)), + ], +) +def test_module_returntype(module: Module, name: str, returntype: type): + obj = module.get(name) + assert isinstance(obj, returntype) - assert isinstance(sourcecode, PdocStr) - assert hasattr(sourcecode, "dedent") - assert isinstance(open.docstring, PdocStr) +def test_pdocstr_attributes(pdocstr_attr: str, pdocstr: PdocStr): + assert hasattr(pdocstr, pdocstr_attr) -def test_module_raises(): - with pytest.raises(RuntimeError): - Module.from_name("not_a_module") +def test_pdocstr_returntypes(pdocstr_attr, pdocstr: PdocStr): + method = getattr(pdocstr, pdocstr_attr) -def test_dedent(funcstr: PdocStr): - s = funcstr.dedent() + assert isinstance(method(), PdocStr) - assert isinstance(s, PdocStr) - assert s.startswith("def dummy():\n pass") +def test_pdocstr_callable(pdocstr_attr, pdocstr: PdocStr): + method = getattr(pdocstr, pdocstr_attr) + + assert callable(method) + + +def test_pdocstr_nodoc(): + text = [ + "", + '"""docstring"""', + "", + "def dummy():", # 3 + " pass", + "", + ] + + funcstr = PdocStr("\n".join(text)) + + assert funcstr.nodoc() == "\n".join(text[3:5]) + + +def test_pdocstr_shebang(): + text = [ + "#! python3", + "", + '"""docstring"""', + "", + "def dummy():", # 4 + " pass", + "", + ] + + funcstr = PdocStr("\n".join(text)) + + assert funcstr.nodoc() == "\n".join(text[4:6]) -def test_autopep8(funcstr: PdocStr): - s = funcstr.indent() - assert isinstance(s, PdocStr) +def test_pdocstr_indent(pdocstr: PdocStr): + s = pdocstr.indent() + assert s.startswith("def dummy():\n pass") + + +def test_pdocstr_dedent(pdocstr: PdocStr): + s = pdocstr.dedent() + + assert s.startswith("def dummy():\n pass") + + +def test_function_attributes(function_prop, function): + assert hasattr(function, function_prop) + + +def test_function_returntypes(function_prop, function): + prop = getattr(function, function_prop) + + assert isinstance(prop, PdocStr) + + +def test_function_property(function_prop, function): + prop = getattr(function, function_prop) + + assert not callable(prop) + + +def test_function_code(function): + doc = function.docstring + + assert doc, "testing function should have a docstring" + assert doc not in function.code