1
0
Fork 0

Switch to asyncclick for the CLI

In Python 3.9.6 after the BLE connection the RuntimeError "got Future
<Future pending> attached to a different loop" was raised: this usually
is due to multiple loop and the decorator to make click command async
was calling asyncio.run.
This commit is contained in:
Daniele Tricoli 2023-02-11 21:49:17 +01:00
parent b5292d922b
commit 6a26b4a503
4 changed files with 92 additions and 41 deletions

77
poetry.lock generated
View file

@ -1,5 +1,26 @@
# This file is automatically @generated by Poetry and should not be changed by hand.
[[package]]
name = "anyio"
version = "3.6.2"
description = "High level compatibility layer for multiple asynchronous event loop implementations"
category = "main"
optional = false
python-versions = ">=3.6.2"
files = [
{file = "anyio-3.6.2-py3-none-any.whl", hash = "sha256:fbbe32bd270d2a2ef3ed1c5d45041250284e31fc0a4df4a5a6071842051a51e3"},
{file = "anyio-3.6.2.tar.gz", hash = "sha256:25ea0d673ae30af41a0c442f81cf3b38c7e79fdc7b60335a4c14e05eb0947421"},
]
[package.dependencies]
idna = ">=2.8"
sniffio = ">=1.1"
[package.extras]
doc = ["packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"]
test = ["contextlib2", "coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "mock (>=4)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (<0.15)", "uvloop (>=0.15)"]
trio = ["trio (>=0.16,<0.22)"]
[[package]]
name = "async-timeout"
version = "4.0.2"
@ -12,6 +33,21 @@ files = [
{file = "async_timeout-4.0.2-py3-none-any.whl", hash = "sha256:8ca1e4fcf50d07413d66d1a5e416e42cfdf5851c981d679a09851a6853383b3c"},
]
[[package]]
name = "asyncclick"
version = "8.1.3.4"
description = "Composable command line interface toolkit, async version"
category = "main"
optional = false
python-versions = ">=3.7"
files = [
{file = "asyncclick-8.1.3.4-py3-none-any.whl", hash = "sha256:f8db604e37dabd43922d58f857817b1dfd8f88695b75c4cc1afe7ff1cc238a7b"},
{file = "asyncclick-8.1.3.4.tar.gz", hash = "sha256:81d98cbf6c8813f9cd5599f586d56cfc532e9e6441391974d10827abb90fe833"},
]
[package.dependencies]
colorama = {version = "*", markers = "platform_system == \"Windows\""}
[[package]]
name = "ble-serial"
version = "2.7.0"
@ -70,21 +106,6 @@ files = [
{file = "bleak_winrt-1.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:63130c11acfe75c504a79c01f9919e87f009f5e742bfc7b7a5c2a9c72bf591a7"},
]
[[package]]
name = "click"
version = "8.1.3"
description = "Composable command line interface toolkit"
category = "main"
optional = false
python-versions = ">=3.7"
files = [
{file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"},
{file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"},
]
[package.dependencies]
colorama = {version = "*", markers = "platform_system == \"Windows\""}
[[package]]
name = "colorama"
version = "0.4.6"
@ -171,6 +192,18 @@ files = [
[package.dependencies]
pyreadline3 = {version = "*", markers = "sys_platform == \"win32\" and python_version >= \"3.8\""}
[[package]]
name = "idna"
version = "3.4"
description = "Internationalized Domain Names in Applications (IDNA)"
category = "main"
optional = false
python-versions = ">=3.5"
files = [
{file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"},
{file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"},
]
[[package]]
name = "prompt-toolkit"
version = "3.0.36"
@ -291,6 +324,18 @@ files = [
[package.extras]
cp2110 = ["hidapi"]
[[package]]
name = "sniffio"
version = "1.3.0"
description = "Sniff out which async library your code is running under"
category = "main"
optional = false
python-versions = ">=3.7"
files = [
{file = "sniffio-1.3.0-py3-none-any.whl", hash = "sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384"},
{file = "sniffio-1.3.0.tar.gz", hash = "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101"},
]
[[package]]
name = "wcwidth"
version = "0.2.6"
@ -306,4 +351,4 @@ files = [
[metadata]
lock-version = "2.0"
python-versions = "^3.9"
content-hash = "cd6d12cd0c07704626be7124ec000b24b98b7cdda9b3d6bcb6be6e67edcd7482"
content-hash = "3b363cd19c44345908dbbd37685e525186b42bb24d260875b8e06fb3e5f49b2c"

View file

@ -17,11 +17,12 @@ packages = [{include = "freakble", from = "src"}]
[tool.poetry.dependencies]
python = "^3.9"
ble-serial = "^2.7.0"
click = "^8.1.3"
prompt-toolkit = "^3.0.36"
asyncclick = "^8.1.3.4"
anyio = "^3.6.2"
[tool.poetry.scripts]
freakble = "freakble.main:run"
freakble = "freakble.__main__:run"
[build-system]

22
src/freakble/__main__.py Normal file
View file

@ -0,0 +1,22 @@
# Copyright © 2023 Daniele Tricoli <eriol@mornie.org>
# SPDX-License-Identifier: BSD-3-Clause
"""A simple tool to send messages into FreakWAN over Bluetooth low energy."""
import asyncio
import logging
from .cli import get_cli
def run():
"""Main entrypoint.""" # noqa: D401
# ble-serial fire a warning on disconnect, but our main use case is to just
# send a message and disconnect, so we disable logging here.
# TODO: Make configurable by the user.
logging.disable()
asyncio.run(get_cli())
if __name__ == "__main__":
run()

View file

@ -1,6 +1,7 @@
# Copyright © 2023 Daniele Tricoli <eriol@mornie.org>
# SPDX-License-Identifier: BSD-3-Clause
"""A simple tool to send messages into FreakWAN over Bluetooth low energy."""
"""CLI related stuff for freakble."""
import asyncio
import logging
@ -8,21 +9,13 @@ import sys
import warnings
from functools import wraps
import click
import asyncclick as click
from . import __version__
from .ble import BLE_interface, scanner, send_conditionally, send_forever, connect
from .repl import REPL
def coro(f):
@wraps(f)
def wrapper(*args, **kwargs):
return asyncio.run(f(*args, **kwargs))
return wrapper
def ble_receive_callback(data: bytes):
"""Print data received from BLE."""
click.echo(data)
@ -65,7 +58,6 @@ def cli(ctx, adapter):
)
@click.argument("words", type=str, nargs=-1)
@click.pass_context
@coro
async def send(ctx, words, device, loop, sleep_time, ble_connection_timeout):
"""Send one or more words over BLE to a specific device."""
msg = " ".join(words)
@ -98,7 +90,6 @@ async def send(ctx, words, device, loop, sleep_time, ble_connection_timeout):
help="service UUID used to filter",
)
@click.pass_context
@coro
async def scan(ctx, scan_time, service_uuid):
"""Scan to find BLE devices."""
devices = await scanner.scan(ctx.obj["ADAPTER"], scan_time, service_uuid)
@ -117,7 +108,6 @@ async def scan(ctx, scan_time, service_uuid):
"--scan-time", default=5, show_default="5 secs", type=float, help="scan duration"
)
@click.pass_context
@coro
async def deep_scan(ctx, device, scan_time):
"""Scan to find services of a specific device."""
devices = await scanner.scan(ctx.obj["ADAPTER"], scan_time, None)
@ -143,7 +133,6 @@ async def deep_scan(ctx, device, scan_time):
help="BLE connection timeout",
)
@click.pass_context
@coro
async def repl(ctx, device, ble_connection_timeout):
"""Start a REPL with the device."""
ble = ctx.obj["BLE"]
@ -162,17 +151,11 @@ async def repl(ctx, device, ble_connection_timeout):
@cli.command()
@coro
async def version():
"""Return freakble version."""
click.echo(f"freakble {__version__}")
def run():
"""CLI entrypoint."""
# ble-serial fire a warning on disconnect, but our main use case is to just
# send a message and disconnect, so we disable logging here.
# TODO: Make configurable by the user.
logging.disable()
asyncio.run(cli(auto_envvar_prefix="FREAKBLE"))
def get_cli():
"""Return the CLI entrypoint."""
return cli(auto_envvar_prefix="FREAKBLE", _anyio_backend="asyncio")