Находки в опенсорсе
Привет! Меня зовут Никита Соболев. Я занимаюсь опенсорс разработкой полный рабочий день. Тут я рассказываю про #python, #c, опенсорс и тд. Поддержать: https://boosty.to/sobolevn РКН: https://vk.cc/cOzn36 Связь: @sobolev_nikita
Показати більше📈 Аналітичний огляд Telegram-каналу Находки в опенсорсе
Канал Находки в опенсорсе (@opensource_findings) у мовному сегменті Англійська є активним учасником. На даний момент спільнота об'єднує 11 456 підписників, посідаючи 10 892 місце в категорії Технології та додатки та 57 444 місце у регіоні Росія.
📊 Показники аудиторії та динаміка
З моменту свого створення невідомо, проект продемонстрував стрімке зростання, зібравши аудиторію у 11 456 підписників.
За останніми даними від 05 червня, 2026, канал демонструє стабільну активність. Хоча за останні 30 днів спостерігається зміна кількості учасників на 109, а за останні 24 години на -2, загальне охоплення залишається високим.
- Статус верифікації: Не верифікований
- Рівень залученості (ER): Середній показник залученості аудиторії становить 62.64%. Протягом перших 24 годин після публікації контент зазвичай збирає 29.94% реакцій від загальної кількості підписників.
- Охоплення публікацій: В середньому кожен допис отримує 7 177 переглядів. Протягом першої доби публікація в середньому набирає 3 431 переглядів.
- Реакції та взаємодія: Аудиторія активно підтримує контент: середня кількість реакцій на один пост – 80.
- Тематичні інтереси: Контент зосереджений навколо ключових тем, таких як pyobject, питон, slots, gil, github.
📝 Опис та контентна політика
Автор описує ресурс як майданчик для висловлення суб'єктивної думки:
“Привет!
Меня зовут Никита Соболев. Я занимаюсь опенсорс разработкой полный рабочий день.
Тут я рассказываю про #python, #c, опенсорс и тд.
Поддержать: https://boosty.to/sobolevn
РКН: https://vk.cc/cOzn36
Связь: @sobolev_nikita”
Завдяки високій частоті оновлень (останні дані отримано 06 червня, 2026), канал підтримує актуальність та високий рівень охоплення публікацій. Аналітика показує, що аудиторія активно взаємодіє з контентом, що робить його важливою точкою впливу в категорії Технології та додатки.
lazy from os import path неправильную форму from os lazy import path. На что человек просто получит голый SyntaxError без подсказок и советов. Оно работает, но DX не самый лучший для новой фичи. Особенно, учитывая тот факт, что from os lazy import path выглядит консистентно с lazy import os.
И первая часть задачи у меня получилась прямо на стриме. Теперь from os lazy import path выдает красивую ошибку:
>>> from os lazy import path
File "<python-input-0>", line 1
from os lazy import path
^^^^
SyntaxError: use 'lazy from ... ' instead of 'from ... lazy import'
А вот часть с from . lazy import name у меня сразу не вышла. На стриме оч сложно программировать. Я, честно сказать, сначала растерялся. А потом понял: в питоне есть значимые пробелы: например для идентации кода. Они превращаются в токен INDENT.
А есть незначимые: a+b и a + b - одинаковый код. Что на самом деле ведет к чудовищам вида:
>>> 1. .real
1.0
>>> 1if True else 0
<python-input-2>:1: SyntaxWarning: invalid decimal literal
1
>>> [1.0for _ in range(1)]
<python-input-3>:1: SyntaxWarning: invalid decimal literal
[1.0]
И как вы уже могли догадаться: from . lazy import x и from .lazy import x - ОДИН И ТОТ ЖЕ КОД.
Более того, он абсолютно корректно работает. И жадно импортирует имя x из модуля lazy.
Что собственно и стало причиной, почему в PEP сделали lazy from, а не from ... lazy import.
Теперь я поправил свой PR, чтобы выкидывать еще один SyntaxWarning:
>>> from . lazy import x
<python-input-0>:1: SyntaxWarning: 'from . lazy import' is the same as 'from .lazy import'; did you mean 'lazy from . import'?
Кстати, тут можно сравнить мой код со слопусом. К вопросу о "качестве" ИИ-поделок.
Теперь оба случая ошибочного импорта обрабатываются корректно.
Про канал / стримы
После стрима случилось главное: мы почти 3 часа обсуждали, что хотим делать и какую ценность нести людям.
Мы поняли, что главная ценность, которую мы можем и хотим давать: помогать людям бороться со страхами. На каждом углу нас пытаются запсиопить тейками вроде "ИИ заменило всех программистов", "IT В С Е", "всех сократили", "работы нет" и прочее.
Кажется, что с таким нужно бороться рациональностью, взвешенной позицией, фактами и техническими контентом. То, что мы делаем и любим. Не хочется хайпить на страхах людей, хочется помогать людям быть счастливыми и уверенными. По мере сил, конечно.
Обсуждение: а какой контент хотелось бы увидеть вам? :)
| Поддержать | YouTube | GitHub | Чат |lazy import может быть использован только на уровне модуля, в других местах - он будет вызывать ошибку синтаксиса. Но, что забавно, lazy import не может быть использован внутри даже try блоков.
Во-вторых, я не уточнил, как будет работать __lazy_modules__, а там дичь.
Мы можем указывать __lazy_modules__ = ['os', 'typing'] в любой версии питона. Очевидно, что работать как ленивые они будут только в 3.15+, в остальных - будет просто неиспользуемый атрибут. Но штука в том, что в разных версиях питона библиотеки будут работать по-разному. Но! Он будет ленивым, только если он может быть ленивым. То есть, если он находится внутри класса, функции, try, тд - он не станет ленивым. Удачи в дебаге, короче.
Ну и самое забавное, мы можем управлять глобальным стейтом всех импортов через -X lazy_imports=none|normal|all и так же через переменную окружения PYTHON_LAZY_IMPORTS. Что оно значит? Отключаем все lazy импорты | все работает так, как написано | все импорты ленивые. Мы можем управлять тем, как работают импорты через переменную окружения!! Перечитайте, если вы тоже не поняли. Я вот не сразу понял.
Внедрение в питон
В stdlib питона уже активно используют lazy импорты. Однако, внутри уже появились циклические импорты. Потому что теперь так можно сделать случайно. И специально. Да, ленивые импорты могут помогать избегать циклических импортов. В некоторых режимах работы.
Теперь stdlib больше не работает в режиме -X lazy_imports=none.
О чем развернулась жаркая дискуссия, прочитать которую я всем советую: https://github.com/python/cpython/issues/149321
Но и режим -X lazy_imports=all все сломал. Теперь с ним некоторые библиотеки начали работать по-другому. Например:
$ PYTHON_LAZY_IMPORTS=normal ./python -c "import shutil; print(shutil._BZ2_SUPPORTED)"
False
$ PYTHON_LAZY_IMPORTS=all ./python -c "import shutil; print(shutil._BZ2_SUPPORTED)"
True
Так как импорт становится ленивым, он больше не проверяет есть ли на самом деле библиотека _bz2 у вас. А просто всегда возвращает True. Что делать - пока никто не знает: https://github.com/python/cpython/issues/150167
Вот в такой ситуации мы все оказались. Зато теперь некоторые скрипты будут запускаться быстрее, потому что импорты в некоторых местах стали ленивыми. Иногда, не точно. Почему нельзя было использовать импорт внутри функции? Я не знаю.
Мое отношение к данной фиче можно только охарактеризовать словами вечного инструмента wemake-python-styleguide: https://github.com/wemake-services/wemake-python-styleguide/issues/3639
Обсуждение: я даже не знаю, честно. Давайте просто обнимемся в комментах.
| Поддержать | YouTube | GitHub | Чат |ValueError: generator already executing.
Почему так? Нельзя дважды запустить один и тот же генератор, даже в одном треде.
Самый простой пример:
def g():
i = next(me)
yield i
me = g()
next(me) # ValueError
Исходник:
static PySendResult // Objects/genobject.c
gen_send_ex(PyGenObject *gen, PyObject *arg, PyObject **presult)
{
int8_t frame_state = FT_ATOMIC_LOAD_INT8_RELAXED(gen->gi_frame_state);
// ...
if (frame_state == FRAME_EXECUTING) {
PyErr_SetString(PyExc_ValueError, "generator already executing");
return PYGEN_ERROR;
}
// ...
}
Во-вторых
Переиспользовать генераторы в разных тредах - особенно плохая идея. О чем теперь явно написано в документации. Данный паттерн не поддерживается во Free-Threading нативно. Корневая причина, для самых любопытных: https://github.com/python/cpython/issues/120496
PR: https://github.com/python/cpython/pull/148894
Документация: https://docs.python.org/3.15/library/threading.html#iterator-synchronization
Был добавлен способ вызова __next__ под локом (как правильно догадались в комментариях):
class serialize_iterator:
def __init__(self, iterable):
self._iterator = iter(iterable)
self._lock = Lock()
def __iter__(self):
return self
def __next__(self):
with self._lock:
return next(self._iterator)
Для того, чтобы код из оригинального примера заработал, нужно заменить iterator = producer(limit) на iterator = threading.serialize_iterator(producer(limit)). Есть еще декоратор @synchronized_iterator для определения threadsafe генераторов сразу.
Теперь - должно быть понятно, почему такая фича была добавлена.
Было ли интересно поковырять? Было ли сложно? :)sum(range(limit)). Получится ли?
import threading
import time
from test.support import threading_helper
limit = 10_000
workers_count = 10
result = 0
result_lock = threading.Lock()
start = threading.Event()
def producer(limit):
for x in range(limit):
yield x
def consumer(iterator):
global result
start.wait()
total = 0
for x in iterator:
total += x
with result_lock:
result += total
iterator = producer(limit) # 🤔
workers = [
threading.Thread(target=consumer, args=(iterator,))
for _ in range(workers_count)
]
with threading_helper.wait_threads_exit():
for worker in workers:
worker.start()
for worker in workers:
# Wait for the worker thread to actually start.
while worker.ident is None:
time.sleep(0.1)
start.set()
for worker in workers:
worker.join()
Перед запуском подумайте сначала сами:
- Что вообще может произойти?
- Как поправить текущую ситуацию в теории?
- Как можно поменять код сейчас без каких-либо новых фичей, чтоб заработало?
- Что было бы идеально увидеть в качестве решения из коробки?
- Где бы вы хотели увидеть такое решение в модулях питона?
Ответ и ссылки будут вечером.
| Поддержать | YouTube | GitHub | Чат |cargo fmt, что делается буквально в каждом Rust проекте: https://github.com/oven-sh/bun/pull/30695
- Ревью не было, потому что внутри PRа +1 009 257, -4 024 и 6000+ коммитов
- unsafe в коде встречает 10487 раз (да, там много ffi, но все равно). Для сравнения в uv (кода правда меньше в 2 раза) - всего 73 раза
- "Скорость работы осталось такой же" - довольно странный тезис, учитывая что zig и rust оба генерят код через LLVM, часто практически идентичный, заслуги ИИ здесь нет
Выводы
- Прикольно, что такое вообще можно сделать
- Как теперь bun будет владеть своей базой кода, кто сможет в ней разобраться и что-то пофиксить - вопрос открытый
- Какой смысл во всем действии (кроме очевидного маркетинга) - вопрос открытый
- Брать ли теперь bun в прод? Конечно нет
Обсуждение: что вы думаете по данному вопросу? Стали бы использовать bun у себя в проекте в новом виде?
| Поддержать | YouTube | GitHub | Чат |--local-partial-types теперь всегда включен. Он нужен для корректной типизации типов в разных скоупах.
a = [] # Needs type annotation when using `local-partial-types`
def func() -> None:
a.append(1)
Включили --strict-bytes по-умолчанию. Раньше тип bytes разрешал передавать memoryview и bytearray. Теперь с новым поведением bytes разрешает только bytes, все остальные типы нужно указывать отдельно.
Теперь можно переопределять переменные, даже разных типов с --allow-redefinition
def foo(cond: bool) -> None:
if cond:
for x in ["a", "b"]:
# Type of "x" is "str" here
...
else:
for x in [1, 2]:
# Type of "x" is "int" here
...
Данная фича раньше была под флагом --allow-redefinition-new, а теперь включена по-умолчанию.
Самое интересное
Добавили --num-workers, который позволяет ускорить mypy кратно на больших кодовых базах. Я буду запускать mypy прямо на кодовой базе mypy (без mypyc, без кеша, но с orjson и `sqlite_cache`):
» rm -rf .mypy_cache && time mypy --config-file mypy_self_check.ini -p mypy -p mypyc --num-workers=8
7.090 total
Против режима с одним воркером (как было до 2.0):
» rm -rf .mypy_cache && time mypy --config-file mypy_self_check.ini -p mypy -p mypyc
25.335 total
А теперь еще убираем orjson и sqlite_cache:
» rm -rf .mypy_cache && time mypy --config-file mypy_self_check.ini -p mypy -p mypyc
28.108 total
Вот такой прирост производительности. Версия с mypyc (то есть та, которую мы скачиваем из pip) будет еще быстрее.
Очень радостно, что mypy становится быстрее. Дальнейшее развитие mypyc приведет к еще большему перфу. и не только mypy.
Обсуждение: Как быстро вы обновляете mypy на своих проектах? Насколько сурово настраиваете? Будет ли профит от нескольких воркеров?
| Поддержать | YouTube | GitHub | Чат |builtin – sentinel, чтобы создавать значения по-умолчанию.
Проблема достаточно понятная, например: нам нужно создать какое-то значение по-умолчанию, чтобы мы знали, что аргумент не был передан. Но None является валидным значением в нашей логике. Потому нужно создать новое особое "пустое" значение.
Данная логика встречается буквально везде:
• dataclasses
• django_modern_rest
• msgspec (тоже самое но на C)
Однако, теперь можно упростить АПИ для создания таких объектов до:
_SENTINEL_VALUE = sentinel('_SENTINEL_VALUE')
А вот пример PR, где в dataclasses уже используют новое АПИ: https://github.com/python/cpython/commit/16952218d0535904236e8a39851133688c9ce1f0
Как оно примерно внутри устроено
Как и все билтины, sentinel написан на C, его исходники вот тут: https://github.com/python/cpython/blob/main/Objects/sentinelobject.c
Но я приведу примерную версию на питоне (из PEP), чтобы было понятнее, как он работает внутри:
class sentinel:
__slots__ = ("__name__", "_module_name")
def __init_subclass__(cls):
raise TypeError("type 'sentinel' is not an acceptable base type")
def __init__(self, name, /):
if not isinstance(name, str):
raise TypeError("sentinel name must be a string")
self.__name__ = name
self._module_name = sys._getframemodulename(1)
@property
def __module__(self):
return self._module_name
def __repr__(self):
return self.__name__
def __reduce__(self):
return self.__name__
def __copy__(self):
return self
def __deepcopy__(self, memo):
return self
def __or__(self, other):
return typing.Union[self, other]
Хороший пример синглтона ^
Что здесь важно?
1. pickle должен корректно работать, для того имя sentinel('NAME') должно совпадать с именем объекта на уровне модуля: NAME =
2. Объект должны быть внешне иммутабельным
3. Копирование объекта должно возвращать тот же самый синглтон
Не только для Питона
Ну и конечно же: есть две новые функции в C-API для создания таких объектов в C-extensions (как msgspec например):
• PyObject *PySentinel_New(const char *name, const char *module_name) для создания
• bool PySentinel_Check(PyObject *obj) для проверки
Вот такая фича. Довольно просто, закрывает понятную проблему. Но не очень ясно, почему builtin.
Обсуждение: Приходилось ли пользоваться чем-то подобным? Какую реализацию синглтона в питоне вы считаете лучшей? Согласны с добавлением нового builtin?
| Поддержать | YouTube | GitHub | Чат |requests морально устарел 10 лет назад. На фоне умирающего HTTPX, у которого не было релиза больше года, и автор которого не хочет релизить новые версии и даже заблокировал возможность создавать новые задачи, автор Zapros попытался написать аналог, способный не только заменить HTTPX, но и предложить кучу новых интересных фич.
from zapros import AsyncClient
async def main() -> None:
async with AsyncClient() as client:
response = await client.get("https://httpbin.org/get")
print(response.status, response.json)
Главная особенность Zapros - его дизайн: вместо того чтобы зависеть от конкретных имплементаций транспортного уровня, Zapros работает с абстракциями, благодаря которым он может поддерживать:
- HTTP/1, HTTP/2 и HTTP/3 - независимость от транспортного уровня позволяет использовать интерфейс Zapros поверх любых транспортных реализаций.
- Rust - поддерживает транспортную реализацию поверх Rust-библиотеки reqwest
- Работа в браузере (через Pyodide) - ещё раз, транспортный уровень `Zapros`-а полностью независим от самого клиента, и из коробки поддерживает работу в браузере, используя fetch API.
Идея независимости от транспортного уровня появилась у автора во время работы над проектом httpx-aiohttp, который был создан, чтобы «спасти» HTTPX от багнутой реализации транспортного уровня, подменяя его на aiohttp. В итоге проект вырос в полноценную библиотеку, используемую в SDK от OpenAI и Anthropic.
Zapros имеет всего лишь 3 зависимости: h11, pywhatwgurl и typing-extensions. Поддерживает Python 3.10 и выше.
Уделяя особое внимание расширяемости, Zapros был спроектирован с удобным механизмом расширения клиента с помощью миддлварей. Из коробки идут миддлвари для:
- Моков - позволяет мокать запросы без необходимости в сторонних библиотеках.
- Кеширования - позволяет кешировать запросы в памяти или на диске (работает поверх библиотеки `hishel`).
- Ретраев - позволяет автоматически повторять запросы при неудаче с помощью настраиваемой логики.
- Кук - автоматически управляет куками.
- Кассет - позволяет записывать и воспроизводить HTTP-взаимодействия, что полезно для тестирования и отладки (аналог vcr).
- Редиректов - автоматически обрабатывает HTTP-редиректы согласно стандарту HTTP (RFC 9111).
from zapros import CacheMiddleware, Client, RetryMiddleware
with (
Client().wrap_with_middleware(
lambda next: RetryMiddleware(next) # wrap with the retry middleware
).wrap_with_middleware(
lambda next: CacheMiddleware(next) # wrap with the cache middleware
) as client
):
# automatically retries failed requests and caches responses
client.get("https://zapros.dev")
Zapros не принуждает использовать ни одну из данных миддлварей: сам класс клиента отвечает только за отправку HTTP-запросов, всё остальное — уже миддлвари, которые вы можете использовать по своему усмотрению. И хотя основные миддлвари написаны так, чтобы покрывать большинство случаев использования, вы можете использовать и свои кастомные решения.
Zapros поддерживает как синхронный, так и асинхронный интерфейс, и использует улучшенную версию механизма unasync, который используется в httpx для поддержки обоих интерфейсов.
Обсуждение: Каким HTTP клиентом пользуетесь вы? Какие у вас с ним проблемы? Чего не хватает? Какой Python HTTP клиент считаете лучшим на данный момент?ExceptionGroup для сортировки группы. Пример:
import asyncio
async def fetch_user(uid):
await asyncio.sleep(0.5)
raise ConnectionError(f"User service timeout for {uid}")
async def fetch_orders(uid):
await asyncio.sleep(0.1)
raise ValueError(f"Invalid user_id format: {uid}")
async def fetch_recommendations(uid):
await asyncio.sleep(2.3)
raise TimeoutError("Recommendation service timeout")
async def get_dashboard(uid):
results = await asyncio.gather(
fetch_user(uid),
fetch_orders(uid),
fetch_recommendations(uid),
return_exceptions=True,
)
errors = [r for r in results if isinstance(r, Exception)]
if errors:
raise ExceptionGroup("dashboard fetch failed", errors)
asyncio.run(get_dashboard("usr_12@34"))
С переменной окружения PYTHON_TRACEBACK_TIMESTAMPS=iso питон будет выводить:
+ Exception Group Traceback (most recent call last):
| File "service.py", line 26, in <module>
| asyncio.run(get_dashboard("usr_12@34"))
| ...
| File "service.py", line 24, in get_dashboard
| raise ExceptionGroup("dashboard fetch failed", errors)
| ExceptionGroup: dashboard fetch failed (3 sub-exceptions) <@2026-04-19T07:24:31.102431Z>
+-+---------------- 1 ----------------
| Traceback (most recent call last):
| File "service.py", line 5, in fetch_user
| raise ConnectionError(f"User service timeout for {uid}")
| ConnectionError: User service timeout for usr_12@34 <@2026-04-19T07:24:29.300461Z>
+---------------- 2 ----------------
| Traceback (most recent call last):
| File "service.py", line 9, in fetch_orders
| raise ValueError(f"Invalid user_id format: {uid}")
| ValueError: Invalid user_id format: usr_12@34 <@2026-04-19T07:24:28.899918Z>
+---------------- 3 ----------------
| Traceback (most recent call last):
| File "service.py", line 13, in fetch_recommendations
| raise TimeoutError("Recommendation service timeout")
| TimeoutError: Recommendation service timeout <@2026-04-19T07:24:31.102394Z>
+----------------------------------—
Обратная совместимость
Данная фича будет включаться только при использовании переменной окружения PYTHON_TRACEBACK_TIMESTAMPS или флага сборки питона -X traceback_timestamps=<format>.
Поддерживаемые форматы:
• ns для отображения таймстампов с точностью до наносекунд <@1776017178.687320256>
• iso для отображения datetime в iso формате
Так же будет добавлен новый атрибут BaseException.__timestamp_ns__, который будет хранить непосредственное время для отображения. Он будет записываться всегда и всегда в формате наносекунд. Но, значение будет не 0, только с проставленной конфигурацией:
>>> try:
... raise ValueError('demo')
... except ValueError as exc:
... saved = exc
>>> # Без флага
>>> saved.__timestamp_ns__
0
>>> # С флагом `PYTHON_TRACEBACK_TIMESTAMPS=iso`
>>> saved.__timestamp_ns__
1776935887649972000
Перф
Никаких значимых изменений замечено не было.
Оффтоп
Если вы думаете, какой пакет уже попал в awesome-python в секцию Web APIs для Django наравне с django-rest-framework, то да, можно поздравить :) И в awesome-django тоже 😊
Обсуждение: Какие у вас лучшие практики логгирования исключений? Как-то работаете с группами по-особому? Видите ли вы применение у себя в проекте?
| Поддержать | YouTube | GitHub | Чат |django-modern-rest@0.5.0 потребовалось компилировать части фреймворка с mypyc для получения перформанса на ровном месте - мне пришлось разобраться. Так давайте и вам расскажу.
Как оно работает?
Пакет - достаточно простой, обычная клиха (украл слово у @diementros). При запуске - указываем какой wheel нужно собрать. Например:
cibuildwheel --only cp313-macosx_arm64 --config-file pyproject.toml
Соберет вам текущий пакет для 3.13 и macos с arm64 архитектурой. А вот конфигурация:
[tool.cibuildwheel]
build = "cp3{11,12,13,14}-*"
build-frontend = "uv"
test-command = 'your_test_command'
Как будет проходить сборка? Полный лог: https://github.com/wemake-services/django-modern-rest/actions/runs/24023507014/job/70057082696#step:4:195
1. Сначала устанавливается нужный питон из готовых образов
2. Подготавливаем окружение
3. Запускаем build систему. Она берется из вашего pyproject.toml / setup.py
Например, у нас она выглядит так (мы используем uv, у которого нет родной билд системы для бинарных зависимостей, потому используем `hatch`):
[build-system]
requires = ["hatchling", "hatch", "hatch-fancy-pypi-readme"]
build-backend = "hatchling.build"
Вторая часть задачи: запустить сам mypyc на нужных файлах. К hatch есть плагинчик hatch-mypy. Его надо тоже настроить:
[tool.hatch.build.targets.wheel.hooks.mypyc]
enable-by-default = false
dependencies = ["hatch-mypyc", "mypy==1.19.1"]
include = ["dmr/_compiled"]
require-runtime-dependencies = true
Тут мы указываем: что билдим, что по-умолчанию билд с mypyc выключен, какие зависимости для билда нужны и что нужно поставить рантайм зависимости для билда. Билдить с mypyc будем только если есть специальный флаг:
[tool.cibuildwheel.environment]
HATCH_BUILD_HOOKS_ENABLE = "1"
Только когда он есть (или мы билдим с cibuildwheel`), то сборка пакета запустится. Такое нужно нам, чтобы иметь возможность делать нативные python-only сборки без .so` частей.
4. Запускаем тесты собранного wheel пакета с test-command, проверяем, что собранный пакет работает
5. Замеряем, что наши скомпилированные части реально стали работать быстрее
Готово!
Запускаем в CI
Последняя часть: нужно как-то запустить CI с 50+ разных вариантов конфигураций. cibuildwheel тут снова поможет. Он умеет выплевывать такие конфигурации для CI командой: CIBW_BUILD="cp313-*" cibuildwheel --print-build-identifiers --platform macos.
Далее дело техники, собираем матрицу всех задач для нужной CI и запускаем такую матрицу:
mypyc:
name: mypyc wheels ${{ matrix.only }}
needs: configure
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include: ${{ fromJson(needs.configure.outputs.include) }}
Самая хитрая часть тут в include: там мы как раз динамически подставляем конфигурации от cibuildwheel.
Получается удобно и довольно просто.
Последним шагом мы просто загружаем данные пакеты при release, используя PyPI Trusted Publisher.
И вот так - к вам приехал новый релиз django-modern-rest с опциональными бинарными частями для СКОРОСТИ: https://github.com/wemake-services/django-modern-rest/releases/tag/0.5.0
Анонс митапа в Нижнем
Кстати, у нас скоро будет PythoNN митап в Нижнем Новгороде со всеми вашими любимыми спикерами: @diementros @pymineral, а еще Роман Фролов и Михаил Васильев.
17 апреля, начало в 18:30.
Регистрация: https://pytho-nn.timepad.ru/event/3880099
Приезжайте, приходите. Будет много пива, настолок, разговоров про питон.
Обсуждение: чем вы билдите колеса на работе? Нужно ли вообще такое где-то, кроме опенсорса?
| Поддержать | YouTube | GitHub | Чат |AGENT.md
• Использовать последние SOTA модели
• Пользоваться скилами готовыми под нужные технологии
Еще я заметил, что некоторые языки подходят лучше, чем другие.
Пока остановился на Go. Язык очень приятно выглядит. Он простой, но выразительный.
Из-за его продвинутой статической типизации и универсальности писать на нем большие проекты будет очень удобно.
Как быстро меняется мир!
Обсуждение: В комментах посоветуйте своё любимое аниме? Смотреть нечего!
Хорошего праздника! #ironid: c435ff72
| Поддержать | YouTube | GitHub | Чат |0.4.0 https://github.com/wemake-services/django-modern-rest
И там мы выпустили поддержку tracecov. Инструмент новый, такого в других фреймворках я не видел.
В чем суть? Там мы считаем не "покрытие кода", а намного более важную метрику: "покрытие тестами нашего АПИ". Ну то есть буквально:
• Какие операции были вызваны?
• С какими телами и параметрами?
• Какие ответы получены по статусам?
• Какие схемы возвращены?
• Работают ли примеры из доки?
Так как мы используем очень строгую схему - у нас такой подход хорошо работает.
Мы интегрировали поддержку tracecov в наш dmr_client, который используется для всех интеграционных тестов. И schemathesis, который мы используем для property-based тестирования OpenAPI спецификации - тоже поддерживает такое.
Один запуск schemathesis позволяет добиться примерно 85+% покрытия всего АПИ. Вау! То есть: тесты можно почти не писать с таким походом.
В pyproject.toml можно добавить:
# Tracecov:
"--tracecov-format=text,html,markdown",
"--tracecov-fail-under-operations=100",
"--tracecov-fail-under-examples=100",
# TODO: set value to 100
"--tracecov-fail-under-parameters=90",
"--tracecov-fail-under-keywords=90",
"--tracecov-fail-under-responses=50",
И тогда тесты будут падать при низком покрытии АПИ. Вот куда можно развиваться, если у вас - как у нас - уже 100% обычного покрытия.
Одной строкой
• Добавили поддержку attrs для моделей
• Добавили msgpack как протокол для АПИ, он значительно быстрее json
• Добавили JsonLines для стриминга событий
• Переработали несколько апишек, стало значительно удобнее. Спасибо первым пользователям за обратную связь!
Обсуждение: Воспользовались бы такой метрикой? И какое покрытие вы считаете оптимальным? И почему 100%?
P.S. Выпустил большую статью про django-modern-rest на Хабру: https://habr.com/ru/articles/1017036 Если есть плюсики - буду очень благодарен за помощь в продвижении!
| Поддержать | YouTube | GitHub | Чат |RESULT = yield from EXPR в CPython.
_i = iter(EXPR)
try:
_y = next(_i)
except StopIteration as _e:
_r = _e.value
else:
while 1:
try:
_s = yield _y
except GeneratorExit as _e:
try:
_m = _i.close
except AttributeError:
pass
else:
_m()
raise _e
except BaseException as _e:
_x = sys.exc_info()
try:
_m = _i.throw
except AttributeError:
raise _e
else:
try:
_y = _m(*_x)
except StopIteration as _e:
_r = _e.value
break
else:
try:
if _s is None:
_y = next(_i)
else:
_y = _i.send(_s)
except StopIteration as _e:
_r = _e.value
break
RESULT = _r
Источник: https://peps.python.org/pep-0380
Никогда не спрашивайте такое на собесах, будьте людьми 🌚
Что будет тут?
async def agenerator():
yield 1
return 2
async def main():
result = async yield from agenerator()
assert result == 2
Страшно. Очень страшно.
P.S. Два поста в один день, когда такое было?!async yield from. И у меня есть много разных мыслей.
Во-первых, оно реально иногда удобно. Во-вторых, реально консистентно визуально и синтаксически с синхронными генераторами:
async def agenerator():
yield 1
yield 2
async def main():
async yield from agenerator()
С другой стороны: https://pyfound.blogspot.com/2024/06/python-language-summit-2024-limiting-yield-in-async-generators.html
> Guido van Rossum lamented that this was "yet another demonstration that async generators were a bridge too far. Could we have a simpler PEP that proposes to deprecate and eventually remove from the language asynchronous generators, just because they're a pain and tend to spawn more complexity".
> Zac had no objections to a PEP deprecating async generators¹. Zac continued, "while static analysis is helpful in some cases, there are inevitably cases that it misses which kept biting us... until we banned all async generators in our codebase".
И вроде бы на примере выше оно выглядит нормально. Но давайте чуть глубже посмотрим.
Кстати, недавно наш коллега – Сергей Мирянов – добавил секцию "Async generators best practices" в доку asyncio.
Всем советую: https://docs.python.org/3.15/library/asyncio-dev.html#asynchronous-generators-best-practices
Если вы можете найти в данном коде 4 ошибки, то можете не читать доку. Остальным обязательно.
import asyncio
work_done = False
async def cursor():
try:
yield 1
finally:
assert work_done
async def rows():
global work_done
try:
yield 2
finally:
await asyncio.sleep(0.1) # immitate some async work
work_done = True
async def main():
async for c in cursor():
async for r in rows():
break
break
asyncio.run(main())
Какие проблемы там подсвечены?
1. Явно использование aclosing(agen) контекста для закрытия AsyncGenerator, иначе может пропасть стадия "уборки за собой", а сам генератор может остаться живым
2. Порядок очистки ресурсов в асинхронных генераторах может быть не таким, как вы думаете
3. Запуск асинхронных генераторов без event loop - плохая идея
4. Итерация асинхронного генератора из двух разных тасок = ошибка
Так вот! Стоит ли углубляться туда?
Обсуждение: что вы думаете про асинхронные генераторы и их развитие? Можете ли честно сказать, что понимаете, как они работают? Можете найти баги с asyncio.CanceledError и очисткой состояния без запуска кода? Я - нет.
| Поддержать | YouTube | GitHub | Чат |django-channels (фу, Артём, без негатива)
– Поддержка других форматов стриминга кроме SSE, например JsonL
– Поддержка cattrs и adaptix
– Скилы для LLM для автоматизации перехода с django-rest-framework и django-ninja
– Поддержка ty
Благодарности
Данный проект не стал бы возможен без:
– Александра и Алексея – соавторов проекта, они затащили гигантский объем работы
– Виктора, кто сделал нам офигительную интерактивную доку!
– А так же 51 других контрибьюторов, кто внес неоценимый вклад в проект
Большое спасибо всем за помощь, обратную связи и поддержку, без вас – ничего бы не вышло.
Лучшее сообщество! 🫶
Ну а я – делаю небольшой перерыв, отдыхаю и работаю дальше!
Обсуждение: какие фичи вы бы хотели увидеть в дальнейших релизах?
P.S. Если у вас есть подкаст / канал / тд, и вы хотите поговорить со мной про веб фреймворки на питоне – пишите в личку! Сделаем интересное :)
| Поддержать | YouTube | GitHub | Чат |
import uuid
import msgspec
from dmr import Body, Controller
from dmr.plugins.msgspec import MsgspecSerializer
class UserCreateModel(msgspec.Struct):
email: str
class UserModel(UserCreateModel):
uid: uuid.UUID
class UserController(
Controller[MsgspecSerializer],
Body[UserCreateModel],
):
def post(self) -> UserModel:
return UserModel(uid=uuid.uuid4(), email=self.parsed_body.email)
Фичи
– Главная фича, которая вообще подтолкнула меня к такому проекту: инфраструктура Джанги. Тут есть буквально все пакеты на все случаи жизни. Но не было нормального REST фреймворка. В комментах я регулярно наблюдал, как люди ненавидят Джангу, но почти всегда говорят про DRF. Да, он был ужасен – то теперь он на свалке истории!
– Все существующие плагины к родной Джанге должны работать
– Официальная поддержка Джанго в одном файле, да, Джанга может быть настолько простой
– Работаем с любыми моделями: pydantic, msgspec, TypedDict, dataclass, тд. Сериализация и валидация не прибиты гвоздями. А значит можно выбирать сериализатор под контроллер. Где-то msgspec + TypedDict для скорости. Где-то pydantic для более широких возможностей валидации. Можно писать свои
– Скорость. Мы довольно быстрые. Самый быстрый Python фреймворк для REST в Django. По скорости можно сравнивать с FastAPI, мы всего лишь на 30% медленнее. Но у нас и Джанга вообще-то. Скорость будет улучшаться, есть разные интересные идеи
– Типизация: типизировано всё! Но самое важное, типизацию не пихают вам в лицо. Нет огромных и сложных типов. Все просто, надежно и удобно. Поддерживаем mypy, pyright, pyrefly в самых строгих вариантах
– Поддержка async везде. От вьюх и моделей до SSE. Никаких sync_to_async внутри
– SSE! Без дополнительных костылей: просто работает (с валидацией сообщений и возможностью строить бизнесовые ADT поверх типов сообщений и крутейшей схемой)
– Семантика. Одна из ключевых фичей: мы очень сильно упоролись по генерации схемы. Добавил auth= в контроллер? В списке ответов появился 401 статус код автоматически. Возвращаешь ответ, заголовок, куку, которой нет в спеке? Во время дебага – случится ошибка валидации. На проде валидацию нужно отключать для скорости. Так мы гарантируем точность ответов и схемы. Не нравится схема? Все легко переопределить или вообще отключить
– Swagger, Scalar, Redoc из коробки, легко настраивать
– Работаем не только с json, поддерживаем content negotiation, можно писать свои парсеры и рендереры
– JWT и DjangoSessionAuth из коробки, есть возможность отзыва токенов и сессий
– Возможность писать заготовки контроллеров и полностью переиспользовать код. Писать плагины под dmr будет просто и удобно
– Загрузка и отдача файлов (но на питоне такое очень осторожно надо делать, лучше на Rust)
– Нет привязки к логике или DI (берите любой, например dishka). Мы просто парсим данные и возвращаем их. То есть: код не превратится в кашу из логики и фреймворка уже через 10 бизнес фичей
– Удобная обработка ошибок на многих уровнях
– Полная возможность для кастомизации. Можно даже поменять формат внутренних ошибок в рамках контроллера
– Удобные тесты: polyfactory, pytest, schemathesis (проходим все правила из коробки)
– Скилы для LLM для написания кода по OpenAPI спеке, llms-full.txt, Context7 для контекста
– Но никакого нейрослопа внутри!
@dataclass
class User:
username: str
age: int
def get_field(obj: Any, field_name: str) -> Any:
return getattr(obj, field_name)
user = User('example', 18)
username = get_field(user, 'username')
# ^ мы знаем, что тут `str`, но никак не можем такое выразить, кроме КУЧИ `@overload` для конкретного типа
# а для общего случая - вообще никак
Никак, обидно. Я даже 100 лет назад делал такую поделку: https://github.com/wemake-services/mypy-extras
Чтобы хоть как-то решать проблему выше.
Предложение
И вот Юрий Селиванов (автор asyncio и edge-db) предлагает добавить в питон специальные действия над типами.
Чтобы было как в TS, где есть условные и рекурсивные типы, готовые операторы как keyof и куча дополнительных типов в npm.
Вот что предлагают добавить:
<type> = ...
# Type booleans are all valid types too
| <type-bool>
# Conditional types
| <type> if <type-bool> else <type>
# Types with variadic arguments can have
# *[... for t in ...] arguments
| <ident>[<variadic-type-arg> +]
# Type member access
| <type>.<name>
| GenericCallable[<type>, lambda <args>: <type>]
А еще:
– Типовые операторы: IsAssignable, IsEquivalent, GetArg, FromUnion, тд
– Методы для интроспекции объектов в типах: Members, Attrs, GetMember, тд
– Создание типов внутри аннотаций: NewProtocol, NewTypedDict
Пример
Показать детали работы всего я, конечно, не смогу. Но смогу показать один пример из ПЕПа.
Понятная проблема: есть какая-то модель пользователя. При создании данной модели - мы указываем все поля, кроме primary_key. Но показывать мы будем наружу все поля, кроме password.
Сейчас мы делаем что-то типа
class UserBase(SQLModel):
name: str = Field(index=True)
age: int | None = Field(default=None, index=True)
class User(HeroBase, table=True):
id: int | None = Field(default=None, primary_key=True)
password: str = Field(hidden=True)
class UserPublic(UserBase):
id: int
class UserCreate(UserBase):
password: str
Но, мы можем создавать такие модели при помощи типов.
Полный код: https://github.com/vercel/python-typemap/blob/main/tests/test_fastapilike_2.py
# Extract the default type from an Init field.
# If it is a Field, then we try pulling out the "default" field,
# otherwise we return the type itself.
type GetDefault[Init] = (
GetFieldItem[Init, Literal["default"]]
if typing.IsAssignable[Init, Field]
else Init
)
# Create takes everything but the primary key and preserves defaults
type Create[T] = typing.NewProtocol[
*[
typing.Member[
p.name,
p.type,
p.quals,
GetDefault[p.init],
]
for p in typing.Iter[typing.Attrs[T]]
if not typing.IsAssignable[
Literal[True],
GetFieldItem[p.init, Literal["primary_key"]],
]
]
]
Данная страшная конструкция будет спрятана внутри SQLModel, а мы будем писать просто:
UserCreate = Create[User]
А внутри уже:
– Полная типизация всех полей
– Новая корректная модель, которая всегда актуальна
Круто?
Мое мнение: в детали данного предложения я пока не вникал, но в целом - направление правильное.
Обсуждение: а что вы думаете про такое развитие типизации в питоне?
P.S. Из телеги и ютюба не перекатываемся. Рекламы на канале и так почти не было, для меня - мало что меняется.
Если вы хотите поддерживать мою работу в опенсорсе и контент без рекламы скам-курсов и вечных прогревов, то всегда можно закинуть на бусти: https://boosty.to/sobolevn
| Поддержать | YouTube | GitHub | Чат |
Вже доступно! Дослідження Telegram за 2025 — головні інсайти року 
