Skip to content

Commit 6caf02e

Browse files
committed
Merge branch 'development'
2 parents 863e09d + bda8b8d commit 6caf02e

File tree

8 files changed

+2192
-730
lines changed

8 files changed

+2192
-730
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,3 +78,4 @@ KEYS
7878
# Lock files
7979
*.lock
8080

81+
branch_diffs/

omnipkg/common_utils.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,68 @@ def sync_context_to_runtime():
119119
# Exit because a failed sync is a fatal error for the script's logic.
120120
sys.exit(1)
121121

122+
def ensure_script_is_running_on_version(required_version: str):
123+
"""
124+
A declarative guard placed at the start of a script. It ensures the script is
125+
running on a specific Python version. If not, it uses the omnipkg API to
126+
find the target interpreter and relaunches the script using os.execve.
127+
"""
128+
major, minor = map(int, required_version.split('.'))
129+
130+
# If we are already running on the correct version, we're done.
131+
if sys.version_info[:2] == (major, minor):
132+
return
133+
134+
# This guard prevents infinite relaunch loops if something goes horribly wrong.
135+
if os.environ.get('OMNIPKG_RELAUNCHED') == '1':
136+
print(f"❌ FATAL ERROR: Relaunch attempted, but still not on Python {required_version}. Aborting.")
137+
sys.exit(1)
138+
139+
print('\n' + '=' * 80)
140+
print(_(' 🚀 AUTOMATIC CONTEXT RELAUNCH REQUIRED'))
141+
print('=' * 80)
142+
print(_(' - Script requires: Python {}').format(required_version))
143+
print(_(' - Currently running: Python {}.{}').format(sys.version_info.major, sys.version_info.minor))
144+
print(_(' - Relaunching into the correct context...'))
145+
146+
try:
147+
# We must import the core late to avoid bootstrap circular dependencies
148+
from omnipkg.core import ConfigManager, omnipkg as OmnipkgCore
149+
150+
cm = ConfigManager(suppress_init_messages=True)
151+
pkg_instance = OmnipkgCore(config_manager=cm)
152+
153+
# Use omnipkg's brain to find the interpreter path
154+
target_exe_path = pkg_instance.interpreter_manager.config_manager.get_interpreter_for_version(required_version)
155+
156+
# If it's not managed, try to adopt it automatically
157+
if not target_exe_path or not target_exe_path.exists():
158+
print(_(' -> Target interpreter not yet managed. Attempting to adopt...'))
159+
if pkg_instance.adopt_interpreter(required_version) != 0:
160+
raise RuntimeError(f"Failed to adopt required Python version {required_version}")
161+
target_exe_path = pkg_instance.interpreter_manager.config_manager.get_interpreter_for_version(required_version)
162+
if not target_exe_path or not target_exe_path.exists():
163+
raise RuntimeError(f"Could not find Python {required_version} even after adoption.")
164+
165+
print(_(' ✅ Target interpreter found at: {}').format(target_exe_path))
166+
167+
# Set the guard variable for the new process to prevent loops
168+
new_env = os.environ.copy()
169+
new_env['OMNIPKG_RELAUNCHED'] = '1'
170+
171+
# This is the key: os.execve REPLACES the current process with a new one.
172+
# It does not return. The script starts over from the top, but inside the correct Python.
173+
os.execve(str(target_exe_path), [str(target_exe_path)] + sys.argv, new_env)
174+
175+
except Exception as e:
176+
print('\n' + '-' * 80)
177+
print(' ❌ FATAL ERROR during context relaunch.')
178+
print(f' -> Error: {e}')
179+
import traceback
180+
traceback.print_exc()
181+
print('-' * 80)
182+
sys.exit(1)
183+
122184
def run_script_in_omnipkg_env(command_list, streaming_title):
123185
"""
124186
A centralized utility to run a command in a fully configured omnipkg environment.

omnipkg/core.py

Lines changed: 1038 additions & 299 deletions
Large diffs are not rendered by default.

omnipkg/loader.py

Lines changed: 294 additions & 114 deletions
Large diffs are not rendered by default.

omnipkg/package_meta_builder.py

Lines changed: 693 additions & 211 deletions
Large diffs are not rendered by default.

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.4.2" # <-- THE NEW LEGENDARY VERSION
7+
version = "1.4.3"
88
authors = [
99
{ name = "1minds3t", email = "[email protected]" },
1010
]

tests/test_rich_switching.py

Lines changed: 64 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,5 @@
11
import sys
22
import os
3-
4-
if sys.version_info[:2] == (3, 9):
5-
print("SKIPPED: The rich switching test is currently unstable on Python 3.9.")
6-
sys.exit(0)
7-
8-
import sys
9-
import os
103
from pathlib import Path
114
import json
125
import subprocess
@@ -153,126 +146,86 @@ def create_test_bubbles(config_manager):
153146

154147
def test_python_import(expected_version: str, config_manager, is_bubble: bool):
155148
print(_(' 🔧 Testing import of version {}...').format(expected_version))
156-
# FIX: Extract config dict from config_manager
157149
config = config_manager.config
158-
config_json_str = json.dumps(config)
159-
160-
test_script_content = f'''\
150+
# We must pass the project root to the subprocess so it can find the `omnipkg` source
151+
project_root_str = str(Path(__file__).resolve().parent.parent)
152+
153+
# --- THIS IS THE NEW, ISOLATED TEST RUNNER ---
154+
# We create a self-contained script to run in a pristine, isolated subprocess.
155+
test_script_content = f"""
161156
import sys
162-
import importlib
163-
from importlib.metadata import version, PackageNotFoundError
164-
from pathlib import Path
165157
import json
158+
import traceback
159+
from pathlib import Path
166160
167-
# Ensure omnipkg's root is in sys.path for importing its modules (e.g., omnipkg.loader)
168-
sys.path.insert(0, r"{Path(__file__).resolve().parents[1].parent}")
161+
# Add the project root to the path to find the omnipkg library
162+
sys.path.insert(0, r'{project_root_str}')
169163
170-
from omnipkg.loader import omnipkgLoader
164+
try:
165+
from omnipkg.loader import omnipkgLoader
166+
from importlib.metadata import version
171167
172-
# omnipkg.core.ConfigManager is also imported in subprocess for omnipkgLoader if config is passed
173-
def test_import_and_version():
174-
target_package_spec = "rich=={expected_version}"
175-
176-
# Load config in the subprocess
177-
subprocess_config = json.loads('{config_json_str}')
168+
# Load the config passed from the main test script
169+
config = json.loads('{json.dumps(config)}')
170+
is_bubble = {is_bubble}
171+
expected_version = "{expected_version}"
172+
target_spec = f"rich=={{expected_version}}"
178173
179-
# If it's the main environment test, we don't use the loader context for explicit switching,
180-
# just import directly from the system state.
181-
if not {is_bubble}: # Use the boolean value directly
182-
try:
183-
actual_version = version('rich')
184-
expected_version = "{expected_version}"
185-
assert actual_version == expected_version, f"Version mismatch! Expected {{expected_version}}, got {{actual_version}}"
186-
print(f"✅ Imported and verified version {{actual_version}}")
187-
except PackageNotFoundError:
188-
print(f"❌ Test failed: Package 'rich' not found in main environment.", file=sys.stderr)
189-
sys.exit(1)
190-
except AssertionError as e:
191-
print(f"❌ Test failed: {{e}}", file=sys.stderr)
192-
sys.exit(1)
193-
except Exception as e:
194-
print(f"❌ An unexpected error occurred in main env test: {{e}}", file=sys.stderr)
195-
sys.exit(1)
196-
return
197-
198-
# For bubble tests, use the omnipkgLoader context manager
199-
try:
200-
with omnipkgLoader(target_package_spec, config=subprocess_config):
201-
# Inside this block, the specific 'rich' version should be active
174+
if is_bubble:
175+
# For bubble tests, activate the loader
176+
with omnipkgLoader(target_spec, config=config):
202177
import rich
203178
actual_version = version('rich')
204-
expected_version = "{expected_version}"
205179
assert actual_version == expected_version, f"Version mismatch! Expected {{expected_version}}, got {{actual_version}}"
206180
print(f"✅ Imported and verified version {{actual_version}}")
207-
except PackageNotFoundError:
208-
print(f"❌ Test failed: Package 'rich' not found in bubble context '{{target_package_spec}}'.", file=sys.stderr)
209-
sys.exit(1)
210-
except AssertionError as e:
211-
print(f"❌ Test failed: {{e}}", file=sys.stderr)
212-
sys.exit(1)
213-
except Exception as e:
214-
print(f"❌ An unexpected error occurred activating/testing bubble '{{target_package_spec}}': {{e}}", file=sys.stderr)
215-
import traceback
216-
traceback.print_exc(file=sys.stderr)
217-
sys.exit(1)
218-
219-
if __name__ == "__main__":
220-
test_import_and_version()
221-
'''
181+
else:
182+
# For the main environment, just import directly
183+
import rich
184+
actual_version = version('rich')
185+
assert actual_version == expected_version, f"Version mismatch! Expected {{expected_version}}, got {{actual_version}}"
186+
print(f"✅ Imported and verified version {{actual_version}}")
187+
188+
except Exception as e:
189+
print(f"❌ TEST FAILED: {{e}}", file=sys.stderr)
190+
traceback.print_exc(file=sys.stderr)
191+
sys.exit(1)
192+
"""
222193

223-
site_packages = Path(config['site_packages_path'])
224-
main_rich_dir = site_packages / 'rich'
225-
main_rich_dist = next(site_packages.glob('rich-*.dist-info'), None)
226-
cloaked_paths_by_test_harness = []
227194
temp_script_path = None
228-
229195
try:
230-
if is_bubble:
231-
if main_rich_dir.exists():
232-
cloak_path = main_rich_dir.with_name(_('rich.{}test_harness_cloaked').format(int(time.time() * 1000)))
233-
shutil.move(main_rich_dir, cloak_path)
234-
cloaked_paths_by_test_harness.append((main_rich_dir, cloak_path))
235-
236-
if main_rich_dist and main_rich_dist.exists():
237-
cloak_path = main_rich_dist.with_name(_('{}.{}test_harness_cloaked').format(main_rich_dist.name, int(time.time() * 1000)))
238-
shutil.move(main_rich_dist, cloak_path)
239-
cloaked_paths_by_test_harness.append((main_rich_dist, cloak_path))
240-
241-
with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as f:
196+
# Write the mini-script to a temporary file
197+
with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False, encoding='utf-8') as f:
242198
f.write(test_script_content)
243199
temp_script_path = f.name
244200

201+
# Get the correct python executable from the config
245202
python_exe = config.get('python_executable', sys.executable)
246203

247-
result = subprocess.run([python_exe, temp_script_path], capture_output=True, text=True, timeout=60)
204+
# Execute the mini-script in ISOLATED MODE
205+
cmd = [python_exe, '-I', temp_script_path]
206+
207+
result = subprocess.run(cmd, capture_output=True, text=True, timeout=90)
248208

249209
if result.returncode == 0:
250-
print(_(' └── {}').format(result.stdout.strip()))
210+
print(f" └── {result.stdout.strip()}")
251211
return True
252212
else:
253-
print(_(' ❌ Subprocess FAILED for version {}:').format(expected_version))
254-
print(_(' STDERR: {}').format(result.stderr.strip()))
213+
print(f" ❌ Subprocess FAILED for version {expected_version}:")
214+
# Print stderr first as it contains the real traceback from the subprocess
215+
print(f" STDERR: {result.stderr.strip()}")
216+
if result.stdout.strip():
217+
print(f" STDOUT: {result.stdout.strip()}")
255218
return False
256219

257-
except subprocess.CalledProcessError as e:
258-
print(_(' ❌ Subprocess FAILED for version {}:').format(expected_version))
259-
print(_(' STDERR: {}').format(e.stderr.strip()))
220+
except Exception as e:
221+
print(f' ❌ An unexpected error occurred while running the test subprocess: {e}')
260222
return False
261223

262224
finally:
263-
# Restore cloaked paths in reverse order
264-
for original, cloaked in reversed(cloaked_paths_by_test_harness):
265-
if cloaked.exists():
266-
if original.exists():
267-
shutil.rmtree(original, ignore_errors=True)
268-
try:
269-
shutil.move(cloaked, original)
270-
except Exception as e:
271-
print(_(' ⚠️ Test harness failed to restore {} from {}: {}').format(original.name, cloaked.name, e))
272-
225+
# Ensure the temporary script is always cleaned up
273226
if temp_script_path and os.path.exists(temp_script_path):
274227
os.unlink(temp_script_path)
275-
228+
276229
def restore_install_strategy(config_manager, original_strategy):
277230
"""Restore the original install strategy"""
278231
if original_strategy != 'stable-main':
@@ -337,13 +290,20 @@ def run_comprehensive_test():
337290
omnipkg_core = OmnipkgCore(config_manager)
338291
site_packages = Path(config_manager.config['site_packages_path'])
339292

340-
# Clean up test bubbles
341-
for bubble in omnipkg_core.multiversion_base.glob('rich-*'):
342-
if bubble.is_dir():
343-
print(_(' 🧹 Removing test bubble: {}').format(bubble.name))
344-
shutil.rmtree(bubble, ignore_errors=True)
345-
346-
# Clean up cloaked packages
293+
# --- START OF THE FIX ---
294+
# Instead of manually deleting directories, use the omnipkg API
295+
# to perform a clean uninstall that also updates the knowledge base.
296+
297+
print(_(' 🧹 Cleaning up test bubbles via omnipkg API...'))
298+
specs_to_uninstall = [f'rich=={v}' for v in BUBBLE_VERSIONS_TO_TEST]
299+
if specs_to_uninstall:
300+
# Uninstall all bubbles in one go. The `install_type='bubble'`
301+
# ensures we only target bubbles and don't touch the active version.
302+
omnipkg_core.smart_uninstall(specs_to_uninstall, force=True, install_type='bubble')
303+
304+
# --- END OF THE FIX ---
305+
306+
# Clean up any residual cloaked packages (this is still good practice)
347307
for cloaked in site_packages.glob('rich.*_omnipkg_cloaked*'):
348308
print(_(' 🧹 Removing residual cloaked: {}').format(cloaked.name))
349309
shutil.rmtree(cloaked, ignore_errors=True)

tests/test_uv_switching.py

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,31 @@
22
import os
33
from pathlib import Path
44

5+
# --- PROJECT PATH SETUP ---
6+
# This must come first so Python can find your modules.
57
project_root = Path(__file__).resolve().parent.parent
68
sys.path.insert(0, str(project_root))
79

8-
from omnipkg.common_utils import sync_context_to_runtime
10+
# --- DETECT CURRENT PYTHON VERSION ---
11+
CURRENT_PYTHON_VERSION = f"{sys.version_info.major}.{sys.version_info.minor}"
12+
print(f"🐍 Detected current Python version: {CURRENT_PYTHON_VERSION}")
13+
14+
# --- BOOTSTRAP SECTION ---
15+
# Import ONLY the necessary utilities for the bootstrap process.
16+
from omnipkg.common_utils import ensure_python_or_relaunch, sync_context_to_runtime
17+
18+
# 1. Declarative script guard: Ensures this script runs on Python 3.9.
19+
# If not, it will relaunch this script with the correct interpreter and exit.
20+
# 1. Declarative script guard: Ensures this script runs on the detected Python version.
21+
# If not, it will relaunch the script with the correct interpreter and exit.
22+
if os.environ.get('OMNIPKG_RELAUNCHED') != '1':
23+
ensure_python_or_relaunch(CURRENT_PYTHON_VERSION)
24+
25+
# 2. Sync guard: Now that we are GUARANTEED to be running on the correct
26+
# interpreter, we sync omnipkg's config to match this runtime.
927
sync_context_to_runtime()
1028

29+
1130
import sys
1231
import os
1332
from pathlib import Path
@@ -142,6 +161,11 @@ def setup_environment():
142161
print(_(' ❌ Failed to install main environment UV version'))
143162
return None, original_strategy
144163

164+
# --- THIS IS THE FIX ---
165+
# Tell omnipkg to update its knowledge about the package we just installed.
166+
force_omnipkg_rescan(omnipkg_core, 'uv')
167+
# --- END OF THE FIX ---
168+
145169
print(_('✅ Environment prepared'))
146170
return config_manager, original_strategy
147171

@@ -160,6 +184,18 @@ def create_test_bubbles(config_manager):
160184

161185
return BUBBLE_VERSIONS_TO_TEST
162186

187+
def force_omnipkg_rescan(omnipkg_core, package_name):
188+
"""Tells omnipkg to forcibly rescan a specific package's metadata."""
189+
print(f' 🧠 Forcing omnipkg KB rebuild for {package_name}...')
190+
try:
191+
# We'll use our new internal method directly for the test
192+
omnipkg_core.rebuild_package_kb([package_name])
193+
print(f' ✅ KB rebuild for {package_name} complete.')
194+
return True
195+
except Exception as e:
196+
print(f' ❌ KB rebuild for {package_name} failed: {e}')
197+
return False
198+
163199
def inspect_bubble_structure(bubble_path):
164200
"""Prints a summary of the bubble's directory structure for verification."""
165201
print(_(' 🔍 Inspecting bubble structure: {}').format(bubble_path.name))
@@ -312,6 +348,8 @@ def run_comprehensive_test():
312348
print(_(' 📦 Restoring main environment: uv=={}').format(MAIN_UV_VERSION))
313349
pip_uninstall_uv()
314350
pip_install_uv(MAIN_UV_VERSION)
351+
352+
force_omnipkg_rescan(omnipkg_core, 'uv')
315353

316354
# Restore original install strategy if it was changed
317355
if original_strategy and original_strategy != 'stable-main':

0 commit comments

Comments
 (0)