Benjamin Schubert pushed to branch bschubert/rework-circular-check at BuildStream / buildstream
Commits:
-
a39c4767
by Daniel Silverstone at 2019-02-12T15:12:30Z
-
cda03313
by Daniel Silverstone at 2019-02-12T15:12:30Z
-
b6f08e1b
by Daniel Silverstone at 2019-02-12T15:12:30Z
-
69ee11c6
by Daniel Silverstone at 2019-02-12T15:12:30Z
-
0928e570
by Daniel Silverstone at 2019-02-12T15:12:30Z
-
022a59f0
by Benjamin Schubert at 2019-02-12T16:14:05Z
-
ae1663f4
by Benjamin Schubert at 2019-02-12T17:00:18Z
-
0c5c450e
by Benjamin Schubert at 2019-02-12T17:00:18Z
5 changed files:
- buildstream/_context.py
- buildstream/_loader/loadelement.py
- buildstream/_loader/loader.py
- buildstream/_profile.py
- buildstream/_yaml.py
Changes:
... | ... | @@ -361,14 +361,17 @@ class Context(): |
361 | 361 |
# (bool): Whether or not to use strict build plan
|
362 | 362 |
#
|
363 | 363 |
def get_strict(self):
|
364 |
+ if self._strict_build_plan is None:
|
|
365 |
+ # Either we're not overridden or we've never worked it out before
|
|
366 |
+ # so work out if we should be strict, and then cache the result
|
|
367 |
+ toplevel = self.get_toplevel_project()
|
|
368 |
+ overrides = self.get_overrides(toplevel.name)
|
|
369 |
+ self._strict_build_plan = _yaml.node_get(overrides, bool, 'strict', default_value=True)
|
|
364 | 370 |
|
365 | 371 |
# If it was set by the CLI, it overrides any config
|
366 |
- if self._strict_build_plan is not None:
|
|
367 |
- return self._strict_build_plan
|
|
368 |
- |
|
369 |
- toplevel = self.get_toplevel_project()
|
|
370 |
- overrides = self.get_overrides(toplevel.name)
|
|
371 |
- return _yaml.node_get(overrides, bool, 'strict', default_value=True)
|
|
372 |
+ # Ditto if we've already computed this, then we return the computed
|
|
373 |
+ # value which we cache here too.
|
|
374 |
+ return self._strict_build_plan
|
|
372 | 375 |
|
373 | 376 |
# get_cache_key():
|
374 | 377 |
#
|
... | ... | @@ -67,14 +67,14 @@ class LoadElement(): |
67 | 67 |
self.node = node # The YAML node
|
68 | 68 |
self.name = filename # The element name
|
69 | 69 |
self.full_name = None # The element full name (with associated junction)
|
70 |
- self.deps = None # The list of Dependency objects
|
|
71 |
- self.node_id = next(self._counter)
|
|
70 |
+ self.dependencies = []
|
|
72 | 71 |
|
73 | 72 |
#
|
74 | 73 |
# Private members
|
75 | 74 |
#
|
76 | 75 |
self._loader = loader # The Loader object
|
77 |
- self._dep_cache = None # The dependency cache, to speed up depends()
|
|
76 |
+ self._dep_cache = BitMap() # The dependency cache, to speed up depends()
|
|
77 |
+ self._node_id = next(self._counter)
|
|
78 | 78 |
|
79 | 79 |
#
|
80 | 80 |
# Initialization
|
... | ... | @@ -94,12 +94,27 @@ class LoadElement(): |
94 | 94 |
'build-depends', 'runtime-depends',
|
95 | 95 |
])
|
96 | 96 |
|
97 |
- self.dependencies = []
|
|
98 |
- |
|
99 | 97 |
@property
|
100 | 98 |
def junction(self):
|
101 | 99 |
return self._loader.project.junction
|
102 | 100 |
|
101 |
+ # _add_dependency()
|
|
102 |
+ #
|
|
103 |
+ # Add an element as a dependency of the current element
|
|
104 |
+ #
|
|
105 |
+ # Args:
|
|
106 |
+ # dependency (LoadElement): element to add as a dependency
|
|
107 |
+ # dep_type (str): type of dependency to add
|
|
108 |
+ #
|
|
109 |
+ # Raises:
|
|
110 |
+ # LoadError: if adding the current dependency would create a circular
|
|
111 |
+ # dependency
|
|
112 |
+ #
|
|
113 |
+ def _add_dependency(self, dependency, dep_type):
|
|
114 |
+ self.dependencies.append(LoadElement.Dependency(dependency, dep_type))
|
|
115 |
+ self._dep_cache.update(dependency._dep_cache)
|
|
116 |
+ self._dep_cache.add(dependency._node_id)
|
|
117 |
+ |
|
103 | 118 |
# depends():
|
104 | 119 |
#
|
105 | 120 |
# Checks if this element depends on another element, directly
|
... | ... | @@ -112,32 +127,59 @@ class LoadElement(): |
112 | 127 |
# (bool): True if this LoadElement depends on 'other'
|
113 | 128 |
#
|
114 | 129 |
def depends(self, other):
|
115 |
- self._ensure_depends_cache()
|
|
116 |
- return other.node_id in self._dep_cache
|
|
130 |
+ return other._node_id in self._dep_cache
|
|
117 | 131 |
|
118 |
- ###########################################
|
|
119 |
- # Private Methods #
|
|
120 |
- ###########################################
|
|
121 |
- def _ensure_depends_cache(self):
|
|
132 |
+ # _ensure_no_circular_deps()
|
|
133 |
+ #
|
|
134 |
+ # Ensure the node is not part of a dependency cycle
|
|
135 |
+ #
|
|
136 |
+ # Raises:
|
|
137 |
+ # (LoadError): In case there was a circular dependency
|
|
138 |
+ #
|
|
139 |
+ def _ensure_no_circular_deps(self):
|
|
140 |
+ if self._node_id in self._dep_cache:
|
|
141 |
+ self._find_circular_deps(set(), set(), [])
|
|
122 | 142 |
|
123 |
- if self._dep_cache:
|
|
143 |
+ # _find_circular_deps():
|
|
144 |
+ #
|
|
145 |
+ # Detect circular dependencies on LoadElements with
|
|
146 |
+ # dependencies already resolved.
|
|
147 |
+ #
|
|
148 |
+ # Args:
|
|
149 |
+ # check_elements (set[LoadElement]): The elements in the chain being checked
|
|
150 |
+ # validated (set[LoadElement]): The elements already validated to not have
|
|
151 |
+ # circular dependencies
|
|
152 |
+ # sequence (list[Element]): current sequence of elements that is checked
|
|
153 |
+ #
|
|
154 |
+ # Raises:
|
|
155 |
+ # (LoadError): In case there was a circular dependency error
|
|
156 |
+ #
|
|
157 |
+ def _find_circular_deps(self, check_elements, validated, sequence):
|
|
158 |
+ # Skip already validated branches
|
|
159 |
+ if self in validated:
|
|
124 | 160 |
return
|
125 | 161 |
|
126 |
- self._dep_cache = BitMap()
|
|
127 |
- |
|
162 |
+ if self in check_elements:
|
|
163 |
+ # Create `chain`, the loop of element dependencies from this
|
|
164 |
+ # element back to itself, by trimming everything before this
|
|
165 |
+ # element from the sequence under consideration.
|
|
166 |
+ chain = sequence[sequence.index(self.full_name):]
|
|
167 |
+ chain.append(self.full_name)
|
|
168 |
+ raise LoadError(LoadErrorReason.CIRCULAR_DEPENDENCY,
|
|
169 |
+ ("Circular dependency detected at element: {}\n" +
|
|
170 |
+ "Dependency chain: {}")
|
|
171 |
+ .format(self.full_name, " -> ".join(chain)))
|
|
172 |
+ |
|
173 |
+ # Push / Check each dependency / Pop
|
|
174 |
+ check_elements.add(self)
|
|
175 |
+ sequence.append(self.full_name)
|
|
128 | 176 |
for dep in self.dependencies:
|
129 |
- elt = dep.element
|
|
130 |
- |
|
131 |
- # Ensure the cache of the element we depend on
|
|
132 |
- elt._ensure_depends_cache()
|
|
133 |
- |
|
134 |
- # We depend on this element
|
|
135 |
- self._dep_cache.add(elt.node_id)
|
|
136 |
- |
|
137 |
- # And we depend on everything this element depends on
|
|
138 |
- self._dep_cache.update(elt._dep_cache)
|
|
177 |
+ dep.element._check_circular_deps(check_elements, validated, sequence)
|
|
178 |
+ check_elements.remove(self)
|
|
179 |
+ sequence.pop()
|
|
139 | 180 |
|
140 |
- self._dep_cache = FrozenBitMap(self._dep_cache)
|
|
181 |
+ # Eliminate duplicate paths
|
|
182 |
+ validated.add(self)
|
|
141 | 183 |
|
142 | 184 |
|
143 | 185 |
# _extract_depends_from_node():
|
... | ... | @@ -127,23 +127,6 @@ class Loader(): |
127 | 127 |
target_elements.append(element)
|
128 | 128 |
profile_end(Topics.LOAD_PROJECT, target)
|
129 | 129 |
|
130 |
- #
|
|
131 |
- # Now that we've resolve the dependencies, scan them for circular dependencies
|
|
132 |
- #
|
|
133 |
- |
|
134 |
- # Set up a dummy element that depends on all top-level targets
|
|
135 |
- # to resolve potential circular dependencies between them
|
|
136 |
- dummy_target = LoadElement("", "", self)
|
|
137 |
- dummy_target.dependencies.extend(
|
|
138 |
- LoadElement.Dependency(element, Symbol.RUNTIME)
|
|
139 |
- for element in target_elements
|
|
140 |
- )
|
|
141 |
- |
|
142 |
- profile_key = "_".join(t for t in targets)
|
|
143 |
- profile_start(Topics.CIRCULAR_CHECK, profile_key)
|
|
144 |
- self._check_circular_deps(dummy_target)
|
|
145 |
- profile_end(Topics.CIRCULAR_CHECK, profile_key)
|
|
146 |
- |
|
147 | 130 |
ret = []
|
148 | 131 |
#
|
149 | 132 |
# Sort direct dependencies of elements by their dependency ordering
|
... | ... | @@ -273,58 +256,14 @@ class Loader(): |
273 | 256 |
"{}: Cannot depend on junction"
|
274 | 257 |
.format(dep.provenance))
|
275 | 258 |
|
276 |
- element.dependencies.append(LoadElement.Dependency(dep_element, dep.dep_type))
|
|
259 |
+ element._add_dependency(dep_element, dep.dep_type)
|
|
277 | 260 |
|
278 | 261 |
deps_names = [dep.name for dep in dependencies]
|
279 | 262 |
self._warn_invalid_elements(deps_names)
|
280 | 263 |
|
281 |
- return element
|
|
264 |
+ element._ensure_no_circular_deps()
|
|
282 | 265 |
|
283 |
- # _check_circular_deps():
|
|
284 |
- #
|
|
285 |
- # Detect circular dependencies on LoadElements with
|
|
286 |
- # dependencies already resolved.
|
|
287 |
- #
|
|
288 |
- # Args:
|
|
289 |
- # element (str): The element to check
|
|
290 |
- #
|
|
291 |
- # Raises:
|
|
292 |
- # (LoadError): In case there was a circular dependency error
|
|
293 |
- #
|
|
294 |
- def _check_circular_deps(self, element, check_elements=None, validated=None, sequence=None):
|
|
295 |
- |
|
296 |
- if check_elements is None:
|
|
297 |
- check_elements = {}
|
|
298 |
- if validated is None:
|
|
299 |
- validated = {}
|
|
300 |
- if sequence is None:
|
|
301 |
- sequence = []
|
|
302 |
- |
|
303 |
- # Skip already validated branches
|
|
304 |
- if validated.get(element) is not None:
|
|
305 |
- return
|
|
306 |
- |
|
307 |
- if check_elements.get(element) is not None:
|
|
308 |
- # Create `chain`, the loop of element dependencies from this
|
|
309 |
- # element back to itself, by trimming everything before this
|
|
310 |
- # element from the sequence under consideration.
|
|
311 |
- chain = sequence[sequence.index(element.full_name):]
|
|
312 |
- chain.append(element.full_name)
|
|
313 |
- raise LoadError(LoadErrorReason.CIRCULAR_DEPENDENCY,
|
|
314 |
- ("Circular dependency detected at element: {}\n" +
|
|
315 |
- "Dependency chain: {}")
|
|
316 |
- .format(element.full_name, " -> ".join(chain)))
|
|
317 |
- |
|
318 |
- # Push / Check each dependency / Pop
|
|
319 |
- check_elements[element] = True
|
|
320 |
- sequence.append(element.full_name)
|
|
321 |
- for dep in element.dependencies:
|
|
322 |
- dep.element._loader._check_circular_deps(dep.element, check_elements, validated, sequence)
|
|
323 |
- del check_elements[element]
|
|
324 |
- sequence.pop()
|
|
325 |
- |
|
326 |
- # Eliminate duplicate paths
|
|
327 |
- validated[element] = True
|
|
266 |
+ return element
|
|
328 | 267 |
|
329 | 268 |
# _sort_dependencies():
|
330 | 269 |
#
|
... | ... | @@ -42,7 +42,6 @@ initialized = False |
42 | 42 |
#
|
43 | 43 |
# The special 'all' value will enable all profiles.
|
44 | 44 |
class Topics():
|
45 |
- CIRCULAR_CHECK = 'circ-dep-check'
|
|
46 | 45 |
SORT_DEPENDENCIES = 'sort-deps'
|
47 | 46 |
LOAD_LOADER = 'load-loader'
|
48 | 47 |
LOAD_CONTEXT = 'load-context'
|
... | ... | @@ -365,8 +365,8 @@ _sentinel = object() |
365 | 365 |
#
|
366 | 366 |
def node_get(node, expected_type, key, indices=None, *, default_value=_sentinel, allow_none=False):
|
367 | 367 |
value = node.get(key, default_value)
|
368 |
- provenance = node_get_provenance(node)
|
|
369 | 368 |
if value is _sentinel:
|
369 |
+ provenance = node_get_provenance(node)
|
|
370 | 370 |
raise LoadError(LoadErrorReason.INVALID_DATA,
|
371 | 371 |
"{}: Dictionary did not contain expected key '{}'".format(provenance, key))
|
372 | 372 |
|
... | ... | @@ -914,6 +914,10 @@ RoundTripRepresenter.add_representer(SanitizedDict, |
914 | 914 |
SafeRepresenter.represent_dict)
|
915 | 915 |
|
916 | 916 |
|
917 |
+# Types we can short-circuit in node_sanitize for speed.
|
|
918 |
+__SANITIZE_SHORT_CIRCUIT_TYPES = (int, float, str, bool, tuple)
|
|
919 |
+ |
|
920 |
+ |
|
917 | 921 |
# node_sanitize()
|
918 | 922 |
#
|
919 | 923 |
# Returnes an alphabetically ordered recursive copy
|
... | ... | @@ -922,9 +926,21 @@ RoundTripRepresenter.add_representer(SanitizedDict, |
922 | 926 |
# Only dicts are ordered, list elements are left in order.
|
923 | 927 |
#
|
924 | 928 |
def node_sanitize(node):
|
929 |
+ # Short-circuit None which occurs ca. twice per element
|
|
930 |
+ if node is None:
|
|
931 |
+ return node
|
|
932 |
+ |
|
933 |
+ node_type = type(node)
|
|
934 |
+ # Next short-circuit integers, floats, strings, booleans, and tuples
|
|
935 |
+ if node_type in __SANITIZE_SHORT_CIRCUIT_TYPES:
|
|
936 |
+ return node
|
|
937 |
+ # Now short-circuit lists. Note this is only for the raw list
|
|
938 |
+ # type, CommentedSeq and others get caught later.
|
|
939 |
+ elif node_type is list:
|
|
940 |
+ return [node_sanitize(elt) for elt in node]
|
|
925 | 941 |
|
926 |
- if isinstance(node, collections.abc.Mapping):
|
|
927 |
- |
|
942 |
+ # Finally ChainMap and dict, and other Mappings need special handling
|
|
943 |
+ if node_type in (dict, ChainMap) or isinstance(node, collections.Mapping):
|
|
928 | 944 |
result = SanitizedDict()
|
929 | 945 |
|
930 | 946 |
key_list = [key for key, _ in node_items(node)]
|
... | ... | @@ -932,10 +948,12 @@ def node_sanitize(node): |
932 | 948 |
result[key] = node_sanitize(node[key])
|
933 | 949 |
|
934 | 950 |
return result
|
935 |
- |
|
951 |
+ # Catch the case of CommentedSeq and friends. This is more rare and so
|
|
952 |
+ # we keep complexity down by still using isinstance here.
|
|
936 | 953 |
elif isinstance(node, list):
|
937 | 954 |
return [node_sanitize(elt) for elt in node]
|
938 | 955 |
|
956 |
+ # Everything else (such as commented scalars) just gets returned as-is.
|
|
939 | 957 |
return node
|
940 | 958 |
|
941 | 959 |
|
... | ... | @@ -1064,15 +1082,52 @@ class ChainMap(collections.ChainMap): |
1064 | 1082 |
return default
|
1065 | 1083 |
|
1066 | 1084 |
|
1085 |
+# Node copying
|
|
1086 |
+#
|
|
1087 |
+# Unfortunately we copy nodes a *lot* and `isinstance()` is super-slow when
|
|
1088 |
+# things from collections.abc get involved. The result is the following
|
|
1089 |
+# intricate but substantially faster group of tuples and the use of `in`.
|
|
1090 |
+#
|
|
1091 |
+# If any of the {node,list}_{chain_,}_copy routines raise a ValueError
|
|
1092 |
+# then it's likely additional types need adding to these tuples.
|
|
1093 |
+ |
|
1094 |
+# When chaining a copy, these types are skipped since the ChainMap will
|
|
1095 |
+# retrieve them from the source node when needed. Other copiers might copy
|
|
1096 |
+# them, so we call them __QUICK_TYPES.
|
|
1097 |
+__QUICK_TYPES = (str, bool,
|
|
1098 |
+ yaml.scalarstring.PreservedScalarString,
|
|
1099 |
+ yaml.scalarstring.SingleQuotedScalarString,
|
|
1100 |
+ yaml.scalarstring.DoubleQuotedScalarString)
|
|
1101 |
+ |
|
1102 |
+# These types have to be iterated like a dictionary
|
|
1103 |
+__DICT_TYPES = (dict, ChainMap, yaml.comments.CommentedMap)
|
|
1104 |
+ |
|
1105 |
+# These types have to be iterated like a list
|
|
1106 |
+__LIST_TYPES = (list, yaml.comments.CommentedSeq)
|
|
1107 |
+ |
|
1108 |
+# These are the provenance types, which have to be cloned rather than any other
|
|
1109 |
+# copying tactic.
|
|
1110 |
+__PROVENANCE_TYPES = (Provenance, DictProvenance, MemberProvenance, ElementProvenance)
|
|
1111 |
+ |
|
1112 |
+# These are the directives used to compose lists, we need this because it's
|
|
1113 |
+# slightly faster during the node_final_assertions checks
|
|
1114 |
+__NODE_ASSERT_COMPOSITION_DIRECTIVES = ('(>)', '(<)', '(=)')
|
|
1115 |
+ |
|
1116 |
+ |
|
1067 | 1117 |
def node_chain_copy(source):
|
1068 | 1118 |
copy = ChainMap({}, source)
|
1069 | 1119 |
for key, value in source.items():
|
1070 |
- if isinstance(value, collections.abc.Mapping):
|
|
1120 |
+ value_type = type(value)
|
|
1121 |
+ if value_type in __DICT_TYPES:
|
|
1071 | 1122 |
copy[key] = node_chain_copy(value)
|
1072 |
- elif isinstance(value, list):
|
|
1123 |
+ elif value_type in __LIST_TYPES:
|
|
1073 | 1124 |
copy[key] = list_chain_copy(value)
|
1074 |
- elif isinstance(value, Provenance):
|
|
1125 |
+ elif value_type in __PROVENANCE_TYPES:
|
|
1075 | 1126 |
copy[key] = value.clone()
|
1127 |
+ elif value_type in __QUICK_TYPES:
|
|
1128 |
+ pass # No need to copy these, the chainmap deals with it
|
|
1129 |
+ else:
|
|
1130 |
+ raise ValueError("Unable to be quick about node_chain_copy of {}".format(value_type))
|
|
1076 | 1131 |
|
1077 | 1132 |
return copy
|
1078 | 1133 |
|
... | ... | @@ -1080,14 +1135,17 @@ def node_chain_copy(source): |
1080 | 1135 |
def list_chain_copy(source):
|
1081 | 1136 |
copy = []
|
1082 | 1137 |
for item in source:
|
1083 |
- if isinstance(item, collections.abc.Mapping):
|
|
1138 |
+ item_type = type(item)
|
|
1139 |
+ if item_type in __DICT_TYPES:
|
|
1084 | 1140 |
copy.append(node_chain_copy(item))
|
1085 |
- elif isinstance(item, list):
|
|
1141 |
+ elif item_type in __LIST_TYPES:
|
|
1086 | 1142 |
copy.append(list_chain_copy(item))
|
1087 |
- elif isinstance(item, Provenance):
|
|
1143 |
+ elif item_type in __PROVENANCE_TYPES:
|
|
1088 | 1144 |
copy.append(item.clone())
|
1089 |
- else:
|
|
1145 |
+ elif item_type in __QUICK_TYPES:
|
|
1090 | 1146 |
copy.append(item)
|
1147 |
+ else: # Fallback
|
|
1148 |
+ raise ValueError("Unable to be quick about list_chain_copy of {}".format(item_type))
|
|
1091 | 1149 |
|
1092 | 1150 |
return copy
|
1093 | 1151 |
|
... | ... | @@ -1095,14 +1153,17 @@ def list_chain_copy(source): |
1095 | 1153 |
def node_copy(source):
|
1096 | 1154 |
copy = {}
|
1097 | 1155 |
for key, value in source.items():
|
1098 |
- if isinstance(value, collections.abc.Mapping):
|
|
1156 |
+ value_type = type(value)
|
|
1157 |
+ if value_type in __DICT_TYPES:
|
|
1099 | 1158 |
copy[key] = node_copy(value)
|
1100 |
- elif isinstance(value, list):
|
|
1159 |
+ elif value_type in __LIST_TYPES:
|
|
1101 | 1160 |
copy[key] = list_copy(value)
|
1102 |
- elif isinstance(value, Provenance):
|
|
1161 |
+ elif value_type in __PROVENANCE_TYPES:
|
|
1103 | 1162 |
copy[key] = value.clone()
|
1104 |
- else:
|
|
1163 |
+ elif value_type in __QUICK_TYPES:
|
|
1105 | 1164 |
copy[key] = value
|
1165 |
+ else:
|
|
1166 |
+ raise ValueError("Unable to be quick about node_copy of {}".format(value_type))
|
|
1106 | 1167 |
|
1107 | 1168 |
ensure_provenance(copy)
|
1108 | 1169 |
|
... | ... | @@ -1112,14 +1173,17 @@ def node_copy(source): |
1112 | 1173 |
def list_copy(source):
|
1113 | 1174 |
copy = []
|
1114 | 1175 |
for item in source:
|
1115 |
- if isinstance(item, collections.abc.Mapping):
|
|
1176 |
+ item_type = type(item)
|
|
1177 |
+ if item_type in __DICT_TYPES:
|
|
1116 | 1178 |
copy.append(node_copy(item))
|
1117 |
- elif isinstance(item, list):
|
|
1179 |
+ elif item_type in __LIST_TYPES:
|
|
1118 | 1180 |
copy.append(list_copy(item))
|
1119 |
- elif isinstance(item, Provenance):
|
|
1181 |
+ elif item_type in __PROVENANCE_TYPES:
|
|
1120 | 1182 |
copy.append(item.clone())
|
1121 |
- else:
|
|
1183 |
+ elif item_type in __QUICK_TYPES:
|
|
1122 | 1184 |
copy.append(item)
|
1185 |
+ else:
|
|
1186 |
+ raise ValueError("Unable to be quick about list_copy of {}".format(item_type))
|
|
1123 | 1187 |
|
1124 | 1188 |
return copy
|
1125 | 1189 |
|
... | ... | @@ -1142,22 +1206,26 @@ def node_final_assertions(node): |
1142 | 1206 |
# indicates that the user intended to override a list which
|
1143 | 1207 |
# never existed in the underlying data
|
1144 | 1208 |
#
|
1145 |
- if key in ['(>)', '(<)', '(=)']:
|
|
1209 |
+ if key in __NODE_ASSERT_COMPOSITION_DIRECTIVES:
|
|
1146 | 1210 |
provenance = node_get_provenance(node, key)
|
1147 | 1211 |
raise LoadError(LoadErrorReason.TRAILING_LIST_DIRECTIVE,
|
1148 | 1212 |
"{}: Attempt to override non-existing list".format(provenance))
|
1149 | 1213 |
|
1150 |
- if isinstance(value, collections.abc.Mapping):
|
|
1214 |
+ value_type = type(value)
|
|
1215 |
+ |
|
1216 |
+ if value_type in __DICT_TYPES:
|
|
1151 | 1217 |
node_final_assertions(value)
|
1152 |
- elif isinstance(value, list):
|
|
1218 |
+ elif value_type in __LIST_TYPES:
|
|
1153 | 1219 |
list_final_assertions(value)
|
1154 | 1220 |
|
1155 | 1221 |
|
1156 | 1222 |
def list_final_assertions(values):
|
1157 | 1223 |
for value in values:
|
1158 |
- if isinstance(value, collections.abc.Mapping):
|
|
1224 |
+ value_type = type(value)
|
|
1225 |
+ |
|
1226 |
+ if value_type in __DICT_TYPES:
|
|
1159 | 1227 |
node_final_assertions(value)
|
1160 |
- elif isinstance(value, list):
|
|
1228 |
+ elif value_type in __LIST_TYPES:
|
|
1161 | 1229 |
list_final_assertions(value)
|
1162 | 1230 |
|
1163 | 1231 |
|