pyhabitat 1.0.19__tar.gz → 1.0.21__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.

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyhabitat
3
- Version: 1.0.19
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
@@ -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.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
@@ -226,18 +227,21 @@ def on_wsl():
226
227
  if "WSL_DISTRO_NAME" in os.environ or "WSL_INTEROP" in os.environ:
227
228
  return True
228
229
 
229
- # --- Check kernel info for 'microsoft' string ---
230
+ # --- Check kernel info for 'microsoft' or 'wsl' string (Fallback) ---
230
231
  # False negative risk:
231
232
  # Custom kernels, future Windows versions, or minimal WSL distros may omit 'microsoft' in strings.
232
233
  # False negative likelihood: Very low to moderate.
233
-
234
234
  try:
235
235
  with open("/proc/version") as f:
236
+ version_info = f.read().lower()
236
237
  if "microsoft" in version_info or "wsl" in version_info:
237
238
  return True
238
- except FileNotFoundError:
239
+ except (IOError, OSError):
240
+ # This block would catch the PermissionError!
241
+ # It would simply 'pass' and move on.
239
242
  pass
240
-
243
+
244
+
241
245
  # Check for WSL-specific mounts (fallback)
242
246
  """
243
247
  /proc/sys/kernel/osrelease
@@ -245,17 +249,17 @@ def on_wsl():
245
249
  Very reliable for detecting WSL1 and WSL2 unless someone compiled a custom kernel and removed the microsoft string.
246
250
 
247
251
  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.
252
+ If /proc/sys/kernel/osrelease cannot be read due to permissions, a containerized WSL distro, or some sandboxed environment.
249
253
  # False negative likelihood: Very low.
250
254
  """
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
255
+ try:
256
+ with open("/proc/sys/kernel/osrelease") as f:
257
+ osrelease = f.read().lower()
258
+ if "microsoft" in osrelease:
259
+ return True
260
+ except (IOError, OSError):
261
+ # This block would catch the PermissionError, an FileNotFound
262
+ pass
259
263
  return False
260
264
 
261
265
  def on_pydroid():
@@ -427,75 +431,52 @@ def is_macos_executable(exec_path: Path | str | None = None, debug: bool = False
427
431
  def is_pipx(exec_path: Path | str | None = None, debug: bool = False, suppress_debug: bool = True) -> bool:
428
432
  """Checks if the executable is running from a pipx managed environment."""
429
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.
430
436
  if not is_valid:
431
437
  return False
432
438
 
433
439
  try:
434
440
  interpreter_path = Path(sys.executable).resolve()
435
441
  pipx_bin_path, pipx_venv_base_path = _get_pipx_paths()
442
+
436
443
  # Normalize paths for comparison
437
444
  norm_exec_path = str(exec_path).lower()
438
445
  norm_interp_path = str(interpreter_path).lower()
446
+ pipx_venv_base_str = str(pipx_venv_base_path).lower()
439
447
 
440
448
  if debug:
441
449
  logging.debug(f"EXEC_PATH: {exec_path}")
442
450
  logging.debug(f"INTERP_PATH: {interpreter_path}")
443
451
  logging.debug(f"PIPX_BIN_PATH: {pipx_bin_path}")
444
452
  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())
453
+ is_in_pipx_venv_base = norm_interp_path.startswith(pipx_venv_base_str)
446
454
  logging.debug(f"Interpreter path resides somewhere within the pipx venv base hierarchy: {is_in_pipx_venv_base}")
447
455
  logging.debug(
448
456
  f"This determines whether the current interpreter is managed by pipx: {is_in_pipx_venv_base}"
449
457
  )
450
458
  if "pipx/venvs" in norm_exec_path or "pipx/venvs" in norm_interp_path:
451
459
  if debug:
452
- logging.debug("True (Signature Check)")
460
+ logging.debug("is_pipx() is True // Signature Check")
453
461
  return True
454
462
 
455
- if norm_interp_path.startswith(str(pipx_venv_base_path).lower()):
463
+ if norm_interp_path.startswith(pipx_venv_base_str):
456
464
  if debug:
457
- logging.debug("True (Interpreter Base Check)")
465
+ logging.debug("is_pipx() is True // Interpreter Base Check")
458
466
  return True
459
467
 
460
- if norm_exec_path.startswith(str(pipx_venv_base_path).lower()):
468
+ if norm_exec_path.startswith(pipx_venv_base_str):
461
469
  if debug:
462
- logging.debug("True (Executable Base Check)")
470
+ logging.debug("is_pipx() is True // Executable Base Check")
463
471
  return True
464
472
 
465
- return False
466
- except Exception:
467
473
  if debug:
468
- logging.debug("False (Exception during pipx check)")
474
+ logging.debug("is_pipx() is False")
469
475
  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())}")
476
-
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
476
 
492
- if debug:
493
- logging.debug("False")
494
- return False
495
477
  except Exception:
496
478
  if debug:
497
479
  logging.debug("False (Exception during pipx check)")
498
- return False
499
480
 
500
481
  def is_python_script(path: Path | str | None = None, debug: bool = False, suppress_debug: bool =False) -> bool:
501
482
  """
@@ -574,8 +555,8 @@ def edit_textfile(path: Path | str | None = None) -> None:
574
555
  #def open_text_file_for_editing(path): # defunct function name as of 1.0.16
575
556
  """
576
557
  Opens a file with the environment's default application (Windows, Linux, macOS)
577
- or a guaranteed console editor (nano) in constrained environments (Termux, iSH)
578
- after ensuring line-ending compatibility.
558
+ or a guaranteed console editor (nano) in constrained environments (Termux, iSH).
559
+ Ensures line-ending compatibility where possible.
579
560
 
