如何获取当前'package'名称?(setup.py)



如何获取当前最顶层的包,即setup.py中定义的名称?

这是我的tree:

.
|-- README.md
|-- the_project_name_for_this_pkg
|   |-- __init__.py
|   |-- __main__.py
|   |-- _config
|   |   `-- foo.conf
|   |-- _data
|   |   `-- logging.yml
|   `-- tests
|       |-- __init__.py
|       `-- test_foo.py   <--- # executing from here
|-- requirements.txt
`-- setup.py
4 directories, 9 files

到目前为止,我工作的唯一解决方案是:

import os
import sys

os.path.basename(sys.path[1])

但这显然是一个糟糕的解决方案。其他解决方案,如在我最上面的__init__.py文件中有一个__name__,并使用ast.parse读取setup.py的相关部分,似乎也很麻烦。

我尝试过的其他解决方案——通过在testspython[sub]包中继承classunittest.TestCase中调用它们——包括检查sys.modules[__name__]inspect.getmodule&inspect.stack,以及这些问题的答案:

  • Python-获取根项目结构的路径
  • 获取完整的软件包模块名称
  • 在Python中获取对象的完全限定类名
  • 如何访问Python中当前执行的模块或类名
  • 获取完整的调用方名称(package.module.function((Python配方(
  • https://docs.python.org/2/library/modulefinder.html

BTW:如果你想知道我为什么想要包名……这样我就可以运行以下内容:

import pkg_resources

version   = pkg_resources.require('the_project_name_for_this_pkg')[0].version
data_file = path.join(resource_filename('the_project_name_for_this_pkg', '__init__.py'),
'_config', 'data_file.txt')

不完全确定更大的目标是什么,但您可能有兴趣阅读有关importlib.resourcesimportlib.metadata的内容。

类似以下内容:

import importlib.metadata
import importlib.resources
version = importlib.metadata.version('SomeProject')
data = importlib.resources.files('top_level_package.sub_package').joinpath('file.txt').read_text()

更普遍地说,从代码中100%可靠地检测项目名称(SomeProject(几乎是不可能的(或者不值得做这么多工作(。只是硬编码更容易。

尽管如此,这里还是有一些技术和想法可以从其中一个模块中检索项目名称:

  • https://bitbucket.org/pypa/distlib/issues/102/getting-the-distribution-that-a-module
  • https://stackoverflow.com/a/22845276/11138259
  • https://stackoverflow.com/a/56032725/11138259

更新

我相信像下面这样的函数应该返回包含当前文件的已安装发行版的名称:

import pathlib
import importlib_metadata
def get_project_name():
for dist in importlib_metadata.distributions():
try:
relative = pathlib.Path(__file__).relative_to(dist.locate_file(''))
except ValueError:
pass
else:
if relative in dist.files:
return dist.metadata['Name']
return None

更新(2021年2月(:

由于importlib_metadata:中新添加了packages_distributions()功能,这看起来可能会变得更容易

  • https://importlib-metadata.readthedocs.io/en/stable/using.html#package-分配
  • https://github.com/python/importlib_metadata/pull/287/files

我一直在研究的解决方案:

from os import listdir, path
from contextlib import suppress
import ast

def get_first_setup_py(cur_dir):
if 'setup.py' in listdir(cur_dir):
return path.join(cur_dir, 'setup.py')
prev_dir = cur_dir
cur_dir = path.realpath(path.dirname(cur_dir))
if prev_dir == cur_dir:
raise StopIteration()
return get_first_setup_py(cur_dir)

setup_py_file_name = get_first_setup_py(path.dirname(__file__))

第一道:

def get_from_setup_py(setup_file): # mostly https://stackoverflow.com/a/47463422
import importlib.util

spec = importlib.util.spec_from_file_location('setup', setup_file)
setup = importlib.util.module_from_spec(spec)
spec.loader.exec_module(setup)
# And now access it
print(setup)

这个选择确实奏效了。因此,我回到了问题中提到的ast解决方案,并获得了第二个通行证:

def parse_package_name_from_setup_py(setup_py_file_name):
with open(setup_py_file_name, 'rt') as f:
parsed_setup_py = ast.parse(f.read(), 'setup.py')
# Assumes you have an `if __name__ == '__main__'` block:
main_body = next(sym for sym in parsed_setup_py.body[::-1]
if isinstance(sym, ast.If)).body
setup_call = next(sym.value
for sym in main_body[::-1]
if isinstance(sym, ast.Expr)
and isinstance(sym.value, ast.Call)
and sym.value.func.id in frozenset(('setup',
'distutils.core.setup',
'setuptools.setup')))
package_name = next(keyword
for keyword in setup_call.keywords
if keyword.arg == 'name'
and isinstance(keyword.value, ast.Name))
# Return the raw string if it is one
if isinstance(package_name.value, ast.Str):
return package_name.value.s
# Otherwise it's a variable defined in the `if __name__ == '__main__'` block:
elif isinstance(package_name.value, ast.Name):
return next(sym.value.s
for sym in main_body
if isinstance(sym, ast.Assign)
and isinstance(sym.value, ast.Str)
and any(target.id == package_name.value.id
for target in sym.targets)
)
else:
raise NotImplemented('Package name extraction only built for raw strings & '
'assigment in the same scope that setup() is called')

第三遍(适用于已安装版本和开发版本(:

# Originally from https://stackoverflow.com/a/56032725;
# but made more concise and added support whence source
class App(object):
def get_app_name(self) -> str:
# Iterate through all installed packages and try to find one
# that has the app's file in it
app_def_path = inspect.getfile(self.__class__)
with suppress(FileNotFoundError):
return next(
(dist.project_name
for dist in pkg_resources.working_set
if any(app_def_path == path.normpath(path.join(dist.location, r[0]))
for r in csv.reader(dist.get_metadata_lines('RECORD')))),
None) or parse_package_name_from_setup_py(
get_first_setup_py(path.dirname(__file__)))

最新更新