ros-parser 0.2.0__tar.gz → 0.4.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. {ros_parser-0.2.0 → ros_parser-0.4.0}/PKG-INFO +1 -1
  2. {ros_parser-0.2.0 → ros_parser-0.4.0}/pyproject.toml +1 -1
  3. {ros_parser-0.2.0 → ros_parser-0.4.0}/src/ros_parser/message_path/models.py +77 -35
  4. {ros_parser-0.2.0 → ros_parser-0.4.0}/src/ros_parser/models.py +11 -14
  5. {ros_parser-0.2.0 → ros_parser-0.4.0}/README.md +0 -0
  6. {ros_parser-0.2.0 → ros_parser-0.4.0}/src/ros_parser/__init__.py +0 -0
  7. {ros_parser-0.2.0 → ros_parser-0.4.0}/src/ros_parser/_lark_standalone_runtime.py +0 -0
  8. {ros_parser-0.2.0 → ros_parser-0.4.0}/src/ros_parser/_utils.py +0 -0
  9. {ros_parser-0.2.0 → ros_parser-0.4.0}/src/ros_parser/message_path/README.md +0 -0
  10. {ros_parser-0.2.0 → ros_parser-0.4.0}/src/ros_parser/message_path/__init__.py +0 -0
  11. {ros_parser-0.2.0 → ros_parser-0.4.0}/src/ros_parser/message_path/_standalone_parser.py +0 -0
  12. {ros_parser-0.2.0 → ros_parser-0.4.0}/src/ros_parser/message_path/_standalone_parser.pyi +0 -0
  13. {ros_parser-0.2.0 → ros_parser-0.4.0}/src/ros_parser/message_path/grammar.lark +0 -0
  14. {ros_parser-0.2.0 → ros_parser-0.4.0}/src/ros_parser/message_path/parser.py +0 -0
  15. {ros_parser-0.2.0 → ros_parser-0.4.0}/src/ros_parser/py.typed +0 -0
  16. {ros_parser-0.2.0 → ros_parser-0.4.0}/src/ros_parser/ros1_msg/__init__.py +0 -0
  17. {ros_parser-0.2.0 → ros_parser-0.4.0}/src/ros_parser/ros1_msg/_standalone_parser.py +0 -0
  18. {ros_parser-0.2.0 → ros_parser-0.4.0}/src/ros_parser/ros1_msg/_standalone_parser.pyi +0 -0
  19. {ros_parser-0.2.0 → ros_parser-0.4.0}/src/ros_parser/ros1_msg/grammar.lark +0 -0
  20. {ros_parser-0.2.0 → ros_parser-0.4.0}/src/ros_parser/ros1_msg/parser.py +0 -0
  21. {ros_parser-0.2.0 → ros_parser-0.4.0}/src/ros_parser/ros1_msg/schema_parser.py +0 -0
  22. {ros_parser-0.2.0 → ros_parser-0.4.0}/src/ros_parser/ros2_msg/__init__.py +0 -0
  23. {ros_parser-0.2.0 → ros_parser-0.4.0}/src/ros_parser/ros2_msg/_standalone_parser.py +0 -0
  24. {ros_parser-0.2.0 → ros_parser-0.4.0}/src/ros_parser/ros2_msg/_standalone_parser.pyi +0 -0
  25. {ros_parser-0.2.0 → ros_parser-0.4.0}/src/ros_parser/ros2_msg/grammar.lark +0 -0
  26. {ros_parser-0.2.0 → ros_parser-0.4.0}/src/ros_parser/ros2_msg/parser.py +0 -0
  27. {ros_parser-0.2.0 → ros_parser-0.4.0}/src/ros_parser/ros2_msg/schema_parser.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: ros-parser
3
- Version: 0.2.0
3
+ Version: 0.4.0
4
4
  Summary: Python parser for ROS message definitions and Foxglove message path syntax
5
5
  Keywords: ros,ros1,ros2,parser,message,robotics
6
6
  Author: Marko Bausch
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "ros-parser"
3
- version = "0.2.0"
3
+ version = "0.4.0"
4
4
  description = "Python parser for ROS message definitions and Foxglove message path syntax"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -5,9 +5,9 @@ from collections.abc import Callable, Mapping, Sequence
5
5
  from dataclasses import dataclass
6
6
  from enum import Enum
7
7
  from operator import neg
8
- from typing import Any, Literal, cast
8
+ from typing import Any, Literal
9
9
 
10
- from ros_parser.models import MessageDefinition, Type
10
+ from ros_parser.models import Field, MessageDefinition, Type
11
11
 
12
12
 
13
13
  class MessagePathError(Exception):
@@ -490,6 +490,15 @@ def _wrap_angle(value: float) -> float:
490
490
  return (value + math.pi) % (2 * math.pi) - math.pi
491
491
 
492
492
 
