diff --git a/CHANGELOG.md b/CHANGELOG.md index 026916d2..e74b3317 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,7 @@ This project adheres to [Semantic Versioning](https://semver.org/). - Fixed escaping of integer indexes with multiple backslashes in the nested JSON builder. ([#1285](https://github.com/httpie/httpie/issues/1285)) - Fixed displaying of status code without a status message on non-`auto` themes. ([#1300](https://github.com/httpie/httpie/issues/1300)) - Improved regulation of top-level arrays. ([#1292](https://github.com/httpie/httpie/commit/225dccb2186f14f871695b6c4e0bfbcdb2e3aa28)) - +- Double `--quiet` flags will now suppress all python level warnings. ([#1271](https://github.com/httpie/httpie/issues/1271)) ## [3.0.2](https://github.com/httpie/httpie/compare/3.0.1...3.0.2) (2022-01-24) diff --git a/httpie/cli/argparser.py b/httpie/cli/argparser.py index 64481096..b1ab8de1 100644 --- a/httpie/cli/argparser.py +++ b/httpie/cli/argparser.py @@ -230,9 +230,11 @@ class HTTPieArgumentParser(BaseHTTPieArgumentParser): self.env.stdout_isatty = False if self.args.quiet: + self.env.quiet = self.args.quiet self.env.stderr = self.env.devnull if not (self.args.output_file_specified and not self.args.download): self.env.stdout = self.env.devnull + self.env.apply_warnings_filter() def _process_auth(self): # TODO: refactor & simplify this method. diff --git a/httpie/context.py b/httpie/context.py index 7a6e6a86..50a8f772 100644 --- a/httpie/context.py +++ b/httpie/context.py @@ -1,8 +1,10 @@ import sys import os +import warnings from contextlib import contextmanager from pathlib import Path from typing import Iterator, IO, Optional +from enum import Enum try: @@ -17,6 +19,17 @@ from .encoding import UTF8 from .utils import repr_dict +class Levels(str, Enum): + WARNING = 'warning' + ERROR = 'error' + + +DISPLAY_THRESHOLDS = { + Levels.WARNING: 2, + Levels.ERROR: float('inf'), # Never hide errors. +} + + class Environment: """ Information about the execution context @@ -87,6 +100,8 @@ class Environment: self.stdout_encoding = getattr( actual_stdout, 'encoding', None) or UTF8 + self.quiet = kwargs.pop('quiet', 0) + def __str__(self): defaults = dict(type(self).__dict__) actual = dict(defaults) @@ -134,6 +149,14 @@ class Environment: self.stdout = original_stdout self.stderr = original_stderr - def log_error(self, msg, level='error'): - assert level in ['error', 'warning'] - self._orig_stderr.write(f'\n{self.program_name}: {level}: {msg}\n\n') + def log_error(self, msg: str, level: Levels = Levels.ERROR) -> None: + if self.stdout_isatty and self.quiet >= DISPLAY_THRESHOLDS[level]: + stderr = self.stderr # Not directly /dev/null, since stderr might be mocked + else: + stderr = self._orig_stderr + + stderr.write(f'\n{self.program_name}: {level}: {msg}\n\n') + + def apply_warnings_filter(self) -> None: + if self.quiet >= DISPLAY_THRESHOLDS[Levels.WARNING]: + warnings.simplefilter("ignore") diff --git a/httpie/core.py b/httpie/core.py index 079de17d..3dbed195 100644 --- a/httpie/core.py +++ b/httpie/core.py @@ -13,7 +13,7 @@ from . import __version__ as httpie_version from .cli.constants import OUT_REQ_BODY from .cli.nested_json import HTTPieSyntaxError from .client import collect_messages -from .context import Environment +from .context import Environment, Levels from .downloads import Downloader from .models import ( RequestsMessageKind, @@ -221,7 +221,7 @@ def program(args: argparse.Namespace, env: Environment) -> ExitStatus: if args.check_status or downloader: exit_status = http_status_to_exit_status(http_status=message.status_code, follow=args.follow) if exit_status != ExitStatus.SUCCESS and (not env.stdout_isatty or args.quiet == 1): - env.log_error(f'HTTP {message.raw.status} {message.raw.reason}', level='warning') + env.log_error(f'HTTP {message.raw.status} {message.raw.reason}', level=Levels.WARNING) write_message(requests_message=message, env=env, args=args, output_options=output_options._replace( body=do_write_body )) diff --git a/tests/test_output.py b/tests/test_output.py index 716ceb09..470673bf 100644 --- a/tests/test_output.py +++ b/tests/test_output.py @@ -5,6 +5,7 @@ from unittest import mock import json import os import io +import warnings from urllib.request import urlopen import pytest @@ -90,6 +91,31 @@ class TestQuietFlag: ) assert 'http: warning: HTTP 500' in r.stderr + @mock.patch('httpie.core.program') + @pytest.mark.parametrize('flags, expected_warnings', [ + ([], 1), + (['-q'], 1), + (['-qq'], 0), + ]) + def test_quiet_on_python_warnings(self, test_patch, httpbin, flags, expected_warnings): + def warn_and_run(*args, **kwargs): + warnings.warn('warning!!') + return ExitStatus.SUCCESS + + test_patch.side_effect = warn_and_run + with pytest.warns(None) as record: + http(*flags, httpbin + '/get') + + assert len(record) == expected_warnings + + def test_double_quiet_on_error(self, httpbin): + r = http( + '-qq', '--check-status', '$$$this.does.not.exist$$$', + tolerate_error_exit_status=True, + ) + assert not r + assert 'Couldn’t resolve the given hostname' in r.stderr + @pytest.mark.parametrize('quiet_flags', QUIET_SCENARIOS) @mock.patch('httpie.cli.argtypes.AuthCredentials._getpass', new=lambda self, prompt: 'password') diff --git a/tests/utils/__init__.py b/tests/utils/__init__.py index 2bd376ee..cf90d684 100644 --- a/tests/utils/__init__.py +++ b/tests/utils/__init__.py @@ -5,6 +5,7 @@ import sys import time import json import tempfile +import warnings from io import BytesIO from pathlib import Path from typing import Any, Optional, Union, List, Iterable @@ -96,6 +97,7 @@ class MockEnvironment(Environment): def cleanup(self): self.stdout.close() self.stderr.close() + warnings.resetwarnings() if self._delete_config_dir: assert self._temp_dir in self.config_dir.parents from shutil import rmtree