Skip to content

Commit 57d8c5d

Browse files
committed
Add a --backend argument to the CLI
1 parent 44bf176 commit 57d8c5d

File tree

7 files changed

+140
-68
lines changed

7 files changed

+140
-68
lines changed

docs/source/examples.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ GNU/Linux XShm backend
107107
----------------------
108108

109109
Select the XShmGetImage backend explicitly and inspect whether it is active or
110-
falling back to XGetImage::
110+
falling back to XGetImage:
111111

112112
.. literalinclude:: examples/linux_xshm_backend.py
113113
:lines: 7-

docs/source/usage.rst

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ GNU/Linux
7979
Display
8080
^^^^^^^
8181

82-
On GNU/Linux, the default display is taken from the :envvar:`DISPLAY` environment variable. You can instead specify which display to use (useful for distant screenshots via SSH) using the ``display`` keyword::
82+
On GNU/Linux, the default display is taken from the :envvar:`DISPLAY` environment variable. You can instead specify which display to use (useful for distant screenshots via SSH) using the ``display`` keyword:
8383

8484
.. literalinclude:: examples/linux_display_keyword.py
8585
:lines: 7-
@@ -116,8 +116,8 @@ You can use ``mss`` via the CLI::
116116
Or via direct call from Python::
117117

118118
$ python -m mss --help
119-
usage: __main__.py [-h] [-c COORDINATES] [-l {0,1,2,3,4,5,6,7,8,9}]
120-
[-m MONITOR] [-o OUTPUT] [-q] [-v] [--with-cursor]
119+
usage: mss [-h] [-c COORDINATES] [-l {0,1,2,3,4,5,6,7,8,9}] [-m MONITOR]
120+
[-o OUTPUT] [--with-cursor] [-q] [-b BACKEND] [-v]
121121

122122
options:
123123
-h, --help show this help message and exit
@@ -129,6 +129,9 @@ Or via direct call from Python::
129129
the monitor to screenshot
130130
-o OUTPUT, --output OUTPUT
131131
the output file name
132+
-b, --backend BACKEND
133+
platform-specific backend to use
134+
(Linux: default/xlib/xgetimage/xshmgetimage; macOS/Windows: default)
132135
--with-cursor include the cursor
133136
-q, --quiet do not print created files
134137
-v, --version show program's version number and exit

src/mss/__main__.py

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,38 @@
33
"""
44

55
import os.path
6+
import platform
67
import sys
7-
from argparse import ArgumentParser
8+
from argparse import ArgumentError, ArgumentParser
89

910
from mss import __version__
1011
from mss.exception import ScreenShotError
1112
from mss.factory import mss
1213
from mss.tools import to_png
1314

1415

16+
def _backend_cli_choices() -> list[str]:
17+
os_name = platform.system().lower()
18+
if os_name == "darwin":
19+
from mss import darwin # noqa: PLC0415
20+
21+
return list(darwin.BACKENDS)
22+
if os_name == "linux":
23+
from mss import linux # noqa: PLC0415
24+
25+
return list(linux.BACKENDS)
26+
if os_name == "windows":
27+
from mss import windows # noqa: PLC0415
28+
29+
return list(windows.BACKENDS)
30+
return ["default"]
31+
32+
1533
def main(*args: str) -> int:
1634
"""Main logic."""
17-
cli_args = ArgumentParser(prog="mss")
35+
backend_choices = _backend_cli_choices()
36+
37+
cli_args = ArgumentParser(prog="mss", exit_on_error=False)
1838
cli_args.add_argument(
1939
"-c",
2040
"--coordinates",
@@ -40,9 +60,19 @@ def main(*args: str) -> int:
4060
action="store_true",
4161
help="do not print created files",
4262
)
63+
cli_args.add_argument(
64+
"-b", "--backend", default="default", choices=backend_choices, help="platform-specific backend to use"
65+
)
4366
cli_args.add_argument("-v", "--version", action="version", version=__version__)
4467

45-
options = cli_args.parse_args(args or None)
68+
try:
69+
options = cli_args.parse_args(args or None)
70+
except ArgumentError as e:
71+
# By default, parse_args will print and the error and exit. We
72+
# return instead of exiting, to make unit testing easier.
73+
cli_args.print_usage(sys.stderr)
74+
print(f"{cli_args.prog}: error: {e}", file=sys.stderr)
75+
return 2
4676
kwargs = {"mon": options.monitor, "output": options.output}
4777
if options.coordinates:
4878
try:
@@ -61,7 +91,7 @@ def main(*args: str) -> int:
6191
kwargs["output"] = "sct-{top}x{left}_{width}x{height}.png"
6292

6393
try:
64-
with mss(with_cursor=options.with_cursor) as sct:
94+
with mss(with_cursor=options.with_cursor, backend=options.backend) as sct:
6595
if options.coordinates:
6696
output = kwargs["output"].format(**kwargs["mon"])
6797
sct_img = sct.grab(kwargs["mon"])

src/mss/darwin.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020

2121
__all__ = ("MSS",)
2222

23+
BACKENDS = ["default"]
24+
2325
MAC_VERSION_CATALINA = 10.16
2426

2527
kCGWindowImageBoundsIgnoreFraming = 1 << 0 # noqa: N816

src/mss/linux/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
from mss.base import MSSBase
44
from mss.exception import ScreenShotError
55

6+
BACKENDS = ["default", "xlib", "xgetimage", "xshmgetimage"]
7+
68

79
def mss(backend: str = "default", **kwargs: Any) -> MSSBase:
810
"""Factory returning a proper MSS class instance.
@@ -30,6 +32,7 @@ class for instantiation.
3032
from . import xshmgetimage # noqa: PLC0415
3133

