From d67332c2234e437113493bfdce37594c1e2ae417 Mon Sep 17 00:00:00 2001 From: Petr Stodulka Date: Wed, 3 Dec 2025 08:48:08 +0100 Subject: [PATCH 01/10] introduce the format_list function The construction of report messages involving lists (e.g., package names, file paths) lacked a consistent formatting which led to inconsistencies and redefinitions when creating reports. This patch introduces the format_list function to provide a consistent formatting option across all reports. The function is available from both leapp.libraries.stdlib and leapp.reporting. It supports custom sorting, item limits, and configurable separator. Jira-ref: RHEL-126447 --- docs/source/best-practices.md | 17 +++++++++++ leapp/libraries/stdlib/__init__.py | 36 +++++++++++++++++++++-- leapp/reporting/__init__.py | 5 +++- packaging/leapp.spec | 2 +- tests/scripts/test_format_list.py | 47 ++++++++++++++++++++++++++++++ 5 files changed, 103 insertions(+), 4 deletions(-) create mode 100644 tests/scripts/test_format_list.py diff --git a/docs/source/best-practices.md b/docs/source/best-practices.md index 11f861a..913dcbd 100644 --- a/docs/source/best-practices.md +++ b/docs/source/best-practices.md @@ -155,6 +155,23 @@ In case of [StopActorExecutionError](leapp.exceptions.StopActorExecutionError) t You can also use the [StopActorExecution](leapp.exceptions.StopActorExecution) and [StopActorExecutionError](leapp.exceptions.StopActorExecutionError) exceptions inside a private or shared library. +## Consistent list formatting in reports + +When constructing report messages that include lists of items (e.g. package names, file paths), use the `format_list` function to ensure consistent formatting across all reports. The function is available from both `leapp.libraries.stdlib` and `leapp.reporting`. It supports custom sorting, item limits, and configurable separators. + +```python +from leapp.reporting import format_list + +pkgs = ['kernel', 'bash', 'glibc'] +msg = 'The following packages will be removed:{}'.format(format_list(pkgs)) + +# Output: +#The following packages will be removed: +# - bash +# - glibc +# - kernel +``` + ## Use the LEAPP and LEAPP\_DEVEL prefixes for new envars In case you need to change a behaviour of actor(s) for testing or development purposes - e.g. be able to skip a functionality in your actor - use environment variables. Such environment variables should start with prefix *LEAPP\_DEVEL*. Such variables are not possible to use on production systems without special *LEAPP\_UNSUPPORTED* variable. This prevents users to break their systems by a mistake. diff --git a/leapp/libraries/stdlib/__init__.py b/leapp/libraries/stdlib/__init__.py index 89e59b2..efe2029 100644 --- a/leapp/libraries/stdlib/__init__.py +++ b/leapp/libraries/stdlib/__init__.py @@ -8,12 +8,15 @@ import logging import os import sys import uuid +from itertools import islice from leapp.exceptions import LeappError -from leapp.utils.audit import create_audit_entry from leapp.libraries.stdlib import api -from leapp.libraries.stdlib.call import _call, STDOUT +from leapp.libraries.stdlib.call import STDOUT, _call from leapp.libraries.stdlib.config import is_debug +from leapp.utils.audit import create_audit_entry + +FMT_LIST_SEPARATOR = '\n - ' class CalledProcessError(LeappError): @@ -214,3 +217,32 @@ def run(args, split=False, callback_raw=_console_logging_handler, callback_lineb ) api.current_logger().debug('External command has finished: {0}'.format(str(args))) return result + + +def format_list(data, sep=FMT_LIST_SEPARATOR, callback_sort=sorted, limit=0): + """ + Format an iterable into a string using a specified separator that is prepended to every item. + + This function can be used to consistently format lists in reports, logs, and error messages. + + :param data: Iterable of items to format. + :type data: Iterable + :param sep: Separator prepended to each item. Defaults to FMT_LIST_SEPARATOR. + :type sep: str + :param callback_sort: Callable returning a new list, called before the limit is applied. + Set to None to preserve original order. Defaults to sorted. + :type callback_sort: Callable or None + :param limit: Maximum number of items to include. Defaults to 0 (no limit). + :type limit: int + :returns: A string with each item prefixed by the specified separator. + :rtype: str + """ + items = data + if callback_sort is not None: + items = callback_sort(data) + + if limit > 0: + items = islice(items, limit) + + res = ['{}{}'.format(sep, item) for item in items] + return ''.join(res) diff --git a/leapp/reporting/__init__.py b/leapp/reporting/__init__.py index 7a0e223..34af17a 100644 --- a/leapp/reporting/__init__.py +++ b/leapp/reporting/__init__.py @@ -6,9 +6,12 @@ import os import six from leapp.compat import string_types +# NOTE(pstodulk): the format_list is imported to provide the function +# also in this library. Its use is not planned here however. +from leapp.libraries.stdlib import format_list +from leapp.libraries.stdlib.api import produce from leapp.models import fields, Model, ErrorModel from leapp.topics import ReportTopic -from leapp.libraries.stdlib.api import produce from leapp.utils.deprecation import deprecated diff --git a/packaging/leapp.spec b/packaging/leapp.spec index 1e32cf6..a06b141 100644 --- a/packaging/leapp.spec +++ b/packaging/leapp.spec @@ -13,7 +13,7 @@ # This is kind of help for more flexible development of leapp repository, # so people do not have to wait for new official release of leapp to ensure # it is installed/used the compatible one. -%global framework_version 6.2 +%global framework_version 6.3 # IMPORTANT: everytime the requirements are changed, increment number by one # - same for Provides in deps subpackage diff --git a/tests/scripts/test_format_list.py b/tests/scripts/test_format_list.py new file mode 100644 index 0000000..1b0ca24 --- /dev/null +++ b/tests/scripts/test_format_list.py @@ -0,0 +1,47 @@ +import pytest + +from leapp.libraries.stdlib import FMT_LIST_SEPARATOR, format_list + +SEP = ', ' + + +@pytest.mark.parametrize('data, kwargs, expected', [ + # Basic usage + ([], {}, ''), + (['c', 'a', 'b'], {}, '{0}a{0}b{0}c'.format(FMT_LIST_SEPARATOR)), + (['c', 'a', 'b'], {'sep': SEP}, ', a, b, c'), + (['a'], {'sep': SEP}, ', a'), + # Sorting + (['c', 'a', 'b'], {'sep': SEP, 'callback_sort': None}, ', c, a, b'), + (['c', 'a', 'b'], {'sep': SEP, 'callback_sort': lambda d: sorted(d, reverse=True)}, ', c, b, a'), + # Limit + (['c', 'a', 'b'], {'sep': SEP, 'limit': 2}, ', a, b'), + (['c', 'a', 'b'], {'sep': SEP, 'limit': 0}, ', a, b, c'), + (['b', 'a'], {'sep': SEP, 'limit': 10}, ', a, b'), + (['c', 'a', 'b'], {'sep': SEP, 'limit': -1}, ', a, b, c'), + # Non-list iterables + ({'a', 'b', 'c'}, {'sep': SEP, 'limit': 2}, ', a, b'), + (('a', 'b'), {'sep': SEP}, ', a, b'), + ({'b': 1, 'a': 2}, {'sep': SEP}, ', a, b'), + # Generators + ((x for x in ['c', 'a', 'b']), {'sep': SEP}, ', a, b, c'), + ((x for x in ['c', 'a', 'b']), {'sep': SEP, 'callback_sort': None, 'limit': 2}, ', c, a'), +], ids=[ + 'empty_data', + 'single_item', + 'default_separator', + 'custom_separator', + 'no_sort', + 'reverse_sort', + 'limit', + 'limit_zero', + 'limit_larger_than_data', + 'negative_limit_ignored', + 'set_input', + 'tuple_input', + 'dict_keys_input', + 'generator_sorted', + 'generator_unsorted_with_limit', +]) +def test_format_list(data, kwargs, expected): + assert format_list(data, **kwargs) == expected -- 2.53.0