...
 
Commits (6)
......@@ -18,8 +18,9 @@ Andrew Watts <andrewwatts@gmail.com>
Anna Martelli Ravenscroft <annaraven@gmail.com>
Sumana Harihareswara <sh@changeset.nyc>
Dustin Ingram <di@di.codes> (https://di.codes)
Jesse Jarzynka <jesse@jessejoe.com> (http://jessejoe.com)
Jesse Jarzynka <jesse@jessejoe.com> (https://www.jessejoe.com/)
László Kiss Kollár <kiss.kollar.laszlo@gmail.com>
Frances Hocutt <frances.hocutt@gmail.com>
Tathagata Dasgupta <tathagatadg@gmail.com>
Wasim Thabraze <wasim@thabraze.me>
\ No newline at end of file
Wasim Thabraze <wasim@thabraze.me>
Varun Kamath <varunkamath18@gmail.com>
\ No newline at end of file
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
https://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
......
Metadata-Version: 2.1
Name: twine
Version: 1.12.1
Version: 1.13.0
Summary: Collection of utilities for publishing packages on PyPI
Home-page: https://twine.readthedocs.io/
Author: Donald Stufft and individual contributors
Author-email: donald@stufft.io
License: Apache License, Version 2.0
Project-URL: Twine source, https://github.com/pypa/twine/
Project-URL: Packaging tutorial, https://packaging.python.org/tutorials/distributing-packages/
Project-URL: Travis CI, https://travis-ci.org/pypa/twine
Project-URL: Twine documentation, https://twine.readthedocs.io/en/latest/
Description: twine
Project-URL: Twine source, https://github.com/pypa/twine/
Description: .. image:: https://img.shields.io/travis/pypa/twine/master.svg?label=travis-ci
:target: https://travis-ci.org/pypa/twine
twine
=====
.. rtd-inclusion-marker-do-not-remove
......@@ -148,7 +152,7 @@ Description: twine
Disabling Keyring
^^^^^^^^^^^^^^^^^
In some cases, the presence of keyring may be problemmatic. To disable
In some cases, the presence of keyring may be problematic. To disable
keyring and defer to a prompt for passwords, uninstall ``keyring``
or if that's not an option, you can also configure keyring to be disabled.
......@@ -171,6 +175,7 @@ Description: twine
[-s] [--sign-with SIGN_WITH] [-i IDENTITY] [-u USERNAME]
[-p PASSWORD] [-c COMMENT] [--config-file CONFIG_FILE]
[--skip-existing] [--cert path] [--client-cert path]
[--verbose] [--disable-progress-bar]
dist [dist ...]
positional arguments:
......@@ -215,6 +220,9 @@ Description: twine
--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.
``twine check``
^^^^^^^^^^^^^^^
......@@ -357,7 +365,9 @@ Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.4
Classifier: Programming Language :: Python :: 3.5
Classifier: Programming Language :: Python :: 3.6
Classifier: Programming Language :: Python :: 3.7
Classifier: Programming Language :: Python :: Implementation :: CPython
Classifier: Programming Language :: Python :: Implementation :: PyPy
Provides-Extra: keyring
Requires-Python: >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*
Provides-Extra: with-blake2
Provides-Extra: keyring
.. image:: https://img.shields.io/travis/pypa/twine/master.svg?label=travis-ci
:target: https://travis-ci.org/pypa/twine
twine
=====
......@@ -137,7 +140,7 @@ The next time you run ``twine`` it will prompt you for a username and will grab
Disabling Keyring
^^^^^^^^^^^^^^^^^
In some cases, the presence of keyring may be problemmatic. To disable
In some cases, the presence of keyring may be problematic. To disable
keyring and defer to a prompt for passwords, uninstall ``keyring``
or if that's not an option, you can also configure keyring to be disabled.
......@@ -160,6 +163,7 @@ Uploads one or more distributions to a repository.
[-s] [--sign-with SIGN_WITH] [-i IDENTITY] [-u USERNAME]
[-p PASSWORD] [-c COMMENT] [--config-file CONFIG_FILE]
[--skip-existing] [--cert path] [--client-cert path]
[--verbose] [--disable-progress-bar]
dist [dist ...]
positional arguments:
......@@ -204,6 +208,9 @@ Uploads one or more distributions to a repository.
--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.
``twine check``
^^^^^^^^^^^^^^^
......
twine (1.13.0-1) unstable; urgency=medium
* New upstream release.
* Update copyright.
* Recommend python3-keyring, used for credential storage when available.
-- Stefano Rivera <stefanor@debian.org> Mon, 25 Feb 2019 11:49:33 -0800
twine (1.12.1-2) unstable; urgency=medium
* Add missing Depends for sphinx docs. (Closes: #911069)
......
......@@ -28,6 +28,7 @@ Depends: python3-setuptools,
${misc:Depends},
${python3:Depends},
${sphinxdoc:Depends}
Recommends: python3-keyring
Description: utility for interacting with PyPI
Twine is a tool for uploading distributions (in the Python meaning) to PyPi.
.
......
......@@ -4,13 +4,26 @@ Source: https://github.com/pypa/twine
Files: *
Copyright:
Donald Stufft <donald@stufft.io>
Dustin Ingram <di@di.codes> (https://di.codes)
Ian Cordasco <graffatcolmingov@gmail.com>
Donald Stufft <donald@stufft.io> (https://caremad.io/)
Jannis Leidel <jannis@leidel.info>
Marc Abramowitz <msabramo@gmail.com>
Ralf Schmitt <ralf@systemexit.de>
Ian Cordasco <graffatcolmingov@gmail.com>
Marc Abramowitz <msabramo@gmail.com> (http://marc-abramowitz.com/)
Tom Myers <tom.stephen.myers@gmail.com>
Rodrigue Cloutier <rodcloutier@gmail.com>
Tyrel Souza <tyrelsouza@gmail.com> (https://tyrelsouza.com)
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>
Dustin Ingram <di@di.codes> (https://di.codes)
Jesse Jarzynka <jesse@jessejoe.com> (https://www.jessejoe.com/)
László Kiss Kollár <kiss.kollar.laszlo@gmail.com>
Frances Hocutt <frances.hocutt@gmail.com>
Tathagata Dasgupta <tathagatadg@gmail.com>
Wasim Thabraze <wasim@thabraze.me>
Varun Kamath <varunkamath18@gmail.com>
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.
......@@ -26,7 +39,7 @@ License: APACHE-2.0
Files: debian/*
Copyright: 2014-2015 Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
2018 Stefano Rivera <stefanor@debian.org>
2019 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,
......
......@@ -3,7 +3,25 @@
=========
Changelog
=========
* :release:`1.13.0 <2019-02-13>`
* :bug:`452` Restore prompts while retaining support for suppressing prompts.
* :bug:`447` Avoid requests-toolbelt to 0.9.0 to prevent attempting to use
openssl when it isn't available.
* :feature:`427` Add disable_progress_bar option to disable tqdm.
* :feature:`426` Allow defining an empty username and password in .pypirc.
* :bug:`441` Only install pyblake2 if needed.
* :bug:`444` Use io.StringIO instead of StringIO.
* :bug:`436` Use modern Python language features.
* :support:`439` Refactor tox env and travis config.
* :bug:`435` Specify python_requires in setup.py
* :bug:`432` Use https URLs everywhere.
* :bug:`428` Fix --skip-existing for Nexus Repos.
* :feature:`419` Support keyring.get_credential.
* :feature:`418` Support keyring.get_username_and_password.
* :bug:`421` Remove unnecessary usage of readme_render.markdown.
* :feature:`` Add Python 3.7 to classifiers.
* :bug:`412` Don't crash if there's no package description.
* :bug:`408` Fix keyring support.
* :release:`1.12.1 <2018-09-24>`
* :bug:`404` Fix regression with upload exit code
* :release:`1.12.0 <2018-09-24>`
......
......@@ -10,14 +10,6 @@ ignore =
[metadata]
license_file = LICENSE
requires-dist =
tqdm >= 4.14
requests >= 2.5.0, != 2.15, != 2.16
requests-toolbelt >= 0.8.0
pkginfo >= 1.4.2
setuptools >= 0.7.0
pyblake2; extra == 'with-blake2' and python_version < '3.6'
keyring; extra == 'keyring'
[egg_info]
tag_build =
......
......@@ -4,7 +4,7 @@
# 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
# https://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,
......@@ -13,19 +13,9 @@
# limitations under the License.
from setuptools import setup
import sys
import twine
blake2_requires = []
if sys.version_info[:2] < (3, 6):
blake2_requires += [
"pyblake2",
]
setup(
name=twine.__title__,
version=twine.__version__,
......@@ -36,6 +26,7 @@ setup(
url=twine.__uri__,
project_urls={
'Packaging tutorial': 'https://packaging.python.org/tutorials/distributing-packages/',
'Travis CI': 'https://travis-ci.org/pypa/twine',
'Twine documentation': 'https://twine.readthedocs.io/en/latest/',
'Twine source': 'https://github.com/pypa/twine/',
},
......@@ -59,6 +50,7 @@ setup(
"Programming Language :: Python :: 3.4",
"Programming Language :: Python :: 3.5",
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: Implementation :: CPython",
"Programming Language :: Python :: Implementation :: PyPy",
],
......@@ -76,16 +68,19 @@ setup(
],
},
python_requires=">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*",
install_requires=[
"pkginfo >= 1.4.2",
"readme_renderer >= 21.0",
"requests >= 2.5.0, != 2.15, != 2.16",
"requests-toolbelt >= 0.8.0",
"requests-toolbelt >= 0.8.0, != 0.9.0",
"setuptools >= 0.7.0",
"tqdm >= 4.14",
],
extras_require={
'with-blake2': blake2_requires,
'with-blake2': [
'pyblake2; python_version<"3.6" and platform_python_implementation=="CPython"',
],
'keyring': [
'keyring',
],
......
......@@ -4,7 +4,7 @@
# 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
# https://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,
......
......@@ -4,7 +4,7 @@
# 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
# https://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,
......@@ -58,11 +58,13 @@ def test_check_passing_distribution(monkeypatch):
renderer = pretend.stub(
render=pretend.call_recorder(lambda *a, **kw: "valid")
)
package = pretend.stub(metadata_dictionary=lambda: {"description": "blah"})
package = pretend.stub(metadata_dictionary=lambda: {
"description": "blah", 'description_content_type': 'text/markdown',
})
output_stream = check.StringIO()
warning_stream = ""
monkeypatch.setattr(check, "_RENDERERS", {"": renderer})
monkeypatch.setattr(check, "_RENDERERS", {None: renderer})
monkeypatch.setattr(check, "_find_dists", lambda a: ["dist/dist.tar.gz"])
monkeypatch.setattr(
check,
......@@ -81,15 +83,41 @@ def test_check_passing_distribution(monkeypatch):
]
def test_check_no_description(monkeypatch, capsys):
package = pretend.stub(metadata_dictionary=lambda: {
'description': None, 'description_content_type': None,
})
monkeypatch.setattr(check, "_find_dists", lambda a: ["dist/dist.tar.gz"])
monkeypatch.setattr(
check,
"PackageFile",
pretend.stub(from_filename=lambda *a, **kw: package),
)
# used to crash with `AttributeError`
output_stream = check.StringIO()
check.check("dist/*", output_stream=output_stream)
assert output_stream.getvalue() == (
'Checking distribution dist/dist.tar.gz: '
'warning: `long_description_content_type` missing. '
'defaulting to `text/x-rst`.\n'
'warning: `long_description` missing.\n'
'Passed\n'
)
def test_check_failing_distribution(monkeypatch):
renderer = pretend.stub(
render=pretend.call_recorder(lambda *a, **kw: None)
)
package = pretend.stub(metadata_dictionary=lambda: {"description": "blah"})
package = pretend.stub(metadata_dictionary=lambda: {
"description": "blah", "description_content_type": 'text/markdown',
})
output_stream = check.StringIO()
warning_stream = "WARNING"
monkeypatch.setattr(check, "_RENDERERS", {"": renderer})
monkeypatch.setattr(check, "_RENDERERS", {None: renderer})
monkeypatch.setattr(check, "_find_dists", lambda a: ["dist/dist.tar.gz"])
monkeypatch.setattr(
check,
......
......@@ -4,7 +4,7 @@
# 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
# https://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,
......
......@@ -2,7 +2,7 @@
# 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
# https://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,
......
......@@ -4,7 +4,7 @@
# 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
# https://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,
......
......@@ -4,7 +4,7 @@
# 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
# https://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,
......@@ -17,6 +17,8 @@ from twine import repository
from twine.utils import DEFAULT_REPOSITORY
import pretend
import pytest
from contextlib import contextmanager
def test_gpg_signature_structure_is_preserved():
......@@ -140,3 +142,49 @@ def test_package_is_uploaded_200s_with_no_releases():
)
assert repo.package_is_uploaded(package) is False
@pytest.mark.parametrize('disable_progress_bar', [
True,
False
])
def test_disable_progress_bar_is_forwarded_to_tqdm(monkeypatch, tmpdir,
disable_progress_bar):
"""Test whether the disable flag is passed to tqdm
when the disable_progress_bar option is passed to the
repository
"""
@contextmanager
def progressbarstub(*args, **kwargs):
assert "disable" in kwargs
assert kwargs["disable"] == disable_progress_bar
yield
monkeypatch.setattr(repository, "ProgressBar", progressbarstub)
repo = repository.Repository(
repository_url=DEFAULT_REPOSITORY,
username='username',
password='password',
disable_progress_bar=disable_progress_bar
)
repo.session = pretend.stub(
post=lambda url, data, allow_redirects, headers: response_with(
status_code=200)
)
fakefile = tmpdir.join('fake.whl')
fakefile.write('.')
def dictfunc():
return {"name": "fake"}
package = pretend.stub(
safe_name='fake',
metadata=pretend.stub(version='2.12.0'),
basefilename="fake.whl",
filename=str(fakefile),
metadata_dictionary=dictfunc
)
repo.upload(package)
......@@ -5,7 +5,7 @@
# 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
# https://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,
......@@ -49,6 +49,7 @@ def test_settings_transforms_config(tmpdir):
assert s.password == 'password'
assert s.cacert is None
assert s.client_cert is None
assert s.disable_progress_bar is False
def test_identity_requires_sign():
......
......@@ -4,7 +4,7 @@
# 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
# https://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,
......@@ -137,6 +137,20 @@ def test_skip_existing_skips_files_already_on_PyPI(monkeypatch):
package=pkg) is True
def test_skip_existing_skips_files_already_on_nexus(monkeypatch):
# Nexus Repository Manager (https://www.sonatype.com/nexus-repository-oss)
# responds with 400 when the file already exists
response = pretend.stub(
status_code=400,
reason="Repository does not allow updating assets: pypi for url: "
"http://www.foo.bar")
pkg = package.PackageFile.from_filename(WHEEL_FIXTURE, None)
assert upload.skip_upload(response=response,
skip_existing=True,
package=pkg) is True
def test_skip_existing_skips_files_already_on_pypiserver(monkeypatch):
# pypiserver (https://pypi.org/project/pypiserver) responds with a
# 409 when the file already exists.
......
......@@ -4,7 +4,7 @@
# 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
# https://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,
......@@ -126,6 +126,26 @@ def test_get_config_missing(tmpdir):
}
def test_empty_userpass(tmpdir):
"""
Empty username and password may be supplied to suppress
prompts. See #426.
"""
pypirc = os.path.join(str(tmpdir), ".pypirc")
with open(pypirc, "w") as fp:
fp.write(textwrap.dedent("""
[pypi]
username=
password=
"""))
config = utils.get_config(pypirc)
pypi = config['pypi']
assert pypi['username'] == pypi['password'] == ''
def test_get_repository_config_missing(tmpdir):
pypirc = os.path.join(str(tmpdir), ".pypirc")
......@@ -229,6 +249,45 @@ def test_get_password_keyring_defers_to_prompt(monkeypatch):
assert pw == 'entered pw'
def test_no_password_defers_to_prompt(monkeypatch):
monkeypatch.setattr(utils, 'password_prompt', lambda prompt: 'entered pw')
pw = utils.get_password('system', 'user', None, {'password': None})
assert pw == 'entered pw'
def test_empty_password_bypasses_prompt(monkeypatch):
monkeypatch.setattr(utils, 'password_prompt', lambda prompt: 'entered pw')
pw = utils.get_password('system', 'user', None, {'password': ''})
assert pw == ''
def test_get_username_and_password_keyring_overrides_prompt(monkeypatch):
import collections
Credential = collections.namedtuple('Credential', 'username password')
class MockKeyring:
@staticmethod
def get_credential(system, user):
return Credential(
'real_user',
'real_user@{system} sekure pa55word'.format(**locals())
)
@staticmethod
def get_password(system, user):
cred = MockKeyring.get_credential(system, user)
if user != cred.username:
raise RuntimeError("unexpected username")
return cred.password
monkeypatch.setitem(sys.modules, 'keyring', MockKeyring)
user = utils.get_username('system', None, {})
assert user == 'real_user'
pw = utils.get_password('system', user, None, {})
assert pw == 'real_user@system sekure pa55word'
@pytest.fixture
def keyring_missing(monkeypatch):
"""
......@@ -237,11 +296,31 @@ def keyring_missing(monkeypatch):
monkeypatch.delitem(sys.modules, 'keyring', raising=False)
@pytest.fixture
def keyring_missing_get_credentials(monkeypatch):
"""
Simulate older versions of keyring that do not have the
'get_credentials' API.
"""
monkeypatch.delattr('keyring.backends.KeyringBackend',
'get_credential', raising=False)
@pytest.fixture
def entered_username(monkeypatch):
monkeypatch.setattr(utils, 'input_func', lambda prompt: 'entered user')
@pytest.fixture
def entered_password(monkeypatch):
monkeypatch.setattr(utils, 'password_prompt', lambda prompt: 'entered pw')
def test_get_username_keyring_missing_get_credentials_prompts(
entered_username, keyring_missing_get_credentials):
assert utils.get_username('system', None, {}) == 'entered user'
def test_get_password_keyring_missing_prompts(
entered_password, keyring_missing):
assert utils.get_password('system', 'user', None, {}) == 'entered pw'
......@@ -261,6 +340,28 @@ def keyring_no_backends(monkeypatch):
monkeypatch.setitem(sys.modules, 'keyring', FailKeyring())
@pytest.fixture
def keyring_no_backends_get_credential(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_credential(system, username):
raise RuntimeError("fail!")
monkeypatch.setitem(sys.modules, 'keyring', FailKeyring())
def test_get_username_runtime_error_suppressed(
entered_username, keyring_no_backends_get_credential, recwarn):
assert utils.get_username('system', None, {}) == 'entered user'
assert len(recwarn) == 1
warning = recwarn.pop(UserWarning)
assert 'fail!' in str(warning)
def test_get_password_runtime_error_suppressed(
entered_password, keyring_no_backends, recwarn):
assert utils.get_password('system', 'user', None, {}) == 'entered pw'
......
......@@ -4,7 +4,7 @@
# 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
# https://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,
......
Metadata-Version: 2.1
Name: twine
Version: 1.12.1
Version: 1.13.0
Summary: Collection of utilities for publishing packages on PyPI
Home-page: https://twine.readthedocs.io/
Author: Donald Stufft and individual contributors
Author-email: donald@stufft.io
License: Apache License, Version 2.0
Project-URL: Twine source, https://github.com/pypa/twine/
Project-URL: Packaging tutorial, https://packaging.python.org/tutorials/distributing-packages/
Project-URL: Travis CI, https://travis-ci.org/pypa/twine
Project-URL: Twine documentation, https://twine.readthedocs.io/en/latest/
Description: twine
Project-URL: Twine source, https://github.com/pypa/twine/
Description: .. image:: https://img.shields.io/travis/pypa/twine/master.svg?label=travis-ci
:target: https://travis-ci.org/pypa/twine
twine
=====
.. rtd-inclusion-marker-do-not-remove
......@@ -148,7 +152,7 @@ Description: twine
Disabling Keyring
^^^^^^^^^^^^^^^^^
In some cases, the presence of keyring may be problemmatic. To disable
In some cases, the presence of keyring may be problematic. To disable
keyring and defer to a prompt for passwords, uninstall ``keyring``
or if that's not an option, you can also configure keyring to be disabled.
......@@ -171,6 +175,7 @@ Description: twine
[-s] [--sign-with SIGN_WITH] [-i IDENTITY] [-u USERNAME]
[-p PASSWORD] [-c COMMENT] [--config-file CONFIG_FILE]
[--skip-existing] [--cert path] [--client-cert path]
[--verbose] [--disable-progress-bar]
dist [dist ...]
positional arguments:
......@@ -215,6 +220,9 @@ Description: twine
--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.
``twine check``
^^^^^^^^^^^^^^^
......@@ -357,7 +365,9 @@ Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.4
Classifier: Programming Language :: Python :: 3.5
Classifier: Programming Language :: Python :: 3.6
Classifier: Programming Language :: Python :: 3.7
Classifier: Programming Language :: Python :: Implementation :: CPython
Classifier: Programming Language :: Python :: Implementation :: PyPy
Provides-Extra: keyring
Requires-Python: >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*
Provides-Extra: with-blake2
Provides-Extra: keyring
pkginfo>=1.4.2
readme_renderer>=21.0
requests!=2.15,!=2.16,>=2.5.0
requests-toolbelt>=0.8.0
requests-toolbelt!=0.9.0,>=0.8.0
setuptools>=0.7.0
tqdm>=4.14
......@@ -9,4 +9,6 @@ tqdm>=4.14
keyring
[with-blake2]
[with-blake2:python_version < "3.6" and platform_python_implementation == "CPython"]
pyblake2
......@@ -4,7 +4,7 @@
# 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
# https://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,
......@@ -22,7 +22,7 @@ __title__ = "twine"
__summary__ = "Collection of utilities for publishing packages on PyPI"
__uri__ = "https://twine.readthedocs.io/"
__version__ = "1.12.1"
__version__ = "1.13.0"
__author__ = "Donald Stufft and individual contributors"
__email__ = "donald@stufft.io"
......
......@@ -5,7 +5,7 @@
# 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
# https://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,
......@@ -27,10 +27,7 @@ def main():
try:
return dispatch(sys.argv[1:])
except (exceptions.TwineException, requests.exceptions.HTTPError) as exc:
return '{0}: {1}'.format(
exc.__class__.__name__,
exc.args[0],
)
return '{}: {}'.format(exc.__class__.__name__, exc.args[0])
if __name__ == "__main__":
......
......@@ -4,7 +4,7 @@
# 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
# https://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,
......@@ -29,7 +29,7 @@ from twine._installed import Installed
def _registered_commands(group='twine.registered_commands'):
registered_commands = pkg_resources.iter_entry_points(group=group)
return dict((c.name, c) for c in registered_commands)
return {c.name: c for c in registered_commands}
def list_dependencies_and_versions():
......@@ -44,7 +44,7 @@ def list_dependencies_and_versions():
def dep_versions():
return ', '.join(
'{0}: {1}'.format(*dependency)
'{}: {}'.format(*dependency)
for dependency in list_dependencies_and_versions()
)
......@@ -55,8 +55,10 @@ def dispatch(argv):
parser.add_argument(
"--version",
action="version",
version="%(prog)s version {0} ({1})".format(twine.__version__,
dep_versions()),
version="%(prog)s version {} ({})".format(
twine.__version__,
dep_versions(),
),
)
parser.add_argument(
"command",
......
......@@ -4,7 +4,7 @@
# 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
# https://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,
......
......@@ -4,7 +4,7 @@
# 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
# https://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,
......@@ -18,25 +18,18 @@ import argparse
import cgi
import re
import sys
from io import StringIO
try:
from StringIO import StringIO
except ImportError:
from _io import StringIO
import readme_renderer.markdown
import readme_renderer.rst
import readme_renderer.txt
from twine.commands import _find_dists
from twine.package import PackageFile
_RENDERERS = {
None: readme_renderer.rst, # Default if description_content_type is None
"": readme_renderer.rst, # Default if description_content_type is None
"text/plain": readme_renderer.txt,
"text/plain": None, # Rendering cannot fail
"text/x-rst": readme_renderer.rst,
"text/markdown": readme_renderer.markdown,
"text/markdown": None, # Rendering cannot fail
}
......@@ -85,28 +78,37 @@ def check(dists, output_stream=sys.stdout):
package = PackageFile.from_filename(filename, comment=None)
metadata = package.metadata_dictionary()
content_type, parameters = cgi.parse_header(
metadata.get("description_content_type") or ""
)
# Get the appropriate renderer
renderer = _RENDERERS.get(content_type, readme_renderer.txt)
# Actually render the given value
rendered = renderer.render(
metadata.get("description"), stream=stream, **parameters
)
description = metadata["description"]
description_content_type = metadata["description_content_type"]
if rendered is None:
failure = True
output_stream.write("Failed\n")
if description_content_type is None:
output_stream.write(
"The project's long_description has invalid markup which will "
"not be rendered on PyPI. The following syntax errors were "
"detected:\n%s" % stream
'warning: `long_description_content_type` missing. '
'defaulting to `text/x-rst`.\n'
)
else:
description_content_type = 'text/x-rst'
content_type, params = cgi.parse_header(description_content_type)
renderer = _RENDERERS.get(content_type, _RENDERERS[None])
if description in {None, 'UNKNOWN\n\n\n'}:
output_stream.write('warning: `long_description` missing.\n')
output_stream.write("Passed\n")
else:
if (
renderer
and renderer.render(description, stream=stream, **params)
is None
):
failure = True
output_stream.write("Failed\n")
output_stream.write(
"The project's long_description has invalid markup which "
"will not be rendered on PyPI. The following syntax "
"errors were detected:\n%s" % stream
)
else:
output_stream.write("Passed\n")
return failure
......
......@@ -4,7 +4,7 @@
# 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
# https://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,
......@@ -24,12 +24,12 @@ from twine import settings
def register(register_settings, package):
repository_url = register_settings.repository_config['repository']
print("Registering package to {0}".format(repository_url))
print("Registering package to {}".format(repository_url))
repository = register_settings.create_repository()
if not os.path.exists(package):
raise exceptions.PackageNotFound(
'"{0}" does not exist on the file system.'.format(package)
'"{}" does not exist on the file system.'.format(package)
)
resp = repository.register(
......
......@@ -4,7 +4,7 @@
# 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
# https://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,
......@@ -26,10 +26,14 @@ from twine import utils
def skip_upload(response, skip_existing, package):
filename = package.basefilename
# NOTE(sigmavirus24): Old PyPI returns the first message while Warehouse
# returns the latter. This papers over the differences.
msg_400 = ('A file named "{0}" already exists for'.format(filename),
'File already exists')
msg_400 = (
# Old PyPI message:
'A file named "{}" already exists for'.format(filename),
# Warehouse message:
'File already exists',
# Nexus Repository OSS message:
'Repository does not allow updating assets',
)
msg_403 = 'Not enough permissions to overwrite artifact'
# NOTE(sigmavirus24): PyPI presently returns a 400 status code with the
# error message in the reason attribute. Other implementations return a
......@@ -38,7 +42,7 @@ def skip_upload(response, skip_existing, package):
# True) AND
# 2. a) The response status code is 409 OR
# 2. b) The response status code is 400 AND it has a reason that matches
# what we expect PyPI to return to us. OR
# what we expect PyPI or Nexus OSS to return to us. OR
# 2. c) The response status code is 403 AND the text matches what we
# expect Artifactory to return to us.
return (skip_existing and (response.status_code == 409 or
......@@ -51,21 +55,19 @@ def upload(upload_settings, dists):
dists = _find_dists(dists)
# Determine if the user has passed in pre-signed distributions
signatures = dict(
(os.path.basename(d), d) for d in dists if d.endswith(".asc")
)
signatures = {os.path.basename(d): d for d in dists if d.endswith(".asc")}
uploads = [i for i in dists if not i.endswith(".asc")]
upload_settings.check_repository_url()
repository_url = upload_settings.repository_config['repository']
print("Uploading distributions to {0}".format(repository_url))
print("Uploading distributions to {}".format(repository_url))
repository = upload_settings.create_repository()
for filename in uploads:
package = PackageFile.from_filename(filename, upload_settings.comment)
skip_message = (
" Skipping {0} because it appears to already exist".format(
" Skipping {} because it appears to already exist".format(
package.basefilename)
)
......
......@@ -5,7 +5,7 @@
# 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
# https://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,
......@@ -50,10 +50,10 @@ class UploadToDeprecatedPyPIDetected(TwineException):
@classmethod
def from_args(cls, target_url, default_url, test_url):
"""Return an UploadToDeprecatedPyPIDetected instance."""
return cls("You're trying to upload to the legacy PyPI site '{0}'. "
return cls("You're trying to upload to the legacy PyPI site '{}'. "
"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. "
"{} (or {}) 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(target_url, default_url, test_url)
......
......@@ -4,7 +4,7 @@
# 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
# https://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,
......@@ -160,7 +160,7 @@ class PackageFile(object):
self.gpg_signature = (signature_filename, gpg.read())
def sign(self, sign_with, identity):
print("Signing {0}".format(self.basefilename))
print("Signing {}".format(self.basefilename))
gpg_args = (sign_with, "--detach-sign")
if identity:
gpg_args += ("--local-user", identity)
......
......@@ -4,7 +4,7 @@
# 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
# https://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,
......@@ -28,7 +28,7 @@ from requests_toolbelt.utils import user_agent
import twine
KEYWORDS_TO_NOT_FLATTEN = set(["gpg_signature", "content"])
KEYWORDS_TO_NOT_FLATTEN = {"gpg_signature", "content"}
LEGACY_PYPI = 'https://pypi.python.org/'
LEGACY_TEST_PYPI = 'https://testpypi.python.org/'
......@@ -47,7 +47,8 @@ class ProgressBar(tqdm):
class Repository(object):
def __init__(self, repository_url, username, password):
def __init__(self, repository_url, username, password,
disable_progress_bar=False):
self.url = repository_url
self.session = requests.session()
self.session.auth = (username, password)
......@@ -55,6 +56,7 @@ class Repository(object):
for scheme in ('http://', 'https://'):
self.session.mount(scheme, self._make_adapter_with_retries())
self._releases_json_data = {}
self.disable_progress_bar = disable_progress_bar
@staticmethod
def _make_adapter_with_retries():
......@@ -106,7 +108,7 @@ class Repository(object):
"protocol_version": "1",
})
print("Registering {0}".format(package.basefilename))
print("Registering {}".format(package.basefilename))
data_to_send = self._convert_data_to_list_of_tuples(data)
encoder = MultipartEncoder(data_to_send)
......@@ -130,7 +132,7 @@ class Repository(object):
data_to_send = self._convert_data_to_list_of_tuples(data)
print("Uploading {0}".format(package.basefilename))
print("Uploading {}".format(package.basefilename))
with open(package.filename, "rb") as fp:
data_to_send.append((
......@@ -140,7 +142,8 @@ class Repository(object):
encoder = MultipartEncoder(data_to_send)
with ProgressBar(total=encoder.len,
unit='B', unit_scale=True, unit_divisor=1024,
miniters=1, file=sys.stdout) as bar:
miniters=1, file=sys.stdout,
disable=self.disable_progress_bar) as bar:
monitor = MultipartEncoderMonitor(
encoder, lambda monitor: bar.update_to(monitor.bytes_read)
)
......
......@@ -5,7 +5,7 @@
# 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
# https://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,
......@@ -46,6 +46,7 @@ class Settings(object):
cacert=None, client_cert=None,
repository_name='pypi', repository_url=None,
verbose=False,
disable_progress_bar=False,
**ignored_kwargs
):
"""Initialize our settings instance.
......@@ -95,10 +96,15 @@ class Settings(object):
will override the settings inferred from ``repository_name``.
:param bool verbose:
Show verbose output.
:param bool disable_progress_bar:
Disable the progress bar.
This defaults to ``False``
"""
self.config_file = config_file
self.comment = comment
self.verbose = verbose
self.disable_progress_bar = disable_progress_bar
self.skip_existing = skip_existing
self._handle_repository_options(
repository_name=repository_name, repository_url=repository_url,
......@@ -206,6 +212,13 @@ class Settings(object):
action="store_true",
help="Show verbose output."
)
parser.add_argument(
"--disable-progress-bar",
default=False,
required=False,
action="store_true",
help="Disable the progress bar."
)
@classmethod
def from_argparse(cls, args):
......@@ -235,7 +248,11 @@ class Settings(object):
)
def _handle_authentication(self, username, password):
self.username = utils.get_username(username, self.repository_config)
self.username = utils.get_username(
self.repository_config['repository'],
username,
self.repository_config
)
self.password = utils.get_password(
self.repository_config['repository'],
self.username,
......@@ -272,6 +289,7 @@ class Settings(object):
self.repository_config['repository'],
self.username,
self.password,
self.disable_progress_bar
)
repo.set_certificate_authority(self.cacert)
repo.set_client_certificate(self.client_cert)
......
......@@ -4,7 +4,7 @@
# 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
# https://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,
......@@ -35,6 +35,11 @@ try:
except ImportError:
from urllib.parse import urlparse, urlunparse
try:
import keyring # noqa
except ImportError:
pass
from twine import exceptions
# Shim for raw_input in python3
......@@ -110,7 +115,7 @@ def get_repository_from_config(config_file, repository, repository_url=None):
}
if repository_url and "://" not in repository_url:
raise exceptions.UnreachableRepositoryURLDetected(
"Repository URL {0} has no protocol. Please add "
"Repository URL {} has no protocol. Please add "
"'https://'. \n".format(repository_url))
try:
return get_config(config_file)[repository]
......@@ -128,8 +133,8 @@ def get_repository_from_config(config_file, repository, repository_url=None):
raise exceptions.InvalidConfiguration(msg)
_HOSTNAMES = set(["pypi.python.org", "testpypi.python.org", "upload.pypi.org",
"test.pypi.org"])
_HOSTNAMES = {"pypi.python.org", "testpypi.python.org", "upload.pypi.org",
"test.pypi.org"}
def normalize_repository_url(url):
......@@ -190,7 +195,7 @@ def get_userpass_value(cli_value, config, key, prompt_strategy=None):
"""
if cli_value is not None:
return cli_value
elif config.get(key):
elif config.get(key) is not None:
return config[key]
elif prompt_strategy:
return prompt_strategy()
......@@ -198,6 +203,23 @@ def get_userpass_value(cli_value, config, key, prompt_strategy=None):
return None
def get_username_from_keyring(system):
if 'keyring' not in sys.modules:
return
try:
getter = sys.modules['keyring'].get_credential
except AttributeError:
return None
try:
creds = getter(system, None)
if creds:
return creds.username
except Exception as exc:
warnings.warn(str(exc))
def password_prompt(prompt_text): # Always expects unicode for our own sanity
prompt = prompt_text
# Workaround for https://github.com/pypa/twine/issues/116
......@@ -211,12 +233,18 @@ def get_password_from_keyring(system, username):
return
try:
import keyring
return keyring.get_password(system, username)
return sys.modules['keyring'].get_password(system, username)
except Exception as exc:
warnings.warn(str(exc))
def username_from_keyring_or_prompt(system):
return (
get_username_from_keyring(system)
or input_func('Enter your username: ')
)
def password_from_keyring_or_prompt(system, username):
return (
get_password_from_keyring(system, username)
......@@ -224,11 +252,18 @@ def password_from_keyring_or_prompt(system, username):
)
get_username = functools.partial(
get_userpass_value,
key='username',
prompt_strategy=functools.partial(input_func, 'Enter your username: '),
)
def get_username(system, cli_value, config):
return get_userpass_value(
cli_value,
config,
key='username',
prompt_strategy=functools.partial(
username_from_keyring_or_prompt,
system,
),
)
get_cacert = functools.partial(
get_userpass_value,
key='ca_cert',
......
......@@ -4,7 +4,7 @@
# 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
# https://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,
......@@ -17,11 +17,7 @@ from __future__ import unicode_literals
import os
import re
import zipfile
try:
from StringIO import StringIO
except ImportError:
from _io import StringIO
from io import StringIO
from pkginfo import distribution
from pkginfo.distribution import Distribution
......