Fix type return and service characteristics returns#180
Merged
Conversation
There was a problem hiding this comment.
Pull request overview
This PR fixes type return values and service characteristics return types across the Bluetooth SIG library. It removes _python_type overrides from characteristics (letting the generic parameter auto-resolve), updates enum-based characteristics to use proper enum types in their generic parameters, reworks the role classifier with a tiered heuristic, changes get_service_characteristics to return instantiated characteristic objects instead of UUID strings, and adds consumer utility functions.
Changes:
- Removes
_python_typeoverrides from ~30 characteristics, updates generic type parameters to use proper enum types (e.g.,BaseCharacteristic[int]→BaseCharacteristic[AlertLevel]), and removes thepropertiesparameter fromBaseCharacteristic.__init__. - Rewrites
role_classifier.pywith a 6-tier classification system, adds_manual_roleoverrides to characteristics that can't be auto-classified, and adds new unit resolution utilities (unit_symbol,get_field_unit,FieldSpec.unit_symbol). - Adds consumer utilities (
prewarm_registries,to_primitive,is_struct_value), updates test fixtures to uselist[CharacteristicTestData]consistently, and normalizesUnknownCharacteristicnaming.
Reviewed changes
Copilot reviewed 169 out of 170 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
src/bluetooth_sig/gatt/characteristics/base.py |
Removes properties param, adds unit_symbol and get_field_unit methods |
src/bluetooth_sig/gatt/characteristics/role_classifier.py |
Complete rewrite with 6-tier classification heuristic |
src/bluetooth_sig/gatt/characteristics/unknown.py |
Normalizes naming to "Unknown: " format, removes properties param |
src/bluetooth_sig/core/query.py |
Changes get_service_characteristics to return characteristic instances |
src/bluetooth_sig/core/translator.py |
Updates return type documentation for get_service_characteristics |
src/bluetooth_sig/utils/values.py |
New to_primitive and is_struct_value utilities |
src/bluetooth_sig/utils/prewarm.py |
New prewarm_registries function |
src/bluetooth_sig/types/registry/gss_characteristic.py |
Adds LRU caching for computed properties, new unit_symbol property |
src/bluetooth_sig/types/registry/common.py |
Adds unit_name field and property |
src/bluetooth_sig/types/registry/units.py |
Adds readable_name property |
src/bluetooth_sig/registry/uuids/units.py |
Adds unit symbol entries and resolve_unit_symbol function |
src/bluetooth_sig/registry/gss.py |
Case-insensitive GSS spec lookup |
src/bluetooth_sig/registry/base.py |
Adds public ensure_loaded() method |
src/bluetooth_sig/gatt/uuid_registry.py |
Multi-field struct handling, unit_name resolution |
src/bluetooth_sig/gatt/services/base.py |
Removes properties from UnknownCharacteristic creation |
src/bluetooth_sig/types/gatt_enums.py |
Updated CharacteristicRole docstrings |
src/bluetooth_sig/types/advertising/ad_structures.py |
Adds from_common_fields factory method |
src/bluetooth_sig/__init__.py |
Exports new utility functions |
| ~30 characteristic files | Remove _python_type, update generic params, add _manual_role |
| ~30 test files | Update fixture return types, use enum values in assertions |
tests/gatt/characteristics/test_characteristic_role.py |
Expanded role tests, removed UNKNOWN class |
tests/gatt/characteristics/test_characteristic_common.py |
Simplify union types to list[CharacteristicTestData] |
tests/docs/test_readme_badges.py |
Handle socket-blocked exceptions gracefully |
examples/connection_managers/*.py |
Remove properties parameter from constructors |
scripts/*.py |
Minor cleanup (Optional→union, f-string fixes, import reorder) |
You can also share your feedback on Copilot code review. Take the survey.
| # Fallback: Create UnknownCharacteristic for unrecognized UUIDs | ||
| char_info = CharacteristicInfo( | ||
| uuid=char_uuid, | ||
| name=f"Unknown Characteristic ({char_uuid.short_form}...)", |
| # Fallback: Create UnknownCharacteristic for unrecognized UUIDs | ||
| char_info = CharacteristicInfo( | ||
| uuid=char_uuid, | ||
| name=f"Unknown Characteristic ({char_uuid.short_form}...)", |
| # Fallback: Create UnknownCharacteristic for unrecognized UUIDs | ||
| char_info = CharacteristicInfo( | ||
| uuid=char_uuid, | ||
| name=char.description or f"Unknown Characteristic ({char_uuid.short_form}...)", |
src/bluetooth_sig/utils/values.py
Outdated
Comment on lines
+57
to
+58
| if (name := getattr(value, "name", None)) is not None: | ||
| return str(name) |
Comment on lines
+15
to
+65
| def is_struct_value(obj: object) -> bool: | ||
| """Check whether *obj* is a parsed struct produced by the library. | ||
|
|
||
| Use this instead of ``hasattr(obj, '__struct_fields__')`` so consumer | ||
| code does not depend on the msgspec implementation detail. | ||
|
|
||
| Args: | ||
| obj: Any parsed characteristic value. | ||
|
|
||
| Returns: | ||
| ``True`` if *obj* is a ``msgspec.Struct`` instance. | ||
|
|
||
| """ | ||
| return isinstance(obj, msgspec.Struct) | ||
|
|
||
|
|
||
| def to_primitive(value: Any) -> int | float | str | bool: # noqa: ANN401 | ||
| """Coerce a parsed characteristic value to a plain Python primitive. | ||
|
|
||
| Handles the full range of types the library may return | ||
| (``bool``, ``IntFlag``, ``IntEnum``, ``Enum``, ``int``, ``float``, | ||
| ``str``, ``datetime``, ``timedelta``, msgspec Structs, …). | ||
|
|
||
| **Order matters:** | ||
|
|
||
| * ``bool`` before ``int`` — ``bool`` is a subclass of ``int``. | ||
| * ``IntFlag`` before the ``.name`` branch — bit-field values expose | ||
| a ``.name`` attribute but should be stored as a plain ``int``. | ||
| * ``IntEnum`` / ``Enum`` → ``.name`` string. | ||
|
|
||
| Args: | ||
| value: Any value returned by ``BaseCharacteristic.parse_value()`` | ||
| or extracted from a struct field. | ||
|
|
||
| Returns: | ||
| A plain ``int``, ``float``, ``str``, or ``bool``. | ||
|
|
||
| """ | ||
| if isinstance(value, bool): | ||
| return value | ||
| if isinstance(value, enum.IntFlag): | ||
| return int(value) | ||
| if (name := getattr(value, "name", None)) is not None: | ||
| return str(name) | ||
| if isinstance(value, int): | ||
| return int(value) | ||
| if isinstance(value, float): | ||
| return value | ||
| if isinstance(value, str): | ||
| return value | ||
| return str(value) |
Comment on lines
+430
to
444
| def get_service_characteristics(self, service_uuid: str) -> list[BaseCharacteristic[Any]]: | ||
| """Get the characteristic instances associated with a service. | ||
|
|
||
| Instantiates each required characteristic class from the service | ||
| definition and returns the live objects. | ||
|
|
||
| Args: | ||
| service_uuid: The service UUID | ||
|
|
||
| Returns: | ||
| List of characteristic UUIDs for this service | ||
| List of BaseCharacteristic instances for this service's | ||
| required characteristics. | ||
|
|
||
| """ | ||
| return self._query.get_service_characteristics(service_uuid) |
| """ | ||
|
|
||
| _template = EnumTemplate.uint8(AccelerationDetectionStatus) | ||
| _template = cast(CodingTemplate[AccelerationDetectionStatus], EnumTemplate.uint8(AccelerationDetectionStatus)) |
8d0a465 to
211581b
Compare
- Remove _python_type overrides from 40 characteristic classes (metadata-only, not used for decoding; contradicted BaseCharacteristic[T] generic param) - Remove _manual_role from 10 enum characteristics (classifier rule 12 handles enum→STATUS automatically) - Enforce list[CharacteristicTestData] return type in valid_test_data fixture (base class + 55 subclass test files); remove isinstance guards - Move Appearance, PnP ID, System ID from MEASUREMENT to INFO test group (they have _manual_role=INFO — device metadata, not sensor data) - Refactor classify_role() into 5 tier functions to fix PLR0911 (too many return statements) and remove ERA001 false-positive comment - Fix test_alert_level: pass AlertLevel(int) instead of raw int to build_value() - Fix examples: remove invalid properties kwarg from BaseCharacteristic and UnknownCharacteristic constructors in 3 connection manager examples - Fix test_readme_badges: add socket_enabled fixture and graceful skip when pytest-socket blocks network access - Fix test_magnetic_flux_density_2d/3d: correct python_type assertions
211581b to
a709166
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.