diff -pruN 0.2.9-8/.github/workflows/bandit.yml 0.2.13-2/.github/workflows/bandit.yml
--- 0.2.9-8/.github/workflows/bandit.yml	1970-01-01 00:00:00.000000000 +0000
+++ 0.2.13-2/.github/workflows/bandit.yml	2025-05-03 20:41:18.000000000 +0000
@@ -0,0 +1,52 @@
+# This workflow uses actions that are not certified by GitHub.
+# They are provided by a third-party and are governed by
+# separate terms of service, privacy policy, and support
+# documentation.
+
+# Bandit is a security linter designed to find common security issues in Python code.
+# This action will run Bandit on your codebase.
+# The results of the scan will be found under the Security tab of your repository.
+
+# https://github.com/marketplace/actions/bandit-scan is ISC licensed, by abirismyname
+# https://pypi.org/project/bandit/ is Apache v2.0 licensed, by PyCQA
+
+name: Bandit
+on:
+  push:
+    branches: [ "master" ]
+  pull_request:
+    # The branches below must be a subset of the branches above
+    branches: [ "master" ]
+  schedule:
+    - cron: '26 18 * * 0'
+
+jobs:
+  bandit:
+    permissions:
+      contents: read # for actions/checkout to fetch code
+      security-events: write # for github/codeql-action/upload-sarif to upload SARIF results
+      actions: read # only required for a private repository by github/codeql-action/upload-sarif to get the Action run status
+
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v4
+      - name: Bandit Scan
+        uses: shundor/python-bandit-scan@ab1d87dfccc5a0ffab88be3aaac6ffe35c10d6cd
+        with: # optional arguments
+          # exit with 0, even with results found
+          exit_zero: true # optional, default is DEFAULT
+          # Github token of the repository (automatically created by Github)
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information.
+          # File or directory to run bandit on
+          # path: # optional, default is .
+          # Report only issues of a given severity level or higher. Can be LOW, MEDIUM or HIGH. Default is UNDEFINED (everything)
+          # level: # optional, default is UNDEFINED
+          # Report only issues of a given confidence level or higher. Can be LOW, MEDIUM or HIGH. Default is UNDEFINED (everything)
+          # confidence: # optional, default is UNDEFINED
+          # comma-separated list of paths (glob patterns supported) to exclude from scan (note that these are in addition to the excluded paths provided in the config file) (default: .svn,CVS,.bzr,.hg,.git,__pycache__,.tox,.eggs,*.egg)
+          # excluded_paths: # optional, default is DEFAULT
+          # comma-separated list of test IDs to skip
+          # skips: # optional, default is DEFAULT
+          # path to a .bandit file that supplies command line arguments
+          # ini_path: # optional, default is DEFAULT
+
diff -pruN 0.2.9-8/.github/workflows/codeql-analysis.yml 0.2.13-2/.github/workflows/codeql-analysis.yml
--- 0.2.9-8/.github/workflows/codeql-analysis.yml	1970-01-01 00:00:00.000000000 +0000
+++ 0.2.13-2/.github/workflows/codeql-analysis.yml	2025-05-03 20:41:18.000000000 +0000
@@ -0,0 +1,41 @@
+
+name: "CodeQL"
+
+on:
+  push:
+    branches: [ master ]
+  pull_request:
+    # The branches below must be a subset of the branches above
+    branches: [ master ]
+  schedule:
+    - cron: '27 12 * * 3'
+
+jobs:
+  analyze:
+    name: Analyze
+    runs-on: ubuntu-latest
+    permissions:
+      actions: read
+      contents: read
+      security-events: write
+
+    strategy:
+      fail-fast: false
+      matrix:
+        language: [ 'python' ]
+
+    steps:
+    - name: Checkout repository
+      uses: actions/checkout@v2
+
+    # Initializes the CodeQL tools for scanning.
+    - name: Initialize CodeQL
+      uses: github/codeql-action/init@v3
+      with:
+        languages: ${{ matrix.language }}
+
+    - name: Autobuild
+      uses: github/codeql-action/autobuild@v3
+
+    - name: Perform CodeQL Analysis
+      uses: github/codeql-action/analyze@v3
diff -pruN 0.2.9-8/.github/workflows/test.yml 0.2.13-2/.github/workflows/test.yml
--- 0.2.9-8/.github/workflows/test.yml	1970-01-01 00:00:00.000000000 +0000
+++ 0.2.13-2/.github/workflows/test.yml	2025-05-03 20:41:18.000000000 +0000
@@ -0,0 +1,33 @@
+name: Test Coverage
+
+on: ["push", "pull_request"]
+
+jobs:
+  test:
+    runs-on: ubuntu-latest
+    strategy:
+      matrix:
+        # supported python versions can be found here
+        # https://github.com/actions/python-versions/releases
+        python-version: ['3.9', '3.10', '3.11', '3.12', '3.13', 'pypy-3.11']
+    steps:
+      - uses: actions/checkout@master
+      - name: set up Python ${{ matrix.python-version }}
+        uses: actions/setup-python@v2
+        with:
+          python-version: ${{ matrix.python-version }}
+      - name: Install dependencies
+        run: |
+          pip install -U wheel
+          pip install -U coverage
+          pip install .
+      - name: Run mypy
+        run: |
+            pip install -U mypy
+            mypy -p xvfbwrapper --install-types --non-interactive
+      - name: Run tests & coverage
+        run: |
+          coverage erase
+          coverage run --source=. -m unittest discover
+          coverage report -m
+
diff -pruN 0.2.9-8/.gitignore 0.2.13-2/.gitignore
--- 0.2.9-8/.gitignore	2016-12-17 21:10:23.000000000 +0000
+++ 0.2.13-2/.gitignore	2025-05-03 20:41:18.000000000 +0000
@@ -7,21 +7,20 @@ __pycache__/
 *.so
 
 # Distribution / packaging
+.eggs/
+.installed.cfg
 .Python
-env/
 build/
 develop-eggs/
 dist/
 downloads/
 eggs/
