Skip to content

Commit ac7b240

Browse files
committed
Handle !relative (and any future constructors) in mkdocs.yml; resolves #199
1 parent fdcc912 commit ac7b240

File tree

3 files changed

+66
-22
lines changed

3 files changed

+66
-22
lines changed

CHANGES.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
- When calling `set-default`, you can now pass `--allow-undefined` to set the
77
default to a version that doesn't exist yet
88
- Add global-level `-q` / `--quiet` option to suppress warning messages
9+
- Add support for handling `!relative` in `mkdocs.yml`
910

1011
### Bug fixes
1112
- When loading an MkDocs config, mike now runs the `startup` and `shutdown`

mike/mkdocs_utils.py

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,31 @@
1212
docs_version_var = 'MIKE_DOCS_VERSION'
1313

1414

15+
class RoundTrippableTag:
16+
def __init__(self, node):
17+
self.node = node
18+
19+
def __repr__(self):
20+
return repr(self.node)
21+
22+
@staticmethod
23+
def constructor(loader, suffix, node):
24+
return RoundTrippableTag(node)
25+
26+
@staticmethod
27+
def representer(dumper, data):
28+
return data.node
29+
30+
31+
class RoundTripLoader(yaml.SafeLoader):
32+
pass
33+
34+
35+
yaml.add_multi_constructor('!', RoundTrippableTag.constructor,
36+
Loader=RoundTripLoader)
37+
yaml.add_multi_representer(RoundTrippableTag, RoundTrippableTag.representer)
38+
39+
1540
def _open_config(config_file=None):
1641
if config_file is None:
1742
config_file = ['mkdocs.yml', 'mkdocs.yaml']
@@ -45,7 +70,7 @@ def load_config(config_file=None, **kwargs):
4570
def inject_plugin(config_file):
4671
with _open_config(config_file) as f:
4772
config_file = f.name
48-
config = mkdocs.utils.yaml_load(f)
73+
config = mkdocs.utils.yaml_load(f, loader=RoundTripLoader)
4974

5075
plugins = config.setdefault('plugins', ['search'])
5176
for i in plugins:

test/unit/test_mkdocs_utils.py

Lines changed: 39 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -86,49 +86,49 @@ def test_nonexist(self):
8686

8787

8888
class TestInjectPlugin(unittest.TestCase):
89+
def setUp(self):
90+
self.out = Stream('mike-mkdocs.yml')
91+
8992
def test_no_plugins(self):
90-
out = Stream('mike-mkdocs.yml')
9193
cfg = '{}'
9294
with mock.patch('builtins.open',
9395
mock_open_files({'mkdocs.yml': cfg})), \
9496
mock.patch('mike.mkdocs_utils.NamedTemporaryFile',
95-
return_value=out), \
97+
return_value=self.out), \
9698
mock.patch('os.remove') as mremove:
9799
with mkdocs_utils.inject_plugin('mkdocs.yml') as f:
98-
self.assertEqual(f, out.name)
99-
newcfg = yaml.load(out.getvalue(), Loader=yaml.Loader)
100+
self.assertEqual(f, self.out.name)
101+
newcfg = yaml.safe_load(self.out.getvalue())
100102
mremove.assert_called_once()
101103

102104
self.assertEqual(newcfg, {'plugins': ['mike', 'search']})
103105

104106
def test_other_plugins(self):
105-
out = Stream('mike-mkdocs.yml')
106107
cfg = 'plugins:\n - foo\n - bar:\n option: true'
107108
with mock.patch('builtins.open',
108109
mock_open_files({'mkdocs.yml': cfg})), \
109110
mock.patch('mike.mkdocs_utils.NamedTemporaryFile',
110-
return_value=out), \
111+
return_value=self.out), \
111112
mock.patch('os.remove') as mremove:
112113
with mkdocs_utils.inject_plugin('mkdocs.yml') as f:
113-
self.assertEqual(f, out.name)
114-
newcfg = yaml.load(out.getvalue(), Loader=yaml.Loader)
114+
self.assertEqual(f, self.out.name)
115+
newcfg = yaml.safe_load(self.out.getvalue())
115116
mremove.assert_called_once()
116117

117118
self.assertEqual(newcfg, {'plugins': [
118119
'mike', 'foo', {'bar': {'option': True}},
119120
]})
120121

121122
def test_other_plugins_dict(self):
122-
out = Stream('mike-mkdocs.yml')
123123
cfg = 'plugins:\n foo: {}\n bar:\n option: true'
124124
with mock.patch('builtins.open',
125125
mock_open_files({'mkdocs.yml': cfg})), \
126126
mock.patch('mike.mkdocs_utils.NamedTemporaryFile',
127-
return_value=out), \
127+
return_value=self.out), \
128128
mock.patch('os.remove') as mremove:
129129
with mkdocs_utils.inject_plugin('mkdocs.yml') as f:
130-
self.assertEqual(f, out.name)
131-
newcfg = yaml.load(out.getvalue(), Loader=yaml.Loader)
130+
self.assertEqual(f, self.out.name)
131+
newcfg = yaml.safe_load(self.out.getvalue())
132132
mremove.assert_called_once()
133133

134134
self.assertEqual(newcfg, {'plugins': {
@@ -140,44 +140,62 @@ def test_other_plugins_dict(self):
140140
)
141141

142142
def test_mike_plugin(self):
143-
out = Stream('mike-mkdocs.yml')
144143
cfg = 'plugins:\n - mike'
145144
with mock.patch('builtins.open',
146145
mock_open_files({'mkdocs.yml': cfg})), \
147146
mock.patch('mike.mkdocs_utils.NamedTemporaryFile',
148-
return_value=out), \
147+
return_value=self.out), \
149148
mock.patch('os.remove') as mremove:
150149
with mkdocs_utils.inject_plugin('mkdocs.yml') as f:
151150
self.assertEqual(f, 'mkdocs.yml')
152-
self.assertEqual(out.getvalue(), '')
151+
self.assertEqual(self.out.getvalue(), '')
153152
mremove.assert_not_called()
154153

155154
def test_mike_plugin_options(self):
156-
out = Stream('mike-mkdocs.yml')
157155
cfg = 'plugins:\n - mike:\n option: true'
158156
with mock.patch('builtins.open',
159157
mock_open_files({'mkdocs.yml': cfg})), \
160158
mock.patch('mike.mkdocs_utils.NamedTemporaryFile',
161-
return_value=out), \
159+
return_value=self.out), \
162160
mock.patch('os.remove') as mremove:
163161
with mkdocs_utils.inject_plugin('mkdocs.yml') as f:
164162
self.assertEqual(f, 'mkdocs.yml')
165-
self.assertEqual(out.getvalue(), '')
163+
self.assertEqual(self.out.getvalue(), '')
166164
mremove.assert_not_called()
167165

166+
def test_round_trip(self):
167+
cfg = ('plugins:\n' +
168+
' - foo:\n option: !relative $config_dir\n' +
169+
' - bar:\n option: !ENV variable\n' +
170+
' - baz:\n option: !ENV [variable, default]'
171+
)
172+
with mock.patch('builtins.open',
173+
mock_open_files({'mkdocs.yml': cfg})), \
174+
mock.patch('mike.mkdocs_utils.NamedTemporaryFile',
175+
return_value=self.out), \
176+
mock.patch('os.remove') as mremove:
177+
with mkdocs_utils.inject_plugin('mkdocs.yml') as f:
178+
self.assertEqual(f, self.out.name)
179+
mremove.assert_called_once()
180+
181+
expected = ('plugins:\n- mike\n' +
182+
"- foo:\n option: !relative '$config_dir'\n" +
183+
"- bar:\n option: !ENV 'variable'\n"
184+
'- baz:\n option: !ENV [variable, default]\n')
185+
self.assertEqual(self.out.getvalue(), expected)
186+
168187
def test_inherit(self):
169-
out = Stream('mike-mkdocs.yml')
170188
main_cfg = 'INHERIT: mkdocs-base.yml\nplugins:\n foo: {}\n'
171189
base_cfg = 'plugins:\n bar: {}\n'
172190
files = {'mkdocs.yml': main_cfg, 'mkdocs-base.yml': base_cfg}
173191
with mock.patch('builtins.open', mock_open_files(files)), \
174192
mock.patch('mike.mkdocs_utils.NamedTemporaryFile',
175-
return_value=out), \
193+
return_value=self.out), \
176194
mock.patch('os.path.exists', return_value=True), \
177195
mock.patch('os.remove') as mremove:
178196
with mkdocs_utils.inject_plugin('mkdocs.yml') as f:
179197
self.assertEqual(f, 'mike-mkdocs.yml')
180-
newcfg = yaml.load(out.getvalue(), Loader=yaml.Loader)
198+
newcfg = yaml.safe_load(self.out.getvalue())
181199
mremove.assert_called_once()
182200

183201
self.assertEqual(newcfg, {'plugins': {

0 commit comments

Comments
 (0)