Skip to content

Commit f4875a2

Browse files
pawamoyhumitos
andauthored
Don't lose python/name tags values in mkdocs.yml (#7507)
Co-authored-by: Manuel Kaufmann <[email protected]>
1 parent c6d9754 commit f4875a2

File tree

3 files changed

+82
-22
lines changed

3 files changed

+82
-22
lines changed
+26-8
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,38 @@
1-
import textwrap
2-
from readthedocs.doc_builder.backends.mkdocs import yaml_load_safely
1+
from readthedocs.doc_builder.backends.mkdocs import (
2+
yaml_dump_safely,
3+
yaml_load_safely,
4+
ProxyPythonName
5+
)
6+
7+
content = '''
8+
int: 3
9+
float: !!float 3
10+
function: !!python/name:python_function
11+
other_function: !!python/name:module.other.function
12+
unknown: !!python/module:python_module
13+
'''
314

415

516
def test_yaml_load_safely():
617
expected = {
718
'int': 3,
819
'float': 3.0,
20+
'function': ProxyPythonName('python_function'),
21+
'other_function': ProxyPythonName('module.other.function'),
922
'unknown': None,
1023
}
11-
12-
content = textwrap.dedent('''
13-
int: 3
14-
float: !!float 3
15-
unknown: !!python/name:unknown_funtion
16-
''')
1724
data = yaml_load_safely(content)
25+
1826
assert data == expected
1927
assert type(data['int']) is int
2028
assert type(data['float']) is float
29+
assert type(data['function']) is ProxyPythonName
30+
assert type(data['other_function']) is ProxyPythonName
31+
assert data['function'].value == 'python_function'
32+
assert data['other_function'].value == 'module.other.function'
33+
34+
35+
def test_yaml_dump_safely():
36+
data = yaml_load_safely(content)
37+
assert yaml_load_safely(yaml_dump_safely(data)) == data
38+

readthedocs/doc_builder/backends/mkdocs.py

+49-9
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,7 @@ def append_conf(self):
214214
user_config['theme'] = self.DEFAULT_THEME_NAME
215215

216216
# Write the modified mkdocs configuration
217-
yaml.safe_dump(
217+
yaml_dump_safely(
218218
user_config,
219219
open(self.yaml_file, 'w'),
220220
)
@@ -326,27 +326,67 @@ class MkdocsHTML(BaseMkdocs):
326326
build_dir = '_build/html'
327327

328328

329-
class SafeLoaderIgnoreUnknown(yaml.SafeLoader): # pylint: disable=too-many-ancestors
329+
# TODO: find a better way to integrate with MkDocs.
330+
# See https://github.com/readthedocs/readthedocs.org/issues/7844
331+
332+
333+
class ProxyPythonName(yaml.YAMLObject):
334+
def __init__(self, value):
335+
self.value = value
336+
337+
def __eq__(self, other):
338+
return self.value == other.value
339+
340+
341+
class SafeLoader(yaml.SafeLoader): # pylint: disable=too-many-ancestors
330342

331343
"""
332-
YAML loader to ignore unknown tags.
344+
Safe YAML loader.
345+
346+
This loader parses special ``!!python/name:`` tags without actually
347+
importing or executing code. Every other special tag is ignored.
333348
334349
Borrowed from https://stackoverflow.com/a/57121993
350+
Issue https://github.com/readthedocs/readthedocs.org/issues/7461
335351
"""
336352

337353
def ignore_unknown(self, node): # pylint: disable=no-self-use, unused-argument
338354
return None
339355

356+
def construct_python_name(self, suffix, node): # pylint: disable=no-self-use, unused-argument
357+
return ProxyPythonName(suffix)
358+
359+
360+
class SafeDumper(yaml.SafeDumper):
340361

341-
SafeLoaderIgnoreUnknown.add_constructor(None, SafeLoaderIgnoreUnknown.ignore_unknown)
362+
"""
363+
Safe YAML dumper.
364+
365+
This dumper allows to avoid losing values of special tags that
366+
were parsed by our safe loader.
367+
"""
368+
369+
def represent_name(self, data):
370+
return self.represent_scalar("tag:yaml.org,2002:python/name:" + data.value, "")
371+
372+
373+
SafeLoader.add_multi_constructor("tag:yaml.org,2002:python/name:", SafeLoader.construct_python_name)
374+
SafeLoader.add_constructor(None, SafeLoader.ignore_unknown)
375+
SafeDumper.add_representer(ProxyPythonName, SafeDumper.represent_name)
342376

343377

344378
def yaml_load_safely(content):
345379
"""
346-
Uses ``SafeLoaderIgnoreUnknown`` loader to skip unknown tags.
380+
Uses ``SafeLoader`` loader to skip unknown tags.
347381
348-
When a YAML contains ``!!python/name:int`` it will complete ignore it an
349-
return ``None`` for those fields instead of failing. We need this to avoid
350-
executing random code, but still support these YAML files.
382+
When a YAML contains ``!!python/name:int`` it will store the ``int``
383+
suffix temporarily to be able to re-dump it later. We need this to avoid
384+
executing random code, but still support these YAML files without
385+
information loss.
351386
"""
352-
return yaml.load(content, Loader=SafeLoaderIgnoreUnknown)
387+
return yaml.load(content, Loader=SafeLoader)
388+
389+
390+
def yaml_dump_safely(content, stream=None):
391+
"""Uses ``SafeDumper`` dumper to write YAML contents."""
392+
return yaml.dump(content, stream=stream, Dumper=SafeDumper)

readthedocs/rtd_tests/tests/test_doc_builder.py

+7-5
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313

1414
from readthedocs.builds.constants import EXTERNAL
1515
from readthedocs.builds.models import Version
16-
from readthedocs.doc_builder.backends.mkdocs import MkdocsHTML, yaml_load_safely
16+
from readthedocs.doc_builder.backends.mkdocs import MkdocsHTML, SafeDumper, yaml_load_safely
1717
from readthedocs.doc_builder.backends.sphinx import (
1818
BaseSphinx,
1919
HtmlBuilder,
@@ -392,7 +392,7 @@ def test_get_theme_name_with_feature_flag(self, checkout_path, run):
392392
}
393393
builder.append_conf()
394394

395-
mock_yaml.safe_dump.assert_called_once_with(
395+
mock_yaml.dump.assert_called_once_with(
396396
{
397397
'site_name': mock.ANY,
398398
'docs_dir': mock.ANY,
@@ -401,7 +401,8 @@ def test_get_theme_name_with_feature_flag(self, checkout_path, run):
401401
'google_analytics': mock.ANY,
402402
'theme': 'readthedocs',
403403
},
404-
mock.ANY,
404+
stream=mock.ANY,
405+
Dumper=SafeDumper,
405406
)
406407
mock_yaml.reset_mock()
407408

@@ -417,7 +418,7 @@ def test_get_theme_name_with_feature_flag(self, checkout_path, run):
417418
}
418419
builder.append_conf()
419420

420-
mock_yaml.safe_dump.assert_called_once_with(
421+
mock_yaml.dump.assert_called_once_with(
421422
{
422423
'site_name': mock.ANY,
423424
'docs_dir': mock.ANY,
@@ -426,7 +427,8 @@ def test_get_theme_name_with_feature_flag(self, checkout_path, run):
426427
'google_analytics': mock.ANY,
427428
'theme': 'customtheme',
428429
},
429-
mock.ANY,
430+
stream=mock.ANY,
431+
Dumper=SafeDumper,
430432
)
431433

432434
@patch('readthedocs.doc_builder.base.BaseBuilder.run')

0 commit comments

Comments
 (0)