diff --git a/pdoc/__init__.py b/pdoc/__init__.py index 25189876..4aba1efa 100644 --- a/pdoc/__init__.py +++ b/pdoc/__init__.py @@ -883,6 +883,8 @@ def __init__(self, module: ModuleType, *, docfilter: Callable[[Doc], bool] = Non global context object will be used. """ super().__init__(module.__name__, self, module) + if self.name.endswith('.__init__') and not self.is_package: + self.name = self.name[:-len('.__init__')] self._context = _global_context if context is None else context """ @@ -913,7 +915,7 @@ def __init__(self, module: ModuleType, *, docfilter: Callable[[Doc], bool] = Non else: def is_from_this_module(obj): mod = inspect.getmodule(obj) - return mod is None or mod.__name__ == self.name + return mod is None or mod.__name__ == self.obj.__name__ public_objs = [(name, inspect.unwrap(obj)) for name, obj in inspect.getmembers(self.obj) @@ -1060,7 +1062,7 @@ def find_class(self, cls: type): or in any of the exported identifiers of the submodules. """ # XXX: Is this corrent? Does it always match - # `Class.module.name + Class.qualname`?. + # `Class.module.name + Class.qualname`?. Especially now? # If not, see what was here before. return self.find_ident(cls.__module__ + '.' + cls.__qualname__) diff --git a/pdoc/cli.py b/pdoc/cli.py index ff58329d..f58603ce 100755 --- a/pdoc/cli.py +++ b/pdoc/cli.py @@ -8,6 +8,7 @@ import os.path as path import sys from http.server import BaseHTTPRequestHandler, HTTPServer +from typing import Sequence import pdoc @@ -55,6 +56,11 @@ action="store_true", help="Overwrites any existing HTML files instead of producing an error.", ) +aa("--pdf", + action="store_true", + help="When set, the specified modules will be printed to standard output, " + "formatted in Markdown-Extra, compatible with most " + "Markdown-(to-HTML-)to-PDF converters.") aa( "--external-links", action="store_true", @@ -281,6 +287,18 @@ def write_html_files(m: pdoc.Module): write_html_files(submodule) +def _flatten_submodules(modules: Sequence[pdoc.Module]): + for module in modules: + yield module + for submodule in module.submodules(): + yield from _flatten_submodules((submodule,)) + + +def print_pdf(modules, **kwargs): + modules = list(_flatten_submodules(modules)) + print(pdoc._render_template('/pdf.mako', modules=modules, **kwargs)) + + def main(_args=None): """ Command-line entry point """ global args @@ -338,6 +356,40 @@ def docfilter(obj, _filters=args.filter.strip().split(',')): for module in args.modules] pdoc.link_inheritance() + if args.pdf: + print_pdf(modules) + print(""" +PDF-ready markdown written to standard output. + ^^^^^^^^^^^^^^^ +Convert this file to PDF using e.g. Pandoc: + + pandoc --metadata=title:"MyProject Documentation" \\ + --toc --toc-depth=4 --from=markdown+abbreviations \\ + --pdf-engine=xelatex --variable=mainfont:"DejaVu Sans" \\ + --output=pdf.pdf pdf.md + +or using Python-Markdown and Chrome/Chromium/WkHtmlToPDF: + + markdown_py --extension=meta \\ + --extension=abbr \\ + --extension=attr_list \\ + --extension=def_list \\ + --extension=fenced_code \\ + --extension=footnotes \\ + --extension=tables \\ + --extension=admonition \\ + --extension=smarty \\ + --extension=toc \\ + pdf.md > pdf.html + + chromium --headless --disable-gpu --print-to-pdf=pdf.pdf pdf.html + + wkhtmltopdf -s A4 --print-media-type pdf.html pdf.pdf + +or similar, at your own discretion.""", + file=sys.stderr) + sys.exit(0) + for module in modules: if args.html: _quit_if_exists(module) diff --git a/pdoc/html_helpers.py b/pdoc/html_helpers.py index 3cb38fae..3b1b5415 100644 --- a/pdoc/html_helpers.py +++ b/pdoc/html_helpers.py @@ -292,16 +292,29 @@ def raw_urls(text): def to_html(text: str, docformat: str = 'numpy,google', *, - module: pdoc.Module = None, link: Callable[..., str] = None, - # Matches markdown code spans not +directly+ within links. - # E.g. `code` and [foo is `bar`]() but not [`code`](...) - # Also skips \-escaped grave quotes. - _code_refs=re.compile(r'(?( *))!!! \w+ \"([^\"]*)\"(.*(?:\n(?P=indent) +.*)*)', + lambda m: '{}**{}:** {}'.format(m.group(2), m.group(3), + re.sub('\n {,4}', '\n', m.group(4))), + text, flags=re.MULTILINE) + return text + + def subh(text, level=2): + # Deepen heading levels so H2 becomes H4 etc. + return re.sub(r'\n(#+) +(.+)\n', r'\n%s\1 \2\n' % ('#' * level), text) +%> + +<%def name="title(level, string, id=None)"> + <% id = ' {#%s}' % id if id is not None else '' %> +${('#' * level) + ' ' + string + id} + + +<%def name="funcdef(f)"> +> `${f.funcdef()} ${f.name}(${', '.join(f.params())})` + + +<%def name="classdef(c)"> +> `class ${c.name}(${', '.join(c.params())})` + + +% for module in modules: +<% + submodules = module.submodules() + variables = module.variables() + functions = module.functions() + classes = module.classes() + + def to_md(text): + return _to_md(text, module) +%> +${title(1, 'Module `%s`' % module.name, module.refname)} +${module.docstring | to_md} + +% if submodules: +${title(2, 'Sub-modules')} + % for m in submodules: +* [${m.name}](#${m.refname}) + % endfor +% endif + +% if variables: +${title(2, 'Variables')} + % for v in variables: +${title(3, 'Variable `%s`' % v.name, v.refname)} +${v.docstring | to_md, subh, subh} + % endfor +% endif + +% if functions: +${title(2, 'Functions')} + % for f in functions: +${title(3, 'Function `%s`' % f.name, f.refname)} + +${funcdef(f)} + +${f.docstring | to_md, subh, subh} + % endfor +% endif + +% if classes: +${title(2, 'Classes')} + % for cls in classes: +${title(3, 'Class `%s`' % cls.name, cls.refname)} + +${classdef(cls)} + +${cls.docstring | to_md, subh} +<% + class_vars = cls.class_variables() + static_methods = cls.functions() + inst_vars = cls.instance_variables() + methods = cls.methods() + mro = cls.mro() + subclasses = cls.subclasses() +%> + % if mro: +${title(4, 'Ancestors (in MRO)')} + % for c in mro: +* [${c.refname}](#${c.refname}) + % endfor + % endif + + % if subclasses: +${title(4, 'Descendants')} + % for c in subclasses: +* [${c.refname}](#${c.refname}) + % endfor + % endif + + % if class_vars: +${title(4, 'Class variables')} + % for v in class_vars: +${title(5, 'Variable `%s`' % v.name, v.refname)} +${v.docstring | to_md, subh, subh} + % endfor + % endif + + % if inst_vars: +${title(4, 'Instance variables')} + % for v in inst_vars: +${title(5, 'Variable `%s`' % v.name, v.refname)} +${v.docstring | to_md, subh, subh} + % endfor + % endif + + % if static_methods: +${title(4, 'Static methods')} + % for f in static_methods: +${title(5, '`Method %s`' % f.name, f.refname)} + +${funcdef(f)} + +${f.docstring | to_md, subh, subh} + % endfor + % endif + + % if methods: +${title(4, 'Methods')} + % for f in methods: +${title(5, 'Method `%s`' % f.name, f.refname)} + +${funcdef(f)} + +${f.docstring | to_md, subh, subh} + % endfor + % endif + % endfor +% endif + +##\## for module in modules: +% endfor + +----- +Generated by *pdoc* ${pdoc.__version__} (). diff --git a/pdoc/test/__init__.py b/pdoc/test/__init__.py index 30c83be5..617ef55a 100644 --- a/pdoc/test/__init__.py +++ b/pdoc/test/__init__.py @@ -326,6 +326,17 @@ def test_text_identifier(self): self.assertNotIn('CONST', out) self.assertNotIn('B docstring', out) + def test_pdf(self): + with redirect_streams() as (stdout, stderr): + run('pdoc', pdf=None) + out = stdout.getvalue() + err = stderr.getvalue() + self.assertIn('pdoc3.github.io', out) + self.assertIn('pandoc', err) + + self.assertIn(str(inspect.signature(pdoc.Doc.__init__)).replace('self, ', ''), + out) + class ApiTest(unittest.TestCase): """ @@ -659,6 +670,11 @@ def test_sorting(self): self.assertNotEqual(sorted_methods, unsorted_methods) self.assertEqual(sorted_methods, sorted(unsorted_methods)) + def test_module_init(self): + mod = pdoc.Module(pdoc.import_module('pdoc.__init__')) + self.assertEqual(mod.name, 'pdoc') + self.assertIn('Module', mod.doc) + class HtmlHelpersTest(unittest.TestCase): """ @@ -865,7 +881,7 @@ def test_reST_directives(self):

Example

Image shows something.

-

+

Note

Can only nest admonitions two levels.

diff --git a/pdoc/test/example_pkg/__init__.py b/pdoc/test/example_pkg/__init__.py index 5d6bdf75..1197c1b3 100644 --- a/pdoc/test/example_pkg/__init__.py +++ b/pdoc/test/example_pkg/__init__.py @@ -219,7 +219,7 @@ def reST_directives(self): Image shows something. - .. image:: /logo.png + .. image:: https://www.debian.org/logos/openlogo-nd-100.png .. note:: Can only nest admonitions two levels.