2010-03-20 12 views
6

Ich versuche das jetzt für fast zwei Stunden, ohne Glück.Mocking ImportError in Python

Ich habe ein Modul, das wie folgt aussieht:

try: 
    from zope.component import queryUtility # and things like this 
except ImportError: 
    # do some fallback operations <-- how to test this? 

später im Code:

try: 
    queryUtility(foo) 
except NameError: 
    # do some fallback actions <-- this one is easy with mocking 
    # zope.component.queryUtility to raise a NameError 

Irgendwelche Ideen?

EDIT:

Alex Vorschlag nicht zu funktionieren scheint:

[email protected] ~/work/ao.shorturl $ ./bin/test --coverage . 
Running zope.testing.testrunner.layer.UnitTests tests: 
    Set up zope.testing.testrunner.layer.UnitTests in 0.000 seconds. 


Error in test /home/aatiis/work/ao.shorturl/src/ao/shorturl/shorturl.txt 
Traceback (most recent call last): 
    File "/usr/lib64/python2.5/unittest.py", line 260, in run 
    testMethod() 
    File "/usr/lib64/python2.5/doctest.py", line 2123, in runTest 
    test, out=new.write, clear_globs=False) 
    File "/usr/lib64/python2.5/doctest.py", line 1361, in run 
    return self.__run(test, compileflags, out) 
    File "/usr/lib64/python2.5/doctest.py", line 1282, in __run 
    exc_info) 
    File "/usr/lib64/python2.5/doctest.py", line 1148, in report_unexpected_exception 
    'Exception raised:\n' + _indent(_exception_traceback(exc_info))) 
    File "/usr/lib64/python2.5/doctest.py", line 1163, in _failure_header 
    out.append(_indent(source)) 
    File "/usr/lib64/python2.5/doctest.py", line 224, in _indent 
    return re.sub('(?m)^(?!$)', indent*' ', s) 
    File "/usr/lib64/python2.5/re.py", line 150, in sub 
    return _compile(pattern, 0).sub(repl, string, count) 
    File "/usr/lib64/python2.5/re.py", line 239, in _compile 
    p = sre_compile.compile(pattern, flags) 
    File "/usr/lib64/python2.5/sre_compile.py", line 507, in compile 
    p = sre_parse.parse(p, flags) 
AttributeError: 'NoneType' object has no attribute 'parse' 



Error in test BaseShortUrlHandler (ao.shorturl) 
Traceback (most recent call last): 
    File "/usr/lib64/python2.5/unittest.py", line 260, in run 
    testMethod() 
    File "/usr/lib64/python2.5/doctest.py", line 2123, in runTest 
    test, out=new.write, clear_globs=False) 
    File "/usr/lib64/python2.5/doctest.py", line 1351, in run 
    self.debugger = _OutputRedirectingPdb(save_stdout) 
    File "/usr/lib64/python2.5/doctest.py", line 324, in __init__ 
    pdb.Pdb.__init__(self, stdout=out) 
    File "/usr/lib64/python2.5/pdb.py", line 57, in __init__ 
    cmd.Cmd.__init__(self, completekey, stdin, stdout) 
    File "/usr/lib64/python2.5/cmd.py", line 90, in __init__ 
    import sys 
    File "<doctest shorturl.txt[10]>", line 4, in fakeimport 
NameError: global name 'realimport' is not defined 

Aber es tut Arbeit wenn ich laufe:

>>> import __builtin__ 
>>> realimport = __builtin__.__import__ 
>>> def fakeimport(name, *args, **kw): 
...  if name == 'zope.component': 
...   raise ImportError 
...  realimport(name, *args, **kw) 
... 
>>> __builtin__.__import__ = fakeimport 

Wenn die Tests laufen der gleiche Code von der interaktiven Python-Konsole.

MEHR EDIT:

Ich verwende zope.testing und eine Testdatei, shorturl.txt, die die Tests alle spezifisch für diesen Teil meines Moduls hat. Zuerst importiere ich das Modul mit zope.component verfügbar, um zu demonstrieren & testen Sie die übliche Verwendung. Das Fehlen von zope.* Paketen wird als Edge-Case betrachtet, daher teste ich es später. Also, ich muss reload() mein Modul, nachdem zope.* nicht verfügbar, irgendwie.

