Skip to content

Commit 41dafd7

Browse files
committed
Merge branch 'development'
2 parents 3ea65ed + 25fd668 commit 41dafd7

3 files changed

Lines changed: 1762 additions & 453 deletions

File tree

omnipkg/cli.py

Lines changed: 178 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1+
"""omnipkg CLI - Enhanced with runtime interpreter switching and language support"""
12
try:
23
from .common_utils import safe_print
34
except ImportError:
45
from omnipkg.common_utils import safe_print
5-
"""omnipkg CLI - Enhanced with runtime interpreter switching and language support"""
66
import sys
77
import argparse
88
from pathlib import Path
@@ -38,41 +38,115 @@ def get_actual_python_version():
3838
except Exception:
3939
return sys.version_info[:2]
4040

41+
def run_demo_with_enforced_context(
42+
source_script_path: Path,
43+
demo_name: str,
44+
pkg_instance: OmnipkgCore,
45+
parser_prog: str,
46+
required_version: str = None
47+
) -> int:
48+
"""
49+
Run a demo test with enforced Python context.
50+
51+
Args:
52+
source_script_path: Path to the test script to run
53+
demo_name: Name of the demo (for display purposes)
54+
pkg_instance: Initialized OmnipkgCore instance
55+
parser_prog: Parser program name (for error messages)
56+
required_version: Optional specific version (e.g., "3.11").
57+
If None, uses currently detected version.
58+
59+
Returns:
60+
Exit code (0 for success, 1 for failure)
61+
"""
62+
# Detect the actual current Python version
63+
actual_version = get_actual_python_version()
64+
65+
# Use required version if specified, otherwise use detected version
66+
target_version_str = required_version if required_version else f"{actual_version[0]}.{actual_version[1]}"
67+
68+
# Validate the source script exists
69+
if not source_script_path.exists():
70+
safe_print(f'❌ Error: Source test file {source_script_path} not found.')
71+
return 1
72+
73+
# Get the Python executable for the target version
74+
python_exe = pkg_instance.config_manager.get_interpreter_for_version(target_version_str)
75+
if not python_exe or not python_exe.exists():
76+
safe_print(f"❌ Python {target_version_str} is not managed by omnipkg.")
77+
safe_print(f" Please adopt it first: {parser_prog} python adopt {target_version_str}")
78+
return 1
79+
80+
safe_print(f'🚀 Running {demo_name} demo with Python {target_version_str} via sterile environment...')
81+
82+
# Create a sterile copy of the script in /tmp to avoid PYTHONPATH contamination
83+
with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False, encoding='utf-8') as temp_script:
84+
temp_script_path = Path(temp_script.name)
85+
temp_script.write(source_script_path.read_text(encoding='utf-8'))
86+
87+
safe_print(f" - Sterile script created at: {temp_script_path}")
88+
89+
try:
90+
# Execute using the enforced Python context
91+
return run_demo_with_live_streaming(
92+
test_file_name=str(temp_script_path),
93+
demo_name=demo_name,
94+
python_exe=str(python_exe)
95+
)
96+
finally:
97+
# Always clean up the temporary file
98+
temp_script_path.unlink(missing_ok=True)
99+
41100
def handle_python_requirement(required_version_str: str, pkg_instance: OmnipkgCore, parser_prog: str) -> bool:
42101
"""
43102
Checks if the current Python version matches the requirement.
44-
If not, it attempts to automatically adopt and/or swap.
103+
If not, it automatically finds, adopts (or downloads), and swaps to it.
45104
"""
105+
# Get the true version of the currently configured Python context
46106
actual_version_tuple = get_actual_python_version()
47107
required_version_tuple = tuple(map(int, required_version_str.split('.')))
108+
109+
# If we are already in the correct context, we're done.
48110
if actual_version_tuple == required_version_tuple:
49111
return True
112+
113+
# --- Start the full healing process ---
50114
print_header(_('Python Version Requirement'))
51-
safe_print(_(' ⚠️ This Demo Requires Python {}').format(required_version_str))
52-
safe_print(_(' - Current Python version: {}.{}').format(actual_version_tuple[0], actual_version_tuple[1]))
53-
safe_print(_(' - omnipkg will now attempt to automatically configure the correct interpreter.'))
54-
safe_print('-' * 60)
115+
safe_print(_(' - Diagnosis: This operation requires Python {}').format(required_version_str))
116+
safe_print(_(' - Current Context: Python {}.{}').format(actual_version_tuple[0], actual_version_tuple[1]))
117+
safe_print(_(' - Action: omnipkg will now attempt to automatically configure the correct interpreter.'))
118+
119+
# Check if the required version is already managed by omnipkg
55120
managed_interpreters = pkg_instance.interpreter_manager.list_available_interpreters()
121+
56122
if required_version_str not in managed_interpreters:
57-
discovered_interpreters = pkg_instance.config_manager.list_available_pythons()
58-
if required_version_str in discovered_interpreters:
59-
safe_print(_('🐍 Python {} found on your system. Adopting it...').format(required_version_str))
60-
if pkg_instance.adopt_interpreter(required_version_str) != 0:
61-
safe_print(_('❌ Failed to adopt Python {}. Please try manually.').format(required_version_str))
62-
safe_print(_(' Run: {} python adopt {}').format(parser_prog, required_version_str))
63-
return False
64-
safe_print(_('✅ Successfully adopted Python {}.').format(required_version_str))
65-
else:
66-
safe_print(_('❌ Required Python version {} not found on your system.').format(required_version_str))
67-
safe_print(_(' Please install Python {} and ensure it is in your PATH.').format(required_version_str))
123+
#
124+
# >>>>>>>> THIS IS THE CRITICAL NEW LOGIC <<<<<<<<
125+
#
126+
# The required version is NOT managed. We must now run the full adopt
127+
# process, which will either find it locally or download it.
128+
safe_print(_('\n - Step 1: Adopting Python {}... (This may trigger a download)').format(required_version_str))
129+
130+
# Call the core adopt_interpreter method. This is the robust function that
131+
# contains the full logic: check local system -> fallback to download.
132+
if pkg_instance.adopt_interpreter(required_version_str) != 0:
133+
safe_print(_(' - ❌ Failed to adopt Python {}. Cannot proceed with healing.').format(required_version_str))
68134
return False
69-
safe_print(_('🔄 Swapping active interpreter to Python {}...').format(required_version_str))
135+
136+
safe_print(_(' - ✅ Successfully adopted Python {}.').format(required_version_str))
137+
#
138+
# >>>>>>>> END OF CRITICAL NEW LOGIC <<<<<<<<
139+
#
140+
141+
# By this point, we are GUARANTEED that the interpreter is managed. Now we can swap.
142+
safe_print(_('\n - Step 2: Swapping active context to Python {}...').format(required_version_str))
70143
if pkg_instance.switch_active_python(required_version_str) != 0:
71-
safe_print(_('❌ Failed to swap to Python {}. Please try manually.').format(required_version_str))
72-
safe_print(_(' Run: {} swap python {}').format(parser_prog, required_version_str))
144+
safe_print(_(' - ❌ Failed to swap to Python {}. Please try manually.').format(required_version_str))
145+
safe_print(_(' Run: {} swap python {}').format(parser_prog, required_version_str))
73146
return False
74-
safe_print(_('✅ Environment successfully configured for Python {}.').format(required_version_str))
75-
safe_print(_('🚀 Proceeding to run the demo...'))
147+
148+
safe_print(_(' - ✅ Environment successfully configured for Python {}.').format(required_version_str))
149+
safe_print(_('🚀 Proceeding...'))
76150
safe_print('=' * 60)
77151
return True
78152

