Skip to content

Commit ab31acd

Browse files
committed
feat(release): Bump version to 1.0.5 after successful stress test and add combo handling logic
1 parent dc8e4a5 commit ab31acd

3 files changed

Lines changed: 177 additions & 35 deletions

File tree

omnipkg/loader.py

Lines changed: 65 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
1+
# Enhanced loader for better C extension handling
2+
13
import sys
24
import json
35
from pathlib import Path
46
import site
57
from importlib.metadata import version as get_version, PackageNotFoundError
8+
import importlib
69

710
class omnipkgLoader:
811
"""
@@ -20,14 +23,38 @@ def __init__(self):
2023

2124
# Track active bubbles for cleanup
2225
self.active_bubbles = set()
26+
self.original_sys_path = sys.path.copy()
27+
28+
def _get_package_modules(self, pkg_name: str):
29+
"""Get all modules related to a package"""
30+
pkg_name_normalized = pkg_name.replace('-', '_')
31+
return [mod for mod in list(sys.modules.keys())
32+
if mod.startswith(pkg_name_normalized) or
33+
mod.replace('_', '-').startswith(pkg_name)]
34+
35+
def _aggressive_module_cleanup(self, pkg_name: str):
36+
"""Aggressively clean package modules from sys.modules"""
37+
modules_to_clear = self._get_package_modules(pkg_name)
38+
39+
for mod_name in modules_to_clear:
40+
if mod_name in sys.modules:
41+
del sys.modules[mod_name]
42+
43+
# Force garbage collection
44+
import gc
45+
gc.collect()
46+
47+
# Invalidate import caches
48+
if hasattr(importlib, 'invalidate_caches'):
49+
importlib.invalidate_caches()
2350

2451
def _deactivate_package_bubbles(self, pkg_name: str):
2552
"""Remove any active bubbles for the given package from sys.path"""
2653
if not self.multiversion_base:
2754
return
2855

2956
bubbles_to_remove = []
30-
for path_str in sys.path:
57+
for path_str in sys.path[:]: # Create a copy to iterate over
3158
path = Path(path_str)
3259
# Check if this path is a bubble for the given package
3360
if (path.parent == self.multiversion_base and
@@ -39,6 +66,27 @@ def _deactivate_package_bubbles(self, pkg_name: str):
3966
self.active_bubbles.discard(bubble_path)
4067
print(f" 🧹 Deactivated bubble: {Path(bubble_path).name}")
4168

69+
def _prioritize_bubble_in_path(self, bubble_path_str: str):
70+
"""Ensure bubble is at the very front of sys.path"""
71+
# Remove from current position if it exists
72+
if bubble_path_str in sys.path:
73+
sys.path.remove(bubble_path_str)
74+
75+
# Insert at position 0 (highest priority)
76+
sys.path.insert(0, bubble_path_str)
77+
78+
# Also remove any site-packages paths that might contain the same package
79+
# and move them after our bubble
80+
site_packages_paths = []
81+
for i, path in enumerate(sys.path[1:], 1): # Skip our bubble at index 0
82+
if 'site-packages' in path and Path(path).exists():
83+
site_packages_paths.append((i, path))
84+
85+
# Move site-packages to after our bubble (but keep their relative order)
86+
for i, (original_index, path) in enumerate(reversed(site_packages_paths)):
87+
sys.path.remove(path)
88+
sys.path.insert(i + 1, path) # Insert after bubble
89+
4290
def activate_snapshot(self, package_spec: str) -> bool:
4391
"""
4492
Activates a specific package version bubble, or confirms if the
@@ -56,12 +104,10 @@ def activate_snapshot(self, package_spec: str) -> bool:
56104
# First, deactivate any existing bubbles for this package
57105
self._deactivate_package_bubbles(pkg_name)
58106

59-
# Clear any cached imports for this package to ensure fresh import
60-
modules_to_clear = [mod for mod in sys.modules.keys() if mod.startswith(pkg_name.replace('-', '_'))]
61-
for mod in modules_to_clear:
62-
del sys.modules[mod]
107+
# Aggressively clear modules for this package (silent)
108+
self._aggressive_module_cleanup(pkg_name)
63109

