Skip to content

Commit 09ba786

Browse files
authored
chore: Housekeeping + improved test coverage (#24)
1 parent 0af462b commit 09ba786

28 files changed

Lines changed: 750 additions & 79 deletions

.pre-commit-config.yaml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ default_install_hook_types:
44

55
repos:
66
- repo: https://github.com/pre-commit/pre-commit-hooks
7-
rev: v5.0.0
7+
rev: v6.0.0
88
hooks:
99
- id: check-added-large-files
1010
- id: check-case-conflict
@@ -23,14 +23,14 @@ repos:
2323
- id: trailing-whitespace
2424

2525
- repo: https://github.com/astral-sh/ruff-pre-commit
26-
rev: "v0.9.10"
26+
rev: "v0.13.2"
2727
hooks:
2828
- id: ruff
2929
args: ["--fix"]
3030
- id: ruff-format
3131

3232
- repo: https://github.com/crate-ci/typos
33-
rev: v1.30.1
33+
rev: v1.36.3
3434
hooks:
3535
- id: typos
3636

@@ -40,7 +40,7 @@ repos:
4040
- id: committed
4141

4242
- repo: https://github.com/python-jsonschema/check-jsonschema
43-
rev: 0.31.2
43+
rev: 0.34.0
4444
hooks:
4545
- id: check-github-workflows
4646
args: ["--verbose"]

pyproject.toml

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -15,22 +15,29 @@ classifiers = [
1515
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
1616
]
1717

18+
dynamic = ["version"]
19+
1820
dependencies = [
1921
"mcap>=1.0.2",
20-
"mcap-ros1-support>=0.6.0",
2122
"mcap-ros2-support>=0.3.0",
2223
"numpy",
2324
"pydantic>=2.4.2",
2425
"tqdm",
25-
"pyyaml>=6.0.1",
26-
"scipy>=1.11.1",
2726
"jsonargparse[signatures]>=4.35.0",
2827
"pointcloud2>=0.2.3",
2928
"pydantic-yaml>=1.2.1",
3029
"platformdirs>=4.3.8",
30+
"kappe[ros1]", # remove when pyyaml is no longer imported by jsonargparse
3131
]
3232

33-
dynamic = ["version"]
33+
[project.optional-dependencies]
34+
all = ["kappe[plugin_image, ros1]"]
35+
plugin_image = ["pillow>=8.0.0"]
36+
ros1 = ["mcap-ros1-support>=0.6.0"]
37+
38+
[dependency-groups]
39+
test = ["pytest-cov>=6.2.1", "pytest>=8.4.1", "kappe[all]", "scipy>=1.11.1"]
40+
3441

3542
[project.urls]
3643
repository = "https://github.com/sensmore/kappe"
@@ -39,13 +46,6 @@ repository = "https://github.com/sensmore/kappe"
3946
kappe = "kappe.cli:main"
4047
mcap_to_json = "kappe.utils.mcap_to_json:main"
4148
json_to_mcap = "kappe.utils.json_to_mcap:main"
42-
43-
[project.optional-dependencies]
44-
plugin_image = ["pillow>=8.0.0"]
45-
46-
[dependency-groups]
47-
test = ["pytest-cov>=6.2.1", "pytest>=8.4.1", "kappe[plugin_image]"]
48-
4949
[build-system]
5050
requires = ["hatchling"]
5151
build-backend = "hatchling.build"
@@ -82,13 +82,11 @@ ignore = [
8282
# formater
8383
'COM812', # missing-trailing-comma
8484
'ISC001', # single-line-implicit-string-concatenation
85-
86-
'UP038', # Deprecated rule
8785
]
8886

8987
[tool.ruff.lint.per-file-ignores]
9088
# Relaxed lints for test files
91-
"test/**/*.py" = ["T201", "SLF001", "ANN201"]
89+
"test/**/*.py" = ["T201", "SLF001", "ANN201", "PLC0415"]
9290

9391
[tool.ruff.lint.flake8-quotes]
9492
inline-quotes = 'single'

src/kappe/module/pointcloud.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import numpy as np
22
from pointcloud2 import create_cloud, read_points
33
from pydantic import BaseModel
4-
from scipy.spatial.transform import Rotation
54

5+
from kappe.utils.rotation import rotate_points
66
from kappe.utils.settings import SettingEgoBounds, SettingRotation
77
from kappe.writer import WrappedDecodedMessage
88

@@ -55,10 +55,9 @@ def point_cloud(cfg: SettingPointCloud, msg: WrappedDecodedMessage) -> None:
5555

5656
quat = cfg.rotation.quaternion
5757
if quat is not None:
58-
rot = Rotation.from_quat(np.array(quat))
5958
stack = np.column_stack([cloud['x'], cloud['y'], cloud['z']])
6059

61-
r_cloud = rot.apply(stack)
60+
r_cloud = rotate_points(stack, quat)
6261

6362
cloud['x'] = r_cloud[:, 0]
6463
cloud['y'] = r_cloud[:, 1]

src/kappe/module/tf.py

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
from typing import Any, Literal
22

33
from pydantic import BaseModel
4-
from scipy.spatial.transform import Rotation
54

5+
from kappe.utils.rotation import quaternion_multiply
66
from kappe.utils.settings import SettingRotation, SettingTranslation
77
from kappe.writer import WrappedDecodedMessage
88

@@ -119,20 +119,17 @@ def tf_apply_offset(cfg: SettingTF, msg: WrappedDecodedMessage) -> None:
119119
# Apply rotation offset
120120
offset_quat = offset_cfg.rotation.quaternion
121121
# Get current rotation as quaternion (x, y, z, w)
122-
current_quat = [
122+
current_quat = (
123123
transform.transform.rotation.x,
124124
transform.transform.rotation.y,
125125
transform.transform.rotation.z,
126126
transform.transform.rotation.w,
127-
]
127+
)
128128

129-
# Use scipy to multiply quaternions
130-
current_rot = Rotation.from_quat(current_quat)
131-
offset_rot = Rotation.from_quat(offset_quat)
132-
result_rot = current_rot * offset_rot
129+
# Multiply quaternions
130+
result_quat = quaternion_multiply(current_quat, offset_quat)
133131

134-
# Get the result quaternion and update the transform
135-
result_quat = result_rot.as_quat()
132+
# Update the transform
136133
transform.transform.rotation.x = result_quat[0]
137134
transform.transform.rotation.y = result_quat[1]
138135
transform.transform.rotation.z = result_quat[2]
@@ -149,7 +146,8 @@ def tf_remove(cfg: SettingTF, msg: WrappedDecodedMessage) -> bool:
149146
ros_msg.transforms = [
150147
tf for tf in ros_msg.transforms if tf.child_frame_id not in cfg.remove
151148
]
152-
else:
149+
else: # pragma: no cover
150+
# Defensive case already prevented by pydantic
153151
raise ValueError(f'Invalid value for remove: {cfg.remove}')
154152
return len(ros_msg.transforms) > 0
155153
return True

src/kappe/module/timing.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,14 @@
33
from typing import Any
44

55
from mcap.records import Message
6-
from mcap_ros1._vendor.genpy.rostime import Duration as ROS1Duration
7-
from mcap_ros1._vendor.genpy.rostime import Time as ROS1Time
86
from pydantic import BaseModel
97

8+
try:
9+
from mcap_ros1._vendor.genpy.rostime import Duration as ROS1Duration
10+
from mcap_ros1._vendor.genpy.rostime import Time as ROS1Time
11+
except ImportError: # pragma: no cover
12+
ROS1Duration = None # type: ignore[assignment]
13+
ROS1Time = None # type: ignore[assignment]
1014
from kappe.writer import WrappedDecodedMessage
1115

1216
logger = logging.getLogger(__name__)
@@ -93,6 +97,9 @@ def fix_ros1_time(msg: Any) -> None:
9397
secs -> sec
9498
nsecs -> nanosec
9599
"""
100+
if ROS1Time is None or ROS1Duration is None: # pragma: no cover
101+
raise RuntimeError('ros1 support not available, please install kappe with the "ros1" extra')
102+
96103
if not hasattr(msg, '__slots__'):
97104
return
98105

@@ -102,10 +109,11 @@ def fix_ros1_time(msg: Any) -> None:
102109
if isinstance(attr, list):
103110
for i in attr:
104111
fix_ros1_time(i)
105-
106112
elif isinstance(attr, ROS1Time | ROS1Duration):
107113
time = TimeMsg()
108114
time.sec = attr.secs
109115
time.nanosec = attr.nsecs
110116

111117
setattr(msg, slot, time)
118+
else:
119+
fix_ros1_time(attr)

src/kappe/plugin.py

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,11 @@ def __init__(self, **_kwargs: Any) -> None:
1616

1717
@property
1818
@abstractmethod
19-
def output_schema(self) -> str:
19+
def output_schema(self) -> str: # pragma: no cover
2020
pass
2121

2222
@abstractmethod
23-
def convert(self, ros_msg: Any) -> Any:
23+
def convert(self, ros_msg: Any) -> Any: # pragma: no cover
2424
pass
2525

2626

@@ -43,12 +43,11 @@ def load_plugin(base_folder: Path | None, plugin_name: str) -> Callable[..., Con
4343
if '.' in plugin_name:
4444
pkg_name, class_name = plugin_name.split('.')
4545

46-
plugin_folders: list[Path | None] = [Path(__file__).parent / 'plugins', base_folder]
46+
plugin_folders: list[Path] = [Path(__file__).parent / 'plugins']
47+
if base_folder:
48+
plugin_folders.append(base_folder)
4749

4850
for path in plugin_folders:
49-
if path is None:
50-
continue
51-
5251
plugin_file = path / f'{pkg_name}.py'
5352
if not plugin_file.exists():
5453
logger.debug('Plugin %s does not exist in %s', plugin_name, path)

src/kappe/plugins/image.py

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import logging
22
from io import BytesIO
3-
from pathlib import Path
43
from typing import Any
54

65
from kappe.plugin import ConverterPlugin
@@ -9,7 +8,7 @@
98

109
try:
1110
from PIL import Image
12-
except ImportError as err:
11+
except ImportError as err: # pragma: no cover
1312
logger.warning('PIL not installed. Install with "pip install pillow"')
1413
raise ImportError from err
1514

@@ -80,15 +79,3 @@ def convert(self, ros_msg: Any) -> Any:
8079
@property
8180
def output_schema(self) -> str:
8281
return 'sensor_msgs/msg/CompressedImage'
83-
84-
85-
class SaveCompress(ConverterPlugin):
86-
def __init__(self, *, quality: int = 10) -> None:
87-
self.quality = quality
88-
self.counter = 0
89-
90-
def convert(self, ros_msg: Any) -> Any:
91-
stream = BytesIO(ros_msg.data)
92-
img = Image.open(stream)
93-
with Path(f'{self.counter:08}.jpeg').open('wb') as f:
94-
img.save(f, format='jpeg', quality=self.quality)

src/kappe/utils/json_to_mcap.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import argparse
12
import io
23
import json
34
import logging
@@ -133,8 +134,6 @@ def json_to_mcap(
133134

134135

135136
def main() -> None:
136-
import argparse
137-
138137
parser = argparse.ArgumentParser(description='Convert JSON to MCAP.')
139138
parser.add_argument('file', type=Path, help='Path to the input JSONL file.')
140139
parser.add_argument('-o', '--output', type=Path, required=True, help='Path to the MCAP file.')

src/kappe/utils/mcap_to_json.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import argparse
12
import json
23
import sys
34
from collections.abc import Generator
@@ -112,8 +113,6 @@ def mcap_to_json(
112113

113114

114115
def main() -> None:
115-
import argparse
116-
117116
parser = argparse.ArgumentParser(description='Convert MCAP file to JSON format.')
118117
parser.add_argument('file', type=Path, help='Path to the MCAP file.')
119118
parser.add_argument(

src/kappe/utils/msg_def.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import logging
22
from collections import deque
3+
from functools import lru_cache
34
from io import BytesIO
45
from pathlib import Path
56
from typing import IO
@@ -75,7 +76,7 @@ def _get_cache_dir(distro: ROS2Distro) -> Path:
7576
)
7677

7778

78-
def _rglob_first(folder: list[Path], pattern: str) -> Path | None:
79+
def _rglob_first(folder: tuple[Path | None, ...], pattern: str) -> Path | None:
7980
for f in folder:
8081
if f is None or not f.exists():
8182
continue
@@ -85,7 +86,7 @@ def _rglob_first(folder: list[Path], pattern: str) -> Path | None:
8586
return None
8687

8788

88-
def _get_msg_def_disk(msg_type: str, folder: list[Path]) -> tuple[str, list[str]] | None:
89+
def _get_msg_def_disk(msg_type: str, folder: tuple[Path, ...]) -> tuple[str, list[str]] | None:
8990
pkg_name = msg_type.split('/')[0]
9091
msg_name = msg_type.split('/')[-1]
9192

@@ -121,9 +122,10 @@ def _get_msg_def(
121122
) -> tuple[str, list[str]] | None:
122123
cache_dir = _get_cache_dir(distro)
123124
_update_cache(cache_dir, distro)
124-
return _get_msg_def_disk(msg_type, [folder, cache_dir] if folder else [cache_dir])
125+
return _get_msg_def_disk(msg_type, (folder, cache_dir) if folder else (cache_dir,))
125126

126127

128+
@lru_cache(maxsize=128)
127129
def get_message_definition(
128130
msg_type: str, distro: ROS2Distro, folder: Path | None = None
129131
) -> str | None:

0 commit comments

Comments
 (0)