Skip to content

Commit 7d54a6d

Browse files
authored
tests: ensure tests never mutate DEFAULT_OPTS or cached TOML opts (#509)
* tests: ensure tests never mutate DEFAULT_OPTS or cached TOML opts * lazy load plugins for speed and avoiding circular import
1 parent e05f2cf commit 7d54a6d

File tree

4 files changed

+57
-26
lines changed

4 files changed

+57
-26
lines changed

.pre-commit-config.yaml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,11 @@ repos:
2323
hooks:
2424
- id: absolufy-imports
2525
- repo: https://github.com/PyCQA/isort
26-
rev: c235f5e450b4b84e58d114ed4c589cbf454175a3 # frozen: 5.13.2
26+
rev: 0a0b7a830386ba6a31c2ec8316849ae4d1b8240d # frozen: 6.0.0
2727
hooks:
2828
- id: isort
2929
- repo: https://github.com/psf/black
30-
rev: 1b2427a2b785cc4aac97c19bb4b9a0de063f9547 # frozen: 24.10.0
30+
rev: 8a737e727ac5ab2f1d4cf5876720ed276dc8dc4b # frozen: 25.1.0
3131
hooks:
3232
- id: black
3333
- repo: https://github.com/hukkin/docformatter
@@ -43,6 +43,6 @@ repos:
4343
- flake8-builtins
4444
- flake8-comprehensions
4545
- repo: https://github.com/pre-commit/pre-commit
46-
rev: cc4a52241565440ce200666799eef70626457488 # frozen: v4.0.1
46+
rev: b152e922ef11a97efe22ca7dc4f90011f0d1711c # frozen: v4.1.0
4747
hooks:
4848
- id: validate_manifest

src/mdformat/_cli.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,11 @@ def run(cli_args: Sequence[str], cache_toml: bool = True) -> int: # noqa: C901
5454
print_error(str(e))
5555
return 1
5656

57-
opts: Mapping = {**DEFAULT_OPTS, **toml_opts, **cli_core_opts}
57+
opts = {**DEFAULT_OPTS, **toml_opts, **cli_core_opts}
58+
59+
# Merge plugin options from CLI.
60+
# Make a copy of opts["plugin"] to not mutate DEFAULT_OPTS or cached TOML.
61+
opts["plugin"] = dict(opts["plugin"])
5862
for plugin_id, plugin_opts in cli_plugin_opts.items():
5963
if plugin_id in opts["plugin"]:
6064
opts["plugin"][plugin_id] |= plugin_opts

src/mdformat/_conf.py

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,24 @@
22

33
import functools
44
from pathlib import Path
5+
from types import MappingProxyType
56
from typing import Mapping
67

78
from mdformat._compat import tomllib
9+
from mdformat._util import EMPTY_MAP
810

9-
DEFAULT_OPTS = {
10-
"wrap": "keep",
11-
"number": False,
12-
"end_of_line": "lf",
13-
"validate": True,
14-
"exclude": [],
15-
"plugin": {},
16-
"extensions": None,
17-
"codeformatters": None,
18-
}
11+
DEFAULT_OPTS = MappingProxyType(
12+
{
13+
"wrap": "keep",
14+
"number": False,
15+
"end_of_line": "lf",
16+
"validate": True,
17+
"exclude": (),
18+
"plugin": EMPTY_MAP,
19+
"extensions": None,
20+
"codeformatters": None,
21+
}
22+
)
1923

2024

2125
class InvalidConfError(Exception):

src/mdformat/plugins.py

Lines changed: 35 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
from __future__ import annotations
22

3-
import argparse
4-
from collections.abc import Callable, Mapping
53
from typing import TYPE_CHECKING, Any, Protocol
64

75
from mdformat._compat import importlib_metadata
86

97
if TYPE_CHECKING:
8+
import argparse
9+
from collections.abc import Callable, Mapping
10+
1011
from markdown_it import MarkdownIt
1112

1213
from mdformat.renderer.typing import Postprocess, Render
@@ -31,13 +32,6 @@ def _load_entrypoints(
3132
return loaded_ifaces, dist_versions
3233

3334

34-
CODEFORMATTERS: Mapping[str, Callable[[str, str], str]]
35-
_CODEFORMATTER_DISTS: Mapping[str, tuple[str, list[str]]]
36-
CODEFORMATTERS, _CODEFORMATTER_DISTS = _load_entrypoints(
37-
importlib_metadata.entry_points(group="mdformat.codeformatter")
38-
)
39-
40-
4135
class ParserExtensionInterface(Protocol):
4236
"""An interface for parser extension plugins."""
4337

@@ -86,8 +80,37 @@ def update_mdit(mdit: MarkdownIt) -> None:
8680
"""Update the parser, e.g. by adding a plugin: `mdit.use(myplugin)`"""
8781

8882

83+
CODEFORMATTERS: Mapping[str, Callable[[str, str], str]]
84+
_CODEFORMATTER_DISTS: Mapping[str, tuple[str, list[str]]]
8985
PARSER_EXTENSIONS: Mapping[str, ParserExtensionInterface]
9086
_PARSER_EXTENSION_DISTS: Mapping[str, tuple[str, list[str]]]
91-
PARSER_EXTENSIONS, _PARSER_EXTENSION_DISTS = _load_entrypoints(
92-
importlib_metadata.entry_points(group="mdformat.parser_extension")
93-
)
87+
88+
89+
def __getattr__(name: str) -> Mapping[str, Any]:
90+
"""Attribute getter fallback.
91+
92+
Used to lazy load CODEFORMATTERS and PARSER_EXTENSIONS. It'd
93+
probably be more readable to use `@functools.cache` decorated
94+
functions, but `__getattr__` is used now for back compatibility.
95+
"""
96+
if name in {"CODEFORMATTERS", "_CODEFORMATTER_DISTS"}:
97+
formatters, formatter_dists = _load_entrypoints(
98+
importlib_metadata.entry_points(group="mdformat.codeformatter")
99+
)
100+
# Cache the values in this module for next time, so that `__getattr__`
101+
# is only called once per `name`.
102+
global CODEFORMATTERS, _CODEFORMATTER_DISTS
103+
CODEFORMATTERS = formatters
104+
_CODEFORMATTER_DISTS = formatter_dists
105+
return formatters if name == "CODEFORMATTERS" else formatter_dists
106+
if name in {"PARSER_EXTENSIONS", "_PARSER_EXTENSION_DISTS"}:
107+
extensions, extension_dists = _load_entrypoints(
108+
importlib_metadata.entry_points(group="mdformat.parser_extension")
109+
)
110+
# Cache the value in this module for next time, so that `__getattr__`
111+
# is only called once per `name`.
112+
global PARSER_EXTENSIONS, _PARSER_EXTENSION_DISTS
113+
PARSER_EXTENSIONS = extensions
114+
_PARSER_EXTENSION_DISTS = extension_dists
115+
return extensions if name == "PARSER_EXTENSIONS" else extension_dists
116+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")

0 commit comments

Comments
 (0)