仅使用 PyPI 包名称以编程方式获取模块名称

Get module name programmatically with only PyPI package name

我想根据软件包名称列表以编程方式安装和导入软件包。对于大多数包来说,这不是问题,因为包名和模块名是相同的。

但是,PyYAML 包是一个例外,因为它的模块被简单地调用 yaml,而且可能还有更多例外。

这是我用来安装和导入 packages/modules 的 python 函数:

def install_and_import(package):
    import importlib
    try:
        importlib.import_module(package) #needs module name!
    except ImportError:
        import pip
        pip.main(['install', package]) #needs package name
    finally:
        globals()[package] = importlib.import_module(package)

为列表中的每个包调用函数,['backoff', 'pyyaml'](从 requirements.txt 解析),我得到:

Collecting backoff
Installing collected packages: backoff
Successfully installed backoff-1.4.3
Collecting pyyaml
Installing collected packages: pyyaml
Successfully installed pyyaml-3.12
[...Trackback...]
ModuleNotFoundError: No module named 'pyyaml'

有没有办法,只给定包名(例如,pyyaml),找出我实际需要导入的模块的名称(例如,yaml)?

在模块名称中使用 distlib (pip install distlib) 和一个 hacky "guess" (这可以改进,但想在我必须得到之前给你我想出的东西回到其他事情上!)

import os.path
import sys

import distlib.database


def to_module(s):
    parts = os.path.splitext(s)[0].split(os.sep)
    if s.endswith('.py'):
        if parts[-1] == '__init__':
            parts.pop()
    elif s.endswith('.so'):
        parts[-1], _, _ = parts[-1].partition('.')
    return '.'.join(parts)


def main():
    dp = distlib.database.DistributionPath()
    dist = dp.get_distribution(sys.argv[1])
    for f, _, _ in dist.list_installed_files():
        if f.endswith(('.py', '.so')):
            print(to_module(f))


if __name__ == '__main__':
    exit(main())

to_module 很容易解释,我使用 DistributionPath()("installed" 模块的表示)来查询已安装的特定包。我从中列出文件,如果它们看起来像模块,则将它们转换为模块。请注意,这不会捕捉到 six(动态添加 six.moves 模块)之类的东西,但它是一个非常好的 first-order 近似值。

我在这里也对 posix 做出假设,对于您需要调整的其他平台(例如 windows,我相信它将使用 .pyd)。

示例输出:

$ python test.py pyyaml
_yaml
yaml
yaml.composer
yaml.constructor
yaml.cyaml
yaml.dumper
yaml.emitter
yaml.error
yaml.events
yaml.loader
yaml.nodes
yaml.parser
yaml.reader
yaml.representer
yaml.resolver
yaml.scanner
yaml.serializer
yaml.tokens
$ python test.py coverage
coverage.pickle2json
coverage.execfile
coverage.python
coverage.summary
coverage.html
coverage.plugin
coverage.pytracer
coverage.config
coverage.__main__
coverage.data
coverage.debug
coverage.annotate
coverage.backward
coverage.parser
coverage.misc
coverage.files
coverage.multiproc
coverage.backunittest
coverage.env
coverage
coverage.control
coverage.cmdline
coverage.results
coverage.version
coverage.plugin_support
coverage.templite
coverage.collector
coverage.xmlreport
coverage.report
coverage.phystokens
coverage.bytecode
coverage.tracer
coverage.fullcoverage.encodings

基于 Anthony Sottile 的 ,我创建了一个简化版本以从包中提供一个模块。大多数适合我的情况的软件包都有一个主模块。 (当然,处理具有多个 "main" 模块的更复杂的包会更好。)

Windows 上进行测试,我发现 .list_installed_files() 存在一些问题(其中一些已在 "solution" 中解决):

  1. os.sep 未根据分发类型正确拆分文件名。 (鸡蛋走 os.sep 而轮子走 posix 方向。)
  2. 对于某些发行版,您可以获得完整路径(看起来像鸡蛋)。这会导致疯狂的模块名称猜测(例如,'C:.Users.Username.AppData.RestOfPath.File')。

这会搜索第一个__init__.py来通知模块名称。如果找不到,它只是 returns 包名称(对我来说涵盖了 90% 的情况)。

def package_to_module(package):
    dp = distlib.database.DistributionPath(include_egg=True)
    dist = dp.get_distribution(package)
    if dist is None:
        raise ModuleNotFoundError
    module = package # until we figure out something better
    for filename, _, _ in dist.list_installed_files():
        if filename.endswith(('.py')):
            parts = os.path.splitext(filename)[0].split(os.sep)
            if len(parts) == 1: # windows sep varies with distribution type
                parts = os.path.splitext(filename)[0].split('/')
            if parts[-1].startswith('_') and not parts[-1].startswith('__'):
                continue # ignore internals
            elif filename.endswith('.py') and parts[-1] == '__init__':
                module = parts[-2]
                break
    return module

一些示例:

>>> package_to_module("pyyaml")
'yaml'
>>> package_to_module("click")
'click'
>>> package_to_module("six")
'six'
>>> package_to_module("pip")
'pip'
>>> package_to_module("doesntexist")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 5, in package_to_module
ModuleNotFoundError