Repository: pypa/twine
Pull request #202 (2016-08-03 00:29:50)

Read environment vars
Supersedes #165 


Patch:
diff --git a/docs/changelog.rst b/docs/changelog.rst
index bf0c81f..fde2ef9 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/twine/commands/register.py b/twine/commands/register.py
index 16eece0..ea6732e 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 ea2a30b..7f7335e 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 a3bfcab..3e21fde 100644
--- a/twine/utils.py
+++ b/twine/utils.py
@@ -19,6 +19,8 @@ import os.path
 import functools
 import getpass
 import sys
+import argparse
+
 
 try:
     import configparser
@@ -172,3 +174,21 @@ get_clientcert = functools.partial(
     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)


Test patch:
diff --git a/tests/helpers.py b/tests/helpers.py
new file mode 100644
index 0000000..c4847ab
--- /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 da723d5..2649fe4 100644
--- a/tests/test_upload.py
+++ b/tests/test_upload.py
@@ -20,8 +20,10 @@ import pretend
 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 5557e40..cb763b7 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -16,9 +16,13 @@ from __future__ import unicode_literals
 
 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