pyhabitat 1.0.17__tar.gz → 1.0.19__tar.gz
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-1.0.17 → pyhabitat-1.0.19}/PKG-INFO +6 -4
- {pyhabitat-1.0.17 → pyhabitat-1.0.19}/README.md +4 -2
- {pyhabitat-1.0.17 → pyhabitat-1.0.19}/pyhabitat/__init__.py +6 -3
- pyhabitat-1.0.19/pyhabitat/__main__.py +4 -0
- pyhabitat-1.0.17/pyhabitat/__main__.py → pyhabitat-1.0.19/pyhabitat/__main__stable.py +3 -0
- pyhabitat-1.0.19/pyhabitat/cli.py +22 -0
- {pyhabitat-1.0.17 → pyhabitat-1.0.19}/pyhabitat/environment.py +309 -148
- {pyhabitat-1.0.17 → pyhabitat-1.0.19}/pyhabitat.egg-info/PKG-INFO +6 -4
- {pyhabitat-1.0.17 → pyhabitat-1.0.19}/pyhabitat.egg-info/SOURCES.txt +3 -0
- pyhabitat-1.0.19/pyhabitat.egg-info/entry_points.txt +2 -0
- {pyhabitat-1.0.17 → pyhabitat-1.0.19}/pyproject.toml +8 -4
- {pyhabitat-1.0.17 → pyhabitat-1.0.19}/LICENSE +0 -0
- {pyhabitat-1.0.17 → pyhabitat-1.0.19}/pyhabitat.egg-info/dependency_links.txt +0 -0
- {pyhabitat-1.0.17 → pyhabitat-1.0.19}/pyhabitat.egg-info/top_level.txt +0 -0
- {pyhabitat-1.0.17 → pyhabitat-1.0.19}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pyhabitat
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.19
|
|
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
|
|
@@ -78,10 +78,12 @@ Key question: "What is this running on?"
|
|
|
78
78
|
| `on_windows()` | Returns `True` on Windows. |
|
|
79
79
|
| `on_apple()` | Returns `True` on macOS (Darwin). |
|
|
80
80
|
| `on_linux()` | Returns `True` on Linux in general. |
|
|
81
|
+
| `on_wsl()` | Returns `True` if running inside Windows Subsystem for Linux (WSL or WSL2). |
|
|
81
82
|
| `on_termux()` | Returns `True` if running in the Termux Android environment. |
|
|
82
83
|
| `on_freebsd()` | Returns `True` on FreeBSD. |
|
|
83
84
|
| `on_ish_alpine()` | Returns `True` if running in the iSH Alpine Linux iOS emulator. |
|
|
84
85
|
| `on_android()` | Returns `True` on any Android-based Linux environment. |
|
|
86
|
+
| `on_pydroid()` | Returns `True` Return True if running under the Pydroid 3 Android app (other versions untested). |
|
|
85
87
|
| `in_repl()` | Returns `True` is the user is currently in a Python REPL; hasattr(sys,'ps1'). |
|
|
86
88
|
|
|
87
89
|
### Packaging and Build Checking
|
|
@@ -118,7 +120,7 @@ Key Question: "What could I do next?"
|
|
|
118
120
|
| Function | Description |
|
|
119
121
|
| :--- | :--- |
|
|
120
122
|
| `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(
|
|
123
|
+
| `interp_path()` | Returns the path to the Python interpreter binary (sys.executable). Returns empty string if unavailable. |
|
|
122
124
|
| `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. |
|
|
123
125
|
|
|
124
126
|
</details>
|
|
@@ -130,7 +132,7 @@ Key Question: "What could I do next?"
|
|
|
130
132
|
|
|
131
133
|
The module exposes all detection functions directly for easy access.
|
|
132
134
|
|
|
133
|
-
### 0\.Example of PyHabitat in Action
|
|
135
|
+
### 0\. Example of PyHabitat in Action
|
|
134
136
|
|
|
135
137
|
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.
|
|
136
138
|
|
|
@@ -63,10 +63,12 @@ Key question: "What is this running on?"
|
|
|
63
63
|
| `on_windows()` | Returns `True` on Windows. |
|
|
64
64
|
| `on_apple()` | Returns `True` on macOS (Darwin). |
|
|
65
65
|
| `on_linux()` | Returns `True` on Linux in general. |
|
|
66
|
+
| `on_wsl()` | Returns `True` if running inside Windows Subsystem for Linux (WSL or WSL2). |
|
|
66
67
|
| `on_termux()` | Returns `True` if running in the Termux Android environment. |
|
|
67
68
|
| `on_freebsd()` | Returns `True` on FreeBSD. |
|
|
68
69
|
| `on_ish_alpine()` | Returns `True` if running in the iSH Alpine Linux iOS emulator. |
|
|
69
70
|
| `on_android()` | Returns `True` on any Android-based Linux environment. |
|
|
71
|
+
| `on_pydroid()` | Returns `True` Return True if running under the Pydroid 3 Android app (other versions untested). |
|
|
70
72
|
| `in_repl()` | Returns `True` is the user is currently in a Python REPL; hasattr(sys,'ps1'). |
|
|
71
73
|
|
|
72
74
|
### Packaging and Build Checking
|
|
@@ -103,7 +105,7 @@ Key Question: "What could I do next?"
|
|
|
103
105
|
| Function | Description |
|
|
104
106
|
| :--- | :--- |
|
|
105
107
|
| `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. |
|
|
106
|
-
| `interp_path(
|
|
108
|
+
| `interp_path()` | Returns the path to the Python interpreter binary (sys.executable). Returns empty string if unavailable. |
|
|
107
109
|
| `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. |
|
|
108
110
|
|
|
109
111
|
</details>
|
|
@@ -115,7 +117,7 @@ Key Question: "What could I do next?"
|
|
|
115
117
|
|
|
116
118
|
The module exposes all detection functions directly for easy access.
|
|
117
119
|
|
|
118
|
-
### 0\.Example of PyHabitat in Action
|
|
120
|
+
### 0\. Example of PyHabitat in Action
|
|
119
121
|
|
|
120
122
|
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.
|
|
121
123
|
|
|
@@ -5,12 +5,14 @@ from .environment import (
|
|
|
5
5
|
matplotlib_is_available_for_headless_image_export,
|
|
6
6
|
tkinter_is_available,
|
|
7
7
|
in_repl,
|
|
8
|
-
on_termux,
|
|
9
8
|
on_freebsd,
|
|
10
9
|
on_linux,
|
|
11
10
|
on_android,
|
|
12
11
|
on_windows,
|
|
12
|
+
on_wsl,
|
|
13
13
|
on_apple,
|
|
14
|
+
on_termux,
|
|
15
|
+
on_pydroid,
|
|
14
16
|
on_ish_alpine,
|
|
15
17
|
as_pyinstaller,
|
|
16
18
|
as_frozen,
|
|
@@ -24,10 +26,9 @@ from .environment import (
|
|
|
24
26
|
web_browser_is_available,
|
|
25
27
|
edit_textfile,
|
|
26
28
|
interp_path,
|
|
29
|
+
main,
|
|
27
30
|
)
|
|
28
31
|
|
|
29
|
-
from .__main__ import main
|
|
30
|
-
|
|
31
32
|
# Optional: Set __all__ for explicit documentation and cleaner imports
|
|
32
33
|
__all__ = [
|
|
33
34
|
'matplotlib_is_available_for_gui_plotting',
|
|
@@ -35,6 +36,8 @@ __all__ = [
|
|
|
35
36
|
'tkinter_is_available',
|
|
36
37
|
'in_repl',
|
|
37
38
|
'on_termux',
|
|
39
|
+
'on_pydroid',
|
|
40
|
+
'on_wsl',
|
|
38
41
|
'on_freebsd',
|
|
39
42
|
'on_linux',
|
|
40
43
|
'on_android',
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from pyhabitat.environment 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)
|
|
@@ -13,6 +13,7 @@ from pathlib import Path
|
|
|
13
13
|
import subprocess
|
|
14
14
|
import io
|
|
15
15
|
import zipfile
|
|
16
|
+
import logging
|
|
16
17
|
|
|
17
18
|
__all__ = [
|
|
18
19
|
'matplotlib_is_available_for_gui_plotting',
|
|
@@ -21,8 +22,10 @@ __all__ = [
|
|
|
21
22
|
'on_termux',
|
|
22
23
|
'on_freebsd',
|
|
23
24
|
'on_linux',
|
|
25
|
+
'on_pydroid',
|
|
24
26
|
'on_android',
|
|
25
27
|
'on_windows',
|
|
28
|
+
'on_wsl',
|
|
26
29
|
'on_apple',
|
|
27
30
|
'on_ish_alpine',
|
|
28
31
|
'as_pyinstaller',
|
|
@@ -37,6 +40,7 @@ __all__ = [
|
|
|
37
40
|
'edit_textfile',
|
|
38
41
|
'in_repl',
|
|
39
42
|
'interp_path',
|
|
43
|
+
'main',
|
|
40
44
|
]
|
|
41
45
|
|
|
42
46
|
# Global cache for tkinter and matplotlib (mpl) availability
|
|
@@ -44,6 +48,7 @@ _TKINTER_AVAILABILITY: bool | None = None
|
|
|
44
48
|
_MATPLOTLIB_EXPORT_AVAILABILITY: bool | None = None
|
|
45
49
|
_MATPLOTLIB_WINDOWED_AVAILABILITY: bool | None = None
|
|
46
50
|
|
|
51
|
+
|
|
47
52
|
# --- GUI CHECKS ---
|
|
48
53
|
def matplotlib_is_available_for_gui_plotting(termux_has_gui=False):
|
|
49
54
|
"""Check if Matplotlib is available AND can use a GUI backend for a popup window."""
|
|
@@ -206,6 +211,64 @@ def on_android() -> bool:
|
|
|
206
211
|
return False
|
|
207
212
|
return "android" in platform.platform().lower()
|
|
208
213
|
|
|
214
|
+
|
|
215
|
+
def on_wsl():
|
|
216
|
+
"""Return True if running inside Windows Subsystem for Linux (WSL or WSL2)."""
|
|
217
|
+
# Must look like Linux, not Windows
|
|
218
|
+
if platform.system() != "Linux":
|
|
219
|
+
return False
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
# --- Check environment variables for WSL2 ---
|
|
223
|
+
# False negative risk:
|
|
224
|
+
# Environment variables may be absent in older WSL1 installs.
|
|
225
|
+
# False negative likelihood: low.
|
|
226
|
+
if "WSL_DISTRO_NAME" in os.environ or "WSL_INTEROP" in os.environ:
|
|
227
|
+
return True
|
|
228
|
+
|
|
229
|
+
# --- Check kernel info for 'microsoft' string ---
|
|
230
|
+
# False negative risk:
|
|
231
|
+
# Custom kernels, future Windows versions, or minimal WSL distros may omit 'microsoft' in strings.
|
|
232
|
+
# False negative likelihood: Very low to moderate.
|
|
233
|
+
|
|
234
|
+
try:
|
|
235
|
+
with open("/proc/version") as f:
|
|
236
|
+
if "microsoft" in version_info or "wsl" in version_info:
|
|
237
|
+
return True
|
|
238
|
+
except FileNotFoundError:
|
|
239
|
+
pass
|
|
240
|
+
|
|
241
|
+
# Check for WSL-specific mounts (fallback)
|
|
242
|
+
"""
|
|
243
|
+
/proc/sys/kernel/osrelease
|
|
244
|
+
Purpose: Contains the kernel release string. In WSL, it usually contains "microsoft" (WSL2) or "microsoft-standard" (WSL1).
|
|
245
|
+
Very reliable for detecting WSL1 and WSL2 unless someone compiled a custom kernel and removed the microsoft string.
|
|
246
|
+
|
|
247
|
+
False negative risk:
|
|
248
|
+
If /proc/version or /proc/sys/kernel/osrelease cannot be read due to permissions, a containerized WSL distro, or some sandboxed environment.
|
|
249
|
+
# False negative likelihood: Very low.
|
|
250
|
+
"""
|
|
251
|
+
if Path("/proc/sys/kernel/osrelease").exists():
|
|
252
|
+
try:
|
|
253
|
+
with open("/proc/sys/kernel/osrelease") as f:
|
|
254
|
+
osrelease = f.read().lower()
|
|
255
|
+
if "microsoft" in osrelease:
|
|
256
|
+
return True
|
|
257
|
+
except Exception:
|
|
258
|
+
pass
|
|
259
|
+
return False
|
|
260
|
+
|
|
261
|
+
def on_pydroid():
|
|
262
|
+
"""Return True if running under Pydroid 3 (Android app)."""
|
|
263
|
+
if not on_android():
|
|
264
|
+
return False
|
|
265
|
+
|
|
266
|
+
exe = (sys.executable or "").lower()
|
|
267
|
+
if "pydroid" in exe or "ru.iiec.pydroid3" in exe:
|
|
268
|
+
return True
|
|
269
|
+
|
|
270
|
+
return any("pydroid" in p.lower() for p in sys.path)
|
|
271
|
+
|
|
209
272
|
def on_windows() -> bool:
|
|
210
273
|
"""Detect if running on Windows."""
|
|
211
274
|
return platform.system() == 'Windows'
|
|
@@ -274,132 +337,74 @@ def as_frozen():
|
|
|
274
337
|
return getattr(sys, 'frozen', False)
|
|
275
338
|
|
|
276
339
|
# --- Binary Characteristic Checks ---
|
|
277
|
-
def is_elf(exec_path: Path | str | None = None, debug: bool = False) -> bool:
|
|
340
|
+
def is_elf(exec_path: Path | str | None = None, debug: bool = False, suppress_debug: bool =False) -> bool:
|
|
278
341
|
"""Checks if the currently running executable (sys.argv[0]) is a standalone PyInstaller-built ELF binary."""
|
|
279
342
|
# If it's a pipx installation, it is not the monolithic binary we are concerned with here.
|
|
280
|
-
|
|
281
|
-
if
|
|
282
|
-
exec_path = Path(sys.argv[0]).resolve()
|
|
283
|
-
else:
|
|
284
|
-
exec_path = Path(exec_path).resolve()
|
|
285
|
-
|
|
286
|
-
if debug:
|
|
287
|
-
print(f"DEBUG: Checking executable path: {exec_path}")
|
|
288
|
-
|
|
289
|
-
if is_pipx():
|
|
343
|
+
exec_path, is_valid = _check_executable_path(exec_path, debug and not suppress_debug)
|
|
344
|
+
if not is_valid:
|
|
290
345
|
return False
|
|
291
346
|
|
|
292
|
-
# Check if the file exists and is readable
|
|
293
|
-
if not exec_path.is_file():
|
|
294
|
-
if debug: print("DEBUG:False (Not a file)")
|
|
295
|
-
return False
|
|
296
347
|
try:
|
|
297
348
|
# Check the magic number: The first four bytes of an ELF file are 0x7f, 'E', 'L', 'F' (b'\x7fELF').
|
|
298
349
|
# This is the most reliable way to determine if the executable is a native binary wrapper (like PyInstaller's).
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
350
|
+
magic_bytes = read_magic_bytes(exec_path, 4, debug and not suppress_debug)
|
|
351
|
+
|
|
302
352
|
return magic_bytes == b'\x7fELF'
|
|
303
353
|
except Exception:
|
|
304
|
-
|
|
354
|
+
if debug:
|
|
355
|
+
logging.debug("False (Exception during file check)")
|
|
305
356
|
return False
|
|
306
357
|
|
|
307
|
-
def is_pyz(exec_path: Path | str | None = None, debug: bool = False) -> bool:
|
|
358
|
+
def is_pyz(exec_path: Path | str | None = None, debug: bool = False, suppress_debug: bool =False) -> bool:
|
|
308
359
|
"""Checks if the currently running executable (sys.argv[0]) is a PYZ zipapp ."""
|
|
309
|
-
# If it's a pipx installation, it is not the monolithic binary we are concerned with here.
|
|
310
|
-
if exec_path is None:
|
|
311
|
-
exec_path = Path(sys.argv[0]).resolve()
|
|
312
|
-
else:
|
|
313
|
-
exec_path = Path(exec_path).resolve()
|
|
314
360
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
if not exec_path.is_file():
|
|
319
|
-
if debug: print("DEBUG:False (Not a file)")
|
|
320
|
-
return False
|
|
321
|
-
|
|
322
|
-
if is_pipx():
|
|
361
|
+
# If it's a pipx installation, it is not the monolithic binary we are concerned with here.
|
|
362
|
+
exec_path, is_valid = _check_executable_path(exec_path, debug and not suppress_debug)
|
|
363
|
+
if not is_valid:
|
|
323
364
|
return False
|
|
324
365
|
|
|
325
366
|
# Check if the extension is PYZ
|
|
326
367
|
if not str(exec_path).endswith(".pyz"):
|
|
368
|
+
if debug:
|
|
369
|
+
logging.debug("False (Not a .pyz file)")
|
|
327
370
|
return False
|
|
328
|
-
|
|
371
|
+
|
|
329
372
|
if not _check_if_zip(exec_path):
|
|
373
|
+
if debug:
|
|
374
|
+
logging.debug("False (Not a valid ZIP file)")
|
|
330
375
|
return False
|
|
331
376
|
|
|
332
|
-
|
|
377
|
+
return True
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def is_windows_portable_executable(exec_path: Path | str | None = None, debug: bool = False, suppress_debug: bool =False) -> bool:
|
|
333
381
|
"""
|
|
334
|
-
Checks if the
|
|
335
|
-
Windows Portable Executable (PE) binary, and explicitly excludes
|
|
336
|
-
pipx-managed environments.
|
|
382
|
+
Checks if the specified path or sys.argv[0] is a Windows Portable Executable (PE) binary.
|
|
337
383
|
Windows Portable Executables include .exe, .dll, and other binaries.
|
|
338
384
|
The standard way to check for a PE is to look for the MZ magic number at the very beginning of the file.
|
|
339
385
|
"""
|
|
340
|
-
|
|
341
|
-
if
|
|
342
|
-
exec_path = Path(sys.argv[0]).resolve()
|
|
343
|
-
else:
|
|
344
|
-
exec_path = Path(exec_path).resolve()
|
|
345
|
-
|
|
346
|
-
if debug:
|
|
347
|
-
print(f"DEBUG: Checking executable path: {exec_path}")
|
|
348
|
-
|
|
349
|
-
# 2. Exclude pipx environments immediately
|
|
350
|
-
if is_pipx():
|
|
351
|
-
if debug: print("DEBUG: False (is_pipx is True)")
|
|
386
|
+
exec_path, is_valid = _check_executable_path(exec_path, debug and not suppress_debug)
|
|
387
|
+
if not is_valid:
|
|
352
388
|
return False
|
|
389
|
+
magic_bytes = read_magic_bytes(exec_path, 2, debug and not suppress_debug)
|
|
390
|
+
result = magic_bytes.startswith(b"MZ")
|
|
353
391
|
|
|
354
|
-
|
|
355
|
-
if not exec_path.is_file():
|
|
356
|
-
if debug: print("DEBUG:False (Not a file)")
|
|
357
|
-
return False
|
|
358
|
-
|
|
359
|
-
try:
|
|
360
|
-
# Check the magic number: All Windows PE files (EXE, DLL, etc.)
|
|
361
|
-
# start with the two-byte header b'MZ' (for Mark Zbikowski).
|
|
362
|
-
with open(exec_path, 'rb') as f:
|
|
363
|
-
magic_bytes = f.read(2)
|
|
364
|
-
|
|
365
|
-
is_pe = magic_bytes == b'MZ'
|
|
366
|
-
|
|
367
|
-
if debug:
|
|
368
|
-
print(f"DEBUG: Magic bytes: {magic_bytes}")
|
|
369
|
-
print(f"DEBUG: {is_pe} (Non-pipx check)")
|
|
370
|
-
|
|
371
|
-
return is_pe
|
|
372
|
-
|
|
373
|
-
except Exception as e:
|
|
374
|
-
if debug: print(f"DEBUG: Error during file check: {e}")
|
|
375
|
-
# Handle exceptions like PermissionError, IsADirectoryError, etc.
|
|
376
|
-
return False
|
|
392
|
+
return result
|
|
377
393
|
|
|
378
|
-
def is_macos_executable(exec_path: Path | str | None = None, debug: bool = False) -> bool:
|
|
394
|
+
def is_macos_executable(exec_path: Path | str | None = None, debug: bool = False, suppress_debug: bool =False) -> bool:
|
|
379
395
|
"""
|
|
380
396
|
Checks if the currently running executable is a macOS/Darwin Mach-O binary,
|
|
381
397
|
and explicitly excludes pipx-managed environments.
|
|
382
398
|
"""
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
else:
|
|
386
|
-
exec_path = Path(exec_path).resolve()
|
|
387
|
-
if debug:
|
|
388
|
-
print(f"DEBUG: Checking executable path: {exec_path}")
|
|
389
|
-
|
|
390
|
-
if is_pipx():
|
|
391
|
-
if debug: print("DEBUG: is_macos_executable: False (is_pipx is True)")
|
|
392
|
-
return False
|
|
393
|
-
|
|
394
|
-
if not exec_path.is_file():
|
|
395
|
-
if debug: print("DEBUG:False (Not a file)")
|
|
399
|
+
exec_path, is_valid = _check_executable_path(exec_path, debug and not suppress_debug)
|
|
400
|
+
if not is_valid:
|
|
396
401
|
return False
|
|
397
402
|
|
|
398
403
|
try:
|
|
399
404
|
# Check the magic number: Mach-O binaries start with specific 4-byte headers.
|
|
400
405
|
# Common ones are: b'\xfe\xed\xfa\xce' (32-bit) or b'\xfe\xed\xfa\xcf' (64-bit)
|
|
401
|
-
|
|
402
|
-
|
|
406
|
+
|
|
407
|
+
magic_bytes = read_magic_bytes(exec_path, 4, debug and not suppress_debug)
|
|
403
408
|
|
|
404
409
|
# Common Mach-O magic numbers (including their reversed-byte counterparts)
|
|
405
410
|
MACHO_MAGIC = {
|
|
@@ -411,75 +416,88 @@ def is_macos_executable(exec_path: Path | str | None = None, debug: bool = False
|
|
|
411
416
|
|
|
412
417
|
is_macho = magic_bytes in MACHO_MAGIC
|
|
413
418
|
|
|
414
|
-
if debug:
|
|
415
|
-
print(f"DEBUG: is_macos_executable: {is_macho} (Non-pipx check)")
|
|
416
419
|
|
|
417
420
|
return is_macho
|
|
418
421
|
|
|
419
422
|
except Exception:
|
|
423
|
+
if debug:
|
|
424
|
+
logging.debug("False (Exception during file check)")
|
|
420
425
|
return False
|
|
421
426
|
|
|
422
|
-
def is_pipx(exec_path: Path | str | None = None, debug: bool = False) -> bool:
|
|
427
|
+
def is_pipx(exec_path: Path | str | None = None, debug: bool = False, suppress_debug: bool = True) -> bool:
|
|
423
428
|
"""Checks if the executable is running from a pipx managed environment."""
|
|
424
|
-
|
|
425
|
-
|
|
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)")
|
|
429
|
+
exec_path, is_valid = _check_executable_path(exec_path, debug and not suppress_debug, check_pipx=False)
|
|
430
|
+
if not is_valid:
|
|
433
431
|
return False
|
|
432
|
+
|
|
434
433
|
try:
|
|
435
|
-
# Helper for case-insensitivity on Windows
|
|
436
|
-
def normalize_path(p: Path) -> str:
|
|
437
|
-
return str(p).lower()
|
|
438
|
-
|
|
439
|
-
# This is the path to the interpreter running the script (e.g., venv/bin/python)
|
|
440
|
-
# In a pipx-managed execution, this is the venv python.
|
|
441
434
|
interpreter_path = Path(sys.executable).resolve()
|
|
442
435
|
pipx_bin_path, pipx_venv_base_path = _get_pipx_paths()
|
|
443
436
|
# Normalize paths for comparison
|
|
444
|
-
norm_exec_path =
|
|
445
|
-
norm_interp_path =
|
|
437
|
+
norm_exec_path = str(exec_path).lower()
|
|
438
|
+
norm_interp_path = str(interpreter_path).lower()
|
|
446
439
|
|
|
447
440
|
if debug:
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
# This is a strong check for both the executable path (your discovery)
|
|
458
|
-
# and the interpreter path (canonical venv location).
|
|
441
|
+
logging.debug(f"EXEC_PATH: {exec_path}")
|
|
442
|
+
logging.debug(f"INTERP_PATH: {interpreter_path}")
|
|
443
|
+
logging.debug(f"PIPX_BIN_PATH: {pipx_bin_path}")
|
|
444
|
+
logging.debug(f"PIPX_VENV_BASE: {pipx_venv_base_path}")
|
|
445
|
+
is_in_pipx_venv_base = norm_interp_path.startswith(str(pipx_venv_base_path).lower())
|
|
446
|
+
logging.debug(f"Interpreter path resides somewhere within the pipx venv base hierarchy: {is_in_pipx_venv_base}")
|
|
447
|
+
logging.debug(
|
|
448
|
+
f"This determines whether the current interpreter is managed by pipx: {is_in_pipx_venv_base}"
|
|
449
|
+
)
|
|
459
450
|
if "pipx/venvs" in norm_exec_path or "pipx/venvs" in norm_interp_path:
|
|
460
|
-
if debug:
|
|
451
|
+
if debug:
|
|
452
|
+
logging.debug("True (Signature Check)")
|
|
461
453
|
return True
|
|
462
454
|
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
if debug: print("is_pipx: True (Interpreter Base Check)")
|
|
455
|
+
if norm_interp_path.startswith(str(pipx_venv_base_path).lower()):
|
|
456
|
+
if debug:
|
|
457
|
+
logging.debug("True (Interpreter Base Check)")
|
|
467
458
|
return True
|
|
468
|
-
|
|
469
|
-
# 3. Targeted Executable Check: The executable's resolved path starts with the PIPX venv base.
|
|
470
|
-
# This is your key Termux discovery, confirming the shim resolves into the venv.
|
|
471
|
-
if norm_exec_path.startswith(normalize_path(pipx_venv_base_path)):
|
|
472
|
-
if debug: print("is_pipx: True (Executable Base Check)")
|
|
473
|
-
return True
|
|
474
459
|
|
|
475
|
-
if
|
|
476
|
-
|
|
460
|
+
if norm_exec_path.startswith(str(pipx_venv_base_path).lower()):
|
|
461
|
+
if debug:
|
|
462
|
+
logging.debug("True (Executable Base Check)")
|
|
463
|
+
return True
|
|
477
464
|
|
|
465
|
+
return False
|
|
478
466
|
except Exception:
|
|
479
|
-
|
|
467
|
+
if debug:
|
|
468
|
+
logging.debug("False (Exception during pipx check)")
|
|
480
469
|
return False
|
|
470
|
+
if debug:
|
|
471
|
+
logging.debug(f"EXEC_PATH: {exec_path}")
|
|
472
|
+
logging.debug(f"INTERP_PATH: {interpreter_path}")
|
|
473
|
+
logging.debug(f"PIPX_BIN_PATH: {pipx_bin_path}")
|
|
474
|
+
logging.debug(f"PIPX_VENV_BASE: {pipx_venv_base_path}")
|
|
475
|
+
logging.debug(f"Check B result: {norm_interp_path.startswith(str(pipx_venv_base_path).lower())}")
|
|
481
476
|
|
|
482
|
-
|
|
477
|
+
if "pipx/venvs" in norm_exec_path or "pipx/venvs" in norm_interp_path:
|
|
478
|
+
if debug:
|
|
479
|
+
logging.debug("True (Signature Check)")
|
|
480
|
+
return True
|
|
481
|
+
|
|
482
|
+
if norm_interp_path.startswith(str(pipx_venv_base_path).lower()):
|
|
483
|
+
if debug:
|
|
484
|
+
logging.debug("True (Interpreter Base Check)")
|
|
485
|
+
return True
|
|
486
|
+
|
|
487
|
+
if norm_exec_path.startswith(str(pipx_venv_base_path).lower()):
|
|
488
|
+
if debug:
|
|
489
|
+
logging.debug("True (Executable Base Check)")
|
|
490
|
+
return True
|
|
491
|
+
|
|
492
|
+
if debug:
|
|
493
|
+
logging.debug("False")
|
|
494
|
+
return False
|
|
495
|
+
except Exception:
|
|
496
|
+
if debug:
|
|
497
|
+
logging.debug("False (Exception during pipx check)")
|
|
498
|
+
return False
|
|
499
|
+
|
|
500
|
+
def is_python_script(path: Path | str | None = None, debug: bool = False, suppress_debug: bool =False) -> bool:
|
|
483
501
|
"""
|
|
484
502
|
Checks if the specified path or running script is a Python source file (.py).
|
|
485
503
|
|
|
@@ -493,19 +511,14 @@ def is_python_script(path: Path | str | None = None, debug: bool = False) -> boo
|
|
|
493
511
|
Returns:
|
|
494
512
|
bool: True if the specified or default path is a Python source file (.py); False otherwise.
|
|
495
513
|
"""
|
|
496
|
-
|
|
497
|
-
|
|
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():
|
|
514
|
+
exec_path, is_valid = _check_executable_path(path, debug and not suppress_debug, check_pipx=False)
|
|
515
|
+
if not is_valid:
|
|
503
516
|
return False
|
|
504
517
|
return exec_path.suffix.lower() == '.py'
|
|
505
518
|
|
|
506
519
|
# --- Interpreter Check ---
|
|
507
520
|
|
|
508
|
-
def interp_path(
|
|
521
|
+
def interp_path(debug: bool = False) -> str:
|
|
509
522
|
"""
|
|
510
523
|
Returns the path to the Python interpreter binary and optionally prints it.
|
|
511
524
|
|
|
@@ -521,8 +534,8 @@ def interp_path(print_path: bool = False) -> str:
|
|
|
521
534
|
str: The path to the Python interpreter binary, or an empty string if unavailable.
|
|
522
535
|
"""
|
|
523
536
|
path = sys.executable
|
|
524
|
-
if
|
|
525
|
-
|
|
537
|
+
if debug:
|
|
538
|
+
logging.debug(f"Python interpreter path: {path}")
|
|
526
539
|
return path
|
|
527
540
|
|
|
528
541
|
# --- TTY Check ---
|
|
@@ -601,7 +614,8 @@ def edit_textfile(path: Path | str | None = None) -> None:
|
|
|
601
614
|
"""Why Not Use check=True on Termux:
|
|
602
615
|
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.
|
|
603
616
|
"""
|
|
604
|
-
|
|
617
|
+
|
|
618
|
+
# --- Helper Functions ---
|
|
605
619
|
def _run_dos2unix(path: Path | str | None = None):
|
|
606
620
|
"""Attempt to run dos2unix, failing silently if not installed."""
|
|
607
621
|
|
|
@@ -618,7 +632,20 @@ def _run_dos2unix(path: Path | str | None = None):
|
|
|
618
632
|
except Exception:
|
|
619
633
|
# Catch other subprocess errors (e.g. permission issues)
|
|
620
634
|
pass
|
|
621
|
-
|
|
635
|
+
|
|
636
|
+
def read_magic_bytes(path: str, length: int = 4, debug: bool = False) -> bytes:
|
|
637
|
+
"""Return the first few bytes of a file for type detection."""
|
|
638
|
+
try:
|
|
639
|
+
with open(path, "rb") as f:
|
|
640
|
+
magic = f.read(length)
|
|
641
|
+
if debug:
|
|
642
|
+
logging.debug(f"Magic bytes: {magic!r}")
|
|
643
|
+
return magic
|
|
644
|
+
except Exception as e:
|
|
645
|
+
if debug:
|
|
646
|
+
logging.debug(f"False (Error during file check: {e})")
|
|
647
|
+
return False
|
|
648
|
+
|
|
622
649
|
def _get_pipx_paths():
|
|
623
650
|
"""
|
|
624
651
|
Returns the configured/default pipx binary and home directories.
|
|
@@ -657,4 +684,138 @@ def _check_if_zip(path: Path | str | None) -> bool:
|
|
|
657
684
|
except Exception:
|
|
658
685
|
# Handle cases where the path might be invalid, or other unexpected errors
|
|
659
686
|
return False
|
|
660
|
-
|
|
687
|
+
|
|
688
|
+
def _check_executable_path(exec_path: Path | str | None, debug: bool = False, check_pipx: bool = True) -> tuple[Path | None, bool]:
|
|
689
|
+
"""Helper function to resolve executable path and perform common checks."""
|
|
690
|
+
if exec_path is None:
|
|
691
|
+
exec_path = Path(sys.argv[0]).resolve() if sys.argv[0] and sys.argv[0] != '-c' else None
|
|
692
|
+
else:
|
|
693
|
+
exec_path = Path(exec_path).resolve()
|
|
694
|
+
|
|
695
|
+
if debug:
|
|
696
|
+
logging.debug(f"Checking executable path: {exec_path}")
|
|
697
|
+
|
|
698
|
+
if exec_path is None:
|
|
699
|
+
if debug:
|
|
700
|
+
logging.debug("False (No valid path)")
|
|
701
|
+
return None, False
|
|
702
|
+
|
|
703
|
+
if check_pipx and is_pipx(exec_path, debug):
|
|
704
|
+
if debug:
|
|
705
|
+
logging.debug("False (is_pipx is True)")
|
|
706
|
+
return exec_path, False
|
|
707
|
+
|
|
708
|
+
if not exec_path.is_file():
|
|
709
|
+
if debug:
|
|
710
|
+
logging.debug("False (Not a file)")
|
|
711
|
+
return exec_path, False
|
|
712
|
+
|
|
713
|
+
return exec_path, True
|
|
714
|
+
|
|
715
|
+
# --- Main Function for report and CLI compatibility ---
|
|
716
|
+
|
|
717
|
+
def main(path=None, debug=False):
|
|
718
|
+
"""Print a comprehensive environment report.
|
|
719
|
+
|
|
720
|
+
Args:
|
|
721
|
+
path (Path | str | None): Path to inspect (defaults to sys.argv[0]).
|
|
722
|
+
debug (bool): Enable verbose debug output.
|
|
723
|
+
"""
|
|
724
|
+
if debug:
|
|
725
|
+
logging.basicConfig(level=logging.DEBUG)
|
|
726
|
+
logging.getLogger('matplotlib').setLevel(logging.WARNING) # Suppress matplotlib debug logs
|
|
727
|
+
print("================================")
|
|
728
|
+
print("======= PyHabitat Report =======")
|
|
729
|
+
print("================================")
|
|
730
|
+
print("\nCurrent Build Checks ")
|
|
731
|
+
print("# // Based on hasattr(sys,..) and getattr(sys,..)")
|
|
732
|
+
print("------------------------------")
|
|
733
|
+
print(f"in_repl(): {in_repl()}")
|
|
734
|
+
print(f"as_frozen(): {as_frozen()}")
|
|
735
|
+
print(f"as_pyinstaller(): {as_pyinstaller()}")
|
|
736
|
+
print("\nOperating System Checks")
|
|
737
|
+
print("# // Based on platform.system()")
|
|
738
|
+
print("------------------------------")
|
|
739
|
+
print(f"on_windows(): {on_windows()}")
|
|
740
|
+
print(f"on_apple(): {on_apple()}")
|
|
741
|
+
print(f"on_linux(): {on_linux()}")
|
|
742
|
+
print(f"on_wsl(): {on_wsl()}")
|
|
743
|
+
print(f"on_android(): {on_android()}")
|
|
744
|
+
print(f"on_termux(): {on_termux()}")
|
|
745
|
+
print(f"on_pydroid(): {on_pydroid()}")
|
|
746
|
+
print(f"on_ish_alpine(): {on_ish_alpine()}")
|
|
747
|
+
print(f"on_freebsd(): {on_freebsd()}")
|
|
748
|
+
print("\nCapability Checks")
|
|
749
|
+
print("-------------------------")
|
|
750
|
+
print(f"tkinter_is_available(): {tkinter_is_available()}")
|
|
751
|
+
print(f"matplotlib_is_available_for_gui_plotting(): {matplotlib_is_available_for_gui_plotting()}")
|
|
752
|
+
print(f"matplotlib_is_available_for_headless_image_export(): {matplotlib_is_available_for_headless_image_export()}")
|
|
753
|
+
print(f"web_browser_is_available(): {web_browser_is_available()}")
|
|
754
|
+
print(f"interactive_terminal_is_available(): {interactive_terminal_is_available()}")
|
|
755
|
+
print("\nInterpreter Checks")
|
|
756
|
+
print("# // Based on sys.executable()")
|
|
757
|
+
print("-----------------------------")
|
|
758
|
+
print(f"interp_path(): {interp_path()}")
|
|
759
|
+
if debug:
|
|
760
|
+
# Do these debug prints once to avoid redundant prints
|
|
761
|
+
# Supress redundant prints explicity using suppress_debug=True,
|
|
762
|
+
# so that only unique information gets printed for each check,
|
|
763
|
+
# even when more than one use the same functions which include debugging logs.
|
|
764
|
+
#print(f"_check_executable_path(interp_path(), debug=True)")
|
|
765
|
+
_check_executable_path(interp_path(), debug=debug)
|
|
766
|
+
#print(f"read_magic_bites(interp_path(), debug=True)")
|
|
767
|
+
read_magic_bytes(interp_path(), debug=debug)
|
|
768
|
+
print(f"is_elf(interp_path()): {is_elf(interp_path(), debug=debug, suppress_debug=True)}")
|
|
769
|
+
print(f"is_windows_portable_executable(interp_path()): {is_windows_portable_executable(interp_path(), debug=debug, suppress_debug=True)}")
|
|
770
|
+
print(f"is_macos_executable(interp_path()): {is_macos_executable(interp_path(), debug=debug, suppress_debug=True)}")
|
|
771
|
+
print(f"is_pyz(interp_path()): {is_pyz(interp_path(), debug=debug, suppress_debug=True)}")
|
|
772
|
+
print(f"is_pipx(interp_path()): {is_pipx(interp_path(), debug=debug, suppress_debug=True)}")
|
|
773
|
+
print(f"is_python_script(interp_path()): {is_python_script(interp_path(), debug=debug, suppress_debug=True)}")
|
|
774
|
+
print("\nCurrent Environment Check")
|
|
775
|
+
print("# // Based on sys.argv[0]")
|
|
776
|
+
print("-----------------------------")
|
|
777
|
+
inspect_path = path if path is not None else (None if sys.argv[0] == '-c' else sys.argv[0])
|
|
778
|
+
logging.debug(f"Inspecting path: {inspect_path}")
|
|
779
|
+
# Early validation of path
|
|
780
|
+
if path is not None:
|
|
781
|
+
path_obj = Path(path)
|
|
782
|
+
if not path_obj.is_file():
|
|
783
|
+
print(f"Error: '{path}' is not a valid file or does not exist.")
|
|
784
|
+
if debug:
|
|
785
|
+
logging.error(f"Invalid path: '{path}' is not a file or does not exist.")
|
|
786
|
+
raise SystemExit(1)
|
|
787
|
+
script_path = None
|
|
788
|
+
if path or (sys.argv[0] and sys.argv[0] != '-c'):
|
|
789
|
+
script_path = Path(path or sys.argv[0]).resolve()
|
|
790
|
+
print(f"sys.argv[0] = {str(sys.argv[0])}")
|
|
791
|
+
if script_path is not None:
|
|
792
|
+
print(f"script_path = {script_path}")
|
|
793
|
+
if debug:
|
|
794
|
+
# Do these debug prints once to avoid redundant prints
|
|
795
|
+
# Supress redundant prints explicity using suppress_debug=True,
|
|
796
|
+
# so that only unique information gets printed for each check,
|
|
797
|
+
# even when more than one use the same functions which include debugging logs.
|
|
798
|
+
p#rint(f"_check_executable_path(script_path, debug=True)")
|
|
799
|
+
_check_executable_path(script_path, debug=debug)
|
|
800
|
+
#print(f"read_magic_bites(script_path, debug=True)")
|
|
801
|
+
read_magic_bytes(script_path, debug=debug)
|
|
802
|
+
print(f"is_elf(): {is_elf(script_path, debug=debug, suppress_debug=True)}")
|
|
803
|
+
print(f"is_windows_portable_executable(): {is_windows_portable_executable(script_path, debug=debug, suppress_debug=True)}")
|
|
804
|
+
print(f"is_macos_executable(): {is_macos_executable(script_path, debug=debug, suppress_debug=True)}")
|
|
805
|
+
print(f"is_pyz(): {is_pyz(script_path, debug=debug, suppress_debug=True)}")
|
|
806
|
+
print(f"is_pipx(): {is_pipx(script_path, debug=debug, suppress_debug=True)}")
|
|
807
|
+
print(f"is_python_script(): {is_python_script(script_path, debug=debug, suppress_debug=True)}")
|
|
808
|
+
else:
|
|
809
|
+
print("Skipping: ")
|
|
810
|
+
print(" is_elf(), ")
|
|
811
|
+
print(" is_windows_portable_executable(), ")
|
|
812
|
+
print(" is_macos_executable(), ")
|
|
813
|
+
print(" is_pyz(), ")
|
|
814
|
+
print(" is_pipx(), ")
|
|
815
|
+
print(" is_python_script(), ")
|
|
816
|
+
print("All False, script_path is None.")
|
|
817
|
+
print("")
|
|
818
|
+
print("=================================")
|
|
819
|
+
print("=== PyHabitat Report Complete ===")
|
|
820
|
+
print("=================================")
|
|
821
|
+
print("")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pyhabitat
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.19
|
|
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
|
|
@@ -78,10 +78,12 @@ Key question: "What is this running on?"
|
|
|
78
78
|
| `on_windows()` | Returns `True` on Windows. |
|
|
79
79
|
| `on_apple()` | Returns `True` on macOS (Darwin). |
|
|
80
80
|
| `on_linux()` | Returns `True` on Linux in general. |
|
|
81
|
+
| `on_wsl()` | Returns `True` if running inside Windows Subsystem for Linux (WSL or WSL2). |
|
|
81
82
|
| `on_termux()` | Returns `True` if running in the Termux Android environment. |
|
|
82
83
|
| `on_freebsd()` | Returns `True` on FreeBSD. |
|
|
83
84
|
| `on_ish_alpine()` | Returns `True` if running in the iSH Alpine Linux iOS emulator. |
|
|
84
85
|
| `on_android()` | Returns `True` on any Android-based Linux environment. |
|
|
86
|
+
| `on_pydroid()` | Returns `True` Return True if running under the Pydroid 3 Android app (other versions untested). |
|
|
85
87
|
| `in_repl()` | Returns `True` is the user is currently in a Python REPL; hasattr(sys,'ps1'). |
|
|
86
88
|
|
|
87
89
|
### Packaging and Build Checking
|
|
@@ -118,7 +120,7 @@ Key Question: "What could I do next?"
|
|
|
118
120
|
| Function | Description |
|
|
119
121
|
| :--- | :--- |
|
|
120
122
|
| `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(
|
|
123
|
+
| `interp_path()` | Returns the path to the Python interpreter binary (sys.executable). Returns empty string if unavailable. |
|
|
122
124
|
| `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. |
|
|
123
125
|
|
|
124
126
|
</details>
|
|
@@ -130,7 +132,7 @@ Key Question: "What could I do next?"
|
|
|
130
132
|
|
|
131
133
|
The module exposes all detection functions directly for easy access.
|
|
132
134
|
|
|
133
|
-
### 0\.Example of PyHabitat in Action
|
|
135
|
+
### 0\. Example of PyHabitat in Action
|
|
134
136
|
|
|
135
137
|
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.
|
|
136
138
|
|
|
@@ -3,8 +3,11 @@ README.md
|
|
|
3
3
|
pyproject.toml
|
|
4
4
|
pyhabitat/__init__.py
|
|
5
5
|
pyhabitat/__main__.py
|
|
6
|
+
pyhabitat/__main__stable.py
|
|
7
|
+
pyhabitat/cli.py
|
|
6
8
|
pyhabitat/environment.py
|
|
7
9
|
pyhabitat.egg-info/PKG-INFO
|
|
8
10
|
pyhabitat.egg-info/SOURCES.txt
|
|
9
11
|
pyhabitat.egg-info/dependency_links.txt
|
|
12
|
+
pyhabitat.egg-info/entry_points.txt
|
|
10
13
|
pyhabitat.egg-info/top_level.txt
|
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
# pyproject.toml
|
|
2
2
|
|
|
3
3
|
[build-system]
|
|
4
|
-
requires = ["setuptools>=61.0.0"]
|
|
4
|
+
requires = ["setuptools>=61.0.0", "wheel"]
|
|
5
5
|
build-backend = "setuptools.build_meta"
|
|
6
6
|
|
|
7
7
|
[project]
|
|
8
8
|
name = "pyhabitat"
|
|
9
|
-
version = "1.0.
|
|
9
|
+
version = "1.0.19"
|
|
10
10
|
#dynamic = ["version"] #
|
|
11
11
|
authors = [
|
|
12
12
|
{ name="George Clayton Bennett", email="george.bennett@memphistn.gov" },
|
|
13
13
|
]
|
|
14
14
|
description = "A lightweight library for detecting system environment, GUI, and build properties."
|
|
15
15
|
readme = "README.md"
|
|
16
|
-
requires-python = ">=3.
|
|
16
|
+
requires-python = ">=3.7"
|
|
17
17
|
license = "MIT"
|
|
18
18
|
keywords = ["environment", "os-detection", "gui", "build-system"]
|
|
19
19
|
classifiers = [
|
|
@@ -21,10 +21,14 @@ classifiers = [
|
|
|
21
21
|
"Operating System :: OS Independent",
|
|
22
22
|
"Topic :: System :: Systems Administration",
|
|
23
23
|
]
|
|
24
|
+
dependencies = []
|
|
25
|
+
|
|
26
|
+
[project.scripts]
|
|
27
|
+
pyhabitat = "pyhabitat.cli:run_cli"
|
|
24
28
|
|
|
25
29
|
[tool.setuptools.packages.find]
|
|
26
30
|
where = ["."]
|
|
27
|
-
include = ["pyhabitat"]
|
|
31
|
+
include = ["pyhabitat*"]
|
|
28
32
|
|
|
29
33
|
[tool.setuptools_scm]
|
|
30
34
|
# This tells setuptools_scm to look at your git history/tags for the version
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|