Bisher habe ich sogar versucht, tempfile.mktempdir() und leer zope/__init__.py und zope/component/__init__.py Dateien im tempdir verwenden, dann tempdir zu sys.path[0] Einsetzen und die alten zope.* Pakete von sys.modules entfernen.

Hat auch nicht funktioniert.

NOCH MEHR EDIT:

In der Zwischenzeit habe ich das versucht:

>>> class NoZope(object): 
...  def find_module(self, fullname, path): 
...   if fullname.startswith('zope'): 
...    raise ImportError 
... 

>>> import sys 
>>> sys.path.insert(0, NoZope()) 

Und es funktioniert gut für den Namensraum des Testsuite (= für alle Einfuhren in shorturl.txt) , aber es wird nicht in meinem Hauptmodul ao.shorturl ausgeführt. Nicht einmal wenn ich es reload() it. Irgendeine Idee warum?

>>> import zope # ok, this raises an ImportError 
>>> reload(ao.shorturl) <module ...> 

zope.interfaces Importieren wirft ein ImportError, so geht es nicht zu dem Teil, wo ich zope.component importieren und es bleibt im ao.shorturl Namespace. Warum?!

>>> ao.shorturl.zope.component # why?! 
<module ...> 

Antwort

9

monkeypatch einfach in den builtins Ihre eigene Version von __import__ - es kann erhöhen, was Sie wollen, wenn es erkennt sie auf die spezifischen Module, für die Sie Fehler verspotten wollen genannt werden. Ausführliche Informationen finden Sie unter the docs.Grob:

try: 
    import builtins 
except ImportError: 
    import __builtin__ as builtins 
realimport = builtins.__import__ 

def myimport(name, globals, locals, fromlist, level): 
    if ...: 
     raise ImportError 
    return realimport(name, globals, locals, fromlist, level) 

builtins.__import__ = myimport 

Anstelle der ... können Sie name == 'zope.component' codieren, oder Dinge arrangieren flexibler mit einem Rückruf der eigenen, die Importe erhöhen auf Wunsch in verschiedenen Fällen machen kann, je nach Ihren spezifischen Testanforderungen, ohne fordert Sie auf, mehrere __import__ ähnliche Funktionen zu kodieren ;-).

Beachten Sie auch, dass wenn das, was Sie verwenden, statt import zope.component oder from zope.component import something, from zope import component ist, wird die name dann 'zope' sein, und 'component' wird dann das einzige Element in der fromlist.

bearbeiten: die Dokumentation für die __import__ Funktion sagen, dass der Name zu importieren ist builtin (wie in Python 3), aber in der Tat brauchen Sie __builtins__ - ich den Code oben bearbeitet haben, so dass es so oder so funktioniert .

+0

Ah, danke! Aus irgendeinem Grund habe ich versucht, 'def __import __()' zu tun, habe es aber nicht 'builtind .__ import__' zugewiesen; wie dumm von mir. Interessant, ich habe gerade Ihre Antwort hier gelesen: http://stackoverflow.com/questions/2216828/mock-y-of-from-x-import-y-in-doctest-python/2217512#2217512 - meinst du? Es würde diese Situation einfacher machen, wenn ich nicht queryUtility in den Bereich meines Moduls importiere. –

+2

@Attila, wenn Sie 'von Zope importieren Komponente' und dann' component.queryUtility' verwendet haben, würde es es einfacher machen, zum Beispiel das echte Ding eine Zeitlang zu verwenden, und eine gemockte/gefälschte Version an anderen Zeiten.Wie ich in dieser Antwort geschrieben habe, empfehle ich es als eine allgemeine Sache, und es ist ein Teil der Art, wie wir Python bei Google kodieren (manchmal ist eine 'as'-Klausel, um den Namen eines Imports zu verkürzen, natürlich gerechtfertigt, aber das tut nicht t ändern Sie die Semantik). –

+1

Wenn Sie 'von Zope Import-Komponente', BTW, Ihre '__import__'-ähnliche Funktion wird' Zope' als das 'name' Argument, und' 'Komponente'' als ein Element in der 'fromlist'-Argument (der einzige, es sei denn du machst 'von zope importieren, das, komponente' oder ähnliches ;-); also sicher sein, entsprechend zu triggern. –

