Commit e5e706a4 by Sébastien Eustace Committed by GitHub

Add support for PyPI API tokens (#1275)

parent 11c2b9a5
......@@ -39,8 +39,18 @@ If you do not specify the password you will be prompted to write it.
!!!note
To publish to PyPI, you can set your credentials for the repository
named `pypi`:
To publish to PyPI, you can set your credentials for the repository named `pypi`.
Note that it is recommended to use [API tokens](https://pypi.org/help/#apitoken)
when uploading packages to PyPI.
Once you have created a new token, you can tell Poetry to use it:
```bash
poetry config pypi-token.pypi my-token
```
If you still want to use you username and password, you can do so with the following
call to `config`.
```bash
poetry config http-basic.pypi username password
......@@ -56,6 +66,7 @@ Keyring support is enabled using the [keyring library](https://pypi.org/project/
Alternatively, you can use environment variables to provide the credentials:
```bash
export POETRY_PYPI_TOKEN_PYPI=my-token
export POETRY_HTTP_BASIC_PYPI_USERNAME=username
export POETRY_HTTP_BASIC_PYPI_PASSWORD=password
```
......
......@@ -176,7 +176,7 @@ To remove a repository (repo is a short alias for repositories):
)
# handle auth
m = re.match(r"^(http-basic)\.(.+)", self.argument("key"))
m = re.match(r"^(http-basic|pypi-token)\.(.+)", self.argument("key"))
if m:
if self.option("unset"):
keyring_repository_password_del(config, m.group(2))
......@@ -209,6 +209,17 @@ To remove a repository (repo is a short alias for repositories):
auth_config_source.add_property(
"{}.{}".format(m.group(1), m.group(2)), property_value
)
elif m.group(1) == "pypi-token":
if len(values) != 1:
raise ValueError(
"Expected only one argument (token), got {}".format(len(values))
)
token = values[0]
auth_config_source.add_property(
"{}.{}".format(m.group(1), m.group(2)), token
)
return 0
......
......@@ -26,6 +26,8 @@ The --repository option should match the name of a configured repository using
the config command.
"""
loggers = ["poetry.masonry.publishing.publisher"]
def handle(self):
from poetry.masonry.publishing.publisher import Publisher
......
from poetry.locations import CONFIG_DIR
from poetry.utils._compat import Path
import logging
from poetry.utils.helpers import get_http_basic_auth
from poetry.utils.toml_file import TomlFile
from .uploader import Uploader
logger = logging.getLogger(__name__)
class Publisher:
"""
Registers and publishes packages to remote repositories.
......@@ -55,10 +57,22 @@ class Publisher:
url = repository["url"]
if not (username and password):
auth = get_http_basic_auth(self._poetry.config, repository_name)
if auth:
username = auth[0]
password = auth[1]
# Check if we have a token first
token = self._poetry.config.get("pypi-token.{}".format(repository_name))
if token:
logger.debug("Found an API token for {}.".format(repository_name))
username = "@token"
password = token
else:
auth = get_http_basic_auth(self._poetry.config, repository_name)
if auth:
logger.debug(
"Found authentication information for {}.".format(
repository_name
)
)
username = auth[0]
password = auth[1]
# Requesting missing credentials
if not username:
......
......@@ -92,3 +92,16 @@ virtualenvs.path = {path} # /foo{sep}virtualenvs
assert expected == tester.io.fetch_output()
assert "poetry.toml" == init.call_args_list[2][0][1].path.name
assert expected == tester.io.fetch_output()
def test_set_pypi_token(app, config_source, config_document, mocker):
init = mocker.spy(ConfigSource, "__init__")
command = app.find("config")
tester = CommandTester(command)
tester.execute("pypi-token.pypi mytoken")
tester.execute("--list")
assert "mytoken" == config_document["pypi-token"]["pypi"]
......@@ -5,10 +5,11 @@ from poetry.masonry.publishing.publisher import Publisher
from poetry.poetry import Poetry
def test_publish_publishes_to_pypi_by_default(fixture_dir, mocker):
def test_publish_publishes_to_pypi_by_default(fixture_dir, mocker, config):
uploader_auth = mocker.patch("poetry.masonry.publishing.uploader.Uploader.auth")
uploader_upload = mocker.patch("poetry.masonry.publishing.uploader.Uploader.upload")
poetry = Poetry.create(fixture_dir("sample_project"))
poetry._config = config
poetry.config.merge(
{"http-basic": {"pypi": {"username": "foo", "password": "bar"}}}
)
......@@ -20,10 +21,11 @@ def test_publish_publishes_to_pypi_by_default(fixture_dir, mocker):
assert [("https://upload.pypi.org/legacy/",)] == uploader_upload.call_args
def test_publish_can_publish_to_given_repository(fixture_dir, mocker):
def test_publish_can_publish_to_given_repository(fixture_dir, mocker, config):
uploader_auth = mocker.patch("poetry.masonry.publishing.uploader.Uploader.auth")
uploader_upload = mocker.patch("poetry.masonry.publishing.uploader.Uploader.upload")
poetry = Poetry.create(fixture_dir("sample_project"))
poetry._config = config
poetry.config.merge(
{
"repositories": {"my-repo": {"url": "http://foo.bar"}},
......@@ -38,8 +40,9 @@ def test_publish_can_publish_to_given_repository(fixture_dir, mocker):
assert [("http://foo.bar",)] == uploader_upload.call_args
def test_publish_raises_error_for_undefined_repository(fixture_dir, mocker):
def test_publish_raises_error_for_undefined_repository(fixture_dir, mocker, config):
poetry = Poetry.create(fixture_dir("sample_project"))
poetry._config = config
poetry.config.merge(
{"http-basic": {"my-repo": {"username": "foo", "password": "bar"}}}
)
......@@ -47,3 +50,17 @@ def test_publish_raises_error_for_undefined_repository(fixture_dir, mocker):
with pytest.raises(RuntimeError):
publisher.publish("my-repo", None, None)
def test_publish_uses_token_if_it_exists(fixture_dir, mocker, config):
uploader_auth = mocker.patch("poetry.masonry.publishing.uploader.Uploader.auth")
uploader_upload = mocker.patch("poetry.masonry.publishing.uploader.Uploader.upload")
poetry = Poetry.create(fixture_dir("sample_project"))
poetry._config = config
poetry.config.merge({"pypi-token": {"pypi": "my-token"}})
publisher = Publisher(poetry, NullIO())
publisher.publish(None, None, None)
assert [("@token", "my-token")] == uploader_auth.call_args
assert [("https://upload.pypi.org/legacy/",)] == uploader_upload.call_args
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment