Skip to content

isqx.mkdocs

plugin ¤

A MkDocs plugin bundling a default KaTeX configuration necessary to render mathematical expressions in the documentation.

isqx.mkdocs.extension.IsqxExtension is responsible for injecting cross-referenced details into the docstrings of attributes and functions. This module also comes with an optional client-side JS script detail-highlight.js that searches for matching symbols within isqx.details.Detail blocks and highlights them.

Note

You must install isqx with the docs extra optional dependencies to use this module.

logger ¤

logger = getLogger(__name__)

IsqxPluginConfig ¤

Bases: Config

definitions ¤

definitions = Type(list, default=[])

Modules that contain the definitions, e.g. isqx.iso80000, isqx.aerospace.

This is used to provide cross-references within the isqx.details.Detail blocks.

Note that griffe will dynamically import these modules. To reduce build time, avoid expensive imports like torch.

details ¤

details = Type(list, default=[])

Paths to the details mapping, e.g. isqx.details.iso80000.TIME_AND_SPACE.

details_highlight ¤

details_highlight = Type(bool, default=True)

Whether to highlight symbols in the details.

load_katex ¤

load_katex = Type(bool, default=True)

Whether to load KaTeX javascript and CSS assets.

PLUGIN_ASSETS_JS ¤

PLUGIN_ASSETS_JS = (
    PATH_PLUGIN_ASSETS / "js" / "detail-highlight.js",
)

KATEX_VERSION ¤

KATEX_VERSION = '0.16.22'

IsqxPlugin ¤

Bases: BasePlugin[IsqxPluginConfig]

on_config ¤

on_config(config: MkDocsConfig) -> MkDocsConfig
Source code in src/isqx/mkdocs/plugin.py
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
def on_config(self, config: MkDocsConfig) -> MkDocsConfig:
    plugin = config.plugins.get("mkdocstrings")
    assert isinstance(plugin, MkdocstringsPlugin)
    options = (
        plugin.config.setdefault("handlers", {})
        .setdefault("python", {})
        .setdefault("options", [])
    )
    extensions = options.setdefault("extensions", [])
    extensions.append(
        {
            "isqx.mkdocs.extension:IsqxExtension": {
                "config": self.config,
                "objects_out_path": f"{config.site_dir}/assets",
            }
        }
    )

    self.extra_css: list[str] = []
    self.extra_scripts: list[ExtraScriptValue | str] = []
    if self.config.details_highlight:
        self.extra_scripts.append(_mjs("js/cmap.mjs"))
        self.extra_scripts.append(_mjs("js/detail-highlight.mjs"))
        self.extra_css.append("css/detail-highlight.css")
    if self.config.load_katex:
        self.extra_scripts.append(_mjs("js/katex.mjs"))
        self.extra_scripts.extend(katex_js(KATEX_VERSION))
        self.extra_css.extend(katex_css(KATEX_VERSION))
    if self.extra_scripts:
        config.extra_javascript[:0] = self.extra_scripts
    if self.extra_css:
        config.extra_css[:0] = self.extra_css
    return config

on_post_build ¤

on_post_build(*, config: MkDocsConfig) -> None
Source code in src/isqx/mkdocs/plugin.py
93
94
95
96
97
98
def on_post_build(self, *, config: MkDocsConfig) -> None:
    output_path = Path(config.site_dir)
    for script in self.extra_scripts:
        maybe_copy_asset(script, str(output_path))
    for css in self.extra_css:
        maybe_copy_asset(css, str(output_path))

katex_js ¤

katex_js(
    version: str,
) -> Generator[ExtraScriptValue, None, None]
Source code in src/isqx/mkdocs/plugin.py
101
102
103
104
105
106
107
108
109
110
def katex_js(version: str) -> Generator[ExtraScriptValue, None, None]:
    yield _script(
        f"https://cdn.jsdelivr.net/npm/katex@{version}/dist/katex.min.js"
    )
    yield _script(
        f"https://cdn.jsdelivr.net/npm/katex@{version}/dist/contrib/auto-render.min.js"
    )
    yield _script(
        f"https://cdn.jsdelivr.net/npm/katex@{version}/dist/contrib/copy-tex.min.js"
    )

katex_css ¤

katex_css(version: str) -> Generator[str, None, None]
Source code in src/isqx/mkdocs/plugin.py
126
127
def katex_css(version: str) -> Generator[str, None, None]:
    yield f"https://cdn.jsdelivr.net/npm/katex@{version}/dist/katex.min.css"

maybe_copy_asset ¤

maybe_copy_asset(
    asset: ExtraScriptValue | str, output_path: str
) -> None
Source code in src/isqx/mkdocs/plugin.py
130
131
132
133
134
135
136
137
def maybe_copy_asset(asset: ExtraScriptValue | str, output_path: str) -> None:
    path = asset.path if isinstance(asset, ExtraScriptValue) else asset
    if path.startswith("js/") or path.startswith("css/"):
        a = PATH_PLUGIN_ASSETS / path
        copy_file(
            str(a),
            str(Path(output_path) / a.relative_to(PATH_PLUGIN_ASSETS)),
        )

extension ¤

A griffe extension to dynamically collect isqx definitions/details.

Details are injected into the docstrings of attributes and functions in the documentation generated by mkdocstrings-python.

Note

You must install isqx with the docs extra optional dependencies to use this module.

logger ¤

logger = get_logger(__name__)

get_templates_path ¤

get_templates_path() -> Path

See: https://mkdocstrings.github.io/usage/handlers/#handler-extensions

Source code in src/isqx/mkdocs/extension.py
85
86
87
def get_templates_path() -> Path:
    """See: https://mkdocstrings.github.io/usage/handlers/#handler-extensions"""
    return PATH_PLUGIN / "templates"

Unit ¤

Unit: TypeAlias = Union[tuple[StrFragment, ...], None]

The unit of measurement rendered as string fragments.

Where ¤

Where(
    symbol: str,
    description: StrFragment | tuple[StrFragment, ...],
    unit: Unit,
)

symbol ¤

symbol: str

description ¤

description: StrFragment | tuple[StrFragment, ...]

unit ¤

unit: Unit

KaTeXWhere ¤

KaTeXWhere(
    katex: str, where: tuple[Where, ...] | None = None
)

katex ¤

katex: str

where ¤

where: tuple[Where, ...] | None = None

SymbolDetail ¤

SymbolDetail(
    katex: str,
    where: tuple[Where, ...] | None = None,
    remarks: str | None = None,
)

Bases: KaTeXWhere

remarks ¤

remarks: str | None = None

katex ¤

katex: str

where ¤

where: tuple[Where, ...] | None = None

EquationDetail ¤

EquationDetail(
    katex: str,
    where: tuple[Where, ...] | None = None,
    assumptions: tuple[
        StrFragment | tuple[StrFragment, ...], ...
    ]
    | None = None,
)

Bases: KaTeXWhere

assumptions ¤

assumptions: (
    tuple[StrFragment | tuple[StrFragment, ...], ...] | None
) = None

katex ¤

katex: str

where ¤

where: tuple[Where, ...] | None = None

WikidataDetail ¤

WikidataDetail(qcode: str)

qcode ¤

qcode: str

Quantity ¤

Quantity(value: str, unit: Unit)

value ¤

value: str

unit ¤

unit: Unit

CanonicalPath ¤

CanonicalPath: TypeAlias = str

QtyKindDetail ¤

QtyKindDetail(
    parent: str | None = None,
    unit_si_coherent: Unit = None,
    tags: tuple[str, ...] = tuple(),
    wikidata: list[WikidataDetail] = list(),
    symbols: list[SymbolDetail] = list(),
    equations: list[EquationDetail] = list(),
)

Several quantities can share the same unit. Two cases:

  • Explicit inheritance: when a quantity is defined by subscripting another, like POTENTIAL_ENERGY = ENERGY["potential"]: this is a strong signal of a parent-child relationship. we can detect this during the griffe AST walking
  • Implicit grouping: some quantities are related by their physical dimension but not defined via subscripting (e.g., SPEED and VELOCITY). we can group them by their underlying SI dimension. the hierarchy can be inferred from the tags.

parent ¤

parent: str | None = None

unit_si_coherent ¤

unit_si_coherent: Unit = None

tags ¤

tags: tuple[str, ...] = field(default_factory=tuple)

wikidata ¤

wikidata: list[WikidataDetail] = field(default_factory=list)

symbols ¤

symbols: list[SymbolDetail] = field(default_factory=list)

equations ¤

equations: list[EquationDetail] = field(
    default_factory=list
)

IsqxExtension ¤

IsqxExtension(
    config: IsqxPluginConfig,
    objects_out_path: str | None = None,
)

Bases: Extension

Source code in src/isqx/mkdocs/extension.py
163
164
165
166
167
168
169
170
171
172
def __init__(
    self, config: IsqxPluginConfig, objects_out_path: str | None = None
):
    self.config = config
    self.definitions: Definitions = {}
    self.objects: dict[str, QtyKindDetail] = {}  # by path
    self.objects_out_path = objects_out_path
    self.possible_parent_maps: dict[str, str] = {}  # child -> parent
    """Note that items in this map are not guaranteed to be isqx objects
    because griffe works with static analysis."""

config ¤

config = config

definitions ¤

definitions: Definitions = {}

objects ¤

objects: dict[str, QtyKindDetail] = {}

objects_out_path ¤

objects_out_path = objects_out_path

possible_parent_maps ¤

possible_parent_maps: dict[str, str] = {}

Note that items in this map are not guaranteed to be isqx objects because griffe works with static analysis.

on_instance ¤

on_instance(
    *,
    node: ast.AST | ObjectNode,
    obj: Object,
    agent: Visitor | Inspector,
    **kwargs: Any,
) -> None
Source code in src/isqx/mkdocs/extension.py
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
def on_instance(
    self,
    *,
    node: ast.AST | ObjectNode,
    obj: Object,
    agent: Visitor | Inspector,
    **kwargs: Any,
) -> None:
    inject_citation_into_docstring(obj, agent)

    # collect strict parent-child relationships
    if (
        isinstance(obj, Attribute)
        and isinstance(obj.value, ExprSubscript)
        and isinstance(
            parent_expr := obj.value.left, (ExprName, ExprAttribute)
        )
    ):
        parent_path = parent_expr.canonical_path
        if parent_path.split(".", 1)[0] in ("typing", "typing_extensions"):
            # heuristic: we captured something invalid like Union[str, int]
            return
        self.possible_parent_maps[obj.canonical_path] = parent_path

on_module_instance ¤

on_module_instance(
    *,
    node: ast.AST | ObjectNode,
    mod: Module,
    agent: Visitor | Inspector,
    **kwargs: Any,
) -> None

Populate definitions from the current module being visited using runtime analysis, if it is matches the path under the configured definitions.

Note that if module a contains FOO: Annotated[float, isqx.M] = 1.0 and module b contains from a import FOO, then self.definitions will contain both (which points to the same object):

  • a.FOO: Definition(value=1.0, annotated_metadata=M)
  • b.FOO: Definition(value=1.0, annotated_metadata=None)

which is not ideal. When griffe visits b, on_alias will make sure the definition for b.FOO is removed.

Source code in src/isqx/mkdocs/extension.py
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
def on_module_instance(
    self,
    *,
    node: ast.AST | ObjectNode,
    mod: Module,
    agent: Visitor | Inspector,
    **kwargs: Any,
) -> None:
    """Populate definitions from the current module being visited using
    runtime analysis, if it is matches the path under the configured
    [definitions][isqx.mkdocs.plugin.IsqxPluginConfig.definitions].

    Note that if module `a` contains `FOO: Annotated[float, isqx.M] = 1.0`
    and module `b` contains `from a import FOO`, then `self.definitions`
    will contain both (which points to the same object):

    - `a.FOO`: Definition(value=1.0, annotated_metadata=M)
    - `b.FOO`: Definition(value=1.0, annotated_metadata=None)

    which is not ideal. When `griffe` visits `b`, `on_alias` will make sure
    the definition for `b.FOO` is removed.
    """
    if mod.path not in self.config.definitions:
        return
    module_rt = dynamic_import(mod.path)
    metadata = dict(module_attribute_metadata(module_rt))
    self.definitions |= {
        f"{mod.path}.{name}": Definition(obj, metadata.get(name))
        for name, obj in inspect.getmembers(module_rt)
        if isinstance(obj, _ARGS_DEFINITION)
    }

