Skip to content

Commit 3dd0066

Browse files
committed
Revert "Cleanup: delete yaml_load_safely (#11285)"
This reverts commit 791643f.
1 parent 6747f91 commit 3dd0066

File tree

2 files changed

+88
-2
lines changed

2 files changed

+88
-2
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
from readthedocs.doc_builder.backends.mkdocs import ProxyPythonName, yaml_load_safely
2+
3+
content = """
4+
int: 3
5+
float: !!float 3
6+
function: !!python/name:python_function
7+
other_function: !!python/name:module.other.function
8+
unknown: !!python/module:python_module
9+
"""
10+
11+
12+
def test_yaml_load_safely():
13+
expected = {
14+
"int": 3,
15+
"float": 3.0,
16+
"function": ProxyPythonName("python_function"),
17+
"other_function": ProxyPythonName("module.other.function"),
18+
"unknown": None,
19+
}
20+
data = yaml_load_safely(content)
21+
22+
assert data == expected
23+
assert type(data["int"]) is int
24+
assert type(data["float"]) is float
25+
assert type(data["function"]) is ProxyPythonName
26+
assert type(data["other_function"]) is ProxyPythonName
27+
assert data["function"].value == "python_function"
28+
assert data["other_function"].value == "module.other.function"

readthedocs/doc_builder/backends/mkdocs.py

+60-2
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,7 @@ def get_final_doctype(self):
6363
allow_symlinks=True,
6464
base_path=self.project_path,
6565
) as fh:
66-
# Use ``.safe_load()`` since ``mkdocs.yml`` is an untrusted source.
67-
config = yaml.safe_load(fh)
66+
config = yaml_load_safely(fh)
6867
use_directory_urls = config.get("use_directory_urls", True)
6968
return MKDOCS if use_directory_urls else MKDOCS_HTML
7069

@@ -120,3 +119,62 @@ def build(self):
120119
class MkdocsHTML(BaseMkdocs):
121120
builder = "build"
122121
build_dir = "_readthedocs/html"
122+
123+
124+
class ProxyPythonName(yaml.YAMLObject):
125+
def __init__(self, value):
126+
self.value = value
127+
128+
def __eq__(self, other):
129+
return self.value == other.value
130+
131+
132+
class SafeLoader(yaml.SafeLoader): # pylint: disable=too-many-ancestors
133+
134+
"""
135+
Safe YAML loader.
136+
137+
This loader parses special ``!!python/name:`` tags without actually
138+
importing or executing code. Every other special tag is ignored.
139+
140+
Borrowed from https://stackoverflow.com/a/57121993
141+
Issue https://github.com/readthedocs/readthedocs.org/issues/7461
142+
"""
143+
144+
def ignore_unknown(self, node): # pylint: disable=unused-argument
145+
return None
146+
147+
def construct_python_name(self, suffix, node): # pylint: disable=unused-argument
148+
return ProxyPythonName(suffix)
149+
150+
151+
class SafeDumper(yaml.SafeDumper):
152+
153+
"""
154+
Safe YAML dumper.
155+
156+
This dumper allows to avoid losing values of special tags that
157+
were parsed by our safe loader.
158+
"""
159+
160+
def represent_name(self, data):
161+
return self.represent_scalar("tag:yaml.org,2002:python/name:" + data.value, "")
162+
163+
164+
SafeLoader.add_multi_constructor(
165+
"tag:yaml.org,2002:python/name:", SafeLoader.construct_python_name
166+
)
167+
SafeLoader.add_constructor(None, SafeLoader.ignore_unknown)
168+
SafeDumper.add_representer(ProxyPythonName, SafeDumper.represent_name)
169+
170+
171+
def yaml_load_safely(content):
172+
"""
173+
Uses ``SafeLoader`` loader to skip unknown tags.
174+
175+
When a YAML contains ``!!python/name:int`` it will store the ``int``
176+
suffix temporarily to be able to re-dump it later. We need this to avoid
177+
executing random code, but still support these YAML files without
178+
information loss.
179+
"""
180+
return yaml.load(content, Loader=SafeLoader)

0 commit comments

Comments
 (0)