setupmeta derives your project's version from git tags, bumps it for you, and fills in the
rest of your setup.py metadata from the files your project already has.
A complete, ready to publish setup.py can look like this:
from setuptools import setup
setup(
name="myproject",
versioning="distance", # Optional: activates git-tag-based versioning
setup_requires="setupmeta", # This is what activates setupmeta
)
Run python setup.py explain to see every setting setupmeta determined, along with the
file and line each value came from. Anything you pass explicitly to setup() (or state in
setup.cfg) is used as-is, so you can always override what setupmeta deduces.
Versioning is opt-in: pass a versioning= strategy to setup() (or state it like any
other auto-filled value, see below). The version then comes from:
git describe --dirty --tags --long --first-parent --match 'v*.*'
which requires git 1.8.4 or later. Only tags starting with v count (configurable with
version_tag, see below). Without any version tag, the version starts from 0.0.0.
The preconfigured strategies yield PEP-440 compliant versions:
| State of the checkout | post |
dev |
distance |
|---|---|---|---|
| No commit yet | 0.0.0 |
0.0.0 |
0.0.0 |
| 1 commit, no tag yet | 0.0.0.post1 |
0.0.1.dev1 |
0.0.1 |
| Same, with pending change | 0.0.0.post1+dirty |
0.0.1.dev1+dirty |
0.0.1+dirty |
Exactly on tag v0.1.0 |
0.1.0 |
0.1.0 |
0.1.0 |
| Same, with pending change | 0.1.0+dirty |
0.1.1.dev0+dirty |
0.1.0+dirty |
| 2 commits after the tag | 0.1.0.post2 |
0.1.1.dev2 |
0.1.2 |
postsuits projects that tag each release and publish occasionally.devmakes untagged versions pre-releases (pip install --preto get them), assuming the next bump will be apatch.distancesuits publishing at every commit: the patch number is the count of commits since the latestv{major}.{minor}.0tag, so onlymajorandminorcan be bumped.devcommitisdevplus the commit id when not exactly on a tag:0.1.1.dev2+g2cff0a6.build-idisdistanceplus a local part that is always shown, made of the first env var whose name ends withBUILD_ID(orlocal) and the commit id:0.1.2+h12.g2cff0a6on a CI server,0.1.2+hlocal.g2cff0a6locally.
python setup.py version shows the current version, --show-next minor the next one.
--bump major|minor|patch does a dry run of a bump; add --commit to apply the
v<version> git tag, and --push to also push it. A bump is refused when:
- the current branch isn't allowed (
mainormasterby default), --commitis given while the checkout has pending changes,- the remote has tags missing locally, or the local branch is behind its remote.
You don't need a __version__ anywhere, but if one is stated in your .py files,
setupmeta warns when it disagrees with the git tag, and a bump rewrites and commits it first.
versioning also accepts a format string of the form branch(<branches>):<main>+<local>,
for example branch(main,release):{major}.{minor}.{patch}{post}+{commitid}{dirty}.
The branch(...): prefix lists the branches where bumps are allowed, and is optional.
These can be used:
{major},{minor},{patch}: components of the latest version tag{distance}: number of commits since that tag{post},{dev}:.postNor.devNwhen not exactly on a tag (Nbeing{distance}, and{dev}also bumpingpatch){devcommit},{commitid},{dirty}:.g<sha>when not exactly on a tag,g<sha>always, and.dirtywhen there are pending changes{$FOO}: value of env varFOO;{$FOO:default}gives a default for when it's undefined (otherwiseNoneis shown);$*FOO,$FOO*and$*FOO*match the first (alphabetically) env var whose name ends with, starts with or containsFOO- anything else is used as-is
Markers other than {major}, {minor}, {patch}, {distance}, {post} and {dev}
are moved to the local part if placed in the main part, as PEP-440 requires.
post+build-id and dev+devcommit are shortcuts for adding the local part of the
build-id or devcommit strategies to another one.
For full control, pass a dict: main and extra (format strings, or callables receiving a
setupmeta.scm.Version; a callable main disables bumps), branches (list or
comma-separated string), and version_tag (glob of git tags to consider, default v*.*).
SETUPMETA_GIT_DESCRIBE_COMMANDreplaces thegit describecommand above (give the full command, without the leadinggit).SCM_DESCRIBEtakes agit describe-like output (eg:v1.2.0-3-g1234567) and uses it instead of asking git, for builds where no git checkout is available.- When
setup.pylives in a subfolder of a git checkout, setupmeta writes the version to a.setupmeta.versionfile next to it, and uses that file when the project is later built from a copy that has no.git.
setupmeta fills in what you didn't state explicitly, following the usual conventions:
name: from a__title__stated insetup.py.packages/py_modules: when neither is given, all packages found undersrc/(settingpackage_diraccordingly), or else the packages whose name starts with your project's name, or a<name>.pymodule.- Simple settings like
version,versioning,url,download_url,bugtrack_url,license,keywords,author,maintainer,contact, orplatforms, from lines like__author__ = "..."(modules are scanned, never imported), orauthor: ...lines in the first paragraph of a docstring. First found wins, looking insetup.py, then<name>.py, then__about__.py,__version__.pyand__init__.pyof your top level packages (also undersrc/). author,maintainer,contact: a value likeBob D bob@example.comis split into name and*_email.url: when pointing to a github org (likehttps://github.com/codrsquad), your project's name is appended, andbugtrack_urldefaults to itsissuespage. Relativedownload_urlandbugtrack_urlare prefixed withurl, and all of them can use{name}and{version}markers.description: the first line of your README, unless it's too short or just your project's name, in which case the first line of the docstring of a scanned module is used.long_description(and its content type): your README (README.rst,README.md, thenREADME*). A line.. [[end long_description]]ends it early, and.. [[include <relative-path>]]includes another file; both must stand alone on their line.entry_points: contents of anentry_points.inifile.license: recognized from the text of aLICENSE*file.include_package_data:Truewhen there's aMANIFEST.in.install_requires: see below.
When building from an sdist, values recorded in its PKG-INFO are used.
When install_requires isn't given, it comes from requirements.in (used as-is), else
requirements.txt, where simple name==version pins are abstracted down to name, as per
this community recommendation (a library shouldn't impose pins on its users). -r other.txt references are followed. install_requires="@path/to/reqs.txt" uses a
given file, and works for extras_require values too.
A comment can mark a line, or start a section applying to all lines below it, with one of:
# abstract(the default):name==versionpins becomename# pinned: kept as-is# indirect: left out ofinstall_requiresaltogether
Only plain == pins are abstracted: click>=7.0 is always kept as-is. For example:
click==8.1.7 requests==2.31.0 # pinned retry>=0.9 # indirect urllib3==2.2.1
yields install_requires=["click", "requests==2.31.0", "retry>=0.9"].
setupmeta adds these setup.py commands, see python setup.py <command> --help:
explain: show all settings and where each came from;--dependenciesshows how requirements were auto-filled,--expandprints the equivalentsetup.pywithout setupmeta (handy if you ever want to stop using it).version: show or bump the version, see Bumping.check: setuptools'check, also reporting how many requirements were abstracted or ignored, and the pending changes (useful to tell why a CI build saw a dirty checkout).
Set SETUPMETA_DEBUG=1 to trace what setupmeta does.
My motivation was to:
- stop having to boilerplate my setup.py's
- learn how to publish to pypi (and do it right, once and for all)
- have a nice workflow for when I want to publish to pypi (
setup.py explainto see what's up at a glance)
I noticed that most open-source projects out there do the same thing over and over, like:
- Read the entire contents of their README file and use it as
long_description(copy-pasting the few lines of code to read the contents of said file) - Reading, grepping, sometimes importing a small
__version__.pyor__about__.pyfile to get values like__version__out of it, and then dutifully doingversion=__version__orversion=about["__version__"]in theirsetup.py - All kinds of creative things to get the
description - etc.
I didn't want to keep doing this anymore myself, so I decided to try and do something about it with this project.
See examples for sample projects, and docs for how setupmeta works inside.