on_alias ¤

on_alias(
    *,
    node: ast.AST | ObjectNode,
    alias: Alias,
    agent: Visitor | Inspector,
    **kwargs: Any,
) -> None
Source code in src/isqx/mkdocs/extension.py
230
231
232
233
234
235
236
237
238
239
240
def on_alias(
    self,
    *,
    node: ast.AST | ObjectNode,
    alias: Alias,
    agent: Visitor | Inspector,
    **kwargs: Any,
) -> None:
    # todo: actually we probably shouldn't do this
    if alias.path in self.definitions:
        del self.definitions[alias.path]

on_package_loaded ¤

on_package_loaded(
    *, pkg: Module, loader: GriffeLoader, **kwargs: Any
) -> None
Source code in src/isqx/mkdocs/extension.py
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
def on_package_loaded(
    self, *, pkg: Module, loader: GriffeLoader, **kwargs: Any
) -> None:
    logger.info(f"loaded {len(self.definitions)} definitions")
    reverse_definitions = _build_reverse_definitions(self.definitions)
    formatter = MkdocsFormatter(reverse_definitions)

    for defs_path in self.config.details:
        for path, item in _process_details_module(
            defs_path, loader, formatter, reverse_definitions
        ):
            objects = self.objects.setdefault(path, QtyKindDetail())
            attr_target: Attribute = loader.modules_collection[path]
            isqx_extras = _get_or_init_isqx_extras(attr_target)

            if isinstance(item, WikidataDetail):
                objects.wikidata.append(item)
                isqx_extras["wikidata"].append(item)
            elif isinstance(item, SymbolDetail):
                objects.symbols.append(item)
                isqx_extras["symbols"].append(item)
            elif isinstance(item, EquationDetail):
                objects.equations.append(item)
                isqx_extras["details"].append(item)

    # set the parent (if found). would be nice for jinja to also show this
    # but we focus on the frontend vis for now.
    for child_path, parent_path in self.possible_parent_maps.items():
        if child_path in self.objects and parent_path in self.objects:
            child_object_detail = self.objects[child_path]
            child_object_detail.parent = parent_path

    for path, details in self.objects.items():
        if path not in self.definitions:
            continue
        definition = self.definitions[path]
        if not isinstance(v := definition.value, QtyKind):
            continue
        details.unit_si_coherent = tuple(
            formatter.fmt(v.unit_si_coherent)
        )  # str for now
        if v.tags is not None:
            details.tags = tuple(str(t) for t in v.tags)  # str for now

    if self.objects_out_path:
        constants = {}
        for path, definition in self.definitions.items():
            if not isinstance(
                definition.value, (LazyProduct, *_ARGS_NUMBER)
            ):
                continue
            value = definition.value
            value_str = str(
                value.to_approx()
                if isinstance(value, LazyProduct)
                else value
            )
            unit_expr: IsqxExpr | None = None
            if not (
                (meta := definition.annotated_metadata)
                and (unit_expr := meta.unit)
            ):
                logger.warning(
                    f"no unit found for {path} in annotated metadata"
                )
                continue  # dimensionless quantities should be annotated anyways
            constants[path] = Quantity(
                value=value_str,
                unit=(
                    tuple(formatter.fmt(unit_expr))  # str for now
                    if unit_expr is not None
                    else None
                ),
            )

        json_path = Path(self.objects_out_path) / "objects.json"
        # do not serialize members that have `None` or empty tuple values to
        # save space
        output_data = {
            "qtyKinds": {
                path: to_dict(details)
                for path, details in self.objects.items()
            },
            "constants": {
                path: to_dict(details)
                for path, details in constants.items()
            },
            "units": {},  # TODO
        }
        json_path.parent.mkdir(parents=True, exist_ok=True)
        with open(json_path, "w") as f:
            json.dump(output_data, f)
        logger.info(f"wrote objects to {json_path}")

to_dict ¤

