【问题标题】:How to access text file in project root from python package under `src/` directory如何从`src/`目录下的python包访问项目根目录中的文本文件
【发布时间】:2018-04-03 11:35:10
【问题描述】:

我希望我的包的版本号位于一个地方,所有需要它的东西都可以引用它。

我在Single Sourcing the Package Version 的 Python 指南中找到了一些建议,并决定尝试 #4,将其存储在我的项目根目录中名为 VERSION 的简单文本文件中。

这是我的项目目录树的缩短版本(你可以看到the full project on GitHub):

.
├── MANIFEST.in
├── README.md
├── setup.py
├── VERSION
├── src/
│   └── fluidspaces/
│       ├── __init__.py
│       ├── __main__.py
│       ├── i3_commands.py
│       ├── rofi_commands.py
│       ├── workspace.py
│       └── workspaces.py
└── tests/
    ├── test_workspace.py
    └── test_workspaces.py

由于VERSIONsetup.py 是兄弟姐妹,因此很容易读取安装脚本中的版本文件并用它做任何我想做的事情。

但是VERSIONsrc/fluidspaces/__main__.py 不是兄弟,主模块不知道项目根目录的路径,所以我不能使用这种方法。

指南有这样的提醒:

警告:使用这种方法,您必须确保 VERSION 文件包含在您的所有源代码和二进制发行版中(例如,将 include VERSION 添加到您的 MANIFEST.in)。

这似乎是合理的 - 不需要项目根路径的包模块,版本文件可以在构建时复制到包中以便于访问 - 但我将该行添加到清单中,版本文件似乎仍然没有出现在任何地方的构建中。

为了构建,我从项目根目录和 virtualenv 中运行 pip install -U .。以下是在 <virtualenv>/lib/python3.6/site-packages 中创建的文件夹:

fluidspaces/
├── i3_commands.py
├── __init__.py
├── __main__.py
├── __pycache__/  # contents snipped
├── rofi_commands.py
├── workspace.py
└── workspaces.py
fluidspaces-0.1.0-py3.6.egg-info/
├── dependency_links.txt
├── entry_points.txt
├── installed-files.txt
├── PKG-INFO
├── SOURCES.txt
└── top_level.txt

我的更多配置文件:

MANIFEST.in

include README.md
include VERSION
graft src
prune tests

setup.py

#!/usr/bin/env python3

from setuptools import setup, find_packages


def readme():
    '''Get long description from readme file'''
    with open('README.md') as f:
        return f.read()


def version():
    '''Get version from version file'''
    with open('VERSION') as f:
        return f.read().strip()


setup(
    name='fluidspaces',
    version=version(),
    description='Navigate i3wm named containers',
    long_description=readme(),
    author='Peter Henry',
    author_email='me@peterhenry.net',
    url='https://github.com/mosbasik/fluidspaces',
    license='MIT',
    classifiers=[
      'Development Status :: 3 - Alpha',
      'Programming Language :: Python :: 3.6',
    ],
    packages=find_packages('src'),
    include_package_data=True,
    package_dir={'': 'src'},
    package_data={'': ['VERSION']},
    setup_requires=[
        'pytest-runner',
    ],
    tests_require=[
        'pytest',
    ],
    entry_points={
        'console_scripts': [
            'fluidspaces = fluidspaces.__main__:main',
        ],
    },
    python_requires='~=3.6',
)

我发现这个 SO 问题 Any python function to get “data_files” root directory? 让我认为 pkg_resources 库是我问题的答案,但我无法弄清楚如何在我的情况下使用它。

我遇到了麻烦,因为我发现的大多数示例都直接在项目根目录中包含 python 包,而不是隔离在 src/ 目录中。我使用src/ 目录是因为以下建议:

我发现并尝试稍微扭转的其他旋钮是package_datainclude_package_datadata_files 用于setup() 的kwargs。不知道它们有多相关。似乎用这些声明的事物与清单中声明的​​事物之间存在一些相互作用,但我不确定细节。

