# noqa PLC0302
"""Basic tests of the controller classes in omni.graph.core"""
import omni.graph.core.tests as ogts


# ==============================================================================================================
class TestControllerApi(ogts.OmniGraphTestCase):
    """Run a simple set of unit tests that exercise the API of the controller. Tests are formatted in such a way
    that the documentation can be extracted directly from the running code, to ensure it stays up to date.
    """

    # --------------------------------------------------------------------------------------------------------------
    async def test_controller_api(self):
        """Contrived test that exercises the entire API of the Controller, as well as serving as a location for
        documentation to be extracted, illustrated by running code so that the documentation does not get out of
        date. The documentation sections are demarcated by {begin/end}-controller-XX pairs.
        """
        # begin-controller-boilerplate
        import omni.graph.core as og

        keys = og.Controller.Keys  # Usually handy to keep this name short as it is used often
        controller = og.Controller()
        # end-controller-boilerplate

        # begin-controller-as-static
        og.Controller.edit("/World/MyGraph", {keys.CREATE_NODES: ("MyClassNode", "omni.graph.tutorials.SimpleData")})
        og.Controller.edit("/World/MyGraph", {keys.SET_VALUES: ("/World/MyGraph/MyClassNode.inputs:a_bool", False)})
        # end-controller-as-static

        # begin-controller-as-object
        controller.edit("/World/MyGraph", {keys.CREATE_NODES: ("MyNode", "omni.graph.tutorials.SimpleData")})
        controller.edit("/World/MyGraph", {keys.SET_VALUES: ("MyNode.inputs:a_bool", False)})
        # end-controller-as-object

        # begin-controller-remember
        (graph, nodes, prims, name_to_path_map) = og.Controller.edit(
            "/World/MyGraph", {keys.CREATE_NODES: ("KnownNode", "omni.graph.tutorials.SimpleData")}
        )
        assert not prims
        # graph = og.Graph
        #     the newly created or existing graph, here the one at the path "/World/MyGraph"
        # nodes = list[og.Node]
        #     the list of nodes created, here [the node named "KnownNode"]
        # prims = list[Usd.Prim]
        #     the list of prims created via the keys.CREATE_PRIMS directive, here []
        # name_to_path_map = dict[str, str]
        #     the map of short names to full paths created, here {"KnownNode": "/World/MyGraph/KnownNode"}

        # You can use the node directly to find the attribute
        og.Controller.edit(graph, {keys.SET_VALUES: (("inputs:a_bool", nodes[0]), False)})

        # You can pass in the map so that the short name of the node can be used to find the attribute
        og.Controller.edit(graph, {keys.SET_VALUES: ("KnownNode.inputs:a_bool", False)}, name_to_path_map)
        # end-controller-remember
        # For later use
        og.Controller.connect("/World/MyGraph/MyClassNode.outputs:a_bool", "/World/MyGraph/KnownNode.inputs:a_bool")

        # begin-controller-access-type
        await og.Controller.evaluate()
        await controller.evaluate()
        # end-controller-access-type

        # begin-controller-evaluate
        # This version evaluates all graphs asynchronously
        await og.Controller.evaluate()
        # This version evaluates the graph defined by the controller object synchronously
        controller.evaluate_sync()
        # This version evaluates the single named graph synchronously
        og.Controller.evaluate_sync(graph)
        # end-controller-evaluate

        # begin-controller-attr-construct
        controller = og.Controller(og.Controller.attribute("/World/MyGraph/MyNode.inputs:a_bool"))
        if controller.get():
            print("This should not be hit as the value was already set to False")
        if og.Controller.get(og.Controller.attribute("/World/MyGraph/MyNode.inputs:a_bool")):
            print("Similarly, this is a variation of the same call that should give the same result")
        # end-controller-attr-construct
        self.assertFalse(controller.get())

        # begin-controller-big-edit
        # Keywords are shown for the edit arguments, however they are not required
        (graph, nodes, prims, name_to_path_map) = og.Controller.edit(
            # First parameter is a graph reference, created if it doesn't already exist.
            # See omni.graph.core.GraphController.create_graph() for more options
            graph_id="/World/MyGraph",
            # Second parameter is a dictionary
            edit_commands={
                # Delete a node or list of nodes that already existed in the graph
                # See omni.graph.core.GraphController.delete_nodes() for more options
                keys.DELETE_NODES: ["MyNode"],
                # Create new nodes in the graph - the resulting og.Nodes are returned in the "nodes" part of the tuple
                # See omni.graph.core.GraphController.create_node() for more options
                keys.CREATE_NODES: [
                    ("src", "omni.graph.tutorials.SimpleData"),
                    ("dst", "omni.graph.tutorials.SimpleData"),
                ],
                # Create new (dynamic) attributes on some nodes.
                # See omni.graph.core.NodeController.create_attribute() for more options
                keys.CREATE_ATTRIBUTES: [
                    ("src.inputs:dyn_float", "float"),
                    ("dst.inputs:dyn_any", "any"),
                ],
                # Create new prims in the stage - the resulting Usd.Prims are returned in the "prims" part of the tuple
                # See omni.graph.core.GraphController.create_prims() for more options
                keys.CREATE_PRIMS: [
                    ("Prim1", {"attrFloat": ("float", 2.0)}),
                    ("Prim2", {"attrBool": ("bool", True)}),
                ],
                # Expose on of the prims to OmniGraph by creating a USD import node to read it as a bundle.
                # The resulting node is in the "nodes" part of the tuple, after any native OmniGraph node types.
                # See omni.graph.core.GraphController.expose_prims() for more options
                keys.EXPOSE_PRIMS: [(og.Controller.PrimExposureType.AS_BUNDLE, "Prim1", "Prim1Exposed")],
                # Connect a source output to a destination input to create a flow of data between the two nodes
                # See omni.graph.core.GraphController.connect() for more options
                keys.CONNECT: [("src.outputs:a_int", "dst.inputs:a_int")],
                # Disconnect an already existing connection.
                # See omni.graph.core.GraphController.disconnect() for more options
                keys.DISCONNECT: [
                    ("/World/MyGraph/MyClassNode.outputs:a_bool", "/World/MyGraph/KnownNode.inputs:a_bool")
                ],
                # Define an attribute's value (inputs and state attributes only - outputs are computed)
                # See omni.graph.core.DataView.set() for more options
                keys.SET_VALUES: [("src.inputs:a_int", 5)],
                # Create graph-local variable values
                # See omni.graph.core.GraphController.create_variables() for more options
                keys.CREATE_VARIABLES: [("a_float_var", og.Type(og.BaseDataType.FLOAT)), ("a_bool_var", "bool")],
            },
            # Parameters from here down could also be saved as part of the object and reused repeatedly if you had
            # created a controller rather than calling edit() as a class method
            path_to_object_map=None,  # Saved object-to-path map, bootstraps any created as part of the call
            update_usd=True,  # Immediately echo the changes to the underlying USD
            undoable=True,  # If False then do not remember previous state - useful for writing tests
            allow_exists_node=False,  # If True then silently succeed requests to create already existing nodes
            allow_exists_prim=False,  # If True then silently succeed requests to create already existing prims
        )
        # end-controller-big-edit

    # --------------------------------------------------------------------------------------------------------------
    async def test_object_lookup_api(self):
        """Contrived test that exercises the entire API of the ObjectLookup, as well as serving as a location for
        documentation to be extracted, illustrated by running code so that the documentation does not get out of
        date. The documentation sections are demarcated by {begin/end}-object-lookup-XX pairs.
        """
        # begin-object-lookup-boilerplate
        import omni.graph.core as og
        import omni.usd
        from pxr import OmniGraphSchema, Sdf

        keys = og.Controller.Keys
        # Note that when you extract the parameters this way it is important to have the trailing "," in the node
        # and prim tuples so that Python doesn't try to interpret them as single objects.
        (graph, (node,), (prim,), _) = og.Controller.edit(
            "/World/MyGraph",
            {
                keys.CREATE_NODES: ("MyNode", "omni.graph.test.TestAllDataTypes"),
                keys.CREATE_PRIMS: ("MyPrim", {"myFloat": ("float", 0)}),
                keys.CREATE_VARIABLES: ("MyVariable", "float"),
            },
        )
        assert prim.IsValid()
        attribute = node.get_attribute("inputs:a_bool")
        relationship_attribute = node.get_attribute("inputs:a_target")
        attribute_type = attribute.get_resolved_type()
        node_type = node.get_node_type()
        variable = graph.get_variables()[0]
        stage = omni.usd.get_context().get_stage()
        # end-object-lookup-boilerplate

        # --------------------------------------------------------------------------------------------------------------
        # begin-object-lookup-graph
        # Look up the graph directly from itself (to simplify code)
        assert graph == og.Controller.graph(graph)

        # Look up the graph by path
        assert graph == og.Controller.graph("/World/MyGraph")

        # Look up the graph by Usd Prim
        graph_prim = stage.GetPrimAtPath("/World/MyGraph")
        assert graph == og.Controller.graph(graph_prim)

        # Look up by a Usd Schema object
        graph_schema = OmniGraphSchema.OmniGraph(graph_prim)
        assert graph == og.Controller.graph(graph_schema)

        # Look up the graph by SdfPath
        path = Sdf.Path("/World/MyGraph")
        assert graph == og.Controller.graph(path)

        # Look up a list of graphs by passing a list of any of the above
        for new_graph in og.Controller.graph([graph, "/World/MyGraph", path]):
            assert graph == new_graph
        # end-object-lookup-graph

        # --------------------------------------------------------------------------------------------------------------
        # begin-object-lookup-node
        # Look up the node directly from itself (to simplify code)
        assert node == og.Controller.node(node)

        # Look up the node by path
        assert node == og.Controller.node("/World/MyGraph/MyNode")

        # Look up the node by partial path and graph.
        # The graph parameter can be any of the ones supported by og.Controller.graph()
        assert node == og.Controller.node(("MyNode", graph))

        # Look up the node by SdfPath
        node_path = Sdf.Path("/World/MyGraph/MyNode")
        assert node == og.Controller.node(node_path)

        # Look up the node from its underlying USD prim backing, if it exists
        node_prim = stage.GetPrimAtPath("/World/MyGraph/MyNode")
        assert node == og.Controller.node(node_prim)

        # Look up a list of nodes by passing a list of any of the above
        for new_node in og.Controller.node([node, "/World/MyGraph/MyNode", ("MyNode", graph), node_path, node_prim]):
            assert node == new_node
        # end-object-lookup-node

        # --------------------------------------------------------------------------------------------------------------
        # begin-object-lookup-attribute
        # Look up the attribute directly from itself (to simplify code)
        assert attribute == og.Controller.attribute(attribute)

        # Look up the attribute by path
        assert attribute == og.Controller.attribute("/World/MyGraph/MyNode.inputs:a_bool")

        # Look up the attribute by SdfPath
        assert attribute == og.Controller.attribute(Sdf.Path("/World/MyGraph/MyNode.inputs:a_bool"))

        # Look up the attribute by name and node
        assert attribute == og.Controller.attribute(("inputs:a_bool", node))

        # These can chain, so you can also look up the attribute by name and node, where node is further looked up by
        # relative path and graph
        assert attribute == og.Controller.attribute(("inputs:a_bool", ("MyNode", graph)))

        # Look up the attribute by SdfPath
        attr_path = Sdf.Path("/World/MyGraph/MyNode.inputs:a_bool")
        assert attribute == og.Controller.attribute(attr_path)

        # Look up the attribute through its Usd counterpart
        stage = omni.usd.get_context().get_stage()
        node_prim = stage.GetPrimAtPath("/World/MyGraph/MyNode")
        usd_attribute = node_prim.GetAttribute("inputs:a_bool")
        assert attribute == og.Controller.attribute(usd_attribute)

        # Look up a list of attributes by passing a list of any of the above
        for new_attribute in og.Controller.attribute(
            [
                attribute,
                "/World/MyGraph/MyNode.inputs:a_bool",
                Sdf.Path("/World/MyGraph/MyNode.inputs:a_bool"),
                ("inputs:a_bool", node),
                ("inputs:a_bool", ("MyNode", graph)),
                attr_path,
                usd_attribute,
            ]
        ):
            assert attribute == new_attribute
        # end-object-lookup-attribute

        # --------------------------------------------------------------------------------------------------------------
        # begin-object-lookup-attribute-type
        attribute_type = og.Type(og.BaseDataType.FLOAT, tuple_count=3, array_depth=1, role=og.AttributeRole.POSITION)
        # Look up the attribute type by OGN type name
        assert attribute_type == og.Controller.attribute_type("pointf[3][]")

        # Look up the attribute type by SDF type name
        assert attribute_type == og.Controller.attribute_type("point3f[]")

        # Look up the attribute type directly from itself (to simplify code)
        assert attribute_type == og.Controller.attribute_type(attribute_type)

        # Look up the attribute type from the attribute with that type
        point_attribute = og.Controller.attribute(("inputs:a_pointf_3_array", node))
        assert attribute_type == og.Controller.attribute_type(point_attribute)

        # Look up the attribute type from the attribute data whose attribute has that type (most commonly done with
        # attributes that have extended types or attributes belonging to bundles)
        point_attribute_data = point_attribute.get_attribute_data()
        assert attribute_type == og.Controller.attribute_type(point_attribute_data)
        # end-object-lookup-attribute-type

        # --------------------------------------------------------------------------------------------------------------
        # begin-object-lookup-node-type
        node_type = node.get_node_type()
        # Look up the node type directly from itself (to simplify code)
        assert node_type == og.Controller.node_type(node_type)

        # Look up the node type from the string that uniquely identifies it
        assert node_type == og.Controller.node_type("omni.graph.test.TestAllDataTypes")

        # Look up the node type from the node with that type
        assert node_type == og.Controller.node_type(node)

        # Look up the node type from the USD Prim backing a node of that type
        assert node_type == og.Controller.node_type(node_prim)

        # Look up a list of node types by passing a list of any of the above
        for new_node_type in og.Controller.node_type(
            [
                node_type,
                "omni.graph.test.TestAllDataTypes",
                node,
                node_prim,
            ]
        ):
            assert node_type == new_node_type
        # end-object-lookup-node-type

        # --------------------------------------------------------------------------------------------------------------
        # begin-object-lookup-prim
        # Look up the prim directly from itself (to simplify code)
        assert node_prim == og.Controller.prim(node_prim)

        # Look up the prim from the prim path as a string
        assert node_prim == og.Controller.prim("/World/MyGraph/MyNode")

        # Look up the prim from the Sdf.Path pointing to the prim
        assert node_prim == og.Controller.prim(node_path)

        # Look up the prim from the OmniGraph node for which it is the backing
        assert node_prim == og.Controller.prim(node)

        # Look up the prim from the (node_path, graph) tuple defining the OmniGraph node for which it is the backing
        assert node_prim == og.Controller.prim(("MyNode", graph))

        # Look up the prim from an OmniGraph graph
        assert graph_prim == og.Controller.prim(graph)

        # Look up a list of prims by passing a list of any of the above
        for new_prim in og.Controller.prim(
            [
                node_prim,
                "/World/MyGraph/MyNode",
                node_path,
                node,
                ("MyNode", graph),
            ]
        ):
            assert node_prim == new_prim
        # end-object-lookup-prim

        # --------------------------------------------------------------------------------------------------------------
        # begin-object-lookup-usd-attribute
        # USD attributes can be looked up with the same parameters as for looking up an OmniGraph attribute

        # Look up the USD attribute directly from itself (to simplify code)
        assert usd_attribute == og.Controller.usd_attribute(usd_attribute)

        # Look up the USD attribute by path
        assert usd_attribute == og.Controller.usd_attribute("/World/MyGraph/MyNode.inputs:a_bool")

        # Look up the USD attribute by name and node
        assert usd_attribute == og.Controller.usd_attribute(("inputs:a_bool", node))

        # These can chain, so you can also look up the USD attribute by name and node, where node is further looked up by
        # relative path and graph
        assert usd_attribute == og.Controller.usd_attribute(("inputs:a_bool", ("MyNode", graph)))

        # Look up the USD attribute by SdfPath
        attr_path = Sdf.Path("/World/MyGraph/MyNode.inputs:a_bool")
        assert usd_attribute == og.Controller.usd_attribute(attr_path)

        # Look up the USD attribute through its OmniGraph counterpart
        assert usd_attribute == og.Controller.usd_attribute(attribute)

        # Look up a list of attributes by passing a list of any of the above
        for new_usd_attribute in og.Controller.usd_attribute(
            [
                usd_attribute,
                "/World/MyGraph/MyNode.inputs:a_bool",
                ("inputs:a_bool", node),
                ("inputs:a_bool", ("MyNode", graph)),
                attr_path,
                attribute,
            ]
        ):
            assert usd_attribute == new_usd_attribute
        # end-object-lookup-usd-attribute

        # --------------------------------------------------------------------------------------------------------------
        # begin-object-lookup-usd-relationship
        # USD relationships can be looked up with the same parameters as for looking up an OmniGraph attribute. This applies
        # when the attribute is backed by a USD relationship instead of a USD attribute.

        usd_relationship = node_prim.GetRelationship("inputs:a_target")

        # Look up the USD relationship directly from itself (to simplify code)
        assert usd_relationship == og.Controller.usd_relationship(usd_relationship)

        # Look up the USD relationship by path
        assert usd_relationship == og.Controller.usd_relationship("/World/MyGraph/MyNode.inputs:a_target")

        # Look up the USD relationship by name and node
        assert usd_relationship == og.Controller.usd_relationship(("inputs:a_target", node))

        # These can chain, so you can also look up the USD relationship by name and node, where node is further looked up by
        # relative path and graph
        assert usd_relationship == og.Controller.usd_relationship(("inputs:a_target", ("MyNode", graph)))

        # Look up the USD attribute by SdfPath
        relationship_path = Sdf.Path("/World/MyGraph/MyNode.inputs:a_target")
        assert usd_relationship == og.Controller.usd_relationship(relationship_path)

        # Look up the USD attribute through its OmniGraph counterpart
        assert usd_relationship == og.Controller.usd_relationship(relationship_attribute)

        # Look up a list of relationship by passing a list of any of the above
        for new_usd_relationship in og.Controller.usd_relationship(
            [
                usd_relationship,
                "/World/MyGraph/MyNode.inputs:a_target",
                ("inputs:a_target", node),
                ("inputs:a_target", ("MyNode", graph)),
                relationship_path,
                relationship_attribute,
            ]
        ):
            assert usd_relationship == new_usd_relationship
        # end-object-lookup-usd-relationship

        # --------------------------------------------------------------------------------------------------------------
        # begin-object-lookup-usd-property
        # USD properties can be looked up with the same parameters as for looking up an OmniGraph attribute. A property
        # is useful when it is not known ahead of time whether an og.Attribute is backed by a USD attribute or a USD relationship.

        # Look up the USD property directly from itself (to simplify code)
        assert usd_relationship == og.Controller.usd_property(usd_relationship)
        assert usd_attribute == og.Controller.usd_property(usd_attribute)

        # Look up the USD property by path
        assert usd_relationship == og.Controller.usd_property("/World/MyGraph/MyNode.inputs:a_target")
        assert usd_attribute == og.Controller.usd_property("/World/MyGraph/MyNode.inputs:a_bool")

        # Look up the USD property by name and node
        assert usd_relationship == og.Controller.usd_property(("inputs:a_target", node))
        assert usd_attribute == og.Controller.usd_property(usd_attribute)

        # These can chain, so you can also look up the USD property by name and node, where node is further looked up by
        # relative path and graph
        assert usd_relationship == og.Controller.usd_property(("inputs:a_target", ("MyNode", graph)))
        assert usd_attribute == og.Controller.usd_property(("inputs:a_bool", ("MyNode", graph)))

        # Look up the USD property by SdfPath
        assert usd_relationship == og.Controller.usd_property(relationship_path)
        assert usd_attribute == og.Controller.usd_property(attr_path)

        # Look up the USD property through its OmniGraph counterpart
        assert usd_relationship == og.Controller.usd_property(relationship_attribute)
        assert usd_attribute == og.Controller.usd_property(usd_attribute)

        # Look up a list of properties by passing a list of any of the above
        for new_usd_property in og.Controller.usd_property(
            [
                usd_relationship,
                usd_attribute,
                "/World/MyGraph/MyNode.inputs:a_target",
                "/World/MyGraph/MyNode.inputs:a_bool",
                ("inputs:a_target", node),
                ("inputs:a_bool", node),
                ("inputs:a_target", ("MyNode", graph)),
                ("inputs:a_bool", ("MyNode", graph)),
                relationship_path,
                attr_path,
                relationship_attribute,
                usd_attribute,
            ]
        ):
            assert new_usd_property in (usd_relationship, usd_attribute)
        # end-object-lookup-usd-property

        # --------------------------------------------------------------------------------------------------------------
        # begin-object-lookup-variable
        # Look up the variable directly from itself (to simplify code)
        assert variable == og.Controller.variable(variable)

        # Look up the variable from a tuple with the variable name and the graph to which it belongs
        assert variable == og.Controller.variable((graph, "MyVariable"))

        # Look up the variable from a path string pointing directly to it
        assert variable == og.Controller.variable(variable.source_path)

        # Look up the variable from an Sdf.Path pointing directly to it
        variable_path = Sdf.Path(variable.source_path)
        assert variable == og.Controller.variable(variable_path)

        # Look up a list of variables by passing a list of any of the above
        for new_variable in og.Controller.variable(
            [
                variable,
                (graph, "MyVariable"),
                variable.source_path,
                variable_path,
            ]
        ):
            assert variable == new_variable
        # end-object-lookup-variable

        # --------------------------------------------------------------------------------------------------------------
        # begin-object-lookup-utilities
        # Look up the path to an attribute given any of the types an attribute lookup recognizes
        for attribute_spec in [
            attribute,
            "/World/MyGraph/MyNode.inputs:a_bool",
            ("inputs:a_bool", node),
            ("inputs:a_bool", ("MyNode", graph)),
            attr_path,
            usd_attribute,
        ]:
            assert attribute.get_path() == og.Controller.attribute_path(attribute_spec)

        # Look up the path to a node given any of the types a node lookup recognizes
        for node_spec in [node, "/World/MyGraph/MyNode", ("MyNode", graph), node_path, node_prim]:
            assert node.get_prim_path() == og.Controller.node_path(node_spec)

        # Look up the path to a prim given any of the types a prim lookup recognizes
        for prim_spec in [node_prim, "/World/MyGraph/MyNode", node_path, node, ("MyNode", graph)]:
            assert node_prim.GetPrimPath() == og.Controller.prim_path(prim_spec)

        # Look up the path to a prim given any of the types a graph lookup recognizes
        graph_path = graph.get_path_to_graph()
        for graph_spec in [graph, graph_path, Sdf.Path(graph_path)]:
            assert graph_path == og.Controller.prim_path(graph_spec)

        # Look up a list of paths to prims given a list of any of the types a prim lookup recognizes
        for new_path in og.Controller.prim_path(
            [node_prim, "/World/MyGraph/MyNode", node_path, node, ("MyNode", graph)]
        ):
            assert node_prim.GetPrimPath() == new_path

        # Separate the graph name from the node name in a full path to a node
        assert (graph, "MyNode") == og.Controller.split_graph_from_node_path("/World/MyGraph/MyNode")

        # Separate the graph name from the node name in an Sdf.Path to a node
        assert (graph, "MyNode") == og.Controller.split_graph_from_node_path(node_path)
        # end-object-lookup-utilities

    # --------------------------------------------------------------------------------------------------------------
    async def test_graph_controller_api(self):
        """Contrived test that exercises the entire API of the GraphController, as well as serving as a location for
        documentation to be extracted, illustrated by running code so that the documentation does not get out of
        date. The documentation sections are demarcated by {begin/end}-graph-controller-XX pairs.
        """
        # begin-graph-controller-boilerplate
        import omni.graph.core as og
        import omni.kit
        from pxr import Sdf

        node_type_name = "omni.graph.test.TestAllDataTypes"
        node_type = og.Controller.node_type(node_type_name)
        # end-graph-controller-boilerplate

        # begin-graph-controller-init
        # Explanation of the non-default values in the constructor
        controller = og.GraphController(
            update_usd=False,  # Only update Fabric when paths are added are removed, do not propagate to USD
            undoable=False,  # Do not save information on changes for later undo (most applicable to testing)
            # If a node specification in og.GraphController.create_node() exists then silently succeed instead of
            # raising an exception
            allow_exists_node=True,
            # If a prim specification in og.GraphController.create_prim() exists then silently succeed instead of
            # raising an exception
            allow_exists_prim=True,
        )
        assert controller is not None
        # The default values are what is assumed when class methods are called. Where they apply to any of the
        # functions they can also be passed to the class method functions to specify non-default values.
        # end-graph-controller-init

        # begin-graph-controller-create_graph
        # Simple method of creating a graph just passes the desire prim path to it. This creates a graph using all
        # of the default parameters.
        graph = og.GraphController.create_graph("/World/MyGraph")
        assert graph.is_valid()

        # If you want to customize the type of graph then instead of passing just a path you can pass a dictionary
        # graph configuration values. See the developer documentation of omni.graph.core.GraphController.create_graph
        # for details on what each parameter means
        action_graph = og.GraphController.create_graph(
            {
                "graph_path": "/World/MyActionGraph",
                "node_name": "MyActionGraph",
                "evaluator_name": "execution",
                "is_global_graph": True,
                "backed_by_usd": True,
                "fc_backing_type": og.GraphBackingType.GRAPH_BACKING_TYPE_FABRIC_SHARED,
                "pipeline_stage": og.GraphPipelineStage.GRAPH_PIPELINE_STAGE_SIMULATION,
                "evaluation_mode": og.GraphEvaluationMode.GRAPH_EVALUATION_MODE_AUTOMATIC,
            }
        )

        # Also accepts the "update_usd" and "undoable" shared construction parameters
        # end-graph-controller-create_graph

        # begin-graph-controller-create_node
        # Creates a new node in an existing graph.
        # The two mandatory parameters are node path and node type. The node path can be any of the types recognized
        # by the omni.graph.core.ObjectLookup.node_path() method and the node type can be any of the types recognized
        # by the omni.graph.core.ObjectLookup.node_type() method.
        node_by_path = og.GraphController.create_node(
            node_id="/World/MyGraph/MyNode",
            node_type_id=node_type_name,
        )
        assert node_by_path.is_valid()
        node_by_name = og.GraphController.create_node(
            node_id=("MyNodeByName", graph),
            node_type_id=node_type,
        )
        assert node_by_name.is_valid()
        node_by_sdf_path = Sdf.Path("/World/MyGraph/MyNodeBySdf")
        node_by_sdf = og.GraphController.create_node(node_id=node_by_sdf_path, node_type_id=node_by_name)
        assert node_by_sdf.is_valid()

        # Also accepts the "update_usd", "undoable", and "allow_exists_node" shared construction parameters
        # end-graph-controller-create_node

        # begin-graph-controller-create_prim
        # Creates a new prim on the USD stage
        # You can just specify the prim path to get a default prim type with no attributes. The prim_path argument
        # can accept any value accepted by omni.graph.core.ObjectLookup.prim_path
        prim_empty = og.GraphController.create_prim(prim_path="/World/MyEmptyPrim")
        assert prim_empty.IsValid()

        # You can add a prim type if you want the prim to be a specific type, including schema types
        prim_cube = og.GraphController.create_prim(prim_path=Sdf.Path("/World/MyCube"), prim_type="Cube")
        assert prim_cube.IsValid()

        # You can also populate the prim with some attributes and values using the attribute_values parameter, which
        # accepts a dictionary of Name:(Type,Value). An attribute named "Name" will be created with type "Type"
        # (specified in either USD or SDF type format), and initial value "Value" (specified in any format compatible
        # with the Usd.Attribute.Set() function). The names do not have to conform to the usual OGN standards of
        # starting with one of the "inputs", "outputs", or "state" namespaces, though they can. The "Type" value is
        # restricted to the USD-native types, so things like "any", "bundle", and "execution" are not allowed.
        prim_with_values = og.GraphController.create_prim(
            prim_path="/World/MyValuedPrim",
            attribute_values={
                "someFloat": ("float", 3.0),
                "inputs:float3": ("float3", [1.0, 2.0, 3.0]),
                "someFloat3": ("float[3]", [4.0, 5.0, 6.0]),
                "someColor": ("color3d", [0.5, 0.6, 0.2]),
            },
        )
        assert prim_with_values.IsValid()

        # Also accepts the "undoable" and "allow_exists_prim" shared construction parameters
        # end-graph-controller-create_prim

        # begin-graph-controller-create_variable
        # To construct a variable the graph must be specified, along with the name and type of variable.
        # The variable type can only be an omni.graph.core.Type or a string representing one of those types.
        float_variable = og.GraphController.create_variable(graph_id=graph, name="FloatVar", var_type="float")
        assert float_variable.valid

        color3_type = og.Type(og.BaseDataType.FLOAT, 3, role=og.AttributeRole.COLOR)
        color3_variable = og.GraphController.create_variable(graph_id=graph, name="Color3Var", var_type=color3_type)
        assert color3_variable.valid

        # Also accepts the "undoable" shared construction parameter
        # end-graph-controller-create_variable

        # begin-graph-controller-delete_node
        # To delete a node you can pass in a node_id, as accepted by omni.graph.core.ObjectLookup.node, or a node
        # name and a graph_id as accepted by omni.graph.core.ObjectLookup.graph.
        og.GraphController.delete_node(node_by_sdf)
        # The undo flag was the global default so this operation is undoable
        omni.kit.undo.undo()
        # HOWEVER, you must get the node reference back as it may have been altered by the undo
        node_by_sdf = og.Controller.node(node_by_sdf_path)

        # Try it a different way
        og.GraphController.delete_node(node_id="MyNodeBySdf", graph_id=graph)

        # If you do not know if the node exists or not you can choose to ignore that case and silently succeed
        og.GraphController.delete_node(node_id=node_by_sdf_path, ignore_if_missing=True)

        # Also accepts the "update_usd" and "undoable" shared construction parameters
        # end-graph-controller-delete_node

        # begin-graph-controller-expose_prim
        # USD prims cannot be directly visible in OmniGraph so instead you must expose them through an import or
        # export node. See the documentation of the function for details on the different ways you can expose a prim
        # to OmniGraph.

        # A prim is exposed as a new OmniGraph node of a given type where the exposure process creates the node and
        # the necessary links to the underlying prim. The OmniGraph node can then be used as any others might.
        # The prim_id can accept any type accepted by omni.graph.core.ObjectLookup.prim() and the node_path_id can
        # accept any type accepted by omni.graph.core.ObjectLookup.node_path()
        exposed_empty = og.GraphController.expose_prim(
            exposure_type=og.GraphController.PrimExposureType.AS_BUNDLE,
            prim_id="/World/MyEmptyPrim",
            node_path_id="/World/MyActionGraph/MyEmptyNode",
        )
        assert exposed_empty is not None

        exposed_cube = og.GraphController.expose_prim(
            exposure_type=og.GraphController.PrimExposureType.AS_ATTRIBUTES,
            prim_id=prim_cube,
            node_path_id=("MyCubeNode", action_graph),
        )
        assert exposed_cube is not None

        # Also accepts the "update_usd" and "undoable" shared construction parameters
        # end-graph-controller-expose_prim

        # begin-graph-controller-connect
        # Once you have more than one node in a graph you will want to connect them so that the results of one node's
        # computation can be passed on to another for further computation - the true power of OmniGraph. The connection
        # sends data from the attribute in "src_spec" and sends it to the attribute in "dst_spec". Both of those
        # parameters can accept anything accepted by omni.graph.core.ObjectLookup.attribute
        og.GraphController.connect(
            src_spec=("outputs:a_bool", ("MyNode", graph)),
            dst_spec="/World/MyGraph/MyNodeByName/outputs:a_bool",
        )

        # Also accepts the "update_usd" and "undoable" shared construction parameters
        # end-graph-controller-connect

        # begin-graph-controller-disconnect
        # As part of wiring the nodes together you may also want to break connections, either to make a new connection
        # elsewhere or just to leave the attributes unconnected. The disconnect method is a mirror of the connect
        # method, taking the same parameters and breaking any existing connection between them. It is any error to try
        # to disconnect two unconnected attributes.
        og.GraphController.disconnect(
            src_spec=("outputs:a_bool", ("MyNode", graph)),
            dst_spec="/World/MyGraph/MyNodeByName/outputs:a_bool",
        )
        omni.kit.undo.undo()

        # Also accepts the "update_usd" and "undoable" shared construction parameters
        # end-graph-controller-disconnect

        # begin-graph-controller-disconnect_all
        # Sometimes you don't know or don't care what an attribute is connected to, you just want to remove all of its
        # connections, both coming to and going from it. The single attribute_spec parameter tells which attribute is to
        # be disconnected, accepting any value accepted by omni.graph.ObjectLookup.attribute
        og.GraphController.disconnect_all(attribute_spec=("outputs:a_bool", ("MyNode", graph)))

        # As this just disconnects "all" if an attribute is not connected to anything it will silently succeed
        og.GraphController.disconnect_all(attribute_spec=("outputs:a_bool", ("MyNode", graph)))

        # Also accepts the "update_usd" and "undoable" shared construction parameters
        # end-graph-controller-disconnect_all

        # begin-graph-controller-set_variable_default_value
        # After creation a graph variable will have zeroes as its default value. You may want to set some other default
        # so that when the graph is instantiated a second time the defaults are non-zero. The variable_id parameter
        # accepts anything accepted by omni.graph.core.ObjectLookup.variable() and the value must be a data type
        # compatible with the type of the (already existing) variable

        # For example you might have a color variable that you wish to initialize in all subsequent graphs to red
        og.GraphController.set_variable_default_value(variable_id=(graph, "Color3Var"), value=(1.0, 0.0, 0.0))
        # end-graph-controller-set_variable_default_value

        # begin-graph-controller-get_variable_default_value
        # If you are using variables to configure your graphs you probably want to know what the default values are,
        # especially if someone else created them. You can read the default for a given variable, where the variable_id
        # parameter accepts anything accepted by omni.graph.core.ObjectLookup.variable().
        color_default = og.GraphController.get_variable_default_value(variable_id=color3_variable)
        assert color_default == (1.0, 0.0, 0.0)
        # end-graph-controller-get_variable_default_value

    # --------------------------------------------------------------------------------------------------------------
    async def test_node_controller_api(self):
        """Contrived test that exercises the entire API of the NodeController, as well as serving as a location for
        documentation to be extracted, illustrated by running code so that the documentation does not get out of
        date. The documentation sections are demarcated by {begin/end}-node-controller-XX pairs.
        """
        # begin-node-controller-boilerplate
        import omni.graph.core as og

        keys = og.Controller.Keys
        (_, (node,), _, _) = og.Controller.edit(
            "/World/MyGraph",
            {
                keys.CREATE_NODES: ("MyNode", "omni.graph.test.TestAllDataTypes"),
            },
        )
        assert node.is_valid()
        # end-node-controller-boilerplate

        # begin-node-controller-init
        # The NodeController constructor only recognizes one parameter
        controller = og.NodeController(
            update_usd=False,  # Only update Fabric when attributes are added or removed, do not propagate to USD
        )
        assert controller is not None
        # end-node-controller-init

        # begin-node-controller-create_attribute
        # Creating new, or "dynamic", attributes on a node requires the same information you would find in a .ogn
        # description of the attribute. The mandatory pieces are "node" on which it is to be created, accepting
        # anything accepted by omni.graph.core.ObjectLookup.node, the name of the attribute not including the port
        # namespace (i.e. without the "inputs:", "outputs:", or "state:" prefix, though you can leave it on if you
        # prefer), the type "attr_type" of the attribute, accepting anything accepted by
        # omni.graph.core.ObjectLookup.attribute_type.

        # The default here is to create an input attribute of type float
        float_attr = og.NodeController.create_attribute(node, "theFloat", "float")
        assert float_attr.is_valid()

        # Using the namespace is okay, but redundant
        double_attr = og.NodeController.create_attribute("/World/MyGraph/MyNode", "inputs:theDouble", "double")
        assert double_attr.is_valid()

        # Unless you want a non-default port type, in which case it will be extracted from the name
        int_attr = og.NodeController.create_attribute(node, "outputs:theInt", og.Type(og.BaseDataType.INT))
        assert int_attr.is_valid()

        # ...or you can just specify the port explicitly and omit the namespace
        int2_attr = og.NodeController.create_attribute(
            node, "theInt2", "int2", attr_port=og.AttributePortType.ATTRIBUTE_PORT_TYPE_OUTPUT
        )
        assert int2_attr.is_valid()

        # The default will set an initial value on your attribute, though it is not remembered in the future
        float_1_attr = og.NodeController.create_attribute(node, "the1Float", "float", attr_default=1.0)
        assert float_1_attr.is_valid()

        # Mismatching between an explicit namespace and a port type will result in a duplicated namespace so be careful
        error_attr = og.NodeController.create_attribute(node, "outputs:theError", "float")
        assert error_attr.get_path() == "/World/MyGraph/MyNode.inputs:outputs:theError"
        assert error_attr.is_valid()

        # Lastly the special "extended" types of attributes (any or union) can be explicitly specified through
        # the "attr_extended_type" parameter. When this is anything other than the default then the "attr_type"
        # parameter will be ignored in favor of the extended type definition, however it must still be a legal type.
        # This simplest type of extended attribute is "any", whose value can be any legal type.
        union_attr = og.NodeController.create_attribute(
            node, "theAny", attr_type="float", attr_extended_type=og.ExtendedAttributeType.EXTENDED_ATTR_TYPE_ANY
        )
        assert union_attr.is_valid()

        # Note that with any extended type the "default" is invalid and will be ignored
        any_other_attr = og.NodeController.create_attribute(
            node, "theOtherAny", "token", default=5, attr_extended_type=og.ExtendedAttributeType.EXTENDED_ATTR_TYPE_ANY
        )
        assert any_other_attr.is_valid()

        # If you want a more restricted set of types you can instead use the extended union type. When specifying
        # that type it will be a 2-tuple where the second value is a list of types accepted by the union. For example
        # this attribute will accept either doubles or floats as value types. (See the documentation on extended
        # attribute types for more information on how types are resolved.)
        union_attr = og.NodeController.create_attribute(
            node,
            "theUnion",
            "token",
            attr_extended_type=(og.ExtendedAttributeType.EXTENDED_ATTR_TYPE_UNION, ["double", "float"]),
        )
        assert union_attr.is_valid()

        # Also accepts the "undoable" shared construction parameter
        # end-node-controller-create_attribute

        # begin-node-controller-remove_attribute
        # Dynamic attributes are a powerful method of reconfiguring a node at runtime, and as such you will also want
        # to remove them. The "attribute" parameter accepts anything accepted by
        # omni.graph.core.ObjectLookup.attribute(). (The "node" parameter, while still functional, is only there for
        # historical reasons and can be ignored.)
        og.NodeController.remove_attribute(error_attr)

        og.NodeController.remove_attribute(("inputs:theUnion", node))

        # Also accepts the "undoable" shared construction parameter
        # end-node-controller-remove_attribute

        # begin-node-controller-safe_node_name
        # This is a utility you can use to ensure a node name is safe for use in the USD backing prim. Normally
        # OmniGraph will take care of this for you but if you wish to dynamically create nodes using USD names you can
        # use this to confirm that your name is safe for use as a prim.
        assert og.NodeController.safe_node_name("omni.graph.node.name") == "omni_graph_node_name"

        # There is also an option to use a shortened name rather than replacing dots with underscores
        assert og.NodeController.safe_node_name("omni.graph.node.name", abbreviated=True) == "name"
        # end-node-controller-safe_node_name

    # --------------------------------------------------------------------------------------------------------------
    async def test_data_view_api(self):
        """Contrived test that exercises the entire API of the DataView, as well as serving as a location for
        documentation to be extracted, illustrated by running code so that the documentation does not get out of
        date. The documentation sections are demarcated by {begin/end}-data-view-XX pairs.
        """
        # begin-data-view-boilerplate
        import omni.graph.core as og

        keys = og.Controller.Keys
        (_, (node, any_node,), _, _) = og.Controller.edit(
            "/World/MyGraph",
            {
                keys.CREATE_NODES: [
                    ("MyNode", "omni.graph.test.TestAllDataTypes"),
                    ("MyAnyNode", "omni.graph.tutorials.ExtendedTypes"),
                ],
                keys.SET_VALUES: [
                    ("MyNode.inputs:a_int", 3),
                ],
            },
        )
        int_attr = og.Controller.attribute("inputs:a_int", node)
        union_attr = og.Controller.attribute("inputs:floatOrToken", any_node)
        float_array_attr = og.Controller.attribute("outputs:a_float_array", node)
        double_array_attr = og.Controller.attribute("outputs:a_double_array", node)
        # end-data-view-boilerplate

        # begin-data-view-init
        # The DataView constructor can take a number of parameters, mostly useful if you intend to make repeated
        # calls with the same configuration.

        # The most common parameter is the attribute on which you will operate. The parameter accepts anything
        # accepted by omni.graph.core.ObjectLookup.attribute()
        per_attr_view = og.DataView(attribute=int_attr)
        assert per_attr_view
        # Subsequent calls to per_attr_view functions will always apply to "int_attr"

        # You can also force USD and undo configurations, as per other classes like omni.graph.core.NodeController
        do_now = og.DataView(update_usd=False, undoable=False)
        assert do_now is not None

        # To keep memory operations on a single device you can configure the DataView to always use the GPU
        gpu_view = og.DataView(on_gpu=True, gpu_ptr_kind=og.PtrToPtrKind.CPU)
        # You can retrieve the GPU pointer kind (i.e. where the memory pointing to GPU arrays lives)
        assert gpu_view.gpu_ptr_kind == og.PtrToPtrKind.CPU

        # And if you are working with an instanced graph you can isolate the DataView to a single instance. Also
        # handy for looping through different instances.
        instance_view = og.DataView(instance=1)
        assert instance_view is not None
        # end-data-view-init

        # begin-data-view-get
        # Reading the value of an attribute is the most common operation you'll want to use.
        assert og.DataView.get(attribute=int_attr) == 3

        # If you've already configured the attribute you don't need to specify it, and you can reuse it
        assert per_attr_view.get() == 3
        assert per_attr_view.get() == 3

        # As a special case, when you have array attributes that you want to write on you can specify an array size
        # when you get the reference with the "reserved_element_count" parameter
        array_to_write = og.DataView.get(attribute=float_array_attr)
        assert len(array_to_write) == 2
        array_to_write = og.DataView.get(attribute=float_array_attr, reserved_element_count=5)
        assert len(array_to_write) == 5

        # Only valid on GPU array attributes is the "return_type" argument. Normally array values are returned in
        # numpy wrappers, however you can get the data as raw pointers as well if you want to handle processing of
        # the data yourself or cast it to some other library type. This also illustrates how you can use the
        # pre-configured constructed GPU view to get specific attribute values on the GPU.
        raw_array = gpu_view.get(attribute=double_array_attr, return_type=og.WrappedArrayType.RAW)

        # The return value is omni.graph.core.DataWrapper, which describes its device-specific data and configuration
        assert raw_array.gpu_ptr_kind == og.PtrToPtrKind.CPU

        # Also accepts overrides to the global parameters "on_gpu", "gpu_ptr_kind", and "instance"
        # end-data-view-get

        # begin-data-view-get_array_size
        # An array size may be set without actually allocating space for it. In the case of very large arrays this can
        # be quite useful. The get_array_size function lets you find the number of elements that will be in the array
        # if you request the data, either on GPU or CPU.
        assert og.DataView.get_array_size(float_array_attr) == 5

        # Also accepts overrides to the global parameter "instance"
        # end-data-view-get_array_size

        # begin-data-view-set
        # The counterpart to getting values is of course setting them. Normally through this interface you will be
        # setting values on input attributes, or sometimes state attributes, relying on the generated database to
        # provide the interface for setting output values as part of your node's compute function.
        # The "attribute" parameter accepts anything accepted by omni.graph.core.ObjectLookup.attribute(), and the
        # "value" parameter must be a legal value for the attribute type.
        og.DataView.set(attribute=int_attr, value=5)
        assert og.DataView.get(int_attr) == 5

        # An optional "update_usd" argument does what you'd expect, preventing the update of the USD backing value
        # for the attribute you just set.
        await og.Controller.evaluate()
        og.DataView.set(int_attr, value=10, update_usd=False)
        usd_attribute = og.ObjectLookup.usd_attribute(int_attr)
        assert usd_attribute.Get() != 10

        # The values being set are flexible as well, with the ability to use a dictionary format so that you can set
        # the type for any of the extended attributes.
        og.DataView.set(union_attr, value={"type": "float", "value": 3.5})
        assert og.DataView.get(union_attr) == 3.5

        # Also accepts overrides to the global parameters "on_gpu", "gpu_ptr_kind", and "instance"
        # end-data-view-set

        # begin-data-view-context
        # Sometimes you are calling unknown code and you want to ensure USD updates are performed the way you want
        # them. The DataView class provides a method that returns a contextmanager for just such a purpose.
        with og.DataView.force_usd_update(False):
            int_view = og.DataView(int_attr)
            int_view.set(value=20)
            # The USD value does not update, even though normally the default is to update
            assert usd_attribute.Get() != 20
        with og.DataView.force_usd_update(True):
            int_view = og.DataView(int_attr)
            int_view.set(value=30)
            # The USD value updates
            assert usd_attribute.Get() == 30
        # end-data-view-context
