Fix the Qt4/Qt5 port that left the app unable to start, and modernise the template - #1
Merged
Merged
Conversation
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
force-pushed
the
modernise-and-fix-qt5-port
branch
from
July 27, 2026 13:04
c243f0f to
00cf0be
Compare
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The application did not start.
CommonMPL.py— the modulemain.pyimports — 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 versionsrequirements.txtitself pins, importing it raises before anything renders:This branch fixes that and the defects it was hiding, then adds the tests, CI and docs to keep it fixed.
What was broken
CommonMPL.pymain.pydisplay_points, which nothing ever assigned, so they returned early forever. Had they not, they calledwx.CallAfter— wx is not imported and not a dependency of a Qt app — and would have raisedNameErrormain.pymarkercolorwas set in__init__but never declared as a trait, so it had no handler wiring and no UI controlmain.py_secondcalc_defaultreturned an undefinedSecondCalcmain.py__init__skippedHasTraits.__init__and swallowed**kwargsfirstcalc.pygradient/yintceptwereRange(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, whosefigure.clear()destroyed the labelled axes before the window was shownfirstcalc.py_rangeplotx_changedwatched a non-existent trait and could not have run: a bareystatement plus undefinedfigure,xtmp,ytmpfirstcalc.pyplotbuttonwas labelled "Plot Me!" with no_plotbutton_firedhandlerfirstcalc.pylistoptionslisted "Option 2" twice, so the menu only ever offered two choicesrequirements.txtrequirements.txt, and leaking absolute paths from the author's machineWhat changed
Nine atomic commits:
chore— add.gitignoreand MIT licencefix— port the matplotlib editor from Qt4 to Qt5fix— repair the dead marker controls inApplicationMainfix— correct invalid slider ranges and remove the broken handler inFirstCalcrefactor— reuse the shared editor in the standalone demobuild— modernise dependency declarationstest— add a headless regression suiteci— run tests and lint on every push and pull requestdocs— rewrite the README and refresh the screenshotTwo design points worth review:
CommonMPL.pyis load-bearing.pyface.qtis 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: offso a formatter cannot silently undo this.plot_xy()onApplicationMainnow 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_changedbodies both collapse into it.Verification
pip install -r requirements.txt.ruff check .clean.main.pyandstandalonematplotlib.pyboth build their full widget trees offscreen.CI covers Python 3.9, 3.11 and 3.12 headlessly via Qt's
offscreenplugin — no display or xvfb needed.Please check before merging
__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).mainandCommonMPLto site-packages would collide with anything else doing the same.pyproject.tomlcarries pytest and ruff config only.