1 """Version and diagnostic information for the mudpy engine."""
3 # Copyright (c) 2018-2021 mudpy authors. Permission to use, copy,
4 # modify, and distribute this software is granted under terms
5 # provided in the LICENSE file distributed with this software.
11 # TODO(fungi) Clean up once Python 3.8 is the oldest interpreter supported
13 import importlib.metadata
15 except ModuleNotFoundError:
22 """Version detail for a Python package."""
24 def __init__(self, package):
26 project_name = package.metadata.get('Name')
28 project_name = package.project_name
29 self.project_name = _normalize_project(project_name)
30 version = package.version
31 self.version_info = tuple(version.split('.'))
33 # Build up a human-friendly version string for display purposes
34 self.text = "%s %s" % (self.project_name, version)
36 # Obtain Git commit ID from PBR metadata if present
38 dist = importlib.metadata.distribution(self.project_name)
40 dist = pkg_resources.get_distribution(self.project_name)
43 pbr_metadata = dist.read_text("pbr.json")
45 pbr_metadata = dist.get_metadata("pbr.json")
46 except (IOError, KeyError):
49 self.git_version = json.loads(pbr_metadata)["git_version"]
51 self.git_version = None
53 self.text = "%s (%s)" % (self.text, self.git_version)
61 """Tracks info on known Python package versions."""
63 def __init__(self, project_name):
64 # Normalize the supplied name
65 project_name = _normalize_project(project_name)
67 # Python info for convenience
68 self.python_version = "%s Python %s" % (
69 sys.platform, sys.version.split(" ")[0])
71 # List of package names for this package's declared dependencies
74 for req in importlib.metadata.distribution(project_name).requires:
75 requirements.append(_normalize_project(req))
77 for req in pkg_resources.get_distribution(project_name).requires():
78 requirements.append(_normalize_project(req.project_name))
80 # Accumulators for Python package versions
81 self.dependencies = {}
84 # Loop over all installed packages
86 distributions = importlib.metadata.distributions()
88 distributions = pkg_resources.working_set
89 for package in distributions:
90 version = VersionDetail(package)
91 # Sort packages into the corresponding buckets
92 if version.project_name in requirements:
93 # This is a dependency
94 self.dependencies[version.project_name] = version
95 elif version.project_name == project_name:
96 # This is our main package
97 self.version = version
99 # This may be a transitive dep, or merely installed
100 self.environment[version.project_name] = version
102 self.dependencies_text = ", ".join(
103 sorted([x.text for x in self.dependencies.values()]))
104 self.environment_text = ", ".join(
105 sorted([x.text for x in self.environment.values()]))
108 return "Running %s on %s with dependencies %s." % (
111 self.dependencies_text,
115 def _normalize_project(project_name):
116 """Strip and normalize Python project names."""
118 # Use lower-case names for ease of comparison
120 project_name = project_name.lower()
122 project_name = pkg_resources.safe_name(project_name).lower()
124 # Remove any version specifiers included with requirements strings
125 for operator in ' <>=!':
126 project_name = project_name.split(operator)[0]