Getting Python package distribution version from within a package

后端 未结 3 1174
灰色年华
灰色年华 2021-01-05 23:53

You can get the version of a python distribution using

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

This is grea

相关标签:
3条回答
  • 2021-01-06 00:19

    If you're looking for a solution that works both from your development—not installed, or just locally called—version, and an installed version, then try this solution.

    Imports:

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

    Utility function:

    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)
    

    Now using Python's ast library:

    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')
    

    Finally replace the function in @Gricey's answer by changing return "development" to:

    return parse_package_name_from_setup_py(get_first_setup_py(path.dirname(__file__)))
    

    Taken from my answer https://stackoverflow.com/a/60352386

    0 讨论(0)
  • 2021-01-06 00:20

    After a couple of hours of exploring pkg_resources and reading the source for pip's uninstall I've got the following working:

    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"
    

    This iterates through all installed packages and for each of those iterates through its list of files and tries to match that to the current file, this matches the package to the distribution. It's not really ideal, and I'm still open to better answers.

    0 讨论(0)
  • 2021-01-06 00:26

    I believe the project's name should be hard-coded if possible. If not then some function like the following could help figuring out the metadata for the installed distribution containing the current file (__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']
    
    0 讨论(0)
提交回复
热议问题