Start checking codebase with the codespell tool
[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.6 is the oldest interpreter supported
12 if not hasattr(__builtins__, 'ModuleNotFoundError'):
13     ModuleNotFoundError = ImportError
14 # TODO(fungi) Clean up once Python 3.8 is the oldest interpreter supported
15 try:
16     import importlib.metadata
17     use_importlib = True
18 except ModuleNotFoundError:
19     import pkg_resources
20     use_importlib = False
21
22
23 class VersionDetail:
24
25     """Version detail for a Python package."""
26
27     def __init__(self, package):
28         if use_importlib:
29             project_name = package.metadata.get('Name')
30         else:
31             project_name = package.project_name
32         self.project_name = _normalize_project(project_name)
33         version = package.version
34         self.version_info = tuple(version.split('.'))
35
36         # Build up a human-friendly version string for display purposes
37         self.text = "%s %s" % (self.project_name, version)
38
39         # Obtain Git commit ID from PBR metadata if present
40         if use_importlib:
41             dist = importlib.metadata.distribution(self.project_name)
42         else:
43             dist = pkg_resources.get_distribution(self.project_name)
44         try:
45             if use_importlib:
46                 pbr_metadata = dist.read_text("pbr.json")
47             else:
48                 pbr_metadata = dist.get_metadata("pbr.json")
49         except (IOError, KeyError):
50             pbr_metadata = None
51         if pbr_metadata:
52             self.git_version = json.loads(pbr_metadata)["git_version"]
53         else:
54             self.git_version = None
55         if self.git_version:
56             self.text = "%s (%s)" % (self.text, self.git_version)
57
58     def __repr__(self):
59         return self.text
60
61
62 class Versions:
63
64     """Tracks info on known Python package versions."""
65
66     def __init__(self, project_name):
67         # Normalize the supplied name
68         project_name = _normalize_project(project_name)
69
70         # Python info for convenience
71         self.python_version = "%s Python %s" % (
72             sys.platform, sys.version.split(" ")[0])
73
74         # List of package names for this package's declared dependencies
75         requirements = []
76         if use_importlib:
77             for req in importlib.metadata.distribution(project_name).requires:
78                 requirements.append(_normalize_project(req))
79         else:
80             for req in pkg_resources.get_distribution(project_name).requires():
81                 requirements.append(_normalize_project(req.project_name))
82
83         # Accumulators for Python package versions
84         self.dependencies = {}
85         self.environment = {}
86
87         # Loop over all installed packages
88         if use_importlib:
89             distributions = importlib.metadata.distributions()
90         else:
91             distributions = pkg_resources.working_set
92         for package in distributions:
93             version = VersionDetail(package)
94             # Sort packages into the corresponding buckets
95             if version.project_name in requirements:
96                 # This is a dependency
97                 self.dependencies[version.project_name] = version
98             elif version.project_name == project_name:
99                 # This is our main package
100                 self.version = version
101             else:
102                 # This may be a transitive dep, or merely installed
103                 self.environment[version.project_name] = version
104
105         self.dependencies_text = ", ".join(
106             sorted([x.text for x in self.dependencies.values()]))
107         self.environment_text = ", ".join(
108             sorted([x.text for x in self.environment.values()]))
109
110     def __repr__(self):
111         return "Running %s on %s with dependencies %s." % (
112             self.version.text,
113             self.python_version,
114             self.dependencies_text,
115             )
116
117
118 def _normalize_project(project_name):
119     """Convenience function to normalize Python project names."""
120     if use_importlib:
121         return project_name.lower()
122     else:
123         return pkg_resources.safe_name(project_name).lower()