Skip to content

Commit 133f8ae

Browse files
authored
Use Rich for log messages (#851)
* Replace warnings.warn with logger.warning * Use rich for logging * Use Rich for top-level errors * Update log level colors * Use caplog in tests instead of capsys. When using Rich, capsys results in wrapped output, which would be very hard to make assertions on. * Test color output * Support --no-color * Fix --no-color test * Use dictConfig * Fix unused import * Avoid keyring backend warning * Add coverage for HTTP error logging * Use reconfigure instead of module attribute * Add changelog entry
1 parent c5769e0 commit 133f8ae

File tree

14 files changed

+179
-78
lines changed

14 files changed

+179
-78
lines changed

changelog/818.feature.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Use Rich to add color to ``upload`` logging output.

mypy.ini

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,6 @@ warn_return_any = True
1818
no_implicit_reexport = True
1919
strict_equality = True
2020

21-
[mypy-colorama]
22-
; https://github.com/tartley/colorama/issues/206
23-
ignore_missing_imports = True
24-
2521
[mypy-pkginfo]
2622
; https://bugs.launchpad.net/pkginfo/+bug/1876591
2723
ignore_missing_imports = True

setup.cfg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ install_requires=
4545
importlib_metadata >= 3.6
4646
keyring >= 15.1
4747
rfc3986 >= 1.4.0
48-
colorama >= 0.4.3
48+
rich
4949
include_package_data = True
5050

5151
[options.entry_points]

tests/test_auth.py

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -149,21 +149,17 @@ def get_credential(system, username):
149149

150150

151151
def test_get_username_runtime_error_suppressed(
152-
entered_username, keyring_no_backends_get_credential, recwarn, config
152+
entered_username, keyring_no_backends_get_credential, caplog, config
153153
):
154154
assert auth.Resolver(config, auth.CredentialInput()).username == "entered user"
155-
assert len(recwarn) == 1
156-
warning = recwarn.pop(UserWarning)
157-
assert "fail!" in str(warning)
155+
assert caplog.messages == ["fail!"]
158156

159157

160158
def test_get_password_runtime_error_suppressed(
161-
entered_password, keyring_no_backends, recwarn, config
159+
entered_password, keyring_no_backends, caplog, config
162160
):
163161
assert auth.Resolver(config, auth.CredentialInput("user")).password == "entered pw"
164-
assert len(recwarn) == 1
165-
warning = recwarn.pop(UserWarning)
166-
assert "fail!" in str(warning)
162+
assert caplog.messages == ["fail!"]
167163

168164

169165
def test_get_username_return_none(entered_username, monkeypatch, config):
@@ -223,8 +219,11 @@ def test_warns_for_empty_password(
223219
config,
224220
caplog,
225221
):
222+
# Avoiding additional warning "No recommended backend was available"
223+
monkeypatch.setattr(auth.keyring, "get_password", lambda system, user: None)
224+
226225
monkeypatch.setattr(getpass, "getpass", lambda prompt: password)
227226

228227
assert auth.Resolver(config, auth.CredentialInput()).password == password
229228

230-
assert caplog.messages[0].startswith(f" {warning}")
229+
assert caplog.messages[0].startswith(warning)

tests/test_main.py

Lines changed: 62 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,18 +12,73 @@
1212

1313
import sys
1414

15-
import colorama
15+
import pretend
16+
import requests
1617

1718
from twine import __main__ as dunder_main
19+
from twine.commands import upload
1820

1921

20-
def test_exception_handling(monkeypatch):
22+
def test_exception_handling(monkeypatch, capsys):
2123
monkeypatch.setattr(sys, "argv", ["twine", "upload", "missing.whl"])
22-
message = "InvalidDistribution: Cannot find file (or expand pattern): 'missing.whl'"
23-
assert dunder_main.main() == colorama.Fore.RED + message + colorama.Style.RESET_ALL
2424

25+
error = dunder_main.main()
26+
assert error
2527

26-
def test_no_color_exception(monkeypatch):
28+
captured = capsys.readouterr()
29+
30+
# Hard-coding control characters for red text; couldn't find a succint alternative.
31+
# Removing trailing whitespace on wrapped lines; trying to test it was ugly.
32+
level = "\x1b[31mERROR \x1b[0m"
33+
assert [line.rstrip() for line in captured.out.splitlines()] == [
34+
f"{level} InvalidDistribution: Cannot find file (or expand pattern):",
35+
" 'missing.whl'",
36+
]
37+
38+
39+
def test_http_exception_handling(monkeypatch, capsys):
40+
monkeypatch.setattr(sys, "argv", ["twine", "upload", "test.whl"])
41+
monkeypatch.setattr(
42+
upload,
43+
"upload",
44+
pretend.raiser(
45+
requests.HTTPError(
46+
response=pretend.stub(
47+
url="https://example.org",
48+
status_code=400,
49+
reason="Error reason",
50+
)
51+
)
52+
),
53+
)
54+
55+
error = dunder_main.main()
56+
assert error
57+
58+
captured = capsys.readouterr()
59+
60+
# Hard-coding control characters for red text; couldn't find a succint alternative.
61+
# Removing trailing whitespace on wrapped lines; trying to test it was ugly.
62+
level = "\x1b[31mERROR \x1b[0m"
63+
assert [line.rstrip() for line in captured.out.splitlines()] == [
64+
f"{level} HTTPError: 400 Bad Request from https://example.org",
65+
" Error reason",
66+
]
67+
68+
69+
def test_no_color_exception(monkeypatch, capsys):
2770
monkeypatch.setattr(sys, "argv", ["twine", "--no-color", "upload", "missing.whl"])
28-
message = "InvalidDistribution: Cannot find file (or expand pattern): 'missing.whl'"
29-
assert dunder_main.main() == message
71+
72+
error = dunder_main.main()
73+
assert error
74+
75+
captured = capsys.readouterr()
76+
77+
# Removing trailing whitespace on wrapped lines; trying to test it was ugly.
78+
assert [line.rstrip() for line in captured.out.splitlines()] == [
79+
"ERROR InvalidDistribution: Cannot find file (or expand pattern):",
80+
" 'missing.whl'",
81+
]
82+
83+
84+
# TODO: Test verbose output formatting

tests/test_settings.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -66,16 +66,14 @@ def test_setup_logging(verbose, log_level):
6666
"verbose",
6767
[True, False],
6868
)
69-
def test_print_config_path_if_verbose(config_file, capsys, make_settings, verbose):
69+
def test_print_config_path_if_verbose(config_file, caplog, make_settings, verbose):
7070
"""Print the location of the .pypirc config used by the user."""
7171
make_settings(verbose=verbose)
7272

