-
Notifications
You must be signed in to change notification settings - Fork 385
/
venv_metadata_inspector.py
173 lines (146 loc) · 5.53 KB
/
venv_metadata_inspector.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
#!/usr/bin/env python3
import json
import sys
from pathlib import Path
from typing import Dict, List, Optional
try:
WindowsError
except NameError:
WINDOWS = False
else:
WINDOWS = True
def get_package_dependencies(package: str) -> List[str]:
try:
import pkg_resources
except Exception:
return []
return [str(r) for r in pkg_resources.get_distribution(package).requires()]
def get_package_version(package: str) -> Optional[str]:
try:
import pkg_resources
return pkg_resources.get_distribution(package).version
except Exception:
return None
def get_apps(package: str, bin_path: Path) -> List[str]:
try:
import pkg_resources
except Exception:
return []
dist = pkg_resources.get_distribution(package)
apps = set()
for section in ["console_scripts", "gui_scripts"]:
# "entry_points" entry in setup.py are found here
for name in pkg_resources.get_entry_map(dist).get(section, []):
if (bin_path / name).exists():
apps.add(name)
if WINDOWS and (bin_path / (name + ".exe")).exists():
# WINDOWS adds .exe to entry_point name
apps.add(name + ".exe")
if dist.has_metadata("RECORD"):
# for non-editable package installs, RECORD is list of installed files
# "scripts" entry in setup.py is found here (test w/ awscli)
for line in dist.get_metadata_lines("RECORD"):
entry = line.split(",")[0] # noqa: T484
path = (Path(dist.location) / entry).resolve()
try:
if path.parent.samefile(bin_path):
apps.add(Path(entry).name)
except FileNotFoundError:
pass
if dist.has_metadata("installed-files.txt"):
# not sure what is found here
for line in dist.get_metadata_lines("installed-files.txt"):
entry = line.split(",")[0] # noqa: T484
path = (Path(dist.egg_info) / entry).resolve() # type: ignore
try:
if path.parent.samefile(bin_path):
apps.add(Path(entry).name)
except FileNotFoundError:
pass
return sorted(apps)
def _dfs_package_apps(
bin_path: Path,
package: str,
app_paths_of_dependencies: Dict[str, List[Path]],
dep_visited: Optional[Dict[str, bool]] = None,
) -> Dict[str, List[Path]]:
if dep_visited is None:
dep_visited = {}
dependencies = get_package_dependencies(package)
for d in dependencies:
app_names = get_apps(d, bin_path)
if app_names:
app_paths_of_dependencies[d] = [bin_path / app for app in app_names]
# recursively search for more
if d not in dep_visited:
# only search if this package isn't already listed to avoid
# infinite recursion
dep_visited[d] = True
app_paths_of_dependencies = _dfs_package_apps(
bin_path, d, app_paths_of_dependencies, dep_visited
)
return app_paths_of_dependencies
def _windows_extra_app_paths(app_paths: List[Path]) -> List[Path]:
# In Windows, editable package have additional files starting with the
# same name that are required to be in the same dir to run the app
# Add "*-script.py", "*.exe.manifest" only to app_paths to make
# execution work; do not add them to apps to ensure they are not listed
app_paths_output = app_paths.copy()
for app_path in app_paths:
win_app_path = app_path.parent / (app_path.stem + "-script.py")
if win_app_path.exists():
app_paths_output.append(win_app_path)
win_app_path = app_path.parent / (app_path.stem + ".exe.manifest")
if win_app_path.exists():
app_paths_output.append(win_app_path)
return app_paths_output
def main():
package = sys.argv[1]
bin_path = Path(sys.argv[2])
apps = get_apps(package, bin_path)
app_paths = [Path(bin_path) / app for app in apps]
if WINDOWS:
app_paths = _windows_extra_app_paths(app_paths)
app_paths = [str(app_path) for app_path in app_paths]
app_paths_of_dependencies = {} # type: Dict[str, List[str]]
apps_of_dependencies = [] # type: List[str]
app_paths_of_dependencies = _dfs_package_apps(
bin_path, package, app_paths_of_dependencies
)
for dep in app_paths_of_dependencies:
apps_of_dependencies += [
dep_path.name for dep_path in app_paths_of_dependencies[dep]
]
if WINDOWS:
app_paths_of_dependencies[dep] = _windows_extra_app_paths(
app_paths_of_dependencies[dep]
)
app_paths_of_dependencies[dep] = [
str(dep_path) for dep_path in app_paths_of_dependencies[dep]
]
output = {
"apps": apps,
"app_paths": app_paths,
"apps_of_dependencies": apps_of_dependencies,
"app_paths_of_dependencies": app_paths_of_dependencies,
"package_version": get_package_version(package),
"python_version": "Python {}.{}.{}".format(
sys.version_info.major, sys.version_info.minor, sys.version_info.micro
),
}
print(json.dumps(output))
if __name__ == "__main__":
try:
main()
except Exception:
print(
json.dumps(
{
"apps": [],
"app_paths": [],
"app_paths_of_dependencies": {},
"package_version": None,
"python_version": None,
}
)
)