pyhabitat 1.0.16__py3-none-any.whl → 1.0.18__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 CHANGED
@@ -4,23 +4,27 @@ 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,
27
+ main,
24
28
  )
25
29
 
26
30
  # Optional: Set __all__ for explicit documentation and cleaner imports
@@ -28,21 +32,25 @@ __all__ = [
28
32
  'matplotlib_is_available_for_gui_plotting',
29
33
  'matplotlib_is_available_for_headless_image_export',
30
34
  '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',
35
+ 'in_repl',
36
+ 'on_termux',
37
+ 'on_freebsd',
38
+ 'on_linux',
39
+ 'on_android',
40
+ 'on_windows',
41
+ 'on_apple',
42
+ 'on_ish_alpine',
43
+ 'as_pyinstaller',
44
+ 'as_frozen',
40
45
  'is_elf',
41
46
  'is_pyz',
42
47
  'is_windows_portable_executable',
43
48
  'is_macos_executable',
44
49
  'is_pipx',
50
+ 'is_python_script',
45
51
  'interactive_terminal_is_available',
46
52
  'web_browser_is_available',
47
53
  'edit_textfile',
54
+ 'interp_path',
55
+ 'main',
48
56
  ]
pyhabitat/__main__.py ADDED
@@ -0,0 +1,4 @@
1
+ from .cli import run_cli
2
+
3
+ if __name__ == "__main__":
4
+ run_cli()
@@ -0,0 +1,72 @@
1
+ #from .cli import run_cli
2
+
3
+ from .environment import (
4
+ in_repl,
5
+ on_termux,
6
+ on_windows,
7
+ on_apple,
8
+ on_linux,
9
+ on_ish_alpine,
10
+ on_android,
11
+ on_freebsd,
12
+ is_elf,
13
+ is_windows_portable_executable,
14
+ is_macos_executable,
15
+ is_pyz,
16
+ is_pipx,
17
+ is_python_script,
18
+ as_frozen,
19
+ as_pyinstaller,
20
+ interp_path,
21
+ tkinter_is_available,
22
+ matplotlib_is_available_for_gui_plotting,
23
+ matplotlib_is_available_for_headless_image_export,
24
+ web_browser_is_available,
25
+ interactive_terminal_is_available
26
+ )
27
+
28
+ def main():
29
+ print("PyHabitat Environment Report")
30
+ print("===========================")
31
+ print("\nInterpreter Checks // Based on sys.executable()")
32
+ print("-----------------------------")
33
+ print(f"interp_path(): {interp_path()}")
34
+ print(f"is_elf(interp_path()): {is_elf(interp_path())}")
35
+ print(f"is_windows_portable_executable(interp_path()): {is_windows_portable_executable(interp_path())}")
36
+ print(f"is_macos_executable(interp_path()): {is_macos_executable(interp_path())}")
37
+ print(f"is_pyz(interp_path()): {is_pyz(interp_path())}")
38
+ print(f"is_pipx(interp_path()): {is_pipx(interp_path())}")
39
+ print(f"is_python_script(interp_path()): {is_python_script(interp_path())}")
40
+ print("\nCurrent Environment Check // Based on sys.argv[0]")
41
+ print("-----------------------------")
42
+ print(f"is_elf(): {is_elf()}")
43
+ print(f"is_windows_portable_executable(): {is_windows_portable_executable()}")
44
+ print(f"is_macos_executable(): {is_macos_executable()}")
45
+ print(f"is_pyz(): {is_pyz()}")
46
+ print(f"is_pipx(): {is_pipx()}")
47
+ print(f"is_python_script(): {is_python_script()}")
48
+ print(f"\nCurrent Build Checks // Based on hasattr(sys,..) and getattr(sys,..)")
49
+ print("------------------------------")
50
+ print(f"in_repl(): {in_repl()}")
51
+ print(f"as_frozen(): {as_frozen()}")
52
+ print(f"as_pyinstaller(): {as_pyinstaller()}")
53
+ print("\nOperating System Checks // Based on platform.system()")
54
+ print("------------------------------")
55
+ print(f"on_termux(): {on_termux()}")
56
+ print(f"on_windows(): {on_windows()}")
57
+ print(f"on_apple(): {on_apple()}")
58
+ print(f"on_linux(): {on_linux()}")
59
+ print(f"on_ish_alpine(): {on_ish_alpine()}")
60
+ print(f"on_android(): {on_android()}")
61
+ print(f"on_freebsd(): {on_freebsd()}")
62
+ print("\nCapability Checks")
63
+ print("-------------------------")
64
+ print(f"tkinter_is_available(): {tkinter_is_available()}")
65
+ print(f"matplotlib_is_available_for_gui_plotting(): {matplotlib_is_available_for_gui_plotting()}")
66
+ print(f"matplotlib_is_available_for_headless_image_export(): {matplotlib_is_available_for_headless_image_export()}")
67
+ print(f"web_browser_is_available(): {web_browser_is_available()}")
68
+ print(f"interactive_terminal_is_available(): {interactive_terminal_is_available()}")
69
+
70
+ if __name__ == "__main__":
71
+ main()
72
+ #run_cli()
pyhabitat/cli.py ADDED
@@ -0,0 +1,22 @@
1
+ import argparse
2
+ from pathlib import Path
3
+ from . import main
4
+
5
+ def run_cli():
6
+ """Parse CLI arguments and run the pyhabitat environment report."""
7
+ parser = argparse.ArgumentParser(
8
+ description="PyHabitat: Python environment and build introspection"
9
+ )
10
+ parser.add_argument(
11
+ "--path",
12
+ type=str,
13
+ default=None,
14
+ help="Path to a script or binary to inspect (defaults to sys.argv[0])",
15
+ )
16
+ parser.add_argument(
17
+ "--debug",
18
+ action="store_true",
19
+ help="Enable verbose debug output",
20
+ )
21
+ args = parser.parse_args()
22
+ main(path=Path(args.path) if args.path else None, debug=args.debug)
pyhabitat/environment.py CHANGED
@@ -13,12 +13,40 @@ from pathlib import Path
13
13
  import subprocess
14
14
  import io
15
15
  import zipfile
16
+ import logging
17
+
18
+ __all__ = [
19
+ 'matplotlib_is_available_for_gui_plotting',
20
+ 'matplotlib_is_available_for_headless_image_export',
21
+ 'tkinter_is_available',
22
+ 'on_termux',
23
+ 'on_freebsd',
24
+ 'on_linux',
25
+ 'on_android',
26
+ 'on_windows',
27
+ 'on_apple',
28
+ 'on_ish_alpine',
29
+ 'as_pyinstaller',
30
+ 'as_frozen',
31
+ 'is_elf',
32
+ 'is_pyz',
33
+ 'is_windows_portable_executable',
34
+ 'is_macos_executable',
35
+ 'is_pipx',
36
+ 'interactive_terminal_is_available',
37
+ 'web_browser_is_available',
38
+ 'edit_textfile',
39
+ 'in_repl',
40
+ 'interp_path',
41
+ 'main',
42
+ ]
16
43
 
17
44
  # Global cache for tkinter and matplotlib (mpl) availability
18
45
  _TKINTER_AVAILABILITY: bool | None = None
19
46
  _MATPLOTLIB_EXPORT_AVAILABILITY: bool | None = None
20
47
  _MATPLOTLIB_WINDOWED_AVAILABILITY: bool | None = None
21
48
 
49
+
22
50
  # --- GUI CHECKS ---
23
51
  def matplotlib_is_available_for_gui_plotting(termux_has_gui=False):
24
52
  """Check if Matplotlib is available AND can use a GUI backend for a popup window."""
@@ -29,7 +57,7 @@ def matplotlib_is_available_for_gui_plotting(termux_has_gui=False):
29
57
 
30
58
  # 1. Termux exclusion check (assume no X11/GUI)
31
59
  # Exclude Termux UNLESS the user explicitly provides termux_has_gui=True.
32
- if is_termux() and not termux_has_gui:
60
+ if on_termux() and not termux_has_gui:
33
61
  _MATPLOTLIB_WINDOWED_AVAILABILITY = False
34
62
  return False
35
63
 
@@ -118,7 +146,7 @@ def tkinter_is_available() -> bool:
118
146
  return False
119
147
 
120
148
  # --- ENVIRONMENT AND OPERATING SYSTEM CHECKS ---
121
- def is_termux() -> bool:
149
+ def on_termux() -> bool:
122
150
  """Detect if running in Termux environment on Android, based on Termux-specific environmental variables."""
123
151
 
124
152
  if platform.system() != 'Linux':
@@ -145,22 +173,22 @@ def is_termux() -> bool:
145
173
 
146
174
  return False
147
175
 
148
- def is_freebsd() -> bool:
176
+ def on_freebsd() -> bool:
149
177
  """Detect if running on FreeBSD."""
150
178
  return platform.system() == 'FreeBSD'
151
179
 
152
- def is_linux():
180
+ def on_linux():
153
181
  """Detect if running on Linux."""
154
182
  return platform.system() == 'Linux'
155
183
 
156
- def is_android() -> bool:
184
+ def on_android() -> bool:
157
185
  """
158
186
  Detect if running on Android.
159
187
 
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().
188
+ Note: The on_termux() function is more robust and safe for Termux.
189
+ Checking for Termux with on_termux() does not require checking for Android with on_android().
162
190
 
163
- is_android() will be True on:
191
+ on_android() will be True on:
164
192
  - Sandboxed IDE's:
165
193
  - Pydroid3
166
194
  - QPython
@@ -170,7 +198,7 @@ def is_android() -> bool:
170
198
  - UserLand
171
199
  - AnLinux
172
200
 
173
- is_android() will be False on:
201
+ on_android() will be False on:
174
202
  - Full Virtual Machines:
175
203
  - VirtualBox
176
204
  - VMware
@@ -181,15 +209,15 @@ def is_android() -> bool:
181
209
  return False
182
210
  return "android" in platform.platform().lower()
183
211
 
184
- def is_windows() -> bool:
212
+ def on_windows() -> bool:
185
213
  """Detect if running on Windows."""
186
214
  return platform.system() == 'Windows'
187
215
 
188
- def is_apple() -> bool:
216
+ def on_apple() -> bool:
189
217
  """Detect if running on Apple."""
190
218
  return platform.system() == 'Darwin'
191
219
 
192
- def is_ish_alpine() -> bool:
220
+ def on_ish_alpine() -> bool:
193
221
  """Detect if running in iSH Alpine environment on iOS."""
194
222
  # platform.system() usually returns 'Linux' in iSH
195
223
 
@@ -208,16 +236,29 @@ def is_ish_alpine() -> bool:
208
236
 
209
237
  return False
210
238
 
239
+ def in_repl() -> bool:
240
+ """
241
+ Detects if the code is running in the Python interactive REPL (e.g., when 'python' is typed in a console).
242
+
243
+ This function specifically checks for the Python REPL by verifying the presence of the interactive
244
+ prompt (`sys.ps1`). It returns False for other interactive terminal scenarios, such as running a
245
+ PyInstaller binary in a console.
246
+
247
+ Returns:
248
+ bool: True if running in the Python REPL; False otherwise.
249
+ """
250
+ return hasattr(sys, 'ps1')
251
+
211
252
 
212
253
  # --- BUILD AND EXECUTABLE CHECKS ---
213
254
 
214
- def is_pyinstaller():
255
+ def as_pyinstaller():
215
256
  """Detects if the Python script is running as a 'frozen' in the course of generating a PyInstaller binary executable."""
216
257
  # If the app is frozen AND has the PyInstaller-specific temporary folder path
217
- return is_frozen() and hasattr(sys, '_MEIPASS')
258
+ return as_frozen() and hasattr(sys, '_MEIPASS')
218
259
 
219
260
  # The standard way to check for a frozen state:
220
- def is_frozen():
261
+ def as_frozen():
221
262
  """
222
263
  Detects if the Python script is running as a 'frozen' (standalone)
223
264
  executable created by a tool like PyInstaller, cx_Freeze, or Nuitka.
@@ -235,109 +276,78 @@ def is_frozen():
235
276
  """
236
277
  return getattr(sys, 'frozen', False)
237
278
 
238
- def is_elf(exec_path : Path = None, debug=False) -> bool:
279
+ # --- Binary Characteristic Checks ---
280
+ def is_elf(exec_path: Path | str | None = None, debug: bool = False) -> bool:
239
281
  """Checks if the currently running executable (sys.argv[0]) is a standalone PyInstaller-built ELF binary."""
240
282
  # If it's a pipx installation, it is not the monolithic binary we are concerned with here.
241
-
242
- if exec_path is None:
243
- exec_path = Path(sys.argv[0]).resolve()
244
- if debug:
245
- print(f"exec_path = {exec_path}")
246
- if is_pipx():
283
+ exec_path, is_valid = _check_executable_path(exec_path, debug)
284
+ if not is_valid:
247
285
  return False
248
286
 
249
- # Check if the file exists and is readable
250
- if not exec_path.is_file():
251
- return False
252
-
253
287
  try:
254
288
  # Check the magic number: The first four bytes of an ELF file are 0x7f, 'E', 'L', 'F' (b'\x7fELF').
255
289
  # This is the most reliable way to determine if the executable is a native binary wrapper (like PyInstaller's).
256
290
  with open(exec_path, 'rb') as f:
257
291
  magic_bytes = f.read(4)
258
-
292
+ if debug:
293
+ logging.debug(f"Magic bytes: {magic_bytes}")
259
294
  return magic_bytes == b'\x7fELF'
260
295
  except Exception:
261
- # Handle exceptions like PermissionError, IsADirectoryError, etc.
296
+ if debug:
297
+ logging.debug("False (Exception during file check)")
262
298
  return False
263
299
 
264
- def is_pyz(exec_path: Path=None, debug=False) -> bool:
300
+ def is_pyz(exec_path: Path | str | None = None, debug: bool = False) -> bool:
265
301
  """Checks if the currently running executable (sys.argv[0]) is a PYZ zipapp ."""
302
+
266
303
  # If it's a pipx installation, it is not the monolithic binary we are concerned with here.
267
- if exec_path is None:
268
- exec_path = Path(sys.argv[0]).resolve()
269
- if debug:
270
- print(f"exec_path = {exec_path}")
271
-
272
- if is_pipx():
304
+ exec_path, is_valid = _check_executable_path(exec_path, debug)
305
+ if not is_valid:
273
306
  return False
274
307
 
275
308
  # Check if the extension is PYZ
276
309
  if not str(exec_path).endswith(".pyz"):
310
+ if debug:
311
+ logging.debug("False (Not a .pyz file)")
277
312
  return False
278
-
279
- if not _check_if_zip():
313
+
314
+ if not _check_if_zip(exec_path):
315
+ if debug:
316
+ logging.debug("False (Not a valid ZIP file)")
280
317
  return False
281
318
 
282
- def is_windows_portable_executable(exec_path: Path = None, debug=False) -> bool:
319
+ return True
320
+
321
+
322
+ def is_windows_portable_executable(exec_path: Path | str | None = None, debug: bool = False) -> bool:
283
323
  """
284
- Checks if the currently running executable (sys.argv[0]) is a
285
- Windows Portable Executable (PE) binary, and explicitly excludes
286
- pipx-managed environments.
324
+ Checks if the specified path or sys.argv[0] is a Windows Portable Executable (PE) binary.
287
325
  Windows Portable Executables include .exe, .dll, and other binaries.
288
326
  The standard way to check for a PE is to look for the MZ magic number at the very beginning of the file.
289
327
  """
290
- # 1. Determine execution path
291
- if exec_path is None:
292
- exec_path = Path(sys.argv[0]).resolve()
293
-
294
- if debug:
295
- print(f"DEBUG: Checking executable path: {exec_path}")
296
-
297
- # 2. Exclude pipx environments immediately
298
- if is_pipx():
299
- if debug: print("DEBUG: is_exe_non_pipx: False (is_pipx is True)")
300
- return False
301
-
302
- # 3. Perform file checks
303
- if not exec_path.is_file():
304
- if debug: print("DEBUG: is_exe_non_pipx: False (Not a file)")
328
+ exec_path, is_valid = _check_executable_path(exec_path, debug)
329
+ if not is_valid:
305
330
  return False
306
-
307
331
  try:
308
- # Check the magic number: All Windows PE files (EXE, DLL, etc.)
309
- # start with the two-byte header b'MZ' (for Mark Zbikowski).
310
332
  with open(exec_path, 'rb') as f:
311
333
  magic_bytes = f.read(2)
312
-
313
- is_pe = magic_bytes == b'MZ'
314
-
315
- if debug:
316
- print(f"DEBUG: Magic bytes: {magic_bytes}")
317
- print(f"DEBUG: is_exe_non_pipx: {is_pe} (Non-pipx check)")
318
-
319
- return is_pe
320
-
334
+ if debug:
335
+ logging.debug(f"Magic bytes: {magic_bytes}")
336
+ return magic_bytes == b'MZ'
321
337
  except Exception as e:
322
- if debug: print(f"DEBUG: is_exe_non_pipx: Error during file check: {e}")
323
- # Handle exceptions like PermissionError, IsADirectoryError, etc.
338
+ if debug:
339
+ logging.debug(f"False (Error during file check: {e})")
324
340
  return False
325
-
326
- def is_macos_executable(exec_path: Path = None, debug=False) -> bool:
341
+
342
+ def is_macos_executable(exec_path: Path | str | None = None, debug: bool = False) -> bool:
327
343
  """
328
344
  Checks if the currently running executable is a macOS/Darwin Mach-O binary,
329
345
  and explicitly excludes pipx-managed environments.
