xvfbwrapper 0.2.29__tar.gz → 0.2.30__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: xvfbwrapper
3
- Version: 0.2.29
3
+ Version: 0.2.30
4
4
  Summary: Manage headless displays with Xvfb (X virtual framebuffer)
5
5
  Author: Corey Goldberg
6
6
  Maintainer: Corey Goldberg
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "xvfbwrapper"
7
- version = "0.2.29"
7
+ version = "0.2.30"
8
8
  description = "Manage headless displays with Xvfb (X virtual framebuffer)"
9
9
  license = "MIT"
10
10
  license-files = ["LICENSE"]
@@ -216,7 +216,8 @@ class TestXvfb(XvfbCleanTestCase):
216
216
  unwriteable_dir = "/etc"
217
217
  xvfb = Xvfb(tempdir=unwriteable_dir)
218
218
  with self.assertRaisesRegex(
219
- RuntimeError, f"Could not access writable temp directory: {unwriteable_dir}"
219
+ RuntimeError,
220
+ f"Could not access writable temp directory: {unwriteable_dir}",
220
221
  ):
221
222
  xvfb.start()
222
223
  self.assertIsNone(xvfb.proc)
@@ -225,7 +226,8 @@ class TestXvfb(XvfbCleanTestCase):
225
226
  unknown_dir = "/tmp/some_unknown_path"
226
227
  xvfb = Xvfb(tempdir=unknown_dir)
227
228
  with self.assertRaisesRegex(
228
- RuntimeError, f"Could not access writable temp directory: {unknown_dir}"
229
+ RuntimeError,
230
+ f"Could not access writable temp directory: {unknown_dir}",
229
231
  ):
230
232
  xvfb.start()
231
233
  self.assertIsNone(xvfb.proc)
@@ -300,7 +302,7 @@ class TestXvfb(XvfbCleanTestCase):
300
302
  self.assertIsNotNone(xvfb.proc)
301
303
 
302
304
  def test_start_fails_with_unknown_kwargs(self):
303
- xvfb = Xvfb(foo="bar", timeout=5)
305
+ xvfb = Xvfb(foo="bar", timeout=2)
304
306
  expected_cmd_args = [
305
307
  "Xvfb",
306
308
  r":\d",
@@ -311,7 +313,8 @@ class TestXvfb(XvfbCleanTestCase):
311
313
  "bar",
312
314
  ]
313
315
  with self.assertRaisesRegex(
314
- RuntimeError, f"Xvfb display did not open: {expected_cmd_args}"
316
+ RuntimeError,
317
+ f"Xvfb display did not open: {expected_cmd_args}",
315
318
  ):
316
319
  xvfb.start()
317
320
  self.assertIsNone(xvfb.proc)
@@ -14,9 +14,6 @@ envlist =
14
14
  lint
15
15
  type
16
16
  py310
17
- py311
18
- py312
19
- py313
20
17
  py314
21
18
  pypy3
22
19
  skip_missing_interpreters = true
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: xvfbwrapper
3
- Version: 0.2.29
3
+ Version: 0.2.30
4
4
  Summary: Manage headless displays with Xvfb (X virtual framebuffer)
5
5
  Author: Corey Goldberg
6
6
  Maintainer: Corey Goldberg
@@ -39,46 +39,39 @@ class Xvfb:
39
39
  set_xdg_session_type: bool = False,
40
40
  environ: MutableMapping[str, str] | None = None,
41
41
  extra_args: Sequence[str] | None = None,
42
- timeout: float = 10,
42
+ timeout: float | None = 10,
43
43
  **kwargs: str,
44
44
  ) -> None:
45
45
  self.width: int = width
46
46
  self.height: int = height
47
47
  self.colordepth: int = colordepth
48
48
  self._tempdir: Path | str = tempdir or tempfile.gettempdir()
49
- self._timeout: float = timeout
49
+ self._timeout: float | None = timeout
50
50
  self.new_display: int | None = display
51
51
  self.environ: MutableMapping[str, str] = environ or os.environ
52
-
53
52
  if set_xdg_session_type:
54
53
  os.environ["XDG_SESSION_TYPE"] = "x11"
55
-
54
+ self.environ["XDG_SESSION_TYPE"] = "x11"
56
55
  if not self._xvfb_exists():
57
56
  raise FileNotFoundError(
58
57
  "Could not find Xvfb. Please install it and try again"
59
58
  )
60
-
61
59
  self.xvfb_cmd: list[str] = []
62
-
63
60
  if not extra_args:
64
61
  extra_args = []
65
-
66
62
  self.extra_xvfb_args: list[str] = [
67
63
  "-screen",
68
64
  "0",
69
65
  f"{self.width}x{self.height}x{self.colordepth}",
70
66
  *extra_args,
71
67
  ]
72
-
73
68
  for key, value in kwargs.items():
74
69
  self.extra_xvfb_args += [f"-{key}", value]
75
-
76
70
  self.orig_display_var: str | None
77
71
  if "DISPLAY" in self.environ:
78
72
  self.orig_display_var = self.environ["DISPLAY"]
79
73
  else:
80
74
  self.orig_display_var = None
81
-
82
75
  self.proc: subprocess.Popen[bytes] | None = None
83
76
  self._lock_display_file: TextIO | None = None
84
77
 
@@ -95,6 +88,8 @@ class Xvfb:
95
88
  self.stop()
96
89
 
97
90
  def start(self) -> None:
91
+ """Start Xvfb."""
92
+
98
93
  if not os.access(self._tempdir, os.W_OK):
99
94
  raise RuntimeError(
100
95
  f"Could not access writable temp directory: {self._tempdir}"
@@ -113,10 +108,11 @@ class Xvfb:
113
108
  close_fds=True,
114
109
  env=dict(self.environ),
115
110
  )
116
- start = time.time()
111
+ start = time.monotonic()
117
112
  while not self._local_display_exists(self.new_display):
118
113
  time.sleep(1e-3)
119
- if time.time() - start > self._timeout:
114
+ elapsed = time.monotonic() - start
115
+ if self._timeout is not None and elapsed > self._timeout:
120
116
  self.stop()
121
117
  raise RuntimeError(f"Xvfb display did not open: {self.xvfb_cmd}")
122
118
  ret_code = self.proc.poll()
@@ -127,6 +123,19 @@ class Xvfb:
127
123
  raise RuntimeError(f"Xvfb did not start ({ret_code}): {self.xvfb_cmd}")
128
124
 
129
125
  def stop(self) -> None:
126
+ """Stop Xvfb and clean up its resources.
127
+
128
+ Terminate the process, escalating to kill if it does not exit within the
129
+ timeout. If the process has already exited, termination is treated as
130
+ successful. If `self._timeout` is set, wait up to that duration for termination
131
+ and, if necessary, an additional duration of the same length after killing the
132
+ process. If `self._timeout` is `None`, wait indefinitely for the process to
133
+ exit and be reaped.
134
+
135
+ This is a one-way lifecycle transition: once called, this object is considered
136
+ stopped and cannot be reused, regardless of whether the process exits
137
+ successfully.
138
+ """
130
139
  if self.proc is None:
131
140
  return
132
141
  try:
@@ -134,13 +143,15 @@ class Xvfb:
134
143
  self.environ.pop("DISPLAY", None)
135
144
  else:
136
145
  self._set_display(self.orig_display_var)
137
-
138
- with suppress(OSError):
146
+ with suppress(ProcessLookupError):
139
147
  self.proc.terminate()
148
+ try:
149
+ self.proc.wait(self._timeout)
150
+ except subprocess.TimeoutExpired:
151
+ self.proc.kill()
140
152
  self.proc.wait(self._timeout)
141
-
142
- self.proc = None
143
153
  finally:
154
+ self.proc = None
144
155
  self._cleanup_lock_file()
145
156
 
146
157
  def _xvfb_exists(self) -> bool:
@@ -160,7 +171,6 @@ class Xvfb:
160
171
  """
161
172
  if self._lock_display_file is None:
162
173
  return
163
-
164
174
  self._lock_display_file.close()
165
175
  with suppress(OSError):
166
176
  Path(self._lock_display_file.name).unlink()
File without changes
File without changes
File without changes
File without changes