diff --git a/doc/cmdline.rst b/doc/cmdline.rst index a6447cef..22e6ac68 100644 --- a/doc/cmdline.rst +++ b/doc/cmdline.rst @@ -71,6 +71,10 @@ or another repository. Name of a repository to upload packages to. Should match a section in ``~/.pypirc``. The default is ``pypi``. +.. option:: --pypirc + + The .pypirc config file to be used. The default is ``~/.pypirc``. + .. seealso:: :doc:`upload` .. _install_cmd: diff --git a/doc/upload.rst b/doc/upload.rst index 73712c0b..df5af5ef 100644 --- a/doc/upload.rst +++ b/doc/upload.rst @@ -14,7 +14,8 @@ you can configure Flit in two main ways: Using .pypirc ------------- -You can create or edit a config file in your home directory, ``~/.pypirc``. +You can create or edit a config file in your home directory, ``~/.pypirc`` that +will be used by default or you can specify a custom location. This is also used by other Python tools such as `twine `_. diff --git a/flit/__init__.py b/flit/__init__.py index bd6aa831..5a79b1f5 100644 --- a/flit/__init__.py +++ b/flit/__init__.py @@ -116,8 +116,12 @@ def main(argv=None): ) ) + parser_publish.add_argument('--pypirc', + help="The .pypirc config file to be used. DEFAULT = \"~/.pypirc\"" + ) + parser_publish.add_argument('--repository', - help="Name of the repository to upload to (must be in ~/.pypirc)" + help="Name of the repository to upload to (must be in the specified .pypirc file)" ) # flit install -------------------------------------------- @@ -184,7 +188,7 @@ def gen_setup_py(): log.warning("Passing --repository before the 'upload' subcommand is deprecated: pass it after") repository = args.repository or args.deprecated_repository from .upload import main - main(args.ini_file, repository, formats=set(args.format or []), + main(args.ini_file, repository, args.pypirc, formats=set(args.format or []), gen_setup_py=gen_setup_py()) elif args.subcmd == 'install': diff --git a/flit/upload.py b/flit/upload.py index a99180f6..162a18c4 100644 --- a/flit/upload.py +++ b/flit/upload.py @@ -18,6 +18,7 @@ log = logging.getLogger(__name__) PYPI = "https://upload.pypi.org/legacy/" +PYPIRC_DEFAULT = "~/.pypirc" SWITCH_TO_HTTPS = ( "http://pypi.python.org/", @@ -59,13 +60,13 @@ def get_repositories(file="~/.pypirc"): return repos -def get_repository(name=None, cfg_file="~/.pypirc"): +def get_repository(pypirc_path="~/.pypirc", name=None): """Get the url, username and password for one repository. - + Returns a dict with keys 'url', 'username', 'password'. There is a hierarchy of possible sources of information: - + Index URL: 1. Command line arg --repository (looked up in .pypirc) 2. $FLIT_INDEX_URL @@ -85,7 +86,8 @@ def get_repository(name=None, cfg_file="~/.pypirc"): 3. keyring 4. Terminal prompt (store to keyring if available) """ - repos_cfg = get_repositories(cfg_file) + log.debug("Loading repositories config from %r", pypirc_path) + repos_cfg = get_repositories(pypirc_path) if name is not None: repo = repos_cfg[name] @@ -114,7 +116,7 @@ def get_repository(name=None, cfg_file="~/.pypirc"): while not repo['username']: repo['username'] = input("Username: ") if repo['url'] == PYPI: - write_pypirc(repo) + write_pypirc(repo, pypirc_path) elif not repo['username']: raise Exception("Could not find username for upload.") @@ -237,10 +239,10 @@ def upload_file(file:Path, metadata:Metadata, repo): resp.raise_for_status() -def do_upload(file:Path, metadata:Metadata, repo_name=None): +def do_upload(file:Path, metadata:Metadata, pypirc_path="~/.pypirc", repo_name=None): """Upload a file to an index server. """ - repo = get_repository(repo_name) + repo = get_repository(pypirc_path, repo_name) upload_file(file, metadata, repo) if repo['is_warehouse']: @@ -252,12 +254,17 @@ def do_upload(file:Path, metadata:Metadata, repo_name=None): log.info("Package is at %s/%s", repo['url'], metadata.name) -def main(ini_path, repo_name, formats=None, gen_setup_py=True): +def main(ini_path, repo_name, pypirc_path=None, formats=None, gen_setup_py=True): """Build and upload wheel and sdist.""" + if pypirc_path is None: + pypirc_path = PYPIRC_DEFAULT + elif not os.path.isfile(pypirc_path): + raise FileNotFoundError("The specified pypirc config file does not exist.") + from . import build built = build.main(ini_path, formats=formats, gen_setup_py=gen_setup_py) if built.wheel is not None: - do_upload(built.wheel.file, built.wheel.builder.metadata, repo_name) + do_upload(built.wheel.file, built.wheel.builder.metadata, pypirc_path, repo_name) if built.sdist is not None: - do_upload(built.sdist.file, built.sdist.builder.metadata, repo_name) + do_upload(built.sdist.file, built.sdist.builder.metadata, pypirc_path, repo_name) diff --git a/tests/test_upload.py b/tests/test_upload.py index 819a0023..cef98795 100644 --- a/tests/test_upload.py +++ b/tests/test_upload.py @@ -1,13 +1,17 @@ from contextlib import contextmanager +from tempfile import NamedTemporaryFile +import os import io import pathlib import sys +import pytest import responses from testpath import modified_env from unittest.mock import patch from flit import upload +from flit.build import ALL_FORMATS samples_dir = pathlib.Path(__file__).parent / 'samples' @@ -17,16 +21,6 @@ 'is_warehouse': True, } -@responses.activate -def test_upload(copy_sample): - responses.add(responses.POST, upload.PYPI, status=200) - td = copy_sample('module1_toml') - - with patch('flit.upload.get_repository', return_value=repo_settings): - upload.main(td / 'pyproject.toml', repo_name='pypi') - - assert len(responses.calls) == 2 - pypirc1 = """ [distutils] index-servers = @@ -38,19 +32,43 @@ def test_upload(copy_sample): """ # That's not a real password. Well, hopefully not. +@contextmanager +def temp_pypirc(content): + try: + temp_file = NamedTemporaryFile("w+", delete=False) + temp_file.write(content) + temp_file.close() + yield temp_file.name + finally: + os.unlink(temp_file.name) + + +@responses.activate +def test_upload(copy_sample): + responses.add(responses.POST, upload.PYPI, status=200) + td = copy_sample('module1_toml') + + with temp_pypirc(pypirc1) as pypirc, \ + patch('flit.upload.get_repository', return_value=repo_settings): + upload.main(td / 'pyproject.toml', repo_name='pypi', pypirc_path=pypirc) + + assert len(responses.calls) == 2 + def test_get_repository(): - repo = upload.get_repository(cfg_file=io.StringIO(pypirc1)) - assert repo['url'] == upload.PYPI - assert repo['username'] == 'fred' - assert repo['password'] == 's3cret' + with temp_pypirc(pypirc1) as pypirc: + repo = upload.get_repository(pypirc_path=pypirc) + assert repo['url'] == upload.PYPI + assert repo['username'] == 'fred' + assert repo['password'] == 's3cret' def test_get_repository_env(): - with modified_env({ + with temp_pypirc(pypirc1) as pypirc, \ + modified_env({ 'FLIT_INDEX_URL': 'https://pypi.example.com', 'FLIT_USERNAME': 'alice', 'FLIT_PASSWORD': 'p4ssword', # Also not a real password }): - repo = upload.get_repository(cfg_file=io.StringIO(pypirc1)) + repo = upload.get_repository(pypirc_path=pypirc) # Because we haven't specified a repo name, environment variables should # have higher priority than the config file. assert repo['url'] == 'https://pypi.example.com' @@ -87,7 +105,66 @@ def get_password(service_name, username): def test_get_repository_keyring(): with modified_env({'FLIT_PASSWORD': None}), \ _fake_keyring('tops3cret'): - repo = upload.get_repository(cfg_file=io.StringIO(pypirc2)) + repo = upload.get_repository(pypirc_path=io.StringIO(pypirc2)) assert repo['username'] == 'fred' assert repo['password'] == 'tops3cret' + + +pypirc3_repo = "https://invalid-repo.inv" +pypirc3_user = "test" +pypirc3_pass = "not_a_real_password" +pypirc3 = f""" +[distutils] = +index-servers = + test123 + +[test123] +repository: {pypirc3_repo} +username: {pypirc3_user} +password: {pypirc3_pass} +""" + + +def test_upload_pypirc_file(copy_sample): + with temp_pypirc(pypirc3) as pypirc, \ + patch("flit.upload.upload_file") as upload_file: + td = copy_sample("module1_toml") + formats = list(ALL_FORMATS)[:1] + upload.main( + td / "pyproject.toml", + formats=set(formats), + repo_name="test123", + pypirc_path=pypirc, + ) + _, _, repo = upload_file.call_args[0] + + assert repo["url"] == pypirc3_repo + assert repo["username"] == pypirc3_user + assert repo["password"] == pypirc3_pass + + +def test_upload_invalid_pypirc_file(copy_sample): + with patch("flit.upload.upload_file"): + td = copy_sample("module1_toml") + formats = list(ALL_FORMATS)[:1] + with pytest.raises(FileNotFoundError): + upload.main( + td / "pyproject.toml", + formats=set(formats), + repo_name="test123", + pypirc_path="./file.invalid", + ) + +def test_upload_default_pypirc_file(copy_sample): + with patch("flit.upload.do_upload") as do_upload: + td = copy_sample("module1_toml") + formats = list(ALL_FORMATS)[:1] + upload.main( + td / "pyproject.toml", + formats=set(formats), + repo_name="test123", + ) + + file = do_upload.call_args[0][2] + assert file == "~/.pypirc"