Hey everyone! Excuse the long post. I’ve broken it up into sections to hopefully make it easier to jump between sections. Any thoughts/recommendations would be very welcome.
Intro
I have a long-term minesweeper project, which is a GUI application that I make available both with PyInstaller packages and on PyPI. I try to keep up with the latest best practices, and I’ve been following this Discourse category for a while now! I’m in the process of trying to move away from having a setup.py
, but while diving deeper into python packaging there’s a few things I’m trying to understand…
I’ve reached the point where I’d like to add some complexity to the packaging of my app:
- Make some features optional (e.g. I provide a CLI tool for querying online highscores, which has a few extra dependencies)
- Provide some performance-critical functionality using Zig/C/Rust and package up into platform-dependent wheels
I’ll keep this post focused on the first point, but may start a separate post about the second point later on.
The question
What’s the recommended way to give the user the ability to control the set of features that gets installed (e.g. with pip)?
More specifically, what I’m interested in is:
- Making extra features available without making them mandatory, to minimise the default set of dependencies
- Having the option for a ‘minimal install’ that installs the minimum code/dependencies required for the app to run, i.e. a way to remove dependencies from the default set
I’m also confused that some examples online seem to suggest having ‘test’ as an optional extra (for running the project’s tests) - is this recommended practice? What’s the expected case where this is useful to the user performing a ‘pip install’?
What I’ve tried and thoughts
The relevant part of my setup.py
(as it is at the time of writing) is here.
This does the following:
-
packages
andpackage_data
include everything needed for all extras - I haven’t found a way to conditionally include based on requested extras -
install_requires
andextras_require
include base dependencies and extra dependencies, as you’d expect (this seems to be the only part of packaging that’s actually intended to be supported by extras?) -
entry_points
includes one default entrypoint and one entrypoint that’s only desired when an extra is specified, although the extra marker seems to be ignored such that if the extra isn’t installed then the script is still created but won’t work since the extra dependencies aren’t installed
The latter point is covered at Console_scripts entrypoints hidden behind extras are always installed · Issue #9726 · pypa/pip · GitHub, in particular the last comment which suggests moving code into separate packages such that ‘extra’ entrypoints are managed by ‘extra’ dependencies. The problem with this is that in my case it’s my implementation of the ‘extra’ that depends on the main application code, not the other way around…
In theory I could refactor out into package dependencies such as minegauler -> minegauler-core
and minegauler[bot] -> minegauler-bot -> minegauler-core
i.e. factor out the core application code that’s needed by extras into a third package such that the main package has a regular dependency on the core and an ‘extras’ dependency on the CLI tool, which also depends on the core… but this feels like a lot more faff than it should be - is proliferation of packages really the answer?
If the recommendation is to use many packages such that extras manage their own code/data/entrypoints and can be declared as simple dependencies then does it make sense to keep them together in a single repo in their own subdirectories? Something like:
|-- minegauler/
| |-- minegauler/
| `-- pyproject.toml
|-- minegauler-bot/
| |-- minegauler_bot/
| `-- pyproject.toml
`-- minegauler-core/
|-- minegauler_core/
`-- pyproject.toml
I don’t like this because project files don’t live at the root of the repo, and I think that breaks being able to pip install using the GitHub URL… So I guess I’d be left managing 3+ dependent repositories at once.
I’m left feeling this use-case isn’t really properly supported in python packaging and I might need to roll my own solution. Does anyone have any better suggestions?