Создание Python-библиотеки для перевода исключений на русский язык

от автора

Столкнувшись с исключением, иногда не понимаешь: «divizion by zero» — что это? Начинающие разработчики часто не могут понять причину ошибки из-за неправильного понимания её а русском языке. Опытные разработчики также сталкиваются с неизвестными им исключениями, а часто лезть в переводчик для понимания ошибки не хочется. Сегодня я напишу модуль для быстрого перевода таких ошибок, и все непойманные исключения и предупреждения в Python будут выводиться на русском языке.

Для начала установим библиотеку, которая будет обращаться к Google Translator для перевода всех ошибок и предупреждений в Python:

pip install deep_translator

Также нам потребуется работать с цветами для вывода ошибок, поэтому нам потребуется установить colorama. Вот команда:

pip install colorama

Теперь приступим к написанию кода! Я разбил код модуля на несколько файлов, каждый из которых направлен на выполнение совей задачи. Вот код:

error_translator.py:

import sysimport warningsimport osimport jsonimport timeimport refrom deep_translator import GoogleTranslatorSOURCE_LANG = 'auto'TARGET_LANG = 'ru'CACHE_DIR = os.path.join(os.path.dirname(__file__), 'error_cache')CACHE_FILE = os.path.join(CACHE_DIR, 'translations.json')MAX_CACHE_ENTRIES = 100000translator = GoogleTranslator(source=SOURCE_LANG, target=TARGET_LANG)def _load_cache():    if not os.path.exists(CACHE_FILE):        return {}    try:        with open(CACHE_FILE, 'r', encoding='utf-8') as f:            return json.load(f)    except:        return {}def _save_cache(cache):    os.makedirs(CACHE_DIR, exist_ok=True)    if len(cache) > MAX_CACHE_ENTRIES:        sorted_items = sorted(cache.items(), key=lambda x: x[1].get('timestamp', 0))        cache = dict(sorted_items[-MAX_CACHE_ENTRIES:])    with open(CACHE_FILE, 'w', encoding='utf-8') as f:        json.dump(cache, f, ensure_ascii=False, indent=2)def cached_translate(text):    if not text:        return text    cache = _load_cache()    if text in cache:        return cache[text]['translation']    try:        translated = translator.translate(text)    except Exception:        return text    cache[text] = {'translation': translated, 'timestamp': time.time()}    _save_cache(cache)    return translateddef translate_exc_message(msg):    cleaned = re.sub(r'0x[0-9a-fA-F]+', '...', msg)    return cached_translate(cleaned)original_excepthook = sys.excepthookdef translating_excepthook(exc_type, exc_value, exc_tb):    if exc_value is not None:        translated = translate_exc_message(str(exc_value))        try:            new_exc = exc_type(translated)        except Exception:            new_exc = exc_value    else:        new_exc = exc_value    original_excepthook(exc_type, new_exc, exc_tb)original_showwarning = warnings.showwarningdef translating_showwarning(message, category, filename, lineno, file=None, line=None):    translated = translate_exc_message(str(message))    original_showwarning(translated, category, filename, lineno, file, line)sys.excepthook = translating_excepthookwarnings.showwarning = translating_showwarning

run.py:

import sysimport subprocessimport reimport osfrom pathlib import Pathimport coloramacolorama.init()sys.path.insert(0, str(Path(__file__).parent))from error_translator import cached_translatesys.stdout.reconfigure(encoding='utf-8', errors='replace')sys.stderr.reconfigure(encoding='utf-8', errors='replace')RED = colorama.Fore.REDRESET = colorama.Style.RESET_ALLerror_header_pattern = re.compile(    r'^(Traceback \(most recent call last\):|Exception in|During handling of|The above exception was)')in_error_block = Falsepending_warning_source = Falsedef translate_line(line):    global in_error_block, pending_warning_source    if pending_warning_source:        pending_warning_source = False        if line.startswith('  ') or line.startswith('\t'):            return RED + line.rstrip('\n') + RESET + '\n'        else:            in_error_block = False            return line    if error_header_pattern.match(line):        in_error_block = True        return RED + line.rstrip('\n') + RESET + '\n'    if not in_error_block and line.startswith('  File '):        in_error_block = True        return RED + line.rstrip('\n') + RESET + '\n'    warning_match = re.search(r'(:\d+:)\s*(\w+Warning):\s*(.*)', line)    if warning_match:        if not in_error_block:            in_error_block = True        pending_warning_source = True        prefix = line[:warning_match.start(1)]        lineno = warning_match.group(1)        category = warning_match.group(2)        msg = warning_match.group(3)        if msg:            translated_msg = cached_translate(msg)            return f"{RED}{prefix}{lineno} {category}: {translated_msg}{RESET}\n"        else:            return RED + line.rstrip('\n') + RESET + '\n'    if in_error_block:        if line.startswith('  File ') or line.startswith('    ') or line.startswith('\t'):            return RED + line.rstrip('\n') + RESET + '\n'        m = re.match(r'^(\s*(?:\w+\.)*\w*(?:Error|Warning|Exception)):\s*(.*)', line)        if m:            exc_part, msg = m.groups()            if msg:                translated = f"{RED}{exc_part}: {cached_translate(msg)}{RESET}\n"            else:                translated = RED + line.rstrip('\n') + RESET + '\n'            in_error_block = False            return translated        if line.strip() == '':            return RED + '\n'        in_error_block = False        return line    return linedef run_target(target_script):    env = os.environ.copy()    env['PYTHONUNBUFFERED'] = '1'    p = subprocess.Popen(        [sys.executable, target_script],        stdout=subprocess.PIPE,        stderr=subprocess.STDOUT,        text=True,        encoding='utf-8',        errors='replace',        env=env,        bufsize=1    )    for line in p.stdout:        sys.stdout.write(translate_line(line))        sys.stdout.flush()    p.wait()    sys.exit(p.returncode)

translate_errors.py:

TARGET = "test.py"; from run import run_target; run_target(TARGET)

Работает это так: при запуске модуля translate_errors.py он запускает наш основной скрипт, название которого мы указали в константе TARGET в отдельном процессе. После выполнения скрипта программиста наш модуль проверяет каждую строчку вывода. Как только модуль натыкается на ошибку, он отправляет её в Google для перевода. Переведённый текст ошибки встаёт на место старого английского, после чего программа выводит переведённую на русский ошибку в консоль.

Пример вывода исключения в Python на русском языке

Пример вывода исключения в Python на русском языке

Не стоит забывать, что это работает не только с ошибками, но и с предупреждениями. Создадим скрипт test_warning.py и вставим приведённый ниже код, а затем запускаем translate_errors.py:

import warningswarnings.warn("This is a test warning")
Переведённое предупреждение

Переведённое предупреждение

В модуле также предусмотрено автоматические кэширование всех переведённых ошибок на диск (ограничение: до 100000 записей, однако его можно изменять в коде модуля). Для того, чтобы переводчик работал, нужно стабильное интернет-соединение. Если интернет будет отсутствовать, перевод выполняться не будет (за исключением случаев, когда ошибка уже записана на диск и берётся из кэшированных записей)!

Если что-то не будет работать, пишите в комментарии — разберёмся. Ниже можно пройти опрос. Он поможет найти баги в работе проекта и улучшить его для всех. По итогам опроса мы обновим код, исправив все баги и выполним предложения по улучшению. Надеюсь, эта статья была полезной!

ссылка на оригинал статьи https://habr.com/ru/articles/1032158/