【问题标题】:Getting Python package distribution version from within a package从包中获取 Python 包分发版本
【发布时间】:2019-09-25 15:54:44
【问题描述】:

您可以使用获取 python 发行版的版本

import pkg_resources
pkg_resources.get_distribution("distro").version

如果您知道分发名称,那就太好了,但是我需要在运行时动态找出我的分发名称。

# Common framework base app class, extended by each app
class App(object):
    def get_app_version(self) -> str:
        package_name = self.__class__.__module__.split('.')[0]
        try:
            return pkg_resources.get_distribution(package_name).version
        except Exception:
            return "development"

这适用于应用程序包名称与分发名称相同的情况(例如requests)。但是,一旦它们不匹配,就会失败(例如,my-app 包含包 my_app)。

所以我需要的是发行版和它们的包之间的映射,我确信它一定存在于某个地方,因为 pip 似乎知道当你调用卸载时要删除什么:

$ pip uninstall requests
Uninstalling requests-2.21.0:
  Would remove:
    /home/user/.virtualenvs/app/lib/python3.6/site-packages/requests-2.21.0.dist-info/*
    /home/user/.virtualenvs/app/lib/python3.6/site-packages/requests/*

如何以编程方式访问此映射?

【问题讨论】:

    标签: python python-3.x python-packaging


    【解决方案1】:

    如果您正在寻找一种解决方案,该解决方案既适用于您的开发(未安装或仅在本地调用)版本,也适用于已安装的版本,请尝试此解决方案。

    进口:

    import ast
    import csv
    import inspect
    from os import listdir, path
    
    import pkg_resources
    

    实用功能:

    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)
    

    现在使用 Python 的 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__':`, and that it's at the end:
        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_version = next(keyword
                               for keyword in setup_call.keywords
                               if keyword.arg == 'version'
                               and isinstance(keyword.value, ast.Name))
    
        # Return the raw string if it is one
        if isinstance(package_version.value, ast.Str):
            return package_version.s
    
        # Otherwise it's a variable at the top of the `if __name__ == '__main__'` block
        elif isinstance(package_version.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_version.value.id
                                for target in sym.targets)
                        )
    
        else:
            raise NotImplemented('Package version extraction only built for raw strings and '
                                 'variables in the same function that setup() is called')
    

    最后通过将return "development" 更改为:

    替换@Gricey 的答案中的函数:
    return parse_package_name_from_setup_py(get_first_setup_py(path.dirname(__file__)))
    

    摘自我的回答https://stackoverflow.com/a/60352386

    【讨论】:

      【解决方案2】:

      如果可能的话,我认为项目的名称应该是硬编码的。如果没有,那么像下面这样的一些函数可以帮助找出包含当前文件 (__file__) 的已安装发行版的元数据:

      import pathlib
      import importlib_metadata
      
      def get_project_distribution():
          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
          return None
      
      project_distribution = get_project_distribution()
      if project_distribution:
          project_name = project_distribution.metadata['Name']
          version = project_distribution.metadata['Version']
      

      更新(2021 年 2 月):

      由于importlib_metadata 中新添加的packages_distributions() 函数,这看起来会变得更容易:

      【讨论】:

        【解决方案3】:

        在探索pkg_resources 和阅读source for pip's uninstall 几个小时后,我完成了以下工作:

        import inspect
        import pkg_resources
        import csv
        
        class App(object):
            def get_app_version(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__)
                for dist in pkg_resources.working_set:
                    try:
                        filenames = [
                            os.path.normpath(os.path.join(dist.location, r[0]))
                            for r in csv.reader(dist.get_metadata_lines("RECORD"))
                        ]
                        if app_def_path in filenames:
                            return dist.version
                    except FileNotFoundError:
                        # Not pip installed or something
                        pass
                return "development"
        

        这会遍历所有已安装的软件包,并且对于每个软件包都会遍历其文件列表并尝试将其与当前文件匹配,这会将软件包与发行版匹配。这不是很理想,我仍然愿意接受更好的答案。

        【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2023-02-10
        • 2021-06-18
        • 2015-01-09
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多