Skip to content

Commit 301a914

Browse files
authored
Fix!: preserve yaml order (#1054)
1 parent ab004c1 commit 301a914

File tree

8 files changed

+33
-22
lines changed

8 files changed

+33
-22
lines changed

sqlmesh/core/schema_loader.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from sqlmesh.core.engine_adapter import EngineAdapter
1111
from sqlmesh.core.model import Model
1212
from sqlmesh.core.state_sync import StateReader
13-
from sqlmesh.utils.yaml import YAML
13+
from sqlmesh.utils import yaml
1414

1515
logger = logging.getLogger(__name__)
1616

@@ -62,4 +62,4 @@ def create_schema_file(
6262
]
6363

6464
with open(path, "w", encoding="utf-8") as file:
65-
YAML().dump(schemas, file)
65+
yaml.dump(schemas, file)

sqlmesh/dbt/builtin.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,7 @@ def from_json(value: str, default: t.Optional[t.Any] = None) -> t.Optional[t.Any
204204

205205
def to_yaml(value: t.Any, default: t.Optional[t.Any] = None) -> t.Optional[t.Any]:
206206
try:
207-
return yaml.dumps(value)
207+
return yaml.dump(value)
208208
except (TypeError, YAMLError):
209209
return default
210210

sqlmesh/dbt/common.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
}
3535

3636

37-
def load_yaml(source: str | Path) -> t.OrderedDict:
37+
def load_yaml(source: str | Path) -> t.Dict:
3838
try:
3939
return load(source, render_jinja=False)
4040
except DuplicateKeyError as ex:

sqlmesh/dbt/profile.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@
88
from sqlmesh.dbt.common import PROJECT_FILENAME, load_yaml
99
from sqlmesh.dbt.context import DbtContext
1010
from sqlmesh.dbt.target import TargetConfig
11+
from sqlmesh.utils import yaml
1112
from sqlmesh.utils.errors import ConfigError
12-
from sqlmesh.utils.yaml import dumps as dump_yaml
1313

1414
logger = logging.getLogger(__name__)
1515

@@ -105,7 +105,7 @@ def _read_profile(
105105
f"Target '{target_name}' not specified in profiles for '{context.profile_name}'."
106106
)
107107

108-
target_fields = load_yaml(context.render(dump_yaml(outputs[target_name])))
108+
target_fields = load_yaml(context.render(yaml.dump(outputs[target_name])))
109109
target = TargetConfig.load(
110110
{"name": target_name, "profile_name": context.profile_name, **target_fields}
111111
)

sqlmesh/magics.py

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,8 @@
2121
from sqlmesh.core.dialect import format_model_expressions, parse
2222
from sqlmesh.core.model import load_model
2323
from sqlmesh.core.test import ModelTestMetadata, get_all_model_tests
24+
from sqlmesh.utils import yaml
2425
from sqlmesh.utils.errors import MagicError, MissingContextException
25-
from sqlmesh.utils.yaml import YAML
26-
from sqlmesh.utils.yaml import dumps as yaml_dumps
27-
from sqlmesh.utils.yaml import load as yaml_load
2826

2927
CONTEXT_VARIABLE_NAMES = [
3028
"context",
@@ -208,8 +206,8 @@ def test(self, line: str, test_def_raw: t.Optional[str] = None) -> None:
208206
return
209207

210208
test = tests[model.name][args.test_name]
211-
test_def = yaml_load(test_def_raw) if test_def_raw else test.body
212-
test_def_output = yaml_dumps(test_def)
209+
test_def = yaml.load(test_def_raw) if test_def_raw else test.body
210+
test_def_output = yaml.dump(test_def)
213211

214212
self._shell.set_next_input(
215213
"\n".join(
@@ -222,10 +220,10 @@ def test(self, line: str, test_def_raw: t.Optional[str] = None) -> None:
222220
)
223221

224222
with open(test.path, "r+", encoding="utf-8") as file:
225-
content = yaml_load(file.read())
223+
content = yaml.load(file.read())
226224
content[args.test_name] = test_def
227225
file.seek(0)
228-
YAML().dump(content, file)
226+
yaml.dump(content, file)
229227
file.truncate()
230228

231229
@magic_arguments()

sqlmesh/utils/yaml.py

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
import io
44
import typing as t
5-
from collections import OrderedDict
65
from os import getenv
76
from pathlib import Path
87

@@ -23,7 +22,7 @@ def load(
2322
raise_if_empty: bool = True,
2423
render_jinja: bool = True,
2524
allow_duplicate_keys: bool = False,
26-
) -> t.OrderedDict:
25+
) -> t.Dict:
2726
"""Loads a YAML object from either a raw string or a file."""
2827
path: t.Optional[Path] = None
2928

@@ -42,13 +41,26 @@ def load(
4241
if raise_if_empty:
4342
error_path = f" '{path}'" if path else ""
4443
raise SQLMeshError(f"YAML source{error_path} can't be empty.")
45-
return OrderedDict()
44+
return {}
4645

4746
return contents
4847

4948

50-
def dumps(value: dict | yaml.CommentedMap | OrderedDict) -> str:
51-
"""Dumps a ruamel.yaml loaded object and converts it into a string"""
49+
@t.overload
50+
def dump(value: t.Any, stream: io.IOBase) -> None:
51+
...
52+
53+
54+
@t.overload
55+
def dump(value: t.Any) -> str:
56+
...
57+
58+
59+
def dump(value: t.Any, stream: t.Optional[io.IOBase] = None) -> t.Optional[str]:
60+
"""Dumps a ruamel.yaml loaded object and converts it into a string or writes it to a stream."""
5261
result = io.StringIO()
53-
yaml.YAML().dump(value, result)
62+
yaml.YAML().dump(value, stream or result)
63+
64+
if stream:
65+
return None
5466
return result.getvalue()

tests/core/test_schema_loader.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,8 +95,8 @@ def test_create_external_models(tmpdir, assert_exp_eq):
9595
def test_no_internal_model_conversion(tmp_path: Path, mocker: MockerFixture):
9696
engine_adapter_mock = mocker.Mock()
9797
engine_adapter_mock.columns.return_value = {
98-
"a": exp.DataType.build("bigint"),
9998
"b": exp.DataType.build("text"),
99+
"a": exp.DataType.build("bigint"),
100100
}
101101

102102
state_reader_mock = mocker.Mock()
@@ -112,3 +112,4 @@ def test_no_internal_model_conversion(tmp_path: Path, mocker: MockerFixture):
112112

113113
assert len(schema) == 1
114114
assert schema[0]["name"] == "tbl_c"
115+
assert list(schema[0]["columns"]) == ["b", "a"]

tests/utils/test_yaml.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ def test_yaml() -> None:
1717
password: "{{ env_var('__SQLMESH_TEST_ENV_PASSWORD__') }}"
1818
"""
1919

20-
assert contents == yaml.dumps(yaml.load(contents, render_jinja=False))
20+
assert contents == yaml.dump(yaml.load(contents, render_jinja=False))
2121

2222
expected_contents = """profile:
2323
target: prod
@@ -32,7 +32,7 @@ def test_yaml() -> None:
3232
os.environ["__SQLMESH_TEST_ENV_USER__"] = "user"
3333
os.environ["__SQLMESH_TEST_ENV_PASSWORD__"] = "password"
3434

35-
assert expected_contents == yaml.dumps(yaml.load(contents))
35+
assert expected_contents == yaml.dump(yaml.load(contents))
3636

3737
# Return the environment to its previous state
3838
del os.environ["__SQLMESH_TEST_ENV_USER__"]

0 commit comments

Comments
 (0)