Skip to content

Commit 3169dfd

Browse files
committed
Add missing CI tests (full matrix, sphinx, runtime tests)
1 parent d463f1e commit 3169dfd

10 files changed

Lines changed: 99 additions & 54 deletions

File tree

.github/workflows/ci.yml

Lines changed: 49 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,22 @@ on:
2121
- "setup.cfg"
2222
- "**/*.py"
2323

24+
concurrency:
25+
# Cancel previous runs for the same PR
26+
# Don't cancel successive pushes to master
27+
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
28+
cancel-in-progress: true
29+
2430
jobs:
2531
mypy:
2632
runs-on: ${{ matrix.os }}
33+
timeout-minutes: &timeout-minutes 5
2734
strategy:
35+
# mypy is os and python-version sensitive. Test on all supported combinations
2836
matrix:
29-
os: [windows-latest, ubuntu-latest, macos-latest]
30-
python-version: ["3.10"]
37+
# Arm runners are faster (as long as the same wheels are available)
38+
os: [windows-11-arm, ubuntu-24.04-arm, macos-latest]
39+
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]
3140
fail-fast: false
3241
steps:
3342
- uses: actions/checkout@v6
@@ -38,15 +47,50 @@ jobs:
3847
- run: uv sync --locked
3948
- run: mypy . --python-version=${{ matrix.python-version }}
4049

50+
tests:
51+
runs-on: ${{ matrix.os }}
52+
timeout-minutes: *timeout-minutes
53+
strategy:
54+
# Test on all supported runtime combinations
55+
matrix:
56+
# Arm runners are faster (as long as the same wheels are available)
57+
# PyWinCtl doesn't have any code that should act differently per architecture
58+
os: [windows-11-arm, ubuntu-24.04-arm, macos-latest]
59+
# TODO: Run tests in parallel on free-threaded python to catch free-threading issues
60+
# See: https://py-free-threading.github.io/testing/
61+
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]
62+
fail-fast: false
63+
steps:
64+
- uses: actions/checkout@v6
65+
- name: Install Linux Packages
66+
if: ${{ startsWith(matrix.os, 'ubuntu') }}
67+
run: |
68+
sudo apt update
69+
sudo apt install gedit xvfb x11-xserver-utils openbox dbus-x11
70+
- uses: astral-sh/setup-uv@v8.2.0
71+
with:
72+
python-version: ${{ matrix.python-version }}
73+
activate-environment: true
74+
- run: uv sync --locked
75+
- name: Run tests (Linux)
76+
if: ${{ startsWith(matrix.os, 'ubuntu') }}
77+
working-directory: tests
78+
run: xvfb-run --server-args="-screen 0 1280x1024x24" bash -c "openbox & sleep 1 && dbus-launch --exit-with-session python test_pywinctl.py"
79+
- name: Run tests (Windows & macOS)
80+
if: ${{ !startsWith(matrix.os, 'ubuntu') }}
81+
working-directory: tests
82+
run: python test_pywinctl.py
83+
4184
sphinx:
42-
runs-on: ubuntu-22.04 # Keep in sync with build.os in .readthedocs.yaml
85+
runs-on: ubuntu-lts-latest # Keep in sync with build.os in .readthedocs.yaml
86+
timeout-minutes: *timeout-minutes
4387
steps:
4488
- uses: actions/checkout@v6
4589
- uses: astral-sh/setup-uv@v8.2.0
4690
with:
47-
python-version: "3.11" # Keep in sync with build.tools.python in .readthedocs.yaml
91+
python-version: "3.14" # Keep in sync with build.tools.python in .readthedocs.yaml
4892
activate-environment: true
4993
- run: uv sync --locked --no-default-groups --group=docs
5094
- name: Build docs
5195
# TODO: Add --fail-on-warning, but still too many warnings right now
52-
run: sphinx-build --keep-going -b html docs/source docs/_build/html
96+
run: sphinx-build --keep-going --builder html docs/source docs/_build/html

.readthedocs.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@ version: 2
66

77
# Set the OS, Python version and other tools you might need
88
build:
9-
os: ubuntu-22.04 # Keep in sync with runs-on in .github/workflows/docs.yml
9+
os: ubuntu-lts-latest # Keep in sync with runs-on in .github/workflows/ci.yml
1010
tools:
11-
python: "3.11" # Keep in sync with python-version in .github/workflows/docs.yml
11+
python: "3.14" # Keep in sync with python-version in .github/workflows/ci.yml
1212
# You can also specify other tool versions:
1313
# nodejs: "20"
1414
# rust: "1.70"

CHANGES.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
0.4.02, 2026/06/16 -- ALL: Fixed WatchDog.stop() not joining the worker thread. Known to have been causing Xlib race conditions on Linux
12
0.4.01, 2024/09/22 -- ALL: Added getAllWindowsDict() general function. Added getPID() method.
23
LINUX: Added bad window filter to check for window.id == 0
34
0.4, 2023/10/11 -- ALL: Added getMonitor() as alias for getDisplay()

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# PyWinCtl <a name="pywinctl"></a>
2-
[![Type Checking](https://github.com/Kalmat/PyWinCtl/actions/workflows/type-checking.yml/badge.svg?branch=dev)](https://github.com/Kalmat/PyWinCtl/actions/workflows/type-checking.yml)
2+
[![CI](https://github.com/Kalmat/PyWinCtl/actions/workflows/ci.yml/badge.svg?branch=dev)](https://github.com/Kalmat/PyWinCtl/actions/workflows/ci.yml)
33
[![PyPI version](https://badge.fury.io/py/PyWinCtl.svg)](https://badge.fury.io/py/PyWinCtl)
44
[![Documentation Status](https://readthedocs.org/projects/pywinctl/badge/?version=latest)](https://pywinctl.readthedocs.io/en/latest/?badge=latest)
55
[![Downloads](https://static.pepy.tech/badge/pywinctl/month)](https://pepy.tech/project/pywinctl)

TODO.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,4 +38,4 @@ macOS / NSWindow:
3838

3939
General:
4040
- [Type_check] PyRect: Create type stubs or add to base library
41-
- [Type_check] Return to "full" type-checking.yml version
41+
- [Type_check] Add back pyright (for PyLance) type checking in ci.yml

pyproject.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,9 @@ classifiers = [
5050
"Programming Language :: Python :: 3.9",
5151
"Programming Language :: Python :: 3.10",
5252
"Programming Language :: Python :: 3.11",
53+
"Programming Language :: Python :: 3.12",
54+
"Programming Language :: Python :: 3.13",
55+
"Programming Language :: Python :: 3.14",
5356
]
5457
dependencies = [
5558
"ewmhlib>=0.2; sys_platform == 'linux'",

setup.cfg

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
description_file = README.md
33

44
[mypy]
5-
python_version = 3.9
65
mypy_path = src/, typings/
76
exclude = build
87
strict = True
@@ -19,5 +18,3 @@ warn_unused_ignores = False
1918
disable_error_code =
2019
# https://github.com/python/mypy/issues/6232 (redefinition with correct type)
2120
attr-defined, assignment,
22-
# https://github.com/python/mypy/issues/13975 (@property mistaken as Callable)
23-
comparison-overlap, truthy-function

setup.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,9 @@
6060
'Programming Language :: Python :: 3 :: Only',
6161
'Programming Language :: Python :: 3.9',
6262
'Programming Language :: Python :: 3.10',
63-
'Programming Language :: Python :: 3.11'
63+
'Programming Language :: Python :: 3.11',
64+
"Programming Language :: Python :: 3.12",
65+
"Programming Language :: Python :: 3.13",
66+
"Programming Language :: Python :: 3.14",
6467
],
6568
)

src/pywinctl/_main.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -582,6 +582,7 @@ def stop(self):
582582
"""
583583
if self._watchdog:
584584
self._watchdog.kill()
585+
self._watchdog.join()
585586

586587
def isAlive(self):
587588
"""Check if watchdog is running

tests/test_pywinctl.py

Lines changed: 37 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,16 @@
55
import subprocess
66
import sys
77
import time
8+
from typing import TypedDict
89

910
import pywinctl
1011

1112

13+
class GetWindowKwargs(TypedDict):
14+
title: str
15+
condition: int # TODO: Consider making pywinctl.Re an IntEnum
16+
17+
1218
def test_basic():
1319

1420
print("PLATFORM:", sys.platform)
@@ -26,52 +32,41 @@ def test_basic():
2632
print()
2733

2834
if sys.platform == "win32":
29-
subprocess.Popen('notepad')
30-
time.sleep(2)
31-
32-
testWindows = [pywinctl.getActiveWindow()]
33-
assert len(testWindows) == 1
34-
35-
npw = testWindows[0]
36-
wait = True
37-
timelap = 0.50
38-
39-
basic_test(npw, wait, timelap)
40-
35+
process = "notepad"
36+
get_window_kwargs: GetWindowKwargs = {
37+
"title": "Notepad",
38+
"condition": pywinctl.Re.ENDSWITH,
39+
}
4140
elif sys.platform == "linux":
42-
subprocess.Popen('gedit')
43-
time.sleep(2)
44-
45-
testWindows = [pywinctl.getActiveWindow()]
46-
assert len(testWindows) == 1
47-
48-
npw = testWindows[0]
49-
wait = True
50-
timelap = 0.50
51-
52-
basic_test(npw, wait, timelap)
53-
41+
process = "gedit"
42+
get_window_kwargs: GetWindowKwargs = {
43+
"title": "gedit",
44+
"condition": pywinctl.Re.ENDSWITH,
45+
}
5446
elif sys.platform == "darwin":
5547
if not pywinctl.checkPermissions(activate=True):
5648
exit()
57-
subprocess.Popen(['touch', 'test.py'])
58-
time.sleep(2)
59-
subprocess.Popen(['open', '-a', 'TextEdit', 'test.py'])
60-
time.sleep(5)
61-
62-
testWindows = pywinctl.getWindowsWithTitle('test.py')
63-
assert len(testWindows) == 1
64-
65-
npw = testWindows[0]
66-
wait = True
67-
timelap = 0.50
49+
process = ["open", "-a", "TextEdit", __file__]
50+
get_window_kwargs: GetWindowKwargs = {
51+
"title": "test_pywinctl.py",
52+
"condition": pywinctl.Re.IS,
53+
}
54+
else:
55+
raise NotImplementedError(
56+
"PyWinCtl currently does not support this platform. "
57+
+ "If you have useful knowledge, please contribute! https://github.com/Kalmat/PyWinCtl"
58+
)
6859

69-
basic_test(npw, wait, timelap)
70-
subprocess.Popen(['rm', 'test.py'])
60+
subprocess.Popen(process)
7161

72-
else:
73-
raise NotImplementedError('PyWinCtl currently does not support this platform. If you have useful knowledge, please contribute! https://github.com/Kalmat/pywinctl')
62+
testWindows: list[pywinctl.Window] = []
63+
deadline = time.time() + 15
64+
while not testWindows and time.time() < deadline:
65+
time.sleep(0.5)
66+
testWindows = pywinctl.getWindowsWithTitle(**get_window_kwargs)
67+
assert len(testWindows) == 1
7468

69+
basic_test(npw=testWindows[0], wait=True, timelap=0.50)
7570

7671
def basic_test(npw: pywinctl.Window | None, wait: bool, timelap: float):
7772
assert npw is not None
@@ -249,8 +244,9 @@ def activeCB(isActive):
249244
print("PARENT INFO")
250245
parent = npw.getParent()
251246
if parent:
252-
print("WINDOW PARENT:", parent, npw.isChild(parent))
253-
assert npw.isChild(parent)
247+
is_child = npw.isChild(parent)
248+
print("WINDOW PARENT:", parent, is_child)
249+
assert is_child
254250
children = npw.getChildren()
255251
for child in children:
256252
if child and isinstance(child, int):

0 commit comments

Comments
 (0)