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 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.use('TkAgg', force=True)
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(str(pipx_venv_base_path).lower())
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 (Signature Check)")
460
+ logging.debug("is_pipx() is True // Signature Check")
456
461
  return True
457
462
 
458
- if norm_interp_path.startswith(str(pipx_venv_base_path).lower()):
463
+ if norm_interp_path.startswith(pipx_venv_base_str):
459
464
  if debug:
460
- logging.debug("True (Interpreter Base Check)")
465
+ logging.debug("is_pipx() is True // Interpreter Base Check")
461
466
  return True
462
467
 
463
- if norm_exec_path.startswith(str(pipx_venv_base_path).lower()):
468
+ if norm_exec_path.startswith(pipx_venv_base_str):
464
469
  if debug:
465
- logging.debug("True (Executable Base Check)")
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("False (Exception during pipx check)")
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
- after ensuring line-ending compatibility.
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
- subprocess.run(['pkg','install', 'dos2unix', 'nano'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
597
- _run_dos2unix(path)
598
- subprocess.run(['nano', path])
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
- subprocess.run(['apk','add', 'dos2unix'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=True)
602
- subprocess.run(['apk','add', 'nano'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=True)
603
- _run_dos2unix(path)
604
- subprocess.run(['nano', path])
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
- _run_dos2unix(path) # Safety conversion for user-defined console apps
608
- subprocess.run(['xdg-open', path])
585
+ _run_dos2unix(path) # Safety conversion for user-defined console apps
586
+ subprocess.run(['xdg-open', str(path)])
609
587
  elif on_apple():
610
- _run_dos2unix(path) # Safety conversion for user-defined console apps
611
- subprocess.run(['open', path])
588
+ _run_dos2unix(path) # Safety conversion for user-defined console apps
589
+ subprocess.run(['open', str(path)])
612
590
  else:
613
- print("Unsupported operating system.")
591
+ print("Unsupported operating system.")
614
592
  except Exception as e:
615
- print("The file could not be opened for editing in the current environment. Known failure points: Pydroid3")
616
-
617
- """Why Not Use check=True on Termux:
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, debug: bool = False, check_pipx: bool = True) -> tuple[Path | None, bool]:
692
- """Helper function to resolve executable path and perform common checks."""
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("False (No valid path)")
697
+ logging.debug("_check_executable_path() returns (None, False) // exec_path is None")
704
698
  return None, False
705
-
706
- if check_pipx and is_pipx(exec_path, debug):
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("False (is_pipx is True)")
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
- if not exec_path.is_file():
712
- if debug:
713
- logging.debug("False (Not a file)")
714
- return exec_path, False
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
- p#rint(f"_check_executable_path(script_path, debug=True)")
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)"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyhabitat
3
- Version: 1.0.20
3
+ Version: 1.0.21
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
@@ -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,,