Skip to content

Rewrite of Exporter, Minor Changes in Importer#15

Open
micha91 wants to merge 15 commits intomasterfrom
add-configured-exporter
Open

Rewrite of Exporter, Minor Changes in Importer#15
micha91 wants to merge 15 commits intomasterfrom
add-configured-exporter

Conversation

@micha91
Copy link
Collaborator

@micha91 micha91 commented Sep 17, 2025

No description provided.

@micha91 micha91 force-pushed the add-configured-exporter branch from abb5919 to f98e3e6 Compare December 3, 2025 15:22
@micha91 micha91 requested a review from Wuestengecko December 3, 2025 15:30
Copy link
Member

@Wuestengecko Wuestengecko left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah IDK if it works then I'm okay with it I guess. Just some minor notes.

help="Decide whether experimental cmake files should be generated.",
envvar="CAPELLA_ROS_TOOLS_GENERATE_CMAKE",
)
def export(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that the function is called export (just like the click command), we no longer need to pass the command name explicitly in the outermost decorator.

-@cli.command("export")
+@cli.command()

Comment on lines +170 to +177
@click.option(
"--generate-cmake",
type=bool,
default=False,
is_flag=True,
help="Decide whether experimental cmake files should be generated.",
envvar="CAPELLA_ROS_TOOLS_GENERATE_CMAKE",
)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a fan of short flag names. And short help texts. :)

Suggested change
@click.option(
"--generate-cmake",
type=bool,
default=False,
is_flag=True,
help="Decide whether experimental cmake files should be generated.",
envvar="CAPELLA_ROS_TOOLS_GENERATE_CMAKE",
)
@click.option(
"--cmake",
type=bool,
default=False,
is_flag=True,
help="Generate cmake files. Experimental.",
envvar="CAPELLA_ROS_TOOLS_GENERATE_CMAKE",
)

Comment on lines +196 to +198
raise RuntimeError(
"Neither config nor root package nor layer specified."
)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be a click.UsageError, which has a much nicer UX. Compare these error messages:

RuntimeError
INFO:capellambse.cli_helpers:Loading model from [$WORKSPACE]/models/ife-demo
Traceback (most recent call last):
  File "", line 198, in _run_module_as_main
  File "", line 88, in _run_code
  File "[$WORKSPACE]/rostools/capella_ros_tools/__main__.py", line 234, in 
    cli()
    ~~~^^
  File "[$WORKSPACE]/rostools/.venv/lib/python3.13/site-packages/click/core.py", line 1462, in __call__
    return self.main(*args, **kwargs)
           ~~~~~~~~~^^^^^^^^^^^^^^^^^
  File "[$WORKSPACE]/rostools/.venv/lib/python3.13/site-packages/click/core.py", line 1383, in main
    rv = self.invoke(ctx)
  File "[$WORKSPACE]/rostools/.venv/lib/python3.13/site-packages/click/core.py", line 1850, in invoke
    return _process_result(sub_ctx.command.invoke(sub_ctx))
                           ~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^
  File "[$WORKSPACE]/rostools/.venv/lib/python3.13/site-packages/click/core.py", line 1246, in invoke
    return ctx.invoke(self.callback, **ctx.params)
           ~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "[$WORKSPACE]/rostools/.venv/lib/python3.13/site-packages/click/core.py", line 814, in invoke
    return callback(*args, **kwargs)
  File "[$WORKSPACE]/rostools/capella_ros_tools/__main__.py", line 196, in export
    raise RuntimeError(
        "Neither config nor root package nor layer specified."
    )
RuntimeError: Neither config nor root package nor layer specified.
UsageError
INFO:capellambse.cli_helpers:Loading model from [$WORKSPACE]/models/ife-demo
Usage: python -m capella_ros_tools export [OPTIONS]
Try 'python -m capella_ros_tools export --help' for help.
Error: Neither config nor root package nor layer specified.

IMO we should also use the parameter names in the error message:

Suggested change
raise RuntimeError(
"Neither config nor root package nor layer specified."
)
raise RuntimeError(
"One of --config=, --root= or --layer= is required."
)

Comment on lines +82 to +85
name = re.sub("[^a-z0-9_]", "_", name)
name = re.sub("^[^a-z]+", "", name)
name = re.sub("_+", "_", name)
return re.sub("_$", "", name)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
name = re.sub("[^a-z0-9_]", "_", name)
name = re.sub("^[^a-z]+", "", name)
name = re.sub("_+", "_", name)
return re.sub("_$", "", name)
name = re.sub("[^a-z0-9]+", "_", name)
name = re.sub("^[^a-z]+", "", name)
return re.sub("_+$", "", name)

Comment on lines +103 to +104
if not camel_case_name or not camel_case_name[0].isalpha():
camel_case_name = "A" + camel_case_name
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In make_snake_case we're stripping invalid characters from the beginning, while here in make_camel_case we're prepending a capital "A" in such cases. Does this have a specific reason? If not, IMO we should have the same behavior in both cases (either both strip, or both prepend).


@staticmethod
def make_snake_case(name: str) -> str:
"""Convert all cases to snake_case."""
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd like to see some basic unit tests for make_snake_case and make_camel_case. IMO they're complex enough to warrant that.

Comment on lines +78 to +85
name = re.sub("([a-z0-9])([A-Z])", r"\1_\2", name)
name = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", name)
name = name.lower()
# Replace invalid characters with underscores
name = re.sub("[^a-z0-9_]", "_", name)
name = re.sub("^[^a-z]+", "", name)
name = re.sub("_+", "_", name)
return re.sub("_$", "", name)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can lead to empty names:

from capella_ros_tools.exporter import RosExportHelper as e
print(repr(e.make_snake_case("1+1=2"))) # ''
print(repr(e.make_snake_case("A 1+1=2"))) # 'a_1_1_2'

Comment on lines +65 to +78
````yaml
packages:
asdf: <capella_pkg_uuid>
built_ins:
ros_pkg: <capella_build_in_pkg_uuid>
custom_pkg:
"abc":
- <capella_cls1_uuid>
"xyz":
- <capella_cls2_uuid>
custom_types:
Bitset16: int16
Bitset32: int32
````
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The world needs more TOML.

Advantages over YAML:

  • it's not YAML
  • tomllib is in stdlib since Python 3.11 (previously tomli on PyPI)
  • very simple and very explicit format
  • no significant whitespace, which is especially beneficial with the config templating feature

Disadvantages:

  • only 4 different types of strings instead of 9
  • can't easily introduce an ACE by accidentally using load() instead of safe_load()
````toml
[packages]
asdf = "<capella_pkg_uuid>"

[built_ins]
ros_pkg = "<capella_built_in_pkg_uuid>"

[custom_pkg]
abc = ["<capella_cls1_uuid>"]
xyz = [
  "<capella_cls2_uuid>",
  "<capella_cls3_uuid>",
]

[custom_types]
Bitset16 = "int16"
Bitset32 = "int32"
````

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants