[docs] switch generated docs to MkDocs

This commit is contained in:
Jan Petykiewicz 2026-04-18 15:05:35 -07:00
commit a82eb5858a
54 changed files with 350 additions and 2000 deletions

4
.gitignore vendored
View file

@ -54,6 +54,10 @@ coverage.xml
# documentation
doc/
site/
_doc_mathimg/
doc.md
doc.htex
# PyBuilder
target/

View file

@ -114,6 +114,33 @@ The most mature user-facing workflows are:
`meanas.fdtd.accumulate_phasor(...)`, and compare those phasors against an
FDFD reference on the same Yee grid.
## Documentation
API and workflow docs are generated from the package docstrings with
[MkDocs](https://www.mkdocs.org/), [Material for MkDocs](https://squidfunk.github.io/mkdocs-material/),
and [mkdocstrings](https://mkdocstrings.github.io/).
Install the docs toolchain with:
```bash
pip3 install -e './meanas[docs]'
```
Then build the docs site with:
```bash
./make_docs.sh
```
This produces:
- a normal multi-page site under `site/`
- a combined printable single-page HTML site under `site/print_page/`
- an optional fully inlined `site/standalone.html` when `htmlark` is available
The docs build uses a local MathJax bundle vendored under `docs/assets/`, so
the rendered HTML does not rely on external services for equation rendering.
Tracked examples under `examples/` are the intended starting points:
- `examples/fdtd.py`: broadband FDTD pulse excitation, phasor extraction, and a

3
docs/api/eigensolvers.md Normal file
View file

@ -0,0 +1,3 @@
# eigensolvers
::: meanas.eigensolvers

15
docs/api/fdfd.md Normal file
View file

@ -0,0 +1,15 @@
# fdfd
::: meanas.fdfd
## Core operator layers
::: meanas.fdfd.functional
::: meanas.fdfd.operators
::: meanas.fdfd.solvers
::: meanas.fdfd.scpml
::: meanas.fdfd.farfield

13
docs/api/fdmath.md Normal file
View file

@ -0,0 +1,13 @@
# fdmath
::: meanas.fdmath
## Functional and sparse operators
::: meanas.fdmath.functional
::: meanas.fdmath.operators
::: meanas.fdmath.vectorization
::: meanas.fdmath.types

15
docs/api/fdtd.md Normal file
View file

@ -0,0 +1,15 @@
# fdtd
::: meanas.fdtd
## Core update and analysis helpers
::: meanas.fdtd.base
::: meanas.fdtd.pml
::: meanas.fdtd.boundaries
::: meanas.fdtd.energy
::: meanas.fdtd.phasor

14
docs/api/index.md Normal file
View file

@ -0,0 +1,14 @@
# API Overview
The package is documented directly from its docstrings. The most useful entry
points are:
- [meanas](meanas.md): top-level package overview
- [eigensolvers](eigensolvers.md): generic eigenvalue utilities used by the mode solvers
- [fdfd](fdfd.md): frequency-domain operators, sources, PML, solvers, and far-field transforms
- [waveguides](waveguides.md): straight, cylindrical, and 3D waveguide mode helpers
- [fdtd](fdtd.md): timestepping, CPML, energy/flux helpers, and phasor extraction
- [fdmath](fdmath.md): shared discrete operators, vectorization helpers, and derivation background
The waveguide and FDTD pages are the best places to start if you want the
mathematical derivations rather than just the callable reference.

3
docs/api/meanas.md Normal file
View file

@ -0,0 +1,3 @@
# meanas
::: meanas

7
docs/api/waveguides.md Normal file
View file

@ -0,0 +1,7 @@
# waveguides
::: meanas.fdfd.waveguide_2d
::: meanas.fdfd.waveguide_3d
::: meanas.fdfd.waveguide_cyl

1
docs/assets/vendor/mathjax/core.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

1
docs/assets/vendor/mathjax/loader.js vendored Normal file

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,38 @@
{
"etag": "7ab8013dfc2c395304109fe189c1772ef87b366a",
"files": {
"core.js": 212262,
"loader.js": 16370,
"startup.js": 22870,
"input/tex-full.js": 270711,
"input/asciimath.js": 107690,
"input/mml.js": 13601,
"input/mml/entities.js": 32232,
"output/chtml.js": 211041,
"output/chtml/fonts/tex.js": 104359,
"output/chtml/fonts/woff-v2/MathJax_Zero.woff": 1368,
"output/chtml/fonts/woff-v2/MathJax_Vector-Regular.woff": 1136,
"output/chtml/fonts/woff-v2/MathJax_Vector-Bold.woff": 1116,
"output/chtml/fonts/woff-v2/MathJax_Typewriter-Regular.woff": 17604,
"output/chtml/fonts/woff-v2/MathJax_Size4-Regular.woff": 5148,
"output/chtml/fonts/woff-v2/MathJax_Size3-Regular.woff": 3244,
"output/chtml/fonts/woff-v2/MathJax_Size2-Regular.woff": 5464,
"output/chtml/fonts/woff-v2/MathJax_Size1-Regular.woff": 5792,
"output/chtml/fonts/woff-v2/MathJax_Script-Regular.woff": 11852,
"output/chtml/fonts/woff-v2/MathJax_SansSerif-Regular.woff": 12660,
"output/chtml/fonts/woff-v2/MathJax_SansSerif-Italic.woff": 14628,
"output/chtml/fonts/woff-v2/MathJax_SansSerif-Bold.woff": 15944,
"output/chtml/fonts/woff-v2/MathJax_Math-Regular.woff": 19288,
"output/chtml/fonts/woff-v2/MathJax_Math-Italic.woff": 19360,
"output/chtml/fonts/woff-v2/MathJax_Math-BoldItalic.woff": 19776,
"output/chtml/fonts/woff-v2/MathJax_Main-Regular.woff": 34160,
"output/chtml/fonts/woff-v2/MathJax_Main-Italic.woff": 20832,
"output/chtml/fonts/woff-v2/MathJax_Main-Bold.woff": 34464,
"output/chtml/fonts/woff-v2/MathJax_Fraktur-Regular.woff": 21480,
"output/chtml/fonts/woff-v2/MathJax_Fraktur-Bold.woff": 22340,
"output/chtml/fonts/woff-v2/MathJax_Calligraphic-Regular.woff": 9600,
"output/chtml/fonts/woff-v2/MathJax_Calligraphic-Bold.woff": 9908,
"output/chtml/fonts/woff-v2/MathJax_AMS-Regular.woff": 40808
},
"version": "3.1.4"
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

1
docs/assets/vendor/mathjax/startup.js vendored Normal file

File diff suppressed because one or more lines are too long

33
docs/index.md Normal file
View file

@ -0,0 +1,33 @@
# meanas
`meanas` is a Python package for finite-difference electromagnetic simulation.
It combines:
- `meanas.fdfd` for frequency-domain operators, sources, waveguide modes, and SCPML
- `meanas.fdtd` for Yee-grid timestepping, CPML, energy/flux accounting, and phasor extraction
- `meanas.fdmath` for the shared discrete operators and derivations underneath both solvers
This documentation is built directly from the package docstrings. The API pages
are the source of truth for the mathematical derivations and calling
conventions.
## Recommended starting points
- Use the [FDTD API](api/fdtd.md) when you need time-domain stepping, CPML, or
phasor extraction.
- Use the [FDFD API](api/fdfd.md) when you need driven frequency-domain solves
or operator algebra.
- Use the [Waveguide API](api/waveguides.md) for mode solving, port sources, and
overlap windows.
- Use the [fdmath API](api/fdmath.md) when you need the lower-level finite-difference
operators or the derivation background shared across the package.
## Build outputs
The docs build generates two HTML views from the same source:
- a normal multi-page site
- a print-oriented combined page under `site/print_page/`
If `htmlark` is installed, `./make_docs.sh` also writes a fully inlined
`site/standalone.html`.

View file

@ -0,0 +1,19 @@
window.MathJax = {
loader: {
load: ["input/tex-full", "output/chtml"]
},
tex: {
processEscapes: true,
processEnvironments: true
},
options: {
ignoreHtmlClass: ".*|",
processHtmlClass: "arithmatex"
}
};
document$.subscribe(() => {
MathJax.typesetPromise?.(
document.querySelectorAll(".arithmatex")
).catch((error) => console.error(error));
});

View file

@ -0,0 +1,13 @@
.md-typeset .arithmatex {
overflow-x: auto;
}
.md-typeset .doc-contents {
overflow-wrap: anywhere;
}
.md-typeset h1 code,
.md-typeset h2 code,
.md-typeset h3 code {
word-break: break-word;
}

View file

@ -2,18 +2,12 @@
set -Eeuo pipefail
cd ~/projects/meanas
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$ROOT"
# Approach 1: pdf to html?
#pdoc3 --pdf --force --template-dir pdoc_templates -o doc . | \
# pandoc --metadata=title:"meanas" --toc --toc-depth=4 --from=markdown+abbreviations --to=html --output=doc.html --gladtex -s -
mkdocs build --clean
# Approach 2: pdf to html with gladtex
rm -rf _doc_mathimg
pdoc --pdf --force --template-dir pdoc_templates -o doc . > doc.md
pandoc --metadata=title:"meanas" --from=markdown+abbreviations --to=html --output=doc.htex --gladtex -s --css pdoc_templates/pdoc.css doc.md
gladtex -a -n -d _doc_mathimg -c white -b black doc.htex
# Approach 3: html with gladtex
#pdoc3 --html --force --template-dir pdoc_templates -o doc .
#find doc -iname '*.html' -exec gladtex -a -n -d _mathimg -c white {} \;
PRINT_PAGE='site/print_page/index.html'
if [[ -f "$PRINT_PAGE" ]] && command -v htmlark >/dev/null 2>&1; then
htmlark "$PRINT_PAGE" -o site/standalone.html
fi

76
mkdocs.yml Normal file
View file

@ -0,0 +1,76 @@
site_name: meanas
site_description: Electromagnetic simulation tools
site_url: ""
repo_url: https://mpxd.net/code/jan/meanas
repo_name: meanas
docs_dir: docs
site_dir: site
strict: false
theme:
name: material
font: false
features:
- navigation.indexes
- navigation.sections
- navigation.top
- content.code.copy
- toc.follow
nav:
- Home: index.md
- API:
- Overview: api/index.md
- meanas: api/meanas.md
- eigensolvers: api/eigensolvers.md
- fdfd: api/fdfd.md
- waveguides: api/waveguides.md
- fdtd: api/fdtd.md
- fdmath: api/fdmath.md
plugins:
- search
- mkdocstrings:
handlers:
python:
paths:
- .
options:
show_root_heading: true
show_root_toc_entry: false
show_source: false
show_signature_annotations: true
show_symbol_type_heading: true
show_symbol_type_toc: true
members_order: source
separate_signature: true
merge_init_into_class: true
docstring_style: google
- print-site
markdown_extensions:
- admonition
- attr_list
- md_in_html
- tables
- toc:
permalink: true
- pymdownx.arithmatex:
generic: true
- pymdownx.highlight:
anchor_linenums: true
- pymdownx.inlinehilite
- pymdownx.snippets
- pymdownx.superfences
- pymdownx.tabbed:
alternate_style: true
extra_css:
- stylesheets/extra.css
extra_javascript:
- javascripts/mathjax.js
- assets/vendor/mathjax/startup.js
watch:
- meanas

View file

@ -1,47 +0,0 @@
<%!
# Template configuration. Copy over in your template directory
# (used with --template-dir) and adapt as required.
html_lang = 'en'
show_inherited_members = False
extract_module_toc_into_sidebar = True
list_class_variables_in_index = True
sort_identifiers = True
show_type_annotations = True
# Show collapsed source code block next to each item.
# Disabling this can improve rendering speed of large modules.
show_source_code = True
# If set, format links to objects in online source code repository
# according to this template. Supported keywords for interpolation
# are: commit, path, start_line, end_line.
#git_link_template = 'https://github.com/USER/PROJECT/blob/{commit}/{path}#L{start_line}-L{end_line}'
#git_link_template = 'https://gitlab.com/USER/PROJECT/blob/{commit}/{path}#L{start_line}-L{end_line}'
#git_link_template = 'https://bitbucket.org/USER/PROJECT/src/{commit}/{path}#lines-{start_line}:{end_line}'
#git_link_template = 'https://CGIT_HOSTNAME/PROJECT/tree/{path}?id={commit}#n{start_line}'
#git_link_template = None
git_link_template = 'https://mpxd.net/code/jan/meanas/src/commit/{commit}/{path}#L{start_line}-L{end_line}'
# A prefix to use for every HTML hyperlink in the generated documentation.
# No prefix results in all links being relative.
link_prefix = ''
# Enable syntax highlighting for code/source blocks by including Highlight.js
syntax_highlighting = True
# Set the style keyword such as 'atom-one-light' or 'github-gist'
# Options: https://github.com/highlightjs/highlight.js/tree/master/src/styles
# Demo: https://highlightjs.org/static/demo/
hljs_style = 'github'
# If set, insert Google Analytics tracking code. Value is GA
# tracking id (UA-XXXXXX-Y).
google_analytics = ''
# If set, render LaTeX math syntax within \(...\) (inline equations),
# or within \[...\] or $$...$$ or `.. math::` (block equations)
# as nicely-formatted math formulas using MathJax.
# Note: in Python docstrings, either all backslashes need to be escaped (\\)
# or you need to use raw r-strings.
latex_math = True
%>

View file

@ -1,389 +0,0 @@
<%!
from pdoc.html_helpers import minify_css
%>
<%def name="mobile()" filter="minify_css">
.flex {
display: flex !important;
}
body {
line-height: 1.5em;
background: black;
color: #DDD;
}
#content {
padding: 20px;
}
#sidebar {
padding: 30px;
overflow: hidden;
}
.http-server-breadcrumbs {
font-size: 130%;
margin: 0 0 15px 0;
}
#footer {
font-size: .75em;
padding: 5px 30px;
border-top: 1px solid #ddd;
text-align: right;
}
#footer p {
margin: 0 0 0 1em;
display: inline-block;
}
#footer p:last-child {
margin-right: 30px;
}
h1, h2, h3, h4, h5 {
font-weight: 300;
}
h1 {
font-size: 2.5em;
line-height: 1.1em;
}
h2 {
font-size: 1.75em;
margin: 1em 0 .50em 0;
}
h3 {
font-size: 1.4em;
margin: 25px 0 10px 0;
}
h4 {
margin: 0;
font-size: 105%;
}
a {
color: #999;
text-decoration: none;
transition: color .3s ease-in-out;
}
a:hover {
color: #18d;
}
.title code {
font-weight: bold;
}
h2[id^="header-"] {
margin-top: 2em;
}
.ident {
color: #7ff;
}
pre code {
background: transparent;
font-size: .8em;
line-height: 1.4em;
}
code {
background: #0d0d0e;
padding: 1px 4px;
overflow-wrap: break-word;
}
h1 code { background: transparent }
pre {
background: #111;
border: 0;
border-top: 1px solid #ccc;
border-bottom: 1px solid #ccc;
margin: 1em 0;
padding: 1ex;
}
#http-server-module-list {
display: flex;
flex-flow: column;
}
#http-server-module-list div {
display: flex;
}
#http-server-module-list dt {
min-width: 10%;
}
#http-server-module-list p {
margin-top: 0;
}
.toc ul,
#index {
list-style-type: none;
margin: 0;
padding: 0;
}
#index code {
background: transparent;
}
#index h3 {
border-bottom: 1px solid #ddd;
}
#index ul {
padding: 0;
}
#index h4 {
font-weight: bold;
}
#index h4 + ul {
margin-bottom:.6em;
}
/* Make TOC lists have 2+ columns when viewport is wide enough.
Assuming ~20-character identifiers and ~30% wide sidebar. */
@media (min-width: 200ex) { #index .two-column { column-count: 2 } }
@media (min-width: 300ex) { #index .two-column { column-count: 3 } }
dl {
margin-bottom: 2em;
}
dl dl:last-child {
margin-bottom: 4em;
}
dd {
margin: 0 0 1em 3em;
}
#header-classes + dl > dd {
margin-bottom: 3em;
}
dd dd {
margin-left: 2em;
}
dd p {
margin: 10px 0;
}
.name {
background: #111;
font-weight: bold;
font-size: .85em;
padding: 5px 10px;
display: inline-block;
min-width: 40%;
}
.name:hover {
background: #101010;
}
.name > span:first-child {
white-space: nowrap;
}
.name.class > span:nth-child(2) {
margin-left: .4em;
}
.inherited {
color: #777;
border-left: 5px solid #eee;
padding-left: 1em;
}
.inheritance em {
font-style: normal;
font-weight: bold;
}
/* Docstrings titles, e.g. in numpydoc format */
.desc h2 {
font-weight: 400;
font-size: 1.25em;
}
.desc h3 {
font-size: 1em;
}
.desc dt code {
background: inherit; /* Don't grey-back parameters */
}
.source summary,
.git-link-div {
color: #aaa;
text-align: right;
font-weight: 400;
font-size: .8em;
text-transform: uppercase;
}
.source summary > * {
white-space: nowrap;
cursor: pointer;
}
.git-link {
color: inherit;
margin-left: 1em;
}
.source pre {
max-height: 500px;
overflow: auto;
margin: 0;
}
.source pre code {
font-size: 12px;
overflow: visible;
}
.hlist {
list-style: none;
}
.hlist li {
display: inline;
}
.hlist li:after {
content: ',\2002';
}
.hlist li:last-child:after {
content: none;
}
.hlist .hlist {
display: inline;
padding-left: 1em;
}
img {
max-width: 100%;
}
.admonition {
padding: .1em .5em;
margin-bottom: 1em;
}
.admonition-title {
font-weight: bold;
}
.admonition.note,
.admonition.info,
.admonition.important {
background: #610;
}
.admonition.todo,
.admonition.versionadded,
.admonition.tip,
.admonition.hint {
background: #202;
}
.admonition.warning,
.admonition.versionchanged,
.admonition.deprecated {
background: #02b;
}
.admonition.error,
.admonition.danger,
.admonition.caution {
background: darkpink;
}
</%def>
<%def name="desktop()" filter="minify_css">
@media screen and (min-width: 700px) {
#sidebar {
width: 30%;
}
#content {
width: 70%;
max-width: 100ch;
padding: 3em 4em;
border-left: 1px solid #ddd;
}
pre code {
font-size: 1em;
}
.item .name {
font-size: 1em;
}
main {
display: flex;
flex-direction: row-reverse;
justify-content: flex-end;
}
.toc ul ul,
#index ul {
padding-left: 1.5em;
}
.toc > ul > li {
margin-top: .5em;
}
}
</%def>
<%def name="print()" filter="minify_css">
@media print {
#sidebar h1 {
page-break-before: always;
}
.source {
display: none;
}
}
@media print {
* {
background: transparent !important;
color: #000 !important; /* Black prints faster: h5bp.com/s */
box-shadow: none !important;
text-shadow: none !important;
}
a[href]:after {
content: " (" attr(href) ")";
font-size: 90%;
}
/* Internal, documentation links, recognized by having a title,
don't need the URL explicity stated. */
a[href][title]:after {
content: none;
}
abbr[title]:after {
content: " (" attr(title) ")";
}
/*
* Don't show links for images, or javascript/internal links
*/
.ir a:after,
a[href^="javascript:"]:after,
a[href^="#"]:after {
content: "";
}
pre,
blockquote {
border: 1px solid #999;
page-break-inside: avoid;
}
thead {
display: table-header-group; /* h5bp.com/t */
}
tr,
img {
page-break-inside: avoid;
}
img {
max-width: 100% !important;
}
@page {
margin: 0.5cm;
}
p,
h2,
h3 {
orphans: 3;
widows: 3;
}
h1,
h2,
h3,
h4,
h5,
h6 {
page-break-after: avoid;
}
}
</%def>

