Skip to content

refactor: improve code organization and add type hints#29

Merged
iomz merged 8 commits intomainfrom
refactor/code-organization-and-type-hints
Nov 8, 2025
Merged

refactor: improve code organization and add type hints#29
iomz merged 8 commits intomainfrom
refactor/code-organization-and-type-hints

Conversation

@iomz
Copy link
Owner

@iomz iomz commented Nov 8, 2025

Refactor: Code Organization and Type Hints

Overview

This PR refactors the codebase to improve code organization, add comprehensive type hints, fix bugs, and enhance maintainability.

Changes

1. Code Organization

  • Moved data models: AggregatedData and EMGDataSingle from core.py to types.py for better organization
  • Created utility module: Added myo/utils.py and moved gatt_char_to_dict function there
  • Updated imports: Modified __init__.py to import from correct modules

2. Bug Fixes

  • Fixed FirmwareInfo.to_dict(): Corrected unlock_pose field to use self._unlock_pose instead of self._has_custom_classifier
  • Fixed Vibrate2 command: Corrected byte shifting from >> 0xFF to >> 8 for proper high byte extraction

3. Type Hints and Documentation

  • Added comprehensive type hints throughout core.py, commands.py, utils.py, and types.py
  • Added detailed docstrings to classes and methods
  • Used Union type for Python 3.8+ compatibility
  • Improved parameter validation with clearer error messages

4. Code Quality Improvements

  • Reduced code duplication: Extracted helper methods _get_emg_handles(), _get_imu_handles(), and _get_classifier_handles() in MyoClient
  • Improved error handling: Added validation checks and clearer error messages
  • Improved logging: Changed from f-strings to % formatting in logger calls
  • Enhanced MyoClient: Better state validation, improved notification handling, and more robust connection management

5. Code Structure

  • Better separation of concerns: Device discovery (Myo), client management (MyoClient), utilities (utils), and types (types)
  • Improved method organization with logical grouping
  • Enhanced maintainability with clearer structure, better documentation, and improved error handling

Files Modified

  • myo/types.py - Added AggregatedData and EMGDataSingle classes, fixed FirmwareInfo bug
  • myo/core.py - Refactored with type hints, improved error handling, extracted helper methods
  • myo/commands.py - Added type hints and improved documentation
  • myo/utils.py - New utility module for GATT helper functions
  • myo/__init__.py - Updated imports to reflect new structure

Testing

  • ✅ All imports verified
  • ✅ No linter errors
  • ✅ Example files work correctly
  • ✅ Backward compatibility maintained

Breaking Changes

None - this refactoring maintains full backward compatibility.

Summary by CodeRabbit

  • Bug Fixes

    • Firmware info now reports the unlock pose as a named pose string (not a boolean).
  • New Features

    • Single-sample EMG and aggregated sensor-data types are now part of the public API.
    • Added a utility to convert Bluetooth GATT characteristics into readable dictionaries.
  • Refactor

    • Command and device interfaces improved with stronger typing, clearer docstrings and input validation.
  • Tests

    • Updated tests to expect the new unlock_pose representation.
  • Chores

    • CI workflow action versions updated.

- Move data models (AggregatedData, EMGDataSingle) from core.py to types.py
- Create utils module for GATT utility functions
- Add comprehensive type hints throughout the codebase
- Add detailed docstrings to classes and methods
- Fix FirmwareInfo.to_dict() bug (unlock_pose field)
- Fix Vibrate2 command byte shifting
- Extract helper methods to reduce code duplication
- Improve error handling and validation
- Update imports in __init__.py

All changes maintain backward compatibility.
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 8, 2025

Walkthrough

Reorganized public exports to move AggregatedData and EMGDataSingle into myo.types and re-export them from myo.__init__; added type hints, docstrings, and input validation across myo.commands and myo.core; introduced myo.utils.gatt_char_to_dict; fixed FirmwareInfo.to_dict unlock_pose serialization; updated tests and CI workflow versions.

Changes

