У меня есть приведенный выше вывод из pytest чтобы было легче найти интересующую информацию. Единственное, что я хотел бы сделать, это сделать имя файла и номер строки выделенными другим цветом.
У меня есть специальный модуль log_helper, который я использую:
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)
# 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
Я пробовал несколько разных подходов, но ничего не добился.
У меня есть приведенный выше вывод из pytest чтобы было легче найти интересующую информацию. Единственное, что я хотел бы сделать, это сделать имя файла и номер строки выделенными другим цветом. У меня есть специальный модуль log_helper, который я использую:[code]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))
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()
# 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) [/code] Я также использую файл pytest.ini: [code][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 [/code] Наконец, у меня есть файл conftest.py: [code]# 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'
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 [/code] Я пробовал несколько разных подходов, но ничего не добился.