Repository: pypa/twine

Issue:
Twine should have a built-in alias for testpypi
Instead of needing to specify the full upload URL for Test PyPI we should always have an alias ready, for example:

```
twine upload --repository=testpypi dist/*
```

Should work even without a `-/.pypirc`. If `testpypi` is defined in `~/.pypic`, it should take precedence.

Discussion:
codecov[bot]: # [Codecov](https://codecov.io/gh/pypa/twine/pull/369?src=pr&el=h1) Report
> Merging [#369](https://codecov.io/gh/pypa/twine/pull/369?src=pr&el=desc) into [master](https://codecov.io/gh/pypa/twine/commit/34c08ef97d05d219ae018f041cd37e1d409b7a4d?src=pr&el=desc) will **decrease** coverage by `0.45%`.
> The diff coverage is `100%`.

[![Impacted file tree graph](https://codecov.io/gh/pypa/twine/pull/369/graphs/tree.svg?width=650&token=NrvN4iFbj6&height=150&src=pr)](https://codecov.io/gh/pypa/twine/pull/369?src=pr&el=tree)

```diff
@@            Coverage Diff            @@
##           master    #369      +/-   ##
=========================================
- Coverage   74.55%   74.1%   -0.46%     
=========================================
  Files          13      13              
  Lines         672     668       -4     
  Branches      101     100       -1     
=========================================
- Hits          501     495       -6     
- Misses        143     145       +2     
  Partials       28      28
```


| [Impacted Files](https://codecov.io/gh/pypa/twine/pull/369?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [twine/utils.py](https://codecov.io/gh/pypa/twine/pull/369/diff?src=pr&el=tree#diff-dHdpbmUvdXRpbHMucHk=) | `82.25% <100%> (-2.12%)` | :arrow_down: |
| [twine/wininst.py](https://codecov.io/gh/pypa/twine/pull/369/diff?src=pr&el=tree#diff-dHdpbmUvd2luaW5zdC5weQ==) | `29.72% <0%> (ø)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/pypa/twine/pull/369?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/pypa/twine/pull/369?src=pr&el=footer). Last update [34c08ef...6e1a1ea](https://codecov.io/gh/pypa/twine/pull/369?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

anlutro: I wonder if I could remove this line and just always configure `testpypi`, regardless of `index-servers`? I left it in to stay consistent with old behaviour. https://github.com/pypa/twine/pull/369/files#diff-547bab308763f89cacec226151fcbb80R83
anlutro: docs failed in Travis - unrelated, I guess?
theacodes: Yes, unrelated and up to me to fix. Can you get lint passing?
anlutro: Amended, lint should pass now.

Patch:
diff --git a/twine/utils.py b/twine/utils.py
index d83e080..4feca1b 100644
--- a/twine/utils.py
+++ b/twine/utils.py
@@ -21,6 +21,7 @@ import getpass
 import sys
 import argparse
 import warnings
+import collections
 
 from requests.exceptions import HTTPError
 
@@ -48,68 +49,52 @@ TEST_REPOSITORY = "https://test.pypi.org/legacy/"
 
 
 def get_config(path="~/.pypirc"):
+    # even if the config file does not exist, set up the parser
+    # variable to reduce the number of if/else statements
+    parser = configparser.RawConfigParser()
+
+    # this list will only be used if index-servers
+    # is not defined in the config file
+    index_servers = ["pypi", "testpypi"]
+
+    # default configuration for each repository
+    defaults = {"username": None, "password": None}
+
     # Expand user strings in the path
     path = os.path.expanduser(path)
 
-    if not os.path.isfile(path):
-        return {"pypi": {"repository": DEFAULT_REPOSITORY,
-                         "username": None,
-                         "password": None
-                         },
-                "pypitest": {"repository": TEST_REPOSITORY,
-                             "username": None,
-                             "password": None
-                             },
-                }
-
     # Parse the rc file
-    parser = configparser.RawConfigParser()
-    parser.read(path)
-
-    # Get a list of repositories from the config file
-    # format: https://docs.python.org/3/distutils/packageindex.html#pypirc
-    if (parser.has_section("distutils") and
-            parser.has_option("distutils", "index-servers")):
-        repositories = parser.get("distutils", "index-servers").split()
-    elif parser.has_section("pypi"):
-        # Special case: if the .pypirc file has a 'pypi' section,
-        # even if there's no list of index servers,
-        # be lenient and include that in our list of repositories.
-        repositories = ['pypi']
-    else:
-        repositories = []
+    if os.path.isfile(path):
+        parser.read(path)
 
-    config = {}
+        # Get a list of index_servers from the config file
+        # format: https://docs.python.org/3/distutils/packageindex.html#pypirc
+        if parser.has_option("distutils", "index-servers"):
+            index_servers = parser.get("distutils", "index-servers").split()
 
-    defaults = {"username": None, "password": None}
-    if parser.has_section("server-login"):
         for key in ["username", "password"]:
             if parser.has_option("server-login", key):
                 defaults[key] = parser.get("server-login", key)
 
-    for repository in repositories:
-        # Skip this repository if it doesn't exist in the config file
-        if not parser.has_section(repository):
-            continue
+    config = collections.defaultdict(lambda: defaults.copy())
 
-        # Mandatory configuration and defaults
-        config[repository] = {
-            "repository": DEFAULT_REPOSITORY,
-            "username": None,
-            "password": None,
-        }
+    # don't require users to manually configure URLs for these repositories
+    config["pypi"]["repository"] = DEFAULT_REPOSITORY
+    if "testpypi" in index_servers:
+        config["testpypi"]["repository"] = TEST_REPOSITORY
 
-        # Optional configuration values
+    # optional configuration values for individual repositories
+    for repository in index_servers:
         for key in [
             "username", "repository", "password",
             "ca_cert", "client_cert",
         ]:
             if parser.has_option(repository, key):
                 config[repository][key] = parser.get(repository, key)
-            elif defaults.get(key):
-                config[repository][key] = defaults[key]
 
-    return config
+    # convert the defaultdict to a regular dict at this point
+    # to prevent surprising behavior later on
+    return dict(config)
 
 
 def get_repository_from_config(config_file, repository, repository_url=None):


Test patch:
diff --git a/tests/test_utils.py b/tests/test_utils.py
index 4cd45a6..4d60e04 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -72,6 +72,11 @@ def test_get_config_no_distutils(tmpdir):
             "username": "testuser",
             "password": "testpassword",
         },
+        "testpypi": {
+            "repository": utils.TEST_REPOSITORY,
+            "username": None,
+            "password": None,
+        },
     }
 
 
@@ -97,6 +102,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 +123,7 @@ def test_get_config_missing(tmpdir):
             "username": None,
             "password": None,
         },
-        "pypitest": {
+        "testpypi": {
             "repository": utils.TEST_REPOSITORY,
             "username": None,
             "password": None
@@ -143,8 +160,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",
         },
     }