Skip to content

Commit 7d4a0a1

Browse files
committed
Add missing CI tests (full matrix, sphinx, runtime tests)
1 parent 9d06c4d commit 7d4a0a1

8 files changed

Lines changed: 124 additions & 52 deletions

File tree

.github/workflows/ci.yml

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
name: CI
2+
3+
on:
4+
workflow_dispatch:
5+
push:
6+
branches:
7+
- master
8+
pull_request:
9+
paths:
10+
- ".github/workflows/type-checking.yml"
11+
- "setup.cfg"
12+
- "pyproject.toml"
13+
- "requirements.txt"
14+
- "**/*.py"
15+
16+
jobs:
17+
mypy:
18+
runs-on: ${{ matrix.os }}
19+
strategy:
20+
# mypy is os and python-version sensitive. Test on all supported combinations
21+
matrix:
22+
# Arm runners are faster (as long as the same wheels are available)
23+
os: [windows-11-arm, ubuntu-24.04-arm, macos-latest]
24+
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]
25+
# actions/setup-python doesn't support Python < 3.11 on ARM
26+
exclude:
27+
- python-version: "3.9"
28+
os: windows-11-arm
29+
- python-version: "3.10"
30+
os: windows-11-arm
31+
include:
32+
- python-version: "3.9"
33+
os: windows-latest
34+
- python-version: "3.10"
35+
os: windows-latest
36+
fail-fast: false
37+
steps:
38+
- uses: actions/checkout@v6
39+
- uses: actions/setup-python@v6
40+
with:
41+
python-version: ${{ matrix.python-version }}
42+
cache: "pip"
43+
- run: pip install -r "requirements.txt"
44+
- run: mypy . --python-version=${{ matrix.python-version }}
45+
46+
tests:
47+
runs-on: ${{ matrix.os }}
48+
strategy:
49+
# Test on all supported runtime combinations
50+
matrix:
51+
# Arm runners are faster (as long as the same wheels are available)
52+
# PyWinCtl doesn't have any code that should act differently per architecture
53+
os: [windows-11-arm, ubuntu-24.04-arm, macos-latest]
54+
# TODO: Run tests in parallel on free-threaded python to catch free-threading issues
55+
# See: https://py-free-threading.github.io/testing/
56+
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]
57+
# actions/setup-python doesn't support Python < 3.11 on ARM
58+
exclude:
59+
- python-version: "3.9"
60+
os: windows-11-arm
61+
- python-version: "3.10"
62+
os: windows-11-arm
63+
include:
64+
- python-version: "3.9"
65+
os: windows-latest
66+
- python-version: "3.10"
67+
os: windows-latest
68+
69+
fail-fast: false
70+
steps:
71+
- uses: actions/checkout@v6
72+
- uses: actions/setup-python@v6
73+
with:
74+
python-version: ${{ matrix.python-version }}
75+
cache: "pip"
76+
- run: pip install -r "requirements.txt"
77+
- name: Run tests (Linux)
78+
if: ${{ startsWith(matrix.os, 'ubuntu') }}
79+
working-directory: tests
80+
run: |
81+
sudo apt update
82+
sudo apt install gedit xvfb x11-xserver-utils openbox dbus-x11
83+
xvfb-run --server-args="-screen 0 1280x1024x24" bash -c "openbox & sleep 1 && dbus-launch --exit-with-session python test_pywinctl.py"
84+
- name: Run tests (Windows & macOS)
85+
run: python test_pywinctl.py
86+
if: ${{ !startsWith(matrix.os, 'ubuntu') }}
87+
working-directory: tests
88+
89+
sphinx:
90+
runs-on: ubuntu-24.04-arm # Keep in sync with build.os in .readthedocs.yaml
91+
steps:
92+
- uses: actions/checkout@v6
93+
- uses: actions/setup-python@v6
94+
with:
95+
python-version: "3.11" # Keep in sync with build.tools.python in .readthedocs.yaml
96+
- run: pip install -r "requirements.txt"
97+
- name: Build docs
98+
# TODO: Add --fail-on-warning, but now still too many warnings
99+
run: sphinx-build --keep-going --builder html docs/source docs/_build/html

.github/workflows/type-checking.yml

