From 60d19df1e46ce161ae25f4cf5e03df77998461c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Sun, 29 Dec 2019 18:04:05 +0100 Subject: [PATCH 001/280] dev: use Black first in pre-commit --- .pre-commit-config.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ed5e59d1..98716e13 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,10 @@ fail_fast: true repos: +- repo: https://github.com/ambv/black + rev: stable + hooks: + - id: black - repo: https://github.com/pre-commit/pre-commit-hooks rev: master hooks: @@ -11,7 +15,3 @@ repos: - id: debug-statements - id: check-ast - id: no-commit-to-branch -- repo: https://github.com/ambv/black - rev: stable - hooks: - - id: black From 54de3d97ba9617ccefdfbd548ba67a0fd469bd3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Sun, 29 Dec 2019 18:04:25 +0100 Subject: [PATCH 002/280] tests: added regression tests for #128 and #135 --- CHANGELOG | 1 + tests/test_leaks.py | 22 ++++++++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index 9d1b7340..2b67528c 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -11,6 +11,7 @@ History: - Windows: use our own instances of GDI32 and User32 DLLs - doc: add project_urls to setup.cfg - doc: add an example using the multiprocessing module (closes #82) + - tests: added regression tests for #128 and #135 4.0.2 2019/02/23 - new contributor: foone diff --git a/tests/test_leaks.py b/tests/test_leaks.py index bd4bf9bd..fbfcd9f7 100644 --- a/tests/test_leaks.py +++ b/tests/test_leaks.py @@ -80,6 +80,26 @@ def with_context_manager(): sct.shot() +def regression_issue_128(): + """Regression test for issue #128: areas overlap.""" + area1 = {"top": 50, "left": 7, "width": 400, "height": 320, "mon": 1} + area2 = {"top": 200, "left": 200, "width": 320, "height": 320, "mon": 1} + with mss() as sct: + sct.grab(area1) + sct.grab(area2) + + +def regression_issue_135(): + """Regression test for issue #135: multiple areas.""" + bounding_box_notes = {"top": 0, "left": 0, "width": 100, "height": 100} + bounding_box_score = {"top": 110, "left": 110, "width": 100, "height": 100} + bounding_box_test = {"top": 220, "left": 220, "width": 100, "height": 100} + with mss() as sct: + sct.grab(bounding_box_notes) + sct.grab(bounding_box_test) + sct.grab(bounding_box_score) + + @pytest.mark.skipif(OS == "darwin", reason="No possible leak on macOS.") @pytest.mark.parametrize( "func", @@ -88,6 +108,8 @@ def with_context_manager(): bound_instance_without_cm_but_use_close, unbound_instance_without_cm, with_context_manager, + regression_issue_128, + regression_issue_135, ), ) def test_resource_leaks(func, monitor_func): From 45ac9024fbbe511dd751fb67672172fc7fa4fb4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Tue, 31 Dec 2019 18:12:20 +0100 Subject: [PATCH 003/280] tests: move tests files into the package --- CHANGELOG | 1 + {tests => mss/tests}/bench_bgra2rgb.py | 0 {tests => mss/tests}/bench_general.py | 0 {tests => mss/tests}/conftest.py | 4 +++- {tests => mss/tests}/res/monitor-1024x768.raw | Bin {tests => mss/tests}/test_bgra_to_rgb.py | 0 {tests => mss/tests}/test_cls_image.py | 0 {tests => mss/tests}/test_find_monitors.py | 0 {tests => mss/tests}/test_get_pixels.py | 0 {tests => mss/tests}/test_gnu_linux.py | 0 {tests => mss/tests}/test_implementation.py | 0 {tests => mss/tests}/test_leaks.py | 0 {tests => mss/tests}/test_macos.py | 0 {tests => mss/tests}/test_save.py | 0 {tests => mss/tests}/test_third_party.py | 0 {tests => mss/tests}/test_tools.py | 0 {tests => mss/tests}/test_windows.py | 0 setup.cfg | 4 +++- tox.ini | 4 ++-- 19 files changed, 9 insertions(+), 4 deletions(-) rename {tests => mss/tests}/bench_bgra2rgb.py (100%) rename {tests => mss/tests}/bench_general.py (100%) rename {tests => mss/tests}/conftest.py (87%) rename {tests => mss/tests}/res/monitor-1024x768.raw (100%) rename {tests => mss/tests}/test_bgra_to_rgb.py (100%) rename {tests => mss/tests}/test_cls_image.py (100%) rename {tests => mss/tests}/test_find_monitors.py (100%) rename {tests => mss/tests}/test_get_pixels.py (100%) rename {tests => mss/tests}/test_gnu_linux.py (100%) rename {tests => mss/tests}/test_implementation.py (100%) rename {tests => mss/tests}/test_leaks.py (100%) rename {tests => mss/tests}/test_macos.py (100%) rename {tests => mss/tests}/test_save.py (100%) rename {tests => mss/tests}/test_third_party.py (100%) rename {tests => mss/tests}/test_tools.py (100%) rename {tests => mss/tests}/test_windows.py (100%) diff --git a/CHANGELOG b/CHANGELOG index 2b67528c..9d9d6053 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -12,6 +12,7 @@ History: - doc: add project_urls to setup.cfg - doc: add an example using the multiprocessing module (closes #82) - tests: added regression tests for #128 and #135 + - tests: move tests files into the package 4.0.2 2019/02/23 - new contributor: foone diff --git a/tests/bench_bgra2rgb.py b/mss/tests/bench_bgra2rgb.py similarity index 100% rename from tests/bench_bgra2rgb.py rename to mss/tests/bench_bgra2rgb.py diff --git a/tests/bench_general.py b/mss/tests/bench_general.py similarity index 100% rename from tests/bench_general.py rename to mss/tests/bench_general.py diff --git a/tests/conftest.py b/mss/tests/conftest.py similarity index 87% rename from tests/conftest.py rename to mss/tests/conftest.py index 13bfba07..0d8eccca 100644 --- a/tests/conftest.py +++ b/mss/tests/conftest.py @@ -43,5 +43,7 @@ def is_travis(): @pytest.fixture(scope="session") def raw(): - with open("tests/res/monitor-1024x768.raw", "rb") as f: + here = os.path.dirname(__file__) + file = os.path.join(here, "res", "monitor-1024x768.raw") + with open(file, "rb") as f: yield f.read() diff --git a/tests/res/monitor-1024x768.raw b/mss/tests/res/monitor-1024x768.raw similarity index 100% rename from tests/res/monitor-1024x768.raw rename to mss/tests/res/monitor-1024x768.raw diff --git a/tests/test_bgra_to_rgb.py b/mss/tests/test_bgra_to_rgb.py similarity index 100% rename from tests/test_bgra_to_rgb.py rename to mss/tests/test_bgra_to_rgb.py diff --git a/tests/test_cls_image.py b/mss/tests/test_cls_image.py similarity index 100% rename from tests/test_cls_image.py rename to mss/tests/test_cls_image.py diff --git a/tests/test_find_monitors.py b/mss/tests/test_find_monitors.py similarity index 100% rename from tests/test_find_monitors.py rename to mss/tests/test_find_monitors.py diff --git a/tests/test_get_pixels.py b/mss/tests/test_get_pixels.py similarity index 100% rename from tests/test_get_pixels.py rename to mss/tests/test_get_pixels.py diff --git a/tests/test_gnu_linux.py b/mss/tests/test_gnu_linux.py similarity index 100% rename from tests/test_gnu_linux.py rename to mss/tests/test_gnu_linux.py diff --git a/tests/test_implementation.py b/mss/tests/test_implementation.py similarity index 100% rename from tests/test_implementation.py rename to mss/tests/test_implementation.py diff --git a/tests/test_leaks.py b/mss/tests/test_leaks.py similarity index 100% rename from tests/test_leaks.py rename to mss/tests/test_leaks.py diff --git a/tests/test_macos.py b/mss/tests/test_macos.py similarity index 100% rename from tests/test_macos.py rename to mss/tests/test_macos.py diff --git a/tests/test_save.py b/mss/tests/test_save.py similarity index 100% rename from tests/test_save.py rename to mss/tests/test_save.py diff --git a/tests/test_third_party.py b/mss/tests/test_third_party.py similarity index 100% rename from tests/test_third_party.py rename to mss/tests/test_third_party.py diff --git a/tests/test_tools.py b/mss/tests/test_tools.py similarity index 100% rename from tests/test_tools.py rename to mss/tests/test_tools.py diff --git a/tests/test_windows.py b/mss/tests/test_windows.py similarity index 100% rename from tests/test_windows.py rename to mss/tests/test_windows.py diff --git a/setup.cfg b/setup.cfg index d65bc01e..49748a12 100644 --- a/setup.cfg +++ b/setup.cfg @@ -31,7 +31,9 @@ classifiers = [options] zip-safe = False include_package_data = True -packages = mss +packages = + mss + mss.tests python_requires = >=3.5 [options.entry_points] diff --git a/tox.ini b/tox.ini index cfa330d9..0340e69d 100644 --- a/tox.ini +++ b/tox.ini @@ -23,7 +23,7 @@ deps = flake8 pylint commands = - python -m flake8 docs mss tests + python -m flake8 docs mss python -m pylint mss [testenv:types] @@ -32,7 +32,7 @@ deps = mypy commands = # "--platform win32" to not fail on ctypes.windll (it does not affect the overall check on other OSes) - python -m mypy --platform win32 --ignore-missing-imports mss tests docs/source/examples + python -m mypy --platform win32 --ignore-missing-imports mss docs/source/examples [testenv:docs] description = Build the documentation From 04f9530a793b78637af539268dbd0e73ed5212b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Tue, 31 Dec 2019 18:38:08 +0100 Subject: [PATCH 004/280] Release 5.0.0 --- CHANGELOG | 3 ++- LICENSE | 2 +- docs/source/conf.py | 2 +- mss/__init__.py | 2 +- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 9d9d6053..f1c88ab6 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -2,7 +2,7 @@ History: -5.0.0 2019/xx/xx +5.0.0 2019/12/31 - removed support for Python 2.7 - MSS: improve type annotations and add CI check - MSS: use __slots__ for better performances @@ -13,6 +13,7 @@ History: - doc: add an example using the multiprocessing module (closes #82) - tests: added regression tests for #128 and #135 - tests: move tests files into the package + - :heart: contributors: @hugovk, @foone, @SergeyKalutsky 4.0.2 2019/02/23 - new contributor: foone diff --git a/LICENSE b/LICENSE index f6d3e2a1..e2acbe65 100644 --- a/LICENSE +++ b/LICENSE @@ -1,5 +1,5 @@ MIT License -Copyright (c) 2016-2019, Mickaël 'Tiger-222' Schoentgen +Copyright (c) 2016-2020, Mickaël 'Tiger-222' Schoentgen Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: diff --git a/docs/source/conf.py b/docs/source/conf.py index 001aeb4d..39c2d07e 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -19,7 +19,7 @@ # General information about the project. project = "Python MSS" -copyright = "2013-2019, Mickaël 'Tiger-222' Schoentgen & contributors" +copyright = "2013-2020, Mickaël 'Tiger-222' Schoentgen & contributors" author = "Tiger-222" # The version info for the project you're documenting, acts as replacement for diff --git a/mss/__init__.py b/mss/__init__.py index de457c1a..94b3740d 100644 --- a/mss/__init__.py +++ b/mss/__init__.py @@ -15,7 +15,7 @@ __version__ = "5.0.0" __author__ = "Mickaël 'Tiger-222' Schoentgen" __copyright__ = """ - Copyright (c) 2013-2019, Mickaël 'Tiger-222' Schoentgen + Copyright (c) 2013-2020, Mickaël 'Tiger-222' Schoentgen Permission to use, copy, modify, and distribute this software and its documentation for any purpose and without fee or royalty is hereby From 2a67be5b56b4c178326e8085c43a243808ca3165 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Tue, 31 Dec 2019 18:41:46 +0100 Subject: [PATCH 005/280] Bump version to 5.0.1 --- docs/source/conf.py | 2 +- mss/__init__.py | 2 +- setup.cfg | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 39c2d07e..ef8f5891 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -27,7 +27,7 @@ # built documents. # # The short X.Y version. -version = "5.0.0" +version = "5.0.1" # The full version, including alpha/beta/rc tags. release = "latest" diff --git a/mss/__init__.py b/mss/__init__.py index 94b3740d..da96747a 100644 --- a/mss/__init__.py +++ b/mss/__init__.py @@ -12,7 +12,7 @@ from .exception import ScreenShotError from .factory import mss -__version__ = "5.0.0" +__version__ = "5.0.1" __author__ = "Mickaël 'Tiger-222' Schoentgen" __copyright__ = """ Copyright (c) 2013-2020, Mickaël 'Tiger-222' Schoentgen diff --git a/setup.cfg b/setup.cfg index 49748a12..9403a756 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = mss -version = 5.0.0 +version = 5.0.1 author = Mickaël 'Tiger-222' Schoentgen author-email = contact@tiger-222.fr description = An ultra fast cross-platform multiple screenshots module in pure python using ctypes. From 5e5f3eecc87660b73d11c4c964571f1515c09531 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Wed, 1 Jan 2020 17:24:16 +0100 Subject: [PATCH 006/280] Windows: fix region caching that resulted in bad captures On Windows, the grabbed region is cached to prevent useless resources allocations and speed-up further calls to `.grab()`. But the check to decide if the cached region needs an update was comparing sizes only. And using same region sizes with different region positions between 2 calls to `.grab()` were resulting in bad screenshots. --- CHANGELOG | 3 +++ mss/tests/test_windows.py | 28 ++++++++++++++++++++++++++++ mss/windows.py | 8 ++++---- 3 files changed, 35 insertions(+), 4 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index f1c88ab6..a33b9cde 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -2,6 +2,9 @@ History: +5.0.1 2020/xx/xx + - Windows: fix region caching that resulted in bad captures + 5.0.0 2019/12/31 - removed support for Python 2.7 - MSS: improve type annotations and add CI check diff --git a/mss/tests/test_windows.py b/mss/tests/test_windows.py index 31904336..859a2311 100644 --- a/mss/tests/test_windows.py +++ b/mss/tests/test_windows.py @@ -20,3 +20,31 @@ def test_implementation(monkeypatch): monkeypatch.setattr(sct.gdi32, "GetDIBits", lambda *args: 0) with pytest.raises(ScreenShotError): sct.shot() + + +def test_region_caching(): + """The region to grab is cached, ensure this is well-done.""" + from mss.windows import MSS + + # Same sizes but different positions + region1 = {"top": 0, "left": 0, "width": 200, "height": 200} + region2 = {"top": 200, "left": 200, "width": 200, "height": 200} + + with mss.mss() as sct: + # Reset the current BMP + if MSS.bmp: + sct.gdi32.DeleteObject(MSS.bmp) + MSS.bmp = None + + # Grab the area 1 + sct.grab(region1) + bmp1 = MSS.bmp + + # Grab the area 2, the cached BMP is updated + sct.grab(region2) + bmp2 = MSS.bmp + assert bmp1 is not bmp2 + + # Grab the area 2 again, the cached BMP is used + sct.grab(region2) + assert bmp2 is MSS.bmp diff --git a/mss/windows.py b/mss/windows.py index 7ce24240..6b43b4d9 100644 --- a/mss/windows.py +++ b/mss/windows.py @@ -26,7 +26,7 @@ from .exception import ScreenShotError if TYPE_CHECKING: - from typing import Any # noqa + from typing import Any, Dict # noqa from .models import Monitor, Monitors # noqa from .screenshot import ScreenShot # noqa @@ -90,7 +90,7 @@ def __init__(self, **_): self._set_cfunctions() self._set_dpi_awareness() - self._bbox = {"height": 0, "width": 0} + self._bbox = {} # type: Dict[str, int] self._data = ctypes.create_string_buffer(0) # type: ctypes.Array[ctypes.c_char] if not MSS.srcdc or not MSS.memdc: @@ -263,8 +263,7 @@ def grab(self, monitor): srcdc, memdc = MSS.srcdc, MSS.memdc width, height = monitor["width"], monitor["height"] - if (self._bbox["height"], self._bbox["width"]) != (height, width): - self._bbox = monitor + if self._bbox != monitor: self._bmi.bmiHeader.biWidth = width self._bmi.bmiHeader.biHeight = -height # Why minus? [1] self._data = ctypes.create_string_buffer(width * height * 4) # [2] @@ -272,6 +271,7 @@ def grab(self, monitor): self.gdi32.DeleteObject(MSS.bmp) MSS.bmp = self.gdi32.CreateCompatibleBitmap(srcdc, width, height) self.gdi32.SelectObject(memdc, MSS.bmp) + self._bbox = monitor self.gdi32.BitBlt( memdc, From 2d24115320534460c9f5f510c429dd36f838d457 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Sat, 4 Jan 2020 11:35:31 +0100 Subject: [PATCH 007/280] Revert "Windows: fix region caching that resulted in bad captures" This reverts commit 5e5f3eecc87660b73d11c4c964571f1515c09531. The patch was indeed a bad idea and fixed nothing ... I reverted it but kept the test. --- CHANGELOG | 3 --- mss/tests/test_windows.py | 4 ++-- mss/windows.py | 8 ++++---- 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index a33b9cde..f1c88ab6 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -2,9 +2,6 @@ History: -5.0.1 2020/xx/xx - - Windows: fix region caching that resulted in bad captures - 5.0.0 2019/12/31 - removed support for Python 2.7 - MSS: improve type annotations and add CI check diff --git a/mss/tests/test_windows.py b/mss/tests/test_windows.py index 859a2311..5b352294 100644 --- a/mss/tests/test_windows.py +++ b/mss/tests/test_windows.py @@ -40,10 +40,10 @@ def test_region_caching(): sct.grab(region1) bmp1 = MSS.bmp - # Grab the area 2, the cached BMP is updated + # Grab the area 2, the cached BMP is used sct.grab(region2) bmp2 = MSS.bmp - assert bmp1 is not bmp2 + assert bmp1 is bmp2 # Grab the area 2 again, the cached BMP is used sct.grab(region2) diff --git a/mss/windows.py b/mss/windows.py index 6b43b4d9..7ce24240 100644 --- a/mss/windows.py +++ b/mss/windows.py @@ -26,7 +26,7 @@ from .exception import ScreenShotError if TYPE_CHECKING: - from typing import Any, Dict # noqa + from typing import Any # noqa from .models import Monitor, Monitors # noqa from .screenshot import ScreenShot # noqa @@ -90,7 +90,7 @@ def __init__(self, **_): self._set_cfunctions() self._set_dpi_awareness() - self._bbox = {} # type: Dict[str, int] + self._bbox = {"height": 0, "width": 0} self._data = ctypes.create_string_buffer(0) # type: ctypes.Array[ctypes.c_char] if not MSS.srcdc or not MSS.memdc: @@ -263,7 +263,8 @@ def grab(self, monitor): srcdc, memdc = MSS.srcdc, MSS.memdc width, height = monitor["width"], monitor["height"] - if self._bbox != monitor: + if (self._bbox["height"], self._bbox["width"]) != (height, width): + self._bbox = monitor self._bmi.bmiHeader.biWidth = width self._bmi.bmiHeader.biHeight = -height # Why minus? [1] self._data = ctypes.create_string_buffer(width * height * 4) # [2] @@ -271,7 +272,6 @@ def grab(self, monitor): self.gdi32.DeleteObject(MSS.bmp) MSS.bmp = self.gdi32.CreateCompatibleBitmap(srcdc, width, height) self.gdi32.SelectObject(memdc, MSS.bmp) - self._bbox = monitor self.gdi32.BitBlt( memdc, From 627a97f734ce3609b81799a8943e26c38832a763 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Sat, 4 Jan 2020 11:40:42 +0100 Subject: [PATCH 008/280] dev: produce wheels for Python 3 only For instance, the version 5.0.0 shipped a wheel letting users know that Python 2 and 3 are supported. This is not true for Python 2. The patch will changes the wheel name: `mss-VERSION-py2.py3-none-any.whl` -> `mss-VERSION-py3-none-any.whl` Also fixed missing test data. --- .gitignore | 1 - CHANGELOG | 3 +++ MANIFEST.in | 3 +++ mss/tests/test_setup.py | 17 +++++++++++++++++ setup.cfg | 7 +------ tox.ini | 1 + 6 files changed, 25 insertions(+), 7 deletions(-) create mode 100644 MANIFEST.in create mode 100644 mss/tests/test_setup.py diff --git a/.gitignore b/.gitignore index ffe6e427..080d5f82 100644 --- a/.gitignore +++ b/.gitignore @@ -3,7 +3,6 @@ build/ dist/ *.egg-info/ .idea/ -MANIFEST* .DS_Store *.orig *.jpg diff --git a/CHANGELOG b/CHANGELOG index f1c88ab6..cb06db8e 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -2,6 +2,9 @@ History: +5.0.1 2020/xx/xx + - produce wheels for Python 3 only + 5.0.0 2019/12/31 - removed support for Python 2.7 - MSS: improve type annotations and add CI check diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 00000000..b8cdc5d1 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,3 @@ +# Include tests files and data +include mss/tests/*.py +recursive-include mss/tests/res * diff --git a/mss/tests/test_setup.py b/mss/tests/test_setup.py new file mode 100644 index 00000000..e1a213c4 --- /dev/null +++ b/mss/tests/test_setup.py @@ -0,0 +1,17 @@ +""" +This is part of the MSS Python's module. +Source: https://github.com/BoboTiG/python-mss +""" + +from subprocess import check_output + +from mss import __version__ + +CMD = "python setup.py sdist bdist_wheel".split() + + +def test_wheel_python_3_only(): + """Ensure the produced wheel is Python 3 only.""" + output = str(check_output(CMD)) + text = "mss-{}-py3-none-any.whl".format(__version__) + assert text in output diff --git a/setup.cfg b/setup.cfg index 9403a756..2a123949 100644 --- a/setup.cfg +++ b/setup.cfg @@ -31,18 +31,13 @@ classifiers = [options] zip-safe = False include_package_data = True -packages = - mss - mss.tests +packages = mss python_requires = >=3.5 [options.entry_points] console_scripts = mss = mss.__main__:main -[bdist_wheel] -universal = 1 - [flake8] ignore = # E203 whitespace before ':', but E203 is not PEP 8 compliant diff --git a/tox.ini b/tox.ini index 0340e69d..1b402bb0 100644 --- a/tox.ini +++ b/tox.ini @@ -14,6 +14,7 @@ deps = # Must pin that version to support PyPy3 numpy==1.15.4 pillow + wheel commands = python -m pytest {posargs} From cecafb24c4554452cf4327808df602dd2d810bee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Sat, 4 Jan 2020 14:01:17 +0100 Subject: [PATCH 009/280] tools: force write of file when saving a PNG file --- CHANGELOG | 1 + mss/tools.py | 27 ++++++++++++++++++--------- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index cb06db8e..f1cc9406 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -4,6 +4,7 @@ History: 5.0.1 2020/xx/xx - produce wheels for Python 3 only + - tools: force write of file when saving a PNG file 5.0.0 2019/12/31 - removed support for Python 2.7 diff --git a/mss/tools.py b/mss/tools.py index 4b0c040b..aa79b2bf 100644 --- a/mss/tools.py +++ b/mss/tools.py @@ -3,6 +3,7 @@ Source: https://github.com/BoboTiG/python-mss """ +import os import struct import zlib from typing import TYPE_CHECKING @@ -22,31 +23,35 @@ def to_png(data, size, level=6, output=None): :param int level: PNG compression level. :param str output: Output file name. """ + # pylint: disable=too-many-locals + + pack = struct.pack + crc32 = zlib.crc32 width, height = size line = width * 3 - png_filter = struct.pack(">B", 0) + png_filter = pack(">B", 0) scanlines = b"".join( [png_filter + data[y * line : y * line + line] for y in range(height)] ) - magic = struct.pack(">8B", 137, 80, 78, 71, 13, 10, 26, 10) + magic = pack(">8B", 137, 80, 78, 71, 13, 10, 26, 10) # Header: size, marker, data, CRC32 ihdr = [b"", b"IHDR", b"", b""] - ihdr[2] = struct.pack(">2I5B", width, height, 8, 2, 0, 0, 0) - ihdr[3] = struct.pack(">I", zlib.crc32(b"".join(ihdr[1:3])) & 0xFFFFFFFF) - ihdr[0] = struct.pack(">I", len(ihdr[2])) + ihdr[2] = pack(">2I5B", width, height, 8, 2, 0, 0, 0) + ihdr[3] = pack(">I", crc32(b"".join(ihdr[1:3])) & 0xFFFFFFFF) + ihdr[0] = pack(">I", len(ihdr[2])) # Data: size, marker, data, CRC32 idat = [b"", b"IDAT", zlib.compress(scanlines, level), b""] - idat[3] = struct.pack(">I", zlib.crc32(b"".join(idat[1:3])) & 0xFFFFFFFF) - idat[0] = struct.pack(">I", len(idat[2])) + idat[3] = pack(">I", crc32(b"".join(idat[1:3])) & 0xFFFFFFFF) + idat[0] = pack(">I", len(idat[2])) # Footer: size, marker, None, CRC32 iend = [b"", b"IEND", b"", b""] - iend[3] = struct.pack(">I", zlib.crc32(iend[1]) & 0xFFFFFFFF) - iend[0] = struct.pack(">I", len(iend[2])) + iend[3] = pack(">I", crc32(iend[1]) & 0xFFFFFFFF) + iend[0] = pack(">I", len(iend[2])) if not output: # Returns raw bytes of the whole PNG data @@ -58,4 +63,8 @@ def to_png(data, size, level=6, output=None): fileh.write(b"".join(idat)) fileh.write(b"".join(iend)) + # Force write of file to disk + fileh.flush() + os.fsync(fileh.fileno()) + return None From a3056eab5748749082e20b00e7ffa07f2e9ef6be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Sat, 4 Jan 2020 14:44:39 +0100 Subject: [PATCH 010/280] tests: fix tests on macOS with Retina display --- CHANGELOG | 1 + mss/tests/conftest.py | 14 +++++++++++++- mss/tests/test_get_pixels.py | 12 ++++++------ mss/tests/test_implementation.py | 18 ++++++++++++------ mss/tests/test_third_party.py | 4 ++-- 5 files changed, 34 insertions(+), 15 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index f1cc9406..cdf64f05 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -5,6 +5,7 @@ History: 5.0.1 2020/xx/xx - produce wheels for Python 3 only - tools: force write of file when saving a PNG file + - tests: fix tests on macOS with Retina display 5.0.0 2019/12/31 - removed support for Python 2.7 diff --git a/mss/tests/conftest.py b/mss/tests/conftest.py index 0d8eccca..f32869e5 100644 --- a/mss/tests/conftest.py +++ b/mss/tests/conftest.py @@ -6,8 +6,8 @@ import glob import os -import mss import pytest +import mss def purge_files(): @@ -47,3 +47,15 @@ def raw(): file = os.path.join(here, "res", "monitor-1024x768.raw") with open(file, "rb") as f: yield f.read() + + +@pytest.fixture(scope="module") +def pixel_ratio(sct): + """Get the pixel, used to adapt test checks.""" + # Grab a 1x1 screenshot + region = {"top": 0, "left": 0, "width": 1, "height": 1} + + # On macOS with Retina display,the width will be 2 instead of 1 + pixel_size = sct.grab(region).size[0] + + return pixel_size diff --git a/mss/tests/test_get_pixels.py b/mss/tests/test_get_pixels.py index 4672a20e..e340e5cf 100644 --- a/mss/tests/test_get_pixels.py +++ b/mss/tests/test_get_pixels.py @@ -16,7 +16,7 @@ def test_grab_monitor(sct): assert isinstance(image.rgb, bytes) -def test_grab_part_of_screen(sct): +def test_grab_part_of_screen(sct, pixel_ratio): monitor = {"top": 160, "left": 160, "width": 160, "height": 160} image = sct.grab(monitor) assert isinstance(image, ScreenShot) @@ -24,11 +24,11 @@ def test_grab_part_of_screen(sct): assert isinstance(image.rgb, bytes) assert image.top == 160 assert image.left == 160 - assert image.width == 160 - assert image.height == 160 + assert image.width == 160 * pixel_ratio + assert image.height == 160 * pixel_ratio -def test_grab_part_of_screen_rounded(sct): +def test_grab_part_of_screen_rounded(sct, pixel_ratio): monitor = {"top": 160, "left": 160, "width": 161, "height": 159} image = sct.grab(monitor) assert isinstance(image, ScreenShot) @@ -36,8 +36,8 @@ def test_grab_part_of_screen_rounded(sct): assert isinstance(image.rgb, bytes) assert image.top == 160 assert image.left == 160 - assert image.width == 161 - assert image.height == 159 + assert image.width == 161 * pixel_ratio + assert image.height == 159 * pixel_ratio def test_grab_individual_pixels(sct): diff --git a/mss/tests/test_implementation.py b/mss/tests/test_implementation.py index 11bfdf49..d43f5fa1 100644 --- a/mss/tests/test_implementation.py +++ b/mss/tests/test_implementation.py @@ -63,10 +63,16 @@ def test_incomplete_class(): sct.grab(sct.shot(mon=222)) -def test_repr(sct): +def test_repr(sct, pixel_ratio): box = {"top": 0, "left": 0, "width": 10, "height": 10} + expected_box = { + "top": 0, + "left": 0, + "width": 10 * pixel_ratio, + "height": 10 * pixel_ratio, + } img = sct.grab(box) - ref = ScreenShot(bytearray(b"42"), box) + ref = ScreenShot(bytearray(b"42"), expected_box) assert repr(img) == repr(ref) @@ -140,7 +146,7 @@ def test_entry_point(capsys, sct): assert out == "Coordinates syntax: top, left, width, height\n" -def test_grab_with_tuple(sct): +def test_grab_with_tuple(sct, pixel_ratio): left = 100 top = 100 right = 500 @@ -151,7 +157,7 @@ def test_grab_with_tuple(sct): # PIL like box = (left, top, right, lower) im = sct.grab(box) - assert im.size == (width, height) + assert im.size == (width * pixel_ratio, height * pixel_ratio) # MSS like box2 = {"left": left, "top": top, "width": width, "height": height} @@ -161,7 +167,7 @@ def test_grab_with_tuple(sct): assert im.rgb == im2.rgb -def test_grab_with_tuple_percents(sct): +def test_grab_with_tuple_percents(sct, pixel_ratio): 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 @@ -173,7 +179,7 @@ def test_grab_with_tuple_percents(sct): # PIL like box = (left, top, right, lower) im = sct.grab(box) - assert im.size == (width, height) + assert im.size == (width * pixel_ratio, height * pixel_ratio) # MSS like box2 = {"left": left, "top": top, "width": width, "height": height} diff --git a/mss/tests/test_third_party.py b/mss/tests/test_third_party.py index caca9814..691dffc1 100644 --- a/mss/tests/test_third_party.py +++ b/mss/tests/test_third_party.py @@ -21,10 +21,10 @@ @pytest.mark.skipif(numpy is None, reason="Numpy module not available.") -def test_numpy(sct): +def test_numpy(sct, pixel_ratio): box = {"top": 0, "left": 0, "width": 10, "height": 10} img = numpy.array(sct.grab(box)) - assert len(img) == 10 + assert len(img) == 10 * pixel_ratio @pytest.mark.skipif(Image is None, reason="PIL module not available.") From 9d01cb0fdfb939ab7b83cf6eb1bce09b27e27dc8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Sat, 4 Jan 2020 15:16:18 +0100 Subject: [PATCH 011/280] MSS: renamed again MSSMixin to MSSBase, now derived from abc.ABCMeta - `MSSMixin` was really a base class, not a mixin. - Correctly implemented abstract attributes. --- CHANGELOG | 1 + CHANGES.rst | 13 +++++++- docs/source/api.rst | 10 +++--- mss/base.py | 9 +++--- mss/darwin.py | 6 ++-- mss/factory.py | 4 +-- mss/linux.py | 4 +-- mss/tests/test_gnu_linux.py | 4 +-- mss/tests/test_implementation.py | 53 ++++++++++---------------------- mss/windows.py | 4 +-- 10 files changed, 52 insertions(+), 56 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index cdf64f05..46adc043 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -4,6 +4,7 @@ History: 5.0.1 2020/xx/xx - produce wheels for Python 3 only + - MSS: renamed again MSSMixin to MSSBase, now derived from abc.ABCMeta - tools: force write of file when saving a PNG file - tests: fix tests on macOS with Retina display diff --git a/CHANGES.rst b/CHANGES.rst index 2f91f597..86b5540d 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,4 +1,15 @@ -5.0.0 (2019-xx-xx) +5.0.1 (2020-xx-xx) +================== + +base.py +------- +- Renamed back ``MSSMixin`` class to ``MSSBase`` +- ``MSSBase`` is now derived from ``abc.ABCMeta`` +- ``MSSBase.monitor`` is now an abstract property +- ``MSSBase.grab()`` is now an abstract method + + +5.0.0 (2019-12-31) ================== darwin.py diff --git a/docs/source/api.rst b/docs/source/api.rst index 6480938d..a6ac98c5 100644 --- a/docs/source/api.rst +++ b/docs/source/api.rst @@ -59,7 +59,7 @@ GNU/Linux :rtype: :class:`~mss.base.ScreenShot` :raises ScreenShotError: When color depth is not 32 (rare). - See :meth:`~mss.base.MSSMixin.grab()` for details. + See :meth:`~mss.base.MSSBase.grab()` for details. .. function:: error_handler(display, event) @@ -82,7 +82,7 @@ Methods .. module:: mss.base -.. class:: MSSMixin +.. class:: MSSBase The parent's class for every OS implementation. @@ -96,9 +96,9 @@ Methods :param dict monitor: region's coordinates. :rtype: :class:`ScreenShot` - :raises NotImplementedError: Subclasses need to implement this. Retrieve screen pixels for a given *region*. + Subclasses need to implement this. .. note:: @@ -194,7 +194,7 @@ Methods Properties ========== -.. class:: mss.base.MSSMixin +.. class:: mss.base.MSSBase .. attribute:: monitors @@ -215,6 +215,8 @@ Properties - ``width``: the width - ``height``: the height + Subclasses need to implement this. + :rtype: list[dict[str, int]] .. class:: mss.base.ScreenShot diff --git a/mss/base.py b/mss/base.py index 4cbe4cf5..59c4c7f5 100644 --- a/mss/base.py +++ b/mss/base.py @@ -3,6 +3,7 @@ Source: https://github.com/BoboTiG/python-mss """ +from abc import ABCMeta, abstractmethod from datetime import datetime from typing import TYPE_CHECKING @@ -16,7 +17,7 @@ from .models import Monitor, Monitors # noqa -class MSSMixin: +class MSSBase(metaclass=ABCMeta): """ This class will be overloaded by a system specific one. """ __slots__ = {"_monitors", "cls_image", "compression_level"} @@ -27,7 +28,7 @@ def __init__(self): self._monitors = [] # type: Monitors def __enter__(self): - # type: () -> MSSMixin + # type: () -> MSSBase """ For the cool call `with MSS() as mss:`. """ return self @@ -41,6 +42,7 @@ def close(self): # type: () -> None """ Clean-up. """ + @abstractmethod def grab(self, monitor): # type: (Monitor) -> ScreenShot """ @@ -51,9 +53,8 @@ def grab(self, monitor): :return :class:`ScreenShot `. """ - raise NotImplementedError("Subclasses need to implement this!") - @property + @abstractmethod def monitors(self): # type: () -> Monitors """ diff --git a/mss/darwin.py b/mss/darwin.py index cd4b9d49..3d1b8f7b 100644 --- a/mss/darwin.py +++ b/mss/darwin.py @@ -8,7 +8,7 @@ import sys from typing import TYPE_CHECKING -from .base import MSSMixin +from .base import MSSBase from .exception import ScreenShotError from .screenshot import Size @@ -57,7 +57,7 @@ def __repr__(self): return "{}<{} {}>".format(type(self).__name__, self.origin, self.size) -class MSS(MSSMixin): +class MSS(MSSBase): """ Multiple ScreenShots implementation for macOS. It uses intensively the CoreGraphics library. @@ -175,7 +175,7 @@ def monitors(self): def grab(self, monitor): # type: (Monitor) -> ScreenShot """ - See :meth:`MSSMixin.grab ` for full details. + See :meth:`MSSBase.grab ` for full details. """ # pylint: disable=too-many-locals diff --git a/mss/factory.py b/mss/factory.py index 1d5b123d..47aea11a 100644 --- a/mss/factory.py +++ b/mss/factory.py @@ -12,11 +12,11 @@ if TYPE_CHECKING: from typing import Any # noqa - from .base import MSSMixin # noqa + from .base import MSSBase # noqa def mss(**kwargs): - # type: (Any) -> MSSMixin + # type: (Any) -> MSSBase """ Factory returning a proper MSS class instance. It detects the plateform we are running on diff --git a/mss/linux.py b/mss/linux.py index 5bd41c82..a11c5758 100644 --- a/mss/linux.py +++ b/mss/linux.py @@ -9,7 +9,7 @@ from types import SimpleNamespace from typing import TYPE_CHECKING -from .base import MSSMixin +from .base import MSSBase from .exception import ScreenShotError if TYPE_CHECKING: @@ -175,7 +175,7 @@ def validate(retval, func, args): raise ScreenShotError(err, details=details) -class MSS(MSSMixin): +class MSS(MSSBase): """ Multiple ScreenShots implementation for GNU/Linux. It uses intensively the Xlib and its Xrandr extension. diff --git a/mss/tests/test_gnu_linux.py b/mss/tests/test_gnu_linux.py index 328fde1c..a4e09f1c 100644 --- a/mss/tests/test_gnu_linux.py +++ b/mss/tests/test_gnu_linux.py @@ -9,7 +9,7 @@ import mss import pytest -from mss.base import MSSMixin +from mss.base import MSSBase from mss.exception import ScreenShotError @@ -32,7 +32,7 @@ def test_factory_systems(monkeypatch): # GNU/Linux monkeypatch.setattr(platform, "system", lambda: "LINUX") with mss.mss() as sct: - assert isinstance(sct, MSSMixin) + assert isinstance(sct, MSSBase) monkeypatch.undo() # macOS diff --git a/mss/tests/test_implementation.py b/mss/tests/test_implementation.py index d43f5fa1..af893017 100644 --- a/mss/tests/test_implementation.py +++ b/mss/tests/test_implementation.py @@ -6,59 +6,43 @@ import os import os.path import platform -import sys +import pytest import mss import mss.tools -from mss.base import MSSMixin +from mss.base import MSSBase from mss.exception import ScreenShotError from mss.screenshot import ScreenShot -import pytest - -PY3 = sys.version[0] > "2" - - -class MSS0(MSSMixin): +class MSS0(MSSBase): """ Nothing implemented. """ pass -class MSS1(MSSMixin): - """ Emulate no monitors. """ +class MSS1(MSSBase): + """ Only `grab()` implemented. """ - @property - def monitors(self): - return [] + def grab(self, monitor): + pass -class MSS2(MSSMixin): - """ Emulate one monitor. """ +class MSS2(MSSBase): + """ Only `monitor` implemented. """ @property def monitors(self): - return [{"top": 0, "left": 0, "width": 10, "height": 10}] - + return [] -def test_incomplete_class(): - # `monitors` property not implemented - with pytest.raises(NotImplementedError): - for filename in MSS0().save(): - assert os.path.isfile(filename) - # `monitors` property is empty - with pytest.raises(ScreenShotError): - for filename in MSS1().save(): - assert os.path.isfile(filename) +@pytest.mark.parametrize("cls", [MSS0, MSS1, MSS2]) +def test_incomplete_class(cls): + with pytest.raises(TypeError): + cls() - # `grab()` not implemented - sct = MSS2() - with pytest.raises(NotImplementedError): - sct.grab(sct.monitors[0]) - # Bad monitor +def test_bad_monitor(sct): with pytest.raises(ScreenShotError): sct.grab(sct.shot(mon=222)) @@ -79,7 +63,7 @@ def test_repr(sct, pixel_ratio): def test_factory(monkeypatch): # Current system with mss.mss() as sct: - assert isinstance(sct, MSSMixin) + assert isinstance(sct, MSSBase) # Unknown monkeypatch.setattr(platform, "system", lambda: "Chuck Norris") @@ -87,10 +71,7 @@ def test_factory(monkeypatch): mss.mss() monkeypatch.undo() - if not PY3: - error = exc.value[0] - else: - error = exc.value.args[0] + error = exc.value.args[0] assert error == "System 'chuck norris' not (yet?) implemented." diff --git a/mss/windows.py b/mss/windows.py index 7ce24240..772528a9 100644 --- a/mss/windows.py +++ b/mss/windows.py @@ -22,7 +22,7 @@ ) from typing import TYPE_CHECKING -from .base import MSSMixin +from .base import MSSBase from .exception import ScreenShotError if TYPE_CHECKING: @@ -65,7 +65,7 @@ class BITMAPINFO(ctypes.Structure): _fields_ = [("bmiHeader", BITMAPINFOHEADER), ("bmiColors", DWORD * 3)] -class MSS(MSSMixin): +class MSS(MSSBase): """ Multiple ScreenShots implementation for Microsoft Windows. """ __slots__ = {"_bbox", "_bmi", "_data", "gdi32", "monitorenumproc", "user32"} From 229bd0b6ce0c2cc4363cef4740d6ce53c0f67642 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Sun, 5 Jan 2020 12:50:47 +0100 Subject: [PATCH 012/280] MSS: clean-up MSSBase.monitors Missed remove from 9d01cb0fdfb939ab7b83cf6eb1bce09b27e27dc8. --- mss/base.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/mss/base.py b/mss/base.py index 59c4c7f5..c4de83e2 100644 --- a/mss/base.py +++ b/mss/base.py @@ -79,8 +79,6 @@ def monitors(self): it must be converted to the appropriate dict. """ - raise NotImplementedError("Subclasses need to implement this!") - def save(self, mon=0, output="monitor-{mon}.png", callback=None): # type: (int, str, Callable[[str], None]) -> Iterator[str] """ From a8cb2d2b771d4a476bbe1da5ce512291b962cb61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Sun, 26 Apr 2020 14:30:01 +0200 Subject: [PATCH 013/280] docs: add the normcap project --- docs/source/where.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/source/where.rst b/docs/source/where.rst index 64920ee2..792fd37b 100644 --- a/docs/source/where.rst +++ b/docs/source/where.rst @@ -44,3 +44,4 @@ Utilities - `Philips Hue Lights Ambiance `_; - `Flexx Python UI toolkit `_; - `NativeShot `_ (Mozilla Firefox module); +- `normcap `_, OCR powered screen-capture tool to capture information instead of images; From 80aa06e970e5a499a5269cc2b67049dd1b1f433c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=B3=B4=E6=B5=B7=E6=B0=8F?= <56487872+narumishi@users.noreply.github.com> Date: Tue, 28 Apr 2020 21:41:56 +0800 Subject: [PATCH 014/280] Windows: fix multi-thread safety (#159) * Windows: fix multi-thread unsafe and update test On Windows, the handle of entire window device context is saved to `srcdc`. But the device context will be released once the thread who creates it has died, so that `srcdc` is no loner valid. Replace `srcdc` with `srcdc_dict` to maintain srcdc values created by multiple threads which ensure the validity of srcdc when it's used. A threading lock is add to prevent multiple threads from grabbing and modifying shared class attributes `bmp`/`srcdc`/`memdc` (their windows object in fact) at same time. Otherwise, unexpected screenshot or unpredictable error will occur. Add test_thread_safety in test_windows.py --- CHANGELOG | 2 ++ CHANGES.rst | 4 ++++ CONTRIBUTORS | 3 +++ mss/tests/test_windows.py | 21 +++++++++++++++++++++ mss/windows.py | 36 ++++++++++++++++++++++++++++++------ 5 files changed, 60 insertions(+), 6 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 46adc043..7b5634bb 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -7,6 +7,8 @@ History: - MSS: renamed again MSSMixin to MSSBase, now derived from abc.ABCMeta - tools: force write of file when saving a PNG file - tests: fix tests on macOS with Retina display + - Windows: fixed multi-thread safety (fixes #150) + - :heart: contributors: @narumishi 5.0.0 2019/12/31 - removed support for Python 2.7 diff --git a/CHANGES.rst b/CHANGES.rst index 86b5540d..e4c1b519 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -8,6 +8,10 @@ base.py - ``MSSBase.monitor`` is now an abstract property - ``MSSBase.grab()`` is now an abstract method +windows.py +---------- + - Replaced ``MSS.srcdc`` with ``MSS.srcdc_dict`` + 5.0.0 (2019-12-31) ================== diff --git a/CONTRIBUTORS b/CONTRIBUTORS index 5bf52f6d..00971c5c 100644 --- a/CONTRIBUTORS +++ b/CONTRIBUTORS @@ -30,6 +30,9 @@ Jochen 'cycomanic' Schroeder [https://github.com/cycomanic] Karan Lyons [https://karanlyons.com] [https://github.com/karanlyons] - MacOS: Proper support for display scaling +narumi [https://github.com/narumishi] + - Windows: fix multi-thread unsafe + Oros [https://ecirtam.net] - GNU/Linux tester diff --git a/mss/tests/test_windows.py b/mss/tests/test_windows.py index 5b352294..53ed7b15 100644 --- a/mss/tests/test_windows.py +++ b/mss/tests/test_windows.py @@ -4,6 +4,7 @@ """ import platform +import threading import mss import pytest @@ -48,3 +49,23 @@ def test_region_caching(): # Grab the area 2 again, the cached BMP is used sct.grab(region2) assert bmp2 is MSS.bmp + + +def run_child_thread(loops): + """Every loop will take about 1 second.""" + for _ in range(loops): + with mss.mss() as sct: + sct.grab(sct.monitors[1]) + + +def test_thread_safety(): + """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 + thread1 = threading.Thread(target=run_child_thread, args=(30,)) + thread2 = threading.Thread(target=run_child_thread, args=(50,)) + thread1.start() + thread2.start() + thread1.join() + thread2.join() diff --git a/mss/windows.py b/mss/windows.py index 772528a9..02faaaee 100644 --- a/mss/windows.py +++ b/mss/windows.py @@ -5,6 +5,7 @@ import sys import ctypes +import threading from ctypes.wintypes import ( BOOL, DOUBLE, @@ -70,10 +71,15 @@ class MSS(MSSBase): __slots__ = {"_bbox", "_bmi", "_data", "gdi32", "monitorenumproc", "user32"} - # Class attributes instancied one time to prevent resource leaks. + # Class attributes instanced one time to prevent resource leaks. bmp = None memdc = None - srcdc = None + + # A dict to maintain *srcdc* values created by multiple threads. + srcdc_dict = {} + + # A threading lock to lock resources(bmp/memdc/srcdc) inside .grab() method. + _lock = threading.Lock() def __init__(self, **_): # type: (Any) -> None @@ -93,9 +99,9 @@ def __init__(self, **_): self._bbox = {"height": 0, "width": 0} self._data = ctypes.create_string_buffer(0) # type: ctypes.Array[ctypes.c_char] - if not MSS.srcdc or not MSS.memdc: - MSS.srcdc = self.user32.GetWindowDC(0) - MSS.memdc = self.gdi32.CreateCompatibleDC(MSS.srcdc) + srcdc = self._get_srcdc() + if not MSS.memdc: + MSS.memdc = self.gdi32.CreateCompatibleDC(srcdc) bmi = BITMAPINFO() bmi.bmiHeader.biSize = ctypes.sizeof(BITMAPINFOHEADER) @@ -174,6 +180,20 @@ def _set_dpi_awareness(self): # Windows Vista, 7, 8 and Server 2012 self.user32.SetProcessDPIAware() + def _get_srcdc(self): + """ + Retrieve a thread-safe HDC from GetWindowDC(). + In multithreading, if the thread who creates *srcdc* is dead, *srcdc* will + no longer be valid to grab the screen. The *srcdc* attribute is replaced + with *srcdc_dict* to maintain the *srcdc* values in multithreading. + Since the current thread and main thread are always alive, reuse their *srcdc* value first. + """ + cur_thread, main_thread = threading.current_thread(), threading.main_thread() + srcdc = MSS.srcdc_dict.get(cur_thread) or MSS.srcdc_dict.get(main_thread) + if not srcdc: + srcdc = MSS.srcdc_dict[cur_thread] = self.user32.GetWindowDC(0) + return srcdc + @property def monitors(self): # type: () -> Monitors @@ -251,6 +271,9 @@ def grab(self, monitor): Thanks to http://stackoverflow.com/a/3688682 """ + # Acquire lock to prevent resources from being modified by multiple threads at same time. + MSS._lock.acquire() + # Convert PIL bbox style if isinstance(monitor, tuple): monitor = { @@ -260,7 +283,7 @@ def grab(self, monitor): "height": monitor[3] - monitor[1], } - srcdc, memdc = MSS.srcdc, MSS.memdc + srcdc, memdc = self._get_srcdc(), MSS.memdc width, height = monitor["width"], monitor["height"] if (self._bbox["height"], self._bbox["width"]) != (height, width): @@ -287,6 +310,7 @@ def grab(self, monitor): bits = self.gdi32.GetDIBits( memdc, MSS.bmp, 0, height, self._data, self._bmi, DIB_RGB_COLORS ) + MSS._lock.release() if bits != height: raise ScreenShotError("gdi32.GetDIBits() failed.") From 7f98b6a6c92a945ed69799c71d580e5aa3b49d93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Tue, 28 Apr 2020 16:10:55 +0200 Subject: [PATCH 015/280] Adjustments following previous commit - Fixed the type annotation of `srcdc_dict`. - Removed obsolete comment in `test_windows.py`. - Fixed `PytestDeprecationWarning: --no-print-logs is deprecated and scheduled for removal in pytest 6.0.` - Fixed Sourcery issues in `test_windows.py`. --- mss/tests/test_windows.py | 16 +++++++--------- mss/windows.py | 4 ++-- setup.cfg | 1 - 3 files changed, 9 insertions(+), 12 deletions(-) diff --git a/mss/tests/test_windows.py b/mss/tests/test_windows.py index 53ed7b15..62f7b6c2 100644 --- a/mss/tests/test_windows.py +++ b/mss/tests/test_windows.py @@ -27,10 +27,6 @@ def test_region_caching(): """The region to grab is cached, ensure this is well-done.""" from mss.windows import MSS - # Same sizes but different positions - region1 = {"top": 0, "left": 0, "width": 200, "height": 200} - region2 = {"top": 200, "left": 200, "width": 200, "height": 200} - with mss.mss() as sct: # Reset the current BMP if MSS.bmp: @@ -38,21 +34,23 @@ def test_region_caching(): MSS.bmp = None # Grab the area 1 + region1 = {"top": 0, "left": 0, "width": 200, "height": 200} sct.grab(region1) - bmp1 = MSS.bmp + bmp1 = id(MSS.bmp) # Grab the area 2, the cached BMP is used + # Same sizes but different positions + region2 = {"top": 200, "left": 200, "width": 200, "height": 200} sct.grab(region2) - bmp2 = MSS.bmp - assert bmp1 is bmp2 + bmp2 = id(MSS.bmp) + assert bmp1 == bmp2 # Grab the area 2 again, the cached BMP is used sct.grab(region2) - assert bmp2 is MSS.bmp + assert bmp2 == id(MSS.bmp) def run_child_thread(loops): - """Every loop will take about 1 second.""" for _ in range(loops): with mss.mss() as sct: sct.grab(sct.monitors[1]) diff --git a/mss/windows.py b/mss/windows.py index 02faaaee..6bce1111 100644 --- a/mss/windows.py +++ b/mss/windows.py @@ -27,7 +27,7 @@ from .exception import ScreenShotError if TYPE_CHECKING: - from typing import Any # noqa + from typing import Any, Dict # noqa from .models import Monitor, Monitors # noqa from .screenshot import ScreenShot # noqa @@ -76,7 +76,7 @@ class MSS(MSSBase): memdc = None # A dict to maintain *srcdc* values created by multiple threads. - srcdc_dict = {} + srcdc_dict = {} # type: Dict[int, Any] # A threading lock to lock resources(bmp/memdc/srcdc) inside .grab() method. _lock = threading.Lock() diff --git a/setup.cfg b/setup.cfg index 2a123949..23c5618b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -51,6 +51,5 @@ addopts = --showlocals --strict --failed-first - --no-print-logs -r fE -v From abff4bf54bed106c3e718c90f32ca8f20fc936c5 Mon Sep 17 00:00:00 2001 From: narumi Date: Thu, 30 Apr 2020 00:04:06 +0800 Subject: [PATCH 016/280] doc: fix the type annotation of srcdc_dict --- mss/windows.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mss/windows.py b/mss/windows.py index 6bce1111..ca5a16b8 100644 --- a/mss/windows.py +++ b/mss/windows.py @@ -76,7 +76,7 @@ class MSS(MSSBase): memdc = None # A dict to maintain *srcdc* values created by multiple threads. - srcdc_dict = {} # type: Dict[int, Any] + srcdc_dict = {} # type: Dict[threading.Thread, int] # A threading lock to lock resources(bmp/memdc/srcdc) inside .grab() method. _lock = threading.Lock() From 7de27cd3701932b1b530adf5ea108ad867f6bdab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Thu, 30 Apr 2020 14:14:11 +0200 Subject: [PATCH 017/280] Release 5.1.0 Also add Python 3.9 to the CI. --- .travis.yml | 17 ++++++++++++++--- CHANGELOG | 2 +- CHANGES.rst | 2 +- appveyor.yml | 6 ++++++ docs/source/conf.py | 2 +- mss/__init__.py | 2 +- setup.cfg | 5 +++-- tox.ini | 5 +++-- 8 files changed, 30 insertions(+), 11 deletions(-) diff --git a/.travis.yml b/.travis.yml index fe8b982e..7c3a51cd 100644 --- a/.travis.yml +++ b/.travis.yml @@ -32,18 +32,25 @@ matrix: - TOXENV=py36 - name: "Python 3.7 on macOS 10.14" os: osx - osx_image: xcode10.2 # Python 3.7.3 running on macOS 10.14.3 + osx_image: xcode11.2 # Python 3.7.4 running on macOS 10.14.4 language: shell env: - PYTHON_VERSION=3.7 - TOXENV=py37 + - name: "Python 3.8 on macOS 10.14" + os: osx + osx_image: xcode11.3 # Python 3.8.0 running on macOS 10.14.6 + language: shell + env: + - PYTHON_VERSION=3.8 + - TOXENV=py38 - os: osx language: shell before_install: - bash .travis/install.sh env: - - PYTHON_VERSION=3.8 - - TOXENV=py38 + - PYTHON_VERSION=3.9 + - TOXENV=py39 - name: "PyPy 3.6 on GNU/Linux" os: linux python: "pypy3" @@ -64,6 +71,10 @@ matrix: os: linux python: "3.8" env: TOXENV=py38 + - name: "Python 3.9 on GNU/Linux" + os: linux + python: "nightly" + env: TOXENV=py39 addons: apt: diff --git a/CHANGELOG b/CHANGELOG index 7b5634bb..f2352b06 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -2,7 +2,7 @@ History: -5.0.1 2020/xx/xx +5.1.0 2020/04/30 - produce wheels for Python 3 only - MSS: renamed again MSSMixin to MSSBase, now derived from abc.ABCMeta - tools: force write of file when saving a PNG file diff --git a/CHANGES.rst b/CHANGES.rst index e4c1b519..52c8df86 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,4 +1,4 @@ -5.0.1 (2020-xx-xx) +5.1.0 (2020-04-30) ================== base.py diff --git a/appveyor.yml b/appveyor.yml index f018e0ae..f78367e0 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -30,6 +30,12 @@ environment: - python: py38-x64 tox_env: py38 python_path: c:\python38-x64 + - python: py39 + tox_env: py39 + python_path: c:\python39 + - python: py39-x64 + tox_env: py39 + python_path: c:\python39-x64 install: - python -m pip install virtualenv diff --git a/docs/source/conf.py b/docs/source/conf.py index ef8f5891..bbe43e7c 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -27,7 +27,7 @@ # built documents. # # The short X.Y version. -version = "5.0.1" +version = "5.1.0" # The full version, including alpha/beta/rc tags. release = "latest" diff --git a/mss/__init__.py b/mss/__init__.py index da96747a..b265bc8d 100644 --- a/mss/__init__.py +++ b/mss/__init__.py @@ -12,7 +12,7 @@ from .exception import ScreenShotError from .factory import mss -__version__ = "5.0.1" +__version__ = "5.1.0" __author__ = "Mickaël 'Tiger-222' Schoentgen" __copyright__ = """ Copyright (c) 2013-2020, Mickaël 'Tiger-222' Schoentgen diff --git a/setup.cfg b/setup.cfg index 23c5618b..440b5aad 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = mss -version = 5.0.1 +version = 5.1.0 author = Mickaël 'Tiger-222' Schoentgen author-email = contact@tiger-222.fr description = An ultra fast cross-platform multiple screenshots module in pure python using ctypes. @@ -20,11 +20,12 @@ classifiers = License :: OSI Approved :: MIT License Programming Language :: Python Programming Language :: Python :: 3 + Programming Language :: Python :: 3 :: Only Programming Language :: Python :: 3.5 Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 - Programming Language :: Python :: 3 :: Only + Programming Language :: Python :: 3.9 Topic :: Multimedia :: Graphics :: Capture :: Screen Capture Topic :: Software Development :: Libraries diff --git a/tox.ini b/tox.ini index 1b402bb0..6abf8e54 100644 --- a/tox.ini +++ b/tox.ini @@ -3,7 +3,7 @@ envlist = lint types docs - py{38,37,36,35,py3} + py{39,38,37,36,35,py3} skip_missing_interpreters = True [testenv] @@ -12,7 +12,8 @@ alwayscopy = True deps = pytest # Must pin that version to support PyPy3 - numpy==1.15.4 + pypy3: numpy==1.15.4 + py{39,38,37,36,35}: numpy pillow wheel commands = From 528290826f27c4ed026ff64bac5425f08ff60d0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Thu, 30 Apr 2020 14:20:22 +0200 Subject: [PATCH 018/280] Bump the version to 5.1.1 --- CHANGELOG | 4 ++++ CHANGES.rst | 6 ++++++ docs/source/conf.py | 2 +- mss/__init__.py | 2 +- setup.cfg | 2 +- 5 files changed, 13 insertions(+), 3 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index f2352b06..ff00fac9 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -2,6 +2,10 @@ History: +5.1.1 2020/xx/xx + - + - :heart: contributors: @ + 5.1.0 2020/04/30 - produce wheels for Python 3 only - MSS: renamed again MSSMixin to MSSBase, now derived from abc.ABCMeta diff --git a/CHANGES.rst b/CHANGES.rst index 52c8df86..d2b068a7 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,9 @@ +5.1.1 (2020-xx-xx) +================== + + + + 5.1.0 (2020-04-30) ================== diff --git a/docs/source/conf.py b/docs/source/conf.py index bbe43e7c..58626a31 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -27,7 +27,7 @@ # built documents. # # The short X.Y version. -version = "5.1.0" +version = "5.1.1" # The full version, including alpha/beta/rc tags. release = "latest" diff --git a/mss/__init__.py b/mss/__init__.py index b265bc8d..d85bf688 100644 --- a/mss/__init__.py +++ b/mss/__init__.py @@ -12,7 +12,7 @@ from .exception import ScreenShotError from .factory import mss -__version__ = "5.1.0" +__version__ = "5.1.1" __author__ = "Mickaël 'Tiger-222' Schoentgen" __copyright__ = """ Copyright (c) 2013-2020, Mickaël 'Tiger-222' Schoentgen diff --git a/setup.cfg b/setup.cfg index 440b5aad..986b0b57 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = mss -version = 5.1.0 +version = 5.1.1 author = Mickaël 'Tiger-222' Schoentgen author-email = contact@tiger-222.fr description = An ultra fast cross-platform multiple screenshots module in pure python using ctypes. From 8c0565724f6da264e8982c99f52781ec7dc3fa23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Thu, 30 Apr 2020 14:23:33 +0200 Subject: [PATCH 019/280] Fix usage of deprecated "license_file" option DeprecationWarning: The "license_file" option is deprecated. Use "license_files" instead. --- CHANGELOG | 2 +- setup.cfg | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index ff00fac9..6abf1b01 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -3,7 +3,7 @@ History: 5.1.1 2020/xx/xx - - + - removed usage of deprecated "license_file" option for "license_files" - :heart: contributors: @ 5.1.0 2020/04/30 diff --git a/setup.cfg b/setup.cfg index 986b0b57..fcc5739a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -13,7 +13,8 @@ project_urls = Tracker = https://github.com/BoboTiG/python-mss/issues keywords = screen, screenshot, screencapture, screengrab license = MIT -license_file = LICENSE +license_files = + LICENSE platforms = Darwin, Linux, Windows classifiers = Development Status :: 5 - Production/Stable From 23037e23561305b18c74d0db27d0c0977df62925 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Sun, 28 Jun 2020 17:53:04 +0200 Subject: [PATCH 020/280] Fixed flake8 usage in pre-commit See https://github.com/pre-commit/pre-commit-hooks/issues/344. --- .pre-commit-config.yaml | 5 ++++- CHANGELOG | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 98716e13..c7f49431 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -5,11 +5,14 @@ repos: rev: stable hooks: - id: black +- repo: https://gitlab.com/pycqa/flake8 + rev: master + hooks: + - id: flake8 - repo: https://github.com/pre-commit/pre-commit-hooks rev: master hooks: - id: trailing-whitespace - - id: flake8 - id: end-of-file-fixer - id: check-docstring-first - id: debug-statements diff --git a/CHANGELOG b/CHANGELOG index 6abf1b01..9e7554d6 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -4,6 +4,7 @@ History: 5.1.1 2020/xx/xx - removed usage of deprecated "license_file" option for "license_files" + - fixed flake8 usage in pre-commit - :heart: contributors: @ 5.1.0 2020/04/30 From 59c22592f6c3cced68490bb0085af545b3e777a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Sun, 28 Jun 2020 17:57:42 +0200 Subject: [PATCH 021/280] Linux: the implementation is now thread-safe (fixes #169) --- CHANGELOG | 1 + mss/linux.py | 124 +++++++++++++++++++++---------- mss/tests/test_implementation.py | 29 ++++++++ 3 files changed, 113 insertions(+), 41 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 9e7554d6..8c26e4ab 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -5,6 +5,7 @@ History: 5.1.1 2020/xx/xx - removed usage of deprecated "license_file" option for "license_files" - fixed flake8 usage in pre-commit + - Linux: the implementation is now thread-safe (fixes #169) - :heart: contributors: @ 5.1.0 2020/04/30 diff --git a/mss/linux.py b/mss/linux.py index a11c5758..e37b65f9 100644 --- a/mss/linux.py +++ b/mss/linux.py @@ -6,6 +6,7 @@ import ctypes import ctypes.util import os +import threading from types import SimpleNamespace from typing import TYPE_CHECKING @@ -187,6 +188,12 @@ class MSS(MSSBase): # Instancied one time to prevent resource leak. display = None + # A dict to maintain *display* values created by multiple threads. + _display_dict = {} # type: Dict[threading.Thread, int] + + # A threading lock to lock resources. + _lock = threading.Lock() + def __init__(self, display=None): # type: (Optional[Union[bytes, str]]) -> None """ GNU/Linux initialisations. """ @@ -221,14 +228,29 @@ def __init__(self, display=None): self._set_cfunctions() - if not MSS.display: - MSS.display = self.xlib.XOpenDisplay(display) - self.root = self.xlib.XDefaultRootWindow(MSS.display) + self.root = self.xlib.XDefaultRootWindow(self._get_display(display)) # Fix for XRRGetScreenResources and XGetImage: # expected LP_Display instance instead of LP_XWindowAttributes self.drawable = ctypes.cast(self.root, ctypes.POINTER(Display)) + def _get_display(self, disp=None): + """ + Retrieve a thread-safe display from XOpenDisplay(). + In multithreading, if the thread who creates *display* is dead, *display* will + no longer be valid to grab the screen. The *display* attribute is replaced + with *_display_dict* to maintain the *display* values in multithreading. + Since the current thread and main thread are always alive, reuse their + *display* value first. + """ + cur_thread, main_thread = threading.current_thread(), threading.main_thread() + display = MSS._display_dict.get(cur_thread) or MSS._display_dict.get( + main_thread + ) + if not display: + display = MSS._display_dict[cur_thread] = self.xlib.XOpenDisplay(disp) + return display + def _set_cfunctions(self): """ Set all ctypes functions and attach them to attributes. @@ -324,7 +346,7 @@ def get_error_details(self): ERROR.details = None xserver_error = ctypes.create_string_buffer(1024) self.xlib.XGetErrorText( - MSS.display, + self._get_display(), details.get("xerror_details", {}).get("error_code", 0), xserver_error, len(xserver_error), @@ -335,53 +357,66 @@ def get_error_details(self): return details - @property - def monitors(self): - # type: () -> Monitors - """ Get positions of monitors (see parent class property). """ + def _monitors_impl(self): + # type: () -> None + """ + Get positions of monitors (has to be run using a threading lock). + It will populate self._monitors. + """ - if not self._monitors: - display = MSS.display - int_ = int - xrandr = self.xrandr + display = self._get_display() + int_ = int + xrandr = self.xrandr + + # All monitors + gwa = XWindowAttributes() + self.xlib.XGetWindowAttributes(display, self.root, ctypes.byref(gwa)) + self._monitors.append( + { + "left": int_(gwa.x), + "top": int_(gwa.y), + "width": int_(gwa.width), + "height": int_(gwa.height), + } + ) + + # Each monitors + mon = xrandr.XRRGetScreenResourcesCurrent(display, self.drawable).contents + crtcs = mon.crtcs + for idx in range(mon.ncrtc): + crtc = xrandr.XRRGetCrtcInfo(display, mon, crtcs[idx]).contents + if crtc.noutput == 0: + xrandr.XRRFreeCrtcInfo(crtc) + continue - # All monitors - gwa = XWindowAttributes() - self.xlib.XGetWindowAttributes(display, self.root, ctypes.byref(gwa)) self._monitors.append( { - "left": int_(gwa.x), - "top": int_(gwa.y), - "width": int_(gwa.width), - "height": int_(gwa.height), + "left": int_(crtc.x), + "top": int_(crtc.y), + "width": int_(crtc.width), + "height": int_(crtc.height), } ) + xrandr.XRRFreeCrtcInfo(crtc) + xrandr.XRRFreeScreenResources(mon) - # Each monitors - mon = xrandr.XRRGetScreenResourcesCurrent(display, self.drawable).contents - crtcs = mon.crtcs - for idx in range(mon.ncrtc): - crtc = xrandr.XRRGetCrtcInfo(display, mon, crtcs[idx]).contents - if crtc.noutput == 0: - xrandr.XRRFreeCrtcInfo(crtc) - continue - - self._monitors.append( - { - "left": int_(crtc.x), - "top": int_(crtc.y), - "width": int_(crtc.width), - "height": int_(crtc.height), - } - ) - xrandr.XRRFreeCrtcInfo(crtc) - xrandr.XRRFreeScreenResources(mon) + @property + def monitors(self): + # type: () -> Monitors + """ Get positions of monitors (see parent class property). """ + + if not self._monitors: + with MSS._lock: + self._monitors_impl() return self._monitors - def grab(self, monitor): + def _grab_impl(self, monitor): # type: (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. + """ # Convert PIL bbox style if isinstance(monitor, tuple): @@ -393,7 +428,7 @@ def grab(self, monitor): } ximage = self.xlib.XGetImage( - MSS.display, + self._get_display(), self.drawable, monitor["left"], monitor["top"], @@ -424,3 +459,10 @@ def grab(self, monitor): self.xlib.XDestroyImage(ximage) return self.cls_image(data, monitor) + + def grab(self, monitor): + # type: (Monitor) -> ScreenShot + """ Retrieve all pixels from a monitor. Pixels have to be RGB. """ + + with MSS._lock: + return self._grab_impl(monitor) diff --git a/mss/tests/test_implementation.py b/mss/tests/test_implementation.py index af893017..62cc8d27 100644 --- a/mss/tests/test_implementation.py +++ b/mss/tests/test_implementation.py @@ -168,3 +168,32 @@ def test_grab_with_tuple_percents(sct, pixel_ratio): assert im.size == im2.size assert im.pos == im2.pos assert im.rgb == im2.rgb + + +def test_thread_safety(): + """Regression test for issue #169.""" + import threading + import time + + def record(check): + """Record for one second.""" + + start_time = time.time() + while time.time() - start_time < 1: + with mss.mss() as sct: + sct.grab(sct.monitors[1]) + + check[threading.current_thread()] = True + + checkpoint = {} + t1 = threading.Thread(target=record, args=(checkpoint,)) + t2 = threading.Thread(target=record, args=(checkpoint,)) + + t1.start() + time.sleep(0.5) + t2.start() + + t1.join() + t2.join() + + assert len(checkpoint) == 2 From b71343001a377609252a2633bb6f3acbfd90e368 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Sun, 28 Jun 2020 18:56:42 +0200 Subject: [PATCH 022/280] tests: fixed Sourcery smells --- mss/tests/test_gnu_linux.py | 3 +-- mss/tests/test_leaks.py | 10 +++++----- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/mss/tests/test_gnu_linux.py b/mss/tests/test_gnu_linux.py index a4e09f1c..6f40b2a2 100644 --- a/mss/tests/test_gnu_linux.py +++ b/mss/tests/test_gnu_linux.py @@ -109,10 +109,9 @@ def find_lib_mocked(lib): def test_region_out_of_monitor_bounds(): display = os.getenv("DISPLAY") - monitor = {"left": -30, "top": 0, "width": 100, "height": 100} - with mss.mss(display=display) as sct: with pytest.raises(ScreenShotError) as exc: + monitor = {"left": -30, "top": 0, "width": 100, "height": 100} assert sct.grab(monitor) assert str(exc.value) diff --git a/mss/tests/test_leaks.py b/mss/tests/test_leaks.py index fbfcd9f7..e7a6ca1b 100644 --- a/mss/tests/test_leaks.py +++ b/mss/tests/test_leaks.py @@ -82,21 +82,21 @@ def with_context_manager(): def regression_issue_128(): """Regression test for issue #128: areas overlap.""" - area1 = {"top": 50, "left": 7, "width": 400, "height": 320, "mon": 1} - area2 = {"top": 200, "left": 200, "width": 320, "height": 320, "mon": 1} with 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(): """Regression test for issue #135: multiple areas.""" - bounding_box_notes = {"top": 0, "left": 0, "width": 100, "height": 100} - bounding_box_score = {"top": 110, "left": 110, "width": 100, "height": 100} - bounding_box_test = {"top": 220, "left": 220, "width": 100, "height": 100} with 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} sct.grab(bounding_box_test) + bounding_box_score = {"top": 110, "left": 110, "width": 100, "height": 100} sct.grab(bounding_box_score) From c15ac7f4a47f2ab26c08187406a6881755494e75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Sun, 28 Jun 2020 18:57:19 +0200 Subject: [PATCH 023/280] doc: add ScreenCapLibrary --- docs/source/where.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/source/where.rst b/docs/source/where.rst index 792fd37b..ab124fb2 100644 --- a/docs/source/where.rst +++ b/docs/source/where.rst @@ -41,7 +41,8 @@ Security Utilities ========= -- `Philips Hue Lights Ambiance `_; - `Flexx Python UI toolkit `_; - `NativeShot `_ (Mozilla Firefox module); - `normcap `_, OCR powered screen-capture tool to capture information instead of images; +- `Philips Hue Lights Ambiance `_; +- `ScreenCapLibrary `_, a Robot Framework test library for capturing screenshots and video recording; From 6da816830dc3b351f0ef7399f957e7d0eb58b9f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Sun, 28 Jun 2020 18:58:42 +0200 Subject: [PATCH 024/280] MSS: the implementation is now thread-safe on all OSes --- CHANGELOG | 4 +- CHANGES.rst | 25 +++++++- README.rst | 2 +- docs/source/conf.py | 2 +- docs/source/index.rst | 2 +- mss/__init__.py | 2 +- mss/base.py | 45 ++++++++++++-- mss/darwin.py | 113 +++++++++++++++------------------- mss/linux.py | 40 +----------- mss/windows.py | 137 ++++++++++++++++++------------------------ setup.cfg | 2 +- 11 files changed, 181 insertions(+), 193 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 8c26e4ab..d38d83be 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -2,10 +2,10 @@ History: -5.1.1 2020/xx/xx +6.0.0 2020/xx/xx - removed usage of deprecated "license_file" option for "license_files" - fixed flake8 usage in pre-commit - - Linux: the implementation is now thread-safe (fixes #169) + - MSS: the implementation is now thread-safe on all OSes (fixes #169) - :heart: contributors: @ 5.1.0 2020/04/30 diff --git a/CHANGES.rst b/CHANGES.rst index d2b068a7..9a12dda8 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,7 +1,30 @@ -5.1.1 (2020-xx-xx) +6.0.0 (2020-xx-xx) ================== +base.py +------- + - Added ``lock`` + - Added ``MSS._grab_impl()`` (abstract method) + - Added ``MSS._monitors_impl()`` (abstract method) + - ``MSS.grab()`` is no more an abstract method + - ``MSS.monitors`` is no more an abstract property + +darwin.py +--------- + - Renamed ``MSS.grab()`` to ``MSS._grab_impl()`` + - Renamed ``MSS.monitors`` to ``MSS._monitors_impl()`` +linux.py +-------- + - Renamed ``MSS.grab()`` to ``MSS._grab_impl()`` + - Renamed ``MSS.monitors`` to ``MSS._monitors_impl()`` + +windows.py +---------- + - Removed ``MSS._lock`` + - Renamed ``MSS.srcdc_dict`` to ``MSS._srcdc_dict`` + - Renamed ``MSS.grab()`` to ``MSS._grab_impl()`` + - Renamed ``MSS.monitors`` to ``MSS._monitors_impl()`` 5.1.0 (2020-04-30) diff --git a/README.rst b/README.rst index 67d88a14..228b6e4c 100644 --- a/README.rst +++ b/README.rst @@ -22,7 +22,7 @@ Python MSS An ultra fast cross-platform multiple screenshots module in pure python using ctypes. -- **Python 3.5+** and PEP8 compliant, no dependency; +- **Python 3.5+** and PEP8 compliant, no dependency, thread-safe; - very basic, it will grab one screen shot by monitor or a screen shot of all monitors and save it to a PNG file; - but you can use PIL and benefit from all its formats (or add yours directly); - integrate well with Numpy and OpenCV; diff --git a/docs/source/conf.py b/docs/source/conf.py index 58626a31..343c74e6 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -27,7 +27,7 @@ # built documents. # # The short X.Y version. -version = "5.1.1" +version = "6.0.0" # The full version, including alpha/beta/rc tags. release = "latest" diff --git a/docs/source/index.rst b/docs/source/index.rst index 228eceb3..def5e870 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -12,7 +12,7 @@ Welcome to Python MSS's documentation! An ultra fast cross-platform multiple screenshots module in pure python using ctypes. - - **Python 3.5+** and :pep:`8` compliant, no dependency; + - **Python 3.5+** and :pep:`8` compliant, no dependency, thread-safe; - very basic, it will grab one screen shot by monitor or a screen shot of all monitors and save it to a PNG file; - but you can use PIL and benefit from all its formats (or add yours directly); - integrate well with Numpy and OpenCV; diff --git a/mss/__init__.py b/mss/__init__.py index d85bf688..a88db5c3 100644 --- a/mss/__init__.py +++ b/mss/__init__.py @@ -12,7 +12,7 @@ from .exception import ScreenShotError from .factory import mss -__version__ = "5.1.1" +__version__ = "6.0.0" __author__ = "Mickaël 'Tiger-222' Schoentgen" __copyright__ = """ Copyright (c) 2013-2020, Mickaël 'Tiger-222' Schoentgen diff --git a/mss/base.py b/mss/base.py index c4de83e2..c5b5f626 100644 --- a/mss/base.py +++ b/mss/base.py @@ -6,6 +6,7 @@ from abc import ABCMeta, abstractmethod from datetime import datetime from typing import TYPE_CHECKING +from threading import Lock from .exception import ScreenShotError from .screenshot import ScreenShot @@ -17,6 +18,9 @@ from .models import Monitor, Monitors # noqa +lock = Lock() + + class MSSBase(metaclass=ABCMeta): """ This class will be overloaded by a system specific one. """ @@ -38,23 +42,51 @@ def __exit__(self, *_): self.close() + @abstractmethod + def _grab_impl(self, monitor): + # type: (Monitor) -> ScreenShot + """ + 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): + # type: () -> None + """ + Get positions of monitors (has to be run using a threading lock). + It must populate self._monitors. + """ + def close(self): # type: () -> None """ Clean-up. """ - @abstractmethod def grab(self, monitor): # type: (Monitor) -> ScreenShot """ Retrieve screen pixels for a given monitor. + Note: *monitor* can be a tuple like PIL.Image.grab() accepts. + :param monitor: The coordinates and size of the box to capture. See :meth:`monitors ` for object details. :return :class:`ScreenShot `. """ + # Convert PIL bbox style + if isinstance(monitor, tuple): + monitor = { + "left": monitor[0], + "top": monitor[1], + "width": monitor[2] - monitor[0], + "height": monitor[3] - monitor[1], + } + + with lock: + return self._grab_impl(monitor) + @property - @abstractmethod def monitors(self): # type: () -> Monitors """ @@ -74,11 +106,14 @@ def monitors(self): 'width': the width, 'height': the height } - - Note: monitor can be a tuple like PIL.Image.grab() accepts, - it must be converted to the appropriate dict. """ + if not self._monitors: + with lock: + self._monitors_impl() + + return self._monitors + def save(self, mon=0, output="monitor-{mon}.png", callback=None): # type: (int, str, Callable[[str], None]) -> Iterator[str] """ diff --git a/mss/darwin.py b/mss/darwin.py index 3d1b8f7b..6434e8fb 100644 --- a/mss/darwin.py +++ b/mss/darwin.py @@ -120,75 +120,60 @@ def cfactory(func, argtypes, restype): cfactory(func="CGDataProviderRelease", argtypes=[void], restype=void) cfactory(func="CFRelease", argtypes=[void], restype=void) - @property - def monitors(self): - # type: () -> Monitors - """ Get positions of monitors (see parent class). """ - - if not self._monitors: - int_ = int - core = self.core - - # All monitors - # We need to update the value with every single monitor found - # using CGRectUnion. Else we will end with infinite values. - all_monitors = CGRect() - self._monitors.append({}) - - # Each monitors - display_count = ctypes.c_uint32(0) - active_displays = (ctypes.c_uint32 * self.max_displays)() - core.CGGetActiveDisplayList( - self.max_displays, active_displays, ctypes.byref(display_count) + def _monitors_impl(self): + # type: () -> None + """ Get positions of monitors. It will populate self._monitors. """ + + int_ = int + core = self.core + + # All monitors + # We need to update the value with every single monitor found + # using CGRectUnion. Else we will end with infinite values. + all_monitors = CGRect() + self._monitors.append({}) + + # Each monitors + display_count = ctypes.c_uint32(0) + active_displays = (ctypes.c_uint32 * self.max_displays)() + core.CGGetActiveDisplayList( + self.max_displays, active_displays, ctypes.byref(display_count) + ) + rotations = {0.0: "normal", 90.0: "right", -90.0: "left"} + for idx in range(display_count.value): + display = active_displays[idx] + rect = core.CGDisplayBounds(display) + rect = core.CGRectStandardize(rect) + width, height = rect.size.width, rect.size.height + rot = core.CGDisplayRotation(display) + if rotations[rot] in ["left", "right"]: + width, height = height, width + self._monitors.append( + { + "left": int_(rect.origin.x), + "top": int_(rect.origin.y), + "width": int_(width), + "height": int_(height), + } ) - rotations = {0.0: "normal", 90.0: "right", -90.0: "left"} - for idx in range(display_count.value): - display = active_displays[idx] - rect = core.CGDisplayBounds(display) - rect = core.CGRectStandardize(rect) - width, height = rect.size.width, rect.size.height - rot = core.CGDisplayRotation(display) - if rotations[rot] in ["left", "right"]: - width, height = height, width - self._monitors.append( - { - "left": int_(rect.origin.x), - "top": int_(rect.origin.y), - "width": int_(width), - "height": int_(height), - } - ) - - # Update AiO monitor's values - all_monitors = core.CGRectUnion(all_monitors, rect) - - # Set the AiO monitor's values - self._monitors[0] = { - "left": int_(all_monitors.origin.x), - "top": int_(all_monitors.origin.y), - "width": int_(all_monitors.size.width), - "height": int_(all_monitors.size.height), - } - - return self._monitors - - def grab(self, monitor): + + # Update AiO monitor's values + all_monitors = core.CGRectUnion(all_monitors, rect) + + # Set the AiO monitor's values + self._monitors[0] = { + "left": int_(all_monitors.origin.x), + "top": int_(all_monitors.origin.y), + "width": int_(all_monitors.size.width), + "height": int_(all_monitors.size.height), + } + + def _grab_impl(self, monitor): # type: (Monitor) -> ScreenShot - """ - See :meth:`MSSBase.grab ` for full details. - """ + """ Retrieve all pixels from a monitor. Pixels have to be RGB. """ # pylint: disable=too-many-locals - # Convert PIL bbox style - if isinstance(monitor, tuple): - monitor = { - "left": monitor[0], - "top": monitor[1], - "width": monitor[2] - monitor[0], - "height": monitor[3] - monitor[1], - } - core = self.core rect = CGRect( (monitor["left"], monitor["top"]), (monitor["width"], monitor["height"]) diff --git a/mss/linux.py b/mss/linux.py index e37b65f9..48b6d980 100644 --- a/mss/linux.py +++ b/mss/linux.py @@ -191,9 +191,6 @@ class MSS(MSSBase): # A dict to maintain *display* values created by multiple threads. _display_dict = {} # type: Dict[threading.Thread, int] - # A threading lock to lock resources. - _lock = threading.Lock() - def __init__(self, display=None): # type: (Optional[Union[bytes, str]]) -> None """ GNU/Linux initialisations. """ @@ -359,10 +356,7 @@ def get_error_details(self): def _monitors_impl(self): # type: () -> None - """ - Get positions of monitors (has to be run using a threading lock). - It will populate self._monitors. - """ + """ Get positions of monitors. It will populate self._monitors. """ display = self._get_display() int_ = int @@ -400,32 +394,9 @@ def _monitors_impl(self): xrandr.XRRFreeCrtcInfo(crtc) xrandr.XRRFreeScreenResources(mon) - @property - def monitors(self): - # type: () -> Monitors - """ Get positions of monitors (see parent class property). """ - - if not self._monitors: - with MSS._lock: - self._monitors_impl() - - return self._monitors - def _grab_impl(self, monitor): # type: (Monitor) -> ScreenShot - """ - Retrieve all pixels from a monitor. Pixels have to be RGB. - That method has to be run using a threading lock. - """ - - # Convert PIL bbox style - if isinstance(monitor, tuple): - monitor = { - "left": monitor[0], - "top": monitor[1], - "width": monitor[2] - monitor[0], - "height": monitor[3] - monitor[1], - } + """ Retrieve all pixels from a monitor. Pixels have to be RGB. """ ximage = self.xlib.XGetImage( self._get_display(), @@ -459,10 +430,3 @@ def _grab_impl(self, monitor): self.xlib.XDestroyImage(ximage) return self.cls_image(data, monitor) - - def grab(self, monitor): - # type: (Monitor) -> ScreenShot - """ Retrieve all pixels from a monitor. Pixels have to be RGB. """ - - with MSS._lock: - return self._grab_impl(monitor) diff --git a/mss/windows.py b/mss/windows.py index ca5a16b8..8d3fc1c4 100644 --- a/mss/windows.py +++ b/mss/windows.py @@ -76,10 +76,7 @@ class MSS(MSSBase): memdc = None # A dict to maintain *srcdc* values created by multiple threads. - srcdc_dict = {} # type: Dict[threading.Thread, int] - - # A threading lock to lock resources(bmp/memdc/srcdc) inside .grab() method. - _lock = threading.Lock() + _srcdc_dict = {} # type: Dict[threading.Thread, int] def __init__(self, **_): # type: (Any) -> None @@ -185,104 +182,89 @@ def _get_srcdc(self): Retrieve a thread-safe HDC from GetWindowDC(). In multithreading, if the thread who creates *srcdc* is dead, *srcdc* will no longer be valid to grab the screen. The *srcdc* attribute is replaced - with *srcdc_dict* to maintain the *srcdc* values in multithreading. + with *_srcdc_dict* to maintain the *srcdc* values in multithreading. Since the current thread and main thread are always alive, reuse their *srcdc* value first. """ cur_thread, main_thread = threading.current_thread(), threading.main_thread() - srcdc = MSS.srcdc_dict.get(cur_thread) or MSS.srcdc_dict.get(main_thread) + srcdc = MSS._srcdc_dict.get(cur_thread) or MSS._srcdc_dict.get(main_thread) if not srcdc: - srcdc = MSS.srcdc_dict[cur_thread] = self.user32.GetWindowDC(0) + srcdc = MSS._srcdc_dict[cur_thread] = self.user32.GetWindowDC(0) return srcdc - @property - def monitors(self): - # type: () -> Monitors - """ Get positions of monitors (see parent class). """ + def _monitors_impl(self): + # type: () -> None + """ Get positions of monitors. It will populate self._monitors. """ + + int_ = int + user32 = self.user32 + get_system_metrics = user32.GetSystemMetrics + + # All monitors + self._monitors.append( + { + "left": int_(get_system_metrics(76)), # SM_XVIRTUALSCREEN + "top": int_(get_system_metrics(77)), # SM_YVIRTUALSCREEN + "width": int_(get_system_metrics(78)), # SM_CXVIRTUALSCREEN + "height": int_(get_system_metrics(79)), # SM_CYVIRTUALSCREEN + } + ) - if not self._monitors: - int_ = int - user32 = self.user32 - get_system_metrics = user32.GetSystemMetrics + # Each monitors + def _callback(monitor, data, rect, dc_): + # types: (int, HDC, LPRECT, LPARAM) -> int + """ + Callback for monitorenumproc() function, it will return + a RECT with appropriate values. + """ + # pylint: disable=unused-argument - # All monitors + rct = rect.contents self._monitors.append( { - "left": int_(get_system_metrics(76)), # SM_XVIRTUALSCREEN - "top": int_(get_system_metrics(77)), # SM_YVIRTUALSCREEN - "width": int_(get_system_metrics(78)), # SM_CXVIRTUALSCREEN - "height": int_(get_system_metrics(79)), # SM_CYVIRTUALSCREEN + "left": int_(rct.left), + "top": int_(rct.top), + "width": int_(rct.right - rct.left), + "height": int_(rct.bottom - rct.top), } ) + return 1 + + callback = self.monitorenumproc(_callback) + user32.EnumDisplayMonitors(0, 0, callback, 0) - # Each monitors - def _callback(monitor, data, rect, dc_): - # types: (int, HDC, LPRECT, LPARAM) -> int - """ - Callback for monitorenumproc() function, it will return - a RECT with appropriate values. - """ - # pylint: disable=unused-argument - - rct = rect.contents - self._monitors.append( - { - "left": int_(rct.left), - "top": int_(rct.top), - "width": int_(rct.right - rct.left), - "height": int_(rct.bottom - rct.top), - } - ) - return 1 - - callback = self.monitorenumproc(_callback) - user32.EnumDisplayMonitors(0, 0, callback, 0) - - return self._monitors - - def grab(self, monitor): + def _grab_impl(self, monitor): # type: (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 few interesting things: + In the code, there are few interesting things: - [1] bmi.bmiHeader.biHeight = -height + [1] bmi.bmiHeader.biHeight = -height - A bottom-up DIB is specified by setting the height to a - positive number, while a top-down DIB is specified by - setting the height to a negative number. - https://msdn.microsoft.com/en-us/library/ms787796.aspx - https://msdn.microsoft.com/en-us/library/dd144879%28v=vs.85%29.aspx + A bottom-up DIB is specified by setting the height to a + positive number, while a top-down DIB is specified by + setting the height to a negative number. + https://msdn.microsoft.com/en-us/library/ms787796.aspx + https://msdn.microsoft.com/en-us/library/dd144879%28v=vs.85%29.aspx - [2] bmi.bmiHeader.biBitCount = 32 - image_data = create_string_buffer(height * width * 4) + [2] bmi.bmiHeader.biBitCount = 32 + image_data = create_string_buffer(height * width * 4) - We grab the image in RGBX mode, so that each word is 32bit - and we have no striding, then we transform to RGB. - Inspired by https://github.com/zoofIO/flexx + We grab the image in RGBX mode, so that each word is 32bit + and we have no striding. + Inspired by https://github.com/zoofIO/flexx - [3] bmi.bmiHeader.biClrUsed = 0 - bmi.bmiHeader.biClrImportant = 0 + [3] bmi.bmiHeader.biClrUsed = 0 + bmi.bmiHeader.biClrImportant = 0 - When biClrUsed and biClrImportant are set to zero, there - is "no" color table, so we can read the pixels of the bitmap - retrieved by gdi32.GetDIBits() as a sequence of RGB values. - Thanks to http://stackoverflow.com/a/3688682 + When biClrUsed and biClrImportant are set to zero, there + is "no" color table, so we can read the pixels of the bitmap + retrieved by gdi32.GetDIBits() as a sequence of RGB values. + Thanks to http://stackoverflow.com/a/3688682 """ - # Acquire lock to prevent resources from being modified by multiple threads at same time. - MSS._lock.acquire() - - # Convert PIL bbox style - if isinstance(monitor, tuple): - monitor = { - "left": monitor[0], - "top": monitor[1], - "width": monitor[2] - monitor[0], - "height": monitor[3] - monitor[1], - } - srcdc, memdc = self._get_srcdc(), MSS.memdc width, height = monitor["width"], monitor["height"] @@ -310,7 +292,6 @@ def grab(self, monitor): bits = self.gdi32.GetDIBits( memdc, MSS.bmp, 0, height, self._data, self._bmi, DIB_RGB_COLORS ) - MSS._lock.release() if bits != height: raise ScreenShotError("gdi32.GetDIBits() failed.") diff --git a/setup.cfg b/setup.cfg index fcc5739a..ed72d80c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = mss -version = 5.1.1 +version = 6.0.0 author = Mickaël 'Tiger-222' Schoentgen author-email = contact@tiger-222.fr description = An ultra fast cross-platform multiple screenshots module in pure python using ctypes. From f84c6dd41969775f7acc2d28690aad7854580d8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Sun, 28 Jun 2020 18:59:33 +0200 Subject: [PATCH 025/280] Ignore 'venv' folder --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 080d5f82..e9437f3a 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,4 @@ dist/ .vscode docs/output/ .mypy_cache/ +venv/ From 3ec89c5a04cf4fbb6e4dafc7229c65eec39190ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Sun, 28 Jun 2020 19:24:37 +0200 Subject: [PATCH 026/280] tests: fixed a random bug on test_grab_with_tuple_percents() (fixes #142) --- CHANGELOG | 1 + setup.cfg | 3 +++ tox.ini | 1 + 3 files changed, 5 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index d38d83be..142f0f18 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -6,6 +6,7 @@ History: - removed usage of deprecated "license_file" option for "license_files" - fixed flake8 usage in pre-commit - MSS: the implementation is now thread-safe on all OSes (fixes #169) + - tests: fixed a random bug on test_grab_with_tuple_percents() (fixes #142) - :heart: contributors: @ 5.1.0 2020/04/30 diff --git a/setup.cfg b/setup.cfg index ed72d80c..54a72835 100644 --- a/setup.cfg +++ b/setup.cfg @@ -55,3 +55,6 @@ addopts = --failed-first -r fE -v + # Trait all tests as flaky by default + --force-flaky + --no-success-flaky-report diff --git a/tox.ini b/tox.ini index 6abf8e54..ff334454 100644 --- a/tox.ini +++ b/tox.ini @@ -10,6 +10,7 @@ skip_missing_interpreters = True passenv = DISPLAY alwayscopy = True deps = + flaky pytest # Must pin that version to support PyPy3 pypy3: numpy==1.15.4 From e0afd3e6e42d4f1636b8d91f6f7245fcaf469321 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Mon, 29 Jun 2020 13:36:46 +0200 Subject: [PATCH 027/280] The module is now available on conda (closes #170) Links: - https://anaconda.org/conda-forge/python-mss - https://github.com/conda-forge/python-mss-feedstock --- CHANGELOG | 1 + README.rst | 6 ++++++ docs/source/installation.rst | 6 ++++++ 3 files changed, 13 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index 142f0f18..9dd1c584 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -5,6 +5,7 @@ History: 6.0.0 2020/xx/xx - removed usage of deprecated "license_file" option for "license_files" - fixed flake8 usage in pre-commit + - the module is now available on conda (closes #170) - MSS: the implementation is now thread-safe on all OSes (fixes #169) - tests: fixed a random bug on test_grab_with_tuple_percents() (fixes #142) - :heart: contributors: @ diff --git a/README.rst b/README.rst index 228b6e4c..e076e615 100644 --- a/README.rst +++ b/README.rst @@ -9,6 +9,8 @@ Python MSS :target: https://saythanks.io/to/BoboTiG .. image:: https://pepy.tech/badge/mss :target: https://pepy.tech/project/mss +.. image:: https://anaconda.org/conda-forge/python-mss/badges/installer/conda.svg + :target: https://anaconda.org/conda-forge/python-mss .. code-block:: python @@ -41,3 +43,7 @@ Installation You can install it with pip:: python -m pip install -U --user mss + +Or you can install it with conda:: + + conda install -c conda-forge python-mss diff --git a/docs/source/installation.rst b/docs/source/installation.rst index b4dc029a..0dae108e 100644 --- a/docs/source/installation.rst +++ b/docs/source/installation.rst @@ -11,6 +11,12 @@ Quite simple:: $ python -m pip install -U --user mss +Conda Package +------------- + +The module is also available from conda:: + + $ conda install -c conda-forge python-mss From Sources ============ From 89a1a395f7190158680d66904ec05aab39d37429 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Mon, 29 Jun 2020 14:12:37 +0200 Subject: [PATCH 028/280] Linux: better handling of the Xrandr extension (fixes #168) --- CHANGELOG | 1 + CHANGES.rst | 1 + mss/base.py | 1 + mss/linux.py | 33 ++++++++++++++++++++++++++++++++- mss/tests/test_gnu_linux.py | 7 +++++++ 5 files changed, 42 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 9dd1c584..ef99b12c 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -7,6 +7,7 @@ History: - fixed flake8 usage in pre-commit - the module is now available on conda (closes #170) - MSS: the implementation is now thread-safe on all OSes (fixes #169) + - Linux: better handling of the Xrandr extension (fixes #168) - tests: fixed a random bug on test_grab_with_tuple_percents() (fixes #142) - :heart: contributors: @ diff --git a/CHANGES.rst b/CHANGES.rst index 9a12dda8..20559956 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -16,6 +16,7 @@ darwin.py linux.py -------- + - Added ``MSS.has_extension()`` - Renamed ``MSS.grab()`` to ``MSS._grab_impl()`` - Renamed ``MSS.monitors`` to ``MSS._monitors_impl()`` diff --git a/mss/base.py b/mss/base.py index c5b5f626..5510cbb2 100644 --- a/mss/base.py +++ b/mss/base.py @@ -13,6 +13,7 @@ from .tools import to_png if TYPE_CHECKING: + # pylint: disable=ungrouped-imports from typing import Any, Callable, Iterator, List, Optional, Type # noqa from .models import Monitor, Monitors # noqa diff --git a/mss/linux.py b/mss/linux.py index 48b6d980..cd21f0cc 100644 --- a/mss/linux.py +++ b/mss/linux.py @@ -10,7 +10,7 @@ from types import SimpleNamespace from typing import TYPE_CHECKING -from .base import MSSBase +from .base import MSSBase, lock from .exception import ScreenShotError if TYPE_CHECKING: @@ -227,10 +227,36 @@ def __init__(self, display=None): self.root = self.xlib.XDefaultRootWindow(self._get_display(display)) + if not self.has_extension("RANDR"): + raise ScreenShotError("No Xrandr extension found.") + # Fix for XRRGetScreenResources and XGetImage: # expected LP_Display instance instead of LP_XWindowAttributes self.drawable = ctypes.cast(self.root, ctypes.POINTER(Display)) + def has_extension(self, extension): + # type: (str) -> bool + """Return True if the given *extension* is part of the extensions list of the server.""" + with lock: + byref = ctypes.byref + c_int = ctypes.c_int + major_opcode_return = c_int() + first_event_return = c_int() + first_error_return = c_int() + + try: + self.xlib.XQueryExtension( + self._get_display(), + extension.encode("latin1"), + byref(major_opcode_return), + byref(first_event_return), + byref(first_error_return), + ) + except ScreenShotError: + return False + else: + return True + def _get_display(self, disp=None): """ Retrieve a thread-safe display from XOpenDisplay(). @@ -297,6 +323,11 @@ def cfactory(func, argtypes, restype, attr=self.xlib): pointer(XImage), ) cfactory("XDestroyImage", [pointer(XImage)], void) + cfactory( + "XQueryExtension", + [pointer(Display), char_p, pointer(c_int), pointer(c_int), pointer(c_int)], + uint, + ) # A simple benchmark calling 10 times those 2 functions: # XRRGetScreenResources(): 0.1755971429956844 s diff --git a/mss/tests/test_gnu_linux.py b/mss/tests/test_gnu_linux.py index 6f40b2a2..6246277f 100644 --- a/mss/tests/test_gnu_linux.py +++ b/mss/tests/test_gnu_linux.py @@ -121,3 +121,10 @@ def test_region_out_of_monitor_bounds(): details = sct.get_error_details() assert details["xerror"] assert isinstance(details["xerror_details"], dict) + + +def test_has_extension(): + display = os.getenv("DISPLAY") + with mss.mss(display=display) as sct: + assert sct.has_extension("RANDR") + assert not sct.has_extension("NOEXT") From c03570b1829ccc4adfeee8cd88ac785f69d65d46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Tue, 30 Jun 2020 17:37:41 +0200 Subject: [PATCH 029/280] Remove the now unused MSS.display class variable from GNU/Linux implementation Finalisation of 59c22592f6c3cced68490bb0085af545b3e777a6. --- CHANGES.rst | 1 + mss/linux.py | 4 ---- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 20559956..bb7742fd 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -17,6 +17,7 @@ darwin.py linux.py -------- - Added ``MSS.has_extension()`` + - Removed ``MSS.display`` - Renamed ``MSS.grab()`` to ``MSS._grab_impl()`` - Renamed ``MSS.monitors`` to ``MSS._monitors_impl()`` diff --git a/mss/linux.py b/mss/linux.py index cd21f0cc..44a792be 100644 --- a/mss/linux.py +++ b/mss/linux.py @@ -184,10 +184,6 @@ class MSS(MSSBase): __slots__ = {"drawable", "root", "xlib", "xrandr"} - # Class attribute to store the display opened with XOpenDisplay(). - # Instancied one time to prevent resource leak. - display = None - # A dict to maintain *display* values created by multiple threads. _display_dict = {} # type: Dict[threading.Thread, int] From e36db0a84b265f84cf5a3cd4d41e669b4ae7cb1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Tue, 30 Jun 2020 17:59:24 +0200 Subject: [PATCH 030/280] Release 6.0.0 --- CHANGELOG | 2 +- CHANGES.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index ef99b12c..22e1c719 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -2,7 +2,7 @@ History: -6.0.0 2020/xx/xx +6.0.0 2020/06/30 - removed usage of deprecated "license_file" option for "license_files" - fixed flake8 usage in pre-commit - the module is now available on conda (closes #170) diff --git a/CHANGES.rst b/CHANGES.rst index bb7742fd..ec182380 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,4 +1,4 @@ -6.0.0 (2020-xx-xx) +6.0.0 (2020-06-30) ================== base.py From 7f69d87cc0273157ebf73acf0dc43b9a24778a75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Tue, 30 Jun 2020 18:02:50 +0200 Subject: [PATCH 031/280] Bump the version to 6.0.1 --- CHANGELOG | 4 +++- CHANGES.rst | 6 ++++++ docs/source/conf.py | 2 +- mss/__init__.py | 2 +- setup.cfg | 2 +- 5 files changed, 12 insertions(+), 4 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 22e1c719..0e5a29cd 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -2,6 +2,9 @@ History: +6.0.1 2020/xx/xx + - :heart: contributors: @ + 6.0.0 2020/06/30 - removed usage of deprecated "license_file" option for "license_files" - fixed flake8 usage in pre-commit @@ -9,7 +12,6 @@ History: - MSS: the implementation is now thread-safe on all OSes (fixes #169) - Linux: better handling of the Xrandr extension (fixes #168) - tests: fixed a random bug on test_grab_with_tuple_percents() (fixes #142) - - :heart: contributors: @ 5.1.0 2020/04/30 - produce wheels for Python 3 only diff --git a/CHANGES.rst b/CHANGES.rst index ec182380..054b24fa 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,9 @@ +6.0.1 (2020-xx-xx) +================== + + + + 6.0.0 (2020-06-30) ================== diff --git a/docs/source/conf.py b/docs/source/conf.py index 343c74e6..c27c6e5a 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -27,7 +27,7 @@ # built documents. # # The short X.Y version. -version = "6.0.0" +version = "6.0.1" # The full version, including alpha/beta/rc tags. release = "latest" diff --git a/mss/__init__.py b/mss/__init__.py index a88db5c3..ca703af7 100644 --- a/mss/__init__.py +++ b/mss/__init__.py @@ -12,7 +12,7 @@ from .exception import ScreenShotError from .factory import mss -__version__ = "6.0.0" +__version__ = "6.0.1" __author__ = "Mickaël 'Tiger-222' Schoentgen" __copyright__ = """ Copyright (c) 2013-2020, Mickaël 'Tiger-222' Schoentgen diff --git a/setup.cfg b/setup.cfg index 54a72835..2001b247 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = mss -version = 6.0.0 +version = 6.0.1 author = Mickaël 'Tiger-222' Schoentgen author-email = contact@tiger-222.fr description = An ultra fast cross-platform multiple screenshots module in pure python using ctypes. From d6a229b2f0110470a1c0d28c3123a33701d1fd2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Sun, 16 Aug 2020 22:42:30 +0200 Subject: [PATCH 032/280] Mac: reduce the number of function calls The `int()` builtin function is no more called. This is a small improvement. --- CHANGELOG | 1 + mss/darwin.py | 17 ++++++++--------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 0e5a29cd..b42a4546 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -3,6 +3,7 @@ History: 6.0.1 2020/xx/xx + - Mac: reduce the number of function calls - :heart: contributors: @ 6.0.0 2020/06/30 diff --git a/mss/darwin.py b/mss/darwin.py index 6434e8fb..891cd2bb 100644 --- a/mss/darwin.py +++ b/mss/darwin.py @@ -92,7 +92,6 @@ def cfactory(func, argtypes, restype): uint32 = ctypes.c_uint32 void = ctypes.c_void_p - size_t = ctypes.c_size_t pointer = ctypes.POINTER cfactory( @@ -109,14 +108,14 @@ def cfactory(func, argtypes, restype): argtypes=[CGRect, uint32, uint32, uint32], restype=void, ) - cfactory(func="CGImageGetWidth", argtypes=[void], restype=size_t) - cfactory(func="CGImageGetHeight", argtypes=[void], restype=size_t) + cfactory(func="CGImageGetWidth", argtypes=[void], restype=int) + cfactory(func="CGImageGetHeight", argtypes=[void], restype=int) cfactory(func="CGImageGetDataProvider", argtypes=[void], restype=void) cfactory(func="CGDataProviderCopyData", argtypes=[void], restype=void) cfactory(func="CFDataGetBytePtr", argtypes=[void], restype=void) cfactory(func="CFDataGetLength", argtypes=[void], restype=ctypes.c_uint64) - cfactory(func="CGImageGetBytesPerRow", argtypes=[void], restype=size_t) - cfactory(func="CGImageGetBitsPerPixel", argtypes=[void], restype=size_t) + cfactory(func="CGImageGetBytesPerRow", argtypes=[void], restype=int) + cfactory(func="CGImageGetBitsPerPixel", argtypes=[void], restype=int) cfactory(func="CGDataProviderRelease", argtypes=[void], restype=void) cfactory(func="CFRelease", argtypes=[void], restype=void) @@ -183,8 +182,8 @@ def _grab_impl(self, monitor): if not image_ref: raise ScreenShotError("CoreGraphics.CGWindowListCreateImage() failed.") - width = int(core.CGImageGetWidth(image_ref)) - height = int(core.CGImageGetHeight(image_ref)) + width = core.CGImageGetWidth(image_ref) + height = core.CGImageGetHeight(image_ref) prov = copy_data = None try: prov = core.CGImageGetDataProvider(image_ref) @@ -195,8 +194,8 @@ def _grab_impl(self, monitor): data = bytearray(raw.contents) # Remove padding per row - bytes_per_row = int(core.CGImageGetBytesPerRow(image_ref)) - bytes_per_pixel = int(core.CGImageGetBitsPerPixel(image_ref)) + bytes_per_row = core.CGImageGetBytesPerRow(image_ref) + bytes_per_pixel = core.CGImageGetBitsPerPixel(image_ref) bytes_per_pixel = (bytes_per_pixel + 7) // 8 if bytes_per_pixel * width != bytes_per_row: From 44f7e5491a3196b0fd786b6eb7cc09768b6c9734 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Sun, 16 Aug 2020 23:09:30 +0200 Subject: [PATCH 033/280] tests: fix macOS intepreter not found on Travis-CI --- .travis.yml | 106 ++++++++++++++++++++++++--------------------- .travis/install.sh | 33 -------------- CHANGELOG | 1 + tox.ini | 5 +-- 4 files changed, 60 insertions(+), 85 deletions(-) delete mode 100644 .travis/install.sh diff --git a/.travis.yml b/.travis.yml index 7c3a51cd..6fb8adb1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,80 +1,88 @@ +# Beta opt-in +# https://docs.travis-ci.com/user/build-config-validation#beta-opt-in +version: ~> 1.0 + language: python dist: xenial +os: linux + +env: + global: + - MAKEFLAGS="-j 2" -matrix: +jobs: fast_finish: true include: - name: Code quality checks - os: linux python: "3.8" env: TOXENV=lint - name: Types checking - os: linux python: "3.8" env: TOXENV=types - name: Documentation build - os: linux python: "3.8" env: TOXENV=docs - - os: osx - language: shell - before_install: - - bash .travis/install.sh - env: - - PYTHON_VERSION=3.5 - - TOXENV=py35 - - name: "Python 3.6 on macOS 10.13" + - name: Python 3.5 on macOS os: osx - osx_image: xcode9.4 # Python 3.6.5 running on macOS 10.13 language: shell - env: - - PYTHON_VERSION=3.6 - - TOXENV=py36 - - name: "Python 3.7 on macOS 10.14" + install: + - eval "$(pyenv init -)" + - pyenv install --skip-existing 3.5.8 + - pyenv global 3.5.8 + env: TOXENV=py35 + - name: Python 3.6 on macOS os: osx - osx_image: xcode11.2 # Python 3.7.4 running on macOS 10.14.4 language: shell - env: - - PYTHON_VERSION=3.7 - - TOXENV=py37 - - name: "Python 3.8 on macOS 10.14" + install: + - eval "$(pyenv init -)" + - pyenv install --skip-existing 3.6.9 + - pyenv global system 3.6.9 + env: TOXENV=py36 + - name: Python 3.7 on macOS os: osx - osx_image: xcode11.3 # Python 3.8.0 running on macOS 10.14.6 language: shell - env: - - PYTHON_VERSION=3.8 - - TOXENV=py38 - - os: osx + install: + - eval "$(pyenv init -)" + - pyenv install --skip-existing 3.7.5 + - pyenv global system 3.7.5 + env: TOXENV=py37 + - name: Python 3.8 on macOS + os: osx language: shell - before_install: - - bash .travis/install.sh - env: - - PYTHON_VERSION=3.9 - - TOXENV=py39 - - name: "PyPy 3.6 on GNU/Linux" - os: linux - python: "pypy3" + install: + - eval "$(pyenv init -)" + - pyenv install --skip-existing 3.8.0 + - pyenv global system 3.8.0 + env: TOXENV=py38 + # - name: Python 3.9 on macOS + # os: osx + # language: shell + # install: + # - eval "$(pyenv init -)" + # - pyenv install --skip-existing 3.9-dev + # - pyenv global system 3.9-dev + # env: TOXENV=py39 + - name: PyPy 3.6 on GNU/Linux + python: pypy3 env: TOXENV=pypy3 - - name: "Python 3.5 on GNU/Linux" - os: linux + - name: Python 3.5 on GNU/Linux python: "3.5" env: TOXENV=py35 - - name: "Python 3.6 on GNU/Linux" - os: linux + - name: Python 3.6 on GNU/Linux python: "3.6" env: TOXENV=py36 - - name: "Python 3.7 on GNU/Linux" - os: linux + - name: Python 3.7 on GNU/Linux python: "3.7" env: TOXENV=py37 - - name: "Python 3.8 on GNU/Linux" - os: linux + - name: Python 3.8 on GNU/Linux python: "3.8" env: TOXENV=py38 - - name: "Python 3.9 on GNU/Linux" - os: linux - python: "nightly" + - name: Python 3.9 on GNU/Linux + python: 3.9-dev env: TOXENV=py39 + # - name: Python 3.10 on GNU/Linux + # python: nightly + # env: TOXENV=py310 addons: apt: @@ -84,8 +92,8 @@ addons: services: - xvfb -install: - - python -m pip install --upgrade pip tox +before_script: + - python3 -m pip install --upgrade pip tox script: - - tox + - python3 -m tox diff --git a/.travis/install.sh b/.travis/install.sh deleted file mode 100644 index 4956d58a..00000000 --- a/.travis/install.sh +++ /dev/null @@ -1,33 +0,0 @@ -#!/bin/bash -# Taken largely from https://stackoverflow.com/q/45257534 -# Install or upgrade to Python 3 -brew update 1>/dev/null -brew upgrade python -# Create and activate a virtualenv for conda -virtualenv -p python3 condavenv -source condavenv/bin/activate -# Grab Miniconda 3 -wget https://repo.continuum.io/miniconda/Miniconda3-latest-MacOSX-x86_64.sh -O miniconda.sh - -# Install our version of miniconda -bash miniconda.sh -b -p $HOME/miniconda -# Modify the PATH, even though this doesn't seem to be effective later on -export PATH="$HOME/miniconda/bin:$PATH" -hash -r -# Configure conda to act non-interactively -conda config --set always_yes yes --set changeps1 no -# Update conda to the latest and greatest -conda update -q conda -# Enable conda-forge for binary packages, if necessary -conda config --add channels conda-forge -# Useful for debugging any issues with conda -conda info -a -echo "Creating conda virtualenv with Python $PYTHON_VERSION" -conda create -n venv python=$PYTHON_VERSION -# For whatever reason, source is not finding the activate script unless we -# specify the full path to it -source $HOME/miniconda/bin/activate venv -# This is the Python that will be used for running tests, so we dump its -# version here to help with troubleshooting -which python -python --version diff --git a/CHANGELOG b/CHANGELOG index b42a4546..204b44bf 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -4,6 +4,7 @@ History: 6.0.1 2020/xx/xx - Mac: reduce the number of function calls + - tests: fix macOS intepreter not found on Travis-CI - :heart: contributors: @ 6.0.0 2020/06/30 diff --git a/tox.ini b/tox.ini index ff334454..8ab0abf5 100644 --- a/tox.ini +++ b/tox.ini @@ -3,8 +3,7 @@ envlist = lint types docs - py{39,38,37,36,35,py3} -skip_missing_interpreters = True + py{310,39,38,37,36,35,py3} [testenv] passenv = DISPLAY @@ -14,7 +13,7 @@ deps = pytest # Must pin that version to support PyPy3 pypy3: numpy==1.15.4 - py{39,38,37,36,35}: numpy + py3{10,9,8,7,6,5}: numpy pillow wheel commands = From a546e14a736b3aceb80cb565f5f6b56f46399d62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Sun, 11 Oct 2020 01:00:33 +0200 Subject: [PATCH 034/280] Mac: support macOS Big Sur (fixes #178) --- CHANGELOG | 1 + mss/base.py | 1 + mss/darwin.py | 17 ++++++++++++++--- mss/linux.py | 1 + mss/screenshot.py | 1 + mss/tests/test_gnu_linux.py | 3 ++- mss/tests/test_macos.py | 11 +++++++---- 7 files changed, 27 insertions(+), 8 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 204b44bf..9fe6668a 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -4,6 +4,7 @@ History: 6.0.1 2020/xx/xx - Mac: reduce the number of function calls + - Mac: support macOS Big Sur (fixes #178) - tests: fix macOS intepreter not found on Travis-CI - :heart: contributors: @ diff --git a/mss/base.py b/mss/base.py index 5510cbb2..ed22b90a 100644 --- a/mss/base.py +++ b/mss/base.py @@ -164,6 +164,7 @@ def save(self, mon=0, output="monitor-{mon}.png", callback=None): try: monitor = monitors[mon] except IndexError: + # pylint: disable=raise-missing-from raise ScreenShotError("Monitor {!r} does not exist.".format(mon)) output = output.format(mon=mon, date=datetime.now(), **monitor) diff --git a/mss/darwin.py b/mss/darwin.py index 891cd2bb..d4f7b247 100644 --- a/mss/darwin.py +++ b/mss/darwin.py @@ -6,6 +6,7 @@ import ctypes import ctypes.util import sys +from platform import mac_ver from typing import TYPE_CHECKING from .base import MSSBase @@ -72,13 +73,23 @@ def __init__(self, **_): self.max_displays = 32 - coregraphics = ctypes.util.find_library("CoreGraphics") + self._init_library() + self._set_cfunctions() + + def _init_library(self): + """ Load the CoreGraphics library. """ + version = float(".".join(mac_ver()[0].split(".")[:2])) + if version < 10.16: + coregraphics = ctypes.util.find_library("CoreGraphics") + else: + # macOS Big Sur and newer + # pylint: disable=line-too-long + coregraphics = "/System/Library/Frameworks/CoreGraphics.framework/Versions/Current/CoreGraphics" + if not coregraphics: raise ScreenShotError("No CoreGraphics library found.") self.core = ctypes.cdll.LoadLibrary(coregraphics) - self._set_cfunctions() - def _set_cfunctions(self): # type: () -> None """ Set all ctypes functions and attach them to attributes. """ diff --git a/mss/linux.py b/mss/linux.py index 44a792be..b38872ee 100644 --- a/mss/linux.py +++ b/mss/linux.py @@ -197,6 +197,7 @@ def __init__(self, display=None): try: display = os.environ["DISPLAY"].encode("utf-8") except KeyError: + # pylint: disable=raise-missing-from raise ScreenShotError("$DISPLAY not set.") if not isinstance(display, bytes): diff --git a/mss/screenshot.py b/mss/screenshot.py index 0e810169..6ed6e9ba 100644 --- a/mss/screenshot.py +++ b/mss/screenshot.py @@ -152,6 +152,7 @@ def pixel(self, coord_x, coord_y): try: return self.pixels[coord_y][coord_x] # type: ignore except IndexError: + # pylint: disable=raise-missing-from raise ScreenShotError( "Pixel location ({}, {}) is out of range.".format(coord_x, coord_y) ) diff --git a/mss/tests/test_gnu_linux.py b/mss/tests/test_gnu_linux.py index 6246277f..f5d300e2 100644 --- a/mss/tests/test_gnu_linux.py +++ b/mss/tests/test_gnu_linux.py @@ -37,7 +37,8 @@ def test_factory_systems(monkeypatch): # macOS monkeypatch.setattr(platform, "system", lambda: "Darwin") - with pytest.raises(ScreenShotError): + with pytest.raises((ScreenShotError, ValueError)): + # ValueError on macOS Big Sur mss.mss() monkeypatch.undo() diff --git a/mss/tests/test_macos.py b/mss/tests/test_macos.py index 9a6f8871..1cf4e4c8 100644 --- a/mss/tests/test_macos.py +++ b/mss/tests/test_macos.py @@ -44,10 +44,13 @@ def test_repr(): def test_implementation(monkeypatch): # No `CoreGraphics` library - monkeypatch.setattr(ctypes.util, "find_library", lambda x: None) - with pytest.raises(ScreenShotError): - mss.mss() - monkeypatch.undo() + version = float(".".join(platform.mac_ver()[0].split(".")[:2])) + + if version < 10.16: + monkeypatch.setattr(ctypes.util, "find_library", lambda x: None) + with pytest.raises(ScreenShotError): + mss.mss() + monkeypatch.undo() with mss.mss() as sct: # Test monitor's rotation From 2b51323730e1cddc932033e58229e8d762271121 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Sun, 11 Oct 2020 01:58:59 +0200 Subject: [PATCH 035/280] Reworked all MSS._set_cfunctions() --- mss/darwin.py | 57 ++++++++++---------- mss/linux.py | 138 +++++++++++++++++++++++++++---------------------- mss/windows.py | 73 ++++++++++---------------- 3 files changed, 130 insertions(+), 138 deletions(-) diff --git a/mss/darwin.py b/mss/darwin.py index d4f7b247..da7989ea 100644 --- a/mss/darwin.py +++ b/mss/darwin.py @@ -94,41 +94,36 @@ def _set_cfunctions(self): # type: () -> None """ Set all ctypes functions and attach them to attributes. """ - def cfactory(func, argtypes, restype): - # type: (str, List[Any], Any) -> None - """ Factorize ctypes creations. """ - self._cfactory( - attr=self.core, func=func, argtypes=argtypes, restype=restype - ) - uint32 = ctypes.c_uint32 void = ctypes.c_void_p pointer = ctypes.POINTER + cfactory = self._cfactory + core = self.core - cfactory( - func="CGGetActiveDisplayList", - argtypes=[uint32, pointer(uint32), pointer(uint32)], - restype=ctypes.c_int32, - ) - cfactory(func="CGDisplayBounds", argtypes=[uint32], restype=CGRect) - cfactory(func="CGRectStandardize", argtypes=[CGRect], restype=CGRect) - cfactory(func="CGRectUnion", argtypes=[CGRect, CGRect], restype=CGRect) - cfactory(func="CGDisplayRotation", argtypes=[uint32], restype=ctypes.c_float) - cfactory( - func="CGWindowListCreateImage", - argtypes=[CGRect, uint32, uint32, uint32], - restype=void, - ) - cfactory(func="CGImageGetWidth", argtypes=[void], restype=int) - cfactory(func="CGImageGetHeight", argtypes=[void], restype=int) - cfactory(func="CGImageGetDataProvider", argtypes=[void], restype=void) - cfactory(func="CGDataProviderCopyData", argtypes=[void], restype=void) - cfactory(func="CFDataGetBytePtr", argtypes=[void], restype=void) - cfactory(func="CFDataGetLength", argtypes=[void], restype=ctypes.c_uint64) - cfactory(func="CGImageGetBytesPerRow", argtypes=[void], restype=int) - cfactory(func="CGImageGetBitsPerPixel", argtypes=[void], restype=int) - cfactory(func="CGDataProviderRelease", argtypes=[void], restype=void) - cfactory(func="CFRelease", argtypes=[void], restype=void) + # Note: keep it sorted + for func, argtypes, restype in ( + ("CGDataProviderCopyData", [void], void), + ("CGDisplayBounds", [uint32], CGRect), + ("CGDisplayRotation", [uint32], ctypes.c_float), + ("CFDataGetBytePtr", [void], void), + ("CFDataGetLength", [void], ctypes.c_uint64), + ("CFRelease", [void], void), + ("CGDataProviderRelease", [void], void), + ( + "CGGetActiveDisplayList", + [uint32, pointer(uint32), pointer(uint32)], + ctypes.c_int32, + ), + ("CGImageGetBitsPerPixel", [void], int), + ("CGImageGetBytesPerRow", [void], int), + ("CGImageGetDataProvider", [void], void), + ("CGImageGetHeight", [void], int), + ("CGImageGetWidth", [void], int), + ("CGRectStandardize", [CGRect], CGRect), + ("CGRectUnion", [CGRect, CGRect], CGRect), + ("CGWindowListCreateImage", [CGRect, uint32, uint32, uint32], void), + ): + cfactory(attr=core, func=func, argtypes=argtypes, restype=restype) # type: ignore def _monitors_impl(self): # type: () -> None diff --git a/mss/linux.py b/mss/linux.py index b38872ee..eda171a8 100644 --- a/mss/linux.py +++ b/mss/linux.py @@ -277,17 +277,6 @@ def _set_cfunctions(self): See https://tronche.com/gui/x/xlib/function-index.html for details. """ - def cfactory(func, argtypes, restype, attr=self.xlib): - # type: (str, List[Any], Any, Any) -> None - """ Factorize ctypes creations. """ - self._cfactory( - attr=attr, - errcheck=validate, - func=func, - argtypes=argtypes, - restype=restype, - ) - void = ctypes.c_void_p c_int = ctypes.c_int uint = ctypes.c_uint @@ -295,36 +284,75 @@ def cfactory(func, argtypes, restype, attr=self.xlib): c_long = ctypes.c_long char_p = ctypes.c_char_p pointer = ctypes.POINTER + cfactory = self._cfactory + xlib = self.xlib + xrandr = self.xrandr - cfactory("XSetErrorHandler", [void], c_int) - cfactory("XGetErrorText", [pointer(Display), c_int, char_p, c_int], void) - cfactory("XOpenDisplay", [char_p], pointer(Display)) - cfactory("XDefaultRootWindow", [pointer(Display)], pointer(XWindowAttributes)) - cfactory( - "XGetWindowAttributes", - [pointer(Display), pointer(XWindowAttributes), pointer(XWindowAttributes)], - c_int, - ) - cfactory( - "XGetImage", - [ - pointer(Display), - pointer(Display), - c_int, + # Note: keep it sorted + for attr, func, argtypes, restype in ( + ( + xlib, + "XDefaultRootWindow", + [pointer(Display)], + pointer(XWindowAttributes), + ), + (xlib, "XDestroyImage", [pointer(XImage)], void), + (xlib, "XGetErrorText", [pointer(Display), c_int, char_p, c_int], void), + ( + xlib, + "XGetImage", + [ + pointer(Display), + pointer(Display), + c_int, + c_int, + uint, + uint, + ulong, + c_int, + ], + pointer(XImage), + ), + ( + xlib, + "XGetWindowAttributes", + [ + pointer(Display), + pointer(XWindowAttributes), + pointer(XWindowAttributes), + ], c_int, + ), + (xlib, "XOpenDisplay", [char_p], pointer(Display)), + ( + xlib, + "XQueryExtension", + [ + pointer(Display), + char_p, + pointer(c_int), + pointer(c_int), + pointer(c_int), + ], uint, - uint, - ulong, - c_int, - ], - pointer(XImage), - ) - cfactory("XDestroyImage", [pointer(XImage)], void) - cfactory( - "XQueryExtension", - [pointer(Display), char_p, pointer(c_int), pointer(c_int), pointer(c_int)], - uint, - ) + ), + (xlib, "XSetErrorHandler", [void], c_int), + (xrandr, "XRRFreeCrtcInfo", [pointer(XRRCrtcInfo)], void), + (xrandr, "XRRFreeScreenResources", [pointer(XRRScreenResources)], void), + ( + xrandr, + "XRRGetCrtcInfo", + [pointer(Display), pointer(XRRScreenResources), c_long], + pointer(XRRCrtcInfo), + ), + ): + cfactory( + attr=attr, + errcheck=validate, + func=func, + argtypes=argtypes, + restype=restype, + ) # type: ignore # A simple benchmark calling 10 times those 2 functions: # XRRGetScreenResources(): 0.1755971429956844 s @@ -332,33 +360,21 @@ def cfactory(func, argtypes, restype, attr=self.xlib): # The second is faster by a factor of 44! So try to use it first. try: cfactory( - "XRRGetScreenResourcesCurrent", - [pointer(Display), pointer(Display)], - pointer(XRRScreenResources), - attr=self.xrandr, + attr=xrandr, + func="XRRGetScreenResourcesCurrent", + errcheck=validate, + argtypes=[pointer(Display), pointer(Display)], + restype=pointer(XRRScreenResources), ) except AttributeError: cfactory( - "XRRGetScreenResources", - [pointer(Display), pointer(Display)], - pointer(XRRScreenResources), - attr=self.xrandr, + attr=xrandr, + func="XRRGetScreenResources", + errcheck=validate, + argtypes=[pointer(Display), pointer(Display)], + restype=pointer(XRRScreenResources), ) - self.xrandr.XRRGetScreenResourcesCurrent = self.xrandr.XRRGetScreenResources - - cfactory( - "XRRGetCrtcInfo", - [pointer(Display), pointer(XRRScreenResources), c_long], - pointer(XRRCrtcInfo), - attr=self.xrandr, - ) - cfactory( - "XRRFreeScreenResources", - [pointer(XRRScreenResources)], - void, - attr=self.xrandr, - ) - cfactory("XRRFreeCrtcInfo", [pointer(XRRCrtcInfo)], void, attr=self.xrandr) + xrandr.XRRGetScreenResourcesCurrent = xrandr.XRRGetScreenResources def get_error_details(self): # type: () -> Optional[Dict[str, Any]] diff --git a/mss/windows.py b/mss/windows.py index 8d3fc1c4..fb9c8d03 100644 --- a/mss/windows.py +++ b/mss/windows.py @@ -114,53 +114,34 @@ def _set_cfunctions(self): void = ctypes.c_void_p pointer = ctypes.POINTER + cfactory = self._cfactory + gdi32 = self.gdi32 + user32 = self.user32 - self._cfactory( - attr=self.user32, func="GetSystemMetrics", argtypes=[INT], restype=INT - ) - self._cfactory( - attr=self.user32, - func="EnumDisplayMonitors", - argtypes=[HDC, void, self.monitorenumproc, LPARAM], - restype=BOOL, - ) - self._cfactory( - attr=self.user32, func="GetWindowDC", argtypes=[HWND], restype=HDC - ) - - self._cfactory( - attr=self.gdi32, func="GetDeviceCaps", argtypes=[HWND, INT], restype=INT - ) - self._cfactory( - attr=self.gdi32, func="CreateCompatibleDC", argtypes=[HDC], restype=HDC - ) - self._cfactory( - attr=self.gdi32, - func="CreateCompatibleBitmap", - argtypes=[HDC, INT, INT], - restype=HBITMAP, - ) - self._cfactory( - attr=self.gdi32, - func="SelectObject", - argtypes=[HDC, HGDIOBJ], - restype=HGDIOBJ, - ) - self._cfactory( - attr=self.gdi32, - func="BitBlt", - argtypes=[HDC, INT, INT, INT, INT, HDC, INT, INT, DWORD], - restype=BOOL, - ) - self._cfactory( - attr=self.gdi32, func="DeleteObject", argtypes=[HGDIOBJ], restype=INT - ) - self._cfactory( - attr=self.gdi32, - func="GetDIBits", - argtypes=[HDC, HBITMAP, UINT, UINT, void, pointer(BITMAPINFO), UINT], - restype=BOOL, - ) + # Note: keep it sorted + for attr, func, argtypes, restype in ( + (gdi32, "BitBlt", [HDC, INT, INT, INT, INT, HDC, INT, INT, DWORD], BOOL), + (gdi32, "CreateCompatibleBitmap", [HDC, INT, INT], HBITMAP), + (gdi32, "CreateCompatibleDC", [HDC], HDC), + (gdi32, "DeleteObject", [HGDIOBJ], INT), + (gdi32, "GetDeviceCaps", [HWND, INT], INT), + ( + gdi32, + "GetDIBits", + [HDC, HBITMAP, UINT, UINT, void, pointer(BITMAPINFO), UINT], + BOOL, + ), + (gdi32, "SelectObject", [HDC, HGDIOBJ], HGDIOBJ), + ( + user32, + "EnumDisplayMonitors", + [HDC, void, self.monitorenumproc, LPARAM], + BOOL, + ), + (user32, "GetSystemMetrics", [INT], INT), + (user32, "GetWindowDC", [HWND], HDC), + ): + cfactory(attr=attr, func=func, argtypes=argtypes, restype=restype) # type: ignore def _set_dpi_awareness(self): """ Set DPI aware to capture full screen on Hi-DPI monitors. """ From cb517e938e4b8d939660001997a97fc2f752a6ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Sat, 17 Oct 2020 17:34:09 +0200 Subject: [PATCH 036/280] tests: fix test_entry_point() when there are several monitors --- CHANGELOG | 1 + mss/tests/test_implementation.py | 9 +++++---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 9fe6668a..797910f3 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -6,6 +6,7 @@ History: - Mac: reduce the number of function calls - Mac: support macOS Big Sur (fixes #178) - tests: fix macOS intepreter not found on Travis-CI + - tests: fix test_entry_point() when there are several monitors - :heart: contributors: @ 6.0.0 2020/06/30 diff --git a/mss/tests/test_implementation.py b/mss/tests/test_implementation.py index 62cc8d27..ba788ef5 100644 --- a/mss/tests/test_implementation.py +++ b/mss/tests/test_implementation.py @@ -96,11 +96,12 @@ def test_entry_point(capsys, sct): fmt = "sct-{width}x{height}.png" for opt in ("-o", "--out"): main([opt, fmt]) - filename = fmt.format(**sct.monitors[1]) out, _ = capsys.readouterr() - assert out.endswith(filename + "\n") - assert os.path.isfile(filename) - os.remove(filename) + for monitor, line in zip(sct.monitors[1:], out.splitlines()): + filename = fmt.format(**monitor) + assert line.endswith(filename) + assert os.path.isfile(filename) + os.remove(filename) fmt = "sct_{mon}-{date:%Y-%m-%d}.png" for opt in ("-o", "--out"): From 2fbd1f8bd82459635e478baaeef3200a3c37b9f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Sat, 17 Oct 2020 17:41:31 +0200 Subject: [PATCH 037/280] MSS: reworked how C functions are initialised --- CHANGELOG | 1 + docs/source/api.rst | 25 +++ mss/darwin.py | 100 ++++++---- mss/linux.py | 367 ++++++++++++++++++------------------ mss/tests/test_gnu_linux.py | 4 +- mss/windows.py | 82 ++++---- 6 files changed, 319 insertions(+), 260 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 797910f3..4e625d6c 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -3,6 +3,7 @@ History: 6.0.1 2020/xx/xx + - MSS: reworked how C functions are initialised - Mac: reduce the number of function calls - Mac: support macOS Big Sur (fixes #178) - tests: fix macOS intepreter not found on Travis-CI diff --git a/docs/source/api.rst b/docs/source/api.rst index a6ac98c5..49fdf113 100644 --- a/docs/source/api.rst +++ b/docs/source/api.rst @@ -5,11 +5,20 @@ MSS API Classes ======= +macOS +----- + +.. module:: mss.darwin + +.. attribute:: CFUNCTIONS + GNU/Linux --------- .. module:: mss.linux +.. attribute:: CFUNCTIONS + .. attribute:: ERROR :type: types.SimpleNamspacedict @@ -18,6 +27,10 @@ GNU/Linux .. versionadded:: 5.0.0 +.. attribute:: PLAINMASK + +.. attribute:: ZPIXMAP + .. class:: MSS .. method:: __init__([display=None]) @@ -76,6 +89,18 @@ GNU/Linux .. versionadded:: 3.3.0 +Windows +------- + +.. module:: mss.windows + +.. attribute:: CAPTUREBLT + +.. attribute:: CFUNCTIONS + +.. attribute:: DIB_RGB_COLORS + +.. attribute:: SRCCOPY Methods ======= diff --git a/mss/darwin.py b/mss/darwin.py index da7989ea..92516365 100644 --- a/mss/darwin.py +++ b/mss/darwin.py @@ -6,6 +6,17 @@ import ctypes import ctypes.util import sys +from ctypes import ( + POINTER, + Structure, + c_double, + c_float, + c_int32, + c_uint64, + c_ubyte, + c_uint32, + c_void_p, +) from platform import mac_ver from typing import TYPE_CHECKING @@ -23,13 +34,13 @@ def cgfloat(): - # type: () -> Union[Type[ctypes.c_double], Type[ctypes.c_float]] + # type: () -> Union[Type[c_double], Type[c_float]] """ Get the appropriate value for a float. """ - return ctypes.c_double if sys.maxsize > 2 ** 32 else ctypes.c_float + return c_double if sys.maxsize > 2 ** 32 else c_float -class CGPoint(ctypes.Structure): +class CGPoint(Structure): """ Structure that contains coordinates of a rectangle. """ _fields_ = [("x", cgfloat()), ("y", cgfloat())] @@ -38,7 +49,7 @@ def __repr__(self): return "{}(left={} top={})".format(type(self).__name__, self.x, self.y) -class CGSize(ctypes.Structure): +class CGSize(Structure): """ Structure that contains dimensions of an rectangle. """ _fields_ = [("width", cgfloat()), ("height", cgfloat())] @@ -49,7 +60,7 @@ def __repr__(self): ) -class CGRect(ctypes.Structure): +class CGRect(Structure): """ Structure that contains information about a rectangle. """ _fields_ = [("origin", CGPoint), ("size", CGSize)] @@ -58,6 +69,42 @@ def __repr__(self): return "{}<{} {}>".format(type(self).__name__, self.origin, self.size) +# 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 = { + "CGDataProviderCopyData": ("core", [c_void_p], c_void_p), + "CGDisplayBounds": ("core", [c_uint32], CGRect), + "CGDisplayRotation": ("core", [c_uint32], c_float), + "CFDataGetBytePtr": ("core", [c_void_p], c_void_p), + "CFDataGetLength": ("core", [c_void_p], c_uint64), + "CFRelease": ("core", [c_void_p], c_void_p), + "CGDataProviderRelease": ("core", [c_void_p], c_void_p), + "CGGetActiveDisplayList": ( + "core", + [c_uint32, POINTER(c_uint32), POINTER(c_uint32)], + c_int32, + ), + "CGImageGetBitsPerPixel": ("core", [c_void_p], int), + "CGImageGetBytesPerRow": ("core", [c_void_p], int), + "CGImageGetDataProvider": ("core", [c_void_p], c_void_p), + "CGImageGetHeight": ("core", [c_void_p], int), + "CGImageGetWidth": ("core", [c_void_p], int), + "CGRectStandardize": ("core", [CGRect], CGRect), + "CGRectUnion": ("core", [CGRect, CGRect], CGRect), + "CGWindowListCreateImage": ( + "core", + [CGRect, c_uint32, c_uint32, c_uint32], + c_void_p, + ), +} + + class MSS(MSSBase): """ Multiple ScreenShots implementation for macOS. @@ -94,36 +141,15 @@ def _set_cfunctions(self): # type: () -> None """ Set all ctypes functions and attach them to attributes. """ - uint32 = ctypes.c_uint32 - void = ctypes.c_void_p - pointer = ctypes.POINTER cfactory = self._cfactory - core = self.core - - # Note: keep it sorted - for func, argtypes, restype in ( - ("CGDataProviderCopyData", [void], void), - ("CGDisplayBounds", [uint32], CGRect), - ("CGDisplayRotation", [uint32], ctypes.c_float), - ("CFDataGetBytePtr", [void], void), - ("CFDataGetLength", [void], ctypes.c_uint64), - ("CFRelease", [void], void), - ("CGDataProviderRelease", [void], void), - ( - "CGGetActiveDisplayList", - [uint32, pointer(uint32), pointer(uint32)], - ctypes.c_int32, - ), - ("CGImageGetBitsPerPixel", [void], int), - ("CGImageGetBytesPerRow", [void], int), - ("CGImageGetDataProvider", [void], void), - ("CGImageGetHeight", [void], int), - ("CGImageGetWidth", [void], int), - ("CGRectStandardize", [CGRect], CGRect), - ("CGRectUnion", [CGRect, CGRect], CGRect), - ("CGWindowListCreateImage", [CGRect, uint32, uint32, uint32], void), - ): - cfactory(attr=core, func=func, argtypes=argtypes, restype=restype) # type: ignore + attrs = {"core": self.core} + for func, (attr, argtypes, restype) in CFUNCTIONS.items(): + cfactory( + attr=attrs[attr], + func=func, + argtypes=argtypes, # type: ignore + restype=restype, + ) def _monitors_impl(self): # type: () -> None @@ -139,8 +165,8 @@ def _monitors_impl(self): self._monitors.append({}) # Each monitors - display_count = ctypes.c_uint32(0) - active_displays = (ctypes.c_uint32 * self.max_displays)() + display_count = c_uint32(0) + active_displays = (c_uint32 * self.max_displays)() core.CGGetActiveDisplayList( self.max_displays, active_displays, ctypes.byref(display_count) ) @@ -196,7 +222,7 @@ def _grab_impl(self, monitor): copy_data = core.CGDataProviderCopyData(prov) data_ref = core.CFDataGetBytePtr(copy_data) buf_len = core.CFDataGetLength(copy_data) - raw = ctypes.cast(data_ref, ctypes.POINTER(ctypes.c_ubyte * buf_len)) + raw = ctypes.cast(data_ref, POINTER(c_ubyte * buf_len)) data = bytearray(raw.contents) # Remove padding per row diff --git a/mss/linux.py b/mss/linux.py index eda171a8..8e56c395 100644 --- a/mss/linux.py +++ b/mss/linux.py @@ -7,6 +7,21 @@ import ctypes.util import os import threading +from ctypes import ( + POINTER, + CFUNCTYPE, + Structure, + c_char_p, + c_int, + c_int32, + c_long, + c_ubyte, + c_uint, + c_uint32, + c_ulong, + c_ushort, + c_void_p, +) from types import SimpleNamespace from typing import TYPE_CHECKING @@ -28,127 +43,127 @@ ZPIXMAP = 2 -class Display(ctypes.Structure): +class Display(Structure): """ Structure that serves as the connection to the X server and that contains all the information about that X server. """ -class Event(ctypes.Structure): +class Event(Structure): """ XErrorEvent to debug eventual errors. https://tronche.com/gui/x/xlib/event-handling/protocol-errors/default-handlers.html """ _fields_ = [ - ("type", ctypes.c_int), - ("display", ctypes.POINTER(Display)), - ("serial", ctypes.c_ulong), - ("error_code", ctypes.c_ubyte), - ("request_code", ctypes.c_ubyte), - ("minor_code", ctypes.c_ubyte), - ("resourceid", ctypes.c_void_p), + ("type", c_int), + ("display", POINTER(Display)), + ("serial", c_ulong), + ("error_code", c_ubyte), + ("request_code", c_ubyte), + ("minor_code", c_ubyte), + ("resourceid", c_void_p), ] -class XWindowAttributes(ctypes.Structure): +class XWindowAttributes(Structure): """ Attributes for the specified window. """ _fields_ = [ - ("x", ctypes.c_int32), - ("y", ctypes.c_int32), - ("width", ctypes.c_int32), - ("height", ctypes.c_int32), - ("border_width", ctypes.c_int32), - ("depth", ctypes.c_int32), - ("visual", ctypes.c_ulong), - ("root", ctypes.c_ulong), - ("class", ctypes.c_int32), - ("bit_gravity", ctypes.c_int32), - ("win_gravity", ctypes.c_int32), - ("backing_store", ctypes.c_int32), - ("backing_planes", ctypes.c_ulong), - ("backing_pixel", ctypes.c_ulong), - ("save_under", ctypes.c_int32), - ("colourmap", ctypes.c_ulong), - ("mapinstalled", ctypes.c_uint32), - ("map_state", ctypes.c_uint32), - ("all_event_masks", ctypes.c_ulong), - ("your_event_mask", ctypes.c_ulong), - ("do_not_propagate_mask", ctypes.c_ulong), - ("override_redirect", ctypes.c_int32), - ("screen", ctypes.c_ulong), + ("x", c_int32), + ("y", c_int32), + ("width", c_int32), + ("height", c_int32), + ("border_width", c_int32), + ("depth", c_int32), + ("visual", c_ulong), + ("root", c_ulong), + ("class", c_int32), + ("bit_gravity", c_int32), + ("win_gravity", c_int32), + ("backing_store", c_int32), + ("backing_planes", c_ulong), + ("backing_pixel", c_ulong), + ("save_under", c_int32), + ("colourmap", c_ulong), + ("mapinstalled", c_uint32), + ("map_state", c_uint32), + ("all_event_masks", c_ulong), + ("your_event_mask", c_ulong), + ("do_not_propagate_mask", c_ulong), + ("override_redirect", c_int32), + ("screen", c_ulong), ] -class XImage(ctypes.Structure): +class XImage(Structure): """ Description of an image as it exists in the client's memory. https://tronche.com/gui/x/xlib/graphics/images.html """ _fields_ = [ - ("width", ctypes.c_int), - ("height", ctypes.c_int), - ("xoffset", ctypes.c_int), - ("format", ctypes.c_int), - ("data", ctypes.c_void_p), - ("byte_order", ctypes.c_int), - ("bitmap_unit", ctypes.c_int), - ("bitmap_bit_order", ctypes.c_int), - ("bitmap_pad", ctypes.c_int), - ("depth", ctypes.c_int), - ("bytes_per_line", ctypes.c_int), - ("bits_per_pixel", ctypes.c_int), - ("red_mask", ctypes.c_ulong), - ("green_mask", ctypes.c_ulong), - ("blue_mask", ctypes.c_ulong), + ("width", c_int), + ("height", c_int), + ("xoffset", c_int), + ("format", c_int), + ("data", c_void_p), + ("byte_order", c_int), + ("bitmap_unit", c_int), + ("bitmap_bit_order", c_int), + ("bitmap_pad", c_int), + ("depth", c_int), + ("bytes_per_line", c_int), + ("bits_per_pixel", c_int), + ("red_mask", c_ulong), + ("green_mask", c_ulong), + ("blue_mask", c_ulong), ] -class XRRModeInfo(ctypes.Structure): +class XRRModeInfo(Structure): """ Voilà, voilà. """ -class XRRScreenResources(ctypes.Structure): +class XRRScreenResources(Structure): """ Structure that contains arrays of XIDs that point to the available outputs and associated CRTCs. """ _fields_ = [ - ("timestamp", ctypes.c_ulong), - ("configTimestamp", ctypes.c_ulong), - ("ncrtc", ctypes.c_int), - ("crtcs", ctypes.POINTER(ctypes.c_long)), - ("noutput", ctypes.c_int), - ("outputs", ctypes.POINTER(ctypes.c_long)), - ("nmode", ctypes.c_int), - ("modes", ctypes.POINTER(XRRModeInfo)), + ("timestamp", c_ulong), + ("configTimestamp", c_ulong), + ("ncrtc", c_int), + ("crtcs", POINTER(c_long)), + ("noutput", c_int), + ("outputs", POINTER(c_long)), + ("nmode", c_int), + ("modes", POINTER(XRRModeInfo)), ] -class XRRCrtcInfo(ctypes.Structure): +class XRRCrtcInfo(Structure): """ Structure that contains CRTC information. """ _fields_ = [ - ("timestamp", ctypes.c_ulong), - ("x", ctypes.c_int), - ("y", ctypes.c_int), - ("width", ctypes.c_int), - ("height", ctypes.c_int), - ("mode", ctypes.c_long), - ("rotation", ctypes.c_int), - ("noutput", ctypes.c_int), - ("outputs", ctypes.POINTER(ctypes.c_long)), - ("rotations", ctypes.c_ushort), - ("npossible", ctypes.c_int), - ("possible", ctypes.POINTER(ctypes.c_long)), + ("timestamp", c_ulong), + ("x", c_int), + ("y", c_int), + ("width", c_int), + ("height", c_int), + ("mode", c_long), + ("rotation", c_int), + ("noutput", c_int), + ("outputs", POINTER(c_long)), + ("rotations", c_ushort), + ("npossible", c_int), + ("possible", POINTER(c_long)), ] -@ctypes.CFUNCTYPE(ctypes.c_int, ctypes.POINTER(Display), ctypes.POINTER(Event)) +@CFUNCTYPE(c_int, POINTER(Display), POINTER(Event)) def error_handler(_, event): # type: (Any, Any) -> int """ Specifies the program's supplied error handler. """ @@ -176,6 +191,71 @@ def validate(retval, func, args): raise ScreenShotError(err, 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: xlib, xrandr. +# +# Note: keep it sorted by cfunction. +CFUNCTIONS = { + "XDefaultRootWindow": ("xlib", [POINTER(Display)], POINTER(XWindowAttributes)), + "XDestroyImage": ("xlib", [POINTER(XImage)], c_void_p), + "XGetErrorText": ("xlib", [POINTER(Display), c_int, c_char_p, c_int], c_void_p), + "XGetImage": ( + "xlib", + [ + POINTER(Display), + POINTER(Display), + c_int, + c_int, + c_uint, + c_uint, + c_ulong, + c_int, + ], + POINTER(XImage), + ), + "XGetWindowAttributes": ( + "xlib", + [POINTER(Display), POINTER(XWindowAttributes), POINTER(XWindowAttributes)], + c_int, + ), + "XOpenDisplay": ("xlib", [c_char_p], POINTER(Display)), + "XQueryExtension": ( + "xlib", + [ + POINTER(Display), + c_char_p, + POINTER(c_int), + POINTER(c_int), + POINTER(c_int), + ], + c_uint, + ), + "XRRFreeCrtcInfo": ("xrandr", [POINTER(XRRCrtcInfo)], c_void_p), + "XRRFreeScreenResources": ("xrandr", [POINTER(XRRScreenResources)], c_void_p), + "XRRGetCrtcInfo": ( + "xrandr", + [POINTER(Display), POINTER(XRRScreenResources), c_long], + POINTER(XRRCrtcInfo), + ), + "XRRGetScreenResources": ( + "xrandr", + [POINTER(Display), POINTER(Display)], + POINTER(XRRScreenResources), + ), + "XRRGetScreenResourcesCurrent": ( + "xrandr", + [POINTER(Display), POINTER(Display)], + POINTER(XRRScreenResources), + ), + "XSetErrorHandler": ("xlib", [c_void_p], c_int), +} + + class MSS(MSSBase): """ Multiple ScreenShots implementation for GNU/Linux. @@ -229,14 +309,12 @@ def __init__(self, display=None): # Fix for XRRGetScreenResources and XGetImage: # expected LP_Display instance instead of LP_XWindowAttributes - self.drawable = ctypes.cast(self.root, ctypes.POINTER(Display)) + self.drawable = ctypes.cast(self.root, POINTER(Display)) def has_extension(self, extension): # type: (str) -> bool """Return True if the given *extension* is part of the extensions list of the server.""" with lock: - byref = ctypes.byref - c_int = ctypes.c_int major_opcode_return = c_int() first_event_return = c_int() first_error_return = c_int() @@ -245,9 +323,9 @@ def has_extension(self, extension): self.xlib.XQueryExtension( self._get_display(), extension.encode("latin1"), - byref(major_opcode_return), - byref(first_event_return), - byref(first_error_return), + ctypes.byref(major_opcode_return), + ctypes.byref(first_event_return), + ctypes.byref(first_error_return), ) except ScreenShotError: return False @@ -272,109 +350,24 @@ def _get_display(self, disp=None): return display def _set_cfunctions(self): - """ - Set all ctypes functions and attach them to attributes. - See https://tronche.com/gui/x/xlib/function-index.html for details. - """ + """ Set all ctypes functions and attach them to attributes. """ - void = ctypes.c_void_p - c_int = ctypes.c_int - uint = ctypes.c_uint - ulong = ctypes.c_ulong - c_long = ctypes.c_long - char_p = ctypes.c_char_p - pointer = ctypes.POINTER cfactory = self._cfactory - xlib = self.xlib - xrandr = self.xrandr - - # Note: keep it sorted - for attr, func, argtypes, restype in ( - ( - xlib, - "XDefaultRootWindow", - [pointer(Display)], - pointer(XWindowAttributes), - ), - (xlib, "XDestroyImage", [pointer(XImage)], void), - (xlib, "XGetErrorText", [pointer(Display), c_int, char_p, c_int], void), - ( - xlib, - "XGetImage", - [ - pointer(Display), - pointer(Display), - c_int, - c_int, - uint, - uint, - ulong, - c_int, - ], - pointer(XImage), - ), - ( - xlib, - "XGetWindowAttributes", - [ - pointer(Display), - pointer(XWindowAttributes), - pointer(XWindowAttributes), - ], - c_int, - ), - (xlib, "XOpenDisplay", [char_p], pointer(Display)), - ( - xlib, - "XQueryExtension", - [ - pointer(Display), - char_p, - pointer(c_int), - pointer(c_int), - pointer(c_int), - ], - uint, - ), - (xlib, "XSetErrorHandler", [void], c_int), - (xrandr, "XRRFreeCrtcInfo", [pointer(XRRCrtcInfo)], void), - (xrandr, "XRRFreeScreenResources", [pointer(XRRScreenResources)], void), - ( - xrandr, - "XRRGetCrtcInfo", - [pointer(Display), pointer(XRRScreenResources), c_long], - pointer(XRRCrtcInfo), - ), - ): - cfactory( - attr=attr, - errcheck=validate, - func=func, - argtypes=argtypes, - restype=restype, - ) # type: ignore - - # A simple benchmark calling 10 times those 2 functions: - # XRRGetScreenResources(): 0.1755971429956844 s - # XRRGetScreenResourcesCurrent(): 0.0039125580078689 s - # The second is faster by a factor of 44! So try to use it first. - try: - cfactory( - attr=xrandr, - func="XRRGetScreenResourcesCurrent", - errcheck=validate, - argtypes=[pointer(Display), pointer(Display)], - restype=pointer(XRRScreenResources), - ) - except AttributeError: - cfactory( - attr=xrandr, - func="XRRGetScreenResources", - errcheck=validate, - argtypes=[pointer(Display), pointer(Display)], - restype=pointer(XRRScreenResources), - ) - xrandr.XRRGetScreenResourcesCurrent = xrandr.XRRGetScreenResources + attrs = { + "xlib": self.xlib, + "xrandr": self.xrandr, + } + for func, (attr, argtypes, restype) in CFUNCTIONS.items(): + try: + cfactory( + attr=attrs[attr], + errcheck=validate, + func=func, + argtypes=argtypes, + restype=restype, + ) # type: ignore + except AttributeError: + pass def get_error_details(self): # type: () -> Optional[Dict[str, Any]] @@ -419,7 +412,15 @@ def _monitors_impl(self): ) # Each monitors - mon = xrandr.XRRGetScreenResourcesCurrent(display, self.drawable).contents + # A simple benchmark calling 10 times those 2 functions: + # XRRGetScreenResources(): 0.1755971429956844 s + # XRRGetScreenResourcesCurrent(): 0.0039125580078689 s + # The second is faster by a factor of 44! So try to use it first. + try: + mon = xrandr.XRRGetScreenResourcesCurrent(display, self.drawable).contents + except AttributeError: + mon = xrandr.XRRGetScreenResources(display, self.drawable).contents + crtcs = mon.crtcs for idx in range(mon.ncrtc): crtc = xrandr.XRRGetCrtcInfo(display, mon, crtcs[idx]).contents @@ -464,9 +465,7 @@ def _grab_impl(self, monitor): raw_data = ctypes.cast( ximage.contents.data, - ctypes.POINTER( - ctypes.c_ubyte * monitor["height"] * monitor["width"] * 4 - ), + POINTER(c_ubyte * monitor["height"] * monitor["width"] * 4), ) data = bytearray(raw_data.contents) finally: diff --git a/mss/tests/test_gnu_linux.py b/mss/tests/test_gnu_linux.py index f5d300e2..10119760 100644 --- a/mss/tests/test_gnu_linux.py +++ b/mss/tests/test_gnu_linux.py @@ -44,8 +44,8 @@ def test_factory_systems(monkeypatch): # Windows monkeypatch.setattr(platform, "system", lambda: "wInDoWs") - with pytest.raises(ValueError): - # wintypes.py:19: ValueError: _type_ 'v' not supported + with pytest.raises(ImportError): + # ImportError: cannot import name 'WINFUNCTYPE' mss.mss() diff --git a/mss/windows.py b/mss/windows.py index fb9c8d03..579b2da2 100644 --- a/mss/windows.py +++ b/mss/windows.py @@ -6,6 +6,7 @@ import sys import ctypes import threading +from ctypes import POINTER, Structure, WINFUNCTYPE, c_void_p from ctypes.wintypes import ( BOOL, DOUBLE, @@ -40,7 +41,7 @@ SRCCOPY = 0x00CC0020 -class BITMAPINFOHEADER(ctypes.Structure): +class BITMAPINFOHEADER(Structure): """ Information about the dimensions and color format of a DIB. """ _fields_ = [ @@ -58,7 +59,7 @@ class BITMAPINFOHEADER(ctypes.Structure): ] -class BITMAPINFO(ctypes.Structure): +class BITMAPINFO(Structure): """ Structure that defines the dimensions and color information for a DIB. """ @@ -66,10 +67,39 @@ class BITMAPINFO(ctypes.Structure): _fields_ = [("bmiHeader", BITMAPINFOHEADER), ("bmiColors", DWORD * 3)] +MONITORNUMPROC = WINFUNCTYPE(INT, DWORD, DWORD, POINTER(RECT), DOUBLE) + + +# 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 = { + "BitBlt": ("gdi32", [HDC, INT, INT, INT, INT, HDC, INT, INT, DWORD], BOOL), + "CreateCompatibleBitmap": ("gdi32", [HDC, INT, INT], HBITMAP), + "CreateCompatibleDC": ("gdi32", [HDC], HDC), + "DeleteObject": ("gdi32", [HGDIOBJ], INT), + "GetDeviceCaps": ("gdi32", [HWND, INT], INT), + "GetDIBits": ( + "gdi32", + [HDC, HBITMAP, UINT, UINT, c_void_p, POINTER(BITMAPINFO), UINT], + BOOL, + ), + "SelectObject": ("gdi32", [HDC, HGDIOBJ], HGDIOBJ), + "EnumDisplayMonitors": ("user32", [HDC, c_void_p, MONITORNUMPROC, LPARAM], BOOL), + "GetSystemMetrics": ("user32", [INT], INT), + "GetWindowDC": ("user32", [HWND], HDC), +} + + class MSS(MSSBase): """ Multiple ScreenShots implementation for Microsoft Windows. """ - __slots__ = {"_bbox", "_bmi", "_data", "gdi32", "monitorenumproc", "user32"} + __slots__ = {"_bbox", "_bmi", "_data", "gdi32", "user32"} # Class attributes instanced one time to prevent resource leaks. bmp = None @@ -84,10 +114,6 @@ def __init__(self, **_): super().__init__() - self.monitorenumproc = ctypes.WINFUNCTYPE( - INT, DWORD, DWORD, ctypes.POINTER(RECT), DOUBLE - ) - self.user32 = ctypes.WinDLL("user32") self.gdi32 = ctypes.WinDLL("gdi32") self._set_cfunctions() @@ -112,36 +138,18 @@ def __init__(self, **_): def _set_cfunctions(self): """ Set all ctypes functions and attach them to attributes. """ - void = ctypes.c_void_p - pointer = ctypes.POINTER cfactory = self._cfactory - gdi32 = self.gdi32 - user32 = self.user32 - - # Note: keep it sorted - for attr, func, argtypes, restype in ( - (gdi32, "BitBlt", [HDC, INT, INT, INT, INT, HDC, INT, INT, DWORD], BOOL), - (gdi32, "CreateCompatibleBitmap", [HDC, INT, INT], HBITMAP), - (gdi32, "CreateCompatibleDC", [HDC], HDC), - (gdi32, "DeleteObject", [HGDIOBJ], INT), - (gdi32, "GetDeviceCaps", [HWND, INT], INT), - ( - gdi32, - "GetDIBits", - [HDC, HBITMAP, UINT, UINT, void, pointer(BITMAPINFO), UINT], - BOOL, - ), - (gdi32, "SelectObject", [HDC, HGDIOBJ], HGDIOBJ), - ( - user32, - "EnumDisplayMonitors", - [HDC, void, self.monitorenumproc, LPARAM], - BOOL, - ), - (user32, "GetSystemMetrics", [INT], INT), - (user32, "GetWindowDC", [HWND], HDC), - ): - cfactory(attr=attr, func=func, argtypes=argtypes, restype=restype) # type: ignore + attrs = { + "gdi32": self.gdi32, + "user32": self.user32, + } + for func, (attr, argtypes, restype) in CFUNCTIONS.items(): + cfactory( + attr=attrs[attr], + func=func, + argtypes=argtypes, + restype=restype, + ) # type: ignore def _set_dpi_awareness(self): """ Set DPI aware to capture full screen on Hi-DPI monitors. """ @@ -210,7 +218,7 @@ def _callback(monitor, data, rect, dc_): ) return 1 - callback = self.monitorenumproc(_callback) + callback = MONITORNUMPROC(_callback) user32.EnumDisplayMonitors(0, 0, callback, 0) def _grab_impl(self, monitor): From f0dfd2ebef26af71b4e06e9ab13782f655184a8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Sat, 17 Oct 2020 18:09:14 +0200 Subject: [PATCH 038/280] Windows: sort C functions --- mss/windows.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mss/windows.py b/mss/windows.py index 579b2da2..478fecb3 100644 --- a/mss/windows.py +++ b/mss/windows.py @@ -83,16 +83,16 @@ class BITMAPINFO(Structure): "CreateCompatibleBitmap": ("gdi32", [HDC, INT, INT], HBITMAP), "CreateCompatibleDC": ("gdi32", [HDC], HDC), "DeleteObject": ("gdi32", [HGDIOBJ], INT), + "EnumDisplayMonitors": ("user32", [HDC, c_void_p, MONITORNUMPROC, LPARAM], BOOL), "GetDeviceCaps": ("gdi32", [HWND, INT], INT), "GetDIBits": ( "gdi32", [HDC, HBITMAP, UINT, UINT, c_void_p, POINTER(BITMAPINFO), UINT], BOOL, ), - "SelectObject": ("gdi32", [HDC, HGDIOBJ], HGDIOBJ), - "EnumDisplayMonitors": ("user32", [HDC, c_void_p, MONITORNUMPROC, LPARAM], BOOL), "GetSystemMetrics": ("user32", [INT], INT), "GetWindowDC": ("user32", [HWND], HDC), + "SelectObject": ("gdi32", [HDC, HGDIOBJ], HGDIOBJ), } From 87a8f497d208b0d1132d97893d37d6ce6b7b0a57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Sat, 17 Oct 2020 18:12:08 +0200 Subject: [PATCH 039/280] Add myself to GitHub sponsors --- .github/FUNDING.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 2767f9c2..204c47d0 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,6 +1,6 @@ # These are supported funding model platforms -github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] +github: BoboTiG patreon: # Replace with a single Patreon username open_collective: # Replace with a single Open Collective username ko_fi: # Replace with a single Ko-fi username From f761b647ced66470c9e1789af0a5c36d2de8371a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Sat, 17 Oct 2020 20:03:27 +0200 Subject: [PATCH 040/280] tests: expand Python versions to 3.9 and 3.10 Also updated those Python versions: - 3.5.8 -> 3.5.10 - 3.6.9 -> 3.6.12 - 3.7.5 -> 3.7.9 - 3.8.0 -> 3.8.6 --- .travis.yml | 64 ++++++++++++++++++++++++----------- CHANGELOG | 1 + appveyor.yml | 63 +++++++++++++++------------------- mss/tests/test_third_party.py | 3 +- tox.ini | 2 +- 5 files changed, 76 insertions(+), 57 deletions(-) diff --git a/.travis.yml b/.travis.yml index 6fb8adb1..0297694e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -26,42 +26,68 @@ jobs: os: osx language: shell install: + - unset PYENV_ROOT + - curl -L https://github.com/pyenv/pyenv-installer/raw/master/bin/pyenv-installer | bash + - export PATH="$HOME/.pyenv/bin:$PATH" - eval "$(pyenv init -)" - - pyenv install --skip-existing 3.5.8 - - pyenv global 3.5.8 + - pyenv install --skip-existing 3.5.10 + - pyenv global 3.5.10 env: TOXENV=py35 - name: Python 3.6 on macOS os: osx language: shell install: + - unset PYENV_ROOT + - curl -L https://github.com/pyenv/pyenv-installer/raw/master/bin/pyenv-installer | bash + - export PATH="$HOME/.pyenv/bin:$PATH" - eval "$(pyenv init -)" - - pyenv install --skip-existing 3.6.9 - - pyenv global system 3.6.9 + - pyenv install --skip-existing 3.6.12 + - pyenv global system 3.6.12 env: TOXENV=py36 - name: Python 3.7 on macOS os: osx language: shell install: + - unset PYENV_ROOT + - curl -L https://github.com/pyenv/pyenv-installer/raw/master/bin/pyenv-installer | bash + - export PATH="$HOME/.pyenv/bin:$PATH" - eval "$(pyenv init -)" - - pyenv install --skip-existing 3.7.5 - - pyenv global system 3.7.5 + - pyenv install --skip-existing 3.7.9 + - pyenv global system 3.7.9 env: TOXENV=py37 - name: Python 3.8 on macOS os: osx language: shell install: + - unset PYENV_ROOT + - curl -L https://github.com/pyenv/pyenv-installer/raw/master/bin/pyenv-installer | bash + - export PATH="$HOME/.pyenv/bin:$PATH" - eval "$(pyenv init -)" - - pyenv install --skip-existing 3.8.0 - - pyenv global system 3.8.0 + - pyenv install --skip-existing 3.8.6 + - pyenv global system 3.8.6 env: TOXENV=py38 - # - name: Python 3.9 on macOS - # os: osx - # language: shell - # install: - # - eval "$(pyenv init -)" - # - pyenv install --skip-existing 3.9-dev - # - pyenv global system 3.9-dev - # env: TOXENV=py39 + - name: Python 3.9 on macOS + os: osx + language: shell + install: + - unset PYENV_ROOT + - curl -L https://github.com/pyenv/pyenv-installer/raw/master/bin/pyenv-installer | bash + - export PATH="$HOME/.pyenv/bin:$PATH" + - eval "$(pyenv init -)" + - pyenv install --skip-existing 3.9-dev + - pyenv global system 3.9-dev + env: TOXENV=py39 + - name: Python 3.10 on macOS + os: osx + language: shell + install: + - unset PYENV_ROOT + - curl -L https://github.com/pyenv/pyenv-installer/raw/master/bin/pyenv-installer | bash + - export PATH="$HOME/.pyenv/bin:$PATH" + - eval "$(pyenv init -)" + - pyenv install --skip-existing 3.10-dev + - pyenv global system 3.10-dev + env: TOXENV=py310 - name: PyPy 3.6 on GNU/Linux python: pypy3 env: TOXENV=pypy3 @@ -80,9 +106,9 @@ jobs: - name: Python 3.9 on GNU/Linux python: 3.9-dev env: TOXENV=py39 - # - name: Python 3.10 on GNU/Linux - # python: nightly - # env: TOXENV=py310 + - name: Python 3.10 on GNU/Linux + python: nightly + env: TOXENV=py310 addons: apt: diff --git a/CHANGELOG b/CHANGELOG index 4e625d6c..a24d64b6 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -6,6 +6,7 @@ History: - MSS: reworked how C functions are initialised - Mac: reduce the number of function calls - Mac: support macOS Big Sur (fixes #178) + - tests: expand Python versions to 3.9 and 3.10 - tests: fix macOS intepreter not found on Travis-CI - tests: fix test_entry_point() when there are several monitors - :heart: contributors: @ diff --git a/appveyor.yml b/appveyor.yml index f78367e0..c96af385 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,47 +1,38 @@ build: off -cache: - - '%LOCALAPPDATA%\pip\Cache' +image: + - Visual Studio 2019 + +platform: + - x64 + - x86 environment: fast_finish: true matrix: - - python: py35 - tox_env: py35 - python_path: c:\python35 - - python: py35-x64 - tox_env: py35 - python_path: c:\python35-x64 - - python: py36 - tox_env: py36 - python_path: c:\python36 - - python: py36-x64 - tox_env: py36 - python_path: c:\python36-x64 - - python: py37 - tox_env: py37 - python_path: c:\python37 - - python: py37-x64 - tox_env: py37 - python_path: c:\python37-x64 - - python: py38 - tox_env: py38 - python_path: c:\python38 - - python: py38-x64 - tox_env: py38 - python_path: c:\python38-x64 - - python: py39 - tox_env: py39 - python_path: c:\python39 - - python: py39-x64 - tox_env: py39 - python_path: c:\python39-x64 + - PYTHON_VERSION: 3.10 + - PYTHON_VERSION: 3.9 + - PYTHON_VERSION: 3.8 + - PYTHON_VERSION: 3.7 + - PYTHON_VERSION: 3.6 + - PYTHON_VERSION: 3.5 + +matrix: + allow_failures: + - PYTHON_VERSION: 3.10 + - PYTHON_VERSION: 3.9 + +init: + # Update Environment Variables based on matrix/platform + - set PY_VER=%PYTHON_VERSION:.=% + - set PYTHON=C:\PYTHON%PY_VER% + - if %PLATFORM%==x64 (set PYTHON=%PYTHON%-x64) + + # Put desired Python version first in PATH + - set PATH=%PYTHON%;%PYTHON%\Scripts;%PATH% install: - - python -m pip install virtualenv - - python -m virtualenv env - - env\Scripts\activate.bat - python -m pip install --upgrade pip tox test_script: - tox -e %tox_env% + - tox -e py%PY_VER% diff --git a/mss/tests/test_third_party.py b/mss/tests/test_third_party.py index 691dffc1..e562bf3d 100644 --- a/mss/tests/test_third_party.py +++ b/mss/tests/test_third_party.py @@ -11,7 +11,8 @@ try: import numpy -except ImportError: +except (ImportError, RuntimeError): + # RuntimeError on Python 3.9 (macOS): Polyfit sanity test emitted a warning, ... numpy = None try: diff --git a/tox.ini b/tox.ini index 8ab0abf5..bfe7ba53 100644 --- a/tox.ini +++ b/tox.ini @@ -13,7 +13,7 @@ deps = pytest # Must pin that version to support PyPy3 pypy3: numpy==1.15.4 - py3{10,9,8,7,6,5}: numpy + py3{9,8,7,6,5}: numpy pillow wheel commands = From 5c0c4f6215c33d0a60ca9c67f2ef1bc8fa632380 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Sat, 31 Oct 2020 18:14:26 +0100 Subject: [PATCH 041/280] Release 6.1.0 --- CHANGELOG | 3 +-- CHANGES.rst | 14 +++++++++++++- docs/source/conf.py | 2 +- mss/__init__.py | 2 +- setup.cfg | 2 +- 5 files changed, 17 insertions(+), 6 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index a24d64b6..d84a392d 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -2,14 +2,13 @@ History: -6.0.1 2020/xx/xx +6.1.0 2020/10/31 - MSS: reworked how C functions are initialised - Mac: reduce the number of function calls - Mac: support macOS Big Sur (fixes #178) - tests: expand Python versions to 3.9 and 3.10 - tests: fix macOS intepreter not found on Travis-CI - tests: fix test_entry_point() when there are several monitors - - :heart: contributors: @ 6.0.0 2020/06/30 - removed usage of deprecated "license_file" option for "license_files" diff --git a/CHANGES.rst b/CHANGES.rst index 054b24fa..7cd482fe 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,7 +1,19 @@ -6.0.1 (2020-xx-xx) +6.1.0 (2020-10-31) ================== +darwin.py +--------- + - Added ``CFUNCTIONS`` +linux.py +-------- + - Added ``CFUNCTIONS`` + +windows.py +---------- + - Added ``CFUNCTIONS`` + - Added ``MONITORNUMPROC`` + - Removed ``MSS.monitorenumproc``. Use ``MONITORNUMPROC`` instead. 6.0.0 (2020-06-30) diff --git a/docs/source/conf.py b/docs/source/conf.py index c27c6e5a..2c5b6c45 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -27,7 +27,7 @@ # built documents. # # The short X.Y version. -version = "6.0.1" +version = "6.1.0" # The full version, including alpha/beta/rc tags. release = "latest" diff --git a/mss/__init__.py b/mss/__init__.py index ca703af7..25b7ab5a 100644 --- a/mss/__init__.py +++ b/mss/__init__.py @@ -12,7 +12,7 @@ from .exception import ScreenShotError from .factory import mss -__version__ = "6.0.1" +__version__ = "6.1.0" __author__ = "Mickaël 'Tiger-222' Schoentgen" __copyright__ = """ Copyright (c) 2013-2020, Mickaël 'Tiger-222' Schoentgen diff --git a/setup.cfg b/setup.cfg index 2001b247..cfdb620f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = mss -version = 6.0.1 +version = 6.1.0 author = Mickaël 'Tiger-222' Schoentgen author-email = contact@tiger-222.fr description = An ultra fast cross-platform multiple screenshots module in pure python using ctypes. From 7b5cff83715b6f2f4c1e46a184c73be2b7657263 Mon Sep 17 00:00:00 2001 From: Bruno Pagani Date: Thu, 29 Jul 2021 07:15:55 +0000 Subject: [PATCH 042/280] setup: fix three setuptools warnings (#203) UserWarning: Usage of dash-separated 'author-email' will not be supported in future versions. Please use the underscore name 'author_email' instead UserWarning: Usage of dash-separated 'home-page' will not be supported in future versions. Please use the underscore name 'home_page' instead UserWarning: Usage of dash-separated 'zip-safe' will not be supported in future versions. Please use the underscore name 'zip_safe' instead --- setup.cfg | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/setup.cfg b/setup.cfg index cfdb620f..bd784411 100644 --- a/setup.cfg +++ b/setup.cfg @@ -2,11 +2,11 @@ name = mss version = 6.1.0 author = Mickaël 'Tiger-222' Schoentgen -author-email = contact@tiger-222.fr +author_email = contact@tiger-222.fr description = An ultra fast cross-platform multiple screenshots module in pure python using ctypes. long_description = file: README.rst url = https://github.com/BoboTiG/python-mss -home-page = https://pypi.org/project/mss/ +home_page = https://pypi.org/project/mss/ project_urls = Documentation = https://python-mss.readthedocs.io Source = https://github.com/BoboTiG/python-mss @@ -31,7 +31,7 @@ classifiers = Topic :: Software Development :: Libraries [options] -zip-safe = False +zip_safe = False include_package_data = True packages = mss python_requires = >=3.5 From a4dca8396c4ae35e85f1072b184c9edd9d36a574 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Sun, 28 Nov 2021 21:12:41 +0100 Subject: [PATCH 043/280] Typos in usage.rst --- docs/source/usage.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/source/usage.rst b/docs/source/usage.rst index e1099a3f..8783d004 100644 --- a/docs/source/usage.rst +++ b/docs/source/usage.rst @@ -9,7 +9,7 @@ So MSS can be used as simply as:: from mss import mss -Or import the good one base on your operating system:: +Or import the good one based on your operating system:: # MacOS X from mss.darwin import MSS as mss @@ -46,7 +46,7 @@ This is a much better usage, memory efficient:: for _ in range(100): sct.shot() -Also, it is a good thing to save the MSS instance inside an attribute of you class and calling it when needed. +Also, it is a good thing to save the MSS instance inside an attribute of your class and calling it when needed. GNU/Linux @@ -57,7 +57,7 @@ On GNU/Linux, you can specify which display to use (useful for distant screensho with mss(display=":0.0") as sct: # ... -A more specific example to only target GNU/Linux: +A more specific example (only valid on GNU/Linux): .. literalinclude:: examples/linux_display_keyword.py :lines: 8- From 572e8640c2175ced32be1575002bc5bdc553841a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Fri, 17 Dec 2021 12:32:10 +0100 Subject: [PATCH 044/280] Add a simple example about getting PNG bytes only --- docs/source/examples.rst | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/docs/source/examples.rst b/docs/source/examples.rst index 137715fd..6adb7bc5 100644 --- a/docs/source/examples.rst +++ b/docs/source/examples.rst @@ -77,6 +77,22 @@ You can tweak the PNG compression level (see :py:func:`zlib.compress()` for deta .. versionadded:: 3.2.0 +Get PNG bytes, no file output +----------------------------- + +You can get the bytes of the PNG image: +:: + + with mss.mss() as sct: + # The monitor or screen part to capture + monitor = sct.monitors[1] # or a region + + # Grab the data + sct_img = sct.grab(monitor) + + # Generate the PNG + png = mss.tools.to_png(sct_img.rgb, sct_img.size) + Advanced ======== From 3ae002bd7d995b767e6907262e66ec81616a96b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Fri, 17 Dec 2021 12:33:50 +0100 Subject: [PATCH 045/280] Update documentation credit date --- docs/source/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 2c5b6c45..2b4277d2 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -19,7 +19,7 @@ # General information about the project. project = "Python MSS" -copyright = "2013-2020, Mickaël 'Tiger-222' Schoentgen & contributors" +copyright = "2013-2021, Mickaël 'Tiger-222' Schoentgen & contributors" author = "Tiger-222" # The version info for the project you're documenting, acts as replacement for From 78e5a8de625734ae84235a82af03a803d56da58b Mon Sep 17 00:00:00 2001 From: Tonyl314 <38325261+Tonyl314@users.noreply.github.com> Date: Sun, 26 Dec 2021 19:29:51 +0100 Subject: [PATCH 046/280] Fix a few typos (#208) --- mss/base.py | 2 +- mss/darwin.py | 2 +- mss/factory.py | 4 ++-- mss/linux.py | 4 ++-- mss/windows.py | 8 ++++---- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/mss/base.py b/mss/base.py index ed22b90a..880ccfa3 100644 --- a/mss/base.py +++ b/mss/base.py @@ -68,7 +68,7 @@ def grab(self, monitor): """ Retrieve screen pixels for a given monitor. - Note: *monitor* can be a tuple like PIL.Image.grab() accepts. + Note: *monitor* can be a tuple like the one PIL.Image.grab() accepts. :param monitor: The coordinates and size of the box to capture. See :meth:`monitors ` for object details. diff --git a/mss/darwin.py b/mss/darwin.py index 92516365..6de21e34 100644 --- a/mss/darwin.py +++ b/mss/darwin.py @@ -164,7 +164,7 @@ def _monitors_impl(self): all_monitors = CGRect() self._monitors.append({}) - # Each monitors + # Each monitor display_count = c_uint32(0) active_displays = (c_uint32 * self.max_displays)() core.CGGetActiveDisplayList( diff --git a/mss/factory.py b/mss/factory.py index 47aea11a..902ce069 100644 --- a/mss/factory.py +++ b/mss/factory.py @@ -19,8 +19,8 @@ def mss(**kwargs): # type: (Any) -> MSSBase """ Factory returning a proper MSS class instance. - It detects the plateform we are running on - and choose the most adapted mss_class to take + It detects the platform we are running on + and chooses the most adapted mss_class to take screenshots. It then proxies its arguments to the class for diff --git a/mss/linux.py b/mss/linux.py index 8e56c395..b952916c 100644 --- a/mss/linux.py +++ b/mss/linux.py @@ -335,7 +335,7 @@ def has_extension(self, extension): def _get_display(self, disp=None): """ Retrieve a thread-safe display from XOpenDisplay(). - In multithreading, if the thread who creates *display* is dead, *display* will + In multithreading, if the thread that creates *display* is dead, *display* will no longer be valid to grab the screen. The *display* attribute is replaced with *_display_dict* to maintain the *display* values in multithreading. Since the current thread and main thread are always alive, reuse their @@ -411,7 +411,7 @@ def _monitors_impl(self): } ) - # Each monitors + # Each monitor # A simple benchmark calling 10 times those 2 functions: # XRRGetScreenResources(): 0.1755971429956844 s # XRRGetScreenResourcesCurrent(): 0.0039125580078689 s diff --git a/mss/windows.py b/mss/windows.py index 478fecb3..0e38202e 100644 --- a/mss/windows.py +++ b/mss/windows.py @@ -152,7 +152,7 @@ def _set_cfunctions(self): ) # type: ignore def _set_dpi_awareness(self): - """ Set DPI aware to capture full screen on Hi-DPI monitors. """ + """ Set DPI awareness to capture full screen on Hi-DPI monitors. """ version = sys.getwindowsversion()[:2] # pylint: disable=no-member if version >= (6, 3): @@ -169,7 +169,7 @@ def _set_dpi_awareness(self): def _get_srcdc(self): """ Retrieve a thread-safe HDC from GetWindowDC(). - In multithreading, if the thread who creates *srcdc* is dead, *srcdc* will + In multithreading, if the thread that creates *srcdc* is dead, *srcdc* will no longer be valid to grab the screen. The *srcdc* attribute is replaced with *_srcdc_dict* to maintain the *srcdc* values in multithreading. Since the current thread and main thread are always alive, reuse their *srcdc* value first. @@ -198,7 +198,7 @@ def _monitors_impl(self): } ) - # Each monitors + # Each monitor def _callback(monitor, data, rect, dc_): # types: (int, HDC, LPRECT, LPARAM) -> int """ @@ -226,7 +226,7 @@ def _grab_impl(self, monitor): """ Retrieve all pixels from a monitor. Pixels have to be RGB. - In the code, there are few interesting things: + In the code, there are a few interesting things: [1] bmi.bmiHeader.biHeight = -height From b05ca23c208acc03ff9824a69ad3bc7f80c08ec7 Mon Sep 17 00:00:00 2001 From: CTPaHHuK-HEbA Date: Tue, 9 Aug 2022 10:13:57 +0300 Subject: [PATCH 047/280] test: fix Pytest deprecated strict option (#214) Fixes #213. --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index bd784411..db614f34 100644 --- a/setup.cfg +++ b/setup.cfg @@ -51,7 +51,7 @@ max-line-length = 120 [tool:pytest] addopts = --showlocals - --strict + --strict-markers --failed-first -r fE -v From be3fb7b427631ebac869888a7f5ff2bc15cb5aa0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Thu, 27 Oct 2022 05:42:17 +0200 Subject: [PATCH 048/280] Version 7.0.0 --- CHANGELOG | 21 +++-- LICENSE | 2 +- appveyor.yml | 7 +- docs/source/conf.py | 6 +- docs/source/examples/callback.py | 8 +- docs/source/examples/custom_cls_image.py | 9 +- docs/source/examples/fps.py | 8 +- docs/source/examples/fps_multiprocessing.py | 11 +-- docs/source/examples/from_pil_tuple.py | 4 +- docs/source/examples/linux_display_keyword.py | 2 - docs/source/examples/opencv_numpy.py | 5 +- docs/source/examples/part_of_screen.py | 2 - .../examples/part_of_screen_monitor_2.py | 2 - docs/source/examples/pil.py | 5 +- docs/source/examples/pil_pixels.py | 5 +- docs/source/support.rst | 3 +- mss/__init__.py | 17 ++-- mss/__main__.py | 14 +-- mss/base.py | 69 +++++++------- mss/darwin.py | 68 ++++++-------- mss/exception.py | 11 +-- mss/factory.py | 27 ++---- mss/linux.py | 93 ++++++++----------- mss/models.py | 5 +- mss/screenshot.py | 89 +++++++----------- mss/tests/bench_bgra2rgb.py | 4 +- mss/tests/bench_general.py | 1 - mss/tests/conftest.py | 24 +++-- mss/tests/test_bgra_to_rgb.py | 2 +- mss/tests/test_get_pixels.py | 2 +- mss/tests/test_gnu_linux.py | 9 +- mss/tests/test_implementation.py | 13 +-- mss/tests/test_leaks.py | 28 ++---- mss/tests/test_macos.py | 7 +- mss/tests/test_save.py | 6 +- mss/tests/test_setup.py | 10 +- mss/tests/test_third_party.py | 18 ++-- mss/tests/test_tools.py | 7 +- mss/tests/test_windows.py | 5 +- mss/tools.py | 10 +- mss/windows.py | 69 +++++++------- mypy.ini | 20 ++++ setup.cfg | 18 +++- tox.ini | 10 +- 44 files changed, 352 insertions(+), 404 deletions(-) create mode 100644 mypy.ini diff --git a/CHANGELOG b/CHANGELOG index d84a392d..b1fddb4e 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -2,6 +2,15 @@ History: +7.0.0 2022/10/27 + - added support for Python 3.11 + - added support for Python 3.10 + - removed support for Python 3.5 + - MSS: modernized the code base (types, f-string, ran isort & black) + - MSS: fixed several Sourcery issues + - MSS: fixed typos here, and there + - doc: fixed an error when building with shpinx + 6.1.0 2020/10/31 - MSS: reworked how C functions are initialised - Mac: reduce the number of function calls @@ -119,14 +128,14 @@ History: 3.0.0 2017/07/06 - big refactor, introducing the ScreenShot class - MSS: add Numpy array interface support to the Screenshot class - - docs: add OpenCV/Numpy, PIL pixels, FPS + - doc: add OpenCV/Numpy, PIL pixels, FPS 2.0.22 2017/04/29 - new contributors: David Becker, redodo - MSS: better use of exception mechanism - Linux: use of hasattr to prevent Exception on early exit - Mac: take into account extra black pixels added when screen with is not divisible by 16 (fix #14) - - docs: add an example to capture only a part of the screen + - doc: add an example to capture only a part of the screen 2.0.18 2016/12/03 - change license to MIT @@ -138,7 +147,7 @@ History: - Linux: skip unused monitors - Linux: use errcheck instead of deprecated restype with callable (fix #11) - Linux: fix security issue (reported by Bandit) - - docs: add documentation (fix #10) + - doc: add documentation (fix #10) - tests: add tests and use Travis CI (fix #9) 2.0.0 2016/06/04 @@ -175,14 +184,14 @@ History: - MSS: little code review - Linux: fix monitor count - tests: remove test-linux binary - - docs: add doc/TESTING - - docs: remove Bonus section from README.rst + - doc: add doc/TESTING + - doc: remove Bonus section from README.rst 0.1.0 2015/04/10 - MSS: fix code with YAPF tool - Linux: fully functional using Xrandr library - Linux: code purgation (no more XML files to parse) - - docs: better tests and examples + - doc: better tests and examples 0.0.8 2015/02/04 - new contributors: sergey-vin, Alexander 'thehesiod' Mohr diff --git a/LICENSE b/LICENSE index e2acbe65..ef5ae019 100644 --- a/LICENSE +++ b/LICENSE @@ -1,5 +1,5 @@ MIT License -Copyright (c) 2016-2020, Mickaël 'Tiger-222' Schoentgen +Copyright (c) 2016-2022, Mickaël 'Tiger-222' Schoentgen Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: diff --git a/appveyor.yml b/appveyor.yml index c96af385..199d179d 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -10,17 +10,12 @@ platform: environment: fast_finish: true matrix: + - PYTHON_VERSION: 3.11 - PYTHON_VERSION: 3.10 - PYTHON_VERSION: 3.9 - PYTHON_VERSION: 3.8 - PYTHON_VERSION: 3.7 - PYTHON_VERSION: 3.6 - - PYTHON_VERSION: 3.5 - -matrix: - allow_failures: - - PYTHON_VERSION: 3.10 - - PYTHON_VERSION: 3.9 init: # Update Environment Variables based on matrix/platform diff --git a/docs/source/conf.py b/docs/source/conf.py index 2b4277d2..35e51fd6 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -19,7 +19,7 @@ # General information about the project. project = "Python MSS" -copyright = "2013-2021, Mickaël 'Tiger-222' Schoentgen & contributors" +copyright = "2013-2022, Mickaël 'Tiger-222' Schoentgen & contributors" author = "Tiger-222" # The version info for the project you're documenting, acts as replacement for @@ -27,7 +27,7 @@ # built documents. # # The short X.Y version. -version = "6.1.0" +version = "7.0.0" # The full version, including alpha/beta/rc tags. release = "latest" @@ -37,7 +37,7 @@ # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. -language = None +language = "en" # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. diff --git a/docs/source/examples/callback.py b/docs/source/examples/callback.py index ee79774c..147c9525 100644 --- a/docs/source/examples/callback.py +++ b/docs/source/examples/callback.py @@ -4,22 +4,20 @@ Screenshot of the monitor 1, with callback. """ - import os import os.path import mss -def on_exists(fname): - # type: (str) -> None +def on_exists(fname: str) -> None: """ Callback example when we try to overwrite an existing screenshot. """ if os.path.isfile(fname): - newfile = fname + ".old" - print("{} -> {}".format(fname, newfile)) + newfile = f"{fname}.old" + print(f"{fname} -> {newfile}") os.rename(fname, newfile) diff --git a/docs/source/examples/custom_cls_image.py b/docs/source/examples/custom_cls_image.py index 4e5d8757..4232e490 100644 --- a/docs/source/examples/custom_cls_image.py +++ b/docs/source/examples/custom_cls_image.py @@ -4,23 +4,26 @@ Screenshot of the monitor 1, using a custom class to handle the data. """ +from typing import Any import mss +from mss.models import Monitor +from mss.screenshot import ScreenShot -class SimpleScreenShot: +class SimpleScreenShot(ScreenShot): """ 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. """ - def __init__(self, data, monitor, **kwargs): + def __init__(self, data: bytearray, monitor: Monitor, **_: Any) -> None: self.data = data self.monitor = monitor with mss.mss() as sct: - sct.cls_image = SimpleScreenShot # type: ignore + sct.cls_image = SimpleScreenShot image = sct.grab(sct.monitors[1]) # ... diff --git a/docs/source/examples/fps.py b/docs/source/examples/fps.py index e8123780..4046f2ab 100644 --- a/docs/source/examples/fps.py +++ b/docs/source/examples/fps.py @@ -5,15 +5,15 @@ Simple naive benchmark to compare with: https://pythonprogramming.net/game-frames-open-cv-python-plays-gta-v/ """ - import time import cv2 -import mss import numpy +import mss + -def screen_record(): +def screen_record() -> int: try: from PIL import ImageGrab except ImportError: @@ -38,7 +38,7 @@ def screen_record(): return fps -def screen_record_efficient(): +def screen_record_efficient() -> int: # 800x600 windowed mode mon = {"top": 40, "left": 0, "width": 800, "height": 640} diff --git a/docs/source/examples/fps_multiprocessing.py b/docs/source/examples/fps_multiprocessing.py index d229cb0a..28caf593 100644 --- a/docs/source/examples/fps_multiprocessing.py +++ b/docs/source/examples/fps_multiprocessing.py @@ -5,16 +5,13 @@ Example using the multiprocessing module to speed-up screen capture. https://github.com/pythonlessons/TensorFlow-object-detection-tutorial """ - from multiprocessing import Process, Queue import mss import mss.tools -def grab(queue): - # type: (Queue) -> None - +def grab(queue: Queue) -> None: rect = {"top": 0, "left": 0, "width": 600, "height": 800} with mss.mss() as sct: @@ -25,9 +22,7 @@ def grab(queue): queue.put(None) -def save(queue): - # type: (Queue) -> None - +def save(queue: Queue) -> None: number = 0 output = "screenshots/file_{}.png" to_png = mss.tools.to_png @@ -43,7 +38,7 @@ def save(queue): if __name__ == "__main__": # The screenshots queue - queue = Queue() # type: Queue + queue: Queue = Queue() # 2 processes: one for grabing and one for saving PNG files Process(target=grab, args=(queue,)).start() diff --git a/docs/source/examples/from_pil_tuple.py b/docs/source/examples/from_pil_tuple.py index 0e56cec1..61f2d94b 100644 --- a/docs/source/examples/from_pil_tuple.py +++ b/docs/source/examples/from_pil_tuple.py @@ -4,11 +4,9 @@ Use PIL bbox style and percent values. """ - import mss import mss.tools - with mss.mss() as sct: # Use the 1st monitor monitor = sct.monitors[1] @@ -23,7 +21,7 @@ # Grab the picture # Using PIL would be something like: # im = ImageGrab(bbox=bbox) - im = sct.grab(bbox) # type: ignore + im = sct.grab(bbox) # Save it! mss.tools.to_png(im.rgb, im.size, output="screenshot.png") diff --git a/docs/source/examples/linux_display_keyword.py b/docs/source/examples/linux_display_keyword.py index d03341df..a0b7b409 100644 --- a/docs/source/examples/linux_display_keyword.py +++ b/docs/source/examples/linux_display_keyword.py @@ -4,10 +4,8 @@ Usage example with a specific display. """ - import mss - with mss.mss(display=":0.0") as sct: for filename in sct.save(): print(filename) diff --git a/docs/source/examples/opencv_numpy.py b/docs/source/examples/opencv_numpy.py index 46e05e03..81130ad3 100644 --- a/docs/source/examples/opencv_numpy.py +++ b/docs/source/examples/opencv_numpy.py @@ -4,13 +4,12 @@ OpenCV/Numpy example. """ - import time import cv2 -import mss import numpy +import mss with mss.mss() as sct: # Part of the screen to capture @@ -29,7 +28,7 @@ # cv2.imshow('OpenCV/Numpy grayscale', # cv2.cvtColor(img, cv2.COLOR_BGRA2GRAY)) - print("fps: {}".format(1 / (time.time() - last_time))) + print(f"fps: {1 / (time.time() - last_time)}") # Press "q" to quit if cv2.waitKey(25) & 0xFF == ord("q"): diff --git a/docs/source/examples/part_of_screen.py b/docs/source/examples/part_of_screen.py index e4705a58..73f93cb6 100644 --- a/docs/source/examples/part_of_screen.py +++ b/docs/source/examples/part_of_screen.py @@ -4,11 +4,9 @@ Example to capture part of the screen. """ - import mss import mss.tools - with mss.mss() as sct: # The screen part to capture monitor = {"top": 160, "left": 160, "width": 160, "height": 135} diff --git a/docs/source/examples/part_of_screen_monitor_2.py b/docs/source/examples/part_of_screen_monitor_2.py index 9bbc771f..61f58f76 100644 --- a/docs/source/examples/part_of_screen_monitor_2.py +++ b/docs/source/examples/part_of_screen_monitor_2.py @@ -4,11 +4,9 @@ Example to capture part of the screen of the monitor 2. """ - import mss import mss.tools - with mss.mss() as sct: # Get information of monitor 2 monitor_number = 2 diff --git a/docs/source/examples/pil.py b/docs/source/examples/pil.py index 4d8e9729..db10f1b4 100644 --- a/docs/source/examples/pil.py +++ b/docs/source/examples/pil.py @@ -4,10 +4,9 @@ PIL example using frombytes(). """ - -import mss from PIL import Image +import mss with mss.mss() as sct: # Get rid of the first, as it represents the "All in One" monitor: @@ -21,6 +20,6 @@ # img = Image.frombytes('RGB', sct_img.size, sct_img.rgb) # And save it! - output = "monitor-{}.png".format(num) + output = f"monitor-{num}.png" img.save(output) print(output) diff --git a/docs/source/examples/pil_pixels.py b/docs/source/examples/pil_pixels.py index 11081746..fcedcec7 100644 --- a/docs/source/examples/pil_pixels.py +++ b/docs/source/examples/pil_pixels.py @@ -4,10 +4,9 @@ PIL examples to play with pixels. """ - -import mss from PIL import Image +import mss with mss.mss() as sct: # Get a screenshot of the 1st monitor @@ -17,7 +16,7 @@ img = Image.new("RGB", sct_img.size) # Best solution: create a list(tuple(R, G, B), ...) for putdata() - pixels = zip(sct_img.raw[2::4], sct_img.raw[1::4], sct_img.raw[0::4]) + pixels = zip(sct_img.raw[2::4], sct_img.raw[1::4], sct_img.raw[::4]) img.putdata(list(pixels)) # But you can set individual pixels too (slower) diff --git a/docs/source/support.rst b/docs/source/support.rst index af421925..ed40ddad 100644 --- a/docs/source/support.rst +++ b/docs/source/support.rst @@ -5,7 +5,7 @@ Support Feel free to try MSS on a system we had not tested, and let us know by creating an `issue `_. - OS: GNU/Linux, macOS and Windows - - Python: 3.5 and newer + - Python: 3.6 and newer Future @@ -31,3 +31,4 @@ Abandoned - Python 3.2 (2016-10-08) - Python 3.3 (2017-12-05) - Python 3.4 (2018-03-19) +- Python 3.5 (2022-10-27) diff --git a/mss/__init__.py b/mss/__init__.py index 25b7ab5a..c23dc87d 100644 --- a/mss/__init__.py +++ b/mss/__init__.py @@ -8,20 +8,19 @@ https://github.com/BoboTiG/python-mss If that URL should fail, try contacting the author. """ - from .exception import ScreenShotError from .factory import mss -__version__ = "6.1.0" +__version__ = "7.0.0" __author__ = "Mickaël 'Tiger-222' Schoentgen" __copyright__ = """ - Copyright (c) 2013-2020, Mickaël 'Tiger-222' Schoentgen +Copyright (c) 2013-2022, Mickaël 'Tiger-222' Schoentgen - Permission to use, copy, modify, and distribute this software and its - documentation for any purpose and without fee or royalty is hereby - granted, provided that the above copyright notice appear in all copies - and that both that copyright notice and this permission notice appear - in supporting documentation or portions thereof, including - modifications, that you make. +Permission to use, copy, modify, and distribute this software and its +documentation for any purpose and without fee or royalty is hereby +granted, provided that the above copyright notice appear in all copies +and that both that copyright notice and this permission notice appear +in supporting documentation or portions thereof, including +modifications, that you make. """ __all__ = ("ScreenShotError", "mss") diff --git a/mss/__main__.py b/mss/__main__.py index 939e7ae0..636730fb 100644 --- a/mss/__main__.py +++ b/mss/__main__.py @@ -2,24 +2,18 @@ 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 typing import TYPE_CHECKING +from typing import List, Optional from . import __version__ from .exception import ScreenShotError from .factory import mss from .tools import to_png -if TYPE_CHECKING: - from typing import List, Optional # noqa - -def main(args=None): - # type: (Optional[List[str]]) -> int - """ Main logic. """ +def main(args: Optional[List[str]] = None) -> int: + """Main logic.""" cli_args = ArgumentParser() cli_args.add_argument( @@ -88,4 +82,6 @@ def main(args=None): if __name__ == "__main__": + import sys + sys.exit(main(sys.argv[1:])) diff --git a/mss/base.py b/mss/base.py index 880ccfa3..bccc2799 100644 --- a/mss/base.py +++ b/mss/base.py @@ -2,69 +2,57 @@ This is part of the MSS Python's module. Source: https://github.com/BoboTiG/python-mss """ - from abc import ABCMeta, abstractmethod from datetime import datetime -from typing import TYPE_CHECKING from threading import Lock +from typing import Any, Callable, Iterator, List, Optional, Tuple, Type, Union from .exception import ScreenShotError +from .models import Monitor, Monitors from .screenshot import ScreenShot from .tools import to_png -if TYPE_CHECKING: - # pylint: disable=ungrouped-imports - from typing import Any, Callable, Iterator, List, Optional, Type # noqa - - from .models import Monitor, Monitors # noqa - - lock = Lock() class MSSBase(metaclass=ABCMeta): - """ This class will be overloaded by a system specific one. """ + """This class will be overloaded by a system specific one.""" __slots__ = {"_monitors", "cls_image", "compression_level"} - def __init__(self): - self.cls_image = ScreenShot # type: Type[ScreenShot] + def __init__(self) -> None: + self.cls_image: Type[ScreenShot] = ScreenShot self.compression_level = 6 - self._monitors = [] # type: Monitors + self._monitors: Monitors = [] - def __enter__(self): - # type: () -> MSSBase - """ For the cool call `with MSS() as mss:`. """ + def __enter__(self) -> "MSSBase": + """For the cool call `with MSS() as mss:`.""" return self - def __exit__(self, *_): - """ For the cool call `with MSS() as mss:`. """ + def __exit__(self, *_: Any) -> None: + """For the cool call `with MSS() as mss:`.""" self.close() @abstractmethod - def _grab_impl(self, monitor): - # type: (Monitor) -> ScreenShot + def _grab_impl(self, monitor: Monitor) -> ScreenShot: """ 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): - # type: () -> None + def _monitors_impl(self) -> None: """ Get positions of monitors (has to be run using a threading lock). It must populate self._monitors. """ - def close(self): - # type: () -> None - """ Clean-up. """ + def close(self) -> None: + """Clean-up.""" - def grab(self, monitor): - # type: (Monitor) -> ScreenShot + def grab(self, monitor: Union[Monitor, Tuple[int, int, int, int]]) -> ScreenShot: """ Retrieve screen pixels for a given monitor. @@ -88,8 +76,7 @@ def grab(self, monitor): return self._grab_impl(monitor) @property - def monitors(self): - # type: () -> Monitors + def monitors(self) -> Monitors: """ Get positions of all monitors. If the monitor has rotation, you have to deal with it @@ -115,8 +102,12 @@ def monitors(self): return self._monitors - def save(self, mon=0, output="monitor-{mon}.png", callback=None): - # type: (int, str, Callable[[str], None]) -> Iterator[str] + def save( + self, + mon: int = 0, + output: str = "monitor-{mon}.png", + callback: Callable[[str], None] = None, + ) -> Iterator[str]: """ Grab a screen shot and save it to a file. @@ -165,7 +156,7 @@ def save(self, mon=0, output="monitor-{mon}.png", callback=None): monitor = monitors[mon] except IndexError: # pylint: disable=raise-missing-from - raise ScreenShotError("Monitor {!r} does not exist.".format(mon)) + raise ScreenShotError(f"Monitor {mon!r} does not exist.") output = output.format(mon=mon, date=datetime.now(), **monitor) if callable(callback): @@ -174,8 +165,7 @@ def save(self, mon=0, output="monitor-{mon}.png", callback=None): to_png(sct.rgb, sct.size, level=self.compression_level, output=output) yield output - def shot(self, **kwargs): - # type: (Any) -> str + def shot(self, **kwargs: Any) -> str: """ Helper to save the screen shot of the 1st monitor, by default. You can pass the same arguments as for ``save``. @@ -185,9 +175,14 @@ def shot(self, **kwargs): return next(self.save(**kwargs)) @staticmethod - def _cfactory(attr, func, argtypes, restype, errcheck=None): - # type: (Any, str, List[Any], Any, Optional[Callable]) -> None - """ Factory to create a ctypes function and automatically manage errors. """ + def _cfactory( + attr: Any, + func: str, + argtypes: List[Any], + restype: Any, + errcheck: Optional[Callable] = None, + ) -> None: + """Factory to create a ctypes function and automatically manage errors.""" meth = getattr(attr, func) meth.argtypes = argtypes diff --git a/mss/darwin.py b/mss/darwin.py index 6de21e34..2e1ca472 100644 --- a/mss/darwin.py +++ b/mss/darwin.py @@ -2,7 +2,6 @@ This is part of the MSS Python's module. Source: https://github.com/BoboTiG/python-mss """ - import ctypes import ctypes.util import sys @@ -12,61 +11,53 @@ c_double, c_float, c_int32, - c_uint64, c_ubyte, c_uint32, + c_uint64, c_void_p, ) from platform import mac_ver -from typing import TYPE_CHECKING +from typing import Any, Type, Union from .base import MSSBase from .exception import ScreenShotError -from .screenshot import Size - -if TYPE_CHECKING: - from typing import Any, List, Type, Union # noqa - - from .models import Monitor, Monitors # noqa - from .screenshot import ScreenShot # noqa +from .models import CFunctions, Monitor +from .screenshot import ScreenShot, Size __all__ = ("MSS",) -def cgfloat(): - # type: () -> Union[Type[c_double], Type[c_float]] - """ Get the appropriate value for a float. """ +def cgfloat() -> Union[Type[c_double], Type[c_float]]: + """Get the appropriate value for a float.""" - return c_double if sys.maxsize > 2 ** 32 else c_float + return c_double if sys.maxsize > 2**32 else c_float class CGPoint(Structure): - """ Structure that contains coordinates of a rectangle. """ + """Structure that contains coordinates of a rectangle.""" _fields_ = [("x", cgfloat()), ("y", cgfloat())] - def __repr__(self): - return "{}(left={} top={})".format(type(self).__name__, self.x, self.y) + def __repr__(self) -> str: + return f"{type(self).__name__}(left={self.x} top={self.y})" class CGSize(Structure): - """ Structure that contains dimensions of an rectangle. """ + """Structure that contains dimensions of an rectangle.""" _fields_ = [("width", cgfloat()), ("height", cgfloat())] - def __repr__(self): - return "{}(width={} height={})".format( - type(self).__name__, self.width, self.height - ) + def __repr__(self) -> str: + return f"{type(self).__name__}(width={self.width} height={self.height})" class CGRect(Structure): - """ Structure that contains information about a rectangle. """ + """Structure that contains information about a rectangle.""" _fields_ = [("origin", CGPoint), ("size", CGSize)] - def __repr__(self): - return "{}<{} {}>".format(type(self).__name__, self.origin, self.size) + def __repr__(self) -> str: + return f"{type(self).__name__}<{self.origin} {self.size}>" # C functions that will be initialised later. @@ -77,7 +68,7 @@ def __repr__(self): # Available attr: core. # # Note: keep it sorted by cfunction. -CFUNCTIONS = { +CFUNCTIONS: CFunctions = { "CGDataProviderCopyData": ("core", [c_void_p], c_void_p), "CGDisplayBounds": ("core", [c_uint32], CGRect), "CGDisplayRotation": ("core", [c_uint32], c_float), @@ -113,8 +104,8 @@ class MSS(MSSBase): __slots__ = {"core", "max_displays"} - def __init__(self, **_): - """ macOS initialisations. """ + def __init__(self, **_: Any) -> None: + """macOS initialisations.""" super().__init__() @@ -123,8 +114,8 @@ def __init__(self, **_): self._init_library() self._set_cfunctions() - def _init_library(self): - """ Load the CoreGraphics library. """ + def _init_library(self) -> None: + """Load the CoreGraphics library.""" version = float(".".join(mac_ver()[0].split(".")[:2])) if version < 10.16: coregraphics = ctypes.util.find_library("CoreGraphics") @@ -137,9 +128,8 @@ def _init_library(self): raise ScreenShotError("No CoreGraphics library found.") self.core = ctypes.cdll.LoadLibrary(coregraphics) - def _set_cfunctions(self): - # type: () -> None - """ Set all ctypes functions and attach them to attributes. """ + def _set_cfunctions(self) -> None: + """Set all ctypes functions and attach them to attributes.""" cfactory = self._cfactory attrs = {"core": self.core} @@ -147,13 +137,12 @@ def _set_cfunctions(self): cfactory( attr=attrs[attr], func=func, - argtypes=argtypes, # type: ignore + argtypes=argtypes, restype=restype, ) - def _monitors_impl(self): - # type: () -> None - """ Get positions of monitors. It will populate self._monitors. """ + def _monitors_impl(self) -> None: + """Get positions of monitors. It will populate self._monitors.""" int_ = int core = self.core @@ -199,9 +188,8 @@ def _monitors_impl(self): "height": int_(all_monitors.size.height), } - def _grab_impl(self, monitor): - # type: (Monitor) -> ScreenShot - """ Retrieve all pixels from a monitor. Pixels have to be RGB. """ + def _grab_impl(self, monitor: Monitor) -> ScreenShot: + """Retrieve all pixels from a monitor. Pixels have to be RGB.""" # pylint: disable=too-many-locals diff --git a/mss/exception.py b/mss/exception.py index e783175b..028a1d72 100644 --- a/mss/exception.py +++ b/mss/exception.py @@ -2,17 +2,12 @@ This is part of the MSS Python's module. Source: https://github.com/BoboTiG/python-mss """ - -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from typing import Any, Dict # noqa +from typing import Any, Dict class ScreenShotError(Exception): - """ Error handling class. """ + """Error handling class.""" - def __init__(self, message, details=None): - # type: (str, Dict[str, Any]) -> None + def __init__(self, message: str, details: Dict[str, Any] = None) -> None: super().__init__(message) self.details = details or {} diff --git a/mss/factory.py b/mss/factory.py index 902ce069..30e15c2a 100644 --- a/mss/factory.py +++ b/mss/factory.py @@ -2,29 +2,22 @@ This is part of the MSS Python's module. Source: https://github.com/BoboTiG/python-mss """ - import platform -from typing import TYPE_CHECKING +from typing import Any +from .base import MSSBase from .exception import ScreenShotError -if TYPE_CHECKING: - from typing import Any # noqa - - from .base import MSSBase # noqa - - -def mss(**kwargs): - # type: (Any) -> MSSBase - """ Factory returning a proper MSS class instance. +def mss(**kwargs: Any) -> MSSBase: + """Factory returning a proper MSS class instance. - It detects the platform we are running on - and chooses the most adapted mss_class to take - screenshots. + It detects the platform we are running on + and chooses the most adapted mss_class to take + screenshots. - It then proxies its arguments to the class for - instantiation. + It then proxies its arguments to the class for + instantiation. """ # pylint: disable=import-outside-toplevel @@ -45,4 +38,4 @@ def mss(**kwargs): return windows.MSS(**kwargs) - raise ScreenShotError("System {!r} not (yet?) implemented.".format(os_)) + raise ScreenShotError(f"System {os_!r} not (yet?) implemented.") diff --git a/mss/linux.py b/mss/linux.py index b952916c..e676763f 100644 --- a/mss/linux.py +++ b/mss/linux.py @@ -2,14 +2,14 @@ This is part of the MSS Python's module. Source: https://github.com/BoboTiG/python-mss """ - +import contextlib import ctypes import ctypes.util import os import threading from ctypes import ( - POINTER, CFUNCTYPE, + POINTER, Structure, c_char_p, c_int, @@ -23,17 +23,12 @@ c_void_p, ) from types import SimpleNamespace -from typing import TYPE_CHECKING +from typing import Any, Dict, Optional, Tuple, Union from .base import MSSBase, lock from .exception import ScreenShotError - -if TYPE_CHECKING: - from typing import Any, Dict, List, Optional, Tuple, Union # noqa - - from .models import Monitor, Monitors # noqa - from .screenshot import ScreenShot # noqa - +from .models import CFunctions, Monitor +from .screenshot import ScreenShot __all__ = ("MSS",) @@ -68,7 +63,7 @@ class Event(Structure): class XWindowAttributes(Structure): - """ Attributes for the specified window. """ + """Attributes for the specified window.""" _fields_ = [ ("x", c_int32), @@ -123,7 +118,7 @@ class XImage(Structure): class XRRModeInfo(Structure): - """ Voilà, voilà. """ + """Voilà, voilà.""" class XRRScreenResources(Structure): @@ -145,7 +140,7 @@ class XRRScreenResources(Structure): class XRRCrtcInfo(Structure): - """ Structure that contains CRTC information. """ + """Structure that contains CRTC information.""" _fields_ = [ ("timestamp", c_ulong), @@ -164,10 +159,8 @@ class XRRCrtcInfo(Structure): @CFUNCTYPE(c_int, POINTER(Display), POINTER(Event)) -def error_handler(_, event): - # type: (Any, Any) -> int - """ Specifies the program's supplied error handler. """ - +def error_handler(_: Any, event: Any) -> int: + """Specifies the program's supplied error handler.""" evt = event.contents ERROR.details = { "type": evt.type, @@ -179,16 +172,16 @@ def error_handler(_, event): return 0 -def validate(retval, func, args): - # type: (int, Any, Tuple[Any, Any]) -> Optional[Tuple[Any, Any]] - """ Validate the returned value of a Xlib or XRANDR function. """ +def validate( + retval: int, func: Any, args: Tuple[Any, Any] +) -> Optional[Tuple[Any, Any]]: + """Validate the returned value of a Xlib or XRANDR function.""" if retval != 0 and not ERROR.details: return args - err = "{}() failed".format(func.__name__) details = {"retval": retval, "args": args} - raise ScreenShotError(err, details=details) + raise ScreenShotError(f"{func.__name__}() failed", details=details) # C functions that will be initialised later. @@ -200,7 +193,7 @@ def validate(retval, func, args): # Available attr: xlib, xrandr. # # Note: keep it sorted by cfunction. -CFUNCTIONS = { +CFUNCTIONS: CFunctions = { "XDefaultRootWindow": ("xlib", [POINTER(Display)], POINTER(XWindowAttributes)), "XDestroyImage": ("xlib", [POINTER(XImage)], c_void_p), "XGetErrorText": ("xlib", [POINTER(Display), c_int, c_char_p, c_int], c_void_p), @@ -265,11 +258,10 @@ class MSS(MSSBase): __slots__ = {"drawable", "root", "xlib", "xrandr"} # A dict to maintain *display* values created by multiple threads. - _display_dict = {} # type: Dict[threading.Thread, int] + _display_dict: Dict[threading.Thread, int] = {} - def __init__(self, display=None): - # type: (Optional[Union[bytes, str]]) -> None - """ GNU/Linux initialisations. """ + def __init__(self, display: Optional[Union[bytes, str]] = None) -> None: + """GNU/Linux initialisations.""" super().__init__() @@ -284,7 +276,7 @@ def __init__(self, display=None): display = display.encode("utf-8") if b":" not in display: - raise ScreenShotError("Bad display value: {!r}.".format(display)) + raise ScreenShotError(f"Bad display value: {display!r}.") x11 = ctypes.util.find_library("X11") if not x11: @@ -311,8 +303,7 @@ def __init__(self, display=None): # expected LP_Display instance instead of LP_XWindowAttributes self.drawable = ctypes.cast(self.root, POINTER(Display)) - def has_extension(self, extension): - # type: (str) -> bool + def has_extension(self, extension: str) -> bool: """Return True if the given *extension* is part of the extensions list of the server.""" with lock: major_opcode_return = c_int() @@ -332,7 +323,7 @@ def has_extension(self, extension): else: return True - def _get_display(self, disp=None): + def _get_display(self, disp: Optional[bytes] = None) -> int: """ Retrieve a thread-safe display from XOpenDisplay(). In multithreading, if the thread that creates *display* is dead, *display* will @@ -342,15 +333,18 @@ def _get_display(self, disp=None): *display* value first. """ cur_thread, main_thread = threading.current_thread(), threading.main_thread() - display = MSS._display_dict.get(cur_thread) or MSS._display_dict.get( + current_display = MSS._display_dict.get(cur_thread) or MSS._display_dict.get( main_thread ) - if not display: - display = MSS._display_dict[cur_thread] = self.xlib.XOpenDisplay(disp) + if current_display: + display = current_display + else: + display = self.xlib.XOpenDisplay(disp) + MSS._display_dict[cur_thread] = display return display - def _set_cfunctions(self): - """ Set all ctypes functions and attach them to attributes. """ + def _set_cfunctions(self) -> None: + """Set all ctypes functions and attach them to attributes.""" cfactory = self._cfactory attrs = { @@ -358,22 +352,19 @@ def _set_cfunctions(self): "xrandr": self.xrandr, } for func, (attr, argtypes, restype) in CFUNCTIONS.items(): - try: + with contextlib.suppress(AttributeError): cfactory( attr=attrs[attr], errcheck=validate, func=func, argtypes=argtypes, restype=restype, - ) # type: ignore - except AttributeError: - pass + ) - def get_error_details(self): - # type: () -> Optional[Dict[str, Any]] - """ Get more information about the latest X server error. """ + def get_error_details(self) -> Optional[Dict[str, Any]]: + """Get more information about the latest X server error.""" - details = {} # type: Dict[str, Any] + details: Dict[str, Any] = {} if ERROR.details: details = {"xerror_details": ERROR.details} @@ -391,9 +382,8 @@ def get_error_details(self): return details - def _monitors_impl(self): - # type: () -> None - """ Get positions of monitors. It will populate self._monitors. """ + def _monitors_impl(self) -> None: + """Get positions of monitors. It will populate self._monitors.""" display = self._get_display() int_ = int @@ -439,9 +429,8 @@ def _monitors_impl(self): xrandr.XRRFreeCrtcInfo(crtc) xrandr.XRRFreeScreenResources(mon) - def _grab_impl(self, monitor): - # type: (Monitor) -> ScreenShot - """ Retrieve all pixels from a monitor. Pixels have to be RGB. """ + def _grab_impl(self, monitor: Monitor) -> ScreenShot: + """Retrieve all pixels from a monitor. Pixels have to be RGB.""" ximage = self.xlib.XGetImage( self._get_display(), @@ -458,9 +447,7 @@ def _grab_impl(self, monitor): bits_per_pixel = ximage.contents.bits_per_pixel if bits_per_pixel != 32: raise ScreenShotError( - "[XImage] bits per pixel value not (yet?) implemented: {}.".format( - bits_per_pixel - ) + f"[XImage] bits per pixel value not (yet?) implemented: {bits_per_pixel}." ) raw_data = ctypes.cast( diff --git a/mss/models.py b/mss/models.py index fe5b6063..9c0851a5 100644 --- a/mss/models.py +++ b/mss/models.py @@ -4,8 +4,7 @@ """ import collections -from typing import Dict, List, Tuple - +from typing import Any, Dict, List, Tuple Monitor = Dict[str, int] Monitors = List[Monitor] @@ -15,3 +14,5 @@ Pos = collections.namedtuple("Pos", "left, top") Size = collections.namedtuple("Size", "width, height") + +CFunctions = Dict[str, Tuple[str, List[Any], Any]] diff --git a/mss/screenshot.py b/mss/screenshot.py index 6ed6e9ba..71ebc2c1 100644 --- a/mss/screenshot.py +++ b/mss/screenshot.py @@ -3,15 +3,10 @@ Source: https://github.com/BoboTiG/python-mss """ -from typing import TYPE_CHECKING +from typing import Any, Dict, Iterator, Optional, Type -from .models import Size, Pos from .exception import ScreenShotError - -if TYPE_CHECKING: - from typing import Any, Dict, Iterator, Optional # noqa - - from .models import Monitor, Pixel, Pixels # noqa +from .models import Monitor, Pixel, Pixels, Pos, Size class ScreenShot: @@ -26,11 +21,11 @@ class ScreenShot: __slots__ = {"__pixels", "__rgb", "pos", "raw", "size"} - def __init__(self, data, monitor, size=None): - # type: (bytearray, Monitor, Optional[Size]) -> None - - self.__pixels = None # type: Optional[Pixels] - self.__rgb = None # type: Optional[bytes] + def __init__( + self, data: bytearray, monitor: Monitor, size: Optional[Size] = None + ) -> None: + self.__pixels: Optional[Pixels] = None + self.__rgb: Optional[bytes] = None #: Bytearray of the raw BGRA pixels retrieved by ctypes #: OS independent implementations. @@ -39,20 +34,14 @@ def __init__(self, data, monitor, size=None): #: NamedTuple of the screen shot coordinates. self.pos = Pos(monitor["left"], monitor["top"]) - if size is not None: - #: NamedTuple of the screen shot size. - self.size = size - else: - self.size = Size(monitor["width"], monitor["height"]) + #: NamedTuple of the screen shot size. + self.size = Size(monitor["width"], monitor["height"]) if size is None else size - def __repr__(self): - return ("<{!s} pos={cls.left},{cls.top} size={cls.width}x{cls.height}>").format( - type(self).__name__, cls=self - ) + def __repr__(self) -> str: + return f"<{type(self).__name__} pos={self.left},{self.top} size={self.width}x{self.height}>" @property - def __array_interface__(self): - # type: () -> Dict[str, Any] + def __array_interface__(self) -> Dict[str, Any]: """ Numpy array interface support. It uses raw data in BGRA form. @@ -68,49 +57,44 @@ def __array_interface__(self): } @classmethod - def from_size(cls, data, width, height): - # type: (bytearray, int, int) -> ScreenShot - """ Instantiate a new class given only screen shot's data and size. """ - + 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) @property - def bgra(self): - # type: () -> bytes - """ BGRA values from the BGRA raw pixels. """ + def bgra(self) -> bytes: + """BGRA values from the BGRA raw pixels.""" return bytes(self.raw) @property - def height(self): - # type: () -> int - """ Convenient accessor to the height size. """ + def height(self) -> int: + """Convenient accessor to the height size.""" return self.size.height @property - def left(self): - # type: () -> int - """ Convenient accessor to the left position. """ + def left(self) -> int: + """Convenient accessor to the left position.""" return self.pos.left @property - def pixels(self): - # type: () -> Pixels + def pixels(self) -> Pixels: """ :return list: RGB tuples. """ if not self.__pixels: - rgb_tuples = zip( - self.raw[2::4], self.raw[1::4], self.raw[0::4] - ) # type: Iterator[Pixel] + 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 return self.__pixels @property - def rgb(self): - # type: () -> bytes + def rgb(self) -> bytes: """ Compute RGB values from the BGRA raw pixels. @@ -120,27 +104,24 @@ def rgb(self): if not self.__rgb: rgb = bytearray(self.height * self.width * 3) raw = self.raw - rgb[0::3] = raw[2::4] + rgb[::3] = raw[2::4] rgb[1::3] = raw[1::4] - rgb[2::3] = raw[0::4] + rgb[2::3] = raw[::4] self.__rgb = bytes(rgb) return self.__rgb @property - def top(self): - # type: () -> int - """ Convenient accessor to the top position. """ + def top(self) -> int: + """Convenient accessor to the top position.""" return self.pos.top @property - def width(self): - # type: () -> int - """ Convenient accessor to the width size. """ + def width(self) -> int: + """Convenient accessor to the width size.""" return self.size.width - def pixel(self, coord_x, coord_y): - # type: (int, int) -> Pixel + def pixel(self, coord_x: int, coord_y: int) -> Pixel: """ Returns the pixel value at a given position. @@ -154,5 +135,5 @@ def pixel(self, coord_x, coord_y): except IndexError: # pylint: disable=raise-missing-from raise ScreenShotError( - "Pixel location ({}, {}) is out of range.".format(coord_x, coord_y) + f"Pixel location ({coord_x}, {coord_y}) is out of range." ) diff --git a/mss/tests/bench_bgra2rgb.py b/mss/tests/bench_bgra2rgb.py index 2560f900..23196842 100644 --- a/mss/tests/bench_bgra2rgb.py +++ b/mss/tests/bench_bgra2rgb.py @@ -28,13 +28,13 @@ numpy_flip 25 numpy_slice 22 """ - import time -import mss import numpy from PIL import Image +import mss + def mss_rgb(im): return im.rgb diff --git a/mss/tests/bench_general.py b/mss/tests/bench_general.py index dab3fb27..1fe44cc5 100644 --- a/mss/tests/bench_general.py +++ b/mss/tests/bench_general.py @@ -25,7 +25,6 @@ access_rgb 574 712 +24.04 output 139 188 +35.25 """ - from time import time import mss diff --git a/mss/tests/conftest.py b/mss/tests/conftest.py index f32869e5..fc6a346c 100644 --- a/mss/tests/conftest.py +++ b/mss/tests/conftest.py @@ -2,16 +2,30 @@ This is part of the MSS Python's module. Source: https://github.com/BoboTiG/python-mss """ - import glob import os import pytest + import mss +@pytest.fixture(autouse=True) +def no_warnings(recwarn): + """Fail on warning.""" + + yield + + warnings = [ + "{w.filename}:{w.lineno} {w.message}".format(w=warning) for warning in recwarn + ] + for warning in warnings: + print(warning) + assert not warnings + + def purge_files(): - """ Remove all generated files from previous runs. """ + """Remove all generated files from previous runs.""" for fname in glob.glob("*.png"): print("Deleting {!r} ...".format(fname)) @@ -55,7 +69,5 @@ def pixel_ratio(sct): # Grab a 1x1 screenshot region = {"top": 0, "left": 0, "width": 1, "height": 1} - # On macOS with Retina display,the width will be 2 instead of 1 - pixel_size = sct.grab(region).size[0] - - return pixel_size + # On macOS with Retina display, the width will be 2 instead of 1 + return sct.grab(region).size[0] diff --git a/mss/tests/test_bgra_to_rgb.py b/mss/tests/test_bgra_to_rgb.py index ee64ed70..9a29bc34 100644 --- a/mss/tests/test_bgra_to_rgb.py +++ b/mss/tests/test_bgra_to_rgb.py @@ -2,8 +2,8 @@ This is part of the MSS Python's module. Source: https://github.com/BoboTiG/python-mss """ - import pytest + from mss.base import ScreenShot diff --git a/mss/tests/test_get_pixels.py b/mss/tests/test_get_pixels.py index e340e5cf..0abf4d94 100644 --- a/mss/tests/test_get_pixels.py +++ b/mss/tests/test_get_pixels.py @@ -2,8 +2,8 @@ This is part of the MSS Python's module. Source: https://github.com/BoboTiG/python-mss """ - import pytest + from mss.base import ScreenShot from mss.exception import ScreenShotError diff --git a/mss/tests/test_gnu_linux.py b/mss/tests/test_gnu_linux.py index 10119760..4596ffbf 100644 --- a/mss/tests/test_gnu_linux.py +++ b/mss/tests/test_gnu_linux.py @@ -2,17 +2,16 @@ This is part of the MSS Python's module. Source: https://github.com/BoboTiG/python-mss """ - import ctypes.util import os import platform -import mss import pytest + +import mss from mss.base import MSSBase from mss.exception import ScreenShotError - if platform.system().lower() != "linux": pytestmark = pytest.mark.skip @@ -97,9 +96,7 @@ def find_lib_mocked(lib): It is a naive approach, but works for now. """ - if lib == "Xrandr": - return None - return x11 + return None if lib == "Xrandr" else x11 # No `Xrandr` library monkeypatch.setattr(ctypes.util, "find_library", find_lib_mocked) diff --git a/mss/tests/test_implementation.py b/mss/tests/test_implementation.py index ba788ef5..f278c97b 100644 --- a/mss/tests/test_implementation.py +++ b/mss/tests/test_implementation.py @@ -2,12 +2,12 @@ This is part of the MSS Python's module. Source: https://github.com/BoboTiG/python-mss """ - import os import os.path import platform import pytest + import mss import mss.tools from mss.base import MSSBase @@ -16,20 +16,20 @@ class MSS0(MSSBase): - """ Nothing implemented. """ + """Nothing implemented.""" pass class MSS1(MSSBase): - """ Only `grab()` implemented. """ + """Only `grab()` implemented.""" def grab(self, monitor): pass class MSS2(MSSBase): - """ Only `monitor` implemented. """ + """Only `monitor` implemented.""" @property def monitors(self): @@ -76,9 +76,10 @@ def test_factory(monkeypatch): def test_entry_point(capsys, sct): - from mss.__main__ import main from datetime import datetime + from mss.__main__ import main + for opt in ("-m", "--monitor"): main([opt, "1"]) out, _ = capsys.readouterr() @@ -113,9 +114,9 @@ def test_entry_point(capsys, sct): os.remove(filename) coordinates = "2,12,40,67" + filename = "sct-2x12_40x67.png" for opt in ("-c", "--coordinates"): main([opt, coordinates]) - filename = "sct-2x12_40x67.png" out, _ = capsys.readouterr() assert out.endswith(filename + "\n") assert os.path.isfile(filename) diff --git a/mss/tests/test_leaks.py b/mss/tests/test_leaks.py index e7a6ca1b..ad8147c3 100644 --- a/mss/tests/test_leaks.py +++ b/mss/tests/test_leaks.py @@ -2,24 +2,19 @@ This is part of the MSS Python's module. Source: https://github.com/BoboTiG/python-mss """ - import os import platform -from typing import TYPE_CHECKING +from typing import Callable import pytest -from mss import mss - -if TYPE_CHECKING: - from typing import Callable # noqa +from mss import mss OS = platform.system().lower() PID = os.getpid() -def get_opened_socket(): - # type: () -> int +def get_opened_socket() -> int: """ GNU/Linux: a way to get the opened sockets count. It will be used to check X server connections are well closed. @@ -27,13 +22,12 @@ def get_opened_socket(): import subprocess - cmd = "lsof -U | grep {}".format(PID) + cmd = f"lsof -U | grep {PID}" output = subprocess.check_output(cmd, shell=True) return len(output.splitlines()) -def get_handles(): - # type: () -> int +def get_handles() -> int: """ Windows: a way to get the GDI handles count. It will be used to check the handles count is not growing, showing resource leaks. @@ -48,14 +42,10 @@ def get_handles(): @pytest.fixture -def monitor_func(): - # type: () -> Callable[[], int] - """ OS specific function to check resources in use. """ - - if OS == "linux": - return get_opened_socket +def monitor_func() -> Callable[[], int]: + """OS specific function to check resources in use.""" - return get_handles + return get_opened_socket if OS == "linux" else get_handles def bound_instance_without_cm(): @@ -113,7 +103,7 @@ def regression_issue_135(): ), ) def test_resource_leaks(func, monitor_func): - """ Check for resource leaks with different use cases. """ + """Check for resource leaks with different use cases.""" # Warm-up func() diff --git a/mss/tests/test_macos.py b/mss/tests/test_macos.py index 1cf4e4c8..113b33e1 100644 --- a/mss/tests/test_macos.py +++ b/mss/tests/test_macos.py @@ -2,21 +2,20 @@ This is part of the MSS Python's module. Source: https://github.com/BoboTiG/python-mss """ - import ctypes.util import platform -import mss import pytest -from mss.exception import ScreenShotError +import mss +from mss.exception import ScreenShotError if platform.system().lower() != "darwin": pytestmark = pytest.mark.skip def test_repr(): - from mss.darwin import CGSize, CGPoint, CGRect + from mss.darwin import CGPoint, CGRect, CGSize # CGPoint point = CGPoint(2.0, 1.0) diff --git a/mss/tests/test_save.py b/mss/tests/test_save.py index bc4fbb28..5c11494e 100644 --- a/mss/tests/test_save.py +++ b/mss/tests/test_save.py @@ -2,7 +2,6 @@ This is part of the MSS Python's module. Source: https://github.com/BoboTiG/python-mss """ - import os.path from datetime import datetime @@ -10,8 +9,7 @@ def test_at_least_2_monitors(sct): - shots = list(sct.save(mon=0)) - assert len(shots) >= 1 + assert list(sct.save(mon=0)) def test_files_exist(sct): @@ -27,7 +25,7 @@ def test_files_exist(sct): def test_callback(sct): def on_exists(fname): if os.path.isfile(fname): - new_file = fname + ".old" + new_file = f"{fname}.old" os.rename(fname, new_file) filename = sct.shot(mon=0, output="mon0.png", callback=on_exists) diff --git a/mss/tests/test_setup.py b/mss/tests/test_setup.py index e1a213c4..87f0e914 100644 --- a/mss/tests/test_setup.py +++ b/mss/tests/test_setup.py @@ -2,8 +2,7 @@ This is part of the MSS Python's module. Source: https://github.com/BoboTiG/python-mss """ - -from subprocess import check_output +from subprocess import STDOUT, check_output from mss import __version__ @@ -12,6 +11,9 @@ def test_wheel_python_3_only(): """Ensure the produced wheel is Python 3 only.""" - output = str(check_output(CMD)) - text = "mss-{}-py3-none-any.whl".format(__version__) + output = check_output(CMD, stderr=STDOUT, text=True) + text = f"mss-{__version__}-py3-none-any.whl" assert text in output + + print(output) + assert "warning" not in output.lower() diff --git a/mss/tests/test_third_party.py b/mss/tests/test_third_party.py index e562bf3d..1c2551f0 100644 --- a/mss/tests/test_third_party.py +++ b/mss/tests/test_third_party.py @@ -2,13 +2,12 @@ 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 - try: import numpy except (ImportError, RuntimeError): @@ -38,9 +37,8 @@ def test_pil(sct): assert img.mode == "RGB" assert img.size == sct_img.size - for x in range(width): - for y in range(height): - assert img.getpixel((x, y)) == sct_img.pixel(x, y) + for x, y in itertools.product(range(width), range(height)): + assert img.getpixel((x, y)) == sct_img.pixel(x, y) img.save("box.png") assert os.path.isfile("box.png") @@ -56,9 +54,8 @@ def test_pil_bgra(sct): assert img.mode == "RGB" assert img.size == sct_img.size - for x in range(width): - for y in range(height): - assert img.getpixel((x, y)) == sct_img.pixel(x, y) + for x, y in itertools.product(range(width), range(height)): + assert img.getpixel((x, y)) == sct_img.pixel(x, y) img.save("box-bgra.png") assert os.path.isfile("box-bgra.png") @@ -74,9 +71,8 @@ def test_pil_not_16_rounded(sct): assert img.mode == "RGB" assert img.size == sct_img.size - for x in range(width): - for y in range(height): - assert img.getpixel((x, y)) == sct_img.pixel(x, y) + for x, y in itertools.product(range(width), range(height)): + assert img.getpixel((x, y)) == sct_img.pixel(x, y) img.save("box.png") assert os.path.isfile("box.png") diff --git a/mss/tests/test_tools.py b/mss/tests/test_tools.py index ecdea05d..d1fa286d 100644 --- a/mss/tests/test_tools.py +++ b/mss/tests/test_tools.py @@ -2,14 +2,13 @@ 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.tools import to_png +from mss.tools import to_png WIDTH = 10 HEIGHT = 10 @@ -27,7 +26,7 @@ def test_bad_compression_level(sct): def test_compression_level(sct): data = b"rgb" * WIDTH * HEIGHT - output = "{}x{}.png".format(WIDTH, HEIGHT) + output = f"{WIDTH}x{HEIGHT}.png" to_png(data, (WIDTH, HEIGHT), level=sct.compression_level, output=output) with open(output, "rb") as png: @@ -58,7 +57,7 @@ def test_compression_levels(level, checksum): def test_output_file(): data = b"rgb" * WIDTH * HEIGHT - output = "{}x{}.png".format(WIDTH, HEIGHT) + output = f"{WIDTH}x{HEIGHT}.png" to_png(data, (WIDTH, HEIGHT), output=output) assert os.path.isfile(output) diff --git a/mss/tests/test_windows.py b/mss/tests/test_windows.py index 62f7b6c2..4694a02b 100644 --- a/mss/tests/test_windows.py +++ b/mss/tests/test_windows.py @@ -2,14 +2,13 @@ This is part of the MSS Python's module. Source: https://github.com/BoboTiG/python-mss """ - import platform import threading -import mss import pytest -from mss.exception import ScreenShotError +import mss +from mss.exception import ScreenShotError if platform.system().lower() != "windows": pytestmark = pytest.mark.skip diff --git a/mss/tools.py b/mss/tools.py index aa79b2bf..47fd74ea 100644 --- a/mss/tools.py +++ b/mss/tools.py @@ -6,14 +6,12 @@ import os import struct import zlib -from typing import TYPE_CHECKING +from typing import Optional, Tuple -if TYPE_CHECKING: - from typing import Optional, Tuple # noqa - -def to_png(data, size, level=6, output=None): - # type: (bytes, Tuple[int, int], int, Optional[str]) -> Optional[bytes] +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 the whole PNG data. diff --git a/mss/windows.py b/mss/windows.py index 0e38202e..e5c8eee1 100644 --- a/mss/windows.py +++ b/mss/windows.py @@ -2,11 +2,10 @@ This is part of the MSS Python's module. Source: https://github.com/BoboTiG/python-mss """ - -import sys import ctypes +import sys import threading -from ctypes import POINTER, Structure, WINFUNCTYPE, c_void_p +from ctypes import POINTER, WINFUNCTYPE, Structure, c_void_p from ctypes.wintypes import ( BOOL, DOUBLE, @@ -18,20 +17,17 @@ INT, LONG, LPARAM, + LPRECT, RECT, UINT, WORD, ) -from typing import TYPE_CHECKING +from typing import Any, Dict from .base import MSSBase from .exception import ScreenShotError - -if TYPE_CHECKING: - from typing import Any, Dict # noqa - - from .models import Monitor, Monitors # noqa - from .screenshot import ScreenShot # noqa +from .models import CFunctions, Monitor +from .screenshot import ScreenShot __all__ = ("MSS",) @@ -42,7 +38,7 @@ class BITMAPINFOHEADER(Structure): - """ Information about the dimensions and color format of a DIB. """ + """Information about the dimensions and color format of a DIB.""" _fields_ = [ ("biSize", DWORD), @@ -78,7 +74,7 @@ class BITMAPINFO(Structure): # Available attr: gdi32, user32. # # Note: keep it sorted by cfunction. -CFUNCTIONS = { +CFUNCTIONS: CFunctions = { "BitBlt": ("gdi32", [HDC, INT, INT, INT, INT, HDC, INT, INT, DWORD], BOOL), "CreateCompatibleBitmap": ("gdi32", [HDC, INT, INT], HBITMAP), "CreateCompatibleDC": ("gdi32", [HDC], HDC), @@ -97,7 +93,7 @@ class BITMAPINFO(Structure): class MSS(MSSBase): - """ Multiple ScreenShots implementation for Microsoft Windows. """ + """Multiple ScreenShots implementation for Microsoft Windows.""" __slots__ = {"_bbox", "_bmi", "_data", "gdi32", "user32"} @@ -106,11 +102,10 @@ class MSS(MSSBase): memdc = None # A dict to maintain *srcdc* values created by multiple threads. - _srcdc_dict = {} # type: Dict[threading.Thread, int] + _srcdc_dict: Dict[threading.Thread, int] = {} - def __init__(self, **_): - # type: (Any) -> None - """ Windows initialisations. """ + def __init__(self, **_: Any) -> None: + """Windows initialisations.""" super().__init__() @@ -120,7 +115,7 @@ def __init__(self, **_): self._set_dpi_awareness() self._bbox = {"height": 0, "width": 0} - self._data = ctypes.create_string_buffer(0) # type: ctypes.Array[ctypes.c_char] + self._data: ctypes.Array[ctypes.c_char] = ctypes.create_string_buffer(0) srcdc = self._get_srcdc() if not MSS.memdc: @@ -135,8 +130,8 @@ def __init__(self, **_): bmi.bmiHeader.biClrImportant = 0 # See grab.__doc__ [3] self._bmi = bmi - def _set_cfunctions(self): - """ Set all ctypes functions and attach them to attributes. """ + def _set_cfunctions(self) -> None: + """Set all ctypes functions and attach them to attributes.""" cfactory = self._cfactory attrs = { @@ -149,10 +144,10 @@ def _set_cfunctions(self): func=func, argtypes=argtypes, restype=restype, - ) # type: ignore + ) - def _set_dpi_awareness(self): - """ Set DPI awareness to capture full screen on Hi-DPI monitors. """ + 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 if version >= (6, 3): @@ -166,7 +161,7 @@ def _set_dpi_awareness(self): # Windows Vista, 7, 8 and Server 2012 self.user32.SetProcessDPIAware() - def _get_srcdc(self): + def _get_srcdc(self) -> int: """ Retrieve a thread-safe HDC from GetWindowDC(). In multithreading, if the thread that creates *srcdc* is dead, *srcdc* will @@ -175,14 +170,18 @@ def _get_srcdc(self): Since the current thread and main thread are always alive, reuse their *srcdc* value first. """ cur_thread, main_thread = threading.current_thread(), threading.main_thread() - srcdc = MSS._srcdc_dict.get(cur_thread) or MSS._srcdc_dict.get(main_thread) - if not srcdc: - srcdc = MSS._srcdc_dict[cur_thread] = self.user32.GetWindowDC(0) + current_srcdc = MSS._srcdc_dict.get(cur_thread) or MSS._srcdc_dict.get( + main_thread + ) + if current_srcdc: + srcdc = current_srcdc + else: + srcdc = self.user32.GetWindowDC(0) + MSS._srcdc_dict[cur_thread] = srcdc return srcdc - def _monitors_impl(self): - # type: () -> None - """ Get positions of monitors. It will populate self._monitors. """ + def _monitors_impl(self) -> None: + """Get positions of monitors. It will populate self._monitors.""" int_ = int user32 = self.user32 @@ -199,8 +198,7 @@ def _monitors_impl(self): ) # Each monitor - def _callback(monitor, data, rect, dc_): - # types: (int, HDC, LPRECT, LPARAM) -> int + def _callback(monitor: int, data: HDC, rect: LPRECT, dc_: LPARAM) -> int: """ Callback for monitorenumproc() function, it will return a RECT with appropriate values. @@ -212,8 +210,8 @@ def _callback(monitor, data, rect, dc_): { "left": int_(rct.left), "top": int_(rct.top), - "width": int_(rct.right - rct.left), - "height": int_(rct.bottom - rct.top), + "width": int_(rct.right) - int_(rct.left), + "height": int_(rct.bottom) - int_(rct.top), } ) return 1 @@ -221,8 +219,7 @@ def _callback(monitor, data, rect, dc_): callback = MONITORNUMPROC(_callback) user32.EnumDisplayMonitors(0, 0, callback, 0) - def _grab_impl(self, monitor): - # type: (Monitor) -> ScreenShot + def _grab_impl(self, monitor: Monitor) -> ScreenShot: """ Retrieve all pixels from a monitor. Pixels have to be RGB. diff --git a/mypy.ini b/mypy.ini new file mode 100644 index 00000000..7b3afaf2 --- /dev/null +++ b/mypy.ini @@ -0,0 +1,20 @@ +[mypy] +# Ensure we know what we do +warn_redundant_casts = True +warn_unused_ignores = True +warn_unused_configs = True + +# Imports management +ignore_missing_imports = True +follow_imports = skip + +# Ensure full coverage +disallow_untyped_defs = True +disallow_incomplete_defs = True +disallow_untyped_calls = True + +; Restrict dynamic typing (a little) +; e.g. `x: List[Any]` or x: List` +; disallow_any_generics = True + +strict_equality = True \ No newline at end of file diff --git a/setup.cfg b/setup.cfg index db614f34..c6a2545b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,10 +1,11 @@ [metadata] name = mss -version = 6.1.0 +version = 7.0.0 author = Mickaël 'Tiger-222' Schoentgen author_email = contact@tiger-222.fr description = An ultra fast cross-platform multiple screenshots module in pure python using ctypes. long_description = file: README.rst +long_description_content_type = text/x-rst url = https://github.com/BoboTiG/python-mss home_page = https://pypi.org/project/mss/ project_urls = @@ -22,20 +23,24 @@ classifiers = Programming Language :: Python Programming Language :: Python :: 3 Programming Language :: Python :: 3 :: Only - Programming Language :: Python :: 3.5 Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 + Programming Language :: Python :: 3.10 + Programming Language :: Python :: 3.11 Topic :: Multimedia :: Graphics :: Capture :: Screen Capture Topic :: Software Development :: Libraries [options] zip_safe = False include_package_data = True -packages = mss +packages = find: python_requires = >=3.5 +[options.packages.find] +where = mss + [options.entry_points] console_scripts = mss = mss.__main__:main @@ -48,6 +53,13 @@ ignore = W503 max-line-length = 120 +[isort] +multi_line_output = 3 +include_trailing_comma = True +force_grid_wrap = 0 +use_parentheses = True +line_length = 88 + [tool:pytest] addopts = --showlocals diff --git a/tox.ini b/tox.ini index bfe7ba53..a814f6b5 100644 --- a/tox.ini +++ b/tox.ini @@ -3,7 +3,7 @@ envlist = lint types docs - py{310,39,38,37,36,35,py3} + py{311,310,39,38,37,36,py3} [testenv] passenv = DISPLAY @@ -13,7 +13,7 @@ deps = pytest # Must pin that version to support PyPy3 pypy3: numpy==1.15.4 - py3{9,8,7,6,5}: numpy + py3{11,10,9,8,7,6}: numpy pillow wheel commands = @@ -22,11 +22,15 @@ commands = [testenv:lint] description = Code quality check deps = + black flake8 pylint + isort commands = python -m flake8 docs mss python -m pylint mss + python -m isort --check-only docs mss + python -m black --check docs mss [testenv:types] description = Type annotations check @@ -34,7 +38,7 @@ deps = mypy commands = # "--platform win32" to not fail on ctypes.windll (it does not affect the overall check on other OSes) - python -m mypy --platform win32 --ignore-missing-imports mss docs/source/examples + python -m mypy --platform win32 --exclude mss/tests mss docs/source/examples [testenv:docs] description = Build the documentation From 45471371118173aea044f7cdc35aed371bd8ea5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Thu, 27 Oct 2022 05:59:00 +0200 Subject: [PATCH 049/280] Document upload process --- README.rst | 9 +++++++++ mss/tests/test_setup.py | 9 ++++++--- tox.ini | 2 ++ 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index e076e615..8efc3ca6 100644 --- a/README.rst +++ b/README.rst @@ -47,3 +47,12 @@ You can install it with pip:: Or you can install it with conda:: conda install -c conda-forge python-mss + +Maintenance +----------- + +For the maintainers, here are commands to upload a new release: + + python -m build --sdist --wheel + twine check dist/* + twine upload dist/* diff --git a/mss/tests/test_setup.py b/mss/tests/test_setup.py index 87f0e914..ee4fcd39 100644 --- a/mss/tests/test_setup.py +++ b/mss/tests/test_setup.py @@ -2,18 +2,21 @@ This is part of the MSS Python's module. Source: https://github.com/BoboTiG/python-mss """ -from subprocess import STDOUT, check_output +from subprocess import STDOUT, check_call, check_output from mss import __version__ -CMD = "python setup.py sdist bdist_wheel".split() +INSTALL = "python -m build --sdist --wheel".split() +CHECK = "twine check dist/*".split() def test_wheel_python_3_only(): """Ensure the produced wheel is Python 3 only.""" - output = check_output(CMD, stderr=STDOUT, text=True) + output = check_output(INSTALL, stderr=STDOUT, text=True) text = f"mss-{__version__}-py3-none-any.whl" assert text in output print(output) assert "warning" not in output.lower() + + check_call(CHECK) diff --git a/tox.ini b/tox.ini index a814f6b5..04e8182b 100644 --- a/tox.ini +++ b/tox.ini @@ -9,12 +9,14 @@ envlist = passenv = DISPLAY alwayscopy = True deps = + build flaky pytest # Must pin that version to support PyPy3 pypy3: numpy==1.15.4 py3{11,10,9,8,7,6}: numpy pillow + twine wheel commands = python -m pytest {posargs} From daa3a53572d0e670ef0528b9d05835aade1dc061 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Thu, 27 Oct 2022 06:03:20 +0200 Subject: [PATCH 050/280] doc: tweak --- CHANGELOG | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index b1fddb4e..240eb876 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -9,7 +9,7 @@ History: - MSS: modernized the code base (types, f-string, ran isort & black) - MSS: fixed several Sourcery issues - MSS: fixed typos here, and there - - doc: fixed an error when building with shpinx + - doc: fixed an error when building the documentation 6.1.0 2020/10/31 - MSS: reworked how C functions are initialised From 36435d9c3748d74926243671523d7239708b647a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Thu, 27 Oct 2022 09:08:29 +0200 Subject: [PATCH 051/280] Version 7.0.1 --- CHANGELOG | 3 +++ README.rst | 1 + docs/source/conf.py | 2 +- mss/__init__.py | 2 +- setup.cfg | 6 ++---- 5 files changed, 8 insertions(+), 6 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 240eb876..f5dc7255 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -2,6 +2,9 @@ History: +7.0.1 2022/10/27 + - fixed the wheel package + 7.0.0 2022/10/27 - added support for Python 3.11 - added support for Python 3.10 diff --git a/README.rst b/README.rst index 8efc3ca6..b2d00fc1 100644 --- a/README.rst +++ b/README.rst @@ -53,6 +53,7 @@ Maintenance For the maintainers, here are commands to upload a new release: + rm -rf build dist python -m build --sdist --wheel twine check dist/* twine upload dist/* diff --git a/docs/source/conf.py b/docs/source/conf.py index 35e51fd6..6fabc35f 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -27,7 +27,7 @@ # built documents. # # The short X.Y version. -version = "7.0.0" +version = "7.0.1" # The full version, including alpha/beta/rc tags. release = "latest" diff --git a/mss/__init__.py b/mss/__init__.py index c23dc87d..cca0e48b 100644 --- a/mss/__init__.py +++ b/mss/__init__.py @@ -11,7 +11,7 @@ from .exception import ScreenShotError from .factory import mss -__version__ = "7.0.0" +__version__ = "7.0.1" __author__ = "Mickaël 'Tiger-222' Schoentgen" __copyright__ = """ Copyright (c) 2013-2022, Mickaël 'Tiger-222' Schoentgen diff --git a/setup.cfg b/setup.cfg index c6a2545b..e65e61e5 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = mss -version = 7.0.0 +version = 7.0.1 author = Mickaël 'Tiger-222' Schoentgen author_email = contact@tiger-222.fr description = An ultra fast cross-platform multiple screenshots module in pure python using ctypes. @@ -35,12 +35,10 @@ classifiers = [options] zip_safe = False include_package_data = True +packages_dir = mss packages = find: python_requires = >=3.5 -[options.packages.find] -where = mss - [options.entry_points] console_scripts = mss = mss.__main__:main From 0ae1d48b97e1d7c2621b678308716f9ec40bb4be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Thu, 27 Oct 2022 09:12:25 +0200 Subject: [PATCH 052/280] doc: tweak --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index b2d00fc1..e2ad276a 100644 --- a/README.rst +++ b/README.rst @@ -51,7 +51,7 @@ Or you can install it with conda:: Maintenance ----------- -For the maintainers, here are commands to upload a new release: +For the maintainers, here are commands to upload a new release:: rm -rf build dist python -m build --sdist --wheel From 9035a12d6c717b811a6ff3896a2da786ed93df07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Thu, 27 Oct 2022 14:27:47 +0200 Subject: [PATCH 053/280] doc: fix Anaconda badge --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index e2ad276a..f249d234 100644 --- a/README.rst +++ b/README.rst @@ -9,7 +9,7 @@ Python MSS :target: https://saythanks.io/to/BoboTiG .. image:: https://pepy.tech/badge/mss :target: https://pepy.tech/project/mss -.. image:: https://anaconda.org/conda-forge/python-mss/badges/installer/conda.svg +.. image:: https://anaconda.org/conda-forge/python-mss/badges/version.svg :target: https://anaconda.org/conda-forge/python-mss From d7b899355272670669ef8fe798b977720a48c6af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Thu, 27 Oct 2022 14:29:43 +0200 Subject: [PATCH 054/280] doc: remove "say thanks" badge --- README.rst | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.rst b/README.rst index f249d234..36c90038 100644 --- a/README.rst +++ b/README.rst @@ -5,8 +5,6 @@ Python MSS :target: https://travis-ci.org/BoboTiG/python-mss .. image:: https://ci.appveyor.com/api/projects/status/72dik18r6b746mb0?svg=true :target: https://ci.appveyor.com/project/BoboTiG/python-mss -.. image:: https://img.shields.io/badge/say-thanks-ff69b4.svg - :target: https://saythanks.io/to/BoboTiG .. image:: https://pepy.tech/badge/mss :target: https://pepy.tech/project/mss .. image:: https://anaconda.org/conda-forge/python-mss/badges/version.svg From cf131ce63dccfa46dd02b187957ae4c9edb2afd5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Thu, 27 Oct 2022 14:30:52 +0200 Subject: [PATCH 055/280] doc: polish Python 3.6 minimal support --- README.rst | 2 +- docs/source/index.rst | 2 +- setup.cfg | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index 36c90038..ead20342 100644 --- a/README.rst +++ b/README.rst @@ -22,7 +22,7 @@ Python MSS An ultra fast cross-platform multiple screenshots module in pure python using ctypes. -- **Python 3.5+** and PEP8 compliant, no dependency, thread-safe; +- **Python 3.6+** and PEP8 compliant, no dependency, thread-safe; - very basic, it will grab one screen shot by monitor or a screen shot of all monitors and save it to a PNG file; - but you can use PIL and benefit from all its formats (or add yours directly); - integrate well with Numpy and OpenCV; diff --git a/docs/source/index.rst b/docs/source/index.rst index def5e870..365adab8 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -12,7 +12,7 @@ Welcome to Python MSS's documentation! An ultra fast cross-platform multiple screenshots module in pure python using ctypes. - - **Python 3.5+** and :pep:`8` compliant, no dependency, thread-safe; + - **Python 3.6+** and :pep:`8` compliant, no dependency, thread-safe; - very basic, it will grab one screen shot by monitor or a screen shot of all monitors and save it to a PNG file; - but you can use PIL and benefit from all its formats (or add yours directly); - integrate well with Numpy and OpenCV; diff --git a/setup.cfg b/setup.cfg index e65e61e5..0cac3aef 100644 --- a/setup.cfg +++ b/setup.cfg @@ -37,7 +37,7 @@ zip_safe = False include_package_data = True packages_dir = mss packages = find: -python_requires = >=3.5 +python_requires = >=3.6 [options.entry_points] console_scripts = From c503c5815d2e3cc82a66f8976c500a19f17a180b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Thu, 27 Oct 2022 14:33:40 +0200 Subject: [PATCH 056/280] dev: ignore cache folders --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index e9437f3a..1345012e 100644 --- a/.gitignore +++ b/.gitignore @@ -14,4 +14,5 @@ dist/ .vscode docs/output/ .mypy_cache/ +__pycache__/ venv/ From 40518940e20a4d695c9ec5fd4d53294cbbace696 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Thu, 27 Oct 2022 14:55:12 +0200 Subject: [PATCH 057/280] test: fix test_wheel_python_3_only() --- mss/tests/test_setup.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/mss/tests/test_setup.py b/mss/tests/test_setup.py index ee4fcd39..8c4d5a7a 100644 --- a/mss/tests/test_setup.py +++ b/mss/tests/test_setup.py @@ -2,21 +2,25 @@ This is part of the MSS Python's module. Source: https://github.com/BoboTiG/python-mss """ +import platform from subprocess import STDOUT, check_call, check_output +import pytest + from mss import __version__ -INSTALL = "python -m build --sdist --wheel".split() +if platform.system().lower() != "linux": + pytestmark = pytest.mark.skip + +# Note: using `--no-isolation` because it doesn't work with `tox` +INSTALL = "python -m build --no-isolation --sdist --wheel".split() CHECK = "twine check dist/*".split() def test_wheel_python_3_only(): """Ensure the produced wheel is Python 3 only.""" - output = check_output(INSTALL, stderr=STDOUT, text=True) - text = f"mss-{__version__}-py3-none-any.whl" + output = str(check_output(INSTALL, stderr=STDOUT)) + text = f"Successfully built mss-{__version__}.tar.gz and mss-{__version__}-py3-none-any.whl" assert text in output - print(output) - assert "warning" not in output.lower() - check_call(CHECK) From 90cd3173d181bf84a0246c88c3d8cf7d2fb22c45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Thu, 27 Oct 2022 14:56:10 +0200 Subject: [PATCH 058/280] test: move the CI from AppVeyor/Travis to GitHub --- .github/workflows/tests.yml | 81 +++++++++++++++++++++++ .travis.yml | 125 ------------------------------------ README.rst | 12 ++-- appveyor.yml | 33 ---------- 4 files changed, 87 insertions(+), 164 deletions(-) create mode 100644 .github/workflows/tests.yml delete mode 100644 .travis.yml delete mode 100644 appveyor.yml diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 00000000..85e3cbab --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,81 @@ +name: Tests + +on: + pull_request: + paths: + - ".github/workflows/tests.yml" + - "setup.cfg" + - "mss/**" + +jobs: + lint: + name: Code quality checks + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + ref: ${{ github.event.inputs.branch }} + - uses: actions/setup-python@v4 + with: + python-version: "3.11" + - name: Install test dependencies + run: python -m pip install -U pip wheel tox + - name: Tests + run: python -m tox -e lint + + types: + name: Types checks + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + ref: ${{ github.event.inputs.branch }} + - uses: actions/setup-python@v4 + with: + python-version: "3.11" + - name: Install test dependencies + run: python -m pip install -U pip wheel tox + - name: Tests + run: python -m tox -e types + + documentation: + name: Documentation build + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + ref: ${{ github.event.inputs.branch }} + - uses: actions/setup-python@v4 + with: + python-version: "3.11" + - name: Install test dependencies + run: python -m pip install -U pip wheel tox + - name: Tests + run: python -m tox -e docs + + tests: + name: "${{ matrix.os }} for ${{ matrix.python }}" + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + python: ["3.6", " 3.7", " 3.8", "3.9", "3.10", "3.11"] + steps: + - uses: actions/checkout@v3 + with: + ref: ${{ github.event.inputs.branch }} + - uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python }} + - name: Install test dependencies + run: python -m pip install -U pip wheel tox + - name: Tests + if: matrix.os == 'ubuntu-latest' + run: | + export DISPLAY=:99 + sudo Xvfb -ac :99 -screen 0 1280x1024x24 > /dev/null 2>&1 & + python -m tox -e py + - name: Tests + if: matrix.os != 'ubuntu-latest' + run: python -m tox -e py diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 0297694e..00000000 --- a/.travis.yml +++ /dev/null @@ -1,125 +0,0 @@ -# Beta opt-in -# https://docs.travis-ci.com/user/build-config-validation#beta-opt-in -version: ~> 1.0 - -language: python -dist: xenial -os: linux - -env: - global: - - MAKEFLAGS="-j 2" - -jobs: - fast_finish: true - include: - - name: Code quality checks - python: "3.8" - env: TOXENV=lint - - name: Types checking - python: "3.8" - env: TOXENV=types - - name: Documentation build - python: "3.8" - env: TOXENV=docs - - name: Python 3.5 on macOS - os: osx - language: shell - install: - - unset PYENV_ROOT - - curl -L https://github.com/pyenv/pyenv-installer/raw/master/bin/pyenv-installer | bash - - export PATH="$HOME/.pyenv/bin:$PATH" - - eval "$(pyenv init -)" - - pyenv install --skip-existing 3.5.10 - - pyenv global 3.5.10 - env: TOXENV=py35 - - name: Python 3.6 on macOS - os: osx - language: shell - install: - - unset PYENV_ROOT - - curl -L https://github.com/pyenv/pyenv-installer/raw/master/bin/pyenv-installer | bash - - export PATH="$HOME/.pyenv/bin:$PATH" - - eval "$(pyenv init -)" - - pyenv install --skip-existing 3.6.12 - - pyenv global system 3.6.12 - env: TOXENV=py36 - - name: Python 3.7 on macOS - os: osx - language: shell - install: - - unset PYENV_ROOT - - curl -L https://github.com/pyenv/pyenv-installer/raw/master/bin/pyenv-installer | bash - - export PATH="$HOME/.pyenv/bin:$PATH" - - eval "$(pyenv init -)" - - pyenv install --skip-existing 3.7.9 - - pyenv global system 3.7.9 - env: TOXENV=py37 - - name: Python 3.8 on macOS - os: osx - language: shell - install: - - unset PYENV_ROOT - - curl -L https://github.com/pyenv/pyenv-installer/raw/master/bin/pyenv-installer | bash - - export PATH="$HOME/.pyenv/bin:$PATH" - - eval "$(pyenv init -)" - - pyenv install --skip-existing 3.8.6 - - pyenv global system 3.8.6 - env: TOXENV=py38 - - name: Python 3.9 on macOS - os: osx - language: shell - install: - - unset PYENV_ROOT - - curl -L https://github.com/pyenv/pyenv-installer/raw/master/bin/pyenv-installer | bash - - export PATH="$HOME/.pyenv/bin:$PATH" - - eval "$(pyenv init -)" - - pyenv install --skip-existing 3.9-dev - - pyenv global system 3.9-dev - env: TOXENV=py39 - - name: Python 3.10 on macOS - os: osx - language: shell - install: - - unset PYENV_ROOT - - curl -L https://github.com/pyenv/pyenv-installer/raw/master/bin/pyenv-installer | bash - - export PATH="$HOME/.pyenv/bin:$PATH" - - eval "$(pyenv init -)" - - pyenv install --skip-existing 3.10-dev - - pyenv global system 3.10-dev - env: TOXENV=py310 - - name: PyPy 3.6 on GNU/Linux - python: pypy3 - env: TOXENV=pypy3 - - name: Python 3.5 on GNU/Linux - python: "3.5" - env: TOXENV=py35 - - name: Python 3.6 on GNU/Linux - python: "3.6" - env: TOXENV=py36 - - name: Python 3.7 on GNU/Linux - python: "3.7" - env: TOXENV=py37 - - name: Python 3.8 on GNU/Linux - python: "3.8" - env: TOXENV=py38 - - name: Python 3.9 on GNU/Linux - python: 3.9-dev - env: TOXENV=py39 - - name: Python 3.10 on GNU/Linux - python: nightly - env: TOXENV=py310 - -addons: - apt: - packages: - - lsof - -services: - - xvfb - -before_script: - - python3 -m pip install --upgrade pip tox - -script: - - python3 -m tox diff --git a/README.rst b/README.rst index ead20342..c51952ed 100644 --- a/README.rst +++ b/README.rst @@ -1,14 +1,14 @@ Python MSS ========== -.. image:: https://travis-ci.org/BoboTiG/python-mss.svg?branch=master - :target: https://travis-ci.org/BoboTiG/python-mss -.. image:: https://ci.appveyor.com/api/projects/status/72dik18r6b746mb0?svg=true - :target: https://ci.appveyor.com/project/BoboTiG/python-mss -.. image:: https://pepy.tech/badge/mss - :target: https://pepy.tech/project/mss +.. image:: https://badge.fury.io/py/mss.svg + :target: https://pypi.org/project/mss/ .. image:: https://anaconda.org/conda-forge/python-mss/badges/version.svg :target: https://anaconda.org/conda-forge/python-mss +.. image:: https://github.com/BoboTiG/python-mss/actions/workflows/tests.yml/badge.svg + :target: https://github.com/BoboTiG/python-mss/actions/workflows/tests.yml +.. image:: https://static.pepy.tech/personalized-badge/mss?period=total&units=international_system&left_color=black&right_color=orange&left_text=Downloads + :target: https://pepy.tech/project/mss .. code-block:: python diff --git a/appveyor.yml b/appveyor.yml deleted file mode 100644 index 199d179d..00000000 --- a/appveyor.yml +++ /dev/null @@ -1,33 +0,0 @@ -build: off - -image: - - Visual Studio 2019 - -platform: - - x64 - - x86 - -environment: - fast_finish: true - matrix: - - PYTHON_VERSION: 3.11 - - PYTHON_VERSION: 3.10 - - PYTHON_VERSION: 3.9 - - PYTHON_VERSION: 3.8 - - PYTHON_VERSION: 3.7 - - PYTHON_VERSION: 3.6 - -init: - # Update Environment Variables based on matrix/platform - - set PY_VER=%PYTHON_VERSION:.=% - - set PYTHON=C:\PYTHON%PY_VER% - - if %PLATFORM%==x64 (set PYTHON=%PYTHON%-x64) - - # Put desired Python version first in PATH - - set PATH=%PYTHON%;%PYTHON%\Scripts;%PATH% - -install: - - python -m pip install --upgrade pip tox - -test_script: - - tox -e py%PY_VER% From 6dbd4bf8c61d0f5f7dfe20a442269d6fd248f12c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Thu, 27 Oct 2022 15:45:46 +0200 Subject: [PATCH 059/280] doc: tweak --- CHANGELOG | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index f5dc7255..c70f9f25 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -9,7 +9,7 @@ History: - added support for Python 3.11 - added support for Python 3.10 - removed support for Python 3.5 - - MSS: modernized the code base (types, f-string, ran isort & black) + - MSS: modernized the code base (types, f-string, ran isort & black) (close #101) - MSS: fixed several Sourcery issues - MSS: fixed typos here, and there - doc: fixed an error when building the documentation From fd77748e16b7e7deb72ef3675294ad76220b1a3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Thu, 27 Oct 2022 15:49:08 +0200 Subject: [PATCH 060/280] doc: add Python-ImageSearch Closes #201. --- docs/source/where.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/source/where.rst b/docs/source/where.rst index ab124fb2..789ae14d 100644 --- a/docs/source/where.rst +++ b/docs/source/where.rst @@ -14,6 +14,7 @@ AI, Computer Vison - `Europilot `_, a self-driving algorithm using Euro Truck Simulator (ETS2); - `gym-mupen64plus `_, an OpenAI Gym environment wrapper for the Mupen64Plus N64 emulator; - `Open Source Self Driving Car Initiative `_; +- `Python-ImageSearch `_, a wrapper around OpenCV2 and PyAutoGUI to do image searching easily; - `PUBGIS `_, a map generator of your position throughout PUBG gameplay; - `Self-Driving-Car-3D-Simulator-With-CNN `_; - `Star Wars - The Old Republic: Galactic StarFighter `_ parser; From 443f2909e6e17dcec23ea344d3f539094316fa25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Tue, 6 Dec 2022 19:28:03 +0100 Subject: [PATCH 061/280] ci: run on master branch pushes --- .github/workflows/tests.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 85e3cbab..dcb36732 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -1,6 +1,9 @@ name: Tests on: + push: + branchs: + - master pull_request: paths: - ".github/workflows/tests.yml" From 84b023efa06c4cb225bbb61c567a7c0c5fed3dfb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Tue, 6 Dec 2022 19:29:23 +0100 Subject: [PATCH 062/280] doc: use the master branch for the tests badge --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index c51952ed..be0acdc8 100644 --- a/README.rst +++ b/README.rst @@ -5,7 +5,7 @@ Python MSS :target: https://pypi.org/project/mss/ .. image:: https://anaconda.org/conda-forge/python-mss/badges/version.svg :target: https://anaconda.org/conda-forge/python-mss -.. image:: https://github.com/BoboTiG/python-mss/actions/workflows/tests.yml/badge.svg +.. image:: https://github.com/BoboTiG/python-mss/actions/workflows/tests.yml/badge.svg?branch=master :target: https://github.com/BoboTiG/python-mss/actions/workflows/tests.yml .. image:: https://static.pepy.tech/personalized-badge/mss?period=total&units=international_system&left_color=black&right_color=orange&left_text=Downloads :target: https://pepy.tech/project/mss From 5e68192887164896cfe8b1e2440eb25d4ca2b77f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Tue, 6 Dec 2022 19:43:15 +0100 Subject: [PATCH 063/280] fix PEP 484 prohibits implicit Optional --- CHANGELOG | 1 + mss/base.py | 2 +- mss/exception.py | 4 ++-- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index c70f9f25..55b08d5f 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -4,6 +4,7 @@ History: 7.0.1 2022/10/27 - fixed the wheel package + - MSS: fixed PEP 484 prohibits implicit Optional 7.0.0 2022/10/27 - added support for Python 3.11 diff --git a/mss/base.py b/mss/base.py index bccc2799..9a72f132 100644 --- a/mss/base.py +++ b/mss/base.py @@ -106,7 +106,7 @@ def save( self, mon: int = 0, output: str = "monitor-{mon}.png", - callback: Callable[[str], None] = None, + callback: Optional[Callable[[str], None]] = None, ) -> Iterator[str]: """ Grab a screen shot and save it to a file. diff --git a/mss/exception.py b/mss/exception.py index 028a1d72..0d297b33 100644 --- a/mss/exception.py +++ b/mss/exception.py @@ -2,12 +2,12 @@ This is part of the MSS Python's module. Source: https://github.com/BoboTiG/python-mss """ -from typing import Any, Dict +from typing import Any, Dict, Optional class ScreenShotError(Exception): """Error handling class.""" - def __init__(self, message: str, details: Dict[str, Any] = None) -> None: + def __init__(self, message: str, details: Optional[Dict[str, Any]] = None) -> None: super().__init__(message) self.details = details or {} From 3905f5c65e81e07d83bd75f6b0f33df6e6fe42dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Tue, 6 Dec 2022 19:46:42 +0100 Subject: [PATCH 064/280] drop support for Python 3.6 --- .github/workflows/tests.yml | 2 +- CHANGELOG | 1 + README.rst | 2 +- docs/source/index.rst | 2 +- docs/source/support.rst | 3 ++- setup.cfg | 3 +-- 6 files changed, 7 insertions(+), 6 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index dcb36732..1ed45b19 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -63,7 +63,7 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest, windows-latest, macos-latest] - python: ["3.6", " 3.7", " 3.8", "3.9", "3.10", "3.11"] + python: [" 3.7", " 3.8", "3.9", "3.10", "3.11"] steps: - uses: actions/checkout@v3 with: diff --git a/CHANGELOG b/CHANGELOG index 55b08d5f..0587afbe 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -4,6 +4,7 @@ History: 7.0.1 2022/10/27 - fixed the wheel package + - dropped support for Python 3.6 - MSS: fixed PEP 484 prohibits implicit Optional 7.0.0 2022/10/27 diff --git a/README.rst b/README.rst index be0acdc8..f88e47ac 100644 --- a/README.rst +++ b/README.rst @@ -22,7 +22,7 @@ Python MSS An ultra fast cross-platform multiple screenshots module in pure python using ctypes. -- **Python 3.6+** and PEP8 compliant, no dependency, thread-safe; +- **Python 3.7+** and PEP8 compliant, no dependency, thread-safe; - very basic, it will grab one screen shot by monitor or a screen shot of all monitors and save it to a PNG file; - but you can use PIL and benefit from all its formats (or add yours directly); - integrate well with Numpy and OpenCV; diff --git a/docs/source/index.rst b/docs/source/index.rst index 365adab8..6d87e645 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -12,7 +12,7 @@ Welcome to Python MSS's documentation! An ultra fast cross-platform multiple screenshots module in pure python using ctypes. - - **Python 3.6+** and :pep:`8` compliant, no dependency, thread-safe; + - **Python 3.7+** and :pep:`8` compliant, no dependency, thread-safe; - very basic, it will grab one screen shot by monitor or a screen shot of all monitors and save it to a PNG file; - but you can use PIL and benefit from all its formats (or add yours directly); - integrate well with Numpy and OpenCV; diff --git a/docs/source/support.rst b/docs/source/support.rst index ed40ddad..3cef168e 100644 --- a/docs/source/support.rst +++ b/docs/source/support.rst @@ -5,7 +5,7 @@ Support Feel free to try MSS on a system we had not tested, and let us know by creating an `issue `_. - OS: GNU/Linux, macOS and Windows - - Python: 3.6 and newer + - Python: 3.7 and newer Future @@ -32,3 +32,4 @@ Abandoned - Python 3.3 (2017-12-05) - Python 3.4 (2018-03-19) - Python 3.5 (2022-10-27) +- Python 3.6 (202x-xx-xx) diff --git a/setup.cfg b/setup.cfg index 0cac3aef..f2174d7f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -23,7 +23,6 @@ classifiers = Programming Language :: Python Programming Language :: Python :: 3 Programming Language :: Python :: 3 :: Only - Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 @@ -37,7 +36,7 @@ zip_safe = False include_package_data = True packages_dir = mss packages = find: -python_requires = >=3.6 +python_requires = >=3.7 [options.entry_points] console_scripts = From 6674dc77d407be45aac5fb7a8960a1e862fd9dfc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Sun, 1 Jan 2023 10:59:21 +0100 Subject: [PATCH 065/280] doc: update dates --- LICENSE | 2 +- docs/source/conf.py | 2 +- mss/__init__.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/LICENSE b/LICENSE index ef5ae019..8d49e5d5 100644 --- a/LICENSE +++ b/LICENSE @@ -1,5 +1,5 @@ MIT License -Copyright (c) 2016-2022, Mickaël 'Tiger-222' Schoentgen +Copyright (c) 2013-2023, Mickaël 'Tiger-222' Schoentgen Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: diff --git a/docs/source/conf.py b/docs/source/conf.py index 6fabc35f..8187f52d 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -19,7 +19,7 @@ # General information about the project. project = "Python MSS" -copyright = "2013-2022, Mickaël 'Tiger-222' Schoentgen & contributors" +copyright = "2013-2023, Mickaël 'Tiger-222' Schoentgen & contributors" author = "Tiger-222" # The version info for the project you're documenting, acts as replacement for diff --git a/mss/__init__.py b/mss/__init__.py index cca0e48b..7ef4d789 100644 --- a/mss/__init__.py +++ b/mss/__init__.py @@ -14,7 +14,7 @@ __version__ = "7.0.1" __author__ = "Mickaël 'Tiger-222' Schoentgen" __copyright__ = """ -Copyright (c) 2013-2022, Mickaël 'Tiger-222' Schoentgen +Copyright (c) 2013-2023, Mickaël 'Tiger-222' Schoentgen Permission to use, copy, modify, and distribute this software and its documentation for any purpose and without fee or royalty is hereby From 90edb9fdb5736542c1fc503523411b533d911abf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Wed, 5 Apr 2023 06:24:47 +0200 Subject: [PATCH 066/280] doc: add Airtest --- docs/source/where.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/source/where.rst b/docs/source/where.rst index 789ae14d..ba815867 100644 --- a/docs/source/where.rst +++ b/docs/source/where.rst @@ -24,6 +24,7 @@ AI, Computer Vison Games ===== +- `Airtest `_, a cross-platform UI automation framework for aames and apps; - `Go Review Partner `_, a tool to help analyse and review your game of go (weiqi, baduk) using strong bots; - `Serpent.AI `_, a Game Agent Framework; From fcaf38208a49d1a6b4a3a5fd83456722cf6400e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Wed, 5 Apr 2023 06:46:46 +0200 Subject: [PATCH 067/280] doc: add OSRS Bot COLOR (OSBC) --- docs/source/where.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/source/where.rst b/docs/source/where.rst index ba815867..05f646bf 100644 --- a/docs/source/where.rst +++ b/docs/source/where.rst @@ -13,6 +13,7 @@ AI, Computer Vison - `DoomPy `_ (Autonomous Anti-Demonic Combat Algorithms); - `Europilot `_, a self-driving algorithm using Euro Truck Simulator (ETS2); - `gym-mupen64plus `_, an OpenAI Gym environment wrapper for the Mupen64Plus N64 emulator; +- `OSRS Bot COLOR (OSBC) `_, a lightweight desktop client for controlling and monitoring color-based automation scripts (bots) for OSRS and private server alternatives; - `Open Source Self Driving Car Initiative `_; - `Python-ImageSearch `_, a wrapper around OpenCV2 and PyAutoGUI to do image searching easily; - `PUBGIS `_, a map generator of your position throughout PUBG gameplay; From 4e897703ea58c7e40f3fd50af31de88d3d378c51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Wed, 5 Apr 2023 08:42:58 +0200 Subject: [PATCH 068/280] ci: tiny improvements --- .github/workflows/tests.yml | 16 ++++++++-------- CHANGELOG | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 1ed45b19..10bf68af 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -2,7 +2,7 @@ name: Tests on: push: - branchs: + branches: - master pull_request: paths: @@ -20,7 +20,7 @@ jobs: ref: ${{ github.event.inputs.branch }} - uses: actions/setup-python@v4 with: - python-version: "3.11" + python-version: "3.x" - name: Install test dependencies run: python -m pip install -U pip wheel tox - name: Tests @@ -35,7 +35,7 @@ jobs: ref: ${{ github.event.inputs.branch }} - uses: actions/setup-python@v4 with: - python-version: "3.11" + python-version: "3.x" - name: Install test dependencies run: python -m pip install -U pip wheel tox - name: Tests @@ -50,7 +50,7 @@ jobs: ref: ${{ github.event.inputs.branch }} - uses: actions/setup-python@v4 with: - python-version: "3.11" + python-version: "3.x" - name: Install test dependencies run: python -m pip install -U pip wheel tox - name: Tests @@ -63,7 +63,7 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest, windows-latest, macos-latest] - python: [" 3.7", " 3.8", "3.9", "3.10", "3.11"] + python: ["3.7", "3.8", "3.9", "3.10", "3.11"] steps: - uses: actions/checkout@v3 with: @@ -73,12 +73,12 @@ jobs: python-version: ${{ matrix.python }} - name: Install test dependencies run: python -m pip install -U pip wheel tox - - name: Tests + - name: Tests on GNU/Linux if: matrix.os == 'ubuntu-latest' run: | export DISPLAY=:99 - sudo Xvfb -ac :99 -screen 0 1280x1024x24 > /dev/null 2>&1 & + sudo Xvfb -ac ${DISPLAY} -screen 0 1280x1024x24 > /dev/null 2>&1 & python -m tox -e py - - name: Tests + - name: Tests on other platforms if: matrix.os != 'ubuntu-latest' run: python -m tox -e py diff --git a/CHANGELOG b/CHANGELOG index 0587afbe..6b14f844 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -4,7 +4,7 @@ History: 7.0.1 2022/10/27 - fixed the wheel package - - dropped support for Python 3.6 + - removed support for Python 3.6 - MSS: fixed PEP 484 prohibits implicit Optional 7.0.0 2022/10/27 From 2cc01a9bb57d2a7e614780464a3e4a9c508dbae4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Wed, 5 Apr 2023 08:43:20 +0200 Subject: [PATCH 069/280] Linux: fix pylint issue Unnecessary "else" after "return", remove the "else" and de-indent the code inside it --- mss/linux.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/mss/linux.py b/mss/linux.py index e676763f..32e49457 100644 --- a/mss/linux.py +++ b/mss/linux.py @@ -320,8 +320,7 @@ def has_extension(self, extension: str) -> bool: ) except ScreenShotError: return False - else: - return True + return True def _get_display(self, disp: Optional[bytes] = None) -> int: """ From 46db94fe19cbc4a91c7f3329f60ba17e9ae0f821 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Wed, 5 Apr 2023 16:06:36 +0200 Subject: [PATCH 070/280] tests: remove tox (#226) - dev: removed pre-commit - tests: added PyPy 3.9 --- .github/dependabot.yml | 20 +++++++++ .github/workflows/tests.yml | 90 ++++++++++++++++++++----------------- .gitignore | 4 +- .pre-commit-config.yaml | 20 --------- CHANGELOG | 5 +++ check.sh | 10 +++++ dev-requirements.txt | 15 +++++++ docs/source/developers.rst | 27 +++-------- mss/__main__.py | 2 +- mss/tests/test_setup.py | 3 +- setup.cfg | 2 + tox.ini | 50 --------------------- 12 files changed, 113 insertions(+), 135 deletions(-) create mode 100644 .github/dependabot.yml delete mode 100644 .pre-commit-config.yaml create mode 100755 check.sh create mode 100644 dev-requirements.txt delete mode 100644 tox.ini diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..4075752b --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,20 @@ +version: 2 +updates: + # GitHub Actions + - package-ecosystem: github-actions + directory: / + schedule: + interval: daily + labels: + - dependencies + - QA/CI + + # Python requirements + - package-ecosystem: pip + directory: / + schedule: + interval: daily + assignees: + - BoboTiG + labels: + - dependencies diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 10bf68af..63180980 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -11,74 +11,84 @@ on: - "mss/**" jobs: - lint: - name: Code quality checks + quality: + name: Quality runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - with: - ref: ${{ github.event.inputs.branch }} - - uses: actions/setup-python@v4 - with: - python-version: "3.x" - - name: Install test dependencies - run: python -m pip install -U pip wheel tox - - name: Tests - run: python -m tox -e lint - - types: - name: Types checks - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - with: - ref: ${{ github.event.inputs.branch }} - uses: actions/setup-python@v4 with: python-version: "3.x" - - name: Install test dependencies - run: python -m pip install -U pip wheel tox + cache: pip + cache-dependency-path: dev-requirements.txt + - name: Install dependencies + run: | + python -m pip install -U pip wheel + python -m pip install -r dev-requirements.txt - name: Tests - run: python -m tox -e types + run: ./check.sh documentation: - name: Documentation build + name: Documentation runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - with: - ref: ${{ github.event.inputs.branch }} - uses: actions/setup-python@v4 with: python-version: "3.x" + cache: pip + cache-dependency-path: dev-requirements.txt - name: Install test dependencies - run: python -m pip install -U pip wheel tox + run: | + python -m pip install -U pip wheel + python -m pip install -r dev-requirements.txt - name: Tests - run: python -m tox -e docs + run: | + sphinx-build -d docs docs/source docs_out --color -W -bhtml tests: - name: "${{ matrix.os }} for ${{ matrix.python }}" - runs-on: ${{ matrix.os }} + name: "${{ matrix.os.emoji }} ${{ matrix.python.name }}" + runs-on: ${{ matrix.os.runs-on }} strategy: fail-fast: false matrix: - os: [ubuntu-latest, windows-latest, macos-latest] - python: ["3.7", "3.8", "3.9", "3.10", "3.11"] + os: + - emoji: 🐧 + runs-on: [ubuntu-latest] + - emoji: 🍎 + runs-on: [macos-latest] + - emoji: 🪟 + runs-on: [windows-latest] + python: + - name: CPython 3.7 + runs-on: "3.7" + - name: CPython 3.8 + runs-on: "3.8" + - name: CPython 3.9 + runs-on: "3.9" + - name: CPython 3.10 + runs-on: "3.10" + - name: CPython 3.11 + runs-on: "3.11" + - name: PyPy 3.9 + runs-on: "pypy-3.9" steps: - uses: actions/checkout@v3 - with: - ref: ${{ github.event.inputs.branch }} - uses: actions/setup-python@v4 with: - python-version: ${{ matrix.python }} + python-version: ${{ matrix.python.runs-on }} + cache: pip + cache-dependency-path: dev-requirements.txt - name: Install test dependencies - run: python -m pip install -U pip wheel tox + run: | + python -m pip install -U pip wheel + python -m pip install -r dev-requirements.txt - name: Tests on GNU/Linux - if: matrix.os == 'ubuntu-latest' + if: matrix.os.emoji == '🐧' run: | export DISPLAY=:99 - sudo Xvfb -ac ${DISPLAY} -screen 0 1280x1024x24 > /dev/null 2>&1 & - python -m tox -e py + sudo Xvfb -ac :99 -screen 0 1280x1024x24 > /dev/null 2>&1 & + python -m pytest - name: Tests on other platforms - if: matrix.os != 'ubuntu-latest' - run: python -m tox -e py + if: matrix.os.emoji != '🐧' + run: python -m pytest diff --git a/.gitignore b/.gitignore index 1345012e..f4d791d8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,8 @@ build/ .cache/ dist/ +*.doctree +docs_out/ *.egg-info/ .idea/ .DS_Store @@ -8,9 +10,9 @@ dist/ *.jpg *.png *.png.old +*.pickle *.pyc .pytest_cache -.tox .vscode docs/output/ .mypy_cache/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml deleted file mode 100644 index c7f49431..00000000 --- a/.pre-commit-config.yaml +++ /dev/null @@ -1,20 +0,0 @@ -fail_fast: true - -repos: -- repo: https://github.com/ambv/black - rev: stable - hooks: - - id: black -- repo: https://gitlab.com/pycqa/flake8 - rev: master - hooks: - - id: flake8 -- repo: https://github.com/pre-commit/pre-commit-hooks - rev: master - hooks: - - id: trailing-whitespace - - id: end-of-file-fixer - - id: check-docstring-first - - id: debug-statements - - id: check-ast - - id: no-commit-to-branch diff --git a/CHANGELOG b/CHANGELOG index 6b14f844..481c2e97 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -2,6 +2,11 @@ History: +7.0.2 2023/0x/xx + - dev: removed pre-commit + - tests: removed tox + - tests: added PyPy 3.9 + 7.0.1 2022/10/27 - fixed the wheel package - removed support for Python 3.6 diff --git a/check.sh b/check.sh new file mode 100755 index 00000000..ba8a974c --- /dev/null +++ b/check.sh @@ -0,0 +1,10 @@ +#!/bin/bash +# +# Small script to ensure quality checks pass before submitting a commit/PR. +# +python -m isort docs mss +python -m black docs mss +python -m flake8 docs mss +python -m pylint mss +# "--platform win32" to not fail on ctypes.windll (it does not affect the overall check on other OSes) +python -m mypy --platform win32 --exclude mss/tests mss docs/source/examples diff --git a/dev-requirements.txt b/dev-requirements.txt new file mode 100644 index 00000000..af5c133b --- /dev/null +++ b/dev-requirements.txt @@ -0,0 +1,15 @@ +-e . +black +build +flake8 +flaky +pytest +pytest-cov +mypy +numpy; platform_python_implementation != "pypy" +numpy==1.15.4; platform_python_implementation == "pypy" +pillow +pylint +sphinx +twine +wheel diff --git a/docs/source/developers.rst b/docs/source/developers.rst index db544bba..909d676e 100644 --- a/docs/source/developers.rst +++ b/docs/source/developers.rst @@ -11,11 +11,6 @@ Setup 2. Create you own branch. 3. Be sure to add/update tests and documentation within your patch. -Additionally, you can install `pre-commit `_ to ensure you are doing things well:: - - $ python -m pip install -U --user pre-commit - $ pre-commit install - Testing ======= @@ -23,9 +18,10 @@ Testing Dependency ---------- -You will need `tox `_:: +You will need `pytest `_:: - $ python -m pip install -U --user tox + $ python -m pip install -U pip wheel + $ python -m pip install -r dev-requirements.txt How to Test? @@ -33,10 +29,7 @@ How to Test? Launch the test suit:: - $ tox - - # or - $ TOXENV=py37 tox + $ python -m pytest This will test MSS and ensure a good code quality. @@ -46,15 +39,7 @@ Code Quality To ensure the code is always well enough using `flake8 `_:: - $ TOXENV=lint tox - - -Static Type Checking -==================== - -To check type annotation using `mypy `_:: - - $ TOXENV=types tox + $ ./check.sh Documentation @@ -62,4 +47,4 @@ Documentation To build the documentation, simply type:: - $ TOXENV=docs tox + $ sphinx-build -d docs docs/source docs_out --color -W -bhtml diff --git a/mss/__main__.py b/mss/__main__.py index 636730fb..8e071a3a 100644 --- a/mss/__main__.py +++ b/mss/__main__.py @@ -81,7 +81,7 @@ def main(args: Optional[List[str]] = None) -> int: return 1 -if __name__ == "__main__": +if __name__ == "__main__": # pragma: nocover import sys sys.exit(main(sys.argv[1:])) diff --git a/mss/tests/test_setup.py b/mss/tests/test_setup.py index 8c4d5a7a..9161fd84 100644 --- a/mss/tests/test_setup.py +++ b/mss/tests/test_setup.py @@ -12,8 +12,7 @@ if platform.system().lower() != "linux": pytestmark = pytest.mark.skip -# Note: using `--no-isolation` because it doesn't work with `tox` -INSTALL = "python -m build --no-isolation --sdist --wheel".split() +INSTALL = "python -m build --sdist --wheel".split() CHECK = "twine check dist/*".split() diff --git a/setup.cfg b/setup.cfg index f2174d7f..09c883db 100644 --- a/setup.cfg +++ b/setup.cfg @@ -64,6 +64,8 @@ addopts = --failed-first -r fE -v + --cov=mss + --cov-report=term-missing # Trait all tests as flaky by default --force-flaky --no-success-flaky-report diff --git a/tox.ini b/tox.ini deleted file mode 100644 index 04e8182b..00000000 --- a/tox.ini +++ /dev/null @@ -1,50 +0,0 @@ -[tox] -envlist = - lint - types - docs - py{311,310,39,38,37,36,py3} - -[testenv] -passenv = DISPLAY -alwayscopy = True -deps = - build - flaky - pytest - # Must pin that version to support PyPy3 - pypy3: numpy==1.15.4 - py3{11,10,9,8,7,6}: numpy - pillow - twine - wheel -commands = - python -m pytest {posargs} - -[testenv:lint] -description = Code quality check -deps = - black - flake8 - pylint - isort -commands = - python -m flake8 docs mss - python -m pylint mss - python -m isort --check-only docs mss - python -m black --check docs mss - -[testenv:types] -description = Type annotations check -deps = - mypy -commands = - # "--platform win32" to not fail on ctypes.windll (it does not affect the overall check on other OSes) - python -m mypy --platform win32 --exclude mss/tests mss docs/source/examples - -[testenv:docs] -description = Build the documentation -deps = sphinx -commands = - sphinx-build -d "{toxworkdir}/docs" docs/source "{toxworkdir}/docs_out" --color -W -bhtml {posargs} - python -c "print('documentation available under file://{toxworkdir}/docs_out/index.html')" From a116dd19245c4a8e00c5f5e1a0bdfc59008ea423 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Wed, 5 Apr 2023 16:10:44 +0200 Subject: [PATCH 071/280] ci: always run tests --- .github/workflows/tests.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 63180980..d51d229e 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -5,10 +5,6 @@ on: branches: - master pull_request: - paths: - - ".github/workflows/tests.yml" - - "setup.cfg" - - "mss/**" jobs: quality: From 2115e081dac070b2a13adf1d9f888cf7c2c4caa2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Wed, 5 Apr 2023 16:59:13 +0200 Subject: [PATCH 072/280] Linux: removed get_error_details(), use the ScreenShotError details attribute instead (#224) It should fix threading issues, and prevent old errors messing with new X11 calls. --- .github/workflows/tests.yml | 2 +- CHANGELOG | 3 +- docs/source/api.rst | 35 ------------------- docs/source/conf.py | 2 +- mss/__init__.py | 2 +- mss/linux.py | 69 ++++++++++++++++--------------------- mss/tests/test_gnu_linux.py | 20 +++++++---- setup.cfg | 4 ++- 8 files changed, 51 insertions(+), 86 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index d51d229e..ee202880 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -83,7 +83,7 @@ jobs: if: matrix.os.emoji == '🐧' run: | export DISPLAY=:99 - sudo Xvfb -ac :99 -screen 0 1280x1024x24 > /dev/null 2>&1 & + sudo Xvfb -ac ${DISPLAY} -screen 0 1280x1024x24 > /dev/null 2>&1 & python -m pytest - name: Tests on other platforms if: matrix.os.emoji != '🐧' diff --git a/CHANGELOG b/CHANGELOG index 481c2e97..6a449f2a 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -2,7 +2,8 @@ History: -7.0.2 2023/0x/xx +8.0.0 2023/0x/xx + - Linux: removed get_error_details(), use the ScreenShotError details attribute instead - dev: removed pre-commit - tests: removed tox - tests: added PyPy 3.9 diff --git a/docs/source/api.rst b/docs/source/api.rst index 49fdf113..03438c15 100644 --- a/docs/source/api.rst +++ b/docs/source/api.rst @@ -19,14 +19,6 @@ GNU/Linux .. attribute:: CFUNCTIONS -.. attribute:: ERROR - - :type: types.SimpleNamspacedict - - The `details` attribute contains the latest Xlib or XRANDR function. It is a dict. - - .. versionadded:: 5.0.0 - .. attribute:: PLAINMASK .. attribute:: ZPIXMAP @@ -40,33 +32,6 @@ GNU/Linux GNU/Linux initializations. - .. method:: get_error_details() - - :rtype: Optional[dict[str, Any]] - - Get more information about the latest X server error. To use in such scenario:: - - with mss.mss() as sct: - # Take a screenshot of a region out of monitor bounds - rect = {"left": -30, "top": 0, "width": 100, "height": 100} - - try: - sct.grab(rect) - except ScreenShotError: - details = sct.get_error_details() - """ - >>> import pprint - >>> pprint.pprint(details) - {'xerror': 'BadFont (invalid Font parameter)', - 'xerror_details': {'error_code': 7, - 'minor_code': 0, - 'request_code': 0, - 'serial': 422, - 'type': 0}} - """ - - .. versionadded:: 4.0.0 - .. method:: grab(monitor) :rtype: :class:`~mss.base.ScreenShot` diff --git a/docs/source/conf.py b/docs/source/conf.py index 8187f52d..cd19fa95 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -27,7 +27,7 @@ # built documents. # # The short X.Y version. -version = "7.0.1" +version = "8.0.0" # The full version, including alpha/beta/rc tags. release = "latest" diff --git a/mss/__init__.py b/mss/__init__.py index 7ef4d789..61a4d3cc 100644 --- a/mss/__init__.py +++ b/mss/__init__.py @@ -11,7 +11,7 @@ from .exception import ScreenShotError from .factory import mss -__version__ = "7.0.1" +__version__ = "8.0.0" __author__ = "Mickaël 'Tiger-222' Schoentgen" __copyright__ = """ Copyright (c) 2013-2023, Mickaël 'Tiger-222' Schoentgen diff --git a/mss/linux.py b/mss/linux.py index 32e49457..dc8eeeb9 100644 --- a/mss/linux.py +++ b/mss/linux.py @@ -22,7 +22,6 @@ c_ushort, c_void_p, ) -from types import SimpleNamespace from typing import Any, Dict, Optional, Tuple, Union from .base import MSSBase, lock @@ -33,7 +32,6 @@ __all__ = ("MSS",) -ERROR = SimpleNamespace(details=None) PLAINMASK = 0x00FFFFFF ZPIXMAP = 2 @@ -158,29 +156,46 @@ class XRRCrtcInfo(Structure): ] +_ERROR = {} + + @CFUNCTYPE(c_int, POINTER(Display), POINTER(Event)) -def error_handler(_: Any, event: Any) -> int: +def error_handler(display: Display, event: Event) -> int: """Specifies the program's supplied error handler.""" + x11 = ctypes.util.find_library("X11") + if not x11: + return 0 + + # Get the specific error message + xlib = ctypes.cdll.LoadLibrary(x11) + get_error = getattr(xlib, "XGetErrorText") + get_error.argtypes = [POINTER(Display), c_int, c_char_p, c_int] + get_error.restype = c_void_p + evt = event.contents - ERROR.details = { - "type": evt.type, - "serial": evt.serial, + error = ctypes.create_string_buffer(1024) + get_error(display, evt.error_code, error, len(error)) + + _ERROR[threading.current_thread()] = { + "error": error.value.decode("utf-8"), "error_code": evt.error_code, - "request_code": evt.request_code, "minor_code": evt.minor_code, + "request_code": evt.request_code, + "serial": evt.serial, + "type": evt.type, } + return 0 -def validate( - retval: int, func: Any, args: Tuple[Any, Any] -) -> Optional[Tuple[Any, Any]]: +def validate(retval: int, func: Any, args: Tuple[Any, Any]) -> Tuple[Any, Any]: """Validate the returned value of a Xlib or XRANDR function.""" - if retval != 0 and not ERROR.details: + current_thread = threading.current_thread() + if retval != 0 and current_thread not in _ERROR: return args - details = {"retval": retval, "args": args} + details = _ERROR.pop(current_thread, {}) raise ScreenShotError(f"{func.__name__}() failed", details=details) @@ -196,7 +211,6 @@ def validate( CFUNCTIONS: CFunctions = { "XDefaultRootWindow": ("xlib", [POINTER(Display)], POINTER(XWindowAttributes)), "XDestroyImage": ("xlib", [POINTER(XImage)], c_void_p), - "XGetErrorText": ("xlib", [POINTER(Display), c_int, c_char_p, c_int], c_void_p), "XGetImage": ( "xlib", [ @@ -331,15 +345,13 @@ def _get_display(self, disp: Optional[bytes] = None) -> int: Since the current thread and main thread are always alive, reuse their *display* value first. """ - cur_thread, main_thread = threading.current_thread(), threading.main_thread() - current_display = MSS._display_dict.get(cur_thread) or MSS._display_dict.get( - main_thread - ) + current_thread = threading.current_thread() + current_display = MSS._display_dict.get(current_thread) if current_display: display = current_display else: display = self.xlib.XOpenDisplay(disp) - MSS._display_dict[cur_thread] = display + MSS._display_dict[current_thread] = display return display def _set_cfunctions(self) -> None: @@ -360,27 +372,6 @@ def _set_cfunctions(self) -> None: restype=restype, ) - def get_error_details(self) -> Optional[Dict[str, Any]]: - """Get more information about the latest X server error.""" - - details: Dict[str, Any] = {} - - if ERROR.details: - details = {"xerror_details": ERROR.details} - ERROR.details = None - xserver_error = ctypes.create_string_buffer(1024) - self.xlib.XGetErrorText( - self._get_display(), - details.get("xerror_details", {}).get("error_code", 0), - xserver_error, - len(xserver_error), - ) - xerror = xserver_error.value.decode("utf-8") - if xerror != "0": - details["xerror"] = xerror - - return details - def _monitors_impl(self) -> None: """Get positions of monitors. It will populate self._monitors.""" diff --git a/mss/tests/test_gnu_linux.py b/mss/tests/test_gnu_linux.py index 4596ffbf..b53ad136 100644 --- a/mss/tests/test_gnu_linux.py +++ b/mss/tests/test_gnu_linux.py @@ -9,6 +9,7 @@ import pytest import mss +import mss.linux from mss.base import MSSBase from mss.exception import ScreenShotError @@ -107,18 +108,23 @@ def find_lib_mocked(lib): def test_region_out_of_monitor_bounds(): display = os.getenv("DISPLAY") + monitor = {"left": -30, "top": 0, "width": 100, "height": 100} + + assert not mss.linux._ERROR + with mss.mss(display=display) as sct: with pytest.raises(ScreenShotError) as exc: - monitor = {"left": -30, "top": 0, "width": 100, "height": 100} - assert sct.grab(monitor) + sct.grab(monitor) assert str(exc.value) - assert "retval" in exc.value.details - assert "args" in exc.value.details - details = sct.get_error_details() - assert details["xerror"] - assert isinstance(details["xerror_details"], dict) + details = exc.value.details + assert details + assert isinstance(details, dict) + assert isinstance(details["error"], str) + assert not mss.linux._ERROR + + assert not mss.linux._ERROR def test_has_extension(): diff --git a/setup.cfg b/setup.cfg index 09c883db..cace210f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = mss -version = 7.0.1 +version = 8.0.0 author = Mickaël 'Tiger-222' Schoentgen author_email = contact@tiger-222.fr description = An ultra fast cross-platform multiple screenshots module in pure python using ctypes. @@ -20,6 +20,8 @@ platforms = Darwin, Linux, Windows classifiers = Development Status :: 5 - Production/Stable License :: OSI Approved :: MIT License + Programming Language :: Python :: Implementation :: CPython + Programming Language :: Python :: Implementation :: PyPy Programming Language :: Python Programming Language :: Python :: 3 Programming Language :: Python :: 3 :: Only From cc7833e5c0fe82014e85634a4bc5a6561bff2c3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Wed, 5 Apr 2023 17:01:32 +0200 Subject: [PATCH 073/280] dev: ignore .coverage file --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index f4d791d8..361d6114 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ build/ .cache/ +.coverage dist/ *.doctree docs_out/ From 2d14fc2d002a7a1e3b040b474a95a3fbf48a42e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Wed, 5 Apr 2023 17:02:32 +0200 Subject: [PATCH 074/280] doc: add Gradient Sampler --- docs/source/where.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/source/where.rst b/docs/source/where.rst index 05f646bf..50123078 100644 --- a/docs/source/where.rst +++ b/docs/source/where.rst @@ -12,9 +12,10 @@ AI, Computer Vison - `DeepEye `_, a deep vision-based software library for autonomous and advanced driver-assistance systems; - `DoomPy `_ (Autonomous Anti-Demonic Combat Algorithms); - `Europilot `_, a self-driving algorithm using Euro Truck Simulator (ETS2); +- `Gradient Sampler `_, sample blender gradients from anything on the screen; - `gym-mupen64plus `_, an OpenAI Gym environment wrapper for the Mupen64Plus N64 emulator; -- `OSRS Bot COLOR (OSBC) `_, a lightweight desktop client for controlling and monitoring color-based automation scripts (bots) for OSRS and private server alternatives; - `Open Source Self Driving Car Initiative `_; +- `OSRS Bot COLOR (OSBC) `_, a lightweight desktop client for controlling and monitoring color-based automation scripts (bots) for OSRS and private server alternatives; - `Python-ImageSearch `_, a wrapper around OpenCV2 and PyAutoGUI to do image searching easily; - `PUBGIS `_, a map generator of your position throughout PUBG gameplay; - `Self-Driving-Car-3D-Simulator-With-CNN `_; From fb98404b37b6d664e2c0e4eabb0dcaab6c6ce65d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Wed, 5 Apr 2023 17:07:16 +0200 Subject: [PATCH 075/280] doc: add wow-fishing-bot And simplify the list. --- docs/source/where.rst | 45 +++++++++++++------------------------------ 1 file changed, 13 insertions(+), 32 deletions(-) diff --git a/docs/source/where.rst b/docs/source/where.rst index 50123078..ad8a05e1 100644 --- a/docs/source/where.rst +++ b/docs/source/where.rst @@ -5,48 +5,29 @@ Who Uses it? This is a non exhaustive list where MSS is integrated or has inspired. Do not hesistate to `say Hello! `_ if you are using MSS too. - -AI, Computer Vison -================== - +- `Airtest `_, a cross-platform UI automation framework for aames and apps; +- `Automation Framework `_, a Batmans utility; - `DeepEye `_, a deep vision-based software library for autonomous and advanced driver-assistance systems; - `DoomPy `_ (Autonomous Anti-Demonic Combat Algorithms); - `Europilot `_, a self-driving algorithm using Euro Truck Simulator (ETS2); +- `Flexx Python UI toolkit `_; +- `Go Review Partner `_, a tool to help analyse and review your game of go (weiqi, baduk) using strong bots; - `Gradient Sampler `_, sample blender gradients from anything on the screen; - `gym-mupen64plus `_, an OpenAI Gym environment wrapper for the Mupen64Plus N64 emulator; +- `NativeShot `_ (Mozilla Firefox module); +- `NCTU Scratch and Python, 2017 Spring `_ (Python course); +- `normcap `_, OCR powered screen-capture tool to capture information instead of images; - `Open Source Self Driving Car Initiative `_; - `OSRS Bot COLOR (OSBC) `_, a lightweight desktop client for controlling and monitoring color-based automation scripts (bots) for OSRS and private server alternatives; +- `Philips Hue Lights Ambiance `_; +- `Pombo `_, a thief recovery software; - `Python-ImageSearch `_, a wrapper around OpenCV2 and PyAutoGUI to do image searching easily; - `PUBGIS `_, a map generator of your position throughout PUBG gameplay; +- `ScreenCapLibrary `_, a Robot Framework test library for capturing screenshots and video recording; - `Self-Driving-Car-3D-Simulator-With-CNN `_; +- `Serpent.AI `_, a Game Agent Framework; - `Star Wars - The Old Republic: Galactic StarFighter `_ parser; +- `Stitch `_, a Python Remote Administration Tool (RAT); - `TensorKart `_, a self-driving MarioKart with TensorFlow; +- `wow-fishing-bot `_, a fishing bot for World of Warcraft that uses template matching from OpenCV; - `Zelda Bowling AI `_; - -Games -===== - -- `Airtest `_, a cross-platform UI automation framework for aames and apps; -- `Go Review Partner `_, a tool to help analyse and review your game of go (weiqi, baduk) using strong bots; -- `Serpent.AI `_, a Game Agent Framework; - -Learning -======== - -- `NCTU Scratch and Python, 2017 Spring `_ (Python course); - -Security -======== - -- `Automation Framework `_, a Batmans utility; -- `Pombo `_, a thief recovery software; -- `Stitch `_, a Python Remote Administration Tool (RAT); - -Utilities -========= - -- `Flexx Python UI toolkit `_; -- `NativeShot `_ (Mozilla Firefox module); -- `normcap `_, OCR powered screen-capture tool to capture information instead of images; -- `Philips Hue Lights Ambiance `_; -- `ScreenCapLibrary `_, a Robot Framework test library for capturing screenshots and video recording; From 85c191e70adc44571cd73eaeee36d33efc9814dc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 5 Apr 2023 17:20:32 +0200 Subject: [PATCH 076/280] build(deps-dev): bump numpy from 1.15.4 to 1.24.2 (#227) Bumps [numpy](https://github.com/numpy/numpy) from 1.15.4 to 1.24.2. - [Release notes](https://github.com/numpy/numpy/releases) - [Changelog](https://github.com/numpy/numpy/blob/main/doc/RELEASE_WALKTHROUGH.rst) - [Commits](https://github.com/numpy/numpy/compare/v1.15.4...v1.24.2) --- updated-dependencies: - dependency-name: numpy dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- dev-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev-requirements.txt b/dev-requirements.txt index af5c133b..c5d21e00 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -7,7 +7,7 @@ pytest pytest-cov mypy numpy; platform_python_implementation != "pypy" -numpy==1.15.4; platform_python_implementation == "pypy" +numpy==1.24.2; platform_python_implementation == "pypy" pillow pylint sphinx From f928310b031ab6d4952d692017a94060240bbf4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Wed, 5 Apr 2023 19:00:48 +0200 Subject: [PATCH 077/280] Linux: added mouse support (partially fixes #55) (#232) Original patch by @zorvios on #188. --- CHANGELOG | 1 + CONTRIBUTORS | 3 ++ docs/source/api.rst | 12 ++++++- mss/base.py | 64 +++++++++++++++++++++++++++++++++-- mss/darwin.py | 6 +++- mss/linux.py | 67 +++++++++++++++++++++++++++++++++++-- mss/tests/test_gnu_linux.py | 11 ++++++ mss/windows.py | 6 +++- 8 files changed, 163 insertions(+), 7 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 6a449f2a..a5241ade 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -3,6 +3,7 @@ History: 8.0.0 2023/0x/xx + - Linux: added mouse support (partially fixes #55) - Linux: removed get_error_details(), use the ScreenShotError details attribute instead - dev: removed pre-commit - tests: removed tox diff --git a/CONTRIBUTORS b/CONTRIBUTORS index 00971c5c..4cc26fb4 100644 --- a/CONTRIBUTORS +++ b/CONTRIBUTORS @@ -12,6 +12,9 @@ Alexander 'thehesiod' Mohr [https://github.com/thehesiod] Andreas Buhr [https://www.andreasbuhr.de] - Bugfix for multi-monitor detection +Boutallaka 'zorvios' Yassir [https://github.com/zorvios] + - GNU/Linux: Mouse support + bubulle [http://indexerror.net/user/bubulle] - Windows: efficiency of MSS.get_pixels() diff --git a/docs/source/api.rst b/docs/source/api.rst index 03438c15..451f5628 100644 --- a/docs/source/api.rst +++ b/docs/source/api.rst @@ -25,13 +25,17 @@ GNU/Linux .. class:: MSS - .. method:: __init__([display=None]) + .. method:: __init__([display=None, with_cursor=False]) :type display: str or None :param display: The display to use. + :param with_cursor: Include the mouse cursor in screenshots. GNU/Linux initializations. + .. versionadded:: 8.0.0 + `with_cursor` keyword argument. + .. method:: grab(monitor) :rtype: :class:`~mss.base.ScreenShot` @@ -76,6 +80,12 @@ Methods The parent's class for every OS implementation. + .. attribute:: compression_level + + PNG compression level used when saving the screenshot data into a file (see :py:func:`zlib.compress()` for details). + + .. versionadded:: 3.2.0 + .. method:: close() Clean-up method. Does nothing by default. diff --git a/mss/base.py b/mss/base.py index 9a72f132..5246f54b 100644 --- a/mss/base.py +++ b/mss/base.py @@ -18,11 +18,12 @@ class MSSBase(metaclass=ABCMeta): """This class will be overloaded by a system specific one.""" - __slots__ = {"_monitors", "cls_image", "compression_level"} + __slots__ = {"_monitors", "cls_image", "compression_level", "with_cursor"} def __init__(self) -> None: self.cls_image: Type[ScreenShot] = ScreenShot self.compression_level = 6 + self.with_cursor = False self._monitors: Monitors = [] def __enter__(self) -> "MSSBase": @@ -35,6 +36,10 @@ def __exit__(self, *_: Any) -> None: self.close() + @abstractmethod + def _cursor_impl(self) -> Optional[ScreenShot]: + """Retrieve all cursor data. Pixels have to be RGB.""" + @abstractmethod def _grab_impl(self, monitor: Monitor) -> ScreenShot: """ @@ -73,7 +78,11 @@ def grab(self, monitor: Union[Monitor, Tuple[int, int, int, int]]) -> ScreenShot } with lock: - return self._grab_impl(monitor) + screenshot = self._grab_impl(monitor) + if self.with_cursor: + cursor = self._cursor_impl() + screenshot = self._merge(screenshot, cursor) # type: ignore[arg-type] + return screenshot @property def monitors(self) -> Monitors: @@ -174,6 +183,57 @@ def shot(self, **kwargs: Any) -> str: kwargs["mon"] = kwargs.get("mon", 1) return next(self.save(**kwargs)) + @staticmethod + 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 + + cx2, cy2 = cx + cw, cy + ch + x2, y2 = x + w, y + h + + overlap = cx < x2 and cx2 > x and cy < y2 and cy2 > y + if not overlap: + return screenshot + + screen_data = screenshot.raw + cursor_data = cursor.raw + + cy, cy2 = (cy - y) * 4, (cy2 - y2) * 4 + cx, cx2 = (cx - x) * 4, (cx2 - x2) * 4 + start_count_y = -cy if cy < 0 else 0 + start_count_x = -cx if cx < 0 else 0 + stop_count_y = ch * 4 - max(cy2, 0) + stop_count_x = cw * 4 - max(cx2, 0) + rgb = range(3) + + for count_y in range(start_count_y, stop_count_y, 4): + pos_s = (count_y + cy) * w + cx + pos_c = count_y * cw + + 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] + + if not alpha: + continue + + if alpha == 255: + screen_data[spos : spos + 3] = cursor_data[cpos : cpos + 3] + else: + alpha = alpha / 255 + for i in rgb: + screen_data[spos + i] = int( + cursor_data[cpos + i] * alpha + + screen_data[spos + i] * (1 - alpha) + ) + + return screenshot + @staticmethod def _cfactory( attr: Any, diff --git a/mss/darwin.py b/mss/darwin.py index 2e1ca472..83543951 100644 --- a/mss/darwin.py +++ b/mss/darwin.py @@ -17,7 +17,7 @@ c_void_p, ) from platform import mac_ver -from typing import Any, Type, Union +from typing import Any, Optional, Type, Union from .base import MSSBase from .exception import ScreenShotError @@ -232,3 +232,7 @@ def _grab_impl(self, monitor: Monitor) -> ScreenShot: core.CFRelease(copy_data) return self.cls_image(data, monitor, size=Size(width, height)) + + def _cursor_impl(self) -> Optional[ScreenShot]: + """Retrieve all cursor data. Pixels have to be RGB.""" + return None diff --git a/mss/linux.py b/mss/linux.py index dc8eeeb9..916efa8d 100644 --- a/mss/linux.py +++ b/mss/linux.py @@ -15,12 +15,14 @@ c_int, c_int32, c_long, + c_short, c_ubyte, c_uint, c_uint32, c_ulong, c_ushort, c_void_p, + cast, ) from typing import Any, Dict, Optional, Tuple, Union @@ -60,6 +62,26 @@ class Event(Structure): ] +class XFixesCursorImage(Structure): + """ + XFixes is an X Window System extension. + See /usr/include/X11/extensions/Xfixes.h + """ + + _fields_ = [ + ("x", c_short), + ("y", c_short), + ("width", c_ushort), + ("height", c_ushort), + ("xhot", c_ushort), + ("yhot", c_ushort), + ("cursor_serial", c_ulong), + ("pixels", POINTER(c_ulong)), + ("atom", c_ulong), + ("name", c_char_p), + ] + + class XWindowAttributes(Structure): """Attributes for the specified window.""" @@ -211,6 +233,7 @@ def validate(retval: int, func: Any, args: Tuple[Any, Any]) -> Tuple[Any, Any]: CFUNCTIONS: CFunctions = { "XDefaultRootWindow": ("xlib", [POINTER(Display)], POINTER(XWindowAttributes)), "XDestroyImage": ("xlib", [POINTER(XImage)], c_void_p), + "XFixesGetCursorImage": ("xfixes", [POINTER(Display)], POINTER(XFixesCursorImage)), "XGetImage": ( "xlib", [ @@ -269,15 +292,18 @@ class MSS(MSSBase): It uses intensively the Xlib and its Xrandr extension. """ - __slots__ = {"drawable", "root", "xlib", "xrandr"} + __slots__ = {"drawable", "root", "xlib", "xrandr", "xfixes", "__with_cursor"} # A dict to maintain *display* values created by multiple threads. _display_dict: Dict[threading.Thread, int] = {} - def __init__(self, display: Optional[Union[bytes, str]] = None) -> None: + def __init__( + self, display: Optional[Union[bytes, str]] = None, with_cursor: bool = False + ) -> None: """GNU/Linux initialisations.""" super().__init__() + self.with_cursor = with_cursor if not display: try: @@ -306,6 +332,13 @@ def __init__(self, display: Optional[Union[bytes, str]] = None) -> None: raise ScreenShotError("No Xrandr extension found.") self.xrandr = ctypes.cdll.LoadLibrary(xrandr) + if self.with_cursor: + xfixes = ctypes.util.find_library("Xfixes") + if xfixes: + self.xfixes = ctypes.cdll.LoadLibrary(xfixes) + else: + self.with_cursor = False + self._set_cfunctions() self.root = self.xlib.XDefaultRootWindow(self._get_display(display)) @@ -361,6 +394,7 @@ def _set_cfunctions(self) -> None: attrs = { "xlib": self.xlib, "xrandr": self.xrandr, + "xfixes": getattr(self, "xfixes", None), } for func, (attr, argtypes, restype) in CFUNCTIONS.items(): with contextlib.suppress(AttributeError): @@ -450,3 +484,32 @@ def _grab_impl(self, monitor: Monitor) -> ScreenShot: self.xlib.XDestroyImage(ximage) return self.cls_image(data, monitor) + + def _cursor_impl(self) -> ScreenShot: + """Retrieve all cursor data. Pixels have to be RGB.""" + + # Read data of cursor/mouse-pointer + cursor_data = self.xfixes.XFixesGetCursorImage(self._get_display()) + if not (cursor_data and cursor_data.contents): + raise ScreenShotError("Cannot read XFixesGetCursorImage()") + + ximage: XFixesCursorImage = cursor_data.contents + monitor = { + "left": ximage.x - ximage.xhot, + "top": ximage.y - ximage.yhot, + "width": ximage.width, + "height": ximage.height, + } + + raw_data = cast( + ximage.pixels, POINTER(c_ulong * monitor["height"] * monitor["width"]) + ) + raw = bytearray(raw_data.contents) + + data = bytearray(monitor["height"] * monitor["width"] * 4) + data[3::4] = raw[3::8] + data[2::4] = raw[2::8] + data[1::4] = raw[1::8] + data[::4] = raw[::8] + + return self.cls_image(data, monitor) diff --git a/mss/tests/test_gnu_linux.py b/mss/tests/test_gnu_linux.py index b53ad136..abd6cba6 100644 --- a/mss/tests/test_gnu_linux.py +++ b/mss/tests/test_gnu_linux.py @@ -132,3 +132,14 @@ def test_has_extension(): with mss.mss(display=display) as sct: assert sct.has_extension("RANDR") assert not sct.has_extension("NOEXT") + + +def test_with_cursor(): + display = os.getenv("DISPLAY") + with mss.mss(display=display, with_cursor=True) as sct: + assert sct.xfixes + assert sct.with_cursor + sct.grab(sct.monitors[1]) + + # Not really sure how to test the cursor presence ... + # Also need to test when the cursor it outside of the screenshot diff --git a/mss/windows.py b/mss/windows.py index e5c8eee1..04f26e67 100644 --- a/mss/windows.py +++ b/mss/windows.py @@ -22,7 +22,7 @@ UINT, WORD, ) -from typing import Any, Dict +from typing import Any, Dict, Optional from .base import MSSBase from .exception import ScreenShotError @@ -282,3 +282,7 @@ def _grab_impl(self, monitor: Monitor) -> ScreenShot: raise ScreenShotError("gdi32.GetDIBits() failed.") return self.cls_image(bytearray(self._data), monitor) + + def _cursor_impl(self) -> Optional[ScreenShot]: + """Retrieve all cursor data. Pixels have to be RGB.""" + return None From c20d95d44662206cf94fe90f94094c0a01277580 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Sat, 8 Apr 2023 09:04:15 +0200 Subject: [PATCH 078/280] linux: several fixes, and tests (#234) * test: Linux with cursor * refactor!: trying to improve Linux issues * doc + more private stuff * doc --- .github/workflows/tests.yml | 7 +- .pylintrc | 1 + CHANGELOG | 12 ++- CHANGES.rst | 94 +++++++++++------- check.sh | 2 +- dev-requirements.txt | 1 + docs/source/api.rst | 97 ++++++++++++++----- mss/__main__.py | 8 +- mss/base.py | 18 ++-- mss/darwin.py | 26 ++--- mss/linux.py | 161 +++++++++++++------------------ mss/screenshot.py | 16 +-- mss/tests/conftest.py | 36 ++----- mss/tests/test_cls_image.py | 14 ++- mss/tests/test_find_monitors.py | 31 +++--- mss/tests/test_get_pixels.py | 31 +++--- mss/tests/test_gnu_linux.py | 137 +++++++++++++++++++------- mss/tests/test_implementation.py | 103 ++++++++++---------- mss/tests/test_leaks.py | 24 ++++- mss/tests/test_save.py | 64 ++++++------ mss/tests/test_third_party.py | 22 +++-- mss/tests/test_tools.py | 14 +-- mss/tools.py | 8 +- mss/windows.py | 12 +-- setup.cfg | 7 +- 25 files changed, 543 insertions(+), 403 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index ee202880..0869d70c 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -79,12 +79,15 @@ jobs: run: | python -m pip install -U pip wheel python -m pip install -r dev-requirements.txt - - name: Tests on GNU/Linux + - name: Install Xvfb + if: matrix.os.emoji == '🐧' + run: sudo apt install xvfb + - name: Tests (GNU/Linux) if: matrix.os.emoji == '🐧' run: | export DISPLAY=:99 sudo Xvfb -ac ${DISPLAY} -screen 0 1280x1024x24 > /dev/null 2>&1 & python -m pytest - - name: Tests on other platforms + - name: Tests (macOS, Windows) if: matrix.os.emoji != '🐧' run: python -m pytest diff --git a/.pylintrc b/.pylintrc index 07fee3fc..97fac66a 100644 --- a/.pylintrc +++ b/.pylintrc @@ -2,5 +2,6 @@ disable = locally-disabled, too-few-public-methods, too-many-instance-attributes, duplicate-code [REPORTS] +max-line-length = 120 output-format = colorized reports = no diff --git a/CHANGELOG b/CHANGELOG index a5241ade..26e21868 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -3,11 +3,13 @@ History: 8.0.0 2023/0x/xx - - Linux: added mouse support (partially fixes #55) - - Linux: removed get_error_details(), use the ScreenShotError details attribute instead - - dev: removed pre-commit - - tests: removed tox - - tests: added PyPy 3.9 + - Linux: added mouse support (#232) + - Linux: refactored how internal handles are stored to fix issues with multiple X servers, and TKinter. + No more side effects, and when leaving the context manager, resources are all freed (#224, #234) + - ci: added PyPy 3.9 (#226) + - dev: removed pre-commit (#226) + - tests: removed tox (#226) + - tests: improved coverage (#234) 7.0.1 2022/10/27 - fixed the wheel package diff --git a/CHANGES.rst b/CHANGES.rst index 7cd482fe..d03e4db2 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,19 +1,43 @@ +8.0.0 (2023-xx-xx) +================== + +base.py +------- +- Added ``compression_level=6`` keyword argument to ``MSS.__init__()`` +- Added ``display=None`` keyword argument to ``MSS.__init__()`` +- Added ``max_displays=32`` keyword argument to ``MSS.__init__()`` +- Added ``with_cursor=False`` keyword argument to ``MSS.__init__()`` +- Added ``MSS.with_cursor`` attribute + +linux.py +-------- +- Added ``MSS.close()`` +- Moved ``MSS.__init__()`` keyword arguments handling to the base class +- Renamed ``error_handler()`` function to ``__error_handler()`` +- Renamed ``_validate()`` function to ``___validate()`` +- Renamed ``MSS.has_extension()`` method to ``_is_extension_enabled()`` +- Removed ``ERROR`` namespace +- Removed ``MSS.drawable`` attribute +- Removed ``MSS.root`` attribute +- Removed ``MSS.get_error_details()`` method. Use ``ScreenShotError.details`` attribute instead. + + 6.1.0 (2020-10-31) ================== darwin.py --------- - - Added ``CFUNCTIONS`` +- Added ``CFUNCTIONS`` linux.py -------- - - Added ``CFUNCTIONS`` +- Added ``CFUNCTIONS`` windows.py ---------- - - Added ``CFUNCTIONS`` - - Added ``MONITORNUMPROC`` - - Removed ``MSS.monitorenumproc``. Use ``MONITORNUMPROC`` instead. +- Added ``CFUNCTIONS`` +- Added ``MONITORNUMPROC`` +- Removed ``MSS.monitorenumproc``. Use ``MONITORNUMPROC`` instead. 6.0.0 (2020-06-30) @@ -21,30 +45,30 @@ windows.py base.py ------- - - Added ``lock`` - - Added ``MSS._grab_impl()`` (abstract method) - - Added ``MSS._monitors_impl()`` (abstract method) - - ``MSS.grab()`` is no more an abstract method - - ``MSS.monitors`` is no more an abstract property +- Added ``lock`` +- Added ``MSS._grab_impl()`` (abstract method) +- Added ``MSS._monitors_impl()`` (abstract method) +- ``MSS.grab()`` is no more an abstract method +- ``MSS.monitors`` is no more an abstract property darwin.py --------- - - Renamed ``MSS.grab()`` to ``MSS._grab_impl()`` - - Renamed ``MSS.monitors`` to ``MSS._monitors_impl()`` +- Renamed ``MSS.grab()`` to ``MSS._grab_impl()`` +- Renamed ``MSS.monitors`` to ``MSS._monitors_impl()`` linux.py -------- - - Added ``MSS.has_extension()`` - - Removed ``MSS.display`` - - Renamed ``MSS.grab()`` to ``MSS._grab_impl()`` - - Renamed ``MSS.monitors`` to ``MSS._monitors_impl()`` +- Added ``MSS.has_extension()`` +- Removed ``MSS.display`` +- Renamed ``MSS.grab()`` to ``MSS._grab_impl()`` +- Renamed ``MSS.monitors`` to ``MSS._monitors_impl()`` windows.py ---------- - - Removed ``MSS._lock`` - - Renamed ``MSS.srcdc_dict`` to ``MSS._srcdc_dict`` - - Renamed ``MSS.grab()`` to ``MSS._grab_impl()`` - - Renamed ``MSS.monitors`` to ``MSS._monitors_impl()`` +- Removed ``MSS._lock`` +- Renamed ``MSS.srcdc_dict`` to ``MSS._srcdc_dict`` +- Renamed ``MSS.grab()`` to ``MSS._grab_impl()`` +- Renamed ``MSS.monitors`` to ``MSS._monitors_impl()`` 5.1.0 (2020-04-30) @@ -59,7 +83,7 @@ base.py windows.py ---------- - - Replaced ``MSS.srcdc`` with ``MSS.srcdc_dict`` +- Replaced ``MSS.srcdc`` with ``MSS.srcdc_dict`` 5.0.0 (2019-12-31) @@ -67,12 +91,12 @@ windows.py darwin.py --------- -- Added `MSS.__slots__` +- Added ``MSS.__slots__`` linux.py -------- -- Added `MSS.__slots__` -- Deleted `MSS.close()` +- Added ``MSS.__slots__`` +- Deleted ``MSS.close()`` - Deleted ``LAST_ERROR`` constant. Use ``ERROR`` namespace instead, specially the ``ERROR.details`` attribute. models.py @@ -92,8 +116,8 @@ screenshot.py windows.py ---------- -- Added `MSS.__slots__` -- Deleted `MSS.close()` +- Added ``MSS.__slots__`` +- Deleted ``MSS.close()`` 4.0.1 (2019-01-26) @@ -149,15 +173,15 @@ windows.py base.py ------- -- Added ``MSSBase.compression_level`` to control the PNG compression level +- Added ``MSSBase.compression_level`` attribute linux.py -------- -- Added ``MSS.drawable`` to speed-up grabbing. +- Added ``MSS.drawable`` attribute screenshot.py ------------- -- Added ``Screenshot.bgra`` to get BGRA bytes. +- Added ``Screenshot.bgra`` attribute tools.py -------- @@ -181,19 +205,19 @@ __main__.py base.py ------- -- Moved ``ScreenShot`` class to screenshot.py +- Moved ``ScreenShot`` class to ``screenshot.py`` darwin.py --------- -- Added ``CGPoint.__repr__()`` -- Added ``CGRect.__repr__()`` -- Added ``CGSize.__repr__()`` +- Added ``CGPoint.__repr__()`` function +- Added ``CGRect.__repr__()`` function +- Added ``CGSize.__repr__()`` function - Removed ``get_infinity()`` function windows.py ---------- -- Added ``scale()`` method to ``MSS`` class -- Added ``scale_factor`` property to ``MSS`` class +- Added ``MSS.scale()`` method +- Added ``MSS.scale_factor`` property 3.0.0 (2017-07-06) diff --git a/check.sh b/check.sh index ba8a974c..e9000a84 100755 --- a/check.sh +++ b/check.sh @@ -3,7 +3,7 @@ # Small script to ensure quality checks pass before submitting a commit/PR. # python -m isort docs mss -python -m black docs mss +python -m black --line-length=120 docs mss python -m flake8 docs mss python -m pylint mss # "--platform win32" to not fail on ctypes.windll (it does not affect the overall check on other OSes) diff --git a/dev-requirements.txt b/dev-requirements.txt index c5d21e00..589b76eb 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -13,3 +13,4 @@ pylint sphinx twine wheel +xvfbwrapper; sys_platform == "linux" diff --git a/docs/source/api.rst b/docs/source/api.rst index 451f5628..c4fe1026 100644 --- a/docs/source/api.rst +++ b/docs/source/api.rst @@ -12,6 +12,22 @@ macOS .. attribute:: CFUNCTIONS + .. versionadded:: 6.1.0 + +.. function:: cgfloat + +.. class:: CGPoint + +.. class:: CGSize + +.. class:: CGRect + +.. class:: MSS + + .. attribute:: core + + .. attribute:: max_displays + GNU/Linux --------- @@ -19,44 +35,37 @@ GNU/Linux .. attribute:: CFUNCTIONS + .. versionadded:: 6.1.0 + .. attribute:: PLAINMASK .. attribute:: ZPIXMAP -.. class:: MSS +.. class:: Display - .. method:: __init__([display=None, with_cursor=False]) +.. class:: Event - :type display: str or None - :param display: The display to use. - :param with_cursor: Include the mouse cursor in screenshots. +.. class:: XFixesCursorImage - GNU/Linux initializations. +.. class:: XWindowAttributes - .. versionadded:: 8.0.0 - `with_cursor` keyword argument. +.. class:: XImage - .. method:: grab(monitor) +.. class:: XRRModeInfo - :rtype: :class:`~mss.base.ScreenShot` - :raises ScreenShotError: When color depth is not 32 (rare). +.. class:: XRRScreenResources - See :meth:`~mss.base.MSSBase.grab()` for details. +.. class:: XRRCrtcInfo -.. function:: error_handler(display, event) +.. class:: MSS - :type display: ctypes.POINTER(Display) - :param display: The display impacted by the error. - :type event: ctypes.POINTER(Event) - :param event: XError details. - :return int: Always ``0``. + .. attribute:: core - Error handler passed to `X11.XSetErrorHandler()` to catch any error that can happen when calling a X11 function. - This will prevent Python interpreter crashes. + .. method:: close() - When such an error happen, a :class:`~mss.exception.ScreenShotError` exception is raised and all `XError` information are added to the :attr:`~mss.exception.ScreenShotError.details` attribute. + Clean-up method. - .. versionadded:: 3.3.0 + .. versionadded:: 8.0.0 Windows ------- @@ -67,28 +76,70 @@ Windows .. attribute:: CFUNCTIONS + .. versionadded:: 6.1.0 + .. attribute:: DIB_RGB_COLORS .. attribute:: SRCCOPY +.. class:: BITMAPINFOHEADER + +.. class:: BITMAPINFO + +.. attribute:: MONITORNUMPROC + + .. versionadded:: 6.1.0 + +.. class:: MSS + + .. attribute:: gdi32 + + .. attribute:: user32 + Methods ======= .. module:: mss.base +.. attribute:: lock + + .. versionadded:: 6.0.0 + .. class:: MSSBase The parent's class for every OS implementation. + .. attribute:: cls_image + .. attribute:: compression_level PNG compression level used when saving the screenshot data into a file (see :py:func:`zlib.compress()` for details). .. versionadded:: 3.2.0 + .. attribute:: with_cursor + + Include the mouse cursor in screenshots. + + .. versionadded:: 8.0.0 + + .. method:: __init__(compression_level=6, display=None, max_displays=32, with_cursor=False) + + :type compression_level: int + :param compression_level: PNG compression level. + :type display: bytes, str or None + :param display: The display to use. Only effective on GNU/Linux. + :type max_displays: int + :param max_displays: Maximum number of displays. Only effective on macOS. + :type with_cursor: bool + :param with_cursor: Include the mouse cursor in screenshots. + + .. versionadded:: 8.0.0 + ``compression_level``, ``display``, ``max_displays``, and ``with_cursor``, keyword arguments. + .. method:: close() - Clean-up method. Does nothing by default. + Clean-up method. .. versionadded:: 4.0.0 diff --git a/mss/__main__.py b/mss/__main__.py index 8e071a3a..1aa41ad9 100644 --- a/mss/__main__.py +++ b/mss/__main__.py @@ -31,12 +31,8 @@ def main(args: Optional[List[str]] = None) -> int: choices=list(range(10)), help="the PNG compression level", ) - cli_args.add_argument( - "-m", "--monitor", default=0, type=int, help="the monitor to screen shot" - ) - cli_args.add_argument( - "-o", "--output", default="monitor-{mon}.png", help="the output file name" - ) + cli_args.add_argument("-m", "--monitor", default=0, type=int, help="the monitor to screen shot") + cli_args.add_argument("-o", "--output", default="monitor-{mon}.png", help="the output file name") cli_args.add_argument( "-q", "--quiet", diff --git a/mss/base.py b/mss/base.py index 5246f54b..3dd6a55d 100644 --- a/mss/base.py +++ b/mss/base.py @@ -20,10 +20,17 @@ class MSSBase(metaclass=ABCMeta): __slots__ = {"_monitors", "cls_image", "compression_level", "with_cursor"} - def __init__(self) -> None: + def __init__( + self, + compression_level: int = 6, + display: Optional[Union[bytes, str]] = None, # Linux only + max_displays: int = 32, # Mac only + with_cursor: bool = False, + ) -> None: + # pylint: disable=unused-argument self.cls_image: Type[ScreenShot] = ScreenShot - self.compression_level = 6 - self.with_cursor = False + self.compression_level = compression_level + self.with_cursor = with_cursor self._monitors: Monitors = [] def __enter__(self) -> "MSSBase": @@ -227,10 +234,7 @@ def _merge(screenshot: ScreenShot, cursor: ScreenShot) -> ScreenShot: else: alpha = alpha / 255 for i in rgb: - screen_data[spos + i] = int( - cursor_data[cpos + i] * alpha - + screen_data[spos + i] * (1 - alpha) - ) + screen_data[spos + i] = int(cursor_data[cpos + i] * alpha + screen_data[spos + i] * (1 - alpha)) return screenshot diff --git a/mss/darwin.py b/mss/darwin.py index 83543951..a4ba900b 100644 --- a/mss/darwin.py +++ b/mss/darwin.py @@ -5,17 +5,7 @@ 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 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 @@ -104,12 +94,12 @@ class MSS(MSSBase): __slots__ = {"core", "max_displays"} - def __init__(self, **_: Any) -> None: + def __init__(self, **kwargs: Any) -> None: """macOS initialisations.""" - super().__init__() + super().__init__(**kwargs) - self.max_displays = 32 + self.max_displays = kwargs.get("max_displays", 32) self._init_library() self._set_cfunctions() @@ -156,9 +146,7 @@ def _monitors_impl(self) -> None: # Each monitor display_count = c_uint32(0) active_displays = (c_uint32 * self.max_displays)() - core.CGGetActiveDisplayList( - self.max_displays, active_displays, ctypes.byref(display_count) - ) + core.CGGetActiveDisplayList(self.max_displays, active_displays, ctypes.byref(display_count)) rotations = {0.0: "normal", 90.0: "right", -90.0: "left"} for idx in range(display_count.value): display = active_displays[idx] @@ -194,9 +182,7 @@ def _grab_impl(self, monitor: Monitor) -> ScreenShot: # pylint: disable=too-many-locals core = self.core - rect = CGRect( - (monitor["left"], monitor["top"]), (monitor["width"], monitor["height"]) - ) + rect = CGRect((monitor["left"], monitor["top"]), (monitor["width"], monitor["height"])) image_ref = core.CGWindowListCreateImage(rect, 1, 0, 0) if not image_ref: diff --git a/mss/linux.py b/mss/linux.py index 916efa8d..4daa234f 100644 --- a/mss/linux.py +++ b/mss/linux.py @@ -2,15 +2,13 @@ This is part of the MSS Python's module. Source: https://github.com/BoboTiG/python-mss """ -import contextlib -import ctypes -import ctypes.util import os -import threading +from contextlib import suppress from ctypes import ( CFUNCTYPE, POINTER, Structure, + byref, c_char_p, c_int, c_int32, @@ -23,8 +21,12 @@ c_ushort, c_void_p, cast, + cdll, + create_string_buffer, ) -from typing import Any, Dict, Optional, Tuple, Union +from ctypes.util import find_library +from threading import current_thread, local +from typing import Any, Tuple from .base import MSSBase, lock from .exception import ScreenShotError @@ -182,23 +184,20 @@ class XRRCrtcInfo(Structure): @CFUNCTYPE(c_int, POINTER(Display), POINTER(Event)) -def error_handler(display: Display, event: Event) -> int: +def _error_handler(display: Display, event: Event) -> int: """Specifies the program's supplied error handler.""" - x11 = ctypes.util.find_library("X11") - if not x11: - return 0 # Get the specific error message - xlib = ctypes.cdll.LoadLibrary(x11) - get_error = getattr(xlib, "XGetErrorText") + xlib = cdll.LoadLibrary(find_library("X11")) # type: ignore[arg-type] + get_error = xlib.XGetErrorText get_error.argtypes = [POINTER(Display), c_int, c_char_p, c_int] get_error.restype = c_void_p evt = event.contents - error = ctypes.create_string_buffer(1024) + error = create_string_buffer(1024) get_error(display, evt.error_code, error, len(error)) - _ERROR[threading.current_thread()] = { + _ERROR[current_thread()] = { "error": error.value.decode("utf-8"), "error_code": evt.error_code, "minor_code": evt.minor_code, @@ -210,14 +209,14 @@ def error_handler(display: Display, event: Event) -> int: return 0 -def validate(retval: int, func: Any, args: Tuple[Any, Any]) -> Tuple[Any, Any]: +def _validate(retval: int, func: Any, args: Tuple[Any, Any]) -> Tuple[Any, Any]: """Validate the returned value of a Xlib or XRANDR function.""" - current_thread = threading.current_thread() - if retval != 0 and current_thread not in _ERROR: + thread = current_thread() + if retval != 0 and thread not in _ERROR: return args - details = _ERROR.pop(current_thread, {}) + details = _ERROR.pop(thread, {}) raise ScreenShotError(f"{func.__name__}() failed", details=details) @@ -231,6 +230,7 @@ def validate(retval: int, func: Any, args: Tuple[Any, Any]) -> Tuple[Any, Any]: # # Note: keep it sorted by cfunction. CFUNCTIONS: CFunctions = { + "XCloseDisplay": ("xlib", [POINTER(Display)], c_void_p), "XDefaultRootWindow": ("xlib", [POINTER(Display)], POINTER(XWindowAttributes)), "XDestroyImage": ("xlib", [POINTER(XImage)], c_void_p), "XFixesGetCursorImage": ("xfixes", [POINTER(Display)], POINTER(XFixesCursorImage)), @@ -292,25 +292,19 @@ class MSS(MSSBase): It uses intensively the Xlib and its Xrandr extension. """ - __slots__ = {"drawable", "root", "xlib", "xrandr", "xfixes", "__with_cursor"} + __slots__ = {"xlib", "xrandr", "xfixes", "_handles"} - # A dict to maintain *display* values created by multiple threads. - _display_dict: Dict[threading.Thread, int] = {} - - def __init__( - self, display: Optional[Union[bytes, str]] = None, with_cursor: bool = False - ) -> None: + def __init__(self, **kwargs: Any) -> None: """GNU/Linux initialisations.""" - super().__init__() - self.with_cursor = with_cursor + super().__init__(**kwargs) + display = kwargs.get("display", b"") if not display: try: display = os.environ["DISPLAY"].encode("utf-8") except KeyError: - # pylint: disable=raise-missing-from - raise ScreenShotError("$DISPLAY not set.") + raise ScreenShotError("$DISPLAY not set.") from None if not isinstance(display, bytes): display = display.encode("utf-8") @@ -318,40 +312,50 @@ def __init__( if b":" not in display: raise ScreenShotError(f"Bad display value: {display!r}.") - x11 = ctypes.util.find_library("X11") + x11 = find_library("X11") if not x11: raise ScreenShotError("No X11 library found.") - self.xlib = ctypes.cdll.LoadLibrary(x11) + self.xlib = cdll.LoadLibrary(x11) # Install the error handler to prevent interpreter crashes: # any error will raise a ScreenShotError exception. - self.xlib.XSetErrorHandler(error_handler) + self.xlib.XSetErrorHandler(_error_handler) - xrandr = ctypes.util.find_library("Xrandr") + xrandr = find_library("Xrandr") if not xrandr: raise ScreenShotError("No Xrandr extension found.") - self.xrandr = ctypes.cdll.LoadLibrary(xrandr) + self.xrandr = cdll.LoadLibrary(xrandr) if self.with_cursor: - xfixes = ctypes.util.find_library("Xfixes") + xfixes = find_library("Xfixes") if xfixes: - self.xfixes = ctypes.cdll.LoadLibrary(xfixes) + self.xfixes = cdll.LoadLibrary(xfixes) else: self.with_cursor = False self._set_cfunctions() - self.root = self.xlib.XDefaultRootWindow(self._get_display(display)) + self._handles = local() + self._handles.display = self.xlib.XOpenDisplay(display) - if not self.has_extension("RANDR"): - raise ScreenShotError("No Xrandr extension found.") + if not self._is_extension_enabled("RANDR"): + raise ScreenShotError("Xrandr not enabled.") + + self._handles.root = self.xlib.XDefaultRootWindow(self._handles.display) # Fix for XRRGetScreenResources and XGetImage: # expected LP_Display instance instead of LP_XWindowAttributes - self.drawable = ctypes.cast(self.root, POINTER(Display)) + self._handles.drawable = cast(self._handles.root, POINTER(Display)) + + def close(self) -> None: + if self._handles.display is not None: + self.xlib.XCloseDisplay(self._handles.display) + self._handles.display = None - def has_extension(self, extension: str) -> bool: - """Return True if the given *extension* is part of the extensions list of the server.""" + _ERROR.clear() + + def _is_extension_enabled(self, name: str) -> bool: + """Return True if the given *extension* is enabled on the server.""" with lock: major_opcode_return = c_int() first_event_return = c_int() @@ -359,34 +363,16 @@ def has_extension(self, extension: str) -> bool: try: self.xlib.XQueryExtension( - self._get_display(), - extension.encode("latin1"), - ctypes.byref(major_opcode_return), - ctypes.byref(first_event_return), - ctypes.byref(first_error_return), + self._handles.display, + name.encode("latin1"), + byref(major_opcode_return), + byref(first_event_return), + byref(first_error_return), ) except ScreenShotError: return False return True - def _get_display(self, disp: Optional[bytes] = None) -> int: - """ - Retrieve a thread-safe display from XOpenDisplay(). - In multithreading, if the thread that creates *display* is dead, *display* will - no longer be valid to grab the screen. The *display* attribute is replaced - with *_display_dict* to maintain the *display* values in multithreading. - Since the current thread and main thread are always alive, reuse their - *display* value first. - """ - current_thread = threading.current_thread() - current_display = MSS._display_dict.get(current_thread) - if current_display: - display = current_display - else: - display = self.xlib.XOpenDisplay(disp) - MSS._display_dict[current_thread] = display - return display - def _set_cfunctions(self) -> None: """Set all ctypes functions and attach them to attributes.""" @@ -397,10 +383,10 @@ def _set_cfunctions(self) -> None: "xfixes": getattr(self, "xfixes", None), } for func, (attr, argtypes, restype) in CFUNCTIONS.items(): - with contextlib.suppress(AttributeError): + with suppress(AttributeError): cfactory( attr=attrs[attr], - errcheck=validate, + errcheck=_validate, func=func, argtypes=argtypes, restype=restype, @@ -409,20 +395,15 @@ def _set_cfunctions(self) -> None: def _monitors_impl(self) -> None: """Get positions of monitors. It will populate self._monitors.""" - display = self._get_display() + display = self._handles.display int_ = int xrandr = self.xrandr # All monitors gwa = XWindowAttributes() - self.xlib.XGetWindowAttributes(display, self.root, ctypes.byref(gwa)) + 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 @@ -431,9 +412,9 @@ def _monitors_impl(self) -> None: # XRRGetScreenResourcesCurrent(): 0.0039125580078689 s # The second is faster by a factor of 44! So try to use it first. try: - mon = xrandr.XRRGetScreenResourcesCurrent(display, self.drawable).contents + mon = xrandr.XRRGetScreenResourcesCurrent(display, self._handles.drawable).contents except AttributeError: - mon = xrandr.XRRGetScreenResources(display, self.drawable).contents + mon = xrandr.XRRGetScreenResources(display, self._handles.drawable).contents crtcs = mon.crtcs for idx in range(mon.ncrtc): @@ -457,8 +438,8 @@ def _grab_impl(self, monitor: Monitor) -> ScreenShot: """Retrieve all pixels from a monitor. Pixels have to be RGB.""" ximage = self.xlib.XGetImage( - self._get_display(), - self.drawable, + self._handles.display, + self._handles.drawable, monitor["left"], monitor["top"], monitor["width"], @@ -470,11 +451,9 @@ 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}." - ) + raise ScreenShotError(f"[XImage] bits per pixel value not (yet?) implemented: {bits_per_pixel}.") - raw_data = ctypes.cast( + raw_data = cast( ximage.contents.data, POINTER(c_ubyte * monitor["height"] * monitor["width"] * 4), ) @@ -489,21 +468,19 @@ def _cursor_impl(self) -> ScreenShot: """Retrieve all cursor data. Pixels have to be RGB.""" # Read data of cursor/mouse-pointer - cursor_data = self.xfixes.XFixesGetCursorImage(self._get_display()) - if not (cursor_data and cursor_data.contents): + ximage = self.xfixes.XFixesGetCursorImage(self._handles.display) + if not (ximage and ximage.contents): raise ScreenShotError("Cannot read XFixesGetCursorImage()") - ximage: XFixesCursorImage = cursor_data.contents + cursor_img: XFixesCursorImage = ximage.contents monitor = { - "left": ximage.x - ximage.xhot, - "top": ximage.y - ximage.yhot, - "width": ximage.width, - "height": ximage.height, + "left": cursor_img.x - cursor_img.xhot, + "top": cursor_img.y - cursor_img.yhot, + "width": cursor_img.width, + "height": cursor_img.height, } - raw_data = cast( - ximage.pixels, POINTER(c_ulong * monitor["height"] * monitor["width"]) - ) + raw_data = cast(cursor_img.pixels, POINTER(c_ulong * monitor["height"] * monitor["width"])) raw = bytearray(raw_data.contents) data = bytearray(monitor["height"] * monitor["width"] * 4) diff --git a/mss/screenshot.py b/mss/screenshot.py index 71ebc2c1..94bedfc1 100644 --- a/mss/screenshot.py +++ b/mss/screenshot.py @@ -21,9 +21,7 @@ class ScreenShot: __slots__ = {"__pixels", "__rgb", "pos", "raw", "size"} - def __init__( - self, data: bytearray, monitor: Monitor, size: Optional[Size] = None - ) -> None: + def __init__(self, data: bytearray, monitor: Monitor, size: Optional[Size] = None) -> None: self.__pixels: Optional[Pixels] = None self.__rgb: Optional[bytes] = None @@ -57,9 +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) @@ -86,9 +82,7 @@ def pixels(self) -> Pixels: """ if not self.__pixels: - rgb_tuples: Iterator[Pixel] = zip( - self.raw[2::4], self.raw[1::4], self.raw[::4] - ) + 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 return self.__pixels @@ -134,6 +128,4 @@ def pixel(self, coord_x: int, coord_y: int) -> Pixel: return self.pixels[coord_y][coord_x] # type: ignore except IndexError: # pylint: disable=raise-missing-from - raise ScreenShotError( - f"Pixel location ({coord_x}, {coord_y}) is out of range." - ) + raise ScreenShotError(f"Pixel location ({coord_x}, {coord_y}) is out of range.") diff --git a/mss/tests/conftest.py b/mss/tests/conftest.py index fc6a346c..979850db 100644 --- a/mss/tests/conftest.py +++ b/mss/tests/conftest.py @@ -4,10 +4,11 @@ """ import glob import os +from pathlib import Path import pytest -import mss +from mss import mss @pytest.fixture(autouse=True) @@ -16,9 +17,7 @@ def no_warnings(recwarn): yield - warnings = [ - "{w.filename}:{w.lineno} {w.message}".format(w=warning) for warning in recwarn - ] + warnings = ["{w.filename}:{w.lineno} {w.message}".format(w=warning) for warning in recwarn] for warning in warnings: print(warning) assert not warnings @@ -41,33 +40,18 @@ def before_tests(request): request.addfinalizer(purge_files) -@pytest.fixture(scope="module") -def sct(): - try: - # `display` kwarg is only for GNU/Linux - return mss.mss(display=os.getenv("DISPLAY")) - except TypeError: - return mss.mss() - - @pytest.fixture(scope="session") -def is_travis(): - return "TRAVIS" in os.environ +def raw() -> bytes: + file = Path(__file__).parent / "res" / "monitor-1024x768.raw" + return file.read_bytes() @pytest.fixture(scope="session") -def raw(): - here = os.path.dirname(__file__) - file = os.path.join(here, "res", "monitor-1024x768.raw") - with open(file, "rb") as f: - yield f.read() - - -@pytest.fixture(scope="module") -def pixel_ratio(sct): +def pixel_ratio() -> int: """Get the pixel, used to adapt test checks.""" # Grab a 1x1 screenshot region = {"top": 0, "left": 0, "width": 1, "height": 1} - # On macOS with Retina display, the width will be 2 instead of 1 - return sct.grab(region).size[0] + with mss(display=os.getenv("DISPLAY")) as sct: + # On macOS with Retina display, the width will be 2 instead of 1 + return sct.grab(region).size[0] diff --git a/mss/tests/test_cls_image.py b/mss/tests/test_cls_image.py index a3b198cc..b531ba10 100644 --- a/mss/tests/test_cls_image.py +++ b/mss/tests/test_cls_image.py @@ -2,18 +2,22 @@ This is part of the MSS Python's module. Source: https://github.com/BoboTiG/python-mss """ +import os + +from mss import mss class SimpleScreenShot: - def __init__(self, data, monitor, **kwargs): + def __init__(self, data, monitor, **_): self.raw = bytes(data) self.monitor = monitor -def test_custom_cls_image(sct): - sct.cls_image = SimpleScreenShot - mon1 = sct.monitors[1] - image = sct.grab(mon1) +def test_custom_cls_image(): + with mss(display=os.getenv("DISPLAY")) as sct: + sct.cls_image = SimpleScreenShot + mon1 = sct.monitors[1] + image = sct.grab(mon1) assert isinstance(image, SimpleScreenShot) assert isinstance(image.raw, bytes) assert isinstance(image.monitor, dict) diff --git a/mss/tests/test_find_monitors.py b/mss/tests/test_find_monitors.py index c5b15695..278dc1b0 100644 --- a/mss/tests/test_find_monitors.py +++ b/mss/tests/test_find_monitors.py @@ -2,33 +2,36 @@ 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(sct): - assert sct.monitors +def test_get_monitors(): + with mss(display=os.getenv("DISPLAY")) as sct: + assert sct.monitors -def test_keys_aio(sct): - all_monitors = sct.monitors[0] + +def test_keys_aio(): + with mss(display=os.getenv("DISPLAY")) as sct: + all_monitors = sct.monitors[0] assert "top" in all_monitors assert "left" in all_monitors assert "height" in all_monitors assert "width" in all_monitors -def test_keys_monitor_1(sct): - mon1 = sct.monitors[1] +def test_keys_monitor_1(): + with mss(display=os.getenv("DISPLAY")) as sct: + mon1 = sct.monitors[1] assert "top" in mon1 assert "left" in mon1 assert "height" in mon1 assert "width" in mon1 -def test_dimensions(sct, is_travis): - mon = sct.monitors[1] - if is_travis: - assert mon["width"] == 1280 - assert mon["height"] == 1240 - else: - assert mon["width"] > 0 - assert mon["height"] > 0 +def test_dimensions(): + with mss(display=os.getenv("DISPLAY")) as sct: + mon = sct.monitors[1] + assert mon["width"] > 0 + assert mon["height"] > 0 diff --git a/mss/tests/test_get_pixels.py b/mss/tests/test_get_pixels.py index 0abf4d94..b6a7b6ea 100644 --- a/mss/tests/test_get_pixels.py +++ b/mss/tests/test_get_pixels.py @@ -2,23 +2,28 @@ This is part of the MSS Python's module. Source: https://github.com/BoboTiG/python-mss """ +import os + import pytest +from mss import mss from mss.base import ScreenShot from mss.exception import ScreenShotError -def test_grab_monitor(sct): - for mon in sct.monitors: - image = sct.grab(mon) - assert isinstance(image, ScreenShot) - assert isinstance(image.raw, bytearray) - assert isinstance(image.rgb, bytes) +def test_grab_monitor(): + with mss(display=os.getenv("DISPLAY")) as sct: + for mon in sct.monitors: + image = sct.grab(mon) + assert isinstance(image, ScreenShot) + assert isinstance(image.raw, bytearray) + assert isinstance(image.rgb, bytes) -def test_grab_part_of_screen(sct, pixel_ratio): +def test_grab_part_of_screen(pixel_ratio): monitor = {"top": 160, "left": 160, "width": 160, "height": 160} - image = sct.grab(monitor) + with mss(display=os.getenv("DISPLAY")) as sct: + image = sct.grab(monitor) assert isinstance(image, ScreenShot) assert isinstance(image.raw, bytearray) assert isinstance(image.rgb, bytes) @@ -28,9 +33,10 @@ def test_grab_part_of_screen(sct, pixel_ratio): assert image.height == 160 * pixel_ratio -def test_grab_part_of_screen_rounded(sct, pixel_ratio): +def test_grab_part_of_screen_rounded(pixel_ratio): monitor = {"top": 160, "left": 160, "width": 161, "height": 159} - image = sct.grab(monitor) + with mss(display=os.getenv("DISPLAY")) as sct: + image = sct.grab(monitor) assert isinstance(image, ScreenShot) assert isinstance(image.raw, bytearray) assert isinstance(image.rgb, bytes) @@ -40,9 +46,10 @@ def test_grab_part_of_screen_rounded(sct, pixel_ratio): assert image.height == 159 * pixel_ratio -def test_grab_individual_pixels(sct): +def test_grab_individual_pixels(): monitor = {"top": 160, "left": 160, "width": 222, "height": 42} - image = sct.grab(monitor) + with mss(display=os.getenv("DISPLAY")) as sct: + image = sct.grab(monitor) assert isinstance(image.pixel(0, 0), tuple) with pytest.raises(ScreenShotError): image.pixel(image.width + 1, 12) diff --git a/mss/tests/test_gnu_linux.py b/mss/tests/test_gnu_linux.py index abd6cba6..6a86ce5f 100644 --- a/mss/tests/test_gnu_linux.py +++ b/mss/tests/test_gnu_linux.py @@ -3,8 +3,8 @@ Source: https://github.com/BoboTiG/python-mss """ import ctypes.util -import os import platform +from unittest.mock import Mock, patch import pytest @@ -13,12 +13,24 @@ from mss.base import MSSBase from mss.exception import ScreenShotError -if platform.system().lower() != "linux": - pytestmark = pytest.mark.skip - +xvfbwrapper = pytest.importorskip("xvfbwrapper") PYPY = platform.python_implementation() == "PyPy" +WIDTH = 200 +HEIGHT = 200 +DEPTH = 24 + + +@pytest.fixture +def display() -> str: + vdisplay = xvfbwrapper.Xvfb(width=WIDTH, height=HEIGHT, colordepth=DEPTH) + vdisplay.start() + try: + yield f":{vdisplay.new_display}" + finally: + vdisplay.stop() + @pytest.mark.skipif(PYPY, reason="Failure on PyPy") def test_factory_systems(monkeypatch): @@ -39,21 +51,20 @@ def test_factory_systems(monkeypatch): monkeypatch.setattr(platform, "system", lambda: "Darwin") with pytest.raises((ScreenShotError, ValueError)): # ValueError on macOS Big Sur - mss.mss() + with mss.mss(): + pass monkeypatch.undo() # Windows monkeypatch.setattr(platform, "system", lambda: "wInDoWs") with pytest.raises(ImportError): # ImportError: cannot import name 'WINFUNCTYPE' - mss.mss() - + with mss.mss(): + pass -def test_arg_display(monkeypatch): - import mss +def test_arg_display(display: str, monkeypatch): # Good value - display = os.getenv("DISPLAY") with mss.mss(display=display): pass @@ -71,22 +82,20 @@ def test_arg_display(monkeypatch): @pytest.mark.skipif(PYPY, reason="Failure on PyPy") def test_bad_display_structure(monkeypatch): - import mss.linux - monkeypatch.setattr(mss.linux, "Display", lambda: None) with pytest.raises(TypeError): with mss.mss(): pass -def test_no_xlib_library(monkeypatch): - monkeypatch.setattr(ctypes.util, "find_library", lambda x: None) - with pytest.raises(ScreenShotError): - with mss.mss(): - pass +def test_no_xlib_library(): + with patch("mss.linux.find_library", return_value=None): + with pytest.raises(ScreenShotError): + with mss.mss(): + pass -def test_no_xrandr_extension(monkeypatch): +def test_no_xrandr_extension(): x11 = ctypes.util.find_library("X11") def find_lib_mocked(lib): @@ -100,15 +109,31 @@ def find_lib_mocked(lib): return None if lib == "Xrandr" else x11 # No `Xrandr` library - monkeypatch.setattr(ctypes.util, "find_library", find_lib_mocked) + with patch("mss.linux.find_library", find_lib_mocked): + with pytest.raises(ScreenShotError): + mss.mss() + + +@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(): + with mss.mss(display=display): pass -def test_region_out_of_monitor_bounds(): - display = os.getenv("DISPLAY") - monitor = {"left": -30, "top": 0, "width": 100, "height": 100} +def test_unsupported_depth(): + vdisplay = xvfbwrapper.Xvfb(width=WIDTH, height=HEIGHT, colordepth=8) + vdisplay.start() + try: + with pytest.raises(ScreenShotError): + with mss.mss(display=f":{vdisplay.new_display}") as sct: + sct.grab(sct.monitors[1]) + finally: + vdisplay.stop() + + +def test_region_out_of_monitor_bounds(display: str): + monitor = {"left": -30, "top": 0, "width": WIDTH, "height": HEIGHT} assert not mss.linux._ERROR @@ -127,19 +152,65 @@ def test_region_out_of_monitor_bounds(): assert not mss.linux._ERROR -def test_has_extension(): - display = os.getenv("DISPLAY") +def test__is_extension_enabled_unknown_name(display: str): + with mss.mss(display=display) as sct: + assert not sct._is_extension_enabled("NOEXT") + + +def test_missing_fast_function_for_monitor_details_retrieval(display: str): + with mss.mss(display=display) as sct: + 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 hasattr(sct.xrandr, "XRRGetScreenResourcesCurrent") + del sct.xrandr.XRRGetScreenResourcesCurrent + screenshot_with_slow_fn = sct.grab(sct.monitors[1]) + + assert set(screenshot_with_slow_fn.rgb) == {0} + + +def test_with_cursor(display: str): with mss.mss(display=display) as sct: - assert sct.has_extension("RANDR") - assert not sct.has_extension("NOEXT") + assert not hasattr(sct, "xfixes") + assert not sct.with_cursor + screenshot_without_cursor = sct.grab(sct.monitors[1]) + # 1 color: black + assert set(screenshot_without_cursor.rgb) == {0} -def test_with_cursor(): - display = os.getenv("DISPLAY") with mss.mss(display=display, with_cursor=True) as sct: - assert sct.xfixes + assert hasattr(sct, "xfixes") assert sct.with_cursor - sct.grab(sct.monitors[1]) + screenshot_with_cursor = sct.grab(sct.monitors[1]) + + # 2 colors: black & white (default cursor is a white cross) + assert set(screenshot_with_cursor.rgb) == {0, 255} + + +def test_with_cursor_but_not_xfixes_extension_found(display: str): + x11 = ctypes.util.find_library("X11") + + def find_lib_mocked(lib): + """ + Returns None to emulate no XRANDR library. + Returns the previous found X11 library else. - # Not really sure how to test the cursor presence ... - # Also need to test when the cursor it outside of the screenshot + It is a naive approach, but works for now. + """ + + return None if lib == "Xfixes" else x11 + + with patch("mss.linux.find_library", find_lib_mocked): + 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): + with mss.mss(display=display, with_cursor=True) as sct: + with patch.object(sct.xfixes, "XFixesGetCursorImage", return_value=None): + with pytest.raises(ScreenShotError): + sct.grab(sct.monitors[1]) diff --git a/mss/tests/test_implementation.py b/mss/tests/test_implementation.py index f278c97b..dee34fc2 100644 --- a/mss/tests/test_implementation.py +++ b/mss/tests/test_implementation.py @@ -8,8 +8,8 @@ import pytest -import mss import mss.tools +from mss import mss from mss.base import MSSBase from mss.exception import ScreenShotError from mss.screenshot import ScreenShot @@ -42,12 +42,13 @@ def test_incomplete_class(cls): cls() -def test_bad_monitor(sct): - with pytest.raises(ScreenShotError): - sct.grab(sct.shot(mon=222)) +def test_bad_monitor(): + with mss(display=os.getenv("DISPLAY")) as sct: + with pytest.raises(ScreenShotError): + sct.shot(mon=222) -def test_repr(sct, pixel_ratio): +def test_repr(pixel_ratio): box = {"top": 0, "left": 0, "width": 10, "height": 10} expected_box = { "top": 0, @@ -55,27 +56,28 @@ def test_repr(sct, pixel_ratio): "width": 10 * pixel_ratio, "height": 10 * pixel_ratio, } - img = sct.grab(box) + with 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): # Current system - with mss.mss() as sct: + with 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] assert error == "System 'chuck norris' not (yet?) implemented." -def test_entry_point(capsys, sct): +def test_entry_point(capsys): from datetime import datetime from mss.__main__ import main @@ -98,11 +100,12 @@ def test_entry_point(capsys, sct): for opt in ("-o", "--out"): main([opt, fmt]) out, _ = capsys.readouterr() - for monitor, line in zip(sct.monitors[1:], out.splitlines()): - filename = fmt.format(**monitor) - assert line.endswith(filename) - assert os.path.isfile(filename) - os.remove(filename) + with mss(display=os.getenv("DISPLAY")) as sct: + for monitor, line in zip(sct.monitors[1:], out.splitlines()): + filename = fmt.format(**monitor) + assert line.endswith(filename) + assert os.path.isfile(filename) + os.remove(filename) fmt = "sct_{mon}-{date:%Y-%m-%d}.png" for opt in ("-o", "--out"): @@ -129,7 +132,7 @@ def test_entry_point(capsys, sct): assert out == "Coordinates syntax: top, left, width, height\n" -def test_grab_with_tuple(sct, pixel_ratio): +def test_grab_with_tuple(pixel_ratio): left = 100 top = 100 right = 500 @@ -137,39 +140,41 @@ def test_grab_with_tuple(sct, pixel_ratio): width = right - left # 400px width height = lower - top # 400px height - # PIL like - box = (left, top, right, lower) - im = sct.grab(box) - assert im.size == (width * pixel_ratio, height * pixel_ratio) - - # MSS like - box2 = {"left": left, "top": top, "width": width, "height": height} - im2 = sct.grab(box2) - assert im.size == im2.size - assert im.pos == im2.pos - assert im.rgb == im2.rgb - - -def test_grab_with_tuple_percents(sct, pixel_ratio): - 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 - right = left + 500 # 500px - lower = top + 500 # 500px - width = right - left - height = lower - top - - # PIL like - box = (left, top, right, lower) - im = sct.grab(box) - assert im.size == (width * pixel_ratio, height * pixel_ratio) - - # MSS like - box2 = {"left": left, "top": top, "width": width, "height": height} - im2 = sct.grab(box2) - assert im.size == im2.size - assert im.pos == im2.pos - assert im.rgb == im2.rgb + with mss(display=os.getenv("DISPLAY")) as sct: + # PIL like + box = (left, top, right, lower) + im = sct.grab(box) + assert im.size == (width * pixel_ratio, height * pixel_ratio) + + # MSS like + box2 = {"left": left, "top": top, "width": width, "height": height} + im2 = sct.grab(box2) + assert im.size == im2.size + assert im.pos == im2.pos + assert im.rgb == im2.rgb + + +def test_grab_with_tuple_percents(pixel_ratio): + with 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 + right = left + 500 # 500px + lower = top + 500 # 500px + width = right - left + height = lower - top + + # PIL like + box = (left, top, right, lower) + im = sct.grab(box) + assert im.size == (width * pixel_ratio, height * pixel_ratio) + + # MSS like + box2 = {"left": left, "top": top, "width": width, "height": height} + im2 = sct.grab(box2) + assert im.size == im2.size + assert im.pos == im2.pos + assert im.rgb == im2.rgb def test_thread_safety(): @@ -182,7 +187,7 @@ def record(check): start_time = time.time() while time.time() - start_time < 1: - with mss.mss() as sct: + with mss() as sct: sct.grab(sct.monitors[1]) check[threading.current_thread()] = True diff --git a/mss/tests/test_leaks.py b/mss/tests/test_leaks.py index ad8147c3..6b48bcc5 100644 --- a/mss/tests/test_leaks.py +++ b/mss/tests/test_leaks.py @@ -49,6 +49,7 @@ def monitor_func() -> Callable[[], int]: def bound_instance_without_cm(): + # Will always leak for now sct = mss() sct.shot() @@ -62,6 +63,7 @@ def bound_instance_without_cm_but_use_close(): def unbound_instance_without_cm(): + # Will always leak for now mss().shot() @@ -90,16 +92,34 @@ def regression_issue_135(): sct.grab(bounding_box_score) +def regression_issue_210(): + """Regression test for issue #210: multiple X servers.""" + xvfbwrapper = pytest.importorskip("xvfbwrapper") + + vdisplay = xvfbwrapper.Xvfb(width=1920, height=1080, colordepth=24) + vdisplay.start() + with mss(): + pass + vdisplay.stop() + + vdisplay = xvfbwrapper.Xvfb(width=1920, height=1080, colordepth=24) + vdisplay.start() + with mss(): + pass + vdisplay.stop() + + @pytest.mark.skipif(OS == "darwin", reason="No possible leak on macOS.") @pytest.mark.parametrize( "func", ( - bound_instance_without_cm, + # bound_instance_without_cm, bound_instance_without_cm_but_use_close, - unbound_instance_without_cm, + # unbound_instance_without_cm, with_context_manager, regression_issue_128, regression_issue_135, + regression_issue_210, ), ) def test_resource_leaks(func, monitor_func): diff --git a/mss/tests/test_save.py b/mss/tests/test_save.py index 5c11494e..6dfbc19c 100644 --- a/mss/tests/test_save.py +++ b/mss/tests/test_save.py @@ -7,60 +7,68 @@ import pytest +from mss import mss -def test_at_least_2_monitors(sct): - assert list(sct.save(mon=0)) +def test_at_least_2_monitors(): + with mss(display=os.getenv("DISPLAY")) as sct: + assert list(sct.save(mon=0)) -def test_files_exist(sct): - for filename in sct.save(): - assert os.path.isfile(filename) - assert os.path.isfile(sct.shot()) +def test_files_exist(): + with mss(display=os.getenv("DISPLAY")) as sct: + for filename in sct.save(): + assert os.path.isfile(filename) + + assert os.path.isfile(sct.shot()) - sct.shot(mon=-1, output="fullscreen.png") - assert os.path.isfile("fullscreen.png") + sct.shot(mon=-1, output="fullscreen.png") + assert os.path.isfile("fullscreen.png") -def test_callback(sct): +def test_callback(): def on_exists(fname): if os.path.isfile(fname): new_file = f"{fname}.old" os.rename(fname, new_file) - filename = sct.shot(mon=0, output="mon0.png", callback=on_exists) - assert os.path.isfile(filename) + with mss(display=os.getenv("DISPLAY")) as sct: + filename = sct.shot(mon=0, output="mon0.png", callback=on_exists) + assert os.path.isfile(filename) - filename = sct.shot(output="mon1.png", callback=on_exists) - assert os.path.isfile(filename) + filename = sct.shot(output="mon1.png", callback=on_exists) + assert os.path.isfile(filename) -def test_output_format_simple(sct): - filename = sct.shot(mon=1, output="mon-{mon}.png") +def test_output_format_simple(): + 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(sct): +def test_output_format_positions_and_sizes(): fmt = "sct-{top}x{left}_{width}x{height}.png" - filename = sct.shot(mon=1, output=fmt) - assert filename == fmt.format(**sct.monitors[1]) + with mss(display=os.getenv("DISPLAY")) as sct: + filename = sct.shot(mon=1, output=fmt) + assert filename == fmt.format(**sct.monitors[1]) assert os.path.isfile(filename) -def test_output_format_date_simple(sct): +def test_output_format_date_simple(): fmt = "sct_{mon}-{date}.png" - try: - filename = sct.shot(mon=1, output=fmt) - except IOError: - # [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.") - else: - assert os.path.isfile(filename) + with mss(display=os.getenv("DISPLAY")) as sct: + try: + filename = sct.shot(mon=1, output=fmt) + assert os.path.isfile(filename) + except IOError: + # [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(sct): +def test_output_format_date_custom(): fmt = "sct_{date:%Y-%m-%d}.png" - filename = sct.shot(mon=1, output=fmt) + with mss(display=os.getenv("DISPLAY")) as sct: + filename = sct.shot(mon=1, output=fmt) assert filename == fmt.format(date=datetime.now()) assert os.path.isfile(filename) diff --git a/mss/tests/test_third_party.py b/mss/tests/test_third_party.py index 1c2551f0..e89afbdf 100644 --- a/mss/tests/test_third_party.py +++ b/mss/tests/test_third_party.py @@ -8,6 +8,8 @@ import pytest +from mss import mss + try: import numpy except (ImportError, RuntimeError): @@ -21,17 +23,19 @@ @pytest.mark.skipif(numpy is None, reason="Numpy module not available.") -def test_numpy(sct, pixel_ratio): +def test_numpy(pixel_ratio): box = {"top": 0, "left": 0, "width": 10, "height": 10} - img = numpy.array(sct.grab(box)) + with mss(display=os.getenv("DISPLAY")) as sct: + img = numpy.array(sct.grab(box)) assert len(img) == 10 * pixel_ratio @pytest.mark.skipif(Image is None, reason="PIL module not available.") -def test_pil(sct): +def test_pil(): width, height = 16, 16 box = {"top": 0, "left": 0, "width": width, "height": height} - sct_img = sct.grab(box) + with mss(display=os.getenv("DISPLAY")) as sct: + sct_img = sct.grab(box) img = Image.frombytes("RGB", sct_img.size, sct_img.rgb) assert img.mode == "RGB" @@ -45,10 +49,11 @@ def test_pil(sct): @pytest.mark.skipif(Image is None, reason="PIL module not available.") -def test_pil_bgra(sct): +def test_pil_bgra(): width, height = 16, 16 box = {"top": 0, "left": 0, "width": width, "height": height} - sct_img = sct.grab(box) + with mss(display=os.getenv("DISPLAY")) as sct: + sct_img = sct.grab(box) img = Image.frombytes("RGB", sct_img.size, sct_img.bgra, "raw", "BGRX") assert img.mode == "RGB" @@ -62,10 +67,11 @@ def test_pil_bgra(sct): @pytest.mark.skipif(Image is None, reason="PIL module not available.") -def test_pil_not_16_rounded(sct): +def test_pil_not_16_rounded(): width, height = 10, 10 box = {"top": 0, "left": 0, "width": width, "height": height} - sct_img = sct.grab(box) + with mss(display=os.getenv("DISPLAY")) as sct: + sct_img = sct.grab(box) img = Image.frombytes("RGB", sct_img.size, sct_img.rgb) assert img.mode == "RGB" diff --git a/mss/tests/test_tools.py b/mss/tests/test_tools.py index d1fa286d..d939cb1c 100644 --- a/mss/tests/test_tools.py +++ b/mss/tests/test_tools.py @@ -8,6 +8,7 @@ import pytest +from mss import mss from mss.tools import to_png WIDTH = 10 @@ -15,20 +16,19 @@ MD5SUM = "055e615b74167c9bdfea16a00539450c" -def test_bad_compression_level(sct): - sct.compression_level = 42 - try: +def test_bad_compression_level(): + with mss(compression_level=42, display=os.getenv("DISPLAY")) as sct: with pytest.raises(zlib.error): sct.shot() - finally: - sct.compression_level = 6 -def test_compression_level(sct): +def test_compression_level(): data = b"rgb" * WIDTH * HEIGHT output = f"{WIDTH}x{HEIGHT}.png" - to_png(data, (WIDTH, HEIGHT), level=sct.compression_level, output=output) + with mss(display=os.getenv("DISPLAY")) as sct: + to_png(data, (WIDTH, HEIGHT), level=sct.compression_level, output=output) + with open(output, "rb") as png: assert hashlib.md5(png.read()).hexdigest() == MD5SUM diff --git a/mss/tools.py b/mss/tools.py index 47fd74ea..ffe1f3d1 100644 --- a/mss/tools.py +++ b/mss/tools.py @@ -9,9 +9,7 @@ from typing import Optional, Tuple -def to_png( - data: bytes, size: Tuple[int, int], level: int = 6, output: Optional[str] = None -) -> Optional[bytes]: +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 the whole PNG data. @@ -29,9 +27,7 @@ def to_png( width, height = size line = width * 3 png_filter = pack(">B", 0) - scanlines = b"".join( - [png_filter + data[y * line : y * line + line] for y in range(height)] - ) + scanlines = b"".join([png_filter + data[y * line : y * line + line] for y in range(height)]) magic = pack(">8B", 137, 80, 78, 71, 13, 10, 26, 10) diff --git a/mss/windows.py b/mss/windows.py index 04f26e67..0172a056 100644 --- a/mss/windows.py +++ b/mss/windows.py @@ -104,10 +104,10 @@ class MSS(MSSBase): # A dict to maintain *srcdc* values created by multiple threads. _srcdc_dict: Dict[threading.Thread, int] = {} - def __init__(self, **_: Any) -> None: + def __init__(self, **kwargs: Any) -> None: """Windows initialisations.""" - super().__init__() + super().__init__(**kwargs) self.user32 = ctypes.WinDLL("user32") self.gdi32 = ctypes.WinDLL("gdi32") @@ -170,9 +170,7 @@ def _get_srcdc(self) -> int: Since the current thread and main thread are always alive, reuse their *srcdc* value first. """ cur_thread, main_thread = threading.current_thread(), threading.main_thread() - current_srcdc = MSS._srcdc_dict.get(cur_thread) or MSS._srcdc_dict.get( - main_thread - ) + current_srcdc = MSS._srcdc_dict.get(cur_thread) or MSS._srcdc_dict.get(main_thread) if current_srcdc: srcdc = current_srcdc else: @@ -275,9 +273,7 @@ def _grab_impl(self, monitor: Monitor) -> ScreenShot: monitor["top"], SRCCOPY | CAPTUREBLT, ) - bits = self.gdi32.GetDIBits( - memdc, MSS.bmp, 0, height, self._data, self._bmi, DIB_RGB_COLORS - ) + bits = self.gdi32.GetDIBits(memdc, MSS.bmp, 0, height, self._data, self._bmi, DIB_RGB_COLORS) if bits != height: raise ScreenShotError("gdi32.GetDIBits() failed.") diff --git a/setup.cfg b/setup.cfg index cace210f..613a73e5 100644 --- a/setup.cfg +++ b/setup.cfg @@ -44,6 +44,10 @@ python_requires = >=3.7 console_scripts = mss = mss.__main__:main +[coverage:run] +omit = + mss/tests/* + [flake8] ignore = # E203 whitespace before ':', but E203 is not PEP 8 compliant @@ -57,13 +61,12 @@ multi_line_output = 3 include_trailing_comma = True force_grid_wrap = 0 use_parentheses = True -line_length = 88 +line_length = 120 [tool:pytest] addopts = --showlocals --strict-markers - --failed-first -r fE -v --cov=mss From e2c51360acd94392a0ddb80e8a0ebb031a269f77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Sat, 8 Apr 2023 16:09:37 +0200 Subject: [PATCH 079/280] doc: more details on the Linux API --- docs/source/api.rst | 18 ++++++++- mss/linux.py | 97 +++++++++++++++++++++++---------------------- 2 files changed, 65 insertions(+), 50 deletions(-) diff --git a/docs/source/api.rst b/docs/source/api.rst index c4fe1026..73b8930c 100644 --- a/docs/source/api.rst +++ b/docs/source/api.rst @@ -43,19 +43,33 @@ GNU/Linux .. class:: Display + Structure that serves as the connection to the X server, and that contains all the information about that X server. + .. class:: Event + XErrorEvent to debug eventual errors. + .. class:: XFixesCursorImage -.. class:: XWindowAttributes + Cursor structure .. class:: XImage + Description of an image as it exists in the client's memory. + +.. class:: XRRCrtcInfo + + Structure that contains CRTC information. + .. class:: XRRModeInfo .. class:: XRRScreenResources -.. class:: XRRCrtcInfo + Structure that contains arrays of XIDs that point to the available outputs and associated CRTCs. + +.. class:: XWindowAttributes + + Attributes for the specified window. .. class:: MSS diff --git a/mss/linux.py b/mss/linux.py index 4daa234f..640a4518 100644 --- a/mss/linux.py +++ b/mss/linux.py @@ -66,8 +66,9 @@ class Event(Structure): class XFixesCursorImage(Structure): """ - XFixes is an X Window System extension. - See /usr/include/X11/extensions/Xfixes.h + 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 """ _fields_ = [ @@ -84,36 +85,6 @@ class XFixesCursorImage(Structure): ] -class XWindowAttributes(Structure): - """Attributes for the specified window.""" - - _fields_ = [ - ("x", c_int32), - ("y", c_int32), - ("width", c_int32), - ("height", c_int32), - ("border_width", c_int32), - ("depth", c_int32), - ("visual", c_ulong), - ("root", c_ulong), - ("class", c_int32), - ("bit_gravity", c_int32), - ("win_gravity", c_int32), - ("backing_store", c_int32), - ("backing_planes", c_ulong), - ("backing_pixel", c_ulong), - ("save_under", c_int32), - ("colourmap", c_ulong), - ("mapinstalled", c_uint32), - ("map_state", c_uint32), - ("all_event_masks", c_ulong), - ("your_event_mask", c_ulong), - ("do_not_propagate_mask", c_ulong), - ("override_redirect", c_int32), - ("screen", c_ulong), - ] - - class XImage(Structure): """ Description of an image as it exists in the client's memory. @@ -139,6 +110,25 @@ class XImage(Structure): ] +class XRRCrtcInfo(Structure): + """Structure that contains CRTC information.""" + + _fields_ = [ + ("timestamp", c_ulong), + ("x", c_int), + ("y", c_int), + ("width", c_int), + ("height", c_int), + ("mode", c_long), + ("rotation", c_int), + ("noutput", c_int), + ("outputs", POINTER(c_long)), + ("rotations", c_ushort), + ("npossible", c_int), + ("possible", POINTER(c_long)), + ] + + class XRRModeInfo(Structure): """Voilà, voilà.""" @@ -161,22 +151,33 @@ class XRRScreenResources(Structure): ] -class XRRCrtcInfo(Structure): - """Structure that contains CRTC information.""" +class XWindowAttributes(Structure): + """Attributes for the specified window.""" _fields_ = [ - ("timestamp", c_ulong), - ("x", c_int), - ("y", c_int), - ("width", c_int), - ("height", c_int), - ("mode", c_long), - ("rotation", c_int), - ("noutput", c_int), - ("outputs", POINTER(c_long)), - ("rotations", c_ushort), - ("npossible", c_int), - ("possible", POINTER(c_long)), + ("x", c_int32), + ("y", c_int32), + ("width", c_int32), + ("height", c_int32), + ("border_width", c_int32), + ("depth", c_int32), + ("visual", c_ulong), + ("root", c_ulong), + ("class", c_int32), + ("bit_gravity", c_int32), + ("win_gravity", c_int32), + ("backing_store", c_int32), + ("backing_planes", c_ulong), + ("backing_pixel", c_ulong), + ("save_under", c_int32), + ("colourmap", c_ulong), + ("mapinstalled", c_uint32), + ("map_state", c_uint32), + ("all_event_masks", c_ulong), + ("your_event_mask", c_ulong), + ("do_not_propagate_mask", c_ulong), + ("override_redirect", c_int32), + ("screen", c_ulong), ] @@ -226,7 +227,7 @@ def _validate(retval: int, func: Any, args: Tuple[Any, Any]) -> Tuple[Any, Any]: # This is a dict: # cfunction: (attr, argtypes, restype) # -# Available attr: xlib, xrandr. +# Available attr: xfixes, xlib, xrandr. # # Note: keep it sorted by cfunction. CFUNCTIONS: CFunctions = { @@ -292,7 +293,7 @@ class MSS(MSSBase): It uses intensively the Xlib and its Xrandr extension. """ - __slots__ = {"xlib", "xrandr", "xfixes", "_handles"} + __slots__ = {"xfixes", "xlib", "xrandr", "_handles"} def __init__(self, **kwargs: Any) -> None: """GNU/Linux initialisations.""" From 1c18df3200c6fe7d982a8cdab10394cd561371a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Sat, 8 Apr 2023 16:32:27 +0200 Subject: [PATCH 080/280] refactor!: the whole source code was migrated to PEP 570 (Python Positional-Only Parameters) Dropped Python 3.7 support at the same time. --- .github/workflows/tests.yml | 2 -- CHANGELOG | 2 ++ README.rst | 2 +- docs/source/index.rst | 2 +- docs/source/support.rst | 5 +-- mss/__main__.py | 4 +-- mss/base.py | 18 ++++++----- mss/darwin.py | 23 +++----------- mss/exception.py | 2 +- mss/linux.py | 63 +++++++------------------------------ mss/screenshot.py | 9 +++--- mss/tools.py | 2 +- mss/windows.py | 17 +++------- setup.cfg | 3 +- 14 files changed, 47 insertions(+), 107 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 0869d70c..bb129792 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -56,8 +56,6 @@ jobs: - emoji: 🪟 runs-on: [windows-latest] python: - - name: CPython 3.7 - runs-on: "3.7" - name: CPython 3.8 runs-on: "3.8" - name: CPython 3.9 diff --git a/CHANGELOG b/CHANGELOG index 26e21868..8b3bd083 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -3,6 +3,8 @@ History: 8.0.0 2023/0x/xx + - the whole source code was migrated to PEP 570 (Python Positional-Only Parameters) + - removed support for Python 3.7 - Linux: added mouse support (#232) - Linux: refactored how internal handles are stored to fix issues with multiple X servers, and TKinter. No more side effects, and when leaving the context manager, resources are all freed (#224, #234) diff --git a/README.rst b/README.rst index f88e47ac..86d85ecd 100644 --- a/README.rst +++ b/README.rst @@ -22,7 +22,7 @@ Python MSS An ultra fast cross-platform multiple screenshots module in pure python using ctypes. -- **Python 3.7+** and PEP8 compliant, no dependency, thread-safe; +- **Python 3.8+** and PEP8 compliant, no dependency, thread-safe; - very basic, it will grab one screen shot by monitor or a screen shot of all monitors and save it to a PNG file; - but you can use PIL and benefit from all its formats (or add yours directly); - integrate well with Numpy and OpenCV; diff --git a/docs/source/index.rst b/docs/source/index.rst index 6d87e645..c3f33ffd 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -12,7 +12,7 @@ Welcome to Python MSS's documentation! An ultra fast cross-platform multiple screenshots module in pure python using ctypes. - - **Python 3.7+** and :pep:`8` compliant, no dependency, thread-safe; + - **Python 3.8+** and :pep:`8` compliant, no dependency, thread-safe; - very basic, it will grab one screen shot by monitor or a screen shot of all monitors and save it to a PNG file; - but you can use PIL and benefit from all its formats (or add yours directly); - integrate well with Numpy and OpenCV; diff --git a/docs/source/support.rst b/docs/source/support.rst index 3cef168e..330f9438 100644 --- a/docs/source/support.rst +++ b/docs/source/support.rst @@ -5,7 +5,7 @@ Support Feel free to try MSS on a system we had not tested, and let us know by creating an `issue `_. - OS: GNU/Linux, macOS and Windows - - Python: 3.7 and newer + - Python: 3.8 and newer Future @@ -32,4 +32,5 @@ Abandoned - Python 3.3 (2017-12-05) - Python 3.4 (2018-03-19) - Python 3.5 (2022-10-27) -- Python 3.6 (202x-xx-xx) +- Python 3.6 (2022-10-27) +- Python 3.7 (2023-xx-xx) diff --git a/mss/__main__.py b/mss/__main__.py index 1aa41ad9..52ef3017 100644 --- a/mss/__main__.py +++ b/mss/__main__.py @@ -4,7 +4,7 @@ """ import os.path from argparse import ArgumentParser -from typing import List, Optional +from typing import List from . import __version__ from .exception import ScreenShotError @@ -12,7 +12,7 @@ from .tools import to_png -def main(args: Optional[List[str]] = None) -> int: +def main(args: List[str], /) -> int: """Main logic.""" cli_args = ArgumentParser() diff --git a/mss/base.py b/mss/base.py index 3dd6a55d..11c39509 100644 --- a/mss/base.py +++ b/mss/base.py @@ -22,6 +22,8 @@ class MSSBase(metaclass=ABCMeta): def __init__( self, + /, + *, compression_level: int = 6, display: Optional[Union[bytes, str]] = None, # Linux only max_displays: int = 32, # Mac only @@ -48,7 +50,7 @@ def _cursor_impl(self) -> Optional[ScreenShot]: """Retrieve all cursor data. Pixels have to be RGB.""" @abstractmethod - def _grab_impl(self, monitor: Monitor) -> ScreenShot: + def _grab_impl(self, monitor: Monitor, /) -> ScreenShot: """ Retrieve all pixels from a monitor. Pixels have to be RGB. That method has to be run using a threading lock. @@ -64,7 +66,7 @@ def _monitors_impl(self) -> None: def close(self) -> None: """Clean-up.""" - def grab(self, monitor: Union[Monitor, Tuple[int, int, int, int]]) -> ScreenShot: + def grab(self, monitor: Union[Monitor, Tuple[int, int, int, int]], /) -> ScreenShot: """ Retrieve screen pixels for a given monitor. @@ -120,6 +122,8 @@ def monitors(self) -> Monitors: def save( self, + /, + *, mon: int = 0, output: str = "monitor-{mon}.png", callback: Optional[Callable[[str], None]] = None, @@ -170,9 +174,8 @@ def save( mon = 0 if mon == -1 else mon try: monitor = monitors[mon] - except IndexError: - # pylint: disable=raise-missing-from - raise ScreenShotError(f"Monitor {mon!r} does not exist.") + except IndexError as exc: + raise ScreenShotError(f"Monitor {mon!r} does not exist.") from exc output = output.format(mon=mon, date=datetime.now(), **monitor) if callable(callback): @@ -181,7 +184,7 @@ def save( to_png(sct.rgb, sct.size, level=self.compression_level, output=output) yield output - def shot(self, **kwargs: Any) -> str: + def shot(self, /, **kwargs: Any) -> str: """ Helper to save the screen shot of the 1st monitor, by default. You can pass the same arguments as for ``save``. @@ -191,7 +194,7 @@ def shot(self, **kwargs: Any) -> str: return next(self.save(**kwargs)) @staticmethod - def _merge(screenshot: ScreenShot, cursor: ScreenShot) -> ScreenShot: + def _merge(screenshot: ScreenShot, cursor: ScreenShot, /) -> ScreenShot: """Create composite image by blending screenshot and mouse cursor.""" # pylint: disable=too-many-locals,invalid-name @@ -244,6 +247,7 @@ def _cfactory( func: str, argtypes: List[Any], restype: Any, + /, errcheck: Optional[Callable] = None, ) -> None: """Factory to create a ctypes function and automatically manage errors.""" diff --git a/mss/darwin.py b/mss/darwin.py index a4ba900b..a6661f68 100644 --- a/mss/darwin.py +++ b/mss/darwin.py @@ -66,11 +66,7 @@ def __repr__(self) -> str: "CFDataGetLength": ("core", [c_void_p], c_uint64), "CFRelease": ("core", [c_void_p], c_void_p), "CGDataProviderRelease": ("core", [c_void_p], c_void_p), - "CGGetActiveDisplayList": ( - "core", - [c_uint32, POINTER(c_uint32), POINTER(c_uint32)], - c_int32, - ), + "CGGetActiveDisplayList": ("core", [c_uint32, POINTER(c_uint32), POINTER(c_uint32)], c_int32), "CGImageGetBitsPerPixel": ("core", [c_void_p], int), "CGImageGetBytesPerRow": ("core", [c_void_p], int), "CGImageGetDataProvider": ("core", [c_void_p], c_void_p), @@ -78,11 +74,7 @@ def __repr__(self) -> str: "CGImageGetWidth": ("core", [c_void_p], int), "CGRectStandardize": ("core", [CGRect], CGRect), "CGRectUnion": ("core", [CGRect, CGRect], CGRect), - "CGWindowListCreateImage": ( - "core", - [CGRect, c_uint32, c_uint32, c_uint32], - c_void_p, - ), + "CGWindowListCreateImage": ("core", [CGRect, c_uint32, c_uint32, c_uint32], c_void_p), } @@ -94,7 +86,7 @@ class MSS(MSSBase): __slots__ = {"core", "max_displays"} - def __init__(self, **kwargs: Any) -> None: + def __init__(self, /, **kwargs: Any) -> None: """macOS initialisations.""" super().__init__(**kwargs) @@ -124,12 +116,7 @@ def _set_cfunctions(self) -> None: cfactory = self._cfactory attrs = {"core": self.core} for func, (attr, argtypes, restype) in CFUNCTIONS.items(): - cfactory( - attr=attrs[attr], - func=func, - argtypes=argtypes, - restype=restype, - ) + cfactory(attrs[attr], func, argtypes, restype) def _monitors_impl(self) -> None: """Get positions of monitors. It will populate self._monitors.""" @@ -176,7 +163,7 @@ def _monitors_impl(self) -> None: "height": int_(all_monitors.size.height), } - def _grab_impl(self, monitor: Monitor) -> ScreenShot: + def _grab_impl(self, monitor: Monitor, /) -> ScreenShot: """Retrieve all pixels from a monitor. Pixels have to be RGB.""" # pylint: disable=too-many-locals diff --git a/mss/exception.py b/mss/exception.py index 0d297b33..9ffb94b6 100644 --- a/mss/exception.py +++ b/mss/exception.py @@ -8,6 +8,6 @@ class ScreenShotError(Exception): """Error handling class.""" - def __init__(self, message: str, details: Optional[Dict[str, Any]] = None) -> None: + def __init__(self, message: str, /, *, details: Optional[Dict[str, Any]] = None) -> None: super().__init__(message) self.details = details or {} diff --git a/mss/linux.py b/mss/linux.py index 640a4518..cfcaebb7 100644 --- a/mss/linux.py +++ b/mss/linux.py @@ -210,7 +210,7 @@ def _error_handler(display: Display, event: Event) -> int: return 0 -def _validate(retval: int, func: Any, args: Tuple[Any, Any]) -> Tuple[Any, Any]: +def _validate(retval: int, func: Any, args: Tuple[Any, Any], /) -> Tuple[Any, Any]: """Validate the returned value of a Xlib or XRANDR function.""" thread = current_thread() @@ -237,52 +237,17 @@ def _validate(retval: int, func: Any, args: Tuple[Any, Any]) -> Tuple[Any, Any]: "XFixesGetCursorImage": ("xfixes", [POINTER(Display)], POINTER(XFixesCursorImage)), "XGetImage": ( "xlib", - [ - POINTER(Display), - POINTER(Display), - c_int, - c_int, - c_uint, - c_uint, - c_ulong, - c_int, - ], + [POINTER(Display), POINTER(Display), c_int, c_int, c_uint, c_uint, c_ulong, c_int], POINTER(XImage), ), - "XGetWindowAttributes": ( - "xlib", - [POINTER(Display), POINTER(XWindowAttributes), POINTER(XWindowAttributes)], - c_int, - ), + "XGetWindowAttributes": ("xlib", [POINTER(Display), POINTER(XWindowAttributes), POINTER(XWindowAttributes)], c_int), "XOpenDisplay": ("xlib", [c_char_p], POINTER(Display)), - "XQueryExtension": ( - "xlib", - [ - POINTER(Display), - c_char_p, - POINTER(c_int), - POINTER(c_int), - POINTER(c_int), - ], - c_uint, - ), + "XQueryExtension": ("xlib", [POINTER(Display), c_char_p, POINTER(c_int), POINTER(c_int), POINTER(c_int)], c_uint), "XRRFreeCrtcInfo": ("xrandr", [POINTER(XRRCrtcInfo)], c_void_p), "XRRFreeScreenResources": ("xrandr", [POINTER(XRRScreenResources)], c_void_p), - "XRRGetCrtcInfo": ( - "xrandr", - [POINTER(Display), POINTER(XRRScreenResources), c_long], - POINTER(XRRCrtcInfo), - ), - "XRRGetScreenResources": ( - "xrandr", - [POINTER(Display), POINTER(Display)], - POINTER(XRRScreenResources), - ), - "XRRGetScreenResourcesCurrent": ( - "xrandr", - [POINTER(Display), POINTER(Display)], - POINTER(XRRScreenResources), - ), + "XRRGetCrtcInfo": ("xrandr", [POINTER(Display), POINTER(XRRScreenResources), c_long], POINTER(XRRCrtcInfo)), + "XRRGetScreenResources": ("xrandr", [POINTER(Display), POINTER(Display)], POINTER(XRRScreenResources)), + "XRRGetScreenResourcesCurrent": ("xrandr", [POINTER(Display), POINTER(Display)], POINTER(XRRScreenResources)), "XSetErrorHandler": ("xlib", [c_void_p], c_int), } @@ -295,7 +260,7 @@ class MSS(MSSBase): __slots__ = {"xfixes", "xlib", "xrandr", "_handles"} - def __init__(self, **kwargs: Any) -> None: + def __init__(self, /, **kwargs: Any) -> None: """GNU/Linux initialisations.""" super().__init__(**kwargs) @@ -355,7 +320,7 @@ def close(self) -> None: _ERROR.clear() - def _is_extension_enabled(self, name: str) -> bool: + def _is_extension_enabled(self, name: str, /) -> bool: """Return True if the given *extension* is enabled on the server.""" with lock: major_opcode_return = c_int() @@ -385,13 +350,7 @@ def _set_cfunctions(self) -> None: } for func, (attr, argtypes, restype) in CFUNCTIONS.items(): with suppress(AttributeError): - cfactory( - attr=attrs[attr], - errcheck=_validate, - func=func, - argtypes=argtypes, - restype=restype, - ) + cfactory(attrs[attr], func, argtypes, restype, errcheck=_validate) def _monitors_impl(self) -> None: """Get positions of monitors. It will populate self._monitors.""" @@ -435,7 +394,7 @@ def _monitors_impl(self) -> None: xrandr.XRRFreeCrtcInfo(crtc) xrandr.XRRFreeScreenResources(mon) - def _grab_impl(self, monitor: Monitor) -> ScreenShot: + def _grab_impl(self, monitor: Monitor, /) -> ScreenShot: """Retrieve all pixels from a monitor. Pixels have to be RGB.""" ximage = self.xlib.XGetImage( diff --git a/mss/screenshot.py b/mss/screenshot.py index 94bedfc1..9c82d726 100644 --- a/mss/screenshot.py +++ b/mss/screenshot.py @@ -21,7 +21,7 @@ class ScreenShot: __slots__ = {"__pixels", "__rgb", "pos", "raw", "size"} - def __init__(self, data: bytearray, monitor: Monitor, size: Optional[Size] = None) -> None: + def __init__(self, data: bytearray, monitor: Monitor, /, *, size: Optional[Size] = None) -> None: self.__pixels: Optional[Pixels] = None self.__rgb: Optional[bytes] = None @@ -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) @@ -126,6 +126,5 @@ def pixel(self, coord_x: int, coord_y: int) -> Pixel: try: return self.pixels[coord_y][coord_x] # type: ignore - except IndexError: - # pylint: disable=raise-missing-from - raise ScreenShotError(f"Pixel location ({coord_x}, {coord_y}) is out of range.") + except IndexError as exc: + raise ScreenShotError(f"Pixel location ({coord_x}, {coord_y}) is out of range.") from exc diff --git a/mss/tools.py b/mss/tools.py index ffe1f3d1..de3a1afb 100644 --- a/mss/tools.py +++ b/mss/tools.py @@ -9,7 +9,7 @@ from typing import Optional, Tuple -def to_png(data: bytes, size: Tuple[int, int], level: int = 6, output: Optional[str] = None) -> Optional[bytes]: +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 the whole PNG data. diff --git a/mss/windows.py b/mss/windows.py index 0172a056..00c380e3 100644 --- a/mss/windows.py +++ b/mss/windows.py @@ -81,11 +81,7 @@ class BITMAPINFO(Structure): "DeleteObject": ("gdi32", [HGDIOBJ], INT), "EnumDisplayMonitors": ("user32", [HDC, c_void_p, MONITORNUMPROC, LPARAM], BOOL), "GetDeviceCaps": ("gdi32", [HWND, INT], INT), - "GetDIBits": ( - "gdi32", - [HDC, HBITMAP, UINT, UINT, c_void_p, POINTER(BITMAPINFO), UINT], - BOOL, - ), + "GetDIBits": ("gdi32", [HDC, HBITMAP, UINT, UINT, c_void_p, POINTER(BITMAPINFO), UINT], BOOL), "GetSystemMetrics": ("user32", [INT], INT), "GetWindowDC": ("user32", [HWND], HDC), "SelectObject": ("gdi32", [HDC, HGDIOBJ], HGDIOBJ), @@ -104,7 +100,7 @@ class MSS(MSSBase): # A dict to maintain *srcdc* values created by multiple threads. _srcdc_dict: Dict[threading.Thread, int] = {} - def __init__(self, **kwargs: Any) -> None: + def __init__(self, /, **kwargs: Any) -> None: """Windows initialisations.""" super().__init__(**kwargs) @@ -139,12 +135,7 @@ def _set_cfunctions(self) -> None: "user32": self.user32, } for func, (attr, argtypes, restype) in CFUNCTIONS.items(): - cfactory( - attr=attrs[attr], - func=func, - argtypes=argtypes, - restype=restype, - ) + cfactory(attrs[attr], func, argtypes, restype) def _set_dpi_awareness(self) -> None: """Set DPI awareness to capture full screen on Hi-DPI monitors.""" @@ -217,7 +208,7 @@ def _callback(monitor: int, data: HDC, rect: LPRECT, dc_: LPARAM) -> int: callback = MONITORNUMPROC(_callback) user32.EnumDisplayMonitors(0, 0, callback, 0) - def _grab_impl(self, monitor: Monitor) -> ScreenShot: + def _grab_impl(self, monitor: Monitor, /) -> ScreenShot: """ Retrieve all pixels from a monitor. Pixels have to be RGB. diff --git a/setup.cfg b/setup.cfg index 613a73e5..fb03cd3e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -25,7 +25,6 @@ classifiers = Programming Language :: Python Programming Language :: Python :: 3 Programming Language :: Python :: 3 :: Only - Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 Programming Language :: Python :: 3.10 @@ -38,7 +37,7 @@ zip_safe = False include_package_data = True packages_dir = mss packages = find: -python_requires = >=3.7 +python_requires = >=3.8 [options.entry_points] console_scripts = From bfabc4d2fc404e22093e26833f15ab7ccd7e1cb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Sun, 9 Apr 2023 00:07:26 +0200 Subject: [PATCH 081/280] fix(Linux): reset X server error handler on exit to prevent issues with Tk/Tkinter --- CHANGELOG | 1 + mss/linux.py | 43 +++++++++++++++++++++++--------- mss/tests/test_gnu_linux.py | 49 +++++++++---------------------------- mss/tests/test_issue_220.py | 47 +++++++++++++++++++++++++++++++++++ 4 files changed, 92 insertions(+), 48 deletions(-) create mode 100644 mss/tests/test_issue_220.py diff --git a/CHANGELOG b/CHANGELOG index 8b3bd083..6657c510 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -5,6 +5,7 @@ History: 8.0.0 2023/0x/xx - the whole source code was migrated to PEP 570 (Python Positional-Only Parameters) - removed support for Python 3.7 + - Linux: reset X server error handler on exit to prevent issues with Tk/Tkinter (#235) - Linux: added mouse support (#232) - Linux: refactored how internal handles are stored to fix issues with multiple X servers, and TKinter. No more side effects, and when leaving the context manager, resources are all freed (#224, #234) diff --git a/mss/linux.py b/mss/linux.py index cfcaebb7..841ea1f1 100644 --- a/mss/linux.py +++ b/mss/linux.py @@ -182,6 +182,24 @@ class XWindowAttributes(Structure): _ERROR = {} +_X11 = find_library("X11") +_XFIXES = find_library("Xfixes") +_XRANDR = find_library("Xrandr") + + +@CFUNCTYPE(c_int, POINTER(Display), POINTER(Event)) +def _default_error_handler(display: Display, event: Event) -> int: + """ + Specifies the default program's supplied error handler. + It's useful when exiting MSS to prevent letting `_error_handler()` as default handler. + Doing so would crash when using Tk/Tkinter, see issue #220. + + Interesting technical stuff can be found here: + https://core.tcl-lang.org/tk/file?name=generic/tkError.c&ci=a527ef995862cb50 + https://github.com/tcltk/tk/blob/b9cdafd83fe77499ff47fa373ce037aff3ae286a/generic/tkError.c + """ + # pylint: disable=unused-argument + return 0 # pragma: nocover @CFUNCTYPE(c_int, POINTER(Display), POINTER(Event)) @@ -189,7 +207,7 @@ def _error_handler(display: Display, event: Event) -> int: """Specifies the program's supplied error handler.""" # Get the specific error message - xlib = cdll.LoadLibrary(find_library("X11")) # type: ignore[arg-type] + xlib = cdll.LoadLibrary(_X11) # type: ignore[arg-type] get_error = xlib.XGetErrorText get_error.argtypes = [POINTER(Display), c_int, c_char_p, c_int] get_error.restype = c_void_p @@ -278,24 +296,21 @@ def __init__(self, /, **kwargs: Any) -> None: if b":" not in display: raise ScreenShotError(f"Bad display value: {display!r}.") - x11 = find_library("X11") - if not x11: + if not _X11: raise ScreenShotError("No X11 library found.") - self.xlib = cdll.LoadLibrary(x11) + self.xlib = cdll.LoadLibrary(_X11) # Install the error handler to prevent interpreter crashes: # any error will raise a ScreenShotError exception. self.xlib.XSetErrorHandler(_error_handler) - xrandr = find_library("Xrandr") - if not xrandr: + if not _XRANDR: raise ScreenShotError("No Xrandr extension found.") - self.xrandr = cdll.LoadLibrary(xrandr) + self.xrandr = cdll.LoadLibrary(_XRANDR) if self.with_cursor: - xfixes = find_library("Xfixes") - if xfixes: - self.xfixes = cdll.LoadLibrary(xfixes) + if _XFIXES: + self.xfixes = cdll.LoadLibrary(_XFIXES) else: self.with_cursor = False @@ -314,10 +329,15 @@ def __init__(self, /, **kwargs: Any) -> None: self._handles.drawable = cast(self._handles.root, POINTER(Display)) def close(self) -> None: + # Remove our error handler + self.xlib.XSetErrorHandler(_default_error_handler) + + # Clean-up if self._handles.display is not None: self.xlib.XCloseDisplay(self._handles.display) self._handles.display = None + # Also empty the error dict _ERROR.clear() def _is_extension_enabled(self, name: str, /) -> bool: @@ -350,7 +370,8 @@ def _set_cfunctions(self) -> None: } for func, (attr, argtypes, restype) in CFUNCTIONS.items(): with suppress(AttributeError): - cfactory(attrs[attr], func, argtypes, restype, errcheck=_validate) + errcheck = None if func == "XSetErrorHandler" else _validate + cfactory(attrs[attr], func, argtypes, restype, errcheck=errcheck) def _monitors_impl(self) -> None: """Get positions of monitors. It will populate self._monitors.""" diff --git a/mss/tests/test_gnu_linux.py b/mss/tests/test_gnu_linux.py index 6a86ce5f..2c8fc6c3 100644 --- a/mss/tests/test_gnu_linux.py +++ b/mss/tests/test_gnu_linux.py @@ -2,7 +2,6 @@ This is part of the MSS Python's module. Source: https://github.com/BoboTiG/python-mss """ -import ctypes.util import platform from unittest.mock import Mock, patch @@ -88,30 +87,18 @@ def test_bad_display_structure(monkeypatch): pass +@patch("mss.linux._X11", new=None) def test_no_xlib_library(): - with patch("mss.linux.find_library", return_value=None): - with pytest.raises(ScreenShotError): - with mss.mss(): - pass + with pytest.raises(ScreenShotError): + with mss.mss(): + pass +@patch("mss.linux._XRANDR", new=None) def test_no_xrandr_extension(): - x11 = ctypes.util.find_library("X11") - - def find_lib_mocked(lib): - """ - Returns None to emulate no XRANDR library. - Returns the previous found X11 library else. - - It is a naive approach, but works for now. - """ - - return None if lib == "Xrandr" else x11 - - # No `Xrandr` library - with patch("mss.linux.find_library", find_lib_mocked): - with pytest.raises(ScreenShotError): - mss.mss() + with pytest.raises(ScreenShotError): + with mss.mss(): + pass @patch("mss.linux.MSS._is_extension_enabled", new=Mock(return_value=False)) @@ -190,23 +177,11 @@ def test_with_cursor(display: str): assert set(screenshot_with_cursor.rgb) == {0, 255} +@patch("mss.linux._XFIXES", new=None) def test_with_cursor_but_not_xfixes_extension_found(display: str): - x11 = ctypes.util.find_library("X11") - - def find_lib_mocked(lib): - """ - Returns None to emulate no XRANDR library. - Returns the previous found X11 library else. - - It is a naive approach, but works for now. - """ - - return None if lib == "Xfixes" else x11 - - with patch("mss.linux.find_library", find_lib_mocked): - with mss.mss(display=display, with_cursor=True) as sct: - assert not hasattr(sct, "xfixes") - assert not sct.with_cursor + 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): diff --git a/mss/tests/test_issue_220.py b/mss/tests/test_issue_220.py new file mode 100644 index 00000000..0d5f143b --- /dev/null +++ b/mss/tests/test_issue_220.py @@ -0,0 +1,47 @@ +""" +This is part of the MSS Python's module. +Source: https://github.com/BoboTiG/python-mss +""" +import pytest + +import mss + +tkinter = pytest.importorskip("tkinter") +root = tkinter.Tk() + + +def take_screenshot(): + region = {"top": 370, "left": 1090, "width": 80, "height": 390} + with mss.mss() as sct: + sct.grab(region) + + +def create_top_level_win(): + top_level_win = tkinter.Toplevel(root) + + take_screenshot_btn = tkinter.Button(top_level_win, text="Take screenshot", command=take_screenshot) + take_screenshot_btn.pack() + + take_screenshot_btn.invoke() + root.update_idletasks() + root.update() + + top_level_win.destroy() + root.update_idletasks() + root.update() + + +def test_regression(capsys): + btn = tkinter.Button(root, text="Open TopLevel", command=create_top_level_win) + btn.pack() + + # First screenshot: it works + btn.invoke() + + # Second screenshot: it should work too + btn.invoke() + + # Check there were no exceptions + captured = capsys.readouterr() + assert not captured.out + assert not captured.err From 0ae0e82a57af9cc407f88130c149dd14dacce04e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Sun, 9 Apr 2023 01:06:56 +0200 Subject: [PATCH 082/280] test: improve #220 test case --- mss/linux.py | 2 +- mss/tests/test_issue_220.py | 26 +++++++++++++++++--------- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/mss/linux.py b/mss/linux.py index 841ea1f1..0e743011 100644 --- a/mss/linux.py +++ b/mss/linux.py @@ -364,9 +364,9 @@ def _set_cfunctions(self) -> None: cfactory = self._cfactory attrs = { + "xfixes": getattr(self, "xfixes", None), "xlib": self.xlib, "xrandr": self.xrandr, - "xfixes": getattr(self, "xfixes", None), } for func, (attr, argtypes, restype) in CFUNCTIONS.items(): with suppress(AttributeError): diff --git a/mss/tests/test_issue_220.py b/mss/tests/test_issue_220.py index 0d5f143b..6ad1de01 100644 --- a/mss/tests/test_issue_220.py +++ b/mss/tests/test_issue_220.py @@ -7,7 +7,15 @@ import mss tkinter = pytest.importorskip("tkinter") -root = tkinter.Tk() + + +@pytest.fixture +def root() -> tkinter.Tk: + master = tkinter.Tk() + try: + yield master + finally: + master.destroy() def take_screenshot(): @@ -16,23 +24,23 @@ def take_screenshot(): sct.grab(region) -def create_top_level_win(): - top_level_win = tkinter.Toplevel(root) +def create_top_level_win(master: tkinter.Tk): + top_level_win = tkinter.Toplevel(master) take_screenshot_btn = tkinter.Button(top_level_win, text="Take screenshot", command=take_screenshot) take_screenshot_btn.pack() take_screenshot_btn.invoke() - root.update_idletasks() - root.update() + master.update_idletasks() + master.update() top_level_win.destroy() - root.update_idletasks() - root.update() + master.update_idletasks() + master.update() -def test_regression(capsys): - btn = tkinter.Button(root, text="Open TopLevel", command=create_top_level_win) +def test_regression(root: tkinter.Tk, capsys): + btn = tkinter.Button(root, text="Open TopLevel", command=lambda: create_top_level_win(root)) btn.pack() # First screenshot: it works From 6723c091b651414c8725a41abd1158296263348e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Sun, 9 Apr 2023 18:48:34 +0200 Subject: [PATCH 083/280] doc: tweak --- CHANGELOG | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 6657c510..bde48337 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -3,16 +3,14 @@ History: 8.0.0 2023/0x/xx - - the whole source code was migrated to PEP 570 (Python Positional-Only Parameters) + - the whole source code was migrated to PEP 570 (Python positional-only parameters) - removed support for Python 3.7 - - Linux: reset X server error handler on exit to prevent issues with Tk/Tkinter (#235) - - Linux: added mouse support (#232) - - Linux: refactored how internal handles are stored to fix issues with multiple X servers, and TKinter. - No more side effects, and when leaving the context manager, resources are all freed (#224, #234) - - ci: added PyPy 3.9 (#226) - - dev: removed pre-commit (#226) - - tests: removed tox (#226) - - tests: improved coverage (#234) + - Linux: added mouse support (related to #55) + - Linux: reset the X server error handler on exit to prevent issues with Tk/Tkinter (fixes #220) + - Linux: added mouse support (related to #55) + - Linux: refactored how internal handles are stored to fix issues with multiple X servers (fixes #210) + - Linux: removed side effects when leaving the context manager, resources are all freed (fixes #210) + - tests: added PyPy 3.9, removed tox, and improved GNU/Linux coverage 7.0.1 2022/10/27 - fixed the wheel package From fda2a6eaea5923a046aa8911695514e9fb17662f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Sun, 9 Apr 2023 18:49:53 +0200 Subject: [PATCH 084/280] CLI: add --with-cursor argument --- CHANGELOG | 1 + docs/source/usage.rst | 21 ++++++++++++++++++++- mss/__main__.py | 3 ++- mss/linux.py | 8 ++++---- 4 files changed, 27 insertions(+), 6 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index bde48337..54ad8f3a 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -10,6 +10,7 @@ History: - Linux: added mouse support (related to #55) - Linux: refactored how internal handles are stored to fix issues with multiple X servers (fixes #210) - Linux: removed side effects when leaving the context manager, resources are all freed (fixes #210) + - CLI: added --with-cursor argument - tests: added PyPy 3.9, removed tox, and improved GNU/Linux coverage 7.0.1 2022/10/27 diff --git a/docs/source/usage.rst b/docs/source/usage.rst index 8783d004..47bf7cbe 100644 --- a/docs/source/usage.rst +++ b/docs/source/usage.rst @@ -72,6 +72,25 @@ You can use ``mss`` via the CLI:: Or via direct call from Python:: - python -m mss --help + $ python -m mss --help + usage: __main__.py [-h] [-c COORDINATES] [-l {0,1,2,3,4,5,6,7,8,9}] + [-m MONITOR] [-o OUTPUT] [-q] [-v] [--with-cursor] + + options: + -h, --help show this help message and exit + -c COORDINATES, --coordinates COORDINATES + the part of the screen to capture: top, left, width, height + -l {0,1,2,3,4,5,6,7,8,9}, --level {0,1,2,3,4,5,6,7,8,9} + the PNG compression level + -m MONITOR, --monitor MONITOR + the monitor to screen shot + -o OUTPUT, --output OUTPUT + the output file name + --with-cursor include the cursor + -q, --quiet do not print created files + -v, --version show program's version number and exit .. versionadded:: 3.1.1 + +.. versionadded:: 8.0.0 + ``--with-cursor`` to include the cursor in screenshots. diff --git a/mss/__main__.py b/mss/__main__.py index 52ef3017..5cf76f1d 100644 --- a/mss/__main__.py +++ b/mss/__main__.py @@ -33,6 +33,7 @@ def main(args: List[str], /) -> int: ) cli_args.add_argument("-m", "--monitor", default=0, type=int, help="the monitor to screen shot") cli_args.add_argument("-o", "--output", default="monitor-{mon}.png", help="the output file name") + cli_args.add_argument("--with-cursor", default=False, action="store_true", help="include the cursor") cli_args.add_argument( "-q", "--quiet", @@ -61,7 +62,7 @@ def main(args: List[str], /) -> int: kwargs["output"] = "sct-{top}x{left}_{width}x{height}.png" try: - with mss() as sct: + with mss(with_cursor=options.with_cursor) as sct: if options.coordinates: output = kwargs["output"].format(**kwargs["mon"]) sct_img = sct.grab(kwargs["mon"]) diff --git a/mss/linux.py b/mss/linux.py index 0e743011..de7e3873 100644 --- a/mss/linux.py +++ b/mss/linux.py @@ -454,20 +454,20 @@ def _cursor_impl(self) -> ScreenShot: raise ScreenShotError("Cannot read XFixesGetCursorImage()") cursor_img: XFixesCursorImage = ximage.contents - monitor = { + region = { "left": cursor_img.x - cursor_img.xhot, "top": cursor_img.y - cursor_img.yhot, "width": cursor_img.width, "height": cursor_img.height, } - raw_data = cast(cursor_img.pixels, POINTER(c_ulong * monitor["height"] * monitor["width"])) + raw_data = cast(cursor_img.pixels, POINTER(c_ulong * region["height"] * region["width"])) raw = bytearray(raw_data.contents) - data = bytearray(monitor["height"] * monitor["width"] * 4) + data = bytearray(region["height"] * region["width"] * 4) data[3::4] = raw[3::8] data[2::4] = raw[2::8] data[1::4] = raw[1::8] data[::4] = raw[::8] - return self.cls_image(data, monitor) + return self.cls_image(data, region) From 5a69a287c19afed7f7f31daff82913adf53086f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Sun, 9 Apr 2023 19:13:03 +0200 Subject: [PATCH 085/280] Version 8.0.0 --- CHANGELOG | 8 ++++---- docs/source/api.rst | 2 -- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 54ad8f3a..84d3bb1b 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -2,9 +2,11 @@ History: -8.0.0 2023/0x/xx - - the whole source code was migrated to PEP 570 (Python positional-only parameters) +8.0.0 2023/04/09 + - removed support for Python 3.6 - removed support for Python 3.7 + - MSS: fixed PEP 484 prohibits implicit Optional + - MSS: the whole source code was migrated to PEP 570 (Python positional-only parameters) - Linux: added mouse support (related to #55) - Linux: reset the X server error handler on exit to prevent issues with Tk/Tkinter (fixes #220) - Linux: added mouse support (related to #55) @@ -15,8 +17,6 @@ History: 7.0.1 2022/10/27 - fixed the wheel package - - removed support for Python 3.6 - - MSS: fixed PEP 484 prohibits implicit Optional 7.0.0 2022/10/27 - added support for Python 3.11 diff --git a/docs/source/api.rst b/docs/source/api.rst index 73b8930c..54913126 100644 --- a/docs/source/api.rst +++ b/docs/source/api.rst @@ -73,8 +73,6 @@ GNU/Linux .. class:: MSS - .. attribute:: core - .. method:: close() Clean-up method. From 5be8b833667d4a64705ca9749b98414c51ca56b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Sun, 9 Apr 2023 19:27:47 +0200 Subject: [PATCH 086/280] Bump the version --- CHANGELOG | 6 ++++-- docs/source/conf.py | 2 +- mss/__init__.py | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 84d3bb1b..6b040e52 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -2,16 +2,18 @@ History: +8.0.1 2023/xx/xx + - + 8.0.0 2023/04/09 - removed support for Python 3.6 - removed support for Python 3.7 - MSS: fixed PEP 484 prohibits implicit Optional - MSS: the whole source code was migrated to PEP 570 (Python positional-only parameters) - - Linux: added mouse support (related to #55) - Linux: reset the X server error handler on exit to prevent issues with Tk/Tkinter (fixes #220) - - Linux: added mouse support (related to #55) - Linux: refactored how internal handles are stored to fix issues with multiple X servers (fixes #210) - Linux: removed side effects when leaving the context manager, resources are all freed (fixes #210) + - Linux: added mouse support (related to #55) - CLI: added --with-cursor argument - tests: added PyPy 3.9, removed tox, and improved GNU/Linux coverage diff --git a/docs/source/conf.py b/docs/source/conf.py index cd19fa95..7c340a3a 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -27,7 +27,7 @@ # built documents. # # The short X.Y version. -version = "8.0.0" +version = "8.0.1" # The full version, including alpha/beta/rc tags. release = "latest" diff --git a/mss/__init__.py b/mss/__init__.py index 61a4d3cc..d441f12a 100644 --- a/mss/__init__.py +++ b/mss/__init__.py @@ -11,7 +11,7 @@ from .exception import ScreenShotError from .factory import mss -__version__ = "8.0.0" +__version__ = "8.0.1" __author__ = "Mickaël 'Tiger-222' Schoentgen" __copyright__ = """ Copyright (c) 2013-2023, Mickaël 'Tiger-222' Schoentgen From 12f9f162d02d1df9e586481c842998ff609ee0b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Sun, 9 Apr 2023 19:28:37 +0200 Subject: [PATCH 087/280] doc: tweak --- CHANGES.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index d03e4db2..8050203b 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,4 +1,4 @@ -8.0.0 (2023-xx-xx) +8.0.0 (2023-04-09) ================== base.py From f53aa90e07e0c55b16966fec2597f9cca7245059 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Sun, 9 Apr 2023 19:34:55 +0200 Subject: [PATCH 088/280] doc: fix version --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index fb03cd3e..ab1cb5ac 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = mss -version = 8.0.0 +version = 8.0.1 author = Mickaël 'Tiger-222' Schoentgen author_email = contact@tiger-222.fr description = An ultra fast cross-platform multiple screenshots module in pure python using ctypes. From dac8c5751926c23304dd9666f93338903097c454 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Sun, 9 Apr 2023 19:41:28 +0200 Subject: [PATCH 089/280] tests: fix test_entry_point() with multiple monitors having the same resolution --- CHANGELOG | 2 +- mss/tests/test_implementation.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 6b040e52..44272dc5 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -3,7 +3,7 @@ History: 8.0.1 2023/xx/xx - - + - tests: fix test_entry_point() with multiple monitors having the same resolution 8.0.0 2023/04/09 - removed support for Python 3.6 diff --git a/mss/tests/test_implementation.py b/mss/tests/test_implementation.py index dee34fc2..6ce5a3af 100644 --- a/mss/tests/test_implementation.py +++ b/mss/tests/test_implementation.py @@ -96,13 +96,13 @@ def test_entry_point(capsys): assert os.path.isfile("monitor-1.png") os.remove("monitor-1.png") - fmt = "sct-{width}x{height}.png" + fmt = "sct-{mon}-{width}x{height}.png" for opt in ("-o", "--out"): main([opt, fmt]) out, _ = capsys.readouterr() with mss(display=os.getenv("DISPLAY")) as sct: - for monitor, line in zip(sct.monitors[1:], out.splitlines()): - filename = fmt.format(**monitor) + for mon, (monitor, line) in enumerate(zip(sct.monitors[1:], out.splitlines()), 1): + filename = fmt.format(mon=mon, **monitor) assert line.endswith(filename) assert os.path.isfile(filename) os.remove(filename) From c35bc06a34886b2fe3d464241f6745ffdb686379 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Sun, 9 Apr 2023 19:46:21 +0200 Subject: [PATCH 090/280] tests: add --with-cursor entry point tests --- mss/tests/test_implementation.py | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/mss/tests/test_implementation.py b/mss/tests/test_implementation.py index 6ce5a3af..efa75b1f 100644 --- a/mss/tests/test_implementation.py +++ b/mss/tests/test_implementation.py @@ -77,20 +77,26 @@ def test_factory(monkeypatch): assert error == "System 'chuck norris' not (yet?) implemented." -def test_entry_point(capsys): +@pytest.mark.parametrize("with_cursor", [False, True]) +def test_entry_point(with_cursor: bool, capsys): from datetime import datetime - from mss.__main__ import main + from mss.__main__ import main as entry_point + + def main(*args): + if with_cursor: + args = args + ("--with-cursor",) + entry_point(args) for opt in ("-m", "--monitor"): - main([opt, "1"]) + main(opt, "1") out, _ = capsys.readouterr() assert out.endswith("monitor-1.png\n") 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 opt in zip(["-m 1", "--monitor=1"], ["-q", "--quiet"]): + main(*opt) out, _ = capsys.readouterr() assert not out assert os.path.isfile("monitor-1.png") @@ -98,7 +104,7 @@ def test_entry_point(capsys): fmt = "sct-{mon}-{width}x{height}.png" for opt in ("-o", "--out"): - main([opt, fmt]) + main(opt, fmt) out, _ = capsys.readouterr() with mss(display=os.getenv("DISPLAY")) as sct: for mon, (monitor, line) in enumerate(zip(sct.monitors[1:], out.splitlines()), 1): @@ -109,7 +115,7 @@ def test_entry_point(capsys): fmt = "sct_{mon}-{date:%Y-%m-%d}.png" for opt in ("-o", "--out"): - main(["-m 1", opt, fmt]) + main("-m 1", opt, fmt) filename = fmt.format(mon=1, date=datetime.now()) out, _ = capsys.readouterr() assert out.endswith(filename + "\n") @@ -119,7 +125,7 @@ def test_entry_point(capsys): coordinates = "2,12,40,67" filename = "sct-2x12_40x67.png" for opt in ("-c", "--coordinates"): - main([opt, coordinates]) + main(opt, coordinates) out, _ = capsys.readouterr() assert out.endswith(filename + "\n") assert os.path.isfile(filename) @@ -127,7 +133,7 @@ def test_entry_point(capsys): coordinates = "2,12,40" for opt in ("-c", "--coordinates"): - main([opt, coordinates]) + main(opt, coordinates) out, _ = capsys.readouterr() assert out == "Coordinates syntax: top, left, width, height\n" From 54c8c8b68c7520399e4d1605dba1be47634f26bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Sun, 9 Apr 2023 20:03:01 +0200 Subject: [PATCH 091/280] CLI: do not raise a ScreenShotError when -q, or --quiet, is used --- CHANGELOG | 1 + mss/__main__.py | 4 +++- mss/tests/test_implementation.py | 27 ++++++++++++++++++++++++--- 3 files changed, 28 insertions(+), 4 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 44272dc5..9fd9dac4 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -3,6 +3,7 @@ History: 8.0.1 2023/xx/xx + - CLI: do not raise a ScreenShotError when -q, or --quiet, is used but return 1 - tests: fix test_entry_point() with multiple monitors having the same resolution 8.0.0 2023/04/09 diff --git a/mss/__main__.py b/mss/__main__.py index 5cf76f1d..73d0faae 100644 --- a/mss/__main__.py +++ b/mss/__main__.py @@ -75,7 +75,9 @@ def main(args: List[str], /) -> int: print(os.path.realpath(file_name)) return 0 except ScreenShotError: - return 1 + if options.quiet: + return 1 + raise if __name__ == "__main__": # pragma: nocover diff --git a/mss/tests/test_implementation.py b/mss/tests/test_implementation.py index efa75b1f..e1ea773b 100644 --- a/mss/tests/test_implementation.py +++ b/mss/tests/test_implementation.py @@ -5,6 +5,7 @@ import os import os.path import platform +from unittest.mock import patch import pytest @@ -83,10 +84,10 @@ def test_entry_point(with_cursor: bool, capsys): from mss.__main__ import main as entry_point - def main(*args): + def main(*args: str, ret: int = 0) -> None: if with_cursor: args = args + ("--with-cursor",) - entry_point(args) + assert entry_point(args) == ret for opt in ("-m", "--monitor"): main(opt, "1") @@ -133,11 +134,31 @@ def main(*args): coordinates = "2,12,40" for opt in ("-c", "--coordinates"): - main(opt, coordinates) + main(opt, coordinates, ret=2) out, _ = capsys.readouterr() assert out == "Coordinates syntax: top, left, width, height\n" +@patch("mss.base.MSSBase.monitors", new=[]) +@pytest.mark.parametrize("quiet", [False, True]) +def test_entry_point_error(quiet: bool, capsys): + from mss.__main__ import main as entry_point + + def main(*args: str) -> int: + if quiet: + args = args + ("--quiet",) + return entry_point(args) + + if quiet: + assert main() == 1 + out, err = capsys.readouterr() + assert not out + assert not err + else: + with pytest.raises(ScreenShotError): + main() + + def test_grab_with_tuple(pixel_ratio): left = 100 top = 100 From ce876444697987b18f8f1164b129ea331a3a4440 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Sun, 9 Apr 2023 20:22:34 +0200 Subject: [PATCH 092/280] fix: ensure --with-cursor, and with_cursor argument & attribute, are simple NOOP on platforms not supporting the feature --- CHANGELOG | 1 + mss/base.py | 5 ++--- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 9fd9dac4..153c1fa6 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -3,6 +3,7 @@ History: 8.0.1 2023/xx/xx + - MSS: ensure --with-cursor, and with_cursor argument & attribute, are simple NOOP on platforms not supporting the feature - CLI: do not raise a ScreenShotError when -q, or --quiet, is used but return 1 - tests: fix test_entry_point() with multiple monitors having the same resolution diff --git a/mss/base.py b/mss/base.py index 11c39509..14a45288 100644 --- a/mss/base.py +++ b/mss/base.py @@ -88,9 +88,8 @@ def grab(self, monitor: Union[Monitor, Tuple[int, int, int, int]], /) -> ScreenS with lock: screenshot = self._grab_impl(monitor) - if self.with_cursor: - cursor = self._cursor_impl() - screenshot = self._merge(screenshot, cursor) # type: ignore[arg-type] + if self.with_cursor and (cursor := self._cursor_impl()): + return self._merge(screenshot, cursor) return screenshot @property From 9ac4931463cac67348060049a1c9609ed8e9c2c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Sun, 9 Apr 2023 20:32:14 +0200 Subject: [PATCH 093/280] Version 8.0.1 --- CHANGELOG | 2 +- docs/source/support.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 153c1fa6..ca7dc358 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -2,7 +2,7 @@ History: -8.0.1 2023/xx/xx +8.0.1 2023/04/09 - MSS: ensure --with-cursor, and with_cursor argument & attribute, are simple NOOP on platforms not supporting the feature - CLI: do not raise a ScreenShotError when -q, or --quiet, is used but return 1 - tests: fix test_entry_point() with multiple monitors having the same resolution diff --git a/docs/source/support.rst b/docs/source/support.rst index 330f9438..8a11d6b6 100644 --- a/docs/source/support.rst +++ b/docs/source/support.rst @@ -33,4 +33,4 @@ Abandoned - Python 3.4 (2018-03-19) - Python 3.5 (2022-10-27) - Python 3.6 (2022-10-27) -- Python 3.7 (2023-xx-xx) +- Python 3.7 (2023-04-09) From d505f5160c65f8b057b2a5cd8f0725f1e7e82a7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Sun, 9 Apr 2023 20:35:06 +0200 Subject: [PATCH 094/280] Bump the version --- CHANGELOG | 3 +++ docs/source/conf.py | 2 +- mss/__init__.py | 2 +- setup.cfg | 2 +- 4 files changed, 6 insertions(+), 3 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index ca7dc358..9d49bf33 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -2,6 +2,9 @@ History: +8.0.2 2023/xx/xx + - + 8.0.1 2023/04/09 - MSS: ensure --with-cursor, and with_cursor argument & attribute, are simple NOOP on platforms not supporting the feature - CLI: do not raise a ScreenShotError when -q, or --quiet, is used but return 1 diff --git a/docs/source/conf.py b/docs/source/conf.py index 7c340a3a..123e4f8e 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -27,7 +27,7 @@ # built documents. # # The short X.Y version. -version = "8.0.1" +version = "8.0.2" # The full version, including alpha/beta/rc tags. release = "latest" diff --git a/mss/__init__.py b/mss/__init__.py index d441f12a..3d81ef7e 100644 --- a/mss/__init__.py +++ b/mss/__init__.py @@ -11,7 +11,7 @@ from .exception import ScreenShotError from .factory import mss -__version__ = "8.0.1" +__version__ = "8.0.2" __author__ = "Mickaël 'Tiger-222' Schoentgen" __copyright__ = """ Copyright (c) 2013-2023, Mickaël 'Tiger-222' Schoentgen diff --git a/setup.cfg b/setup.cfg index ab1cb5ac..0bc0aa41 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = mss -version = 8.0.1 +version = 8.0.2 author = Mickaël 'Tiger-222' Schoentgen author_email = contact@tiger-222.fr description = An ultra fast cross-platform multiple screenshots module in pure python using ctypes. From d5c7a458d8805168cb27a1fdfbcd88d2b492c075 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Sun, 9 Apr 2023 22:51:45 +0200 Subject: [PATCH 095/280] CLI: fix arguments handling --- CHANGELOG | 2 +- mss/__main__.py | 5 ++--- mss/tests/test_implementation.py | 13 +++++++++++-- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 9d49bf33..92b9930c 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -3,7 +3,7 @@ History: 8.0.2 2023/xx/xx - - + - CLI: fixed arguments handling 8.0.1 2023/04/09 - MSS: ensure --with-cursor, and with_cursor argument & attribute, are simple NOOP on platforms not supporting the feature diff --git a/mss/__main__.py b/mss/__main__.py index 73d0faae..0aa43983 100644 --- a/mss/__main__.py +++ b/mss/__main__.py @@ -4,7 +4,6 @@ """ import os.path from argparse import ArgumentParser -from typing import List from . import __version__ from .exception import ScreenShotError @@ -12,7 +11,7 @@ from .tools import to_png -def main(args: List[str], /) -> int: +def main(*args: str) -> int: """Main logic.""" cli_args = ArgumentParser() @@ -83,4 +82,4 @@ def main(args: List[str], /) -> int: if __name__ == "__main__": # pragma: nocover import sys - sys.exit(main(sys.argv[1:])) + sys.exit(main(*sys.argv[1:])) diff --git a/mss/tests/test_implementation.py b/mss/tests/test_implementation.py index e1ea773b..5252bbb9 100644 --- a/mss/tests/test_implementation.py +++ b/mss/tests/test_implementation.py @@ -87,7 +87,16 @@ def test_entry_point(with_cursor: bool, capsys): def main(*args: str, ret: int = 0) -> None: if with_cursor: args = args + ("--with-cursor",) - assert entry_point(args) == ret + assert entry_point(*args) == ret + + # No arguments + main() + out, _ = capsys.readouterr() + for mon, line in enumerate(out.splitlines(), 1): + filename = f"monitor-{mon}.png" + assert line.endswith(filename) + assert os.path.isfile(filename) + os.remove(filename) for opt in ("-m", "--monitor"): main(opt, "1") @@ -147,7 +156,7 @@ def test_entry_point_error(quiet: bool, capsys): def main(*args: str) -> int: if quiet: args = args + ("--quiet",) - return entry_point(args) + return entry_point(*args) if quiet: assert main() == 1 From 00afcd9b19acf8f688a7d9b1970d7481f1d5c9d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Sun, 9 Apr 2023 23:31:48 +0200 Subject: [PATCH 096/280] tests: compress monitor-1024x768.raw --- mss/tests/conftest.py | 10 ++++++++-- mss/tests/res/monitor-1024x768.raw | Bin 3145728 -> 0 bytes mss/tests/res/monitor-1024x768.raw.zip | Bin 0 -> 37834 bytes 3 files changed, 8 insertions(+), 2 deletions(-) delete mode 100644 mss/tests/res/monitor-1024x768.raw create mode 100644 mss/tests/res/monitor-1024x768.raw.zip diff --git a/mss/tests/conftest.py b/mss/tests/conftest.py index 979850db..a0d24bb1 100644 --- a/mss/tests/conftest.py +++ b/mss/tests/conftest.py @@ -4,7 +4,9 @@ """ import glob import os +from hashlib import md5 from pathlib import Path +from zipfile import ZipFile import pytest @@ -42,8 +44,12 @@ def before_tests(request): @pytest.fixture(scope="session") def raw() -> bytes: - file = Path(__file__).parent / "res" / "monitor-1024x768.raw" - return file.read_bytes() + file = Path(__file__).parent / "res" / "monitor-1024x768.raw.zip" + with ZipFile(file) as fh: + data = fh.read(file.with_suffix("").name) + + assert md5(data).hexdigest() == "125696266e2a8f5240f6bc17e4df98c6" + return data @pytest.fixture(scope="session") diff --git a/mss/tests/res/monitor-1024x768.raw b/mss/tests/res/monitor-1024x768.raw deleted file mode 100644 index 65a1c7202d8425a146199f5ddd6540705262d2b3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3145728 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 literal 0 HcmV?d00001 From 751bd1345886ce56247f0db529443fa5b2562db4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Sun, 9 Apr 2023 23:36:49 +0200 Subject: [PATCH 097/280] tests: skip Tk tests on PyPy --- mss/tests/test_issue_220.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/mss/tests/test_issue_220.py b/mss/tests/test_issue_220.py index 6ad1de01..2e143eef 100644 --- a/mss/tests/test_issue_220.py +++ b/mss/tests/test_issue_220.py @@ -2,12 +2,19 @@ This is part of the MSS Python's module. Source: https://github.com/BoboTiG/python-mss """ +import platform + import pytest import mss tkinter = pytest.importorskip("tkinter") +if platform.python_implementation() == "PyPy": + # PyPy 7.3.11 [Python 3.9.16] fails on GitHub: + # RuntimeError: tk.h version (8.5) doesn't match libtk.a version (8.6) + pytestmark = pytest.mark.skip + @pytest.fixture def root() -> tkinter.Tk: From 426826b02634ca2decd35f931e2299397dcd2dcd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Sun, 9 Apr 2023 23:42:44 +0200 Subject: [PATCH 098/280] fix: SetuptoolsDeprecationWarning: Installing 'XXX' as data is deprecated, please list it in packages --- CHANGELOG | 1 + setup.cfg | 8 ++++++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 92b9930c..16a76223 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -3,6 +3,7 @@ History: 8.0.2 2023/xx/xx + - fixed SetuptoolsDeprecationWarning: Installing 'XXX' as data is deprecated, please list it in packages - CLI: fixed arguments handling 8.0.1 2023/04/09 diff --git a/setup.cfg b/setup.cfg index 0bc0aa41..e7189841 100644 --- a/setup.cfg +++ b/setup.cfg @@ -35,10 +35,14 @@ classifiers = [options] zip_safe = False include_package_data = True -packages_dir = mss -packages = find: +packages = find_namespace: python_requires = >=3.8 +[options.packages.find] +include = + mss + mss.* + [options.entry_points] console_scripts = mss = mss.__main__:main From 79481550e3067d06f602fdf1631e6500ce5d153d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Sun, 9 Apr 2023 23:57:18 +0200 Subject: [PATCH 099/280] tests: skip Tk tests on macOS only --- mss/tests/test_issue_220.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mss/tests/test_issue_220.py b/mss/tests/test_issue_220.py index 2e143eef..da68972c 100644 --- a/mss/tests/test_issue_220.py +++ b/mss/tests/test_issue_220.py @@ -10,8 +10,8 @@ tkinter = pytest.importorskip("tkinter") -if platform.python_implementation() == "PyPy": - # PyPy 7.3.11 [Python 3.9.16] fails on GitHub: +if platform.system().lower() == "darwin" and platform.python_implementation() == "PyPy": + # [macOS] PyPy 7.3.11 [Python 3.9.16] fails on GitHub: # RuntimeError: tk.h version (8.5) doesn't match libtk.a version (8.6) pytestmark = pytest.mark.skip From 1549f9e8556cd6b66b3575a9e6062552921f08f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Mon, 10 Apr 2023 00:00:09 +0200 Subject: [PATCH 100/280] Version 8.0.2 --- CHANGELOG | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 16a76223..2ada29ca 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -2,7 +2,7 @@ History: -8.0.2 2023/xx/xx +8.0.2 2023/04/09 - fixed SetuptoolsDeprecationWarning: Installing 'XXX' as data is deprecated, please list it in packages - CLI: fixed arguments handling From c6355ac85300946a44ae6f36b44a40cef466a3d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Mon, 10 Apr 2023 00:02:50 +0200 Subject: [PATCH 101/280] Bump the version --- CHANGELOG | 3 +++ docs/source/conf.py | 2 +- mss/__init__.py | 2 +- setup.cfg | 2 +- 4 files changed, 6 insertions(+), 3 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 2ada29ca..4962ce4a 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -2,6 +2,9 @@ History: +8.0.3 2023/xx/xx + - + 8.0.2 2023/04/09 - fixed SetuptoolsDeprecationWarning: Installing 'XXX' as data is deprecated, please list it in packages - CLI: fixed arguments handling diff --git a/docs/source/conf.py b/docs/source/conf.py index 123e4f8e..f6bfc6c5 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -27,7 +27,7 @@ # built documents. # # The short X.Y version. -version = "8.0.2" +version = "8.0.3" # The full version, including alpha/beta/rc tags. release = "latest" diff --git a/mss/__init__.py b/mss/__init__.py index 3d81ef7e..17c7d059 100644 --- a/mss/__init__.py +++ b/mss/__init__.py @@ -11,7 +11,7 @@ from .exception import ScreenShotError from .factory import mss -__version__ = "8.0.2" +__version__ = "8.0.3" __author__ = "Mickaël 'Tiger-222' Schoentgen" __copyright__ = """ Copyright (c) 2013-2023, Mickaël 'Tiger-222' Schoentgen diff --git a/setup.cfg b/setup.cfg index e7189841..d00e37d3 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = mss -version = 8.0.2 +version = 8.0.3 author = Mickaël 'Tiger-222' Schoentgen author_email = contact@tiger-222.fr description = An ultra fast cross-platform multiple screenshots module in pure python using ctypes. From 7f76d7c3c141bd735ffe6dd09fe51298633ddd36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Mon, 10 Apr 2023 00:30:42 +0200 Subject: [PATCH 102/280] doc: add videostream_censor --- docs/source/where.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/source/where.rst b/docs/source/where.rst index ad8a05e1..95acc6f1 100644 --- a/docs/source/where.rst +++ b/docs/source/where.rst @@ -29,5 +29,6 @@ Do not hesistate to `say Hello! `_ - `Star Wars - The Old Republic: Galactic StarFighter `_ parser; - `Stitch `_, a Python Remote Administration Tool (RAT); - `TensorKart `_, a self-driving MarioKart with TensorFlow; -- `wow-fishing-bot `_, a fishing bot for World of Warcraft that uses template matching from OpenCV; +- `videostream_censor `_, a real time video recording censor ; +- `wow-fishing-bot `_, a fishing bot for World of Warcraft that uses template matching from OpenCV; - `Zelda Bowling AI `_; From d5b71d180127fdf8f80007de2a2a08194c7701bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20G=C3=B3rny?= Date: Mon, 10 Apr 2023 10:18:10 +0000 Subject: [PATCH 103/280] Include documentation sources in sdist (#240) Add documentation sources to the list of files included in sdist archives, in order to make it possible to build offline documentation using them. This is necessary in order to make it possible for Linux distributions such as Gentoo to be able to use sdists. --- MANIFEST.in | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/MANIFEST.in b/MANIFEST.in index b8cdc5d1..d9e645ae 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,3 +1,4 @@ -# Include tests files and data +# Include test files, documentation sources and data include mss/tests/*.py +recursive-include docs/source * recursive-include mss/tests/res * From 74464088a104c92aeba354ed3f693d4e204f6ed5 Mon Sep 17 00:00:00 2001 From: Shin-myoung-serp Date: Mon, 10 Apr 2023 21:51:49 +0900 Subject: [PATCH 104/280] linux: fix issue #220 by letting close() restore the previous error handler. (#241) --- mss/linux.py | 34 ++++++++++++---------------------- 1 file changed, 12 insertions(+), 22 deletions(-) diff --git a/mss/linux.py b/mss/linux.py index de7e3873..83613c5c 100644 --- a/mss/linux.py +++ b/mss/linux.py @@ -187,21 +187,6 @@ class XWindowAttributes(Structure): _XRANDR = find_library("Xrandr") -@CFUNCTYPE(c_int, POINTER(Display), POINTER(Event)) -def _default_error_handler(display: Display, event: Event) -> int: - """ - Specifies the default program's supplied error handler. - It's useful when exiting MSS to prevent letting `_error_handler()` as default handler. - Doing so would crash when using Tk/Tkinter, see issue #220. - - Interesting technical stuff can be found here: - https://core.tcl-lang.org/tk/file?name=generic/tkError.c&ci=a527ef995862cb50 - https://github.com/tcltk/tk/blob/b9cdafd83fe77499ff47fa373ce037aff3ae286a/generic/tkError.c - """ - # pylint: disable=unused-argument - return 0 # pragma: nocover - - @CFUNCTYPE(c_int, POINTER(Display), POINTER(Event)) def _error_handler(display: Display, event: Event) -> int: """Specifies the program's supplied error handler.""" @@ -266,7 +251,7 @@ def _validate(retval: int, func: Any, args: Tuple[Any, Any], /) -> Tuple[Any, An "XRRGetCrtcInfo": ("xrandr", [POINTER(Display), POINTER(XRRScreenResources), c_long], POINTER(XRRCrtcInfo)), "XRRGetScreenResources": ("xrandr", [POINTER(Display), POINTER(Display)], POINTER(XRRScreenResources)), "XRRGetScreenResourcesCurrent": ("xrandr", [POINTER(Display), POINTER(Display)], POINTER(XRRScreenResources)), - "XSetErrorHandler": ("xlib", [c_void_p], c_int), + "XSetErrorHandler": ("xlib", [c_void_p], c_void_p), } @@ -276,7 +261,7 @@ class MSS(MSSBase): It uses intensively the Xlib and its Xrandr extension. """ - __slots__ = {"xfixes", "xlib", "xrandr", "_handles"} + __slots__ = {"xfixes", "xlib", "xrandr", "_handles", "_old_error_handler"} def __init__(self, /, **kwargs: Any) -> None: """GNU/Linux initialisations.""" @@ -300,10 +285,6 @@ def __init__(self, /, **kwargs: Any) -> None: raise ScreenShotError("No X11 library found.") self.xlib = cdll.LoadLibrary(_X11) - # Install the error handler to prevent interpreter crashes: - # any error will raise a ScreenShotError exception. - self.xlib.XSetErrorHandler(_error_handler) - if not _XRANDR: raise ScreenShotError("No Xrandr extension found.") self.xrandr = cdll.LoadLibrary(_XRANDR) @@ -316,6 +297,10 @@ def __init__(self, /, **kwargs: Any) -> None: self._set_cfunctions() + # Install the error handler to prevent interpreter crashes: + # any error will raise a ScreenShotError exception. + self._old_error_handler = self.xlib.XSetErrorHandler(_error_handler) + self._handles = local() self._handles.display = self.xlib.XOpenDisplay(display) @@ -330,7 +315,12 @@ def __init__(self, /, **kwargs: Any) -> None: def close(self) -> None: # Remove our error handler - self.xlib.XSetErrorHandler(_default_error_handler) + # It's required when exiting MSS to prevent letting `_error_handler()` as default handler. + # Doing so would crash when using Tk/Tkinter, see issue #220. + # Interesting technical stuff can be found here: + # https://core.tcl-lang.org/tk/file?name=generic/tkError.c&ci=a527ef995862cb50 + # https://github.com/tcltk/tk/blob/b9cdafd83fe77499ff47fa373ce037aff3ae286a/generic/tkError.c + self.xlib.XSetErrorHandler(self._old_error_handler) # Clean-up if self._handles.display is not None: From 304136bb8e7f148d2fa004be71325a7e72ce7f5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Mon, 10 Apr 2023 13:02:50 +0200 Subject: [PATCH 105/280] doc: move to markdown + PEP 561 compatibility --- CHANGELOG | 265 ---------------------------------------- CHANGELOG.md | 269 +++++++++++++++++++++++++++++++++++++++++ CHANGES.md | 188 ++++++++++++++++++++++++++++ CHANGES.rst | 249 -------------------------------------- CONTRIBUTORS | 56 --------- CONTRIBUTORS.md | 18 +++ LICENSE => LICENSE.txt | 0 MANIFEST.in | 8 +- README.md | 54 +++++++++ README.rst | 57 --------- mss/py.typed | 0 setup.cfg | 4 +- 12 files changed, 538 insertions(+), 630 deletions(-) delete mode 100644 CHANGELOG create mode 100644 CHANGELOG.md create mode 100644 CHANGES.md delete mode 100644 CHANGES.rst delete mode 100644 CONTRIBUTORS create mode 100644 CONTRIBUTORS.md rename LICENSE => LICENSE.txt (100%) create mode 100644 README.md delete mode 100644 README.rst create mode 100644 mss/py.typed diff --git a/CHANGELOG b/CHANGELOG deleted file mode 100644 index 4962ce4a..00000000 --- a/CHANGELOG +++ /dev/null @@ -1,265 +0,0 @@ -History: - - - -8.0.3 2023/xx/xx - - - -8.0.2 2023/04/09 - - fixed SetuptoolsDeprecationWarning: Installing 'XXX' as data is deprecated, please list it in packages - - CLI: fixed arguments handling - -8.0.1 2023/04/09 - - MSS: ensure --with-cursor, and with_cursor argument & attribute, are simple NOOP on platforms not supporting the feature - - CLI: do not raise a ScreenShotError when -q, or --quiet, is used but return 1 - - tests: fix test_entry_point() with multiple monitors having the same resolution - -8.0.0 2023/04/09 - - removed support for Python 3.6 - - removed support for Python 3.7 - - MSS: fixed PEP 484 prohibits implicit Optional - - MSS: the whole source code was migrated to PEP 570 (Python positional-only parameters) - - Linux: reset the X server error handler on exit to prevent issues with Tk/Tkinter (fixes #220) - - Linux: refactored how internal handles are stored to fix issues with multiple X servers (fixes #210) - - Linux: removed side effects when leaving the context manager, resources are all freed (fixes #210) - - Linux: added mouse support (related to #55) - - CLI: added --with-cursor argument - - tests: added PyPy 3.9, removed tox, and improved GNU/Linux coverage - -7.0.1 2022/10/27 - - fixed the wheel package - -7.0.0 2022/10/27 - - added support for Python 3.11 - - added support for Python 3.10 - - removed support for Python 3.5 - - MSS: modernized the code base (types, f-string, ran isort & black) (close #101) - - MSS: fixed several Sourcery issues - - MSS: fixed typos here, and there - - doc: fixed an error when building the documentation - -6.1.0 2020/10/31 - - MSS: reworked how C functions are initialised - - Mac: reduce the number of function calls - - Mac: support macOS Big Sur (fixes #178) - - tests: expand Python versions to 3.9 and 3.10 - - tests: fix macOS intepreter not found on Travis-CI - - tests: fix test_entry_point() when there are several monitors - -6.0.0 2020/06/30 - - removed usage of deprecated "license_file" option for "license_files" - - fixed flake8 usage in pre-commit - - the module is now available on conda (closes #170) - - MSS: the implementation is now thread-safe on all OSes (fixes #169) - - Linux: better handling of the Xrandr extension (fixes #168) - - tests: fixed a random bug on test_grab_with_tuple_percents() (fixes #142) - -5.1.0 2020/04/30 - - produce wheels for Python 3 only - - MSS: renamed again MSSMixin to MSSBase, now derived from abc.ABCMeta - - tools: force write of file when saving a PNG file - - tests: fix tests on macOS with Retina display - - Windows: fixed multi-thread safety (fixes #150) - - :heart: contributors: @narumishi - -5.0.0 2019/12/31 - - removed support for Python 2.7 - - MSS: improve type annotations and add CI check - - MSS: use __slots__ for better performances - - MSS: better handle resources to prevent leaks - - MSS: improve monitors finding - - Windows: use our own instances of GDI32 and User32 DLLs - - doc: add project_urls to setup.cfg - - doc: add an example using the multiprocessing module (closes #82) - - tests: added regression tests for #128 and #135 - - tests: move tests files into the package - - :heart: contributors: @hugovk, @foone, @SergeyKalutsky - -4.0.2 2019/02/23 - - new contributor: foone - - Windows: ignore missing SetProcessDPIAware() on Window XP (fixes #109) - -4.0.1 2019/01/26 - - Linux: fix several XLib functions signature (fixes #92) - - Linux: improve monitors finding by a factor of 44 - -4.0.0 2019/01/11 - - MSS: remove use of setup.py for setup.cfg - - MSS: renamed MSSBase to MSSMixin in base.py - - MSS: refactor ctypes argtype, restype and errcheck setup (fixes #84) - - Linux: ensure resources are freed in grab() - - Windows: avoid unnecessary class attributes - - MSS: ensure calls without context manager will not leak resources or document them (fixes #72 and #85) - - MSS: fix Flake8 C408: Unnecessary dict call - rewrite as a literal, in exceptions.py - - MSS: fix Flake8 I100: Import statements are in the wrong order - - MSS: fix Flake8 I201: Missing newline before sections or imports - - MSS: fix PyLint bad-super-call: Bad first argument 'Exception' given to super() - - tests: use tox, enable PyPy and PyPy3, add macOS and Windows CI - -3.3.2 2018/11/20 - - new contributors: hugovk, Andreas Buhr - - MSS: do monitor detection in MSS constructor (fixes #79) - - MSS: specify compliant Python versions for pip install - - tests: enable Python 3.7 - - tests: fix test_entry_point() with multiple monitors - -3.3.1 2018/09/22 - - Linux: fix a memory leak introduced with 7e8ae5703f0669f40532c2be917df4328bc3985e (fixes #72) - - doc: add the download statistics badge - -3.3.0 2018/09/04 - - Linux: add an error handler for the XServer to prevent interpreter crash (fix #61) - - MSS: fix a ResourceWarning: unclosed file in setup.py - - tests: fix a ResourceWarning: unclosed file - - doc: fix a typo in Screenshot.pixel() method (thanks to @mchlnix) - - big code clean-up using black - -3.2.1 2018/05/21 - - new contributor: Ryan Fox - - Windows: enable Hi-DPI awareness - -3.2.0 2018/03/22 - - removed support for Python 3.4 - - MSS: add the Screenshot.bgra attribute - - MSS: speed-up grabbing on the 3 platforms - - tools: add PNG compression level control to to_png() - - tests: add leaks.py and benchmarks.py for manual testing - - doc: add an example about capturing part of the monitor 2 - - doc: add an example about computing BGRA values to RGB - -3.1.2 2018/01/05 - - removed support for Python 3.3 - - MSS: possibility to get the whole PNG raw bytes - - Windows: capture all visible windows - - doc: improvements and fixes (fix #37) - - CI: build the documentation - -3.1.1 2017/11/27 - - MSS: add the 'mss' entry point - -3.1.0 2017/11/16 - - new contributor: Karan Lyons - - MSS: add more way of customization to the output argument of save() - - MSS: possibility to use custom class to handle screen shot data - - Mac: properly support all display scaling and resolutions (fix #14, #19, #21, #23) - - Mac: fix memory leaks (fix #24) - - Linux: handle bad display value - - Windows: take into account zoom factor for high-DPI displays (fix #20) - - doc: several fixes (fix #22) - - tests: a lot of tests added for better coverage - - add the 'Say Thanks' button - -3.0.1 2017/07/06 - - fix examples links - -3.0.0 2017/07/06 - - big refactor, introducing the ScreenShot class - - MSS: add Numpy array interface support to the Screenshot class - - doc: add OpenCV/Numpy, PIL pixels, FPS - -2.0.22 2017/04/29 - - new contributors: David Becker, redodo - - MSS: better use of exception mechanism - - Linux: use of hasattr to prevent Exception on early exit - - Mac: take into account extra black pixels added when screen with is not divisible by 16 (fix #14) - - doc: add an example to capture only a part of the screen - -2.0.18 2016/12/03 - - change license to MIT - - new contributor: Jochen 'cycomanic' Schroeder - - MSS: add type hints - - MSS: remove unused code (reported by Vulture) - - Linux: remove MSS library - - Linux: insanely fast using only ctypes - - Linux: skip unused monitors - - Linux: use errcheck instead of deprecated restype with callable (fix #11) - - Linux: fix security issue (reported by Bandit) - - doc: add documentation (fix #10) - - tests: add tests and use Travis CI (fix #9) - -2.0.0 2016/06/04 - - split the module into several files - - MSS: a lot of code refactor and optimizations - - MSS: rename save_img() to to_png() - - MSS: save(): replace 'screen' argument by 'mon' - - Mac: get rid of the PyObjc module, 100% ctypes - - Linux: prevent segfault when DISPLAY is set but no X server started - - Linux: prevent segfault when Xrandr is not loaded - - Linux: get_pixels() insanely fast, use of MSS library (C code) - - Windows: fix #6, screen shot not correct on Windows 8 - - add issue and pull request templates - -1.0.2 2016/04/22 - - MSS: fix non existent alias - -1.0.1 2016/04/22 - - MSS: fix #7, libpng warning (ignoring bad filter type) - -1.0.0 2015/04/16 - - Python 2.6 to 3.5 ready - - MSS: code purgation and review, no more debug information - - MSS: fix #5, add a shortcut to take automatically use the proper MSS class - - MSS: few optimizations into save_img() - - Darwin: remove rotation from information returned by enum_display_monitors() - - Linux: fix object has no attribute 'display' into __del__ - - Linux: use of XDestroyImage() instead of XFree() - - Linux: optimizations of get_pixels() - - Windows: huge optimization of get_pixels() - - CLI: delete --debug argument - -0.1.1 2015/04/10 - - MSS: little code review - - Linux: fix monitor count - - tests: remove test-linux binary - - doc: add doc/TESTING - - doc: remove Bonus section from README.rst - -0.1.0 2015/04/10 - - MSS: fix code with YAPF tool - - Linux: fully functional using Xrandr library - - Linux: code purgation (no more XML files to parse) - - doc: better tests and examples - -0.0.8 2015/02/04 - - new contributors: sergey-vin, Alexander 'thehesiod' Mohr - - MSS: fix #3, filename's dir is not used when saving - - MSS: fix "E713 test for membership should be 'not in'" - - MSS: raise an exception for unimplemented methods - - Windows: fix #4, robustness to MSSWindows.get_pixels - -0.0.7 2014/03/20 - - MSS: fix path where screenshots are saved - -0.0.6 2014/03/19 - - new contributor: Sam from sametmax.com - - Python 3.4 ready - - PEP8 compliant - - MSS: review module structure to fit the "Code Like a Pythonista: Idiomatic Python" - - MSS: refactoring of all enum_display_monitors() methods - - MSS: fix misspellings using 'codespell' tool - - MSS: better way to manage output filenames (callback) - - MSS: several fixes here and there, code refactoring - - MSS: moved into a MSS:save_img() method - - Linux: add XFCE4 support - - CLI: possibility to append '--debug' to the command line - -0.0.5 2013/11/01 - - MSS: code simplified - - Windows: few optimizations into _arrange() - -0.0.4 2013/10/31 - - Linux: use of memoization => huge time/operations gains - -0.0.3 2013/10/30 - - MSS: remove PNG filters - - MSS: remove 'ext' argument, using only PNG - - MSS: do not overwrite existing image files - - MSS: few optimizations into png() - - Linux: few optimizations into get_pixels() - -0.0.2 2013/10/21 - - new contributors: Oros, Eownis - - add support for python 3 on Windows and GNU/Linux - -0.0.1 2013/07/01 - - first release diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..12f7c9ec --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,269 @@ +# History + +See Git checking messages for full history. + +## 8.0.3 (2023/xx/xx) +- now PEP 561 compatible +- include more files in sdist +- remove `venv` files from sdist +- use markdown for the README, and changelogs +- :heart: contributors: @mgorny + +## 8.0.2 (2023/04/09) +- fixed `SetuptoolsDeprecationWarning`: Installing 'XXX' as data is deprecated, please list it in packages +- CLI: fixed arguments handling + +## 8.0.1 (2023/04/09) +- MSS: ensure `--with-cursor`, and `with_cursor` argument & attribute, are simple NOOP on platforms not supporting the feature +- CLI: do not raise a ScreenShotError when `-q`, or `--quiet`, is used but return ` +- tests: fixed `test_entry_point()` with multiple monitors having the same resolution + +## 8.0.0 (2023/04/09) +- removed support for Python 3.6 +- removed support for Python 3.7 +- MSS: fixed PEP 484 prohibits implicit Optional +- MSS: the whole source code was migrated to PEP 570 (Python positional-only parameters) +- Linux: reset the X server error handler on exit to prevent issues with Tk/Tkinter (fixes #220) +- Linux: refactored how internal handles are stored to fixed issues with multiple X servers (fixes #210) +- Linux: removed side effects when leaving the context manager, resources are all freed (fixes #210) +- Linux: added mouse support (related to #55) +- CLI: added `--with-cursor` argument +- tests: added PyPy 3.9, removed tox, and improved GNU/Linux coverage +- :heart: contributors: @zorvios + +## 7.0.1 (2022/10/27) +- fixed the wheel package + +## 7.0.0 (2022/10/27) +- added support for Python 3.11 +- added support for Python 3.10 +- removed support for Python 3.5 +- MSS: modernized the code base (types, `f-string`, ran `isort` & `black`) (closes #101) +- MSS: fixed several Sourcery issues +- MSS: fixed typos here, and there +- doc: fixed an error when building the documentation + +## 6.1.0 (2020/10/31) +- MSS: reworked how C functions are initialised +- Mac: reduce the number of function calls +- Mac: support macOS Big Sur (fixes #178) +- tests: expand Python versions to 3.9 and 3.10 +- tests: fixed macOS intepreter not found on Travis-CI +- tests: fixed `test_entry_point()` when there are several monitors + +## 6.0.0 (2020/06/30) +- removed usage of deprecated `license_file` option for `license_files` +- fixed flake8 usage in pre-commit +- the module is now available on conda (closes #170) +- MSS: the implementation is now thread-safe on all OSes (fixes #169) +- Linux: better handling of the Xrandr extension (fixes #168) +- tests: fixed a random bug on `test_grab_with_tuple_percents()` (fixes #142) + +## 5.1.0 (2020/04/30) +- produce wheels for Python 3 only +- MSS: renamed again `MSSMixin` to `MSSBase`, now derived from `abc.ABCMeta` +- tools: force write of file when saving a PNG file +- tests: fixed tests on macOS with Retina display +- Windows: fixed multi-thread safety (fixes #150) +- :heart: contributors: @narumishi + +## 5.0.0 (2019/12/31) +- removed support for Python 2.7 +- MSS: improve type annotations and add CI check +- MSS: use `__slots__` for better performances +- MSS: better handle resources to prevent leaks +- MSS: improve monitors finding +- Windows: use our own instances of `GDI32` and `User32` DLLs +- doc: add `project_urls` to `setup.cfg` +- doc: add an example using the multiprocessing module (closes #82) +- tests: added regression tests for #128 and #135 +- tests: move tests files into the package +- :heart: contributors: @hugovk, @foone, @SergeyKalutsky + +## 4.0.2 (2019/02/23) +- Windows: ignore missing `SetProcessDPIAware()` on Window XP (fixes #109) +- :heart: contributors: @foone + +## 4.0.1 (2019/01/26) +- Linux: fixed several Xlib functions signature (fixes #92) +- Linux: improve monitors finding by a factor of 44 + +## 4.0.0 (2019/01/11) +- MSS: remove use of `setup.py` for `setup.cfg` +- MSS: renamed `MSSBase` to `MSSMixin` in `base.py` +- MSS: refactor ctypes `argtype`, `restype` and `errcheck` setup (fixes #84) +- Linux: ensure resources are freed in `grab()` +- Windows: avoid unnecessary class attributes +- MSS: ensure calls without context manager will not leak resources or document them (fixes #72 and #85) +- MSS: fixed Flake8 C408: Unnecessary dict call- rewrite as a literal, in `exceptions.py` +- MSS: fixed Flake8 I100: Import statements are in the wrong order +- MSS: fixed Flake8 I201: Missing newline before sections or imports +- MSS: fixed PyLint bad-super-call: Bad first argument 'Exception' given to `super()` +- tests: use tox, enable PyPy and PyPy3, add macOS and Windows CI + +## 3.3.2 (2018/11/20) +- MSS: do monitor detection in MSS constructor (fixes #79) +- MSS: specify compliant Python versions for pip install +- tests: enable Python 3.7 +- tests: fixed `test_entry_point()` with multiple monitors +- :heart: contributors: @hugovk, @andreasbuhr + +## 3.3.1 (2018/09/22) +- Linux: fixed a memory leak introduced with 7e8ae5703f0669f40532c2be917df4328bc3985e (fixes #72) +- doc: add the download statistics badge + +## 3.3.0 (2018/09/04) +- Linux: add an error handler for the XServer to prevent interpreter crash (fixes #61) +- MSS: fixed a `ResourceWarning`: unclosed file in `setup.py` +- tests: fixed a `ResourceWarning`: unclosed file +- doc: fixed a typo in `Screenshot.pixel()` method (thanks to @mchlnix) +- big code clean-up using `black` + +## 3.2.1 (2018/05/21) +- Windows: enable Hi-DPI awareness +- :heart: contributors: @FoxRow + +## 3.2.0 (2018/03/22) +- removed support for Python 3.4 +- MSS: add the `Screenshot.bgra` attribute +- MSS: speed-up grabbing on the 3 platforms +- tools: add PNG compression level control to `to_png()` +- tests: add `leaks.py` and `benchmarks.py` for manual testing +- doc: add an example about capturing part of the monitor 2 +- doc: add an example about computing BGRA values to RGB + +## 3.1.2 (2018/01/05) +- removed support for Python 3.3 +- MSS: possibility to get the whole PNG raw bytes +- Windows: capture all visible windows +- doc: improvements and fixes (fixes #37) +- CI: build the documentation + +## 3.1.1 (2017/11/27) +- MSS: add the `mss` entry point + +## 3.1.0 (2017/11/16) +- MSS: add more way of customization to the output argument of `save()` +- MSS: possibility to use custom class to handle screen shot data +- Mac: properly support all display scaling and resolutions (fixes #14, #19, #21, #23) +- Mac: fixed memory leaks (fixes #24) +- Linux: handle bad display value +- Windows: take into account zoom factor for high-DPI displays (fixes #20) +- doc: several fixes (fixes #22) +- tests: a lot of tests added for better coverage +- add the 'Say Thanks' button +- :heart: contributors: @karanlyons + +## 3.0.1 (2017/07/06) +- fixed examples links + +## 3.0.0 (2017/07/06) +- big refactor, introducing the `ScreenShot` class +- MSS: add Numpy array interface support to the `Screenshot` class +- doc: add OpenCV/Numpy, PIL pixels, FPS + +## 2.0.22 2017/04/29 +- MSS: better use of exception mechanism +- Linux: use of hasattr to prevent Exception on early exit +- Mac: take into account extra black pixels added when screen with is not divisible by 16 (fixes #14) +- doc: add an example to capture only a part of the screen +- :heart: contributors: David Becker, @redodo + +## 2.0.18 2016/12/03 +- change license to MIT +- MSS: add type hints +- MSS: remove unused code (reported by `Vulture`) +- Linux: remove MSS library +- Linux: insanely fast using only ctypes +- Linux: skip unused monitors +- Linux: use `errcheck` instead of deprecated `restype` with callable (fixes #11) +- Linux: fixed security issue (reported by Bandit) +- doc: add documentation (fixes #10) +- tests: add tests and use Travis CI (fixes #9) +- :heart: contributors: @cycomanic + +## 2.0.0 (2016/06/04) +- add issue and pull request templates +- split the module into several files +- MSS: a lot of code refactor and optimizations +- MSS: rename `save_img()` to `to_png()` +- MSS: `save()`: replace `screen` argument by `mon` +- Mac: get rid of the `PyObjC` module, 100% ctypes +- Linux: prevent segfault when `DISPLAY` is set but no X server started +- Linux: prevent segfault when Xrandr is not loaded +- Linux: `get_pixels()` insanely fast, use of MSS library (C code) +- Windows: screen shot not correct on Windows 8 (fixes #6) + +## 1.0.2 (2016/04/22) +- MSS: fixed non existent alias + +## 1.0.1 (2016/04/22) +- MSS: `libpng` warning (ignoring bad filter type) (fixes #7) + +## 1.0.0 (2015/04/16) +- Python 2.6 to 3.5 ready +- MSS: code purgation and review, no more debug information +- MSS: add a shortcut to take automatically use the proper `MSS` class (fixes #5) +- MSS: few optimizations into `save_img()` +- Darwin: remove rotation from information returned by `enum_display_monitors()` +- Linux: fixed `object has no attribute 'display' into __del__` +- Linux: use of `XDestroyImage()` instead of `XFree()` +- Linux: optimizations of `get_pixels()` +- Windows: huge optimization of `get_pixels()` +- CLI: delete `--debug` argument + +## 0.1.1 (2015/04/10) +- MSS: little code review +- Linux: fixed monitor count +- tests: remove `test-linux` binary +- doc: add `doc/TESTING` +- doc: remove Bonus section from README + +## 0.1.0 (2015/04/10) +- MSS: fixed code with `YAPF` tool +- Linux: fully functional using Xrandr library +- Linux: code purgation (no more XML files to parse) +- doc: better tests and examples + +## 0.0.8 (2015/02/04) +- MSS: filename's dir is not used when saving (fixes #3) +- MSS: fixed flake8 error: E713 test for membership should be 'not in' +- MSS: raise an exception for unimplemented methods +- Windows: robustness to `MSSWindows.get_pixels` (fixes #4) +- :heart: contributors: @sergey-vin, @thehesiod + +## 0.0.7 (2014/03/20) +- MSS: fixed path where screenshots are saved + +## 0.0.6 (2014/03/19) +- Python 3.4 ready +- PEP8 compliant +- MSS: review module structure to fit the "Code Like a Pythonista: Idiomatic Python" +- MSS: refactoring of all `enum_display_monitors()` methods +- MSS: fixed misspellings using `codespell` tool +- MSS: better way to manage output filenames (callback) +- MSS: several fixes here and there, code refactoring +- Linux: add XFCE4 support +- CLI: possibility to append `--debug` to the command line +- :heart: contributors: @sametmax + +## 0.0.5 (2013/11/01) +- MSS: code simplified +- Windows: few optimizations into `_arrange()` + +## 0.0.4 (2013/10/31) +- Linux: use of memoization => huge time/operations gains + +## 0.0.3 (2013/10/30) +- MSS: removed PNG filters +- MSS: removed `ext` argument, using only PNG +- MSS: do not overwrite existing image files +- MSS: few optimizations into `png()` +- Linux: few optimizations into `get_pixels()` + +## 0.0.2 (2013/10/21) +- added support for python 3 on Windows and GNU/Linux +- :heart: contributors: Oros, Eownis + +## 0.0.1 (2013/07/01) +- first release diff --git a/CHANGES.md b/CHANGES.md new file mode 100644 index 00000000..6038fdd7 --- /dev/null +++ b/CHANGES.md @@ -0,0 +1,188 @@ +# Technical Changes +## 8.0.0 (2023-04-09) + +### base.py +- Added `compression_level=6` keyword argument to `MSS.__init__()` +- Added `display=None` keyword argument to `MSS.__init__()` +- Added `max_displays=32` keyword argument to `MSS.__init__()` +- Added `with_cursor=False` keyword argument to `MSS.__init__()` +- Added `MSS.with_cursor` attribute + +### linux.py +- Added `MSS.close()` +- Moved `MSS.__init__()` keyword arguments handling to the base class +- Renamed `error_handler()` function to `__error_handler()` +- Renamed `_validate()` function to `___validate()` +- Renamed `MSS.has_extension()` method to `_is_extension_enabled()` +- Removed `ERROR` namespace +- Removed `MSS.drawable` attribute +- Removed `MSS.root` attribute +- Removed `MSS.get_error_details()` method. Use `ScreenShotError.details` attribute instead. + +## 6.1.0 (2020-10-31) + +### darwin.py +- Added `CFUNCTIONS` + +### linux.py +- Added `CFUNCTIONS` + +### windows.py +- Added `CFUNCTIONS` +- Added `MONITORNUMPROC` +- Removed `MSS.monitorenumproc`. Use `MONITORNUMPROC` instead. + +## 6.0.0 (2020-06-30) + +### base.py +- Added `lock` +- Added `MSS._grab_impl()` (abstract method) +- Added `MSS._monitors_impl()` (abstract method) +- `MSS.grab()` is no more an abstract method +- `MSS.monitors` is no more an abstract property + +### darwin.py +- Renamed `MSS.grab()` to `MSS._grab_impl()` +- Renamed `MSS.monitors` to `MSS._monitors_impl()` + +### linux.py +- Added `MSS.has_extension()` +- Removed `MSS.display` +- Renamed `MSS.grab()` to `MSS._grab_impl()` +- Renamed `MSS.monitors` to `MSS._monitors_impl()` + +### windows.py +- Removed `MSS._lock` +- Renamed `MSS.srcdc_dict` to `MSS._srcdc_dict` +- Renamed `MSS.grab()` to `MSS._grab_impl()` +- Renamed `MSS.monitors` to `MSS._monitors_impl()` + +## 5.1.0 (2020-04-30) + +### base.py +- Renamed back `MSSMixin` class to `MSSBase` +- `MSSBase` is now derived from `abc.ABCMeta` +- `MSSBase.monitor` is now an abstract property +- `MSSBase.grab()` is now an abstract method + +### windows.py +- Replaced `MSS.srcdc` with `MSS.srcdc_dict` + +## 5.0.0 (2019-12-31) + +### darwin.py +- Added `MSS.__slots__` + +### linux.py +- Added `MSS.__slots__` +- Deleted `MSS.close()` +- Deleted `LAST_ERROR` constant. Use `ERROR` namespace instead, specially the `ERROR.details` attribute. + +### models.py +- Added `Monitor` +- Added `Monitors` +- Added `Pixel` +- Added `Pixels` +- Added `Pos` +- Added `Size` + +### screenshot.py +- Added `ScreenShot.__slots__` +- Removed `Pos`. Use `models.Pos` instead. +- Removed `Size`. Use `models.Size` instead. + +### windows.py +- Added `MSS.__slots__` +- Deleted `MSS.close()` + +## 4.0.1 (2019-01-26) + +### linux.py +- Removed use of `MSS.xlib.XDefaultScreen()` +4.0.0 (2019-01-11) + +### base.py +- Renamed `MSSBase` class to `MSSMixin` + +### linux.py +- Renamed `MSS.__del__()` method to `MSS.close()` +- Deleted `MSS.last_error` attribute. Use `LAST_ERROR` constant instead. +- Added `validate()` function +- Added `MSS.get_error_details()` method + +### windows.py +- Renamed `MSS.__exit__()` method to `MSS.close()` + +## 3.3.0 (2018-09-04) + +### exception.py +- Added `details` attribute to `ScreenShotError` exception. Empty dict by default. + +### linux.py +- Added `error_handler()` function + +## 3.2.1 (2018-05-21) + +### windows.py +- Removed `MSS.scale_factor` property +- Removed `MSS.scale()` method + +## 3.2.0 (2018-03-22) + +### base.py +- Added `MSSBase.compression_level` attribute + +### linux.py +- Added `MSS.drawable` attribute + +### screenshot.py +- Added `Screenshot.bgra` attribute + +### tools.py +- Changed signature of `to_png(data, size, output=None)` to `to_png(data, size, level=6, output=None)`. `level` is the Zlib compression level. + +## 3.1.2 (2018-01-05) + +### tools.py +- Changed signature of `to_png(data, size, output)` to `to_png(data, size, output=None)`. If `output` is `None`, the raw PNG bytes will be returned. + +## 3.1.1 (2017-11-27) + +### \_\_main\_\_.py +- Added `args` argument to `main()` + +### base.py +- Moved `ScreenShot` class to `screenshot.py` + +### darwin.py +- Added `CGPoint.__repr__()` function +- Added `CGRect.__repr__()` function +- Added `CGSize.__repr__()` function +- Removed `get_infinity()` function + +### windows.py +- Added `MSS.scale()` method +- Added `MSS.scale_factor` property + +## 3.0.0 (2017-07-06) + +### base.py +- Added the `ScreenShot` class containing data for a given screen shot (support the Numpy array interface [`ScreenShot.__array_interface__`]) +- Added `shot()` method to `MSSBase`. It takes the same arguments as the `save()` method. +- Renamed `get_pixels` to `grab`. It now returns a `ScreenShot` object. +- Moved `to_png` method to `tools.py`. It is now a simple function. +- Removed `enum_display_monitors()` method. Use `monitors` property instead. +- Removed `monitors` attribute. Use `monitors` property instead. +- Removed `width` attribute. Use `ScreenShot.size[0]` attribute or `ScreenShot.width` property instead. +- Removed `height` attribute. Use `ScreenShot.size[1]` attribute or `ScreenShot.height` property instead. +- Removed `image`. Use the `ScreenShot.raw` attribute or `ScreenShot.rgb` property instead. +- Removed `bgra_to_rgb()` method. Use `ScreenShot.rgb` property instead. + +### darwin.py +- Removed `_crop_width()` method. Screen shots are now using the width set by the OS (rounded to 16). + +### exception.py +- Renamed `ScreenshotError` class to `ScreenShotError` + +### tools.py +- Changed signature of `to_png(data, monitor, output)` to `to_png(data, size, output)` where `size` is a `tuple(width, height)` diff --git a/CHANGES.rst b/CHANGES.rst deleted file mode 100644 index 8050203b..00000000 --- a/CHANGES.rst +++ /dev/null @@ -1,249 +0,0 @@ -8.0.0 (2023-04-09) -================== - -base.py -------- -- Added ``compression_level=6`` keyword argument to ``MSS.__init__()`` -- Added ``display=None`` keyword argument to ``MSS.__init__()`` -- Added ``max_displays=32`` keyword argument to ``MSS.__init__()`` -- Added ``with_cursor=False`` keyword argument to ``MSS.__init__()`` -- Added ``MSS.with_cursor`` attribute - -linux.py --------- -- Added ``MSS.close()`` -- Moved ``MSS.__init__()`` keyword arguments handling to the base class -- Renamed ``error_handler()`` function to ``__error_handler()`` -- Renamed ``_validate()`` function to ``___validate()`` -- Renamed ``MSS.has_extension()`` method to ``_is_extension_enabled()`` -- Removed ``ERROR`` namespace -- Removed ``MSS.drawable`` attribute -- Removed ``MSS.root`` attribute -- Removed ``MSS.get_error_details()`` method. Use ``ScreenShotError.details`` attribute instead. - - -6.1.0 (2020-10-31) -================== - -darwin.py ---------- -- Added ``CFUNCTIONS`` - -linux.py --------- -- Added ``CFUNCTIONS`` - -windows.py ----------- -- Added ``CFUNCTIONS`` -- Added ``MONITORNUMPROC`` -- Removed ``MSS.monitorenumproc``. Use ``MONITORNUMPROC`` instead. - - -6.0.0 (2020-06-30) -================== - -base.py -------- -- Added ``lock`` -- Added ``MSS._grab_impl()`` (abstract method) -- Added ``MSS._monitors_impl()`` (abstract method) -- ``MSS.grab()`` is no more an abstract method -- ``MSS.monitors`` is no more an abstract property - -darwin.py ---------- -- Renamed ``MSS.grab()`` to ``MSS._grab_impl()`` -- Renamed ``MSS.monitors`` to ``MSS._monitors_impl()`` - -linux.py --------- -- Added ``MSS.has_extension()`` -- Removed ``MSS.display`` -- Renamed ``MSS.grab()`` to ``MSS._grab_impl()`` -- Renamed ``MSS.monitors`` to ``MSS._monitors_impl()`` - -windows.py ----------- -- Removed ``MSS._lock`` -- Renamed ``MSS.srcdc_dict`` to ``MSS._srcdc_dict`` -- Renamed ``MSS.grab()`` to ``MSS._grab_impl()`` -- Renamed ``MSS.monitors`` to ``MSS._monitors_impl()`` - - -5.1.0 (2020-04-30) -================== - -base.py -------- -- Renamed back ``MSSMixin`` class to ``MSSBase`` -- ``MSSBase`` is now derived from ``abc.ABCMeta`` -- ``MSSBase.monitor`` is now an abstract property -- ``MSSBase.grab()`` is now an abstract method - -windows.py ----------- -- Replaced ``MSS.srcdc`` with ``MSS.srcdc_dict`` - - -5.0.0 (2019-12-31) -================== - -darwin.py ---------- -- Added ``MSS.__slots__`` - -linux.py --------- -- Added ``MSS.__slots__`` -- Deleted ``MSS.close()`` -- Deleted ``LAST_ERROR`` constant. Use ``ERROR`` namespace instead, specially the ``ERROR.details`` attribute. - -models.py ---------- -- Added ``Monitor`` -- Added ``Monitors`` -- Added ``Pixel`` -- Added ``Pixels`` -- Added ``Pos`` -- Added ``Size`` - -screenshot.py -------------- -- Added `ScreenShot.__slots__` -- Removed ``Pos``. Use ``models.Pos`` instead. -- Removed ``Size``. Use ``models.Size`` instead. - -windows.py ----------- -- Added ``MSS.__slots__`` -- Deleted ``MSS.close()`` - - -4.0.1 (2019-01-26) -================== - -linux.py --------- -- Removed use of ``MSS.xlib.XDefaultScreen()`` - - -4.0.0 (2019-01-11) -================== - -base.py -------- -- Renamed ``MSSBase`` class to ``MSSMixin`` - -linux.py --------- -- Renamed ``MSS.__del__()`` method to ``MSS.close()`` -- Deleted ``MSS.last_error`` attribute. Use ``LAST_ERROR`` constant instead. -- Added ``validate()`` function -- Added ``MSS.get_error_details()`` method - -windows.py ----------- -- Renamed ``MSS.__exit__()`` method to ``MSS.close()`` - - -3.3.0 (2018-09-04) -================== - -exception.py ------------- -- Added ``details`` attribute to ``ScreenShotError`` exception. Empty dict by default. - -linux.py --------- -- Added ``error_handler()`` function - - -3.2.1 (2018-05-21) -================== - -windows.py ----------- -- Removed ``MSS.scale_factor`` property -- Removed ``MSS.scale()`` method - - -3.2.0 (2018-03-22) -================== - -base.py -------- -- Added ``MSSBase.compression_level`` attribute - -linux.py --------- -- Added ``MSS.drawable`` attribute - -screenshot.py -------------- -- Added ``Screenshot.bgra`` attribute - -tools.py --------- -- Changed signature of ``to_png(data, size, output=None)`` to ``to_png(data, size, level=6, output=None)``. ``level`` is the Zlib compression level. - - -3.1.2 (2018-01-05) -================== - -tools.py --------- -- Changed signature of ``to_png(data, size, output)`` to ``to_png(data, size, output=None)``. If ``output`` is ``None``, the raw PNG bytes will be returned. - - -3.1.1 (2017-11-27) -================== - -__main__.py ------------ -- Added ``args`` argument to ``main()`` - -base.py -------- -- Moved ``ScreenShot`` class to ``screenshot.py`` - -darwin.py ---------- -- Added ``CGPoint.__repr__()`` function -- Added ``CGRect.__repr__()`` function -- Added ``CGSize.__repr__()`` function -- Removed ``get_infinity()`` function - -windows.py ----------- -- Added ``MSS.scale()`` method -- Added ``MSS.scale_factor`` property - - -3.0.0 (2017-07-06) -================== - -base.py -------- -- Added the ``ScreenShot`` class containing data for a given screen shot (support the Numpy array interface [``ScreenShot.__array_interface__``]) -- Added ``shot()`` method to ``MSSBase``. It takes the same arguments as the ``save()`` method. -- Renamed ``get_pixels`` to ``grab``. It now returns a ``ScreenShot`` object. -- Moved ``to_png`` method to ``tools.py``. It is now a simple function. -- Removed ``enum_display_monitors()`` method. Use ``monitors`` property instead. -- Removed ``monitors`` attribute. Use ``monitors`` property instead. -- Removed ``width`` attribute. Use ``ScreenShot.size[0]`` attribute or ``ScreenShot.width`` property instead. -- Removed ``height`` attribute. Use ``ScreenShot.size[1]`` attribute or ``ScreenShot.height`` property instead. -- Removed ``image``. Use the ``ScreenShot.raw`` attribute or ``ScreenShot.rgb`` property instead. -- Removed ``bgra_to_rgb()`` method. Use ``ScreenShot.rgb`` property instead. - -darwin.py ---------- -- Removed ``_crop_width()`` method. Screen shots are now using the width set by the OS (rounded to 16). - -exception.py ------------- -- Renamed ``ScreenshotError`` class to ``ScreenShotError`` - -tools.py --------- -- Changed signature of ``to_png(data, monitor, output)`` to ``to_png(data, size, output)`` where ``size`` is a ``tuple(width, height)`` diff --git a/CONTRIBUTORS b/CONTRIBUTORS deleted file mode 100644 index 4cc26fb4..00000000 --- a/CONTRIBUTORS +++ /dev/null @@ -1,56 +0,0 @@ -# Many thanks to all those who helped :) -# (sorted alphabetically) - -# Nickname or fullname [URL] [URL2] [URLN] -# - major contribution -# - major contribution 2 -# - major contribution N - -Alexander 'thehesiod' Mohr [https://github.com/thehesiod] - - Windows: robustness to MSS.get_pixels() - -Andreas Buhr [https://www.andreasbuhr.de] - - Bugfix for multi-monitor detection - -Boutallaka 'zorvios' Yassir [https://github.com/zorvios] - - GNU/Linux: Mouse support - -bubulle [http://indexerror.net/user/bubulle] - - Windows: efficiency of MSS.get_pixels() - -Condé 'Eownis' Titouan [https://titouan.co] - - MacOS X tester - -David Becker [https://davide.me] and redodo [https://github.com/redodo] - - Mac: Take into account extra black pixels added when screen with is not divisible by 16 - -Hugo van Kemenade [https://github.com/hugovk] - - Drop support for legacy Python 2.7 - -Jochen 'cycomanic' Schroeder [https://github.com/cycomanic] - - GNU/Linux: use errcheck instead of deprecated restype with callable, for enum_display_monitors() - -Karan Lyons [https://karanlyons.com] [https://github.com/karanlyons] - - MacOS: Proper support for display scaling - -narumi [https://github.com/narumishi] - - Windows: fix multi-thread unsafe - -Oros [https://ecirtam.net] - - GNU/Linux tester - -Ryan Fox ryan@foxrow.com [https://foxrow.com] - - Windows fullscreen shots on HiDPI screens - -Sam [http://sametmax.com] [https://github.com/sametmax] - - code review and advices - - the factory - -sergey-vin [https://github.com/sergey-vin] - - bug report - -yoch [http://indexerror.net/user/yoch] - - Windows: efficiency of MSS.get_pixels() - -Wagoun - - equipment loan (Macbook Pro) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md new file mode 100644 index 00000000..fcf2810d --- /dev/null +++ b/CONTRIBUTORS.md @@ -0,0 +1,18 @@ +# Contributors + +The full list can be found here: https://github.com/BoboTiG/python-mss/graphs/contributors + +That document is mostly useful for users without a GitHub account (sorted alphabetically): + +- [bubulle](http://indexerror.net/user/bubulle) + - Windows: efficiency of MSS.get_pixels() +- [Condé 'Eownis' Titouan](https://titouan.co) + - MacOS X tester +- [David Becker](https://davide.me) + - Mac: Take into account extra black pixels added when screen with is not divisible by 16 +- [Oros](https://ecirtam.net) + - GNU/Linux tester +- [yoch](http://indexerror.net/user/yoch) + - Windows: efficiency of `MSS.get_pixels()` +- Wagoun + - equipment loan (Macbook Pro) diff --git a/LICENSE b/LICENSE.txt similarity index 100% rename from LICENSE rename to LICENSE.txt diff --git a/MANIFEST.in b/MANIFEST.in index d9e645ae..707c6e97 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,4 +1,10 @@ -# Include test files, documentation sources and data +include CHANGELOG.md +include CHANGES.md +include CONTRIBUTORS.md +include LICENSE.txt +include README.md +include dev-requirements.txt include mss/tests/*.py +include mss/py.typed recursive-include docs/source * recursive-include mss/tests/res * diff --git a/README.md b/README.md new file mode 100644 index 00000000..50c0d70c --- /dev/null +++ b/README.md @@ -0,0 +1,54 @@ +# Python MSS + +[![PyPI version](https://badge.fury.io/py/mss.svg)](https://badge.fury.io/py/mss) +[![Anaconda version](https://anaconda.org/conda-forge/python-mss/badges/version.svg)](https://anaconda.org/conda-forge/python-mss) +[![Tests workflow](https://github.com/BoboTiG/python-mss/actions/workflows/tests.yml/badge.svg?branch=master)](https://github.com/BoboTiG/python-mss/actions/workflows/tests.yml) +[![Downloads](https://static.pepy.tech/personalized-badge/mss?period=total&units=international_system&left_color=black&right_color=orange&left_text=Downloads)](https://pepy.tech/project/mss) + +```python +from mss import mss + +# The simplest use, save a screen shot of the 1st monitor +with mss() as sct: + sct.shot() +``` + +An ultra fast cross-platform multiple screenshots module in pure python using ctypes. + +- **Python 3.8+**, PEP8 compliant, no dependency, thread-safe; +- very basic, it will grab one screen shot by monitor or a screen shot of all monitors and save it to a PNG file; +- but you can use PIL and benefit from all its formats (or add yours directly); +- integrate well with Numpy and OpenCV; +- it could be easily embedded into games and other software which require fast and platform optimized methods to grab screen shots (like AI, Computer Vision); +- get the [source code on GitHub](https://github.com/BoboTiG/python-mss); +- learn with a [bunch of examples](https://python-mss.readthedocs.io/examples.html); +- you can [report a bug](https://github.com/BoboTiG/python-mss/issues); +- need some help? Use the tag *python-mss* on [StackOverflow](https://stackoverflow.com/questions/tagged/python-mss); +- and there is a [complete, and beautiful, documentation](https://python-mss.readthedocs.io) :) +- **MSS** stands for Multiple Screen Shots; + + +## Installation + +You can install it with pip: + +```shell +python -m pip install -U --user mss +``` + +Or you can install it with conda: + +```shell +conda install -c conda-forge python-mss +``` + +## Maintenance + +For the maintainers, here are commands to upload a new release: + +```shell +rm -rf build dist +python -m build --sdist --wheel +twine check dist/* +twine upload dist/* +``` \ No newline at end of file diff --git a/README.rst b/README.rst deleted file mode 100644 index 86d85ecd..00000000 --- a/README.rst +++ /dev/null @@ -1,57 +0,0 @@ -Python MSS -========== - -.. image:: https://badge.fury.io/py/mss.svg - :target: https://pypi.org/project/mss/ -.. image:: https://anaconda.org/conda-forge/python-mss/badges/version.svg - :target: https://anaconda.org/conda-forge/python-mss -.. image:: https://github.com/BoboTiG/python-mss/actions/workflows/tests.yml/badge.svg?branch=master - :target: https://github.com/BoboTiG/python-mss/actions/workflows/tests.yml -.. image:: https://static.pepy.tech/personalized-badge/mss?period=total&units=international_system&left_color=black&right_color=orange&left_text=Downloads - :target: https://pepy.tech/project/mss - - -.. code-block:: python - - from mss import mss - - # The simplest use, save a screen shot of the 1st monitor - with mss() as sct: - sct.shot() - - -An ultra fast cross-platform multiple screenshots module in pure python using ctypes. - -- **Python 3.8+** and PEP8 compliant, no dependency, thread-safe; -- very basic, it will grab one screen shot by monitor or a screen shot of all monitors and save it to a PNG file; -- but you can use PIL and benefit from all its formats (or add yours directly); -- integrate well with Numpy and OpenCV; -- it could be easily embedded into games and other software which require fast and platform optimized methods to grab screen shots (like AI, Computer Vision); -- get the `source code on GitHub `_; -- learn with a `bunch of examples `_; -- you can `report a bug `_; -- need some help? Use the tag *python-mss* on `StackOverflow `_; -- and there is a `complete, and beautiful, documentation `_ :) -- **MSS** stands for Multiple Screen Shots; - - -Installation ------------- - -You can install it with pip:: - - python -m pip install -U --user mss - -Or you can install it with conda:: - - conda install -c conda-forge python-mss - -Maintenance ------------ - -For the maintainers, here are commands to upload a new release:: - - rm -rf build dist - python -m build --sdist --wheel - twine check dist/* - twine upload dist/* diff --git a/mss/py.typed b/mss/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/setup.cfg b/setup.cfg index d00e37d3..9c5918d8 100644 --- a/setup.cfg +++ b/setup.cfg @@ -4,8 +4,8 @@ version = 8.0.3 author = Mickaël 'Tiger-222' Schoentgen author_email = contact@tiger-222.fr description = An ultra fast cross-platform multiple screenshots module in pure python using ctypes. -long_description = file: README.rst -long_description_content_type = text/x-rst +long_description = file: README.md +long_description_content_type = text/markdown url = https://github.com/BoboTiG/python-mss home_page = https://pypi.org/project/mss/ project_urls = From 7f912723ad25314dc3473cf6b1ee8e434f9c3192 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Mon, 10 Apr 2023 15:06:49 +0200 Subject: [PATCH 106/280] Linux: restore the original X error handler in `.close()` (changelog + adjustments) --- CHANGELOG.md | 5 ++- CHANGES.md | 1 + mss/linux.py | 71 ++++++++++++++++++-------------- mss/tests/test_implementation.py | 4 +- 4 files changed, 46 insertions(+), 35 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 12f7c9ec..1b54cd0d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,10 +4,11 @@ See Git checking messages for full history. ## 8.0.3 (2023/xx/xx) - now PEP 561 compatible -- include more files in sdist +- include more files in sdist (#240) - remove `venv` files from sdist - use markdown for the README, and changelogs -- :heart: contributors: @mgorny +- Linux: restore the original X error handler in `.close()` (#241) +- :heart: contributors: @mgorny, @relent95 ## 8.0.2 (2023/04/09) - fixed `SetuptoolsDeprecationWarning`: Installing 'XXX' as data is deprecated, please list it in packages diff --git a/CHANGES.md b/CHANGES.md index 6038fdd7..3b5f41c2 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,4 +1,5 @@ # Technical Changes + ## 8.0.0 (2023-04-09) ### base.py diff --git a/mss/linux.py b/mss/linux.py index 83613c5c..b84af026 100644 --- a/mss/linux.py +++ b/mss/linux.py @@ -55,12 +55,12 @@ class Event(Structure): _fields_ = [ ("type", c_int), - ("display", POINTER(Display)), - ("serial", c_ulong), - ("error_code", c_ubyte), - ("request_code", c_ubyte), - ("minor_code", c_ubyte), - ("resourceid", c_void_p), + ("display", POINTER(Display)), # Display the event was read from + ("serial", c_ulong), # serial number of failed request + ("error_code", c_ubyte), # error code of failed request + ("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 ] @@ -92,21 +92,21 @@ class XImage(Structure): """ _fields_ = [ - ("width", c_int), - ("height", c_int), - ("xoffset", c_int), - ("format", c_int), - ("data", c_void_p), - ("byte_order", c_int), - ("bitmap_unit", c_int), - ("bitmap_bit_order", c_int), - ("bitmap_pad", c_int), - ("depth", c_int), - ("bytes_per_line", c_int), - ("bits_per_pixel", c_int), - ("red_mask", c_ulong), - ("green_mask", c_ulong), - ("blue_mask", c_ulong), + ("width", c_int), # size of image + ("height", c_int), # size of image + ("xoffset", c_int), # number of pixels offset in X direction + ("format", c_int), # XYBitmap, XYPixmap, ZPixmap + ("data", c_void_p), # pointer to image data + ("byte_order", c_int), # data byte order, LSBFirst, MSBFirst + ("bitmap_unit", c_int), # quant. of scanline 8, 16, 32 + ("bitmap_bit_order", c_int), # LSBFirst, MSBFirst + ("bitmap_pad", c_int), # 8, 16, 32 either XY or ZPixmap + ("depth", c_int), # depth of image + ("bytes_per_line", c_int), # accelarator to next line + ("bits_per_pixel", c_int), # bits per pixel (ZPixmap) + ("red_mask", c_ulong), # bits in z arrangment + ("green_mask", c_ulong), # bits in z arrangment + ("blue_mask", c_ulong), # bits in z arrangment ] @@ -268,6 +268,13 @@ def __init__(self, /, **kwargs: Any) -> None: super().__init__(**kwargs) + # Available thread-specific variables + self._handles = local() + self._handles.display = None + self._handles.drawable = None + self._handles.original_error_handler = None + self._handles.root = None + display = kwargs.get("display", b"") if not display: try: @@ -297,11 +304,9 @@ def __init__(self, /, **kwargs: Any) -> None: self._set_cfunctions() - # Install the error handler to prevent interpreter crashes: - # any error will raise a ScreenShotError exception. - self._old_error_handler = self.xlib.XSetErrorHandler(_error_handler) + # Install the error handler to prevent interpreter crashes: any error will raise a ScreenShotError exception + self._handles.original_error_handler = self.xlib.XSetErrorHandler(_error_handler) - self._handles = local() self._handles.display = self.xlib.XOpenDisplay(display) if not self._is_extension_enabled("RANDR"): @@ -315,17 +320,21 @@ def __init__(self, /, **kwargs: Any) -> None: def close(self) -> None: # Remove our error handler - # It's required when exiting MSS to prevent letting `_error_handler()` as default handler. - # Doing so would crash when using Tk/Tkinter, see issue #220. - # Interesting technical stuff can be found here: - # https://core.tcl-lang.org/tk/file?name=generic/tkError.c&ci=a527ef995862cb50 - # https://github.com/tcltk/tk/blob/b9cdafd83fe77499ff47fa373ce037aff3ae286a/generic/tkError.c - self.xlib.XSetErrorHandler(self._old_error_handler) + if self._handles.original_error_handler is not None: + # It's required when exiting MSS to prevent letting `_error_handler()` as default handler. + # Doing so would crash when using Tk/Tkinter, see issue #220. + # Interesting technical stuff can be found here: + # https://core.tcl-lang.org/tk/file?name=generic/tkError.c&ci=a527ef995862cb50 + # https://github.com/tcltk/tk/blob/b9cdafd83fe77499ff47fa373ce037aff3ae286a/generic/tkError.c + self.xlib.XSetErrorHandler(self._handles.original_error_handler) + self._handles.original_error_handler = None # Clean-up if self._handles.display is not None: self.xlib.XCloseDisplay(self._handles.display) self._handles.display = None + self._handles.drawable = None + self._handles.root = None # Also empty the error dict _ERROR.clear() diff --git a/mss/tests/test_implementation.py b/mss/tests/test_implementation.py index 5252bbb9..2de3b82f 100644 --- a/mss/tests/test_implementation.py +++ b/mss/tests/test_implementation.py @@ -168,7 +168,7 @@ def main(*args: str) -> int: main() -def test_grab_with_tuple(pixel_ratio): +def test_grab_with_tuple(pixel_ratio: int): left = 100 top = 100 right = 500 @@ -190,7 +190,7 @@ def test_grab_with_tuple(pixel_ratio): assert im.rgb == im2.rgb -def test_grab_with_tuple_percents(pixel_ratio): +def test_grab_with_tuple_percents(pixel_ratio: int): with mss(display=os.getenv("DISPLAY")) as sct: monitor = sct.monitors[1] left = monitor["left"] + monitor["width"] * 5 // 100 # 5% from the left From 1f3bac37f0c2f461a4140f03298a70cb977e3b90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Mon, 10 Apr 2023 15:38:48 +0200 Subject: [PATCH 107/280] Linux: fix `XRRCrtcInfo.width`, and `XRRCrtcInfo.height`, C types (+ docs) --- CHANGELOG.md | 1 + CHANGES.md | 5 ++++ mss/linux.py | 68 +++++++++++++++++++++++++++++----------------------- 3 files changed, 44 insertions(+), 30 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b54cd0d..1ed90ffa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ See Git checking messages for full history. - remove `venv` files from sdist - use markdown for the README, and changelogs - Linux: restore the original X error handler in `.close()` (#241) +- Linux: fixed `XRRCrtcInfo.width`, and `XRRCrtcInfo.height`, C types - :heart: contributors: @mgorny, @relent95 ## 8.0.2 (2023/04/09) diff --git a/CHANGES.md b/CHANGES.md index 3b5f41c2..94155679 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,10 @@ # Technical Changes +## 8.0.3 (2023-04-xx) + +### linux.py +- Added to `XErrorEvent` class (`Event` will be removed in v9.0.0) + ## 8.0.0 (2023-04-09) ### base.py diff --git a/mss/linux.py b/mss/linux.py index b84af026..c28245a2 100644 --- a/mss/linux.py +++ b/mss/linux.py @@ -44,10 +44,11 @@ class Display(Structure): """ 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 """ -class Event(Structure): +class XErrorEvent(Structure): """ XErrorEvent to debug eventual errors. https://tronche.com/gui/x/xlib/event-handling/protocol-errors/default-handlers.html @@ -63,6 +64,9 @@ class Event(Structure): ("resourceid", c_void_p), # resource ID ] +# TODO: remove in v9.0.0 +Event = XErrorEvent + class XFixesCursorImage(Structure): """ @@ -111,14 +115,17 @@ class XImage(Structure): class XRRCrtcInfo(Structure): - """Structure that contains CRTC information.""" + """ + Structure that contains CRTC information. + https://gitlab.freedesktop.org/xorg/lib/libxrandr/-/blob/master/include/X11/extensions/Xrandr.h#L360 + """ _fields_ = [ ("timestamp", c_ulong), ("x", c_int), ("y", c_int), - ("width", c_int), - ("height", c_int), + ("width", c_uint), + ("height", c_uint), ("mode", c_long), ("rotation", c_int), ("noutput", c_int), @@ -130,13 +137,14 @@ class XRRCrtcInfo(Structure): class XRRModeInfo(Structure): - """Voilà, voilà.""" + """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 available outputs and associated CRTCs. + https://gitlab.freedesktop.org/xorg/lib/libxrandr/-/blob/master/include/X11/extensions/Xrandr.h#L265 """ _fields_ = [ @@ -155,29 +163,29 @@ class XWindowAttributes(Structure): """Attributes for the specified window.""" _fields_ = [ - ("x", c_int32), - ("y", c_int32), - ("width", c_int32), - ("height", c_int32), - ("border_width", c_int32), - ("depth", c_int32), - ("visual", c_ulong), - ("root", c_ulong), - ("class", c_int32), - ("bit_gravity", c_int32), - ("win_gravity", c_int32), - ("backing_store", c_int32), - ("backing_planes", c_ulong), - ("backing_pixel", c_ulong), - ("save_under", c_int32), - ("colourmap", c_ulong), - ("mapinstalled", c_uint32), - ("map_state", c_uint32), - ("all_event_masks", c_ulong), - ("your_event_mask", c_ulong), - ("do_not_propagate_mask", c_ulong), - ("override_redirect", c_int32), - ("screen", c_ulong), + ("x", c_int32), # location of window + ("y", c_int32), # location of window + ("width", c_int32), # width of window + ("height", c_int32), # height of window + ("border_width", c_int32), # border width of window + ("depth", c_int32), # depth of window + ("visual", c_ulong), # the associated visual structure + ("root", c_ulong), # root of screen containing window + ("class", c_int32), # InputOutput, InputOnly + ("bit_gravity", c_int32), # one of bit gravity values + ("win_gravity", c_int32), # one of the window gravity values + ("backing_store", c_int32), # NotUseful, WhenMapped, Always + ("backing_planes", c_ulong), # planes to be preserved if possible + ("backing_pixel", c_ulong), # value to be used when restoring planes + ("save_under", c_int32), # boolean, should bits under be saved? + ("colormap", c_ulong), # color map to be associated with window + ("mapinstalled", c_uint32), # boolean, is color map currently installed + ("map_state", c_uint32), # IsUnmapped, IsUnviewable, IsViewable + ("all_event_masks", c_ulong), # set of events all people have interest in + ("your_event_mask", c_ulong), # my event mask + ("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 ] @@ -187,8 +195,8 @@ class XWindowAttributes(Structure): _XRANDR = find_library("Xrandr") -@CFUNCTYPE(c_int, POINTER(Display), POINTER(Event)) -def _error_handler(display: Display, event: Event) -> int: +@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 From 181cf7d4787f264c178cb924b860180644a785df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Mon, 10 Apr 2023 16:40:42 +0200 Subject: [PATCH 108/280] Support Python 3.12 (#242) --- .github/workflows/tests.yml | 2 ++ setup.cfg | 10 ++++++++++ 2 files changed, 12 insertions(+) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index bb129792..9e525d8b 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -64,6 +64,8 @@ jobs: runs-on: "3.10" - name: CPython 3.11 runs-on: "3.11" + - name: CPython 3.12 + runs-on: "3.12-dev" - name: PyPy 3.9 runs-on: "pypy-3.9" steps: diff --git a/setup.cfg b/setup.cfg index 9c5918d8..69c6d3a9 100644 --- a/setup.cfg +++ b/setup.cfg @@ -19,7 +19,16 @@ license_files = platforms = Darwin, Linux, Windows classifiers = Development Status :: 5 - Production/Stable + Environment :: MacOS X + Intended Audience :: Developers + Intended Audience :: Education + Intended Audience :: End Users/Desktop + Intended Audience :: Information Technology + Intended Audience :: Science/Research License :: OSI Approved :: MIT License + Operating System :: MacOS + Operating System :: Microsoft :: Windows + Operating System :: Unix Programming Language :: Python :: Implementation :: CPython Programming Language :: Python :: Implementation :: PyPy Programming Language :: Python @@ -29,6 +38,7 @@ classifiers = Programming Language :: Python :: 3.9 Programming Language :: Python :: 3.10 Programming Language :: Python :: 3.11 + Programming Language :: Python :: 3.12 Topic :: Multimedia :: Graphics :: Capture :: Screen Capture Topic :: Software Development :: Libraries From 0c45da64442ebb795feaa8dbc3e9b02381476c40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Mon, 10 Apr 2023 16:41:14 +0200 Subject: [PATCH 109/280] doc: tweak --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ed90ffa..1329af09 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ See Git checking messages for full history. ## 8.0.3 (2023/xx/xx) +- added support for Python 3.12 - now PEP 561 compatible - include more files in sdist (#240) - remove `venv` files from sdist From 3c801ea6826eefc81d6cc5419740c481cac4bfa5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Mon, 10 Apr 2023 16:41:55 +0200 Subject: [PATCH 110/280] tests: add tests to cover #243 --- mss/tests/test_setup.py | 149 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 143 insertions(+), 6 deletions(-) diff --git a/mss/tests/test_setup.py b/mss/tests/test_setup.py index 9161fd84..1fb9c968 100644 --- a/mss/tests/test_setup.py +++ b/mss/tests/test_setup.py @@ -12,14 +12,151 @@ if platform.system().lower() != "linux": pytestmark = pytest.mark.skip -INSTALL = "python -m build --sdist --wheel".split() +SDIST = "python -m build --sdist".split() +WHEEL = "python -m build --wheel".split() CHECK = "twine check dist/*".split() -def test_wheel_python_3_only(): - """Ensure the produced wheel is Python 3 only.""" - output = str(check_output(INSTALL, stderr=STDOUT)) - text = f"Successfully built mss-{__version__}.tar.gz and mss-{__version__}-py3-none-any.whl" - assert text in output +def test_sdist(): + output = check_output(SDIST, stderr=STDOUT, text=True) + expected = f""" +creating mss-{__version__} +creating mss-{__version__}/docs +creating mss-{__version__}/docs/source +creating mss-{__version__}/docs/source/examples +creating mss-{__version__}/mss +creating mss-{__version__}/mss.egg-info +creating mss-{__version__}/mss/tests +creating mss-{__version__}/mss/tests/res +copying files to mss-{__version__}... +copying CHANGELOG.md -> mss-{__version__} +copying CHANGES.md -> mss-{__version__} +copying CODE_OF_CONDUCT.md -> mss-{__version__} +copying CONTRIBUTORS.md -> mss-{__version__} +copying LICENSE.txt -> mss-{__version__} +copying MANIFEST.in -> mss-{__version__} +copying README.md -> mss-{__version__} +copying dev-requirements.txt -> mss-{__version__} +copying setup.cfg -> mss-{__version__} +copying setup.py -> mss-{__version__} +copying docs/source/api.rst -> mss-{__version__}/docs/source +copying docs/source/conf.py -> mss-{__version__}/docs/source +copying docs/source/developers.rst -> mss-{__version__}/docs/source +copying docs/source/examples.rst -> mss-{__version__}/docs/source +copying docs/source/index.rst -> mss-{__version__}/docs/source +copying docs/source/installation.rst -> mss-{__version__}/docs/source +copying docs/source/support.rst -> mss-{__version__}/docs/source +copying docs/source/usage.rst -> mss-{__version__}/docs/source +copying docs/source/where.rst -> mss-{__version__}/docs/source +copying docs/source/examples/callback.py -> mss-{__version__}/docs/source/examples +copying docs/source/examples/custom_cls_image.py -> mss-{__version__}/docs/source/examples +copying docs/source/examples/fps.py -> mss-{__version__}/docs/source/examples +copying docs/source/examples/fps_multiprocessing.py -> mss-{__version__}/docs/source/examples +copying docs/source/examples/from_pil_tuple.py -> mss-{__version__}/docs/source/examples +copying docs/source/examples/linux_display_keyword.py -> mss-{__version__}/docs/source/examples +copying docs/source/examples/opencv_numpy.py -> mss-{__version__}/docs/source/examples +copying docs/source/examples/part_of_screen.py -> mss-{__version__}/docs/source/examples +copying docs/source/examples/part_of_screen_monitor_2.py -> mss-{__version__}/docs/source/examples +copying docs/source/examples/pil.py -> mss-{__version__}/docs/source/examples +copying docs/source/examples/pil_pixels.py -> mss-{__version__}/docs/source/examples +copying mss/__init__.py -> mss-{__version__}/mss +copying mss/__main__.py -> mss-{__version__}/mss +copying mss/base.py -> mss-{__version__}/mss +copying mss/darwin.py -> mss-{__version__}/mss +copying mss/exception.py -> mss-{__version__}/mss +copying mss/factory.py -> mss-{__version__}/mss +copying mss/linux.py -> mss-{__version__}/mss +copying mss/models.py -> mss-{__version__}/mss +copying mss/py.typed -> mss-{__version__}/mss +copying mss/screenshot.py -> mss-{__version__}/mss +copying mss/tools.py -> mss-{__version__}/mss +copying mss/windows.py -> mss-{__version__}/mss +copying mss.egg-info/PKG-INFO -> mss-{__version__}/mss.egg-info +copying mss.egg-info/SOURCES.txt -> mss-{__version__}/mss.egg-info +copying mss.egg-info/dependency_links.txt -> mss-{__version__}/mss.egg-info +copying mss.egg-info/entry_points.txt -> mss-{__version__}/mss.egg-info +copying mss.egg-info/not-zip-safe -> mss-{__version__}/mss.egg-info +copying mss.egg-info/top_level.txt -> mss-{__version__}/mss.egg-info +copying mss/tests/bench_bgra2rgb.py -> mss-{__version__}/mss/tests +copying mss/tests/bench_general.py -> mss-{__version__}/mss/tests +copying mss/tests/conftest.py -> mss-{__version__}/mss/tests +copying mss/tests/test_bgra_to_rgb.py -> mss-{__version__}/mss/tests +copying mss/tests/test_cls_image.py -> mss-{__version__}/mss/tests +copying mss/tests/test_find_monitors.py -> mss-{__version__}/mss/tests +copying mss/tests/test_get_pixels.py -> mss-{__version__}/mss/tests +copying mss/tests/test_gnu_linux.py -> mss-{__version__}/mss/tests +copying mss/tests/test_implementation.py -> mss-{__version__}/mss/tests +copying mss/tests/test_issue_220.py -> mss-{__version__}/mss/tests +copying mss/tests/test_leaks.py -> mss-{__version__}/mss/tests +copying mss/tests/test_macos.py -> mss-{__version__}/mss/tests +copying mss/tests/test_save.py -> mss-{__version__}/mss/tests +copying mss/tests/test_setup.py -> mss-{__version__}/mss/tests +copying mss/tests/test_third_party.py -> mss-{__version__}/mss/tests +copying mss/tests/test_tools.py -> mss-{__version__}/mss/tests +copying mss/tests/test_windows.py -> mss-{__version__}/mss/tests +copying mss/tests/res/monitor-1024x768.raw.zip -> mss-{__version__}/mss/tests/res +Writing mss-{__version__}/setup.cfg + """ + + print(output) + for line in expected.splitlines(): + if not (line := line.strip()): + continue + assert line in output + assert output.count("copying ") == expected.count("copying ") + assert f"Successfully built mss-{__version__}.tar.gz" in output + assert "warning" not in output.lower() + + check_call(CHECK) + + +def test_wheel(): + output = check_output(WHEEL, stderr=STDOUT, text=True) + expected = f""" +adding 'mss/__init__.py' +adding 'mss/__main__.py' +adding 'mss/base.py' +adding 'mss/darwin.py' +adding 'mss/exception.py' +adding 'mss/factory.py' +adding 'mss/linux.py' +adding 'mss/models.py' +adding 'mss/py.typed' +adding 'mss/screenshot.py' +adding 'mss/tools.py' +adding 'mss/windows.py' +adding 'mss/tests/bench_bgra2rgb.py' +adding 'mss/tests/bench_general.py' +adding 'mss/tests/conftest.py' +adding 'mss/tests/test_bgra_to_rgb.py' +adding 'mss/tests/test_cls_image.py' +adding 'mss/tests/test_find_monitors.py' +adding 'mss/tests/test_get_pixels.py' +adding 'mss/tests/test_gnu_linux.py' +adding 'mss/tests/test_implementation.py' +adding 'mss/tests/test_issue_220.py' +adding 'mss/tests/test_leaks.py' +adding 'mss/tests/test_macos.py' +adding 'mss/tests/test_save.py' +adding 'mss/tests/test_setup.py' +adding 'mss/tests/test_third_party.py' +adding 'mss/tests/test_tools.py' +adding 'mss/tests/test_windows.py' +adding 'mss/tests/res/monitor-1024x768.raw.zip' +adding 'mss-{__version__}.dist-info/METADATA' +adding 'mss-{__version__}.dist-info/WHEEL' +adding 'mss-{__version__}.dist-info/entry_points.txt' +adding 'mss-{__version__}.dist-info/top_level.txt' +adding 'mss-{__version__}.dist-info/RECORD' + """ + + print(output) + for line in expected.splitlines(): + if not (line := line.strip()): + continue + assert line in output + assert output.count("adding ") == expected.count("adding ") + assert f"Successfully built mss-{__version__}-py3-none-any.whl" in output + assert "warning" not in output.lower() check_call(CHECK) From fdbb99708243c2f68d83620186019a50b33a66cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Mon, 10 Apr 2023 20:43:47 +0200 Subject: [PATCH 111/280] Linux: clean-up --- mss/linux.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mss/linux.py b/mss/linux.py index c28245a2..fe935005 100644 --- a/mss/linux.py +++ b/mss/linux.py @@ -269,7 +269,7 @@ class MSS(MSSBase): It uses intensively the Xlib and its Xrandr extension. """ - __slots__ = {"xfixes", "xlib", "xrandr", "_handles", "_old_error_handler"} + __slots__ = {"xfixes", "xlib", "xrandr", "_handles"} def __init__(self, /, **kwargs: Any) -> None: """GNU/Linux initialisations.""" From 8e513dfb2f10ecd3e7911fdcb2702e54da266531 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Mon, 10 Apr 2023 20:47:00 +0200 Subject: [PATCH 112/280] doc: tweak --- CHANGES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 94155679..0411960f 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -3,7 +3,7 @@ ## 8.0.3 (2023-04-xx) ### linux.py -- Added to `XErrorEvent` class (`Event` will be removed in v9.0.0) +- Added `XErrorEvent` class (old `Event` class is just an alias now, and will be removed in v9.0.0) ## 8.0.0 (2023-04-09) From 2be72dc4d263fb80a6e5c2dfeb581da57fb02e13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Mon, 10 Apr 2023 20:51:43 +0200 Subject: [PATCH 113/280] tests: mark test_setup.py as xfail until #243 is fixed --- mss/tests/test_setup.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/mss/tests/test_setup.py b/mss/tests/test_setup.py index 1fb9c968..e29f2318 100644 --- a/mss/tests/test_setup.py +++ b/mss/tests/test_setup.py @@ -17,6 +17,7 @@ CHECK = "twine check dist/*".split() +@pytest.mark.xfail("Issue #243) def test_sdist(): output = check_output(SDIST, stderr=STDOUT, text=True) expected = f""" @@ -110,6 +111,7 @@ def test_sdist(): check_call(CHECK) +@pytest.mark.xfail("Issue #243) def test_wheel(): output = check_output(WHEEL, stderr=STDOUT, text=True) expected = f""" From c617a203b55cde2ee90684be93fe9a54749911fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Mon, 10 Apr 2023 20:52:44 +0200 Subject: [PATCH 114/280] doc: tweak --- docs/source/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/index.rst b/docs/source/index.rst index c3f33ffd..c42c6712 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -12,7 +12,7 @@ Welcome to Python MSS's documentation! An ultra fast cross-platform multiple screenshots module in pure python using ctypes. - - **Python 3.8+** and :pep:`8` compliant, no dependency, thread-safe; + - **Python 3.8+**, :pep:`8` compliant, no dependency, thread-safe; - very basic, it will grab one screen shot by monitor or a screen shot of all monitors and save it to a PNG file; - but you can use PIL and benefit from all its formats (or add yours directly); - integrate well with Numpy and OpenCV; From 529961d35062934ecb4f1e6bba94fd2d81337223 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Mon, 10 Apr 2023 20:54:17 +0200 Subject: [PATCH 115/280] tests(fix): Update test_setup.py --- mss/tests/test_setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mss/tests/test_setup.py b/mss/tests/test_setup.py index e29f2318..043270a8 100644 --- a/mss/tests/test_setup.py +++ b/mss/tests/test_setup.py @@ -17,7 +17,7 @@ CHECK = "twine check dist/*".split() -@pytest.mark.xfail("Issue #243) +@pytest.mark.xfail("Issue #243") def test_sdist(): output = check_output(SDIST, stderr=STDOUT, text=True) expected = f""" @@ -111,7 +111,7 @@ def test_sdist(): check_call(CHECK) -@pytest.mark.xfail("Issue #243) +@pytest.mark.xfail("Issue #243") def test_wheel(): output = check_output(WHEEL, stderr=STDOUT, text=True) expected = f""" From 9ac0c5593f5bc5d879ba5c79d87dae5dbf27d1cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Mon, 10 Apr 2023 21:12:03 +0200 Subject: [PATCH 116/280] tests(fix): Update test_setup.py --- mss/tests/test_setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mss/tests/test_setup.py b/mss/tests/test_setup.py index 043270a8..fe7e761e 100644 --- a/mss/tests/test_setup.py +++ b/mss/tests/test_setup.py @@ -17,7 +17,7 @@ CHECK = "twine check dist/*".split() -@pytest.mark.xfail("Issue #243") +@pytest.mark.xfail(True, "Issue #243") def test_sdist(): output = check_output(SDIST, stderr=STDOUT, text=True) expected = f""" @@ -111,7 +111,7 @@ def test_sdist(): check_call(CHECK) -@pytest.mark.xfail("Issue #243") +@pytest.mark.xfail(True, "Issue #243") def test_wheel(): output = check_output(WHEEL, stderr=STDOUT, text=True) expected = f""" From 4997021299ab5a3a6dfb30c43bdbb11b87d2634e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Mon, 10 Apr 2023 21:14:10 +0200 Subject: [PATCH 117/280] tests(fix): Update test_setup.py --- mss/tests/test_setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mss/tests/test_setup.py b/mss/tests/test_setup.py index fe7e761e..0a9be4be 100644 --- a/mss/tests/test_setup.py +++ b/mss/tests/test_setup.py @@ -17,7 +17,7 @@ CHECK = "twine check dist/*".split() -@pytest.mark.xfail(True, "Issue #243") +@pytest.mark.xfail(True, reason="Issue #243") def test_sdist(): output = check_output(SDIST, stderr=STDOUT, text=True) expected = f""" @@ -111,7 +111,7 @@ def test_sdist(): check_call(CHECK) -@pytest.mark.xfail(True, "Issue #243") +@pytest.mark.xfail(True, reason="Issue #243") def test_wheel(): output = check_output(WHEEL, stderr=STDOUT, text=True) expected = f""" From 431e31ebd03764ac18107b76848d76c269f4a77a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Mon, 10 Apr 2023 22:39:17 +0200 Subject: [PATCH 118/280] dev: split requirements --- MANIFEST.in | 1 + dev-requirements.txt | 9 +-------- docs/source/developers.rst | 11 ++++++----- mss/tests/test_setup.py | 3 +++ setup.cfg | 3 --- tests-requirements.txt | 6 ++++++ 6 files changed, 17 insertions(+), 16 deletions(-) create mode 100644 tests-requirements.txt diff --git a/MANIFEST.in b/MANIFEST.in index 707c6e97..b8c16d14 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -4,6 +4,7 @@ include CONTRIBUTORS.md include LICENSE.txt include README.md include dev-requirements.txt +include tests-requirements.txt include mss/tests/*.py include mss/py.typed recursive-include docs/source * diff --git a/dev-requirements.txt b/dev-requirements.txt index 589b76eb..94fe3ae8 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,16 +1,9 @@ --e . +-r tests-requirements.txt black build flake8 -flaky -pytest -pytest-cov mypy -numpy; platform_python_implementation != "pypy" -numpy==1.24.2; platform_python_implementation == "pypy" -pillow pylint sphinx twine wheel -xvfbwrapper; sys_platform == "linux" diff --git a/docs/source/developers.rst b/docs/source/developers.rst index 909d676e..c48e54ac 100644 --- a/docs/source/developers.rst +++ b/docs/source/developers.rst @@ -20,8 +20,10 @@ Dependency You will need `pytest `_:: - $ python -m pip install -U pip wheel - $ python -m pip install -r dev-requirements.txt + $ python -m venv venv + $ . venv/bin/activate + $ python -m pip install -U pip + $ python -m pip install -r tests-requirements.txt How to Test? @@ -31,14 +33,13 @@ Launch the test suit:: $ python -m pytest -This will test MSS and ensure a good code quality. - Code Quality ============ -To ensure the code is always well enough using `flake8 `_:: +To ensure the code quality is correct enough:: + $ python -m pip install -r dev-requirements.txt $ ./check.sh diff --git a/mss/tests/test_setup.py b/mss/tests/test_setup.py index 0a9be4be..f0ee3df8 100644 --- a/mss/tests/test_setup.py +++ b/mss/tests/test_setup.py @@ -12,6 +12,9 @@ if platform.system().lower() != "linux": pytestmark = pytest.mark.skip +pytest.importorskip("build") +pytest.importorskip("twine") + SDIST = "python -m build --sdist".split() WHEEL = "python -m build --wheel".split() CHECK = "twine check dist/*".split() diff --git a/setup.cfg b/setup.cfg index 69c6d3a9..025d7df9 100644 --- a/setup.cfg +++ b/setup.cfg @@ -84,6 +84,3 @@ addopts = -v --cov=mss --cov-report=term-missing - # Trait all tests as flaky by default - --force-flaky - --no-success-flaky-report diff --git a/tests-requirements.txt b/tests-requirements.txt new file mode 100644 index 00000000..fd9c0a3d --- /dev/null +++ b/tests-requirements.txt @@ -0,0 +1,6 @@ +-e . +pytest +pytest-cov +numpy +pillow +xvfbwrapper; sys_platform == "linux" From 0eade2c36a14c4283ac9f1410d98b19837ef3af3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Tue, 11 Apr 2023 09:37:19 +0200 Subject: [PATCH 119/280] dev: refine requirements --- .github/workflows/tests.yml | 8 ++++---- dev-requirements.txt | 2 -- tests-requirements.txt | 2 +- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 9e525d8b..803186eb 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -19,7 +19,7 @@ jobs: cache-dependency-path: dev-requirements.txt - name: Install dependencies run: | - python -m pip install -U pip wheel + python -m pip install -U pip python -m pip install -r dev-requirements.txt - name: Tests run: ./check.sh @@ -33,11 +33,11 @@ jobs: with: python-version: "3.x" cache: pip - cache-dependency-path: dev-requirements.txt + cache-dependency-path: tests-requirements.txt - name: Install test dependencies run: | - python -m pip install -U pip wheel - python -m pip install -r dev-requirements.txt + python -m pip install -U pip + python -m pip install -r tests-requirements.txt - name: Tests run: | sphinx-build -d docs docs/source docs_out --color -W -bhtml diff --git a/dev-requirements.txt b/dev-requirements.txt index 94fe3ae8..cfbe3361 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,9 +1,7 @@ --r tests-requirements.txt black build flake8 mypy pylint -sphinx twine wheel diff --git a/tests-requirements.txt b/tests-requirements.txt index fd9c0a3d..5d3f6470 100644 --- a/tests-requirements.txt +++ b/tests-requirements.txt @@ -1,6 +1,6 @@ --e . pytest pytest-cov numpy pillow +sphinx xvfbwrapper; sys_platform == "linux" From 5ea58e9a8c0bba6e09e7b8b49c4f4c83d8450aa5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Tue, 11 Apr 2023 09:42:49 +0200 Subject: [PATCH 120/280] tests: reflect what is awaited in the wheel (#243) --- mss/tests/test_setup.py | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/mss/tests/test_setup.py b/mss/tests/test_setup.py index f0ee3df8..b27e48cf 100644 --- a/mss/tests/test_setup.py +++ b/mss/tests/test_setup.py @@ -130,24 +130,6 @@ def test_wheel(): adding 'mss/screenshot.py' adding 'mss/tools.py' adding 'mss/windows.py' -adding 'mss/tests/bench_bgra2rgb.py' -adding 'mss/tests/bench_general.py' -adding 'mss/tests/conftest.py' -adding 'mss/tests/test_bgra_to_rgb.py' -adding 'mss/tests/test_cls_image.py' -adding 'mss/tests/test_find_monitors.py' -adding 'mss/tests/test_get_pixels.py' -adding 'mss/tests/test_gnu_linux.py' -adding 'mss/tests/test_implementation.py' -adding 'mss/tests/test_issue_220.py' -adding 'mss/tests/test_leaks.py' -adding 'mss/tests/test_macos.py' -adding 'mss/tests/test_save.py' -adding 'mss/tests/test_setup.py' -adding 'mss/tests/test_third_party.py' -adding 'mss/tests/test_tools.py' -adding 'mss/tests/test_windows.py' -adding 'mss/tests/res/monitor-1024x768.raw.zip' adding 'mss-{__version__}.dist-info/METADATA' adding 'mss-{__version__}.dist-info/WHEEL' adding 'mss-{__version__}.dist-info/entry_points.txt' From 798e90d7aefb7adb00969bacaf7f526d883bdba8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Fri, 14 Apr 2023 18:03:08 +0200 Subject: [PATCH 121/280] doc: tweak --- CHANGELOG.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1329af09..dbbe6a18 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,10 +4,10 @@ See Git checking messages for full history. ## 8.0.3 (2023/xx/xx) - added support for Python 3.12 -- now PEP 561 compatible -- include more files in sdist (#240) -- remove `venv` files from sdist -- use markdown for the README, and changelogs +- MSS: added PEP 561 compatible +- MSS: include more files in the sdist package (#240) +- MSS: remove `venv` files from the sdist package +- MSS: use markdown for the README, and changelogs - Linux: restore the original X error handler in `.close()` (#241) - Linux: fixed `XRRCrtcInfo.width`, and `XRRCrtcInfo.height`, C types - :heart: contributors: @mgorny, @relent95 @@ -124,7 +124,7 @@ See Git checking messages for full history. ## 3.2.1 (2018/05/21) - Windows: enable Hi-DPI awareness -- :heart: contributors: @FoxRow +- :heart: contributors: @ryanfox ## 3.2.0 (2018/03/22) - removed support for Python 3.4 From 5f3a16bc9206b0eec037bf55a5b71b636dbe55bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Sat, 15 Apr 2023 00:24:29 +0200 Subject: [PATCH 122/280] chore: move pylint config to setup.cfg --- .pylintrc | 7 ------- setup.cfg | 8 ++++++++ 2 files changed, 8 insertions(+), 7 deletions(-) delete mode 100644 .pylintrc diff --git a/.pylintrc b/.pylintrc deleted file mode 100644 index 97fac66a..00000000 --- a/.pylintrc +++ /dev/null @@ -1,7 +0,0 @@ -[MESSAGES CONTROL] -disable = locally-disabled, too-few-public-methods, too-many-instance-attributes, duplicate-code - -[REPORTS] -max-line-length = 120 -output-format = colorized -reports = no diff --git a/setup.cfg b/setup.cfg index 025d7df9..200cfb9c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -76,6 +76,14 @@ force_grid_wrap = 0 use_parentheses = True line_length = 120 +[pylint.MESSAGES CONTROL] +disable = locally-disabled, too-few-public-methods, too-many-instance-attributes, duplicate-code + +[pylint.REPORTS] +max-line-length = 120 +output-format = colorized +reports = no + [tool:pytest] addopts = --showlocals From a8635053823d8acdf905fa4c677d44f581b0cb4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Sat, 15 Apr 2023 00:46:43 +0200 Subject: [PATCH 123/280] dev: review the structure of the repository to fix packaging issues (#244) --- .github/workflows/tests.yml | 6 +- CHANGELOG.md | 1 + MANIFEST.in | 6 +- check.sh | 10 +- setup.cfg | 14 +-- {mss => src/mss}/__init__.py | 0 {mss => src/mss}/__main__.py | 0 {mss => src/mss}/base.py | 0 {mss => src/mss}/darwin.py | 0 {mss => src/mss}/exception.py | 0 {mss => src/mss}/factory.py | 0 {mss => src/mss}/linux.py | 1 + {mss => src/mss}/models.py | 0 {mss => src/mss}/py.typed | 0 {mss => src/mss}/screenshot.py | 0 {mss => src/mss}/tools.py | 0 {mss => src/mss}/windows.py | 0 {mss => src}/tests/bench_bgra2rgb.py | 0 {mss => src}/tests/bench_general.py | 0 {mss => src}/tests/conftest.py | 0 .../tests/res/monitor-1024x768.raw.zip | Bin {mss => src}/tests/test_bgra_to_rgb.py | 0 {mss => src}/tests/test_cls_image.py | 0 {mss => src}/tests/test_find_monitors.py | 0 {mss => src}/tests/test_get_pixels.py | 0 {mss => src}/tests/test_gnu_linux.py | 0 {mss => src}/tests/test_implementation.py | 0 {mss => src}/tests/test_issue_220.py | 13 +-- {mss => src}/tests/test_leaks.py | 0 {mss => src}/tests/test_macos.py | 0 {mss => src}/tests/test_save.py | 0 {mss => src}/tests/test_setup.py | 90 +++++++++--------- {mss => src}/tests/test_third_party.py | 0 {mss => src}/tests/test_tools.py | 0 {mss => src}/tests/test_windows.py | 0 35 files changed, 73 insertions(+), 68 deletions(-) rename {mss => src/mss}/__init__.py (100%) rename {mss => src/mss}/__main__.py (100%) rename {mss => src/mss}/base.py (100%) rename {mss => src/mss}/darwin.py (100%) rename {mss => src/mss}/exception.py (100%) rename {mss => src/mss}/factory.py (100%) rename {mss => src/mss}/linux.py (99%) rename {mss => src/mss}/models.py (100%) rename {mss => src/mss}/py.typed (100%) rename {mss => src/mss}/screenshot.py (100%) rename {mss => src/mss}/tools.py (100%) rename {mss => src/mss}/windows.py (100%) rename {mss => src}/tests/bench_bgra2rgb.py (100%) rename {mss => src}/tests/bench_general.py (100%) rename {mss => src}/tests/conftest.py (100%) rename {mss => src}/tests/res/monitor-1024x768.raw.zip (100%) rename {mss => src}/tests/test_bgra_to_rgb.py (100%) rename {mss => src}/tests/test_cls_image.py (100%) rename {mss => src}/tests/test_find_monitors.py (100%) rename {mss => src}/tests/test_get_pixels.py (100%) rename {mss => src}/tests/test_gnu_linux.py (100%) rename {mss => src}/tests/test_implementation.py (100%) rename {mss => src}/tests/test_issue_220.py (80%) rename {mss => src}/tests/test_leaks.py (100%) rename {mss => src}/tests/test_macos.py (100%) rename {mss => src}/tests/test_save.py (100%) rename {mss => src}/tests/test_setup.py (59%) rename {mss => src}/tests/test_third_party.py (100%) rename {mss => src}/tests/test_tools.py (100%) rename {mss => src}/tests/test_windows.py (100%) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 803186eb..ff8f17f5 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -74,11 +74,15 @@ jobs: with: python-version: ${{ matrix.python.runs-on }} cache: pip - cache-dependency-path: dev-requirements.txt + cache-dependency-path: | + dev-requirements.txt + tests-requirements.txt + check-latest: true - name: Install test dependencies run: | python -m pip install -U pip wheel python -m pip install -r dev-requirements.txt + python -m pip install -r tests-requirements.txt - name: Install Xvfb if: matrix.os.emoji == '🐧' run: sudo apt install xvfb diff --git a/CHANGELOG.md b/CHANGELOG.md index dbbe6a18..a3e75d65 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ See Git checking messages for full history. ## 8.0.3 (2023/xx/xx) - added support for Python 3.12 +- dev: review the structure of the repository to fix packaging issues (#243) - MSS: added PEP 561 compatible - MSS: include more files in the sdist package (#240) - MSS: remove `venv` files from the sdist package diff --git a/MANIFEST.in b/MANIFEST.in index b8c16d14..01747690 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -5,7 +5,7 @@ include LICENSE.txt include README.md include dev-requirements.txt include tests-requirements.txt -include mss/tests/*.py -include mss/py.typed +include src/tests/*.py +include src/mss/py.typed recursive-include docs/source * -recursive-include mss/tests/res * +recursive-include src/tests/res * diff --git a/check.sh b/check.sh index e9000a84..0e48e93f 100755 --- a/check.sh +++ b/check.sh @@ -2,9 +2,9 @@ # # Small script to ensure quality checks pass before submitting a commit/PR. # -python -m isort docs mss -python -m black --line-length=120 docs mss -python -m flake8 docs mss -python -m pylint mss +python -m isort docs src +python -m black --line-length=120 docs src +python -m flake8 docs src +python -m pylint src/mss # "--platform win32" to not fail on ctypes.windll (it does not affect the overall check on other OSes) -python -m mypy --platform win32 --exclude mss/tests mss docs/source/examples +python -m mypy --platform win32 --exclude src/tests src docs/source/examples diff --git a/setup.cfg b/setup.cfg index 200cfb9c..ef95380a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -43,15 +43,16 @@ classifiers = Topic :: Software Development :: Libraries [options] -zip_safe = False -include_package_data = True -packages = find_namespace: python_requires = >=3.8 +package_dir = + = src +packages = find: [options.packages.find] -include = - mss - mss.* +where = src + +[options.package_data] +mss = py.typed [options.entry_points] console_scripts = @@ -85,6 +86,7 @@ output-format = colorized reports = no [tool:pytest] +pythonpath = src addopts = --showlocals --strict-markers diff --git a/mss/__init__.py b/src/mss/__init__.py similarity index 100% rename from mss/__init__.py rename to src/mss/__init__.py diff --git a/mss/__main__.py b/src/mss/__main__.py similarity index 100% rename from mss/__main__.py rename to src/mss/__main__.py diff --git a/mss/base.py b/src/mss/base.py similarity index 100% rename from mss/base.py rename to src/mss/base.py diff --git a/mss/darwin.py b/src/mss/darwin.py similarity index 100% rename from mss/darwin.py rename to src/mss/darwin.py diff --git a/mss/exception.py b/src/mss/exception.py similarity index 100% rename from mss/exception.py rename to src/mss/exception.py diff --git a/mss/factory.py b/src/mss/factory.py similarity index 100% rename from mss/factory.py rename to src/mss/factory.py diff --git a/mss/linux.py b/src/mss/linux.py similarity index 99% rename from mss/linux.py rename to src/mss/linux.py index fe935005..b71b4ecc 100644 --- a/mss/linux.py +++ b/src/mss/linux.py @@ -64,6 +64,7 @@ class XErrorEvent(Structure): ("resourceid", c_void_p), # resource ID ] + # TODO: remove in v9.0.0 Event = XErrorEvent diff --git a/mss/models.py b/src/mss/models.py similarity index 100% rename from mss/models.py rename to src/mss/models.py diff --git a/mss/py.typed b/src/mss/py.typed similarity index 100% rename from mss/py.typed rename to src/mss/py.typed diff --git a/mss/screenshot.py b/src/mss/screenshot.py similarity index 100% rename from mss/screenshot.py rename to src/mss/screenshot.py diff --git a/mss/tools.py b/src/mss/tools.py similarity index 100% rename from mss/tools.py rename to src/mss/tools.py diff --git a/mss/windows.py b/src/mss/windows.py similarity index 100% rename from mss/windows.py rename to src/mss/windows.py diff --git a/mss/tests/bench_bgra2rgb.py b/src/tests/bench_bgra2rgb.py similarity index 100% rename from mss/tests/bench_bgra2rgb.py rename to src/tests/bench_bgra2rgb.py diff --git a/mss/tests/bench_general.py b/src/tests/bench_general.py similarity index 100% rename from mss/tests/bench_general.py rename to src/tests/bench_general.py diff --git a/mss/tests/conftest.py b/src/tests/conftest.py similarity index 100% rename from mss/tests/conftest.py rename to src/tests/conftest.py diff --git a/mss/tests/res/monitor-1024x768.raw.zip b/src/tests/res/monitor-1024x768.raw.zip similarity index 100% rename from mss/tests/res/monitor-1024x768.raw.zip rename to src/tests/res/monitor-1024x768.raw.zip diff --git a/mss/tests/test_bgra_to_rgb.py b/src/tests/test_bgra_to_rgb.py similarity index 100% rename from mss/tests/test_bgra_to_rgb.py rename to src/tests/test_bgra_to_rgb.py diff --git a/mss/tests/test_cls_image.py b/src/tests/test_cls_image.py similarity index 100% rename from mss/tests/test_cls_image.py rename to src/tests/test_cls_image.py diff --git a/mss/tests/test_find_monitors.py b/src/tests/test_find_monitors.py similarity index 100% rename from mss/tests/test_find_monitors.py rename to src/tests/test_find_monitors.py diff --git a/mss/tests/test_get_pixels.py b/src/tests/test_get_pixels.py similarity index 100% rename from mss/tests/test_get_pixels.py rename to src/tests/test_get_pixels.py diff --git a/mss/tests/test_gnu_linux.py b/src/tests/test_gnu_linux.py similarity index 100% rename from mss/tests/test_gnu_linux.py rename to src/tests/test_gnu_linux.py diff --git a/mss/tests/test_implementation.py b/src/tests/test_implementation.py similarity index 100% rename from mss/tests/test_implementation.py rename to src/tests/test_implementation.py diff --git a/mss/tests/test_issue_220.py b/src/tests/test_issue_220.py similarity index 80% rename from mss/tests/test_issue_220.py rename to src/tests/test_issue_220.py index da68972c..3fefccca 100644 --- a/mss/tests/test_issue_220.py +++ b/src/tests/test_issue_220.py @@ -2,23 +2,20 @@ This is part of the MSS Python's module. Source: https://github.com/BoboTiG/python-mss """ -import platform - import pytest import mss tkinter = pytest.importorskip("tkinter") -if platform.system().lower() == "darwin" and platform.python_implementation() == "PyPy": - # [macOS] PyPy 7.3.11 [Python 3.9.16] fails on GitHub: - # RuntimeError: tk.h version (8.5) doesn't match libtk.a version (8.6) - pytestmark = pytest.mark.skip - @pytest.fixture def root() -> tkinter.Tk: - master = tkinter.Tk() + try: + master = tkinter.Tk() + except RuntimeError: + pytest.skip(reason="tk.h version (8.5) doesn't match libtk.a version (8.6)") + try: yield master finally: diff --git a/mss/tests/test_leaks.py b/src/tests/test_leaks.py similarity index 100% rename from mss/tests/test_leaks.py rename to src/tests/test_leaks.py diff --git a/mss/tests/test_macos.py b/src/tests/test_macos.py similarity index 100% rename from mss/tests/test_macos.py rename to src/tests/test_macos.py diff --git a/mss/tests/test_save.py b/src/tests/test_save.py similarity index 100% rename from mss/tests/test_save.py rename to src/tests/test_save.py diff --git a/mss/tests/test_setup.py b/src/tests/test_setup.py similarity index 59% rename from mss/tests/test_setup.py rename to src/tests/test_setup.py index b27e48cf..975676e4 100644 --- a/mss/tests/test_setup.py +++ b/src/tests/test_setup.py @@ -20,7 +20,6 @@ CHECK = "twine check dist/*".split() -@pytest.mark.xfail(True, reason="Issue #243") def test_sdist(): output = check_output(SDIST, stderr=STDOUT, text=True) expected = f""" @@ -28,14 +27,14 @@ def test_sdist(): creating mss-{__version__}/docs creating mss-{__version__}/docs/source creating mss-{__version__}/docs/source/examples -creating mss-{__version__}/mss -creating mss-{__version__}/mss.egg-info -creating mss-{__version__}/mss/tests -creating mss-{__version__}/mss/tests/res +creating mss-{__version__}/src +creating mss-{__version__}/src/mss +creating mss-{__version__}/src/mss.egg-info +creating mss-{__version__}/src/tests +creating mss-{__version__}/src/tests/res copying files to mss-{__version__}... copying CHANGELOG.md -> mss-{__version__} copying CHANGES.md -> mss-{__version__} -copying CODE_OF_CONDUCT.md -> mss-{__version__} copying CONTRIBUTORS.md -> mss-{__version__} copying LICENSE.txt -> mss-{__version__} copying MANIFEST.in -> mss-{__version__} @@ -43,6 +42,7 @@ def test_sdist(): copying dev-requirements.txt -> mss-{__version__} copying setup.cfg -> mss-{__version__} copying setup.py -> mss-{__version__} +copying tests-requirements.txt -> mss-{__version__} copying docs/source/api.rst -> mss-{__version__}/docs/source copying docs/source/conf.py -> mss-{__version__}/docs/source copying docs/source/developers.rst -> mss-{__version__}/docs/source @@ -63,42 +63,41 @@ def test_sdist(): copying docs/source/examples/part_of_screen_monitor_2.py -> mss-{__version__}/docs/source/examples copying docs/source/examples/pil.py -> mss-{__version__}/docs/source/examples copying docs/source/examples/pil_pixels.py -> mss-{__version__}/docs/source/examples -copying mss/__init__.py -> mss-{__version__}/mss -copying mss/__main__.py -> mss-{__version__}/mss -copying mss/base.py -> mss-{__version__}/mss -copying mss/darwin.py -> mss-{__version__}/mss -copying mss/exception.py -> mss-{__version__}/mss -copying mss/factory.py -> mss-{__version__}/mss -copying mss/linux.py -> mss-{__version__}/mss -copying mss/models.py -> mss-{__version__}/mss -copying mss/py.typed -> mss-{__version__}/mss -copying mss/screenshot.py -> mss-{__version__}/mss -copying mss/tools.py -> mss-{__version__}/mss -copying mss/windows.py -> mss-{__version__}/mss -copying mss.egg-info/PKG-INFO -> mss-{__version__}/mss.egg-info -copying mss.egg-info/SOURCES.txt -> mss-{__version__}/mss.egg-info -copying mss.egg-info/dependency_links.txt -> mss-{__version__}/mss.egg-info -copying mss.egg-info/entry_points.txt -> mss-{__version__}/mss.egg-info -copying mss.egg-info/not-zip-safe -> mss-{__version__}/mss.egg-info -copying mss.egg-info/top_level.txt -> mss-{__version__}/mss.egg-info -copying mss/tests/bench_bgra2rgb.py -> mss-{__version__}/mss/tests -copying mss/tests/bench_general.py -> mss-{__version__}/mss/tests -copying mss/tests/conftest.py -> mss-{__version__}/mss/tests -copying mss/tests/test_bgra_to_rgb.py -> mss-{__version__}/mss/tests -copying mss/tests/test_cls_image.py -> mss-{__version__}/mss/tests -copying mss/tests/test_find_monitors.py -> mss-{__version__}/mss/tests -copying mss/tests/test_get_pixels.py -> mss-{__version__}/mss/tests -copying mss/tests/test_gnu_linux.py -> mss-{__version__}/mss/tests -copying mss/tests/test_implementation.py -> mss-{__version__}/mss/tests -copying mss/tests/test_issue_220.py -> mss-{__version__}/mss/tests -copying mss/tests/test_leaks.py -> mss-{__version__}/mss/tests -copying mss/tests/test_macos.py -> mss-{__version__}/mss/tests -copying mss/tests/test_save.py -> mss-{__version__}/mss/tests -copying mss/tests/test_setup.py -> mss-{__version__}/mss/tests -copying mss/tests/test_third_party.py -> mss-{__version__}/mss/tests -copying mss/tests/test_tools.py -> mss-{__version__}/mss/tests -copying mss/tests/test_windows.py -> mss-{__version__}/mss/tests -copying mss/tests/res/monitor-1024x768.raw.zip -> mss-{__version__}/mss/tests/res +copying src/mss/__init__.py -> mss-{__version__}/src/mss +copying src/mss/__main__.py -> mss-{__version__}/src/mss +copying src/mss/base.py -> mss-{__version__}/src/mss +copying src/mss/darwin.py -> mss-{__version__}/src/mss +copying src/mss/exception.py -> mss-{__version__}/src/mss +copying src/mss/factory.py -> mss-{__version__}/src/mss +copying src/mss/linux.py -> mss-{__version__}/src/mss +copying src/mss/models.py -> mss-{__version__}/src/mss +copying src/mss/py.typed -> mss-{__version__}/src/mss +copying src/mss/screenshot.py -> mss-{__version__}/src/mss +copying src/mss/tools.py -> mss-{__version__}/src/mss +copying src/mss/windows.py -> mss-{__version__}/src/mss +copying src/mss.egg-info/PKG-INFO -> mss-{__version__}/src/mss.egg-info +copying src/mss.egg-info/SOURCES.txt -> mss-{__version__}/src/mss.egg-info +copying src/mss.egg-info/dependency_links.txt -> mss-{__version__}/src/mss.egg-info +copying src/mss.egg-info/entry_points.txt -> mss-{__version__}/src/mss.egg-info +copying src/mss.egg-info/top_level.txt -> mss-{__version__}/src/mss.egg-info +copying src/tests/bench_bgra2rgb.py -> mss-{__version__}/src/tests +copying src/tests/bench_general.py -> mss-{__version__}/src/tests +copying src/tests/conftest.py -> mss-{__version__}/src/tests +copying src/tests/test_bgra_to_rgb.py -> mss-{__version__}/src/tests +copying src/tests/test_cls_image.py -> mss-{__version__}/src/tests +copying src/tests/test_find_monitors.py -> mss-{__version__}/src/tests +copying src/tests/test_get_pixels.py -> mss-{__version__}/src/tests +copying src/tests/test_gnu_linux.py -> mss-{__version__}/src/tests +copying src/tests/test_implementation.py -> mss-{__version__}/src/tests +copying src/tests/test_issue_220.py -> mss-{__version__}/src/tests +copying src/tests/test_leaks.py -> mss-{__version__}/src/tests +copying src/tests/test_macos.py -> mss-{__version__}/src/tests +copying src/tests/test_save.py -> mss-{__version__}/src/tests +copying src/tests/test_setup.py -> mss-{__version__}/src/tests +copying src/tests/test_third_party.py -> mss-{__version__}/src/tests +copying src/tests/test_tools.py -> mss-{__version__}/src/tests +copying src/tests/test_windows.py -> mss-{__version__}/src/tests +copying src/tests/res/monitor-1024x768.raw.zip -> mss-{__version__}/src/tests/res Writing mss-{__version__}/setup.cfg """ @@ -107,17 +106,18 @@ def test_sdist(): if not (line := line.strip()): continue assert line in output - assert output.count("copying ") == expected.count("copying ") + assert output.count("copying") == expected.count("copying") assert f"Successfully built mss-{__version__}.tar.gz" in output assert "warning" not in output.lower() check_call(CHECK) -@pytest.mark.xfail(True, reason="Issue #243") def test_wheel(): output = check_output(WHEEL, stderr=STDOUT, text=True) expected = f""" +creating build/bdist.linux-x86_64/wheel/mss-{__version__}.dist-info/WHEEL + and adding 'build/bdist.linux-x86_64/wheel' to it adding 'mss/__init__.py' adding 'mss/__main__.py' adding 'mss/base.py' @@ -142,7 +142,7 @@ def test_wheel(): if not (line := line.strip()): continue assert line in output - assert output.count("adding ") == expected.count("adding ") + assert output.count("adding") == expected.count("adding") assert f"Successfully built mss-{__version__}-py3-none-any.whl" in output assert "warning" not in output.lower() diff --git a/mss/tests/test_third_party.py b/src/tests/test_third_party.py similarity index 100% rename from mss/tests/test_third_party.py rename to src/tests/test_third_party.py diff --git a/mss/tests/test_tools.py b/src/tests/test_tools.py similarity index 100% rename from mss/tests/test_tools.py rename to src/tests/test_tools.py diff --git a/mss/tests/test_windows.py b/src/tests/test_windows.py similarity index 100% rename from mss/tests/test_windows.py rename to src/tests/test_windows.py From 960fa597ee94774b618580605142766469f29fe3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Sat, 15 Apr 2023 00:49:42 +0200 Subject: [PATCH 124/280] dev: renamed the `master` branch to `main` --- .github/workflows/tests.yml | 2 +- CHANGELOG.md | 1 + README.md | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index ff8f17f5..58e90519 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -3,7 +3,7 @@ name: Tests on: push: branches: - - master + - main pull_request: jobs: diff --git a/CHANGELOG.md b/CHANGELOG.md index a3e75d65..d56e29de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ See Git checking messages for full history. ## 8.0.3 (2023/xx/xx) - added support for Python 3.12 +- dev: renamed the `master` branch to `main` - dev: review the structure of the repository to fix packaging issues (#243) - MSS: added PEP 561 compatible - MSS: include more files in the sdist package (#240) diff --git a/README.md b/README.md index 50c0d70c..92d51bb0 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![PyPI version](https://badge.fury.io/py/mss.svg)](https://badge.fury.io/py/mss) [![Anaconda version](https://anaconda.org/conda-forge/python-mss/badges/version.svg)](https://anaconda.org/conda-forge/python-mss) -[![Tests workflow](https://github.com/BoboTiG/python-mss/actions/workflows/tests.yml/badge.svg?branch=master)](https://github.com/BoboTiG/python-mss/actions/workflows/tests.yml) +[![Tests workflow](https://github.com/BoboTiG/python-mss/actions/workflows/tests.yml/badge.svg?branch=main)](https://github.com/BoboTiG/python-mss/actions/workflows/tests.yml) [![Downloads](https://static.pepy.tech/personalized-badge/mss?period=total&units=international_system&left_color=black&right_color=orange&left_text=Downloads)](https://pepy.tech/project/mss) ```python From a042e4f6551706e562f31a30867d7758e244c9d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Sat, 15 Apr 2023 00:52:31 +0200 Subject: [PATCH 125/280] Version 8.0.3 --- CHANGELOG.md | 11 +++++------ CHANGES.md | 2 +- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d56e29de..b13ef352 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,16 +2,15 @@ See Git checking messages for full history. -## 8.0.3 (2023/xx/xx) +## 8.0.3 (2023/04/15) - added support for Python 3.12 -- dev: renamed the `master` branch to `main` -- dev: review the structure of the repository to fix packaging issues (#243) -- MSS: added PEP 561 compatible +- MSS: added PEP 561 compatibility - MSS: include more files in the sdist package (#240) -- MSS: remove `venv` files from the sdist package -- MSS: use markdown for the README, and changelogs - Linux: restore the original X error handler in `.close()` (#241) - Linux: fixed `XRRCrtcInfo.width`, and `XRRCrtcInfo.height`, C types +- doc: use markdown for the README, and changelogs +- dev: renamed the `master` branch to `main` +- dev: review the structure of the repository to fix/improve packaging issues (#243) - :heart: contributors: @mgorny, @relent95 ## 8.0.2 (2023/04/09) diff --git a/CHANGES.md b/CHANGES.md index 0411960f..ff7d8599 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,6 @@ # Technical Changes -## 8.0.3 (2023-04-xx) +## 8.0.3 (2023-04-15) ### linux.py - Added `XErrorEvent` class (old `Event` class is just an alias now, and will be removed in v9.0.0) From ef97a27cbeae46cbed8f348679cd9729b4755fc0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Sat, 15 Apr 2023 00:55:29 +0200 Subject: [PATCH 126/280] Bump the version --- CHANGELOG.md | 3 +++ README.md | 4 ++-- docs/source/conf.py | 2 +- setup.cfg | 2 +- 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b13ef352..e71b4611 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ See Git checking messages for full history. +## 8.0.4-dev (2023/xx/xx) +- + ## 8.0.3 (2023/04/15) - added support for Python 3.12 - MSS: added PEP 561 compatibility diff --git a/README.md b/README.md index 92d51bb0..7b58256e 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ For the maintainers, here are commands to upload a new release: ```shell rm -rf build dist -python -m build --sdist --wheel +python -m build twine check dist/* twine upload dist/* -``` \ No newline at end of file +``` diff --git a/docs/source/conf.py b/docs/source/conf.py index f6bfc6c5..ba683a4d 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -27,7 +27,7 @@ # built documents. # # The short X.Y version. -version = "8.0.3" +version = "8.0.4-dev" # The full version, including alpha/beta/rc tags. release = "latest" diff --git a/setup.cfg b/setup.cfg index ef95380a..3cbdfc53 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = mss -version = 8.0.3 +version = 8.0.4-dev author = Mickaël 'Tiger-222' Schoentgen author_email = contact@tiger-222.fr description = An ultra fast cross-platform multiple screenshots module in pure python using ctypes. From 6fc741011f3d9f415861206ec379fbb1e8afd4d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20G=C3=B3rny?= Date: Sat, 15 Apr 2023 16:59:27 +0200 Subject: [PATCH 127/280] linux: Add failure handling to XOpenDisplay() call (#247) Add an explicit check for the return value of XOpenDisplay(). This function does not seem to use X11 error handlers, and only returns NULL when it fails. Without an explicit check, mss ended up passing this NULL value to further calls and causing segfaults in libX11. Now it triggers an explicit exception instead. --- src/mss/linux.py | 2 ++ src/tests/test_gnu_linux.py | 5 +++++ 2 files changed, 7 insertions(+) diff --git a/src/mss/linux.py b/src/mss/linux.py index b71b4ecc..557cedbc 100644 --- a/src/mss/linux.py +++ b/src/mss/linux.py @@ -317,6 +317,8 @@ def __init__(self, /, **kwargs: Any) -> None: self._handles.original_error_handler = self.xlib.XSetErrorHandler(_error_handler) self._handles.display = self.xlib.XOpenDisplay(display) + if not self._handles.display: + raise ScreenShotError(f"Unable to open display: {display!r}.") if not self._is_extension_enabled("RANDR"): raise ScreenShotError("Xrandr not enabled.") diff --git a/src/tests/test_gnu_linux.py b/src/tests/test_gnu_linux.py index 2c8fc6c3..621f6575 100644 --- a/src/tests/test_gnu_linux.py +++ b/src/tests/test_gnu_linux.py @@ -72,6 +72,11 @@ def test_arg_display(display: str, monkeypatch): with mss.mss(display="0"): pass + # Invalid `display` that is not trivially distinguishable. + with pytest.raises(ScreenShotError): + with mss.mss(display=":INVALID"): + pass + # No `DISPLAY` in envars monkeypatch.delenv("DISPLAY") with pytest.raises(ScreenShotError): From 30c9e6fd2ae5eca9b863c6e4213ca8ca3309a2a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20G=C3=B3rny?= Date: Sat, 15 Apr 2023 17:00:55 +0200 Subject: [PATCH 128/280] ci: run tests via xvfb-run on GitHub Actions (#248) Use the convenience `xvfb-run` script to start Xvfb on a free DISPLAY and use it to run the test suite. It is part of the `xvfb` package on Debian. --- .github/workflows/tests.yml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 58e90519..6c5d4a05 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -88,10 +88,7 @@ jobs: run: sudo apt install xvfb - name: Tests (GNU/Linux) if: matrix.os.emoji == '🐧' - run: | - export DISPLAY=:99 - sudo Xvfb -ac ${DISPLAY} -screen 0 1280x1024x24 > /dev/null 2>&1 & - python -m pytest + run: xvfb-run python -m pytest - name: Tests (macOS, Windows) if: matrix.os.emoji != '🐧' run: python -m pytest From cb7f814fd3f8d24b6f260cb3766f4c8d609b6ca5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20G=C3=B3rny?= Date: Sat, 15 Apr 2023 20:45:52 +0200 Subject: [PATCH 129/280] tests: Use PyVirtualDisplay instead of xvfbwrapper (#249) Use the more modern PyVirtualDisplay package instead of xvfbwrapper to run Xvfb. Most importantly, it uses the more robust approach of starting Xvfb with `-displayfd` and it is actively maintained upstream. Unfortunately, this does not seem sufficient to entirely eliminate random test failures. --- src/tests/test_gnu_linux.py | 18 +++++------------- src/tests/test_leaks.py | 22 +++++++++------------- tests-requirements.txt | 2 +- 3 files changed, 15 insertions(+), 27 deletions(-) diff --git a/src/tests/test_gnu_linux.py b/src/tests/test_gnu_linux.py index 621f6575..1357790e 100644 --- a/src/tests/test_gnu_linux.py +++ b/src/tests/test_gnu_linux.py @@ -12,7 +12,7 @@ from mss.base import MSSBase from mss.exception import ScreenShotError -xvfbwrapper = pytest.importorskip("xvfbwrapper") +pyvirtualdisplay = pytest.importorskip("pyvirtualdisplay") PYPY = platform.python_implementation() == "PyPy" @@ -23,12 +23,8 @@ @pytest.fixture def display() -> str: - vdisplay = xvfbwrapper.Xvfb(width=WIDTH, height=HEIGHT, colordepth=DEPTH) - vdisplay.start() - try: - yield f":{vdisplay.new_display}" - finally: - vdisplay.stop() + with pyvirtualdisplay.Display(size=(WIDTH, HEIGHT), color_depth=DEPTH) as vdisplay: + yield vdisplay.new_display_var @pytest.mark.skipif(PYPY, reason="Failure on PyPy") @@ -114,14 +110,10 @@ def test_xrandr_extension_exists_but_is_not_enabled(display: str): def test_unsupported_depth(): - vdisplay = xvfbwrapper.Xvfb(width=WIDTH, height=HEIGHT, colordepth=8) - vdisplay.start() - try: + with pyvirtualdisplay.Display(size=(WIDTH, HEIGHT), color_depth=8) as vdisplay: with pytest.raises(ScreenShotError): - with mss.mss(display=f":{vdisplay.new_display}") as sct: + with mss.mss(display=vdisplay.new_display_var) as sct: sct.grab(sct.monitors[1]) - finally: - vdisplay.stop() def test_region_out_of_monitor_bounds(display: str): diff --git a/src/tests/test_leaks.py b/src/tests/test_leaks.py index 6b48bcc5..7e6a25e9 100644 --- a/src/tests/test_leaks.py +++ b/src/tests/test_leaks.py @@ -94,19 +94,15 @@ def regression_issue_135(): def regression_issue_210(): """Regression test for issue #210: multiple X servers.""" - xvfbwrapper = pytest.importorskip("xvfbwrapper") - - vdisplay = xvfbwrapper.Xvfb(width=1920, height=1080, colordepth=24) - vdisplay.start() - with mss(): - pass - vdisplay.stop() - - vdisplay = xvfbwrapper.Xvfb(width=1920, height=1080, colordepth=24) - vdisplay.start() - with mss(): - pass - vdisplay.stop() + 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): + with mss(): + pass @pytest.mark.skipif(OS == "darwin", reason="No possible leak on macOS.") diff --git a/tests-requirements.txt b/tests-requirements.txt index 5d3f6470..ce691570 100644 --- a/tests-requirements.txt +++ b/tests-requirements.txt @@ -3,4 +3,4 @@ pytest-cov numpy pillow sphinx -xvfbwrapper; sys_platform == "linux" +PyVirtualDisplay; sys_platform == "linux" From 69655e60073d95401fa68b4b0827c44a4a4d8a92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Sat, 15 Apr 2023 22:16:41 +0200 Subject: [PATCH 130/280] doc: tweak --- CHANGELOG.md | 7 +++++-- docs/source/conf.py | 2 +- setup.cfg | 2 +- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e71b4611..9f37acd7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,8 +2,11 @@ See Git checking messages for full history. -## 8.0.4-dev (2023/xx/xx) -- +## 8.0.4 (2023/xx/xx) +- Linux: add failure handling to `XOpenDisplay()` call (#247) +- CI: run tests via xvfb-run on GitHub Actions (#248) +- tests: Use `PyVirtualDisplay` instead of `xvfbwrapper` (#249) +- :heart: contributors: @mgorny ## 8.0.3 (2023/04/15) - added support for Python 3.12 diff --git a/docs/source/conf.py b/docs/source/conf.py index ba683a4d..3fa52bff 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -27,7 +27,7 @@ # built documents. # # The short X.Y version. -version = "8.0.4-dev" +version = "8.0.4" # The full version, including alpha/beta/rc tags. release = "latest" diff --git a/setup.cfg b/setup.cfg index 3cbdfc53..ddb55a74 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = mss -version = 8.0.4-dev +version = 8.0.4 author = Mickaël 'Tiger-222' Schoentgen author_email = contact@tiger-222.fr description = An ultra fast cross-platform multiple screenshots module in pure python using ctypes. From b62832b2dc88d523e0e406c6d67a001a922b275a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Sat, 15 Apr 2023 22:31:45 +0200 Subject: [PATCH 131/280] linux: tweaks --- src/mss/__init__.py | 2 +- src/mss/linux.py | 6 +++--- tests-requirements.txt | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/mss/__init__.py b/src/mss/__init__.py index 17c7d059..56c3820c 100644 --- a/src/mss/__init__.py +++ b/src/mss/__init__.py @@ -11,7 +11,7 @@ from .exception import ScreenShotError from .factory import mss -__version__ = "8.0.3" +__version__ = "8.0.4" __author__ = "Mickaël 'Tiger-222' Schoentgen" __copyright__ = """ Copyright (c) 2013-2023, Mickaël 'Tiger-222' Schoentgen diff --git a/src/mss/linux.py b/src/mss/linux.py index 557cedbc..5ad0dc57 100644 --- a/src/mss/linux.py +++ b/src/mss/linux.py @@ -223,7 +223,7 @@ 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 Xlib or XRANDR function.""" + """Validate the returned value of a C function call.""" thread = current_thread() if retval != 0 and thread not in _ERROR: @@ -331,7 +331,7 @@ def __init__(self, /, **kwargs: Any) -> None: def close(self) -> None: # Remove our error handler - if self._handles.original_error_handler is not None: + if self._handles.original_error_handler: # It's required when exiting MSS to prevent letting `_error_handler()` as default handler. # Doing so would crash when using Tk/Tkinter, see issue #220. # Interesting technical stuff can be found here: @@ -341,7 +341,7 @@ def close(self) -> None: self._handles.original_error_handler = None # Clean-up - if self._handles.display is not None: + if self._handles.display: self.xlib.XCloseDisplay(self._handles.display) self._handles.display = None self._handles.drawable = None diff --git a/tests-requirements.txt b/tests-requirements.txt index ce691570..c0677bb1 100644 --- a/tests-requirements.txt +++ b/tests-requirements.txt @@ -1,6 +1,6 @@ -pytest -pytest-cov numpy pillow +pytest +pytest-cov +pyvirtualdisplay; sys_platform == "linux" sphinx -PyVirtualDisplay; sys_platform == "linux" From 16ef017541b741d36c2250e65c3abc21e7effda3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Sat, 15 Apr 2023 22:51:23 +0200 Subject: [PATCH 132/280] CI: allow to trigger the test workflow manually And skip xvfb installation: it's already installed. --- .github/workflows/tests.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 6c5d4a05..67c139bb 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -5,6 +5,7 @@ on: branches: - main pull_request: + workflow_dispatch: jobs: quality: @@ -83,9 +84,6 @@ jobs: python -m pip install -U pip wheel python -m pip install -r dev-requirements.txt python -m pip install -r tests-requirements.txt - - name: Install Xvfb - if: matrix.os.emoji == '🐧' - run: sudo apt install xvfb - name: Tests (GNU/Linux) if: matrix.os.emoji == '🐧' run: xvfb-run python -m pytest From ba2542015f1d51cb2ad07f30cc72df5382951d9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Sat, 15 Apr 2023 22:57:12 +0200 Subject: [PATCH 133/280] mac: remove obsolete comment --- src/mss/darwin.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/mss/darwin.py b/src/mss/darwin.py index a6661f68..8a8fbc34 100644 --- a/src/mss/darwin.py +++ b/src/mss/darwin.py @@ -103,7 +103,6 @@ def _init_library(self) -> None: coregraphics = ctypes.util.find_library("CoreGraphics") else: # macOS Big Sur and newer - # pylint: disable=line-too-long coregraphics = "/System/Library/Frameworks/CoreGraphics.framework/Versions/Current/CoreGraphics" if not coregraphics: From e3e9652bc0a459675361d18953d64863d033046f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Sat, 15 Apr 2023 23:53:16 +0200 Subject: [PATCH 134/280] Windows: refactored how internal handles are stored (#250) --- CHANGELOG.md | 4 +- src/mss/windows.py | 103 ++++++++++++++++---------------------- src/tests/test_windows.py | 52 +++++++++++++++---- 3 files changed, 87 insertions(+), 72 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f37acd7..bee984c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,9 +4,11 @@ See Git checking messages for full history. ## 8.0.4 (2023/xx/xx) - Linux: add failure handling to `XOpenDisplay()` call (#247) +- Windows: refactored how internal handles are stored +- Windows: removed side effects when leaving the context manager, resources are all freed - CI: run tests via xvfb-run on GitHub Actions (#248) - tests: Use `PyVirtualDisplay` instead of `xvfbwrapper` (#249) -- :heart: contributors: @mgorny +- :heart: contributors: @mgorny, @CTPaHHuK-HEbA ## 8.0.3 (2023/04/15) - added support for Python 3.12 diff --git a/src/mss/windows.py b/src/mss/windows.py index 00c380e3..68165ce0 100644 --- a/src/mss/windows.py +++ b/src/mss/windows.py @@ -4,8 +4,7 @@ """ import ctypes import sys -import threading -from ctypes import POINTER, WINFUNCTYPE, Structure, c_void_p +from ctypes import POINTER, WINFUNCTYPE, Structure, c_int, c_void_p from ctypes.wintypes import ( BOOL, DOUBLE, @@ -22,7 +21,8 @@ UINT, WORD, ) -from typing import Any, Dict, Optional +from threading import local +from typing import Any, Optional from .base import MSSBase from .exception import ScreenShotError @@ -78,12 +78,14 @@ class BITMAPINFO(Structure): "BitBlt": ("gdi32", [HDC, INT, INT, INT, INT, HDC, INT, INT, DWORD], BOOL), "CreateCompatibleBitmap": ("gdi32", [HDC, INT, INT], HBITMAP), "CreateCompatibleDC": ("gdi32", [HDC], HDC), + "DeleteDC": ("gdi32", [HDC], HDC), "DeleteObject": ("gdi32", [HGDIOBJ], INT), "EnumDisplayMonitors": ("user32", [HDC, c_void_p, MONITORNUMPROC, LPARAM], BOOL), "GetDeviceCaps": ("gdi32", [HWND, INT], INT), "GetDIBits": ("gdi32", [HDC, HBITMAP, UINT, UINT, c_void_p, POINTER(BITMAPINFO), UINT], BOOL), "GetSystemMetrics": ("user32", [INT], INT), "GetWindowDC": ("user32", [HWND], HDC), + "ReleaseDC": ("user32", [HWND, HDC], c_int), "SelectObject": ("gdi32", [HDC, HGDIOBJ], HGDIOBJ), } @@ -91,14 +93,7 @@ class BITMAPINFO(Structure): class MSS(MSSBase): """Multiple ScreenShots implementation for Microsoft Windows.""" - __slots__ = {"_bbox", "_bmi", "_data", "gdi32", "user32"} - - # Class attributes instanced one time to prevent resource leaks. - bmp = None - memdc = None - - # A dict to maintain *srcdc* values created by multiple threads. - _srcdc_dict: Dict[threading.Thread, int] = {} + __slots__ = {"gdi32", "user32", "_handles"} def __init__(self, /, **kwargs: Any) -> None: """Windows initialisations.""" @@ -110,12 +105,12 @@ def __init__(self, /, **kwargs: Any) -> None: self._set_cfunctions() self._set_dpi_awareness() - self._bbox = {"height": 0, "width": 0} - self._data: ctypes.Array[ctypes.c_char] = ctypes.create_string_buffer(0) - - srcdc = self._get_srcdc() - if not MSS.memdc: - MSS.memdc = self.gdi32.CreateCompatibleDC(srcdc) + # Available thread-specific variables + self._handles = local() + self._handles.region_height_width = (0, 0) + self._handles.bmp = None + self._handles.srcdc = self.user32.GetWindowDC(0) + self._handles.memdc = self.gdi32.CreateCompatibleDC(self._handles.srcdc) bmi = BITMAPINFO() bmi.bmiHeader.biSize = ctypes.sizeof(BITMAPINFOHEADER) @@ -124,7 +119,21 @@ def __init__(self, /, **kwargs: Any) -> None: bmi.bmiHeader.biCompression = 0 # 0 = BI_RGB (no compression) bmi.bmiHeader.biClrUsed = 0 # See grab.__doc__ [3] bmi.bmiHeader.biClrImportant = 0 # See grab.__doc__ [3] - self._bmi = bmi + self._handles.bmi = bmi + + def close(self) -> None: + # Clean-up + if self._handles.bmp: + self.gdi32.DeleteObject(self._handles.bmp) + self._handles.bmp = None + + if self._handles.memdc: + self.gdi32.DeleteDC(self._handles.memdc) + self._handles.memdc = None + + if self._handles.srcdc: + self.user32.ReleaseDC(0, self._handles.srcdc) + self._handles.srcdc = None def _set_cfunctions(self) -> None: """Set all ctypes functions and attach them to attributes.""" @@ -149,26 +158,9 @@ def _set_dpi_awareness(self) -> None: # These applications are not automatically scaled by the system. ctypes.windll.shcore.SetProcessDpiAwareness(2) elif (6, 0) <= version < (6, 3): - # Windows Vista, 7, 8 and Server 2012 + # Windows Vista, 7, 8, and Server 2012 self.user32.SetProcessDPIAware() - def _get_srcdc(self) -> int: - """ - Retrieve a thread-safe HDC from GetWindowDC(). - In multithreading, if the thread that creates *srcdc* is dead, *srcdc* will - no longer be valid to grab the screen. The *srcdc* attribute is replaced - with *_srcdc_dict* to maintain the *srcdc* values in multithreading. - Since the current thread and main thread are always alive, reuse their *srcdc* value first. - """ - cur_thread, main_thread = threading.current_thread(), threading.main_thread() - current_srcdc = MSS._srcdc_dict.get(cur_thread) or MSS._srcdc_dict.get(main_thread) - if current_srcdc: - srcdc = current_srcdc - else: - srcdc = self.user32.GetWindowDC(0) - MSS._srcdc_dict[cur_thread] = srcdc - return srcdc - def _monitors_impl(self) -> None: """Get positions of monitors. It will populate self._monitors.""" @@ -240,35 +232,26 @@ def _grab_impl(self, monitor: Monitor, /) -> ScreenShot: Thanks to http://stackoverflow.com/a/3688682 """ - srcdc, memdc = self._get_srcdc(), MSS.memdc + srcdc, memdc = self._handles.srcdc, self._handles.memdc + gdi = self.gdi32 width, height = monitor["width"], monitor["height"] - if (self._bbox["height"], self._bbox["width"]) != (height, width): - self._bbox = monitor - self._bmi.bmiHeader.biWidth = width - self._bmi.bmiHeader.biHeight = -height # Why minus? [1] - self._data = ctypes.create_string_buffer(width * height * 4) # [2] - if MSS.bmp: - self.gdi32.DeleteObject(MSS.bmp) - MSS.bmp = self.gdi32.CreateCompatibleBitmap(srcdc, width, height) - self.gdi32.SelectObject(memdc, MSS.bmp) - - self.gdi32.BitBlt( - memdc, - 0, - 0, - width, - height, - srcdc, - monitor["left"], - monitor["top"], - SRCCOPY | CAPTUREBLT, - ) - bits = self.gdi32.GetDIBits(memdc, MSS.bmp, 0, height, self._data, self._bmi, DIB_RGB_COLORS) + if self._handles.region_height_width != (height, width): + self._handles.region_height_width = (height, width) + self._handles.bmi.bmiHeader.biWidth = width + self._handles.bmi.bmiHeader.biHeight = -height # Why minus? [1] + self._handles.data = ctypes.create_string_buffer(width * height * 4) # [2] + if self._handles.bmp: + gdi.DeleteObject(self._handles.bmp) + self._handles.bmp = gdi.CreateCompatibleBitmap(srcdc, width, height) + gdi.SelectObject(memdc, self._handles.bmp) + + 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.") - return self.cls_image(bytearray(self._data), monitor) + return self.cls_image(bytearray(self._handles.data), monitor) def _cursor_impl(self) -> Optional[ScreenShot]: """Retrieve all cursor data. Pixels have to be RGB.""" diff --git a/src/tests/test_windows.py b/src/tests/test_windows.py index 4694a02b..3f247ca8 100644 --- a/src/tests/test_windows.py +++ b/src/tests/test_windows.py @@ -24,34 +24,46 @@ def test_implementation(monkeypatch): def test_region_caching(): """The region to grab is cached, ensure this is well-done.""" - from mss.windows import MSS - with mss.mss() as sct: - # Reset the current BMP - if MSS.bmp: - sct.gdi32.DeleteObject(MSS.bmp) - MSS.bmp = None - # Grab the area 1 region1 = {"top": 0, "left": 0, "width": 200, "height": 200} sct.grab(region1) - bmp1 = id(MSS.bmp) + bmp1 = id(sct._handles.bmp) # Grab the area 2, the cached BMP is used # Same sizes but different positions region2 = {"top": 200, "left": 200, "width": 200, "height": 200} sct.grab(region2) - bmp2 = id(MSS.bmp) + bmp2 = id(sct._handles.bmp) assert bmp1 == bmp2 # Grab the area 2 again, the cached BMP is used sct.grab(region2) - assert bmp2 == id(MSS.bmp) + assert bmp2 == id(sct._handles.bmp) + + +def test_region_not_caching(): + """The region to grab is not bad cached previous grab.""" + grab1 = mss.mss() + grab2 = mss.mss() + + region1 = {"top": 0, "left": 0, "width": 100, "height": 100} + region2 = {"top": 0, "left": 0, "width": 50, "height": 1} + grab1.grab(region1) + bmp1 = id(grab1._handles.bmp) + grab2.grab(region2) + bmp2 = id(grab2._handles.bmp) + assert bmp1 != bmp2 + + # Grab the area 1, is not bad cached BMP previous grab the area 2 + grab1.grab(region1) + bmp1 = id(grab1._handles.bmp) + assert bmp1 != bmp2 def run_child_thread(loops): for _ in range(loops): - with mss.mss() as sct: + with mss.mss() as sct: # New sct for every loop sct.grab(sct.monitors[1]) @@ -66,3 +78,21 @@ def test_thread_safety(): thread2.start() thread1.join() thread2.join() + + +def run_child_thread_bbox(loops, bbox): + 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 + 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))) + thread2 = threading.Thread(target=run_child_thread_bbox, args=(100, (0, 0, 50, 1))) + thread1.start() + thread2.start() + thread1.join() + thread2.join() From 3ab953d76770390db52ecb0cae1ed94feee9d6a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Sat, 15 Apr 2023 23:56:33 +0200 Subject: [PATCH 135/280] doc: tweak --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bee984c2..39217e69 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,8 +3,8 @@ See Git checking messages for full history. ## 8.0.4 (2023/xx/xx) -- Linux: add failure handling to `XOpenDisplay()` call (#247) -- Windows: refactored how internal handles are stored +- Linux: add failure handling to `XOpenDisplay()` call (fixes #246) +- Windows: refactored how internal handles are stored (fixes #198) - Windows: removed side effects when leaving the context manager, resources are all freed - CI: run tests via xvfb-run on GitHub Actions (#248) - tests: Use `PyVirtualDisplay` instead of `xvfbwrapper` (#249) From d1e3baa1fa1a4450886e0001d5d3fac3490d334b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Sat, 15 Apr 2023 23:57:55 +0200 Subject: [PATCH 136/280] Windows: change naming, it seems more natural to use w*h rather than h*w --- src/mss/windows.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/mss/windows.py b/src/mss/windows.py index 68165ce0..a8c28d32 100644 --- a/src/mss/windows.py +++ b/src/mss/windows.py @@ -107,7 +107,7 @@ def __init__(self, /, **kwargs: Any) -> None: # Available thread-specific variables self._handles = local() - self._handles.region_height_width = (0, 0) + self._handles.region_width_height = (0, 0) self._handles.bmp = None self._handles.srcdc = self.user32.GetWindowDC(0) self._handles.memdc = self.gdi32.CreateCompatibleDC(self._handles.srcdc) @@ -236,8 +236,8 @@ def _grab_impl(self, monitor: Monitor, /) -> ScreenShot: gdi = self.gdi32 width, height = monitor["width"], monitor["height"] - if self._handles.region_height_width != (height, width): - self._handles.region_height_width = (height, width) + if self._handles.region_width_height != (width, height): + self._handles.region_width_height = (width, height) self._handles.bmi.bmiHeader.biWidth = width self._handles.bmi.bmiHeader.biHeight = -height # Why minus? [1] self._handles.data = ctypes.create_string_buffer(width * height * 4) # [2] From efd0854d3f9c4a7d703490b7b10ce0b38b9cbc53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Sun, 16 Apr 2023 00:44:52 +0200 Subject: [PATCH 137/280] tests: enhance `test_get_pixels.py`, and try to fix a random failure at the same time (#252) --- CHANGELOG.md | 3 +- src/tests/conftest.py | 5 ++++ src/tests/test_bgra_to_rgb.py | 2 +- src/tests/test_get_pixels.py | 52 ++++++++++++++++------------------- 4 files changed, 31 insertions(+), 31 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 39217e69..68648e08 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,8 @@ See Git checking messages for full history. - Windows: refactored how internal handles are stored (fixes #198) - Windows: removed side effects when leaving the context manager, resources are all freed - CI: run tests via xvfb-run on GitHub Actions (#248) -- tests: Use `PyVirtualDisplay` instead of `xvfbwrapper` (#249) +- tests: enhance ``test_get_pixels.py``, and try to fix a random failure at the same time (related to #251) +- tests: use `PyVirtualDisplay` instead of `xvfbwrapper` (#249) - :heart: contributors: @mgorny, @CTPaHHuK-HEbA ## 8.0.3 (2023/04/15) diff --git a/src/tests/conftest.py b/src/tests/conftest.py index a0d24bb1..cfcbcec5 100644 --- a/src/tests/conftest.py +++ b/src/tests/conftest.py @@ -4,6 +4,7 @@ """ import glob import os +import platform from hashlib import md5 from pathlib import Path from zipfile import ZipFile @@ -55,6 +56,10 @@ 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 + # Grab a 1x1 screenshot region = {"top": 0, "left": 0, "width": 1, "height": 1} diff --git a/src/tests/test_bgra_to_rgb.py b/src/tests/test_bgra_to_rgb.py index 9a29bc34..1fa2c042 100644 --- a/src/tests/test_bgra_to_rgb.py +++ b/src/tests/test_bgra_to_rgb.py @@ -14,7 +14,7 @@ def test_bad_length(): image.rgb -def test_good_types(raw): +def test_good_types(raw: bytes): image = ScreenShot.from_size(bytearray(raw), 1024, 768) assert isinstance(image.raw, bytearray) assert isinstance(image.rgb, bytes) diff --git a/src/tests/test_get_pixels.py b/src/tests/test_get_pixels.py index b6a7b6ea..8535538e 100644 --- a/src/tests/test_get_pixels.py +++ b/src/tests/test_get_pixels.py @@ -2,6 +2,8 @@ This is part of the MSS Python's module. Source: https://github.com/BoboTiG/python-mss """ + +import itertools import os import pytest @@ -21,35 +23,27 @@ def test_grab_monitor(): def test_grab_part_of_screen(pixel_ratio): - monitor = {"top": 160, "left": 160, "width": 160, "height": 160} - with mss(display=os.getenv("DISPLAY")) as sct: - image = sct.grab(monitor) - assert isinstance(image, ScreenShot) - assert isinstance(image.raw, bytearray) - assert isinstance(image.rgb, bytes) - assert image.top == 160 - assert image.left == 160 - assert image.width == 160 * pixel_ratio - assert image.height == 160 * pixel_ratio - - -def test_grab_part_of_screen_rounded(pixel_ratio): - monitor = {"top": 160, "left": 160, "width": 161, "height": 159} with mss(display=os.getenv("DISPLAY")) as sct: - image = sct.grab(monitor) - assert isinstance(image, ScreenShot) - assert isinstance(image.raw, bytearray) - assert isinstance(image.rgb, bytes) - assert image.top == 160 - assert image.left == 160 - assert image.width == 161 * pixel_ratio - assert image.height == 159 * pixel_ratio - - -def test_grab_individual_pixels(): - monitor = {"top": 160, "left": 160, "width": 222, "height": 42} - with mss(display=os.getenv("DISPLAY")) as sct: - image = sct.grab(monitor) - assert isinstance(image.pixel(0, 0), tuple) + for width, height in itertools.product(range(1, 42), range(1, 42)): + monitor = {"top": 160, "left": 160, "width": width, "height": height} + image = sct.grab(monitor) + + assert image.top == 160 + assert image.left == 160 + assert image.width == width * pixel_ratio + assert image.height == height * pixel_ratio + + +def test_get_pixel(raw: bytes): + image = ScreenShot.from_size(bytearray(raw), 1024, 768) + assert image.width == 1024 + assert image.height == 768 + assert len(image.pixels) == 768 + assert len(image.pixels[0]) == 1024 + + assert image.pixel(0, 0) == (135, 152, 192) + assert image.pixel(image.width // 2, image.height // 2) == (0, 0, 0) + assert image.pixel(image.width - 1, image.height - 1) == (135, 152, 192) + with pytest.raises(ScreenShotError): image.pixel(image.width + 1, 12) From 1cef4bf24e5bb1ada7a8cd85c1e7612cae561758 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Sun, 16 Apr 2023 00:58:39 +0200 Subject: [PATCH 138/280] tests: clean-up --- src/tests/conftest.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/tests/conftest.py b/src/tests/conftest.py index cfcbcec5..72da4df4 100644 --- a/src/tests/conftest.py +++ b/src/tests/conftest.py @@ -63,6 +63,6 @@ def pixel_ratio() -> int: # Grab a 1x1 screenshot region = {"top": 0, "left": 0, "width": 1, "height": 1} - with mss(display=os.getenv("DISPLAY")) as sct: - # On macOS with Retina display, the width will be 2 instead of 1 + with mss() as sct: + # On macOS with Retina display, the width can be 2 instead of 1 return sct.grab(region).size[0] From 05c749666f0f9d69840ad80f5ddd59566685597d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20G=C3=B3rny?= Date: Sun, 16 Apr 2023 08:49:27 +0200 Subject: [PATCH 139/280] tests: use pytest-rerunfailures (#253) Use pytest-rerunfailures plugin to attempt to rerun failing tests up to 5 times. While this doesn't solve the underlying issue, it should keep the CI green and detecting real issues until we figure out how to solve it properly. --- setup.cfg | 1 + tests-requirements.txt | 1 + 2 files changed, 2 insertions(+) diff --git a/setup.cfg b/setup.cfg index ddb55a74..463f2c6b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -94,3 +94,4 @@ addopts = -v --cov=mss --cov-report=term-missing + --reruns 5 diff --git a/tests-requirements.txt b/tests-requirements.txt index c0677bb1..59484eba 100644 --- a/tests-requirements.txt +++ b/tests-requirements.txt @@ -2,5 +2,6 @@ numpy pillow pytest pytest-cov +pytest-rerunfailures pyvirtualdisplay; sys_platform == "linux" sphinx From ec1ebe950d0fc6be1d87272238044d2509bbfbaa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Sun, 16 Apr 2023 10:24:06 +0200 Subject: [PATCH 140/280] doc: tweak --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 68648e08..b8ec3010 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ See Git checking messages for full history. - CI: run tests via xvfb-run on GitHub Actions (#248) - tests: enhance ``test_get_pixels.py``, and try to fix a random failure at the same time (related to #251) - tests: use `PyVirtualDisplay` instead of `xvfbwrapper` (#249) +- tests: automatic rerun in case of failure (related to #251) - :heart: contributors: @mgorny, @CTPaHHuK-HEbA ## 8.0.3 (2023/04/15) From 780976dfc629742b5ed36f32a72750899a1864c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Tue, 18 Apr 2023 22:16:05 +0200 Subject: [PATCH 141/280] Mac: tiny improvement in monitors finding (#254) --- CHANGELOG.md | 1 + src/mss/darwin.py | 5 ++--- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b8ec3010..91203298 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ See Git checking messages for full history. ## 8.0.4 (2023/xx/xx) - Linux: add failure handling to `XOpenDisplay()` call (fixes #246) +- Mac: tiny improvement in moniors finding - Windows: refactored how internal handles are stored (fixes #198) - Windows: removed side effects when leaving the context manager, resources are all freed - CI: run tests via xvfb-run on GitHub Actions (#248) diff --git a/src/mss/darwin.py b/src/mss/darwin.py index 8a8fbc34..1dd37eab 100644 --- a/src/mss/darwin.py +++ b/src/mss/darwin.py @@ -133,14 +133,13 @@ def _monitors_impl(self) -> None: display_count = c_uint32(0) active_displays = (c_uint32 * self.max_displays)() core.CGGetActiveDisplayList(self.max_displays, active_displays, ctypes.byref(display_count)) - rotations = {0.0: "normal", 90.0: "right", -90.0: "left"} for idx in range(display_count.value): display = active_displays[idx] rect = core.CGDisplayBounds(display) rect = core.CGRectStandardize(rect) width, height = rect.size.width, rect.size.height - rot = core.CGDisplayRotation(display) - if rotations[rot] in ["left", "right"]: + if core.CGDisplayRotation(display) in {90.0, -90.0}: + # {0.0: "normal", 90.0: "right", -90.0: "left"} width, height = height, width self._monitors.append( { From c300fb714801d7eb82aeaa050f8ad85ede37860a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Tue, 18 Apr 2023 22:19:08 +0200 Subject: [PATCH 142/280] Version 9.0.0 --- CHANGELOG.md | 8 ++++---- CHANGES.md | 14 ++++++++++++-- docs/source/api.rst | 2 +- setup.cfg | 2 +- src/mss/__init__.py | 2 +- src/mss/linux.py | 4 ---- 6 files changed, 19 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 91203298..9f86c5c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,13 +2,13 @@ See Git checking messages for full history. -## 8.0.4 (2023/xx/xx) +## 9.0.0 (2023/04/18) - Linux: add failure handling to `XOpenDisplay()` call (fixes #246) - Mac: tiny improvement in moniors finding - Windows: refactored how internal handles are stored (fixes #198) -- Windows: removed side effects when leaving the context manager, resources are all freed -- CI: run tests via xvfb-run on GitHub Actions (#248) -- tests: enhance ``test_get_pixels.py``, and try to fix a random failure at the same time (related to #251) +- Windows: removed side effects when leaving the context manager, resources are all freed (fixes #209) +- CI: run tests via `xvfb-run` on GitHub Actions (#248) +- tests: enhance `test_get_pixels.py`, and try to fix a random failure at the same time (related to #251) - tests: use `PyVirtualDisplay` instead of `xvfbwrapper` (#249) - tests: automatic rerun in case of failure (related to #251) - :heart: contributors: @mgorny, @CTPaHHuK-HEbA diff --git a/CHANGES.md b/CHANGES.md index ff7d8599..ea3376f3 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,15 @@ # Technical Changes +## 9.0.0 (2023-04-18) + +### linux.py +- Removed `XEvent` class. Use `XErrorEvent` instead. + +### windows.py +- Added `MSS.close()` method +- Removed `MSS.bmp` attribute +- Removed `MSS.memdc` attribute + ## 8.0.3 (2023-04-15) ### linux.py @@ -17,8 +27,8 @@ ### linux.py - Added `MSS.close()` - Moved `MSS.__init__()` keyword arguments handling to the base class -- Renamed `error_handler()` function to `__error_handler()` -- Renamed `_validate()` function to `___validate()` +- Renamed `error_handler()` function to `_error_handler()` +- Renamed `validate()` function to `__validate()` - Renamed `MSS.has_extension()` method to `_is_extension_enabled()` - Removed `ERROR` namespace - Removed `MSS.drawable` attribute diff --git a/docs/source/api.rst b/docs/source/api.rst index 54913126..3eae42b8 100644 --- a/docs/source/api.rst +++ b/docs/source/api.rst @@ -45,7 +45,7 @@ GNU/Linux Structure that serves as the connection to the X server, and that contains all the information about that X server. -.. class:: Event +.. class:: XErrorEvent XErrorEvent to debug eventual errors. diff --git a/setup.cfg b/setup.cfg index 463f2c6b..0ca582e4 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = mss -version = 8.0.4 +version = 9.0.0 author = Mickaël 'Tiger-222' Schoentgen author_email = contact@tiger-222.fr description = An ultra fast cross-platform multiple screenshots module in pure python using ctypes. diff --git a/src/mss/__init__.py b/src/mss/__init__.py index 56c3820c..e7e8503f 100644 --- a/src/mss/__init__.py +++ b/src/mss/__init__.py @@ -11,7 +11,7 @@ from .exception import ScreenShotError from .factory import mss -__version__ = "8.0.4" +__version__ = "9.0.0" __author__ = "Mickaël 'Tiger-222' Schoentgen" __copyright__ = """ Copyright (c) 2013-2023, Mickaël 'Tiger-222' Schoentgen diff --git a/src/mss/linux.py b/src/mss/linux.py index 5ad0dc57..8b118e89 100644 --- a/src/mss/linux.py +++ b/src/mss/linux.py @@ -65,10 +65,6 @@ class XErrorEvent(Structure): ] -# TODO: remove in v9.0.0 -Event = XErrorEvent - - class XFixesCursorImage(Structure): """ Cursor structure. From d02afcf36f3c2f90cef40c72a9b32ac5f764934a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Tue, 18 Apr 2023 22:21:02 +0200 Subject: [PATCH 143/280] Bump the version --- CHANGELOG.md | 4 ++++ setup.cfg | 2 +- src/mss/__init__.py | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f86c5c4..996c06db 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ See Git checking messages for full history. +## 9.0.1 (2023/xx/xx) +- +- :heart: contributors: @ + ## 9.0.0 (2023/04/18) - Linux: add failure handling to `XOpenDisplay()` call (fixes #246) - Mac: tiny improvement in moniors finding diff --git a/setup.cfg b/setup.cfg index 0ca582e4..bd33c7e9 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = mss -version = 9.0.0 +version = 9.0.1 author = Mickaël 'Tiger-222' Schoentgen author_email = contact@tiger-222.fr description = An ultra fast cross-platform multiple screenshots module in pure python using ctypes. diff --git a/src/mss/__init__.py b/src/mss/__init__.py index e7e8503f..df47393a 100644 --- a/src/mss/__init__.py +++ b/src/mss/__init__.py @@ -11,7 +11,7 @@ from .exception import ScreenShotError from .factory import mss -__version__ = "9.0.0" +__version__ = "9.0.1" __author__ = "Mickaël 'Tiger-222' Schoentgen" __copyright__ = """ Copyright (c) 2013-2023, Mickaël 'Tiger-222' Schoentgen From 88ecf4706c65fbadf3d3eafd81e29a34c5bb5395 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Tue, 18 Apr 2023 22:24:44 +0200 Subject: [PATCH 144/280] doc: tweak --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 996c06db..5cf753b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,7 @@ See Git checking messages for full history. ## 9.0.0 (2023/04/18) - Linux: add failure handling to `XOpenDisplay()` call (fixes #246) -- Mac: tiny improvement in moniors finding +- Mac: tiny improvement in monitors finding - Windows: refactored how internal handles are stored (fixes #198) - Windows: removed side effects when leaving the context manager, resources are all freed (fixes #209) - CI: run tests via `xvfb-run` on GitHub Actions (#248) From a7a6bce916098f0bce7959bd6220e30064641b69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Thu, 20 Apr 2023 07:45:30 +0200 Subject: [PATCH 145/280] CLI: fix entry point not taking into account arguments (#255) --- CHANGELOG.md | 3 +- src/mss/__main__.py | 9 +++-- src/tests/test_implementation.py | 60 +++++++++++++++++++------------- 3 files changed, 41 insertions(+), 31 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5cf753b9..f115fa91 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,8 +3,7 @@ See Git checking messages for full history. ## 9.0.1 (2023/xx/xx) -- -- :heart: contributors: @ +- CLI: fixed entry point not taking into account arguments ## 9.0.0 (2023/04/18) - Linux: add failure handling to `XOpenDisplay()` call (fixes #246) diff --git a/src/mss/__main__.py b/src/mss/__main__.py index 0aa43983..4dff5a1a 100644 --- a/src/mss/__main__.py +++ b/src/mss/__main__.py @@ -3,6 +3,7 @@ Source: https://github.com/BoboTiG/python-mss """ import os.path +import sys from argparse import ArgumentParser from . import __version__ @@ -14,7 +15,7 @@ def main(*args: str) -> int: """Main logic.""" - cli_args = ArgumentParser() + cli_args = ArgumentParser(prog="mss") cli_args.add_argument( "-c", "--coordinates", @@ -42,7 +43,7 @@ def main(*args: str) -> int: ) cli_args.add_argument("-v", "--version", action="version", version=__version__) - options = cli_args.parse_args(args) + options = cli_args.parse_args(args or None) kwargs = {"mon": options.monitor, "output": options.output} if options.coordinates: try: @@ -80,6 +81,4 @@ def main(*args: str) -> int: if __name__ == "__main__": # pragma: nocover - import sys - - sys.exit(main(*sys.argv[1:])) + sys.exit(main()) diff --git a/src/tests/test_implementation.py b/src/tests/test_implementation.py index 2de3b82f..caeca228 100644 --- a/src/tests/test_implementation.py +++ b/src/tests/test_implementation.py @@ -5,12 +5,15 @@ import os import os.path import platform -from unittest.mock import patch +import sys +from datetime import datetime +from unittest.mock import Mock, patch import pytest import mss.tools from mss import mss +from mss.__main__ import main as entry_point from mss.base import MSSBase from mss.exception import ScreenShotError from mss.screenshot import ScreenShot @@ -78,12 +81,9 @@ def test_factory(monkeypatch): assert error == "System 'chuck norris' not (yet?) implemented." +@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): - from datetime import datetime - - from mss.__main__ import main as entry_point - def main(*args: str, ret: int = 0) -> None: if with_cursor: args = args + ("--with-cursor",) @@ -91,8 +91,8 @@ def main(*args: str, ret: int = 0) -> None: # No arguments main() - out, _ = capsys.readouterr() - for mon, line in enumerate(out.splitlines(), 1): + captured = capsys.readouterr() + for mon, line in enumerate(captured.out.splitlines(), 1): filename = f"monitor-{mon}.png" assert line.endswith(filename) assert os.path.isfile(filename) @@ -100,24 +100,24 @@ def main(*args: str, ret: int = 0) -> None: for opt in ("-m", "--monitor"): main(opt, "1") - out, _ = capsys.readouterr() - assert out.endswith("monitor-1.png\n") + captured = capsys.readouterr() + assert captured.out.endswith("monitor-1.png\n") assert os.path.isfile("monitor-1.png") os.remove("monitor-1.png") for opt in zip(["-m 1", "--monitor=1"], ["-q", "--quiet"]): main(*opt) - out, _ = capsys.readouterr() - assert not out + captured = capsys.readouterr() + assert not captured.out assert os.path.isfile("monitor-1.png") os.remove("monitor-1.png") fmt = "sct-{mon}-{width}x{height}.png" for opt in ("-o", "--out"): main(opt, fmt) - out, _ = capsys.readouterr() + captured = capsys.readouterr() with mss(display=os.getenv("DISPLAY")) as sct: - for mon, (monitor, line) in enumerate(zip(sct.monitors[1:], out.splitlines()), 1): + for mon, (monitor, line) in enumerate(zip(sct.monitors[1:], captured.out.splitlines()), 1): filename = fmt.format(mon=mon, **monitor) assert line.endswith(filename) assert os.path.isfile(filename) @@ -127,8 +127,8 @@ def main(*args: str, ret: int = 0) -> None: for opt in ("-o", "--out"): main("-m 1", opt, fmt) filename = fmt.format(mon=1, date=datetime.now()) - out, _ = capsys.readouterr() - assert out.endswith(filename + "\n") + captured = capsys.readouterr() + assert captured.out.endswith(filename + "\n") assert os.path.isfile(filename) os.remove(filename) @@ -136,23 +136,22 @@ def main(*args: str, ret: int = 0) -> None: filename = "sct-2x12_40x67.png" for opt in ("-c", "--coordinates"): main(opt, coordinates) - out, _ = capsys.readouterr() - assert out.endswith(filename + "\n") + captured = capsys.readouterr() + assert captured.out.endswith(filename + "\n") assert os.path.isfile(filename) os.remove(filename) coordinates = "2,12,40" for opt in ("-c", "--coordinates"): main(opt, coordinates, ret=2) - out, _ = capsys.readouterr() - assert out == "Coordinates syntax: top, left, width, height\n" + captured = capsys.readouterr() + assert captured.out == "Coordinates syntax: top, left, width, height\n" +@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): - from mss.__main__ import main as entry_point - def main(*args: str) -> int: if quiet: args = args + ("--quiet",) @@ -160,14 +159,27 @@ def main(*args: str) -> int: if quiet: assert main() == 1 - out, err = capsys.readouterr() - assert not out - assert not err + captured = capsys.readouterr() + assert not captured.out + assert not captured.err else: with pytest.raises(ScreenShotError): main() +def test_entry_point_with_no_argument(capsys): + # 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"]): + with pytest.raises(SystemExit) as exc: + entry_point() + assert exc.value.code == 0 + + captured = capsys.readouterr() + assert not captured.err + assert "usage: mss" in captured.out + + def test_grab_with_tuple(pixel_ratio: int): left = 100 top = 100 From d13d37b97051b3ac913d2b4d15d3f1d4ef4f4ded Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Thu, 20 Apr 2023 07:46:01 +0200 Subject: [PATCH 146/280] Version 9.0.1 --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f115fa91..c55d92c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ See Git checking messages for full history. -## 9.0.1 (2023/xx/xx) +## 9.0.1 (2023/04/20) - CLI: fixed entry point not taking into account arguments ## 9.0.0 (2023/04/18) From 5e029a25ef5c75aacce57eadd64af96f03b43f2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Thu, 20 Apr 2023 08:58:38 +0200 Subject: [PATCH 147/280] Bump the version --- CHANGELOG.md | 4 ++++ docs/source/conf.py | 2 +- src/mss/__init__.py | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c55d92c1..f1ca0295 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ See Git checking messages for full history. +## 9.0.2 (2023/xx/xx) +- +- :heart: contributors: @ + ## 9.0.1 (2023/04/20) - CLI: fixed entry point not taking into account arguments diff --git a/docs/source/conf.py b/docs/source/conf.py index 3fa52bff..962ea97b 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -27,7 +27,7 @@ # built documents. # # The short X.Y version. -version = "8.0.4" +version = "9.0.2" # The full version, including alpha/beta/rc tags. release = "latest" diff --git a/src/mss/__init__.py b/src/mss/__init__.py index df47393a..77c563ef 100644 --- a/src/mss/__init__.py +++ b/src/mss/__init__.py @@ -11,7 +11,7 @@ from .exception import ScreenShotError from .factory import mss -__version__ = "9.0.1" +__version__ = "9.0.2" __author__ = "Mickaël 'Tiger-222' Schoentgen" __copyright__ = """ Copyright (c) 2013-2023, Mickaël 'Tiger-222' Schoentgen From 353fa11995089d6680297e28d896748c2a089d6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Wed, 26 Apr 2023 23:23:25 +0200 Subject: [PATCH 148/280] Bump the version, second try --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index bd33c7e9..fb06b0fa 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = mss -version = 9.0.1 +version = 9.0.2 author = Mickaël 'Tiger-222' Schoentgen author_email = contact@tiger-222.fr description = An ultra fast cross-platform multiple screenshots module in pure python using ctypes. From 24e066c7e04e52e6ba3339b1b6665a18dc915d24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Thu, 27 Apr 2023 01:10:03 +0200 Subject: [PATCH 149/280] feat: level up the packaging using `hatchling` (#256) --- .github/workflows/tests.yml | 12 +- CHANGELOG.md | 2 +- MANIFEST.in | 11 -- dev-requirements.txt | 7 -- docs/source/conf.py | 14 ++- docs/source/developers.rst | 4 +- mypy.ini | 20 ---- pyproject.toml | 169 ++++++++++++++++++++++++++++ setup.cfg | 97 ---------------- setup.py | 4 - src/tests/test_setup.py | 214 ++++++++++++++++-------------------- tests-requirements.txt | 7 -- 12 files changed, 283 insertions(+), 278 deletions(-) delete mode 100644 MANIFEST.in delete mode 100644 dev-requirements.txt delete mode 100644 mypy.ini create mode 100644 pyproject.toml delete mode 100644 setup.cfg delete mode 100644 setup.py delete mode 100644 tests-requirements.txt diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 67c139bb..f46daa37 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -17,11 +17,10 @@ jobs: with: python-version: "3.x" cache: pip - cache-dependency-path: dev-requirements.txt - name: Install dependencies run: | python -m pip install -U pip - python -m pip install -r dev-requirements.txt + python -m pip install -e '.[dev]' - name: Tests run: ./check.sh @@ -34,11 +33,10 @@ jobs: with: python-version: "3.x" cache: pip - cache-dependency-path: tests-requirements.txt - name: Install test dependencies run: | python -m pip install -U pip - python -m pip install -r tests-requirements.txt + python -m pip install -e '.[test]' - name: Tests run: | sphinx-build -d docs docs/source docs_out --color -W -bhtml @@ -75,15 +73,11 @@ jobs: with: python-version: ${{ matrix.python.runs-on }} cache: pip - cache-dependency-path: | - dev-requirements.txt - tests-requirements.txt check-latest: true - name: Install test dependencies run: | python -m pip install -U pip wheel - python -m pip install -r dev-requirements.txt - python -m pip install -r tests-requirements.txt + python -m pip install -e '.[dev,test]' - name: Tests (GNU/Linux) if: matrix.os.emoji == '🐧' run: xvfb-run python -m pytest diff --git a/CHANGELOG.md b/CHANGELOG.md index f1ca0295..eebccd66 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,7 @@ See Git checking messages for full history. ## 9.0.2 (2023/xx/xx) -- +- level up the packaging using `hatchling` - :heart: contributors: @ ## 9.0.1 (2023/04/20) diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index 01747690..00000000 --- a/MANIFEST.in +++ /dev/null @@ -1,11 +0,0 @@ -include CHANGELOG.md -include CHANGES.md -include CONTRIBUTORS.md -include LICENSE.txt -include README.md -include dev-requirements.txt -include tests-requirements.txt -include src/tests/*.py -include src/mss/py.typed -recursive-include docs/source * -recursive-include src/tests/res * diff --git a/dev-requirements.txt b/dev-requirements.txt deleted file mode 100644 index cfbe3361..00000000 --- a/dev-requirements.txt +++ /dev/null @@ -1,7 +0,0 @@ -black -build -flake8 -mypy -pylint -twine -wheel diff --git a/docs/source/conf.py b/docs/source/conf.py index 962ea97b..42164fea 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -1,3 +1,13 @@ +# Lets prevent misses, and import the module to get the proper version. +# So that the version in only defined once across the whole code base: +# src/mss/__init__.py +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).parent.parent.parent / "src")) + +from mss import __version__ # noqa + # -- General configuration ------------------------------------------------ # Add any Sphinx extension module names here, as strings. They can be @@ -27,7 +37,7 @@ # built documents. # # The short X.Y version. -version = "9.0.2" +version = __version__ # The full version, including alpha/beta/rc tags. release = "latest" @@ -75,4 +85,4 @@ # ---------------------------------------------- # Example configuration for intersphinx: refer to the Python standard library. -intersphinx_mapping = {"https://docs.python.org/3/": None} +intersphinx_mapping = {"python": ("https://docs.python.org/3", None)} diff --git a/docs/source/developers.rst b/docs/source/developers.rst index c48e54ac..d9c3e537 100644 --- a/docs/source/developers.rst +++ b/docs/source/developers.rst @@ -23,7 +23,7 @@ You will need `pytest `_:: $ python -m venv venv $ . venv/bin/activate $ python -m pip install -U pip - $ python -m pip install -r tests-requirements.txt + $ python -m pip install -e '.[test]' How to Test? @@ -39,7 +39,7 @@ Code Quality To ensure the code quality is correct enough:: - $ python -m pip install -r dev-requirements.txt + $ python -m pip install -e '.[dev]' $ ./check.sh diff --git a/mypy.ini b/mypy.ini deleted file mode 100644 index 7b3afaf2..00000000 --- a/mypy.ini +++ /dev/null @@ -1,20 +0,0 @@ -[mypy] -# Ensure we know what we do -warn_redundant_casts = True -warn_unused_ignores = True -warn_unused_configs = True - -# Imports management -ignore_missing_imports = True -follow_imports = skip - -# Ensure full coverage -disallow_untyped_defs = True -disallow_incomplete_defs = True -disallow_untyped_calls = True - -; Restrict dynamic typing (a little) -; e.g. `x: List[Any]` or x: List` -; disallow_any_generics = True - -strict_equality = True \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..c352b799 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,169 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "mss" +description = "An ultra fast cross-platform multiple screenshots module in pure python using ctypes." +readme = "README.md" +requires-python = ">= 3.8" +authors = [ + { name = "Mickaël 'Tiger-222' Schoentgen", email="contact@tiger-222.fr" }, +] +maintainers = [ + { name = "Mickaël 'Tiger-222' Schoentgen", email="contact@tiger-222.fr" }, +] +license = { file = "LICENSE.txt" } +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Environment :: MacOS X", + "Intended Audience :: Developers", + "Intended Audience :: Education", + "Intended Audience :: End Users/Desktop", + "Intended Audience :: Information Technology", + "Intended Audience :: Science/Research", + "License :: OSI Approved :: MIT License", + "Operating System :: MacOS", + "Operating System :: Microsoft :: Windows", + "Operating System :: Unix", + "Programming Language :: Python :: Implementation :: CPython", + "Programming Language :: Python :: Implementation :: PyPy", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Topic :: Multimedia :: Graphics :: Capture :: Screen Capture", + "Topic :: Software Development :: Libraries", +] +keywords = [ + "BitBlt", + "ctypes", + "EnumDisplayMonitors", + "CGGetActiveDisplayList", + "CGImageGetBitsPerPixel", + "monitor", + "screen", + "screenshot", + "screencapture", + "screengrab", + "XGetImage", + "XGetWindowAttributes", + "XRRGetScreenResourcesCurrent", +] +dynamic = ["version"] + +[project.urls] +Homepage = "https://github.com/BoboTiG/python-mss" +Documentation = "https://python-mss.readthedocs.io" +Changelog = "https://github.com/BoboTiG/python-mss/blob/main/CHANGELOG.md" +Source = "https://github.com/BoboTiG/python-mss" +Sponsor = "https://github.com/sponsors/BoboTiG" +Tracker = "https://github.com/BoboTiG/python-mss/issues" +"Released Versions" = "https://github.com/BoboTiG/python-mss/releases" + +[project.scripts] +mss = "mss.__main__:main" + +[project.optional-dependencies] +test = [ + "numpy", + "pillow", + "pytest", + "pytest-cov", + "pytest-rerunfailures", + "pyvirtualdisplay; sys_platform == 'linux'", + "sphinx", +] +dev = [ + "black", + "build", + "flake8-pyproject", + "mypy", + "pylint", + "twine", + "wheel", +] + +[tool.hatch.version] +path = "src/mss/__init__.py" + +[tool.hatch.build] +skip-excluded-dirs = true + +[tool.hatch.build.targets.sdist] +only-include = [ + "CHANGELOG.md", + "CHANGES.md", + "CONTRIBUTORS.md", + "docs/source", + "src", +] + +[tool.hatch.build.targets.wheel] +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 +warn_unused_ignores = true +warn_unused_configs = true + +# Imports management +ignore_missing_imports = true +follow_imports = "skip" + +# Ensure full coverage +disallow_untyped_defs = true +disallow_incomplete_defs = true +disallow_untyped_calls = true + +# Restrict dynamic typing (a little) +# e.g. `x: List[Any]` or x: List` +# disallow_any_generics = 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 = """ + --showlocals + --strict-markers + -r fE + -vvv + --cov=src/mss + --cov-report=term-missing +""" diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index fb06b0fa..00000000 --- a/setup.cfg +++ /dev/null @@ -1,97 +0,0 @@ -[metadata] -name = mss -version = 9.0.2 -author = Mickaël 'Tiger-222' Schoentgen -author_email = contact@tiger-222.fr -description = An ultra fast cross-platform multiple screenshots module in pure python using ctypes. -long_description = file: README.md -long_description_content_type = text/markdown -url = https://github.com/BoboTiG/python-mss -home_page = https://pypi.org/project/mss/ -project_urls = - Documentation = https://python-mss.readthedocs.io - Source = https://github.com/BoboTiG/python-mss - Tracker = https://github.com/BoboTiG/python-mss/issues -keywords = screen, screenshot, screencapture, screengrab -license = MIT -license_files = - LICENSE -platforms = Darwin, Linux, Windows -classifiers = - Development Status :: 5 - Production/Stable - Environment :: MacOS X - Intended Audience :: Developers - Intended Audience :: Education - Intended Audience :: End Users/Desktop - Intended Audience :: Information Technology - Intended Audience :: Science/Research - License :: OSI Approved :: MIT License - Operating System :: MacOS - Operating System :: Microsoft :: Windows - Operating System :: Unix - Programming Language :: Python :: Implementation :: CPython - Programming Language :: Python :: Implementation :: PyPy - Programming Language :: Python - Programming Language :: Python :: 3 - Programming Language :: Python :: 3 :: Only - Programming Language :: Python :: 3.8 - Programming Language :: Python :: 3.9 - Programming Language :: Python :: 3.10 - Programming Language :: Python :: 3.11 - Programming Language :: Python :: 3.12 - Topic :: Multimedia :: Graphics :: Capture :: Screen Capture - Topic :: Software Development :: Libraries - -[options] -python_requires = >=3.8 -package_dir = - = src -packages = find: - -[options.packages.find] -where = src - -[options.package_data] -mss = py.typed - -[options.entry_points] -console_scripts = - mss = mss.__main__:main - -[coverage:run] -omit = - mss/tests/* - -[flake8] -ignore = - # E203 whitespace before ':', but E203 is not PEP 8 compliant - E203 - # W503 line break before binary operator, but W503 is not PEP 8 compliant - W503 -max-line-length = 120 - -[isort] -multi_line_output = 3 -include_trailing_comma = True -force_grid_wrap = 0 -use_parentheses = True -line_length = 120 - -[pylint.MESSAGES CONTROL] -disable = locally-disabled, too-few-public-methods, too-many-instance-attributes, duplicate-code - -[pylint.REPORTS] -max-line-length = 120 -output-format = colorized -reports = no - -[tool:pytest] -pythonpath = src -addopts = - --showlocals - --strict-markers - -r fE - -v - --cov=mss - --cov-report=term-missing - --reruns 5 diff --git a/setup.py b/setup.py deleted file mode 100644 index 056ba45d..00000000 --- a/setup.py +++ /dev/null @@ -1,4 +0,0 @@ -import setuptools - - -setuptools.setup() diff --git a/src/tests/test_setup.py b/src/tests/test_setup.py index 975676e4..5bec6fb6 100644 --- a/src/tests/test_setup.py +++ b/src/tests/test_setup.py @@ -3,7 +3,9 @@ Source: https://github.com/BoboTiG/python-mss """ import platform +import tarfile from subprocess import STDOUT, check_call, check_output +from zipfile import ZipFile import pytest @@ -17,133 +19,109 @@ SDIST = "python -m build --sdist".split() WHEEL = "python -m build --wheel".split() -CHECK = "twine check dist/*".split() +CHECK = "twine check --strict".split() def test_sdist(): output = check_output(SDIST, stderr=STDOUT, text=True) - expected = f""" -creating mss-{__version__} -creating mss-{__version__}/docs -creating mss-{__version__}/docs/source -creating mss-{__version__}/docs/source/examples -creating mss-{__version__}/src -creating mss-{__version__}/src/mss -creating mss-{__version__}/src/mss.egg-info -creating mss-{__version__}/src/tests -creating mss-{__version__}/src/tests/res -copying files to mss-{__version__}... -copying CHANGELOG.md -> mss-{__version__} -copying CHANGES.md -> mss-{__version__} -copying CONTRIBUTORS.md -> mss-{__version__} -copying LICENSE.txt -> mss-{__version__} -copying MANIFEST.in -> mss-{__version__} -copying README.md -> mss-{__version__} -copying dev-requirements.txt -> mss-{__version__} -copying setup.cfg -> mss-{__version__} -copying setup.py -> mss-{__version__} -copying tests-requirements.txt -> mss-{__version__} -copying docs/source/api.rst -> mss-{__version__}/docs/source -copying docs/source/conf.py -> mss-{__version__}/docs/source -copying docs/source/developers.rst -> mss-{__version__}/docs/source -copying docs/source/examples.rst -> mss-{__version__}/docs/source -copying docs/source/index.rst -> mss-{__version__}/docs/source -copying docs/source/installation.rst -> mss-{__version__}/docs/source -copying docs/source/support.rst -> mss-{__version__}/docs/source -copying docs/source/usage.rst -> mss-{__version__}/docs/source -copying docs/source/where.rst -> mss-{__version__}/docs/source -copying docs/source/examples/callback.py -> mss-{__version__}/docs/source/examples -copying docs/source/examples/custom_cls_image.py -> mss-{__version__}/docs/source/examples -copying docs/source/examples/fps.py -> mss-{__version__}/docs/source/examples -copying docs/source/examples/fps_multiprocessing.py -> mss-{__version__}/docs/source/examples -copying docs/source/examples/from_pil_tuple.py -> mss-{__version__}/docs/source/examples -copying docs/source/examples/linux_display_keyword.py -> mss-{__version__}/docs/source/examples -copying docs/source/examples/opencv_numpy.py -> mss-{__version__}/docs/source/examples -copying docs/source/examples/part_of_screen.py -> mss-{__version__}/docs/source/examples -copying docs/source/examples/part_of_screen_monitor_2.py -> mss-{__version__}/docs/source/examples -copying docs/source/examples/pil.py -> mss-{__version__}/docs/source/examples -copying docs/source/examples/pil_pixels.py -> mss-{__version__}/docs/source/examples -copying src/mss/__init__.py -> mss-{__version__}/src/mss -copying src/mss/__main__.py -> mss-{__version__}/src/mss -copying src/mss/base.py -> mss-{__version__}/src/mss -copying src/mss/darwin.py -> mss-{__version__}/src/mss -copying src/mss/exception.py -> mss-{__version__}/src/mss -copying src/mss/factory.py -> mss-{__version__}/src/mss -copying src/mss/linux.py -> mss-{__version__}/src/mss -copying src/mss/models.py -> mss-{__version__}/src/mss -copying src/mss/py.typed -> mss-{__version__}/src/mss -copying src/mss/screenshot.py -> mss-{__version__}/src/mss -copying src/mss/tools.py -> mss-{__version__}/src/mss -copying src/mss/windows.py -> mss-{__version__}/src/mss -copying src/mss.egg-info/PKG-INFO -> mss-{__version__}/src/mss.egg-info -copying src/mss.egg-info/SOURCES.txt -> mss-{__version__}/src/mss.egg-info -copying src/mss.egg-info/dependency_links.txt -> mss-{__version__}/src/mss.egg-info -copying src/mss.egg-info/entry_points.txt -> mss-{__version__}/src/mss.egg-info -copying src/mss.egg-info/top_level.txt -> mss-{__version__}/src/mss.egg-info -copying src/tests/bench_bgra2rgb.py -> mss-{__version__}/src/tests -copying src/tests/bench_general.py -> mss-{__version__}/src/tests -copying src/tests/conftest.py -> mss-{__version__}/src/tests -copying src/tests/test_bgra_to_rgb.py -> mss-{__version__}/src/tests -copying src/tests/test_cls_image.py -> mss-{__version__}/src/tests -copying src/tests/test_find_monitors.py -> mss-{__version__}/src/tests -copying src/tests/test_get_pixels.py -> mss-{__version__}/src/tests -copying src/tests/test_gnu_linux.py -> mss-{__version__}/src/tests -copying src/tests/test_implementation.py -> mss-{__version__}/src/tests -copying src/tests/test_issue_220.py -> mss-{__version__}/src/tests -copying src/tests/test_leaks.py -> mss-{__version__}/src/tests -copying src/tests/test_macos.py -> mss-{__version__}/src/tests -copying src/tests/test_save.py -> mss-{__version__}/src/tests -copying src/tests/test_setup.py -> mss-{__version__}/src/tests -copying src/tests/test_third_party.py -> mss-{__version__}/src/tests -copying src/tests/test_tools.py -> mss-{__version__}/src/tests -copying src/tests/test_windows.py -> mss-{__version__}/src/tests -copying src/tests/res/monitor-1024x768.raw.zip -> mss-{__version__}/src/tests/res -Writing mss-{__version__}/setup.cfg - """ - - print(output) - for line in expected.splitlines(): - if not (line := line.strip()): - continue - assert line in output - assert output.count("copying") == expected.count("copying") - assert f"Successfully built mss-{__version__}.tar.gz" in output + file = f"mss-{__version__}.tar.gz" + assert f"Successfully built {file}" in output assert "warning" not in output.lower() - check_call(CHECK) + check_call(CHECK + [f"dist/{file}"]) + + with tarfile.open(f"dist/{file}", mode="r:gz") as fh: + files = sorted(fh.getnames()) + + assert files == [ + f"mss-{__version__}/.gitignore", + f"mss-{__version__}/CHANGELOG.md", + f"mss-{__version__}/CHANGES.md", + f"mss-{__version__}/CONTRIBUTORS.md", + f"mss-{__version__}/LICENSE.txt", + f"mss-{__version__}/PKG-INFO", + f"mss-{__version__}/README.md", + f"mss-{__version__}/docs/source/api.rst", + f"mss-{__version__}/docs/source/conf.py", + f"mss-{__version__}/docs/source/developers.rst", + f"mss-{__version__}/docs/source/examples.rst", + f"mss-{__version__}/docs/source/examples/callback.py", + f"mss-{__version__}/docs/source/examples/custom_cls_image.py", + f"mss-{__version__}/docs/source/examples/fps.py", + f"mss-{__version__}/docs/source/examples/fps_multiprocessing.py", + f"mss-{__version__}/docs/source/examples/from_pil_tuple.py", + f"mss-{__version__}/docs/source/examples/linux_display_keyword.py", + f"mss-{__version__}/docs/source/examples/opencv_numpy.py", + f"mss-{__version__}/docs/source/examples/part_of_screen.py", + f"mss-{__version__}/docs/source/examples/part_of_screen_monitor_2.py", + f"mss-{__version__}/docs/source/examples/pil.py", + f"mss-{__version__}/docs/source/examples/pil_pixels.py", + f"mss-{__version__}/docs/source/index.rst", + f"mss-{__version__}/docs/source/installation.rst", + f"mss-{__version__}/docs/source/support.rst", + f"mss-{__version__}/docs/source/usage.rst", + f"mss-{__version__}/docs/source/where.rst", + f"mss-{__version__}/pyproject.toml", + f"mss-{__version__}/src/mss/__init__.py", + f"mss-{__version__}/src/mss/__main__.py", + f"mss-{__version__}/src/mss/base.py", + f"mss-{__version__}/src/mss/darwin.py", + f"mss-{__version__}/src/mss/exception.py", + f"mss-{__version__}/src/mss/factory.py", + f"mss-{__version__}/src/mss/linux.py", + f"mss-{__version__}/src/mss/models.py", + f"mss-{__version__}/src/mss/py.typed", + f"mss-{__version__}/src/mss/screenshot.py", + f"mss-{__version__}/src/mss/tools.py", + f"mss-{__version__}/src/mss/windows.py", + f"mss-{__version__}/src/tests/bench_bgra2rgb.py", + f"mss-{__version__}/src/tests/bench_general.py", + f"mss-{__version__}/src/tests/conftest.py", + f"mss-{__version__}/src/tests/res/monitor-1024x768.raw.zip", + f"mss-{__version__}/src/tests/test_bgra_to_rgb.py", + f"mss-{__version__}/src/tests/test_cls_image.py", + f"mss-{__version__}/src/tests/test_find_monitors.py", + f"mss-{__version__}/src/tests/test_get_pixels.py", + f"mss-{__version__}/src/tests/test_gnu_linux.py", + f"mss-{__version__}/src/tests/test_implementation.py", + f"mss-{__version__}/src/tests/test_issue_220.py", + f"mss-{__version__}/src/tests/test_leaks.py", + f"mss-{__version__}/src/tests/test_macos.py", + f"mss-{__version__}/src/tests/test_save.py", + f"mss-{__version__}/src/tests/test_setup.py", + f"mss-{__version__}/src/tests/test_third_party.py", + f"mss-{__version__}/src/tests/test_tools.py", + f"mss-{__version__}/src/tests/test_windows.py", + ] def test_wheel(): output = check_output(WHEEL, stderr=STDOUT, text=True) - expected = f""" -creating build/bdist.linux-x86_64/wheel/mss-{__version__}.dist-info/WHEEL - and adding 'build/bdist.linux-x86_64/wheel' to it -adding 'mss/__init__.py' -adding 'mss/__main__.py' -adding 'mss/base.py' -adding 'mss/darwin.py' -adding 'mss/exception.py' -adding 'mss/factory.py' -adding 'mss/linux.py' -adding 'mss/models.py' -adding 'mss/py.typed' -adding 'mss/screenshot.py' -adding 'mss/tools.py' -adding 'mss/windows.py' -adding 'mss-{__version__}.dist-info/METADATA' -adding 'mss-{__version__}.dist-info/WHEEL' -adding 'mss-{__version__}.dist-info/entry_points.txt' -adding 'mss-{__version__}.dist-info/top_level.txt' -adding 'mss-{__version__}.dist-info/RECORD' - """ - - print(output) - for line in expected.splitlines(): - if not (line := line.strip()): - continue - assert line in output - assert output.count("adding") == expected.count("adding") - assert f"Successfully built mss-{__version__}-py3-none-any.whl" in output + file = f"mss-{__version__}-py3-none-any.whl" + assert f"Successfully built {file}" in output assert "warning" not in output.lower() - check_call(CHECK) + check_call(CHECK + [f"dist/{file}"]) + + with ZipFile(f"dist/{file}") as fh: + files = sorted(fh.namelist()) + + assert files == [ + f"mss-{__version__}.dist-info/METADATA", + f"mss-{__version__}.dist-info/RECORD", + f"mss-{__version__}.dist-info/WHEEL", + f"mss-{__version__}.dist-info/entry_points.txt", + f"mss-{__version__}.dist-info/licenses/LICENSE.txt", + "mss/__init__.py", + "mss/__main__.py", + "mss/base.py", + "mss/darwin.py", + "mss/exception.py", + "mss/factory.py", + "mss/linux.py", + "mss/models.py", + "mss/py.typed", + "mss/screenshot.py", + "mss/tools.py", + "mss/windows.py", + ] diff --git a/tests-requirements.txt b/tests-requirements.txt deleted file mode 100644 index 59484eba..00000000 --- a/tests-requirements.txt +++ /dev/null @@ -1,7 +0,0 @@ -numpy -pillow -pytest -pytest-cov -pytest-rerunfailures -pyvirtualdisplay; sys_platform == "linux" -sphinx From 0e5808dcecbc8bdcad6e0871bdb9008b786500f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Thu, 27 Apr 2023 09:43:18 +0200 Subject: [PATCH 150/280] CI: automate release publishing on tag creation --- .github/workflows/release.yml | 36 +++++++++++++++++++++++++++++++++++ CHANGELOG.md | 1 + README.md | 11 ----------- docs/source/conf.py | 4 ++-- src/mss/__init__.py | 5 +++-- 5 files changed, 42 insertions(+), 15 deletions(-) create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..fcac89ac --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,36 @@ +name: Release + +on: + push: + tags: + - '*' + workflow_dispatch: + +jobs: + release: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Install Python + uses: actions/setup-python@v4 + with: + python-version: "3.x" + cache: pip + - name: Install build dependencies + run: | + python -m pip install -U pip + python -m pip install -e '.[dev]' + - name: Build + run: python -m build + - name: Check + run: twine check --strict dist/* + - name: What will we publish? + run: ls -l dist + - name: Publish + uses: pypa/gh-action-pypi-publish@master + with: + user: __token__ + password: ${{ secrets.PYPI_API_TOKEN }} + skip_existing: true + print_hash: true diff --git a/CHANGELOG.md b/CHANGELOG.md index eebccd66..887a0c81 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ See Git checking messages for full history. ## 9.0.2 (2023/xx/xx) - level up the packaging using `hatchling` +- CI: automated release publishing on tag creation - :heart: contributors: @ ## 9.0.1 (2023/04/20) diff --git a/README.md b/README.md index 7b58256e..eb0bfce0 100644 --- a/README.md +++ b/README.md @@ -41,14 +41,3 @@ Or you can install it with conda: ```shell conda install -c conda-forge python-mss ``` - -## Maintenance - -For the maintainers, here are commands to upload a new release: - -```shell -rm -rf build dist -python -m build -twine check dist/* -twine upload dist/* -``` diff --git a/docs/source/conf.py b/docs/source/conf.py index 42164fea..aab04ec1 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -6,7 +6,7 @@ sys.path.insert(0, str(Path(__file__).parent.parent.parent / "src")) -from mss import __version__ # noqa +from mss import __author__, __date__, __version__ # noqa # -- General configuration ------------------------------------------------ @@ -29,7 +29,7 @@ # General information about the project. project = "Python MSS" -copyright = "2013-2023, Mickaël 'Tiger-222' Schoentgen & contributors" +copyright = f"{__date__}, {__author__} & contributors" author = "Tiger-222" # The version info for the project you're documenting, acts as replacement for diff --git a/src/mss/__init__.py b/src/mss/__init__.py index 77c563ef..75cabbcb 100644 --- a/src/mss/__init__.py +++ b/src/mss/__init__.py @@ -13,8 +13,9 @@ __version__ = "9.0.2" __author__ = "Mickaël 'Tiger-222' Schoentgen" -__copyright__ = """ -Copyright (c) 2013-2023, Mickaël 'Tiger-222' Schoentgen +__date__ = "2013-2023" +__copyright__ = f""" +Copyright (c) {__date__}, {__author__} Permission to use, copy, modify, and distribute this software and its documentation for any purpose and without fee or royalty is hereby From 600827a13da20b256d02b61bde32d1c26bdce07d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 16 Nov 2023 13:29:20 +0100 Subject: [PATCH 151/280] build(deps): bump actions/checkout from 3 to 4 (#266) Bumps [actions/checkout](https://github.com/actions/checkout) from 3 to 4. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v3...v4) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/release.yml | 2 +- .github/workflows/tests.yml | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index fcac89ac..f06174a8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install Python uses: actions/setup-python@v4 with: diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index f46daa37..6e1b05d9 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -12,7 +12,7 @@ jobs: name: Quality runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: actions/setup-python@v4 with: python-version: "3.x" @@ -28,7 +28,7 @@ jobs: name: Documentation runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: actions/setup-python@v4 with: python-version: "3.x" @@ -68,7 +68,7 @@ jobs: - name: PyPy 3.9 runs-on: "pypy-3.9" steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: actions/setup-python@v4 with: python-version: ${{ matrix.python.runs-on }} From e66199adcdad737ef4d66183083bd26c827090da Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 6 Dec 2023 14:15:17 +0100 Subject: [PATCH 152/280] build(deps): bump actions/setup-python from 4 to 5 (#271) Bumps [actions/setup-python](https://github.com/actions/setup-python) from 4 to 5. - [Release notes](https://github.com/actions/setup-python/releases) - [Commits](https://github.com/actions/setup-python/compare/v4...v5) --- updated-dependencies: - dependency-name: actions/setup-python dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/release.yml | 2 +- .github/workflows/tests.yml | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f06174a8..a4127b8f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -13,7 +13,7 @@ jobs: - name: Checkout uses: actions/checkout@v4 - name: Install Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: "3.x" cache: pip diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 6e1b05d9..8b0b4f45 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: actions/setup-python@v4 + - uses: actions/setup-python@v5 with: python-version: "3.x" cache: pip @@ -29,7 +29,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: actions/setup-python@v4 + - uses: actions/setup-python@v5 with: python-version: "3.x" cache: pip @@ -69,7 +69,7 @@ jobs: runs-on: "pypy-3.9" steps: - uses: actions/checkout@v4 - - uses: actions/setup-python@v4 + - uses: actions/setup-python@v5 with: python-version: ${{ matrix.python.runs-on }} cache: pip From 0262c856dee148f303f89d3ffacfed5be1e601aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Sat, 10 Feb 2024 12:29:20 +0100 Subject: [PATCH 153/280] ci: pin pypa/gh-action-pypi-publish --- .github/workflows/release.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a4127b8f..871822d8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -19,7 +19,7 @@ jobs: cache: pip - name: Install build dependencies run: | - python -m pip install -U pip + python -m pip install -U pip python -m pip install -e '.[dev]' - name: Build run: python -m build @@ -28,7 +28,7 @@ jobs: - name: What will we publish? run: ls -l dist - name: Publish - uses: pypa/gh-action-pypi-publish@master + uses: pypa/gh-action-pypi-publish@release/v1 with: user: __token__ password: ${{ secrets.PYPI_API_TOKEN }} From 0937e2fd9069f270998ae2f1b8849251cc38ec3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Mon, 26 Feb 2024 17:10:17 +0100 Subject: [PATCH 154/280] chore: update dates --- LICENSE.txt | 2 +- pyproject.toml | 4 ++-- src/mss/__init__.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/LICENSE.txt b/LICENSE.txt index 8d49e5d5..bdcbc505 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,5 +1,5 @@ MIT License -Copyright (c) 2013-2023, Mickaël 'Tiger-222' Schoentgen +Copyright (c) 2013-2024, Mickaël 'Tiger-222' Schoentgen Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: diff --git a/pyproject.toml b/pyproject.toml index c352b799..f9b41b21 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,10 +8,10 @@ description = "An ultra fast cross-platform multiple screenshots module in pure readme = "README.md" requires-python = ">= 3.8" authors = [ - { name = "Mickaël 'Tiger-222' Schoentgen", email="contact@tiger-222.fr" }, + { name = "Mickaël Schoentgen", email="contact@tiger-222.fr" }, ] maintainers = [ - { name = "Mickaël 'Tiger-222' Schoentgen", email="contact@tiger-222.fr" }, + { name = "Mickaël Schoentgen", email="contact@tiger-222.fr" }, ] license = { file = "LICENSE.txt" } classifiers = [ diff --git a/src/mss/__init__.py b/src/mss/__init__.py index 75cabbcb..500983ce 100644 --- a/src/mss/__init__.py +++ b/src/mss/__init__.py @@ -13,7 +13,7 @@ __version__ = "9.0.2" __author__ = "Mickaël 'Tiger-222' Schoentgen" -__date__ = "2013-2023" +__date__ = "2013-2024" __copyright__ = f""" Copyright (c) {__date__}, {__author__} From b9893476418dd4b3c6a09968f14d4421c58fff38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Mon, 26 Feb 2024 18:15:30 +0100 Subject: [PATCH 155/280] chore: review gitignore file --- .gitignore | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/.gitignore b/.gitignore index 361d6114..d117f10a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,11 +1,6 @@ -build/ -.cache/ +# Files .coverage -dist/ *.doctree -docs_out/ -*.egg-info/ -.idea/ .DS_Store *.orig *.jpg @@ -13,9 +8,18 @@ docs_out/ *.png.old *.pickle *.pyc -.pytest_cache -.vscode + +# Folders +build/ +.cache/ +dist/ +docs_out/ +*.egg-info/ +.idea/ +.pytest_cache/ +.vscode/ docs/output/ .mypy_cache/ __pycache__/ +ruff_cache/ venv/ 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 156/280] 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 0e48e93f..7bb90ae2 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 aab04ec1..20ae6344 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 147c9525..a1071762 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 4232e490..c57e1113 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 4046f2ab..7a338439 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 28caf593..a54ac3e8 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 61f2d94b..c5ed5f44 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 a0b7b409..2070aea3 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 81130ad3..94bdbc39 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 73f93cb6..bcc17bb3 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 61f58f76..56bfbdc3 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 db10f1b4..01a6b01d 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 fcedcec7..54c57227 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 f9b41b21..5a0d1252 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 500983ce..cb490e2f 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 4dff5a1a..9b74506f 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 14a45288..4495bf14 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 1dd37eab..f247c516 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 9ffb94b6..42013672 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 30e15c2a..fea7df31 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 8b118e89..7d0c8fca 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 9c0851a5..a6a7bf8f 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 9c82d726..cad551b9 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 de3a1afb..316939cd 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 a8c28d32..fab27942 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 23196842..49043f5c 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 1fe44cc5..cec26d52 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 72da4df4..a97ccc39 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 1fa2c042..ddd9529e 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 b531ba10..eb6b8596 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 278dc1b0..7939e173 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 8535538e..ec85f7f7 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 1357790e..cca29abb 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 caeca228..2ec96ac1 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 3fefccca..203147ef 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 7e6a25e9..7c0fd0f7 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 113b33e1..9346581f 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 6dfbc19c..a46a4fb2 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 5bec6fb6..1bd5a667 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 e89afbdf..b5443c74 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 d939cb1c..618f682e 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 3f247ca8..12273955 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))) From 599f86c452b2f043ee94c11cc5cbb5aee387263c Mon Sep 17 00:00:00 2001 From: Andon Li <60678140+Andon-Li@users.noreply.github.com> Date: Tue, 27 Feb 2024 10:25:50 -0500 Subject: [PATCH 157/280] fix: Pixels model correction (#274) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Changed Pixels model and updated documentation. * Update screenshot.py --------- Co-authored-by: Mickaël Schoentgen --- docs/source/api.rst | 4 ++-- src/mss/models.py | 2 +- src/mss/screenshot.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/source/api.rst b/docs/source/api.rst index 3eae42b8..cef76dab 100644 --- a/docs/source/api.rst +++ b/docs/source/api.rst @@ -312,9 +312,9 @@ Properties .. attribute:: pixels - List of RGB tuples. + List of row tuples that contain RGB tuples. - :rtype: list[tuple(int, int, int)] + :rtype: list[tuple(tuple(int, int, int), ...)] .. attribute:: pos diff --git a/src/mss/models.py b/src/mss/models.py index a6a7bf8f..e756d629 100644 --- a/src/mss/models.py +++ b/src/mss/models.py @@ -8,7 +8,7 @@ Monitors = List[Monitor] Pixel = Tuple[int, int, int] -Pixels = List[Pixel] +Pixels = List[Tuple[Pixel, ...]] CFunctions = Dict[str, Tuple[str, List[Any], Any]] diff --git a/src/mss/screenshot.py b/src/mss/screenshot.py index cad551b9..2d5f72a5 100644 --- a/src/mss/screenshot.py +++ b/src/mss/screenshot.py @@ -118,7 +118,7 @@ def pixel(self, coord_x: int, coord_y: int) -> Pixel: :return tuple: The pixel value as (R, G, B). """ try: - return self.pixels[coord_y][coord_x] # type: ignore[return-value] + return self.pixels[coord_y][coord_x] except IndexError as exc: msg = f"Pixel location ({coord_x}, {coord_y}) is out of range." raise ScreenShotError(msg) from exc From 4cbff748b12d9b96fb1418e532ca6bc939072567 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Tue, 27 Feb 2024 16:29:47 +0100 Subject: [PATCH 158/280] doc: tweak --- CHANGELOG.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 887a0c81..03362dd3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,8 +4,11 @@ See Git checking messages for full history. ## 9.0.2 (2023/xx/xx) - level up the packaging using `hatchling` +- use `ruff` to lint th code base (#275) +- MSS: minor optimization when using an output file format without date (#275) +- MSS: fixed `Pixel` model type (#274) - CI: automated release publishing on tag creation -- :heart: contributors: @ +- :heart: contributors: @Andon-Li ## 9.0.1 (2023/04/20) - CLI: fixed entry point not taking into account arguments From 4741c0bfcfa6ab2fa836196442ea52a4cede0e8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Tue, 27 Feb 2024 16:30:27 +0100 Subject: [PATCH 159/280] doc: tweak --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 03362dd3..e0e18ea6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,8 +3,8 @@ See Git checking messages for full history. ## 9.0.2 (2023/xx/xx) -- level up the packaging using `hatchling` -- use `ruff` to lint th code base (#275) +- leveled up the packaging using `hatchling` +- used `ruff` to lint the code base (#275) - MSS: minor optimization when using an output file format without date (#275) - MSS: fixed `Pixel` model type (#274) - CI: automated release publishing on tag creation From ba5366966f7f1f501dbe909f76860fd75cb3909f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Tue, 23 Jul 2024 23:31:30 +0200 Subject: [PATCH 160/280] Update FUNDING.yml --- .github/FUNDING.yml | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 204c47d0..f5c8e790 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,12 +1,3 @@ -# These are supported funding model platforms - github: BoboTiG -patreon: # Replace with a single Patreon username -open_collective: # Replace with a single Open Collective username -ko_fi: # Replace with a single Ko-fi username -tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel -community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry -liberapay: # Replace with a single Liberapay username +polar: tiger-222 issuehunt: BoboTiG -otechie: # Replace with a single Otechie username -custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] From 13ae8f65f159e34f9de5774be975be66bdf33f1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Tue, 30 Jul 2024 13:59:55 +0200 Subject: [PATCH 161/280] fix: run ruff --- check.sh | 2 +- docs/source/examples/callback.py | 1 + docs/source/examples/custom_cls_image.py | 1 + docs/source/examples/fps.py | 1 + docs/source/examples/fps_multiprocessing.py | 1 + docs/source/examples/from_pil_tuple.py | 1 + docs/source/examples/linux_display_keyword.py | 1 + docs/source/examples/opencv_numpy.py | 1 + docs/source/examples/part_of_screen.py | 1 + docs/source/examples/part_of_screen_monitor_2.py | 1 + docs/source/examples/pil.py | 1 + docs/source/examples/pil_pixels.py | 1 + src/mss/__init__.py | 1 + src/mss/__main__.py | 1 + src/mss/base.py | 1 + src/mss/darwin.py | 1 + src/mss/exception.py | 1 + src/mss/factory.py | 1 + src/mss/linux.py | 1 + src/mss/screenshot.py | 1 + src/mss/tools.py | 1 + src/mss/windows.py | 1 + src/tests/bench_bgra2rgb.py | 1 + src/tests/bench_general.py | 1 + src/tests/conftest.py | 1 + src/tests/test_bgra_to_rgb.py | 1 + src/tests/test_cls_image.py | 1 + src/tests/test_find_monitors.py | 1 + src/tests/test_gnu_linux.py | 1 + src/tests/test_implementation.py | 1 + src/tests/test_issue_220.py | 1 + src/tests/test_leaks.py | 1 + src/tests/test_macos.py | 1 + src/tests/test_save.py | 1 + src/tests/test_setup.py | 1 + src/tests/test_third_party.py | 1 + src/tests/test_tools.py | 1 + src/tests/test_windows.py | 1 + 38 files changed, 38 insertions(+), 1 deletion(-) diff --git a/check.sh b/check.sh index 7bb90ae2..8028ab20 100755 --- a/check.sh +++ b/check.sh @@ -4,8 +4,8 @@ # set -eu -python -m ruff --fix docs src python -m ruff format docs src +python -m ruff check --fix 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 src docs/source/examples diff --git a/docs/source/examples/callback.py b/docs/source/examples/callback.py index a1071762..cb644436 100644 --- a/docs/source/examples/callback.py +++ b/docs/source/examples/callback.py @@ -3,6 +3,7 @@ Screenshot of the monitor 1, with callback. """ + import os import os.path diff --git a/docs/source/examples/custom_cls_image.py b/docs/source/examples/custom_cls_image.py index c57e1113..f3575794 100644 --- a/docs/source/examples/custom_cls_image.py +++ b/docs/source/examples/custom_cls_image.py @@ -3,6 +3,7 @@ Screenshot of the monitor 1, using a custom class to handle the data. """ + from typing import Any import mss diff --git a/docs/source/examples/fps.py b/docs/source/examples/fps.py index 7a338439..537e2eb1 100644 --- a/docs/source/examples/fps.py +++ b/docs/source/examples/fps.py @@ -4,6 +4,7 @@ Simple naive benchmark to compare with: https://pythonprogramming.net/game-frames-open-cv-python-plays-gta-v/ """ + import time import cv2 diff --git a/docs/source/examples/fps_multiprocessing.py b/docs/source/examples/fps_multiprocessing.py index a54ac3e8..c83455f4 100644 --- a/docs/source/examples/fps_multiprocessing.py +++ b/docs/source/examples/fps_multiprocessing.py @@ -4,6 +4,7 @@ Example using the multiprocessing module to speed-up screen capture. https://github.com/pythonlessons/TensorFlow-object-detection-tutorial """ + from multiprocessing import Process, Queue import mss diff --git a/docs/source/examples/from_pil_tuple.py b/docs/source/examples/from_pil_tuple.py index c5ed5f44..3c5297b9 100644 --- a/docs/source/examples/from_pil_tuple.py +++ b/docs/source/examples/from_pil_tuple.py @@ -3,6 +3,7 @@ Use PIL bbox style and percent values. """ + import mss import mss.tools diff --git a/docs/source/examples/linux_display_keyword.py b/docs/source/examples/linux_display_keyword.py index 2070aea3..bb6c3950 100644 --- a/docs/source/examples/linux_display_keyword.py +++ b/docs/source/examples/linux_display_keyword.py @@ -3,6 +3,7 @@ Usage example with a specific display. """ + import mss with mss.mss(display=":0.0") as sct: diff --git a/docs/source/examples/opencv_numpy.py b/docs/source/examples/opencv_numpy.py index 94bdbc39..87d11dd5 100644 --- a/docs/source/examples/opencv_numpy.py +++ b/docs/source/examples/opencv_numpy.py @@ -3,6 +3,7 @@ OpenCV/Numpy example. """ + import time import cv2 diff --git a/docs/source/examples/part_of_screen.py b/docs/source/examples/part_of_screen.py index bcc17bb3..5ef341dc 100644 --- a/docs/source/examples/part_of_screen.py +++ b/docs/source/examples/part_of_screen.py @@ -3,6 +3,7 @@ Example to capture part of the screen. """ + import mss import mss.tools diff --git a/docs/source/examples/part_of_screen_monitor_2.py b/docs/source/examples/part_of_screen_monitor_2.py index 56bfbdc3..6099f58a 100644 --- a/docs/source/examples/part_of_screen_monitor_2.py +++ b/docs/source/examples/part_of_screen_monitor_2.py @@ -3,6 +3,7 @@ Example to capture part of the screen of the monitor 2. """ + import mss import mss.tools diff --git a/docs/source/examples/pil.py b/docs/source/examples/pil.py index 01a6b01d..cdf33ed3 100644 --- a/docs/source/examples/pil.py +++ b/docs/source/examples/pil.py @@ -3,6 +3,7 @@ PIL example using frombytes(). """ + import mss from PIL import Image diff --git a/docs/source/examples/pil_pixels.py b/docs/source/examples/pil_pixels.py index 54c57227..5a5d6772 100644 --- a/docs/source/examples/pil_pixels.py +++ b/docs/source/examples/pil_pixels.py @@ -3,6 +3,7 @@ PIL examples to play with pixels. """ + import mss from PIL import Image diff --git a/src/mss/__init__.py b/src/mss/__init__.py index cb490e2f..6a8595af 100644 --- a/src/mss/__init__.py +++ b/src/mss/__init__.py @@ -7,6 +7,7 @@ https://github.com/BoboTiG/python-mss If that URL should fail, try contacting the author. """ + from mss.exception import ScreenShotError from mss.factory import mss diff --git a/src/mss/__main__.py b/src/mss/__main__.py index 9b74506f..1cac8fca 100644 --- a/src/mss/__main__.py +++ b/src/mss/__main__.py @@ -1,6 +1,7 @@ """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 diff --git a/src/mss/base.py b/src/mss/base.py index 4495bf14..4438693f 100644 --- a/src/mss/base.py +++ b/src/mss/base.py @@ -1,6 +1,7 @@ """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 diff --git a/src/mss/darwin.py b/src/mss/darwin.py index f247c516..ce951efb 100644 --- a/src/mss/darwin.py +++ b/src/mss/darwin.py @@ -1,6 +1,7 @@ """This is part of the MSS Python's module. Source: https://github.com/BoboTiG/python-mss. """ + from __future__ import annotations import ctypes diff --git a/src/mss/exception.py b/src/mss/exception.py index 42013672..61b3e69f 100644 --- a/src/mss/exception.py +++ b/src/mss/exception.py @@ -1,6 +1,7 @@ """This is part of the MSS Python's module. Source: https://github.com/BoboTiG/python-mss. """ + from __future__ import annotations from typing import Any, Dict diff --git a/src/mss/factory.py b/src/mss/factory.py index fea7df31..83ea0d32 100644 --- a/src/mss/factory.py +++ b/src/mss/factory.py @@ -1,6 +1,7 @@ """This is part of the MSS Python's module. Source: https://github.com/BoboTiG/python-mss. """ + import platform from typing import Any diff --git a/src/mss/linux.py b/src/mss/linux.py index 7d0c8fca..54aa72ea 100644 --- a/src/mss/linux.py +++ b/src/mss/linux.py @@ -1,6 +1,7 @@ """This is part of the MSS Python's module. Source: https://github.com/BoboTiG/python-mss. """ + from __future__ import annotations import os diff --git a/src/mss/screenshot.py b/src/mss/screenshot.py index 2d5f72a5..c795361e 100644 --- a/src/mss/screenshot.py +++ b/src/mss/screenshot.py @@ -1,6 +1,7 @@ """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 diff --git a/src/mss/tools.py b/src/mss/tools.py index 316939cd..2383665f 100644 --- a/src/mss/tools.py +++ b/src/mss/tools.py @@ -1,6 +1,7 @@ """This is part of the MSS Python's module. Source: https://github.com/BoboTiG/python-mss. """ + from __future__ import annotations import os diff --git a/src/mss/windows.py b/src/mss/windows.py index fab27942..2e102745 100644 --- a/src/mss/windows.py +++ b/src/mss/windows.py @@ -1,6 +1,7 @@ """This is part of the MSS Python's module. Source: https://github.com/BoboTiG/python-mss. """ + from __future__ import annotations import ctypes diff --git a/src/tests/bench_bgra2rgb.py b/src/tests/bench_bgra2rgb.py index 49043f5c..299561d5 100644 --- a/src/tests/bench_bgra2rgb.py +++ b/src/tests/bench_bgra2rgb.py @@ -27,6 +27,7 @@ numpy_flip 25 numpy_slice 22 """ + import time import mss diff --git a/src/tests/bench_general.py b/src/tests/bench_general.py index cec26d52..5de4f1c1 100644 --- a/src/tests/bench_general.py +++ b/src/tests/bench_general.py @@ -24,6 +24,7 @@ access_rgb 574 712 +24.04 output 139 188 +35.25 """ + from __future__ import annotations from time import time diff --git a/src/tests/conftest.py b/src/tests/conftest.py index a97ccc39..5a4964bc 100644 --- a/src/tests/conftest.py +++ b/src/tests/conftest.py @@ -1,6 +1,7 @@ """This is part of the MSS Python's module. Source: https://github.com/BoboTiG/python-mss. """ + import glob import os import platform diff --git a/src/tests/test_bgra_to_rgb.py b/src/tests/test_bgra_to_rgb.py index ddd9529e..99e55311 100644 --- a/src/tests/test_bgra_to_rgb.py +++ b/src/tests/test_bgra_to_rgb.py @@ -1,6 +1,7 @@ """This is part of the MSS Python's module. Source: https://github.com/BoboTiG/python-mss. """ + import pytest from mss.base import ScreenShot diff --git a/src/tests/test_cls_image.py b/src/tests/test_cls_image.py index eb6b8596..10c9cfed 100644 --- a/src/tests/test_cls_image.py +++ b/src/tests/test_cls_image.py @@ -1,6 +1,7 @@ """This is part of the MSS Python's module. Source: https://github.com/BoboTiG/python-mss. """ + import os from typing import Any diff --git a/src/tests/test_find_monitors.py b/src/tests/test_find_monitors.py index 7939e173..81c6e1ea 100644 --- a/src/tests/test_find_monitors.py +++ b/src/tests/test_find_monitors.py @@ -1,6 +1,7 @@ """This is part of the MSS Python's module. Source: https://github.com/BoboTiG/python-mss. """ + import os from mss import mss diff --git a/src/tests/test_gnu_linux.py b/src/tests/test_gnu_linux.py index cca29abb..8f9f68f4 100644 --- a/src/tests/test_gnu_linux.py +++ b/src/tests/test_gnu_linux.py @@ -1,6 +1,7 @@ """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 diff --git a/src/tests/test_implementation.py b/src/tests/test_implementation.py index 2ec96ac1..23413cf5 100644 --- a/src/tests/test_implementation.py +++ b/src/tests/test_implementation.py @@ -1,6 +1,7 @@ """This is part of the MSS Python's module. Source: https://github.com/BoboTiG/python-mss. """ + from __future__ import annotations import os diff --git a/src/tests/test_issue_220.py b/src/tests/test_issue_220.py index 203147ef..e76f78a4 100644 --- a/src/tests/test_issue_220.py +++ b/src/tests/test_issue_220.py @@ -1,6 +1,7 @@ """This is part of the MSS Python's module. Source: https://github.com/BoboTiG/python-mss. """ + import mss import pytest diff --git a/src/tests/test_leaks.py b/src/tests/test_leaks.py index 7c0fd0f7..57de1c78 100644 --- a/src/tests/test_leaks.py +++ b/src/tests/test_leaks.py @@ -1,6 +1,7 @@ """This is part of the MSS Python's module. Source: https://github.com/BoboTiG/python-mss. """ + import os import platform from typing import Callable diff --git a/src/tests/test_macos.py b/src/tests/test_macos.py index 9346581f..10e43ff4 100644 --- a/src/tests/test_macos.py +++ b/src/tests/test_macos.py @@ -1,6 +1,7 @@ """This is part of the MSS Python's module. Source: https://github.com/BoboTiG/python-mss. """ + import ctypes.util import platform diff --git a/src/tests/test_save.py b/src/tests/test_save.py index a46a4fb2..3ae0f6f8 100644 --- a/src/tests/test_save.py +++ b/src/tests/test_save.py @@ -1,6 +1,7 @@ """This is part of the MSS Python's module. Source: https://github.com/BoboTiG/python-mss. """ + import os.path from datetime import datetime diff --git a/src/tests/test_setup.py b/src/tests/test_setup.py index 1bd5a667..41b94d59 100644 --- a/src/tests/test_setup.py +++ b/src/tests/test_setup.py @@ -1,6 +1,7 @@ """This is part of the MSS Python's module. Source: https://github.com/BoboTiG/python-mss. """ + import platform import tarfile from subprocess import STDOUT, check_call, check_output diff --git a/src/tests/test_third_party.py b/src/tests/test_third_party.py index b5443c74..c62191b0 100644 --- a/src/tests/test_third_party.py +++ b/src/tests/test_third_party.py @@ -1,6 +1,7 @@ """This is part of the MSS Python's module. Source: https://github.com/BoboTiG/python-mss. """ + import itertools import os import os.path diff --git a/src/tests/test_tools.py b/src/tests/test_tools.py index 618f682e..2f41e495 100644 --- a/src/tests/test_tools.py +++ b/src/tests/test_tools.py @@ -1,6 +1,7 @@ """This is part of the MSS Python's module. Source: https://github.com/BoboTiG/python-mss. """ + import hashlib import os.path import zlib diff --git a/src/tests/test_windows.py b/src/tests/test_windows.py index 12273955..19a7056c 100644 --- a/src/tests/test_windows.py +++ b/src/tests/test_windows.py @@ -1,6 +1,7 @@ """This is part of the MSS Python's module. Source: https://github.com/BoboTiG/python-mss. """ + from __future__ import annotations import threading From 2de0e74fc03d30eb0af9ef50ef82c7ada6cb0845 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Tue, 30 Jul 2024 14:02:17 +0200 Subject: [PATCH 162/280] chore: update Sphinx conf --- docs/source/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 20ae6344..f9d35d23 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -12,7 +12,7 @@ extensions = ["sphinx.ext.intersphinx"] templates_path = ["_templates"] -source_suffix = ".rst" +source_suffix = {".rst": "restructuredtext"} master_doc = "index" # General information about the project. From 1210521b39e7d188bc842a604c8b3fd06f6df458 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Tue, 30 Jul 2024 14:05:08 +0200 Subject: [PATCH 163/280] tests: skip covered lines in coverage report --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 5a0d1252..2819e3ee 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -135,7 +135,7 @@ addopts = """ -r fE -vvv --cov=src/mss - --cov-report=term-missing + --cov-report=term-missing:skip-covered """ [tool.ruff] From 708969136fd76749a75fc96bcf5c1d7cb91d021c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Tue, 30 Jul 2024 14:23:03 +0200 Subject: [PATCH 164/280] feat: add support for Python 3.13 (#277) --- .github/workflows/tests.yml | 4 +++- CHANGELOG.md | 1 + pyproject.toml | 3 ++- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 8b0b4f45..61468c46 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -64,7 +64,9 @@ jobs: - name: CPython 3.11 runs-on: "3.11" - name: CPython 3.12 - runs-on: "3.12-dev" + runs-on: "3.12" + - name: CPython 3.13 + runs-on: "3.13-dev" - name: PyPy 3.9 runs-on: "pypy-3.9" steps: diff --git a/CHANGELOG.md b/CHANGELOG.md index e0e18ea6..ddb1a8fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ See Git checking messages for full history. ## 9.0.2 (2023/xx/xx) +- added support for Python 3.13 - leveled up the packaging using `hatchling` - used `ruff` to lint the code base (#275) - MSS: minor optimization when using an output file format without date (#275) diff --git a/pyproject.toml b/pyproject.toml index 2819e3ee..c8065c39 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,6 +36,7 @@ classifiers = [ "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", "Topic :: Multimedia :: Graphics :: Capture :: Screen Capture", "Topic :: Software Development :: Libraries", ] @@ -70,7 +71,7 @@ mss = "mss.__main__:main" [project.optional-dependencies] test = [ - "numpy", + "numpy ; sys_platform == 'windows' and python_version >= '3.13'", "pillow", "pytest", "pytest-cov", From 36cd527c9f6fcf58c89284c99970137c6d90cac1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Sun, 1 Sep 2024 09:38:09 +0200 Subject: [PATCH 165/280] chore: pin all deps --- pyproject.toml | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index c8065c39..50a398e1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -70,21 +70,21 @@ Tracker = "https://github.com/BoboTiG/python-mss/issues" mss = "mss.__main__:main" [project.optional-dependencies] -test = [ - "numpy ; sys_platform == 'windows' and python_version >= '3.13'", - "pillow", - "pytest", - "pytest-cov", - "pytest-rerunfailures", - "pyvirtualdisplay; sys_platform == 'linux'", - "sphinx", -] dev = [ - "build", - "mypy", - "ruff", - "twine", - "wheel", + "build==1.2.1", + "mypy==1.11.2", + "ruff==0.6.3", + "twine==5.1.1", + "wheel==0.44.0", +] +test = [ + "numpy==2.1.0 ; sys_platform == 'windows' and python_version >= '3.13'", + "pillow==10.4.0", + "pytest==8.3.2", + "pytest-cov==5.0.0", + "pytest-rerunfailures==14.0.0", + "pyvirtualdisplay==3.0 ; sys_platform == 'linux'", + "sphinx==8.0.2", ] [tool.hatch.version] From 557894d8db56738cc3638fa2b88088e15c11eeef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Sun, 1 Sep 2024 09:38:40 +0200 Subject: [PATCH 166/280] chore: run ruff --- docs/source/examples/fps.py | 3 ++- docs/source/examples/opencv_numpy.py | 3 ++- docs/source/examples/pil.py | 3 ++- docs/source/examples/pil_pixels.py | 3 ++- src/tests/bench_bgra2rgb.py | 5 +++-- src/tests/conftest.py | 1 + src/tests/test_bgra_to_rgb.py | 1 + src/tests/test_get_pixels.py | 1 + src/tests/test_gnu_linux.py | 5 +++-- src/tests/test_implementation.py | 3 ++- src/tests/test_issue_220.py | 5 +++-- src/tests/test_leaks.py | 5 +++-- src/tests/test_macos.py | 3 ++- src/tests/test_save.py | 1 + src/tests/test_setup.py | 1 + src/tests/test_third_party.py | 1 + src/tests/test_tools.py | 1 + src/tests/test_windows.py | 3 ++- 18 files changed, 33 insertions(+), 15 deletions(-) diff --git a/docs/source/examples/fps.py b/docs/source/examples/fps.py index 537e2eb1..4e4080ec 100644 --- a/docs/source/examples/fps.py +++ b/docs/source/examples/fps.py @@ -8,9 +8,10 @@ import time import cv2 -import mss import numpy as np +import mss + def screen_record() -> int: try: diff --git a/docs/source/examples/opencv_numpy.py b/docs/source/examples/opencv_numpy.py index 87d11dd5..9275de2b 100644 --- a/docs/source/examples/opencv_numpy.py +++ b/docs/source/examples/opencv_numpy.py @@ -7,9 +7,10 @@ import time import cv2 -import mss import numpy as np +import mss + with mss.mss() as sct: # Part of the screen to capture monitor = {"top": 40, "left": 0, "width": 800, "height": 640} diff --git a/docs/source/examples/pil.py b/docs/source/examples/pil.py index cdf33ed3..03ff778c 100644 --- a/docs/source/examples/pil.py +++ b/docs/source/examples/pil.py @@ -4,9 +4,10 @@ PIL example using frombytes(). """ -import mss from PIL import Image +import mss + with mss.mss() as sct: # Get rid of the first, as it represents the "All in One" monitor: for num, monitor in enumerate(sct.monitors[1:], 1): diff --git a/docs/source/examples/pil_pixels.py b/docs/source/examples/pil_pixels.py index 5a5d6772..d1264bc6 100644 --- a/docs/source/examples/pil_pixels.py +++ b/docs/source/examples/pil_pixels.py @@ -4,9 +4,10 @@ PIL examples to play with pixels. """ -import mss from PIL import Image +import mss + with mss.mss() as sct: # Get a screenshot of the 1st monitor sct_img = sct.grab(sct.monitors[1]) diff --git a/src/tests/bench_bgra2rgb.py b/src/tests/bench_bgra2rgb.py index 299561d5..6acaffb3 100644 --- a/src/tests/bench_bgra2rgb.py +++ b/src/tests/bench_bgra2rgb.py @@ -30,11 +30,12 @@ import time -import mss import numpy as np -from mss.screenshot import ScreenShot from PIL import Image +import mss +from mss.screenshot import ScreenShot + def mss_rgb(im: ScreenShot) -> bytes: return im.rgb diff --git a/src/tests/conftest.py b/src/tests/conftest.py index 5a4964bc..1234c8d5 100644 --- a/src/tests/conftest.py +++ b/src/tests/conftest.py @@ -11,6 +11,7 @@ from zipfile import ZipFile import pytest + from mss import mss diff --git a/src/tests/test_bgra_to_rgb.py b/src/tests/test_bgra_to_rgb.py index 99e55311..a481c1f1 100644 --- a/src/tests/test_bgra_to_rgb.py +++ b/src/tests/test_bgra_to_rgb.py @@ -3,6 +3,7 @@ """ import pytest + from mss.base import ScreenShot diff --git a/src/tests/test_get_pixels.py b/src/tests/test_get_pixels.py index ec85f7f7..211c7108 100644 --- a/src/tests/test_get_pixels.py +++ b/src/tests/test_get_pixels.py @@ -6,6 +6,7 @@ import os import pytest + from mss import mss from mss.base import ScreenShot from mss.exception import ScreenShotError diff --git a/src/tests/test_gnu_linux.py b/src/tests/test_gnu_linux.py index 8f9f68f4..b7d7829a 100644 --- a/src/tests/test_gnu_linux.py +++ b/src/tests/test_gnu_linux.py @@ -6,9 +6,10 @@ 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,7 +22,7 @@ DEPTH = 24 -@pytest.fixture() +@pytest.fixture def display() -> Generator: with pyvirtualdisplay.Display(size=(WIDTH, HEIGHT), color_depth=DEPTH) as vdisplay: yield vdisplay.new_display_var diff --git a/src/tests/test_implementation.py b/src/tests/test_implementation.py index 23413cf5..54abb272 100644 --- a/src/tests/test_implementation.py +++ b/src/tests/test_implementation.py @@ -12,9 +12,10 @@ from typing import TYPE_CHECKING from unittest.mock import Mock, patch +import pytest + import mss import mss.tools -import pytest from mss.__main__ import main as entry_point from mss.base import MSSBase from mss.exception import ScreenShotError diff --git a/src/tests/test_issue_220.py b/src/tests/test_issue_220.py index e76f78a4..24884c4b 100644 --- a/src/tests/test_issue_220.py +++ b/src/tests/test_issue_220.py @@ -2,13 +2,14 @@ Source: https://github.com/BoboTiG/python-mss. """ -import mss import pytest +import mss + tkinter = pytest.importorskip("tkinter") -@pytest.fixture() +@pytest.fixture def root() -> tkinter.Tk: # type: ignore[name-defined] try: master = tkinter.Tk() diff --git a/src/tests/test_leaks.py b/src/tests/test_leaks.py index 57de1c78..094438b3 100644 --- a/src/tests/test_leaks.py +++ b/src/tests/test_leaks.py @@ -6,9 +6,10 @@ import platform from typing import Callable -import mss import pytest +import mss + OS = platform.system().lower() PID = os.getpid() @@ -36,7 +37,7 @@ def get_handles() -> int: 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 diff --git a/src/tests/test_macos.py b/src/tests/test_macos.py index 10e43ff4..cce8121b 100644 --- a/src/tests/test_macos.py +++ b/src/tests/test_macos.py @@ -5,8 +5,9 @@ import ctypes.util import platform -import mss import pytest + +import mss from mss.exception import ScreenShotError if platform.system().lower() != "darwin": diff --git a/src/tests/test_save.py b/src/tests/test_save.py index 3ae0f6f8..a8d6e385 100644 --- a/src/tests/test_save.py +++ b/src/tests/test_save.py @@ -6,6 +6,7 @@ from datetime import datetime import pytest + from mss import mss try: diff --git a/src/tests/test_setup.py b/src/tests/test_setup.py index 41b94d59..2dba9e6f 100644 --- a/src/tests/test_setup.py +++ b/src/tests/test_setup.py @@ -8,6 +8,7 @@ from zipfile import ZipFile import pytest + from mss import __version__ if platform.system().lower() != "linux": diff --git a/src/tests/test_third_party.py b/src/tests/test_third_party.py index c62191b0..1313d862 100644 --- a/src/tests/test_third_party.py +++ b/src/tests/test_third_party.py @@ -7,6 +7,7 @@ import os.path import pytest + from mss import mss try: diff --git a/src/tests/test_tools.py b/src/tests/test_tools.py index 2f41e495..ff742e88 100644 --- a/src/tests/test_tools.py +++ b/src/tests/test_tools.py @@ -7,6 +7,7 @@ import zlib import pytest + from mss import mss from mss.tools import to_png diff --git a/src/tests/test_windows.py b/src/tests/test_windows.py index 19a7056c..f5d99663 100644 --- a/src/tests/test_windows.py +++ b/src/tests/test_windows.py @@ -7,8 +7,9 @@ import threading from typing import Tuple -import mss import pytest + +import mss from mss.exception import ScreenShotError try: From 136a5d96486941919db434183ad9705f9eb77a0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Sun, 1 Sep 2024 09:42:22 +0200 Subject: [PATCH 167/280] fix: mypy --- src/tests/test_setup.py | 4 +++- src/tests/third_party/__init__.py | 0 src/tests/third_party/test_numpy.py | 21 ++++++++++++++++++ .../test_pil.py} | 22 ++----------------- 4 files changed, 26 insertions(+), 21 deletions(-) create mode 100644 src/tests/third_party/__init__.py create mode 100644 src/tests/third_party/test_numpy.py rename src/tests/{test_third_party.py => third_party/test_pil.py} (70%) diff --git a/src/tests/test_setup.py b/src/tests/test_setup.py index 2dba9e6f..5fd2f81d 100644 --- a/src/tests/test_setup.py +++ b/src/tests/test_setup.py @@ -89,9 +89,11 @@ def test_sdist() -> None: f"mss-{__version__}/src/tests/test_macos.py", f"mss-{__version__}/src/tests/test_save.py", f"mss-{__version__}/src/tests/test_setup.py", - f"mss-{__version__}/src/tests/test_third_party.py", f"mss-{__version__}/src/tests/test_tools.py", f"mss-{__version__}/src/tests/test_windows.py", + f"mss-{__version__}/src/tests/third_party/__init__.py", + f"mss-{__version__}/src/tests/third_party/test_numpy.py", + f"mss-{__version__}/src/tests/third_party/test_pil.py", ] diff --git a/src/tests/third_party/__init__.py b/src/tests/third_party/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/tests/third_party/test_numpy.py b/src/tests/third_party/test_numpy.py new file mode 100644 index 00000000..a7a279dc --- /dev/null +++ b/src/tests/third_party/test_numpy.py @@ -0,0 +1,21 @@ +"""This is part of the MSS Python's module. +Source: https://github.com/BoboTiG/python-mss. +""" + +import os +import os.path + +import pytest + +from mss import mss + +pytest.importorskip("numpy", reason="Numpy module not available.") + +import numpy as np # noqa: E402 + + +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 = np.array(sct.grab(box)) + assert len(img) == 10 * pixel_ratio diff --git a/src/tests/test_third_party.py b/src/tests/third_party/test_pil.py similarity index 70% rename from src/tests/test_third_party.py rename to src/tests/third_party/test_pil.py index 1313d862..3555d7af 100644 --- a/src/tests/test_third_party.py +++ b/src/tests/third_party/test_pil.py @@ -10,27 +10,11 @@ from mss import mss -try: - import numpy as np -except (ImportError, RuntimeError): - # RuntimeError on Python 3.9 (macOS): Polyfit sanity test emitted a warning, ... - np = None +pytest.importorskip("PIL", reason="PIL module not available.") -try: - from PIL import Image -except ImportError: - Image = None +from PIL import Image # noqa: E402 -@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 = 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() -> None: width, height = 16, 16 box = {"top": 0, "left": 0, "width": width, "height": height} @@ -48,7 +32,6 @@ def test_pil() -> None: assert os.path.isfile("box.png") -@pytest.mark.skipif(Image is None, reason="PIL module not available.") def test_pil_bgra() -> None: width, height = 16, 16 box = {"top": 0, "left": 0, "width": width, "height": height} @@ -66,7 +49,6 @@ def test_pil_bgra() -> None: assert os.path.isfile("box-bgra.png") -@pytest.mark.skipif(Image is None, reason="PIL module not available.") def test_pil_not_16_rounded() -> None: width, height = 10, 10 box = {"top": 0, "left": 0, "width": width, "height": height} From e17e96a39d9a1f56c0ba2814c86610da1d200a08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Sun, 1 Sep 2024 12:31:49 +0200 Subject: [PATCH 168/280] Version 9.0.2 --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ddb1a8fa..94fb97c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ See Git checking messages for full history. -## 9.0.2 (2023/xx/xx) +## 9.0.2 (2024/09/01) - added support for Python 3.13 - leveled up the packaging using `hatchling` - used `ruff` to lint the code base (#275) From 97e7164eb48920cea8ec50c064ce77ca22791cdb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Sun, 1 Sep 2024 12:36:56 +0200 Subject: [PATCH 169/280] Bump the version --- CHANGELOG.md | 4 ++++ src/mss/__init__.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 94fb97c3..bff27284 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ See Git checking messages for full history. +## 10.0.0 (2024/xx/xx) +- +- :heart: contributors: @ + ## 9.0.2 (2024/09/01) - added support for Python 3.13 - leveled up the packaging using `hatchling` diff --git a/src/mss/__init__.py b/src/mss/__init__.py index 6a8595af..428819ca 100644 --- a/src/mss/__init__.py +++ b/src/mss/__init__.py @@ -11,7 +11,7 @@ from mss.exception import ScreenShotError from mss.factory import mss -__version__ = "9.0.2" +__version__ = "10.0.0" __author__ = "Mickaël Schoentgen" __date__ = "2013-2024" __copyright__ = f""" From f2162601b3c7d777e5dfd328bf78f16d078e551c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Sun, 1 Sep 2024 12:38:32 +0200 Subject: [PATCH 170/280] core!: remove support for Python 3.8 --- .github/workflows/tests.yml | 2 -- CHANGELOG.md | 2 +- README.md | 2 +- docs/source/index.rst | 2 +- docs/source/support.rst | 3 ++- pyproject.toml | 3 +-- 6 files changed, 6 insertions(+), 8 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 61468c46..70475d44 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -55,8 +55,6 @@ jobs: - emoji: 🪟 runs-on: [windows-latest] python: - - name: CPython 3.8 - runs-on: "3.8" - name: CPython 3.9 runs-on: "3.9" - name: CPython 3.10 diff --git a/CHANGELOG.md b/CHANGELOG.md index bff27284..dd09d146 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,7 @@ See Git checking messages for full history. ## 10.0.0 (2024/xx/xx) -- +- removed support for Python 3.8 - :heart: contributors: @ ## 9.0.2 (2024/09/01) diff --git a/README.md b/README.md index eb0bfce0..0fca3527 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ with mss() as sct: An ultra fast cross-platform multiple screenshots module in pure python using ctypes. -- **Python 3.8+**, PEP8 compliant, no dependency, thread-safe; +- **Python 3.9+**, PEP8 compliant, no dependency, thread-safe; - very basic, it will grab one screen shot by monitor or a screen shot of all monitors and save it to a PNG file; - but you can use PIL and benefit from all its formats (or add yours directly); - integrate well with Numpy and OpenCV; diff --git a/docs/source/index.rst b/docs/source/index.rst index c42c6712..3a0d03b8 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -12,7 +12,7 @@ Welcome to Python MSS's documentation! An ultra fast cross-platform multiple screenshots module in pure python using ctypes. - - **Python 3.8+**, :pep:`8` compliant, no dependency, thread-safe; + - **Python 3.9+**, :pep:`8` compliant, no dependency, thread-safe; - very basic, it will grab one screen shot by monitor or a screen shot of all monitors and save it to a PNG file; - but you can use PIL and benefit from all its formats (or add yours directly); - integrate well with Numpy and OpenCV; diff --git a/docs/source/support.rst b/docs/source/support.rst index 8a11d6b6..102dc01d 100644 --- a/docs/source/support.rst +++ b/docs/source/support.rst @@ -5,7 +5,7 @@ Support Feel free to try MSS on a system we had not tested, and let us know by creating an `issue `_. - OS: GNU/Linux, macOS and Windows - - Python: 3.8 and newer + - Python: 3.9 and newer Future @@ -34,3 +34,4 @@ Abandoned - Python 3.5 (2022-10-27) - Python 3.6 (2022-10-27) - Python 3.7 (2023-04-09) +- Python 3.8 (2024-09-01) diff --git a/pyproject.toml b/pyproject.toml index 50a398e1..ab7a25a3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "hatchling.build" name = "mss" description = "An ultra fast cross-platform multiple screenshots module in pure python using ctypes." readme = "README.md" -requires-python = ">= 3.8" +requires-python = ">= 3.9" authors = [ { name = "Mickaël Schoentgen", email="contact@tiger-222.fr" }, ] @@ -31,7 +31,6 @@ classifiers = [ "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", From 1b22eef6acc11a9cf306982a301195da40b1b464 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Sun, 1 Sep 2024 12:41:15 +0200 Subject: [PATCH 171/280] core: modernize following Python 3.8 drop --- check.sh | 2 +- pyproject.toml | 2 -- src/mss/base.py | 6 +++--- src/mss/exception.py | 4 ++-- src/mss/linux.py | 4 ++-- src/mss/screenshot.py | 4 ++-- src/tests/test_gnu_linux.py | 18 +++++++++++------- src/tests/test_implementation.py | 12 +++++++----- src/tests/test_windows.py | 3 +-- 9 files changed, 29 insertions(+), 26 deletions(-) diff --git a/check.sh b/check.sh index 8028ab20..d07b3576 100755 --- a/check.sh +++ b/check.sh @@ -5,7 +5,7 @@ set -eu python -m ruff format docs src -python -m ruff check --fix docs src +python -m ruff check --fix --unsafe-fixes 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 src docs/source/examples diff --git a/pyproject.toml b/pyproject.toml index ab7a25a3..282c7a87 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -165,10 +165,8 @@ ignore = [ "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"] diff --git a/src/mss/base.py b/src/mss/base.py index 4438693f..6d04c686 100644 --- a/src/mss/base.py +++ b/src/mss/base.py @@ -7,7 +7,7 @@ from abc import ABCMeta, abstractmethod from datetime import datetime from threading import Lock -from typing import TYPE_CHECKING, Any, List, Tuple +from typing import TYPE_CHECKING, Any from mss.exception import ScreenShotError from mss.screenshot import ScreenShot @@ -75,7 +75,7 @@ def _monitors_impl(self) -> None: def close(self) -> None: # noqa:B027 """Clean-up.""" - def grab(self, monitor: Monitor | Tuple[int, int, int, int], /) -> ScreenShot: + 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. @@ -245,7 +245,7 @@ def _merge(screenshot: ScreenShot, cursor: ScreenShot, /) -> ScreenShot: def _cfactory( attr: Any, func: str, - argtypes: List[Any], + argtypes: list[Any], restype: Any, /, errcheck: Callable | None = None, diff --git a/src/mss/exception.py b/src/mss/exception.py index 61b3e69f..7fdf2113 100644 --- a/src/mss/exception.py +++ b/src/mss/exception.py @@ -4,12 +4,12 @@ from __future__ import annotations -from typing import Any, Dict +from typing import Any class ScreenShotError(Exception): """Error handling class.""" - def __init__(self, message: str, /, *, details: Dict[str, Any] | None = 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/linux.py b/src/mss/linux.py index 54aa72ea..46ba33e3 100644 --- a/src/mss/linux.py +++ b/src/mss/linux.py @@ -28,7 +28,7 @@ ) from ctypes.util import find_library from threading import current_thread, local -from typing import TYPE_CHECKING, Any, Tuple +from typing import TYPE_CHECKING, Any from mss.base import MSSBase, lock from mss.exception import ScreenShotError @@ -215,7 +215,7 @@ def _error_handler(display: Display, event: XErrorEvent) -> int: return 0 -def _validate(retval: int, func: Any, args: Tuple[Any, Any], /) -> Tuple[Any, Any]: +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: diff --git a/src/mss/screenshot.py b/src/mss/screenshot.py index c795361e..67ad6a43 100644 --- a/src/mss/screenshot.py +++ b/src/mss/screenshot.py @@ -4,7 +4,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any, Dict +from typing import TYPE_CHECKING, Any from mss.exception import ScreenShotError from mss.models import Monitor, Pixel, Pixels, Pos, Size @@ -42,7 +42,7 @@ def __repr__(self) -> str: return f"<{type(self).__name__} pos={self.left},{self.top} size={self.width}x{self.height}>" @property - def __array_interface__(self) -> Dict[str, Any]: + def __array_interface__(self) -> dict[str, Any]: """Numpy array interface support. It uses raw data in BGRA form. diff --git a/src/tests/test_gnu_linux.py b/src/tests/test_gnu_linux.py index b7d7829a..87d53eea 100644 --- a/src/tests/test_gnu_linux.py +++ b/src/tests/test_gnu_linux.py @@ -99,10 +99,12 @@ def test_xrandr_extension_exists_but_is_not_enabled(display: str) -> None: 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]) + with ( + pyvirtualdisplay.Display(size=(WIDTH, HEIGHT), color_depth=8) as vdisplay, + pytest.raises(ScreenShotError), + mss.mss(display=vdisplay.new_display_var) as sct, + ): + sct.grab(sct.monitors[1]) def test_region_out_of_monitor_bounds(display: str) -> None: @@ -176,6 +178,8 @@ def test_with_cursor_but_not_xfixes_extension_found(display: str) -> None: 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]) + with ( + patch.object(sct.xfixes, "XFixesGetCursorImage", return_value=None), + pytest.raises(ScreenShotError), + ): + sct.grab(sct.monitors[1]) diff --git a/src/tests/test_implementation.py b/src/tests/test_implementation.py index 54abb272..0d81311e 100644 --- a/src/tests/test_implementation.py +++ b/src/tests/test_implementation.py @@ -180,11 +180,13 @@ def main(*args: str) -> int: 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"]): - with pytest.raises(SystemExit) as exc: - entry_point() - assert exc.value.code == 0 + with ( + patch("mss.factory.mss", new=Mock(side_effect=RuntimeError("Boom!"))), + patch.object(sys, "argv", ["mss", "--help"]), + pytest.raises(SystemExit) as exc, + ): + entry_point() + assert exc.value.code == 0 captured = capsys.readouterr() assert not captured.err diff --git a/src/tests/test_windows.py b/src/tests/test_windows.py index f5d99663..7a8e0714 100644 --- a/src/tests/test_windows.py +++ b/src/tests/test_windows.py @@ -5,7 +5,6 @@ from __future__ import annotations import threading -from typing import Tuple import pytest @@ -92,7 +91,7 @@ def test_thread_safety() -> None: thread2.join() -def run_child_thread_bbox(loops: int, bbox: Tuple[int, int, int, int]) -> None: +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) From 01dcb4dd9eb8738b36b18f3b5ccd3e9f39ad1086 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Sun, 1 Sep 2024 13:37:27 +0200 Subject: [PATCH 172/280] docs: tweak --- CHANGELOG.md | 152 +++++++++++++++++++++++++-------------------------- 1 file changed, 76 insertions(+), 76 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dd09d146..4aaa4663 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,11 +2,11 @@ See Git checking messages for full history. -## 10.0.0 (2024/xx/xx) +## 10.0.0 (2024-xx-xx) - removed support for Python 3.8 - :heart: contributors: @ -## 9.0.2 (2024/09/01) +## 9.0.2 (2024-09-01) - added support for Python 3.13 - leveled up the packaging using `hatchling` - used `ruff` to lint the code base (#275) @@ -15,10 +15,10 @@ See Git checking messages for full history. - CI: automated release publishing on tag creation - :heart: contributors: @Andon-Li -## 9.0.1 (2023/04/20) +## 9.0.1 (2023-04-20) - CLI: fixed entry point not taking into account arguments -## 9.0.0 (2023/04/18) +## 9.0.0 (2023-04-18) - Linux: add failure handling to `XOpenDisplay()` call (fixes #246) - Mac: tiny improvement in monitors finding - Windows: refactored how internal handles are stored (fixes #198) @@ -29,27 +29,27 @@ See Git checking messages for full history. - tests: automatic rerun in case of failure (related to #251) - :heart: contributors: @mgorny, @CTPaHHuK-HEbA -## 8.0.3 (2023/04/15) +## 8.0.3 (2023-04-15) - added support for Python 3.12 - MSS: added PEP 561 compatibility - MSS: include more files in the sdist package (#240) - Linux: restore the original X error handler in `.close()` (#241) - Linux: fixed `XRRCrtcInfo.width`, and `XRRCrtcInfo.height`, C types -- doc: use markdown for the README, and changelogs +- docs: use Markdown for the README, and changelogs - dev: renamed the `master` branch to `main` - dev: review the structure of the repository to fix/improve packaging issues (#243) - :heart: contributors: @mgorny, @relent95 -## 8.0.2 (2023/04/09) +## 8.0.2 (2023-04-09) - fixed `SetuptoolsDeprecationWarning`: Installing 'XXX' as data is deprecated, please list it in packages - CLI: fixed arguments handling -## 8.0.1 (2023/04/09) +## 8.0.1 (2023-04-09) - MSS: ensure `--with-cursor`, and `with_cursor` argument & attribute, are simple NOOP on platforms not supporting the feature -- CLI: do not raise a ScreenShotError when `-q`, or `--quiet`, is used but return ` +- CLI: do not raise a `ScreenShotError` when `-q`, or `--quiet`, is used but return ` - tests: fixed `test_entry_point()` with multiple monitors having the same resolution -## 8.0.0 (2023/04/09) +## 8.0.0 (2023-04-09) - removed support for Python 3.6 - removed support for Python 3.7 - MSS: fixed PEP 484 prohibits implicit Optional @@ -59,38 +59,38 @@ See Git checking messages for full history. - Linux: removed side effects when leaving the context manager, resources are all freed (fixes #210) - Linux: added mouse support (related to #55) - CLI: added `--with-cursor` argument -- tests: added PyPy 3.9, removed tox, and improved GNU/Linux coverage +- tests: added PyPy 3.9, removed `tox`, and improved GNU/Linux coverage - :heart: contributors: @zorvios -## 7.0.1 (2022/10/27) +## 7.0.1 (2022-10-27) - fixed the wheel package -## 7.0.0 (2022/10/27) +## 7.0.0 (2022-10-27) - added support for Python 3.11 - added support for Python 3.10 - removed support for Python 3.5 - MSS: modernized the code base (types, `f-string`, ran `isort` & `black`) (closes #101) - MSS: fixed several Sourcery issues - MSS: fixed typos here, and there -- doc: fixed an error when building the documentation +- docs: fixed an error when building the documentation -## 6.1.0 (2020/10/31) -- MSS: reworked how C functions are initialised +## 6.1.0 (2020-10-31) +- MSS: reworked how C functions are initialized - Mac: reduce the number of function calls - Mac: support macOS Big Sur (fixes #178) - tests: expand Python versions to 3.9 and 3.10 -- tests: fixed macOS intepreter not found on Travis-CI +- tests: fixed macOS interpreter not found on Travis-CI - tests: fixed `test_entry_point()` when there are several monitors -## 6.0.0 (2020/06/30) +## 6.0.0 (2020-06-30) - removed usage of deprecated `license_file` option for `license_files` - fixed flake8 usage in pre-commit -- the module is now available on conda (closes #170) +- the module is now available on Conda (closes #170) - MSS: the implementation is now thread-safe on all OSes (fixes #169) - Linux: better handling of the Xrandr extension (fixes #168) - tests: fixed a random bug on `test_grab_with_tuple_percents()` (fixes #142) -## 5.1.0 (2020/04/30) +## 5.1.0 (2020-04-30) - produce wheels for Python 3 only - MSS: renamed again `MSSMixin` to `MSSBase`, now derived from `abc.ABCMeta` - tools: force write of file when saving a PNG file @@ -98,109 +98,109 @@ See Git checking messages for full history. - Windows: fixed multi-thread safety (fixes #150) - :heart: contributors: @narumishi -## 5.0.0 (2019/12/31) +## 5.0.0 (2019-12-31) - removed support for Python 2.7 - MSS: improve type annotations and add CI check - MSS: use `__slots__` for better performances - MSS: better handle resources to prevent leaks - MSS: improve monitors finding - Windows: use our own instances of `GDI32` and `User32` DLLs -- doc: add `project_urls` to `setup.cfg` -- doc: add an example using the multiprocessing module (closes #82) +- docs: add `project_urls` to `setup.cfg` +- docs: add an example using the multiprocessing module (closes #82) - tests: added regression tests for #128 and #135 - tests: move tests files into the package - :heart: contributors: @hugovk, @foone, @SergeyKalutsky -## 4.0.2 (2019/02/23) +## 4.0.2 (2019-02-23) - Windows: ignore missing `SetProcessDPIAware()` on Window XP (fixes #109) - :heart: contributors: @foone -## 4.0.1 (2019/01/26) +## 4.0.1 (2019-01-26) - Linux: fixed several Xlib functions signature (fixes #92) - Linux: improve monitors finding by a factor of 44 -## 4.0.0 (2019/01/11) +## 4.0.0 (2019-01-11) - MSS: remove use of `setup.py` for `setup.cfg` - MSS: renamed `MSSBase` to `MSSMixin` in `base.py` - MSS: refactor ctypes `argtype`, `restype` and `errcheck` setup (fixes #84) - Linux: ensure resources are freed in `grab()` - Windows: avoid unnecessary class attributes - MSS: ensure calls without context manager will not leak resources or document them (fixes #72 and #85) -- MSS: fixed Flake8 C408: Unnecessary dict call- rewrite as a literal, in `exceptions.py` +- MSS: fixed Flake8 C408: Unnecessary dict call - rewrite as a literal, in `exceptions.py` - MSS: fixed Flake8 I100: Import statements are in the wrong order - MSS: fixed Flake8 I201: Missing newline before sections or imports - MSS: fixed PyLint bad-super-call: Bad first argument 'Exception' given to `super()` -- tests: use tox, enable PyPy and PyPy3, add macOS and Windows CI +- tests: use `tox`, enable PyPy and PyPy3, add macOS and Windows CI -## 3.3.2 (2018/11/20) +## 3.3.2 (2018-11-20) - MSS: do monitor detection in MSS constructor (fixes #79) - MSS: specify compliant Python versions for pip install - tests: enable Python 3.7 - tests: fixed `test_entry_point()` with multiple monitors - :heart: contributors: @hugovk, @andreasbuhr -## 3.3.1 (2018/09/22) +## 3.3.1 (2018-09-22) - Linux: fixed a memory leak introduced with 7e8ae5703f0669f40532c2be917df4328bc3985e (fixes #72) -- doc: add the download statistics badge +- docs: add the download statistics badge -## 3.3.0 (2018/09/04) +## 3.3.0 (2018-09-04) - Linux: add an error handler for the XServer to prevent interpreter crash (fixes #61) - MSS: fixed a `ResourceWarning`: unclosed file in `setup.py` - tests: fixed a `ResourceWarning`: unclosed file -- doc: fixed a typo in `Screenshot.pixel()` method (thanks to @mchlnix) +- docs: fixed a typo in `Screenshot.pixel()` method (thanks to @mchlnix) - big code clean-up using `black` -## 3.2.1 (2018/05/21) +## 3.2.1 (2018-05-21) - Windows: enable Hi-DPI awareness - :heart: contributors: @ryanfox -## 3.2.0 (2018/03/22) +## 3.2.0 (2018-03-22) - removed support for Python 3.4 - MSS: add the `Screenshot.bgra` attribute - MSS: speed-up grabbing on the 3 platforms - tools: add PNG compression level control to `to_png()` - tests: add `leaks.py` and `benchmarks.py` for manual testing -- doc: add an example about capturing part of the monitor 2 -- doc: add an example about computing BGRA values to RGB +- docs: add an example about capturing part of the monitor 2 +- docs: add an example about computing BGRA values to RGB -## 3.1.2 (2018/01/05) +## 3.1.2 (2018-01-05) - removed support for Python 3.3 - MSS: possibility to get the whole PNG raw bytes -- Windows: capture all visible windows -- doc: improvements and fixes (fixes #37) +- Windows: capture all visible window +- docs: improvements and fixes (fixes #37) - CI: build the documentation -## 3.1.1 (2017/11/27) +## 3.1.1 (2017-11-27) - MSS: add the `mss` entry point -## 3.1.0 (2017/11/16) +## 3.1.0 (2017-11-16) - MSS: add more way of customization to the output argument of `save()` -- MSS: possibility to use custom class to handle screen shot data +- MSS: possibility to use custom class to handle screenshot data - Mac: properly support all display scaling and resolutions (fixes #14, #19, #21, #23) - Mac: fixed memory leaks (fixes #24) - Linux: handle bad display value - Windows: take into account zoom factor for high-DPI displays (fixes #20) -- doc: several fixes (fixes #22) +- docs: several fixes (fixes #22) - tests: a lot of tests added for better coverage - add the 'Say Thanks' button - :heart: contributors: @karanlyons -## 3.0.1 (2017/07/06) +## 3.0.1 (2017-07-06) - fixed examples links -## 3.0.0 (2017/07/06) +## 3.0.0 (2017-07-06) - big refactor, introducing the `ScreenShot` class - MSS: add Numpy array interface support to the `Screenshot` class -- doc: add OpenCV/Numpy, PIL pixels, FPS +- docs: add OpenCV/Numpy, PIL pixels, FPS -## 2.0.22 2017/04/29 +## 2.0.22 (2017-04-29) - MSS: better use of exception mechanism -- Linux: use of hasattr to prevent Exception on early exit +- Linux: use of `hasattr()` to prevent Exception on early exit - Mac: take into account extra black pixels added when screen with is not divisible by 16 (fixes #14) -- doc: add an example to capture only a part of the screen +- docs: add an example to capture only a part of the screen - :heart: contributors: David Becker, @redodo -## 2.0.18 2016/12/03 +## 2.0.18 (2016-12-03) - change license to MIT - MSS: add type hints - MSS: remove unused code (reported by `Vulture`) @@ -209,11 +209,11 @@ See Git checking messages for full history. - Linux: skip unused monitors - Linux: use `errcheck` instead of deprecated `restype` with callable (fixes #11) - Linux: fixed security issue (reported by Bandit) -- doc: add documentation (fixes #10) +- docs: add documentation (fixes #10) - tests: add tests and use Travis CI (fixes #9) - :heart: contributors: @cycomanic -## 2.0.0 (2016/06/04) +## 2.0.0 (2016-06-04) - add issue and pull request templates - split the module into several files - MSS: a lot of code refactor and optimizations @@ -223,17 +223,17 @@ See Git checking messages for full history. - Linux: prevent segfault when `DISPLAY` is set but no X server started - Linux: prevent segfault when Xrandr is not loaded - Linux: `get_pixels()` insanely fast, use of MSS library (C code) -- Windows: screen shot not correct on Windows 8 (fixes #6) +- Windows: screenshot not correct on Windows 8 (fixes #6) -## 1.0.2 (2016/04/22) -- MSS: fixed non existent alias +## 1.0.2 (2016-04-22) +- MSS: fixed non-existent alias -## 1.0.1 (2016/04/22) +## 1.0.1 (2016-04-22) - MSS: `libpng` warning (ignoring bad filter type) (fixes #7) -## 1.0.0 (2015/04/16) +## 1.0.0 (2015-04-16) - Python 2.6 to 3.5 ready -- MSS: code purgation and review, no more debug information +- MSS: code clean-up and review, no more debug information - MSS: add a shortcut to take automatically use the proper `MSS` class (fixes #5) - MSS: few optimizations into `save_img()` - Darwin: remove rotation from information returned by `enum_display_monitors()` @@ -243,30 +243,30 @@ See Git checking messages for full history. - Windows: huge optimization of `get_pixels()` - CLI: delete `--debug` argument -## 0.1.1 (2015/04/10) +## 0.1.1 (2015-04-10) - MSS: little code review - Linux: fixed monitor count - tests: remove `test-linux` binary -- doc: add `doc/TESTING` -- doc: remove Bonus section from README +- docs: add `doc/TESTING` +- docs: remove Bonus section from README -## 0.1.0 (2015/04/10) +## 0.1.0 (2015-04-10) - MSS: fixed code with `YAPF` tool - Linux: fully functional using Xrandr library -- Linux: code purgation (no more XML files to parse) -- doc: better tests and examples +- Linux: code clean-up (no more XML files to parse) +- docs: better tests and examples -## 0.0.8 (2015/02/04) -- MSS: filename's dir is not used when saving (fixes #3) -- MSS: fixed flake8 error: E713 test for membership should be 'not in' +## 0.0.8 (2015-02-04) +- MSS: filename's directory is not used when saving (fixes #3) +- MSS: fixed flake8 error: E713 test for membership should be 'not in' - MSS: raise an exception for unimplemented methods - Windows: robustness to `MSSWindows.get_pixels` (fixes #4) - :heart: contributors: @sergey-vin, @thehesiod -## 0.0.7 (2014/03/20) +## 0.0.7 (2014-03-20) - MSS: fixed path where screenshots are saved -## 0.0.6 (2014/03/19) +## 0.0.6 (2014-03-19) - Python 3.4 ready - PEP8 compliant - MSS: review module structure to fit the "Code Like a Pythonista: Idiomatic Python" @@ -278,23 +278,23 @@ See Git checking messages for full history. - CLI: possibility to append `--debug` to the command line - :heart: contributors: @sametmax -## 0.0.5 (2013/11/01) +## 0.0.5 (2013-11-01) - MSS: code simplified - Windows: few optimizations into `_arrange()` -## 0.0.4 (2013/10/31) -- Linux: use of memoization => huge time/operations gains +## 0.0.4 (2013-10-31) +- Linux: use of memoization → huge time/operations gains -## 0.0.3 (2013/10/30) +## 0.0.3 (2013-10-30) - MSS: removed PNG filters - MSS: removed `ext` argument, using only PNG - MSS: do not overwrite existing image files - MSS: few optimizations into `png()` - Linux: few optimizations into `get_pixels()` -## 0.0.2 (2013/10/21) +## 0.0.2 (2013-10-21) - added support for python 3 on Windows and GNU/Linux - :heart: contributors: Oros, Eownis -## 0.0.1 (2013/07/01) +## 0.0.1 (2013-07-01) - first release From 16694f6e2460e1aaa61368b48968c6bbb0d04820 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Sun, 1 Sep 2024 13:37:44 +0200 Subject: [PATCH 173/280] fix: Python 3.9 test deps --- pyproject.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 282c7a87..631eac99 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -83,7 +83,8 @@ test = [ "pytest-cov==5.0.0", "pytest-rerunfailures==14.0.0", "pyvirtualdisplay==3.0 ; sys_platform == 'linux'", - "sphinx==8.0.2", + "sphinx==8.0.2 ; python_version >= '3.10'", + "sphinx<=8 ; python_version == '3.9'", ] [tool.hatch.version] From 292cab538caa2514cd0b785e1533bbca8f5f1f2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Sun, 1 Sep 2024 13:42:01 +0200 Subject: [PATCH 174/280] core!: remove support for Python 3.9 --- .github/workflows/tests.yml | 6 ++---- CHANGELOG.md | 1 + README.md | 2 +- docs/source/examples/pil_pixels.py | 2 +- docs/source/index.rst | 2 +- docs/source/support.rst | 3 ++- pyproject.toml | 8 +++----- src/mss/models.py | 12 ++++++------ src/mss/screenshot.py | 4 ++-- src/tests/conftest.py | 2 +- src/tests/test_implementation.py | 4 ++-- src/tests/test_leaks.py | 2 +- 12 files changed, 23 insertions(+), 25 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 70475d44..57fe0895 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -55,8 +55,6 @@ jobs: - emoji: 🪟 runs-on: [windows-latest] python: - - name: CPython 3.9 - runs-on: "3.9" - name: CPython 3.10 runs-on: "3.10" - name: CPython 3.11 @@ -65,8 +63,8 @@ jobs: runs-on: "3.12" - name: CPython 3.13 runs-on: "3.13-dev" - - name: PyPy 3.9 - runs-on: "pypy-3.9" + - name: PyPy 3.10 + runs-on: "pypy-3.10" steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 diff --git a/CHANGELOG.md b/CHANGELOG.md index 4aaa4663..0cfeda99 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ See Git checking messages for full history. ## 10.0.0 (2024-xx-xx) - removed support for Python 3.8 +- removed support for Python 3.9 - :heart: contributors: @ ## 9.0.2 (2024-09-01) diff --git a/README.md b/README.md index 0fca3527..79808291 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ with mss() as sct: An ultra fast cross-platform multiple screenshots module in pure python using ctypes. -- **Python 3.9+**, PEP8 compliant, no dependency, thread-safe; +- **Python 3.10+**, PEP8 compliant, no dependency, thread-safe; - very basic, it will grab one screen shot by monitor or a screen shot of all monitors and save it to a PNG file; - but you can use PIL and benefit from all its formats (or add yours directly); - integrate well with Numpy and OpenCV; diff --git a/docs/source/examples/pil_pixels.py b/docs/source/examples/pil_pixels.py index d1264bc6..d231dbbb 100644 --- a/docs/source/examples/pil_pixels.py +++ b/docs/source/examples/pil_pixels.py @@ -16,7 +16,7 @@ img = Image.new("RGB", sct_img.size) # Best solution: create a list(tuple(R, G, B), ...) for putdata() - pixels = zip(sct_img.raw[2::4], sct_img.raw[1::4], sct_img.raw[::4]) + pixels = zip(sct_img.raw[2::4], sct_img.raw[1::4], sct_img.raw[::4], strict=False) img.putdata(list(pixels)) # But you can set individual pixels too (slower) diff --git a/docs/source/index.rst b/docs/source/index.rst index 3a0d03b8..d2d40634 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -12,7 +12,7 @@ Welcome to Python MSS's documentation! An ultra fast cross-platform multiple screenshots module in pure python using ctypes. - - **Python 3.9+**, :pep:`8` compliant, no dependency, thread-safe; + - **Python 3.10+**, :pep:`8` compliant, no dependency, thread-safe; - very basic, it will grab one screen shot by monitor or a screen shot of all monitors and save it to a PNG file; - but you can use PIL and benefit from all its formats (or add yours directly); - integrate well with Numpy and OpenCV; diff --git a/docs/source/support.rst b/docs/source/support.rst index 102dc01d..3462aae9 100644 --- a/docs/source/support.rst +++ b/docs/source/support.rst @@ -5,7 +5,7 @@ Support Feel free to try MSS on a system we had not tested, and let us know by creating an `issue `_. - OS: GNU/Linux, macOS and Windows - - Python: 3.9 and newer + - Python: 3.10 and newer Future @@ -35,3 +35,4 @@ Abandoned - Python 3.6 (2022-10-27) - Python 3.7 (2023-04-09) - Python 3.8 (2024-09-01) +- Python 3.9 (2024-09-01) diff --git a/pyproject.toml b/pyproject.toml index 631eac99..c68e7c77 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "hatchling.build" name = "mss" description = "An ultra fast cross-platform multiple screenshots module in pure python using ctypes." readme = "README.md" -requires-python = ">= 3.9" +requires-python = ">= 3.10" authors = [ { name = "Mickaël Schoentgen", email="contact@tiger-222.fr" }, ] @@ -31,7 +31,6 @@ classifiers = [ "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", @@ -83,8 +82,7 @@ test = [ "pytest-cov==5.0.0", "pytest-rerunfailures==14.0.0", "pyvirtualdisplay==3.0 ; sys_platform == 'linux'", - "sphinx==8.0.2 ; python_version >= '3.10'", - "sphinx<=8 ; python_version == '3.9'", + "sphinx==8.0.2", ] [tool.hatch.version] @@ -149,7 +147,7 @@ exclude = [ ] line-length = 120 indent-width = 4 -target-version = "py38" +target-version = "py310" [tool.ruff.lint] extend-select = ["ALL"] diff --git a/src/mss/models.py b/src/mss/models.py index e756d629..665a41bc 100644 --- a/src/mss/models.py +++ b/src/mss/models.py @@ -2,15 +2,15 @@ Source: https://github.com/BoboTiG/python-mss. """ -from typing import Any, Dict, List, NamedTuple, Tuple +from typing import Any, NamedTuple -Monitor = Dict[str, int] -Monitors = List[Monitor] +Monitor = dict[str, int] +Monitors = list[Monitor] -Pixel = Tuple[int, int, int] -Pixels = List[Tuple[Pixel, ...]] +Pixel = tuple[int, int, int] +Pixels = list[tuple[Pixel, ...]] -CFunctions = Dict[str, Tuple[str, List[Any], Any]] +CFunctions = dict[str, tuple[str, list[Any], Any]] class Pos(NamedTuple): diff --git a/src/mss/screenshot.py b/src/mss/screenshot.py index 67ad6a43..2588d8b6 100644 --- a/src/mss/screenshot.py +++ b/src/mss/screenshot.py @@ -80,8 +80,8 @@ def left(self) -> int: def pixels(self) -> Pixels: """: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)) + rgb_tuples: Iterator[Pixel] = zip(self.raw[2::4], self.raw[1::4], self.raw[::4], strict=False) + self.__pixels = list(zip(*[iter(rgb_tuples)] * self.width, strict=False)) return self.__pixels diff --git a/src/tests/conftest.py b/src/tests/conftest.py index 1234c8d5..0a30eb85 100644 --- a/src/tests/conftest.py +++ b/src/tests/conftest.py @@ -5,9 +5,9 @@ import glob import os import platform +from collections.abc import Generator from hashlib import md5 from pathlib import Path -from typing import Generator from zipfile import ZipFile import pytest diff --git a/src/tests/test_implementation.py b/src/tests/test_implementation.py index 0d81311e..d0b3a5d2 100644 --- a/src/tests/test_implementation.py +++ b/src/tests/test_implementation.py @@ -116,7 +116,7 @@ def main(*args: str, ret: int = 0) -> None: assert os.path.isfile("monitor-1.png") os.remove("monitor-1.png") - for opts in zip(["-m 1", "--monitor=1"], ["-q", "--quiet"]): + for opts in zip(["-m 1", "--monitor=1"], ["-q", "--quiet"], strict=False): main(*opts) captured = capsys.readouterr() assert not captured.out @@ -128,7 +128,7 @@ def main(*args: str, ret: int = 0) -> None: main(opt, fmt) captured = capsys.readouterr() with mss.mss(display=os.getenv("DISPLAY")) as sct: - for mon, (monitor, line) in enumerate(zip(sct.monitors[1:], captured.out.splitlines()), 1): + for mon, (monitor, line) in enumerate(zip(sct.monitors[1:], captured.out.splitlines(), strict=False), 1): filename = fmt.format(mon=mon, **monitor) assert line.endswith(filename) assert os.path.isfile(filename) diff --git a/src/tests/test_leaks.py b/src/tests/test_leaks.py index 094438b3..db5de6a5 100644 --- a/src/tests/test_leaks.py +++ b/src/tests/test_leaks.py @@ -4,7 +4,7 @@ import os import platform -from typing import Callable +from collections.abc import Callable import pytest From 17e40d5da4adcb51b5e2a4bc75c63711eb04e3a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Sun, 1 Sep 2024 13:45:50 +0200 Subject: [PATCH 175/280] chore: tiny coverage improvement --- src/mss/base.py | 4 ++-- src/mss/darwin.py | 2 +- src/mss/linux.py | 2 +- src/mss/screenshot.py | 2 +- src/mss/windows.py | 2 +- src/tests/bench_general.py | 2 +- src/tests/test_implementation.py | 2 +- 7 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/mss/base.py b/src/mss/base.py index 6d04c686..00981a8b 100644 --- a/src/mss/base.py +++ b/src/mss/base.py @@ -13,14 +13,14 @@ from mss.screenshot import ScreenShot from mss.tools import to_png -if TYPE_CHECKING: +if TYPE_CHECKING: # pragma: nocover from collections.abc import Callable, Iterator from mss.models import Monitor, Monitors try: from datetime import UTC -except ImportError: +except ImportError: # pragma: nocover # Python < 3.11 from datetime import timezone diff --git a/src/mss/darwin.py b/src/mss/darwin.py index ce951efb..6d4fa289 100644 --- a/src/mss/darwin.py +++ b/src/mss/darwin.py @@ -15,7 +15,7 @@ from mss.exception import ScreenShotError from mss.screenshot import ScreenShot, Size -if TYPE_CHECKING: +if TYPE_CHECKING: # pragma: nocover from mss.models import CFunctions, Monitor __all__ = ("MSS",) diff --git a/src/mss/linux.py b/src/mss/linux.py index 46ba33e3..5ae285e9 100644 --- a/src/mss/linux.py +++ b/src/mss/linux.py @@ -33,7 +33,7 @@ from mss.base import MSSBase, lock from mss.exception import ScreenShotError -if TYPE_CHECKING: +if TYPE_CHECKING: # pragma: nocover from mss.models import CFunctions, Monitor from mss.screenshot import ScreenShot diff --git a/src/mss/screenshot.py b/src/mss/screenshot.py index 2588d8b6..c1600272 100644 --- a/src/mss/screenshot.py +++ b/src/mss/screenshot.py @@ -9,7 +9,7 @@ from mss.exception import ScreenShotError from mss.models import Monitor, Pixel, Pixels, Pos, Size -if TYPE_CHECKING: +if TYPE_CHECKING: # pragma: nocover from collections.abc import Iterator diff --git a/src/mss/windows.py b/src/mss/windows.py index 2e102745..0f41cd62 100644 --- a/src/mss/windows.py +++ b/src/mss/windows.py @@ -29,7 +29,7 @@ from mss.base import MSSBase from mss.exception import ScreenShotError -if TYPE_CHECKING: +if TYPE_CHECKING: # pragma: nocover from mss.models import CFunctions, Monitor from mss.screenshot import ScreenShot diff --git a/src/tests/bench_general.py b/src/tests/bench_general.py index 5de4f1c1..100a4729 100644 --- a/src/tests/bench_general.py +++ b/src/tests/bench_general.py @@ -33,7 +33,7 @@ import mss import mss.tools -if TYPE_CHECKING: +if TYPE_CHECKING: # pragma: nocover from collections.abc import Callable from mss.base import MSSBase diff --git a/src/tests/test_implementation.py b/src/tests/test_implementation.py index d0b3a5d2..42429b17 100644 --- a/src/tests/test_implementation.py +++ b/src/tests/test_implementation.py @@ -21,7 +21,7 @@ from mss.exception import ScreenShotError from mss.screenshot import ScreenShot -if TYPE_CHECKING: +if TYPE_CHECKING: # pragma: nocover from mss.models import Monitor try: From be34137fd3e80e80a06adf7604ca3baaa30742fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Sun, 1 Sep 2024 13:48:17 +0200 Subject: [PATCH 176/280] ci: no need to specifically install `wheel` --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 57fe0895..5605f44a 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -74,7 +74,7 @@ jobs: check-latest: true - name: Install test dependencies run: | - python -m pip install -U pip wheel + python -m pip install -U pip python -m pip install -e '.[dev,test]' - name: Tests (GNU/Linux) if: matrix.os.emoji == '🐧' From c48194ae143567c55294503fdf21a8fa80399155 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Sun, 1 Sep 2024 13:49:07 +0200 Subject: [PATCH 177/280] ci: wording --- .github/workflows/tests.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 5605f44a..036f3854 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -33,7 +33,7 @@ jobs: with: python-version: "3.x" cache: pip - - name: Install test dependencies + - name: Install dependencies run: | python -m pip install -U pip python -m pip install -e '.[test]' @@ -72,7 +72,7 @@ jobs: python-version: ${{ matrix.python.runs-on }} cache: pip check-latest: true - - name: Install test dependencies + - name: Install dependencies run: | python -m pip install -U pip python -m pip install -e '.[dev,test]' From b6bccdff62380110491b035be3b5612867e87328 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Sun, 1 Sep 2024 14:15:23 +0200 Subject: [PATCH 178/280] Overall clean-up Mypy is broken, cf https://github.com/python/mypy/issues/17396 --- .github/workflows/tests.yml | 2 +- .gitignore | 1 - .vscode/settings.json | 23 ++++++++++++++++++ CHANGES.md | 4 ++-- README.md | 14 +++++------ docs/source/api.rst | 30 ++++++++++++------------ docs/source/developers.rst | 2 +- docs/source/examples.rst | 14 +++++------ docs/source/examples/custom_cls_image.py | 2 +- docs/source/examples/fps.py | 6 +---- docs/source/index.rst | 10 ++++---- docs/source/installation.rst | 2 +- docs/source/usage.rst | 4 ++-- pyproject.toml | 6 +++-- src/mss/__main__.py | 2 +- src/mss/base.py | 28 +++++++++++----------- src/mss/screenshot.py | 8 +++---- src/tests/test_cls_image.py | 2 +- src/tests/test_implementation.py | 2 +- 19 files changed, 91 insertions(+), 71 deletions(-) create mode 100644 .vscode/settings.json diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 036f3854..c7f10e79 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -36,7 +36,7 @@ jobs: - name: Install dependencies run: | python -m pip install -U pip - python -m pip install -e '.[test]' + python -m pip install -e '.[docs]' - name: Tests run: | sphinx-build -d docs docs/source docs_out --color -W -bhtml diff --git a/.gitignore b/.gitignore index d117f10a..9605fbde 100644 --- a/.gitignore +++ b/.gitignore @@ -17,7 +17,6 @@ docs_out/ *.egg-info/ .idea/ .pytest_cache/ -.vscode/ docs/output/ .mypy_cache/ __pycache__/ diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..ed065c83 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,23 @@ +{ + "languageToolLinter.languageTool.ignoredWordsInWorkspace": [ + "bgra", + "ctypes", + "eownis", + "memoization", + "noop", + "numpy", + "oros", + "pylint", + "pypy", + "python-mss", + "pythonista", + "sdist", + "sourcery", + "tk", + "tkinter", + "xlib", + "xrandr", + "xserver", + "zlib" + ] +} \ No newline at end of file diff --git a/CHANGES.md b/CHANGES.md index ea3376f3..cda211e9 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -183,7 +183,7 @@ ## 3.0.0 (2017-07-06) ### base.py -- Added the `ScreenShot` class containing data for a given screen shot (support the Numpy array interface [`ScreenShot.__array_interface__`]) +- Added the `ScreenShot` class containing data for a given screenshot (support the Numpy array interface [`ScreenShot.__array_interface__`]) - Added `shot()` method to `MSSBase`. It takes the same arguments as the `save()` method. - Renamed `get_pixels` to `grab`. It now returns a `ScreenShot` object. - Moved `to_png` method to `tools.py`. It is now a simple function. @@ -195,7 +195,7 @@ - Removed `bgra_to_rgb()` method. Use `ScreenShot.rgb` property instead. ### darwin.py -- Removed `_crop_width()` method. Screen shots are now using the width set by the OS (rounded to 16). +- Removed `_crop_width()` method. Screenshots are now using the width set by the OS (rounded to 16). ### exception.py - Renamed `ScreenshotError` class to `ScreenShotError` diff --git a/README.md b/README.md index 79808291..5ede99b2 100644 --- a/README.md +++ b/README.md @@ -8,24 +8,24 @@ ```python from mss import mss -# The simplest use, save a screen shot of the 1st monitor +# The simplest use, save a screenshot of the 1st monitor with mss() as sct: sct.shot() ``` -An ultra fast cross-platform multiple screenshots module in pure python using ctypes. +An ultra-fast cross-platform multiple screenshots module in pure python using ctypes. - **Python 3.10+**, PEP8 compliant, no dependency, thread-safe; -- very basic, it will grab one screen shot by monitor or a screen shot of all monitors and save it to a PNG file; +- very basic, it will grab one screenshot by monitor or a screenshot of all monitors and save it to a PNG file; - but you can use PIL and benefit from all its formats (or add yours directly); - integrate well with Numpy and OpenCV; -- it could be easily embedded into games and other software which require fast and platform optimized methods to grab screen shots (like AI, Computer Vision); +- it could be easily embedded into games and other software which require fast and platform optimized methods to grab screenshots (like AI, Computer Vision); - get the [source code on GitHub](https://github.com/BoboTiG/python-mss); - learn with a [bunch of examples](https://python-mss.readthedocs.io/examples.html); - you can [report a bug](https://github.com/BoboTiG/python-mss/issues); -- need some help? Use the tag *python-mss* on [StackOverflow](https://stackoverflow.com/questions/tagged/python-mss); +- need some help? Use the tag *python-mss* on [Stack Overflow](https://stackoverflow.com/questions/tagged/python-mss); - and there is a [complete, and beautiful, documentation](https://python-mss.readthedocs.io) :) -- **MSS** stands for Multiple Screen Shots; +- **MSS** stands for Multiple ScreenShots; ## Installation @@ -36,7 +36,7 @@ You can install it with pip: python -m pip install -U --user mss ``` -Or you can install it with conda: +Or you can install it with Conda: ```shell conda install -c conda-forge python-mss diff --git a/docs/source/api.rst b/docs/source/api.rst index cef76dab..b1d87f4b 100644 --- a/docs/source/api.rst +++ b/docs/source/api.rst @@ -173,18 +173,18 @@ Methods :param int mon: the monitor's number. :param str output: the output's file name. :type callback: callable or None - :param callback: callback called before saving the screen shot to a file. Takes the *output* argument as parameter. + :param callback: callback called before saving the screenshot to a file. Takes the *output* argument as parameter. :rtype: iterable :return: Created file(s). - Grab a screen shot and save it to a file. + Grab a screenshot and save it to a file. The *output* parameter can take several keywords to customize the filename: - ``{mon}``: the monitor number - - ``{top}``: the screen shot y-coordinate of the upper-left corner - - ``{left}``: the screen shot x-coordinate of the upper-left corner - - ``{width}``: the screen shot's width - - ``{height}``: the screen shot's height + - ``{top}``: the screenshot y-coordinate of the upper-left corner + - ``{left}``: the screenshot x-coordinate of the upper-left corner + - ``{width}``: the screenshot's width + - ``{height}``: the screenshot's height - ``{date}``: the current date using the default formatter As it is using the :py:func:`format()` function, you can specify formatting options like ``{date:%Y-%m-%s}``. @@ -199,14 +199,14 @@ Methods :return str: The created file. - Helper to save the screen shot of the first monitor, by default. + Helper to save the screenshot of the first monitor, by default. You can pass the same arguments as for :meth:`save()`. .. versionadded:: 3.0.0 .. class:: ScreenShot - Screen shot object. + Screenshot object. .. note:: @@ -221,7 +221,7 @@ Methods :param int height: the monitor's height. :rtype: :class:`ScreenShot` - Instantiate a new class given only screen shot's data and size. + Instantiate a new class given only screenshot's data and size. .. method:: pixel(coord_x, coord_y) @@ -300,13 +300,13 @@ Properties .. attribute:: height - The screen shot's height. + The screenshot's height. :rtype: int .. attribute:: left - The screen shot's left coordinate. + The screenshot's left coordinate. :rtype: int @@ -318,7 +318,7 @@ Properties .. attribute:: pos - The screen shot's coordinates. + The screenshot's coordinates. :rtype: :py:func:`collections.namedtuple()` @@ -332,19 +332,19 @@ Properties .. attribute:: size - The screen shot's size. + The screenshot's size. :rtype: :py:func:`collections.namedtuple()` .. attribute:: top - The screen shot's top coordinate. + The screenshot's top coordinate. :rtype: int .. attribute:: width - The screen shot's width. + The screenshot's width. :rtype: int diff --git a/docs/source/developers.rst b/docs/source/developers.rst index d9c3e537..1d29bd72 100644 --- a/docs/source/developers.rst +++ b/docs/source/developers.rst @@ -23,7 +23,7 @@ You will need `pytest `_:: $ python -m venv venv $ . venv/bin/activate $ python -m pip install -U pip - $ python -m pip install -e '.[test]' + $ python -m pip install -e '.[tests]' How to Test? diff --git a/docs/source/examples.rst b/docs/source/examples.rst index 6adb7bc5..aede8c30 100644 --- a/docs/source/examples.rst +++ b/docs/source/examples.rst @@ -5,22 +5,22 @@ Examples Basics ====== -One screen shot per monitor ---------------------------- +One screenshot per monitor +-------------------------- :: for filename in sct.save(): print(filename) -Screen shot of the monitor 1 ----------------------------- +Screenshot of the monitor 1 +--------------------------- :: filename = sct.shot() print(filename) -A screen shot to grab them all ------------------------------- +A screenshot to grab them all +----------------------------- :: filename = sct.shot(mon=-1, output='fullscreen.png') @@ -29,7 +29,7 @@ A screen shot to grab them all Callback -------- -Screen shot of the monitor 1 with a callback: +Screenshot of the monitor 1 with a callback: .. literalinclude:: examples/callback.py :lines: 8- diff --git a/docs/source/examples/custom_cls_image.py b/docs/source/examples/custom_cls_image.py index f3575794..2a1f8102 100644 --- a/docs/source/examples/custom_cls_image.py +++ b/docs/source/examples/custom_cls_image.py @@ -12,7 +12,7 @@ class SimpleScreenShot(ScreenShot): - """Define your own custom method to deal with screen shot raw data. + """Define your own custom method to deal with screenshot 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 4e4080ec..f9e76134 100644 --- a/docs/source/examples/fps.py +++ b/docs/source/examples/fps.py @@ -9,16 +9,12 @@ import cv2 import numpy as np +from PIL import ImageGrab import mss def screen_record() -> int: - try: - from PIL import ImageGrab - except ImportError: - return 0 - # 800x600 windowed mode mon = (0, 40, 800, 640) diff --git a/docs/source/index.rst b/docs/source/index.rst index d2d40634..5dae783e 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -5,7 +5,7 @@ Welcome to Python MSS's documentation! from mss import mss - # The simplest use, save a screen shot of the 1st monitor + # The simplest use, save a screenshot of the 1st monitor with mss() as sct: sct.shot() @@ -13,15 +13,15 @@ Welcome to Python MSS's documentation! An ultra fast cross-platform multiple screenshots module in pure python using ctypes. - **Python 3.10+**, :pep:`8` compliant, no dependency, thread-safe; - - very basic, it will grab one screen shot by monitor or a screen shot of all monitors and save it to a PNG file; + - very basic, it will grab one screenshot by monitor or a screenshot of all monitors and save it to a PNG file; - but you can use PIL and benefit from all its formats (or add yours directly); - integrate well with Numpy and OpenCV; - - it could be easily embedded into games and other software which require fast and platform optimized methods to grab screen shots (like AI, Computer Vision); + - it could be easily embedded into games and other software which require fast and platform optimized methods to grab screenshots (like AI, Computer Vision); - get the `source code on GitHub `_; - learn with a `bunch of examples `_; - you can `report a bug `_; - - need some help? Use the tag *python-mss* on `StackOverflow `_; - - **MSS** stands for Multiple Screen Shots; + - need some help? Use the tag *python-mss* on `Stack Overflow `_; + - **MSS** stands for Multiple ScreenShots; +-------------------------+ | Content | diff --git a/docs/source/installation.rst b/docs/source/installation.rst index 0dae108e..d003f790 100644 --- a/docs/source/installation.rst +++ b/docs/source/installation.rst @@ -14,7 +14,7 @@ Quite simple:: Conda Package ------------- -The module is also available from conda:: +The module is also available from Conda:: $ conda install -c conda-forge python-mss diff --git a/docs/source/usage.rst b/docs/source/usage.rst index 47bf7cbe..903ee38f 100644 --- a/docs/source/usage.rst +++ b/docs/source/usage.rst @@ -11,7 +11,7 @@ So MSS can be used as simply as:: Or import the good one based on your operating system:: - # MacOS X + # macOS from mss.darwin import MSS as mss # GNU/Linux @@ -83,7 +83,7 @@ Or via direct call from Python:: -l {0,1,2,3,4,5,6,7,8,9}, --level {0,1,2,3,4,5,6,7,8,9} the PNG compression level -m MONITOR, --monitor MONITOR - the monitor to screen shot + the monitor to screenshot -o OUTPUT, --output OUTPUT the output file name --with-cursor include the cursor diff --git a/pyproject.toml b/pyproject.toml index c68e7c77..7ff6377c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -75,14 +75,16 @@ dev = [ "twine==5.1.1", "wheel==0.44.0", ] -test = [ +docs = [ + "sphinx==8.0.2", +] +tests = [ "numpy==2.1.0 ; sys_platform == 'windows' and python_version >= '3.13'", "pillow==10.4.0", "pytest==8.3.2", "pytest-cov==5.0.0", "pytest-rerunfailures==14.0.0", "pyvirtualdisplay==3.0 ; sys_platform == 'linux'", - "sphinx==8.0.2", ] [tool.hatch.version] diff --git a/src/mss/__main__.py b/src/mss/__main__.py index 1cac8fca..384ad344 100644 --- a/src/mss/__main__.py +++ b/src/mss/__main__.py @@ -30,7 +30,7 @@ def main(*args: str) -> int: choices=list(range(10)), help="the PNG compression level", ) - cli_args.add_argument("-m", "--monitor", default=0, type=int, help="the monitor to screen shot") + cli_args.add_argument("-m", "--monitor", default=0, type=int, help="the monitor to screenshot") cli_args.add_argument("-o", "--output", default="monitor-{mon}.png", help="the output file name") cli_args.add_argument("--with-cursor", default=False, action="store_true", help="include the cursor") cli_args.add_argument( diff --git a/src/mss/base.py b/src/mss/base.py index 00981a8b..9e926079 100644 --- a/src/mss/base.py +++ b/src/mss/base.py @@ -132,28 +132,28 @@ def save( output: str = "monitor-{mon}.png", callback: Callable[[str], None] | None = None, ) -> Iterator[str]: - """Grab a screen shot and save it to a file. + """Grab a screenshot and save it to a file. - :param int mon: The monitor to screen shot (default=0). - -1: grab one screen shot of all monitors - 0: grab one screen shot by monitor - N: grab the screen shot of the monitor N + :param int mon: The monitor to screenshot (default=0). + -1: grab one screenshot of all monitors + 0: grab one screenshot by monitor + N: grab the screenshot of the monitor N :param str output: The output filename. It can take several keywords to customize the filename: - `{mon}`: the monitor number - - `{top}`: the screen shot y-coordinate of the upper-left corner - - `{left}`: the screen shot x-coordinate of the upper-left corner - - `{width}`: the screen shot's width - - `{height}`: the screen shot's height + - `{top}`: the screenshot y-coordinate of the upper-left corner + - `{left}`: the screenshot x-coordinate of the upper-left corner + - `{width}`: the screenshot's width + - `{height}`: the screenshot's height - `{date}`: the current date using the default formatter As it is using the `format()` function, you can specify formatting options like `{date:%Y-%m-%s}`. :param callable callback: Callback called before saving the - screen shot to a file. Take the `output` argument as parameter. + screenshot to a file. Take the `output` argument as parameter. :return generator: Created file(s). """ @@ -163,7 +163,7 @@ def save( raise ScreenShotError(msg) if mon == 0: - # One screen shot by monitor + # One screenshot by monitor for idx, monitor in enumerate(monitors[1:], 1): fname = output.format(mon=idx, date=datetime.now(UTC) if "{date" in output else None, **monitor) if callable(callback): @@ -172,8 +172,8 @@ def save( to_png(sct.rgb, sct.size, level=self.compression_level, output=fname) yield fname else: - # A screen shot of all monitors together or - # a screen shot of the monitor N. + # A screenshot of all monitors together or + # a screenshot of the monitor N. mon = 0 if mon == -1 else mon try: monitor = monitors[mon] @@ -189,7 +189,7 @@ 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 screenshot of the 1st monitor, by default. You can pass the same arguments as for ``save``. """ kwargs["mon"] = kwargs.get("mon", 1) diff --git a/src/mss/screenshot.py b/src/mss/screenshot.py index c1600272..e51a37f2 100644 --- a/src/mss/screenshot.py +++ b/src/mss/screenshot.py @@ -14,7 +14,7 @@ class ScreenShot: - """Screen shot object. + """Screenshot object. .. note:: @@ -32,10 +32,10 @@ def __init__(self, data: bytearray, monitor: Monitor, /, *, size: Size | None = #: OS independent implementations. self.raw = data - #: NamedTuple of the screen shot coordinates. + #: NamedTuple of the screenshot coordinates. self.pos = Pos(monitor["left"], monitor["top"]) - #: NamedTuple of the screen shot size. + #: NamedTuple of the screenshot size. self.size = Size(monitor["width"], monitor["height"]) if size is None else size def __repr__(self) -> str: @@ -57,7 +57,7 @@ def __array_interface__(self) -> dict[str, Any]: @classmethod 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.""" + """Instantiate a new class given only screenshot's data and size.""" monitor = {"left": 0, "top": 0, "width": width, "height": height} return cls(data, monitor) diff --git a/src/tests/test_cls_image.py b/src/tests/test_cls_image.py index 10c9cfed..84ed9268 100644 --- a/src/tests/test_cls_image.py +++ b/src/tests/test_cls_image.py @@ -17,7 +17,7 @@ def __init__(self, data: bytearray, monitor: Monitor, **_: Any) -> None: def test_custom_cls_image() -> None: with mss(display=os.getenv("DISPLAY")) as sct: - sct.cls_image = SimpleScreenShot # type: ignore[assignment] + sct.cls_image = SimpleScreenShot mon1 = sct.monitors[1] image = sct.grab(mon1) assert isinstance(image, SimpleScreenShot) diff --git a/src/tests/test_implementation.py b/src/tests/test_implementation.py index 42429b17..97b6e87b 100644 --- a/src/tests/test_implementation.py +++ b/src/tests/test_implementation.py @@ -40,7 +40,7 @@ class MSS0(MSSBase): class MSS1(MSSBase): """Only `grab()` implemented.""" - def grab(self, monitor: Monitor) -> None: # type: ignore[override] + def grab(self, monitor: Monitor) -> None: pass From d696aa49a8e7533fb7277a37606e5cd439769304 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Sun, 1 Sep 2024 14:24:07 +0200 Subject: [PATCH 179/280] ci: fixes --- .github/workflows/tests.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index c7f10e79..ee770f71 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -21,7 +21,7 @@ jobs: run: | python -m pip install -U pip python -m pip install -e '.[dev]' - - name: Tests + - name: Check run: ./check.sh documentation: @@ -37,7 +37,7 @@ jobs: run: | python -m pip install -U pip python -m pip install -e '.[docs]' - - name: Tests + - name: Build run: | sphinx-build -d docs docs/source docs_out --color -W -bhtml @@ -75,7 +75,7 @@ jobs: - name: Install dependencies run: | python -m pip install -U pip - python -m pip install -e '.[dev,test]' + python -m pip install -e '.[dev,tests]' - name: Tests (GNU/Linux) if: matrix.os.emoji == '🐧' run: xvfb-run python -m pytest From a1466d540373c22770aa607209c5b9ca1b61177e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Sun, 1 Sep 2024 14:25:50 +0200 Subject: [PATCH 180/280] tests: restore Mypy --- src/tests/test_cls_image.py | 2 +- src/tests/test_implementation.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/tests/test_cls_image.py b/src/tests/test_cls_image.py index 84ed9268..10c9cfed 100644 --- a/src/tests/test_cls_image.py +++ b/src/tests/test_cls_image.py @@ -17,7 +17,7 @@ def __init__(self, data: bytearray, monitor: Monitor, **_: Any) -> None: 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_implementation.py b/src/tests/test_implementation.py index 97b6e87b..42429b17 100644 --- a/src/tests/test_implementation.py +++ b/src/tests/test_implementation.py @@ -40,7 +40,7 @@ class MSS0(MSSBase): class MSS1(MSSBase): """Only `grab()` implemented.""" - def grab(self, monitor: Monitor) -> None: + def grab(self, monitor: Monitor) -> None: # type: ignore[override] pass From 38eb63e043542d8ee819634421c0f0b56f04250d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Mon, 2 Sep 2024 07:11:56 +0200 Subject: [PATCH 181/280] fix: test_leaks.py on PyPy --- src/tests/test_leaks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tests/test_leaks.py b/src/tests/test_leaks.py index db5de6a5..f27e7f77 100644 --- a/src/tests/test_leaks.py +++ b/src/tests/test_leaks.py @@ -124,4 +124,4 @@ def test_resource_leaks(func: Callable[[], None], monitor_func: Callable[[], int new_resources = monitor_func() allocated_resources = max(allocated_resources, new_resources) - assert original_resources == allocated_resources + assert allocated_resources <= original_resources From b5f78b60f84d410e3d51b989d74d893888ac403b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 4 Sep 2024 16:44:58 +0200 Subject: [PATCH 182/280] build(deps): bump numpy from 2.1.0 to 2.1.1 (#278) Bumps [numpy](https://github.com/numpy/numpy) from 2.1.0 to 2.1.1. - [Release notes](https://github.com/numpy/numpy/releases) - [Changelog](https://github.com/numpy/numpy/blob/main/doc/RELEASE_WALKTHROUGH.rst) - [Commits](https://github.com/numpy/numpy/compare/v2.1.0...v2.1.1) --- updated-dependencies: - dependency-name: numpy dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 7ff6377c..28db256a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -79,7 +79,7 @@ docs = [ "sphinx==8.0.2", ] tests = [ - "numpy==2.1.0 ; sys_platform == 'windows' and python_version >= '3.13'", + "numpy==2.1.1 ; sys_platform == 'windows' and python_version >= '3.13'", "pillow==10.4.0", "pytest==8.3.2", "pytest-cov==5.0.0", From e71252f23bba70b4c4a75ae0568ed5e77cae0e4a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 6 Sep 2024 14:44:40 +0200 Subject: [PATCH 183/280] build(deps): bump ruff from 0.6.3 to 0.6.4 (#279) Bumps [ruff](https://github.com/astral-sh/ruff) from 0.6.3 to 0.6.4. - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/0.6.3...0.6.4) --- updated-dependencies: - dependency-name: ruff dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 28db256a..3a8f7c27 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -71,7 +71,7 @@ mss = "mss.__main__:main" dev = [ "build==1.2.1", "mypy==1.11.2", - "ruff==0.6.3", + "ruff==0.6.4", "twine==5.1.1", "wheel==0.44.0", ] From 1e25e600d07184d08db8fbf6a488da0016bfce37 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 Sep 2024 17:07:49 +0200 Subject: [PATCH 184/280] build(deps): bump build from 1.2.1 to 1.2.2 (#280) Bumps [build](https://github.com/pypa/build) from 1.2.1 to 1.2.2. - [Release notes](https://github.com/pypa/build/releases) - [Changelog](https://github.com/pypa/build/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pypa/build/compare/1.2.1...1.2.2) --- updated-dependencies: - dependency-name: build dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 3a8f7c27..4d58bbd6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -69,7 +69,7 @@ mss = "mss.__main__:main" [project.optional-dependencies] dev = [ - "build==1.2.1", + "build==1.2.2", "mypy==1.11.2", "ruff==0.6.4", "twine==5.1.1", From 6b39f264783b0b2215dde2c185f71896b69d9783 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 10 Sep 2024 16:36:23 +0200 Subject: [PATCH 185/280] build(deps): bump pytest from 8.3.2 to 8.3.3 (#281) Bumps [pytest](https://github.com/pytest-dev/pytest) from 8.3.2 to 8.3.3. - [Release notes](https://github.com/pytest-dev/pytest/releases) - [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest/compare/8.3.2...8.3.3) --- updated-dependencies: - dependency-name: pytest dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 4d58bbd6..64e73b10 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -81,7 +81,7 @@ docs = [ tests = [ "numpy==2.1.1 ; sys_platform == 'windows' and python_version >= '3.13'", "pillow==10.4.0", - "pytest==8.3.2", + "pytest==8.3.3", "pytest-cov==5.0.0", "pytest-rerunfailures==14.0.0", "pyvirtualdisplay==3.0 ; sys_platform == 'linux'", From 4c53a5dcd40909a995a674141cbe97ca5fc0519e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 16 Sep 2024 14:25:13 +0200 Subject: [PATCH 186/280] build(deps): bump ruff from 0.6.4 to 0.6.5 (#282) Bumps [ruff](https://github.com/astral-sh/ruff) from 0.6.4 to 0.6.5. - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/0.6.4...0.6.5) --- updated-dependencies: - dependency-name: ruff dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 64e73b10..5ff278fb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -71,7 +71,7 @@ mss = "mss.__main__:main" dev = [ "build==1.2.2", "mypy==1.11.2", - "ruff==0.6.4", + "ruff==0.6.5", "twine==5.1.1", "wheel==0.44.0", ] From f846c8ef20a61ff69ba2d536a6a695ce9cdb6763 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 20 Sep 2024 17:51:12 +0200 Subject: [PATCH 187/280] build(deps): bump ruff from 0.6.5 to 0.6.6 (#283) Bumps [ruff](https://github.com/astral-sh/ruff) from 0.6.5 to 0.6.6. - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/0.6.5...0.6.6) --- updated-dependencies: - dependency-name: ruff dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 5ff278fb..82bcc5eb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -71,7 +71,7 @@ mss = "mss.__main__:main" dev = [ "build==1.2.2", "mypy==1.11.2", - "ruff==0.6.5", + "ruff==0.6.6", "twine==5.1.1", "wheel==0.44.0", ] From 78bd35734138c21ccd4d418832230544b65fb55a Mon Sep 17 00:00:00 2001 From: Kian-Meng Ang Date: Sat, 21 Sep 2024 00:09:27 +0800 Subject: [PATCH 188/280] docs: fix typos (#284) Found via `codespell -H` --- docs/source/examples/fps_multiprocessing.py | 2 +- docs/source/where.rst | 2 +- src/mss/linux.py | 8 ++++---- src/tests/test_windows.py | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/source/examples/fps_multiprocessing.py b/docs/source/examples/fps_multiprocessing.py index c83455f4..c4a2a38a 100644 --- a/docs/source/examples/fps_multiprocessing.py +++ b/docs/source/examples/fps_multiprocessing.py @@ -40,6 +40,6 @@ def save(queue: Queue) -> None: # The screenshots queue queue: Queue = Queue() - # 2 processes: one for grabing and one for saving PNG files + # 2 processes: one for grabbing and one for saving PNG files Process(target=grab, args=(queue,)).start() Process(target=save, args=(queue,)).start() diff --git a/docs/source/where.rst b/docs/source/where.rst index 95acc6f1..18c87e6c 100644 --- a/docs/source/where.rst +++ b/docs/source/where.rst @@ -3,7 +3,7 @@ Who Uses it? ============ This is a non exhaustive list where MSS is integrated or has inspired. -Do not hesistate to `say Hello! `_ if you are using MSS too. +Do not hesitate to `say Hello! `_ if you are using MSS too. - `Airtest `_, a cross-platform UI automation framework for aames and apps; - `Automation Framework `_, a Batmans utility; diff --git a/src/mss/linux.py b/src/mss/linux.py index 5ae285e9..6dac52b8 100644 --- a/src/mss/linux.py +++ b/src/mss/linux.py @@ -103,11 +103,11 @@ class XImage(Structure): ("bitmap_bit_order", c_int), # LSBFirst, MSBFirst ("bitmap_pad", c_int), # 8, 16, 32 either XY or ZPixmap ("depth", c_int), # depth of image - ("bytes_per_line", c_int), # accelarator to next line + ("bytes_per_line", c_int), # accelerator to next line ("bits_per_pixel", c_int), # bits per pixel (ZPixmap) - ("red_mask", c_ulong), # bits in z arrangment - ("green_mask", c_ulong), # bits in z arrangment - ("blue_mask", c_ulong), # bits in z arrangment + ("red_mask", c_ulong), # bits in z arrangement + ("green_mask", c_ulong), # bits in z arrangement + ("blue_mask", c_ulong), # bits in z arrangement ) diff --git a/src/tests/test_windows.py b/src/tests/test_windows.py index 7a8e0714..1e5763b3 100644 --- a/src/tests/test_windows.py +++ b/src/tests/test_windows.py @@ -80,7 +80,7 @@ def run_child_thread(loops: int) -> None: 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. + The following code will throw a ScreenShotError exception if thread-safety is not guaranteed. """ # Let thread 1 finished ahead of thread 2 thread1 = threading.Thread(target=run_child_thread, args=(30,)) @@ -100,7 +100,7 @@ def run_child_thread_bbox(loops: int, bbox: tuple[int, int, int, int]) -> None: 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. + The following code will throw a ScreenShotError exception if thread-safety is not guaranteed. """ thread1 = threading.Thread(target=run_child_thread_bbox, args=(100, (0, 0, 100, 100))) thread2 = threading.Thread(target=run_child_thread_bbox, args=(100, (0, 0, 50, 1))) From cc2058ca590c33e9b0e16ca59655c5344129afb4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Sep 2024 15:49:17 +0200 Subject: [PATCH 189/280] build(deps): bump ruff from 0.6.6 to 0.6.7 (#285) Bumps [ruff](https://github.com/astral-sh/ruff) from 0.6.6 to 0.6.7. - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/0.6.6...0.6.7) --- updated-dependencies: - dependency-name: ruff dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 82bcc5eb..e397e275 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -71,7 +71,7 @@ mss = "mss.__main__:main" dev = [ "build==1.2.2", "mypy==1.11.2", - "ruff==0.6.6", + "ruff==0.6.7", "twine==5.1.1", "wheel==0.44.0", ] From 72c51e9ec792a2bd439b83b204736fddb50b03e2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 26 Sep 2024 16:34:48 +0200 Subject: [PATCH 190/280] build(deps): bump ruff from 0.6.7 to 0.6.8 (#286) Bumps [ruff](https://github.com/astral-sh/ruff) from 0.6.7 to 0.6.8. - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/0.6.7...0.6.8) --- updated-dependencies: - dependency-name: ruff dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index e397e275..d5ad838f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -71,7 +71,7 @@ mss = "mss.__main__:main" dev = [ "build==1.2.2", "mypy==1.11.2", - "ruff==0.6.7", + "ruff==0.6.8", "twine==5.1.1", "wheel==0.44.0", ] From a041855081b0c837e36be5a92b5a68db3836ff79 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 7 Oct 2024 15:45:07 +0200 Subject: [PATCH 191/280] build(deps): bump ruff from 0.6.8 to 0.6.9 (#287) Bumps [ruff](https://github.com/astral-sh/ruff) from 0.6.8 to 0.6.9. - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/0.6.8...0.6.9) --- updated-dependencies: - dependency-name: ruff dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index d5ad838f..b7f90981 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -71,7 +71,7 @@ mss = "mss.__main__:main" dev = [ "build==1.2.2", "mypy==1.11.2", - "ruff==0.6.8", + "ruff==0.6.9", "twine==5.1.1", "wheel==0.44.0", ] From a83afb0e6dc66522de58d5f86df765ce391073e0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 7 Oct 2024 15:45:16 +0200 Subject: [PATCH 192/280] build(deps): bump numpy from 2.1.1 to 2.1.2 (#288) Bumps [numpy](https://github.com/numpy/numpy) from 2.1.1 to 2.1.2. - [Release notes](https://github.com/numpy/numpy/releases) - [Changelog](https://github.com/numpy/numpy/blob/main/doc/RELEASE_WALKTHROUGH.rst) - [Commits](https://github.com/numpy/numpy/compare/v2.1.1...v2.1.2) --- updated-dependencies: - dependency-name: numpy dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index b7f90981..edd6e8b1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -79,7 +79,7 @@ docs = [ "sphinx==8.0.2", ] tests = [ - "numpy==2.1.1 ; sys_platform == 'windows' and python_version >= '3.13'", + "numpy==2.1.2 ; sys_platform == 'windows' and python_version >= '3.13'", "pillow==10.4.0", "pytest==8.3.3", "pytest-cov==5.0.0", From abfc77796ff4629ff7c6cf25a96cf25191b409e2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 7 Oct 2024 15:45:29 +0200 Subject: [PATCH 193/280] build(deps): bump build from 1.2.2 to 1.2.2.post1 (#289) Bumps [build](https://github.com/pypa/build) from 1.2.2 to 1.2.2.post1. - [Release notes](https://github.com/pypa/build/releases) - [Changelog](https://github.com/pypa/build/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pypa/build/compare/1.2.2...1.2.2.post1) --- updated-dependencies: - dependency-name: build dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index edd6e8b1..6fb0e06d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -69,7 +69,7 @@ mss = "mss.__main__:main" [project.optional-dependencies] dev = [ - "build==1.2.2", + "build==1.2.2.post1", "mypy==1.11.2", "ruff==0.6.9", "twine==5.1.1", From 6682717bf230f97b1e0e40e798d6558ef7e88d94 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 11 Oct 2024 19:22:24 +0200 Subject: [PATCH 194/280] build(deps): bump sphinx from 8.0.2 to 8.1.0 (#290) Bumps [sphinx](https://github.com/sphinx-doc/sphinx) from 8.0.2 to 8.1.0. - [Release notes](https://github.com/sphinx-doc/sphinx/releases) - [Changelog](https://github.com/sphinx-doc/sphinx/blob/master/CHANGES.rst) - [Commits](https://github.com/sphinx-doc/sphinx/compare/v8.0.2...v8.1.0) --- updated-dependencies: - dependency-name: sphinx dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 6fb0e06d..ce963251 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -76,7 +76,7 @@ dev = [ "wheel==0.44.0", ] docs = [ - "sphinx==8.0.2", + "sphinx==8.1.0", ] tests = [ "numpy==2.1.2 ; sys_platform == 'windows' and python_version >= '3.13'", From 6af2b25dc38c4a069dc3612594be04e5adfc8da0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Oct 2024 15:28:19 +0200 Subject: [PATCH 195/280] build(deps): bump sphinx from 8.1.0 to 8.1.3 (#292) Bumps [sphinx](https://github.com/sphinx-doc/sphinx) from 8.1.0 to 8.1.3. - [Release notes](https://github.com/sphinx-doc/sphinx/releases) - [Changelog](https://github.com/sphinx-doc/sphinx/blob/master/CHANGES.rst) - [Commits](https://github.com/sphinx-doc/sphinx/compare/v8.1.0...v8.1.3) --- updated-dependencies: - dependency-name: sphinx dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index ce963251..b879754e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -76,7 +76,7 @@ dev = [ "wheel==0.44.0", ] docs = [ - "sphinx==8.1.0", + "sphinx==8.1.3", ] tests = [ "numpy==2.1.2 ; sys_platform == 'windows' and python_version >= '3.13'", From fed610cb32861ce78038e0537d4aebce6d29f014 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Oct 2024 15:28:28 +0200 Subject: [PATCH 196/280] build(deps): bump mypy from 1.11.2 to 1.12.0 (#291) Bumps [mypy](https://github.com/python/mypy) from 1.11.2 to 1.12.0. - [Changelog](https://github.com/python/mypy/blob/master/CHANGELOG.md) - [Commits](https://github.com/python/mypy/compare/v1.11.2...v1.12.0) --- updated-dependencies: - dependency-name: mypy dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index b879754e..b9dd1bc3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -70,7 +70,7 @@ mss = "mss.__main__:main" [project.optional-dependencies] dev = [ "build==1.2.2.post1", - "mypy==1.11.2", + "mypy==1.12.0", "ruff==0.6.9", "twine==5.1.1", "wheel==0.44.0", From 5109046f1f6003a23ad0844486caac2d8cdd4185 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 16 Oct 2024 15:17:59 +0200 Subject: [PATCH 197/280] build(deps): bump pillow from 10.4.0 to 11.0.0 (#293) Bumps [pillow](https://github.com/python-pillow/Pillow) from 10.4.0 to 11.0.0. - [Release notes](https://github.com/python-pillow/Pillow/releases) - [Changelog](https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst) - [Commits](https://github.com/python-pillow/Pillow/compare/10.4.0...11.0.0) --- updated-dependencies: - dependency-name: pillow dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index b9dd1bc3..908d4190 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -80,7 +80,7 @@ docs = [ ] tests = [ "numpy==2.1.2 ; sys_platform == 'windows' and python_version >= '3.13'", - "pillow==10.4.0", + "pillow==11.0.0", "pytest==8.3.3", "pytest-cov==5.0.0", "pytest-rerunfailures==14.0.0", From e990a22a81e5fa37874da256f81ce3edb6ab6749 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Fri, 18 Oct 2024 11:25:53 +0200 Subject: [PATCH 198/280] docs: funding --- .well-known/funding-manifest-urls | 1 + 1 file changed, 1 insertion(+) create mode 100644 .well-known/funding-manifest-urls diff --git a/.well-known/funding-manifest-urls b/.well-known/funding-manifest-urls new file mode 100644 index 00000000..b59ae9a3 --- /dev/null +++ b/.well-known/funding-manifest-urls @@ -0,0 +1 @@ +https://www.tiger-222.fr/funding.json From 81618b8f060aa1b75c1de282d24fa0382725dfcd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Fri, 18 Oct 2024 19:17:02 +0200 Subject: [PATCH 199/280] feat(ci): automerge --- .github/workflows/tests.yml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index ee770f71..7e6a4773 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -7,6 +7,10 @@ on: pull_request: workflow_dispatch: +permissions: + contents: write + pull-requests: write + jobs: quality: name: Quality @@ -82,3 +86,15 @@ jobs: - name: Tests (macOS, Windows) if: matrix.os.emoji != '🐧' run: python -m pytest + + automerge: + name: Automerge + runs-on: ubuntu-latest + needs: [documentation, quality, tests] + if: ${{ github.actor == 'dependabot[bot]' }} + steps: + - name: Automerge + run: gh pr merge --auto --rebase "$PR_URL" + env: + PR_URL: ${{github.event.pull_request.html_url}} + GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} From 84d1124b55716c3970c2ac3cdd66a841ba6a3668 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Fri, 18 Oct 2024 22:29:28 +0200 Subject: [PATCH 200/280] fix(ci): prevent issue with automerge --- .github/workflows/tests.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 7e6a4773..d55fd06d 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -1,9 +1,6 @@ name: Tests on: - push: - branches: - - main pull_request: workflow_dispatch: From cfea6e43b25ceb25f8a5808298b94a29d910630a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Fri, 18 Oct 2024 22:31:55 +0200 Subject: [PATCH 201/280] docs: add ScreenVivid --- docs/source/where.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/source/where.rst b/docs/source/where.rst index 18c87e6c..a6307d08 100644 --- a/docs/source/where.rst +++ b/docs/source/where.rst @@ -24,6 +24,7 @@ Do not hesitate to `say Hello! `_ - `Python-ImageSearch `_, a wrapper around OpenCV2 and PyAutoGUI to do image searching easily; - `PUBGIS `_, a map generator of your position throughout PUBG gameplay; - `ScreenCapLibrary `_, a Robot Framework test library for capturing screenshots and video recording; +- `ScreenVivid `_, an open source cross-platform screen recorder for everyone ; - `Self-Driving-Car-3D-Simulator-With-CNN `_; - `Serpent.AI `_, a Game Agent Framework; - `Star Wars - The Old Republic: Galactic StarFighter `_ parser; From 738fdb7933526027f74dca765fa7565945cfae34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Fri, 18 Oct 2024 22:37:43 +0200 Subject: [PATCH 202/280] fix(ci): stop testing PyPy It fails again, but not because of MSS. I want a green CI, so lets skip it. --- .github/workflows/tests.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index d55fd06d..0592accd 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -64,8 +64,6 @@ jobs: runs-on: "3.12" - name: CPython 3.13 runs-on: "3.13-dev" - - name: PyPy 3.10 - runs-on: "pypy-3.10" steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 From b447e035b84a6cf2ee7c02d5f24fd9d8aeb3970f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 18 Oct 2024 20:39:23 +0000 Subject: [PATCH 203/280] build(deps): bump ruff from 0.6.9 to 0.7.0 Bumps [ruff](https://github.com/astral-sh/ruff) from 0.6.9 to 0.7.0. - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/0.6.9...0.7.0) --- updated-dependencies: - dependency-name: ruff dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 908d4190..3969ddfd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -71,7 +71,7 @@ mss = "mss.__main__:main" dev = [ "build==1.2.2.post1", "mypy==1.12.0", - "ruff==0.6.9", + "ruff==0.7.0", "twine==5.1.1", "wheel==0.44.0", ] From 0f17558d1946d02db3b1481439ea24fe5aecac2b Mon Sep 17 00:00:00 2001 From: Shravan Asati Date: Sun, 20 Oct 2024 12:23:25 +0530 Subject: [PATCH 204/280] docs: include import statements in example (#295) --- docs/source/examples.rst | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/source/examples.rst b/docs/source/examples.rst index aede8c30..7bb8157b 100644 --- a/docs/source/examples.rst +++ b/docs/source/examples.rst @@ -32,7 +32,7 @@ Callback Screenshot of the monitor 1 with a callback: .. literalinclude:: examples/callback.py - :lines: 8- + :lines: 7- Part of the screen @@ -41,7 +41,7 @@ Part of the screen You can capture only a part of the screen: .. literalinclude:: examples/part_of_screen.py - :lines: 8- + :lines: 7- .. versionadded:: 3.0.0 @@ -52,7 +52,7 @@ Part of the screen of the 2nd monitor This is an example of capturing some part of the screen of the monitor 2: .. literalinclude:: examples/part_of_screen_monitor_2.py - :lines: 8- + :lines: 7- .. versionadded:: 3.0.0 @@ -64,7 +64,7 @@ You can use the same value as you would do with ``PIL.ImageGrab(bbox=tuple(...)) This is an example that uses it, but also using percentage values: .. literalinclude:: examples/from_pil_tuple.py - :lines: 8- + :lines: 7- .. versionadded:: 3.1.0 @@ -99,7 +99,7 @@ Advanced You can handle data using a custom class: .. literalinclude:: examples/custom_cls_image.py - :lines: 8- + :lines: 7- .. versionadded:: 3.1.0 @@ -110,7 +110,7 @@ You can use the Python Image Library (aka Pillow) to do whatever you want with r This is an example using `frombytes() `_: .. literalinclude:: examples/pil.py - :lines: 8- + :lines: 7- .. versionadded:: 3.0.0 @@ -120,7 +120,7 @@ Playing with pixels This is an example using `putdata() `_: .. literalinclude:: examples/pil_pixels.py - :lines: 8- + :lines: 7- .. versionadded:: 3.0.0 @@ -132,7 +132,7 @@ You can easily view a HD movie with VLC and see it too in the OpenCV window. And with __no__ lag please. .. literalinclude:: examples/opencv_numpy.py - :lines: 8- + :lines: 7- .. versionadded:: 3.0.0 @@ -145,7 +145,7 @@ Benchmark Simple naive benchmark to compare with `Reading game frames in Python with OpenCV - Python Plays GTA V `_: .. literalinclude:: examples/fps.py - :lines: 9- + :lines: 8- .. versionadded:: 3.0.0 @@ -156,7 +156,7 @@ Performances can be improved by delegating the PNG file creation to a specific w This is a simple example using the :py:mod:`multiprocessing` inspired by the `TensorFlow Object Detection Introduction `_ project: .. literalinclude:: examples/fps_multiprocessing.py - :lines: 9- + :lines: 8- .. versionadded:: 5.0.0 From 07cf703c459d120d0a641375eb66e48bffb45e48 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 21 Oct 2024 12:45:52 +0000 Subject: [PATCH 205/280] build(deps): bump mypy from 1.12.0 to 1.12.1 Bumps [mypy](https://github.com/python/mypy) from 1.12.0 to 1.12.1. - [Changelog](https://github.com/python/mypy/blob/master/CHANGELOG.md) - [Commits](https://github.com/python/mypy/compare/v1.12.0...v1.12.1) --- updated-dependencies: - dependency-name: mypy dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 3969ddfd..202cb56b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -70,7 +70,7 @@ mss = "mss.__main__:main" [project.optional-dependencies] dev = [ "build==1.2.2.post1", - "mypy==1.12.0", + "mypy==1.12.1", "ruff==0.7.0", "twine==5.1.1", "wheel==0.44.0", From 034958afe2f44138864fc3aafb746738a8b2a540 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 23 Oct 2024 12:18:37 +0000 Subject: [PATCH 206/280] build(deps): bump mypy from 1.12.1 to 1.13.0 Bumps [mypy](https://github.com/python/mypy) from 1.12.1 to 1.13.0. - [Changelog](https://github.com/python/mypy/blob/master/CHANGELOG.md) - [Commits](https://github.com/python/mypy/compare/v1.12.1...v1.13.0) --- updated-dependencies: - dependency-name: mypy dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 202cb56b..82766b16 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -70,7 +70,7 @@ mss = "mss.__main__:main" [project.optional-dependencies] dev = [ "build==1.2.2.post1", - "mypy==1.12.1", + "mypy==1.13.0", "ruff==0.7.0", "twine==5.1.1", "wheel==0.44.0", From 55a5c89ff3f0e8d1628488bf44b72361c02f72c6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 25 Oct 2024 12:24:29 +0000 Subject: [PATCH 207/280] build(deps): bump ruff from 0.7.0 to 0.7.1 Bumps [ruff](https://github.com/astral-sh/ruff) from 0.7.0 to 0.7.1. - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/0.7.0...0.7.1) --- updated-dependencies: - dependency-name: ruff dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 82766b16..f7ee5e90 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -71,7 +71,7 @@ mss = "mss.__main__:main" dev = [ "build==1.2.2.post1", "mypy==1.13.0", - "ruff==0.7.0", + "ruff==0.7.1", "twine==5.1.1", "wheel==0.44.0", ] From 2af64fd4dc05b7dbe079b60c963669226cb1cfce Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 30 Oct 2024 12:28:59 +0000 Subject: [PATCH 208/280] build(deps): bump pytest-cov from 5.0.0 to 6.0.0 Bumps [pytest-cov](https://github.com/pytest-dev/pytest-cov) from 5.0.0 to 6.0.0. - [Changelog](https://github.com/pytest-dev/pytest-cov/blob/master/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest-cov/compare/v5.0.0...v6.0.0) --- updated-dependencies: - dependency-name: pytest-cov dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index f7ee5e90..4afbafbf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -82,7 +82,7 @@ tests = [ "numpy==2.1.2 ; sys_platform == 'windows' and python_version >= '3.13'", "pillow==11.0.0", "pytest==8.3.3", - "pytest-cov==5.0.0", + "pytest-cov==6.0.0", "pytest-rerunfailures==14.0.0", "pyvirtualdisplay==3.0 ; sys_platform == 'linux'", ] From a133909998d646f663cdf8ce16415a4e7a609f65 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Nov 2024 12:23:48 +0000 Subject: [PATCH 209/280] build(deps): bump ruff from 0.7.1 to 0.7.2 Bumps [ruff](https://github.com/astral-sh/ruff) from 0.7.1 to 0.7.2. - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/0.7.1...0.7.2) --- updated-dependencies: - dependency-name: ruff dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 4afbafbf..2df0a2b3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -71,7 +71,7 @@ mss = "mss.__main__:main" dev = [ "build==1.2.2.post1", "mypy==1.13.0", - "ruff==0.7.1", + "ruff==0.7.2", "twine==5.1.1", "wheel==0.44.0", ] From 98eadcb407171ce24f703ed3bf9584b6a941cc35 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Nov 2024 12:23:56 +0000 Subject: [PATCH 210/280] build(deps): bump numpy from 2.1.2 to 2.1.3 Bumps [numpy](https://github.com/numpy/numpy) from 2.1.2 to 2.1.3. - [Release notes](https://github.com/numpy/numpy/releases) - [Changelog](https://github.com/numpy/numpy/blob/main/doc/RELEASE_WALKTHROUGH.rst) - [Commits](https://github.com/numpy/numpy/compare/v2.1.2...v2.1.3) --- updated-dependencies: - dependency-name: numpy dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 2df0a2b3..06ac0d51 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -79,7 +79,7 @@ docs = [ "sphinx==8.1.3", ] tests = [ - "numpy==2.1.2 ; sys_platform == 'windows' and python_version >= '3.13'", + "numpy==2.1.3 ; sys_platform == 'windows' and python_version >= '3.13'", "pillow==11.0.0", "pytest==8.3.3", "pytest-cov==6.0.0", From b4546bc32c2f13dbb2043979dd8f9fedc4b9769e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 8 Nov 2024 12:32:47 +0000 Subject: [PATCH 211/280] build(deps): bump ruff from 0.7.2 to 0.7.3 Bumps [ruff](https://github.com/astral-sh/ruff) from 0.7.2 to 0.7.3. - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/0.7.2...0.7.3) --- updated-dependencies: - dependency-name: ruff dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 06ac0d51..e772f822 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -71,7 +71,7 @@ mss = "mss.__main__:main" dev = [ "build==1.2.2.post1", "mypy==1.13.0", - "ruff==0.7.2", + "ruff==0.7.3", "twine==5.1.1", "wheel==0.44.0", ] From f050649e498f269d779082b0b390a896711a6f99 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Nov 2024 12:52:56 +0000 Subject: [PATCH 212/280] build(deps): bump wheel from 0.44.0 to 0.45.0 Bumps [wheel](https://github.com/pypa/wheel) from 0.44.0 to 0.45.0. - [Release notes](https://github.com/pypa/wheel/releases) - [Changelog](https://github.com/pypa/wheel/blob/main/docs/news.rst) - [Commits](https://github.com/pypa/wheel/compare/0.44.0...0.45.0) --- updated-dependencies: - dependency-name: wheel dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index e772f822..df78ece3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -73,7 +73,7 @@ dev = [ "mypy==1.13.0", "ruff==0.7.3", "twine==5.1.1", - "wheel==0.44.0", + "wheel==0.45.0", ] docs = [ "sphinx==8.1.3", From 38263d8a6f8a7a7f340171722e83a1a4a30bf6a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Thu, 14 Nov 2024 07:36:22 +0100 Subject: [PATCH 213/280] feat: Python 3.14 support (#300) * feat: Python 3.14 support * Update pyproject.toml * Update test_numpy.py * Update test_pil.py * only test third-party on Linux and Python 3.13 * dot not pin third-party, abd test on all supported Python versions * revert --- .github/workflows/tests.yml | 4 +++- CHANGELOG.md | 1 + pyproject.toml | 6 +++--- src/tests/third_party/test_numpy.py | 4 +--- src/tests/third_party/test_pil.py | 4 +--- 5 files changed, 9 insertions(+), 10 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 0592accd..1caf9b99 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -63,7 +63,9 @@ jobs: - name: CPython 3.12 runs-on: "3.12" - name: CPython 3.13 - runs-on: "3.13-dev" + runs-on: "3.13" + - name: CPython 3.14 + runs-on: "3.14-dev" steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 diff --git a/CHANGELOG.md b/CHANGELOG.md index 0cfeda99..0752f7ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ See Git checking messages for full history. ## 10.0.0 (2024-xx-xx) - removed support for Python 3.8 - removed support for Python 3.9 +- added support for Python 3.14 - :heart: contributors: @ ## 9.0.2 (2024-09-01) diff --git a/pyproject.toml b/pyproject.toml index df78ece3..27f7abe4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,6 +35,7 @@ classifiers = [ "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", "Topic :: Multimedia :: Graphics :: Capture :: Screen Capture", "Topic :: Software Development :: Libraries", ] @@ -73,14 +74,13 @@ dev = [ "mypy==1.13.0", "ruff==0.7.3", "twine==5.1.1", - "wheel==0.45.0", ] docs = [ "sphinx==8.1.3", ] tests = [ - "numpy==2.1.3 ; sys_platform == 'windows' and python_version >= '3.13'", - "pillow==11.0.0", + "numpy==2.1.3 ; sys_platform == 'linux' and python_version == '3.13'", + "pillow==11.0.0 ; sys_platform == 'linux' and python_version == '3.13'", "pytest==8.3.3", "pytest-cov==6.0.0", "pytest-rerunfailures==14.0.0", diff --git a/src/tests/third_party/test_numpy.py b/src/tests/third_party/test_numpy.py index a7a279dc..6a2f2e09 100644 --- a/src/tests/third_party/test_numpy.py +++ b/src/tests/third_party/test_numpy.py @@ -9,9 +9,7 @@ from mss import mss -pytest.importorskip("numpy", reason="Numpy module not available.") - -import numpy as np # noqa: E402 +np = pytest.importorskip("numpy", reason="Numpy module not available.") def test_numpy(pixel_ratio: int) -> None: diff --git a/src/tests/third_party/test_pil.py b/src/tests/third_party/test_pil.py index 3555d7af..a7d3b7be 100644 --- a/src/tests/third_party/test_pil.py +++ b/src/tests/third_party/test_pil.py @@ -10,9 +10,7 @@ from mss import mss -pytest.importorskip("PIL", reason="PIL module not available.") - -from PIL import Image # noqa: E402 +Image = pytest.importorskip("PIL.Image", reason="PIL module not available.") def test_pil() -> None: From 139ee83f4c0a3a8fc36914d6d478cfd6eb33c878 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Thu, 14 Nov 2024 08:36:32 +0100 Subject: [PATCH 214/280] fix: restore Python 3.9 support (#305) * fix: restore Python 3.9 support * fix `TypeError: zip() takes no keyword arguments` --- .github/workflows/tests.yml | 2 ++ CHANGELOG.md | 1 - README.md | 2 +- docs/source/examples/pil_pixels.py | 2 +- docs/source/index.rst | 2 +- docs/source/support.rst | 3 +-- pyproject.toml | 5 +++-- src/mss/base.py | 6 ++++-- src/mss/screenshot.py | 4 ++-- src/tests/test_implementation.py | 4 ++-- 10 files changed, 17 insertions(+), 14 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 1caf9b99..fa93b929 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -56,6 +56,8 @@ jobs: - emoji: 🪟 runs-on: [windows-latest] python: + - name: CPython 3.9 + runs-on: "3.9" - name: CPython 3.10 runs-on: "3.10" - name: CPython 3.11 diff --git a/CHANGELOG.md b/CHANGELOG.md index 0752f7ef..8aedde98 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,6 @@ See Git checking messages for full history. ## 10.0.0 (2024-xx-xx) - removed support for Python 3.8 -- removed support for Python 3.9 - added support for Python 3.14 - :heart: contributors: @ diff --git a/README.md b/README.md index 5ede99b2..abfc4f72 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ with mss() as sct: An ultra-fast cross-platform multiple screenshots module in pure python using ctypes. -- **Python 3.10+**, PEP8 compliant, no dependency, thread-safe; +- **Python 3.9+**, PEP8 compliant, no dependency, thread-safe; - very basic, it will grab one screenshot by monitor or a screenshot of all monitors and save it to a PNG file; - but you can use PIL and benefit from all its formats (or add yours directly); - integrate well with Numpy and OpenCV; diff --git a/docs/source/examples/pil_pixels.py b/docs/source/examples/pil_pixels.py index d231dbbb..d1264bc6 100644 --- a/docs/source/examples/pil_pixels.py +++ b/docs/source/examples/pil_pixels.py @@ -16,7 +16,7 @@ img = Image.new("RGB", sct_img.size) # Best solution: create a list(tuple(R, G, B), ...) for putdata() - pixels = zip(sct_img.raw[2::4], sct_img.raw[1::4], sct_img.raw[::4], strict=False) + pixels = zip(sct_img.raw[2::4], sct_img.raw[1::4], sct_img.raw[::4]) img.putdata(list(pixels)) # But you can set individual pixels too (slower) diff --git a/docs/source/index.rst b/docs/source/index.rst index 5dae783e..f28aee40 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -12,7 +12,7 @@ Welcome to Python MSS's documentation! An ultra fast cross-platform multiple screenshots module in pure python using ctypes. - - **Python 3.10+**, :pep:`8` compliant, no dependency, thread-safe; + - **Python 3.9+**, :pep:`8` compliant, no dependency, thread-safe; - very basic, it will grab one screenshot by monitor or a screenshot of all monitors and save it to a PNG file; - but you can use PIL and benefit from all its formats (or add yours directly); - integrate well with Numpy and OpenCV; diff --git a/docs/source/support.rst b/docs/source/support.rst index 3462aae9..102dc01d 100644 --- a/docs/source/support.rst +++ b/docs/source/support.rst @@ -5,7 +5,7 @@ Support Feel free to try MSS on a system we had not tested, and let us know by creating an `issue `_. - OS: GNU/Linux, macOS and Windows - - Python: 3.10 and newer + - Python: 3.9 and newer Future @@ -35,4 +35,3 @@ Abandoned - Python 3.6 (2022-10-27) - Python 3.7 (2023-04-09) - Python 3.8 (2024-09-01) -- Python 3.9 (2024-09-01) diff --git a/pyproject.toml b/pyproject.toml index 27f7abe4..94746c11 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "hatchling.build" name = "mss" description = "An ultra fast cross-platform multiple screenshots module in pure python using ctypes." readme = "README.md" -requires-python = ">= 3.10" +requires-python = ">= 3.9" authors = [ { name = "Mickaël Schoentgen", email="contact@tiger-222.fr" }, ] @@ -31,6 +31,7 @@ classifiers = [ "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", @@ -149,7 +150,7 @@ exclude = [ ] line-length = 120 indent-width = 4 -target-version = "py310" +target-version = "py39" [tool.ruff.lint] extend-select = ["ALL"] diff --git a/src/mss/base.py b/src/mss/base.py index 9e926079..cf588d0d 100644 --- a/src/mss/base.py +++ b/src/mss/base.py @@ -39,9 +39,11 @@ def __init__( /, *, compression_level: int = 6, - display: bytes | str | None = None, # noqa:ARG002 Linux only - max_displays: int = 32, # noqa:ARG002 Mac only with_cursor: bool = False, + # Linux only + display: bytes | str | None = None, # noqa: ARG002 + # Mac only + max_displays: int = 32, # noqa: ARG002 ) -> None: self.cls_image: type[ScreenShot] = ScreenShot self.compression_level = compression_level diff --git a/src/mss/screenshot.py b/src/mss/screenshot.py index e51a37f2..5bcf654b 100644 --- a/src/mss/screenshot.py +++ b/src/mss/screenshot.py @@ -80,8 +80,8 @@ def left(self) -> int: def pixels(self) -> Pixels: """:return list: RGB tuples.""" if not self.__pixels: - rgb_tuples: Iterator[Pixel] = zip(self.raw[2::4], self.raw[1::4], self.raw[::4], strict=False) - self.__pixels = list(zip(*[iter(rgb_tuples)] * self.width, strict=False)) + 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)) return self.__pixels diff --git a/src/tests/test_implementation.py b/src/tests/test_implementation.py index 42429b17..146d336f 100644 --- a/src/tests/test_implementation.py +++ b/src/tests/test_implementation.py @@ -116,7 +116,7 @@ def main(*args: str, ret: int = 0) -> None: assert os.path.isfile("monitor-1.png") os.remove("monitor-1.png") - for opts in zip(["-m 1", "--monitor=1"], ["-q", "--quiet"], strict=False): + for opts in zip(["-m 1", "--monitor=1"], ["-q", "--quiet"]): main(*opts) captured = capsys.readouterr() assert not captured.out @@ -128,7 +128,7 @@ def main(*args: str, ret: int = 0) -> None: main(opt, fmt) captured = capsys.readouterr() with mss.mss(display=os.getenv("DISPLAY")) as sct: - for mon, (monitor, line) in enumerate(zip(sct.monitors[1:], captured.out.splitlines(), strict=False), 1): + for mon, (monitor, line) in enumerate(zip(sct.monitors[1:], captured.out.splitlines()), 1): filename = fmt.format(mon=mon, **monitor) assert line.endswith(filename) assert os.path.isfile(filename) From a25dab032a1f3391dde9936859828f97f32aa082 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Thu, 14 Nov 2024 09:52:56 +0100 Subject: [PATCH 215/280] feat: tweak `ruff` rules (#306) --- CHANGES.md | 12 ++++++++ docs/source/examples/callback.py | 12 ++++---- pyproject.toml | 46 +++++++++++++++++-------------- src/mss/base.py | 5 ++-- src/mss/darwin.py | 13 ++++++--- src/mss/factory.py | 1 - src/mss/linux.py | 8 ++++-- src/mss/tools.py | 9 ++++-- src/mss/windows.py | 3 +- src/tests/__init__.py | 0 src/tests/conftest.py | 18 ++++++------ src/tests/test_implementation.py | 44 +++++++++++++++-------------- src/tests/test_save.py | 25 +++++++++-------- src/tests/test_setup.py | 1 + src/tests/test_tools.py | 41 ++++++++++++++------------- src/tests/third_party/test_pil.py | 16 +++++++---- 16 files changed, 144 insertions(+), 110 deletions(-) create mode 100644 src/tests/__init__.py diff --git a/CHANGES.md b/CHANGES.md index cda211e9..74021f68 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,17 @@ # Technical Changes +## 10.0.0 (2024-11-xx) + +### base.py +- Added `OPAQUE` + +### darwin.py +- Added `MAC_VERSION_CATALINA` + +### linux.py +- Added `BITS_PER_PIXELS_32` +- Added `SUPPORTED_BITS_PER_PIXELS` + ## 9.0.0 (2023-04-18) ### linux.py diff --git a/docs/source/examples/callback.py b/docs/source/examples/callback.py index cb644436..5a93d122 100644 --- a/docs/source/examples/callback.py +++ b/docs/source/examples/callback.py @@ -4,18 +4,18 @@ Screenshot of the monitor 1, with callback. """ -import os -import os.path +from pathlib import Path import mss def on_exists(fname: str) -> None: """Callback example when we try to overwrite an existing screenshot.""" - if os.path.isfile(fname): - newfile = f"{fname}.old" - print(f"{fname} -> {newfile}") - os.rename(fname, newfile) + file = Path(fname) + if file.is_file(): + newfile = file.with_name(f"{file.name}.old") + print(f"{fname} → {newfile}") + file.rename(newfile) with mss.mss() as sct: diff --git a/pyproject.toml b/pyproject.toml index 94746c11..1f5fc2d1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -152,28 +152,34 @@ line-length = 120 indent-width = 4 target-version = "py39" -[tool.ruff.lint] -extend-select = ["ALL"] -ignore = [ - "ANN101", - "ANN401", - "C90", - "COM812", - "D", # TODO - "ERA", - "FBT", - "INP001", - "ISC001", - "PTH", - "PL", - "S", - "SLF", - "T201", -] -fixable = ["ALL"] - [tool.ruff.format] quote-style = "double" indent-style = "space" skip-magic-trailing-comma = false line-ending = "auto" + +[tool.ruff.lint] +fixable = ["ALL"] +extend-select = ["ALL"] +ignore = [ + "ANN401", # typing.Any + "C90", # complexity + "COM812", # conflict + "D", # TODO + "ISC001", # conflict + "T201", # `print()` +] + +[tool.ruff.lint.per-file-ignores] +"docs/source/*" = [ + "ERA001", # commented code + "INP001", # file `xxx` is part of an implicit namespace package +] +"src/tests/*" = [ + "FBT001", # boolean-typed positional argument in function definition + "PLR2004", # magic value used in comparison + "S101", # use of `assert` detected + "S602", # `subprocess` call with `shell=True` + "S603", # `subprocess` call + "SLF001", # private member accessed +] diff --git a/src/mss/base.py b/src/mss/base.py index cf588d0d..8a7397f5 100644 --- a/src/mss/base.py +++ b/src/mss/base.py @@ -28,6 +28,8 @@ lock = Lock() +OPAQUE = 255 + class MSSBase(metaclass=ABCMeta): """This class will be overloaded by a system specific one.""" @@ -200,7 +202,6 @@ def shot(self, /, **kwargs: Any) -> str: @staticmethod def _merge(screenshot: ScreenShot, cursor: ScreenShot, /) -> ScreenShot: """Create composite image by blending screenshot and mouse cursor.""" - (cx, cy), (cw, ch) = cursor.pos, cursor.size (x, y), (w, h) = screenshot.pos, screenshot.size @@ -234,7 +235,7 @@ def _merge(screenshot: ScreenShot, cursor: ScreenShot, /) -> ScreenShot: if not alpha: continue - if alpha == 255: + if alpha == OPAQUE: screen_raw[spos : spos + 3] = cursor_raw[cpos : cpos + 3] else: alpha2 = alpha / 255 diff --git a/src/mss/darwin.py b/src/mss/darwin.py index 6d4fa289..a56e05a8 100644 --- a/src/mss/darwin.py +++ b/src/mss/darwin.py @@ -20,6 +20,8 @@ __all__ = ("MSS",) +MAC_VERSION_CATALINA = 10.16 + def cgfloat() -> type[c_double | c_float]: """Get the appropriate value for a float.""" @@ -59,7 +61,7 @@ def __repr__(self) -> str: # # Note: keep it sorted by cfunction. CFUNCTIONS: CFunctions = { - # cfunction: (attr, argtypes, restype) + # Syntax: cfunction: (attr, argtypes, restype) "CGDataProviderCopyData": ("core", [c_void_p], c_void_p), "CGDisplayBounds": ("core", [c_uint32], CGRect), "CGDisplayRotation": ("core", [c_uint32], c_float), @@ -98,7 +100,7 @@ def __init__(self, /, **kwargs: Any) -> None: def _init_library(self) -> None: """Load the CoreGraphics library.""" version = float(".".join(mac_ver()[0].split(".")[:2])) - if version < 10.16: + if version < MAC_VERSION_CATALINA: coregraphics = ctypes.util.find_library("CoreGraphics") else: # macOS Big Sur and newer @@ -136,9 +138,13 @@ def _monitors_impl(self) -> None: rect = core.CGDisplayBounds(display) rect = core.CGRectStandardize(rect) width, height = rect.size.width, rect.size.height + + # 0.0: normal + # 90.0: right + # -90.0: left if core.CGDisplayRotation(display) in {90.0, -90.0}: - # {0.0: "normal", 90.0: "right", -90.0: "left"} width, height = height, width + self._monitors.append( { "left": int_(rect.origin.x), @@ -161,7 +167,6 @@ def _monitors_impl(self) -> None: def _grab_impl(self, monitor: Monitor, /) -> ScreenShot: """Retrieve all pixels from a monitor. Pixels have to be RGB.""" - core = self.core rect = CGRect((monitor["left"], monitor["top"]), (monitor["width"], monitor["height"])) diff --git a/src/mss/factory.py b/src/mss/factory.py index 83ea0d32..b0793e8c 100644 --- a/src/mss/factory.py +++ b/src/mss/factory.py @@ -19,7 +19,6 @@ def mss(**kwargs: Any) -> MSSBase: It then proxies its arguments to the class for instantiation. """ - os_ = platform.system().lower() if os_ == "darwin": diff --git a/src/mss/linux.py b/src/mss/linux.py index 6dac52b8..d357e3be 100644 --- a/src/mss/linux.py +++ b/src/mss/linux.py @@ -42,6 +42,10 @@ PLAINMASK = 0x00FFFFFF ZPIXMAP = 2 +BITS_PER_PIXELS_32 = 32 +SUPPORTED_BITS_PER_PIXELS = { + BITS_PER_PIXELS_32, +} class Display(Structure): @@ -233,7 +237,7 @@ def _validate(retval: int, func: Any, args: tuple[Any, Any], /) -> tuple[Any, An # # Note: keep it sorted by cfunction. CFUNCTIONS: CFunctions = { - # cfunction: (attr, argtypes, restype) + # Syntax: cfunction: (attr, argtypes, restype) "XCloseDisplay": ("xlib", [POINTER(Display)], c_void_p), "XDefaultRootWindow": ("xlib", [POINTER(Display)], POINTER(XWindowAttributes)), "XDestroyImage": ("xlib", [POINTER(XImage)], c_void_p), @@ -433,7 +437,7 @@ def _grab_impl(self, monitor: Monitor, /) -> ScreenShot: try: bits_per_pixel = ximage.contents.bits_per_pixel - if bits_per_pixel != 32: + if bits_per_pixel not in SUPPORTED_BITS_PER_PIXELS: msg = f"[XImage] bits per pixel value not (yet?) implemented: {bits_per_pixel}." raise ScreenShotError(msg) diff --git a/src/mss/tools.py b/src/mss/tools.py index 2383665f..9eb8b6f7 100644 --- a/src/mss/tools.py +++ b/src/mss/tools.py @@ -7,9 +7,13 @@ import os import struct import zlib +from typing import TYPE_CHECKING +if TYPE_CHECKING: + from pathlib import Path -def to_png(data: bytes, size: tuple[int, int], /, *, level: int = 6, output: str | None = None) -> bytes | None: + +def to_png(data: bytes, size: tuple[int, int], /, *, level: int = 6, output: Path | str | None = None) -> bytes | None: """Dump data to a PNG file. If `output` is `None`, create no file but return the whole PNG data. @@ -18,7 +22,6 @@ def to_png(data: bytes, size: tuple[int, int], /, *, level: int = 6, output: str :param int level: PNG compression level. :param str output: Output file name. """ - pack = struct.pack crc32 = zlib.crc32 @@ -49,7 +52,7 @@ def to_png(data: bytes, size: tuple[int, int], /, *, level: int = 6, output: str # Returns raw bytes of the whole PNG data return magic + b"".join(ihdr + idat + iend) - with open(output, "wb") as fileh: + with open(output, "wb") as fileh: # noqa: PTH123 fileh.write(magic) fileh.write(b"".join(ihdr)) fileh.write(b"".join(idat)) diff --git a/src/mss/windows.py b/src/mss/windows.py index 0f41cd62..7a3a78f5 100644 --- a/src/mss/windows.py +++ b/src/mss/windows.py @@ -74,7 +74,7 @@ class BITMAPINFO(Structure): # # Note: keep it sorted by cfunction. CFUNCTIONS: CFunctions = { - # cfunction: (attr, argtypes, restype) + # Syntax: 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), @@ -179,7 +179,6 @@ def _callback(_monitor: int, _data: HDC, rect: LPRECT, _dc: LPARAM) -> int: """Callback for monitorenumproc() function, it will return a RECT with appropriate values. """ - rct = rect.contents self._monitors.append( { diff --git a/src/tests/__init__.py b/src/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/tests/conftest.py b/src/tests/conftest.py index 0a30eb85..5d455821 100644 --- a/src/tests/conftest.py +++ b/src/tests/conftest.py @@ -2,11 +2,9 @@ Source: https://github.com/BoboTiG/python-mss. """ -import glob -import os import platform from collections.abc import Generator -from hashlib import md5 +from hashlib import sha256 from pathlib import Path from zipfile import ZipFile @@ -28,13 +26,13 @@ def _no_warnings(recwarn: pytest.WarningsRecorder) -> Generator: def purge_files() -> None: """Remove all generated files from previous runs.""" - for fname in glob.glob("*.png"): - print(f"Deleting {fname!r} ...") - os.unlink(fname) + for file in Path().glob("*.png"): + print(f"Deleting {file} ...") + file.unlink() - for fname in glob.glob("*.png.old"): - print(f"Deleting {fname!r} ...") - os.unlink(fname) + for file in Path().glob("*.png.old"): + print(f"Deleting {file} ...") + file.unlink() @pytest.fixture(scope="module", autouse=True) @@ -48,7 +46,7 @@ def raw() -> bytes: with ZipFile(file) as fh: data = fh.read(file.with_suffix("").name) - assert md5(data).hexdigest() == "125696266e2a8f5240f6bc17e4df98c6" + assert sha256(data).hexdigest() == "d86ed4366d5a882cfe1345de82c87b81aef9f9bf085f4c42acb6f63f3967eccd" return data diff --git a/src/tests/test_implementation.py b/src/tests/test_implementation.py index 146d336f..5672f044 100644 --- a/src/tests/test_implementation.py +++ b/src/tests/test_implementation.py @@ -9,6 +9,7 @@ import platform import sys from datetime import datetime +from pathlib import Path from typing import TYPE_CHECKING from unittest.mock import Mock, patch @@ -104,24 +105,25 @@ def main(*args: str, ret: int = 0) -> None: main() captured = capsys.readouterr() for mon, line in enumerate(captured.out.splitlines(), 1): - filename = f"monitor-{mon}.png" - assert line.endswith(filename) - assert os.path.isfile(filename) - os.remove(filename) + filename = Path(f"monitor-{mon}.png") + assert line.endswith(filename.name) + assert filename.is_file() + filename.unlink() + file = Path("monitor-1.png") for opt in ("-m", "--monitor"): main(opt, "1") captured = capsys.readouterr() - assert captured.out.endswith("monitor-1.png\n") - assert os.path.isfile("monitor-1.png") - os.remove("monitor-1.png") + assert captured.out.endswith(f"{file.name}\n") + assert filename.is_file() + filename.unlink() 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") - os.remove("monitor-1.png") + assert filename.is_file() + filename.unlink() fmt = "sct-{mon}-{width}x{height}.png" for opt in ("-o", "--out"): @@ -129,28 +131,28 @@ def main(*args: str, ret: int = 0) -> None: captured = capsys.readouterr() 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) - assert os.path.isfile(filename) - os.remove(filename) + filename = Path(fmt.format(mon=mon, **monitor)) + assert line.endswith(filename.name) + assert filename.is_file() + filename.unlink() 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(tz=UTC)) + filename = Path(fmt.format(mon=1, date=datetime.now(tz=UTC))) captured = capsys.readouterr() - assert captured.out.endswith(filename + "\n") - assert os.path.isfile(filename) - os.remove(filename) + assert captured.out.endswith(f"{filename}\n") + assert filename.is_file() + filename.unlink() coordinates = "2,12,40,67" - filename = "sct-2x12_40x67.png" + filename = Path("sct-2x12_40x67.png") for opt in ("-c", "--coordinates"): main(opt, coordinates) captured = capsys.readouterr() - assert captured.out.endswith(filename + "\n") - assert os.path.isfile(filename) - os.remove(filename) + assert captured.out.endswith(f"{filename}\n") + assert filename.is_file() + filename.unlink() coordinates = "2,12,40" for opt in ("-c", "--coordinates"): diff --git a/src/tests/test_save.py b/src/tests/test_save.py index a8d6e385..9597206c 100644 --- a/src/tests/test_save.py +++ b/src/tests/test_save.py @@ -4,6 +4,7 @@ import os.path from datetime import datetime +from pathlib import Path import pytest @@ -26,33 +27,33 @@ def test_at_least_2_monitors() -> None: def test_files_exist() -> None: with mss(display=os.getenv("DISPLAY")) as sct: for filename in sct.save(): - assert os.path.isfile(filename) + assert Path(filename).is_file() - assert os.path.isfile(sct.shot()) + assert Path(sct.shot()).is_file() sct.shot(mon=-1, output="fullscreen.png") - assert os.path.isfile("fullscreen.png") + assert Path("fullscreen.png").is_file() 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) + file = Path(fname) + if Path(file).is_file(): + file.rename(f"{file.name}.old") with mss(display=os.getenv("DISPLAY")) as sct: filename = sct.shot(mon=0, output="mon0.png", callback=on_exists) - assert os.path.isfile(filename) + assert Path(filename).is_file() filename = sct.shot(output="mon1.png", callback=on_exists) - assert os.path.isfile(filename) + assert Path(filename).is_file() 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) + assert Path(filename).is_file() def test_output_format_positions_and_sizes() -> None: @@ -60,7 +61,7 @@ def test_output_format_positions_and_sizes() -> None: with mss(display=os.getenv("DISPLAY")) as sct: filename = sct.shot(mon=1, output=fmt) assert filename == fmt.format(**sct.monitors[1]) - assert os.path.isfile(filename) + assert Path(filename).is_file() def test_output_format_date_simple() -> None: @@ -68,7 +69,7 @@ def test_output_format_date_simple() -> None: with mss(display=os.getenv("DISPLAY")) as sct: try: filename = sct.shot(mon=1, output=fmt) - assert os.path.isfile(filename) + assert Path(filename).is_file() 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.") @@ -79,4 +80,4 @@ def test_output_format_date_custom() -> None: with mss(display=os.getenv("DISPLAY")) as sct: filename = sct.shot(mon=1, output=fmt) assert filename == fmt.format(date=datetime.now(tz=UTC)) - assert os.path.isfile(filename) + assert Path(filename).is_file() diff --git a/src/tests/test_setup.py b/src/tests/test_setup.py index 5fd2f81d..d4788677 100644 --- a/src/tests/test_setup.py +++ b/src/tests/test_setup.py @@ -74,6 +74,7 @@ def test_sdist() -> None: f"mss-{__version__}/src/mss/screenshot.py", f"mss-{__version__}/src/mss/tools.py", f"mss-{__version__}/src/mss/windows.py", + f"mss-{__version__}/src/tests/__init__.py", f"mss-{__version__}/src/tests/bench_bgra2rgb.py", f"mss-{__version__}/src/tests/bench_general.py", f"mss-{__version__}/src/tests/conftest.py", diff --git a/src/tests/test_tools.py b/src/tests/test_tools.py index ff742e88..a1494833 100644 --- a/src/tests/test_tools.py +++ b/src/tests/test_tools.py @@ -5,6 +5,7 @@ import hashlib import os.path import zlib +from pathlib import Path import pytest @@ -13,7 +14,7 @@ WIDTH = 10 HEIGHT = 10 -MD5SUM = "055e615b74167c9bdfea16a00539450c" +MD5SUM = "ee1b645cc989cbfc48e613b395a929d3d79a922b77b9b38e46647ff6f74acef5" def test_bad_compression_level() -> None: @@ -23,50 +24,48 @@ def test_bad_compression_level() -> None: def test_compression_level() -> None: data = b"rgb" * WIDTH * HEIGHT - output = f"{WIDTH}x{HEIGHT}.png" + output = Path(f"{WIDTH}x{HEIGHT}.png") with mss(display=os.getenv("DISPLAY")) as sct: to_png(data, (WIDTH, HEIGHT), level=sct.compression_level, output=output) - with open(output, "rb") as png: - assert hashlib.md5(png.read()).hexdigest() == MD5SUM + assert hashlib.sha256(output.read_bytes()).hexdigest() == MD5SUM @pytest.mark.parametrize( ("level", "checksum"), [ - (0, "f37123dbc08ed7406d933af11c42563e"), - (1, "7d5dcf2a2224445daf19d6d91cf31cb5"), - (2, "bde05376cf51cf951e26c31c5f55e9d5"), - (3, "3d7e73c2a9c2d8842b363eeae8085919"), - (4, "9565a5caf89a9221459ee4e02b36bf6e"), - (5, "4d722e21e7d62fbf1e3154de7261fc67"), - (6, "055e615b74167c9bdfea16a00539450c"), - (7, "4d88d3f5923b6ef05b62031992294839"), - (8, "4d88d3f5923b6ef05b62031992294839"), - (9, "4d88d3f5923b6ef05b62031992294839"), + (0, "547191069e78eef1c5899f12c256dd549b1338e67c5cd26a7cbd1fc5a71b83aa"), + (1, "841665ec73b641dfcafff5130b497f5c692ca121caeb06b1d002ad3de5c77321"), + (2, "b11107163207f68f36294deb3f8e6b6a5a11399a532917bdd59d1d5f1117d4d0"), + (3, "31278bad8c1c077c715ac4f3b497694a323a71a87c5ff8bdc7600a36bd8d8c96"), + (4, "8f7237e1394d9ddc71fcb1fa4a2c2953087562ef6eac85d32d8154b61b287fb0"), + (5, "83a55f161bad2d511b222dcd32059c9adf32c3238b65f9aa576f19bc0a6c8fec"), + (6, "ee1b645cc989cbfc48e613b395a929d3d79a922b77b9b38e46647ff6f74acef5"), + (7, "85f8d1b01cef926c111b194229bd6c01e2a65b18b4dd902293698e6de8f4e9ac"), + (8, "85f8d1b01cef926c111b194229bd6c01e2a65b18b4dd902293698e6de8f4e9ac"), + (9, "85f8d1b01cef926c111b194229bd6c01e2a65b18b4dd902293698e6de8f4e9ac"), ], ) 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 + sha256 = hashlib.sha256(raw).hexdigest() + assert sha256 == checksum def test_output_file() -> None: data = b"rgb" * WIDTH * HEIGHT - output = f"{WIDTH}x{HEIGHT}.png" + output = Path(f"{WIDTH}x{HEIGHT}.png") to_png(data, (WIDTH, HEIGHT), output=output) - assert os.path.isfile(output) - with open(output, "rb") as png: - assert hashlib.md5(png.read()).hexdigest() == MD5SUM + assert output.is_file() + assert hashlib.sha256(output.read_bytes()).hexdigest() == MD5SUM 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 + assert hashlib.sha256(raw).hexdigest() == MD5SUM diff --git a/src/tests/third_party/test_pil.py b/src/tests/third_party/test_pil.py index a7d3b7be..a3194485 100644 --- a/src/tests/third_party/test_pil.py +++ b/src/tests/third_party/test_pil.py @@ -5,6 +5,7 @@ import itertools import os import os.path +from pathlib import Path import pytest @@ -26,8 +27,9 @@ def test_pil() -> None: for x, y in itertools.product(range(width), range(height)): assert img.getpixel((x, y)) == sct_img.pixel(x, y) - img.save("box.png") - assert os.path.isfile("box.png") + output = Path("box.png") + img.save(output) + assert output.is_file() def test_pil_bgra() -> None: @@ -43,8 +45,9 @@ def test_pil_bgra() -> None: for x, y in itertools.product(range(width), range(height)): assert img.getpixel((x, y)) == sct_img.pixel(x, y) - img.save("box-bgra.png") - assert os.path.isfile("box-bgra.png") + output = Path("box-bgra.png") + img.save(output) + assert output.is_file() def test_pil_not_16_rounded() -> None: @@ -60,5 +63,6 @@ def test_pil_not_16_rounded() -> None: for x, y in itertools.product(range(width), range(height)): assert img.getpixel((x, y)) == sct_img.pixel(x, y) - img.save("box.png") - assert os.path.isfile("box.png") + output = Path("box.png") + img.save(output) + assert output.is_file() From 4484be8f8b907f110a4de7a9b604bc52e30a928a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Thu, 14 Nov 2024 10:20:18 +0100 Subject: [PATCH 216/280] Linux: fix a threadding issue in `.close()` when calling `XCloseDisplay()` --- CHANGELOG.md | 1 + src/mss/linux.py | 15 ++++++++------- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8aedde98..9946de92 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ See Git checking messages for full history. ## 10.0.0 (2024-xx-xx) - removed support for Python 3.8 - added support for Python 3.14 +- Linux: fixed a threadding issue in `.close()` when calling `XCloseDisplay()` (#251) - :heart: contributors: @ ## 9.0.2 (2024-09-01) diff --git a/src/mss/linux.py b/src/mss/linux.py index d357e3be..6ea56afe 100644 --- a/src/mss/linux.py +++ b/src/mss/linux.py @@ -329,6 +329,14 @@ def __init__(self, /, **kwargs: Any) -> None: self._handles.drawable = cast(self._handles.root, POINTER(Display)) def close(self) -> None: + # Clean-up + if self._handles.display: + with lock: + self.xlib.XCloseDisplay(self._handles.display) + self._handles.display = None + self._handles.drawable = None + self._handles.root = None + # Remove our error handler if self._handles.original_error_handler: # It's required when exiting MSS to prevent letting `_error_handler()` as default handler. @@ -339,13 +347,6 @@ def close(self) -> None: self.xlib.XSetErrorHandler(self._handles.original_error_handler) self._handles.original_error_handler = None - # Clean-up - if self._handles.display: - self.xlib.XCloseDisplay(self._handles.display) - self._handles.display = None - self._handles.drawable = None - self._handles.root = None - # Also empty the error dict _ERROR.clear() From a8291f10b138e86a57c4ca29ae8682c31d76b673 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Thu, 14 Nov 2024 10:20:54 +0100 Subject: [PATCH 217/280] Linux: minor optimization when checking for a X extension status By moving the code not using the thread lock outside of the context manager. --- CHANGELOG.md | 1 + src/mss/linux.py | 16 ++++++++-------- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9946de92..681e2b45 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ See Git checking messages for full history. - removed support for Python 3.8 - added support for Python 3.14 - Linux: fixed a threadding issue in `.close()` when calling `XCloseDisplay()` (#251) +- Linux: minor optimization when checking for a X extension status (#251) - :heart: contributors: @ ## 9.0.2 (2024-09-01) diff --git a/src/mss/linux.py b/src/mss/linux.py index 6ea56afe..20f85507 100644 --- a/src/mss/linux.py +++ b/src/mss/linux.py @@ -352,12 +352,12 @@ def close(self) -> None: def _is_extension_enabled(self, name: str, /) -> bool: """Return True if the given *extension* is enabled on the server.""" - with lock: - major_opcode_return = c_int() - first_event_return = c_int() - first_error_return = c_int() + major_opcode_return = c_int() + first_event_return = c_int() + first_error_return = c_int() - try: + try: + with lock: self.xlib.XQueryExtension( self._handles.display, name.encode("latin1"), @@ -365,9 +365,9 @@ def _is_extension_enabled(self, name: str, /) -> bool: byref(first_event_return), byref(first_error_return), ) - except ScreenShotError: - return False - return True + except ScreenShotError: + return False + return True def _set_cfunctions(self) -> None: """Set all ctypes functions and attach them to attributes.""" From 20a87e142555dbf2e64e432c496b780995eac9ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Thu, 14 Nov 2024 10:36:07 +0100 Subject: [PATCH 218/280] Version 10.0.0 --- CHANGELOG.md | 2 +- CHANGES.md | 2 +- docs/source/support.rst | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 681e2b45..d8e6c052 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ See Git checking messages for full history. -## 10.0.0 (2024-xx-xx) +## 10.0.0 (2024-11-14) - removed support for Python 3.8 - added support for Python 3.14 - Linux: fixed a threadding issue in `.close()` when calling `XCloseDisplay()` (#251) diff --git a/CHANGES.md b/CHANGES.md index 74021f68..2b456f87 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,6 @@ # Technical Changes -## 10.0.0 (2024-11-xx) +## 10.0.0 (2024-11-14) ### base.py - Added `OPAQUE` diff --git a/docs/source/support.rst b/docs/source/support.rst index 102dc01d..c0e4effb 100644 --- a/docs/source/support.rst +++ b/docs/source/support.rst @@ -4,7 +4,7 @@ Support Feel free to try MSS on a system we had not tested, and let us know by creating an `issue `_. - - OS: GNU/Linux, macOS and Windows + - OS: GNU/Linux, macOS, and Windows - Python: 3.9 and newer @@ -34,4 +34,4 @@ Abandoned - Python 3.5 (2022-10-27) - Python 3.6 (2022-10-27) - Python 3.7 (2023-04-09) -- Python 3.8 (2024-09-01) +- Python 3.8 (2024-11-14) From 2b3ae4d6152a27cfc5321a3d941fac647fa22f0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Thu, 14 Nov 2024 10:39:43 +0100 Subject: [PATCH 219/280] Bump the version --- CHANGELOG.md | 6 +++++- src/mss/__init__.py | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d8e6c052..39068fc1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,12 +2,16 @@ See Git checking messages for full history. +## 10.0.1 (202x-xx-xx) +- +- :heart: contributors: @ + ## 10.0.0 (2024-11-14) - removed support for Python 3.8 - added support for Python 3.14 - Linux: fixed a threadding issue in `.close()` when calling `XCloseDisplay()` (#251) - Linux: minor optimization when checking for a X extension status (#251) -- :heart: contributors: @ +- :heart: contributors: @kianmeng, @shravanasati, @mgorny ## 9.0.2 (2024-09-01) - added support for Python 3.13 diff --git a/src/mss/__init__.py b/src/mss/__init__.py index 428819ca..29798f60 100644 --- a/src/mss/__init__.py +++ b/src/mss/__init__.py @@ -11,7 +11,7 @@ from mss.exception import ScreenShotError from mss.factory import mss -__version__ = "10.0.0" +__version__ = "10.0.1" __author__ = "Mickaël Schoentgen" __date__ = "2013-2024" __copyright__ = f""" From d060947ea5412aa8cf9f8d69d5b9bab6cacfbebe Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 15 Nov 2024 12:35:55 +0000 Subject: [PATCH 220/280] build(deps): bump ruff from 0.7.3 to 0.7.4 Bumps [ruff](https://github.com/astral-sh/ruff) from 0.7.3 to 0.7.4. - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/0.7.3...0.7.4) --- updated-dependencies: - dependency-name: ruff dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 1f5fc2d1..16f228b4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -73,7 +73,7 @@ mss = "mss.__main__:main" dev = [ "build==1.2.2.post1", "mypy==1.13.0", - "ruff==0.7.3", + "ruff==0.7.4", "twine==5.1.1", ] docs = [ From 1f0de269b3bd05452c3e081ea9f3709421abd77a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 20 Nov 2024 13:00:47 +0000 Subject: [PATCH 221/280] build(deps): bump pytest-rerunfailures from 14.0.0 to 15.0 Bumps [pytest-rerunfailures](https://github.com/pytest-dev/pytest-rerunfailures) from 14.0.0 to 15.0. - [Changelog](https://github.com/pytest-dev/pytest-rerunfailures/blob/master/CHANGES.rst) - [Commits](https://github.com/pytest-dev/pytest-rerunfailures/compare/14.0...15.0) --- updated-dependencies: - dependency-name: pytest-rerunfailures dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 16f228b4..f6044914 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -84,7 +84,7 @@ tests = [ "pillow==11.0.0 ; sys_platform == 'linux' and python_version == '3.13'", "pytest==8.3.3", "pytest-cov==6.0.0", - "pytest-rerunfailures==14.0.0", + "pytest-rerunfailures==15.0", "pyvirtualdisplay==3.0 ; sys_platform == 'linux'", ] From 01805665de7f2b6a6f72d0edee7f2e3b9e6a3bd2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 22 Nov 2024 12:58:30 +0000 Subject: [PATCH 222/280] build(deps): bump ruff from 0.7.4 to 0.8.0 Bumps [ruff](https://github.com/astral-sh/ruff) from 0.7.4 to 0.8.0. - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/0.7.4...0.8.0) --- updated-dependencies: - dependency-name: ruff dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index f6044914..8a24f538 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -73,7 +73,7 @@ mss = "mss.__main__:main" dev = [ "build==1.2.2.post1", "mypy==1.13.0", - "ruff==0.7.4", + "ruff==0.8.0", "twine==5.1.1", ] docs = [ From 909c0e2805c37960bdc5027a2a768d2c3e613de8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 29 Nov 2024 12:15:02 +0000 Subject: [PATCH 223/280] build(deps): bump ruff from 0.8.0 to 0.8.1 Bumps [ruff](https://github.com/astral-sh/ruff) from 0.8.0 to 0.8.1. - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/0.8.0...0.8.1) --- updated-dependencies: - dependency-name: ruff dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 8a24f538..75fef4ba 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -73,7 +73,7 @@ mss = "mss.__main__:main" dev = [ "build==1.2.2.post1", "mypy==1.13.0", - "ruff==0.8.0", + "ruff==0.8.1", "twine==5.1.1", ] docs = [ From 5a3f3359b5b6d0e1d4c4ac0f9e12145f380dc448 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Dec 2024 13:06:34 +0000 Subject: [PATCH 224/280] build(deps): bump pytest from 8.3.3 to 8.3.4 Bumps [pytest](https://github.com/pytest-dev/pytest) from 8.3.3 to 8.3.4. - [Release notes](https://github.com/pytest-dev/pytest/releases) - [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest/compare/8.3.3...8.3.4) --- updated-dependencies: - dependency-name: pytest dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 75fef4ba..d12df6e9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -82,7 +82,7 @@ docs = [ tests = [ "numpy==2.1.3 ; sys_platform == 'linux' and python_version == '3.13'", "pillow==11.0.0 ; sys_platform == 'linux' and python_version == '3.13'", - "pytest==8.3.3", + "pytest==8.3.4", "pytest-cov==6.0.0", "pytest-rerunfailures==15.0", "pyvirtualdisplay==3.0 ; sys_platform == 'linux'", From 60b94a4303731ed8cea4fcbaf9616000fb9bfa69 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Dec 2024 13:06:40 +0000 Subject: [PATCH 225/280] build(deps): bump twine from 5.1.1 to 6.0.1 Bumps [twine](https://github.com/pypa/twine) from 5.1.1 to 6.0.1. - [Release notes](https://github.com/pypa/twine/releases) - [Changelog](https://github.com/pypa/twine/blob/main/docs/changelog.rst) - [Commits](https://github.com/pypa/twine/compare/v5.1.1...6.0.1) --- updated-dependencies: - dependency-name: twine dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index d12df6e9..3f82b39e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -74,7 +74,7 @@ dev = [ "build==1.2.2.post1", "mypy==1.13.0", "ruff==0.8.1", - "twine==5.1.1", + "twine==6.0.1", ] docs = [ "sphinx==8.1.3", From 1537ad68b20fdc1ca00c0a3fdedabf4303277301 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 6 Dec 2024 12:36:59 +0000 Subject: [PATCH 226/280] build(deps): bump ruff from 0.8.1 to 0.8.2 Bumps [ruff](https://github.com/astral-sh/ruff) from 0.8.1 to 0.8.2. - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/0.8.1...0.8.2) --- updated-dependencies: - dependency-name: ruff dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 3f82b39e..6f9a0ef2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -73,7 +73,7 @@ mss = "mss.__main__:main" dev = [ "build==1.2.2.post1", "mypy==1.13.0", - "ruff==0.8.1", + "ruff==0.8.2", "twine==6.0.1", ] docs = [ From 1a88ff5f6042e321b2de9389636fe13da75d9a2c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 Dec 2024 12:43:13 +0000 Subject: [PATCH 227/280] build(deps): bump numpy from 2.1.3 to 2.2.0 Bumps [numpy](https://github.com/numpy/numpy) from 2.1.3 to 2.2.0. - [Release notes](https://github.com/numpy/numpy/releases) - [Changelog](https://github.com/numpy/numpy/blob/main/doc/RELEASE_WALKTHROUGH.rst) - [Commits](https://github.com/numpy/numpy/compare/v2.1.3...v2.2.0) --- updated-dependencies: - dependency-name: numpy dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 6f9a0ef2..90e3ebb2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -80,7 +80,7 @@ docs = [ "sphinx==8.1.3", ] tests = [ - "numpy==2.1.3 ; sys_platform == 'linux' and python_version == '3.13'", + "numpy==2.2.0 ; sys_platform == 'linux' and python_version == '3.13'", "pillow==11.0.0 ; sys_platform == 'linux' and python_version == '3.13'", "pytest==8.3.4", "pytest-cov==6.0.0", From 189d74b79468ccc271a1178bcb0a59e3cad136d7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 13 Dec 2024 12:29:26 +0000 Subject: [PATCH 228/280] build(deps): bump ruff from 0.8.2 to 0.8.3 Bumps [ruff](https://github.com/astral-sh/ruff) from 0.8.2 to 0.8.3. - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/0.8.2...0.8.3) --- updated-dependencies: - dependency-name: ruff dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 90e3ebb2..88d9e1e3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -73,7 +73,7 @@ mss = "mss.__main__:main" dev = [ "build==1.2.2.post1", "mypy==1.13.0", - "ruff==0.8.2", + "ruff==0.8.3", "twine==6.0.1", ] docs = [ From 01564bed2412470bebc9fa1e11887683df4fd55d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 20 Dec 2024 12:43:22 +0000 Subject: [PATCH 229/280] build(deps): bump ruff from 0.8.3 to 0.8.4 Bumps [ruff](https://github.com/astral-sh/ruff) from 0.8.3 to 0.8.4. - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/0.8.3...0.8.4) --- updated-dependencies: - dependency-name: ruff dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 88d9e1e3..6ab23cb8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -73,7 +73,7 @@ mss = "mss.__main__:main" dev = [ "build==1.2.2.post1", "mypy==1.13.0", - "ruff==0.8.3", + "ruff==0.8.4", "twine==6.0.1", ] docs = [ From a7b413c5ea347cc024c8cab54865196ebd8a4041 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Dec 2024 12:39:08 +0000 Subject: [PATCH 230/280] build(deps): bump mypy from 1.13.0 to 1.14.0 Bumps [mypy](https://github.com/python/mypy) from 1.13.0 to 1.14.0. - [Changelog](https://github.com/python/mypy/blob/master/CHANGELOG.md) - [Commits](https://github.com/python/mypy/compare/v1.13.0...v1.14.0) --- updated-dependencies: - dependency-name: mypy dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 6ab23cb8..39be579a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -72,7 +72,7 @@ mss = "mss.__main__:main" [project.optional-dependencies] dev = [ "build==1.2.2.post1", - "mypy==1.13.0", + "mypy==1.14.0", "ruff==0.8.4", "twine==6.0.1", ] From c7dc21d3bd2996ed1b059196d9b3dee0e76352ef Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Dec 2024 12:39:18 +0000 Subject: [PATCH 231/280] build(deps): bump numpy from 2.2.0 to 2.2.1 Bumps [numpy](https://github.com/numpy/numpy) from 2.2.0 to 2.2.1. - [Release notes](https://github.com/numpy/numpy/releases) - [Changelog](https://github.com/numpy/numpy/blob/main/doc/RELEASE_WALKTHROUGH.rst) - [Commits](https://github.com/numpy/numpy/compare/v2.2.0...v2.2.1) --- updated-dependencies: - dependency-name: numpy dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 39be579a..4ded615d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -80,7 +80,7 @@ docs = [ "sphinx==8.1.3", ] tests = [ - "numpy==2.2.0 ; sys_platform == 'linux' and python_version == '3.13'", + "numpy==2.2.1 ; sys_platform == 'linux' and python_version == '3.13'", "pillow==11.0.0 ; sys_platform == 'linux' and python_version == '3.13'", "pytest==8.3.4", "pytest-cov==6.0.0", From a5efc6b4c06203e2a3a27fd1b75f139ebcb74a08 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 31 Dec 2024 12:39:57 +0000 Subject: [PATCH 232/280] build(deps): bump mypy from 1.14.0 to 1.14.1 Bumps [mypy](https://github.com/python/mypy) from 1.14.0 to 1.14.1. - [Changelog](https://github.com/python/mypy/blob/master/CHANGELOG.md) - [Commits](https://github.com/python/mypy/compare/v1.14.0...v1.14.1) --- updated-dependencies: - dependency-name: mypy dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 4ded615d..65a4b469 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -72,7 +72,7 @@ mss = "mss.__main__:main" [project.optional-dependencies] dev = [ "build==1.2.2.post1", - "mypy==1.14.0", + "mypy==1.14.1", "ruff==0.8.4", "twine==6.0.1", ] From 4a8ae5db4d9a1319372d6956b2f8976c379557e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Wed, 1 Jan 2025 10:49:21 +0100 Subject: [PATCH 233/280] chore: update dates --- LICENSE.txt | 2 +- src/mss/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/LICENSE.txt b/LICENSE.txt index bdcbc505..0b055a04 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,5 +1,5 @@ MIT License -Copyright (c) 2013-2024, Mickaël 'Tiger-222' Schoentgen +Copyright (c) 2013-2025, Mickaël 'Tiger-222' Schoentgen Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: diff --git a/src/mss/__init__.py b/src/mss/__init__.py index 29798f60..f0282e4f 100644 --- a/src/mss/__init__.py +++ b/src/mss/__init__.py @@ -13,7 +13,7 @@ __version__ = "10.0.1" __author__ = "Mickaël Schoentgen" -__date__ = "2013-2024" +__date__ = "2013-2025" __copyright__ = f""" Copyright (c) {__date__}, {__author__} From 9889b7bcbb75266c120575e54b36a7395f67099a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 2 Jan 2025 12:50:54 +0000 Subject: [PATCH 234/280] build(deps): bump ruff from 0.8.4 to 0.8.5 Bumps [ruff](https://github.com/astral-sh/ruff) from 0.8.4 to 0.8.5. - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/0.8.4...0.8.5) --- updated-dependencies: - dependency-name: ruff dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 65a4b469..a70d09a7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -73,7 +73,7 @@ mss = "mss.__main__:main" dev = [ "build==1.2.2.post1", "mypy==1.14.1", - "ruff==0.8.4", + "ruff==0.8.5", "twine==6.0.1", ] docs = [ From 3c988a9f15a7a881e49d97da67e0590ffd54c57e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 2 Jan 2025 12:51:03 +0000 Subject: [PATCH 235/280] build(deps): bump pillow from 11.0.0 to 11.1.0 Bumps [pillow](https://github.com/python-pillow/Pillow) from 11.0.0 to 11.1.0. - [Release notes](https://github.com/python-pillow/Pillow/releases) - [Changelog](https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst) - [Commits](https://github.com/python-pillow/Pillow/compare/11.0.0...11.1.0) --- updated-dependencies: - dependency-name: pillow dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index a70d09a7..5e02e483 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -81,7 +81,7 @@ docs = [ ] tests = [ "numpy==2.2.1 ; sys_platform == 'linux' and python_version == '3.13'", - "pillow==11.0.0 ; sys_platform == 'linux' and python_version == '3.13'", + "pillow==11.1.0 ; sys_platform == 'linux' and python_version == '3.13'", "pytest==8.3.4", "pytest-cov==6.0.0", "pytest-rerunfailures==15.0", From 62f871bb1894fcddd3e72d9a1d79e808674737e3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 Jan 2025 12:14:08 +0000 Subject: [PATCH 236/280] build(deps): bump ruff from 0.8.5 to 0.8.6 Bumps [ruff](https://github.com/astral-sh/ruff) from 0.8.5 to 0.8.6. - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/0.8.5...0.8.6) --- updated-dependencies: - dependency-name: ruff dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 5e02e483..1f4827dd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -73,7 +73,7 @@ mss = "mss.__main__:main" dev = [ "build==1.2.2.post1", "mypy==1.14.1", - "ruff==0.8.5", + "ruff==0.8.6", "twine==6.0.1", ] docs = [ From 4a772f632d45507220925e3c9b6768ea5eea92c4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 10 Jan 2025 12:24:14 +0000 Subject: [PATCH 237/280] build(deps): bump ruff from 0.8.6 to 0.9.0 Bumps [ruff](https://github.com/astral-sh/ruff) from 0.8.6 to 0.9.0. - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/0.8.6...0.9.0) --- updated-dependencies: - dependency-name: ruff dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 1f4827dd..0640ae93 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -73,7 +73,7 @@ mss = "mss.__main__:main" dev = [ "build==1.2.2.post1", "mypy==1.14.1", - "ruff==0.8.6", + "ruff==0.9.0", "twine==6.0.1", ] docs = [ From ef6334d3832c03025df9c90124804a38000e3af9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Jan 2025 12:35:39 +0000 Subject: [PATCH 238/280] build(deps): bump ruff from 0.9.0 to 0.9.1 Bumps [ruff](https://github.com/astral-sh/ruff) from 0.9.0 to 0.9.1. - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/0.9.0...0.9.1) --- updated-dependencies: - dependency-name: ruff dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 0640ae93..83e8f121 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -73,7 +73,7 @@ mss = "mss.__main__:main" dev = [ "build==1.2.2.post1", "mypy==1.14.1", - "ruff==0.9.0", + "ruff==0.9.1", "twine==6.0.1", ] docs = [ From 6ab7321ab60f6baaaa4e1711253acab1a2499760 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 17 Jan 2025 12:57:01 +0000 Subject: [PATCH 239/280] build(deps): bump ruff from 0.9.1 to 0.9.2 Bumps [ruff](https://github.com/astral-sh/ruff) from 0.9.1 to 0.9.2. - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/0.9.1...0.9.2) --- updated-dependencies: - dependency-name: ruff dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 83e8f121..dcb01cca 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -73,7 +73,7 @@ mss = "mss.__main__:main" dev = [ "build==1.2.2.post1", "mypy==1.14.1", - "ruff==0.9.1", + "ruff==0.9.2", "twine==6.0.1", ] docs = [ From 4888a8bd8ad555c66339411ffcdf3b025891757f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 20 Jan 2025 12:16:37 +0000 Subject: [PATCH 240/280] build(deps): bump numpy from 2.2.1 to 2.2.2 Bumps [numpy](https://github.com/numpy/numpy) from 2.2.1 to 2.2.2. - [Release notes](https://github.com/numpy/numpy/releases) - [Changelog](https://github.com/numpy/numpy/blob/main/doc/RELEASE_WALKTHROUGH.rst) - [Commits](https://github.com/numpy/numpy/compare/v2.2.1...v2.2.2) --- updated-dependencies: - dependency-name: numpy dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index dcb01cca..0396ff19 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -80,7 +80,7 @@ docs = [ "sphinx==8.1.3", ] tests = [ - "numpy==2.2.1 ; sys_platform == 'linux' and python_version == '3.13'", + "numpy==2.2.2 ; sys_platform == 'linux' and python_version == '3.13'", "pillow==11.1.0 ; sys_platform == 'linux' and python_version == '3.13'", "pytest==8.3.4", "pytest-cov==6.0.0", From a17ba2a5159bf7ba8e6bb0a7a2e5cd3e86604953 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 22 Jan 2025 12:28:27 +0000 Subject: [PATCH 241/280] build(deps): bump twine from 6.0.1 to 6.1.0 Bumps [twine](https://github.com/pypa/twine) from 6.0.1 to 6.1.0. - [Release notes](https://github.com/pypa/twine/releases) - [Changelog](https://github.com/pypa/twine/blob/main/docs/changelog.rst) - [Commits](https://github.com/pypa/twine/compare/6.0.1...6.1.0) --- updated-dependencies: - dependency-name: twine dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 0396ff19..c8751bd2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -74,7 +74,7 @@ dev = [ "build==1.2.2.post1", "mypy==1.14.1", "ruff==0.9.2", - "twine==6.0.1", + "twine==6.1.0", ] docs = [ "sphinx==8.1.3", From f55a9e1f3b56805656a8b7c4226bd9592ddcf17c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 24 Jan 2025 12:37:29 +0000 Subject: [PATCH 242/280] build(deps): bump ruff from 0.9.2 to 0.9.3 Bumps [ruff](https://github.com/astral-sh/ruff) from 0.9.2 to 0.9.3. - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/0.9.2...0.9.3) --- updated-dependencies: - dependency-name: ruff dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index c8751bd2..8c58af4e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -73,7 +73,7 @@ mss = "mss.__main__:main" dev = [ "build==1.2.2.post1", "mypy==1.14.1", - "ruff==0.9.2", + "ruff==0.9.3", "twine==6.1.0", ] docs = [ From 56bcf5ffd14d0cbbb20b72ca0b3a46369da2f74f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 31 Jan 2025 12:41:13 +0000 Subject: [PATCH 243/280] build(deps): bump ruff from 0.9.3 to 0.9.4 Bumps [ruff](https://github.com/astral-sh/ruff) from 0.9.3 to 0.9.4. - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/0.9.3...0.9.4) --- updated-dependencies: - dependency-name: ruff dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 8c58af4e..4085c048 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -73,7 +73,7 @@ mss = "mss.__main__:main" dev = [ "build==1.2.2.post1", "mypy==1.14.1", - "ruff==0.9.3", + "ruff==0.9.4", "twine==6.1.0", ] docs = [ From 6a66c48b08993dc81b8bf72fbb84602a34ec88a0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 5 Feb 2025 12:47:24 +0000 Subject: [PATCH 244/280] build(deps): bump mypy from 1.14.1 to 1.15.0 Bumps [mypy](https://github.com/python/mypy) from 1.14.1 to 1.15.0. - [Changelog](https://github.com/python/mypy/blob/master/CHANGELOG.md) - [Commits](https://github.com/python/mypy/compare/v1.14.1...v1.15.0) --- updated-dependencies: - dependency-name: mypy dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 4085c048..839977e2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -72,7 +72,7 @@ mss = "mss.__main__:main" [project.optional-dependencies] dev = [ "build==1.2.2.post1", - "mypy==1.14.1", + "mypy==1.15.0", "ruff==0.9.4", "twine==6.1.0", ] From d0447864ed64951755fbb2ce2a01b4a23f7a68ad Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 7 Feb 2025 12:58:28 +0000 Subject: [PATCH 245/280] build(deps): bump ruff from 0.9.4 to 0.9.5 Bumps [ruff](https://github.com/astral-sh/ruff) from 0.9.4 to 0.9.5. - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/0.9.4...0.9.5) --- updated-dependencies: - dependency-name: ruff dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 839977e2..1d072d89 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -73,7 +73,7 @@ mss = "mss.__main__:main" dev = [ "build==1.2.2.post1", "mypy==1.15.0", - "ruff==0.9.4", + "ruff==0.9.5", "twine==6.1.0", ] docs = [ From a47dd9b98bd678928e42f94f70bee9077088890f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 11 Feb 2025 12:10:32 +0000 Subject: [PATCH 246/280] build(deps): bump ruff from 0.9.5 to 0.9.6 Bumps [ruff](https://github.com/astral-sh/ruff) from 0.9.5 to 0.9.6. - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/0.9.5...0.9.6) --- updated-dependencies: - dependency-name: ruff dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 1d072d89..2b1b2e49 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -73,7 +73,7 @@ mss = "mss.__main__:main" dev = [ "build==1.2.2.post1", "mypy==1.15.0", - "ruff==0.9.5", + "ruff==0.9.6", "twine==6.1.0", ] docs = [ From 74cf90e0cad65284aafa1de4441c5a78cc91f436 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 14 Feb 2025 12:25:21 +0000 Subject: [PATCH 247/280] build(deps): bump numpy from 2.2.2 to 2.2.3 Bumps [numpy](https://github.com/numpy/numpy) from 2.2.2 to 2.2.3. - [Release notes](https://github.com/numpy/numpy/releases) - [Changelog](https://github.com/numpy/numpy/blob/main/doc/RELEASE_WALKTHROUGH.rst) - [Commits](https://github.com/numpy/numpy/compare/v2.2.2...v2.2.3) --- updated-dependencies: - dependency-name: numpy dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 2b1b2e49..acf3d19c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -80,7 +80,7 @@ docs = [ "sphinx==8.1.3", ] tests = [ - "numpy==2.2.2 ; sys_platform == 'linux' and python_version == '3.13'", + "numpy==2.2.3 ; sys_platform == 'linux' and python_version == '3.13'", "pillow==11.1.0 ; sys_platform == 'linux' and python_version == '3.13'", "pytest==8.3.4", "pytest-cov==6.0.0", From bfa0628ea43f2b881fb2535143fbcf9f731f186d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 19 Feb 2025 13:24:30 +0000 Subject: [PATCH 248/280] build(deps): bump sphinx from 8.1.3 to 8.2.0 Bumps [sphinx](https://github.com/sphinx-doc/sphinx) from 8.1.3 to 8.2.0. - [Release notes](https://github.com/sphinx-doc/sphinx/releases) - [Changelog](https://github.com/sphinx-doc/sphinx/blob/master/CHANGES.rst) - [Commits](https://github.com/sphinx-doc/sphinx/compare/v8.1.3...v8.2.0) --- updated-dependencies: - dependency-name: sphinx dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index acf3d19c..cf17491a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -77,7 +77,7 @@ dev = [ "twine==6.1.0", ] docs = [ - "sphinx==8.1.3", + "sphinx==8.2.0", ] tests = [ "numpy==2.2.3 ; sys_platform == 'linux' and python_version == '3.13'", From 8248043fd2fb0c80ee52a0aaee8ff946494da3dc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 21 Feb 2025 12:13:14 +0000 Subject: [PATCH 249/280] build(deps): bump ruff from 0.9.6 to 0.9.7 Bumps [ruff](https://github.com/astral-sh/ruff) from 0.9.6 to 0.9.7. - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/0.9.6...0.9.7) --- updated-dependencies: - dependency-name: ruff dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index cf17491a..a7d555d5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -73,7 +73,7 @@ mss = "mss.__main__:main" dev = [ "build==1.2.2.post1", "mypy==1.15.0", - "ruff==0.9.6", + "ruff==0.9.7", "twine==6.1.0", ] docs = [ From 67b0a22d31f41f26353a160764f66c890515ffbc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Feb 2025 15:29:29 +0000 Subject: [PATCH 250/280] build(deps): bump sphinx from 8.2.0 to 8.2.1 Bumps [sphinx](https://github.com/sphinx-doc/sphinx) from 8.2.0 to 8.2.1. - [Release notes](https://github.com/sphinx-doc/sphinx/releases) - [Changelog](https://github.com/sphinx-doc/sphinx/blob/v8.2.1/CHANGES.rst) - [Commits](https://github.com/sphinx-doc/sphinx/compare/v8.2.0...v8.2.1) --- updated-dependencies: - dependency-name: sphinx dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index a7d555d5..cfbf3ed6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -77,7 +77,7 @@ dev = [ "twine==6.1.0", ] docs = [ - "sphinx==8.2.0", + "sphinx==8.2.1", ] tests = [ "numpy==2.2.3 ; sys_platform == 'linux' and python_version == '3.13'", From 9f8c01e4af57ed4b2b67e64c23bc5b4ad1b350ee Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 28 Feb 2025 13:01:47 +0000 Subject: [PATCH 251/280] build(deps): bump ruff from 0.9.7 to 0.9.9 Bumps [ruff](https://github.com/astral-sh/ruff) from 0.9.7 to 0.9.9. - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/0.9.7...0.9.9) --- updated-dependencies: - dependency-name: ruff dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index cfbf3ed6..4ee54f58 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -73,7 +73,7 @@ mss = "mss.__main__:main" dev = [ "build==1.2.2.post1", "mypy==1.15.0", - "ruff==0.9.7", + "ruff==0.9.9", "twine==6.1.0", ] docs = [ From 6a5c7910db0ed2edd8d76a72d9e96d5a975f6cb5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 Mar 2025 12:51:07 +0000 Subject: [PATCH 252/280] build(deps): bump sphinx from 8.2.1 to 8.2.3 Bumps [sphinx](https://github.com/sphinx-doc/sphinx) from 8.2.1 to 8.2.3. - [Release notes](https://github.com/sphinx-doc/sphinx/releases) - [Changelog](https://github.com/sphinx-doc/sphinx/blob/master/CHANGES.rst) - [Commits](https://github.com/sphinx-doc/sphinx/compare/v8.2.1...v8.2.3) --- updated-dependencies: - dependency-name: sphinx dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 4ee54f58..c7edf3e4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -77,7 +77,7 @@ dev = [ "twine==6.1.0", ] docs = [ - "sphinx==8.2.1", + "sphinx==8.2.3", ] tests = [ "numpy==2.2.3 ; sys_platform == 'linux' and python_version == '3.13'", From 09955abf00e897a07b919fdcf8cd62e0bcb6c74d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 Mar 2025 12:51:13 +0000 Subject: [PATCH 253/280] build(deps): bump pytest from 8.3.4 to 8.3.5 Bumps [pytest](https://github.com/pytest-dev/pytest) from 8.3.4 to 8.3.5. - [Release notes](https://github.com/pytest-dev/pytest/releases) - [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest/compare/8.3.4...8.3.5) --- updated-dependencies: - dependency-name: pytest dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index c7edf3e4..59ba5c35 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -82,7 +82,7 @@ docs = [ tests = [ "numpy==2.2.3 ; sys_platform == 'linux' and python_version == '3.13'", "pillow==11.1.0 ; sys_platform == 'linux' and python_version == '3.13'", - "pytest==8.3.4", + "pytest==8.3.5", "pytest-cov==6.0.0", "pytest-rerunfailures==15.0", "pyvirtualdisplay==3.0 ; sys_platform == 'linux'", From 216486cb90907b22b5c14ce5596c9a0ea1f99ba2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 10 Mar 2025 12:48:13 +0000 Subject: [PATCH 254/280] build(deps): bump ruff from 0.9.9 to 0.9.10 Bumps [ruff](https://github.com/astral-sh/ruff) from 0.9.9 to 0.9.10. - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/0.9.9...0.9.10) --- updated-dependencies: - dependency-name: ruff dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 59ba5c35..0cc7943e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -73,7 +73,7 @@ mss = "mss.__main__:main" dev = [ "build==1.2.2.post1", "mypy==1.15.0", - "ruff==0.9.9", + "ruff==0.9.10", "twine==6.1.0", ] docs = [ From a5f35709f6d4c671cea955e84f728379190d23d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Tue, 11 Mar 2025 11:24:45 +0100 Subject: [PATCH 255/280] fix(ci): documentation configuration --- .readthedocs.yml | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/.readthedocs.yml b/.readthedocs.yml index 0a201cf2..c62360fd 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -1,5 +1,22 @@ -# http://read-the-docs.readthedocs.io/en/latest/yaml-config.html +version: 2 + +build: + os: ubuntu-24.04 + tools: + python: "3.13" + +sphinx: + configuration: docs/source/conf.py + fail_on_warning: true + +formats: + - htmlzip + - epub + - pdf -# Use that Python version to build the documentation python: - version: 3 + install: + - method: pip + path: . + extra_requirements: + - docs From 3792fd6f794aedc708afdeb40a44d4be04598dbc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Tue, 11 Mar 2025 11:47:19 +0100 Subject: [PATCH 256/280] feat: modernize the documentation --- .gitignore | 2 +- docs/icon.png | Bin 0 -> 11004 bytes docs/source/conf.py | 22 ++++++++++++++++++++-- docs/source/developers.rst | 1 + docs/source/usage.rst | 8 ++++---- pyproject.toml | 3 +++ 6 files changed, 29 insertions(+), 7 deletions(-) create mode 100644 docs/icon.png diff --git a/.gitignore b/.gitignore index 9605fbde..79426812 100644 --- a/.gitignore +++ b/.gitignore @@ -4,7 +4,7 @@ .DS_Store *.orig *.jpg -*.png +/*.png *.png.old *.pickle *.pyc diff --git a/docs/icon.png b/docs/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..ac15301957ae395a5bbff6d08d4cb97a8f883aee GIT binary patch literal 11004 zcmaia^;cBg`}UckhM~J*R1~B^q+{rmZV(WV2NY05x@PE*kOmQk4yC(?4wdc(LAqmT zzI@jE2fRO=eb(A%?S02}pL6YXojY7hLx~7V4+Q{#NJUv*=i!d{-vz;a7(1oj`~m=u zP8E3>J&)NvU;NM3|0VuQtT9eDdu3MBHt_zj*sO_7sY3?{71O; z$R%X#^X3beG#A>HCHb0e0=}7_2Ag)SFMfd&b)VK#to~q zq{`x&)*UDB!R6jT+tU5QdfPoOl|$mU#=_NGu<6s*w*-K&=L{JA%Dfi%7@y1y%&-0b zr>H>7O-r|>)P`?4Tl{|fG^X>|t?2z{;1ZaEb1|-~dCQwQgu%S4nN5z3UDhqCotEhy#zn)JB2KdCq z3376BKHlA2UZ$p|e(>K?v$$H~Y%o*@Jg&A1EPnp@(d2)Bck8oob0Yz}T7AtT{CdPr z(2|eVqbVy3G?*?rcD9)llT@~5u8jqlw&6baze3_M^X2E|wWHC1S;HqBzw5ar0s6Xu zo`kM0`TC6v@8yf5zp=^5c)Gf}hdUPcvq&Om;bLT|(E$ws@TuCwacDydBiiz7J>bA+ zL;Nt1hts0|GTmJ}Q#mC2gT=Ke7n5179dCh5kiU*Mr^gr9u1;&R4CzY*~| zJ$zPhQB!_5mAP6JtIu2Q=G}MOkz9|poMcK8;^#ik?&nx?ABFOO`|gJ{h>GvH7rZ#v z^1A_j{MBC0!ps|C@%^mrj#9IinueyKCjRhwebp$rJHRNaQ=s)cD=~*PrOhXTZE0cU zBYHf6h?<((Dly(6TpS=~ks{T3{z(>6l3r7AG5ug7%R@{tfNjawYqn>9-Xy6Wyv)as zUb+1ivHHaHoI$^!2IbWH|en9Kx%cj4IM~2o{*Za(>XlJnniTc%BdCaGWXN9Guv!|3SjE!f`)91I!4|Amt zIS&KMDJJ7r6N6iCm{kwp{j~A%x(B9>$(#@Q5*G?uHT-)L|5XW#{dpWF*o?i+N`$ipoP~%oE)5WmocT%|MPi1!@?*?h4Rj8ec7?L5+Bg2 z;GrzbomwBHm=}Me_otG?tee#!Yumc2DE=XPbG65=`4@+>Z0)1Z=seaI!&}_4z!uMe zwIHT`$in7>O+CE?M@aXW2_@?udC-A%R(xynqI(Mf!|Wd~RX8KU#DReURVogrx0JjV z$d30B0R=T8pG}~|M~n@7i3!TGhF3z5vTS_Y%k6>kBubX6cRE=FB`hA<*wEcDF!5`= z{qhhX_L}@izIZ*OTxT6sSF#Tj4&!$jKL**j~`Tz zh04vf>o#a+UtXT%feIQ~j<5FriT46jcZMDFd`HQDth2o0OosZFAW7W;N*Wr)S4X2t zAy8ixM7vlf^>%m8NX@4X^KhsZxcwb~V!aq{OFZw^c|bEeJ0Ogb_h%pu>Fn%!km7rC ziKrR^w$n3QI2Q-ZWFXmeZ7?eTp}>QKFQJK#4Ug1h3-DuHP9k9Z{Pv<0tf)cvh6dfx z^QRE@c2`+GfLu>zN>xT?WAv}ys&DsRP!J6Edq~{U$APv-G4!kBzsK}`BgM!>;E@68 z!=^Ymo-!=FR6d2J!l_(ZMtWWHGOlG4bdN;4DR-_*O^>`Jh8HFxLjHA))(uOS@s@Mn z)K$lnonXY3G2XuB_qBY{T7@LMF1aK_m&CzJt``2)=$PpN7lM zJ%_t=)$1QLpY9+f#K#A*xR+i!HtZVh)gj1>IF3l%qfp#nWn=3c{H|}Qs)G9&3@=f9 zh7NJ~mVt~ObfcxCBV%EIgFTW$xKMZ6zoxClPaB6$Pm0Fqt6@hT1Y-2qSmj@p)WW2?qt9B2`ZT=mBwiYqK-w{1s!#Du@e%XcX#1FRET*71YtQ#!)YS8DRqCT zw9<|zh41wfQMc$t!0wQl_&rka0I6HBgk;SD>BQ>>dlz<1KKfZA*Li4&@nIYtn0jnCZh{*))&Lk=CfH!!H)WIIQ!lop*epI6a!Db5XPWc{O-js zu6x_S51)m~A-|J9!YkuV|G^8?0;<1oRWluH@jdUDyAB$HaqR8yqp9Bp_Ojg1p(Yu% zKPul3-P|NwqP^+k<8P>{Y=p5V@8}66sSA}8KPJUFb6{6DODLrG?!7tR-rWx1G5cq0 zn?g00gDo=;ij9xQ(%G>X6kr49y>|AN+igFntKd|1gX!q(QJuH4Pn0CX^>!aE^j(e9 z*p*h+lQ0sWnW$LVE284_kHXN6F~SvPGos7GEf$5|u)wjb~;%6Uam0(QG7 zv5)NmE=Ya31*C86SErCNpYtO;?Q|Z)%b&70gq7)e{5g!4VFVjUiUn1Ri zV2u{eq4yP(i%F<|P5Liw_&>GM@H?_k^el(ZKM93-M9JpiIfk|>QL3~0o7bYP?aF_RQE8SsWvG63i+$Tx#$=@*iI_QS-# zH!9@g&rWd=BFrzmZ%^QAVy(>46#oq;FQ-r>XL?4c=Rtdg9FA7lopQ#0l&; z+Nav?NGbzplE(@(ds%?&as^2Za4Z<;9o_^pDkGXwSx>F*g?!EBm~`F z8W$btAeqHU%)T(2ISzN@aRsrldEI6vj^c`ku$cHL_ABh4gAlFe9*e@Cpa8|4~40 zgFU$be%`OB0Q|P*%vYi3G4~<4x)RSSC?Lsy##SNQ3cSr*M~%FL7mV@hKPw$d7mvBB zq#gLBD#G>Zmf%$VyGHLNmY&LEk>~D*(u<! zOqn)7b<44-*scAMU(7 z88F`XpJNBFjnCh27J=5i(J$hMNuFd#dJ{{XWHOTji2WR+)G8O3kLNd@^(S4967o?& znsD+dByCIEXc*C=*Ur%J^f=o| zbu!u(8-2p~oUj0Q(HFCDF-U4vk@7Y;1MVkzA;PK}BjUX3qyTK5iQxkLXldUahL5|o z)`TIfdJMw!PH4INOPAQN4o%CKFuh_5-wk(-jDV(tMl24_Uvxp^$0`@q++g_cXW2=o z8`7D`kREE0@;2(vcHjEEao*@5;|||EEk=h@-j_k%5goWRnZth{~@``fp3j zV;N%7yKbq@A7F_Ep-Mriz~2bTsHi;BbK>mW*fhSVenw3Sf)#_oXz_z|+|UJ(LU8wh zlQFGH!CfFiBkAH8IdF>c?aGGs^eB$3zYj`F!|KCUl!}q<;@4KZD!wj_(Br-23{eg(tRxlMn1y!%?W z#7qZ(BU!O4!lom_RGQ7dx&>|-MJ+3j+c%7EefR`O`Y>`v*yJ8R&RcERv>1BAdE~ps z<%93&@?X>xmPT9T)@RX?#R|Bht(c#ZfIud`&a0xItLzF3E14#x1x+N3Cy@>otlNYr zo$CoJ+GM_YZ&JV{6=V+KL!{Hgcu|Fgc7K7`l$7a{z9%u8MfX<}nHSin=jW9Og*Ty4 zi}7D0V0dZJR0S-@Fo$p`1fSINwq&6}g!g6`>+T7au8~=oEY%fXq1rbG>IV1xNADc> zn1&~3@y}C5`LdhrmDR0WyY)*bVR6l&-}K?Sq=2jpeAoMY*H{Ra>Al}z|Lsrh9RNq3 zOPmhNd3kNd_}^Gx{1a~Lro!37#Ud7yV zJu&L?>?0V>CVi>m>*gjz&|FXuN<=m=PeenUpIk$%iGZGMy#pQ`_5by3>2hP}hEnRR zNHfr}r)Nf7&vTDyFE%qdCZJyiWZUP~Q7tIk2`R3$u3PA7^jqX7vHok#7v^N_W=?GL z)^;_n-K{(E7fsfR7uE)&mYWVaG$Y_;!my0)pBZfkJM*-GF}`?lvDUN-nDnEukrB3F z%p>t9-^x_9*w~A~nVBHP-p~s3d9{B*_l)nflwz0_h6>^sSe~{w-=5Dg;332hnsqhF zgo{9hdYekoZP#m901M)k+0CL$f;a2>f{4KK{#Jk5I=o0LR-#MBSp-P@C)S`8|e zytV{QCAcXHeZfOM985xcoCK7F6ZPT<7U0Rf_?RV(HZIC6%R4Sver(~pw7=h9;Nxl+ z{-EpQS>aC(lE0&Fhx0tWGH$!ehCAxYpnyzA=u@E-xhLu+`c(|Y3W0)LH3QS2grG_U zw}&N)#A~GL(S%H+`43$=ms{8ARu{aKSGLq#3W6@5s*V_bI{@Sq!<#mh-gc;Mm`>}2 zSl6|R2)24AFG!Um$yVEW)w7{by&ocwsLEtOMHJm;s^+P2K@J!+4DhQ~ny z$nI|_XzSPU+gdkl4=-`$24GZD;iq1=FP+YqlNks%iFL$8Z|`{U+=wWOr)sE6=mR+EmMWerNZ zTyYgQ%P807g3|ukQMz>uE_lTct=ZGN`w~iyj{kmvz^`~su{DRQt z?b{rS1FP>#)6)>CHjZzP|MH6Be>ypN<6}kcdgaCp`i=~N;ULoJ=}|2$Ob*H|rwm}q zCYY;o=`^x2aPz$ANYsE9-ZDQr9@LXbR!z?>C@5H=idt2yiAVf?K%BSQsa+JJgVP1L zL>r5E*p=NI>k>Bm_>^faFMI|_Wk}!Oq2rfs*{TPwwKJ33Y3_c}XuJNFanXIEF22jy z>^yubQ0RUFJ^o6im_<31lWQ0`gZ<%V#Z{t??)dm*nD4jkgi?`1E;uZrsLt6El^HcG zE`9oDc$=aD^R?2~w^TuoK=I+|fgJcgVYdE)T`N+QXq0P9mhtJDZ$EBI)sU=$0#f>^ zA6qdEXD=hWL@N%0&%C|Tqb10sf7SnToo1!=TAHY+|JmgAA124bgR|f4Ig@_uW$a2j zzKr@R0&6z^W%*i$o)liq4aBYavGhCrUr;l0E61$=JO0_s;iSFALV39D<=-1Arc7W4?dXi`Abjl zr!R4bFk9$Y)uDvrt7~esZ-ym`2e9j@2f(fa6mJSETzRum-i=N6>2%)28-p3aOiky< zb#aaXwlFCrobIQ2K6`LO=~6k@&5A}x|DAf z?Gv2ISxt2;r~US^h!BhH=+8~V*6zQK<{HNA2;1crrmx?T9M*lVu8~tTp)TkpsDO?T z$>SbkAqW-h1Jvvu&7t!0)`w!Gt~U27pwhMgihdf&y{hBv>`eYxhw2X{S1#6)=LX^E z_G&R5e7!Cu&ob<(ecpHOgV>HVuuLL8Np-TUKp3-l0&xq%UTMIQ;lhE@>gEG^AWLT4I02@8|* zarp}VtH?BuDY@7zQ9>hI8(mQ}pI(J)8N~jQL{PF_+-45R94te=L`Qe-reo2`M{!Ss zJv~LW44W7#)t}p`6PGKENWTgFnGLO`QrGgorFW1cfk{fnw*>29<9a@!WTR!GSEzk1 zFAlF-!?nt(h&HB~z~48o5tFg^yB4wuVbi`i_$hzE35Ne%SG)iF!?rqayXKw#$}Dz3 z7oGeK3ra_!1X^fyedR5>`z`9~`{8)_Hcavb0a3N{FzW^SONhC*8iB)56=Ebdj5(03 zwC*$d$A@%T3kgw&i#h%ep*PK+>#sNc{D@o3m%6wT^e2veKa2~bk0{uT9KU=a?7X|h zGm;c~{XS@QR3}<6!VQ>tF4sBP(~3mqX!>n1jO{$n#83g?&l*q4pI_vd&^IzgsVU23 zEiO9I(&ZQ=5AEymnjEa9y%aVJE#7{&xxW>x-a^9lT!oS2&cEyy|5-kx#8-vf)i-}X z5Iad%l0?{VN=UhNtspB0zV``6eY}`hI@Z9klA~h_IJ$P#>;KlXnGbSQ+3R2? z{SFeR*R3DPI5=vpC37P(`w00&nsI>CN^GFae_q;npTqx|Hmg3IYHnf%zt)&xjf#>F z;IiK`!*}G4LZ}moJ~yE@Gj%6vKA#j-R?N~C5h;VkG<|t41ivJGQA<~i4n4Xp!tm*a zJUM;Jz#YZXx;B)%i3%zB+oPTadxrUvUx=uqIL>toGr$T=(%esG1 zat76pm|kr7d*UKtT{!H-niv{e0__SC&4cE!p6q^yRqyzvGC8@gU^zW%YQ~SfM_5{Q zAA|{`qGha1h(kj$Q%pgnr5b$$11tNR8RtCgyk%>#W%m?xg;<2H)TwdWejIN6Gwo)e*~+WeTv&qC1gp-LfW zF|j^b3UYJuAoa)hCx42l**RyZZCL4}=r2CR_=IFGNC}2422;2llpvY0--vlJkJZ|z zWLC(jsL=hVc|s`)QxAxOIqvQqJQPOk#nU3~^tRsqZ1ap4%7F%-EzK-bBxJJ?^s-o6E^clHD+t3C*4=0sX|b(8K?$Ew zq{h9PXG5(gyr>W!R=`qR!~n}PI?P@dZ6*Sv+*8$QK>=p~zz>=MY;05xo)kds_F15L z<_$jhzW1N=_o`0?HC}&caHfi$+=O1}rVqH4!+MomX4blQX>m742?3Eh&r}0i{oE{V7?7HR@l2jR7ChIb z4Wx{lFtzu__Z7E(0mTG*l#P3Rj6fK+tabXcCXWs`^k?kX@7)wHw4<%v-#S*mgxuxjj3O3pc97x>_FX zCWuY_)|(_AC9~UW;J3l;>T_3@d1t6c(lIoY-g3d?-b48>Q8t2~NMbt8SCHsII0`by zGH*$WqiFwIF6Zq}K_W9GAIQtbNwd&ye9_c)$%PS}x7!K%(xECu{QT7*zxb~gIU{I8 zxj6n!-*C_s?$x*Z!`> zvrmCxKWMpDLAa`b(o)=EFhdwRH7ys_?pP>l6JuUH)eCsy_k<@ zUgArbj`bJ2I5q|AvXztA+w$BV!<#2z2LTzuvw`QqF3e&jB@TbS>2PT!SOgphk=);Y z#k*W!@Ct!dp}>m0iZCKmAAds{y@Oet8Dq~ouFO0Xl>)gp-j%5zX8V_Wi}$hSS6WN` z-=qY<=6sOR!;i`m7Csg!pS9m!U^pN4OC*RP%U3U}7-82|S3tw_m>^|Tz;aLu&z_DB z<^HE*&@pd#N*W@4*ZoVP@bAs~SbA`Lu4aIPHF2XI^PRvp1B%~=UXN~#n8#y}&w;@f zch=lIm@+fMEu4h;X8OBqpXWSrs;2@UKR^`PQ_YOzRx zT-+`dj$GhDRxJH%-X6j*jO5l1=H`1MiRE)LR_^Xi=iVZ9bgDV*p@#f-jyt)o2Yo^C zNX5|xLb6fyE9X!LOt@k2p5)Ck_-M#+atMTHJfd{d8(mUTUI zipC*z;l{Th#BP89C2dy|yax@RkJ1@|F26Ut@^}H|a4j%g|34EG@T? zHX2u`@$oNDSpW1gsnT79nkp$FF6ntKbKL&PILU8YYh`qAK)jLnCSkDIVq(tcvGCmWCAi2bQ;Ute^1c2cj24mq_j(&`G-kMs1BI1^Zj^rc?#U2q(dx~__O-R36B2v||IaxwIQET49e zc<*OO*8F?W7IqxJyPW|Lh&CAbl*%4FutU}wqd2-rafYl2&ze$2!lz>I3K{}-cFNi% z7M03IZHF`D9`Q4pl`mFXu{m=pA$sK;fAUN>x zfT(ibL(Kld%YP((hx^hc)IhQ2ScX1;p9zPTL{jiNv)}{~Dh4i58!fuC6hr$sQ@(vi zW-Kz>IW&XZt|g#)SlwGHmMX7Sd=QK6Z#-5e>JFjY8<(NfUi&xMOvM~(D|U`l1#Ke~ zUOl!lZB{Dh*i!%CWHz z>r6;uD)ZW(Fgcd+eNjw%q>V)-YTt)#pH7zB>uv6|i(n{W;YnMB{)8Z#<{ujvQ1qWD*s@2ci-uH zX2|cfzo%#CF<|icB8-ca8!#XYn*hTZ?bSE5o`$PXFhUgyd1A3xE-V_`>B8_9$$P0SGIZ;jA}JRG}<|FGAwRpVvzM3D+_(8>`a&RxX&oRWn2(cjn&W;j;TbYZiPI2grA4ore6cZPt8 z32$!qh1)k^_`WItFF^+N#Y-0z?M&UD`!OI0^Sc$2`IcIJk?nBYj7QJObadkytmGDC ziw^S~mj4!6n*GL&|4U(+$Bb^5X)YsjOQ4b4Yn9Z}Gayd-IS&I)bB1?zc0AuTeLb>g zXXgr-H=`Gi4ev3pl$NCvka%tx;zZl+aC4;H1x-F2t9cg*>>dg5K z;-&p)82Z7_c!g#z8HLBg2WplI*8NUeWSLa{J5=5i-|NaM=sKR)40h`Kq&PjkVB+qfg!z zl_Euil@rcxkK&jiSj*O_7@E^&+VlXcrCFf=RF|D=3O#L&9Hvk{7#Saxxaee&I@@Y( z&2W-fGsdHaKzxi_$^c;H+`g*HCN@%7BBBU#rB3>?N zRu6}XppEI@jECi{Z6K#1udDRsTAg&vxl9!cz)5ryMAb?T~R6)&E2 zKT&dOVSB>e8Jn1x3LpEe8w$Z+QWnJsJaC_zWh8uIziP&TxvqCFM~?(MfW(x^pK`Q_ zMh124;cH@pMP!~Mn}~WmduHjF-3`}i9QNyOHtwsDtMv5jUfLsT+-TI!j zqP{Bn$wW*+2rjwdIDI2KHpr@8P^G23}HO$G+Rb_+1G59G;s?5H(m`g=c$WOJiR&|`clN*I}Vkkua1 z7q_qApls&Tcnh5R;TS?=0fEZnVaov-dG}xrK%;dLS@?lQJIqNU|M3o;YxRZMXif9E*MyiLBav52nxgr(b_#BD$fuf9 z+bYqQl9CJRV@(elLw3oAWyF~(tL9tnlpO<&@=tS8!M9nxoqIXK2*uRA&U*FL)i{Wd z9{Q^hj3cWwS9t_ zwl+STmUM=T7=2+4k`;X4@WJHQfhi|x*!!nfOXcJwfc%NnNPw)pcS+k&!J)h0r2l-- zPdy<9z~FeWB0zTEFQU~mrR|9)_Ce&aA2!;+xU&)m@vZsBUwi1BY8k!0_t4b}s3>U2 Jm&#fO{2w7`#lHXm literal 0 HcmV?d00001 diff --git a/docs/source/conf.py b/docs/source/conf.py index f9d35d23..a0d9b993 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -10,10 +10,15 @@ # -- General configuration ------------------------------------------------ -extensions = ["sphinx.ext.intersphinx"] +extensions = [ + "sphinx_copybutton", + "sphinx.ext.intersphinx", + "sphinx_new_tab_link", +] templates_path = ["_templates"] source_suffix = {".rst": "restructuredtext"} master_doc = "index" +new_tab_link_show_external_link_icon = True # General information about the project. project = "Python MSS" @@ -28,7 +33,20 @@ # -- Options for HTML output ---------------------------------------------- -html_theme = "default" +html_theme = "shibuya" +html_theme_options = { + "accent_color": "lime", + "globaltoc_expand_depth": 1, + "toctree_titles_only": False, +} +html_favicon = "../icon.png" +html_context = { + "source_type": "github", + "source_user": "BoboTiG", + "source_repo": "python-mss", + "source_docs_path": "/docs/source/", + "source_version": "main", +} htmlhelp_basename = "PythonMSSdoc" diff --git a/docs/source/developers.rst b/docs/source/developers.rst index 1d29bd72..3dfe19bc 100644 --- a/docs/source/developers.rst +++ b/docs/source/developers.rst @@ -48,4 +48,5 @@ Documentation To build the documentation, simply type:: + $ python -m pip install -e '.[docs]' $ sphinx-build -d docs docs/source docs_out --color -W -bhtml diff --git a/docs/source/usage.rst b/docs/source/usage.rst index 903ee38f..4e105a8b 100644 --- a/docs/source/usage.rst +++ b/docs/source/usage.rst @@ -11,12 +11,12 @@ So MSS can be used as simply as:: Or import the good one based on your operating system:: - # macOS - from mss.darwin import MSS as mss - # GNU/Linux from mss.linux import MSS as mss + # macOS + from mss.darwin import MSS as mss + # Microsoft Windows from mss.windows import MSS as mss @@ -60,7 +60,7 @@ On GNU/Linux, you can specify which display to use (useful for distant screensho A more specific example (only valid on GNU/Linux): .. literalinclude:: examples/linux_display_keyword.py - :lines: 8- + :lines: 9- Command Line diff --git a/pyproject.toml b/pyproject.toml index 0cc7943e..4b4b7a67 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -77,7 +77,10 @@ dev = [ "twine==6.1.0", ] docs = [ + "shibuya==2025.2.28", "sphinx==8.2.3", + "sphinx-copybutton==0.5.2", + "sphinx-new-tab-link==0.7.0", ] tests = [ "numpy==2.2.3 ; sys_platform == 'linux' and python_version == '3.13'", From 52270eee98fbb65478e63b384f1fe79c0417f9fc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 14 Mar 2025 12:52:52 +0000 Subject: [PATCH 257/280] build(deps): bump ruff from 0.9.10 to 0.10.0 Bumps [ruff](https://github.com/astral-sh/ruff) from 0.9.10 to 0.10.0. - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/0.9.10...0.10.0) --- updated-dependencies: - dependency-name: ruff dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 4b4b7a67..be024346 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -73,7 +73,7 @@ mss = "mss.__main__:main" dev = [ "build==1.2.2.post1", "mypy==1.15.0", - "ruff==0.9.10", + "ruff==0.10.0", "twine==6.1.0", ] docs = [ From feafc882224df50cb5f23125cd4c46a9ac210a7f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 Mar 2025 12:58:40 +0000 Subject: [PATCH 258/280] build(deps): bump numpy from 2.2.3 to 2.2.4 Bumps [numpy](https://github.com/numpy/numpy) from 2.2.3 to 2.2.4. - [Release notes](https://github.com/numpy/numpy/releases) - [Changelog](https://github.com/numpy/numpy/blob/main/doc/RELEASE_WALKTHROUGH.rst) - [Commits](https://github.com/numpy/numpy/compare/v2.2.3...v2.2.4) --- updated-dependencies: - dependency-name: numpy dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index be024346..88d7cc3e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -83,7 +83,7 @@ docs = [ "sphinx-new-tab-link==0.7.0", ] tests = [ - "numpy==2.2.3 ; sys_platform == 'linux' and python_version == '3.13'", + "numpy==2.2.4 ; sys_platform == 'linux' and python_version == '3.13'", "pillow==11.1.0 ; sys_platform == 'linux' and python_version == '3.13'", "pytest==8.3.5", "pytest-cov==6.0.0", From 2e4023603493a3304fdb4d756fba676c7ee31e90 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 Mar 2025 12:58:48 +0000 Subject: [PATCH 259/280] build(deps): bump ruff from 0.10.0 to 0.11.0 Bumps [ruff](https://github.com/astral-sh/ruff) from 0.10.0 to 0.11.0. - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/0.10.0...0.11.0) --- updated-dependencies: - dependency-name: ruff dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 88d7cc3e..80b28a8f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -73,7 +73,7 @@ mss = "mss.__main__:main" dev = [ "build==1.2.2.post1", "mypy==1.15.0", - "ruff==0.10.0", + "ruff==0.11.0", "twine==6.1.0", ] docs = [ From bc566167eb1c78c60588bda6cbeb3a6d86cb84e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Thu, 20 Mar 2025 10:05:59 +0100 Subject: [PATCH 260/280] docs: tweak --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index abfc4f72..cda77937 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,8 @@ [![Tests workflow](https://github.com/BoboTiG/python-mss/actions/workflows/tests.yml/badge.svg?branch=main)](https://github.com/BoboTiG/python-mss/actions/workflows/tests.yml) [![Downloads](https://static.pepy.tech/personalized-badge/mss?period=total&units=international_system&left_color=black&right_color=orange&left_text=Downloads)](https://pepy.tech/project/mss) +[![Patreon](https://img.shields.io/badge/Patreon-F96854?style=for-the-badge&logo=patreon&logoColor=white)](https://www.patreon.com/mschoentgen) + ```python from mss import mss From a6fd57322ac12dd0202c0ddb54320f94e2a61b01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Thu, 20 Mar 2025 10:17:53 +0100 Subject: [PATCH 261/280] docs: tweak --- docs/source/index.rst | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/docs/source/index.rst b/docs/source/index.rst index f28aee40..e0e44719 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -1,6 +1,14 @@ Welcome to Python MSS's documentation! ====================================== +|PyPI Version| +|PyPI Status| +|PyPI Python Versions| +|GitHub Build Status| +|GitHub License| + +|Patreon| + .. code-block:: python from mss import mss @@ -43,3 +51,16 @@ Indices and tables * :ref:`genindex` * :ref:`search` + +.. |PyPI Version| image:: https://img.shields.io/pypi/v/mss.svg + :target: https://pypi.python.org/pypi/mss/ +.. |PyPI Status| image:: https://img.shields.io/pypi/status/mss.svg + :target: https://pypi.python.org/pypi/mss/ +.. |PyPI Python Versions| image:: https://img.shields.io/pypi/pyversions/mss.svg + :target: https://pypi.python.org/pypi/mss/ +.. |Github Build Status| image:: https://github.com/BoboTiG/python-mss/actions/workflows/tests.yml/badge.svg?branch=main + :target: https://github.com/BoboTiG/python-mss/actions/workflows/tests.yml +.. |GitHub License| image:: https://img.shields.io/github/license/BoboTiG/python-mss.svg + :target: https://github.com/BoboTiG/python-mss/blob/main/LICENSE.txt +.. |Patreon| image:: https://img.shields.io/badge/Patreon-F96854?style=for-the-badge&logo=patreon&logoColor=white + :target: https://www.patreon.com/mschoentgen From ea90fe4be39d09fd43cea99bf0b36e7df5efa1ed Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 21 Mar 2025 12:29:42 +0000 Subject: [PATCH 262/280] build(deps): bump ruff from 0.11.0 to 0.11.1 Bumps [ruff](https://github.com/astral-sh/ruff) from 0.11.0 to 0.11.1. - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/0.11.0...0.11.1) --- updated-dependencies: - dependency-name: ruff dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 80b28a8f..f02b506c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -73,7 +73,7 @@ mss = "mss.__main__:main" dev = [ "build==1.2.2.post1", "mypy==1.15.0", - "ruff==0.11.0", + "ruff==0.11.1", "twine==6.1.0", ] docs = [ From d3febcf4c41da4a4db0c5312cf8aaf386518f63f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Mar 2025 13:39:43 +0000 Subject: [PATCH 263/280] build(deps): bump ruff from 0.11.1 to 0.11.2 Bumps [ruff](https://github.com/astral-sh/ruff) from 0.11.1 to 0.11.2. - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/0.11.1...0.11.2) --- updated-dependencies: - dependency-name: ruff dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index f02b506c..cfcdd5ed 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -73,7 +73,7 @@ mss = "mss.__main__:main" dev = [ "build==1.2.2.post1", "mypy==1.15.0", - "ruff==0.11.1", + "ruff==0.11.2", "twine==6.1.0", ] docs = [ From 64f33e0b0eefc04adb613e6c56b1cac9dfb75608 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Mar 2025 13:39:47 +0000 Subject: [PATCH 264/280] build(deps): bump shibuya from 2025.2.28 to 2025.3.24 Bumps [shibuya](https://github.com/lepture/shibuya) from 2025.2.28 to 2025.3.24. - [Release notes](https://github.com/lepture/shibuya/releases) - [Changelog](https://github.com/lepture/shibuya/blob/main/docs/changelog.rst) - [Commits](https://github.com/lepture/shibuya/compare/2025.2.28...2025.3.24) --- updated-dependencies: - dependency-name: shibuya dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index cfcdd5ed..ffe77034 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -77,7 +77,7 @@ dev = [ "twine==6.1.0", ] docs = [ - "shibuya==2025.2.28", + "shibuya==2025.3.24", "sphinx==8.2.3", "sphinx-copybutton==0.5.2", "sphinx-new-tab-link==0.7.0", From d7d3366139e4bd3ee04c3d390dd5bdcff154f782 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Thu, 27 Mar 2025 10:19:12 +0100 Subject: [PATCH 265/280] Update README.md --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index cda77937..d81a79ce 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,10 @@ [![Tests workflow](https://github.com/BoboTiG/python-mss/actions/workflows/tests.yml/badge.svg?branch=main)](https://github.com/BoboTiG/python-mss/actions/workflows/tests.yml) [![Downloads](https://static.pepy.tech/personalized-badge/mss?period=total&units=international_system&left_color=black&right_color=orange&left_text=Downloads)](https://pepy.tech/project/mss) -[![Patreon](https://img.shields.io/badge/Patreon-F96854?style=for-the-badge&logo=patreon&logoColor=white)](https://www.patreon.com/mschoentgen) +> [!TIP] +> Become **my boss** to help me work on this awesome software, and make the world better: +> +> [![Patreon](https://img.shields.io/badge/Patreon-F96854?style=for-the-badge&logo=patreon&logoColor=white)](https://www.patreon.com/mschoentgen) ```python from mss import mss From 7679c761cdad537bdb3400975881c2455357b6c4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 1 Apr 2025 12:45:27 +0000 Subject: [PATCH 266/280] build(deps): bump pytest-cov from 6.0.0 to 6.1.0 Bumps [pytest-cov](https://github.com/pytest-dev/pytest-cov) from 6.0.0 to 6.1.0. - [Changelog](https://github.com/pytest-dev/pytest-cov/blob/master/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest-cov/compare/v6.0.0...v6.1.0) --- updated-dependencies: - dependency-name: pytest-cov dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index ffe77034..7a2b8e5a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -86,7 +86,7 @@ tests = [ "numpy==2.2.4 ; sys_platform == 'linux' and python_version == '3.13'", "pillow==11.1.0 ; sys_platform == 'linux' and python_version == '3.13'", "pytest==8.3.5", - "pytest-cov==6.0.0", + "pytest-cov==6.1.0", "pytest-rerunfailures==15.0", "pyvirtualdisplay==3.0 ; sys_platform == 'linux'", ] From 1a0dbf1159c76c3e493eaee0e6bd7e5ce213a46e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 2 Apr 2025 12:50:10 +0000 Subject: [PATCH 267/280] build(deps): bump sphinx-new-tab-link from 0.7.0 to 0.8.0 Bumps [sphinx-new-tab-link](https://github.com/ftnext/sphinx-new-tab-link) from 0.7.0 to 0.8.0. - [Release notes](https://github.com/ftnext/sphinx-new-tab-link/releases) - [Commits](https://github.com/ftnext/sphinx-new-tab-link/compare/v0.7.0...v0.8.0) --- updated-dependencies: - dependency-name: sphinx-new-tab-link dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 7a2b8e5a..a077371c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -80,7 +80,7 @@ docs = [ "shibuya==2025.3.24", "sphinx==8.2.3", "sphinx-copybutton==0.5.2", - "sphinx-new-tab-link==0.7.0", + "sphinx-new-tab-link==0.8.0", ] tests = [ "numpy==2.2.4 ; sys_platform == 'linux' and python_version == '3.13'", From f531c38987c088f986e37b7c0a643f1f52a2acfb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Thu, 3 Apr 2025 19:48:37 +0200 Subject: [PATCH 268/280] mac: take screenshots at the nominal resolution (#346) --- CHANGELOG.md | 6 +++--- CHANGES.md | 8 ++++++++ src/mss/__init__.py | 2 +- src/mss/darwin.py | 8 +++++++- src/mss/linux.py | 2 +- src/mss/windows.py | 2 +- src/tests/conftest.py | 17 ----------------- src/tests/test_get_pixels.py | 6 +++--- src/tests/test_implementation.py | 17 ++++++----------- src/tests/test_macos.py | 15 +++++++++++++++ src/tests/third_party/test_numpy.py | 4 ++-- 11 files changed, 47 insertions(+), 40 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 39068fc1..f0da0127 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,9 +2,9 @@ See Git checking messages for full history. -## 10.0.1 (202x-xx-xx) -- -- :heart: contributors: @ +## 10.1.0.dev0 (2025-xx-xx) +- Mac: up to 60% performances improvement by taking screenshots at nominal resolution (e.g. scaling is off by default). To enable back scaling, set `mss.darwin.IMAGE_OPTIONS = 0`. (#257) +- :heart: contributors: @brycedrennan ## 10.0.0 (2024-11-14) - removed support for Python 3.8 diff --git a/CHANGES.md b/CHANGES.md index 2b456f87..f1030bd0 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,13 @@ # Technical Changes +## 10.1.0 (2025-xx-xx) + +### darwin.py +- Added `IMAGE_OPTIONS` +- Added `kCGWindowImageBoundsIgnoreFraming` +- Added `kCGWindowImageNominalResolution` +- Added `kCGWindowImageShouldBeOpaque` + ## 10.0.0 (2024-11-14) ### base.py diff --git a/src/mss/__init__.py b/src/mss/__init__.py index f0282e4f..ef0faaae 100644 --- a/src/mss/__init__.py +++ b/src/mss/__init__.py @@ -11,7 +11,7 @@ from mss.exception import ScreenShotError from mss.factory import mss -__version__ = "10.0.1" +__version__ = "10.1.0.dev0" __author__ = "Mickaël Schoentgen" __date__ = "2013-2025" __copyright__ = f""" diff --git a/src/mss/darwin.py b/src/mss/darwin.py index a56e05a8..f001398b 100644 --- a/src/mss/darwin.py +++ b/src/mss/darwin.py @@ -22,6 +22,12 @@ MAC_VERSION_CATALINA = 10.16 +kCGWindowImageBoundsIgnoreFraming = 1 << 0 # noqa: N816 +kCGWindowImageNominalResolution = 1 << 4 # noqa: N816 +kCGWindowImageShouldBeOpaque = 1 << 1 # noqa: N816 +# Note: set `IMAGE_OPTIONS = 0` to turn on scaling (see issue #257 for more information) +IMAGE_OPTIONS = kCGWindowImageBoundsIgnoreFraming | kCGWindowImageShouldBeOpaque | kCGWindowImageNominalResolution + def cgfloat() -> type[c_double | c_float]: """Get the appropriate value for a float.""" @@ -170,7 +176,7 @@ def _grab_impl(self, monitor: Monitor, /) -> ScreenShot: core = self.core rect = CGRect((monitor["left"], monitor["top"]), (monitor["width"], monitor["height"])) - image_ref = core.CGWindowListCreateImage(rect, 1, 0, 0) + image_ref = core.CGWindowListCreateImage(rect, 1, 0, IMAGE_OPTIONS) if not image_ref: msg = "CoreGraphics.CGWindowListCreateImage() failed." raise ScreenShotError(msg) diff --git a/src/mss/linux.py b/src/mss/linux.py index 20f85507..009b4234 100644 --- a/src/mss/linux.py +++ b/src/mss/linux.py @@ -264,7 +264,7 @@ class MSS(MSSBase): It uses intensively the Xlib and its Xrandr extension. """ - __slots__ = {"xfixes", "xlib", "xrandr", "_handles"} + __slots__ = {"_handles", "xfixes", "xlib", "xrandr"} def __init__(self, /, **kwargs: Any) -> None: """GNU/Linux initialisations.""" diff --git a/src/mss/windows.py b/src/mss/windows.py index 7a3a78f5..d5e2bb78 100644 --- a/src/mss/windows.py +++ b/src/mss/windows.py @@ -93,7 +93,7 @@ class BITMAPINFO(Structure): class MSS(MSSBase): """Multiple ScreenShots implementation for Microsoft Windows.""" - __slots__ = {"gdi32", "user32", "_handles"} + __slots__ = {"_handles", "gdi32", "user32"} def __init__(self, /, **kwargs: Any) -> None: """Windows initialisations.""" diff --git a/src/tests/conftest.py b/src/tests/conftest.py index 5d455821..97928b78 100644 --- a/src/tests/conftest.py +++ b/src/tests/conftest.py @@ -2,7 +2,6 @@ Source: https://github.com/BoboTiG/python-mss. """ -import platform from collections.abc import Generator from hashlib import sha256 from pathlib import Path @@ -10,8 +9,6 @@ import pytest -from mss import mss - @pytest.fixture(autouse=True) def _no_warnings(recwarn: pytest.WarningsRecorder) -> Generator: @@ -48,17 +45,3 @@ def raw() -> bytes: assert sha256(data).hexdigest() == "d86ed4366d5a882cfe1345de82c87b81aef9f9bf085f4c42acb6f63f3967eccd" return data - - -@pytest.fixture(scope="session") -def pixel_ratio() -> int: - """Get the pixel, used to adapt test checks.""" - if platform.system().lower() != "darwin": - return 1 - - # Grab a 1x1 screenshot - region = {"top": 0, "left": 0, "width": 1, "height": 1} - - with mss() as sct: - # On macOS with Retina display, the width can be 2 instead of 1 - return sct.grab(region).size[0] diff --git a/src/tests/test_get_pixels.py b/src/tests/test_get_pixels.py index 211c7108..486823c3 100644 --- a/src/tests/test_get_pixels.py +++ b/src/tests/test_get_pixels.py @@ -21,7 +21,7 @@ def test_grab_monitor() -> None: assert isinstance(image.rgb, bytes) -def test_grab_part_of_screen(pixel_ratio: int) -> None: +def test_grab_part_of_screen() -> 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} @@ -29,8 +29,8 @@ def test_grab_part_of_screen(pixel_ratio: int) -> None: assert image.top == 160 assert image.left == 160 - assert image.width == width * pixel_ratio - assert image.height == height * pixel_ratio + assert image.width == width + assert image.height == height def test_get_pixel(raw: bytes) -> None: diff --git a/src/tests/test_implementation.py b/src/tests/test_implementation.py index 5672f044..294ccc80 100644 --- a/src/tests/test_implementation.py +++ b/src/tests/test_implementation.py @@ -64,14 +64,9 @@ def test_bad_monitor() -> None: sct.shot(mon=222) -def test_repr(pixel_ratio: int) -> None: +def test_repr() -> None: box = {"top": 0, "left": 0, "width": 10, "height": 10} - expected_box = { - "top": 0, - "left": 0, - "width": 10 * pixel_ratio, - "height": 10 * pixel_ratio, - } + expected_box = {"top": 0, "left": 0, "width": 10, "height": 10} with mss.mss(display=os.getenv("DISPLAY")) as sct: img = sct.grab(box) ref = ScreenShot(bytearray(b"42"), expected_box) @@ -195,7 +190,7 @@ def test_entry_point_with_no_argument(capsys: pytest.CaptureFixture) -> None: assert "usage: mss" in captured.out -def test_grab_with_tuple(pixel_ratio: int) -> None: +def test_grab_with_tuple() -> None: left = 100 top = 100 right = 500 @@ -207,7 +202,7 @@ def test_grab_with_tuple(pixel_ratio: int) -> None: # PIL like box = (left, top, right, lower) im = sct.grab(box) - assert im.size == (width * pixel_ratio, height * pixel_ratio) + assert im.size == (width, height) # MSS like box2 = {"left": left, "top": top, "width": width, "height": height} @@ -217,7 +212,7 @@ def test_grab_with_tuple(pixel_ratio: int) -> None: assert im.rgb == im2.rgb -def test_grab_with_tuple_percents(pixel_ratio: int) -> None: +def test_grab_with_tuple_percents() -> 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 @@ -230,7 +225,7 @@ def test_grab_with_tuple_percents(pixel_ratio: int) -> None: # PIL like box = (left, top, right, lower) im = sct.grab(box) - assert im.size == (width * pixel_ratio, height * pixel_ratio) + assert im.size == (width, height) # MSS like box2 = {"left": left, "top": top, "width": width, "height": height} diff --git a/src/tests/test_macos.py b/src/tests/test_macos.py index cce8121b..c89ea2a8 100644 --- a/src/tests/test_macos.py +++ b/src/tests/test_macos.py @@ -4,6 +4,7 @@ import ctypes.util import platform +from unittest.mock import patch import pytest @@ -67,3 +68,17 @@ def test_implementation(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setattr(sct.core, "CGWindowListCreateImage", lambda *_: None) with pytest.raises(ScreenShotError): sct.grab(sct.monitors[1]) + + +def test_scaling_on() -> None: + """Screnshots are taken at the nominal resolution by default, but scaling can be turned on manually.""" + # Grab a 1x1 screenshot + region = {"top": 0, "left": 0, "width": 1, "height": 1} + + with mss.mss() as sct: + # Nominal resolution, i.e.: scaling is off + assert sct.grab(region).size[0] == 1 + + # Retina resolution, i.e.: scaling is on + with patch.object(mss.darwin, "IMAGE_OPTIONS", 0): + assert sct.grab(region).size[0] in {1, 2} # 1 on the CI, 2 for all other the world diff --git a/src/tests/third_party/test_numpy.py b/src/tests/third_party/test_numpy.py index 6a2f2e09..6d5cf286 100644 --- a/src/tests/third_party/test_numpy.py +++ b/src/tests/third_party/test_numpy.py @@ -12,8 +12,8 @@ np = pytest.importorskip("numpy", reason="Numpy module not available.") -def test_numpy(pixel_ratio: int) -> None: +def test_numpy() -> None: box = {"top": 0, "left": 0, "width": 10, "height": 10} with mss(display=os.getenv("DISPLAY")) as sct: img = np.array(sct.grab(box)) - assert len(img) == 10 * pixel_ratio + assert len(img) == 10 From 9e8093368a8d0e6c599b8bd3f19500bba439d0c1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 4 Apr 2025 12:37:05 +0000 Subject: [PATCH 269/280] build(deps): bump ruff from 0.11.2 to 0.11.3 Bumps [ruff](https://github.com/astral-sh/ruff) from 0.11.2 to 0.11.3. - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/0.11.2...0.11.3) --- updated-dependencies: - dependency-name: ruff dependency-version: 0.11.3 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index a077371c..f169b5bb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -73,7 +73,7 @@ mss = "mss.__main__:main" dev = [ "build==1.2.2.post1", "mypy==1.15.0", - "ruff==0.11.2", + "ruff==0.11.3", "twine==6.1.0", ] docs = [ From 0c73322bedbdf19bc5374f19e8fcc3a8c0870b68 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 7 Apr 2025 12:42:55 +0000 Subject: [PATCH 270/280] build(deps): bump pytest-cov from 6.1.0 to 6.1.1 Bumps [pytest-cov](https://github.com/pytest-dev/pytest-cov) from 6.1.0 to 6.1.1. - [Changelog](https://github.com/pytest-dev/pytest-cov/blob/master/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest-cov/compare/v6.1.0...v6.1.1) --- updated-dependencies: - dependency-name: pytest-cov dependency-version: 6.1.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index f169b5bb..8c9a5975 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -86,7 +86,7 @@ tests = [ "numpy==2.2.4 ; sys_platform == 'linux' and python_version == '3.13'", "pillow==11.1.0 ; sys_platform == 'linux' and python_version == '3.13'", "pytest==8.3.5", - "pytest-cov==6.1.0", + "pytest-cov==6.1.1", "pytest-rerunfailures==15.0", "pyvirtualdisplay==3.0 ; sys_platform == 'linux'", ] From de7c659c2ad23475a355e9632b622d606c20a171 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 7 Apr 2025 12:46:19 +0000 Subject: [PATCH 271/280] build(deps): bump ruff from 0.11.3 to 0.11.4 Bumps [ruff](https://github.com/astral-sh/ruff) from 0.11.3 to 0.11.4. - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/0.11.3...0.11.4) --- updated-dependencies: - dependency-name: ruff dependency-version: 0.11.4 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 8c9a5975..6be795cc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -73,7 +73,7 @@ mss = "mss.__main__:main" dev = [ "build==1.2.2.post1", "mypy==1.15.0", - "ruff==0.11.3", + "ruff==0.11.4", "twine==6.1.0", ] docs = [ From d4fa195edc788594b26fdd9d51c2753079be91f1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 13 Apr 2025 22:05:57 +0200 Subject: [PATCH 272/280] build(deps): bump ruff from 0.11.4 to 0.11.5 (#359) Bumps [ruff](https://github.com/astral-sh/ruff) from 0.11.4 to 0.11.5. - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/0.11.4...0.11.5) --- updated-dependencies: - dependency-name: ruff dependency-version: 0.11.5 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 6be795cc..76d6ef81 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -73,7 +73,7 @@ mss = "mss.__main__:main" dev = [ "build==1.2.2.post1", "mypy==1.15.0", - "ruff==0.11.4", + "ruff==0.11.5", "twine==6.1.0", ] docs = [ From daa342c8ace4f7e71d5b931be2bb1435846f6734 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Apr 2025 14:25:29 +0200 Subject: [PATCH 273/280] build(deps): bump pillow from 11.1.0 to 11.2.1 (#360) Bumps [pillow](https://github.com/python-pillow/Pillow) from 11.1.0 to 11.2.1. - [Release notes](https://github.com/python-pillow/Pillow/releases) - [Changelog](https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst) - [Commits](https://github.com/python-pillow/Pillow/compare/11.1.0...11.2.1) --- updated-dependencies: - dependency-name: pillow dependency-version: 11.2.1 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 76d6ef81..23be9e46 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -84,7 +84,7 @@ docs = [ ] tests = [ "numpy==2.2.4 ; sys_platform == 'linux' and python_version == '3.13'", - "pillow==11.1.0 ; sys_platform == 'linux' and python_version == '3.13'", + "pillow==11.2.1 ; sys_platform == 'linux' and python_version == '3.13'", "pytest==8.3.5", "pytest-cov==6.1.1", "pytest-rerunfailures==15.0", From 6e02270f3578076cd2637801478f97236f4dae25 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 18 Apr 2025 18:20:41 +0200 Subject: [PATCH 274/280] build(deps): bump ruff from 0.11.5 to 0.11.6 (#361) Bumps [ruff](https://github.com/astral-sh/ruff) from 0.11.5 to 0.11.6. - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/0.11.5...0.11.6) --- updated-dependencies: - dependency-name: ruff dependency-version: 0.11.6 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 23be9e46..4177a4f1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -73,7 +73,7 @@ mss = "mss.__main__:main" dev = [ "build==1.2.2.post1", "mypy==1.15.0", - "ruff==0.11.5", + "ruff==0.11.6", "twine==6.1.0", ] docs = [ From 83a3747fae4b982a2703139c1f7907e41e12856d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Thu, 24 Apr 2025 04:19:31 +0200 Subject: [PATCH 275/280] Update dependabot.yml --- .github/dependabot.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 4075752b..8d9e0b26 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -4,7 +4,7 @@ updates: - package-ecosystem: github-actions directory: / schedule: - interval: daily + interval: weekly labels: - dependencies - QA/CI @@ -13,7 +13,7 @@ updates: - package-ecosystem: pip directory: / schedule: - interval: daily + interval: weekly assignees: - BoboTiG labels: From 0dcf3de08e27ef4940aef3f4327c71c37e8008b7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 28 Apr 2025 17:01:38 +0200 Subject: [PATCH 276/280] build(deps): bump shibuya from 2025.3.24 to 2025.4.25 (#362) Bumps [shibuya](https://github.com/lepture/shibuya) from 2025.3.24 to 2025.4.25. - [Release notes](https://github.com/lepture/shibuya/releases) - [Changelog](https://github.com/lepture/shibuya/blob/main/docs/changelog.rst) - [Commits](https://github.com/lepture/shibuya/compare/2025.3.24...2025.4.25) --- updated-dependencies: - dependency-name: shibuya dependency-version: 2025.4.25 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 4177a4f1..bc323977 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -77,7 +77,7 @@ dev = [ "twine==6.1.0", ] docs = [ - "shibuya==2025.3.24", + "shibuya==2025.4.25", "sphinx==8.2.3", "sphinx-copybutton==0.5.2", "sphinx-new-tab-link==0.8.0", From 4affa0c8d32c1408e4e5704e6caf415699d38521 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 28 Apr 2025 17:01:54 +0200 Subject: [PATCH 277/280] build(deps): bump ruff from 0.11.6 to 0.11.7 (#363) Bumps [ruff](https://github.com/astral-sh/ruff) from 0.11.6 to 0.11.7. - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/0.11.6...0.11.7) --- updated-dependencies: - dependency-name: ruff dependency-version: 0.11.7 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index bc323977..8f415779 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -73,7 +73,7 @@ mss = "mss.__main__:main" dev = [ "build==1.2.2.post1", "mypy==1.15.0", - "ruff==0.11.6", + "ruff==0.11.7", "twine==6.1.0", ] docs = [ From f6d572527709aa06ebd01487c2bd66f0dc44df58 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 5 May 2025 18:41:45 +0200 Subject: [PATCH 278/280] build(deps): bump ruff from 0.11.7 to 0.11.8 (#365) Bumps [ruff](https://github.com/astral-sh/ruff) from 0.11.7 to 0.11.8. - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/0.11.7...0.11.8) --- updated-dependencies: - dependency-name: ruff dependency-version: 0.11.8 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 8f415779..6280be8d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -73,7 +73,7 @@ mss = "mss.__main__:main" dev = [ "build==1.2.2.post1", "mypy==1.15.0", - "ruff==0.11.7", + "ruff==0.11.8", "twine==6.1.0", ] docs = [ From 355cc0b174ce051a0656bc5498ff156aee622d33 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 12 May 2025 14:41:14 +0200 Subject: [PATCH 279/280] build(deps): bump ruff from 0.11.8 to 0.11.9 (#366) Bumps [ruff](https://github.com/astral-sh/ruff) from 0.11.8 to 0.11.9. - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/0.11.8...0.11.9) --- updated-dependencies: - dependency-name: ruff dependency-version: 0.11.9 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 6280be8d..200517fa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -73,7 +73,7 @@ mss = "mss.__main__:main" dev = [ "build==1.2.2.post1", "mypy==1.15.0", - "ruff==0.11.8", + "ruff==0.11.9", "twine==6.1.0", ] docs = [ From 076fab9ee934c7e3ec14282445b911fa3a7ac50b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 12 May 2025 14:41:30 +0200 Subject: [PATCH 280/280] build(deps): bump pytest-rerunfailures from 15.0 to 15.1 (#367) Bumps [pytest-rerunfailures](https://github.com/pytest-dev/pytest-rerunfailures) from 15.0 to 15.1. - [Changelog](https://github.com/pytest-dev/pytest-rerunfailures/blob/master/CHANGES.rst) - [Commits](https://github.com/pytest-dev/pytest-rerunfailures/compare/15.0...15.1) --- updated-dependencies: - dependency-name: pytest-rerunfailures dependency-version: '15.1' dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 200517fa..1f3581dd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -87,7 +87,7 @@ tests = [ "pillow==11.2.1 ; sys_platform == 'linux' and python_version == '3.13'", "pytest==8.3.5", "pytest-cov==6.1.1", - "pytest-rerunfailures==15.0", + "pytest-rerunfailures==15.1", "pyvirtualdisplay==3.0 ; sys_platform == 'linux'", ]