我如何在运行时检查python模块是否有效而不导入它?

我如何在运行时检查python模块是否有效而不导入它?,python,python-2.7,eval,python-import,python-importlib,Python,Python 2.7,Eval,Python Import,Python Importlib,我有一个包,其中只包含一个子包,我需要在运行时导入其中一个子包,但我需要测试它们是否有效。以下是我的文件夹结构: game/ __init__.py game1/ __init__.py constants.py ... game2/ __init__.py constants.py ... 目前,启动时运行的代码执行以下操作: import pkgutil import game as _game # Detect the known games f

我有一个包,其中只包含一个子包,我需要在运行时导入其中一个子包,但我需要测试它们是否有效。以下是我的文件夹结构:

game/
 __init__.py
 game1/
   __init__.py
   constants.py
   ...
 game2/
   __init__.py
   constants.py
   ...
目前,启动时运行的代码执行以下操作:

import pkgutil
import game as _game
# Detect the known games
for importer,modname,ispkg in pkgutil.iter_modules(_game.__path__):
    if not ispkg: continue # game support modules are packages
    # Equivalent of "from game import <modname>"
    try:
        module = __import__('game',globals(),locals(),[modname],-1)
    except ImportError:
        deprint(u'Error in game support module:', modname, traceback=True)
        continue
    submod = getattr(module,modname)
    if not hasattr(submod,'fsName') or not hasattr(submod,'exe'): continue
    _allGames[submod.fsName.lower()] = submod

因此,如果存在与导入语句与vis错误检查完全等效的语句-而不导入模块,我需要一个明确的答案。这是我的问题,许多回答者和评论者回答了不同的问题。

如果您想编译文件而不导入它(在当前解释器中),您可以使用:

上述代码将错误写入
std.error
。如果要引发异常,则必须将
doraise
设置为
True
(默认值
False
)。因此,您的代码将是:

from py_compile import compile, PyCompileError

try:
    compile('/path/to/valid/python/file.py', doraise=True)
    valid_file = True
except PyCompileError:
    valid_file = False
根据:

将源文件编译为字节码并写出字节码缓存文件。源代码从名为file的文件加载。字节码写入
cfile
,默认为file+
'c'
('o',如果在当前解释器中启用了优化)。如果指定了dfile,则在错误消息中它将用作源文件的名称,而不是文件名。如果
doraise
为true,则编译文件时遇到错误时会引发
PyCompileError
。如果
doraise
为false(默认值),则会将错误字符串写入
sys.stderr
,但不会引发异常

检查以确保未导入编译的模块(在当前解释器中):


也许您正在寻找
py\u compile
compileall
模块。
这里是文档:

您可以将所需的作为模块加载,并从程序中调用它。
例如:

import py_compile

try:
    py_compile.compile(your_py_file, doraise=True)
    module_ok = True
except py_compile.PyCompileError:
    module_ok = False

你不能有效地做你想做的事。为了查看包是否“有效”,您需要运行它——而不仅仅是检查它是否存在——因为它可能有错误或未满足的依赖项

