Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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/
Expand Down
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ optional `str` functions can be added to `<pdoc_attr>` 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:

Expand Down Expand Up @@ -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/)
Expand Down
2 changes: 1 addition & 1 deletion docs/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,4 +104,4 @@ def main() -> int:


if __name__ == "__main__":
SystemExit(main())
raise SystemExit(main())
4 changes: 3 additions & 1 deletion jinja2_pdoc/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
32 changes: 32 additions & 0 deletions jinja2_pdoc/wrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,11 @@ class PdocStr(str):
inhertits from `str` with a `dedent` method
"""

_regex_doc = re.compile(
r"^(?:#!.*?)?(?P<doc>\"{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,
Expand All @@ -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
9 changes: 6 additions & 3 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

133 changes: 100 additions & 33 deletions tests/test_wrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -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