@@ -141,13 +215,17 @@ def stress_test_command():
141215
return False
142216

143217
def run_actual_stress_test():
144-
"""Run the actual stress test - only called if Python 3.11."""
218+
"""Run the actual stress test by locating and executing the test file."""
145219
safe_print(_('🔥 Starting stress test...'))
146220
try:
147-
from . import stress_test
148-
stress_test.run()
149-
except ImportError:
150-
safe_print(_('❌ Stress test module not found. Implementation needed.'))
221+
# Define the correct path to the refactored test file
222+
test_file_path = TESTS_DIR / 'test_version_combos.py'
223+
224+
# Reuse the robust live streaming runner
225+
run_demo_with_live_streaming(
226+
test_file_name=str(test_file_path),
227+
demo_name="Stress Test"
228+
)
151229
except Exception as e:
152230
safe_print(_('❌ An error occurred during stress test execution: {}').format(e))
153231
import traceback
@@ -179,15 +257,16 @@ def run_demo_with_live_streaming(test_file_name: str, demo_name: str, python_exe
179257

180258
# Step 2: Determine the final path to the SCRIPT to be executed.
181259
input_path = Path(test_file_name)
260+
182261
if input_path.is_absolute():
183-
# For temp files, the path is already correct and absolute.
262+
# If we're given an absolute path (like for a temp file), use it directly.
184263
test_file_path = input_path
185264
else:
186-
# For project-internal tests, build the path relative to the context's project root.
187-
source_dir_name = 'omnipkg' if "stress_test" in str(test_file_name) else 'tests'
188-
test_file_path = project_root_in_context / source_dir_name / input_path.name
189-
# --- END: ROBUST PATHING LOGIC ---
190-
265+
# Otherwise, assume all standard demo tests are in the 'tests' directory.
266+
# This is simpler and more reliable than checking filenames.
267+
test_file_path = project_root_in_context / 'tests' / input_path.name
268+
# --- END OF NEW LOGIC ---
269+
191270
safe_print(_('🚀 Running {} demo from source: {}...').format(demo_name.capitalize(), test_file_path))
192271

193272
if not test_file_path.exists():
@@ -355,6 +434,11 @@ def create_parser():
355434
prune_parser.add_argument('package', help=_('Package whose bubbles to prune'))
356435
prune_parser.add_argument('--keep-latest', type=int, metavar='N', help=_('Keep N most recent bubbled versions'))
357436
prune_parser.add_argument('--yes', '-y', dest='force', action='store_true', help=_('Skip confirmation'))
437+
upgrade_parser = subparsers.add_parser('upgrade', help=_('Upgrade omnipkg or other packages to the latest version'))
438+
upgrade_parser.add_argument('package', nargs='?', default='omnipkg', help=_('Package to upgrade (defaults to omnipkg itself)'))
439+
upgrade_parser.add_argument('--version', help=_('Specify a version to upgrade/downgrade to'))
440+
upgrade_parser.add_argument('--yes', '-y', dest='force', action='store_true', help=_('Skip confirmation prompt'))
441+
upgrade_parser.add_argument('--force-dev', action='store_true', help=_('Force upgrade even in a developer environment (use with caution)'))
358442
return parser
359443

360444
def print_header(title):
@@ -532,9 +616,14 @@ def main():
532616
safe_print(_('6. Flask test (under construction)'))
533617
safe_print(_('7. Auto-healing Test (omnipkg run)')) # <--- ADD THIS
534618
safe_print(_('8. 🌠 Quantum Multiverse Warp (Concurrent Python Installations)'))
535-
safe_print(_('9. Flask Port Finder Test (auto-healing with Flask)')) # <--- ADD THIS
619+
safe_print(_('9. Flask Port Finder Test (auto-healing with Flask)')) # <-- ADD THIS LINE
620+
def run_and_stream(cmd_list):
621+
process = subprocess.Popen(cmd_list, text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, encoding='utf-8', errors='replace')
622+
for line in process.stdout:
623+
safe_print(line, end='')
624+
return process.wait()
536625
try:
537-
response = input(_('Enter your choice (1-9): ')).strip()
626+
response = input(_('Enter your choice (1-8): ')).strip()
538627
except EOFError:
539628
response = ''
540629
test_file = None
@@ -548,8 +637,44 @@ def main():
548637
return 1
549638
return run_demo_with_live_streaming(str(test_file), demo_name)
550639
elif response == '2':
551-
test_file = TESTS_DIR / 'test_uv_switching.py'
640+
# INSERT THESE LINES HERE (after line 586):
641+
actual_version = get_actual_python_version()
642+
actual_version_str = f"{actual_version[0]}.{actual_version[1]}"
643+
552644
demo_name = 'uv'
645+
demo_name = 'uv'
646+
source_script_path = TESTS_DIR / 'test_uv_switching.py'
647+
if not source_script_path.exists():
648+
safe_print(f'❌ Error: Source test file {source_script_path} not found.')
649+
return 1
650+
651+
# Get the Python executable for the current version
652+
python_exe = pkg_instance.config_manager.get_interpreter_for_version(actual_version_str)
653+
if not python_exe or not python_exe.exists():
654+
safe_print(f"❌ Python {actual_version_str} is not managed by omnipkg.")
655+
safe_print(f" Please adopt it first: {parser.prog} python adopt {actual_version_str}")
656+
return 1
657+
658+
safe_print(f'🚀 Running {demo_name} demo with Python {actual_version_str} via sterile environment...')
659+
660+
# Create sterile copy
661+
with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False, encoding='utf-8') as temp_script:
662+
temp_script_path = Path(temp_script.name)
663+
temp_script.write(source_script_path.read_text(encoding='utf-8'))
664+
665+
safe_print(f" - Sterile script created at: {temp_script_path}")
666+
667+
returncode = 1
668+
try:
669+
# Use run_demo_with_live_streaming with the detected Python version
670+
return run_demo_with_live_streaming(
671+
test_file_name=str(temp_script_path),
672+
demo_name=demo_name,
673+
python_exe=str(python_exe)
674+
)
675+
finally:
676+
temp_script_path.unlink(missing_ok=True)
677+
553678
elif response == '3':
554679
if not handle_python_requirement('3.11', pkg_instance, parser.prog):
555680
return 1
@@ -567,37 +692,14 @@ def main():
567692
safe_print(_(' Creating a sterile temporary copy to ensure a clean run...'))
568693
safe_print('!'*60)
569694

570-
# 1. Find the source script.
571-
source_script_path = TESTS_DIR / 'test_multiverse_healing.py'
695+
return run_demo_with_enforced_context(
696+
source_script_path=TESTS_DIR / 'test_multiverse_healing.py',
697+
demo_name='multiverse_healing',
698+
pkg_instance=pkg_instance,
699+
parser_prog=parser.prog,
700+
required_version='3.11' # This test requires 3.11 specifically
701+
)
572702

573-
if not source_script_path.exists():
574-
safe_print(_('❌ Error: Source test file {} not found.').format(source_script_path))
575-
return 1
576-
577-
# 2. Create a temporary, sterile copy of the script.
578-
with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False, encoding='utf-8') as temp_script:
579-
temp_script_path = Path(temp_script.name)
580-
temp_script.write(source_script_path.read_text(encoding='utf-8'))
581-
582-
safe_print(f" - Sterile script created at: {temp_script_path}")
583-
584-
try:
585-
# 3. Get the required Python 3.11 interpreter.
586-
python_311_exe = pkg_instance.config_manager.get_interpreter_for_version('3.11')
587-
if not python_311_exe or not python_311_exe.exists():
588-
safe_print("❌ Python 3.11 is required and not managed by omnipkg.")
589-
safe_print(" Please adopt it first: omnipkg python adopt 3.11")
590-
return 1
591-
592-
# 4. Execute the STERILE script, which will have a clean sys.path.
593-
return run_demo_with_live_streaming(
594-
test_file_name=str(temp_script_path), # Use the absolute path to the temp file
595-
demo_name='multiverse_healing',
596-
python_exe=str(python_311_exe)
597-
)
598-
finally:
599-
# 5. Clean up the temporary script no matter what.
600-
temp_script_path.unlink(missing_ok=True)
601703
elif response == '6':
602704
test_file = TESTS_DIR / 'test_rich_switching.py'
603705
demo_name = 'rich'
@@ -684,7 +786,7 @@ def main():
684786
safe_print(_('❌ Demo failed with return code {}').format(returncode))
685787
return returncode
686788
else:
687-
safe_print(_('❌ Invalid choice. Please select 1, 2, 3, 4, 5, 6, 7, 8, or 9.'))
789+
safe_print(_('❌ Invalid choice. Please select 1, 2, 3, 4, 5, 6, 7, 8 or 9.'))
688790
return 1
689791
if not test_file.exists():
690792
safe_print(_('❌ Error: Test file {} not found.').format(test_file))
@@ -752,6 +854,14 @@ def main():
752854
return pkg_instance.reset_configuration(force=args.force)
753855
elif args.command == 'run':
754856
return execute_run_command(args.script_and_args, cm, verbose=args.verbose)
857+
elif args.command == 'upgrade':
858+
if args.package.lower() == 'omnipkg':
859+
return pkg_instance.smart_upgrade(version=args.version, force=args.force, skip_dev_check=args.force_dev)
860+
861+
else:
862+
# Upgrading other packages is just a reinstall of the latest
863+
safe_print(_("Redirecting to smart_install to get the latest version of '{}'...").format(args.package))
864+
return pkg_instance.smart_install([args.package], force_reinstall=True)
755865
else:
756866
parser.print_help()
757867
safe_print(_("\n💡 Did you mean 'omnipkg config set language <code>'?"))
@@ -762,6 +872,7 @@ def main():
762872
except Exception as e:
763873
safe_print(_('\n❌ An unexpected error occurred: {}').format(e))
764874
import traceback
875+
765876
traceback.print_exc()
766877
return 1
767878
if __name__ == '__main__':

0 commit comments

Comments
 (0)