pyhabitat 1.1.23__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.
- pyhabitat/__init__.py +29 -0
- pyhabitat/__main__.py +6 -0
- pyhabitat/cli.py +140 -0
- pyhabitat/environment.py +1080 -0
- pyhabitat/reporting.py +120 -0
- pyhabitat/system_info.py +131 -0
- pyhabitat/version_info.py +209 -0
- pyhabitat-1.1.23.dist-info/METADATA +226 -0
- pyhabitat-1.1.23.dist-info/RECORD +13 -0
- pyhabitat-1.1.23.dist-info/WHEEL +5 -0
- pyhabitat-1.1.23.dist-info/entry_points.txt +2 -0
- pyhabitat-1.1.23.dist-info/licenses/LICENSE +7 -0
- pyhabitat-1.1.23.dist-info/top_level.txt +1 -0
pyhabitat/reporting.py
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# src/pyhabitat/report.py
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
import sys
|
|
4
|
+
import logging
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from pyhabitat import environment as env
|
|
8
|
+
def report(path=None, debug=False):
|
|
9
|
+
"""Print a comprehensive environment report.
|
|
10
|
+
|
|
11
|
+
Args:
|
|
12
|
+
path (Path | str | None): Path to inspect (defaults to sys.argv[0]).
|
|
13
|
+
debug (bool): Enable verbose debug output.
|
|
14
|
+
"""
|
|
15
|
+
if debug:
|
|
16
|
+
logging.basicConfig(level=logging.DEBUG)
|
|
17
|
+
logging.getLogger('matplotlib').setLevel(logging.WARNING) # Suppress matplotlib debug logs
|
|
18
|
+
print("================================")
|
|
19
|
+
print("======= PyHabitat Report =======")
|
|
20
|
+
print("================================")
|
|
21
|
+
print("\nCurrent Build Checks ")
|
|
22
|
+
print("# // Based on hasattr(sys,..) and getattr(sys,..)")
|
|
23
|
+
print("------------------------------")
|
|
24
|
+
print(f"in_repl(): {env.in_repl()}")
|
|
25
|
+
print(f"as_frozen(): {env.as_frozen()}")
|
|
26
|
+
print(f"as_pyinstaller(): {env.as_pyinstaller()}")
|
|
27
|
+
print("\nOperating System Checks")
|
|
28
|
+
print("# // Based on platform.system()")
|
|
29
|
+
print("------------------------------")
|
|
30
|
+
print(f"on_windows(): {env.on_windows()}")
|
|
31
|
+
print(f"on_macos(): {env.on_macos()}")
|
|
32
|
+
print(f"on_linux(): {env.on_linux()}")
|
|
33
|
+
print(f"on_wsl(): {env.on_wsl()}")
|
|
34
|
+
print(f"on_android(): {env.on_android()}")
|
|
35
|
+
print(f"on_termux(): {env.on_termux()}")
|
|
36
|
+
print(f"on_pydroid(): {env.on_pydroid()}")
|
|
37
|
+
print(f"on_ish_alpine(): {env.on_ish_alpine()}")
|
|
38
|
+
print(f"on_freebsd(): {env.on_freebsd()}")
|
|
39
|
+
print("\nCapability Checks")
|
|
40
|
+
print("-------------------------")
|
|
41
|
+
print(f"tkinter_is_available(): {env.tkinter_is_available()}")
|
|
42
|
+
print(f"matplotlib_is_available_for_gui_plotting(): {env.matplotlib_is_available_for_gui_plotting()}")
|
|
43
|
+
print(f"matplotlib_is_available_for_headless_image_export(): {env.matplotlib_is_available_for_headless_image_export()}")
|
|
44
|
+
print(f"web_browser_is_available(): {env.web_browser_is_available()}")
|
|
45
|
+
print(f"interactive_terminal_is_available(): {env.interactive_terminal_is_available()}")
|
|
46
|
+
print("\nInterpreter Checks")
|
|
47
|
+
print("# // Based on sys.executable()")
|
|
48
|
+
print("-----------------------------")
|
|
49
|
+
print(f"interp_path(): {env.interp_path()}")
|
|
50
|
+
if debug:
|
|
51
|
+
# Do these debug prints once to avoid redundant prints
|
|
52
|
+
# Supress redundant prints explicity using suppress_debug=True,
|
|
53
|
+
# so that only unique information gets printed for each check,
|
|
54
|
+
# even when more than one use the same functions which include debugging logs.
|
|
55
|
+
#print(f"env.check_executable_path(env.interp_path(), debug=True)")
|
|
56
|
+
env.check_executable_path(env.interp_path(), debug=debug)
|
|
57
|
+
#print(f"read_magic_bites(env.interp_path(), debug=True)")
|
|
58
|
+
env.read_magic_bytes(env.interp_path(), debug=debug)
|
|
59
|
+
print(f"is_elf(env.interp_path()): {env.is_elf(env.interp_path(), debug=debug, suppress_debug=True)}")
|
|
60
|
+
print(f"is_windows_portable_executable(env.interp_path()): {env.is_windows_portable_executable(env.interp_path(), debug=debug, suppress_debug=True)}")
|
|
61
|
+
print(f"is_macos_executable(env.interp_path()): {env.is_macos_executable(env.interp_path(), debug=debug, suppress_debug=True)}")
|
|
62
|
+
print(f"is_pyz(env.interp_path()): {env.is_pyz(env.interp_path(), debug=debug, suppress_debug=True)}")
|
|
63
|
+
print(f"is_pipx(env.interp_path()): {env.is_pipx(env.interp_path(), debug=debug, suppress_debug=True)}")
|
|
64
|
+
print(f"is_python_script(env.interp_path()): {env.is_python_script(env.interp_path(), debug=debug, suppress_debug=True)}")
|
|
65
|
+
print("\nCurrent Environment Check")
|
|
66
|
+
print("# // Based on sys.argv[0]")
|
|
67
|
+
print("-----------------------------")
|
|
68
|
+
inspect_path = path if path is not None else (None if sys.argv[0] == '-c' else sys.argv[0])
|
|
69
|
+
logging.debug(f"Inspecting path: {inspect_path}")
|
|
70
|
+
# Early validation of path
|
|
71
|
+
if path is not None:
|
|
72
|
+
path_obj = Path(path)
|
|
73
|
+
if not path_obj.is_file():
|
|
74
|
+
print(f"Error: '{path}' is not a valid file or does not exist.")
|
|
75
|
+
if debug:
|
|
76
|
+
logging.error(f"Invalid path: '{path}' is not a file or does not exist.")
|
|
77
|
+
raise SystemExit(1)
|
|
78
|
+
script_path = None
|
|
79
|
+
if path or (sys.argv[0] and sys.argv[0] != '-c'):
|
|
80
|
+
script_path = Path(path or sys.argv[0]).resolve()
|
|
81
|
+
print(f"sys.argv[0] = {str(sys.argv[0])}")
|
|
82
|
+
if script_path is not None:
|
|
83
|
+
print(f"script_path = {script_path}")
|
|
84
|
+
if debug:
|
|
85
|
+
# Do these debug prints once to avoid redundant prints
|
|
86
|
+
# Supress redundant prints explicity using suppress_debug=True,
|
|
87
|
+
# so that only unique information gets printed for each check,
|
|
88
|
+
# even when more than one use the same functions which include debugging logs.
|
|
89
|
+
#print(f"check_executable_path(script_path, debug=True)")
|
|
90
|
+
env.check_executable_path(script_path, debug=debug)
|
|
91
|
+
#print(f"read_magic_bites(script_path, debug=True)")
|
|
92
|
+
env.read_magic_bytes(script_path, debug=debug)
|
|
93
|
+
print(f"is_elf(): {env.is_elf(script_path, debug=debug, suppress_debug=True)}")
|
|
94
|
+
print(f"is_windows_portable_executable(): {env.is_windows_portable_executable(script_path, debug=debug, suppress_debug=True)}")
|
|
95
|
+
print(f"is_macos_executable(): {env.is_macos_executable(script_path, debug=debug, suppress_debug=True)}")
|
|
96
|
+
print(f"is_pyz(): {env.is_pyz(script_path, debug=debug, suppress_debug=True)}")
|
|
97
|
+
print(f"is_pipx(): {env.is_pipx(script_path, debug=debug, suppress_debug=True)}")
|
|
98
|
+
print(f"is_python_script(): {env.is_python_script(script_path, debug=debug, suppress_debug=True)}")
|
|
99
|
+
else:
|
|
100
|
+
print("Skipping: ")
|
|
101
|
+
print(" is_elf(), ")
|
|
102
|
+
print(" is_windows_portable_executable(), ")
|
|
103
|
+
print(" is_macos_executable(), ")
|
|
104
|
+
print(" is_pyz(), ")
|
|
105
|
+
print(" is_pipx(), ")
|
|
106
|
+
print(" is_python_script(), ")
|
|
107
|
+
print("All False, script_path is None.")
|
|
108
|
+
print("")
|
|
109
|
+
print("=================================")
|
|
110
|
+
print("=== PyHabitat Report Complete ===")
|
|
111
|
+
print("=================================")
|
|
112
|
+
print("")
|
|
113
|
+
interactive = env.in_repl() or sys.flags.interactive
|
|
114
|
+
if not interactive:
|
|
115
|
+
# Keep window open.
|
|
116
|
+
try:
|
|
117
|
+
input("Press Return to Continue...")
|
|
118
|
+
except Exception as e:
|
|
119
|
+
logging.debug("input() failed")
|
|
120
|
+
|
pyhabitat/system_info.py
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# src/pyhabitat/system_info.py
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
import platform
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
import os
|
|
7
|
+
|
|
8
|
+
try:
|
|
9
|
+
import distro # external package, best for Linux detection, but pyhabitat doesnt use external packages
|
|
10
|
+
except ImportError:
|
|
11
|
+
distro = None
|
|
12
|
+
|
|
13
|
+
class SystemInfo:
|
|
14
|
+
"""Detects the current OS, distro, and version information."""
|
|
15
|
+
|
|
16
|
+
def __init__(self):
|
|
17
|
+
self.system = platform.system() # "Windows", "Linux", "Darwin"
|
|
18
|
+
self.release = platform.release()
|
|
19
|
+
self.version = platform.version()
|
|
20
|
+
self.architecture = platform.machine()
|
|
21
|
+
|
|
22
|
+
def detect_linux_distro(self) -> dict:
|
|
23
|
+
"""Return Linux distribution info (if available)."""
|
|
24
|
+
if self.system != "Linux":
|
|
25
|
+
return {}
|
|
26
|
+
|
|
27
|
+
if distro:
|
|
28
|
+
return {
|
|
29
|
+
"id": distro.id(),
|
|
30
|
+
"name": distro.name(),
|
|
31
|
+
"version": distro.version(),
|
|
32
|
+
"like": distro.like(),
|
|
33
|
+
}
|
|
34
|
+
else:
|
|
35
|
+
# fallback to /etc/os-release parsing
|
|
36
|
+
os_release = Path("/etc/os-release")
|
|
37
|
+
if os_release.exists():
|
|
38
|
+
info = {}
|
|
39
|
+
for line in os_release.read_text().splitlines():
|
|
40
|
+
if "=" in line:
|
|
41
|
+
k, v = line.split("=", 1)
|
|
42
|
+
info[k.strip()] = v.strip().strip('"')
|
|
43
|
+
return {
|
|
44
|
+
"id": info.get("ID"),
|
|
45
|
+
"name": info.get("NAME"),
|
|
46
|
+
"version": info.get("VERSION_ID"),
|
|
47
|
+
"like": info.get("ID_LIKE"),
|
|
48
|
+
}
|
|
49
|
+
return {"id": "unknown", "name": "unknown", "version": "unknown"}
|
|
50
|
+
|
|
51
|
+
def detect_android_termux(self) -> bool:
|
|
52
|
+
if "ANDROID_ROOT" in os.environ or "TERMUX_VERSION" in os.environ:
|
|
53
|
+
return True
|
|
54
|
+
if "android" in self.release.lower():
|
|
55
|
+
return True
|
|
56
|
+
return False
|
|
57
|
+
|
|
58
|
+
def get_windows_tag(self) -> str:
|
|
59
|
+
"""Differentiate Windows 10 vs 11 based on build number."""
|
|
60
|
+
release, version, csd, ptype = platform.win32_ver()
|
|
61
|
+
try:
|
|
62
|
+
build_number = int(version.split(".")[-1])
|
|
63
|
+
except Exception:
|
|
64
|
+
build_number = 0
|
|
65
|
+
|
|
66
|
+
if build_number >= 22000:
|
|
67
|
+
return "windows11"
|
|
68
|
+
return "windows10"
|
|
69
|
+
|
|
70
|
+
def get_os_tag(self) -> str:
|
|
71
|
+
"""Return a compact string for use in filenames (e.g. ubuntu22.04)."""
|
|
72
|
+
if self.system == "Windows":
|
|
73
|
+
return self.get_windows_tag()
|
|
74
|
+
|
|
75
|
+
if self.system == "Darwin":
|
|
76
|
+
mac_ver = platform.mac_ver()[0].split(".")[0] or "macos"
|
|
77
|
+
return f"macos{mac_ver}"
|
|
78
|
+
|
|
79
|
+
if self.system == "Linux":
|
|
80
|
+
if self.detect_android_termux():
|
|
81
|
+
return "android"
|
|
82
|
+
|
|
83
|
+
info = self.detect_linux_distro()
|
|
84
|
+
distro_id = info.get("id") or "linux"
|
|
85
|
+
distro_ver = (info.get("version") or "").replace(".", "")
|
|
86
|
+
if distro_ver:
|
|
87
|
+
return f"{distro_id}{info['version']}"
|
|
88
|
+
return distro_id
|
|
89
|
+
|
|
90
|
+
return self.system.lower()
|
|
91
|
+
|
|
92
|
+
def get_arch(self) -> str:
|
|
93
|
+
arch = self.architecture.lower()
|
|
94
|
+
if arch in ("amd64", "x86_64"):
|
|
95
|
+
return "x86_64"
|
|
96
|
+
return self.architecture
|
|
97
|
+
|
|
98
|
+
def to_dict(self) -> dict:
|
|
99
|
+
"""Return a full snapshot of system information."""
|
|
100
|
+
info = {
|
|
101
|
+
"system": self.system,
|
|
102
|
+
"release": self.release,
|
|
103
|
+
"version": self.version,
|
|
104
|
+
"arch": self.architecture,
|
|
105
|
+
"os_tag": self.get_os_tag(),
|
|
106
|
+
}
|
|
107
|
+
if self.system == "Linux" and self.detect_android_termux():
|
|
108
|
+
info["id"] = "android"
|
|
109
|
+
info["name"] = "Android (Termux)"
|
|
110
|
+
elif self.system == "Linux":
|
|
111
|
+
info.update(self.detect_linux_distro())
|
|
112
|
+
elif self.system == "Windows":
|
|
113
|
+
info["win_version"] = platform.win32_ver()
|
|
114
|
+
elif self.system == "Darwin":
|
|
115
|
+
info["mac_ver"] = platform.mac_ver()[0]
|
|
116
|
+
return info
|
|
117
|
+
|
|
118
|
+
def pretty_print(self):
|
|
119
|
+
"""Nicely formatted printout of system info."""
|
|
120
|
+
info = self.to_dict()
|
|
121
|
+
print("--- System Information ---")
|
|
122
|
+
for k, v in info.items():
|
|
123
|
+
print(f"{k:10}: {v}")
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
if __name__ == "__main__":
|
|
127
|
+
sysinfo = SystemInfo()
|
|
128
|
+
sysinfo.pretty_print()
|
|
129
|
+
sysinfo.get_os_tag()
|
|
130
|
+
sysinfo.get_arch()
|
|
131
|
+
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
# src/pyhabitat/version_info
|
|
2
|
+
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
|
|
3
|
+
import sys
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
import re
|
|
6
|
+
|
|
7
|
+
try:
|
|
8
|
+
# Python 3.8+
|
|
9
|
+
from importlib import metadata
|
|
10
|
+
PackageNotFoundError = metadata.PackageNotFoundError
|
|
11
|
+
except (ImportError, AttributeError):
|
|
12
|
+
try:
|
|
13
|
+
# Python 3.7 Backport
|
|
14
|
+
import importlib_metadata as metadata
|
|
15
|
+
PackageNotFoundError = metadata.PackageNotFoundError
|
|
16
|
+
except ImportError:
|
|
17
|
+
metadata = None
|
|
18
|
+
# Define a dummy exception so the code doesn't crash on 3.7
|
|
19
|
+
# if the backport is missing
|
|
20
|
+
class PackageNotFoundError(Exception): pass
|
|
21
|
+
|
|
22
|
+
from .system_info import SystemInfo
|
|
23
|
+
|
|
24
|
+
# -- Versioning --
|
|
25
|
+
PIP_PACKAGE_NAME = "pyhabitat"
|
|
26
|
+
# Auto-detected at build time (fallback)
|
|
27
|
+
FALLBACK_VERSION = "dev"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _running_inside_pyinstaller() -> bool:
|
|
31
|
+
return hasattr(sys, "_MEIPASS")
|
|
32
|
+
|
|
33
|
+
def _read_embedded_version() -> str | None:
|
|
34
|
+
# Check PyInstaller runtime
|
|
35
|
+
if _running_inside_pyinstaller():
|
|
36
|
+
base = Path(sys._MEIPASS) # temp folder where PyInstaller unpacks files
|
|
37
|
+
else:
|
|
38
|
+
base = Path(__file__).parent
|
|
39
|
+
|
|
40
|
+
try:
|
|
41
|
+
return (base / "VERSION").read_text().strip()
|
|
42
|
+
except Exception:
|
|
43
|
+
return None
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def find_pyproject(start: Path) -> Path | None:
|
|
47
|
+
# Look back up to 3 levels: pyhabitat/, src/, project_root/
|
|
48
|
+
# This covers the src-layout structure you showed.
|
|
49
|
+
# Ensure we start at the directory of the file, resolved to absolute path
|
|
50
|
+
current = start.resolve().parent if start.is_file() else start.resolve()
|
|
51
|
+
|
|
52
|
+
# Climb up to 3 levels (package -> src -> project_root)
|
|
53
|
+
for _ in range(4):
|
|
54
|
+
candidate = current / "pyproject.toml"
|
|
55
|
+
if candidate.exists():
|
|
56
|
+
if _ensure_relevant_pyproject_file(candidate):
|
|
57
|
+
return candidate
|
|
58
|
+
# Move up one level for the next iteration
|
|
59
|
+
if current.parent == current: break # Hit root
|
|
60
|
+
current = current.parent
|
|
61
|
+
|
|
62
|
+
return None
|
|
63
|
+
|
|
64
|
+
def _ensure_relevant_pyproject_file(candidate):
|
|
65
|
+
content = candidate.read_text(encoding="utf-8")
|
|
66
|
+
# Ensure this TOML defines 'name = "pyhabitat"'
|
|
67
|
+
# (handles both [project] and [tool.poetry] name definitions)
|
|
68
|
+
name_match = re.search(r'^name\s*=\s*["\']pyhabitat["\']', content, re.MULTILINE)
|
|
69
|
+
poetry_name_match = re.search(r'\[tool\.poetry\].*?name\s*=\s*["\']pyhabitat["\']', content, re.DOTALL)
|
|
70
|
+
|
|
71
|
+
if name_match or poetry_name_match:
|
|
72
|
+
return True
|
|
73
|
+
else:
|
|
74
|
+
return False
|
|
75
|
+
|
|
76
|
+
def get_package_name() -> str:
|
|
77
|
+
# 1. Check pyproject.toml FIRST (Crucial for Build/Dev environments)
|
|
78
|
+
try:
|
|
79
|
+
pyproject = find_pyproject(Path(__file__))
|
|
80
|
+
if pyproject:
|
|
81
|
+
content = pyproject.read_text(encoding="utf-8")
|
|
82
|
+
match = re.search(r'^\s*name\s*=\s*["\']([^"\']+)["\']', content, re.MULTILINE)
|
|
83
|
+
if match:
|
|
84
|
+
return match.group(1)
|
|
85
|
+
except Exception:
|
|
86
|
+
pass
|
|
87
|
+
|
|
88
|
+
# 2. Check installed metadata (If running from a pip/pipx install)
|
|
89
|
+
if metadata:
|
|
90
|
+
try:
|
|
91
|
+
return metadata(PIP_PACKAGE_NAME)["Name"]
|
|
92
|
+
except Exception:
|
|
93
|
+
pass
|
|
94
|
+
|
|
95
|
+
# 3. Final Hardcoded Fallback
|
|
96
|
+
return PIP_PACKAGE_NAME
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def get_version_for_build() -> str:
|
|
100
|
+
return get_version_from_pyproject()
|
|
101
|
+
|
|
102
|
+
def get_version_from_pyproject() -> str:
|
|
103
|
+
"""
|
|
104
|
+
Read the version from pyproject.toml without external dependencies.
|
|
105
|
+
Handles both Poetry and PEP-621 formats:
|
|
106
|
+
version = "0.1.0"
|
|
107
|
+
[project]
|
|
108
|
+
version = "0.1.0"
|
|
109
|
+
"""
|
|
110
|
+
pyproject = find_pyproject(Path(__file__))
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
if not pyproject or not pyproject.exists():
|
|
114
|
+
return "Unknown (pyproject.toml missing)"
|
|
115
|
+
|
|
116
|
+
text = pyproject.read_text(encoding="utf-8")
|
|
117
|
+
#print(text)
|
|
118
|
+
# 1. Match PEP 621 style:
|
|
119
|
+
# version = "0.1.0" inside a [project] table
|
|
120
|
+
project_section = re.search(
|
|
121
|
+
r"\[project\](.*?)(?:\n\[|$)",
|
|
122
|
+
text,
|
|
123
|
+
re.DOTALL | re.IGNORECASE,
|
|
124
|
+
)
|
|
125
|
+
if project_section:
|
|
126
|
+
match = re.search(
|
|
127
|
+
r'version\s*=\s*["\']([^"\']+)["\']',
|
|
128
|
+
project_section.group(1),
|
|
129
|
+
)
|
|
130
|
+
if match:
|
|
131
|
+
return match.group(1)
|
|
132
|
+
|
|
133
|
+
# 2. Match Poetry style:
|
|
134
|
+
# [tool.poetry]
|
|
135
|
+
# version = "0.1.0"
|
|
136
|
+
poetry_section = re.search(
|
|
137
|
+
r"\[tool\.poetry\](.*?)(?:\n\[|$)",
|
|
138
|
+
text,
|
|
139
|
+
re.DOTALL | re.IGNORECASE,
|
|
140
|
+
)
|
|
141
|
+
if poetry_section:
|
|
142
|
+
match = re.search(
|
|
143
|
+
r'version\s*=\s*["\']([^"\']+)["\']',
|
|
144
|
+
poetry_section.group(1),
|
|
145
|
+
)
|
|
146
|
+
if match:
|
|
147
|
+
return match.group(1)
|
|
148
|
+
|
|
149
|
+
# fallback
|
|
150
|
+
return "Unknown (version not found)"
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def get_package_version() -> str:
|
|
154
|
+
"""
|
|
155
|
+
Correct priority:
|
|
156
|
+
|
|
157
|
+
1. embedded VERSION file (inside .pyz)
|
|
158
|
+
2. pyproject.toml (local source)
|
|
159
|
+
3. installed package metadata (pip)
|
|
160
|
+
4. fallback
|
|
161
|
+
"""
|
|
162
|
+
|
|
163
|
+
# 1. Running inside a binary / .pyz
|
|
164
|
+
v = _read_embedded_version()
|
|
165
|
+
if v:
|
|
166
|
+
return v
|
|
167
|
+
|
|
168
|
+
# 3. Check installed metadata (Only if metadata lib is available)
|
|
169
|
+
if metadata:
|
|
170
|
+
try:
|
|
171
|
+
return metadata.version(PIP_PACKAGE_NAME)
|
|
172
|
+
except (metadata.PackageNotFoundError, AttributeError):
|
|
173
|
+
pass
|
|
174
|
+
|
|
175
|
+
# 3. Local source tree → pyproject.toml
|
|
176
|
+
v = get_version_from_pyproject()
|
|
177
|
+
if v and not v.startswith("Unknown"):
|
|
178
|
+
return v
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
# 4. Default
|
|
184
|
+
return FALLBACK_VERSION
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def get_python_version():
|
|
188
|
+
py_major = sys.version_info.major
|
|
189
|
+
py_minor = sys.version_info.minor
|
|
190
|
+
py_version = f"py{py_major}{py_minor}"
|
|
191
|
+
return py_version
|
|
192
|
+
|
|
193
|
+
def form_dynamic_binary_name(package_name: str, package_version: str, py_version: str, os_tag: str, arch: str) -> str:
|
|
194
|
+
# Use hyphens for the CLI/EXE/ELF name
|
|
195
|
+
return f"{package_name}-{package_version}-{py_version}-{os_tag}-{arch}"
|
|
196
|
+
|
|
197
|
+
__version__ = get_package_version()
|
|
198
|
+
|
|
199
|
+
if __name__ == "__main__":
|
|
200
|
+
package_name = get_package_name()
|
|
201
|
+
package_version = get_package_version()
|
|
202
|
+
py_version = get_python_version()
|
|
203
|
+
|
|
204
|
+
sysinfo = SystemInfo()
|
|
205
|
+
os_tag = sysinfo.get_os_tag()
|
|
206
|
+
architecture = sysinfo.get_arch()
|
|
207
|
+
|
|
208
|
+
bin_name = form_dynamic_binary_name(package_name, package_version, py_version, os_tag, architecture)
|
|
209
|
+
print(f"bin_name = {bin_name}")
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pyhabitat
|
|
3
|
+
Version: 1.1.23
|
|
4
|
+
Summary: A lightweight library for detecting system environment, GUI, and build properties.
|
|
5
|
+
Author-email: George Clayton Bennett <george.bennett@memphistn.gov>
|
|
6
|
+
Maintainer-email: George Clayton Bennett <george.bennett@memphistn.gov>
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
Project-URL: Homepage, https://github.com/city-of-memphis-wastewater/pyhabitat
|
|
9
|
+
Project-URL: Repository, https://github.com/city-of-memphis-wastewater/pyhabitat
|
|
10
|
+
Keywords: environment,platform-detection,os-detection,container,docker,wsl,termux,unix,windows,macos,ci,runtime-detection
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
20
|
+
Classifier: Operating System :: OS Independent
|
|
21
|
+
Classifier: Intended Audience :: Developers
|
|
22
|
+
Classifier: Topic :: System :: Operating System
|
|
23
|
+
Classifier: Topic :: Software Development :: Build Tools
|
|
24
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
25
|
+
Classifier: Topic :: Utilities
|
|
26
|
+
Requires-Python: >=3.7
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
License-File: LICENSE
|
|
29
|
+
Requires-Dist: importlib-metadata; python_version < "3.8"
|
|
30
|
+
Dynamic: license-file
|
|
31
|
+
|
|
32
|
+
# pyhabitat 🧭
|
|
33
|
+
|
|
34
|
+
## An Introspection Library for Python Environments and Builds
|
|
35
|
+
|
|
36
|
+
**`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:
|
|
37
|
+
|
|
38
|
+
* **OS and Environments:** Operating Systems and common container/emulation environments (e.g., Termux, iSH).
|
|
39
|
+
* **Build States:** Application build systems (e.g., PyInstaller, pipx).
|
|
40
|
+
* **GUI Backends:** Availability of graphical toolkits (e.g., Matplotlib, Tkinter).
|
|
41
|
+
|
|
42
|
+
Stop writing verbose `sys.platform` and environment variable checks. Use **`pyhabitat`** to implement clean, **architectural logic** based on the execution habitat.
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
Read the code on [github](https://github.com/City-of-Memphis-Wastewater/pyhabitat/blob/main/pyhabitat/environment.py). 🌐
|
|
47
|
+
|
|
48
|
+
<p align="center">
|
|
49
|
+
<img src="https://raw.githubusercontent.com/City-of-Memphis-Wastewater/pyhabitat/main/assets/pyhabitat-ico-alpha.png" width="256px">
|
|
50
|
+
</p>
|
|
51
|
+
<!--p align="center">
|
|
52
|
+
<img src="https://raw.githubusercontent.com/City-of-Memphis-Wastewater/pyhabitat/main/assets/pyhabitat-ico_256x256.ico" width="200" alt="ICO Version" />
|
|
53
|
+
</p-->
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## 📦 Installation
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
pip install pyhabitat
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
<details>
|
|
66
|
+
<summary> 🧠 Motivation </summary>
|
|
67
|
+
|
|
68
|
+
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, projects use pyhabitat in their logic to trigger **localhost plotting** when a GUI is not available. You can set different behaviors for pyhabitat.on_wsl(), pyhabitat.on_termux(), pyhabitat.on_windows(), and pyhabitat.on_linux().
|
|
69
|
+
|
|
70
|
+
Our team is fundamentally driven by enabling mobile computing for true utility applications.
|
|
71
|
+
We liked it when our CLI's and our servers run on every device in the drawer.
|
|
72
|
+
|
|
73
|
+
This project has a `pipx` installable **CLI**
|
|
74
|
+
|
|
75
|
+
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**.
|
|
76
|
+
|
|
77
|
+
</details>
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
<details>
|
|
82
|
+
<summary> 🚀 Features </summary>
|
|
83
|
+
|
|
84
|
+
* **Definitive Environment Checks:** Rigorous checks catered to Termux and iSH (iOS Alpine). Accurate, typical modern detection for Windows, macOS (Apple), Linux, FreeBSD, Android.
|
|
85
|
+
* **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).
|
|
86
|
+
* **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.
|
|
87
|
+
* **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).
|
|
88
|
+
|
|
89
|
+
</details>
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
<details>
|
|
94
|
+
<summary> 📚 Function Reference </summary>
|
|
95
|
+
|
|
96
|
+
### OS and Environment Checking
|
|
97
|
+
|
|
98
|
+
Key question: "What is this running on?"
|
|
99
|
+
|
|
100
|
+
| Function | Description |
|
|
101
|
+
| :--- | :--- |
|
|
102
|
+
| `on_windows()` | Returns `True` on Windows. |
|
|
103
|
+
| `on_macos()` | Returns `True` on macOS (Darwin). |
|
|
104
|
+
| `on_linux()` | Returns `True` on Linux in general. |
|
|
105
|
+
| `on_wsl()` | Returns `True` if running inside Windows Subsystem for Linux (WSL or WSL2). |
|
|
106
|
+
| `on_termux()` | Returns `True` if running in the Termux Android environment. |
|
|
107
|
+
| `on_freebsd()` | Returns `True` on FreeBSD. |
|
|
108
|
+
| `on_ish_alpine()` | Returns `True` if running in the iSH Alpine Linux iOS emulator. |
|
|
109
|
+
| `on_android()` | Returns `True` on any Android-based Linux environment. |
|
|
110
|
+
| `on_pydroid()` | Returns `True` Return True if running under the Pydroid 3 Android app (other versions untested). |
|
|
111
|
+
| `in_repl()` | Returns `True` is the user is currently in a Python REPL; hasattr(sys,'ps1'). |
|
|
112
|
+
|
|
113
|
+
### Packaging and Build Checking
|
|
114
|
+
|
|
115
|
+
Key question: "What is the character of my executable or my build state?"
|
|
116
|
+
|
|
117
|
+
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.
|
|
118
|
+
|
|
119
|
+
| Function | Description |
|
|
120
|
+
| :--- | :--- |
|
|
121
|
+
| `as_frozen()` | Returns `True` if the script is running as a standalone executable (any bundler). |
|
|
122
|
+
| `as_pyinstaller()` | Returns `True` if the script is frozen and generated by PyInstaller (has `_MEIPASS`). |
|
|
123
|
+
| `is_python_script(path=None)` | Returns `True` if the script or specified path is a Python source file (.py). |
|
|
124
|
+
| `is_pipx(path=None)` | Returns `True` if the script or specified path is from a pipx-managed virtual environment. |
|
|
125
|
+
| `is_elf(path=None)` | Returns `True` if the script or specified path is an ELF binary (Linux standalone executable, non-pipx). |
|
|
126
|
+
| `is_pyz(path=None)` | Returns `True` if the script or specified path is a Python zipapp (.pyz, non-pipx). |
|
|
127
|
+
| `is_windows_portable_executable(path=None)` | Returns `True` if the script or specified path is a Windows PE binary (MZ header, non-pipx). |
|
|
128
|
+
| `is_msix()` | Returns `True` if the currently running software or the target path is an MSIX package, like distributed from the Microsoft Store. |
|
|
129
|
+
| `is_macos_executable(path=None)` | Returns `True` if the script or specified path is a macOS Mach-O binary (non-pipx). |
|
|
130
|
+
|
|
131
|
+
### Capability Checking
|
|
132
|
+
|
|
133
|
+
Key Question: "What could I do next?"
|
|
134
|
+
|
|
135
|
+
| Function | Description |
|
|
136
|
+
| :--- | :--- |
|
|
137
|
+
| `tkinter_is_available()` | Checks if Tkinter is imported and can successfully create a window. |
|
|
138
|
+
| `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`. |
|
|
139
|
+
| `matplotlib_is_available_for_headless_image_export()` | Checks for Matplotlib and its Agg backend, required for saving images without a GUI. |
|
|
140
|
+
| `interactive_terminal_is_available()` | Checks if standard input and output streams are connected to a TTY (allows safe use of interactive prompts). |
|
|
141
|
+
| `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). |
|
|
142
|
+
|
|
143
|
+
### Utility
|
|
144
|
+
|
|
145
|
+
| Function | Description |
|
|
146
|
+
| :--- | :--- |
|
|
147
|
+
| `edit_textfile(path)` | Opens a text file for editing using the default editor (Windows, Linux, macOS) or nano in Termux/iSH. Can be called from REPL mode. Path argument (str or Path) uses Path.resolve() for stability. |
|
|
148
|
+
| `show_system_explorer(path)` | Launches the appropriate view of the folder based on system. Defaults to Path.cwd(). |
|
|
149
|
+
| `interp_path()` | Returns the path to the Python interpreter binary (sys.executable). Returns empty string if unavailable. |
|
|
150
|
+
| `report()` | 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. |
|
|
151
|
+
|
|
152
|
+
</details>
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
<details>
|
|
157
|
+
<summary> 💻 Usage Examples </summary>
|
|
158
|
+
|
|
159
|
+
The module exposes all detection functions directly for easy access.
|
|
160
|
+
|
|
161
|
+
### 0\. Example of PyHabitat in Action
|
|
162
|
+
|
|
163
|
+
The `pyhabitat` library is used extensively in [PDF Link Check](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/pdflinkcheck/blob/main/pyproject.toml).
|
|
164
|
+
|
|
165
|
+
### 1\. Running the Environment Report
|
|
166
|
+
|
|
167
|
+
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.
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
# In the terminal
|
|
171
|
+
python -m pyhabitat
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
```python
|
|
175
|
+
# In the Python REPL
|
|
176
|
+
import pyhabitat as ph
|
|
177
|
+
ph.report()
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### Text Editing
|
|
181
|
+
|
|
182
|
+
Use this function to open a text file for editing.
|
|
183
|
+
Ideal use case: Edit a configuration file, if prompted by a CLI command like 'config --textedit'.
|
|
184
|
+
|
|
185
|
+
```python
|
|
186
|
+
from pathlib import Path
|
|
187
|
+
import pyhabitat as ph
|
|
188
|
+
|
|
189
|
+
ph.edit_textfile(path=Path('./config.json'))
|
|
190
|
+
```
|
|
191
|
+
</details>
|
|
192
|
+
|
|
193
|
+
---
|
|
194
|
+
|
|
195
|
+
<details> <summary>🏗️ Build Instructions</summary>
|
|
196
|
+
|
|
197
|
+
### Build Options
|
|
198
|
+
|
|
199
|
+
You can build PyHabitat in two ways:
|
|
200
|
+
|
|
201
|
+
| Output | Command | Notes |
|
|
202
|
+
|---------|------------------------------|-------------------------|
|
|
203
|
+
| PYZ | `python build_pyz.py` | Cross-platform zipapp |
|
|
204
|
+
| EXE/ELF | `python build_executable.py` | PyIndtaller executable |
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
✅ Notes:
|
|
208
|
+
|
|
209
|
+
.pyz is cross-platform but requires Python on the host system.
|
|
210
|
+
|
|
211
|
+
</details>
|
|
212
|
+
|
|
213
|
+
---
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
🤝 Contributing
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
Contributions are welcome\! If there is an environment or build system that is not correctly detected, or that you would like to have added, please open an issue or submit a pull request with the relevant detection logic.
|
|
220
|
+
|
|
221
|
+
## 📄 License
|
|
222
|
+
|
|
223
|
+
This project is licensed under the MIT License. See the LICENSE file for details.
|
|
224
|
+
|
|
225
|
+
---
|
|
226
|
+
|