PyWorkout — Architecture¶
Layout¶
PyWorkout/
├── main.py the CLI — command loop, workout data, all session state
├── gui.py a separate Tkinter percentage tracker
├── __main__.py module entry point (python -m pyworkout)
├── docker-compose.yml containerised run
├── Dockerfile published to GHCR
├── pyproject.toml packaging; published to PyPI
├── setup.cfg pytest and coverage configuration
└── tests/
├── test_main.py CLI behaviour
└── test_gui.py GUI components
Two programs, not one¶
main.py and gui.py are independent. The CLI does not import the GUI, and the GUI is not
a front end for the CLI — it is a separate Tkinter window that displays percentage
completion for a fixed exercise list.
They share the project and the name, and nothing else. Worth knowing before looking for the integration point: there isn't one.
main.py is a single function¶
The entire CLI — muscle-group selection, the command loop, timing, statistics, help output,
and the exercise data itself — lives inside one workout() function of roughly six hundred
lines.
Consequences that show up in practice:
- Session state is local variables, which is why
skipandstatsinteract badly: they manipulate overlapping bookkeeping in the same scope rather than through a shared model. See Roadmap. - Help text is printed inline in two places (around lines 613 and 632) and the two
copies have drifted — one documents the
skip/statslimitation, the other omits it. - Tests reach the logic through
builtins.inputandbuiltins.print. Every test intest_main.pypatches those and asserts against captured output, because there is no return value to inspect. That is a consequence of the structure, not a testing choice.
Extracting the workout data and the session state into their own modules is the change that would unlock most of the rest.
Data¶
Exercise definitions — muscle groups, exercises, sets, reps — are literals in main.py.
There is no data file, no database, and nothing persisted between runs. Closing the program
discards the session.
Video paths are also literals, under a # Video File Paths comment, which is why the
video command requires editing source to work. See Configuration.
Timing¶
Elapsed time is computed from a start timestamp captured by start and compared against
the current time on each next, stats, and end. There is no pause, and no persistence —
the timer measures wall-clock time from start, including any time you spent away from the
terminal.
The known timer defect tracked for 2.0.0 lives here.
Distribution¶
The same code ships four ways: PyPI (pip install pyworkout), a GHCR container, a Windows
executable attached to releases, and the source itself. pyproject.toml drives the first,
Dockerfile the second.
Because the PyPI and GHCR pages render the README off-site, its images must be absolute
URLs — they point at .github/icons/PyWorkout/. Relative image paths would break there
even though they work on github.com.
Testing¶
setup.cfg configures pytest with coverage, branch coverage, and three report formats.
Coverage sits around 54%. GUI tests skip in headless environments, since Tkinter needs a
display — expected in CI rather than a failure.