Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions ci-constraints.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
mkdocstrings==0.26.1
griffe==1.3.1
mkdocstrings==0.30.0
griffe==1.12.1
mkdocs-material==9.2.1
81 changes: 49 additions & 32 deletions mkdocstrings_handlers/vba/_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,21 @@

from __future__ import annotations

import copy
from collections import ChainMap
from copy import deepcopy
from pathlib import Path
from typing import (
Any,
MutableMapping,
Dict,
Mapping,
Tuple,
ClassVar,
)

from griffe import patch_loggers
from markdown import Markdown
from mkdocs.config.defaults import MkDocsConfig
from mkdocs.exceptions import PluginError
from mkdocstrings.handlers.base import BaseHandler, CollectionError
from mkdocstrings import BaseHandler, CollectionError
from mkdocstrings.loggers import get_logger

from ._crossref import do_crossref, do_multi_crossref
Expand Down Expand Up @@ -51,17 +51,17 @@ def __init__(self, *, base_dir: Path, encoding: str, **kwargs: Any) -> None:
self.base_dir = base_dir
self.encoding = encoding

name: str = "vba"
name: ClassVar[str] = "vba"
"""
The handler's name.
"""

domain: str = "vba"
domain: ClassVar[str] = "vba"
"""
The cross-documentation domain/language for this handler.
"""

fallback_theme = "material"
fallback_theme: ClassVar[str] = "material"
"""
The theme to fall back to.
"""
Expand Down Expand Up @@ -107,6 +107,17 @@ def __init__(self, *, base_dir: Path, encoding: str, **kwargs: Any) -> None:
**`docstring_section_style`** | `str` | The style used to render docstring sections. Options: `table`, `list`, `spacy`. | `table`
"""

def get_options(self, local_options: Mapping[str, Any]) -> Dict[str, Any]:
"""Combine the default options with the local options.

Arguments:
local_options: The options provided in Markdown pages.

Returns:
The combined options.
"""
return deepcopy({**self.default_config, **local_options})

def collect(
self,
identifier: str,
Expand Down Expand Up @@ -141,75 +152,81 @@ def collect(
def render(
self,
data: VbaModuleInfo,
config: Mapping[str, Any],
options: MutableMapping[str, Any],
*,
locale: str | None = None,
) -> str:
final_config = ChainMap(dict(copy.deepcopy(config)), self.default_config)
template = self.env.get_template(f"module.html")

# Heading level is a "state" variable, that will change at each step
# of the rendering recursion. Therefore, it's easier to use it as a plain value
# than as an item in a dictionary.
heading_level = final_config["heading_level"]
heading_level = options["heading_level"]
try:
final_config["members_order"] = Order(final_config["members_order"])
options["members_order"] = Order(options["members_order"])
except ValueError:
choices = "', '".join(item.value for item in Order)
raise PluginError(
f"Unknown members_order '{final_config['members_order']}', choose between '{choices}'."
f"Unknown members_order '{options['members_order']}', choose between '{choices}'."
)

return template.render(
config=final_config,
config=options,
module=data,
heading_level=heading_level,
root=True,
)

def update_env(self, md: Markdown, config: Dict[Any, Any]) -> None:
super().update_env(md, config)
def update_env(self, config: Dict[Any, Any]) -> None:
self.env.trim_blocks = True
self.env.lstrip_blocks = True
self.env.keep_trailing_newline = False
self.env.filters["crossref"] = do_crossref
self.env.filters["multi_crossref"] = do_multi_crossref
self.env.filters["order_members"] = do_order_members

def get_anchors(self, data: VbaModuleInfo) -> Tuple[str, ...]:
def get_aliases(self, identifier: str) -> Tuple[str, ...]:
"""Get the aliases of the given identifier.

Aliases are used to register secondary URLs in mkdocs-autorefs,
to make sure cross-references work with any location of the same object.

Arguments:
identifier: The identifier to get aliases for.

Returns:
A tuple of aliases for the given identifier.
"""
try:
data = self.collect(identifier, {})
except CollectionError:
return ()
return data.path.as_posix(), *(p.signature.name for p in data.procedures)
Comment on lines +200 to 204
Copy link
Member Author

@pawamoy pawamoy Jan 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have replicated previous behavior here, but I have to note that this seems incorrect to return both the path of the file, as well as all the procedure names within it. It's possible that the previous method (get_anchors) was confusing, and the "anchors" concept not well explained. The renaming to get_aliases is to make it more clear: it's not really about HTML anchors, it's about obtaining all the different "aliases" ("locations") for a given object, to correctly populate mkdocs-autorefs data. I know nothing about VBA, but surely Procedure1 is not an alias of Procedure2?

Aliases (and the identifier) are supposed to be strings that you can pass to collect.

Here I'd either just return (data.path.as_posix(),) (or even an empty tuple ()), or add a way to support collecting Procedure1 without a file path (or these procedure objects should expose fully qualified names that can be collected).

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will have to dig in and re-learn this code. A few years ago, I wrote the VBA handler by copying the Python handler and modifying it until it works. Even back then, I did not understand all the code. I'll try to go from this PR and see if I can build up a better understanding.



def get_handler(
*,
theme: str = "material",
custom_templates: str | None = None,
config_file_path: str | None = None,
encoding: str = "latin1",
tool_config: MkDocsConfig | None = None,
**kwargs: Any,
) -> VbaHandler:
"""
Get a new `VbaHandler`.

Arguments:
theme: The theme to use when rendering contents.
custom_templates: Directory containing custom templates.
config_file_path: The MkDocs configuration file path.
encoding:
The encoding to use when reading VBA files.
Excel exports .bas and .cls files as `latin1`.
See https://en.wikipedia.org/wiki/ISO/IEC_8859-1 .
tool_config: SSG configuration.
kwargs: Extra keyword arguments that we don't use.

Returns:
An instance of `VbaHandler`.
"""
return VbaHandler(
base_dir=(
Path(config_file_path).resolve().parent
if config_file_path
else Path(".").resolve()
),
encoding=encoding,
handler="vba",
theme=theme,
custom_templates=custom_templates,
base_dir = (
Path(getattr(tool_config, "config_file_path", None) or "./mkdocs.yml")
.resolve()
.parent
)
return VbaHandler(base_dir=base_dir, encoding=encoding, **kwargs)
35 changes: 18 additions & 17 deletions mkdocstrings_handlers/vba/_util.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import re
from typing import List, Generator
from typing import List, Generator, Tuple

from griffe import Docstring, Function, Parameters, Parameter, Parser

Expand Down Expand Up @@ -132,29 +132,30 @@ def parse_signature(line: str) -> VbaSignatureInfo:

def find_procedures(code: str) -> Generator[VbaProcedureInfo, None, None]:
lines = code.splitlines()
procedure = None

procedure: Tuple[VbaSignatureInfo, int] | None = None
"""
The signature and first line number of the procedure currently being parsed,
or None when scanning for the start of the next procedure.
"""

for i, line in enumerate(lines):
if procedure is None:
# Looking for signature. Ignore everything until we find one.
if not is_signature(line):
continue

procedure = {
"signature": parse_signature(line),
"first_line": i + 1,
}
procedure = parse_signature(line), i + 1
continue

if is_end(line):
# Found the end of a procedure.
procedure["last_line"] = i + 1
signature, first_line = procedure
last_line = i + 1

# The docstring consists of the comment lines directly after the signature.
docstring_lines = []
procedure_source = lines[
procedure["first_line"] - 1 : procedure["last_line"] - 1
]
procedure_source = lines[first_line - 1 : last_line - 1]
for source_line in procedure_source[1:]:
if not is_comment(source_line):
break
Expand All @@ -167,29 +168,29 @@ def find_procedures(code: str) -> Generator[VbaProcedureInfo, None, None]:
value=docstring_value,
parser=Parser.google,
parser_options={},
lineno=procedure["first_line"] + 1,
lineno=first_line + 1,
parent=Function(
name=procedure["signature"].name,
name=signature.name,
parameters=Parameters(
*(
Parameter(
name=arg.name,
annotation=arg.arg_type,
default=arg.default,
)
for arg in procedure["signature"].args
for arg in signature.args
)
),
returns=procedure["signature"].return_type,
returns=signature.return_type,
),
)

# Yield it and start over.
yield VbaProcedureInfo(
signature=procedure["signature"],
signature=signature,
docstring=docstring,
first_line=procedure["first_line"],
last_line=procedure["last_line"],
first_line=first_line,
last_line=last_line,
source=procedure_source,
)
procedure = None
Expand Down
4 changes: 2 additions & 2 deletions mypy-requirements.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
mypy==1.11.2
types-setuptools==75.*
mypy==1.17.*
types-setuptools==80.*
types-Markdown==3.*
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
"setuptools_scm",
],
install_requires=[
"mkdocstrings>=0.26.1,<1",
"mkdocstrings>=0.30,<1",
"griffe>=1.3.1,<2",
"mkdocs-material>=9.2,<10",
],
Expand Down
2 changes: 1 addition & 1 deletion test/handler/test_collect.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
def _test_collect(*, write_bytes: bytes, read_encoding: str) -> VbaModuleInfo:
with TemporaryDirectory() as tmp_dir_str:
tmp_dir = Path(tmp_dir_str)
handler = get_handler(encoding=read_encoding)
handler = get_handler(encoding=read_encoding, mdx=[], mdx_config={})
p = tmp_dir / "source.bas"
p.write_bytes(write_bytes)
return handler.collect(identifier=p.as_posix(), config={})
Expand Down