pyhabitat 0.0.0__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.
- pyhabitat-0.0.0/LICENSE +7 -0
- pyhabitat-0.0.0/PKG-INFO +159 -0
- pyhabitat-0.0.0/README.md +143 -0
- pyhabitat-0.0.0/pyhabitat/__init__.py +17 -0
- pyhabitat-0.0.0/pyhabitat/environment.py +487 -0
- pyhabitat-0.0.0/pyhabitat.egg-info/PKG-INFO +159 -0
- pyhabitat-0.0.0/pyhabitat.egg-info/SOURCES.txt +9 -0
- pyhabitat-0.0.0/pyhabitat.egg-info/dependency_links.txt +1 -0
- pyhabitat-0.0.0/pyhabitat.egg-info/top_level.txt +1 -0
- pyhabitat-0.0.0/pyproject.toml +32 -0
- pyhabitat-0.0.0/setup.cfg +4 -0
pyhabitat-0.0.0/LICENSE
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
Copyright © 2025 George Clayton Bennett
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
4
|
+
|
|
5
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
6
|
+
|
|
7
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
pyhabitat-0.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pyhabitat
|
|
3
|
+
Version: 0.0.0
|
|
4
|
+
Summary: A robust library for detecting system environment, GUI, and build properties.
|
|
5
|
+
Author-email: George Clayton Bennett <george.bennett@memphistn.gov>
|
|
6
|
+
License: MIT
|
|
7
|
+
Keywords: environment,os-detection,gui,build-system
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Classifier: Topic :: System :: Systems Administration
|
|
12
|
+
Requires-Python: >=3.8
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
License-File: LICENSE
|
|
15
|
+
Dynamic: license-file
|
|
16
|
+
|
|
17
|
+
# pyhabitat 🧭
|
|
18
|
+
|
|
19
|
+
## A Focused Introspection Library for Python Environments and Builds
|
|
20
|
+
|
|
21
|
+
**`pyhabitat`** is a **focused, lightweight library for Python build and environment introspection**. It accurately and securely determines the execution context of a running script by providing definitive checks for:
|
|
22
|
+
|
|
23
|
+
* **OS and Environments:** Operating Systems and common container/emulation environments (e.g., Termux, iSH).
|
|
24
|
+
* **Build States:** Application build systems (e.g., PyInstaller, pipx).
|
|
25
|
+
* **GUI Backends:** Availability of graphical toolkits (e.g., Matplotlib, Tkinter).
|
|
26
|
+
|
|
27
|
+
Stop writing verbose `sys.platform` and environment variable checks. Use **`pyhabitat`** to implement clean, **architectural logic** based on the execution habitat.
|
|
28
|
+
|
|
29
|
+
This library is especially useful for **leveraging Python in mobile environments** (`Termux` on Android and `iSH` on iOS), which often have particular limitations and require special handling. For example, it helps automate work-arounds like using **localhost plotting** when `matplotlib` is unavailable or **web-based interfaces** when `tkinter` is missing.
|
|
30
|
+
|
|
31
|
+
Our team is fundamentally driven by enabling mobile computing for true utility applications, leveraging environments like Termux (Android) and iSH (iOS). This includes highly practical solutions, such as deploying a lightweight Python web server (e.g., Flask, http.server, FastAPI) directly on a handset, or orchestrating full-stack, utility-grade applications that allow technicians to manage data and systems right from their mobile device in a way that is cross-platform and not overly catered to the App Store.
|
|
32
|
+
|
|
33
|
+
Another key goal of this project is to facilitate the orchestration of wider system installation for **`pipx` CLI tools** for additional touch points, like addition to context menus and widgets.
|
|
34
|
+
|
|
35
|
+
Ultimately, [City-of-Memphis-Wastewater](https://github.com/City-of-Memphis-Wastewater) aims to produce **reference-quality code** for the documented proper approach. We recognize that many people (and bots) are searching for ideal solutions, and our functions are built upon extensive research and testing to go **beyond simple `platform.system()` checks**.
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## 🚀 Features
|
|
40
|
+
|
|
41
|
+
* **Definitive Environment Checks:** Rigorous checks catered to Termux and iSH (iOS Alpine). Accurate, typical modern detection for Windows, macOS (Apple), Linux, FreeBSD, Android.
|
|
42
|
+
* **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).
|
|
43
|
+
* **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.
|
|
44
|
+
* **Executable Type Inspection:** Uses file magic numbers (ELF and MZ) to confirm if the running script is a monolithic, frozen binary (non-pipx).
|
|
45
|
+
|
|
46
|
+
## 📦 Installation
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
pip install pyhabitat
|
|
50
|
+
```
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## 📚 API Reference
|
|
54
|
+
|
|
55
|
+
### OS and Environment
|
|
56
|
+
|
|
57
|
+
| Function | Description |
|
|
58
|
+
| :--- | :--- |
|
|
59
|
+
| `is_windows()` | Returns `True` on Windows. |
|
|
60
|
+
| `is_apple()` | Returns `True` on macOS (Darwin). |
|
|
61
|
+
| `is_linux()` | Returns `True` on Linux in general. |
|
|
62
|
+
| `is_termux()` | Returns `True` if running in the Termux Android environment. |
|
|
63
|
+
| `is_ish_alpine()` | Returns `True` if running in the iSH Alpine Linux iOS emulator. |
|
|
64
|
+
| `is_android()` | Returns `True` on any Android-based Linux environment. |
|
|
65
|
+
|
|
66
|
+
### Build and Packaging
|
|
67
|
+
|
|
68
|
+
| Function | Description |
|
|
69
|
+
| :--- | :--- |
|
|
70
|
+
| `is_frozen()` | Returns `True` if the script is running as a standalone executable (any bundler). |
|
|
71
|
+
| `is_pipx()` | Returns `True` if running from a pipx managed virtual environment. |
|
|
72
|
+
| `is_elf()` | Checks if the executable is an ELF binary (Linux standalone executable), excluding pipx. |
|
|
73
|
+
| `is_windows_portable_executable()` | Checks if the executable is a Windows PE binary (MZ header), excluding pipx. |
|
|
74
|
+
| `is_macos_executable()` | Checks if the executable is a macOS/Darwin Mach-O binary, excluding pipx. |
|
|
75
|
+
|
|
76
|
+
### Capabilities
|
|
77
|
+
|
|
78
|
+
| Function | Description |
|
|
79
|
+
| :--- | :--- |
|
|
80
|
+
| `tkinter_is_available()` | Checks if Tkinter is imported and can successfully create a window. |
|
|
81
|
+
| `matplotlib_is_available_for_gui_plotting(termux_has_gui=False)` | Checks for Matplotlib and its TkAgg backend, required for interactive plotting. |
|
|
82
|
+
| `matplotlib_is_available_for_headless_image_export()` | Checks for Matplotlib and its Agg backend, required for saving images without a GUI. |
|
|
83
|
+
| `is_interactive_terminal()` | Checks if standard input and output streams are connected to a TTY (allows safe use of interactive prompts). |
|
|
84
|
+
| `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). |
|
|
85
|
+
|
|
86
|
+
### Actions
|
|
87
|
+
| Function | Description |
|
|
88
|
+
| :--- | :--- |
|
|
89
|
+
| `open_text_file_in_default_app()` | Smoothly opens a text file for editing (for configuration editing prompted by a CLI flag). |
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## 💻 Usage Examples
|
|
94
|
+
|
|
95
|
+
The module exposes all detection functions directly for easy access.
|
|
96
|
+
|
|
97
|
+
### 1\. Checking Environment and Build Type
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
from pyhabitat import is_termux, is_windows, is_pipx, is_frozen
|
|
101
|
+
|
|
102
|
+
if is_pipx():
|
|
103
|
+
print("Running inside a pipx virtual environment. This is not a standalone binary.")
|
|
104
|
+
|
|
105
|
+
elif is_frozen():
|
|
106
|
+
print("Running as a frozen executable (PyInstaller, cx_Freeze, etc.).")
|
|
107
|
+
|
|
108
|
+
elif is_termux():
|
|
109
|
+
# Expected cases:
|
|
110
|
+
#- pkg install python-numpy python-cryptography
|
|
111
|
+
#- Avoiding matplotlib unless the user explicitly confirms that termux_has_gui=False in matplotlib_is_available_for_gui_plotting(termux_has_gui=False).
|
|
112
|
+
#- Auto-selection of 'termux-open-url' and 'xdg-open' in logic.
|
|
113
|
+
#- Installation on the system, like orchestrating the construction of Termux Widget entries in ~/.shortcuts.
|
|
114
|
+
print("Running in the Termux environment on Android.")
|
|
115
|
+
|
|
116
|
+
elif is_windows():
|
|
117
|
+
print("Running on Windows.")
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### 2\. Checking GUI and Plotting Availability
|
|
121
|
+
|
|
122
|
+
Use these functions to determine if you can show an interactive plot or if you must save an image file.
|
|
123
|
+
|
|
124
|
+
```python
|
|
125
|
+
from pyhabitat import matplotlib_is_available_for_gui_plotting, matplotlib_is_available_for_headless_image_export
|
|
126
|
+
|
|
127
|
+
if matplotlib_is_available_for_gui_plotting():
|
|
128
|
+
# We can safely call plt.show()
|
|
129
|
+
print("GUI plotting is available! Using TkAgg backend.")
|
|
130
|
+
import matplotlib.pyplot as plt
|
|
131
|
+
plt.figure()
|
|
132
|
+
plt.show()
|
|
133
|
+
|
|
134
|
+
elif matplotlib_is_available_for_headless_image_export():
|
|
135
|
+
# We must save the plot to a file or buffer
|
|
136
|
+
print("GUI unavailable, but headless image export is possible.")
|
|
137
|
+
# Code to use 'Agg' backend and save to disk...
|
|
138
|
+
|
|
139
|
+
else:
|
|
140
|
+
print("Matplotlib is not installed or the environment is too restrictive for plotting.")
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### 3\. Text Editing
|
|
144
|
+
|
|
145
|
+
Use this function to smoothly open a text file for editing.
|
|
146
|
+
Ideal use case: Edit a configuration file, if prompted by a CLI command like 'config --textedit'.
|
|
147
|
+
|
|
148
|
+
```python
|
|
149
|
+
open_text_file_in_default_app(filepath=Path('./config.json'))
|
|
150
|
+
```
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
## 🤝 Contributing
|
|
154
|
+
|
|
155
|
+
Contributions are welcome\! If you find an environment or build system that is not correctly detected (e.g., a new container or a specific bundler), please open an issue or submit a pull request with the relevant detection logic.
|
|
156
|
+
|
|
157
|
+
## 📄 License
|
|
158
|
+
|
|
159
|
+
This project is licensed under the MIT License. See the LICENSE file for details.
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# pyhabitat 🧭
|
|
2
|
+
|
|
3
|
+
## A Focused Introspection Library for Python Environments and Builds
|
|
4
|
+
|
|
5
|
+
**`pyhabitat`** is a **focused, lightweight library for Python build and environment introspection**. It accurately and securely determines the execution context of a running script by providing definitive checks for:
|
|
6
|
+
|
|
7
|
+
* **OS and Environments:** Operating Systems and common container/emulation environments (e.g., Termux, iSH).
|
|
8
|
+
* **Build States:** Application build systems (e.g., PyInstaller, pipx).
|
|
9
|
+
* **GUI Backends:** Availability of graphical toolkits (e.g., Matplotlib, Tkinter).
|
|
10
|
+
|
|
11
|
+
Stop writing verbose `sys.platform` and environment variable checks. Use **`pyhabitat`** to implement clean, **architectural logic** based on the execution habitat.
|
|
12
|
+
|
|
13
|
+
This library is especially useful for **leveraging Python in mobile environments** (`Termux` on Android and `iSH` on iOS), which often have particular limitations and require special handling. For example, it helps automate work-arounds like using **localhost plotting** when `matplotlib` is unavailable or **web-based interfaces** when `tkinter` is missing.
|
|
14
|
+
|
|
15
|
+
Our team is fundamentally driven by enabling mobile computing for true utility applications, leveraging environments like Termux (Android) and iSH (iOS). This includes highly practical solutions, such as deploying a lightweight Python web server (e.g., Flask, http.server, FastAPI) directly on a handset, or orchestrating full-stack, utility-grade applications that allow technicians to manage data and systems right from their mobile device in a way that is cross-platform and not overly catered to the App Store.
|
|
16
|
+
|
|
17
|
+
Another key goal of this project is to facilitate the orchestration of wider system installation for **`pipx` CLI tools** for additional touch points, like addition to context menus and widgets.
|
|
18
|
+
|
|
19
|
+
Ultimately, [City-of-Memphis-Wastewater](https://github.com/City-of-Memphis-Wastewater) aims to produce **reference-quality code** for the documented proper approach. We recognize that many people (and bots) are searching for ideal solutions, and our functions are built upon extensive research and testing to go **beyond simple `platform.system()` checks**.
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## 🚀 Features
|
|
24
|
+
|
|
25
|
+
* **Definitive Environment Checks:** Rigorous checks catered to Termux and iSH (iOS Alpine). Accurate, typical modern detection for Windows, macOS (Apple), Linux, FreeBSD, Android.
|
|
26
|
+
* **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).
|
|
27
|
+
* **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.
|
|
28
|
+
* **Executable Type Inspection:** Uses file magic numbers (ELF and MZ) to confirm if the running script is a monolithic, frozen binary (non-pipx).
|
|
29
|
+
|
|
30
|
+
## 📦 Installation
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pip install pyhabitat
|
|
34
|
+
```
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## 📚 API Reference
|
|
38
|
+
|
|
39
|
+
### OS and Environment
|
|
40
|
+
|
|
41
|
+
| Function | Description |
|
|
42
|
+
| :--- | :--- |
|
|
43
|
+
| `is_windows()` | Returns `True` on Windows. |
|
|
44
|
+
| `is_apple()` | Returns `True` on macOS (Darwin). |
|
|
45
|
+
| `is_linux()` | Returns `True` on Linux in general. |
|
|
46
|
+
| `is_termux()` | Returns `True` if running in the Termux Android environment. |
|
|
47
|
+
| `is_ish_alpine()` | Returns `True` if running in the iSH Alpine Linux iOS emulator. |
|
|
48
|
+
| `is_android()` | Returns `True` on any Android-based Linux environment. |
|
|
49
|
+
|
|
50
|
+
### Build and Packaging
|
|
51
|
+
|
|
52
|
+
| Function | Description |
|
|
53
|
+
| :--- | :--- |
|
|
54
|
+
| `is_frozen()` | Returns `True` if the script is running as a standalone executable (any bundler). |
|
|
55
|
+
| `is_pipx()` | Returns `True` if running from a pipx managed virtual environment. |
|
|
56
|
+
| `is_elf()` | Checks if the executable is an ELF binary (Linux standalone executable), excluding pipx. |
|
|
57
|
+
| `is_windows_portable_executable()` | Checks if the executable is a Windows PE binary (MZ header), excluding pipx. |
|
|
58
|
+
| `is_macos_executable()` | Checks if the executable is a macOS/Darwin Mach-O binary, excluding pipx. |
|
|
59
|
+
|
|
60
|
+
### Capabilities
|
|
61
|
+
|
|
62
|
+
| Function | Description |
|
|
63
|
+
| :--- | :--- |
|
|
64
|
+
| `tkinter_is_available()` | Checks if Tkinter is imported and can successfully create a window. |
|
|
65
|
+
| `matplotlib_is_available_for_gui_plotting(termux_has_gui=False)` | Checks for Matplotlib and its TkAgg backend, required for interactive plotting. |
|
|
66
|
+
| `matplotlib_is_available_for_headless_image_export()` | Checks for Matplotlib and its Agg backend, required for saving images without a GUI. |
|
|
67
|
+
| `is_interactive_terminal()` | Checks if standard input and output streams are connected to a TTY (allows safe use of interactive prompts). |
|
|
68
|
+
| `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). |
|
|
69
|
+
|
|
70
|
+
### Actions
|
|
71
|
+
| Function | Description |
|
|
72
|
+
| :--- | :--- |
|
|
73
|
+
| `open_text_file_in_default_app()` | Smoothly opens a text file for editing (for configuration editing prompted by a CLI flag). |
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## 💻 Usage Examples
|
|
78
|
+
|
|
79
|
+
The module exposes all detection functions directly for easy access.
|
|
80
|
+
|
|
81
|
+
### 1\. Checking Environment and Build Type
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
from pyhabitat import is_termux, is_windows, is_pipx, is_frozen
|
|
85
|
+
|
|
86
|
+
if is_pipx():
|
|
87
|
+
print("Running inside a pipx virtual environment. This is not a standalone binary.")
|
|
88
|
+
|
|
89
|
+
elif is_frozen():
|
|
90
|
+
print("Running as a frozen executable (PyInstaller, cx_Freeze, etc.).")
|
|
91
|
+
|
|
92
|
+
elif is_termux():
|
|
93
|
+
# Expected cases:
|
|
94
|
+
#- pkg install python-numpy python-cryptography
|
|
95
|
+
#- Avoiding matplotlib unless the user explicitly confirms that termux_has_gui=False in matplotlib_is_available_for_gui_plotting(termux_has_gui=False).
|
|
96
|
+
#- Auto-selection of 'termux-open-url' and 'xdg-open' in logic.
|
|
97
|
+
#- Installation on the system, like orchestrating the construction of Termux Widget entries in ~/.shortcuts.
|
|
98
|
+
print("Running in the Termux environment on Android.")
|
|
99
|
+
|
|
100
|
+
elif is_windows():
|
|
101
|
+
print("Running on Windows.")
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### 2\. Checking GUI and Plotting Availability
|
|
105
|
+
|
|
106
|
+
Use these functions to determine if you can show an interactive plot or if you must save an image file.
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
from pyhabitat import matplotlib_is_available_for_gui_plotting, matplotlib_is_available_for_headless_image_export
|
|
110
|
+
|
|
111
|
+
if matplotlib_is_available_for_gui_plotting():
|
|
112
|
+
# We can safely call plt.show()
|
|
113
|
+
print("GUI plotting is available! Using TkAgg backend.")
|
|
114
|
+
import matplotlib.pyplot as plt
|
|
115
|
+
plt.figure()
|
|
116
|
+
plt.show()
|
|
117
|
+
|
|
118
|
+
elif matplotlib_is_available_for_headless_image_export():
|
|
119
|
+
# We must save the plot to a file or buffer
|
|
120
|
+
print("GUI unavailable, but headless image export is possible.")
|
|
121
|
+
# Code to use 'Agg' backend and save to disk...
|
|
122
|
+
|
|
123
|
+
else:
|
|
124
|
+
print("Matplotlib is not installed or the environment is too restrictive for plotting.")
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### 3\. Text Editing
|
|
128
|
+
|
|
129
|
+
Use this function to smoothly open a text file for editing.
|
|
130
|
+
Ideal use case: Edit a configuration file, if prompted by a CLI command like 'config --textedit'.
|
|
131
|
+
|
|
132
|
+
```python
|
|
133
|
+
open_text_file_in_default_app(filepath=Path('./config.json'))
|
|
134
|
+
```
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
## 🤝 Contributing
|
|
138
|
+
|
|
139
|
+
Contributions are welcome\! If you find an environment or build system that is not correctly detected (e.g., a new container or a specific bundler), please open an issue or submit a pull request with the relevant detection logic.
|
|
140
|
+
|
|
141
|
+
## 📄 License
|
|
142
|
+
|
|
143
|
+
This project is licensed under the MIT License. See the LICENSE file for details.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# pyhabitat/__init__.py
|
|
2
|
+
|
|
3
|
+
from .environment import (
|
|
4
|
+
is_termux,
|
|
5
|
+
is_windows,
|
|
6
|
+
is_pipx,
|
|
7
|
+
matplotlib_is_available_for_gui_plotting,
|
|
8
|
+
# Add all key functions here
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
# Optional: Set __all__ for explicit imports
|
|
12
|
+
__all__ = [
|
|
13
|
+
'is_termux',
|
|
14
|
+
'is_windows',
|
|
15
|
+
'is_pipx',
|
|
16
|
+
'matplotlib_is_available_for_gui_plotting',
|
|
17
|
+
]
|
|
@@ -0,0 +1,487 @@
|
|
|
1
|
+
'''
|
|
2
|
+
Title: environment.py
|
|
3
|
+
Author: Clayton Bennett
|
|
4
|
+
Created: 23 July 2024
|
|
5
|
+
'''
|
|
6
|
+
from __future__ import annotations # Delays annotation evaluation, allowing modern 3.10+ type syntax and forward references in older Python versions 3.8 and 3.9
|
|
7
|
+
import platform
|
|
8
|
+
import sys
|
|
9
|
+
import os
|
|
10
|
+
import webbrowser
|
|
11
|
+
import shutil
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
import subprocess
|
|
14
|
+
import io
|
|
15
|
+
|
|
16
|
+
from pipeline.helpers import check_if_zip
|
|
17
|
+
|
|
18
|
+
# Global cache for tkinter and matplotlib (mpl) availability
|
|
19
|
+
_TKINTER_AVAILABILITY: bool | None = None
|
|
20
|
+
_MATPLOTLIB_EXPORT_AVAILABILITY: bool | None = None
|
|
21
|
+
_MATPLOTLIB_WINDOWED_AVAILABILITY: bool | None = None
|
|
22
|
+
|
|
23
|
+
# --- GUI CHECKS ---
|
|
24
|
+
def matplotlib_is_available_for_gui_plotting(termux_has_gui=False):
|
|
25
|
+
"""Check if Matplotlib is available AND can use a GUI backend for a popup window."""
|
|
26
|
+
global _MATPLOTLIB_WINDOWED_AVAILABILITY
|
|
27
|
+
|
|
28
|
+
if _MATPLOTLIB_WINDOWED_AVAILABILITY is not None:
|
|
29
|
+
return _MATPLOTLIB_WINDOWED_AVAILABILITY
|
|
30
|
+
|
|
31
|
+
# 1. Termux exclusion check (assume no X11/GUI)
|
|
32
|
+
# Exclude Termux UNLESS the user explicitly provides termux_has_gui=True.
|
|
33
|
+
if is_termux() and not termux_has_gui:
|
|
34
|
+
_MATPLOTLIB_WINDOWED_AVAILABILITY = False
|
|
35
|
+
return False
|
|
36
|
+
|
|
37
|
+
# 2. Tkinter check (The most definitive check for a working display environment)
|
|
38
|
+
# If tkinter can't open a window, Matplotlib's TkAgg backend will fail.
|
|
39
|
+
if not tkinter_is_available():
|
|
40
|
+
_MATPLOTLIB_WINDOWED_AVAILABILITY = False
|
|
41
|
+
return False
|
|
42
|
+
|
|
43
|
+
# 3. Matplotlib + TkAgg check
|
|
44
|
+
try:
|
|
45
|
+
import matplotlib
|
|
46
|
+
# Force the common GUI backend. At this point, we know tkinter is *available*.
|
|
47
|
+
# # 'TkAgg' is often the most reliable cross-platform test.
|
|
48
|
+
# 'TkAgg' != 'Agg'. The Agg backend is for non-gui image export.
|
|
49
|
+
matplotlib.use('TkAgg', force=True)
|
|
50
|
+
import matplotlib.pyplot as plt
|
|
51
|
+
# A simple test call to ensure the backend initializes
|
|
52
|
+
# This final test catches any edge cases where tkinter is present but
|
|
53
|
+
# Matplotlib's *integration* with it is broken
|
|
54
|
+
plt.figure()
|
|
55
|
+
plt.close()
|
|
56
|
+
|
|
57
|
+
_MATPLOTLIB_WINDOWED_AVAILABILITY = True
|
|
58
|
+
return True
|
|
59
|
+
|
|
60
|
+
except Exception:
|
|
61
|
+
# Catches Matplotlib ImportError or any runtime error from the plt.figure() call
|
|
62
|
+
_MATPLOTLIB_WINDOWED_AVAILABILITY = False
|
|
63
|
+
return False
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def matplotlib_is_available_for_headless_image_export():
|
|
67
|
+
"""Check if Matplotlib is available AND can use the Agg backend for image export."""
|
|
68
|
+
global _MATPLOTLIB_EXPORT_AVAILABILITY
|
|
69
|
+
|
|
70
|
+
if _MATPLOTLIB_EXPORT_AVAILABILITY is not None:
|
|
71
|
+
return _MATPLOTLIB_EXPORT_AVAILABILITY
|
|
72
|
+
|
|
73
|
+
try:
|
|
74
|
+
import matplotlib
|
|
75
|
+
# The Agg backend (for PNG/JPEG export) is very basic and usually available
|
|
76
|
+
# if the core library is installed. We explicitly set it just in case.
|
|
77
|
+
# 'Agg' != 'TkAgg'. The TkAgg backend is for interactive gui image display.
|
|
78
|
+
matplotlib.use('Agg', force=True)
|
|
79
|
+
import matplotlib.pyplot as plt
|
|
80
|
+
|
|
81
|
+
# A simple test to ensure a figure can be generated
|
|
82
|
+
plt.figure()
|
|
83
|
+
# Ensure it can save to an in-memory buffer (to avoid disk access issues)
|
|
84
|
+
buf = io.BytesIO()
|
|
85
|
+
plt.savefig(buf, format='png')
|
|
86
|
+
plt.close()
|
|
87
|
+
|
|
88
|
+
_MATPLOTLIB_EXPORT_AVAILABILITY = True
|
|
89
|
+
return True
|
|
90
|
+
|
|
91
|
+
except Exception:
|
|
92
|
+
_MATPLOTLIB_EXPORT_AVAILABILITY = False
|
|
93
|
+
return False
|
|
94
|
+
|
|
95
|
+
def tkinter_is_available() -> bool:
|
|
96
|
+
"""Check if tkinter is available and can successfully connect to a display."""
|
|
97
|
+
global _TKINTER_AVAILABILITY
|
|
98
|
+
|
|
99
|
+
# 1. Return cached result if already calculated
|
|
100
|
+
if _TKINTER_AVAILABILITY is not None:
|
|
101
|
+
return _TKINTER_AVAILABILITY
|
|
102
|
+
|
|
103
|
+
# 2. Perform the full, definitive check
|
|
104
|
+
try:
|
|
105
|
+
import tkinter as tk
|
|
106
|
+
|
|
107
|
+
# Perform the actual GUI backend test for absolute certainty.
|
|
108
|
+
# This only runs once per script execution.
|
|
109
|
+
root = tk.Tk()
|
|
110
|
+
root.withdraw()
|
|
111
|
+
root.update()
|
|
112
|
+
root.destroy()
|
|
113
|
+
|
|
114
|
+
_TKINTER_AVAILABILITY = True
|
|
115
|
+
return True
|
|
116
|
+
except Exception:
|
|
117
|
+
# Fails if: tkinter module is missing OR the display backend is unavailable
|
|
118
|
+
_TKINTER_AVAILABILITY = False
|
|
119
|
+
return False
|
|
120
|
+
|
|
121
|
+
# --- ENVIRONMENT AND OPERATING SYSTEM CHECKS ---
|
|
122
|
+
def is_termux() -> bool:
|
|
123
|
+
"""Detect if running in Termux environment on Android, based on Termux-specific environmental variables."""
|
|
124
|
+
|
|
125
|
+
if platform.system() != 'Linux':
|
|
126
|
+
return False
|
|
127
|
+
|
|
128
|
+
termux_path_prefix = '/data/data/com.termux'
|
|
129
|
+
|
|
130
|
+
# Termux-specific environment variable ($PREFIX)
|
|
131
|
+
# The actual prefix is /data/data/com.termux/files/usr
|
|
132
|
+
if os.environ.get('PREFIX', default='').startswith(termux_path_prefix + '/usr'):
|
|
133
|
+
return True
|
|
134
|
+
|
|
135
|
+
# Termux-specific environment variable ($HOME)
|
|
136
|
+
# The actual home is /data/data/com.termux/files/home
|
|
137
|
+
if os.environ.get('HOME', default='').startswith(termux_path_prefix + '/home'):
|
|
138
|
+
return True
|
|
139
|
+
|
|
140
|
+
# Code insight: The os.environ.get command returns the supplied default if the key is not found.
|
|
141
|
+
# None is retured if a default is not speficied.
|
|
142
|
+
|
|
143
|
+
# Termux-specific environment variable ($TERMUX_VERSION)
|
|
144
|
+
if 'TERMUX_VERSION' in os.environ:
|
|
145
|
+
return True
|
|
146
|
+
|
|
147
|
+
return False
|
|
148
|
+
|
|
149
|
+
def is_freebsd() -> bool:
|
|
150
|
+
"""Detect if running on FreeBSD."""
|
|
151
|
+
return platform.system() == 'FreeBSD'
|
|
152
|
+
|
|
153
|
+
def is_linux():
|
|
154
|
+
"""Detect if running on Linux."""
|
|
155
|
+
return platform.system() == 'Linux'
|
|
156
|
+
|
|
157
|
+
def is_android() -> bool:
|
|
158
|
+
"""
|
|
159
|
+
Detect if running on Android.
|
|
160
|
+
|
|
161
|
+
Note: The is_termux() function is more robust and safe for Termux.
|
|
162
|
+
Checking for Termux with is_termux() does not require checking for Android with is_android().
|
|
163
|
+
|
|
164
|
+
is_android() will be True on:
|
|
165
|
+
- Sandboxed IDE's:
|
|
166
|
+
- Pydroid3
|
|
167
|
+
- QPython
|
|
168
|
+
- `proot`-reliant user-space containers:
|
|
169
|
+
- Termux
|
|
170
|
+
- Andronix
|
|
171
|
+
- UserLand
|
|
172
|
+
- AnLinux
|
|
173
|
+
|
|
174
|
+
is_android() will be False on:
|
|
175
|
+
- Full Virtual Machines:
|
|
176
|
+
- VirtualBox
|
|
177
|
+
- VMware
|
|
178
|
+
- QEMU
|
|
179
|
+
"""
|
|
180
|
+
# Explicitly check for Linux kernel name first
|
|
181
|
+
if platform.system() != 'Linux':
|
|
182
|
+
return False
|
|
183
|
+
return "android" in platform.platform().lower()
|
|
184
|
+
|
|
185
|
+
def is_windows() -> bool:
|
|
186
|
+
"""Detect if running on Windows."""
|
|
187
|
+
return platform.system() == 'Windows'
|
|
188
|
+
|
|
189
|
+
def is_apple() -> bool:
|
|
190
|
+
"""Detect if running on Apple."""
|
|
191
|
+
return platform.system() == 'Darwin'
|
|
192
|
+
|
|
193
|
+
def is_ish_alpine() -> bool:
|
|
194
|
+
"""Detect if running in iSH Alpine environment on iOS."""
|
|
195
|
+
# platform.system() usually returns 'Linux' in iSH
|
|
196
|
+
|
|
197
|
+
# iSH runs on iOS but reports 'Linux' via platform.system()
|
|
198
|
+
if platform.system() != 'Linux':
|
|
199
|
+
return False
|
|
200
|
+
|
|
201
|
+
# On iSH, /etc/apk/ will exist. However, this is not unique to iSH as standard Alpine Linux also has this directory.
|
|
202
|
+
# Therefore, we need an additional check to differentiate iSH from standard Alpine.
|
|
203
|
+
# HIGHLY SPECIFIC iSH CHECK: Look for the unique /proc/ish/ directory.
|
|
204
|
+
# This directory is created by the iSH pseudo-kernel and does not exist
|
|
205
|
+
# on standard Alpine or other Linux distributions.
|
|
206
|
+
if os.path.isdir('/etc/apk/') and os.path.isdir('/proc/ish'):
|
|
207
|
+
# This combination is highly specific to iSH Alpine.
|
|
208
|
+
return True
|
|
209
|
+
|
|
210
|
+
return False
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
# --- BUILD AND EXECUTABLE CHECKS ---
|
|
214
|
+
|
|
215
|
+
def pyinstaller():
|
|
216
|
+
"""Detects if the Python script is running as a 'frozen' in the course of generating a PyInstaller binary executable."""
|
|
217
|
+
# If the app is frozen AND has the PyInstaller-specific temporary folder path
|
|
218
|
+
return is_frozen() and hasattr(sys, '_MEIPASS')
|
|
219
|
+
|
|
220
|
+
# The standard way to check for a frozen state:
|
|
221
|
+
def is_frozen():
|
|
222
|
+
"""
|
|
223
|
+
Detects if the Python script is running as a 'frozen' (standalone)
|
|
224
|
+
executable created by a tool like PyInstaller, cx_Freeze, or Nuitka.
|
|
225
|
+
|
|
226
|
+
This check is crucial for handling file paths, finding resources,
|
|
227
|
+
and general environment assumptions, as a frozen executable's
|
|
228
|
+
structure differs significantly from a standard script execution
|
|
229
|
+
or a virtual environment.
|
|
230
|
+
|
|
231
|
+
The check is based on examining the 'frozen' attribute of the sys module.
|
|
232
|
+
|
|
233
|
+
Returns:
|
|
234
|
+
bool: True if the application is running as a frozen executable;
|
|
235
|
+
False otherwise.
|
|
236
|
+
"""
|
|
237
|
+
return getattr(sys, 'frozen', False)
|
|
238
|
+
|
|
239
|
+
def is_elf(exec_path : Path = None, debug=False) -> bool:
|
|
240
|
+
"""Checks if the currently running executable (sys.argv[0]) is a standalone PyInstaller-built ELF binary."""
|
|
241
|
+
# If it's a pipx installation, it is not the monolithic binary we are concerned with here.
|
|
242
|
+
|
|
243
|
+
if exec_path is None:
|
|
244
|
+
exec_path = Path(sys.argv[0]).resolve()
|
|
245
|
+
if debug:
|
|
246
|
+
print(f"exec_path = {exec_path}")
|
|
247
|
+
if is_pipx():
|
|
248
|
+
return False
|
|
249
|
+
|
|
250
|
+
# Check if the file exists and is readable
|
|
251
|
+
if not exec_path.is_file():
|
|
252
|
+
return False
|
|
253
|
+
|
|
254
|
+
try:
|
|
255
|
+
# Check the magic number: The first four bytes of an ELF file are 0x7f, 'E', 'L', 'F' (b'\x7fELF').
|
|
256
|
+
# This is the most reliable way to determine if the executable is a native binary wrapper (like PyInstaller's).
|
|
257
|
+
with open(exec_path, 'rb') as f:
|
|
258
|
+
magic_bytes = f.read(4)
|
|
259
|
+
|
|
260
|
+
return magic_bytes == b'\x7fELF'
|
|
261
|
+
except Exception:
|
|
262
|
+
# Handle exceptions like PermissionError, IsADirectoryError, etc.
|
|
263
|
+
return False
|
|
264
|
+
|
|
265
|
+
def is_pyz(exec_path: Path=None, debug=False) -> bool:
|
|
266
|
+
"""Checks if the currently running executable (sys.argv[0]) is a PYZ zipapp ."""
|
|
267
|
+
# If it's a pipx installation, it is not the monolithic binary we are concerned with here.
|
|
268
|
+
if exec_path is None:
|
|
269
|
+
exec_path = Path(sys.argv[0]).resolve()
|
|
270
|
+
if debug:
|
|
271
|
+
print(f"exec_path = {exec_path}")
|
|
272
|
+
|
|
273
|
+
if is_pipx():
|
|
274
|
+
return False
|
|
275
|
+
|
|
276
|
+
# Check if the extension is PYZ
|
|
277
|
+
if not str(exec_path).endswith(".pyz"):
|
|
278
|
+
return False
|
|
279
|
+
|
|
280
|
+
if not check_if_zip():
|
|
281
|
+
return False
|
|
282
|
+
|
|
283
|
+
def is_windows_portable_executable(exec_path: Path = None, debug=False) -> bool:
|
|
284
|
+
"""
|
|
285
|
+
Checks if the currently running executable (sys.argv[0]) is a
|
|
286
|
+
Windows Portable Executable (PE) binary, and explicitly excludes
|
|
287
|
+
pipx-managed environments.
|
|
288
|
+
Windows Portable Executables include .exe, .dll, and other binaries.
|
|
289
|
+
The standard way to check for a PE is to look for the MZ magic number at the very beginning of the file.
|
|
290
|
+
"""
|
|
291
|
+
# 1. Determine execution path
|
|
292
|
+
if exec_path is None:
|
|
293
|
+
exec_path = Path(sys.argv[0]).resolve()
|
|
294
|
+
|
|
295
|
+
if debug:
|
|
296
|
+
print(f"DEBUG: Checking executable path: {exec_path}")
|
|
297
|
+
|
|
298
|
+
# 2. Exclude pipx environments immediately
|
|
299
|
+
if is_pipx():
|
|
300
|
+
if debug: print("DEBUG: is_exe_non_pipx: False (is_pipx is True)")
|
|
301
|
+
return False
|
|
302
|
+
|
|
303
|
+
# 3. Perform file checks
|
|
304
|
+
if not exec_path.is_file():
|
|
305
|
+
if debug: print("DEBUG: is_exe_non_pipx: False (Not a file)")
|
|
306
|
+
return False
|
|
307
|
+
|
|
308
|
+
try:
|
|
309
|
+
# Check the magic number: All Windows PE files (EXE, DLL, etc.)
|
|
310
|
+
# start with the two-byte header b'MZ' (for Mark Zbikowski).
|
|
311
|
+
with open(exec_path, 'rb') as f:
|
|
312
|
+
magic_bytes = f.read(2)
|
|
313
|
+
|
|
314
|
+
is_pe = magic_bytes == b'MZ'
|
|
315
|
+
|
|
316
|
+
if debug:
|
|
317
|
+
print(f"DEBUG: Magic bytes: {magic_bytes}")
|
|
318
|
+
print(f"DEBUG: is_exe_non_pipx: {is_pe} (Non-pipx check)")
|
|
319
|
+
|
|
320
|
+
return is_pe
|
|
321
|
+
|
|
322
|
+
except Exception as e:
|
|
323
|
+
if debug: print(f"DEBUG: is_exe_non_pipx: Error during file check: {e}")
|
|
324
|
+
# Handle exceptions like PermissionError, IsADirectoryError, etc.
|
|
325
|
+
return False
|
|
326
|
+
|
|
327
|
+
def is_macos_executable(exec_path: Path = None, debug=False) -> bool:
|
|
328
|
+
"""
|
|
329
|
+
Checks if the currently running executable is a macOS/Darwin Mach-O binary,
|
|
330
|
+
and explicitly excludes pipx-managed environments.
|
|
331
|
+
"""
|
|
332
|
+
if exec_path is None:
|
|
333
|
+
exec_path = Path(sys.argv[0]).resolve()
|
|
334
|
+
|
|
335
|
+
if is_pipx():
|
|
336
|
+
if debug: print("DEBUG: is_macos_executable: False (is_pipx is True)")
|
|
337
|
+
return False
|
|
338
|
+
|
|
339
|
+
if not exec_path.is_file():
|
|
340
|
+
return False
|
|
341
|
+
|
|
342
|
+
try:
|
|
343
|
+
# Check the magic number: Mach-O binaries start with specific 4-byte headers.
|
|
344
|
+
# Common ones are: b'\xfe\xed\xfa\xce' (32-bit) or b'\xfe\xed\xfa\xcf' (64-bit)
|
|
345
|
+
with open(exec_path, 'rb') as f:
|
|
346
|
+
magic_bytes = f.read(4)
|
|
347
|
+
|
|
348
|
+
# Common Mach-O magic numbers (including their reversed-byte counterparts)
|
|
349
|
+
MACHO_MAGIC = {
|
|
350
|
+
b'\xfe\xed\xfa\xce', # MH_MAGIC
|
|
351
|
+
b'\xce\xfa\xed\xfe', # MH_CIGAM (byte-swapped)
|
|
352
|
+
b'\xfe\xed\xfa\xcf', # MH_MAGIC_64
|
|
353
|
+
b'\xcf\xfa\xed\xfe', # MH_CIGAM_64 (byte-swapped)
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
is_macho = magic_bytes in MACHO_MAGIC
|
|
357
|
+
|
|
358
|
+
if debug:
|
|
359
|
+
print(f"DEBUG: is_macos_executable: {is_macho} (Non-pipx check)")
|
|
360
|
+
|
|
361
|
+
return is_macho
|
|
362
|
+
|
|
363
|
+
except Exception:
|
|
364
|
+
return False
|
|
365
|
+
|
|
366
|
+
def is_pipx(debug=False) -> bool:
|
|
367
|
+
"""Checks if the executable is running from a pipx managed environment."""
|
|
368
|
+
try:
|
|
369
|
+
# Helper for case-insensitivity on Windows
|
|
370
|
+
def normalize_path(p: Path) -> str:
|
|
371
|
+
return str(p).lower()
|
|
372
|
+
|
|
373
|
+
exec_path = Path(sys.argv[0]).resolve()
|
|
374
|
+
|
|
375
|
+
# This is the path to the interpreter running the script (e.g., venv/bin/python)
|
|
376
|
+
# In a pipx-managed execution, this is the venv python.
|
|
377
|
+
interpreter_path = Path(sys.executable).resolve()
|
|
378
|
+
pipx_bin_path, pipx_venv_base_path = get_pipx_paths()
|
|
379
|
+
# Normalize paths for comparison
|
|
380
|
+
norm_exec_path = normalize_path(exec_path)
|
|
381
|
+
norm_interp_path = normalize_path(interpreter_path)
|
|
382
|
+
|
|
383
|
+
if debug:
|
|
384
|
+
# --- DEBUGGING OUTPUT ---
|
|
385
|
+
print(f"DEBUG: EXEC_PATH: {exec_path}")
|
|
386
|
+
print(f"DEBUG: INTERP_PATH: {interpreter_path}")
|
|
387
|
+
print(f"DEBUG: PIPX_BIN_PATH: {pipx_bin_path}")
|
|
388
|
+
print(f"DEBUG: PIPX_VENV_BASE: {pipx_venv_base_path}")
|
|
389
|
+
print(f"DEBUG: Check B result: {normalize_path(interpreter_path).startswith(normalize_path(pipx_venv_base_path))}")
|
|
390
|
+
# ------------------------
|
|
391
|
+
|
|
392
|
+
# 1. Signature Check (Most Robust): Look for the unique 'pipx/venvs' string.
|
|
393
|
+
# This is a strong check for both the executable path (your discovery)
|
|
394
|
+
# and the interpreter path (canonical venv location).
|
|
395
|
+
if "pipx/venvs" in norm_exec_path or "pipx/venvs" in norm_interp_path:
|
|
396
|
+
if debug: print("is_pipx: True (Signature Check)")
|
|
397
|
+
return True
|
|
398
|
+
|
|
399
|
+
# 2. Targeted Venv Check: The interpreter's path starts with the PIPX venv base.
|
|
400
|
+
# This is a canonical check if the signature check is somehow missed.
|
|
401
|
+
if norm_interp_path.startswith(normalize_path(pipx_venv_base_path)):
|
|
402
|
+
if debug: print("is_pipx: True (Interpreter Base Check)")
|
|
403
|
+
return True
|
|
404
|
+
|
|
405
|
+
# 3. Targeted Executable Check: The executable's resolved path starts with the PIPX venv base.
|
|
406
|
+
# This is your key Termux discovery, confirming the shim resolves into the venv.
|
|
407
|
+
if norm_exec_path.startswith(normalize_path(pipx_venv_base_path)):
|
|
408
|
+
if debug: print("is_pipx: True (Executable Base Check)")
|
|
409
|
+
return True
|
|
410
|
+
|
|
411
|
+
if debug: print("is_pipx: False")
|
|
412
|
+
return False
|
|
413
|
+
|
|
414
|
+
except Exception:
|
|
415
|
+
# Fallback for unexpected path errors
|
|
416
|
+
return False
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
# --- TTY CHECK ---
|
|
421
|
+
def is_interactive_terminal():
|
|
422
|
+
"""
|
|
423
|
+
Check if the script is running in an interactive terminal.
|
|
424
|
+
Assumpton:
|
|
425
|
+
If is_interactive_terminal() returns True,
|
|
426
|
+
then typer.prompt() will work reliably.
|
|
427
|
+
"""
|
|
428
|
+
# Check if a tty is attached to stdin
|
|
429
|
+
return sys.stdin.isatty() and sys.stdout.isatty()
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
# --- Browser Check ---
|
|
433
|
+
def web_browser_is_available() -> bool:
|
|
434
|
+
""" Check if a web browser can be launched in the current environment."""
|
|
435
|
+
try:
|
|
436
|
+
# 1. Standard Python check
|
|
437
|
+
webbrowser.get()
|
|
438
|
+
return True
|
|
439
|
+
except webbrowser.Error:
|
|
440
|
+
# Fallback needed. Check for external launchers.
|
|
441
|
+
# 2. Termux specific check
|
|
442
|
+
if shutil.which("termux-open-url"):
|
|
443
|
+
return True
|
|
444
|
+
# 3. General Linux check
|
|
445
|
+
if shutil.which("xdg-open"):
|
|
446
|
+
return True
|
|
447
|
+
return False
|
|
448
|
+
|
|
449
|
+
# --- LAUNCH MECHANISMS BASED ON ENVIRONMENT ---
|
|
450
|
+
def open_text_file_in_default_app(filepath):
|
|
451
|
+
"""Opens a file with its default application based on the OS."""
|
|
452
|
+
if is_windows():
|
|
453
|
+
os.startfile(filepath)
|
|
454
|
+
elif is_termux():
|
|
455
|
+
subprocess.run(['nano', filepath])
|
|
456
|
+
elif is_ish_alpine():
|
|
457
|
+
subprocess.run(['apk','add', 'nano'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
458
|
+
subprocess.run(['nano', filepath])
|
|
459
|
+
elif is_linux():
|
|
460
|
+
subprocess.run(['xdg-open', filepath])
|
|
461
|
+
elif is_apple():
|
|
462
|
+
subprocess.run(['open', filepath])
|
|
463
|
+
else:
|
|
464
|
+
print("Unsupported operating system.")
|
|
465
|
+
|
|
466
|
+
def get_pipx_paths():
|
|
467
|
+
"""Returns the configured/default pipx binary and home directories."""
|
|
468
|
+
# 1. PIPX_BIN_DIR (where the symlinks live, e.g., ~/.local/bin)
|
|
469
|
+
pipx_bin_dir_str = os.environ.get('PIPX_BIN_DIR')
|
|
470
|
+
if pipx_bin_dir_str:
|
|
471
|
+
pipx_bin_path = Path(pipx_bin_dir_str).resolve()
|
|
472
|
+
else:
|
|
473
|
+
# Default binary path (common across platforms for user installs)
|
|
474
|
+
pipx_bin_path = Path.home() / '.local' / 'bin'
|
|
475
|
+
|
|
476
|
+
# 2. PIPX_HOME (where the isolated venvs live, e.g., ~/.local/pipx/venvs)
|
|
477
|
+
pipx_home_str = os.environ.get('PIPX_HOME')
|
|
478
|
+
if pipx_home_str:
|
|
479
|
+
# PIPX_HOME is the base, venvs are in PIPX_HOME/venvs
|
|
480
|
+
pipx_venv_base = Path(pipx_home_str).resolve() / 'venvs'
|
|
481
|
+
else:
|
|
482
|
+
# Fallback to the modern default for PIPX_HOME (XDG standard)
|
|
483
|
+
# Note: pipx is smart and may check the older ~/.local/pipx too
|
|
484
|
+
# but the XDG one is the current standard.
|
|
485
|
+
pipx_venv_base = Path.home() / '.local' / 'share' / 'pipx' / 'venvs'
|
|
486
|
+
|
|
487
|
+
return pipx_bin_path, pipx_venv_base.resolve()
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pyhabitat
|
|
3
|
+
Version: 0.0.0
|
|
4
|
+
Summary: A robust library for detecting system environment, GUI, and build properties.
|
|
5
|
+
Author-email: George Clayton Bennett <george.bennett@memphistn.gov>
|
|
6
|
+
License: MIT
|
|
7
|
+
Keywords: environment,os-detection,gui,build-system
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Classifier: Topic :: System :: Systems Administration
|
|
12
|
+
Requires-Python: >=3.8
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
License-File: LICENSE
|
|
15
|
+
Dynamic: license-file
|
|
16
|
+
|
|
17
|
+
# pyhabitat 🧭
|
|
18
|
+
|
|
19
|
+
## A Focused Introspection Library for Python Environments and Builds
|
|
20
|
+
|
|
21
|
+
**`pyhabitat`** is a **focused, lightweight library for Python build and environment introspection**. It accurately and securely determines the execution context of a running script by providing definitive checks for:
|
|
22
|
+
|
|
23
|
+
* **OS and Environments:** Operating Systems and common container/emulation environments (e.g., Termux, iSH).
|
|
24
|
+
* **Build States:** Application build systems (e.g., PyInstaller, pipx).
|
|
25
|
+
* **GUI Backends:** Availability of graphical toolkits (e.g., Matplotlib, Tkinter).
|
|
26
|
+
|
|
27
|
+
Stop writing verbose `sys.platform` and environment variable checks. Use **`pyhabitat`** to implement clean, **architectural logic** based on the execution habitat.
|
|
28
|
+
|
|
29
|
+
This library is especially useful for **leveraging Python in mobile environments** (`Termux` on Android and `iSH` on iOS), which often have particular limitations and require special handling. For example, it helps automate work-arounds like using **localhost plotting** when `matplotlib` is unavailable or **web-based interfaces** when `tkinter` is missing.
|
|
30
|
+
|
|
31
|
+
Our team is fundamentally driven by enabling mobile computing for true utility applications, leveraging environments like Termux (Android) and iSH (iOS). This includes highly practical solutions, such as deploying a lightweight Python web server (e.g., Flask, http.server, FastAPI) directly on a handset, or orchestrating full-stack, utility-grade applications that allow technicians to manage data and systems right from their mobile device in a way that is cross-platform and not overly catered to the App Store.
|
|
32
|
+
|
|
33
|
+
Another key goal of this project is to facilitate the orchestration of wider system installation for **`pipx` CLI tools** for additional touch points, like addition to context menus and widgets.
|
|
34
|
+
|
|
35
|
+
Ultimately, [City-of-Memphis-Wastewater](https://github.com/City-of-Memphis-Wastewater) aims to produce **reference-quality code** for the documented proper approach. We recognize that many people (and bots) are searching for ideal solutions, and our functions are built upon extensive research and testing to go **beyond simple `platform.system()` checks**.
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## 🚀 Features
|
|
40
|
+
|
|
41
|
+
* **Definitive Environment Checks:** Rigorous checks catered to Termux and iSH (iOS Alpine). Accurate, typical modern detection for Windows, macOS (Apple), Linux, FreeBSD, Android.
|
|
42
|
+
* **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).
|
|
43
|
+
* **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.
|
|
44
|
+
* **Executable Type Inspection:** Uses file magic numbers (ELF and MZ) to confirm if the running script is a monolithic, frozen binary (non-pipx).
|
|
45
|
+
|
|
46
|
+
## 📦 Installation
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
pip install pyhabitat
|
|
50
|
+
```
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## 📚 API Reference
|
|
54
|
+
|
|
55
|
+
### OS and Environment
|
|
56
|
+
|
|
57
|
+
| Function | Description |
|
|
58
|
+
| :--- | :--- |
|
|
59
|
+
| `is_windows()` | Returns `True` on Windows. |
|
|
60
|
+
| `is_apple()` | Returns `True` on macOS (Darwin). |
|
|
61
|
+
| `is_linux()` | Returns `True` on Linux in general. |
|
|
62
|
+
| `is_termux()` | Returns `True` if running in the Termux Android environment. |
|
|
63
|
+
| `is_ish_alpine()` | Returns `True` if running in the iSH Alpine Linux iOS emulator. |
|
|
64
|
+
| `is_android()` | Returns `True` on any Android-based Linux environment. |
|
|
65
|
+
|
|
66
|
+
### Build and Packaging
|
|
67
|
+
|
|
68
|
+
| Function | Description |
|
|
69
|
+
| :--- | :--- |
|
|
70
|
+
| `is_frozen()` | Returns `True` if the script is running as a standalone executable (any bundler). |
|
|
71
|
+
| `is_pipx()` | Returns `True` if running from a pipx managed virtual environment. |
|
|
72
|
+
| `is_elf()` | Checks if the executable is an ELF binary (Linux standalone executable), excluding pipx. |
|
|
73
|
+
| `is_windows_portable_executable()` | Checks if the executable is a Windows PE binary (MZ header), excluding pipx. |
|
|
74
|
+
| `is_macos_executable()` | Checks if the executable is a macOS/Darwin Mach-O binary, excluding pipx. |
|
|
75
|
+
|
|
76
|
+
### Capabilities
|
|
77
|
+
|
|
78
|
+
| Function | Description |
|
|
79
|
+
| :--- | :--- |
|
|
80
|
+
| `tkinter_is_available()` | Checks if Tkinter is imported and can successfully create a window. |
|
|
81
|
+
| `matplotlib_is_available_for_gui_plotting(termux_has_gui=False)` | Checks for Matplotlib and its TkAgg backend, required for interactive plotting. |
|
|
82
|
+
| `matplotlib_is_available_for_headless_image_export()` | Checks for Matplotlib and its Agg backend, required for saving images without a GUI. |
|
|
83
|
+
| `is_interactive_terminal()` | Checks if standard input and output streams are connected to a TTY (allows safe use of interactive prompts). |
|
|
84
|
+
| `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). |
|
|
85
|
+
|
|
86
|
+
### Actions
|
|
87
|
+
| Function | Description |
|
|
88
|
+
| :--- | :--- |
|
|
89
|
+
| `open_text_file_in_default_app()` | Smoothly opens a text file for editing (for configuration editing prompted by a CLI flag). |
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## 💻 Usage Examples
|
|
94
|
+
|
|
95
|
+
The module exposes all detection functions directly for easy access.
|
|
96
|
+
|
|
97
|
+
### 1\. Checking Environment and Build Type
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
from pyhabitat import is_termux, is_windows, is_pipx, is_frozen
|
|
101
|
+
|
|
102
|
+
if is_pipx():
|
|
103
|
+
print("Running inside a pipx virtual environment. This is not a standalone binary.")
|
|
104
|
+
|
|
105
|
+
elif is_frozen():
|
|
106
|
+
print("Running as a frozen executable (PyInstaller, cx_Freeze, etc.).")
|
|
107
|
+
|
|
108
|
+
elif is_termux():
|
|
109
|
+
# Expected cases:
|
|
110
|
+
#- pkg install python-numpy python-cryptography
|
|
111
|
+
#- Avoiding matplotlib unless the user explicitly confirms that termux_has_gui=False in matplotlib_is_available_for_gui_plotting(termux_has_gui=False).
|
|
112
|
+
#- Auto-selection of 'termux-open-url' and 'xdg-open' in logic.
|
|
113
|
+
#- Installation on the system, like orchestrating the construction of Termux Widget entries in ~/.shortcuts.
|
|
114
|
+
print("Running in the Termux environment on Android.")
|
|
115
|
+
|
|
116
|
+
elif is_windows():
|
|
117
|
+
print("Running on Windows.")
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### 2\. Checking GUI and Plotting Availability
|
|
121
|
+
|
|
122
|
+
Use these functions to determine if you can show an interactive plot or if you must save an image file.
|
|
123
|
+
|
|
124
|
+
```python
|
|
125
|
+
from pyhabitat import matplotlib_is_available_for_gui_plotting, matplotlib_is_available_for_headless_image_export
|
|
126
|
+
|
|
127
|
+
if matplotlib_is_available_for_gui_plotting():
|
|
128
|
+
# We can safely call plt.show()
|
|
129
|
+
print("GUI plotting is available! Using TkAgg backend.")
|
|
130
|
+
import matplotlib.pyplot as plt
|
|
131
|
+
plt.figure()
|
|
132
|
+
plt.show()
|
|
133
|
+
|
|
134
|
+
elif matplotlib_is_available_for_headless_image_export():
|
|
135
|
+
# We must save the plot to a file or buffer
|
|
136
|
+
print("GUI unavailable, but headless image export is possible.")
|
|
137
|
+
# Code to use 'Agg' backend and save to disk...
|
|
138
|
+
|
|
139
|
+
else:
|
|
140
|
+
print("Matplotlib is not installed or the environment is too restrictive for plotting.")
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### 3\. Text Editing
|
|
144
|
+
|
|
145
|
+
Use this function to smoothly open a text file for editing.
|
|
146
|
+
Ideal use case: Edit a configuration file, if prompted by a CLI command like 'config --textedit'.
|
|
147
|
+
|
|
148
|
+
```python
|
|
149
|
+
open_text_file_in_default_app(filepath=Path('./config.json'))
|
|
150
|
+
```
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
## 🤝 Contributing
|
|
154
|
+
|
|
155
|
+
Contributions are welcome\! If you find an environment or build system that is not correctly detected (e.g., a new container or a specific bundler), please open an issue or submit a pull request with the relevant detection logic.
|
|
156
|
+
|
|
157
|
+
## 📄 License
|
|
158
|
+
|
|
159
|
+
This project is licensed under the MIT License. See the LICENSE file for details.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pyhabitat
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# pyproject.toml
|
|
2
|
+
|
|
3
|
+
[build-system]
|
|
4
|
+
requires = ["setuptools>=61.0.0"]
|
|
5
|
+
build-backend = "setuptools.build_meta"
|
|
6
|
+
|
|
7
|
+
[project]
|
|
8
|
+
name = "pyhabitat"
|
|
9
|
+
#version = "1.0.1"
|
|
10
|
+
authors = [
|
|
11
|
+
{ name="George Clayton Bennett", email="george.bennett@memphistn.gov" },
|
|
12
|
+
]
|
|
13
|
+
dynamic = ["version"] #
|
|
14
|
+
description = "A robust library for detecting system environment, GUI, and build properties."
|
|
15
|
+
readme = "README.md"
|
|
16
|
+
requires-python = ">=3.8"
|
|
17
|
+
license = { text = "MIT" }
|
|
18
|
+
keywords = ["environment", "os-detection", "gui", "build-system"]
|
|
19
|
+
classifiers = [
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"License :: OSI Approved :: MIT License",
|
|
22
|
+
"Operating System :: OS Independent",
|
|
23
|
+
"Topic :: System :: Systems Administration",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
[tool.setuptools.packages.find]
|
|
27
|
+
where = ["."]
|
|
28
|
+
include = ["pyhabitat"] # Only include the main package
|
|
29
|
+
|
|
30
|
+
[tool.setuptools_scm]
|
|
31
|
+
# This tells setuptools_scm to look at your git history/tags for the version
|
|
32
|
+
write_to = "pyhabitat/_version.py"
|