73-
captured = capsys.readouterr()
74-
7573
if verbose:
76-
assert captured.out == f"Using configuration from {config_file}\n"
74+
assert caplog.messages == [f"Using configuration from {config_file}"]
7775
else:
78-
assert captured.out == ""
76+
assert caplog.messages == []
7977

8078

8179
def test_identity_requires_sign():

tests/test_upload.py

Lines changed: 28 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ def upload_settings(make_settings, stub_repository):
5959
return upload_settings
6060

6161

62-
def test_make_package_pre_signed_dist(upload_settings, capsys):
62+
def test_make_package_pre_signed_dist(upload_settings, caplog):
6363
"""Create a PackageFile and print path, size, and user-provided signature."""
6464
filename = helpers.WHEEL_FIXTURE
6565
expected_size = "15.4 KB"
@@ -74,12 +74,13 @@ def test_make_package_pre_signed_dist(upload_settings, capsys):
7474
assert package.filename == filename
7575
assert package.gpg_signature is not None
7676

77-
captured = capsys.readouterr()
78-
assert captured.out.count(f"{filename} ({expected_size})") == 1
79-
assert captured.out.count(f"Signed with {signed_filename}") == 1
77+
assert caplog.messages == [
78+
f"{filename} ({expected_size})",
79+
f"Signed with {signed_filename}",
80+
]
8081

8182

