问题
You can get the version of a python distribution using
import pkg_resources
pkg_resources.get_distribution("distro").version
This is great if you know the distribution name, however I need to dynamically figure out my distribution name at runtime.
# 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"
This works for cases where the app's package name is the same as the distribution name (e.g. requests
). However this fails once they don't match (e.g. my-app
containing package my_app
).
So what I need is a mapping between distributions and their packages, which I'm sure must exist somewhere since pip seems to know what to delete when you call uninstall:
$ 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/*
How do I programatically access this mapping?
回答1:
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.
回答2:
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
回答3:
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']
来源:https://stackoverflow.com/questions/56031858/getting-python-package-distribution-version-from-within-a-package