Repository: skypilot-org/skypilot (Python)

Issue:
Depending on dev releases makes conda-forge release impossible
I maintain the conda-forge feedstock for skypilot at https://github.com/conda-forge/skypilot-feedstock/. This makes it possible to install skypilot via `conda install skypilot`.

A recent PR https://github.com/skypilot-org/skypilot/pull/5197 adds a dependency on the 2.4 series of omegaconf for the skypilot v0.9.0 release:
https://github.com/skypilot-org/skypilot/blob/58a4a9751b6f77de1ba611209825dfab7d66c61a/sky/setup_files/dependencies.py#L56

There is no stable 2.4 release of omegaconf: https://pypi.org/project/omegaconf/#history

Judging by their release cadence, I don't think we can expect an imminent release.

Conda-forge has a policy against publishing dev versions. Therefore, we cannot publish omegaconf v2.4 until they make a stable release. Correspondingly, skypilot v0.9.0 is not installable from conda-forge, so conda-forge users are currently stuck on v0.8.1.


Discussion:
Thank you flagging this @maresb, and thank you for maintaining the conda skypilot feedstock. We have a similar [issue for uv based installation](https://github.com/skypilot-org/skypilot/issues/5355), we hope to have this resolved by the next stable release in a couple of weeks. 
This is being fixed by #5375



Patch:
diff --git a/.github/workflows/release-build.yml b/.github/workflows/release-build.yml
index c8f57c3060b..7eabc904b71 100644
--- a/.github/workflows/release-build.yml
+++ b/.github/workflows/release-build.yml
@@ -117,7 +117,7 @@ jobs:
           source ~/pypi-check-env/bin/activate
 
           echo "Installing skypilot==${LATEST_PYPI_VERSION} from PyPI using uv..."
-          uv pip install --prerelease=allow "azure-cli>=2.65.0" "omegaconf>=2.4.0dev3"
+          uv pip install --prerelease=allow "azure-cli>=2.65.0"
           uv pip install skypilot==${LATEST_PYPI_VERSION}
 
           echo "Extracting API version from installed PyPI package..."
diff --git a/Dockerfile b/Dockerfile
index 43e0f75e068..ddb22c86d67 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -25,7 +25,7 @@ RUN conda install -c conda-forge google-cloud-sdk && \
     rm kubectl && \
     # Install uv and skypilot
     curl -LsSf https://astral.sh/uv/install.sh | sh && \
-    ~/.local/bin/uv pip install --prerelease allow 'azure-cli>=2.65.0' 'omegaconf>=2.4.0dev3' --system && \
+    ~/.local/bin/uv pip install --prerelease allow azure-cli --system && \
     ~/.local/bin/uv pip install skypilot-nightly[all] --system && \
     # Cleanup all caches to reduce the image size
     conda clean -afy && \
diff --git a/docs/source/getting-started/installation.rst b/docs/source/getting-started/installation.rst
index 8f4a7518f70..125c5e058ec 100644
--- a/docs/source/getting-started/installation.rst
+++ b/docs/source/getting-started/installation.rst
@@ -132,10 +132,10 @@ Installing via ``uv`` is also supported:
 .. code-block:: shell
 
   uv venv --seed --python 3.10
-  uv pip install --prerelease allow 'azure-cli>=2.65.0'
-  # Explicitly install prerelease dependency to work around https://docs.astral.sh/uv/pip/compatibility/#pre-release-compatibility
-  # Optionally only install specific clouds - e.g. 'skypilot[aws,gcp,kubernetes]'
-  uv pip install 'omegaconf>=2.4.0dev3' 'skypilot[all]'
+  uv pip install "skypilot[kubernetes,aws,gcp]"
+  # Azure CLI has an issue with uv, and requires '--prerelease allow'.
+  uv pip install --prerelease allow azure-cli
+  uv pip install "skypilot[all]"
 
 
 Alternatively, we also provide a :ref:`Docker image <docker-image>` as a quick way to try out SkyPilot.
diff --git a/sky/setup_files/dependencies.py b/sky/setup_files/dependencies.py
index b1ae53a2048..0938af28dac 100644
--- a/sky/setup_files/dependencies.py
+++ b/sky/setup_files/dependencies.py
@@ -53,7 +53,6 @@
     'aiofiles',
     'httpx',
     'setproctitle',
-    'omegaconf>=2.4.0dev3,<2.5',
 ]
 
 local_ray = [
diff --git a/sky/skypilot_config.py b/sky/skypilot_config.py
index 75653cb01b1..ec0a6534a68 100644
--- a/sky/skypilot_config.py
+++ b/sky/skypilot_config.py
@@ -56,8 +56,6 @@
 import typing
 from typing import Any, Dict, Iterator, List, Optional, Tuple
 
-from omegaconf import OmegaConf
-
 from sky import exceptions
 from sky import sky_logging
 from sky.adaptors import common as adaptors_common
@@ -321,6 +319,31 @@ def _parse_config_file(config_path: str) -> config_utils.Config:
     return config
 
 
+def _parse_dotlist(dotlist: List[str]) -> config_utils.Config:
+    """Parse a comma-separated list of key-value pairs into a dictionary.
+
+    Args:
+        dotlist: A comma-separated list of key-value pairs.
+
+    Returns:
+        A config_utils.Config object with the parsed key-value pairs.
+    """
+    config: config_utils.Config = config_utils.Config()
+    for arg in dotlist:
+        try:
+            key, value = arg.split('=', 1)
+        except ValueError as e:
+            raise ValueError(f'Invalid config override: {arg}. '
+                             'Please use the format: key=value') from e
+        if len(key) == 0 or len(value) == 0:
+            raise ValueError(f'Invalid config override: {arg}. '
+                             'Please use the format: key=value')
+        value = yaml.safe_load(value)
+        nested_keys = tuple(key.split('.'))
+        config.set_nested(nested_keys, value)
+    return config
+
+
 def _reload_config_from_internal_file(internal_config_path: str) -> None:
     global _dict, _loaded_config_path
     # Reset the global variables, to avoid using stale values.
@@ -483,11 +506,9 @@ def _compose_cli_config(cli_config: Optional[List[str]]) -> config_utils.Config:
                     'Cannot use multiple --config flags with a config file.')
             config_source = maybe_config_path
             # cli_config is a path to a config file
