Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use Rich for log messages #851

Merged
merged 16 commits into from
Feb 5, 2022
Merged
4 changes: 0 additions & 4 deletions mypy.ini
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,6 @@ warn_return_any = True
no_implicit_reexport = True
strict_equality = True

[mypy-colorama]
; https://github.com/tartley/colorama/issues/206
ignore_missing_imports = True

[mypy-pkginfo]
; https://bugs.launchpad.net/pkginfo/+bug/1876591
ignore_missing_imports = True
Expand Down
2 changes: 1 addition & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ install_requires=
importlib_metadata >= 3.6
keyring >= 15.1
rfc3986 >= 1.4.0
colorama >= 0.4.3
rich
include_package_data = True

[options.entry_points]
Expand Down
14 changes: 5 additions & 9 deletions tests/test_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -149,21 +149,17 @@ def get_credential(system, username):


def test_get_username_runtime_error_suppressed(
entered_username, keyring_no_backends_get_credential, recwarn, config
entered_username, keyring_no_backends_get_credential, caplog, config
):
assert auth.Resolver(config, auth.CredentialInput()).username == "entered user"
assert len(recwarn) == 1
warning = recwarn.pop(UserWarning)
assert "fail!" in str(warning)
assert caplog.messages == ["fail!"]


def test_get_password_runtime_error_suppressed(
entered_password, keyring_no_backends, recwarn, config
entered_password, keyring_no_backends, caplog, config
):
assert auth.Resolver(config, auth.CredentialInput("user")).password == "entered pw"
assert len(recwarn) == 1
warning = recwarn.pop(UserWarning)
assert "fail!" in str(warning)
assert caplog.messages == ["fail!"]


def test_get_username_return_none(entered_username, monkeypatch, config):
Expand Down Expand Up @@ -227,4 +223,4 @@ def test_warns_for_empty_password(

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

assert caplog.messages[0].startswith(f" {warning}")
assert caplog.messages[0].startswith(warning)
37 changes: 29 additions & 8 deletions tests/test_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,39 @@

import sys

import colorama

from twine import __main__ as dunder_main


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

error = dunder_main.main()
assert error

captured = capsys.readouterr()

# Hard-coding control characters for red text; couldn't find a succint alternative.
# Removing trailing whitespace on wrapped lines; trying to test it was ugly.
level = "\x1b[31mERROR \x1b[0m"
assert [line.rstrip() for line in captured.out.splitlines()] == [
f"{level} InvalidDistribution: Cannot find file (or expand pattern):",
" 'missing.whl'",
]

def test_no_color_exception(monkeypatch):

def test_no_color_exception(monkeypatch, capsys):
monkeypatch.setattr(sys, "argv", ["twine", "--no-color", "upload", "missing.whl"])
message = "InvalidDistribution: Cannot find file (or expand pattern): 'missing.whl'"
assert dunder_main.main() == message

error = dunder_main.main()
assert error

captured = capsys.readouterr()

# Removing trailing whitespace on wrapped lines; trying to test it was ugly.
assert [line.rstrip() for line in captured.out.splitlines()] == [
"ERROR InvalidDistribution: Cannot find file (or expand pattern):",
" 'missing.whl'",
]


# TODO: Test verbose output formatting
8 changes: 3 additions & 5 deletions tests/test_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,16 +66,14 @@ def test_setup_logging(verbose, log_level):
"verbose",
[True, False],
)
def test_print_config_path_if_verbose(config_file, capsys, make_settings, verbose):
def test_print_config_path_if_verbose(config_file, caplog, make_settings, verbose):
"""Print the location of the .pypirc config used by the user."""
make_settings(verbose=verbose)

captured = capsys.readouterr()

if verbose:
assert captured.out == f"Using configuration from {config_file}\n"
assert caplog.messages == [f"Using configuration from {config_file}"]
else:
assert captured.out == ""
assert caplog.messages == []


def test_identity_requires_sign():
Expand Down
44 changes: 25 additions & 19 deletions tests/test_upload.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ def upload_settings(make_settings, stub_repository):
return upload_settings


def test_make_package_pre_signed_dist(upload_settings, capsys):
def test_make_package_pre_signed_dist(upload_settings, caplog):
"""Create a PackageFile and print path, size, and user-provided signature."""
filename = helpers.WHEEL_FIXTURE
expected_size = "15.4 KB"
Expand All @@ -69,12 +69,13 @@ def test_make_package_pre_signed_dist(upload_settings, capsys):
assert package.filename == filename
assert package.gpg_signature is not None

captured = capsys.readouterr()
assert captured.out.count(f"{filename} ({expected_size})") == 1
assert captured.out.count(f"Signed with {signed_filename}") == 1
assert caplog.messages == [
f"{filename} ({expected_size})",
f"Signed with {signed_filename}",
]


def test_make_package_unsigned_dist(upload_settings, monkeypatch, capsys):
def test_make_package_unsigned_dist(upload_settings, monkeypatch, caplog):
"""Create a PackageFile and print path, size, and Twine-generated signature."""
filename = helpers.NEW_WHEEL_FIXTURE
expected_size = "21.9 KB"
Expand All @@ -93,9 +94,10 @@ def stub_sign(package, *_):
assert package.filename == filename
assert package.gpg_signature is not None

captured = capsys.readouterr()
assert captured.out.count(f"{filename} ({expected_size})") == 1
assert captured.out.count(f"Signed with {package.signed_filename}") == 1
assert caplog.messages == [
f"{filename} ({expected_size})",
f"Signed with {package.signed_filename}",
]


def test_successs_prints_release_urls(upload_settings, stub_repository, capsys):
Expand All @@ -118,24 +120,23 @@ def test_successs_prints_release_urls(upload_settings, stub_repository, capsys):
assert captured.out.count(NEW_RELEASE_URL) == 1


def test_print_packages_if_verbose(upload_settings, capsys):
def test_print_packages_if_verbose(upload_settings, caplog):
"""Print the path and file size of each distribution attempting to be uploaded."""
dists_to_upload = {
helpers.WHEEL_FIXTURE: "15.4 KB",
helpers.NEW_WHEEL_FIXTURE: "21.9 KB",
helpers.SDIST_FIXTURE: "20.8 KB",
helpers.NEW_SDIST_FIXTURE: "26.1 KB",
helpers.NEW_WHEEL_FIXTURE: "21.9 KB",
}

upload_settings.verbose = True

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

captured = capsys.readouterr()

for filename, size in dists_to_upload.items():
assert captured.out.count(f"{filename} ({size})") == 1
assert caplog.messages == [
f"{filename} ({size})" for filename, size in dists_to_upload.items()
]


def test_success_with_pre_signed_distribution(upload_settings, stub_repository):
Expand Down Expand Up @@ -181,7 +182,9 @@ def test_success_when_gpg_is_run(upload_settings, stub_repository, monkeypatch):


@pytest.mark.parametrize("verbose", [False, True])
def test_exception_for_http_status(verbose, upload_settings, stub_response, capsys):
def test_exception_for_http_status(
verbose, upload_settings, stub_response, capsys, caplog
):
upload_settings.verbose = verbose

stub_response.is_redirect = False
Expand All @@ -196,11 +199,14 @@ def test_exception_for_http_status(verbose, upload_settings, stub_response, caps
assert RELEASE_URL not in captured.out

if verbose:
assert stub_response.text in captured.out
assert "--verbose" not in captured.out
assert caplog.messages == [
f"{helpers.WHEEL_FIXTURE} (15.4 KB)",
f"Content received from server:\n{stub_response.text}",
]
else:
assert stub_response.text not in captured.out
assert "--verbose" in captured.out
assert caplog.messages == [
"Error during upload. Retry with the --verbose option for more details."
]


def test_get_config_old_format(make_settings, config_file):
Expand Down
13 changes: 8 additions & 5 deletions tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -265,7 +265,7 @@ def test_check_status_code_for_deprecated_pypi_url(repo_url):
[True, False],
)
def test_check_status_code_for_missing_status_code(
capsys, repo_url, verbose, make_settings
caplog, repo_url, verbose, make_settings, config_file
):
"""Print HTTP errors based on verbosity level."""
response = pretend.stub(
Expand All @@ -280,12 +280,15 @@ def test_check_status_code_for_missing_status_code(
with pytest.raises(requests.HTTPError):
utils.check_status_code(response, verbose)

captured = capsys.readouterr()

if verbose:
assert captured.out.count("Content received from server:\nForbidden\n") == 1
assert caplog.messages == [
f"Using configuration from {config_file}",
f"Content received from server:\n{response.text}",
]
else:
assert captured.out.count("--verbose option") == 1
assert caplog.messages == [
"Error during upload. Retry with the --verbose option for more details."
]


@pytest.mark.parametrize(
Expand Down
26 changes: 12 additions & 14 deletions twine/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,40 +13,38 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import http
import logging
import sys
from typing import Any

import colorama
import requests

from twine import cli
from twine import exceptions

logger = logging.getLogger(__name__)


def main() -> Any:
# Ensure that all errors are logged, even before argparse
cli.configure_logging()

try:
result = cli.dispatch(sys.argv[1:])
error = cli.dispatch(sys.argv[1:])
except requests.HTTPError as exc:
error = True
status_code = exc.response.status_code
status_phrase = http.HTTPStatus(status_code).phrase
result = (
logger.error(
f"{exc.__class__.__name__}: {status_code} {status_phrase} "
f"from {exc.response.url}\n"
f"{exc.response.reason}"
)
except exceptions.TwineException as exc:
result = f"{exc.__class__.__name__}: {exc.args[0]}"

return _format_error(result) if isinstance(result, str) else result


def _format_error(message: str) -> str:
pre_style, post_style = "", ""
if not cli.args.no_color:
colorama.init()
pre_style, post_style = colorama.Fore.RED, colorama.Style.RESET_ALL
error = True
logger.error(f"{exc.__class__.__name__}: {exc.args[0]}")

return f"{pre_style}{message}{post_style}"
return error


if __name__ == "__main__":
Expand Down
5 changes: 2 additions & 3 deletions twine/auth.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import functools
import getpass
import logging
import warnings
from typing import Callable, Optional, Type, cast

import keyring
Expand Down Expand Up @@ -64,7 +63,7 @@ def get_username_from_keyring(self) -> Optional[str]:
# To support keyring prior to 15.2
pass
except Exception as exc:
warnings.warn(str(exc))
logger.warning(str(exc))
return None

def get_password_from_keyring(self) -> Optional[str]:
Expand All @@ -74,7 +73,7 @@ def get_password_from_keyring(self) -> Optional[str]:
logger.info("Querying keyring for password")
return cast(str, keyring.get_password(system, username))
except Exception as exc:
warnings.warn(str(exc))
logger.warning(str(exc))
return None

def username_from_keyring_or_prompt(self) -> str:
Expand Down
Loading