From 9db767f134f9da5b642de10cd032d40c1738a20e Mon Sep 17 00:00:00 2001 From: Peter Corke Date: Mon, 20 Jul 2026 19:41:16 +1000 Subject: [PATCH 1/2] fix(models): rename list() to catalog(), fix LBR, add robot_descriptions listing models.list() shadowed the builtin list, flagged as tech debt last session. Renamed src/roboticstoolbox/models/list.py -> catalog.py, function list() -> catalog(). list() is kept as a deprecated FutureWarning-emitting shim forwarding to catalog(), to be dropped in a future major version. Docs (arm_dh.rst/arm_erobot.rst) and tests/test_models.py updated to call catalog() directly. While testing catalog(), found LBR (Kuka) was the one model that fails to load: its bundled xacro file does $(find kuka_lbr_iiwa_support) but rtb-data ships that content under the directory name "kuka_lbr_iiwa" (no "_support" suffix). The tech-debt.md entry for this proposed renaming the bundled directory (requiring an rtb-data release), but that doesn't actually work -- verified empirically against a throwaway copy -- since xacrodoc's package auto-discovery doesn't register a directory as a package purely by name-matching. Fixed instead with a new extra_packages kwarg on URDFRobot.__init__/URDF_file (mirroring the existing patch= convention) that registers an explicit package-name alias. No rtb-data release needed. Also adds a second table to catalog(), listing models importable by bare name via robot_descriptions but not wrapped by a dedicated RTB class -- 123 URDF-loadable entries with static metadata (name/manufacturer/dof/tags), sourced directly from robot_descriptions.DESCRIPTIONS with no download or instantiation required. Co-Authored-By: Claude Sonnet 5 --- docs/source/arm_dh.rst | 2 +- docs/source/arm_erobot.rst | 4 +- src/roboticstoolbox/models/URDF/LBR.py | 5 + src/roboticstoolbox/models/URDF/URDFRobot.py | 19 +- src/roboticstoolbox/models/__init__.py | 4 +- src/roboticstoolbox/models/catalog.py | 181 +++++++++++++++++++ tech-debt.md | 67 +++---- tests/test_models.py | 22 +-- 8 files changed, 244 insertions(+), 60 deletions(-) create mode 100644 src/roboticstoolbox/models/catalog.py diff --git a/docs/source/arm_dh.rst b/docs/source/arm_dh.rst index 4535007de..87b0ce319 100644 --- a/docs/source/arm_dh.rst +++ b/docs/source/arm_dh.rst @@ -77,7 +77,7 @@ standard or modified. They can be listed by: .. runblock:: pycon >>> import roboticstoolbox as rtb - >>> rtb.models.list(mtype="DH") + >>> rtb.models.catalog(mtype="DH") .. automodule:: roboticstoolbox.models.DH :members: diff --git a/docs/source/arm_erobot.rst b/docs/source/arm_erobot.rst index 80401a94f..e4516f91d 100644 --- a/docs/source/arm_erobot.rst +++ b/docs/source/arm_erobot.rst @@ -49,7 +49,7 @@ They can be listed by: .. runblock:: pycon >>> import roboticstoolbox as rtb - >>> rtb.models.list(mtype="ETS") + >>> rtb.models.catalog(mtype="ETS") .. automodule:: roboticstoolbox.models.ETS :members: @@ -65,7 +65,7 @@ standard or modified. They can be listed by: .. runblock:: pycon >>> import roboticstoolbox as rtb - >>> rtb.models.list(mtype="URDF") + >>> rtb.models.catalog(mtype="URDF") .. automodule:: roboticstoolbox.models.URDF :members: diff --git a/src/roboticstoolbox/models/URDF/LBR.py b/src/roboticstoolbox/models/URDF/LBR.py index fdfa4f24e..350713b99 100644 --- a/src/roboticstoolbox/models/URDF/LBR.py +++ b/src/roboticstoolbox/models/URDF/LBR.py @@ -32,9 +32,14 @@ class LBR(URDFRobot): def __init__(self): + # The bundled file does $(find kuka_lbr_iiwa_support) to locate its + # own macro file, but rtb-data ships that content under the + # directory name "kuka_lbr_iiwa" (no "_support" suffix), so xacro's + # package lookup can't find it without this alias. super().__init__( "kuka_description/kuka_lbr_iiwa/urdf/lbr_iiwa_14_r820.xacro", manufacturer="Kuka", + extra_packages={"kuka_lbr_iiwa_support": "kuka_description/kuka_lbr_iiwa"}, ) # self.qdlim = np.array([ diff --git a/src/roboticstoolbox/models/URDF/URDFRobot.py b/src/roboticstoolbox/models/URDF/URDFRobot.py index a9fa6473d..805915f00 100644 --- a/src/roboticstoolbox/models/URDF/URDFRobot.py +++ b/src/roboticstoolbox/models/URDF/URDFRobot.py @@ -290,6 +290,7 @@ def URDF_file( file: "str | Path | TextIO", model: "str | None" = None, patch: "Callable[[str], str] | None" = None, + extra_packages: "dict[str, str] | None" = None, ) -> tuple: """Parse a URDF or xacro file, return (elinks, name). @@ -306,6 +307,14 @@ def URDF_file( the upstream repo or the bundled `rtb-data` copy. See ``Valkyrie.py`` and ``Fetch.py`` for real examples — each documents exactly which upstream bug its patch works around. + + ``extra_packages``, if given, maps additional xacro package names to + paths relative to the bundled ``rtb-data`` xacro root. Use it when an + upstream file does ``$(find some_package)`` for a package name that + doesn't match any directory name rtb-data actually ships under — xacro's + package lookup only auto-discovers directories that are already known by + name, it doesn't fall back to searching by content. See ``LBR.py`` for a + real example. """ import rtbdata @@ -313,6 +322,11 @@ def URDF_file( pkg_map = {d.name: str(d) for d in xacro_root.iterdir() if d.is_dir()} packages.update_package_cache(pkg_map) + if extra_packages is not None: + packages.update_package_cache( + {name: str(xacro_root / path) for name, path in extra_packages.items()} + ) + xacro_args = None if isinstance(file, str): file = Path(file) @@ -376,9 +390,12 @@ def __init__( manufacturer: str = "", gripper_link_index: "int | None" = None, patch: "Callable[[str], str] | None" = None, + extra_packages: "dict[str, str] | None" = None, **kwargs, ): - elinks, name, filepath = URDF_file(urdf_path, patch=patch) + elinks, name, filepath = URDF_file( + urdf_path, patch=patch, extra_packages=extra_packages + ) if gripper_link_index is not None: kwargs["gripper_links"] = elinks[gripper_link_index] super().__init__(elinks, name=name, manufacturer=manufacturer, **kwargs) diff --git a/src/roboticstoolbox/models/__init__.py b/src/roboticstoolbox/models/__init__.py index 8aa7a663b..e6ea7af0a 100644 --- a/src/roboticstoolbox/models/__init__.py +++ b/src/roboticstoolbox/models/__init__.py @@ -1,7 +1,7 @@ from roboticstoolbox.models.URDF import * # noqa from roboticstoolbox.models import ETS # noqa from roboticstoolbox.models import DH # noqa -from roboticstoolbox.models.list import list +from roboticstoolbox.models.catalog import catalog, list -__all__ = ["list", "ETS", "DH"] +__all__ = ["catalog", "list", "ETS", "DH"] diff --git a/src/roboticstoolbox/models/catalog.py b/src/roboticstoolbox/models/catalog.py new file mode 100644 index 000000000..8529fa5b7 --- /dev/null +++ b/src/roboticstoolbox/models/catalog.py @@ -0,0 +1,181 @@ +import warnings +from roboticstoolbox.robot.Robot import Robot +from roboticstoolbox.tools import rtb_get_param +from roboticstoolbox.robot.ERobot import ERobot2 +from roboticstoolbox.models.URDF.URDFRobot import _rd_link +from ansitable import ANSITable, Column +import inspect + +# import importlib + + +def catalog(keywords=None, dof=None, mtype=None, border="thin"): + """ + Display all robot models in summary form + + :param keywords: keywords to filter on, defaults to None + :type keywords: tuple of str, optional + :param dof: number of DoF to filter on, defaults to None + :type dof: int, optional + :param mtype: model type "DH", "ETS", "URDF", defaults to all types + :type mtype: str, optional + + - ``catalog()`` displays a list of all models provided by the Toolbox. + It lists the name, manufacturer, model type, number of DoF, and + keywords. It also lists models importable by name from + `robot_descriptions `_ + but not otherwise wrapped by the Toolbox. + + - ``catalog(mtype=MT)`` as above, but only displays Toolbox models of + type ``MT`` where ``MT`` is one of "DH", "ETS" or "URDF", and omits + the robot_descriptions listing. + + - ``catalog(keywords=KW)`` as above, but only displays models that have + a keyword in the tuple ``KW``. + + - ``catalog(dof=N)`` as above, but only display models that have ``N`` + degrees of freedom. + + The filters can be combined + + - ``catalog(keywords=KW, dof=N)`` are those models that have a keyword + in ``KW`` and have ``N`` degrees of freedom. + """ + + import roboticstoolbox.models as models + + # module = importlib.import_module( + # '.' + os.path.splitext(file)[0], package='bdsim.blocks') + + unicode = rtb_get_param("unicode") + if not unicode: + border = "ascii" + + def make_table(border=None): + table = ANSITable( + Column("class", headalign="^", colalign="<"), + Column("name", headalign="^", colalign="<"), + Column("manufacturer", headalign="^", colalign="<"), + Column("type", headalign="^", colalign="<"), + Column("DoF", colalign="<"), + Column("dims", colalign="<"), + Column("structure", colalign="<", width=16), + Column("dynamics", colalign="<"), + Column("geometry", colalign="<"), + Column("keywords", headalign="^", colalign="<"), + border=border, + ) + + if mtype is not None: + categories = [mtype] + else: + categories = ["DH", "URDF", "ETS"] + for category in categories: + # get all classes in this category + group = models.__dict__[category] + for cls in group.__dict__.values(): + if inspect.isclass(cls) and issubclass(cls, Robot): + # we found a BaseRobot subclass, instantiate it + try: + robot = cls() + except Exception: + print(f"failed to load {cls}") + continue + try: + structure = robot.structure + except Exception: # pragma nocover + structure = "" + + # apply filters + if keywords is not None: + if len(set(keywords) & set(robot.keywords)) == 0: + continue + if dof is not None and robot.n != dof: + continue # pragma nocover + + dims = 0 + + if isinstance(robot, ERobot2): + dims = 2 + else: + dims = 3 + # add the row + table.row( + cls.__name__, + robot.name, + robot.manufacturer, + category, + robot.n, + f"{dims}d", + structure, + "Y" if robot._hasdynamics else "", + "Y" if robot._hasgeometry else "", + ", ".join(robot.keywords), + ) + + table.print() + + def make_rd_table(border=None): + from robot_descriptions import DESCRIPTIONS + from robot_descriptions._descriptions import Format + + table = ANSITable( + Column("name", headalign="^", colalign="<"), + Column("robot", headalign="^", colalign="<"), + Column("manufacturer", headalign="^", colalign="<"), + Column("DoF", colalign="<"), + Column("keywords", headalign="^", colalign="<"), + border=border, + ) + + for key, description in sorted(DESCRIPTIONS.items()): + if Format.URDF not in description.formats: + continue + + name = key.removesuffix("_description").removesuffix("_official") + tags = sorted(description.tags) + + if keywords is not None: + if len(set(keywords) & set(tags)) == 0: + continue + if dof is not None and description.dof != dof: + continue + + table.row( + name, + description.robot, + description.maker, + description.dof if description.dof is not None else "", + ", ".join(tags), + ) + + print(f"\nImportable from {_rd_link()}\n") + table.print() + + make_table(border=border) + if mtype is None: + make_rd_table(border=border) + + +def list(keywords=None, dof=None, mtype=None, border="thin"): # pragma nocover + """ + Display all robot models in summary form (deprecated) + + :deprecated: 1.4.0 + ``list`` shadows the builtin and will be removed in a future + release. Use :func:`catalog` instead. + """ + warnings.warn( + "models.list() is deprecated and will be removed in a future " + "release, use models.catalog() instead", + FutureWarning, + stacklevel=2, + ) + return catalog(keywords=keywords, dof=dof, mtype=mtype, border=border) + + +if __name__ == "__main__": # pragma nocover + catalog(border="ascii") + catalog(keywords=("dynamics",), border="thin") + catalog(dof=6) + catalog(keywords=("dynamics",), dof=6) diff --git a/tech-debt.md b/tech-debt.md index 3d5a37e85..2a21c85d8 100644 --- a/tech-debt.md +++ b/tech-debt.md @@ -454,8 +454,7 @@ in flight. ### Bundled xacro tree has a naming mismatch: `LBR` (Kuka) -Found 2026-07-05 while investigating a runblock `unbound prefix`/`unknown -macro`/`PackageNotFoundError` sweep. `LBR.py` loads +Found 2026-07-05, fixed 2026-07-20. `LBR.py` loads `"kuka_description/kuka_lbr_iiwa/urdf/lbr_iiwa_14_r820.xacro"` from the bundled `rtb-data` xacro tree (an explicit `.xacro` suffix path, so this doesn't go through `robot_descriptions` at all — see below). The xacro file @@ -468,16 +467,24 @@ itself does: but the bundled directory is named `kuka_lbr_iiwa` (no `_support` suffix), so `$(find kuka_lbr_iiwa_support)` fails to resolve — `xacrodoc.packages.PackageNotFoundError: Package not found: -kuka_lbr_iiwa_support`. `kuka_lbr_iiwa_support` is the real upstream ROS -package name (confirmed against the actual macro file's own `$(find ...)` -reference), so the bundled folder's name is simply wrong, not the xacro -content. - -**Proposed fix:** rename the bundled directory -`rtb-data/xacro/kuka_description/kuka_lbr_iiwa/` → -`.../kuka_lbr_iiwa_support/` to match what the xacro file already expects. -No xacro content needs editing. Requires an `rtb-data` release (can't be -fixed from `roboticstoolbox-python` alone). +kuka_lbr_iiwa_support`. + +The originally-proposed fix (rename the bundled directory to +`kuka_lbr_iiwa_support/`, requiring an `rtb-data` release) turned out not to +work even in isolation: `XacroDoc.from_file()`'s package auto-discovery +doesn't register a directory as a package purely by name-matching against +its own `$(find ...)` references — nothing in the bundled data carries a ROS +`package.xml` marker for it to key off. So renaming the directory alone, +tested empirically against a throwaway copy, still raised the same +`PackageNotFoundError`. + +**Actual fix:** `URDFRobot.__init__`/`URDF_file()` gained an +`extra_packages: dict[str, str] | None` kwarg (mirroring the existing +`patch=` convention for known-broken upstream references) that registers +additional package-name aliases via `xacrodoc.packages.update_package_cache()` +before parsing. `LBR.py` now passes +`extra_packages={"kuka_lbr_iiwa_support": "kuka_description/kuka_lbr_iiwa"}`. +No `rtb-data` release needed — fixed entirely within `roboticstoolbox-python`. ### `Valkyrie` and `Fetch` load via `robot_descriptions`, whose supplied files are broken upstream — patched on the way in @@ -946,36 +953,12 @@ just systematically rather than one-off as each breaks the build. ## `roboticstoolbox.models.list()` shadows the builtin `list` -### Background - -Found 2026-07-05 while fixing a stale `mtype=` kwarg in the docs -(`arm_dh.rst`/`arm_erobot.rst` runblock examples called -`rtb.models.list(mtype="DH")`, but the function's parameter had been -renamed to `type` at some point without updating the docs). - -Two things stood out while looking at `src/roboticstoolbox/models/list.py`: - -1. The function itself is named `list`, shadowing the builtin `list` - within any scope that does `from roboticstoolbox.models.list import - list` or `import roboticstoolbox.models as models; models.list(...)`. - It's always called qualified (`rtb.models.list(...)`) in practice, so - this hasn't bitten anyone yet, but it's a landmine for future edits to - that file — reaching for `list(...)` to build an actual list inside the - function body would silently recurse/shadow instead of erroring. -2. (Already fixed, see commit around 2026-07-05) one of its parameters was - named `type`, shadowing the builtin `type`. This has been renamed to - `mtype` — matching what the docs already (mistakenly, but presciently) - assumed the parameter was called — and all call sites (docs, tests) - updated to match. - -### Proposed fix - -Renaming the function itself (e.g. to `list_models`) would fix the -remaining shadowing, but `list` is public API (`rtb.models.list`) and a -rename is a breaking change for any external callers — deferred rather -than done opportunistically. If/when a broader API-breaking pass happens -on this module (or at the next major version bump), rename `list` to -something that doesn't shadow a builtin. +Found 2026-07-05, fixed 2026-07-20. `src/roboticstoolbox/models/list.py` +renamed to `catalog.py`, function renamed `list` → `catalog`. `models.list()` +is kept as a deprecated `FutureWarning`-emitting shim (public API, so not +removed outright) that forwards to `catalog()`; to be dropped in a future +major version. Docs (`arm_dh.rst`/`arm_erobot.rst`) and `tests/test_models.py` +updated to call `catalog()` directly. --- diff --git a/tests/test_models.py b/tests/test_models.py index bcdc84f6f..7bbc39431 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -8,12 +8,16 @@ import numpy.testing as nt -#@unittest.skip("BUG in .models()") +# @unittest.skip("BUG in .models()") class TestModels(unittest.TestCase): - def test_list(self): - rp.models.list() - rp.models.list("UR", 6) - rp.models.list(mtype="DH") + def test_catalog(self): + rp.models.catalog() + rp.models.catalog("UR", 6) + rp.models.catalog(mtype="DH") + + def test_list_deprecated(self): + with self.assertWarns(FutureWarning): + rp.models.list(mtype="DH") def test_puma(self): puma = rp.models.DH.Puma560() @@ -157,13 +161,7 @@ class TestModelSmoke(unittest.TestCase): # Remove an entry once its underlying issue is actually fixed -- if you # don't, this test starts failing for the *opposite* reason (a listed # failure unexpectedly started passing). - EXPECTED_FAILURES = { - # Bundled rtb-data xacro tree names this directory "kuka_lbr_iiwa", - # but the xacro file's own $(find kuka_lbr_iiwa_support) expects - # the "_support" suffix -- needs an rtb-data rename + republish. - # See tech-debt.md, "rtb-data" section. - ("URDF", "LBR"), - } + EXPECTED_FAILURES = set() def test_all_models_construct(self): unexpected_failures = [] From 44915c46b8a55fed783ffbb3716054876f1610ac Mon Sep 17 00:00:00 2001 From: Peter Corke Date: Tue, 21 Jul 2026 09:06:14 +1000 Subject: [PATCH 2/2] feat(models): add sorton param to catalog(), show RD table for mtype=URDF Adds sorton="name"/"manufacturer"/"dof" to catalog(), case insensitive, sorting both tables via ANSITable.sort() before printing. DoF sorts numerically with blank values (common in the robot_descriptions table) first. Renames columns for consistency across both tables: the toolbox table's "name" and the robot_descriptions table's "robot" both become "robot name"; the robot_descriptions table's identifying column becomes "robot_descriptions name" (matching robot_descriptions' own CLI/module naming) to disambiguate from "robot name". Both tables gain a "model type" column, hardcoded to "URDF" in the robot_descriptions table since only URDF-format entries are listed. catalog(mtype="URDF") now also shows the robot_descriptions table, since every entry in it is a URDF model -- previously the table was shown only when mtype was left unfiltered. Co-Authored-By: Claude Sonnet 5 --- src/roboticstoolbox/models/catalog.py | 77 +++++++++++++---- src/roboticstoolbox/models/list.py | 120 -------------------------- 2 files changed, 61 insertions(+), 136 deletions(-) delete mode 100644 src/roboticstoolbox/models/list.py diff --git a/src/roboticstoolbox/models/catalog.py b/src/roboticstoolbox/models/catalog.py index 8529fa5b7..09f817fe8 100644 --- a/src/roboticstoolbox/models/catalog.py +++ b/src/roboticstoolbox/models/catalog.py @@ -9,7 +9,7 @@ # import importlib -def catalog(keywords=None, dof=None, mtype=None, border="thin"): +def catalog(keywords=None, dof=None, mtype=None, sorton=None, border="thin"): """ Display all robot models in summary form @@ -17,18 +17,26 @@ def catalog(keywords=None, dof=None, mtype=None, border="thin"): :type keywords: tuple of str, optional :param dof: number of DoF to filter on, defaults to None :type dof: int, optional - :param mtype: model type "DH", "ETS", "URDF", defaults to all types + :param mtype: filter on model type: "DH", "ETS", "URDF", defaults to all types :type mtype: str, optional + :param sorton: column to sort rows on: "name", "manufacturer" or "dof", + case insensitive, defaults to None (construction order) + :type sorton: str, optional - ``catalog()`` displays a list of all models provided by the Toolbox. It lists the name, manufacturer, model type, number of DoF, and - keywords. It also lists models importable by name from + keywords. It also lists models available from `robot_descriptions `_ - but not otherwise wrapped by the Toolbox. + but not otherwise wrapped by the Toolbox: "robot_descriptions name" + is the string ``robot_descriptions`` itself calls the *name* (its own + ``python -m robot_descriptions pull NAME`` CLI) -- pass it to + :class:`~roboticstoolbox.models.URDF.URDFRobot.URDFRobot` to load that + model, e.g. ``URDFRobot("ur5")``. - ``catalog(mtype=MT)`` as above, but only displays Toolbox models of - type ``MT`` where ``MT`` is one of "DH", "ETS" or "URDF", and omits - the robot_descriptions listing. + type ``MT`` where ``MT`` is one of "DH", "ETS" or "URDF". The + robot_descriptions listing (all URDF) is shown for ``MT="URDF"`` or + unfiltered, and omitted for ``MT="DH"``/``MT="ETS"``. - ``catalog(keywords=KW)`` as above, but only displays models that have a keyword in the tuple ``KW``. @@ -36,6 +44,12 @@ def catalog(keywords=None, dof=None, mtype=None, border="thin"): - ``catalog(dof=N)`` as above, but only display models that have ``N`` degrees of freedom. + - ``catalog(sorton=S)`` as above, but rows are sorted by column ``S``. + ``name`` sorts by the robot's name, ``manufacturer`` sorts by the robot's + manufacturer, and ``dof`` sorts by the robot's number of degrees of freedom. Sorting is case insensitive. + Applies to both tables when both are shown. Rows with no ``DoF`` + value (common in the robot_descriptions table) sort first. + The filters can be combined - ``catalog(keywords=KW, dof=N)`` are those models that have a keyword @@ -51,12 +65,33 @@ def catalog(keywords=None, dof=None, mtype=None, border="thin"): if not unicode: border = "ascii" + # sorton maps to (column-in-main-table, column-in-rd-table, key). The two + # tables spell the "robot's own name" column differently -- "name" here, + # "robot name" in the robot_descriptions table, which also has a second, + # distinct "robot_descriptions name" column (see make_rd_table) -- so + # sorton="name" resolves to a different literal header per table. + sort_col_main = None + sort_col_rd = None + sort_key = None + if sorton is not None: + sort_map = { + "name": ("robot name", "robot name", None), + "manufacturer": ("manufacturer", "manufacturer", None), + "dof": ("DoF", "DoF", lambda s: int(s) if s.strip() else -1), + } + try: + sort_col_main, sort_col_rd, sort_key = sort_map[sorton.lower()] + except KeyError: + raise ValueError( + f"sorton must be one of {sorted(sort_map)}, got {sorton!r}" + ) + def make_table(border=None): table = ANSITable( Column("class", headalign="^", colalign="<"), - Column("name", headalign="^", colalign="<"), + Column("robot name", headalign="^", colalign="<"), Column("manufacturer", headalign="^", colalign="<"), - Column("type", headalign="^", colalign="<"), + Column("model type", headalign="^", colalign="<"), Column("DoF", colalign="<"), Column("dims", colalign="<"), Column("structure", colalign="<", width=16), @@ -113,6 +148,8 @@ def make_table(border=None): ", ".join(robot.keywords), ) + if sort_col_main is not None: + table.sort(sort_col_main, key=sort_key) table.print() def make_rd_table(border=None): @@ -120,9 +157,10 @@ def make_rd_table(border=None): from robot_descriptions._descriptions import Format table = ANSITable( - Column("name", headalign="^", colalign="<"), - Column("robot", headalign="^", colalign="<"), + Column("robot_descriptions name", headalign="^", colalign="<"), + Column("robot name", headalign="^", colalign="<"), Column("manufacturer", headalign="^", colalign="<"), + Column("model type", headalign="^", colalign="<"), Column("DoF", colalign="<"), Column("keywords", headalign="^", colalign="<"), border=border, @@ -132,7 +170,7 @@ def make_rd_table(border=None): if Format.URDF not in description.formats: continue - name = key.removesuffix("_description").removesuffix("_official") + rd_name = key.removesuffix("_description").removesuffix("_official") tags = sorted(description.tags) if keywords is not None: @@ -142,22 +180,27 @@ def make_rd_table(border=None): continue table.row( - name, + rd_name, description.robot, description.maker, + "URDF", description.dof if description.dof is not None else "", ", ".join(tags), ) - print(f"\nImportable from {_rd_link()}\n") + print(f"\nImportable from {_rd_link()}:\n") + if sort_col_rd is not None: + table.sort(sort_col_rd, key=sort_key) table.print() make_table(border=border) - if mtype is None: + if mtype is None or mtype == "URDF": make_rd_table(border=border) -def list(keywords=None, dof=None, mtype=None, border="thin"): # pragma nocover +def list( # pragma nocover + keywords=None, dof=None, mtype=None, sorton=None, border="thin" +): """ Display all robot models in summary form (deprecated) @@ -171,7 +214,9 @@ def list(keywords=None, dof=None, mtype=None, border="thin"): # pragma nocover FutureWarning, stacklevel=2, ) - return catalog(keywords=keywords, dof=dof, mtype=mtype, border=border) + return catalog( + keywords=keywords, dof=dof, mtype=mtype, sorton=sorton, border=border + ) if __name__ == "__main__": # pragma nocover diff --git a/src/roboticstoolbox/models/list.py b/src/roboticstoolbox/models/list.py deleted file mode 100644 index 16ff58cfa..000000000 --- a/src/roboticstoolbox/models/list.py +++ /dev/null @@ -1,120 +0,0 @@ -from typing import Type -from roboticstoolbox.robot.Robot import Robot -from roboticstoolbox.tools import rtb_get_param -from roboticstoolbox.robot.ERobot import ERobot2 -from ansitable import ANSITable, Column -import inspect - -# import importlib - - -def list(keywords=None, dof=None, mtype=None, border="thin"): - """ - Display all robot models in summary form - - :param keywords: keywords to filter on, defaults to None - :type keywords: tuple of str, optional - :param dof: number of DoF to filter on, defaults to None - :type dof: int, optional - :param mtype: model type "DH", "ETS", "URDF", defaults to all types - :type mtype: str, optional - - - ``list()`` displays a list of all models provided by the Toolbox. It - lists the name, manufacturer, model type, number of DoF, and keywords. - - - ``list(mtype=MT)`` as above, but only displays models of type ``MT`` - where ``MT`` is one of "DH", "ETS" or "URDF". - - - ``list(keywords=KW)`` as above, but only displays models that have a - keyword in the tuple ``KW``. - - - ``list(dof=N)`` as above, but only display models that have ``N`` - degrees of freedom. - - The filters can be combined - - - ``list(keywords=KW, dof=N)`` are those models that have a keyword in - ``KW`` and have ``N`` degrees of freedom. - """ - - import roboticstoolbox.models as models - - # module = importlib.import_module( - # '.' + os.path.splitext(file)[0], package='bdsim.blocks') - - unicode = rtb_get_param("unicode") - if not unicode: - border = "ascii" - - def make_table(border=None): - table = ANSITable( - Column("class", headalign="^", colalign="<"), - Column("name", headalign="^", colalign="<"), - Column("manufacturer", headalign="^", colalign="<"), - Column("type", headalign="^", colalign="<"), - Column("DoF", colalign="<"), - Column("dims", colalign="<"), - Column("structure", colalign="<"), - Column("dynamics", colalign="<"), - Column("geometry", colalign="<"), - Column("keywords", headalign="^", colalign="<"), - border=border, - ) - - if mtype is not None: - categories = [mtype] - else: - categories = ["DH", "URDF", "ETS"] - for category in categories: - # get all classes in this category - group = models.__dict__[category] - for cls in group.__dict__.values(): - if inspect.isclass(cls) and issubclass(cls, Robot): - # we found a BaseRobot subclass, instantiate it - try: - robot = cls() - except Exception: - print(f"failed to load {cls}") - continue - try: - structure = robot.structure - except Exception: # pragma nocover - structure = "" - - # apply filters - if keywords is not None: - if len(set(keywords) & set(robot.keywords)) == 0: - continue - if dof is not None and robot.n != dof: - continue # pragma nocover - - dims = 0 - - if isinstance(robot, ERobot2): - dims = 2 - else: - dims = 3 - # add the row - table.row( - cls.__name__, - robot.name, - robot.manufacturer, - category, - robot.n, - f"{dims}d", - structure, - "Y" if robot._hasdynamics else "", - "Y" if robot._hasgeometry else "", - ", ".join(robot.keywords), - ) - - table.print() - - make_table(border=border) - - -if __name__ == "__main__": # pragma nocover - list(border="ascii") - list(keywords=("dynamics",), border="thin") - list(dof=6) - list(keywords=("dynamics",), dof=6)