Skip to content

Commit d5b294c

Browse files
committed
Monkey patch to replace read_toml_opts entirely. Resolves csala#4
1 parent 9f1d852 commit d5b294c

File tree

3 files changed

+87
-158
lines changed

3 files changed

+87
-158
lines changed

mdformat_pyproject/plugin.py

Lines changed: 18 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import pathlib
44
import sys
5-
from typing import TYPE_CHECKING, MutableMapping, Optional, Sequence, Union
5+
from typing import TYPE_CHECKING, MutableMapping, Optional, Sequence, Tuple, Union
66

77
import markdown_it
88
import mdformat
@@ -27,19 +27,14 @@
2727

2828

2929
@cache
30-
def _find_pyproject_toml_path(search_path: str) -> Optional[pathlib.Path]:
31-
"""Find the pyproject.toml file that corresponds to the search path.
30+
def _find_pyproject_toml_path(search_path: pathlib.Path) -> Optional[pathlib.Path]:
31+
"""Find the pyproject.toml file that applies to the search path.
3232
3333
The search is done ascending through the folders tree until a pyproject.toml
3434
file is found in the same folder. If the root '/' is reached, None is returned.
35-
36-
The special path "-" used for stdin inputs is replaced with the current working
37-
directory.
3835
"""
39-
if search_path == "-":
40-
search_path = pathlib.Path.cwd()
41-
else:
42-
search_path = pathlib.Path(search_path).resolve()
36+
if search_path.is_file():
37+
search_path = search_path.parent
4338

4439
for parent in (search_path, *search_path.parents):
4540
candidate = parent / "pyproject.toml"
@@ -68,50 +63,26 @@ def _parse_pyproject(pyproject_path: pathlib.Path) -> Optional[_ConfigOptions]:
6863

6964

7065
@cache
71-
def _reload_cli_opts() -> _ConfigOptions:
72-
"""Re-parse the sys.argv array to deduce which arguments were used in the CLI.
73-
74-
If unknown arguments are found, we deduce that mdformat is being used as a
75-
python library and therefore no mdformat command line arguments were passed.
66+
def read_toml_opts(conf_dir: pathlib.Path) -> Tuple[MutableMapping, Optional[pathlib.Path]]:
67+
"""Alternative read_toml_opts that reads from pyproject.toml instead of .mdformat.toml.
7668
77-
Notice that the strategy above does not fully close the door to situations
78-
with colliding arguments with different meanings, but the rarity of the
79-
situation and the complexity of a possible solution makes the risk worth taking.
69+
Notice that if `.mdformat.toml` exists it is ignored.
8070
"""
81-
import mdformat._cli
82-
83-
if hasattr(mdformat.plugins, "_PARSER_EXTENSION_DISTS"):
84-
# New API, mdformat>=0.7.19
85-
arg_parser = mdformat._cli.make_arg_parser(
86-
mdformat.plugins._PARSER_EXTENSION_DISTS,
87-
mdformat.plugins._CODEFORMATTER_DISTS,
88-
mdformat.plugins.PARSER_EXTENSIONS,
89-
)
71+
pyproject_path = _find_pyproject_toml_path(conf_dir)
72+
if pyproject_path:
73+
pyproject_opts = _parse_pyproject(pyproject_path)
9074
else:
91-
# Backwards compatibility, mdformat<0.7.19
92-
arg_parser = mdformat._cli.make_arg_parser(
93-
mdformat.plugins.PARSER_EXTENSIONS,
94-
mdformat.plugins.CODEFORMATTERS,
95-
)
75+
pyproject_opts = {}
9676

97-
args, unknown = arg_parser.parse_known_args(sys.argv[1:])
98-
if unknown:
99-
return {}
100-
101-
return {key: value for key, value in vars(args).items() if value is not None}
77+
return pyproject_opts, pyproject_path
10278

10379

10480
def update_mdit(mdit: markdown_it.MarkdownIt) -> None:
105-
"""Read the pyproject.toml file and re-create the mdformat options."""
106-
mdformat_options: _ConfigOptions = mdit.options["mdformat"]
107-
file_path = mdformat_options.get("filename", "-")
108-
pyproject_path = _find_pyproject_toml_path(file_path)
109-
if pyproject_path:
110-
pyproject_opts = _parse_pyproject(pyproject_path)
111-
if pyproject_opts is not None:
112-
cli_opts = _reload_cli_opts()
113-
mdformat_options.update(**pyproject_opts)
114-
mdformat_options.update(**cli_opts)
81+
"""No-op, since this plugin only monkey patches and does not modify mdit."""
82+
pass
11583

11684

11785
RENDERERS: MutableMapping[str, "Render"] = {}
86+
87+
# Monkey patch mdformat._conf to use our own read_toml_opts version
88+
mdformat._conf.read_toml_opts = read_toml_opts

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ profile = "black"
4646
[tool.mdformat]
4747
wrap = 99
4848
number = true
49+
exclude = [".tox/**", ".venv/**"]
4950

