Skip to content

Commit f1137cf

Browse files
authored
Add --dry-run option for publish command (#2199)
This change introduces `--dry-run` option for the publish command. When used, will perform all actions required for publishing except for uploading build artifacts. Resolves: #2181
1 parent ab8ef19 commit f1137cf

File tree

6 files changed

+58
-25
lines changed

6 files changed

+58
-25
lines changed

docs/docs/cli.md

+1
Original file line numberDiff line numberDiff line change
@@ -325,6 +325,7 @@ It can also build the package if you pass it the `--build` option.
325325
Should match a repository name set by the [`config`](#config) command.
326326
* `--username (-u)`: The username to access the repository.
327327
* `--password (-p)`: The password to access the repository.
328+
* `--dry-run`: Perform all actions except upload the package.
328329

329330
## config
330331

poetry/console/commands/publish.py

+2
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ class PublishCommand(Command):
2626
flag=False,
2727
),
2828
option("build", None, "Build the package before publishing."),
29+
option("dry-run", None, "Perform all actions except upload the package."),
2930
]
3031

3132
help = """The publish command builds and uploads the package to a remote repository.
@@ -79,4 +80,5 @@ def handle(self):
7980
self.option("password"),
8081
cert,
8182
client_cert,
83+
self.option("dry-run"),
8284
)

poetry/publishing/publisher.py

+10-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,15 @@ def __init__(self, poetry, io):
2626
def files(self):
2727
return self._uploader.files
2828

29-
def publish(self, repository_name, username, password, cert=None, client_cert=None):
29+
def publish(
30+
self,
31+
repository_name,
32+
username,
33+
password,
34+
cert=None,
35+
client_cert=None,
36+
dry_run=False,
37+
):
3038
if repository_name:
3139
self._io.write_line(
3240
"Publishing <c1>{}</c1> (<c2>{}</c2>) "
@@ -90,4 +98,5 @@ def publish(self, repository_name, username, password, cert=None, client_cert=No
9098
url,
9199
cert=cert or get_cert(self._poetry.config, repository_name),
92100
client_cert=resolved_client_cert,
101+
dry_run=dry_run,
93102
)

poetry/publishing/uploader.py

+20-16
Original file line numberDiff line numberDiff line change
@@ -95,8 +95,8 @@ def is_authenticated(self):
9595
return self._username is not None and self._password is not None
9696

9797
def upload(
98-
self, url, cert=None, client_cert=None
99-
): # type: (str, Optional[Path], Optional[Path]) -> None
98+
self, url, cert=None, client_cert=None, dry_run=False
99+
): # type: (str, Optional[Path], Optional[Path], bool) -> None
100100
session = self.make_session()
101101

102102
if cert:
@@ -106,7 +106,7 @@ def upload(
106106
session.cert = str(client_cert)
107107

108108
try:
109-
self._upload(session, url)
109+
self._upload(session, url, dry_run)
110110
finally:
111111
session.close()
112112

@@ -188,9 +188,9 @@ def post_data(self, file):
188188

189189
return data
190190

191-
def _upload(self, session, url):
191+
def _upload(self, session, url, dry_run=False):
192192
try:
193-
self._do_upload(session, url)
193+
self._do_upload(session, url, dry_run)
194194
except HTTPError as e:
195195
if (
196196
e.response.status_code == 400
@@ -203,15 +203,16 @@ def _upload(self, session, url):
203203

204204
raise UploadError(e)
205205

206-
def _do_upload(self, session, url):
206+
def _do_upload(self, session, url, dry_run=False):
207207
for file in self.files:
208208
# TODO: Check existence
209209

210-
resp = self._upload_file(session, url, file)
210+
resp = self._upload_file(session, url, file, dry_run)
211211

212-
resp.raise_for_status()
212+
if not dry_run:
213+
resp.raise_for_status()
213214

214-
def _upload_file(self, session, url, file):
215+
def _upload_file(self, session, url, file, dry_run=False):
215216
data = self.post_data(file)
216217
data.update(
217218
{
@@ -238,14 +239,17 @@ def _upload_file(self, session, url, file):
238239

239240
bar.start()
240241

241-
resp = session.post(
242-
url,
243-
data=monitor,
244-
allow_redirects=False,
245-
headers={"Content-Type": monitor.content_type},
246-
)
242+
resp = None
243+
244+
if not dry_run:
245+
resp = session.post(
246+
url,
247+
data=monitor,
248+
allow_redirects=False,
249+
headers={"Content-Type": monitor.content_type},
250+
)
247251

248-
if resp.ok:
252+
if dry_run or resp.ok:
249253
bar.set_format(
250254
" - Uploading <c1>{0}</c1> <fg=green>%percent%%</>".format(
251255
file.name

tests/console/commands/test_publish.py

+19-2
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ def test_publish_with_cert(app_tester, mocker):
6060
app_tester.execute("publish --cert path/to/ca.pem")
6161

6262
assert [
63-
(None, None, None, Path("path/to/ca.pem"), None)
63+
(None, None, None, Path("path/to/ca.pem"), None, False)
6464
] == publisher_publish.call_args
6565

6666

@@ -69,5 +69,22 @@ def test_publish_with_client_cert(app_tester, mocker):
6969

7070
app_tester.execute("publish --client-cert path/to/client.pem")
7171
assert [
72-
(None, None, None, None, Path("path/to/client.pem"))
72+
(None, None, None, None, Path("path/to/client.pem"), False)
7373
] == publisher_publish.call_args
74+
75+
76+
def test_publish_dry_run(app_tester, http):
77+
http.register_uri(
78+
http.POST, "https://upload.pypi.org/legacy/", status=403, body="Forbidden"
79+
)
80+
81+
exit_code = app_tester.execute("publish --dry-run --username foo --password bar")
82+
83+
assert 0 == exit_code
84+
85+
output = app_tester.io.fetch_output()
86+
error = app_tester.io.fetch_error()
87+
88+
assert "Publishing simple-project (1.2.3) to PyPI" in output
89+
assert "- Uploading simple-project-1.2.3.tar.gz" in error
90+
assert "- Uploading simple_project-1.2.3-py2.py3-none-any.whl" in error

tests/publishing/test_publisher.py

+6-6
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ def test_publish_publishes_to_pypi_by_default(fixture_dir, mocker, config):
2323
assert [("foo", "bar")] == uploader_auth.call_args
2424
assert [
2525
("https://upload.pypi.org/legacy/",),
26-
{"cert": None, "client_cert": None},
26+
{"cert": None, "client_cert": None, "dry_run": False},
2727
] == uploader_upload.call_args
2828

2929

@@ -45,7 +45,7 @@ def test_publish_can_publish_to_given_repository(fixture_dir, mocker, config):
4545
assert [("foo", "bar")] == uploader_auth.call_args
4646
assert [
4747
("http://foo.bar",),
48-
{"cert": None, "client_cert": None},
48+
{"cert": None, "client_cert": None, "dry_run": False},
4949
] == uploader_upload.call_args
5050

5151

@@ -74,7 +74,7 @@ def test_publish_uses_token_if_it_exists(fixture_dir, mocker, config):
7474
assert [("__token__", "my-token")] == uploader_auth.call_args
7575
assert [
7676
("https://upload.pypi.org/legacy/",),
77-
{"cert": None, "client_cert": None},
77+
{"cert": None, "client_cert": None, "dry_run": False},
7878
] == uploader_upload.call_args
7979

8080

@@ -98,7 +98,7 @@ def test_publish_uses_cert(fixture_dir, mocker, config):
9898
assert [("foo", "bar")] == uploader_auth.call_args
9999
assert [
100100
("https://foo.bar",),
101-
{"cert": Path(cert), "client_cert": None},
101+
{"cert": Path(cert), "client_cert": None, "dry_run": False},
102102
] == uploader_upload.call_args
103103

104104

@@ -119,7 +119,7 @@ def test_publish_uses_client_cert(fixture_dir, mocker, config):
119119

120120
assert [
121121
("https://foo.bar",),
122-
{"cert": None, "client_cert": Path(client_cert)},
122+
{"cert": None, "client_cert": Path(client_cert), "dry_run": False},
123123
] == uploader_upload.call_args
124124

125125

@@ -137,5 +137,5 @@ def test_publish_read_from_environment_variable(fixture_dir, environ, mocker, co
137137
assert [("bar", "baz")] == uploader_auth.call_args
138138
assert [
139139
("https://foo.bar",),
140-
{"cert": None, "client_cert": None},
140+
{"cert": None, "client_cert": None, "dry_run": False},
141141
] == uploader_upload.call_args

0 commit comments

Comments
 (0)