Rewrite a script with syside.query
New in v0.11.0
This page is for a reader with a working Syside Automator script who wants to
see what syside.query changes before touching it. Two scripts from the
Examples collection are shown twice: the published version, and a rewrite that
prints the same output using syside.query. Click the tab above each block to
switch between them.
What replaces what
Written against the element classes |
With |
|---|---|
|
|
|
|
|
|
|
|
scan |
|
scan |
|
|
|
|
|
walk |
|
The right-hand column returns lists and scalars, so the isinstance checks,
.collect() calls and try_cast guards that surround the left-hand forms
disappear with them.
Extract parts
The extract parts example walks the ownership
tree, prints the part decomposition, and lists the parts of two definitions.
In the rewrite, for_each with a lambda becomes a for loop over
contents, and the type scan in
show_parts_of_type becomes one lookup of the definition followed by a
specializes test per part.
import pathlib
import syside
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")
# 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 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 element.try_cast(syside.PartUsage): # Check if element is a part usage
print(" " * part_level, element.name)
new_part_level = part_level + 1
else:
new_part_level = part_level
# Recursively call show_part_decomposition() for each owned element
# (child element).
element.owned_elements.for_each(
lambda owned_element: show_part_decomposition(
owned_element, new_part_level
)
)
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 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()
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, then prints each variant’s part hierarchy with evaluated
attribute values, dropping attributes that a redefinition has replaced. The
rewrite drops the hand-written find_element_by_name in favour of
find_by_name_and_type, reads
inherited and owned features with features_of, and builds the redefined set from
redefines instead of inspecting heritage
relationships. Evaluating a value still goes through
Compiler.evaluate_feature, since
syside.query reads structure and does not evaluate expressions.
import pathlib
import sys
import syside
EXAMPLE_DIR = pathlib.Path(__file__).parent
MODEL_FILE_PATH = EXAMPLE_DIR / "example_model.sysml"
STANDARD_LIBRARY = syside.Environment.get_default().lib
def find_element_by_name(
model: syside.Model, name: str
) -> syside.Element | None:
"""Search the model for a specific element by name."""
# Iterates through all model elements that subset Element type
# e.g. PartUsage, ItemUsage, OccurrenceUsage, etc.
for element in model.elements(syside.Element, include_subtypes=True):
if element.name == name:
return element
return None
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 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
"""
# Filter to get only user-defined attributes (exclude standard library)
filtered_attributes = deduplicate_attributes(
[
x
for x in part.usages.collect()
if type(x) is syside.AttributeUsage
and x.document.document_tier is syside.DocumentTier.Project
]
)
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 = [
x
for x in element.usages.collect()
if type(x) is syside.PartUsage
and x.document.document_tier is syside.DocumentTier.Project
]
# 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
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()
if __name__ == "__main__":
main()
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()
Both versions of each script are run by the documentation build against the example model, and the rewrite must print exactly the output shown on the example’s page.