diff --git a/environment.yml b/environment.yml index 6ef91fbe5..8a03713c5 100644 --- a/environment.yml +++ b/environment.yml @@ -7,6 +7,6 @@ dependencies: - pandas>=0.21.0,<4.0.0 - numpy>=1.13.0,<3.0.0 - scikit-learn>=1.0.0,<2.0.0 - - pyjuliacall>=0.9.28,<0.9.29 + - pyjuliacall>=0.9.28,<0.9.36 - click>=7.0.0,<9.0.0 - typing-extensions>=4.0.0,<5.0.0 diff --git a/pyproject.toml b/pyproject.toml index fccd37f7a..6d2af2c4c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,7 +22,7 @@ dependencies = [ "pandas>=0.21.0,<4.0.0", "numpy>=1.13.0,<3.0.0", "scikit_learn>=1.0.0,<2.0.0", - "juliacall>=0.9.28,<0.9.29", + "juliacall>=0.9.28,<0.9.36", "click>=7.0.0,<9.0.0", "typing-extensions>=4.0.0,<5.0.0", ] @@ -35,15 +35,15 @@ docs = [ dev = [ "coverage>=7,<8", "beartype>=0.19,<0.23", - "ipykernel>=6,<7", + "ipykernel>=6,<8", "ipython>=8,<9", - "jax[cpu]>=0.4,<0.6", + "jax[cpu]>=0.4,<0.11", "jupyter>=1,<2", "mypy>=1,<3", "nbval>=0.11,<0.12", "pandas-stubs", "pre-commit>=3.0,<5", - "pytest-cov>=5,<7", + "pytest-cov>=5,<8", "pytest>=8,<10", "tensorboard>=2,<3", "torch>=2,<3", diff --git a/pysr/julia_registry_helpers.py b/pysr/julia_registry_helpers.py index ad34a98b1..12a3e5321 100644 --- a/pysr/julia_registry_helpers.py +++ b/pysr/julia_registry_helpers.py @@ -3,6 +3,7 @@ from __future__ import annotations import os +import subprocess import warnings from collections.abc import Callable from typing import TypeVar @@ -12,6 +13,36 @@ PREFERENCE_KEY = "JULIA_PKG_SERVER_REGISTRY_PREFERENCE" +def _contains_registry_operation(value) -> bool: + if isinstance(value, (list, tuple)): + text = "\n".join(str(item) for item in value) + else: + text = str(value) + return "Pkg.Registry" in text or "/registry/" in text + + +def _is_registry_error(error: Exception) -> bool: + error_type = str(type(error)) + error_message = str(error) + if "JuliaError" in error_type: + return any( + phrase in error_message + for phrase in ( + "Unsatisfiable requirements detected", + "could not download", + "/registry/", + "Registry.update", + ) + ) + if isinstance(error, subprocess.CalledProcessError): + return any( + _contains_registry_operation(value) + for value in (error.cmd, error.output, error.stderr) + if value is not None + ) + return False + + def try_with_registry_fallback(f: Callable[..., T], *args, **kwargs) -> T: """Execute function with modified Julia registry preference. @@ -22,10 +53,7 @@ def try_with_registry_fallback(f: Callable[..., T], *args, **kwargs) -> T: try: return f(*args, **kwargs) except Exception as initial_error: - # Check if this is a Julia registry error by looking at the error message - if "JuliaError" not in str( - type(initial_error) - ) or "Unsatisfiable requirements detected" not in str(initial_error): + if not _is_registry_error(initial_error): raise initial_error old_value = os.environ.get(PREFERENCE_KEY, None) diff --git a/pysr/test/test_startup.py b/pysr/test/test_startup.py index 8bd81cd93..05c984452 100644 --- a/pysr/test/test_startup.py +++ b/pysr/test/test_startup.py @@ -210,6 +210,31 @@ def test_julia_error_triggers_fallback(self): # Verify environment is restored self.assertEqual(os.environ[PREFERENCE_KEY], "conservative") + def test_subprocess_registry_error_triggers_fallback(self): + os.environ[PREFERENCE_KEY] = "conservative" + attempts = [] + + def failing_then_successful_operation(): + attempts.append(os.environ[PREFERENCE_KEY]) + if len(attempts) == 1: + raise subprocess.CalledProcessError( + 1, + [ + "julia", + "--startup-file=no", + "-e", + "import Pkg\nPkg.Registry.update()\nPkg.add([])", + ], + ) + return "success" + + with self.assertWarns(Warning): + result = try_with_registry_fallback(failing_then_successful_operation) + + self.assertEqual(result, "success") + self.assertEqual(attempts, ["conservative", "eager"]) + self.assertEqual(os.environ[PREFERENCE_KEY], "conservative") + def test_eager_mode_fails_directly(self): os.environ[PREFERENCE_KEY] = "eager"