Use consistent spacing in tox variables and lists
[mudpy.git] / mudpy / version.py
1 """Version and diagnostic information for the mudpy engine."""
2
3 # Copyright (c) 2018-2020 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.
6
7 import json
8 import sys
9
10
11 # TODO(fungi) Clean up once Python 3.8 is the oldest interpreter supported
12 try:
13     import importlib.metadata
14     use_importlib = True
15 except ModuleNotFoundError:
16     import pkg_resources
17     use_importlib = False
18
19
20 class VersionDetail:
21
22     """Version detail for a Python package."""
23
24     def __init__(self, package):
25         if use_importlib:
26             project_name = package.metadata.get('Name')
27         else:
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('.'))
32
33         # Build up a human-friendly version string for display purposes
34         self.text = "%s %s" % (self.project_name, version)
35
36         # Obtain Git commit ID from PBR metadata if present
37         if use_importlib:
38             dist = importlib.metadata.distribution(self.project_name)
39         else:
40             dist = pkg_resources.get_distribution(self.project_name)
41         try:
42             if use_importlib:
43                 pbr_metadata = dist.read_text("pbr.json")
44             else:
45                 pbr_metadata = dist.get_metadata("pbr.json")
46         except (IOError, KeyError):
47             pbr_metadata = None
48         if pbr_metadata:
49             self.git_version = json.loads(pbr_metadata)["git_version"]
50         else:
51             self.git_version = None
52         if self.git_version:
53             self.text = "%s (%s)" % (self.text, self.git_version)
54
55     def __repr__(self):
56         return self.text
57
58
59 class Versions:
60
61     """Tracks info on known Python package versions."""
62
63     def __init__(self, project_name):
64         # Normalize the supplied name
65         project_name = _normalize_project(project_name)
66
67         # Python info for convenience
68         self.python_version = "%s Python %s" % (
69             sys.platform, sys.version.split(" ")[0])
70
71         # List of package names for this package's declared dependencies
72         requirements = []
73         if use_importlib:
74             for req in importlib.metadata.distribution(project_name).requires:
75                 requirements.append(_normalize_project(req))
76         else:
77             for req in pkg_resources.get_distribution(project_name).requires():
78                 requirements.append(_normalize_project(req.project_name))
79
80         # Accumulators for Python package versions
81         self.dependencies = {}
82         self.environment = {}
83
84         # Loop over all installed packages
85         if use_importlib:
86             distributions = importlib.metadata.distributions()
87         else:
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
98             else:
99                 # This may be a transitive dep, or merely installed
100                 self.environment[version.project_name] = version
101
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()]))
106
107     def __repr__(self):
108         return "Running %s on %s with dependencies %s." % (
109             self.version.text,
110             self.python_version,
111             self.dependencies_text,
112             )
113
114
115 def _normalize_project(project_name):
116     """Convenience function to normalize Python project names."""
117     if use_importlib:
118         return project_name.lower()
119     else:
120         return pkg_resources.safe_name(project_name).lower()