Skip to content

Commit c6f31c3

Browse files
committed
List the keyword args for mss.MSS explicitly, instead of using **kwargs.
This lets IDEs and code checkers know what is acceptable. This lets them autocomplete, and identify incorrect options in user code.
1 parent 6f7d4cb commit c6f31c3

6 files changed

Lines changed: 71 additions & 41 deletions

File tree

src/mss/base.py

Lines changed: 59 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,21 @@
4949
OPAQUE = 255
5050

5151

52+
# A sentinel value to indicate that a parameter was not passed, as opposed to being passed with a value of None. This
53+
# is used in the MSS constructor to distinguish between the user not passing a parameter, and the user explicitly
54+
# passing None (which is the default for some parameters). This allows us to preserve the existing behavior of ignoring
55+
# certain parameters on certain platforms, while still allowing users to explicitly set those parameters on platforms
56+
# where they are supported.
57+
class _PlatformSpecific:
58+
def __init__(self, sphinx_repr: Any) -> None:
59+
self.sphinx_repr = str(sphinx_repr)
60+
61+
def __repr__(self) -> str:
62+
# This is used to get Sphinx to show a useful default when it shows the default in the summary, rather than
63+
# an opaque object.
64+
return self.sphinx_repr
65+
66+
5267
__all__ = ()
5368

5469

@@ -64,14 +79,12 @@ class MSSImplementation(ABC):
6479
with_cursor: bool
6580

6681
def __init__(self, /, *, with_cursor: bool = False) -> None:
67-
# We put with_cursor on the MSSImplementation because the Xlib
68-
# backend will turn it off if the library isn't installed.
69-
# (It's not a separate library under XCB.) So, we need to let
70-
# the backend mutate it.
71-
72-
# We should remove this expectation in 11.0. It seems
73-
# unlikely to be practically useful, Xlib is legacy, and just
74-
# complicates things.
82+
# We put with_cursor on the MSSImplementation because the Xlib backend will turn it off if the library isn't
83+
# installed. (It's not a separate library under XCB.) So, we need to let the backend mutate it. Note that
84+
# the other platforms don't support with_cursor, and don't pass it to us.
85+
#
86+
# TODO(jholveck): #493 We should remove this expectation in 11.0. It seems unlikely to be practically useful,
87+
# Xlib is legacy, and just complicates things.
7588
self.with_cursor = with_cursor
7689

7790
@abstractmethod
@@ -156,43 +169,67 @@ class MSS:
156169
157170
:param backend: Backend selector, for platforms with multiple backends.
158171
:param compression_level: PNG compression level.
159-
:param with_cursor: Include the mouse cursor in screenshots.
172+
:param with_cursor: Include the mouse cursor in screenshots (GNU/Linux only)
173+
:type display: bool, optional (default False)
160174
:param display: X11 display name (GNU/Linux only).
161175
:type display: bytes | str, optional (default :envvar:`$DISPLAY`)
162176
:param max_displays: Maximum number of displays to enumerate (macOS only).
163177
:type max_displays: int, optional (default 32)
164178
165179
.. versionadded:: 8.0.0
166180
``compression_level``, ``display``, ``max_displays``, and ``with_cursor`` keyword arguments.
181+
182+
.. versionadded:: 10.2.0
183+
``backend`` keyword argument.
167184
"""
168185

186+
# We want to:
187+
# * Let Sphinx, IDEs, and code-checkers know all the possible kwargs.
188+
# * Know if a user explicitly passed an unsupported platform-dependent keyword, so we can warn.
189+
# * Show a meaningful default in the Sphinx doc's summary string
190+
#
191+
# To accomplish this:
192+
# * We list the possibilities explicitly in the __init__ kwargs.
193+
# * We use a sentinel value, so we can tell whether or not the user actually gave us a value.
194+
# * We represent the "default value" sentinel object with something different, so Sphinx formats it usefully.
195+
_PD_WITH_CURSOR = _PlatformSpecific(False) # noqa: FBT003
196+
_PD_DISPLAY = _PlatformSpecific(None)
197+
_PD_MAX_DISPLAYS = _PlatformSpecific(32)
198+
169199
def __init__(
170200
self,
171201
/,
172202
*,
173203
backend: str = "default",
174204
compression_level: int = 6,
175-
**kwargs: Any,
205+
with_cursor: bool | _PlatformSpecific = _PD_WITH_CURSOR,
206+
display: bytes | str | None | _PlatformSpecific = _PD_DISPLAY,
207+
max_displays: int | _PlatformSpecific = _PD_MAX_DISPLAYS,
176208
) -> None:
177-
# TODO(jholveck): #493 Accept platform-specific kwargs on all platforms for migration ease. Foreign kwargs
178-
# are silently stripped with a warning.
179-
platform_only: dict[str, str] = {
180-
"display": "linux",
181-
"max_displays": "darwin",
182-
}
183-
os_ = platform.system().lower()
184-
for kwarg_name, target_platform in platform_only.items():
185-
if kwarg_name in kwargs and os_ != target_platform:
186-
kwargs.pop(kwarg_name)
209+
impl_kwargs = {}
210+
211+
system = platform.system().lower()
212+
for name, value, supported_platform in [
213+
("with_cursor", with_cursor, "Linux"),
214+
("display", display, "Linux"),
215+
("max_displays", max_displays, "Darwin"),
216+
]:
217+
if isinstance(value, _PlatformSpecific):
218+
continue
219+
if system != supported_platform.lower():
220+
# TODO(jholveck): #493 Accept platform-specific kwargs on all platforms for migration ease. Foreign
221+
# kwargs are silently stripped with a warning.
187222
warnings.warn(
188-
f"{kwarg_name} is only used on {target_platform}. This will be an error in the future.",
223+
f"{name} is only available on {supported_platform}. This will be an error in the future.",
189224
DeprecationWarning,
190225
stacklevel=2,
191226
)
227+
else:
228+
impl_kwargs[name] = value
192229

193230
self._impl: MSSImplementation = _choose_impl(
194231
backend=backend,
195-
**kwargs,
232+
**impl_kwargs,
196233
)
197234

198235
# The cls_image is only used atomically, so does not require locking.

src/mss/darwin.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -137,9 +137,8 @@ class MSSImplDarwin(MSSImplementation):
137137

138138
__slots__ = {"core", "max_displays"}
139139

140-
def __init__(self, backend: str = "default", max_displays: int = 32, **kwargs: Any) -> None:
141-
kwargs.pop("with_cursor", None)
142-
super().__init__(with_cursor=False, **kwargs)
140+
def __init__(self, *, backend: str = "default", max_displays: int = 32) -> None:
141+
super().__init__()
143142

144143
if backend != "default":
145144
msg = 'The only valid backend on this platform is "default".'

src/mss/linux/base.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,8 @@ class MSSImplXCBBase(MSSImplementation):
4242
Lists other parameters.
4343
"""
4444

