# pyhabitat 🧭

## An Introspection Library for Python Environments and Builds

**`pyhabitat`** is a **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:

* **OS and Environments:** Operating Systems and common container/emulation environments (e.g., Termux, iSH).
* **Build States:** Application build systems (e.g., PyInstaller, pipx).
* **GUI Backends:** Availability of graphical toolkits (e.g., Matplotlib, Tkinter).

Stop writing verbose `sys.platform` and environment variable checks. Use **`pyhabitat`** to implement clean, **architectural logic** based on the execution habitat.

---

Read the code on [github](https://github.com/City-of-Memphis-Wastewater/pyhabitat/blob/main/pyhabitat/environment.py). 🌐

---

## 📦 Installation

```bash
pip install pyhabitat
```

---

<details>
<summary> 🧠 Motivation </summary>

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. 

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.

Another key goal of this project is to facilitate the orchestration of wider system installation for **`pipx` CLI tools** for additional touch points, like context menus and widgets.

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**.

</details>

---

<details>
<summary> 🚀 Features </summary>

  * **Definitive Environment Checks:** Rigorous checks catered to Termux and iSH (iOS Alpine). Accurate, typical modern detection for Windows, macOS (Apple), Linux, FreeBSD, Android.
  * **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).
  * **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.
  * **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).

</details>

---

<details>
<summary> 📚 Function Reference </summary>

### OS and Environment Checking

Key question: "What is this running on?"

| Function | Description |
| :--- | :--- |
| `on_windows()` | Returns `True` on Windows. |
| `on_apple()` | Returns `True` on macOS (Darwin). |
| `on_linux()` | Returns `True` on Linux in general. |
| `on_wsl()` | Returns `True` if running inside Windows Subsystem for Linux (WSL or WSL2). |
| `on_termux()` | Returns `True` if running in the Termux Android environment. |
| `on_freebsd()` | Returns `True` on FreeBSD. |
| `on_ish_alpine()` | Returns `True` if running in the iSH Alpine Linux iOS emulator. |
| `on_android()` | Returns `True` on any Android-based Linux environment. |
| `on_pydroid()` | Returns `True` Return True if running under the Pydroid 3 Android app (other versions untested). |
| `in_repl()` | Returns `True` is the user is currently in a Python REPL; hasattr(sys,'ps1'). |

### Packaging and Build Checking

Key question: "What is the character of my executable or my build state?"

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.

| Function | Description |
| :--- | :--- |
| `as_frozen()` | Returns `True` if the script is running as a standalone executable (any bundler). |
| `as_pyinstaller()` | Returns `True` if the script is frozen and generated by PyInstaller (has `_MEIPASS`). |
| `is_python_script(path=None)` | Returns `True` if the script or specified path is a Python source file (.py). |
| `is_pipx(path=None)` | Returns `True` if the script or specified path is from a pipx-managed virtual environment. |
| `is_elf(path=None)` | Returns `True` if the script or specified path is an ELF binary (Linux standalone executable, non-pipx). |
| `is_pyz(path=None)` | Returns `True` if the script or specified path is a Python zipapp (.pyz, non-pipx). |
| `is_windows_portable_executable(path=None)` | Returns `True` if the script or specified path is a Windows PE binary (MZ header, non-pipx). |
| `is_macos_executable(path=None)` | Returns `True` if the script or specified path is a macOS Mach-O binary (non-pipx). |

### Capability Checking

Key Question: "What could I do next?"

| Function | Description |
| :--- | :--- |
| `tkinter_is_available()` | Checks if Tkinter is imported and can successfully create a window. |
| `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`. |
| `matplotlib_is_available_for_headless_image_export()` | Checks for Matplotlib and its Agg backend, required for saving images without a GUI. |
| `interactive_terminal_is_available()` | Checks if standard input and output streams are connected to a TTY (allows safe use of interactive prompts). |
| `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). 	|

### Utility

| Function | Description |
| :--- | :--- |
| `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. |
| `interp_path()` | Returns the path to the Python interpreter binary (sys.executable). Returns empty string if unavailable. |
| `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. |

</details>

---

<details>
<summary> 💻 Usage Examples </summary>

The module exposes all detection functions directly for easy access.

### 0\. Example of PyHabitat in Action

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.

### 1\. Running the Environment Report

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.

```bash
# In the terminal
python -m pyhabitat
```

```python
# In the Python REPL
import pyhabitat as ph
ph.main()
```

### 2\. Checking Environment and Build Type

```python
from pyhabitat import on_termux, on_windows, is_pipx, is_python_script, as_frozen

if is_pipx():
    print("Running inside a pipx virtual environment. This is not a standalone binary.")

if as_frozen():
    print("Running as a frozen executable (PyInstaller, cx_Freeze, etc.).")

if is_python_script():
    print("Running as a Python source script (.py).")

if on_termux(): 
	# Expected cases: 
	#- pkg install python-numpy python-cryptography
	#- Avoiding matplotlib unless the user explicitly sets termux_has_gui=True in matplotlib_is_available_for_gui_plotting().
	#- Auto-selection of 'termux-open-url' and 'xdg-open' in logic.
	#- Installation on the system, like orchestrating the construction of Termux Widget entries in ~/.shortcuts.
    print("Running in the Termux environment on Android.")
    
if on_windows():
    print("Running on Windows.")
```

### 3\. Checking GUI and Plotting Availability

Use these functions to determine if you can show an interactive plot or if you must save an image file.

```python
from pyhabitat import matplotlib_is_available_for_gui_plotting, matplotlib_is_available_for_headless_image_export

if matplotlib_is_available_for_gui_plotting():
    # We can safely call plt.show()
    print("GUI plotting is available! Using TkAgg backend.")
    import matplotlib.pyplot as plt
    plt.figure()
    plt.show()

elif matplotlib_is_available_for_headless_image_export():
    # We must save the plot to a file or buffer
    print("GUI unavailable, but headless image export is possible.")
    # Code to use 'Agg' backend and save to disk...
    
else:
    print("Matplotlib is not installed or the environment is too restrictive for plotting.")
```

### 4\. Text Editing

Use this function to open a text file for editing. 
Ideal use case: Edit a configuration file, if prompted by a CLI command like 'config --textedit'.

```python
from pathlib import Path
import pyhabitat as ph

ph.edit_textfile(path=Path('./config.json'))
```
</details>

---

## 🤝 Contributing

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.

## 📄 License

This project is licensed under the MIT License. See the LICENSE file for details.