to_dict(obj: Any) -> dict[str, Any]
Source code in src/isqx/mkdocs/extension.py
337
338
def to_dict(obj: Any) -> dict[str, Any]:
    return asdict(obj, dict_factory=lambda x: {k: v for (k, v) in x if v})

Definition ¤

Bases: NamedTuple

value ¤

annotated_metadata ¤

annotated_metadata: AnnotatedMetadata | None

Definitions ¤

Definitions: TypeAlias = dict[CanonicalPath, Definition]

MkdocsFormatter ¤

MkdocsFormatter(reverse_definitions: _ReverseDefinitions)

Bases: BasicFormatter

Source code in src/isqx/mkdocs/extension.py
362
363
364
365
366
367
def __init__(
    self,
    reverse_definitions: _ReverseDefinitions,
):
    self.reverse_definitions = reverse_definitions
    super().__init__(verbose=False)

reverse_definitions ¤

reverse_definitions = reverse_definitions

visit_named ¤

visit_named(
    expr: NamedExpr, state: _BasicFormatterState
) -> Generator[StrFragment, None, None]
Source code in src/isqx/mkdocs/extension.py
369
370
371
372
373
374
375
376
377
378
379
def visit_named(
    self, expr: NamedExpr, state: _BasicFormatterState
) -> Generator[StrFragment, None, None]:
    name_formatted: str = next(super().visit_named(expr, state))  # type: ignore
    if reverse_def := self.reverse_definitions.get(id(expr)):
        yield Anchor(
            text=name_formatted,
            path=reverse_def.path,
        )
    else:
        yield name_formatted

fmt ¤

fmt(expr: Expr) -> Generator[StrFragment, None, None]
Source code in src/isqx/_fmt.py
179
180
181
182
183
184
185
186
187
188
189
190
191
def fmt(self, expr: Expr) -> Generator[StrFragment, None, None]:
    state = _BasicFormatterState()
    yield from self.visit(expr, state)

    if self.verbose and state.definitions:
        seen_definitions: set[str] = set()
        for name, expr in state.definitions.items():
            yield from self._fmt_definition(
                name,
                expr,
                seen_definitions=seen_definitions,
                depth=0,
            )

visit_exp ¤

visit_exp(
    expr: Exp, state: _BasicFormatterState
) -> Generator[StrFragment, None, None]
Source code in src/isqx/_fmt.py
299
300
301
302
303
304
def visit_exp(
    self, expr: Exp, state: _BasicFormatterState
) -> Generator[StrFragment, None, None]:
    with state._set_parent_precedence(PRECEDENCE[Exp]):
        yield from self.visit(expr.base, state)
    yield str(expr.exponent).translate(_BASIC_EXPONENT_MAP)

visit_mul ¤

visit_mul(
    expr: Mul, state: _BasicFormatterState
) -> Generator[StrFragment, None, None]
Source code in src/isqx/_fmt.py
306
307
308
309
310
311
312
313
314
def visit_mul(
    self, expr: Mul, state: _BasicFormatterState
) -> Generator[StrFragment, None, None]:
    precedence_expr = PRECEDENCE[Mul]
    for i, term in enumerate(expr.terms):
        with state._set_parent_precedence(precedence_expr):
            yield from self.visit(term, state)
        if i < len(expr.terms) - 1:
            yield self.infix_mul

visit_scaled ¤

visit_scaled(
    expr: Scaled, state: _BasicFormatterState
) -> Generator[StrFragment, None, None]
Source code in src/isqx/_fmt.py
316
317
318
319
320
321
322
323
324
325
def visit_scaled(
    self, expr: Scaled, state: _BasicFormatterState
) -> Generator[StrFragment, None, None]:
    yield from _format_factor(
        expr.factor,
        infix_mul=self.infix_mul,
        format_product=self._fmt_product,
    )
    with state._set_parent_precedence(PRECEDENCE[Scaled]):
        yield from self.visit(expr.reference, state)

visit_tagged ¤

