Skip to content

Refactored Parse Function#50

Merged
ShreyShingala merged 3 commits intomainfrom
Parse_Refactor
Jan 16, 2026
Merged

Refactored Parse Function#50
ShreyShingala merged 3 commits intomainfrom
Parse_Refactor

Conversation

@ShreyShingala
Copy link
Member

@ShreyShingala ShreyShingala commented Nov 5, 2025

Created a new parsley_message.py file containing two new classes: ParsleyError and ParsleyObject.

ParsleyError class stores error details and is created when an error occurs.

ParsleyObject class stores the parsed can message to replace the old dict.

  • Uses string for BoardTypeID and BoardInstID as it should be able to have any value
  • Allows you to equate with a dect and get a value out of it dict syntax style using the string name.
  • Uses generic type t for the time being until the proper class has been made.
Screenshot 2025-12-16 at 2 34 35 AM

Added tests in test_parse_to_object.py. Tests are basically copy pasted from test_parse, but ported over to work for classes. Main difference is parse_to_object always returns a parsley_object which is already converts everything to the decoded string (only returning the hex in an error), while in parsley the functions returned bytes themselves (SID/data) so tests were edited to account for this.


This change is Reviewable

Summary by CodeRabbit

  • New Features

    • Added ParsleyObject and ParsleyError for structured message handling.
    • Introduced parser classes for USB debug, live telemetry, logger pages, and bitstring inputs; centralized parsing utilities.
  • Refactor

    • Modernized type hints to Python 3.10+ syntax across parsing modules.
  • Deprecations

    • Marked legacy parsing entry points as deprecated in favor of the new parser classes.
  • Tests

    • Added comprehensive parsing unit tests and validations for ParsleyObject/ParsleyError.
  • Chores

    • Added runtime dependency on pydantic.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link

coderabbitai bot commented Nov 5, 2025

Walkthrough

Adds Pydantic-based ParsleyObject/ParsleyError models, a static _ParsleyParseInternal parser with helper functions, four concrete ParsleyParser implementations, modernized type hints and deprecations on legacy parse APIs, and declares runtime dependency pydantic>=2.12.3. Tests for new models and parsing paths were added.

Changes

