Skip to content

Commit

Permalink
refactor: Rework CLI and adjust docs
Browse files Browse the repository at this point in the history
  • Loading branch information
huyenngn committed Jan 16, 2024
1 parent 0104dc8 commit 38cd28a
Show file tree
Hide file tree
Showing 16 changed files with 11,141 additions and 335 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,9 @@ venv.bak/
.spyderproject
.spyproject

# VSCode settings
.vscode/

# Rope project settings
.ropeproject

Expand Down
70 changes: 0 additions & 70 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,10 @@ pytest

We additionally recommend that you set up your editor / IDE as follows.

- Indent with 4 spaces per level of indentation
- Indent with 4 spaces per level of indentation

- Maximum line length of 79 (add a ruler / thin line / highlighting / ...)
- Maximum line length of 79 (add a ruler / thin line / highlighting / ...)

- _If you use Visual Studio Code_: Consider using a platform which supports
third-party language servers more easily, and continue with the next point.
- _If you use Visual Studio Code_: Consider using a platform which supports
third-party language servers more easily, and continue with the next point.

Expand All @@ -55,40 +51,19 @@ We additionally recommend that you set up your editor / IDE as follows.
}
```

```json
"[python]": {
"editor.codeActionsOnSave": {
"source.organizeImports": true
}
}
```

Note that the Pylance language server is not recommended, as it occasionally
causes false-positive errors for perfectly valid code.
Note that the Pylance language server is not recommended, as it occasionally
causes false-positive errors for perfectly valid code.

- _If you do not use VSC_: Set up your editor to use the [python-lsp-server],
and make sure that the relevant plugins are installed. You can install
everything that's needed into the virtualenv with pip:
- _If you do not use VSC_: Set up your editor to use the [python-lsp-server],
and make sure that the relevant plugins are installed. You can install
everything that's needed into the virtualenv with pip:

[python-lsp-server]: https://github.com/python-lsp/python-lsp-server
[python-lsp-server]: https://github.com/python-lsp/python-lsp-server

```sh
pip install "python-lsp-server[pylint]" python-lsp-black pyls-isort pylsp-mypy
```

```sh
pip install "python-lsp-server[pylint]" python-lsp-black pyls-isort pylsp-mypy
```

This will provide as-you-type linting as well as automatic formatting on
save. Language server clients are available for a wide range of editors, from
Vim/Emacs to PyCharm/IDEA.
This will provide as-you-type linting as well as automatic formatting on
save. Language server clients are available for a wide range of editors, from
Vim/Emacs to PyCharm/IDEA.
Expand All @@ -99,84 +74,50 @@ We base our code style on a modified version of the
[Google style guide for Python code](https://google.github.io/styleguide/pyguide.html).
The key differences are:

- **Docstrings**: The [Numpy style guide] applies here.
- **Docstrings**: The [Numpy style guide] applies here.

[numpy style guide]: https://numpydoc.readthedocs.io/en/latest/format.html#docstring-standard
[numpy style guide]: https://numpydoc.readthedocs.io/en/latest/format.html#docstring-standard

When writing docstrings for functions, use the imperative style, as per
[PEP-257]). For example, write "Do X and Y" instead of "Does X and Y".
When writing docstrings for functions, use the imperative style, as per
[PEP-257]). For example, write "Do X and Y" instead of "Does X and Y".

[pep-257]: https://peps.python.org/pep-0257/
[pep-257]: https://peps.python.org/pep-0257/

- **Overridden methods**: If the documentation did not change from the base
class (i.e. the base class' method's docstring still applies without
modification), do not add a short docstring á la "See base class". This lets
automated tools pick up the full base class docstring instead, and is
therefore more useful in IDEs etc.
- **Overridden methods**: If the documentation did not change from the base
class (i.e. the base class' method's docstring still applies without
modification), do not add a short docstring á la "See base class". This lets
automated tools pick up the full base class docstring instead, and is
therefore more useful in IDEs etc.

- **Linting**: Use [pylint] for static code analysis, and [mypy] for static
type checking.
- **Linting**: Use [pylint] for static code analysis, and [mypy] for static
type checking.

[pylint]: https://github.com/PyCQA/pylint
[mypy]: https://github.com/python/mypy
[pylint]: https://github.com/PyCQA/pylint
[mypy]: https://github.com/python/mypy

- **Formatting**: Use [black] as code auto-formatter. The maximum line length
is 79, as per [PEP-8]. This setting should be automatically picked up from
the `pyproject.toml` file. The reason for the shorter line length is that it
avoids wrapping and overflows in side-by-side split views (e.g. diffs) if
there's also information displayed to the side of it (e.g. a tree view of the
modified files).
- **Formatting**: Use [black] as code auto-formatter. The maximum line length
is 79, as per [PEP-8]. This setting should be automatically picked up from
the `pyproject.toml` file. The reason for the shorter line length is that it
avoids wrapping and overflows in side-by-side split views (e.g. diffs) if
there's also information displayed to the side of it (e.g. a tree view of the
modified files).

[black]: https://github.com/psf/black
[pep-8]: https://www.python.org/dev/peps/pep-0008/
[black]: https://github.com/psf/black
[pep-8]: https://www.python.org/dev/peps/pep-0008/

Be aware of the different line length of 72 for docstrings. We currently do
not have a satisfactory solution to automatically apply or enforce this.
Be aware of the different line length of 72 for docstrings. We currently do
not have a satisfactory solution to automatically apply or enforce this.

Note that, while you're encouraged to do so in general, it is not a hard
requirement to break up long strings into smaller parts. Additionally, never
break up strings that are presented to the user in e.g. log messages, as that
makes it significantly harder to grep for them.
Note that, while you're encouraged to do so in general, it is not a hard
requirement to break up long strings into smaller parts. Additionally, never
break up strings that are presented to the user in e.g. log messages, as that
makes it significantly harder to grep for them.

Use [isort] for automatic sorting of imports. Its settings should
automatically be picked up from the `pyproject.toml` file as well.
Use [isort] for automatic sorting of imports. Its settings should
automatically be picked up from the `pyproject.toml` file as well.

[isort]: https://github.com/PyCQA/isort
[isort]: https://github.com/PyCQA/isort

- **Typing**: We do not make an exception for `typing` imports. Instead of
writing `from typing import SomeName`, use `import typing as t` and access
typing related classes like `t.TypedDict`.
- **Typing**: We do not make an exception for `typing` imports. Instead of
writing `from typing import SomeName`, use `import typing as t` and access
typing related classes like `t.TypedDict`.
Expand All @@ -194,28 +135,17 @@ The key differences are:
`t.Optional[...]` and always explicitly annotate where `None` is possible.

[pep-604-style unions]: https://www.python.org/dev/peps/pep-0604/
[pep-604-style unions]: https://www.python.org/dev/peps/pep-0604/

- **Python style rules**: For conflicting parts, the [Black code style] wins.
If you have set up black correctly, you don't need to worry about this though
:)
- **Python style rules**: For conflicting parts, the [Black code style] wins.
If you have set up black correctly, you don't need to worry about this though
:)

[black code style]: https://black.readthedocs.io/en/stable/the_black_code_style/current_style.html
[black code style]: https://black.readthedocs.io/en/stable/the_black_code_style/current_style.html

- When working with `dict`s, consider using `t.TypedDict` instead of a more
generic `dict[str, float|int|str]`-like annotation where possible, as the
latter is much less precise (often requiring additional `assert`s or
`isinstance` checks to pass) and can grow unwieldy very quickly.
- When working with `dict`s, consider using `t.TypedDict` instead of a more
generic `dict[str, float|int|str]`-like annotation where possible, as the
latter is much less precise (often requiring additional `assert`s or
`isinstance` checks to pass) and can grow unwieldy very quickly.

- Prefer `t.NamedTuple` over `collections.namedtuple`, because the former uses
a more convenient `class ...:` syntax and also supports type annotations.
- Prefer `t.NamedTuple` over `collections.namedtuple`, because the former uses
a more convenient `class ...:` syntax and also supports type annotations.
127 changes: 64 additions & 63 deletions capella_ros_tools/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,40 @@
from capella_ros_tools.snapshot import app


@click.command()
@click.group(context_settings={"default_map": {}})
@click.version_option(
version=capella_ros_tools.__version__,
prog_name="capella-ros-tools",
message="%(prog)s %(version)s",
)
def cli():
"""CLI for capella-ros-tools."""


@cli.command("import")
@click.argument(
"msg_path", type=str, required=True, help="Path to ROS messages."
)
@click.argument(
"capella_path",
type=click.Path(path_type=Path),
required=True,
help="Path to Capella model.",
)
@click.argument(
"layer",
type=click.Choice(["oa", "la", "sa", "pa"], case_sensitive=False),
required=True,
help="Layer of Capella data package.",
)
@click.option(
"--exists-action",
"action",
type=click.Choice(["k", "o", "a", "c"], case_sensitive=False),
default="c" if sys.stdin.isatty() else "a",
help="Default action when an element already exists: (c)heck, (k)eep, (o)verwrite, (a)bort.",
type=click.Choice(
["skip", "replace", "abort", "ask"], case_sensitive=False
),
default="ask" if sys.stdin.isatty() else "abort",
help="Default action when an element already exists.",
)
@click.option(
"--no-deps",
Expand All @@ -34,71 +56,20 @@
help="Don’t install message dependencies.",
)
@click.option("--port", type=int, help="Port for HTML display.")
@click.option(
"-i",
"in_",
nargs=2,
type=(click.Choice(["capella", "messages"]), str),
required=True,
help="Input file type and path.",
)
@click.option(
"-o",
"out",
nargs=2,
type=(
click.Choice(["capella", "messages"]),
click.Path(path_type=Path),
),
required=True,
help="Output file type and path.",
)
@click.option(
"-l",
"layer",
type=click.Choice(["oa", "sa", "la", "pa"], case_sensitive=True),
required=True,
help="Layer to use.",
)
def cli(
in_: tuple[str, str],
out: tuple[str, str],
def import_msg(
msg_path: t.Any,
capella_path: Path,
layer: str,
action: str,
port: int,
no_deps: bool,
port: int,
):
"""Convert between Capella and ROS message definitions."""
input_type, input_path = in_
output_type, output = out

if input_type == output_type:
raise click.UsageError(
"Input and output must be different file types."
)
if "capella" not in (input_type, output_type):
raise click.UsageError(
"Either input or output must be a capella file."
)
if "messages" not in (input_type, output_type):
raise click.UsageError(
"Either input or output must be a messages file."
)

input: t.Any = Path(input_path)

if not input.exists() and input_type == "messages":
input = capellambse.filehandler.get_filehandler(input_path).rootdir
elif not input.exists() and input_type == "capella":
input = capellambse.filehandler.get_filehandler(input_path)
"""Import ROS messages into Capella data package."""

msg_path, capella_path, convert_class = (
(input, output, msg2capella.Converter)
if input_type == "messages"
else (output, input, capella2msg.Converter)
)
if not Path(msg_path).exists():
msg_path = capellambse.filehandler.get_filehandler(msg_path).rootdir

converter: t.Any = convert_class(
converter: t.Any = msg2capella.Converter(
msg_path, capella_path, layer, action, no_deps
)
converter.convert()
Expand All @@ -107,5 +78,35 @@ def cli(
app.start(converter.model.model, layer, port)


@cli.command("export")
@click.argument("capella_path", type=str, required=True)
@click.argument(
"layer",
type=click.Choice(["oa", "la", "sa", "pa"], case_sensitive=False),
required=True,
)
@click.argument("msg_path", type=click.Path(path_type=Path), required=True)
@click.option(
"--exists-action",
"action",
type=click.Choice(
["keep", "overwrite", "abort", "ask"], case_sensitive=False
),
default="ask" if sys.stdin.isatty() else "abort",
help="Default action when an element already exists.",
)
def export_capella(
capella_path: t.Any,
msg_path: Path,
layer: str,
):
"""Export Capella data package to ROS messages."""
if not Path(capella_path).exists():
capella_path = capellambse.filehandler.get_filehandler(capella_path)

converter: t.Any = capella2msg.Converter(capella_path, msg_path, layer)
converter.convert()


if __name__ == "__main__":
cli()
2 changes: 1 addition & 1 deletion capella_ros_tools/modules/capella/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ class BaseCapellaModel:

def __init__(
self,
path_to_capella_model: str,
path_to_capella_model: t.Any,
layer: str,
) -> None:
self.model = capellambse.MelodyModel(path_to_capella_model)
Expand Down
Loading

0 comments on commit 38cd28a

Please sign in to comment.