64-
# Check if the currently installed system version matches after bubble cleanup
110+
# Check if we need a bubble (system version doesn't match)
65111
try:
66112
active_version = get_version(pkg_name)
67113
if active_version == requested_version:
@@ -71,7 +117,7 @@ def activate_snapshot(self, package_spec: str) -> bool:
71117
# The package isn't in the main environment, so we must use a bubble.
72118
pass
73119

74-
# If the system version doesn't match, proceed to find and activate a bubble.
120+
# Find and activate the bubble
75121
if not self.multiversion_base or not self.multiversion_base.exists():
76122
print(f" ❌ Bubble directory not found at {self.multiversion_base}")
77123
return False
@@ -84,12 +130,14 @@ def activate_snapshot(self, package_spec: str) -> bool:
84130
print(f" ❌ Bubble not found for {package_spec} at {bubble_path}")
85131
return False
86132

87-
# Activate the bubble by putting it at the front of sys.path
88133
bubble_path_str = str(bubble_path)
89-
sys.path.insert(0, bubble_path_str)
134+
135+
# Prioritize this bubble in sys.path
136+
self._prioritize_bubble_in_path(bubble_path_str)
90137
self.active_bubbles.add(bubble_path_str)
91138

92139
print(f" ✅ Activated bubble: {bubble_path_str}")
140+
print(f" 🔧 sys.path[0]: {sys.path[0]}")
93141

94142
# Show bubble info if manifest exists
95143
manifest_path = bubble_path / '.omnipkg_manifest.json'
@@ -103,4 +151,11 @@ def activate_snapshot(self, package_spec: str) -> bool:
103151

104152
except Exception as e:
105153
print(f" ❌ Error during bubble activation for {package_spec}: {e}")
106-
return False
154+
return False
155+
156+
def reset_environment(self):
157+
"""Reset sys.path to its original state"""
158+
print(" 🔄 Resetting environment to original state...")
159+
sys.path.clear()
160+
sys.path.extend(self.original_sys_path)
161+
self.active_bubbles.clear()

omnipkg/stress_test.py

Lines changed: 111 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,12 @@
22
import importlib
33
import shutil
44
import time
5+
import gc
56
from .loader import omnipkgLoader
67
from .core import omnipkg as OmnipkgCore, ConfigManager
8+
from pathlib import Path
9+
import os
10+
import subprocess
711

812
def print_header(title):
913
"""Prints a consistent, pretty header for the test stages."""
@@ -20,13 +24,11 @@ def setup():
2024
packages_to_test = ["numpy", "scipy"]
2125

2226
for pkg in packages_to_test:
23-
# Find all bubble directories for this package
2427
for bubble in omnipkg_core.multiversion_base.glob(f"{pkg}-*"):
2528
if bubble.is_dir():
2629
print(f" - Removing old bubble: {bubble.name}")
2730
shutil.rmtree(bubble)
2831

29-
# Use omnipkg to ensure the 'good' versions are installed
3032
print(" - Setting main environment to a known good state...")
3133
omnipkg_core.smart_install(["numpy==1.26.4", "scipy==1.16.1"])
3234
print("✅ Environment is clean and ready for testing.")
@@ -39,47 +41,135 @@ def run_test():
3941
print("\n💥 NUMPY VERSION JUGGLING:")
4042
for numpy_ver in ["1.24.3", "1.26.4"]:
4143
print(f"\n⚡ Switching to numpy=={numpy_ver}")
44+
4245
if loader.activate_snapshot(f"numpy=={numpy_ver}"):
4346
import numpy as np
44-
importlib.reload(np)
47+
4548
print(f" ✅ Version: {np.__version__}")
4649
print(f" 🔢 Array sum: {np.array([1,2,3]).sum()}")
50+
51+
if np.__version__ != numpy_ver:
52+
print(f" ⚠️ WARNING: Expected {numpy_ver}, got {np.__version__}!")
53+
else:
54+
print(f" 🎯 Version verification: PASSED")
4755
else:
4856
print(f" ❌ Activation failed for numpy=={numpy_ver}!")
4957