visit_tagged(
    expr: Tagged, state: _BasicFormatterState
) -> Generator[StrFragment, None, None]
Source code in src/isqx/_fmt.py
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
def visit_tagged(
    self, expr: Tagged, state: _BasicFormatterState
) -> Generator[StrFragment, None, None]:
    precedence_expr = PRECEDENCE[Tagged]
    with state._set_parent_precedence(precedence_expr):
        yield from self.visit(expr.reference, state)
    yield "["
    num_tags = len(expr.tags)
    for i, tag in enumerate(expr.tags):
        if isinstance(tag, _RatioBetween):
            yield "`"
            with state._set_parent_precedence(Precedence.NONE):
                yield from self.visit(tag.numerator, state)
            yield "` to `"
            if isinstance(q := tag.denominator, Quantity):
                yield from _format_factor(
                    q.value,
                    infix_mul=self.infix_mul,
                    format_product=self._fmt_product,
                )
                with state._set_parent_precedence(Precedence.NONE):
                    yield from self.visit(q.unit, state)
            else:
                with state._set_parent_precedence(Precedence.NONE):
                    yield from self.visit(q, state)
            yield "`"
        elif isinstance(tag, OriginAt):
            yield "relative to `"
            if isinstance(loc := tag.location, Quantity):
                yield from _format_factor(
                    loc.value,
                    infix_mul=self.infix_mul,
                    format_product=self._fmt_product,
                )
                with state._set_parent_precedence(precedence_expr):
                    yield from self.visit(loc.unit, state)
            else:  # hashable
                yield repr(tag.location)
            yield "`"
        elif tag is DELTA:
            yield "Δ"
        elif tag is DIFFERENTIAL:
            yield "differential"
        elif tag is INEXACT_DIFFERENTIAL:
            yield "inexact differential"
        else:
            yield repr(tag)
        if i < num_tags - 1:
            yield ", "
    yield "]"

visit_translated ¤

visit_translated(
    expr: Translated, state: _BasicFormatterState
) -> Generator[StrFragment, None, None]
Source code in src/isqx/_fmt.py
327
328
329
330
def visit_translated(
    self, expr: Translated, state: _BasicFormatterState
) -> Generator[StrFragment, None, None]:
    yield from self.visit_named(expr, state)

visit_log ¤

visit_log(
    expr: Log, state: _BasicFormatterState
) -> Generator[StrFragment, None, None]
Source code in src/isqx/_fmt.py
332
333
334
335
336
337
338
339
340
341
342
343
def visit_log(
    self, expr: Log, state: _BasicFormatterState
) -> Generator[StrFragment, None, None]:
    if expr.base is _E:
        yield "ln"
    else:
        yield "log"
        yield str(expr.base).translate(_BASIC_SUBSCRIPT_MAP)
    yield "("
    with state._set_parent_precedence(PRECEDENCE[Log]):
        yield from self.visit(expr.reference, state)
    yield ")"

overrides ¤

overrides: dict[Name, str] = field(default_factory=dict)

verbose ¤

verbose: bool = False

infix_mul ¤

infix_mul: str = ' · '

visit ¤

visit(
    expr: Expr, state: _BasicFormatterState
) -> Generator[StrFragment, None, None]
Source code in src/isqx/_fmt.py
223
224
225
226
227
228
229
230
231
232
def visit(
    self, expr: Expr, state: _BasicFormatterState
) -> Generator[StrFragment, None, None]:
    precedence_expr = PRECEDENCE[type(expr)]
    needs_parentheses = state.parent_precedence >= precedence_expr
    if needs_parentheses:
        yield "("
    yield from visit_expr(self, expr, state)
    if needs_parentheses:
        yield ")"

parse_katex_where_static ¤

