Skip to content

Commit

Permalink
Add support for PyPI API tokens (#1275)
Browse files Browse the repository at this point in the history
  • Loading branch information
sdispater authored Aug 2, 2019
1 parent 11c2b9a commit e5e706a
Show file tree
Hide file tree
Showing 6 changed files with 81 additions and 13 deletions.
15 changes: 13 additions & 2 deletions docs/docs/repositories.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
```
Expand Down
13 changes: 12 additions & 1 deletion poetry/console/commands/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ def handle(self):
)

# 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))
Expand Down Expand Up @@ -209,6 +209,17 @@ def handle(self):
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

Expand Down
2 changes: 2 additions & 0 deletions poetry/console/commands/publish.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ class PublishCommand(Command):
the config command.
"""

loggers = ["poetry.masonry.publishing.publisher"]

def handle(self):
from poetry.masonry.publishing.publisher import Publisher

Expand Down
28 changes: 21 additions & 7 deletions poetry/masonry/publishing/publisher.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
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.
Expand Down Expand Up @@ -55,10 +57,22 @@ def publish(self, repository_name, username, password):
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:
Expand Down
13 changes: 13 additions & 0 deletions tests/console/commands/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,3 +92,16 @@ def test_list_displays_set_get_local_setting(
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"]
23 changes: 20 additions & 3 deletions tests/masonry/publishing/test_publisher.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@
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"}}}
)
Expand All @@ -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"}},
Expand All @@ -38,12 +40,27 @@ 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"}}}
)
publisher = Publisher(poetry, NullIO())

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

0 comments on commit e5e706a

Please sign in to comment.