pdoc.render_helpers
1from __future__ import annotations 2 3from collections.abc import Collection 4from collections.abc import Iterable 5from collections.abc import Mapping 6from contextlib import contextmanager 7import html 8import inspect 9import os 10import re 11from unittest.mock import patch 12import warnings 13 14from jinja2 import ext 15from jinja2 import nodes 16import pygments.formatters 17import pygments.lexers 18 19try: 20 # Jinja2 >= 3.0 21 from jinja2 import pass_context # type: ignore 22except ImportError: # pragma: no cover 23 from jinja2 import contextfilter as pass_context # type: ignore 24 25from jinja2.runtime import Context 26from markupsafe import Markup 27 28import pdoc.markdown2 29 30from . import docstrings 31from ._compat import cache 32from ._compat import removesuffix 33 34lexer = pygments.lexers.PythonLexer() 35""" 36The pygments lexer used for pdoc.render_helpers.highlight. 37Overwrite this to configure pygments lexing. 38""" 39 40formatter = pygments.formatters.HtmlFormatter( 41 cssclass="pdoc-code codehilite", 42 linenos="inline", 43 anchorlinenos=True, 44) 45""" 46The pygments formatter used for pdoc.render_helpers.highlight. 47Overwrite this to configure pygments highlighting of code blocks. 48 49The usage of the `.codehilite` CSS selector in custom templates is deprecated since pdoc 10, use `.pdoc-code` instead. 50""" 51 52signature_formatter = pygments.formatters.HtmlFormatter(nowrap=True) 53""" 54The pygments formatter used for pdoc.render_helpers.format_signature. 55Overwrite this to configure pygments highlighting of signatures. 56""" 57 58# Keep in sync with the documentation in pdoc/__init__.py! 59markdown_extensions = { 60 "alerts": None, 61 "code-friendly": None, 62 "cuddled-lists": None, 63 "fenced-code-blocks": {"cssclass": formatter.cssclass}, 64 "footnotes": None, 65 "header-ids": None, 66 "link-patterns": None, 67 "markdown-in-html": None, 68 "mermaid": None, 69 "pyshell": None, 70 "strike": None, 71 "tables": None, 72 "task_list": None, 73 "toc": {"depth": 2}, 74} 75""" 76The default extensions loaded for `markdown2`. 77Overwrite this to configure Markdown rendering. 78""" 79markdown_link_patterns = [ 80 ( 81 re.compile( 82 r""" 83 \b 84 ( 85 (?:https?://|(?<!//)www\.) # prefix - https:// or www. 86 \w[\w_\-]*(?:\.\w[\w_\-]*)* # host 87 [^<>\s"']* # rest of url 88 (?<![?!.,:*_~);]) # exclude trailing punctuation 89 (?=[?!.,:*_~);]?(?:[<\s]|$)) # make sure that we're not followed by " or ', i.e. we're outside of href="...". 90 ) 91 """, 92 re.X, 93 ), 94 r"\1", 95 ) 96] 97""" 98Link pattern used for markdown2's [`link-patterns` extra](https://github.com/trentm/python-markdown2/wiki/link-patterns). 99""" 100 101 102@cache 103def highlight(doc: pdoc.doc.Doc) -> str: 104 """Highlight the source code of a documentation object using pygments.""" 105 if isinstance(doc, str): # pragma: no cover 106 warnings.warn( 107 "Passing a string to the `highlight` render helper is deprecated, pass a pdoc.doc.Doc object instead.", 108 DeprecationWarning, 109 ) 110 return Markup(pygments.highlight(doc, lexer, formatter)) 111 112 # set up correct line numbers and anchors 113 formatter.linespans = doc.qualname or "L" 114 formatter.linenostart = doc.source_lines[0] + 1 if doc.source_lines else 1 115 return Markup(pygments.highlight(doc.source, lexer, formatter)) 116 117 118def format_signature(sig: inspect.Signature, colon: bool) -> str: 119 """Format and highlight a function signature using pygments. Returns HTML.""" 120 # First get a list with all params as strings. 121 result = pdoc.doc._PrettySignature._params(sig) # type: ignore 122 return_annot = pdoc.doc._PrettySignature._return_annotation_str(sig) # type: ignore 123 124 multiline = ( 125 sum(len(x) + 2 for x in result) + len(return_annot) 126 > pdoc.doc._PrettySignature.MULTILINE_CUTOFF 127 ) 128 129 def _try_highlight(code: str) -> str: 130 """Try to highlight a piece of code using pygments, but return the input as-is if pygments detects errors.""" 131 pretty = pygments.highlight(code, lexer, signature_formatter).strip() 132 if '<span class="err">' not in pretty: 133 return pretty 134 else: 135 return html.escape(code) 136 137 # Next, individually highlight each parameter using pygments and wrap it in a span.param. 138 # This later allows us to properly control line breaks. 139 pretty_result = [] 140 for i, param in enumerate(result): 141 pretty = _try_highlight(param) 142 if multiline: 143 pretty = f"""<span class="param">\t{pretty},</span>""" 144 else: 145 pretty = f"""<span class="param">{pretty}, </span>""" 146 pretty_result.append(pretty) 147 148 # remove last comma. 149 if pretty_result: 150 pretty_result[-1] = pretty_result[-1].rpartition(",")[0] + "</span>" 151 152 # Add return annotation. 153 anno = ")" 154 if return_annot: 155 anno += f" -> {_try_highlight(return_annot)}" 156 if colon: 157 anno += ":" 158 if return_annot or colon: 159 anno = f'<span class="return-annotation">{anno}</span>' 160 161 rendered = "(" + "".join(pretty_result) + anno 162 163 if multiline: 164 rendered = f'<span class="signature pdoc-code multiline">{rendered}</span>' 165 else: 166 rendered = f'<span class="signature pdoc-code condensed">{rendered}</span>' 167 168 return Markup(rendered) 169 170 171@cache 172def to_html(docstring: str) -> str: 173 """ 174 Convert `docstring` from Markdown to HTML. 175 """ 176 # careful: markdown2 returns a subclass of str with an extra 177 # .toc_html attribute. don't further process the result, 178 # otherwise this attribute will be lost. 179 return pdoc.markdown2.markdown( # type: ignore 180 docstring, 181 extras=markdown_extensions, 182 link_patterns=markdown_link_patterns, 183 ) 184 185 186@pass_context 187def to_markdown_with_context(context: Context, docstring: str) -> str: 188 """ 189 Converts `docstring` from a custom docformat to Markdown (if necessary), and then from Markdown to HTML. 190 """ 191 module: pdoc.doc.Module = context["module"] 192 docformat: str = context["docformat"] 193 return to_markdown(docstring, module, docformat) 194 195 196def to_markdown(docstring: str, module: pdoc.doc.Module, default_docformat: str) -> str: 197 docformat = getattr(module.obj, "__docformat__", default_docformat) or "" 198 return docstrings.convert(docstring, docformat, module.source_file) 199 200 201def possible_sources( 202 all_modules: Collection[str], identifier: str 203) -> Iterable[tuple[str, str]]: 204 """ 205 For a given identifier, return all possible sources where it could originate from. 206 For example, assume `examplepkg._internal.Foo` with all_modules=["examplepkg"]. 207 This could be a Foo class in _internal.py, or a nested `class _internal: class Foo` in examplepkg. 208 We return both candidates as we don't know if _internal.py exists. 209 It may not be in all_modules because it's been excluded by `__all__`. 210 However, if `examplepkg._internal` is in all_modules we know that it can only be that option. 211 212 >>> possible_sources(["examplepkg"], "examplepkg.Foo.bar") 213 examplepkg.Foo, bar 214 examplepkg, Foo.bar 215 """ 216 if identifier in all_modules: 217 yield identifier, "" 218 return 219 220 modulename = identifier 221 qualname = None 222 while modulename: 223 modulename, _, add = modulename.rpartition(".") 224 qualname = f"{add}.{qualname}" if qualname else add 225 yield modulename, qualname 226 if modulename in all_modules: 227 return 228 raise ValueError(f"Invalid identifier: {identifier}") 229 230 231def split_identifier(all_modules: Collection[str], fullname: str) -> tuple[str, str]: 232 """ 233 Split an identifier into a `(modulename, qualname)` tuple. For example, `pdoc.render_helpers.split_identifier` 234 would be split into `("pdoc.render_helpers","split_identifier")`. This is necessary to generate links to the 235 correct module. 236 """ 237 warnings.warn( 238 "pdoc.render_helpers.split_identifier is deprecated and will be removed in a future release. " 239 "Use pdoc.render_helpers.possible_sources instead.", 240 DeprecationWarning, 241 ) 242 *_, last = possible_sources(all_modules, fullname) 243 return last 244 245 246def _relative_link(current: list[str], target: list[str]) -> str: 247 if target == current: 248 return f"../{target[-1]}.html" 249 elif target[: len(current)] == current: 250 return "/".join(target[len(current) :]) + ".html" 251 else: 252 return "../" + _relative_link(current[:-1], target) 253 254 255@cache 256def relative_link(current_module: str, target_module: str) -> str: 257 """Compute the relative link to another module's HTML file.""" 258 if current_module == target_module: 259 return "" 260 return _relative_link( 261 current_module.split(".")[:-1], 262 target_module.split("."), 263 ) 264 265 266def qualname_candidates(identifier: str, context_qualname: str) -> list[str]: 267 """ 268 Given an identifier in a current namespace, return all possible qualnames in the current module. 269 For example, if we are in Foo's subclass Bar and `baz()` is the identifier, 270 return `Foo.Bar.baz()`, `Foo.baz()`, and `baz()`. 271 """ 272 end = len(context_qualname) 273 ret = [] 274 while end > 0: 275 ret.append(f"{context_qualname[:end]}.{identifier}") 276 end = context_qualname.rfind(".", 0, end) 277 ret.append(identifier) 278 return ret 279 280 281def module_candidates(identifier: str, current_module: str) -> Iterable[str]: 282 """ 283 Given an identifier and the current module name, return the module names we should look at 284 to find where the target object is exposed. Module names are ordered by preferences, i.e. 285 we always prefer the current module and then top-level modules over their children. 286 287 >>> module_candidates("foo.bar.baz", "qux") 288 qux 289 foo 290 foo.bar 291 foo.bar.baz 292 >>> module_candidates("foo.bar.baz", "foo.bar") 293 foo.bar 294 foo 295 foo.bar.baz 296 """ 297 yield current_module 298 299 end = identifier.find(".") 300 while end > 0: 301 if (name := identifier[:end]) != current_module: 302 yield name 303 end = identifier.find(".", end + 1) 304 305 if identifier != current_module: 306 yield identifier 307 308 309@pass_context 310def linkify( 311 context: Context, code: str, namespace: str = "", shorten: bool = True 312) -> str: 313 """ 314 Link all identifiers in a block of text. Identifiers referencing unknown modules or modules that 315 are not rendered at the moment will be ignored. 316 A piece of text is considered to be an identifier if it either contains a `.` or is surrounded by `<code>` tags. 317 318 If `shorten` is True, replace identifiers with short forms where possible. 319 For example, replace "current_module.Foo" with "Foo". This is useful for annotations 320 (which are verbose), but undesired for docstrings (where we want to preserve intent). 321 """ 322 323 def linkify_repl(m: re.Match): 324 """ 325 Resolve `text` to the most suitable documentation object. 326 """ 327 text = m.group(0) 328 plain_text = text.replace( 329 '</span><span class="o">.</span><span class="n">', "." 330 ) 331 identifier = removesuffix(plain_text, "()") 332 mod: pdoc.doc.Module = context["module"] 333 334 # Check if this is a relative reference. These cannot be local and need to be resolved. 335 if identifier.startswith("."): 336 taken_from_mod = mod 337 if namespace and (ns := mod.get(namespace)): 338 # Imported from somewhere else, so the relative reference should be from the original module. 339 taken_from_mod = context["all_modules"].get(ns.taken_from[0], mod) 340 if taken_from_mod.is_package: 341 # If we are in __init__.py, we want `.foo` to refer to a child module. 342 parent_module = taken_from_mod.modulename 343 else: 344 # If we are in a leaf module, we want `.foo` to refer to the adjacent module. 345 parent_module = taken_from_mod.modulename.rpartition(".")[0] 346 while identifier.startswith(".."): 347 identifier = identifier[1:] 348 parent_module = parent_module.rpartition(".")[0] 349 identifier = parent_module + identifier 350 else: 351 # Is this a local reference within this module? 352 for qualname in qualname_candidates(identifier, namespace): 353 doc = mod.get(qualname) 354 if doc and context["is_public"](doc).strip(): 355 return f'<a href="#{qualname}">{plain_text}</a>' 356 357 # Is this a reference pointing straight at a module? 358 if identifier in context["all_modules"]: 359 return f'<a href="{relative_link(context["module"].modulename, identifier)}">{identifier}</a>' 360 361 try: 362 sources = list(possible_sources(context["all_modules"], identifier)) 363 except ValueError: 364 # possible_sources did not find a parent module. 365 return text 366 367 # Try to find the actual target object so that we can then later verify 368 # that objects exposed at a parent module with the same name point to it. 369 target_object = None 370 for module_name, qualname in sources: 371 if doc := context["all_modules"].get(module_name, {}).get(qualname): 372 target_object = doc.obj 373 break 374 375 # Look at the different modules where our target object may be exposed. 376 for module_name in module_candidates(identifier, mod.modulename): 377 module: pdoc.doc.Module | None = context["all_modules"].get(module_name) 378 if not module: 379 continue 380 381 for _, qualname in sources: 382 doc = module.get(qualname) 383 # Check if they have an object with the same name, 384 # and verify that it's pointing to the right thing and is public. 385 if ( 386 doc 387 and (target_object is doc.obj or target_object is None) 388 and context["is_public"](doc).strip() 389 ): 390 if shorten: 391 if module == mod: 392 url_text = qualname 393 else: 394 url_text = doc.fullname 395 if plain_text.endswith("()"): 396 url_text += "()" 397 else: 398 url_text = plain_text 399 return f'<a href="{relative_link(context["module"].modulename, doc.modulename)}#{qualname}">{url_text}</a>' 400 401 # No matches found. 402 return text 403 404 return Markup( 405 re.sub( 406 r""" 407 # Part 1: foo.bar or foo.bar() (without backticks) 408 (?<![/=?#&\.]) # heuristic: not part of a URL 409 # First part of the identifier (e.g. "foo") - this is optional for relative references. 410 (?: 411 \b 412 (?!\d)[a-zA-Z0-9_]+ 413 | 414 \.* # We may also start with multiple dots. 415 ) 416 # Rest of the identifier (e.g. ".bar" or "..bar") 417 (?: 418 # A single dot or a dot surrounded with pygments highlighting. 419 (?:\.|</span><span\ class="o">\.</span><span\ class="n">) 420 (?!\d)[a-zA-Z0-9_]+ 421 )+ 422 (?:\(\)|\b(?!\(\))) # we either end on () or on a word boundary. 423 (?!</a>) # not an existing link 424 (?![/#]) # heuristic: not part of a URL 425 426 | # Part 2: `foo` or `foo()`. `foo.bar` is already covered with part 1. 427 (?<=<code>) 428 (?!\d)[a-zA-Z0-9_]+ 429 (?:\(\))? 430 (?=</code>(?!</a>)) 431 """, 432 linkify_repl, 433 code, 434 flags=re.VERBOSE, 435 ) 436 ) 437 438 439@pass_context 440def link(context: Context, spec: tuple[str, str], text: str | None = None) -> str: 441 """Create a link for a specific `(modulename, qualname)` tuple.""" 442 mod: pdoc.doc.Module = context["module"] 443 modulename, qualname = spec 444 445 # Check if the object we are interested is also imported and re-exposed in the current namespace. 446 # https://github.com/mitmproxy/pdoc/issues/490: We need to do this for every level, not just the tail. 447 doc: pdoc.doc.Doc | None = mod 448 for part in qualname.split("."): 449 doc = doc.get(part) if isinstance(doc, pdoc.doc.Namespace) else None 450 if not ( 451 doc 452 and doc.taken_from[0] == modulename 453 and context["is_public"](doc).strip() 454 ): 455 break 456 else: 457 # everything down to the tail is imported and re-exposed. 458 if text: 459 text = text.replace(f"{modulename}.", f"{mod.modulename}.") 460 modulename = mod.modulename 461 462 if mod.modulename == modulename: 463 fullname = qualname 464 else: 465 fullname = removesuffix(f"{modulename}.{qualname}", ".") 466 467 if qualname: 468 qualname = f"#{qualname}" 469 if modulename in context["all_modules"]: 470 return Markup( 471 f'<a href="{relative_link(context["module"].modulename, modulename)}{qualname}">{text or fullname}</a>' 472 ) 473 return text or fullname 474 475 476def edit_url( 477 modulename: str, is_package: bool, mapping: Mapping[str, str] 478) -> str | None: 479 """Create a link to edit a particular file in the used version control system.""" 480 for m, prefix in mapping.items(): 481 if m == modulename or modulename.startswith(f"{m}."): 482 filename = modulename[len(m) + 1 :].replace(".", "/") 483 if is_package: 484 filename = f"{filename}/__init__.py".lstrip("/") 485 else: 486 filename += ".py" 487 return f"{prefix}{filename}" 488 return None 489 490 491def root_module_name(all_modules: Mapping[str, pdoc.doc.Module]) -> str | None: 492 """ 493 Return the name of the (unique) top-level module, or `None` 494 if no such module exists. 495 496 For example, assuming `foo`, `foo.bar`, and `foo.baz` are documented, 497 this function will return `foo`. If `foo` and `bar` are documented, 498 this function will return `None` as there is no unique top-level module. 499 """ 500 shortest_name = min(all_modules, key=len, default=None) 501 prefix = f"{shortest_name}." 502 all_others_are_submodules = all( 503 x.startswith(prefix) or x == shortest_name for x in all_modules 504 ) 505 if all_others_are_submodules: 506 return shortest_name 507 else: 508 return None 509 510 511def minify_css(css: str) -> str: 512 """Do some very basic CSS minification.""" 513 css = re.sub(r"[ ]{4}|\n|(?<=[:{}]) | (?=[{}])", "", css) 514 css = re.sub( 515 r"/\*.+?\*/", lambda m: m.group(0) if m.group(0).startswith("/*!") else "", css 516 ) 517 return Markup(css.replace("<style", "\n<style")) 518 519 520@contextmanager 521def defuse_unsafe_reprs(): 522 """This decorator is applied by pdoc before calling an object's repr(). 523 It applies some heuristics to patch our sensitive information. 524 For example, `os.environ`'s default `__repr__` implementation exposes all 525 local secrets. 526 """ 527 with patch.object(os._Environ, "__repr__", lambda self: "os.environ"): 528 yield 529 530 531class DefaultMacroExtension(ext.Extension): 532 """ 533 This extension provides a new `{% defaultmacro %}` statement, which defines a macro only if it does not exist. 534 535 For example, 536 537 ```html+jinja 538 {% defaultmacro example() %} 539 test 123 540 {% enddefaultmacro %} 541 ``` 542 543 is equivalent to 544 545 ```html+jinja 546 {% macro default_example() %} 547 test 123 548 {% endmacro %} 549 {% if not example %} 550 {% macro example() %} 551 test 123 552 {% endmacro %} 553 {% endif %} 554 ``` 555 556 Additionally, the default implementation is also available as `default_$macroname`, which makes it possible 557 to reference it in the override. 558 """ 559 560 tags = {"defaultmacro"} 561 562 def parse(self, parser): 563 m = nodes.Macro(lineno=next(parser.stream).lineno) 564 name = parser.parse_assign_target(name_only=True).name 565 m.name = f"default_{name}" 566 parser.parse_signature(m) 567 m.body = parser.parse_statements(("name:enddefaultmacro",), drop_needle=True) 568 569 if_stmt = nodes.If( 570 nodes.Not(nodes.Name(name, "load")), 571 [nodes.Macro(name, m.args, m.defaults, m.body)], 572 [], 573 [], 574 ) 575 return [m, if_stmt]
The pygments lexer used for pdoc.render_helpers.highlight. Overwrite this to configure pygments lexing.
The pygments formatter used for pdoc.render_helpers.highlight. Overwrite this to configure pygments highlighting of code blocks.
The usage of the .codehilite
CSS selector in custom templates is deprecated since pdoc 10, use .pdoc-code
instead.
The pygments formatter used for pdoc.render_helpers.format_signature. Overwrite this to configure pygments highlighting of signatures.
The default extensions loaded for markdown2
.
Overwrite this to configure Markdown rendering.
Link pattern used for markdown2's link-patterns
extra.
103@cache 104def highlight(doc: pdoc.doc.Doc) -> str: 105 """Highlight the source code of a documentation object using pygments.""" 106 if isinstance(doc, str): # pragma: no cover 107 warnings.warn( 108 "Passing a string to the `highlight` render helper is deprecated, pass a pdoc.doc.Doc object instead.", 109 DeprecationWarning, 110 ) 111 return Markup(pygments.highlight(doc, lexer, formatter)) 112 113 # set up correct line numbers and anchors 114 formatter.linespans = doc.qualname or "L" 115 formatter.linenostart = doc.source_lines[0] + 1 if doc.source_lines else 1 116 return Markup(pygments.highlight(doc.source, lexer, formatter))
Highlight the source code of a documentation object using pygments.
119def format_signature(sig: inspect.Signature, colon: bool) -> str: 120 """Format and highlight a function signature using pygments. Returns HTML.""" 121 # First get a list with all params as strings. 122 result = pdoc.doc._PrettySignature._params(sig) # type: ignore 123 return_annot = pdoc.doc._PrettySignature._return_annotation_str(sig) # type: ignore 124 125 multiline = ( 126 sum(len(x) + 2 for x in result) + len(return_annot) 127 > pdoc.doc._PrettySignature.MULTILINE_CUTOFF 128 ) 129 130 def _try_highlight(code: str) -> str: 131 """Try to highlight a piece of code using pygments, but return the input as-is if pygments detects errors.""" 132 pretty = pygments.highlight(code, lexer, signature_formatter).strip() 133 if '<span class="err">' not in pretty: 134 return pretty 135 else: 136 return html.escape(code) 137 138 # Next, individually highlight each parameter using pygments and wrap it in a span.param. 139 # This later allows us to properly control line breaks. 140 pretty_result = [] 141 for i, param in enumerate(result): 142 pretty = _try_highlight(param) 143 if multiline: 144 pretty = f"""<span class="param">\t{pretty},</span>""" 145 else: 146 pretty = f"""<span class="param">{pretty}, </span>""" 147 pretty_result.append(pretty) 148 149 # remove last comma. 150 if pretty_result: 151 pretty_result[-1] = pretty_result[-1].rpartition(",")[0] + "</span>" 152 153 # Add return annotation. 154 anno = ")" 155 if return_annot: 156 anno += f" -> {_try_highlight(return_annot)}" 157 if colon: 158 anno += ":" 159 if return_annot or colon: 160 anno = f'<span class="return-annotation">{anno}</span>' 161 162 rendered = "(" + "".join(pretty_result) + anno 163 164 if multiline: 165 rendered = f'<span class="signature pdoc-code multiline">{rendered}</span>' 166 else: 167 rendered = f'<span class="signature pdoc-code condensed">{rendered}</span>' 168 169 return Markup(rendered)
Format and highlight a function signature using pygments. Returns HTML.
172@cache 173def to_html(docstring: str) -> str: 174 """ 175 Convert `docstring` from Markdown to HTML. 176 """ 177 # careful: markdown2 returns a subclass of str with an extra 178 # .toc_html attribute. don't further process the result, 179 # otherwise this attribute will be lost. 180 return pdoc.markdown2.markdown( # type: ignore 181 docstring, 182 extras=markdown_extensions, 183 link_patterns=markdown_link_patterns, 184 )
Convert docstring
from Markdown to HTML.
187@pass_context 188def to_markdown_with_context(context: Context, docstring: str) -> str: 189 """ 190 Converts `docstring` from a custom docformat to Markdown (if necessary), and then from Markdown to HTML. 191 """ 192 module: pdoc.doc.Module = context["module"] 193 docformat: str = context["docformat"] 194 return to_markdown(docstring, module, docformat)
Converts docstring
from a custom docformat to Markdown (if necessary), and then from Markdown to HTML.
202def possible_sources( 203 all_modules: Collection[str], identifier: str 204) -> Iterable[tuple[str, str]]: 205 """ 206 For a given identifier, return all possible sources where it could originate from. 207 For example, assume `examplepkg._internal.Foo` with all_modules=["examplepkg"]. 208 This could be a Foo class in _internal.py, or a nested `class _internal: class Foo` in examplepkg. 209 We return both candidates as we don't know if _internal.py exists. 210 It may not be in all_modules because it's been excluded by `__all__`. 211 However, if `examplepkg._internal` is in all_modules we know that it can only be that option. 212 213 >>> possible_sources(["examplepkg"], "examplepkg.Foo.bar") 214 examplepkg.Foo, bar 215 examplepkg, Foo.bar 216 """ 217 if identifier in all_modules: 218 yield identifier, "" 219 return 220 221 modulename = identifier 222 qualname = None 223 while modulename: 224 modulename, _, add = modulename.rpartition(".") 225 qualname = f"{add}.{qualname}" if qualname else add 226 yield modulename, qualname 227 if modulename in all_modules: 228 return 229 raise ValueError(f"Invalid identifier: {identifier}")
For a given identifier, return all possible sources where it could originate from.
For example, assume examplepkg._internal.Foo
with all_modules=["examplepkg"].
This could be a Foo class in _internal.py, or a nested class _internal: class Foo
in examplepkg.
We return both candidates as we don't know if _internal.py exists.
It may not be in all_modules because it's been excluded by __all__
.
However, if examplepkg._internal
is in all_modules we know that it can only be that option.
>>> possible_sources(["examplepkg"], "examplepkg.Foo.bar")
examplepkg.Foo, bar
examplepkg, Foo.bar
232def split_identifier(all_modules: Collection[str], fullname: str) -> tuple[str, str]: 233 """ 234 Split an identifier into a `(modulename, qualname)` tuple. For example, `pdoc.render_helpers.split_identifier` 235 would be split into `("pdoc.render_helpers","split_identifier")`. This is necessary to generate links to the 236 correct module. 237 """ 238 warnings.warn( 239 "pdoc.render_helpers.split_identifier is deprecated and will be removed in a future release. " 240 "Use pdoc.render_helpers.possible_sources instead.", 241 DeprecationWarning, 242 ) 243 *_, last = possible_sources(all_modules, fullname) 244 return last
Split an identifier into a (modulename, qualname)
tuple. For example, pdoc.render_helpers.split_identifier
would be split into ("pdoc.render_helpers","split_identifier")
. This is necessary to generate links to the
correct module.
256@cache 257def relative_link(current_module: str, target_module: str) -> str: 258 """Compute the relative link to another module's HTML file.""" 259 if current_module == target_module: 260 return "" 261 return _relative_link( 262 current_module.split(".")[:-1], 263 target_module.split("."), 264 )
Compute the relative link to another module's HTML file.
267def qualname_candidates(identifier: str, context_qualname: str) -> list[str]: 268 """ 269 Given an identifier in a current namespace, return all possible qualnames in the current module. 270 For example, if we are in Foo's subclass Bar and `baz()` is the identifier, 271 return `Foo.Bar.baz()`, `Foo.baz()`, and `baz()`. 272 """ 273 end = len(context_qualname) 274 ret = [] 275 while end > 0: 276 ret.append(f"{context_qualname[:end]}.{identifier}") 277 end = context_qualname.rfind(".", 0, end) 278 ret.append(identifier) 279 return ret
Given an identifier in a current namespace, return all possible qualnames in the current module.
For example, if we are in Foo's subclass Bar and baz()
is the identifier,
return Foo.Bar.baz()
, Foo.baz()
, and baz()
.
282def module_candidates(identifier: str, current_module: str) -> Iterable[str]: 283 """ 284 Given an identifier and the current module name, return the module names we should look at 285 to find where the target object is exposed. Module names are ordered by preferences, i.e. 286 we always prefer the current module and then top-level modules over their children. 287 288 >>> module_candidates("foo.bar.baz", "qux") 289 qux 290 foo 291 foo.bar 292 foo.bar.baz 293 >>> module_candidates("foo.bar.baz", "foo.bar") 294 foo.bar 295 foo 296 foo.bar.baz 297 """ 298 yield current_module 299 300 end = identifier.find(".") 301 while end > 0: 302 if (name := identifier[:end]) != current_module: 303 yield name 304 end = identifier.find(".", end + 1) 305 306 if identifier != current_module: 307 yield identifier
Given an identifier and the current module name, return the module names we should look at to find where the target object is exposed. Module names are ordered by preferences, i.e. we always prefer the current module and then top-level modules over their children.
>>> module_candidates("foo.bar.baz", "qux")
qux
foo
foo.bar
foo.bar.baz
>>> module_candidates("foo.bar.baz", "foo.bar")
foo.bar
foo
foo.bar.baz
310@pass_context 311def linkify( 312 context: Context, code: str, namespace: str = "", shorten: bool = True 313) -> str: 314 """ 315 Link all identifiers in a block of text. Identifiers referencing unknown modules or modules that 316 are not rendered at the moment will be ignored. 317 A piece of text is considered to be an identifier if it either contains a `.` or is surrounded by `<code>` tags. 318 319 If `shorten` is True, replace identifiers with short forms where possible. 320 For example, replace "current_module.Foo" with "Foo". This is useful for annotations 321 (which are verbose), but undesired for docstrings (where we want to preserve intent). 322 """ 323 324 def linkify_repl(m: re.Match): 325 """ 326 Resolve `text` to the most suitable documentation object. 327 """ 328 text = m.group(0) 329 plain_text = text.replace( 330 '</span><span class="o">.</span><span class="n">', "." 331 ) 332 identifier = removesuffix(plain_text, "()") 333 mod: pdoc.doc.Module = context["module"] 334 335 # Check if this is a relative reference. These cannot be local and need to be resolved. 336 if identifier.startswith("."): 337 taken_from_mod = mod 338 if namespace and (ns := mod.get(namespace)): 339 # Imported from somewhere else, so the relative reference should be from the original module. 340 taken_from_mod = context["all_modules"].get(ns.taken_from[0], mod) 341 if taken_from_mod.is_package: 342 # If we are in __init__.py, we want `.foo` to refer to a child module. 343 parent_module = taken_from_mod.modulename 344 else: 345 # If we are in a leaf module, we want `.foo` to refer to the adjacent module. 346 parent_module = taken_from_mod.modulename.rpartition(".")[0] 347 while identifier.startswith(".."): 348 identifier = identifier[1:] 349 parent_module = parent_module.rpartition(".")[0] 350 identifier = parent_module + identifier 351 else: 352 # Is this a local reference within this module? 353 for qualname in qualname_candidates(identifier, namespace): 354 doc = mod.get(qualname) 355 if doc and context["is_public"](doc).strip(): 356 return f'<a href="#{qualname}">{plain_text}</a>' 357 358 # Is this a reference pointing straight at a module? 359 if identifier in context["all_modules"]: 360 return f'<a href="{relative_link(context["module"].modulename, identifier)}">{identifier}</a>' 361 362 try: 363 sources = list(possible_sources(context["all_modules"], identifier)) 364 except ValueError: 365 # possible_sources did not find a parent module. 366 return text 367 368 # Try to find the actual target object so that we can then later verify 369 # that objects exposed at a parent module with the same name point to it. 370 target_object = None 371 for module_name, qualname in sources: 372 if doc := context["all_modules"].get(module_name, {}).get(qualname): 373 target_object = doc.obj 374 break 375 376 # Look at the different modules where our target object may be exposed. 377 for module_name in module_candidates(identifier, mod.modulename): 378 module: pdoc.doc.Module | None = context["all_modules"].get(module_name) 379 if not module: 380 continue 381 382 for _, qualname in sources: 383 doc = module.get(qualname) 384 # Check if they have an object with the same name, 385 # and verify that it's pointing to the right thing and is public. 386 if ( 387 doc 388 and (target_object is doc.obj or target_object is None) 389 and context["is_public"](doc).strip() 390 ): 391 if shorten: 392 if module == mod: 393 url_text = qualname 394 else: 395 url_text = doc.fullname 396 if plain_text.endswith("()"): 397 url_text += "()" 398 else: 399 url_text = plain_text 400 return f'<a href="{relative_link(context["module"].modulename, doc.modulename)}#{qualname}">{url_text}</a>' 401 402 # No matches found. 403 return text 404 405 return Markup( 406 re.sub( 407 r""" 408 # Part 1: foo.bar or foo.bar() (without backticks) 409 (?<![/=?#&\.]) # heuristic: not part of a URL 410 # First part of the identifier (e.g. "foo") - this is optional for relative references. 411 (?: 412 \b 413 (?!\d)[a-zA-Z0-9_]+ 414 | 415 \.* # We may also start with multiple dots. 416 ) 417 # Rest of the identifier (e.g. ".bar" or "..bar") 418 (?: 419 # A single dot or a dot surrounded with pygments highlighting. 420 (?:\.|</span><span\ class="o">\.</span><span\ class="n">) 421 (?!\d)[a-zA-Z0-9_]+ 422 )+ 423 (?:\(\)|\b(?!\(\))) # we either end on () or on a word boundary. 424 (?!</a>) # not an existing link 425 (?![/#]) # heuristic: not part of a URL 426 427 | # Part 2: `foo` or `foo()`. `foo.bar` is already covered with part 1. 428 (?<=<code>) 429 (?!\d)[a-zA-Z0-9_]+ 430 (?:\(\))? 431 (?=</code>(?!</a>)) 432 """, 433 linkify_repl, 434 code, 435 flags=re.VERBOSE, 436 ) 437 )
Link all identifiers in a block of text. Identifiers referencing unknown modules or modules that
are not rendered at the moment will be ignored.
A piece of text is considered to be an identifier if it either contains a .
or is surrounded by <code>
tags.
If shorten
is True, replace identifiers with short forms where possible.
For example, replace "current_module.Foo" with "Foo". This is useful for annotations
(which are verbose), but undesired for docstrings (where we want to preserve intent).
440@pass_context 441def link(context: Context, spec: tuple[str, str], text: str | None = None) -> str: 442 """Create a link for a specific `(modulename, qualname)` tuple.""" 443 mod: pdoc.doc.Module = context["module"] 444 modulename, qualname = spec 445 446 # Check if the object we are interested is also imported and re-exposed in the current namespace. 447 # https://github.com/mitmproxy/pdoc/issues/490: We need to do this for every level, not just the tail. 448 doc: pdoc.doc.Doc | None = mod 449 for part in qualname.split("."): 450 doc = doc.get(part) if isinstance(doc, pdoc.doc.Namespace) else None 451 if not ( 452 doc 453 and doc.taken_from[0] == modulename 454 and context["is_public"](doc).strip() 455 ): 456 break 457 else: 458 # everything down to the tail is imported and re-exposed. 459 if text: 460 text = text.replace(f"{modulename}.", f"{mod.modulename}.") 461 modulename = mod.modulename 462 463 if mod.modulename == modulename: 464 fullname = qualname 465 else: 466 fullname = removesuffix(f"{modulename}.{qualname}", ".") 467 468 if qualname: 469 qualname = f"#{qualname}" 470 if modulename in context["all_modules"]: 471 return Markup( 472 f'<a href="{relative_link(context["module"].modulename, modulename)}{qualname}">{text or fullname}</a>' 473 ) 474 return text or fullname
Create a link for a specific (modulename, qualname)
tuple.
477def edit_url( 478 modulename: str, is_package: bool, mapping: Mapping[str, str] 479) -> str | None: 480 """Create a link to edit a particular file in the used version control system.""" 481 for m, prefix in mapping.items(): 482 if m == modulename or modulename.startswith(f"{m}."): 483 filename = modulename[len(m) + 1 :].replace(".", "/") 484 if is_package: 485 filename = f"{filename}/__init__.py".lstrip("/") 486 else: 487 filename += ".py" 488 return f"{prefix}{filename}" 489 return None
Create a link to edit a particular file in the used version control system.
492def root_module_name(all_modules: Mapping[str, pdoc.doc.Module]) -> str | None: 493 """ 494 Return the name of the (unique) top-level module, or `None` 495 if no such module exists. 496 497 For example, assuming `foo`, `foo.bar`, and `foo.baz` are documented, 498 this function will return `foo`. If `foo` and `bar` are documented, 499 this function will return `None` as there is no unique top-level module. 500 """ 501 shortest_name = min(all_modules, key=len, default=None) 502 prefix = f"{shortest_name}." 503 all_others_are_submodules = all( 504 x.startswith(prefix) or x == shortest_name for x in all_modules 505 ) 506 if all_others_are_submodules: 507 return shortest_name 508 else: 509 return None
Return the name of the (unique) top-level module, or None
if no such module exists.
For example, assuming foo
, foo.bar
, and foo.baz
are documented,
this function will return foo
. If foo
and bar
are documented,
this function will return None
as there is no unique top-level module.
512def minify_css(css: str) -> str: 513 """Do some very basic CSS minification.""" 514 css = re.sub(r"[ ]{4}|\n|(?<=[:{}]) | (?=[{}])", "", css) 515 css = re.sub( 516 r"/\*.+?\*/", lambda m: m.group(0) if m.group(0).startswith("/*!") else "", css 517 ) 518 return Markup(css.replace("<style", "\n<style"))
Do some very basic CSS minification.
521@contextmanager 522def defuse_unsafe_reprs(): 523 """This decorator is applied by pdoc before calling an object's repr(). 524 It applies some heuristics to patch our sensitive information. 525 For example, `os.environ`'s default `__repr__` implementation exposes all 526 local secrets. 527 """ 528 with patch.object(os._Environ, "__repr__", lambda self: "os.environ"): 529 yield
This decorator is applied by pdoc before calling an object's repr().
It applies some heuristics to patch our sensitive information.
For example, os.environ
's default __repr__
implementation exposes all
local secrets.
532class DefaultMacroExtension(ext.Extension): 533 """ 534 This extension provides a new `{% defaultmacro %}` statement, which defines a macro only if it does not exist. 535 536 For example, 537 538 ```html+jinja 539 {% defaultmacro example() %} 540 test 123 541 {% enddefaultmacro %} 542 ``` 543 544 is equivalent to 545 546 ```html+jinja 547 {% macro default_example() %} 548 test 123 549 {% endmacro %} 550 {% if not example %} 551 {% macro example() %} 552 test 123 553 {% endmacro %} 554 {% endif %} 555 ``` 556 557 Additionally, the default implementation is also available as `default_$macroname`, which makes it possible 558 to reference it in the override. 559 """ 560 561 tags = {"defaultmacro"} 562 563 def parse(self, parser): 564 m = nodes.Macro(lineno=next(parser.stream).lineno) 565 name = parser.parse_assign_target(name_only=True).name 566 m.name = f"default_{name}" 567 parser.parse_signature(m) 568 m.body = parser.parse_statements(("name:enddefaultmacro",), drop_needle=True) 569 570 if_stmt = nodes.If( 571 nodes.Not(nodes.Name(name, "load")), 572 [nodes.Macro(name, m.args, m.defaults, m.body)], 573 [], 574 [], 575 ) 576 return [m, if_stmt]
This extension provides a new {% defaultmacro %}
statement, which defines a macro only if it does not exist.
For example,
{% defaultmacro example() %}
test 123
{% enddefaultmacro %}
is equivalent to
{% macro default_example() %}
test 123
{% endmacro %}
{% if not example %}
{% macro example() %}
test 123
{% endmacro %}
{% endif %}
Additionally, the default implementation is also available as default_$macroname
, which makes it possible
to reference it in the override.
563 def parse(self, parser): 564 m = nodes.Macro(lineno=next(parser.stream).lineno) 565 name = parser.parse_assign_target(name_only=True).name 566 m.name = f"default_{name}" 567 parser.parse_signature(m) 568 m.body = parser.parse_statements(("name:enddefaultmacro",), drop_needle=True) 569 570 if_stmt = nodes.If( 571 nodes.Not(nodes.Name(name, "load")), 572 [nodes.Macro(name, m.args, m.defaults, m.body)], 573 [], 574 [], 575 ) 576 return [m, if_stmt]
If any of the tags
matched this method is called with the
parser as first argument. The token the parser stream is pointing at
is the name token that matched. This method has to return one or a
list of multiple nodes.
Inherited Members
- jinja2.ext.Extension
- Extension
- priority
- environment
- bind
- preprocess
- filter_stream
- attr
- call_method