3

Dies ist, was ich in meinen Unittests abgestimmt habe.

Es verwendet PEP-302 "New Import Hooks". (Achtung: das PEP-302 Dokument und die prägnante Release Notes I verknüpft ist nicht gerade genau.)

Ich benutze meta_path, weil es so früh wie möglich ist in der Importsequenz.

Wenn das Modul bereits importiert wurde (wie in meinem Fall, weil frühere Unittests es vortäuschen), müssen Sie es aus sys.modules entfernen, bevor Sie das reload auf dem abhängigen Modul ausführen.

Ensure we fallback to using ~/.pif if XDG doesn't exist. 

>>> import sys 

>>> class _(): 
... def __init__(self, modules): 
... self.modules = modules 
... 
... def find_module(self, fullname, path=None): 
... if fullname in self.modules: 
... raise ImportError('Debug import failure for %s' % fullname) 

>>> fail_loader = _(['xdg.BaseDirectory']) 
>>> sys.meta_path.append(fail_loader) 

>>> del sys.modules['xdg.BaseDirectory'] 

>>> reload(pif.index) #doctest: +ELLIPSIS 
<module 'pif.index' from '...'> 

>>> pif.index.CONFIG_DIR == os.path.expanduser('~/.pif') 
True 

>>> sys.meta_path.remove(fail_loader) 

Wo der Code innerhalb pif.index wie folgt aussieht:

try: 
    import xdg.BaseDirectory 

    CONFIG_DIR = os.path.join(xdg.BaseDirectory.xdg_data_home, 'pif') 
except ImportError: 
    CONFIG_DIR = os.path.expanduser('~/.pif') 

Zur Beantwortung der Frage, warum das neu nachgeladen Modul verfügt über Eigenschaften der alten und neuen Lasten, sind hier zwei Beispiel Dateien.

Die erste ist ein Modul y mit einem Importfehlerfall.

# y.py 

try: 
    import sys 

    _loaded_with = 'sys' 
except ImportError: 
    import os 

    _loaded_with = 'os' 

Die zweite ist x, die wie Griffe zu verlassen demonstriert für ein Modul seine Eigenschaften beeinflussen können, wenn neu geladen werden.

# x.py 

import sys 

import y 

assert y._loaded_with == 'sys' 
assert y.sys 

class _(): 
    def __init__(self, modules): 
     self.modules = modules 

    def find_module(self, fullname, path=None): 
     if fullname in self.modules: 
      raise ImportError('Debug import failure for %s' % fullname) 

# Importing sys will not raise an ImportError. 
fail_loader = _(['sys']) 
sys.meta_path.append(fail_loader) 

# Demonstrate that reloading doesn't work if the module is already in the 
# cache. 

reload(y) 

assert y._loaded_with == 'sys' 
assert y.sys 

# Now we remove sys from the modules cache, and try again. 
del sys.modules['sys'] 

reload(y) 

assert y._loaded_with == 'os' 
assert y.sys 
assert y.os 

# Now we remove the handles to the old y so it can get garbage-collected. 
del sys.modules['y'] 
del y 

import y 

assert y._loaded_with == 'os' 
try: 
    assert y.sys 
except AttributeError: 
    pass 
assert y.os 
+0

Großartig, jetzt habe ich es geschafft, einen 'ImportError' zu erstellen, was alles ist, was ich brauche. Das Interessante ist folgendes: wenn ich 'ao.shorturl' neu lade, und darin habe ich' versuchen: import zope.component, zope.interface; außer ImportError: fallback() ', und ich bekomme den ersten' ImportError' für 'zope.component', ** zope.interface wird weiterhin in ao.shorturl (ao.shorturl.zope.interface) ** verfügbar sein. Warum das? –

+0

Ich habe gerade einen weiteren Abschnitt hinzugefügt, der beschreibt, warum das passiert. tl; dr, müssen Sie 'del ao.shorturl' vor Ihrem 'reload'. –

0

Wenn Sie nichts dagegen haben Ihr Programm selbst zu ändern, können Sie auch den Import Aufruf in Funktion setzen könnte und dass in Ihren Tests patchen.