Python: перехватить действие загрузки класса - PullRequest
5 голосов
/ 23 декабря 2009

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

Причина: я работаю над устаревшим кодом. Мне нужно написать некоторый код модульного теста, прежде чем я начну улучшать / реорганизовывать. Однако код импортирует определенный модуль, который не будет работать в настройках модульного теста. (Из-за зависимости сервера базы данных)

Псевдо-код:

from LegacyDataLoader import load_me_data
...
def do_something():
   data = load_me_data()

Таким образом, в идеале, когда python оправдывает строку импорта выше в модульном тесте, вместо него загружается альтернативный класс, говорит MockDataLoader.

Я все еще использую 2.4.3. Я полагаю, есть хук импорта, которым я могу манипулировать

Редактировать

Спасибо большое за ответы. Все они очень полезны.

Один конкретный тип предположений касается манипуляции с PYTHONPATH. Это не работает в моем случае. Так что я подробно остановлюсь здесь.

Исходная кодовая база организована таким образом

./dir1/myapp/database/LegacyDataLoader.py
./dir1/myapp/database/Other.py
./dir1/myapp/database/__init__.py
./dir1/myapp/__init__.py

Моя цель - улучшить класс Other в модуле Other. Но так как это устаревший код, я не чувствую себя комфортно, работая над ним, не привязав сначала к нему набор тестов.

Теперь я ввожу код этого юнит теста

./unit_test/test.py

Содержание просто:

from myapp.database.Other import Other

def test1():
            o = Other()
            o.do_something()

if __name__ == "__main__":
            test1()

Когда CI-сервер запускает вышеуказанный тест, тест не пройден. Это связано с тем, что класс Other использует LegacyDataLoader, а LegacydataLoader не может установить соединение базы данных с сервером БД из поля CI.

Теперь давайте добавим поддельный класс, как предложено:

./unit_test_fake/myapp/database/LegacyDataLoader.py
./unit_test_fake/myapp/database/__init__.py
./unit_test_fake/myapp/__init__.py

Измените PYTHONPATH на

export PYTHONPATH=unit_test_fake:dir1:unit_test

Теперь тест не пройден по другой причине

  File "unit_test/test.py", line 1, in <module>
    from myapp.database.Other import Other
ImportError: No module named Other

Это как-то связано с тем, как python разрешает классы / атрибуты в модуле

Ответы [ 4 ]

3 голосов
/ 23 декабря 2009

Вы можете перехватывать операторы import и from ... import, определяя свою собственную функцию __import__ и назначая ее для __builtin__.__import__ (обязательно сохраните предыдущее значение, поскольку ваше переопределение, несомненно, захочет делегировать его; и вам нужно import __builtin__, чтобы получить модуль встроенных объектов).

Например (специфично для Py2.4, поскольку это то, о чем вы спрашиваете), сохраните в aim.py следующее:

import __builtin__
realimp = __builtin__.__import__
def my_import(name, globals={}, locals={}, fromlist=[]):
  print 'importing', name, fromlist
  return realimp(name, globals, locals, fromlist)
__builtin__.__import__ = my_import

from os import path

и сейчас:

$ python2.4 aim.py
importing os ('path',)

Так что это позволяет вам перехватывать любой конкретный запрос на импорт, который вы хотите, и изменять импортированные модули [s], как вы хотите, прежде чем их возвращать - см. Спецификации здесь . Это тот тип "крючка", который вы ищете, верно?

1 голос
/ 23 декабря 2009

Есть более чистые способы сделать это, но я предполагаю, что вы не можете изменить файл, содержащий from LegacyDataLoader import load_me_data.

Самое простое, что можно сделать, это, вероятно, создать новый каталог под названием testing_shims и создать в нем файл LegacyDataLoader.py. В этом файле определите все фальшивые load_me_data, которые вам нравятся. При запуске модульных тестов поместите test_shims в переменную среды PYTHONPATH в качестве первого каталога. Кроме того, вы можете изменить ваш тестовый бегун так, чтобы в качестве первого значения в sys.path.

вставлялся test_shims.

Таким образом, ваш файл будет найден при импорте LegacyDataLoader, и ваш код будет загружен вместо реального кода.

1 голос
/ 23 декабря 2009

Оператор import просто извлекает данные из sys.modules, если там найдено подходящее имя, поэтому самое простое - убедиться, что вы вставляете свой собственный модуль в sys.modules под целевым именем до что-нибудь еще пытается импортировать реальную вещь.

# in test code
import sys
import MockDataLoader
sys.modules['LegacyDataLoader'] = MockDataLoader

import module_under_test

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

# in test code
import module_under_test

def mock_load_me_data():
    # do mock stuff here

module_under_test.load_me_data = mock_load_me_data

Это просто заменяет соответствующее имя прямо в самом модуле, поэтому, когда вы вызываете тестируемый код, предположительно do_something() в вашем вопросе, это вызывает вашу ложную подпрограмму.

1 голос
/ 23 декабря 2009

Что ж, если импорт завершится неудачно из-за исключения, вы можете поместить его в попытку ... кроме цикла:

try:
    from LegacyDataLoader import load_me_data
except: # put error that occurs here, so as not to mask actual problems
    from MockDataLoader import load_me_data

Это то, что вы ищете? Если это не удастся, но не вызовет исключения, вы можете запустить модульное тестирование с помощью специального тега командной строки, например --unittest, например:

import sys
if "--unittest" in sys.argv:
    from MockDataLoader import load_me_data
else:
    from LegacyDataLoader import load_me_data
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...