You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何修改pytest输出中文件名与行号的显示颜色?

实现pytest日志中文件名和行号的颜色高亮

下面分三部分修改现有代码,覆盖自定义日志工具和pytest自带日志的颜色高亮需求:

一、修改自定义log_helper模块的格式化器

在RelativePathFormatter中添加颜色逻辑,给路径和行号加上高亮:

import logging
import os
import pprint

# 新增ANSI颜色常量
CYAN = '\033[96m'
RESET = '\033[0m'

# 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)
        self.base_path = base_path if base_path else os.path.expanduser('~/python')
    
    def format(self, record):
        # 先处理路径相对化
        if record.pathname.startswith(self.base_path):
            record.pathname = os.path.relpath(record.pathname, self.base_path)
        # 给路径和行号添加颜色
        colored_path = f"{CYAN}{record.pathname}:{record.lineno}{RESET}"
        # 临时替换格式字符串中的路径行号部分
        original_format = self._fmt
        self._fmt = original_format.replace("%(pathname)s:%(lineno)d", colored_path)
        # 执行格式化
        result = super().format(record)
        # 恢复原格式避免影响后续日志
        self._fmt = original_format
        return result

# 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)

二、修改conftest.py配置pytest自带日志

在pytest_configure钩子中直接修改日志格式,给路径行号添加颜色:

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

import time
from pathlib import Path
from collections import defaultdict

import pytest
import logging

# 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

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

    # 给log_cli格式中的路径和行号添加颜色
    config.option.log_cli_format = f'%(levelname)-8s {CYAN}%(pathname)s:%(lineno)d{RESET} - %(message)s'
    
    # 保留原有日志级别颜色设置
    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):
    # 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

三、修改pytest.ini(可选)

如果不想在conftest中硬编码格式,可直接修改pytest.ini的日志格式:

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

注意事项

  • 确保终端支持ANSI颜色代码(现代终端基本都支持,Windows需开启终端颜色功能)
  • 若需关闭颜色,可添加环境变量判断,比如检测到NO_COLOR存在时跳过颜色代码添加

内容的提问来源于stack exchange,提问作者StevieD

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.18 02:00:22