-            parsed_config = OmegaConf.to_object(
-                OmegaConf.load(maybe_config_path))
+            parsed_config = _parse_config_file(maybe_config_path)
         else:  # cli_config is a comma-separated list of key-value pairs
-            parsed_config = OmegaConf.to_object(
-                OmegaConf.from_dotlist(cli_config))
+            parsed_config = _parse_dotlist(cli_config)
         _validate_config(parsed_config, config_source)
     except ValueError as e:
         raise ValueError(f'Invalid config override: {cli_config}. '


Test patch:
diff --git a/tests/test_config.py b/tests/test_config.py
index 87d74443b68..9e275f8d174 100644
--- a/tests/test_config.py
+++ b/tests/test_config.py
@@ -751,6 +751,48 @@ def test_hierarchical_client_config(monkeypatch, tmp_path):
                                       None) == ['azure', 'kubernetes']
 
 
+def test_parse_dotlist():
+    dotlist = ['key1=value1', 'key2']
+    with pytest.raises(ValueError, match='Invalid config override'):
+        skypilot_config._parse_dotlist(dotlist)
+
+    dotlist = ['key1=value1', 'key2=']
+    with pytest.raises(ValueError, match='Invalid config override'):
+        skypilot_config._parse_dotlist(dotlist)
+
+    # test parsing multiple parameters
+    dotlist = ['key1=value1', 'key2=value2']
+    config = skypilot_config._parse_dotlist(dotlist)
+    assert config.get_nested(('key1',), None) == 'value1'
+    assert config.get_nested(('key2',), None) == 'value2'
+
+    # test parsing nested parameters
+    dotlist = ['key1.key2=value1']
+    config = skypilot_config._parse_dotlist(dotlist)
+    assert config.get_nested(('key1', 'key2'), None) == 'value1'
+
+    # test parsing list parameters
+    dotlist = ['key1=[1,2,3]']
+    config = skypilot_config._parse_dotlist(dotlist)
+    assert config.get_nested(('key1',), None) == [1, 2, 3]
+
+    # test parsing map parameters
+    dotlist = ['key1.key2={"key3": "value3"}']
+    config = skypilot_config._parse_dotlist(dotlist)
+    assert config.get_nested(('key1', 'key2', 'key3'), None) == 'value3'
+
+    # test parsing complex parameters
+    dotlist = ['key1.key2={"key3": [1,2,3], "key4": {"key5": "value5"}}']
+    config = skypilot_config._parse_dotlist(dotlist)
+    assert config.get_nested(('key1', 'key2', 'key3'), None) == [1, 2, 3]
+    assert config.get_nested(('key1', 'key2', 'key4', 'key5'), None) == 'value5'
+
+    # test parsing values with special characters
+    dotlist = ['key1="a,b,c=d"']
+    config = skypilot_config._parse_dotlist(dotlist)
+    assert config.get_nested(('key1',), None) == 'a,b,c=d'
+
+
 @mock.patch('sky.skylet.constants.SKIPPED_CLIENT_OVERRIDE_KEYS',
             [('aws', 'vpc_name')])
 def test_override_skypilot_config_with_disallowed_keys(monkeypatch, tmp_path):