3234
return xshmgetimage.MSS(**kwargs)
35+
assert backend not in BACKENDS # noqa: S101
3336
msg = f"Backend {backend!r} not (yet?) implemented."
3437
raise ScreenShotError(msg)
3538

src/mss/windows.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@
3535

3636
__all__ = ("MSS",)
3737

38+
BACKENDS = ["default"]
39+
3840

3941
CAPTUREBLT = 0x40000000
4042
DIB_RGB_COLORS = 0

src/tests/test_implementation.py

Lines changed: 92 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -95,72 +95,104 @@ def test_factory_unknown_system(backend: str, monkeypatch: pytest.MonkeyPatch) -
9595
assert error == "System 'chuck norris' not (yet?) implemented."
9696

9797

98-
@patch.object(sys, "argv", new=[]) # Prevent side effects while testing
98+
@pytest.fixture
99+
def reset_sys_argv(monkeypatch: pytest.MonkeyPatch) -> None:
100+
monkeypatch.setattr(sys, "argv", [])
101+
102+
103+
@pytest.mark.usefixtures("reset_sys_argv")
99104
@pytest.mark.parametrize("with_cursor", [False, True])
100-
def test_entry_point(with_cursor: bool, capsys: pytest.CaptureFixture) -> None:
101-
def main(*args: str, ret: int = 0) -> None:
105+
class TestEntryPoint:
106+
"""CLI entry-point scenarios split into focused tests."""
107+
108+
@staticmethod
109+
def _run_main(with_cursor: bool, *args: str, ret: int = 0) -> None:
102110
if with_cursor:
103111
args = (*args, "--with-cursor")
104112
assert entry_point(*args) == ret
105113

106-
# No arguments
107-
main()
108-
captured = capsys.readouterr()
109-
for mon, line in enumerate(captured.out.splitlines(), 1):
110-
filename = Path(f"monitor-{mon}.png")
111-
assert line.endswith(filename.name)
112-
assert filename.is_file()
113-
filename.unlink()
114-
115-
file = Path("monitor-1.png")
116-
for opt in ("-m", "--monitor"):
117-
main(opt, "1")
118-
captured = capsys.readouterr()
119-
assert captured.out.endswith(f"{file.name}\n")
120-
assert filename.is_file()
121-
filename.unlink()
122-
123-
for opts in zip(["-m 1", "--monitor=1"], ["-q", "--quiet"]):
124-
main(*opts)
125-
captured = capsys.readouterr()
126-
assert not captured.out
127-
assert filename.is_file()
128-
filename.unlink()
129-
130-
fmt = "sct-{mon}-{width}x{height}.png"
131-
for opt in ("-o", "--out"):
132-
main(opt, fmt)
133-
captured = capsys.readouterr()
134-
with mss.mss(display=os.getenv("DISPLAY")) as sct:
135-
for mon, (monitor, line) in enumerate(zip(sct.monitors[1:], captured.out.splitlines()), 1):
136-
filename = Path(fmt.format(mon=mon, **monitor))
137-
assert line.endswith(filename.name)
138-
assert filename.is_file()
139-
filename.unlink()
140-
141-
fmt = "sct_{mon}-{date:%Y-%m-%d}.png"
142-
for opt in ("-o", "--out"):
143-
main("-m 1", opt, fmt)
144-
filename = Path(fmt.format(mon=1, date=datetime.now(tz=UTC)))
145-
captured = capsys.readouterr()
146-
assert captured.out.endswith(f"{filename}\n")
147-
assert filename.is_file()
148-
filename.unlink()
149-
150-
coordinates = "2,12,40,67"
151-
filename = Path("sct-2x12_40x67.png")
152-
for opt in ("-c", "--coordinates"):
153-
main(opt, coordinates)
154-
captured = capsys.readouterr()
155-
assert captured.out.endswith(f"{filename}\n")
156-
assert filename.is_file()
157-
filename.unlink()
158-
159-
coordinates = "2,12,40"
160-
for opt in ("-c", "--coordinates"):
161-
main(opt, coordinates, ret=2)
114+
def test_no_arguments(self, with_cursor: bool, capsys: pytest.CaptureFixture) -> None:
115+
self._run_main(with_cursor)
162116
captured = capsys.readouterr()
163-
assert captured.out == "Coordinates syntax: top, left, width, height\n"
117+
for mon, line in enumerate(captured.out.splitlines(), 1):
118+
filename = Path(f"monitor-{mon}.png")
119+
assert line.endswith(filename.name)
120+
assert filename.is_file()
121+
filename.unlink()
122+
123+
def test_monitor_option_and_quiet(self, with_cursor: bool, capsys: pytest.CaptureFixture) -> None:
124+
file = Path("monitor-1.png")
125+
filename: Path | None = None
126+
for opt in ("-m", "--monitor"):
127+
self._run_main(with_cursor, opt, "1")
128+
captured = capsys.readouterr()
129+
assert captured.out.endswith(f"{file.name}\n")
130+
filename = Path(captured.out.rstrip())
131+
assert filename.is_file()
132+
filename.unlink()
133+
134+
assert filename is not None
135+
for opts in zip(["-m 1", "--monitor=1"], ["-q", "--quiet"]):
136+
self._run_main(with_cursor, *opts)
137+
captured = capsys.readouterr()
138+
assert not captured.out
139+
assert filename.is_file()
140+
filename.unlink()
141+
142+
def test_custom_output_pattern(self, with_cursor: bool, capsys: pytest.CaptureFixture) -> None:
143+
fmt = "sct-{mon}-{width}x{height}.png"
144+
for opt in ("-o", "--out"):
145+
self._run_main(with_cursor, opt, fmt)
146+
captured = capsys.readouterr()
147+
with mss.mss(display=os.getenv("DISPLAY")) as sct:
148+
for mon, (monitor, line) in enumerate(zip(sct.monitors[1:], captured.out.splitlines()), 1):
149+
filename = Path(fmt.format(mon=mon, **monitor))
150+
assert line.endswith(filename.name)
151+
assert filename.is_file()
152+
filename.unlink()
153+
154+
def test_output_pattern_with_date(self, with_cursor: bool, capsys: pytest.CaptureFixture) -> None:
155+
fmt = "sct_{mon}-{date:%Y-%m-%d}.png"
156+
for opt in ("-o", "--out"):
157+
self._run_main(with_cursor, "-m 1", opt, fmt)
158+
filename = Path(fmt.format(mon=1, date=datetime.now(tz=UTC)))
159+
captured = capsys.readouterr()
160+
assert captured.out.endswith(f"{filename}\n")
161+
assert filename.is_file()
162+
filename.unlink()
163+
164+
def test_coordinates_capture(self, with_cursor: bool, capsys: pytest.CaptureFixture) -> None:
165+
coordinates = "2,12,40,67"
166+
filename = Path("sct-2x12_40x67.png")
167+
for opt in ("-c", "--coordinates"):
168+
self._run_main(with_cursor, opt, coordinates)
169+
captured = capsys.readouterr()
170+
assert captured.out.endswith(f"{filename}\n")
171+
assert filename.is_file()
172+
filename.unlink()
173+
174+
def test_invalid_coordinates(self, with_cursor: bool, capsys: pytest.CaptureFixture) -> None:
175+
coordinates = "2,12,40"
176+
for opt in ("-c", "--coordinates"):
177+
self._run_main(with_cursor, opt, coordinates, ret=2)
178+
captured = capsys.readouterr()
179+
assert captured.out == "Coordinates syntax: top, left, width, height\n"
180+
181+
def test_backend_option(self, with_cursor: bool, capsys: pytest.CaptureFixture) -> None:
182+
backend = "default"
183+
for opt in ("-b", "--backend"):
184+
self._run_main(with_cursor, opt, backend, "-m1")
185+
captured = capsys.readouterr()
186+
filename = Path(captured.out.rstrip())
187+
assert filename.is_file()
188+
filename.unlink()
189+
190+
def test_invalid_backend_option(self, with_cursor: bool, capsys: pytest.CaptureFixture) -> None:
191+
backend = "chuck_norris"
192+
for opt in ("-b", "--backend"):
193+
self._run_main(with_cursor, opt, backend, "-m1", ret=2)
194+
captured = capsys.readouterr()
195+
assert "argument -b/--backend: invalid choice: 'chuck_norris' (choose from" in captured.err
164196

165197

166198
@patch.object(sys, "argv", new=[]) # Prevent side effects while testing

0 commit comments

Comments
 (0)