- PVSM.RU - https://www.pvsm.ru -
До | После |
---|---|
|
|
Так получилось, что аж с 2012 года я разрабатываю open source браузерку, являясь единственным программистом. На Python само собой. Браузерка — штука не самая простая, сейчас в основной части проекта больше 1000 модулей и более 120 000 строк кода на Python. В сумме же с проектами-спутниками будет раза в полтора больше.
В какой-то момент мне надоело возиться с этажами импортов в начале каждого файла и я решил разобраться с этой проблемой раз и навсегда. Так родилась библиотека smart_imports (github [1], pypi [2]).
Идея достаточно проста. Любой сложный проект со временем формирует собственное соглашение об именовании всего. Если это соглашение превратить в более формальные правила, то любую сущность можно будет импортировать автоматически по имени ассоциированной с ней переменной.
Например, не надо будет писать import math
чтобы обратиться к math.pi
— мы и так можем понять, что в данном случае math
— модуль стандартной библиотеки.
Smart imports поддерживают Python >= 3.5 Библиотека полностью покрыта тестами, coverage > 95% [3]. Сам пользуюсь уже год.
За подробностями приглашаю под кат.
Итак, код из заглавной картинки работает следующим образом:
smart_imports.all()
библиотека строит AST модуля, из которого сделан вызов;Неинициализированные переменные ищутся во всех местах кода, включая новый синтаксис.
Автоматическое импортирование включается только для тех компонентов проекта, которые явно вызывают smart_imoprts.all()
. Кроме того, использование smart imports не запрещает использовать обычные импорты. Это позволяет внедрять библиотеку постепенно, равно как и разрешать сложные циклические зависимости.
Дотошливый читатель заметит, что AST модуля конструируется два раза:
smart_imports.all()
.AST действительно можно строить только один раз (для этого надо встроиться в процесс импорта модулей с помощью import hooks реализованных в PEP-0302 [4], но такое решение замедляет импорт.
Само собой, AST каждого модуля строится и анализируется только раз за запуск.
Библиотеку можно использовать без дополнительной конфигурации. По умолчанию она импортирует модули по следующим правилам:
os.path
будет импортирован при наличии переменной os_path
.Работа smart imports не сказывается на показателях работы программы, но увеличивает время её запуска.
Из-за повторного построения AST время первого запуска увеличивается примерно в 1.5-2 раза. Для малых проектов это несущественно. В больших проектах же время запуска страдает скорее от структуры зависимостей между модулями, чем от времени импорта конкретного модуля.
Когда если smart imports станут популярными, перепишу работу с AST на C — это должно заметно снизить издержки при запуске.
Для ускорения загрузки, результаты обработки AST модулей можно кэшировать на файловой системе. Включается кэширование в конфиге. Само собой, кэш инвалидируется при изменении исходников.
На время запуска влияет как перечень правил поиска модулей, так и их последовательность. Так как некоторые правила используют стандартную функциональность Python для поиска модулей. Исключить эти расходы можно явно указав соответствие имён и модулей с помощью правила «Кастомизированные имена» (см. далее).
Дефолтная конфигурация была описана ранее. Её должно хватать для работы со стандартной библиотекой в небольших проектах.
{
"cache_dir": null,
"rules": [{"type": "rule_local_modules"},
{"type": "rule_stdlib"},
{"type": "rule_predefined_names"},
{"type": "rule_global_modules"}]
}
При необходимости, более сложный конфиг можно положить на файловую систему.
Пример сложного конфига [7] (из браузерки).
Во время вызова smart_import.all()
библиотека определяет положение вызывающего модуля на файловой системе и начинает искать файл smart_imports.json
по направлению от текущего каталога к корневому. Если такой файл найден, он считается конфигурацией для текущего модуля.
Можно использовать несколько разных конфигов (разместив их в разных каталогах).
Параметров конфигурации сейчас не так много:
{
// Каталог для хранения кэша AST.
// Если не указан или null — кэш не используется.
"cache_dir": null|"string",
// Список конфигов правил в порядке их применения.
"rules": []
}
Порядок указания правил в конфиге определяет порядок их применения. Первое сработавшее правило останавливает дальнейший поиск импортов.
В примерах конфигов далее будет часто фигурировать правило rule_predefined_names
, оно необходимо чтобы корректно распознавались встроенные функции (например, print
).
Правило позволяет игнорировать предопределённые имена вроде __file__
и встроенные функции, например print
.
# конфиг:
# {
# "rules": [{"type": "rule_predefined_names"}]
# }
import smart_imports
smart_imports.all()
# мы не будем искать модуль с именем __file__
# хотя в коде эта переменная не проинициализирована
print(__file__)
Проверяет, есть ли рядом с текущим модулем (в том же каталоге) модуль с указанным именем. Если есть, импортирует его.
# конфиг:
# {
# "rules": [{"type": "rule_predefined_names"},
# {"type": "rule_local_modules"}]
# }
#
# код на файловой системе:
#
# my_package
# |-- __init__.py
# |-- a.py
# |-- b.py
# b.py
import smart_imports
smart_imports.all()
# Будет импортирован модуль "a.py"
print(a)
Пробует импортировать модуль непосредственно по имени. Например, модуль requests [6].
# конфиг:
# {
# "rules": [{"type": "rule_predefined_names"},
# {"type": "rule_global_modules"}]
# }
#
# ставим дополнительный пакет
#
# pip install requests
import smart_imports
smart_imports.all()
# Будет импортирован модуль requests
print(requests.get('http://example.com'))
Соотносит с именем конкретный модуль или его атрибут. Соответствие указывается в конфиге правила.
# конфиг:
# {
# "rules": [{"type": "rule_predefined_names"},
# {"type": "rule_custom",
# "variables": {"my_import_module": {"module": "os.path"},
# "my_import_attribute": {"module": "random", "attribute": "seed"}}}]
# }
import smart_imports
smart_imports.all()
# В примере исплользованы модули стандартной библиотеки
# Но аналогично можно импортировать любой другой модуль
print(my_import_module)
print(my_import_attribute)
Проверяет, не является ли имя модулем стандартной библиотеки. Например math [8] или os.path [9] который трансформируется в os_path
.
Работает быстрее чем правило импорта глобальных модулей, так как проверяет наличие модуля по закэшированному списку. Списки для каждой версии Python берутся отсюда: github.com/jackmaney/python-stdlib-list [10]
# конфиг:
# {
# "rules": [{"type": "rule_predefined_names"},
# {"type": "rule_stdlib"}]
# }
import smart_imports
smart_imports.all()
print(math.pi)
Импортирует модуль по имени, из пакета, ассоциированного с его префиксом. Удобно использовать, когда у вас есть несколько пакетов использующихся во всём коде. Например к модулям пакета utils
можно обращаться с префиксом utils_
.
# конфиг:
# {
# "rules": [{"type": "rule_predefined_names"},
# {"type": "rule_prefix",
# "prefixes": [{"prefix": "utils_", "module": "my_package.utils"}]}]
# }
#
# код на файловой системе:
#
# my_package
# |-- __init__.py
# |-- utils
# |-- |-- __init__
# |-- |-- a.py
# |-- |-- b.py
# |-- subpackage
# |-- |-- __init__
# |-- |-- c.py
# c.py
import smart_imports
smart_imports.all()
print(utils_a)
print(utils_b)
Если у вас есть одноимённые субпакеты в разных частях проекта (например, tests
или migrations
), для них можно разрешить искать модули для импорта по имени в родительских пакетах.
# конфиг:
# {
# "rules": [{"type": "rule_predefined_names"},
# {"type": "rule_local_modules_from_parent",
# "suffixes": [".tests"]}]
# }
#
# код на файловой системе:
#
# my_package
# |-- __init__.py
# |-- a.py
# |-- tests
# |-- |-- __init__
# |-- |-- b.py
# b.py
import smart_imports
smart_imports.all()
print(a)
Для модулей из конкретного пакета разрешает поиск импортов по имени в других пакетах (указанных в конфиге). В моём случае это правило оказалось полезным для случаев, когда не хотелось распространять работу предыдущего правила (Модуль из родительского пакета) на весь проект.
# конфиг:
# {
# "rules": [{"type": "rule_predefined_names"},
# {"type": "rule_local_modules_from_namespace",
# "map": {"my_package.subpackage_1": ["my_package.subpackage_2"]}}]
# }
#
# код на файловой системе:
#
# my_package
# |-- __init__.py
# |-- subpackage_1
# |-- |-- __init__
# |-- |-- a.py
# |-- subpackage_2
# |-- |-- __init__
# |-- |-- b.py
# a.py
import smart_imports
smart_imports.all()
print(b)
Добавить собственное правило довольно просто:
Пример можно найти в реализации текущий правил [13]
Пропали многострочные списки импортов в начале каждого исходника.
Cократилось количество строк. До перехода браузерки на smart imports в ней было 6688 строк отвечающих за импорт. После перехода осталось 2084 (по две строки smart_imports на каждый файл + 130 импортов, вызываемых явно из функций и подобных мест).
Приятным бонусом оказалась стандартизация имён в проекте. Код стало легче читать и легче писать. Пропала необходимость думать над именами импортируемых сущностей — есть несколько чётких правил, которым просто следовать.
Идея определять свойства кода по именам переменных мне нравится, поэтому буду пробовать развивать её как в рамках smart imports, так и в рамках других проектов.
Касательно smart imports, планирую:
Кроме того, мне интересно ваше мнение по поводу дефолтного поведения библиотеки и правил импорта.
Спасибо что осилили эту простыню текста :-D
Автор: Елецкий Алексей
Источник [14]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/python/324094
Ссылки в тексте:
[1] github: https://github.com/Tiendil/smart-imports
[2] pypi: https://pypi.org/project/smart-imports/
[3] coverage > 95%: https://coveralls.io/github/Tiendil/smart-imports?branch=develop
[4] PEP-0302: https://www.python.org/dev/peps/pep-0302/
[5] ast: https://docs.python.org/3/library/ast.html
[6] requests: https://pypi.org/project/requests/
[7] Пример сложного конфига: https://gist.github.com/Tiendil/837423689595e548b0b22f20c2c2149f
[8] math: https://docs.python.org/3/library/math.html
[9] os.path: https://docs.python.org/3/library/os.path.html
[10] github.com/jackmaney/python-stdlib-list: https://github.com/jackmaney/python-stdlib-list
[11] smart_imports.rules.BaseRule: https://github.com/Tiendil/smart-imports/blob/develop/smart_imports/rules.py#L100
[12] smart_imports.rules.register: https://github.com/Tiendil/smart-imports/blob/develop/smart_imports/rules.py#L336
[13] реализации текущий правил: https://github.com/Tiendil/smart-imports/blob/develop/smart_imports/rules.py
[14] Источник: https://habr.com/ru/post/459930/?utm_source=habrahabr&utm_medium=rss&utm_campaign=459930
Нажмите здесь для печати.