Package: test-imports
Test Python importsA Python package for failing and mocking imports in automated tests.Note:This package was made with CPython in mind. There are no guarantees that it will work with other versions.ContentFailing importsMocking importsFailing importsThis was the original motive to create the package. I needed to test the behaviour of another package that had optional support forPIL, and I wanted the tests to check the behaviour both when the package is present and when it is not.The usage is straightforward:fromtest_importsimportfail_importsdeff()->bool:try:importPIL.ImageexceptImportError:returnFalseelse:returnTruedeftest_success()->None:assertf()isTrue@fail_imports("PIL")deftest_decorator_fail()->None:assertf()isFalsedeftest_context_manager_fail()->None:withfail_imports("PIL"):assertf()isFalsetest_success()test_decorator_fail()test_context_manager_fail()All positional arguments infail_importsare treated as the modules whose imports are to fail. Each of them can be:a compiled regular expression, matched (usingre.match, i.e., anchored at the beginning of the string) against names of the modules being imported; ora string, which is matched literally, except for the asterisk, which is used to match any substring. The matching is done on complete strings.For example,"foo.bar*"will matchfoo.barandfoo.bard, but notfoodbar(because dot is matched literally, not as in regular expressions). Further,"foo.*r"will matchfoo.bar, but notfoo.bardbecause only the whole strings are matched. If you wantfoo.bardto match, the expression needs to befoo.*r*, or you can supply a compiled regular expression that would match it.This function also supports some customisation through keyword-only arguments:hide_modulesis a sequence of module names matching patterns (as described above) that won't be failed, but will be removed fromsys.modules, thus causing them to "reload". This helps test imports inside those modules because, if they are not "reloaded", their imports are not re-executed.exceptionis either a class or an instance of the exception to be raised when an import fails. Unsurprisingly, this defaults toModuleNotFoundError.debugis a Boolean flag. If set toTrue, the package will produce extra prints in an attempt to help with its usage.Mocking importsLike more "normal" mocking, the mocking of modules is used to replace one object with another one, pretending to be the original. On the surface, mocking imports is easy:fromtest_importsimportmock_importsdeftest_success()->None:importmathasserthasattr(math,"sin")assertnothasattr(math,"digits")@mock_imports(math="string")deftest_decorator_fail()->None:importmathassertnothasattr(math,"sin")asserthasattr(math,"digits")deftest_context_manager_fail()->None:withmock_imports(math="string"):importmathassertnothasattr(math,"sin")asserthasattr(math,"digits")test_success()test_decorator_fail()test_context_manager_fail()However, mocking definitions are a bit more complicated than the ones forfail_imports.First, there is a problem of mocking modules inside packages. One cannot dofail_imports(PIL.Image="mock_pil_image")because dots cannot be a part of arguments' names. Instead, we use double underscores (similar to, for example, Django):withmock_imports(PIL__Image="math"):fromPILimportImageassertnothasattr(Image,"new")asserthasattr(Image,"sin")There is also a potential problem of collisions in names between the function's arguments and mocked modules. For example, there is a packagedebug, which we could not mock ifdebugwas used as a keyword argument to turn on debugging outputs. That's why argument names are prefixed with"TI_"(so,TI_debug=Trueinstead ofdebug=True).Both of these can still cause potential conflicts. Some module can have double underscores in its name and some package's name could start withTI_. To account for these cases,mock_importstakes two positional-only arguments:prefixis the prefix for keyword-only arguments that are recognised by this function. For example, ifprefixis set to its default version"TI_", then the debugging value is assigned asTI_debug. Any names beginning with"TI_"that are not recognised as arguments are considered invalid.In other words, if you want to mock a module with a name starting with"TI_"(for example,TI_module), you need to change this prefix to something else and adjust keyword-arguments accordingly.dotis the string used instead of dot in module names.So, these two calls are equivalent:mock_imports(PIL__Image="mock_pil_image",TI_debug=True)# andmock_imports("PREFIX_","__xxx__",PIL__xxx__Image="mock_pil_image",PREFIX_debug=True,)This still does not allow matching with asterisk or with regular expressions, but it would hardly make sense to do so (mocking multiple different modules with the same one). However, if really needed, one can use the following Python "trick":mock_imports(**{"tests.module*":math})This will will loadmathinstead of any module with a full name beginning with"tests.module".The remaining arguments are keyword only (always prefix their names withprefix!):hide_modulesis as above: a sequence of module names matching patterns (as described above) that won't be failed, but will be removed fromsys.modules, thus causing them to "reload". This helps test imports inside those modules because, if they are not "reloaded", their imports are not re-executed.reloadis a Boolean flag. IfTrue, every imported module or its mock is reloaded on import. Depending on how they are written, this may help reset mocked modules from previous tests.debugis a Boolean flag. If set toTrue, the package will produce extra prints in an attempt to help with its usage.