Находки в опенсорсе
10.7K subscribers
11 photos
1 video
3 files
819 links
Привет!

Меня зовут Никита Соболев. Я занимаюсь опенсорс разработкой полный рабочий день.

Тут я рассказываю про #python, #c, опенсорс и тд.
Поддержать: https://boosty.to/sobolevn
РКН: https://vk.cc/cOzn36

Связь: @sobolev_nikita
Download Telegram
Forwarded from Никита Соболев
Большая сходка любителей настолок, питона и пива в Москве!

Где? Ресторан Paulaner на Полянке: https://yandex.ru/maps/org/paulaner/44880575916/?ll=37.620383%2C55.734745&z=17.97
Когда? Четверг 24 октября с 18:30 и до закрытия

Что в планах?
- Игра в https://github.com/sobolevn/ship-it-boardgame 0.0.19й версии
- Разговоры про программирование

Ждем всех :)
24🔥10👍3💩1
Техническое объявление

Я научился делать открытые чаты для канала 😅
Теперь можно вступать в @opensource_findings_chat и общаться на темы: Python, опенсорса, программирования и всего такого :)
Не забывайте о правилах: https://gist.github.com/sobolevn/d9a598a23e6bb89e51ada71033e9103f

В связи с последними событиями, я продублировал и закрытый чат из Дискорда в ТГ. Всех, кто подписан на https://boosty.to/sobolevn должно было пригласить автоматически. Если будут проблемы - пишите в чате, решим.

А в рамках ЛКПП 11 уже началось голосование за новую тему выпуска для желающих: https://boosty.to/sobolevn/posts/127ef142-1864-48e1-b410-fe49409c3192
🔥21👍52👎1
Как ruff убил isort и поломал все мои проекты

Я пользовался isort сколько себя помню. Буквально с первых релизов, когда весь isort еще был написан в одном файле на много тысяч строк. Пользовался настолько активно, что у меня даже был свой --profile=wemake https://pycqa.github.io/isort/docs/configuration/profiles.html#wemake

Который включал:


[isort]
# profile = wemake
# =
multi_line_output = 3
include_trailing_comma = true
use_parentheses = true
line_length = 80


Где:
- multi_line_output указывает, как разбивать на новые строки длинные импорты. Демо тут: https://pycqa.github.io/isort/docs/configuration/multi_line_output_modes.html
- include_trailing_comma добавляет финальные запятые для уменьшения diff при добавлении новых имен в импорт
- use_parentheses для использования () вместо \ - опять же для уменьшения diff
- line_length - максимальный размер строки, кстати он ничего не имеет общего с размером ваших мониторов. потому что длина строки - метрика сложности кода. код на 160 символов - в два раза сложнее. подробности тут: https://sobolevn.me/2019/10/complexity-waterfall

Все было хорошо, все работало годами. И тут появляется ruff. Большинство меинтейнеров проектов, которые ruff "заменил" – выгорают от таких поворотов. И перестают заниматься своими проектами.

В целом - и норм, потому что оно уже работало. До одного очень странного случая.
Буквально один из последних коммитов в isort - сломал мой профиль. Пришел человек, кто неправильно понял суть line_length и поправил значение в --profile=wemake с 80 на 79 https://github.com/PyCQA/isort/pull/2183

Его PR без уточнений с моей стороны приняли. Релизнули новую версию isort.
И у меня на всех проектах начал отваливаться линтер. Говорит: неправильно ты импорты оформляешь. Я очень удивился.

Какое-то время у меня ушло на дебаг, потому что случай странный. В итоге я нашел баг, сделал свой PR: https://github.com/PyCQA/isort/pull/2241
И тут авторы окончательно выгорели. Больше уже никто ничего не мерджил.
Я писал письма им в личку, пинговал коллег по PyCQA, заходил к ним в дискорд. Тишина.

Какие у меня есть варианты?
- Везде явно ставить line_length = 80 в дополнение к --profile=wemake, что все еще ломает опыт всем пользователям https://github.com/wemake-services/wemake-python-styleguide
- Ставить прошлую версию isort, что тоже стремный хак

Ну и в ruff нет --profile https://docs.astral.sh/ruff/settings/#lintisort

Статический анализ – ад!
😢4627🤯14🤬5😁4👍3👎1🔥1🤔1
Нерегулярная воскресная рубрика про интересный опенсорс

Если у вас есть интересные опенсорсные проекты, про которые вы хотите рассказать, то пишите в чат.
С вас пост. С меня редактура и размещение. Давайте помогать друг другу!

А сегодняшний пост будет про очень прикольную библиотеку https://github.com/airtai/faststream от ее автора.

FastStream

Это современный фреймворк для разработки асинхронных сервисов поверх брокеров сообщений. Он взлетел за счет очень простого, интуитивного API:


from faststream import FastStream
from faststream.rabbit import RabbitBroker

broker = RabbitBroker()
app = FastStream(broker)

@broker.subscriber("in-queue")
@broker.publisher("out-queue")
async def handle_msg(user: str) -> str:
return f"User: {user} registered"


Но за простотой кроется достаточно интересное внутреннее устройство. Основная трудность, с которой борется FastStream (и почему у инструмента нет аналогов) - двухэтапная инициализация объектов. Это значит, что все вложенные объекты и данные, необходимые для функционирования "запчастей" не известны на момент их создания через __init__ и должны быть доставлены позже.

Как, например, в случае с мидлварями и декомпозицией приложения на отдельные router'ы


from faststream.nats import NatsBroker, NatsRouter
from faststream.nats.prometheus import NatsPrometheusMiddleware

router = NatsRouter()
publisher = router.publisher("out")

@router.subscriber("in")
async def handler(msg):
await publisher.publish("in")

broker = NatsBroker(middlewares=[NatsPrometheusMiddleware()])
broker.include_router(router)


На момент регистрации ни publisher, ни subscriber ничего не знают о своих будущих мидлварях. Они создаются позже, в брокере. Что значит: на момент включения router'а в брокер мы должны передать подобные зависимости в роутер. FastAPI, например, решает эту проблему путем создания новых эндпоинтов как копий из экземпляра router'a. Однако, тут данный подход не сработает - тот же publisher используется внутри кода обработчика. Пересоздавать объекты мы не можем - старая ссылка должна быть валидна.

Все объекты должны создаваться готовыми к использования настолько, насколько возможно, но также поддерживать операции переноса между различными контейнерами (роутеры / брокеры).

Более того, мидлвари и другие объекты, передаваемые в Broker.__init__ - самая безобидная часть айсберга. Большая часть объектов требует наличия реального объекта connection к брокеру (который появляется только после асинхронного await broker.start() ). Взглянем на небольшой кусочек кода aiokafka (используется внутри FastStream):


consumer = AIOKafkaConsumer(...)
await consumer.start()
async for msg in consumer:
...


Как мы видим, для чтения сообщений необходим объект Consumer'а (который и держит connection ). Соответственно, нам нобходимо доставить этот объект до subscriber'ов FastStream уже после запуска приложения.

При запуске брокера, мы должны рекурсивно пройтись по всему дереву вложенных объектов и проинициализаровать его повторно реальными объектами, необходимыми для функционирования компонентов. Все это развестистое дерево двухэтапной инициализации с необходимостью сохранения валидности всех ссылок на уже созданные объекты приводит к довольно сложной, но интересной внутренней структуре проекта.

И это только небольшая часть сложностей, с которой вынужден бороться фреймворк! А там еще есть:
- in-memory тестирование
- собственный DI вдохновленный FastAPI
- сериализация на интроспекции типов
- поддержка разных бекендов: Kafka, RabbitMQ, Redis, NATS
- свой CLI
- много всякого-разного!

Если вы ищете интересный проект для участия в Open Source - FastStream сейчас нуждается в контрибуторах: ревью PR'ов, участие в обсуждениях, большие и маленькие фичи, правки в документацию - мы будем рады любому участию!

* Telegram группа проекта
* Доклад от создателя фреймворка с PiterPy
1🔥73👍2111🕊2🤡2
`LOAD_CONST` разделили на три опкода в 3.14

https://github.com/python/cpython/pull/125972

В Python 3.14 распилили один из самых популярных опкодов: LOAD_CONST. Он, как можно понять из названия, он загружал константы из frame->co_consts:


// 3.13:
pure inst(LOAD_CONST, (-- value)) {
value = GETITEM(FRAME_CO_CONSTS, oparg);
Py_INCREF(value);
}



>>> def func():
... return 1

>>> func.__code__.co_consts
(None, 1)


Теперь LOAD_CONST разделен на:
- LOAD_SMALL_INT для интов в range(256)
- LOAD_CONST_IMMORTAL для загрузки бесмертных объектов (на 1 Py_INCREF меньше, см PyStackRef_FromPyObjectNew vs `PyStackRef_FromPyObjectImmortal`)
- LOAD_CONST для оставшихся

А еще и RETURN_CONST удалили под шумок.

И вот демо байткода:


>>> import dis
>>> def func():
... x = 1
... y = ...
... z = 'привет, мир'

>>> dis.dis(func, adaptive=True)
2 LOAD_SMALL_INT 1
STORE_FAST 0 (x)

3 LOAD_CONST 1 (Ellipsis)
STORE_FAST 1 (y)

4 LOAD_CONST 2 ('привет, мир')
STORE_FAST 2 (z)
LOAD_CONST 0 (None)
RETURN_VALUE

>>> # Create caches for tier1 adaptive interpreter to work:
>>> for _ in range(100):
... func()
>>> dis.dis(func, adaptive=True)
2 LOAD_SMALL_INT 1
STORE_FAST 0 (x)

3 LOAD_CONST_IMMORTAL 1 (Ellipsis)
STORE_FAST 1 (y)

4 LOAD_CONST 2 ('привет, мир')
STORE_FAST 2 (z)
LOAD_CONST_IMMORTAL 0 (None)
RETURN_VALUE


Зачем нужен LOAD_SMALL_INT?

https://github.com/python/cpython/issues/101291

Если вы внимательно смотрели мой видос про int, то вы помните, как выглядят инты внутри питона:


typedef struct _PyLongValue {
uintptr_t lv_tag; /* Number of digits, sign and flags */
digit ob_digit[1];
} _PyLongValue;

struct _longobject {
PyObject_HEAD
_PyLongValue long_value;
};


Большие и сложные объекты. Но, для очень частых маленьких чисел, такое переусложнение замедляет работу. Мы можем просто представлять числа в рамках одного машинного слова и складывать их сразу в oparg, без необходимости заргужать их из co_consts:


op(_LOAD_SMALL_INT, (-- value)) {
PyObject *val = PyLong_FromLong(this_instr->oparg);
value = sym_new_const(ctx, val);
}


В Python2, кстати, работало быстрее, потому что там был честный int тип.

Обсуждение

Задумываетесь ли вы про подобные микро-оптимизации, когда пишите код?
35👍9👌2
Argument Clinic

https://devguide.python.org/development-tools/clinic/

Если вы когда-нибудь смотрели исходники питона, то вы замечали внутри вот такие комментарии (взял за пример `sum()`):


/*[clinic input]
sum as builtin_sum

iterable: object
/
start: object(c_default="NULL") = 0

Return the sum of a 'start' value (default: 0) plus an iterable of numbers.
[clinic start generated code]*/

static PyObject *
builtin_sum_impl(PyObject *module, PyObject *iterable, PyObject *start)
/*[clinic end generated code: output=df758cec7d1d302f input=162b50765250d222]*/
{
// ...
}


Есть достаточно понятная проблема: нужно как-то иметь возможность передавать аргументы из Python кода в C код. Учитывая, что бывает много всяких видов Python и C функций (`METH_FASTCALL`, METH_O и тд), то все становится не так уж и просто.

AC позволяет делать достаточно просто описание сигнатуры функции при помощи специального DSL в комментариях.
И даже больше:
- Он генерирует сигнатуру сишной функции со всеми параметрами сразу после тега [clinic start generated code]
- Он хранит последнее состояние в /*[clinic end generated code: output=df758cec7d1d302f input=162b50765250d222]*/
- А еще он создает макросы вида:


PyDoc_STRVAR(builtin_sum__doc__,
"sum($module, iterable, /, start=0)\n"
"--\n"
"\n"
"Return the sum of a \'start\' value (default: 0) plus an iterable of numbers");

#define BUILTIN_SUM_METHODDEF \
{"sum", _PyCFunction_CAST(builtin_sum), METH_FASTCALL|METH_KEYWORDS, builtin_sum__doc__},


Чтобы потом использовать их для добавления методов в модули / классы:


static PyMethodDef builtin_methods[] = {
BUILTIN_SUM_METHODDEF
// ...
};


Как оно внутри?

- Есть большая либа внутри питона для работы с AC (с тестами и mypy)
- Есть make clinic для вызова данной либы на код, который вы меняете
- Можно кастомизировать выполнение либы на питоне, создавая питон код внутри C комментариев
- Мы используем AC даже для C-API тестов
- Сам генератор использует публичный C-API для выдергивания агрументов из переданных объектов. Код генерируется страшный, но читаемый, для примера кусок из файла Python/clinic/bltinmodule.c.h:


static PyObject *
builtin_sum(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyObject *kwnames)
{
PyObject *return_value = NULL;
#if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE)

// ...

static const char * const _keywords[] = {"", "start", NULL};
static _PyArg_Parser _parser = {
.keywords = _keywords,
.fname = "sum",
.kwtuple = KWTUPLE,
};
PyObject *argsbuf[2];
Py_ssize_t noptargs = nargs + (kwnames ? PyTuple_GET_SIZE(kwnames) : 0) - 1;
PyObject *iterable;
PyObject *start = NULL;

args = _PyArg_UnpackKeywords(args, nargs, NULL, kwnames, &_parser, 1, 2, 0, argsbuf);
if (!args) {
goto exit;
}
iterable = args[0];
if (!noptargs) {
goto skip_optional_pos;
}
start = args[1];
skip_optional_pos:
return_value = builtin_sum_impl(module, iterable, start);

exit:
return return_value;
}


С ним значительно удобнее, чем писать такое руками!

---

Кстати, скоро мы с моими друзьями с Хабра делаем совместную движуху: https://vibe.habr.com/?utm_source=opensource_findings

В программе:
- Общение с разными ребятами, кто занимается карьерой
- Игра в карьерную настолку
- Специальные активности, чтобы понять, какие вайбы в работе подходят именно вам
2👍399🤡2😱1
Нерегулярная рубрика "Посмотрите, что пишут!"

Кирилл - core разработчик CPython, пристально следит за обсуждениями, новыми фичами, интересными багами в питоне.

Если вам нравится мой контент - его канал вам тоже понравится. Один из немногих, на кого я сам подписан.
Узнаю оттуда много интересного.

Подписывайтесь! @cpython_notes
🔥18🤔1
Forwarded from CPython notes
Был смержен тред-локал байткод - https://github.com/python/cpython/pull/123926

Зачем это нужно и как это связано с nogil aka free-threaded?

Напомню, что в версии 3.11 был добавлен так называемый <адаптивный> интерпретатор, который умеет в специализацию.
Типичная операция x + y в обычном случае превращается в BINARY_OP опкод, который делает x.__add__(y), что несомненно не очень-то и дешево, так как надо производить лукап __add__ и прочие связанные с этим операции.
Адаптивный интерпретатор "подстраивается" под ситуации когда у вас достаточно много случаев в коде когда в операции x + y оба операнда являются интами, и тогда можно сэкономить на лукапе и дергать специализированную сишную функцию для сложения интов, которая не производит никакого лукапа атрибутов, и именно на этом мы выигрываем по производительности.

Так вот, в случае с nogil сборкой адаптивный интерпретатор не является потокобезопасным, что делало nogil сборку действительно медленнее(потому что специализация была выключена в nogil сборке). Pull request отмеченный вышел делает адаптивный интерпретатор потокобезопасным и вместе с этим у codeobject появляется атрибут co_tlbc :)
🔥45👍7😁4
Чем корутина реально отличается от генератора?

Достаточно часто можно услышать, что корутины произошли от генераторов. Живы еще те динозавры, которые помнят @asyncio.coroutine в py3.4 и корутины на yield

Определения

На самом деле, определить, что такое генератор и корутина - не так просто, как может показаться. Я пользуюсь следующими определениям:
- генератор = что-то, что соответствует интерфейсу _collections_abc.Generator
- корутина = что-то, что соответствует интерфейсу _collections_abc.Coroutine

(да, вы можете создавать свои нестандартные генераторы и корутины на Python и C)

Уже в самих интерфейсах, вы можете увидеть, что часть методов идентична, часть отличается:


>>> set(dir(gen)) - set(dir(coro))
{'gi_running', '__next__', 'gi_frame', 'gi_yieldfrom', 'gi_suspended', '__iter__', 'gi_code'}

>>> set(dir(coro)) - set(dir(gen))
{'cr_running', '__await__', 'cr_frame', 'cr_suspended', 'cr_origin', 'cr_code', 'cr_await'}


Копнем чуть глубже, создадим "generator function" и "coroutine function" (функции, которые вернут генератор / корутину):


import asyncio

async def coro_func():
await asyncio.sleep(1)

def gen_func():
yield 1


Байткод их создания будет абсотютно идентичным (разница только в именах):


2 LOAD_CONST 0 (<code object ...>)
MAKE_FUNCTION
STORE_NAME 0 (<NAME>)


Потому что сами объекты функций будут очень похожими, отличаться будут объекты CodeObject, которые создаются компилятором в assemble.c. Проходимся по всем AST нодам в функции и вычисляем, есть ли там yield или yield from:


static int
symtable_visit_expr(struct symtable *st, expr_ty e)
{
switch (e->kind) {
// ...
case Yield_kind:
if (e->v.Yield.value)
VISIT(st, expr, e->v.Yield.value);
st->st_cur->ste_generator = 1; // теперь функция будет "generator function"
if (st->st_cur->ste_comprehension) {
return symtable_raise_if_comprehension_block(st, e);
}
break;
case YieldFrom_kind:
VISIT(st, expr, e->v.YieldFrom.value);
st->st_cur->ste_generator = 1;
if (st->st_cur->ste_comprehension) {
return symtable_raise_if_comprehension_block(st, e);
}
break;
}
}


И для корутины:


// symtable_visit_stmt(struct symtable *st, stmt_ty s)
case AsyncFunctionDef_kind: {
if (!symtable_add_def(st, s->v.AsyncFunctionDef.name, DEF_LOCAL, LOCATION(s)))
return 0;
// ...
st->st_cur->ste_coroutine = 1;

// symtable_visit_expr(struct symtable *st, expr_ty e)
case Await_kind:
if (!allows_top_level_await(st)) {
if (!_PyST_IsFunctionLike(st->st_cur)) {
return PyErr_SetString(PyExc_SyntaxError, "'await' outside function");
}
if (!IS_ASYNC_DEF(st) && st->st_cur->ste_comprehension == NoComprehension) {
return PyErr_SetString(PyExc_SyntaxError, "'await' outside async function");
}
}
VISIT(st, expr, e->v.Await.value);
st->st_cur->ste_coroutine = 1;


И потом уже просто вычисляем флаги для CodeObject:


static int compute_code_flags(compiler *c)
{
if (_PyST_IsFunctionLike(ste)) {
flags |= ...;
if (ste->ste_generator && !ste->ste_coroutine)
flags |= CO_GENERATOR;
if (ste->ste_generator && ste->ste_coroutine)
flags |= CO_ASYNC_GENERATOR;
// ...
}
if (ste->ste_coroutine && !ste->ste_generator) {
flags |= CO_COROUTINE;
}
return flags;
}


Результат:


>>> from inspect import CO_COROUTINE, CO_GENERATOR
>>> gen_func.__code__.co_flags & CO_GENERATOR
32
>>> coro_func.__code__.co_flags & CO_GENERATOR
0
>>> coro_func.__code__.co_flags & CO_COROUTINE
128


Что происходит при вызове?

Если сделать dis функций gen_func и coro_func, то у них первым байткодом будет RETURN_GENERATOR (см _Py_MakeCoro):
🔥25👍93🙏2👏1
PyObject *
_Py_MakeCoro(PyFunctionObject *func)
{
int coro_flags = ((PyCodeObject *)func->func_code)->co_flags &
(CO_GENERATOR | CO_COROUTINE | CO_ASYNC_GENERATOR);
assert(coro_flags);
if (coro_flags == CO_GENERATOR) {
return make_gen(&PyGen_Type, func);
}
if (coro_flags == CO_ASYNC_GENERATOR) {
return make_gen(&PyAsyncGen_Type, func);
}

assert (coro_flags == CO_COROUTINE);
PyObject *coro = make_gen(&PyCoro_Type, func);
return coro;
}


Вот тут как раз при выполнении функции и будут создаваться инстансы types.GeneratorType и types.CoroutineType.

Вот и вся разница :)

Дальше уже во многих местах на основе флагов / методов - объекты начинают вести себя по-разному. Однако, все еще генератор можно превратить в корутину при помощи types.coroutine

Узнали сегодня что-то новое?
735🔥32👍13
Кстати, у нас в Нижнем Новгороде будет митап по питону 22 ноября: https://xn--r1a.website/pytho_nn/15099

4 крутейших спикера:
- "Уязвимый" Python – Юлия Волкова (CodeScoring, Санкт-Петербург)
- "Английский для разработчика" – James Stuart Black (JB Teach, Нижний Новгород)
- "Программирование и искусство" – Дмитрий Сошников (НИУ ВШЭ/МАИ/Yandex Cloud, Москва)
- "Квантовое программирование на Python: Погружение в квантовые вычисления для разработчиков" – Бейлак Алиев (Райффайзен банк, Москва)

И еще:
- Общение в баре после митапа
- Игра в мою настолку: https://github.com/sobolevn/ship-it-boardgame

Регистрация: https://pytho-nn.timepad.ru/event/3089004/
Чат местного сообщества: @pytho_nn

Если будете в Нижнем - заходите! Ждем всех любителей питонов :)
👍28🔥19🎉5🤯1
Как работает диспатчеризация байткода внутри VM? Computed GOTOs

Многие из вас знают, что внутри питона есть большой switch-case, который выполняется в цикле, он находит нужный байткод и выполняет его. Выглядит оно примерно как-то так:


#define LOAD_CONST 79

PyObject* _Py_HOT_FUNCTION
_PyEval_EvalFrameDefault(PyThreadState *tstate,
_PyInterpreterFrame *frame,
int throwflag)
{
DISPATCH_GOTO(); // разворачивается в `goto dispatch_opcode`
dispatch_opcode:
switch (opcode) {
TARGET(LOAD_CONST): // разворачивается в `case 79:`
{
frame->instr_ptr = next_instr;
next_instr += 1;
_PyStackRef value = PyStackRef_FromPyObjectNew(
GETITEM(FRAME_CO_CONSTS, oparg));
// ...
}
// ...
}

opcode = next_instr->op.code;
DISPATCH_GOTO(); // разворачивается в `goto dispatch_opcode;`

exit:
// end of cycle: success or error
}


Но, на самом деле – все не совсем так просто! Данный switch по сути является самой горячей частью кода во всем интерпретаторе, он выполняется буквально на любое действие. Любое ускорение данного места дает ускорение всему коду на питоне. А значит – такие ускорения были придуманы.

Концепт Computed GOTOs

Вводная статья на тему, кто вообще никогда о таком не слышал. Если очень кратко:
- Создаем известную в compile-time таблицу переходов, которая использует лейблы для goto. Назовем ее opcode_targets
- Вместо switch просто используем goto *opcode_targets[opcode]
- Проверяем в configure, что компилятор поддерживает такую фичу (`gcc` поддерживает, --with-computed-gotos по-умолчанию включено)
- Накручиваем DSL для виртуальной машины:


#if USE_COMPUTED_GOTOS
# define TARGET(op) TARGET_##op:
# define DISPATCH_GOTO() goto *opcode_targets[opcode]
#else
# define TARGET(op) case op: TARGET_##op:
# define DISPATCH_GOTO() goto dispatch_opcode
#endif


Итого, используя тот же DSL на макросах, благодаря флагу USE_COMPUTED_GOTOS (который выставляется в configure) – получаем совсем другой код в _PyEval_EvalFrameDefault:


#define LOAD_CONST 79

PyObject* _Py_HOT_FUNCTION
_PyEval_EvalFrameDefault(PyThreadState *tstate,
_PyInterpreterFrame *frame,
int throwflag)
{
DISPATCH_GOTO(); // // goto *opcode_targets[opcode]
TARGET(LOAD_CONST): // TARGET_79:
{
frame->instr_ptr = next_instr;
next_instr += 1;
_PyStackRef value = PyStackRef_FromPyObjectNew(
GETITEM(FRAME_CO_CONSTS, oparg));
// ...
}
// ...

opcode = next_instr->op.code;
DISPATCH_GOTO(); // goto *opcode_targets[opcode]

exit:
// end of cycle: success or error
}


Данная реализация где-то на 15% быстрее реализации на switch. Но для простоты все продолжают говорить, что внутри VM switch+case

Узнали сегодня что-то новое? :)

| Поддержать | YouTube | GitHub |
🔥95👍3618🤯2😱1
Лучший курс по Python 11: bytearray

42 минуты С-шного хардкора про bytearray, что может быть лучше?

https://www.youtube.com/watch?v=5UFx29EVlkU

В видео будет про:
- Разные аллокаторы в CPython: PyMem_Malloc и PyMem_Realloc
- C-pointer math (для самых маленьких)
- Разные хитрые оптимизации для работы с bytearray

Бонус 1

Я обещал поделиться логикой стратегии изменения размера bytearray.

Бонус 2

Интересный вопрос, который я не осветил в видео. Почему код работает так?


>>> b = bytearray(b'1234')
>>> del b[:4:2]
>>> b.__alloc__()
5


Почему так?

1. Вызывается функция: bytearray_ass_subscript
2. values будет NULL, потому как удаление (сишный аналог __delitem__ из питона работает так)
3. Дальше распаковываем slice в переменные тут
4. Удаляем тут хитрым и достаточно быстрым способом

Ответы

Ответ на вопрос из видео с поиском бага на слайде в Cшном коде: https://github.com/python/cpython/pull/126981

| Поддержать | YouTube | GitHub |
6🔥92👍20🥰6🤯4🤔21
Аллокаторы в СPython: PyArena

Один из самых простых аллокаторов в питоне. Исходники.

По сути данный аллокатор является небольшой оберткой поверх PyMem_Malloc, но с интересной особенностью. Если PyMem_Malloc имеет PyMem_Free для освобождения памяти каждого конкретного объекта, то PyArena имеет только _PyArena_Free(PyArena *arena) для освобождения сразу всей арены со всеми объектами, которые являются ее частью.

Смотрим:


struct _arena {
/* Pointer to the first block allocated for the arena, never NULL.
It is used only to find the first block when the arena is
being freed. */
block *a_head;

/* Pointer to the block currently used for allocation. Its
ab_next field should be NULL. If it is not-null after a
call to block_alloc(), it means a new block has been allocated
and a_cur should be reset to point it. */
block *a_cur;

/* A Python list object containing references to all the PyObject
pointers associated with this arena. They will be DECREFed
when the arena is freed. */
PyObject *a_objects;
};


Как мы видим, арена содержит два указателя на блоки. А вот и они:


typedef struct _block {
/* Total number of bytes owned by this block available to pass out.
Read-only after initialization. The first such byte starts at
ab_mem */
size_t ab_size;

/* Total number of bytes already passed out. The next byte available
to pass out starts at ab_mem + ab_offset */
size_t ab_offset;

/* An arena maintains a singly-linked, NULL-terminated list of
all blocks owned by the arena. These are linked via the
ab_next member */
struct _block *ab_next;

/* Pointer to the first allocatable byte owned by this block. Read-
only after initialization */
void *ab_mem;
} block;


И очищаем сразу все внутри арены:


void _PyArena_Free(PyArena *arena)
{
assert(arena);
// ...
block_free(arena->a_head);
Py_DECREF(arena->a_objects);
PyMem_Free(arena);
}


Обратите внимание, что у PyArena есть block'и и есть список обычных PyObject *. Что достигается за счет следующих АПИ:
- _PyArena_New – создает новую арену и выделяет память под нее. Создает пустой список под будущие объекты
- _PyArena_Free – очищает память существующей арены. Удаляет все блоки из памяти, декрефит объекты в списке, их собирает reference-counter
- _PyArena_Malloc – создает новый block нужного размера и сохраняет указатель на него в single-linked list
- _PyArena_AddObject – добавляет PyObject * в список отслеживаемых объектов и гарантирует, что он будет жить столько, сколько живет сама арена

Использование

Где нужна арена? На самом деле – много где. Сам подход с ареной – можно сравнить с lifetime из Rust. Все объекты внутри арены живут до одного общего конца.

Используется там, где объекты логически имеют общий lifetime. Например, при парсинге кода в AST. Ведь все дерево объектов в AST – имеет общий лайфтайм. Так намного проще обрабатывать ошибки, если произошло что-то плохое, мы просто убиваем всю арену. И нам не надо чистить все объекты в памяти ручками.

Крайне удобная штука.

Большая статья по теме: https://rfleury.com/p/untangling-lifetimes-the-arena-allocator

Выводы

Вот и single-linked list с алгособесов пригодился! 🌚️️️️
👍4235🔥10👏1😢1👌1
Кто парсит парсер? Метаграмматики

Звучит как название нового фильма Марвел, но на самом деле – перед нами достаточно интересная задача.

В CPython с недавних пор (вспоминаем проект Guido van Rossum по внедрению PEG парсера) грамматика описана вот так (ссылка):


lambdef[expr_ty]:
| 'lambda' a=[lambda_params] ':' b=expression {
_PyAST_Lambda((a) ? a : CHECK(arguments_ty, _PyPegen_empty_arguments(p)), b, EXTRA) }

lambda_params[arguments_ty]:
| invalid_lambda_parameters
| lambda_parameters


На данном примере – грамматика для описания lambda функций.

Что здесь что?
- lambdef и lambda_params обозначают названия правил
- [expr_ty] и [arguments_ty] – метаинформация, которая будет использована парсером позже. Тут буквально куски C кода написаны
- У правил есть варианты: описаны через |, сначала пробуем первое, потом второе и тд
- Правила "складываются" в более сложные правила. Напимер lambdadef содержит в себе: a=[lambda_params] (что конечно же обозначает парсинг параметров lambda функции)
- 'lambda' – обозначает ключевое слово lambda, а ':' - физический символ : в коде
- Внутри {} у нас снова идет C код: данная часть называется "действием", она буквально описывает, какой C код вызывать при успешном парсинге данного правила. _PyAST_Lambda((a) ? a : CHECK(arguments_ty, _PyPegen_empty_arguments(p)), b, EXTRA) – в нашем случае вызывает функцию _PyAST_Lambda

И так мы маленькими кусочками описываем всю большую грамматику Python. Разобрались.

Команда make regen-pegen позволит вам автоматически сгенерировать C парсер из грамматики выше. И получится вот такое:


if (
(_keyword = _PyPegen_expect_token(p, 609)) // token='lambda'
&& (a = lambda_params_rule(p), !p->error_indicator) // lambda_params?
&& (_literal = _PyPegen_expect_token(p, 11)) // token=':'
&& (b = expression_rule(p)) // expression
)
{
Token *_token = _PyPegen_get_last_nonnwhitespace_token(p);

_res = _PyAST_Lambda(
(a) ? a : CHECK(arguments_ty, _PyPegen_empty_arguments(p)),
b, EXTRA);
goto done;
}


И уже данный парсер будет вызван, чтобы превратить lambda x, y: ... в AST при работе питона. Подводка закончена.

Что за метаграмматики?

Ключевой вопрос: а кто парсит файл с грамматикой? Кто определяет, что такое "правило", "варианты", "действие"?

Оказывается, что в питоне есть еще один уровень грамматик. Грамматика, которая определяет правила основной грамматики. Мы её так и называем – метаграмматика.

Выглядит она вот так:


rule[Rule]:
| rulename memoflag? ":" alts NEWLINE INDENT more_alts DEDENT {
Rule(rulename[0], rulename[1], Rhs(alts.alts + more_alts.alts), memo=opt) }
| rulename memoflag? ":" NEWLINE INDENT more_alts DEDENT {
Rule(rulename[0], rulename[1], more_alts, memo=opt) }
| rulename memoflag? ":" alts NEWLINE { Rule(rulename[0], rulename[1], alts, memo=opt) }


Здесь мы как раз видим в похожем синтаксисе определение, что такое "правило": с "именем" правила и "альтернативами".

Здесь тоже есть куски кода: [Rule] и Rule(rulename[0], rulename[1], Rhs(alts.alts + more_alts.alts), memo=opt), но они уже на питоне. Потому что метаграмматика генерирует парсер для грамматики на питоне:


@memoize
def rule(self) -> Optional[Rule]:
# rule: rulename memoflag? ":" alts NEWLINE INDENT more_alts DEDENT | rulename memoflag? ":" NEWLINE INDENT more_alts DEDENT | rulename memoflag? ":" alts NEWLINE
mark = self._mark()
if (
(rulename := self.rulename())
and (opt := self.memoflag(),)
and (literal := self.expect(":"))
and (alts := self.alts())
and (_newline := self.expect('NEWLINE'))
and (_indent := self.expect('INDENT'))
and (more_alts := self.more_alts())
and (_dedent := self.expect('DEDENT'))
):
return Rule(rulename[0], rulename[1],
Rhs(alts.alts + more_alts.alts), memo=opt)


Запускаем make regen-pegen-metaparser, и получаем на выходе питоновский парсер для грамматик.
🤯44👍3011🔥4👎1
Финалим

- Метаграмматика создает метапарсер на питоне для парсинга грамматики
- Метапарсер парсит грамматику и создает парсер на Си
- Си парсер парсит наш код на питоне

Но где грамматика для метаграмматики? Ответ можно найти тут.

| Поддержать | YouTube | GitHub | Чат |
🤯42👍199🔥6😁3🤡3😱1
Как работает CI для опенсорса?

Любой крупный опенсорс проект невозможен без обильного тестирования. CI-сервисы уже многие годы являются нашими обязательными спутниками. Но как они работают?

Давайте разбирать на примере GitVerse.

Важнейшие части:
- репозиторий – откуда мы берем задачи и код для запуска;
- DSL – описание того, как и что мы будем запускать. Обычно в yaml;
- runner (self-hosted или shared) – где мы запускаем определенные нами задачи.

Поговорим про две последние части.

DSL

С DSL все очень интересно. В GitVerse синтаксис и рантайм совместимы с GitHub Actions — значит, можно переиспользовать почти все существующие actions из маркетплейса.

Пример переиспользования wemake-python-styleguide GHA можно найти тут:


name: wps
'on':
push:
branches:
- master
pull_request:
jobs:
build:
runs-on: ubuntu-cloud-runner # <- отличие от GHA
steps:
- uses: actions/checkout@v4
- uses: wemake-services/wemake-python-styleguide@master


Работает! Вот ссылка на логи.

Пока есть проблемы с actions/cache, но обещают скоро пофиксить.

Runner

Как и всегда: можно делать свои self-hosted раннеры, есть простой способ завести раннеры в cloud.ru, есть hosted runners от платформы.

Что интересно? Интересно, что сам раннер построен поверх таскраннера act, который умеет запускать GitHub Actions локально. Документация тут. Можно попробовать запустить мой пример локально:


» act -W .gitverse/workflows/wps.yaml --container-architecture linux/amd64 -P ubuntu-cloud-runner=node:16-buster-slim

[wps] git clone 'https://github.com/wemake-services/wemake-python-styleguide' # ref=master
[wps] Run Main actions/checkout@v4
[wps] 🐳 docker cp src=/Users/sobolev/Desktop/wps-test/. dst=/Users/sobolev/Desktop/wps-test
[wps] Success - Main actions/checkout@v4
[wps] Run Main wemake-services/wemake-python-styleguide@master
[wps] Failure - Main wemake-services/wemake-python-styleguide@master
[wps] ::set-output:: output=./script.py
2:1 WPS421 Found wrong function call: print
print('hello world')
^
[wps] 🏁 Job failed


Круто?

Внутри self-hosted runner выполняется похожий код. В GitVerse есть свой act_runner поверх act, чтобы было удобнее. Там гошный standalone бинарник, легко скачать и использовать.

Пока hosted runners в бете, но их постепенно выкатывают на всех пользователей. Если у вас что-то не работает, то создать багу можно тут.

Послесловие

Ребята из GitVerse решили поддержать мою работу в опенсорсе, что огромная редкость в наших реалиях.
Большое им спасибо.

Реклама. АО «СберТех» ИНН: 7736632467. erid: 2W5zFHCJ2RN
🔥75👍38💩19👎75🤮4🤬3👌3👏1😁1
wemake-python-styleguide@1.0.0 релизнут!

https://github.com/wemake-services/wemake-python-styleguide/releases/tag/1.0.0

Самый строгий линтер в мире стал еще строже и еще удобнее.

ruff

Некоторое время назад я понял, что если сейчас не поддержать ruff, то проект умрет. Сказано – сделано.
Теперь wemake-python-styleguide поддерживает работу вместе с ruff. Что оно означает на практике?

- Теперь WPS не выкидывает никаких ошибок, которые противоречили бы ruff. Например, я убрал все стилистические правила, чтобы решать все простым ruff format
- Все дублирующие правила из WPS были убраны в пользу ruff. Ведь ruff быстрее их находит и некоторые даже фиксит
- Теперь можно использовать ruff check && ruff format && flake8 --select=WPS ., WPS, конечно, может найти дополнительные ошибки, но не будет конфликтовать с ruff как раньше
- Поддержка полная. От preview = true до самых заковыристых правил PyLint, да теперь WPS совместим с PyLint из ruff

Black, кстати, теперь тоже поддерживается.

Конфигурацию можно найти тут.

Что еще интересного в релизе?

- Множество новых правил сложности
- Крутая поддержка match и case. Находим дубликаты case условий, проверяем сложность, находим много разных ошибок
- Много новой конфигурации, чтобы точечно настраивать отдельные правила линтера
- Куча багов поправлено!

Статистика релиза:
- WPS стал минимум в 2.4 х быстрее, потому что я удалил много кода и много flake8 плагинов
- Количество коммитов с прошлого релиза: 294
- Количество задач, которые я закрыл в процессе работы (с 195 до 26) = ~170
- Изменений: 490 файлов, +15к, -26к
- Количество контрибьюторов в проект достигло двухсот!

Страдайте Наслаждайтесь! Всех с наступающим 🎄

| Поддержать | YouTube | GitHub | Чат |
22🔥238👏3123👍10🎉8🤮2🤡2🤩1
Статический анализ GitHub Actions

Сразу после релиза новой версии линтера, я задался вопросом обновления своего шаблона для создания новых питоновских библиотек: https://github.com/wemake-services/wemake-python-package

И я понял, что я несколько отстал в вопросе стат анализа GitHub Actions и прочей инфраструктуры.
Расскажу о своих находках.

pre-commit ci

Все знают про пакет pre-commit? Несколько лет назад он получил еще и свой собственный CI, который умеет запускаться без дополнительного конфига. И автоматически пушить вам в ветку любые изменения. Что супер удобно для всяких ruff / black / isort и прочего. У нас такое стоит в большом количестве проектов. Вот пример из typeshed. Вот что поменялось автоматически.

Строить CI на базе pre-commit очень удобно, потому что тебе просто нужно скопировать пару строк в конфиг. А плюсов много:
- Автоматически исправляются многие проблемы
- Автоматически запускается CI, 0 настроек
- Локально все тоже работает одной командой: pre-commit run TASK_ID -a

actionlint

Первый раз я увидел actionlint внутри CPython и затащил его в mypy. Actionlint на #go, он предлагает набор проверок для ваших GitHub Actions от безопасности до валидации спеки вашего yml. Довольно полезно, позволяет найти много мест для улучшений.


test.yaml:3:5: unexpected key "branch" for "push" section. expected one of "branches", ..., "workflows" [syntax-check]
|
3 | branch: main
| ^~~~~~~
test.yaml:10:28: label "linux-latest" is unknown. available labels are "macos-latest", ..., "windows". if it is a custom label for self-hosted runner, set list of labels in actionlint.yaml config file [runner-label]
|
10 | os: [macos-latest, linux-latest]
| ^~~~~~~~~~~~~
test.yaml:13:41: "github.event.head_commit.message" is potentially untrusted. avoid using it directly in inline scripts. instead, pass it through an environment variable. see https://docs.github.com/en/actions/security-for-github-actions/security-guides/security-hardening-for-github-actions for more details [expression]
|
13 | - run: echo "Checking commit '${{ github.event.head_commit.message }}'"
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~


Даже умеет автоматом shellcheck запускать на ваши run: скрипты!

zizmor

Исходники. Уже на #rust, он более злой. Делает похожие вещи: находит проблемы безопасности. Находит много проблем.

Вот пример, сколько всего он нашел в mypy.


warning[artipacked]: credential persistence through GitHub Actions artifacts
--> mypy/.github/workflows/mypy_primer.yml:37:9
|
37 | - uses: actions/checkout@v4
| _________-
38 | | with:
39 | | path: mypy_to_test
40 | | fetch-depth: 0
| |________________________- does not set persist-credentials: false
|
= note: audit confidence → Low

error[dangerous-triggers]: use of fundamentally insecure workflow trigger
--> mypy/.github/workflows/mypy_primer_comment.yml:3:1
|
3 | / on:
4 | | workflow_run:
... |
7 | | types:
8 | | - completed
| |_________________^ workflow_run is almost always used insecurely
|
= note: audit confidence → Medium


check-jsonschema

Еще есть вот такой проект, он в основном полезен за счет доп интеграций: можно проверять dependabot.yml, renovate.yml, readthedocs.yml и многое другое.

Ставится просто как:


- repo: https://github.com/python-jsonschema/check-jsonschema
rev: 0.30.0
hooks:
- id: check-dependabot
- id: check-github-workflows


Выводы

Как всегда – статический анализ многому меня научил. Я узнал много нового про безопасность GitHub Actions, про вектора атаки, про лучшие практики. А сколько проблем в ваших actions?

Скоро ждите весь новый тулинг в python шаблоне v2025 😎
👍6616🤯7🔥5👎3😱1
LaranaJS – Рендерим фронтенд в картинку! 🌚

LaranaJS – это большой эксперимент по поиску альтернативных способов рисовать графические интерфейсы. Если большинство других фреймворков полагаются на такие устаревшие технологии как HTML и CSS и вендорлочат себя на браузеры, то Larana делает всё иначе.

Вот как устроены сетевые взаимодействия в LaranaJS.

Браузер запрашивает страницу

На этом этапе происходят создание сессии, резолв роута и инициализация страницы. В то же время разные подсистемы (рендерер, менеджер сессий, роутер и т. д.) генерируют клиентский код — он минимальный – просто canvas и немного работы с сетью по вебсокетам:


<html>
<!--Minimal head-->
<body>
<canvas id="canvas"></canvas>
<script>
// Network code
</script>
</body>
</html>


Клиент открывает соединение

При полной загрузке страницы создаётся подключение по веб-сокетам и начинается обмен сообщениями:

- Сервер отрисовывает UI в виде изображения (png) и отправляет его на клиент.
- Клиент принимает изображение и вставляет его в canvas.

Все дальнейшие взаимодействия происходят в виде обмена событиями – действия пользователя отправляются клиентом, команды и изображения отправляются сервером.


// event
{
"event": "mousemove",
"x": 0,
"y": 0,
}
// response
{
"image": "", // изображение в base64
"x": 0, // координаты для вставки изображения
"y": 0,
"w": 0,
"h": 0,
}


Такая архитектура позволяет сократить размер клиента до 6KB #js и запускаться в любом браузере c 2009 года. При этом есть возможность написать собственное клиентское приложение и запускать его хоть на esp32 с подключённым дисплеем.

Несмотря на новизну подхода, сама разработка интерфейсов остаётся привычной. Например, вот код страницы с типичным каунтером:


class HomePage extends Page {
title() {
return 'Hello, World!'
}

init() {
const { initState } = this.useState()
initState({ counter: 0 })
}

root() {
return layout({
style: 'row',
children: [
button({ text: '+', onClick: () => this.increment() }),
text({ model: 'counter' }),
button({ text: '-', onClick: () => this.decrement() }),
],
})
}
}


Большинство задач можно решить с помощью готовых компонентов. Но если есть необходимость сделать что-то необычное, то есть прямой доступ к созданию команд для рендеринга в компоненте figure. Например, рисовать сложные фигуры вроде снежинок:


root() {
return figure({
template: (fig, queue) => {
line({
borderColor: '#aaaaff',
borderWidth: 2,
points: [
point({ x: x - halfRadius, y: y - halfRadius }),
point({ x: x + halfRadius, y: y + halfRadius }),
point({ x: x + halfRadius, y: y - halfRadius, moveTo: true }),
point({ x: x - halfRadius, y: y + halfRadius }),
point({ x: x + halfRadius, y, moveTo: true }),
point({ x: x - halfRadius, y }),
point({ x, y: y - halfRadius, moveTo: true }),
point({ x, y: y + halfRadius }),
],
}).to(queue)
},
})
}


Специально для этого поста я подготовил новогоднее демо:

- Репозиторий: https://github.com/laranatech/snowflakes-demo
- Потыкать: https://snowflakes.larana.tech

Подсистемы

Выше я упоминал подсистемы вроде рендерера и менеджера сессий. Они уже вшиты в фреймворк и можно использовать готовые. Можно написать и собстенные, а потом просто добавить их в приложение:


const app = new LaranaApp({
config,
renderer: new ClientRenderer({}),
sessionManager: new MemorySessionManager({}),
router: new DefaultRouter({ routes }),
})

app.run()


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

Больше подобного авангарда в канале @laranatech!
Автор: @e_kucheriavyi
352💩32🤔23🔥8🤯8👍5🤡3👏2🤩2
Лучший курс по Python 12: tuple

https://youtube.com/watch?v=P5OY3Y4Fc7k

Я решил окончательно упороться: сделал видео про tuple на 1ч 30м. Зато я рассказал про tuple вообще все, что знал сам. Для джунов:

- В чем разница между tuple и list?
- Аннотации tuple
- Тип произведение
- TypeVarTuple, PEP646, Unpack

Для мидлов:
- ast.Tuple
- tuple_iterator
- collections.abc
- collections.namedtuple
- typing.NamedTuple

Для сениоров:
- PyTupleObject
- PyVarObject
- tp_alloc, tp_dealloc, freelists
- __len__
- __hash__
- Мутабельность tuple
- PyTuple_Pack, Py_BuildValue
- Виртуальная машина и компилятор: BUILD_TUPLE
- INTRINSIC_LIST_TO_TUPLE
- Оптимизации компилятора
- PySequenceTuple

Обещанный бонус

В видео я обещал, что расскажу в тг, что такое Py_TRASHCAN_BEGIN и Py_TRASHCAN_END.
Документация и исходники: https://github.com/python/cpython/blob/d05140f9f77d7dfc753dd1e5ac3a5962aaa03eff/Include/cpython/object.h#L431-L507

По факту - данные два макроса представляют собой do/while цикл, который позволяет более удобно управлять сборкой "контейнеров" (tuple, в нашем случае). Каждый объект внутри "контейнера" может тоже быть контейнером. Таким образом про Py_DECREF(op->ob_item[i]) можно начать каскадную деаллокацию объектов внутри. И мы можем столкнуться с переполнением стека вызовов.


#define Py_TRASHCAN_BEGIN(op, dealloc) \
do { \
PyThreadState *tstate = PyThreadState_Get(); \
if (tstate->c_recursion_remaining <= Py_TRASHCAN_HEADROOM && Py_TYPE(op)->tp_dealloc == (destructor)dealloc) { \
_PyTrash_thread_deposit_object(tstate, (PyObject *)op); \
break; \
} \
tstate->c_recursion_remaining--;
/* The body of the deallocator is here. */

#define Py_TRASHCAN_END \
tstate->c_recursion_remaining++; \
if (tstate->delete_later && tstate->c_recursion_remaining > (Py_TRASHCAN_HEADROOM*2)) { \
_PyTrash_thread_destroy_chain(tstate); \
} \
} while (0);


По сути, мы просто при достижении определенного "большого" значения (50) перестаем выполнять деаллокацию напрямую, просто добавляем объекты в список для деаллокации на потом. Вот и вся хитрость!

Завершение

Если вам нравится мой технический контент – его всегда можно поддержать:
- Материально
- Морально: поделиться с вашими коллегами, чтобы они тоже знали все про кортежи :)

#lkpp

| Поддержать | YouTube | GitHub | Чат |
253🔥170👍2410😱8