From a8c7661627131878a5d9234c4ab56df06f23be07 Mon Sep 17 00:00:00 2001 From: Daniel Mach Date: Wed, 24 Jan 2024 09:49:22 +0100 Subject: [PATCH 1/5] Implement 'quiet' conf option --- osc/commandline.py | 5 +++-- osc/conf.py | 14 ++++++++++++++ tests/test_conf.py | 4 ++++ 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/osc/commandline.py b/osc/commandline.py index 321bd4e0..68374ea0 100644 --- a/osc/commandline.py +++ b/osc/commandline.py @@ -344,13 +344,13 @@ class OscMainCommand(MainCommand): "-v", "--verbose", action="store_true", - help="increase verbosity", + help="increase verbosity (conflicts with --quiet)", ) self.add_argument( "-q", "--quiet", action="store_true", - help="be quiet, not verbose", + help="be quiet, not verbose (conflicts with --verbose)", ) self.add_argument( "--debug", @@ -434,6 +434,7 @@ class OscMainCommand(MainCommand): override_http_full_debug=args.http_full_debug, override_no_keyring=args.no_keyring, override_post_mortem=args.post_mortem, + override_quiet=args.quiet, override_traceback=args.traceback, override_verbose=args.verbose, overrides=overrides, diff --git a/osc/conf.py b/osc/conf.py index 4cbfd9ea..0d914da7 100644 --- a/osc/conf.py +++ b/osc/conf.py @@ -559,6 +559,16 @@ class Options(OscOptions): section=True, ) # type: ignore[assignment] + quiet: bool = Field( + default=False, + description=textwrap.dedent( + """ + Reduce amount of printed information to bare minimum. + Takes priority over ``verbose``. + """ + ), + ) # type: ignore[assignment] + verbose: bool = Field( default=False, description=textwrap.dedent( @@ -1782,6 +1792,7 @@ def get_config(override_conffile=None, override_http_full_debug=None, override_traceback=None, override_post_mortem=None, + override_quiet=None, override_no_keyring=None, override_verbose=None, overrides=None @@ -1822,6 +1833,9 @@ def get_config(override_conffile=None, if override_no_keyring is not None: overrides["use_keyring"] = not override_no_keyring + if override_quiet is not None: + overrides["quiet"] = override_quiet + if override_verbose is not None: overrides["verbose"] = override_verbose diff --git a/tests/test_conf.py b/tests/test_conf.py index 3569632f..bfe1c327 100644 --- a/tests/test_conf.py +++ b/tests/test_conf.py @@ -43,6 +43,7 @@ debug = 0 http_debug = 0 http_full_debug = 0 http_retries = 3 +quiet = 0 verbose = 0 no_preinstallimage = 0 traceback = 0 @@ -218,6 +219,9 @@ class TestExampleConfig(unittest.TestCase): def test_http_retries(self): self.assertEqual(self.config["http_retries"], 3) + def test_quiet(self): + self.assertEqual(self.config["quiet"], False) + def test_verbose(self): self.assertEqual(self.config["verbose"], False) From 8a38a9da8202fe4ca9e0dbb8f2e9bff6e64d4a8c Mon Sep 17 00:00:00 2001 From: Daniel Mach Date: Wed, 24 Jan 2024 09:49:23 +0100 Subject: [PATCH 2/5] Implement get_callback that allows modifying returned value to the Field class --- osc/util/models.py | 16 ++++++++++++++-- tests/test_models.py | 25 +++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/osc/util/models.py b/osc/util/models.py index 46f2f422..d0f8dfb0 100644 --- a/osc/util/models.py +++ b/osc/util/models.py @@ -10,6 +10,7 @@ import copy import inspect import sys import types +from typing import Callable from typing import get_type_hints # supported types @@ -76,6 +77,7 @@ class Field(property): default: Any = NotSet, description: Optional[str] = None, exclude: bool = False, + get_callback: Optional[Callable] = None, **extra, ): # the default value; it can be a factory function that is lazily evaluated on the first use @@ -106,6 +108,10 @@ class Field(property): # whether to exclude this field from export self.exclude = exclude + # optional callback to postprocess returned field value + # it takes (model_instance, value) and returns modified value + self.get_callback = get_callback + # extra fields self.extra = extra @@ -235,12 +241,18 @@ class Field(property): def get(self, obj): try: - return obj._values[self.name] + result = obj._values[self.name] + if self.get_callback is not None: + result = self.get_callback(obj, result) + return result except KeyError: pass try: - return obj._defaults[self.name] + result = obj._defaults[self.name] + if self.get_callback is not None: + result = self.get_callback(obj, result) + return result except KeyError: pass diff --git a/tests/test_models.py b/tests/test_models.py index add9bd55..df6e5125 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -291,6 +291,31 @@ class Test(unittest.TestCase): self.assertEqual(c.field, "new-text") self.assertEqual(c.field2, "text") + def test_get_callback(self): + class Model(BaseModel): + quiet: bool = Field( + default=False, + ) + verbose: bool = Field( + default=False, + # return False if ``quiet`` is True; return the actual value otherwise + get_callback=lambda obj, value: False if obj.quiet else value, + ) + + m = Model() + self.assertEqual(m.quiet, False) + self.assertEqual(m.verbose, False) + + m.quiet = True + m.verbose = True + self.assertEqual(m.quiet, True) + self.assertEqual(m.verbose, False) + + m.quiet = False + m.verbose = True + self.assertEqual(m.quiet, False) + self.assertEqual(m.verbose, True) + if __name__ == "__main__": unittest.main() From c7af0e458f118a47ea0de7c6afb1b038f493441e Mon Sep 17 00:00:00 2001 From: Daniel Mach Date: Wed, 24 Jan 2024 09:49:24 +0100 Subject: [PATCH 3/5] Use Field.get_callback to handle quiet/verbose and http_debug/http_full_debug options --- osc/conf.py | 8 ++++++-- tests/test_conf.py | 27 +++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/osc/conf.py b/osc/conf.py index 0d914da7..998ebae1 100644 --- a/osc/conf.py +++ b/osc/conf.py @@ -564,7 +564,7 @@ class Options(OscOptions): description=textwrap.dedent( """ Reduce amount of printed information to bare minimum. - Takes priority over ``verbose``. + If enabled, automatically sets ``verbose`` to ``False``. """ ), ) # type: ignore[assignment] @@ -574,8 +574,10 @@ class Options(OscOptions): description=textwrap.dedent( """ Increase amount of printed information to stdout. + Automatically set to ``False`` when ``quiet`` is enabled. """ ), + get_callback=lambda conf, value: False if conf.quiet else value, ) # type: ignore[assignment] debug: bool = Field( @@ -592,8 +594,10 @@ class Options(OscOptions): description=textwrap.dedent( """ Print HTTP traffic to stderr. + Automatically set to ``True`` when``http_full_debug`` is enabled. """ ), + get_callback=lambda conf, value: True if conf.http_full_debug else value, ) # type: ignore[assignment] http_full_debug: bool = Field( @@ -601,6 +605,7 @@ class Options(OscOptions): description=textwrap.dedent( """ [CAUTION!] Print HTTP traffic incl. authentication data to stderr. + If enabled, automatically sets ``http_debug`` to ``True``. """ ), ) # type: ignore[assignment] @@ -1821,7 +1826,6 @@ def get_config(override_conffile=None, overrides["http_debug"] = override_http_debug if override_http_full_debug is not None: - overrides["http_debug"] = override_http_full_debug or overrides["http_debug"] overrides["http_full_debug"] = override_http_full_debug if override_traceback is not None: diff --git a/tests/test_conf.py b/tests/test_conf.py index bfe1c327..0f4db601 100644 --- a/tests/test_conf.py +++ b/tests/test_conf.py @@ -434,6 +434,33 @@ class TestExampleConfig(unittest.TestCase): self.assertEqual(self.config["apiurl_aliases"], expected) +class TestOverrides(unittest.TestCase): + def test_verbose(self): + self.options = osc.conf.Options() + self.assertEqual(self.options.quiet, False) + self.assertEqual(self.options.verbose, False) + + self.options.quiet = True + self.options.verbose = True + self.assertEqual(self.options.quiet, True) + # ``verbose`` is forced to ``False`` by the ``quiet`` option + self.assertEqual(self.options.verbose, False) + + self.options.quiet = False + self.assertEqual(self.options.quiet, False) + self.assertEqual(self.options.verbose, True) + + def test_http_debug(self): + self.options = osc.conf.Options() + self.assertEqual(self.options.http_debug, False) + self.assertEqual(self.options.http_full_debug, False) + + self.options.http_full_debug = True + # ``http_debug`` forced to ``True`` by the ``http_full_debug`` option + self.assertEqual(self.options.http_debug, True) + self.assertEqual(self.options.http_full_debug, True) + + class TestFromParent(unittest.TestCase): def setUp(self): self.options = osc.conf.Options() From 7d6eebeabbcf4ce24b6b652486ce5f47e23903dc Mon Sep 17 00:00:00 2001 From: Daniel Mach Date: Wed, 24 Jan 2024 09:49:24 +0100 Subject: [PATCH 4/5] Refactor 'meter' module, use config settings to pick the right class --- osc/meter.py | 51 ++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 36 insertions(+), 15 deletions(-) diff --git a/osc/meter.py b/osc/meter.py index b0317770..8ea13b6d 100644 --- a/osc/meter.py +++ b/osc/meter.py @@ -5,6 +5,9 @@ import signal +from abc import ABC +from abc import abstractmethod +from typing import Optional try: import progressbar as pb @@ -13,9 +16,25 @@ except ImportError: have_pb_module = False -class PBTextMeter: +class TextMeterBase(ABC): + @abstractmethod + def start(self, basename: str, size: Optional[int] = None): + pass - def start(self, basename, size=None): + @abstractmethod + def update(self, amount_read: int): + pass + + @abstractmethod + def end(self): + pass + + +class PBTextMeter(TextMeterBase): + def __init__(self): + self.bar: pb.ProgressBar + + def start(self, basename: str, size: Optional[int] = None): if size is None: widgets = [f"{basename}: ", pb.AnimatedMarker(), ' ', pb.Timer()] self.bar = pb.ProgressBar(widgets=widgets, maxval=pb.UnknownLength) @@ -33,7 +52,7 @@ class PBTextMeter: signal.siginterrupt(signal.SIGWINCH, False) self.bar.start() - def update(self, amount_read): + def update(self, amount_read: int): self.bar.update(amount_read) def end(self): @@ -41,25 +60,27 @@ class PBTextMeter: class NoPBTextMeter: - def start(self, basename, size=None): + def start(self, basename: str, size: Optional[int] = None): pass - def update(self, *args, **kwargs): + def update(self, amount_read: int): pass - def end(self, *args, **kwargs): + def end(self): pass -def create_text_meter(*args, **kwargs): - use_pb_fallback = kwargs.pop('use_pb_fallback', True) - if have_pb_module or use_pb_fallback: - return TextMeter(*args, **kwargs) - return None +def create_text_meter(*args, **kwargs) -> TextMeterBase: + from .conf import config + + # this option is no longer used + kwargs.pop("use_pb_fallback", True) + + meter_class = PBTextMeter + if not have_pb_module or config.quiet or not config.show_download_progress: + meter_class = NoPBTextMeter + + return meter_class(*args, **kwargs) -if have_pb_module: - TextMeter = PBTextMeter -else: - TextMeter = NoPBTextMeter # vim: sw=4 et From 76a5432a7d1580f1f5b34959dc4f0f3b1f73d155 Mon Sep 17 00:00:00 2001 From: Daniel Mach Date: Wed, 24 Jan 2024 09:49:25 +0100 Subject: [PATCH 5/5] Don't show meter in terminals that are not interactive --- osc/meter.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osc/meter.py b/osc/meter.py index 8ea13b6d..b9d1b766 100644 --- a/osc/meter.py +++ b/osc/meter.py @@ -5,6 +5,7 @@ import signal +import sys from abc import ABC from abc import abstractmethod from typing import Optional @@ -77,7 +78,7 @@ def create_text_meter(*args, **kwargs) -> TextMeterBase: kwargs.pop("use_pb_fallback", True) meter_class = PBTextMeter - if not have_pb_module or config.quiet or not config.show_download_progress: + if not have_pb_module or config.quiet or not config.show_download_progress or not sys.stdout.isatty(): meter_class = NoPBTextMeter return meter_class(*args, **kwargs)