parse_katex_where_static(
    katex_where_rt: HasKaTeXWhere,
    katex_where_st: str | Expr,
) -> tuple[str, ExprDict | None]
Source code in src/isqx/mkdocs/extension.py
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
def parse_katex_where_static(
    katex_where_rt: HasKaTeXWhere,
    katex_where_st: str | Expr,
) -> tuple[str, ExprDict | None]:
    # working with static analysis is very annoying, we have to handle kwargs
    # and defaults. using a bunch of asserts for now.
    assert isinstance(katex_where_st, ExprCall), katex_where_st
    katex_st = katex_where_st.arguments[0]
    katex_st_value = (
        katex_st.value if isinstance(katex_st, ExprKeyword) else katex_st
    )
    assert isinstance(katex_st_value, str), katex_st_value
    katex_st_value = (
        katex_st_value.removeprefix("'")
        .removeprefix('"')
        .removesuffix("'")
        .removesuffix('"')
        .replace("\\\\", "\\")
    )  # regex is probably better but eh
    if not len(katex_where_st.arguments) > 1:
        return katex_st_value, None
    where_st = katex_where_st.arguments[1]
    where_st_value = where_st
    if isinstance(where_st, ExprKeyword):
        if where_st.name != "where":
            return katex_st_value, None
        where_st_value = where_st.value
    assert isinstance(where_st_value, ExprDict), where_st_value
    assert katex_where_rt.katex == katex_st_value, (
        f"`griffe` skipped an entry!"
        "\nthis can happen in two keys in the dictionary refer to the same "
        "value. check the definitions."
        f"\nruntime value {katex_where_rt.katex}\ndoes not match griffe: {katex_st_value}"
    )
    return katex_st_value, where_st_value

parse_where ¤

parse_where(
    key_rt: DetailKey | Callable[..., DetailKey],
    detail_rt: HasKaTeXWhere,
    where_st: ExprDict,
    *,
    self_path: str,
    reverse_definitions: _ReverseDefinitions,
    formatter: MkdocsFormatter,
    mut_self_symbols: list[str],
) -> Generator[Where, None, None]
Source code in src/isqx/mkdocs/extension.py
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
def parse_where(
    key_rt: DetailKey | Callable[..., DetailKey],
    detail_rt: HasKaTeXWhere,
    where_st: ExprDict,
    *,
    self_path: str,
    reverse_definitions: _ReverseDefinitions,
    formatter: MkdocsFormatter,
    mut_self_symbols: list[str],
) -> Generator[Where, None, None]:
    if detail_rt.where is None:
        return
    for (w_rt_k, w_rt_v), w_st_v in zip(
        detail_rt.where.items(),
        where_st.values,
    ):
        description = _parse_fragments(w_rt_v, w_st_v, self_path)
        unit_expr: IsqxExpr | None = None
        frags_for_unit_inference = (
            w_rt_v if isinstance(w_rt_v, tuple) else (w_rt_v,)
        )
        for frag in frags_for_unit_inference:
            if isinstance(frag, (str, Anchor)):
                continue
            if isinstance(frag, _RefSelf):
                mut_self_symbols.append(w_rt_k)
                if isinstance(key_rt, _ARGS_DETAIL_KEY):
                    unit_expr = _get_unit_expr(key_rt, reverse_definitions)
            else:
                unit_expr = _get_unit_expr(frag, reverse_definitions)
            if unit_expr is not None:
                break  # only infer unit from the first fragment

        unit: Unit
        if unit_expr is None:
            unit = None
        elif kind(unit_expr) == "dimensionless":
            unit = ("dimensionless",)
        else:
            unit = tuple(s for s in formatter.fmt(unit_expr))  # str  for now

        yield Where(
            symbol=w_rt_k,
            description=description,
            unit=unit,
        )

screaming_to_normal ¤

screaming_to_normal(s: str, i: int) -> str
Source code in src/isqx/mkdocs/extension.py
656
657
658
659
def screaming_to_normal(s: str, i: int) -> str:
    # a better way would be to parse the docstring but that might be too verbose
    normalized = s.split(".")[-1].lower().replace("_", " ")
    return normalized.capitalize() if i == 0 else normalized

inject_citation_into_docstring ¤

inject_citation_into_docstring(
    obj: Object, agent: Visitor | Inspector
) -> None
Source code in src/isqx/mkdocs/extension.py
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
def inject_citation_into_docstring(
    obj: Object, agent: Visitor | Inspector
) -> None:
    if not obj.canonical_path.startswith("isqx._citations."):
        return
    # quick hack to put runtime value into the docstring
    doc_str = dynamic_import(obj.path)
    if not isinstance(doc_str, str):
        logger.warning(
            f"expected citation {obj.path} to be a string, got {type(doc_str)}"
        )
        return
    obj.docstring = Docstring(
        doc_str,
        parent=obj,
        parser=agent.docstring_parser,
        parser_options=agent.docstring_options,
    )