Compare commits

...

11 Commits

Author SHA1 Message Date
Gilles Boccon-Gibod
b50a48fed3 add pyee group util 2023-09-19 16:14:53 -07:00
David Duarte
393ea6a7bb pandora_server: Load server config
Pandora server has it's own config that we load from the 'server'
property of the current bumble config file
2023-09-18 14:28:42 -07:00
Gilles Boccon-Gibod
0d36d99a73 Merge pull request #287 from google/revert-286-gbg/package-depencencies-for-wasm
Revert "make cryptography a valid dependency for emscripten targets"
2023-09-13 23:37:42 -07:00
Gilles Boccon-Gibod
d8a9f5a724 Revert "make cryptography a valid dependency for emscripten targets" 2023-09-13 23:36:33 -07:00
Gilles Boccon-Gibod
2c66e1a042 Merge pull request #285 from google/gbg/fix-mypy-errors
mypy: ignore false positive errors
2023-09-13 23:30:50 -07:00
Gilles Boccon-Gibod
d5eccdb00f Merge pull request #286 from google/gbg/package-depencencies-for-wasm
make cryptography a valid dependency for emscripten targets
2023-09-13 23:30:28 -07:00
Gilles Boccon-Gibod
32626573a6 ignore false positive errors 2023-09-13 23:17:00 -07:00
Gilles Boccon-Gibod
caa82b8f7e make cryptography a valid dependency for emscripten targets 2023-09-13 22:38:28 -07:00
Gilles Boccon-Gibod
5af347b499 Merge pull request #282 from google/gbg/multi-python-pre-commit-check
run pre-commit tests with all supported Python versions
2023-09-13 07:47:32 -07:00
zxzxwu
4ed5bb5a9e Merge pull request #281 from zxzxwu/cleanup-transport
Replace | typing usage with Optional and Union
2023-09-13 13:31:41 +08:00
Josh Wu
f39f5f531c Replace | typing usage with Optional and Union 2023-09-12 15:50:51 +08:00
12 changed files with 239 additions and 23 deletions

View File

@@ -3,7 +3,7 @@ import click
import logging
import json
from bumble.pandora import PandoraDevice, serve
from bumble.pandora import PandoraDevice, Config, serve
from typing import Dict, Any
BUMBLE_SERVER_GRPC_PORT = 7999
@@ -29,12 +29,14 @@ def main(grpc_port: int, rootcanal_port: int, transport: str, config: str) -> No
transport = transport.replace('<rootcanal-port>', str(rootcanal_port))
bumble_config = retrieve_config(config)
if 'transport' not in bumble_config.keys():
bumble_config.update({'transport': transport})
bumble_config.setdefault('transport', transport)
device = PandoraDevice(bumble_config)
server_config = Config()
server_config.load_from_dict(bumble_config.get('server', {}))
logging.basicConfig(level=logging.DEBUG)
asyncio.run(serve(device, port=grpc_port))
asyncio.run(serve(device, config=server_config, port=grpc_port))
def retrieve_config(config: str) -> Dict[str, Any]:

View File

@@ -80,7 +80,7 @@ class BaseError(Exception):
def __init__(
self,
error_code: int | None,
error_code: Optional[int],
error_namespace: str = '',
error_name: str = '',
details: str = '',

View File

@@ -4397,7 +4397,7 @@ class HCI_Event(HCI_Packet):
if len(parameters) != length:
raise ValueError('invalid packet length')
cls: Type[HCI_Event | HCI_LE_Meta_Event] | None
cls: Any
if event_code == HCI_LE_META_EVENT:
# We do this dispatch here and not in the subclass in order to avoid call
# loops

View File

@@ -757,7 +757,7 @@ class Channel(EventEmitter):
)
self.state = new_state
def send_pdu(self, pdu: SupportsBytes | bytes) -> None:
def send_pdu(self, pdu: Union[SupportsBytes, bytes]) -> None:
self.manager.send_pdu(self.connection, self.destination_cid, pdu)
def send_control_frame(self, frame: L2CAP_Control_Frame) -> None:
@@ -1098,7 +1098,7 @@ class LeConnectionOrientedChannel(EventEmitter):
elif new_state == self.DISCONNECTED:
self.emit('close')
def send_pdu(self, pdu: SupportsBytes | bytes) -> None:
def send_pdu(self, pdu: Union[SupportsBytes, bytes]) -> None:
self.manager.send_pdu(self.connection, self.destination_cid, pdu)
def send_control_frame(self, frame: L2CAP_Control_Frame) -> None:
@@ -1571,7 +1571,7 @@ class ChannelManager:
if connection_handle in self.identifiers:
del self.identifiers[connection_handle]
def send_pdu(self, connection, cid: int, pdu: SupportsBytes | bytes) -> None:
def send_pdu(self, connection, cid: int, pdu: Union[SupportsBytes, bytes]) -> None:
pdu_str = pdu.hex() if isinstance(pdu, bytes) else str(pdu)
logger.debug(
f'{color(">>> Sending L2CAP PDU", "blue")} '

View File

@@ -18,6 +18,8 @@
import logging
import grpc.aio
from typing import Optional, Union
from .common import PumpedTransport, PumpedPacketSource, PumpedPacketSink, Transport
# pylint: disable=no-name-in-module
@@ -33,7 +35,7 @@ logger = logging.getLogger(__name__)
# -----------------------------------------------------------------------------
async def open_android_emulator_transport(spec: str | None) -> Transport:
async def open_android_emulator_transport(spec: Optional[str]) -> Transport:
'''
Open a transport connection to an Android emulator via its gRPC interface.
The parameter string has this syntax:
@@ -82,7 +84,7 @@ async def open_android_emulator_transport(spec: str | None) -> Transport:
logger.debug(f'connecting to gRPC server at {server_address}')
channel = grpc.aio.insecure_channel(server_address)
service: EmulatedBluetoothServiceStub | VhciForwardingServiceStub
service: Union[EmulatedBluetoothServiceStub, VhciForwardingServiceStub]
if mode == 'host':
# Connect as a host
service = EmulatedBluetoothServiceStub(channel)

View File

@@ -122,7 +122,7 @@ def publish_grpc_port(grpc_port) -> bool:
# -----------------------------------------------------------------------------
async def open_android_netsim_controller_transport(
server_host: str | None, server_port: int
server_host: Optional[str], server_port: int
) -> Transport:
if not server_port:
raise ValueError('invalid port')

View File

@@ -23,6 +23,8 @@ import socket
import ctypes
import collections
from typing import Optional
from .common import Transport, ParserSource
@@ -33,7 +35,7 @@ logger = logging.getLogger(__name__)
# -----------------------------------------------------------------------------
async def open_hci_socket_transport(spec: str | None) -> Transport:
async def open_hci_socket_transport(spec: Optional[str]) -> Transport:
'''
Open an HCI Socket (only available on some platforms).
The parameter string is either empty (to use the first/default Bluetooth adapter)
@@ -45,9 +47,9 @@ async def open_hci_socket_transport(spec: str | None) -> Transport:
# Create a raw HCI socket
try:
hci_socket = socket.socket(
socket.AF_BLUETOOTH,
socket.SOCK_RAW | socket.SOCK_NONBLOCK,
socket.BTPROTO_HCI, # type: ignore
socket.AF_BLUETOOTH, # type: ignore[attr-defined]
socket.SOCK_RAW | socket.SOCK_NONBLOCK, # type: ignore[attr-defined]
socket.BTPROTO_HCI, # type: ignore[attr-defined]
)
except AttributeError as error:
# Not supported on this platform
@@ -78,7 +80,7 @@ async def open_hci_socket_transport(spec: str | None) -> Transport:
bind_address = struct.pack(
# pylint: disable=no-member
'<HHH',
socket.AF_BLUETOOTH,
socket.AF_BLUETOOTH, # type: ignore[attr-defined]
adapter_index,
HCI_CHANNEL_USER,
)

View File

@@ -23,6 +23,8 @@ import atexit
import os
import logging
from typing import Optional
from .common import Transport, StreamPacketSource, StreamPacketSink
# -----------------------------------------------------------------------------
@@ -32,7 +34,7 @@ logger = logging.getLogger(__name__)
# -----------------------------------------------------------------------------
async def open_pty_transport(spec: str | None) -> Transport:
async def open_pty_transport(spec: Optional[str]) -> Transport:
'''
Open a PTY transport.
The parameter string may be empty, or a path name where a symbolic link

View File

@@ -17,6 +17,8 @@
# -----------------------------------------------------------------------------
import logging
from typing import Optional
from .common import Transport
from .file import open_file_transport
@@ -27,7 +29,7 @@ logger = logging.getLogger(__name__)
# -----------------------------------------------------------------------------
async def open_vhci_transport(spec: str | None) -> Transport:
async def open_vhci_transport(spec: Optional[str]) -> Transport:
'''
Open a VHCI transport (only available on some platforms).
The parameter string is either empty (to use the default VHCI device

View File

@@ -19,9 +19,19 @@ import asyncio
import logging
import traceback
import collections
from contextlib import ExitStack
import sys
from typing import Awaitable, Set, TypeVar
from functools import wraps
from typing import (
Awaitable,
Callable,
ContextManager,
Mapping,
Optional,
Set,
Tuple,
TypeVar,
)
import functools
from pyee import EventEmitter
from .colors import color
@@ -167,7 +177,7 @@ class AsyncRunner:
"""
def decorator(func):
@wraps(func)
@functools.wraps(func)
def wrapper(*args, **kwargs):
coroutine = func(*args, **kwargs)
if queue is None:
@@ -302,3 +312,77 @@ class FlowControlAsyncPipe:
self.resume_source()
self.check_pump()
# -----------------------------------------------------------------------------
def event_emitter_once_for_group(
emitter: EventEmitter,
handlers: Mapping[str, Callable],
context: Optional[ContextManager] = None,
) -> None:
"""
Register event listeners as a group, with optional context manager.
For each entry in the map, an event listener is registered with the emitter.
When any of the event names in the handlers map is emitted by the emitter,
the corresponding handler is invoked, then all of the listeners are removed from
the emitter.
If a context manager is specified, it will be entered prior to registering the
listeners, and exited when any of the events is emitted.
Args:
emitter:
The event emitter with which to register the event listeners.
handlers:
A map that associates an event name with an event handler.
context:
A context manager to manager resources, or None if not needed.
"""
event_emitters_once_for_group(
{(emitter, event_name): handler for event_name, handler in handlers.items()},
context,
)
# -----------------------------------------------------------------------------
def event_emitters_once_for_group(
handlers: Mapping[Tuple[EventEmitter, str], Callable],
context: Optional[ContextManager] = None,
) -> None:
"""
Register event listeners as a group, with optional context manager.
Similar to event_emitter_once_for_group, but instead of registering the listeners
with a single emitter, each event may by associate with a different emitter.
Args:
handlers:
A map that associates an (emitter, event_name) pair with an event handler.
context:
A context manager to manager resources, or None if not needed.
"""
# Setup an exit stack to enter and exit the context, if any.
if context is not None:
exit_stack = ExitStack()
exit_stack.enter_context(context)
else:
exit_stack = None
def on_event(handler, *args, **kwargs) -> None:
# Invoke the handler.
handler(*args, **kwargs)
# Release the context, if any.
if exit_stack is not None:
exit_stack.close()
# Remove all listeners.
for (emitter, event_name), listener in listeners.items():
emitter.remove_listener(event_name, listener)
listeners = {
(emitter, event_name): emitter.on(
event_name, functools.partial(on_event, handler)
)
for (emitter, event_name), handler in handlers.items()
}

View File

@@ -84,7 +84,7 @@ development =
black == 22.10
grpcio-tools >= 1.57.0
invoke >= 1.7.3
mypy == 1.2.0
mypy == 1.5.0
nox >= 2022
pylint == 2.15.8
types-appdirs >= 1.4.3

122
tests/utils_test.py Normal file
View File

@@ -0,0 +1,122 @@
# Copyright 2021-2022 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# -----------------------------------------------------------------------------
# Imports
# -----------------------------------------------------------------------------
import contextlib
from pyee import EventEmitter
from bumble.utils import event_emitter_once_for_group, event_emitters_once_for_group
# -----------------------------------------------------------------------------
def test_event_emitter_once_for_group():
results = {'event1': None, 'event2': None, 'released': 0}
def handler1(x):
results['event1'] = x
def handler2(y):
results['event2'] = y
emitter = EventEmitter()
event_emitter_once_for_group(
emitter,
{
'event1': handler1,
'event2': handler2,
},
)
emitter.emit('event1', 'hello')
assert results['event1'] == 'hello'
assert results['event2'] is None
results['event1'] = None
emitter.emit('event1', 'hello')
emitter.emit('event2', 1234)
assert results['event1'] is None
assert results['event2'] is None
@contextlib.contextmanager
def managed():
try:
yield 1234
finally:
results['released'] += 1
event_emitter_once_for_group(
emitter,
{
'event1': handler1,
'event2': handler2,
},
managed(),
)
assert results['released'] == 0
emitter.emit('event2', 7756)
assert results['event1'] is None
assert results['event2'] == 7756
assert results['released'] == 1
# -----------------------------------------------------------------------------
def test_event_emitters_once_for_group():
results = {'event1': None, 'event2': None, 'released': 0}
def handler1(x):
results['event1'] = x
def handler2(y):
results['event2'] = y
emitter1 = EventEmitter()
emitter2 = EventEmitter()
event_emitters_once_for_group(
{
(emitter1, 'event1'): handler1,
(emitter2, 'event2'): handler2,
},
)
emitter1.emit('event1', 'hello')
emitter2.emit('event1', 'foobar')
assert results['event1'] == 'hello'
assert results['event2'] is None
results['event1'] = None
emitter1.emit('event1', 'hello')
emitter1.emit('event2', 1234)
emitter2.emit('event1', 'hello')
emitter2.emit('event2', 1234)
assert results['event1'] is None
assert results['event2'] is None
# -----------------------------------------------------------------------------
if __name__ == '__main__':
test_event_emitter_once_for_group()
test_event_emitters_once_for_group()