Repository: sktime/sktime
Pull request #8585 (2025-07-20 14:37:18)

[BUG] ensure `deps` dependency inspection is compatible with disjunctions in estimator dependency tags
This PR fixes an unreported bug, namely that the `deps` dependency inspection utility would fail if an estimator in the spec contained a disjunction dependency, for instance, `DartsXGBRegressor`.

This PR fixes the bug by ensuring that disjunctive statements are resolved by picking the first element, to result in one of multiple possible choices for dependencies.

One could add a feature to return a disjunctive dependency, but doing this by default would violate the implicit contract that the return of `deps` results in a list of PEP 440 dependency specifier str (not a disjunction, possibly).


Patch:
diff --git a/docs/source/developer_guide/dependencies.rst b/docs/source/developer_guide/dependencies.rst
index 0a1f0f23f..ae8f10b8c 100644
--- a/docs/source/developer_guide/dependencies.rst
+++ b/docs/source/developer_guide/dependencies.rst
@@ -97,7 +97,7 @@ Of course, attempts at using the module will result in runtime failures or unexp
 
 .. code-block:: python
 
-    from sktime.utils.importing import _safe_import
+    from sktime.utils.dependencies import _safe_import
 
     nn = _safe_import("torch.nn")
 
diff --git a/pyproject.toml b/pyproject.toml
index fd4cc2c57..3ba8bc889 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -274,7 +274,6 @@ tests = [
 # these are for special uses and may be changed or removed at any time
 binder = [
   "jupyter",
-  "pandas<2.0.0",
   "skchange",
 ]
 cython_extras = [
@@ -315,6 +314,20 @@ mlflow_tests = [
   "mlflow<4.0",
   "moto",
 ]
+notebooks = [
+  # needed for the blog post notebooks
+  "matplotlib",
+  "numpy<2",
+  "pmdarima",
+  "seaborn",
+  "tbats",
+  # needed for the examples
+  "dtw-python",
+  "prophet",
+  "pytorch-forecasting",
+  "skpro",
+  "statsforecast",
+]
 numpy1 = [
   "numpy<2.0.0",
 ]
diff --git a/sktime/forecasting/pytorchforecasting.py b/sktime/forecasting/pytorchforecasting.py
index e48695024..8b42570c8 100644
--- a/sktime/forecasting/pytorchforecasting.py
+++ b/sktime/forecasting/pytorchforecasting.py
@@ -123,6 +123,7 @@ class PytorchForecastingTFT(_PytorchForecastingAdapter):
         # CI and test flags
         # -----------------
         "tests:core": True,  # should tests be triggered by framework changes?
+        "tests:skip_all": True,
     }
 
     def __init__(
diff --git a/sktime/registry/_craft.py b/sktime/registry/_craft.py
index 89cf0907e..a5a420755 100644
--- a/sktime/registry/_craft.py
+++ b/sktime/registry/_craft.py
@@ -92,15 +92,22 @@ def build_obj():
 def deps(spec):
     """Get PEP 440 dependency requirements for a craft spec.
 
+    This will result in a list of PEP 440 compatible requirement string.
+
+    In case the spec includes estimators with disjunctions in their requirement
+    specifications, the first disjunctive requirement is returned, i.e.,
+    any disjunctions are resolved by picking the first dependency in the disjunction.
+
     Parameters
     ----------
     spec : str, sktime/skbase compatible object specification
-        i.e., a string that executes to construct an object if all imports were present
+        i.e., a string that executes to construct an object if all imports were present.
         imports inferred are of any classes in the scope of ``all_estimators``
-        option 1: a string that evaluates to an estimator
-        option 2: a sequence of assignments in valid python code,
-            with the object to be defined preceded by a "return"
-            assignments can use names of classes as if all imports were present
+
+        * option 1: a string that evaluates to an estimator
+        * option 2: a sequence of assignments in valid python code,
+          with the object to be defined preceded by a "return".
+          assignments can use names of classes as if all imports were present
 
     Returns
     -------
@@ -122,10 +129,24 @@ def deps(spec):
 
         new_deps = cls.get_class_tag("python_dependencies")
 
-        if isinstance(new_deps, list):
-            dep_strs += new_deps
+        def _resolve_disjunctions(dep):
+            """Resolve disjunctions in dependencies by picking first."""
+            if isinstance(dep, list):
+                return dep[0]  # pick first dependency in disjunction
+            return dep
+
+        if new_deps is None:
+            new_deps_coerced = []
         elif isinstance(new_deps, str) and len(new_deps) > 0:
-            dep_strs += [new_deps]
+            new_deps_coerced = [new_deps]
+        elif isinstance(new_deps, str) and len(new_deps) == 0:
+            new_deps_coerced = []
+        elif isinstance(new_deps, list):
+            new_deps_coerced = [_resolve_disjunctions(dep) for dep in new_deps]
+        else:
+            new_deps_coerced = new_deps
+
+        dep_strs += new_deps_coerced
 
         reqs = list(set(dep_strs))
 


Test patch:
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 2cc29617d..73c0b4587 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -68,7 +68,11 @@ jobs:
       - name: Install dependencies
         run: |
           python -m pip install --upgrade pip
-          python -m pip install .[all_extras,binder,dev,mlflow,dl]
+          python -m pip install .[notebooks,binder,dev]
+
+      - name: Show dependencies
+        run: python -m pip list
+
       - name: Run example notebooks
         run: build_tools/run_examples.sh
         shell: bash
@@ -85,7 +89,11 @@ jobs:
       - name: Install dependencies
         run: |
           python -m pip install --upgrade pip
-          python -m pip install .[all_extras,binder,dev,mlflow]
+          python -m pip install .[notebooks,binder,mlflow]
+
+      - name: Show dependencies
+        run: python -m pip list
+
       - name: Run example notebooks
         run: build_tools/run_blogposts.sh
         shell: bash
diff --git a/sktime/registry/tests/test_craft.py b/sktime/registry/tests/test_craft.py
index 8db41dbe6..c1c2dfe87 100644
--- a/sktime/registry/tests/test_craft.py
+++ b/sktime/registry/tests/test_craft.py
@@ -122,3 +122,8 @@ def test_imports():
         "Imputer"
     )
     assert imports(pipe_spec_with_deps) == pipe_imports
+
+
+def test_deps_with_disjunction():
+    """Check that deps retrieves the correct requirement set for disjunctions."""
+    assert deps("DartsXGBModel") == ["u8darts>=0.29"]
diff --git a/sktime/tests/_config.py b/sktime/tests/_config.py
index 360753c64..ea873dbe9 100644
--- a/sktime/tests/_config.py
+++ b/sktime/tests/_config.py
@@ -77,7 +77,6 @@ EXCLUDE_ESTIMATORS = [
     # Test estimators
     "_TransformChangeNInstances",
     # ptf global models fail the tests, see #7997
-    "PytorchForecastingTFT",
     "PytorchForecastingNBeats",
     "PytorchForecastingNHiTS",
     "PytorchForecastingDeepAR",