580
561
  This function is known to fail on PyDroid3, where on_linus() is True but xdg-open
581
562
  is not available.
@@ -590,28 +571,28 @@ def edit_textfile(path: Path | str | None = None) -> None:
590
571
  os.startfile(path)
591
572
  elif on_termux():
592
573
  # Install dependencies if missing (Termux pkg returns non-zero if already installed, so no check=True)
593
- subprocess.run(['pkg','install', 'dos2unix', 'nano'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
594
- _run_dos2unix(path)
595
- 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)])
596
577
  elif on_ish_alpine():
597
578
  # Install dependencies if missing (apk returns 0 if already installed, so check=True is safe)
598
- subprocess.run(['apk','add', 'dos2unix'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=True)
599
- subprocess.run(['apk','add', 'nano'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=True)
600
- _run_dos2unix(path)
601
- 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)])
602
583
  # --- Standard Unix-like Systems (Conversion + Default App) ---
603
584
  elif on_linux():
604
- _run_dos2unix(path) # Safety conversion for user-defined console apps
605
- subprocess.run(['xdg-open', path])
585
+ _run_dos2unix(path) # Safety conversion for user-defined console apps
586
+ subprocess.run(['xdg-open', str(path)])
606
587
  elif on_apple():
607
- _run_dos2unix(path) # Safety conversion for user-defined console apps
608
- subprocess.run(['open', path])
588
+ _run_dos2unix(path) # Safety conversion for user-defined console apps
589
+ subprocess.run(['open', str(path)])
609
590
  else:
610
- print("Unsupported operating system.")
591
+ print("Unsupported operating system.")
611
592
  except Exception as e:
612
- print("The file could not be opened for editing in the current environment. Known failure points: Pydroid3")
613
-
614
- """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:
615
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.
616
597
  """
617
598
 
@@ -633,8 +614,10 @@ def _run_dos2unix(path: Path | str | None = None):
633
614
  # Catch other subprocess errors (e.g. permission issues)
634
615
  pass
635
616
 
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."""
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
+ """
638
621
  try:
639
622
  with open(path, "rb") as f:
640
623
  magic = f.read(length)
@@ -644,7 +627,9 @@ def read_magic_bytes(path: str, length: int = 4, debug: bool = False) -> bytes:
644
627
  except Exception as e:
645
628
  if debug:
646
629
  logging.debug(f"False (Error during file check: {e})")
647
- 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
648
633
 
649
634
  def _get_pipx_paths():
650
635
  """
@@ -685,8 +670,19 @@ def _check_if_zip(path: Path | str | None) -> bool:
685
670
  # Handle cases where the path might be invalid, or other unexpected errors
686
671
  return False
687
672
 
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."""
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
690
686
  if exec_path is None:
691
687
  exec_path = Path(sys.argv[0]).resolve() if sys.argv[0] and sys.argv[0] != '-c' else None
692
688
  else:
@@ -695,22 +691,31 @@ def _check_executable_path(exec_path: Path | str | None, debug: bool = False, ch
695
691
  if debug:
696
692
  logging.debug(f"Checking executable path: {exec_path}")
697
693
 
694
+ # 2. Handle missing path
698
695
  if exec_path is None:
699
696
  if debug:
700
- logging.debug("False (No valid path)")
697
+ logging.debug("_check_executable_path() returns (None, False) // exec_path is None")
701
698
  return None, False
702
-
703
- 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():
704
702
  if debug:
705
- logging.debug("False (is_pipx is True)")
703
+ logging.debug("_check_executable_path() returns (exec_path, False) // exec_path is not a file")
706
704
  return exec_path, False
707
705
 
708
- if not exec_path.is_file():
709
- if debug:
710
- logging.debug("False (Not a file)")
711
- 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
712
716
 
713
- return exec_path, True
717
+ return exec_path, True
718
+
714
719
 
715
720
  # --- Main Function for report and CLI compatibility ---
716
721
 
@@ -795,7 +800,7 @@ def main(path=None, debug=False):
795
800
  # Supress redundant prints explicity using suppress_debug=True,
796
801
  # so that only unique information gets printed for each check,
797
802
  # even when more than one use the same functions which include debugging logs.
798
- p#rint(f"_check_executable_path(script_path, debug=True)")
803
+ #print(f"_check_executable_path(script_path, debug=True)")
799
804
  _check_executable_path(script_path, debug=debug)
800
805
  #print(f"read_magic_bites(script_path, debug=True)")
801
806
  read_magic_bytes(script_path, debug=debug)
@@ -818,4 +823,7 @@ def main(path=None, debug=False):
818
823
  print("=================================")
819
824
  print("=== PyHabitat Report Complete ===")
820
825
  print("=================================")
821
- print("")
826
+ print("")
827
+
828
+ if __name__ == "__main__":
829
+ 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)"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyhabitat
3
- Version: 1.0.19
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
@@ -6,6 +6,7 @@ pyhabitat/__main__.py
6
6
  pyhabitat/__main__stable.py
7
7
  pyhabitat/cli.py
8
8
  pyhabitat/environment.py
9
+ pyhabitat/utils.py
9
10
  pyhabitat.egg-info/PKG-INFO
10
11
  pyhabitat.egg-info/SOURCES.txt
11
12
  pyhabitat.egg-info/dependency_links.txt
@@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta"
6
6
 
7
7
  [project]
8
8
  name = "pyhabitat"
9
- version = "1.0.19"
9
+ version = "1.0.21"
10
10
  #dynamic = ["version"] #
11
11
  authors = [
12
12
  { name="George Clayton Bennett", email="george.bennett@memphistn.gov" },
File without changes
File without changes
File without changes