45-
def __init__(self, /, display: str | bytes | None = None, **kwargs: Any) -> None: # noqa: PLR0912
46-
super().__init__(**kwargs)
45+
def __init__(self, *, display: str | bytes | None = None, with_cursor: bool = False) -> None: # noqa: PLR0912
46+
super().__init__(with_cursor=with_cursor)
4747

4848
if not display:
4949
display = None

src/mss/linux/xshmgetimage.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,8 @@ class MSSImplXShmGetImage(MSSImplXCBBase):
4545
Lists constructor parameters.
4646
"""
4747

48-
def __init__(self, /, **kwargs: Any) -> None:
49-
super().__init__(**kwargs)
48+
def __init__(self, *, display: str | bytes | None = None, with_cursor: bool = False) -> None:
49+
super().__init__(display=display, with_cursor=with_cursor)
5050

5151
# These are the objects we need to clean up when we shut down. They are created in _setup_shm.
5252
self._memfd: int | None = None

src/mss/windows.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -209,9 +209,8 @@ class MSSImplWindows(MSSImplementation):
209209
"user32",
210210
}
211211

212-
def __init__(self, backend: str = "default", **kwargs: Any) -> None:
213-
kwargs.pop("with_cursor", None)
214-
super().__init__(with_cursor=False, **kwargs)
212+
def __init__(self, *, backend: str = "default") -> None:
213+
super().__init__()
215214

216215
if backend != "default":
217216
msg = 'The only valid backend on this platform is "default".'

src/tests/test_compat_10_1.py

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -157,16 +157,11 @@ def test_deprecated_factory_accepts_documented_kwargs() -> None:
157157
"display": getenv("DISPLAY"), # None on non-Linux
158158
}
159159

160-
with pytest.warns(DeprecationWarning) as caught: # noqa: PT030 (we test the contents below)
160+
with (
161+
pytest.warns(DeprecationWarning, match=r"^mss\.mss is deprecated"),
162+
pytest.warns(DeprecationWarning, match=r"is only available on"),
163+
):
161164
context = mss.mss(**kwargs)
162165

163-
expected_messages = {"mss.mss is deprecated", "is only used on"}
164-
165-
assert any("mss.mss is deprecated" in str(w.message) for w in caught)
166-
assert any("is only used on" in str(w.message) for w in caught)
167-
assert all(any(expected in str(w.message) for expected in expected_messages) for w in caught), (
168-
f"Unexpected warnings: {[str(w.message) for w in caught]}"
169-
)
170-
171166
with context as sct:
172167
assert isinstance(sct, MSSBase)

0 commit comments

Comments
 (0)