View file

@ -1,445 +0,0 @@
<%
import os
import pdoc
from pdoc.html_helpers import extract_toc, glimpse, to_html as _to_html, format_git_link, _md, to_markdown
from markdown.inlinepatterns import InlineProcessor
from markdown.util import AtomicString, etree
def link(d, name=None, fmt='{}'):
name = fmt.format(name or d.qualname + ('()' if isinstance(d, pdoc.Function) else ''))
if not isinstance(d, pdoc.Doc) or isinstance(d, pdoc.External) and not external_links:
return name
url = d.url(relative_to=module, link_prefix=link_prefix,
top_ancestor=not show_inherited_members)
return '<a title="{}" href="{}">{}</a>'.format(d.refname, url, name)
# Altered latex delimeters (allow inline $...$, wrap in <eq></eq>)
class _MathPattern(InlineProcessor):
NAME = 'pdoc-math'
PATTERN = r'(?<!\S|\\)(?:\\\((.+?)\\\)|\\\[(.+?)\\\]|\$\$(.+?)\$\$|\$(\S.*?)\$)'
PRIORITY = 181 # Larger than that of 'escape' pattern
def handleMatch(self, m, data):
for value, is_block in zip(m.groups(), (False, True, True, False)):
if value:
break
wrapper = etree.Element('eq')
wrapper.text = AtomicString(value)
return wrapper, m.start(0), m.end(0)
def to_html(text: str):
if not latex_math and _MathPattern.NAME in _md.inlinePatterns:
_md.inlinePatterns.deregister(_MathPattern.NAME)
elif latex_math and _MathPattern.NAME not in _md.inlinePatterns:
_md.inlinePatterns.register(_MathPattern(_MathPattern.PATTERN),
_MathPattern.NAME,
_MathPattern.PRIORITY)
md = to_markdown(text, docformat='numpy,google', module=module, link=link)
return _md.reset().convert(md)
# def to_html(text):
# return _to_html(text, module=module, link=link, latex_math=latex_math)
%>
<%def name="ident(name)"><span class="ident">${name}</span></%def>
<%def name="show_source(d)">
% if (show_source_code or git_link_template) and d.source and d.obj is not getattr(d.inherits, 'obj', None):
<% git_link = format_git_link(git_link_template, d) %>
% if show_source_code:
<details class="source">
<summary>
<span>Expand source code</span>
% if git_link:
<a href="${git_link}" class="git-link">Browse git</a>
%endif
</summary>
<pre><code class="python">${d.source | h}</code></pre>
</details>
% elif git_link:
<div class="git-link-div"><a href="${git_link}" class="git-link">Browse git</a></div>
%endif
%endif
</%def>
<%def name="show_desc(d, short=False)">
<%
inherits = ' inherited' if d.inherits else ''
docstring = glimpse(d.docstring) if short or inherits else d.docstring
%>
% if d.inherits:
<p class="inheritance">
<em>Inherited from:</em>
% if hasattr(d.inherits, 'cls'):
<code>${link(d.inherits.cls)}</code>.<code>${link(d.inherits, d.name)}</code>
% else:
<code>${link(d.inherits)}</code>
% endif
</p>
% endif
<section class="desc${inherits}">${docstring | to_html}</section>
% if not isinstance(d, pdoc.Module):
${show_source(d)}
% endif
</%def>
<%def name="show_module_list(modules)">
<h1>Python module list</h1>
% if not modules:
<p>No modules found.</p>
% else:
<dl id="http-server-module-list">
% for name, desc in modules:
<div class="flex">
<dt><a href="${link_prefix}${name}">${name}</a></dt>
<dd>${desc | glimpse, to_html}</dd>
</div>
% endfor
</dl>
% endif
</%def>
<%def name="show_column_list(items)">
<%
two_column = len(items) >= 6 and all(len(i.name) < 20 for i in items)
%>
<ul class="${'two-column' if two_column else ''}">
% for item in items:
<li><code>${link(item, item.name)}</code></li>
% endfor
</ul>
</%def>
<%def name="show_module(module)">
<%
variables = module.variables(sort=sort_identifiers)
classes = module.classes(sort=sort_identifiers)
functions = module.functions(sort=sort_identifiers)
submodules = module.submodules()
%>
<%def name="show_func(f)">
<dt id="${f.refname}"><code class="name flex">
<%
params = ', '.join(f.params(annotate=show_type_annotations, link=link))
returns = show_type_annotations and f.return_annotation(link=link) or ''
if returns:
returns = ' ->\N{NBSP}' + returns
%>
<span>${f.funcdef()} ${ident(f.name)}</span>(<span>${params})${returns}</span>
</code></dt>
<dd>${show_desc(f)}</dd>
</%def>
<header>
% if http_server:
<nav class="http-server-breadcrumbs">
<a href="/">All packages</a>
<% parts = module.name.split('.')[:-1] %>
% for i, m in enumerate(parts):
<% parent = '.'.join(parts[:i+1]) %>
:: <a href="/${parent.replace('.', '/')}/">${parent}</a>
% endfor
</nav>
% endif
<h1 class="title">${'Namespace' if module.is_namespace else 'Module'} <code>${module.name}</code></h1>
</header>
<section id="section-intro">
${module.docstring | to_html}
${show_source(module)}
</section>
<section>
% if submodules:
<h2 class="section-title" id="header-submodules">Sub-modules</h2>
<dl>
% for m in submodules:
<dt><code class="name">${link(m)}</code></dt>
<dd>${show_desc(m, short=True)}</dd>
% endfor
</dl>
% endif
</section>
<section>
% if variables:
<h2 class="section-title" id="header-variables">Global variables</h2>
<dl>
% for v in variables:
<dt id="${v.refname}"><code class="name">var ${ident(v.name)}</code></dt>
<dd>${show_desc(v)}</dd>
% endfor
</dl>
% endif
</section>
<section>
% if functions:
<h2 class="section-title" id="header-functions">Functions</h2>
<dl>
% for f in functions:
${show_func(f)}
% endfor
</dl>
% endif
</section>
<section>
% if classes:
<h2 class="section-title" id="header-classes">Classes</h2>
<dl>
% for c in classes:
<%
class_vars = c.class_variables(show_inherited_members, sort=sort_identifiers)
smethods = c.functions(show_inherited_members, sort=sort_identifiers)
inst_vars = c.instance_variables(show_inherited_members, sort=sort_identifiers)
methods = c.methods(show_inherited_members, sort=sort_identifiers)
mro = c.mro()
subclasses = c.subclasses()
params = ', '.join(c.params(annotate=show_type_annotations, link=link))
%>
<dt id="${c.refname}"><code class="flex name class">
<span>class ${ident(c.name)}</span>
% if params:
<span>(</span><span>${params})</span>
% endif
</code></dt>
<dd>${show_desc(c)}
% if mro:
<h3>Ancestors</h3>
<ul class="hlist">
% for cls in mro:
<li>${link(cls)}</li>
% endfor
</ul>
%endif
% if subclasses:
<h3>Subclasses</h3>
<ul class="hlist">
% for sub in subclasses:
<li>${link(sub)}</li>
% endfor
</ul>
% endif
% if class_vars:
<h3>Class variables</h3>
<dl>
% for v in class_vars:
<dt id="${v.refname}"><code class="name">var ${ident(v.name)}</code></dt>
<dd>${show_desc(v)}</dd>
% endfor
</dl>
% endif
% if smethods:
<h3>Static methods</h3>
<dl>
% for f in smethods:
${show_func(f)}
% endfor
</dl>
% endif
% if inst_vars:
<h3>Instance variables</h3>
<dl>
% for v in inst_vars:
<dt id="${v.refname}"><code class="name">var ${ident(v.name)}</code></dt>
<dd>${show_desc(v)}</dd>
% endfor
</dl>
% endif
% if methods:
<h3>Methods</h3>
<dl>
% for f in methods:
${show_func(f)}
% endfor
</dl>
% endif
% if not show_inherited_members:
<%
members = c.inherited_members()
%>
% if members:
<h3>Inherited members</h3>
<ul class="hlist">
% for cls, mems in members:
<li><code><b>${link(cls)}</b></code>:
<ul class="hlist">
% for m in mems:
<li><code>${link(m, name=m.name)}</code></li>
% endfor
</ul>
</li>
% endfor
</ul>
% endif
% endif
</dd>
% endfor
</dl>
% endif
</section>
</%def>
<%def name="module_index(module)">
<%
variables = module.variables(sort=sort_identifiers)
classes = module.classes(sort=sort_identifiers)
functions = module.functions(sort=sort_identifiers)
submodules = module.submodules()
supermodule = module.supermodule
%>
<nav id="sidebar">
<%include file="logo.mako"/>
<h1>Index</h1>
${extract_toc(module.docstring) if extract_module_toc_into_sidebar else ''}
<ul id="index">
% if supermodule:
<li><h3>Super-module</h3>
<ul>
<li><code>${link(supermodule)}</code></li>
</ul>
</li>
% endif
% if submodules:
<li><h3><a href="#header-submodules">Sub-modules</a></h3>
<ul>
% for m in submodules:
<li><code>${link(m)}</code></li>
% endfor
</ul>
</li>
% endif
% if variables:
<li><h3><a href="#header-variables">Global variables</a></h3>
${show_column_list(variables)}
</li>
% endif
% if functions:
<li><h3><a href="#header-functions">Functions</a></h3>
${show_column_list(functions)}
</li>
% endif
% if classes:
<li><h3><a href="#header-classes">Classes</a></h3>
<ul>
% for c in classes:
<li>
<h4><code>${link(c)}</code></h4>
<%
members = c.functions(sort=sort_identifiers) + c.methods(sort=sort_identifiers)
if list_class_variables_in_index:
members += (c.instance_variables(sort=sort_identifiers) +
c.class_variables(sort=sort_identifiers))
if not show_inherited_members:
members = [i for i in members if not i.inherits]
if sort_identifiers:
members = sorted(members)
%>
% if members:
${show_column_list(members)}
% endif
</li>
% endfor
</ul>
</li>
% endif
</ul>
</nav>
</%def>
<!doctype html>
<html lang="${html_lang}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1" />
<meta name="generator" content="pdoc ${pdoc.__version__}" />
<%
module_list = 'modules' in context.keys() # Whether we're showing module list in server mode
%>
% if module_list:
<title>Python module list</title>
<meta name="description" content="A list of documented Python modules." />
% else:
<title>${module.name} API documentation</title>
<meta name="description" content="${module.docstring | glimpse, trim, h}" />
% endif
<link href='https://mpxd.net/scripts/normalize.css/normalize.css' rel='stylesheet'>
<link href='https://mpxd.net/scripts/sanitize.css/sanitize.css' rel='stylesheet'>
% if syntax_highlighting:
<link href="https://mpxd.net/scripts/highlightjs/styles/${hljs_style}.min.css" rel="stylesheet">
%endif
<%namespace name="css" file="css.mako" />
<style>${css.mobile()}</style>
<style media="screen and (min-width: 700px)">${css.desktop()}</style>
<style media="print">${css.print()}</style>
% if google_analytics:
<script>
window.ga=window.ga||function(){(ga.q=ga.q||[]).push(arguments)};ga.l=+new Date;
ga('create', '${google_analytics}', 'auto'); ga('send', 'pageview');
</script><script async src='https://www.google-analytics.com/analytics.js'></script>
% endif
<%include file="head.mako"/>
</head>
<body>
<main>
% if module_list:
<article id="content">
${show_module_list(modules)}
</article>
% else:
<article id="content">
${show_module(module)}
</article>
${module_index(module)}
% endif
</main>
<footer id="footer">
<%include file="credits.mako"/>
<p>Generated by <a href="https://pdoc3.github.io/pdoc"><cite>pdoc</cite> ${pdoc.__version__}</a>.</p>
</footer>
% if syntax_highlighting:
<script src="https://mpxd.net/scripts/highlightjs/highlight.pack.js"></script>
<script>hljs.initHighlightingOnLoad()</script>
% endif
% if http_server and module: ## Auto-reload on file change in dev mode
<script>
setInterval(() =>
fetch(window.location.href, {
method: "HEAD",
cache: "no-store",
headers: {"If-None-Match": "${os.stat(module.obj.__file__).st_mtime}"},
}).then(response => response.ok && window.location.reload()), 700);
</script>
% endif
</body>
</html>

