From 84a23b249fef210bd7bfc06729c4192e5f228598 Mon Sep 17 00:00:00 2001 From: Erich L Foster Date: Mon, 20 Jan 2025 11:09:08 +0000 Subject: [PATCH 01/30] Update to remove non-parser code --- README.md | 333 +------------------ behavior_tree/__init__.py | 24 -- package.xml | 26 +- py_trees_parser/__init__.py | 4 + {behavior_tree => py_trees_parser}/parser.py | 0 py_trees_parser/utils.py | 2 + setup.cfg | 4 +- setup.py | 18 +- 8 files changed, 29 insertions(+), 382 deletions(-) delete mode 100644 behavior_tree/__init__.py create mode 100644 py_trees_parser/__init__.py rename {behavior_tree => py_trees_parser}/parser.py (100%) create mode 100644 py_trees_parser/utils.py diff --git a/README.md b/README.md index 90f56ec..0028a60 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,16 @@ -# Behavior Tree +# py_trees Parser -This is the SAM XL behavior tree module. It contains generic behaviors for -perception and for simple motion planning and executing. +This is a xml parser for processing and building a py_trees behavior tree. The +hope is that most if not all capabilities of py_trees will be available for xml +parsing. As such, a py_trees behavior tree will be able to be created by simply +creating an xml file. ## Dependencies - ElementTree -- geometry_msgs -- moveit2 -- numpy -- opencv2 -- python-pcl - ros-humble - ros-humble-py-trees -- ros-humble-py-trees-ros-interfaces - ros-humble-py-trees-ros -- ros2_numpy -- sensor_msgs -- std_srvs -- tf2 To install dependencies you can run the following commands: @@ -31,218 +23,12 @@ rosdep -q install --from-paths src/ --ignore-src -y --rosdistro "${ROS_DISTRO}" where `ROS_DISTRO` is an environment variable containing the ros2 distribution name. -## Robot Node - -There is a single robot node which will live in the blackboard under the state -client (see Blackboards for more information about this). The intention of the -robot node is to be the location for image/sensor subscriptions, joint state -subscriptions, and the tf buffer. Additionally, any trigger services that may -need to be run can be accessed via the `Robot.command` function, where the -command to be called is passed via a string. These commands are defined as a -dictionary within the robot node. As for the tf tree one obtains transforms -from the robot node via the `Robot.lookup_transform` function. This is just a -convenience wrapper to the `tf2_ros.lookup_transform` function and is used in -the same manner. - -For the `Robot` node to be completely setup it is required to run -`robot.setup()`. This will load the `triggers`, and `sensors` and will setup -the subscriptions for those items. The `triggers` and `sensors` are configured -via a sensor and trigger file. These configuration files are expected to be -passed to the `Robot` node as an absolute path. - -### Sensor Configuration - -To configure the sensors to be used by the robot node, yaml can be passed to -the sensors parameter. The yaml should contain all sensors types, the sensor -name, and the topic to listen to. Additionally, any `camera_info` with their -sensor name and topics should also be included. - -#### Example - -```yaml -sensors: - rgb: - webcam: "/camera/webcam/color/image_raw" - realsense: "/camera/realsense/color/image_raw" - depth: - realsense: "/camera/realsense/depth/image_rect_raw" - camera_info: - webcam_rgb: "/camera/webcam/color/camera_info" - realsense_rgb: "/camera/realsense/color/camera_info" - realsense_depth: "/camera/realsense/depth/camera_info" -``` - -### Trigger Configuration - -Similarly to the sensor configuration the robot node can have triggers -configured via passing a yaml the `triggers` parameter. The triggers should -contain the name of the service and the service topic that the trigger service -is listening on. These triggers can be triggered via the `Trigger` behavior. - -#### Example - -```yaml -triggers: - service1: /triggers/service1 - service2: /triggers/service2 - service3: /triggers/service3 -``` - -## Blackboards - -- `State`: This blackboard contains the keys related to the state of the robot, - which includes the robot node and the state. -- `Perception`: This blackboard contains the keys related to the perception - stack of the robot, which includes images, point clouds, objects, etc. -- `Movement`: This blackboard contains the keys related to the movement of the - robot, which would include things like waypoints. - -When creating the blackboards you would want to follow the pattern below: - -```python -import py_tress - -from behavior_tree import Robot -from behavior_tree.data import Blackboards - -blackboard = Blackboards() -blackboard.set("state", key="state", value=State()) -robot = Robot() -robot.setup() # required for sensor and trigger setup -blackboard.set("state", key="robot", value=robot) -blackboard.set("perception", key="objects", value=None) -blackboard.set("movement", key="waypoints", value=None) -``` - -It is not necessary to register every client when creating the blackboard, -rather you can register the ones you need/want. Additionally, for variables on -the blackboard like "waypoints" or "objects" if you attempt to access them -before they have been set you will receive a `KeyError`, so be aware of this -when accessing variables. - -Additionally, be aware that the `Robot` node only needs to be created once and -should be done when creating the tree. Ideally, one would create the tree and -then add the `Robot` node to the tree in the following way: - -``` - tree = py_trees_ros.trees.BehaviourTree(root=root, unicode_tree_debug=True) - tree.setup(node=robot, timeout=15.0) -``` - -This will set the `Robot` node as the `py_trees_ros` node. Thus only one ros -node will exist in the tree unless another is created. - -## Decorators - -- `ContinueCancel` ([`py_trees.decorators.Decorator`]): This decorator waits - for a key press from the user, it will continue to return - `py_trees.common.Status.RUNNING` until the `continue_key` or `cancel_key` is - pressed, at which point it will return `py_trees.common.Status.SUCCESS` or the - childs last status, respectively. -- `RepeatFromBlackboard`: This is a decorator similar to the - `py_trees.decorators.Repeat`, however instead of having the number of - successes specified when creating the `Repeat` decorator a `num_key` is - specified. The `num_key` parameter is a key in the blackboard where the - decorator will look for the number of times to repeat the child behaviors. - The number of repitions will be updated each time `initialize` is called. - This decorator will return `RUNNING` until the children have been run the - number of times in the `num_key` and then will return `SUCCESS` or `FAILURE` - depending on the success of failure state of the children. - -## Behaviors - -- `ContinueCancel` ([`py_trees.behaviour.Behaviour`]): This behavior waits for - a key press from the user, it will continue to return -`py_trees.common.Status.RUNNING` until the `continue_key` or `cancel_key` is - pressed, at which point it will return `py_trees.common.Status.SUCCESS` or - `py_trees.common.Status.FAILURE`, respectively. -- `EmitFromYaml` (`py_trees.behaviour.Behaviour`): A behavior that sets a - blackboard variable given data from a file. Each time the behavior is ticked it - will read another line from a file and set a blackboard variable. If all - messages have been published it will continue from the beginning of the file. -- `SaveImage` ([`py_python.behavior.Behaviour`]): Save the current image for - "camera" to `perception` blackboard. -- `SaveImageToDrive` ([`py_python.behavior.Behaviour`]): Save the current image - for "camera" to drive. -- `SavePointCloud` ([`py_python.behavior.Behaviour`]): Save the current point - cloud for "camera" to `perception` blackboard. -- `Trigger` ([`py_trees.behaviour.Behaviour`]`]): Send a trigger for the given - trigger name. (see [triggers](#trigger-configuration) for details) -- `TransformToBlackboard` ([`py_python.behavior.Behaviour`]): Save the - requested transformation to the blackboard. - -### Action Behaviors - -- `ActionClient` ([`py_trees.behaviour.Behaviour`]): This is an abstract action - client class that sets up most of the steps that are necessary for an - ActionClient. This requires that any derived class create `get_goal` - `validate_result` functions, which creates the specific goal needed for the - desired action and validates/processes the action result, respectively. The - `get_goal` method should return the goal request for the action while - `validate_result` should return a `tuple[bool, str]` contains the result and - the feedback string. -- `Move` ([`behavior_tree.behaviors.ActionClient`]): Execute robot motion along - the given trajectory. -- `PlanJointMotion` ([`behavior_tree.PlanJoinSpaceMotion`]): Plan robot - movement to a joint configuration by providing a `link_name` and - `target_pose_key`. -- `PlanToNamedTarget` ([`behavior_tree.PlanJoinSpaceMotion`]): Plan robot - movement to a joint_configuration predefined by name. - -### Service Behaviors - -- `ServiceClient` ([`py_trees.behaviour.Behaviour`]): This is an abstract - service client class that sets up most of the steps that are necessary for a - ServiceClient. This requires that any derived class create a `get_request` - function, which creates the specific goal needed for the desired service, - additionally it requires a `validate_service_response` function that will - eventually set `self.success` depending if the response from the service was - valid or not. -- `PlanCartesian` ([`behavior_tree.ServiceClient`]): Plan robot - movement along the given waypoints. - -### Pick & Place pipeline - -The components that could make up the process of `pick&place` are created as -`ActionClient`s and `ServiceClient`s. The planning and motion are separate -and so a robot motion consists of a planning stage followed by an execution -stage. The motion is accomplished via the action `behavior_tree.actions.Move`. - -We distinguish three different types of motion requests: - -- `behavior_tree.services.PlanCartesian`: By providing among others a - `list of waypoints`, motions like __approaching__ a pick/place pose or - __retreating__ from one could be achieved with cartesian planning ensuring a - linear motion. -- `behavior_tree.actions.PlanJointMotion`: To reach a specific joint pose in - order to __rotate end-effector__ for example, the user can send a `Joint` - request by specifying a `target_pose_key` where the _goal pose_ would be - stored. -- `behavior_tree.actions.PlanToNamedTarget`: By providing a priorly configured - `named_target` i.e. a `named_joint_configuration`, the robot can move to - "general areas of interest" notably __general pick/place location__, - __inspection_station__ or __open/close fingers__ for a finger gripper... - -_____ -It is necessary to create a specific __gripper_behavior__ depending on its type -(fingers, suction, magnetic...) and how it is connected to the robot system. -One example used in Thermoplast is `SetABBIOSignal` which can -activate/deactivate the signal of the magnetic gripper. -____ -The _pick & place pipeline_ can then be composed of the different motion -behaviors the behavior_tree provides and easily costumized to the specific -use-case in place. See subtrees examples: `pick.xml` and `place.xml`, the -`pick_and_place.xml` tree is created by providing the formers as substrees. - ## XML Parser The Behavior Tree Parser (`BTParse`) is a Python module that allows you to parse an XML file representing a behavior tree and construct the corresponding -behavior tree using the PyTrees library. It supports composite nodes, behavior -nodes from PyTrees, and custom behavior nodes defined in your local library. -The tree file can be passed to the `Robot` node via the parameter `tree_file`. -The location of the tree file should be included as an absolute path, so that -the parser can find that file. +behavior tree using the `py_trees` library. It supports composite nodes, behavior +nodes from `py_trees`, and custom behavior nodes defined in your local library. For any parameter that is python code, the code must be surrounded by `$()`. This allows the parser know that the following parameter value is in fact code @@ -250,11 +36,15 @@ and should be evaluated as code. Idioms are also now supported. An idiom is a special function that produces a behavior tree, the function is expected to either take no children, takes -a list of children via parameters "taks" or "subtrees", or takes a single +a list of children via parameters "subtrees", or takes a single child via parameter "behavior". The xml parser will treat children in the same way that it treats children for all other behaviors. That is the children should be a subnode of the idiom node. +Additionally, it is possible to create a subtree, where a subtree is an xml +containing a complete behavior tree. This xml file can be included in other xml +files and therefore allows for complete modularity of trees. + ### Basic Usage To use the Behavior Tree Parser, follow these steps: @@ -281,7 +71,7 @@ Assuming the above is saved in `behavior_tree.xml` it can be imported via the following code: ```python -from behavior_tree import parser +from py_trees_parser import BTParser import py_trees from rclpy import logging @@ -290,7 +80,7 @@ logger.set_level(logging.LoggingSeverity.DEBUG) # Parse the XML file and create the behavior tree: xml_file = "behavior_tree.xml" -parser = BTParse(xml_file, logger) +parser = BTParser(xml_file, logger) behavior_tree = parser.parse() ``` @@ -421,98 +211,3 @@ and finally, subtree2 ``` - -## Running the Behavior Tree - -### Launch the Behavior Tree: - -To easily create a launch file the class `BehaviorTreeLaunch` was created. This -class provides some convenience methods for setting up a behavior tree launch -file. The CTOR for the `BehaviorTreeLaunch` class takes the following -parameters - -- `package` (`str`, optional): The package name for the ROS node. Defaults to "behavior_tree". -- `executable` (`str`, optional): The executable for the ROS node. Defaults to "behavior_tree". -- `name` (`str`, optional): The name for the ROS node. Defaults to "behavior_tree". - -And the provided methods are as follows: - -- `add_arg`: Add a new launch argument to the launch description. - + `name`: Name of the argument. - + `default_value`: Default value for the argument. - + `description`: Description of the argument. - + `kwargs`: Additional keyword arguments for the DeclareLaunchArgument action. -- `add_tree_file`: Set the path to the behavior tree XML file. - + `tree_file`: Path to the behavior tree XML file. -+ `add_config`: Add a new launch argument for the config file. - + `config`: Path to the config file. -- `add_parameter`: Add a new launch parameter. - + `name`: Name of the parameter. - + `value`: Value of the parameter. -- `set_log_level`: Set the log level for the ROS node. - + `log_level`: The log level for the ROS node. - Possible values are "debug", "error", "fatal", "info", or "warn" -- `add_remapping`: Add a new topic remappings to the launch description. - + `source`: The source topic. - + `destination`: The destination topic. -- `get_launch_description`: Return the launch description. - -A simple example can be found in `behavior_tree.launch.py` - -### Render a Tree - -To be able to render tree, open a terminal and run the following: - -```shell - py-trees-render behavior_tree.tree.view_tree -b -v --kwargs='{"xml_file": "/path/to/tree_file.xml"}' -``` - -### Viewing the Behavior Tree - -There are two ways to view the Behavior Tree. The first is has a GUI and -takes advantage of py-trees-ros-viewer. To view the behavior tree using -this method you simply run the following command: - -```shell -py-trees-tree-viewer -``` - -A GUI will appear that allows you to view the behavior tree. For more -details see the -[documentation](https://github.com/splintered-reality/py_trees_ros_viewer). - -The second method for viewing the behavior tree will result in the behavior -tree getting printed in the terminal. The tree will be updated on tick. This -can be achieved with the following command: - -```shell -py-trees-tree-watcher -b -``` - -This command will print the current state of the behavior tree including the -blackboard. To see more details run `py-trees-tree-watcher --help`. - -### Launch Parameters - -The following launch parameters apply to `behavior_tree.launch.py` - -| Parameter Name | Description | Default Value | -| :--- | :--- | :--- | -| config_file | Configuration containing configuration of node | "continue_cancel.yml" | -| webcam_image_topic | Topic to listen to for webcam image messages | "/camera/webcam/color/image" | -| webcam_info_topic | Topic to listen to for webcam camera info | "/camera/webcam/color/camera_info" | -| realsense_image_topic | Topic to listen to for realsense image messages | "/camera/realsense/color/image" | -| realsense_rgb_info_topic | Topic to listen to for realsense rgb camera info | "/camera/realsense/color/camera_info" | -| depth_image_topic | Topic to listen to for depth image messages | "/camera/realsense/depth/image" | -| depth_info_topic | Topic to listen to for depth camera info | "/camera/realsense/depth/camera_info" | -| pointcloud_topic | Topic to listen to for point cloud messages | "/camera/realsense/depth/image" | -| joint_state_topic | Topic to listen to for joint state messages | "~/joint_states" | -| compute_cartesian_path_topic | Topic to publish cartesian path messages too | "~/compute_cartesian_path" | - -## Relevant Links - - -[py_trees]: https://github.com/splintered-reality/py_trees -[py_trees_ros]: https://github.com/splintered-reality/py_trees_ros -[py_trees_ros_viewer]: https://github.com/splintered-reality/py_trees_ros_viewer -[py_trees_ros_tutorials]: https://github.com/splintered-reality/py_trees_ros_tutorials diff --git a/behavior_tree/__init__.py b/behavior_tree/__init__.py deleted file mode 100644 index b810494..0000000 --- a/behavior_tree/__init__.py +++ /dev/null @@ -1,24 +0,0 @@ -# this needs to be imported first -from behavior_tree.blackboards import Blackboards -from behavior_tree.data import State -from behavior_tree.parser import BTParser -from behavior_tree.robot import Robot - -from .action_client import ActionClient -from .bringup import BehaviorTreeLaunch -from .service_client import ServiceClient -from .tree import create_tree, setup_blackboard, setup_tree, view_tree - -__all__ = ( - "ActionClient", - "BehaviorTreeLaunch", - "Blackboards", - "BTParser", - "Robot", - "ServiceClient", - "State", - "create_tree", - "setup_blackboard", - "setup_tree", - "view_tree", -) diff --git a/package.xml b/package.xml index 8fee5b5..66ab5e9 100644 --- a/package.xml +++ b/package.xml @@ -1,37 +1,17 @@ - behavior_tree + py_trees_parser 0.6.0 - Behavior tree implementation for SAM XL + A py_trees xml parser Erich L Foster - Proprietary + apache 2.0 ament_index_python - cv_bridge - control_msgs - geometry_msgs - moveit_msgs - python3-pandas py_trees py_trees_ros - py_trees_ros_interfaces - py_trees_ros_viewer pynput-pip - python3-numpy - python3-opencv - python3-pandas - python3-pynput python3-setuptools - python3-yaml - rclpy - ros2_numpy - rosidl_runtime_py - sensor_msgs - simple_motion_msgs - std_srvs - tf_transformations - tf2_ros ament_copyright ament_flake8 diff --git a/py_trees_parser/__init__.py b/py_trees_parser/__init__.py new file mode 100644 index 0000000..5a3504d --- /dev/null +++ b/py_trees_parser/__init__.py @@ -0,0 +1,4 @@ +# this needs to be imported first +from behavior_tree.parser import BTParser + +__all__ = ("BTParser",) diff --git a/behavior_tree/parser.py b/py_trees_parser/parser.py similarity index 100% rename from behavior_tree/parser.py rename to py_trees_parser/parser.py diff --git a/py_trees_parser/utils.py b/py_trees_parser/utils.py new file mode 100644 index 0000000..c384302 --- /dev/null +++ b/py_trees_parser/utils.py @@ -0,0 +1,2 @@ +def check_battery_low_on_blackboard(blackboard) -> bool: + return blackboard.battery_low_warning diff --git a/setup.cfg b/setup.cfg index 003349d..6b9ba35 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,4 +1,4 @@ [develop] -script_dir=$base/lib/behavior_tree +script_dir=$base/lib/py_trees_parser [install] -install_scripts=$base/lib/behavior_tree +install_scripts=$base/lib/py_trees_parser diff --git a/setup.py b/setup.py index 6fd4326..6b656d4 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ from setuptools import find_packages, setup -package_name = "behavior_tree" +package_name = "py_trees_parser" setup( name=package_name, @@ -12,12 +12,6 @@ data_files=[ ("share/ament_index/resource_index/packages", [os.path.join("resource", package_name)]), (os.path.join("share/", package_name), ["package.xml"]), - ( - os.path.join("share", package_name, "launch"), - glob(os.path.join("launch", "*launch.[pxy][yma]*")), - ), - (os.path.join("share", package_name, "config"), glob(os.path.join("config", "*.yml"))), - (os.path.join("share", package_name, "trees"), glob(os.path.join("trees", "*.xml"))), ( os.path.join("share", package_name, "test", "data"), glob(os.path.join("test", "data", "*.xml")), @@ -27,12 +21,8 @@ zip_safe=True, maintainer="Erich L Foster", maintainer_email="e.l.f.foster@tudelft.nl", - description="Behavior tree implementation for SAM XL", - license="Proprietary", + description="A py_trees xml parser", + license="apache 2.0", tests_require=["pytest"], - entry_points={ - "console_scripts": [ - "behavior_tree = behavior_tree.tree:main", - ], - }, + entry_points={}, ) From 3b381e05b2a08e16f8562296c8f73cdc739f0581 Mon Sep 17 00:00:00 2001 From: Erich L Foster Date: Mon, 20 Jan 2025 11:13:06 +0000 Subject: [PATCH 02/30] Add license file --- LICENSE | 201 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 201 insertions(+) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. From ff038f8ac9f5dc54b5d75cb0e38eb221ec4d4e73 Mon Sep 17 00:00:00 2001 From: Erich L Foster Date: Tue, 21 Jan 2025 12:10:40 +0000 Subject: [PATCH 03/30] Add repos file --- dependencies.repos | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 dependencies.repos diff --git a/dependencies.repos b/dependencies.repos new file mode 100644 index 0000000..c26e440 --- /dev/null +++ b/dependencies.repos @@ -0,0 +1,10 @@ +repositories: + vendors/py_trees: + type: git + url: https://github.com/splintered-reality/py_trees.git + version: 2.2.3 + + vendors/py_trees_ros: + type: git + url: https://github.com/splintered-reality/py_trees_ros.git + version: 1729e289a4d7783dfe3b633bf4d144f3526fce0c From 54e7acbcb79e19f795dac832d280b74cd2c3755e Mon Sep 17 00:00:00 2001 From: Erich L Foster Date: Tue, 21 Jan 2025 12:26:20 +0000 Subject: [PATCH 04/30] Add github action --- .github/workflows/testing.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 .github/workflows/testing.yml diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml new file mode 100644 index 0000000..1488e6e --- /dev/null +++ b/.github/workflows/testing.yml @@ -0,0 +1,12 @@ +name: "testing" +on: pull_request +jobs: + testing: + runs-on: ubuntu-22.04 + steps: + - uses: ros-tooling/setup-ros@v0.7 + - uses: ros-tooling/action-ros-ci@v0.3 + with: + package-name: py_tree_parser + target-ros2-distro: humble + vcs-repo-file-url: https://raw.githubusercontent.com/sam-xl/py_trees_parser/refs/heads/ORTHO-277/create_bt_parser_repo/dependencies.repos From 4d5f2e6e7ff0effafa6dd142c6fe4463f81da536 Mon Sep 17 00:00:00 2001 From: Erich L Foster Date: Wed, 22 Jan 2025 08:14:57 +0000 Subject: [PATCH 05/30] Change behavior_tree reference to py_trees_parser --- py_trees_parser/__init__.py | 2 +- .../behaviors/testing_behaviours.py | 303 ++++++++++++++++++ py_trees_parser/utils.py | 2 - test/data/test6.xml | 12 +- test/test_parser.py | 2 +- 5 files changed, 312 insertions(+), 9 deletions(-) create mode 100644 py_trees_parser/behaviors/testing_behaviours.py delete mode 100644 py_trees_parser/utils.py diff --git a/py_trees_parser/__init__.py b/py_trees_parser/__init__.py index 5a3504d..d6ecfb3 100644 --- a/py_trees_parser/__init__.py +++ b/py_trees_parser/__init__.py @@ -1,4 +1,4 @@ # this needs to be imported first -from behavior_tree.parser import BTParser +from py_trees_parser.parser import BTParser __all__ = ("BTParser",) diff --git a/py_trees_parser/behaviors/testing_behaviours.py b/py_trees_parser/behaviors/testing_behaviours.py new file mode 100644 index 0000000..87bdb43 --- /dev/null +++ b/py_trees_parser/behaviors/testing_behaviours.py @@ -0,0 +1,303 @@ +"""Behaviours for the tutorials.""" +# +# License: BSD +# https://github.com/splintered-reality/py_trees_ros_tutorials/raw/devel/LICENSE +# +############################################################################## +# Documentation +############################################################################## + +############################################################################## +# Imports +############################################################################## + +import py_trees +import py_trees_ros +import rcl_interfaces.msg as rcl_msgs +import rcl_interfaces.srv as rcl_srvs +import std_msgs.msg as std_msgs + +############################################################################## +# Behaviours +############################################################################## + + +class FlashLedStrip(py_trees.behaviour.Behaviour): + """ + Flash an LED strip a certain color. + + This behaviour simply shoots a command off to the LEDStrip to flash + a certain colour and returns :attr:`~py_trees.common.Status.RUNNING`. + Note that this behaviour will never return with + :attr:`~py_trees.common.Status.SUCCESS` but will send a clearing + command to the LEDStrip if it is cancelled or interrupted by a higher + priority behaviour. + + Publishers: + ---------- + * **/led_strip/command** (:class:`std_msgs.msg.String`) + + * colourised string command for the led strip ['red', 'green', 'blue'] + + Args: + ---- + name: name of the behaviour + topic_name : name of the battery state topic + colour: colour to flash ['red', 'green', blue'] + + """ + + def __init__(self, name: str, topic_name: str = "/led_strip/command", colour: str = "red"): + super(FlashLedStrip, self).__init__(name=name) + self.topic_name = topic_name + self.colour = colour + + def setup(self, **kwargs): + """ + Set up the publisher which will stream commands to the mock robot. + + Args: + ---- + **kwargs (:obj:`dict`): look for the 'node' object being passed down from the tree + + Raises + ------ + :class:`KeyError`: if a ros2 node isn't passed under the key 'node' in kwargs + + """ + self.logger.debug("{}.setup()".format(self.qualified_name)) + try: + self.node = kwargs["node"] + except KeyError as e: + error_message = "didn't find 'node' in setup's kwargs [{}]".format(self.qualified_name) + raise KeyError(error_message) from e # 'direct cause' traceability + + self.publisher = self.node.create_publisher( + msg_type=std_msgs.String, + topic=self.topic_name, + qos_profile=py_trees_ros.utilities.qos_profile_latched(), + ) + self.feedback_message = "publisher created" + + def update(self) -> py_trees.common.Status: + """ + Annoy the led strip to keep firing every time it ticks over. + + The led strip will clear itself if no command is forthcoming within a + certain period of time. This behaviour will only finish if it is + terminated or priority interrupted from above. + + Returns + ------- + Always returns :attr:`~py_trees.common.Status.RUNNING` + + """ + self.logger.debug("%s.update()" % self.__class__.__name__) + self.publisher.publish(std_msgs.String(data=self.colour)) + self.feedback_message = "flashing {0}".format(self.colour) + return py_trees.common.Status.RUNNING + + def terminate(self, new_status: py_trees.common.Status): + """ + Shoot off a clearing command to the led strip. + + Args: + ---- + new_status: the behaviour is transitioning to this new status + + """ + self.logger.debug( + "{}.terminate({})".format( + self.qualified_name, + "{}->{}".format(self.status, new_status) + if self.status != new_status + else "{}".format(new_status), + ) + ) + self.publisher.publish(std_msgs.String(data="")) + self.feedback_message = "cleared" + + +class ScanContext(py_trees.behaviour.Behaviour): + """ + Allude to switching the context of the runtime system for a scanning action. + + Technically, it reaches out to the mock robots safety sensor + dynamic parameter, switches it off in :meth:`initialise()` and maintains + that for the the duration of the context before returning it to + it's original value in :meth:`terminate()`. + + Args: + ---- + name (:obj:`str`): name of the behaviour + + """ + + def __init__(self, name): + super().__init__(name=name) + + self.cached_context = None + + def setup(self, **kwargs): + """ + Set up the ros2 communications infrastructure. + + Args: + ---- + **kwargs (:obj:`dict`): look for the 'node' object being passed down from the tree + + Raises + ------ + :class:`KeyError`: if a ros2 node isn't passed under the key 'node' in kwargs + + """ + self.logger.debug("%s.setup()" % self.__class__.__name__) + + # ros2 node + try: + self.node = kwargs["node"] + except KeyError as e: + error_message = "didn't find 'node' in setup's kwargs [{}]".format(self.qualified_name) + raise KeyError(error_message) from e # 'direct cause' traceability + + # parameter service clients + self.parameter_clients = { + "get_safety_sensors": self.node.create_client( + rcl_srvs.GetParameters, "/safety_sensors/get_parameters" + ), + "set_safety_sensors": self.node.create_client( + rcl_srvs.SetParameters, "/safety_sensors/set_parameters" + ), + } + for name, client in self.parameter_clients.items(): + if not client.wait_for_service(timeout_sec=3.0): + raise RuntimeError("client timed out waiting for server [{}]".format(name)) + + def initialise(self): + """ + Reset the cached context and trigger the chain of get/set parameter calls. + + The calls that are triggered are involved in changing the context. + + .. note:: + + Completing the chain of service calls here + (with `rclpy.spin_until_future_complete(node, future)`) + is not possible if this behaviour is encapsulated inside, e.g. + a tree tick activated by a ros2 timer callback, since it is + already part of a scheduled job in a spinning node. It will + just deadlock. + + Prefer instead to chain a sequence of events that will be + completed over a span of ticks instead of at best, blocking + here and at worst, falling into deadlock. + + """ + self.logger.debug("%s.initialise()" % self.__class__.__name__) + self.cached_context = None + # kickstart get/set parameter chain + self._send_get_parameter_request() + + def update(self) -> py_trees.common.Status: + """ + Complete the chain of calls begun in :meth:`initialise()` and then maintain the context. + + (i.e. :class:`py_trees.behaviour.Behaviour` and return + :data:`~py_trees.common.Status.RUNNING`). + + """ + self.logger.debug("%s.update()" % self.__class__.__name__) + all_done = False + + # wait for get_parameter to return + if self.cached_context is None: + if self._process_get_parameter_response(): + self._send_set_parameter_request(value=True) + return py_trees.common.Status.RUNNING + + # wait for set parameter to return + if not all_done: + if self._process_set_parameter_response(): + all_done = True + return py_trees.common.Status.RUNNING + + # just spin around, wait for an interrupt to trigger terminate + return py_trees.common.Status.RUNNING + + def terminate(self, new_status: py_trees.common.Status): + """ + Reset the parameters back to their original (cached) values. + + Args: + ---- + new_status: the behaviour is transitioning to this new status + + """ + self.logger.debug( + "%s.terminate(%s)" + % ( + self.__class__.__name__, + "%s->%s" % (self.status, new_status) + if self.status != new_status + else "%s" % new_status, + ) + ) + if new_status == py_trees.common.Status.INVALID and self.cached_context is not None: + self._send_set_parameter_request(value=self.cached_context) + # don't worry about the response, no chance to catch it anyway + + def _send_get_parameter_request(self): + request = rcl_srvs.GetParameters.Request() # noqa + request.names.append("enabled") + self.get_parameter_future = self.parameter_clients["get_safety_sensors"].call_async( + request + ) + + def _process_get_parameter_response(self) -> bool: + if not self.get_parameter_future.done(): + return False + if self.get_parameter_future.result() is None: + self.feedback_message = "failed to retrieve the safety sensors context" + self.node.get_logger().error(self.feedback_message) + # self.node.get_logger().info('Service call failed %r' % (future.exception(),)) + raise RuntimeError(self.feedback_message) + if len(self.get_parameter_future.result().values) > 1: + self.feedback_message = "expected one parameter value, got multiple [{}]".format( + "/safety_sensors/enabled" + ) + raise RuntimeError(self.feedback_message) + value = self.get_parameter_future.result().values[0] + if value.type != rcl_msgs.ParameterType.PARAMETER_BOOL: # noqa + self.feedback_message = "expected parameter type bool, got [{}]{}]".format( + value.type, "/safety_sensors/enabled" + ) + self.node.get_logger().error(self.feedback_message) + raise RuntimeError(self.feedback_message) + self.cached_context = value.bool_value + return True + + def _send_set_parameter_request(self, value: bool): + request = rcl_srvs.SetParameters.Request() # noqa + parameter = rcl_msgs.Parameter() + parameter.name = "enabled" + parameter.value.type = rcl_msgs.ParameterType.PARAMETER_BOOL # noqa + parameter.value.bool_value = value + request.parameters.append(parameter) + self.set_parameter_future = self.parameter_clients["set_safety_sensors"].call_async( + request + ) + + def _process_set_parameter_response(self) -> bool: + if not self.get_parameter_future.done(): + return False + if self.set_parameter_future.result() is not None: + self.feedback_message = "reconfigured the safety sensors context" + else: + self.feedback_message = "failed to reconfigure the safety sensors context" + self.node.get_logger().error(self.feedback_message) + # self.node.get_logger().info('service call failed %r' % (future.exception(),)) + return True + + +def check_battery_low_on_blackboard(blackboard) -> bool: + return blackboard.battery_low_warning diff --git a/py_trees_parser/utils.py b/py_trees_parser/utils.py deleted file mode 100644 index c384302..0000000 --- a/py_trees_parser/utils.py +++ /dev/null @@ -1,2 +0,0 @@ -def check_battery_low_on_blackboard(blackboard) -> bool: - return blackboard.battery_low_warning diff --git a/test/data/test6.xml b/test/data/test6.xml index abc0a65..0f0fc11 100644 --- a/test/data/test6.xml +++ b/test/data/test6.xml @@ -12,9 +12,9 @@ - + - + - + - + diff --git a/test/test_parser.py b/test/test_parser.py index 8b682e6..f130ec0 100644 --- a/test/test_parser.py +++ b/test/test_parser.py @@ -7,7 +7,7 @@ from ament_index_python.packages import get_package_share_directory from conftest import log_test_execution -from behavior_tree.parser import BTParser +from py_trees_parser.parser import BTParser SHARE_DIR = get_package_share_directory("behavior_tree") From 5782f8a4c3413b8349a471bc67baae1b7a3acff9 Mon Sep 17 00:00:00 2001 From: Erich L Foster Date: Wed, 22 Jan 2025 08:21:20 +0000 Subject: [PATCH 06/30] Fix typo package name in action --- .github/workflows/testing.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index 1488e6e..d8e33f9 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -7,6 +7,6 @@ jobs: - uses: ros-tooling/setup-ros@v0.7 - uses: ros-tooling/action-ros-ci@v0.3 with: - package-name: py_tree_parser + package-name: py_trees_parser target-ros2-distro: humble vcs-repo-file-url: https://raw.githubusercontent.com/sam-xl/py_trees_parser/refs/heads/ORTHO-277/create_bt_parser_repo/dependencies.repos From dec1138a558b73a5f510800083b5b6314a0b3782 Mon Sep 17 00:00:00 2001 From: Erich L Foster Date: Wed, 22 Jan 2025 08:27:55 +0000 Subject: [PATCH 07/30] Add missing resource file --- resource/py_trees_parser | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 resource/py_trees_parser diff --git a/resource/py_trees_parser b/resource/py_trees_parser new file mode 100644 index 0000000..e69de29 From 4b67fcd4df17a08980c32bf0a0f2b467fd0c1758 Mon Sep 17 00:00:00 2001 From: Erich L Foster Date: Wed, 22 Jan 2025 08:32:00 +0000 Subject: [PATCH 08/30] Add PR template --- .github/pull_request_template.md | 34 ++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 .github/pull_request_template.md diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..3ea2112 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,34 @@ +Description of MR/PR... + +Fixes # + +## Motivation and Context + +Why is this change implemented? Add screenshots, images, or other media if this helps explain. + +## Changes + +- Item 1 +- Item 2 + +## Type of changes + +- Bug fix (non-breaking change which fixes an issue) +- New feature (non-breaking change which adds functionality) +- Breaking change (fix or feature that would cause existing functionality to change) + +[comment]: # (Delete the lines in "Type of changes" that are not appropriate) + +## Checklist + +- [ ] Update dependencies +- [ ] Update version +- [ ] Update README + +## Testing + +This should contain instructions on how to test the new code, or details on how +the new code was tested, e.g. + +1. `colcon build --symlink-install --merge-install --packages-up-to py_trees_parser` +2. `colcon test --merge-install --event-handlers console_cohesion+ --packages-select py_trees_parser` From a37019eed8b8a82c0616283f88711bfd72c189f1 Mon Sep 17 00:00:00 2001 From: Erich L Foster Date: Wed, 22 Jan 2025 10:19:30 +0000 Subject: [PATCH 09/30] Fix testing --- ...ing_behaviours.py => testing_behaviors.py} | 0 test/data/test6.xml | 8 +-- test/test_parser.py | 53 +++++++++++-------- 3 files changed, 35 insertions(+), 26 deletions(-) rename py_trees_parser/behaviors/{testing_behaviours.py => testing_behaviors.py} (100%) diff --git a/py_trees_parser/behaviors/testing_behaviours.py b/py_trees_parser/behaviors/testing_behaviors.py similarity index 100% rename from py_trees_parser/behaviors/testing_behaviours.py rename to py_trees_parser/behaviors/testing_behaviors.py diff --git a/test/data/test6.xml b/test/data/test6.xml index 0f0fc11..3149e5c 100644 --- a/test/data/test6.xml +++ b/test/data/test6.xml @@ -14,7 +14,7 @@ - + - + - - diff --git a/test/test_parser.py b/test/test_parser.py index f130ec0..0b4c09a 100644 --- a/test/test_parser.py +++ b/test/test_parser.py @@ -5,15 +5,37 @@ import pytest import rclpy from ament_index_python.packages import get_package_share_directory -from conftest import log_test_execution from py_trees_parser.parser import BTParser -SHARE_DIR = get_package_share_directory("behavior_tree") +SHARE_DIR = get_package_share_directory("py_trees_parser") rclpy.logging.get_logger("BTParser").set_level(rclpy.logging.LoggingSeverity.DEBUG) +@pytest.fixture(scope="module") +def ros_init(): + rclpy.init() + yield + rclpy.shutdown() + + +@pytest.fixture +def setup_parser(ros_init): + def _setup(tree_file): + xml = os.path.join(SHARE_DIR, tree_file) + parser = BTParser(xml, log_level=rclpy.logging.LoggingSeverity.DEBUG) + try: + root = parser.parse() + py_trees_ros.trees.BehaviourTree(root=root, unicode_tree_debug=True) + except Exception as ex: + assert False, f"parse raised an exception {ex}" + + return root + + return _setup + + @pytest.mark.parametrize( "tree_file", [ @@ -24,26 +46,13 @@ "test/data/test_subtree_main.xml", ], ) -@log_test_execution -def test_tree_parser(ros_init, tree_file): - xml = os.path.join(SHARE_DIR, tree_file) - parser = BTParser(xml, log_level=rclpy.logging.LoggingSeverity.DEBUG) - try: - root = parser.parse() - py_trees_ros.trees.BehaviourTree(root=root, unicode_tree_debug=True) - except Exception as ex: - assert False, f"parse raised an exception {ex}" - - -@log_test_execution -def test_subtree_and_args(ros_init): - xml = os.path.join(SHARE_DIR, "test/data/test_args.xml") - parser = BTParser(xml, log_level=rclpy.logging.LoggingSeverity.DEBUG) - try: - root = parser.parse() - py_trees_ros.trees.BehaviourTree(root=root, unicode_tree_debug=True) - except Exception as ex: - assert False, f"parse raised an exception {ex}" +def test_tree_parser(setup_parser, tree_file): + _ = setup_parser(tree_file) + + +def test_subtree_and_args(setup_parser): + tree_file = "test/data/test_args.xml" + root = setup_parser(tree_file) assert root.name == "Subtree Selector" for child in root.children: From 72ec7cd6a507fd91a2f0de8b95bca09dbf819084 Mon Sep 17 00:00:00 2001 From: Erich L Foster Date: Wed, 22 Jan 2025 10:19:45 +0000 Subject: [PATCH 10/30] Remove behavior_tree references from README --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 0028a60..2f4d549 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ creating an xml file. To install dependencies you can run the following commands: ```shell -vcs import src < src/behavior_tree/dependencies.repos +vcs import src < src/py_trees_parser/dependencies.repos rosdep update rosdep -q install --from-paths src/ --ignore-src -y --rosdistro "${ROS_DISTRO}" ``` @@ -87,7 +87,7 @@ behavior_tree = parser.parse() ### Using Your Own Behaviors The xml parser can use any behavior, whether it is part of `py_trees`, `py_trees_ros`, -`behavior_tree`, or your own python module. The way the parser knows the existance of +or your own python module. The way the parser knows the existance of the behavior is via the behavior tag in the xml. The behavior tag should be the fully qualified python path of the behavior, so if you have the following structure of your python module From ec27685c35df12c5309cf2f7adf3b21a5ba3c432 Mon Sep 17 00:00:00 2001 From: Erich L Foster Date: Wed, 22 Jan 2025 10:29:05 +0000 Subject: [PATCH 11/30] Add pyproject file for linting --- pyproject.toml | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 pyproject.toml diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..3dad137 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,10 @@ +[tool.ruff] +line_length = 99 +[tool.black] +line-length = 99 +[tool.isort] +atomic = true +profile = "black" +line_length = 99 +[tool.pep257] +ignore = "D100, D101, D102, D103, D104, D105, D106, D107, D203, D212, D404" From 8b0f314f98291e88c233fd398e0642d4292fb877 Mon Sep 17 00:00:00 2001 From: Erich L Foster Date: Wed, 22 Jan 2025 12:20:27 +0000 Subject: [PATCH 12/30] Try industrial_ci --- .github/workflows/testing.yml | 35 ++++++++++++++++++++++++++--------- dependencies.repos => .repos | 0 2 files changed, 26 insertions(+), 9 deletions(-) rename dependencies.repos => .repos (100%) diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index d8e33f9..0adfa43 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -1,12 +1,29 @@ -name: "testing" -on: pull_request +on: # this determines when this workflow is run + pull_request: + workflow_dispatch: # allow manually starting this workflow + jobs: - testing: - runs-on: ubuntu-22.04 + industrial_ci: + name: ROS ${{ matrix.ROS_DISTRO }} (${{ matrix.ROS_REPO }}) + runs-on: ubuntu-latest + strategy: + # fail-fast: false # uncomment if failing jobs should not cancel the others immediately + matrix: # matrix is the product of entries + ROS_DISTRO: [humble] + ROS_REPO: [main] + env: + UPSTREAM_WORKSPACE: .repos steps: - - uses: ros-tooling/setup-ros@v0.7 - - uses: ros-tooling/action-ros-ci@v0.3 + - name: Checkout out Git repository + uses: actions/checkout@v4 # clone target repository + - name: Run python linting + uses: chartboost/ruff-action@v1 with: - package-name: py_trees_parser - target-ros2-distro: humble - vcs-repo-file-url: https://raw.githubusercontent.com/sam-xl/py_trees_parser/refs/heads/ORTHO-277/create_bt_parser_repo/dependencies.repos + args: --check . + - name: Run unit tests + uses: ros-industrial/industrial_ci@master # run industrial_ci + env: # either pass all entries explicitly + ROS_DISTRO: ${{ matrix.ROS_DISTRO }} + ROS_REPO: ${{ matrix.ROS_REPO }} + # with: # or pass the full matrix as config + # config: ${{toJSON(matrix)}} diff --git a/dependencies.repos b/.repos similarity index 100% rename from dependencies.repos rename to .repos From 68ed62166c109171fb3c4e097843cd96b1d50b28 Mon Sep 17 00:00:00 2001 From: Erich L Foster Date: Wed, 22 Jan 2025 12:48:02 +0000 Subject: [PATCH 13/30] Update ruff action and pyproject.toml --- .github/workflows/testing.yml | 3 ++- pyproject.toml | 50 +++++++++++++++++++++++++++++------ 2 files changed, 44 insertions(+), 9 deletions(-) diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index 0adfa43..299784f 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -19,7 +19,8 @@ jobs: - name: Run python linting uses: chartboost/ruff-action@v1 with: - args: --check . + config: pyproject.toml + changed-files: true - name: Run unit tests uses: ros-industrial/industrial_ci@master # run industrial_ci env: # either pass all entries explicitly diff --git a/pyproject.toml b/pyproject.toml index 3dad137..7e94c64 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,10 +1,44 @@ [tool.ruff] -line_length = 99 -[tool.black] line-length = 99 -[tool.isort] -atomic = true -profile = "black" -line_length = 99 -[tool.pep257] -ignore = "D100, D101, D102, D103, D104, D105, D106, D107, D203, D212, D404" +# industrial_ci gets cloned to the root of the repository in the ci/cd pipeline +extend-exclude = [".industrial_ci"] + +[tool.ruff.lint] +select = [ + # pydocstyle: check for docstring conventions + "D", + # pycodestyle: check PEP 8 code style conventions + "E", + # pyflakes: check for various errors (e.g. unused imports or variables) + "F", + # pyupgrade: check for old syntax in newer python versions + "UP", + # flake8-bugbear: check for bugs and design problems (e.g. using mutalble data structures as defaults) + "B", + # flake8-simplify: check for basic code simplifications + "SIM", + # isort: import sorting + "I", +] + +extend-select = [ + # enable D213 on top of the Google convention + # more pretty than D212 + "D213" +] + +ignore = [ + # disable "missing docstring in public package" error + # already done in setup.py + "D104", + # disable "multi-line docstring summary should start at the first line" error + # ugly! and using alternative (D213) instead + "D212", + # disable: "missing argument description in the docstring" + # overkill for simple functions with descriptive argument names + "D417" +] + +[tool.ruff.lint.pydocstyle] +# limit `pydocstyle` rules to those that adhere to the Google convention +convention = "google" From a4f2ef273c307c80fab9a5875f0a87e8d840553b Mon Sep 17 00:00:00 2001 From: Erich L Foster Date: Wed, 22 Jan 2025 12:58:02 +0000 Subject: [PATCH 14/30] Update linting rules and apply ruff --- py_trees_parser/__init__.py | 13 ++++++++ py_trees_parser/parser.py | 62 +++++++++++++++++++++++++++---------- pyproject.toml | 6 ++++ setup.py | 13 ++++++++ test/test_flake8.py | 23 -------------- test/test_parser.py | 28 ++++++++++++++++- test/test_pep257.py | 23 -------------- 7 files changed, 104 insertions(+), 64 deletions(-) delete mode 100644 test/test_flake8.py delete mode 100644 test/test_pep257.py diff --git a/py_trees_parser/__init__.py b/py_trees_parser/__init__.py index d6ecfb3..9676319 100644 --- a/py_trees_parser/__init__.py +++ b/py_trees_parser/__init__.py @@ -1,3 +1,16 @@ +# Copyright 2025 SAM-XL +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. # this needs to be imported first from py_trees_parser.parser import BTParser diff --git a/py_trees_parser/parser.py b/py_trees_parser/parser.py index 603cdb7..59bb4ea 100644 --- a/py_trees_parser/parser.py +++ b/py_trees_parser/parser.py @@ -1,3 +1,25 @@ +# Copyright 2025 SAM-XL +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Module for parsing behavior tree XML files. + +This module contains the `BTParser` class, which is used to parse behavior tree XML files. + +The `BTParser` class has the following methods: +""" + import ast import importlib import inspect @@ -24,7 +46,7 @@ def is_float(value: str) -> bool: ---- value: The string to check. - Returns + Returns: ------- True if the string can be converted to a float, False otherwise. @@ -46,7 +68,7 @@ def is_code(value: str) -> bool: ---- value: The string to check. - Returns + Returns: ------- True if the string represents code, False otherwise. @@ -65,7 +87,7 @@ def is_arg(value: str) -> bool: ---- value: The string to check. - Returns + Returns: ------- True if the string represents an argument, False otherwise. @@ -81,7 +103,7 @@ def extract_modules(ast_tree: ast.AST) -> list[str]: ---- ast_tree (ast.AST): The abstract syntax tree parsed from the input string. - Returns + Returns: ------- list[str]: A list of strings, where each string represents a module or submodule. @@ -122,7 +144,7 @@ class BTParser: This class takes an XML file and a dictionary of behavior tree classes, and uses them to construct a behavior tree. - Attributes + Attributes: ---------- file (str): The XML file to parse. logger (logging.Logger): A logger for debugging and error messages. @@ -139,6 +161,7 @@ def __init__( file: str, log_level: logging.LoggingSeverity = logging.LoggingSeverity.INFO, ): + """Initialize the BTParser.""" self.file = file self.logger = rclpy.logging.get_logger("BTParser") @@ -152,11 +175,11 @@ def _get_handle(self, value: str) -> tuple[str, Any]: ---- value (str): The string to retrieve the handle from. - Returns + Returns: ------- A tuple containing the module name and the handle. - Raises + Raises: ------ KeyError: If the node_type is not an expected type. @@ -168,7 +191,7 @@ def _get_handle(self, value: str) -> tuple[str, Any]: try: module_name, obj_name = value.rsplit(".", 1) except ValueError as ex: - raise KeyError(f"Error parsing handle: {ex}") + raise KeyError("Error parsing handle") from ex try: module = importlib.import_module(module_name) @@ -210,7 +233,7 @@ def _string_num_or_code(self, value: str) -> Any: ---- value: The string to convert. - Returns + Returns: ------- The converted value. @@ -235,7 +258,7 @@ def _get_kwargs(self, params: str) -> dict: ---- params (str): The string to retrieve keyword arguments from. - Returns + Returns: ------- A dictionary of keyword arguments. @@ -266,7 +289,7 @@ def _convert_attribs(self, node_attribs: dict) -> dict: ---- node_attribs (dict): The attributes of the XML node. - Returns + Returns: ------- A dictionary of converted attributes. @@ -293,11 +316,11 @@ def _create_node( children (list): A list of child nodes. node_attribs (dict): A dictionary of node attributes. - Returns + Returns: ------- The created node. - Raises + Raises: ------ KeyError: If the node_type is not an expected type. BTParseError: If the parsed obj cannot be parsed correctly. @@ -379,20 +402,25 @@ def _sub_args(self, args, var): return None - def _build_tree(self, xml_node: ElementTree, args: dict = {}) -> py_trees.behaviour.Behaviour: + def _build_tree( + self, xml_node: ElementTree, args: dict | None = None + ) -> py_trees.behaviour.Behaviour: """ Build the behavior tree from an XML node. Args: ---- xml_node (ElementTree): The XML node to build the tree from. - args (list[tuple[str, str]]): Arguments for substitutions in elements. + args (list[tuple[str, str]]): Arguments for substitutions in elements, default None. - Returns + Returns: ------- The built behavior tree. """ + if args is None: + args = {} + if xml_node is None: self.logger.warn("Received an xml_node of type None this shouldn't happen") return None @@ -433,7 +461,7 @@ def parse(self) -> py_trees.behaviour.Behaviour: """ Parse the XML file and build the behavior tree. - Returns + Returns: ------- The built behavior tree. diff --git a/pyproject.toml b/pyproject.toml index 7e94c64..433b6fb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,3 +42,9 @@ ignore = [ [tool.ruff.lint.pydocstyle] # limit `pydocstyle` rules to those that adhere to the Google convention convention = "google" + +[tool.ruff.lint.per-file-ignores] +"setup.py" = ["D100"] +"test/*" = ["B011"] +"test/test_copyright.py" = ["D"] +"py_trees_parser/behaviors/testing_behaviors.py" = ["D", "UP"] diff --git a/setup.py b/setup.py index 6b656d4..9829468 100644 --- a/setup.py +++ b/setup.py @@ -1,3 +1,16 @@ +# Copyright 2025 SAM-XL +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. import os from glob import glob diff --git a/test/test_flake8.py b/test/test_flake8.py deleted file mode 100644 index 22fffcb..0000000 --- a/test/test_flake8.py +++ /dev/null @@ -1,23 +0,0 @@ -# Copyright 2017 Open Source Robotics Foundation, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import pytest -from ament_flake8.main import main_with_errors - - -@pytest.mark.flake8 -@pytest.mark.linter -def test_flake8(): - rc, errors = main_with_errors(argv=[]) - assert rc == 0, "Found %d code style errors / warnings:\n" % len(errors) + "\n".join(errors) diff --git a/test/test_parser.py b/test/test_parser.py index 0b4c09a..48b8296 100644 --- a/test/test_parser.py +++ b/test/test_parser.py @@ -1,3 +1,24 @@ +# Copyright 2025 SAM-XL +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +Tests for the BTParser module. + +These tests cover parsing behavior tree XML files, validating the +structure and contents, and ensuring the resulting py_trees.behaviour.Behaviour +instances are valid. +""" + import os import py_trees @@ -15,6 +36,7 @@ @pytest.fixture(scope="module") def ros_init(): + """Initialize ros.""" rclpy.init() yield rclpy.shutdown() @@ -22,6 +44,8 @@ def ros_init(): @pytest.fixture def setup_parser(ros_init): + """Setup the parser and test file processing.""" + def _setup(tree_file): xml = os.path.join(SHARE_DIR, tree_file) parser = BTParser(xml, log_level=rclpy.logging.LoggingSeverity.DEBUG) @@ -47,10 +71,12 @@ def _setup(tree_file): ], ) def test_tree_parser(setup_parser, tree_file): + """Test parser for the given tree files.""" _ = setup_parser(tree_file) def test_subtree_and_args(setup_parser): + """Test that subtree arguments are working as expected.""" tree_file = "test/data/test_args.xml" root = setup_parser(tree_file) @@ -62,4 +88,4 @@ def test_subtree_and_args(setup_parser): assert child.name == "Flip Eggs" assert child.period == 2 else: - assert False, f"Unexpected child node type {type(child)}" + assert False, f"Unexpected child node type {type(child)}" # noqa diff --git a/test/test_pep257.py b/test/test_pep257.py deleted file mode 100644 index 4eddb46..0000000 --- a/test/test_pep257.py +++ /dev/null @@ -1,23 +0,0 @@ -# Copyright 2015 Open Source Robotics Foundation, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import pytest -from ament_pep257.main import main - - -@pytest.mark.linter -@pytest.mark.pep257 -def test_pep257(): - rc = main(argv=[".", "test"]) - assert rc == 0, "Found code style errors / warnings" From 19f5cea9e0c231d77991f54aa63bff5d32655fdb Mon Sep 17 00:00:00 2001 From: Erich L Foster Date: Wed, 22 Jan 2025 13:52:51 +0000 Subject: [PATCH 15/30] Remove commented lines in action --- .github/workflows/testing.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index 299784f..ff75c1d 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -26,5 +26,3 @@ jobs: env: # either pass all entries explicitly ROS_DISTRO: ${{ matrix.ROS_DISTRO }} ROS_REPO: ${{ matrix.ROS_REPO }} - # with: # or pass the full matrix as config - # config: ${{toJSON(matrix)}} From 57cae047c2417580bc7b0e69a613b485a8e2ba39 Mon Sep 17 00:00:00 2001 From: Erich L Foster Date: Wed, 22 Jan 2025 13:58:33 +0000 Subject: [PATCH 16/30] Add linting test --- .ci/python/lint.sh | 25 ++++++++++++++++ test/test_python_formatting_check.py | 45 ++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+) create mode 100644 .ci/python/lint.sh create mode 100644 test/test_python_formatting_check.py diff --git a/.ci/python/lint.sh b/.ci/python/lint.sh new file mode 100644 index 0000000..27d9bfc --- /dev/null +++ b/.ci/python/lint.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash +# Copyright 2025 SAM-XL +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +set -ex + +pip install ruff==0.0.92 + +# lint the code (replaces flake8, isort, etc.) +ruff check . + +# check if formatting is correct (~ equivalent of `black format --check`) +ruff format --check . + diff --git a/test/test_python_formatting_check.py b/test/test_python_formatting_check.py new file mode 100644 index 0000000..0ff07d1 --- /dev/null +++ b/test/test_python_formatting_check.py @@ -0,0 +1,45 @@ +# Copyright 2025 SAM-XL +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""The path of the Python linting script used by the CI/CD pipeline.""" + +import subprocess +from pathlib import Path + +SCRIPT_FILEPATH = Path(".ci/python/lint.sh") + + +class FormattingError(Exception): + """ + A custom exception raised for formatting-related errors. + + This exception is intended to signal formatting errors using the same + script as the CI/CD pipeline uses. + """ + + pass + + +def test_python_format_check(request): + """Test if the Python code is formatted correctly.""" + rootdir = request.config.rootdir + # Run the same linting script as the CI/CD pipeline uses to check the formatting + result = subprocess.run( + ["bash", rootdir / SCRIPT_FILEPATH], + capture_output=True, + text=True, + ) + try: + result.check_returncode() + except subprocess.CalledProcessError as ex: + raise FormattingError(result.stdout + result.stderr) from ex From da5d5cffdd29824e2f077dc54726dd2ab3ce8f9c Mon Sep 17 00:00:00 2001 From: Erich L Foster Date: Wed, 22 Jan 2025 15:00:46 +0000 Subject: [PATCH 17/30] Fix copyright testing --- CONTRIBUTING.md | 13 ++++++++ .../behaviors/testing_behaviors.py | 31 +++++++++++++++++-- test/test_copyright.py | 1 - 3 files changed, 41 insertions(+), 4 deletions(-) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..6f63de9 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,13 @@ +Any contribution that you make to this repository will +be under the Apache 2 License, as dictated by that +[license](http://www.apache.org/licenses/LICENSE-2.0.html): + +~~~ +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. +~~~ diff --git a/py_trees_parser/behaviors/testing_behaviors.py b/py_trees_parser/behaviors/testing_behaviors.py index 87bdb43..81e1200 100644 --- a/py_trees_parser/behaviors/testing_behaviors.py +++ b/py_trees_parser/behaviors/testing_behaviors.py @@ -1,11 +1,36 @@ -"""Behaviours for the tutorials.""" +# Copyright 2025 Daniel Stonier +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. # -# License: BSD -# https://github.com/splintered-reality/py_trees_ros_tutorials/raw/devel/LICENSE +# * Neither the name of the Daniel Stonier nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. # +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + + ############################################################################## # Documentation ############################################################################## +"""Behaviours for the tutorials.""" ############################################################################## # Imports diff --git a/test/test_copyright.py b/test/test_copyright.py index 60c2d1e..2881f31 100644 --- a/test/test_copyright.py +++ b/test/test_copyright.py @@ -17,7 +17,6 @@ # Remove the `skip` decorator once the source file(s) have a copyright header -@pytest.mark.skip(reason="No copyright header has been placed in the generated source file.") @pytest.mark.copyright @pytest.mark.linter def test_copyright(): From 76f97f3fabf0fb19275f2a01ed9934b80078f2c7 Mon Sep 17 00:00:00 2001 From: Erich L Foster Date: Wed, 22 Jan 2025 15:14:23 +0000 Subject: [PATCH 18/30] Fix ruff version --- .ci/python/lint.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/python/lint.sh b/.ci/python/lint.sh index 27d9bfc..9fb569d 100644 --- a/.ci/python/lint.sh +++ b/.ci/python/lint.sh @@ -15,7 +15,7 @@ set -ex -pip install ruff==0.0.92 +pip install ruff==0.9.2 # lint the code (replaces flake8, isort, etc.) ruff check . From 1a7f56825297050d770e65a2bca74b933f215d1d Mon Sep 17 00:00:00 2001 From: Erich L Foster Date: Fri, 24 Jan 2025 07:53:01 +0000 Subject: [PATCH 19/30] Update change log --- CHANGELOG.rst | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 8879424..b47400a 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,10 @@ -Changelog for package behavior_tree +Changelog for package py_trees_parser .. This is only a rough description of the main changes of the repository +0.6.0 (2025-01-24) +------------------ +* Split py_trees_parser out into its own repo + 0.5.0 (2024-10-14) ------------------ * Use `args` in subtrees From ad4c0478f291f0cdaa6609722d90d259e6ff8a19 Mon Sep 17 00:00:00 2001 From: Erich L Foster Date: Mon, 27 Jan 2025 14:08:33 +0100 Subject: [PATCH 20/30] Add .local/bin to PATH for linting --- .ci/python/lint.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/.ci/python/lint.sh b/.ci/python/lint.sh index 9fb569d..76f8402 100644 --- a/.ci/python/lint.sh +++ b/.ci/python/lint.sh @@ -15,6 +15,7 @@ set -ex +export PATH=$PATH:$HOME/.local/bin pip install ruff==0.9.2 # lint the code (replaces flake8, isort, etc.) From 6b676c44a8ecc8b71f969a560f5616c2f5a53ef7 Mon Sep 17 00:00:00 2001 From: Erich L Foster Date: Mon, 27 Jan 2025 14:57:57 +0100 Subject: [PATCH 21/30] Add .gitignore --- .gitignore | 68 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cada3e7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,68 @@ +# Python +__pycache__/ +*.py[cod] +*.pyo +*.pyd +*.egg-info/ +dist/ +build/ +.eggs/ +*.egg + +# ROS 2 specific +build/ +install/ +log/ +*.so +*.dSYM/ +*.swp +*.swo + +# VS Code settings +.vscode/ +*.code-workspace + +# Pytest +.cache/ +*.cover +*.coverage +nosetests.xml +coverage.xml +*.log + +# Jupyter Notebook +.ipynb_checkpoints + +# Environment variables +.env +.venv/ +venv/ +env/ +ENV/ +env.bak/ +venv.bak/ + +# MacOS +.DS_Store + +# Linux +*~ + +# Temporary files +*.tmp +*.temp +*.bak +*.orig +*.swp +*.swo + +# IDE specific +.idea/ +*.iml +*.ipr +*.iws + +# Other +*.log +*.out +*.err From b6e810fbfc67a43c06f84f0aafcdef591fce15ca Mon Sep 17 00:00:00 2001 From: Erich L Foster Date: Tue, 28 Jan 2025 10:30:08 +0000 Subject: [PATCH 22/30] Fix cascading args and add a test Playing with args Remove unused import --- py_trees_parser/parser.py | 75 +++++++++++++++++++++--------- test/data/test1.xml | 4 +- test/data/test6.xml | 8 ++-- test/data/test_args.xml | 3 +- test/data/test_cascade_args.xml | 7 +++ test/data/test_idioms.xml | 2 +- test/data/test_subtree_args.xml | 2 +- test/data/test_subtree_cascade.xml | 6 +++ test/data/test_subtree_main.xml | 4 +- test/data/test_subtree_sub.xml | 2 +- test/test_parser.py | 16 +++++++ 11 files changed, 93 insertions(+), 36 deletions(-) create mode 100644 test/data/test_cascade_args.xml create mode 100644 test/data/test_subtree_cascade.xml diff --git a/py_trees_parser/parser.py b/py_trees_parser/parser.py index 59bb4ea..8a94258 100644 --- a/py_trees_parser/parser.py +++ b/py_trees_parser/parser.py @@ -25,7 +25,8 @@ import inspect import types from typing import Any -from xml.etree import ElementInclude, ElementTree +from xml.etree import ElementTree +from xml.etree.ElementTree import Element import py_trees import rclpy @@ -227,7 +228,7 @@ def _parse_code(self, value: str) -> Any: def _string_num_or_code(self, value: str) -> Any: """ - Convert a string to either an integer, float, code, or leaves it as a string. + Convert a string to either an integer, float, code, or leave it as a string. Args: ---- @@ -375,16 +376,19 @@ def _create_node( return node - def _process_args(self, xml_node: ElementTree, args: dict) -> None: + def _process_args(self, xml_node: Element, args: dict) -> None: """ Substitute arguments in the subtree. Args: ---- - xml_node (ElementTree): The XML node to substitute arguments in. + xml_node (Element): The XML node to substitute arguments in. args (dict[str, str]): Arguments to substitute in the subtree. """ + if len(args) == 0: + return + for attr_name, attr_value in list(xml_node.attrib.items()): arg_value = self._sub_args(args, attr_value) if arg_value is not None: @@ -403,14 +407,16 @@ def _sub_args(self, args, var): return None def _build_tree( - self, xml_node: ElementTree, args: dict | None = None + self, + xml_node: Element, + args: dict | None = None, ) -> py_trees.behaviour.Behaviour: """ Build the behavior tree from an XML node. Args: ---- - xml_node (ElementTree): The XML node to build the tree from. + xml_node (Element): The XML node to build the tree from. args (list[tuple[str, str]]): Arguments for substitutions in elements, default None. Returns: @@ -420,36 +426,37 @@ def _build_tree( """ if args is None: args = {} + else: + self.logger.debug(f"{args = }") if xml_node is None: self.logger.warn("Received an xml_node of type None this shouldn't happen") return None + self._process_args(xml_node, args) + if xml_node.tag.lower() == "subtree": - self.logger.debug(f"Found subtree: {xml_node.attrib.get('name')}") + subtree_name = xml_node.attrib.get("name") + include = self._string_num_or_code(xml_node.attrib.get("include")) + self.logger.debug(f"Found subtree: {subtree_name}, {include}") new_args = {} for child_xml in xml_node: if child_xml.tag.lower() == "arg": # create argument dict + self._process_args(child_xml, args) name = child_xml.attrib.get("name") - if (value := self._sub_args(args, child_xml.attrib.get("value"))) is not None: - new_args[name] = value - else: - new_args[name] = child_xml.attrib.get("value") - + new_args[name] = child_xml.attrib.get("value") self.logger.debug(f"Found arg: {name} = {new_args[name]}") else: # no more args so parse subtree - self.logger.debug( - f"Parsing subtree starting with tag: {child_xml.tag.lower()}" + raise AttributeError( + f"Unexpected tag in subtree ({subtree_name}): {child_xml.tag.lower()}" ) - self._process_args(child_xml, new_args) - args.update(new_args) - return self._build_tree(child_xml, args) + return self._build_tree(self._get_xml(include), {**args, **new_args}) # we only need to find children if the node is a composite children = list() for child_xml in xml_node: self._process_args(child_xml, args) - child = self._build_tree(child_xml) + child = self._build_tree(child_xml, args) children.append(child) # build the actual node @@ -457,6 +464,33 @@ def _build_tree( return node + def _get_xml(self, file) -> Element: + """ + Load the XML file as an ElementTree. + + Args: + ---- + file (str): The path to the XML file. + + Returns: + ------- + The root element of the XML file. + + Raises: + ------ + FileNotFoundError: If the XML file cannot be found. + + """ + try: + with open(file) as f: + xml_str = f.read() + except FileNotFoundError as ex: + self.logger.error(f"XML file {file} not found") + raise FileNotFoundError(f"XML file {file} not found") from ex + + root = ElementTree.fromstring(xml_str) + return root + def parse(self) -> py_trees.behaviour.Behaviour: """ Parse the XML file and build the behavior tree. @@ -466,9 +500,6 @@ def parse(self) -> py_trees.behaviour.Behaviour: The built behavior tree. """ - xml = ElementTree.parse(self.file) - - root = xml.getroot() - ElementInclude.include(root, base_url=self.file) + root = self._get_xml(self.file) return self._build_tree(root) diff --git a/test/data/test1.xml b/test/data/test1.xml index 9a50406..dfbc2ce 100644 --- a/test/data/test1.xml +++ b/test/data/test1.xml @@ -2,13 +2,13 @@ https://py-trees-ros-tutorials.readthedocs.io/en/release-2.1.x/tutorials.html#tree --> - + - + diff --git a/test/data/test6.xml b/test/data/test6.xml index 3149e5c..6cbed45 100644 --- a/test/data/test6.xml +++ b/test/data/test6.xml @@ -2,7 +2,7 @@ https://py-trees-ros-tutorials.readthedocs.io/en/release-2.1.x/tutorials.html#id13 --> - + - + - + - + diff --git a/test/data/test_args.xml b/test/data/test_args.xml index a950c89..73679c1 100644 --- a/test/data/test_args.xml +++ b/test/data/test_args.xml @@ -1,7 +1,6 @@ - + - diff --git a/test/data/test_cascade_args.xml b/test/data/test_cascade_args.xml new file mode 100644 index 0000000..7533e5d --- /dev/null +++ b/test/data/test_cascade_args.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/test/data/test_idioms.xml b/test/data/test_idioms.xml index 41a2cc7..15d21d4 100644 --- a/test/data/test_idioms.xml +++ b/test/data/test_idioms.xml @@ -1,4 +1,4 @@ - + diff --git a/test/data/test_subtree_args.xml b/test/data/test_subtree_args.xml index b6edf39..a6553c6 100644 --- a/test/data/test_subtree_args.xml +++ b/test/data/test_subtree_args.xml @@ -1,4 +1,4 @@ - + diff --git a/test/data/test_subtree_cascade.xml b/test/data/test_subtree_cascade.xml new file mode 100644 index 0000000..308e317 --- /dev/null +++ b/test/data/test_subtree_cascade.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/test/data/test_subtree_main.xml b/test/data/test_subtree_main.xml index 12f35e5..c7b7481 100644 --- a/test/data/test_subtree_main.xml +++ b/test/data/test_subtree_main.xml @@ -4,7 +4,5 @@ - - - + diff --git a/test/data/test_subtree_sub.xml b/test/data/test_subtree_sub.xml index 1321006..6c847ec 100644 --- a/test/data/test_subtree_sub.xml +++ b/test/data/test_subtree_sub.xml @@ -1,4 +1,4 @@ - + diff --git a/test/test_parser.py b/test/test_parser.py index 48b8296..05496aa 100644 --- a/test/test_parser.py +++ b/test/test_parser.py @@ -89,3 +89,19 @@ def test_subtree_and_args(setup_parser): assert child.period == 2 else: assert False, f"Unexpected child node type {type(child)}" # noqa + + +def test_subtree_cascaded_args(setup_parser): + """Test that cascaded args through subtrees is working as expected.""" + tree_file = "test/data/test_cascade_args.xml" + root = setup_parser(tree_file) + + assert root.name == "Subtree Selector" + for child in root.children: + if isinstance(child, py_trees.behaviours.Running): + assert child.name == "Idle" + elif isinstance(child, py_trees.behaviours.Periodic): + assert child.name == "Flip Eggs" + assert child.period == 2 + else: + assert False, f"Unexpected child node type {type(child)}" # noqa From 4183600554b5485ed2782a8d075d0e66e3c60593 Mon Sep 17 00:00:00 2001 From: Erich L Foster Date: Thu, 30 Jan 2025 09:36:31 +0000 Subject: [PATCH 23/30] Update PR template --- .github/pull_request_template.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 3ea2112..1798c11 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -30,5 +30,5 @@ Why is this change implemented? Add screenshots, images, or other media if this This should contain instructions on how to test the new code, or details on how the new code was tested, e.g. -1. `colcon build --symlink-install --merge-install --packages-up-to py_trees_parser` -2. `colcon test --merge-install --event-handlers console_cohesion+ --packages-select py_trees_parser` +1. `colcon build --packages-up-to py_trees_parser` +2. `colcon test --event-handlers console_cohesion+ --packages-select py_trees_parser` From 31f1c9b9814fcd2a2688cd3c58f2eb7695d54c1b Mon Sep 17 00:00:00 2001 From: Erich L Foster Date: Thu, 30 Jan 2025 09:36:46 +0000 Subject: [PATCH 24/30] Pin py_trees_ros to a 2.3.0 --- .repos | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.repos b/.repos index c26e440..e043886 100644 --- a/.repos +++ b/.repos @@ -7,4 +7,4 @@ repositories: vendors/py_trees_ros: type: git url: https://github.com/splintered-reality/py_trees_ros.git - version: 1729e289a4d7783dfe3b633bf4d144f3526fce0c + version: 2.3.0 From a041d5755cb15f0ec13abf82e993df8648ad8519 Mon Sep 17 00:00:00 2001 From: Erich L Foster Date: Thu, 30 Jan 2025 09:37:07 +0000 Subject: [PATCH 25/30] Update README and changelog --- CHANGELOG.rst | 24 ++-------------- README.md | 76 ++++++++++++++++++++------------------------------- 2 files changed, 33 insertions(+), 67 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index b47400a..1af1906 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -11,12 +11,7 @@ Changelog for package py_trees_parser 0.4.0 (2024-09-18) ------------------ -* Make `ImageDict` more encapsulated, by adding setters and getters -* Remove Thermoplast specific sensors from `ImageDict` -* Generalize `Robot._sensors` and its subscriptions -* Generalize `Robot._triggers` and its services -* Fix bug in `camera_info` subscription that used wrong data type -* Improve encapsulation of Robot +* No relevant changes for py_trees_parser 0.3.0 (2024-09-03) ------------------ @@ -28,22 +23,9 @@ Changelog for package py_trees_parser * Bump version * Add `Changelog` -0.1.7 (2024-08-14) ------------------- -* Add Joint motion behaviors: `MoveToNamedTarget` and `MoveJoint` -* Create `Pick and Place` pipeline - -0.1.4 (2024-08-02) ------------------- -* Add a `RepeatFromBlackboard` Behavior - -0.1.2 (2024-07-24) ------------------- -* Add a `SetABBIOSignal` Behavior - 0.1.0 (2024-07-23) ------------------ -* Create a cartesian motion behavior `MoveCartesian` +* No relevant changes for py_trees_parser 0.0.3 (2024-07-03) ------------------ @@ -51,4 +33,4 @@ Changelog for package py_trees_parser 0.0.2 (2024-04-02) ------------------ -* First release of behavior_tree +* First release of py_trees_parser diff --git a/README.md b/README.md index 2f4d549..e466b6e 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,8 @@ This is a xml parser for processing and building a py_trees behavior tree. The hope is that most if not all capabilities of py_trees will be available for xml -parsing. As such, a py_trees behavior tree will be able to be created by simply -creating an xml file. +parsing. As such, a py_trees behavior tree can be created by simply creating an +xml file. ## Dependencies @@ -30,10 +30,14 @@ parse an XML file representing a behavior tree and construct the corresponding behavior tree using the `py_trees` library. It supports composite nodes, behavior nodes from `py_trees`, and custom behavior nodes defined in your local library. +### Python Interpreter + For any parameter that is python code, the code must be surrounded by `$()`. This allows the parser know that the following parameter value is in fact code and should be evaluated as code. +### Idioms + Idioms are also now supported. An idiom is a special function that produces a behavior tree, the function is expected to either take no children, takes a list of children via parameters "subtrees", or takes a single @@ -41,6 +45,8 @@ child via parameter "behavior". The xml parser will treat children in the same way that it treats children for all other behaviors. That is the children should be a subnode of the idiom node. +### Subtrees + Additionally, it is possible to create a subtree, where a subtree is an xml containing a complete behavior tree. This xml file can be included in other xml files and therefore allows for complete modularity of trees. @@ -87,7 +93,7 @@ behavior_tree = parser.parse() ### Using Your Own Behaviors The xml parser can use any behavior, whether it is part of `py_trees`, `py_trees_ros`, -or your own python module. The way the parser knows the existance of +or your own python module. The way the parser knows the existence of the behavior is via the behavior tag in the xml. The behavior tag should be the fully qualified python path of the behavior, so if you have the following structure of your python module @@ -125,60 +131,36 @@ It is possible to include sub-trees in the xml file containing a behavior tree. This is made possible via the following: ```xml - - - + ``` The included subtree should be a complete tree, but can only contain one root. It is possible to include multiple sub-trees and a sub-tree can also include -another subtree. However, be aware that the all directories are reltative to -the main. For example, if we have four behavior tree files in a directory like -so: - -``` -config -├── main.xml -└── subtree - ├── subtree1.xml - ├── subtree2.xml - └── subtree3.xml -``` - -and the following is our main.xml +another subtree. However, be aware that the all directories are absolute, but it +is possible to use python to determine the path like so: ```xml - - - - - - + ``` - -and we then include `subtree3.xml` in `subtree1.xml` the location of -`subtree3.xml` must still be relative to `main.xml`: - -```xml - - - -``` - #### Arguments It is also possible to use arguments for subtrees. The syntax of which looks like ```xml - + - ``` -with subtree +and then inside the subtree ```xml @@ -189,22 +171,24 @@ with subtree Furthermore, one can cascade arguments down subtrees using the following syntax: ```xml - + - ``` -with subtree1 +and in subtree1 ```xml - - + + ``` -and finally, subtree2 +and finally in subtree2 ```xml From e9dc63a4f79c6608f7f3beeb0edc7de8ae858fa8 Mon Sep 17 00:00:00 2001 From: Erich L Foster Date: Thu, 30 Jan 2025 13:36:25 +0000 Subject: [PATCH 26/30] Fix docstring --- py_trees_parser/parser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/py_trees_parser/parser.py b/py_trees_parser/parser.py index 8a94258..f4aa371 100644 --- a/py_trees_parser/parser.py +++ b/py_trees_parser/parser.py @@ -417,7 +417,7 @@ def _build_tree( Args: ---- xml_node (Element): The XML node to build the tree from. - args (list[tuple[str, str]]): Arguments for substitutions in elements, default None. + args (dict[tuple[str, str]]): Arguments for substitutions in elements, default None. Returns: ------- From 46a410f0c47f08d9855b94b91ada6ea9cd84c760 Mon Sep 17 00:00:00 2001 From: Erich L Foster Date: Thu, 30 Jan 2025 13:47:05 +0000 Subject: [PATCH 27/30] Update test to include a nested subtree for cascading args --- test/data/test_subtree_cascade.xml | 14 ++++++++------ test/test_parser.py | 6 ++++-- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/test/data/test_subtree_cascade.xml b/test/data/test_subtree_cascade.xml index 308e317..1413832 100644 --- a/test/data/test_subtree_cascade.xml +++ b/test/data/test_subtree_cascade.xml @@ -1,6 +1,8 @@ - - - - - - + + + + + + + + diff --git a/test/test_parser.py b/test/test_parser.py index 05496aa..9b48d7d 100644 --- a/test/test_parser.py +++ b/test/test_parser.py @@ -81,6 +81,7 @@ def test_subtree_and_args(setup_parser): root = setup_parser(tree_file) assert root.name == "Subtree Selector" + for child in root.children: if isinstance(child, py_trees.behaviours.Running): assert child.name == "Idle" @@ -92,11 +93,12 @@ def test_subtree_and_args(setup_parser): def test_subtree_cascaded_args(setup_parser): - """Test that cascaded args through subtrees is working as expected.""" + """Test that cascaded args through subtrees and nested subtrees is working as expected.""" tree_file = "test/data/test_cascade_args.xml" - root = setup_parser(tree_file) + root = setup_parser(tree_file).children[0] assert root.name == "Subtree Selector" + for child in root.children: if isinstance(child, py_trees.behaviours.Running): assert child.name == "Idle" From 02f2d8055ae2630199a5ae5cec5005fa9c7347b3 Mon Sep 17 00:00:00 2001 From: Erich L Foster Date: Fri, 31 Jan 2025 08:29:04 +0000 Subject: [PATCH 28/30] Remove unused dependencies --- package.xml | 2 -- 1 file changed, 2 deletions(-) diff --git a/package.xml b/package.xml index 66ab5e9..e15a5e0 100644 --- a/package.xml +++ b/package.xml @@ -14,8 +14,6 @@ python3-setuptools ament_copyright - ament_flake8 - ament_pep257 python3-pytest From 83c378887411b1a6b89581095cecc8998f3a6b49 Mon Sep 17 00:00:00 2001 From: Erich L Foster Date: Fri, 31 Jan 2025 08:34:38 +0000 Subject: [PATCH 29/30] Add example of python interpreter --- README.md | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index e466b6e..fc2d7a1 100644 --- a/README.md +++ b/README.md @@ -30,11 +30,25 @@ parse an XML file representing a behavior tree and construct the corresponding behavior tree using the `py_trees` library. It supports composite nodes, behavior nodes from `py_trees`, and custom behavior nodes defined in your local library. +### Examples + +For examples see `test/data/`. + ### Python Interpreter For any parameter that is python code, the code must be surrounded by `$()`. This allows the parser know that the following parameter value is in fact code -and should be evaluated as code. +and should be evaluated as code. For a concrete example see below: + +```xml + +``` + +In the above example the `qos_profile` is evaluated as python code. Notice to +use any python module you must use the fully qualified name. ### Idioms @@ -47,7 +61,7 @@ children should be a subnode of the idiom node. ### Subtrees -Additionally, it is possible to create a subtree, where a subtree is an xml +Additionally, it is possible to create a [subtree](#sub-trees), where a subtree is an xml containing a complete behavior tree. This xml file can be included in other xml files and therefore allows for complete modularity of trees. From c8e97224b1a60a2b9f274b38ba7050df329e8943 Mon Sep 17 00:00:00 2001 From: Erich L Foster Date: Fri, 31 Jan 2025 12:20:41 +0000 Subject: [PATCH 30/30] Fix minor typos --- .ci/python/lint.sh | 2 +- README.md | 2 +- package.xml | 2 +- py_trees_parser/__init__.py | 2 +- py_trees_parser/parser.py | 4 ++-- setup.py | 4 ++-- test/test_parser.py | 2 +- test/test_python_formatting_check.py | 5 +++-- 8 files changed, 12 insertions(+), 11 deletions(-) diff --git a/.ci/python/lint.sh b/.ci/python/lint.sh index 76f8402..70251e5 100644 --- a/.ci/python/lint.sh +++ b/.ci/python/lint.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -# Copyright 2025 SAM-XL +# Copyright 2025 SAM XL # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/README.md b/README.md index fc2d7a1..8c29f80 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ name. ## XML Parser -The Behavior Tree Parser (`BTParse`) is a Python module that allows you to +The Behavior Tree Parser (`BTParser`) is a Python class that allows you to parse an XML file representing a behavior tree and construct the corresponding behavior tree using the `py_trees` library. It supports composite nodes, behavior nodes from `py_trees`, and custom behavior nodes defined in your local library. diff --git a/package.xml b/package.xml index e15a5e0..e0fa8d4 100644 --- a/package.xml +++ b/package.xml @@ -5,7 +5,7 @@ 0.6.0 A py_trees xml parser Erich L Foster - apache 2.0 + Apache-2.0 ament_index_python py_trees diff --git a/py_trees_parser/__init__.py b/py_trees_parser/__init__.py index 9676319..843ceb1 100644 --- a/py_trees_parser/__init__.py +++ b/py_trees_parser/__init__.py @@ -1,4 +1,4 @@ -# Copyright 2025 SAM-XL +# Copyright 2025 SAM XL # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/py_trees_parser/parser.py b/py_trees_parser/parser.py index f4aa371..eccfe78 100644 --- a/py_trees_parser/parser.py +++ b/py_trees_parser/parser.py @@ -1,4 +1,4 @@ -# Copyright 2025 SAM-XL +# Copyright 2025 SAM XL # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -417,7 +417,7 @@ def _build_tree( Args: ---- xml_node (Element): The XML node to build the tree from. - args (dict[tuple[str, str]]): Arguments for substitutions in elements, default None. + args (dict[str, str]): Arguments for substitutions in elements, default None. Returns: ------- diff --git a/setup.py b/setup.py index 9829468..de4cdb3 100644 --- a/setup.py +++ b/setup.py @@ -1,4 +1,4 @@ -# Copyright 2025 SAM-XL +# Copyright 2025 SAM XL # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -35,7 +35,7 @@ maintainer="Erich L Foster", maintainer_email="e.l.f.foster@tudelft.nl", description="A py_trees xml parser", - license="apache 2.0", + license="Apache-2.0", tests_require=["pytest"], entry_points={}, ) diff --git a/test/test_parser.py b/test/test_parser.py index 9b48d7d..cc4a9ea 100644 --- a/test/test_parser.py +++ b/test/test_parser.py @@ -1,4 +1,4 @@ -# Copyright 2025 SAM-XL +# Copyright 2025 SAM XL # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/test/test_python_formatting_check.py b/test/test_python_formatting_check.py index 0ff07d1..a3a2727 100644 --- a/test/test_python_formatting_check.py +++ b/test/test_python_formatting_check.py @@ -1,4 +1,4 @@ -# Copyright 2025 SAM-XL +# Copyright 2025 SAM XL # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -11,11 +11,12 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -"""The path of the Python linting script used by the CI/CD pipeline.""" +"""Test for linting of all python files.""" import subprocess from pathlib import Path +"""The path of the Python linting script used by the CI/CD pipeline.""" SCRIPT_FILEPATH = Path(".ci/python/lint.sh")