Skip to content

Commit dbee01f

Browse files
committed
Cleanups for some minor potential future issues
I asked GitHub Copilot to review my changes. It didn't see any problems, but I looked at its thought process and cleaned up things that it had to think carefully about. * Fix typo in an older CHANGELOG entry. * Add comments about why each documented public attribute of MSSBase, and a few undocumented but not _-prefixed attributes of MSS implementations, are safe to not be protected under the lock. * Document that the user isn't supposed to change with_cursor. Also document that MSS might ignore it. * Clarify that the comment "The attributes below are protected by self._lock" is meant to contrast with the attributes above that comment. * In MSSBase.monitors, read the return value self._monitors under the lock. Current code doesn't ever change self._monitors once it's created, so it's safe to return outside the lock, but verifying that requires inspection of the rest of the codebase. This makes it clear and future-proof. * Add a test that we can use the same MSS object on multiple threads.
1 parent d67d53d commit dbee01f

File tree

5 files changed

+54
-22
lines changed

5 files changed

+54
-22
lines changed

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ See Git checking messages for full history.
2020
## 10.0.0 (2024-11-14)
2121
- removed support for Python 3.8
2222
- added support for Python 3.14
23-
- Linux: fixed a threadding issue in `.close()` when calling `XCloseDisplay()` (#251)
23+
- Linux: fixed a threading issue in `.close()` when calling `XCloseDisplay()` (#251)
2424
- Linux: minor optimization when checking for a X extension status (#251)
2525
- :heart: contributors: @kianmeng, @shravanasati, @mgorny
2626

src/mss/base.py

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -73,17 +73,29 @@ def __init__(
7373
# Mac only
7474
max_displays: int = 32, # noqa: ARG002
7575
) -> None:
76+
# The cls_image is only used atomically, so does not require locking.
7677
self.cls_image: type[ScreenShot] = ScreenShot
78+
# The compression level is only used atomically, so does not require locking.
7779
#: PNG compression level used when saving the screenshot data into a file
7880
#: (see :py:func:`zlib.compress()` for details).
7981
#:
8082
#: .. versionadded:: 3.2.0
8183
self.compression_level = compression_level
84+
# The with_cursor attribute is not meant to be changed after initialization.
8285
#: Include the mouse cursor in screenshots.
8386
#:
87+
#: In some circumstances, it may not be possible to include the cursor. In that case, MSS will automatically
88+
#: change this to False when the object is created.
89+
#:
90+
#: This should not be changed after creating the object.
91+
#:
8492
#: .. versionadded:: 8.0.0
8593
self.with_cursor = with_cursor
86-
# All attributes below are protected by self._lock.
94+
95+
# The attributes below are protected by self._lock. The attributes above are user-visible, so we don't
96+
# control when they're modified. Currently, we only make sure that they're safe to modify while locked, or
97+
# document that the user shouldn't change them. We could also use properties protect them against changes, or
98+
# change them under the lock.
8799
self._lock = Lock()
88100
self._monitors: Monitors = []
89101
self._closed = False
@@ -209,8 +221,7 @@ def monitors(self) -> Monitors:
209221
with self._lock:
210222
if not self._monitors:
211223
self._monitors_impl()
212-
213-
return self._monitors
224+
return self._monitors
214225

215226
def save(
216227
self,

src/mss/darwin.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ class MSS(MSSBase):
120120
def __init__(self, /, **kwargs: Any) -> None:
121121
super().__init__(**kwargs)
122122

123+
# max_displays is only used by _monitors_impl, while the lock is held.
123124
#: Maximum number of displays to handle.
124125
self.max_displays = kwargs.get("max_displays", 32)
125126

src/mss/windows.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,7 @@ class MSS(MSSBase):
139139
def __init__(self, /, **kwargs: Any) -> None:
140140
super().__init__(**kwargs)
141141

142+
# user32 and gdi32 should not be changed after initialization.
142143
self.user32 = ctypes.WinDLL("user32", use_last_error=True)
143144
self.gdi32 = ctypes.WinDLL("gdi32", use_last_error=True)
144145
self._set_cfunctions()

src/tests/test_implementation.py

Lines changed: 37 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424

2525
if TYPE_CHECKING: # pragma: nocover
2626
from collections.abc import Callable
27+
from typing import Any
2728

2829
from mss.models import Monitor
2930

@@ -299,27 +300,45 @@ def test_grab_with_tuple_percents(mss_impl: Callable[..., MSSBase]) -> None:
299300
assert im.rgb == im2.rgb
300301

301302

302-
def test_thread_safety(backend: str) -> None:
303-
"""Regression test for issue #169."""
303+
class TestThreadSafety:
304+
def run_test(self, do_grab: Callable[[], Any]) -> None:
305+
def record() -> None:
306+
"""Record for one second."""
307+
start_time = time.time()
308+
while time.time() - start_time < 1:
309+
do_grab()
304310

305-
def record(check: dict) -> None:
306-
"""Record for one second."""
307-
start_time = time.time()
308-
while time.time() - start_time < 1:
309-
with mss.mss(backend=backend) as sct:
310-
sct.grab(sct.monitors[1])
311+
checkpoint[threading.current_thread()] = True
312+
313+
checkpoint: dict = {}
314+
t1 = threading.Thread(target=record)
315+
t2 = threading.Thread(target=record)
316+
317+
t1.start()
318+
time.sleep(0.5)
319+
t2.start()
311320

312-
check[threading.current_thread()] = True
321+
t1.join()
322+
t2.join()
313323

314-
checkpoint: dict = {}
315-
t1 = threading.Thread(target=record, args=(checkpoint,))
316-
t2 = threading.Thread(target=record, args=(checkpoint,))
324+
assert len(checkpoint) == 2
325+
326+
def test_issue_169(self, backend: str) -> None:
327+
"""Regression test for issue #169."""
328+
329+
def do_grab() -> None:
330+
with mss.mss(backend=backend) as sct:
331+
sct.grab(sct.monitors[1])
317332

318-
t1.start()
319-
time.sleep(0.5)
320-
t2.start()
333+
self.run_test(do_grab)
321334

322-
t1.join()
323-
t2.join()
335+
def test_same_object_multiple_threads(self, backend: str) -> None:
336+
"""Ensure that the same MSS object can be used by multiple threads.
324337
325-
assert len(checkpoint) == 2
338+
This also implicitly tests that it can be used on a thread
339+
different than the one that created it.
340+
"""
341+
if backend == "xlib":
342+
pytest.skip("The xlib backend does not support this ability")
343+
with mss.mss(backend=backend) as sct:
344+
self.run_test(lambda: sct.grab(sct.monitors[1]))

0 commit comments

Comments
 (0)