diff --git a/docs/changelog.rst b/docs/changelog.rst index bf0c81fe..fde2ef9e 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,6 +4,18 @@ Changelog ========= +* :release:`1.8.0 <2016-xx-yy>` + + * :feature:`201` Switch from upload.pypi.io to upload.pypi.org. + + * :feature:`144` Retrieve configuration from the environment as a default. + + - Repository URL will default to ``TWINE_REPOSITORY`` + + - Username will default to ``TWINE_USERNAME`` + + - Password will default to ``TWINE_PASSWORD`` + * :release:`1.7.4 <2016-07-09>` * Correct a packaging error. diff --git a/tests/helpers.py b/tests/helpers.py new file mode 100644 index 00000000..c4847ab6 --- /dev/null +++ b/tests/helpers.py @@ -0,0 +1,40 @@ +# Copyright 2016 Ian 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. +"""Test functions useful across twine's tests.""" + +import contextlib +import os + + +@contextlib.contextmanager +def set_env(**environ): + """Set the process environment variables temporarily. + + >>> with set_env(PLUGINS_DIR=u'test/plugins'): + ... "PLUGINS_DIR" in os.environ + True + + >>> "PLUGINS_DIR" in os.environ + False + + :param environ: Environment variables to set + :type environ: dict[str, unicode] + """ + old_environ = dict(os.environ) + os.environ.update(environ) + try: + yield + finally: + os.environ.clear() + os.environ.update(old_environ) diff --git a/tests/test_upload.py b/tests/test_upload.py index da723d5c..2649fe46 100644 --- a/tests/test_upload.py +++ b/tests/test_upload.py @@ -20,8 +20,10 @@ import pytest from twine.commands import upload -from twine import package +from twine import package, cli +import twine +import helpers WHEEL_FIXTURE = 'tests/fixtures/twine-1.5.0-py2.py3-none-any.whl' @@ -48,7 +50,8 @@ def test_ensure_if_no_wheel_files(): def test_find_dists_expands_globs(): files = sorted(upload.find_dists(['twine/__*.py'])) - expected = ['twine/__init__.py', 'twine/__main__.py'] + expected = [os.path.join('twine', '__init__.py'), + os.path.join('twine', '__main__.py')] assert expected == files @@ -124,3 +127,20 @@ def test_skip_upload_respects_skip_existing(monkeypatch): assert upload.skip_upload(response=response, skip_existing=False, package=pkg) is False + + +def test_password_and_username_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"} + 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"] + result_kwargs = replaced_upload.calls[1].kwargs + assert None is result_kwargs["password"] + assert None is result_kwargs["username"] diff --git a/tests/test_utils.py b/tests/test_utils.py index 5557e403..cb763b7f 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -16,9 +16,13 @@ import os.path import textwrap + import pytest from twine.utils import DEFAULT_REPOSITORY, get_config, get_userpass_value +from twine import utils + +import helpers def test_get_config(tmpdir): @@ -121,3 +125,27 @@ def test_get_config_deprecated_pypirc(): def test_get_userpass_value(cli_value, config, key, strategy, expected): ret = get_userpass_value(cli_value, config, key, strategy) assert ret == expected + + +@pytest.mark.parametrize( + ('env_name', 'default', 'environ', 'expected'), + [ + ('MY_PASSWORD', None, {}, None), + ('MY_PASSWORD', None, {'MY_PASSWORD': 'foo'}, 'foo'), + ('URL', 'https://example.org', {}, 'https://example.org'), + ('URL', 'https://example.org', {'URL': 'https://pypi.org'}, + 'https://pypi.org'), + ], +) +def test_default_to_environment_action(env_name, default, environ, expected): + option_strings = ('-x', '--example') + dest = 'example' + with helpers.set_env(**environ): + action = utils.EnvironmentDefault( + env=env_name, + default=default, + option_strings=option_strings, + dest=dest, + ) + assert action.env == env_name + assert action.default == expected diff --git a/twine/commands/register.py b/twine/commands/register.py index 16eece0b..ea6732e3 100644 --- a/twine/commands/register.py +++ b/twine/commands/register.py @@ -62,17 +62,27 @@ def main(args): parser = argparse.ArgumentParser(prog="twine register") parser.add_argument( "-r", "--repository", + action=utils.EnvironmentDefault, + env='TWINE_REPOSITORY', default="pypi", help="The repository to register the package to (default: " "%(default)s)", ) parser.add_argument( "-u", "--username", - help="The username to authenticate to the repository as", + action=utils.EnvironmentDefault, + env='TWINE_USERNAME', + required=False, help="The username to authenticate to the repository " + "as (can also be set via %(env)s environment " + "variable)", ) parser.add_argument( "-p", "--password", - help="The password to authenticate to the repository with", + action=utils.EnvironmentDefault, + env='TWINE_PASSWORD', + required=False, help="The password to authenticate to the repository " + "with (can also be set via %(env)s environment " + "variable)", ) parser.add_argument( "-c", "--comment", diff --git a/twine/commands/upload.py b/twine/commands/upload.py index ea2a30bd..7f7335eb 100644 --- a/twine/commands/upload.py +++ b/twine/commands/upload.py @@ -149,6 +149,8 @@ def main(args): parser = argparse.ArgumentParser(prog="twine upload") parser.add_argument( "-r", "--repository", + action=utils.EnvironmentDefault, + env="TWINE_REPOSITORY", default="pypi", help="The repository to upload the files to (default: %(default)s)", ) @@ -169,11 +171,19 @@ def main(args): ) parser.add_argument( "-u", "--username", - help="The username to authenticate to the repository as", + action=utils.EnvironmentDefault, + env="TWINE_USERNAME", + required=False, help="The username to authenticate to the repository " + "as (can also be set via %(env)s environment " + "variable)", ) parser.add_argument( "-p", "--password", - help="The password to authenticate to the repository with", + action=utils.EnvironmentDefault, + env="TWINE_PASSWORD", + required=False, help="The password to authenticate to the repository " + "with (can also be set via %(env)s environment " + "variable)", ) parser.add_argument( "-c", "--comment", diff --git a/twine/utils.py b/twine/utils.py index a3bfcab8..3e21fdeb 100644 --- a/twine/utils.py +++ b/twine/utils.py @@ -19,6 +19,8 @@ import functools import getpass import sys +import argparse + try: import configparser @@ -172,3 +174,21 @@ def password_prompt(prompt_text): # Always expects unicode for our own sanity get_userpass_value, key='client_cert', ) + + +class EnvironmentDefault(argparse.Action): + """Get values from environment variable.""" + + def __init__(self, env, required=True, default=None, **kwargs): + default = os.environ.get(env, default) + self.env = env + if default: + required = False + super(EnvironmentDefault, self).__init__( + default=default, + required=required, + **kwargs + ) + + def __call__(self, parser, namespace, values, option_string=None): + setattr(namespace, self.dest, values)