Cohort / File(s) Summary
Project Metadata
pyproject.toml
Added runtime dependency pydantic>=2.12.3.
Public exports
src/parsley/__init__.py
Exported ParsleyObject, ParsleyError, ParsleyParser, USBDebugParser, LiveTelemetryParser, LoggerParser, BitstringParser.
Legacy API / parsing helpers
src/parsley/parsley.py
Modernized type hints (list/tuple/
Data models & validation
src/parsley/parsley_message.py
New ParsleyObject (Pydantic BaseModel, Generic) with field validators and __getitem__; new ParsleyError dataclass-like container with __getitem__.
Parsing core & concrete parsers
src/parsley/parse_to_object.py
Added _ParsleyParseInternal static utilities and parse_to_object, ParsleyParser ABC, and concrete parsers: USBDebugParser, LiveTelemetryParser, LoggerParser, BitstringParser.
Tests
tests/test_parsley.py, tests/test_parsley_message.py, tests/test_parse_to_object.py
Minor formatting tweaks; added tests for ParsleyObject/ParsleyError validations and comprehensive tests covering internal parsing, concrete parsers, success and error paths.

Sequence Diagram(s)

sequenceDiagram
    participant Client as Client
    participant Parser as Parser\n(USBDebug/Logger/Live/Bitstring)
    participant Internal as _ParsleyParseInternal
    participant Validator as Pydantic\nParsleyObject
    participant Result as ParsleyObject/\nParsleyError

    Client->>Parser: parse(input)
    activate Parser
    Parser->>Internal: parse_to_object(msg_sid, msg_data)
    activate Internal
    Internal->>Internal: decode SID & board IDs\nlookup message definition\nparse bit fields
    Internal->>Validator: construct ParsleyObject(data)
    activate Validator
    Validator->>Validator: validate msg_prio\nvalidate msg_type
    Validator-->>Internal: validated object
    deactivate Validator
    alt success
        Internal-->>Parser: ParsleyObject
    else failure
        Internal-->>Parser: ParsleyError
    end
    deactivate Internal
    Parser-->>Client: ParsleyObject / ParsleyError
    deactivate Parser
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • JasonBrave
  • ChrisYx511
  • yashjain128

Poem

🐰 I hopped through bytes and bits today,
Pydantic blossoms paved the way,
Parsers hum in tidy rows,
Validators guard the flows,
Hooray for clean parsed play! ✨

🚥 Pre-merge checks | ✅ 1 | ❌ 2
❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 7.14% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'Refactored Parse Function' is overly broad and vague. While refactoring is mentioned, the PR introduces significant new architectural changes including new classes (ParsleyObject, ParsleyError), multiple parser implementations (USBDebugParser, LiveTelemetryParser, LoggerParser, BitstringParser), a dependency upgrade (pydantic>=2.12.3), and deprecation of existing public APIs. The title does not capture the main objective of replacing dict-based message representation with typed Pydantic models. Consider a more specific title that highlights the core change, such as 'Replace dict-based parsing with ParsleyObject/ParsleyError models' or 'Introduce typed Pydantic models for parsed CAN messages'.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/parsley/parsley.py (1)

32-72: Fix return type annotation for parse().

The function signature indicates it returns ParsleyObject, but line 61 returns ParsleyError. The return type should be Union[ParsleyObject, ParsleyError].

Apply this diff:

-def parse(msg_sid: bytes, msg_data: bytes) -> ParsleyObject:
+def parse(msg_sid: bytes, msg_data: bytes) -> Union[ParsleyObject, ParsleyError]:

Additionally, line 50 has an invalid type annotation data: T = {}. This should be:

-    data: T = {}
+    data: dict = {}
🧹 Nitpick comments (3)
src/parsley/__init__.py (1)

15-15: Consider exporting ParsleyError as well.

Since parse() can return ParsleyError, users of the library may need to check for or handle error cases. Exporting ParsleyError would make the public API more complete and allow users to write isinstance(result, parsley.ParsleyError) checks.

Apply this diff:

-from .parsley_message import ParsleyObject
+from .parsley_message import ParsleyObject, ParsleyError
src/parsley/parsley_message.py (2)

12-17: Consider whether ParsleyError should inherit from Exception.

ParsleyError is a dataclass but doesn't inherit from Exception. This means it cannot be raised or caught in try-except blocks. Based on the usage in src/parsley/parsley.py (lines 61-64), it's being returned rather than raised, which is acceptable. However, the name "Error" typically implies an exception class.

Consider either:

  1. Renaming to ParsleyParseFailure or ParsleyParseResult to clarify it's a return value, not an exception
  2. Inheriting from Exception if you plan to raise it in the future

31-59: Simplify the equality implementation.

The current implementation is verbose and repetitive. It can be significantly simplified using early returns and standard comparison.

Apply this diff:

     def __eq__(self, other: object) -> bool: #allows comparison to dicts
         if isinstance(other, dict):
-            isSame = True
-            
-            if self.board_type_id != other.get('board_type_id'):
-                isSame = False
-            if self.board_inst_id != other.get('board_inst_id'):
-                isSame = False
-            if self.msg_prio != other.get('msg_prio'):
-                isSame = False
-            if self.msg_type != other.get('msg_type'):
-                isSame = False
-            if self.data != other.get('data'):
-                isSame = False
-                
-            return isSame
-        if isinstance(other, ParsleyObject):
-            isSame = True
-            if self.board_type_id != other.board_type_id:
-                isSame = False
-            if self.board_inst_id != other.board_inst_id:
-                isSame = False
-            if self.msg_prio != other.msg_prio:
-                isSame = False
-            if self.msg_type != other.msg_type:
-                isSame = False
-            if self.data != other.data:
-                isSame = False
-            return isSame
+            return (
+                self.board_type_id == other.get('board_type_id') and
+                self.board_inst_id == other.get('board_inst_id') and
+                self.msg_prio == other.get('msg_prio') and
+                self.msg_type == other.get('msg_type') and
+                self.data == other.get('data')
+            )
+        if isinstance(other, ParsleyObject):
+            return (
+                self.board_type_id == other.board_type_id and
+                self.board_inst_id == other.board_inst_id and
+                self.msg_prio == other.msg_prio and
+                self.msg_type == other.msg_type and
+                self.data == other.data
+            )
+        return NotImplemented
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5a19090 and e0887ae.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (6)
  • pyproject.toml (2 hunks)
  • src/parsley/__init__.py (1 hunks)
  • src/parsley/message_types.py (0 hunks)
  • src/parsley/parsley.py (4 hunks)
  • src/parsley/parsley_message.py (1 hunks)
  • tests/test_parsley.py (3 hunks)
💤 Files with no reviewable changes (1)
  • src/parsley/message_types.py
🧰 Additional context used
🧬 Code graph analysis (3)
src/parsley/__init__.py (1)
src/parsley/parsley_message.py (1)
  • ParsleyObject (20-70)
tests/test_parsley.py (3)
src/parsley/parsley_message.py (1)
  • ParsleyError (13-17)
src/parsley/parsley.py (1)
  • parse (32-72)
tests/utils.py (1)
  • create_msg_sid_from_strings (11-24)
src/parsley/parsley.py (4)
src/parsley/bitstring.py (1)
  • BitString (1-49)
src/parsley/fields.py (10)
  • Field (6-36)
  • Switch (181-197)
  • Bitfield (199-232)
  • decode (18-23)
  • decode (49-50)
  • decode (91-96)
  • decode (122-124)
  • decode (162-166)
  • decode (213-228)
  • get_fields (193-194)
src/parsley/parsley_message.py (2)
  • ParsleyObject (20-70)
  • ParsleyError (13-17)
src/parsley/parse_utils.py (1)
  • hexify (1-13)
🪛 Ruff (0.14.3)
src/parsley/parsley.py

87-87: Do not catch blind exception: Exception

(BLE001)

🔇 Additional comments (8)
tests/test_parsley.py (5)

112-112: LGTM!

The updated error handling correctly checks for ParsleyError instances instead of inspecting dict error keys.


118-118: LGTM!

Correctly verifies error case with isinstance check.


124-124: LGTM!

Properly checks for ParsleyError on invalid SID input.


147-147: LGTM!

Correctly updated to check for ParsleyError when message data is malformed.


137-139: Test expectations verified as correct and properly aligned with parse behavior.

The verification confirms that parse() successfully handles invalid board type IDs by returning a fallback hex value. When an invalid board type ID (0x1F) is encountered, parse_board_type_id() catches the ValueError during enum decoding and applies the hexify() fallback, which returns a hex string with the '0x' prefix. The parse() function returns a valid ParsleyObject without raising an error, and the test assertion checking for '0x' in board_type_id is correct.

src/parsley/parsley_message.py (1)

61-70: Handle invalid keys in getitem.

The current implementation will raise a KeyError if an invalid key is accessed, which is the correct dict behavior. The implementation looks good and properly supports dict-like access for backwards compatibility.

src/parsley/parsley.py (1)

157-157: LGTM!

The return type Iterator[Tuple[bytes, bytes]] correctly reflects that this is a generator function (uses yield on line 203).

pyproject.toml (1)

21-21: No issues found.

The latest pydantic version on PyPI is 2.12.3, matching the PR requirement. Known security advisories for pydantic 2.x only affect versions < 2.4.0 (regex DoS from 2024); version 2.12.3 is well above this threshold and has no applicable vulnerabilities.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/parsley/parsley.py (1)

32-72: Fix parse()'s inconsistent return type

On parse failures this function now returns a ParsleyError, yet the signature still promises ParsleyObject. Callers (see parse_message) will keep assuming the happy path and crash. Either make parse() consistently yield a ParsleyObject even for errors, or update the signature to Union[ParsleyObject, ParsleyError] and propagate that expectation.

-def parse(msg_sid: bytes, msg_data: bytes) -> ParsleyObject:
+def parse(msg_sid: bytes, msg_data: bytes) -> Union[ParsleyObject, ParsleyError]:
♻️ Duplicate comments (2)
src/parsley/parsley.py (2)

2-28: Remove the unused TypeVar T

parse_fields returns a heterogenous dict of field values, so the new TypeVar never captures a single type and just reintroduces the incorrect hint from the last review. Please drop TypeVar and annotate with dict[str, Any] instead.

-from typing import List, Tuple, Union, Iterator, TypeVar
+from typing import Any, Iterator, List, Tuple, Union
@@
-T = TypeVar("T")
-
-def parse_fields(bit_str: BitString, fields: List[Field]) -> dict[str, T]:
+def parse_fields(bit_str: BitString, fields: List[Field]) -> dict[str, Any]:

84-96: Tighten exception handling in to_enum()

The broad except Exception from the earlier revision is still here, and the extra in-function import remains redundant. Please narrow the exception to the expected KeyError/AttributeError and rely on the module-level mt import.

     def to_enum(enum_cls, name: str):
         try:
             return enum_cls[name]
-        except Exception:
+        except (KeyError, AttributeError):
             return name
-
-    import parsley.message_types as mt
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e0887ae and f5c5697.

📒 Files selected for processing (2)
  • src/parsley/parsley.py (4 hunks)
  • src/parsley/parsley_message.py (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/parsley/parsley_message.py
🧰 Additional context used
🧬 Code graph analysis (1)
src/parsley/parsley.py (4)
src/parsley/bitstring.py (1)
  • BitString (1-49)
src/parsley/fields.py (10)
  • Field (6-36)
  • Switch (181-197)
  • Bitfield (199-232)
  • decode (18-23)
  • decode (49-50)
  • decode (91-96)
  • decode (122-124)
  • decode (162-166)
  • decode (213-228)
  • get_fields (193-194)
src/parsley/parsley_message.py (2)
  • ParsleyObject (19-72)
  • ParsleyError (13-17)
src/parsley/parse_utils.py (1)
  • hexify (1-13)
🪛 Ruff (0.14.3)
src/parsley/parsley.py

87-87: Do not catch blind exception: Exception

(BLE001)

Copy link
Contributor

@ChrisYx511 ChrisYx511 left a comment

Choose a reason for hiding this comment

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

In general, let's try not making this a fully breaking change and also take the opportunity to cleanup the parser code while we're at it.

Copy link
Contributor

@ChrisYx511 ChrisYx511 left a comment

Choose a reason for hiding this comment

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

See above.

@JasonBrave
Copy link
Member

pls rebase with latest main branch

@ShreyShingala ShreyShingala marked this pull request as draft November 20, 2025 00:46
@ShreyShingala ShreyShingala marked this pull request as ready for review November 26, 2025 04:50
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🧹 Nitpick comments (11)
src/parsley/__init__.py (1)

5-5: Inconsistency: ParsleyObject and ParsleyError are imported but excluded from __all__.

These classes are imported at line 5 but not listed in __all__. If they should be part of the public API, add them to __all__. If not, consider whether this import is needed at package level.

Apply this diff to include them in the public API:

 __all__ = ["parse_fields", "parse", "parse_board_type_id", "parse_board_inst_id",
     "parse_bitstring",
     "parse_live_telemetry",
     "parse_usb_debug",
     "parse_logger",
     "format_line",
     "encode_data",
-    "calculate_msg_bit_len"]
+    "calculate_msg_bit_len",
+    "ParsleyObject",
+    "ParsleyError"]

Also applies to: 17-24

src/parsley/parsley_message.py (2)

2-2: Remove unused Literal import.

Literal is imported but never used in this file.

-from typing import Literal, Generic, TypeVar
+from typing import Generic, TypeVar

13-14: Generic[T] on ParsleyError is unused.

The type parameter T is declared but never referenced in ParsleyError's fields. Either remove the generic or use it (e.g., for msg_data typing).

 @dataclass
-class ParsleyError(Generic[T]):
+class ParsleyError:
     """Custom error container for Parsley errors."""
src/parsley/parsley.py (3)

4-4: Remove unused BaseModel import.

BaseModel is imported but not used in this file.

-from pydantic import BaseModel

13-15: Incorrect TypeVar usage in return type.

T = TypeVar("T") is unbound here, so dict[str, T] doesn't provide meaningful type information. Use Any instead.

-T = TypeVar("T")
-
-def parse_fields(bit_str: BitString, fields: List[Field]) -> dict[str, T]:
+from typing import Any
+
+def parse_fields(bit_str: BitString, fields: List[Field]) -> dict[str, Any]:

119-119: Return type annotation includes None but function is a generator.

parse_logger is a generator (uses yield), so its return type should be Iterator[Tuple[bytes, bytes]] rather than Union[Tuple[bytes, bytes], None].

-def parse_logger(buf: bytes, page_number: int) -> Union[Tuple[bytes, bytes], None]:
+def parse_logger(buf: bytes, page_number: int) -> Iterator[Tuple[bytes, bytes]]:
tests/test_parse_to_object.py (2)

181-183: Remove unused exception variable e.

The as e captures are not used. Per Ruff F841.

-    with pytest.raises(IndexError) as e:
+    with pytest.raises(IndexError):
         BitstringParser().parse(bit_str)

Also applies to: 188-190


203-203: Use equality check instead of tuple membership.

res['data']['msg_data'] in ('0x0') checks if the value is in a single-element tuple. Use == for clarity.

-        assert res['data']['msg_data'] in ('0x0')
+        assert res['data']['msg_data'] == '0x0'
src/parsley/parse_to_object.py (3)

15-15: Incorrect TypeVar usage - should use Any.

T = TypeVar("T") is unbound and used in dict[str, T], which doesn't provide meaningful type information.

-T = TypeVar("T")
+from typing import Any

And update the function signature:

-    def _parse_fields(bit_str: BitString, fields: List[Field]) -> dict[str, T]:
+    def _parse_fields(bit_str: BitString, fields: List[Field]) -> dict[str, Any]:
         ...
-        res: dict[str, T] = {}
+        res: dict[str, Any] = {}

Also applies to: 68-75


87-92: Return type includes None but function never returns None.

_format_can_message always returns a tuple, so remove None from the union.

     @staticmethod
-    def _format_can_message(msg_sid: int, msg_data: List[int]) -> Union[Tuple[bytes, bytes], None]:
+    def _format_can_message(msg_sid: int, msg_data: List[int]) -> Tuple[bytes, bytes]:

217-220: Replace en-dash characters with hyphens.

The docstring uses en-dash (–) characters which can cause issues. Use standard hyphens (-) instead.

-        0  – 2  : ASCII 'L','O','G'
-        4  – 12 : SID (uint32 LE) | timestamp (uint32 LE) | DLC (uint8)
-        13 – .. : up to 8 bytes CAN payload
+        0  - 2  : ASCII 'L','O','G'
+        4  - 12 : SID (uint32 LE) | timestamp (uint32 LE) | DLC (uint8)
+        13 - .. : up to 8 bytes CAN payload
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0acaa63 and b5e852f.

📒 Files selected for processing (6)
  • src/parsley/__init__.py (2 hunks)
  • src/parsley/parse_to_object.py (1 hunks)
  • src/parsley/parsley.py (2 hunks)
  • src/parsley/parsley_message.py (1 hunks)
  • tests/test_parse_to_object.py (1 hunks)
  • tests/test_parsley.py (0 hunks)
💤 Files with no reviewable changes (1)
  • tests/test_parsley.py
🧰 Additional context used
🧬 Code graph analysis (3)
src/parsley/parse_to_object.py (5)
src/parsley/parsley_message.py (2)
  • ParsleyObject (25-49)
  • ParsleyError (14-23)
src/parsley/bitstring.py (3)
  • BitString (1-49)
  • push (30-41)
  • pop (12-28)
src/parsley/fields.py (4)
  • Field (6-36)
  • Switch (181-197)
  • Bitfield (199-232)
  • get_fields (193-194)
src/parsley/parsley.py (4)
  • format_line (201-211)
  • calculate_msg_bit_len (214-218)
  • encode_data (175-193)
  • parse (34-65)
src/parsley/parse_utils.py (1)
  • hexify (1-13)
src/parsley/parsley.py (1)
src/parsley/parsley_message.py (2)
  • ParsleyObject (25-49)
  • ParsleyError (14-23)
src/parsley/__init__.py (1)
src/parsley/parsley_message.py (2)
  • ParsleyObject (25-49)
  • ParsleyError (14-23)
🪛 Ruff (0.14.5)
src/parsley/parse_to_object.py

177-177: Avoid specifying long messages outside the exception class

(TRY003)


195-195: Avoid specifying long messages outside the exception class

(TRY003)


197-197: Avoid specifying long messages outside the exception class

(TRY003)


206-206: Avoid specifying long messages outside the exception class

(TRY003)


217-217: String contains ambiguous (EN DASH). Did you mean - (HYPHEN-MINUS)?

(RUF001)


219-219: String contains ambiguous (EN DASH). Did you mean - (HYPHEN-MINUS)?

(RUF001)


220-220: String contains ambiguous (EN DASH). Did you mean - (HYPHEN-MINUS)?

(RUF001)


235-235: Avoid specifying long messages outside the exception class

(TRY003)


238-238: Avoid specifying long messages outside the exception class

(TRY003)


241-241: Avoid specifying long messages outside the exception class

(TRY003)


252-252: Avoid specifying long messages outside the exception class

(TRY003)

tests/test_parse_to_object.py

181-181: Local variable e is assigned to but never used

Remove assignment to unused variable e

(F841)


188-188: Local variable e is assigned to but never used

Remove assignment to unused variable e

(F841)

src/parsley/__init__.py

17-24: __all__ is not sorted

Apply an isort-style sorting to __all__

(RUF022)

src/parsley/parsley_message.py

39-39: Avoid specifying long messages outside the exception class

(TRY003)


45-45: Avoid specifying long messages outside the exception class

(TRY003)

🔇 Additional comments (6)
src/parsley/parsley_message.py (1)

36-46: Validators look good.

The runtime validation for msg_prio and msg_type against mt.msg_prio and mt.msg_type dictionaries provides useful runtime checks. This aligns with the Pydantic approach.

tests/test_parse_to_object.py (2)

25-52: Good test coverage for successful parsing.

The test thoroughly validates all fields of the parsed result including board IDs, message priority, type, and nested data fields with proper approximation for floating-point comparison.


284-328: Comprehensive logger parsing tests.

Good coverage of the logger parsing including valid multi-message scenarios, wrong size, wrong signature, wrong page number, and empty buffer cases.

src/parsley/parse_to_object.py (3)

23-26: Good static class pattern.

Raising NotImplementedError in __init__ effectively prevents instantiation, enforcing static-only usage.


164-169: Abstract base class is well-structured.

The ParsleyParser ABC provides a clean interface for different input format parsers.


112-162: Core parsing logic handles errors gracefully.

The parse_to_object method properly handles both int and bytes SID, catches parsing errors, and returns appropriate ParsleyError objects with context.

Copy link
Contributor

@ChrisYx511 ChrisYx511 left a comment

Choose a reason for hiding this comment

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

implementation looks good! please add unit tests! you can probably just use existing tests with some minor modifications.
i haven't looked too too in detail though will do a more thourough pass with tests.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

♻️ Duplicate comments (2)
src/parsley/parsley.py (1)

13-15: Incorrect TypeVar usage in return type.

T is an unbound TypeVar used in the return annotation dict[str, T], which provides no meaningful type information. The function returns a dictionary with mixed value types (strings, ints, floats, nested dicts), so dict[str, Any] is the appropriate type.

This was flagged in a previous review but appears to still be present.

-from typing import List, Tuple, Union, Iterator, TypeVar
+from typing import Any, List, Tuple, Union, Iterator

-T = TypeVar("T")
-
-def parse_fields(bit_str: BitString, fields: List[Field]) -> dict[str, T]:
+def parse_fields(bit_str: BitString, fields: List[Field]) -> dict[str, Any]:
src/parsley/parse_to_object.py (1)

232-258: Generator method has incorrect return type annotation.

LoggerParser.parse uses yield (line 258), making it a generator. The return type should reflect this. This was flagged in a previous review.

-    def parse(self, buf: bytes, page_number: int) -> Union[ParsleyObject, ParsleyError]:
+    def parse(self, buf: bytes, page_number: int) -> Iterator[Union[ParsleyObject, ParsleyError]]:

Note: Iterator is already imported at line 4.

🧹 Nitpick comments (4)
tests/test_parse_to_object.py (1)

185-196: Remove unused exception capture variables.

The as e captures are assigned but never used. Per static analysis (Ruff F841), these can be simplified.

     def test_parse_bitstring_empty(self):
         bit_str = BitString() #just an empty bitstring
-        with pytest.raises(IndexError) as e:
+        with pytest.raises(IndexError):
             BitstringParser().parse(bit_str)
         # message content is not asserted as an index error is expected
         
     def test_parse_bitstring_small(self):
         bit_str = BitString()
         bit_str.push(b'\xFF', 8) # only 8 bits, less than required 29 bits for SID
-        with pytest.raises(IndexError) as e:
+        with pytest.raises(IndexError):
             BitstringParser().parse(bit_str)
         # message content is not asserted as an index error is expected
src/parsley/parse_to_object.py (3)

87-92: Remove impossible None from return type.

_format_can_message always returns a tuple; the Union[..., None] is misleading since None is never returned.

     @staticmethod
-    def _format_can_message(msg_sid: int, msg_data: List[int]) -> Union[Tuple[bytes, bytes], None]:
+    def _format_can_message(msg_sid: int, msg_data: List[int]) -> Tuple[bytes, bytes]:

213-225: Replace ambiguous en-dash characters with hyphen-minus.

The docstring contains en-dash characters () that should be regular hyphens (-) for consistency and to avoid encoding issues. Per static analysis (Ruff RUF001).

     """
     Parse one logger record.

     Layout  (little-endian unless stated):
-        0  – 2  : ASCII 'L','O','G'
+        0  - 2  : ASCII 'L','O','G'
         3       : page number (uint8)
-        4  – 12 : SID (uint32 LE) | timestamp (uint32 LE) | DLC (uint8)
-        13 – .. : up to 8 bytes CAN payload
+        4  - 12 : SID (uint32 LE) | timestamp (uint32 LE) | DLC (uint8)
+        13 - .. : up to 8 bytes CAN payload
         -- ff-padding may follow, removed before parsing --

     Returns whatever `format_can_message()` returns.
     Raises ValueError on any structural problem.
     """

174-174: Inconsistent Union syntax across parsers.

USBDebugParser uses the modern | syntax (ParsleyObject | ParsleyError) while other parsers use Union[...]. Consider standardizing on one style for consistency.

If using Python 3.10+, prefer | everywhere:

# Line 193
-    def parse(self, frame: bytes) -> Union[ParsleyObject, ParsleyError]:
+    def parse(self, frame: bytes) -> ParsleyObject | ParsleyError:

# Line 232 (after fixing Iterator issue)
-    def parse(self, buf: bytes, page_number: int) -> Iterator[Union[ParsleyObject, ParsleyError]]:
+    def parse(self, buf: bytes, page_number: int) -> Iterator[ParsleyObject | ParsleyError]:

# Line 263
-    def parse(self, bit_str: BitString) -> Union[ParsleyObject, ParsleyError]:
+    def parse(self, bit_str: BitString) -> ParsleyObject | ParsleyError:
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b5e852f and 2271da9.

📒 Files selected for processing (3)
  • src/parsley/parse_to_object.py (1 hunks)
  • src/parsley/parsley.py (2 hunks)
  • tests/test_parse_to_object.py (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
tests/test_parse_to_object.py (6)
src/parsley/bitstring.py (3)
  • BitString (1-49)
  • push (30-41)
  • pop (12-28)
src/parsley/parse_to_object.py (14)
  • parse_to_object (113-162)
  • _ParsleyParseInternal (23-162)
  • ParsleyParser (164-169)
  • USBDebugParser (171-188)
  • LoggerParser (210-258)
  • BitstringParser (260-266)
  • parse (168-169)
  • parse (174-188)
  • parse (193-208)
  • parse (232-258)
  • parse (263-266)
  • calculate_msg_bit_len (41-45)
  • format_line (28-38)
  • encode_data (48-66)
src/parsley/parsley_message.py (2)
  • ParsleyError (14-23)
  • ParsleyObject (25-49)
tests/test_parsley.py (9)
  • test_parse (16-47)
  • test_parse_partial_byte_fields (49-75)
  • test_parse_sensor_analog (77-105)
  • test_parse_bad_msg_type (107-111)
  • test_parse_bitstring (161-170)
  • test_calculate_msg_bit_length (200-204)
  • test_format_line (206-221)
  • test_encode_data (223-248)
  • test_parse_usb_debug (250-257)
tests/utils.py (2)
  • create_msg_sid_from_strings (11-24)
  • approx (8-9)
src/parsley/parsley.py (4)
  • parse (34-65)
  • calculate_msg_bit_len (214-218)
  • format_line (201-211)
  • encode_data (175-193)
src/parsley/parse_to_object.py (4)
src/parsley/parsley_message.py (2)
  • ParsleyObject (25-49)
  • ParsleyError (14-23)
src/parsley/bitstring.py (3)
  • BitString (1-49)
  • push (30-41)
  • pop (12-28)
src/parsley/fields.py (4)
  • Field (6-36)
  • Switch (181-197)
  • Bitfield (199-232)
  • get_fields (193-194)
src/parsley/parse_utils.py (1)
  • hexify (1-13)
src/parsley/parsley.py (3)
src/parsley/bitstring.py (1)
  • BitString (1-49)
src/parsley/fields.py (1)
  • Field (6-36)
src/parsley/parsley_message.py (2)
  • ParsleyObject (25-49)
  • ParsleyError (14-23)
🪛 Ruff (0.14.8)
tests/test_parse_to_object.py

187-187: Local variable e is assigned to but never used

Remove assignment to unused variable e

(F841)


194-194: Local variable e is assigned to but never used

Remove assignment to unused variable e

(F841)

src/parsley/parse_to_object.py

177-177: Avoid specifying long messages outside the exception class

(TRY003)


195-195: Avoid specifying long messages outside the exception class

(TRY003)


197-197: Avoid specifying long messages outside the exception class

(TRY003)


206-206: Avoid specifying long messages outside the exception class

(TRY003)


217-217: String contains ambiguous (EN DASH). Did you mean - (HYPHEN-MINUS)?

(RUF001)


219-219: String contains ambiguous (EN DASH). Did you mean - (HYPHEN-MINUS)?

(RUF001)


220-220: String contains ambiguous (EN DASH). Did you mean - (HYPHEN-MINUS)?

(RUF001)


235-235: Avoid specifying long messages outside the exception class

(TRY003)


238-238: Avoid specifying long messages outside the exception class

(TRY003)


241-241: Avoid specifying long messages outside the exception class

(TRY003)


252-252: Avoid specifying long messages outside the exception class

(TRY003)

🔇 Additional comments (14)
tests/test_parse_to_object.py (8)

1-16: LGTM - Well-organized imports and setup.

The imports cover all necessary components for testing the new parsing architecture, including the internal parser classes, message types, and utilities.


19-23: Clean helper for normalizing test results.

The _to_dict method appropriately handles both ParsleyError and ParsleyObject types, making test assertions consistent.


109-114: Past review issue addressed.

The test now correctly uses isinstance(result, ParsleyError) as suggested in the previous review, and the 'error' in result.error check is valid since error messages are prefixed with "error:".


30-107: Comprehensive parsing tests with good coverage.

The tests thoroughly cover successful parsing scenarios for GENERAL_BOARD_STATUS, DEBUG_RAW, and SENSOR_ANALOG messages, mirroring the patterns in test_parsley.py while validating the new ParsleyObject return type.


130-171: Good edge case coverage for invalid board IDs.

The tests properly verify that invalid board_type_id and board_inst_id values fall back to hexified strings (e.g., "0x1F") while still returning a valid ParsleyObject.


262-288: Solid USB debug parser test coverage.

Tests cover normal parsing, empty data, invalid format (missing $), and empty lines, ensuring proper error handling via ValueError.


290-365: Thorough logger parser tests.

Good coverage including multi-message pages, wrong size, wrong signature, wrong page number, and empty pages. The buffer construction is well-documented with comments.


367-408: Complete live telemetry parser tests.

Tests cover basic parsing with CRC validation, too-short frames, wrong header, and bad CRC scenarios.

src/parsley/parsley.py (1)

33-34: Deprecation decorator correctly applied.

The @deprecated decorator is properly used to mark the old parse function, directing users to the new class-based implementation. The typo from the previous review ("implementatio") has been fixed.

src/parsley/parse_to_object.py (5)

1-22: Well-structured module header and imports.

Good organization with clear module docstring, necessary imports, and module-level constants for formatting.


112-162: Core parsing logic looks correct.

The parse_to_object method properly handles both bytes and int SID inputs, decodes fields, and returns appropriate ParsleyObject or ParsleyError results. The docstring typo ("obejct") from the previous review has been fixed.


171-188: Good implementation of USBDebugParser.

Clean parsing of ASCII USB-debug lines with proper error handling for invalid format.


190-208: Solid LiveTelemetryParser implementation.

Proper frame validation (length, header, CRC) before parsing. The CRC8 validation is correctly implemented.


260-266: Clean BitstringParser implementation.

Straightforward parsing of BitString objects to extract SID and data.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2271da9 and 4dbc6a7.

📒 Files selected for processing (2)
  • src/parsley/parsley.py (2 hunks)
  • tests/test_parse_to_object.py (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
tests/test_parse_to_object.py (2)
src/parsley/parse_to_object.py (14)
  • parse_to_object (113-162)
  • _ParsleyParseInternal (23-162)
  • USBDebugParser (171-188)
  • LiveTelemetryParser (190-208)
  • LoggerParser (210-258)
  • BitstringParser (260-266)
  • parse (168-169)
  • parse (174-188)
  • parse (193-208)
  • parse (232-258)
  • parse (263-266)
  • calculate_msg_bit_len (41-45)
  • format_line (28-38)
  • encode_data (48-66)
src/parsley/parsley_message.py (2)
  • ParsleyError (14-23)
  • ParsleyObject (25-49)
src/parsley/parsley.py (2)
src/parsley/bitstring.py (1)
  • BitString (1-49)
src/parsley/fields.py (3)
  • Field (6-36)
  • Switch (181-197)
  • Bitfield (199-232)
🪛 Ruff (0.14.8)
tests/test_parse_to_object.py

187-187: Local variable e is assigned to but never used

Remove assignment to unused variable e

(F841)


194-194: Local variable e is assigned to but never used

Remove assignment to unused variable e

(F841)

🔇 Additional comments (8)
src/parsley/parsley.py (1)

31-31: Deprecation decorator applied correctly.

The @deprecated decorator is properly applied with a clear migration message pointing users to the class-based implementation.

tests/test_parse_to_object.py (7)

1-17: LGTM!

The imports are well-organized and appropriate for the comprehensive test suite covering internal parsing logic, parser implementations, and data models.


18-28: LGTM!

The _to_dict helper method provides a clean way to convert both success and error results to dictionaries for assertions, and the instantiation test correctly validates that the internal class cannot be instantiated.


30-58: LGTM!

Comprehensive test that validates parsing of a complete GENERAL_BOARD_STATUS message, including timestamp encoding, bitfield manipulation, and field-by-field result verification.


109-155: LGTM!

Excellent coverage of error cases including malformed message types, empty inputs, invalid SIDs, bad board IDs, and incomplete message data. The assertions correctly verify both error types and error recovery behavior (hexification of invalid board IDs).


262-288: LGTM!

Comprehensive testing of the USB debug parser covering valid messages, empty data, format violations, and edge cases. The assertions correctly validate both successful parsing and appropriate error handling.


290-365: LGTM!

Thorough testing of the logger parser with proper validation of the binary format, including multi-message pages, structural validation (size, signature, page number), and empty page handling. The tests correctly use struct packing and validate the expected parsing behavior.


367-409: LGTM!

Comprehensive testing of the live telemetry parser including proper frame construction with CRC calculation, validation of length and header requirements, and CRC verification. The tests correctly exercise both success and error paths.

Comment on lines +185 to +196
def test_parse_bitstring_empty(self):
bit_str = BitString() #just an empty bitstring
with pytest.raises(IndexError) as e:
BitstringParser().parse(bit_str)
# message content is not asserted as an index error is expected

def test_parse_bitstring_small(self):
bit_str = BitString()
bit_str.push(b'\xFF', 8) # only 8 bits, less than required 29 bits for SID
with pytest.raises(IndexError) as e:
BitstringParser().parse(bit_str)
# message content is not asserted as an index error is expected
Copy link

@coderabbitai coderabbitai bot Dec 16, 2025

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Remove unused exception info variables.

Lines 187 and 194 assign the exception info to e but never use it. Either remove the as e clause or add assertions on the exception message.

Apply this diff:

     def test_parse_bitstring_empty(self):
         bit_str = BitString() #just an empty bitstring
-        with pytest.raises(IndexError) as e:
+        with pytest.raises(IndexError):
             BitstringParser().parse(bit_str)
         # message content is not asserted as an index error is expected
         
     def test_parse_bitstring_small(self):
         bit_str = BitString()
         bit_str.push(b'\xFF', 8) # only 8 bits, less than required 29 bits for SID
-        with pytest.raises(IndexError) as e:
+        with pytest.raises(IndexError):
             BitstringParser().parse(bit_str)
🧰 Tools
🪛 Ruff (0.14.8)

187-187: Local variable e is assigned to but never used

Remove assignment to unused variable e

(F841)


194-194: Local variable e is assigned to but never used

Remove assignment to unused variable e

(F841)

🤖 Prompt for AI Agents
In tests/test_parse_to_object.py around lines 185 to 196, the tests bind the
raised IndexError to a variable (`as e`) but never use it; remove the unused `as
e` clauses (change `with pytest.raises(IndexError) as e:` to `with
pytest.raises(IndexError):`) or alternately add explicit assertions against
`str(e.value)` if you intend to validate the exception message — update both
test_parse_bitstring_empty and test_parse_bitstring_small accordingly so no
unused variable remains.

Copy link
Contributor

Choose a reason for hiding this comment

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

Copy link

Choose a reason for hiding this comment

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

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

Copy link
Contributor

@ChrisYx511 ChrisYx511 left a comment

Choose a reason for hiding this comment

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

some final small changes! i think it's mainly just a typing mistake

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/parsley/parsley.py (1)

115-161: Incorrect return type for generator function.

The parse_logger function uses yield (line 161), making it a generator. The return type annotation should reflect this.

🔎 Proposed fix
+from typing import Any, Iterator
-from typing import Any

-def parse_logger(buf: bytes, page_number: int) -> tuple[bytes, bytes] | None:
+def parse_logger(buf: bytes, page_number: int) -> Iterator[tuple[bytes, bytes]]:
🤖 Fix all issues with AI agents
In @src/parsley/parse_to_object.py:
- Around line 86-90: The return type annotation of format_can_message
incorrectly includes | None even though the function always returns a tuple of
bytes; update the function signature for format_can_message(msg_sid: int,
msg_data: list[int]) to return tuple[bytes, bytes] (remove the optional None),
and verify no callers or type hints elsewhere expect None so no other changes
are required.
- Around line 215-218: Replace the EN DASH (U+2013) characters with standard
hyphen-minus '-' in the docstring lines that currently read "0  – 2  : ASCII
'L','O','G'", "4  – 12 : SID (uint32 LE) | timestamp (uint32 LE) | DLC (uint8)"
and "13 – .. : up to 8 bytes CAN payload"; update those three ranges to use '-'
(e.g. "0 - 2 : ...", "4 - 12 : ...", "13 - .. : ...") and save the file with
UTF-8 encoding so the docstring only contains ASCII hyphens.

In @src/parsley/parsley_message.py:
- Around line 42-46: Both validators validate_msg_type and validate_msg_prio
incorrectly reject None even though the type hints allow None; update each
validator (validate_msg_type and validate_msg_prio in parsley_message.py) to
first check for value is None and return it (i.e., skip membership validation)
before testing membership against mt.msg_type / mt.msg_prio so None passes
validation and only non-None values are validated.
- Around line 36-40: The validator validate_msg_prio currently assumes a
non-null string and will raise when msg_prio is None; update validate_msg_prio
to skip validation when value is None (e.g., check `if value is not None and
value not in mt.msg_prio`) so optional msg_prio (typed str | None) is allowed,
then return value as before.

In @tests/test_parse_to_object.py:
- Line 187: Remove the unused exception variable in the pytest.raises context
managers: change usages of "with pytest.raises(IndexError) as e:" to "with
pytest.raises(IndexError):" (and do the same for the second occurrence at the
other location), since the captured exception variable `e` is never referenced.
🧹 Nitpick comments (6)
src/parsley/parsley_message.py (1)

14-14: Unused type parameter on ParsleyError.

ParsleyError declares Generic[T] but never uses the type variable T in its fields. This adds unnecessary complexity without providing type safety benefits.

🔎 Proposed fix
-class ParsleyError(Generic[T]):
+class ParsleyError:
     """Custom error container for Parsley errors."""
tests/test_parsley.py (1)

12-12: Unused import: ParsleyError.

The import of ParsleyError is not used in this test file. All tests still work with dict-based assertions (e.g., 'error' in res['data'] at lines 112, 118, 124, 147). Consider removing this import or updating the tests to use ParsleyError if that was the intention.

tests/test_parsley_message.py (1)

35-54: Consider testing None values for optional fields.

The tests verify that invalid string values raise ValidationError for msg_prio and msg_type, but don't test whether None (which is allowed by the type annotations at lines 10-11 in parsley_message.py) is accepted or rejected. Add tests to ensure None is handled correctly by the validators.

🔎 Suggested test additions
def test_parsley_object_none_prio():
    """Test that None is valid for msg_prio if intended."""
    obj = ParsleyObject(
        board_type_id='ANY',
        board_inst_id='GROUND',
        msg_prio=None,
        msg_type='RESET_CMD',
        data={}
    )
    assert obj['msg_prio'] is None


def test_parsley_object_none_type():
    """Test that None is valid for msg_type if intended."""
    obj = ParsleyObject(
        board_type_id='ANY',
        board_inst_id='GROUND',
        msg_prio='HIGH',
        msg_type=None,
        data={}
    )
    assert obj['msg_type'] is None
src/parsley/__init__.py (2)

24-47: Consider sorting __all__ for maintainability.

Static analysis suggests applying isort-style sorting to __all__. While not critical, alphabetical ordering improves maintainability and reduces merge conflicts.

🔎 Proposed sorted order
 __all__ = [
     "BitString",
+    "BitstringParser",
+    "LiveTelemetryParser",
+    "LoggerParser",
+    "ParsleyError",
+    "ParsleyObject",
+    "ParsleyParser",
+    "USBDebugParser",
+    "calculate_msg_bit_len",
+    "encode_data",
     "fields",
+    "format_line",
     "message_definitions",
     "message_types",
-    "ParsleyObject",
-    "ParsleyError",
-    "ParsleyParser",
-    "USBDebugParser",
-    "LiveTelemetryParser",
-    "LoggerParser",
-    "BitstringParser",
-    "parse_fields",
     "parse",
+    "parse_bitstring",
     "parse_board_type_id",
     "parse_board_inst_id",
-    "parse_bitstring",
+    "parse_fields",
     "parse_live_telemetry",
-    "parse_usb_debug",
     "parse_logger",
-    "format_line",
-    "encode_data",
-    "calculate_msg_bit_len",
+    "parse_usb_debug",
 ]

13-22: Pipeline warnings about partially unknown types in legacy parse functions.

The linter reports that several functions from parsley.py have partially unknown types (parse, parse_board_type_id, format_line, encode_data, calculate_msg_bit_len). While this doesn't block the current PR, consider adding type annotations to these legacy functions in a follow-up to improve type safety across the codebase.

tests/test_parse_to_object.py (1)

114-114: Consider more specific error assertions.

The assertions assert 'error' in result.error only check if the substring "error" appears in the message. Since the error format is always f"error: {error}" (line 151 in parse_to_object.py), this will always pass. Consider asserting specific error content or just checking that the error field is non-empty.

Example improvement
# Instead of:
assert 'error' in result.error

# Consider:
assert result.error  # Non-empty check
# Or check for specific error types:
assert 'KeyError' in result.error or 'ValueError' in result.error

Also applies to: 121-121, 128-128, 155-155

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4dbc6a7 and 7fd01c6.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (8)
  • pyproject.toml
  • src/parsley/__init__.py
  • src/parsley/parse_to_object.py
  • src/parsley/parsley.py
  • src/parsley/parsley_message.py
  • tests/test_parse_to_object.py
  • tests/test_parsley.py
  • tests/test_parsley_message.py
🧰 Additional context used
🧬 Code graph analysis (4)
tests/test_parsley_message.py (1)
src/parsley/parsley_message.py (2)
  • ParsleyObject (25-49)
  • ParsleyError (14-23)
tests/test_parsley.py (3)
src/parsley/parsley_message.py (1)
  • ParsleyError (14-23)
tests/test_parse_to_object.py (1)
  • test_parse_empty (116-121)
src/parsley/parsley.py (1)
  • parse (30-61)
src/parsley/parsley.py (3)
src/parsley/bitstring.py (2)
  • BitString (1-49)
  • pop (12-28)
src/parsley/fields.py (3)
  • Field (6-36)
  • Switch (181-197)
  • Bitfield (199-232)
src/parsley/parse_to_object.py (3)
  • parse_fields (67-83)
  • format_can_message (86-90)
  • encode_data (46-64)
src/parsley/parse_to_object.py (5)
src/parsley/parsley_message.py (2)
  • ParsleyObject (25-49)
  • ParsleyError (14-23)
src/parsley/bitstring.py (3)
  • BitString (1-49)
  • push (30-41)
  • pop (12-28)
src/parsley/fields.py (4)
  • Field (6-36)
  • Switch (181-197)
  • Bitfield (199-232)
  • get_fields (193-194)
src/parsley/parsley.py (4)
  • format_line (197-207)
  • calculate_msg_bit_len (210-214)
  • encode_data (171-189)
  • parse (30-61)
src/parsley/parse_utils.py (1)
  • hexify (1-13)
🪛 GitHub Actions: Run Lint
src/parsley/__init__.py

[warning] 14-14: Type of "parse" is partially unknown


[warning] 14-14: Type of "parse" is partially unknown


[warning] 14-14: Type of "parse_board_type_id" is partially unknown


[warning] 19-5: Type of "format_line" is partially unknown


[warning] 20-5: Type of "encode_data" is partially unknown


[warning] 21-5: Type of "calculate_msg_bit_len" is partially unknown

🪛 GitHub Check: Run Basedpyright
src/parsley/parse_to_object.py

[failure] 12-12:
Import "crc8" could not be resolved (reportMissingImports)

🪛 Ruff (0.14.10)
src/parsley/__init__.py

24-47: __all__ is not sorted

Apply an isort-style sorting to __all__

(RUF022)

src/parsley/parse_to_object.py

175-175: Avoid specifying long messages outside the exception class

(TRY003)


193-193: Avoid specifying long messages outside the exception class

(TRY003)


195-195: Avoid specifying long messages outside the exception class

(TRY003)


204-204: Avoid specifying long messages outside the exception class

(TRY003)


215-215: String contains ambiguous (EN DASH). Did you mean - (HYPHEN-MINUS)?

(RUF001)


217-217: String contains ambiguous (EN DASH). Did you mean - (HYPHEN-MINUS)?

(RUF001)


218-218: String contains ambiguous (EN DASH). Did you mean - (HYPHEN-MINUS)?

(RUF001)


233-233: Avoid specifying long messages outside the exception class

(TRY003)


236-236: Avoid specifying long messages outside the exception class

(TRY003)


239-239: Avoid specifying long messages outside the exception class

(TRY003)


250-250: Avoid specifying long messages outside the exception class

(TRY003)

src/parsley/parsley_message.py

39-39: Avoid specifying long messages outside the exception class

(TRY003)


45-45: Avoid specifying long messages outside the exception class

(TRY003)

tests/test_parse_to_object.py

187-187: Local variable e is assigned to but never used

Remove assignment to unused variable e

(F841)


194-194: Local variable e is assigned to but never used

Remove assignment to unused variable e

(F841)

🔇 Additional comments (10)
pyproject.toml (1)

21-21: No action needed. Pydantic 2.12.3 is a valid, secure version with no known vulnerabilities. The constraint >=2.12.3 appropriately allows patch and minor updates within the v2.x series, including the latest 2.12.5.

tests/test_parse_to_object.py (2)

25-28: LGTM - Well-designed test for static class enforcement.

The test correctly verifies that the internal class cannot be instantiated and validates the error message content.


30-107: Excellent test coverage across parser implementations.

The test suite comprehensively covers:

  • Valid parsing scenarios with multiple field types (Numeric, Enum, ASCII, Bitfield)
  • Error handling for malformed inputs
  • Format-specific parsers (USB debug, logger, live telemetry)
  • Edge cases (empty/minimal data, CRC validation, page number mismatches)

The use of realistic CAN message construction and proper type checking demonstrates thorough validation of the new parser infrastructure.

Also applies to: 262-409

src/parsley/parsley.py (3)

2-2: LGTM - Modern type annotations properly applied.

The import additions and type annotation updates correctly use:

  • Any for heterogeneous dictionary values
  • Modern Python 3.9+ syntax (list, dict instead of List, Dict)
  • deprecated decorator for the legacy function

Also applies to: 9-9, 11-11


29-29: Deprecation decorator properly applied.

The @deprecated decorator correctly marks the legacy parse function with an appropriate version and clear migration guidance.


157-157: LGTM - Helpful local variable type annotation.

The explicit type annotation list[int] improves code clarity and enables better static type checking.

src/parsley/parse_to_object.py (4)

21-24: LGTM - Static-only class pattern correctly implemented.

The __init__ guard properly prevents instantiation with a clear error message, enforcing the static-only design.


92-108: LGTM - Board ID parsing with fallback to hex representation.

Both methods correctly handle unknown board IDs by falling back to hexified representation, enabling parsing to continue even with unrecognized hardware.


111-160: Well-structured parsing with comprehensive error handling.

The parse_to_object method effectively:

  • Accepts both bytes and integer SID formats for flexibility
  • Extracts and decodes all message components systematically
  • Gracefully handles decode failures by wrapping them in ParsleyError
  • Returns strongly-typed results (ParsleyObject or ParsleyError)

The defensive initialization of variables and clear separation of success/error paths make the code maintainable.


12-12: No action required — crc8 is properly declared as a dependency.

The crc8 import is already specified in both pyproject.toml (as "crc8>=0.2.1") and requirements.txt. The static analysis warning is a false positive, likely due to the package not being installed in the CI environment.

Comment on lines +42 to +46
@field_validator("msg_type")
def validate_msg_type(cls, value):
if value not in mt.msg_type:
raise ValueError(f"Invalid msg_type type '{value}'")
return value
Copy link

@coderabbitai coderabbitai bot Jan 7, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# First, let's look at the structure of the parsley_message.py file
head -60 src/parsley/parsley_message.py

Repository: waterloo-rocketry/parsley

Length of output: 1418


🏁 Script executed:

# Find the definition of mt and what msg_type contains
rg -n "msg_type" src/parsley/parsley_message.py

Repository: waterloo-rocketry/parsley

Length of output: 304


🏁 Script executed:

# Search for where mt is imported from and what it contains
rg -n "import.*mt\|from.*mt" src/parsley/

Repository: waterloo-rocketry/parsley

Length of output: 51


🏁 Script executed:

# Find and examine the message_types.py file
find src/parsley -name "message_types.py" -o -name "*message_types*"

Repository: waterloo-rocketry/parsley

Length of output: 97


🏁 Script executed:

# Look for msg_type definition in the repository
rg -n "msg_type\s*=" src/parsley/ --type=py -A 2

Repository: waterloo-rocketry/parsley

Length of output: 3192


🏁 Script executed:

# Read the message_types.py file to see the full msg_type definition
cat src/parsley/message_types.py

Repository: waterloo-rocketry/parsley

Length of output: 7447


Validator doesn't handle None for optional msg_type.

MsgType is defined as str | None (line 11), but the validator at line 44 checks if value not in mt.msg_type without special-casing None. The msg_type dictionary in message_types.py contains only string keys (UNDEFINED, GENERAL_BOARD_STATUS, etc.) and does not include None. This means the validator will raise ValueError when None is passed, despite None being a valid value according to the type hint. The codebase confirms that None is an expected value (e.g., parse_to_object.py line 134 sets msg_type = None).

Add a check to skip validation for None values:

if value is not None and value not in mt.msg_type:
    raise ValueError(f"Invalid msg_type type '{value}'")

Apply the same fix to the validate_msg_prio validator (line 39), which has the identical issue.

🧰 Tools
🪛 Ruff (0.14.10)

45-45: Avoid specifying long messages outside the exception class

(TRY003)

🤖 Prompt for AI Agents
In @src/parsley/parsley_message.py around lines 42 - 46, Both validators
validate_msg_type and validate_msg_prio incorrectly reject None even though the
type hints allow None; update each validator (validate_msg_type and
validate_msg_prio in parsley_message.py) to first check for value is None and
return it (i.e., skip membership validation) before testing membership against
mt.msg_type / mt.msg_prio so None passes validation and only non-None values are
validated.

Copy link
Contributor

Choose a reason for hiding this comment

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

Copy link

Choose a reason for hiding this comment

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

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

Copy link
Member Author

Choose a reason for hiding this comment

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

Not an issue, Both msg_prio and msg_type are only set to None at the beginning. Lowkey could've been a "" too but set to None to make it more clear to future developers that this is/will be changed. None never reaches parsley cause in the try loop there are only two potentials where they can't get replaced

  1. msg_prio returns error at .decode() -> creates a new Parsley error which doesn't feature board priority and the msg_type which is a none gets hexified

  2. msg_type eturns error at .decode() -> creates a new Parsley error and the msg_type gets hexified (the priority which would've been a normal string isn't included)

Could add a defensive fix, but idt its required


def test_parse_bitstring_empty(self):
bit_str = BitString() #just an empty bitstring
with pytest.raises(IndexError) as e:
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Remove unused exception variables.

The as e clause captures the exception but never uses it. Remove the unused variable binding.

🔎 Proposed fix
     def test_parse_bitstring_empty(self):
         bit_str = BitString() #just an empty bitstring
-        with pytest.raises(IndexError) as e:
+        with pytest.raises(IndexError):
             BitstringParser().parse(bit_str)
         # message content is not asserted as an index error is expected
         
     def test_parse_bitstring_small(self):
         bit_str = BitString()
         bit_str.push(b'\xFF', 8) # only 8 bits, less than required 29 bits for SID
-        with pytest.raises(IndexError) as e:
+        with pytest.raises(IndexError):
             BitstringParser().parse(bit_str)

Also applies to: 194-194

🧰 Tools
🪛 Ruff (0.14.10)

187-187: Local variable e is assigned to but never used

Remove assignment to unused variable e

(F841)

🤖 Prompt for AI Agents
In @tests/test_parse_to_object.py at line 187, Remove the unused exception
variable in the pytest.raises context managers: change usages of "with
pytest.raises(IndexError) as e:" to "with pytest.raises(IndexError):" (and do
the same for the second occurrence at the other location), since the captured
exception variable `e` is never referenced.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In @src/parsley/parsley_message.py:
- Around line 36-40: The validators validate_msg_prio and validate_msg_type
reject valid None values because they test membership in mt.msg_prio /
mt.msg_type without a None guard; update each validator (validate_msg_prio and
the counterpart validate_msg_type) to first allow None by only performing the
membership check when value is not None (e.g., if value is not None and value
not in mt.msg_prio / mt.msg_type) and otherwise return the value unchanged so
None is accepted.
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7fd01c6 and 0d2e87b.

📒 Files selected for processing (2)
  • src/parsley/parsley_message.py
  • tests/test_parsley.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/test_parsley.py
🧰 Additional context used
🪛 Ruff (0.14.10)
src/parsley/parsley_message.py

39-39: Avoid specifying long messages outside the exception class

(TRY003)


45-45: Avoid specifying long messages outside the exception class

(TRY003)

🔇 Additional comments (5)
src/parsley/parsley_message.py (5)

1-4: LGTM!

Imports are appropriate and well-organized for the functionality provided.


6-11: Type aliases are well-defined.

The type aliases clearly express intent, and the union types str | None correctly represent optional fields that will be validated at runtime.


13-23: LGTM!

ParsleyError as a dataclass is appropriate for this error container, and the __getitem__ method provides convenient dict-like access for backwards compatibility.


25-34: Class structure is sound.

The use of BaseModel with Generic[T] is a valid Pydantic pattern, and the generic data: T field provides appropriate flexibility for various data types.


48-49: LGTM!

The __getitem__ implementation using model_dump() is clean and provides dict-like access for backwards compatibility.

Comment on lines +36 to +40
@field_validator("msg_prio")
def validate_msg_prio(cls, value):
if value not in mt.msg_prio:
raise ValueError(f"Invalid msg_prio type '{value}'")
return value
Copy link

@coderabbitai coderabbitai bot Jan 7, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Validators reject valid None values.

Both validators have the critical issue already flagged in past review comments: they don't check for None before testing dictionary membership. Since MsgPrio and MsgType are typed as str | None, and the codebase does pass None values (confirmed in parse_to_object.py), these validators will incorrectly raise ValueError when None is provided.

Fix by adding a None check in each validator:

if value is not None and value not in mt.msg_prio:  # or mt.msg_type

Also applies to: 42-46

🧰 Tools
🪛 Ruff (0.14.10)

39-39: Avoid specifying long messages outside the exception class

(TRY003)

🤖 Prompt for AI Agents
In @src/parsley/parsley_message.py around lines 36 - 40, The validators
validate_msg_prio and validate_msg_type reject valid None values because they
test membership in mt.msg_prio / mt.msg_type without a None guard; update each
validator (validate_msg_prio and the counterpart validate_msg_type) to first
allow None by only performing the membership check when value is not None (e.g.,
if value is not None and value not in mt.msg_prio / mt.msg_type) and otherwise
return the value unchanged so None is accepted.

Copy link
Contributor

Choose a reason for hiding this comment

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

Copy link

Choose a reason for hiding this comment

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

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

Copy link
Member Author

Choose a reason for hiding this comment

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

Not an issue, Both msg_prio and msg_type are only set to None at the beginning. Lowkey could've been a "" too but set to None to make it more clear to future developers that this is/will be changed. None never reaches parsley cause in the try loop there are only two potentials where they can't get replaced

  1. msg_prio returns error at .decode() -> creates a new Parsley error which doesn't feature board priority and the msg_type which is a none gets hexified

  2. msg_type eturns error at .decode() -> creates a new Parsley error and the msg_type gets hexified (the priority which would've been a normal string isn't included)

Could add a defensive fix, but idt its required

Copy link
Contributor

@ChrisYx511 ChrisYx511 left a comment

Choose a reason for hiding this comment

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

Fix nits / Explain. Otherwise lgtm!! I'd get someone else to review too :)


return res

@deprecated(version='2026.2', reason="This function is outdated, use the class implementation instead")
Copy link
Contributor

Choose a reason for hiding this comment

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

Link to the specific class that should be used, also deprecate the remainder (parse_usb_debug, etc.)

Copy link
Member Author

@ShreyShingala ShreyShingala Jan 13, 2026

Choose a reason for hiding this comment

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

Alright, will do! Also in another PR should I replace all of them so that they just use the object models under the hood because the code is mostly just copy pasted?

Comment on lines +185 to +196
def test_parse_bitstring_empty(self):
bit_str = BitString() #just an empty bitstring
with pytest.raises(IndexError) as e:
BitstringParser().parse(bit_str)
# message content is not asserted as an index error is expected

def test_parse_bitstring_small(self):
bit_str = BitString()
bit_str.push(b'\xFF', 8) # only 8 bits, less than required 29 bits for SID
with pytest.raises(IndexError) as e:
BitstringParser().parse(bit_str)
# message content is not asserted as an index error is expected
Copy link
Contributor

Choose a reason for hiding this comment

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

Comment on lines +36 to +40
@field_validator("msg_prio")
def validate_msg_prio(cls, value):
if value not in mt.msg_prio:
raise ValueError(f"Invalid msg_prio type '{value}'")
return value
Copy link
Contributor

Choose a reason for hiding this comment

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

Comment on lines +42 to +46
@field_validator("msg_type")
def validate_msg_type(cls, value):
if value not in mt.msg_type:
raise ValueError(f"Invalid msg_type type '{value}'")
return value
Copy link
Contributor

Choose a reason for hiding this comment

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

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@src/parsley/parsley.py`:
- Around line 121-122: The parse_logger function is a generator (it uses yield)
but its signature declares a non-iterator return type; change the signature of
parse_logger to return Iterator[tuple[bytes, bytes]] and add/import Iterator
from typing (e.g., from typing import Iterator) at the top of the file so the
annotation matches its generator behavior and remove the optional None from the
return type.
♻️ Duplicate comments (1)
src/parsley/parse_to_object.py (1)

230-256: Return type should be Iterator for generator method.

The parse method uses yield (line 256), making it a generator. The return type annotation should reflect this.

🔧 Proposed fix

Update the import on line 4:

-from typing import Any
+from typing import Any, Iterator

Update the method signature:

-    def parse(self, buf: bytes, page_number: int) -> ParsleyObject | ParsleyError:
+    def parse(self, buf: bytes, page_number: int) -> Iterator[ParsleyObject | ParsleyError]:
🧹 Nitpick comments (2)
src/parsley/parse_to_object.py (1)

80-81: Redundant decode for Bitfield.

Line 76 already assigns res[field.name] = field.decode(data) for all fields. The Bitfield-specific block re-decodes with the same call, overwriting with an identical value.

🧹 Suggested cleanup
         if isinstance(field, Switch):
             nested_fields = field.get_fields(res[field.name])
             res.update(_ParsleyParseInternal.parse_fields(bit_str, nested_fields))
-        if isinstance(field, Bitfield):
-            res[field.name] = field.decode(data)
src/parsley/parsley.py (1)

89-89: Unnecessary | None in return type annotations.

Both parse_live_telemetry (line 89) and parse_usb_debug (line 105) always return tuple[bytes, bytes] via format_can_message(). The | None is misleading since these functions never return None.

Since these are deprecated, this is a minor nit.

🧹 Optional cleanup
-def parse_live_telemetry(frame: bytes) -> tuple[bytes, bytes] | None:
+def parse_live_telemetry(frame: bytes) -> tuple[bytes, bytes]:
-def parse_usb_debug(line: str) -> tuple[bytes, bytes] | None:
+def parse_usb_debug(line: str) -> tuple[bytes, bytes]:

Also applies to: 105-105

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0d2e87b and fa75cbd.

📒 Files selected for processing (2)
  • src/parsley/parse_to_object.py
  • src/parsley/parsley.py
🧰 Additional context used
🪛 GitHub Check: Run Basedpyright
src/parsley/parse_to_object.py

[failure] 12-12:
Import "crc8" could not be resolved (reportMissingImports)

🪛 Ruff (0.14.11)
src/parsley/parse_to_object.py

175-175: Avoid specifying long messages outside the exception class

(TRY003)


193-193: Avoid specifying long messages outside the exception class

(TRY003)


195-195: Avoid specifying long messages outside the exception class

(TRY003)


204-204: Avoid specifying long messages outside the exception class

(TRY003)


233-233: Avoid specifying long messages outside the exception class

(TRY003)


236-236: Avoid specifying long messages outside the exception class

(TRY003)


239-239: Avoid specifying long messages outside the exception class

(TRY003)


250-250: Avoid specifying long messages outside the exception class

(TRY003)

🔇 Additional comments (10)
src/parsley/parse_to_object.py (6)

92-160: LGTM!

The parsing helpers and parse_to_object method are well-structured. Good handling of both integer and bytes input for msg_sid, and appropriate error handling that returns ParsleyError with hex representations on parse failures.


162-167: LGTM!

The abstract base class pattern is appropriate for defining the parser interface.


169-186: LGTM!

Clean implementation that handles both SID-only and SID:data formats correctly.


188-206: LGTM!

Proper validation of frame structure and CRC checksum before parsing.


258-264: LGTM!

Clean implementation that extracts SID and data from the BitString before delegating to the internal parser.


12-12: No action required. The crc8 dependency is properly declared in pyproject.toml (line 20) and requirements.txt (line 2) as crc8>=0.2.1. The static analysis warning was a false positive.

Likely an incorrect or invalid review comment.

src/parsley/parsley.py (4)

9-12: LGTM!

Proper deprecation decorator with clear migration path to the new internal implementation.


30-80: LGTM!

Appropriate deprecation with clear migration guidance. Existing behavior is preserved for backwards compatibility.


170-198: LGTM!

Correct return type annotations and proper deprecation with migration guidance.


206-225: LGTM!

Proper deprecation with clear migration paths for the remaining utility functions.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment on lines +121 to +122
@deprecated(version='2026.2', reason="Deprecated; use LoggerParser.parse in the new LoggerParser object")
def parse_logger(buf: bytes, page_number: int) -> tuple[bytes, bytes] | None:
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Generator function needs Iterator return type.

parse_logger uses yield (line 168), making it a generator. The return type should be Iterator[tuple[bytes, bytes]] instead of tuple[bytes, bytes] | None.

🔧 Proposed fix

Update imports:

-from typing import Any
+from typing import Any, Iterator

Update signature:

-def parse_logger(buf: bytes, page_number: int) -> tuple[bytes, bytes] | None:
+def parse_logger(buf: bytes, page_number: int) -> Iterator[tuple[bytes, bytes]]:
🤖 Prompt for AI Agents
In `@src/parsley/parsley.py` around lines 121 - 122, The parse_logger function is
a generator (it uses yield) but its signature declares a non-iterator return
type; change the signature of parse_logger to return Iterator[tuple[bytes,
bytes]] and add/import Iterator from typing (e.g., from typing import Iterator)
at the top of the file so the annotation matches its generator behavior and
remove the optional None from the return type.

@ShreyShingala ShreyShingala merged commit 2a160e3 into main Jan 16, 2026
3 of 4 checks passed
@ShreyShingala ShreyShingala deleted the Parse_Refactor branch January 16, 2026 01:29
@ShreyShingala
Copy link
Member Author

Also solved #95

@ShreyShingala
Copy link
Member Author

and #76

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.

4 participants