pyhabitat 1.0.16__py3-none-any.whl → 1.0.18__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.
Potentially problematic release.
This version of pyhabitat might be problematic. Click here for more details.
- pyhabitat/__init__.py +26 -18
- pyhabitat/__main__.py +4 -0
- pyhabitat/__main__stable.py +72 -0
- pyhabitat/cli.py +22 -0
- pyhabitat/environment.py +323 -141
- {pyhabitat-1.0.16.dist-info → pyhabitat-1.0.18.dist-info}/METADATA +62 -34
- pyhabitat-1.0.18.dist-info/RECORD +11 -0
- pyhabitat-1.0.18.dist-info/entry_points.txt +2 -0
- pyhabitat-1.0.16.dist-info/RECORD +0 -7
- {pyhabitat-1.0.16.dist-info → pyhabitat-1.0.18.dist-info}/WHEEL +0 -0
- {pyhabitat-1.0.16.dist-info → pyhabitat-1.0.18.dist-info}/licenses/LICENSE +0 -0
- {pyhabitat-1.0.16.dist-info → pyhabitat-1.0.18.dist-info}/top_level.txt +0 -0
pyhabitat/__init__.py
CHANGED
|
@@ -4,23 +4,27 @@ from .environment import (
|
|
|
4
4
|
matplotlib_is_available_for_gui_plotting,
|
|
5
5
|
matplotlib_is_available_for_headless_image_export,
|
|
6
6
|
tkinter_is_available,
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
7
|
+
in_repl,
|
|
8
|
+
on_termux,
|
|
9
|
+
on_freebsd,
|
|
10
|
+
on_linux,
|
|
11
|
+
on_android,
|
|
12
|
+
on_windows,
|
|
13
|
+
on_apple,
|
|
14
|
+
on_ish_alpine,
|
|
15
|
+
as_pyinstaller,
|
|
16
|
+
as_frozen,
|
|
16
17
|
is_elf,
|
|
17
18
|
is_pyz,
|
|
18
19
|
is_windows_portable_executable,
|
|
19
20
|
is_macos_executable,
|
|
20
21
|
is_pipx,
|
|
22
|
+
is_python_script,
|
|
21
23
|
interactive_terminal_is_available,
|
|
22
24
|
web_browser_is_available,
|
|
23
25
|
edit_textfile,
|
|
26
|
+
interp_path,
|
|
27
|
+
main,
|
|
24
28
|
)
|
|
25
29
|
|
|
26
30
|
# Optional: Set __all__ for explicit documentation and cleaner imports
|
|
@@ -28,21 +32,25 @@ __all__ = [
|
|
|
28
32
|
'matplotlib_is_available_for_gui_plotting',
|
|
29
33
|
'matplotlib_is_available_for_headless_image_export',
|
|
30
34
|
'tkinter_is_available',
|
|
31
|
-
'
|
|
32
|
-
'
|
|
33
|
-
'
|
|
34
|
-
'
|
|
35
|
-
'
|
|
36
|
-
'
|
|
37
|
-
'
|
|
38
|
-
'
|
|
39
|
-
'
|
|
35
|
+
'in_repl',
|
|
36
|
+
'on_termux',
|
|
37
|
+
'on_freebsd',
|
|
38
|
+
'on_linux',
|
|
39
|
+
'on_android',
|
|
40
|
+
'on_windows',
|
|
41
|
+
'on_apple',
|
|
42
|
+
'on_ish_alpine',
|
|
43
|
+
'as_pyinstaller',
|
|
44
|
+
'as_frozen',
|
|
40
45
|
'is_elf',
|
|
41
46
|
'is_pyz',
|
|
42
47
|
'is_windows_portable_executable',
|
|
43
48
|
'is_macos_executable',
|
|
44
49
|
'is_pipx',
|
|
50
|
+
'is_python_script',
|
|
45
51
|
'interactive_terminal_is_available',
|
|
46
52
|
'web_browser_is_available',
|
|
47
53
|
'edit_textfile',
|
|
54
|
+
'interp_path',
|
|
55
|
+
'main',
|
|
48
56
|
]
|
pyhabitat/__main__.py
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
#from .cli import run_cli
|
|
2
|
+
|
|
3
|
+
from .environment import (
|
|
4
|
+
in_repl,
|
|
5
|
+
on_termux,
|
|
6
|
+
on_windows,
|
|
7
|
+
on_apple,
|
|
8
|
+
on_linux,
|
|
9
|
+
on_ish_alpine,
|
|
10
|
+
on_android,
|
|
11
|
+
on_freebsd,
|
|
12
|
+
is_elf,
|
|
13
|
+
is_windows_portable_executable,
|
|
14
|
+
is_macos_executable,
|
|
15
|
+
is_pyz,
|
|
16
|
+
is_pipx,
|
|
17
|
+
is_python_script,
|
|
18
|
+
as_frozen,
|
|
19
|
+
as_pyinstaller,
|
|
20
|
+
interp_path,
|
|
21
|
+
tkinter_is_available,
|
|
22
|
+
matplotlib_is_available_for_gui_plotting,
|
|
23
|
+
matplotlib_is_available_for_headless_image_export,
|
|
24
|
+
web_browser_is_available,
|
|
25
|
+
interactive_terminal_is_available
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
def main():
|
|
29
|
+
print("PyHabitat Environment Report")
|
|
30
|
+
print("===========================")
|
|
31
|
+
print("\nInterpreter Checks // Based on sys.executable()")
|
|
32
|
+
print("-----------------------------")
|
|
33
|
+
print(f"interp_path(): {interp_path()}")
|
|
34
|
+
print(f"is_elf(interp_path()): {is_elf(interp_path())}")
|
|
35
|
+
print(f"is_windows_portable_executable(interp_path()): {is_windows_portable_executable(interp_path())}")
|
|
36
|
+
print(f"is_macos_executable(interp_path()): {is_macos_executable(interp_path())}")
|
|
37
|
+
print(f"is_pyz(interp_path()): {is_pyz(interp_path())}")
|
|
38
|
+
print(f"is_pipx(interp_path()): {is_pipx(interp_path())}")
|
|
39
|
+
print(f"is_python_script(interp_path()): {is_python_script(interp_path())}")
|
|
40
|
+
print("\nCurrent Environment Check // Based on sys.argv[0]")
|
|
41
|
+
print("-----------------------------")
|
|
42
|
+
print(f"is_elf(): {is_elf()}")
|
|
43
|
+
print(f"is_windows_portable_executable(): {is_windows_portable_executable()}")
|
|
44
|
+
print(f"is_macos_executable(): {is_macos_executable()}")
|
|
45
|
+
print(f"is_pyz(): {is_pyz()}")
|
|
46
|
+
print(f"is_pipx(): {is_pipx()}")
|
|
47
|
+
print(f"is_python_script(): {is_python_script()}")
|
|
48
|
+
print(f"\nCurrent Build Checks // Based on hasattr(sys,..) and getattr(sys,..)")
|
|
49
|
+
print("------------------------------")
|
|
50
|
+
print(f"in_repl(): {in_repl()}")
|
|
51
|
+
print(f"as_frozen(): {as_frozen()}")
|
|
52
|
+
print(f"as_pyinstaller(): {as_pyinstaller()}")
|
|
53
|
+
print("\nOperating System Checks // Based on platform.system()")
|
|
54
|
+
print("------------------------------")
|
|
55
|
+
print(f"on_termux(): {on_termux()}")
|
|
56
|
+
print(f"on_windows(): {on_windows()}")
|
|
57
|
+
print(f"on_apple(): {on_apple()}")
|
|
58
|
+
print(f"on_linux(): {on_linux()}")
|
|
59
|
+
print(f"on_ish_alpine(): {on_ish_alpine()}")
|
|
60
|
+
print(f"on_android(): {on_android()}")
|
|
61
|
+
print(f"on_freebsd(): {on_freebsd()}")
|
|
62
|
+
print("\nCapability Checks")
|
|
63
|
+
print("-------------------------")
|
|
64
|
+
print(f"tkinter_is_available(): {tkinter_is_available()}")
|
|
65
|
+
print(f"matplotlib_is_available_for_gui_plotting(): {matplotlib_is_available_for_gui_plotting()}")
|
|
66
|
+
print(f"matplotlib_is_available_for_headless_image_export(): {matplotlib_is_available_for_headless_image_export()}")
|
|
67
|
+
print(f"web_browser_is_available(): {web_browser_is_available()}")
|
|
68
|
+
print(f"interactive_terminal_is_available(): {interactive_terminal_is_available()}")
|
|
69
|
+
|
|
70
|
+
if __name__ == "__main__":
|
|
71
|
+
main()
|
|
72
|
+
#run_cli()
|
pyhabitat/cli.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from . import main
|
|
4
|
+
|
|
5
|
+
def run_cli():
|
|
6
|
+
"""Parse CLI arguments and run the pyhabitat environment report."""
|
|
7
|
+
parser = argparse.ArgumentParser(
|
|
8
|
+
description="PyHabitat: Python environment and build introspection"
|
|
9
|
+
)
|
|
10
|
+
parser.add_argument(
|
|
11
|
+
"--path",
|
|
12
|
+
type=str,
|
|
13
|
+
default=None,
|
|
14
|
+
help="Path to a script or binary to inspect (defaults to sys.argv[0])",
|
|
15
|
+
)
|
|
16
|
+
parser.add_argument(
|
|
17
|
+
"--debug",
|
|
18
|
+
action="store_true",
|
|
19
|
+
help="Enable verbose debug output",
|
|
20
|
+
)
|
|
21
|
+
args = parser.parse_args()
|
|
22
|
+
main(path=Path(args.path) if args.path else None, debug=args.debug)
|
pyhabitat/environment.py
CHANGED
|
@@ -13,12 +13,40 @@ from pathlib import Path
|
|
|
13
13
|
import subprocess
|
|
14
14
|
import io
|
|
15
15
|
import zipfile
|
|
16
|
+
import logging
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
'matplotlib_is_available_for_gui_plotting',
|
|
20
|
+
'matplotlib_is_available_for_headless_image_export',
|
|
21
|
+
'tkinter_is_available',
|
|
22
|
+
'on_termux',
|
|
23
|
+
'on_freebsd',
|
|
24
|
+
'on_linux',
|
|
25
|
+
'on_android',
|
|
26
|
+
'on_windows',
|
|
27
|
+
'on_apple',
|
|
28
|
+
'on_ish_alpine',
|
|
29
|
+
'as_pyinstaller',
|
|
30
|
+
'as_frozen',
|
|
31
|
+
'is_elf',
|
|
32
|
+
'is_pyz',
|
|
33
|
+
'is_windows_portable_executable',
|
|
34
|
+
'is_macos_executable',
|
|
35
|
+
'is_pipx',
|
|
36
|
+
'interactive_terminal_is_available',
|
|
37
|
+
'web_browser_is_available',
|
|
38
|
+
'edit_textfile',
|
|
39
|
+
'in_repl',
|
|
40
|
+
'interp_path',
|
|
41
|
+
'main',
|
|
42
|
+
]
|
|
16
43
|
|
|
17
44
|
# Global cache for tkinter and matplotlib (mpl) availability
|
|
18
45
|
_TKINTER_AVAILABILITY: bool | None = None
|
|
19
46
|
_MATPLOTLIB_EXPORT_AVAILABILITY: bool | None = None
|
|
20
47
|
_MATPLOTLIB_WINDOWED_AVAILABILITY: bool | None = None
|
|
21
48
|
|
|
49
|
+
|
|
22
50
|
# --- GUI CHECKS ---
|
|
23
51
|
def matplotlib_is_available_for_gui_plotting(termux_has_gui=False):
|
|
24
52
|
"""Check if Matplotlib is available AND can use a GUI backend for a popup window."""
|
|
@@ -29,7 +57,7 @@ def matplotlib_is_available_for_gui_plotting(termux_has_gui=False):
|
|
|
29
57
|
|
|
30
58
|
# 1. Termux exclusion check (assume no X11/GUI)
|
|
31
59
|
# Exclude Termux UNLESS the user explicitly provides termux_has_gui=True.
|
|
32
|
-
if
|
|
60
|
+
if on_termux() and not termux_has_gui:
|
|
33
61
|
_MATPLOTLIB_WINDOWED_AVAILABILITY = False
|
|
34
62
|
return False
|
|
35
63
|
|
|
@@ -118,7 +146,7 @@ def tkinter_is_available() -> bool:
|
|
|
118
146
|
return False
|
|
119
147
|
|
|
120
148
|
# --- ENVIRONMENT AND OPERATING SYSTEM CHECKS ---
|
|
121
|
-
def
|
|
149
|
+
def on_termux() -> bool:
|
|
122
150
|
"""Detect if running in Termux environment on Android, based on Termux-specific environmental variables."""
|
|
123
151
|
|
|
124
152
|
if platform.system() != 'Linux':
|
|
@@ -145,22 +173,22 @@ def is_termux() -> bool:
|
|
|
145
173
|
|
|
146
174
|
return False
|
|
147
175
|
|
|
148
|
-
def
|
|
176
|
+
def on_freebsd() -> bool:
|
|
149
177
|
"""Detect if running on FreeBSD."""
|
|
150
178
|
return platform.system() == 'FreeBSD'
|
|
151
179
|
|
|
152
|
-
def
|
|
180
|
+
def on_linux():
|
|
153
181
|
"""Detect if running on Linux."""
|
|
154
182
|
return platform.system() == 'Linux'
|
|
155
183
|
|
|
156
|
-
def
|
|
184
|
+
def on_android() -> bool:
|
|
157
185
|
"""
|
|
158
186
|
Detect if running on Android.
|
|
159
187
|
|
|
160
|
-
Note: The
|
|
161
|
-
Checking for Termux with
|
|
188
|
+
Note: The on_termux() function is more robust and safe for Termux.
|
|
189
|
+
Checking for Termux with on_termux() does not require checking for Android with on_android().
|
|
162
190
|
|
|
163
|
-
|
|
191
|
+
on_android() will be True on:
|
|
164
192
|
- Sandboxed IDE's:
|
|
165
193
|
- Pydroid3
|
|
166
194
|
- QPython
|
|
@@ -170,7 +198,7 @@ def is_android() -> bool:
|
|
|
170
198
|
- UserLand
|
|
171
199
|
- AnLinux
|
|
172
200
|
|
|
173
|
-
|
|
201
|
+
on_android() will be False on:
|
|
174
202
|
- Full Virtual Machines:
|
|
175
203
|
- VirtualBox
|
|
176
204
|
- VMware
|
|
@@ -181,15 +209,15 @@ def is_android() -> bool:
|
|
|
181
209
|
return False
|
|
182
210
|
return "android" in platform.platform().lower()
|
|
183
211
|
|
|
184
|
-
def
|
|
212
|
+
def on_windows() -> bool:
|
|
185
213
|
"""Detect if running on Windows."""
|
|
186
214
|
return platform.system() == 'Windows'
|
|
187
215
|
|
|
188
|
-
def
|
|
216
|
+
def on_apple() -> bool:
|
|
189
217
|
"""Detect if running on Apple."""
|
|
190
218
|
return platform.system() == 'Darwin'
|
|
191
219
|
|
|
192
|
-
def
|
|
220
|
+
def on_ish_alpine() -> bool:
|
|
193
221
|
"""Detect if running in iSH Alpine environment on iOS."""
|
|
194
222
|
# platform.system() usually returns 'Linux' in iSH
|
|
195
223
|
|
|
@@ -208,16 +236,29 @@ def is_ish_alpine() -> bool:
|
|
|
208
236
|
|
|
209
237
|
return False
|
|
210
238
|
|
|
239
|
+
def in_repl() -> bool:
|
|
240
|
+
"""
|
|
241
|
+
Detects if the code is running in the Python interactive REPL (e.g., when 'python' is typed in a console).
|
|
242
|
+
|
|
243
|
+
This function specifically checks for the Python REPL by verifying the presence of the interactive
|
|
244
|
+
prompt (`sys.ps1`). It returns False for other interactive terminal scenarios, such as running a
|
|
245
|
+
PyInstaller binary in a console.
|
|
246
|
+
|
|
247
|
+
Returns:
|
|
248
|
+
bool: True if running in the Python REPL; False otherwise.
|
|
249
|
+
"""
|
|
250
|
+
return hasattr(sys, 'ps1')
|
|
251
|
+
|
|
211
252
|
|
|
212
253
|
# --- BUILD AND EXECUTABLE CHECKS ---
|
|
213
254
|
|
|
214
|
-
def
|
|
255
|
+
def as_pyinstaller():
|
|
215
256
|
"""Detects if the Python script is running as a 'frozen' in the course of generating a PyInstaller binary executable."""
|
|
216
257
|
# If the app is frozen AND has the PyInstaller-specific temporary folder path
|
|
217
|
-
return
|
|
258
|
+
return as_frozen() and hasattr(sys, '_MEIPASS')
|
|
218
259
|
|
|
219
260
|
# The standard way to check for a frozen state:
|
|
220
|
-
def
|
|
261
|
+
def as_frozen():
|
|
221
262
|
"""
|
|
222
263
|
Detects if the Python script is running as a 'frozen' (standalone)
|
|
223
264
|
executable created by a tool like PyInstaller, cx_Freeze, or Nuitka.
|
|
@@ -235,109 +276,78 @@ def is_frozen():
|
|
|
235
276
|
"""
|
|
236
277
|
return getattr(sys, 'frozen', False)
|
|
237
278
|
|
|
238
|
-
|
|
279
|
+
# --- Binary Characteristic Checks ---
|
|
280
|
+
def is_elf(exec_path: Path | str | None = None, debug: bool = False) -> bool:
|
|
239
281
|
"""Checks if the currently running executable (sys.argv[0]) is a standalone PyInstaller-built ELF binary."""
|
|
240
282
|
# If it's a pipx installation, it is not the monolithic binary we are concerned with here.
|
|
241
|
-
|
|
242
|
-
if
|
|
243
|
-
exec_path = Path(sys.argv[0]).resolve()
|
|
244
|
-
if debug:
|
|
245
|
-
print(f"exec_path = {exec_path}")
|
|
246
|
-
if is_pipx():
|
|
283
|
+
exec_path, is_valid = _check_executable_path(exec_path, debug)
|
|
284
|
+
if not is_valid:
|
|
247
285
|
return False
|
|
248
286
|
|
|
249
|
-
# Check if the file exists and is readable
|
|
250
|
-
if not exec_path.is_file():
|
|
251
|
-
return False
|
|
252
|
-
|
|
253
287
|
try:
|
|
254
288
|
# Check the magic number: The first four bytes of an ELF file are 0x7f, 'E', 'L', 'F' (b'\x7fELF').
|
|
255
289
|
# This is the most reliable way to determine if the executable is a native binary wrapper (like PyInstaller's).
|
|
256
290
|
with open(exec_path, 'rb') as f:
|
|
257
291
|
magic_bytes = f.read(4)
|
|
258
|
-
|
|
292
|
+
if debug:
|
|
293
|
+
logging.debug(f"Magic bytes: {magic_bytes}")
|
|
259
294
|
return magic_bytes == b'\x7fELF'
|
|
260
295
|
except Exception:
|
|
261
|
-
|
|
296
|
+
if debug:
|
|
297
|
+
logging.debug("False (Exception during file check)")
|
|
262
298
|
return False
|
|
263
299
|
|
|
264
|
-
def is_pyz(exec_path: Path=None, debug=False) -> bool:
|
|
300
|
+
def is_pyz(exec_path: Path | str | None = None, debug: bool = False) -> bool:
|
|
265
301
|
"""Checks if the currently running executable (sys.argv[0]) is a PYZ zipapp ."""
|
|
302
|
+
|
|
266
303
|
# If it's a pipx installation, it is not the monolithic binary we are concerned with here.
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
if debug:
|
|
270
|
-
print(f"exec_path = {exec_path}")
|
|
271
|
-
|
|
272
|
-
if is_pipx():
|
|
304
|
+
exec_path, is_valid = _check_executable_path(exec_path, debug)
|
|
305
|
+
if not is_valid:
|
|
273
306
|
return False
|
|
274
307
|
|
|
275
308
|
# Check if the extension is PYZ
|
|
276
309
|
if not str(exec_path).endswith(".pyz"):
|
|
310
|
+
if debug:
|
|
311
|
+
logging.debug("False (Not a .pyz file)")
|
|
277
312
|
return False
|
|
278
|
-
|
|
279
|
-
if not _check_if_zip():
|
|
313
|
+
|
|
314
|
+
if not _check_if_zip(exec_path):
|
|
315
|
+
if debug:
|
|
316
|
+
logging.debug("False (Not a valid ZIP file)")
|
|
280
317
|
return False
|
|
281
318
|
|
|
282
|
-
|
|
319
|
+
return True
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def is_windows_portable_executable(exec_path: Path | str | None = None, debug: bool = False) -> bool:
|
|
283
323
|
"""
|
|
284
|
-
Checks if the
|
|
285
|
-
Windows Portable Executable (PE) binary, and explicitly excludes
|
|
286
|
-
pipx-managed environments.
|
|
324
|
+
Checks if the specified path or sys.argv[0] is a Windows Portable Executable (PE) binary.
|
|
287
325
|
Windows Portable Executables include .exe, .dll, and other binaries.
|
|
288
326
|
The standard way to check for a PE is to look for the MZ magic number at the very beginning of the file.
|
|
289
327
|
"""
|
|
290
|
-
|
|
291
|
-
if
|
|
292
|
-
exec_path = Path(sys.argv[0]).resolve()
|
|
293
|
-
|
|
294
|
-
if debug:
|
|
295
|
-
print(f"DEBUG: Checking executable path: {exec_path}")
|
|
296
|
-
|
|
297
|
-
# 2. Exclude pipx environments immediately
|
|
298
|
-
if is_pipx():
|
|
299
|
-
if debug: print("DEBUG: is_exe_non_pipx: False (is_pipx is True)")
|
|
300
|
-
return False
|
|
301
|
-
|
|
302
|
-
# 3. Perform file checks
|
|
303
|
-
if not exec_path.is_file():
|
|
304
|
-
if debug: print("DEBUG: is_exe_non_pipx: False (Not a file)")
|
|
328
|
+
exec_path, is_valid = _check_executable_path(exec_path, debug)
|
|
329
|
+
if not is_valid:
|
|
305
330
|
return False
|
|
306
|
-
|
|
307
331
|
try:
|
|
308
|
-
# Check the magic number: All Windows PE files (EXE, DLL, etc.)
|
|
309
|
-
# start with the two-byte header b'MZ' (for Mark Zbikowski).
|
|
310
332
|
with open(exec_path, 'rb') as f:
|
|
311
333
|
magic_bytes = f.read(2)
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
if debug:
|
|
316
|
-
print(f"DEBUG: Magic bytes: {magic_bytes}")
|
|
317
|
-
print(f"DEBUG: is_exe_non_pipx: {is_pe} (Non-pipx check)")
|
|
318
|
-
|
|
319
|
-
return is_pe
|
|
320
|
-
|
|
334
|
+
if debug:
|
|
335
|
+
logging.debug(f"Magic bytes: {magic_bytes}")
|
|
336
|
+
return magic_bytes == b'MZ'
|
|
321
337
|
except Exception as e:
|
|
322
|
-
if debug:
|
|
323
|
-
|
|
338
|
+
if debug:
|
|
339
|
+
logging.debug(f"False (Error during file check: {e})")
|
|
324
340
|
return False
|
|
325
|
-
|
|
326
|
-
def is_macos_executable(exec_path: Path = None, debug=False) -> bool:
|
|
341
|
+
|
|
342
|
+
def is_macos_executable(exec_path: Path | str | None = None, debug: bool = False) -> bool:
|
|
327
343
|
"""
|
|
328
344
|
Checks if the currently running executable is a macOS/Darwin Mach-O binary,
|
|
329
345
|
and explicitly excludes pipx-managed environments.
|
|
330
346
|
"""
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
if is_pipx():
|
|
335
|
-
if debug: print("DEBUG: is_macos_executable: False (is_pipx is True)")
|
|
347
|
+
exec_path, is_valid = _check_executable_path(exec_path, debug)
|
|
348
|
+
if not is_valid:
|
|
336
349
|
return False
|
|
337
350
|
|
|
338
|
-
if not exec_path.is_file():
|
|
339
|
-
return False
|
|
340
|
-
|
|
341
351
|
try:
|
|
342
352
|
# Check the magic number: Mach-O binaries start with specific 4-byte headers.
|
|
343
353
|
# Common ones are: b'\xfe\xed\xfa\xce' (32-bit) or b'\xfe\xed\xfa\xcf' (64-bit)
|
|
@@ -355,68 +365,128 @@ def is_macos_executable(exec_path: Path = None, debug=False) -> bool:
|
|
|
355
365
|
is_macho = magic_bytes in MACHO_MAGIC
|
|
356
366
|
|
|
357
367
|
if debug:
|
|
358
|
-
|
|
368
|
+
logging.debug(f"Magic bytes: {magic_bytes}")
|
|
359
369
|
|
|
360
370
|
return is_macho
|
|
361
371
|
|
|
362
372
|
except Exception:
|
|
373
|
+
if debug:
|
|
374
|
+
logging.debug("False (Exception during file check)")
|
|
363
375
|
return False
|
|
364
376
|
|
|
365
|
-
def is_pipx(debug=False) -> bool:
|
|
377
|
+
def is_pipx(exec_path: Path | str | None = None, debug: bool = False) -> bool:
|
|
366
378
|
"""Checks if the executable is running from a pipx managed environment."""
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
return str(p).lower()
|
|
371
|
-
|
|
372
|
-
exec_path = Path(sys.argv[0]).resolve()
|
|
379
|
+
exec_path, is_valid = _check_executable_path(exec_path, debug, check_pipx=False)
|
|
380
|
+
if not is_valid:
|
|
381
|
+
return False
|
|
373
382
|
|
|
374
|
-
|
|
375
|
-
# In a pipx-managed execution, this is the venv python.
|
|
383
|
+
try:
|
|
376
384
|
interpreter_path = Path(sys.executable).resolve()
|
|
377
385
|
pipx_bin_path, pipx_venv_base_path = _get_pipx_paths()
|
|
378
386
|
# Normalize paths for comparison
|
|
379
|
-
norm_exec_path =
|
|
380
|
-
norm_interp_path =
|
|
387
|
+
norm_exec_path = str(exec_path).lower()
|
|
388
|
+
norm_interp_path = str(interpreter_path).lower()
|
|
381
389
|
|
|
382
390
|
if debug:
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
# ------------------------
|
|
390
|
-
|
|
391
|
-
# 1. Signature Check (Most Robust): Look for the unique 'pipx/venvs' string.
|
|
392
|
-
# This is a strong check for both the executable path (your discovery)
|
|
393
|
-
# and the interpreter path (canonical venv location).
|
|
391
|
+
logging.debug(f"EXEC_PATH: {exec_path}")
|
|
392
|
+
logging.debug(f"INTERP_PATH: {interpreter_path}")
|
|
393
|
+
logging.debug(f"PIPX_BIN_PATH: {pipx_bin_path}")
|
|
394
|
+
logging.debug(f"PIPX_VENV_BASE: {pipx_venv_base_path}")
|
|
395
|
+
logging.debug(f"Check B result: {norm_interp_path.startswith(str(pipx_venv_base_path).lower())}")
|
|
396
|
+
|
|
394
397
|
if "pipx/venvs" in norm_exec_path or "pipx/venvs" in norm_interp_path:
|
|
395
|
-
if debug:
|
|
398
|
+
if debug:
|
|
399
|
+
logging.debug("True (Signature Check)")
|
|
396
400
|
return True
|
|
397
401
|
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
if debug: print("is_pipx: True (Interpreter Base Check)")
|
|
402
|
+
if norm_interp_path.startswith(str(pipx_venv_base_path).lower()):
|
|
403
|
+
if debug:
|
|
404
|
+
logging.debug("True (Interpreter Base Check)")
|
|
402
405
|
return True
|
|
403
|
-
|
|
404
|
-
# 3. Targeted Executable Check: The executable's resolved path starts with the PIPX venv base.
|
|
405
|
-
# This is your key Termux discovery, confirming the shim resolves into the venv.
|
|
406
|
-
if norm_exec_path.startswith(normalize_path(pipx_venv_base_path)):
|
|
407
|
-
if debug: print("is_pipx: True (Executable Base Check)")
|
|
408
|
-
return True
|
|
409
406
|
|
|
410
|
-
if
|
|
407
|
+
if norm_exec_path.startswith(str(pipx_venv_base_path).lower()):
|
|
408
|
+
if debug:
|
|
409
|
+
logging.debug("True (Executable Base Check)")
|
|
410
|
+
return True
|
|
411
|
+
|
|
412
|
+
if debug:
|
|
413
|
+
logging.debug("False")
|
|
414
|
+
return False
|
|
415
|
+
except Exception:
|
|
416
|
+
if debug:
|
|
417
|
+
logging.debug("False (Exception during pipx check)")
|
|
411
418
|
return False
|
|
419
|
+
if debug:
|
|
420
|
+
logging.debug(f"EXEC_PATH: {exec_path}")
|
|
421
|
+
logging.debug(f"INTERP_PATH: {interpreter_path}")
|
|
422
|
+
logging.debug(f"PIPX_BIN_PATH: {pipx_bin_path}")
|
|
423
|
+
logging.debug(f"PIPX_VENV_BASE: {pipx_venv_base_path}")
|
|
424
|
+
logging.debug(f"Check B result: {norm_interp_path.startswith(str(pipx_venv_base_path).lower())}")
|
|
425
|
+
|
|
426
|
+
if "pipx/venvs" in norm_exec_path or "pipx/venvs" in norm_interp_path:
|
|
427
|
+
if debug:
|
|
428
|
+
logging.debug("True (Signature Check)")
|
|
429
|
+
return True
|
|
430
|
+
|
|
431
|
+
if norm_interp_path.startswith(str(pipx_venv_base_path).lower()):
|
|
432
|
+
if debug:
|
|
433
|
+
logging.debug("True (Interpreter Base Check)")
|
|
434
|
+
return True
|
|
412
435
|
|
|
436
|
+
if norm_exec_path.startswith(str(pipx_venv_base_path).lower()):
|
|
437
|
+
if debug:
|
|
438
|
+
logging.debug("True (Executable Base Check)")
|
|
439
|
+
return True
|
|
440
|
+
|
|
441
|
+
if debug:
|
|
442
|
+
logging.debug("False")
|
|
443
|
+
return False
|
|
413
444
|
except Exception:
|
|
414
|
-
|
|
445
|
+
if debug:
|
|
446
|
+
logging.debug("False (Exception during pipx check)")
|
|
415
447
|
return False
|
|
416
|
-
|
|
448
|
+
def is_python_script(path: Path | str | None = None, debug: bool = False) -> bool:
|
|
449
|
+
"""
|
|
450
|
+
Checks if the specified path or running script is a Python source file (.py).
|
|
417
451
|
|
|
452
|
+
By default, checks the running script (`sys.argv[0]`). If a specific `path` is
|
|
453
|
+
provided, checks that path instead. Uses `Path.resolve()` for stable path handling.
|
|
418
454
|
|
|
419
|
-
|
|
455
|
+
Args:
|
|
456
|
+
path: Optional; path to the file to check (str or Path). If None, defaults to `sys.argv[0]`.
|
|
457
|
+
debug: If True, prints the path being checked.
|
|
458
|
+
|
|
459
|
+
Returns:
|
|
460
|
+
bool: True if the specified or default path is a Python source file (.py); False otherwise.
|
|
461
|
+
"""
|
|
462
|
+
exec_path, is_valid = _check_executable_path(path, debug, check_pipx=False)
|
|
463
|
+
if not is_valid:
|
|
464
|
+
return False
|
|
465
|
+
return exec_path.suffix.lower() == '.py'
|
|
466
|
+
|
|
467
|
+
# --- Interpreter Check ---
|
|
468
|
+
|
|
469
|
+
def interp_path(print_path: bool = False) -> str:
|
|
470
|
+
"""
|
|
471
|
+
Returns the path to the Python interpreter binary and optionally prints it.
|
|
472
|
+
|
|
473
|
+
This function wraps `sys.executable` to provide the path to the interpreter
|
|
474
|
+
(e.g., '/data/data/com.termux/files/usr/bin/python3' in Termux or the embedded
|
|
475
|
+
interpreter in a frozen executable). If the path is empty (e.g., in some embedded
|
|
476
|
+
or sandboxed environments), an empty string is returned.
|
|
477
|
+
|
|
478
|
+
Args:
|
|
479
|
+
print_path: If True, prints the interpreter path to stdout.
|
|
480
|
+
|
|
481
|
+
Returns:
|
|
482
|
+
str: The path to the Python interpreter binary, or an empty string if unavailable.
|
|
483
|
+
"""
|
|
484
|
+
path = sys.executable
|
|
485
|
+
if print_path:
|
|
486
|
+
print(f"Python interpreter path: {path}")
|
|
487
|
+
return path
|
|
488
|
+
|
|
489
|
+
# --- TTY Check ---
|
|
420
490
|
def interactive_terminal_is_available():
|
|
421
491
|
"""
|
|
422
492
|
Check if the script is running in an interactive terminal.
|
|
@@ -440,7 +510,7 @@ def web_browser_is_available() -> bool:
|
|
|
440
510
|
except webbrowser.Error:
|
|
441
511
|
# Fallback needed. Check for external launchers.
|
|
442
512
|
# 2. Termux specific check
|
|
443
|
-
if
|
|
513
|
+
if on_termux() and shutil.which("termux-open-url"):
|
|
444
514
|
return True
|
|
445
515
|
# 3. General Linux check
|
|
446
516
|
if shutil.which("xdg-open"):
|
|
@@ -448,34 +518,42 @@ def web_browser_is_available() -> bool:
|
|
|
448
518
|
return False
|
|
449
519
|
|
|
450
520
|
# --- LAUNCH MECHANISMS BASED ON ENVIRONMENT ---
|
|
451
|
-
def edit_textfile(
|
|
452
|
-
#def open_text_file_for_editing(
|
|
521
|
+
def edit_textfile(path: Path | str | None = None) -> None:
|
|
522
|
+
#def open_text_file_for_editing(path): # defunct function name as of 1.0.16
|
|
453
523
|
"""
|
|
454
524
|
Opens a file with the environment's default application (Windows, Linux, macOS)
|
|
455
525
|
or a guaranteed console editor (nano) in constrained environments (Termux, iSH)
|
|
456
526
|
after ensuring line-ending compatibility.
|
|
527
|
+
|
|
528
|
+
This function is known to fail on PyDroid3, where on_linus() is True but xdg-open
|
|
529
|
+
is not available.
|
|
457
530
|
"""
|
|
531
|
+
if path is None:
|
|
532
|
+
return
|
|
533
|
+
|
|
534
|
+
path = Path(path).resolve()
|
|
535
|
+
|
|
458
536
|
try:
|
|
459
|
-
if
|
|
460
|
-
os.startfile(
|
|
461
|
-
elif
|
|
537
|
+
if on_windows():
|
|
538
|
+
os.startfile(path)
|
|
539
|
+
elif on_termux():
|
|
462
540
|
# Install dependencies if missing (Termux pkg returns non-zero if already installed, so no check=True)
|
|
463
541
|
subprocess.run(['pkg','install', 'dos2unix', 'nano'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
464
|
-
_run_dos2unix(
|
|
465
|
-
subprocess.run(['nano',
|
|
466
|
-
elif
|
|
542
|
+
_run_dos2unix(path)
|
|
543
|
+
subprocess.run(['nano', path])
|
|
544
|
+
elif on_ish_alpine():
|
|
467
545
|
# Install dependencies if missing (apk returns 0 if already installed, so check=True is safe)
|
|
468
546
|
subprocess.run(['apk','add', 'dos2unix'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=True)
|
|
469
547
|
subprocess.run(['apk','add', 'nano'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=True)
|
|
470
|
-
_run_dos2unix(
|
|
471
|
-
subprocess.run(['nano',
|
|
548
|
+
_run_dos2unix(path)
|
|
549
|
+
subprocess.run(['nano', path])
|
|
472
550
|
# --- Standard Unix-like Systems (Conversion + Default App) ---
|
|
473
|
-
elif
|
|
474
|
-
_run_dos2unix(
|
|
475
|
-
subprocess.run(['xdg-open',
|
|
476
|
-
elif
|
|
477
|
-
_run_dos2unix(
|
|
478
|
-
subprocess.run(['open',
|
|
551
|
+
elif on_linux():
|
|
552
|
+
_run_dos2unix(path) # Safety conversion for user-defined console apps
|
|
553
|
+
subprocess.run(['xdg-open', path])
|
|
554
|
+
elif on_apple():
|
|
555
|
+
_run_dos2unix(path) # Safety conversion for user-defined console apps
|
|
556
|
+
subprocess.run(['open', path])
|
|
479
557
|
else:
|
|
480
558
|
print("Unsupported operating system.")
|
|
481
559
|
except Exception as e:
|
|
@@ -484,14 +562,18 @@ def edit_textfile(filepath) -> None:
|
|
|
484
562
|
"""Why Not Use check=True on Termux:
|
|
485
563
|
The pkg utility in Termux is a wrapper around Debian's apt. When you run pkg install <package>, if the package is already installed, the utility often returns an exit code of 100 (or another non-zero value) to indicate that no changes were made because the package was already present.
|
|
486
564
|
"""
|
|
487
|
-
|
|
488
|
-
|
|
565
|
+
|
|
566
|
+
# --- Helper Functions ---
|
|
567
|
+
def _run_dos2unix(path: Path | str | None = None):
|
|
489
568
|
"""Attempt to run dos2unix, failing silently if not installed."""
|
|
569
|
+
|
|
570
|
+
path = Path(path).resolve()
|
|
571
|
+
|
|
490
572
|
try:
|
|
491
573
|
# We rely on shutil.which not being needed, as this is a robust built-in utility on most targets
|
|
492
574
|
# The command won't raise an exception unless the process itself fails, not just if the utility isn't found.
|
|
493
575
|
# We also don't use check=True here to allow silent failure if the utility is missing (e.g., minimalist Linux).
|
|
494
|
-
subprocess.run(['dos2unix',
|
|
576
|
+
subprocess.run(['dos2unix', path], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
495
577
|
except FileNotFoundError:
|
|
496
578
|
# This will be raised if 'dos2unix' is not on the system PATH
|
|
497
579
|
pass
|
|
@@ -526,11 +608,111 @@ def _get_pipx_paths():
|
|
|
526
608
|
return pipx_bin_path, pipx_venv_base.resolve()
|
|
527
609
|
|
|
528
610
|
|
|
529
|
-
def _check_if_zip(
|
|
611
|
+
def _check_if_zip(path: Path | str | None) -> bool:
|
|
530
612
|
"""Checks if the file at the given path is a valid ZIP archive."""
|
|
613
|
+
if path is None:
|
|
614
|
+
return False
|
|
615
|
+
path = Path(path).resolve()
|
|
616
|
+
|
|
531
617
|
try:
|
|
532
|
-
return zipfile.is_zipfile(
|
|
618
|
+
return zipfile.is_zipfile(path)
|
|
533
619
|
except Exception:
|
|
534
620
|
# Handle cases where the path might be invalid, or other unexpected errors
|
|
535
621
|
return False
|
|
536
|
-
|
|
622
|
+
|
|
623
|
+
def _check_executable_path(exec_path: Path | str | None, debug: bool = False, check_pipx: bool = True) -> tuple[Path | None, bool]:
|
|
624
|
+
"""Helper function to resolve executable path and perform common checks."""
|
|
625
|
+
if exec_path is None:
|
|
626
|
+
exec_path = Path(sys.argv[0]).resolve() if sys.argv[0] and sys.argv[0] != '-c' else None
|
|
627
|
+
else:
|
|
628
|
+
exec_path = Path(exec_path).resolve()
|
|
629
|
+
|
|
630
|
+
if debug:
|
|
631
|
+
logging.debug(f"Checking executable path: {exec_path}")
|
|
632
|
+
|
|
633
|
+
if exec_path is None:
|
|
634
|
+
if debug:
|
|
635
|
+
logging.debug("False (No valid path)")
|
|
636
|
+
return None, False
|
|
637
|
+
|
|
638
|
+
if check_pipx and is_pipx(exec_path, debug):
|
|
639
|
+
if debug:
|
|
640
|
+
logging.debug("False (is_pipx is True)")
|
|
641
|
+
return exec_path, False
|
|
642
|
+
|
|
643
|
+
if not exec_path.is_file():
|
|
644
|
+
if debug:
|
|
645
|
+
logging.debug("False (Not a file)")
|
|
646
|
+
return exec_path, False
|
|
647
|
+
|
|
648
|
+
return exec_path, True
|
|
649
|
+
|
|
650
|
+
# --- Main Function for report and CLI compatibility ---
|
|
651
|
+
|
|
652
|
+
def main(path=None, debug=False):
|
|
653
|
+
"""Print a comprehensive environment report.
|
|
654
|
+
|
|
655
|
+
Args:
|
|
656
|
+
path (Path | str | None): Path to inspect (defaults to sys.argv[0]).
|
|
657
|
+
debug (bool): Enable verbose debug output.
|
|
658
|
+
"""
|
|
659
|
+
if debug:
|
|
660
|
+
logging.basicConfig(level=logging.DEBUG)
|
|
661
|
+
logging.getLogger('matplotlib').setLevel(logging.WARNING) # Suppress matplotlib debug logs
|
|
662
|
+
print("PyHabitat Environment Report")
|
|
663
|
+
print("===========================")
|
|
664
|
+
print("\nCurrent Build Checks // Based on hasattr(sys,..) and getattr(sys,..)")
|
|
665
|
+
print("------------------------------")
|
|
666
|
+
print(f"in_repl(): {in_repl()}")
|
|
667
|
+
print(f"as_frozen(): {as_frozen()}")
|
|
668
|
+
print(f"as_pyinstaller(): {as_pyinstaller()}")
|
|
669
|
+
print("\nOperating System Checks // Based on platform.system()")
|
|
670
|
+
print("------------------------------")
|
|
671
|
+
print(f"on_termux(): {on_termux()}")
|
|
672
|
+
print(f"on_windows(): {on_windows()}")
|
|
673
|
+
print(f"on_apple(): {on_apple()}")
|
|
674
|
+
print(f"on_linux(): {on_linux()}")
|
|
675
|
+
print(f"on_ish_alpine(): {on_ish_alpine()}")
|
|
676
|
+
print(f"on_android(): {on_android()}")
|
|
677
|
+
print(f"on_freebsd(): {on_freebsd()}")
|
|
678
|
+
print("\nCapability Checks")
|
|
679
|
+
print("-------------------------")
|
|
680
|
+
print(f"tkinter_is_available(): {tkinter_is_available()}")
|
|
681
|
+
print(f"matplotlib_is_available_for_gui_plotting(): {matplotlib_is_available_for_gui_plotting()}")
|
|
682
|
+
print(f"matplotlib_is_available_for_headless_image_export(): {matplotlib_is_available_for_headless_image_export()}")
|
|
683
|
+
print(f"web_browser_is_available(): {web_browser_is_available()}")
|
|
684
|
+
print(f"interactive_terminal_is_available(): {interactive_terminal_is_available()}")
|
|
685
|
+
print("\nInterpreter Checks // Based on sys.executable()")
|
|
686
|
+
print("-----------------------------")
|
|
687
|
+
print(f"interp_path(): {interp_path()}")
|
|
688
|
+
print(f"is_elf(interp_path()): {is_elf(interp_path(), debug=debug)}")
|
|
689
|
+
print(f"is_windows_portable_executable(interp_path()): {is_windows_portable_executable(interp_path(), debug=debug)}")
|
|
690
|
+
print(f"is_macos_executable(interp_path()): {is_macos_executable(interp_path(), debug=debug)}")
|
|
691
|
+
print(f"is_pyz(interp_path()): {is_pyz(interp_path(), debug=debug)}")
|
|
692
|
+
print(f"is_pipx(interp_path()): {is_pipx(interp_path(), debug=debug)}")
|
|
693
|
+
print(f"is_python_script(interp_path()): {is_python_script(interp_path(), debug=debug)}")
|
|
694
|
+
print("\nCurrent Environment Check // Based on sys.argv[0]")
|
|
695
|
+
print("-----------------------------")
|
|
696
|
+
inspect_path = path if path is not None else (None if sys.argv[0] == '-c' else sys.argv[0])
|
|
697
|
+
logging.debug(f"Inspecting path: {inspect_path}")
|
|
698
|
+
# Early validation of path
|
|
699
|
+
if path is not None:
|
|
700
|
+
path_obj = Path(path)
|
|
701
|
+
if not path_obj.is_file():
|
|
702
|
+
print(f"Error: '{path}' is not a valid file or does not exist.")
|
|
703
|
+
if debug:
|
|
704
|
+
logging.error(f"Invalid path: '{path}' is not a file or does not exist.")
|
|
705
|
+
raise SystemExit(1)
|
|
706
|
+
script_path = None
|
|
707
|
+
if path or (sys.argv[0] and sys.argv[0] != '-c'):
|
|
708
|
+
script_path = Path(path or sys.argv[0]).resolve()
|
|
709
|
+
logging.debug(f"Script path resolved: {script_path}")
|
|
710
|
+
if script_path is not None:
|
|
711
|
+
print(f"is_elf(): {is_elf(script_path, debug=debug)}")
|
|
712
|
+
print(f"is_windows_portable_executable(): {is_windows_portable_executable(script_path, debug=debug)}")
|
|
713
|
+
print(f"is_macos_executable(): {is_macos_executable(script_path, debug=debug)}")
|
|
714
|
+
print(f"is_pyz(): {is_pyz(script_path, debug=debug)}")
|
|
715
|
+
print(f"is_pipx(): {is_pipx(script_path, debug=debug)}")
|
|
716
|
+
print(f"is_python_script(): {is_python_script(script_path, debug=debug)}")
|
|
717
|
+
else:
|
|
718
|
+
print("script_path is None")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pyhabitat
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.18
|
|
4
4
|
Summary: A lightweight library for detecting system environment, GUI, and build properties.
|
|
5
5
|
Author-email: George Clayton Bennett <george.bennett@memphistn.gov>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -8,7 +8,7 @@ Keywords: environment,os-detection,gui,build-system
|
|
|
8
8
|
Classifier: Programming Language :: Python :: 3
|
|
9
9
|
Classifier: Operating System :: OS Independent
|
|
10
10
|
Classifier: Topic :: System :: Systems Administration
|
|
11
|
-
Requires-Python: >=3.
|
|
11
|
+
Requires-Python: >=3.7
|
|
12
12
|
Description-Content-Type: text/markdown
|
|
13
13
|
License-File: LICENSE
|
|
14
14
|
Dynamic: license-file
|
|
@@ -59,11 +59,11 @@ Ultimately, [City-of-Memphis-Wastewater](https://github.com/City-of-Memphis-Wast
|
|
|
59
59
|
|
|
60
60
|
* **Definitive Environment Checks:** Rigorous checks catered to Termux and iSH (iOS Alpine). Accurate, typical modern detection for Windows, macOS (Apple), Linux, FreeBSD, Android.
|
|
61
61
|
* **GUI Availability:** Rigorous, cached checks to determine if the environment supports a graphical popup window (Tkinter/Matplotlib TkAgg) or just headless image export (Matplotlib Agg).
|
|
62
|
-
* **Build/Packaging Detection:** Reliable detection of standalone executables
|
|
63
|
-
* **Executable Type Inspection:** Uses file magic numbers (ELF
|
|
62
|
+
* **Build/Packaging Detection:** Reliable detection of standalone executables (PyInstaller), Python zipapps (.pyz), Python source scripts (.py), and correct identification/exclusion of pipx-managed virtual environments.
|
|
63
|
+
* **Executable Type Inspection:** Uses file magic numbers (ELF, MZ, Mach-O) to confirm if the running script is a monolithic, frozen binary (non-pipx) or zipapp (.pyz).
|
|
64
64
|
|
|
65
65
|
</details>
|
|
66
|
-
|
|
66
|
+
|
|
67
67
|
---
|
|
68
68
|
|
|
69
69
|
<details>
|
|
@@ -75,26 +75,31 @@ Key question: "What is this running on?"
|
|
|
75
75
|
|
|
76
76
|
| Function | Description |
|
|
77
77
|
| :--- | :--- |
|
|
78
|
-
| `
|
|
79
|
-
| `
|
|
80
|
-
| `
|
|
81
|
-
| `
|
|
82
|
-
| `
|
|
83
|
-
| `
|
|
78
|
+
| `on_windows()` | Returns `True` on Windows. |
|
|
79
|
+
| `on_apple()` | Returns `True` on macOS (Darwin). |
|
|
80
|
+
| `on_linux()` | Returns `True` on Linux in general. |
|
|
81
|
+
| `on_termux()` | Returns `True` if running in the Termux Android environment. |
|
|
82
|
+
| `on_freebsd()` | Returns `True` on FreeBSD. |
|
|
83
|
+
| `on_ish_alpine()` | Returns `True` if running in the iSH Alpine Linux iOS emulator. |
|
|
84
|
+
| `on_android()` | Returns `True` on any Android-based Linux environment. |
|
|
85
|
+
| `in_repl()` | Returns `True` is the user is currently in a Python REPL; hasattr(sys,'ps1'). |
|
|
84
86
|
|
|
85
87
|
### Packaging and Build Checking
|
|
86
88
|
|
|
87
|
-
Key question: "What is the character of my executable?"
|
|
89
|
+
Key question: "What is the character of my executable or my build state?"
|
|
90
|
+
|
|
91
|
+
These functions accept an optional path argument (Path or str), defaulting to sys.argv[0] (e.g., pyhabitat/__main__.py for python -m pyhabitat, empty in REPL). Path.resolve() is used for stability.
|
|
88
92
|
|
|
89
93
|
| Function | Description |
|
|
90
94
|
| :--- | :--- |
|
|
91
|
-
| `
|
|
92
|
-
| `
|
|
93
|
-
| `
|
|
94
|
-
| `
|
|
95
|
-
| `
|
|
96
|
-
| `
|
|
97
|
-
|
|
95
|
+
| `as_frozen()` | Returns `True` if the script is running as a standalone executable (any bundler). |
|
|
96
|
+
| `as_pyinstaller()` | Returns `True` if the script is frozen and generated by PyInstaller (has `_MEIPASS`). |
|
|
97
|
+
| `is_python_script(path=None)` | Returns `True` if the script or specified path is a Python source file (.py). |
|
|
98
|
+
| `is_pipx(path=None)` | Returns `True` if the script or specified path is from a pipx-managed virtual environment. |
|
|
99
|
+
| `is_elf(path=None)` | Returns `True` if the script or specified path is an ELF binary (Linux standalone executable, non-pipx). |
|
|
100
|
+
| `is_pyz(path=None)` | Returns `True` if the script or specified path is a Python zipapp (.pyz, non-pipx). |
|
|
101
|
+
| `is_windows_portable_executable(path=None)` | Returns `True` if the script or specified path is a Windows PE binary (MZ header, non-pipx). |
|
|
102
|
+
| `is_macos_executable(path=None)` | Returns `True` if the script or specified path is a macOS Mach-O binary (non-pipx). |
|
|
98
103
|
|
|
99
104
|
### Capability Checking
|
|
100
105
|
|
|
@@ -103,16 +108,18 @@ Key Question: "What could I do next?"
|
|
|
103
108
|
| Function | Description |
|
|
104
109
|
| :--- | :--- |
|
|
105
110
|
| `tkinter_is_available()` | Checks if Tkinter is imported and can successfully create a window. |
|
|
106
|
-
| `matplotlib_is_available_for_gui_plotting(termux_has_gui=False)` | Checks for Matplotlib and its TkAgg backend, required for interactive plotting. |
|
|
111
|
+
| `matplotlib_is_available_for_gui_plotting(termux_has_gui=False)` | Checks for Matplotlib and its TkAgg backend, required for interactive plotting. Set `termux_has_gui=True` for Termux with GUI support; defaults to `False`. |
|
|
107
112
|
| `matplotlib_is_available_for_headless_image_export()` | Checks for Matplotlib and its Agg backend, required for saving images without a GUI. |
|
|
108
113
|
| `interactive_terminal_is_available()` | Checks if standard input and output streams are connected to a TTY (allows safe use of interactive prompts). |
|
|
109
114
|
| `web_browser_is_available()` | Check if a web browser can be launched in the current environment (allows safe use of web-based prompts and localhost plotting). |
|
|
110
115
|
|
|
111
|
-
###
|
|
116
|
+
### Utility
|
|
112
117
|
|
|
113
118
|
| Function | Description |
|
|
114
119
|
| :--- | :--- |
|
|
115
|
-
| `edit_textfile()` |
|
|
120
|
+
| `edit_textfile(path)` | Opens a text file for editing using the default editor (Windows, Linux, macOS) or nano in Termux/iSH. In REPL mode, prints an error. Path argument (str or Path) uses Path.resolve() for stability. |
|
|
121
|
+
| `interp_path(print_path=False)` | Returns the path to the Python interpreter binary (sys.executable). Optionally prints the path. Returns empty string if unavailable. |
|
|
122
|
+
| `main()` | Prints a comprehensive environment report with sections: Interpreter Checks (sys.executable), Current Environment Check (sys.argv[0]), Current Build Checks (sys attributes), Operating System Checks (platform.system()), and Capability Checks. Run via `python -m pyhabitat` or `import pyhabitat; pyhabitat.main()` in the REPL. |
|
|
116
123
|
|
|
117
124
|
</details>
|
|
118
125
|
|
|
@@ -123,39 +130,57 @@ Key Question: "What could I do next?"
|
|
|
123
130
|
|
|
124
131
|
The module exposes all detection functions directly for easy access.
|
|
125
132
|
|
|
126
|
-
### 0\.
|
|
133
|
+
### 0\. Example of PyHabitat in Action
|
|
127
134
|
|
|
128
135
|
The `pipeline-eds` package uses the `pyhabitat` library to handle [configuration](https://github.com/City-of-Memphis-Wastewater/pipeline/blob/main/src/pipeline/security_and_config.py) and [plotting](https://github.com/City-of-Memphis-Wastewater/pipeline/blob/main/src/pipeline/cli.py), among other things.
|
|
129
136
|
|
|
130
|
-
### 1\.
|
|
137
|
+
### 1\. Running the Environment Report
|
|
138
|
+
|
|
139
|
+
Run a comprehensive environment report from the command line or REPL to inspect the interpreter (sys.executable), running script (sys.argv[0]), build state, operating system, and capabilities.
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
# In the terminal
|
|
143
|
+
python -m pyhabitat
|
|
144
|
+
```
|
|
131
145
|
|
|
132
146
|
```python
|
|
133
|
-
|
|
147
|
+
# In the Python REPL
|
|
148
|
+
import pyhabitat as ph
|
|
149
|
+
ph.main()
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### 2\. Checking Environment and Build Type
|
|
153
|
+
|
|
154
|
+
```python
|
|
155
|
+
from pyhabitat import on_termux, on_windows, is_pipx, is_python_script, as_frozen
|
|
134
156
|
|
|
135
157
|
if is_pipx():
|
|
136
158
|
print("Running inside a pipx virtual environment. This is not a standalone binary.")
|
|
137
159
|
|
|
138
|
-
|
|
160
|
+
if as_frozen():
|
|
139
161
|
print("Running as a frozen executable (PyInstaller, cx_Freeze, etc.).")
|
|
140
162
|
|
|
141
|
-
|
|
163
|
+
if is_python_script():
|
|
164
|
+
print("Running as a Python source script (.py).")
|
|
165
|
+
|
|
166
|
+
if on_termux():
|
|
142
167
|
# Expected cases:
|
|
143
168
|
#- pkg install python-numpy python-cryptography
|
|
144
|
-
#- Avoiding matplotlib unless the user explicitly
|
|
169
|
+
#- Avoiding matplotlib unless the user explicitly sets termux_has_gui=True in matplotlib_is_available_for_gui_plotting().
|
|
145
170
|
#- Auto-selection of 'termux-open-url' and 'xdg-open' in logic.
|
|
146
171
|
#- Installation on the system, like orchestrating the construction of Termux Widget entries in ~/.shortcuts.
|
|
147
172
|
print("Running in the Termux environment on Android.")
|
|
148
173
|
|
|
149
|
-
|
|
174
|
+
if on_windows():
|
|
150
175
|
print("Running on Windows.")
|
|
151
176
|
```
|
|
152
177
|
|
|
153
|
-
###
|
|
178
|
+
### 3\. Checking GUI and Plotting Availability
|
|
154
179
|
|
|
155
180
|
Use these functions to determine if you can show an interactive plot or if you must save an image file.
|
|
156
181
|
|
|
157
182
|
```python
|
|
158
|
-
from pyhabitat import matplotlib_is_available_for_gui_plotting, matplotlib_is_available_for_headless_image_export
|
|
183
|
+
from pyhabitat import matplotlib_is_available_for_gui_plotting, matplotlib_is_available_for_headless_image_export
|
|
159
184
|
|
|
160
185
|
if matplotlib_is_available_for_gui_plotting():
|
|
161
186
|
# We can safely call plt.show()
|
|
@@ -173,13 +198,16 @@ else:
|
|
|
173
198
|
print("Matplotlib is not installed or the environment is too restrictive for plotting.")
|
|
174
199
|
```
|
|
175
200
|
|
|
176
|
-
###
|
|
201
|
+
### 4\. Text Editing
|
|
177
202
|
|
|
178
|
-
Use this function to
|
|
203
|
+
Use this function to open a text file for editing.
|
|
179
204
|
Ideal use case: Edit a configuration file, if prompted by a CLI command like 'config --textedit'.
|
|
180
205
|
|
|
181
206
|
```python
|
|
182
|
-
|
|
207
|
+
from pathlib import Path
|
|
208
|
+
import pyhabitat as ph
|
|
209
|
+
|
|
210
|
+
ph.edit_textfile(path=Path('./config.json'))
|
|
183
211
|
```
|
|
184
212
|
</details>
|
|
185
213
|
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
pyhabitat/__init__.py,sha256=GcpHBpfEer9O9dKBUREU1RTd5w0im9ph5ew4bs7jOvY,1228
|
|
2
|
+
pyhabitat/__main__.py,sha256=hhH17lkw-ZalKp9NolnPGwW0KYxbXirspWvhBKNyBks,67
|
|
3
|
+
pyhabitat/__main__stable.py,sha256=UACpHLrr_Rmf0L5dJCEae6kFzLn7dqCqIri68IBnb10,2910
|
|
4
|
+
pyhabitat/cli.py,sha256=vuRczazuumIaJl4Td2VvCR5lXcKKNH90V8Yx0KZUJck,663
|
|
5
|
+
pyhabitat/environment.py,sha256=7yXMCHvTRkfwwQnsgzrDXPDG9m8h8yhI5oCNlJTHWUA,28026
|
|
6
|
+
pyhabitat-1.0.18.dist-info/licenses/LICENSE,sha256=D4fg30ctUGnCJlWu3ONv5-V8JE1v3ctakoJTcVjsJlg,1072
|
|
7
|
+
pyhabitat-1.0.18.dist-info/METADATA,sha256=Xk4blTODZGW6NiuqN5e33SuePkTb-PnNU5pGGzeHXvs,10733
|
|
8
|
+
pyhabitat-1.0.18.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
9
|
+
pyhabitat-1.0.18.dist-info/entry_points.txt,sha256=409xZ-BrarQJJLtO-aActCGkL0FMhNVi9wsq3u7tRHM,52
|
|
10
|
+
pyhabitat-1.0.18.dist-info/top_level.txt,sha256=zXYK44Qu8EqxUETREvd2diMUaB5JiGRErkwFaoLQnnI,10
|
|
11
|
+
pyhabitat-1.0.18.dist-info/RECORD,,
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
pyhabitat/__init__.py,sha256=MiYIzYPqB3o6ZFXQ1C9BV2M2KI17BSq9CA6p8Uc7_h0,1096
|
|
2
|
-
pyhabitat/environment.py,sha256=v9e47TdbtMSiljkjr9-lfdUILQu95HLH0SB6k7QIba0,20757
|
|
3
|
-
pyhabitat-1.0.16.dist-info/licenses/LICENSE,sha256=D4fg30ctUGnCJlWu3ONv5-V8JE1v3ctakoJTcVjsJlg,1072
|
|
4
|
-
pyhabitat-1.0.16.dist-info/METADATA,sha256=APK_RIRIKYUzZ3nuNgcRUvbDhaHZnWXAwvatvPwU_Qg,9001
|
|
5
|
-
pyhabitat-1.0.16.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
6
|
-
pyhabitat-1.0.16.dist-info/top_level.txt,sha256=zXYK44Qu8EqxUETREvd2diMUaB5JiGRErkwFaoLQnnI,10
|
|
7
|
-
pyhabitat-1.0.16.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|