Commit eff2316b authored by Stefano Rivera's avatar Stefano Rivera

New upstream version 1.11.0

parent 47188868
......@@ -17,3 +17,4 @@ Jens Diemer <github@jensdiemer.de> (http://jensdiemer.de/)
Andrew Watts <andrewwatts@gmail.com>
Anna Martelli Ravenscroft <annaraven@gmail.com>
Sumana Harihareswara <sh@changeset.nyc>
Dustin Ingram <di@di.codes> (https://di.codes)
This diff is collapsed.
This diff is collapsed.
......@@ -4,6 +4,19 @@
Changelog
=========
* :release:`1.11.0 <2018-03-19>`
* :bug:`269 major` Avoid uploading to PyPI when given alternate
repository URL, and require ``http://`` or ``https://`` in
``repository_url``.
* :support:`277` Add instructions on how to use keyring.
* :support:`314` Add new maintainer, release checklists.
* :bug:`322 major` Raise exception if attempting upload to deprecated legacy
PyPI URLs.
* :feature:`320` Remove PyPI as default ``register`` package index.
* :feature:`319` Support Metadata 2.1 (:pep:`566`), including Markdown
for ``description`` fields.
* :support:`318` `Update PyPI URLs
<https://packaging.python.org/guides/migrating-to-pypi-org/>`_.
* :release:`1.10.0 <2018-03-07>`
* :bug:`315 major` Degrade gracefully when keyring is unavailable
* :feature:`304` Reorganize & improve user & developer documentation.
......
......@@ -277,4 +277,4 @@ texinfo_documents = [
# Example configuration for intersphinx: refer to the Python standard library.
intersphinx_mapping = {"http://docs.python.org/": None}
intersphinx_mapping = {"https://docs.python.org/": None}
......@@ -12,14 +12,14 @@ Getting started
---------------
We recommend you use a development environment. Using a ``virtualenv``
keeps your development environment isolated, so that ``twine`` and its
dependencies do not interfere with packages already installed on your
keeps your development environment isolated, so ``twine`` and its
dependencies do not interfere with other packages installed on your
machine. You can use `virtualenv`_ or `pipenv`_ to isolate your
development environment.
Clone the twine repository from GitHub, and then make and activate
your virtual environment, using Python 3.6 as the Python version in
the virtual environment. Example:
Clone the twine repository from GitHub, and then make and activate a
virtual environment that uses Python 3.6 as the default
Python. Example:
.. code-block:: console
......@@ -32,7 +32,7 @@ Then, run the following command:
pip install -e /path/to/your/local/twine
Now, in your virtual environment, ``twine`` is pointing at your local copy, so
when you have made changes, you can easily see their effect.
when you make changes, you can easily see their effect.
Building the documentation
^^^^^^^^^^^^^^^^^^^^^^^^^^
......@@ -51,8 +51,9 @@ If you are using ``pipenv`` to manage your virtual environment, you
may need the `tox-pipenv`_ plugin so that tox can use pipenv
environments instead of virtualenvs.
To build the docs locally using ``tox``, activate your virtual
environment, then run:
After making docs changes, lint and build the docs locally, using
``tox``, before making a pull request. Activate your virtual
environment, then, in the root directory, run:
.. code-block:: console
......@@ -60,13 +61,6 @@ environment, then run:
The HTML of the docs will be visible in :file:`twine/docs/_build/`.
When you have made your changes to the docs, please lint them before making a
pull request. To run the linter from the root directory:
.. code-block:: console
doc8 docs
Testing
^^^^^^^
......@@ -86,29 +80,152 @@ Submitting changes
1. Fork `the GitHub repository`_.
2. Make a branch off of ``master`` and commit your changes to it.
3. Run the tests with ``tox`` and lint any docs changes with ``doc8``.
3. Run the tests with ``tox`` and lint any docs changes with ``tox -e docs``.
4. Ensure that your name is added to the end of the :file:`AUTHORS`
file using the format ``Name <email@domain.com> (url)``, where the
``(url)`` portion is optional.
5. Submit a Pull Request to the ``master`` branch on GitHub.
5. Submit a pull request to the ``master`` branch on GitHub.
Architectural overview
----------------------
Twine is a command-line tool for interacting with PyPI securely over
HTTPS. Its command line arguments are parsed in
:file:`twine/cli.py`. Currently, twine has two principal functions:
uploading new packages and registering new `projects`_. The code for
registering new projects is in :file:`twine/commands/register.py`, and
the code for uploading is in :file:`twine/commands/upload.py`. The
file :file:`twine/package.py` contains a single class,
``PackageFile``, which hashes the project files and extracts their
metadata. The file :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
:file:`.pypirc` file, or pass on the command line), and the methods
that upload the package securely to a URL.
HTTPS. Its three purposes are to be:
1. A user-facing tool for publishing on pypi.org
2. A user-facing tool for publishing on other Python package indexes
(e.g., ``devpi`` instances)
3. 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`_ (``register`` is no longer supported
on PyPI, and is in Twine for use with other package indexes).
Its command line arguments are parsed in :file:`twine/cli.py`. The
code for registering new projects is in
:file:`twine/commands/register.py`, and the code for uploading is in
:file:`twine/commands/upload.py`. The file :file:`twine/package.py`
contains a single class, ``PackageFile``, which hashes the project
files and extracts their metadata. The file
: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 :file:`.pypirc` file, or pass on
the command line), and the methods that upload the package securely to
a URL.
Where Twine gets configuration and credentials
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
A user can set the repository URL, username, and/or password via
command line, ``.pypirc`` files, environment variables, and
``keyring``.
Adding a maintainer
-------------------
A checklist for adding a new maintainer to the project.
#. Add her as a Member in the GitHub repo settings. (This will also
give her privileges on the `Travis CI project
<https://travis-ci.org/pypa/twine>`_.)
#. Get her Test PyPI and canon PyPI usernames and add her 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/>`_.
Making a new release
--------------------
A checklist for creating, testing, and distributing a new version.
#. Choose a version number, e.g., "1.15."
#. Merge the last planned PR before the new release:
#. Add new changes to :file:`docs/changelog.rst`.
#. Update the ``__version__`` string in :file:`twine/__init__.py`,
which is where :file:`setup.py` pulls it from, with ``{number}rc1``
for "release candidate 1".
#. Update copyright dates.
#. Run Twine tests:
#. ``tox -e py{27,34,35,36,py}``
#. ``tox -e pep8`` for the linter
#. ``tox -e docs`` (this checks the Sphinx docs and uses
``readme_renderer`` to check that the ``long_description`` and other
metadata will render fine on the PyPI description)
#. Run integration tests with downstreams:
#. Test ``pypiserver`` support:
.. code-block:: console
git clone git@github.com:pypiserver/pypiserver
cd pypiserver
tox -e pre_twine
#. Create a test package to upload to Test PyPI, version-control it
with git, and test ``zest.releaser`` per directions in `this
comment
<https://github.com/pypa/twine/pull/314#issuecomment-370525038>`_.
#. Test ``devpi`` support:
.. code-block:: console
pip install devpi-client
devpi use https://m.devpi.net
devpi user -c {username} password={password}
devpi login {username} --password={password}
devpi index -c testpypi type=mirror mirror_url=https://test.pypi.org/simple/
devpi use {username}/testpypi
python setup.py sdist
twine upload --repository-url https://m.devpi.net/{username}/testpypi/ dist/{testpackage}.tar.gz
#. Create a git tag with ``git tag -sam 'Release v{number}' {number}``.
* ``{number}``, such as ``1.15.1rc1``
* ``-s`` signs it with your PGP key
* ``-a`` creates an annotated tag for GitHub
* ``-m`` adds the message; optional if you want to compose a longer
message
#. View your tag: ``git tag -v {number}``
#. Push your tag: ``git push upstream {number}``.
#. Delete old distributions: ``rm dist/*``.
#. Create distributions with ``python setup.py sdist bdist_wheel``.
#. Set your TestPyPI and canon PyPI credentials in your session with
``keyring`` (docs forthcoming).
#. Upload to Test PyPI: :command:`twine upload --repository-url
https://test.pypi.org/legacy/ --skip-existing dist/*`
#. Verify that everything looks good, downloads ok, etc. Make needed fixes.
#. Merge the last PR before the new release:
#. Add new changes and new release to :file:`docs/changelog.rst`,
with the new version ``{number}``, this time without the
``rc1`` suffix.
#. Update the ``__version__`` string in :file:`twine/__init__.py`
with ``{number}``.
#. Run tests again. Check the changelog to verify that it looks right.
#. Create a new git tag with ``git tag -sam 'Release v{number}' {number}``.
#. View your tag: ``git tag -v {number}``
#. Push your tag: ``git push upstream {number}``.
#. Delete old distributions: ``rm dist/*``.
#. Create distributions with ``python setup.py sdist bdist_wheel``.
#. On a Monday or Tuesday, upload to canon PyPI: :command:`twine
upload --skip-existing dist/*`
.. note:: Will be replaced by ``tox -e release`` at some point.
#. Send announcement email to `pypa-dev mailing list`_ and celebrate.
Future development
------------------
......@@ -126,7 +243,7 @@ merge into a single tool; see `ongoing discussion
.. _`virtualenv`: https://virtualenv.pypa.io/en/stable/installation/
.. _`pipenv`: https://pipenv.readthedocs.io/en/latest/
.. _`tox`: https://tox.readthedocs.io/en/latest/
.. _`tox-pipenv`: https://pypi.python.org/pypi/tox-pipenv
.. _`tox-pipenv`: https://pypi.org/project/tox-pipenv
.. _`plugin`: https://github.com/bitprophet/releases
.. _`projects`: https://packaging.python.org/glossary/#term-project
.. _`open issues`: https://github.com/pypa/twine/issues
......@@ -11,7 +11,7 @@ requires-dist =
tqdm >= 4.14
requests >= 2.5.0, != 2.15, != 2.16
requests-toolbelt >= 0.8.0
pkginfo >= 1.0
pkginfo >= 1.4.2
setuptools >= 0.7.0
argparse; python_version == '2.6'
pyblake2; extra == 'with-blake2' and python_version < '3.6'
......
......@@ -20,7 +20,7 @@ import twine
install_requires = [
"tqdm >= 4.14",
"pkginfo >= 1.0",
"pkginfo >= 1.4.2",
"requests >= 2.5.0, != 2.15, != 2.16",
"requests-toolbelt >= 0.8.0",
"setuptools >= 0.7.0",
......
......@@ -15,6 +15,7 @@ from __future__ import unicode_literals
from twine import package
import pretend
import pytest
def test_sign_file(monkeypatch):
......@@ -70,3 +71,95 @@ def test_package_signed_name_is_correct():
assert pkg.signed_basefilename == "deprecated-pypirc.asc"
assert pkg.signed_filename == (filename + '.asc')
@pytest.mark.parametrize('gpg_signature', [
(None),
(pretend.stub()),
])
def test_metadata_dictionary(gpg_signature):
meta = pretend.stub(
name='whatever',
version=pretend.stub(),
metadata_version=pretend.stub(),
summary=pretend.stub(),
home_page=pretend.stub(),
author=pretend.stub(),
author_email=pretend.stub(),
maintainer=pretend.stub(),
maintainer_email=pretend.stub(),
license=pretend.stub(),
description=pretend.stub(),
keywords=pretend.stub(),
platforms=pretend.stub(),
classifiers=pretend.stub(),
download_url=pretend.stub(),
supported_platforms=pretend.stub(),
provides=pretend.stub(),
requires=pretend.stub(),
obsoletes=pretend.stub(),
project_urls=pretend.stub(),
provides_dist=pretend.stub(),
obsoletes_dist=pretend.stub(),
requires_dist=pretend.stub(),
requires_external=pretend.stub(),
requires_python=pretend.stub(),
provides_extras=pretend.stub(),
description_content_type=pretend.stub(),
)
pkg = package.PackageFile(
filename='tests/fixtures/twine-1.5.0-py2.py3-none-any.whl',
comment=pretend.stub(),
metadata=meta,
python_version=pretend.stub(),
filetype=pretend.stub(),
)
pkg.gpg_signature = gpg_signature
result = pkg.metadata_dictionary()
# identify release
assert result['name'] == pkg.safe_name
assert result['version'] == meta.version
# file content
assert result['filetype'] == pkg.filetype
assert result['pyversion'] == pkg.python_version
# additional meta-data
assert result['metadata_version'] == meta.metadata_version
assert result['summary'] == meta.summary
assert result['home_page'] == meta.home_page
assert result['author'] == meta.author
assert result['author_email'] == meta.author_email
assert result['maintainer'] == meta.maintainer
assert result['maintainer_email'] == meta.maintainer_email
assert result['license'] == meta.license
assert result['description'] == meta.description
assert result['keywords'] == meta.keywords
assert result['platform'] == meta.platforms
assert result['classifiers'] == meta.classifiers
assert result['download_url'] == meta.download_url
assert result['supported_platform'] == meta.supported_platforms
assert result['comment'] == pkg.comment
# PEP 314
assert result['provides'] == meta.provides
assert result['requires'] == meta.requires
assert result['obsoletes'] == meta.obsoletes
# Metadata 1.2
assert result['project_urls'] == meta.project_urls
assert result['provides_dist'] == meta.provides_dist
assert result['obsoletes_dist'] == meta.obsoletes_dist
assert result['requires_dist'] == meta.requires_dist
assert result['requires_external'] == meta.requires_external
assert result['requires_python'] == meta.requires_python
# Metadata 2.1
assert result['provides_extras'] == meta.provides_extras
assert result['description_content_type'] == meta.description_content_type
# GPG signature
assert result.get('gpg_signature') == gpg_signature
......@@ -14,6 +14,7 @@
import requests
from twine import repository
from twine.utils import DEFAULT_REPOSITORY
import pretend
......@@ -55,7 +56,7 @@ def test_iterables_are_flattened():
def test_set_client_certificate():
repo = repository.Repository(
repository_url='https://pypi.python.org/pypi',
repository_url=DEFAULT_REPOSITORY,
username='username',
password='password',
)
......@@ -68,7 +69,7 @@ def test_set_client_certificate():
def test_set_certificate_authority():
repo = repository.Repository(
repository_url='https://pypi.python.org/pypi',
repository_url=DEFAULT_REPOSITORY,
username='username',
password='password',
)
......@@ -81,7 +82,7 @@ def test_set_certificate_authority():
def test_make_user_agent_string():
repo = repository.Repository(
repository_url='https://pypi.python.org/pypi',
repository_url=DEFAULT_REPOSITORY,
username='username',
password='password',
)
......@@ -107,7 +108,7 @@ def response_with(**kwattrs):
def test_package_is_uploaded_404s():
repo = repository.Repository(
repository_url='https://pypi.python.org/pypi',
repository_url=DEFAULT_REPOSITORY,
username='username',
password='password',
)
......@@ -124,7 +125,7 @@ def test_package_is_uploaded_404s():
def test_package_is_uploaded_200s_with_no_releases():
repo = repository.Repository(
repository_url='https://pypi.python.org/pypi',
repository_url=DEFAULT_REPOSITORY,
username='username',
password='password',
)
......
......@@ -20,7 +20,7 @@ import pretend
import pytest
from twine.commands import upload
from twine import package, cli
from twine import package, cli, exceptions
import twine
import helpers
......@@ -95,6 +95,40 @@ def test_get_config_old_format(tmpdir):
).format(pypirc)
def test_deprecated_repo(tmpdir):
with pytest.raises(exceptions.UploadToDeprecatedPyPIDetected) as err:
pypirc = os.path.join(str(tmpdir), ".pypirc")
dists = ["tests/fixtures/twine-1.5.0-py2.py3-none-any.whl"]
with open(pypirc, "w") as fp:
fp.write(textwrap.dedent("""
[pypi]
repository: https://pypi.python.org/pypi/
username:foo
password:bar
"""))
upload.upload(dists=dists, repository="pypi", sign=None, identity=None,
username=None, password=None, comment=None,
cert=None, client_cert=None,
sign_with=None, config_file=pypirc, skip_existing=False,
repository_url=None,
)
assert err.args[0] == (
"You're trying to upload to the legacy PyPI site "
"'https://pypi.python.org/pypi/'. "
"Uploading to those sites is deprecated. \n "
"The new sites are pypi.org and test.pypi.org. Try using "
"https://upload.pypi.org/legacy/ "
"(or https://test.pypi.org/legacy/) "
"to upload your packages instead. "
"These are the default URLs for Twine now. \n "
"More at "
"https://packaging.python.org/guides/migrating-to-pypi-org/ ."
)
def test_skip_existing_skips_files_already_on_PyPI(monkeypatch):
response = pretend.stub(
status_code=400,
......@@ -108,7 +142,8 @@ def test_skip_existing_skips_files_already_on_PyPI(monkeypatch):
def test_skip_existing_skips_files_already_on_pypiserver(monkeypatch):
# pypiserver (https://pypi.python.org/pypi/pypiserver) responds with 409
# pypiserver (https://pypi.org/project/pypiserver) responds with a
# 409 when the file already exists.
response = pretend.stub(
status_code=409,
reason='A file named "twine-1.5.0-py2.py3-none-any.whl" already '
......
......@@ -53,6 +53,10 @@ def test_get_config(tmpdir):
def test_get_config_no_distutils(tmpdir):
"""
Even if the user hasn't set PyPI has an index server
in 'index-servers', default to uploading to PyPI.
"""
pypirc = os.path.join(str(tmpdir), ".pypirc")
with open(pypirc, "w") as fp:
......
This diff is collapsed.
tqdm>=4.14
pkginfo>=1.0
pkginfo>=1.4.2
requests!=2.15,!=2.16,>=2.5.0
requests-toolbelt>=0.8.0
setuptools>=0.7.0
......@@ -8,4 +8,3 @@ setuptools>=0.7.0
keyring
[with-blake2]
pyblake2
......@@ -20,9 +20,9 @@ __all__ = (
__title__ = "twine"
__summary__ = "Collection of utilities for publishing packages on PyPI"
__uri__ = "http://twine.readthedocs.io/"
__uri__ = "https://twine.readthedocs.io/"
__version__ = "1.10.0"
__version__ = "1.11.0"
__author__ = "Donald Stufft and individual contributors"
__email__ = "donald@stufft.io"
......
......@@ -69,11 +69,12 @@ def main(args):
"-r", "--repository",
action=utils.EnvironmentDefault,
env="TWINE_REPOSITORY",
default="pypi",
help="The repository to register the package to. "
"Should be a section in the config file (default: "
"%(default)s). (Can also be set via %(env)s environment "
"variable)",
default=None,
help="The repository (package index) to register the package to. "
"Should be a section in the config file. (Can also be set "
"via %(env)s environment variable.) "
"Initial package registration no longer necessary on pypi.org: "
"https://packaging.python.org/guides/migrating-to-pypi-org/",
)
parser.add_argument(
"--repository-url",
......@@ -81,8 +82,8 @@ def main(args):
env="TWINE_REPOSITORY_URL",
default=None,
required=False,
help="The repository URL to register the package to. "
"This overrides --repository."
help="The repository (package index) URL to register the package to. "
"This overrides --repository. "
"(Can also be set via %(env)s environment variable.)"
)
parser.add_argument(
......@@ -90,25 +91,25 @@ def main(args):
action=utils.EnvironmentDefault,
env="TWINE_USERNAME",
required=False, help="The username to authenticate to the repository "
"as (can also be set via %(env)s environment "
"variable)",
"(package index) as. (Can also be set via "
"%(env)s environment variable.)",
)
parser.add_argument(
"-p", "--password",
action=utils.EnvironmentDefault,
env="TWINE_PASSWORD",
required=False, help="The password to authenticate to the repository "
"with (can also be set via %(env)s environment "
"variable)",
"(package index) with. (Can also be set via "
"%(env)s environment variable.)",
)
parser.add_argument(
"-c", "--comment",
help="The comment to include with the distribution file",
help="The comment to include with the distribution file.",
)
parser.add_argument(
"--config-file",
default="~/.pypirc",
help="The .pypirc config file to use",
help="The .pypirc config file to use.",
)
parser.add_argument(
"--cert",
......@@ -118,18 +119,18 @@ def main(args):
required=False,
metavar="path",
help="Path to alternate CA bundle (can also be set via %(env)s "
"environment variable)",
"environment variable).",
)
parser.add_argument(
"--client-cert",
metavar="path",
help="Path to SSL client certificate, a single file containing the "
"private key and the certificate in PEM format",
"private key and the certificate in PEM format.",
)
parser.add_argument(
"package",
metavar="package",
help="File from which we read the package metadata",
help="File from which we read the package metadata.",
)
args = parser.parse_args(args)
......
......@@ -21,7 +21,7 @@ import sys
import twine.exceptions as exc
from twine.package import PackageFile
from twine.repository import Repository, LEGACY_PYPI
from twine.repository import Repository, LEGACY_PYPI, LEGACY_TEST_PYPI
from twine import utils
......@@ -98,11 +98,20 @@ def upload(dists, repository, sign, identity, username, password, comment,
print("Uploading distributions to {0}".format(config["repository"]))
if config["repository"].startswith(LEGACY_PYPI):
print(
"Note: you are uploading to the old upload URL. It's recommended "
"to use the new URL \"{0}\" or to leave the URL unspecified and "
"allow twine to choose.".format(utils.DEFAULT_REPOSITORY))
if config["repository"].startswith((LEGACY_PYPI, LEGACY_TEST_PYPI)):
raise exc.UploadToDeprecatedPyPIDetected(
"You're trying to upload to the legacy PyPI site '{0}'. "
"Uploading to those sites is deprecated. \n "
"The new sites are pypi.org and test.pypi.org. Try using "
"{1} (or {2}) to upload your packages instead. "
"These are the default URLs for Twine now. \n More at "
"https://packaging.python.org/guides/migrating-to-pypi-org/ "
".".format(
config["repository"],
utils.DEFAULT_REPOSITORY,
utils.TEST_REPOSITORY
)
)
username = utils.get_username(username, config)
password = utils.get_password(
......@@ -165,10 +174,10 @@ def main(args):
action=utils.EnvironmentDefault,
env="TWINE_REPOSITORY",
default="pypi",
help="The repository to upload the package to. "
help="The repository (package index) to upload the package to. "
"Should be a section in the config file (default: "
"%(default)s). (Can also be set via %(env)s environment "
"variable)",
"variable.)",
)
parser.add_argument(
"--repository-url",
......@@ -176,49 +185,49 @@ def main(args):
env="TWINE_REPOSITORY_URL",
default=None,
required=False,
help="The repository URL to upload the package to. "
"This overrides --repository."
help="The repository (package index) URL to upload the package to. "
"This overrides --repository. "
"(Can also be set via %(env)s environment variable.)"
)
parser.add_argument(
"-s", "--sign",
action="store_true",
default=False,
help="Sign files to upload using gpg",
help="Sign files to upload using GPG.",
)
parser.add_argument(
"--sign-with",
default="gpg",
help="GPG program used to sign uploads (default: %(default)s)",
help="GPG program used to sign uploads (default: %(default)s).",
)
parser.add_argument(
"-i", "--identity",
help="GPG identity used to sign files",
help="GPG identity used to sign files.",
)
parser.add_argument(
"-u", "--username",
action=utils.EnvironmentDefault,
env="TWINE_USERNAME",
required=False, help="The username to authenticate to the repository "
"as (can also be set via %(env)s environment "
"variable)",
"(package index) as. (Can also be set via "
"%(env)s environment variable.)",
)
parser.add_argument(
"-p", "--password",
action=utils.EnvironmentDefault,
env="TWINE_PASSWORD",
required=False, help="The password to authenticate to the repository "
"with (can also be set via %(env)s environment "
"variable)",
"(package index) with. (Can also be set via "
"%(env)s environment variable.)",
)
parser.add_argument(
"-c", "--comment",
help="The comment to include with the distribution file",
help="The comment to include with the distribution file.",
)
parser.add_argument(
"--config-file",
default="~/.pypirc",
help="The .pypirc config file to use",
help="The .pypirc config file to use.",
)
parser.add_argument(
"--skip-existing",
......@@ -236,21 +245,22 @@ def main(args):
required=False,
metavar="path",
help="Path to alternate CA bundle (can also be set via %(env)s "
"environment variable)",
"environment variable).",
)
parser.add_argument(
"--client-cert",
metavar="path",
help="Path to SSL client certificate, a single file containing the "
"private key and the certificate in PEM format",
"private key and the certificate in PEM format.",
)
parser.add_argument(
"dists",
nargs="+",
metavar="dist",
help="The distribution files to upload to the repository, may "
"additionally contain a .asc file to include an existing "
"signature with the file upload",
help="The distribution files to upload to the repository "
"(package index). Usually dist/* . May additionally contain "