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 |
|
