Привет, Хабр! Стоит ли говорить, что Python ОЧЕНЬ и ОЧЕНЬ популярный язык программирования, местами даже догоняя JavaScript. Python в мире программирования — это эсперанто, легкий язык созданный для всех, но его владельцам не мешало бы помыться.
В мире программирования создание собственных библиотек — это не просто возможность пополнения своего портфолио или способ структурировать код, а настоящий акт творческого самовыражения (и иногда велосипедостроения). Каждый разработчик иногда использовал в нескольких своих проектах однообразный код, который приходилось каждый раз перемещать. Да и хотя бы как упаковать свои идеи и знания в удобный и доступный формат, которым можно будет поделиться с сообществом.
Если вы ловили себя на мысли: «А почему мне бы не создать свою полноценную библиотеку?», то я рекомендую прочитать вам мою статью.
Эту статью вы можете использовать как шпаргалку для создания своих python-библиотек. Я полностью расскажу все этапы создания библиотеки: документация, тестирование, архитектура, публикация и управление зависимостями
Некоторые из вас могут подумать что мы изобретаем велосипед. А я в ответ скажу — сможете ли вы прямо сейчас, без подсказок, только по памяти, нарисовать велосипед без ошибок?
Итак, как обычно начинается создание проектов на python? Банально создание виртуального окружения
python3 -m venv venv source venv/bin/activate
Но в этом проекте я решил отойти от такого способа, и использовать вместо этого систему правлению проектами Poetry. Poetry — это инструмент для управления зависимостями и сборкой пакетов в Python. А также при помощи Poetry очень легко опубликовать свою библиотеку на PyPi!
В Poetry представлен полный набор инструментов, которые могут понадобиться для детерминированного управления проектами на Python. В том числе, сборка пакетов, поддержка разных версий языка, тестирование и развертывание проектов.
Все началось с того, что создателю Poetry Себастьену Юстасу потребовался единый инструмент для управления проектами от начала до конца, надежный и интуитивно понятный, который бы мог использоваться и в рамках сообщества. Одного лишь менеджера зависимостей было недостаточно, чтобы управлять запуском тестов, процессом развертывания и всем созависимым окружением. Этот функционал находится за гранью возможностей обычных пакетных менеджеров, таких как Pip или Conda. Так появился Python Poetry.
Установить poetry можно через pipx: pipx install poetry
и через pip: pip install poetry --break-system-requirements
. Это установит poetry глобально во всю систему.
Итак, давайте создадим проект при помощи poetry и установим зависимости:
poetry new <имя_проекта> cd <имя_проекта> poetry shell poetry add asttokens executing colorama rich ruff loguru pygments
❯ Структура проекта
В этой статье я буду создавать библиотеку pycolor_palette-loguru — простой модуль для различных цветных сообшений и дебага.
Вы можете посмотреть репозиторий по ссылке.
Структуру проекта вы видите ниже:
. ├── docs/ ├── example.py ├── poetry.lock ├── pycolor_palette_loguru │ ├── __init__.py │ ├── logger │ │ ├── __init__.py │ │ ├── logger.py │ ├── paint.py │ └── pygments_colorschemes.py ├── pyproject.toml ├── README.md └── tests/
При использовании poetry, README.md, pyproject.toml, tests и директория вашей библиотеки (в моем случае pycolor_palette_loguru) будут созданы сами. poetry.lock — лок файл, также создается poetry.
Директория docs нужна для документации, tests для тестов.
Полная структура кода (из корня репозитория):
. ├── Doxyfile ├── LICENSE ├── pycolor-palette │ ├── docs │ │ └── ru │ │ └── article.md │ ├── example.py │ ├── poetry.lock │ ├── pycolor_palette_loguru │ │ ├── __init__.py │ │ ├── logger │ │ │ ├── __init__.py │ │ │ ├── logger.py │ │ │ └── __pycache__ │ │ │ ├── __init__.cpython-312.pyc │ │ │ └── logger.cpython-312.pyc │ │ ├── paint.py │ │ ├── __pycache__ │ │ │ ├── __init__.cpython-312.pyc │ │ │ ├── paint.cpython-312.pyc │ │ │ └── pygments_colorschemes.cpython-312.pyc │ │ └── pygments_colorschemes.py │ ├── pyproject.toml │ ├── README.md │ └── tests │ └── __init__.py └── README.md
Итак, начнем работу над проектом с документации.
❯ Создание документации при помощи Doxygen
В этом разделе я расскажу о системе документирования исходных текстов Doxygen, которая на сегодняшний день, по имеющему основания заявлению разработчиков, стала фактически стандартом для документирования программного обеспечения, написанного на языке python, а также получила пусть и менее широкое распространение и среди ряда других языков.
Устанавливается Doxygen просто:
sudo pacman -S doxygen # Arch sudo apt install doxygen # Ubuntu/Debian
Суть автоматизированного софта для генерации документации такая: на вход подаются файлы исходного кода, комментированные особым образом, а на выходе мы получаем структурированный формат документации.
Рассматриваемая система Doxygen как раз и выполняет эту задачу: она позволяет генерировать на основе исходного кода, содержащего комментарии специального вида, красивую и удобную документацию, содержащую в себе ссылки, диаграммы классов, вызовов и т.п. в различных форматах: HTML, LaTeX, CHM, RTF, PostScript, PDF, man-страницы.
В большинстве случаев Doxygen используется для документации программного обеспечения, написанного на языке C++, однако на самом деле данная система поддерживает гораздо большое число других языков: C, Objective-C, C#, PHP, Java, Python, IDL, Fortran, VHDL, Tcl, и частично D.
Итак, сначала нам нужно будет перейти в рабочую директорию и создать Doxyfile — файл конфигурации:
doxygen -g
В Doxyfile содержится краткое описание проекта, его версия и подобные вещи. Некоторые значения желательно сразу изменить.
Вот основные значения:
PROJECT_NAME = "Project Name" # Имя проекта PROJECT_NUMBER = 0.1.0 # Версия проекта PROJECT_BRIEF = "Yet another project" # Краткое описание проекта OUTPUT_DIRECTORY = docs # Куда складывать сгенерированную документацию OUTPUT_LANGUAGE = English # Язык документации GENERATE_LATEX = YES # Генерация LaTeX INPUT = src include # Директории, где искать файлы RECURSIVE = YES # Рекурсивный обход директорий USE_MATHJAX = YES # Использование mathjax (для latex в html)
-
PROJECT_NAME — название проекта.
-
PROJECT_NUMBER — версия проекта. Я придерживаюсь схемы «major.minor.patch».
-
PROJECT_BRIEF — краткое описание проекта.
-
OUTPUT_DIRECTORY — директория, куда будет записываться созданная документация.
-
OUTPUT_LANGUAGE — язык документации (доступные значения: Afrikaans, Arabic, Armenian, Brazilian, Bulgarian, Catalan, Chinese, Chinese-Traditional, Croatian, Czech, Danish, Dutch, English (United States), Esperanto, Farsi (Persian), Finnish, French, German, Greek, Hindi, Hungarian, Indonesian, Italian, Japanese, Japanese-en (Japanese with English messages), Korean, Korean-en (Korean with English messages), Latvian, Lithuanian, Macedonian, Norwegian, Persian (Farsi), Polish, Portuguese, Romanian, Russian, Serbian, Serbian-Cyrillic, Slovak, Slovene, Spanish, Swedish, Turkish, Ukrainian and Vietnamese).
-
GENERATE_LATEX — позволяет генерировать LaTeX.
-
INPUT — директории, откуда будет браться исходный код. Разделяются пробелами.
-
RECURSIVE — рекурсивный обход директорий.
-
USE_MATHJAX — для использования latex-формул в html.
Больше настроек вы можете посмотреть в этой статье.
Кастомизация
Дефолтный стиль, мягко говоря, некрасивый. Поэтому мы будем использовать кастомную css-тему:
HTML_STYLESHEET = ./docs/doxygen-styles.css # путь до css стилей
Данный файл стилей вы можете скачать отсюда.
Посмотреть, что получилось у меня, вы можете по ссылке. А мой Doxyfile здесь.
Форма написания комментариев
Документация кода в Doxygen осуществляется при помощи документирующего блока. При этом существует два подхода к его размещению. Он может быть размещен перед или после объявления или определения класса, члена класса, функции, пространства имён и т.д.
Для того, чтобы doxygen правильно создал документацию, стоит следовать стилистике написания комментариев. Рассмотрим пример:
def debug_message(text: str, highlight: bool=False) -> str: """ print debug message :param text: The text :type text: str :param highlight: The highlight :type highlight: bool :returns: message :rtype: str """ prefix = f'{BG.blue}{FG.black}' if highlight else f'{FG.blue}' message = '%s%-*s | %-*s%s ::: %s%s' % (prefix, 20, datetime.now().strftime('%Y-%m-%d %H:%M:%S'), 20, 'DEBUG', Style.reset, text, Style.reset) print(message)
-
@brief — краткое описание.
-
@details — детали, подробное описание.
-
@todo — что-то нужно доделать. Doxygen генерирует отдельную страницу со списком всех @todo.
-
@warning — предупреждение.
-
@ref — ссылка на связанный класс или метод.
-
@param — передаваемый параметр, имеет направление ([in], [out], [in,out]).
-
@return — возвращаемое значение.
Также мы можем использовать latex-формулы: чтобы обозначить ее, надо в начале и в конце вставить \f$
. Для создания latex-формул можно использовать онлайн редактор latex.
Также существуют следующие метки:
-
@authors — автор/ы;
-
@version — версия;
-
@date — дата;
-
@bug — известные ошибки;
-
@copyright — лицензия;
-
@example — файл примера работы;
-
@mainpage Title — комментарий содержит текст для титульного листа документации;
-
@file fname — описание конкретного файла;
-
@deprecated — помечает класс или метод устаревшим. Как и с @todo, Doxygen генерирует отдельную страницу со списком всех устаревших классов и методов.
В действительности, Doxygen поддерживает куда больше команд. Например, он позволяет писать многостраничную @pagee) документацию с разделами @sectionn) и подразделами @subsectionn), указывать версии методов и классов @versionn), и не только.
Больше можно прочитать здесь.
Деплой документации на github-pages
Для начала создадим репозиторий на GitHub. Откройте главную вкладку репозитория.
Перейдите на вкладку Settings и откройте раздел Pages:
В этом разделе выберете Static HTML и нажмите кнопку Configure.
Откроется файл конфигурации задачи для CI/CD пайплайна. Все настройки хранятся в static.yaml файле. С помощью пайплайна можно вызывать системные команды. Вызов всех команд описывается с помощью шагов. Описание шагов начинается после строки steps. Путь к этой строке: jobs -> deploy -> steps.
Первые два шага по умолчанию выглядят так:
При обновлении GitHub Pages может что-то поменяться, но я не думаю, что будет сложно понять, где надо писать свои шаги.
Теперь требуется добавить шаги с установкой Doxygen. В качестве системы используется Ubuntu, а значит пакеты устанавливаются через apt.
# Install Doxygen - name: Install Doxygen run: sudo apt install doxygen && doxygen --version # Create documentation - name: Create documentation run: doxygen
Документация создается, но надо ее развернуть. Для этого необходимо указать путь к папке с index.html в шаге Upload artifact. Путь к главной странице сайта: ./html/index.html. Тогда этот шаг будет выглядеть так:
Я же указал в конфигурации Doxyfile, что документация сохраняется в docs, поэтому я указываю путь ./docs/html/index.html.
На этом настройка закончена. Ссылка на документацию находится в раздел Settings -> Pages. То есть <username>.github.io/<reponame>
.
P.S. инструкция взята с этой статье
❯ Тестирование
Не секрет, что разработчики создают программы, которые рано или поздно становятся очень масштабными (если смотреть на количество строчек кода). А с этим приходит и большая ответственность за качество.
В Python есть несколько библиотек для тестирования. В этой статье мы рассмотрим unittest и pytest.
Начнем с unittest, потому что именно с нее многие знакомятся с миром тестирования. Причина проста: библиотека по умолчанию встроена в стандартную библиотеку языка Python.
По формату написания тестов она сильно напоминает библиотеку JUnit, используемую в языке Java для написания тестов:
-
тесты должны быть написаны в классе;
-
класс должен быть наследован от базового класса unittest.TestCase;
-
имена всех функций, являющихся тестами, должны начинаться с ключевого слова test;
-
внутри функций должны быть вызовы операторов сравнения (assertX) — именно они будут проверять наши полученные значения на соответствие заявленным.
-
Является частью стандартной библиотеки языка Python: не нужно устанавливать ничего дополнительно;
-
Гибкая структура и условия запуска тестов. Для каждого теста можно назначить теги, в соответствии с которыми будем запускаться либо одна, либо другая группа тестов;
-
Быстрая генерация отчетов о проведенном тестировании, как в формате plaintext, так и в формате XML.
-
Для проведения тестирования придётся написать достаточно большое количество кода (по сравнению с другими библиотеками);
-
Из-за того, что разработчики вдохновлялись форматом библиотеки JUnit, названия основных функций написаны в стиле camelCase (например setUp и assertEqual). В языке python согласно рекомендациям pep8 должен использоваться формат названий snake_case (например set_up и assert_equal).
Давайте я покажу код:
from math import sqrt import unittest def square_it_up(num: float) -> float: """ square num :param num: The number :type num: float :returns: value :rtype: float """ return num ** 2 def square_eq_solver(a, b, c): """ решение квадратных уравнений :param a: a :type a: int/float :param b: b :type b: int/float :param c: c :type c: int/float :returns: roots :rtype: list """ result = [] discriminant = b * b - 4 * a * c if discriminant == 0: result.append(-b / (2 * a)) elif discriminant > 0: result.append((-b + sqrt(discriminant)) / (2 * a)) result.append((-b - sqrt(discriminant)) / (2 * a)) return result class BasicTestCase(unittest.TestCase): def test_square_it_up(self): res = square_it_up(10) res2 = square_it_up(2) self.assertEqual(res, 100) self.assertEqual(res2, 4) class SquareEqSolverTestCase(unittest.TestCase): def test_no_root(self): res = square_eq_solver(10, 0, 2) self.assertEqual(len(res), 0) def test_single_root(self): res = square_eq_solver(10, 0, 0) self.assertEqual(len(res), 1) self.assertEqual(res, [0]) def test_multiple_root(self): res = square_eq_solver(2, 5, -3) self.assertEqual(len(res), 2) self.assertEqual(res, [0.5, -3])
Мы создали функции для возведения в квадрат и решения квадратного уравнения. Они как раз и будут тестироваться
Мы создаем классы Test Cases, которые наследуются от unittest.TestCase. Мы берем какое-либо значение с заданными параметрами и проверяем, совпадает ли оно с требованиями через функцию assertEqual().
Давайте запустим тесты через команду:
python3 -m unittest tests/unittest_example.py .... ---------------------------------------------------------------------- Ran 4 tests in 0.005s OK
Отлично! Все получилось. Не будем задерживаться, перейдем к pytest. А документация по unittest доступна по ссылке.
Pytest позволяет провести модульное тестирование (тестирование отдельных компонентов программы), функциональное тестирование (тестирование способности кода удовлетворять бизнес-требования), тестирование API (application programming interface) и многое другое.
Установить его при помощи poetry просто:
poetry add pytest poetry install # если нужна установка
И его намного проще использовать чем unittest.
Рассмотрим наш прошлый пример, но уже с применением pytest:
from math import sqrt def square_it_up(num: float) -> float: """ square num :param num: The number :type num: float :returns: value :rtype: float """ return num ** 2 def square_eq_solver(a, b, c): """ решение квадратных уравнений :param a: a :type a: int/float :param b: b :type b: int/float :param c: c :type c: int/float :returns: roots :rtype: list """ result = [] discriminant = b * b - 4 * a * c if discriminant == 0: result.append(-b / (2 * a)) elif discriminant > 0: result.append((-b + sqrt(discriminant)) / (2 * a)) result.append((-b - sqrt(discriminant)) / (2 * a)) return result def test_square_it_up(): assert square_it_up(10) == 100 def test_no_root(): assert len(square_eq_solver(10, 0, 2)) == 0 def test_single_root(): assert len(square_eq_solver(10, 0, 0)) == 1 def test_multiple_root(): assert square_eq_solver(2, 5, -3) == [0.5, -3]
Мы просто используем ключевое слово assert.
Запуск тестов также как и у unittest:
pytest tests/pytest_example.py ============================================================================= test session starts ============================================================================== platform linux -- Python 3.12.6, pytest-8.3.3, pluggy-1.5.0 rootdir: /home/alexeev/Desktop/Projects/pycolor-palette/pycolor-palette configfile: pyproject.toml collected 4 items tests/pytest_example.py .... [100%] ============================================================================== 4 passed in 0.13s ===============================================================================
Вот и все, что я хотел сказать о тестировании. Быстро, но базу рассказал. Документация по pytest доступна по ссылке.
❯ Пишем код
Как я уже говорил, моя библиотека-пример — это будет небольшой модуль для дебага и логгирования. Я брал icecream source code и использую loguru для логгинга.
В итоге, у нас получится такая библиотека (ниже пример кода):
#!venv/bin/python3 """pycolor_palette Example File. Copyright Alexeev Bronislav (C) 2024 """ from loguru import logger from pycolor_palette_loguru.logger import PyDBG_Obj, benchmark, set_default_theme, debug_func, setup_logger from pycolor_palette_loguru.paint import info_message, warn_message, error_message, other_message, FG, Style, debug_message, run_exception from pycolor_palette_loguru.pygments_colorschemes import CatppuccinMocha set_default_theme(CatppuccinMocha) pydbg_obj = PyDBG_Obj() setup_logger('DEBUG') @benchmark @debug_func def debug_print() -> list: num = 12 float_int = 12.12 string = 'Hello' boolean = True list_array = [1, 2, 3, 'Hi', True, 12.2] dictionary = {1: "HELLO", 2: "WORLD"} pydbg_obj(num, float_int, string, boolean, list_array, dictionary) debug_print() logger.debug("This is debug!") logger.info("This is info!") logger.warning("This is warning!") logger.error("This is error!") # Simple messages info_message('INFORMATION') warn_message('WARNING') error_message('EXCEPTION') debug_message('DEBUG') other_message('SOME TEXT', 'OTHER') # Highlight bg info_message('Highlight INFORMATION', True) warn_message('Highlight WARNING', True) error_message('Highlight EXCEPTION', True) debug_message('Highlight DEBUG', True) other_message('Highlight SOME TEXT', 'OTHER', True) print(f'{FG.red}{Style.bold}BOLD RED{Style.reset}{Style.dim} example{Style.reset}') run_exception('EXCEPTION')
А также вы ее можете сами установить через pip:
pip3 install pycolor_palette-loguru
И вы также сможете сделать! Так что не буду медлить, начнем творить!
Цветные цвета
Итак, начнем с самого базового файла в нашей библиотеки — файле paint.py, которые отвечает за форматирование и цвет в терминале.
Вот сам код:
#!/usr/bin/python3 from datetime import datetime from sys import stdout, stdin from time import sleep import os def cls(): """ Clear screen (unix). """ os.system('clear') class FG: """ Foreground class. """ black = "\u001b[30m" red = "\u001b[31m" green = "\u001b[32m" yellow = "\u001b[33m" blue = "\u001b[34m" magenta = "\u001b[35m" cyan = "\u001b[36m" white = "\u001b[37m" @staticmethod def rgb(r: int, g: int, b: int) -> str: """ Function for convert rgb to ansi color code. :param r: red color :type r: int :param g: green color :type g: int :param b: blue color :type b: int :returns: color :rtype: str """ return f"\u001b[38;2;{r};{g};{b}m" class BG: """ Background class. """ black = "\u001b[40m" red = "\u001b[41m" green = "\u001b[42m" yellow = "\u001b[43m" blue = "\u001b[44m" magenta = "\u001b[45m" cyan = "\u001b[46m" white = "\u001b[47m" @staticmethod def rgb(r: int, g: int, b: int) -> str: """ Function for convert rgb to ansi color code. :param r: red color :type r: int :param g: green color :type g: int :param b: blue color :type b: int :returns: color :rtype: str """ return f"\u001b[48;2;{r};{g};{b}m" class Style: """ Style class. """ reset = "\u001b[0m" bold = "\u001b[1m" dim = "\u001b[2m" italic = "\u001b[3m" underline = "\u001b[4m" reverse = "\u001b[7m" clear = "\u001b[2J" clearline = "\u001b[2K" up = "\u001b[1A" down = "\u001b[1B" right = "\u001b[1C" left = "\u001b[1D" nextline = "\u001b[1E" prevline = "\u001b[1F" top = "\u001b[0;0H" @staticmethod def to(x, y): """ Move cursor to x, y. :param x: x :type x: int :param y: y :type y: int :returns: cursor :rtype: string """ return f"\u001b[{y};{x}H" @staticmethod def write(text="\n"): """ Print to stdout. :param text: The text :type text: str """ stdout.write(text) stdout.flush() @staticmethod def writew(text="\n", wait=0.01): """ Print (typewrite effect). :param text: The text :type text: str :param wait: The wait :type wait: float """ for char in text: stdout.write(char) stdout.flush() sleep(wait) @staticmethod def read(begin=""): """ Read input from keyboard. :param begin: The begin :type begin: str """ text = "" stdout.write(begin) stdout.flush() while True: char = ord(stdin.read(1)) if char == 3: return elif char in (10, 13): return text else: text += chr(char) @staticmethod def readw(begin="", wait=0.5): """ Read input with wait. :param begin: The begin :type begin: str :param wait: The wait :type wait: float """ text = "" for char in begin: stdout.write(char) stdout.flush() sleep(wait) while True: char = ord(stdin.read(1)) if char == 3: return elif char in (10, 13): return text else: text += chr(char) def info_message(text: str, highlight: bool=False) -> str: """ print info message :param text: The text :type text: str :param highlight: The highlight :type highlight: bool :returns: message :rtype: str """ prefix = f'{BG.green}{FG.black}' if highlight else f'{FG.green}' message = '%s%-*s | %-*s%s ::: %s%s' % (prefix, 20, datetime.now().strftime('%Y-%m-%d %H:%M:%S'), 20, 'INFO', Style.reset, text, Style.reset) print(message) def warn_message(text: str, highlight: bool=False) -> str: """ print warn message :param text: The text :type text: str :param highlight: The highlight :type highlight: bool :returns: message :rtype: str """ prefix = f'{BG.yellow}{FG.black}' if highlight else f'{FG.yellow}' message = '%s%-*s | %-*s%s ::: %s%s' % (prefix, 20, datetime.now().strftime('%Y-%m-%d %H:%M:%S'), 20, 'WARNING', Style.reset, text, Style.reset) print(message) def error_message(text: str, highlight: bool=False) -> str: """ print error message :param text: The text :type text: str :param highlight: The highlight :type highlight: bool :returns: message :rtype: str """ prefix = f'{BG.red}{FG.black}' if highlight else f'{FG.red}' message = '%s%-*s | %-*s%s ::: %s%s' % (prefix, 20, datetime.now().strftime('%Y-%m-%d %H:%M:%S'), 20, 'ERROR', Style.reset, text, Style.reset) print(message) def debug_message(text: str, highlight: bool=False) -> str: """ print debug message :param text: The text :type text: str :param highlight: The highlight :type highlight: bool :returns: message :rtype: str """ prefix = f'{BG.blue}{FG.black}' if highlight else f'{FG.blue}' message = '%s%-*s | %-*s%s ::: %s%s' % (prefix, 20, datetime.now().strftime('%Y-%m-%d %H:%M:%S'), 20, 'DEBUG', Style.reset, text, Style.reset) print(message) def other_message(text: str, msg_type: str, highlight: bool=False) -> str: """ print message :param text: The text :type text: str :param highlight: The highlight :type highlight: bool :returns: message :rtype: str """ prefix = f'{BG.magenta}{FG.black}' if highlight else f'{FG.magenta}' message = '%s%-*s | %-*s%s ::: %s%s' % (prefix, 20, datetime.now().strftime('%Y-%m-%d %H:%M:%S'), 20, msg_type, Style.reset, text, Style.reset) print(message) def run_exception(text: str, highlight: bool=False): """ print and raise exception :param text: The text :type text: str :param highlight: The highlight :type highlight: bool :returns: message :rtype: str """ prefix = f'{BG.red}{FG.black}' if highlight else f'{FG.red}' message = '%s%s%-*s | %-*s ::: %s%s' % (Style.bold, prefix, 20, datetime.now().strftime('%Y-%m-%d %H:%M:%S'), 20, "EXCEPTION", text, Style.reset) print(message) raise Exception(text)
Классы FG и BG отображают цвет текста и цвет фона (с возможностью передать цвет через RGB), а вот Style поинтереснее — форматирование, чтение ввода и управление курсорам. Также мы имеем функции для вывода красивых дебаг-сообщений:
2024-10-01 00:38:42 | INFO ::: INFORMATION 2024-10-01 00:38:42 | WARNING ::: WARNING 2024-10-01 00:38:42 | ERROR ::: EXCEPTION 2024-10-01 00:38:42 | DEBUG ::: DEBUG 2024-10-01 00:38:42 | OTHER ::: SOME TEXT
Мы используем форматирование через % для выравнивания текста. В начале дата, потом тип сообщения, а в конце текст сообщения. А run_exception
вызывает исключение:
2024-10-01 00:38:42 | EXCEPTION ::: EXCEPTION Traceback (most recent call last): File "/home/alexeev/Desktop/Projects/pycolor-palette/pycolor-palette/example.py", line 50, in <module> run_exception('EXCEPTION') File "/home/alexeev/Desktop/Projects/pycolor-palette/pycolor-palette/pycolor_palette_loguru/paint.py", line 290, in run_exception raise Exception(text) Exception: EXCEPTION
Итак, начнем писать код цветовых схем для будущего класса PyDBG_OBJ (вдохновлен библиотекой icecream) — этот класс нужен для более красивого и понятного дебага:
pydbg_obj | num: 12 float_int: 12.12 string: 'Hello' boolean: True list_array: [1, 2, 3, 'Hi', True, 12.2] dictionary: {1: 'HELLO', 2: 'WORLD'}
Но он также имеет подсветку синтаксиса. Для этого я использую pygments, и в коде я реализовал три цветовых схемы — Solarized, Catppuccin и Gruvbox.
Сначала импортируем токены и класс стиля
from pygments.style import Style from pygments.token import ( Text, Name, Error, Other, String, Number, Keyword, Generic, Literal, Comment, Operator, Whitespace, Punctuation)
После создадим класс, наследуемый от Style:
class SolarizedDark(Style): """ This class describes a solarized dark colorscheme. """ BASE03 = '#002b36' # noqa BASE02 = '#073642' # noqa BASE01 = '#586e75' # noqa BASE00 = '#657b83' # noqa BASE0 = '#839496' # noqa BASE1 = '#93a1a1' # noqa BASE2 = '#eee8d5' # noqa BASE3 = '#fdf6e3' # noqa YELLOW = '#b58900' # noqa ORANGE = '#cb4b16' # noqa RED = '#dc322f' # noqa MAGENTA = '#d33682' # noqa VIOLET = '#6c71c4' # noqa BLUE = '#268bd2' # noqa CYAN = '#2aa198' # noqa GREEN = '#859900' # noqa styles = { Text: BASE0, Whitespace: BASE03, Error: RED, Other: BASE0, Name: BASE1, Name.Attribute: BASE0, Name.Builtin: BLUE, Name.Builtin.Pseudo: BLUE, Name.Class: BLUE, Name.Constant: YELLOW, Name.Decorator: ORANGE, Name.Entity: ORANGE, Name.Exception: ORANGE, Name.Function: BLUE, Name.Property: BLUE, Name.Label: BASE0, Name.Namespace: YELLOW, Name.Other: BASE0, Name.Tag: GREEN, Name.Variable: ORANGE, Name.Variable.Class: BLUE, Name.Variable.Global: BLUE, Name.Variable.Instance: BLUE, String: CYAN, String.Backtick: CYAN, String.Char: CYAN, String.Doc: CYAN, String.Double: CYAN, String.Escape: ORANGE, String.Heredoc: CYAN, String.Interpol: ORANGE, String.Other: CYAN, String.Regex: CYAN, String.Single: CYAN, String.Symbol: CYAN, Number: CYAN, Number.Float: CYAN, Number.Hex: CYAN, Number.Integer: CYAN, Number.Integer.Long: CYAN, Number.Oct: CYAN, Keyword: GREEN, Keyword.Constant: GREEN, Keyword.Declaration: GREEN, Keyword.Namespace: ORANGE, Keyword.Pseudo: ORANGE, Keyword.Reserved: GREEN, Keyword.Type: GREEN, Generic: BASE0, Generic.Deleted: BASE0, Generic.Emph: BASE0, Generic.Error: BASE0, Generic.Heading: BASE0, Generic.Inserted: BASE0, Generic.Output: BASE0, Generic.Prompt: BASE0, Generic.Strong: BASE0, Generic.Subheading: BASE0, Generic.Traceback: BASE0, Literal: BASE0, Literal.Date: BASE0, Comment: BASE01, Comment.Multiline: BASE01, Comment.Preproc: BASE01, Comment.Single: BASE01, Comment.Special: BASE01, Operator: BASE0, Operator.Word: GREEN, Punctuation: BASE0, }
Я не буду расписывать другие цветовые схемы и классы. Если надо, вы можете просмотреть их по этой ссылке.
Перейдем к самому главному — директории logger/. Создадим в ней сразу __init__.py
:
from pycolor_palette_loguru.logger.logger import PyDBG_Obj, set_default_theme, benchmark, debug_func, setup_logger __all__ = (set_default_theme, PyDBG_Obj, debug_func, benchmark, setup_logger)
И создадим файл logger.py. Именно в этом файле и будет центральный функционал.
Сначала нужно импортировать все нужные модули и библиотеки:
from time import time import ast import inspect import pprint import sys import warnings from datetime import datetime import functools from contextlib import contextmanager from os.path import basename, realpath from textwrap import dedent import colorama import executing from pygments import highlight from pygments.formatters import Terminal256Formatter from pygments.lexers import PythonLexer as PyLexer, Python3Lexer as Py3Lexer from typing import Union, List import logging from loguru import logger # ❯ Написанные модули from pycolor_palette_loguru.paint import debug_message from pycolor_palette_loguru.pygments_colorschemes import *
Создадим несколько переменных и функцию для смены темы:
PYTHON2 = (sys.version_info[0] == 2) _absent = object() default_theme = Terminal256Formatter(style=CatppuccinMocha) def set_default_theme(theme): global default_theme default_theme = Terminal256Formatter(style=theme)
Функция set_default_theme позволяет задать новую тему подсветки кода.
Создадим класс и функцию, отвечающие за конфигурацию логгера loguru:
class InterceptHandler(logging.Handler): """ This class describes an intercept handler. """ def emit(self, record) -> None: """ Get corresponding Loguru level if it exists :param record: The record :type record: record :returns: None :rtype: None """ try: level = logger.level(record.levelname).name except ValueError: level = record.levelno frame, depth = logging.currentframe(), 2 while frame.f_code.co_filename == logging.__file__: frame = frame.f_back depth += 1 logger.opt(depth=depth, exception=record.exc_info).log(level, record.getMessage()) def setup_logger(level: Union[str, int] = 'DEBUG', ignored: List[str] = "") -> None: """ Setup logger :param level: The level :type level: str :param ignored: The ignored :type ignored: List[str] """ logging.basicConfig( handlers=[InterceptHandler()], level=logging.getLevelName(level) ) for ignore in ignored: logger.disable(ignore) logger.info('Logging is successfully configured')
Функция setup_logger настраивает логгер. Кстати, loguru пользоваться очень просто:
from loguru import logger logger.info("info message")
Это будет независимо от файла, и будет распространяться на всю сессию.
Напишем некоторые базовые функции, которые пригодятся при выводе сообщений:
@contextmanager def supportTerminalColorsInWindows(): """ Support terminal colors in Windows OS with colorama. """ colorama.init() yield colorama.deinit() def stderrPrint(*args): """ Print to stderr. :param args: The arguments :type args: list """ print(*args) def isLiteral(s): """ Check string if literal. :param s: string :type s: str :returns: True if the specified s is literal, False otherwise. :rtype: bool """ try: ast.literal_eval(s) except Exception: return False return True def bindStaticVariable(name, value): def decorator(fn): """ Wrapper :param fn: The function :type fn: Function """ setattr(fn, name, value) return fn return decorator @bindStaticVariable( 'lexer', PyLexer(ensurenl=False) if PYTHON2 else Py3Lexer(ensurenl=False)) def colorize(s): """ Colorize with pygments. :param s: string :type s: str :returns: highlighted :rtype: str """ self = colorize return highlight(s, self.lexer, default_theme)
Теперь настроим базовые константы:
DEFAULT_PREFIX = 'pydbg_obj | ' DEFAULT_LINE_WRAP_WIDTH = 80 # Characters. DEFAULT_CONTEXT_DELIMITER = '~ ' DEFAULT_OUTPUT_FUNCTION = colorized_stderr_print DEFAULT_ARG_TO_STRING_FUNCTION = pprint.pformat NO_SOURCE_AVAILABLE_WARNING_MESSAGE = ( 'Failed to access the underlying source code for analysis. Was PyDBG_Obj() ' 'invoked in a REPL (e.g. from the command line), a frozen application ' '(e.g. packaged with PyInstaller), or did the underlying source code ' 'change during execution?')
Напишем остальные вспомогательные функции:
def colorized_stderr_print(obj): """ Colorized stderr print. :param obj: The object :type obj: object """ for s in obj.split('; '): if not s.startswith(f'{DEFAULT_PREFIX} |'): s = f'{DEFAULT_PREFIX} | {s}' colored = colorize(s) with supportTerminalColorsInWindows(): stderrPrint(colored) def callOrValue(obj): """ Call or value. :param obj: The object :type obj: obj :returns: function :rtype: func """ return obj() if callable(obj) else obj class Source(executing.Source): """ Source. """ def get_text_with_indentation(self, node): """ Get text with indents. :param node: The node :type node: node asttokens :returns: The text with indentation. """ result = self.asttokens().get_text(node) if '\n' in result: result = ' ' * node.first_token.start[1] + result result = dedent(result) result = result.strip() return result def prefixLines(prefix, s, startAtLine=0): """ Prefix lines. :param prefix: The prefix :param s: { parameter_description } :param startAtLine: The start at line """ lines = s.splitlines() for i in range(startAtLine, len(lines)): lines[i] = prefix + lines[i] return lines def prefixFirstLineIndentRemaining(prefix, s): """ First line indent remaining prefix. :param prefix: The prefix :type prefix: prefix :param s: param :type s: type :returns: lines :rtype: list """ indent = ' ' * len(prefix) lines = prefixLines(indent, s, startAtLine=1) lines[0] = prefix + lines[0] return lines def formatPair(prefix, arg, value): """ Formatting pair. :param prefix: The prefix :param arg: The argument :param value: The value """ if arg is _absent: argLines = [] valuePrefix = prefix else: argLines = prefixFirstLineIndentRemaining(prefix, arg) valuePrefix = argLines[-1] + ': ' looksLikeAString = (value[0] + value[-1]) in ["''", '""'] if looksLikeAString: # Align the start of multiline strings. valueLines = prefixLines(' ', value, startAtLine=1) value = '\n'.join(valueLines) valueLines = prefixFirstLineIndentRemaining(valuePrefix, value) lines = argLines[:-1] + valueLines return '\n'.join(lines) def singledispatch(func): """ Single dispatch function. :param func: The function :type func: function :returns: func :rtype: func :raises NotImplementedError """ if "singledispatch" not in dir(functools): def unsupport_py2(*args, **kwargs): raise NotImplementedError( "functools.singledispatch is missing in " + sys.version ) func.register = func.unregister = unsupport_py2 return func func = functools.singledispatch(func) # add unregister based on https://stackoverflow.com/a/25951784 closure = dict(zip(func.register.__code__.co_freevars, func.register.__closure__)) registry = closure['registry'].cell_contents dispatch_cache = closure['dispatch_cache'].cell_contents def unregister(cls): del registry[cls] dispatch_cache.clear() func.unregister = unregister return func @singledispatch def argumentToString(obj): """ Convert argument to string. :param obj: The object :type obj: obj :returns: String representation of the argument. :rtype: string """ s = DEFAULT_ARG_TO_STRING_FUNCTION(obj) s = s.replace('\\n', '\n') # Preserve string newlines in output. return s
И создадим главный класс — PyDBG_Obj:
class PyDBG_Obj: """Advanced print for debuging. >>> pydbg_obj | num: 12 float_int: 12.12 string: 'Hello' boolean: True list_array: [1, 2, 3, 'Hi', True, 12.2] dictionary: {1: 'HELLO', 2: 'WORLD'} """ _pairDelimiter = '; ' lineWrapWidth = DEFAULT_LINE_WRAP_WIDTH contextDelimiter = DEFAULT_CONTEXT_DELIMITER def __init__(self, prefix=DEFAULT_PREFIX, outputFunction=DEFAULT_OUTPUT_FUNCTION, argToStringFunction=argumentToString, includeContext=False, contextAbsPath=False): """ Initialization. :param prefix: The prefix :type prefix: prefix :param outputFunction: The output function :type outputFunction: output function :param argToStringFunction: The argument to string function :type argToStringFunction: function :param includeContext: The include context :type includeContext: bool :param contextAbsPath: The context absolute path :type contextAbsPath: bool """ self.enabled = True self.prefix = prefix self.includeContext = includeContext self.outputFunction = outputFunction self.argToStringFunction = argToStringFunction self.contextAbsPath = contextAbsPath def __call__(self, *args): """ Call magic method. :param args: The arguments :type args: list :returns: passthrough :rtype: list """ if self.enabled: callFrame = inspect.currentframe().f_back self.outputFunction(self._format(callFrame, *args)) if not args: passthrough = None elif len(args) == 1: passthrough = args[0] else: passthrough = args return passthrough def format(self, *args): """ Format arguments. :param args: The arguments :type args: list :returns: formatted out :rtype: call frame formatted """ callFrame = inspect.currentframe().f_back out = self._format(callFrame, *args) return out def _format(self, callFrame, *args): """ Format helper function. :param callFrame: The call frame :type callFrame: call frame :param args: The arguments :type args: list :returns: formatted :rtype: formatted out """ prefix = callOrValue(self.prefix) context = self._formatContext(callFrame) if not args: time = self._formatTime() out = prefix + context + time else: if not self.includeContext: context = '' out = self._formatArgs( callFrame, prefix, context, args) return out def _formatArgs(self, callFrame, prefix, context, args): """ Format arguments. :param callFrame: The call frame :type callFrame: call frame :param prefix: The prefix :type prefix: prefix :param context: The context :type context: content :param args: The arguments :type args: args :returns: formatted args :rtype: args """ callNode = Source.executing(callFrame).node if callNode is not None: source = Source.for_frame(callFrame) sanitizedArgStrs = [ source.get_text_with_indentation(arg) for arg in callNode.args] else: warnings.warn( NO_SOURCE_AVAILABLE_WARNING_MESSAGE, category=RuntimeWarning, stacklevel=4) sanitizedArgStrs = [_absent] * len(args) pairs = list(zip(sanitizedArgStrs, args)) out = self._constructArgumentOutput(prefix, context, pairs) return out def _constructArgumentOutput(self, prefix, context, pairs): """ Construct argument output. :param prefix: The prefix :type prefix: prefix :param context: context :type context: context :param pairs: The pairs :type pairs: pairs :returns: argument output :rtype: string """ def argPrefix(arg): return '%s: ' % arg pairs = [(arg, self.argToStringFunction(val)) for arg, val in pairs] pairStrs = [ val if (isLiteral(arg) or arg is _absent) else (argPrefix(arg) + val) for arg, val in pairs] allArgsOnOneLine = self._pairDelimiter.join(pairStrs) multilineArgs = len(allArgsOnOneLine.splitlines()) > 1 contextDelimiter = self.contextDelimiter if context else '' allPairs = prefix + context + contextDelimiter + allArgsOnOneLine firstLineTooLong = len(allPairs.splitlines()[0]) > self.lineWrapWidth if multilineArgs or firstLineTooLong: if context: lines = [prefix + context] + [ formatPair(len(prefix) * ' ', arg, value) for arg, value in pairs ] else: argLines = [ formatPair('', arg, value) for arg, value in pairs ] lines = prefixFirstLineIndentRemaining(prefix, '\n'.join(argLines)) else: lines = [prefix + context + contextDelimiter + allArgsOnOneLine] return '\n'.join(lines) def _formatContext(self, callFrame): """ Function for format call frame. :param callFrame: callframe :type callFrame: call frame :returns: context :rtype: string """ filename, lineNumber, parentFunction = self._getContext(callFrame) if parentFunction != '<module>': parentFunction = '%s()' % parentFunction context = f'{filename}:{lineNumber} in {parentFunction}' return context def _formatTime(self): """ Function for format time. :returns: format time :rtype: str """ now = datetime.now() formatted = now.strftime('%H:%M:%S.%f')[:-3] return ' at %s' % formatted def _getContext(self, callFrame): """ Get context of call frame. :param callFrame: The call frame :type callFrame: callFrame :returns: The context. :rtype: context """ frameInfo = inspect.getframeinfo(callFrame) lineNumber = frameInfo.lineno parentFunction = frameInfo.function filepath = (realpath if self.contextAbsPath else basename)(frameInfo.filename) return filepath, lineNumber, parentFunction def enable(self): """ Enable pydbg_obj. """ self.enabled = True def disable(self): """ Disable pydbg_obj. """ self.enabled = False def configureOutput(self, prefix=_absent, outputFunction=_absent, argToStringFunction=_absent, includeContext=_absent, contextAbsPath=_absent): """ Configure output of pydbg_obj. :param prefix: The prefix :type prefix: prefix :param outputFunction: The output function :type outputFunction: output function :param argToStringFunction: The argument to string function :type argToStringFunction: arg to string function :param includeContext: The include context :type includeContext: include context :param contextAbsPath: The context absolute path :type contextAbsPath: context abs path :raises TypeError: no parameter provided """ noParameterProvided = all( v is _absent for k,v in locals().items() if k != 'self') if noParameterProvided: raise TypeError('configureOutput() missing at least one argument') if prefix is not _absent: self.prefix = prefix if outputFunction is not _absent: self.outputFunction = outputFunction if argToStringFunction is not _absent: self.argToStringFunction = argToStringFunction if includeContext is not _absent: self.includeContext = includeContext if contextAbsPath is not _absent: self.contextAbsPath = contextAbsPath
И напишем две функции-декоратора, первая отвечает за сообщение об исполнении функции, вторая нужна для высчитывания скорости выполнения функции:
def debug_func(func, *args, **kwargs): """Decorator for print info about function. Arguments: --------- + func - executed func """ def wrapper(): func(*args, **kwargs) message = f'debug @ Function {func.__name__}() executed at {datetime.now()}' debug_message(message, False) return wrapper def benchmark(func, *args, **kwargs): """Measuring the speed of function execution (decorator). Arguments: --------- + func - executed func """ start = time() def wrapper(): func(*args, **kwargs) end = time() total = round(end - start, 2) debug_message(f'benchmark {func} @ Execution function {func.__name__} time: {total} sec', True) return wrapper
И это весь код на данный момент.
В итоге структура проекта такова:
. ├── example.py ├── poetry.lock ├── pycolor_palette_loguru │ ├── __init__.py │ ├── logger │ │ ├── __init__.py │ │ ├── logger.py │ ├── paint.py │ └── pygments_colorschemes.py ├── pyproject.toml ├── README.md └── tests ├── pytest_example.py └── unittest_example.py
Ruff
Ruff — это новый быстроразвивающийся линтер Python-кода, призванный заменить flake8 и isort.
Основным преимуществом Ruff является его скорость: он в 10–100 раз быстрее аналогов (линтер написан на Rust).
Ruff может форматировать код, например, автоматически удалять неиспользуемые импорты. Сортировка и группировка строк импорта практически идентична isort.
Инструмент используется во многих популярных open-source проектах, таких как FastAPI и Pydantic.
Настройка Ruff осуществляется в файле pyproject.toml.
Для использования ruff как линтер можно использовать следующие команды:
ruff check # Lint all files in the current directory (and any subdirectories). ruff check path/to/code/ # Lint all files in `/path/to/code` (and any subdirectories). ruff check path/to/code/*.py # Lint all `.py` files in `/path/to/code`. ruff check path/to/code/to/file.py # Lint `file.py`. ruff check @arguments.txt # Lint using an input file, treating its contents as newline-delimited command-line arguments. ruff check . --fix # Lint all files in current directory and fix
А если как форматтер:
ruff format # Format all files in the current directory (and any subdirectories). ruff format path/to/code/ # Format all files in `/path/to/code` (and any subdirectories). ruff format path/to/code/*.py # Format all `.py` files in `/path/to/code`. ruff format path/to/code/to/file.py # Format `file.py`. ruff format @arguments.txt # Format using an input file, treating its contents as newline-delimited command-line arguments. ruff format . # Format all files in current directory
Для конфигурации ruff’а просто можно изменить файл pyproject.toml (созданный poetry):
# Exclude a variety of commonly ignored directories. exclude = [ ".bzr", ".direnv", ".eggs", ".git", ".git-rewrite", ".hg", ".ipynb_checkpoints", ".mypy_cache", ".nox", ".pants.d", ".pyenv", ".pytest_cache", ".pytype", ".ruff_cache", ".svn", ".tox", ".venv", ".vscode", "__pypackages__", "_build", "buck-out", "build", "dist", "node_modules", "site-packages", "venv", ] # Same as Black. line-length = 88 indent-width = 4 # Assume Python 3.8 target-version = "py38" [lint] # Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default. select = ["E4", "E7", "E9", "F"] ignore = [] # Allow fix for all enabled rules (when `--fix`) is provided. fixable = ["ALL"] unfixable = [] # Allow unused variables when underscore-prefixed. dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" [format] # Like Black, use double quotes for strings. quote-style = "double" # Like Black, indent with spaces, rather than tabs. indent-style = "space" # Like Black, respect magic trailing commas. skip-magic-trailing-comma = false # Like Black, automatically detect the appropriate line ending. line-ending = "auto"
Финальные штрихи
Создадим файл __init__.py
в директории модуля. Этот файл отвечает за инициализацию модуля.
from pycolor_palette_loguru.logger import PyDBG_Obj, benchmark, set_default_theme, debug_func, setup_logger from pycolor_palette_loguru.paint import info_message, warn_message, error_message, other_message, FG, Style, debug_message, run_exception, BG from pycolor_palette_loguru.pygments_colorschemes import CatppuccinMocha, SolarizedDark, GruvboxDark __all__ = (PyDBG_Obj, benchmark, set_default_theme, debug_func, setup_logger, info_message, error_message, other_message, FG, Style, BG, debug_message, run_exception, BG, CatppuccinMocha, SolarizedDark, GruvboxDark)
❯ Публикация на PyPi
PyPi — официальный репозиторий Python для загрузки и скачивания пакетов. Это официальный ресурс пакетов для третьих лиц, которым управляет Python Software Foundation. После публикации на PyPI пакеты становятся доступными для установки.
Итак, вам потребуется аккаунт на PyPi. Зарегистрироваться можно по этой ссылке.
Дальше вам нужно будет подключить 2FA для безопасности аккаунта:
Аутентификация с помощью токена — это рекомендуемый способ проверки учетной записи PyPI в командной строке. При этом вместо имени пользователя и пароля можно использовать автоматически сгенерированный токен. Токены можно добавлять и отзывать в любое время; с их помощью можно предоставлять доступ к отдельным частям вашей учетной записи. Это делает их безопасными и значительно уменьшает риск взлома. Теперь создадим новый API-токен для учетной записи, для этого перейдите в настройки учетной записи:
Прокрутите вниз и найдите раздел “API tokens”. Нажмите “Add API token”:
Теперь с помощью этого токена можно настроить свои учетные данные в Poetry для подготовки к публикации. Чтобы не добавлять свой API токен к каждой команде, которой он нужен в Poetry, мы сделаем это один раз с помощью команды config:
poetry config pypi-token.pypi your-api-token
Добавленный API токен будет использоваться как учетные данные. Poetry уведомит о том, что ваши учетные данные хранятся в простом текстовом файле. Если использовать обычное имя пользователя и пароль для учетных данных, то это будет небезопасно. Хранение токенов безопасно и удобно, так как они легко удаляются, обновляются и генерируются случайным образом. Но также можно вводить свой API токен вручную для каждой команды.
Далее нам нужно будет собрать и опубликовать пакет через команды:
poetry build poetry publish
Если на этапе публикации выяснилось, что имя проекта занято, то измените в файле pyproject.toml название проекта, а далее согласно ему измените директорию модуля, и заново запустите сборку и публикацию.
Вы можете просмотреть свои созданные проекты по ссылке.
В итоге, кстати, мой pyproject.toml получился такой:
[tool.poetry] name = "pycolor_palette-loguru" version = "0.1.2" description = "Python library for color beautiful output and logging" authors = ["Alexeev Bronislav <alexeev.dev@inbox.ru>"] readme = "README.md" [project] name = "pycolor_palette-loguru" description = "Python library for color beautiful output and logging" readme = "README.md" requires-python = ">=3.9" keywords = ["color", 'icecream', 'loguru', 'logging', 'pycolor', "palette"] license = {text = "MIT License"} dynamic = ["version"] [tool.poetry.dependencies] python = "^3.12" rich = "^13.8.1" ruff = "^0.6.8" loguru = "^0.7.2" pygments = "^2.18.0" colorama = "^0.4.6" executing = "^2.1.0" asttokens = "^2.4.1" pytest = "^8.3.3" [build-system] requires = ["poetry-core"] build-backend = "poetry.core.masonry.api" # Exclude a variety of commonly ignored directories. exclude = [ ".bzr", ".direnv", ".eggs", ".git", ".git-rewrite", ".hg", ".ipynb_checkpoints", ".mypy_cache", ".nox", ".pants.d", ".pyenv", ".pytest_cache", ".pytype", ".ruff_cache", ".svn", ".tox", ".venv", ".vscode", "__pypackages__", "_build", "buck-out", "build", "dist", "node_modules", "site-packages", "venv", ] # Same as Black. line-length = 88 indent-width = 4 # Assume Python 3.8 target-version = "py38" [lint] # Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default. select = ["E4", "E7", "E9", "F"] ignore = [] # Allow fix for all enabled rules (when `--fix`) is provided. fixable = ["ALL"] unfixable = [] # Allow unused variables when underscore-prefixed. dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" [format] # Like Black, use double quotes for strings. quote-style = "double" # Like Black, indent with spaces, rather than tabs. indent-style = "space" # Like Black, respect magic trailing commas. skip-magic-trailing-comma = false # Like Black, automatically detect the appropriate line ending. line-ending = "auto"
Заключение
Репозиторий исходного кода доступен по ссылке.
Буду рад, если вы присоединитесь к моему небольшому телеграм-блогу. Анонсы статей, новости из мира IT и полезные материалы для изучения программирования и смежных областей.
Надеюсь, вам понравилась данная статья.
📚 Читайте также:
-
Создаем свою библиотеку на C++ с тестированием, CMake и блекджеком;
-
Оптимизация парсера/компилятора при помощи дата-ориентированного проектирования: разбор кейса;
-
О ненадежных рассказчиках, апокрифистике и нестандартном взгляде на «Вархаммер».
Новости, обзоры продуктов и конкурсы от команды Timeweb.Cloud — в нашем Telegram-канале ↩
Перейти ↩
Источники
ссылка на оригинал статьи https://habr.com/ru/articles/847370/
Добавить комментарий