Привет, Хабр! Стоит ли говорить, что 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-канале ↩
Перейти ↩
Источники
Комментарии (6)
TIEugene
23.10.2024 09:01Статья неплохая, но:
>#!venv/bin/python3
Прибивать гвоздями к venv - не очень хорошая идея
названия функций/методов таки лучше snake_case (согласно PEP)
прибивать гвоздями точные версии зависимостей тоже не очень хорошая идея; это мешает пакетированию и/или может вызвать конфликт
leshabirukov
23.10.2024 09:01В этом разделе я расскажу о системе документирования исходных текстов Doxygen, которая на сегодняшний день, по имеющему основания заявлению разработчиков, стала фактически стандартом для документирования программного обеспечения, написанного на языке python
Недавно изучал вопрос, и увидел совсем другое, - rst * sphinx * autodoc
Можно пруфы? Ниже мои.
Newest 'documentation+python' Questions - Stack Overflow
Related Tags ============ python-sphinx × 85 docstring × 50 python-3.x × 21 documentation-generation × 20 django × 19 autodoc × 17 doxygen × 15 restructuredtext × 15 pydoc × 14 pandas × 10 pdoc × 10 pycharm × 9
Repository search results (python+doxygen): 110 results
Repository search results (python+sphinx):1.3k results
В стане С++ ,- да, доксиджен рулит. У меня к сфинксу особых предпочтений нет, я вообще хотел asciidoc, но выбрал связку rst * sphinx * autodoc как дефолтную для питона.
DrArgentum Автор
23.10.2024 09:01По личным наблюдениям я доксиген наблюдаю довольно часто.
Но с вами согласен, да, немного не прав
dlinyj
Очень ценная и полезная статья.
Единственное пожелание, если код никак не относится к тексту, его либо сразу размещать на гите, либо в крайнем случае прятать под спойлер. Смысла вот в тексте код размещать нет, его я проматываю, чтобы перейти к другой части текста.
В остальном, большое спасибо за публикацию. Возьму на заметку.
DrArgentum Автор
Спасибо, учту!