5058
# ===== SCIPY C-EXTENSION CHAOS =====
5159
print("\n\n🔥 SCIPY C-EXTENSION TEST:")
5260
for scipy_ver in ["1.12.0", "1.16.1"]:
5361
print(f"\n🌋 Switching to scipy=={scipy_ver}")
62+
5463
if loader.activate_snapshot(f"scipy=={scipy_ver}"):
64+
import scipy as sp
5565
import scipy.sparse
5666
import scipy.linalg
57-
importlib.reload(scipy)
58-
print(f" ✅ Version: {scipy.__version__}")
59-
eye = scipy.sparse.eye(3)
60-
print(f" ♻️ Sparse matrix: {eye.nnz} non-zeros")
61-
det = scipy.linalg.det([[0, 2], [1, 1]])
62-
print(f" 📐 Linalg det: {det}")
67+
68+
print(f" ✅ Version: {sp.__version__}")
69+
print(f" ♻️ Sparse matrix: {sp.sparse.eye(3).nnz} non-zeros")
70+
print(f" 📐 Linalg det: {sp.linalg.det([[0, 2], [1, 1]])}")
71+
72+
if sp.__version__ != scipy_ver:
73+
print(f" ⚠️ WARNING: Expected {scipy_ver}, got {sp.__version__}!")
74+
else:
75+
print(f" 🎯 Version verification: PASSED")
6376
else:
6477
print(f" ❌ Activation failed for scipy=={scipy_ver}!")
6578

66-
# ===== THE IMPOSSIBLE TEST =====
79+
# ===== THE IMPOSSIBLE TEST (using clean process) =====
6780
print("\n\n🤯 NUMPY + SCIPY VERSION MIXING:")
6881
combos = [("1.24.3", "1.12.0"), ("1.26.4", "1.16.1")]
82+
83+
temp_script_path = Path(os.getcwd()) / "omnipkg_combo_test.py"
84+
6985
for np_ver, sp_ver in combos:
7086
print(f"\n🌀 COMBO: numpy=={np_ver} + scipy=={sp_ver}")
71-
loader.activate_snapshot(f"numpy=={np_ver}")
72-
loader.activate_snapshot(f"scipy=={sp_ver}")
7387

74-
import numpy as np
75-
import scipy.sparse
76-
importlib.reload(np)
77-
importlib.reload(scipy)
88+
# Get bubble paths
89+
config_manager = ConfigManager()
90+
omnipkg_core = OmnipkgCore(config_manager.config)
91+
multiversion_base = omnipkg_core.multiversion_base
7892

79-
print(f" 🧪 numpy: {np.__version__}, scipy: {scipy.__version__}")
80-
result = np.array([1,2,3]) @ scipy.sparse.eye(3)
81-
print(f" 🔗 Compatibility check: {result}")
93+
numpy_bubble = multiversion_base / f"numpy-{np_ver}"
94+
scipy_bubble = multiversion_base / f"scipy-{sp_ver}"
8295

