Лучший способ создания «виртуальных» модулей в Python 3 - PullRequest
0 голосов
/ 16 апреля 2020

Описание проблемы

У меня есть пакет с OS-зависимыми модулями. Эти модули имеют одинаковый API, каждый.

В коде клиента я могу сделать что-то вроде:

import platform

if platform.system() == "Windows":
  from my_package import os_win32 as my_package_platform
elif platform.system() == "Linux":
  from my_package import os_linux as my_package_platform

# use my_package_platform in client code

Однако я хотел бы сделать что-то вроде

from my_package import my_package_platform

и автоматически импортировать нужный модуль.

Возможное решение 1: Реальный модуль импортирует все из указанного c модуля

На самом деле есть модуль my_package_platform.py:

import platform
import importlib as imp
import importlib.util as impu
import sys

exports = ["symbol1", "symbol2"]

def update_globals(mod, var_list):
    for var in var_list:
        globals()[var] = mod.__dict__.get(var)


if platform.system() == "Windows":
    mod = imp.import_module(".os_win32", __package__)
elif platform.system() == "Linux":
    mod = imp.import_module(".os_linux", __package__)
else:
    raise NotImplementedError("not implemented for platform %s".format(platform.system()))

update_globals(mod, exports)

Однако это похоже на работу кажется немного хакерским. Также необходимо сохранить переменную exports.

Возможное решение 2: Использование манипуляции sys.meta_path для создания «виртуального модуля»

Добавьте следующее в __init__.py пакета :

import platform
import importlib.abc
import importlib.util as impu
import sys


class _VirtualModuleFinder(importlib.abc.MetaPathFinder):

    def find_spec(self, fullname: str, path, target=None):
        if fullname == __package__ + ".my_package_platform":
            if platform.system() == "win32":
                return impu.find_spec(__package__ + ".os_win32")
            elif platform.system() == "Linux":
                return impu.find_spec(__package__ + ".os_linux")
            else:
                return None


sys.meta_path.append(_VirtualModuleFinder())

Это работает довольно элегантно, однако кажется немного излишним. С другой стороны, дополнительный Finder вызывается только тогда, когда модуль не найден другими способами.

Одно предупреждение: модуль spe c будет содержать исходное имя модуля. (Что может быть ошибкой или функцией - я не до конца понимаю последствия).

Вопросы

  • Какие недостатки вы видите для каждого метода?
  • Есть ли лучшие методы или "лучшие практики"?

1 Ответ

0 голосов
/ 17 апреля 2020

Я думаю, что нашел решение, которое менее взломано и, кажется, работает лучше для угловых случаев:

import platform
import importlib as imp
import importlib.util as impu
import sys

mpp_name = impu.resolve_name(".my_package_platform", __package__)

if mpp_name not in sys.modules:
    if platform.system() == "win32":
        my_package_platform = imp.import_module(".os_win32", __package__)
    elif platform.system() == "Linux":
        my_package_platform = imp.import_module(".os_linux", __package__)
    else:
        raise ValueError("my_package_platform undefined for this platform")

    sys.modules[mpp_name] = my_package_platform

Работает даже с Mock.patch. Отсутствует только интеграция IDE с PyCharm - не знаю почему - я думаю, у них есть собственный алгоритм поиска модулей.

...