使用
pycompile
compileall
仅测试是否可以编译python文件,而不是导入模块。这两者之间有很大的区别

  • 这种方法意味着您知道模块的实际文件结构--
    import foo
    可以表示
    /foo.py
    /foo/\uuuuu init\uuuuu.py
  • 这种方法不能保证模块在解释器的pythonpath中,或者是解释器将加载的模块。如果您在
    /site-packages/
    中有多个版本,或者python正在寻找许多可能的模块位置之一,那么事情就会变得棘手
  • 仅仅因为你的文件“编译”并不意味着它将“运行”。作为一个包,它可能有未满足的依赖项,甚至会引发错误
  • 假设这是您的python文件:

     from makebelieve import nothing
     raise ValueError("ABORT")
    
    上面的代码将被编译,但如果您导入它们。。。如果未安装
    makebelie
    ,它将引发ImportError,如果安装,它将引发ValueError

    我的建议是:

  • 导入包,然后卸载模块。要卸载它们,只需迭代
    sys.modules.keys()中的内容​​​。如果您担心加载的外部模块,您可以覆盖
    import
    ,以记录软件包加载的内容。我写的一个糟糕的分析包就是一个例子:[我忘了我从哪里想到要覆盖导入。可能是
    芹菜
    ?]正如一些人所指出的,卸载模块完全依赖于解释器——但几乎所有选项都有许多缺点

  • 使用子流程通过
    popen
    启动新的解释器。ie
    popen('python','-m','module_name')
    。如果您对每个需要的模块都这样做,那么这将有很大的开销(每个解释器和导入的开销),但是您可以编写一个“.py”文件来导入您需要的所有内容,并尝试运行它。在这两种情况下,您都必须分析输出——因为导入“有效”包可能会在执行过程中导致可接受的错误。我记不起子流程是否继承了您的环境变量,但我相信它确实继承了。子进程是一个全新的操作系统进程/解释器,因此模块将加载到该短暂进程的内存中


  • 我相信,
    imp.find_模块
    至少满足您的一些要求:

    快速测试表明它不会触发导入:

    >>> import imp
    >>> import sys
    >>> len(sys.modules)
    47
    >>> imp.find_module('email')
    (None, 'C:\\Python27\\lib\\email', ('', '', 5))
    >>> len(sys.modules)
    47
    >>> import email
    >>> len(sys.modules)
    70
    
    下面是我的一些代码(尝试对模块进行分类)中的一个示例用法:

    我们已经有了一个(免责声明:我没有编写该代码,我只是当前的维护者),其
    加载模块

    def load_module(self,fullname):
        if fullname in sys.modules:
            return sys.modules[fullname]
        else: # set to avoid reimporting recursively
            sys.modules[fullname] = imp.new_module(fullname)
        if isinstance(fullname,unicode):
            filename = fullname.replace(u'.',u'\\')
            ext = u'.py'
            initfile = u'__init__'
        else:
            filename = fullname.replace('.','\\')
            ext = '.py'
            initfile = '__init__'
        try:
            if os.path.exists(filename+ext):
                with open(filename+ext,'U') as fp:
                    mod = imp.load_source(fullname,filename+ext,fp)
                    sys.modules[fullname] = mod
                    mod.__loader__ = self
            else:
                mod = sys.modules[fullname]
                mod.__loader__ = self
                mod.__file__ = os.path.join(os.getcwd(),filename)
                mod.__path__ = [filename]
                #init file
                initfile = os.path.join(filename,initfile+ext)
                if os.path.exists(initfile):
                    with open(initfile,'U') as fp:
                        code = fp.read()
                    exec compile(code, initfile, 'exec') in mod.__dict__
            return mod
        except Exception as e: # wrap in ImportError a la python2 - will keep
            # the original traceback even if import errors nest
            print 'fail', filename+ext
            raise ImportError, u'caused by ' + repr(e), sys.exc_info()[2]
    
    因此,我想我可以用可重写的方法替换访问
    sys.modules
    缓存的部分,这些方法将在我的重写中不使用缓存:

    因此:

    并定义:

    class FakeUnicodeImporter(UnicodeImporter):
    
        _modules_to_discard = {}
    
        def _check_imported(self, fullname):
            return fullname in sys.modules or fullname in self._modules_to_discard
    
        def _get_imported(self, fullname):
            try:
                return sys.modules[fullname]
            except KeyError:
                return self._modules_to_discard[fullname]
    
        def _add_to_imported(self, fullname, mod):
            self._modules_to_discard[fullname] = mod
    
        @classmethod
        def cleanup(cls):
            cls._modules_to_discard.clear()
    
    然后,我在sys.meta_路径中添加了导入器,很好:

    importer = sys.meta_path[0]
    try:
        if not hasattr(sys,'frozen'):
            sys.meta_path = [fake_importer()]
        perform_the_imports() # see question
    finally:
        fake_importer.cleanup()
        sys.meta_path = [importer]
    
    对吧??错了

    Traceback (most recent call last):
      File "bash\bush.py", line 74, in __supportedGames
        module = __import__('game',globals(),locals(),[modname],-1)
      File "Wrye Bash Launcher.pyw", line 83, in load_module
        exec compile(code, initfile, 'exec') in mod.__dict__
      File "bash\game\game1\__init__.py", line 29, in <module>
        from .constants import *
    ImportError: caused by SystemError("Parent module 'bash.game.game1' not loaded, cannot perform relative import",)
    
    然而,这以一种新的方式爆发了——或者说是两种方式:

    • 对游戏/软件包的引用保存在sys.modules中的
      bash
      顶级软件包实例中:

      bash\
        __init__.py
        the_code_in_question_is_here.py
        game\
          ...
      
      因为
      game
      作为
      bash.game
      导入。该引用包含对所有
      game1、game2、
      子包的引用,因此这些子包从未被垃圾收集

    • 同一
      bash
      模块实例将对另一个模块(brec)的引用保留为
      bash.brec
      。此引用作为
      从导入。。在game\game1中导入brec而不触发导入,以更新
      SomeClass
      。然而,在另一个模块中,从…brec import SomeClass导入
      格式的导入确实触发了导入,并且brec模块的另一个实例在sys.modules中结束。该实例有一个未更新的
      SomeClass
      ,并使用属性进行了破坏
      class FakeUnicodeImporter(UnicodeImporter):
      
          _modules_to_discard = {}
      
          def _check_imported(self, fullname):
              return fullname in sys.modules or fullname in self._modules_to_discard
      
          def _get_imported(self, fullname):
              try:
                  return sys.modules[fullname]
              except KeyError:
                  return self._modules_to_discard[fullname]
      
          def _add_to_imported(self, fullname, mod):
              self._modules_to_discard[fullname] = mod
      
          @classmethod
          def cleanup(cls):
              cls._modules_to_discard.clear()
      
      importer = sys.meta_path[0]
      try:
          if not hasattr(sys,'frozen'):
              sys.meta_path = [fake_importer()]
          perform_the_imports() # see question
      finally:
          fake_importer.cleanup()
          sys.meta_path = [importer]
      
      Traceback (most recent call last):
        File "bash\bush.py", line 74, in __supportedGames
          module = __import__('game',globals(),locals(),[modname],-1)
        File "Wrye Bash Launcher.pyw", line 83, in load_module
          exec compile(code, initfile, 'exec') in mod.__dict__
        File "bash\game\game1\__init__.py", line 29, in <module>
          from .constants import *
      ImportError: caused by SystemError("Parent module 'bash.game.game1' not loaded, cannot perform relative import",)
      
      class FakeUnicodeImporter(UnicodeImporter):
      
          _modules_to_discard = set()
      
          def _check_imported(self, fullname):
              return fullname in sys.modules or fullname in self._modules_to_discard
      
          def _add_to_imported(self, fullname, mod):
              super(FakeUnicodeImporter, self)._add_to_imported(fullname, mod)
              self._modules_to_discard.add(fullname)
      
          @classmethod
          def cleanup(cls):
              for m in cls._modules_to_discard: del sys.modules[m]
      
      bash\
        __init__.py
        the_code_in_question_is_here.py
        game\
          ...