330
346
  """
331
- if exec_path is None:
332
- exec_path = Path(sys.argv[0]).resolve()
333
-
334
- if is_pipx():
335
- if debug: print("DEBUG: is_macos_executable: False (is_pipx is True)")
347
+ exec_path, is_valid = _check_executable_path(exec_path, debug)
348
+ if not is_valid:
336
349
  return False
337
350
 
338
- if not exec_path.is_file():
339
- return False
340
-
341
351
  try:
342
352
  # Check the magic number: Mach-O binaries start with specific 4-byte headers.
343
353
  # Common ones are: b'\xfe\xed\xfa\xce' (32-bit) or b'\xfe\xed\xfa\xcf' (64-bit)
@@ -355,68 +365,128 @@ def is_macos_executable(exec_path: Path = None, debug=False) -> bool:
355
365
  is_macho = magic_bytes in MACHO_MAGIC
356
366
 
357
367
  if debug:
358
- print(f"DEBUG: is_macos_executable: {is_macho} (Non-pipx check)")
368
+ logging.debug(f"Magic bytes: {magic_bytes}")
359
369
 
360
370
  return is_macho
361
371
 
362
372
  except Exception:
373
+ if debug:
374
+ logging.debug("False (Exception during file check)")
363
375
  return False
364
376
 
365
- def is_pipx(debug=False) -> bool:
377
+ def is_pipx(exec_path: Path | str | None = None, debug: bool = False) -> bool:
366
378
  """Checks if the executable is running from a pipx managed environment."""
367
- try:
368
- # Helper for case-insensitivity on Windows
369
- def normalize_path(p: Path) -> str:
370
- return str(p).lower()
371
-
372
- exec_path = Path(sys.argv[0]).resolve()
379
+ exec_path, is_valid = _check_executable_path(exec_path, debug, check_pipx=False)
380
+ if not is_valid:
381
+ return False
373
382
 
374
- # This is the path to the interpreter running the script (e.g., venv/bin/python)
375
- # In a pipx-managed execution, this is the venv python.
383
+ try:
376
384
  interpreter_path = Path(sys.executable).resolve()
377
385
  pipx_bin_path, pipx_venv_base_path = _get_pipx_paths()
378
386
  # Normalize paths for comparison
379
- norm_exec_path = normalize_path(exec_path)
380
- norm_interp_path = normalize_path(interpreter_path)
387
+ norm_exec_path = str(exec_path).lower()
388
+ norm_interp_path = str(interpreter_path).lower()
381
389
 
382
390
  if debug:
383
- # --- DEBUGGING OUTPUT ---
384
- print(f"DEBUG: EXEC_PATH: {exec_path}")
385
- print(f"DEBUG: INTERP_PATH: {interpreter_path}")
386
- print(f"DEBUG: PIPX_BIN_PATH: {pipx_bin_path}")
387
- print(f"DEBUG: PIPX_VENV_BASE: {pipx_venv_base_path}")
388
- print(f"DEBUG: Check B result: {normalize_path(interpreter_path).startswith(normalize_path(pipx_venv_base_path))}")
389
- # ------------------------
390
-
391
- # 1. Signature Check (Most Robust): Look for the unique 'pipx/venvs' string.
392
- # This is a strong check for both the executable path (your discovery)
393
- # and the interpreter path (canonical venv location).
391
+ logging.debug(f"EXEC_PATH: {exec_path}")
392
+ logging.debug(f"INTERP_PATH: {interpreter_path}")
393
+ logging.debug(f"PIPX_BIN_PATH: {pipx_bin_path}")
394
+ logging.debug(f"PIPX_VENV_BASE: {pipx_venv_base_path}")
395
+ logging.debug(f"Check B result: {norm_interp_path.startswith(str(pipx_venv_base_path).lower())}")
396
+
394
397
  if "pipx/venvs" in norm_exec_path or "pipx/venvs" in norm_interp_path:
395
- if debug: print("is_pipx: True (Signature Check)")
398
+ if debug:
399
+ logging.debug("True (Signature Check)")
396
400
  return True
397
401
 
398
- # 2. Targeted Venv Check: The interpreter's path starts with the PIPX venv base.
399
- # This is a canonical check if the signature check is somehow missed.
400
- if norm_interp_path.startswith(normalize_path(pipx_venv_base_path)):
401
- if debug: print("is_pipx: True (Interpreter Base Check)")
402
+ if norm_interp_path.startswith(str(pipx_venv_base_path).lower()):
403
+ if debug:
404
+ logging.debug("True (Interpreter Base Check)")
402
405
  return True
403
-
404
- # 3. Targeted Executable Check: The executable's resolved path starts with the PIPX venv base.
405
- # This is your key Termux discovery, confirming the shim resolves into the venv.
406
- if norm_exec_path.startswith(normalize_path(pipx_venv_base_path)):
407
- if debug: print("is_pipx: True (Executable Base Check)")
408
- return True
409
406
 
410
- if debug: print("is_pipx: False")
407
+ if norm_exec_path.startswith(str(pipx_venv_base_path).lower()):
408
+ if debug:
409
+ logging.debug("True (Executable Base Check)")
410
+ return True
411
+
412
+ if debug:
413
+ logging.debug("False")
414
+ return False
415
+ except Exception:
416
+ if debug:
417
+ logging.debug("False (Exception during pipx check)")
411
418
  return False
419
+ if debug:
420
+ logging.debug(f"EXEC_PATH: {exec_path}")
421
+ logging.debug(f"INTERP_PATH: {interpreter_path}")
422
+ logging.debug(f"PIPX_BIN_PATH: {pipx_bin_path}")
423
+ logging.debug(f"PIPX_VENV_BASE: {pipx_venv_base_path}")
424
+ logging.debug(f"Check B result: {norm_interp_path.startswith(str(pipx_venv_base_path).lower())}")
425
+
426
+ if "pipx/venvs" in norm_exec_path or "pipx/venvs" in norm_interp_path:
427
+ if debug:
428
+ logging.debug("True (Signature Check)")
429
+ return True
430
+
431
+ if norm_interp_path.startswith(str(pipx_venv_base_path).lower()):
432
+ if debug:
433
+ logging.debug("True (Interpreter Base Check)")
434
+ return True
412
435
 
436
+ if norm_exec_path.startswith(str(pipx_venv_base_path).lower()):
437
+ if debug:
438
+ logging.debug("True (Executable Base Check)")
439
+ return True
440
+
441
+ if debug:
442
+ logging.debug("False")
443
+ return False
413
444
  except Exception:
414
- # Fallback for unexpected path errors
445
+ if debug:
446
+ logging.debug("False (Exception during pipx check)")
415
447
  return False
416
-
448
+ def is_python_script(path: Path | str | None = None, debug: bool = False) -> bool:
449
+ """
450
+ Checks if the specified path or running script is a Python source file (.py).
417
451
 
452
+ By default, checks the running script (`sys.argv[0]`). If a specific `path` is
453
+ provided, checks that path instead. Uses `Path.resolve()` for stable path handling.
418
454
 
419
- # --- TTY CHECK ---
455
+ Args:
456
+ path: Optional; path to the file to check (str or Path). If None, defaults to `sys.argv[0]`.
457
+ debug: If True, prints the path being checked.
458
+
459
+ Returns:
460
+ bool: True if the specified or default path is a Python source file (.py); False otherwise.
461
+ """
462
+ exec_path, is_valid = _check_executable_path(path, debug, check_pipx=False)
463
+ if not is_valid:
464
+ return False
465
+ return exec_path.suffix.lower() == '.py'
466
+
467
+ # --- Interpreter Check ---
468
+
469
+ def interp_path(print_path: bool = False) -> str:
470
+ """
471
+ Returns the path to the Python interpreter binary and optionally prints it.
472
+
473
+ This function wraps `sys.executable` to provide the path to the interpreter
474
+ (e.g., '/data/data/com.termux/files/usr/bin/python3' in Termux or the embedded
475
+ interpreter in a frozen executable). If the path is empty (e.g., in some embedded
476
+ or sandboxed environments), an empty string is returned.
477
+
478
+ Args:
479
+ print_path: If True, prints the interpreter path to stdout.
480
+
481
+ Returns:
482
+ str: The path to the Python interpreter binary, or an empty string if unavailable.
483
+ """
484
+ path = sys.executable
485
+ if print_path:
486
+ print(f"Python interpreter path: {path}")
487
+ return path
488
+
489
+ # --- TTY Check ---
420
490
  def interactive_terminal_is_available():
421
491
  """
422
492
  Check if the script is running in an interactive terminal.
@@ -440,7 +510,7 @@ def web_browser_is_available() -> bool:
440
510
  except webbrowser.Error:
441
511
  # Fallback needed. Check for external launchers.
442
512
  # 2. Termux specific check
443
- if is_termux() and shutil.which("termux-open-url"):
513
+ if on_termux() and shutil.which("termux-open-url"):
444
514
  return True
445
515
  # 3. General Linux check
446
516
  if shutil.which("xdg-open"):
@@ -448,34 +518,42 @@ def web_browser_is_available() -> bool:
448
518
  return False
449
519
 
450
520
  # --- LAUNCH MECHANISMS BASED ON ENVIRONMENT ---
451
- def edit_textfile(filepath) -> None:
452
- #def open_text_file_for_editing(filepath):
521
+ def edit_textfile(path: Path | str | None = None) -> None:
522
+ #def open_text_file_for_editing(path): # defunct function name as of 1.0.16
453
523
  """
454
524
  Opens a file with the environment's default application (Windows, Linux, macOS)
455
525
  or a guaranteed console editor (nano) in constrained environments (Termux, iSH)
456
526
  after ensuring line-ending compatibility.
527
+
528
+ This function is known to fail on PyDroid3, where on_linus() is True but xdg-open
529
+ is not available.
457
530
  """
531
+ if path is None:
532
+ return
533
+
534
+ path = Path(path).resolve()
535
+
458
536
  try:
459
- if is_windows():
460
- os.startfile(filepath)
461
- elif is_termux():
537
+ if on_windows():
538
+ os.startfile(path)
539
+ elif on_termux():
462
540
  # Install dependencies if missing (Termux pkg returns non-zero if already installed, so no check=True)
463
541
  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():
542
+ _run_dos2unix(path)
543
+ subprocess.run(['nano', path])
544
+ elif on_ish_alpine():
467
545
  # Install dependencies if missing (apk returns 0 if already installed, so check=True is safe)
