Mock xdg-desktop-portal FileChooser D-Bus service for automating native file dialogs in headless/CI GUI and browser tests.
Project description
filechooser-portal-mock
A mock xdg-desktop-portal
FileChooser D-Bus service for automating native file dialogs in headless/CI
GUI and browser tests.
When a modern application (Chromium, any GTK app) needs to open a file dialog,
it does not draw the dialog itself — it makes a D-Bus call to
org.freedesktop.portal.Desktop. In a headless or CI environment there is no
desktop portal to answer, so the application hangs or times out.
filechooser-portal-mock impersonates that portal. It answers the
OpenFile/SaveFile call and emits a Response signal carrying a path you
choose, so the application proceeds exactly as if a human had picked it — with
no GUI, under Xvfb, in CI.
The flagship use case is loading an unpacked Chrome extension in an
automated test (chrome://extensions → "Load unpacked"), but it works for any
open/save file dialog routed through the portal.
app under test filechooser-portal-mock
┌───────────────┐ OpenFile() ┌───────────────────────┐
│ Chrome / │ ────────────► │ org.freedesktop │
│ GTK app │ │ .portal.Desktop │
│ │ ◄──────────── │ (FileChooser mock) │
└───────────────┘ Response └───────────────────────┘
"user picked file:///path/you/configured"
Why a portal mock (and not clicking the dialog)?
The file picker is a separate, privileged process; you cannot reliably
screen-scrape or xdotool it across desktops and versions. Intercepting the
request at the D-Bus layer is robust and deterministic. See
docs/protocol.md for the protocol details and the
hard-won pydbus-vs-dbus-python findings that make this work in subprocess
contexts.
Installation
This package binds to your system's GLib via PyGObject and pydbus, which are built against system libraries. Install the system dependencies first.
Debian / Ubuntu:
sudo apt install -y dbus python3-dev pkg-config \
libcairo2-dev libgirepository-2.0-dev libglib2.0-dev
pip install filechooser-portal-mock
Older distributions provide girepository under
libgirepository1.0-dev.
Fedora:
sudo dnf install -y dbus python3-devel pkgconf-pkg-config \
cairo-devel gobject-introspection-devel glib2-devel
pip install filechooser-portal-mock
If your distribution packages PyGObject and pydbus (e.g. python3-gi,
python3-pydbus), you can install those from the system instead of building
them from source.
A virtual X server (Xvfb) is only needed by the application under test, not
by this tool. The portal itself needs no display.
Quick start
Command line (works from any language / test harness)
# Start a portal on a private bus that answers every file dialog with a path:
filechooser-portal --isolated-bus /abs/path/to/extension
# -> prints: DBUS_SESSION_BUS_ADDRESS=unix:abstract=...
# FILECHOOSER_PORTAL_READY
# Other answers:
filechooser-portal --cancel # simulate the user cancelling
filechooser-portal --error # simulate a failed request
Point your application at the printed bus address
(DBUS_SESSION_BUS_ADDRESS=...) and it will receive that path when it opens a
file dialog. The FILECHOOSER_PORTAL_READY line lets you wait for readiness
instead of sleeping.
Python (context manager)
from filechooser_portal import serve
with serve("/abs/path/to/extension") as portal:
launch_app(env=portal.env) # portal.env has DBUS_SESSION_BUS_ADDRESS set
# portal (and its private bus) are torn down on exit
pytest fixture
Installing the package registers a file_chooser_portal fixture automatically:
def test_load_unpacked(file_chooser_portal):
portal = file_chooser_portal("/abs/path/to/extension")
launch_chrome(env=portal.env)
...
Loading an unpacked Chrome extension (Selenium)
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from filechooser_portal import serve
with serve("/abs/path/to/extension") as portal:
options = Options()
options.add_argument("--ozone-platform=x11") # run under Xvfb, not headless
# Run Chrome on the same private bus as the portal:
import os
os.environ["DBUS_SESSION_BUS_ADDRESS"] = portal.bus_address
driver = webdriver.Chrome(options=options)
driver.get("chrome://extensions")
# enable developer mode, then click "Load unpacked" via JS; the portal
# answers the directory chooser with your extension path.
A complete, runnable version is in
examples/selenium_load_unpacked.py.
Dynamic behavior (write your own)
The CLI is intentionally static. For per-request logic, subclass the portal and override the hooks, then run it:
from filechooser_portal import FileChooserPortal, Response, run_portal
class MyPortal(FileChooserPortal):
def on_open_file(self, parent_window, title, options):
if "extension" in title.lower():
return Response.select("/abs/path/to/extension")
return Response.cancel()
run_portal(MyPortal()) # uses $DBUS_SESSION_BUS_ADDRESS
You can also pass a callable as the answer:
portal = FileChooserPortal(
lambda method, parent, title, options: Response.select("/abs/path")
)
See examples/dynamic_portal.py.
What it implements
org.freedesktop.portal.FileChooser:OpenFile,SaveFile,versionorg.freedesktop.portal.Settings:Read,ReadAll(minimal — Chrome probescolor-schemeduring portal availability checks)org.freedesktop.portal.Request: theResponsesignal andClose
Response codes follow the portal spec: 0 success, 1 cancelled, 2 ended.
Limitations
- Linux / D-Bus only.
- One configured answer (or one callable/subclass) drives all requests; there is no built-in IPC to change the answer of an already-running CLI process — use the Python API for dynamic behavior.
- Implements the FileChooser portal, not the entire portal surface.
Versioning and releases
Versions are derived from git via hatch-vcs
(git describe). Every push to main is automatically published to PyPI as a
post-release (e.g. 0.1.post3 for the third commit after v0.1), so
pip install filechooser-portal-mock always installs the latest main build.
Pushing a vX.Y tag publishes a clean release (e.g. 0.1).
Development
git clone https://github.com/mithro/filechooser-portal-mock
cd filechooser-portal-mock
uv run --extra test pytest -v
The core tests act as a D-Bus client against the portal on a private bus and need no display.
License
Apache-2.0. Originally developed within the youtube-shortcuts
project and extracted into a standalone tool.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file filechooser_portal_mock-0.1.post1.tar.gz.
File metadata
- Download URL: filechooser_portal_mock-0.1.post1.tar.gz
- Upload date:
- Size: 26.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a9e6136523e69361f33444ffe23f1b8645fbf96b59ffd61494f9feba1822a4ca
|
|
| MD5 |
b4fc113a182b905d448f7da7f0fa5fad
|
|
| BLAKE2b-256 |
149156cd0a0c59118760215988da6b7af80a566df611dc36eafa324912d962a6
|
Provenance
The following attestation bundles were made for filechooser_portal_mock-0.1.post1.tar.gz:
Publisher:
release.yml on mithro/filechooser-portal-mock
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
filechooser_portal_mock-0.1.post1.tar.gz -
Subject digest:
a9e6136523e69361f33444ffe23f1b8645fbf96b59ffd61494f9feba1822a4ca - Sigstore transparency entry: 1755170856
- Sigstore integration time:
-
Permalink:
mithro/filechooser-portal-mock@ece4276085800c749e337a7fa4426a4ad11576cd -
Branch / Tag:
refs/heads/main - Owner: https://github.com/mithro
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@ece4276085800c749e337a7fa4426a4ad11576cd -
Trigger Event:
push
-
Statement type:
File details
Details for the file filechooser_portal_mock-0.1.post1-py3-none-any.whl.
File metadata
- Download URL: filechooser_portal_mock-0.1.post1-py3-none-any.whl
- Upload date:
- Size: 22.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
55a23c2799cc63bb6e315471dcc3b06554230d6b42c45bced464fdc97ca1125d
|
|
| MD5 |
f29a653e8243397f593ecdfac86f79f6
|
|
| BLAKE2b-256 |
bc57ef92a1672c4c0bc382cf6fa6bede22e6944224efc9997fbf3e126ef14be3
|
Provenance
The following attestation bundles were made for filechooser_portal_mock-0.1.post1-py3-none-any.whl:
Publisher:
release.yml on mithro/filechooser-portal-mock
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
filechooser_portal_mock-0.1.post1-py3-none-any.whl -
Subject digest:
55a23c2799cc63bb6e315471dcc3b06554230d6b42c45bced464fdc97ca1125d - Sigstore transparency entry: 1755170882
- Sigstore integration time:
-
Permalink:
mithro/filechooser-portal-mock@ece4276085800c749e337a7fa4426a4ad11576cd -
Branch / Tag:
refs/heads/main - Owner: https://github.com/mithro
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@ece4276085800c749e337a7fa4426a4ad11576cd -
Trigger Event:
push
-
Statement type: