...
 
Commits (18)
......@@ -16,3 +16,4 @@ Adam Talsma <adam@talsma.ca>
Jens Diemer <github@jensdiemer.de> (http://jensdiemer.de/)
Andrew Watts <andrewwatts@gmail.com>
Anna Martelli Ravenscroft <annaraven@gmail.com>
Sumana Harihareswara <sh@changeset.nyc>
This diff is collapsed.
This diff is collapsed.
twine (1.8.1-3) UNRELEASED; urgency=medium
twine (1.10.0-2) UNRELEASED; urgency=medium
[ Ondřej Nový ]
* d/copyright: Use https protocol in Format field
* d/watch: Use https protocol
* Install README.rst too, due to the filesystem structure, it wasn't
included in our rst directory. (Closes: #827568, again)
-- Stefano Rivera <stefanor@debian.org> Fri, 16 Mar 2018 17:36:20 -0700
twine (1.10.0-1) unstable; urgency=medium
[ Stefano Rivera ]
* Team upload.
* New upstream release. (Closes: #825341, #884649)
- Has useful documentation. (Closes: #827568, #827569)
* Add symlink to ease finding rst docs.
* Drop patches, superseded upstream.
* Update copyright.
* Update Homepage to point to the canonical GitHub repo.
* Migrate to git, pq, and salsa.debian.org.
* Add myself to uploaders.
* Declare Rules-Requires-Root: no.
* Bump debhelper compat to 10.
* Bump Standards-Version to 4.1.3, no changes needed.
* Patch no-needs-sphinx: Don't requires sphinx 1.7.0.
* Don't compress HTML in docs.
* Lintian override PyPI capitalization.
[ Ondřej Nový ]
* d/copyright: Use https protocol in Format field
* d/watch: Use https protocol
-- Ondřej Nový <onovy@debian.org> Tue, 27 Feb 2018 21:11:28 +0100
-- Stefano Rivera <stefanor@debian.org> Fri, 16 Mar 2018 17:13:20 -0700
twine (1.8.1-2) unstable; urgency=medium
......
......@@ -4,7 +4,8 @@ Priority: optional
Maintainer: Python Applications Packaging Team <python-apps-team@lists.alioth.debian.org>
Uploaders: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>,
Barry Warsaw <barry@debian.org>,
Build-Depends: debhelper (>= 9),
Stefano Rivera <stefanor@debian.org>
Build-Depends: debhelper (>= 10),
dh-python,
python3-all,
python3-pkg-resources,
......@@ -13,18 +14,17 @@ Build-Depends: debhelper (>= 9),
python3-requests (>= 2.3.0),
python3-setuptools (>= 0.7),
python3-sphinx,
python3-sphinx-rtd-theme,
Standards-Version: 3.9.8
python3-sphinx-rtd-theme
Standards-Version: 4.1.3
X-Python3-Version: >= 3.3
Vcs-Git: https://salsa.debian.org/python-team/applications/twine.git
Vcs-Browser: https://salsa.debian.org/python-team/applications/twine
Homepage: https://github.com/pypa/twine
Rules-Requires-Root: no
Package: twine
Architecture: all
Depends: python3-setuptools,
${misc:Depends},
${python3:Depends},
Depends: python3-setuptools, ${misc:Depends}, ${python3:Depends}
Description: utility for interacting with PyPI
Twine is a tool for uploading distributions (in the Python meaning) to PyPi.
.
......
......@@ -9,6 +9,7 @@ Copyright:
Ralf Schmitt <ralf@systemexit.de>
Ian Cordasco <graffatcolmingov@gmail.com>
Marc Abramowitz <msabramo@gmail.com>
Sumana Harihareswara <sh@changeset.nyc>
License: APACHE-2.0
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
......@@ -24,6 +25,7 @@ License: APACHE-2.0
Files: debian/*
Copyright: 2014-2015 Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
2018 Stefano Rivera <stefanor@debian.org>
License: GPL-3
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License version 3,
......
README.rst
build/sphinx/html
/usr/share/doc/twine/html/_sources /usr/share/doc/twine/rst
twine: capitalization-error-in-description PyPi PyPI
# The word 'python' is used as a part of an example command
twine: capitalization-error-in-description python Python
Description: link changelog.rst from index.rst
The generated documentation is otherwise useless so let's just link the
changelog for now. Upstream is aware of this issue and will fix it with the
next release.
Bug: https://github.com/dstufft/twine/issues/46
Forwarded: yes
Last-Update: 2014-04-20
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -11,6 +11,7 @@
.. toctree::
:maxdepth: 2
+ changelog
Indices and tables
From: Stefano Rivera <stefanor@debian.org>
Date: Fri, 16 Mar 2018 15:16:55 -0700
Subject: We don't actually need sphinx 1.7.0
To the best of our knowledge. And it isn't in unstable yet.
Forwarded: no
---
docs/conf.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/conf.py b/docs/conf.py
index a1e8a0c..1f25ba8 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -26,7 +26,7 @@ import twine
# -- General configuration ----------------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here.
-needs_sphinx = '1.7.0'
+#needs_sphinx = '1.7.0'
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
changelog-docs
no-needs-sphinx
......@@ -9,3 +9,6 @@ override_dh_auto_build:
# Build sphinx html documentation and man pages
python3 setup.py build_sphinx -b html
python3 setup.py build_sphinx -b man
override_dh_compress:
dh_compress -X.html
This diff is collapsed.
......@@ -26,7 +26,7 @@ import twine
# -- General configuration ----------------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here.
# needs_sphinx = '1.0'
needs_sphinx = '1.7.0'
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
......@@ -43,6 +43,8 @@ extensions = [
releases_issue_uri = "https://github.com/pypa/twine/issues/%s"
releases_release_uri = "https://github.com/pypa/twine/tree/%s"
releases_debug = False # Change to True to see debug output
# Add any paths that contain templates here, relative to this directory.
templates_path = ["_templates"]
......@@ -57,7 +59,7 @@ master_doc = "index"
# General information about the project.
project = "twine"
copyright = "2013, Donald Stufft and individual contributors"
copyright = "2018, Donald Stufft and individual contributors"
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
......
Contributing
============
We are happy you have decided to contribute to ``twine``.
Please see `the GitHub repository`_ for code and more documentation,
and the `official Python Packaging User Guide`_ for user documentation. You can
also join ``#pypa`` or ``#pypa-dev`` `on Freenode`_, or the `pypa-dev
mailing list`_, to ask questions or get involved.
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
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:
.. code-block:: console
mkvirtualenv -p /usr/bin/python3.6 twine
Then, run the following command:
.. code-block:: console
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.
Building the documentation
^^^^^^^^^^^^^^^^^^^^^^^^^^
Additions and edits to twine's documentation are welcome and
appreciated.
We use ``tox`` to build docs. Activate your virtual environment, then
install ``tox``.
.. code-block:: console
pip install tox
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:
.. code-block:: console
tox -e docs
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
^^^^^^^
Tests with twine are run using `tox`_, and tested against the following Python
versions: 2.7, 3.4, 3,5, and 3.6. To run these tests locally, you will need to
have these versions of Python installed on your machine.
Either use ``tox`` to build against all supported Python versions (if
you have them installed) or use ``tox -e py{version}`` to test against
a specific version, e.g., ``tox -e py27`` or ``tox -e py34``.
Also, always run ``tox -e pep8`` before submitting a pull request.
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``.
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.
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.
Future development
------------------
See our `open issues`_.
In the future, ``pip`` and ``twine`` may
merge into a single tool; see `ongoing discussion
<https://github.com/pypa/packaging-problems/issues/60>`_.
.. _`official Python Packaging User Guide`: https://packaging.python.org/tutorials/distributing-packages/
.. _`the GitHub repository`: https://github.com/pypa/twine
.. _`on Freenode`: https://webchat.freenode.net/?channels=%23pypa-dev,pypa
.. _`pypa-dev mailing list`: https://groups.google.com/forum/#!forum/pypa-dev
.. _`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
.. _`plugin`: https://github.com/bitprophet/releases
.. _`projects`: https://packaging.python.org/glossary/#term-project
.. _`open issues`: https://github.com/pypa/twine/issues
.. twine documentation master file, created by
.. twine documentation master file, originally created by
sphinx-quickstart on Tue Aug 13 11:51:54 2013.
You can adapt this file completely to your liking, but it should at least
contain the root `toctree` directive.
......@@ -6,17 +6,20 @@
Welcome to twine's documentation!
=================================
Contents:
.. toctree::
:maxdepth: 2
.. contents:: Table of Contents
:local:
Twine user documentation
------------------------
.. include:: ../README.rst
:start-after: rtd-inclusion-marker-do-not-remove
.. toctree::
:caption: Further documentation
:maxdepth: 3
Indices and tables
==================
contributing
changelog
Python Packaging User Guide <https://packaging.python.org/tutorials/distributing-packages/>
* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`
......@@ -8,16 +8,16 @@ ignore =
[metadata]
requires-dist =
clint
requests >= 2.5.0
requests-toolbelt >= 0.5.1
tqdm >= 4.14
requests >= 2.5.0, != 2.15, != 2.16
requests-toolbelt >= 0.8.0
pkginfo >= 1.0
setuptools >= 0.7.0
argparse; python_version == '2.6'
pyblake2; extra == 'with-blake2'
pyblake2; extra == 'with-blake2' and python_version < '3.6'
keyring; extra == 'keyring'
[egg_info]
tag_build =
tag_date = 0
tag_svn_revision = 0
# Copyright 2013 Donald Stufft
# Copyright 2018 Donald Stufft and individual contributors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
......@@ -19,10 +19,10 @@ import twine
install_requires = [
"clint",
"tqdm >= 4.14",
"pkginfo >= 1.0",
"requests >= 2.5.0",
"requests-toolbelt >= 0.5.1",
"requests >= 2.5.0, != 2.15, != 2.16",
"requests-toolbelt >= 0.8.0",
"setuptools >= 0.7.0",
]
......@@ -32,6 +32,14 @@ if sys.version_info[:2] < (2, 7):
]
blake2_requires = []
if sys.version_info[:2] < (3, 6):
blake2_requires += [
"pyblake2",
]
setup(
name=twine.__title__,
version=twine.__version__,
......@@ -40,6 +48,11 @@ setup(
long_description=open("README.rst").read(),
license=twine.__license__,
url=twine.__uri__,
project_urls={
'Packaging tutorial': 'https://packaging.python.org/tutorials/distributing-packages/',
'Twine documentation': 'https://twine.readthedocs.io/en/latest/',
'Twine source': 'https://github.com/pypa/twine/',
},
author=twine.__author__,
author_email=twine.__email__,
......@@ -57,10 +70,10 @@ setup(
"Programming Language :: Python :: 2",
"Programming Language :: Python :: 2.7",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.2",
"Programming Language :: Python :: 3.3",
"Programming Language :: Python :: 3.4",
"Programming Language :: Python :: 3.5",
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: Implementation :: CPython",
"Programming Language :: Python :: Implementation :: PyPy",
],
......@@ -79,8 +92,9 @@ setup(
install_requires=install_requires,
extras_require={
'with-blake2': [
'pyblake2',
]
'with-blake2': blake2_requires,
'keyring': [
'keyring',
],
},
)
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from twine import __main__ as dunder_main
import pretend
def test_exception_handling(monkeypatch):
replaced_dispatch = pretend.raiser(KeyError('foo'))
monkeypatch.setattr(dunder_main, 'dispatch', replaced_dispatch)
assert dunder_main.main() == 'KeyError: foo'
......@@ -11,8 +11,12 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import requests
from twine import repository
import pretend
def test_gpg_signature_structure_is_preserved():
data = {
......@@ -90,3 +94,48 @@ def test_make_user_agent_string():
assert 'requests-toolbelt/' in user_agent
assert 'pkginfo/' in user_agent
assert 'setuptools/' in user_agent
def response_with(**kwattrs):
resp = requests.Response()
for attr, value in kwattrs.items():
if hasattr(resp, attr):
setattr(resp, attr, value)
return resp
def test_package_is_uploaded_404s():
repo = repository.Repository(
repository_url='https://pypi.python.org/pypi',
username='username',
password='password',
)
repo.session = pretend.stub(
get=lambda url, headers: response_with(status_code=404)
)
package = pretend.stub(
safe_name='fake',
metadata=pretend.stub(version='2.12.0'),
)
assert repo.package_is_uploaded(package) is False
def test_package_is_uploaded_200s_with_no_releases():
repo = repository.Repository(
repository_url='https://pypi.python.org/pypi',
username='username',
password='password',
)
repo.session = pretend.stub(
get=lambda url, headers: response_with(status_code=200,
_content=b'{"releases": {}}',
_content_consumed=True),
)
package = pretend.stub(
safe_name='fake',
metadata=pretend.stub(version='2.12.0'),
)
assert repo.package_is_uploaded(package) is False
......@@ -88,7 +88,7 @@ def test_get_config_old_format(tmpdir):
except KeyError as err:
assert err.args[0] == (
"Missing 'pypi' section from the configuration file\n"
"or not a complete URL in --repository.\n"
"or not a complete URL in --repository-url.\n"
"Maybe you have a out-dated '{0}' format?\n"
"more info: "
"https://docs.python.org/distutils/packageindex.html#pypirc\n"
......@@ -132,20 +132,23 @@ def test_skip_upload_respects_skip_existing(monkeypatch):
package=pkg) is False
def test_password_and_username_from_env(monkeypatch):
def test_values_from_env(monkeypatch):
def none_upload(*args, **kwargs):
pass
replaced_upload = pretend.call_recorder(none_upload)
monkeypatch.setattr(twine.commands.upload, "upload", replaced_upload)
testenv = {"TWINE_USERNAME": "pypiuser",
"TWINE_PASSWORD": "pypipassword"}
"TWINE_PASSWORD": "pypipassword",
"TWINE_CERT": "/foo/bar.crt"}
with helpers.set_env(**testenv):
cli.dispatch(["upload", "path/to/file"])
cli.dispatch(["upload", "path/to/file"])
result_kwargs = replaced_upload.calls[0].kwargs
assert "pypipassword" == result_kwargs["password"]
assert "pypiuser" == result_kwargs["username"]
assert "/foo/bar.crt" == result_kwargs["cert"]
result_kwargs = replaced_upload.calls[1].kwargs
assert None is result_kwargs["password"]
assert None is result_kwargs["username"]
assert None is result_kwargs["cert"]
......@@ -14,9 +14,15 @@
from __future__ import absolute_import, division, print_function
from __future__ import unicode_literals
import sys
import os.path
import textwrap
try:
import builtins
except ImportError:
import __builtin__ as builtins
import pytest
from twine import utils
......@@ -115,6 +121,8 @@ def test_get_repository_config_missing(tmpdir):
}
assert (utils.get_repository_from_config(pypirc, 'foo', repository_url) ==
exp)
assert (utils.get_repository_from_config(pypirc, 'pypi', repository_url) ==
exp)
exp = {
"repository": utils.DEFAULT_REPOSITORY,
"username": None,
......@@ -172,3 +180,75 @@ def test_default_to_environment_action(env_name, default, environ, expected):
)
assert action.env == env_name
assert action.default == expected
def test_get_password_keyring_overrides_prompt(monkeypatch):
class MockKeyring:
@staticmethod
def get_password(system, user):
return '{user}@{system} sekure pa55word'.format(**locals())
monkeypatch.setitem(sys.modules, 'keyring', MockKeyring)
pw = utils.get_password('system', 'user', None, {})
assert pw == 'user@system sekure pa55word'
def test_get_password_keyring_defers_to_prompt(monkeypatch):
monkeypatch.setattr(utils, 'password_prompt', lambda prompt: 'entered pw')
class MockKeyring:
@staticmethod
def get_password(system, user):
return
monkeypatch.setitem(sys.modules, 'keyring', MockKeyring)
pw = utils.get_password('system', 'user', None, {})
assert pw == 'entered pw'
@pytest.fixture
def keyring_missing(monkeypatch):
"""
Simulate that 'import keyring' raises an ImportError
"""
real_import = builtins.__import__
def my_import(name, *args, **kwargs):
if name == 'keyring':
raise ImportError
return real_import(name, *args, **kwargs)
monkeypatch.setattr(builtins, '__import__', my_import)
@pytest.fixture
def entered_password(monkeypatch):
monkeypatch.setattr(utils, 'password_prompt', lambda prompt: 'entered pw')
def test_get_password_keyring_missing_prompts(
entered_password, keyring_missing):
assert utils.get_password('system', 'user', None, {}) == 'entered pw'
@pytest.fixture
def keyring_no_backends(monkeypatch):
"""
Simulate that keyring has no available backends. When keyring
has no backends for the system, the backend will be a
fail.Keyring, which raises RuntimeError on get_password.
"""
class FailKeyring(object):
@staticmethod
def get_password(system, username):
raise RuntimeError("fail!")
monkeypatch.setitem(sys.modules, 'keyring', FailKeyring())
def test_get_password_runtime_error_suppressed(
entered_password, keyring_no_backends, recwarn):
assert utils.get_password('system', 'user', None, {}) == 'entered pw'
assert len(recwarn) == 1
warning = recwarn.pop(UserWarning)
assert 'fail!' in str(warning)
......@@ -28,3 +28,20 @@ def example_wheel(request):
def test_version_parsing(example_wheel):
assert example_wheel.py_version == 'py2.py3'
def test_find_metadata_files():
names = [
b'package/lib/__init__.py',
b'package/lib/version.py',
b'package/METADATA.txt',
b'package/METADATA.json',
b'package/METADATA',
]
expected = [
['package', 'METADATA'],
['package', 'METADATA.json'],
['package', 'METADATA.txt'],
]
candidates = wheel.Wheel.find_candidate_metadata_files(names)
assert expected == candidates
This diff is collapsed.
......@@ -7,11 +7,13 @@ setup.py
docs/Makefile
docs/changelog.rst
docs/conf.py
docs/contributing.rst
docs/index.rst
docs/make.bat
docs/_static/.empty
tests/helpers.py
tests/test_cli.py
tests/test_main.py
tests/test_package.py
tests/test_repository.py
tests/test_upload.py
......@@ -31,7 +33,6 @@ twine.egg-info/PKG-INFO
twine.egg-info/SOURCES.txt
twine.egg-info/dependency_links.txt
twine.egg-info/entry_points.txt
twine.egg-info/pbr.json
twine.egg-info/requires.txt
twine.egg-info/top_level.txt
twine/commands/__init__.py
......
{"is_release": true, "git_version": "5c06ed2"}
\ No newline at end of file
clint
pkginfo >= 1.0
requests >= 2.5.0
requests-toolbelt >= 0.5.1
setuptools >= 0.7.0
tqdm>=4.14
pkginfo>=1.0
requests!=2.15,!=2.16,>=2.5.0
requests-toolbelt>=0.8.0
setuptools>=0.7.0
[keyring]
keyring
[with-blake2]
pyblake2
# Copyright 2013 Donald Stufft
# Copyright 2018 Donald Stufft and individual contributors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
......@@ -12,7 +12,6 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from __future__ import absolute_import, division, print_function
from __future__ import unicode_literals
__all__ = (
"__title__", "__summary__", "__uri__", "__version__", "__author__",
......@@ -20,13 +19,13 @@ __all__ = (
)
__title__ = "twine"
__summary__ = "Collection of utilities for interacting with PyPI"
__uri__ = "https://github.com/pypa/twine"
__summary__ = "Collection of utilities for publishing packages on PyPI"
__uri__ = "http://twine.readthedocs.io/"
__version__ = "1.8.1"
__version__ = "1.10.0"
__author__ = "Donald Stufft and individual contributors"
__email__ = "donald@stufft.io"
__license__ = "Apache License, Version 2.0"
__copyright__ = "Copyright 2013 Donald Stufft"
__copyright__ = "Copyright 2018 Donald Stufft and individual contributors"
......@@ -21,7 +21,13 @@ from twine.cli import dispatch
def main():
return dispatch(sys.argv[1:])
try:
return dispatch(sys.argv[1:])
except Exception as exc:
return '{0}: {1}'.format(
exc.__class__.__name__,
exc.args[0],
)
if __name__ == "__main__":
......
......@@ -18,7 +18,7 @@ import argparse
import pkg_resources
import setuptools
import clint
import tqdm
import requests
import requests_toolbelt
import pkginfo
......@@ -38,7 +38,7 @@ def list_dependencies_and_versions():
('requests', requests.__version__),
('setuptools', setuptools.__version__),
('requests-toolbelt', requests_toolbelt.__version__),
('clint', clint.__version__),
('tqdm', tqdm.__version__),
]
......
......@@ -15,7 +15,6 @@ from __future__ import absolute_import, unicode_literals, print_function
import argparse
import os.path
import sys
from twine import exceptions as exc
from twine.package import PackageFile
......@@ -37,7 +36,9 @@ def register(package, repository, username, password, comment, config_file,
print("Registering package to {0}".format(config["repository"]))
username = utils.get_username(username, config)
password = utils.get_password(password, config)
password = utils.get_password(
config["repository"], username, password, config,
)
ca_cert = utils.get_cacert(cert, config)
client_cert = utils.get_clientcert(client_cert, config)
......@@ -69,9 +70,10 @@ def main(args):
action=utils.EnvironmentDefault,
env="TWINE_REPOSITORY",
default="pypi",
help="The repository to register the package to. Can be a section in "
"the config file or a full URL to the repository (default: "
"%(default)s)",
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)",
)
parser.add_argument(
"--repository-url",
......@@ -79,9 +81,9 @@ def main(args):
env="TWINE_REPOSITORY_URL",
default=None,
required=False,
help="The repository URL to upload the package to. This can be "
"specified with --repository because it will be used if there is "
"no configuration for the value passed to --repository."
help="The repository URL to register the package to. "
"This overrides --repository."
"(Can also be set via %(env)s environment variable.)"
)
parser.add_argument(
"-u", "--username",
......@@ -110,14 +112,19 @@ def main(args):
)
parser.add_argument(
"--cert",
action=utils.EnvironmentDefault,
env="TWINE_CERT",
default=None,
required=False,
metavar="path",
help="Path to alternate CA bundle",
help="Path to alternate CA bundle (can also be set via %(env)s "
"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 forma",
"private key and the certificate in PEM format",
)
parser.add_argument(
"package",
......@@ -128,7 +135,4 @@ def main(args):
args = parser.parse_args(args)
# Call the register function with the args from the command line
try:
register(**vars(args))
except Exception as exc:
sys.exit("{exc.__class__.__name__}: {exc}".format(exc=exc))
register(**vars(args))
......@@ -21,7 +21,7 @@ import sys
import twine.exceptions as exc
from twine.package import PackageFile
from twine.repository import Repository
from twine.repository import Repository, LEGACY_PYPI
from twine import utils
......@@ -98,8 +98,16 @@ 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))
username = utils.get_username(username, config)
password = utils.get_password(password, config)
password = utils.get_password(
config["repository"], username, password, config,
)
ca_cert = utils.get_cacert(cert, config)
client_cert = utils.get_clientcert(client_cert, config)
......@@ -157,9 +165,10 @@ def main(args):
action=utils.EnvironmentDefault,
env="TWINE_REPOSITORY",
default="pypi",
help="The repository to register the package to. Can be a section in "
"the config file or a full URL to the repository (default: "
"%(default)s)",
help="The repository 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)",
)
parser.add_argument(
"--repository-url",
......@@ -167,9 +176,9 @@ def main(args):
env="TWINE_REPOSITORY_URL",
default=None,
required=False,
help="The repository URL to upload the package to. This can be "
"specified with --repository because it will be used if there is "
"no configuration for the value passed to --repository."
help="The repository URL to upload the package to. "
"This overrides --repository."
"(Can also be set via %(env)s environment variable.)"
)
parser.add_argument(
"-s", "--sign",
......@@ -221,14 +230,19 @@ def main(args):
)
parser.add_argument(
"--cert",
action=utils.EnvironmentDefault,
env="TWINE_CERT",
default=None,
required=False,
metavar="path",
help="Path to alternate CA bundle",
help="Path to alternate CA bundle (can also be set via %(env)s "
"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 forma",
"private key and the certificate in PEM format",
)
parser.add_argument(
"dists",
......@@ -242,10 +256,7 @@ def main(args):
args = parser.parse_args(args)
# Call the upload function with the arguments from the command line
try:
upload(**vars(args))
except Exception as exc:
sys.exit("{exc.__class__.__name__}: {exc}".format(exc=exc))
upload(**vars(args))
if __name__ == "__main__":
......
......@@ -21,9 +21,12 @@ import pkginfo
import pkg_resources
try:
import pyblake2
from hashlib import blake2b
except ImportError:
pyblake2 = None
try:
from pyblake2 import blake2b
except ImportError:
blake2b = None
from twine.wheel import Wheel
from twine.wininst import WinInst
......@@ -59,8 +62,8 @@ class PackageFile(object):
self.gpg_signature = None
blake2_256_hash = None
if pyblake2 is not None:
blake2_256_hash = pyblake2.blake2b(digest_size=256 // 8)
if blake2b is not None:
blake2_256_hash = blake2b(digest_size=256 // 8)
# NOTE(sigmavirus24): We may or may not be able to use blake2 so let's
# either use the methods or lambdas to do nothing.
blake_update = getattr(blake2_256_hash, 'update', lambda *args: None)
......
......@@ -13,7 +13,9 @@
# limitations under the License.
from __future__ import absolute_import, unicode_literals, print_function
from clint.textui.progress import Bar as ProgressBar
import sys
from tqdm import tqdm
import requests
from requests import adapters
......@@ -22,7 +24,7 @@ from requests.packages.urllib3 import util
from requests_toolbelt.multipart import (
MultipartEncoder, MultipartEncoderMonitor
)
from requests_toolbelt.utils.user_agent import user_agent
from requests_toolbelt.utils import user_agent
import twine
......@@ -33,6 +35,16 @@ WAREHOUSE = 'https://upload.pypi.org/'
OLD_WAREHOUSE = 'https://upload.pypi.io/'
class ProgressBar(tqdm):
def update_to(self, n):
"""Update the bar in the way compatible with requests-toolbelt.
This is identical to tqdm.update, except ``n`` will be the current
value - not the delta as tqdm expects.
"""
self.update(n - self.n) # will also do self.n = n
class Repository(object):
def __init__(self, repository_url, username, password):
self.url = repository_url
......@@ -57,7 +69,11 @@ class Repository(object):
def _make_user_agent_string():
from twine import cli
dependencies = cli.list_dependencies_and_versions()
return user_agent('twine', twine.__version__, extras=dependencies)
return user_agent.UserAgentBuilder(
'twine', twine.__version__,
).include_extras(
dependencies
).include_implementation().build()
def close(self):
self.session.close()
......@@ -108,7 +124,7 @@ class Repository(object):
data.update({
# action
":action": "file_upload",
"protcol_version": "1",
"protocol_version": "1",
})
data_to_send = self._convert_data_to_list_of_tuples(data)
......@@ -121,18 +137,19 @@ class Repository(object):
(package.basefilename, fp, "application/octet-stream"),
))
encoder = MultipartEncoder(data_to_send)
bar = ProgressBar(expected_size=encoder.len, filled_char='=')
monitor = MultipartEncoderMonitor(
encoder, lambda monitor: bar.show(monitor.bytes_read)
)
resp = self.session.post(
self.url,
data=monitor,
allow_redirects=False,
headers={'Content-Type': monitor.content_type},
)
bar.done()
with ProgressBar(total=encoder.len,
unit='B', unit_scale=True, unit_divisor=1024,
miniters=1, file=sys.stdout) as bar:
monitor = MultipartEncoderMonitor(
encoder, lambda monitor: bar.update_to(monitor.bytes_read)
)
resp = self.session.post(
self.url,
data=monitor,
allow_redirects=False,
headers={'Content-Type': monitor.content_type},
)
return resp
......@@ -173,7 +190,10 @@ class Repository(object):
url=LEGACY_PYPI)
headers = {'Accept': 'application/json'}
response = self.session.get(url, headers=headers)
releases = response.json()['releases']
if response.status_code == 200:
releases = response.json()['releases']
else:
releases = {}
self._releases_json_data[safe_name] = releases
packages = releases.get(package.metadata.version, [])
......
......@@ -20,7 +20,7 @@ import functools
import getpass
import sys
import argparse
import warnings
try:
import configparser
......@@ -90,7 +90,10 @@ def get_config(path="~/.pypirc"):
}
# Optional configuration values
for key in ["username", "repository", "password"]:
for key in [
"username", "repository", "password",
"ca_cert", "client_cert",
]:
if parser.has_option(repository, key):
config[repository][key] = parser.get(repository, key)
elif defaults.get(key):
......@@ -101,20 +104,20 @@ def get_config(path="~/.pypirc"):
def get_repository_from_config(config_file, repository, repository_url=None):
# Get our config from the .pypirc file
if repository_url and "://" in repository_url:
# assume that the repository is actually an URL and just sent
# them a dummy with the repo set
return {
"repository": repository_url,
"username": None,
"password": None,
}
try:
return get_config(config_file)[repository]
except KeyError:
if repository_url and "://" in repository_url:
# assume that the repsoitory is actually an URL and just sent
# them a dummy with the repo set
return {
"repository": repository_url,
"username": None,
"password": None,
}
msg = (
"Missing '{repo}' section from the configuration file\n"
"or not a complete URL in --repository.\n"
"or not a complete URL in --repository-url.\n"
"Maybe you have a out-dated '{cfg}' format?\n"
"more info: "
"https://docs.python.org/distutils/packageindex.html#pypirc\n"
......@@ -141,7 +144,7 @@ def check_status_code(response):
response.url.startswith(("https://pypi.python.org",
"https://testpypi.python.org"))):
print("It appears you're uploading to pypi.python.org (or testpypi) "
"you've recieved a 500 error response. PyPI is being phased "
"you've received a 500 error response. PyPI is being phased "
"out for 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.")
......@@ -184,18 +187,33 @@ def password_prompt(prompt_text): # Always expects unicode for our own sanity
# Workaround for https://github.com/pypa/twine/issues/116
if os.name == 'nt' and sys.version_info < (3, 0):
prompt = prompt_text.encode('utf8')
return functools.partial(getpass.getpass, prompt=prompt)
return getpass.getpass(prompt)
def get_password_from_keyring(system, username):
try:
import keyring
except ImportError:
return
try:
return keyring.get_password(system, username)
except Exception as exc:
warnings.warn(str(exc))
def password_from_keyring_or_prompt(system, username):
return (
get_password_from_keyring(system, username)
or password_prompt('Enter your password: ')
)
get_username = functools.partial(
get_userpass_value,
key='username',
prompt_strategy=functools.partial(input_func, 'Enter your username: '),
)
get_password = functools.partial(
get_userpass_value,
key='password',
prompt_strategy=password_prompt('Enter your password: '),
)
get_cacert = functools.partial(
get_userpass_value,
key='ca_cert',
......@@ -222,3 +240,16 @@ class EnvironmentDefault(argparse.Action):
def __call__(self, parser, namespace, values, option_string=None):
setattr(namespace, self.dest, values)
def get_password(system, username, cli_value, config):
return get_userpass_value(
cli_value,
config,
key='password',
prompt_strategy=functools.partial(
password_from_keyring_or_prompt,
system,
username,
),
)
......@@ -38,6 +38,12 @@ wheel_file_re = re.compile(
re.VERBOSE)
def try_decode(s):
if isinstance(s, bytes):
return s.decode('utf8')
return s
class Wheel(Distribution):
def __init__(self, filename, metadata_version=None):
......@@ -51,6 +57,15 @@ class Wheel(Distribution):
wheel_info = wheel_file_re.match(self.basefilename)
return wheel_info.group("pyver")
@staticmethod
def find_candidate_metadata_files(names):
"""Filter files that may be METADATA files."""
tuples = [
x.split('/') for x in map(try_decode, names)
if 'METADATA' in x
]
return [x[1] for x in sorted([(len(x), x) for x in tuples])]
def read(self):
fqn = os.path.abspath(os.path.normpath(self.filename))
if not os.path.exists(fqn):
......@@ -66,9 +81,7 @@ class Wheel(Distribution):
raise ValueError('Not a known archive format: %s' % fqn)
try:
tuples = [x.split('/') for x in names if 'METADATA' in x]
schwarz = sorted([(len(x), x) for x in tuples])
for path in [x[1] for x in schwarz]:
for path in self.find_candidate_metadata_files(names):
candidate = '/'.join(path)
data = read_file(candidate)
if b'Metadata-Version' in data:
......