Skip to content

Fix type return and service characteristics returns#180

Merged
RonanB96 merged 2 commits intomainfrom
fix/upstream-consumer-improvements
Mar 18, 2026
Merged

Fix type return and service characteristics returns#180
RonanB96 merged 2 commits intomainfrom
fix/upstream-consumer-improvements

Conversation

@RonanB96
Copy link
Owner

No description provided.

Copilot AI review requested due to automatic review settings March 16, 2026 11:55
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

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_type overrides from ~30 characteristics, updates generic type parameters to use proper enum types (e.g., BaseCharacteristic[int]BaseCharacteristic[AlertLevel]), and removes the properties parameter from BaseCharacteristic.__init__.
  • Rewrites role_classifier.py with a 6-tier classification system, adds _manual_role overrides 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 use list[CharacteristicTestData] consistently, and normalizes UnknownCharacteristic naming.

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}...)",
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))
@RonanB96 RonanB96 force-pushed the fix/upstream-consumer-improvements branch from 8d0a465 to 211581b Compare March 16, 2026 16:42
- 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
@RonanB96 RonanB96 force-pushed the fix/upstream-consumer-improvements branch from 211581b to a709166 Compare March 18, 2026 10:45
@RonanB96 RonanB96 merged commit dcf8b8e into main Mar 18, 2026
11 checks passed
@RonanB96 RonanB96 deleted the fix/upstream-consumer-improvements branch March 18, 2026 11:14
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.

2 participants