Как стать автором
Обновить
SM Lab
Рассказываем про ИТ в «Спортмастере»

Динамический импорт модулей в Python

Время на прочтение3 мин
Количество просмотров8.3K

Давайте представим ситуацию, когда вам нужно установить на все виртуальные машины (агенты сервера сборки) определенный пакет Python. Но вы не можете изменить образ агента, а загрузка, к примеру из pypi.org или github.com непроверенных пакетов, ограничена. Как тут не вспомнить последние новости про вредоносные изменения в пакете nmp или более свежую информацию про PyPi.

Python использует подход под названием EAFP — Easier to ask for forgiveness, than permission (легче попросить прощения, чем разрешения). Это значит, что проще предположить, что что-то существует (к примеру, словарь в словаре, или в нашем случае модуль в системе) или получить ошибку в противном случае.

Этот подход, развитый в PEP-0302, позволяет делать хук импорта модулей, что в итоге приводит нас к возможности написания следующего кода:

      from <какое-то хранилище> import <модуль>

Поскольку я часто сталкиваюсь с автоматизацией развертывания CI/CD с помощью Bamboo, то для меня естественным решением было разместить код выверенного модуля в артефактах JFrog и написать (согласно PEP-0302) классы finder и loader для загрузки и установки модулей. В моем случае код импорта модулей выглядит следующим образом:

      from <artifactory_local.path_to_ artifactory> import <module_tar_gz>

Согласно PEP-0302 в протоколе участвуют два класса: finder и loader.

Продемонстрирую модуль amtImport.py, реализующий работу этих классов:

import sys
import pip
import os
import requests
import shutil

class IntermediateModule:
    """
    Module for paths like `artifactory_local.path`
    """

    def __init__(self, fullname):
        self.__package__ = fullname
        self.__path__ = fullname.split('.')
        self.__name__ = fullname

class ArtifactoryFinder:
    """
    Handles `artifactory_local....` modules
    """
    def find_module(self, module_name, package_path):
        if module_name.startswith('artifactory_local'):
            return ArtifactoryLoader()

class ArtifactoryLoader:
    """
    Installs and imports modules from artifactory
    """
    artifactory_modules: list = []

    def _is_installed(self, fullname):
        try:
            self._import_module(fullname)
            ArtifactoryLoader.artifactory_modules.append(fullname)
            return True
        except ImportError:
            return False
    
    def _import_module(self, fullname):
        actual_name = '.'.join(fullname.split('.')[2:])
        actual_name = actual_name.replace('_', '.').split('.')[0]
        return __import__(actual_name)

    def _install_module(self, fullname):
        if not self._is_installed(fullname):
            actual_name = '.'.join(fullname.split('.')[2:])
            url = fullname.replace('artifactory_local', '').replace(actual_name, '').replace('_', '/')
            actual_name = actual_name.replace('_', '.')
            url = 'https://artifactory.app.local' + url.replace('.', '/') + actual_name
            auth = (os.getenv("bamboo_artifactory_user_name"), os.getenv("bamboo_artifactory_access_token_secret"))
            file_name = f"{os.getenv('bamboo_build_working_directory')}/{actual_name}"
            with requests.get(url=url, auth=auth, stream=True) as r:
                if r.status_code != 200:
                    raise Exception(f"Status code {r.status_code}: {r.reason}")              
                with open(file_name, 'wb') as f:
                    shutil.copyfileobj(r.raw, f)
            pip.main(['install', file_name])
            ArtifactoryLoader.artifactory_modules.append(fullname)

    def _is_repository_path(self, fullname):
        return fullname.count('.') == 2

    def _is_intermediate_path(self, fullname):
        return fullname.count('.') < 2

    def load_module(self, fullname):
        if self._is_repository_path(fullname):
            self._install_module(fullname)

        if self._is_intermediate_path(fullname):
            module = IntermediateModule(fullname)
        else:
            module = self._import_module(fullname)

        sys.modules[fullname] = module

sys.meta_path.append(ArtifactoryFinder())

(Код тестировался для Python 3.6)

Итог: динамический импорт из артефактов можно лаконично оформить следующим кодом:

from amtImport import ArtifactoryLoader
# pyright: reportMissingImports=false, reportUnusedImport=false
from artifactory_local.artifactory_amt_Sealed import smbprotocol_1_9_0_tar_gz

if "artifactory_local.artifactory_amt_Sealed.smbprotocol_1_9_0_tar_gz" in ArtifactoryLoader.artifactory_modules:
    print("Package successfully loaded")

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

P.S. Основой для данной статьи послужил пакет import_from_github_com.

Теги:
Хабы:
Всего голосов 13: ↑8 и ↓5+5
Комментарии8

Публикации

Информация

Сайт
xn----8sbd2bd3a.xn--p1ai
Дата регистрации
Дата основания
Численность
1 001–5 000 человек
Местоположение
Россия
Представитель
Алина Айсина