pyhabitat 1.0.20__tar.gz → 1.0.22__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.20 → pyhabitat-1.0.22}/PKG-INFO +1 -1
- {pyhabitat-1.0.20 → pyhabitat-1.0.22}/pyhabitat/__init__.py +3 -1
- {pyhabitat-1.0.20 → pyhabitat-1.0.22}/pyhabitat/cli.py +11 -0
- {pyhabitat-1.0.20 → pyhabitat-1.0.22}/pyhabitat/environment.py +93 -79
- pyhabitat-1.0.22/pyhabitat/utils.py +10 -0
- {pyhabitat-1.0.20 → pyhabitat-1.0.22}/pyhabitat.egg-info/PKG-INFO +1 -1
- {pyhabitat-1.0.20 → pyhabitat-1.0.22}/pyhabitat.egg-info/SOURCES.txt +1 -0
- {pyhabitat-1.0.20 → pyhabitat-1.0.22}/pyproject.toml +1 -1
- {pyhabitat-1.0.20 → pyhabitat-1.0.22}/LICENSE +0 -0
- {pyhabitat-1.0.20 → pyhabitat-1.0.22}/README.md +0 -0
- {pyhabitat-1.0.20 → pyhabitat-1.0.22}/pyhabitat/__main__.py +0 -0
- {pyhabitat-1.0.20 → pyhabitat-1.0.22}/pyhabitat/__main__stable.py +0 -0
- {pyhabitat-1.0.20 → pyhabitat-1.0.22}/pyhabitat.egg-info/dependency_links.txt +0 -0
- {pyhabitat-1.0.20 → pyhabitat-1.0.22}/pyhabitat.egg-info/entry_points.txt +0 -0
- {pyhabitat-1.0.20 → pyhabitat-1.0.22}/pyhabitat.egg-info/top_level.txt +0 -0
- {pyhabitat-1.0.20 → pyhabitat-1.0.22}/setup.cfg +0 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# pyhabitat/__init__.py
|
|
2
|
-
|
|
2
|
+
from .utils import get_version
|
|
3
3
|
from .environment import (
|
|
4
4
|
matplotlib_is_available_for_gui_plotting,
|
|
5
5
|
matplotlib_is_available_for_headless_image_export,
|
|
@@ -58,3 +58,5 @@ __all__ = [
|
|
|
58
58
|
'interp_path',
|
|
59
59
|
'main',
|
|
60
60
|
]
|
|
61
|
+
|
|
62
|
+
__version__ = get_version()
|
|
@@ -1,18 +1,28 @@
|
|
|
1
1
|
import argparse
|
|
2
2
|
from pathlib import Path
|
|
3
3
|
from pyhabitat.environment import main
|
|
4
|
+
from pyhabitat.utils import get_version
|
|
4
5
|
|
|
5
6
|
def run_cli():
|
|
6
7
|
"""Parse CLI arguments and run the pyhabitat environment report."""
|
|
8
|
+
current_version = get_version()
|
|
7
9
|
parser = argparse.ArgumentParser(
|
|
8
10
|
description="PyHabitat: Python environment and build introspection"
|
|
9
11
|
)
|
|
12
|
+
# Add the version argument
|
|
13
|
+
parser.add_argument(
|
|
14
|
+
'-v', '--version',
|
|
15
|
+
action='version',
|
|
16
|
+
version=f'%(prog)s {current_version}'
|
|
17
|
+
)
|
|
18
|
+
# Add the path argument
|
|
10
19
|
parser.add_argument(
|
|
11
20
|
"--path",
|
|
12
21
|
type=str,
|
|
13
22
|
default=None,
|
|
14
23
|
help="Path to a script or binary to inspect (defaults to sys.argv[0])",
|
|
15
24
|
)
|
|
25
|
+
# Add the debug argument
|
|
16
26
|
parser.add_argument(
|
|
17
27
|
"--debug",
|
|
18
28
|
action="store_true",
|
|
@@ -20,3 +30,4 @@ def run_cli():
|
|
|
20
30
|
)
|
|
21
31
|
args = parser.parse_args()
|
|
22
32
|
main(path=Path(args.path) if args.path else None, debug=args.debug)
|
|
33
|
+
|
|
@@ -75,7 +75,8 @@ def matplotlib_is_available_for_gui_plotting(termux_has_gui=False):
|
|
|
75
75
|
# Force the common GUI backend. At this point, we know tkinter is *available*.
|
|
76
76
|
# # 'TkAgg' is often the most reliable cross-platform test.
|
|
77
77
|
# 'TkAgg' != 'Agg'. The Agg backend is for non-gui image export.
|
|
78
|
-
matplotlib.
|
|
78
|
+
if matplotlib.get_backend().lower() != 'tkagg':
|
|
79
|
+
matplotlib.use('TkAgg', force=True)
|
|
79
80
|
import matplotlib.pyplot as plt
|
|
80
81
|
# A simple test call to ensure the backend initializes
|
|
81
82
|
# This final test catches any edge cases where tkinter is present but
|
|
@@ -351,9 +352,10 @@ def is_elf(exec_path: Path | str | None = None, debug: bool = False, suppress_de
|
|
|
351
352
|
# Check the magic number: The first four bytes of an ELF file are 0x7f, 'E', 'L', 'F' (b'\x7fELF').
|
|
352
353
|
# This is the most reliable way to determine if the executable is a native binary wrapper (like PyInstaller's).
|
|
353
354
|
magic_bytes = read_magic_bytes(exec_path, 4, debug and not suppress_debug)
|
|
354
|
-
|
|
355
|
+
if magic_bytes is None:
|
|
356
|
+
return False
|
|
355
357
|
return magic_bytes == b'\x7fELF'
|
|
356
|
-
except
|
|
358
|
+
except (OSError, IOError) as e:
|
|
357
359
|
if debug:
|
|
358
360
|
logging.debug("False (Exception during file check)")
|
|
359
361
|
return False
|
|
@@ -369,7 +371,7 @@ def is_pyz(exec_path: Path | str | None = None, debug: bool = False, suppress_de
|
|
|
369
371
|
# Check if the extension is PYZ
|
|
370
372
|
if not str(exec_path).endswith(".pyz"):
|
|
371
373
|
if debug:
|
|
372
|
-
logging.debug("False (Not a .pyz file)")
|
|
374
|
+
logging.debug("is_pyz()=False (Not a .pyz file)")
|
|
373
375
|
return False
|
|
374
376
|
|
|
375
377
|
if not _check_if_zip(exec_path):
|
|
@@ -389,11 +391,17 @@ def is_windows_portable_executable(exec_path: Path | str | None = None, debug: b
|
|
|
389
391
|
exec_path, is_valid = _check_executable_path(exec_path, debug and not suppress_debug)
|
|
390
392
|
if not is_valid:
|
|
391
393
|
return False
|
|
392
|
-
|
|
393
|
-
|
|
394
|
+
try:
|
|
395
|
+
magic_bytes = read_magic_bytes(exec_path, 2, debug and not suppress_debug)
|
|
396
|
+
if magic_bytes is None:
|
|
397
|
+
return False
|
|
398
|
+
result = magic_bytes.startswith(b"MZ")
|
|
399
|
+
return result
|
|
400
|
+
except (OSError, IOError) as e:
|
|
401
|
+
if debug:
|
|
402
|
+
logging.debug(f"is_windows_portable_executable() = False (Exception: {e})")
|
|
403
|
+
return False
|
|
394
404
|
|
|
395
|
-
return result
|
|
396
|
-
|
|
397
405
|
def is_macos_executable(exec_path: Path | str | None = None, debug: bool = False, suppress_debug: bool =False) -> bool:
|
|
398
406
|
"""
|
|
399
407
|
Checks if the currently running executable is a macOS/Darwin Mach-O binary,
|
|
@@ -408,7 +416,8 @@ def is_macos_executable(exec_path: Path | str | None = None, debug: bool = False
|
|
|
408
416
|
# Common ones are: b'\xfe\xed\xfa\xce' (32-bit) or b'\xfe\xed\xfa\xcf' (64-bit)
|
|
409
417
|
|
|
410
418
|
magic_bytes = read_magic_bytes(exec_path, 4, debug and not suppress_debug)
|
|
411
|
-
|
|
419
|
+
if magic_bytes is None:
|
|
420
|
+
return False
|
|
412
421
|
# Common Mach-O magic numbers (including their reversed-byte counterparts)
|
|
413
422
|
MACHO_MAGIC = {
|
|
414
423
|
b'\xfe\xed\xfa\xce', # MH_MAGIC
|
|
@@ -422,83 +431,61 @@ def is_macos_executable(exec_path: Path | str | None = None, debug: bool = False
|
|
|
422
431
|
|
|
423
432
|
return is_macho
|
|
424
433
|
|
|
425
|
-
except
|
|
434
|
+
except (OSError, IOError) as e:
|
|
426
435
|
if debug:
|
|
427
|
-
logging.debug("False (Exception during file check)")
|
|
436
|
+
logging.debug("is_macos_executable() = False (Exception during file check)")
|
|
428
437
|
return False
|
|
429
|
-
|
|
438
|
+
|
|
439
|
+
|
|
430
440
|
def is_pipx(exec_path: Path | str | None = None, debug: bool = False, suppress_debug: bool = True) -> bool:
|
|
431
441
|
"""Checks if the executable is running from a pipx managed environment."""
|
|
432
442
|
exec_path, is_valid = _check_executable_path(exec_path, debug and not suppress_debug, check_pipx=False)
|
|
443
|
+
# check_pipx arg should be false when calling from inside of is_pipx() to avoid recursion error
|
|
444
|
+
# For safety, _check_executable_path() guards against this.
|
|
433
445
|
if not is_valid:
|
|
434
446
|
return False
|
|
435
447
|
|
|
436
448
|
try:
|
|
437
449
|
interpreter_path = Path(sys.executable).resolve()
|
|
438
450
|
pipx_bin_path, pipx_venv_base_path = _get_pipx_paths()
|
|
451
|
+
|
|
439
452
|
# Normalize paths for comparison
|
|
440
453
|
norm_exec_path = str(exec_path).lower()
|
|
441
454
|
norm_interp_path = str(interpreter_path).lower()
|
|
455
|
+
pipx_venv_base_str = str(pipx_venv_base_path).lower()
|
|
442
456
|
|
|
443
457
|
if debug:
|
|
444
458
|
logging.debug(f"EXEC_PATH: {exec_path}")
|
|
445
459
|
logging.debug(f"INTERP_PATH: {interpreter_path}")
|
|
446
460
|
logging.debug(f"PIPX_BIN_PATH: {pipx_bin_path}")
|
|
447
461
|
logging.debug(f"PIPX_VENV_BASE: {pipx_venv_base_path}")
|
|
448
|
-
is_in_pipx_venv_base = norm_interp_path.startswith(
|
|
462
|
+
is_in_pipx_venv_base = norm_interp_path.startswith(pipx_venv_base_str)
|
|
449
463
|
logging.debug(f"Interpreter path resides somewhere within the pipx venv base hierarchy: {is_in_pipx_venv_base}")
|
|
450
464
|
logging.debug(
|
|
451
465
|
f"This determines whether the current interpreter is managed by pipx: {is_in_pipx_venv_base}"
|
|
452
466
|
)
|
|
453
467
|
if "pipx/venvs" in norm_exec_path or "pipx/venvs" in norm_interp_path:
|
|
454
468
|
if debug:
|
|
455
|
-
logging.debug("True
|
|
469
|
+
logging.debug("is_pipx() is True // Signature Check")
|
|
456
470
|
return True
|
|
457
471
|
|
|
458
|
-
if norm_interp_path.startswith(
|
|
472
|
+
if norm_interp_path.startswith(pipx_venv_base_str):
|
|
459
473
|
if debug:
|
|
460
|
-
logging.debug("True
|
|
474
|
+
logging.debug("is_pipx() is True // Interpreter Base Check")
|
|
461
475
|
return True
|
|
462
476
|
|
|
463
|
-
if norm_exec_path.startswith(
|
|
477
|
+
if norm_exec_path.startswith(pipx_venv_base_str):
|
|
464
478
|
if debug:
|
|
465
|
-
logging.debug("True
|
|
479
|
+
logging.debug("is_pipx() is True // Executable Base Check")
|
|
466
480
|
return True
|
|
467
481
|
|
|
468
|
-
return False
|
|
469
|
-
except Exception:
|
|
470
482
|
if debug:
|
|
471
|
-
logging.debug("
|
|
483
|
+
logging.debug("is_pipx() is False")
|
|
472
484
|
return False
|
|
473
|
-
if debug:
|
|
474
|
-
logging.debug(f"EXEC_PATH: {exec_path}")
|
|
475
|
-
logging.debug(f"INTERP_PATH: {interpreter_path}")
|
|
476
|
-
logging.debug(f"PIPX_BIN_PATH: {pipx_bin_path}")
|
|
477
|
-
logging.debug(f"PIPX_VENV_BASE: {pipx_venv_base_path}")
|
|
478
|
-
logging.debug(f"Check B result: {norm_interp_path.startswith(str(pipx_venv_base_path).lower())}")
|
|
479
|
-
|
|
480
|
-
if "pipx/venvs" in norm_exec_path or "pipx/venvs" in norm_interp_path:
|
|
481
|
-
if debug:
|
|
482
|
-
logging.debug("True (Signature Check)")
|
|
483
|
-
return True
|
|
484
|
-
|
|
485
|
-
if norm_interp_path.startswith(str(pipx_venv_base_path).lower()):
|
|
486
|
-
if debug:
|
|
487
|
-
logging.debug("True (Interpreter Base Check)")
|
|
488
|
-
return True
|
|
489
|
-
|
|
490
|
-
if norm_exec_path.startswith(str(pipx_venv_base_path).lower()):
|
|
491
|
-
if debug:
|
|
492
|
-
logging.debug("True (Executable Base Check)")
|
|
493
|
-
return True
|
|
494
485
|
|
|
495
|
-
if debug:
|
|
496
|
-
logging.debug("False")
|
|
497
|
-
return False
|
|
498
486
|
except Exception:
|
|
499
487
|
if debug:
|
|
500
488
|
logging.debug("False (Exception during pipx check)")
|
|
501
|
-
return False
|
|
502
489
|
|
|
503
490
|
def is_python_script(path: Path | str | None = None, debug: bool = False, suppress_debug: bool =False) -> bool:
|
|
504
491
|
"""
|
|
@@ -577,8 +564,8 @@ def edit_textfile(path: Path | str | None = None) -> None:
|
|
|
577
564
|
#def open_text_file_for_editing(path): # defunct function name as of 1.0.16
|
|
578
565
|
"""
|
|
579
566
|
Opens a file with the environment's default application (Windows, Linux, macOS)
|
|
580
|
-
or a guaranteed console editor (nano) in constrained environments (Termux, iSH)
|
|
581
|
-
|
|
567
|
+
or a guaranteed console editor (nano) in constrained environments (Termux, iSH).
|
|
568
|
+
Ensures line-ending compatibility where possible.
|
|
582
569
|
|
|
583
570
|
This function is known to fail on PyDroid3, where on_linus() is True but xdg-open
|
|
584
571
|
is not available.
|
|
@@ -593,28 +580,28 @@ def edit_textfile(path: Path | str | None = None) -> None:
|
|
|
593
580
|
os.startfile(path)
|
|
594
581
|
elif on_termux():
|
|
595
582
|
# Install dependencies if missing (Termux pkg returns non-zero if already installed, so no check=True)
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
583
|
+
subprocess.run(['pkg','install', 'dos2unix', 'nano'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
584
|
+
_run_dos2unix(path)
|
|
585
|
+
subprocess.run(['nano', str(path)])
|
|
599
586
|
elif on_ish_alpine():
|
|
600
587
|
# Install dependencies if missing (apk returns 0 if already installed, so check=True is safe)
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
588
|
+
subprocess.run(['apk','add', 'dos2unix'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=True)
|
|
589
|
+
subprocess.run(['apk','add', 'nano'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=True)
|
|
590
|
+
_run_dos2unix(path)
|
|
591
|
+
subprocess.run(['nano', str(path)])
|
|
605
592
|
# --- Standard Unix-like Systems (Conversion + Default App) ---
|
|
606
593
|
elif on_linux():
|
|
607
|
-
|
|
608
|
-
|
|
594
|
+
_run_dos2unix(path) # Safety conversion for user-defined console apps
|
|
595
|
+
subprocess.run(['xdg-open', str(path)])
|
|
609
596
|
elif on_apple():
|
|
610
|
-
|
|
611
|
-
|
|
597
|
+
_run_dos2unix(path) # Safety conversion for user-defined console apps
|
|
598
|
+
subprocess.run(['open', str(path)])
|
|
612
599
|
else:
|
|
613
|
-
|
|
600
|
+
print("Unsupported operating system.")
|
|
614
601
|
except Exception as e:
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
602
|
+
print("The file could not be opened for editing in the current environment: {e}")
|
|
603
|
+
"""
|
|
604
|
+
Why Not Use check=True on Termux:
|
|
618
605
|
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.
|
|
619
606
|
"""
|
|
620
607
|
|
|
@@ -636,8 +623,10 @@ def _run_dos2unix(path: Path | str | None = None):
|
|
|
636
623
|
# Catch other subprocess errors (e.g. permission issues)
|
|
637
624
|
pass
|
|
638
625
|
|
|
639
|
-
def read_magic_bytes(path: str, length: int = 4, debug: bool = False) -> bytes:
|
|
640
|
-
"""Return the first few bytes of a file for type detection.
|
|
626
|
+
def read_magic_bytes(path: str, length: int = 4, debug: bool = False) -> bytes | None:
|
|
627
|
+
"""Return the first few bytes of a file for type detection.
|
|
628
|
+
Returns None if the file cannot be read or does not exist.
|
|
629
|
+
"""
|
|
641
630
|
try:
|
|
642
631
|
with open(path, "rb") as f:
|
|
643
632
|
magic = f.read(length)
|
|
@@ -647,7 +636,9 @@ def read_magic_bytes(path: str, length: int = 4, debug: bool = False) -> bytes:
|
|
|
647
636
|
except Exception as e:
|
|
648
637
|
if debug:
|
|
649
638
|
logging.debug(f"False (Error during file check: {e})")
|
|
650
|
-
return False
|
|
639
|
+
#return False # not typesafe
|
|
640
|
+
#return b'' # could be misunderstood as what was found
|
|
641
|
+
return None # no way to conflate that this was a legitimate error
|
|
651
642
|
|
|
652
643
|
def _get_pipx_paths():
|
|
653
644
|
"""
|
|
@@ -688,8 +679,19 @@ def _check_if_zip(path: Path | str | None) -> bool:
|
|
|
688
679
|
# Handle cases where the path might be invalid, or other unexpected errors
|
|
689
680
|
return False
|
|
690
681
|
|
|
691
|
-
def _check_executable_path(exec_path: Path | str | None,
|
|
692
|
-
|
|
682
|
+
def _check_executable_path(exec_path: Path | str | None,
|
|
683
|
+
debug: bool = False,
|
|
684
|
+
check_pipx: bool = True
|
|
685
|
+
) -> tuple[Path | None, bool]: #compensate with __future__, may cause type checker issues
|
|
686
|
+
"""
|
|
687
|
+
Helper function to resolve an executable path and perform common checks.
|
|
688
|
+
|
|
689
|
+
Returns:
|
|
690
|
+
tuple[Path | None, bool]: (Resolved path, is_valid)
|
|
691
|
+
- Path: The resolved Path object, or None if invalid
|
|
692
|
+
- bool: Whether the path should be considered valid for subsequent checks
|
|
693
|
+
"""
|
|
694
|
+
# 1. Determine path
|
|
693
695
|
if exec_path is None:
|
|
694
696
|
exec_path = Path(sys.argv[0]).resolve() if sys.argv[0] and sys.argv[0] != '-c' else None
|
|
695
697
|
else:
|
|
@@ -698,22 +700,31 @@ def _check_executable_path(exec_path: Path | str | None, debug: bool = False, ch
|
|
|
698
700
|
if debug:
|
|
699
701
|
logging.debug(f"Checking executable path: {exec_path}")
|
|
700
702
|
|
|
703
|
+
# 2. Handle missing path
|
|
701
704
|
if exec_path is None:
|
|
702
705
|
if debug:
|
|
703
|
-
logging.debug("
|
|
706
|
+
logging.debug("_check_executable_path() returns (None, False) // exec_path is None")
|
|
704
707
|
return None, False
|
|
705
|
-
|
|
706
|
-
|
|
708
|
+
|
|
709
|
+
# 3. Ensure path actually exists and is a file
|
|
710
|
+
if not exec_path.is_file():
|
|
707
711
|
if debug:
|
|
708
|
-
logging.debug("
|
|
712
|
+
logging.debug("_check_executable_path() returns (exec_path, False) // exec_path is not a file")
|
|
709
713
|
return exec_path, False
|
|
710
714
|
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
+
# 4. Avoid recursive pipx check loops
|
|
716
|
+
# This guard ensures we don’t recursively call _check_executable_path()
|
|
717
|
+
# via is_pipx() -> _check_executable_path() -> is_pipx() -> ...
|
|
718
|
+
if check_pipx:
|
|
719
|
+
caller = sys._getframe(1).f_code.co_name
|
|
720
|
+
if caller != "is_pipx":
|
|
721
|
+
if is_pipx(exec_path, debug):
|
|
722
|
+
if debug:
|
|
723
|
+
logging.debug("_check_executable_path() returns (exec_path, False) // is_pipx(exec_path) is True")
|
|
724
|
+
return exec_path, False
|
|
715
725
|
|
|
716
|
-
return exec_path, True
|
|
726
|
+
return exec_path, True
|
|
727
|
+
|
|
717
728
|
|
|
718
729
|
# --- Main Function for report and CLI compatibility ---
|
|
719
730
|
|
|
@@ -798,7 +809,7 @@ def main(path=None, debug=False):
|
|
|
798
809
|
# Supress redundant prints explicity using suppress_debug=True,
|
|
799
810
|
# so that only unique information gets printed for each check,
|
|
800
811
|
# even when more than one use the same functions which include debugging logs.
|
|
801
|
-
|
|
812
|
+
#print(f"_check_executable_path(script_path, debug=True)")
|
|
802
813
|
_check_executable_path(script_path, debug=debug)
|
|
803
814
|
#print(f"read_magic_bites(script_path, debug=True)")
|
|
804
815
|
read_magic_bytes(script_path, debug=debug)
|
|
@@ -821,4 +832,7 @@ def main(path=None, debug=False):
|
|
|
821
832
|
print("=================================")
|
|
822
833
|
print("=== PyHabitat Report Complete ===")
|
|
823
834
|
print("=================================")
|
|
824
|
-
print("")
|
|
835
|
+
print("")
|
|
836
|
+
|
|
837
|
+
if __name__ == "__main__":
|
|
838
|
+
main(debug=True)
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
from importlib.metadata import version, PackageNotFoundError
|
|
2
|
+
def get_version() -> str:
|
|
3
|
+
"""Retrieves the installed package version."""
|
|
4
|
+
try:
|
|
5
|
+
# The package name 'pyhabitat' must exactly match the name in your pyproject.toml
|
|
6
|
+
return version('pyhabitat')
|
|
7
|
+
except PackageNotFoundError:
|
|
8
|
+
# This occurs if the script is run directly from the source directory
|
|
9
|
+
# without being installed in editable mode, or if the package name is wrong.
|
|
10
|
+
return "Not Installed (Local Development or Incorrect Name)"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|