96+
# Build PYTHONPATH with bubbles at the front
97+
bubble_paths = []
98+
if numpy_bubble.exists():
99+
bubble_paths.append(str(numpy_bubble))
100+
if scipy_bubble.exists():
101+
bubble_paths.append(str(scipy_bubble))
102+
103+
# Write the subprocess script
104+
temp_script_content = f"""
105+
import sys
106+
import os
107+
108+
# Verify the bubbles are in our path
109+
print("🔍 Python path (first 5 entries):")
110+
for idx, path in enumerate(sys.path[:5]):
111+
print(f" {{idx}}: {{path}}")
112+
113+
try:
114+
import numpy as np
115+
import scipy as sp
116+
import scipy.sparse
117+
118+
print(f" 🧪 numpy: {{np.__version__}}, scipy: {{sp.__version__}}")
119+
print(f" 📍 numpy location: {{np.__file__}}")
120+
print(f" 📍 scipy location: {{sp.__file__}}")
121+
122+
result = np.array([1,2,3]) @ sp.sparse.eye(3).toarray()
123+
print(f" 🔗 Compatibility check: {{result}}")
124+
125+
if np.__version__ != "{np_ver}" or sp.__version__ != "{sp_ver}":
126+
print(f" ❌ Version mismatch! Expected numpy=={np_ver}, scipy=={sp_ver} but got numpy={{np.__version__}}, scipy={{sp.__version__}}", file=sys.stderr)
127+
sys.exit(1)
128+
else:
129+
print(f" 🎯 Version verification: BOTH PASSED!")
130+
131+
except Exception as e:
132+
print(f" ❌ Test failed in subprocess: {{e}}", file=sys.stderr)
133+
import traceback
134+
traceback.print_exc(file=sys.stderr)
135+
sys.exit(1)
136+
"""
137+
try:
138+
with open(temp_script_path, "w") as f:
139+
f.write(temp_script_content)
140+
141+
# Create clean environment with bubbles prioritized
142+
clean_env = os.environ.copy()
143+
144+
# Build PYTHONPATH with bubbles first, then existing paths
145+
existing_pythonpath = clean_env.get('PYTHONPATH', '')
146+
if existing_pythonpath:
147+
new_pythonpath = ':'.join(bubble_paths + [existing_pythonpath])
148+
else:
149+
new_pythonpath = ':'.join(bubble_paths)
150+
151+
clean_env['PYTHONPATH'] = new_pythonpath
152+
153+
# Remove any conda/pip environment variables that might interfere
154+
env_vars_to_remove = ['CONDA_DEFAULT_ENV', 'CONDA_PREFIX', 'PIP_TARGET']
155+
for var in env_vars_to_remove:
156+
clean_env.pop(var, None)
157+
158+
print(f" 🔧 PYTHONPATH: {new_pythonpath}")
159+
160+
subprocess.run(
161+
[sys.executable, temp_script_path],
162+
check=True,
163+
cwd=os.getcwd(),
164+
env=clean_env
165+
)
166+
except subprocess.CalledProcessError as e:
167+
print(f" ❌ Subprocess test failed for combo numpy=={np_ver} + scipy=={sp_ver}")
168+
print(f" 💥 Exit code: {e.returncode}")
169+
finally:
170+
if temp_script_path.exists():
171+
os.remove(temp_script_path)
172+
83173
print("\n\n 🚨 OMNIPKG SURVIVED NUCLEAR TESTING! 🎇")
84174

85175
def cleanup():
@@ -103,7 +193,6 @@ def run():
103193
try:
104194
setup()
105195

106-
# --- Create the bubbles for the test ---
107196
print_header("STEP 2: Creating Test Bubbles with `omnipkg`")
108197
config_manager = ConfigManager()
109198
omnipkg_core = OmnipkgCore(config_manager.config)
@@ -115,9 +204,8 @@ def run():
115204
name, version = pkg.split('==')
116205
print(f"\n--- Creating bubble for {name}=={version} ---")
117206
omnipkg_core.bubble_manager.create_isolated_bubble(name, version)
118-
time.sleep(1) # Give filesystem a moment to settle
207+
time.sleep(1)
119208

120-
# --- Run the actual test ---
121209
print_header("STEP 3: Executing the Nuclear Test")
122210
run_test()
123211

@@ -126,8 +214,7 @@ def run():
126214
import traceback
127215
traceback.print_exc()
128216
finally:
129-
# --- ALWAYS run the cleanup ---
130217
cleanup()
131218

132219
if __name__ == "__main__":
133-
run()
220+
run()

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "omnipkg"
7-
version = "1.0.4"
7+
version = "1.0.5"
88
authors = [
99
{ name = "1minds3t", email = "omnipkg@proton.me" },
1010
]

0 commit comments

Comments
 (0)