From fd51ed19191f02de6ab17149926cb5f387daf2ca Mon Sep 17 00:00:00 2001 From: Brian Rutledge Date: Sun, 20 Jun 2021 16:00:59 -0400 Subject: [PATCH] Raise exception for missing config file --- tests/conftest.py | 8 ++++++-- tests/test_utils.py | 22 ++++++++++++++++++---- twine/settings.py | 4 ++-- twine/utils.py | 20 ++++++++++++++++---- 4 files changed, 42 insertions(+), 12 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 181d5fe3..479b241a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -4,11 +4,15 @@ import pytest from twine import settings +from twine import utils @pytest.fixture() -def config_file(tmpdir): - return tmpdir / ".pypirc" +def config_file(tmpdir, monkeypatch): + path = tmpdir / ".pypirc" + # Mimic common case of .pypirc in home directory + monkeypatch.setattr(utils, "DEFAULT_CONFIG_FILE", path) + return path @pytest.fixture diff --git a/tests/test_utils.py b/tests/test_utils.py index d14c49aa..903155f9 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -171,10 +171,24 @@ def test_get_repository_config_with_invalid_url(config_file, repo_url, message): utils.get_repository_from_config(config_file, "pypi", repo_url) -def test_get_repository_config_missing_config(config_file): - """Raise an exception when a repository isn't defined in .pypirc.""" - with pytest.raises(exceptions.InvalidConfiguration): - utils.get_repository_from_config(config_file, "foobar") +def test_get_repository_config_missing_repository(write_config_file): + """Raise an exception when a custom repository isn't defined in .pypirc.""" + config_file = write_config_file("") + with pytest.raises( + exceptions.InvalidConfiguration, + match="Missing 'missing-repository'", + ): + utils.get_repository_from_config(config_file, "missing-repository") + + +@pytest.mark.parametrize("repository", ["pypi", "missing-repository"]) +def test_get_repository_config_missing_file(repository): + """Raise an exception when a custom config file doesn't exist.""" + with pytest.raises( + exceptions.InvalidConfiguration, + match=r"No such file.*missing-file", + ): + utils.get_repository_from_config("missing-file", repository) def test_get_config_deprecated_pypirc(): diff --git a/twine/settings.py b/twine/settings.py index 2fe4f897..fe2f26ce 100644 --- a/twine/settings.py +++ b/twine/settings.py @@ -52,7 +52,7 @@ def __init__( password: Optional[str] = None, non_interactive: bool = False, comment: Optional[str] = None, - config_file: str = "~/.pypirc", + config_file: str = utils.DEFAULT_CONFIG_FILE, skip_existing: bool = False, cacert: Optional[str] = None, client_cert: Optional[str] = None, @@ -244,7 +244,7 @@ def register_argparse_arguments(parser: argparse.ArgumentParser) -> None: ) parser.add_argument( "--config-file", - default="~/.pypirc", + default=utils.DEFAULT_CONFIG_FILE, help="The .pypirc config file to use.", ) parser.add_argument( diff --git a/twine/utils.py b/twine/utils.py index c98cebf1..e5bdb4b5 100644 --- a/twine/utils.py +++ b/twine/utils.py @@ -33,6 +33,8 @@ DEFAULT_REPOSITORY = "https://upload.pypi.org/legacy/" TEST_REPOSITORY = "https://test.pypi.org/legacy/" +DEFAULT_CONFIG_FILE = "~/.pypirc" + # TODO: In general, it seems to be assumed that the values retrieved from # instances of this type aren't None, except for username and password. # Type annotations would be cleaner if this were Dict[str, str], but that @@ -48,13 +50,20 @@ def get_config(path: str) -> Dict[str, RepositoryConfig]: Format: https://packaging.python.org/specifications/pypirc/ - If the file doesn't exist, return a default configuration for pypyi and testpypi. + If the default config file doesn't exist, return a default configuration for + pypyi and testpypi. """ + realpath = os.path.realpath(os.path.expanduser(path)) parser = configparser.RawConfigParser() - path = os.path.expanduser(path) - if parser.read(path): - logger.info(f"Using configuration from {path}") + try: + with open(realpath) as f: + parser.read_file(f) + logger.info(f"Using configuration from {realpath}") + except FileNotFoundError: + # User probably set --config-file, but the file can't be read + if path != DEFAULT_CONFIG_FILE: + raise # server-login is obsolete, but retained for backwards compatibility defaults: RepositoryConfig = { @@ -121,8 +130,11 @@ def get_repository_from_config( "username": None, "password": None, } + try: return get_config(config_file)[repository] + except OSError as exc: + raise exceptions.InvalidConfiguration(str(exc)) except KeyError: raise exceptions.InvalidConfiguration( f"Missing '{repository}' section from {config_file}.\n"