pyhabitat 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of pyhabitat might be problematic. Click here for more details.

pyhabitat/__init__.py ADDED
@@ -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,122 @@
1
+ Metadata-Version: 2.4
2
+ Name: pyhabitat
3
+ Version: 0.1.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 Robust Environment and Build Introspection Library for Python
20
+
21
+ **`pyhabitat`** is a focused, lightweight library designed to accurately and securely determine the execution context of a running Python script. It provides definitive checks for the Operating System (OS), common container/emulation environments (Termux, iSH), build states (PyInstaller, pipx), and the availability of GUI backends (Matplotlib, Tkinter).
22
+
23
+ Stop writing verbose `sys.platform` and environment variable checks. Instead, use **`pyhabitat`** to implement architectural logic in your code.
24
+
25
+ ## 🚀 Features
26
+
27
+ * **Definitive Environment Checks:** Accurate detection for Windows, macOS (Apple), Linux, FreeBSD, Android (general), Termux, and iSH (iOS Alpine).
28
+ * **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).
29
+ * **Build/Packaging Detection:** Reliable detection of standalone executables built by tools like PyInstaller, and, crucially, correct identification and exclusion of pipx-managed virtual environments.
30
+ * **Executable Type Inspection:** Uses file magic numbers (ELF and MZ) to confirm if the running script is a monolithic, frozen binary (non-pipx).
31
+
32
+ ## 📦 Installation
33
+
34
+ ```bash
35
+ pip install pyhabitat
36
+ ```
37
+
38
+ ## 💻 Usage Examples
39
+
40
+ The module exposes all detection functions directly for easy access.
41
+
42
+ ### 1\. Checking Environment and Build Type
43
+
44
+ ```python
45
+ from pyhabitat import is_termux, is_windows, is_pipx, is_frozen
46
+
47
+ if is_pipx():
48
+ print("Running inside a pipx virtual environment. This is not a standalone binary.")
49
+
50
+ elif is_frozen():
51
+ print("Running as a frozen executable (PyInstaller, cx_Freeze, etc.).")
52
+
53
+ elif is_termux():
54
+ print("Running in the Termux environment on Android.")
55
+
56
+ elif is_windows():
57
+ print("Running on Windows.")
58
+ ```
59
+
60
+ ### 2\. Checking GUI and Plotting Availability
61
+
62
+ Use these functions to determine if you can show an interactive plot or if you must save an image file.
63
+
64
+ ```python
65
+ from pyhabitat import matplotlib_is_available_for_gui_plotting, matplotlib_is_available_for_headless_image_export
66
+
67
+ if matplotlib_is_available_for_gui_plotting():
68
+ # We can safely call plt.show()
69
+ print("GUI plotting is available! Using TkAgg backend.")
70
+ import matplotlib.pyplot as plt
71
+ plt.figure()
72
+ plt.show()
73
+
74
+ elif matplotlib_is_available_for_headless_image_export():
75
+ # We must save the plot to a file or buffer
76
+ print("GUI unavailable, but headless image export is possible.")
77
+ # Code to use 'Agg' backend and save to disk...
78
+
79
+ else:
80
+ print("Matplotlib is not installed or the environment is too restrictive for plotting.")
81
+ ```
82
+
83
+ ## 📚 API Reference
84
+
85
+ All functions are Boolean checks and are cached after the first call (except for functions that take arguments, like matplotlib\_is\_available\_for\_gui\_plotting).
86
+
87
+ ### OS and Environment
88
+
89
+ | Function | Description |
90
+ | :--- | :--- |
91
+ | `is_windows()` | Returns `True` on Windows. |
92
+ | `is_apple()` | Returns `True` on macOS (Darwin). |
93
+ | `is_linux()` | Returns `True` on general Linux (not Termux/iSH). |
94
+ | `is_termux()` | Returns `True` if running in the Termux Android environment. |
95
+ | `is_ish_alpine()` | Returns `True` if running in the iSH Alpine Linux iOS emulator. |
96
+ | `is_android()` | Returns `True` on any Android-based Linux environment. |
97
+
98
+ ### Build and Packaging
99
+
100
+ | Function | Description |
101
+ | :--- | :--- |
102
+ | `is_frozen()` | Returns `True` if the script is running as a standalone executable (any bundler). |
103
+ | `is_pipx()` | Returns `True` if running from a pipx managed virtual environment. |
104
+ | `is_elf()` | Checks if the executable is an ELF binary (Linux standalone executable), excluding pipx. |
105
+ | `is_windows_portable_executable()` | Checks if the executable is a Windows PE binary (MZ header), excluding pipx. |
106
+
107
+ ### Capabilities
108
+
109
+ | Function | Description |
110
+ | :--- | :--- |
111
+ | `tkinter_is_available()` | Checks if Tkinter is imported and can successfully create a window. |
112
+ | `matplotlib_is_available_for_gui_plotting(termux_has_gui=False)` | Checks for Matplotlib and its TkAgg backend, required for interactive plotting. |
113
+ | `matplotlib_is_available_for_headless_image_export()` | Checks for Matplotlib and its Agg backend, required for saving images without a GUI. |
114
+ | `is_interactive_terminal()` | Checks if standard input and output streams are connected to a TTY (allows safe use of interactive prompts). |
115
+
116
+ ## 🤝 Contributing
117
+
118
+ 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.
119
+
120
+ ## 📄 License
121
+
122
+ This project is licensed under the MIT License. See the LICENSE file for details.
@@ -0,0 +1,7 @@
1
+ pyhabitat/__init__.py,sha256=ti7a8v4pTunZlC6KYZNb_LV2qy7JPw62SzGDPVJ3WuA,336
2
+ pyhabitat/environment.py,sha256=GKdTC3CIcTG2BIt4fUKnRy2hZaaGRxWucC4asQRdcyU,18046
3
+ pyhabitat-0.1.0.dist-info/licenses/LICENSE,sha256=D4fg30ctUGnCJlWu3ONv5-V8JE1v3ctakoJTcVjsJlg,1072
4
+ pyhabitat-0.1.0.dist-info/METADATA,sha256=vR6StbVAlfrOFlUl75K1_dTsC-tjyxva_6fdlOTLCgg,5429
5
+ pyhabitat-0.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
6
+ pyhabitat-0.1.0.dist-info/top_level.txt,sha256=zXYK44Qu8EqxUETREvd2diMUaB5JiGRErkwFaoLQnnI,10
7
+ pyhabitat-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -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.
@@ -0,0 +1 @@
1
+ pyhabitat