...
 
Commits (9)
[run]
branch = True
[report]
# Regexes for lines to exclude from consideration
exclude_lines =
# Have to re-enable the standard pragma
pragma: no cover
# Don't complain if non-runnable code isn't run
if __name__ == .__main__.:
# Paths to omit from consideration
omit =
# __main__.py exists only as a very basic wrapper around warehouse.cli
# and exists only to provide setuptools and python -m a place to point
# at.
*/twine/__main__.py
......@@ -18,3 +18,8 @@ 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)
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
include LICENSE
include README.rst
include AUTHORS
include .coveragerc
recursive-include tests *.py
recursive-include docs *.bat
recursive-include docs *.empty
recursive-include docs *.py
recursive-include docs *.rst
recursive-include docs Makefile
recursive-include tests *.py *.whl deprecated-pypirc
recursive-include docs *.bat *.empty *.py *.rst Makefile *.txt
prune docs/_build
prune *.yml
Metadata-Version: 2.1
Name: twine
Version: 1.11.0
Version: 1.12.1
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: Twine documentation, https://twine.readthedocs.io/en/latest/
Project-URL: Twine source, https://github.com/pypa/twine/
Description: twine
=====
.. rtd-inclusion-marker-do-not-remove
Twine is `a utility`_ for `publishing`_ packages on `PyPI`_.
Twine is `a utility`_ for `publishing`_ Python packages on `PyPI`_.
Currently it only supports registering `projects`_ and uploading `distributions`_.
It provides build system independent uploads of source and binary
`distribution artifacts <distributions>`_ for both new and existing
`projects`_.
Why Should I Use This?
......@@ -26,17 +28,16 @@ Description: twine
security and testability.
The biggest reason to use ``twine`` is that it securely authenticates
you to `PyPI`_ over HTTPS using a verified connection, while ``python
setup.py upload`` `only recently stopped using HTTP
<https://bugs.python.org/issue12226>`_ in Python 2.7.9+ and Python
3.2+. This means anytime you use ``python setup.py upload`` with an
older Python version, you expose your username and password to being
easily sniffed. Twine uses only verified TLS to upload to PyPI,
protecting your credentials from theft.
you to `PyPI`_ over HTTPS using a verified connection regardless of
the underlying Python version, while whether or not
``python setup.py upload`` will work correctly and securely depends
on your build system, your Python version and the underlying operating
system.
Secondly, it allows you to precreate your distribution files.
``python setup.py upload`` only allows you to upload something that you've
created in the same command invocation. This means that you cannot test the
``python setup.py upload`` only allows you to upload something that you're
building with ``distutils`` or ``setuptools``, and created in the same
command invocation. This means that you cannot test the
exact file you're going to upload to PyPI to ensure that it works before
uploading it.
......@@ -144,10 +145,24 @@ Description: twine
.. _`Using Keyring on headless systems`:
https://keyring.readthedocs.io/en/latest/#using-keyring-on-headless-linux-systems
Disabling Keyring
^^^^^^^^^^^^^^^^^
In some cases, the presence of keyring may be problemmatic. 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.
See `twine 338 <https://github.com/pypa/twine/issues/338>`_ for a
discussion on ways to do that.
Options
-------
``twine upload``
^^^^^^^^^^^^^^^^
Uploads one or more distributions to a repository.
.. code-block:: console
$ twine upload -h
......@@ -201,16 +216,29 @@ Description: twine
containing the private key and the certificate in PEM
format.
Twine also includes a ``register`` command.
``twine check``
^^^^^^^^^^^^^^^
Checks whether your distributions long description will render correctly on PyPI.
.. code-block:: console
$ twine check -h
usage: twine check [-h] dist [dist ...]
positional arguments:
dist The distribution files to check, usually dist/*
optional arguments:
-h, --help show this help message and exit
``twine register``
^^^^^^^^^^^^^^^^^^
.. WARNING::
``register`` is `no longer necessary if you are
uploading to pypi.org
<https://packaging.python.org/guides/migrating-to-pypi-org/#registering-package-names-metadata>`_. As
such, it is `no longer supported
<https://github.com/pypa/warehouse/issues/1627>`_ in `Warehouse`_
(the new PyPI software running on pypi.org). However, you may need
this if you are using a different package index.
**WARNING**: The ``register`` command is `no longer necessary if you are uploading to
pypi.org`_. As such, it is `no longer supported`_ in `Warehouse`_ (the new
PyPI software running on pypi.org). However, you may need this if you are using
a different package index.
For completeness, its usage:
......@@ -310,6 +338,8 @@ Description: twine
.. _`PyPA Code of Conduct`: https://www.pypa.io/en/latest/code-of-conduct/
.. _`Warehouse`: https://github.com/pypa/warehouse
.. _`wheels`: https://packaging.python.org/glossary/#term-wheel
.. _`no longer necessary if you are uploading to pypi.org`: https://packaging.python.org/guides/migrating-to-pypi-org/#registering-package-names-metadata
.. _`no longer supported`: https://github.com/pypa/warehouse/issues/1627
Platform: UNKNOWN
Classifier: Intended Audience :: Developers
......@@ -324,7 +354,6 @@ Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 2
Classifier: Programming Language :: Python :: 2.7
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.3
Classifier: Programming Language :: Python :: 3.4
Classifier: Programming Language :: Python :: 3.5
Classifier: Programming Language :: Python :: 3.6
......
......@@ -3,9 +3,11 @@ twine
.. rtd-inclusion-marker-do-not-remove
Twine is `a utility`_ for `publishing`_ packages on `PyPI`_.
Twine is `a utility`_ for `publishing`_ Python packages on `PyPI`_.
Currently it only supports registering `projects`_ and uploading `distributions`_.
It provides build system independent uploads of source and binary
`distribution artifacts <distributions>`_ for both new and existing
`projects`_.
Why Should I Use This?
......@@ -15,17 +17,16 @@ The goal of ``twine`` is to improve PyPI interaction by improving
security and testability.
The biggest reason to use ``twine`` is that it securely authenticates
you to `PyPI`_ over HTTPS using a verified connection, while ``python
setup.py upload`` `only recently stopped using HTTP
<https://bugs.python.org/issue12226>`_ in Python 2.7.9+ and Python
3.2+. This means anytime you use ``python setup.py upload`` with an
older Python version, you expose your username and password to being
easily sniffed. Twine uses only verified TLS to upload to PyPI,
protecting your credentials from theft.
you to `PyPI`_ over HTTPS using a verified connection regardless of
the underlying Python version, while whether or not
``python setup.py upload`` will work correctly and securely depends
on your build system, your Python version and the underlying operating
system.
Secondly, it allows you to precreate your distribution files.
``python setup.py upload`` only allows you to upload something that you've
created in the same command invocation. This means that you cannot test the
``python setup.py upload`` only allows you to upload something that you're
building with ``distutils`` or ``setuptools``, and created in the same
command invocation. This means that you cannot test the
exact file you're going to upload to PyPI to ensure that it works before
uploading it.
......@@ -133,10 +134,24 @@ The next time you run ``twine`` it will prompt you for a username and will grab
.. _`Using Keyring on headless systems`:
https://keyring.readthedocs.io/en/latest/#using-keyring-on-headless-linux-systems
Disabling Keyring
^^^^^^^^^^^^^^^^^
In some cases, the presence of keyring may be problemmatic. 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.
See `twine 338 <https://github.com/pypa/twine/issues/338>`_ for a
discussion on ways to do that.
Options
-------
``twine upload``
^^^^^^^^^^^^^^^^
Uploads one or more distributions to a repository.
.. code-block:: console
$ twine upload -h
......@@ -190,16 +205,29 @@ Options
containing the private key and the certificate in PEM
format.
Twine also includes a ``register`` command.
``twine check``
^^^^^^^^^^^^^^^
Checks whether your distributions long description will render correctly on PyPI.
.. code-block:: console
$ twine check -h
usage: twine check [-h] dist [dist ...]
positional arguments:
dist The distribution files to check, usually dist/*
optional arguments:
-h, --help show this help message and exit
``twine register``
^^^^^^^^^^^^^^^^^^
.. WARNING::
``register`` is `no longer necessary if you are
uploading to pypi.org
<https://packaging.python.org/guides/migrating-to-pypi-org/#registering-package-names-metadata>`_. As
such, it is `no longer supported
<https://github.com/pypa/warehouse/issues/1627>`_ in `Warehouse`_
(the new PyPI software running on pypi.org). However, you may need
this if you are using a different package index.
**WARNING**: The ``register`` command is `no longer necessary if you are uploading to
pypi.org`_. As such, it is `no longer supported`_ in `Warehouse`_ (the new
PyPI software running on pypi.org). However, you may need this if you are using
a different package index.
For completeness, its usage:
......@@ -299,3 +327,5 @@ trackers, chat rooms, and mailing lists is expected to follow the
.. _`PyPA Code of Conduct`: https://www.pypa.io/en/latest/code-of-conduct/
.. _`Warehouse`: https://github.com/pypa/warehouse
.. _`wheels`: https://packaging.python.org/glossary/#term-wheel
.. _`no longer necessary if you are uploading to pypi.org`: https://packaging.python.org/guides/migrating-to-pypi-org/#registering-package-names-metadata
.. _`no longer supported`: https://github.com/pypa/warehouse/issues/1627
twine (1.11.0-2) UNRELEASED; urgency=medium
twine (1.12.1-1) unstable; urgency=medium
[ Stefano Rivera ]
* New upstream release (LP: #1775886)
* New B-D: python3-readme-renderer.
* Bump debhelper compat level to 11.
* Bump Standards-Version to 4.2.1, no changes needed.
[ Ondřej Nový ]
* d/control: Remove ancient X-Python3-Version field
-- Ondřej Nový <onovy@debian.org> Mon, 14 May 2018 08:07:19 +0200
-- Stefano Rivera <stefanor@debian.org> Sat, 13 Oct 2018 14:42:11 +0200
twine (1.11.0-1) unstable; urgency=medium
......
......@@ -5,17 +5,18 @@ Maintainer: Python Applications Packaging Team <python-apps-team@lists.alioth.de
Uploaders: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>,
Barry Warsaw <barry@debian.org>,
Stefano Rivera <stefanor@debian.org>
Build-Depends: debhelper (>= 10),
Build-Depends: debhelper (>= 11),
dh-python,
python3-all,
python3-pkg-resources,
python3-pkginfo (>= 1.4.2),
python3-readme-renderer (>= 17.4),
python3-releases (>= 0.6.1),
python3-requests (>= 2.3.0),
python3-setuptools (>= 0.7),
python3-sphinx,
python3-sphinx-rtd-theme
Standards-Version: 4.1.3
Standards-Version: 4.2.1
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
......
......@@ -10,7 +10,7 @@ Forwarded: no
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/conf.py b/docs/conf.py
index a1e8a0c..1f25ba8 100644
index 67bc813..53fe6bd 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -26,7 +26,7 @@ import twine
......
......@@ -4,6 +4,13 @@
Changelog
=========
* :release:`1.12.1 <2018-09-24>`
* :bug:`404` Fix regression with upload exit code
* :release:`1.12.0 <2018-09-24>`
* :feature:`395 major` Add ``twine check`` command to check long description
* :feature:`392 major` Drop support for Python 3.3
* :feature:`363` Empower ``--skip-existing`` for Artifactory repositories
* :bug:`367` Avoid MD5 when Python is compiled in FIPS mode
* :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
......
......@@ -73,7 +73,7 @@ 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.
Also, always run ``tox -e lint`` before submitting a pull request.
Submitting changes
^^^^^^^^^^^^^^^^^^
......@@ -157,7 +157,7 @@ A checklist for creating, testing, and distributing a new version.
#. Run Twine tests:
#. ``tox -e py{27,34,35,36,py}``
#. ``tox -e pep8`` for the linter
#. ``tox -e lint`` 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)
......
doc8>=0.8.0
readme-renderer>=17.4
releases>=1.4.0
Sphinx>=1.7.0
sphinx_rtd_theme>=0.2.4
tox>=2.9.1
twine>=1.10.0
[wheel]
[bdist_wheel]
universal = 1
[check-manifest]
ignore =
.travis.yml
tox.ini
.github
.github/*
[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
argparse; python_version == '2.6'
pyblake2; extra == 'with-blake2' and python_version < '3.6'
keyring; extra == 'keyring'
......
......@@ -18,20 +18,6 @@ import sys
import twine
install_requires = [
"tqdm >= 4.14",
"pkginfo >= 1.4.2",
"requests >= 2.5.0, != 2.15, != 2.16",
"requests-toolbelt >= 0.8.0",
"setuptools >= 0.7.0",
]
if sys.version_info[:2] < (2, 7):
install_requires += [
"argparse",
]
blake2_requires = []
if sys.version_info[:2] < (3, 6):
......@@ -70,7 +56,6 @@ setup(
"Programming Language :: Python :: 2",
"Programming Language :: Python :: 2.7",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.3",
"Programming Language :: Python :: 3.4",
"Programming Language :: Python :: 3.5",
"Programming Language :: Python :: 3.6",
......@@ -82,6 +67,7 @@ setup(
entry_points={
"twine.registered_commands": [
"check = twine.commands.check:main",
"upload = twine.commands.upload:main",
"register = twine.commands.register:main",
],
......@@ -90,7 +76,14 @@ setup(
],
},
install_requires=install_requires,
install_requires=[
"pkginfo >= 1.4.2",
"readme_renderer >= 21.0",
"requests >= 2.5.0, != 2.15, != 2.16",
"requests-toolbelt >= 0.8.0",
"setuptools >= 0.7.0",
"tqdm >= 4.14",
],
extras_require={
'with-blake2': blake2_requires,
'keyring': [
......
[server-login]
username:testusername
password:testpassword
[pypi]
foo:bar
# Copyright 2018 Dustin Ingram
#
# 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 __future__ import unicode_literals
import pretend
from twine.commands import check
def test_warningstream_write_match():
stream = check._WarningStream()
stream.output = pretend.stub(write=pretend.call_recorder(lambda a: None))
stream.write("<string>:2: (WARNING/2) Title underline too short.")
assert stream.output.write.calls == [
pretend.call("line 2: Warning: Title underline too short.\n")
]
def test_warningstream_write_nomatch():
stream = check._WarningStream()
stream.output = pretend.stub(write=pretend.call_recorder(lambda a: None))
stream.write("this does not match")
assert stream.output.write.calls == [pretend.call("this does not match")]
def test_warningstream_str():
stream = check._WarningStream()
stream.output = pretend.stub(getvalue=lambda: "result")
assert str(stream) == "result"
def test_check_no_distributions(monkeypatch):
stream = check.StringIO()
monkeypatch.setattr(check, "_find_dists", lambda a: [])
assert not check.check("dist/*", output_stream=stream)
assert stream.getvalue() == ""
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"})
output_stream = check.StringIO()
warning_stream = ""
monkeypatch.setattr(check, "_RENDERERS", {"": renderer})
monkeypatch.setattr(check, "_find_dists", lambda a: ["dist/dist.tar.gz"])
monkeypatch.setattr(
check,
"PackageFile",
pretend.stub(from_filename=lambda *a, **kw: package),
)
monkeypatch.setattr(check, "_WarningStream", lambda: warning_stream)
assert not check.check("dist/*", output_stream=output_stream)
assert (
output_stream.getvalue()
== "Checking distribution dist/dist.tar.gz: Passed\n"
)
assert renderer.render.calls == [
pretend.call("blah", stream=warning_stream)
]
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"})
output_stream = check.StringIO()
warning_stream = "WARNING"
monkeypatch.setattr(check, "_RENDERERS", {"": renderer})
monkeypatch.setattr(check, "_find_dists", lambda a: ["dist/dist.tar.gz"])
monkeypatch.setattr(
check,
"PackageFile",
pretend.stub(from_filename=lambda *a, **kw: package),
)
monkeypatch.setattr(check, "_WarningStream", lambda: warning_stream)
assert check.check("dist/*", output_stream=output_stream)
assert output_stream.getvalue() == (
"Checking distribution dist/dist.tar.gz: Failed\n"
"The project's long_description has invalid markup which will not be "
"rendered on PyPI. The following syntax errors were detected:\n"
"WARNING"
)
assert renderer.render.calls == [
pretend.call("blah", stream=warning_stream)
]
def test_main(monkeypatch):
check_result = pretend.stub()
check_stub = pretend.call_recorder(lambda a: check_result)
monkeypatch.setattr(check, "check", check_stub)
assert check.main(["dist/*"]) == check_result
assert check_stub.calls == [pretend.call(["dist/*"])]
import os
import pytest
from twine.commands import _find_dists, _group_wheel_files_first
from twine import exceptions
def test_ensure_wheel_files_uploaded_first():
files = _group_wheel_files_first(
["twine/foo.py", "twine/first.whl", "twine/bar.py", "twine/second.whl"]
)
expected = [
"twine/first.whl",
"twine/second.whl",
"twine/foo.py",
"twine/bar.py",
]
assert expected == files
def test_ensure_if_no_wheel_files():
files = _group_wheel_files_first(["twine/foo.py", "twine/bar.py"])
expected = ["twine/foo.py", "twine/bar.py"]
assert expected == files
def test_find_dists_expands_globs():
files = sorted(_find_dists(["twine/__*.py"]))
expected = [
os.path.join("twine", "__init__.py"),
os.path.join("twine", "__main__.py"),
]
assert expected == files
def test_find_dists_errors_on_invalid_globs():
with pytest.raises(exceptions.InvalidDistribution):
_find_dists(["twine/*.rb"])
def test_find_dists_handles_real_files():
expected = [
"twine/__init__.py",
"twine/__main__.py",
"twine/cli.py",
"twine/utils.py",
"twine/wheel.py",
]
files = _find_dists(expected)
assert expected == files
......@@ -11,11 +11,14 @@
# limitations under the License.
from twine import __main__ as dunder_main
from twine import exceptions
import pretend
def test_exception_handling(monkeypatch):
replaced_dispatch = pretend.raiser(KeyError('foo'))
replaced_dispatch = pretend.raiser(
exceptions.InvalidConfiguration('foo')
)
monkeypatch.setattr(dunder_main, 'dispatch', replaced_dispatch)
assert dunder_main.main() == 'KeyError: foo'
assert dunder_main.main() == 'InvalidConfiguration: foo'
......@@ -12,6 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from __future__ import unicode_literals
import platform
from twine import package
import pretend
......@@ -163,3 +164,47 @@ def test_metadata_dictionary(gpg_signature):
# GPG signature
assert result.get('gpg_signature') == gpg_signature
TWINE_1_5_0_WHEEL_HEXDIGEST = package.Hexdigest(
'1919f967e990bee7413e2a4bc35fd5d1',
'd86b0f33f0c7df49e888b11c43b417da5520cbdbce9f20618b1494b600061e67',
'b657a4148d05bd0098c1d6d8cc4e14e766dbe93c3a5ab6723b969da27a87bac0',
)
if platform.python_implementation().lower() == 'pypy':
# pyblake2 refuses to install on PyPy
TWINE_1_5_0_WHEEL_HEXDIGEST = TWINE_1_5_0_WHEEL_HEXDIGEST._replace(
blake2=None,
)
def test_hash_manager():
"""Verify our HashManager works."""
filename = 'tests/fixtures/twine-1.5.0-py2.py3-none-any.whl'
hasher = package.HashManager(filename)
hasher.hash()
assert hasher.hexdigest() == TWINE_1_5_0_WHEEL_HEXDIGEST
def test_fips_hash_manager(monkeypatch):
"""Verify the behaviour if hashlib is using FIPS mode."""
replaced_md5 = pretend.raiser(ValueError('fipsmode'))
monkeypatch.setattr(package.hashlib, 'md5', replaced_md5)
filename = 'tests/fixtures/twine-1.5.0-py2.py3-none-any.whl'
hasher = package.HashManager(filename)
hasher.hash()
hashes = TWINE_1_5_0_WHEEL_HEXDIGEST._replace(md5=None)
assert hasher.hexdigest() == hashes
def test_no_blake2_hash_manager(monkeypatch):
"""Verify the behaviour with missing blake2."""
monkeypatch.setattr(package, 'blake2b', None)
filename = 'tests/fixtures/twine-1.5.0-py2.py3-none-any.whl'
hasher = package.HashManager(filename)
hasher.hash()
hashes = TWINE_1_5_0_WHEEL_HEXDIGEST._replace(blake2=None)
assert hasher.hexdigest() == hashes
"""Tests for the Settings class and module."""
# Copyright 2018 Ian Stapleton Cordasco
#
# 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 __future__ import unicode_literals
import os.path
import textwrap
from twine import exceptions
from twine import settings
import pytest
def test_settings_takes_no_positional_arguments():
"""Verify that the Settings initialization is kw-only."""
with pytest.raises(TypeError):
settings.Settings('a', 'b', 'c')
def test_settings_transforms_config(tmpdir):
"""Verify that the settings object transforms the passed in options."""
pypirc = os.path.join(str(tmpdir), ".pypirc")
with open(pypirc, "w") as fp:
fp.write(textwrap.dedent("""
[pypi]
repository: https://upload.pypi.org/legacy/
username:username
password:password
"""))
s = settings.Settings(config_file=pypirc)
assert (s.repository_config['repository'] ==
'https://upload.pypi.org/legacy/')
assert s.sign is False
assert s.sign_with == 'gpg'
assert s.identity is None
assert s.username == 'username'
assert s.password == 'password'
assert s.cacert is None
assert s.client_cert is None
def test_identity_requires_sign():
"""Verify that if a user passes identity, we require sign=True."""
with pytest.raises(exceptions.InvalidSigningConfiguration):
settings.Settings(sign=False, identity='fakeid')
......@@ -20,7 +20,7 @@ import pretend
import pytest
from twine.commands import upload
from twine import package, cli, exceptions
from twine import package, cli, exceptions, settings
import twine
import helpers
......@@ -28,48 +28,42 @@ import helpers
WHEEL_FIXTURE = 'tests/fixtures/twine-1.5.0-py2.py3-none-any.whl'
def test_ensure_wheel_files_uploaded_first():
files = upload.group_wheel_files_first(["twine/foo.py",
"twine/first.whl",
"twine/bar.py",
"twine/second.whl"])
expected = ["twine/first.whl",
"twine/second.whl",
"twine/foo.py",
"twine/bar.py"]
assert expected == files
def test_ensure_if_no_wheel_files():
files = upload.group_wheel_files_first(["twine/foo.py",
"twine/bar.py"])
expected = ["twine/foo.py",
"twine/bar.py"]
assert expected == files
def test_successful_upload(tmpdir):
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]
username:foo
password:bar
"""))
def test_find_dists_expands_globs():
files = sorted(upload.find_dists(['twine/__*.py']))
expected = [os.path.join('twine', '__init__.py'),
os.path.join('twine', '__main__.py')]
assert expected == files
upload_settings = settings.Settings(
repository_name="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, verbose=False,
)
stub_response = pretend.stub(
is_redirect=False, status_code=201, raise_for_status=lambda: None
)
stub_repository = pretend.stub(
upload=lambda package: stub_response, close=lambda: None
)
def test_find_dists_errors_on_invalid_globs():
with pytest.raises(ValueError):
upload.find_dists(['twine/*.rb'])
upload_settings.create_repository = lambda: stub_repository
result = upload.upload(upload_settings, dists)
def test_find_dists_handles_real_files():
expected = ['twine/__init__.py', 'twine/__main__.py', 'twine/cli.py',
'twine/utils.py', 'twine/wheel.py']
files = upload.find_dists(expected)
assert expected == files
# Raising an exception or returning anything truthy would mean that the
# upload has failed
assert result is None
def test_get_config_old_format(tmpdir):
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("""
......@@ -79,12 +73,12 @@ def test_get_config_old_format(tmpdir):
"""))
try:
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,
)
settings.Settings(
repository_name="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, verbose=False,
)
except KeyError as err:
assert err.args[0] == (
"Missing 'pypi' section from the configuration file\n"
......@@ -108,25 +102,27 @@ def test_deprecated_repo(tmpdir):
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/ ."
)
upload_settings = settings.Settings(
repository_name="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, verbose=False,
)
upload.upload(upload_settings, dists)
assert err.value.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):
......@@ -155,6 +151,21 @@ def test_skip_existing_skips_files_already_on_pypiserver(monkeypatch):
package=pkg) is True
def test_skip_existing_skips_files_already_on_artifactory(monkeypatch):
# Artifactory (https://jfrog.com/artifactory/) responds with 403
# when the file already exists.
response = pretend.stub(
status_code=403,
text="Not enough permissions to overwrite artifact "
"'pypi-local:twine/1.5.0/twine-1.5.0-py2.py3-none-any.whl'"
"(user 'twine-deployer' needs DELETE permission).")
pkg = package.PackageFile.from_filename(WHEEL_FIXTURE, None)
assert upload.skip_upload(response=response,
skip_existing=True,
package=pkg) is True
def test_skip_upload_respects_skip_existing(monkeypatch):
response = pretend.stub(
status_code=400,
......@@ -178,12 +189,7 @@ def test_values_from_env(monkeypatch):
"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"]
upload_settings = replaced_upload.calls[0].args[0]
assert "pypipassword" == upload_settings.password
assert "pypiuser" == upload_settings.username
assert "/foo/bar.crt" == upload_settings.cacert
......@@ -18,11 +18,6 @@ import sys
import os.path
import textwrap
try:
import builtins
except ImportError:
import __builtin__ as builtins
import pytest
from twine import utils
......@@ -72,6 +67,11 @@ def test_get_config_no_distutils(tmpdir):
"username": "testuser",
"password": "testpassword",
},
"testpypi": {
"repository": utils.TEST_REPOSITORY,
"username": None,
"password": None,
},
}
......@@ -97,6 +97,18 @@ def test_get_config_no_section(tmpdir):
}
def test_get_config_override_pypi_url(tmpdir):
pypirc = os.path.join(str(tmpdir), ".pypirc")
with open(pypirc, "w") as fp:
fp.write(textwrap.dedent("""
[pypi]
repository = http://pypiproxy
"""))
assert utils.get_config(pypirc)['pypi']['repository'] == 'http://pypiproxy'
def test_get_config_missing(tmpdir):
pypirc = os.path.join(str(tmpdir), ".pypirc")
......@@ -106,7 +118,7 @@ def test_get_config_missing(tmpdir):
"username": None,
"password": None,
},
"pypitest": {
"testpypi": {
"repository": utils.TEST_REPOSITORY,
"username": None,
"password": None
......@@ -143,8 +155,13 @@ def test_get_config_deprecated_pypirc():
assert utils.get_config(deprecated_pypirc_path) == {
"pypi": {
"repository": utils.DEFAULT_REPOSITORY,
"username": 'testusername',
"password": 'testpassword',
"username": "testusername",
"password": "testpassword",
},
"testpypi": {
"repository": utils.TEST_REPOSITORY,
"username": "testusername",
"password": "testpassword",
},
}
......@@ -217,13 +234,7 @@ 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)
monkeypatch.delitem(sys.modules, 'keyring', raising=False)
@pytest.fixture
......@@ -256,3 +267,27 @@ def test_get_password_runtime_error_suppressed(
assert len(recwarn) == 1
warning = recwarn.pop(UserWarning)
assert 'fail!' in str(warning)
def test_no_positional_on_method():
class T(object):
@utils.no_positional(allow_self=True)
def __init__(self, foo=False):
self.foo = foo
with pytest.raises(TypeError):
T(1)
t = T(foo=True)
assert t.foo
def test_no_positional_on_function():
@utils.no_positional()
def t(foo=False):
return foo
with pytest.raises(TypeError):
t(1)
assert t(foo=True)
Metadata-Version: 2.1
Name: twine
Version: 1.11.0
Version: 1.12.1
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: Twine documentation, https://twine.readthedocs.io/en/latest/
Project-URL: Twine source, https://github.com/pypa/twine/
Description: twine
=====
.. rtd-inclusion-marker-do-not-remove
Twine is `a utility`_ for `publishing`_ packages on `PyPI`_.
Twine is `a utility`_ for `publishing`_ Python packages on `PyPI`_.
Currently it only supports registering `projects`_ and uploading `distributions`_.
It provides build system independent uploads of source and binary
`distribution artifacts <distributions>`_ for both new and existing
`projects`_.
Why Should I Use This?
......@@ -26,17 +28,16 @@ Description: twine
security and testability.
The biggest reason to use ``twine`` is that it securely authenticates
you to `PyPI`_ over HTTPS using a verified connection, while ``python
setup.py upload`` `only recently stopped using HTTP
<https://bugs.python.org/issue12226>`_ in Python 2.7.9+ and Python
3.2+. This means anytime you use ``python setup.py upload`` with an
older Python version, you expose your username and password to being
easily sniffed. Twine uses only verified TLS to upload to PyPI,
protecting your credentials from theft.
you to `PyPI`_ over HTTPS using a verified connection regardless of
the underlying Python version, while whether or not
``python setup.py upload`` will work correctly and securely depends
on your build system, your Python version and the underlying operating
system.
Secondly, it allows you to precreate your distribution files.
``python setup.py upload`` only allows you to upload something that you've
created in the same command invocation. This means that you cannot test the
``python setup.py upload`` only allows you to upload something that you're
building with ``distutils`` or ``setuptools``, and created in the same
command invocation. This means that you cannot test the
exact file you're going to upload to PyPI to ensure that it works before
uploading it.
......@@ -144,10 +145,24 @@ Description: twine
.. _`Using Keyring on headless systems`:
https://keyring.readthedocs.io/en/latest/#using-keyring-on-headless-linux-systems
Disabling Keyring
^^^^^^^^^^^^^^^^^
In some cases, the presence of keyring may be problemmatic. 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.
See `twine 338 <https://github.com/pypa/twine/issues/338>`_ for a
discussion on ways to do that.
Options
-------
``twine upload``
^^^^^^^^^^^^^^^^
Uploads one or more distributions to a repository.
.. code-block:: console
$ twine upload -h
......@@ -201,16 +216,29 @@ Description: twine
containing the private key and the certificate in PEM
format.
Twine also includes a ``register`` command.
``twine check``
^^^^^^^^^^^^^^^
Checks whether your distributions long description will render correctly on PyPI.
.. code-block:: console
$ twine check -h
usage: twine check [-h] dist [dist ...]
positional arguments:
dist The distribution files to check, usually dist/*
optional arguments:
-h, --help show this help message and exit
``twine register``
^^^^^^^^^^^^^^^^^^
.. WARNING::
``register`` is `no longer necessary if you are
uploading to pypi.org
<https://packaging.python.org/guides/migrating-to-pypi-org/#registering-package-names-metadata>`_. As
such, it is `no longer supported
<https://github.com/pypa/warehouse/issues/1627>`_ in `Warehouse`_
(the new PyPI software running on pypi.org). However, you may need
this if you are using a different package index.
**WARNING**: The ``register`` command is `no longer necessary if you are uploading to
pypi.org`_. As such, it is `no longer supported`_ in `Warehouse`_ (the new
PyPI software running on pypi.org). However, you may need this if you are using
a different package index.
For completeness, its usage:
......@@ -310,6 +338,8 @@ Description: twine
.. _`PyPA Code of Conduct`: https://www.pypa.io/en/latest/code-of-conduct/
.. _`Warehouse`: https://github.com/pypa/warehouse
.. _`wheels`: https://packaging.python.org/glossary/#term-wheel
.. _`no longer necessary if you are uploading to pypi.org`: https://packaging.python.org/guides/migrating-to-pypi-org/#registering-package-names-metadata
.. _`no longer supported`: https://github.com/pypa/warehouse/issues/1627
Platform: UNKNOWN
Classifier: Intended Audience :: Developers
......@@ -324,7 +354,6 @@ Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 2
Classifier: Programming Language :: Python :: 2.7
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.3
Classifier: Programming Language :: Python :: 3.4
Classifier: Programming Language :: Python :: 3.5
Classifier: Programming Language :: Python :: 3.6
......
.coveragerc
AUTHORS
LICENSE
MANIFEST.in
......@@ -10,15 +11,22 @@ docs/conf.py
docs/contributing.rst
docs/index.rst
docs/make.bat
docs/requirements.txt
docs/_static/.empty
tests/helpers.py
tests/test_check.py
tests/test_cli.py
tests/test_commands.py
tests/test_main.py
tests/test_package.py
tests/test_repository.py
tests/test_settings.py
tests/test_upload.py
tests/test_utils.py
tests/test_wheel.py
tests/alt-fixtures/twine-1.5.0-py2.py3-none-any.whl
tests/fixtures/deprecated-pypirc
tests/fixtures/twine-1.5.0-py2.py3-none-any.whl
twine/__init__.py
twine/__main__.py
twine/_installed.py
......@@ -26,6 +34,7 @@ twine/cli.py
twine/exceptions.py
twine/package.py
twine/repository.py
twine/settings.py
twine/utils.py
twine/wheel.py
twine/wininst.py
......@@ -36,5 +45,6 @@ twine.egg-info/entry_points.txt
twine.egg-info/requires.txt
twine.egg-info/top_level.txt
twine/commands/__init__.py
twine/commands/check.py
twine/commands/register.py
twine/commands/upload.py
\ No newline at end of file
......@@ -2,6 +2,7 @@
twine = twine.__main__:main
[twine.registered_commands]
check = twine.commands.check:main
register = twine.commands.register:main
upload = twine.commands.upload:main
tqdm>=4.14
pkginfo>=1.4.2
readme_renderer>=21.0
requests!=2.15,!=2.16,>=2.5.0
requests-toolbelt>=0.8.0
setuptools>=0.7.0
tqdm>=4.14
[keyring]
keyring
[with-blake2]
pyblake2
......@@ -22,7 +22,7 @@ __title__ = "twine"
__summary__ = "Collection of utilities for publishing packages on PyPI"
__uri__ = "https://twine.readthedocs.io/"
__version__ = "1.11.0"
__version__ = "1.12.1"
__author__ = "Donald Stufft and individual contributors"
__email__ = "donald@stufft.io"
......
......@@ -17,13 +17,16 @@ from __future__ import unicode_literals
import sys
import requests
from twine import exceptions
from twine.cli import dispatch
def main():
try:
return dispatch(sys.argv[1:])
except Exception as exc:
except (exceptions.TwineException, requests.exceptions.HTTPError) as exc:
return '{0}: {1}'.format(
exc.__class__.__name__,
exc.args[0],
......
......@@ -72,4 +72,4 @@ def dispatch(argv):
main = registered_commands[args.command].load()
main(args.args)
return main(args.args)
......@@ -13,3 +13,38 @@
# limitations under the License.
from __future__ import absolute_import, division, print_function
from __future__ import unicode_literals
import glob
import os.path
from twine import exceptions
__all__ = []
def _group_wheel_files_first(files):
if not any(fname for fname in files if fname.endswith(".whl")):
# Return early if there's no wheel files
return files
files.sort(key=lambda x: -1 if x.endswith(".whl") else 0)
return files
def _find_dists(dists):
uploads = []
for filename in dists:
if os.path.exists(filename):
uploads.append(filename)
continue
# The filename didn't exist so it may be a glob
files = glob.glob(filename)
# If nothing matches, files is []
if not files:
raise exceptions.InvalidDistribution(
"Cannot find file (or expand pattern): '%s'" % filename
)
# Otherwise, files will be filenames that exist
uploads.extend(files)
return _group_wheel_files_first(uploads)
# Copyright 2018 Dustin Ingram
#
# 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 __future__ import absolute_import, division, print_function
from __future__ import unicode_literals
import argparse
import cgi
import re
import sys
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/x-rst": readme_renderer.rst,
"text/markdown": readme_renderer.markdown,
}
# Regular expression used to capture and reformat doctuils warnings into
# something that a human can understand. This is loosely borrowed from
# Sphinx: https://github.com/sphinx-doc/sphinx/blob
# /c35eb6fade7a3b4a6de4183d1dd4196f04a5edaf/sphinx/util/docutils.py#L199
_REPORT_RE = re.compile(
r"^<string>:(?P<line>(?:\d+)?): "
r"\((?P<level>DEBUG|INFO|WARNING|ERROR|SEVERE)/(\d+)?\) "
r"(?P<message>.*)",
re.DOTALL | re.MULTILINE,
)
class _WarningStream(object):
def __init__(self):
self.output = StringIO()
def write(self, text):
matched = _REPORT_RE.search(text)
if not matched:
self.output.write(text)
return
self.output.write(
"line {line}: {level_text}: {message}\n".format(
level_text=matched.group("level").capitalize(),
line=matched.group("line"),
message=matched.group("message").rstrip("\r\n"),
)
)
def __str__(self):
return self.output.getvalue()
def check(dists, output_stream=sys.stdout):
uploads = [i for i in _find_dists(dists) if not i.endswith(".asc")]
stream = _WarningStream()
failure = False
for filename in uploads:
output_stream.write("Checking distribution %s: " % filename)
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
)
if rendered 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
def main(args):
parser = argparse.ArgumentParser(prog="twine check")
parser.add_argument(
"dists",
nargs="+",
metavar="dist",
help="The distribution files to check, usually dist/*",
)
args = parser.parse_args(args)
# Call the check function with the arguments from the command line
return check(args.dists)
......@@ -16,48 +16,31 @@ from __future__ import absolute_import, unicode_literals, print_function
import argparse
import os.path
from twine import exceptions as exc
from twine.package import PackageFile
from twine.repository import Repository
from twine import utils
from twine import exceptions
from twine import settings
def register(package, repository, username, password, comment, config_file,
cert, client_cert, repository_url):
config = utils.get_repository_from_config(
config_file,
repository,
repository_url,
)
config["repository"] = utils.normalize_repository_url(
config["repository"]