Skip to content

Commit

Permalink
feat(core): New command: pdm publish (#1107)
Browse files Browse the repository at this point in the history
  • Loading branch information
frostming committed Jun 24, 2022
1 parent 184e42d commit 994c08e
Show file tree
Hide file tree
Showing 20 changed files with 1,019 additions and 78 deletions.
50 changes: 50 additions & 0 deletions docs/docs/usage/project.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,56 @@ If `-g/--global` option is used, the first item will be replaced by `~/.pdm/glob

You can find all available configuration items in [Configuration Page](../configuration.md).

## Publish the project to PyPI

With PDM, you can build and then upload your project to PyPI in one step.

```bash
pdm publish
```

You can specify which repository you would like to publish:

```bash
pdm publish -r pypi
```

PDM will look for the repository named `pypi` from the configuration and use the URL for upload.
You can also give the URL directly with `-r/--repository` option:

```bash
pdm publish -r https://test.pypi.org/simple
```

See all supported options by typing `pdm publish --help`.

### Configure the repository secrets for upload

When using the `pdm publish` command, it reads the repository secrets from the *global* config file(`~/.pdm/config.toml`). The content of the config is as follows:

```toml
[repository.pypi]
username = "frostming"
password = "<secret>"

[repository.company]
url = "https://pypi.company.org/legacy/"
username = "frostming"
password = "<secret>"
```

!!! NOTE
You don't need to configure the `url` for `pypi` and `testpypi` repositories, they are filled by default values.

To change the repository config from the command line, use the `pdm config` command:

```bash
pdm config repository.pypi.username "__token__"
pdm config repository.pypi.password "my-pypi-token"

pdm config repository.company.url "https://pypi.company.org/legacy/"
```

## Cache the installation of wheels

If a package is required by many projects on the system, each project has to keep its own copy. This may become a waste of disk space especially for data science and machine learning libraries.
Expand Down
1 change: 1 addition & 0 deletions news/1107.feature.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add a new command `publish` to PDM since it is required for so many people and it will make the workflow easier.
16 changes: 14 additions & 2 deletions pdm.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion pdm/cli/commands/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,11 @@ def _get_config(self, project: Project, options: argparse.Namespace) -> None:
err=True,
)
options.key = project.project_config.deprecated[options.key]
project.core.ui.echo(project.config[options.key])
if options.key.split(".")[0] == "repository":
value = project.global_config[options.key]
else:
value = project.config[options.key]
project.core.ui.echo(value)

def _set_config(self, project: Project, options: argparse.Namespace) -> None:
config = project.project_config if options.local else project.global_config
Expand Down
165 changes: 165 additions & 0 deletions pdm/cli/commands/publish/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
from __future__ import annotations

import argparse
import os

import requests
from rich.progress import (
BarColumn,
DownloadColumn,
TimeRemainingColumn,
TransferSpeedColumn,
)

from pdm.cli import actions
from pdm.cli.commands.base import BaseCommand
from pdm.cli.commands.publish.package import PackageFile
from pdm.cli.commands.publish.repository import Repository
from pdm.cli.options import project_option, verbose_option
from pdm.exceptions import PdmUsageError, PublishError
from pdm.project import Project
from pdm.termui import logger


class Command(BaseCommand):
"""Build and publish the project to PyPI"""

arguments = [verbose_option, project_option]

def add_arguments(self, parser: argparse.ArgumentParser) -> None:
parser.add_argument(
"-r",
"--repository",
help="The repository name or url to publish the package to"
" [env var: PDM_PUBLISH_REPO]",
)
parser.add_argument(
"-u",
"--username",
help="The username to access the repository"
" [env var: PDM_PUBLISH_USERNAME]",
)
parser.add_argument(
"-P",
"--password",
help="The password to access the repository"
" [env var: PDM_PUBLISH_PASSWORD]",
)
parser.add_argument(
"-S",
"--sign",
action="store_true",
help="Upload the package with PGP signature",
)
parser.add_argument(
"-i",
"--identity",
help="GPG identity used to sign files.",
)
parser.add_argument(
"-c",
"--comment",
help="The comment to include with the distribution file.",
)
parser.add_argument(
"--no-build",
action="store_false",
dest="build",
help="Don't build the package before publishing",
)

@staticmethod
def _make_package(
filename: str, signatures: dict[str, str], options: argparse.Namespace
) -> PackageFile:
p = PackageFile.from_filename(filename, options.comment)
if p.base_filename in signatures:
p.add_gpg_signature(signatures[p.base_filename], p.base_filename + ".asc")
elif options.sign:
p.sign(options.identity)
return p

@staticmethod
def _check_response(response: requests.Response) -> None:
message = ""
if response.status_code == 410 and "pypi.python.org" in response.url:
message = (
"Uploading to these sites is deprecated. "
"Try using https://upload.pypi.org/legacy/ "
"(or https://test.pypi.org/legacy/) instead."
)
elif response.status_code == 405 and "pypi.org" in response.url:
message = (
"It appears you're trying to upload to pypi.org but have an "
"invalid URL."
)
else:
try:
response.raise_for_status()
except requests.HTTPError as err:
message = str(err)
if message:
raise PublishError(message)

@staticmethod
def get_repository(project: Project, options: argparse.Namespace) -> Repository:
repository = options.repository or os.getenv("PDM_PUBLISH_REPO", "pypi")
username = options.username or os.getenv("PDM_PUBLISH_USERNAME")
password = options.password or os.getenv("PDM_PUBLISH_PASSWORD")

config = project.global_config.get_repository_config(repository)
if config is None:
raise PdmUsageError(f"Missing repository config of {repository}")
if username is not None:
config.username = username
if password is not None:
config.password = password
return Repository(project, config.url, config.username, config.password)

def handle(self, project: Project, options: argparse.Namespace) -> None:
if options.build:
actions.do_build(project)

package_files = [
str(p)
for p in project.root.joinpath("dist").iterdir()
if not p.name.endswith(".asc")
]
signatures = {
p.stem: str(p)
for p in project.root.joinpath("dist").iterdir()
if p.name.endswith(".asc")
}

repository = self.get_repository(project, options)
uploaded: list[PackageFile] = []
with project.core.ui.make_progress(
" [progress.percentage]{task.percentage:>3.0f}%",
BarColumn(),
DownloadColumn(),
"•",
TimeRemainingColumn(
compact=True,
elapsed_when_finished=True,
),
"•",
TransferSpeedColumn(),
) as progress, project.core.ui.logging("publish"):
packages = sorted(
(self._make_package(p, signatures, options) for p in package_files),
# Upload wheels first if they exist.
key=lambda p: not p.base_filename.endswith(".whl"),
)
for package in packages:
resp = repository.upload(package, progress)
logger.debug(
"Response from %s:\n%s %s", resp.url, resp.status_code, resp.reason
)
self._check_response(resp)
uploaded.append(package)

release_urls = repository.get_release_urls(uploaded)
if release_urls:
project.core.ui.echo("\n[green]View at:")
for url in release_urls:
project.core.ui.echo(url)
Loading

0 comments on commit 994c08e

Please sign in to comment.