pyhabitat 1.0.16__tar.gz → 1.0.17__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.16
3
+ Version: 1.0.17
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
@@ -59,11 +59,11 @@ Ultimately, [City-of-Memphis-Wastewater](https://github.com/City-of-Memphis-Wast
59
59
 
60
60
  * **Definitive Environment Checks:** Rigorous checks catered to Termux and iSH (iOS Alpine). Accurate, typical modern detection for Windows, macOS (Apple), Linux, FreeBSD, Android.
61
61
  * **GUI Availability:** Rigorous, cached checks to determine if the environment supports a graphical popup window (Tkinter/Matplotlib TkAgg) or just headless image export (Matplotlib Agg).
62
- * **Build/Packaging Detection:** Reliable detection of standalone executables built by tools like PyInstaller, and, crucially, correct identification and exclusion of pipx-managed virtual environments, which also user binaries that could conflate the check.
63
- * **Executable Type Inspection:** Uses file magic numbers (ELF and MZ) to confirm if the running script is a monolithic, frozen binary (non-pipx).
62
+ * **Build/Packaging Detection:** Reliable detection of standalone executables (PyInstaller), Python zipapps (.pyz), Python source scripts (.py), and correct identification/exclusion of pipx-managed virtual environments.
63
+ * **Executable Type Inspection:** Uses file magic numbers (ELF, MZ, Mach-O) to confirm if the running script is a monolithic, frozen binary (non-pipx) or zipapp (.pyz).
64
64
 
65
65
  </details>
66
-
66
+
67
67
  ---
68
68
 
69
69
  <details>
@@ -75,26 +75,31 @@ Key question: "What is this running on?"
75
75
 
76
76
  | Function | Description |
77
77
  | :--- | :--- |
78
- | `is_windows()` | Returns `True` on Windows. |
79
- | `is_apple()` | Returns `True` on macOS (Darwin). |
80
- | `is_linux()` | Returns `True` on Linux in general. |
81
- | `is_termux()` | Returns `True` if running in the Termux Android environment. |
82
- | `is_ish_alpine()` | Returns `True` if running in the iSH Alpine Linux iOS emulator. |
83
- | `is_android()` | Returns `True` on any Android-based Linux environment. |
78
+ | `on_windows()` | Returns `True` on Windows. |
79
+ | `on_apple()` | Returns `True` on macOS (Darwin). |
80
+ | `on_linux()` | Returns `True` on Linux in general. |
81
+ | `on_termux()` | Returns `True` if running in the Termux Android environment. |
82
+ | `on_freebsd()` | Returns `True` on FreeBSD. |
83
+ | `on_ish_alpine()` | Returns `True` if running in the iSH Alpine Linux iOS emulator. |
84
+ | `on_android()` | Returns `True` on any Android-based Linux environment. |
85
+ | `in_repl()` | Returns `True` is the user is currently in a Python REPL; hasattr(sys,'ps1'). |
84
86
 
85
87
  ### Packaging and Build Checking
86
88
 
87
- Key question: "What is the character of my executable?"
89
+ Key question: "What is the character of my executable or my build state?"
90
+
91
+ These functions accept an optional path argument (Path or str), defaulting to sys.argv[0] (e.g., pyhabitat/__main__.py for python -m pyhabitat, empty in REPL). Path.resolve() is used for stability.
88
92
 
89
93
  | Function | Description |
90
94
  | :--- | :--- |
91
- | `is_frozen()` | Returns `True` if the script is running as a standalone executable (any bundler). |
92
- | `is_pyinstaller()` | Returns `True` if the script is_frozen() and was generated by Pyinstaller (has MEI). |
93
- | `is_pipx()` | Returns `True` if running from a pipx managed virtual environment. |
94
- | `is_elf()` | Checks if the executable is an ELF binary (Linux standalone executable), excluding pipx. |
95
- | `is_windows_portable_executable()` | Checks if the executable is a Windows PE binary (MZ header), excluding pipx. |
96
- | `is_macos_executable()` | Checks if the executable is a macOS/Darwin Mach-O binary, excluding pipx. || `is_macos_executable()` | Checks if the executable is a macOS/Darwin Mach-O binary, excluding pipx. |
97
-
95
+ | `as_frozen()` | Returns `True` if the script is running as a standalone executable (any bundler). |
96
+ | `as_pyinstaller()` | Returns `True` if the script is frozen and generated by PyInstaller (has `_MEIPASS`). |
97
+ | `is_python_script(path=None)` | Returns `True` if the script or specified path is a Python source file (.py). |
98
+ | `is_pipx(path=None)` | Returns `True` if the script or specified path is from a pipx-managed virtual environment. |
99
+ | `is_elf(path=None)` | Returns `True` if the script or specified path is an ELF binary (Linux standalone executable, non-pipx). |
100
+ | `is_pyz(path=None)` | Returns `True` if the script or specified path is a Python zipapp (.pyz, non-pipx). |
101
+ | `is_windows_portable_executable(path=None)` | Returns `True` if the script or specified path is a Windows PE binary (MZ header, non-pipx). |
102
+ | `is_macos_executable(path=None)` | Returns `True` if the script or specified path is a macOS Mach-O binary (non-pipx). |
98
103
 
99
104
  ### Capability Checking
100
105
 
@@ -103,16 +108,18 @@ Key Question: "What could I do next?"
103
108
  | Function | Description |
104
109
  | :--- | :--- |
105
110
  | `tkinter_is_available()` | Checks if Tkinter is imported and can successfully create a window. |
106
- | `matplotlib_is_available_for_gui_plotting(termux_has_gui=False)` | Checks for Matplotlib and its TkAgg backend, required for interactive plotting. |
111
+ | `matplotlib_is_available_for_gui_plotting(termux_has_gui=False)` | Checks for Matplotlib and its TkAgg backend, required for interactive plotting. Set `termux_has_gui=True` for Termux with GUI support; defaults to `False`. |
107
112
  | `matplotlib_is_available_for_headless_image_export()` | Checks for Matplotlib and its Agg backend, required for saving images without a GUI. |
108
113
  | `interactive_terminal_is_available()` | Checks if standard input and output streams are connected to a TTY (allows safe use of interactive prompts). |
109
114
  | `web_browser_is_available()` | Check if a web browser can be launched in the current environment (allows safe use of web-based prompts and localhost plotting). |
110
115
 
111
- ### Actions
116
+ ### Utility
112
117
 
113
118
  | Function | Description |
114
119
  | :--- | :--- |
115
- | `edit_textfile()` | Smoothly opens a text file for editing (for configuration editing prompted by a CLI flag). |
120
+ | `edit_textfile(path)` | Opens a text file for editing using the default editor (Windows, Linux, macOS) or nano in Termux/iSH. In REPL mode, prints an error. Path argument (str or Path) uses Path.resolve() for stability. |
121
+ | `interp_path(print_path=False)` | Returns the path to the Python interpreter binary (sys.executable). Optionally prints the path. Returns empty string if unavailable. |
122
+ | `main()` | Prints a comprehensive environment report with sections: Interpreter Checks (sys.executable), Current Environment Check (sys.argv[0]), Current Build Checks (sys attributes), Operating System Checks (platform.system()), and Capability Checks. Run via `python -m pyhabitat` or `import pyhabitat; pyhabitat.main()` in the REPL. |
116
123
 
117
124
  </details>
118
125
 
@@ -123,39 +130,57 @@ Key Question: "What could I do next?"
123
130
 
124
131
  The module exposes all detection functions directly for easy access.
125
132
 
126
- ### 0\. Current Use
133
+ ### 0\.Example of PyHabitat in Action
127
134
 
128
135
  The `pipeline-eds` package uses the `pyhabitat` library to handle [configuration](https://github.com/City-of-Memphis-Wastewater/pipeline/blob/main/src/pipeline/security_and_config.py) and [plotting](https://github.com/City-of-Memphis-Wastewater/pipeline/blob/main/src/pipeline/cli.py), among other things.
129
136
 
130
- ### 1\. Checking Environment and Build Type
137
+ ### 1\. Running the Environment Report
138
+
139
+ Run a comprehensive environment report from the command line or REPL to inspect the interpreter (sys.executable), running script (sys.argv[0]), build state, operating system, and capabilities.
140
+
141
+ ```bash
142
+ # In the terminal
143
+ python -m pyhabitat
144
+ ```
131
145
 
132
146
  ```python
133
- from pyhabitat import is_termux, is_windows, is_pipx, is_frozen
147
+ # In the Python REPL
148
+ import pyhabitat as ph
149
+ ph.main()
150
+ ```
151
+
152
+ ### 2\. Checking Environment and Build Type
153
+
154
+ ```python
155
+ from pyhabitat import on_termux, on_windows, is_pipx, is_python_script, as_frozen
134
156
 
135
157
  if is_pipx():
136
158
  print("Running inside a pipx virtual environment. This is not a standalone binary.")
137
159
 
138
- elif is_frozen():
160
+ if as_frozen():
139
161
  print("Running as a frozen executable (PyInstaller, cx_Freeze, etc.).")
140
162
 
141
- elif is_termux():
163
+ if is_python_script():
164
+ print("Running as a Python source script (.py).")
165
+
166
+ if on_termux():
142
167
  # Expected cases:
143
168
  #- pkg install python-numpy python-cryptography
144
- #- Avoiding matplotlib unless the user explicitly confirms that termux_has_gui=False in matplotlib_is_available_for_gui_plotting(termux_has_gui=False).
169
+ #- Avoiding matplotlib unless the user explicitly sets termux_has_gui=True in matplotlib_is_available_for_gui_plotting().
145
170
  #- Auto-selection of 'termux-open-url' and 'xdg-open' in logic.
146
171
  #- Installation on the system, like orchestrating the construction of Termux Widget entries in ~/.shortcuts.
147
172
  print("Running in the Termux environment on Android.")
148
173
 
149
- elif is_windows():
174
+ if on_windows():
150
175
  print("Running on Windows.")
151
176
  ```
152
177
 
153
- ### 2\. Checking GUI and Plotting Availability
178
+ ### 3\. Checking GUI and Plotting Availability
154
179
 
155
180
  Use these functions to determine if you can show an interactive plot or if you must save an image file.
156
181
 
157
182
  ```python
158
- from pyhabitat import matplotlib_is_available_for_gui_plotting, matplotlib_is_available_for_headless_image_export,
183
+ from pyhabitat import matplotlib_is_available_for_gui_plotting, matplotlib_is_available_for_headless_image_export
159
184
 
160
185
  if matplotlib_is_available_for_gui_plotting():
161
186
  # We can safely call plt.show()
@@ -173,13 +198,16 @@ else:
173
198
  print("Matplotlib is not installed or the environment is too restrictive for plotting.")
174
199
  ```
175
200
 
176
- ### 3\. Text Editing
201
+ ### 4\. Text Editing
177
202
 
178
- Use this function to smoothly open a text file for editing.
203
+ Use this function to open a text file for editing.
179
204
  Ideal use case: Edit a configuration file, if prompted by a CLI command like 'config --textedit'.
180
205
 
181
206
  ```python
182
- edit_textfile(filepath=Path('./config.json'))
207
+ from pathlib import Path
208
+ import pyhabitat as ph
209
+
210
+ ph.edit_textfile(path=Path('./config.json'))
183
211
  ```
184
212
  </details>
185
213
 
@@ -44,11 +44,11 @@ Ultimately, [City-of-Memphis-Wastewater](https://github.com/City-of-Memphis-Wast
44
44
 
45
45
  * **Definitive Environment Checks:** Rigorous checks catered to Termux and iSH (iOS Alpine). Accurate, typical modern detection for Windows, macOS (Apple), Linux, FreeBSD, Android.
46
46
  * **GUI Availability:** Rigorous, cached checks to determine if the environment supports a graphical popup window (Tkinter/Matplotlib TkAgg) or just headless image export (Matplotlib Agg).
47
- * **Build/Packaging Detection:** Reliable detection of standalone executables built by tools like PyInstaller, and, crucially, correct identification and exclusion of pipx-managed virtual environments, which also user binaries that could conflate the check.
48
- * **Executable Type Inspection:** Uses file magic numbers (ELF and MZ) to confirm if the running script is a monolithic, frozen binary (non-pipx).
47
+ * **Build/Packaging Detection:** Reliable detection of standalone executables (PyInstaller), Python zipapps (.pyz), Python source scripts (.py), and correct identification/exclusion of pipx-managed virtual environments.
48
+ * **Executable Type Inspection:** Uses file magic numbers (ELF, MZ, Mach-O) to confirm if the running script is a monolithic, frozen binary (non-pipx) or zipapp (.pyz).
49
49
 
50
50
  </details>
51
-
51
+
52
52
  ---
53
53
 
54
54
  <details>
@@ -60,26 +60,31 @@ Key question: "What is this running on?"
60
60
 
61
61
  | Function | Description |
62
62
  | :--- | :--- |
63
- | `is_windows()` | Returns `True` on Windows. |
64
- | `is_apple()` | Returns `True` on macOS (Darwin). |
65
- | `is_linux()` | Returns `True` on Linux in general. |
66
- | `is_termux()` | Returns `True` if running in the Termux Android environment. |
67
- | `is_ish_alpine()` | Returns `True` if running in the iSH Alpine Linux iOS emulator. |
68
- | `is_android()` | Returns `True` on any Android-based Linux environment. |
63
+ | `on_windows()` | Returns `True` on Windows. |
64
+ | `on_apple()` | Returns `True` on macOS (Darwin). |
65
+ | `on_linux()` | Returns `True` on Linux in general. |
66
+ | `on_termux()` | Returns `True` if running in the Termux Android environment. |
67
+ | `on_freebsd()` | Returns `True` on FreeBSD. |
68
+ | `on_ish_alpine()` | Returns `True` if running in the iSH Alpine Linux iOS emulator. |
69
+ | `on_android()` | Returns `True` on any Android-based Linux environment. |
70
+ | `in_repl()` | Returns `True` is the user is currently in a Python REPL; hasattr(sys,'ps1'). |
69
71
 
70
72
  ### Packaging and Build Checking
71
73
 
72
- Key question: "What is the character of my executable?"
74
+ Key question: "What is the character of my executable or my build state?"
75
+
76
+ These functions accept an optional path argument (Path or str), defaulting to sys.argv[0] (e.g., pyhabitat/__main__.py for python -m pyhabitat, empty in REPL). Path.resolve() is used for stability.
73
77
 
74
78
  | Function | Description |
75
79
  | :--- | :--- |
76
- | `is_frozen()` | Returns `True` if the script is running as a standalone executable (any bundler). |
77
- | `is_pyinstaller()` | Returns `True` if the script is_frozen() and was generated by Pyinstaller (has MEI). |
78
- | `is_pipx()` | Returns `True` if running from a pipx managed virtual environment. |
79
- | `is_elf()` | Checks if the executable is an ELF binary (Linux standalone executable), excluding pipx. |
80
- | `is_windows_portable_executable()` | Checks if the executable is a Windows PE binary (MZ header), excluding pipx. |
81
- | `is_macos_executable()` | Checks if the executable is a macOS/Darwin Mach-O binary, excluding pipx. || `is_macos_executable()` | Checks if the executable is a macOS/Darwin Mach-O binary, excluding pipx. |
82
-
80
+ | `as_frozen()` | Returns `True` if the script is running as a standalone executable (any bundler). |
81
+ | `as_pyinstaller()` | Returns `True` if the script is frozen and generated by PyInstaller (has `_MEIPASS`). |
82
+ | `is_python_script(path=None)` | Returns `True` if the script or specified path is a Python source file (.py). |
83
+ | `is_pipx(path=None)` | Returns `True` if the script or specified path is from a pipx-managed virtual environment. |
84
+ | `is_elf(path=None)` | Returns `True` if the script or specified path is an ELF binary (Linux standalone executable, non-pipx). |
85
+ | `is_pyz(path=None)` | Returns `True` if the script or specified path is a Python zipapp (.pyz, non-pipx). |
86
+ | `is_windows_portable_executable(path=None)` | Returns `True` if the script or specified path is a Windows PE binary (MZ header, non-pipx). |
87
+ | `is_macos_executable(path=None)` | Returns `True` if the script or specified path is a macOS Mach-O binary (non-pipx). |
83
88
 
84
89
  ### Capability Checking
85
90
 
@@ -88,16 +93,18 @@ Key Question: "What could I do next?"
88
93
  | Function | Description |
89
94
  | :--- | :--- |
90
95
  | `tkinter_is_available()` | Checks if Tkinter is imported and can successfully create a window. |
91
- | `matplotlib_is_available_for_gui_plotting(termux_has_gui=False)` | Checks for Matplotlib and its TkAgg backend, required for interactive plotting. |
96
+ | `matplotlib_is_available_for_gui_plotting(termux_has_gui=False)` | Checks for Matplotlib and its TkAgg backend, required for interactive plotting. Set `termux_has_gui=True` for Termux with GUI support; defaults to `False`. |
92
97
  | `matplotlib_is_available_for_headless_image_export()` | Checks for Matplotlib and its Agg backend, required for saving images without a GUI. |
93
98
  | `interactive_terminal_is_available()` | Checks if standard input and output streams are connected to a TTY (allows safe use of interactive prompts). |
94
99
  | `web_browser_is_available()` | Check if a web browser can be launched in the current environment (allows safe use of web-based prompts and localhost plotting). |
95
100
 
96
- ### Actions
101
+ ### Utility
97
102
 
98
103
  | Function | Description |
99
104
  | :--- | :--- |
100
- | `edit_textfile()` | Smoothly opens a text file for editing (for configuration editing prompted by a CLI flag). |
105
+ | `edit_textfile(path)` | Opens a text file for editing using the default editor (Windows, Linux, macOS) or nano in Termux/iSH. In REPL mode, prints an error. Path argument (str or Path) uses Path.resolve() for stability. |
106
+ | `interp_path(print_path=False)` | Returns the path to the Python interpreter binary (sys.executable). Optionally prints the path. Returns empty string if unavailable. |
107
+ | `main()` | Prints a comprehensive environment report with sections: Interpreter Checks (sys.executable), Current Environment Check (sys.argv[0]), Current Build Checks (sys attributes), Operating System Checks (platform.system()), and Capability Checks. Run via `python -m pyhabitat` or `import pyhabitat; pyhabitat.main()` in the REPL. |
101
108
 
102
109
  </details>
103
110
 
@@ -108,39 +115,57 @@ Key Question: "What could I do next?"
108
115
 
109
116
  The module exposes all detection functions directly for easy access.
110
117
 
111
- ### 0\. Current Use
118
+ ### 0\.Example of PyHabitat in Action
112
119
 
113
120
  The `pipeline-eds` package uses the `pyhabitat` library to handle [configuration](https://github.com/City-of-Memphis-Wastewater/pipeline/blob/main/src/pipeline/security_and_config.py) and [plotting](https://github.com/City-of-Memphis-Wastewater/pipeline/blob/main/src/pipeline/cli.py), among other things.
114
121
 
115
- ### 1\. Checking Environment and Build Type
122
+ ### 1\. Running the Environment Report
123
+
124
+ Run a comprehensive environment report from the command line or REPL to inspect the interpreter (sys.executable), running script (sys.argv[0]), build state, operating system, and capabilities.
125
+
126
+ ```bash
127
+ # In the terminal
128
+ python -m pyhabitat
129
+ ```
116
130
 
117
131
  ```python
118
- from pyhabitat import is_termux, is_windows, is_pipx, is_frozen
132
+ # In the Python REPL
133
+ import pyhabitat as ph
134
+ ph.main()
135
+ ```
136
+
137
+ ### 2\. Checking Environment and Build Type
138
+
139
+ ```python
140
+ from pyhabitat import on_termux, on_windows, is_pipx, is_python_script, as_frozen
119
141
 
120
142
  if is_pipx():
121
143
  print("Running inside a pipx virtual environment. This is not a standalone binary.")
122
144
 
123
- elif is_frozen():
145
+ if as_frozen():
124
146
  print("Running as a frozen executable (PyInstaller, cx_Freeze, etc.).")
125
147
 
126
- elif is_termux():
148
+ if is_python_script():
149
+ print("Running as a Python source script (.py).")
150
+
151
+ if on_termux():
127
152
  # Expected cases:
128
153
  #- pkg install python-numpy python-cryptography
129
- #- Avoiding matplotlib unless the user explicitly confirms that termux_has_gui=False in matplotlib_is_available_for_gui_plotting(termux_has_gui=False).
154
+ #- Avoiding matplotlib unless the user explicitly sets termux_has_gui=True in matplotlib_is_available_for_gui_plotting().
130
155
  #- Auto-selection of 'termux-open-url' and 'xdg-open' in logic.
131
156
  #- Installation on the system, like orchestrating the construction of Termux Widget entries in ~/.shortcuts.
132
157
  print("Running in the Termux environment on Android.")
133
158
 
134
- elif is_windows():
159
+ if on_windows():
135
160
  print("Running on Windows.")
136
161
  ```
137
162
 
138
- ### 2\. Checking GUI and Plotting Availability
163
+ ### 3\. Checking GUI and Plotting Availability
139
164
 
140
165
  Use these functions to determine if you can show an interactive plot or if you must save an image file.
141
166
 
142
167
  ```python
143
- from pyhabitat import matplotlib_is_available_for_gui_plotting, matplotlib_is_available_for_headless_image_export,
168
+ from pyhabitat import matplotlib_is_available_for_gui_plotting, matplotlib_is_available_for_headless_image_export
144
169
 
145
170
  if matplotlib_is_available_for_gui_plotting():
146
171
  # We can safely call plt.show()
@@ -158,13 +183,16 @@ else:
158
183
  print("Matplotlib is not installed or the environment is too restrictive for plotting.")
159
184
  ```
160
185
 
161
- ### 3\. Text Editing
186
+ ### 4\. Text Editing
162
187
 
163
- Use this function to smoothly open a text file for editing.
188
+ Use this function to open a text file for editing.
164
189
  Ideal use case: Edit a configuration file, if prompted by a CLI command like 'config --textedit'.
165
190
 
166
191
  ```python
167
- edit_textfile(filepath=Path('./config.json'))
192
+ from pathlib import Path
193
+ import pyhabitat as ph
194
+
195
+ ph.edit_textfile(path=Path('./config.json'))
168
196
  ```
169
197
  </details>
170
198
 
@@ -4,45 +4,54 @@ from .environment import (
4
4
  matplotlib_is_available_for_gui_plotting,
5
5
  matplotlib_is_available_for_headless_image_export,
6
6
  tkinter_is_available,
7
- is_termux,
8
- is_freebsd,
9
- is_linux,
10
- is_android,
11
- is_windows,
12
- is_apple,
13
- is_ish_alpine,
14
- is_pyinstaller,
15
- is_frozen,
7
+ in_repl,
8
+ on_termux,
9
+ on_freebsd,
10
+ on_linux,
11
+ on_android,
12
+ on_windows,
13
+ on_apple,
14
+ on_ish_alpine,
15
+ as_pyinstaller,
16
+ as_frozen,
16
17
  is_elf,
17
18
  is_pyz,
18
19
  is_windows_portable_executable,
19
20
  is_macos_executable,
20
21
  is_pipx,
22
+ is_python_script,
21
23
  interactive_terminal_is_available,
22
24
  web_browser_is_available,
23
25
  edit_textfile,
26
+ interp_path,
24
27
  )
25
28
 
29
+ from .__main__ import main
30
+
26
31
  # Optional: Set __all__ for explicit documentation and cleaner imports
27
32
  __all__ = [
28
33
  'matplotlib_is_available_for_gui_plotting',
29
34
  'matplotlib_is_available_for_headless_image_export',
30
35
  'tkinter_is_available',
31
- 'is_termux',
32
- 'is_freebsd',
33
- 'is_linux',
34
- 'is_android',
35
- 'is_windows',
36
- 'is_apple',
37
- 'is_ish_alpine',
38
- 'is_pyinstaller',
39
- 'is_frozen',
36
+ 'in_repl',
37
+ 'on_termux',
38
+ 'on_freebsd',
39
+ 'on_linux',
40
+ 'on_android',
41
+ 'on_windows',
42
+ 'on_apple',
43
+ 'on_ish_alpine',
44
+ 'as_pyinstaller',
45
+ 'as_frozen',
40
46
  'is_elf',
41
47
  'is_pyz',
42
48
  'is_windows_portable_executable',
43
49
  'is_macos_executable',
44
50
  'is_pipx',
51
+ 'is_python_script',
45
52
  'interactive_terminal_is_available',
46
53
  'web_browser_is_available',
47
54
  'edit_textfile',
55
+ 'interp_path',
56
+ 'main',
48
57
  ]
@@ -0,0 +1,69 @@
1
+ from .environment import (
2
+ in_repl,
3
+ on_termux,
4
+ on_windows,
5
+ on_apple,
6
+ on_linux,
7
+ on_ish_alpine,
8
+ on_android,
9
+ on_freebsd,
10
+ is_elf,
11
+ is_windows_portable_executable,
12
+ is_macos_executable,
13
+ is_pyz,
14
+ is_pipx,
15
+ is_python_script,
16
+ as_frozen,
17
+ as_pyinstaller,
18
+ interp_path,
19
+ tkinter_is_available,
20
+ matplotlib_is_available_for_gui_plotting,
21
+ matplotlib_is_available_for_headless_image_export,
22
+ web_browser_is_available,
23
+ interactive_terminal_is_available
24
+ )
25
+
26
+ def main():
27
+ print("PyHabitat Environment Report")
28
+ print("===========================")
29
+ print("\nInterpreter Checks // Based on sys.executable()")
30
+ print("-----------------------------")
31
+ print(f"interp_path(): {interp_path()}")
32
+ print(f"is_elf(interp_path()): {is_elf(interp_path())}")
33
+ print(f"is_windows_portable_executable(interp_path()): {is_windows_portable_executable(interp_path())}")
34
+ print(f"is_macos_executable(interp_path()): {is_macos_executable(interp_path())}")
35
+ print(f"is_pyz(interp_path()): {is_pyz(interp_path())}")
36
+ print(f"is_pipx(interp_path()): {is_pipx(interp_path())}")
37
+ print(f"is_python_script(interp_path()): {is_python_script(interp_path())}")
38
+ print("\nCurrent Environment Check // Based on sys.argv[0]")
39
+ print("-----------------------------")
40
+ print(f"is_elf(): {is_elf()}")
41
+ print(f"is_windows_portable_executable(): {is_windows_portable_executable()}")
42
+ print(f"is_macos_executable(): {is_macos_executable()}")
43
+ print(f"is_pyz(): {is_pyz()}")
44
+ print(f"is_pipx(): {is_pipx()}")
45
+ print(f"is_python_script(): {is_python_script()}")
46
+ print(f"\nCurrent Build Checks // Based on hasattr(sys,..) and getattr(sys,..)")
47
+ print("------------------------------")
48
+ print(f"in_repl(): {in_repl()}")
49
+ print(f"as_frozen(): {as_frozen()}")
50
+ print(f"as_pyinstaller(): {as_pyinstaller()}")
51
+ print("\nOperating System Checks // Based on platform.system()")
52
+ print("------------------------------")
53
+ print(f"on_termux(): {on_termux()}")
54
+ print(f"on_windows(): {on_windows()}")
55
+ print(f"on_apple(): {on_apple()}")
56
+ print(f"on_linux(): {on_linux()}")
57
+ print(f"on_ish_alpine(): {on_ish_alpine()}")
58
+ print(f"on_android(): {on_android()}")
59
+ print(f"on_freebsd(): {on_freebsd()}")
60
+ print("\nCapability Checks")
61
+ print("-------------------------")
62
+ print(f"tkinter_is_available(): {tkinter_is_available()}")
63
+ print(f"matplotlib_is_available_for_gui_plotting(): {matplotlib_is_available_for_gui_plotting()}")
64
+ print(f"matplotlib_is_available_for_headless_image_export(): {matplotlib_is_available_for_headless_image_export()}")
65
+ print(f"web_browser_is_available(): {web_browser_is_available()}")
66
+ print(f"interactive_terminal_is_available(): {interactive_terminal_is_available()}")
67
+
68
+ if __name__ == "__main__":
69
+ main()
@@ -14,6 +14,31 @@ import subprocess
14
14
  import io
15
15
  import zipfile
16
16
 
17
+ __all__ = [
18
+ 'matplotlib_is_available_for_gui_plotting',
19
+ 'matplotlib_is_available_for_headless_image_export',
20
+ 'tkinter_is_available',
21
+ 'on_termux',
22
+ 'on_freebsd',
23
+ 'on_linux',
24
+ 'on_android',
25
+ 'on_windows',
26
+ 'on_apple',
27
+ 'on_ish_alpine',
28
+ 'as_pyinstaller',
29
+ 'as_frozen',
30
+ 'is_elf',
31
+ 'is_pyz',
32
+ 'is_windows_portable_executable',
33
+ 'is_macos_executable',
34
+ 'is_pipx',
35
+ 'interactive_terminal_is_available',
36
+ 'web_browser_is_available',
37
+ 'edit_textfile',
38
+ 'in_repl',
39
+ 'interp_path',
40
+ ]
41
+
17
42
  # Global cache for tkinter and matplotlib (mpl) availability
18
43
  _TKINTER_AVAILABILITY: bool | None = None
19
44
  _MATPLOTLIB_EXPORT_AVAILABILITY: bool | None = None
@@ -29,7 +54,7 @@ def matplotlib_is_available_for_gui_plotting(termux_has_gui=False):
29
54
 
30
55
  # 1. Termux exclusion check (assume no X11/GUI)
31
56
  # Exclude Termux UNLESS the user explicitly provides termux_has_gui=True.
32
- if is_termux() and not termux_has_gui:
57
+ if on_termux() and not termux_has_gui:
33
58
  _MATPLOTLIB_WINDOWED_AVAILABILITY = False
34
59
  return False
35
60
 
@@ -118,7 +143,7 @@ def tkinter_is_available() -> bool:
118
143
  return False
119
144
 
120
145
  # --- ENVIRONMENT AND OPERATING SYSTEM CHECKS ---
121
- def is_termux() -> bool:
146
+ def on_termux() -> bool:
122
147
  """Detect if running in Termux environment on Android, based on Termux-specific environmental variables."""
123
148
 
124
149
  if platform.system() != 'Linux':
@@ -145,22 +170,22 @@ def is_termux() -> bool:
145
170
 
146
171
  return False
147
172
 
148
- def is_freebsd() -> bool:
173
+ def on_freebsd() -> bool:
149
174
  """Detect if running on FreeBSD."""
150
175
  return platform.system() == 'FreeBSD'
151
176
 
152
- def is_linux():
177
+ def on_linux():
153
178
  """Detect if running on Linux."""
154
179
  return platform.system() == 'Linux'
155
180
 
156
- def is_android() -> bool:
181
+ def on_android() -> bool:
157
182
  """
158
183
  Detect if running on Android.
159
184
 
160
- Note: The is_termux() function is more robust and safe for Termux.
161
- Checking for Termux with is_termux() does not require checking for Android with is_android().
185
+ Note: The on_termux() function is more robust and safe for Termux.
186
+ Checking for Termux with on_termux() does not require checking for Android with on_android().
162
187
 
163
- is_android() will be True on:
188
+ on_android() will be True on:
164
189
  - Sandboxed IDE's:
165
190
  - Pydroid3
166
191
  - QPython
@@ -170,7 +195,7 @@ def is_android() -> bool:
170
195
  - UserLand
171
196
  - AnLinux
172
197
 
173
- is_android() will be False on:
198
+ on_android() will be False on:
174
199
  - Full Virtual Machines:
175
200
  - VirtualBox
176
201
  - VMware
@@ -181,15 +206,15 @@ def is_android() -> bool:
181
206
  return False
182
207
  return "android" in platform.platform().lower()
183
208
 
184
- def is_windows() -> bool:
209
+ def on_windows() -> bool:
185
210
  """Detect if running on Windows."""
186
211
  return platform.system() == 'Windows'
187
212
 
188
- def is_apple() -> bool:
213
+ def on_apple() -> bool:
189
214
  """Detect if running on Apple."""
190
215
  return platform.system() == 'Darwin'
191
216
 
192
- def is_ish_alpine() -> bool:
217
+ def on_ish_alpine() -> bool:
193
218
  """Detect if running in iSH Alpine environment on iOS."""
194
219
  # platform.system() usually returns 'Linux' in iSH
195
220
 
@@ -208,16 +233,29 @@ def is_ish_alpine() -> bool:
208
233
 
209
234
  return False
210
235
 
236
+ def in_repl() -> bool:
237
+ """
238
+ Detects if the code is running in the Python interactive REPL (e.g., when 'python' is typed in a console).
239
+
240
+ This function specifically checks for the Python REPL by verifying the presence of the interactive
241
+ prompt (`sys.ps1`). It returns False for other interactive terminal scenarios, such as running a
242
+ PyInstaller binary in a console.
243
+
244
+ Returns:
245
+ bool: True if running in the Python REPL; False otherwise.
246
+ """
247
+ return hasattr(sys, 'ps1')
248
+
211
249
 
212
250
  # --- BUILD AND EXECUTABLE CHECKS ---
213
251
 
214
- def is_pyinstaller():
252
+ def as_pyinstaller():
215
253
  """Detects if the Python script is running as a 'frozen' in the course of generating a PyInstaller binary executable."""
216
254
  # If the app is frozen AND has the PyInstaller-specific temporary folder path
217
- return is_frozen() and hasattr(sys, '_MEIPASS')
255
+ return as_frozen() and hasattr(sys, '_MEIPASS')
218
256
 
219
257
  # The standard way to check for a frozen state:
220
- def is_frozen():
258
+ def as_frozen():
221
259
  """
222
260
  Detects if the Python script is running as a 'frozen' (standalone)
223
261
  executable created by a tool like PyInstaller, cx_Freeze, or Nuitka.
@@ -235,21 +273,26 @@ def is_frozen():
235
273
  """
236
274
  return getattr(sys, 'frozen', False)
237
275
 
238
- def is_elf(exec_path : Path = None, debug=False) -> bool:
276
+ # --- Binary Characteristic Checks ---
277
+ def is_elf(exec_path: Path | str | None = None, debug: bool = False) -> bool:
239
278
  """Checks if the currently running executable (sys.argv[0]) is a standalone PyInstaller-built ELF binary."""
240
279
  # If it's a pipx installation, it is not the monolithic binary we are concerned with here.
241
280
 
242
281
  if exec_path is None:
243
282
  exec_path = Path(sys.argv[0]).resolve()
283
+ else:
284
+ exec_path = Path(exec_path).resolve()
285
+
244
286
  if debug:
245
- print(f"exec_path = {exec_path}")
287
+ print(f"DEBUG: Checking executable path: {exec_path}")
288
+
246
289
  if is_pipx():
247
290
  return False
248
291
 
249
292
  # Check if the file exists and is readable
250
293
  if not exec_path.is_file():
251
- return False
252
-
294
+ if debug: print("DEBUG:False (Not a file)")
295
+ return False
253
296
  try:
254
297
  # Check the magic number: The first four bytes of an ELF file are 0x7f, 'E', 'L', 'F' (b'\x7fELF').
255
298
  # This is the most reliable way to determine if the executable is a native binary wrapper (like PyInstaller's).
@@ -261,13 +304,20 @@ def is_elf(exec_path : Path = None, debug=False) -> bool:
261
304
  # Handle exceptions like PermissionError, IsADirectoryError, etc.
262
305
  return False
263
306
 
264
- def is_pyz(exec_path: Path=None, debug=False) -> bool:
307
+ def is_pyz(exec_path: Path | str | None = None, debug: bool = False) -> bool:
265
308
  """Checks if the currently running executable (sys.argv[0]) is a PYZ zipapp ."""
266
309
  # If it's a pipx installation, it is not the monolithic binary we are concerned with here.
267
310
  if exec_path is None:
268
311
  exec_path = Path(sys.argv[0]).resolve()
312
+ else:
313
+ exec_path = Path(exec_path).resolve()
314
+
269
315
  if debug:
270
- print(f"exec_path = {exec_path}")
316
+ print(f"DEBUG: Checking executable path: {exec_path}")
317
+
318
+ if not exec_path.is_file():
319
+ if debug: print("DEBUG:False (Not a file)")
320
+ return False
271
321
 
272
322
  if is_pipx():
273
323
  return False
@@ -276,10 +326,10 @@ def is_pyz(exec_path: Path=None, debug=False) -> bool:
276
326
  if not str(exec_path).endswith(".pyz"):
277
327
  return False
278
328
 
279
- if not _check_if_zip():
329
+ if not _check_if_zip(exec_path):
280
330
  return False
281
331
 
282
- def is_windows_portable_executable(exec_path: Path = None, debug=False) -> bool:
332
+ def is_windows_portable_executable(exec_path: Path | str | None = None, debug: bool = False) -> bool:
283
333
  """
284
334
  Checks if the currently running executable (sys.argv[0]) is a
285
335
  Windows Portable Executable (PE) binary, and explicitly excludes
@@ -290,18 +340,20 @@ def is_windows_portable_executable(exec_path: Path = None, debug=False) -> bool:
290
340
  # 1. Determine execution path
291
341
  if exec_path is None:
292
342
  exec_path = Path(sys.argv[0]).resolve()
343
+ else:
344
+ exec_path = Path(exec_path).resolve()
293
345
 
294
346
  if debug:
295
347
  print(f"DEBUG: Checking executable path: {exec_path}")
296
348
 
297
349
  # 2. Exclude pipx environments immediately
298
350
  if is_pipx():
299
- if debug: print("DEBUG: is_exe_non_pipx: False (is_pipx is True)")
351
+ if debug: print("DEBUG: False (is_pipx is True)")
300
352
  return False
301
353
 
302
354
  # 3. Perform file checks
303
355
  if not exec_path.is_file():
304
- if debug: print("DEBUG: is_exe_non_pipx: False (Not a file)")
356
+ if debug: print("DEBUG:False (Not a file)")
305
357
  return False
306
358
 
307
359
  try:
@@ -314,30 +366,35 @@ def is_windows_portable_executable(exec_path: Path = None, debug=False) -> bool:
314
366
 
315
367
  if debug:
316
368
  print(f"DEBUG: Magic bytes: {magic_bytes}")
317
- print(f"DEBUG: is_exe_non_pipx: {is_pe} (Non-pipx check)")
369
+ print(f"DEBUG: {is_pe} (Non-pipx check)")
318
370
 
319
371
  return is_pe
320
372
 
321
373
  except Exception as e:
322
- if debug: print(f"DEBUG: is_exe_non_pipx: Error during file check: {e}")
374
+ if debug: print(f"DEBUG: Error during file check: {e}")
323
375
  # Handle exceptions like PermissionError, IsADirectoryError, etc.
324
376
  return False
325
377
 
326
- def is_macos_executable(exec_path: Path = None, debug=False) -> bool:
378
+ def is_macos_executable(exec_path: Path | str | None = None, debug: bool = False) -> bool:
327
379
  """
328
380
  Checks if the currently running executable is a macOS/Darwin Mach-O binary,
329
381
  and explicitly excludes pipx-managed environments.
330
382
  """
331
383
  if exec_path is None:
332
384
  exec_path = Path(sys.argv[0]).resolve()
333
-
385
+ else:
386
+ exec_path = Path(exec_path).resolve()
387
+ if debug:
388
+ print(f"DEBUG: Checking executable path: {exec_path}")
389
+
334
390
  if is_pipx():
335
391
  if debug: print("DEBUG: is_macos_executable: False (is_pipx is True)")
336
392
  return False
337
393
 
338
394
  if not exec_path.is_file():
395
+ if debug: print("DEBUG:False (Not a file)")
339
396
  return False
340
-
397
+
341
398
  try:
342
399
  # Check the magic number: Mach-O binaries start with specific 4-byte headers.
343
400
  # Common ones are: b'\xfe\xed\xfa\xce' (32-bit) or b'\xfe\xed\xfa\xcf' (64-bit)
@@ -362,15 +419,23 @@ def is_macos_executable(exec_path: Path = None, debug=False) -> bool:
362
419
  except Exception:
363
420
  return False
364
421
 
365
- def is_pipx(debug=False) -> bool:
422
+ def is_pipx(exec_path: Path | str | None = None, debug: bool = False) -> bool:
366
423
  """Checks if the executable is running from a pipx managed environment."""
424
+ if exec_path is None:
425
+ exec_path = Path(sys.argv[0]).resolve()
426
+ else:
427
+ exec_path = Path(exec_path).resolve()
428
+ if debug:
429
+ print(f"DEBUG: Checking executable path: {exec_path}")
430
+
431
+ if not exec_path.is_file():
432
+ if debug: print("DEBUG:False (Not a file)")
433
+ return False
367
434
  try:
368
435
  # Helper for case-insensitivity on Windows
369
436
  def normalize_path(p: Path) -> str:
370
437
  return str(p).lower()
371
438
 
372
- exec_path = Path(sys.argv[0]).resolve()
373
-
374
439
  # This is the path to the interpreter running the script (e.g., venv/bin/python)
375
440
  # In a pipx-managed execution, this is the venv python.
376
441
  interpreter_path = Path(sys.executable).resolve()
@@ -413,10 +478,54 @@ def is_pipx(debug=False) -> bool:
413
478
  except Exception:
414
479
  # Fallback for unexpected path errors
415
480
  return False
416
-
417
481
 
482
+ def is_python_script(path: Path | str | None = None, debug: bool = False) -> bool:
483
+ """
484
+ Checks if the specified path or running script is a Python source file (.py).
485
+
486
+ By default, checks the running script (`sys.argv[0]`). If a specific `path` is
487
+ provided, checks that path instead. Uses `Path.resolve()` for stable path handling.
488
+
489
+ Args:
490
+ path: Optional; path to the file to check (str or Path). If None, defaults to `sys.argv[0]`.
491
+ debug: If True, prints the path being checked.
492
+
493
+ Returns:
494
+ bool: True if the specified or default path is a Python source file (.py); False otherwise.
495
+ """
496
+ if path is None:
497
+ exec_path = Path(sys.argv[0]).resolve()
498
+ else:
499
+ exec_path = Path(path).resolve()
500
+ if debug:
501
+ print(f"Checking Python script for path: {exec_path}")
502
+ if not exec_path.is_file():
503
+ return False
504
+ return exec_path.suffix.lower() == '.py'
505
+
506
+ # --- Interpreter Check ---
507
+
508
+ def interp_path(print_path: bool = False) -> str:
509
+ """
510
+ Returns the path to the Python interpreter binary and optionally prints it.
511
+
512
+ This function wraps `sys.executable` to provide the path to the interpreter
513
+ (e.g., '/data/data/com.termux/files/usr/bin/python3' in Termux or the embedded
514
+ interpreter in a frozen executable). If the path is empty (e.g., in some embedded
515
+ or sandboxed environments), an empty string is returned.
516
+
517
+ Args:
518
+ print_path: If True, prints the interpreter path to stdout.
519
+
520
+ Returns:
521
+ str: The path to the Python interpreter binary, or an empty string if unavailable.
522
+ """
523
+ path = sys.executable
524
+ if print_path:
525
+ print(f"Python interpreter path: {path}")
526
+ return path
418
527
 
419
- # --- TTY CHECK ---
528
+ # --- TTY Check ---
420
529
  def interactive_terminal_is_available():
421
530
  """
422
531
  Check if the script is running in an interactive terminal.
@@ -440,7 +549,7 @@ def web_browser_is_available() -> bool:
440
549
  except webbrowser.Error:
441
550
  # Fallback needed. Check for external launchers.
442
551
  # 2. Termux specific check
443
- if is_termux() and shutil.which("termux-open-url"):
552
+ if on_termux() and shutil.which("termux-open-url"):
444
553
  return True
445
554
  # 3. General Linux check
446
555
  if shutil.which("xdg-open"):
@@ -448,34 +557,42 @@ def web_browser_is_available() -> bool:
448
557
  return False
449
558
 
450
559
  # --- LAUNCH MECHANISMS BASED ON ENVIRONMENT ---
451
- def edit_textfile(filepath) -> None:
452
- #def open_text_file_for_editing(filepath):
560
+ def edit_textfile(path: Path | str | None = None) -> None:
561
+ #def open_text_file_for_editing(path): # defunct function name as of 1.0.16
453
562
  """
454
563
  Opens a file with the environment's default application (Windows, Linux, macOS)
455
564
  or a guaranteed console editor (nano) in constrained environments (Termux, iSH)
456
565
  after ensuring line-ending compatibility.
566
+
567
+ This function is known to fail on PyDroid3, where on_linus() is True but xdg-open
568
+ is not available.
457
569
  """
570
+ if path is None:
571
+ return
572
+
573
+ path = Path(path).resolve()
574
+
458
575
  try:
459
- if is_windows():
460
- os.startfile(filepath)
461
- elif is_termux():
576
+ if on_windows():
577
+ os.startfile(path)
578
+ elif on_termux():
462
579
  # Install dependencies if missing (Termux pkg returns non-zero if already installed, so no check=True)
463
580
  subprocess.run(['pkg','install', 'dos2unix', 'nano'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
464
- _run_dos2unix(filepath)
465
- subprocess.run(['nano', filepath])
466
- elif is_ish_alpine():
581
+ _run_dos2unix(path)
582
+ subprocess.run(['nano', path])
583
+ elif on_ish_alpine():
467
584
  # Install dependencies if missing (apk returns 0 if already installed, so check=True is safe)
468
585
  subprocess.run(['apk','add', 'dos2unix'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=True)
469
586
  subprocess.run(['apk','add', 'nano'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=True)
470
- _run_dos2unix(filepath)
471
- subprocess.run(['nano', filepath])
587
+ _run_dos2unix(path)
588
+ subprocess.run(['nano', path])
472
589
  # --- Standard Unix-like Systems (Conversion + Default App) ---
473
- elif is_linux():
474
- _run_dos2unix(filepath) # Safety conversion for user-defined console apps
475
- subprocess.run(['xdg-open', filepath])
476
- elif is_apple():
477
- _run_dos2unix(filepath) # Safety conversion for user-defined console apps
478
- subprocess.run(['open', filepath])
590
+ elif on_linux():
591
+ _run_dos2unix(path) # Safety conversion for user-defined console apps
592
+ subprocess.run(['xdg-open', path])
593
+ elif on_apple():
594
+ _run_dos2unix(path) # Safety conversion for user-defined console apps
595
+ subprocess.run(['open', path])
479
596
  else:
480
597
  print("Unsupported operating system.")
481
598
  except Exception as e:
@@ -485,13 +602,16 @@ def edit_textfile(filepath) -> None:
485
602
  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.
486
603
  """
487
604
 
488
- def _run_dos2unix(filepath):
605
+ def _run_dos2unix(path: Path | str | None = None):
489
606
  """Attempt to run dos2unix, failing silently if not installed."""
607
+
608
+ path = Path(path).resolve()
609
+
490
610
  try:
491
611
  # We rely on shutil.which not being needed, as this is a robust built-in utility on most targets
492
612
  # The command won't raise an exception unless the process itself fails, not just if the utility isn't found.
493
613
  # We also don't use check=True here to allow silent failure if the utility is missing (e.g., minimalist Linux).
494
- subprocess.run(['dos2unix', filepath], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
614
+ subprocess.run(['dos2unix', path], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
495
615
  except FileNotFoundError:
496
616
  # This will be raised if 'dos2unix' is not on the system PATH
497
617
  pass
@@ -526,10 +646,14 @@ def _get_pipx_paths():
526
646
  return pipx_bin_path, pipx_venv_base.resolve()
527
647
 
528
648
 
529
- def _check_if_zip(file_path: str | Path) -> bool:
649
+ def _check_if_zip(path: Path | str | None) -> bool:
530
650
  """Checks if the file at the given path is a valid ZIP archive."""
651
+ if path is None:
652
+ return False
653
+ path = Path(path).resolve()
654
+
531
655
  try:
532
- return zipfile.is_zipfile(file_path)
656
+ return zipfile.is_zipfile(path)
533
657
  except Exception:
534
658
  # Handle cases where the path might be invalid, or other unexpected errors
535
659
  return False
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyhabitat
3
- Version: 1.0.16
3
+ Version: 1.0.17
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
@@ -59,11 +59,11 @@ Ultimately, [City-of-Memphis-Wastewater](https://github.com/City-of-Memphis-Wast
59
59
 
60
60
  * **Definitive Environment Checks:** Rigorous checks catered to Termux and iSH (iOS Alpine). Accurate, typical modern detection for Windows, macOS (Apple), Linux, FreeBSD, Android.
61
61
  * **GUI Availability:** Rigorous, cached checks to determine if the environment supports a graphical popup window (Tkinter/Matplotlib TkAgg) or just headless image export (Matplotlib Agg).
62
- * **Build/Packaging Detection:** Reliable detection of standalone executables built by tools like PyInstaller, and, crucially, correct identification and exclusion of pipx-managed virtual environments, which also user binaries that could conflate the check.
63
- * **Executable Type Inspection:** Uses file magic numbers (ELF and MZ) to confirm if the running script is a monolithic, frozen binary (non-pipx).
62
+ * **Build/Packaging Detection:** Reliable detection of standalone executables (PyInstaller), Python zipapps (.pyz), Python source scripts (.py), and correct identification/exclusion of pipx-managed virtual environments.
63
+ * **Executable Type Inspection:** Uses file magic numbers (ELF, MZ, Mach-O) to confirm if the running script is a monolithic, frozen binary (non-pipx) or zipapp (.pyz).
64
64
 
65
65
  </details>
66
-
66
+
67
67
  ---
68
68
 
69
69
  <details>
@@ -75,26 +75,31 @@ Key question: "What is this running on?"
75
75
 
76
76
  | Function | Description |
77
77
  | :--- | :--- |
78
- | `is_windows()` | Returns `True` on Windows. |
79
- | `is_apple()` | Returns `True` on macOS (Darwin). |
80
- | `is_linux()` | Returns `True` on Linux in general. |
81
- | `is_termux()` | Returns `True` if running in the Termux Android environment. |
82
- | `is_ish_alpine()` | Returns `True` if running in the iSH Alpine Linux iOS emulator. |
83
- | `is_android()` | Returns `True` on any Android-based Linux environment. |
78
+ | `on_windows()` | Returns `True` on Windows. |
79
+ | `on_apple()` | Returns `True` on macOS (Darwin). |
80
+ | `on_linux()` | Returns `True` on Linux in general. |
81
+ | `on_termux()` | Returns `True` if running in the Termux Android environment. |
82
+ | `on_freebsd()` | Returns `True` on FreeBSD. |
83
+ | `on_ish_alpine()` | Returns `True` if running in the iSH Alpine Linux iOS emulator. |
84
+ | `on_android()` | Returns `True` on any Android-based Linux environment. |
85
+ | `in_repl()` | Returns `True` is the user is currently in a Python REPL; hasattr(sys,'ps1'). |
84
86
 
85
87
  ### Packaging and Build Checking
86
88
 
87
- Key question: "What is the character of my executable?"
89
+ Key question: "What is the character of my executable or my build state?"
90
+
91
+ These functions accept an optional path argument (Path or str), defaulting to sys.argv[0] (e.g., pyhabitat/__main__.py for python -m pyhabitat, empty in REPL). Path.resolve() is used for stability.
88
92
 
89
93
  | Function | Description |
90
94
  | :--- | :--- |
91
- | `is_frozen()` | Returns `True` if the script is running as a standalone executable (any bundler). |
92
- | `is_pyinstaller()` | Returns `True` if the script is_frozen() and was generated by Pyinstaller (has MEI). |
93
- | `is_pipx()` | Returns `True` if running from a pipx managed virtual environment. |
94
- | `is_elf()` | Checks if the executable is an ELF binary (Linux standalone executable), excluding pipx. |
95
- | `is_windows_portable_executable()` | Checks if the executable is a Windows PE binary (MZ header), excluding pipx. |
96
- | `is_macos_executable()` | Checks if the executable is a macOS/Darwin Mach-O binary, excluding pipx. || `is_macos_executable()` | Checks if the executable is a macOS/Darwin Mach-O binary, excluding pipx. |
97
-
95
+ | `as_frozen()` | Returns `True` if the script is running as a standalone executable (any bundler). |
96
+ | `as_pyinstaller()` | Returns `True` if the script is frozen and generated by PyInstaller (has `_MEIPASS`). |
97
+ | `is_python_script(path=None)` | Returns `True` if the script or specified path is a Python source file (.py). |
98
+ | `is_pipx(path=None)` | Returns `True` if the script or specified path is from a pipx-managed virtual environment. |
99
+ | `is_elf(path=None)` | Returns `True` if the script or specified path is an ELF binary (Linux standalone executable, non-pipx). |
100
+ | `is_pyz(path=None)` | Returns `True` if the script or specified path is a Python zipapp (.pyz, non-pipx). |
101
+ | `is_windows_portable_executable(path=None)` | Returns `True` if the script or specified path is a Windows PE binary (MZ header, non-pipx). |
102
+ | `is_macos_executable(path=None)` | Returns `True` if the script or specified path is a macOS Mach-O binary (non-pipx). |
98
103
 
99
104
  ### Capability Checking
100
105
 
@@ -103,16 +108,18 @@ Key Question: "What could I do next?"
103
108
  | Function | Description |
104
109
  | :--- | :--- |
105
110
  | `tkinter_is_available()` | Checks if Tkinter is imported and can successfully create a window. |
106
- | `matplotlib_is_available_for_gui_plotting(termux_has_gui=False)` | Checks for Matplotlib and its TkAgg backend, required for interactive plotting. |
111
+ | `matplotlib_is_available_for_gui_plotting(termux_has_gui=False)` | Checks for Matplotlib and its TkAgg backend, required for interactive plotting. Set `termux_has_gui=True` for Termux with GUI support; defaults to `False`. |
107
112
  | `matplotlib_is_available_for_headless_image_export()` | Checks for Matplotlib and its Agg backend, required for saving images without a GUI. |
108
113
  | `interactive_terminal_is_available()` | Checks if standard input and output streams are connected to a TTY (allows safe use of interactive prompts). |
109
114
  | `web_browser_is_available()` | Check if a web browser can be launched in the current environment (allows safe use of web-based prompts and localhost plotting). |
110
115
 
111
- ### Actions
116
+ ### Utility
112
117
 
113
118
  | Function | Description |
114
119
  | :--- | :--- |
115
- | `edit_textfile()` | Smoothly opens a text file for editing (for configuration editing prompted by a CLI flag). |
120
+ | `edit_textfile(path)` | Opens a text file for editing using the default editor (Windows, Linux, macOS) or nano in Termux/iSH. In REPL mode, prints an error. Path argument (str or Path) uses Path.resolve() for stability. |
121
+ | `interp_path(print_path=False)` | Returns the path to the Python interpreter binary (sys.executable). Optionally prints the path. Returns empty string if unavailable. |
122
+ | `main()` | Prints a comprehensive environment report with sections: Interpreter Checks (sys.executable), Current Environment Check (sys.argv[0]), Current Build Checks (sys attributes), Operating System Checks (platform.system()), and Capability Checks. Run via `python -m pyhabitat` or `import pyhabitat; pyhabitat.main()` in the REPL. |
116
123
 
117
124
  </details>
118
125
 
@@ -123,39 +130,57 @@ Key Question: "What could I do next?"
123
130
 
124
131
  The module exposes all detection functions directly for easy access.
125
132
 
126
- ### 0\. Current Use
133
+ ### 0\.Example of PyHabitat in Action
127
134
 
128
135
  The `pipeline-eds` package uses the `pyhabitat` library to handle [configuration](https://github.com/City-of-Memphis-Wastewater/pipeline/blob/main/src/pipeline/security_and_config.py) and [plotting](https://github.com/City-of-Memphis-Wastewater/pipeline/blob/main/src/pipeline/cli.py), among other things.
129
136
 
130
- ### 1\. Checking Environment and Build Type
137
+ ### 1\. Running the Environment Report
138
+
139
+ Run a comprehensive environment report from the command line or REPL to inspect the interpreter (sys.executable), running script (sys.argv[0]), build state, operating system, and capabilities.
140
+
141
+ ```bash
142
+ # In the terminal
143
+ python -m pyhabitat
144
+ ```
131
145
 
132
146
  ```python
133
- from pyhabitat import is_termux, is_windows, is_pipx, is_frozen
147
+ # In the Python REPL
148
+ import pyhabitat as ph
149
+ ph.main()
150
+ ```
151
+
152
+ ### 2\. Checking Environment and Build Type
153
+
154
+ ```python
155
+ from pyhabitat import on_termux, on_windows, is_pipx, is_python_script, as_frozen
134
156
 
135
157
  if is_pipx():
136
158
  print("Running inside a pipx virtual environment. This is not a standalone binary.")
137
159
 
138
- elif is_frozen():
160
+ if as_frozen():
139
161
  print("Running as a frozen executable (PyInstaller, cx_Freeze, etc.).")
140
162
 
141
- elif is_termux():
163
+ if is_python_script():
164
+ print("Running as a Python source script (.py).")
165
+
166
+ if on_termux():
142
167
  # Expected cases:
143
168
  #- pkg install python-numpy python-cryptography
144
- #- Avoiding matplotlib unless the user explicitly confirms that termux_has_gui=False in matplotlib_is_available_for_gui_plotting(termux_has_gui=False).
169
+ #- Avoiding matplotlib unless the user explicitly sets termux_has_gui=True in matplotlib_is_available_for_gui_plotting().
145
170
  #- Auto-selection of 'termux-open-url' and 'xdg-open' in logic.
146
171
  #- Installation on the system, like orchestrating the construction of Termux Widget entries in ~/.shortcuts.
147
172
  print("Running in the Termux environment on Android.")
148
173
 
149
- elif is_windows():
174
+ if on_windows():
150
175
  print("Running on Windows.")
151
176
  ```
152
177
 
153
- ### 2\. Checking GUI and Plotting Availability
178
+ ### 3\. Checking GUI and Plotting Availability
154
179
 
155
180
  Use these functions to determine if you can show an interactive plot or if you must save an image file.
156
181
 
157
182
  ```python
158
- from pyhabitat import matplotlib_is_available_for_gui_plotting, matplotlib_is_available_for_headless_image_export,
183
+ from pyhabitat import matplotlib_is_available_for_gui_plotting, matplotlib_is_available_for_headless_image_export
159
184
 
160
185
  if matplotlib_is_available_for_gui_plotting():
161
186
  # We can safely call plt.show()
@@ -173,13 +198,16 @@ else:
173
198
  print("Matplotlib is not installed or the environment is too restrictive for plotting.")
174
199
  ```
175
200
 
176
- ### 3\. Text Editing
201
+ ### 4\. Text Editing
177
202
 
178
- Use this function to smoothly open a text file for editing.
203
+ Use this function to open a text file for editing.
179
204
  Ideal use case: Edit a configuration file, if prompted by a CLI command like 'config --textedit'.
180
205
 
181
206
  ```python
182
- edit_textfile(filepath=Path('./config.json'))
207
+ from pathlib import Path
208
+ import pyhabitat as ph
209
+
210
+ ph.edit_textfile(path=Path('./config.json'))
183
211
  ```
184
212
  </details>
185
213
 
@@ -2,6 +2,7 @@ LICENSE
2
2
  README.md
3
3
  pyproject.toml
4
4
  pyhabitat/__init__.py
5
+ pyhabitat/__main__.py
5
6
  pyhabitat/environment.py
6
7
  pyhabitat.egg-info/PKG-INFO
7
8
  pyhabitat.egg-info/SOURCES.txt
@@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta"
6
6
 
7
7
  [project]
8
8
  name = "pyhabitat"
9
- version = "1.0.16"
9
+ version = "1.0.17"
10
10
  #dynamic = ["version"] #
11
11
  authors = [
12
12
  { name="George Clayton Bennett", email="george.bennett@memphistn.gov" },
File without changes
File without changes