ThreeDimensionModeller 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ThreeDimensionModeller/__init__.py +8 -0
- ThreeDimensionModeller/__main__.py +6 -0
- ThreeDimensionModeller/about_page.py +200 -0
- ThreeDimensionModeller/check_system.py +521 -0
- ThreeDimensionModeller/cli.py +367 -0
- ThreeDimensionModeller/menu_language.py +754 -0
- ThreeDimensionModeller/menu_model.py +276 -0
- ThreeDimensionModeller/menu_painter.py +487 -0
- ThreeDimensionModeller/menu_session.py +118 -0
- ThreeDimensionModeller/model.py +835 -0
- ThreeDimensionModeller/self_management.py +79 -0
- ThreeDimensionModeller/system_log.py +92 -0
- ThreeDimensionModeller/tui.py +475 -0
- threedimensionmodeller-1.0.0.dist-info/METADATA +386 -0
- threedimensionmodeller-1.0.0.dist-info/RECORD +19 -0
- threedimensionmodeller-1.0.0.dist-info/WHEEL +5 -0
- threedimensionmodeller-1.0.0.dist-info/entry_points.txt +2 -0
- threedimensionmodeller-1.0.0.dist-info/licenses/LICENSE.md +21 -0
- threedimensionmodeller-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,521 @@
|
|
|
1
|
+
# =============================================================================
|
|
2
|
+
# Host check for the about page.
|
|
3
|
+
# requirement-python-oop — class CheckSystem.
|
|
4
|
+
# requirement-python-about — line text and the pyenv and conda path reads.
|
|
5
|
+
# requirement-python-cli-logging — in_venv, in_pyenv, and in_conda read the one logger.
|
|
6
|
+
# =============================================================================
|
|
7
|
+
from __future__ import print_function, unicode_literals
|
|
8
|
+
|
|
9
|
+
import datetime
|
|
10
|
+
import os
|
|
11
|
+
import platform
|
|
12
|
+
import shutil
|
|
13
|
+
import sys
|
|
14
|
+
import sysconfig
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class CheckSystem:
|
|
20
|
+
"""Reads for the [CHECK SYSTEM] block. One class, one module.
|
|
21
|
+
|
|
22
|
+
The about composer calls this object. It does not own the star box.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(self, logger=None, app_name="", version="", console_name=""):
|
|
26
|
+
self.logger = logger
|
|
27
|
+
if logger is not None:
|
|
28
|
+
logger.log_message("instantiated", component="CheckSystem")
|
|
29
|
+
self.app_name = app_name
|
|
30
|
+
self.version = version
|
|
31
|
+
self.console_name = console_name
|
|
32
|
+
|
|
33
|
+
def parse_sys_version(self, version_text, python_version):
|
|
34
|
+
"""
|
|
35
|
+
General Purpose: Python display and C library string from sys.version.
|
|
36
|
+
The C library line is the compiler token, such as GCC 13.3.0.
|
|
37
|
+
"""
|
|
38
|
+
gcc = version_text
|
|
39
|
+
if "\n" in gcc:
|
|
40
|
+
gcc = gcc.split("\n", 1)[1]
|
|
41
|
+
elif "[" in gcc and "]" in gcc:
|
|
42
|
+
gcc = gcc.split("[", 1)[1].split("]", 1)[0]
|
|
43
|
+
if gcc == "GCC":
|
|
44
|
+
gcc = "[GCC]"
|
|
45
|
+
if " (Red Hat" in gcc:
|
|
46
|
+
gcc = gcc.split(" (Red Hat", 1)[0] + "]"
|
|
47
|
+
probe = gcc if "[PyPy " in gcc else version_text
|
|
48
|
+
if "[PyPy " in probe and "with" in probe:
|
|
49
|
+
extra = probe.split("with", 1)[0].split("[", 1)[1].strip()
|
|
50
|
+
python_version = "{} ({})".format(python_version, extra)
|
|
51
|
+
gcc = "[" + probe.split("with ", 1)[1]
|
|
52
|
+
shown = gcc.strip()
|
|
53
|
+
if shown.startswith("[") and shown.endswith("]"):
|
|
54
|
+
shown = shown[1:-1].strip()
|
|
55
|
+
return python_version, shown
|
|
56
|
+
|
|
57
|
+
def arch_label(self, version_text, machine=None):
|
|
58
|
+
"""General Purpose: amd64 / arm64 / x86 style name for this host."""
|
|
59
|
+
if "AMD64" in version_text:
|
|
60
|
+
return "amd64"
|
|
61
|
+
if "AMD32" in version_text:
|
|
62
|
+
return "x86"
|
|
63
|
+
if machine is None:
|
|
64
|
+
machine = platform.machine()
|
|
65
|
+
mapped = {
|
|
66
|
+
"x86_64": "amd64",
|
|
67
|
+
"amd64": "amd64",
|
|
68
|
+
"aarch64": "arm64",
|
|
69
|
+
"arm64": "arm64",
|
|
70
|
+
"i386": "x86",
|
|
71
|
+
"i686": "x86",
|
|
72
|
+
"x86": "x86",
|
|
73
|
+
}
|
|
74
|
+
return mapped.get(machine.lower(), machine)
|
|
75
|
+
|
|
76
|
+
def libc_label(self, version_text, shell, detected=None):
|
|
77
|
+
"""General Purpose: libc token used in the binary type, such as glibc."""
|
|
78
|
+
if detected is None:
|
|
79
|
+
try:
|
|
80
|
+
detected = platform.libc_ver()[0] or ""
|
|
81
|
+
except Exception:
|
|
82
|
+
detected = "unknown"
|
|
83
|
+
name = detected
|
|
84
|
+
if ("AMD64" in version_text or "AMD32" in version_text) and "MSC" in version_text:
|
|
85
|
+
name = "msc"
|
|
86
|
+
if "clang" in version_text.lower():
|
|
87
|
+
name = "clang"
|
|
88
|
+
if name == "" and shell == "/bin/ash":
|
|
89
|
+
name = "muslc"
|
|
90
|
+
return name
|
|
91
|
+
|
|
92
|
+
def binary_type(self, arch, libc):
|
|
93
|
+
"""General Purpose: arch-libc token, such as amd64-glibc."""
|
|
94
|
+
if arch == "":
|
|
95
|
+
return ""
|
|
96
|
+
if libc == "":
|
|
97
|
+
return "{}-".format(arch)
|
|
98
|
+
return "{}-{}".format(arch, libc)
|
|
99
|
+
|
|
100
|
+
def current_user(self):
|
|
101
|
+
"""General Purpose: Login name for the about system check."""
|
|
102
|
+
user = os.environ.get("USER") or os.environ.get("USERNAME") or ""
|
|
103
|
+
if user:
|
|
104
|
+
return user
|
|
105
|
+
try:
|
|
106
|
+
import getpass
|
|
107
|
+
return getpass.getuser()
|
|
108
|
+
except Exception:
|
|
109
|
+
return ""
|
|
110
|
+
|
|
111
|
+
def shell_text(self):
|
|
112
|
+
"""General Purpose: The shell path from the environment."""
|
|
113
|
+
return os.environ.get("SHELL") or ""
|
|
114
|
+
|
|
115
|
+
def python_executable_name(self):
|
|
116
|
+
"""General Purpose: Basename of the running Python executable."""
|
|
117
|
+
exe = sys.executable or ""
|
|
118
|
+
if "/" in exe:
|
|
119
|
+
return exe.rsplit("/", 1)[-1]
|
|
120
|
+
if "\\" in exe:
|
|
121
|
+
return exe.rsplit("\\", 1)[-1]
|
|
122
|
+
return exe
|
|
123
|
+
|
|
124
|
+
def command_location(self, name):
|
|
125
|
+
"""General Purpose: PATH location of a tool, or an empty string."""
|
|
126
|
+
found = shutil.which(name)
|
|
127
|
+
return found or ""
|
|
128
|
+
|
|
129
|
+
def _has_pyenv_launcher(self, root):
|
|
130
|
+
"""True when bin/pyenv exists. The launcher may be a symlink."""
|
|
131
|
+
return os.path.lexists(os.path.join(root, "bin", "pyenv"))
|
|
132
|
+
|
|
133
|
+
def _pyenv_version_token(self, token):
|
|
134
|
+
"""One version segment. system, blanks, and path tricks are skipped."""
|
|
135
|
+
if token in ("", "system", ".", ".."):
|
|
136
|
+
return False
|
|
137
|
+
if "/" in token or "\\" in token:
|
|
138
|
+
return False
|
|
139
|
+
return True
|
|
140
|
+
|
|
141
|
+
def _pyenv_version_file_lines(self, root):
|
|
142
|
+
path = os.path.join(root, "version")
|
|
143
|
+
if not os.path.isfile(path):
|
|
144
|
+
return []
|
|
145
|
+
try:
|
|
146
|
+
with open(path, "r", encoding="utf-8") as handle:
|
|
147
|
+
text = handle.read()
|
|
148
|
+
except OSError:
|
|
149
|
+
return []
|
|
150
|
+
return text.splitlines()
|
|
151
|
+
|
|
152
|
+
def pyenv_root(self):
|
|
153
|
+
"""
|
|
154
|
+
General Purpose: Pyenv root when bin/pyenv is present, else empty.
|
|
155
|
+
A non-empty PYENV_ROOT is the only candidate. Otherwise ~/.pyenv.
|
|
156
|
+
"""
|
|
157
|
+
named = os.environ.get("PYENV_ROOT")
|
|
158
|
+
if named is not None and named.strip() != "":
|
|
159
|
+
root = os.path.abspath(os.path.expanduser(named.strip()))
|
|
160
|
+
if self._has_pyenv_launcher(root):
|
|
161
|
+
return root
|
|
162
|
+
return ""
|
|
163
|
+
home = os.path.expanduser("~")
|
|
164
|
+
if home == "" or home == "~":
|
|
165
|
+
return ""
|
|
166
|
+
root = os.path.abspath(os.path.join(home, ".pyenv"))
|
|
167
|
+
if self._has_pyenv_launcher(root):
|
|
168
|
+
return root
|
|
169
|
+
return ""
|
|
170
|
+
|
|
171
|
+
def in_venv(self):
|
|
172
|
+
"""General Purpose: True when the process logger reports a venv."""
|
|
173
|
+
if self.logger is None:
|
|
174
|
+
return False
|
|
175
|
+
return self.logger.inVenv()
|
|
176
|
+
|
|
177
|
+
def in_pyenv(self):
|
|
178
|
+
"""General Purpose: True when the process logger reports pyenv."""
|
|
179
|
+
if self.logger is None:
|
|
180
|
+
return False
|
|
181
|
+
return self.logger.inPyenv()
|
|
182
|
+
|
|
183
|
+
def in_conda(self):
|
|
184
|
+
"""General Purpose: True when the process logger reports conda."""
|
|
185
|
+
if self.logger is None:
|
|
186
|
+
return False
|
|
187
|
+
return self.logger.inConda()
|
|
188
|
+
|
|
189
|
+
def under_pyenv(self):
|
|
190
|
+
"""General Purpose: True when this check has a pyenv root."""
|
|
191
|
+
return self.pyenv_root() != ""
|
|
192
|
+
|
|
193
|
+
def pyenv_location(self):
|
|
194
|
+
"""
|
|
195
|
+
General Purpose: The pyenv launcher at bin/pyenv.
|
|
196
|
+
This is not the libexec file that can appear first on PATH.
|
|
197
|
+
"""
|
|
198
|
+
root = self.pyenv_root()
|
|
199
|
+
if root == "":
|
|
200
|
+
return ""
|
|
201
|
+
return os.path.join(root, "bin", "pyenv")
|
|
202
|
+
|
|
203
|
+
def pyenv_version_names(self, root):
|
|
204
|
+
"""
|
|
205
|
+
General Purpose: Selected pyenv versions, skipping system and path tricks.
|
|
206
|
+
PYENV_VERSION wins over the root version file.
|
|
207
|
+
"""
|
|
208
|
+
raw = os.environ.get("PYENV_VERSION")
|
|
209
|
+
if raw is not None and raw.strip() != "":
|
|
210
|
+
parts = raw.split(":")
|
|
211
|
+
else:
|
|
212
|
+
parts = self._pyenv_version_file_lines(root)
|
|
213
|
+
names = []
|
|
214
|
+
for part in parts:
|
|
215
|
+
token = part.strip()
|
|
216
|
+
if self._pyenv_version_token(token):
|
|
217
|
+
names.append(token)
|
|
218
|
+
return names
|
|
219
|
+
|
|
220
|
+
def pyenv_interpreter(self, name):
|
|
221
|
+
"""
|
|
222
|
+
General Purpose: python2 or python3 inside the pyenv root, or empty.
|
|
223
|
+
A selected version bin wins. Otherwise the shim.
|
|
224
|
+
"""
|
|
225
|
+
root = self.pyenv_root()
|
|
226
|
+
if root == "":
|
|
227
|
+
return ""
|
|
228
|
+
for version in self.pyenv_version_names(root):
|
|
229
|
+
candidate = os.path.join(root, "versions", version, "bin", name)
|
|
230
|
+
if os.path.lexists(candidate):
|
|
231
|
+
return candidate
|
|
232
|
+
shim = os.path.join(root, "shims", name)
|
|
233
|
+
if os.path.lexists(shim):
|
|
234
|
+
return shim
|
|
235
|
+
return ""
|
|
236
|
+
|
|
237
|
+
def _conda_name_token(self, token):
|
|
238
|
+
"""One environment segment. Blanks and path tricks are skipped."""
|
|
239
|
+
if token in ("", ".", ".."):
|
|
240
|
+
return False
|
|
241
|
+
if "/" in token or "\\" in token:
|
|
242
|
+
return False
|
|
243
|
+
return True
|
|
244
|
+
|
|
245
|
+
def _has_conda_launcher(self, root):
|
|
246
|
+
"""True when bin/conda exists. The launcher may be a symlink."""
|
|
247
|
+
return os.path.lexists(os.path.join(root, "bin", "conda"))
|
|
248
|
+
|
|
249
|
+
def _root_from_conda_exe(self, exe):
|
|
250
|
+
"""Root when CONDA_EXE lives in bin or condabin. Otherwise empty."""
|
|
251
|
+
path = os.path.abspath(os.path.expanduser(exe.strip()))
|
|
252
|
+
parent = os.path.dirname(path)
|
|
253
|
+
if os.path.basename(parent) not in ("bin", "condabin"):
|
|
254
|
+
return ""
|
|
255
|
+
root = os.path.dirname(parent)
|
|
256
|
+
if self._has_conda_launcher(root):
|
|
257
|
+
return root
|
|
258
|
+
return ""
|
|
259
|
+
|
|
260
|
+
def _root_from_conda_prefix(self, prefix):
|
|
261
|
+
"""Root when the prefix is the base or one envs segment. Otherwise empty."""
|
|
262
|
+
path = os.path.abspath(os.path.expanduser(prefix.strip()))
|
|
263
|
+
if self._has_conda_launcher(path):
|
|
264
|
+
return path
|
|
265
|
+
name = os.path.basename(path)
|
|
266
|
+
envs = os.path.dirname(path)
|
|
267
|
+
if os.path.basename(envs) != "envs":
|
|
268
|
+
return ""
|
|
269
|
+
if not self._conda_name_token(name):
|
|
270
|
+
return ""
|
|
271
|
+
root = os.path.dirname(envs)
|
|
272
|
+
if self._has_conda_launcher(root):
|
|
273
|
+
return root
|
|
274
|
+
return ""
|
|
275
|
+
|
|
276
|
+
def _prefix_inside_conda(self, root, prefix):
|
|
277
|
+
"""The prefix when it is the root or root/envs/<name>. Otherwise empty."""
|
|
278
|
+
path = os.path.abspath(os.path.expanduser(prefix.strip()))
|
|
279
|
+
root_abs = os.path.abspath(root)
|
|
280
|
+
if path == root_abs:
|
|
281
|
+
return path
|
|
282
|
+
envs = os.path.join(root_abs, "envs")
|
|
283
|
+
marker = envs + os.sep
|
|
284
|
+
if not path.startswith(marker):
|
|
285
|
+
return ""
|
|
286
|
+
if not self._conda_name_token(path[len(marker):]):
|
|
287
|
+
return ""
|
|
288
|
+
return path
|
|
289
|
+
|
|
290
|
+
def conda_root(self):
|
|
291
|
+
"""
|
|
292
|
+
General Purpose: Conda root when bin/conda is present, else empty.
|
|
293
|
+
A non-empty CONDA_EXE is the only candidate. Otherwise a non-empty
|
|
294
|
+
CONDA_PREFIX. Otherwise the first login install that has bin/conda.
|
|
295
|
+
"""
|
|
296
|
+
exe = os.environ.get("CONDA_EXE")
|
|
297
|
+
if exe is not None and exe.strip() != "":
|
|
298
|
+
return self._root_from_conda_exe(exe)
|
|
299
|
+
prefix = os.environ.get("CONDA_PREFIX")
|
|
300
|
+
if prefix is not None and prefix.strip() != "":
|
|
301
|
+
return self._root_from_conda_prefix(prefix)
|
|
302
|
+
home = os.path.expanduser("~")
|
|
303
|
+
if home == "" or home == "~":
|
|
304
|
+
return ""
|
|
305
|
+
for name in ("miniconda3", "anaconda3", "miniforge3", "mambaforge"):
|
|
306
|
+
root = os.path.abspath(os.path.join(home, name))
|
|
307
|
+
if self._has_conda_launcher(root):
|
|
308
|
+
return root
|
|
309
|
+
return ""
|
|
310
|
+
|
|
311
|
+
def under_conda(self):
|
|
312
|
+
"""General Purpose: True when this check has a conda root."""
|
|
313
|
+
return self.conda_root() != ""
|
|
314
|
+
|
|
315
|
+
def conda_location(self):
|
|
316
|
+
"""
|
|
317
|
+
General Purpose: The conda launcher at bin/conda.
|
|
318
|
+
This is not the condabin file that can appear first on PATH.
|
|
319
|
+
"""
|
|
320
|
+
root = self.conda_root()
|
|
321
|
+
if root == "":
|
|
322
|
+
return ""
|
|
323
|
+
return os.path.join(root, "bin", "conda")
|
|
324
|
+
|
|
325
|
+
def conda_prefix(self):
|
|
326
|
+
"""
|
|
327
|
+
General Purpose: Active conda prefix inside the root, else the root.
|
|
328
|
+
Empty when the check is not under conda.
|
|
329
|
+
"""
|
|
330
|
+
root = self.conda_root()
|
|
331
|
+
if root == "":
|
|
332
|
+
return ""
|
|
333
|
+
named = os.environ.get("CONDA_PREFIX")
|
|
334
|
+
if named is not None and named.strip() != "":
|
|
335
|
+
inside = self._prefix_inside_conda(root, named)
|
|
336
|
+
if inside != "":
|
|
337
|
+
return inside
|
|
338
|
+
return root
|
|
339
|
+
|
|
340
|
+
def conda_interpreter(self, name):
|
|
341
|
+
"""
|
|
342
|
+
General Purpose: python2 or python3 inside the conda prefix, or empty.
|
|
343
|
+
An active prefix does not borrow the base interpreter.
|
|
344
|
+
"""
|
|
345
|
+
prefix = self.conda_prefix()
|
|
346
|
+
if prefix == "":
|
|
347
|
+
return ""
|
|
348
|
+
candidate = os.path.join(prefix, "bin", name)
|
|
349
|
+
if os.path.lexists(candidate):
|
|
350
|
+
return candidate
|
|
351
|
+
return ""
|
|
352
|
+
|
|
353
|
+
def about_tool_location(self, name):
|
|
354
|
+
"""
|
|
355
|
+
General Purpose: Location line for python2, python3, conda, or pyenv.
|
|
356
|
+
Under conda, python2 and python3 stay inside the prefix and conda is bin/conda.
|
|
357
|
+
Otherwise under pyenv, those interpreters stay inside the pyenv root.
|
|
358
|
+
"""
|
|
359
|
+
if name == "conda" and self.under_conda():
|
|
360
|
+
return self.conda_location()
|
|
361
|
+
if name == "pyenv" and self.under_pyenv():
|
|
362
|
+
return self.pyenv_location()
|
|
363
|
+
if name in ("python2", "python3") and self.under_conda():
|
|
364
|
+
return self.conda_interpreter(name)
|
|
365
|
+
if name in ("python2", "python3") and self.under_pyenv():
|
|
366
|
+
return self.pyenv_interpreter(name)
|
|
367
|
+
return self.command_location(name)
|
|
368
|
+
|
|
369
|
+
def os_text(self):
|
|
370
|
+
"""General Purpose: Pretty operating-system name, such as Ubuntu 24.04 LTS."""
|
|
371
|
+
try:
|
|
372
|
+
info = platform.freedesktop_os_release()
|
|
373
|
+
except Exception:
|
|
374
|
+
info = {}
|
|
375
|
+
pretty = info.get("PRETTY_NAME") or ""
|
|
376
|
+
if pretty:
|
|
377
|
+
return pretty
|
|
378
|
+
return platform.platform()
|
|
379
|
+
|
|
380
|
+
def inside_docker(self, marker="/.dockerenv"):
|
|
381
|
+
"""General Purpose: True when the docker marker file is present."""
|
|
382
|
+
return os.path.exists(marker)
|
|
383
|
+
|
|
384
|
+
def cpython_soabi(self):
|
|
385
|
+
"""General Purpose: CPython ABI string shown as the Cython string."""
|
|
386
|
+
value = sysconfig.get_config_var("SOABI") or ""
|
|
387
|
+
return value
|
|
388
|
+
|
|
389
|
+
def process_id(self):
|
|
390
|
+
"""General Purpose: This process id as decimal text."""
|
|
391
|
+
return str(os.getpid())
|
|
392
|
+
|
|
393
|
+
def _home_dir(self):
|
|
394
|
+
"""HOME when set. Otherwise an absolute expanduser result. Else empty."""
|
|
395
|
+
home = os.environ.get("HOME") or ""
|
|
396
|
+
if home.strip():
|
|
397
|
+
return home
|
|
398
|
+
try:
|
|
399
|
+
expanded = os.path.expanduser("~")
|
|
400
|
+
except Exception:
|
|
401
|
+
return ""
|
|
402
|
+
if expanded and os.path.isabs(expanded):
|
|
403
|
+
return expanded
|
|
404
|
+
return ""
|
|
405
|
+
|
|
406
|
+
def _cache_leaf(self, username, pid_text):
|
|
407
|
+
"""Preferred leaf. No empty username segment."""
|
|
408
|
+
if username:
|
|
409
|
+
return "cache-{}-{}-{}".format(self.app_name, username, pid_text)
|
|
410
|
+
return "cache-{}-{}".format(self.app_name, pid_text)
|
|
411
|
+
|
|
412
|
+
def cache_folder_preferred(self):
|
|
413
|
+
"""General Purpose: RAM cache candidate under /dev/shm/cache."""
|
|
414
|
+
leaf = self._cache_leaf(self.current_user(), self.process_id())
|
|
415
|
+
return os.path.join("/dev/shm", "cache", leaf)
|
|
416
|
+
|
|
417
|
+
def cache_folder_first_fallback(self):
|
|
418
|
+
"""General Purpose: First cache fallback under /tmp/cache."""
|
|
419
|
+
leaf = self._cache_leaf(self.current_user(), self.process_id())
|
|
420
|
+
return os.path.join("/tmp", "cache", leaf)
|
|
421
|
+
|
|
422
|
+
def cache_folder_second_fallback(self):
|
|
423
|
+
"""General Purpose: Second cache fallback. The leaf has no username."""
|
|
424
|
+
home = self._home_dir()
|
|
425
|
+
if not home:
|
|
426
|
+
return ""
|
|
427
|
+
leaf = "cache-{}-{}".format(self.app_name, self.process_id())
|
|
428
|
+
return os.path.join(home, ".cache", leaf)
|
|
429
|
+
|
|
430
|
+
def cache_folder_used(self):
|
|
431
|
+
"""
|
|
432
|
+
General Purpose: Which cache candidate this run would use.
|
|
433
|
+
Directory existence only. This read does not create the folder.
|
|
434
|
+
"""
|
|
435
|
+
if os.path.isdir("/dev/shm"):
|
|
436
|
+
return self.cache_folder_preferred()
|
|
437
|
+
if os.path.isdir("/tmp"):
|
|
438
|
+
return self.cache_folder_first_fallback()
|
|
439
|
+
return self.cache_folder_second_fallback()
|
|
440
|
+
|
|
441
|
+
def persistence_storage(self):
|
|
442
|
+
"""General Purpose: Durable data directory under the login home."""
|
|
443
|
+
home = self._home_dir()
|
|
444
|
+
if not home:
|
|
445
|
+
return ""
|
|
446
|
+
return os.path.join(home, ".local", self.app_name)
|
|
447
|
+
|
|
448
|
+
def tty_interactive(self, stream=None):
|
|
449
|
+
"""General Purpose: yes when stdout is a terminal, otherwise no."""
|
|
450
|
+
target = sys.stdout if stream is None else stream
|
|
451
|
+
try:
|
|
452
|
+
interactive = bool(target.isatty())
|
|
453
|
+
except Exception:
|
|
454
|
+
interactive = False
|
|
455
|
+
return "yes" if interactive else "no"
|
|
456
|
+
|
|
457
|
+
def self_location(self):
|
|
458
|
+
"""
|
|
459
|
+
General Purpose: Path of the running program.
|
|
460
|
+
A console-script argv wins. Otherwise the CLI module file.
|
|
461
|
+
The about page names that program file, so this read stays on it.
|
|
462
|
+
"""
|
|
463
|
+
here = os.path.realpath(
|
|
464
|
+
os.path.join(os.path.dirname(os.path.abspath(__file__)), "cli.py")
|
|
465
|
+
)
|
|
466
|
+
argv0 = sys.argv[0] if sys.argv else ""
|
|
467
|
+
if argv0 and os.path.isfile(argv0):
|
|
468
|
+
argv_real = os.path.realpath(argv0)
|
|
469
|
+
base = os.path.basename(argv_real)
|
|
470
|
+
if base in (self.console_name, self.app_name):
|
|
471
|
+
return argv_real
|
|
472
|
+
same_dir = os.path.dirname(argv_real) == os.path.dirname(here)
|
|
473
|
+
if same_dir and base in ("cli.py", "__main__.py"):
|
|
474
|
+
return argv_real
|
|
475
|
+
return here
|
|
476
|
+
|
|
477
|
+
def check_system_lines(self, now=None):
|
|
478
|
+
"""
|
|
479
|
+
General Purpose: Host check block for the about page.
|
|
480
|
+
The stamp is local time with microseconds.
|
|
481
|
+
"""
|
|
482
|
+
when = now if now is not None else datetime.datetime.now()
|
|
483
|
+
stamp = when.strftime("%Y-%m-%d %H:%M:%S.%f")
|
|
484
|
+
version_text = sys.version
|
|
485
|
+
py_text, c_library = self.parse_sys_version(
|
|
486
|
+
version_text, platform.python_version()
|
|
487
|
+
)
|
|
488
|
+
arch = self.arch_label(version_text)
|
|
489
|
+
shell = self.shell_text()
|
|
490
|
+
libc = self.libc_label(version_text, shell)
|
|
491
|
+
rows = [
|
|
492
|
+
"{} {}(v{}) [CHECK SYSTEM]:".format(stamp, self.app_name, self.version),
|
|
493
|
+
" Now checking your operation system!",
|
|
494
|
+
" Python: {}".format(py_text),
|
|
495
|
+
" C Library: {}".format(c_library),
|
|
496
|
+
" Operation System: {}".format(self.os_text()),
|
|
497
|
+
" Architecture: {}".format(arch),
|
|
498
|
+
" Current User: {}".format(self.current_user()),
|
|
499
|
+
" Shell: {}".format(shell),
|
|
500
|
+
" Python Executable: {}".format(self.python_executable_name()),
|
|
501
|
+
" python2 location: {}".format(self.about_tool_location("python2")),
|
|
502
|
+
" python3 location: {}".format(self.about_tool_location("python3")),
|
|
503
|
+
" conda location: {}".format(self.about_tool_location("conda")),
|
|
504
|
+
" pyenv location: {}".format(self.about_tool_location("pyenv")),
|
|
505
|
+
" Inside docker container: {}".format(self.inside_docker()),
|
|
506
|
+
" Cython String: {}".format(self.cpython_soabi()),
|
|
507
|
+
" Binary Type: {}".format(self.binary_type(arch, libc)),
|
|
508
|
+
" Location: {}".format(self.self_location()),
|
|
509
|
+
" PID: {}".format(self.process_id()),
|
|
510
|
+
" Cache folder used: {}".format(self.cache_folder_used()),
|
|
511
|
+
" Cache folder (preferred): {}".format(self.cache_folder_preferred()),
|
|
512
|
+
" Cache folder (1st fallback): {}".format(
|
|
513
|
+
self.cache_folder_first_fallback()
|
|
514
|
+
),
|
|
515
|
+
" Cache folder (2nd fallback): {}".format(
|
|
516
|
+
self.cache_folder_second_fallback()
|
|
517
|
+
),
|
|
518
|
+
" Persistence storage: {}".format(self.persistence_storage()),
|
|
519
|
+
" TTY / Interactive: {}".format(self.tty_interactive()),
|
|
520
|
+
]
|
|
521
|
+
return rows
|