From 3fc2088e782078289d2f0e0c7f40d15b0d2b4544 Mon Sep 17 00:00:00 2001 From: Darafei Praliaskouski Date: Thu, 15 Oct 2026 02:09:49 +0400 Subject: [PATCH 1/3] Queue asynchronous ADB commands behind busy operations --- androidtv/adb_manager/adb_manager_async.py | 10 +++---- tests/test_adb_manager_async.py | 35 ++++++++++++++++++++++ 2 files changed, 40 insertions(+), 5 deletions(-) diff --git a/androidtv/adb_manager/adb_manager_async.py b/androidtv/adb_manager/adb_manager_async.py index e5325291..2ea3fec1 100644 --- a/androidtv/adb_manager/adb_manager_async.py +++ b/androidtv/adb_manager/adb_manager_async.py @@ -20,7 +20,6 @@ from ..constants import ( DEFAULT_ADB_TIMEOUT_S, DEFAULT_AUTH_TIMEOUT_S, - DEFAULT_LOCK_TIMEOUT_S, DEFAULT_TRANSPORT_TIMEOUT_S, ) from ..exceptions import LockNotAcquiredException @@ -140,15 +139,16 @@ async def device(self, serial): @asynccontextmanager -async def _acquire(lock, timeout=DEFAULT_LOCK_TIMEOUT_S): - """Handle acquisition and release of an ``asyncio.Lock`` object with a timeout. +async def _acquire(lock, timeout=None): + """Queue access to an ``asyncio.Lock``, with an optional acquisition timeout. Parameters ---------- lock : asyncio.Lock The lock that we will try to acquire - timeout : float - The timeout in seconds + timeout : float, None + Optional acquisition timeout in seconds. Commands wait in order by default; + the ADB transport retains its own timeout. Yields ------ diff --git a/tests/test_adb_manager_async.py b/tests/test_adb_manager_async.py index d17c45e7..7c90caca 100644 --- a/tests/test_adb_manager_async.py +++ b/tests/test_adb_manager_async.py @@ -195,6 +195,41 @@ async def test_adb_shell_lock_not_acquired_not_released(self): release.assert_not_called() + @awaiter + async def test_adb_shell_waits_for_busy_connection(self): + """A valid command must survive a screenshot holding the lock over 3 s.""" + with async_patchers.patch_connect(True)[self.PATCH_KEY], async_patchers.patch_shell("TEST")[self.PATCH_KEY]: + self.assertTrue(await self.adb.connect()) + lock = asyncio.Lock() + await lock.acquire() + with patch.object(self.adb, "_adb_lock", lock): + command = asyncio.create_task(self.adb.shell("TEST")) + try: + await asyncio.sleep(3.05) + self.assertFalse(command.done()) + finally: + lock.release() + result = await command + self.assertEqual(result, "TEST") + self.assertFalse(lock.locked()) + + @awaiter + async def test_cancelled_waiter_does_not_release_busy_connection(self): + """Cancelling a queued command must not unlock another command.""" + with async_patchers.patch_connect(True)[self.PATCH_KEY], async_patchers.patch_shell("TEST")[self.PATCH_KEY]: + self.assertTrue(await self.adb.connect()) + lock = asyncio.Lock() + await lock.acquire() + with patch.object(self.adb, "_adb_lock", lock): + command = asyncio.create_task(self.adb.shell("TEST")) + await asyncio.sleep(0) + command.cancel() + with self.assertRaises(asyncio.CancelledError): + await command + self.assertTrue(lock.locked()) + lock.release() + self.assertEqual(await self.adb.shell("TEST"), "TEST") + @awaiter async def test_adb_push_fail(self): """Test when an ADB push command is not executed because the device is unavailable.""" From 41a863802c88549bda63fb09f796a53e845f9e30 Mon Sep 17 00:00:00 2001 From: Komzpa Date: Thu, 15 Oct 2026 10:26:59 +0400 Subject: [PATCH 2/3] CI: run Python 3.7 job on ubuntu-22.04 (setup-python lacks 3.7 on ubuntu-latest) --- .github/workflows/python-package.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 5989973c..58bb40d2 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -15,7 +15,7 @@ env: jobs: build: - runs-on: ubuntu-latest + runs-on: ${{ matrix.python-version == '3.7' && 'ubuntu-22.04' || 'ubuntu-latest' }} strategy: matrix: python-version: ['3.7', '3.8', '3.9', '3.10', '3.11', '3.12'] From 3c44a54542dd9c2ec48f9a82e4fd1020f94c3e16 Mon Sep 17 00:00:00 2001 From: Darafei Praliaskouski Date: Sun, 18 Oct 2026 03:07:46 +0400 Subject: [PATCH 3/3] Pin black to 23.3.0 so the unpinned formatter stops drifting under the lint matrix The requirement was a bare `black`, so each CI Python resolved a different release (23.3.0 on 3.7 ... 26.10.1 on 3.10-3.12) and the newest normalizes androidtv/exceptions.py's module docstring value, which the repo's own formatting cannot absorb without a literal change. 23.3.0 is the newest release that still supports the maintained Python 3.7 leg and it leaves package, tests, and setup.py clean as authored. --- venv_requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/venv_requirements.txt b/venv_requirements.txt index 8f953308..a2c26b9d 100644 --- a/venv_requirements.txt +++ b/venv_requirements.txt @@ -1,5 +1,5 @@ # Standard requirements -black +black==23.3.0 coverage coveralls flake8