Writing a setup.py
typically involves lots of boilerplate and copy-pasting from project to project.
This package aims to simplify that and bring some DRY principle to python packaging.
Here's what your (complete, and ready to ship to pypi) setup.py
could look like with setupmeta:
from setuptools import setup setup( name='myproject', versioning='distance', # Optional, would activate tag-based versioning setup_requires='setupmeta' # This is where setupmeta comes in )
And that should be it - setupmeta will take it from there, extracting everything else from the rest of your project (following typical conventions commonly used).
You can use the explain command (see commands) to see what setupmeta deduced from your project, for the above it would look something like this (you can see which file and which line each setting came from, note that a lot of info is typically extracted from your project, if you follow usual conventions):
~/myproject: python setup.py explain author: (auto-adjust ) Your Name \_: (myproject.py:7 ) Your Name<[email protected]> author_email: (auto-adjust ) [email protected] classifiers: (classifiers.txt ) 6 items: ['Development Status :: ... description: (README.rst:1 ) First line of your README entry_points: (entry_points.ini) [console_scripts] ... install_requires: (requirements.txt) ['click', ... license: (auto-fill ) MIT long_description: (README.rst ) Long description would be your inlined README name: (explicit ) myproject py_modules: (auto-fill ) ['myproject'] setup_requires: (explicit ) ['setupmeta'] version: (git ) 1.2.3.post2 versioning: (explicit ) distance
See examples for more.
The goal of this project is to:
- Allow to write very short (yet complete)
setup.py
-s, without boilerplate, and encourage good common packaging practices - Point out missing important info (example: version) in
setup.py explain
- Support tag-based versioning (like setuptools_scm, but with super simple configuration/defaults and automated
bump
capability) - Provide useful Commands to see the metadata (explain), version (including support for bumping versions), cleanall, twine, etc
Everything that you explicitly provide in your original
setuptools.setup()
call is taken as-is (never changed), and internally labelled asexplicit
. So if you don't like something that setupmeta deduces, you can always explicitly state it.name
is auto-filled from your setup.py's__title__
(if there is one, sometimes having a constant is quite handy...)packages
andpackage_dir
is auto-filled accordingly if you have a<name>/__init__.py
orsrc/<name>/__init__.py
filepy_modules
is auto-filled if you have a<name>.py
fileclassifiers
is auto-filled from fileclassifiers.txt
(one classification per line, ignoring empty lines and python style comments)entry_points
is auto-filled from fileentry_points.ini
(bonus: tools like PyCharm have a nice syntax highlighter for those)install_requires
is auto-filled if you have arequirements.txt
(orpinned.txt
) file, pinning is abstracted away by default as per community recommendation, see requirements for more info.tests_require
is auto-filled if you have atests/requirements.txt
, orrequirements-dev.txt
, ordev-requirements.txt
, ortest-requirements.txt
filedescription
will be the 1st line of your README (unless that 1st line is too short, or is just the project's name), or the 1st line of the first docstring found in the scanned files (see list below)long_description
is auto-filled from your README file (looking forREADME.rst
,README.md
, thenREADME*
, first one found wins). Special tokens can be used (notation aimed at them easily being rst comments):.. [[end long_description]]
as end marker, so you don't have to use the entire file as long description.. [[include <relative-path>]]
if you want another file included as well (for example, people like to addHISTORY.txt
as well)- these tokens must appear either at beginning/end of line, or be after/before at least one space character
version
can be stated explicitly, or be computed from git tags usingversioning=...
(see versioning for more info):With
versioning='distance'
, your git tags will be of the formv{major}.{minor}.0
, the number of commits since latest version tag will be used to auto-fill the "patch" part of the version:- tag "v1.0.0", no commits since tag -> version is "1.0.0"
- tag "v1.0.0", 5 commits since tag -> version is "1.0.5"
- if checkout is dirty,
+{commitid}
is added -> version would be "1.0.5.post5+g123"
With
versioning='post'
, your git tags will be of the formv{major}.{minor}.{patch}
, a "post" addendum will be present if there are commits since latest version tag:- tag "v1.0.0", no commits since tag -> version is "1.0.0"
- tag "v1.0.0", 5 commits since tag -> version is "1.0.0.post5"
- if checkout is dirty,
+{commitid}
is added -> version would be "1.0.0.post5+g123"
With
versioning='build-id'
, your git tags will be of the formv{major}.{minor}.0
, the number of commits since latest version tag will be used to auto-fill the "patch" part of the version:- tag "v1.0.0", no commits since tag,
BUILD_ID=12
-> version is "1.0.0+h12.g123" - tag "v1.0.0", no commits since tag,
BUILD_ID
not defined -> version is "1.0.0+hlocal.g123" - tag "v1.0.0", 5 commits since tag,
BUILD_ID=12
-> version is "1.0.5+h12.g456" - tag "v1.0.0", 5 commits since tag,
BUILD_ID
not defined -> version is "1.0.5+hlocal.g456" - if checkout is dirty,
.dirty
is added -> version would be "1.0.5+hlocal.g456.dirty"
- tag "v1.0.0", no commits since tag,
Use the bump command (see commands) to easily bump (ie: increment major, minor or patch + apply git tag)
Version format can be customized, see versioning for more info
version
,versioning
,url
,download_url
,license
,keywords
,author
,contact
,maintainer
, andplatforms
will be auto-filled from:Lines of the form
__key__ = "value"
in your modules (simple constants only, expressions are ignored - the modules are not imported but scanned using regexes)Lines of the form
key: value
in your docstringFiles are examined in this order (first find wins):
setup.py
<package>.py
(mccabe for example)<package>/__about__.py
(cryptography for example)<package>/__version__.py
(requests for example)<package>/__init__.py
(changes, arrow for example)src/
is also examined (for those who like to have their packages undersrc
)
URLs can be simplified:
url
may use{name}
, it will be expanded appropriately- if
url
points to your general github repo (like: https://github.com/zsimic), thename
of your project is auto-appended to it - if
download_url
is a relative path, it is auto-filled by prefixing it withurl
download_url
may use{name}
and/or{version}
, those will be expanded appropriately
author
,maintainer
andcontact
names and emails can be combined into one line (setupmeta will figure out the email part and auto-fill it properly)- i.e.:
author: Bob D [email protected]
will yield the properauthor
andauthor_email
settings
- i.e.:
This should hopefully work nicely for the vast majority of python projects out there. If you need advanced stuff, you can still leverage setupmeta for all the usual stuff above, and go explicit wherever needed.
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 explain
to 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__.py
or__about__.py
file 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.
- Support more SCMs, like
hg
Since setupmeta is designed to be used as setup_requires
, it is important it doesn't break in any edge case,
as that would be disruptive to all clients.
Test coverage is at 100%, and should remain at that at all times. If you are contributing, please craft test cases that exercise all possible edge cases.