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 syside.query

element.owned_elements.for_each(callback)

for child in query.contents(element)

element.owner, then its owner, and so on

query.containers(element)

for e in model.elements(syside.Element, include_subtypes=True), then if e.name == name

query.find_by_name_and_type(model, name, ExpectedType)

model.nodes(syside.PartUsage)

query.find_elements_of_type(model, syside.PartUsage)

scan part.heritage.elements for a definition by name

query.specializes(part, definition)

scan attribute.heritage.relationships for Redefinition, take first_target

query.redefines(attribute)

part.usages.collect() filtered to attributes

query.features_of(part) filtered to attributes

definition.variants.collect()

query.variants(definition)

walk element.owned_elements for Documentation, read .body

query.description(element)

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.