pyhabitat 1.0.16__py3-none-any.whl → 1.0.17__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 +27 -18
- pyhabitat/__main__.py +69 -0
- pyhabitat/environment.py +180 -56
- {pyhabitat-1.0.16.dist-info → pyhabitat-1.0.17.dist-info}/METADATA +61 -33
- pyhabitat-1.0.17.dist-info/RECORD +8 -0
- pyhabitat-1.0.16.dist-info/RECORD +0 -7
- {pyhabitat-1.0.16.dist-info → pyhabitat-1.0.17.dist-info}/WHEEL +0 -0
- {pyhabitat-1.0.16.dist-info → pyhabitat-1.0.17.dist-info}/licenses/LICENSE +0 -0
- {pyhabitat-1.0.16.dist-info → pyhabitat-1.0.17.dist-info}/top_level.txt +0 -0
pyhabitat/__init__.py
CHANGED
|
@@ -4,45 +4,54 @@ 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,
|
|
24
27
|
)
|
|
25
28
|
|
|
29
|
+
from .__main__ import main
|
|
30
|
+
|
|
26
31
|
# Optional: Set __all__ for explicit documentation and cleaner imports
|
|
27
32
|
__all__ = [
|
|
28
33
|
'matplotlib_is_available_for_gui_plotting',
|
|
29
34
|
'matplotlib_is_available_for_headless_image_export',
|
|
30
35
|
'tkinter_is_available',
|
|
31
|
-
'
|
|
32
|
-
'
|
|
33
|
-
'
|
|
34
|
-
'
|
|
35
|
-
'
|
|
36
|
-
'
|
|
37
|
-
'
|
|
38
|
-
'
|
|
39
|
-
'
|
|
36
|
+
'in_repl',
|
|
37
|
+
'on_termux',
|
|
38
|
+
'on_freebsd',
|
|
39
|
+
'on_linux',
|
|
40
|
+
'on_android',
|
|
41
|
+
'on_windows',
|
|
42
|
+
'on_apple',
|
|
43
|
+
'on_ish_alpine',
|
|
44
|
+
'as_pyinstaller',
|
|
45
|
+
'as_frozen',
|
|
40
46
|
'is_elf',
|
|
41
47
|
'is_pyz',
|
|
42
48
|
'is_windows_portable_executable',
|
|
43
49
|
'is_macos_executable',
|
|
44
50
|
'is_pipx',
|
|
51
|
+
'is_python_script',
|
|
45
52
|
'interactive_terminal_is_available',
|
|
46
53
|
'web_browser_is_available',
|
|
47
54
|
'edit_textfile',
|
|
55
|
+
'interp_path',
|
|
56
|
+
'main',
|
|
48
57
|
]
|
pyhabitat/__main__.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
from .environment import (
|
|
2
|
+
in_repl,
|
|
3
|
+
on_termux,
|
|
4
|
+
on_windows,
|
|
5
|
+
on_apple,
|
|
6
|
+
on_linux,
|
|
7
|
+
on_ish_alpine,
|
|
8
|
+
on_android,
|
|
9
|
+
on_freebsd,
|
|
10
|
+
is_elf,
|
|
11
|
+
is_windows_portable_executable,
|
|
12
|
+
is_macos_executable,
|
|
13
|
+
is_pyz,
|
|
14
|
+
is_pipx,
|
|
15
|
+
is_python_script,
|
|
16
|
+
as_frozen,
|
|
17
|
+
as_pyinstaller,
|
|
18
|
+
interp_path,
|
|
19
|
+
tkinter_is_available,
|
|
20
|
+
matplotlib_is_available_for_gui_plotting,
|
|
21
|
+
matplotlib_is_available_for_headless_image_export,
|
|
22
|
+
web_browser_is_available,
|
|
23
|
+
interactive_terminal_is_available
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
def main():
|
|
27
|
+
print("PyHabitat Environment Report")
|
|
28
|
+
print("===========================")
|
|
29
|
+
print("\nInterpreter Checks // Based on sys.executable()")
|
|
30
|
+
print("-----------------------------")
|
|
31
|
+
print(f"interp_path(): {interp_path()}")
|
|
32
|
+
print(f"is_elf(interp_path()): {is_elf(interp_path())}")
|
|
33
|
+
print(f"is_windows_portable_executable(interp_path()): {is_windows_portable_executable(interp_path())}")
|
|
34
|
+
print(f"is_macos_executable(interp_path()): {is_macos_executable(interp_path())}")
|
|
35
|
+
print(f"is_pyz(interp_path()): {is_pyz(interp_path())}")
|
|
36
|
+
print(f"is_pipx(interp_path()): {is_pipx(interp_path())}")
|
|
37
|
+
print(f"is_python_script(interp_path()): {is_python_script(interp_path())}")
|
|
38
|
+
print("\nCurrent Environment Check // Based on sys.argv[0]")
|
|
39
|
+
print("-----------------------------")
|
|
40
|
+
print(f"is_elf(): {is_elf()}")
|
|
41
|
+
print(f"is_windows_portable_executable(): {is_windows_portable_executable()}")
|
|
42
|
+
print(f"is_macos_executable(): {is_macos_executable()}")
|
|
43
|
+
print(f"is_pyz(): {is_pyz()}")
|
|
44
|
+
print(f"is_pipx(): {is_pipx()}")
|
|
45
|
+
print(f"is_python_script(): {is_python_script()}")
|
|
46
|
+
print(f"\nCurrent Build Checks // Based on hasattr(sys,..) and getattr(sys,..)")
|
|
47
|
+
print("------------------------------")
|
|
48
|
+
print(f"in_repl(): {in_repl()}")
|
|
49
|
+
print(f"as_frozen(): {as_frozen()}")
|
|
50
|
+
print(f"as_pyinstaller(): {as_pyinstaller()}")
|
|
51
|
+
print("\nOperating System Checks // Based on platform.system()")
|
|
52
|
+
print("------------------------------")
|
|
53
|
+
print(f"on_termux(): {on_termux()}")
|
|
54
|
+
print(f"on_windows(): {on_windows()}")
|
|
55
|
+
print(f"on_apple(): {on_apple()}")
|
|
56
|
+
print(f"on_linux(): {on_linux()}")
|
|
57
|
+
print(f"on_ish_alpine(): {on_ish_alpine()}")
|
|
58
|
+
print(f"on_android(): {on_android()}")
|
|
59
|
+
print(f"on_freebsd(): {on_freebsd()}")
|
|
60
|
+
print("\nCapability Checks")
|
|
61
|
+
print("-------------------------")
|
|
62
|
+
print(f"tkinter_is_available(): {tkinter_is_available()}")
|
|
63
|
+
print(f"matplotlib_is_available_for_gui_plotting(): {matplotlib_is_available_for_gui_plotting()}")
|
|
64
|
+
print(f"matplotlib_is_available_for_headless_image_export(): {matplotlib_is_available_for_headless_image_export()}")
|
|
65
|
+
print(f"web_browser_is_available(): {web_browser_is_available()}")
|
|
66
|
+
print(f"interactive_terminal_is_available(): {interactive_terminal_is_available()}")
|
|
67
|
+
|
|
68
|
+
if __name__ == "__main__":
|
|
69
|
+
main()
|
pyhabitat/environment.py
CHANGED
|
@@ -14,6 +14,31 @@ import subprocess
|
|
|
14
14
|
import io
|
|
15
15
|
import zipfile
|
|
16
16
|
|
|
17
|
+
__all__ = [
|
|
18
|
+
'matplotlib_is_available_for_gui_plotting',
|
|
19
|
+
'matplotlib_is_available_for_headless_image_export',
|
|
20
|
+
'tkinter_is_available',
|
|
21
|
+
'on_termux',
|
|
22
|
+
'on_freebsd',
|
|
23
|
+
'on_linux',
|
|
24
|
+
'on_android',
|
|
25
|
+
'on_windows',
|
|
26
|
+
'on_apple',
|
|
27
|
+
'on_ish_alpine',
|
|
28
|
+
'as_pyinstaller',
|
|
29
|
+
'as_frozen',
|
|
30
|
+
'is_elf',
|
|
31
|
+
'is_pyz',
|
|
32
|
+
'is_windows_portable_executable',
|
|
33
|
+
'is_macos_executable',
|
|
34
|
+
'is_pipx',
|
|
35
|
+
'interactive_terminal_is_available',
|
|
36
|
+
'web_browser_is_available',
|
|
37
|
+
'edit_textfile',
|
|
38
|
+
'in_repl',
|
|
39
|
+
'interp_path',
|
|
40
|
+
]
|
|
41
|
+
|
|
17
42
|
# Global cache for tkinter and matplotlib (mpl) availability
|
|
18
43
|
_TKINTER_AVAILABILITY: bool | None = None
|
|
19
44
|
_MATPLOTLIB_EXPORT_AVAILABILITY: bool | None = None
|
|
@@ -29,7 +54,7 @@ def matplotlib_is_available_for_gui_plotting(termux_has_gui=False):
|
|
|
29
54
|
|
|
30
55
|
# 1. Termux exclusion check (assume no X11/GUI)
|
|
31
56
|
# Exclude Termux UNLESS the user explicitly provides termux_has_gui=True.
|
|
32
|
-
if
|
|
57
|
+
if on_termux() and not termux_has_gui:
|
|
33
58
|
_MATPLOTLIB_WINDOWED_AVAILABILITY = False
|
|
34
59
|
return False
|
|
35
60
|
|
|
@@ -118,7 +143,7 @@ def tkinter_is_available() -> bool:
|
|
|
118
143
|
return False
|
|
119
144
|
|
|
120
145
|
# --- ENVIRONMENT AND OPERATING SYSTEM CHECKS ---
|
|
121
|
-
def
|
|
146
|
+
def on_termux() -> bool:
|
|
122
147
|
"""Detect if running in Termux environment on Android, based on Termux-specific environmental variables."""
|
|
123
148
|
|
|
124
149
|
if platform.system() != 'Linux':
|
|
@@ -145,22 +170,22 @@ def is_termux() -> bool:
|
|
|
145
170
|
|
|
146
171
|
return False
|
|
147
172
|
|
|
148
|
-
def
|
|
173
|
+
def on_freebsd() -> bool:
|
|
149
174
|
"""Detect if running on FreeBSD."""
|
|
150
175
|
return platform.system() == 'FreeBSD'
|
|
151
176
|
|
|
152
|
-
def
|
|
177
|
+
def on_linux():
|
|
153
178
|
"""Detect if running on Linux."""
|
|
154
179
|
return platform.system() == 'Linux'
|
|
155
180
|
|
|
156
|
-
def
|
|
181
|
+
def on_android() -> bool:
|
|
157
182
|
"""
|
|
158
183
|
Detect if running on Android.
|
|
159
184
|
|
|
160
|
-
Note: The
|
|
161
|
-
Checking for Termux with
|
|
185
|
+
Note: The on_termux() function is more robust and safe for Termux.
|
|
186
|
+
Checking for Termux with on_termux() does not require checking for Android with on_android().
|
|
162
187
|
|
|
163
|
-
|
|
188
|
+
on_android() will be True on:
|
|
164
189
|
- Sandboxed IDE's:
|
|
165
190
|
- Pydroid3
|
|
166
191
|
- QPython
|
|
@@ -170,7 +195,7 @@ def is_android() -> bool:
|
|
|
170
195
|
- UserLand
|
|
171
196
|
- AnLinux
|
|
172
197
|
|
|
173
|
-
|
|
198
|
+
on_android() will be False on:
|
|
174
199
|
- Full Virtual Machines:
|
|
175
200
|
- VirtualBox
|
|
176
201
|
- VMware
|
|
@@ -181,15 +206,15 @@ def is_android() -> bool:
|
|
|
181
206
|
return False
|
|
182
207
|
return "android" in platform.platform().lower()
|
|
183
208
|
|
|
184
|
-
def
|
|
209
|
+
def on_windows() -> bool:
|
|
185
210
|
"""Detect if running on Windows."""
|
|
186
211
|
return platform.system() == 'Windows'
|
|
187
212
|
|
|
188
|
-
def
|
|
213
|
+
def on_apple() -> bool:
|
|
189
214
|
"""Detect if running on Apple."""
|
|
190
215
|
return platform.system() == 'Darwin'
|
|
191
216
|
|
|
192
|
-
def
|
|
217
|
+
def on_ish_alpine() -> bool:
|
|
193
218
|
"""Detect if running in iSH Alpine environment on iOS."""
|
|
194
219
|
# platform.system() usually returns 'Linux' in iSH
|
|
195
220
|
|
|
@@ -208,16 +233,29 @@ def is_ish_alpine() -> bool:
|
|
|
208
233
|
|
|
209
234
|
return False
|
|
210
235
|
|
|
236
|
+
def in_repl() -> bool:
|
|
237
|
+
"""
|
|
238
|
+
Detects if the code is running in the Python interactive REPL (e.g., when 'python' is typed in a console).
|
|
239
|
+
|
|
240
|
+
This function specifically checks for the Python REPL by verifying the presence of the interactive
|
|
241
|
+
prompt (`sys.ps1`). It returns False for other interactive terminal scenarios, such as running a
|
|
242
|
+
PyInstaller binary in a console.
|
|
243
|
+
|
|
244
|
+
Returns:
|
|
245
|
+
bool: True if running in the Python REPL; False otherwise.
|
|
246
|
+
"""
|
|
247
|
+
return hasattr(sys, 'ps1')
|
|
248
|
+
|
|
211
249
|
|
|
212
250
|
# --- BUILD AND EXECUTABLE CHECKS ---
|
|
213
251
|
|
|
214
|
-
def
|
|
252
|
+
def as_pyinstaller():
|
|
215
253
|
"""Detects if the Python script is running as a 'frozen' in the course of generating a PyInstaller binary executable."""
|
|
216
254
|
# If the app is frozen AND has the PyInstaller-specific temporary folder path
|
|
217
|
-
return
|
|
255
|
+
return as_frozen() and hasattr(sys, '_MEIPASS')
|
|
218
256
|
|
|
219
257
|
# The standard way to check for a frozen state:
|
|
220
|
-
def
|
|
258
|
+
def as_frozen():
|
|
221
259
|
"""
|
|
222
260
|
Detects if the Python script is running as a 'frozen' (standalone)
|
|
223
261
|
executable created by a tool like PyInstaller, cx_Freeze, or Nuitka.
|
|
@@ -235,21 +273,26 @@ def is_frozen():
|
|
|
235
273
|
"""
|
|
236
274
|
return getattr(sys, 'frozen', False)
|
|
237
275
|
|
|
238
|
-
|
|
276
|
+
# --- Binary Characteristic Checks ---
|
|
277
|
+
def is_elf(exec_path: Path | str | None = None, debug: bool = False) -> bool:
|
|
239
278
|
"""Checks if the currently running executable (sys.argv[0]) is a standalone PyInstaller-built ELF binary."""
|
|
240
279
|
# If it's a pipx installation, it is not the monolithic binary we are concerned with here.
|
|
241
280
|
|
|
242
281
|
if exec_path is None:
|
|
243
282
|
exec_path = Path(sys.argv[0]).resolve()
|
|
283
|
+
else:
|
|
284
|
+
exec_path = Path(exec_path).resolve()
|
|
285
|
+
|
|
244
286
|
if debug:
|
|
245
|
-
print(f"
|
|
287
|
+
print(f"DEBUG: Checking executable path: {exec_path}")
|
|
288
|
+
|
|
246
289
|
if is_pipx():
|
|
247
290
|
return False
|
|
248
291
|
|
|
249
292
|
# Check if the file exists and is readable
|
|
250
293
|
if not exec_path.is_file():
|
|
251
|
-
|
|
252
|
-
|
|
294
|
+
if debug: print("DEBUG:False (Not a file)")
|
|
295
|
+
return False
|
|
253
296
|
try:
|
|
254
297
|
# Check the magic number: The first four bytes of an ELF file are 0x7f, 'E', 'L', 'F' (b'\x7fELF').
|
|
255
298
|
# This is the most reliable way to determine if the executable is a native binary wrapper (like PyInstaller's).
|
|
@@ -261,13 +304,20 @@ def is_elf(exec_path : Path = None, debug=False) -> bool:
|
|
|
261
304
|
# Handle exceptions like PermissionError, IsADirectoryError, etc.
|
|
262
305
|
return False
|
|
263
306
|
|
|
264
|
-
def is_pyz(exec_path: Path=None, debug=False) -> bool:
|
|
307
|
+
def is_pyz(exec_path: Path | str | None = None, debug: bool = False) -> bool:
|
|
265
308
|
"""Checks if the currently running executable (sys.argv[0]) is a PYZ zipapp ."""
|
|
266
309
|
# If it's a pipx installation, it is not the monolithic binary we are concerned with here.
|
|
267
310
|
if exec_path is None:
|
|
268
311
|
exec_path = Path(sys.argv[0]).resolve()
|
|
312
|
+
else:
|
|
313
|
+
exec_path = Path(exec_path).resolve()
|
|
314
|
+
|
|
269
315
|
if debug:
|
|
270
|
-
print(f"
|
|
316
|
+
print(f"DEBUG: Checking executable path: {exec_path}")
|
|
317
|
+
|
|
318
|
+
if not exec_path.is_file():
|
|
319
|
+
if debug: print("DEBUG:False (Not a file)")
|
|
320
|
+
return False
|
|
271
321
|
|
|
272
322
|
if is_pipx():
|
|
273
323
|
return False
|
|
@@ -276,10 +326,10 @@ def is_pyz(exec_path: Path=None, debug=False) -> bool:
|
|
|
276
326
|
if not str(exec_path).endswith(".pyz"):
|
|
277
327
|
return False
|
|
278
328
|
|
|
279
|
-
if not _check_if_zip():
|
|
329
|
+
if not _check_if_zip(exec_path):
|
|
280
330
|
return False
|
|
281
331
|
|
|
282
|
-
def is_windows_portable_executable(exec_path: Path = None, debug=False) -> bool:
|
|
332
|
+
def is_windows_portable_executable(exec_path: Path | str | None = None, debug: bool = False) -> bool:
|
|
283
333
|
"""
|
|
284
334
|
Checks if the currently running executable (sys.argv[0]) is a
|
|
285
335
|
Windows Portable Executable (PE) binary, and explicitly excludes
|
|
@@ -290,18 +340,20 @@ def is_windows_portable_executable(exec_path: Path = None, debug=False) -> bool:
|
|
|
290
340
|
# 1. Determine execution path
|
|
291
341
|
if exec_path is None:
|
|
292
342
|
exec_path = Path(sys.argv[0]).resolve()
|
|
343
|
+
else:
|
|
344
|
+
exec_path = Path(exec_path).resolve()
|
|
293
345
|
|
|
294
346
|
if debug:
|
|
295
347
|
print(f"DEBUG: Checking executable path: {exec_path}")
|
|
296
348
|
|
|
297
349
|
# 2. Exclude pipx environments immediately
|
|
298
350
|
if is_pipx():
|
|
299
|
-
if debug: print("DEBUG:
|
|
351
|
+
if debug: print("DEBUG: False (is_pipx is True)")
|
|
300
352
|
return False
|
|
301
353
|
|
|
302
354
|
# 3. Perform file checks
|
|
303
355
|
if not exec_path.is_file():
|
|
304
|
-
if debug: print("DEBUG:
|
|
356
|
+
if debug: print("DEBUG:False (Not a file)")
|
|
305
357
|
return False
|
|
306
358
|
|
|
307
359
|
try:
|
|
@@ -314,30 +366,35 @@ def is_windows_portable_executable(exec_path: Path = None, debug=False) -> bool:
|
|
|
314
366
|
|
|
315
367
|
if debug:
|
|
316
368
|
print(f"DEBUG: Magic bytes: {magic_bytes}")
|
|
317
|
-
print(f"DEBUG:
|
|
369
|
+
print(f"DEBUG: {is_pe} (Non-pipx check)")
|
|
318
370
|
|
|
319
371
|
return is_pe
|
|
320
372
|
|
|
321
373
|
except Exception as e:
|
|
322
|
-
if debug: print(f"DEBUG:
|
|
374
|
+
if debug: print(f"DEBUG: Error during file check: {e}")
|
|
323
375
|
# Handle exceptions like PermissionError, IsADirectoryError, etc.
|
|
324
376
|
return False
|
|
325
377
|
|
|
326
|
-
def is_macos_executable(exec_path: Path = None, debug=False) -> bool:
|
|
378
|
+
def is_macos_executable(exec_path: Path | str | None = None, debug: bool = False) -> bool:
|
|
327
379
|
"""
|
|
328
380
|
Checks if the currently running executable is a macOS/Darwin Mach-O binary,
|
|
329
381
|
and explicitly excludes pipx-managed environments.
|
|
330
382
|
"""
|
|
331
383
|
if exec_path is None:
|
|
332
384
|
exec_path = Path(sys.argv[0]).resolve()
|
|
333
|
-
|
|
385
|
+
else:
|
|
386
|
+
exec_path = Path(exec_path).resolve()
|
|
387
|
+
if debug:
|
|
388
|
+
print(f"DEBUG: Checking executable path: {exec_path}")
|
|
389
|
+
|
|
334
390
|
if is_pipx():
|
|
335
391
|
if debug: print("DEBUG: is_macos_executable: False (is_pipx is True)")
|
|
336
392
|
return False
|
|
337
393
|
|
|
338
394
|
if not exec_path.is_file():
|
|
395
|
+
if debug: print("DEBUG:False (Not a file)")
|
|
339
396
|
return False
|
|
340
|
-
|
|
397
|
+
|
|
341
398
|
try:
|
|
342
399
|
# Check the magic number: Mach-O binaries start with specific 4-byte headers.
|
|
343
400
|
# Common ones are: b'\xfe\xed\xfa\xce' (32-bit) or b'\xfe\xed\xfa\xcf' (64-bit)
|
|
@@ -362,15 +419,23 @@ def is_macos_executable(exec_path: Path = None, debug=False) -> bool:
|
|
|
362
419
|
except Exception:
|
|
363
420
|
return False
|
|
364
421
|
|
|
365
|
-
def is_pipx(debug=False) -> bool:
|
|
422
|
+
def is_pipx(exec_path: Path | str | None = None, debug: bool = False) -> bool:
|
|
366
423
|
"""Checks if the executable is running from a pipx managed environment."""
|
|
424
|
+
if exec_path is None:
|
|
425
|
+
exec_path = Path(sys.argv[0]).resolve()
|
|
426
|
+
else:
|
|
427
|
+
exec_path = Path(exec_path).resolve()
|
|
428
|
+
if debug:
|
|
429
|
+
print(f"DEBUG: Checking executable path: {exec_path}")
|
|
430
|
+
|
|
431
|
+
if not exec_path.is_file():
|
|
432
|
+
if debug: print("DEBUG:False (Not a file)")
|
|
433
|
+
return False
|
|
367
434
|
try:
|
|
368
435
|
# Helper for case-insensitivity on Windows
|
|
369
436
|
def normalize_path(p: Path) -> str:
|
|
370
437
|
return str(p).lower()
|
|
371
438
|
|
|
372
|
-
exec_path = Path(sys.argv[0]).resolve()
|
|
373
|
-
|
|
374
439
|
# This is the path to the interpreter running the script (e.g., venv/bin/python)
|
|
375
440
|
# In a pipx-managed execution, this is the venv python.
|
|
376
441
|
interpreter_path = Path(sys.executable).resolve()
|
|
@@ -413,10 +478,54 @@ def is_pipx(debug=False) -> bool:
|
|
|
413
478
|
except Exception:
|
|
414
479
|
# Fallback for unexpected path errors
|
|
415
480
|
return False
|
|
416
|
-
|
|
417
481
|
|
|
482
|
+
def is_python_script(path: Path | str | None = None, debug: bool = False) -> bool:
|
|
483
|
+
"""
|
|
484
|
+
Checks if the specified path or running script is a Python source file (.py).
|
|
485
|
+
|
|
486
|
+
By default, checks the running script (`sys.argv[0]`). If a specific `path` is
|
|
487
|
+
provided, checks that path instead. Uses `Path.resolve()` for stable path handling.
|
|
488
|
+
|
|
489
|
+
Args:
|
|
490
|
+
path: Optional; path to the file to check (str or Path). If None, defaults to `sys.argv[0]`.
|
|
491
|
+
debug: If True, prints the path being checked.
|
|
492
|
+
|
|
493
|
+
Returns:
|
|
494
|
+
bool: True if the specified or default path is a Python source file (.py); False otherwise.
|
|
495
|
+
"""
|
|
496
|
+
if path is None:
|
|
497
|
+
exec_path = Path(sys.argv[0]).resolve()
|
|
498
|
+
else:
|
|
499
|
+
exec_path = Path(path).resolve()
|
|
500
|
+
if debug:
|
|
501
|
+
print(f"Checking Python script for path: {exec_path}")
|
|
502
|
+
if not exec_path.is_file():
|
|
503
|
+
return False
|
|
504
|
+
return exec_path.suffix.lower() == '.py'
|
|
505
|
+
|
|
506
|
+
# --- Interpreter Check ---
|
|
507
|
+
|
|
508
|
+
def interp_path(print_path: bool = False) -> str:
|
|
509
|
+
"""
|
|
510
|
+
Returns the path to the Python interpreter binary and optionally prints it.
|
|
511
|
+
|
|
512
|
+
This function wraps `sys.executable` to provide the path to the interpreter
|
|
513
|
+
(e.g., '/data/data/com.termux/files/usr/bin/python3' in Termux or the embedded
|
|
514
|
+
interpreter in a frozen executable). If the path is empty (e.g., in some embedded
|
|
515
|
+
or sandboxed environments), an empty string is returned.
|
|
516
|
+
|
|
517
|
+
Args:
|
|
518
|
+
print_path: If True, prints the interpreter path to stdout.
|
|
519
|
+
|
|
520
|
+
Returns:
|
|
521
|
+
str: The path to the Python interpreter binary, or an empty string if unavailable.
|
|
522
|
+
"""
|
|
523
|
+
path = sys.executable
|
|
524
|
+
if print_path:
|
|
525
|
+
print(f"Python interpreter path: {path}")
|
|
526
|
+
return path
|
|
418
527
|
|
|
419
|
-
# --- TTY
|
|
528
|
+
# --- TTY Check ---
|
|
420
529
|
def interactive_terminal_is_available():
|
|
421
530
|
"""
|
|
422
531
|
Check if the script is running in an interactive terminal.
|
|
@@ -440,7 +549,7 @@ def web_browser_is_available() -> bool:
|
|
|
440
549
|
except webbrowser.Error:
|
|
441
550
|
# Fallback needed. Check for external launchers.
|
|
442
551
|
# 2. Termux specific check
|
|
443
|
-
if
|
|
552
|
+
if on_termux() and shutil.which("termux-open-url"):
|
|
444
553
|
return True
|
|
445
554
|
# 3. General Linux check
|
|
446
555
|
if shutil.which("xdg-open"):
|
|
@@ -448,34 +557,42 @@ def web_browser_is_available() -> bool:
|
|
|
448
557
|
return False
|
|
449
558
|
|
|
450
559
|
# --- LAUNCH MECHANISMS BASED ON ENVIRONMENT ---
|
|
451
|
-
def edit_textfile(
|
|
452
|
-
#def open_text_file_for_editing(
|
|
560
|
+
def edit_textfile(path: Path | str | None = None) -> None:
|
|
561
|
+
#def open_text_file_for_editing(path): # defunct function name as of 1.0.16
|
|
453
562
|
"""
|
|
454
563
|
Opens a file with the environment's default application (Windows, Linux, macOS)
|
|
455
564
|
or a guaranteed console editor (nano) in constrained environments (Termux, iSH)
|
|
456
565
|
after ensuring line-ending compatibility.
|
|
566
|
+
|
|
567
|
+
This function is known to fail on PyDroid3, where on_linus() is True but xdg-open
|
|
568
|
+
is not available.
|
|
457
569
|
"""
|
|
570
|
+
if path is None:
|
|
571
|
+
return
|
|
572
|
+
|
|
573
|
+
path = Path(path).resolve()
|
|
574
|
+
|
|
458
575
|
try:
|
|
459
|
-
if
|
|
460
|
-
os.startfile(
|
|
461
|
-
elif
|
|
576
|
+
if on_windows():
|
|
577
|
+
os.startfile(path)
|
|
578
|
+
elif on_termux():
|
|
462
579
|
# Install dependencies if missing (Termux pkg returns non-zero if already installed, so no check=True)
|
|
463
580
|
subprocess.run(['pkg','install', 'dos2unix', 'nano'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
464
|
-
_run_dos2unix(
|
|
465
|
-
subprocess.run(['nano',
|
|
466
|
-
elif
|
|
581
|
+
_run_dos2unix(path)
|
|
582
|
+
subprocess.run(['nano', path])
|
|
583
|
+
elif on_ish_alpine():
|
|
467
584
|
# Install dependencies if missing (apk returns 0 if already installed, so check=True is safe)
|
|
468
585
|
subprocess.run(['apk','add', 'dos2unix'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=True)
|
|
469
586
|
subprocess.run(['apk','add', 'nano'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=True)
|
|
470
|
-
_run_dos2unix(
|
|
471
|
-
subprocess.run(['nano',
|
|
587
|
+
_run_dos2unix(path)
|
|
588
|
+
subprocess.run(['nano', path])
|
|
472
589
|
# --- 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',
|
|
590
|
+
elif on_linux():
|
|
591
|
+
_run_dos2unix(path) # Safety conversion for user-defined console apps
|
|
592
|
+
subprocess.run(['xdg-open', path])
|
|
593
|
+
elif on_apple():
|
|
594
|
+
_run_dos2unix(path) # Safety conversion for user-defined console apps
|
|
595
|
+
subprocess.run(['open', path])
|
|
479
596
|
else:
|
|
480
597
|
print("Unsupported operating system.")
|
|
481
598
|
except Exception as e:
|
|
@@ -485,13 +602,16 @@ def edit_textfile(filepath) -> None:
|
|
|
485
602
|
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
603
|
"""
|
|
487
604
|
|
|
488
|
-
def _run_dos2unix(
|
|
605
|
+
def _run_dos2unix(path: Path | str | None = None):
|
|
489
606
|
"""Attempt to run dos2unix, failing silently if not installed."""
|
|
607
|
+
|
|
608
|
+
path = Path(path).resolve()
|
|
609
|
+
|
|
490
610
|
try:
|
|
491
611
|
# We rely on shutil.which not being needed, as this is a robust built-in utility on most targets
|
|
492
612
|
# The command won't raise an exception unless the process itself fails, not just if the utility isn't found.
|
|
493
613
|
# 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',
|
|
614
|
+
subprocess.run(['dos2unix', path], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
495
615
|
except FileNotFoundError:
|
|
496
616
|
# This will be raised if 'dos2unix' is not on the system PATH
|
|
497
617
|
pass
|
|
@@ -526,10 +646,14 @@ def _get_pipx_paths():
|
|
|
526
646
|
return pipx_bin_path, pipx_venv_base.resolve()
|
|
527
647
|
|
|
528
648
|
|
|
529
|
-
def _check_if_zip(
|
|
649
|
+
def _check_if_zip(path: Path | str | None) -> bool:
|
|
530
650
|
"""Checks if the file at the given path is a valid ZIP archive."""
|
|
651
|
+
if path is None:
|
|
652
|
+
return False
|
|
653
|
+
path = Path(path).resolve()
|
|
654
|
+
|
|
531
655
|
try:
|
|
532
|
-
return zipfile.is_zipfile(
|
|
656
|
+
return zipfile.is_zipfile(path)
|
|
533
657
|
except Exception:
|
|
534
658
|
# Handle cases where the path might be invalid, or other unexpected errors
|
|
535
659
|
return False
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pyhabitat
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.17
|
|
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
|
|
@@ -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,8 @@
|
|
|
1
|
+
pyhabitat/__init__.py,sha256=-WMvkW27x91heqsUfCCKHJntusBG2AIXCr6J29Pw9W8,1246
|
|
2
|
+
pyhabitat/__main__.py,sha256=GT_PXbj6X2nlZIbRiFBKXDiv158_YrsoLhf7FsLFV-Q,2868
|
|
3
|
+
pyhabitat/environment.py,sha256=d9nUoWeu86oWNGYBNACOWti71mGiTCADpBLIAW87sxw,24806
|
|
4
|
+
pyhabitat-1.0.17.dist-info/licenses/LICENSE,sha256=D4fg30ctUGnCJlWu3ONv5-V8JE1v3ctakoJTcVjsJlg,1072
|
|
5
|
+
pyhabitat-1.0.17.dist-info/METADATA,sha256=-uJXF7he0eiYXH63gvd9jXyHXef-V9vL7Yq0E2bZda0,10732
|
|
6
|
+
pyhabitat-1.0.17.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
7
|
+
pyhabitat-1.0.17.dist-info/top_level.txt,sha256=zXYK44Qu8EqxUETREvd2diMUaB5JiGRErkwFaoLQnnI,10
|
|
8
|
+
pyhabitat-1.0.17.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
|