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