Skip to main content

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, version
  • org.freedesktop.portal.Settings: Read, ReadAll (minimal — Chrome probes color-scheme during portal availability checks)
  • org.freedesktop.portal.Request: the Response signal and Close

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.

Release files for filechooser-portal-mock 0.1.post1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for filechooser-portal-mock 0.1.post1
File Size Uploaded
filechooser_portal_mock-0.1.post1.tar.gz 26.8 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for filechooser-portal-mock 0.1.post1
File Interpreter ABI Platform
filechooser_portal_mock-0.1.post1-py3-none-any.whl Python 3 none any Details

Total release size: 49.5 kB

Release files / filechooser_portal_mock-0.1.post1.tar.gz

Download URL filechooser_portal_mock-0.1.post1.tar.gz
Size 26.8 kB
Tags Source
SHA-256 checksum
How to use checksums
a9e6136523e69361f33444ffe23f1b8645fbf96b59ffd61494f9feba1822a4ca
BLAKE2b-256 checksum
How to use checksums
149156cd0a0c59118760215988da6b7af80a566df611dc36eafa324912d962a6
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.12

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Jun 8, 2026.

Transparency log

Release files / filechooser_portal_mock-0.1.post1-py3-none-any.whl

Download URL filechooser_portal_mock-0.1.post1-py3-none-any.whl
Size 22.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
55a23c2799cc63bb6e315471dcc3b06554230d6b42c45bced464fdc97ca1125d
BLAKE2b-256 checksum
How to use checksums
bc57ef92a1672c4c0bc382cf6fa6bede22e6944224efc9997fbf3e126ef14be3
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.12

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Jun 8, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.1.post1 This release

2 release files

0.1

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page