From 7b5cff83715b6f2f4c1e46a184c73be2b7657263 Mon Sep 17 00:00:00 2001 From: Bruno Pagani Date: Thu, 29 Jul 2021 07:15:55 +0000 Subject: [PATCH 001/242] 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 cfdb620..bd78441 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 002/242] 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 e1099a3..8783d00 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 003/242] 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 137715f..6adb7bc 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 004/242] 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 2c5b6c4..2b4277d 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 005/242] 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 ed22b90..880ccfa 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 9251636..6de21e3 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 47aea11..902ce06 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 8e56c39..b952916 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 478fecb..0e38202 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 006/242] 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 bd78441..db614f3 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 007/242] 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 d84a392..b1fddb4 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 e2acbe6..ef5ae01 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 c96af38..199d179 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 2b4277d..35e51fd 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 ee79774..147c952 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 4e5d875..4232e49 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 e812378..4046f2a 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 d229cb0..28caf59 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 0e56cec..61f2d94 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 d03341d..a0b7b40 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 46e05e0..81130ad 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 e4705a5..73f93cb 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 9bbc771..61f58f7 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 4d8e972..db10f1b 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 1108174..fcedcec 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 af42192..ed40dda 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 25b7ab5..c23dc87 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 939e7ae..636730f 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 880ccfa..bccc279 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 6de21e3..2e1ca47 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 e783175..028a1d7 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 902ce06..30e15c2 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 b952916..e676763 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 fe5b606..9c0851a 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 6ed6e9b..71ebc2c 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 2560f90..2319684 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 dab3fb2..1fe44cc 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 f32869e..fc6a346 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 ee64ed7..9a29bc3 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 e340e5c..0abf4d9 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 1011976..4596ffb 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 ba788ef..f278c97 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 e7a6ca1..ad8147c 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 1cf4e4c..113b33e 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 bc4fbb2..5c11494 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 e1a213c..87f0e91 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 e562bf3..1c2551f 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 ecdea05..d1fa286 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 62f7b6c..4694a02 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 aa79b2b..47fd74e 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 0e38202..e5c8eee 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 0000000..7b3afaf --- /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 db614f3..c6a2545 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 bfe7ba5..a814f6b 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 008/242] 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 e076e61..8efc3ca 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 87f0e91..ee4fcd3 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 a814f6b..04e8182 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 009/242] doc: tweak --- CHANGELOG | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index b1fddb4..240eb87 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 010/242] 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 240eb87..f5dc725 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 8efc3ca..b2d00fc 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 35e51fd..6fabc35 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 c23dc87..cca0e48 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 c6a2545..e65e61e 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 011/242] doc: tweak --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index b2d00fc..e2ad276 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 012/242] doc: fix Anaconda badge --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index e2ad276..f249d23 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 013/242] doc: remove "say thanks" badge --- README.rst | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.rst b/README.rst index f249d23..36c9003 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 014/242] 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 36c9003..ead2034 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 def5e87..365adab 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 e65e61e..0cac3ae 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 015/242] dev: ignore cache folders --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index e9437f3..1345012 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 016/242] 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 ee4fcd3..8c4d5a7 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 017/242] 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 0000000..85e3cba --- /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 0297694..0000000 --- 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 ead2034..c51952e 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 199d179..0000000 --- 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 018/242] doc: tweak --- CHANGELOG | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index f5dc725..c70f9f2 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 019/242] 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 ab124fb..789ae14 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 020/242] 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 85e3cba..dcb3673 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 021/242] 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 c51952e..be0acdc 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 022/242] 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 c70f9f2..55b08d5 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 bccc279..9a72f13 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 028a1d7..0d297b3 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 023/242] 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 dcb3673..1ed45b1 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 55b08d5..0587afb 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 be0acdc..f88e47a 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 365adab..6d87e64 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 ed40dda..3cef168 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 0cac3ae..f2174d7 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 024/242] 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 ef5ae01..8d49e5d 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 6fabc35..8187f52 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 cca0e48..7ef4d78 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 025/242] 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 789ae14..ba81586 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 026/242] 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 ba81586..05f646b 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 027/242] 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 1ed45b1..10bf68a 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 0587afb..6b14f84 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 028/242] 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 e676763..32e4945 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 029/242] 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 0000000..4075752 --- /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 10bf68a..6318098 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 1345012..f4d791d 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 c7f4943..0000000 --- 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 6b14f84..481c2e9 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 0000000..ba8a974 --- /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 0000000..af5c133 --- /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 db544bb..909d676 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 636730f..8e071a3 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 8c4d5a7..9161fd8 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 f2174d7..09c883d 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 04e8182..0000000 --- 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 030/242] 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 6318098..d51d229 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 031/242] 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 d51d229..ee20288 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 481c2e9..6a449f2 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 49fdf11..03438c1 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 8187f52..cd19fa9 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 7ef4d78..61a4d3c 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 32e4945..dc8eeeb 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 4596ffb..b53ad13 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 09c883d..cace210 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 032/242] dev: ignore .coverage file --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index f4d791d..361d611 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 033/242] 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 05f646b..5012307 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 034/242] 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 5012307..ad8a05e 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 035/242] 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 af5c133..c5d21e0 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 036/242] 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 6a449f2..a5241ad 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 00971c5..4cc26fb 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 03438c1..451f562 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 9a72f13..5246f54 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 2e1ca47..8354395 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 dc8eeeb..916efa8 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 b53ad13..abd6cba 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 e5c8eee..04f26e6 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 037/242] 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 ee20288..0869d70 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 07fee3f..97fac66 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 a5241ad..26e2186 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 7cd482f..d03e4db 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 ba8a974..e9000a8 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 c5d21e0..589b76e 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 451f562..c4fe102 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 8e071a3..1aa41ad 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 5246f54..3dd6a55 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 8354395..a4ba900 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 916efa8..4daa234 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 71ebc2c..94bedfc 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 fc6a346..979850d 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 a3b198c..b531ba1 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 c5b1569..278dc1b 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 0abf4d9..b6a7b6e 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 abd6cba..6a86ce5 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 f278c97..dee34fc 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 ad8147c..6b48bcc 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 5c11494..6dfbc19 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 1c2551f..e89afbd 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 d1fa286..d939cb1 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 47fd74e..ffe1f3d 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 04f26e6..0172a05 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 cace210..613a73e 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 038/242] 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 c4fe102..73b8930 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 4daa234..640a451 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 039/242] 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 0869d70..bb12979 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 26e2186..8b3bd08 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 f88e47a..86d85ec 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 6d87e64..c3f33ff 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 3cef168..330f943 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 1aa41ad..52ef301 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 3dd6a55..11c3950 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 a4ba900..a6661f6 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 0d297b3..9ffb94b 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 640a451..cfcaebb 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 94bedfc..9c82d72 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 ffe1f3d..de3a1af 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 0172a05..00c380e 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 613a73e..fb03cd3 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 040/242] 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 8b3bd08..6657c51 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 cfcaebb..841ea1f 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 6a86ce5..2c8fc6c 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 0000000..0d5f143 --- /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 041/242] 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 841ea1f..0e74301 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 0d5f143..6ad1de0 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 042/242] doc: tweak --- CHANGELOG | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 6657c51..bde4833 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 043/242] 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 bde4833..54ad8f3 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 8783d00..47bf7cb 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 52ef301..5cf76f1 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="http://github.com/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 0e74301..de7e387 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 044/242] 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 54ad8f3..84d3bb1 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 73b8930..5491312 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 045/242] 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 84d3bb1..6b040e5 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 cd19fa9..7c340a3 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 61a4d3c..d441f12 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 046/242] doc: tweak --- CHANGES.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index d03e4db..8050203 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 047/242] doc: fix version --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index fb03cd3..ab1cb5a 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 048/242] 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 6b040e5..44272dc 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 dee34fc..6ce5a3a 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 049/242] 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 6ce5a3a..efa75b1 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 050/242] 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 44272dc..9fd9dac 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 5cf76f1..73d0faa 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 efa75b1..e1ea773 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 051/242] 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 9fd9dac..153c1fa 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 11c3950..14a4528 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 052/242] 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 153c1fa..ca7dc35 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 330f943..8a11d6b 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 053/242] 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 ca7dc35..9d49bf3 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 7c340a3..123e4f8 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 d441f12..3d81ef7 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 ab1cb5a..0bc0aa4 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 054/242] 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 9d49bf3..92b9930 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 73d0faa..0aa4398 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 e1ea773..5252bbb 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 055/242] 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 979850d..a0d24bb 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 zcmeFacf4d}k^j#y3=G*J$C&|!oP&~tAqP={l5l^t@89;Ey?t+|(|x-8^GChvp3~<%Pt~VBRrNgQ^u70< zYiI3n&$YAW3PC@V$7p@lip(AN%@?w8$YLSOgiH-tC1lNz^+GlY*&<|S$c`bqh3px! zZ^*$RM}!<5@|KXJLJki(G~}R=144A&-XVL1>>jdn$hILGbK{V8Lsko!9{uvd zQb;R4)w^f@7*BZpIpl94FNKU$TwV$Lo)3}DvRyvZ^KZQKuaA4H?(cZ^$%pa$toQi` z2f%;%P|v^d&i`rv{Fe{o`8nF>s%PVIt6x5sFQfT5*16*c$bb1Vman6IcKZPNFJDIU zajbI>50L-zWh`HdpDj5+zEh**Q_;_Q$2$(7|MIDxzs2*X51{|@spx0DbO{*4N_Cdbai40Q)cBGHt5r2z%<`DQVAB%o- zJWqV~=m29N-{d3y7WIVq?CJsbU%tu5qTd|rx&Gz@?7w`IkN8`w#S6VdSR&-!5bDLC zX!#`H{|)1sCqo#cU$uOYFT{T||5XPk zhrB98?^9O_Su^kkS`Hi-=T8keC*-{$?+^J%$Za8C4f%G+eIY+i{ln1rhx{_+w;_KB zksZxqsCU@E3wa=8Y?aNjT|UScdcS-e&3Det8=o&6vSf(vn-QXM zHVV(f+vhlo-i$X3Ac_pM-|DVfyHHOC0m>OGiXfDmE zxn+ZFkxjBqHp*7nEZgP7k|FX*zTxLsnpz;t)H7BNSuF zvUAAmLuB9HA<9en(2DQ6M_kt!8cSnpY|Wv$G^ggqZ`mZ=^K-`- z^M@=N0uIyT=T$@24p~2BlMuy0c5NG?*z6K=R!FP>gTsk$pmr3;9UM_d}iuskN`{pI?soriYB@ODmtB7~{fa_26ei{yd84nz8y; znHc;v<`({~_&3%)OU0NPyG+~L8C!TO9#4nN2pP+lRzCYwjH$I(J2tj@uekPHNReyu zSA2gc<5(DJZfah;acuSe%X{SivN3O?yw`PqNm0HkFP{ieJ!;i&?z<(%7%N6) zeRfs)@JQ(Y33(vory=)+d^1FAjeiSKuRkN?fRHUiRt#Z3a%=pIAFXPQQI?Nok1K3i z^_%-NM%i|jHU428@Lyw;WnbCj>RIFqZD62#HAdNXmN~xm=5V=LUFCg!qnPP=pz39_ zzUv;{+vq%1zE00*?`#4CU8{Sl>{GM0>G|*xA$Nt0_Dn$M^qH>Fwat#-iR+J!d7I^_ z?qTj`@8x>mKO4h#@fVKGj8PjqVo=n?2wnVqykqV|k9U3XbM8YIKOgUy`_SWEU;Lc= z(8bTkJLW$0c-I#{=RS1t^YM;r`Orwb#~ZJcpHB(CUeQ9$0Sn!$G3wd0ZX07WhsLNk z{!SejUt`o89~;`$qm92{2W-bKja8gufewzZv5Mm_)4}mIR&o5Y?bm$Z*RC$w&hZHy zm_uVQ-e|g=ImR1%wE1*yy;1zqxq8<(dw#!B#@D%K$EkPydqx>w=jvVG?D<{je)^qzGBhmAuv z51ARF_Y%8>>=mLKaY)EfA;*TC9CCWdSs~|#%nH%-Y^_@^$>R${pBHjY$eAJU2+@6S z4beN+gF^JaWA_k!_ivk!Ekb02Y-!YPUAJ6FJ?{7FyliUJ2jS5xJL}CWn;NzM(-pK| zHZ@|up6Pe{eSSU6YtPFj-ZzZphjOO&zAF7J+s3k8pPgD^`(@jB_TN}x`(@jB_W!KH z_RF@h?4K__AMKfARdPVK$;Ns6f2$Ad zmyM&@KOxRP+Xwc`M%h~QVdXxsom`NuMf;bHb1(LR53*6V7VX#ZXwL)ixhi_B?|IDZ z1N^l&8?A@wJ7ju?|C0*)p!I{eBRl5~spZh*I9@zN-@~g4SBiG%`$tpL&SDJo9Cyi( z)k2;QDc;-bW9?ry3|T2ecFNvb{B^ukh(5nIq}TQq=l_?mNzWl6h%dR17hQOam@pG;HvPX8wK4D$7onz&Jey2`x!iO#6XFY$@voSq;J1pdwkds4%(Yr&w5K^?W-m!4L zFz?g-8b{-4T#c`JG@s_hcG)AlCWT;YJxv@|h-($I^+GlcnHjQ6$Q~h@=g^S1gd898 z#gKaT7SDeyt`#qJpYGQ<8c*YDe9fczG_U5D9gBrb45?>x@w{TRNXXJ5(?S%B^+Po7 z)*(BF)IJZrD~^kH);qpAu01G3HtTt+#?g2hSL16Q&8K-azwjK5??szOJFfL(YK$X1 zRL2z8%|f;c*)F6R{(K{@7q@kv?$m{x8M# zPo_K(`tguQLe2^)&eg5s2V(9b&dnab5Mz9-M?U<$1NO_`mqWJgfe)IuS-k6A|9p%! zxr;t1AL`lC>iHjZ$bRwh+aaF}nH8cOyCCF!A-9CwAM$8Ot9I7APccw^YbSqS7xzC? zf^GF|u08*B%saCU`^CS9O0->dQ*o21eyW~67V==o9U=RLw1fR+asNL;YBA~L&-=vK z(?iw@**HXNHLcIuv6*{k#5hldbkg49SO<5=R&1`-_l;ruEAiQ1LKOEurL>YGTJP7+ z*{Ppbk8!mYzBA;rA)g4jKIE#9i$YEc**#>#5bY&2@0lS*e8)T1J)N?--Eod+!G7J- z?!2A4Uu)fF`me6lJ)N4r6XU4AjmH^2*R`FPvy)?JJ*|Af#(G-U=-N)s-KnuAhFz*F z%767Ee9+H2r_Xdvr{?oG#exPR`+Jhe$a%jkrAxDQC z7ouKwYKVBRb@#a;7lh0TxisX;kPn1h6QUk@L&!}bs!z9td_F{Lxi5!&E##h%?}U6W zb9>0=LNxZLLp0CLAvcC-?vI4Xp36ei zt7Y#yLrw`fKIF|IhlFTtxo60(A=`(%HssYI>xZlnvQo(MAzIHZ95P>s_DE&Ns-5vw zpMDeUo8w2lUC!|?0tGx=lE9(1I47- zIJSEIryc*w@n11%HJ;7fH!4)y6m23Xn@vnUR6{A*SJKjBu#kl$%3fKQ?W1tu* zR^!F9+0XTzRmZ+RFi@_*uKj&e`&pwYX3f_4de`fBmKfK%Kd?~D>cz9!^Y3!}`;PyLS+jAjcm0iyf8WJl zF{>BPX3u}$@$b9%D`w5cx!(2uJ)i#Q`L$~^#jIXDn>oLtbHDF$A08-n&BS`NYj<+| z`!@cH-Dojw=G@7Sf8WMmv1=yQqg{KgG=0;{1v-qVm;cmk2(H* z8-K-aw3s$}ZgI!HZ)49MKrw7I-o@+m_sJN?x<9c{42yAX^!P1~f8X<8F>Ey6#p^D2 z{QExs`kR!+xHfuxisRq+@mCBRjd$_7rQ_Zg&4Iq%{}9K&KQK_N z6f?z6F;pxS(?DxIZTyS%zmfPi(*L!-SU2RWA&!4FFi($vM^ueMpGoUkwZt3&lk7SB$2GjJEzC?>X>)F!heK`*xh>?*kb6Sz3;9{duR|URc_QSwkQYPDmpL&ICfdJ09r9?1a_Uzh z_lNu-3;FkuPlpKW>q3qW87=pD_OI9}hKi+PS{viBex5t7)mmYJ5Is8{YaOs& ze7;eLo;~QD^VT8ThU^%!Ysecy_6pf2WdD$ZLyihLG31Ppb3-l)xisXe5OL|FAs-L9 zCFC<9pAWes+Zag+dk$Sv*9~leMm#7P3kRePI3gdE<~;4r~|4JB8Haz+v&3{5~e+xR8@V zP6;_Zbi)@l@vQf6m zX4x(u_xyd~t=kP|{q4mmC49U<=uIXk2t z58fZ2eIVpRA017LYN2}VN{#yL! zl6|#xKy^zT*d^rkA$x?pDP-S}149l8IXvXeA=H8s@RNWg)`< zuOZ!v|1WY*%`F>bi)?yNh`3seyL^#P9&+=Uu2n%5%Y=n`p5@yAkFKmguFrN{k z7$_EsiDIJ|)y7^ioU0Ad{pueJgp9?3+B(2`VDq?VtB`F%b_m%yWVevrL-q>UCuIK+ zYQYilv#>cPL|7Ghpc<-L`tFePLWK84A;SNKkZ#5Qw>hWgmJPB+Hpw>GD9(zzqs3i5 zlJoLi7zhhtB5Z__uo7m%P8hx>M3@r$)#K-}Vy{@W((~tv`$iKuP+JF7XO;;O_ojug z9#|`WUN2=8Y?aO8u(&Kvj}~|NzDJ0#5GKM#7zrz3ChUZvuoR}kR z(?W#3VxU+|2_f%08GHCJA;zB+0teJ%H1EGC|hN-Y?lwke3!5CS-uMcVIfR}jW7~c!c5o+Lt!aQg{?3a*1}xaD+aF$SvW+o zQH&I;xk?Z?FgeCvC}h!)#Y2_~SuSM7km(_+P2#EeN-fwVe%AU$zHJ?{ZHVgJ&LP4; zIiOsi9_$xC9~g3Q$YCKzgw%53xHvu`#wM17RUdgpDu~R>hnbrovVjFBc-rg}q{+SSTiC#=e$M6JxIVLKX;FBt*4m z$q?0|Vl7xB&aE9H9&8k%dZxOj`X-;Z2@&^|1G|Ro7P5QDULkJ^p(Y4N;dyAt;UPzc zyg5X9c5X;F`c}jhOErKg)LcAYbH@e3Os0`7R8Eg)k8|!bn&NGhtWE zd11U{h%jFyL@`h-6qDK*mHqSFF`xLRnj`+z)`Df@c!iK@A*xrag{&E}PRRNp8-~=@ z1J%5lA=`!Q7*bmkid;}G9T0L*2zbzVJ7URg$PSwTFiN2uNcf7Qg*!SjbB>})PvNERF{f9VU;+yYDjH85DzvE zc}>XXA+EGuaXEKwj>|ULNS=$w#C)6hnOv8z z@>#wMgJQl5tLY)dT2G(vRnFJrz=W`Mo)Fa`*`~TQB}8kJr9;Hs+Ik=!tRAvf$hslo zz3Lt{0WK&EJun;CIht%e~uoH&DQkW8VVJ*z-#iLi}Wz(dP$sw|J;gH&TARa6qGBsqS5b<1f zj+#*9f^tNCQR|}DhA4l8>9!%;hbXs%_rHa7JN~LUnp-x=7TF}*WTR}A&9YrSh}X3- zr?<;@VIVAoiLenyONFS0*57P{8=|$!f+4D7)CBpf^=y#~%86A&R2SC_QNE})tQWFDh;UW!gcF~O zpSvCZJ7P|9n?5evWTR}A&9a@^PM+7soV7f;E^LI6uo7m%t`Us;;TqKh)vx(Nji0{!6aSc4(H}hKjDHnFC0`CRR5L;Q4bVWwR}*IoE`!vR7ZvNeIebB|9|D&vVmAD z|91>IIOM>P?L$-xlY#2j{l!x&Qn4@AM!sTFND;y;pO=3HzDVQ$Y%K}pXGbM#;q*7i+oUBRIX?} zM2!$9s1xcNkA&32zG~;ilbb`-H_F0rkj51sCWfdlDsSi+suSXcYQBqzNwL-dy+n~)gy^rHBzCIVC_2Zz$rCa`N)#vdS2mTnM z^-Z@h8>D%T8G!hU178pEexUgrxM)D)FAkg?GDx}E4PR~;p!mNOzN~5vG?fE)4p976 z1MYYHn~MMU2Q2>L!0F~d!yNe40LK5t@Ja8By3s%TeU3*5F#f6m|LyoU#D6_M5nh8R zdSBG9`Kfo|djF~S9C~lp?)y!>=b0IDT*!w)z7g_R$RNfYpVSA0UB7Wa?`VpB`Qq0&ja$|_DxhG`M<4n9q( z9sjoD|4Lx^?GWvA<_&2lAG>+4*7087HIISf{reET$2cuy#gJ~strK&t9X4yPI*jqx zd;1+i_6yNoP4A;W8uEpZFNWM6^4XAELv9ZF*N}@t4hwm8h`ztri5T>9jBkby!x(${ z{M7-7Upw46bckcG_z&~@TK#^;puT@;w4OaV#QCr1wTj8f_1v=`Yj`U* zFBQJ(ch^S4p`CMje(?JcJ^RvgPwkP957|9LzbCguNUgmG#c@0Hb@E=>Qfu2F|9oor z`n@`GZ=BbDSicjk7+xQ8Rmgio&JH;xUIb_q2?Z(CI;{<5Wt7!9&bYgQ=!vSpC5uA*-f19N>hqMf?kiF;*(Y^fqfgKpD36^OrV z7<8Pg>ff0ah`(&8Dn5g5m%bDJd=ELGxn;wk<6LF`KHkIlYwjvzGYq!r_e^@R4%D2Q zdl)dTx{n7$96GU&ZOBt0jl_1WYcz(&(wO~*!?4>sZ^TUR z=57jkHe{?g*M6pJb&u}V7{iR?I^qAuA^MKX%#a;Jb_>z_TYcyKkdUK7^qr8CLrxDl zE9Cr;Ss|B)ToIzaspAVn&I>swM91WwC zp8fisNtJA_wMDj#Xa9{AwqLf5XaCPCY`<(9%l`S|^Oq`Yzig9@^?cZ6f2$AdmyM&@uitlnwh!!=jk2}q!^(YNJ2@a*i}o)Y=U(gs zA7rC!E!wZ+Tl&EMTkF^^`)Brn{o0$2*2DCFjTR00NripT`a#^0opXoOa%gfKFCOx? z5b~sI`i!t`YT8+h!2)q!-$__4=|-Ii1I^8}h@DUxqXqGhO@RkpBp|C*;c^8ut?+*M(deazTjreq6`_A?t_8F4?z8 z$b^tu{PpwvA)Lx@bGc%7Z0B6eoPxGJe+cH$5BEv$w-SjtMzAL>Rq0 zh|#?g2hSL16Q&8K;>UG~VXNg>!;PZNg~;#$RQy^u{qW`^t%vPX#K zIW*)gA^LrvFNQQ~|HtB5@lyBcevPB?G_JS~<57jZnb+eGILbeNOhCknk>&0!|r~5UI#?!bOU-PKO%kE;I9nHo@&dLAzLZ*Z) z6QVg*2@y`~hTIy`DE>c;Yu^~MZ-|~b>*v)%RM!^_X@)0_*j~IwzRVl4P>B3djGMv# zrMUjdlqW(z9`Z=YSs}%_x^?_O%-tyd^{#s%#`su|eE53@?3Z6JhiuyeA2e@0T$?@r ze2g`@i#{kHnzeDf>wnN8`^Cp^hkP<*R)})!f{^!x+!Ats$fF_S+0*Rjih=4|JNf&% zxc`|FY-_}qr(@okZP+jVJyfFYs+)?NMDaHQ_2H=xO?^;mJ)5c>Z|^+bE46C1KHoX@wyCjGy<4BJ zn|ig>(^D^-8ax)t<4LJ?ZxvsV+#BmA^qkOpUPCl1}_ z$m_!v&FR?o76anY?f8Gtv9AR8i9@&J|0~D867eSv-HJc!GuQpz*8L*>#HDN=9Phqg ziM%hypSYAA|Jz;nD;a;{Qg-|wcKjPtUErg);fSVb(#k|y6;yq?~D0I zoH`x<(;ff5h(B@abo_62-|vg~6Q@qc|L2Z>U&Nm{bt?Y6U-0{!YOV8%eV=3EMBLit z!E4<2`y%g&Kbp9;8-L!HGp==i)B)nwZu~#&zTcPmPu$v#|92h#zKlO{Yc>9SZ|gb7 zzc1rY+=yedJXp=^zP^lmaeYP{n~ncW$A2K=PaK<#|2!H0M_vE>k_W_%I5r#q2^s&Z z9sju;VsDE1W}SNk~taU+h6#-Dn@?;$La@xRw~pfBnGaUyQSk+_b> z1NgFN#$A8AK=&M(+Ie5?yeCe?jW`n5@#4>WgDDyBNP*!M2>!k#z~7veetCRIYH)gM9+Nkx=J7jHgLm0+XZ)99Uj59x*nwTxiQV{tU-*gN-~cY* z1a9C6uHX#r#DTaFr%uFtv@xhB3nu=HrKayp%j4BjubY~>wRs-TOub`jaM&}C$%_N? z_^{MRr9LM0X{i;HZVK_n4(!5C?8XoL!cY93nf8MVIDs2DPRr}S8Qh5jaTzUUWt^Lk z_DoK_NNVa3{9Gx|ubvve@N=^~zg6n(QtzDl^{K&c?>weo5{GWaUUN~`u>-rX6T9&P zzwi^k!2w*r3EaRDT)`RKi9;DNZf4B6GcNO|UNkj!Q|>Ge~3f3<9~73p*gVwyRZ|x@dLl`6TiU$T)+w3z_AxGEMv}j zGJf!xdPYB6IzL}7_0-hhM!u|(=UIPsJO0^0vjEzbM5ypQLd=0SVUC%)Wb9D2c-+BxCR@6&#GU4>lhCmVj(r}3}l zf`0x?`q59=SHV{LZxPRaJ>DgFPz7B3#fHBRNdCiD)(5O_`Xvs%vXkfW;5R6mbxp5u zZFOGOG1q5aemM1+S=WwC{knqQJ&$=WvQHi#m--{272EHpekS#x#JlLn%jw_r(5=Rz z7x$f*_7>|h``~BOuRk}a8He?95vM^t{%hg`|9cV3X69Tf3v=T_e8K_lNSnn^GShTJhsOP%mQH%$!@Ly*&S{ zT71g3=WXf#2B~*UePBWFoyWVTrd~FKb(OB=eZ{ZCA7M=WDjV;rjelCkvr4h)hAsHb z`mAbk>6Sg0C;z*Fc@^eZHu>J^de3-&Nj%Z?SNi1>`8n&OZpFFYJhu#4{I5!XiNhm# z{6DE5Nd42)_oe=3YMyKSTOPBoKO>IC?*meAnR><4JXatGZjJNxV$#j?*nwTv-FaRC zZ@L-l(dNPq$Gtmo2j|1GE{qnFZl1#q?6U3-5BOf>3q9%s%!?h4dw1eq#F_Wz-OPE- z#k@s(eB50g@cls6mu2&Sd6?Jn?ryy6;m!AG?<}bwFfQ}dv%}|m%Ll$cd_=Af?@B&2 z`aJ=z<37e=++m6D(TQK#^^|#vxRyO{aeU7|XWka^Df_r;_`}Px{brt`{~Ysqr|n=| z=4n-rXIHD^HhQ1uANx6Iqd2yDo$>E2{^W5n4xDc_u7iHxlw99jk?X@Eeq|psFLpGG z_qttLpN)qDb~J1MP8G2qJDRnBJY2f{d876(SP|Td_+Zy~ek@Q?`>|_0`5q30cckw#zFXB=5G4mGfDeHK>2IIwbW!cf{ zxcfC|Ki9Q7Pg(c9C+u&8Q(RY;9j%UgOM~`vU90nyb>F=W+Rt@m+0p8_jc}*-npWp2 z>%Lms8u_^_J6avL5u0nTX?32m?yI$}k)O-5qt$Ug++hB4U90nyeIND}W5D^c=O4E5 z*9qRp=Z#Zuo_c0#eqUm@Jl-oc>xe`0_^8y!ran3K>8a03eST=gc2;V>pUv}@OA4P~ zn8)X(J}0#7Ju~$?QZwIM^O)abJt&X){f^!9n7{kCO&)KN8XUkybCn^><+w$j`*_ZE zAUJh82Jpjp&xv;t4{+)<{+|y17w7Wv|HTBHI)y*q(;xKT&+inMzzN*ijRSqAQ}zwZ zIN;VU{-?U{wp4e)tzGki*v9G1G>DL=st9Gi{9 zMy~rU@xB;8aBLQT_7TPLeEfgc{9|DPj?LnKwd;OI_z#ZF;{Pq<-x2)5u~GadWZi$( z_;&<TEpYz_|TyLK1wV1W~ z_dI604t0e8yfJm7n;OXhv6rG7fJaUUN8-oI>^xUG~LJi)ifdye^hslxr&8vlm)_b-VP z--iHy@T9(gcTw{>^@seSAM;$#xiDugfXlAQ+ci^zA9zm6^F`b_F7{u(PqAv=w`b}j zQqv#!p3S9kUa`3;_2*K5CG}mY?@s;Q)IUu9%ha8YFXR3=$N!Ji_oU|U*kSJ{^89tF zuS|VGYU=xOaV+c(NWFe)@B+U@@_eKF;Q8}<;-u134=EDj(E_t?I9&eg@X6jv1?~xjR4$b4Yq&`0N7ekkU|HpD% z>Lv3rKlWfB_F_N&;2-|tKX@#beoRbV56@Q5lShl>_)DjrmKq+cpU2p{bsq1Sy59Fg z?~2dM!2jkP_n@2~ocVq#_Fy0OVn6=iAO7M${A{$ow~Bx9zIySPnts9$)??)BX8HM6 zskcjAR{i-#jIX#-x0#Rmu?PFG7yI#tbv$^Fx1TNIRrWFQpD*#8l6slc__0bJ!zX_4 z`qnsK2LFGUA-s4m3073u(v4QV&ZE{$3IMiPg(#&$gk5AqM!1|5ee? zXB;LEXdLJdRl&QS9Y4r8R3#UvkKazaKACz}YWmm(dHlZAx1_#5^`oil`CXOscftTifrcZF|RQ#S!e`luu-Nv8#_fVhW&U%x4L$m&5efn7X_h9NfQtubK+nCqR zcUju;kJO#wRkz03C-Iw}dacwOr{=jD&(CUo@8!=k(!QrccMGdB<~cb1=p~k|%za;r zxWAJ3{UtT|{--=fcdL)^e7_Y8dU4O{fs^Ltx$vEd+hXSn2{@qh= zn40$z_;hBTFN5>7V=@o(TF;Kp5naNcd5rs<>j<9fmbL$7T;?(UbK(ykpNu@Htrwm8 znQ@uNdUX7GK23jV7k|cKT;o1E?nPWCWhGogLFyl+ z{?F7rU*NiX^Zd6{Gv3{K{MFQ7NzJ|(dv4G3pG%G1pUz|axjBz-OpV_k$z$-jERWe& zgYP@zytr{n>f=+tIrSl_d2YF99`BlZ`_!*Z{p!@~r(PrVN~xDm&GWg1^LW10yhp0X zSDzodma%^NdElwM`CaNqQvW%1zdrw!PnN;Q0DeB3@qQxpA5+8YU*s|MbXe=dDuIK1 zcWxMiVdenu*@mqSu&y5F*xPs4KJsAL^1rO_T|1|oQ~ksN@?co#t6x!KPgATI_z|1a?~KR@#{2mTKS$csVG|DQVc|0nkS+_!9d zXVzWh#h~Ut-=i>JI>Q&^IG8w+Cxcc8_+FiPJD9w6Tssq2@?_BR|I=Rgd);pijLQM? zWKi;dV#Hj&Jnh(z8+-TA&h*fJ#*sWBZ~DCsY?1lj&oQy>&dY9dfV}DV{6F7)-+kX4 zXqp4$O~2YvfVCT#)v%XxBS1s&wSkf&4IQ#Kpyp5{{OdQ-*)WX zm#f>CyA>Pqs9*AbvE29be<-?zljm`+3?c`}Bl4>1b%4LKI*75bPp&b&;sAM7_56R2 z*Zp4in*$x^0C`pQ{O5ZMy{eO**FAa=IY3@jHUIgZ0^eI0#2DBo*Op#!fV?8ls#XV9 z&*y%JgZv% z-{@z)&VO^DbPkYbRm=bHJNBi=-hH{ceYsn)A2IY8c#huzM9=DWkO?|SUr zZ+q8otAagwM;>-t^LNPh%XL6iaIqcEjbY#bc}E_0EB_~CzE4WMXzHgz4+HM@(Q)o2 z2goz>jyxnU%dP|SWWG;My-@1wod2$W=0NEjAkWA<@{qhNEB{$%%$xbXK)k~}4E+s%LW z73?$Ecf2a|oO-lu9`kpJ{wt0LIUe@S@$Ci&$RqNKJR|SOL-LY5C2w2Jf7TD|E7)hS z@8JItFO_+}TPaZoux9+6k%8F@z@l9%Kuc}pHQTL*ZKzv-!{dfpHj@^~MsJ z-^-+4A@%gsGgABagq$N~<;Wn9OWu%2@{qhFPsv;IxUBr=c?5Q0Cw8xy z`Tq&$zw4hlFfIqk6Y_>UBCp6Z@~+qUPaR@if}PlnAG>57c-b5nmjj*XBOar_)+6Kv zc|zWhN8}ZGM&6Z`|LhysN3gG8ox!}=fnDrVW@Nsvk$TUF|ILB_!vXSwJRxt$ zBl3zoBk#yV@{&9yZ^`3k*Z({>Ys!*r;Nd{bAUV`FUS+}hCCv#$TRYeJR~p4Q}UKPZZ`k9exA(N$*Fn1$a;hI z2kVg)^E&n`>|fZ=u)aApuN(IG+yCLNfv062kQd|$c|#tNSL7LaM;?-w zQs0;QXQ_Xk`k~ZMq<${-i>Zf=U-q}TnhH;N|NeCH=F!yjsbA&s{i%PD`a7w=mipgQ ze>ye1zAlfC4y_y`56BDhguEe-$Sd-Uydw|EOY*dszwI8+o%T{+*k`csU?0N1gnbJ8 zmX-7V)l#pOdcD*erG8E7EmCitdfU`HrrtI68&dC;dY{z$r#?9KQK?T%eMahYQ(qKX zIdf_1t5Q>!KAOiLPkl@3&!ql*>N`?@IrZ05e={{a`CcBwpC9G%b5r^BCU1BX|YR z;2k`Km+%zc!ee+1&*43JKwgk1aq#QT^$J}&i1sZU9L zdg^zkJ}dRPsoxWNB!v3$zC5OmT#?6DrG}r^sV|S@vBn%E`ANY~{00Yb0Vi++ zM{osaa3>DLg*Xv6;z(SHGjWFphlPFOJUoFn@My369G<~DcnB}yDZGWp@EV@Od-8z1 zn3nU9H{=m{MV^s&itq5l={%rN2Y#D>SI%%kox4*r=@;J>UX6+J2gCnm+%zc z!ee;-{3P;tmhs z1w4T_@CaVPvmL`9<3P;tmhs1w4T_@CaVPGk6CN;pJ<>Pvy?Wso^oa zhUYW#bMk<^AWz5}@`$`5&&Yf7P;>S|n4kT}0%-^KVn6=iAO7M$c&wiHgBSR*Zvx-V z^SZ54Z7ydjUsEAouI)0kBd?7=?j6!zl}>zQQ= z{!hze_CesaR-Ruk^@gcmoqDs>Tc)0wdb`xr>s|AhdQN?RW1fcx@B*H|8+Zh-;2FF- zA;&p6^{J`fo*MqXE05v#IeC0;>X$T!<5K zBaXzCI1_hx059MPyn#nEGd^3UhIjA~Ucys&3y(#I`vyqAD{Zf)bRI|Jci%!{hfJUxiX0Ic=~ll`ibA* z050GJZr}*6;0*4>fw&MS;zk^aD{&_7@Bm)G6LeLdTQ29)Kls!^_Kd(NnX$M z7vi>cp5He04ykue4G-u8^ac6^JlZd>KQQ&dsSitiL~8g6U*Yd@`S}T{PfC4e>OstX z`Sfgv^xM);{00Yb0Vi++M{osaa3>DLg*Xv6;z(SHGjWFp@B*H|8+Zh-;2FGIEB%3& z@D$#{Q@`SvZkmswO@8j7wG5we?^#Z9ENj)X?lBro2Q7@^V)Klu~ z8hJhYEaI|Jo_}@f%~EffnmBKh$JBlLz^-|Ix753*-YfN+Qo}d;4Sa;3hvs#Mr#>?E zn^VK{bMsg^GKlh0`c=$tZ~zx@0yl63S8xV*;y_%86LBMs#FaP`cX$9V;0e5eNAL=s z!8>>eFQ;Z4;q5Ycyku&4zDORE2jm5LGFN^+XnOASi~7bohx$i7q&`wFc|Nj2-ajoh zxUZJSYo=Z&_4=tdOidl9u2bh(=grLPwoAQZYWfBJgMI?v;2(U1pY){za-4%w!)thc zVVobtyf6B6O!|-C-~cY*1a9C6uHX#r#DTaFC*npNi7Rm??(hI!z!P`_kKh$NgLl)? zA9x8*;VnFd*9+zK@SZ%FJI@bVzK`b*^^W>SJ!Bumz6cz_b-Bcg_0{w|UM2Oasj1V{ zZR$92-Z-y&P3p~4(+}tm+vIup1i#?huK78AhyCRqc^=-vV|cx9etuQxVy+D0@p0)d zeuD$JfD^cZBe;SyxDyBBLY#;jaU`z9nYhCPcmYq~4LpKZ@C@F;!==)%#Z$vucnq)M zIlP}MuOD{JK4+f91DwEZkvu;o^%AL!zmOv)-d0&>!Fr ze4>xQH~6md)mpq2I@EBgx$8L!01~K1@KAoC);5Rsc3pjxrID#uUgFA5`F2sqr z5l7-moQXR;fEVxt-oPVx1<&9eJcO6<6yCyPcn!~oJ>SRkADkv7UnZvp*M;+#I!oQ9 z4il&4^SY_2S4vGiXFW%~r~bna_yT|66McmJBJ0W5=J@m_E;Q_pWC-4Ry!7F$M@8BW4gs1R! zLdJPKzWejBzsCTM)KTgxxD$s(6Ys@RFPWNp&3cV`PJJi-GxB=)0bkb2^Xv!V7wboO z2M^&TJcYOPDf$-t{!)xTi1}Xh>8A7(zrg`qzzN*I5nRC;+=&BmAx^}NI1*RlOx)oC zynrY01|GpHcn0s_A-sgA&gZf7xl{N|%)Fd8_57(9OwD?XdQJT%zRTow#2-Gu5BNf# zST(O_y|`u`(_iQ}>*e_kQp4Ac^B6wE@6W~gVy+D0@g3HfirO>&cxk$*PFcS7u?|qyn#p52l@zo zh37-{>I8j>K1IEFBzfB`?|uE)b(4B>bK=W>qhI)EJF8uvs1Fmf->{CPPO!eDPOzS! zUa+o!r_SfG^SNDop3XY3VaCh(Rqgy5wD?dj*k`hTXqVT%zqz{cc-DcnTn8%g_X7qk z&l~k;dh*Zt+g$#33a{s~4)FZ3(YX0KuNykSfvueXm3Z#?$E*W9zv)D5J%($ql1o&VH<(@X3Dt?)q<$>*cs%px)vO2X2jE?= z_%(rK>RVD@nfj8{SERl=^@memm-=I=Z%oZN_vEqjxte)e zcKtjl;y)}o*h@dpO`OY)r^g@8JZ?4acVzwVWo``e-27g~-xKFn%L!ubz*Wkz-XJ^HTT!QuaS@3S3pwsvLXUHoWoS`Hz23sI$*M_`5g{I`>`s zD!_qP(uQxR=6%k*aXhGN?q2%E^LY2x;q9&6j6M1N`^;~CALF#l?-iZrU8!?xC(gWA z?Is62SMO>Kzi+=o#(%%myjSD*qdyww6{jzx{$lFeQ-3z~t*LKL{jaGnPJLKt^|x22 z=I<}|vgdj3YiE5k$r)odd(UpExw@a4)#$IeTAg{QI@TnfLtv48Eu5 zTN4jw@-#_?$<|*I%;`^SwM?OCN+C4S@Pi~1kuE(E) z^7C%^(ob`MOFi6tzBBpQZ5&TczJD*Y_}HkvH?QaYF#ji-JiIrO2P)^z@ln*ZQZi3zUI>mN7_YMkwtYuIQU;O~S#Kcwpb{04{7F!DL)&Y*MP<3pPN z_&wi_Uu~k38snhs>I{4qJO~Z5_ICTpDiCK4^G5!tl51hcQ-8j%^ zhKa+S@D<$J#h<@t(g~Y}X$){{7ylbwcN(fYMQ(vxyZHai__u^VxV4J^eA#zjGX5>$ z4{qStY#cU9{EK?4eKb9fi=0>@_YzuI-DrMgqZ9~_&-|69htBlv@3 zqxkdx-Jdo79l;+Q!F9Yitn9wq5!{P)2wca*f7!(UMf1NSaR5hf9S?s#zs2}>1piwT zM{sRa&&)h1=8=z^TaWVItkHegJc;|Fseh93Gw#jd&hrQA4tUO;xYo;q$@%%>so$2` z_&10D8HwA}0-odL!2)?beY8zQm>yHyufdfJU=0IJ^pik{=AO3t(eDaq~^KmYx8)g)Wqk& zJU%M*iK+Si)46#(E3~l2zuWV;)A-+(<6cvkkNL3&`>+@L@dy9#7yrQne83C*;5GO* zs_6$ygx$J_d}1Ev-7-Jt`)_=2jPJc2mY*M!`sCE`=-qkzh0vYGUgN>%3v+ztV}9(x zKJ3MQ{J}r`#eeVsAMl!#el?1Bz3YnkutJVYo~@V1o2H(bdY9CDq{g2^^Y|^P`Tss& zjN>x!|5%Poy<|S-#~$p%UhKyo{KH@T2am(Stsz2X|@fBCfQg%ETpSaAM{w$Q5_>jkC z@&BcmPjP*+pr6R&$5TI&`mE5{K1}+7^uLT8XlBe8(vFX%9+nvVy(0J%tC!QBZ9@-3 z4Dc8KtD>LJI7}YUIM5%ef_FVTevomfN-j_zznyk{GWD#~^sx)__g!WqmHML8 zCxzDiyQkhTHSZC8M|2Ip(XWFF?Vo*kbfx`aRT8235X5j@u|YyZo*%wzoL#2-99 z8F^4!FFN%z<1&x+==k$|n*P!*{*1%8#(i|$i?~e8e!zN#{?GoX7#GfS9rrO#ar~+t z?~s0@AIf8NRqeK2jq%zz87FhV954sW0dv3{FbB*5bHE%h2h0I;z#K3K%mH)2954sW z0dv3{FbB*5bHE%h2h0I;z#K3K%mH)2954sW0dv3{FbB*5bHE%h2h0I;z#K3K%mH)2 z954sW0dv3{FbB*5bHE%h2h0I;z#K3K%mH)2954sW0dv3{FbB*5bHE%h2h0I;z#K3K z%mH)2954sW0dv3{FbB*5bHE%h2h0I;z#K3K%mH)2954sW0dv3{FbB*5bHE%h2h0I; zz#K3K%mH)2954sW0dv3{FbB*5bHE%h2h0I;z#K3K%mH)2954sW0dv3{FbB*5bHE%h z2h0I;z#K3K%mH)2954sW0dv3{FbB*5bHE%h2h0I;z#K3K%mH)2954sW0dv3{FbB*5 zbHE%h2h0I;z#K3K%mH)2954sW0dv3{FbB*5bHE%h2h0I;z#K3K%mH)2954sW0dv3{ zFbB*5bD)1Y@Y;K3+kRc>k&t^+|Hk^Cco>J-yyzAV-$~wGpZes`sxQl@cCG2(TI1L` zcIH5DIk0Hf(a(lfzqc5Je&E1M$<>di_IjYVb;Nbt_?rX$!-0jOZYo|s%=+06HQzRR zZBmZ|Pb80bOkTSd^-nEwtQ;$Ipt~ISxck1JbqpW}Uhq2LpX++p)9%(&<88eAfdgyj z{^;cato`-&hCV-h&kg>P{9YmX-VeEH8{I?90dt^C4xF3y&^>YZdg3s~{FYn?l$lc= z+cn!9FbDd91E0xyIE=l(J`dlThwlk4?dJe~FJKOs14GV%JKg{N-hT)=aLkbBeLw6? zY@_$k=0Ld|`1}yoe((E-Z{J^xx!(hnTUT7SjjuUSeGc5}{_p+&5OUzx>9cE8^=p)4 z1t%mH&?7&*Y-^>q&%z8>h9Ups48_kXW{ z%mH&?NI7tI`0YMm42G2h=MHJ!5940PK6{UE4$R4cOWgmx{uyEp+>yS!F3edMjEC_s z2L_)5?-}CSKdgIR`~APqdmeND_p{&NfP0-W7)B19mE3nv_}`vjEQSw@mD~f|1I+<* zV8}S|cK3fj{|^of`X6?7QFp!-4gOB=5ZsnsXmyJdDRMa^MZ_|K9%x2k3vpuHWMR@4cTn zU=9ok2X;z7hrRCGf3E?=f$Kx0hjA}ppB;B|V7NH2mHWT$(4X{~sK1@1GNc2NRRkybkbQ z&>S!a29yKTH`o6;>%Z|Z9^$}XLZtaW8LJOy9jM(C_-F4C&4JFF|_kTb8&Kl&}UvBL;zQ(s62VTk?ygar44wcsd z-B=?m>i+Nk@BVKNm;;sKz)cyCZu9`pF}!Py$?pG^%KLiz1E2Ta(Hs~)4ji5J**$uA zdvuwxpOE}|CD)s-F~e759COFq9H=G-{+@Mt8LtEUI|wCm0G_!HRI?5^E{=;iFuWW% zt;E`2=Kj)S_s4UKr*oa@8Z*2##<6$o&4Fri;8R(j-B0_YpY~gv9&!Ko_W;ZRbHE&^ zBnQ6b{_p4i!GW^Y760SBuVmhPKUU5CnB(HObdv+$cmFS~_m$OC`)OSM?@2c~?YUe- z%>i@392lPi_q+eQ|ChM`|B3hi-v65e=72d+Ne=uvd!O^Zl6mjF z(fE5KV`Pj5odXZM|9k%*9O!i4XFR@NGU$0^|J*Cg0dt_=IPjGFe`&ps9yrYUF7N-n z|2GHB0dt^|9N_P>40DgN@8#|Pzu>&DWZw7NKE?KWZ*2~Ao&)@SR{mbAdtrIKa2T=u zjQhX)zd2wIm;;sO!2A)PVeC)#x!kz(|E9cl@BG@`m>N@az#QlV2d23Hd;L=a2d?+J zztZdePSg&M;T~iT^fw2VE1~v#Z(Z)*x*zcUp!>i3zd2wIm;;sOz$)(l-v5`tfyG5iZw{CP=0K%6@M`ye@Bd5Sz&SZ) zC)TJQ!+RZbz#K3KI?sWvN~rzbTbH}H?gxBNcmJ>Sy5F_5^R?8N8qGP##yh2MX#0q&Xd~D13LRWV86t*pK`&rx^|lb=72dc zJRIQvAblfOSsXac{lBIDznS~L z`@j3YIbaT$1AWYa^U`*IAE3GK18fku3A3`+Wggeqngiy5IbaTy$pQY4jr)Id{eKPj zf3JPK{xb*60dt^_IpF<&bA6w6(n{|C&ig*j`!e?w9@~3XbD%#sa9s8=`kHlP>w#t6 z|K0!H|IGn&z#Qmf4*2%~8|(MQb$7{c`IOTC&ugn2+z#OP92VTm)#XgpPX5HL#fWNr^`}vPff7StiaR2wZ-yARp%mH(tk2t{p z1E!BzxAq+1SA7)oZmlgnkM}X=fH`0eRF?y{WZ!b%Z>{fx+t1wpz3w*$%mH)29Oxqs zygzNFk6E|24)}f_#oX)h>aNEf8^^{RFbBHDfwQ7-x#zc!!FSyMz3w*$%mH)29Oxqs zyd`XP|8E}yzDLomb(`n$9>E+i2h0I;pdAkE>Hgo5_0JuBwB~DvFTU6H#vCvQ%z?q+ zz!vWR9qIqKx&QmQpE+O-m;>fOA8}wt*y`v1?PG9rAI02j_rX}ZJC2T{IbaU7!+}NI z|2wk&`4{(pulvmbbHE%h2l{{m&!>&v`?tUM7lt3|gP6CoPV~LrXP5)#fH^Q29N_QO zy8m~k|6k$$?{&X9U=ElA=0G2C;0I}=`+sNp|9kr&=3cuG#@gL+bR5kAbD$j#e98U4 zBk%oLKb;$R>gRT1;(LvsIbaT$1Li>49QarF|Bm+mGu;2Z_VN19954sWfj;2CrD-Gk zSL^oP1DqUqdjIcrd)e!Dk8h5d1Li=x9C%0eEBF2O`abx*)&1Y={#;YHo14Dh_1zpW z2g>BY5$^vTeg1#A`@j3Y`@cD04wwUdz=7SeU$K9+Ztpq3{)uOqYc`MVJ%Kr34wwVx zK)W1xwflcZ_y2pj|9jmZ9B4N$eZMg@2h4%8IWW!rzoY$sSNDJSKKFlfz#K3K`hWuq zWWVxr|Ms5ygWtA^XW8pEkMF&KIbaT$1Li<09C$X@CiE@q&h&r&k8&$9@jcFEbHE%h z2h4%8Iq)0z|BgQQXZ^I1_y2zGm$_AT4tsp}33I?4XoUm+;r?Gr|6j}f-~HeH-yARp z%z+AX;NR0W_N~^PdH%n0;3_OzS(Eu5<7N(+1LlA^P&Nl{aQ`o5|G&KZzt=uq|Cs~k zfH_cM4*2i>cV@p|d~UM1`@i$P!g*izzQW^s?`jTIp9B1#x?=zGaeM261>OJM|K0!1 z0dv3{s4xeP%0A_O-(J5j;x{pI^%}VPYhcI7F)|0tfnnsp9`668y#HrC@M4A6e#6Lj z`|LVz4wwVxz-$g|>Hc48|L5;3nQyK+=72e14wwT~;=pR||E2c-C%pgn`#*EQ954sW zfeLb9v9O6})z+QI;-L!0yh`g&+v5F@IbaT$1Ks7oOWBv^cK%*Vx@%V;gVdZ4Q_NRpbDDskT@7=gzMM{?q;6>wa^< z954sWfeLZpn`w)Ce&>6BEvENYDCSjMhuS9ZkIVscz#Ql<2R`fmU+Q~*)&O_A|9jnU z4wwVxfH_bh4qThI(5I}+c@A)Ag<|eCc6Zm<#@l$C1Li;#IdGx-e^>VZ|K|Shb-y`a z4wwVxK!rGPO4#B(fB6{PQlXevaUE)#ygxDr%mH(tyBs*g{l6>gpBvo&z3w*$%mH)2 z9Hp#&-2c0>{<+fq-|K#Jz#K3K%z+AV zV0zf%{eSrwysttrui`q?HhF(!4wwVxKzBK?ko$jE)<5UD|9jnU4wwVxfH_bB4*WfB z@ZP`ty}vMiSBSLNx5mI6FbB*5bD);A-#xyZZe9jqd;M|L*_hfH`0eRDc8LWK?sI?e<^L>r{acOoujAl2m;>g(uySBM_y4Z$ z|JO;phm~*k+qKplFbB+mYH(mV_y4Z;{~7N8e(o0>aQ!m|=72fSYYxok{@>O9zk>U} zd!PHiIbaT$13l!xpL1^BOIvs6{r{4IyRh!{y3g}F2Ihb{U=9o?2Y&1R-_?CT>#K#` z|GoC{`p+CN2h4#Ua^U+pH+{*vJNy4hfxGwr!@1`6UdwSe2h4#gaNx`C|2^6Nv(9>{ zhikto@Xt254w?hzfH}|`4*386bZ5U$zc1>)y8rw6pE+O-m;>fO4><6FoU_=MeB7ON z!IM3Rx!2mgS!)}2<8BU^16AU{`R@Nceg6MP_kXYZ%>i@3954rZz=4xvPVe=5hXKET zQKdDZZSfw(954sWfx+j%!S4S(UH|-757vCn>%q@!$JjA82h4%-IPg06|DN{$AG!bg zxt}>;4wwVxKo2;uY0T;8|GmTD|LZ}_$6FKnbMG6>0dv3{7<>+_>i*x;_0Kol|Gn-v z2h0I;z#QlS2NsJtz5nkW2K@acueAq%t?d{)#^!)IPz4UWoNJA_-T!;K{`q_l)_hgq zpKWj*GzZK9bD%dI;P1|O@87$M$4x)pcN;~HZQm;>g(aC2ZM_y0=t|D(MB_ukk0e{;Yb zFbBHGfladS_}zc+-u(;vgEJ0Zj}P~H-0^q(%>i?uTO3%){l60X|GnM+z3vYVbSv*Y zkFhof%z;JS|0~h|U+@0!-sk>r4wwVxKo>ahV$Lxkw4eR=9s|CI;WfF}YQDsmeh=;o z=lP9NJ2#!1=72e14wwUFa^U9ZHyVGD>^t-!`VxJLzC|A^t_S>FVD@@|^%3~6equev z`f8!X8{GN*i*@sSnKAd+t|{h#IbaT$10RZhBOXl2zVoWo^d6w;0xYMChn`IE;HsH+cm`;FbB*5bKs2ZH%q6c57C$C zQ}iwR7=4XCN8h6l^4@z;*8$D;eV(hZ&u6{0RN}r&>glPSo6b#hz#K3K%z-jF@Yd`% z%N6<%eThCr-=dEd-wzDSI)J{g(aC6|m>^Cc>UOx4*sd+9y-=dGv*BV_1&^P<_c>wm)_j$kHYTpN6@CN^BiTm@R zotwtM954sW0dt@%4(yiwW?E|c5PgY0)#y5a{V#oy{V{!$KH4wqfa34*(f1quULWhG z;(i}|!5jQn$~gSl{om^!bHE%h2h4#IIk0t%tzNuxYTiH7m*`XDtpnKW804 zA7dRrpQG=w4xlfxKc;WeN9n8VudDVP0J~VPuzq1Z!}^Bx4t}#9qVMzEg!R+Z#FOVN z^!-)yJn{H#9CteA9?zJX1LlA^U=BQ%{bj8}-=PoDmskhTx7feZ*XVQfJ-#1EU!+g6 zU#5@JSF83Mpi}#P)<>+DSU*imym`*TdTaGOzh>%Rgm#`919QL}FbB+mPIKT_*Evnc;3&x zpY_Pp1XEx`Vf7IbpU;fKGy1U0G^-IS9uOV-|cn( z=XnHn@w|fP7d+44eLv4Tcn`w!5cd7xG%fLEJq50;x7hcy9wR=TUiW!C?52YC)apQLZnN9n74AAr7FmHv-i*oobI zuK~aC6Tf*L!oHv9CEx~*yk}v(1@5fJzMJuJo(^}OI{uEoIbaTSngds7f7u}WKYfQj zL|>v$@w}QoMqi`P(f4@%O<&}Dar904Xuaw%j(Fb`S zPM_rafD7fk^i}#SeV0BwS6*eiQnJ=F1#-RH*f@3a0YkcaF_eP z-}{*Z=72e14s?(Mf6K8p53Sz1VQTsg?*Z1#^Ykg+1JK9lYrNm4@9`dhzDS>>Z_-EU zt9%cDzT2zs0d%YXgCn?tbFu&55%_zJZVb!;bHE%h2Rg-pZ)9KDB>Q`-{eN2C-)jFa zeh+Y1`hTPQ|IfPrd;Mb$m;>g3InY54oD^ee-@kF{R{Q_3?f(}{ycS8#bB(gz|9>*( z>C~Ff;}}bGz#K3KhL;0B&wldi>|gYMzyF`T9^!ij6B8fa|4+{2V*fAu`~M%!csXZ> zH)kDt$KD(;2ioDlE7?c*z90R6qwH(+|6%|Bf3g4f`uG3O&iJ$wd*AEYVGfuB=D_fB z;4|6(Uz7c0y!Zb6{@<|v{y*P8=v(wL_O*@v-5>VN^QT?BUuU09-|bc3FV4$) zg!KvE17Q8adWQ84>zziQ1Ar&L55V79V*SN)fXx!G-(*~!)5Dk3j=5uQ4wwUDap1n} z4?ASPDE586=f8e_PT!}`(0AxV^rcSi``I_sN9n74->%qydwtA$g!KvQ71l5K!|wys z`#vD=19%R=?@Lb0IDjvB^W24a5Fg^o`fe;P_%qiEbHE%h2L_h|&t|`$m70BGt9_ro zGo#R#rsXmFe)<^u{w4Eseh-fK;q=Av_WkU$d)@Er&5wPoSLRE97f6kNJO{vk@Zj$P zf*1IKC-|}s0Dt09Tn7-}TVj6YH{b7ct`BamJGPFkIWXKD`0uRu7pLAnw0c(Ydws?4 z^{j0hu@SI}a#07tN4uHS- zUwj|HI-vNyK=1|cM%Mwvb7tCeeQ4E)J5%3X(BH`8qP{23qraKQ_4K#$^P=YZ;&~s> zj*D?H4$4pSpxQj3e%_dM?(!z}WqEx)eQADP)R*M>^HLuXTD^!q&;Fi%&pwd%`aIv~ z_xW1w`_pq?`VxJgzQyxw`aXS*zDFOVFVZLJoAc!O6H?2^evrA-PVC2@;`;#n=Y0V9 zfLF8Y0NxAmUWV^yteN)ket`7=afb)+f_330c^~}QG>_4n=P`PVJVtMs$3@NOeBac1 zHm{9?aZr6T4~ByWudP!b>(xu@ESf$~J*G}mw;S#EYo{Ib3Hk}dabx{PT!}MrXFUKP;K{lfC-?)OSQo%EH2f=S_*vBO+In`qGY-Z< z_02pO4j$0Qi*=FXVjbm}dP^OqE>owe+thLTJ$0VCPai1m^XdEa5%!ntH~F{G`xEoNe(dp$*xc+oV9~^5O6tW^FPVDj)XS!39YCCj8*wDA_0|Kc z=Q!{KzOXKUNAL=s!Mmb{m-RG!ww_(@jDvAd9Wf7vg9r5SdiAkbPdTRUQisvhX*6}5 zx=z1mA4ngdFVH9G8|)|PEA$!q4tEu8&?{Vn@l_P^|h>HGcmp1+ZvKiV~{ z1LjG5=1o1hfEW0IC-^Rw*RkGW{l$8WI1xAENL-0C&j*MDU*HdXLKoKu@D9y- zp{U_2e72rl?~H?SP`xk@hJy$6@nU`Cm^xb2)LrT@nmSG0rjAqBsq@r*`T%`_K0)80 zkI+}>GxVKy`#ybev>rJO=kUMYI$(k9^9!Y3Bz18;Kpa?)5hvnC9EmH>2Z%S{=YS9J z1HQ0Mghy!j1>cGqJ{C26E%MsOv+JI5Fb=AJ=D~3ApjaO{F4j|ysk_u+G0J~y%!&^2>sp9i;j^O*G%IDN0g2O&v#5=h5^5`U3r)zCj=1 zc{6>6zSHPFe?s=JM*E+yo6Uc4o7m=hfcOzl;!C`VKYS>z3*ZaSh2RsKbpjgx)zk2` z$a5dhj=OO%4yu9X!C>=%zFx0Ra-O=1rtYGt%hYK!b)33Rou}@zucsf-C)h_e+V9!t z)2HZL?&Guh+bJA)E&$HsJs)6w#`gz^C-EiT#dQI^fG6+<4WH2P3=RM4X?P32t!MMZ zI1CXEC!TP^Toz+s4wwVxfH`0em;>g(@NgiW@33EDpH)w@A49WWFKYI2?CaR)vF}5( zALKbc?~{4I%=>%ZJG0MZ-^o6-Q~O$v)0yW4Ja-{J#EbY5PvT3w(L67J7ijnbe~KEO zq2XUWU61EJKbr@}VSsT^kN@BYKQPzNe)`k7o_OMMi!m?<%mH)2954sW0drsoIPitr zZ=dVD^Uj;=?Qeg(_P*@1>S^|6MP1ytvyWqc$9@mZdwTYR><`&5vVZh`yfgdpe!-Er z!vlB$PtfoO4X?)2@DUA<>+#*^XYXgD`+xSf+va-w@y9I2z#K3K%mH)2954sW zfg$6-gMawLTp#+-hvqu%)Kl3fvhPB(AFHR?x1kxwdbsxQ{T6Td&2t6o*?ct)1CGNb z7r$?=#~yuTuE!pG)M5>+Lx|F3@az+8_!@~8h_zVy{Ek9_jBPmetO$3Kod^UO0NPd)wg$kStp zK7Z<|r$(N9^2w1Wo_J#9@y8z@dF-*r{wcrt?Qce|_`sDTdInHChkyQGyp4siFbB*5 zbHE%h2h4#gaNt)DJmCF*AFSW~tijJ3%mH(tRSszX|Hz;IIEP&J!K+3ddibG{d%kt= z$onpP|HykUoh`FM|5M`JtV^$&Etg##*Q8wjfsug3IbaT$1HI;e)`qRFD}A5$B<6rQU=CE91FG?V`s2fM z$hosF9C_x?e;&E=gC81MWtBZgR-N&tkr^SYtP-;Fn?_a+S!t!cN2X1C!_LK&_kXbIryMwM-Dvjxsm<%dv0X&%|1S|>82kax%861kG%1Xe;s+l8~!@7 z+irgy*>#uaMt0cYnUN)y*lpxP*MIb%@BjBb$4+o z-0P8%J@$As_W6&-KL62?4K}!LWc~F%GVxv3DNeauo#| zKB**-LI@!cLN6gS5fDLwND=VIhEfF-MZjO_qBI2+6@C%1(t8VpbdmrG0i^fdLPt=L zq99G`{_~wXvpIL~wtKeBoIUS8&)zw^r_9Vd-?=m2%$(Vm3lk>1T-acP7Yd6lx^3a~ zbI&c@d;k3f`Tx>*_}rbl4<1jNwIB4X-wV$_-%SHP(h_o@MAN}OW z*#Dn$*4c$oqc$(>yz{e#{r3Hv@%S$lcK^n6#^*n2eEw5~HP-lJVfEGjSlD)(rwiZz z{yz$b9P(me#T6ef3?BSQA&TxZ?=1>voqs{$mfLQP{eNjZeD2P@l|1gel^S{mK&;&I_)k3JmTcI(a2ZMWPMJ$V0pk*@FdxiBrK`PW<>bDTH#>Ub`VyI!@h zne3l?_L*qvY@pg9hDJ=+oyGMQ3&H{S5q=)L#;Ykce9i{H00E$-USJ^O5&CZ$d3xbXb*;?EN=zA#Qp zqx)SvkDLC4DaIS0d-j=eIO~cTaeu?brqAKtd++I%=d_&W&+GF4H{Wz)^yZsycDtra zM``H1(pzu6)iC?JO&jl=v(AX#GI9U&rI+f8zcf9yK38I+^V$)ArSw%xSEX}mec!J9 z^!ENso7ZONr&2nasp(o}#QyfxAEC2;`st^lxu)-R-n_YywnI(tj2Y9TYVS1ZmcO4q zZCZ5Il~;Dl`_oT78GomJcinYoubBMpxo27bpY{J){}BzKfezMy{Quvcd#>C2z#knN z`~Pz;ys$8A*v5tRK5>0v-F2=nti9Itg*C@tR~R?$db1|rb!Okug?@dHFg|~dd0%PY zSD8@n+2*}T;rvT4EnG2Yc44PIb}f{~!{_eYTgBtA*IgT(f9~0_x1M{|mC^ib=0*42 zb5C66^X6U^{q_3mBJ=x=W_;lJ=;^1Pimo;LXPS1(Eyl~u zm(W}Uz2#wb%|`aCE%o?<1=0EEofBzX{y%TL5q&EJ~ zTW*P2s-Ggsu5^3$tSh2frYw{{m9h9gWaa`~Y0Kz=`|ppQeBz1t`zuV@md3NN%pQr4 znK@%xbo;Hh6yvPMMrmm@{+n*RA=c9j!*jOjpSj8ONj>xQ)5ZAjGx=9u-2UsYop0K@ zPe;|#Rg3?^JonUTGC%IPM;>}8)^oXXa>tZvzL*A6J~}T>x4Fk$`RTmm{Z~A`%)M1D zua)??czTl`l|`(d2OcP+Cx zDh+i`obF!ez8be``%2^Hb9?Hta-5$sKKJUYu8O7^pR2dF^B?OWS=Z_G6^_3)#dDzSsQ9RYyG$Z~C&p$VQ4rI=(nbE{cE{fAU+w^Cs z&HC_z55{cHH@cl|_D`ESIXe5y)1&9ja|hMZRWJWiotEauvAN*9bE8Wtl#@HA^jg!V zcHJxDsxlULJ?o-=HgR?5x%;nLnk(h?Nwa>3u2YVW`oMadA8AAo~n`t(-3F79f&ucoUFa&gJU{H-`yTlH!araBXW^nrlL`X|u2UF3{4C@D&otYo z7y9-+&G>rv-p6d~eTIFXX+rnbG3ymBn>Mv@`Lrpq|996XT*$gzEacB!`(0ziAAR(Z z8S{Uw*pAm6bd53VmpbUc@44r$_?XL#_t!NvZm;?DI`6T^ z9*s5RJk9mj&5y_0b*nwP9;wF-FI(%&iUsqKjrYi}d+|S-Z4cnMsSvV{dh;YnJB6rKMUqxnq)= zSKo!wATO#pCn^ig0nm5x{0p=GggZ}pce<*U=5l#`@WC$KRN~Xy{5hBo$@`D(_Sa{Q`PF8Q$+W3c<9Y51liS+ojteDJKSiaqRNGhSoaFafdvlhlD^5G@ zwD>)H)+{sU>+y>5CC}})ZT|m0Vb%%4|Ko8w!Q+qzbc_bdYe4_w^_-aeg?sP){e25B z{QZT(#Hmw^haXq?;ukM3?6lK#v;BYe{TmbNy}NnuVb=beXVwUqXtsZA-zS++``0Q= zzG7zK(#e;{{@-1na3SkNjX6CwbggUwzeZ zSUCUTz4zWL=3l<*op;{uR&K8R6kcWKd`>m*V9HybTk%w4(`fupnz5$ocAVU$-GwSE^$lHgLD$+4 z`d;fC%$z>e%yWGrZrheiS6ZElUuk}lY$}zLJ0_|58*aEhW;n;R$=&^=^)8hMJ@4VP zr_U;v8_m3Xm+#Wh-CO1I`uO9IRppZ=Kk~gwgFLwUjkNucxu2Xa%cVmaaU3e?$dyYy zOe$UDR<0Ork| z8(bNwf1;9Zs_mx~)KzXI)``^YlzHa@rj?p#$ z{=)O|bu_nM*MH2+!Pi^kw=bK1hzIVwFMh^A>A2sFt2=-0STwT#;fEiJy`sFA)-sT9 z)H;Wn+o*ZLnrEQrwbd8!vGLAY^Qaz9ci6s+%O_o06ny;^2T@Fd3!M}&IgHq*Btq-wI4O-{1wwbGxw^6{$CzT*GuX^ zX_vt)XX3e2cnl)Cgv1ffJkF4}`<@xE@kBYnY-Ff?Mk=C^@ zm#(xrRXnBnacruUlRGA<`CDy2k$&Go{Y$#`m2+mtX}{sG*T?f1lIJ;HRlB#!<+Y2A z{*RRBlO{ipn!YHd?J_eLMeA|u-ch-{X8My9U!`;;^HWYoy=7AF8kPQ+NE3SZM)zIp z%5$r|uRZ^}7nU#aRbw$_%9Kd&YRCBA7khkj|CCEdwSDD|Pky(+%!#?;ikWf#6(+Z} zFL`XGZO!{p--&xO{@+j=|4{3Z{%yj7dB5G}@G%?TmL|;Oa}00T{ENNM_e(bZyp_S5 zp1Hd+clX^`lb$@4!N2O^umAD-f3Gn(%>&STk8gjo@aTfa3R7p#G9G>f^Zr3$z4cBg z?6ud0#^?Xsd_KUww=<#M2Nt&9{&chd;=+(2hnvp_$KR>%V8&H*3Kv{*aqR!y^$EK; zx0A#4sa-yPwrLyOVAlJ4*v#ow{K@vZKD*~;=a@D7v<9HtFJCLKDDST4jPk%u3Q{iZud?QnT&mrsrNYYhSSe%6ek$ycb&E{`G4dF31vuNm9D-;6t| zU#gM)^7-=8`aS2z&GQC&CRyXmnqPnJ*=NO}y#GUHoiok*Ryyn9luXynX3X|lGd}A4 zY8p<{rc93eRW7}xYmUZs*Upc}inB zgwm|_8ua^yS|3oyYhJs?+sl<%TAh~5k7HAse;zh^_N_ z%?w>RrR9Ga{2S_bd9gwLp9}xm{GI=g`v6S)Q=VzpZ|+{W@BaH^?;b^CjF;cdzQ18Y zy*2M&?+NC;nR)MF-g_qA-?ZN?A7A6#3onTMzpl;O4}*J+SJ(6Pu6?d?Z?&1-XSJC% zma6skHU22ytvR~$=5~!0%X`Tys{Ax3c#1j3wYL>&oKVk_%WIcjvr?JsclnanNN&p~ z$ZI6OOK#V*KOQ?YK34frUUiPVq~<_AYS#Q${F;aGq4ECz`1=cS-s=*!I<1tue)~bs0+fEA_VZLTm!RA`?e%nCx$7mr zOLJ|lXESB;M_gi~GJ(=zPBmt^aSPr>f^#Zrt`$rv0AQd+EI0 z?D?ULL)Me`5RQ4v`lgi|#I1!Gd@^ zCYiQ+IO%w$PtV3{TvQCShM@AGXCU15>gDwEKmS=gU%pme-CaX{0lMxhue@CR&h68h z{+d58_U=1{j-|>}pWijt)ORnM^$zt}&uZwip8t2}m7XJ?qwiG4?wC?G>Qib){3>TX zTd(I%6sH*KxeaYgQ;PS^H{XcAS6=nK)}c9^_lKHlRDNR+`J@wGy9+mt54NeCo}Q`Uur8HSyw$ zVvXxr9+xMD<;nvE?t*Z;Gm{XEcymG}>``!HI>vdxTGw;?wI-YL@S2(X@4jni*zEX~f9L7Ta_7~Z4eUw=^s%_{@z|6RM1 zu(A0F@Pi*3Km$Q(pq&5z_I`U8rp%aDIQ6VE3O_pf$ifegJiO-n&?Am4Y&zkk_&D_+ z>~p~P3WptaMB%vqJHGIn-wL7knQ0q0$ zGrnJQ_qF!KTW`JDl68&=ltu#`=Kr5F z<6a?+2fFcYH$VFoo4!_s?s@7X?D${1eaPd1@%N<#x2`~=^ZR>SehtZ{yY!R zJN|Axf`cqSr{~^=2ix=A^+LP9LrstCuh_jo{@=6byXTzZ{Fd|o=2@}AS?8V|kJ)Q1 zzmd1b{dFx}vl(>~*Pv~K2G9T+Km%w14WNP6X~6pb=S{m&ZN=WU_e+mo-(JIwcZb^E z4lP~xH;KP{{?|R{9a=s^P0#!$@psR|yLlg><~Ou-UEC!8?sBh>Z>=|Y=m01co4G=K)$uYsKVe>V@HS?iB= zly6`>wl(9iaD{6-HIVfG5ibZp00Izz00eSJ;Di%Si0XOg4ouE6?D|^p|M40G?)T@m zX=l5fxbT5!01dQS1Ni@(RqkXL0uX=z1RzjCK;FOBtDa+OU4#8$6TttMU3>5W(-zm9 z0Qb8AjOQ~K91Wm>Olu&|{(so89it_d*gxviXHeJ`a-gaE`i?tBKiGf&XwV?|1zJQT z3jz>;KwSjt@%Hr`Q&(gh6f6P!e_8+Uo(I^!)njs8pK)I_fCk#3fpY$T@Zi4Dh8qrv zcHVhpwCk=*M*5t5_30BupZ)BxX#f3}j`rJcbhOSoizath-PXAqZ8RkM_P3Xc4n1^C z^v!RMipGu|SUjiJ{^Ef(Hv0BmEb8BXY2)n&S3Gi=Wlo7!T=BZ7Z{Oh+k7@soKGC9! zZW4_cv0t?0l1G^EyU~CF%O{f2=>Ei|s%$Q{*kaMrOD}E8zB0c*pK?kx|GMj<;lr!* zyYS+Rqd)xVPgP=DxNDI`7Ktvo^wQ|~I>w?Fp4I%VzKacv28z$HiCr{jSl< zf7!jq_Z{~=OS!H{O;P7!rr8>(lzS+-benF zwxyO@D*DlnepGKVyB*g|KCQ@qwe;=2`|eHGb2m7iKGfsw>p8|#@`<)d1n~b={r{_` zU9P@>G3{ud69+yJ4WNNmX`r0{Km71zquH~^7vD=R*}q%Ovge+o;(e1QtsYICIxgPc zbkm{Tj;ZuvvBmnu$4{HKMs&_Ot46bCjgMx|Tq9a*t*$}n*GumDsc8J{1Eb+vkM9xTY8QVu8aHXb9^ZGu7eiND zBiiKB+0o{6Z;w{}!C_6w(m@9uWcm{~Y|6FVMOIyP)##|Bj&dKXhOLOdYJ_HIZ_-!m zTDA83$e+@re!s7M?Q6A?S9X6h`LrVc)zbIB|NFnur$7DaviZvLm-Tr2dXC951JWK9 zf9p|gL(}ebU=toGrE(ZpP-#L;&;Jdh6kF9ETk?CjMM& ze|%7tKYjWPjQaH(6_1~5Y=79Wo#M~^`Ym2OOCDOsxOWtFrKNa)+34SYDKoGA6Q*yV zzd3GX{7&xg(`OOm$=5avRyD_zjuH2Zuidxru=xD0<5o_dUU6I39Wvze@p(E({TIr| zs8L79=QO%s7g^)*pG6a9Tpz9Uo$p6uzPwYk-tW(hK6TEds89a^CO-pBT%)3W_uDV_ z;XCfMQ~Y`H#TW7$GGs{nosQGHPoJLrGFkm6Sr2p6V zYp=6Tl#DxuQ0NB8UwCGx6qW&Wm&QIyL*dp=%J_8r_Y4u%nVEmoN z`uhy*7jG~1oe8o3U*fBuEMCt=>->M8eto0CYc3NF7}c}ytM4NH*l)XwYg)#rf@aoSDBcc>FT)yWjAhb2v1= zq1^s;esyxR>i&mCLslIZZ8rO+Xt5PnNnW1?HyRxxR+ga zSu;PVd--X<^0C}<%b9y~#Z>8Y`BvU_tdi3ZX`BzzrkKV(E4J((d_#EY}G=KG$UnRb!k@YtJ;xcq-&-+#RP@MIo z&-wpy`gG~5#;=;b-T9S5UANZ#qjPKVE4RNMZ(q+bmD0}l*&x99e_eeZZe75w@Hyf8 z9Lk6?LIXWDP|p9mTI#stmM`o7&p2b1=#*1dmi36A4bU6_%^66(MvWR^))HPJ`sFW^ zbJvqcbZzU|0nIgV{=f8yTKh|n*W7ve<3&M~*zS+y2t||3im<+3deIK4zI^ z{$$$FGtC(MmBpwA58lvxH=}rs<(8Xl{KvY*{hF_@_?KDc&!!E1Uh%j^7yV4}n0hw` z4O%Zgf5|1wk9GET|H#j5ZpL57Mzseckj8;Bn_jr4anfph>KD%0c&f?pw6@Mpx?_Oh% z`C|N?`U-|@v{JmSYb4(*{rMa2@F@2Gmwzu>@wYq0$LRV)He8{&f6;ZvMypTUw|M?4 zXMHOgIA&r0Nv-pX7b>%%c?qjuwx7BFKJoQe_|1-SsL$%h&}-G7h9WXG}GXeXVex%Jd+bq6@{;ajuTG3B_ z@)Psi(0=BB!!M(6ed}B1Iimf|vp|OykCDIHV~;(eAO7%%(IJN%5*>5QF=n2Q>FY4B zkt0V&2OMyK`G0X(^s8U}D&E%TO*h@Ncz$vtP5fW{;uqsI?z`{4ak>;&vJXmqUYdKL zbnE)Z9(!!B(x<%Wn#$V`e(;0n$3OmY+>e$__g>~6`pQ?n63?&Kv5I5-_?~l7O7o{{ zh{rzr>=U1R;DHC4|ILTT-0Ho)`tFW6;)o(=I83EjZ?apn*(jAdUam+5i(Lt{$Cs+RBmU0%#up^2;yM zt%B1t{qpsi8&K)h$Mhfk{O8NX$LX4-uUh*{kJsmZ{YIGaxCy2`{gc=?FR{b{@#jH< zK3P0`;J{Vl@0MNmg7}!E|L@y(XzcZuUw)1mXW7cM>wg>{CsvCXG_}d9_{DTLVn{V3t{cHc? zq-d>U|Idu|_m9>+;ZM=#^X`Z~Gi6@1*{mC)rMBEQZWnJlVM272@o4h@`^#II&+C11 zA-`2tStb7N{0lCKk4gG}wJkN?KjVrkqOCvw`REtN9Ty*?XZrP$zrTFSl=!`Y=?D1K zMjJ)bOkaTZYfQ@df5rU+6ZZ~ge(^Qtdg>!^W60HzIsY%uyVTwjq7mD#6RmN@0nus~ z>>ZD{YyY^(--}i~|2y%t4~>JbG5rUTJi1=}7hg0Qy2;AXvOn53_Rpigw|V?op5DFW z-*x>_-`pfxa<`4*?Xf@oV!T`P2Ub1r+tIkG-;bXi7`gL?@%|N#UpP;?c>W$6M@#O~ z^_+n_-^EcWbk8L$w*CrnKY;or7T>n3Ph-(_y87VM@3FdhK4Fm+N5=gQi*K`LG-#ED zYz4zYR%t!h{e4P7JZGN^pCT;xEaB|FF z&wHy+z`eA++ittLkMTHtat@1jR7;=w6Ov&=H_x4Jiy z9$a6R%b&iNH`m+o+hvzsx}{ggRJ*>?r@jZh#6f)tU;gr!;LP?Uo}4U z@Krj}=KtH@{&vjV9g~eA9i5!)ocPrO#UYYMtLLAtfK`xqoW$tF*r!Z(q+b zB}wLE#t7j5d*%OKKfw1GU+g))2oso~fp9gD#{X;1{^ZHyqO;CgCA#RM)#7*Q8i0#0 z-Y?pB-_gG{Yi$qqkbVJywWe(n2 zU&k+(F6DFOQ}>9*9=*Ns_M_tKtKUK|rDgmqlZHb-ANO4(pLM8bzn%kG>T}yfnhUVX z_YRI0TY1&^y1o4)Pqoc9+jKik{zYT9dg$__HO#Agp8^6xg-2cyf<}=ZjMx*L~&^!hA(sx?-ZjCk8sQBDLrSxg;wmg)^@|9n` z*D=qy9B2B;k}vu5uYdjP$*+2Bm&>0HlJ`%}m(d*gpZ@fx-OjIcef1d}fBf`(w5EAhDfz0FzH0ichhMex zT^jW4hgj-e+J9E-{;9>U(*An9eLcsxJP>A+0RBIf|9{!|9Q6e(jo%5^?@%_B4H{Ue zf!_N6MHlTGY3;sW{9?Iy{{JG2^o_^=^-Mr-Uc2nFWbFO*8v^BCwf2`gC;44k|1VE3 zf4}(R3&-P@U;c`s{~t2s3-Nv(Q~Gve^v?fl?7rNqU%w@~ovUXFN?%%=P}kS93gupD z_fNRuuhFQ_ZW%xOzy4p&FJ5}1^QSh&|8H-`M&<8!*>%_W+G<}X{lCU>^|{r!c_^Jz`cC>h`E$iz=~XFzI$r*NBh%03URtL>o<6xwN2Tj)4!qK*^%>kt-ng{? zK>N!1|9bORiI1*Z58p=eFORNgP&AKewbfRO*O5rh_fQ(DrLUSk>)}`Je8sJo`kQ|7 zi(ka=m5$kd`w>O&Z|q@@SFQa$PAKz*^T+b* zm0sn>|Fu@1eD~f|L?j+&-nY9 zF`ZS5?*Rk4&#N9f^eb_kUEaSZphos@a@m|{*{|*#um3+`#&zb|fAjl}<{582>zy3` z*V_K_+}oARyH4h%-1xtq70`JHnl<0`k{{4#H~z1&M14NUcqe%#_ttu6YJ)rfKXcZs z;<{ySet`T&t(WuDq5wQGet@4Rzy4tes}di+a0{z%@f+WP-#AUp$$?Lgo`Sau$zWd&VmGY=+x6%B|Q|q~iT}?k_(qFsy ztEI1+KI`FE?R*!1rLdm+CwXoqe(sp${(8K9J;%6f6K0bD{=Xsr|B?0o6Yx{{^HY>L zWu7Gsl=J_4?X^_Ao}cFS>lyxIju{(&UV7;P=9$ty@w5MvCXF-Cu#Pa}a--tk@H_Fu z6}we-m7f1!WtBnk@md$)h$EJbudB5KciXLNeXm;kyG2sv!;m4{#2#8}@C_UGH8YQL zi}+}*pFeEa4)J^J*opBm!-ww{e;zPEZCQF1v8Ny0-tXobZ7IS2JQ~LUt=<<;oERUot9ce$pXC|U zp1f-A+-TOEInme5{N)o*I;org*ZC7KyDXZ0<(1LSrv0t{0)=k8-}!&VeYhF--^)C! zd(E}i#&IRbFDnsr{$GB3%wgNc&**C|{3>UEE1qxf&RgQkpNM_2jvKyZ_r5C~qwlnD z#glf5MttcL#q|TVubO}F)4y-L_MqnX%X^RB=dgTqCLhZlxv(Fs)_xbiehYB3IX6X%ue-i^{{J`8xWl^7A$P}>hVsr@+b=nW z?2f6&ztrQ8^o+iIs@7{OeKiulen&uKKsl~6-{(8K9 zJ;x-|%Xan%;Qt%)|85?@9kw5!PnP@uX~n;yf&6Hood5sxpI3@~zr26)z1nJn;>v9O z^%sjSys)cpK>JTUb!FrI7U@>eRr3Gqt+!aWYr5-e4uM{^_IHb<$_M#$&A*p#*Lc2L z|G#wZeR2-}(4pJMR~#^4Idkrb#?zl~*2DX*@dDG#T2ogPV;eYdoVniF@paw#?)o~Y z5&u7U@CMzk>#o1_(!Y=6Z*;%r{_D5hHMXlY0EezVK3@NS(CB5Neu+8E@(#Z?KZ`=9* zl~3CvTH%DR$KF?K;V0L>7s!4?7KyZOzt#fOZ}pYF^vt}T4H!42YaRU_>;G4ozpvlr zTjlgUhUl2`Xj}o-v-dyfys3S8jW9L=kkSm=3Qg^JMOq+ zH~+ugcH70?QTsJ7UVQ`d$)&GK>C3J(HLk5O zfBi<{L1r#a>F@MayT1BVG~Tas)%T&a>b@*}Rr3G!p2u@t$kJ>4 zQO8A_PQNxkFPcZK-@&T(l26bY-sOIeTjy!) zQfvOXvDRwWa{HYBkNvvQrPlm&&jGk&6c*P2IB2Wn*G=8-zvPgp->{y(si~1vxR`zm zaNyVxQM|5!S*Nh{Rcn9g@%4UIxy#Si^Q=a_*X+%4&5B>w(HL;)bKx#6(xmp6u5aM> z*AuFbO=HjH`VK0cUr+wz|2NoRgV?htf9I~!H7k9uadxdgSLvnsKI$K+^h%RIcdbf% zo0)&jkGLHO#=A;ruzREjUP~7z^E+w0@8|) zMFX9$fwcZVn>AZ~sU9`zXyZ#~84q(#asI%tVb#~-uJ6i$9M#7>Kp-hhos4e&zsyy%2Eg*a{A#2%^)yG{t;tvBP`*k*K;y*jH|tWI z-tZ~D<8L?vH7-z3{+$1>ajDe1<@+^DQ1%4Agh0j!==a^V zetoN6S`Sfc3Md|p$A|W+mH)4N}2Ko&FUJD7-$e@p!Tht~gZ zLLUHqAZP#$grEWZe->+K(hLCzKmY;|XdVH1`+DBZi;*jZNC5xe694b!0o-i+0azE1 zG2kG^fJ5-1q`^;r2>w6B8kjUe00Izz00bZaf%X!>|F_ityMBPZ@H~Dz58UC72D&tW z|8H;IN^B5-00bZa0SG`KL<0E#w)p?Q8xNwsfRT6+{3seg13ol>{|~VSCQT5400bZa z0SG{#y#(<8ZSntZ9>5<>yIn0RF$dbt|z!00Izz00bZafe;Dc|J&;S z-?#pML;3*d13?35pq(1P|A$xulO_m200Izz00bb=UIO_4w)%fJ58wvd575@}zjk^J z;=41MG=M3dhgbuXCI~z{}fB*y_0D<-r!2bv7 z|J^)*6O5`9XJ>7i3YN*0sMb^ z>sDfe00bZa0SG_<0wEH>|7XJgyMBP3@L<{YV3Z%_=Su_l{}5|n(gXnrKmY;|fB*#A zO920$DgXbZ@eb+>80yPI5HJ1}4P;XT`2Y6St;7Za2tWV=5P$##LL`9y&y@dn^8kKf zJean59smYt01eco0sMc6H85#{00bZa0SG_<0_`P$|IeiVf5-a&HRuDN4+IUMfqFE6 z|8H;IN^B5-00bZa0SG`KL<0E#O!|K}58!#*4{)vxFRU}X$R2l|4bQUS*)}}GhNs!^ z6dV58hJUi*Nj5yrhDVxEW042jaAzBCWW!}_Sl4(Z$Kq=?$t*w5&d;DtmE&ji~ zbt|z!00Izz00bZafe;Dc|MCCTJVTrIJ#FI|X2Ps`K+>JRvRlXM^@BFuCk^#4;QvFc zfk_htAOHafKmY;|XfFZ$KmI=;?=sz{1uvaHFP)__A8395y#d`jHTwqe|Lv_?i46h} zfB*y_009VuNC5wj{}0f^e8>8$EO|oGnx8(4W34`GdhU?!E&P9oH85#{00bZa0SG_< z0_`P$|HuD_2tWV=5P$##{1U+be2n6u|`2P%f9QQlx<4l@;_(9^#2lvleebw~T8M=20 z`2P@UT+#pm2tWV=5P$##{1U+bDFK=}@4=}n?9CurN;s5ZzlAzq`K~|8H{T$>ksb z0SG_<0uX>e1_|K*@&C>rWH5Zmrg@|Z@#ru}tAUlQe%{UCJy3UF1^z#S6*NhP00bZa z0SG_<0!=1>|HuD#faf{g<{_>Bw5V6*NhP00bZa0SG_<0!=1>|HuD#g151G zxWk5S9k+UYAjjpa&tRz0iOT$Ws~7x#lj{{O2LT8`00Izz00bZqCIS3E{=Wk~(2uRZ z!`J7_*VkL#CwHLcYxZF<{vT!?OsXIN0SG_<0uX>eI|<Ojro|Jzw{5*Gv@009U<00I!mC;|LG{=b90&@`L3ddG!1t~2Kg^tHbKrViFR z{y(F2Fv*4h1Rwwb2tWV=?IeKz$N%TR6YXOCAO5~G{e7)+J;auOO^@8k_X6|(+gY;` z7X%;x0SG_<0uTt30RA8U-^rfG=IvP@b`9=13MnAzAQ|>Cu|Jd@!|3AJU(*Jy3 z5P$##AOHafKmY>yB!K_N|L4dXxpm+0_nqPIYn9_gIns8`z759z^I6A}YY0F90uX=z z1R#(kfd9w;=g1>{Z1cLl32S*r_Gg#T_O^U$dg2^uzkvTw)_iP300Izz00bZafqW9c z|KtDjyB!K^~++M~@ zbUd3kY#Fc`2T#?@#Go;5P$##AOHafBnjaEE4P>N61ib> zp)G6tf42O8AM5+C;a;tMuj2ocH6Pm$fB*y_009U`K;r~H3T340SG_<0uV?N!2jpQ+v1UGd!$FLcXI2<(GL)2 zKfn^!_rFrf>+*fi|7-aFWX;Dm1Rwwb2tWV=5XdJ1{D04OGTtHgOpdb}!2gHo|4-u{ z?fpH9|IcR~Pp%;V0SG_<0uX>ek^ug{_w8uBN5?b!(3bId6Ve9|4uj9x^7xp0u=n>M z{y$msu?+zTKmY;|fB*#YNdW)f`*t+mBR9 z|8HLV8n2QYmJ>`_(+AKx20K~4HOI8M=jy-b@c;R&x|&0{dw@_fef#s4?(yniV}{C~3MV;cexfB*y_009W(lK}o7Ph08} z_*}Op4m7Or|IK4?jOB^%Z=&ya_cQ)KpLIOBh5!U0009U<00Kz@`2V`S9-gEHp2T^R zjE1ZW*c|Ua#`^v@Gpd73{BOYjCu=^oApijgKmY;|fIvP8;Q#Uec&>&u;Kr?)3y`iq z;Ad#CVGY*ogTVjivyLa%5P$##AOHafKp;r~|KIv{H{Pb>89!;u82_Kn|3An*()xP@ z|DUY+*oFWEAOHafKmY>yB!K^KeY+cP)A5YmymkD4I{*I+_eksS5&VBX>v(bv0SG_< z0uX=z1d;^s|E+I#<83;gG5$ZD?|1zrH*t@&{vN^qCu=^oApijgKmY;|fIvP8;Qw3S z?#A16JmaTq89P5q*t`CKBMd(=tN9E&$E0V3|IcR~Pp%;V0SG_<0uX>ek^ufceR~=& zl6!U^Sq*G%LOg%t3_f8w-e|cxzVr(;p>G8LpRD=Vh5!U0009U<00Q|WP~!jJw(V}h zuJ$$@TFc=rt2Z})zEb#xJ+3r--G2Y?lJH;l^J_MI)rSAH;d3^8+=dU?(D}|e_VcMW zJko^n_g}T)FdO3e8~6M$Y!gHLe?IGYat#3pKmY;|fB*!N1WNorZSgj>#qp1fm!knR z&`u5D|C2Qz+Yo>N1Rwwb2tXj81n~cOkal_y;zDm|z>fy-|M{%r$u$HZ009U<00Iz5 z5~$(-*R^f+-adc3L4$4l5%&C_o3OWW>-b;U^W1gno!2nI#_Q&xf1EOYcb>b>miD@} z((%WX@zr8lrI-XoZ00Izz00bZafg}O^ej%3%KBC3 zV+l_?z7YoRSsyiqet?$hwiO;YkJr381mEw{fd9{D9Z#+y009U<00IzzK#~Cdza>6u zvTJh_Hstqf^%ZPm_~y}9lIJkXeX!NzYa!nE`2S?h$2J5Y009U<00I!mCjtC_Q~aZw zm%bpxc7G1ibh!0h_~I~qai^EdT2_B;UC*z#_ci`MpLIOBh5!U0009U<00Kz@`2U9d z|0vt;|8H;G;vC$MNuAS~T$zK@#9N)0_f30~cLF~u>Cu=^oApijgKmY;|fIvP8 z;Qv$k|IM@1?$2Rg!=pCOeQY`5m&5SOol*`bX7QfF|L3!gC)W^w00bZa0SG`KNdW)f zEB}A6^^-x2=Vj4@v^mZ1+W1D=a_bcTNk07aXI#@}{WkUg4E{e^^RW#92tWV=5P$## z@<{;yUzh*CFw5e4&?3tjyS1Fc?O?1Xy=|DVq~o?Jr!0uX=z1Rwx` zB!P_k|K)A_`~=%RpKilzZFs8<@37%rHoV7%_uKG+lF)tsrv1LD<8WNF=*N9=eb$zN zE9cVC>EmvD+?_VO-G(>X@M;@gV8eY(DDT|QhWO~#jMI;>d|xo3FAZfbUi^Qu=3^TI z5P$##AOHafNi+jtT@nbp8eHl!b*IsE`f+xtIr-aCZ<3Hbkf*74*T z0uX=z1Rwwb2qXz)#Q%Svc0Zmv1P_%m4Q?LEaI0hb1A5mVFgGQ4`Uw*ABk=#pnvZP= zKmY;|fB*y_kWT`E`~S;m_v8CJPy<`g4^VX-f^@1E21&et`ZqKm2Z=hdxjIe?IGYat#3pKmY;|fB*!N1lr{P z``C8>BY3`=o-Z@wiIipA%JNWq|73Mez?wGw{BEZQ-|y~I{C~3MV;cexfB*y_009W( zlR%68{}|iuf8Vu13ESow=|}MBwOXX}>Grx==?B=$q*rN0ivcje|L3!gC)W^w00bZa z0SG`KNuWjke?!{+cpiLT%QSF@WgBWgz_+;nTUPd|`%>`#$(oOC2tWV=5P$##AdpW2 zE%N`{(f$vw{f{5-m@;`TaR0xEz5i$1bTaRs@&9~Qu;dy75P$##AOHaf1V*4m{y)@l zNz#>%b>%5%${7umXn^tmz$#yo0|5v?00Izz00cY`$f*DSQH8c-1(2ncA z)Fl2_!M|6(^S8O>->mWUw=Msr>~%xaTWIO|vE`I{4!-4Gdj7%f;E(_JP{9%<1Rwwb z2tWV=5D1JwiT`))(gWMk9t}8-el~3nn9wn4XLzNJH#Gm#uJo*H`G4Sv|7$khC2cy| zmG4mEIn3s}otiHf*Vl5&|2nI`a!gWxhyM?(@+CPCfB*y_009Uu z#5HfW+k2W&UTGH_?r6iUZRmby+_#_lKa-wNS++7=-?ru8i~j%{|KmC3Ki}rt7yped z|7OioXr)$LaqYJ(|FM=syUXEMHvVpT!~c7zV2Kg}5P$##AOHaf1V*65|EF!IUS6)f z$#*@xzv~Z3%enNt5!PS44C@}OyLlhW=Rq5~`5mP^d;56@%fDP)eAlz@%gyIl%W~^2 z@6zK>wdD!xo~(N?!T$$V`H~z6KmY;|fB*y_;DJDs{C{b?ywT66)Y+~-%^ufie4(80 zp09tZ&ip@Yj|*kKL!%r&XY!+Kd{k%t-T3k+?RkyH@B93p*nITxZZ&wEX!rY0f1~lI zectA;8PETkw%p&XQO=cg@n3u1^7cAq%K`uIp@Jn!2tWV=5P$##AP^XV7Wx0r+jeNB z_G&2KZ&|Iq+`zuSm~%tPW3_br-lnsxUr3RE*MG4fBl&;Crqewy&?tvbTV}3bq8j@U z_PKtR4O5hLIabGIq})r{AKPI5``YvG4mtn$|G+9=k^=zuKB}xcD00Izz00bZq7y<1}CzLT|j0Re#0sOy*3YI7#009U<00IzzKwty{_y48+0PErpTIUa73tKeMxf;O# z2Uhu#90))F0uX=z1R&snKt}w(>j!ww_!Dgp#iw+xPoZonTQtyo4dDMhRIo$|0SG_< z0uX=z1Og+FG5_!S0WLEBK-;Vf00T6D2GBqq8o>VtR{4?~2tWV=5P$##AmD*OM*Y9* z2iS=|0Qx}C02)98wKahM_fWwSB?KS<0SG_<0uTs{K*s(5-%Pt-eE~yg|JQE+!w5!b zAio;G{|8q2k{k#?00Izz00bc5fk4LnznceeqVY964^YP(VEkWx{U2qY@3P1Ld#GTE z5&{r_00bZa0SE*}phNus2iE^@fXB(V$D#Zwe>6~D1Ni^IDqoTV0SG_<0uX=z1UwMv z5dZJy0nE4k0IUbbTmUqH2GBr41NeUr6)aIg00Izz00bZafxrlKnE!YE0DCe9Sbhu; zE^t8u`P2aZKd{P|S^vLY9@_Wh5g&^N&_E>(;Qu{TutW&~2tWV=5P$##0wd7j{@=|5m}lC= z+GZXA4A1}?$fXAG|AAG$BnJWzfB*y_009ViAdnOP@A?6Dr4N8U5Hx@W@~8p)zlREz zC?Nm=2tWV=5P(2n1ajv8pE2!z^#u&hWBZ>x<9pFSel>vq53KSfIS_yV1Rwwb2tdFC zft>k&HxJ-A%=4%L{C{ATFUf%b1Rwwb2tWV=9th;r|GRkrvu!^B^8lC&fdq)*AK8$9>)X8GvnZBAU_(w{|8q2k{k#?00Izz00bc5fdKwL zUI*YQI}czGekebFh_a@v^P~a%zlREzC?Nm=2tWV=5P(2n1n~c{|9A5MewrsgKwj~` zXdpit!2btU`H~z6KmY;|fB*y_;DG@CznlMm+xQ{%1&qTN<;NFM)|7P~G=Ts2P{9%< z1Rwwb2tWV=5D1I_{=b|5ck=*d7(bxxj$RLp{87GWAU_(w{|8q2k{k#?00Izz00bc5 zfdKx$tp9iY06XB1^5c&vYs$KlHGu#3P{9%<1Rwwb2tWV=5D1I_{=cgKUts)z`T_=a zvM(TS_+B*7`5M6g2Uhu#90))F0uX=z1R&sn0RF$K|9A5Mj>I2zzCWUjDdSGo0RG=Y z1xu6=fB*y_009U>|GNDDo5mNYFJRS9_6Ot*|BD7XR|EL}z$#yo0|5v?00Izz z00cY`!2j3f|J^)*DaIdZn{@$UfCkV&rZj;6_fWwSB?KS<0SG_<0uTs{0RF#M{{Lg^ z|Gz{Z0DT~601aeG1Ni^IDqoTV0SG_<0uX=z1UwMH|M$xOyLkYQ*?xfjS!(~2R{Sa& z$gc+Q{~jt>qJ#hhAOHafKmY=P5y1ba^8cPuXWl1Ni^IDqoTV0SG_< z0uX=z1UwMH|2O3S-!Sce^#!b$B_BXq@vUeee;UC5d#GTE5&{r_00bZa0SE*}0RP{R z|9A5MCK*4a?M$r)MtUhPG>{(+;Qs@wd`S)jAOHafKmY;|@IV0n-xUAv)&=|mekwnH zin6Axv!nt1zlREzC?Nm=2tWV=5P(2n1n~b&@&9ffz(d9lXq$BbnG3;O5Ht{!2JruZ zRlXz#0uX=z1Rwwb2zVfX|8J`Qcl`i|WNBQGv@(v320C8@_~TjKv;H-1We0n6j7I^S1O#*}duG=TpPtnwu}5P$##AOHafK)?e5{C`XQzncee zvGE1k4r(4S>7hK(K>jp<|MyVA5+wv6009U<00IyQi~#<>rT*Wo3%Ct_D}R2AGN;T# z(g6NHu*#R@KmY;|fB*y_009pK@c%9K|85?@1I7nvyI)9tfVALG(LjDQfdBVU!4f3| zAOHafKmY;|2#f&!zb*dX^#kmW-^!2QqO2+FkTih*53KSfIS_yV1Rwwb2tdFC0sMbk z{r{`R52!C-Oh~?fwBS?GK<8@!|L>uKB}xcD00Izz00bZq7y~5n;Qu{TutW&~2tWV=5P$##0waL` z57Pg;et>=OYx(tSls#qNOby`w1FL*V4g??o0SG_<0ub;(0RJDf|9{c6|J4^Ts+m53 zYvC)=K>jp<|MyVA5+wv6009U<00IyQi~#;WX#el#0i0_5nzoy|9vIhyCmP6)2JruZ zRlXz#0uX=z1Rwwb2zVfX|IdW~|IqsXP4H{^@oSVdW!+2-;Qu{TutW&~2tWV=5P$## z0waL`&xHSX^8jwP{Q#^B$Xp2Kf}nw3X#oEpSmjG{AOHafKmY;|fPe=A`2S4#f7cJN zS2M>2xfbKhXrS{ofdBVU!4f3|AOHafKmY;|2#f&!Ka>9d?{*%*NPJu8`!>p$GH!+j z@c)5Tz9a_%5P$##AOHafcp!lP&!qo%^8o&6`vH1254ag#f@|PO(LlB|fdBVU!4f3| zAOHafKmY;|2#f&!KhysIee3@>#EWIii&0*bSGzTU{|~J4B{>j)00bZa0SG|A0|ESh zrv1O02XKS!2VfomYr>%cG*DIp_~JHr3Fetp8uWZCd!loRFDE)C%SJyftn z2>}Q|00Izz00aUffdB6(|L^7j{MPmZl${6MF26xMcw00Ojt21mfmOaF2Lcd)00bZa z0SI^?fdB7E|Nox#|DVKvh2y^{3(CT$2JrtLDp;a~00bZa0SG_<0)Y|0|97PSck=+Q zwfz9h17J-!G>~%*;Qs@wd`S)jAOHafKmY;|@IV0n-_icx^#kncbKH@*8TUp5{xyLA z_fWwSB?KS<0SG_<0uTs{0RBH0{{I;}4`3KR%)bw#3@8I18o>VtR{4?~2tWV=5P$## zAmD)j{y!J~-^~L!!S(|%7d+>4!F~8o;`H0^f&ceV!4f3|AOHafKmY;|2#f&!KUe<$ zzt;b+gBSDb#o!PBc549tA6Vr}av%T!2tWV=5P*OO0{H)2`F}SL;A-0sz&rrv0ic1L zXaN83p@Jn!2tWV=5P$##AP^V<{C_U}zv~Cssoi6c#K*Wd8t|h5{C{ATFUf%b1Rwwb z2tWV=9thz7bM611wDSOl;>rAYGPuLNT^hjud#GTE5&{r_00bZa0SE*}0RNwB|L^7j z{KEDFFc&;0bHUr?If*Bn{tf(pV3jY)fdB*`009U<00JHe;Q#Ue#-qJs{r?*HwQ&3z zWkFdqQv>*a4;3s?LI45~fB*y_0D-^=;Q#Ue*86KN!1H$dTpM0kXLyl4?mQcwWy7;= zc!mv6v*9T={Id=JWW$qec$^K7G@<4s9Bjj#ZMcyQm$4!97MQoti5lo*_3iG1r_5(J zXQ$U8{y(tFm*hYI0uX=z1Rwwb4+QZ4>D$wdw8c5MCETyETnCx36a650%160wZS~u0 zk6-Qo3jW_i1xu6=fB*y_009U>|CadBrncLwwdvVcw)~#9T!xtt&kTdCXkZ?eH8||F7`>9x7O(ga8B}009U<00My#!2jd_Tj@Chx%PKVI`Gyo2vq~eT7A~# z2tWV=5P$##JP^SD?Jpzft{hk3{JAYm~q%wEw`d`(~`vU(TSmjG{ zAOHafKmY;|fPe=A_<#I=J3UAsacyhT5t1JyE%_>sy{-OQI^VzaUc>);s9=c_0uX=z z1Rwwb2n0p||BwGK^$9un{GiR_U=!lKVbCrOENS)jijR8?{~uW8OL8Cp0SG_<0uX?J z2Lkwi{J#$`5=fjsx9Mn?|0AA!_cfg9Q}_6P4;3s?LI45~fB*y_0D-^=;Q#UeK0S%& zxZUrpk2C4Oo5P@08u*;mS6k=%m)=|W|G+9=k^=zi3^2MdEW4zq_~a|AAG$BnJWzfB*y_009ViAb|hJ|2uy`c>GJ2!AKJ}%L8(q z{IOig>gU~%?kW7ghYFS`ApijgKmY;|fIwgb@c;P#kUUBtX*u1d1Mi+c@7|0upB{*g zyUjbm{|8q2k{k#?00Izz00bc5fdKv={~w}P2_#LMnshY74|0usaNO1ErJL^#M9T&I zzlREzC?Nm=2tWV=5P(2n1n~d(|4{r%fa$oyrpc}2hL6vOkMC_c4>dZ`InM{E;bLC| z{y(tFm*hYI0uX=z1Rwwb4+QZ4`2UbSOCV|cu}Md7Jt4>EhuO)2=(Ond%cTeZ@1cSv zN(evz0uX=z1RxL?0sKGyznoX#yKcI0>$t68xZvwy;54wI)x$?wxL@%9fmOaF2Lcd) z00bZa0SI^?fdB92^Qw-YWuc8v8e3YP(=1=-{|NKh7tq(}KxKbZmhLzFzlREzC?Nm= z2tWV=5P(2n1n~bY^^5c;oYs+icd`Bt|DR9)e~2yrOx@r3|G+9=k^=z}fow z{bL*d`X;0gpmP~)Z_788@Ajn5dsi^00bZa0SJUf0RNBw55=EkI2||I zw7YfT=nLqKzJNidTve6}GOSJfe`u91>45+QAOHafKmY|GTmsI+=&~n57XUqTh zG3BN*yQUL$kN@{r(GoQTAOHafKmY;|2!sIsKb60upWrmO)!`BU$NEY70kY8#@LgM8 z9eID_{{yLXNd^QU009U<00I#3NC5wjkFCSUcBCI_L%tuiakzEl=m!Y1A7BaV`(J5; zCcNEC_>AXaW)L48~=C{(gzR@gU{LW_}H^PTI%~<-thk(D_Ww4 z00bZa0SG_<0)Y^~|KtCiKOk(m7Uo*UZk;*$0{rv^^f%=o_V?#W^Z5TjDqWHR0SG_< z0uX=z1UwSJ|F_)pwPkECPd=st^7XU)gGY?+UsAE2%MAS+wm@8nVQ`2RpEU6KI-2tWV=5P$##JQBeFeAO!IL`2Re6qCoOK!K8ye zfYveC$@2B$y93c!0skLJrAsm(009U<00IzzfJXxOfBb(a{v^lgxWneXp9$#)XdZ*X zmgh4$)>g641OM-_q9tkwKmY;|fB*y_5C{SMKmH$Y6pRKA#Q!(7|Kk|Tv*>~Itabc< zAeAo3fB*y_009U<00JHf;Qxd1et4AAa$gp#3)q~o|1sA0znN#hUz&IPzsHJ}s38CW z2tWV=5P(1+1n~d4_nh=QgxBxj#;ti4AYFgJ&q}>7pO5GH|3K+{Xvz?50fQe@f zxOwaN|8)NU8PDYx61$K5JUyh5!U0009U<00LnU!2kDbC*vJp;#mVv+4!BGCG1^)z!8R@m<2fw?<-yS z|1j!YQUL)7KmY;|fB*!162SlC5q$X(;_acmk1UhTO^D}joWUn7#~UqI$2XV#0{DNQ zH7#*N00Izz00bZafiMV^`2V+UJDIR+><|vw;_#N$ubV$#DSX2oR~o)eCHhd`BWPoX+rt?ui9{!4e|Vqdwv*JV2J+@qs}E25P$## zAOHafK)@$~68}%zxkB3+f5vz;8bAYC)&TzBXH84o5P$##AOHafKp+eP_Dsf1q2`f0SG_<0ub;?pv3=|J{S9}X!q8?T#({&C9q-FfahTiWZ^O2;2l##fF(^1Czabt}c+ z&!+E*7RB%8+gH-%`ZoS#np?5Gv5lvc!*5!Y@6tS+Y~v}7|GRC<|GpLSUz#312QaS) z|L?P=C2j~n00Izz00bZq27wa)Pdhz;c6xxG>L8n@4^3F=L7VaUd3)Uz1I#=31ouQo zXaN5oMx9G4AOHafKmY;|fPhZ|`2YIm&gRUQe$nQ)8Qwn4HC%r{|D18}2%kxrh50`P z|L?P=C2j~n00Izz00bZq1_Atkm~C}W9_n`{-ZXx_Rp+?p9>#m(fgfk=zhg9j{|}?i zB^3~W00bZa0SG|ACjtCF{@-}(`GNWVO8p5t(+_}N(Lg#4;QxKrw8RYo2tWV=5P$## z!XSYEPuG^txCa?%)BlnQD|zjZzjy2FbQZU{gC0uX=z1RxLw0sMbQ z`u|zUc7KkyzeGPkN9vnCjPiXLL#^KbQ|_L}|A$fMk_rew00Izz00bc5lK}p|Bm9w@ z_x`tX?S8&H{^ynfzOlS-giE%$Y+&@QGIYO}S#}-{{@-U!OWY8E00bZa0SG`K36L#=%U>0>7JWzf1XbmEYd>{;%u4$Nz^>=aLEtKmY;|fB*y_ z;FAFUKa>7{yWX_>@&85t?|jnJmPudB5kE+U1ihJJud+z)x_`xtEX_*ygWVaETxR<%S90SG_<0uX=z z1cD%d{}0mdxOwRdn$hm(8g=`BH<#dW>$~#eGs$OIa|zb6`m4I0UvKYg{C^NtE{T8u z1Rwwb2tWV=UJ2m;gYy5QY`g!zy=~ibe6Kye^ZM1oOD$LYaajJiQ_JHU_WrM?$<+Js z|6Z$FB8LD3AOHafKmYbn*mCMr|4F|5^mClpj(ZgU zA4HW)A|L<(2tWV=5P*PJ0{DNg{{LX>CsU8EmvD+?_VO-G(>X@M;@gV8eY(D6iYkhFSH! zp{IL<<@mO%ktRnC(l86cIVw&5pF?0 zK)2_3O8XfGSw64k`Ch>P2T|pc2nav`0uX=z1R&s*K!^K(_uTX|d2aWU_m21fu212o z*5{V`-@JbQl;z?0F5{}PV~qt0jaFI(n= z?S0Yd{Q=X^c;)^+{J+<#mdGIh0SG_<0uX>e5Cl5J|DR2}zjC`DHn6c8__PT-sSlvP z%}+=FZeO$RU;KX%RW6Bu00bZa0SG_<0$vGZ)c^Of?fyra)i&ljc;p;u;81)2WVJ6~ zO`Cpxx3d>>Sn&T|t6Cz500bZa0SG_<0znYSnExMR+x_q7$g43=zZV_{UG%CSXS(H; zm41M|?ETL@k>Z}f{|8a!k_ZSu00Izz00bc5l|V-P|Aw^td*$o!M=AVKj{6So2)rNQ zTipMR-~af3uT?FPLjVF0fB*y_0D&L~WW@h(NBh6=_J5B3E&Ue5Cri59km{MxAwmER(#h&PX%M{pR8yA{~tt^ zOClfu0SG_<0uX?JR|5Ed{7Y8+3+Y6wXdt}?@c&+`S|W!41Rwwb2tWV=K@hp&_Gr+fd3Do$|VsHfB*y_009UVtQRR{d2tWV=5P$##AmEh%{vZF675_py(JC59 zuL1nO*Q%DtApijgKmY;|fItug@c-%C(|8fsp#e0I6%FA3gQ#*z1Oy-e0SG_<0ub;@ z0RNAF$%=m=ooE#eq}Krc-)mJ%fCjRn0sMauRW6Bu00bZa0SG_<0$vH=|M4$b@h_wkt)hYS z8o>X1t!jxJ0uX=z1Rwwb2n0a@|DV1+jTeC(8bAYC(E$EGh$@#vKmY;|fB*y_00FNA z@c;OitoRqwiB{1-dJW+Jy;ikE4gm;200Izz00e>{fd5b5p2my74h^7ztY`rLA4HW) zA|L<(2tWV=5P*PJ0{DOYOIG{~=|rn&AiW0g|6Z$FB8LD3AOHafKmY{|}<4-G>~2c_MbBCtaPXdo*Z!2btP<&p>pKmY;|fB*y_;FSRWAODgS|3W&^DjGm4dDNSsB%dJ1Rwwb2tWV=5b#O> z|Brvkihm)UXcY~l*8u+CYgJ3+5P$##AOHafKp+SL`2Y0nX}k#R&;T09iU#oiK~%XU z0s;_#00bZa0SI^{fd9w8WW~RbPPB>!(rWk zhX&9kX{4$f3H<7kwX9i5P$## zAOL|N2;l$Ix2N$UutNiAAS)Wc{|8a!k_ZSu00Izz00bc5l>q)9|B@B|LORhZ8c44J z{J+<#mdGIh0SG_<0uX>e5Cri5>D$wI5!j&tG>{by;QxcDa!CXPAOHafKmY;|@Jayx zkAKOEe<7V{6%C}<0RG==RZHX$fB*y_009Uj02;`O2JrtuRJkMq z0uX=z1Rwwb2zVuc|Hr>%#lMhFw2B7OYXJZ6wW=j@2tWV=5P$##AP@ur{D1oPG+qRD zXaEglMFaT%AgWvv0Rad=00Izz00g`e!2jc4vf^JzCt5`V={11=_gd8wIRqd80SG_< z0uTs-0RBIHdm1kSJ2Zd>vZ4X}e-KqJiGTnEAOHafKmY<>3E=8f%F={ z|9h=!i5vnDfB*y_009UDK>+`szCDc>fgKt^16k1k{y&H+mqb7S0uX=z1RwwbuLSV_ z_?N8s7t)DV(Lj0);Qzf=wL}g92tWV=5P$##f*^qZPv4%#i@**Ipnj{6GFBEB=LaqE$4IUIX}luT?FPLjVF0fB*y_ z0D&L~;Q!ONr|}}NLj!0aD;mK62T|pc2nav`0uX=z1R&s*0RA8Uk`@0#I?*Z`NUs6> zzt^gk$RPj$2tWV=5P(1s1n~dq+tYXv*r5S5kQEK!|AVMF7zmQI}iU!ha0RQi`swHv=KmY;|fB*y_5Cj4IfBN<`UIcb% z01aeC1Ni?Ss$3EQ0SG_<0uX=z1iTW!|Kne>;$KK7T15ltHGu#3TGbLc1Rwwb2tWV= z5D0<*{y%+t8ZQDnG=K)Oq5=GW5LGUTfB*y_009U<00LeK;Q#S2S@AEV6Ro0w^cukb zd#!4T90Cx400bZa0SE*^0RNx9J&hNE9U4FbSp z&_Gr+fd3Do$|VsHfB*y_009UVtQRR{d2tWV=5P$##AmEh%{vZF675_py(JC59uL1nO z*Q%DtApijgKmY;|fItug@c-%C(|8fsp#e0I6%FA3gQ#*z1Oy-e0SG_<0ub;@0RNAF z$%=m=ooE#eq}Krc-)mJ%fCjRn0sMauRW6Bu00bZa0SG_<0$vH=|M4$b@h_wkt)hYS8o>X1 zt!jxJ0uX=z1Rwwb2n0a@|DV1+jTeC(8bAYC(E$EGh$@#vKmY;|fB*y_00FNA@c;Oi ztoRqwiB{1-dJW+Jy;ikE4gm;200Izz00e>{fd5b5p2my74h^7ztY`rLA4HW)A|L<( z2tWV=5P*PJ0{DOYOIG{~=|rn&AiW0g|6Z$FB8LD3AOHafKmY{|}<4-G>~2c_MbBCtaPXdo*Z!2btP<&p>pKmY;|fB*y_;FSRWAODgS|3W&^DjGm4dDNSsB%dJ1Rwwb2tWV=5b#O>|Brvk zihm)UXcY~l*8u+CYgJ3+5P$##AOHafKp+SL`2Y0nX}k#R&;T09iU#oiK~%XU0s;_# z00bZa0SI^{fd9w8WW~RbPPB>!(rWH~_+B)C2G9T+Km%w14dhe|L|9XYIX})+|-Ei=wLbUa|KqwMS6J+b&A2*rRr-U9l>5>>!eS z`TPgp`@_xiKF{;@e4X>U=bqe0?v24q99(J~0Dur6%xwPJoJN<7IRkLNi2$hY_n+Kc zKX|)&3W|z66Ze;rlos-Q=Xdz`ea);5^^nKv--k`m)ZNbyczjGQuI1jkPR<-5_vrZe z_$rq9vxAG>U%j{KK!`<~Jxt#5z_R1)hu?4}y9RmB$>q1xY9KbdbRP$$EgK6Z=hNTp z#GdR*38mt{%7~mx6!XBMjHN95V6Okli1Xg5Aj!jjuRc~JFDzK47X(>jE{?p9%%bIJu4(M zs?%scl%}_aP-UmITC_RL(kK&LC;1;$tm%Sla8q52_Kkozx0t=8C823Ys~#zdRd3C8pqHZeI4SdTG9{n z{#>N{L``DU_|>zW&*N3^W2B!M(dUYWG4+zu8vgUp{;J1SWlsLofb5GQG3hk(rw1KL&_Z-_hu{t=RcGDdy-K@!OoxiG>AF(Y0Wt8diHh~z>u6NK8=zg z?eUEPZn+`WYeVyA5^5j4mFc_@8^Q1WywlBU%YAY7UcU~^&JwtJVvawWr}h20D0)1f zoW6wOkthZ0Xc}#9`t0a!(XYzzcBi;+yK%x+`)m@7FD4j&mvvqU@G*EYF5qd;=X@?? zu+NtgW>aNjF6EG<EAXd2O88pYdm9Lta#P|n;4pTw+os{L=VI{P?*Tvb!?RxuTF z$_?^IGZe(9Pt0F^Ri$m@< z&GJDGm{lJ|*O>U1iR5!znKz)#`{5QZ8x7g(`NKsYXG}jPHF%M!foH5qtTV_i|2k(h zNnZ;rQY}_Xx^foO*(1iiw%*hyu;w34OVlKr5c7uUrsj-6_ykv*ddH`EBo&4Q-FjNY zFxpw{PUU3?Y@~7U$)t0m**P(hvsX5pBmZJRIYYhU#Z4%5dkVYBPa*R1uikQ71$8`o z@s7-GK)mrUWYFuBrTBzgpL)j{ERrZSG)m&rL@BhA)zJ;cO6?|)5cYNuGrWR6 z)lI5z?F)+f7Dq3*q?0kM>20J`+jr_{CUu#}J691iM@0nZE^-koq1h*LMp-Cjs#>bl z79R{7R1zDE0dHzULm(f@4LWGBP3P2{^Rp(kYrTo!&y|dQUP1m-{mXhQR|T~$IR+97 zD5f?qEM4Le`CwI5HNcV9P@40F9=%tU#9Bb&kLJ9|iI=HEK48+v^Zi%urgZX*{r})N zGjv@f2MQ~U^MUi~Py0iriP*C3wd|e+2}Gm9f0!2SwC?$6lj>|+mrm=iPy(9>uKF>s zpv|h-gJMtKi|;RlfMX3zp*Vt0pq*r+)T6Ykx@a9D zg_^8A;EG#7Co58|E}j+6g%OIrHCg%RAb3f94}vx}%`()9T0q(ALi6Fl29=*$fN^{K z!C|T))q?1L_}Ht}K}7+=zO5)#V0D42&M3pksLw|FjNW~&c8;QL?_g3@cD~QNwZp%! zs2S1?JYzgRE9KoGsiQ-e9oR@IUrdq<{V4VyackO|PUVTuj5*@2Oe}_)Y9*Xiruyzi zQ{*ImS&W)kj0urW@Zvfv==WgmA#=%WKNlgj69HJxVE;*H(SB+zO@~}hduDF&RhesB zMKreppRrV;9Sn@og8bn+6YjSUSS7ET)!3KJRQy-&#xk0&>ZySq^a(B@$2%nv+txJ9 z)ObCoVbrYg=3IU`>EMHeW_kqu+`bBZ+?>FBwVl7xKgE;DOHraT8v&)^8;r`Mt zmUT+XGIiv@=rm^xOJvEuE_Z#jmtlF!^@TZ|d~rK(*F7%fS}XkGl~i%Yc}c$RT3O4t z{+1(+a#!EI4AomrzW`x^bFTrlS6@}Qwj};?wH6=o?6`;ca#iIU->SdvR+M3j;4JNj zZ)$}%b1kg1XlNL#JiYOHrmnUNbuWCUjH^bU%iTT-!*H_1$NGnL?3ScWn8RqUjNl?h z?1@VCP-IjDQNZ}^*ECdYD`836s1X*;GdbCsw1fQI#pG!E2p0OV64S6*+P^Q)1Syd@ zU74&H;jKq!$TS69SH5lcf9%Kc1pZOTw(u6B12)agE>*oZe4JRX*u^|K6Z)>ux|H%J zBfENn(FJwPcY_j{QyxISC@CdQOKMNq4CaFdmN8M_tSDLJH2(W^igb#eu^?feHzWU* zchlV*@L&9){IiWlP?hps{Ar$6M@JK(n(_KQf!~D@TRsF3N_xu%veXj|`Ok2l8`5H3 zG*aKlCQ8M4K+V%$S&v%vgRv^vbBnDayYvxUKFL%KMCk4Q7|h_ z-zbXtXGj^HPVkl#7D6ezIxEul8+BCEOy=m0=XW~3xdjEy+^?5vt3xyegS62L@D2<_ z?TkMN0iQLdj`6T?Z{`#HL>i&>(q)yIkSa1fnUt26ik2=yCrO%eiIh10o#zvm zCr`$8M0%iXkul_4bk-9qUw`iD!vD#`3aoBPihsJDMHj8Cta-f}8wwTi2*DoL#}_z? zrCz|Y34P`T*!8FD;=TDhAL6&Q=XJzvxYq+u@%yvSGFI1oen<2&C_aAxKIEovqPNWzT0!=^-r1woo)kE?_V z#P_Vb6vf((^yacfAzqSv=r58hVqM^7Nwn;d-=44gaWI>u4MhYEhWY6105a+GsES z11T%5j3e%Gv0sS(!ROrJ5^)e#G~HfIB|K2+&npQyPq$=Rb~ax5msgLoT!W=D5Cp37 zp#4!UrvGTmarS1TJWhfw?&p=`pqL@~dr>Z{bGed~A7#ly&2Lqh9P;b2HfvBNXqwX( zIthqPS`q$tf1UHSKfV5N0kp<>a%fF*Ssx$L(;3ww>|BD@P{u^?#NJLtP8D+FEZZv1 zL6|QwZf)o{8imMk?@Z25uO!Sdm)&j`mzb#TA@fG;`-x`Kto>R?-4gS01$@*6-JQa0 zZ6@c?!Uq^^e4lxJ6a3aP;Xen%>;(116k|D;3Mu03R6q_3-^Upn(#IU(;TW$Dst>vj z*S%giK5psgVCpbf80b-Nlq091p;5oHv$3&x2QFQ=wOL1Q(I}?)`8ql%!N&{bl!`#a zhyny^fBdIh#@STRuW2fb1EwcPS=4~YzsD14!((5cz`gBD~zy{nTzy z{bR0er4vwMNt;LzUs-{uue5r11Zq;U~#X<9h=NRpfU^(V}4iBFssfI${($ShE>% zkzZ^iOoy9SA%35{k&iOXm>Ke$NuZ8T-_o(Kh8M zw8cp4shid9CCsTliGURa8AR&MeU*@KwtahEj@WTjm{(Bh>O+JjyzNQCI*?`C3EyX< zox>wlwwq>d{)|$di%`@a4BsUd>vb`tqK$w*l!L0lVEfjkF5X8`8;Xo8$iD2RY5Aa2 zyZ@?VzZ5#wi8xjW>W)BYlElff1%?+84Cw4)eNmAxpqfj6@bevg@U!dnH>CO&&R~-@ zfuIoX5l{@u8@W8S;5-Qa-rY(iBYY6@*x4kRF>s)U`?jm{h6DbkOW%QIU1!8AT$(8h z|KEEh@)ocnEYHDK$jOa@pi7386gRS0L?q%ig`!d9ZLbl?pMdMp)Nq)g#fnIs4>;#Q z7rK(D2u(7ZqkC`U*I&+8b5RBm!6@@dpj373C@@1 zzesOn+wap7d3|qrJ-)2?YWwF!XKKz~hlj93w-i;2;$+S#-)Mo5WYI43#s}CAdnIH7>8m)W&lCaJ4vZ>ea~{YbX%}=_NSs6)5ES zcNeak99%E>u}gZvYe>wCg|TTN@jN6`7lQg2ASv=OJcsn$>qtXGpU2SUK;4OgHwC9H&R8()u_A+! zQWpP#U<1o}Tn6G&p=R9LWdB`UO6t3d>=JOsMgGT+yk{2o9$oFbaF)biVf(iSOR&hC zbL|Pdz40Qp9knJFYKn6e1`h{&T^j*+tFNJ?rc(Lzr_+2|keAX~Q(2)>8;neZQ+Cm? zai5}p4SqAw#GD(U9l56Yil(Y50!3D{g-8oqMV=99VLs>kA-q)GOl^kdUNq$|6 zedq|TVh8&ICAViMZDnuE8&L(ipH`WS)YK@2?AP5Z^3d{EagpBNg&Q2-6U-zXtd??7 z<3?);WM(!+yq9lKG=R?%h2D87QEDIqv<7aeJ&JpU6>kV zt6u>-Jh075^)yIcke1VYI~RRe5MVp*Z<8v(W?L%K(p|S+0a^XmL`+;i78$cC7Iq6x z@&_t)XcRqCVcd_Kzp0sLj}NiSX_`H8s#MJ8M@UK??d>h>f7~3uNlfi2 z1E-p6PZZ_q_9oh#l4TlC_f1e6oUrFSiBKhyZTn)12aK5DPt#B#mU^b8f-zVR-j*jP zKc{I1(tP9J-H?p%Kp-I3j}myCHo5e8cUfMn8cq-BMs2wn=^IQB3d$Q#%gtS5o$)wr z;rCr43E{l=fRd(}?6m{WE|ow+rRXfzyx$t_XKqKz` z%(c6If^i5T+cif~3Mj+M zDPLo=gn6-F1&;RSJXfI!VfA)ndF$7t zmc{$Awa+yiOX`|GR87*=uW?F}4h5Iw-=wyObYurz7aerVf`S8g7lOK{WTrZfL!e%D zyp}YlC=tw1pWB$mERlS%;D%N^G~x}E9*}KN$@#)=tWp` z@S1wC8_35=Zwgm=Z8+!&uvcBlPa==OoK84-;kRHeg`B$F0`raQ1+La`5JXks(;Ys# zm3MB|!Mu4Y+x9MhbLQ;XFPN?bjq_5egE%~wXRdLulIO18sM0Rc$M70bV=zpcU1kc} zZc=@09*mxl+xkN7y!h?&aGg2VLgQ1SqZNly;k#l3C#WQMXk#6QDf;TuIalB6T_4e> z;e@_-cd)Y&i#7d=JRXvK>gCo6lR(nv3ip~>fj$WVh7o`e?_9&+`?N&Ge?h}l%CsV{ zN>T)eBJM7D#k5Gjhi_)B&0@Zt+>~?hs25lDUr$X!>J2I@CY%lMsY|DfH?CNBjD#nT z)s!M>!S*BQ4Ees(d?sp~BV}tAAXTq?1Ir<9Y|lG)>H;jf9tj+!g&qxeT4n-*rDOaC zA_45#HV6>QQYFA33H|$d8$Ne~+s$=SH)SPh-gl+H%1R<6xIOkFiI=`i*HE%7ua37a z+<=%ylxxjG<|Y4(zT)jmuHdmPHvSRT-?3)kM@~4wU@3Y+P4^_Oa0Ps{RM_6CtH27w zh;K;EY?RO5}$%J(4VM)9H{9Fq)|JMlo8xYP@+2u}s#nJTFq!$FIwcg(5J z##>}7aTpnom4=m#T;IeV8QXRYnsPH~8CdiSiPi_?D(lVj^m*_R|6sg^0A#}^!JHV& z0@$UvsDHhjs20}m?@BTx!x%At&7uhIb$0st^&|bCb?$|?fO#H&QB9AdU&7$<2v*Ka z*fxZqidtUdr7hmn=X0X&h#D;JUI&rH;YD!~CKv2C0=3a+RvU;7GjzWrgRpWJE*CDpZeB=VE?SORPFhyJuF(;E zDX1r?HSu(a^OJ$XiI6AuB}`$Jf4>7fw1gD#7>?sk)@*9S5lP0F_PedmB}7Y_{jPZRWFfx8yRtTRWZ$;n zKiy<7F2|klM=l}o)q#KTM|nK8w@><8{H#wR-sQ<^UkSZ`=LWe&Yw%V46w z?J$|5jixiNlK+m|;oAT4*|XY*jX^U=3`W*av;}@|zj?E|YxX8`Ufqr8Zgkax8u9D5 z;O|_=X9_lt+cDNX%*j=-GmNlJN{l;MQ~6JQGNg#qj+34(ur_xafW0%sJfW>!yHSTz z^fhK==AyGWEALC7S#L(aVnx+kjg(Q6R3yn9voS;t=#LE<5scgjU<=QS&ol2t1R3>~ zsj==v6y)TOh_e4ecFox;nz5u~p7^oEz2u57I5QASw4h?z5l+P<%sCPjJq7{2Y=kzi zQ#nA|e9oGpN-g9qUxS3G-w2dtQOk%*?TlEIvHi@cD6|}(64I-Ei2_d(#LBQ%`e+E< zXt?f?z%sbA5aAFRpm$kHsovMguaLpHFLOt8oO2m-UUP|?7`nx1AeGOcIvyE4G-qN9LKp@mcrZRm$v2z>_*f(ua^X7<>Lnc&Mb2 zzQWm-`DKE~B&VkqU73$vcaz_IiKT`iv!Ck^y~CVZqx&D;iK!@PaEuSbmWYgW1YA^b zsb~&`j0GA7X^D+-sX83T)nlLx)`)Y&ny(dV?PjZ=&TiFfDg?3S2cMMVX3l$*S9n#} zccJ*IXuj4A{~X~B4ylv-cd!S1{O#+6ximR{~}t`0}or5~iKSS@&l`7o)C5MahyeTHnhaExk>>HB7#J zn{@|(ocQ;ro>bd(f2!6d)zwEDk^MrNkeg-BcRZUW@`jrh?o4UL4uSBSxt3n%9tz3-vo5AYflpfDHEA5)D6T;SXHvWi zP^z3#Nm$|+$ z$!>?hq0Kjl4%ZC@aZMYYLpG_4E@dcc#4nGv9{dz{Xozfng*o;gt)d!~!D4FDDFg(t z#5=+QX+BDde>@jCM11`_k)&KYnQMCT>k)4qe`^@L@CkO{SOwCHgA+G+PeU_m_hv=F zOFmHVrO=Il8GNh6OX06=+dh8;oj?=ro@k=)`z4qnR zzA`a>oZAYUh%cqf$*bS+xSkxw4MOo(J-!+#B-0Amb_LWyQRn;o^J!W8U2v(lsdxO|O3f7-P^0$8 z9cO67){jrl2;t_CPtf;MkUzd!e_{z2V|3xe%iTZVuq))#%II@B<5B!V{)Q|rf@Wg5 zaig`;klrT^{qnk&wjiU#ovy$1Epizzk76xVMt5H!-@P$7Yo9}RDxN|XU~U{ZmcLVr zeF4BrJ8+L&6aydqp(9@Sj~Z}>3R3P-^LxAu5=FWUmq0hEF|*WR&O#A0NKg5dc~_Bn z@-ke&sudJI;q$!KHvIX~72iLde;|?qV6$J^U4vc3HgOSgp*BV_`m8mjth2OnjsO(tRQJZC+%pjjG#I z-uPP{UZ}T@zcg+dS0_LIS8SbmmpcQ-c+E};Q#MQh1tPOJG5=z|N&Q4qN6arE4b%{Y zf|cC@^-3Rfjv+TCGseIs?jzt9xQPdIB)O}&8~R^y=;!@DDEW!Tt5tB|aYadNNzmxp zpXsN@kdSA!aM4Uy9fx?{9o>iT-H!N-tr63|y_GEKmNSPc#0KAzmR?spn*J~cd5qny zmoPCMJ+~&li~HS;d2yRQ*qm;R=M`i ze^(F7ntRYWA(cu02*{3X&p_2yfqWL>+rA^)ZhS<@O_sBnC#_TvFmjAG}iRr zhgAQjOXjSszgJK#q*kEkz-Y}3rSIkzO0p62P4d%y7tF@nfv5OeY6#+MS>Ac&4&17h zwI|AduM*}UeRCKRvOjKQ;w;g-H#`Kjl$Cm|g9lUWPP1&6P1R{#GjqijLiNSWny^ln z>CU;-%?tT2tS=E5?#4)eCl4%Wc~q7nN@$CwxjPJ)p%%D{6W%S|>$OYu+wsEc zmO7BNeeCHCn?rxf6n3|J1ljI$3MU4q*X?h{c-Q&0UnpA>UH53W6J7T`n*7>8_Rl;I zo=W*==eDB}ySpLdmA7ywL3{M^pKMi_N`)%HEGpo=6PdGPvo5^23@?~6;i7pIqk!i^ zs^R%I7>&Z8cxv;Jr%CVOYMFcwGim)y+pf_AgIw{l&dX^%%|x(eU18`B?QW5Orx(d@ zy5dWD63idPI($QnsCtK1tKa-$G+ccilpuDX31vr=`9pfAH=6a6O0L^Sy;&6#H=#Rf zbiZXh@dgd+%B0(mrY)%<{%M{ZAU+JM(AI!Ivx^Jh)4ODRSE6qxoV8^lC0 z#>4d&z^%MOo5{m)5Dn;rf$=%SFFs4Rx&y;;%& zG#RwyPsm=}Yw}Vyn`kZPfyA9J!9OzX1wl&h)HuU`{=;)1x_)7(nw+aBLtsIB@})G2 zFyMR47__P30S{Qg$vxya|Zc=bmn^T#SRcyb*zJ!S%* zYBQ_^EF*n-mhsvMi1RZe2tBLKWwruXHzC&^W^Nwt$6KOJbmL_59jM4l$ol}=2iWxf z<@5x-<{>ILRW!Z zXF{)AAg&ahw?tHnjU*tYY4U>EojZp#Z0qpZkrMPAs}DIe3&X1A%C+ms@OUi41|Kk3ARreZ1hwOQFv(Y+ zLZL%I&_gU`?}fFe{;W2GXM^8A1W=FT_IRXZ=`QpqfLB^8&AQDb1>Qs~0i-8?ecuQ) zrSV2)!BBbc|DN@KpJD;vF8Fm-o6ln(;h+OB6K8+VX3xgX^hG$sh~;1m?RNJMJ;k(N zT&)GcyAc1|adFx^Y9yrDk4?svK6#o>eG>*$H@pV(RPkR_Z-o{?A~8r=*5lCPVz1+a z)rFAa^KPQS*P?XZ*{{d%VsFDQY`H{6THC!zi#MM1z|wlnzdS<2b(=QK7z~tvS{L0+WP_Z0d2)6o%>7p}RsI zqCl+=tZ72u;KFRQ6k`eX@--@P(m;W&RU_EcSb!mvo0>yC z!LG}cvYn4-Cz`A&pa{n_MCsC6OMokj@?#>aLXCDD`6vtP;PSMgXe0S0CUys4cv(C( z4U#zxi*0#C!LhUzW;!_y>NYL~U4tsnD+$)}HHP;ww7;XhH|IgU5MxZpCFcF$i_NFN zk2D&$4Vd8|=;Aqs@;W796YMTb&w;xcE5xTN^>lL*qtHb#)B#;YU@s*i6SL~c@@Mqg{* zog{By9;umz%ir*H56X2qZrYuO`#FyU^RrPWpyicCvX^~i8^g^{wbkhJf8_+V znK=nPCTYLL{R|!%!>v(Gojw#0=<5qz7N%C|J&__$5J;%h$36BSbY=Mofi`LAz zP^r(|fRO$<>LLREf!8PH2ceUgC+NZf&S02qO+R1FeG2_S#m33h0Y#uH%Y|l^>~v1(tR6)EK_+f?zZDH zbWs|dK$?5x;|=yC=}l43E)^pa8eI4>)aDwu=-#e>Fn#3w>Am9=$ECt6=E*?4!jAp+ z8Az+xevn=N!d5qnmwd-0?7j6AGDsh?@G{eVl_{s2jTMF${APQX>Ob`hg5;!|@P6(b zm?~={>mBb@EDeiz7NO`+2^`C`LNRO7(&z6klgm*!f!@#V?bmnAg(5 zSUY$#$S6t*l#0ag9k^(Wn;wRZQsr(;^iaH@l?s|R;TZ4_ZhZ;09MG$Q2^BK=bg>}) zw!>dTU#eeT9>OrQvc| zbKWT)65%5RPMM1?cG*24otOswPz_~8fNRy`CTu^}s1est9r&x&cv&7smV3=#{vL_6 zJ%pw|cL>oxyZH+t8)LQCDE?wO9(^vW6oP#P`MPDn<2Ft+ejbj-x(Qi}{6Wcnm6-e% zQntUcJ3D3sTU@ZMIT%TlAIU!)sjeyMuJ}(2?(dh07fPatg&?{5Y0x+GJhRMAS?TSU zbNpc$kG=!q0IJl~CBo zI8}-%f?I+jDjEn3fw_cKS})1L(~3()QXKEUN3*)dSe7{CzN`8A@|c^92ypUu#3iM5 zjqgtTAdoJV$rYMuQJ=(ExnvA!EdhTeTF+WOa)rlv>u4i;-F zos!Fa7^10KuseH@fVu91YHeW&9gAG4-t7ldP*q${6(duPkJquu?Dz)iKU-d6^aGN- zWRl9sm0oK60jaw`=JudBVvqQ(ami|U80?4&QNDXRB)eU6;#k?gNOY>JdbMWVd`b!P zptoc19(0JcpaG?b!4E|yRfCi5$Yl7?@;J;xW`tuJv8&XeB0A}e{v6`dbaS$wyl57f zw-Ot3z+qa(CR}X%-QnO=2JlUPwEHq)07#6qE~#THcYDG$c!ix{5VD1*caSEwIXpn@ z54j_P_Ensabzp1 zg}Uua`*R+hHeH4=xDBC3hXu%F1VeRH?)u*8Xk0W-sBLMZVem z=Pz0mwvr1E;`SY}JVhS*j69_Xaap*1Tlt~dr`wJIOC1Bmf23a28|r2nR9r?SD{=7{CE!1>^MC4Xutb(b6;bxh5O0e9b?kD$Yi)U zQk$>C)l%+At=7l4WsPFb zPrT(P4_DUc{Dx{hI4y{?hjP)s(d?JT7=XbP`wvxh+1oWbdOq0!lQ=DI$UZCJOzwX$ z>k4(3G%TH^<`Nx8dnG+LiRoccz;LUM*!(5vbZk%)h>nx|z3xaRL7gn$|AE<-SH1cB zNJ&;{x<>mkoo#S;S}lR%&<(RK_W(vUF|CH6**)$HIfj3h*;#-x_uD}Td#Dz36L9}y z>@t$BbBM}=Id+kdLh`?jk7N=zQf)ESG8dMFr{Tu_WS2hB5)*~CRM@&PSqP14aB*83 zi`5I1f@kGk{VAdF;N`_*lG7~JwkJY~{9BA|LatHyTas;@$1y=$SpI}88tCNVjQi;_ zbBCdqrv!>kK}p4j4-(|oP1<zk=^pTw^Fv%(j@6r(~ZValR~=#Ho0^ zOsT1dak$&s$bR62LHGahpv*&9@{}NJK|~&R9~`8a&B=$Y<1mV`cEp?X(#OG26tJ#3 zmUL6g0;Hoifdt`h-l{#@?dYr{f4<3BKtnc68_ar5HwpP70sLt&wmOB5Ta;!{+?$RO z@mwC48^u2MV`i=B3o#~u46tox(Nqwb5wpr!hoD1#PqHW`uwA;+4Cg=j{*$fF1PsYAn zCu;k~P@HvtCgKS;Df~P4pn#)f6uv2Ur^Fj zEm)q*cpQ7yhF_POzvbITe4I?MvEd)7R)aO-D31@68is#bUYKl#bnmgET0FQF2gXhn zM9hJ?x`i~oz|>l025vtJ4A+jIGUi^*i5v-jy3LrnkSZLVq9+r*qED^i zQX@JfJ5+a)W#hs9?k$A^gGd&A1Ilc8DY9QwvtE? zA?X+4&B;pI!TOSISGaPoM zlP6KB)T77wg-hFfLasmFI#|U#cJTGHKtBZjD{T7D^3AWS{ad+dl#VfK;PiA&+)_xhN@kLHG2ixPehO&5?#4#FWf4!i5y<< z-MR){Fehj$2<2$db?e+%)M55`P(BS}@~I?54x%rm@{oP3v;QtWE!XLzpH1zYh-&tpw~ z|KxjYQvO|p2**8FezaF?XVQ3N^}h9qKeX%z!Y-M`y{ zziK>E`ue*RSPqc;_~F1o_5mIdtiBo<@&bVOe6d}5?Y$f@;V~;DZRKebA)|1t$JirI zg6mnTCjI||PuF8qP0s%ZMIRu+8V-Qbnf(FMU9mb<%0MxZ`)`!90nc(wgEIt+8eC(t0aVu_+L&LhKQn0kqn%y>bQM8U1(6 zbmOrC0L+eF0qsgRVwBbHnlLBh%F3;P{Te8`UkvsXi7kt7@I2&-)yn6!XUR4^Y!Yj( zS04$SrjdH8|FSOY5ZLzb#H#CxbzQCHg%FaA#&XUMgXR2w3}dhxQGdk>&iaP!DxZ3| zT=LS&kQv`5>PvZ}t=GhAKGGL58&h#4m_Gu(Vb9ZlT`jRtyLK}I(! z)cRLW>vTj<*DAOC9XdQ4@y`=DJD!PT9^nJXSpKxewgk?4hu2`IwX*E`=j`yN83NA8 zx(L7}NM%i|5xFLexHr=X4E%t#3&22rw;3sL+YzJ zh9_aCA)GKDcAq?W$lg=K|D!C9b-)KDzSQ?FN5LfC3Ug%GOSs^|sqnhkiHbb)Kn6KT(1^!o0jxiVZrA|78}b*oFey%?1WEvX42{zw?O5=J1q1>8yd%;@kW_A z*2x%yRk^4Gj#d*d#Q=<5IHvp9kt8kvKzT4~EnOKW_E4|^50H7b2^Zd!_#XmeD5k;% z0pLs~RH{Fb3l^ZKVowKC=6lH+M#Q`in)`7-s_;VP^IeyHB>;^nP&{=q z?|~}6r)nOk9MXF#;eo2Urvx4-`UffkjdPOPg+v|<7NGm3G77s1#4Ks5uVv_ z+XoZ@*fu|We&&$7fdcTgmVohjQ+34ilF0vIW>fJP7E53*d#6`SDFl z=XK%!zz)j<`e8IkaLloke;HsB@6Qh&fERIVJ)ma8577EKcmGxh_Cs4Vn$Cl2kqGcA z>K%jzEe3d9o>f40@&VjPD?Hq5awJ~G0U@oaH%@yppGdJ;_y7s+Q!nS)M-lAP}w7)Uf% z8GlA|!ur9BZ_L%DXGH*zsDu2$)5c8Qyga=N4ISCGa6?) z?4R;1PwRcD#cdStWfKpwoClfMgKP-uCT!(5j*Ijs$xM6j^*ra8Ug`OC|^dATC zy9)&KA~~Slj8r@cUG%?@#PFCIXo!s(VAol4TI9Ba_}9D4=)Jqz0F`v0;YqwJZg8L}9eqQXEg*L=*X1 zzq{U5XNuzz=yYpOY*5T`Emt=bu?qqyt9Ow7jlMgc#sI?Ne3{#cZ~lJ6U;!v3{s+H4 zz+`D}y0Z=y3=V+fy7tgxS7^KS<^K-)Fep_0i(gj%ByL#&p{GhurV?Ngck-(vZ`Rp9HSUW#%%9mqjym|v0cKhMm+Py(<;aQ`yA)cbSs+hErs zEJXRoS}XMC8~phfyI(Z_>kQ|=s6fZni{D!FUg1K22{Kh-NQNMgg-n&p@B5Jwvi#@f z{7btcM7dtAm3GN62;$s*&YfxWE8~|e3J8c>ue)^^x|q(~t7@DM29I4~J$+y71)t)9 zWrCRJX#rtr=KJ|Zzux|9{=U-;+`agzY};MjgsAw^v;&8L@Aevg002=IZkH*uifp2E zyC*>rJCCief{%(MSu($j|J{tY?; zdj$fX>kxp~lv2oA@o6X~_*VcX!Bh8U?OpPH7p}hK@8|oxw`ExmCNmJaM)p_*9T#79 z3(s~cA^k8hp3wiPtoT}W;5Cm=nQgfKA#kccz_E!W3A8%TKc25byW!RO5IeDxzmTd` zFH<`Jpl0~jr&+mPBtXwA?1c&pumnSI_=WBvP<=4;fy3i9nHo_5U_Rt>{D&vMIDytr zpmcSCUJ#w>Nf4kHohpv7mO3~YdPwVZQ_@*eBvaAs752I!2oR4J-i6}~?PH#v`&p7+ zVEAKO@B!d|wVvFL7~$XCQr2OCQfOM*bE6kpaG30Ix!R;h&mk;%um(-;Rrw@juCWenzI{ABmw!J z6pHk%&G+`W#L(22+*-Z1`k;sXr)++a(MT}~pjD#fdPnbCQhLb~yoA@lDXkOj;ju*0 zl^%S7y1l6O?&%-IdzRgl-go#9ZufV(Z7CjHpMSCyKW7sMOUmOh?jBTuYN!k7XL-05KMtRP18x4Mn9K!Z869*Erj+*n4yfa z{2Z4I1y+>+HcQFXe>qEoqMw}n;-jmq5Vmk8D6?RKQ#{&pCmM*;Gn z1*nB9FX01nKKD}P`vu03=?coH`RNn57gX||MymDpymfnvU5d5FuLcGEPrXPcDEb2- z0}NMy?gjK?L@QvXKe0ABaL9AFds6&9;#2H8o|KIARS%8+Fm6*$Fcm!;l5kL4Ui~Yl z3Zb4-H9=T_O9>`RhZ72rQG{NL^0xp|*n@x|bL5%Qf`-5(R@*YAkGnLZ6?ON1P`?R( z18X}%dgBB7P3jviQ4m;T7?Xpr@daN;Kx^e7t1^N9A$mH-W}ZaVV=bsV@Y>pjg? zeGr8dQ%T&0d{VC6=;w{b_`cBEFqm{GT(lxXl%TYVTvtG}D1T~bt=n#;z07)%ojWi( zw1)?INrOyaKVoA>L$h6*`Q3cv+0=Z>qIs>G&J6tHR37w$#&Z;j!SaDutQbQnFSh)x zVtsJ#Z&Qdg{M6G)e@zA|`zZ;-FIp9%ENUkVN^#>(bO~gr{et^&{{BJ zPIZ_VGyw_H$FUDj*3~<^47lga7v6od8^@_T6vh`+DLQFR@MY(Zh%5d7XiwUk@vU++Dtv zZi`*b8gTeF>O7Z~^T2uZ`74L~VXMy$uRj01@L}Zcg)f(OmTtVwIaSfAa2F}d0k!(n zZ}m}5@xzX_YvhO@)$`ON@NsquS0g62#xT5$jJ)wH+mW3{cl$PHT?y~oh@2iB_t@TT zg;2E7gW9n3zFbCQVT&?lPVyCm*I6^L4`Px$BZ3! zX|1(HwX5hvQp<5Md=>0Ne%f9J0r{q%bqUU1eo6fq+c?s@Vb!ZyQb)WDsPG;wA7D0T z-FBHNxC@J zv$;95nQ#HqE$S{`iAVJ|4s9w}HhK);Our4->WcVu$6kcU$L5*(1?A@EtrlaIGZpvi z0{15|7L)On-CSuiqPa7&jhkNQH!FG`RrU0fP6N%0 zOS`Bi^TShPPxOo+l5I7Q$>1ffMq96D?ZL>ui;T8nxJh2NTv|*qLu%|Cw>xO|sYzXAxnOjOMj zO2Q8{fxdOi?VU)2>*s!DNYpyvP6?)a<&R3o>c^0dl@5AvHmhk;)=RA`Iq{lT^O!W* z@@~ekWzkB@#JAtLSy0nrvH}?&6%uZclmB4=``UKDr$+a#HF#}(^O4+xDMx!&j zBG(Ddlimn#J9IN5M;yNLRUXBbi$}44HTK@j+#7Sb-nAx^_u}C$Qb+f>$gZ-Z150>m z;cb08RmSxdTPQ0xBk2yFXXEDR)Rg5RSKD2gKkyc#bU4Q^RmWG)BqB{OcT`jk>f89I z3a`#qRZCTV*mm8kP(YsPSB)OE{C%I-OH_n0d z*1)Ctf`Mi9U5ks%ZkpNo<1z-uTRDG#vSNLXON)yS8=G?KBcdcx&(A z;J7`s2Koxr^WC-<7Z(ruY-*P?F`?hn0=%o;R*F1lQ%RV${PryYszKqoq6~us9#%M_ zVM4Kb%?4!^lg3=7Afcz}qqe7V^72xbwp+#s1O-Bkqsv#<`auF?oW=@XA}@ZaZU*yv zc4#I{=98{5(NkNB8izkoipv*gw@(v^m{u(QE1(=1@l``gHi1qt`y-uF{zp26%#Zts z(!l+RypQPfuUm(>@~THcp=>?Nb-c21Lj7*c=^yW1nb7xGmDFQ!bRhY++&+kV^aOl+ za>-ZLSxGk?zGiR#&;V>ghyPv{xQ&+nAls-oGqa(dkEaUo?i!TPJqwEh((OxT_Ce?#7nyTFKNIfE2V*)oq; z#felynuP8v{UC`)kMysABET0o*yFw=x7|Hd)K>b-IVQ1PZc^Li ztrDQPzP7=9EnmrVT_kY;X;4p*I`Q8?%u>RR8E=AA^F#fqzV{_LGO z{rAYRKbZ?#*x0(1HG9aRwx=Hm6t#q0_8GhR5}Gvj$ntGSsdpDxT(gp(G&-*C?ac~l zk?mdeI7N7zX4Oa&Bq-d+eLWGe+5dQg@xvmjL=#ed%exKQ9&vgU5GJNbkV|-9Fwkkna6nW2y)<0|3clz+C%=X?l8r~q3a8m z7A%v8J_&*Ia(N2zs;PBc*SjX}@Hu~0&RP2U#8s&TnwlDxQJa=A|Mr6vfg3_-GcYuK zoig-QLfz_-Y~fsHfU?|Oi%zX`Ur4~Gb-;`e>Pxd-sHE48(1ApI1QKr2;ncqjvG0C+ zxQ&;7TZ|c+PLnZRrp@2^(b<)6`Db==EuB>X?TeIcr^IUrktyTif3sXH=kqQv;t` z))>XMBkYVNZjAdOYfY-9r16-7CGUzZu1t6Lx6?+*T)$wMZ2cM+X*)Yjh+WOW^_itp zx`aT7|J$AcO{KM^rFw4bir#<557>&;)UrIs6yHnDNCZ0oS_fz`1NXkzh&vR{pF-7@ zo?qj{DGwy8{n89Su~eDY-{6g-C`pG%D5!T^9)8%sW|`11C%)eUSFS`r0M4<#p=wFV zk+=N;LXKwBoS4-|Muvuhs2oQYmZGlLjb)OOlAU80^#mrTI_p35_tV*O8Sz)0Vf-#U=hC!E+N@Nh;B#h^}{j z9Pa6?-F*D~u5>$*<%Ut+W>MqF%Hra~cMkEfooVkPA|fDrFJ68t6Rc*(DSV{w4b%2> zdGlsmT$By-5Ih2U_hIL5uhtfl-4KNs8KFdEai2xs`~qEFy``(KAA?|HeXRHZK?f2d zC`m~C6*S#)6WC*xYVNVzPm6N=ihi*A{KvdOl=n@|C>8o$|1rTz1-y`hrDd}+aK@M` z>hcZvpGHK0^snwA_J@9IZ?g^>uaaMg&KF4<{g&eLy$@tzeY$09Pvm^f>|vo^lISKp zF9Feg)&FN-pLzeVPOtlul|y#Bgv}{hiF#FopJwR3-rkG*+uhtW6?erMS*tjkAoB8) z)yKa0@)}`%#-J$c)ryi6$vs5ZDdhS8pYeS6i^AN{E}+ zG61mg&bw#IfiLb3-n!1Kf9(6|{xDZG$4FIuzz%cg%1BG|PpOP)R+hHh zZY>ur<7ZnY-cCh8u<3&%Y&KYh+M3+6P64076B4YeMsoN>8LuvebA#`tqdStGq|LSF&F zCw{Ttx*+DwXC~huT08HnULyuhRjVI?CZpP&5qf11Jv(M67<{(Ksnv~9lzKNdR_@Id zvGL&b%M=c#{j=~30rG`Pu`k?ScV}cI{PwixUrQ(jg&Am>cQh&lDeyyUK76>q819Su zo?!-4`(fT3UgZDXXkXD@%Q z)bAekfM)gJVs9{UbYcSaPh_nrK-09*cGSz4oD{I~vw>st3O)OqXNi~i?5+>Z&hm{i zgocHcZ*95uQryU_DC8vPjOBKSiFJa&Gu z4MCzXb$`$IKMHO{{mxdzf+7L1+D_FoMEKzJ1aoTAwc~h+dV(bZv-Cu#DV!A8rM(!d z3=ra*<+< z6f7wyEqsMDT8691Ez^9Pp-T^HzPQDXFEV$&xlCo5K{4v^O_XBtoIe-U-@MxsiJ#Iw zXOb-_?kmpjGlCi~JT?mcwtSi4Q`yw8HXMKJp;B~)u*U^a^_<@{jz61z%kE@7vjFd% z8T;rft44j_(l=2H48Lb5vSBkuw}3WYqlt>vH| zFC%*AIvw6_!R3iUBASmsdOg@z@xl*&YpJMN-nLPBaoC~yvCoU`n5RhC6yo`FKo-@& z?C9^icL$$?Lo;BXSl-k2;)77s27n|NAmKYOJ>YpX_xe! zd&lSG<&)y)W28RY$!-T+Bayu#+ApfjoQtAvxobVYx;gqq<=u#KV;S!q7b5JL^fW_< zC#jNf;QUKJ&VMD%z1_!8$f;_egK%K-BckQBo~Bd)7pve&AY3&J@c^;7&@*3VDO=7x z`@_Gc+O25xm$i+R8U4L%LE}g%@yV&zOp!jlOHR&?4DPPRcABGu)9<}%^E7PjzfeaF znA209U%v7{FJBQq(C=+y^WLonH+gkeU0*G{KRmasHcEcdu$=G8V(%X+BjCxk(EV;D zo1`)JGN=OD(7-5^cu8GVS_4O^>Rd zdf!o=2l&d852S0q>eTy6Th7-0cOz&aWrWMJBnfUdN4BuBM6_GlXEr!gpB3^hSw`3E zjxy(%8SE?wCZgUS(cR<~Rg3?rC$KmC6MDQ&KZfVSm0np9zi+9(a>;a<*DLk2O0Y4t z>O83Z1o;@N{d-X#OR>MB!hMBg9liG8+MWOdG--ltP}_XT5{7^~GJ;5JX-nAhE-*(7 zv)M$<8Ta!b^NCzlgFp8WmeFj#V;H)flopIsoH(TNkUcFPMX4BOlj$W1e zdkFNxS%^?u6)qAy>h&)95Ch-W+fRw~e@M(Js~^@&avN}dUuUjNWw_`3z5Qz>j!B09 zZOOdRdYk86XB+dIcbDD20_KL5GQ;4P(RT*FBVtz>Sp}VE} zTPw*w2AAg{xbNC6N5`Q)$68e0jq~_s!{VqYxno1#i)H;|qdMPAJKSC5inR3BSDH)2 zdn!vS-U7o{4ylQcuYaM|(bK!GVdoB#|FaulVG`RLVBy@6s=dbf+w8A2L3vjXDFlm^ z+>cLP>@uL}@uGO@DFZ6MCRIG%1(u5W%$#t`vn8qtLkFx zf*6fa{g1Bei8PgWr)RPNQ6r1Rt)4JbtGb9s093f+7W>XFWP$!liTMDKCqfMf647l| zV@9g4Moir|QOk>m_<>0Ww0)~ct5`t11|5%$F3Y2bk1i1Dr&2mCT{BIK5WF`i6oY&` zqhteqM1}ec`An1 z4TG7|ndDd6YY`VXdAk7lsi`~rj73n_i}=^EQEtxD%pBd9fQ}*3( zx)&dD5E0?#0+_qF_d>%pE+Yj?mEBWiy1*!zF3?*JOU|k`$v?6(k7uJnr^%<05w|(Y zt=Zj&bLj7|6nhAwaIdZuj}@D2%v<91 zjVdYWHj!4aXI($pWLin?(6{6;y!Fjo9Z+WAZoJ^hv3|9eq}jXnDOoExKk|a$UcV_h zsjxmYE*co8B5A6ttCvCZyssXm#l0Ismx-{~1fGs3vQd@I7o{JwQLy_Odk0w|-6Hv0@(Qn>y%4NbONyu9c5axbH6Dc&o!xg8<98Ser1e`5bgnZVs3SGf2(zA@kifdgHLz7E*greX zte1MHzN`n0NMpsYKF-c`4oNE2k+R|fFmKRP@Pm|LVagE23GH`e?;(VLT9J=^a`?5B zkgY=P54DU0ntv~66O&#k`HR=e}0+Vc;hNg;-)mUJ%A2BRN|{IqUF8gb3}=iZs_ zStg@M`0E-w8{e|Tvn^KRBe%MS%fYj6l_XMN6OEQ8f6n9~sh4>ZpFy{xrBkUG`LN;)hw;Q|3FySSkJ&(b>Bh-JAZvd}5_UZK?F8KYBOA zpR3_X?+zdzRxPIizXat`dd$JiBXX5h#>`DbKu#uH%b<2Xp|*z`zq0~k?aGV9;_aokAYz>g}llSF4k z+pvc18f|S1SSgVvLIKE5Jp3k-LQft|1XDDqx32+H6MGv^YI?zenxOu%4@iP&1$S#9 zcUzGSNuGb(cs?mETg2_RJ1LL1rREnfV{#|H-+DTw*{}z@(B*15apxELxh=&h{zIU7 z5Yxlg_?25X(E0N+Bp3|hED-1eWQYRtae65LV}$61Zf5p4p!P1~b_*YQ&NaC>AlC4C z*PV!O!o;a}hPZ}3fxi@eeg?RNJ#~)HGM_)nTq`fFho?RO+$n-?0@M@~_uAJ1hfDs2 z7qDX#47dGbuq~S8f1i;L2~Ppo@0h--gPjf3{zQIWOc6)`)J;hP-`fMEC_>*6xXGjb z0-tudPX}g#%DhKm8jswVl}9{{8^4qY7kLQ&X=k&vb@M)lLcMr%&(*t@qHQL)u`!f_ z@)CF+Jr_Hd!M?MiHm{f|_7@x%-#m-nYZgII`%m%WAYl)_Y6~y}-0afM5GuQyaxRV`@x@*A+RPGO9!7qitC zd3SO9*H}c-vwQ2K69U4;p*~GF)YQ_%F7!w_Z?un>Q}$+X{>UeXeGj}s+!mAi^vetc zBXD=OC3klAv(&!i-Eg5JaXfRCOa1yFOa{~vwtze{?nq@9#eP#zkmk$nR}ZY0$?u3` zXPbS<2jaYa+oA}`(}Z^Q8#kJTMnzprll7)!|9vPslx7uPY$4T4#~xSEV0rHQ=9L%- zOZ@resNLe#&<^wALOuG`ANJKdErPZCLldw!q|igj2iF){n|v`8=R-tsi?g=OfhzFC`}R{#>7B^{hpyY_wis?l%m&2<485Vv~L0hZix1 zDM31ZxNlASVp{4S-{VpO3n?};zGj_L^f?gw&4l>)SfD}iv@B=j$7g#(OxixzV<3#) zdnvYFD5*&WpOBTR`lr<5HvJEUSXAU99)Ek?DJ_!*EcvRw10{POuT$vS5TFmCkwQ*21(h%n*J0hGZCA8tOmQ8uNq zrssqkmKI&XFSbp=nS0YrXX%~v6oQNhVy)`wb$-`ma=jV-P5^e~o~ zx^jSlxT+uIlj;Nqc5vc4oYI%uOin*{>Ao%@=SRs`sIfaFvV*&Giz`tH)Y=-fQ{BB7 z^l)_5B84yhSLhobugrX1;jR}r2nsV+Vr-~nGv!$GnFL*}R#$s7_3jR(E|i8vi|?5f zO9cN$JW&M0lvs9$<;e%n8r_0 zvK_alyoU1yFuWFf*3VU!qRsoOdbVG?8@uZ4{c84mvPXf(b{>ZHW+~iKkBPeSA+cyG z#&*@HE-TE|6His~{o$54GYg9vw^jM;s(@D+!rOk$FRUxCeAUkHy?SY}iQX6bs@UMN zX5P!lJIVnzGYy{QZ+=}ec}cjaHaW>(6z`;0VwCLr0!5ERA|tW7U$|GHqavLRac1eD zSg|gtx-n;4P$;Ggs~L9TvbMJNmdc)Zx@GC>a*LgVi3F*VjSWZJl_)J6@b0p-@S;t_ z)aK*69MqUqtQH3Nh0U8Ktj-{CD+m}4lPFhmv`GMNyh$q^d~l1SBs)8Mw{fo+4wUM@ z{5S}3E3mcAHjmgU9!yYVP=2&Idubc*w4i@Is@j2Oi}qlGvJbJ|y*EGMjhlHx6%u~` zsWZw^SaIYKQrpOi?XwaRbz!}5!TB0TOX}Qn%Nv^wx=PBxC5{-m^YA1p=x-DYJg~bO zq-o<05~X{hs6UD{OiS=_#zC1)F2kXs>(AbhPbu6( z&7>mNll5wz>*!?5E4AAvl?|g0GZ79lU8+MTWcD+y)EzGVnH0?v-Go z-;pwTgLjeGylyz+Ccd{iD0Dxm%abylv^oow!{og*p-#u^xsY}jj*SceZhncN$tNq4 z^x6}GGK_t0W8&&K40aWMYAdZ}tK-U39_ETlA^2GkrjbIo__vMHXaT!)aOVq3O&j$C zqIwQ`&o8fMjP40BRQw!c?s*sR-3cJlLAtqx4bJ(-%9Fto3Rk!ry;XVY@G$XK$?0>F z%LajLXjxQ!;x?djt?a|zxmbmVhBw7xR5!St-Y90J^Vvm=nKaXO3QD!Tsm|)}GPcER z38$EX-|N<9-2BrL2@-K0sNiK0HNM^Ky}G-)AY{aU)$3zHby>(U+**6x>rBp=Ojxt;JXrSX2bUx#|W_<-ZetG7!HA$jh?GB+q0ntiO7K*I=6=01^W728%< z8BE9fA2wM&ODr-SueKFk(O+hR{nH~zVJ+cN9I}@b{rAu4*T2nKE)P@AszP87wigGA z4Qq9fl@L{YQ+R!8%7L#-$$5tM^{*=7d>K~rEfoVKH{<&bi5?r{jEh~->iftFw6Cx4 zBv@E6yZ_+*vc4?j&DLDoM(I3mLb-&Nq5pKVN2y8k$7f<}*)CFYO2oqfD55F~xan`x zCijS;YrY)sh~Cf?V(vt-G`4lI7?z!8?`-dCZ%45(QFV&mIN&^kyfSKTq=$HpWwruu zy#>tL%*8XHW=7}2c!kl7vOCM}3m<4gAdQOo*i@Z|e#??Wh-2GO&PYz~y1y~9)RAk3HJ ziTGjKFizg6sq{NNdtn9fEctw}SG7UEB^u8f*84eo%o?#9cbrBXhPRf1$BEX-tWWbtM;WX1xj`dg z&BCIIn=_wb^9Ru1fXiD$glXHUzf4K!kqLRt>lWw6OKP%9ALOG*+g01fHo&2gDG2gr z(!Pl6P4U`-()RjYtUmhZ!&jDlW__qHcH-z;4BhsATBaR?EmigPbSA{VK{~4g@E@m! zp_Q+{d>^|<{9yJSOvH5_b{=pr)-RIv=C2m$$5!^zH`GjH;Zu&&CaMdSO=;YWi8BQ+ z`A5{1p<7qLx}mlmwbR+E@vo;FKmk9TJG2ibWw}QmAnP1wn-1BPxFzjBV~L%Ii9T1P zAA`w0(xf|Fwblip`K+G3Gz6kQ2iYe5-B#s745Oh{LM}NGy^ox0*ZL;>^RNeOCCSh_OLNIdVzam?m z0BEHv6*Cyr+h2AXX4+bYjE}$lT>;K5ei)zjfcGl0*K35TS*I^rg_ef3;Kj1u8Zd!r zI}jME{<^n$Q^#`CY)H3Am&y+E5UlJp`jhwR0)%kT#4TVVx&Vm08`@rSSC|xAIJ4WV zm!rNRI@MG?PrBVqogbVce^uC^pg|Fwx2|F&Sx))0R!X5aTuZheaqwi|J>yNGytbQS zFCPR*f~tS0^Ka{$^ebEUvMp51^UPAVCYJz{2Wt4W@r2Gu)(+V2_B76j{KpQq4*?5j zk@cYd!)CN-KWuYq3akIuw-6^!quF}dld{3%);BpXp>GRU73!L-{Bz$}z+ejmno{5*@Ul?U0=xO#$nh0F2@y^I z`k6>zbug9s1NS>5B%A8(hcova9(@>&?rRH|-Ecm8t(p{*$eq8|(Vc<=jGbYB>VYpm zxB14{N!|{4_GK=yk5Uj4zi41hSR{)+m+K+7Dce-!?sUAY&70v&3p(dRpugF_##xV< zo5IBhqkKsI;0f!C{Qi)3jtJ_f z0UW$S;4hSw;n$-Um4?W>=;`?z%PAZ#3nriv_}hfCeNIq+wJ*Xn4dRbQIn{1=b~Gw= zBc;zVRpgrRyuPZ-d|=w7YzNBT*4QJN+wYv}isgnQcJ_V|M5Cc~FoDCvJ$Be%QY&yj z2!J4z9zl&wa>bPCS%hAff%GqtHP|4`5By_~oz4{2@@}Gk>4Pmol`ZxbKwKd`JOIWx+MLNwEPLPD2DutS(PE*sTa1gi@{U_^&=ly0Q zjO%;`KsJQX^N!+3NzAV&S{eEN^j!I$F?jL#yOO5-$04}Oa&?{8{LUTikYR2pH>T7?5~keTLTHV zme9}UzZITZApq&$w|6{S*4v+_UgWl&Jy8BW@wmB02ddh({o~emH|!i+j~@GV|j^plF!6Yq*n9`*aFa> z;zWo@QuIjJ+phtv@HNjl3IE2!yeY4Zz@Zr&&|5#DS#>|8?_eS&ph{T3M#u!<$q>IBXewTK*eupBL&#e5m?9xs9xJQS* zzKg($!mwuE{uSBbf^<`S%=`mGJY;ueU<+2Mv_>)h3B1s-8iE0p##S%q-?}H?jePz0 zEji#sK$lDjlLt)^PBsCc{#Aca5-UU!zk>>KSn|HL{eMEanQ=NpQg9x32AtT1j@^cd zfmO+0voIw)GQxOo;;r7H5GooLA*aY~SnboqUhBw}dQJ1`@4%N2%mrgyKDW40pA zdB~(ZN5VZmaW$g!?M!!$$C}!knkJ*=6lk49c~MW3R;8CTbEb(sfNIv+d&R;X1qDx{ zS_*!#e$Frxu~#Ap6E`tP!+v4}Ayu@E_+yJfB@ltrx*d&2rr+E?I6(SJGm@7lYktP& z(|j5>p>dmi)c9lbYZO_w57i#ryT{Jsaqo)9eQuqq2U*N1_s)nVsHkg>_WW+*50F(b zUaqekTf2pa;I0tT(1}>2d!Ib1aF1x%n(`Nq_jOiAY(gt0T*50+bYqw;O^PCo7IX$? zJuwc$fYA&9Ym>sr3kDYnNgb0DeQ(#5<=wpo!cC$-XDq6^uS`L)4;c$UI@bncoBxNxsy~Hl zVK4bLn^qD!3z#PAZuKYf;K`L$JZycINPlneb_KZ-Tn035)Lm}E^8Pv**-$!NthcRK1!ZsB9CL`i0Xm&Goyd!JK)It-P?o3wlrhQ) zrGfH9y+Zk-gbWuYumad?-g%cd*b0WE-rCedsbJd`8<3<*_izEm$K>z3Bxl-|3nXb+ zxKgo%%&|BIJGb+Rs6;uNeK2#Su_#NWxKL22PuLG1inX1$HP-#VjlY3WJ zWXwBMA)8H|aQxJ(-4K^%w^mM8vCkI-=rbuBRedV0PQ*5%U0K(7 zeo7Np(7~mIb~$EbVF+qL^HNQQgbOdqvYlNxk^H_=AhGnFyv8_auql!YK>at)WYL+= zQCZkU-c9+^?3;-b@;v`kqp60KQ}~+Nc21UE%MS?^&R<&1Mjqh@Dt8Z;p?!3p?Xe0| zT*}oqw1TY+x|B3#r(d6WM|&^0s58Xow2Zc6;M~)X0%GS*eYFTKaLC=7DR<~Dmd396gr?fp&e*~!exc8lGl0q}&dG_a?sk}|)%O~``Dk~+^XZYMx?ma$! zt1G4FSU>gVQ6N@r%wsohrdgfJe@JF7hSC;z$*-@ivyNHa%7^a#6-7(E+Y$(iE`UK+O16DA-*h&c#scUAhdzLfnA#<2H+v6&>;>W4} z3?59*tyD>!Vs$i$=F+K-7Oyr&DdRsZg`dCM+bfGmK9BXCTQY4`<^Iiiy#GvbW8O1E zt-7yDA{$^q(i#8gsH*C2@DJMjzE8K-a%R@!A1#BwOHQ%tZqDHPP5&{lssi5dBuKb$ zPrd+u*{9>z$v#zg@J2TV8g5?SvObYf&S?LsDczpNU4V4>kG92(brf|?2@B^a@m$lV zlbg3jJk2on1HA}hx7D$ps<+V8Eyk?(pLRL1ET&gSgvx^Qrj6%lPt-yEn?Jc^W!!vr z_TGsABpT|8hy*mz>BPL4uJx19>$kSf{=;LSc?vL|IY0piCo2}zLmQAmv_;DMC}?#A zMbzSfndeIeJ?clStW=HXK5P`vP{)5gF;$PwN`wdk(PZ45An$gz764*`F0`wWP_`}3F5UeGNk#062R-tk8M^OXaBg8XdkZr8V>+2N#&ny zEsiI<3MBw-#d>$_f>)QhBAk54K1}Vqo$2y-vT-6iE3^~80gm7^ji+>{!af@Gm~kT> zo9t$^eH+ST;u_{&TR{9Sk_%5HL)rkE(f8X*W*^&JKow?lZ~ivg!;_6zQ6+$_bV?Sb z@!A0f7O?tTGEY;>zgqCVe3?C zHye$&u2!B5_ex9&#t^R93%k3tS@QS!(bJf`+&@NY2Ic(H6^sbcWKl>MEUHq(ShhKI zlUW!@gr)XbDLGu|U-usmUO$uH-Dg90!9?)iEJyF<#N0U$m}xhOe8rC$BM+{eiGTe}>V7$mH7t;YDrB7b3%kyb+BQqZ$g5`}bE#^@Ud!sUMKu$l z_&u4q?g?*GdL!hxJDO}=Y#$nL?lg0qEL7}K(Bn`3@BVZ5>svzEHI3W{7dPaOB(M+E zd-cq6J5?`7v{rsvo@s1Nj!_;e(($mIq4nSB&sd!<@@wMUL#&2!#&VYGlV_Y zIRythckC&fs=X6<-XpaBKqk%<^LlPVUG%g~MdiQy!ySCCfG#qfCXGrXFGLbJ({F-2 zR`mlCq+)rdVNt30uwfaW!tDSqsn>DW#jNMb7S>OnLCvfg!|eNOlGG((!vr0SN}seO zbPS}8F-Y(3Y@HY#z=@FZWjLQJ;)~yG zdaI4~v-CZ0B}@-l5AJF011}}VQd$-DuD&kiBV|wkdX-^E;5?cDQ#Z7!4H|6V_@RR$ zrkn67Cob$SRn!Z6`;Dicg>MPY(>{&+Qt`3#&HZ1{Xg#yG$Busu#s%2#KGGjoEx&O$ zS~)MH=tpleydH^dL}ChD;DfSx&cuv*o2cD9tvk|upc?xX=XUX9Vk@w~R7fbMNMc@? z2Q;c?KZ)2}7dcV{%Sm9Y)95Nsnq^LgC*QpIxRpD}*#>*WZChUeaTSBVHcz;{*W~|& zs7%RE0Bgw}ci90@Z&aeey8gT}wG0YlYI3A+29p529H3sjSF%NfsPy9dJpItHs3T&l zssQ!43%`#Tl7k1T@3&*kVT~^iu=zQX?3jS|N*BMC!_or9UqI|^Vv^ydl4AOFht=lX zq4Q%$hRLiL55QN3ugy2yT}Z!^$ds`p77$f0J8xpY;i&G9GLTPm z6_wnVnV;|l$kK#t0=#KhU~DLh!kY;iIclz=~CGL8uMFBmX1% zmxG<`a?DXfd_KkRnAO=to=&sie zGk$;!C-}B6oj>Hbh{sE5-G%}6Nv7dVfXkg){E@|z8wcohcA8gOLj8$`V@xGZ9L5^6 zsn(V)nsqg9Rm|>u^RuZexBbxpyvv~WW@+PRb-5x9VhaJKv4;xt1_sC4r;dm56l5OJ zF%IzB?Hrl2t;lBi$94kb_w_H-3>3f5H!NqIjuBu<2(C7u@yZgbWAeNlD`B@RRgm{H z-J1^7%Yvdm@8SX=mR%k}cUqPM;P_-LKU!}#w#mg8ikU@nBA6Qc{gb_!sL%$gapVZ5 z^)vZg)?R;RDzu5Jf&Y>3q;3G7sLx^qd;qmUgQK`zW{aq7u#@q)P5yP@^x9g0U4mo=4e6<|+w*H%0_| zSMu~_9CZ(oyD<<7)gz~F3j-3HY@Tm{6SDQmiv)7Ydq<^cGlZE}(fm?6b*mzUnX=Zx zeCdNwkt0e<=*Oe?;}`C}Ja>bI6|tg7tz=Du4bR(J#=~m2IRmqQ9|c&<6F{>*&-6gt zuKjUy3c&A!AGIqAtCk+)R!&41;uHh3)sA~-fV_Qjea2W=_1jVGqwN7;M_W#R)VWTM zt^ybEFf8|ZY&B1G3K{J{BE)WN7XGxZlGm>4h~y2RkmZxBjrySN5vXxMk8=vobRJv7 z6MeK0d3fa)fa>$E=1H$NJ~Fh?FfOYQ}bOU8!c$=$z?yCL~1 z{;-?#bcxwEWVBqZkue%fQCVUaS${F#dXRfQ`zN2>sggH0>pGI}3vskRU_B_8jn2Ut z*SBL@dyMBneN<(Di~YLkJ=M~r6I5QR!(f-|x_MXi|6${~f^9zKUC$GJf)6DB6&32O z>eck);ea+047=?LRUgbhA@nIba7ryj_BiBJ9vnn#QK39k)lQsesWT*DO%oqXIL z@L!_6>b#l$q&@wA&3zJCn9(PMy-=KNj0*KP{fFyd`lNhBXygByPB-TNmsxv^?f2i9 zCnXNM97njowaG(P?c*&>jfbSoOf|j_<-Aq^JAr%Ob5}I;D0vT*#=~MqKb2AQ?49H= z%0ERm(;v5se3T$(7e3|g-F)ZHkP|dP?J|bbZMtWe@OQ*2*qN>rsW3EZ-;5VF51vZK z&RNySVZnio{clAdnduR`W}}z z(|P~yfNJ#EahaaI*`q5_F16ldVSnuA97({cK^<&!+gWx!$DlV{_c zD&{01s9oFerzo&F<^-frY0>$nxz^JZ{$t6&tfbKt*(N8FD3*WNUh=VuaI*BMVz=?5 z_Lh%T(;I-YJhoIVfTG*2NnAk-UYYdSi1KXVMCNqRE4S0S@nT8Ocn+C5sa}6>n>+hP ztdh-2vP`C<8yY9K?a6^`GTB4q3UzF&9m7b-#jX`2%Vuyqf-13)BS@>j9dYv|)QOIn*CCcBalK7v-u~QavvVL_Q)(#XdmZR9SO;08talwC6tx<)j`(}pUXJW>-v7{3@F{4|YMav{dG^aeF zr(E7FohbQ>qRQ4~9eHD_*@xo)-KlK|Y26=Qn=HjM`Y`<~ny7ELOf;%|UO75J?BZ1k zdx^>EkTyr}(-vNo@9w)YP8tUq8wF`xWN9mTE*A0HBuPT7R{`1U-EYJ?-9&Rh+Syv) zp7o}rp2$YMoI~7m|F@J)`dO(0+`7d@#Nj_$X8j8rnZ|S1N{9`nLDDW#CknOJ#15nX z93Seh!qU-B7h`5om*?LiESl=g0hI1apsZ68J$DVAB?wPvs_F!?)*_v){}1nKL#5U-aycrSz;*6cx@umXrC&;#_1m8ydTQIA z&25NsJ$7ZXe6sRL@^0IG7|IsktL2!=N$Y1mca1}j{(s%6%@aRi+x8W zY&CreMDY`7`o?tf7y+qKHd9BF;}LrQ5O}BaO4pe8RYzrfbv8>7{=^B-@EOi>c3$2q zc=7zFvPCn4e;}kkW&v0;I6fqg=}+13v)qLCMiG0)N)-;Wa8cLT=gr0>OF=yQhvIq% z>j+hT`-&$!U`uOF0#SYGJaA`r+`>uKXY-(7Wd&iVev4~BT5PxT^-yXfh5t+QSsJBH zG*|qttUbxe%rWME0wMhHi|MkEQ0DG>V@YuD+=)ZDwAdz}0iEdq_uS^vI7U*mi83VP z!Hn{|e)Af9Y21(JO~s3D%9qXT+=~Vczl{j=wZadZC)xbeJ8G`Le(gUw<#}Vj3XaR-L7uq%E zJKT%C`>d}7E;=-zmov|_r%#fUU&nA1P=PvOX4WX=J=1dcgkX;Ttz(A=gkvydr;e*k z93Szg?oZu}Y;tgcxbqhr`>b-Qod~dDRhL94~k)&@Bf$ z`}q{9SE@WuKo)KWIF>nnqK|01wa(K+7T(k6?VorsLFNgm>c2k3LYLIIhsL^0N%;3zY3OP&1- ztK?f0itJ*gBTI>s{0dQqHr&jSsLX`3Sg1XT^qMc*3CiEHD$P1bkcNdrTw)7dcvLa z(GPY+vHt0ErG_;@Z-Nd+AG%Z>lF7gnzR=Wuw#~|hTh(Jw(o8aR`-o_~8W!4Xw6zgn zKkRf5`s9HmllTTnAyw)nGg=Y0VoNb2PtfC>%AP zA}cBppEZ*JVtEpV_E;{R)nbsW>oQWuUqFzCA9nzwIr+z$qDS0G_r#a(8ZK}1BVZHQ z7bOB(lgvA6u3-siolR8*ATGH4<{EWYWyfITcgaAgJI#76g^&2xPXD1byJ}#&lhZ%4 zd-1yll+Y0hme*tcsr9*X!2QR%mOegT>;sI=K zP*g=#VIc_Ec>yIJf>eY8l^h-zB@?$M0T8rG^?Ef8>#+zsr+&n10Lxh#QYYIaKpo*V z94rWwtzwEi_dD(TuL1vF$-d7pzkvvXc?DXq6;H4H?^OP6$-je z|I--1mh9@t5RBLS0tF*WRwom4sHkO+ndFF2! zea`B{DLl$?3SObapMVDqic2wiU;DrVBf<;~;ZUruFeBZ~9@-gQ6K=7GB3 zHdC)VNDfAgA8#H941cEeIwFt-Ah^Q2`ZwLMiZ~BV1n6fF<+*>;FaShg(UqL3q%L57 z**p++{*l^QLpMOJd4+QGK>2^PNyJ>AL>^B8ZvkWCfBLr@Q_Ig=LlduM0zaIFf$YqJ zPY3VtF*Li07|R|qfz*KnKDNlL%0l|0V`KiF5F7udgZ*1h$u5VF*v6kD>p>RCM-coo mAq0JKa!=Llum)67sl7L9L}TOJ&s#?q{f-870Y30m-{D`2>>?uo 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 056/242] 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 6ad1de0..2e143ee 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 057/242] 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 92b9930..16a7622 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 0bc0aa4..e718984 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 058/242] 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 2e143ee..da68972 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 059/242] Version 8.0.2 --- CHANGELOG | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 16a7622..2ada29c 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 060/242] 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 2ada29c..4962ce4 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 123e4f8..f6bfc6c 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 3d81ef7..17c7d05 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 e718984..d00e37d 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 061/242] 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 ad8a05e..95acc6f 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 062/242] 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 b8cdc5d..d9e645a 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 063/242] 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 de7e387..83613c5 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 064/242] 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 4962ce4..0000000 --- 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 0000000..12f7c9e --- /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 0000000..6038fdd --- /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 8050203..0000000 --- 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 4cc26fb..0000000 --- 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 0000000..fcf2810 --- /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 d9e645a..707c6e9 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 0000000..50c0d70 --- /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 86d85ec..0000000 --- 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 0000000..e69de29 diff --git a/setup.cfg b/setup.cfg index d00e37d..9c5918d 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 065/242] 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 12f7c9e..1b54cd0 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 6038fdd..3b5f41c 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 83613c5..b84af02 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 5252bbb..2de3b82 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 066/242] 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 1b54cd0..1ed90ff 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 3b5f41c..9415567 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 b84af02..c28245a 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 067/242] 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 bb12979..9e525d8 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 9c5918d..69c6d3a 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 068/242] doc: tweak --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ed90ff..1329af0 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 069/242] 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 9161fd8..1fb9c96 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 070/242] 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 c28245a..fe93500 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 071/242] doc: tweak --- CHANGES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 9415567..0411960 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 072/242] 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 1fb9c96..e29f231 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 073/242] 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 c3f33ff..c42c671 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 074/242] 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 e29f231..043270a 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 075/242] 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 043270a..fe7e761 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 076/242] 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 fe7e761..0a9be4b 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 077/242] 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 707c6e9..b8c16d1 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 589b76e..94fe3ae 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 909d676..c48e54a 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 0a9be4b..f0ee3df 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 69c6d3a..025d7df 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 0000000..fd9c0a3 --- /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 078/242] 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 9e525d8..803186e 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 94fe3ae..cfbe336 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 fd9c0a3..5d3f647 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 079/242] 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 f0ee3df..b27e48c 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 080/242] doc: tweak --- CHANGELOG.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1329af0..dbbe6a1 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 081/242] 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 97fac66..0000000 --- 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 025d7df..200cfb9 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 082/242] 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 803186e..ff8f17f 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 dbbe6a1..a3e75d6 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 b8c16d1..0174769 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 e9000a8..0e48e93 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 200cfb9..ef95380 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 fe93500..b71b4ec 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 da68972..3fefccc 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 b27e48c..975676e 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 083/242] 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 ff8f17f..58e9051 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 a3e75d6..d56e29d 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 50c0d70..92d51bb 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 084/242] 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 d56e29d..b13ef35 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 0411960..ff7d859 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 085/242] 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 b13ef35..e71b461 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 92d51bb..7b58256 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 f6bfc6c..ba683a4 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 ef95380..3cbdfc5 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 086/242] 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 b71b4ec..557cedb 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 2c8fc6c..621f657 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 087/242] 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 58e9051..6c5d4a0 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 088/242] 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 621f657..1357790 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 6b48bcc..7e6a25e 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 5d3f647..ce69157 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 089/242] 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 e71b461..9f37acd 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 ba683a4..3fa52bf 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 3cbdfc5..ddb55a7 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 090/242] 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 17c7d05..56c3820 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 557cedb..5ad0dc5 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 ce69157..c0677bb 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 091/242] 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 6c5d4a0..67c139b 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 092/242] 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 a6661f6..8a8fbc3 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 093/242] 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 9f37acd..bee984c 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 00c380e..68165ce 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 4694a02..3f247ca 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 094/242] doc: tweak --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bee984c..39217e6 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 095/242] 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 68165ce..a8c28d3 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 096/242] 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 39217e6..68648e0 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 a0d24bb..cfcbcec 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 9a29bc3..1fa2c04 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 b6a7b6e..8535538 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 097/242] 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 cfcbcec..72da4df 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 098/242] 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 ddb55a7..463f2c6 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 c0677bb..59484eb 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 099/242] doc: tweak --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 68648e0..b8ec301 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 100/242] 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 b8ec301..9120329 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 8a8fbc3..1dd37ea 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 101/242] 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 9120329..9f86c5c 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 ff7d859..ea3376f 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 5491312..3eae42b 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 463f2c6..0ca582e 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 56c3820..e7e8503 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 5ad0dc5..8b118e8 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 102/242] 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 9f86c5c..996c06d 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 0ca582e..bd33c7e 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 e7e8503..df47393 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 103/242] doc: tweak --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 996c06d..5cf753b 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 104/242] 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 5cf753b..f115fa9 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 0aa4398..4dff5a1 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="http://github.com/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 2de3b82..caeca22 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 105/242] Version 9.0.1 --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f115fa9..c55d92c 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 106/242] 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 c55d92c..f1ca029 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 3fa52bf..962ea97 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 df47393..77c563e 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 107/242] Bump the version, second try --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index bd33c7e..fb06b0f 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 108/242] 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 67c139b..f46daa3 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 f1ca029..eebccd6 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 0174769..0000000 --- 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 cfbe336..0000000 --- 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 962ea97..42164fe 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 c48e54a..d9c3e53 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 7b3afaf..0000000 --- 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 0000000..c352b79 --- /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 fb06b0f..0000000 --- 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 056ba45..0000000 --- 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 975676e..5bec6fb 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 59484eb..0000000 --- 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 109/242] 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 0000000..fcac89a --- /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 eebccd6..887a0c8 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 7b58256..eb0bfce 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 42164fe..aab04ec 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 77c563e..75cabbc 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 110/242] 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 fcac89a..f06174a 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 f46daa3..6e1b05d 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 111/242] 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 f06174a..a4127b8 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 6e1b05d..8b0b4f4 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 112/242] 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 a4127b8..871822d 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 113/242] 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 8d49e5d..bdcbc50 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 c352b79..f9b41b2 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 75cabbc..500983c 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 114/242] chore: review gitignore file --- .gitignore | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/.gitignore b/.gitignore index 361d611..d117f10 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 115/242] feat: use ruff (#275) --- check.sh | 11 +- docs/source/conf.py | 46 +------ docs/source/examples/callback.py | 10 +- docs/source/examples/custom_cls_image.py | 8 +- docs/source/examples/fps.py | 12 +- docs/source/examples/fps_multiprocessing.py | 5 +- docs/source/examples/from_pil_tuple.py | 5 +- docs/source/examples/linux_display_keyword.py | 5 +- docs/source/examples/opencv_numpy.py | 10 +- docs/source/examples/part_of_screen.py | 5 +- .../examples/part_of_screen_monitor_2.py | 5 +- docs/source/examples/pil.py | 8 +- docs/source/examples/pil_pixels.py | 8 +- pyproject.toml | 72 ++++++----- src/mss/__init__.py | 9 +- src/mss/__main__.py | 14 +- src/mss/base.py | 99 +++++++-------- src/mss/darwin.py | 52 ++++---- src/mss/exception.py | 11 +- src/mss/factory.py | 19 ++- src/mss/linux.py | 120 +++++++++--------- src/mss/models.py | 21 +-- src/mss/screenshot.py | 50 ++++---- src/mss/tools.py | 13 +- src/mss/windows.py | 58 ++++----- src/tests/bench_bgra2rgb.py | 31 +++-- src/tests/bench_general.py | 24 ++-- src/tests/conftest.py | 24 ++-- src/tests/test_bgra_to_rgb.py | 14 +- src/tests/test_cls_image.py | 13 +- src/tests/test_find_monitors.py | 13 +- src/tests/test_get_pixels.py | 12 +- src/tests/test_gnu_linux.py | 95 +++++++------- src/tests/test_implementation.py | 84 ++++++------ src/tests/test_issue_220.py | 18 ++- src/tests/test_leaks.py | 72 +++++------ src/tests/test_macos.py | 56 ++++---- src/tests/test_save.py | 34 +++-- src/tests/test_setup.py | 14 +- src/tests/test_third_party.py | 22 ++-- src/tests/test_tools.py | 25 ++-- src/tests/test_windows.py | 43 ++++--- 42 files changed, 599 insertions(+), 671 deletions(-) diff --git a/check.sh b/check.sh index 0e48e93..7bb90ae 100755 --- a/check.sh +++ b/check.sh @@ -2,9 +2,10 @@ # # Small script to ensure quality checks pass before submitting a commit/PR. # -python -m isort docs src -python -m black --line-length=120 docs src -python -m flake8 docs src -python -m pylint src/mss +set -eu + +python -m ruff --fix docs src +python -m ruff format docs src + # "--platform win32" to not fail on ctypes.windll (it does not affect the overall check on other OSes) -python -m mypy --platform win32 --exclude src/tests src docs/source/examples +python -m mypy --platform win32 src docs/source/examples diff --git a/docs/source/conf.py b/docs/source/conf.py index aab04ec..20ae634 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -6,67 +6,29 @@ sys.path.insert(0, str(Path(__file__).parent.parent.parent / "src")) -from mss import __author__, __date__, __version__ # noqa +import mss # -- General configuration ------------------------------------------------ -# Add any Sphinx extension module names here, as strings. They can be -# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom -# ones. extensions = ["sphinx.ext.intersphinx"] - -# Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] - -# The suffix(es) of source filenames. -# You can specify multiple suffix as a list of string: -# -# source_suffix = ['.rst', '.md'] source_suffix = ".rst" - -# The master toctree document. master_doc = "index" # General information about the project. project = "Python MSS" -copyright = f"{__date__}, {__author__} & contributors" -author = "Tiger-222" +copyright = f"{mss.__date__}, {mss.__author__} & contributors" # noqa:A001 +author = mss.__author__ +version = mss.__version__ -# The version info for the project you're documenting, acts as replacement for -# |version| and |release|, also used in various other places throughout the -# built documents. -# -# The short X.Y version. -version = __version__ - -# The full version, including alpha/beta/rc tags. release = "latest" - -# The language for content autogenerated by Sphinx. Refer to documentation -# for a list of supported languages. -# -# This is also used if you do content translation via gettext catalogs. -# Usually you set "language" from the command line for these cases. language = "en" - -# List of patterns, relative to source directory, that match files and -# directories to ignore when looking for source files. -# This patterns also effect to html_static_path and html_extra_path -exclude_patterns = [] - -# The name of the Pygments (syntax highlighting) style to use. -pygments_style = "sphinx" - todo_include_todos = True # -- Options for HTML output ---------------------------------------------- -# The theme to use for HTML and HTML Help pages. See the documentation for -# a list of builtin themes. html_theme = "default" - -# Output file base name for HTML help builder. htmlhelp_basename = "PythonMSSdoc" diff --git a/docs/source/examples/callback.py b/docs/source/examples/callback.py index 147c952..a107176 100644 --- a/docs/source/examples/callback.py +++ b/docs/source/examples/callback.py @@ -1,6 +1,5 @@ -""" -This is part of the MSS Python's module. -Source: https://github.com/BoboTiG/python-mss +"""This is part of the MSS Python's module. +Source: https://github.com/BoboTiG/python-mss. Screenshot of the monitor 1, with callback. """ @@ -11,10 +10,7 @@ def on_exists(fname: str) -> None: - """ - Callback example when we try to overwrite an existing screenshot. - """ - + """Callback example when we try to overwrite an existing screenshot.""" if os.path.isfile(fname): newfile = f"{fname}.old" print(f"{fname} -> {newfile}") diff --git a/docs/source/examples/custom_cls_image.py b/docs/source/examples/custom_cls_image.py index 4232e49..c57e111 100644 --- a/docs/source/examples/custom_cls_image.py +++ b/docs/source/examples/custom_cls_image.py @@ -1,6 +1,5 @@ -""" -This is part of the MSS Python's module. -Source: https://github.com/BoboTiG/python-mss +"""This is part of the MSS Python's module. +Source: https://github.com/BoboTiG/python-mss. Screenshot of the monitor 1, using a custom class to handle the data. """ @@ -12,8 +11,7 @@ class SimpleScreenShot(ScreenShot): - """ - Define your own custom method to deal with screen shot raw data. + """Define your own custom method to deal with screen shot raw data. Of course, you can inherit from the ScreenShot class and change or add new methods. """ diff --git a/docs/source/examples/fps.py b/docs/source/examples/fps.py index 4046f2a..7a33843 100644 --- a/docs/source/examples/fps.py +++ b/docs/source/examples/fps.py @@ -1,6 +1,5 @@ -""" -This is part of the MSS Python's module. -Source: https://github.com/BoboTiG/python-mss +"""This is part of the MSS Python's module. +Source: https://github.com/BoboTiG/python-mss. Simple naive benchmark to compare with: https://pythonprogramming.net/game-frames-open-cv-python-plays-gta-v/ @@ -8,9 +7,8 @@ import time import cv2 -import numpy - import mss +import numpy as np def screen_record() -> int: @@ -27,7 +25,7 @@ def screen_record() -> int: last_time = time.time() while time.time() - last_time < 1: - img = numpy.asarray(ImageGrab.grab(bbox=mon)) + img = np.asarray(ImageGrab.grab(bbox=mon)) fps += 1 cv2.imshow(title, cv2.cvtColor(img, cv2.COLOR_BGR2RGB)) @@ -48,7 +46,7 @@ def screen_record_efficient() -> int: last_time = time.time() while time.time() - last_time < 1: - img = numpy.asarray(sct.grab(mon)) + img = np.asarray(sct.grab(mon)) fps += 1 cv2.imshow(title, img) diff --git a/docs/source/examples/fps_multiprocessing.py b/docs/source/examples/fps_multiprocessing.py index 28caf59..a54ac3e 100644 --- a/docs/source/examples/fps_multiprocessing.py +++ b/docs/source/examples/fps_multiprocessing.py @@ -1,6 +1,5 @@ -""" -This is part of the MSS Python's module. -Source: https://github.com/BoboTiG/python-mss +"""This is part of the MSS Python's module. +Source: https://github.com/BoboTiG/python-mss. Example using the multiprocessing module to speed-up screen capture. https://github.com/pythonlessons/TensorFlow-object-detection-tutorial diff --git a/docs/source/examples/from_pil_tuple.py b/docs/source/examples/from_pil_tuple.py index 61f2d94..c5ed5f4 100644 --- a/docs/source/examples/from_pil_tuple.py +++ b/docs/source/examples/from_pil_tuple.py @@ -1,6 +1,5 @@ -""" -This is part of the MSS Python's module. -Source: https://github.com/BoboTiG/python-mss +"""This is part of the MSS Python's module. +Source: https://github.com/BoboTiG/python-mss. Use PIL bbox style and percent values. """ diff --git a/docs/source/examples/linux_display_keyword.py b/docs/source/examples/linux_display_keyword.py index a0b7b40..2070aea 100644 --- a/docs/source/examples/linux_display_keyword.py +++ b/docs/source/examples/linux_display_keyword.py @@ -1,6 +1,5 @@ -""" -This is part of the MSS Python's module. -Source: https://github.com/BoboTiG/python-mss +"""This is part of the MSS Python's module. +Source: https://github.com/BoboTiG/python-mss. Usage example with a specific display. """ diff --git a/docs/source/examples/opencv_numpy.py b/docs/source/examples/opencv_numpy.py index 81130ad..94bdbc3 100644 --- a/docs/source/examples/opencv_numpy.py +++ b/docs/source/examples/opencv_numpy.py @@ -1,15 +1,13 @@ -""" -This is part of the MSS Python's module. -Source: https://github.com/BoboTiG/python-mss +"""This is part of the MSS Python's module. +Source: https://github.com/BoboTiG/python-mss. OpenCV/Numpy example. """ import time import cv2 -import numpy - import mss +import numpy as np with mss.mss() as sct: # Part of the screen to capture @@ -19,7 +17,7 @@ last_time = time.time() # Get raw pixels from the screen, save it to a Numpy array - img = numpy.array(sct.grab(monitor)) + img = np.array(sct.grab(monitor)) # Display the picture cv2.imshow("OpenCV/Numpy normal", img) diff --git a/docs/source/examples/part_of_screen.py b/docs/source/examples/part_of_screen.py index 73f93cb..bcc17bb 100644 --- a/docs/source/examples/part_of_screen.py +++ b/docs/source/examples/part_of_screen.py @@ -1,6 +1,5 @@ -""" -This is part of the MSS Python's module. -Source: https://github.com/BoboTiG/python-mss +"""This is part of the MSS Python's module. +Source: https://github.com/BoboTiG/python-mss. Example to capture part of the screen. """ diff --git a/docs/source/examples/part_of_screen_monitor_2.py b/docs/source/examples/part_of_screen_monitor_2.py index 61f58f7..56bfbdc 100644 --- a/docs/source/examples/part_of_screen_monitor_2.py +++ b/docs/source/examples/part_of_screen_monitor_2.py @@ -1,6 +1,5 @@ -""" -This is part of the MSS Python's module. -Source: https://github.com/BoboTiG/python-mss +"""This is part of the MSS Python's module. +Source: https://github.com/BoboTiG/python-mss. Example to capture part of the screen of the monitor 2. """ diff --git a/docs/source/examples/pil.py b/docs/source/examples/pil.py index db10f1b..01a6b01 100644 --- a/docs/source/examples/pil.py +++ b/docs/source/examples/pil.py @@ -1,12 +1,10 @@ -""" -This is part of the MSS Python's module. -Source: https://github.com/BoboTiG/python-mss +"""This is part of the MSS Python's module. +Source: https://github.com/BoboTiG/python-mss. PIL example using frombytes(). """ -from PIL import Image - import mss +from PIL import Image with mss.mss() as sct: # Get rid of the first, as it represents the "All in One" monitor: diff --git a/docs/source/examples/pil_pixels.py b/docs/source/examples/pil_pixels.py index fcedcec..54c5722 100644 --- a/docs/source/examples/pil_pixels.py +++ b/docs/source/examples/pil_pixels.py @@ -1,12 +1,10 @@ -""" -This is part of the MSS Python's module. -Source: https://github.com/BoboTiG/python-mss +"""This is part of the MSS Python's module. +Source: https://github.com/BoboTiG/python-mss. PIL examples to play with pixels. """ -from PIL import Image - import mss +from PIL import Image with mss.mss() as sct: # Get a screenshot of the 1st monitor diff --git a/pyproject.toml b/pyproject.toml index f9b41b2..5a0d125 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -79,11 +79,9 @@ test = [ "sphinx", ] dev = [ - "black", "build", - "flake8-pyproject", "mypy", - "pylint", + "ruff", "twine", "wheel", ] @@ -108,26 +106,6 @@ packages = [ "src/mss", ] -[tool.black] -target-version = ["py38"] -line-length = 120 -safe = true - -[tool.flake8] -max-line-length = 120 -ignore = [ - "E203", # Whitespace before ':', but it's not PEP 8 compliant - "W503", # Line break before binary operator, but it's not PEP 8 compliant -] - -[tool.isort] -py_version = 38 -line_length = 120 -multi_line_output = 3 -include_trailing_comma = true -force_grid_wrap = 0 -use_parentheses = true - [tool.mypy] # Ensure we know what we do warn_redundant_casts = true @@ -149,14 +127,6 @@ disallow_untyped_calls = true strict_equality = true -[tool.pylint."MESSAGES CONTROL"] -disable = "locally-disabled,too-few-public-methods,too-many-instance-attributes,duplicate-code" - -[tool.pylint.REPORTS] -max-line-length = 120 -output-format = "colorized" -reports = "no" - [tool.pytest.ini_options] pythonpath = "src" addopts = """ @@ -167,3 +137,43 @@ addopts = """ --cov=src/mss --cov-report=term-missing """ + +[tool.ruff] +exclude = [ + ".git", + ".mypy_cache", + ".pytest_cache", + ".ruff_cache", + "venv", +] +line-length = 120 +indent-width = 4 +target-version = "py38" + +[tool.ruff.lint] +extend-select = ["ALL"] +ignore = [ + "ANN101", + "ANN401", + "C90", + "COM812", + "D", # TODO + "ERA", + "FBT", + "INP001", + "ISC001", + "PTH", + "PL", + "S", + "SIM117", # TODO: remove wen dropping Python 3.8 support + "SLF", + "T201", + "UP006", # TODO: remove wen dropping Python 3.8 support +] +fixable = ["ALL"] + +[tool.ruff.format] +quote-style = "double" +indent-style = "space" +skip-magic-trailing-comma = false +line-ending = "auto" diff --git a/src/mss/__init__.py b/src/mss/__init__.py index 500983c..cb490e2 100644 --- a/src/mss/__init__.py +++ b/src/mss/__init__.py @@ -1,5 +1,4 @@ -""" -An ultra fast cross-platform multiple screenshots module in pure python +"""An ultra fast cross-platform multiple screenshots module in pure python using ctypes. This module is maintained by Mickaël Schoentgen . @@ -8,11 +7,11 @@ https://github.com/BoboTiG/python-mss If that URL should fail, try contacting the author. """ -from .exception import ScreenShotError -from .factory import mss +from mss.exception import ScreenShotError +from mss.factory import mss __version__ = "9.0.2" -__author__ = "Mickaël 'Tiger-222' Schoentgen" +__author__ = "Mickaël Schoentgen" __date__ = "2013-2024" __copyright__ = f""" Copyright (c) {__date__}, {__author__} diff --git a/src/mss/__main__.py b/src/mss/__main__.py index 4dff5a1..9b74506 100644 --- a/src/mss/__main__.py +++ b/src/mss/__main__.py @@ -1,20 +1,18 @@ -""" -This is part of the MSS Python's module. -Source: https://github.com/BoboTiG/python-mss +"""This is part of the MSS Python's module. +Source: https://github.com/BoboTiG/python-mss. """ import os.path import sys from argparse import ArgumentParser -from . import __version__ -from .exception import ScreenShotError -from .factory import mss -from .tools import to_png +from mss import __version__ +from mss.exception import ScreenShotError +from mss.factory import mss +from mss.tools import to_png def main(*args: str) -> int: """Main logic.""" - cli_args = ArgumentParser(prog="mss") cli_args.add_argument( "-c", diff --git a/src/mss/base.py b/src/mss/base.py index 14a4528..4495bf1 100644 --- a/src/mss/base.py +++ b/src/mss/base.py @@ -1,16 +1,29 @@ +"""This is part of the MSS Python's module. +Source: https://github.com/BoboTiG/python-mss. """ -This is part of the MSS Python's module. -Source: https://github.com/BoboTiG/python-mss -""" +from __future__ import annotations + from abc import ABCMeta, abstractmethod from datetime import datetime from threading import Lock -from typing import Any, Callable, Iterator, List, Optional, Tuple, Type, Union +from typing import TYPE_CHECKING, Any, List, Tuple + +from mss.exception import ScreenShotError +from mss.screenshot import ScreenShot +from mss.tools import to_png + +if TYPE_CHECKING: + from collections.abc import Callable, Iterator + + from mss.models import Monitor, Monitors -from .exception import ScreenShotError -from .models import Monitor, Monitors -from .screenshot import ScreenShot -from .tools import to_png +try: + from datetime import UTC +except ImportError: + # Python < 3.11 + from datetime import timezone + + UTC = timezone.utc lock = Lock() @@ -25,50 +38,44 @@ def __init__( /, *, compression_level: int = 6, - display: Optional[Union[bytes, str]] = None, # Linux only - max_displays: int = 32, # Mac only + display: bytes | str | None = None, # noqa:ARG002 Linux only + max_displays: int = 32, # noqa:ARG002 Mac only with_cursor: bool = False, ) -> None: - # pylint: disable=unused-argument - self.cls_image: Type[ScreenShot] = ScreenShot + self.cls_image: type[ScreenShot] = ScreenShot self.compression_level = compression_level self.with_cursor = with_cursor self._monitors: Monitors = [] - def __enter__(self) -> "MSSBase": + def __enter__(self) -> MSSBase: # noqa:PYI034 """For the cool call `with MSS() as mss:`.""" - return self - def __exit__(self, *_: Any) -> None: + def __exit__(self, *_: object) -> None: """For the cool call `with MSS() as mss:`.""" - self.close() @abstractmethod - def _cursor_impl(self) -> Optional[ScreenShot]: + def _cursor_impl(self) -> ScreenShot | None: """Retrieve all cursor data. Pixels have to be RGB.""" @abstractmethod def _grab_impl(self, monitor: Monitor, /) -> ScreenShot: - """ - Retrieve all pixels from a monitor. Pixels have to be RGB. + """Retrieve all pixels from a monitor. Pixels have to be RGB. That method has to be run using a threading lock. """ @abstractmethod def _monitors_impl(self) -> None: - """ - Get positions of monitors (has to be run using a threading lock). + """Get positions of monitors (has to be run using a threading lock). It must populate self._monitors. """ - def close(self) -> None: + def close(self) -> None: # noqa:B027 """Clean-up.""" - def grab(self, monitor: Union[Monitor, Tuple[int, int, int, int]], /) -> ScreenShot: - """ - Retrieve screen pixels for a given monitor. + def grab(self, monitor: Monitor | Tuple[int, int, int, int], /) -> ScreenShot: + """Retrieve screen pixels for a given monitor. Note: *monitor* can be a tuple like the one PIL.Image.grab() accepts. @@ -76,7 +83,6 @@ def grab(self, monitor: Union[Monitor, Tuple[int, int, int, int]], /) -> ScreenS See :meth:`monitors ` for object details. :return :class:`ScreenShot `. """ - # Convert PIL bbox style if isinstance(monitor, tuple): monitor = { @@ -94,8 +100,7 @@ def grab(self, monitor: Union[Monitor, Tuple[int, int, int, int]], /) -> ScreenS @property def monitors(self) -> Monitors: - """ - Get positions of all monitors. + """Get positions of all monitors. If the monitor has rotation, you have to deal with it inside this method. @@ -112,7 +117,6 @@ def monitors(self) -> Monitors: 'height': the height } """ - if not self._monitors: with lock: self._monitors_impl() @@ -125,10 +129,9 @@ def save( *, mon: int = 0, output: str = "monitor-{mon}.png", - callback: Optional[Callable[[str], None]] = None, + callback: Callable[[str], None] | None = None, ) -> Iterator[str]: - """ - Grab a screen shot and save it to a file. + """Grab a screen shot and save it to a file. :param int mon: The monitor to screen shot (default=0). -1: grab one screen shot of all monitors @@ -153,15 +156,15 @@ def save( :return generator: Created file(s). """ - monitors = self.monitors if not monitors: - raise ScreenShotError("No monitor found.") + msg = "No monitor found." + raise ScreenShotError(msg) if mon == 0: # One screen shot by monitor for idx, monitor in enumerate(monitors[1:], 1): - fname = output.format(mon=idx, date=datetime.now(), **monitor) + fname = output.format(mon=idx, date=datetime.now(UTC) if "{date" in output else None, **monitor) if callable(callback): callback(fname) sct = self.grab(monitor) @@ -174,9 +177,10 @@ def save( try: monitor = monitors[mon] except IndexError as exc: - raise ScreenShotError(f"Monitor {mon!r} does not exist.") from exc + msg = f"Monitor {mon!r} does not exist." + raise ScreenShotError(msg) from exc - output = output.format(mon=mon, date=datetime.now(), **monitor) + output = output.format(mon=mon, date=datetime.now(UTC) if "{date" in output else None, **monitor) if callable(callback): callback(output) sct = self.grab(monitor) @@ -184,11 +188,9 @@ def save( yield output def shot(self, /, **kwargs: Any) -> str: - """ - Helper to save the screen shot of the 1st monitor, by default. + """Helper to save the screen shot of the 1st monitor, by default. You can pass the same arguments as for ``save``. """ - kwargs["mon"] = kwargs.get("mon", 1) return next(self.save(**kwargs)) @@ -196,8 +198,6 @@ def shot(self, /, **kwargs: Any) -> str: def _merge(screenshot: ScreenShot, cursor: ScreenShot, /) -> ScreenShot: """Create composite image by blending screenshot and mouse cursor.""" - # pylint: disable=too-many-locals,invalid-name - (cx, cy), (cw, ch) = cursor.pos, cursor.size (x, y), (w, h) = screenshot.pos, screenshot.size @@ -208,8 +208,8 @@ def _merge(screenshot: ScreenShot, cursor: ScreenShot, /) -> ScreenShot: if not overlap: return screenshot - screen_data = screenshot.raw - cursor_data = cursor.raw + screen_raw = screenshot.raw + cursor_raw = cursor.raw cy, cy2 = (cy - y) * 4, (cy2 - y2) * 4 cx, cx2 = (cx - x) * 4, (cx2 - x2) * 4 @@ -226,17 +226,17 @@ def _merge(screenshot: ScreenShot, cursor: ScreenShot, /) -> ScreenShot: for count_x in range(start_count_x, stop_count_x, 4): spos = pos_s + count_x cpos = pos_c + count_x - alpha = cursor_data[cpos + 3] + alpha = cursor_raw[cpos + 3] if not alpha: continue if alpha == 255: - screen_data[spos : spos + 3] = cursor_data[cpos : cpos + 3] + screen_raw[spos : spos + 3] = cursor_raw[cpos : cpos + 3] else: - alpha = alpha / 255 + alpha2 = alpha / 255 for i in rgb: - screen_data[spos + i] = int(cursor_data[cpos + i] * alpha + screen_data[spos + i] * (1 - alpha)) + screen_raw[spos + i] = int(cursor_raw[cpos + i] * alpha2 + screen_raw[spos + i] * (1 - alpha2)) return screenshot @@ -247,10 +247,9 @@ def _cfactory( argtypes: List[Any], restype: Any, /, - errcheck: Optional[Callable] = None, + errcheck: Callable | None = None, ) -> None: """Factory to create a ctypes function and automatically manage errors.""" - meth = getattr(attr, func) meth.argtypes = argtypes meth.restype = restype diff --git a/src/mss/darwin.py b/src/mss/darwin.py index 1dd37ea..f247c51 100644 --- a/src/mss/darwin.py +++ b/src/mss/darwin.py @@ -1,32 +1,34 @@ +"""This is part of the MSS Python's module. +Source: https://github.com/BoboTiG/python-mss. """ -This is part of the MSS Python's module. -Source: https://github.com/BoboTiG/python-mss -""" +from __future__ import annotations + import ctypes import ctypes.util import sys from ctypes import POINTER, Structure, c_double, c_float, c_int32, c_ubyte, c_uint32, c_uint64, c_void_p from platform import mac_ver -from typing import Any, Optional, Type, Union +from typing import TYPE_CHECKING, Any + +from mss.base import MSSBase +from mss.exception import ScreenShotError +from mss.screenshot import ScreenShot, Size -from .base import MSSBase -from .exception import ScreenShotError -from .models import CFunctions, Monitor -from .screenshot import ScreenShot, Size +if TYPE_CHECKING: + from mss.models import CFunctions, Monitor __all__ = ("MSS",) -def cgfloat() -> Union[Type[c_double], Type[c_float]]: +def cgfloat() -> type[c_double | c_float]: """Get the appropriate value for a float.""" - return c_double if sys.maxsize > 2**32 else c_float class CGPoint(Structure): """Structure that contains coordinates of a rectangle.""" - _fields_ = [("x", cgfloat()), ("y", cgfloat())] + _fields_ = (("x", cgfloat()), ("y", cgfloat())) def __repr__(self) -> str: return f"{type(self).__name__}(left={self.x} top={self.y})" @@ -35,7 +37,7 @@ def __repr__(self) -> str: class CGSize(Structure): """Structure that contains dimensions of an rectangle.""" - _fields_ = [("width", cgfloat()), ("height", cgfloat())] + _fields_ = (("width", cgfloat()), ("height", cgfloat())) def __repr__(self) -> str: return f"{type(self).__name__}(width={self.width} height={self.height})" @@ -44,7 +46,7 @@ def __repr__(self) -> str: class CGRect(Structure): """Structure that contains information about a rectangle.""" - _fields_ = [("origin", CGPoint), ("size", CGSize)] + _fields_ = (("origin", CGPoint), ("size", CGSize)) def __repr__(self) -> str: return f"{type(self).__name__}<{self.origin} {self.size}>" @@ -52,13 +54,11 @@ def __repr__(self) -> str: # C functions that will be initialised later. # -# This is a dict: -# cfunction: (attr, argtypes, restype) -# # Available attr: core. # # Note: keep it sorted by cfunction. CFUNCTIONS: CFunctions = { + # cfunction: (attr, argtypes, restype) "CGDataProviderCopyData": ("core", [c_void_p], c_void_p), "CGDisplayBounds": ("core", [c_uint32], CGRect), "CGDisplayRotation": ("core", [c_uint32], c_float), @@ -79,16 +79,14 @@ def __repr__(self) -> str: class MSS(MSSBase): - """ - Multiple ScreenShots implementation for macOS. + """Multiple ScreenShots implementation for macOS. It uses intensively the CoreGraphics library. """ __slots__ = {"core", "max_displays"} def __init__(self, /, **kwargs: Any) -> None: - """macOS initialisations.""" - + """MacOS initialisations.""" super().__init__(**kwargs) self.max_displays = kwargs.get("max_displays", 32) @@ -106,12 +104,12 @@ def _init_library(self) -> None: coregraphics = "/System/Library/Frameworks/CoreGraphics.framework/Versions/Current/CoreGraphics" if not coregraphics: - raise ScreenShotError("No CoreGraphics library found.") + msg = "No CoreGraphics library found." + raise ScreenShotError(msg) self.core = ctypes.cdll.LoadLibrary(coregraphics) def _set_cfunctions(self) -> None: """Set all ctypes functions and attach them to attributes.""" - cfactory = self._cfactory attrs = {"core": self.core} for func, (attr, argtypes, restype) in CFUNCTIONS.items(): @@ -119,7 +117,6 @@ def _set_cfunctions(self) -> None: def _monitors_impl(self) -> None: """Get positions of monitors. It will populate self._monitors.""" - int_ = int core = self.core @@ -147,7 +144,7 @@ def _monitors_impl(self) -> None: "top": int_(rect.origin.y), "width": int_(width), "height": int_(height), - } + }, ) # Update AiO monitor's values @@ -164,14 +161,13 @@ def _monitors_impl(self) -> None: def _grab_impl(self, monitor: Monitor, /) -> ScreenShot: """Retrieve all pixels from a monitor. Pixels have to be RGB.""" - # pylint: disable=too-many-locals - core = self.core rect = CGRect((monitor["left"], monitor["top"]), (monitor["width"], monitor["height"])) image_ref = core.CGWindowListCreateImage(rect, 1, 0, 0) if not image_ref: - raise ScreenShotError("CoreGraphics.CGWindowListCreateImage() failed.") + msg = "CoreGraphics.CGWindowListCreateImage() failed." + raise ScreenShotError(msg) width = core.CGImageGetWidth(image_ref) height = core.CGImageGetHeight(image_ref) @@ -204,6 +200,6 @@ def _grab_impl(self, monitor: Monitor, /) -> ScreenShot: return self.cls_image(data, monitor, size=Size(width, height)) - def _cursor_impl(self) -> Optional[ScreenShot]: + def _cursor_impl(self) -> ScreenShot | None: """Retrieve all cursor data. Pixels have to be RGB.""" return None diff --git a/src/mss/exception.py b/src/mss/exception.py index 9ffb94b..4201367 100644 --- a/src/mss/exception.py +++ b/src/mss/exception.py @@ -1,13 +1,14 @@ +"""This is part of the MSS Python's module. +Source: https://github.com/BoboTiG/python-mss. """ -This is part of the MSS Python's module. -Source: https://github.com/BoboTiG/python-mss -""" -from typing import Any, Dict, Optional +from __future__ import annotations + +from typing import Any, Dict class ScreenShotError(Exception): """Error handling class.""" - def __init__(self, message: str, /, *, details: Optional[Dict[str, Any]] = None) -> None: + def __init__(self, message: str, /, *, details: Dict[str, Any] | None = None) -> None: super().__init__(message) self.details = details or {} diff --git a/src/mss/factory.py b/src/mss/factory.py index 30e15c2..fea7df3 100644 --- a/src/mss/factory.py +++ b/src/mss/factory.py @@ -1,12 +1,11 @@ -""" -This is part of the MSS Python's module. -Source: https://github.com/BoboTiG/python-mss +"""This is part of the MSS Python's module. +Source: https://github.com/BoboTiG/python-mss. """ import platform from typing import Any -from .base import MSSBase -from .exception import ScreenShotError +from mss.base import MSSBase +from mss.exception import ScreenShotError def mss(**kwargs: Any) -> MSSBase: @@ -19,23 +18,23 @@ def mss(**kwargs: Any) -> MSSBase: It then proxies its arguments to the class for instantiation. """ - # pylint: disable=import-outside-toplevel os_ = platform.system().lower() if os_ == "darwin": - from . import darwin + from mss import darwin return darwin.MSS(**kwargs) if os_ == "linux": - from . import linux + from mss import linux return linux.MSS(**kwargs) if os_ == "windows": - from . import windows + from mss import windows return windows.MSS(**kwargs) - raise ScreenShotError(f"System {os_!r} not (yet?) implemented.") + msg = f"System {os_!r} not (yet?) implemented." + raise ScreenShotError(msg) diff --git a/src/mss/linux.py b/src/mss/linux.py index 8b118e8..7d0c8fc 100644 --- a/src/mss/linux.py +++ b/src/mss/linux.py @@ -1,7 +1,8 @@ +"""This is part of the MSS Python's module. +Source: https://github.com/BoboTiG/python-mss. """ -This is part of the MSS Python's module. -Source: https://github.com/BoboTiG/python-mss -""" +from __future__ import annotations + import os from contextlib import suppress from ctypes import ( @@ -26,12 +27,14 @@ ) from ctypes.util import find_library from threading import current_thread, local -from typing import Any, Tuple +from typing import TYPE_CHECKING, Any, Tuple + +from mss.base import MSSBase, lock +from mss.exception import ScreenShotError -from .base import MSSBase, lock -from .exception import ScreenShotError -from .models import CFunctions, Monitor -from .screenshot import ScreenShot +if TYPE_CHECKING: + from mss.models import CFunctions, Monitor + from mss.screenshot import ScreenShot __all__ = ("MSS",) @@ -41,20 +44,18 @@ class Display(Structure): - """ - Structure that serves as the connection to the X server + """Structure that serves as the connection to the X server and that contains all the information about that X server. - https://github.com/garrybodsworth/pyxlib-ctypes/blob/master/pyxlib/xlib.py#L831 + https://github.com/garrybodsworth/pyxlib-ctypes/blob/master/pyxlib/xlib.py#L831. """ class XErrorEvent(Structure): - """ - XErrorEvent to debug eventual errors. - https://tronche.com/gui/x/xlib/event-handling/protocol-errors/default-handlers.html + """XErrorEvent to debug eventual errors. + https://tronche.com/gui/x/xlib/event-handling/protocol-errors/default-handlers.html. """ - _fields_ = [ + _fields_ = ( ("type", c_int), ("display", POINTER(Display)), # Display the event was read from ("serial", c_ulong), # serial number of failed request @@ -62,17 +63,16 @@ class XErrorEvent(Structure): ("request_code", c_ubyte), # major op-code of failed request ("minor_code", c_ubyte), # minor op-code of failed request ("resourceid", c_void_p), # resource ID - ] + ) class XFixesCursorImage(Structure): - """ - Cursor structure. + """Cursor structure. /usr/include/X11/extensions/Xfixes.h - https://github.com/freedesktop/xorg-libXfixes/blob/libXfixes-6.0.0/include/X11/extensions/Xfixes.h#L96 + https://github.com/freedesktop/xorg-libXfixes/blob/libXfixes-6.0.0/include/X11/extensions/Xfixes.h#L96. """ - _fields_ = [ + _fields_ = ( ("x", c_short), ("y", c_short), ("width", c_ushort), @@ -83,16 +83,15 @@ class XFixesCursorImage(Structure): ("pixels", POINTER(c_ulong)), ("atom", c_ulong), ("name", c_char_p), - ] + ) class XImage(Structure): - """ - Description of an image as it exists in the client's memory. - https://tronche.com/gui/x/xlib/graphics/images.html + """Description of an image as it exists in the client's memory. + https://tronche.com/gui/x/xlib/graphics/images.html. """ - _fields_ = [ + _fields_ = ( ("width", c_int), # size of image ("height", c_int), # size of image ("xoffset", c_int), # number of pixels offset in X direction @@ -108,16 +107,15 @@ class XImage(Structure): ("red_mask", c_ulong), # bits in z arrangment ("green_mask", c_ulong), # bits in z arrangment ("blue_mask", c_ulong), # bits in z arrangment - ] + ) class XRRCrtcInfo(Structure): - """ - Structure that contains CRTC information. - https://gitlab.freedesktop.org/xorg/lib/libxrandr/-/blob/master/include/X11/extensions/Xrandr.h#L360 + """Structure that contains CRTC information. + https://gitlab.freedesktop.org/xorg/lib/libxrandr/-/blob/master/include/X11/extensions/Xrandr.h#L360. """ - _fields_ = [ + _fields_ = ( ("timestamp", c_ulong), ("x", c_int), ("y", c_int), @@ -130,21 +128,20 @@ class XRRCrtcInfo(Structure): ("rotations", c_ushort), ("npossible", c_int), ("possible", POINTER(c_long)), - ] + ) class XRRModeInfo(Structure): - """https://gitlab.freedesktop.org/xorg/lib/libxrandr/-/blob/master/include/X11/extensions/Xrandr.h#L248""" + """https://gitlab.freedesktop.org/xorg/lib/libxrandr/-/blob/master/include/X11/extensions/Xrandr.h#L248.""" class XRRScreenResources(Structure): - """ - Structure that contains arrays of XIDs that point to the + """Structure that contains arrays of XIDs that point to the available outputs and associated CRTCs. - https://gitlab.freedesktop.org/xorg/lib/libxrandr/-/blob/master/include/X11/extensions/Xrandr.h#L265 + https://gitlab.freedesktop.org/xorg/lib/libxrandr/-/blob/master/include/X11/extensions/Xrandr.h#L265. """ - _fields_ = [ + _fields_ = ( ("timestamp", c_ulong), ("configTimestamp", c_ulong), ("ncrtc", c_int), @@ -153,13 +150,13 @@ class XRRScreenResources(Structure): ("outputs", POINTER(c_long)), ("nmode", c_int), ("modes", POINTER(XRRModeInfo)), - ] + ) class XWindowAttributes(Structure): """Attributes for the specified window.""" - _fields_ = [ + _fields_ = ( ("x", c_int32), # location of window ("y", c_int32), # location of window ("width", c_int32), # width of window @@ -183,7 +180,7 @@ class XWindowAttributes(Structure): ("do_not_propagate_mask", c_ulong), # set of events that should not propagate ("override_redirect", c_int32), # boolean value for override-redirect ("screen", c_ulong), # back pointer to correct screen - ] + ) _ERROR = {} @@ -195,7 +192,6 @@ class XWindowAttributes(Structure): @CFUNCTYPE(c_int, POINTER(Display), POINTER(XErrorEvent)) def _error_handler(display: Display, event: XErrorEvent) -> int: """Specifies the program's supplied error handler.""" - # Get the specific error message xlib = cdll.LoadLibrary(_X11) # type: ignore[arg-type] get_error = xlib.XGetErrorText @@ -220,25 +216,23 @@ def _error_handler(display: Display, event: XErrorEvent) -> int: def _validate(retval: int, func: Any, args: Tuple[Any, Any], /) -> Tuple[Any, Any]: """Validate the returned value of a C function call.""" - thread = current_thread() if retval != 0 and thread not in _ERROR: return args details = _ERROR.pop(thread, {}) - raise ScreenShotError(f"{func.__name__}() failed", details=details) + msg = f"{func.__name__}() failed" + raise ScreenShotError(msg, details=details) # C functions that will be initialised later. # See https://tronche.com/gui/x/xlib/function-index.html for details. # -# This is a dict: -# cfunction: (attr, argtypes, restype) -# # Available attr: xfixes, xlib, xrandr. # # Note: keep it sorted by cfunction. CFUNCTIONS: CFunctions = { + # cfunction: (attr, argtypes, restype) "XCloseDisplay": ("xlib", [POINTER(Display)], c_void_p), "XDefaultRootWindow": ("xlib", [POINTER(Display)], POINTER(XWindowAttributes)), "XDestroyImage": ("xlib", [POINTER(XImage)], c_void_p), @@ -261,8 +255,7 @@ def _validate(retval: int, func: Any, args: Tuple[Any, Any], /) -> Tuple[Any, An class MSS(MSSBase): - """ - Multiple ScreenShots implementation for GNU/Linux. + """Multiple ScreenShots implementation for GNU/Linux. It uses intensively the Xlib and its Xrandr extension. """ @@ -270,7 +263,6 @@ class MSS(MSSBase): def __init__(self, /, **kwargs: Any) -> None: """GNU/Linux initialisations.""" - super().__init__(**kwargs) # Available thread-specific variables @@ -285,20 +277,24 @@ def __init__(self, /, **kwargs: Any) -> None: try: display = os.environ["DISPLAY"].encode("utf-8") except KeyError: - raise ScreenShotError("$DISPLAY not set.") from None + msg = "$DISPLAY not set." + raise ScreenShotError(msg) from None if not isinstance(display, bytes): display = display.encode("utf-8") if b":" not in display: - raise ScreenShotError(f"Bad display value: {display!r}.") + msg = f"Bad display value: {display!r}." + raise ScreenShotError(msg) if not _X11: - raise ScreenShotError("No X11 library found.") + msg = "No X11 library found." + raise ScreenShotError(msg) self.xlib = cdll.LoadLibrary(_X11) if not _XRANDR: - raise ScreenShotError("No Xrandr extension found.") + msg = "No Xrandr extension found." + raise ScreenShotError(msg) self.xrandr = cdll.LoadLibrary(_XRANDR) if self.with_cursor: @@ -314,10 +310,12 @@ def __init__(self, /, **kwargs: Any) -> None: self._handles.display = self.xlib.XOpenDisplay(display) if not self._handles.display: - raise ScreenShotError(f"Unable to open display: {display!r}.") + msg = f"Unable to open display: {display!r}." + raise ScreenShotError(msg) if not self._is_extension_enabled("RANDR"): - raise ScreenShotError("Xrandr not enabled.") + msg = "Xrandr not enabled." + raise ScreenShotError(msg) self._handles.root = self.xlib.XDefaultRootWindow(self._handles.display) @@ -367,7 +365,6 @@ def _is_extension_enabled(self, name: str, /) -> bool: def _set_cfunctions(self) -> None: """Set all ctypes functions and attach them to attributes.""" - cfactory = self._cfactory attrs = { "xfixes": getattr(self, "xfixes", None), @@ -381,7 +378,6 @@ def _set_cfunctions(self) -> None: def _monitors_impl(self) -> None: """Get positions of monitors. It will populate self._monitors.""" - display = self._handles.display int_ = int xrandr = self.xrandr @@ -390,7 +386,7 @@ def _monitors_impl(self) -> None: gwa = XWindowAttributes() self.xlib.XGetWindowAttributes(display, self._handles.root, byref(gwa)) self._monitors.append( - {"left": int_(gwa.x), "top": int_(gwa.y), "width": int_(gwa.width), "height": int_(gwa.height)} + {"left": int_(gwa.x), "top": int_(gwa.y), "width": int_(gwa.width), "height": int_(gwa.height)}, ) # Each monitor @@ -416,14 +412,13 @@ def _monitors_impl(self) -> None: "top": int_(crtc.y), "width": int_(crtc.width), "height": int_(crtc.height), - } + }, ) xrandr.XRRFreeCrtcInfo(crtc) xrandr.XRRFreeScreenResources(mon) def _grab_impl(self, monitor: Monitor, /) -> ScreenShot: """Retrieve all pixels from a monitor. Pixels have to be RGB.""" - ximage = self.xlib.XGetImage( self._handles.display, self._handles.drawable, @@ -438,7 +433,8 @@ def _grab_impl(self, monitor: Monitor, /) -> ScreenShot: try: bits_per_pixel = ximage.contents.bits_per_pixel if bits_per_pixel != 32: - raise ScreenShotError(f"[XImage] bits per pixel value not (yet?) implemented: {bits_per_pixel}.") + msg = f"[XImage] bits per pixel value not (yet?) implemented: {bits_per_pixel}." + raise ScreenShotError(msg) raw_data = cast( ximage.contents.data, @@ -453,11 +449,11 @@ def _grab_impl(self, monitor: Monitor, /) -> ScreenShot: def _cursor_impl(self) -> ScreenShot: """Retrieve all cursor data. Pixels have to be RGB.""" - # Read data of cursor/mouse-pointer ximage = self.xfixes.XFixesGetCursorImage(self._handles.display) if not (ximage and ximage.contents): - raise ScreenShotError("Cannot read XFixesGetCursorImage()") + msg = "Cannot read XFixesGetCursorImage()" + raise ScreenShotError(msg) cursor_img: XFixesCursorImage = ximage.contents region = { diff --git a/src/mss/models.py b/src/mss/models.py index 9c0851a..a6a7bf8 100644 --- a/src/mss/models.py +++ b/src/mss/models.py @@ -1,10 +1,8 @@ -""" -This is part of the MSS Python's module. -Source: https://github.com/BoboTiG/python-mss +"""This is part of the MSS Python's module. +Source: https://github.com/BoboTiG/python-mss. """ -import collections -from typing import Any, Dict, List, Tuple +from typing import Any, Dict, List, NamedTuple, Tuple Monitor = Dict[str, int] Monitors = List[Monitor] @@ -12,7 +10,14 @@ Pixel = Tuple[int, int, int] Pixels = List[Pixel] -Pos = collections.namedtuple("Pos", "left, top") -Size = collections.namedtuple("Size", "width, height") - CFunctions = Dict[str, Tuple[str, List[Any], Any]] + + +class Pos(NamedTuple): + left: int + top: int + + +class Size(NamedTuple): + width: int + height: int diff --git a/src/mss/screenshot.py b/src/mss/screenshot.py index 9c82d72..cad551b 100644 --- a/src/mss/screenshot.py +++ b/src/mss/screenshot.py @@ -1,17 +1,19 @@ +"""This is part of the MSS Python's module. +Source: https://github.com/BoboTiG/python-mss. """ -This is part of the MSS Python's module. -Source: https://github.com/BoboTiG/python-mss -""" +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, Dict -from typing import Any, Dict, Iterator, Optional, Type +from mss.exception import ScreenShotError +from mss.models import Monitor, Pixel, Pixels, Pos, Size -from .exception import ScreenShotError -from .models import Monitor, Pixel, Pixels, Pos, Size +if TYPE_CHECKING: + from collections.abc import Iterator class ScreenShot: - """ - Screen shot object. + """Screen shot object. .. note:: @@ -21,9 +23,9 @@ class ScreenShot: __slots__ = {"__pixels", "__rgb", "pos", "raw", "size"} - def __init__(self, data: bytearray, monitor: Monitor, /, *, size: Optional[Size] = None) -> None: - self.__pixels: Optional[Pixels] = None - self.__rgb: Optional[bytes] = None + def __init__(self, data: bytearray, monitor: Monitor, /, *, size: Size | None = None) -> None: + self.__pixels: Pixels | None = None + self.__rgb: bytes | None = None #: Bytearray of the raw BGRA pixels retrieved by ctypes #: OS independent implementations. @@ -40,13 +42,11 @@ def __repr__(self) -> str: @property def __array_interface__(self) -> Dict[str, Any]: - """ - Numpy array interface support. + """Numpy array interface support. It uses raw data in BGRA form. See https://docs.scipy.org/doc/numpy/reference/arrays.interface.html """ - return { "version": 3, "shape": (self.height, self.width, 4), @@ -55,7 +55,7 @@ def __array_interface__(self) -> Dict[str, Any]: } @classmethod - def from_size(cls: Type["ScreenShot"], data: bytearray, width: int, height: int, /) -> "ScreenShot": + def from_size(cls: type[ScreenShot], data: bytearray, width: int, height: int, /) -> ScreenShot: """Instantiate a new class given only screen shot's data and size.""" monitor = {"left": 0, "top": 0, "width": width, "height": height} return cls(data, monitor) @@ -77,24 +77,19 @@ def left(self) -> int: @property def pixels(self) -> Pixels: - """ - :return list: RGB tuples. - """ - + """:return list: RGB tuples.""" if not self.__pixels: rgb_tuples: Iterator[Pixel] = zip(self.raw[2::4], self.raw[1::4], self.raw[::4]) - self.__pixels = list(zip(*[iter(rgb_tuples)] * self.width)) # type: ignore + self.__pixels = list(zip(*[iter(rgb_tuples)] * self.width)) return self.__pixels @property def rgb(self) -> bytes: - """ - Compute RGB values from the BGRA raw pixels. + """Compute RGB values from the BGRA raw pixels. :return bytes: RGB pixels. """ - if not self.__rgb: rgb = bytearray(self.height * self.width * 3) raw = self.raw @@ -116,15 +111,14 @@ def width(self) -> int: return self.size.width def pixel(self, coord_x: int, coord_y: int) -> Pixel: - """ - Returns the pixel value at a given position. + """Returns the pixel value at a given position. :param int coord_x: The x coordinate. :param int coord_y: The y coordinate. :return tuple: The pixel value as (R, G, B). """ - try: - return self.pixels[coord_y][coord_x] # type: ignore + return self.pixels[coord_y][coord_x] # type: ignore[return-value] except IndexError as exc: - raise ScreenShotError(f"Pixel location ({coord_x}, {coord_y}) is out of range.") from exc + msg = f"Pixel location ({coord_x}, {coord_y}) is out of range." + raise ScreenShotError(msg) from exc diff --git a/src/mss/tools.py b/src/mss/tools.py index de3a1af..316939c 100644 --- a/src/mss/tools.py +++ b/src/mss/tools.py @@ -1,17 +1,15 @@ +"""This is part of the MSS Python's module. +Source: https://github.com/BoboTiG/python-mss. """ -This is part of the MSS Python's module. -Source: https://github.com/BoboTiG/python-mss -""" +from __future__ import annotations import os import struct import zlib -from typing import Optional, Tuple -def to_png(data: bytes, size: Tuple[int, int], /, *, level: int = 6, output: Optional[str] = None) -> Optional[bytes]: - """ - Dump data to a PNG file. If `output` is `None`, create no file but return +def to_png(data: bytes, size: tuple[int, int], /, *, level: int = 6, output: str | None = None) -> bytes | None: + """Dump data to a PNG file. If `output` is `None`, create no file but return the whole PNG data. :param bytes data: RGBRGB...RGB data. @@ -19,7 +17,6 @@ def to_png(data: bytes, size: Tuple[int, int], /, *, level: int = 6, output: Opt :param int level: PNG compression level. :param str output: Output file name. """ - # pylint: disable=too-many-locals pack = struct.pack crc32 = zlib.crc32 diff --git a/src/mss/windows.py b/src/mss/windows.py index a8c28d3..fab2794 100644 --- a/src/mss/windows.py +++ b/src/mss/windows.py @@ -1,7 +1,8 @@ +"""This is part of the MSS Python's module. +Source: https://github.com/BoboTiG/python-mss. """ -This is part of the MSS Python's module. -Source: https://github.com/BoboTiG/python-mss -""" +from __future__ import annotations + import ctypes import sys from ctypes import POINTER, WINFUNCTYPE, Structure, c_int, c_void_p @@ -22,12 +23,14 @@ WORD, ) from threading import local -from typing import Any, Optional +from typing import TYPE_CHECKING, Any + +from mss.base import MSSBase +from mss.exception import ScreenShotError -from .base import MSSBase -from .exception import ScreenShotError -from .models import CFunctions, Monitor -from .screenshot import ScreenShot +if TYPE_CHECKING: + from mss.models import CFunctions, Monitor + from mss.screenshot import ScreenShot __all__ = ("MSS",) @@ -40,7 +43,7 @@ class BITMAPINFOHEADER(Structure): """Information about the dimensions and color format of a DIB.""" - _fields_ = [ + _fields_ = ( ("biSize", DWORD), ("biWidth", LONG), ("biHeight", LONG), @@ -52,15 +55,13 @@ class BITMAPINFOHEADER(Structure): ("biYPelsPerMeter", LONG), ("biClrUsed", DWORD), ("biClrImportant", DWORD), - ] + ) class BITMAPINFO(Structure): - """ - Structure that defines the dimensions and color information for a DIB. - """ + """Structure that defines the dimensions and color information for a DIB.""" - _fields_ = [("bmiHeader", BITMAPINFOHEADER), ("bmiColors", DWORD * 3)] + _fields_ = (("bmiHeader", BITMAPINFOHEADER), ("bmiColors", DWORD * 3)) MONITORNUMPROC = WINFUNCTYPE(INT, DWORD, DWORD, POINTER(RECT), DOUBLE) @@ -68,13 +69,11 @@ class BITMAPINFO(Structure): # C functions that will be initialised later. # -# This is a dict: -# cfunction: (attr, argtypes, restype) -# # Available attr: gdi32, user32. # # Note: keep it sorted by cfunction. CFUNCTIONS: CFunctions = { + # cfunction: (attr, argtypes, restype) "BitBlt": ("gdi32", [HDC, INT, INT, INT, INT, HDC, INT, INT, DWORD], BOOL), "CreateCompatibleBitmap": ("gdi32", [HDC, INT, INT], HBITMAP), "CreateCompatibleDC": ("gdi32", [HDC], HDC), @@ -97,7 +96,6 @@ class MSS(MSSBase): def __init__(self, /, **kwargs: Any) -> None: """Windows initialisations.""" - super().__init__(**kwargs) self.user32 = ctypes.WinDLL("user32") @@ -137,7 +135,6 @@ def close(self) -> None: def _set_cfunctions(self) -> None: """Set all ctypes functions and attach them to attributes.""" - cfactory = self._cfactory attrs = { "gdi32": self.gdi32, @@ -148,8 +145,7 @@ def _set_cfunctions(self) -> None: def _set_dpi_awareness(self) -> None: """Set DPI awareness to capture full screen on Hi-DPI monitors.""" - - version = sys.getwindowsversion()[:2] # pylint: disable=no-member + version = sys.getwindowsversion()[:2] if version >= (6, 3): # Windows 8.1+ # Here 2 = PROCESS_PER_MONITOR_DPI_AWARE, which means: @@ -163,7 +159,6 @@ def _set_dpi_awareness(self) -> None: def _monitors_impl(self) -> None: """Get positions of monitors. It will populate self._monitors.""" - int_ = int user32 = self.user32 get_system_metrics = user32.GetSystemMetrics @@ -175,16 +170,14 @@ def _monitors_impl(self) -> None: "top": int_(get_system_metrics(77)), # SM_YVIRTUALSCREEN "width": int_(get_system_metrics(78)), # SM_CXVIRTUALSCREEN "height": int_(get_system_metrics(79)), # SM_CYVIRTUALSCREEN - } + }, ) # Each monitor - def _callback(monitor: int, data: HDC, rect: LPRECT, dc_: LPARAM) -> int: - """ - Callback for monitorenumproc() function, it will return + def _callback(_monitor: int, _data: HDC, rect: LPRECT, _dc: LPARAM) -> int: + """Callback for monitorenumproc() function, it will return a RECT with appropriate values. """ - # pylint: disable=unused-argument rct = rect.contents self._monitors.append( @@ -193,7 +186,7 @@ def _callback(monitor: int, data: HDC, rect: LPRECT, dc_: LPARAM) -> int: "top": int_(rct.top), "width": int_(rct.right) - int_(rct.left), "height": int_(rct.bottom) - int_(rct.top), - } + }, ) return 1 @@ -201,8 +194,7 @@ def _callback(monitor: int, data: HDC, rect: LPRECT, dc_: LPARAM) -> int: user32.EnumDisplayMonitors(0, 0, callback, 0) def _grab_impl(self, monitor: Monitor, /) -> ScreenShot: - """ - Retrieve all pixels from a monitor. Pixels have to be RGB. + """Retrieve all pixels from a monitor. Pixels have to be RGB. In the code, there are a few interesting things: @@ -231,7 +223,6 @@ def _grab_impl(self, monitor: Monitor, /) -> ScreenShot: retrieved by gdi32.GetDIBits() as a sequence of RGB values. Thanks to http://stackoverflow.com/a/3688682 """ - srcdc, memdc = self._handles.srcdc, self._handles.memdc gdi = self.gdi32 width, height = monitor["width"], monitor["height"] @@ -249,10 +240,11 @@ def _grab_impl(self, monitor: Monitor, /) -> ScreenShot: gdi.BitBlt(memdc, 0, 0, width, height, srcdc, monitor["left"], monitor["top"], SRCCOPY | CAPTUREBLT) bits = gdi.GetDIBits(memdc, self._handles.bmp, 0, height, self._handles.data, self._handles.bmi, DIB_RGB_COLORS) if bits != height: - raise ScreenShotError("gdi32.GetDIBits() failed.") + msg = "gdi32.GetDIBits() failed." + raise ScreenShotError(msg) return self.cls_image(bytearray(self._handles.data), monitor) - def _cursor_impl(self) -> Optional[ScreenShot]: + def _cursor_impl(self) -> ScreenShot | None: """Retrieve all cursor data. Pixels have to be RGB.""" return None diff --git a/src/tests/bench_bgra2rgb.py b/src/tests/bench_bgra2rgb.py index 2319684..49043f5 100644 --- a/src/tests/bench_bgra2rgb.py +++ b/src/tests/bench_bgra2rgb.py @@ -1,6 +1,5 @@ -""" -This is part of the MSS Python's module. -Source: https://github.com/BoboTiG/python-mss +"""This is part of the MSS Python's module. +Source: https://github.com/BoboTiG/python-mss. 2018-03-19. @@ -30,34 +29,34 @@ """ import time -import numpy -from PIL import Image - import mss +import numpy as np +from mss.screenshot import ScreenShot +from PIL import Image -def mss_rgb(im): +def mss_rgb(im: ScreenShot) -> bytes: return im.rgb -def numpy_flip(im): - frame = numpy.array(im, dtype=numpy.uint8) - return numpy.flip(frame[:, :, :3], 2).tobytes() +def numpy_flip(im: ScreenShot) -> bytes: + frame = np.array(im, dtype=np.uint8) + return np.flip(frame[:, :, :3], 2).tobytes() -def numpy_slice(im): - return numpy.array(im, dtype=numpy.uint8)[..., [2, 1, 0]].tobytes() +def numpy_slice(im: ScreenShot) -> bytes: + return np.array(im, dtype=np.uint8)[..., [2, 1, 0]].tobytes() -def pil_frombytes_rgb(im): +def pil_frombytes_rgb(im: ScreenShot) -> bytes: return Image.frombytes("RGB", im.size, im.rgb).tobytes() -def pil_frombytes(im): +def pil_frombytes(im: ScreenShot) -> bytes: return Image.frombytes("RGB", im.size, im.bgra, "raw", "BGRX").tobytes() -def benchmark(): +def benchmark() -> None: with mss.mss() as sct: im = sct.grab(sct.monitors[0]) for func in ( @@ -71,7 +70,7 @@ def benchmark(): start = time.time() while (time.time() - start) <= 1: func(im) - im._ScreenShot__rgb = None + im._ScreenShot__rgb = None # type: ignore[attr-defined] count += 1 print(func.__name__.ljust(17), count) diff --git a/src/tests/bench_general.py b/src/tests/bench_general.py index 1fe44cc..cec26d5 100644 --- a/src/tests/bench_general.py +++ b/src/tests/bench_general.py @@ -1,6 +1,5 @@ -""" -This is part of the MSS Python's module. -Source: https://github.com/BoboTiG/python-mss +"""This is part of the MSS Python's module. +Source: https://github.com/BoboTiG/python-mss. 2018-03-19. @@ -25,32 +24,41 @@ access_rgb 574 712 +24.04 output 139 188 +35.25 """ +from __future__ import annotations + from time import time +from typing import TYPE_CHECKING import mss import mss.tools +if TYPE_CHECKING: + from collections.abc import Callable + + from mss.base import MSSBase + from mss.screenshot import ScreenShot + -def grab(sct): +def grab(sct: MSSBase) -> ScreenShot: monitor = {"top": 144, "left": 80, "width": 1397, "height": 782} return sct.grab(monitor) -def access_rgb(sct): +def access_rgb(sct: MSSBase) -> bytes: im = grab(sct) return im.rgb -def output(sct, filename=None): +def output(sct: MSSBase, filename: str | None = None) -> None: rgb = access_rgb(sct) mss.tools.to_png(rgb, (1397, 782), output=filename) -def save(sct): +def save(sct: MSSBase) -> None: output(sct, filename="screenshot.png") -def benchmark(func): +def benchmark(func: Callable) -> None: count = 0 start = time() diff --git a/src/tests/conftest.py b/src/tests/conftest.py index 72da4df..a97ccc3 100644 --- a/src/tests/conftest.py +++ b/src/tests/conftest.py @@ -1,46 +1,43 @@ -""" -This is part of the MSS Python's module. -Source: https://github.com/BoboTiG/python-mss +"""This is part of the MSS Python's module. +Source: https://github.com/BoboTiG/python-mss. """ import glob import os import platform from hashlib import md5 from pathlib import Path +from typing import Generator from zipfile import ZipFile import pytest - from mss import mss @pytest.fixture(autouse=True) -def no_warnings(recwarn): +def _no_warnings(recwarn: pytest.WarningsRecorder) -> Generator: """Fail on warning.""" - yield - warnings = ["{w.filename}:{w.lineno} {w.message}".format(w=warning) for warning in recwarn] + warnings = [f"{warning.filename}:{warning.lineno} {warning.message}" for warning in recwarn] for warning in warnings: print(warning) assert not warnings -def purge_files(): +def purge_files() -> None: """Remove all generated files from previous runs.""" - for fname in glob.glob("*.png"): - print("Deleting {!r} ...".format(fname)) + print(f"Deleting {fname!r} ...") os.unlink(fname) for fname in glob.glob("*.png.old"): - print("Deleting {!r} ...".format(fname)) + print(f"Deleting {fname!r} ...") os.unlink(fname) @pytest.fixture(scope="module", autouse=True) -def before_tests(request): - request.addfinalizer(purge_files) +def _before_tests() -> None: + purge_files() @pytest.fixture(scope="session") @@ -56,7 +53,6 @@ def raw() -> bytes: @pytest.fixture(scope="session") def pixel_ratio() -> int: """Get the pixel, used to adapt test checks.""" - if platform.system().lower() != "darwin": return 1 diff --git a/src/tests/test_bgra_to_rgb.py b/src/tests/test_bgra_to_rgb.py index 1fa2c04..ddd9529 100644 --- a/src/tests/test_bgra_to_rgb.py +++ b/src/tests/test_bgra_to_rgb.py @@ -1,20 +1,18 @@ -""" -This is part of the MSS Python's module. -Source: https://github.com/BoboTiG/python-mss +"""This is part of the MSS Python's module. +Source: https://github.com/BoboTiG/python-mss. """ import pytest - from mss.base import ScreenShot -def test_bad_length(): +def test_bad_length() -> None: data = bytearray(b"789c626001000000ffff030000060005") image = ScreenShot.from_size(data, 1024, 768) - with pytest.raises(ValueError): - image.rgb + with pytest.raises(ValueError, match="attempt to assign"): + _ = image.rgb -def test_good_types(raw: bytes): +def test_good_types(raw: bytes) -> None: image = ScreenShot.from_size(bytearray(raw), 1024, 768) assert isinstance(image.raw, bytearray) assert isinstance(image.rgb, bytes) diff --git a/src/tests/test_cls_image.py b/src/tests/test_cls_image.py index b531ba1..eb6b859 100644 --- a/src/tests/test_cls_image.py +++ b/src/tests/test_cls_image.py @@ -1,21 +1,22 @@ -""" -This is part of the MSS Python's module. -Source: https://github.com/BoboTiG/python-mss +"""This is part of the MSS Python's module. +Source: https://github.com/BoboTiG/python-mss. """ import os +from typing import Any from mss import mss +from mss.models import Monitor class SimpleScreenShot: - def __init__(self, data, monitor, **_): + def __init__(self, data: bytearray, monitor: Monitor, **_: Any) -> None: self.raw = bytes(data) self.monitor = monitor -def test_custom_cls_image(): +def test_custom_cls_image() -> None: with mss(display=os.getenv("DISPLAY")) as sct: - sct.cls_image = SimpleScreenShot + sct.cls_image = SimpleScreenShot # type: ignore[assignment] mon1 = sct.monitors[1] image = sct.grab(mon1) assert isinstance(image, SimpleScreenShot) diff --git a/src/tests/test_find_monitors.py b/src/tests/test_find_monitors.py index 278dc1b..7939e17 100644 --- a/src/tests/test_find_monitors.py +++ b/src/tests/test_find_monitors.py @@ -1,18 +1,17 @@ -""" -This is part of the MSS Python's module. -Source: https://github.com/BoboTiG/python-mss +"""This is part of the MSS Python's module. +Source: https://github.com/BoboTiG/python-mss. """ import os from mss import mss -def test_get_monitors(): +def test_get_monitors() -> None: with mss(display=os.getenv("DISPLAY")) as sct: assert sct.monitors -def test_keys_aio(): +def test_keys_aio() -> None: with mss(display=os.getenv("DISPLAY")) as sct: all_monitors = sct.monitors[0] assert "top" in all_monitors @@ -21,7 +20,7 @@ def test_keys_aio(): assert "width" in all_monitors -def test_keys_monitor_1(): +def test_keys_monitor_1() -> None: with mss(display=os.getenv("DISPLAY")) as sct: mon1 = sct.monitors[1] assert "top" in mon1 @@ -30,7 +29,7 @@ def test_keys_monitor_1(): assert "width" in mon1 -def test_dimensions(): +def test_dimensions() -> None: with mss(display=os.getenv("DISPLAY")) as sct: mon = sct.monitors[1] assert mon["width"] > 0 diff --git a/src/tests/test_get_pixels.py b/src/tests/test_get_pixels.py index 8535538..ec85f7f 100644 --- a/src/tests/test_get_pixels.py +++ b/src/tests/test_get_pixels.py @@ -1,19 +1,17 @@ -""" -This is part of the MSS Python's module. -Source: https://github.com/BoboTiG/python-mss +"""This is part of the MSS Python's module. +Source: https://github.com/BoboTiG/python-mss. """ import itertools import os import pytest - from mss import mss from mss.base import ScreenShot from mss.exception import ScreenShotError -def test_grab_monitor(): +def test_grab_monitor() -> None: with mss(display=os.getenv("DISPLAY")) as sct: for mon in sct.monitors: image = sct.grab(mon) @@ -22,7 +20,7 @@ def test_grab_monitor(): assert isinstance(image.rgb, bytes) -def test_grab_part_of_screen(pixel_ratio): +def test_grab_part_of_screen(pixel_ratio: int) -> None: with mss(display=os.getenv("DISPLAY")) as sct: for width, height in itertools.product(range(1, 42), range(1, 42)): monitor = {"top": 160, "left": 160, "width": width, "height": height} @@ -34,7 +32,7 @@ def test_grab_part_of_screen(pixel_ratio): assert image.height == height * pixel_ratio -def test_get_pixel(raw: bytes): +def test_get_pixel(raw: bytes) -> None: image = ScreenShot.from_size(bytearray(raw), 1024, 768) assert image.width == 1024 assert image.height == 768 diff --git a/src/tests/test_gnu_linux.py b/src/tests/test_gnu_linux.py index 1357790..cca29ab 100644 --- a/src/tests/test_gnu_linux.py +++ b/src/tests/test_gnu_linux.py @@ -1,14 +1,13 @@ -""" -This is part of the MSS Python's module. -Source: https://github.com/BoboTiG/python-mss +"""This is part of the MSS Python's module. +Source: https://github.com/BoboTiG/python-mss. """ import platform +from collections.abc import Generator from unittest.mock import Mock, patch -import pytest - import mss import mss.linux +import pytest from mss.base import MSSBase from mss.exception import ScreenShotError @@ -21,21 +20,19 @@ DEPTH = 24 -@pytest.fixture -def display() -> str: +@pytest.fixture() +def display() -> Generator: with pyvirtualdisplay.Display(size=(WIDTH, HEIGHT), color_depth=DEPTH) as vdisplay: yield vdisplay.new_display_var @pytest.mark.skipif(PYPY, reason="Failure on PyPy") -def test_factory_systems(monkeypatch): - """ - Here, we are testing all systems. +def test_factory_systems(monkeypatch: pytest.MonkeyPatch) -> None: + """Here, we are testing all systems. Too hard to maintain the test for all platforms, so test only on GNU/Linux. """ - # GNU/Linux monkeypatch.setattr(platform, "system", lambda: "LINUX") with mss.mss() as sct: @@ -44,79 +41,69 @@ def test_factory_systems(monkeypatch): # macOS monkeypatch.setattr(platform, "system", lambda: "Darwin") - with pytest.raises((ScreenShotError, ValueError)): - # ValueError on macOS Big Sur - with mss.mss(): - pass + # ValueError on macOS Big Sur + with pytest.raises((ScreenShotError, ValueError)), mss.mss(): + pass monkeypatch.undo() # Windows monkeypatch.setattr(platform, "system", lambda: "wInDoWs") - with pytest.raises(ImportError): - # ImportError: cannot import name 'WINFUNCTYPE' - with mss.mss(): - pass + with pytest.raises(ImportError, match="cannot import name 'WINFUNCTYPE'"), mss.mss(): + pass -def test_arg_display(display: str, monkeypatch): +def test_arg_display(display: str, monkeypatch: pytest.MonkeyPatch) -> None: # Good value with mss.mss(display=display): pass # Bad `display` (missing ":" in front of the number) - with pytest.raises(ScreenShotError): - with mss.mss(display="0"): - pass + with pytest.raises(ScreenShotError), mss.mss(display="0"): + pass # Invalid `display` that is not trivially distinguishable. - with pytest.raises(ScreenShotError): - with mss.mss(display=":INVALID"): - pass + with pytest.raises(ScreenShotError), mss.mss(display=":INVALID"): + pass # No `DISPLAY` in envars monkeypatch.delenv("DISPLAY") - with pytest.raises(ScreenShotError): - with mss.mss(): - pass + with pytest.raises(ScreenShotError), mss.mss(): + pass @pytest.mark.skipif(PYPY, reason="Failure on PyPy") -def test_bad_display_structure(monkeypatch): +def test_bad_display_structure(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setattr(mss.linux, "Display", lambda: None) - with pytest.raises(TypeError): - with mss.mss(): - pass + with pytest.raises(TypeError), mss.mss(): + pass @patch("mss.linux._X11", new=None) -def test_no_xlib_library(): - with pytest.raises(ScreenShotError): - with mss.mss(): - pass +def test_no_xlib_library() -> None: + with pytest.raises(ScreenShotError), mss.mss(): + pass @patch("mss.linux._XRANDR", new=None) -def test_no_xrandr_extension(): - with pytest.raises(ScreenShotError): - with mss.mss(): - pass +def test_no_xrandr_extension() -> None: + with pytest.raises(ScreenShotError), mss.mss(): + pass @patch("mss.linux.MSS._is_extension_enabled", new=Mock(return_value=False)) -def test_xrandr_extension_exists_but_is_not_enabled(display: str): - with pytest.raises(ScreenShotError): - with mss.mss(display=display): - pass +def test_xrandr_extension_exists_but_is_not_enabled(display: str) -> None: + with pytest.raises(ScreenShotError), mss.mss(display=display): + pass -def test_unsupported_depth(): +def test_unsupported_depth() -> None: with pyvirtualdisplay.Display(size=(WIDTH, HEIGHT), color_depth=8) as vdisplay: with pytest.raises(ScreenShotError): with mss.mss(display=vdisplay.new_display_var) as sct: sct.grab(sct.monitors[1]) -def test_region_out_of_monitor_bounds(display: str): +def test_region_out_of_monitor_bounds(display: str) -> None: monitor = {"left": -30, "top": 0, "width": WIDTH, "height": HEIGHT} assert not mss.linux._ERROR @@ -136,19 +123,22 @@ def test_region_out_of_monitor_bounds(display: str): assert not mss.linux._ERROR -def test__is_extension_enabled_unknown_name(display: str): +def test__is_extension_enabled_unknown_name(display: str) -> None: with mss.mss(display=display) as sct: + assert isinstance(sct, mss.linux.MSS) # For Mypy assert not sct._is_extension_enabled("NOEXT") -def test_missing_fast_function_for_monitor_details_retrieval(display: str): +def test_missing_fast_function_for_monitor_details_retrieval(display: str) -> None: with mss.mss(display=display) as sct: + assert isinstance(sct, mss.linux.MSS) # For Mypy assert hasattr(sct.xrandr, "XRRGetScreenResourcesCurrent") screenshot_with_fast_fn = sct.grab(sct.monitors[1]) assert set(screenshot_with_fast_fn.rgb) == {0} with mss.mss(display=display) as sct: + assert isinstance(sct, mss.linux.MSS) # For Mypy assert hasattr(sct.xrandr, "XRRGetScreenResourcesCurrent") del sct.xrandr.XRRGetScreenResourcesCurrent screenshot_with_slow_fn = sct.grab(sct.monitors[1]) @@ -156,7 +146,7 @@ def test_missing_fast_function_for_monitor_details_retrieval(display: str): assert set(screenshot_with_slow_fn.rgb) == {0} -def test_with_cursor(display: str): +def test_with_cursor(display: str) -> None: with mss.mss(display=display) as sct: assert not hasattr(sct, "xfixes") assert not sct.with_cursor @@ -175,14 +165,15 @@ def test_with_cursor(display: str): @patch("mss.linux._XFIXES", new=None) -def test_with_cursor_but_not_xfixes_extension_found(display: str): +def test_with_cursor_but_not_xfixes_extension_found(display: str) -> None: with mss.mss(display=display, with_cursor=True) as sct: assert not hasattr(sct, "xfixes") assert not sct.with_cursor -def test_with_cursor_failure(display: str): +def test_with_cursor_failure(display: str) -> None: with mss.mss(display=display, with_cursor=True) as sct: + assert isinstance(sct, mss.linux.MSS) # For Mypy with patch.object(sct.xfixes, "XFixesGetCursorImage", return_value=None): with pytest.raises(ScreenShotError): sct.grab(sct.monitors[1]) diff --git a/src/tests/test_implementation.py b/src/tests/test_implementation.py index caeca22..2ec96ac 100644 --- a/src/tests/test_implementation.py +++ b/src/tests/test_implementation.py @@ -1,34 +1,44 @@ +"""This is part of the MSS Python's module. +Source: https://github.com/BoboTiG/python-mss. """ -This is part of the MSS Python's module. -Source: https://github.com/BoboTiG/python-mss -""" +from __future__ import annotations + import os import os.path import platform import sys from datetime import datetime +from typing import TYPE_CHECKING from unittest.mock import Mock, patch -import pytest - +import mss import mss.tools -from mss import mss +import pytest from mss.__main__ import main as entry_point from mss.base import MSSBase from mss.exception import ScreenShotError from mss.screenshot import ScreenShot +if TYPE_CHECKING: + from mss.models import Monitor + +try: + from datetime import UTC +except ImportError: + # Python < 3.11 + from datetime import timezone + + UTC = timezone.utc + class MSS0(MSSBase): """Nothing implemented.""" - pass - class MSS1(MSSBase): """Only `grab()` implemented.""" - def grab(self, monitor): + def grab(self, monitor: Monitor) -> None: # type: ignore[override] pass @@ -36,23 +46,22 @@ class MSS2(MSSBase): """Only `monitor` implemented.""" @property - def monitors(self): + def monitors(self) -> list: return [] @pytest.mark.parametrize("cls", [MSS0, MSS1, MSS2]) -def test_incomplete_class(cls): +def test_incomplete_class(cls: type[MSSBase]) -> None: with pytest.raises(TypeError): cls() -def test_bad_monitor(): - with mss(display=os.getenv("DISPLAY")) as sct: - with pytest.raises(ScreenShotError): - sct.shot(mon=222) +def test_bad_monitor() -> None: + with mss.mss(display=os.getenv("DISPLAY")) as sct, pytest.raises(ScreenShotError): + sct.shot(mon=222) -def test_repr(pixel_ratio): +def test_repr(pixel_ratio: int) -> None: box = {"top": 0, "left": 0, "width": 10, "height": 10} expected_box = { "top": 0, @@ -60,21 +69,21 @@ def test_repr(pixel_ratio): "width": 10 * pixel_ratio, "height": 10 * pixel_ratio, } - with mss(display=os.getenv("DISPLAY")) as sct: + with mss.mss(display=os.getenv("DISPLAY")) as sct: img = sct.grab(box) ref = ScreenShot(bytearray(b"42"), expected_box) assert repr(img) == repr(ref) -def test_factory(monkeypatch): +def test_factory(monkeypatch: pytest.MonkeyPatch) -> None: # Current system - with mss() as sct: + with mss.mss() as sct: assert isinstance(sct, MSSBase) # Unknown monkeypatch.setattr(platform, "system", lambda: "Chuck Norris") with pytest.raises(ScreenShotError) as exc: - mss() + mss.mss() monkeypatch.undo() error = exc.value.args[0] @@ -83,10 +92,10 @@ def test_factory(monkeypatch): @patch.object(sys, "argv", new=[]) # Prevent side effects while testing @pytest.mark.parametrize("with_cursor", [False, True]) -def test_entry_point(with_cursor: bool, capsys): +def test_entry_point(with_cursor: bool, capsys: pytest.CaptureFixture) -> None: def main(*args: str, ret: int = 0) -> None: if with_cursor: - args = args + ("--with-cursor",) + args = (*args, "--with-cursor") assert entry_point(*args) == ret # No arguments @@ -105,8 +114,8 @@ def main(*args: str, ret: int = 0) -> None: assert os.path.isfile("monitor-1.png") os.remove("monitor-1.png") - for opt in zip(["-m 1", "--monitor=1"], ["-q", "--quiet"]): - main(*opt) + for opts in zip(["-m 1", "--monitor=1"], ["-q", "--quiet"]): + main(*opts) captured = capsys.readouterr() assert not captured.out assert os.path.isfile("monitor-1.png") @@ -116,7 +125,7 @@ def main(*args: str, ret: int = 0) -> None: for opt in ("-o", "--out"): main(opt, fmt) captured = capsys.readouterr() - with mss(display=os.getenv("DISPLAY")) as sct: + with mss.mss(display=os.getenv("DISPLAY")) as sct: for mon, (monitor, line) in enumerate(zip(sct.monitors[1:], captured.out.splitlines()), 1): filename = fmt.format(mon=mon, **monitor) assert line.endswith(filename) @@ -126,7 +135,7 @@ def main(*args: str, ret: int = 0) -> None: fmt = "sct_{mon}-{date:%Y-%m-%d}.png" for opt in ("-o", "--out"): main("-m 1", opt, fmt) - filename = fmt.format(mon=1, date=datetime.now()) + filename = fmt.format(mon=1, date=datetime.now(tz=UTC)) captured = capsys.readouterr() assert captured.out.endswith(filename + "\n") assert os.path.isfile(filename) @@ -151,10 +160,10 @@ def main(*args: str, ret: int = 0) -> None: @patch.object(sys, "argv", new=[]) # Prevent side effects while testing @patch("mss.base.MSSBase.monitors", new=[]) @pytest.mark.parametrize("quiet", [False, True]) -def test_entry_point_error(quiet: bool, capsys): +def test_entry_point_error(quiet: bool, capsys: pytest.CaptureFixture) -> None: def main(*args: str) -> int: if quiet: - args = args + ("--quiet",) + args = (*args, "--quiet") return entry_point(*args) if quiet: @@ -167,7 +176,7 @@ def main(*args: str) -> int: main() -def test_entry_point_with_no_argument(capsys): +def test_entry_point_with_no_argument(capsys: pytest.CaptureFixture) -> None: # Make sure to fail if arguments are not handled with patch("mss.factory.mss", new=Mock(side_effect=RuntimeError("Boom!"))): with patch.object(sys, "argv", ["mss", "--help"]): @@ -180,7 +189,7 @@ def test_entry_point_with_no_argument(capsys): assert "usage: mss" in captured.out -def test_grab_with_tuple(pixel_ratio: int): +def test_grab_with_tuple(pixel_ratio: int) -> None: left = 100 top = 100 right = 500 @@ -188,7 +197,7 @@ def test_grab_with_tuple(pixel_ratio: int): width = right - left # 400px width height = lower - top # 400px height - with mss(display=os.getenv("DISPLAY")) as sct: + with mss.mss(display=os.getenv("DISPLAY")) as sct: # PIL like box = (left, top, right, lower) im = sct.grab(box) @@ -202,8 +211,8 @@ def test_grab_with_tuple(pixel_ratio: int): assert im.rgb == im2.rgb -def test_grab_with_tuple_percents(pixel_ratio: int): - with mss(display=os.getenv("DISPLAY")) as sct: +def test_grab_with_tuple_percents(pixel_ratio: int) -> None: + with mss.mss(display=os.getenv("DISPLAY")) as sct: monitor = sct.monitors[1] left = monitor["left"] + monitor["width"] * 5 // 100 # 5% from the left top = monitor["top"] + monitor["height"] * 5 // 100 # 5% from the top @@ -225,22 +234,21 @@ def test_grab_with_tuple_percents(pixel_ratio: int): assert im.rgb == im2.rgb -def test_thread_safety(): +def test_thread_safety() -> None: """Regression test for issue #169.""" import threading import time - def record(check): + def record(check: dict) -> None: """Record for one second.""" - start_time = time.time() while time.time() - start_time < 1: - with mss() as sct: + with mss.mss() as sct: sct.grab(sct.monitors[1]) check[threading.current_thread()] = True - checkpoint = {} + checkpoint: dict = {} t1 = threading.Thread(target=record, args=(checkpoint,)) t2 = threading.Thread(target=record, args=(checkpoint,)) diff --git a/src/tests/test_issue_220.py b/src/tests/test_issue_220.py index 3fefccc..203147e 100644 --- a/src/tests/test_issue_220.py +++ b/src/tests/test_issue_220.py @@ -1,16 +1,14 @@ +"""This is part of the MSS Python's module. +Source: https://github.com/BoboTiG/python-mss. """ -This is part of the MSS Python's module. -Source: https://github.com/BoboTiG/python-mss -""" -import pytest - import mss +import pytest tkinter = pytest.importorskip("tkinter") -@pytest.fixture -def root() -> tkinter.Tk: +@pytest.fixture() +def root() -> tkinter.Tk: # type: ignore[name-defined] try: master = tkinter.Tk() except RuntimeError: @@ -22,13 +20,13 @@ def root() -> tkinter.Tk: master.destroy() -def take_screenshot(): +def take_screenshot() -> None: region = {"top": 370, "left": 1090, "width": 80, "height": 390} with mss.mss() as sct: sct.grab(region) -def create_top_level_win(master: tkinter.Tk): +def create_top_level_win(master: tkinter.Tk) -> None: # type: ignore[name-defined] top_level_win = tkinter.Toplevel(master) take_screenshot_btn = tkinter.Button(top_level_win, text="Take screenshot", command=take_screenshot) @@ -43,7 +41,7 @@ def create_top_level_win(master: tkinter.Tk): master.update() -def test_regression(root: tkinter.Tk, capsys): +def test_regression(root: tkinter.Tk, capsys: pytest.CaptureFixture) -> None: # type: ignore[name-defined] btn = tkinter.Button(root, text="Open TopLevel", command=lambda: create_top_level_win(root)) btn.pack() diff --git a/src/tests/test_leaks.py b/src/tests/test_leaks.py index 7e6a25e..7c0fd0f 100644 --- a/src/tests/test_leaks.py +++ b/src/tests/test_leaks.py @@ -1,25 +1,21 @@ -""" -This is part of the MSS Python's module. -Source: https://github.com/BoboTiG/python-mss +"""This is part of the MSS Python's module. +Source: https://github.com/BoboTiG/python-mss. """ import os import platform from typing import Callable +import mss import pytest -from mss import mss - OS = platform.system().lower() PID = os.getpid() def get_opened_socket() -> int: - """ - GNU/Linux: a way to get the opened sockets count. + """GNU/Linux: a way to get the opened sockets count. It will be used to check X server connections are well closed. """ - import subprocess cmd = f"lsof -U | grep {PID}" @@ -28,62 +24,59 @@ def get_opened_socket() -> int: def get_handles() -> int: - """ - Windows: a way to get the GDI handles count. + """Windows: a way to get the GDI handles count. It will be used to check the handles count is not growing, showing resource leaks. """ - import ctypes - PQI = 0x400 # PROCESS_QUERY_INFORMATION - GR_GDIOBJECTS = 0 - h = ctypes.windll.kernel32.OpenProcess(PQI, 0, PID) + PROCESS_QUERY_INFORMATION = 0x400 # noqa:N806 + GR_GDIOBJECTS = 0 # noqa:N806 + h = ctypes.windll.kernel32.OpenProcess(PROCESS_QUERY_INFORMATION, 0, PID) return ctypes.windll.user32.GetGuiResources(h, GR_GDIOBJECTS) -@pytest.fixture +@pytest.fixture() def monitor_func() -> Callable[[], int]: """OS specific function to check resources in use.""" - return get_opened_socket if OS == "linux" else get_handles -def bound_instance_without_cm(): - # Will always leak for now - sct = mss() +def bound_instance_without_cm() -> None: + # Will always leak + sct = mss.mss() sct.shot() -def bound_instance_without_cm_but_use_close(): - sct = mss() +def bound_instance_without_cm_but_use_close() -> None: + sct = mss.mss() sct.shot() sct.close() # Calling .close() twice should be possible sct.close() -def unbound_instance_without_cm(): - # Will always leak for now - mss().shot() +def unbound_instance_without_cm() -> None: + # Will always leak + mss.mss().shot() -def with_context_manager(): - with mss() as sct: +def with_context_manager() -> None: + with mss.mss() as sct: sct.shot() -def regression_issue_128(): +def regression_issue_128() -> None: """Regression test for issue #128: areas overlap.""" - with mss() as sct: + with mss.mss() as sct: area1 = {"top": 50, "left": 7, "width": 400, "height": 320, "mon": 1} sct.grab(area1) area2 = {"top": 200, "left": 200, "width": 320, "height": 320, "mon": 1} sct.grab(area2) -def regression_issue_135(): +def regression_issue_135() -> None: """Regression test for issue #135: multiple areas.""" - with mss() as sct: + with mss.mss() as sct: bounding_box_notes = {"top": 0, "left": 0, "width": 100, "height": 100} sct.grab(bounding_box_notes) bounding_box_test = {"top": 220, "left": 220, "width": 100, "height": 100} @@ -92,23 +85,21 @@ def regression_issue_135(): sct.grab(bounding_box_score) -def regression_issue_210(): +def regression_issue_210() -> None: """Regression test for issue #210: multiple X servers.""" pyvirtualdisplay = pytest.importorskip("pyvirtualdisplay") - with pyvirtualdisplay.Display(size=(1920, 1080), color_depth=24): - with mss(): - pass + with pyvirtualdisplay.Display(size=(1920, 1080), color_depth=24), mss.mss(): + pass - with pyvirtualdisplay.Display(size=(1920, 1080), color_depth=24): - with mss(): - pass + with pyvirtualdisplay.Display(size=(1920, 1080), color_depth=24), mss.mss(): + pass @pytest.mark.skipif(OS == "darwin", reason="No possible leak on macOS.") @pytest.mark.parametrize( "func", - ( + [ # bound_instance_without_cm, bound_instance_without_cm_but_use_close, # unbound_instance_without_cm, @@ -116,11 +107,10 @@ def regression_issue_210(): regression_issue_128, regression_issue_135, regression_issue_210, - ), + ], ) -def test_resource_leaks(func, monitor_func): +def test_resource_leaks(func: Callable[[], None], monitor_func: Callable[[], int]) -> None: """Check for resource leaks with different use cases.""" - # Warm-up func() diff --git a/src/tests/test_macos.py b/src/tests/test_macos.py index 113b33e..9346581 100644 --- a/src/tests/test_macos.py +++ b/src/tests/test_macos.py @@ -1,60 +1,60 @@ -""" -This is part of the MSS Python's module. -Source: https://github.com/BoboTiG/python-mss +"""This is part of the MSS Python's module. +Source: https://github.com/BoboTiG/python-mss. """ import ctypes.util import platform -import pytest - import mss +import pytest from mss.exception import ScreenShotError if platform.system().lower() != "darwin": pytestmark = pytest.mark.skip +import mss.darwin -def test_repr(): - from mss.darwin import CGPoint, CGRect, CGSize +def test_repr() -> None: # CGPoint - point = CGPoint(2.0, 1.0) - ref = CGPoint() - ref.x = 2.0 - ref.y = 1.0 - assert repr(point) == repr(ref) + point = mss.darwin.CGPoint(2.0, 1.0) + ref1 = mss.darwin.CGPoint() + ref1.x = 2.0 + ref1.y = 1.0 + assert repr(point) == repr(ref1) # CGSize - size = CGSize(2.0, 1.0) - ref = CGSize() - ref.width = 2.0 - ref.height = 1.0 - assert repr(size) == repr(ref) + size = mss.darwin.CGSize(2.0, 1.0) + ref2 = mss.darwin.CGSize() + ref2.width = 2.0 + ref2.height = 1.0 + assert repr(size) == repr(ref2) # CGRect - rect = CGRect(point, size) - ref = CGRect() - ref.origin.x = 2.0 - ref.origin.y = 1.0 - ref.size.width = 2.0 - ref.size.height = 1.0 - assert repr(rect) == repr(ref) + rect = mss.darwin.CGRect(point, size) + ref3 = mss.darwin.CGRect() + ref3.origin.x = 2.0 + ref3.origin.y = 1.0 + ref3.size.width = 2.0 + ref3.size.height = 1.0 + assert repr(rect) == repr(ref3) -def test_implementation(monkeypatch): +def test_implementation(monkeypatch: pytest.MonkeyPatch) -> None: # No `CoreGraphics` library version = float(".".join(platform.mac_ver()[0].split(".")[:2])) if version < 10.16: - monkeypatch.setattr(ctypes.util, "find_library", lambda x: None) + monkeypatch.setattr(ctypes.util, "find_library", lambda _: None) with pytest.raises(ScreenShotError): mss.mss() monkeypatch.undo() with mss.mss() as sct: + assert isinstance(sct, mss.darwin.MSS) # For Mypy + # Test monitor's rotation original = sct.monitors[1] - monkeypatch.setattr(sct.core, "CGDisplayRotation", lambda x: -90.0) + monkeypatch.setattr(sct.core, "CGDisplayRotation", lambda _: -90.0) sct._monitors = [] modified = sct.monitors[1] assert original["width"] == modified["height"] @@ -62,6 +62,6 @@ def test_implementation(monkeypatch): monkeypatch.undo() # Test bad data retrieval - monkeypatch.setattr(sct.core, "CGWindowListCreateImage", lambda *args: None) + monkeypatch.setattr(sct.core, "CGWindowListCreateImage", lambda *_: None) with pytest.raises(ScreenShotError): sct.grab(sct.monitors[1]) diff --git a/src/tests/test_save.py b/src/tests/test_save.py index 6dfbc19..a46a4fb 100644 --- a/src/tests/test_save.py +++ b/src/tests/test_save.py @@ -1,21 +1,27 @@ -""" -This is part of the MSS Python's module. -Source: https://github.com/BoboTiG/python-mss +"""This is part of the MSS Python's module. +Source: https://github.com/BoboTiG/python-mss. """ import os.path from datetime import datetime import pytest - from mss import mss +try: + from datetime import UTC +except ImportError: + # Python < 3.11 + from datetime import timezone + + UTC = timezone.utc + -def test_at_least_2_monitors(): +def test_at_least_2_monitors() -> None: with mss(display=os.getenv("DISPLAY")) as sct: assert list(sct.save(mon=0)) -def test_files_exist(): +def test_files_exist() -> None: with mss(display=os.getenv("DISPLAY")) as sct: for filename in sct.save(): assert os.path.isfile(filename) @@ -26,8 +32,8 @@ def test_files_exist(): assert os.path.isfile("fullscreen.png") -def test_callback(): - def on_exists(fname): +def test_callback() -> None: + def on_exists(fname: str) -> None: if os.path.isfile(fname): new_file = f"{fname}.old" os.rename(fname, new_file) @@ -40,14 +46,14 @@ def on_exists(fname): assert os.path.isfile(filename) -def test_output_format_simple(): +def test_output_format_simple() -> None: with mss(display=os.getenv("DISPLAY")) as sct: filename = sct.shot(mon=1, output="mon-{mon}.png") assert filename == "mon-1.png" assert os.path.isfile(filename) -def test_output_format_positions_and_sizes(): +def test_output_format_positions_and_sizes() -> None: fmt = "sct-{top}x{left}_{width}x{height}.png" with mss(display=os.getenv("DISPLAY")) as sct: filename = sct.shot(mon=1, output=fmt) @@ -55,20 +61,20 @@ def test_output_format_positions_and_sizes(): assert os.path.isfile(filename) -def test_output_format_date_simple(): +def test_output_format_date_simple() -> None: fmt = "sct_{mon}-{date}.png" with mss(display=os.getenv("DISPLAY")) as sct: try: filename = sct.shot(mon=1, output=fmt) assert os.path.isfile(filename) - except IOError: + except OSError: # [Errno 22] invalid mode ('wb') or filename: 'sct_1-2019-01-01 21:20:43.114194.png' pytest.mark.xfail("Default date format contains ':' which is not allowed.") -def test_output_format_date_custom(): +def test_output_format_date_custom() -> None: fmt = "sct_{date:%Y-%m-%d}.png" with mss(display=os.getenv("DISPLAY")) as sct: filename = sct.shot(mon=1, output=fmt) - assert filename == fmt.format(date=datetime.now()) + assert filename == fmt.format(date=datetime.now(tz=UTC)) assert os.path.isfile(filename) diff --git a/src/tests/test_setup.py b/src/tests/test_setup.py index 5bec6fb..1bd5a66 100644 --- a/src/tests/test_setup.py +++ b/src/tests/test_setup.py @@ -1,6 +1,5 @@ -""" -This is part of the MSS Python's module. -Source: https://github.com/BoboTiG/python-mss +"""This is part of the MSS Python's module. +Source: https://github.com/BoboTiG/python-mss. """ import platform import tarfile @@ -8,7 +7,6 @@ from zipfile import ZipFile import pytest - from mss import __version__ if platform.system().lower() != "linux": @@ -22,13 +20,13 @@ CHECK = "twine check --strict".split() -def test_sdist(): +def test_sdist() -> None: output = check_output(SDIST, stderr=STDOUT, text=True) file = f"mss-{__version__}.tar.gz" assert f"Successfully built {file}" in output assert "warning" not in output.lower() - check_call(CHECK + [f"dist/{file}"]) + check_call([*CHECK, f"dist/{file}"]) with tarfile.open(f"dist/{file}", mode="r:gz") as fh: files = sorted(fh.getnames()) @@ -95,13 +93,13 @@ def test_sdist(): ] -def test_wheel(): +def test_wheel() -> None: output = check_output(WHEEL, stderr=STDOUT, text=True) file = f"mss-{__version__}-py3-none-any.whl" assert f"Successfully built {file}" in output assert "warning" not in output.lower() - check_call(CHECK + [f"dist/{file}"]) + check_call([*CHECK, f"dist/{file}"]) with ZipFile(f"dist/{file}") as fh: files = sorted(fh.namelist()) diff --git a/src/tests/test_third_party.py b/src/tests/test_third_party.py index e89afbd..b5443c7 100644 --- a/src/tests/test_third_party.py +++ b/src/tests/test_third_party.py @@ -1,20 +1,18 @@ -""" -This is part of the MSS Python's module. -Source: https://github.com/BoboTiG/python-mss +"""This is part of the MSS Python's module. +Source: https://github.com/BoboTiG/python-mss. """ import itertools import os import os.path import pytest - from mss import mss try: - import numpy + import numpy as np except (ImportError, RuntimeError): # RuntimeError on Python 3.9 (macOS): Polyfit sanity test emitted a warning, ... - numpy = None + np = None try: from PIL import Image @@ -22,16 +20,16 @@ Image = None -@pytest.mark.skipif(numpy is None, reason="Numpy module not available.") -def test_numpy(pixel_ratio): +@pytest.mark.skipif(np is None, reason="Numpy module not available.") +def test_numpy(pixel_ratio: int) -> None: box = {"top": 0, "left": 0, "width": 10, "height": 10} with mss(display=os.getenv("DISPLAY")) as sct: - img = numpy.array(sct.grab(box)) + img = np.array(sct.grab(box)) assert len(img) == 10 * pixel_ratio @pytest.mark.skipif(Image is None, reason="PIL module not available.") -def test_pil(): +def test_pil() -> None: width, height = 16, 16 box = {"top": 0, "left": 0, "width": width, "height": height} with mss(display=os.getenv("DISPLAY")) as sct: @@ -49,7 +47,7 @@ def test_pil(): @pytest.mark.skipif(Image is None, reason="PIL module not available.") -def test_pil_bgra(): +def test_pil_bgra() -> None: width, height = 16, 16 box = {"top": 0, "left": 0, "width": width, "height": height} with mss(display=os.getenv("DISPLAY")) as sct: @@ -67,7 +65,7 @@ def test_pil_bgra(): @pytest.mark.skipif(Image is None, reason="PIL module not available.") -def test_pil_not_16_rounded(): +def test_pil_not_16_rounded() -> None: width, height = 10, 10 box = {"top": 0, "left": 0, "width": width, "height": height} with mss(display=os.getenv("DISPLAY")) as sct: diff --git a/src/tests/test_tools.py b/src/tests/test_tools.py index d939cb1..618f682 100644 --- a/src/tests/test_tools.py +++ b/src/tests/test_tools.py @@ -1,13 +1,11 @@ -""" -This is part of the MSS Python's module. -Source: https://github.com/BoboTiG/python-mss +"""This is part of the MSS Python's module. +Source: https://github.com/BoboTiG/python-mss. """ import hashlib import os.path import zlib import pytest - from mss import mss from mss.tools import to_png @@ -16,13 +14,12 @@ MD5SUM = "055e615b74167c9bdfea16a00539450c" -def test_bad_compression_level(): - with mss(compression_level=42, display=os.getenv("DISPLAY")) as sct: - with pytest.raises(zlib.error): - sct.shot() +def test_bad_compression_level() -> None: + with mss(compression_level=42, display=os.getenv("DISPLAY")) as sct, pytest.raises(zlib.error): + sct.shot() -def test_compression_level(): +def test_compression_level() -> None: data = b"rgb" * WIDTH * HEIGHT output = f"{WIDTH}x{HEIGHT}.png" @@ -34,7 +31,7 @@ def test_compression_level(): @pytest.mark.parametrize( - "level, checksum", + ("level", "checksum"), [ (0, "f37123dbc08ed7406d933af11c42563e"), (1, "7d5dcf2a2224445daf19d6d91cf31cb5"), @@ -48,14 +45,15 @@ def test_compression_level(): (9, "4d88d3f5923b6ef05b62031992294839"), ], ) -def test_compression_levels(level, checksum): +def test_compression_levels(level: int, checksum: str) -> None: data = b"rgb" * WIDTH * HEIGHT raw = to_png(data, (WIDTH, HEIGHT), level=level) + assert isinstance(raw, bytes) md5 = hashlib.md5(raw).hexdigest() assert md5 == checksum -def test_output_file(): +def test_output_file() -> None: data = b"rgb" * WIDTH * HEIGHT output = f"{WIDTH}x{HEIGHT}.png" to_png(data, (WIDTH, HEIGHT), output=output) @@ -65,7 +63,8 @@ def test_output_file(): assert hashlib.md5(png.read()).hexdigest() == MD5SUM -def test_output_raw_bytes(): +def test_output_raw_bytes() -> None: data = b"rgb" * WIDTH * HEIGHT raw = to_png(data, (WIDTH, HEIGHT)) + assert isinstance(raw, bytes) assert hashlib.md5(raw).hexdigest() == MD5SUM diff --git a/src/tests/test_windows.py b/src/tests/test_windows.py index 3f247ca..1227395 100644 --- a/src/tests/test_windows.py +++ b/src/tests/test_windows.py @@ -1,30 +1,36 @@ +"""This is part of the MSS Python's module. +Source: https://github.com/BoboTiG/python-mss. """ -This is part of the MSS Python's module. -Source: https://github.com/BoboTiG/python-mss -""" -import platform -import threading +from __future__ import annotations -import pytest +import threading +from typing import Tuple import mss +import pytest from mss.exception import ScreenShotError -if platform.system().lower() != "windows": +try: + import mss.windows +except ImportError: pytestmark = pytest.mark.skip -def test_implementation(monkeypatch): +def test_implementation(monkeypatch: pytest.MonkeyPatch) -> None: # Test bad data retrieval with mss.mss() as sct: - monkeypatch.setattr(sct.gdi32, "GetDIBits", lambda *args: 0) + assert isinstance(sct, mss.windows.MSS) # For Mypy + + monkeypatch.setattr(sct.gdi32, "GetDIBits", lambda *_: 0) with pytest.raises(ScreenShotError): sct.shot() -def test_region_caching(): +def test_region_caching() -> None: """The region to grab is cached, ensure this is well-done.""" with mss.mss() as sct: + assert isinstance(sct, mss.windows.MSS) # For Mypy + # Grab the area 1 region1 = {"top": 0, "left": 0, "width": 200, "height": 200} sct.grab(region1) @@ -42,11 +48,14 @@ def test_region_caching(): assert bmp2 == id(sct._handles.bmp) -def test_region_not_caching(): +def test_region_not_caching() -> None: """The region to grab is not bad cached previous grab.""" grab1 = mss.mss() grab2 = mss.mss() + assert isinstance(grab1, mss.windows.MSS) # For Mypy + assert isinstance(grab2, mss.windows.MSS) # For Mypy + region1 = {"top": 0, "left": 0, "width": 100, "height": 100} region2 = {"top": 0, "left": 0, "width": 50, "height": 1} grab1.grab(region1) @@ -61,14 +70,15 @@ def test_region_not_caching(): assert bmp1 != bmp2 -def run_child_thread(loops): +def run_child_thread(loops: int) -> None: for _ in range(loops): with mss.mss() as sct: # New sct for every loop sct.grab(sct.monitors[1]) -def test_thread_safety(): +def test_thread_safety() -> None: """Thread safety test for issue #150. + The following code will throw a ScreenShotError exception if thread-safety is not guaranted. """ # Let thread 1 finished ahead of thread 2 @@ -80,14 +90,15 @@ def test_thread_safety(): thread2.join() -def run_child_thread_bbox(loops, bbox): +def run_child_thread_bbox(loops: int, bbox: Tuple[int, int, int, int]) -> None: with mss.mss() as sct: # One sct for all loops for _ in range(loops): sct.grab(bbox) -def test_thread_safety_regions(): - """Thread safety test for different regions +def test_thread_safety_regions() -> None: + """Thread safety test for different regions. + The following code will throw a ScreenShotError exception if thread-safety is not guaranted. """ thread1 = threading.Thread(target=run_child_thread_bbox, args=(100, (0, 0, 100, 100))) 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 116/242] 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 3eae42b..cef76da 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 a6a7bf8..e756d62 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 cad551b..2d5f72a 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 117/242] doc: tweak --- CHANGELOG.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 887a0c8..03362dd 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 118/242] doc: tweak --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 03362dd..e0e18ea 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 119/242] 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 204c47d..f5c8e79 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 120/242] 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 7bb90ae..8028ab2 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 a107176..cb64443 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 c57e111..f357579 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 7a33843..537e2eb 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 a54ac3e..c83455f 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 c5ed5f4..3c5297b 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 2070aea..bb6c395 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 94bdbc3..87d11dd 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 bcc17bb..5ef341d 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 56bfbdc..6099f58 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 01a6b01..cdf33ed 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 54c5722..5a5d677 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 cb490e2..6a8595a 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 9b74506..1cac8fc 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 4495bf1..4438693 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 f247c51..ce951ef 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 4201367..61b3e69 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 fea7df3..83ea0d3 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 7d0c8fc..54aa72e 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 2d5f72a..c795361 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 316939c..2383665 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 fab2794..2e10274 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 49043f5..299561d 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 cec26d5..5de4f1c 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 a97ccc3..5a4964b 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 ddd9529..99e5531 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 eb6b859..10c9cfe 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 7939e17..81c6e1e 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 cca29ab..8f9f68f 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 2ec96ac..23413cf 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 203147e..e76f78a 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 7c0fd0f..57de1c7 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 9346581..10e43ff 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 a46a4fb..3ae0f6f 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 1bd5a66..41b94d5 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 b5443c7..c62191b 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 618f682..2f41e49 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 1227395..19a7056 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 121/242] 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 20ae634..f9d35d2 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 122/242] 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 5a0d125..2819e3e 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 123/242] 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 8b0b4f4..61468c4 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 e0e18ea..ddb1a8f 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 2819e3e..c8065c3 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 124/242] chore: pin all deps --- pyproject.toml | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index c8065c3..50a398e 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 125/242] 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 537e2eb..4e4080e 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 87d11dd..9275de2 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 cdf33ed..03ff778 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 5a5d677..d1264bc 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 299561d..6acaffb 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 5a4964b..1234c8d 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 99e5531..a481c1f 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 ec85f7f..211c710 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 8f9f68f..b7d7829 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 23413cf..54abb27 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 e76f78a..24884c4 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 57de1c7..094438b 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 10e43ff..cce8121 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 3ae0f6f..a8d6e38 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 41b94d5..2dba9e6 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 c62191b..1313d86 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 2f41e49..ff742e8 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 19a7056..f5d9966 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 126/242] 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 2dba9e6..5fd2f81 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 0000000..e69de29 diff --git a/src/tests/third_party/test_numpy.py b/src/tests/third_party/test_numpy.py new file mode 100644 index 0000000..a7a279d --- /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 1313d86..3555d7a 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 127/242] Version 9.0.2 --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ddb1a8f..94fb97c 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 128/242] 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 94fb97c..bff2728 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 6a8595a..428819c 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 129/242] 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 61468c4..70475d4 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 bff2728..dd09d14 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 eb0bfce..0fca352 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 c42c671..3a0d03b 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 8a11d6b..102dc01 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 50a398e..ab7a25a 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 130/242] 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 8028ab2..d07b357 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 ab7a25a..282c7a8 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 4438693..6d04c68 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 61b3e69..7fdf211 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 54aa72e..46ba33e 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 c795361..67ad6a4 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 b7d7829..87d53ee 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 54abb27..0d81311 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 f5d9966..7a8e071 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 131/242] docs: tweak --- CHANGELOG.md | 152 +++++++++++++++++++++++++-------------------------- 1 file changed, 76 insertions(+), 76 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dd09d14..4aaa466 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 132/242] 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 282c7a8..631eac9 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 133/242] 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 70475d4..57fe089 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 4aaa466..0cfeda9 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 0fca352..7980829 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 d1264bc..d231dbb 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 3a0d03b..d2d4063 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 102dc01..3462aae 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 631eac9..c68e7c7 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 e756d62..665a41b 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 67ad6a4..2588d8b 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 1234c8d..0a30eb8 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 0d81311..d0b3a5d 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 094438b..db5de6a 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 134/242] 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 6d04c68..00981a8 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 ce951ef..6d4fa28 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 46ba33e..5ae285e 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 2588d8b..c160027 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 2e10274..0f41cd6 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 5de4f1c..100a472 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 d0b3a5d..42429b1 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 135/242] 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 57fe089..5605f44 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 136/242] 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 5605f44..036f385 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 137/242] 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 036f385..c7f10e7 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 d117f10..9605fbd 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 0000000..ed065c8 --- /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 ea3376f..cda211e 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 7980829..5ede99b 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 cef76da..b1d87f4 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 d9c3e53..1d29bd7 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 6adb7bc..aede8c3 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 f357579..2a1f810 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 4e4080e..f9e7613 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 d2d4063..5dae783 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 0dae108..d003f79 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 47bf7cb..903ee38 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 c68e7c7..7ff6377 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 1cac8fc..384ad34 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="http://github.com/store_true", help="include the cursor") cli_args.add_argument( diff --git a/src/mss/base.py b/src/mss/base.py index 00981a8..9e92607 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 c160027..e51a37f 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 10c9cfe..84ed926 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 42429b1..97b6e87 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 138/242] 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 c7f10e7..ee770f7 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 139/242] 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 84ed926..10c9cfe 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 97b6e87..42429b1 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 140/242] 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 db5de6a..f27e7f7 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 141/242] 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 7ff6377..28db256 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 142/242] 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 28db256..3a8f7c2 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 143/242] 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 3a8f7c2..4d58bbd 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 144/242] 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 4d58bbd..64e73b1 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 145/242] 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 64e73b1..5ff278f 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 146/242] 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 5ff278f..82bcc5e 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 147/242] 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 c83455f..c4a2a38 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 95acc6f..18c87e6 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 5ae285e..6dac52b 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 7a8e071..1e5763b 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 148/242] 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 82bcc5e..e397e27 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 149/242] 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 e397e27..d5ad838 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 150/242] 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 d5ad838..b7f9098 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 151/242] 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 b7f9098..edd6e8b 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 152/242] 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 edd6e8b..6fb0e06 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 153/242] 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 6fb0e06..ce96325 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 154/242] 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 ce96325..b879754 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 155/242] 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 b879754..b9dd1bc 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 156/242] 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 b9dd1bc..908d419 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 157/242] 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 0000000..b59ae9a --- /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 158/242] 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 ee770f7..7e6a477 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 159/242] 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 7e6a477..d55fd06 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 160/242] 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 18c87e6..a6307d0 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 161/242] 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 d55fd06..0592acc 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 162/242] 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 908d419..3969ddf 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 163/242] 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 aede8c3..7bb8157 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 164/242] 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 3969ddf..202cb56 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 165/242] 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 202cb56..82766b1 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 166/242] 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 82766b1..f7ee5e9 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 167/242] 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 f7ee5e9..4afbafb 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 168/242] 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 4afbafb..2df0a2b 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 169/242] 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 2df0a2b..06ac0d5 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 170/242] 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 06ac0d5..e772f82 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 171/242] 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 e772f82..df78ece 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 172/242] 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 0592acc..1caf9b9 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 0cfeda9..0752f7e 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 df78ece..27f7abe 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 a7a279d..6a2f2e0 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 3555d7a..a7d3b7b 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 173/242] 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 1caf9b9..fa93b92 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 0752f7e..8aedde9 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 5ede99b..abfc4f7 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 d231dbb..d1264bc 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 5dae783..f28aee4 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 3462aae..102dc01 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 27f7abe..94746c1 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 9e92607..cf588d0 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 e51a37f..5bcf654 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 42429b1..146d336 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 174/242] 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 cda211e..74021f6 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 cb64443..5a93d12 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 94746c1..1f5fc2d 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 cf588d0..8a7397f 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 6d4fa28..a56e05a 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 83ea0d3..b0793e8 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 6dac52b..d357e3b 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 2383665..9eb8b6f 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 0f41cd6..7a3a78f 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 0000000..e69de29 diff --git a/src/tests/conftest.py b/src/tests/conftest.py index 0a30eb8..5d45582 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 146d336..5672f04 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 a8d6e38..9597206 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 5fd2f81..d478867 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 ff742e8..a149483 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 a7d3b7b..a319448 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 175/242] 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 8aedde9..9946de9 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 d357e3b..6ea56af 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 176/242] 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 9946de9..681e2b4 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 6ea56af..20f8550 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 177/242] 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 681e2b4..d8e6c05 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 74021f6..2b456f8 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 102dc01..c0e4eff 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 178/242] 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 d8e6c05..39068fc 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 428819c..29798f6 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 179/242] 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 1f5fc2d..16f228b 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 180/242] 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 16f228b..f604491 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 181/242] 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 f604491..8a24f53 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 182/242] 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 8a24f53..75fef4b 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 183/242] 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 75fef4b..d12df6e 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 184/242] 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 d12df6e..3f82b39 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 185/242] 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 3f82b39..6f9a0ef 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 186/242] 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 6f9a0ef..90e3ebb 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 187/242] 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 90e3ebb..88d9e1e 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 188/242] 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 88d9e1e..6ab23cb 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 189/242] 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 6ab23cb..39be579 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 190/242] 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 39be579..4ded615 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 191/242] 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 4ded615..65a4b46 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 192/242] 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 bdcbc50..0b055a0 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 29798f6..f0282e4 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 193/242] 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 65a4b46..a70d09a 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 194/242] 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 a70d09a..5e02e48 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 195/242] 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 5e02e48..1f4827d 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 196/242] 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 1f4827d..0640ae9 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 197/242] 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 0640ae9..83e8f12 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 198/242] 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 83e8f12..dcb01cc 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 199/242] 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 dcb01cc..0396ff1 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 200/242] 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 0396ff1..c8751bd 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 201/242] 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 c8751bd..8c58af4 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 202/242] 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 8c58af4..4085c04 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 203/242] 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 4085c04..839977e 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 204/242] 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 839977e..1d072d8 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 205/242] 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 1d072d8..2b1b2e4 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 206/242] 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 2b1b2e4..acf3d19 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 207/242] 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 acf3d19..cf17491 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 208/242] 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 cf17491..a7d555d 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 209/242] 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 a7d555d..cfbf3ed 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 210/242] 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 cfbf3ed..4ee54f5 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 211/242] 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 4ee54f5..c7edf3e 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 212/242] 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 c7edf3e..59ba5c3 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 213/242] 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 59ba5c3..0cc7943 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 214/242] fix(ci): documentation configuration --- .readthedocs.yml | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/.readthedocs.yml b/.readthedocs.yml index 0a201cf..c62360f 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 215/242] 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 9605fbd..7942681 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 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 Jm&#fO{2w7`#lHXm literal 0 HcmV?d00001 diff --git a/docs/source/conf.py b/docs/source/conf.py index f9d35d2..a0d9b99 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 1d29bd7..3dfe19b 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 903ee38..4e105a8 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 0cc7943..4b4b7a6 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 216/242] 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 4b4b7a6..be02434 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 217/242] 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 be02434..88d7cc3 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 218/242] 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 88d7cc3..80b28a8 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 219/242] docs: tweak --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index abfc4f7..cda7793 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 220/242] 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 f28aee4..e0e4471 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 221/242] 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 80b28a8..f02b506 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 222/242] 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 f02b506..cfcdd5e 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 223/242] 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 cfcdd5e..ffe7703 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 224/242] Update README.md --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index cda7793..d81a79c 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 225/242] 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 ffe7703..7a2b8e5 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 226/242] 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 7a2b8e5..a077371 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 227/242] 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 39068fc..f0da012 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 2b456f8..f1030bd 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 f0282e4..ef0faaa 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 a56e05a..f001398 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 20f8550..009b423 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 7a3a78f..d5e2bb7 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 5d45582..97928b7 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 211c710..486823c 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 5672f04..294ccc8 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 cce8121..c89ea2a 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 6a2f2e0..6d5cf28 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 228/242] 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 a077371..f169b5b 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 229/242] 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 f169b5b..8c9a597 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 230/242] 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 8c9a597..6be795c 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 231/242] 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 6be795c..76d6ef8 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 232/242] 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 76d6ef8..23be9e4 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 233/242] 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 23be9e4..4177a4f 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 234/242] 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 4075752..8d9e0b2 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 235/242] 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 4177a4f..bc32397 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 236/242] 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 bc32397..8f41577 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 237/242] 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 8f41577..6280be8 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 238/242] 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 6280be8..200517f 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 239/242] 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 200517f..1f3581d 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'", ] From 9ee1b9c718c638bc1a30d5052c15f28b8cc53887 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 19 May 2025 15:16:50 +0200 Subject: [PATCH 240/242] build(deps): bump ruff from 0.11.9 to 0.11.10 (#368) Bumps [ruff](https://github.com/astral-sh/ruff) from 0.11.9 to 0.11.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.11.9...0.11.10) --- updated-dependencies: - dependency-name: ruff dependency-version: 0.11.10 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 1f3581d..6f7b092 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.9", + "ruff==0.11.10", "twine==6.1.0", ] docs = [ From 96e9ead785dcc6e6ab3f0d690fd4077a34a9d693 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 26 May 2025 15:56:25 +0200 Subject: [PATCH 241/242] build(deps): bump ruff from 0.11.10 to 0.11.11 (#369) Bumps [ruff](https://github.com/astral-sh/ruff) from 0.11.10 to 0.11.11. - [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.10...0.11.11) --- updated-dependencies: - dependency-name: ruff dependency-version: 0.11.11 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 6f7b092..c321917 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.10", + "ruff==0.11.11", "twine==6.1.0", ] docs = [ From d7813b5d9794a73aaf01632955e9d98d49112d4e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Jun 2025 15:09:10 +0200 Subject: [PATCH 242/242] build(deps): bump shibuya from 2025.4.25 to 2025.5.30 (#370) Bumps [shibuya](https://github.com/lepture/shibuya) from 2025.4.25 to 2025.5.30. - [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.4.25...2025.5.30) --- updated-dependencies: - dependency-name: shibuya dependency-version: 2025.5.30 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 c321917..0712d9b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -77,7 +77,7 @@ dev = [ "twine==6.1.0", ] docs = [ - "shibuya==2025.4.25", + "shibuya==2025.5.30", "sphinx==8.2.3", "sphinx-copybutton==0.5.2", "sphinx-new-tab-link==0.8.0",