493
+ def _sign(value: float) -> int:
494
+ """Return the sign of a numeric value: 1, -1, or 0."""
495
+ if value > 0:
496
+ return 1
497
+ if value < 0:
498
+ return -1
499
+ return 0
500
+
501
+
493
502
  _FUNCTIONS_NO_ARGS: dict[str, Callable[..., Any]] = {
494
503
  "abs": abs,
495
504
  "acos": math.acos,
@@ -503,7 +512,7 @@ _FUNCTIONS_NO_ARGS: dict[str, Callable[..., Any]] = {
503
512
  "log2": math.log2,
504
513
  "log10": math.log10,
505
514
  "negative": neg,
506
- "sign": lambda value: 1 if value > 0 else (-1 if value < 0 else 0),
515
+ "sign": _sign,
507
516
  "sin": math.sin,
508
517
  "sqrt": math.sqrt,
509
518
  "tan": math.tan,
@@ -520,6 +529,8 @@ _FUNCTIONS_NO_ARGS: dict[str, Callable[..., Any]] = {
520
529
  "wrap_angle": _wrap_angle,
521
530
  }
522
531
 
532
+ TIMESERIES_OPS: set[str] = {"delta", "derivative", "timedelta"}
533
+
523
534
 
524
535
  def _timeseries_sentinel(value: float) -> float: # noqa: ARG001
525
536
  """Sentinel for time-series functions. Raises when called without TransformContext."""
@@ -529,22 +540,33 @@ def _timeseries_sentinel(value: float) -> float: # noqa: ARG001
529
540
  )
530
541
 
531
542
 
532
- TIMESERIES_OPS: set[str] = {"delta", "derivative", "timedelta"}
543
+ for _op in TIMESERIES_OPS:
544
+ _FUNCTIONS_NO_ARGS[_op] = _timeseries_sentinel
533
545
 
534
- # Add time-series sentinels to _FUNCTIONS_NO_ARGS
535
- _FUNCTIONS_NO_ARGS["delta"] = _timeseries_sentinel
536
- _FUNCTIONS_NO_ARGS["derivative"] = _timeseries_sentinel
537
- _FUNCTIONS_NO_ARGS["timedelta"] = _timeseries_sentinel
546
+
547
+ def _get_field(obj: Any, name: str) -> Any:
548
+ """Get a field from an object, trying mapping access first, then attribute access."""
549
+ if isinstance(obj, Mapping):
550
+ try:
551
+ return obj[name]
552
+ except KeyError:
553
+ raise MessagePathError(f"Field '{name}' not found in mapping") from None
554
+ try:
555
+ return getattr(obj, name)
556
+ except AttributeError:
557
+ raise MessagePathError(
558
+ f"Field '{name}' not found on object of type '{type(obj).__name__}'"
559
+ ) from None
538
560
 
539
561
 
540
562
  def _norm(obj: Any) -> float:
541
563
  """Euclidean norm of object with x/y/z fields."""
542
564
  try:
543
- x = obj.x if hasattr(obj, "x") else obj["x"]
544
- y = obj.y if hasattr(obj, "y") else obj["y"]
545
- z = obj.z if hasattr(obj, "z") else obj["z"]
546
- except (AttributeError, KeyError, TypeError) as e:
547
- raise MessagePathError("norm requires an object with x, y, z fields") from e
565
+ x = _get_field(obj, "x")
566
+ y = _get_field(obj, "y")
567
+ z = _get_field(obj, "z")
568
+ except MessagePathError:
569
+ raise MessagePathError("norm requires an object with x, y, z fields") from None
548
570
  return math.sqrt(x * x + y * y + z * z)
549
571
 
550
572
 
@@ -570,12 +592,12 @@ class Quaternion:
570
592
  def _quaternion_to_euler(obj: Any) -> EulerAngles:
571
593
  """Convert quaternion (x,y,z,w) to EulerAngles(roll, pitch, yaw)."""
572
594
  try:
573
- x = obj.x if hasattr(obj, "x") else obj["x"]
574
- y = obj.y if hasattr(obj, "y") else obj["y"]
575
- z = obj.z if hasattr(obj, "z") else obj["z"]
576
- w = obj.w if hasattr(obj, "w") else obj["w"]
577
- except (AttributeError, KeyError, TypeError) as e:
578
- raise MessagePathError("rpy requires an object with x, y, z, w fields") from e
595
+ x = _get_field(obj, "x")
596
+ y = _get_field(obj, "y")
597
+ z = _get_field(obj, "z")
598
+ w = _get_field(obj, "w")
599
+ except MessagePathError:
600
+ raise MessagePathError("rpy requires an object with x, y, z, w fields") from None
579
601
 
580
602
  t0 = 2.0 * (w * x + y * z)
581
603
  t1 = 1.0 - 2.0 * (x * x + y * y)
@@ -594,13 +616,13 @@ def _quaternion_to_euler(obj: Any) -> EulerAngles:
594
616
  def _euler_to_quaternion(obj: Any) -> Quaternion:
595
617
  """Convert (roll, pitch, yaw) stored as (x, y, z) to Quaternion(x, y, z, w)."""
596
618
  try:
597
- roll = obj.x if hasattr(obj, "x") else obj["x"]
598
- pitch = obj.y if hasattr(obj, "y") else obj["y"]
599
- yaw = obj.z if hasattr(obj, "z") else obj["z"]
600
- except (AttributeError, KeyError, TypeError) as e:
619
+ roll = _get_field(obj, "x")
620
+ pitch = _get_field(obj, "y")
621
+ yaw = _get_field(obj, "z")
622
+ except MessagePathError:
601
623
  raise MessagePathError(
602
624
  "quat requires an object with x, y, z fields (roll, pitch, yaw)"
603
- ) from e
625
+ ) from None
604
626
 
