From 20a24c5c805dde4257181d5479373412a73b4b68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Tue, 27 Feb 2024 16:09:26 +0100 Subject: [PATCH] feat: use ruff (#275) --- check.sh | 11 +- docs/source/conf.py | 46 +------ docs/source/examples/callback.py | 10 +- docs/source/examples/custom_cls_image.py | 8 +- docs/source/examples/fps.py | 12 +- docs/source/examples/fps_multiprocessing.py | 5 +- docs/source/examples/from_pil_tuple.py | 5 +- docs/source/examples/linux_display_keyword.py | 5 +- docs/source/examples/opencv_numpy.py | 10 +- docs/source/examples/part_of_screen.py | 5 +- .../examples/part_of_screen_monitor_2.py | 5 +- docs/source/examples/pil.py | 8 +- docs/source/examples/pil_pixels.py | 8 +- pyproject.toml | 72 ++++++----- src/mss/__init__.py | 9 +- src/mss/__main__.py | 14 +- src/mss/base.py | 99 +++++++-------- src/mss/darwin.py | 52 ++++---- src/mss/exception.py | 11 +- src/mss/factory.py | 19 ++- src/mss/linux.py | 120 +++++++++--------- src/mss/models.py | 21 +-- src/mss/screenshot.py | 50 ++++---- src/mss/tools.py | 13 +- src/mss/windows.py | 58 ++++----- src/tests/bench_bgra2rgb.py | 31 +++-- src/tests/bench_general.py | 24 ++-- src/tests/conftest.py | 24 ++-- src/tests/test_bgra_to_rgb.py | 14 +- src/tests/test_cls_image.py | 13 +- src/tests/test_find_monitors.py | 13 +- src/tests/test_get_pixels.py | 12 +- src/tests/test_gnu_linux.py | 95 +++++++------- src/tests/test_implementation.py | 84 ++++++------ src/tests/test_issue_220.py | 18 ++- src/tests/test_leaks.py | 72 +++++------ src/tests/test_macos.py | 56 ++++---- src/tests/test_save.py | 34 +++-- src/tests/test_setup.py | 14 +- src/tests/test_third_party.py | 22 ++-- src/tests/test_tools.py | 25 ++-- src/tests/test_windows.py | 43 ++++--- 42 files changed, 599 insertions(+), 671 deletions(-) diff --git a/check.sh b/check.sh index 0e48e93..7bb90ae 100755 --- a/check.sh +++ b/check.sh @@ -2,9 +2,10 @@ # # Small script to ensure quality checks pass before submitting a commit/PR. # -python -m isort docs src -python -m black --line-length=120 docs src -python -m flake8 docs src -python -m pylint src/mss +set -eu + +python -m ruff --fix docs src +python -m ruff format docs src + # "--platform win32" to not fail on ctypes.windll (it does not affect the overall check on other OSes) -python -m mypy --platform win32 --exclude src/tests src docs/source/examples +python -m mypy --platform win32 src docs/source/examples diff --git a/docs/source/conf.py b/docs/source/conf.py index aab04ec..20ae634 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -6,67 +6,29 @@ sys.path.insert(0, str(Path(__file__).parent.parent.parent / "src")) -from mss import __author__, __date__, __version__ # noqa +import mss # -- General configuration ------------------------------------------------ -# Add any Sphinx extension module names here, as strings. They can be -# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom -# ones. extensions = ["sphinx.ext.intersphinx"] - -# Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] - -# The suffix(es) of source filenames. -# You can specify multiple suffix as a list of string: -# -# source_suffix = ['.rst', '.md'] source_suffix = ".rst" - -# The master toctree document. master_doc = "index" # General information about the project. project = "Python MSS" -copyright = f"{__date__}, {__author__} & contributors" -author = "Tiger-222" +copyright = f"{mss.__date__}, {mss.__author__} & contributors" # noqa:A001 +author = mss.__author__ +version = mss.__version__ -# The version info for the project you're documenting, acts as replacement for -# |version| and |release|, also used in various other places throughout the -# built documents. -# -# The short X.Y version. -version = __version__ - -# The full version, including alpha/beta/rc tags. release = "latest" - -# The language for content autogenerated by Sphinx. Refer to documentation -# for a list of supported languages. -# -# This is also used if you do content translation via gettext catalogs. -# Usually you set "language" from the command line for these cases. language = "en" - -# List of patterns, relative to source directory, that match files and -# directories to ignore when looking for source files. -# This patterns also effect to html_static_path and html_extra_path -exclude_patterns = [] - -# The name of the Pygments (syntax highlighting) style to use. -pygments_style = "sphinx" - todo_include_todos = True # -- Options for HTML output ---------------------------------------------- -# The theme to use for HTML and HTML Help pages. See the documentation for -# a list of builtin themes. html_theme = "default" - -# Output file base name for HTML help builder. htmlhelp_basename = "PythonMSSdoc" diff --git a/docs/source/examples/callback.py b/docs/source/examples/callback.py index 147c952..a107176 100644 --- a/docs/source/examples/callback.py +++ b/docs/source/examples/callback.py @@ -1,6 +1,5 @@ -""" -This is part of the MSS Python's module. -Source: https://github.com/BoboTiG/python-mss +"""This is part of the MSS Python's module. +Source: https://github.com/BoboTiG/python-mss. Screenshot of the monitor 1, with callback. """ @@ -11,10 +10,7 @@ def on_exists(fname: str) -> None: - """ - Callback example when we try to overwrite an existing screenshot. - """ - + """Callback example when we try to overwrite an existing screenshot.""" if os.path.isfile(fname): newfile = f"{fname}.old" print(f"{fname} -> {newfile}") diff --git a/docs/source/examples/custom_cls_image.py b/docs/source/examples/custom_cls_image.py index 4232e49..c57e111 100644 --- a/docs/source/examples/custom_cls_image.py +++ b/docs/source/examples/custom_cls_image.py @@ -1,6 +1,5 @@ -""" -This is part of the MSS Python's module. -Source: https://github.com/BoboTiG/python-mss +"""This is part of the MSS Python's module. +Source: https://github.com/BoboTiG/python-mss. Screenshot of the monitor 1, using a custom class to handle the data. """ @@ -12,8 +11,7 @@ class SimpleScreenShot(ScreenShot): - """ - Define your own custom method to deal with screen shot raw data. + """Define your own custom method to deal with screen shot raw data. Of course, you can inherit from the ScreenShot class and change or add new methods. """ diff --git a/docs/source/examples/fps.py b/docs/source/examples/fps.py index 4046f2a..7a33843 100644 --- a/docs/source/examples/fps.py +++ b/docs/source/examples/fps.py @@ -1,6 +1,5 @@ -""" -This is part of the MSS Python's module. -Source: https://github.com/BoboTiG/python-mss +"""This is part of the MSS Python's module. +Source: https://github.com/BoboTiG/python-mss. Simple naive benchmark to compare with: https://pythonprogramming.net/game-frames-open-cv-python-plays-gta-v/ @@ -8,9 +7,8 @@ import time import cv2 -import numpy - import mss +import numpy as np def screen_record() -> int: @@ -27,7 +25,7 @@ def screen_record() -> int: last_time = time.time() while time.time() - last_time < 1: - img = numpy.asarray(ImageGrab.grab(bbox=mon)) + img = np.asarray(ImageGrab.grab(bbox=mon)) fps += 1 cv2.imshow(title, cv2.cvtColor(img, cv2.COLOR_BGR2RGB)) @@ -48,7 +46,7 @@ def screen_record_efficient() -> int: last_time = time.time() while time.time() - last_time < 1: - img = numpy.asarray(sct.grab(mon)) + img = np.asarray(sct.grab(mon)) fps += 1 cv2.imshow(title, img) diff --git a/docs/source/examples/fps_multiprocessing.py b/docs/source/examples/fps_multiprocessing.py index 28caf59..a54ac3e 100644 --- a/docs/source/examples/fps_multiprocessing.py +++ b/docs/source/examples/fps_multiprocessing.py @@ -1,6 +1,5 @@ -""" -This is part of the MSS Python's module. -Source: https://github.com/BoboTiG/python-mss +"""This is part of the MSS Python's module. +Source: https://github.com/BoboTiG/python-mss. Example using the multiprocessing module to speed-up screen capture. https://github.com/pythonlessons/TensorFlow-object-detection-tutorial diff --git a/docs/source/examples/from_pil_tuple.py b/docs/source/examples/from_pil_tuple.py index 61f2d94..c5ed5f4 100644 --- a/docs/source/examples/from_pil_tuple.py +++ b/docs/source/examples/from_pil_tuple.py @@ -1,6 +1,5 @@ -""" -This is part of the MSS Python's module. -Source: https://github.com/BoboTiG/python-mss +"""This is part of the MSS Python's module. +Source: https://github.com/BoboTiG/python-mss. Use PIL bbox style and percent values. """ diff --git a/docs/source/examples/linux_display_keyword.py b/docs/source/examples/linux_display_keyword.py index a0b7b40..2070aea 100644 --- a/docs/source/examples/linux_display_keyword.py +++ b/docs/source/examples/linux_display_keyword.py @@ -1,6 +1,5 @@ -""" -This is part of the MSS Python's module. -Source: https://github.com/BoboTiG/python-mss +"""This is part of the MSS Python's module. +Source: https://github.com/BoboTiG/python-mss. Usage example with a specific display. """ diff --git a/docs/source/examples/opencv_numpy.py b/docs/source/examples/opencv_numpy.py index 81130ad..94bdbc3 100644 --- a/docs/source/examples/opencv_numpy.py +++ b/docs/source/examples/opencv_numpy.py @@ -1,15 +1,13 @@ -""" -This is part of the MSS Python's module. -Source: https://github.com/BoboTiG/python-mss +"""This is part of the MSS Python's module. +Source: https://github.com/BoboTiG/python-mss. OpenCV/Numpy example. """ import time import cv2 -import numpy - import mss +import numpy as np with mss.mss() as sct: # Part of the screen to capture @@ -19,7 +17,7 @@ last_time = time.time() # Get raw pixels from the screen, save it to a Numpy array - img = numpy.array(sct.grab(monitor)) + img = np.array(sct.grab(monitor)) # Display the picture cv2.imshow("OpenCV/Numpy normal", img) diff --git a/docs/source/examples/part_of_screen.py b/docs/source/examples/part_of_screen.py index 73f93cb..bcc17bb 100644 --- a/docs/source/examples/part_of_screen.py +++ b/docs/source/examples/part_of_screen.py @@ -1,6 +1,5 @@ -""" -This is part of the MSS Python's module. -Source: https://github.com/BoboTiG/python-mss +"""This is part of the MSS Python's module. +Source: https://github.com/BoboTiG/python-mss. Example to capture part of the screen. """ diff --git a/docs/source/examples/part_of_screen_monitor_2.py b/docs/source/examples/part_of_screen_monitor_2.py index 61f58f7..56bfbdc 100644 --- a/docs/source/examples/part_of_screen_monitor_2.py +++ b/docs/source/examples/part_of_screen_monitor_2.py @@ -1,6 +1,5 @@ -""" -This is part of the MSS Python's module. -Source: https://github.com/BoboTiG/python-mss +"""This is part of the MSS Python's module. +Source: https://github.com/BoboTiG/python-mss. Example to capture part of the screen of the monitor 2. """ diff --git a/docs/source/examples/pil.py b/docs/source/examples/pil.py index db10f1b..01a6b01 100644 --- a/docs/source/examples/pil.py +++ b/docs/source/examples/pil.py @@ -1,12 +1,10 @@ -""" -This is part of the MSS Python's module. -Source: https://github.com/BoboTiG/python-mss +"""This is part of the MSS Python's module. +Source: https://github.com/BoboTiG/python-mss. PIL example using frombytes(). """ -from PIL import Image - import mss +from PIL import Image with mss.mss() as sct: # Get rid of the first, as it represents the "All in One" monitor: diff --git a/docs/source/examples/pil_pixels.py b/docs/source/examples/pil_pixels.py index fcedcec..54c5722 100644 --- a/docs/source/examples/pil_pixels.py +++ b/docs/source/examples/pil_pixels.py @@ -1,12 +1,10 @@ -""" -This is part of the MSS Python's module. -Source: https://github.com/BoboTiG/python-mss +"""This is part of the MSS Python's module. +Source: https://github.com/BoboTiG/python-mss. PIL examples to play with pixels. """ -from PIL import Image - import mss +from PIL import Image with mss.mss() as sct: # Get a screenshot of the 1st monitor diff --git a/pyproject.toml b/pyproject.toml index f9b41b2..5a0d125 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -79,11 +79,9 @@ test = [ "sphinx", ] dev = [ - "black", "build", - "flake8-pyproject", "mypy", - "pylint", + "ruff", "twine", "wheel", ] @@ -108,26 +106,6 @@ packages = [ "src/mss", ] -[tool.black] -target-version = ["py38"] -line-length = 120 -safe = true - -[tool.flake8] -max-line-length = 120 -ignore = [ - "E203", # Whitespace before ':', but it's not PEP 8 compliant - "W503", # Line break before binary operator, but it's not PEP 8 compliant -] - -[tool.isort] -py_version = 38 -line_length = 120 -multi_line_output = 3 -include_trailing_comma = true -force_grid_wrap = 0 -use_parentheses = true - [tool.mypy] # Ensure we know what we do warn_redundant_casts = true @@ -149,14 +127,6 @@ disallow_untyped_calls = true strict_equality = true -[tool.pylint."MESSAGES CONTROL"] -disable = "locally-disabled,too-few-public-methods,too-many-instance-attributes,duplicate-code" - -[tool.pylint.REPORTS] -max-line-length = 120 -output-format = "colorized" -reports = "no" - [tool.pytest.ini_options] pythonpath = "src" addopts = """ @@ -167,3 +137,43 @@ addopts = """ --cov=src/mss --cov-report=term-missing """ + +[tool.ruff] +exclude = [ + ".git", + ".mypy_cache", + ".pytest_cache", + ".ruff_cache", + "venv", +] +line-length = 120 +indent-width = 4 +target-version = "py38" + +[tool.ruff.lint] +extend-select = ["ALL"] +ignore = [ + "ANN101", + "ANN401", + "C90", + "COM812", + "D", # TODO + "ERA", + "FBT", + "INP001", + "ISC001", + "PTH", + "PL", + "S", + "SIM117", # TODO: remove wen dropping Python 3.8 support + "SLF", + "T201", + "UP006", # TODO: remove wen dropping Python 3.8 support +] +fixable = ["ALL"] + +[tool.ruff.format] +quote-style = "double" +indent-style = "space" +skip-magic-trailing-comma = false +line-ending = "auto" diff --git a/src/mss/__init__.py b/src/mss/__init__.py index 500983c..cb490e2 100644 --- a/src/mss/__init__.py +++ b/src/mss/__init__.py @@ -1,5 +1,4 @@ -""" -An ultra fast cross-platform multiple screenshots module in pure python +"""An ultra fast cross-platform multiple screenshots module in pure python using ctypes. This module is maintained by Mickaël Schoentgen . @@ -8,11 +7,11 @@ https://github.com/BoboTiG/python-mss If that URL should fail, try contacting the author. """ -from .exception import ScreenShotError -from .factory import mss +from mss.exception import ScreenShotError +from mss.factory import mss __version__ = "9.0.2" -__author__ = "Mickaël 'Tiger-222' Schoentgen" +__author__ = "Mickaël Schoentgen" __date__ = "2013-2024" __copyright__ = f""" Copyright (c) {__date__}, {__author__} diff --git a/src/mss/__main__.py b/src/mss/__main__.py index 4dff5a1..9b74506 100644 --- a/src/mss/__main__.py +++ b/src/mss/__main__.py @@ -1,20 +1,18 @@ -""" -This is part of the MSS Python's module. -Source: https://github.com/BoboTiG/python-mss +"""This is part of the MSS Python's module. +Source: https://github.com/BoboTiG/python-mss. """ import os.path import sys from argparse import ArgumentParser -from . import __version__ -from .exception import ScreenShotError -from .factory import mss -from .tools import to_png +from mss import __version__ +from mss.exception import ScreenShotError +from mss.factory import mss +from mss.tools import to_png def main(*args: str) -> int: """Main logic.""" - cli_args = ArgumentParser(prog="mss") cli_args.add_argument( "-c", diff --git a/src/mss/base.py b/src/mss/base.py index 14a4528..4495bf1 100644 --- a/src/mss/base.py +++ b/src/mss/base.py @@ -1,16 +1,29 @@ +"""This is part of the MSS Python's module. +Source: https://github.com/BoboTiG/python-mss. """ -This is part of the MSS Python's module. -Source: https://github.com/BoboTiG/python-mss -""" +from __future__ import annotations + from abc import ABCMeta, abstractmethod from datetime import datetime from threading import Lock -from typing import Any, Callable, Iterator, List, Optional, Tuple, Type, Union +from typing import TYPE_CHECKING, Any, List, Tuple + +from mss.exception import ScreenShotError +from mss.screenshot import ScreenShot +from mss.tools import to_png + +if TYPE_CHECKING: + from collections.abc import Callable, Iterator + + from mss.models import Monitor, Monitors -from .exception import ScreenShotError -from .models import Monitor, Monitors -from .screenshot import ScreenShot -from .tools import to_png +try: + from datetime import UTC +except ImportError: + # Python < 3.11 + from datetime import timezone + + UTC = timezone.utc lock = Lock() @@ -25,50 +38,44 @@ def __init__( /, *, compression_level: int = 6, - display: Optional[Union[bytes, str]] = None, # Linux only - max_displays: int = 32, # Mac only + display: bytes | str | None = None, # noqa:ARG002 Linux only + max_displays: int = 32, # noqa:ARG002 Mac only with_cursor: bool = False, ) -> None: - # pylint: disable=unused-argument - self.cls_image: Type[ScreenShot] = ScreenShot + self.cls_image: type[ScreenShot] = ScreenShot self.compression_level = compression_level self.with_cursor = with_cursor self._monitors: Monitors = [] - def __enter__(self) -> "MSSBase": + def __enter__(self) -> MSSBase: # noqa:PYI034 """For the cool call `with MSS() as mss:`.""" - return self - def __exit__(self, *_: Any) -> None: + def __exit__(self, *_: object) -> None: """For the cool call `with MSS() as mss:`.""" - self.close() @abstractmethod - def _cursor_impl(self) -> Optional[ScreenShot]: + def _cursor_impl(self) -> ScreenShot | None: """Retrieve all cursor data. Pixels have to be RGB.""" @abstractmethod def _grab_impl(self, monitor: Monitor, /) -> ScreenShot: - """ - Retrieve all pixels from a monitor. Pixels have to be RGB. + """Retrieve all pixels from a monitor. Pixels have to be RGB. That method has to be run using a threading lock. """ @abstractmethod def _monitors_impl(self) -> None: - """ - Get positions of monitors (has to be run using a threading lock). + """Get positions of monitors (has to be run using a threading lock). It must populate self._monitors. """ - def close(self) -> None: + def close(self) -> None: # noqa:B027 """Clean-up.""" - def grab(self, monitor: Union[Monitor, Tuple[int, int, int, int]], /) -> ScreenShot: - """ - Retrieve screen pixels for a given monitor. + def grab(self, monitor: Monitor | Tuple[int, int, int, int], /) -> ScreenShot: + """Retrieve screen pixels for a given monitor. Note: *monitor* can be a tuple like the one PIL.Image.grab() accepts. @@ -76,7 +83,6 @@ def grab(self, monitor: Union[Monitor, Tuple[int, int, int, int]], /) -> ScreenS See :meth:`monitors ` for object details. :return :class:`ScreenShot `. """ - # Convert PIL bbox style if isinstance(monitor, tuple): monitor = { @@ -94,8 +100,7 @@ def grab(self, monitor: Union[Monitor, Tuple[int, int, int, int]], /) -> ScreenS @property def monitors(self) -> Monitors: - """ - Get positions of all monitors. + """Get positions of all monitors. If the monitor has rotation, you have to deal with it inside this method. @@ -112,7 +117,6 @@ def monitors(self) -> Monitors: 'height': the height } """ - if not self._monitors: with lock: self._monitors_impl() @@ -125,10 +129,9 @@ def save( *, mon: int = 0, output: str = "monitor-{mon}.png", - callback: Optional[Callable[[str], None]] = None, + callback: Callable[[str], None] | None = None, ) -> Iterator[str]: - """ - Grab a screen shot and save it to a file. + """Grab a screen shot and save it to a file. :param int mon: The monitor to screen shot (default=0). -1: grab one screen shot of all monitors @@ -153,15 +156,15 @@ def save( :return generator: Created file(s). """ - monitors = self.monitors if not monitors: - raise ScreenShotError("No monitor found.") + msg = "No monitor found." + raise ScreenShotError(msg) if mon == 0: # One screen shot by monitor for idx, monitor in enumerate(monitors[1:], 1): - fname = output.format(mon=idx, date=datetime.now(), **monitor) + fname = output.format(mon=idx, date=datetime.now(UTC) if "{date" in output else None, **monitor) if callable(callback): callback(fname) sct = self.grab(monitor) @@ -174,9 +177,10 @@ def save( try: monitor = monitors[mon] except IndexError as exc: - raise ScreenShotError(f"Monitor {mon!r} does not exist.") from exc + msg = f"Monitor {mon!r} does not exist." + raise ScreenShotError(msg) from exc - output = output.format(mon=mon, date=datetime.now(), **monitor) + output = output.format(mon=mon, date=datetime.now(UTC) if "{date" in output else None, **monitor) if callable(callback): callback(output) sct = self.grab(monitor) @@ -184,11 +188,9 @@ def save( yield output def shot(self, /, **kwargs: Any) -> str: - """ - Helper to save the screen shot of the 1st monitor, by default. + """Helper to save the screen shot of the 1st monitor, by default. You can pass the same arguments as for ``save``. """ - kwargs["mon"] = kwargs.get("mon", 1) return next(self.save(**kwargs)) @@ -196,8 +198,6 @@ def shot(self, /, **kwargs: Any) -> str: def _merge(screenshot: ScreenShot, cursor: ScreenShot, /) -> ScreenShot: """Create composite image by blending screenshot and mouse cursor.""" - # pylint: disable=too-many-locals,invalid-name - (cx, cy), (cw, ch) = cursor.pos, cursor.size (x, y), (w, h) = screenshot.pos, screenshot.size @@ -208,8 +208,8 @@ def _merge(screenshot: ScreenShot, cursor: ScreenShot, /) -> ScreenShot: if not overlap: return screenshot - screen_data = screenshot.raw - cursor_data = cursor.raw + screen_raw = screenshot.raw + cursor_raw = cursor.raw cy, cy2 = (cy - y) * 4, (cy2 - y2) * 4 cx, cx2 = (cx - x) * 4, (cx2 - x2) * 4 @@ -226,17 +226,17 @@ def _merge(screenshot: ScreenShot, cursor: ScreenShot, /) -> ScreenShot: for count_x in range(start_count_x, stop_count_x, 4): spos = pos_s + count_x cpos = pos_c + count_x - alpha = cursor_data[cpos + 3] + alpha = cursor_raw[cpos + 3] if not alpha: continue if alpha == 255: - screen_data[spos : spos + 3] = cursor_data[cpos : cpos + 3] + screen_raw[spos : spos + 3] = cursor_raw[cpos : cpos + 3] else: - alpha = alpha / 255 + alpha2 = alpha / 255 for i in rgb: - screen_data[spos + i] = int(cursor_data[cpos + i] * alpha + screen_data[spos + i] * (1 - alpha)) + screen_raw[spos + i] = int(cursor_raw[cpos + i] * alpha2 + screen_raw[spos + i] * (1 - alpha2)) return screenshot @@ -247,10 +247,9 @@ def _cfactory( argtypes: List[Any], restype: Any, /, - errcheck: Optional[Callable] = None, + errcheck: Callable | None = None, ) -> None: """Factory to create a ctypes function and automatically manage errors.""" - meth = getattr(attr, func) meth.argtypes = argtypes meth.restype = restype diff --git a/src/mss/darwin.py b/src/mss/darwin.py index 1dd37ea..f247c51 100644 --- a/src/mss/darwin.py +++ b/src/mss/darwin.py @@ -1,32 +1,34 @@ +"""This is part of the MSS Python's module. +Source: https://github.com/BoboTiG/python-mss. """ -This is part of the MSS Python's module. -Source: https://github.com/BoboTiG/python-mss -""" +from __future__ import annotations + import ctypes import ctypes.util import sys from ctypes import POINTER, Structure, c_double, c_float, c_int32, c_ubyte, c_uint32, c_uint64, c_void_p from platform import mac_ver -from typing import Any, Optional, Type, Union +from typing import TYPE_CHECKING, Any + +from mss.base import MSSBase +from mss.exception import ScreenShotError +from mss.screenshot import ScreenShot, Size -from .base import MSSBase -from .exception import ScreenShotError -from .models import CFunctions, Monitor -from .screenshot import ScreenShot, Size +if TYPE_CHECKING: + from mss.models import CFunctions, Monitor __all__ = ("MSS",) -def cgfloat() -> Union[Type[c_double], Type[c_float]]: +def cgfloat() -> type[c_double | c_float]: """Get the appropriate value for a float.""" - return c_double if sys.maxsize > 2**32 else c_float class CGPoint(Structure): """Structure that contains coordinates of a rectangle.""" - _fields_ = [("x", cgfloat()), ("y", cgfloat())] + _fields_ = (("x", cgfloat()), ("y", cgfloat())) def __repr__(self) -> str: return f"{type(self).__name__}(left={self.x} top={self.y})" @@ -35,7 +37,7 @@ def __repr__(self) -> str: class CGSize(Structure): """Structure that contains dimensions of an rectangle.""" - _fields_ = [("width", cgfloat()), ("height", cgfloat())] + _fields_ = (("width", cgfloat()), ("height", cgfloat())) def __repr__(self) -> str: return f"{type(self).__name__}(width={self.width} height={self.height})" @@ -44,7 +46,7 @@ def __repr__(self) -> str: class CGRect(Structure): """Structure that contains information about a rectangle.""" - _fields_ = [("origin", CGPoint), ("size", CGSize)] + _fields_ = (("origin", CGPoint), ("size", CGSize)) def __repr__(self) -> str: return f"{type(self).__name__}<{self.origin} {self.size}>" @@ -52,13 +54,11 @@ def __repr__(self) -> str: # C functions that will be initialised later. # -# This is a dict: -# cfunction: (attr, argtypes, restype) -# # Available attr: core. # # Note: keep it sorted by cfunction. CFUNCTIONS: CFunctions = { + # cfunction: (attr, argtypes, restype) "CGDataProviderCopyData": ("core", [c_void_p], c_void_p), "CGDisplayBounds": ("core", [c_uint32], CGRect), "CGDisplayRotation": ("core", [c_uint32], c_float), @@ -79,16 +79,14 @@ def __repr__(self) -> str: class MSS(MSSBase): - """ - Multiple ScreenShots implementation for macOS. + """Multiple ScreenShots implementation for macOS. It uses intensively the CoreGraphics library. """ __slots__ = {"core", "max_displays"} def __init__(self, /, **kwargs: Any) -> None: - """macOS initialisations.""" - + """MacOS initialisations.""" super().__init__(**kwargs) self.max_displays = kwargs.get("max_displays", 32) @@ -106,12 +104,12 @@ def _init_library(self) -> None: coregraphics = "/System/Library/Frameworks/CoreGraphics.framework/Versions/Current/CoreGraphics" if not coregraphics: - raise ScreenShotError("No CoreGraphics library found.") + msg = "No CoreGraphics library found." + raise ScreenShotError(msg) self.core = ctypes.cdll.LoadLibrary(coregraphics) def _set_cfunctions(self) -> None: """Set all ctypes functions and attach them to attributes.""" - cfactory = self._cfactory attrs = {"core": self.core} for func, (attr, argtypes, restype) in CFUNCTIONS.items(): @@ -119,7 +117,6 @@ def _set_cfunctions(self) -> None: def _monitors_impl(self) -> None: """Get positions of monitors. It will populate self._monitors.""" - int_ = int core = self.core @@ -147,7 +144,7 @@ def _monitors_impl(self) -> None: "top": int_(rect.origin.y), "width": int_(width), "height": int_(height), - } + }, ) # Update AiO monitor's values @@ -164,14 +161,13 @@ def _monitors_impl(self) -> None: def _grab_impl(self, monitor: Monitor, /) -> ScreenShot: """Retrieve all pixels from a monitor. Pixels have to be RGB.""" - # pylint: disable=too-many-locals - core = self.core rect = CGRect((monitor["left"], monitor["top"]), (monitor["width"], monitor["height"])) image_ref = core.CGWindowListCreateImage(rect, 1, 0, 0) if not image_ref: - raise ScreenShotError("CoreGraphics.CGWindowListCreateImage() failed.") + msg = "CoreGraphics.CGWindowListCreateImage() failed." + raise ScreenShotError(msg) width = core.CGImageGetWidth(image_ref) height = core.CGImageGetHeight(image_ref) @@ -204,6 +200,6 @@ def _grab_impl(self, monitor: Monitor, /) -> ScreenShot: return self.cls_image(data, monitor, size=Size(width, height)) - def _cursor_impl(self) -> Optional[ScreenShot]: + def _cursor_impl(self) -> ScreenShot | None: """Retrieve all cursor data. Pixels have to be RGB.""" return None diff --git a/src/mss/exception.py b/src/mss/exception.py index 9ffb94b..4201367 100644 --- a/src/mss/exception.py +++ b/src/mss/exception.py @@ -1,13 +1,14 @@ +"""This is part of the MSS Python's module. +Source: https://github.com/BoboTiG/python-mss. """ -This is part of the MSS Python's module. -Source: https://github.com/BoboTiG/python-mss -""" -from typing import Any, Dict, Optional +from __future__ import annotations + +from typing import Any, Dict class ScreenShotError(Exception): """Error handling class.""" - def __init__(self, message: str, /, *, details: Optional[Dict[str, Any]] = None) -> None: + def __init__(self, message: str, /, *, details: Dict[str, Any] | None = None) -> None: super().__init__(message) self.details = details or {} diff --git a/src/mss/factory.py b/src/mss/factory.py index 30e15c2..fea7df3 100644 --- a/src/mss/factory.py +++ b/src/mss/factory.py @@ -1,12 +1,11 @@ -""" -This is part of the MSS Python's module. -Source: https://github.com/BoboTiG/python-mss +"""This is part of the MSS Python's module. +Source: https://github.com/BoboTiG/python-mss. """ import platform from typing import Any -from .base import MSSBase -from .exception import ScreenShotError +from mss.base import MSSBase +from mss.exception import ScreenShotError def mss(**kwargs: Any) -> MSSBase: @@ -19,23 +18,23 @@ def mss(**kwargs: Any) -> MSSBase: It then proxies its arguments to the class for instantiation. """ - # pylint: disable=import-outside-toplevel os_ = platform.system().lower() if os_ == "darwin": - from . import darwin + from mss import darwin return darwin.MSS(**kwargs) if os_ == "linux": - from . import linux + from mss import linux return linux.MSS(**kwargs) if os_ == "windows": - from . import windows + from mss import windows return windows.MSS(**kwargs) - raise ScreenShotError(f"System {os_!r} not (yet?) implemented.") + msg = f"System {os_!r} not (yet?) implemented." + raise ScreenShotError(msg) diff --git a/src/mss/linux.py b/src/mss/linux.py index 8b118e8..7d0c8fc 100644 --- a/src/mss/linux.py +++ b/src/mss/linux.py @@ -1,7 +1,8 @@ +"""This is part of the MSS Python's module. +Source: https://github.com/BoboTiG/python-mss. """ -This is part of the MSS Python's module. -Source: https://github.com/BoboTiG/python-mss -""" +from __future__ import annotations + import os from contextlib import suppress from ctypes import ( @@ -26,12 +27,14 @@ ) from ctypes.util import find_library from threading import current_thread, local -from typing import Any, Tuple +from typing import TYPE_CHECKING, Any, Tuple + +from mss.base import MSSBase, lock +from mss.exception import ScreenShotError -from .base import MSSBase, lock -from .exception import ScreenShotError -from .models import CFunctions, Monitor -from .screenshot import ScreenShot +if TYPE_CHECKING: + from mss.models import CFunctions, Monitor + from mss.screenshot import ScreenShot __all__ = ("MSS",) @@ -41,20 +44,18 @@ class Display(Structure): - """ - Structure that serves as the connection to the X server + """Structure that serves as the connection to the X server and that contains all the information about that X server. - https://github.com/garrybodsworth/pyxlib-ctypes/blob/master/pyxlib/xlib.py#L831 + https://github.com/garrybodsworth/pyxlib-ctypes/blob/master/pyxlib/xlib.py#L831. """ class XErrorEvent(Structure): - """ - XErrorEvent to debug eventual errors. - https://tronche.com/gui/x/xlib/event-handling/protocol-errors/default-handlers.html + """XErrorEvent to debug eventual errors. + https://tronche.com/gui/x/xlib/event-handling/protocol-errors/default-handlers.html. """ - _fields_ = [ + _fields_ = ( ("type", c_int), ("display", POINTER(Display)), # Display the event was read from ("serial", c_ulong), # serial number of failed request @@ -62,17 +63,16 @@ class XErrorEvent(Structure): ("request_code", c_ubyte), # major op-code of failed request ("minor_code", c_ubyte), # minor op-code of failed request ("resourceid", c_void_p), # resource ID - ] + ) class XFixesCursorImage(Structure): - """ - Cursor structure. + """Cursor structure. /usr/include/X11/extensions/Xfixes.h - https://github.com/freedesktop/xorg-libXfixes/blob/libXfixes-6.0.0/include/X11/extensions/Xfixes.h#L96 + https://github.com/freedesktop/xorg-libXfixes/blob/libXfixes-6.0.0/include/X11/extensions/Xfixes.h#L96. """ - _fields_ = [ + _fields_ = ( ("x", c_short), ("y", c_short), ("width", c_ushort), @@ -83,16 +83,15 @@ class XFixesCursorImage(Structure): ("pixels", POINTER(c_ulong)), ("atom", c_ulong), ("name", c_char_p), - ] + ) class XImage(Structure): - """ - Description of an image as it exists in the client's memory. - https://tronche.com/gui/x/xlib/graphics/images.html + """Description of an image as it exists in the client's memory. + https://tronche.com/gui/x/xlib/graphics/images.html. """ - _fields_ = [ + _fields_ = ( ("width", c_int), # size of image ("height", c_int), # size of image ("xoffset", c_int), # number of pixels offset in X direction @@ -108,16 +107,15 @@ class XImage(Structure): ("red_mask", c_ulong), # bits in z arrangment ("green_mask", c_ulong), # bits in z arrangment ("blue_mask", c_ulong), # bits in z arrangment - ] + ) class XRRCrtcInfo(Structure): - """ - Structure that contains CRTC information. - https://gitlab.freedesktop.org/xorg/lib/libxrandr/-/blob/master/include/X11/extensions/Xrandr.h#L360 + """Structure that contains CRTC information. + https://gitlab.freedesktop.org/xorg/lib/libxrandr/-/blob/master/include/X11/extensions/Xrandr.h#L360. """ - _fields_ = [ + _fields_ = ( ("timestamp", c_ulong), ("x", c_int), ("y", c_int), @@ -130,21 +128,20 @@ class XRRCrtcInfo(Structure): ("rotations", c_ushort), ("npossible", c_int), ("possible", POINTER(c_long)), - ] + ) class XRRModeInfo(Structure): - """https://gitlab.freedesktop.org/xorg/lib/libxrandr/-/blob/master/include/X11/extensions/Xrandr.h#L248""" + """https://gitlab.freedesktop.org/xorg/lib/libxrandr/-/blob/master/include/X11/extensions/Xrandr.h#L248.""" class XRRScreenResources(Structure): - """ - Structure that contains arrays of XIDs that point to the + """Structure that contains arrays of XIDs that point to the available outputs and associated CRTCs. - https://gitlab.freedesktop.org/xorg/lib/libxrandr/-/blob/master/include/X11/extensions/Xrandr.h#L265 + https://gitlab.freedesktop.org/xorg/lib/libxrandr/-/blob/master/include/X11/extensions/Xrandr.h#L265. """ - _fields_ = [ + _fields_ = ( ("timestamp", c_ulong), ("configTimestamp", c_ulong), ("ncrtc", c_int), @@ -153,13 +150,13 @@ class XRRScreenResources(Structure): ("outputs", POINTER(c_long)), ("nmode", c_int), ("modes", POINTER(XRRModeInfo)), - ] + ) class XWindowAttributes(Structure): """Attributes for the specified window.""" - _fields_ = [ + _fields_ = ( ("x", c_int32), # location of window ("y", c_int32), # location of window ("width", c_int32), # width of window @@ -183,7 +180,7 @@ class XWindowAttributes(Structure): ("do_not_propagate_mask", c_ulong), # set of events that should not propagate ("override_redirect", c_int32), # boolean value for override-redirect ("screen", c_ulong), # back pointer to correct screen - ] + ) _ERROR = {} @@ -195,7 +192,6 @@ class XWindowAttributes(Structure): @CFUNCTYPE(c_int, POINTER(Display), POINTER(XErrorEvent)) def _error_handler(display: Display, event: XErrorEvent) -> int: """Specifies the program's supplied error handler.""" - # Get the specific error message xlib = cdll.LoadLibrary(_X11) # type: ignore[arg-type] get_error = xlib.XGetErrorText @@ -220,25 +216,23 @@ def _error_handler(display: Display, event: XErrorEvent) -> int: def _validate(retval: int, func: Any, args: Tuple[Any, Any], /) -> Tuple[Any, Any]: """Validate the returned value of a C function call.""" - thread = current_thread() if retval != 0 and thread not in _ERROR: return args details = _ERROR.pop(thread, {}) - raise ScreenShotError(f"{func.__name__}() failed", details=details) + msg = f"{func.__name__}() failed" + raise ScreenShotError(msg, details=details) # C functions that will be initialised later. # See https://tronche.com/gui/x/xlib/function-index.html for details. # -# This is a dict: -# cfunction: (attr, argtypes, restype) -# # Available attr: xfixes, xlib, xrandr. # # Note: keep it sorted by cfunction. CFUNCTIONS: CFunctions = { + # cfunction: (attr, argtypes, restype) "XCloseDisplay": ("xlib", [POINTER(Display)], c_void_p), "XDefaultRootWindow": ("xlib", [POINTER(Display)], POINTER(XWindowAttributes)), "XDestroyImage": ("xlib", [POINTER(XImage)], c_void_p), @@ -261,8 +255,7 @@ def _validate(retval: int, func: Any, args: Tuple[Any, Any], /) -> Tuple[Any, An class MSS(MSSBase): - """ - Multiple ScreenShots implementation for GNU/Linux. + """Multiple ScreenShots implementation for GNU/Linux. It uses intensively the Xlib and its Xrandr extension. """ @@ -270,7 +263,6 @@ class MSS(MSSBase): def __init__(self, /, **kwargs: Any) -> None: """GNU/Linux initialisations.""" - super().__init__(**kwargs) # Available thread-specific variables @@ -285,20 +277,24 @@ def __init__(self, /, **kwargs: Any) -> None: try: display = os.environ["DISPLAY"].encode("utf-8") except KeyError: - raise ScreenShotError("$DISPLAY not set.") from None + msg = "$DISPLAY not set." + raise ScreenShotError(msg) from None if not isinstance(display, bytes): display = display.encode("utf-8") if b":" not in display: - raise ScreenShotError(f"Bad display value: {display!r}.") + msg = f"Bad display value: {display!r}." + raise ScreenShotError(msg) if not _X11: - raise ScreenShotError("No X11 library found.") + msg = "No X11 library found." + raise ScreenShotError(msg) self.xlib = cdll.LoadLibrary(_X11) if not _XRANDR: - raise ScreenShotError("No Xrandr extension found.") + msg = "No Xrandr extension found." + raise ScreenShotError(msg) self.xrandr = cdll.LoadLibrary(_XRANDR) if self.with_cursor: @@ -314,10 +310,12 @@ def __init__(self, /, **kwargs: Any) -> None: self._handles.display = self.xlib.XOpenDisplay(display) if not self._handles.display: - raise ScreenShotError(f"Unable to open display: {display!r}.") + msg = f"Unable to open display: {display!r}." + raise ScreenShotError(msg) if not self._is_extension_enabled("RANDR"): - raise ScreenShotError("Xrandr not enabled.") + msg = "Xrandr not enabled." + raise ScreenShotError(msg) self._handles.root = self.xlib.XDefaultRootWindow(self._handles.display) @@ -367,7 +365,6 @@ def _is_extension_enabled(self, name: str, /) -> bool: def _set_cfunctions(self) -> None: """Set all ctypes functions and attach them to attributes.""" - cfactory = self._cfactory attrs = { "xfixes": getattr(self, "xfixes", None), @@ -381,7 +378,6 @@ def _set_cfunctions(self) -> None: def _monitors_impl(self) -> None: """Get positions of monitors. It will populate self._monitors.""" - display = self._handles.display int_ = int xrandr = self.xrandr @@ -390,7 +386,7 @@ def _monitors_impl(self) -> None: gwa = XWindowAttributes() self.xlib.XGetWindowAttributes(display, self._handles.root, byref(gwa)) self._monitors.append( - {"left": int_(gwa.x), "top": int_(gwa.y), "width": int_(gwa.width), "height": int_(gwa.height)} + {"left": int_(gwa.x), "top": int_(gwa.y), "width": int_(gwa.width), "height": int_(gwa.height)}, ) # Each monitor @@ -416,14 +412,13 @@ def _monitors_impl(self) -> None: "top": int_(crtc.y), "width": int_(crtc.width), "height": int_(crtc.height), - } + }, ) xrandr.XRRFreeCrtcInfo(crtc) xrandr.XRRFreeScreenResources(mon) def _grab_impl(self, monitor: Monitor, /) -> ScreenShot: """Retrieve all pixels from a monitor. Pixels have to be RGB.""" - ximage = self.xlib.XGetImage( self._handles.display, self._handles.drawable, @@ -438,7 +433,8 @@ def _grab_impl(self, monitor: Monitor, /) -> ScreenShot: try: bits_per_pixel = ximage.contents.bits_per_pixel if bits_per_pixel != 32: - raise ScreenShotError(f"[XImage] bits per pixel value not (yet?) implemented: {bits_per_pixel}.") + msg = f"[XImage] bits per pixel value not (yet?) implemented: {bits_per_pixel}." + raise ScreenShotError(msg) raw_data = cast( ximage.contents.data, @@ -453,11 +449,11 @@ def _grab_impl(self, monitor: Monitor, /) -> ScreenShot: def _cursor_impl(self) -> ScreenShot: """Retrieve all cursor data. Pixels have to be RGB.""" - # Read data of cursor/mouse-pointer ximage = self.xfixes.XFixesGetCursorImage(self._handles.display) if not (ximage and ximage.contents): - raise ScreenShotError("Cannot read XFixesGetCursorImage()") + msg = "Cannot read XFixesGetCursorImage()" + raise ScreenShotError(msg) cursor_img: XFixesCursorImage = ximage.contents region = { diff --git a/src/mss/models.py b/src/mss/models.py index 9c0851a..a6a7bf8 100644 --- a/src/mss/models.py +++ b/src/mss/models.py @@ -1,10 +1,8 @@ -""" -This is part of the MSS Python's module. -Source: https://github.com/BoboTiG/python-mss +"""This is part of the MSS Python's module. +Source: https://github.com/BoboTiG/python-mss. """ -import collections -from typing import Any, Dict, List, Tuple +from typing import Any, Dict, List, NamedTuple, Tuple Monitor = Dict[str, int] Monitors = List[Monitor] @@ -12,7 +10,14 @@ Pixel = Tuple[int, int, int] Pixels = List[Pixel] -Pos = collections.namedtuple("Pos", "left, top") -Size = collections.namedtuple("Size", "width, height") - CFunctions = Dict[str, Tuple[str, List[Any], Any]] + + +class Pos(NamedTuple): + left: int + top: int + + +class Size(NamedTuple): + width: int + height: int diff --git a/src/mss/screenshot.py b/src/mss/screenshot.py index 9c82d72..cad551b 100644 --- a/src/mss/screenshot.py +++ b/src/mss/screenshot.py @@ -1,17 +1,19 @@ +"""This is part of the MSS Python's module. +Source: https://github.com/BoboTiG/python-mss. """ -This is part of the MSS Python's module. -Source: https://github.com/BoboTiG/python-mss -""" +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, Dict -from typing import Any, Dict, Iterator, Optional, Type +from mss.exception import ScreenShotError +from mss.models import Monitor, Pixel, Pixels, Pos, Size -from .exception import ScreenShotError -from .models import Monitor, Pixel, Pixels, Pos, Size +if TYPE_CHECKING: + from collections.abc import Iterator class ScreenShot: - """ - Screen shot object. + """Screen shot object. .. note:: @@ -21,9 +23,9 @@ class ScreenShot: __slots__ = {"__pixels", "__rgb", "pos", "raw", "size"} - def __init__(self, data: bytearray, monitor: Monitor, /, *, size: Optional[Size] = None) -> None: - self.__pixels: Optional[Pixels] = None - self.__rgb: Optional[bytes] = None + def __init__(self, data: bytearray, monitor: Monitor, /, *, size: Size | None = None) -> None: + self.__pixels: Pixels | None = None + self.__rgb: bytes | None = None #: Bytearray of the raw BGRA pixels retrieved by ctypes #: OS independent implementations. @@ -40,13 +42,11 @@ def __repr__(self) -> str: @property def __array_interface__(self) -> Dict[str, Any]: - """ - Numpy array interface support. + """Numpy array interface support. It uses raw data in BGRA form. See https://docs.scipy.org/doc/numpy/reference/arrays.interface.html """ - return { "version": 3, "shape": (self.height, self.width, 4), @@ -55,7 +55,7 @@ def __array_interface__(self) -> Dict[str, Any]: } @classmethod - def from_size(cls: Type["ScreenShot"], data: bytearray, width: int, height: int, /) -> "ScreenShot": + def from_size(cls: type[ScreenShot], data: bytearray, width: int, height: int, /) -> ScreenShot: """Instantiate a new class given only screen shot's data and size.""" monitor = {"left": 0, "top": 0, "width": width, "height": height} return cls(data, monitor) @@ -77,24 +77,19 @@ def left(self) -> int: @property def pixels(self) -> Pixels: - """ - :return list: RGB tuples. - """ - + """:return list: RGB tuples.""" if not self.__pixels: rgb_tuples: Iterator[Pixel] = zip(self.raw[2::4], self.raw[1::4], self.raw[::4]) - self.__pixels = list(zip(*[iter(rgb_tuples)] * self.width)) # type: ignore + self.__pixels = list(zip(*[iter(rgb_tuples)] * self.width)) return self.__pixels @property def rgb(self) -> bytes: - """ - Compute RGB values from the BGRA raw pixels. + """Compute RGB values from the BGRA raw pixels. :return bytes: RGB pixels. """ - if not self.__rgb: rgb = bytearray(self.height * self.width * 3) raw = self.raw @@ -116,15 +111,14 @@ def width(self) -> int: return self.size.width def pixel(self, coord_x: int, coord_y: int) -> Pixel: - """ - Returns the pixel value at a given position. + """Returns the pixel value at a given position. :param int coord_x: The x coordinate. :param int coord_y: The y coordinate. :return tuple: The pixel value as (R, G, B). """ - try: - return self.pixels[coord_y][coord_x] # type: ignore + return self.pixels[coord_y][coord_x] # type: ignore[return-value] except IndexError as exc: - raise ScreenShotError(f"Pixel location ({coord_x}, {coord_y}) is out of range.") from exc + msg = f"Pixel location ({coord_x}, {coord_y}) is out of range." + raise ScreenShotError(msg) from exc diff --git a/src/mss/tools.py b/src/mss/tools.py index de3a1af..316939c 100644 --- a/src/mss/tools.py +++ b/src/mss/tools.py @@ -1,17 +1,15 @@ +"""This is part of the MSS Python's module. +Source: https://github.com/BoboTiG/python-mss. """ -This is part of the MSS Python's module. -Source: https://github.com/BoboTiG/python-mss -""" +from __future__ import annotations import os import struct import zlib -from typing import Optional, Tuple -def to_png(data: bytes, size: Tuple[int, int], /, *, level: int = 6, output: Optional[str] = None) -> Optional[bytes]: - """ - Dump data to a PNG file. If `output` is `None`, create no file but return +def to_png(data: bytes, size: tuple[int, int], /, *, level: int = 6, output: str | None = None) -> bytes | None: + """Dump data to a PNG file. If `output` is `None`, create no file but return the whole PNG data. :param bytes data: RGBRGB...RGB data. @@ -19,7 +17,6 @@ def to_png(data: bytes, size: Tuple[int, int], /, *, level: int = 6, output: Opt :param int level: PNG compression level. :param str output: Output file name. """ - # pylint: disable=too-many-locals pack = struct.pack crc32 = zlib.crc32 diff --git a/src/mss/windows.py b/src/mss/windows.py index a8c28d3..fab2794 100644 --- a/src/mss/windows.py +++ b/src/mss/windows.py @@ -1,7 +1,8 @@ +"""This is part of the MSS Python's module. +Source: https://github.com/BoboTiG/python-mss. """ -This is part of the MSS Python's module. -Source: https://github.com/BoboTiG/python-mss -""" +from __future__ import annotations + import ctypes import sys from ctypes import POINTER, WINFUNCTYPE, Structure, c_int, c_void_p @@ -22,12 +23,14 @@ WORD, ) from threading import local -from typing import Any, Optional +from typing import TYPE_CHECKING, Any + +from mss.base import MSSBase +from mss.exception import ScreenShotError -from .base import MSSBase -from .exception import ScreenShotError -from .models import CFunctions, Monitor -from .screenshot import ScreenShot +if TYPE_CHECKING: + from mss.models import CFunctions, Monitor + from mss.screenshot import ScreenShot __all__ = ("MSS",) @@ -40,7 +43,7 @@ class BITMAPINFOHEADER(Structure): """Information about the dimensions and color format of a DIB.""" - _fields_ = [ + _fields_ = ( ("biSize", DWORD), ("biWidth", LONG), ("biHeight", LONG), @@ -52,15 +55,13 @@ class BITMAPINFOHEADER(Structure): ("biYPelsPerMeter", LONG), ("biClrUsed", DWORD), ("biClrImportant", DWORD), - ] + ) class BITMAPINFO(Structure): - """ - Structure that defines the dimensions and color information for a DIB. - """ + """Structure that defines the dimensions and color information for a DIB.""" - _fields_ = [("bmiHeader", BITMAPINFOHEADER), ("bmiColors", DWORD * 3)] + _fields_ = (("bmiHeader", BITMAPINFOHEADER), ("bmiColors", DWORD * 3)) MONITORNUMPROC = WINFUNCTYPE(INT, DWORD, DWORD, POINTER(RECT), DOUBLE) @@ -68,13 +69,11 @@ class BITMAPINFO(Structure): # C functions that will be initialised later. # -# This is a dict: -# cfunction: (attr, argtypes, restype) -# # Available attr: gdi32, user32. # # Note: keep it sorted by cfunction. CFUNCTIONS: CFunctions = { + # cfunction: (attr, argtypes, restype) "BitBlt": ("gdi32", [HDC, INT, INT, INT, INT, HDC, INT, INT, DWORD], BOOL), "CreateCompatibleBitmap": ("gdi32", [HDC, INT, INT], HBITMAP), "CreateCompatibleDC": ("gdi32", [HDC], HDC), @@ -97,7 +96,6 @@ class MSS(MSSBase): def __init__(self, /, **kwargs: Any) -> None: """Windows initialisations.""" - super().__init__(**kwargs) self.user32 = ctypes.WinDLL("user32") @@ -137,7 +135,6 @@ def close(self) -> None: def _set_cfunctions(self) -> None: """Set all ctypes functions and attach them to attributes.""" - cfactory = self._cfactory attrs = { "gdi32": self.gdi32, @@ -148,8 +145,7 @@ def _set_cfunctions(self) -> None: def _set_dpi_awareness(self) -> None: """Set DPI awareness to capture full screen on Hi-DPI monitors.""" - - version = sys.getwindowsversion()[:2] # pylint: disable=no-member + version = sys.getwindowsversion()[:2] if version >= (6, 3): # Windows 8.1+ # Here 2 = PROCESS_PER_MONITOR_DPI_AWARE, which means: @@ -163,7 +159,6 @@ def _set_dpi_awareness(self) -> None: def _monitors_impl(self) -> None: """Get positions of monitors. It will populate self._monitors.""" - int_ = int user32 = self.user32 get_system_metrics = user32.GetSystemMetrics @@ -175,16 +170,14 @@ def _monitors_impl(self) -> None: "top": int_(get_system_metrics(77)), # SM_YVIRTUALSCREEN "width": int_(get_system_metrics(78)), # SM_CXVIRTUALSCREEN "height": int_(get_system_metrics(79)), # SM_CYVIRTUALSCREEN - } + }, ) # Each monitor - def _callback(monitor: int, data: HDC, rect: LPRECT, dc_: LPARAM) -> int: - """ - Callback for monitorenumproc() function, it will return + def _callback(_monitor: int, _data: HDC, rect: LPRECT, _dc: LPARAM) -> int: + """Callback for monitorenumproc() function, it will return a RECT with appropriate values. """ - # pylint: disable=unused-argument rct = rect.contents self._monitors.append( @@ -193,7 +186,7 @@ def _callback(monitor: int, data: HDC, rect: LPRECT, dc_: LPARAM) -> int: "top": int_(rct.top), "width": int_(rct.right) - int_(rct.left), "height": int_(rct.bottom) - int_(rct.top), - } + }, ) return 1 @@ -201,8 +194,7 @@ def _callback(monitor: int, data: HDC, rect: LPRECT, dc_: LPARAM) -> int: user32.EnumDisplayMonitors(0, 0, callback, 0) def _grab_impl(self, monitor: Monitor, /) -> ScreenShot: - """ - Retrieve all pixels from a monitor. Pixels have to be RGB. + """Retrieve all pixels from a monitor. Pixels have to be RGB. In the code, there are a few interesting things: @@ -231,7 +223,6 @@ def _grab_impl(self, monitor: Monitor, /) -> ScreenShot: retrieved by gdi32.GetDIBits() as a sequence of RGB values. Thanks to http://stackoverflow.com/a/3688682 """ - srcdc, memdc = self._handles.srcdc, self._handles.memdc gdi = self.gdi32 width, height = monitor["width"], monitor["height"] @@ -249,10 +240,11 @@ def _grab_impl(self, monitor: Monitor, /) -> ScreenShot: gdi.BitBlt(memdc, 0, 0, width, height, srcdc, monitor["left"], monitor["top"], SRCCOPY | CAPTUREBLT) bits = gdi.GetDIBits(memdc, self._handles.bmp, 0, height, self._handles.data, self._handles.bmi, DIB_RGB_COLORS) if bits != height: - raise ScreenShotError("gdi32.GetDIBits() failed.") + msg = "gdi32.GetDIBits() failed." + raise ScreenShotError(msg) return self.cls_image(bytearray(self._handles.data), monitor) - def _cursor_impl(self) -> Optional[ScreenShot]: + def _cursor_impl(self) -> ScreenShot | None: """Retrieve all cursor data. Pixels have to be RGB.""" return None diff --git a/src/tests/bench_bgra2rgb.py b/src/tests/bench_bgra2rgb.py index 2319684..49043f5 100644 --- a/src/tests/bench_bgra2rgb.py +++ b/src/tests/bench_bgra2rgb.py @@ -1,6 +1,5 @@ -""" -This is part of the MSS Python's module. -Source: https://github.com/BoboTiG/python-mss +"""This is part of the MSS Python's module. +Source: https://github.com/BoboTiG/python-mss. 2018-03-19. @@ -30,34 +29,34 @@ """ import time -import numpy -from PIL import Image - import mss +import numpy as np +from mss.screenshot import ScreenShot +from PIL import Image -def mss_rgb(im): +def mss_rgb(im: ScreenShot) -> bytes: return im.rgb -def numpy_flip(im): - frame = numpy.array(im, dtype=numpy.uint8) - return numpy.flip(frame[:, :, :3], 2).tobytes() +def numpy_flip(im: ScreenShot) -> bytes: + frame = np.array(im, dtype=np.uint8) + return np.flip(frame[:, :, :3], 2).tobytes() -def numpy_slice(im): - return numpy.array(im, dtype=numpy.uint8)[..., [2, 1, 0]].tobytes() +def numpy_slice(im: ScreenShot) -> bytes: + return np.array(im, dtype=np.uint8)[..., [2, 1, 0]].tobytes() -def pil_frombytes_rgb(im): +def pil_frombytes_rgb(im: ScreenShot) -> bytes: return Image.frombytes("RGB", im.size, im.rgb).tobytes() -def pil_frombytes(im): +def pil_frombytes(im: ScreenShot) -> bytes: return Image.frombytes("RGB", im.size, im.bgra, "raw", "BGRX").tobytes() -def benchmark(): +def benchmark() -> None: with mss.mss() as sct: im = sct.grab(sct.monitors[0]) for func in ( @@ -71,7 +70,7 @@ def benchmark(): start = time.time() while (time.time() - start) <= 1: func(im) - im._ScreenShot__rgb = None + im._ScreenShot__rgb = None # type: ignore[attr-defined] count += 1 print(func.__name__.ljust(17), count) diff --git a/src/tests/bench_general.py b/src/tests/bench_general.py index 1fe44cc..cec26d5 100644 --- a/src/tests/bench_general.py +++ b/src/tests/bench_general.py @@ -1,6 +1,5 @@ -""" -This is part of the MSS Python's module. -Source: https://github.com/BoboTiG/python-mss +"""This is part of the MSS Python's module. +Source: https://github.com/BoboTiG/python-mss. 2018-03-19. @@ -25,32 +24,41 @@ access_rgb 574 712 +24.04 output 139 188 +35.25 """ +from __future__ import annotations + from time import time +from typing import TYPE_CHECKING import mss import mss.tools +if TYPE_CHECKING: + from collections.abc import Callable + + from mss.base import MSSBase + from mss.screenshot import ScreenShot + -def grab(sct): +def grab(sct: MSSBase) -> ScreenShot: monitor = {"top": 144, "left": 80, "width": 1397, "height": 782} return sct.grab(monitor) -def access_rgb(sct): +def access_rgb(sct: MSSBase) -> bytes: im = grab(sct) return im.rgb -def output(sct, filename=None): +def output(sct: MSSBase, filename: str | None = None) -> None: rgb = access_rgb(sct) mss.tools.to_png(rgb, (1397, 782), output=filename) -def save(sct): +def save(sct: MSSBase) -> None: output(sct, filename="screenshot.png") -def benchmark(func): +def benchmark(func: Callable) -> None: count = 0 start = time() diff --git a/src/tests/conftest.py b/src/tests/conftest.py index 72da4df..a97ccc3 100644 --- a/src/tests/conftest.py +++ b/src/tests/conftest.py @@ -1,46 +1,43 @@ -""" -This is part of the MSS Python's module. -Source: https://github.com/BoboTiG/python-mss +"""This is part of the MSS Python's module. +Source: https://github.com/BoboTiG/python-mss. """ import glob import os import platform from hashlib import md5 from pathlib import Path +from typing import Generator from zipfile import ZipFile import pytest - from mss import mss @pytest.fixture(autouse=True) -def no_warnings(recwarn): +def _no_warnings(recwarn: pytest.WarningsRecorder) -> Generator: """Fail on warning.""" - yield - warnings = ["{w.filename}:{w.lineno} {w.message}".format(w=warning) for warning in recwarn] + warnings = [f"{warning.filename}:{warning.lineno} {warning.message}" for warning in recwarn] for warning in warnings: print(warning) assert not warnings -def purge_files(): +def purge_files() -> None: """Remove all generated files from previous runs.""" - for fname in glob.glob("*.png"): - print("Deleting {!r} ...".format(fname)) + print(f"Deleting {fname!r} ...") os.unlink(fname) for fname in glob.glob("*.png.old"): - print("Deleting {!r} ...".format(fname)) + print(f"Deleting {fname!r} ...") os.unlink(fname) @pytest.fixture(scope="module", autouse=True) -def before_tests(request): - request.addfinalizer(purge_files) +def _before_tests() -> None: + purge_files() @pytest.fixture(scope="session") @@ -56,7 +53,6 @@ def raw() -> bytes: @pytest.fixture(scope="session") def pixel_ratio() -> int: """Get the pixel, used to adapt test checks.""" - if platform.system().lower() != "darwin": return 1 diff --git a/src/tests/test_bgra_to_rgb.py b/src/tests/test_bgra_to_rgb.py index 1fa2c04..ddd9529 100644 --- a/src/tests/test_bgra_to_rgb.py +++ b/src/tests/test_bgra_to_rgb.py @@ -1,20 +1,18 @@ -""" -This is part of the MSS Python's module. -Source: https://github.com/BoboTiG/python-mss +"""This is part of the MSS Python's module. +Source: https://github.com/BoboTiG/python-mss. """ import pytest - from mss.base import ScreenShot -def test_bad_length(): +def test_bad_length() -> None: data = bytearray(b"789c626001000000ffff030000060005") image = ScreenShot.from_size(data, 1024, 768) - with pytest.raises(ValueError): - image.rgb + with pytest.raises(ValueError, match="attempt to assign"): + _ = image.rgb -def test_good_types(raw: bytes): +def test_good_types(raw: bytes) -> None: image = ScreenShot.from_size(bytearray(raw), 1024, 768) assert isinstance(image.raw, bytearray) assert isinstance(image.rgb, bytes) diff --git a/src/tests/test_cls_image.py b/src/tests/test_cls_image.py index b531ba1..eb6b859 100644 --- a/src/tests/test_cls_image.py +++ b/src/tests/test_cls_image.py @@ -1,21 +1,22 @@ -""" -This is part of the MSS Python's module. -Source: https://github.com/BoboTiG/python-mss +"""This is part of the MSS Python's module. +Source: https://github.com/BoboTiG/python-mss. """ import os +from typing import Any from mss import mss +from mss.models import Monitor class SimpleScreenShot: - def __init__(self, data, monitor, **_): + def __init__(self, data: bytearray, monitor: Monitor, **_: Any) -> None: self.raw = bytes(data) self.monitor = monitor -def test_custom_cls_image(): +def test_custom_cls_image() -> None: with mss(display=os.getenv("DISPLAY")) as sct: - sct.cls_image = SimpleScreenShot + sct.cls_image = SimpleScreenShot # type: ignore[assignment] mon1 = sct.monitors[1] image = sct.grab(mon1) assert isinstance(image, SimpleScreenShot) diff --git a/src/tests/test_find_monitors.py b/src/tests/test_find_monitors.py index 278dc1b..7939e17 100644 --- a/src/tests/test_find_monitors.py +++ b/src/tests/test_find_monitors.py @@ -1,18 +1,17 @@ -""" -This is part of the MSS Python's module. -Source: https://github.com/BoboTiG/python-mss +"""This is part of the MSS Python's module. +Source: https://github.com/BoboTiG/python-mss. """ import os from mss import mss -def test_get_monitors(): +def test_get_monitors() -> None: with mss(display=os.getenv("DISPLAY")) as sct: assert sct.monitors -def test_keys_aio(): +def test_keys_aio() -> None: with mss(display=os.getenv("DISPLAY")) as sct: all_monitors = sct.monitors[0] assert "top" in all_monitors @@ -21,7 +20,7 @@ def test_keys_aio(): assert "width" in all_monitors -def test_keys_monitor_1(): +def test_keys_monitor_1() -> None: with mss(display=os.getenv("DISPLAY")) as sct: mon1 = sct.monitors[1] assert "top" in mon1 @@ -30,7 +29,7 @@ def test_keys_monitor_1(): assert "width" in mon1 -def test_dimensions(): +def test_dimensions() -> None: with mss(display=os.getenv("DISPLAY")) as sct: mon = sct.monitors[1] assert mon["width"] > 0 diff --git a/src/tests/test_get_pixels.py b/src/tests/test_get_pixels.py index 8535538..ec85f7f 100644 --- a/src/tests/test_get_pixels.py +++ b/src/tests/test_get_pixels.py @@ -1,19 +1,17 @@ -""" -This is part of the MSS Python's module. -Source: https://github.com/BoboTiG/python-mss +"""This is part of the MSS Python's module. +Source: https://github.com/BoboTiG/python-mss. """ import itertools import os import pytest - from mss import mss from mss.base import ScreenShot from mss.exception import ScreenShotError -def test_grab_monitor(): +def test_grab_monitor() -> None: with mss(display=os.getenv("DISPLAY")) as sct: for mon in sct.monitors: image = sct.grab(mon) @@ -22,7 +20,7 @@ def test_grab_monitor(): assert isinstance(image.rgb, bytes) -def test_grab_part_of_screen(pixel_ratio): +def test_grab_part_of_screen(pixel_ratio: int) -> None: with mss(display=os.getenv("DISPLAY")) as sct: for width, height in itertools.product(range(1, 42), range(1, 42)): monitor = {"top": 160, "left": 160, "width": width, "height": height} @@ -34,7 +32,7 @@ def test_grab_part_of_screen(pixel_ratio): assert image.height == height * pixel_ratio -def test_get_pixel(raw: bytes): +def test_get_pixel(raw: bytes) -> None: image = ScreenShot.from_size(bytearray(raw), 1024, 768) assert image.width == 1024 assert image.height == 768 diff --git a/src/tests/test_gnu_linux.py b/src/tests/test_gnu_linux.py index 1357790..cca29ab 100644 --- a/src/tests/test_gnu_linux.py +++ b/src/tests/test_gnu_linux.py @@ -1,14 +1,13 @@ -""" -This is part of the MSS Python's module. -Source: https://github.com/BoboTiG/python-mss +"""This is part of the MSS Python's module. +Source: https://github.com/BoboTiG/python-mss. """ import platform +from collections.abc import Generator from unittest.mock import Mock, patch -import pytest - import mss import mss.linux +import pytest from mss.base import MSSBase from mss.exception import ScreenShotError @@ -21,21 +20,19 @@ DEPTH = 24 -@pytest.fixture -def display() -> str: +@pytest.fixture() +def display() -> Generator: with pyvirtualdisplay.Display(size=(WIDTH, HEIGHT), color_depth=DEPTH) as vdisplay: yield vdisplay.new_display_var @pytest.mark.skipif(PYPY, reason="Failure on PyPy") -def test_factory_systems(monkeypatch): - """ - Here, we are testing all systems. +def test_factory_systems(monkeypatch: pytest.MonkeyPatch) -> None: + """Here, we are testing all systems. Too hard to maintain the test for all platforms, so test only on GNU/Linux. """ - # GNU/Linux monkeypatch.setattr(platform, "system", lambda: "LINUX") with mss.mss() as sct: @@ -44,79 +41,69 @@ def test_factory_systems(monkeypatch): # macOS monkeypatch.setattr(platform, "system", lambda: "Darwin") - with pytest.raises((ScreenShotError, ValueError)): - # ValueError on macOS Big Sur - with mss.mss(): - pass + # ValueError on macOS Big Sur + with pytest.raises((ScreenShotError, ValueError)), mss.mss(): + pass monkeypatch.undo() # Windows monkeypatch.setattr(platform, "system", lambda: "wInDoWs") - with pytest.raises(ImportError): - # ImportError: cannot import name 'WINFUNCTYPE' - with mss.mss(): - pass + with pytest.raises(ImportError, match="cannot import name 'WINFUNCTYPE'"), mss.mss(): + pass -def test_arg_display(display: str, monkeypatch): +def test_arg_display(display: str, monkeypatch: pytest.MonkeyPatch) -> None: # Good value with mss.mss(display=display): pass # Bad `display` (missing ":" in front of the number) - with pytest.raises(ScreenShotError): - with mss.mss(display="0"): - pass + with pytest.raises(ScreenShotError), mss.mss(display="0"): + pass # Invalid `display` that is not trivially distinguishable. - with pytest.raises(ScreenShotError): - with mss.mss(display=":INVALID"): - pass + with pytest.raises(ScreenShotError), mss.mss(display=":INVALID"): + pass # No `DISPLAY` in envars monkeypatch.delenv("DISPLAY") - with pytest.raises(ScreenShotError): - with mss.mss(): - pass + with pytest.raises(ScreenShotError), mss.mss(): + pass @pytest.mark.skipif(PYPY, reason="Failure on PyPy") -def test_bad_display_structure(monkeypatch): +def test_bad_display_structure(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setattr(mss.linux, "Display", lambda: None) - with pytest.raises(TypeError): - with mss.mss(): - pass + with pytest.raises(TypeError), mss.mss(): + pass @patch("mss.linux._X11", new=None) -def test_no_xlib_library(): - with pytest.raises(ScreenShotError): - with mss.mss(): - pass +def test_no_xlib_library() -> None: + with pytest.raises(ScreenShotError), mss.mss(): + pass @patch("mss.linux._XRANDR", new=None) -def test_no_xrandr_extension(): - with pytest.raises(ScreenShotError): - with mss.mss(): - pass +def test_no_xrandr_extension() -> None: + with pytest.raises(ScreenShotError), mss.mss(): + pass @patch("mss.linux.MSS._is_extension_enabled", new=Mock(return_value=False)) -def test_xrandr_extension_exists_but_is_not_enabled(display: str): - with pytest.raises(ScreenShotError): - with mss.mss(display=display): - pass +def test_xrandr_extension_exists_but_is_not_enabled(display: str) -> None: + with pytest.raises(ScreenShotError), mss.mss(display=display): + pass -def test_unsupported_depth(): +def test_unsupported_depth() -> None: with pyvirtualdisplay.Display(size=(WIDTH, HEIGHT), color_depth=8) as vdisplay: with pytest.raises(ScreenShotError): with mss.mss(display=vdisplay.new_display_var) as sct: sct.grab(sct.monitors[1]) -def test_region_out_of_monitor_bounds(display: str): +def test_region_out_of_monitor_bounds(display: str) -> None: monitor = {"left": -30, "top": 0, "width": WIDTH, "height": HEIGHT} assert not mss.linux._ERROR @@ -136,19 +123,22 @@ def test_region_out_of_monitor_bounds(display: str): assert not mss.linux._ERROR -def test__is_extension_enabled_unknown_name(display: str): +def test__is_extension_enabled_unknown_name(display: str) -> None: with mss.mss(display=display) as sct: + assert isinstance(sct, mss.linux.MSS) # For Mypy assert not sct._is_extension_enabled("NOEXT") -def test_missing_fast_function_for_monitor_details_retrieval(display: str): +def test_missing_fast_function_for_monitor_details_retrieval(display: str) -> None: with mss.mss(display=display) as sct: + assert isinstance(sct, mss.linux.MSS) # For Mypy assert hasattr(sct.xrandr, "XRRGetScreenResourcesCurrent") screenshot_with_fast_fn = sct.grab(sct.monitors[1]) assert set(screenshot_with_fast_fn.rgb) == {0} with mss.mss(display=display) as sct: + assert isinstance(sct, mss.linux.MSS) # For Mypy assert hasattr(sct.xrandr, "XRRGetScreenResourcesCurrent") del sct.xrandr.XRRGetScreenResourcesCurrent screenshot_with_slow_fn = sct.grab(sct.monitors[1]) @@ -156,7 +146,7 @@ def test_missing_fast_function_for_monitor_details_retrieval(display: str): assert set(screenshot_with_slow_fn.rgb) == {0} -def test_with_cursor(display: str): +def test_with_cursor(display: str) -> None: with mss.mss(display=display) as sct: assert not hasattr(sct, "xfixes") assert not sct.with_cursor @@ -175,14 +165,15 @@ def test_with_cursor(display: str): @patch("mss.linux._XFIXES", new=None) -def test_with_cursor_but_not_xfixes_extension_found(display: str): +def test_with_cursor_but_not_xfixes_extension_found(display: str) -> None: with mss.mss(display=display, with_cursor=True) as sct: assert not hasattr(sct, "xfixes") assert not sct.with_cursor -def test_with_cursor_failure(display: str): +def test_with_cursor_failure(display: str) -> None: with mss.mss(display=display, with_cursor=True) as sct: + assert isinstance(sct, mss.linux.MSS) # For Mypy with patch.object(sct.xfixes, "XFixesGetCursorImage", return_value=None): with pytest.raises(ScreenShotError): sct.grab(sct.monitors[1]) diff --git a/src/tests/test_implementation.py b/src/tests/test_implementation.py index caeca22..2ec96ac 100644 --- a/src/tests/test_implementation.py +++ b/src/tests/test_implementation.py @@ -1,34 +1,44 @@ +"""This is part of the MSS Python's module. +Source: https://github.com/BoboTiG/python-mss. """ -This is part of the MSS Python's module. -Source: https://github.com/BoboTiG/python-mss -""" +from __future__ import annotations + import os import os.path import platform import sys from datetime import datetime +from typing import TYPE_CHECKING from unittest.mock import Mock, patch -import pytest - +import mss import mss.tools -from mss import mss +import pytest from mss.__main__ import main as entry_point from mss.base import MSSBase from mss.exception import ScreenShotError from mss.screenshot import ScreenShot +if TYPE_CHECKING: + from mss.models import Monitor + +try: + from datetime import UTC +except ImportError: + # Python < 3.11 + from datetime import timezone + + UTC = timezone.utc + class MSS0(MSSBase): """Nothing implemented.""" - pass - class MSS1(MSSBase): """Only `grab()` implemented.""" - def grab(self, monitor): + def grab(self, monitor: Monitor) -> None: # type: ignore[override] pass @@ -36,23 +46,22 @@ class MSS2(MSSBase): """Only `monitor` implemented.""" @property - def monitors(self): + def monitors(self) -> list: return [] @pytest.mark.parametrize("cls", [MSS0, MSS1, MSS2]) -def test_incomplete_class(cls): +def test_incomplete_class(cls: type[MSSBase]) -> None: with pytest.raises(TypeError): cls() -def test_bad_monitor(): - with mss(display=os.getenv("DISPLAY")) as sct: - with pytest.raises(ScreenShotError): - sct.shot(mon=222) +def test_bad_monitor() -> None: + with mss.mss(display=os.getenv("DISPLAY")) as sct, pytest.raises(ScreenShotError): + sct.shot(mon=222) -def test_repr(pixel_ratio): +def test_repr(pixel_ratio: int) -> None: box = {"top": 0, "left": 0, "width": 10, "height": 10} expected_box = { "top": 0, @@ -60,21 +69,21 @@ def test_repr(pixel_ratio): "width": 10 * pixel_ratio, "height": 10 * pixel_ratio, } - with mss(display=os.getenv("DISPLAY")) as sct: + with mss.mss(display=os.getenv("DISPLAY")) as sct: img = sct.grab(box) ref = ScreenShot(bytearray(b"42"), expected_box) assert repr(img) == repr(ref) -def test_factory(monkeypatch): +def test_factory(monkeypatch: pytest.MonkeyPatch) -> None: # Current system - with mss() as sct: + with mss.mss() as sct: assert isinstance(sct, MSSBase) # Unknown monkeypatch.setattr(platform, "system", lambda: "Chuck Norris") with pytest.raises(ScreenShotError) as exc: - mss() + mss.mss() monkeypatch.undo() error = exc.value.args[0] @@ -83,10 +92,10 @@ def test_factory(monkeypatch): @patch.object(sys, "argv", new=[]) # Prevent side effects while testing @pytest.mark.parametrize("with_cursor", [False, True]) -def test_entry_point(with_cursor: bool, capsys): +def test_entry_point(with_cursor: bool, capsys: pytest.CaptureFixture) -> None: def main(*args: str, ret: int = 0) -> None: if with_cursor: - args = args + ("--with-cursor",) + args = (*args, "--with-cursor") assert entry_point(*args) == ret # No arguments @@ -105,8 +114,8 @@ def main(*args: str, ret: int = 0) -> None: assert os.path.isfile("monitor-1.png") os.remove("monitor-1.png") - for opt in zip(["-m 1", "--monitor=1"], ["-q", "--quiet"]): - main(*opt) + for opts in zip(["-m 1", "--monitor=1"], ["-q", "--quiet"]): + main(*opts) captured = capsys.readouterr() assert not captured.out assert os.path.isfile("monitor-1.png") @@ -116,7 +125,7 @@ def main(*args: str, ret: int = 0) -> None: for opt in ("-o", "--out"): main(opt, fmt) captured = capsys.readouterr() - with mss(display=os.getenv("DISPLAY")) as sct: + with mss.mss(display=os.getenv("DISPLAY")) as sct: for mon, (monitor, line) in enumerate(zip(sct.monitors[1:], captured.out.splitlines()), 1): filename = fmt.format(mon=mon, **monitor) assert line.endswith(filename) @@ -126,7 +135,7 @@ def main(*args: str, ret: int = 0) -> None: fmt = "sct_{mon}-{date:%Y-%m-%d}.png" for opt in ("-o", "--out"): main("-m 1", opt, fmt) - filename = fmt.format(mon=1, date=datetime.now()) + filename = fmt.format(mon=1, date=datetime.now(tz=UTC)) captured = capsys.readouterr() assert captured.out.endswith(filename + "\n") assert os.path.isfile(filename) @@ -151,10 +160,10 @@ def main(*args: str, ret: int = 0) -> None: @patch.object(sys, "argv", new=[]) # Prevent side effects while testing @patch("mss.base.MSSBase.monitors", new=[]) @pytest.mark.parametrize("quiet", [False, True]) -def test_entry_point_error(quiet: bool, capsys): +def test_entry_point_error(quiet: bool, capsys: pytest.CaptureFixture) -> None: def main(*args: str) -> int: if quiet: - args = args + ("--quiet",) + args = (*args, "--quiet") return entry_point(*args) if quiet: @@ -167,7 +176,7 @@ def main(*args: str) -> int: main() -def test_entry_point_with_no_argument(capsys): +def test_entry_point_with_no_argument(capsys: pytest.CaptureFixture) -> None: # Make sure to fail if arguments are not handled with patch("mss.factory.mss", new=Mock(side_effect=RuntimeError("Boom!"))): with patch.object(sys, "argv", ["mss", "--help"]): @@ -180,7 +189,7 @@ def test_entry_point_with_no_argument(capsys): assert "usage: mss" in captured.out -def test_grab_with_tuple(pixel_ratio: int): +def test_grab_with_tuple(pixel_ratio: int) -> None: left = 100 top = 100 right = 500 @@ -188,7 +197,7 @@ def test_grab_with_tuple(pixel_ratio: int): width = right - left # 400px width height = lower - top # 400px height - with mss(display=os.getenv("DISPLAY")) as sct: + with mss.mss(display=os.getenv("DISPLAY")) as sct: # PIL like box = (left, top, right, lower) im = sct.grab(box) @@ -202,8 +211,8 @@ def test_grab_with_tuple(pixel_ratio: int): assert im.rgb == im2.rgb -def test_grab_with_tuple_percents(pixel_ratio: int): - with mss(display=os.getenv("DISPLAY")) as sct: +def test_grab_with_tuple_percents(pixel_ratio: int) -> None: + with mss.mss(display=os.getenv("DISPLAY")) as sct: monitor = sct.monitors[1] left = monitor["left"] + monitor["width"] * 5 // 100 # 5% from the left top = monitor["top"] + monitor["height"] * 5 // 100 # 5% from the top @@ -225,22 +234,21 @@ def test_grab_with_tuple_percents(pixel_ratio: int): assert im.rgb == im2.rgb -def test_thread_safety(): +def test_thread_safety() -> None: """Regression test for issue #169.""" import threading import time - def record(check): + def record(check: dict) -> None: """Record for one second.""" - start_time = time.time() while time.time() - start_time < 1: - with mss() as sct: + with mss.mss() as sct: sct.grab(sct.monitors[1]) check[threading.current_thread()] = True - checkpoint = {} + checkpoint: dict = {} t1 = threading.Thread(target=record, args=(checkpoint,)) t2 = threading.Thread(target=record, args=(checkpoint,)) diff --git a/src/tests/test_issue_220.py b/src/tests/test_issue_220.py index 3fefccc..203147e 100644 --- a/src/tests/test_issue_220.py +++ b/src/tests/test_issue_220.py @@ -1,16 +1,14 @@ +"""This is part of the MSS Python's module. +Source: https://github.com/BoboTiG/python-mss. """ -This is part of the MSS Python's module. -Source: https://github.com/BoboTiG/python-mss -""" -import pytest - import mss +import pytest tkinter = pytest.importorskip("tkinter") -@pytest.fixture -def root() -> tkinter.Tk: +@pytest.fixture() +def root() -> tkinter.Tk: # type: ignore[name-defined] try: master = tkinter.Tk() except RuntimeError: @@ -22,13 +20,13 @@ def root() -> tkinter.Tk: master.destroy() -def take_screenshot(): +def take_screenshot() -> None: region = {"top": 370, "left": 1090, "width": 80, "height": 390} with mss.mss() as sct: sct.grab(region) -def create_top_level_win(master: tkinter.Tk): +def create_top_level_win(master: tkinter.Tk) -> None: # type: ignore[name-defined] top_level_win = tkinter.Toplevel(master) take_screenshot_btn = tkinter.Button(top_level_win, text="Take screenshot", command=take_screenshot) @@ -43,7 +41,7 @@ def create_top_level_win(master: tkinter.Tk): master.update() -def test_regression(root: tkinter.Tk, capsys): +def test_regression(root: tkinter.Tk, capsys: pytest.CaptureFixture) -> None: # type: ignore[name-defined] btn = tkinter.Button(root, text="Open TopLevel", command=lambda: create_top_level_win(root)) btn.pack() diff --git a/src/tests/test_leaks.py b/src/tests/test_leaks.py index 7e6a25e..7c0fd0f 100644 --- a/src/tests/test_leaks.py +++ b/src/tests/test_leaks.py @@ -1,25 +1,21 @@ -""" -This is part of the MSS Python's module. -Source: https://github.com/BoboTiG/python-mss +"""This is part of the MSS Python's module. +Source: https://github.com/BoboTiG/python-mss. """ import os import platform from typing import Callable +import mss import pytest -from mss import mss - OS = platform.system().lower() PID = os.getpid() def get_opened_socket() -> int: - """ - GNU/Linux: a way to get the opened sockets count. + """GNU/Linux: a way to get the opened sockets count. It will be used to check X server connections are well closed. """ - import subprocess cmd = f"lsof -U | grep {PID}" @@ -28,62 +24,59 @@ def get_opened_socket() -> int: def get_handles() -> int: - """ - Windows: a way to get the GDI handles count. + """Windows: a way to get the GDI handles count. It will be used to check the handles count is not growing, showing resource leaks. """ - import ctypes - PQI = 0x400 # PROCESS_QUERY_INFORMATION - GR_GDIOBJECTS = 0 - h = ctypes.windll.kernel32.OpenProcess(PQI, 0, PID) + PROCESS_QUERY_INFORMATION = 0x400 # noqa:N806 + GR_GDIOBJECTS = 0 # noqa:N806 + h = ctypes.windll.kernel32.OpenProcess(PROCESS_QUERY_INFORMATION, 0, PID) return ctypes.windll.user32.GetGuiResources(h, GR_GDIOBJECTS) -@pytest.fixture +@pytest.fixture() def monitor_func() -> Callable[[], int]: """OS specific function to check resources in use.""" - return get_opened_socket if OS == "linux" else get_handles -def bound_instance_without_cm(): - # Will always leak for now - sct = mss() +def bound_instance_without_cm() -> None: + # Will always leak + sct = mss.mss() sct.shot() -def bound_instance_without_cm_but_use_close(): - sct = mss() +def bound_instance_without_cm_but_use_close() -> None: + sct = mss.mss() sct.shot() sct.close() # Calling .close() twice should be possible sct.close() -def unbound_instance_without_cm(): - # Will always leak for now - mss().shot() +def unbound_instance_without_cm() -> None: + # Will always leak + mss.mss().shot() -def with_context_manager(): - with mss() as sct: +def with_context_manager() -> None: + with mss.mss() as sct: sct.shot() -def regression_issue_128(): +def regression_issue_128() -> None: """Regression test for issue #128: areas overlap.""" - with mss() as sct: + with mss.mss() as sct: area1 = {"top": 50, "left": 7, "width": 400, "height": 320, "mon": 1} sct.grab(area1) area2 = {"top": 200, "left": 200, "width": 320, "height": 320, "mon": 1} sct.grab(area2) -def regression_issue_135(): +def regression_issue_135() -> None: """Regression test for issue #135: multiple areas.""" - with mss() as sct: + with mss.mss() as sct: bounding_box_notes = {"top": 0, "left": 0, "width": 100, "height": 100} sct.grab(bounding_box_notes) bounding_box_test = {"top": 220, "left": 220, "width": 100, "height": 100} @@ -92,23 +85,21 @@ def regression_issue_135(): sct.grab(bounding_box_score) -def regression_issue_210(): +def regression_issue_210() -> None: """Regression test for issue #210: multiple X servers.""" pyvirtualdisplay = pytest.importorskip("pyvirtualdisplay") - with pyvirtualdisplay.Display(size=(1920, 1080), color_depth=24): - with mss(): - pass + with pyvirtualdisplay.Display(size=(1920, 1080), color_depth=24), mss.mss(): + pass - with pyvirtualdisplay.Display(size=(1920, 1080), color_depth=24): - with mss(): - pass + with pyvirtualdisplay.Display(size=(1920, 1080), color_depth=24), mss.mss(): + pass @pytest.mark.skipif(OS == "darwin", reason="No possible leak on macOS.") @pytest.mark.parametrize( "func", - ( + [ # bound_instance_without_cm, bound_instance_without_cm_but_use_close, # unbound_instance_without_cm, @@ -116,11 +107,10 @@ def regression_issue_210(): regression_issue_128, regression_issue_135, regression_issue_210, - ), + ], ) -def test_resource_leaks(func, monitor_func): +def test_resource_leaks(func: Callable[[], None], monitor_func: Callable[[], int]) -> None: """Check for resource leaks with different use cases.""" - # Warm-up func() diff --git a/src/tests/test_macos.py b/src/tests/test_macos.py index 113b33e..9346581 100644 --- a/src/tests/test_macos.py +++ b/src/tests/test_macos.py @@ -1,60 +1,60 @@ -""" -This is part of the MSS Python's module. -Source: https://github.com/BoboTiG/python-mss +"""This is part of the MSS Python's module. +Source: https://github.com/BoboTiG/python-mss. """ import ctypes.util import platform -import pytest - import mss +import pytest from mss.exception import ScreenShotError if platform.system().lower() != "darwin": pytestmark = pytest.mark.skip +import mss.darwin -def test_repr(): - from mss.darwin import CGPoint, CGRect, CGSize +def test_repr() -> None: # CGPoint - point = CGPoint(2.0, 1.0) - ref = CGPoint() - ref.x = 2.0 - ref.y = 1.0 - assert repr(point) == repr(ref) + point = mss.darwin.CGPoint(2.0, 1.0) + ref1 = mss.darwin.CGPoint() + ref1.x = 2.0 + ref1.y = 1.0 + assert repr(point) == repr(ref1) # CGSize - size = CGSize(2.0, 1.0) - ref = CGSize() - ref.width = 2.0 - ref.height = 1.0 - assert repr(size) == repr(ref) + size = mss.darwin.CGSize(2.0, 1.0) + ref2 = mss.darwin.CGSize() + ref2.width = 2.0 + ref2.height = 1.0 + assert repr(size) == repr(ref2) # CGRect - rect = CGRect(point, size) - ref = CGRect() - ref.origin.x = 2.0 - ref.origin.y = 1.0 - ref.size.width = 2.0 - ref.size.height = 1.0 - assert repr(rect) == repr(ref) + rect = mss.darwin.CGRect(point, size) + ref3 = mss.darwin.CGRect() + ref3.origin.x = 2.0 + ref3.origin.y = 1.0 + ref3.size.width = 2.0 + ref3.size.height = 1.0 + assert repr(rect) == repr(ref3) -def test_implementation(monkeypatch): +def test_implementation(monkeypatch: pytest.MonkeyPatch) -> None: # No `CoreGraphics` library version = float(".".join(platform.mac_ver()[0].split(".")[:2])) if version < 10.16: - monkeypatch.setattr(ctypes.util, "find_library", lambda x: None) + monkeypatch.setattr(ctypes.util, "find_library", lambda _: None) with pytest.raises(ScreenShotError): mss.mss() monkeypatch.undo() with mss.mss() as sct: + assert isinstance(sct, mss.darwin.MSS) # For Mypy + # Test monitor's rotation original = sct.monitors[1] - monkeypatch.setattr(sct.core, "CGDisplayRotation", lambda x: -90.0) + monkeypatch.setattr(sct.core, "CGDisplayRotation", lambda _: -90.0) sct._monitors = [] modified = sct.monitors[1] assert original["width"] == modified["height"] @@ -62,6 +62,6 @@ def test_implementation(monkeypatch): monkeypatch.undo() # Test bad data retrieval - monkeypatch.setattr(sct.core, "CGWindowListCreateImage", lambda *args: None) + monkeypatch.setattr(sct.core, "CGWindowListCreateImage", lambda *_: None) with pytest.raises(ScreenShotError): sct.grab(sct.monitors[1]) diff --git a/src/tests/test_save.py b/src/tests/test_save.py index 6dfbc19..a46a4fb 100644 --- a/src/tests/test_save.py +++ b/src/tests/test_save.py @@ -1,21 +1,27 @@ -""" -This is part of the MSS Python's module. -Source: https://github.com/BoboTiG/python-mss +"""This is part of the MSS Python's module. +Source: https://github.com/BoboTiG/python-mss. """ import os.path from datetime import datetime import pytest - from mss import mss +try: + from datetime import UTC +except ImportError: + # Python < 3.11 + from datetime import timezone + + UTC = timezone.utc + -def test_at_least_2_monitors(): +def test_at_least_2_monitors() -> None: with mss(display=os.getenv("DISPLAY")) as sct: assert list(sct.save(mon=0)) -def test_files_exist(): +def test_files_exist() -> None: with mss(display=os.getenv("DISPLAY")) as sct: for filename in sct.save(): assert os.path.isfile(filename) @@ -26,8 +32,8 @@ def test_files_exist(): assert os.path.isfile("fullscreen.png") -def test_callback(): - def on_exists(fname): +def test_callback() -> None: + def on_exists(fname: str) -> None: if os.path.isfile(fname): new_file = f"{fname}.old" os.rename(fname, new_file) @@ -40,14 +46,14 @@ def on_exists(fname): assert os.path.isfile(filename) -def test_output_format_simple(): +def test_output_format_simple() -> None: with mss(display=os.getenv("DISPLAY")) as sct: filename = sct.shot(mon=1, output="mon-{mon}.png") assert filename == "mon-1.png" assert os.path.isfile(filename) -def test_output_format_positions_and_sizes(): +def test_output_format_positions_and_sizes() -> None: fmt = "sct-{top}x{left}_{width}x{height}.png" with mss(display=os.getenv("DISPLAY")) as sct: filename = sct.shot(mon=1, output=fmt) @@ -55,20 +61,20 @@ def test_output_format_positions_and_sizes(): assert os.path.isfile(filename) -def test_output_format_date_simple(): +def test_output_format_date_simple() -> None: fmt = "sct_{mon}-{date}.png" with mss(display=os.getenv("DISPLAY")) as sct: try: filename = sct.shot(mon=1, output=fmt) assert os.path.isfile(filename) - except IOError: + except OSError: # [Errno 22] invalid mode ('wb') or filename: 'sct_1-2019-01-01 21:20:43.114194.png' pytest.mark.xfail("Default date format contains ':' which is not allowed.") -def test_output_format_date_custom(): +def test_output_format_date_custom() -> None: fmt = "sct_{date:%Y-%m-%d}.png" with mss(display=os.getenv("DISPLAY")) as sct: filename = sct.shot(mon=1, output=fmt) - assert filename == fmt.format(date=datetime.now()) + assert filename == fmt.format(date=datetime.now(tz=UTC)) assert os.path.isfile(filename) diff --git a/src/tests/test_setup.py b/src/tests/test_setup.py index 5bec6fb..1bd5a66 100644 --- a/src/tests/test_setup.py +++ b/src/tests/test_setup.py @@ -1,6 +1,5 @@ -""" -This is part of the MSS Python's module. -Source: https://github.com/BoboTiG/python-mss +"""This is part of the MSS Python's module. +Source: https://github.com/BoboTiG/python-mss. """ import platform import tarfile @@ -8,7 +7,6 @@ from zipfile import ZipFile import pytest - from mss import __version__ if platform.system().lower() != "linux": @@ -22,13 +20,13 @@ CHECK = "twine check --strict".split() -def test_sdist(): +def test_sdist() -> None: output = check_output(SDIST, stderr=STDOUT, text=True) file = f"mss-{__version__}.tar.gz" assert f"Successfully built {file}" in output assert "warning" not in output.lower() - check_call(CHECK + [f"dist/{file}"]) + check_call([*CHECK, f"dist/{file}"]) with tarfile.open(f"dist/{file}", mode="r:gz") as fh: files = sorted(fh.getnames()) @@ -95,13 +93,13 @@ def test_sdist(): ] -def test_wheel(): +def test_wheel() -> None: output = check_output(WHEEL, stderr=STDOUT, text=True) file = f"mss-{__version__}-py3-none-any.whl" assert f"Successfully built {file}" in output assert "warning" not in output.lower() - check_call(CHECK + [f"dist/{file}"]) + check_call([*CHECK, f"dist/{file}"]) with ZipFile(f"dist/{file}") as fh: files = sorted(fh.namelist()) diff --git a/src/tests/test_third_party.py b/src/tests/test_third_party.py index e89afbd..b5443c7 100644 --- a/src/tests/test_third_party.py +++ b/src/tests/test_third_party.py @@ -1,20 +1,18 @@ -""" -This is part of the MSS Python's module. -Source: https://github.com/BoboTiG/python-mss +"""This is part of the MSS Python's module. +Source: https://github.com/BoboTiG/python-mss. """ import itertools import os import os.path import pytest - from mss import mss try: - import numpy + import numpy as np except (ImportError, RuntimeError): # RuntimeError on Python 3.9 (macOS): Polyfit sanity test emitted a warning, ... - numpy = None + np = None try: from PIL import Image @@ -22,16 +20,16 @@ Image = None -@pytest.mark.skipif(numpy is None, reason="Numpy module not available.") -def test_numpy(pixel_ratio): +@pytest.mark.skipif(np is None, reason="Numpy module not available.") +def test_numpy(pixel_ratio: int) -> None: box = {"top": 0, "left": 0, "width": 10, "height": 10} with mss(display=os.getenv("DISPLAY")) as sct: - img = numpy.array(sct.grab(box)) + img = np.array(sct.grab(box)) assert len(img) == 10 * pixel_ratio @pytest.mark.skipif(Image is None, reason="PIL module not available.") -def test_pil(): +def test_pil() -> None: width, height = 16, 16 box = {"top": 0, "left": 0, "width": width, "height": height} with mss(display=os.getenv("DISPLAY")) as sct: @@ -49,7 +47,7 @@ def test_pil(): @pytest.mark.skipif(Image is None, reason="PIL module not available.") -def test_pil_bgra(): +def test_pil_bgra() -> None: width, height = 16, 16 box = {"top": 0, "left": 0, "width": width, "height": height} with mss(display=os.getenv("DISPLAY")) as sct: @@ -67,7 +65,7 @@ def test_pil_bgra(): @pytest.mark.skipif(Image is None, reason="PIL module not available.") -def test_pil_not_16_rounded(): +def test_pil_not_16_rounded() -> None: width, height = 10, 10 box = {"top": 0, "left": 0, "width": width, "height": height} with mss(display=os.getenv("DISPLAY")) as sct: diff --git a/src/tests/test_tools.py b/src/tests/test_tools.py index d939cb1..618f682 100644 --- a/src/tests/test_tools.py +++ b/src/tests/test_tools.py @@ -1,13 +1,11 @@ -""" -This is part of the MSS Python's module. -Source: https://github.com/BoboTiG/python-mss +"""This is part of the MSS Python's module. +Source: https://github.com/BoboTiG/python-mss. """ import hashlib import os.path import zlib import pytest - from mss import mss from mss.tools import to_png @@ -16,13 +14,12 @@ MD5SUM = "055e615b74167c9bdfea16a00539450c" -def test_bad_compression_level(): - with mss(compression_level=42, display=os.getenv("DISPLAY")) as sct: - with pytest.raises(zlib.error): - sct.shot() +def test_bad_compression_level() -> None: + with mss(compression_level=42, display=os.getenv("DISPLAY")) as sct, pytest.raises(zlib.error): + sct.shot() -def test_compression_level(): +def test_compression_level() -> None: data = b"rgb" * WIDTH * HEIGHT output = f"{WIDTH}x{HEIGHT}.png" @@ -34,7 +31,7 @@ def test_compression_level(): @pytest.mark.parametrize( - "level, checksum", + ("level", "checksum"), [ (0, "f37123dbc08ed7406d933af11c42563e"), (1, "7d5dcf2a2224445daf19d6d91cf31cb5"), @@ -48,14 +45,15 @@ def test_compression_level(): (9, "4d88d3f5923b6ef05b62031992294839"), ], ) -def test_compression_levels(level, checksum): +def test_compression_levels(level: int, checksum: str) -> None: data = b"rgb" * WIDTH * HEIGHT raw = to_png(data, (WIDTH, HEIGHT), level=level) + assert isinstance(raw, bytes) md5 = hashlib.md5(raw).hexdigest() assert md5 == checksum -def test_output_file(): +def test_output_file() -> None: data = b"rgb" * WIDTH * HEIGHT output = f"{WIDTH}x{HEIGHT}.png" to_png(data, (WIDTH, HEIGHT), output=output) @@ -65,7 +63,8 @@ def test_output_file(): assert hashlib.md5(png.read()).hexdigest() == MD5SUM -def test_output_raw_bytes(): +def test_output_raw_bytes() -> None: data = b"rgb" * WIDTH * HEIGHT raw = to_png(data, (WIDTH, HEIGHT)) + assert isinstance(raw, bytes) assert hashlib.md5(raw).hexdigest() == MD5SUM diff --git a/src/tests/test_windows.py b/src/tests/test_windows.py index 3f247ca..1227395 100644 --- a/src/tests/test_windows.py +++ b/src/tests/test_windows.py @@ -1,30 +1,36 @@ +"""This is part of the MSS Python's module. +Source: https://github.com/BoboTiG/python-mss. """ -This is part of the MSS Python's module. -Source: https://github.com/BoboTiG/python-mss -""" -import platform -import threading +from __future__ import annotations -import pytest +import threading +from typing import Tuple import mss +import pytest from mss.exception import ScreenShotError -if platform.system().lower() != "windows": +try: + import mss.windows +except ImportError: pytestmark = pytest.mark.skip -def test_implementation(monkeypatch): +def test_implementation(monkeypatch: pytest.MonkeyPatch) -> None: # Test bad data retrieval with mss.mss() as sct: - monkeypatch.setattr(sct.gdi32, "GetDIBits", lambda *args: 0) + assert isinstance(sct, mss.windows.MSS) # For Mypy + + monkeypatch.setattr(sct.gdi32, "GetDIBits", lambda *_: 0) with pytest.raises(ScreenShotError): sct.shot() -def test_region_caching(): +def test_region_caching() -> None: """The region to grab is cached, ensure this is well-done.""" with mss.mss() as sct: + assert isinstance(sct, mss.windows.MSS) # For Mypy + # Grab the area 1 region1 = {"top": 0, "left": 0, "width": 200, "height": 200} sct.grab(region1) @@ -42,11 +48,14 @@ def test_region_caching(): assert bmp2 == id(sct._handles.bmp) -def test_region_not_caching(): +def test_region_not_caching() -> None: """The region to grab is not bad cached previous grab.""" grab1 = mss.mss() grab2 = mss.mss() + assert isinstance(grab1, mss.windows.MSS) # For Mypy + assert isinstance(grab2, mss.windows.MSS) # For Mypy + region1 = {"top": 0, "left": 0, "width": 100, "height": 100} region2 = {"top": 0, "left": 0, "width": 50, "height": 1} grab1.grab(region1) @@ -61,14 +70,15 @@ def test_region_not_caching(): assert bmp1 != bmp2 -def run_child_thread(loops): +def run_child_thread(loops: int) -> None: for _ in range(loops): with mss.mss() as sct: # New sct for every loop sct.grab(sct.monitors[1]) -def test_thread_safety(): +def test_thread_safety() -> None: """Thread safety test for issue #150. + The following code will throw a ScreenShotError exception if thread-safety is not guaranted. """ # Let thread 1 finished ahead of thread 2 @@ -80,14 +90,15 @@ def test_thread_safety(): thread2.join() -def run_child_thread_bbox(loops, bbox): +def run_child_thread_bbox(loops: int, bbox: Tuple[int, int, int, int]) -> None: with mss.mss() as sct: # One sct for all loops for _ in range(loops): sct.grab(bbox) -def test_thread_safety_regions(): - """Thread safety test for different regions +def test_thread_safety_regions() -> None: + """Thread safety test for different regions. + The following code will throw a ScreenShotError exception if thread-safety is not guaranted. """ thread1 = threading.Thread(target=run_child_thread_bbox, args=(100, (0, 0, 100, 100)))