pyhabitat 1.0.20__py3-none-any.whl → 1.0.22__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
@@ -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 Exception:
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
- magic_bytes = read_magic_bytes(exec_path, 2, debug and not suppress_debug)
393
- result = magic_bytes.startswith(b"MZ")
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 Exception:
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(str(pipx_venv_base_path).lower())
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 (Signature Check)")
469
+ logging.debug("is_pipx() is True // Signature Check")
456
470
  return True
457
471
 
458
- if norm_interp_path.startswith(str(pipx_venv_base_path).lower()):
472
+ if norm_interp_path.startswith(pipx_venv_base_str):
459
473
  if debug:
460
- logging.debug("True (Interpreter Base Check)")
474
+ logging.debug("is_pipx() is True // Interpreter Base Check")
461
475
  return True
462
476
 
463
- if norm_exec_path.startswith(str(pipx_venv_base_path).lower()):
477
+ if norm_exec_path.startswith(pipx_venv_base_str):
464
478
  if debug:
465
- logging.debug("True (Executable Base Check)")
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("False (Exception during pipx check)")
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
- after ensuring line-ending compatibility.
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
- subprocess.run(['pkg','install', 'dos2unix', 'nano'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
597
- _run_dos2unix(path)
598
- subprocess.run(['nano', path])
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
- 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])
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
- _run_dos2unix(path) # Safety conversion for user-defined console apps
608
- subprocess.run(['xdg-open', path])
594
+ _run_dos2unix(path) # Safety conversion for user-defined console apps
595
+ subprocess.run(['xdg-open', str(path)])
609
596
  elif on_apple():
610
- _run_dos2unix(path) # Safety conversion for user-defined console apps
611
- subprocess.run(['open', path])
597
+ _run_dos2unix(path) # Safety conversion for user-defined console apps
598
+ subprocess.run(['open', str(path)])
612
599
  else:
613
- print("Unsupported operating system.")
600
+ print("Unsupported operating system.")
614
601
  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:
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, debug: bool = False, check_pipx: bool = True) -> tuple[Path | None, bool]:
692
- """Helper function to resolve executable path and perform common checks."""
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("False (No valid path)")
706
+ logging.debug("_check_executable_path() returns (None, False) // exec_path is None")
704
707
  return None, False
705
-
706
- if check_pipx and is_pipx(exec_path, debug):
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("False (is_pipx is True)")
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
- if not exec_path.is_file():
712
- if debug:
713
- logging.debug("False (Not a file)")
714
- return exec_path, False
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
- p#rint(f"_check_executable_path(script_path, debug=True)")
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)
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.22
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=CwAJDYRNbA5Dzb7feHptxmgKb4_IayQGA_yRGHEpse0,33876
6
+ pyhabitat/utils.py,sha256=h-TPwxZr93LOvjrIrDrsBWdU2Vhc4FUvAhDqIv-N0GQ,537
7
+ pyhabitat-1.0.22.dist-info/licenses/LICENSE,sha256=D4fg30ctUGnCJlWu3ONv5-V8JE1v3ctakoJTcVjsJlg,1072
8
+ pyhabitat-1.0.22.dist-info/METADATA,sha256=5y9z1BrdEYa0X29X0H213fgbD3fIo0XoMFPtudCW-Gg,10900
9
+ pyhabitat-1.0.22.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
10
+ pyhabitat-1.0.22.dist-info/entry_points.txt,sha256=409xZ-BrarQJJLtO-aActCGkL0FMhNVi9wsq3u7tRHM,52
11
+ pyhabitat-1.0.22.dist-info/top_level.txt,sha256=zXYK44Qu8EqxUETREvd2diMUaB5JiGRErkwFaoLQnnI,10
12
+ pyhabitat-1.0.22.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,,