xvfbwrapper 0.2.28__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.28
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
@@ -164,7 +164,7 @@ xvfb.start()
164
164
  from xvfbwrapper import Xvfb
165
165
 
166
166
  xvfb = Xvfb(display=23)
167
- xvfb.start() # Xvfb will start on display :23
167
+ xvfb.start() # Xvfb will start on display :23
168
168
  ```
169
169
 
170
170
  #### Setting XDG_SESSION_TYPE:
@@ -194,7 +194,7 @@ creating an `xvfbwrapper.Xvfb` instance. For example:
194
194
  from xvfbwrapper import Xvfb
195
195
 
196
196
  xvfb = Xvfb(nolisten="tcp")
197
- xvfb.start() # Xvfb will be called with the `-nolisten tcp` argument
197
+ xvfb.start() # Xvfb will be called with the `-nolisten tcp` argument
198
198
  ```
199
199
 
200
200
  However, there are other possible types of arguments:
@@ -212,7 +212,7 @@ an `xvfbwrapper.Xvfb` instance. For example:
212
212
  from xvfbwrapper import Xvfb
213
213
 
214
214
  xvfb = Xvfb(extra_args=("ttyxx", "-nocursor", "+extension", "RANDR"))
215
- xvfb.start() # Xvfb will be called with the `ttyxx -nocursor +extension RANDR` arguments
215
+ xvfb.start() # Xvfb will be called with the `ttyxx -nocursor +extension RANDR` arguments
216
216
  ```
217
217
 
218
218
  #### Multithreaded execution:
@@ -260,7 +260,6 @@ from xvfbwrapper import Xvfb
260
260
 
261
261
 
262
262
  class TestPage(unittest.TestCase):
263
-
264
263
  def setUp(self):
265
264
  xvfb = Xvfb(set_xdg_session_type=True)
266
265
  xvfb.start()
@@ -133,7 +133,7 @@ xvfb.start()
133
133
  from xvfbwrapper import Xvfb
134
134
 
135
135
  xvfb = Xvfb(display=23)
136
- xvfb.start() # Xvfb will start on display :23
136
+ xvfb.start() # Xvfb will start on display :23
137
137
  ```
138
138
 
139
139
  #### Setting XDG_SESSION_TYPE:
@@ -163,7 +163,7 @@ creating an `xvfbwrapper.Xvfb` instance. For example:
163
163
  from xvfbwrapper import Xvfb
164
164
 
165
165
  xvfb = Xvfb(nolisten="tcp")
166
- xvfb.start() # Xvfb will be called with the `-nolisten tcp` argument
166
+ xvfb.start() # Xvfb will be called with the `-nolisten tcp` argument
167
167
  ```
168
168
 
169
169
  However, there are other possible types of arguments:
@@ -181,7 +181,7 @@ an `xvfbwrapper.Xvfb` instance. For example:
181
181
  from xvfbwrapper import Xvfb
182
182
 
183
183
  xvfb = Xvfb(extra_args=("ttyxx", "-nocursor", "+extension", "RANDR"))
184
- xvfb.start() # Xvfb will be called with the `ttyxx -nocursor +extension RANDR` arguments
184
+ xvfb.start() # Xvfb will be called with the `ttyxx -nocursor +extension RANDR` arguments
185
185
  ```
186
186
 
187
187
  #### Multithreaded execution:
@@ -229,7 +229,6 @@ from xvfbwrapper import Xvfb
229
229
 
230
230
 
231
231
  class TestPage(unittest.TestCase):
232
-
233
232
  def setUp(self):
234
233
  xvfb = Xvfb(set_xdg_session_type=True)
235
234
  xvfb.start()
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "xvfbwrapper"
7
- version = "0.2.28"
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"]
@@ -52,9 +52,7 @@ type = [
52
52
  "types-psutil",
53
53
  ]
54
54
  validate = [
55
- "packaging",
56
- "tomli",
57
- "validate-pyproject",
55
+ "validate-pyproject[all]",
58
56
  ]
59
57
 
60
58
  [tool.setuptools]
@@ -21,9 +21,6 @@ class XvfbCleanTestCase(unittest.TestCase):
21
21
 
22
22
  - Records existing Xvfb processes at class start.
23
23
  - Kills newly spawned Xvfb processes at class end.
