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. ...@@ -39,8 +39,18 @@ If you do not specify the password you will be prompted to write it.
!!!note !!!note
To publish to PyPI, you can set your credentials for the repository To publish to PyPI, you can set your credentials for the repository named `pypi`.
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 ```bash
poetry config http-basic.pypi username password poetry config http-basic.pypi username password
...@@ -56,6 +66,7 @@ Keyring support is enabled using the [keyring library](https://pypi.org/project/ ...@@ -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: Alternatively, you can use environment variables to provide the credentials:
```bash ```bash
export POETRY_PYPI_TOKEN_PYPI=my-token
export POETRY_HTTP_BASIC_PYPI_USERNAME=username export POETRY_HTTP_BASIC_PYPI_USERNAME=username
export POETRY_HTTP_BASIC_PYPI_PASSWORD=password export POETRY_HTTP_BASIC_PYPI_PASSWORD=password
``` ```
......
...@@ -176,7 +176,7 @@ To remove a repository (repo is a short alias for repositories): ...@@ -176,7 +176,7 @@ To remove a repository (repo is a short alias for repositories):
) )
# handle auth # 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 m:
if self.option("unset"): if self.option("unset"):
keyring_repository_password_del(config, m.group(2)) keyring_repository_password_del(config, m.group(2))
...@@ -209,6 +209,17 @@ To remove a repository (repo is a short alias for repositories): ...@@ -209,6 +209,17 @@ To remove a repository (repo is a short alias for repositories):
auth_config_source.add_property( auth_config_source.add_property(
"{}.{}".format(m.group(1), m.group(2)), property_value "{}.{}".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 return 0
......
...@@ -26,6 +26,8 @@ The --repository option should match the name of a configured repository using ...@@ -26,6 +26,8 @@ The --repository option should match the name of a configured repository using
the config command. the config command.
""" """
loggers = ["poetry.masonry.publishing.publisher"]
def handle(self): def handle(self):
from poetry.masonry.publishing.publisher import Publisher from poetry.masonry.publishing.publisher import Publisher
......
from poetry.locations import CONFIG_DIR import logging
from poetry.utils._compat import Path
from poetry.utils.helpers import get_http_basic_auth from poetry.utils.helpers import get_http_basic_auth
from poetry.utils.toml_file import TomlFile
from .uploader import Uploader from .uploader import Uploader
logger = logging.getLogger(__name__)
class Publisher: class Publisher:
""" """
Registers and publishes packages to remote repositories. Registers and publishes packages to remote repositories.
...@@ -55,10 +57,22 @@ class Publisher: ...@@ -55,10 +57,22 @@ class Publisher:
url = repository["url"] url = repository["url"]
if not (username and password): if not (username and password):
auth = get_http_basic_auth(self._poetry.config, repository_name) # Check if we have a token first
if auth: token = self._poetry.config.get("pypi-token.{}".format(repository_name))
username = auth[0] if token:
password = auth[1] 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 # Requesting missing credentials
if not username: if not username:
......
...@@ -92,3 +92,16 @@ virtualenvs.path = {path} # /foo{sep}virtualenvs ...@@ -92,3 +92,16 @@ virtualenvs.path = {path} # /foo{sep}virtualenvs
assert expected == tester.io.fetch_output() assert expected == tester.io.fetch_output()
assert "poetry.toml" == init.call_args_list[2][0][1].path.name 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 ...@@ -5,10 +5,11 @@ from poetry.masonry.publishing.publisher import Publisher
from poetry.poetry import Poetry 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_auth = mocker.patch("poetry.masonry.publishing.uploader.Uploader.auth")
uploader_upload = mocker.patch("poetry.masonry.publishing.uploader.Uploader.upload") uploader_upload = mocker.patch("poetry.masonry.publishing.uploader.Uploader.upload")
poetry = Poetry.create(fixture_dir("sample_project")) poetry = Poetry.create(fixture_dir("sample_project"))
poetry._config = config
poetry.config.merge( poetry.config.merge(
{"http-basic": {"pypi": {"username": "foo", "password": "bar"}}} {"http-basic": {"pypi": {"username": "foo", "password": "bar"}}}
) )
...@@ -20,10 +21,11 @@ def test_publish_publishes_to_pypi_by_default(fixture_dir, mocker): ...@@ -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 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_auth = mocker.patch("poetry.masonry.publishing.uploader.Uploader.auth")
uploader_upload = mocker.patch("poetry.masonry.publishing.uploader.Uploader.upload") uploader_upload = mocker.patch("poetry.masonry.publishing.uploader.Uploader.upload")
poetry = Poetry.create(fixture_dir("sample_project")) poetry = Poetry.create(fixture_dir("sample_project"))
poetry._config = config
poetry.config.merge( poetry.config.merge(
{ {
"repositories": {"my-repo": {"url": "http://foo.bar"}}, "repositories": {"my-repo": {"url": "http://foo.bar"}},
...@@ -38,8 +40,9 @@ def test_publish_can_publish_to_given_repository(fixture_dir, mocker): ...@@ -38,8 +40,9 @@ def test_publish_can_publish_to_given_repository(fixture_dir, mocker):
assert [("http://foo.bar",)] == uploader_upload.call_args 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 = Poetry.create(fixture_dir("sample_project"))
poetry._config = config
poetry.config.merge( poetry.config.merge(
{"http-basic": {"my-repo": {"username": "foo", "password": "bar"}}} {"http-basic": {"my-repo": {"username": "foo", "password": "bar"}}}
) )
...@@ -47,3 +50,17 @@ def test_publish_raises_error_for_undefined_repository(fixture_dir, mocker): ...@@ -47,3 +50,17 @@ def test_publish_raises_error_for_undefined_repository(fixture_dir, mocker):
with pytest.raises(RuntimeError): with pytest.raises(RuntimeError):
publisher.publish("my-repo", None, None) 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