View file

@ -1,539 +0,0 @@
"""
Helper functions for HTML output.
"""
import inspect
import os
import re
import subprocess
import traceback
from functools import partial, lru_cache
from typing import Callable, Match
from warnings import warn
import markdown
from markdown.inlinepatterns import InlineProcessor
from markdown.util import AtomicString, etree
import pdoc
@lru_cache()
def minify_css(css: str,
_whitespace=partial(re.compile(r'\s*([,{:;}])\s*').sub, r'\1'),
_comments=partial(re.compile(r'/\*.*?\*/', flags=re.DOTALL).sub, ''),
_trailing_semicolon=partial(re.compile(r';\s*}').sub, '}')):
"""
Minify CSS by removing extraneous whitespace, comments, and trailing semicolons.
"""
return _trailing_semicolon(_whitespace(_comments(css))).strip()
def minify_html(html: str,
_minify=partial(
re.compile(r'(.*?)(<pre\b.*?</pre\b\s*>)|(.*)', re.IGNORECASE | re.DOTALL).sub,
lambda m, _norm_space=partial(re.compile(r'\s\s+').sub, '\n'): (
_norm_space(m.group(1) or '') +
(m.group(2) or '') +
_norm_space(m.group(3) or '')))):
"""
Minify HTML by replacing all consecutive whitespace with a single space
(or newline) character, except inside `<pre>` tags.
"""
return _minify(html)
def glimpse(text: str, max_length=153, *, paragraph=True,
_split_paragraph=partial(re.compile(r'\s*\n\s*\n\s*').split, maxsplit=1),
_trim_last_word=partial(re.compile(r'\S+$').sub, ''),
_remove_titles=partial(re.compile(r'^(#+|-{4,}|={4,})', re.MULTILINE).sub, ' ')):
"""
Returns a short excerpt (e.g. first paragraph) of text.
If `paragraph` is True, the first paragraph will be returned,
but never longer than `max_length` characters.
"""
text = text.lstrip()
if paragraph:
text, *rest = _split_paragraph(text)
if rest:
text = text.rstrip('.')
text += ''
text = _remove_titles(text).strip()
if len(text) > max_length:
text = _trim_last_word(text[:max_length - 2])
if not text.endswith('.') or not paragraph:
text = text.rstrip('. ') + ''
return text
_md = markdown.Markdown(
output_format='html5',
extensions=[
"markdown.extensions.abbr",
"markdown.extensions.attr_list",
"markdown.extensions.def_list",
"markdown.extensions.fenced_code",
"markdown.extensions.footnotes",
"markdown.extensions.tables",
"markdown.extensions.admonition",
"markdown.extensions.smarty",
"markdown.extensions.toc",
],
extension_configs={
"markdown.extensions.smarty": dict(
smart_dashes=True,
smart_ellipses=True,
smart_quotes=False,
smart_angled_quotes=False,
),
},
)
class _ToMarkdown:
"""
This class serves as a namespace for methods converting common
documentation formats into markdown our Python-Markdown with
addons can ingest.
If debugging regexs (I can't imagine why that would be necessary
they are all perfect!) an insta-preview tool such as RegEx101.com
will come in handy.
"""
@staticmethod
def _deflist(name, type, desc,
# Wraps any identifiers and string literals in parameter type spec
# in backticks while skipping common "stopwords" such as 'or', 'of',
# 'optional' ... See §4 Parameters:
# https://numpydoc.readthedocs.io/en/latest/format.html#sections
_type_parts=partial(
re.compile(r'[\w.\'"]+').sub,
lambda m: ('{}' if m.group(0) in ('of', 'or', 'default', 'optional') else
'`{}`').format(m.group(0)))):
"""
Returns `name`, `type`, and `desc` formatted as a
Python-Markdown definition list entry. See also:
https://python-markdown.github.io/extensions/definition_lists/
"""
type = _type_parts(type or '')
desc = desc or '&nbsp;'
assert _ToMarkdown._is_indented_4_spaces(desc)
assert name or type
ret = ""
if name:
ret += '**`{}`**'.format(name)
if type:
ret += ' :&ensp;{}'.format(type) if ret else type
ret += '\n: {}\n\n'.format(desc)
return ret
@staticmethod
def _numpy_params(match,
_name_parts=partial(re.compile(', ').sub, '`**, **`')):
""" Converts NumpyDoc parameter (etc.) sections into Markdown. """
name, type, desc = match.group("name", "type", "desc")
type = type or match.groupdict().get('just_type', None)
desc = desc.strip()
name = name and _name_parts(name)
return _ToMarkdown._deflist(name, type, desc)
@staticmethod
def _numpy_seealso(match):
"""
Converts NumpyDoc "See Also" section either into referenced code,
optionally within a definition list.
"""
spec_with_desc, simple_list = match.groups()
if spec_with_desc:
return '\n\n'.join('`{}`\n: {}'.format(*map(str.strip, line.split(':', 1)))
for line in filter(None, spec_with_desc.split('\n')))
return ', '.join('`{}`'.format(i) for i in simple_list.split(', '))
@staticmethod
def _numpy_sections(match):
"""
Convert sections with parameter, return, and see also lists to Markdown
lists.
"""
section, body = match.groups()
if section.title() == 'See Also':
body = re.sub(r'^((?:\n?[\w.]* ?: .*)+)|(.*\w.*)',
_ToMarkdown._numpy_seealso, body)
elif section.title() in ('Returns', 'Yields', 'Raises', 'Warns'):
body = re.sub(r'^(?:(?P<name>\*{0,2}\w+(?:, \*{0,2}\w+)*)'
r'(?: ?: (?P<type>.*))|'
r'(?P<just_type>\w[^\n`*]*))(?<!\.)$'
r'(?P<desc>(?:\n(?: {4}.*|$))*)',
_ToMarkdown._numpy_params, body, flags=re.MULTILINE)
else:
body = re.sub(r'^(?P<name>\*{0,2}\w+(?:, \*{0,2}\w+)*)'
r'(?: ?: (?P<type>.*))?(?<!\.)$'
r'(?P<desc>(?:\n(?: {4}.*|$))*)',
_ToMarkdown._numpy_params, body, flags=re.MULTILINE)
return section + '\n-----\n' + body
@staticmethod
def numpy(text):
"""
Convert `text` in numpydoc docstring format to Markdown
to be further converted later.
"""
return re.sub(r'^(\w[\w ]+)\n-{3,}\n'
r'((?:(?!.+\n-+).*$\n?)*)',
_ToMarkdown._numpy_sections, text, flags=re.MULTILINE)
@staticmethod
def _is_indented_4_spaces(txt, _3_spaces_or_less=re.compile(r'\n\s{0,3}\S').search):
return '\n' not in txt or not _3_spaces_or_less(txt)
@staticmethod
def _fix_indent(name, type, desc):
"""Maybe fix indent from 2 to 4 spaces."""
if not _ToMarkdown._is_indented_4_spaces(desc):
desc = desc.replace('\n', '\n ')
return name, type, desc
@staticmethod
def indent(indent, text, *, clean_first=False):
if clean_first:
text = inspect.cleandoc(text)
return re.sub(r'\n', '\n' + indent, indent + text.rstrip())
@staticmethod
def google(text,
_googledoc_sections=partial(
re.compile(r'^([A-Z]\w+):$\n((?:\n?(?: {2,}.*|$))+)', re.MULTILINE).sub,
lambda m, _params=partial(
re.compile(r'^([\w*]+)(?: \(([\w.,=\[\] ]+)\))?: '
r'((?:.*)(?:\n(?: {2,}.*|$))*)', re.MULTILINE).sub,
lambda m: _ToMarkdown._deflist(*_ToMarkdown._fix_indent(*m.groups()))): (
m.group() if not m.group(2) else '\n{}\n-----\n{}'.format(
m.group(1), _params(inspect.cleandoc('\n' + m.group(2))))))):
"""
Convert `text` in Google-style docstring format to Markdown
to be further converted later.
"""
return _googledoc_sections(text)
@staticmethod
def _admonition(match, module=None, limit_types=None):
indent, type, value, text = match.groups()
if limit_types and type not in limit_types:
return match.group(0)
if type == 'include' and module:
try:
return _ToMarkdown._include_file(indent, value,
_ToMarkdown._directive_opts(text), module)
except Exception as e:
raise RuntimeError('`.. include:: {}` error in module {!r}: {}'
.format(value, module.name, e))
if type in ('image', 'figure'):
return '{}![{}]({})\n'.format(
indent, text.translate(str.maketrans({'\n': ' ',
'[': '\\[',
']': '\\]'})).strip(), value)
if type == 'math':
return _ToMarkdown.indent(indent,
'\\[ ' + text.strip() + ' \\]',
clean_first=True)
if type == 'versionchanged':
title = 'Changed in version:&ensp;' + value
elif type == 'versionadded':
title = 'Added in version:&ensp;' + value
elif type == 'deprecated' and value:
title = 'Deprecated since version:&ensp;' + value
elif type == 'admonition':
title = value
elif type.lower() == 'todo':
title = 'TODO'
text = value + ' ' + text
else:
title = type.capitalize()
if value:
title += ':&ensp;' + value
text = _ToMarkdown.indent(indent + ' ', text, clean_first=True)
return '{}!!! {} "{}"\n{}\n'.format(indent, type, title, text)
@staticmethod
def admonitions(text, module, limit_types=None):
"""
Process reStructuredText's block directives such as
`.. warning::`, `.. deprecated::`, `.. versionadded::`, etc.
and turn them into Python-M>arkdown admonitions.
`limit_types` is optionally a set of directives to limit processing to.
See: https://python-markdown.github.io/extensions/admonition/
"""
substitute = partial(re.compile(r'^(?P<indent> *)\.\. ?(\w+)::(?: *(.*))?'
r'((?:\n(?:(?P=indent) +.*| *$))*)', re.MULTILINE).sub,
partial(_ToMarkdown._admonition, module=module,
limit_types=limit_types))
# Apply twice for nested (e.g. image inside warning)
return substitute(substitute(text))
@staticmethod
def _include_file(indent: str, path: str, options: dict, module: pdoc.Module) -> str:
start_line = int(options.get('start-line', 0))
end_line = int(options.get('end-line', 0)) or None
start_after = options.get('start-after')
end_before = options.get('end-before')
with open(os.path.join(os.path.dirname(module.obj.__file__), path),
encoding='utf-8') as f:
text = ''.join(list(f)[start_line:end_line])
if start_after:
text = text[text.index(start_after) + len(start_after):]
if end_before:
text = text[:text.index(end_before)]
return _ToMarkdown.indent(indent, text)
@staticmethod
def _directive_opts(text: str) -> dict:
return dict(re.findall(r'^ *:([^:]+): *(.*)', text, re.MULTILINE))
@staticmethod
def doctests(text,
_indent_doctests=partial(
re.compile(r'(?:^(?P<fence>```|~~~).*\n)?'
r'(?:^>>>.*'
r'(?:\n(?:(?:>>>|\.\.\.).*))*'
r'(?:\n.*)?\n\n?)+'
r'(?P=fence)?', re.MULTILINE).sub,
lambda m: (m.group(0) if m.group('fence') else
('\n ' + '\n '.join(m.group(0).split('\n')) + '\n\n')))):
"""
Indent non-fenced (`~~~`) top-level (0-indented)
doctest blocks so they render as code.
"""
if not text.endswith('\n'): # Needed for the r'(?:\n.*)?\n\n?)+' line (GH-72)
text += '\n'
return _indent_doctests(text)
@staticmethod
def raw_urls(text):
"""Wrap URLs in Python-Markdown-compatible <angle brackets>."""
return re.sub(r'(?<![<"\'])(\s*)((?:http|ftp)s?://[^>)\s]+)(\s*)', r'\1<\2>\3', text)
class _MathPattern(InlineProcessor):
NAME = 'pdoc-math'
PATTERN = r'(?<!\S|\\)(?:\\\((.+?)\\\)|\\\[(.+?)\\\]|\$\$(.+?)\$\$|\$(\S.*?)\$)'
PRIORITY = 181 # Larger than that of 'escape' pattern
def handleMatch(self, m, data):
for value, is_block in zip(m.groups(), (False, True, True, False)):
if value:
break
# script = etree.Element('script', type='math/tex' + ('; mode=display' if is_block else ''))
# preview = etree.Element('span', {'class': 'MathJax_Preview'})
# preview.text = script.text = AtomicString(value)
# wrapper = etree.Element('span')
## wrapper.extend([preview, script])
# p = subprocess.Popen('/home/jan/projects/meanas/node_modules/.bin/katex',
# bufsize=4096,
# stdin=subprocess.PIPE,
# stdout=subprocess.PIPE)
# stdout, stderr = p.communicate(value.encode())
# if stderr:
# print(stderr.decode(), '\ninput:', value)
# wrapper.text = stdout.decode())
wrapper = etree.Element('eq')
wrapper.text = AtomicString(value)
return wrapper, m.start(0), m.end(0)
def to_html(text: str, docformat: str = 'numpy,google', *,
module: pdoc.Module = None, link: Callable[..., str] = None,
latex_math: bool = False):
"""
Returns HTML of `text` interpreted as `docformat`.
By default, Numpydoc and Google-style docstrings are assumed,
as well as pure Markdown.
`module` should be the documented module (so the references can be
resolved) and `link` is the hyperlinking function like the one in the
example template.
"""
# Optionally register our math syntax processor
if not latex_math and _MathPattern.NAME in _md.inlinePatterns:
_md.inlinePatterns.deregister(_MathPattern.NAME)
elif latex_math and _MathPattern.NAME not in _md.inlinePatterns:
_md.inlinePatterns.register(_MathPattern(_MathPattern.PATTERN),
_MathPattern.NAME,
_MathPattern.PRIORITY)
md = to_markdown(text, docformat=docformat, module=module, link=link)
return _md.reset().convert(md)
def to_markdown(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'(?<![\[\\])`(?!])(?:[^`]|(?<=\\)`)+`').sub):
"""
Returns `text`, assumed to be a docstring in `docformat`, converted to markdown.
`module` should be the documented module (so the references can be
resolved) and `link` is the hyperlinking function like the one in the
example template.
"""
assert all(i in (None, '', 'numpy', 'google') for i in docformat.split(',')), docformat
text = _ToMarkdown.admonitions(text, module)
text = _ToMarkdown.raw_urls(text)
if 'google' in docformat:
text = _ToMarkdown.google(text)
# If doing both, do numpy after google, otherwise google-style's
# headings are incorrectly interpreted as numpy params
if 'numpy' in docformat:
text = _ToMarkdown.numpy(text)
text = _ToMarkdown.doctests(text)
if module and link:
text = _code_refs(partial(_linkify, link=link, module=module, fmt='`{}`'), text)
return text
class ReferenceWarning(UserWarning):
"""
This warning is raised in `to_html` when a object reference in markdown
doesn't match any documented objects.
Look for this warning to catch typos / references to obsolete symbols.
"""
def _linkify(match: Match, link: Callable[..., str], module: pdoc.Module,
_is_pyident=re.compile(r'^[a-zA-Z_]\w*(\.\w+)+$').match, **kwargs):
matched = match.group(0)
refname = matched.strip('`')
dobj = module.find_ident(refname)
if isinstance(dobj, pdoc.External):
if not _is_pyident(refname):
return matched
# If refname in documentation has a typo or is obsolete, warn.
# XXX: Assume at least the first part of refname, i.e. the package, is correct.
module_part = module.find_ident(refname.split('.')[0])
if not isinstance(module_part, pdoc.External):
warn('Code reference `{}` in module "{}" does not match any '
'documented object.'.format(refname, module.refname),
ReferenceWarning, stacklevel=3)
return link(dobj, **kwargs)
def extract_toc(text: str):
"""
Returns HTML Table of Contents containing markdown titles in `text`.
"""
toc, _ = _md.reset().convert('[TOC]\n\n@CUT@\n\n' + text).split('@CUT@', 1)
if toc.endswith('<p>'): # CUT was put into its own paragraph
toc = toc[:-3].rstrip()
return toc
def format_git_link(template: str, dobj: pdoc.Doc):
"""
Interpolate `template` as a formatted string literal using values extracted
from `dobj` and the working environment.
"""
if not template:
return None
try:
if 'commit' in _str_template_fields(template):
commit = _git_head_commit()
abs_path = inspect.getfile(inspect.unwrap(dobj.obj))
path = _project_relative_path(abs_path)
lines, start_line = inspect.getsourcelines(dobj.obj)
end_line = start_line + len(lines) - 1
url = template.format(**locals())
return url
except Exception:
warn('format_git_link for {} failed:\n{}'.format(dobj.obj, traceback.format_exc()))
return None
@lru_cache()
def _git_head_commit():
"""
If the working directory is part of a git repository, return the
head git commit hash. Otherwise, raise a CalledProcessError.
"""
process_args = ['git', 'rev-parse', 'HEAD']
try:
commit = subprocess.check_output(process_args, universal_newlines=True).strip()
return commit
except OSError as error:
warn("git executable not found on system:\n{}".format(error))
except subprocess.CalledProcessError as error:
warn(
"Ensure pdoc is run within a git repository.\n"
"`{}` failed with output:\n{}"
.format(' '.join(process_args), error.output)
)
return None
@lru_cache()
def _git_project_root():
"""
Return the path to project root directory or None if indeterminate.
"""
path = None
for cmd in (['git', 'rev-parse', '--show-superproject-working-tree'],
['git', 'rev-parse', '--show-toplevel']):
try:
path = subprocess.check_output(cmd, universal_newlines=True).rstrip('\r\n')
if path:
break
except (subprocess.CalledProcessError, OSError):
pass
return path
@lru_cache()
def _project_relative_path(absolute_path):
"""
Convert an absolute path of a python source file to a project-relative path.
Assumes the project's path is either the current working directory or
Python library installation.
"""
from distutils.sysconfig import get_python_lib
for prefix_path in (_git_project_root() or os.getcwd(),
get_python_lib()):
common_path = os.path.commonpath([prefix_path, absolute_path])
if common_path == prefix_path:
# absolute_path is a descendant of prefix_path
return os.path.relpath(absolute_path, prefix_path)
raise RuntimeError(
"absolute path {!r} is not a descendant of the current working directory "
"or of the system's python library."
.format(absolute_path)
)
@lru_cache()
def _str_template_fields(template):
"""
Return a list of `str.format` field names in a template string.
"""
from string import Formatter
return [
field_name
for _, field_name, _, _ in Formatter().parse(template)
if field_name is not None
]

View file

@ -1,185 +0,0 @@
<%!
import re
import pdoc
from pdoc.html_helpers import to_markdown, format_git_link
def link(d, fmt='{}'):
name = fmt.format(d.qualname + ('()' if isinstance(d, pdoc.Function) else ''))
if isinstance(d, pdoc.External):
return name
return '[{}](#{})'.format(name, d.refname)
def _to_md(text, module):
text = to_markdown(text, module=module, link=link)
# Setext H2 headings to atx H2 headings
text = re.sub(r'\n(.+)\n-{3,}\n', r'\n## \1\n\n', text)
# Convert admonitions into simpler paragraphs, dedent contents
text = re.sub(r'^(?P<indent>( *))!!! \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>
<%def name="funcdef(f)">
<%
returns = show_type_annotations and f.return_annotation() or ''
if returns:
returns = ' -> ' + returns
%>
> `${f.funcdef()} ${f.name}(${', '.join(f.params(annotate=show_type_annotations))})${returns}`
</%def>
<%def name="classdef(c)">
> `class ${c.name}(${', '.join(c.params(annotate=show_type_annotations))})`
</%def>
<%def name="show_source(d)">
% if (show_source_code or git_link_template) and d.source and d.obj is not getattr(d.inherits, 'obj', None):
<% git_link = format_git_link(git_link_template, d) %>
[[view code]](${git_link})
%endif
</%def>
---
description: |
API documentation for modules: ${', '.join(m.name for m in modules)}.
lang: en
classoption: oneside
geometry: margin=1in
papersize: a4
linkcolor: blue
links-as-notes: true
...
% 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, ('Namespace' if module.is_namespace else '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)}
${show_source(v)}
${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)}
${show_source(f)}
${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)}
${show_source(cls)}
${classdef(cls)}
${cls.docstring | to_md, subh}
<%
class_vars = cls.class_variables(show_inherited_members, sort=sort_identifiers)
static_methods = cls.functions(show_inherited_members, sort=sort_identifiers)
inst_vars = cls.instance_variables(show_inherited_members, sort=sort_identifiers)
methods = cls.methods(show_inherited_members, sort=sort_identifiers)
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__} (<https://pdoc3.github.io>).

View file

@ -1,381 +0,0 @@
.flex {
display: flex !important;
}
body {
line-height: 1.5em;
background: black;
color: #DDD;
max-width: 140ch;
}
#content {
padding: 20px;
}
#sidebar {
padding: 30px;
overflow: hidden;
}
.http-server-breadcrumbs {
font-size: 130%;
margin: 0 0 15px 0;
}
#footer {
font-size: .75em;
padding: 5px 30px;
border-top: 1px solid #ddd;
text-align: right;
}
#footer p {
margin: 0 0 0 1em;
display: inline-block;
}
#footer p:last-child {
margin-right: 30px;
}
h1, h2, h3, h4, h5 {
font-weight: 300;
}
h1 {
font-size: 2.5em;
line-height: 1.1em;
border-top: 20px white;
}
h2 {
font-size: 1.75em;
margin: 1em 0 .50em 0;
}
h3 {
font-size: 1.4em;
margin: 25px 0 10px 0;
}
h4 {
margin: 0;
font-size: 105%;
}
a {
color: #999;
text-decoration: none;
transition: color .3s ease-in-out;
}
a:hover {
color: #18d;
}
.title code {
font-weight: bold;
}
h2[id^="header-"] {
margin-top: 2em;
}
.ident {
color: #7ff;
}
pre code {
background: transparent;
font-size: .8em;
line-height: 1.4em;
}
code {
background: #0d0d0e;
padding: 1px 4px;
overflow-wrap: break-word;
}
h1 code { background: transparent }
pre {
background: #111;
border: 0;
border-top: 1px solid #ccc;
border-bottom: 1px solid #ccc;
margin: 1em 0;
padding: 1ex;
}
#http-server-module-list {
display: flex;
flex-flow: column;
}
#http-server-module-list div {
display: flex;
}
#http-server-module-list dt {
min-width: 10%;
}
#http-server-module-list p {
margin-top: 0;
}
.toc ul,
#index {
list-style-type: none;
margin: 0;
padding: 0;
}
#index code {
background: transparent;
}
#index h3 {
border-bottom: 1px solid #ddd;
}
#index ul {
padding: 0;
}
#index h4 {
font-weight: bold;
}
#index h4 + ul {
margin-bottom:.6em;
}
/* Make TOC lists have 2+ columns when viewport is wide enough.
Assuming ~20-character identifiers and ~30% wide sidebar. */
@media (min-width: 200ex) { #index .two-column { column-count: 2 } }
@media (min-width: 300ex) { #index .two-column { column-count: 3 } }
dl {
margin-bottom: 2em;
}
dl dl:last-child {
margin-bottom: 4em;
}
dd {
margin: 0 0 1em 3em;
}
#header-classes + dl > dd {
margin-bottom: 3em;
}
dd dd {
margin-left: 2em;
}
dd p {
margin: 10px 0;
}
blockquote code {
background: #111;
font-weight: bold;
font-size: .85em;
padding: 5px 10px;
display: inline-block;
min-width: 40%;
}
blockquote code:hover {
background: #101010;
}
.name > span:first-child {
white-space: nowrap;
}
.name.class > span:nth-child(2) {
margin-left: .4em;
}
.inherited {
color: #777;
border-left: 5px solid #eee;
padding-left: 1em;
}
.inheritance em {
font-style: normal;
font-weight: bold;
}
/* Docstrings titles, e.g. in numpydoc format */
.desc h2 {
font-weight: 400;
font-size: 1.25em;
}
.desc h3 {
font-size: 1em;
}
.desc dt code {
background: inherit; /* Don't grey-back parameters */
}
.source summary,
.git-link-div {
color: #aaa;
text-align: right;
font-weight: 400;
font-size: .8em;
text-transform: uppercase;
}
.source summary > * {
white-space: nowrap;
cursor: pointer;
}
.git-link {
color: inherit;
margin-left: 1em;
}
.source pre {
max-height: 500px;
overflow: auto;
margin: 0;
}
.source pre code {
font-size: 12px;
overflow: visible;
}
.hlist {
list-style: none;
}
.hlist li {
display: inline;
}
.hlist li:after {
content: ',\2002';
}
.hlist li:last-child:after {
content: none;
}
.hlist .hlist {
display: inline;
padding-left: 1em;
}
img {
max-width: 100%;
}
.admonition {
padding: .1em .5em;
margin-bottom: 1em;
}
.admonition-title {
font-weight: bold;
}
.admonition.note,
.admonition.info,
.admonition.important {
background: #610;
}
.admonition.todo,
.admonition.versionadded,
.admonition.tip,
.admonition.hint {
background: #202;
}
.admonition.warning,
.admonition.versionchanged,
.admonition.deprecated {
background: #02b;
}
.admonition.error,
.admonition.danger,
.admonition.caution {
background: darkpink;
}
@media screen and (min-width: 700px) {
#sidebar {
width: 30%;
}
#content {
width: 70%;
max-width: 100ch;
padding: 3em 4em;
border-left: 1px solid #ddd;
}
pre code {
font-size: 1em;
}
.item .name {
font-size: 1em;
}
main {
display: flex;
flex-direction: row-reverse;
justify-content: flex-end;
}
.toc ul ul,
#index ul {
padding-left: 1.5em;
}
.toc > ul > li {
margin-top: .5em;
}
}
@media print {
#sidebar h1 {
page-break-before: always;
}
.source {
display: none;
}
}
@media print {
* {
background: transparent !important;
color: #000 !important; /* Black prints faster: h5bp.com/s */
box-shadow: none !important;
text-shadow: none !important;
}
a[href]:after {
content: " (" attr(href) ")";
font-size: 90%;
}
/* Internal, documentation links, recognized by having a title,
don't need the URL explicity stated. */
a[href][title]:after {
content: none;
}
abbr[title]:after {
content: " (" attr(title) ")";
}
/*
* Don't show links for images, or javascript/internal links
*/
.ir a:after,
a[href^="javascript:"]:after,
a[href^="#"]:after {
content: "";
}
pre,
blockquote {
border: 1px solid #999;
page-break-inside: avoid;
}
thead {
display: table-header-group; /* h5bp.com/t */
}
tr,
img {
page-break-inside: avoid;
}
img {
max-width: 100% !important;
}
@page {
margin: 0.5cm;
}
p,
h2,
h3 {
orphans: 3;
widows: 3;
}
h1,
h2,
h3,
h4,
h5,
h6 {
page-break-after: avoid;
}
}

View file

@ -49,7 +49,27 @@ dependencies = [
path = "meanas/__init__.py"
[project.optional-dependencies]
dev = ["pytest", "coverage", "pdoc", "gridlock"]
dev = [
"pytest",
"coverage",
"gridlock",
"mkdocs>=1.6",
"mkdocs-material>=9.5",
"mkdocstrings[python]>=0.25",
"mkdocs-print-site-plugin>=2.3",
"pymdown-extensions>=10.7",
"htmlark>=1.0",
"ruff>=0.6",
]
docs = [
"mkdocs>=1.6",
"mkdocs-material>=9.5",
"mkdocstrings[python]>=0.25",
"mkdocs-print-site-plugin>=2.3",
"pymdown-extensions>=10.7",
"htmlark>=1.0",
"ruff>=0.6",
]
examples = [
"gridlock>=2.1",
"matplotlib>=3.10.8",