From 55087a901e86eeda2a62e33765bb48299f004a78 Mon Sep 17 00:00:00 2001 From: Batuhan Taskaya Date: Mon, 7 Mar 2022 15:40:35 +0300 Subject: [PATCH] Introduce a mode to suppress all warnings (#1283) --- CHANGELOG.md | 2 +- httpie/cli/argparser.py | 2 ++ httpie/context.py | 29 ++++++++++++++++++++++++++--- httpie/core.py | 4 ++-- tests/test_output.py | 26 ++++++++++++++++++++++++++ tests/utils/__init__.py | 2 ++ 6 files changed, 59 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 026916d2f4..e74b331772 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 64481096c7..b1ab8de155 100644 --- a/httpie/cli/argparser.py +++ b/httpie/cli/argparser.py @@ -230,9 +230,11 @@ def _setup_standard_streams(self): 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 7a6e6a865c..50a8f772cd 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 .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 @@ def __init__(self, devnull=None, **kwargs): 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 @@ def as_silent(self) -> Iterator[None]: 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 079de17d28..3dbed19523 100644 --- a/httpie/core.py +++ b/httpie/core.py @@ -13,7 +13,7 @@ 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 request_body_read_callback(chunk: bytes): 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 716ceb097d..470673bf91 100644 --- a/tests/test_output.py +++ b/tests/test_output.py @@ -5,6 +5,7 @@ import json import os import io +import warnings from urllib.request import urlopen import pytest @@ -90,6 +91,31 @@ def test_quiet_quiet_with_check_status_non_zero_pipe(self, httpbin): ) 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 2bd376eef4..cf90d684b9 100644 --- a/tests/utils/__init__.py +++ b/tests/utils/__init__.py @@ -5,6 +5,7 @@ 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 @@ def create_temp_config_dir(self): 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