Lines changed: 0 additions & 29 deletions
This file was deleted.

.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
9+
os: ubuntu-24.04 # Keep in sync with runs-on in .github/workflows/docs.yml
1010
tools:
11-
python: "3.11"
11+
python: "3.11" # Keep in sync with python-version in .github/workflows/docs.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()

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ dependencies = [
6464
[project.optional-dependencies]
6565
dev = [
6666
"ewmhlib>=0.2",
67-
"mypy>=0.990",
67+
"mypy>=0.990,<2",
6868
"myst-parser",
6969
"types-python-xlib>=0.32",
7070
"types-pywin32>=305.0.0.3",
@@ -78,4 +78,4 @@ Homepage = "https://github.com/Kalmat/PyWinCtl"
7878
where = ["src"] # list of folders that contain the packages (["."] by default)
7979

8080
[tool.setuptools.dynamic]
81-
version = {attr = "pywinctl.__version__"} # any module attribute compatible with ast.literal_eval
81+
version = {attr = "pywinctl.__version__"} # any module attribute compatible with ast.literal_eval

setup.cfg

Lines changed: 1 addition & 4 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.8
65
mypy_path = src/, typings/
76
strict = True
87

@@ -16,7 +15,5 @@ disallow_untyped_defs=False
1615
warn_unused_ignores = False
1716

1817
disable_error_code =
19-
# https://github.com/python/mypy/issues/6232 (redifinition with correct type)
18+
# https://github.com/python/mypy/issues/6232 (redefinition with correct type)
2019
attr-defined, assignment,
21-
# https://github.com/python/mypy/issues/13975 (@property mistaken as Callable)
22-
comparison-overlap, truthy-function

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: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -29,27 +29,29 @@ def test_basic():
2929
subprocess.Popen('notepad')
3030
time.sleep(2)
3131

32-
testWindows = [pywinctl.getActiveWindow()]
33-
assert len(testWindows) == 1
34-
35-
npw = testWindows[0]
36-
wait = True
37-
timelap = 0.50
32+
npw = pywinctl.getActiveWindow()
3833

39-
basic_test(npw, wait, timelap)
34+
basic_test(npw, wait=True, timelap=0.50)
4035

4136
elif sys.platform == "linux":
37+
from pywinctl._pywinctl_linux import LinuxWindow
38+
4239
subprocess.Popen('gedit')
43-
time.sleep(2)
4440

45-
testWindows = [pywinctl.getActiveWindow()]
41+
# gedit startup time is unpredictable in headless CI (Xvfb + openbox), and
42+
# getActiveWindow() is unreliable without a real display session. Poll by title
43+
# instead — gedit's title varies ("Untitled Document 1 - gedit"), so use CONTAINS.
44+
testWindows: list[LinuxWindow] = []
45+
deadline = time.time() + 15
46+
while not testWindows and time.time() < deadline:
47+
time.sleep(0.5)
48+
testWindows = pywinctl.getWindowsWithTitle('gedit', condition=pywinctl.Re.CONTAINS)
4649
assert len(testWindows) == 1
4750

4851
npw = testWindows[0]
49-
wait = True
50-
timelap = 0.50
5152

52-
basic_test(npw, wait, timelap)
53+
54+
basic_test(npw, wait=True, timelap=0.50)
5355

5456
elif sys.platform == "darwin":
5557
if not pywinctl.checkPermissions(activate=True):
@@ -66,7 +68,7 @@ def test_basic():
6668
wait = True
6769
timelap = 0.50
6870

69-
basic_test(npw, wait, timelap)
71+
basic_test(npw, wait=True, timelap=0.50)
7072
subprocess.Popen(['rm', 'test.py'])
7173

7274
else:
@@ -249,8 +251,9 @@ def activeCB(isActive):
249251
print("PARENT INFO")
250252
parent = npw.getParent()
251253
if parent:
252-
print("WINDOW PARENT:", parent, npw.isChild(parent))
253-
assert npw.isChild(parent)
254+
is_child = npw.isChild(parent)
255+
print("WINDOW PARENT:", parent, is_child)
256+
assert is_child
254257
children = npw.getChildren()
255258
for child in children:
256259
if child and isinstance(child, int):

0 commit comments

Comments
 (0)