From 5eab83e0ef690dec306a3cc1b1cbc26378cfed92 Mon Sep 17 00:00:00 2001 From: Thomas Meckel Date: Fri, 15 May 2026 20:10:26 +0000 Subject: [PATCH 1/5] fix(ci): add python env setup and update pyright Add explicit Python 3.11 configuration, workflow read permissions, and a virtual environment setup step that installs project dependencies before running pre-commit checks. This ensures pyright and other tools can resolve installed packages during PR validation. Also update actions/setup-python to v6 and bump pyright in pre-commit to v1.1.409. --- .github/workflows/pr-checks.yml | 13 ++++++++++++- .pre-commit-config.yaml | 2 +- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pr-checks.yml b/.github/workflows/pr-checks.yml index cab2475..fe0f3aa 100644 --- a/.github/workflows/pr-checks.yml +++ b/.github/workflows/pr-checks.yml @@ -4,6 +4,9 @@ name: Pull Request validation on: - pull_request +permissions: + contents: read + env: IMAGE_NAME: ophiosdev/ocrpdf @@ -17,7 +20,15 @@ jobs: - name: Set up Python id: setup-python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 + with: + python-version: '3.11' + + - name: Create project venv and install dependencies + run: | + "${{ steps.setup-python.outputs.python-path }}" -m venv .venv + "$PWD/.venv/bin/python" -m pip install -r requirements.txt -r requirements-dev.txt + echo "$PWD/.venv/bin" >> "$GITHUB_PATH" - name: Run pre-commit checks id: pre-commit diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3c79c37..8ade387 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -63,6 +63,6 @@ repos: - id: ruff-format - repo: https://github.com/RobertCraigie/pyright-python - rev: v1.1.408 + rev: v1.1.409 hooks: - id: pyright From 7554cfd51988e695ed66a8cdb7b7945b357db350 Mon Sep 17 00:00:00 2001 From: Thomas Meckel Date: Fri, 15 May 2026 20:11:22 +0000 Subject: [PATCH 2/5] fix(smbmonitor): correct task types and cleanup logic Add explicit asyncio.Task type annotations to internal watcher and consumer tasks to eliminate pyright ignore comments. Fix incorrect None check guarding watcher task cancellation during non-graceful stop. Filter None tasks before passing to asyncio.wait. --- packages/smbmonitor/src/smbmonitor.py | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/packages/smbmonitor/src/smbmonitor.py b/packages/smbmonitor/src/smbmonitor.py index c42a899..aa51d78 100644 --- a/packages/smbmonitor/src/smbmonitor.py +++ b/packages/smbmonitor/src/smbmonitor.py @@ -65,7 +65,7 @@ def __init__( raise TypeError(f"Handler {handler} is not callable") # Check if the handler is an async function - if not asyncio.iscoroutinefunction(handler): # pyright: ignore[reportDeprecated] + if not asyncio.iscoroutinefunction(handler): raise TypeError(f"Handler {handler} is not a coroutine function") # Inspect the handler's signature @@ -100,8 +100,8 @@ def _can_bind_args(sig: Signature, arg_count: int) -> bool: self._enforce_encryption = enforce_encryption self._server, self._share, self._watch_path = self._parse_unc_path(unc_path) self._stop_event = asyncio.Event() - self._watcher_task = None - self._consumer_task = None + self._watcher_task: asyncio.Task[None] | None = None + self._consumer_task: asyncio.Task[None] | None = None self._queue = asyncio.Queue() self._port = port self._uuid = uuid4() @@ -271,17 +271,20 @@ async def stop( log.info("Stopping SMB monitoring") if graceful: self._stop_event.set() - _, pending = await asyncio.wait( - (self._watcher_task, self._consumer_task), # pyright: ignore[reportArgumentType, reportCallIssue] - timeout=2.0, - return_when=asyncio.ALL_COMPLETED, - ) - for task in pending: - task.cancel() + tasks = [self._watcher_task, self._consumer_task] + active_tasks = {t for t in tasks if t is not None} + if active_tasks: + _, pending = await asyncio.wait( + active_tasks, + timeout=2.0, + return_when=asyncio.ALL_COMPLETED, + ) + for task in pending: + task.cancel() else: - _ = self._watcher_task.cancel() if self._consumer_task else None + _ = self._watcher_task.cancel() if self._watcher_task else None _ = self._consumer_task.cancel() if self._consumer_task else None def is_running(self) -> bool: """Check if the monitoring tasks are currently running.""" - return self._watcher_task and not self._watcher_task.done() # pyright: ignore[reportReturnType] + return self._watcher_task is not None and not self._watcher_task.done() From 0e161e2b33ea3b66b9446fc00e602d9e5da7a574 Mon Sep 17 00:00:00 2001 From: Thomas Meckel Date: Fri, 15 May 2026 20:13:10 +0000 Subject: [PATCH 3/5] =?UTF-8?q?test(smbmonitor):=20=F0=9F=A7=AA=20add=20te?= =?UTF-8?q?sts=20for=20graceful=20stop=20behavior?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add unit tests covering graceful monitor shutdown scenarios: - waiting for active watcher and consumer tasks to complete - cancelling pending tasks after timeout expires - handling missing consumer task without error --- .../tests/test_smbmonitor_handlers.py | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/packages/smbmonitor/tests/test_smbmonitor_handlers.py b/packages/smbmonitor/tests/test_smbmonitor_handlers.py index 9fed3da..6750cfb 100644 --- a/packages/smbmonitor/tests/test_smbmonitor_handlers.py +++ b/packages/smbmonitor/tests/test_smbmonitor_handlers.py @@ -121,3 +121,68 @@ async def handler(action, server, share, watch_path, filename) -> None: await asyncio.wait_for(consumer_task, timeout=2.0) asyncio.run(_run()) + + +def test_stop_graceful_waits_for_active_tasks() -> None: + async def _run() -> None: + monitor = SmbMonitor(_unc_path(), [handler_full]) + release = asyncio.Event() + + async def _task() -> None: + await release.wait() + + monitor._watcher_task = asyncio.create_task(_task()) + monitor._consumer_task = asyncio.create_task(_task()) + + stop_task = asyncio.create_task(monitor.stop(graceful=True)) + await asyncio.sleep(0) + assert not stop_task.done() + assert monitor._stop_event.is_set() + + release.set() + await asyncio.wait_for(stop_task, timeout=1.0) + assert monitor._watcher_task.done() + assert monitor._consumer_task.done() + + asyncio.run(_run()) + + +def test_stop_graceful_cancels_pending_tasks_after_timeout() -> None: + async def _run() -> None: + monitor = SmbMonitor(_unc_path(), [handler_full]) + started = asyncio.Event() + + async def _never_finishes() -> None: + started.set() + await asyncio.Future() + + monitor._watcher_task = asyncio.create_task(_never_finishes()) + await asyncio.wait_for(started.wait(), timeout=1.0) + + await monitor.stop(graceful=True) + + with pytest.raises(asyncio.CancelledError): + await monitor._watcher_task + + assert monitor._watcher_task.cancelled() + + asyncio.run(_run()) + + +def test_stop_graceful_handles_missing_consumer_task() -> None: + async def _run() -> None: + monitor = SmbMonitor(_unc_path(), [handler_full]) + finished = asyncio.Event() + + async def _task() -> None: + finished.set() + + monitor._watcher_task = asyncio.create_task(_task()) + monitor._consumer_task = None + + await monitor.stop(graceful=True) + + assert monitor._stop_event.is_set() + await asyncio.wait_for(finished.wait(), timeout=1.0) + + asyncio.run(_run()) From db5ce8361c2ab33acc3065200ae53b210381b2d2 Mon Sep 17 00:00:00 2001 From: Thomas Meckel Date: Fri, 15 May 2026 20:19:22 +0000 Subject: [PATCH 4/5] ci: add pytest and pip cache to PR checks --- .github/workflows/pr-checks.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pr-checks.yml b/.github/workflows/pr-checks.yml index fe0f3aa..c659d54 100644 --- a/.github/workflows/pr-checks.yml +++ b/.github/workflows/pr-checks.yml @@ -23,17 +23,21 @@ jobs: uses: actions/setup-python@v6 with: python-version: '3.11' + cache: pip - name: Create project venv and install dependencies run: | "${{ steps.setup-python.outputs.python-path }}" -m venv .venv "$PWD/.venv/bin/python" -m pip install -r requirements.txt -r requirements-dev.txt - echo "$PWD/.venv/bin" >> "$GITHUB_PATH" + echo "$PWD/.venv/bin" >> "$GITHUB_PATH" - name: Run pre-commit checks id: pre-commit uses: cloudposse/github-action-pre-commit@v4.0.0 + - name: Run pytest + run: .venv/bin/pytest + - name: Build Docker image if Dockerfile changed run: | if git diff --name-only origin/${{ github.base_ref }} | grep -q '^Dockerfile$'; then From 7ecf29c01c266ced7c75950fb7cc5622416dcf3c Mon Sep 17 00:00:00 2001 From: Thomas Meckel Date: Fri, 15 May 2026 20:24:33 +0000 Subject: [PATCH 5/5] chore(smbmonitor): add pyright ignore for deprecated --- packages/smbmonitor/src/smbmonitor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/smbmonitor/src/smbmonitor.py b/packages/smbmonitor/src/smbmonitor.py index aa51d78..e754ae4 100644 --- a/packages/smbmonitor/src/smbmonitor.py +++ b/packages/smbmonitor/src/smbmonitor.py @@ -65,7 +65,7 @@ def __init__( raise TypeError(f"Handler {handler} is not callable") # Check if the handler is an async function - if not asyncio.iscoroutinefunction(handler): + if not asyncio.iscoroutinefunction(handler): # pyright: ignore[reportDeprecated] raise TypeError(f"Handler {handler} is not a coroutine function") # Inspect the handler's signature