5051
[tool.coverage.report]
5152
exclude_lines = [

tests/test_plugin.py

Lines changed: 68 additions & 111 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,12 @@
66

77
import markdown_it
88
import pytest
9-
from mdformat._conf import InvalidConfError
109

1110
from mdformat_pyproject import plugin
1211

13-
THIS_MODULE_PATH = pathlib.Path(__file__).parent
12+
THIS_MODULE_PATH = pathlib.Path(__file__)
13+
THIS_MODULE_PARENT = THIS_MODULE_PATH.parent
14+
PYPROJECT_PATH = THIS_MODULE_PARENT.parent / "pyproject.toml"
1415

1516

1617
def setup_function():
@@ -21,29 +22,36 @@ def setup_function():
2122

2223

2324
@pytest.fixture
24-
def fake_filename():
25+
def nonexistent_path():
2526
fake_parent = "/fake"
2627
while pathlib.Path(fake_parent).exists():
2728
fake_parent += "e"
2829

29-
return str(pathlib.Path(fake_parent) / "path" / "to" / "a" / "file.md")
30+
return pathlib.Path(fake_parent) / "path" / "to" / "a" / "file.md"
3031

3132

32-
@unittest.mock.patch("mdformat_pyproject.plugin.pathlib.Path.cwd", lambda: THIS_MODULE_PATH)
33-
def test__find_pyproject_toml_path_cwd():
34-
"""Test _find_pyproject_toml_path when search_path is `-`.
33+
def test__find_pyproject_toml_path_directory_inside_project():
34+
"""Test _find_pyproject_toml_path when search_path points at a directory within the project.
3535
36-
Setup:
37-
- Patch Path.cwd to return the path of this module, to ensure
38-
that the `cwd` points at a subfolder of the project regardless
39-
of where the `pytest` command was executed.
4036
Input:
41-
- search_path="-"
37+
- search_path=THIS_MODULE_PATH -> directory is inside the project
4238
Expected output:
4339
- pyproject.toml of this project.
4440
"""
45-
returned = plugin._find_pyproject_toml_path("-")
46-
assert returned == THIS_MODULE_PATH.parent / "pyproject.toml"
41+
returned = plugin._find_pyproject_toml_path(THIS_MODULE_PARENT)
42+
assert returned == PYPROJECT_PATH
43+
44+
45+
def test__find_pyproject_toml_path_directory_outside_project(nonexistent_path):
46+
"""Test _find_pyproject_toml_path when search_path points at a directory within the project.
47+
48+
Input:
49+
- search_path=nonexistent_path.parent -> directory is outside the project
50+
Expected output:
51+
- pyproject.toml of this project.
52+
"""
53+
returned = plugin._find_pyproject_toml_path(nonexistent_path.parent)
54+
assert returned is None
4755

4856

4957
def test__find_pyproject_toml_path_file_inside_project():
@@ -54,131 +62,80 @@ def test__find_pyproject_toml_path_file_inside_project():
5462
Expected output:
5563
- pyproject.toml of this project.
5664
"""
57-
returned = plugin._find_pyproject_toml_path(__file__)
58-
assert returned == THIS_MODULE_PATH.parent / "pyproject.toml"
65+
returned = plugin._find_pyproject_toml_path(THIS_MODULE_PATH)
66+
assert returned == PYPROJECT_PATH
5967

6068

61-
def test__find_pyproject_toml_path_file_outside_of_project(fake_filename):
69+
def test__find_pyproject_toml_path_file_outside_of_project(nonexistent_path):
6270
"""Test _find_pyproject_toml_path when search_path points at a file outside of a project.
6371
6472
Input:
65-
- search_path="/fake/folder/path" -> A madeup path to an inexisting folder.
73+
- search_path="/fake/folder/path" -> A madeup path to an nonexistent folder.
6674
Expected output:
6775
- None
6876
"""
69-
returned = plugin._find_pyproject_toml_path(fake_filename)
77+
returned = plugin._find_pyproject_toml_path(nonexistent_path)
7078
assert returned is None
7179

7280

73-
def get_mdit(filename, **kwargs):
74-
mdit = unittest.mock.Mock(spec_set=markdown_it.MarkdownIt())
75-
mdformat_options = {
76-
"check": False,
77-
"end_of_line": "lf",
78-
"filename": str(pathlib.Path(filename).resolve()),
79-
"number": False,
80-
"paths": [filename],
81-
"wrap": 80,
82-
}
83-
mdit.options = {"mdformat": {**mdformat_options, **kwargs}}
84-
return mdit
85-
86-
87-
def test_update_mdit_no_config(fake_filename):
88-
"""Test update_mdit when there is no pyproject.toml.
89-
90-
Input:
91-
- mdit with the default opts and a filename located inside a fake folder
92-
Excepted Side Effect:
93-
- mdit options should remain untouched
94-
"""
95-
mdit = get_mdit(fake_filename)
96-
expected_options = copy.deepcopy(mdit.options["mdformat"])
97-
98-
plugin.update_mdit(mdit)
99-
100-
assert mdit.options["mdformat"] == expected_options
101-
102-
103-
def test_update_mdit_pyproject():
104-
"""Test update_mdit when there is configuration inside the pyproject.toml file.
81+
def test_read_toml_opts_with_pyproject():
82+
"""Test read_toml_opts when there is a pyproject.toml file.
10583
10684
Input:
107-
- mdit with the default opts and a filename located inside the current project.
108-
Excepted Side Effect:
109-
- mdit options should be updated to the pyproject values
85+
- conf_dir pointing to this module's folder
86+
Expected Output:
87+
- Tuple containing:
88+
- Dict with the mdformat options from pyproject.toml
89+
- Path to the pyproject.toml file
11090
"""
111-
mdit = get_mdit(__file__)
112-
113-
plugin.update_mdit(mdit)
114-
115-
mdformat_options = mdit.options["mdformat"]
116-
assert mdformat_options["wrap"] == 99
117-
assert mdformat_options["number"] is True
118-
assert mdformat_options["end_of_line"] == "lf"
119-
91+
# run
92+
opts, path = plugin.read_toml_opts(THIS_MODULE_PATH)
12093

121-
_BROKEN_OPTS = {"tool": {"mdformat": {"invalid": "option"}}}
94+
# assert
95+
assert opts == {"wrap": 99, "number": True, "exclude": [".tox/**", ".venv/**"]}
96+
assert path == PYPROJECT_PATH
12297

12398

124-
@unittest.mock.patch("mdformat_pyproject.plugin.tomllib.load", lambda _: _BROKEN_OPTS)
125-
def test_update_mdit_invalid_pyproject():
126-
"""Test update_mdit when there are invlid options inside the pyproject.toml file.
99+
def test_read_toml_opts_without_pyproject(nonexistent_path):
100+
"""Test read_toml_opts when there is no pyproject.toml file.
127101
128-
Setup:
129-
- Mock tomllib.load to return an invalid pyproject.toml file.
130-
- Also ensure that the load cache is clear
131102
Input:
132-
- mdit with the default opts and a filename located inside the current project.
133-
Excepted Side Effect:
134-
- _validate_keys should raise an exception.
135-
103+
- conf_dir pointing to a non-existent folder
104+
Expected Output:
105+
- Tuple containing:
106+
- Empty dict
107+
- None
136108
"""
137-
mdit = get_mdit(__file__)
109+
# run
110+
opts, path = plugin.read_toml_opts(nonexistent_path)
138111

139-
with pytest.raises(InvalidConfError):
140-
plugin.update_mdit(mdit)
112+
# assert
113+
assert opts == {}
114+
assert path is None
141115

142116

143-
@unittest.mock.patch("mdformat_pyproject.plugin.sys.argv", ["mdformat", "--wrap", "70", __file__])
144-
def test_update_mdit_pyproject_and_cli():
145-
"""Test update_mdit when there are conflicting pyproject.toml configuration and cli argumnents.
117+
def test_update_mdit_no_config():
118+
"""Test update_mdit which is now a no-op.
146119
147-
Setup:
148-
- Patch sys.argv to inject cli options different than the pyproject.toml.
149120
Input:
150-
- mdit with the default opts and a filename located inside the current project.
151-
Excepted Side Effect:
152-
- mdit options should be updated, with the cli options having priority over the
153-
pyproject ones.
121+
- mdit with arbitrary configuration
122+
Expected Side Effect:
123+
- mdit options should remain untouched
154124
"""
155-
mdit = get_mdit(__file__)
156-
expected_options = copy.deepcopy(mdit.options["mdformat"])
157-
158-
plugin.update_mdit(mdit)
159-
160-
expected_options["wrap"] = 70
161-
expected_options["number"] = True
162-
assert mdit.options["mdformat"] == expected_options
163-
164-
165-
@unittest.mock.patch("mdformat_pyproject.plugin.sys.argv", ["fake", "--wrap", "70", "--unknown"])
166-
def test_update_mdit_unknown_cli_arguments():
167-
"""Test update_mdit when there are unknown arguments passed in the command line.
125+
filename = "/some/file/name.toml"
126+
mdformat_options = {
127+
"check": False,
128+
"end_of_line": "lf",
129+
"filename": filename,
130+
"number": False,
131+
"paths": [filename],
132+
"wrap": 80,
133+
}
134+
mdit = unittest.mock.Mock(spec_set=markdown_it.MarkdownIt())
135+
mdit.options = {"mdformat": mdformat_options}
168136

169-
Setup:
170-
- Mock sys.argv to inject unknown cli options.
171-
Input:
172-
- mdit with the default opts and a filename located inside the current project.
173-
Excepted Side Effect:
174-
- The CLI arguments are discarded and only the pyproject.toml options are
175-
injected into the mdit options.
176-
"""
177-
mdit = get_mdit(__file__)
178-
expected_options = copy.deepcopy(mdit.options["mdformat"])
137+
expected_options = copy.deepcopy(mdformat_options)
179138

180139
plugin.update_mdit(mdit)
181140

182-
expected_options["wrap"] = 99 # Still from pyproject
183-
expected_options["number"] = True
184141
assert mdit.options["mdformat"] == expected_options

0 commit comments

Comments
 (0)