24
-
25
- - Records existing Xvfb processes at test start.
26
- - Kills newly spawned Xvfb processes at test end.
27
24
  """
28
25
 
29
26
  # ---------- Utility ----------
@@ -219,7 +216,8 @@ class TestXvfb(XvfbCleanTestCase):
219
216
  unwriteable_dir = "/etc"
220
217
  xvfb = Xvfb(tempdir=unwriteable_dir)
221
218
  with self.assertRaisesRegex(
222
- RuntimeError, f"Could not access writable temp directory: {unwriteable_dir}"
219
+ RuntimeError,
220
+ f"Could not access writable temp directory: {unwriteable_dir}",
223
221
  ):
224
222
  xvfb.start()
225
223
  self.assertIsNone(xvfb.proc)
@@ -228,7 +226,8 @@ class TestXvfb(XvfbCleanTestCase):
228
226
  unknown_dir = "/tmp/some_unknown_path"
229
227
  xvfb = Xvfb(tempdir=unknown_dir)
230
228
  with self.assertRaisesRegex(
231
- RuntimeError, f"Could not access writable temp directory: {unknown_dir}"
229
+ RuntimeError,
230
+ f"Could not access writable temp directory: {unknown_dir}",
232
231
  ):
233
232
  xvfb.start()
234
233
  self.assertIsNone(xvfb.proc)
@@ -269,7 +268,8 @@ class TestXvfb(XvfbCleanTestCase):
269
268
  self.assertEqual(xvfb.new_display, display_num)
270
269
  self.assertIsNotNone(xvfb.proc)
271
270
  with self.assertRaisesRegex(
272
- RuntimeError, f"Could not lock display :{display_num}"
271
+ RuntimeError,
272
+ f"Could not lock display :{display_num}",
273
273
  ):
274
274
  xvfb2.start()
275
275
 
@@ -302,7 +302,7 @@ class TestXvfb(XvfbCleanTestCase):
302
302
  self.assertIsNotNone(xvfb.proc)
303
303
 
304
304
  def test_start_fails_with_unknown_kwargs(self):
305
- xvfb = Xvfb(foo="bar", timeout=5)
305
+ xvfb = Xvfb(foo="bar", timeout=2)
306
306
  expected_cmd_args = [
307
307
  "Xvfb",
308
308
  r":\d",
@@ -313,7 +313,8 @@ class TestXvfb(XvfbCleanTestCase):
313
313
  "bar",
314
314
  ]
315
315
  with self.assertRaisesRegex(
316
- RuntimeError, f"Xvfb display did not open: {expected_cmd_args}"
316
+ RuntimeError,
317
+ f"Xvfb display did not open: {expected_cmd_args}",
317
318
  ):
318
319
  xvfb.start()
319
320
  self.assertIsNone(xvfb.proc)
@@ -348,15 +349,15 @@ class TestXvfb(XvfbCleanTestCase):
348
349
  def test_environ_keyword_isolates_environment_modification(self):
349
350
  # Check that start and stop methods modified the environ dict if
350
351
  # passed and does not modify os.environ
351
- env_duped = os.environ.copy()
352
- xvfb = Xvfb(environ=env_duped)
352
+ custom_env = os.environ.copy()
353
+ xvfb = Xvfb(environ=custom_env)
353
354
  xvfb.start()
354
355
  new_display = f":{xvfb.new_display}"
355
356
  self.assertEqual(":0", os.environ["DISPLAY"])
356
- self.assertEqual(new_display, env_duped["DISPLAY"])
357
+ self.assertEqual(new_display, custom_env["DISPLAY"])
357
358
  xvfb.stop()
358
359
  self.assertEqual(":0", os.environ["DISPLAY"])
359
- self.assertEqual(":0", env_duped["DISPLAY"])
360
+ self.assertEqual(":0", custom_env["DISPLAY"])
360
361
  self.assertIsNone(xvfb.proc)
361
362
 
362
363
  def test_environ_is_passed_to_xvfb_subprocess(self):
@@ -367,7 +368,10 @@ class TestXvfb(XvfbCleanTestCase):
367
368
  self.addCleanup(xvfb.stop)
368
369
  xvfb.start()
369
370
  proc = psutil.Process(xvfb.proc.pid)
370
- self.assertEqual(proc.environ().get("XVFBWRAPPER_TEST_MARKER"), marker_value)
371
+ self.assertEqual(
372
+ proc.environ().get("XVFBWRAPPER_TEST_MARKER"),
373
+ marker_value,
374
+ )
371
375
 
372
376
  def test_start_failure_without_initial_display_env(self):
373
377
  # Provide a custom env *without* DISPLAY so orig_display_var == None
@@ -388,7 +392,8 @@ class TestXvfb(XvfbCleanTestCase):
388
392
  with (
389
393
  patch.object(xvfb, "_local_display_exists", return_value=False),
390
394
  self.assertRaisesRegex(
391
- RuntimeError, f"Xvfb display did not open: {expected_cmd_args}"
395
+ RuntimeError,
396
+ f"Xvfb display did not open: {expected_cmd_args}",
392
397
  ),
393
398
  ):
394
399
  xvfb.start()
@@ -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.28
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
@@ -164,7 +164,7 @@ xvfb.start()
164
164
  from xvfbwrapper import Xvfb
165
165
 
166
166
  xvfb = Xvfb(display=23)
167
- xvfb.start() # Xvfb will start on display :23
167
+ xvfb.start() # Xvfb will start on display :23
168
168
  ```
169
169
 
170
170
  #### Setting XDG_SESSION_TYPE:
@@ -194,7 +194,7 @@ creating an `xvfbwrapper.Xvfb` instance. For example:
194
194
  from xvfbwrapper import Xvfb
195
195
 
196
196
  xvfb = Xvfb(nolisten="tcp")
197
- xvfb.start() # Xvfb will be called with the `-nolisten tcp` argument
197
+ xvfb.start() # Xvfb will be called with the `-nolisten tcp` argument
198
198
  ```
199
199
 
200
200
  However, there are other possible types of arguments:
@@ -212,7 +212,7 @@ an `xvfbwrapper.Xvfb` instance. For example:
212
212
  from xvfbwrapper import Xvfb
213
213
 
214
214
  xvfb = Xvfb(extra_args=("ttyxx", "-nocursor", "+extension", "RANDR"))
215
- xvfb.start() # Xvfb will be called with the `ttyxx -nocursor +extension RANDR` arguments
215
+ xvfb.start() # Xvfb will be called with the `ttyxx -nocursor +extension RANDR` arguments
216
216
  ```
217
217
 
218
218
  #### Multithreaded execution:
@@ -260,7 +260,6 @@ from xvfbwrapper import Xvfb
260
260
 
261
261
 
262
262
  class TestPage(unittest.TestCase):
263
-
264
263
  def setUp(self):
265
264
  xvfb = Xvfb(set_xdg_session_type=True)
266
265
  xvfb.start()
@@ -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