【问题讨论】:

    标签: python versioning setuptools setup.py pkg-resources


    【解决方案1】:

    在 Freenode 上的#python IRC 频道中与一些人讨论过这个问题。我学会了:

    • pkg_resources 可能是如何我应该按照我的要求去做,但这需要将版本文件放在包目录而不是项目根目录中。
    • setup.py 中,我可以从包目录中读取这样的版本文件,而无需导入包本身(出于某些原因,这是不行的),但它需要硬编码从根目录到包的路径,我想避免。

    最终我决定使用 setuptools_scm 包从我的 git 标签而不是从我的 repo 中的文件获取版本信息(其他人正在使用他们的包这样做,他们的论点很有说服力)。

    结果,我很容易在setup.py 中得到了我的版本号:

    setup.py

    from setuptools import setup, find_packages
    
    def readme():
        '''Get long description from readme file'''
        with open('README.md') as f:
            return f.read()
    
    setup(
        name='fluidspaces',
        use_scm_version=True,  # use this instead of version
        description='Navigate i3wm named containers',
        long_description=readme(),
        author='Peter Henry',
        author_email='me@peterhenry.net',
        url='https://github.com/mosbasik/fluidspaces',
        license='MIT',
        classifiers=[
          'Development Status :: 3 - Alpha',
          'Programming Language :: Python :: 3.6',
        ],
        packages=find_packages('src'),
        package_dir={'': 'src'},
        setup_requires=[
            'pytest-runner',
            'setuptools_scm',  # require package for setup
        ],
        tests_require=[
            'pytest',
        ],
        entry_points={
            'console_scripts': [
                'fluidspaces = fluidspaces.__main__:main',
            ],
        },
        python_requires='~=3.6',
    )
    

    但我最终不得不有一个硬编码路径来指示项目根目录相对于包代码应该是什么,这是我之前一直在避免的。我认为this issue on the setuptools_scm GitHub repo 可能是为什么这是必要的。

    src/fluidspaces/__main__.py

    import argparse
    from setuptools_scm import get_version  # import this function
    
    def main(args=None):
        # set up command line argument parsing
        parser = argparse.ArgumentParser()
        parser.add_argument('-V', '--version',
                            action='version',
                            version=get_version(root='../..', relative_to=__file__))  # and call it here
    

    【讨论】:

      【解决方案2】:

      对于仍在寻找答案的人们,下面是我尝试遵循Single Sourcing the Package Version 指南的品种#4。值得注意的是,当有其他更简单的解决方案时,为什么您可能会选择此解决方案。如链接所述,当您拥有可能还希望轻松检查版本的外部工具(例如 CI/CD 工具)时,此方法非常有用。

      文件树

      myproj
      ├── MANIFEST.in
      ├── myproj
      │   ├── VERSION
      │   └── __init__.py
      └── setup.py
      

      myproj/VERSION

      1.4.2
      

      MANIFEST.in

      include myproj/VERSION
      

      setup.py

      with open('myproj/VERSION') as version_file:
          version = version_file.read().strip()
      
      setup(
          ...
          version=version,
          ...
          include_package_data=True,  # needed for the VERSION file
          ...
      )
      

      myproj/__init__.py

      import pkgutil
      
      __name__ = 'myproj'
      __version__ = pkgutil.get_data(__name__, 'VERSION').decode()
      
      

      值得注意的是,在 setup.cfg 中设置配置是在 setup.py 设置函数中包含所有内容的一个很好、干净的替代方案。您可以执行以下操作,而不是读取 setup.py 中的版本,然后将其包含在函数中:

      setup.cfg

      [metadata]
      name = my_package
      version = attr: myproj.VERSION
      

      在完整示例中,我选择将所有内容都保留在 setup.py 中,以便减少一个文件,并且不确定 VERSION 文件中版本周围的潜在空白是否会被 cfg 解决方案剥离。

      【讨论】:

      • 一些注意事项:1. 如果文件VERSION 从未在包中使用,为什么还要将它放在包中?只需将其放在setup.py 旁边即可。 2. 为什么要将版本放在一个文件中,如果该文件只从setup.py 读取一次,然后将版本字符串直接放在setup.py 中。 3. 因为至少是 setuptools39.2.0 版本,这可以通过使用 setup.cfg file: version = VERSION.txt 来简化。 4. 更改__name__ 的值可能会有风险。
      • 在投反对票之前,您可能需要考虑等待问题的答案。在许多情况下,将版本保存在文本文件中,除了库之外还可以读取。回复:您的问题 1-3,我希望我的 CI/CD 管道可以轻松读取该版本,该管道会查找版本文本文件。 Re 4:名称是硬编码的。无论是在 init 还是 setup.py 中,我都看不出风险有什么不同。 pypi 文档中记录了单源版本的 6 种方式,这是其中之一。如果您不喜欢它,是的,当然还有其他方法可以做到。
      • 另外,我很高兴更新我的答案以进行仍然满足 OP 需求的改进(例如可能是您的 setup.cfg 注释)。您似乎很快就批评了,但我没有看到您发布答案。
      • 投票失败,但在编辑答案之前无法更改。这个答案很好(我的答案和你的很接近),但可以改进,因此需要注释。如果文件VERSION 打算作为包数据安装,请确保设置include_package_data 或将其添加到package_data__name__ 是一种通常已经设置的特殊变量(由解释器?),所以我认为覆盖它是不好的做法。
      • 我将在今天晚些时候查看 setup.cfg 和 include_package_data 的更改和更新。谢谢。
      猜你喜欢
      • 2018-10-12
      • 1970-01-01
      • 2019-06-25
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2017-01-06
      • 2019-11-05
      • 2023-04-01
      相关资源
      最近更新 更多