82-
def test_make_package_unsigned_dist(upload_settings, monkeypatch, capsys):
83+
def test_make_package_unsigned_dist(upload_settings, monkeypatch, caplog):
8384
"""Create a PackageFile and print path, size, and Twine-generated signature."""
8485
filename = helpers.NEW_WHEEL_FIXTURE
8586
expected_size = "21.9 KB"
@@ -98,9 +99,10 @@ def stub_sign(package, *_):
9899
assert package.filename == filename
99100
assert package.gpg_signature is not None
100101

101-
captured = capsys.readouterr()
102-
assert captured.out.count(f"{filename} ({expected_size})") == 1
103-
assert captured.out.count(f"Signed with {package.signed_filename}") == 1
102+
assert caplog.messages == [
103+
f"{filename} ({expected_size})",
104+
f"Signed with {package.signed_filename}",
105+
]
104106

105107

106108
def test_successs_prints_release_urls(upload_settings, stub_repository, capsys):
@@ -123,27 +125,26 @@ def test_successs_prints_release_urls(upload_settings, stub_repository, capsys):
123125
assert captured.out.count(NEW_RELEASE_URL) == 1
124126

125127

126-
def test_print_packages_if_verbose(upload_settings, capsys):
128+
def test_print_packages_if_verbose(upload_settings, caplog):
127129
"""Print the path and file size of each distribution attempting to be uploaded."""
128130
dists_to_upload = {
129131
helpers.WHEEL_FIXTURE: "15.4 KB",
132+
helpers.NEW_WHEEL_FIXTURE: "21.9 KB",
130133
helpers.SDIST_FIXTURE: "20.8 KB",
131134
helpers.NEW_SDIST_FIXTURE: "26.1 KB",
132-
helpers.NEW_WHEEL_FIXTURE: "21.9 KB",
133135
}
134136

135137
upload_settings.verbose = True
136138

137139
result = upload.upload(upload_settings, dists_to_upload.keys())
138140
assert result is None
139141

140-
captured = capsys.readouterr()
141-
142-
for filename, size in dists_to_upload.items():
143-
assert captured.out.count(f"{filename} ({size})") == 1
142+
assert [m for m in caplog.messages if m.endswith("KB)")] == [
143+
f"{filename} ({size})" for filename, size in dists_to_upload.items()
144+
]
144145

145146

146-
def test_print_response_if_verbose(upload_settings, stub_response, capsys):
147+
def test_print_response_if_verbose(upload_settings, stub_response, caplog):
147148
"""Print details about the response from the repostiry."""
148149
upload_settings.verbose = True
149150

@@ -153,13 +154,12 @@ def test_print_response_if_verbose(upload_settings, stub_response, capsys):
153154
)
154155
assert result is None
155156

156-
captured = capsys.readouterr()
157157
response_log = (
158158
f"Response from {stub_response.url}:\n"
159159
f"{stub_response.status_code} {stub_response.reason}"
160160
)
161161

162-
assert captured.out.count(response_log) == 2
162+
assert caplog.messages.count(response_log) == 2
163163

164164

165165
def test_success_with_pre_signed_distribution(upload_settings, stub_repository):
@@ -205,7 +205,9 @@ def test_success_when_gpg_is_run(upload_settings, stub_repository, monkeypatch):
205205

206206

207207
@pytest.mark.parametrize("verbose", [False, True])
208-
def test_exception_for_http_status(verbose, upload_settings, stub_response, capsys):
208+
def test_exception_for_http_status(
209+
verbose, upload_settings, stub_response, capsys, caplog
210+
):
209211
upload_settings.verbose = verbose
210212

