[Python] How can I change the color of the file name and line number in pytest output?

Discussão em 'Python' iniciado por Stack, Setembro 25, 2024.

  1. Stack

    Stack Membro Participativo

    [​IMG]

    I have the above output from pytest to make it easier to find info of interest. One thing I'd like to do is make he file name and line number pop out in a different color.

    I have a custom log_helper module I'm using:

    import logging
    import os
    import pprint

    # Initialize the logger at the module level, so it's only created once
    logger = logging.getLogger('debug_logger')

    # Set up the logger only if it hasn't been set up already
    if not logger.handlers:
    log_level = os.environ.get('LOG_LEVEL', 'WARNING').upper()
    logger.setLevel(getattr(logging, log_level, logging.WARNING))

    formatter = logging.Formatter('%(levelname)s: %(message)s')
    handler = logging.StreamHandler()
    handler.setFormatter(formatter)
    logger.addHandler(handler)

    class RelativePathFormatter(logging.Formatter):
    def __init__(self, *args, base_path=None, **kwargs):
    super().__init__(*args, **kwargs)
    # Define the base path (in your case ~/python)
    self.base_path = base_path if base_path else os.path.expanduser('~/python')

    def format(self, record):
    # Modify the pathname to be relative to the base path
    if record.pathname.startswith(self.base_path):
    record.pathname = os.path.relpath(record.pathname, self.base_path)
    return super().format(record)

    # Setup logger using the custom formatter
    def get_logger(name='debug_logger'):
    logger = logging.getLogger(name)

    # Remove existing handlers to avoid conflicts
    if logger.hasHandlers():
    logger.handlers.clear()

    log_level = os.environ.get('LOG_LEVEL', 'DEBUG').upper()
    logger.setLevel(getattr(logging, log_level, logging.DEBUG))

    # Apply custom formatter
    formatter = RelativePathFormatter(
    '%(levelname)-8s %(pathname)s:%(lineno)d - %(message)s',
    base_path=os.path.expanduser('~/python')
    )

    handler = logging.StreamHandler()
    handler.setFormatter(formatter)
    logger.addHandler(handler)

    return logger

    # Debug print function that respects log level and logs caller info
    def d(data, depth=None):
    """
    Debug print function that logs data in DEBUG mode with optional depth control for complex data structures.
    Shows the correct file and line number of the caller, not log_helper.py.

    :param data: The data to log (complex or simple).
    :param depth: Optional depth to limit the structure's representation, defaults to None.
    """
    logger = get_logger() # Get the global logger

    # Bail out immediately if the logger is not in DEBUG mode
    if not logger.isEnabledFor(logging.DEBUG):
    return

    # Format the data for debug output
    formatted_data = pprint.pformat(data, depth=depth)

    # Check if the formatted data has multiple lines
    if "\n" in formatted_data:
    # Add a newline before the data if it spans multiple lines
    formatted_data = "\n" + formatted_data

    # Adjust the stack level so the logger reports the caller's file and line number
    logger.debug(formatted_data, stacklevel=2)


    I'm also using a pytest.ini file:

    [pytest]
    # Include numbered files as test files
    python_files = test_*.py *_test.py [0-9]*-*.py 00-*_test
    log_cli_format = %(levelname)-8s %(pathname)s:%(lineno)d - %(message)s


    Finally, I have this conftest.py file:

    # File: ~/python/tests/conftest.py

    import time
    from pathlib import Path
    from collections import defaultdict

    import pytest

    # ANSI color codes
    CYAN = '\033[96m'
    YELLOW = '\033[93m'
    GREEN = '\033[92m'
    RED = '\033[91m'
    RESET = '\033[0m'

    test_results = {}
    test_structure = defaultdict(lambda: defaultdict(list))
    start_time = None

    import pytest # Make sure pytest is imported
    import logging

    @pytest.hookimpl(trylast=True)
    def pytest_configure(config):
    logging_plugin = config.pluginmanager.get_plugin("logging-plugin")

    # Change color on existing log level
    logging_plugin.log_cli_handler.formatter.add_color_level(logging.DEBUG, "blue")
    logging_plugin.log_cli_handler.formatter.add_color_level(logging.INFO, "cyan")

    def pytest_sessionstart(session):
    global start_time
    start_time = time.time()

    def pytest_collection_modifyitems(session, config, items):
    # for item in items:
    # path = Path(item.fspath).relative_to(Path(session.config.rootdir))
    # dir_path = path.parent
    # file_name = path.name
    # test_structure[dir_path][file_name].append(item)

    # Sort items based on their location in the file
    items.sort(key=lambda x: x.location[1])

    @pytest.hookimpl(tryfirst=True, hookwrapper=True)
    def pytest_runtest_makereport(item, call):
    outcome = yield
    result = outcome.get_result()

    if result.when == 'call':
    test_results[item.nodeid] = result.outcome

    def pytest_sessionfinish(session, exitstatus):
    global start_time
    duration = time.time() - start_time

    verbosity = session.config.option.verbose

    if verbosity > 1:
    print("\n")
    for dir_path in sorted(test_structure.keys()):
    print(f"\n{CYAN}{dir_path}{RESET}")
    for file_name in sorted(test_structure[dir_path].keys()):
    print(f" {YELLOW}{file_name}{RESET}")
    for item in test_structure[dir_path][file_name]:
    result = test_results.get(item.nodeid, "UNKNOWN")
    if result == "passed":
    color = GREEN
    elif result == "failed":
    color = RED
    else:
    color = YELLOW
    indent = " "
    if "::" in item.name:
    class_name, test_name = item.name.split("::")
    print(f" {class_name}")
    indent = " "
    else:
    test_name = item.name
    print(f"{indent}{test_name} {color}{result.upper()}{RESET}")

    # Print summary
    passed = sum(1 for result in test_results.values() if result == "passed")
    failed = sum(1 for result in test_results.values() if result == "failed")
    total = len(test_results)
    print(f"\n{GREEN if failed == 0 else RED}=== {passed} passed, {failed} failed, {total} total in {duration:.2f}s ==={RESET}\n")

    def pytest_terminal_summary(terminalreporter, exitstatus, config):
    # Completely override the default summary
    pass


    I've tried a few different approaches but haven't gotten anywhere.

    Continue reading...

Compartilhe esta Página