-.eggs/
 lib/
 lib64/
 parts/
 sdist/
 var/
 *.egg-info/
-.installed.cfg
 *.egg
 
 # PyInstaller
@@ -31,19 +30,23 @@ var/
 *.spec
 
 # Installer logs
-pip-log.txt
 pip-delete-this-directory.txt
+pip-log.txt
 
 # Unit test / coverage reports
-htmlcov/
-.tox/
+.cache
 .coverage
 .coverage.*
-.cache
-nosetests.xml
+.hypothesis/
+.pytest_cache/
+.tox/
 coverage.xml
+htmlcov/
+nosetests.xml
 *,cover
-.hypothesis/
+
+# Type checking
+.mypy_cache/
 
 # Translations
 *.mo
@@ -54,8 +57,8 @@ coverage.xml
 local_settings.py
 
 # Flask stuff:
-instance/
 .webassets-cache
+instance/
 
 # Scrapy stuff:
 .scrapy
@@ -79,8 +82,14 @@ celerybeat-schedule
 .env
 
 # virtualenv
+.env/
+.venv/
+.ENV/
+.VENV/
+env/
 venv/
 ENV/
+VENV/
 
 # Spyder project settings
 .spyderproject
diff -pruN 0.2.9-8/.travis.yml 0.2.13-2/.travis.yml
--- 0.2.9-8/.travis.yml	2016-12-17 21:10:23.000000000 +0000
+++ 0.2.13-2/.travis.yml	1970-01-01 00:00:00.000000000 +0000
@@ -1,12 +0,0 @@
-language: python
-python:
-  - "2.7"
-  - "3.3"
-  - "3.4"
-  - "3.5"
-  - "pypy"
-before_install:
-  - "sudo apt-get update -qq"
-  - "sudo apt-get install -y xvfb"
-script:
-  - "python -m unittest discover"
diff -pruN 0.2.9-8/LICENSE 0.2.13-2/LICENSE
--- 0.2.9-8/LICENSE	2016-12-17 21:10:23.000000000 +0000
+++ 0.2.13-2/LICENSE	2025-05-03 20:41:18.000000000 +0000
@@ -1,5 +1,6 @@
-xvfbwrapper - Copyright (c) 2012-2016 Corey Goldberg
+MIT License
 
+Copyright (c) 2012-2025 Corey Goldberg
 
 Permission is hereby granted, free of charge, to any person obtaining a copy
 of this software and associated documentation files (the "Software"), to deal
@@ -8,13 +9,13 @@ to use, copy, modify, merge, publish, di
 copies of the Software, and to permit persons to whom the Software is
 furnished to do so, subject to the following conditions:
 