605
627
  cr, sr = math.cos(roll / 2), math.sin(roll / 2)
606
628
  cp, sp = math.cos(pitch / 2), math.sin(pitch / 2)
@@ -633,6 +655,28 @@ _OBJECT_FUNCTIONS: dict[str, Callable[..., Any]] = {
633
655
  "magnitude": _magnitude,
634
656
  }
635
657
 
658
+ _FLOAT64_TYPE = Type(type_name="float64", package_name=None)
659
+
660
+ _OBJECT_FUNCTION_RETURN_TYPES: dict[str, MessageDefinition] = {
661
+ "rpy": MessageDefinition(
662
+ name="EulerAngles",
663
+ fields_all=[
664
+ Field(type=_FLOAT64_TYPE, name="roll"),
665
+ Field(type=_FLOAT64_TYPE, name="pitch"),
666
+ Field(type=_FLOAT64_TYPE, name="yaw"),
667
+ ],
668
+ ),
669
+ "quat": MessageDefinition(
670
+ name="Quaternion",
671
+ fields_all=[
672
+ Field(type=_FLOAT64_TYPE, name="x"),
673
+ Field(type=_FLOAT64_TYPE, name="y"),
674
+ Field(type=_FLOAT64_TYPE, name="z"),
675
+ Field(type=_FLOAT64_TYPE, name="w"),
676
+ ],
677
+ ),
678
+ }
679
+
636
680
 
637
681
  @dataclass
638
682
  class MathModifier(Action):
@@ -688,7 +732,8 @@ class MathModifier(Action):
688
732
 
689
733
  try:
690
734
  if func := _FUNCTIONS_NO_ARGS.get(self.operation):
691
- return cast("int | float", func(value, *args))
735
+ result: int | float = func(value, *args)
736
+ return result
692
737
 
693
738
  # Unknown operation
694
739
  raise MessagePathError(f"Unknown math modifier '{self.operation}'")
@@ -722,16 +767,13 @@ class MathModifier(Action):
722
767
 
723
768
  # Object-level functions work on complex types
724
769
  if self.operation in _OBJECT_FUNCTIONS:
725
- # These functions return numeric types
726
- float_type = Type(
727
- type_name="float64",
728
- package_name=None,
729
- is_array=False,
730
- array_size=None,
731
- is_upper_bound=False,
732
- string_upper_bound=None,
733
- )
734
- return float_type, None
770
+ result_def = _OBJECT_FUNCTION_RETURN_TYPES.get(self.operation)
771
+ if result_def is not None:
772
+ result_type = Type(type_name=result_def.name or "unknown", package_name=None)
773
+ return result_type, result_def
774
+
775
+ # norm, magnitude → scalar float
776
+ return _FLOAT64_TYPE, None
735
777
 
736
778
  # Time-series functions work on numeric types, preserve type
737
779
  if self.operation in TIMESERIES_OPS:
@@ -106,8 +106,6 @@ class Field:
106
106
  if self.default_value is not None:
107
107
  if isinstance(self.default_value, str):
108
108
  result += f" '{self.default_value}'"
109
- elif isinstance(self.default_value, list):
110
- result += f" {self.default_value}"
111
109
  else:
112
110
  result += f" {self.default_value}"
113
111
  return result
@@ -182,11 +180,7 @@ class ServiceDefinition:
182
180
 
183
181
  def __str__(self) -> str:
184
182
  """Return the string representation of the service."""
185
- lines = [f"# {self.name}"]
186
- lines.append(str(self.request))
187
- lines.append("---")
188
- lines.append(str(self.response))
189
- return "\n".join(lines)
183
+ return "\n".join([f"# {self.name}", str(self.request), "---", str(self.response)])
190
184
 
191
185
 
192
186
  @dataclass
@@ -200,10 +194,13 @@ class ActionDefinition:
200
194
 
201
195
  def __str__(self) -> str:
202
196
  """Return the string representation of the action."""
203
- lines = [f"# {self.name}"]
204
- lines.append(str(self.goal))
205
- lines.append("---")
206
- lines.append(str(self.result))
207
- lines.append("---")
208
- lines.append(str(self.feedback))
209
- return "\n".join(lines)
197
+ return "\n".join(
198
+ [
199
+ f"# {self.name}",
200
+ str(self.goal),
201
+ "---",
202
+ str(self.result),
203
+ "---",
204
+ str(self.feedback),
205
+ ]
206
+ )
File without changes