如何获取当前的“包”名称? (设置.py)
Posted
技术标签:
【中文标题】如何获取当前的“包”名称? (设置.py)【英文标题】:How do I get the current 'package' name? (setup.py) 【发布时间】:2020-06-06 14:57:26 【问题描述】:如何获取当前最顶层的包,即 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
的相关部分也似乎很麻烦。
我尝试过的其他解决方案——通过在我的tests
python [sub] 包中的unittest.TestCase
继承class
中调用它们——包括检查sys.modules[__name__]
、inspect.getmodule
和inspect.stack
,以及这些问题的答案:
顺便说一句:如果你想知道我为什么要包名……这样我就可以运行类似的东西:
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')
【问题讨论】:
似乎您混淆了项目名称和***包的名称。它们通常是相同的,但不匹配的情况仍然很多。 在test_foo.py
中应该已经在包本身中定义了,直接使用包名即可。
@metatoaster 它在包本身的哪里定义? - 我在魔法变量inspect
或sys.modules
中找不到它。
这将是您在setup.py
中定义的任何内容;包系统在事后被固定在 Python 上,并且鉴于包名称的字符串通常不会更改,因此将相同的 str
值硬编码到 test_foo.py
而不是试图提出来要容易得多用 Python 解决这个问题的方法。
或者,您可以利用 EntryPoints - 在 setup.py
创建您自己的入口点,其值将引用具有您需要的资源的模块。从您的代码(或任何其他包,就此而言)只需查询,然后使用该结果输入resource_filename
以获得所需的内容。
【参考方案1】:
不完全确定更大的目标是什么,但也许您可能有兴趣了解importlib.resources
和importlib.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://***.com/a/22845276/11138259 https://***.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()
函数,这看起来会变得更容易:
【讨论】:
我写了一个import ast
的答案来计算SomeProject
的价值……它不是超级通用的,但确实适用于一些常见的用例(并且可以简单地扩展更多)。【参考方案2】:
我一直在研究的解决方案:
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://***.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://***.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__)))
【讨论】:
我没有看到任何用例。setup.py
文件从未安装,因此根本无法读取。当然,当它直接从源代码存储库的克隆中执行时,它看起来好像可以工作,但是一旦安装,它就不可能工作了。我错过了什么吗?我真的不明白这段代码的意义。如果你想访问项目的名称,有更好的技术:bitbucket.org/pypa/distlib/issues/102/… - ***.com/a/22845276/11138259 - ***.com/a/56032725/11138259 - 但是为什么呢?
如果你真的想从setup.py
读取项目的名称,那么可以使用this technique 通过执行(部分)设置来检索元数据。或者将元数据放在setup.cfg
文件中,这样parse 比setup.py
更容易。
关于setup.py
安装后不可用的好点。所以我真的需要某种if
调节并在安装后使用不同的解决方案。我会尝试this answer you mentioned...虽然我担心它可能会在两个包之间混淆。
您应该只读取已安装发行版的元数据(develop 和/或 editable 模式,也可以使用此元数据)。如果元数据不可读,则表明存在打包或安装问题,没有理由回退到从setup.py
读取。我看不出它是如何在两个已安装的发行版之间混淆的。
@sinoroc - 从setup.py
读取的回退是如果包没有安装,或者如果你有cd
'd 到包源目录并且正在运行python -m <mod name>
.否则它不会在pkg_resources.resource_filename
中选择正确的文件...以上是关于如何获取当前的“包”名称? (设置.py)的主要内容,如果未能解决你的问题,请参考以下文章