Cohort / File(s) Summary
Export reorganization
myo/__init__.py
Now imports only Myo and MyoClient from myo.core and re-exports an expanded set of types from myo.types, including newly moved AggregatedData and EMGDataSingle plus EMG/IMU/FV/firmware/motion enums/types.
Commands typing & validation
myo/commands.py
Adds type annotations and docstrings for Command and subclasses; makes cmd: int explicit; documents payload/data; validates LED RGB inputs (raises ValueError); corrects Vibrate2 duration byte extraction; expands imports to include classifier/EMG/IMU/vibration enums.
Core API typing & refactor
myo/core.py
Introduces comprehensive typing, async signatures, and docstrings for Myo and MyoClient; removes in-file AggregatedData/EMGDataSingle (moved to myo.types); tightens null-guards, logging, validation (e.g., LED), and refactors notify/handle derivation and routing.
New data types & bugfix
myo/types.py
Adds AggregatedData and EMGDataSingle classes (ctor, __str__, json, to_dict); fixes FirmwareInfo.to_dict to use self._unlock_pose for "unlock_pose".
Utility for GATT characteristic conversion
myo/utils.py
New async gatt_char_to_dict(client: BleakClient, char: BleakGATTCharacteristic) -> Optional[dict] converting characteristics to dicts with decoded values for known UUIDs (manufacturer, firmware, battery); includes module logger.
Tests updated
tests/test_myo/test_types.py
Adjusts expected FirmwareInfo serialization: unlock_pose is now a named pose string (e.g., 'DOUBLE_TAP') instead of a boolean.
CI workflow updates
.github/workflows/build.yml
Updates GitHub Actions versions (checkout/setup-python/ruff-action) and restricts pull_request trigger to main; matrix and steps otherwise retained.

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant MyoClient
  participant Bleak as BleakClient
  participant Utils as myo.utils.gatt_char_to_dict
  rect rgba(214,234,248,0.5)
    note right of MyoClient: Typed async API & notify routing
  end
  Caller->>MyoClient: with_device(mac, aggregate_*)
  MyoClient->>Bleak: connect() / get_services()
  Bleak-->>MyoClient: characteristics list
  loop each characteristic
    MyoClient->>Utils: gatt_char_to_dict(Bleak, char)
    Utils-->>MyoClient: dict(name, uuid, properties, value?)
  end
  MyoClient->>MyoClient: derive handles (EMG/IMU/classifier)
  Caller->>MyoClient: start() / start_notify(handles)
  MyoClient->>Bleak: start_notify(handle, notify_callback)
  Bleak->>MyoClient: notify_callback(sender, data)
  MyoClient->>MyoClient: route/convert data -> (FVData/IMUData/EMGData / AggregatedData)
  MyoClient-->>Caller: on_data / on_aggregated_data callbacks
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • Files/areas to focus on:
    • Async/await correctness, error paths, and return types in myo/core.py.
    • Cross-module type imports and forward references between myo.types, myo.core, and myo.commands.
    • LED input validation and Vibrate2 duration byte-order fix in myo/commands.py.
    • gatt_char_to_dict decoding logic and readable-property handling in myo/utils.py.
    • Test expectation change for FirmwareInfo.unlock_pose in tests/test_myo/test_types.py.

Poem

🐰
I nibbled bytes and hopped through types,
Docstrings soft as carrot stripes.
A util hums, aggregated friends,
EMG and firmware make amends.
Hooray — the library hops ahead.

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately captures the main changes: code refactoring with improved organization (moving types/utils) and comprehensive type hints added across multiple modules.
Docstring Coverage ✅ Passed Docstring coverage is 88.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch refactor/code-organization-and-type-hints

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1e3c816 and db4ed35.

📒 Files selected for processing (4)
  • myo/commands.py (6 hunks)
  • myo/core.py (5 hunks)
  • myo/types.py (5 hunks)
  • myo/utils.py (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
myo/utils.py (2)
myo/profile.py (1)
  • Handle (101-123)
myo/types.py (11)
  • FirmwareInfo (142-179)
  • FirmwareVersion (183-192)
  • to_dict (54-65)
  • to_dict (105-106)
  • to_dict (123-124)
  • to_dict (157-179)
  • to_dict (215-216)
  • to_dict (242-247)
  • to_dict (281-291)
  • to_dict (393-401)
  • to_dict (437-444)
myo/commands.py (1)
myo/types.py (7)
  • ClassifierMode (80-82)
  • EMGMode (130-137)
  • IMUMode (252-257)
  • SleepMode (320-322)
  • UnlockType (331-334)
  • UserActionType (338-339)
  • VibrationType (343-347)
myo/core.py (3)
myo/types.py (12)
  • json (51-52)
  • json (102-103)
  • json (120-121)
  • json (239-240)
  • json (278-279)
  • json (382-391)
  • json (426-435)
  • AggregatedData (358-401)
  • EMGData (94-106)
  • EMGDataSingle (404-444)
  • FVData (111-124)
  • IMUData (204-247)
myo/profile.py (2)
  • GATTProfile (10-97)
  • Handle (101-123)
myo/utils.py (1)
  • gatt_char_to_dict (20-69)

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

@iomz iomz self-assigned this Nov 8, 2025
@iomz iomz added the enhancement New feature or request label Nov 8, 2025
The test was expecting the old buggy behavior where unlock_pose
returned True (which was actually has_custom_classifier value).
Now it correctly expects 'DOUBLE_TAP' which is the actual unlock
pose name parsed from the firmware info blob.
@codecov
Copy link

codecov bot commented Nov 8, 2025

Codecov Report

❌ Patch coverage is 54.92958% with 32 lines in your changes missing coverage. Please review.
✅ Project coverage is 80.23%. Comparing base (e438c11) to head (db4ed35).
⚠️ Report is 9 commits behind head on main.

Files with missing lines Patch % Lines
myo/utils.py 30.00% 21 Missing ⚠️
myo/types.py 57.14% 9 Missing ⚠️
myo/commands.py 89.47% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main      #29      +/-   ##
==========================================
- Coverage   85.63%   80.23%   -5.41%     
==========================================
  Files           7        8       +1     
  Lines         376      425      +49     
  Branches       32       38       +6     
==========================================
+ Hits          322      341      +19     
- Misses         50       80      +30     
  Partials        4        4              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@iomz
Copy link
Owner Author

iomz commented Nov 8, 2025

@coderabbitai help

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 8, 2025

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

CodeRabbit commands

These commands are invoked using PR/Issue comments.

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai evaluate custom pre-merge check --instructions <custom-checks-instructions> --name <custom-checks-title> [--mode <error|warning>] to validate the custom pre-merge checks instructions. Defaults to error when --mode is omitted.
    • @coderabbitai ignore pre-merge checks to override pre-merge checks and get an approval on PR.
    • @coderabbitai run pre-merge checks to run pre-merge checks on the pull request.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai generate unit tests to generate unit tests for this PR.
  • @coderabbitai resolve to resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore or @coderabbit ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit configuration file (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • You can also validate your configuration using the online YAML validator.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, support, documentation and community

  • Visit our status page to check the current availability of CodeRabbit.
  • Create a ticket on our support page for assistance with any issues or questions.
  • Visit our documentation site for detailed information on how to use CodeRabbit.
  • Join our Discord community to connect with other users and get help from the community.
  • Follow us on X/Twitter for updates and announcements.

@iomz
Copy link
Owner Author

iomz commented Nov 8, 2025

@coderabbitai full review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 8, 2025

✅ Actions performed

Full review triggered.

Copy link
Contributor

@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 e438c11 and c607579.

📒 Files selected for processing (5)
  • myo/__init__.py (1 hunks)
  • myo/commands.py (6 hunks)
  • myo/core.py (5 hunks)
  • myo/types.py (2 hunks)
  • myo/utils.py (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (4)
myo/utils.py (2)
myo/profile.py (1)
  • Handle (101-123)
myo/types.py (11)
  • FirmwareInfo (143-167)
  • FirmwareVersion (171-180)
  • to_dict (55-66)
  • to_dict (106-107)
  • to_dict (124-125)
  • to_dict (158-167)
  • to_dict (203-204)
  • to_dict (230-235)
  • to_dict (269-279)
  • to_dict (359-360)
  • to_dict (375-376)
myo/__init__.py (2)
myo/core.py (2)
  • Myo (50-234)
  • MyoClient (237-701)
myo/types.py (5)
  • AggregatedData (346-360)
  • ClassifierEvent (27-66)
  • ClassifierMode (81-83)
  • EMGData (95-107)
  • EMGDataSingle (363-376)
myo/commands.py (1)
myo/types.py (7)
  • ClassifierMode (81-83)
  • EMGMode (131-138)
  • IMUMode (240-245)
  • SleepMode (308-310)
  • UnlockType (319-322)
  • UserActionType (326-327)
  • VibrationType (331-335)
myo/core.py (4)
myo/commands.py (10)
  • Command (19-37)
  • DeepSleep (83-89)
  • LED (93-116)
  • SetMode (41-65)
  • SetSleepMode (161-171)
  • Unlock (175-185)
  • UserAction (188-198)
  • Vibrate (69-79)
  • Vibrate2 (120-157)
  • data (30-34)
myo/profile.py (2)
  • GATTProfile (10-97)
  • Handle (101-123)
myo/types.py (18)
  • AggregatedData (346-360)
  • ClassifierMode (81-83)
  • EMGData (95-107)
  • EMGDataSingle (363-376)
  • EMGMode (131-138)
  • FVData (112-125)
  • IMUData (192-235)
  • IMUMode (240-245)
  • MotionEvent (250-279)
  • SleepMode (308-310)
  • VibrationType (331-335)
  • json (52-53)
  • json (103-104)
  • json (121-122)
  • json (227-228)
  • json (266-267)
  • json (356-357)
  • json (372-373)
myo/utils.py (1)
  • gatt_char_to_dict (19-57)

Copy link
Contributor

@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

♻️ Duplicate comments (2)
myo/utils.py (2)

44-53: Fix battery-level decoding

BleakClient.read_gatt_char returns a bytearray; calling ord on it raises TypeError, so the battery branch currently crashes. Please extract the first byte (and allow for empty payloads) instead.

-        elif char_name == Handle.BATTERY_LEVEL.name:
-            value = ord(blob)
+        elif char_name == Handle.BATTERY_LEVEL.name:
+            value = blob[0] if blob else None

55-56: Preserve falsy characteristic values

The if value: guard discards legitimate readings such as a 0% battery level. Only skip assignment when the value is None.

-    if value:
+    if value is not None:
         cd["value"] = value
🧹 Nitpick comments (2)
myo/core.py (2)

308-310: Consider removing unnecessary None check.

The BleakClient constructor won't return None according to the Bleak library documentation. It will either return a client instance or raise an exception. This None check may be unnecessary defensive code.


377-379: Consider using ABC for interface methods.

The callback methods (on_classifier_event, on_aggregated_data, on_emg_data, etc.) are meant to be overridden by subclasses. Consider using abc.ABC and @abstractmethod decorators for clearer intent and compile-time enforcement.

Example:

+from abc import ABC, abstractmethod
+
-class MyoClient:
+class MyoClient(ABC):
     ...
+    @abstractmethod
     async def on_classifier_event(self, ce: ClassifierEvent) -> None:
         """Handle classifier events. Override in subclasses."""
         raise NotImplementedError()

Also applies to: 401-412, 414-421, 423-430, 432-439, 441-448, 450-457

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e438c11 and 023beec.

📒 Files selected for processing (6)
  • myo/__init__.py (1 hunks)
  • myo/commands.py (6 hunks)
  • myo/core.py (5 hunks)
  • myo/types.py (2 hunks)
  • myo/utils.py (1 hunks)
  • tests/test_myo/test_types.py (1 hunks)
🔇 Additional comments (7)
myo/core.py (7)

10-11: LGTM! Good import organization.

The addition of type hints imports and moving gatt_char_to_dict to the utils module improves code organization.

Also applies to: 45-45


55-63: LGTM! Proper initialization and validation.

The explicit type hints and None-guard in the device property with a clear error message are excellent defensive programming practices.


66-114: LGTM! Robust discovery methods.

The factory methods have proper type hints, comprehensive docstrings, and good error handling with informative logging.


143-166: Excellent input validation!

The comprehensive validation of LED payload (length and value ranges) with clear error messages is a great defensive programming practice that prevents invalid data from reaching the device.


588-614: Excellent refactoring!

Extracting these helper methods (_get_emg_handles, _get_imu_handles, _get_classifier_handles) significantly improves code maintainability by:

  • Eliminating duplication between start() and stop() methods
  • Centralizing the logic for determining which handles to subscribe/unsubscribe
  • Making the code more readable with clear method names

616-638: LGTM! Robust notification management.

The start() and stop() methods properly validate state before operations and make excellent use of the helper methods for clean, maintainable code.

Also applies to: 646-671


297-301: LGTM! Consistent and thorough validation.

The consistent validation pattern across all methods ensures that:

  • Operations requiring connection check self._client is None
  • Operations requiring setup check mode state
  • Error messages are clear and actionable

This significantly improves API usability and debugging experience.

Also applies to: 315-319, 366-375, 504-520, 527-531, 547-549, 618-621, 640-644, 648-651, 673-677, 679-683, 685-689, 691-695, 697-701

Copy link
Contributor

@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: 3

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 023beec and c7690ab.

📒 Files selected for processing (3)
  • .github/workflows/build.yml (2 hunks)
  • myo/core.py (5 hunks)
  • myo/utils.py (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
myo/utils.py (2)
myo/profile.py (1)
  • Handle (101-123)
myo/types.py (11)
  • FirmwareInfo (143-167)
  • FirmwareVersion (171-180)
  • to_dict (55-66)
  • to_dict (106-107)
  • to_dict (124-125)
  • to_dict (158-167)
  • to_dict (203-204)
  • to_dict (230-235)
  • to_dict (269-279)
  • to_dict (359-360)
  • to_dict (375-376)
myo/core.py (4)
myo/types.py (17)
  • json (52-53)
  • json (103-104)
  • json (121-122)
  • json (227-228)
  • json (266-267)
  • json (356-357)
  • json (372-373)
  • AggregatedData (346-360)
  • ClassifierMode (81-83)
  • EMGData (95-107)
  • EMGDataSingle (363-376)
  • EMGMode (131-138)
  • FVData (112-125)
  • IMUData (192-235)
  • IMUMode (240-245)
  • SleepMode (308-310)
  • VibrationType (331-335)
myo/commands.py (10)
  • Command (19-37)
  • DeepSleep (83-89)
  • LED (93-116)
  • SetMode (41-65)
  • SetSleepMode (161-171)
  • Unlock (175-185)
  • UserAction (188-198)
  • Vibrate (69-79)
  • Vibrate2 (120-157)
  • data (30-34)
myo/profile.py (2)
  • GATTProfile (10-97)
  • Handle (101-123)
myo/utils.py (1)
  • gatt_char_to_dict (20-58)

Copy link
Contributor

@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: 0

🧹 Nitpick comments (3)
myo/core.py (3)

157-162: Consider using a helper for RGB validation.

The validation logic is correct but duplicated for logo and line. Since this is "Chill" mode, consider extracting a helper if this pattern repeats elsewhere.

For example:

def _validate_rgb(values: list[int], name: str) -> None:
    if len(values) != 3:
        raise ValueError(f"{name} must have 3 values (R, G, B)")
    if any(not isinstance(v, int) or not 0 <= v <= 255 for v in values):
        raise ValueError(f"{name} values must be integers 0-255")

276-283: Consider adding timeout or retry limit to device discovery.

The infinite loop will retry forever if the device is unavailable. For better UX, consider adding a timeout or max retry count with appropriate logging.

Example:

max_retries = 30  # 30 seconds
for attempt in range(max_retries):
    if mac and mac != "":
        self.m = await Myo.with_mac(mac)
    else:
        self.m = await Myo.with_uuid()
    
    if self.m is not None:
        break
    
    logger.warning("Device not found, retrying... (%d/%d)", attempt + 1, max_retries)
    await asyncio.sleep(1)
else:
    raise TimeoutError("Failed to discover device after 30 seconds")

306-310: Remove unnecessary None check; consider exception handling.

The BleakClient() constructor never returns None, so the check at lines 307-308 is dead code and can be removed.

Additionally, if connect() raises an exception, self._client is left assigned but not connected. Consider wrapping in try/except to clean up state on failure:

     self._client = BleakClient(self.device)
-    if self._client is None:
-        raise RuntimeError("Failed to create BLE client")
-
-    await self._client.connect()
+    try:
+        await self._client.connect()
+    except Exception:
+        self._client = None
+        raise
     logger.info("connected to %s: %s", self.device.name, self.device.address)
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c7690ab and a6a29cc.

📒 Files selected for processing (2)
  • .github/workflows/build.yml (2 hunks)
  • myo/core.py (5 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • .github/workflows/build.yml
🧰 Additional context used
🧬 Code graph analysis (1)
myo/core.py (4)
myo/types.py (19)
  • json (52-53)
  • json (103-104)
  • json (121-122)
  • json (227-228)
  • json (266-267)
  • json (356-357)
  • json (372-373)
  • AggregatedData (346-360)
  • ClassifierEvent (27-66)
  • ClassifierMode (81-83)
  • EMGData (95-107)
  • EMGDataSingle (363-376)
  • EMGMode (131-138)
  • FVData (112-125)
  • IMUData (192-235)
  • IMUMode (240-245)
  • MotionEvent (250-279)
  • SleepMode (308-310)
  • VibrationType (331-335)
myo/commands.py (9)
  • Command (19-37)
  • LED (93-116)
  • SetMode (41-65)
  • SetSleepMode (161-171)
  • Unlock (175-185)
  • UserAction (188-198)
  • Vibrate (69-79)
  • Vibrate2 (120-157)
  • data (30-34)
myo/profile.py (2)
  • GATTProfile (10-97)
  • Handle (101-123)
myo/utils.py (1)
  • gatt_char_to_dict (20-58)
🔇 Additional comments (15)
myo/core.py (15)

57-65: LGTM: Clean initialization and property access.

The type hints and validation ensure callers must use with_mac() or with_uuid() before accessing the device.


68-92: LGTM: MAC-based discovery is correctly implemented.

The case-insensitive comparison and error handling are appropriate.


103-107: LGTM: UUID discovery now handles None service_uuids safely.

The guard uuids = adv.service_uuids or [] prevents the TypeError flagged in the previous review.


116-129: LGTM: Battery level conversion now handles bytearray correctly.

The fix replaces ord(val) with int.from_bytes(val, "little") and adds validation, resolving the TypeError from the previous review.


203-217: LGTM: AttributeError handling is now defensive.

The use of getattr(client, "is_connected", None) in the debug log prevents re-raising the AttributeError, addressing the previous review concern.


131-232: LGTM: Command delegation methods are well-documented and typed.

The type hints and docstrings provide clear interfaces for all command methods.


238-255: LGTM: Well-documented initialization with clear aggregation flags.

The docstrings and type hints clarify the purpose of aggregate_all and aggregate_emg.


319-328: LGTM: Graceful handling of already-disconnected state.

The early return for None client prevents double-disconnect errors.


589-615: LGTM: Helper methods eliminate duplication.

Extracting these handle-selection methods makes start() and stop() much cleaner and reduces the risk of inconsistency.


462-503: LGTM: Notification routing correctly handles aggregation modes.

The conditional routing based on aggregate_all and aggregate_emg flags ensures data flows to the appropriate handlers.


564-572: LGTM: Mode enforcement for aggregate_all is appropriate.

Forcing specific modes when aggregate_all=True ensures the aggregation logic receives the required FVData and IMUData streams.


617-672: LGTM: Symmetric start/stop with proper mode validation.

The validation ensures setup() has been called before start() or stop(), and the helper methods keep the logic clean.


375-460: LGTM: Well-documented abstract interface for event handlers.

The docstrings clearly explain what data each handler receives, and the note about EMG vs FV/IMU intervals (line 404-405) is particularly helpful.


295-373: LGTM: Client wrapper methods properly validate and delegate.

All methods check connection state before delegating to the underlying Myo instance.


380-396: LGTM: Thread-safe aggregation with proper synchronization.

The lock protects the aggregation state, and the logic correctly waits for both FVData and IMUData before triggering the aggregated handler.

@iomz
Copy link
Owner Author

iomz commented Nov 8, 2025

@coderabbitai generate docstrings

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 8, 2025

✅ Actions performed

Initiated docstring generation; will generate only if new commits exist.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 8, 2025

Note

Docstrings generation - SUCCESS
Generated docstrings for this pull request at #30

coderabbitai bot added a commit that referenced this pull request Nov 8, 2025
Docstrings generation was requested by @iomz.

* #29 (comment)

The following files were modified:

* `myo/commands.py`
* `myo/core.py`
* `myo/types.py`
* `myo/utils.py`
Docstrings generation was requested by @iomz.

* #29 (comment)

The following files were modified:

* `myo/commands.py`
* `myo/core.py`
* `myo/types.py`
* `myo/utils.py`

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Copy link
Contributor

@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: 0

♻️ Duplicate comments (2)
myo/core.py (2)

126-138: Past issue resolved.

The UUID matching now safely handles None service_uuids by defaulting to an empty list, preventing the TypeError mentioned in previous reviews.


148-164: Past issue resolved.

The battery level reading now correctly uses int.from_bytes() with proper empty-payload validation, fixing the TypeError from the previous ord(val) usage.

🧹 Nitpick comments (3)
myo/utils.py (1)

41-68: Past review issues resolved; consider optional defensive error handling.

The fixes for battery level decoding (int.from_bytes with empty-blob guard) and the falsy-value check (is not None) correctly address the prior critical and major issues.

For additional robustness, consider wrapping UTF-8 decoding and struct unpacks in try-except blocks to handle malformed payloads gracefully, though this is optional given the controlled BLE environment.

Example defensive pattern:

try:
    value = blob.decode("utf-8")
except UnicodeDecodeError:
    logger.warning("Invalid UTF-8 in MANUFACTURER_NAME_STRING")
    value = binascii.b2a_hex(blob).decode("utf-8")
myo/commands.py (1)

86-86: Wrap long docstring lines to meet style guide.

Three docstring lines exceed the 120-character limit. Consider wrapping them for consistency with the project's linting rules.

Based on static analysis hints.

Also applies to: 127-127, 195-195

myo/core.py (1)

257-263: Logging safety improved; consider adding inline comment.

The use of getattr(client, "is_connected", None) correctly prevents the logger itself from raising an AttributeError. The docstring mentions the exception handling, but an inline comment explaining this is a known Bleak quirk would improve maintainability.

Example:

except AttributeError:
    # Known Bleak issue: is_connected may be missing on certain backends
    logger.debug(...)

Based on past review feedback.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a6a29cc and 1e3c816.

📒 Files selected for processing (4)
  • myo/commands.py (6 hunks)
  • myo/core.py (5 hunks)
  • myo/types.py (2 hunks)
  • myo/utils.py (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
myo/utils.py (2)
myo/profile.py (1)
  • Handle (101-123)
myo/types.py (11)
  • FirmwareInfo (143-180)
  • FirmwareVersion (184-193)
  • to_dict (55-66)
  • to_dict (106-107)
  • to_dict (124-125)
  • to_dict (158-180)
  • to_dict (216-217)
  • to_dict (243-248)
  • to_dict (282-292)
  • to_dict (393-401)
  • to_dict (436-443)
myo/core.py (5)
myo/types.py (19)
  • json (52-53)
  • json (103-104)
  • json (121-122)
  • json (240-241)
  • json (279-280)
  • json (382-391)
  • json (425-434)
  • AggregatedData (359-401)
  • ClassifierEvent (27-66)
  • ClassifierMode (81-83)
  • EMGData (95-107)
  • EMGDataSingle (404-443)
  • EMGMode (131-138)
  • FVData (112-125)
  • IMUData (205-248)
  • IMUMode (253-258)
  • MotionEvent (263-292)
  • SleepMode (321-323)
  • VibrationType (344-348)
myo/commands.py (10)
  • Command (19-53)
  • DeepSleep (118-129)
  • LED (133-156)
  • SetMode (57-94)
  • SetSleepMode (207-223)
  • Unlock (227-243)
  • UserAction (246-262)
  • Vibrate (98-114)
  • Vibrate2 (160-203)
  • data (35-44)
myo/profile.py (2)
  • GATTProfile (10-97)
  • Handle (101-123)
myo/utils.py (1)
  • gatt_char_to_dict (20-68)
examples/sample_client.py (6)
  • on_classifier_event (24-26)
  • on_aggregated_data (28-29)
  • on_emg_data (31-33)
  • on_fv_data (35-37)
  • on_imu_data (39-41)
  • on_motion_event (43-44)
myo/commands.py (1)
myo/types.py (7)
  • ClassifierMode (81-83)
  • EMGMode (131-138)
  • IMUMode (253-258)
  • SleepMode (321-323)
  • UnlockType (332-335)
  • UserActionType (339-340)
  • VibrationType (344-348)
🪛 GitHub Actions: build
myo/commands.py

[error] 86-86: E501 Line too long (138 > 120)

🪛 GitHub Check: ruff
myo/core.py

[failure] 188-188: Ruff (E501)
myo/core.py:188:121: E501 Line too long (121 > 120)


[failure] 171-171: Ruff (E501)
myo/core.py:171:121: E501 Line too long (139 > 120)


[failure] 134-134: Ruff (E501)
myo/core.py:134:121: E501 Line too long (152 > 120)


[failure] 61-61: Ruff (E501)
myo/core.py:61:121: E501 Line too long (124 > 120)


[failure] 255-255: Ruff (E501)
myo/core.py:255:121: E501 Line too long (211 > 120)


[failure] 247-247: Ruff (E501)
myo/core.py:247:121: E501 Line too long (132 > 120)


[failure] 238-238: Ruff (E501)
myo/core.py:238:121: E501 Line too long (134 > 120)

myo/commands.py

[failure] 86-86: Ruff (E501)
myo/commands.py:86:121: E501 Line too long (138 > 120)


[failure] 127-127: Ruff (E501)
myo/commands.py:127:121: E501 Line too long (127 > 120)


[failure] 195-195: Ruff (E501)
myo/commands.py:195:121: E501 Line too long (127 > 120)

🔇 Additional comments (10)
myo/commands.py (3)

19-53: LGTM!

The type hints and docstrings for the Command base class are clear, accurate, and improve code maintainability.


197-203: Critical bug fix verified.

The high-byte extraction now correctly uses >> 8 instead of the previous >> 0xFF (which would have shifted by 255 bits). This properly encodes the 16-bit duration as two bytes in the payload.


138-156: LGTM!

The input validation for LED RGB data is a good defensive addition that will catch malformed inputs early with a clear error message.

myo/types.py (3)

158-180: Critical bug fix verified.

The to_dict() method now correctly maps "unlock_pose" to self._unlock_pose instead of incorrectly returning self._has_custom_classifier. This aligns with the __init__ logic and ensures the serialized firmware info contains the correct unlock pose name.


359-401: LGTM!

The AggregatedData class is well-structured with clear type hints and comprehensive docstrings. The move from core.py to types.py improves code organization.


404-443: LGTM!

The EMGDataSingle class provides a clean wrapper for single EMG samples with proper type hints and docstrings. Moving it to types.py improves module organization.

myo/core.py (4)

179-197: LGTM!

The LED validation is comprehensive, checking length, type, and value range (0-255) with clear error messages. This defensive approach prevents invalid data from reaching the device.


705-746: LGTM!

The helper methods _get_emg_handles, _get_imu_handles, and _get_classifier_handles cleanly encapsulate the mode-to-handle mapping logic, reducing duplication in the start/stop methods.


365-367: LGTM!

The consistent state validation across methods (_client is None checks, mode validation) with clear error messages significantly improves the API's safety and user experience.

Also applies to: 394-396, 428-430, 462-464, 621-622, 640-642, 660-662, 757-760, 790-792, 803-806, 838-840, 852-854, 866-868, 880-882, 895-897


287-560: LGTM!

Converting MyoClient to an ABC with abstract handler methods enforces a clear contract for subclasses and improves type safety. The type hints and docstrings for each abstract method are accurate and comprehensive.

@iomz iomz merged commit c385880 into main Nov 8, 2025
6 of 8 checks passed
@iomz iomz deleted the refactor/code-organization-and-type-hints branch November 8, 2025 09:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant