pyhabitat 1.0.17__py3-none-any.whl → 1.0.19__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 CHANGED
@@ -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',
pyhabitat/__main__.py CHANGED
@@ -1,69 +1,4 @@
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()}")
1
+ from pyhabitat.cli import run_cli
67
2
 
68
3
  if __name__ == "__main__":
69
- main()
4
+ run_cli()
@@ -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 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)
pyhabitat/environment.py CHANGED
@@ -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 exec_path is None:
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
- with open(exec_path, 'rb') as f:
300
- magic_bytes = f.read(4)
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
- # Handle exceptions like PermissionError, IsADirectoryError, etc.
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
- if debug:
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
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
- def is_windows_portable_executable(exec_path: Path | str | None = None, debug: bool = False) -> bool:
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 currently running executable (sys.argv[0]) is a
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
- # 1. Determine execution path
341
- if exec_path is None:
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
- # 3. Perform file checks
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
- if exec_path is None:
384
- exec_path = Path(sys.argv[0]).resolve()
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
- with open(exec_path, 'rb') as f:
402
- magic_bytes = f.read(4)
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
- 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)")
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 = normalize_path(exec_path)
445
- norm_interp_path = normalize_path(interpreter_path)
437
+ norm_exec_path = str(exec_path).lower()
438
+ norm_interp_path = str(interpreter_path).lower()
446
439
 
447
440
  if debug:
448
- # --- DEBUGGING OUTPUT ---
449
- print(f"DEBUG: EXEC_PATH: {exec_path}")
450
- print(f"DEBUG: INTERP_PATH: {interpreter_path}")
451
- print(f"DEBUG: PIPX_BIN_PATH: {pipx_bin_path}")
452
- print(f"DEBUG: PIPX_VENV_BASE: {pipx_venv_base_path}")
453
- print(f"DEBUG: Check B result: {normalize_path(interpreter_path).startswith(normalize_path(pipx_venv_base_path))}")
454
- # ------------------------
455
-
456
- # 1. Signature Check (Most Robust): Look for the unique 'pipx/venvs' string.
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: print("is_pipx: True (Signature Check)")
451
+ if debug:
452
+ logging.debug("True (Signature Check)")
461
453
  return True
462
454
 
463
- # 2. Targeted Venv Check: The interpreter's path starts with the PIPX venv base.
464
- # This is a canonical check if the signature check is somehow missed.
465
- if norm_interp_path.startswith(normalize_path(pipx_venv_base_path)):
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 debug: print("is_pipx: False")
476
- return False
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
- # Fallback for unexpected path errors
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
- def is_python_script(path: Path | str | None = None, debug: bool = False) -> bool:
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
- 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():
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(print_path: bool = False) -> str:
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 print_path:
525
- print(f"Python interpreter path: {path}")
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.17
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.8
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(print_path=False)` | Returns the path to the Python interpreter binary (sys.executable). Optionally prints the path. Returns empty string if unavailable. |
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
 
@@ -0,0 +1,11 @@
1
+ pyhabitat/__init__.py,sha256=B_yS2yBG2kjHkte_xissSG8aJ15af-soBo3Cy7caiEc,1288
2
+ pyhabitat/__main__.py,sha256=qlOKShd_Xk0HGpYOySXQSQe3K60a9wwrstCAceJkKIs,76
3
+ pyhabitat/__main__stable.py,sha256=UACpHLrr_Rmf0L5dJCEae6kFzLn7dqCqIri68IBnb10,2910
4
+ pyhabitat/cli.py,sha256=ZlY6v_IT7Mw-ekR3WPT8NLsqMQ_RXI-cckVq9oLT4AU,683
5
+ pyhabitat/environment.py,sha256=2wyjdWNRSv8VxaXkjoQz285ATAlhddrY_dor45mMnQ4,32871
6
+ pyhabitat-1.0.19.dist-info/licenses/LICENSE,sha256=D4fg30ctUGnCJlWu3ONv5-V8JE1v3ctakoJTcVjsJlg,1072
7
+ pyhabitat-1.0.19.dist-info/METADATA,sha256=N8o0nM2KOq7X4B-JNUMcePtqjinqsdqrqZ11Ct8qsRY,10900
8
+ pyhabitat-1.0.19.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
9
+ pyhabitat-1.0.19.dist-info/entry_points.txt,sha256=409xZ-BrarQJJLtO-aActCGkL0FMhNVi9wsq3u7tRHM,52
10
+ pyhabitat-1.0.19.dist-info/top_level.txt,sha256=zXYK44Qu8EqxUETREvd2diMUaB5JiGRErkwFaoLQnnI,10
11
+ pyhabitat-1.0.19.dist-info/RECORD,,
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ pyhabitat = pyhabitat.cli:run_cli
@@ -1,8 +0,0 @@
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,,