pyhabitat 1.0.30__py3-none-any.whl → 1.0.35__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/demo.py ADDED
File without changes
pyhabitat/environment.py CHANGED
@@ -17,6 +17,12 @@ import logging
17
17
  import getpass
18
18
  import select
19
19
 
20
+ # On Windows, we need the msvcrt module for non-blocking I/O
21
+ try:
22
+ import msvcrt
23
+ except ImportError:
24
+ msvcrt = None
25
+
20
26
  __all__ = [
21
27
  'matplotlib_is_available_for_gui_plotting',
22
28
  'matplotlib_is_available_for_headless_image_export',
@@ -556,21 +562,46 @@ def interactive_terminal_is_available():
556
562
  then typer.prompt() or input() will work reliably,
557
563
  without getting lost in a log or lost entirely.
558
564
 
565
+ Solution correctly identifies that true interactivity requires:
566
+ (1) a TTY (potential) connection
567
+ (2) the ability to execute
568
+ (3) the ability to read I/O
569
+ (4) ignores known limitatons in restrictive environments
570
+
571
+ Jargon:
572
+ A TTY, short for Teletypewriter or TeleTYpe,
573
+ is a conceptual or physical device that serves
574
+ as the interface for a user to interact with
575
+ a computer system.
559
576
  """
560
577
  # Address walmart demo unit edge case, fast check, though this might hamstring othwrwise successful processes
561
578
  if user_darrin_deyoung():
562
579
  return False
580
+
581
+ # Check if a tty is attached to stdin,
582
+ # quick failure here if not before testing spwaning and reading
583
+ if not (sys.stdin.isatty() and sys.stdout.isatty()):
584
+ return False
585
+
563
586
  # Check of a new shell can be launched to print stuff
564
587
  if not can_spawn_shell():
565
588
  return False
589
+
566
590
  # A user can interact with a console, providing input
567
591
  #if not can_read_input():
568
592
  # return False
569
- # Check if a tty is attached to stdin
593
+
570
594
  return sys.stdin.isatty() and sys.stdout.isatty()
571
595
 
572
596
  def user_darrin_deyoung():
573
597
  """Common demo unit undicator, edge case that is unable to launch terminal"""
598
+ # Enable teating on non-Windows, non-demo systems
599
+ # where this function would otherwise return False.
600
+ # Linux: `export USER_DARRIN_DEYOUNG=True`
601
+ if os.getenv('USER_DARRIN_DEYOUNG','').lower() == "true":
602
+ print("env var USER_DARRIN_DEYOUNG is set to True.")
603
+ return True
604
+ # Darrin Deyoung is the typical username on demo-mode Windows systems
574
605
  if not on_windows():
575
606
  return False
576
607
  username = getpass.getuser()
@@ -581,25 +612,33 @@ def can_spawn_shell(override_known:bool=False)->bool:
581
612
  global _CAN_SPAWN_SHELL
582
613
  if _CAN_SPAWN_SHELL is not None and override_known is False:
583
614
  return _CAN_SPAWN_SHELL
584
- try:
585
- result = subprocess.run( ['echo', 'hello'],
586
- stdout=subprocess.PIPE, stderr=subprocess.PIPE,
587
- timeout=2 )
588
-
589
- _CAN_SPAWN_SHELL = True
615
+
616
+ try:
617
+ # Use a simple, universally applicable command with shell=True
618
+ # 'true' on Linux/macOS, or a basic command on Windows via cmd.exe
619
+ # A simple 'echo' or 'exit 0' would also work
620
+ result = subprocess.run(
621
+ 'exit 0', # A shell-internal command that succeeds on most shells
622
+ stdout=subprocess.PIPE,
623
+ stderr=subprocess.PIPE,
624
+ timeout=2,
625
+ shell=True # <--- ESSENTIAL for cross-platform reliability
626
+ )
590
627
 
591
- return result.returncode == 0
628
+ _CAN_SPAWN_SHELL = result.returncode == 0
629
+ return _CAN_SPAWN_SHELL
630
+
592
631
  except subprocess.TimeoutExpired:
593
- logging.debug("Shell spawn failed: TimeoutExpired")
632
+ print("Shell spawn failed: TimeoutExpired")
594
633
  _CAN_SPAWN_SHELL = result.returncode == 0
595
634
  return _CAN_SPAWN_SHELL
596
635
  except subprocess.SubprocessError:
597
- logging.debug("Shell spawn failed: SubprocessError")
636
+ print("Shell spawn failed: SubprocessError")
598
637
  _CAN_SPAWN_SHELL = False
599
638
  return False
600
639
  except OSError:
601
640
  _CAN_SPAWN_SHELL = False
602
- logging.debug("Shell spawn failed: OSError (likely permission or missing binary)")
641
+ print("Shell spawn failed: OSError (likely permission or missing binary)")
603
642
  return False
604
643
 
605
644
  def can_read_input(override_known:bool=False)-> bool:
@@ -607,10 +646,43 @@ def can_read_input(override_known:bool=False)-> bool:
607
646
  global _CAN_READ_INPUT
608
647
  if _CAN_READ_INPUT is not None and override_known is False:
609
648
  return _CAN_READ_INPUT
649
+
650
+ # --- 1. Windows Specific Check (msvcrt) ---
651
+ if msvcrt is not None and sys.stdin.isatty():
652
+ try:
653
+ # msvcrt.kbhit() checks if a keyboard hit is present
654
+ # We don't read the input yet, just check if it's there
655
+ _CAN_READ_INPUT = msvcrt.kbhit()
656
+ # If kbhit returns True, it means a key press is waiting.
657
+ # We assume if the terminal *is* a TTY, it *can* read input.
658
+ # We can't actually call input() without blocking, so we check TTY instead.
659
+ if _CAN_READ_INPUT:
660
+ return True
661
+
662
+ # Since we are checking if a *user can* interact, if we are in a TTY,
663
+ # we assume the capability exists, even if nothing is currently buffered.
664
+ # This prevents the false negative when no key is pressed.
665
+ _CAN_READ_INPUT = True
666
+ return True
667
+
668
+ except Exception as e:
669
+ # Catch errors in the kbhit check itself
670
+ logging.debug(f"msvcrt check failed: {e}")
671
+ pass # Fall through to the select check
672
+
673
+ # --- 2. POSIX/General Check (select) ---
674
+ # This block is reliable on Linux/macOS and other POSIX systems.
610
675
  try:
611
- # ERROR THIS IS NOT A BOOLEAN, IS RETURNS []
676
+ # _CAN_READ_INPUT is assigned the read-ready list ([] or [sys.stdin])
677
+ # The return value is then the boolean conversion of that list's truthiness.
678
+ # 1. select.select(...) returns a 3-element tuple.
679
+ # 2. [0] gets the read-ready list (rlist).
680
+ # 3. Wrapping the result in bool() converts the list's truth value:
681
+ # - [] becomes False
682
+ # - [sys.stdin] becomes True
612
683
  _CAN_READ_INPUT = select.select([sys.stdin], [], [], 0.1)[0]
613
- return _CAN_READ_INPUT
684
+ # Return the boolean value of the list: True if [sys.stdin], False if []
685
+ return bool(_CAN_READ_INPUT) # <--- Requied to convert list to boolean
614
686
  except ValueError:
615
687
  logging.debug("Input check failed: ValueError (invalid file descriptor)")
616
688
  _CAN_READ_INPUT = False
@@ -619,6 +691,9 @@ def can_read_input(override_known:bool=False)-> bool:
619
691
  logging.debug("Input check failed: OSError (likely I/O issue)")
620
692
  _CAN_READ_INPUT = False
621
693
  return False
694
+
695
+ # Final fallback: if nothing worked, assume False
696
+ return False
622
697
 
623
698
  # --- Browser Check ---
624
699
  def web_browser_is_available() -> bool:
@@ -1,13 +1,17 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyhabitat
3
- Version: 1.0.30
3
+ Version: 1.0.35
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
7
7
  Keywords: environment,os-detection,gui,build-system
8
8
  Classifier: Programming Language :: Python :: 3
9
9
  Classifier: Operating System :: OS Independent
10
- Classifier: Topic :: System :: Systems Administration
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Environment :: Console
12
+ Classifier: Topic :: Software Development :: Build Tools
13
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
14
+ Classifier: Topic :: Utilities
11
15
  Requires-Python: >=3.7
12
16
  Description-Content-Type: text/markdown
13
17
  License-File: LICENSE
@@ -0,0 +1,17 @@
1
+ pyhabitat/__init__.py,sha256=7XNuHIDBXvjn_EORWZZUNsBS3qpvWRUha1NHwC3d6bw,1539
2
+ pyhabitat/cli.py,sha256=L7VA-OOust2g6_MxedvvOr4RdnS_-5ZGP2Aj4VtdqAg,2221
3
+ pyhabitat/demo.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
+ pyhabitat/environment.py,sha256=zUlZlB9GGbZviEeOkV3yk7GceqUtbfZYXloeRyOK6vU,40221
5
+ pyhabitat/report.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
+ pyhabitat/utils.py,sha256=2IGLKGAvDFGQgIzlId4ZGGTJRGG0MXR9UVH3nlQKmHg,988
7
+ pyhabitat-1.0.35.dist-info/licenses/LICENSE,sha256=D4fg30ctUGnCJlWu3ONv5-V8JE1v3ctakoJTcVjsJlg,1072
8
+ pyhabitat-build/__main__.py,sha256=6wcF1BhvoRQe4iwq1Px0GNughRXGFRBufWRdaZePu1s,99
9
+ pyhabitat-build/pyhabitat/__init__.py,sha256=7XNuHIDBXvjn_EORWZZUNsBS3qpvWRUha1NHwC3d6bw,1539
10
+ pyhabitat-build/pyhabitat/cli.py,sha256=L7VA-OOust2g6_MxedvvOr4RdnS_-5ZGP2Aj4VtdqAg,2221
11
+ pyhabitat-build/pyhabitat/environment.py,sha256=zUlZlB9GGbZviEeOkV3yk7GceqUtbfZYXloeRyOK6vU,40221
12
+ pyhabitat-build/pyhabitat/utils.py,sha256=2IGLKGAvDFGQgIzlId4ZGGTJRGG0MXR9UVH3nlQKmHg,988
13
+ pyhabitat-1.0.35.dist-info/METADATA,sha256=1dpi0OLV7wFvGCooNpCwxaUL8rhTFi_avWVTkVgqffY,11086
14
+ pyhabitat-1.0.35.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
15
+ pyhabitat-1.0.35.dist-info/entry_points.txt,sha256=409xZ-BrarQJJLtO-aActCGkL0FMhNVi9wsq3u7tRHM,52
16
+ pyhabitat-1.0.35.dist-info/top_level.txt,sha256=zI4aHwfZxhq45fnyLM34qDaerg2Iyb4mcsOP-itrJPY,26
17
+ pyhabitat-1.0.35.dist-info/RECORD,,
@@ -30,6 +30,8 @@ from .environment import (
30
30
  user_darrin_deyoung,
31
31
  can_read_input,
32
32
  can_spawn_shell,
33
+ is_ascii,
34
+ is_binary,
33
35
  )
34
36
 
35
37
  # Optional: Set __all__ for explicit documentation and cleaner imports
@@ -63,6 +65,8 @@ __all__ = [
63
65
  'user_darrin_deyoung',
64
66
  'can_read_input',
65
67
  'can_spawn_shell',
68
+ 'is_ascii',
69
+ 'is_binary',
66
70
  ]
67
71
 
68
72
  __version__ = get_version()
@@ -17,6 +17,12 @@ import logging
17
17
  import getpass
18
18
  import select
19
19
 
20
+ # On Windows, we need the msvcrt module for non-blocking I/O
21
+ try:
22
+ import msvcrt
23
+ except ImportError:
24
+ msvcrt = None
25
+
20
26
  __all__ = [
21
27
  'matplotlib_is_available_for_gui_plotting',
22
28
  'matplotlib_is_available_for_headless_image_export',
@@ -512,6 +518,19 @@ def is_python_script(path: Path | str | None = None, debug: bool = False, suppre
512
518
  return False
513
519
  return exec_path.suffix.lower() == '.py'
514
520
 
521
+ # --- File encoding check ---
522
+ def is_binary(path:str|Path|None=None)->bool:
523
+ """
524
+ Target file is encoded as binary.
525
+ """
526
+ pass
527
+
528
+ def is_ascii(path:str|Path|None=None)->bool:
529
+ """
530
+ Target file is encoded as ascii, plaintext.
531
+ """
532
+ pass
533
+
515
534
  # --- Interpreter Check ---
516
535
 
517
536
  def interp_path(debug: bool = False) -> str:
@@ -543,21 +562,46 @@ def interactive_terminal_is_available():
543
562
  then typer.prompt() or input() will work reliably,
544
563
  without getting lost in a log or lost entirely.
545
564
 
565
+ Solution correctly identifies that true interactivity requires:
566
+ (1) a TTY (potential) connection
567
+ (2) the ability to execute
568
+ (3) the ability to read I/O
569
+ (4) ignores known limitatons in restrictive environments
570
+
571
+ Jargon:
572
+ A TTY, short for Teletypewriter or TeleTYpe,
573
+ is a conceptual or physical device that serves
574
+ as the interface for a user to interact with
575
+ a computer system.
546
576
  """
547
577
  # Address walmart demo unit edge case, fast check, though this might hamstring othwrwise successful processes
548
- if in_repl() and user_darrin_deyoung():
578
+ if user_darrin_deyoung():
549
579
  return False
550
- # A new shell can be launched to print stuff
580
+
581
+ # Check if a tty is attached to stdin,
582
+ # quick failure here if not before testing spwaning and reading
583
+ if not (sys.stdin.isatty() and sys.stdout.isatty()):
584
+ return False
585
+
586
+ # Check of a new shell can be launched to print stuff
551
587
  if not can_spawn_shell():
552
588
  return False
589
+
553
590
  # A user can interact with a console, providing input
554
591
  #if not can_read_input():
555
592
  # return False
556
- # Check if a tty is attached to stdin
593
+
557
594
  return sys.stdin.isatty() and sys.stdout.isatty()
558
595
 
559
596
  def user_darrin_deyoung():
560
597
  """Common demo unit undicator, edge case that is unable to launch terminal"""
598
+ # Enable teating on non-Windows, non-demo systems
599
+ # where this function would otherwise return False.
600
+ # Linux: `export USER_DARRIN_DEYOUNG=True`
601
+ if os.getenv('USER_DARRIN_DEYOUNG','').lower() == "true":
602
+ print("env var USER_DARRIN_DEYOUNG is set to True.")
603
+ return True
604
+ # Darrin Deyoung is the typical username on demo-mode Windows systems
561
605
  if not on_windows():
562
606
  return False
563
607
  username = getpass.getuser()
@@ -568,25 +612,33 @@ def can_spawn_shell(override_known:bool=False)->bool:
568
612
  global _CAN_SPAWN_SHELL
569
613
  if _CAN_SPAWN_SHELL is not None and override_known is False:
570
614
  return _CAN_SPAWN_SHELL
571
- try:
572
- result = subprocess.run( ['echo', 'hello'],
573
- stdout=subprocess.PIPE, stderr=subprocess.PIPE,
574
- timeout=2 )
575
-
576
- _CAN_SPAWN_SHELL = True
615
+
616
+ try:
617
+ # Use a simple, universally applicable command with shell=True
618
+ # 'true' on Linux/macOS, or a basic command on Windows via cmd.exe
619
+ # A simple 'echo' or 'exit 0' would also work
620
+ result = subprocess.run(
621
+ 'exit 0', # A shell-internal command that succeeds on most shells
622
+ stdout=subprocess.PIPE,
623
+ stderr=subprocess.PIPE,
624
+ timeout=2,
625
+ shell=True # <--- ESSENTIAL for cross-platform reliability
626
+ )
577
627
 
578
- return result.returncode == 0
628
+ _CAN_SPAWN_SHELL = result.returncode == 0
629
+ return _CAN_SPAWN_SHELL
630
+
579
631
  except subprocess.TimeoutExpired:
580
- logging.debug("Shell spawn failed: TimeoutExpired")
632
+ print("Shell spawn failed: TimeoutExpired")
581
633
  _CAN_SPAWN_SHELL = result.returncode == 0
582
634
  return _CAN_SPAWN_SHELL
583
635
  except subprocess.SubprocessError:
584
- logging.debug("Shell spawn failed: SubprocessError")
636
+ print("Shell spawn failed: SubprocessError")
585
637
  _CAN_SPAWN_SHELL = False
586
638
  return False
587
639
  except OSError:
588
640
  _CAN_SPAWN_SHELL = False
589
- logging.debug("Shell spawn failed: OSError (likely permission or missing binary)")
641
+ print("Shell spawn failed: OSError (likely permission or missing binary)")
590
642
  return False
591
643
 
592
644
  def can_read_input(override_known:bool=False)-> bool:
@@ -594,10 +646,43 @@ def can_read_input(override_known:bool=False)-> bool:
594
646
  global _CAN_READ_INPUT
595
647
  if _CAN_READ_INPUT is not None and override_known is False:
596
648
  return _CAN_READ_INPUT
649
+
650
+ # --- 1. Windows Specific Check (msvcrt) ---
651
+ if msvcrt is not None and sys.stdin.isatty():
652
+ try:
653
+ # msvcrt.kbhit() checks if a keyboard hit is present
654
+ # We don't read the input yet, just check if it's there
655
+ _CAN_READ_INPUT = msvcrt.kbhit()
656
+ # If kbhit returns True, it means a key press is waiting.
657
+ # We assume if the terminal *is* a TTY, it *can* read input.
658
+ # We can't actually call input() without blocking, so we check TTY instead.
659
+ if _CAN_READ_INPUT:
660
+ return True
661
+
662
+ # Since we are checking if a *user can* interact, if we are in a TTY,
663
+ # we assume the capability exists, even if nothing is currently buffered.
664
+ # This prevents the false negative when no key is pressed.
665
+ _CAN_READ_INPUT = True
666
+ return True
667
+
668
+ except Exception as e:
669
+ # Catch errors in the kbhit check itself
670
+ logging.debug(f"msvcrt check failed: {e}")
671
+ pass # Fall through to the select check
672
+
673
+ # --- 2. POSIX/General Check (select) ---
674
+ # This block is reliable on Linux/macOS and other POSIX systems.
597
675
  try:
598
- # ERROR THIS IS NOT A BOOLEAN, IS RETURNS []
676
+ # _CAN_READ_INPUT is assigned the read-ready list ([] or [sys.stdin])
677
+ # The return value is then the boolean conversion of that list's truthiness.
678
+ # 1. select.select(...) returns a 3-element tuple.
679
+ # 2. [0] gets the read-ready list (rlist).
680
+ # 3. Wrapping the result in bool() converts the list's truth value:
681
+ # - [] becomes False
682
+ # - [sys.stdin] becomes True
599
683
  _CAN_READ_INPUT = select.select([sys.stdin], [], [], 0.1)[0]
600
- return _CAN_READ_INPUT
684
+ # Return the boolean value of the list: True if [sys.stdin], False if []
685
+ return bool(_CAN_READ_INPUT) # <--- Requied to convert list to boolean
601
686
  except ValueError:
602
687
  logging.debug("Input check failed: ValueError (invalid file descriptor)")
603
688
  _CAN_READ_INPUT = False
@@ -606,6 +691,9 @@ def can_read_input(override_known:bool=False)-> bool:
606
691
  logging.debug("Input check failed: OSError (likely I/O issue)")
607
692
  _CAN_READ_INPUT = False
608
693
  return False
694
+
695
+ # Final fallback: if nothing worked, assume False
696
+ return False
609
697
 
610
698
  # --- Browser Check ---
611
699
  def web_browser_is_available() -> bool:
@@ -623,6 +711,7 @@ def web_browser_is_available() -> bool:
623
711
  if shutil.which("xdg-open"):
624
712
  return True
625
713
  return False
714
+
626
715
 
627
716
  # --- LAUNCH MECHANISMS BASED ON ENVIRONMENT ---
628
717
  def edit_textfile(path: Path | str | None = None) -> None:
@@ -1,16 +0,0 @@
1
- pyhabitat/__init__.py,sha256=7XNuHIDBXvjn_EORWZZUNsBS3qpvWRUha1NHwC3d6bw,1539
2
- pyhabitat/cli.py,sha256=L7VA-OOust2g6_MxedvvOr4RdnS_-5ZGP2Aj4VtdqAg,2221
3
- pyhabitat/environment.py,sha256=Us6AXG3T3ocSa8giPvg_zSPVneIQsxa6FjBgcBINmOg,36980
4
- pyhabitat/report.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
- pyhabitat/utils.py,sha256=2IGLKGAvDFGQgIzlId4ZGGTJRGG0MXR9UVH3nlQKmHg,988
6
- pyhabitat-1.0.30.dist-info/licenses/LICENSE,sha256=D4fg30ctUGnCJlWu3ONv5-V8JE1v3ctakoJTcVjsJlg,1072
7
- pyhabitat-build/__main__.py,sha256=6wcF1BhvoRQe4iwq1Px0GNughRXGFRBufWRdaZePu1s,99
8
- pyhabitat-build/pyhabitat/__init__.py,sha256=UIlLMIMnJHwV1UHAqNgvC_HURGtCUUgp-34-329FV6E,1477
9
- pyhabitat-build/pyhabitat/cli.py,sha256=L7VA-OOust2g6_MxedvvOr4RdnS_-5ZGP2Aj4VtdqAg,2221
10
- pyhabitat-build/pyhabitat/environment.py,sha256=4CL7y1KRoYvaHU9uR5jToOmvo_l9OUQPSHdW-i-WXRM,36721
11
- pyhabitat-build/pyhabitat/utils.py,sha256=2IGLKGAvDFGQgIzlId4ZGGTJRGG0MXR9UVH3nlQKmHg,988
12
- pyhabitat-1.0.30.dist-info/METADATA,sha256=8C1M_s19eT8bPsQr6jt7KOXkebKpntZzmKwHlwZydcI,10900
13
- pyhabitat-1.0.30.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
14
- pyhabitat-1.0.30.dist-info/entry_points.txt,sha256=409xZ-BrarQJJLtO-aActCGkL0FMhNVi9wsq3u7tRHM,52
15
- pyhabitat-1.0.30.dist-info/top_level.txt,sha256=zI4aHwfZxhq45fnyLM34qDaerg2Iyb4mcsOP-itrJPY,26
16
- pyhabitat-1.0.30.dist-info/RECORD,,