Skip to content

Fix the Qt4/Qt5 port that left the app unable to start, and modernise the template - #1

Merged
bgriffen merged 10 commits into
masterfrom
modernise-and-fix-qt5-port
Jul 27, 2026
Merged

Fix the Qt4/Qt5 port that left the app unable to start, and modernise the template#1
bgriffen merged 10 commits into
masterfrom
modernise-and-fix-qt5-port

Conversation

@bgriffen

Copy link
Copy Markdown
Owner

Summary

The application did not start. CommonMPL.py — the module main.py imports — was still entirely Qt4, despite the earlier "updated to qt5 and traits6" commit, which only ported the standalone demo's private copy of the same editor. On the versions requirements.txt itself pins, importing it raises before anything renders:

mpl.use('Qt4Agg')                     -> ValueError, backend removed
matplotlib.backends.backend_qt4agg    -> removed in matplotlib 3.5
traitsui.qt4.editor                   -> removed in TraitsUI 8

This branch fixes that and the defects it was hiding, then adds the tests, CI and docs to keep it fixed.

What was broken

Area Defect
CommonMPL.py Qt4 imports throughout — app could not import, let alone run
main.py All three marker handlers guarded on display_points, which nothing ever assigned, so they returned early forever. Had they not, they called wx.CallAfter — wx is not imported and not a dependency of a Qt app — and would have raised NameError
main.py markercolor was set in __init__ but never declared as a trait, so it had no handler wiring and no UI control
main.py _secondcalc_default returned an undefined SecondCalc
main.py __init__ skipped HasTraits.__init__ and swallowed **kwargs
firstcalc.py gradient/yintcept were Range(0.0, 5.0, 10.0) — default outside its own bounds. The editor clamps it to 5.0 while building the UI, firing _gradient_changed, whose figure.clear() destroyed the labelled axes before the window was shown
firstcalc.py _rangeplotx_changed watched a non-existent trait and could not have run: a bare y statement plus undefined figure, xtmp, ytmp
firstcalc.py plotbutton was labelled "Plot Me!" with no _plotbutton_fired handler
firstcalc.py listoptions listed "Option 2" twice, so the menu only ever offered two choices
requirements.txt Declared no Qt binding — neither TraitsUI nor matplotlib depends on one, so a clean install could not run the app
conda files Two per-platform lock files pinning Python 3.5, Qt 4.8, traits 4.6 and matplotlib 1.5.1 to exact build hashes that no longer resolve, contradicting requirements.txt, and leaking absolute paths from the author's machine

What changed

Nine atomic commits:

  1. chore — add .gitignore and MIT licence
  2. fix — port the matplotlib editor from Qt4 to Qt5
  3. fix — repair the dead marker controls in ApplicationMain
  4. fix — correct invalid slider ranges and remove the broken handler in FirstCalc
  5. refactor — reuse the shared editor in the standalone demo
  6. build — modernise dependency declarations
  7. test — add a headless regression suite
  8. ci — run tests and lint on every push and pull request
  9. docs — rewrite the README and refresh the screenshot

Two design points worth review:

  • Import order in CommonMPL.py is load-bearing. pyface.qt is imported first so it resolves the Qt binding; matplotlib then reuses it rather than independently picking a different one and crashing on the first shared widget. The imports are fenced with # isort: off so a formatter cannot silently undo this.
  • plot_xy() on ApplicationMain now owns the line. Tabs draw through it instead of rebuilding the figure by hand. That keeps the axis labels alive across replots and gives the marker controls a live handle on what is drawn. The duplicated, byte-identical _gradient_changed/_yintcept_changed bodies both collapse into it.

Verification

  • 44 tests, each pinned to a specific defect above, passing on both a dev environment and a clean pip install -r requirements.txt.
  • Confirmed the suite has teeth by reintroducing three original bugs; each fails the test written for it.
  • ruff check . clean.
  • main.py and standalonematplotlib.py both build their full widget trees offscreen.
  • The README's "adding a tab" example was applied to a copy of the tree and run.

CI covers Python 3.9, 3.11 and 3.12 headlessly via Qt's offscreen plugin — no display or xvfb needed.

