comparison env/lib/python3.7/site-packages/virtualenv/discovery/py_info.py @ 0:26e78fe6e8c4 draft

"planemo upload commit c699937486c35866861690329de38ec1a5d9f783"
author shellac
date Sat, 02 May 2020 07:14:21 -0400
parents
children
comparison
equal deleted inserted replaced
-1:000000000000 0:26e78fe6e8c4
1 """
2 The PythonInfo contains information about a concrete instance of a Python interpreter
3
4 Note: this file is also used to query target interpreters, so can only use standard library methods
5 """
6 from __future__ import absolute_import, print_function
7
8 import json
9 import logging
10 import os
11 import platform
12 import re
13 import sys
14 import sysconfig
15 from collections import OrderedDict, namedtuple
16 from distutils import dist
17 from distutils.command.install import SCHEME_KEYS
18 from string import digits
19
20 VersionInfo = namedtuple("VersionInfo", ["major", "minor", "micro", "releaselevel", "serial"])
21
22
23 def _get_path_extensions():
24 return list(OrderedDict.fromkeys([""] + os.environ.get("PATHEXT", "").lower().split(os.pathsep)))
25
26
27 EXTENSIONS = _get_path_extensions()
28 _CONF_VAR_RE = re.compile(r"\{\w+\}")
29
30
31 class PythonInfo(object):
32 """Contains information for a Python interpreter"""
33
34 def __init__(self):
35 def u(v):
36 return v.decode("utf-8") if isinstance(v, bytes) else v
37
38 def abs_path(v):
39 return None if v is None else os.path.abspath(v) # unroll relative elements from path (e.g. ..)
40
41 # qualifies the python
42 self.platform = u(sys.platform)
43 self.implementation = u(platform.python_implementation())
44 if self.implementation == "PyPy":
45 self.pypy_version_info = tuple(u(i) for i in sys.pypy_version_info)
46
47 # this is a tuple in earlier, struct later, unify to our own named tuple
48 self.version_info = VersionInfo(*list(u(i) for i in sys.version_info))
49 self.architecture = 64 if sys.maxsize > 2 ** 32 else 32
50
51 self.version = u(sys.version)
52 self.os = u(os.name)
53
54 # information about the prefix - determines python home
55 self.prefix = u(abs_path(getattr(sys, "prefix", None))) # prefix we think
56 self.base_prefix = u(abs_path(getattr(sys, "base_prefix", None))) # venv
57 self.real_prefix = u(abs_path(getattr(sys, "real_prefix", None))) # old virtualenv
58
59 # information about the exec prefix - dynamic stdlib modules
60 self.base_exec_prefix = u(abs_path(getattr(sys, "base_exec_prefix", None)))
61 self.exec_prefix = u(abs_path(getattr(sys, "exec_prefix", None)))
62
63 self.executable = u(abs_path(sys.executable)) # the executable we were invoked via
64 self.original_executable = u(abs_path(self.executable)) # the executable as known by the interpreter
65 self.system_executable = self._fast_get_system_executable() # the executable we are based of (if available)
66
67 try:
68 __import__("venv")
69 has = True
70 except ImportError:
71 has = False
72 self.has_venv = has
73 self.path = [u(i) for i in sys.path]
74 self.file_system_encoding = u(sys.getfilesystemencoding())
75 self.stdout_encoding = u(getattr(sys.stdout, "encoding", None))
76
77 self.sysconfig_paths = {u(i): u(sysconfig.get_path(i, expand=False)) for i in sysconfig.get_path_names()}
78 config_var_keys = set()
79 for element in self.sysconfig_paths.values():
80 for k in _CONF_VAR_RE.findall(element):
81 config_var_keys.add(u(k[1:-1]))
82 config_var_keys.add("PYTHONFRAMEWORK")
83
84 self.sysconfig_vars = {u(i): u(sysconfig.get_config_var(i) or "") for i in config_var_keys}
85 if self.implementation == "PyPy" and sys.version_info.major == 2:
86 self.sysconfig_vars[u"implementation_lower"] = u"python"
87
88 self.distutils_install = {u(k): u(v) for k, v in self._distutils_install().items()}
89 confs = {k: (self.system_prefix if v.startswith(self.prefix) else v) for k, v in self.sysconfig_vars.items()}
90 self.system_stdlib = self.sysconfig_path("stdlib", confs)
91 self.system_stdlib_platform = self.sysconfig_path("platstdlib", confs)
92 self.max_size = getattr(sys, "maxsize", getattr(sys, "maxint", None))
93 self._creators = None
94
95 def _fast_get_system_executable(self):
96 """Try to get the system executable by just looking at properties"""
97 if self.real_prefix or (
98 self.base_prefix is not None and self.base_prefix != self.prefix
99 ): # if this is a virtual environment
100 if self.real_prefix is None:
101 base_executable = getattr(sys, "_base_executable", None) # some platforms may set this to help us
102 if base_executable is not None: # use the saved system executable if present
103 if sys.executable != base_executable: # we know we're in a virtual environment, cannot be us
104 return base_executable
105 return None # in this case we just can't tell easily without poking around FS and calling them, bail
106 # if we're not in a virtual environment, this is already a system python, so return the original executable
107 # note we must choose the original and not the pure executable as shim scripts might throw us off
108 return self.original_executable
109
110 @staticmethod
111 def _distutils_install():
112 # follow https://github.com/pypa/pip/blob/master/src/pip/_internal/locations.py#L95
113 # note here we don't import Distribution directly to allow setuptools to patch it
114 d = dist.Distribution({"script_args": "--no-user-cfg"}) # conf files not parsed so they do not hijack paths
115 if hasattr(sys, "_framework"):
116 sys._framework = None # disable macOS static paths for framework
117 i = d.get_command_obj("install", create=True)
118 i.prefix = os.sep # paths generated are relative to prefix that contains the path sep, this makes it relative
119 i.finalize_options()
120 result = {key: (getattr(i, "install_{}".format(key))[1:]).lstrip(os.sep) for key in SCHEME_KEYS}
121 return result
122
123 @property
124 def version_str(self):
125 return ".".join(str(i) for i in self.version_info[0:3])
126
127 @property
128 def version_release_str(self):
129 return ".".join(str(i) for i in self.version_info[0:2])
130
131 @property
132 def python_name(self):
133 version_info = self.version_info
134 return "python{}.{}".format(version_info.major, version_info.minor)
135
136 @property
137 def is_old_virtualenv(self):
138 return self.real_prefix is not None
139
140 @property
141 def is_venv(self):
142 return self.base_prefix is not None and self.version_info.major == 3
143
144 def sysconfig_path(self, key, config_var=None, sep=os.sep):
145 pattern = self.sysconfig_paths[key]
146 if config_var is None:
147 config_var = self.sysconfig_vars
148 else:
149 base = {k: v for k, v in self.sysconfig_vars.items()}
150 base.update(config_var)
151 config_var = base
152 return pattern.format(**config_var).replace(u"/", sep)
153
154 def creators(self, refresh=False):
155 if self._creators is None or refresh is True:
156 from virtualenv.run.plugin.creators import CreatorSelector
157
158 self._creators = CreatorSelector.for_interpreter(self)
159 return self._creators
160
161 @property
162 def system_include(self):
163 path = self.sysconfig_path(
164 "include",
165 {k: (self.system_prefix if v.startswith(self.prefix) else v) for k, v in self.sysconfig_vars.items()},
166 )
167 if not os.path.exists(path): # some broken packaging don't respect the sysconfig, fallback to distutils path
168 # the pattern include the distribution name too at the end, remove that via the parent call
169 fallback = os.path.join(self.prefix, os.path.dirname(self.distutils_install["headers"]))
170 if os.path.exists(fallback):
171 path = fallback
172 return path
173
174 @property
175 def system_prefix(self):
176 return self.real_prefix or self.base_prefix or self.prefix
177
178 @property
179 def system_exec_prefix(self):
180 return self.real_prefix or self.base_exec_prefix or self.exec_prefix
181
182 def __unicode__(self):
183 content = repr(self)
184 if sys.version_info == 2:
185 content = content.decode("utf-8")
186 return content
187
188 def __repr__(self):
189 return "{}({!r})".format(
190 self.__class__.__name__, {k: v for k, v in self.__dict__.items() if not k.startswith("_")}
191 )
192
193 def __str__(self):
194 content = "{}({})".format(
195 self.__class__.__name__,
196 ", ".join(
197 "{}={}".format(k, v)
198 for k, v in (
199 ("spec", self.spec,),
200 (
201 "system"
202 if self.system_executable is not None and self.system_executable != self.executable
203 else None,
204 self.system_executable,
205 ),
206 (
207 "original"
208 if (
209 self.original_executable != self.system_executable
210 and self.original_executable != self.executable
211 )
212 else None,
213 self.original_executable,
214 ),
215 ("exe", self.executable),
216 ("platform", self.platform),
217 ("version", repr(self.version)),
218 ("encoding_fs_io", "{}-{}".format(self.file_system_encoding, self.stdout_encoding)),
219 )
220 if k is not None
221 ),
222 )
223 return content
224
225 @property
226 def spec(self):
227 return "{}{}-{}".format(self.implementation, ".".join(str(i) for i in self.version_info), self.architecture)
228
229 @classmethod
230 def clear_cache(cls, app_data):
231 # this method is not used by itself, so here and called functions can import stuff locally
232 from virtualenv.discovery.cached_py_info import clear
233
234 clear(app_data)
235 cls._cache_exe_discovery.clear()
236
237 def satisfies(self, spec, impl_must_match):
238 """check if a given specification can be satisfied by the this python interpreter instance"""
239 if spec.path:
240 if self.executable == os.path.abspath(spec.path):
241 return True # if the path is a our own executable path we're done
242 if not spec.is_abs:
243 # if path set, and is not our original executable name, this does not match
244 basename = os.path.basename(self.original_executable)
245 spec_path = spec.path
246 if sys.platform == "win32":
247 basename, suffix = os.path.splitext(basename)
248 if spec_path.endswith(suffix):
249 spec_path = spec_path[: -len(suffix)]
250 if basename != spec_path:
251 return False
252
253 if impl_must_match:
254 if spec.implementation is not None and spec.implementation.lower() != self.implementation.lower():
255 return False
256
257 if spec.architecture is not None and spec.architecture != self.architecture:
258 return False
259
260 for our, req in zip(self.version_info[0:3], (spec.major, spec.minor, spec.micro)):
261 if req is not None and our is not None and our != req:
262 return False
263 return True
264
265 _current_system = None
266 _current = None
267
268 @classmethod
269 def current(cls, app_data=None):
270 """
271 This locates the current host interpreter information. This might be different than what we run into in case
272 the host python has been upgraded from underneath us.
273 """
274 if cls._current is None:
275 cls._current = cls.from_exe(sys.executable, app_data, raise_on_error=True, resolve_to_host=False)
276 return cls._current
277
278 @classmethod
279 def current_system(cls, app_data=None):
280 """
281 This locates the current host interpreter information. This might be different than what we run into in case
282 the host python has been upgraded from underneath us.
283 """
284 if cls._current_system is None:
285 cls._current_system = cls.from_exe(sys.executable, app_data, raise_on_error=True, resolve_to_host=True)
286 return cls._current_system
287
288 def _to_json(self):
289 # don't save calculated paths, as these are non primitive types
290 return json.dumps(self._to_dict(), indent=2)
291
292 def _to_dict(self):
293 data = {var: (getattr(self, var) if var not in ("_creators",) else None) for var in vars(self)}
294 # noinspection PyProtectedMember
295 data["version_info"] = data["version_info"]._asdict() # namedtuple to dictionary
296 return data
297
298 @classmethod
299 def from_exe(cls, exe, app_data=None, raise_on_error=True, ignore_cache=False, resolve_to_host=True):
300 """Given a path to an executable get the python information"""
301 # this method is not used by itself, so here and called functions can import stuff locally
302 from virtualenv.discovery.cached_py_info import from_exe
303
304 proposed = from_exe(cls, app_data, exe, raise_on_error=raise_on_error, ignore_cache=ignore_cache)
305 # noinspection PyProtectedMember
306 if isinstance(proposed, PythonInfo) and resolve_to_host:
307 proposed = proposed._resolve_to_system(app_data, proposed)
308 return proposed
309
310 @classmethod
311 def _from_json(cls, payload):
312 # the dictionary unroll here is to protect against pypy bug of interpreter crashing
313 raw = json.loads(payload)
314 return cls._from_dict({k: v for k, v in raw.items()})
315
316 @classmethod
317 def _from_dict(cls, data):
318 data["version_info"] = VersionInfo(**data["version_info"]) # restore this to a named tuple structure
319 result = cls()
320 result.__dict__ = {k: v for k, v in data.items()}
321 return result
322
323 @classmethod
324 def _resolve_to_system(cls, app_data, target):
325 start_executable = target.executable
326 prefixes = OrderedDict()
327 while target.system_executable is None:
328 prefix = target.real_prefix or target.base_prefix or target.prefix
329 if prefix in prefixes:
330 if len(prefixes) == 1:
331 # if we're linking back to ourselves accept ourselves with a WARNING
332 logging.info("%r links back to itself via prefixes", target)
333 target.system_executable = target.executable
334 break
335 for at, (p, t) in enumerate(prefixes.items(), start=1):
336 logging.error("%d: prefix=%s, info=%r", at, p, t)
337 logging.error("%d: prefix=%s, info=%r", len(prefixes) + 1, prefix, target)
338 raise RuntimeError("prefixes are causing a circle {}".format("|".join(prefixes.keys())))
339 prefixes[prefix] = target
340 target = target.discover_exe(app_data, prefix=prefix, exact=False)
341 if target.executable != target.system_executable:
342 target = cls.from_exe(target.system_executable, app_data)
343 target.executable = start_executable
344 return target
345
346 _cache_exe_discovery = {}
347
348 def discover_exe(self, app_data, prefix, exact=True):
349 key = prefix, exact
350 if key in self._cache_exe_discovery and prefix:
351 logging.debug("discover exe from cache %s - exact %s: %r", prefix, exact, self._cache_exe_discovery[key])
352 return self._cache_exe_discovery[key]
353 logging.debug("discover exe for %s in %s", self, prefix)
354 # we don't know explicitly here, do some guess work - our executable name should tell
355 possible_names = self._find_possible_exe_names()
356 possible_folders = self._find_possible_folders(prefix)
357 discovered = []
358 for folder in possible_folders:
359 for name in possible_names:
360 info = self._check_exe(app_data, folder, name, exact, discovered)
361 if info is not None:
362 self._cache_exe_discovery[key] = info
363 return info
364 if exact is False and discovered:
365 info = self._select_most_likely(discovered, self)
366 folders = os.pathsep.join(possible_folders)
367 self._cache_exe_discovery[key] = info
368 logging.debug("no exact match found, chosen most similar of %s within base folders %s", info, folders)
369 return info
370 msg = "failed to detect {} in {}".format("|".join(possible_names), os.pathsep.join(possible_folders))
371 raise RuntimeError(msg)
372
373 def _check_exe(self, app_data, folder, name, exact, discovered):
374 exe_path = os.path.join(folder, name)
375 if not os.path.exists(exe_path):
376 return None
377 info = self.from_exe(exe_path, app_data, resolve_to_host=False, raise_on_error=False)
378 if info is None: # ignore if for some reason we can't query
379 return None
380 for item in ["implementation", "architecture", "version_info"]:
381 found = getattr(info, item)
382 searched = getattr(self, item)
383 if found != searched:
384 if item == "version_info":
385 found, searched = ".".join(str(i) for i in found), ".".join(str(i) for i in searched)
386 executable = info.executable
387 logging.debug("refused interpreter %s because %s differs %s != %s", executable, item, found, searched)
388 if exact is False:
389 discovered.append(info)
390 break
391 else:
392 return info
393 return None
394
395 @staticmethod
396 def _select_most_likely(discovered, target):
397 # no exact match found, start relaxing our requirements then to facilitate system package upgrades that
398 # could cause this (when using copy strategy of the host python)
399 def sort_by(info):
400 # we need to setup some priority of traits, this is as follows:
401 # implementation, major, minor, micro, architecture, tag, serial
402 matches = [
403 info.implementation == target.implementation,
404 info.version_info.major == target.version_info.major,
405 info.version_info.minor == target.version_info.minor,
406 info.architecture == target.architecture,
407 info.version_info.micro == target.version_info.micro,
408 info.version_info.releaselevel == target.version_info.releaselevel,
409 info.version_info.serial == target.version_info.serial,
410 ]
411 priority = sum((1 << pos if match else 0) for pos, match in enumerate(reversed(matches)))
412 return priority
413
414 sorted_discovered = sorted(discovered, key=sort_by, reverse=True) # sort by priority in decreasing order
415 most_likely = sorted_discovered[0]
416 return most_likely
417
418 def _find_possible_folders(self, inside_folder):
419 candidate_folder = OrderedDict()
420 executables = OrderedDict()
421 executables[os.path.realpath(self.executable)] = None
422 executables[self.executable] = None
423 executables[os.path.realpath(self.original_executable)] = None
424 executables[self.original_executable] = None
425 for exe in executables.keys():
426 base = os.path.dirname(exe)
427 # following path pattern of the current
428 if base.startswith(self.prefix):
429 relative = base[len(self.prefix) :]
430 candidate_folder["{}{}".format(inside_folder, relative)] = None
431
432 # or at root level
433 candidate_folder[inside_folder] = None
434 return list(i for i in candidate_folder.keys() if os.path.exists(i))
435
436 def _find_possible_exe_names(self):
437 name_candidate = OrderedDict()
438 for name in self._possible_base():
439 for at in (3, 2, 1, 0):
440 version = ".".join(str(i) for i in self.version_info[:at])
441 for arch in ["-{}".format(self.architecture), ""]:
442 for ext in EXTENSIONS:
443 candidate = "{}{}{}{}".format(name, version, arch, ext)
444 name_candidate[candidate] = None
445 return list(name_candidate.keys())
446
447 def _possible_base(self):
448 possible_base = OrderedDict()
449 basename = os.path.splitext(os.path.basename(self.executable))[0].rstrip(digits)
450 possible_base[basename] = None
451 possible_base[self.implementation] = None
452 # python is always the final option as in practice is used by multiple implementation as exe name
453 if "python" in possible_base:
454 del possible_base["python"]
455 possible_base["python"] = None
456 for base in possible_base:
457 lower = base.lower()
458 yield lower
459 from virtualenv.info import fs_is_case_sensitive
460
461 if fs_is_case_sensitive():
462 if base != lower:
463 yield base
464 upper = base.upper()
465 if upper != base:
466 yield upper
467
468
469 if __name__ == "__main__":
470 # dump a JSON representation of the current python
471 # noinspection PyProtectedMember
472 print(PythonInfo()._to_json())