Skip to content

Commit d89b5a3

Browse files
authored
Address #543 (ruamel.yaml changes) (#546)
* Address #543 (ruamel.yaml changes) * Don't complain about coverage tests of error handling in top-level parser
1 parent 66d78fe commit d89b5a3

File tree

4 files changed

+228
-3
lines changed

4 files changed

+228
-3
lines changed

pyproject.toml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,7 @@ dependencies = [
4949
"rich>=13.0.0", # databinder
5050
"aiofile", # compatible versions controlled through miniopy-async
5151
"make-it-sync", # compatible versions controlled through func_adl
52-
"ruamel.yaml>=0.18,<0.18.7", # FIXME: ruamel-yaml v0.18.7 has a breaking API change relative to v0.18.6
53-
"ccorp-yaml-include-relative-path>=0.0.4",
52+
"ruamel.yaml>=0.18.7",
5453
"filelock>=3.12.0",
5554
"tenacity >= 9.0.0"
5655
]

servicex/servicex_client.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ def _load_ServiceXSpec(
118118
file_path = config
119119

120120
import sys
121-
from ccorp.ruamel.yaml.include import YAML
121+
from .yaml_parser import YAML
122122

123123
yaml = YAML()
124124

servicex/yaml_parser/__init__.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# Copyright (c) 2024, IRIS-HEP
2+
# All rights reserved.
3+
#
4+
# Redistribution and use in source and binary forms, with or without
5+
# modification, are permitted provided that the following conditions are met:
6+
#
7+
# * Redistributions of source code must retain the above copyright notice, this
8+
# list of conditions and the following disclaimer.
9+
#
10+
# * Redistributions in binary form must reproduce the above copyright notice,
11+
# this list of conditions and the following disclaimer in the documentation
12+
# and/or other materials provided with the distribution.
13+
#
14+
# * Neither the name of the copyright holder nor the names of its
15+
# contributors may be used to endorse or promote products derived from
16+
# this software without specific prior written permission.
17+
#
18+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
19+
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
20+
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21+
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
22+
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
23+
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
24+
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
25+
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
26+
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
27+
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28+
29+
from .parser import YAML
30+
31+
__all__ = ["YAML"]

servicex/yaml_parser/parser.py

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
# Original code released under
2+
# The MIT License (MIT)
3+
#
4+
# Copyright (c) 2014-2018 Tristan Sweeney, Cambridge Consultants
5+
#
6+
# Permission is hereby granted, free of charge, to any person obtaining a copy
7+
# of this software and associated documentation files (the "Software"), to deal
8+
# in the Software without restriction, including without limitation the rights
9+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
# copies of the Software, and to permit persons to whom the Software is
11+
# furnished to do so, subject to the following conditions:
12+
#
13+
# The above copyright notice and this permission notice shall be included in
14+
# all copies or substantial portions of the Software.
15+
#
16+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22+
# SOFTWARE.
23+
#
24+
# The code has been modified:
25+
# Copyright (c) 2024, IRIS-HEP
26+
# All rights reserved.
27+
#
28+
# Redistribution and use in source and binary forms, with or without
29+
# modification, are permitted provided that the following conditions are met:
30+
#
31+
# * Redistributions of source code must retain the above copyright notice, this
32+
# list of conditions and the following disclaimer.
33+
#
34+
# * Redistributions in binary form must reproduce the above copyright notice,
35+
# this list of conditions and the following disclaimer in the documentation
36+
# and/or other materials provided with the distribution.
37+
#
38+
# * Neither the name of the copyright holder nor the names of its
39+
# contributors may be used to endorse or promote products derived from
40+
# this software without specific prior written permission.
41+
#
42+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
43+
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
44+
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
45+
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
46+
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
47+
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
48+
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
49+
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
50+
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
51+
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
52+
53+
import types
54+
from typing import Union, Any, Protocol
55+
from pathlib import Path
56+
import os
57+
58+
import ruamel.yaml
59+
import ruamel.yaml.composer
60+
import ruamel.yaml.constructor
61+
from ruamel.yaml.nodes import ScalarNode, MappingNode, SequenceNode
62+
63+
64+
class TextFileLike(Protocol):
65+
def read(self, size: int) -> Union[str, bytes]:
66+
"""read function for a file-like object"""
67+
68+
69+
class CompositingComposer(ruamel.yaml.composer.Composer):
70+
compositors = {k: {} for k in (ScalarNode, MappingNode, SequenceNode)}
71+
72+
@classmethod
73+
def add_compositor(cls, tag, compositor, *, nodeTypes=(ScalarNode,)):
74+
for nodeType in nodeTypes:
75+
cls.compositors[nodeType][tag] = compositor
76+
77+
@classmethod
78+
def get_compositor(cls, tag, nodeType):
79+
return cls.compositors[nodeType].get(tag, None)
80+
81+
def __compose_dispatch(self, anchor, nodeType, callback):
82+
event = self.parser.peek_event()
83+
compositor = self.get_compositor(event.tag, nodeType) or callback
84+
if isinstance(compositor, types.MethodType):
85+
return compositor(anchor)
86+
else:
87+
return compositor(self, anchor)
88+
89+
def compose_scalar_node(self, anchor):
90+
return self.__compose_dispatch(anchor, ScalarNode, super().compose_scalar_node)
91+
92+
def compose_sequence_node(self, anchor):
93+
return self.__compose_dispatch(
94+
anchor, SequenceNode, super().compose_sequence_node
95+
)
96+
97+
def compose_mapping_node(self, anchor):
98+
return self.__compose_dispatch(
99+
anchor, MappingNode, super().compose_mapping_node
100+
)
101+
102+
103+
class ExcludingConstructor(ruamel.yaml.constructor.Constructor):
104+
filters = {k: [] for k in (MappingNode, SequenceNode)}
105+
106+
@classmethod
107+
def add_filter(cls, filter, *, nodeTypes=(MappingNode,)):
108+
for nodeType in nodeTypes:
109+
cls.filters[nodeType].append(filter)
110+
111+
def construct_mapping(self, node):
112+
node.value = [
113+
(key_node, value_node)
114+
for key_node, value_node in node.value
115+
if not any(f(key_node, value_node) for f in self.filters[MappingNode])
116+
]
117+
return super().construct_mapping(node)
118+
119+
def construct_sequence(self, node, deep=True):
120+
node.value = [
121+
value_node
122+
for value_node in node.value
123+
if not any(f(value_node) for f in self.filters[SequenceNode])
124+
]
125+
return super().construct_sequence(node)
126+
127+
128+
class YAML(ruamel.yaml.YAML):
129+
def __init__(self, *args, **kwargs):
130+
if "typ" not in kwargs:
131+
kwargs["typ"] = "safe"
132+
elif kwargs["typ"] not in ("safe", "unsafe") and kwargs["typ"] not in (
133+
["safe"],
134+
["unsafe"],
135+
): # pragma: no cover
136+
raise Exception(
137+
"Can't do typ={} parsing w/ composition time directives!".format(
138+
kwargs["typ"]
139+
)
140+
)
141+
142+
if "pure" not in kwargs:
143+
kwargs["pure"] = True
144+
elif not kwargs["pure"]: # pragma: no cover
145+
raise Exception(
146+
"Can't do non-pure python parsing w/ composition time directives!"
147+
)
148+
149+
super().__init__(*args, **kwargs)
150+
self.Composer = CompositingComposer
151+
self.Constructor = ExcludingConstructor
152+
153+
def compose(self, stream: Union[Path, str, bytes, TextFileLike]) -> Any:
154+
"""
155+
at this point you either have the non-pure Parser (which has its own reader and
156+
scanner) or you have the pure Parser.
157+
If the pure Parser is set, then set the Reader and Scanner, if not already set.
158+
If either the Scanner or Reader are set, you cannot use the non-pure Parser,
159+
so reset it to the pure parser and set the Reader resp. Scanner if necessary
160+
"""
161+
constructor, parser = self.get_constructor_parser(stream)
162+
try:
163+
return self.composer.get_single_node()
164+
finally:
165+
parser.dispose()
166+
try:
167+
self._reader.reset_reader()
168+
except AttributeError: # pragma: no cover
169+
pass
170+
try:
171+
self._scanner.reset_scanner()
172+
except AttributeError: # pragma: no cover
173+
pass
174+
175+
def fork(self):
176+
return type(self)(typ=self.typ, pure=self.pure)
177+
178+
179+
def include_compositor(self, anchor):
180+
event = self.parser.get_event()
181+
yaml = self.loader.fork()
182+
path = os.path.join(os.path.dirname(self.loader.reader.name), event.value)
183+
with open(os.path.abspath(path)) as f:
184+
rv = yaml.compose(f)
185+
self.loader.composer.anchors.update(yaml.composer.anchors)
186+
return rv
187+
188+
189+
def exclude_filter(key_node, value_node=None):
190+
value_node = value_node or key_node # copy ref if None
191+
return key_node.tag == "!exclude" or value_node.tag == "!exclude"
192+
193+
194+
CompositingComposer.add_compositor("!include", include_compositor)
195+
ExcludingConstructor.add_filter(exclude_filter, nodeTypes=(MappingNode, SequenceNode))

0 commit comments

Comments
 (0)