pyhabitat 1.0.20__py3-none-any.whl → 1.0.21__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 +3 -1
- pyhabitat/cli.py +11 -0
- pyhabitat/environment.py +73 -68
- pyhabitat/utils.py +10 -0
- {pyhabitat-1.0.20.dist-info → pyhabitat-1.0.21.dist-info}/METADATA +1 -1
- pyhabitat-1.0.21.dist-info/RECORD +12 -0
- pyhabitat-1.0.20.dist-info/RECORD +0 -11
- {pyhabitat-1.0.20.dist-info → pyhabitat-1.0.21.dist-info}/WHEEL +0 -0
- {pyhabitat-1.0.20.dist-info → pyhabitat-1.0.21.dist-info}/entry_points.txt +0 -0
- {pyhabitat-1.0.20.dist-info → pyhabitat-1.0.21.dist-info}/licenses/LICENSE +0 -0
- {pyhabitat-1.0.20.dist-info → pyhabitat-1.0.21.dist-info}/top_level.txt +0 -0
pyhabitat/__init__.py
CHANGED
|
@@ -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()
|
pyhabitat/cli.py
CHANGED
|
@@ -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
|
+
|
pyhabitat/environment.py
CHANGED
|
@@ -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
|
|
@@ -430,75 +431,52 @@ def is_macos_executable(exec_path: Path | str | None = None, debug: bool = False
|
|
|
430
431
|
def is_pipx(exec_path: Path | str | None = None, debug: bool = False, suppress_debug: bool = True) -> bool:
|
|
431
432
|
"""Checks if the executable is running from a pipx managed environment."""
|
|
432
433
|
exec_path, is_valid = _check_executable_path(exec_path, debug and not suppress_debug, check_pipx=False)
|
|
434
|
+
# check_pipx arg should be false when calling from inside of is_pipx() to avoid recursion error
|
|
435
|
+
# For safety, _check_executable_path() guards against this.
|
|
433
436
|
if not is_valid:
|
|
434
437
|
return False
|
|
435
438
|
|
|
436
439
|
try:
|
|
437
440
|
interpreter_path = Path(sys.executable).resolve()
|
|
438
441
|
pipx_bin_path, pipx_venv_base_path = _get_pipx_paths()
|
|
442
|
+
|
|
439
443
|
# Normalize paths for comparison
|
|
440
444
|
norm_exec_path = str(exec_path).lower()
|
|
441
445
|
norm_interp_path = str(interpreter_path).lower()
|
|
446
|
+
pipx_venv_base_str = str(pipx_venv_base_path).lower()
|
|
442
447
|
|
|
443
448
|
if debug:
|
|
444
449
|
logging.debug(f"EXEC_PATH: {exec_path}")
|
|
445
450
|
logging.debug(f"INTERP_PATH: {interpreter_path}")
|
|
446
451
|
logging.debug(f"PIPX_BIN_PATH: {pipx_bin_path}")
|
|
447
452
|
logging.debug(f"PIPX_VENV_BASE: {pipx_venv_base_path}")
|
|
448
|
-
is_in_pipx_venv_base = norm_interp_path.startswith(
|
|
453
|
+
is_in_pipx_venv_base = norm_interp_path.startswith(pipx_venv_base_str)
|
|
449
454
|
logging.debug(f"Interpreter path resides somewhere within the pipx venv base hierarchy: {is_in_pipx_venv_base}")
|
|
450
455
|
logging.debug(
|
|
451
456
|
f"This determines whether the current interpreter is managed by pipx: {is_in_pipx_venv_base}"
|
|
452
457
|
)
|
|
453
458
|
if "pipx/venvs" in norm_exec_path or "pipx/venvs" in norm_interp_path:
|
|
454
459
|
if debug:
|
|
455
|
-
logging.debug("True
|
|
460
|
+
logging.debug("is_pipx() is True // Signature Check")
|
|
456
461
|
return True
|
|
457
462
|
|
|
458
|
-
if norm_interp_path.startswith(
|
|
463
|
+
if norm_interp_path.startswith(pipx_venv_base_str):
|
|
459
464
|
if debug:
|
|
460
|
-
logging.debug("True
|
|
465
|
+
logging.debug("is_pipx() is True // Interpreter Base Check")
|
|
461
466
|
return True
|
|
462
467
|
|
|
463
|
-
if norm_exec_path.startswith(
|
|
468
|
+
if norm_exec_path.startswith(pipx_venv_base_str):
|
|
464
469
|
if debug:
|
|
465
|
-
logging.debug("True
|
|
470
|
+
logging.debug("is_pipx() is True // Executable Base Check")
|
|
466
471
|
return True
|
|
467
472
|
|
|
468
|
-
return False
|
|
469
|
-
except Exception:
|
|
470
473
|
if debug:
|
|
471
|
-
logging.debug("
|
|
474
|
+
logging.debug("is_pipx() is False")
|
|
472
475
|
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
476
|
|
|
495
|
-
if debug:
|
|
496
|
-
logging.debug("False")
|
|
497
|
-
return False
|
|
498
477
|
except Exception:
|
|
499
478
|
if debug:
|
|
500
479
|
logging.debug("False (Exception during pipx check)")
|
|
501
|
-
return False
|
|
502
480
|
|
|
503
481
|
def is_python_script(path: Path | str | None = None, debug: bool = False, suppress_debug: bool =False) -> bool:
|
|
504
482
|
"""
|
|
@@ -577,8 +555,8 @@ def edit_textfile(path: Path | str | None = None) -> None:
|
|
|
577
555
|
#def open_text_file_for_editing(path): # defunct function name as of 1.0.16
|
|
578
556
|
"""
|
|
579
557
|
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
|
-
|
|
558
|
+
or a guaranteed console editor (nano) in constrained environments (Termux, iSH).
|
|
559
|
+
Ensures line-ending compatibility where possible.
|
|
582
560
|
|
|
583
561
|
This function is known to fail on PyDroid3, where on_linus() is True but xdg-open
|
|
584
562
|
is not available.
|
|
@@ -593,28 +571,28 @@ def edit_textfile(path: Path | str | None = None) -> None:
|
|
|
593
571
|
os.startfile(path)
|
|
594
572
|
elif on_termux():
|
|
595
573
|
# Install dependencies if missing (Termux pkg returns non-zero if already installed, so no check=True)
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
574
|
+
subprocess.run(['pkg','install', 'dos2unix', 'nano'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
575
|
+
_run_dos2unix(path)
|
|
576
|
+
subprocess.run(['nano', str(path)])
|
|
599
577
|
elif on_ish_alpine():
|
|
600
578
|
# Install dependencies if missing (apk returns 0 if already installed, so check=True is safe)
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
579
|
+
subprocess.run(['apk','add', 'dos2unix'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=True)
|
|
580
|
+
subprocess.run(['apk','add', 'nano'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=True)
|
|
581
|
+
_run_dos2unix(path)
|
|
582
|
+
subprocess.run(['nano', str(path)])
|
|
605
583
|
# --- Standard Unix-like Systems (Conversion + Default App) ---
|
|
606
584
|
elif on_linux():
|
|
607
|
-
|
|
608
|
-
|
|
585
|
+
_run_dos2unix(path) # Safety conversion for user-defined console apps
|
|
586
|
+
subprocess.run(['xdg-open', str(path)])
|
|
609
587
|
elif on_apple():
|
|
610
|
-
|
|
611
|
-
|
|
588
|
+
_run_dos2unix(path) # Safety conversion for user-defined console apps
|
|
589
|
+
subprocess.run(['open', str(path)])
|
|
612
590
|
else:
|
|
613
|
-
|
|
591
|
+
print("Unsupported operating system.")
|
|
614
592
|
except Exception as e:
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
593
|
+
print("The file could not be opened for editing in the current environment: {e}")
|
|
594
|
+
"""
|
|
595
|
+
Why Not Use check=True on Termux:
|
|
618
596
|
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
597
|
"""
|
|
620
598
|
|
|
@@ -636,8 +614,10 @@ def _run_dos2unix(path: Path | str | None = None):
|
|
|
636
614
|
# Catch other subprocess errors (e.g. permission issues)
|
|
637
615
|
pass
|
|
638
616
|
|
|
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.
|
|
617
|
+
def read_magic_bytes(path: str, length: int = 4, debug: bool = False) -> bytes | None:
|
|
618
|
+
"""Return the first few bytes of a file for type detection.
|
|
619
|
+
Returns None if the file cannot be read or does not exist.
|
|
620
|
+
"""
|
|
641
621
|
try:
|
|
642
622
|
with open(path, "rb") as f:
|
|
643
623
|
magic = f.read(length)
|
|
@@ -647,7 +627,9 @@ def read_magic_bytes(path: str, length: int = 4, debug: bool = False) -> bytes:
|
|
|
647
627
|
except Exception as e:
|
|
648
628
|
if debug:
|
|
649
629
|
logging.debug(f"False (Error during file check: {e})")
|
|
650
|
-
return False
|
|
630
|
+
#return False # not typesafe
|
|
631
|
+
#return b'' # could be misunderstood as what was found
|
|
632
|
+
return None # no way to conflate that this was a legitimate error
|
|
651
633
|
|
|
652
634
|
def _get_pipx_paths():
|
|
653
635
|
"""
|
|
@@ -688,8 +670,19 @@ def _check_if_zip(path: Path | str | None) -> bool:
|
|
|
688
670
|
# Handle cases where the path might be invalid, or other unexpected errors
|
|
689
671
|
return False
|
|
690
672
|
|
|
691
|
-
def _check_executable_path(exec_path: Path | str | None,
|
|
692
|
-
|
|
673
|
+
def _check_executable_path(exec_path: Path | str | None,
|
|
674
|
+
debug: bool = False,
|
|
675
|
+
check_pipx: bool = True
|
|
676
|
+
) -> tuple[Path | None, bool]: #compensate with __future__, may cause type checker issues
|
|
677
|
+
"""
|
|
678
|
+
Helper function to resolve an executable path and perform common checks.
|
|
679
|
+
|
|
680
|
+
Returns:
|
|
681
|
+
tuple[Path | None, bool]: (Resolved path, is_valid)
|
|
682
|
+
- Path: The resolved Path object, or None if invalid
|
|
683
|
+
- bool: Whether the path should be considered valid for subsequent checks
|
|
684
|
+
"""
|
|
685
|
+
# 1. Determine path
|
|
693
686
|
if exec_path is None:
|
|
694
687
|
exec_path = Path(sys.argv[0]).resolve() if sys.argv[0] and sys.argv[0] != '-c' else None
|
|
695
688
|
else:
|
|
@@ -698,22 +691,31 @@ def _check_executable_path(exec_path: Path | str | None, debug: bool = False, ch
|
|
|
698
691
|
if debug:
|
|
699
692
|
logging.debug(f"Checking executable path: {exec_path}")
|
|
700
693
|
|
|
694
|
+
# 2. Handle missing path
|
|
701
695
|
if exec_path is None:
|
|
702
696
|
if debug:
|
|
703
|
-
logging.debug("
|
|
697
|
+
logging.debug("_check_executable_path() returns (None, False) // exec_path is None")
|
|
704
698
|
return None, False
|
|
705
|
-
|
|
706
|
-
|
|
699
|
+
|
|
700
|
+
# 3. Ensure path actually exists and is a file
|
|
701
|
+
if not exec_path.is_file():
|
|
707
702
|
if debug:
|
|
708
|
-
logging.debug("
|
|
703
|
+
logging.debug("_check_executable_path() returns (exec_path, False) // exec_path is not a file")
|
|
709
704
|
return exec_path, False
|
|
710
705
|
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
706
|
+
# 4. Avoid recursive pipx check loops
|
|
707
|
+
# This guard ensures we don’t recursively call _check_executable_path()
|
|
708
|
+
# via is_pipx() -> _check_executable_path() -> is_pipx() -> ...
|
|
709
|
+
if check_pipx:
|
|
710
|
+
caller = sys._getframe(1).f_code.co_name
|
|
711
|
+
if caller != "is_pipx":
|
|
712
|
+
if is_pipx(exec_path, debug):
|
|
713
|
+
if debug:
|
|
714
|
+
logging.debug("_check_executable_path() returns (exec_path, False) // is_pipx(exec_path) is True")
|
|
715
|
+
return exec_path, False
|
|
715
716
|
|
|
716
|
-
return exec_path, True
|
|
717
|
+
return exec_path, True
|
|
718
|
+
|
|
717
719
|
|
|
718
720
|
# --- Main Function for report and CLI compatibility ---
|
|
719
721
|
|
|
@@ -798,7 +800,7 @@ def main(path=None, debug=False):
|
|
|
798
800
|
# Supress redundant prints explicity using suppress_debug=True,
|
|
799
801
|
# so that only unique information gets printed for each check,
|
|
800
802
|
# even when more than one use the same functions which include debugging logs.
|
|
801
|
-
|
|
803
|
+
#print(f"_check_executable_path(script_path, debug=True)")
|
|
802
804
|
_check_executable_path(script_path, debug=debug)
|
|
803
805
|
#print(f"read_magic_bites(script_path, debug=True)")
|
|
804
806
|
read_magic_bytes(script_path, debug=debug)
|
|
@@ -821,4 +823,7 @@ def main(path=None, debug=False):
|
|
|
821
823
|
print("=================================")
|
|
822
824
|
print("=== PyHabitat Report Complete ===")
|
|
823
825
|
print("=================================")
|
|
824
|
-
print("")
|
|
826
|
+
print("")
|
|
827
|
+
|
|
828
|
+
if __name__ == "__main__":
|
|
829
|
+
main(debug=True)
|
pyhabitat/utils.py
ADDED
|
@@ -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)"
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
pyhabitat/__init__.py,sha256=spekwjNKjr9i8HgPmShCMpNFzol3okeQw3t2JReqtaY,1346
|
|
2
|
+
pyhabitat/__main__.py,sha256=qlOKShd_Xk0HGpYOySXQSQe3K60a9wwrstCAceJkKIs,76
|
|
3
|
+
pyhabitat/__main__stable.py,sha256=UACpHLrr_Rmf0L5dJCEae6kFzLn7dqCqIri68IBnb10,2910
|
|
4
|
+
pyhabitat/cli.py,sha256=80t9cOwfmDktnHnhGHL-iEgJBXkB0bVCJBPrVWzWUPU,980
|
|
5
|
+
pyhabitat/environment.py,sha256=7S6dqdmT9cheT2nkjwmLHGt8Iks1Fayezq5P7OEy8NI,33468
|
|
6
|
+
pyhabitat/utils.py,sha256=h-TPwxZr93LOvjrIrDrsBWdU2Vhc4FUvAhDqIv-N0GQ,537
|
|
7
|
+
pyhabitat-1.0.21.dist-info/licenses/LICENSE,sha256=D4fg30ctUGnCJlWu3ONv5-V8JE1v3ctakoJTcVjsJlg,1072
|
|
8
|
+
pyhabitat-1.0.21.dist-info/METADATA,sha256=DYmPZjp8zQrDX0nHfjG7oXHPyyIGr_Zqivmvl1q-t8o,10900
|
|
9
|
+
pyhabitat-1.0.21.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
10
|
+
pyhabitat-1.0.21.dist-info/entry_points.txt,sha256=409xZ-BrarQJJLtO-aActCGkL0FMhNVi9wsq3u7tRHM,52
|
|
11
|
+
pyhabitat-1.0.21.dist-info/top_level.txt,sha256=zXYK44Qu8EqxUETREvd2diMUaB5JiGRErkwFaoLQnnI,10
|
|
12
|
+
pyhabitat-1.0.21.dist-info/RECORD,,
|
|
@@ -1,11 +0,0 @@
|
|
|
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=YlrcmovQBa7JapQLCqG9R6pjmYElL-8oVUHVSXH4MLw,33007
|
|
6
|
-
pyhabitat-1.0.20.dist-info/licenses/LICENSE,sha256=D4fg30ctUGnCJlWu3ONv5-V8JE1v3ctakoJTcVjsJlg,1072
|
|
7
|
-
pyhabitat-1.0.20.dist-info/METADATA,sha256=1MXhoZ1tOLB_tm1JHkH21-vubm3QWtlSW4os7GTB_WE,10900
|
|
8
|
-
pyhabitat-1.0.20.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
9
|
-
pyhabitat-1.0.20.dist-info/entry_points.txt,sha256=409xZ-BrarQJJLtO-aActCGkL0FMhNVi9wsq3u7tRHM,52
|
|
10
|
-
pyhabitat-1.0.20.dist-info/top_level.txt,sha256=zXYK44Qu8EqxUETREvd2diMUaB5JiGRErkwFaoLQnnI,10
|
|
11
|
-
pyhabitat-1.0.20.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|