Please check before merging

  • The MIT licence is my choice, not yours. The repo had none; swap or drop it if you want different terms.
  • Deleted files: the two conda lock files, the empty root __init__.py, and both old screenshots (they predate the port and show the empty plot area the startup bug produced, replaced by one current capture).
  • No packaging metadata was added deliberately — publishing top-level modules named main and CommonMPL to site-packages would collide with anything else doing the same. pyproject.toml carries pytest and ruff config only.

Brendan Griffen added 10 commits July 27, 2026 23:01
The repository had no .gitignore, so __pycache__ and virtualenvs were
one stray command away from being committed, and no LICENSE, which
leaves reuse terms undefined for a repo whose whole purpose is to be
copied as a starting point.
CommonMPL was still entirely Qt4 despite the earlier 'updated to qt5'
commit, so the application could not start at all on the versions its
own requirements.txt pins:

  * mpl.use('Qt4Agg')                 -> ValueError, backend removed
  * matplotlib.backends.backend_qt4agg -> gone since matplotlib 3.5
  * traitsui.qt4.editor                -> gone since TraitsUI 8

Port to backend_qtagg and traitsui.qt. pyface.qt is imported first so it
pins the Qt binding and matplotlib reuses it, rather than the two
independently resolving different bindings and crashing when they share
a widget.

Also subclass the Qt Editor rather than the toolkit-agnostic base, which
supplies a real set_size_policy so the figure honours resizing, and give
update_editor an actual redraw instead of a no-op.

Replace the 'from CommonMPL import *' star imports in main and firstcalc
with explicit ones. CommonMPL was doubling as a grab-bag re-exporting the
whole traits API; it now exports only the editor and declares __all__.

Verified by building the full widget tree offscreen.
The Marker and Size controls in the left panel could never do anything:

  * every handler guarded on 'display_points', an attribute nothing ever
    assigned, so all three returned early forever;
  * had the guard passed, each called wx.CallAfter -- wx is neither
    imported nor a dependency of what is now a Qt application, so the
    controls would have raised NameError instead;
  * markercolor was assigned in __init__ but never declared as a trait,
    so _markercolor_changed had no trait to fire it and no UI control.

Declare markercolor/markerstyle/markersize properly, give markercolor a
control, and add plot_xy() which owns the line and keeps display_points
pointing at it, so the marker controls act on live data. plot_xy reuses
the axes instead of clearing the figure, and re-enables autoscaling that
the placeholder 0..1 limits had switched off.

Also drop _secondcalc_default, which returned an undefined SecondCalc,
and the __init__ that skipped HasTraits.__init__ and swallowed **kwargs;
the defaults it set now live on the trait declarations.
…Calc

gradient and yintcept were declared Range(0.0, 5.0, 10.0) -- a default
outside its own bounds. Traits keeps 10.0 at init, then the RangeEditor
clamps it to 5.0 while building the UI, firing _gradient_changed during
startup. That handler called figure.clear(), so the axes labelled by
_display_default were destroyed before the window was even shown. Give
both sliders defaults inside their range (y = x).

Delete _rangeplotx_changed. It watched a trait that does not exist, and
could not have run regardless: a bare 'y' expression statement, and
references to undefined 'figure', 'xtmp' and 'ytmp'.

_gradient_changed and _yintcept_changed were byte-identical copies that
each rebuilt the figure by hand; both now delegate to _replot, which
goes through the main window's plot_xy. Wire up plotbutton, which was
labelled 'Plot Me!' but had no _plotbutton_fired handler at all.

Deduplicate listoptions, which offered 'Option 2' twice and so only ever
showed two choices; initialise yval so the '1/x' readout is not 0.0
until the slider is first moved; take masterpath's default at
instantiation rather than at import; and forward **kwargs through
__init__ to HasTraits.
standalonematplotlib carried its own copy of _MPLFigureEditor. Keeping
two implementations is what let them drift apart: this copy had been
ported to Qt5 while the one in CommonMPL that the actual application
imports was left on Qt4 and broken.

Import the editor instead, dropping ~45 duplicated lines along with the
no-op set_size_policy stub the copy needed to work around subclassing
the toolkit-agnostic Editor base.

Rename Test to RectangleSelectorDemo so pytest does not try to collect
it as a test class, and keep MPLInitHandler's contract explicit.
requirements.txt declared no Qt binding at all, so a clean
'pip install -r requirements.txt' left you unable to start the app --
both TraitsUI and matplotlib need one and neither depends on one. Add
PyQt5 and relax the exact pins to the lower bounds the code genuinely
requires, documenting why each floor exists.

Replace the two per-platform conda lock files with a single
environment.yml. They pinned Python 3.5, Qt 4.8, traits 4.6, traitsui
6.0 and matplotlib 1.5.1 to exact build hashes against the 'defaults'
channel -- unresolvable today, and contradicting requirements.txt, which
already asked for traitsui 8. They also leaked absolute prefixes from
the author's machine.

Delete the empty top-level __init__.py: the modules here are imported as
top-level scripts, and an __init__.py at the repository root only
confuses tooling into treating the checkout as a package.
44 tests covering the defects fixed in this branch, runnable with no
display via QT_QPA_PLATFORM=offscreen so they work in CI and over ssh.

Each is pinned to a specific bug rather than to general behaviour: that
no Qt4 or wx names return to the sources, that matplotlib and pyface
resolve the same Qt binding, that Range defaults sit inside their own
bounds, that replotting reuses the axes rather than clearing the figure,
that autoscaling recovers from the placeholder limits, and that the
marker controls reach the plotted line.

Verified the suite has teeth by reintroducing three of the original
bugs; each one fails the test written for it.

pyproject.toml carries pytest and ruff configuration only. The
repository is a template to copy rather than a library to install, so it
declares no build backend -- shipping top-level modules called 'main'
and 'CommonMPL' to site-packages would collide with anything else doing
the same.
Tests run on Python 3.9, 3.11 and 3.12 against the offscreen Qt platform
plugin, so no display or xvfb is needed. PyQt5's wheels bundle Qt but
still link against system libraries the runner image lacks, so those are
installed explicitly; the offscreen plugin needs them too.

CI installs from requirements.txt rather than a bespoke list, so the
file stays honest about what is actually required to run the app.

Also drop the redundant '# -*- coding: utf-8 -*-' declarations (UTF-8 is
the default in Python 3) and fence CommonMPL's imports with isort: off.
Sorting them alphabetically would put matplotlib before pyface and
silently undo the binding coordination the port depends on.
The old README described software that could not run: it advertised a
Qt5 backend the code did not use, told you to 'source activate' a conda
environment built from lock files that no longer resolve, and offered
'pip install -r requirements.txt' as an alternative that left you
without a Qt binding.

Rewrite it around what the template is actually for: a centred header
with CI, Python, Qt and licence badges, then features, prerequisites,
installation, usage, a file-by-file map, a worked example of adding a
tab, and how to run the tests and linter. The author's original links
and framing are kept.

Add a section on the Traits naming convention, including the two
behaviours that produced bugs in this repository -- editors clamping an
out-of-range Range default during startup, and change handlers being
bound at class creation.

Verified the 'adding a tab' example by applying it to a copy of the
tree: the new tab plots through plot_xy and the marker controls act on
its line.

Replace the two stale platform screenshots with one current capture,
rendered offscreen from the fixed application. The old images predate
the port and show the empty plot area the startup bug produced.
actions/checkout@v4 and actions/setup-python@v5 target Node 20, which
the runners now force onto Node 24 with a deprecation warning on every
job. Move to v5/v6 so a freshly added workflow does not ship already
warning.
@bgriffen
bgriffen force-pushed the modernise-and-fix-qt5-port branch from c243f0f to 00cf0be Compare July 27, 2026 13:04
@bgriffen
bgriffen merged commit 66ecd9e into master Jul 27, 2026
4 checks passed
@bgriffen
bgriffen deleted the modernise-and-fix-qt5-port branch July 27, 2026 13:53
bgriffen added a commit that referenced this pull request Jul 27, 2026
Fix the Qt4/Qt5 port that left the app unable to start, and modernise the template
bgriffen added a commit that referenced this pull request Jul 27, 2026
Fix the Qt4/Qt5 port that left the app unable to start, and modernise the template
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant