Rewrite a script with syside.query
New in v0.11.0
This page is for a reader who already has Syside Automator scripts written
against the element classes: model.nodes, owned_elements.for_each,
heritage, .collect() on a LazyIterator, and
try_cast to narrow a type. query replaces the loops written
around those with one call each, and returns plain lists and values that need
no lock, no callback and no cast. The comparisons below show the functions that
change in two scripts from the Examples collection; everything else in those
scripts stays as it was.
What replaces what
Each pair below is one idiom written against the element classes, then the
syside.query call that replaces it.
Iterating the children of an element:
element.owned_elements.for_each(lambda child: handle(child))
for child in query.contents(element):
handle(child)
Finding a definition by name:
definition = None
for element in model.elements(syside.PartDefinition, include_subtypes=True):
if element.name == name:
definition = element
break
definition = query.find_by_name_and_type(model, name, syside.PartDefinition)
Finding the parts typed by a definition:
for part in model.nodes(syside.PartUsage):
for supertype in part.heritage.elements:
if (
supertype.try_cast(syside.PartDefinition)
and supertype.declared_name == "Wheel"
):
handle(part)
wheel = query.find_by_name_and_type(model, "Wheel", syside.PartDefinition)
for part in query.find_elements_of_type(model, syside.PartUsage):
if query.specializes(part, wheel):
handle(part)
Collecting the features an attribute redefines:
redefined = set()
for relationship in attribute.heritage.relationships:
if isinstance(relationship, syside.Redefinition):
redefined.add(relationship.first_target)
redefined = set(query.redefines(attribute))
Reading the attributes of a part:
attributes = [
feature
for feature in part.usages.collect()
if type(feature) is syside.AttributeUsage
]
attributes = [
feature
for feature in query.features_of(part)
if type(feature) is syside.AttributeUsage
]
Iterating the variants of a variation:
for variant in definition.variants.collect():
handle(variant)
for variant in query.variants(definition):
handle(variant)
Extract parts
The extract parts example prints the ownership tree and lists the parts of two definitions. Two of its functions change.
walk_ownership_tree recurses over the children of an element. The callback
passed to for_each becomes a for loop over contents:
def walk_ownership_tree(element: syside.Element, level: int = 0) -> None:
"""
Prints out all elements in a model in a tree-like format, where child
elements appear indented under their parent elements. For example:
Parent
Child1
Child2
Grandchild
Args:
element: The model element to start printing from
level: How many levels to indent (increases for nested elements)
"""
if element.name is not None:
print(" " * level, element.name)
else:
print(" " * level, "anonymous element")
# Recursively call walk_ownership_tree() for each owned element
# (child element).
element.owned_elements.for_each(
lambda owned_element: walk_ownership_tree(owned_element, level + 1)
)
def walk_ownership_tree(element: syside.Element, level: int = 0) -> None:
"""
Prints out all elements in a model in a tree-like format, where child
elements appear indented under their parent elements. For example:
Parent
Child1
Child2
Grandchild
Args:
element: The model element to start printing from
level: How many levels to indent (increases for nested elements)
"""
if element.name is not None:
print(" " * level, element.name)
else:
print(" " * level, "anonymous element")
# `contents` returns the owned elements as a plain list, so a regular
# `for` loop replaces the callback passed to `for_each`.
for owned_element in query.contents(element):
walk_ownership_tree(owned_element, level + 1)
show_parts_of_type lists the parts typed by a definition. Scanning each part’s
heritage for a definition with the right name becomes one lookup of the
definition and a specializes test per part,
which also finds parts typed by a subtype of it:
def show_parts_of_type(model: syside.Model, part_type: str) -> None:
for part in model.nodes(syside.PartUsage):
for element in part.heritage.elements:
if (
element.try_cast(syside.PartDefinition)
and element.declared_name == part_type
):
print("- ", part.name)
def show_parts_of_type(model: syside.Model, part_type: str) -> None:
# Look the definition up once, then ask each part whether it
# specializes it. `specializes` follows the whole heritage chain, so
# a part typed by a subtype of `part_type` is found as well.
definition = query.find_by_name_and_type(
model, part_type, syside.PartDefinition
)
assert definition is not None, f"no part definition named {part_type}"
for part in query.find_elements_of_type(model, syside.PartUsage):
if query.specializes(part, definition):
print("- ", part.name)
The whole rewritten script
import pathlib
import syside
from syside import query
EXAMPLE_DIR = pathlib.Path(__file__).parent
MODEL_FILE_PATH = EXAMPLE_DIR / "example_model.sysml"
def walk_ownership_tree(element: syside.Element, level: int = 0) -> None:
"""
Prints out all elements in a model in a tree-like format, where child
elements appear indented under their parent elements. For example:
Parent
Child1
Child2
Grandchild
Args:
element: The model element to start printing from
level: How many levels to indent (increases for nested elements)
"""
if element.name is not None:
print(" " * level, element.name)
else:
print(" " * level, "anonymous element")
# `contents` returns the owned elements as a plain list, so a regular
# `for` loop replaces the callback passed to `for_each`.
for owned_element in query.contents(element):
walk_ownership_tree(owned_element, level + 1)
def show_part_decomposition(
element: syside.Element, part_level: int = 0
) -> None:
"""
Prints out a hierarchical view of parts in a model, with indentation
showing parent-child relationships. The function calls itself repeatedly
to handle nested parts at deeper levels.
For example, if a car has an engine and wheels, it would print:
Car
Engine
Wheels
Args:
element: The model element to start printing from
part_level: How many levels of indentation to use (increases for
nested parts)
"""
if isinstance(element, syside.PartUsage):
print(" " * part_level, element.name)
new_part_level = part_level + 1
else:
new_part_level = part_level
for owned_element in query.contents(element):
show_part_decomposition(owned_element, new_part_level)
def show_parts_of_type(model: syside.Model, part_type: str) -> None:
# Look the definition up once, then ask each part whether it
# specializes it. `specializes` follows the whole heritage chain, so
# a part typed by a subtype of `part_type` is found as well.
definition = query.find_by_name_and_type(
model, part_type, syside.PartDefinition
)
assert definition is not None, f"no part definition named {part_type}"
for part in query.find_elements_of_type(model, syside.PartUsage):
if query.specializes(part, definition):
print("- ", part.name)
def main() -> None:
(model, diagnostics) = syside.load_model([MODEL_FILE_PATH])
# Only errors cause an exception. Syside may also report warnings and
# informational messages, but not for this example.
assert not diagnostics.contains_errors(warnings_as_errors=True)
print("\nWalk the ownership tree printing all elements.")
for doc in model.user_docs:
# Since Syside is a multi-threaded application, we need to lock the
# document to ensure that the document is not modified from another
# thread while we are accessing it.
with doc.lock() as locked:
walk_ownership_tree(locked.root_node)
print("\nShow part decomposition.")
for doc in model.user_docs:
with doc.lock() as locked:
show_part_decomposition(locked.root_node)
print("\nShow all electrical parts.")
show_parts_of_type(model, "Electrical")
print("\nShow all mechanical parts.")
show_parts_of_type(model, "Mechanical")
if __name__ == "__main__":
main()
Extract variants
The extract variants example finds a variation definition and prints each variant’s parts with their evaluated attributes, skipping attributes that a redefinition has replaced.
deduplicate_attributes collects the redefined attributes. Inspecting each
attribute’s heritage relationships becomes a call to redefines:
def deduplicate_attributes(
attributes: list[syside.AttributeUsage],
) -> list[syside.Feature]:
"""Removes attributes that have been redefined, keeping only the active versions.
Args:
attributes: List of attributes to deduplicate
"""
redefined = set()
for attribute in attributes:
for inherited_relationship in attribute.heritage.relationships:
if isinstance(inherited_relationship, syside.Redefinition):
redefined.add(inherited_relationship.first_target)
# Only keep attributes that are not redefined by anything
return [attr for attr in attributes if attr not in redefined]
def deduplicate_attributes(
attributes: list[syside.AttributeUsage],
) -> list[syside.Feature]:
"""Removes attributes that have been redefined, keeping only the active versions.
Args:
attributes: List of attributes to deduplicate
"""
# `redefines` lists the features an attribute redefines, so the
# redefined set is one comprehension instead of a scan of each
# attribute's heritage relationships.
redefined = {
redefined_attribute
for attribute in attributes
for redefined_attribute in query.redefines(attribute)
}
# Only keep attributes that are not redefined by anything
return [attr for attr in attributes if attr not in redefined]
main looks the variation definition up and iterates its variants. The
hand-written find_element_by_name (a scan of every element in the model,
followed by an isinstance check) becomes
find_by_name_and_type, and variants.collect()
becomes
variants:
def main() -> None:
# Load SysML model and get diagnostics (errors/warnings)
(model, _) = syside.load_model([MODEL_FILE_PATH], warnings_as_errors=True)
# Find the variation definition containing all configurations
available_configurations = find_element_by_name(
model, "AvailableConfigurations"
)
assert available_configurations is not None and isinstance(
available_configurations, syside.PartDefinition
)
# For each variant configuration, print its part hierarchy and attributes
for index, config in enumerate(available_configurations.variants.collect()):
if type(config) is syside.PartUsage:
print(
"\n",
"=" * 40,
f"\n CONFIGURATION #{index + 1}: {config.name}\n",
"=" * 40,
)
walk_ownership_tree(config)
print()
def main() -> None:
# Load SysML model and get diagnostics (errors/warnings)
(model, _) = syside.load_model([MODEL_FILE_PATH], warnings_as_errors=True)
# Find the variation definition containing all configurations. The
# lookup is by name and type at once, so no isinstance check follows.
available_configurations = query.find_by_name_and_type(
model, "AvailableConfigurations", syside.PartDefinition
)
assert available_configurations is not None
# For each variant configuration, print its part hierarchy and attributes
for index, config in enumerate(query.variants(available_configurations)):
if type(config) is syside.PartUsage:
print(
"\n",
"=" * 40,
f"\n CONFIGURATION #{index + 1}: {config.name}\n",
"=" * 40,
)
walk_ownership_tree(config)
print()
Evaluating an attribute’s value still goes through
Compiler.evaluate_feature:
syside.query reads the structure of a model and does not evaluate expressions.
The whole rewritten script
import pathlib
import sys
import syside
from syside import query
EXAMPLE_DIR = pathlib.Path(__file__).parent
MODEL_FILE_PATH = EXAMPLE_DIR / "example_model.sysml"
STANDARD_LIBRARY = syside.Environment.get_default().lib
def is_user_defined(element: syside.Element) -> bool:
"""True for elements written in the model, false for library elements."""
return element.document.document_tier is syside.DocumentTier.Project
def deduplicate_attributes(
attributes: list[syside.AttributeUsage],
) -> list[syside.Feature]:
"""Removes attributes that have been redefined, keeping only the active versions.
Args:
attributes: List of attributes to deduplicate
"""
# `redefines` lists the features an attribute redefines, so the
# redefined set is one comprehension instead of a scan of each
# attribute's heritage relationships.
redefined = {
redefined_attribute
for attribute in attributes
for redefined_attribute in query.redefines(attribute)
}
# Only keep attributes that are not redefined by anything
return [attr for attr in attributes if attr not in redefined]
def show_part_attributes(part: syside.PartUsage, level: int = 0) -> None:
"""Prints attributes and their evaluated values for a part.
Args:
part: The part whose attributes to display
level: Indentation level for hierarchical display
"""
# `features_of` returns owned and inherited features as a plain list.
# Filter to get only user-defined attributes (exclude standard library)
filtered_attributes = deduplicate_attributes(
[
feature
for feature in query.features_of(part)
if type(feature) is syside.AttributeUsage
and is_user_defined(feature)
]
)
for attribute in filtered_attributes:
# Evaluate the attribute value in the context of the part
value = evaluate_feature(attribute, part)
# For enum values, display just the name
if type(value) is syside.EnumerationUsage:
value = value.name
print(" " * level, f" └ Attribute: {attribute.name} = {value}")
def evaluate_feature(
feature: syside.Feature, scope: syside.Type
) -> syside.Value | None:
"""Evaluates a feature within a given scope.
Args:
feature: The feature to evaluate (attribute, part, etc.)
scope: The context in which to evaluate the feature
"""
compiler = syside.Compiler()
value, compilation_report = compiler.evaluate_feature(
feature=feature,
scope=scope,
stdlib=STANDARD_LIBRARY,
experimental_quantities=True,
)
if compilation_report.fatal:
print(compilation_report.diagnostics)
sys.exit(1)
return value
def walk_ownership_tree(element: syside.PartUsage, level: int = 0) -> None:
"""Recursively prints the part hierarchy with attributes in a tree format.
Args:
element: The part to display and traverse
level: Indentation level for hierarchical display
"""
# Skip printing root node name
if level > 0:
if element.name is not None:
print(" " * level, f"Part: {element.name}")
else:
print(" " * level, "Part: <anonymous>")
# Get attributes and their values
show_part_attributes(element, level)
# Filter child parts: exclude library elements
filtered_children = [
feature
for feature in query.features_of(element)
if type(feature) is syside.PartUsage and is_user_defined(feature)
]
# Recursively process each child part
for child in filtered_children:
walk_ownership_tree(child, level + 1)
def main() -> None:
# Load SysML model and get diagnostics (errors/warnings)
(model, _) = syside.load_model([MODEL_FILE_PATH], warnings_as_errors=True)
# Find the variation definition containing all configurations. The
# lookup is by name and type at once, so no isinstance check follows.
available_configurations = query.find_by_name_and_type(
model, "AvailableConfigurations", syside.PartDefinition
)
assert available_configurations is not None
# For each variant configuration, print its part hierarchy and attributes
for index, config in enumerate(query.variants(available_configurations)):
if type(config) is syside.PartUsage:
print(
"\n",
"=" * 40,
f"\n CONFIGURATION #{index + 1}: {config.name}\n",
"=" * 40,
)
walk_ownership_tree(config)
print()
if __name__ == "__main__":
main()
Each rewrite prints exactly the output shown on the example’s page.