468
546
  subprocess.run(['apk','add', 'dos2unix'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=True)
469
547
  subprocess.run(['apk','add', 'nano'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=True)
470
- _run_dos2unix(filepath)
471
- subprocess.run(['nano', filepath])
548
+ _run_dos2unix(path)
549
+ subprocess.run(['nano', path])
472
550
  # --- 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])
551
+ elif on_linux():
552
+ _run_dos2unix(path) # Safety conversion for user-defined console apps
553
+ subprocess.run(['xdg-open', path])
554
+ elif on_apple():
555
+ _run_dos2unix(path) # Safety conversion for user-defined console apps
556
+ subprocess.run(['open', path])
479
557
  else:
480
558
  print("Unsupported operating system.")
481
559
  except Exception as e:
@@ -484,14 +562,18 @@ def edit_textfile(filepath) -> None:
484
562
  """Why Not Use check=True on Termux:
485
563
  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
564
  """
487
-
488
- def _run_dos2unix(filepath):
565
+
566
+ # --- Helper Functions ---
567
+ def _run_dos2unix(path: Path | str | None = None):
489
568
  """Attempt to run dos2unix, failing silently if not installed."""
569
+
570
+ path = Path(path).resolve()
571
+
490
572
  try:
491
573
  # We rely on shutil.which not being needed, as this is a robust built-in utility on most targets
492
574
  # The command won't raise an exception unless the process itself fails, not just if the utility isn't found.
493
575
  # 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)
576
+ subprocess.run(['dos2unix', path], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
495
577
  except FileNotFoundError:
496
578
  # This will be raised if 'dos2unix' is not on the system PATH
497
579
  pass
@@ -526,11 +608,111 @@ def _get_pipx_paths():
526
608
  return pipx_bin_path, pipx_venv_base.resolve()
527
609
 
528
610
 
529
- def _check_if_zip(file_path: str | Path) -> bool:
611
+ def _check_if_zip(path: Path | str | None) -> bool:
530
612
  """Checks if the file at the given path is a valid ZIP archive."""
613
+ if path is None:
614
+ return False
615
+ path = Path(path).resolve()
616
+
531
617
  try:
532
- return zipfile.is_zipfile(file_path)
618
+ return zipfile.is_zipfile(path)
533
619
  except Exception:
534
620
  # Handle cases where the path might be invalid, or other unexpected errors
535
621
  return False
536
-
622
+
623
+ def _check_executable_path(exec_path: Path | str | None, debug: bool = False, check_pipx: bool = True) -> tuple[Path | None, bool]:
624
+ """Helper function to resolve executable path and perform common checks."""
625
+ if exec_path is None:
626
+ exec_path = Path(sys.argv[0]).resolve() if sys.argv[0] and sys.argv[0] != '-c' else None
627
+ else:
628
+ exec_path = Path(exec_path).resolve()
629
+
630
+ if debug:
631
+ logging.debug(f"Checking executable path: {exec_path}")
632
+
633
+ if exec_path is None:
634
+ if debug:
635
+ logging.debug("False (No valid path)")
636
+ return None, False
637
+
638
+ if check_pipx and is_pipx(exec_path, debug):
639
+ if debug:
640
+ logging.debug("False (is_pipx is True)")
641
+ return exec_path, False
642
+
643
+ if not exec_path.is_file():
644
+ if debug:
645
+ logging.debug("False (Not a file)")
646
+ return exec_path, False
647
+
648
+ return exec_path, True
649
+
650
+ # --- Main Function for report and CLI compatibility ---
651
+
652
+ def main(path=None, debug=False):
653
+ """Print a comprehensive environment report.
654
+
655
+ Args:
656
+ path (Path | str | None): Path to inspect (defaults to sys.argv[0]).
657
+ debug (bool): Enable verbose debug output.
658
+ """
659
+ if debug:
660
+ logging.basicConfig(level=logging.DEBUG)
661
+ logging.getLogger('matplotlib').setLevel(logging.WARNING) # Suppress matplotlib debug logs
662
+ print("PyHabitat Environment Report")
663
+ print("===========================")
664
+ print("\nCurrent Build Checks // Based on hasattr(sys,..) and getattr(sys,..)")
665
+ print("------------------------------")
666
+ print(f"in_repl(): {in_repl()}")
667
+ print(f"as_frozen(): {as_frozen()}")
668
+ print(f"as_pyinstaller(): {as_pyinstaller()}")
669
+ print("\nOperating System Checks // Based on platform.system()")
670
+ print("------------------------------")
671
+ print(f"on_termux(): {on_termux()}")
672
+ print(f"on_windows(): {on_windows()}")
673
+ print(f"on_apple(): {on_apple()}")
674
+ print(f"on_linux(): {on_linux()}")
675
+ print(f"on_ish_alpine(): {on_ish_alpine()}")
676
+ print(f"on_android(): {on_android()}")
677
+ print(f"on_freebsd(): {on_freebsd()}")
678
+ print("\nCapability Checks")
679
+ print("-------------------------")
680
+ print(f"tkinter_is_available(): {tkinter_is_available()}")
681
+ print(f"matplotlib_is_available_for_gui_plotting(): {matplotlib_is_available_for_gui_plotting()}")
682
+ print(f"matplotlib_is_available_for_headless_image_export(): {matplotlib_is_available_for_headless_image_export()}")
683
+ print(f"web_browser_is_available(): {web_browser_is_available()}")
684
+ print(f"interactive_terminal_is_available(): {interactive_terminal_is_available()}")
685
+ print("\nInterpreter Checks // Based on sys.executable()")
686
+ print("-----------------------------")
687
+ print(f"interp_path(): {interp_path()}")
688
+ print(f"is_elf(interp_path()): {is_elf(interp_path(), debug=debug)}")
689
+ print(f"is_windows_portable_executable(interp_path()): {is_windows_portable_executable(interp_path(), debug=debug)}")
690
+ print(f"is_macos_executable(interp_path()): {is_macos_executable(interp_path(), debug=debug)}")
691
+ print(f"is_pyz(interp_path()): {is_pyz(interp_path(), debug=debug)}")
692
+ print(f"is_pipx(interp_path()): {is_pipx(interp_path(), debug=debug)}")
693
+ print(f"is_python_script(interp_path()): {is_python_script(interp_path(), debug=debug)}")
694
+ print("\nCurrent Environment Check // Based on sys.argv[0]")
695
+ print("-----------------------------")
696
+ inspect_path = path if path is not None else (None if sys.argv[0] == '-c' else sys.argv[0])
697
+ logging.debug(f"Inspecting path: {inspect_path}")
698
+ # Early validation of path
699
+ if path is not None:
700
+ path_obj = Path(path)
701
+ if not path_obj.is_file():
702
+ print(f"Error: '{path}' is not a valid file or does not exist.")
703
+ if debug:
704
+ logging.error(f"Invalid path: '{path}' is not a file or does not exist.")
705
+ raise SystemExit(1)
706
+ script_path = None
707
+ if path or (sys.argv[0] and sys.argv[0] != '-c'):
708
+ script_path = Path(path or sys.argv[0]).resolve()
709
+ logging.debug(f"Script path resolved: {script_path}")
710
+ if script_path is not None:
711
+ print(f"is_elf(): {is_elf(script_path, debug=debug)}")
712
+ print(f"is_windows_portable_executable(): {is_windows_portable_executable(script_path, debug=debug)}")
713
+ print(f"is_macos_executable(): {is_macos_executable(script_path, debug=debug)}")
714
+ print(f"is_pyz(): {is_pyz(script_path, debug=debug)}")
715
+ print(f"is_pipx(): {is_pipx(script_path, debug=debug)}")
716
+ print(f"is_python_script(): {is_python_script(script_path, debug=debug)}")
717
+ else:
718
+ print("script_path is None")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyhabitat
3
- Version: 1.0.16
3
+ Version: 1.0.18
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
@@ -8,7 +8,7 @@ Keywords: environment,os-detection,gui,build-system
8
8
  Classifier: Programming Language :: Python :: 3
9
9
  Classifier: Operating System :: OS Independent
10
10
  Classifier: Topic :: System :: Systems Administration
11
- Requires-Python: >=3.8
11
+ Requires-Python: >=3.7
12
12
  Description-Content-Type: text/markdown
13
13
  License-File: LICENSE
14
14
  Dynamic: license-file
@@ -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
 
@@ -0,0 +1,11 @@
1
+ pyhabitat/__init__.py,sha256=GcpHBpfEer9O9dKBUREU1RTd5w0im9ph5ew4bs7jOvY,1228
2
+ pyhabitat/__main__.py,sha256=hhH17lkw-ZalKp9NolnPGwW0KYxbXirspWvhBKNyBks,67
3
+ pyhabitat/__main__stable.py,sha256=UACpHLrr_Rmf0L5dJCEae6kFzLn7dqCqIri68IBnb10,2910
4
+ pyhabitat/cli.py,sha256=vuRczazuumIaJl4Td2VvCR5lXcKKNH90V8Yx0KZUJck,663
5
+ pyhabitat/environment.py,sha256=7yXMCHvTRkfwwQnsgzrDXPDG9m8h8yhI5oCNlJTHWUA,28026
6
+ pyhabitat-1.0.18.dist-info/licenses/LICENSE,sha256=D4fg30ctUGnCJlWu3ONv5-V8JE1v3ctakoJTcVjsJlg,1072
7
+ pyhabitat-1.0.18.dist-info/METADATA,sha256=Xk4blTODZGW6NiuqN5e33SuePkTb-PnNU5pGGzeHXvs,10733
8
+ pyhabitat-1.0.18.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
9
+ pyhabitat-1.0.18.dist-info/entry_points.txt,sha256=409xZ-BrarQJJLtO-aActCGkL0FMhNVi9wsq3u7tRHM,52
10
+ pyhabitat-1.0.18.dist-info/top_level.txt,sha256=zXYK44Qu8EqxUETREvd2diMUaB5JiGRErkwFaoLQnnI,10
11
+ pyhabitat-1.0.18.dist-info/RECORD,,
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ pyhabitat = pyhabitat.cli:run_cli
@@ -1,7 +0,0 @@
1
- pyhabitat/__init__.py,sha256=MiYIzYPqB3o6ZFXQ1C9BV2M2KI17BSq9CA6p8Uc7_h0,1096
2
- pyhabitat/environment.py,sha256=v9e47TdbtMSiljkjr9-lfdUILQu95HLH0SB6k7QIba0,20757
3
- pyhabitat-1.0.16.dist-info/licenses/LICENSE,sha256=D4fg30ctUGnCJlWu3ONv5-V8JE1v3ctakoJTcVjsJlg,1072
4
- pyhabitat-1.0.16.dist-info/METADATA,sha256=APK_RIRIKYUzZ3nuNgcRUvbDhaHZnWXAwvatvPwU_Qg,9001
5
- pyhabitat-1.0.16.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
6
- pyhabitat-1.0.16.dist-info/top_level.txt,sha256=zXYK44Qu8EqxUETREvd2diMUaB5JiGRErkwFaoLQnnI,10
7
- pyhabitat-1.0.16.dist-info/RECORD,,