211213
stub_response.is_redirect = False
@@ -221,11 +223,15 @@ def test_exception_for_http_status(verbose, upload_settings, stub_response, caps
221223
assert RELEASE_URL not in captured.out
222224

223225
if verbose:
224-
assert stub_response.text in captured.out
225-
assert "--verbose" not in captured.out
226+
assert caplog.messages == [
227+
f"{helpers.WHEEL_FIXTURE} (15.4 KB)",
228+
f"Response from {stub_response.url}:\n403 {stub_response.reason}",
229+
stub_response.text,
230+
]
226231
else:
227-
assert stub_response.text not in captured.out
228-
assert "--verbose" in captured.out
232+
assert caplog.messages == [
233+
"Error during upload. Retry with the --verbose option for more details."
234+
]
229235

230236

231237
def test_get_config_old_format(make_settings, config_file):

tests/test_utils.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -265,7 +265,7 @@ def test_check_status_code_for_deprecated_pypi_url(repo_url):
265265
[True, False],
266266
)
267267
def test_check_status_code_for_missing_status_code(
268-
capsys, repo_url, verbose, make_settings
268+
caplog, repo_url, verbose, make_settings, config_file
269269
):
270270
"""Print HTTP errors based on verbosity level."""
271271
response = pretend.stub(
@@ -280,9 +280,10 @@ def test_check_status_code_for_missing_status_code(
280280
with pytest.raises(requests.HTTPError):
281281
utils.check_status_code(response, verbose)
282282

283-
captured = capsys.readouterr()
284-
285-
assert captured.out.count("--verbose option") == 0 if verbose else 1
283+
message = (
284+
"Error during upload. Retry with the --verbose option for more details."
285+
)
286+
assert caplog.messages.count(message) == 0 if verbose else 1
286287

287288

288289
@pytest.mark.parametrize(

twine/__main__.py

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -13,40 +13,38 @@
1313
# See the License for the specific language governing permissions and
1414
# limitations under the License.
1515
import http
16+
import logging
1617
import sys
1718
from typing import Any
1819

19-
import colorama
2020
import requests
2121

2222
from twine import cli
2323
from twine import exceptions
2424

25+
logger = logging.getLogger(__name__)
26+
2527

2628
def main() -> Any:
29+
# Ensure that all errors are logged, even before argparse
30+
cli.configure_output()
31+
2732
try:
28-
result = cli.dispatch(sys.argv[1:])
33+
error = cli.dispatch(sys.argv[1:])
2934
except requests.HTTPError as exc:
35+
error = True
3036
status_code = exc.response.status_code
3137
status_phrase = http.HTTPStatus(status_code).phrase
32-
result = (
38+
logger.error(
3339
f"{exc.__class__.__name__}: {status_code} {status_phrase} "
3440
f"from {exc.response.url}\n"
3541
f"{exc.response.reason}"
3642
)
3743
except exceptions.TwineException as exc:
38-
result = f"{exc.__class__.__name__}: {exc.args[0]}"
39-
40-
return _format_error(result) if isinstance(result, str) else result
41-
42-
43-
def _format_error(message: str) -> str:
44-
pre_style, post_style = "", ""
45-
if not cli.args.no_color:
46-
colorama.init()
47-
pre_style, post_style = colorama.Fore.RED, colorama.Style.RESET_ALL
44+
error = True
45+
logger.error(f"{exc.__class__.__name__}: {exc.args[0]}")
4846

49-
return f"{pre_style}{message}{post_style}"
47+
return error
5048

5149

5250
if __name__ == "__main__":

twine/auth.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import functools
22
import getpass
33
import logging
4-
import warnings
54
from typing import Callable, Optional, Type, cast
65

76
import keyring
@@ -64,7 +63,7 @@ def get_username_from_keyring(self) -> Optional[str]:
6463
# To support keyring prior to 15.2
6564
pass
6665
except Exception as exc:
67-
warnings.warn(str(exc))
66+
logger.warning(str(exc))
6867
return None
6968

7069
def get_password_from_keyring(self) -> Optional[str]:
@@ -74,7 +73,7 @@ def get_password_from_keyring(self) -> Optional[str]:
7473
logger.info("Querying keyring for password")
7574
return cast(str, keyring.get_password(system, username))
7675
except Exception as exc:
77-
warnings.warn(str(exc))
76+
logger.warning(str(exc))
7877
return None
7978

8079
def username_from_keyring_or_prompt(self) -> str:

0 commit comments

Comments
 (0)