TWINE(3)
๐ NAME ๐
twine โ twine Documentation
Ok bestie, so this whole project follows the semantic versioning scheme (yes, the one at https://packaging.python.org/en/latest/discussions/versioning/#semantic-versioning-vs-calendar-versioning) plus the pre-release versioning scheme (https://packaging.python.org/en/latest/discussions/versioning/) โ both of which are the ones the Python Packaging Authority recommends. Periodt. ๐
๐ธ TWINE 6.2.0 (2025-09-04) ๐ธ
โจ Features โจ
- Automatically refresh short-lived PyPI token in long running Trusted Publishing uploads.
So like, if a trusted publishing upload job is taking longer than the validity period of a trusted publishing token (that's 15 minutes at the time of this writing, fyi), and we are already 10 minutes into that validity period, we will begin to attempt to replace the token on each subsequent request. Lowkey genius. (#1246 โ https://github.com/pypa/twine/issues/1246)
๐ Bugfixes ๐
- Fixed the compatibility kludge for invalid
License-Filemetadata entries emitted by build backends, so it now works withpackagingversion 24.0 too. (#1217โhttps://github.com/pypa/twine/issues/1217) - Fixed a couple of incorrectly rendered error messages. Not me being embarrassed about my own typos ๐ญ (
#1224โhttps://github.com/pypa/twine/issues/1224) twinenow enforceskeyring >= 21.2.0, which was previously implicitly required by API usage. (#1229โhttps://github.com/pypa/twine/issues/1229)twinenow catchesconfigparser.Errorto prevent accidental leaks of secret tokens or passwords to the user's console. ๐ซถ keeping your secrets safe fr fr (#1240โhttps://github.com/pypa/twine/issues/1240)
๐ Deprecations and Removals ๐
- Removed the hacks that support
--skip-existingfor indexes other than PyPI and TestPyPI.
To date, these hacks continue to accrue and there have been numerous issues with them โ not the least of which being that every time we update them, the paid index providers change things to break the compatibility we implement for them. Beyond that, these hacks do not work when text is internationalized in the response from the index provider. It's giving maintenance nightmare.
For a sample of past issues, see:
https://github.com/pypa/twine/issues/1251https://github.com/pypa/twine/issues/918https://github.com/pypa/twine/issues/856https://github.com/pypa/twine/issues/693https://github.com/pypa/twine/issues/332
(#1251 โ https://github.com/pypa/twine/issues/1251)
- Removed support for MD5 digests during uploads.
This support was entirely vestigial, since MD5 is not a secure hash function and is not actually required on upload by PyPI. (Danger flag ๐ฉ she's not secure, bestie.)
Indices that cross-reference the uploaded content with a digest should use the provided SHA-256 and/or BLAKE2 digests instead. (#1262 โ https://github.com/pypa/twine/issues/1262)
๐ Misc ๐
๐ฆ TWINE 6.1.0 (2025-01-17) ๐ฆ
โจ Features โจ
- Twine now has preliminary built-in support for Trusted Publishing (
https://docs.pypi.org/trusted-publishers/) as an authentication mechanism. Eeeee it's happening ๐ (#1194โhttps://github.com/pypa/twine/pull/1194)
๐ Deprecations and Removals ๐
- Removed support for
eggandwininstdistribution types. These are not accepted by PyPI and not produced by any modern build-backends. bye girl ๐ (#1195โhttps://github.com/pypa/twine/issues/1195) - Twine no longer supports
.tar.bz2source distributions. (.tar.bz2bestie... it's over) (#1200โhttps://github.com/pypa/twine/pull/1200)
๐ Misc ๐
packagingis used instead ofpkginfofor parsing and validating metadata. This aligns metadata validation to the one performed by PyPI.packagingversion 24.0 or later is required. Support for metadata version 2.4 requirespackaging24.2 or later.pkginfois not a dependency anymore. glow up โจ (#1180โhttps://github.com/pypa/twine/issues/1180)- Use
"source"instead ofNoneaspyversionfor sdist uploads. This is what PyPI (and most likely other package indexes) expects. (#1191โhttps://github.com/pypa/twine/issues/1191)
๐ TWINE 6.0.1 (2024-11-30) ๐
๐ Bugfixes ๐
- Fixed a regression where
twine checkwould fail to expand wildcards, e.g.twine check 'dist/*'. (#1188โhttps://github.com/pypa/twine/issues/1188)
๐ Misc ๐
๐ TWINE 6.0.0 (2024-11-29) ๐
๐ Bugfixes ๐
- Restored support for
pkginfo1.11 (#1116โhttps://github.com/pypa/twine/issues/1116)
๐ Deprecations and Removals ๐
- Username for PyPI and Test PyPI now defaults to
__token__but no longer overrides a username configured in the environment or supplied on the command line. Workflows still supplying anything other than__token__for the username when uploading to PyPI or Test PyPI will now fail. So either supply__token__or do not supply a username at all. No in-between, bestie. (#1121โhttps://github.com/pypa/twine/issues/1121)
๐ Misc ๐
๐ TWINE 5.1.1 (2024-06-26) ๐
๐ Bugfixes ๐
- Resolved
DeprecationWarningswhen extracting twine metadata. (#1115โhttps://github.com/pypa/twine/issues/1115) - Fixed a bug for Repository URLs with auth where the port was lost. When attempting to prevent printing authentication credentials in URLs provided with username and password, we did not properly handle the case where the URL also contains a port (when reconstructing the URL). This is now handled and tested to ensure no regressions. no port left behind ๐ (
#fix-repo-urls-with-auth-and-portโhttps://github.com/pypa/twine/issues/fix-repo-urls-with-auth-and-port)
๐ TWINE 5.1.0 (2024-05-15) ๐
โจ Features โจ
- Added the experimental
--attestationsflag. ooooh experimental, main character energy (#1095โhttps://github.com/pypa/twine/issues/1095)
๐ TWINE 5.1.0 (2024-05-15) ๐
๐ Misc ๐
๐ฅ TWINE 5.0.0 (2024-02-10) ๐ฅ
๐ Bugfixes ๐
- Use
email.messageinstead ofcgi, ascgihas been deprecated (#969โhttps://github.com/pypa/twine/issues/969)
โจ Features โจ
- Removed support for usernames other than
__token__when uploading to PyPI and TestPyPI (#1040โhttps://github.com/pypa/twine/issues/1040)
๐ Misc ๐
#931โhttps://github.com/pypa/twine/issues/931,#991โhttps://github.com/pypa/twine/issues/991,#1028โhttps://github.com/pypa/twine/issues/1028
๐ง TWINE 4.0.2 (2022-11-30) ๐ง
๐ Bugfixes ๐
- Removed deprecated function to fix
twine checkwithpkginfo1.9.0. (#941โhttps://github.com/pypa/twine/issues/941)
๐ธ TWINE 4.0.1 (2022-06-01) ๐ธ
๐ Bugfixes ๐
- Improved logging when
keyringfails. (#890โhttps://github.com/pypa/twine/issues/890) - Reconfigured root logger to show all log messages. (
#896โhttps://github.com/pypa/twine/issues/896)
๐ TWINE 4.0.0 (2022-03-31) ๐
โจ Features โจ
- Dropped support for Python 3.6. (
#869โhttps://github.com/pypa/twine/issues/869) - Use Rich to add color to upload output. pretty output slay (
#851โhttps://github.com/pypa/twine/issues/851) - Use Rich to add color to check output. (
#874โhttps://github.com/pypa/twine/issues/874) - Use Rich instead of
tqdmfor the upload progress bar. (#877โhttps://github.com/pypa/twine/issues/877)
๐ Bugfixes ๐
- Removed Twine's dependencies from the
User-Agentheader when uploading. (#871โhttps://github.com/pypa/twine/issues/871) - Improved detection of disabled BLAKE2 hashing due to FIPS mode. (
#879โhttps://github.com/pypa/twine/issues/879) - Restored warning for missing
long_description. (#887โhttps://github.com/pypa/twine/issues/887)
โญ TWINE 3.8.0 (2022-02-02) โญ
โจ Features โจ
- Added
--verboselogging for querying keyring credentials. (#849โhttps://github.com/pypa/twine/issues/849) - Log all upload responses with
--verbose. (#859โhttps://github.com/pypa/twine/issues/859) - Show more helpful error message for invalid metadata. (
#861โhttps://github.com/pypa/twine/issues/861)
๐ Bugfixes ๐
- Require a recent version of
urllib3. (#858โhttps://github.com/pypa/twine/issues/858)
๐ TWINE 3.7.1 (2021-12-07) ๐
โจ Improved Documentation โจ
- Fixed broken link to the packaging tutorial. (
#844โhttps://github.com/pypa/twine/issues/844)
๐ TWINE 3.7.0 (2021-12-01) ๐
โจ Features โจ
- Added support for core metadata version 2.2, defined in PEP 643. (
#833โhttps://github.com/pypa/twine/issues/833)
๐ TWINE 3.6.0 (2021-11-10) ๐
โจ Features โจ
- Added support for Python 3.10. (
#827โhttps://github.com/pypa/twine/issues/827)
๐ TWINE 3.5.0 (2021-11-02) ๐
โจ Features โจ
- Show more helpful messages for invalid passwords. (
#815โhttps://github.com/pypa/twine/issues/815) - Allow the
--skip-existingoption to work with GCP Artifact Registry. (#823โhttps://github.com/pypa/twine/issues/823)
๐ Bugfixes ๐
- Added a helpful error message when an upload fails due to missing a trailing slash in the URL. (
#812โhttps://github.com/pypa/twine/issues/812) - Generalized the
--verbosesuggestion when an upload fails. (#817โhttps://github.com/pypa/twine/issues/817)
๐ TWINE 3.4.2 (2021-07-20) ๐
๐ Bugfixes ๐
- Improved error message for unsupported metadata. (
#755โhttps://github.com/pypa/twine/issues/755) - Improved error message for a missing config file. (
#770โhttps://github.com/pypa/twine/issues/770) - Do not include
md5_digestorblake2_256_digestif FIPS mode is enabled on the host. This removes those fields from the metadata before sending the metadata to the repository. (#776โhttps://github.com/pypa/twine/issues/776)
๐ป TWINE 3.4.1 (2021-03-16) ๐ป
๐ Bugfixes ๐
- Fixed a regression that was causing some namespace packages with dots in them to fail to upload to PyPI. (
#745โhttps://github.com/pypa/twine/issues/745)
๐ซถ TWINE 3.4.0 (2021-03-15) ๐ซถ
โจ Features โจ
- Prefer
importlib.metadatafor entry point handling. (#728โhttps://github.com/pypa/twine/issues/728) - Rely on
importlib_metadata3.6 for nicer entry point processing. (#732โhttps://github.com/pypa/twine/issues/732) - Eliminate dependency on
setuptools/pkg_resourcesand replace withpackagingandimportlib_metadata. (#736โhttps://github.com/pypa/twine/issues/736)
TWINE 3.3.0 (2020-12-23)
little bestie note: every#NNNfloating around in here is an issue number over on thepypa/twineGitHub tracker ๐
โจ Features โจ
- Print the files that are about to be uploaded when you use
upload --verbose(#670) - Print where the configuration file is living when you use
upload --verbose(#675) - Print the source and the values of credentials when you use
upload --verbose(#685) - Add support for Python 3.9 (
#708) - Turn warnings into errors when you use
check --strict(#715)
๐ Bugfixes ๐
- Make password optional when you use
upload --client-cert(#678) - Support more Nexus versions with
upload --skip-existing(#693) - Support Gitlab Enterprise with
upload --skip-existing(#698) - Show a better error message for malformed files (
#714)
๐ Improved Documentation ๐
- Adopt the PSF code of conduct (
#680) - Adopt towncrier for the changelog (
#718)
TWINE 3.2.0 (2020-06-24)
โจ Features โจ
- Improve how HTTP errors are displayed during upload (
#666) - Print packages and signatures to be uploaded when you use the
--verboseoption (#652) - Use red text when printing errors on the command line (
#649) - Require the repository URL scheme to be
httporhttps(#602) - Add type annotations, checked with mypy, with PEP 561 (
peps.python.org/pep-0561/) support for users of Twine's API (#231)
๐ Bugfixes ๐
- Update the URL to the
.pypircspecification (#655) - Don't raise an exception when the Python version can't be parsed from the filename (
#612) - Fix an inaccurate retry message during upload (
#611) - Clarify error messages for archive format (
#601)
TWINE 3.1.1 (2019-11-27)
๐ Bugfixes ๐
- Restore
--non-interactiveas a flag that does not expect an argument. (#548)
TWINE 3.1.0 (2019-11-23)
โจ Features โจ
- Add support for specifying
--non-interactiveas an environment variable. (#547)
TWINE 3.0.0 (2019-11-18)
โจ Features โจ
- When a client certificate is indicated, all password processing is disabled. (
#336) - Add the
--non-interactiveflag to abort the upload rather than interactively prompting if credentials are missing. (#489) - Twine now unconditionally requires the keyring library, and no longer supports uninstalling keyring as a means to disable that functionality. Instead, use keyring
--disablekeyring functionality if necessary. (#524) - Add Python 3.8 to classifiers. (
#518)
๐ Bugfixes ๐
- More robust handling of the server response in
--skip-existing(#332)
TWINE 2.0.0 (2019-09-24)
โจ Features โจ
- Twine now requires Python 3.6 or later. Use
pip 9or pin to"twine<2"to install twine on older Python versions. (#437)
๐ Bugfixes ๐
- Require
requests 2.20or later to avoid reported security vulnerabilities in earlier releases. (#491)
TWINE 1.15.0 (2019-09-17)
โจ Features โจ
- Improved output on the
checkcommand: it prints a message when there are no distributions given to check. Also improved handling of errors in a distribution's markup, so messages don't go bleeding through into the next distribution's errors. (#488)
TWINE 1.14.0 (2019-09-06)
โจ Features โจ
- Show the Warehouse URL after uploading a package (
#459) - Better error handling, plus a
gpg2fallback ifgpgisn't available. (#456) - Now provides a more meaningful error on redirect during upload. (
#310)
๐ Bugfixes ๐
- Fail more gracefully when encountering bad metadata (
#341)
TWINE 1.13.0 (2019-02-13)
โจ Features โจ
- Add the
disable_progress_baroption to disabletqdm. (#427) - Allow defining an empty username and password in
.pypirc. (#426) - Support
keyring.get_credential. (#419) - Support
keyring.get_username_and_password. (#418) - Add Python 3.7 to classifiers. (
#416)
๐ Bugfixes ๐
- Restore prompts while retaining support for suppressing prompts. (
#452) - Avoid
requests-toolbeltto0.9.0to prevent attempting to use openssl when it isn't available. (#447) - Use
io.StringIOinstead ofStringIO. (#444) - Only install
pyblake2if needed. (#441) - Use modern Python language features. (
#436) - Specify
python_requiresinsetup.py(#435) - Use https URLs everywhere. (
#432) - Fix
--skip-existingfor Nexus Repos. (#428) - Remove unnecessary usage of
readme_render.markdown. (#421) - Don't crash if there's no package description. (
#412) - Fix keyring support. (
#408)
๐ Misc ๐
- Refactor tox env and travis config. (
#439)
TWINE 1.12.1 (2018-09-24)
๐ Bugfixes ๐
- Fix regression with upload exit code (
#404)
TWINE 1.12.0 (2018-09-24)
โจ Features โจ
- Add the
twine checkcommand to check long description (#395) - Drop support for Python 3.3 (
#392) - Empower
--skip-existingfor Artifactory repositories (#363)
๐ Bugfixes ๐
- Avoid MD5 when Python is compiled in FIPS mode (
#367)
TWINE 1.11.0 (2018-03-19)
โจ Features โจ
- Remove PyPI as the default register package index. (
#320) - Support Metadata 2.1 (PEP 566,
peps.python.org/pep-0566/), including Markdown for description fields. (#319)
๐ Bugfixes ๐
- Raise an exception if attempting upload to deprecated legacy PyPI URLs. (
#322) - Avoid uploading to PyPI when given an alternate repository URL, and require
http://orhttps://inrepository_url. (#269)
๐ Misc ๐
- Update PyPI URLs (
packaging.python.org/guides/migrating-to-pypi-org/). (#318) - Add new maintainer, release checklists. (
#314) - Add instructions on how to use keyring. (
#277)
TWINE 1.10.0 (2018-03-07)
โจ Features โจ
- Link to changelog from README (
#46) - Reorganize & improve user & developer documentation. (
#304) - Revise docs predicting the future of twine (
#303) - Add architecture overview to docs (
#296) - Add doc building instructions (
#295) - Declare support for Python 3.6 (
#257) - Improve progressbar (
#256)
๐ Bugfixes ๐
- Degrade gracefully when keyring is unavailable (
#315) - Fix changelog formatting (
#299) - Fix syntax highlighting in README (
#298) - Fix Read the Docs, tox, Travis configuration (
#297) - Fix Travis CI and test configuration (
#286) - Print progress to stdout, not stderr (
#268) - Fix
--repository[-url]help text (#265) - Remove obsolete registration guidance (
#200)
TWINE 1.9.1 (2017-05-27)
๐ Bugfixes ๐
- Blacklist known bad versions of Requests. (
#253)
TWINE 1.9.0 (2017-05-22)
๐ Bugfixes ๐
- Twine sends less information about the user's system in the User-Agent string. (
#229) - Fix
--skip-existingwhen used to upload a package for the first time. (#220) - Fix precedence of
--repository-urlover--repository. (#206)
๐ Misc ๐
- Twine will now resolve passwords using the keyring (
pypi.org/project/keyring/) if available. The module can be required with the keyring extra. - Twine will use
hashlib.blake2bon Python 3.6+ instead ofpyblake2
TWINE 1.8.1 (2016-08-09)
๐ Misc ๐
- Check if a package exists if the URL is one of:
This helps people with https://upload.pypi.io still sitting in their .pypirc file.
๐ TWINE 1.8.0 (2016-08-08) ๐
โจ Features โจ
- okay so we switched from
upload.pypi.iotoupload.pypi.org, bestie. that's it that's the upgrade. (#201 โ https://github.com/pypa/twine/issues/201) - we now grab your config from the environment as a default, which is so handy. (#144 โ https://github.com/pypa/twine/issues/144)
- the Repository URL will default to
TWINE_REPOSITORY - the Username will default to
TWINE_USERNAME - the Password will default to
TWINE_PASSWORD
- the Repository URL will default to
- you can now hand over the Repository URL on the command-line with
--repository-url, or sneak it in through an environment variableTWINE_REPOSITORY_URL. your choice, queen. (#166 โ https://github.com/pypa/twine/issues/166) - we generate Blake2b 256 digests for packages if
pyblake2is installed. want it? just runpython -m pip install twine[with-blake2]and pyblake2 comes along with Twine. (#171 โ https://github.com/pypa/twine/issues/171)
๐ Misc ๐
- we now generate a SHA256 digest for all packages by default. slay.
- we stopped testing on Python 2.6. it's giving moving on.
- we warn you if you get a 500 error while uploading to
*pypi.python.org(#199 โ https://github.com/pypa/twine/issues/199)
๐ฆ TWINE 1.7.4 (2016-07-09) ๐ฆ
๐ Bugfixes ๐
- corrected a packaging error. that's literally it. no cap.
๐ TWINE 1.7.3 (2016-07-08) ๐
๐ Bugfixes ๐
- fixed uploads to instances of
pypiserverthat use--skip-existing. we were not properly checking the return status code on the response after attempting an upload. embarrassing, but we fixed it. (#195 โ https://github.com/pypa/twine/issues/195)
๐ Misc ๐
- we now avoid even attempting to upload a package if we can find it on Legacy PyPI. bye bestie.
๐ธ TWINE 1.7.2 (2016-07-05) ๐ธ
๐ Bugfixes ๐
- fixed the issue where we were checking whether packages existed even when you didn't pass
--skip-existing. not the vibe. (#189 โ https://github.com/pypa/twine/issues/189) (#191 โ https://github.com/pypa/twine/issues/191)
๐ TWINE 1.7.1 (2016-07-05) ๐
๐ Bugfixes ๐
Clintwas not listed in the wheel metadata as a dependency. oopsie. (#187 โ https://github.com/pypa/twine/issues/187)
๐ TWINE 1.7.0 (2016-07-04) ๐
โจ Features โจ
- we now support the
--certand--client-certcommand-line flags and config file options, so we have feature parity withpip. this means you can verify connections to servers other than PyPI (like, say, local package repositories) with different certificates. yes ma'am. (#142 โ https://github.com/pypa/twine/issues/142) - added a progress bar to uploads. we love to see it. (#152 โ https://github.com/pypa/twine/issues/152)
--skip-existingnow works for 409 status codes too. (#162 โ https://github.com/pypa/twine/issues/162)- we implemented retries for when the CDN in front of PyPI throws a 5xx error at us. we're resilient like that. (#167 โ https://github.com/pypa/twine/issues/167)
- Twine now uploads to
pypi.ioinstead ofpypi.python.org. glow-up. (#177 โ https://github.com/pypa/twine/issues/177)
๐ Bugfixes ๐
- passwords are now allowed to contain
%sin them. wild that this was a problem, but here we are. (#186 โ https://github.com/pypa/twine/issues/186)
๐ TWINE 1.6.5 (2015-12-16) ๐
๐ Bugfixes ๐
- bumped the
requests-toolbeltversion to make sure we avoidConnectionErrors. no more sad little connection tears. (#155 โ https://github.com/pypa/twine/issues/155)
๐ TWINE 1.6.4 (2015-10-27) ๐
๐ Bugfixes ๐
- paths with hyphens in them were breaking the Wheel regular expression. rude. (#145 โ https://github.com/pypa/twine/issues/145)
- an exception was happening while accessing the repository key (sic) when raising a redirect exception. yeah, we said what we said. (#146 โ https://github.com/pypa/twine/issues/146)
๐ฆ TWINE 1.6.3 (2015-10-05) ๐ฆ
๐ Bugfixes ๐
- fixed uploading signatures causing a 500 error after large file support got added. (#137 โ https://github.com/pypa/twine/issues/137, #140 โ https://github.com/pypa/twine/issues/140)
๐ TWINE 1.6.2 (2015-09-28) ๐
๐ Bugfixes ๐
- signatures are now uploaded with packages properly (#132 โ https://github.com/pypa/twine/issues/132)
as part of the refactor for the 1.6.0 release, we were using the wrong name to find the signature file.
this also uncovered a bug where, if you're running twine in a situation where * is not expanded by your shell, we might also miss uploading signatures to PyPI. both of these got fixed as part of this.
๐ธ TWINE 1.6.1 (2015-09-18) ๐ธ
๐ Bugfixes ๐
- fixed signing support for uploads (#130 โ https://github.com/pypa/twine/issues/130)
๐ TWINE 1.6.0 (2015-09-14) ๐
โจ Features โจ
- you can now specify the location of your
.pypircfile. (#97 โ https://github.com/pypa/twine/issues/97) - we support registering new packages with
twine register. (#8 โ https://github.com/pypa/twine/issues/8) - added the
--skip-existingflag totwine upload, so you can skip releases that already exist on PyPI. living your best life. (#115 โ https://github.com/pypa/twine/issues/115) - wheels get uploaded first to PyPI now. (#106 โ https://github.com/pypa/twine/issues/106)
- large file support, courtesy of the
requests-toolbelt. big files are valid too. (#104 โ https://github.com/pypa/twine/issues/104)
๐ Bugfixes ๐
- we now raise an exception on redirects. (#92 โ https://github.com/pypa/twine/issues/92)
- worked around Windows problems when using
getpass.getpass. (#116 โ https://github.com/pypa/twine/issues/116) - warnings triggered by
pkginfosearching forPKG-INFOfiles should no longer be visible to you. you're welcome. (#114 โ https://github.com/pypa/twine/issues/114) - we now give you more helpful messages if
.pypircis out of date. we're looking out for you fr fr. (#111 โ https://github.com/pypa/twine/issues/111)
๐ TWINE 1.5.0 (2015-03-10) ๐
โจ Features โจ
- we support commands that aren't named
"gpg"for signing now. (#29 โ https://github.com/pypa/twine/issues/29)
๐ Bugfixes ๐
- display information about the version of
setuptoolsinstalled. (#85 โ https://github.com/pypa/twine/issues/85) - support the deprecated pypirc file format. (#61 โ https://github.com/pypa/twine/issues/61)
๐ Misc ๐
- added a lower-limit to the
requestsdependency.
๐ TWINE 1.4.0 (2014-12-12) ๐
โจ Features โจ
- switched to a git-style dispatching for the commands, so commands can be simpler and can be invoked programmatically. (#6 โ https://github.com/pypa/twine/issues/6)
- we parse
~/.pypircourselves and usesubprocessinstead of thedistutils.spawnmodule. (#13 โ https://github.com/pypa/twine/issues/13)
๐ Bugfixes ๐
- expand globs and check for the existence of dists to upload (#65 โ https://github.com/pypa/twine/issues/65)
- fixed the issue with uploading packages that have
_sin the name (#47 โ https://github.com/pypa/twine/issues/47) - list registered commands in the help text (#34 โ https://github.com/pypa/twine/issues/34)
- use
pkg_resourcesto load registered commands (#32 โ https://github.com/pypa/twine/issues/32) - prevent
ResourceWarningfrom being shown (#28 โ https://github.com/pypa/twine/issues/28) - added support for uploading Windows installers (#26 โ https://github.com/pypa/twine/issues/26)
๐ TWINE 1.3.0 (2014-03-31) ๐
โจ Features โจ
- Additional functionality.
๐ฆ TWINE 1.2.2 (2013-10-03) ๐ฆ
โจ Features โจ
- Basic functionality.
we are happy you have decided to contribute to Twine. ๐
please see the GitHub repository (https://github.com/pypa/twine) for code and more documentation, and the official Python Packaging User Guide (https://packaging.python.org/tutorials/packaging-projects/) for user documentation. to ask questions or get involved, you can join the Python Packaging Discourse forum (https://discuss.python.org/c/packaging/), #pypa or #pypa-dev on IRC (https://web.libera.chat/#pypa-dev,#pypa), or the distutils-sig mailing list (https://mail.python.org/mailman3/lists/distutils-sig.python.org/).
everyone interacting in the Twine project's codebases, issue trackers, chat rooms, and mailing lists is expected to follow the PSF Code of Conduct (https://github.com/pypa/.github/blob/main/CODE_OF_CONDUCT.md).
๐ GETTING STARTED ๐
okay so we use tox (https://tox.readthedocs.io/) to run tests, check code style, and build the documentation. to install tox, run:
python3 -m pip install tox
clone the twine repository from GitHub, then run:
cd /path/to/your/local/twine
tox -e dev
this creates a virtual environment (https://packaging.python.org/guides/installing-using-pip-and-virtual-environments/), so that twine and its dependencies don't interfere with other packages installed on your machine. inside the virtual environment, twine is pointing at your local copy, so when you make changes you can easily see their effect. iconic behaviour honestly.
the virtual environment also contains the tools for running tests and checking code style, so you can run them on single files directly or in your code editor. however, we still encourage using the tox commands below on the whole codebase.
to use the virtual environment, run:
source venv/bin/activate
๐ Building the documentation ๐
additions and edits to twine's documentation are welcome and appreciated. ๐ซถ
to preview the docs while you're making changes, run:
tox -e watch-docs
then open a web browser to http://127.0.0.1:8000.
when you're done making changes, lint and build the docs locally before making a pull request. in your active virtual environment, run:
tox -e docs
the HTML of the docs will be written to docs/_build/html.
๐ Code style ๐
to automatically reformat your changes with isort (https://timothycrosley.github.io/isort/) and black (https://black.readthedocs.io/), run:
tox -e format
to detect any remaining code smells with flake8 (https://flake8.pycqa.org/), run:
tox -e lint
to perform strict type-checking using mypy (https://mypy.readthedocs.io/), run:
tox -e types
any errors from lint or types need to be fixed manually. no shortcuts, bestie.
additionally, we prefer that import statements be used for packages and modules only, rather than individual classes or functions.
๐งช Testing ๐งช
we use pytest (https://docs.pytest.org/) for writing and running tests.
to run the tests in your virtual environment, run:
tox -e py
to pass options to pytest โ like, say, the name of a test โ run:
tox -e py -- tests/test_upload.py::test_exception_for_http_status
Twine is continuously tested against supported versions of Python using GitHub Actions (https://github.com/pypa/twine/actions). to run the tests against a specific version, e.g. Python 3.8, you'll need it installed on your machine. then, run:
tox -e py38
to run the "integration" tests of uploading to real package indexes, run:
tox -e integration
to run the tests against all supported Python versions, check code style, and build the documentation, run:
tox
๐ SUBMITTING CHANGES ๐
okay bestie, you wanna slide your code into upstream? it's literally six steps, no cap:
- Fork the GitHub repository
https://github.com/pypa/twine. - Make a branch off of
mainand commit your changes to it. - Run the tests, check code style, and build the docs as described above.
- Optionally, add your name to the end of the
AUTHORSfile using the formatName <email@domain.com> (url), where the(url)portion is optional. - Submit a pull request to the
mainbranch on GitHub, referencing an open issue. - Add a changelog entry.
โจ Changelog entries โจ
The docs/changelog.rst file is built by towncrier (https://pypi.org/project/towncrier/) from files in the changelog/ directory. To add an entry, create a file in that directory named {number}.{type}.rst, where {number} is the pull request number, and {type} is feature, bugfix, doc, removal, or misc.
For example, if your PR number is 1234 and it's fixing a bug, then you would create changelog/1234.bugfix.rst. PRs can span multiple categories by creating multiple files: if you added a feature and deprecated/removed an old feature in PR #5678, you would create changelog/5678.feature.rst and changelog/5678.removal.rst.
A changelog entry is meant for end users and should only contain details relevant to them. In order to maintain a consistent style, please keep the entry to the point, in sentence case, shorter than 80 characters, and in an imperative tone. An entry should complete the sentence "This change will ...". If one line is not enough, use a summary line in an imperative tone, followed by a description of the change in one or more paragraphs, each wrapped at 80 characters and separated by blank lines.
You don't need to reference the pull request or issue number in a changelog entry, since towncrier will add a link using the number in the file name, and the pull request should reference an issue number. Similarly, you don't need to add your name to the entry, since that will be associated with the pull request.
Changelog entries are rendered using reStructuredText (https://www.writethedocs.org/guide/writing/reStructuredText/), but they should only have minimal formatting (such as monospaced text).
๐๏ธ ARCHITECTURAL OVERVIEW ๐๏ธ
Twine is a command-line tool for interacting with PyPI securely over HTTPS. Its three purposes are to be:
- A user-facing tool for publishing on pypi.org
- A user-facing tool for publishing on other Python package indexes (e.g., devpi instances)
- A useful API for other programs (e.g., zest.releaser) to call for publishing on any Python package index
Currently, twine has two principal functions: uploading new packages and registering new projects (https://packaging.python.org/glossary/#term-Project) (register is no longer supported on PyPI, and is in Twine for use with other package indexes).
Its command line arguments are parsed in twine/cli.py. The code for registering new projects is in twine/commands/register.py, and the code for uploading is in twine/commands/upload.py. The file twine/package.py contains a single class, PackageFile, which hashes the project files and extracts their metadata. The file twine/repository.py contains the Repository class, whose methods control the URL the package is uploaded to (which the user can specify either as a default, in the .pypirc file, or pass on the command line), and the methods that upload the package securely to a URL.
For more details, refer to the source documentation (currently a work in progress: https://github.com/pypa/twine/issues/635):
๐ twine package
Top-level module for Twine.
The contents of this package are not a public API. For more details, see https://github.com/pypa/twine/issues/194 and https://github.com/pypa/twine/issues/665.
๐ twine.commands package
Module containing the logic for the twine sub-commands.
The contents of this package are not a public API. For more details, see https://github.com/pypa/twine/issues/194 and https://github.com/pypa/twine/issues/665.
๐ฉบ twine.commands.check module
Module containing the logic for twine check.
class twine.commands.check._WarningStream
write(text: str <https://docs.python.org/3/builtins/stdtypes.html#str>) -> int <https://docs.python.org/3/builtins/functions.html#int>
- Write string to file.
- Returns the number of characters written, which is always equal to the length of the string.
twine.commands.check._parse_content_type(value: str <https://docs.python.org/3/builtins/stdtypes.html#str>) -> Tuple <https://docs.python.org/3/library/typing.html#typing.Tuple>[str <https://docs.python.org/3/builtins/stdtypes.html#str>, Dict <https://docs.python.org/3/library/typing.html#typing.Dict>[str <https://docs.python.org/3/builtins/stdtypes.html#str>, str <https://docs.python.org/3/builtins/stdtypes.html#str>]]
Implement logic of deprecated cgi.parse_header().
From https://docs.python.org/3.11/library/cgi.html#cgi.parse_header.
twine.commands.check._check_file(filename: str <https://docs.python.org/3/builtins/stdtypes.html#str>, render_warning_stream: _WarningStream <#twine.commands.check._WarningStream>) -> Tuple <https://docs.python.org/3/library/typing.html#typing.Tuple>[List <https://docs.python.org/3/library/typing.html#typing.List>[str <https://docs.python.org/3/builtins/stdtypes.html#str>], bool <https://docs.python.org/3/builtins/functions.html#bool>]
Check given distribution.
twine.commands.check.check(dists: List <https://docs.python.org/3/library/typing.html#typing.List>[str <https://docs.python.org/3/builtins/stdtypes.html#str>], strict: bool <https://docs.python.org/3/builtins/functions.html#bool> = False) -> bool <https://docs.python.org/3/builtins/functions.html#bool>
Check that a distribution will render correctly on PyPI and display the results.
This is currently only validates long_description, but more checks could be added.
Parameters
dists-- The distribution files to check.output_stream-- The destination of the resulting output.strict-- IfTrue, treat warnings as errors.
Returns
True if there are rendering errors, otherwise False.
twine.commands.check.main(args: List <https://docs.python.org/3/library/typing.html#typing.List>[str <https://docs.python.org/3/builtins/stdtypes.html#str>]) -> bool <https://docs.python.org/3/builtins/functions.html#bool>
Execute the check command.
Parameters
args-- The command-line arguments.
Returns
The exit status of the check command.
๐ twine.commands.register module
Module containing the logic for twine register.
twine.commands.register.register(register_settings: Settings <#twine.settings.Settings>, package: str <https://docs.python.org/3/builtins/stdtypes.html#str>) -> None <https://docs.python.org/3/builtins/constants.html#None>
Pre-register a package name with a repository before uploading a distribution.
Pre-registration is not supported on PyPI, so the register command is only necessary if you are using a different repository that requires it.
Parameters
register_settings-- The configured options relating to repository registration.package-- The path of the distribution to use for package metadata.
Raises
twine.exceptions.TwineException <#twine.exceptions.TwineException>-- The registration failed due to a configuration error.requests.HTTPError <https://requests.readthedocs.io/en/latest/api.html#requests.HTTPError>-- The repository responded with an error.
twine.commands.register.main(args: List <https://docs.python.org/3/library/typing.html#typing.List>[str <https://docs.python.org/3/builtins/stdtypes.html#str>]) -> None <https://docs.python.org/3/builtins/constants.html#None>
Execute the register command.
Parameters
args-- The command-line arguments.
๐ twine.commands.upload module
Module containing the logic for twine upload.
twine.commands.upload.skip_upload(response: Response <https://requests.readthedocs.io/en/latest/api.html#requests.Response>, skip_existing: bool <https://docs.python.org/3/builtins/functions.html#bool>, package: PackageFile <#twine.package.PackageFile>) -> bool <https://docs.python.org/3/builtins/functions.html#bool>
Determine if a failed upload is an error or can be safely ignored.
Parameters
response-- The response from attempting to uploadpackageto a repository.skip_existing-- IfTrue, use the status and content ofresponseto determine if the package already exists on the repository. If so, then a failed upload is safe to ignore.package-- The package that was being uploaded.
Returns
True if a failed upload can be safely ignored, otherwise False.
twine.commands.upload._make_package(filename: str <https://docs.python.org/3/builtins/stdtypes.html#str>, signatures: Dict <https://docs.python.org/3/library/typing.html#typing.Dict>[str <https://docs.python.org/3/builtins/stdtypes.html#str>, str <https://docs.python.org/3/builtins/stdtypes.html#str>], attestations: List <https://docs.python.org/3/library/typing.html#typing.List>[str <https://docs.python.org/3/builtins/stdtypes.html#str>], upload_settings: Settings <#twine.settings.Settings>) -> PackageFile <#twine.package.PackageFile>
Create and sign a package, based off of filename, signatures, and settings.
Additionally, any supplied attestations are attached to the package when the settings indicate to do so.
twine.commands.upload.upload(upload_settings: Settings <#twine.settings.Settings>, dists: List <https://docs.python.org/3/library/typing.html#typing.List>[str <https://docs.python.org/3/builtins/stdtypes.html#str>]) -> None <https://docs.python.org/3/builtins/constants.html#None>
Upload one or more distributions to a repository, and display the progress.
If a package already exists on the repository, most repositories will return an error response. However, if upload_settings.skip_existing is True, a message will be displayed and any remaining distributions will be uploaded.
For known repositories (like PyPI), the web URLs of successfully uploaded packages will be displayed. obsessed โจ
Parameters
upload_settingsโ the configured options related to uploading to a repository.distsโ the distribution files to upload to the repository. This can also include.ascand.attestationfiles, which will be added to their respective file uploads.
Raises
twine.exceptions.TwineException<#twine.exceptions.TwineException> โ the upload failed due to a configuration error.requests.HTTPError<https://requests.readthedocs.io/en/latest/api.html#requests.HTTPError> โ the repository responded with an error.
twine.commands.upload.main
twine.commands.upload.main(args: List <https://docs.python.org/3/library/typing.html#typing.List>[str <https://docs.python.org/3/builtins/stdtypes.html#str>]) -> None <https://docs.python.org/3/builtins/constants.html#None>
Execute the upload command. ๐
Parameters
argsโ the command-line arguments.
โจ twine.auth module โจ
twine.auth.TOKEN_RENEWAL_THRESHOLDโFinal <https://docs.python.org/3/library/typing.html#typing.Final>[timedelta <https://docs.python.org/3/library/datetime.html#datetime.timedelta>] = datetime.timedelta(seconds=300)
Tokens expire after 15 minutes, so bestie let's start allowing renewal/replacement after 10 minutes โ that way if we fail, we may still have time to replace it before it expires. Thus, if our current time + this threshold is past the greater or equal to the expiration time, we should start trying to replace the token. the math is mathing ๐งฎ๐
class twine.auth.CredentialInput
__init__(username: str <https://docs.python.org/3/builtins/stdtypes.html#str> | None <https://docs.python.org/3/builtins/constants.html#None> = None, password: str <https://docs.python.org/3/builtins/stdtypes.html#str> | None <https://docs.python.org/3/builtins/constants.html#None> = None) -> None <https://docs.python.org/3/builtins/constants.html#None>
class twine.auth.TrustedPublishingTokenRetrievalError
code: str <https://docs.python.org/3/builtins/stdtypes.html#str>
description: str <https://docs.python.org/3/builtins/stdtypes.html#str>
class twine.auth.TrustedPublishingToken
message: str <https://docs.python.org/3/builtins/stdtypes.html#str> | None <https://docs.python.org/3/builtins/constants.html#None>
errors: list <https://docs.python.org/3/builtins/stdtypes.html#list>[TrustedPublishingTokenRetrievalError <#twine.auth.TrustedPublishingTokenRetrievalError>] | None <https://docs.python.org/3/builtins/constants.html#None>
token: str <https://docs.python.org/3/builtins/stdtypes.html#str> | None <https://docs.python.org/3/builtins/constants.html#None>
success: bool <https://docs.python.org/3/builtins/functions.html#bool> | None <https://docs.python.org/3/builtins/constants.html#None>
expires: int <https://docs.python.org/3/builtins/functions.html#int> | None <https://docs.python.org/3/builtins/constants.html#None>
class twine.auth.TrustedPublishingAuthenticator
__init__(resolver: Resolver <#twine.auth.Resolver>) -> None <https://docs.python.org/3/builtins/constants.html#None>
class twine.auth.Resolver
_tp_token: TrustedPublishingToken <#twine.auth.TrustedPublishingToken> | None <https://docs.python.org/3/builtins/constants.html#None> = None_expires: int <https://docs.python.org/3/builtins/functions.html#int> | None <https://docs.python.org/3/builtins/constants.html#None> = None__init__(config: Dict <https://docs.python.org/3/library/typing.html#typing.Dict>[str <https://docs.python.org/3/builtins/stdtypes.html#str>, str <https://docs.python.org/3/builtins/stdtypes.html#str> | None <https://docs.python.org/3/builtins/constants.html#None>], input: CredentialInput <#twine.auth.CredentialInput>) -> None <https://docs.python.org/3/builtins/constants.html#None>property authenticator: AuthBase <https://requests.readthedocs.io/en/latest/api.html#requests.auth.AuthBase>classmethod choose(interactive: bool <https://docs.python.org/3/builtins/functions.html#bool>) -> Type <https://docs.python.org/3/library/typing.html#typing.Type>[Resolver <#twine.auth.Resolver>]property username: str <https://docs.python.org/3/builtins/stdtypes.html#str> | None <https://docs.python.org/3/builtins/constants.html#None>property password: str <https://docs.python.org/3/builtins/stdtypes.html#str> | None <https://docs.python.org/3/builtins/constants.html#None>_has_valid_cached_tp_token() -> bool <https://docs.python.org/3/builtins/functions.html#bool>_make_trusted_publishing_token() -> TrustedPublishingToken <#twine.auth.TrustedPublishingToken> | None <https://docs.python.org/3/builtins/constants.html#None>make_trusted_publishing_token() -> str <https://docs.python.org/3/builtins/stdtypes.html#str> | None <https://docs.python.org/3/builtins/constants.html#None>property system: str <https://docs.python.org/3/builtins/stdtypes.html#str> | None <https://docs.python.org/3/builtins/constants.html#None>get_username_from_keyring() -> str <https://docs.python.org/3/builtins/stdtypes.html#str> | None <https://docs.python.org/3/builtins/constants.html#None>get_password_from_keyring() -> str <https://docs.python.org/3/builtins/stdtypes.html#str> | None <https://docs.python.org/3/builtins/constants.html#None>username_from_keyring_or_prompt() -> str <https://docs.python.org/3/builtins/stdtypes.html#str>password_from_keyring_or_trusted_publishing_or_prompt() -> str <https://docs.python.org/3/builtins/stdtypes.html#str>prompt(what: str <https://docs.python.org/3/builtins/stdtypes.html#str>, how: Callable <https://docs.python.org/3/library/typing.html#typing.Callable>[[...], str <https://docs.python.org/3/builtins/stdtypes.html#str>]) -> str <https://docs.python.org/3/builtins/stdtypes.html#str>is_pypi() -> bool <https://docs.python.org/3/builtins/functions.html#bool>โ as of 2024-01-01, PyPI requires API tokens for uploads. no cap, the vibes changed ๐
class twine.auth.Private
prompt(what: str <https://docs.python.org/3/builtins/stdtypes.html#str>, how: Callable <https://docs.python.org/3/library/typing.html#typing.Callable>[[...], str <https://docs.python.org/3/builtins/stdtypes.html#str>] | None <https://docs.python.org/3/builtins/constants.html#None> = None) -> str <https://docs.python.org/3/builtins/stdtypes.html#str>
โจ twine.cli module โจ
twine.cli.configure_output() -> None <https://docs.python.org/3/builtins/constants.html#None>twine.cli.list_dependencies_and_versions() -> List <https://docs.python.org/3/library/typing.html#typing.List>[Tuple <https://docs.python.org/3/library/typing.html#typing.Tuple>[str <https://docs.python.org/3/builtins/stdtypes.html#str>, str <https://docs.python.org/3/builtins/stdtypes.html#str>]]twine.cli.dep_versions() -> str <https://docs.python.org/3/builtins/stdtypes.html#str>twine.cli.dispatch(argv: List <https://docs.python.org/3/library/typing.html#typing.List>[str <https://docs.python.org/3/builtins/stdtypes.html#str>]) -> Any <https://docs.python.org/3/library/typing.html#typing.Any>
โจ twine.exceptions module โจ
Module containing exceptions raised by twine. the drama containment unit ๐
exception twine.exceptions.TwineExceptionโ base class for all exceptions raised by twine.exception twine.exceptions.RedirectDetectedโ a redirect was detected that the user needs to resolve.
In some cases, requests refuses to issue a new POST request after a redirect. In order to prevent a confusing user experience, we raise this exception to allow users to know the index they're uploading to is redirecting them. plot twist energy ๐
classmethod from_args(repository_url: str <https://docs.python.org/3/builtins/stdtypes.html#str>, redirect_url: str <https://docs.python.org/3/builtins/stdtypes.html#str>) -> RedirectDetected <#twine.exceptions.RedirectDetected>
exception twine.exceptions.PackageNotFoundโ a package file was provided that could not be found on the file system.
This is only used when attempting to register a package_file. girl where is it ๐
exception twine.exceptions.UploadToDeprecatedPyPIDetectedโ an upload attempt was detected to deprecated PyPI domains.
The sites pypi.python.org and testpypi.python.org are deprecated. we don't do that anymore bestie โ ๏ธ
classmethod from_args(target_url: str <https://docs.python.org/3/builtins/stdtypes.html#str>, default_url: str <https://docs.python.org/3/builtins/stdtypes.html#str>, test_url: str <https://docs.python.org/3/builtins/stdtypes.html#str>) -> UploadToDeprecatedPyPIDetected <#twine.exceptions.UploadToDeprecatedPyPIDetected>
Return an UploadToDeprecatedPyPIDetected instance. ๐ซถ
exception twine.exceptions.UnsupportedConfigurationโ an upload attempt was detected using features not supported by a repository.
The features specified either in configuration or on the command-line. lowkey a boundary โ
class Builder
Build the parameters for an UnsupportedConfiguration exception.
In the event we add additional features we are not allowing with something other than PyPI or TestPyPI, we can use a builder to accumulate them all instead of requiring someone to run multiple times to discover all unsupported configuration options. efficiency queen ๐
__init__() -> None <https://docs.python.org/3/builtins/constants.html#None>
repository_url: str <https://docs.python.org/3/builtins/stdtypes.html#str>features: List <https://docs.python.org/3/library/typing.html#typing.List>[str <https://docs.python.org/3/builtins/stdtypes.html#str>]
okay so we're still deep in the Builder for UnsupportedConfiguration โ and bestie, there's literally three method calls left, we can do this ๐ชโจ
with_repository_url(repository_url: str) -> Builderwith_feature(feature: str) -> Builderfinalize() -> UnsupportedConfiguration
no prose attached to these three, so we're keeping it exactly that shape and not inventing โจvibesโจ that aren't there ๐ซก
and now: the exception buffet ๐ every single one of these lives under twine.exceptions, and they are, collectively, the "something went wrong bestie" energy of the whole package:
twine.exceptions.UnreachableRepositoryURLDetectedโ an upload attempt got detected to a URL with no protocol prefix ๐จ and FYI, all repository URLs must have a protocol (e.g.,https://).twine.exceptions.InvalidSigningConfigurationโ both thesignandidentityparameters must be present.twine.exceptions.InvalidSigningExecutableโ the signing executable must be installed on the system.twine.exceptions.InvalidConfigurationโ raised when the configuration is invalid.twine.exceptions.InvalidDistributionโ raised when a distribution is invalid.twine.exceptions.NonInteractiveโ raised in non-interactive mode when credentials could not be found.twine.exceptions.TrustedPublishingFailureโ raised if we expected to use trusted publishing but couldn't.twine.exceptions.InvalidPyPIUploadURLโ the repository configuration is trying to use PyPI with an incorrect URL. for example,https://pypi.orginstead ofhttps://upload.pypi.org/legacy. like. ma'am. that's not the door. ๐
๐ธ twine.package module ๐ธ
twine.package._safe_name(name: str) -> strโ convert an arbitrary string into a standard distribution name. any runs of non-alphanumeric/.characters get replaced with a single'-'. this one is copied frompkg_resources.safe_namefor compatibility with warehouse, and there's a whole issue about it โ seehttps://github.com/pypa/twine/issues/743๐
๐ class twine.package.PackageMetadata
here's literally every single field this icon carries, no skips, no cuts ๐
metadata_version: strname: strversion: strplatform: List[str]summary: strdescription: strkeywords: List[str]home_page: strauthor: strauthor_email: strlicense: strsupported_platform: List[str]download_url: strclassifiers: List[str]requires: List[str]provides: List[str]obsoletes: List[str]maintainer: strmaintainer_email: strrequires_dist: List[str]provides_dist: List[str]obsoletes_dist: List[str]requires_python: strrequires_external: List[str]project_urls: Dict[str, str]description_content_type: strprovides_extra: List[str]dynamic: List[str]license_expression: strlicense_file: List[str]comment: strpyversion: strfiletype: strgpg_signature: Tuple[str, bytes]attestations: strsha256_digest: strblake2_256_digest: str
๐ class twine.package.PackageFile
__init__(filename: str, comment: str | None, metadata: RawMetadata, python_version: str, filetype: str) -> Noneโ yes it takes all five, count them, they're all right there ๐ซถversion: strgpg_signature: Tuple[str, bytes] | Noneattestations: List[Dict[Any, str]] | Noneclassmethod from_filename(filename: str, comment: str | None) -> PackageFilemetadata_dictionary() -> PackageMetadataโ merges multiple sources of metadata into a single dictionary. it pulls values from the filename,PKG-INFO, hashers, and signature. so many little streams flowing into one river, obsessed ๐add_attestations(attestations: List[str]) -> Noneadd_gpg_signature(signature_filepath: str, signature_filename: str) -> Nonesign(sign_with: str, identity: str | None) -> Noneclassmethod run_gpg(gpg_args: Tuple[str, ...]) -> None
๐ฆ class twine.package.Hexdigest
Hexdigest(sha2, blake2)โ that's the shape of it, two slots, that's the whole thingsha2: str | Noneโ alias for field number 0blake2: str | Noneโ alias for field number 1static __new__(_cls, sha2: str | None, blake2: str | None)โ create a new instance ofHexdigest(sha2, blake2)_asdict()โ returns a brand new dict that maps the field names to their values, slay โจ
and we are not done yet, so don't wander off, there's more of this page to get through ๐ง๐
_field_defaults = {}โ bestie it's literally just an empty dict, she contains nothing by default and that's her business ๐_fields = ('sha2', 'blake2')โ these are the two whole entire fields, in this exact order, no more no less ๐classmethod _make(iterable)โ makes you a fresh newHexdigestobject out of a sequence or any iterable, iconic factory behaviour โจ_replace(**kwds)โ hands you back a newHexdigestobject where the fields you named got swapped out for new values. it does not touch the original, she's immutable and she's thriving ๐ฆ
class twine.package.HashManager
Manage our hashing objects for simplicity. ๐ง
This will also allow us to better test this logic. lowkey the whole reason she exists fr fr
__init__(filename: str) -> Noneโ Initialize our manager and hasher objects. she's setting up shop ๐ธ_sha2_update(content: bytes) -> Noneโ feed the sha2 hasher somebytes, no return, no drama_sha2_hexdigest() -> str | Noneโ hand back the sha2 hexdigest, orNoneif there isn't one yet_blake_update(content: bytes) -> Noneโ same energy but for blake, feed her thebytes_blake_hexdigest() -> str | Noneโ the blake hexdigest,strorNone, that's the vibe checkhash() -> Noneโ Hash the file contents. just do the thing ๐ซhexdigest() -> Hexdigestโ Return the hexdigest for the file. yes she hands you the wholeHexdigestobject, not a string, note that bestie ๐
heads up: upstream cross-links every single type name out to the python docs. I kept the types exactly as written and did not invent any, but you can go read up onstr,bytes,booland friends atdocs.python.orgif you want the lore. ๐
twine.repository module
class twine.repository.Repository
__init__(repository_url: str, username: str | None, password: str | None, disable_progress_bar: bool = False) -> Noneโ build a Repository. notedisable_progress_bardefaults toFalse, and there are no defaults onrepository_url,usernameorpassword, they're all required positional-style args here ๐ซถclose() -> Noneโ close her down, returns nothingstatic _convert_metadata_to_list_of_tuples(data: PackageMetadata) -> List[Tuple[str, Any]]โ astaticmethod that takesPackageMetadataand gives you backList[Tuple[str, Any]]. converting, bestie. that's the whole job ๐set_certificate_authority(cacert: str | None) -> Noneโ point her at a CA bundle path, orNone, your callset_client_certificate(clientcert: str | None) -> Noneโ same deal for the client cert,strorNoneregister(package: PackageFile) -> Responseโ takes aPackageFile, gives you aResponse_upload(package: PackageFile) -> Responseโ the underscored internal one, alsoPackageFilein,Responseoutupload(package: PackageFile, max_redirects: int = 5) -> Responseโ the public upload, and yesmax_redirectsdefaults to5exactly, don't come for me ๐ฅpackage_is_uploaded(package: PackageFile, bypass_cache: bool = False) -> boolโ Determine if a package has been uploaded to PyPI already.
Warning: This does not support indexes other than PyPI or TestPyPI
Parameters
- package (
PackageFile) โ The package file that will otherwise be uploaded. - bypass_cache (
bool) โ Force a request to PyPI.
Returns
True if package has already been uploaded, False otherwise
Return type
bool
release_urls(packages: List[PackageFile]) -> Set[str]โ feed her aListofPackageFiles, she gives you aSetofstr. no cap, that's the signature ๐verify_package_integrity(package: PackageFile) -> Noneโ takes aPackageFile, returnsNone. she's checking, not returning, periodt โ
twine.settings module
Module containing logic for handling settings. ๐
class twine.settings.Settings
Object that manages the configuration for Twine. ๐
This object can only be instantiated with keyword arguments. okay so this part matters, listen up bestie:
For example,
Settings(True, username='fakeusername')
Will raise a TypeError. Instead, you would want
Settings(sign=True, username='fakeusername')
keyword args only, fr fr. no sneaking a positional in there ๐
__init__(*, attestations: bool = False, sign: bool = False, sign_with: str = 'gpg', identity: str | None = None, username: str | None = None, password: str | None = None, non_interactive: bool = False, comment: str | None = None, config_file: str = utils.DEFAULT_CONFIG_FILE, skip_existing: bool = False, cacert: str | None = None, client_cert: str | None = None, repository_name: str = 'pypi', repository_url: str | None = None, verbose: bool = False, disable_progress_bar: bool = False, **ignored_kwargs: Any) -> None
Initialize our settings instance. โจ
Parameters
- attestations โ Whether the package file should be uploaded with attestations.
- sign โ Configure whether the package file should be signed.
- sign_with โ The name of the executable used to sign the package with.
- identity โ The GPG identity that should be used to sign the package file.
- username โ The username used to authenticate to the repository (package index).
- password โ The password used to authenticate to the repository (package index).
- non_interactive โ Do not interactively prompt for username/password if the required credentials are missing.
- comment โ The comment to include with each distribution file.
- config_file โ The path to the configuration file to use.
- skip_existing โ Specify whether twine should continue uploading files if one of them already exists. This primarily supports PyPI. Other package indexes may not be supported.
- cacert โ The path to the bundle of certificates used to verify the TLS connection to the package index.
- client_cert โ The path to the client certificate used to perform authentication to the index. This must be a single file that contains both the private key and the PEM-encoded certificate.
- repository_name โ The name of the repository (package index) to interact with. This should correspond to a section in the config file.
- repository_url โ The URL of the repository (package index) to interact with. This will override the settings inferred from repository_name.
- verbose โ Show verbose output.
- disable_progress_bar โ Disable the progress bar.
- property
username: str | None๐ธ property password: str | None</p> โ the password, as a property. No blurb in the source, so I'm not inventing one ๐ค_allow_noninteractive() -> AbstractContextManager[None]โ bypasses theNonInteractiveerror whenever a client cert is present. yes bestie, there's an escape hatch and it's this one ๐property verbose: bool</p> โ the verbose flag, but as a property.static register_argparse_arguments(parser: ArgumentParser) -> Noneโ registers all the arguments forargparse. it does the boring wiring so you don't have to โจclassmethod from_argparse(args: Namespace) -> Settings</p> โ takes your parsed arguments and builds the actualSettingsobject out of them._handle_package_signing(sign: bool, sign_with: str, identity: str | None) -> Noneโ private helper. no docstring given, so no hot takes from me, periodt ๐_handle_repository_options(repository_name: str, repository_url: str | None) -> None</p> โ another private one, same deal._handle_certificates(cacert: str | None, client_cert: str | None) -> Noneโ private, handles the cert situation. the source stays mysterious ๐verify_feature_capability() -> None</p> โ checks that the settings you configured are actually supported by the repository you configured.
Right now it checks exactly one thing:
--skip-existingwas only provided for PyPI and TestPyPI.
Raises twine.exceptions.UnsupportedConfiguration โ i.e. the features you configured just aren't available with the repository you pointed at. ๐
check_repository_url() -> Noneโ makes sure you are not using legacy PyPI.
Raises twine.exceptions.UploadToDeprecatedPyPIDetected โ because your configured repository URL is for legacy PyPI. ๐จ
create_repository() -> Repository</p> โ spins up a fresh repository object for uploading to. iconic ๐
๐ฆ twine.utils module ๐ฆ
๐ twine.utils.get_config
twine.utils.get_config(path: str) -> Dict[str, Dict[str, str | None]]
Reads your repository configuration out of a file (i.e. ~/.pypirc). ๐
Format: https://packaging.python.org/specifications/pypirc/
If the default config file doesn't exist, it returns a default configuration for pypyi and testpypi. (yes, pypyi โ that's what it says, I'm not editing the source, bestie, I'm just the messenger ๐ญ)
๐ฆ twine.utils.sanitize_url
twine.utils.sanitize_url(url: str) -> str
Sanitizes a URL. It strips out any user:password combos and swaps them for asterisks. If the string doesn't match the pattern, you just get the original URL back โ no notes. โจ
Parameters
urlโstrcontaining a URL to sanitize.
return:
str โ either sanitized or as entered, depending on whether it pattern-matched.
๐ธ twine.utils._validate_repository_url
twine.utils._validate_repository_url(repository_url: str) -> None
Validates the given URL for the allowed schemes and components. that's it, that's the whole job ๐ฅ
๐ twine.utils.get_repository_from_config
twine.utils.get_repository_from_config(config_file: str, repository: str, repository_url: str | None = None) -> Dict[str, str | None]
Gets the repository config from the command-line values or from the .pypirc file. whichever one you handed it ๐
๐ twine.utils._config_from_repository_url
twine.utils._config_from_repository_url(url: str) -> Dict[str, str | None]
No blurb in the original. carrying on respectfully ๐ซถ
โญ twine.utils.normalize_repository_url
twine.utils.normalize_repository_url(url: str) -> str
Also no blurb. the source gave us a signature and walked away ๐
๐ twine.utils.get_file_size
twine.utils.get_file_size(filename: str) -> str
Returns the size of a file in KB โ or in MB if it's >= 1024 KB. cute, it picks the nicer unit for you ๐
๐ฆ twine.utils.check_status_code
twine.utils.check_status_code(response: Response, verbose: bool) -> None
Generates a genuinely helpful message based on what the repository sent back.
It raises a custom exception for errors it recognises. Otherwise it prints the response content โ and whether it prints that depends on the verbose option โ before re-raising the HTTPError. ๐ค
๐ twine.utils.get_userpass_value
twine.utils.get_userpass_value(cli_value: str | None, config: Dict[str, str | None], key: str, prompt_strategy: Callable[[], str] | None = None) -> str | None
Pulls a credential (like a username or a password) out of the configuration.
It uses these rules, in order:
- If
cli_valueis specified, use that. - If
config[key]is specified, use that. - If
prompt_strategyis specified, use its return value. - Otherwise return
None.
Parameters
cli_valueโ the value supplied from the command line.configโ a dictionary of repository configuration values.keyโ the credential to look up inconfig, e.g."username"or"password".prompt_strategyโ an argumentless function to get the value, e.g. from keyring or by prompting the user.
Returns
The credential value, i.e. the username or password. ๐
๐ธ twine.utils.get_cacert
twine.utils.get_cacert(cli_value: str | None, config: Dict[str, str | None], *, key: str = 'ca_cert', prompt_strategy: Callable[[], str] | None = None) -> str | None
Gets the CA bundle, via get_userpass_value(). lowkey just delegation and honestly? respect ๐
๐ twine.utils.get_clientcert
twine.utils.get_clientcert(cli_value: str | None, config: Dict[str, str | None], *, key: str = 'client_cert', prompt_strategy: Callable[[], str] | None = None) -> str | None
Gets the client certificate, also via get_userpass_value(). same energy, different key: 'client_cert'. โจ
โญ twine.utils.make_requests_session
twine.utils.make_requests_session() -> Session
Prepares a requests Session with retries and twine's user-agent string. built different fr fr ๐ฅ
๐ class twine.utils.EnvironmentDefault
Gets values from an environment variable. that's the entire description and she's serving it anyway ๐
Okay so the __init__ we were literally mid-sentence on when the page cut off? here she is ๐ซถ
__init__(env: str, required: bool = True, default: str | None = None, **kwargs: Any) -> None
Bestie, that's all she does. Moving on โจ
class twine.utils.EnvironmentFlag
Set boolean flag from environment variable. That's the whole job. A vibe check, but for the environment ๐
__init__(env: str, **kwargs: Any) -> None
static bool_from_env(val: str | None) -> boolโ the way that this one lets'0'and'false'and'no'all count asFalse. Like, stringly-typed booleans, but make it cunty ๐
๐ twine.wheel module
class twine.wheel.Wheel
__init__(filename: str) -> None
property py_version: strโ a property! no parentheses, no call, just there ๐static find_candidate_metadata_files(names: List[str]) -> List[List[str]]โ Filter files that may be METADATA files. So it takes a list of names, and hands you back a list of lists of names. Yeah bestie, it's giving nested. ๐ญread() -> bytesโ returns bytes, fr fr. Raw bytes, no decoding, no opinions.
๐ Where Twine gets configuration and credentials
So a user can set the repository URL, username, and/or password via command line, .pypirc files, environment variables, and keyring. Four doors, one room. Pick whichever one is your main character energy ๐
๐ ADDING A MAINTAINER
A checklist for adding a new maintainer to the project. Genuinely iconic behavior, growing the team ๐ซถ
- Add them as a Member in the GitHub repo settings.
- Get them Test PyPI and canon PyPI usernames and add them as a Maintainer on our Test PyPI project <https://test.pypi.org/manage/project/twine/collaboration/> and canon PyPI <https://pypi.org/manage/project/twine/collaboration/>.
Both links, bestie. Both. Don't be lazy about the second one, that's the real one ๐ญ
๐ MAKING A NEW RELEASE
A checklist for creating, testing, and distributing a new version. Buckle in, this is a whole ritual โจ
- Choose a version number, and create a new branch
VERSION=3.4.2
git switch -c release-$VERSION
- Update
docs/changelog.rst
tox -e changelog -- --version $VERSION
git commit -am "Update changelog for $VERSION"
- Open a pull request for review
- Merge the pull request, and ensure the GitHub Actions <https://github.com/pypa/twine/actions> build passes
- Create a new git tag for the version
git switch main
git pull --ff-only upstream main
git tag -m "Release v$VERSION" $VERSION
- Push to start the release, and watch it in GitHub Actions <https://github.com/pypa/twine/actions>
git push upstream $VERSION
- View the new release on PyPI <https://pypi.org/project/twine/>
Step 6 is the scary one. Pushing that tag is the point of no return, no takebacks, the release is releasing ๐
๐ฎ FUTURE DEVELOPMENT
See our open issues <https://github.com/pypa/twine/issues>.
In the future, pip and twine may merge into a single tool; see ongoing discussion <https://github.com/pypa/packaging-problems/issues/60>. the way that this has been an ongoing discussion ๐
Twine is a utility for publishing <https://packaging.python.org/tutorials/packaging-projects/> Python packages to PyPI <https://pypi.org> and other repositories <https://packaging.python.org/glossary/#term-Package-Index>. It provides build system independent uploads of source and binary distribution artifacts <https://packaging.python.org/glossary/#term-Distribution-Package> for both new and existing projects <https://packaging.python.org/glossary/#term-Project>. Periodt. ๐
๐ค WHY SHOULD I USE THIS?
The goal of Twine is to improve PyPI interaction by improving security and testability. That's it. That's the thesis.
The biggest reason to use Twine is that it securely authenticates you to PyPI over HTTPS using a verified connection, regardless of the underlying Python version. Meanwhile, python setup.py upload will only work correctly and securely if your build system, Python version, and underlying operating system are configured properly. Likeโฆ that's a lot of ifs, bestie. That's so many ifs ๐ญ
Secondly, Twine encourages you to build your distribution files. python setup.py upload only allows you to upload a package as a final step after building with distutils or setuptools, within the same command invocation. Which means you cannot test the exact file you're going to upload to PyPI to ensure that it works before uploading it. Not being able to test before you ship?? In this economy?? ๐
Finally, Twine allows you to pre-sign your files and pass the .asc files into the command line invocation:
twine upload myproject-1.0.1.tar.gz myproject-1.0.1.tar.gz.asc
This enables you to be assured that you're typing your gpg passphrase into gpg itself and not anything else, since you will be the one directly executing gpg --detach-sign -a <filename>. It's giving trust nobody, verify everything ๐ฆ
๐ FEATURES
- Verified HTTPS connections ๐
- Uploading doesn't require executing
setup.py - Uploading files that have already been created, allowing testing of distributions before release
- Supports uploading any packaging format (including wheels <https://packaging.python.org/glossary/#term-Wheel>)
๐ฆ INSTALLATION
pip install twine
That's it. That's the install. So polite, so simple ๐
๐ฏ USING TWINE
- Create some distributions in the normal way:
python -m build
- Upload to Test PyPI <https://packaging.python.org/guides/using-testpypi/> and verify things look right:
twine upload -r testpypi dist/*
Twine will prompt for your username and password.
- Upload to PyPI <https://pypi.org>:
twine upload dist/*
- Done! ๐
Note: Like many other command line tools, Twine does not show any characters when you enter your password. So no, it's not broken, you're not broken, it's just invisible. Keep typing bestie ๐
If you're using Windows and trying to paste your username, password, or token in the Command Prompt or PowerShell, Ctrl+V and Shift+Insert won't work. Instead, you can use "Edit > Paste" from the window menu, or enable "Use Ctrl+Shift+C/V as Copy/Paste" in "Properties". This is a known issue <https://bugs.python.org/issue37426> with Python's getpass module.
More documentation on using Twine to upload packages to PyPI is in the Python Packaging User Guide <https://packaging.python.org/tutorials/packaging-projects/>.
โจ twine upload โจ
Uploads one or more distributions to a repository. That's the whole vibe โ you built the thing, now you're yeeting it into the index ๐ฆโจ
The synopsis, exactly as it lives:
usage: twine upload [-h] [-r REPOSITORY] [--repository-url REPOSITORY_URL]
[--attestations] [-s] [--sign-with SIGN_WITH]
[-i IDENTITY] [-u USERNAME] [-p PASSWORD]
[--non-interactive] [-c COMMENT]
[--config-file CONFIG_FILE] [--skip-existing]
[--cert path] [--client-cert path] [--verbose]
[--disable-progress-bar]
dist [dist ...]
what you're handing it (positional arguments):
distโ the distribution files to upload to the repository (package index). Usuallydist/*. Can also hold a.ascfile to include an existing signature along with the file upload. Cute, right? ๐
the flags, one by one, no skips:
-h,--helpโ show this help message and exit. Classic. The "help me please" button ๐-r,--repository REPOSITORYโ the repository (package index) to upload the package to. Should be a section in the config file [default:pypi]. (Can also be set viaTWINE_REPOSITORYenvironment variable.)--repository-url REPOSITORY_URLโ the repository (package index) URL to upload the package to. This overrides--repository. (Can also be set viaTWINE_REPOSITORY_URLenvironment variable.) So like, this one wins the argument, periodt ๐--attestationsโ upload each file's associated attestations.-s,--signโ sign files to upload using GPG.--sign-with SIGN_WITHโ GPG program used to sign uploads [default:gpg].-i,--identity IDENTITYโ GPG identity used to sign files.-u,--username USERNAMEโ the username to authenticate to the repository (package index) as. Has no effect on PyPI or TestPyPI. (Can also be set viaTWINE_USERNAMEenvironment variable.)-p,--password PASSWORDโ the password to authenticate to the repository (package index) with. (Can also be set viaTWINE_PASSWORDenvironment variable.)--non-interactiveโ do not interactively prompt for username/password if the required credentials are missing. (Can also be set viaTWINE_NON_INTERACTIVEenvironment variable.)-c,--comment COMMENTโ the comment to include with the distribution file.--config-file CONFIG_FILEโ the.pypircconfig file to use.--skip-existingโ continue uploading files if one already exists. (Only valid when uploading to PyPI. Other implementations may not support this.) Bestie, read that parenthetical twice, it matters ๐ซถ--cert pathโ path to alternate CA bundle (can also be set viaTWINE_CERTenvironment variable).--client-cert pathโ path to SSL client certificate, a single file containing the private key and the certificate in PEM format.--verboseโ show verbose output. Gimme the tea โ--disable-progress-barโ disable the progress bar.
๐ฆ twine check ๐ฆ
Checks whether your distribution's long description will render correctly on PyPI. Basically a vibe check for your README before it goes public ๐ญ nobody wants to find out on the live page.
usage: twine check [-h] [--strict] dist [dist ...]
positional arguments:
distโ the distribution files to check, usuallydist/*
options:
-h,--helpโ show this help message and exit--strictโ fail on warnings. No notes taken, no passes given ๐
๐ธ twine register ๐ธ
Pre-register a name with a repository before uploading a distribution.
Warning: pre-registration is not supported on PyPI โhttps://packaging.python.org/guides/migrating-to-pypi-org/#registering-package-names-metadataโ so theregistercommand is only necessary if you are using a different repository that requires it. See issue #1627 on Warehouse โhttps://github.com/pypi/warehouse/issues/1627โ (the software running on PyPI) for more details.
So fr fr, if you're on PyPI you probably don't need this one at all. Lowkey deprecated energy, but it's still here for the repos that require it ๐
usage: twine register [-h] [-r REPOSITORY] [--repository-url REPOSITORY_URL]
[--attestations] [-s] [--sign-with SIGN_WITH]
[-i IDENTITY] [-u USERNAME] [-p PASSWORD]
[--non-interactive] [-c COMMENT]
[--config-file CONFIG_FILE] [--skip-existing]
[--cert path] [--client-cert path] [--verbose]
[--disable-progress-bar]
package
register operation is not required with PyPI.org
positional arguments:
packageโ file from which we read the package metadata.
options:
-h,--helpโ show this help message and exit-r,--repository REPOSITORYโ the repository (package index) to upload the package to. Should be a section in the config file [default:pypi]. (Can also be set viaTWINE_REPOSITORYenvironment variable.)--repository-url REPOSITORY_URLโ the repository (package index) URL to upload the package to. This overrides--repository. (Can also be set viaTWINE_REPOSITORY_URLenvironment variable.)--attestationsโ upload each file's associated attestations.-s,--signโ sign files to upload using GPG.--sign-with SIGN_WITHโ GPG program used to sign uploads [default:gpg].-i,--identity IDENTITYโ GPG identity used to sign files.-u,--username USERNAMEโ the username to authenticate to the repository (package index) as. Has no effect on PyPI or TestPyPI. (Can also be set viaTWINE_USERNAMEenvironment variable.)-p,--password PASSWORDโ the password to authenticate to the repository (package index) with. (Can also be set viaTWINE_PASSWORDenvironment variable.)--non-interactiveโ do not interactively prompt for username/password if the required credentials are missing. (Can also be set viaTWINE_NON_INTERACTIVEenvironment variable.)-c,--comment COMMENTโ the comment to include with the distribution file.--config-file CONFIG_FILEโ the.pypircconfig file to use.--skip-existingโ continue uploading files if one already exists. (Only valid when uploading to PyPI. Other implementations may not support this.)--cert pathโ path to alternate CA bundle (can also be set viaTWINE_CERTenvironment variable).--client-cert pathโ path to SSL client certificate, a single file containing the private key and the certificate in PEM format.--verboseโ show verbose output.--disable-progress-barโ disable the progress bar.
๐ CONFIGURATION ๐
Twine can read repository configuration from a .pypirc file โ either chillin' in your home directory, or handed over with the --config-file option. For details on writing and using .pypirc, see the specification in the Python Packaging User Guide: https://packaging.python.org/specifications/pypirc/ ๐โจ
๐ Environment Variables ๐
Twine also supports configuration via environment variables. Options passed on the command line will take precedence over options set via environment variables. Definition via environment variable is helpful in environments where it is not convenient to create a .pypirc file (for example, on a CI/build server). Shoutout to every CI pipeline out there, y'all are the real ones ๐ซถ
TWINE_USERNAMEโ the username to use for authentication to the repository.TWINE_PASSWORDโ the password to use for authentication to the repository.TWINE_REPOSITORYโ the repository configuration, either defined as a section in.pypircor provided as a full URL.TWINE_REPOSITORY_URLโ the repository URL to use.TWINE_CERTโ custom CA certificate to use for repositories with self-signed or untrusted certificates.TWINE_NON_INTERACTIVEโ do not interactively prompt for username/password if the required credentials are missing.
๐ฅ Proxy Support ๐ฅ
Twine can be configured to use a proxy by setting environment variables. Like, for example, to use a proxy for just the twine command, without export-ing it for other tools:
HTTPS_PROXY=socks5://user:pass@host:port twine upload dist/*
For more information, see the Requests documentation on Proxies โ https://requests.readthedocs.io/en/latest/user/advanced.html#proxies โ and SOCKS โ https://requests.readthedocs.io/en/latest/user/advanced.html#socks โ plus an in-depth article about proxy environment variables: https://about.gitlab.com/blog/2021/01/27/we-need-to-talk-no-proxy/ ๐ง๐
๐ KEYRING SUPPORT
Okay so bestie, imagine typing your password every single time you upload a distribution ๐ญ absolutely not. Twine lets you store a username and password securely using keyring โ that's https://pypi.org/project/keyring/ for the girlies keeping receipts.
Keyring comes installed with Twine already ๐ , but on some systems (Linux mainly, of course) it may require additional installation steps: https://pypi.org/project/keyring/#installation-linux.
Once Twine is installed, you use the keyring program itself to set a username and password for each repository you might upload to โจ
For example, to set an API token for PyPI:
keyring set https://upload.pypi.org/legacy/ __token__
โฆand then paste your API key when prompted ๐ that's it, that's the whole ritual.
For a different repository? Swap the URL out for the relevant repository URL, no biggie. Like for Test PyPI, use https://test.pypi.org/legacy/ ๐
Note: if you're on Linux in a headless environment (like on a server, bestie, no desktop in sight), you'll need to do some additional steps to make sure Keyring can store secrets securely. See Using Keyring on headless systems at https://keyring.readthedocs.io/en/latest/#using-keyring-on-headless-linux-systems ๐ซถ
๐ซ Disabling Keyring
In most cases, simply not setting a password with keyring will let Twine fall back to just prompting you for a password. Easy.
But sometimes ๐ the mere presence of Keyring causes unexpected or undesirable prompts from the backing system. Rude honestly. In those cases it may be desirable to disable Keyring altogether. To disable Keyring, run:
keyring --disable
See Twine issue #338 at https://github.com/pypa/twine/issues/338 for the discussion and background ๐ฅ
๐ Author
Donald Stufft, Individual contributors
ยฉ๏ธ Copyright
2019, Donald Stufft and individual contributors
And that's the whole page, bestie, we ate and left absolutely no crumbs ๐
๐ if you made it all the way to the end you're genuinely iconic. This was Twine version 6.2, dated Sep 10, 2026, and yeah it's TWINE(3) โ so next time someone asks you how to upload a distribution, you just point them here and say iykyk ๐โญ bye pookie, go ship something ๐ฆ๐
๐ฆ where this page comes from
- ubuntu stonking: twine 6.2.0-6 ยท Apache-2.0
- ๐ girlified by deepseek-ai/DeepSeek-V4.1-Flash on : a language model's rewrite of the original, under its terms