-The above copyright notice and this permission notice shall be included in
-all copies or substantial portions of the Software.
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
 
 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-THE SOFTWARE.
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff -pruN 0.2.9-8/MANIFEST.in 0.2.13-2/MANIFEST.in
--- 0.2.9-8/MANIFEST.in	2016-12-17 21:10:23.000000000 +0000
+++ 0.2.13-2/MANIFEST.in	2025-05-03 20:41:18.000000000 +0000
@@ -1,4 +1,2 @@
-prune *
-include README.rst
-include LICENSE
-include *.py
+include test_xvfb.py
+include tox.ini
diff -pruN 0.2.9-8/README.md 0.2.13-2/README.md
--- 0.2.9-8/README.md	1970-01-01 00:00:00.000000000 +0000
+++ 0.2.13-2/README.md	2025-05-03 20:41:18.000000000 +0000
@@ -0,0 +1,207 @@
+# xvfbwrapper
+
+#### Manage headless displays with Xvfb (X virtual framebuffer)
+
+- Copyright (c) 2012-2025 [Corey Goldberg][github-home]
+- Development: [GitHub][github-repo]
+- Releases: [PyPI][pypi]
+- License: [MIT][mit-license]
+
+[github-home]: https://github.com/cgoldberg
+[github-repo]: https://github.com/cgoldberg/xvfbwrapper
+[pypi]: https://pypi.org/project/xvfbwrapper
+[mit-license]: https://raw.githubusercontent.com/cgoldberg/xvfbwrapper/refs/heads/master/LICENSE
+
+----
+
+## About
+
+`xvfbwrapper` is a python module for controlling X11 virtual displays with Xvfb.
+
+----
+
+## What is Xvfb?
+
+
+`Xvfb` (X virtual framebuffer) is a display server implementing the X11
+display server protocol. It runs in memory and does not require a physical
+display or input devices. Only a network layer is necessary.
+
+`Xvfb` is useful for programs that run on a headless servers, but require X Windows.
+
+----
+
+## Installation
+
+```
+pip install xvfbwrapper
+```
+
+----
+
+## System Requirements
+
+- Python 3.9+
+- X Window System
+- Xvfb (`sudo apt-get install xvfb`, `yum install xorg-x11-server-Xvfb`, etc)
+- File locking with `fcntl`
+
+----
+
+## Examples
+
+#### Basic Usage:
+
+```python
+from xvfbwrapper import Xvfb
+
+xvfb = Xvfb()
+xvfb.start()
+try:
+    # launch stuff inside virtual display here
+finally:
+    # always either wrap your usage of Xvfb() with try/finally, or
+    # alternatively use Xvfb() as a context manager. If you don't,
+    # you'll probably end up with a bunch of junk in /tmp
+    xvfb.stop()
+```
+
+#### Specifying display geometry:
+
+```python
+from xvfbwrapper import Xvfb
+
+xvfb = Xvfb(width=1280, height=740)
+xvfb.start()
+try:
+    # launch stuff inside virtual display here
+finally:
+    xvfb.stop()
+```
+
+#### Specifying display number:
+
+```python
+from xvfbwrapper import Xvfb
+
+xvfb = Xvfb(display=23)
+xvfb.start()
+# Xvfb is started with display :23
+# see vdisplay.new_display
+try:
+    # launch stuff inside virtual display here
+finally:
+    xvfb.stop()
+```
+
+#### Usage as a context manager:
+
+```python
+from xvfbwrapper import Xvfb
+
+with Xvfb() as xvfb:
+    # launch stuff inside virtual display here
+    # Xvfb will stop when this block completes
+```
+
+#### Multithreaded execution:
+
+To run several Xvfb displays at the same time, you can use the `environ`
+keyword when starting the `Xvfb` instances. This provides isolation between
+threads. Be sure to use the environment dictionary you initialize Xvfb with
+in your subsequent calls. Also, if you wish to inherit your current
+environment, you must use the copy method of `os.environ` and not simply
+assign a new variable to `os.environ`:
+
+```python
+import os
+
+from xvfbwrapper import Xvfb
+
+isolated_environment1 = os.environ.copy()
+xvfb1 = Xvfb(environ=isolated_environment1)
+xvfb1.start()
+
+isolated_environment2 = os.environ.copy()
+xvfb2 = Xvfb(environ=isolated_environment2)
+xvfb2.start()
+
+try:
+    # launch stuff inside virtual displays here
+finally:
+    xvfb1.stop()
+    xvfb2.stop()
+```
+
+#### Usage in testing - headless Selenium WebDriver tests:
+
+This is a test using `selenium` and `xvfbwrapper` to run tests
+on Chrome with a headless display. (see: [selenium docs][selenium-docs])
+
+[selenium-docs]: https://www.selenium.dev/selenium/docs/api/py
+
+```python
+import os
+import unittest
+
+from selenium import webdriver
+from xvfbwrapper import Xvfb
+
+# force X11 in case we are running on a Wayland system
+os.environ["XDG_SESSION_TYPE"] = "x11"
+
+
+class TestPages(unittest.TestCase):
+
+    def setUp(self):
+        xvfb = Xvfb()
+        self.addCleanup(xvfb.stop)
+        xvfb.start()
+        self.driver = webdriver.Chrome()
+        self.addCleanup(self.driver.quit)
+
+    def test_selenium_homepage(self):
+        self.driver.get("https://www.selenium.dev")
+        self.assertIn("Selenium", self.driver.title)
+
+
+if __name__ == "__main__":
+    unittest.main()
+```
+
+- virtual display is launched
+- browser launches inside virtual display (headless)
+- browser quits during cleanup
+- virtual display stops during cleanup
+
+----
+
+## xvfbwrapper Development
+
+Clone the repo:
+
+```
+git clone https://github.com/cgoldberg/xvfbwrapper.git
+cd xvfbwrapper
+```
+
+Create a virtual env and install required testing packages:
+
+```
+python -m venv venv
+source ./venv/bin/activate
+pip install -r requirements_test.txt
+```
+
+Run all unit tests in the default Python environment:
+
+```
+pytest
+```
+
+Run all unit tests, linting, and type checking across all supported/installed
+Python environments:
+
+```
+tox
+```
diff -pruN 0.2.9-8/README.rst 0.2.13-2/README.rst
--- 0.2.9-8/README.rst	2016-12-17 21:10:23.000000000 +0000
+++ 0.2.13-2/README.rst	1970-01-01 00:00:00.000000000 +0000
@@ -1,159 +0,0 @@
-===============
-    xvfbwrapper
-===============
-
-
-**Manage headless displays with Xvfb (X virtual framebuffer)**
-
-.. image:: https://travis-ci.org/cgoldberg/xvfbwrapper.svg?branch=master
-    :target: https://travis-ci.org/cgoldberg/xvfbwrapper
-
-----
-
----------
-    Info:
----------
-
-- Dev: https://github.com/cgoldberg/xvfbwrapper
-- Releases: https://pypi.python.org/pypi/xvfbwrapper
-- Author: `Corey Goldberg <https://github.com/cgoldberg>`_ - 2012-2016
-- License: MIT
-
-----
-
-----------------------
-    About xvfbwrapper:
-----------------------
-
-xvfbwrapper is a python wrapper for controlling Xvfb.
-
-----
-
----------------
-    About Xvfb:
----------------
-
-Xvfb (X virtual framebuffer) is a display server implementing the X11 display server protocol. It runs in memory and does not require a physical display.  Only a network layer is necessary.
-
-Xvfb is especially useful for running acceptance tests on headless servers.
-
-----
-
-
-----------------------------------
-    Install xvfbwrapper from PyPI:
-----------------------------------
-
-  ``pip install xvfbwrapper``
-
-----
-
-------------------------
-    System Requirements:
-------------------------
-
-* Xvfb (``sudo apt-get install xvfb``, or similar)
-* Python 2.7 or 3.3+
-
-----
-
-++++++++++++
-    Examples
-++++++++++++
-
-****************
-    Basic Usage:
-****************
-
-::
-
-    from xvfbwrapper import Xvfb
-
-    vdisplay = Xvfb()
-    vdisplay.start()
-
-    # launch stuff inside
-    # virtual display here.
-
-    vdisplay.stop()
-
-----
-
-*********************************************
-    Basic Usage, specifying display geometry:
-*********************************************
-
-::
-
-    from xvfbwrapper import Xvfb
-
-    vdisplay = Xvfb(width=1280, height=740, colordepth=16)
-    vdisplay.start()
-
-    # launch stuff inside
-    # virtual display here.
-
-    vdisplay.stop()
-
-----
-
-*******************************
-    Usage as a Context Manager:
-*******************************
-
-::
-
-    from xvfbwrapper import Xvfb
-
-    with Xvfb() as xvfb:
-        # launch stuff inside virtual display here.
-        # It starts/stops around this code block.
-
-----
-
-*******************************************************
-    Testing Example: Headless Selenium WebDriver Tests:
-*******************************************************
-
-::
-
-    import unittest
-
-    from selenium import webdriver
-    from xvfbwrapper import Xvfb
-
-
-    class TestPages(unittest.TestCase):
-
-        def setUp(self):
-            self.xvfb = Xvfb(width=1280, height=720)
-            self.addCleanup(self.xvfb.stop)
-            self.xvfb.start()
-            self.browser = webdriver.Firefox()
-            self.addCleanup(self.browser.quit)
-
-        def testUbuntuHomepage(self):
-            self.browser.get('http://www.ubuntu.com')
-            self.assertIn('Ubuntu', self.browser.title)
-
-        def testGoogleHomepage(self):
-            self.browser.get('http://www.google.com')
-            self.assertIn('Google', self.browser.title)
-
-
-    if __name__ == '__main__':
-        unittest.main()
-
-
-The test class above uses `selenium` and `xvfbwrapper` to run each test case with Firefox inside a headless display.
-
-* virtual display is launched
-* Firefox launches inside virtual display (headless)
-* browser is not shown while tests are run
-* conditions are asserted in each test case
-* browser quits during cleanup
-* virtual display stops during cleanup
-
-*Look Ma', no browser!*
-
-(You can also take screenshots inside the virtual display for diagnosing test failures)
diff -pruN 0.2.9-8/debian/changelog 0.2.13-2/debian/changelog
--- 0.2.9-8/debian/changelog	2025-01-02 14:35:10.000000000 +0000
+++ 0.2.13-2/debian/changelog	2025-09-28 13:28:06.000000000 +0000
@@ -1,3 +1,16 @@
+python-xvfbwrapper (0.2.13-2) unstable; urgency=medium
+
+  * Uploading to unstable.
+
+ -- Thomas Goirand <zigo@debian.org>  Sun, 28 Sep 2025 15:28:06 +0200
+
+python-xvfbwrapper (0.2.13-1) experimental; urgency=medium
+
+  * New upstream release.
+  * Add pybuild-plugin-pyproject and use pybuild.
+
+ -- Thomas Goirand <zigo@debian.org>  Wed, 27 Aug 2025 16:21:19 +0200
+
 python-xvfbwrapper (0.2.9-8) unstable; urgency=medium
 
   * Removed Gonéri Le Bouder from uploaders (Closes: #1087035).
diff -pruN 0.2.9-8/debian/control 0.2.13-2/debian/control
--- 0.2.9-8/debian/control	2025-01-02 14:35:10.000000000 +0000
+++ 0.2.13-2/debian/control	2025-09-28 13:28:06.000000000 +0000
@@ -8,6 +8,7 @@ Build-Depends:
  debhelper-compat (= 11),
  dh-python,
  openstack-pkg-tools,
+ pybuild-plugin-pyproject,
  python3-all,
  python3-setuptools,
 Standards-Version: 4.4.1
diff -pruN 0.2.9-8/debian/python3-xvfbwrapper.install 0.2.13-2/debian/python3-xvfbwrapper.install
--- 0.2.9-8/debian/python3-xvfbwrapper.install	2025-01-02 14:35:10.000000000 +0000
+++ 0.2.13-2/debian/python3-xvfbwrapper.install	1970-01-01 00:00:00.000000000 +0000
@@ -1 +0,0 @@
-/usr/*
diff -pruN 0.2.9-8/debian/rules 0.2.13-2/debian/rules
--- 0.2.9-8/debian/rules	2025-01-02 14:35:10.000000000 +0000
+++ 0.2.13-2/debian/rules	2025-09-28 13:28:06.000000000 +0000
@@ -10,21 +10,3 @@ override_dh_auto_clean:
 	rm -rf build *.egg-info
 	find . -iname '*.pyc' -delete
 	for i in $$(find . -type d -iname __pycache__) ; do rm -rf $$i ; done
-
-override_dh_auto_build:
-	echo "Do nothing"
-
-override_dh_auto_test:
-	echo "Do nothing"
-
-override_dh_auto_install:
-	pkgos-dh_auto_install --no-py2 --in-tmp
-
-ifeq (,$(findstring nocheck, $(DEB_BUILD_OPTIONS)))
-	echo "Disabled tests, as they fail without X running"
-#	nosetests
-#	nosetests3
-#	set -e && for pyvers in $(PYTHONS) $(PYTHON3S); do \
-#		python$$pyvers setup.py test ; \
-#	done
-endif
diff -pruN 0.2.9-8/pyproject.toml 0.2.13-2/pyproject.toml
--- 0.2.9-8/pyproject.toml	1970-01-01 00:00:00.000000000 +0000
+++ 0.2.13-2/pyproject.toml	2025-05-03 20:41:18.000000000 +0000
@@ -0,0 +1,57 @@
+[build-system]
+requires = ["setuptools"]
+build-backend = "setuptools.build_meta"
+
+[project]
+name = "xvfbwrapper"
+version = "0.2.13"
+description = "Manage headless displays with Xvfb (X virtual framebuffer)"
+license = "MIT"
+license-files = ["LICENSE"]
+authors = [{name = "Corey Goldberg"}]
+maintainers = [{name = "Corey Goldberg"}]
+readme = "README.md"
+requires-python = ">= 3.9"
+keywords = ["Xvfb", "headless", "display", "X11", "X Window System"]
+classifiers = [
+        "Environment :: Console",
+        "Environment :: X11 Applications",
+        "Intended Audience :: Developers",
+        "Intended Audience :: End Users/Desktop",
+        "Intended Audience :: Information Technology",
+        "Operating System :: POSIX",
+        "Operating System :: POSIX :: Linux",
+        "Operating System :: Unix",
+        "Programming Language :: Python",
+        "Programming Language :: Python :: 3",
+        "Programming Language :: Python :: 3.9",
+        "Programming Language :: Python :: 3.10",
+        "Programming Language :: Python :: 3.11",
+        "Programming Language :: Python :: 3.12",
+        "Programming Language :: Python :: 3.13",
+        "Topic :: Software Development :: Libraries :: Python Modules",
+]
+
+[project.urls]
+homepage = "https://github.com/cgoldberg/xvfbwrapper"
+source = "https://github.com/cgoldberg/xvfbwrapper"
+download = "https://pypi.org/project/xvfbwrapper"
+
+[tool.setuptools]
+py-modules = ["xvfbwrapper"]
+
+[tool.black]
+line-length = 88
+target-version = ["py39"]
+
+[tool.isort]
+profile = "black"
+py_version = 39
+
+[tool.autoflake]
+in-place = true
+max-line-length = ["88"]
+min-python-version = ["3.9"]
+remove-all-unused-imports = true
+remove-duplicate-keys = true
+remove-unused-variables = true
diff -pruN 0.2.9-8/requirements_test.txt 0.2.13-2/requirements_test.txt
--- 0.2.9-8/requirements_test.txt	1970-01-01 00:00:00.000000000 +0000
+++ 0.2.13-2/requirements_test.txt	2025-05-03 20:41:18.000000000 +0000
@@ -0,0 +1,2 @@
+pytest
+tox
diff -pruN 0.2.9-8/setup.py 0.2.13-2/setup.py
--- 0.2.9-8/setup.py	2016-12-17 21:10:23.000000000 +0000
+++ 0.2.13-2/setup.py	1970-01-01 00:00:00.000000000 +0000
@@ -1,52 +0,0 @@
-#!/usr/bin/env python
-
-
-"""distutils setup/install script for xvfbwrapper"""
-
-
-import os
-
-try:
-    from setuptools import setup
-except ImportError:
-    from distutils.core import setup
-
-
-this_dir = os.path.abspath(os.path.dirname(__file__))
-with open(os.path.join(this_dir, 'README.rst')) as f:
-    LONG_DESCRIPTION = '\n' + f.read()
-
-tests_require = []
-try:
-    from unittest import mock  # noqa
-except ImportError:
-    tests_require.append('mock')
-
-setup(
-    name='xvfbwrapper',
-    version='0.2.9',
-    py_modules=['xvfbwrapper'],
-    author='Corey Goldberg',
-    author_email='cgoldberg _at_ gmail.com',
-    description='run headless display inside X virtual framebuffer (Xvfb)',
-    long_description=LONG_DESCRIPTION,
-    url='https://github.com/cgoldberg/xvfbwrapper',
-    download_url='http://pypi.python.org/pypi/xvfbwrapper',
-    tests_require=tests_require,
-    keywords='xvfb virtual display headless x11'.split(),
-    license='MIT',
-    classifiers=[
-        'Operating System :: Unix',
-        'Operating System :: POSIX :: Linux',
-        'Intended Audience :: Developers',
-        'License :: OSI Approved :: MIT License',
-        'Programming Language :: Python',
-        'Programming Language :: Python :: 2',
-        'Programming Language :: Python :: 2.7',
-        'Programming Language :: Python :: 3',
-        'Programming Language :: Python :: 3.3',
-        'Programming Language :: Python :: 3.4',
-        'Programming Language :: Python :: 3.5',
-        'Topic :: Software Development :: Libraries :: Python Modules',
-    ]
-)
diff -pruN 0.2.9-8/test_xvfb.py 0.2.13-2/test_xvfb.py
--- 0.2.9-8/test_xvfb.py	2016-12-17 21:10:23.000000000 +0000
+++ 0.2.13-2/test_xvfb.py	2025-05-03 20:41:18.000000000 +0000
@@ -1,26 +1,29 @@
-#!/usr/bin/env python
+#!/usr/bin/env python3
 
 import os
 import sys
 import unittest
-try:
-    from unittest.mock import patch
-except ImportError:
-    from mock import patch
+from unittest.mock import patch
 
 from xvfbwrapper import Xvfb
 
+# Force X11 in case we are running on a Wayland system
+os.environ["XDG_SESSION_TYPE"] = "x11"
 
-class TestXvfb(unittest.TestCase):
 
-    def reset_display(self):
-        os.environ['DISPLAY'] = ':0'
+# Using mock.patch as a class decorator applies it to every
+# test_* method and removes it after test completes.
+@patch.dict("os.environ", {"DISPLAY": ":0"})
+class TestXvfb(unittest.TestCase):
 
     def setUp(self):
-        self.reset_display()
+        pass
 
-    def test_xvfb_binary_not_exists(self):
-        with patch('xvfbwrapper.Xvfb.xvfb_exists') as xvfb_exists:
+    def tearDown(self):
+        pass
+
+    def test_xvfb_binary_does_not_exist(self):
+        with patch("xvfbwrapper.Xvfb._xvfb_exists") as xvfb_exists:
             xvfb_exists.return_value = False
             with self.assertRaises(EnvironmentError):
                 Xvfb()
@@ -29,38 +32,72 @@ class TestXvfb(unittest.TestCase):
         xvfb = Xvfb()
         self.addCleanup(xvfb.stop)
         xvfb.start()
-        display_var = ':{}'.format(xvfb.new_display)
-        self.assertEqual(display_var, os.environ['DISPLAY'])
+        display_var = f":{xvfb.new_display}"
+        self.assertEqual(display_var, os.environ["DISPLAY"])
         self.assertIsNotNone(xvfb.proc)
 
     def test_stop(self):
-        orig_display = os.environ['DISPLAY']
+        orig_display = os.environ["DISPLAY"]
         xvfb = Xvfb()
         xvfb.start()
-        self.assertNotEqual(orig_display, os.environ['DISPLAY'])
+        self.assertNotEqual(orig_display, os.environ["DISPLAY"])
         xvfb.stop()
-        self.assertEqual(orig_display, os.environ['DISPLAY'])
+        self.assertEqual(orig_display, os.environ["DISPLAY"])
         self.assertIsNone(xvfb.proc)
 
-    def test_start_without_existing_display(self):
-        del os.environ['DISPLAY']
-        xvfb = Xvfb()
-        self.addCleanup(xvfb.stop)
-        self.addCleanup(self.reset_display)
-        xvfb.start()
-        display_var = ':{}'.format(xvfb.new_display)
-        self.assertEqual(display_var, os.environ['DISPLAY'])
-        self.assertIsNotNone(xvfb.proc)
+    def test_stop_with_xquartz(self):
+        # Check that xquartz pattern for display server is dealt with by
+        # xvfb.stop() and restored appropriately
+        xquartz_display = (
+            "/private/tmp/com.apple.launchd.CgDzCWvNb1/org.macosforge.xquartz:0"
+        )
+        with patch.dict("os.environ", {"DISPLAY": xquartz_display}):
+            xvfb = Xvfb()
+            xvfb.start()
+            self.assertNotEqual(xquartz_display, os.environ["DISPLAY"])
+            xvfb.stop()
+            self.assertEqual(xquartz_display, os.environ["DISPLAY"])
+        self.assertIsNone(xvfb.proc)
 
-    def test_as_context_manager(self):
-        orig_display = os.environ['DISPLAY']
+    def test_start_and_stop_as_context_manager(self):
+        orig_display = os.environ["DISPLAY"]
         with Xvfb() as xvfb:
-            display_var = ':{}'.format(xvfb.new_display)
-            self.assertEqual(display_var, os.environ['DISPLAY'])
+            display_var = f":{xvfb.new_display}"
+            self.assertEqual(display_var, os.environ["DISPLAY"])
             self.assertIsNotNone(xvfb.proc)
-        self.assertEqual(orig_display, os.environ['DISPLAY'])
+        self.assertEqual(orig_display, os.environ["DISPLAY"])
         self.assertIsNone(xvfb.proc)
 
+    def test_start_without_existing_display(self):
+        with patch.dict("os.environ", {}):
+            del os.environ["DISPLAY"]
+            xvfb = Xvfb()
+            self.addCleanup(xvfb.stop)
+            xvfb.start()
+            display_var = f":{xvfb.new_display}"
+            self.assertEqual(display_var, os.environ["DISPLAY"])
+        self.assertIsNotNone(xvfb.proc)
+
+    def test_start_with_empty_display(self):
+        with patch.dict("os.environ", {}):
+            os.environ["DISPLAY"] = ""
+            xvfb = Xvfb()
+            self.addCleanup(xvfb.stop)
+            xvfb.start()
+            display_var = f":{xvfb.new_display}"
+            self.assertEqual(display_var, os.environ["DISPLAY"])
+        self.assertIsNotNone(xvfb.proc)
+
+    def test_start_with_specific_display(self):
+        xvfb = Xvfb(display=42)
+        xvfb2 = Xvfb(display=42)
+        self.addCleanup(xvfb.stop)
+        xvfb.start()
+        self.assertEqual(xvfb.new_display, 42)
+        self.assertIsNotNone(xvfb.proc)
+        with self.assertRaises(ValueError):
+            xvfb2.start()
+
     def test_start_with_kwargs(self):
         w = 800
         h = 600
@@ -71,20 +108,20 @@ class TestXvfb(unittest.TestCase):
         self.assertEqual(w, xvfb.width)
         self.assertEqual(h, xvfb.height)
         self.assertEqual(depth, xvfb.colordepth)
-        display_var = ':{}'.format(xvfb.new_display)
-        self.assertEqual(display_var, os.environ['DISPLAY'])
+        display_var = f":{xvfb.new_display}"
+        self.assertEqual(display_var, os.environ["DISPLAY"])
         self.assertIsNotNone(xvfb.proc)
 
     def test_start_with_arbitrary_kwargs(self):
-        xvfb = Xvfb(nolisten='tcp')
+        xvfb = Xvfb(nolisten="tcp")
         self.addCleanup(xvfb.stop)
         xvfb.start()
-        display_var = ':{}'.format(xvfb.new_display)
-        self.assertEqual(display_var, os.environ['DISPLAY'])
+        display_var = f":{xvfb.new_display}"
+        self.assertEqual(display_var, os.environ["DISPLAY"])
         self.assertIsNotNone(xvfb.proc)
 
     def test_start_fails_with_unknown_kwargs(self):
-        xvfb = Xvfb(foo='bar')
+        xvfb = Xvfb(foo="bar")
         with self.assertRaises(RuntimeError):
             xvfb.start()
 
@@ -96,11 +133,14 @@ class TestXvfb(unittest.TestCase):
         self.addCleanup(xvfb2._cleanup_lock_file)
         self.addCleanup(xvfb3._cleanup_lock_file)
         side_effect = [11, 11, 22, 11, 22, 11, 22, 22, 22, 33]
-        with patch('xvfbwrapper.randint',
-                   side_effect=side_effect) as mockrandint:
+        with patch("xvfbwrapper.randint", side_effect=side_effect) as mockrandint:
             self.assertEqual(xvfb._get_next_unused_display(), 11)
             self.assertEqual(mockrandint.call_count, 1)
-            if sys.version_info >= (3, 2):
+            if sys.implementation.name == "cpython":
+                # ResourceWarning is only raised on CPython because
+                # of an implementation detail in it's garbage collector.
+                # This does not occur on other Python implementations
+                # (like PyPy).
                 with self.assertWarns(ResourceWarning):
                     self.assertEqual(xvfb2._get_next_unused_display(), 22)
                     self.assertEqual(mockrandint.call_count, 3)
@@ -111,3 +151,36 @@ class TestXvfb(unittest.TestCase):
                 self.assertEqual(mockrandint.call_count, 3)
                 self.assertEqual(xvfb3._get_next_unused_display(), 33)
                 self.assertEqual(mockrandint.call_count, 10)
+
+    def test_environ_keyword_isolates_environment_modification(self):
+        # Check that start and stop methods modified the environ dict if
+        # passed and does not modify os.environ
+        env_duped = os.environ.copy()
+        xvfb = Xvfb(environ=env_duped)
+        xvfb.start()
+        new_display = f":{xvfb.new_display}"
+        self.assertEqual(":0", os.environ["DISPLAY"])
+        self.assertEqual(new_display, env_duped["DISPLAY"])
+        xvfb.stop()
+        self.assertEqual(":0", os.environ["DISPLAY"])
+        self.assertEqual(":0", env_duped["DISPLAY"])
+        self.assertIsNone(xvfb.proc)
+
+    def test_start_failure_without_initial_display_env(self):
+        # Provide a custom env *without* DISPLAY so orig_display_var == None
+        custom_env = {"PATH": os.environ.get("PATH", "")}
+        xvfb = Xvfb(timeout=0.5, environ=custom_env)
+        # Ensure any spawned proc is cleaned up
+        self.addCleanup(lambda: xvfb.proc and xvfb.proc.terminate())
+        # Force the display socket to never appear
+        with patch.object(xvfb, "_local_display_exists", return_value=False):
+            # On old code this will KeyError *inside* stop()
+            # On fixed code this raises RuntimeError cleanly
+            with self.assertRaises(RuntimeError):
+                xvfb.start()
+        # After failure, calling stop() again must not raise an exception
+        xvfb.stop()
+        # We never injected DISPLAY into our custom env
+        self.assertNotIn("DISPLAY", custom_env)
+        # There should be no lingering proc
+        self.assertIsNone(xvfb.proc)
diff -pruN 0.2.9-8/tox.ini 0.2.13-2/tox.ini
--- 0.2.9-8/tox.ini	2016-12-17 21:10:23.000000000 +0000
+++ 0.2.13-2/tox.ini	2025-05-03 20:41:18.000000000 +0000
@@ -1,17 +1,64 @@
-# Tox (http://tox.testrun.org/) is a tool for running tests
-# in multiple virtualenvs. This configuration file will run the
-# test suite on all supported python versions. To use it, "pip install tox"
-# and then run "tox" from this directory.
+# Tox (https://tox.wiki/) is a tool for running tests in multiple
+# virtualenvs. This configuration file will run the test suite on all
+# supported python versions. To use it, run "tox" from this directory.
+#
+# For a specific environment, run:
+#     "tox -e <env>" (i.e.: "tox -e py313" or "tox -e lint")
+#
+# This tox configuration will skip any Python interpreters that can't be found.
+# To manage multiple Python interpreters for covering all versions, you can use
+# pyenv: https://github.com/pyenv/pyenv
 
 [tox]
-envlist=flake8,py27,py33,py34,py35,pypy
+env_list =
+    lint
+    type
+    validate-pyproject
+    py39
+    py310
+    py311
+    py312
+    py313
+    pypy3
+skip_missing_interpreters = True
 
 [testenv]
-commands={envpython} -m unittest discover
-deps=
-    py27,pypy: mock
+description = run unit tests
+deps =
+    pytest
+commands =
+    # "-vv" means extra verbose
+    # "-r fEsxXp" means show extra test summary info as specified by:
+    #   (f)ailed, (E)rror, (s)kipped, (x)failed, (X)passed, (p)assed
+    pytest -vv -r fEsxXp {posargs:.}
 
-[testenv:flake8]
-basepython=python3
-deps=flake8
-commands=flake8
+[testenv:validate-pyproject]
+description = validate project configuration
+skip_install = true
+deps =
+    packaging
+    validate-pyproject
+commands =
+    validate-pyproject pyproject.toml
+
+[testenv:lint]
+description = run linters
+deps =
+    autoflake
+    black
+    flake8
+    isort
+skip_install = True
+commands =
+    black .
+    isort .
+    autoflake .
+    flake8 --max-line-length=88 --extend-exclude={env:VIRTUAL_ENV} .
+
+[testenv:type]
+description = run type checks
+deps =
+    mypy
+skip_install = True
+commands =
+    mypy --install-types --non-interactive .
diff -pruN 0.2.9-8/xvfbwrapper.py 0.2.13-2/xvfbwrapper.py
--- 0.2.9-8/xvfbwrapper.py	2016-12-17 21:10:23.000000000 +0000
+++ 0.2.13-2/xvfbwrapper.py	2025-05-03 20:41:18.000000000 +0000
@@ -1,101 +1,128 @@
-#!/usr/bin/env python
-#
-#   * Corey Goldberg, 2012, 2013, 2015, 2016
-#
-#   * inspired by: PyVirtualDisplay
+#!/usr/bin/env python3
+# Corey Goldberg, 2012-2025
+# License: MIT
 
-
-"""wrapper for running display inside X virtual framebuffer (Xvfb)"""
+"""Run a headless display inside X virtual framebuffer (Xvfb)"""
 
 
 import os
+import platform
+import shutil
 import subprocess
 import tempfile
 import time
-import fcntl
-from random import randint
 
 try:
-    BlockingIOError
-except NameError:
-    # python 2
-    BlockingIOError = IOError
+    import fcntl
+except ImportError:
+    system = platform.system()
+    raise OSError(f"xvfbwrapper is not supported on this platform: {system}")
 
+from random import randint
 
-class Xvfb(object):
+
+class Xvfb:
 
     # Maximum value to use for a display. 32-bit maxint is the
     # highest Xvfb currently supports
     MAX_DISPLAY = 2147483647
-    SLEEP_TIME_BEFORE_START = 0.1
 
-    def __init__(self, width=800, height=680, colordepth=24, tempdir=None,
-                 **kwargs):
+    def __init__(
+        self,
+        width=800,
+        height=680,
+        colordepth=24,
+        tempdir=None,
+        display=None,
+        environ=None,
+        timeout=10,
+        **kwargs,
+    ):
         self.width = width
         self.height = height
         self.colordepth = colordepth
         self._tempdir = tempdir or tempfile.gettempdir()
+        self._timeout = timeout
+        self.new_display = display
+
+        self.environ = environ if environ else os.environ
 
-        if not self.xvfb_exists():
-            msg = 'Can not find Xvfb. Please install it and try again.'
-            raise EnvironmentError(msg)
+        if not self._xvfb_exists():
+            raise OSError("Can't find Xvfb. Please install it and try again")
 
-        self.extra_xvfb_args = ['-screen', '0', '{}x{}x{}'.format(
-                                self.width, self.height, self.colordepth)]
+        self.xvfb_cmd = []
+        self.extra_xvfb_args = [
+            "-screen",
+            "0",
+            f"{self.width}x{self.height}x{self.colordepth}",
+        ]
 
         for key, value in kwargs.items():
-            self.extra_xvfb_args += ['-{}'.format(key), value]
+            self.extra_xvfb_args += [f"-{key}", value]
 
-        if 'DISPLAY' in os.environ:
-            self.orig_display = os.environ['DISPLAY'].split(':')[1]
+        if "DISPLAY" in self.environ:
+            self.orig_display_var = self.environ["DISPLAY"]
         else:
-            self.orig_display = None
+            self.orig_display_var = None
 
         self.proc = None
 
-    def __enter__(self):
+    def __enter__(self) -> "Xvfb":
         self.start()
         return self
 
     def __exit__(self, exc_type, exc_val, exc_tb):
         self.stop()
 
-    def start(self):
-        self.new_display = self._get_next_unused_display()
-        display_var = ':{}'.format(self.new_display)
-        self.xvfb_cmd = ['Xvfb', display_var] + self.extra_xvfb_args
-        with open(os.devnull, 'w') as fnull:
-            self.proc = subprocess.Popen(self.xvfb_cmd,
-                                         stdout=fnull,
-                                         stderr=fnull,
-                                         close_fds=True)
-        # give Xvfb time to start
-        time.sleep(self.__class__.SLEEP_TIME_BEFORE_START)
+    def start(self) -> None:
+        if self.new_display is not None:
+            if not self._get_lock_for_display(self.new_display):
+                raise ValueError(f"Could not lock display :{self.new_display}")
+        else:
+            self.new_display = self._get_next_unused_display()
+        display_var = f":{self.new_display}"
+        self.xvfb_cmd = ["Xvfb", display_var] + self.extra_xvfb_args
+        self.proc = subprocess.Popen(
+            self.xvfb_cmd,
+            stdout=subprocess.DEVNULL,
+            stderr=subprocess.DEVNULL,
+            close_fds=True,
+        )
+        start = time.time()
+        while not self._local_display_exists(self.new_display):
+            time.sleep(1e-3)
+            if time.time() - start > self._timeout:
+                self.stop()
+                raise RuntimeError(f"Xvfb display did not open: {self.xvfb_cmd}")
         ret_code = self.proc.poll()
         if ret_code is None:
-            self._set_display_var(self.new_display)
+            self._set_display(display_var)
         else:
             self._cleanup_lock_file()
-            raise RuntimeError('Xvfb did not start')
+            raise RuntimeError(f"Xvfb did not start ({ret_code}): {self.xvfb_cmd}")
 
-    def stop(self):
+    def stop(self) -> None:
         try:
-            if self.orig_display is None:
-                del os.environ['DISPLAY']
+            if self.orig_display_var is None:
+                self.environ.pop("DISPLAY", None)
             else:
-                self._set_display_var(self.orig_display)
+                self._set_display(self.orig_display_var)
             if self.proc is not None:
                 try:
                     self.proc.terminate()
-                    self.proc.wait()
+                    self.proc.wait(self._timeout)
                 except OSError:
                     pass
                 self.proc = None
         finally:
             self._cleanup_lock_file()
 
+    def _xvfb_exists(self) -> bool:
+        """Check that Xvfb is available on PATH and is executable."""
+        return True if shutil.which("Xvfb") is not None else False
+
     def _cleanup_lock_file(self):
-        '''
+        """
         This should always get called if the process exits safely
         with Xvfb.stop() (whether called explicitly, or by __exit__).
 
@@ -104,36 +131,48 @@ class Xvfb(object):
         Xvfb.stop() in a finally block, or use Xvfb as a context manager
         to ensure lock files are purged.
 
-        '''
+        """
         self._lock_display_file.close()
         try:
             os.remove(self._lock_display_file.name)
         except OSError:
             pass
 
-    def _get_next_unused_display(self):
-        '''
+    def _get_lock_for_display(self, display) -> bool:
+        """
         In order to ensure multi-process safety, this method attempts
         to acquire an exclusive lock on a temporary file whose name
         contains the display number for Xvfb.
-        '''
-        tempfile_path = os.path.join(self._tempdir, '.X{0}-lock')
-        while True:
-            rand = randint(1, self.__class__.MAX_DISPLAY)
-            self._lock_display_file = open(tempfile_path.format(rand), 'w')
+        """
+        tempfile_path = os.path.join(self._tempdir, f".X{display}-lock")
+        try:
+            self._lock_display_file = open(tempfile_path, "w")
+        except PermissionError:
+            return False
+        else:
             try:
-                fcntl.flock(self._lock_display_file,
-                            fcntl.LOCK_EX | fcntl.LOCK_NB)
+                fcntl.flock(self._lock_display_file, fcntl.LOCK_EX | fcntl.LOCK_NB)
             except BlockingIOError:
-                continue
+                return False
             else:
+                return True
+
+    def _get_next_unused_display(self) -> int:
+        """
+        Randomly chooses a display number and tries to acquire a lock for this
+        number. If the lock could be acquired, returns this number, otherwise
+        choses a new one.
+        :return: free display number
+        """
+        while True:
+            rand = randint(1, self.__class__.MAX_DISPLAY)
+            if self._get_lock_for_display(rand):
                 return rand
+            else:
+                continue
 
-    def _set_display_var(self, display):
-        os.environ['DISPLAY'] = ':{}'.format(display)
+    def _local_display_exists(self, display) -> bool:
+        return os.path.exists(f"/tmp/.X11-unix/X{display}")
 
-    def xvfb_exists(self):
-        """Check that Xvfb is available on PATH and is executable."""
-        paths = os.environ['PATH'].split(os.pathsep)
-        return any(os.access(os.path.join(path, 'Xvfb'), os.X_OK)
-                   for path in paths)
+    def _set_display(self, display_var):
+        self.environ["DISPLAY"] = display_var
