Skip to content

Commit d444e2d

Browse files
Add a read-only context manager for YAML files to prevent round-trip failures
1 parent 040cdab commit d444e2d

8 files changed

Lines changed: 101 additions & 13 deletions

File tree

src/usethis/_integrations/ci/bitbucket/cache.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from usethis._integrations.ci.bitbucket.dump import bitbucket_fancy_dump
77
from usethis._integrations.ci.bitbucket.io_ import (
88
edit_bitbucket_pipelines_yaml,
9+
read_bitbucket_pipelines_yaml,
910
)
1011
from usethis._integrations.ci.bitbucket.schema import Definitions
1112
from usethis._integrations.file.yaml.update import update_ruamel_yaml_map
@@ -16,7 +17,7 @@
1617

1718

1819
def get_cache_by_name() -> dict[str, Cache]:
19-
with edit_bitbucket_pipelines_yaml() as doc:
20+
with read_bitbucket_pipelines_yaml() as doc:
2021
config = doc.model
2122

2223
if config.definitions is None:

src/usethis/_integrations/ci/bitbucket/io_.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from usethis._config import usethis_config
1010
from usethis._console import tick_print
1111
from usethis._integrations.ci.bitbucket.schema import PipelinesConfiguration
12-
from usethis._integrations.file.yaml.io_ import edit_yaml
12+
from usethis._integrations.file.yaml.io_ import edit_yaml, read_yaml
1313
from usethis.errors import FileConfigError
1414

1515
if TYPE_CHECKING:
@@ -66,6 +66,20 @@ def edit_bitbucket_pipelines_yaml() -> Generator[
6666
_validate_config(doc.content)
6767

6868

69+
@contextmanager
70+
def read_bitbucket_pipelines_yaml() -> Generator[
71+
BitbucketPipelinesYAMLDocument, None, None
72+
]:
73+
"""A context manager to read 'bitbucket-pipelines.yml'."""
74+
name = "bitbucket-pipelines.yml"
75+
path = usethis_config.cpd() / name
76+
77+
with read_yaml(path) as doc:
78+
config = _validate_config(doc.content)
79+
yield BitbucketPipelinesYAMLDocument(content=doc.content, model=config)
80+
_validate_config(doc.content)
81+
82+
6983
def _validate_config(ruamel_content: YAMLLiteral) -> PipelinesConfiguration:
7084
try:
7185
return PipelinesConfiguration.model_validate(ruamel_content)

src/usethis/_integrations/ci/bitbucket/steps.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,10 @@
1818
from usethis._integrations.ci.bitbucket.cache import _add_caches_via_doc, remove_cache
1919
from usethis._integrations.ci.bitbucket.dump import bitbucket_fancy_dump
2020
from usethis._integrations.ci.bitbucket.errors import UnexpectedImportPipelineError
21-
from usethis._integrations.ci.bitbucket.io_ import edit_bitbucket_pipelines_yaml
21+
from usethis._integrations.ci.bitbucket.io_ import (
22+
edit_bitbucket_pipelines_yaml,
23+
read_bitbucket_pipelines_yaml,
24+
)
2225
from usethis._integrations.ci.bitbucket.pipeweld import (
2326
apply_pipeweld_instruction_via_doc,
2427
get_pipeweld_pipeline_from_default,
@@ -403,7 +406,7 @@ def get_steps_in_default() -> list[Step]:
403406
if not (usethis_config.cpd() / "bitbucket-pipelines.yml").exists():
404407
return []
405408

406-
with edit_bitbucket_pipelines_yaml() as doc:
409+
with read_bitbucket_pipelines_yaml() as doc:
407410
config = doc.model
408411

409412
if config.pipelines is None:

src/usethis/_integrations/file/yaml/io_.py

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -468,19 +468,30 @@ def edit_yaml(
468468
guess_indent: bool = True,
469469
) -> Generator[YAMLDocument, None, None]:
470470
"""A context manager to modify a YAML file in-place, with managed read and write."""
471+
with read_yaml(yaml_path, guess_indent=guess_indent) as yaml_document:
472+
yield yaml_document
473+
start_empty = not yaml_document.content
474+
if start_empty and not yaml_document.content:
475+
# No change
476+
return
477+
yaml_document.roundtripper.dump(yaml_document.content, stream=yaml_path)
478+
479+
480+
@contextmanager
481+
def read_yaml(
482+
yaml_path: Path,
483+
*,
484+
guess_indent: bool = True,
485+
) -> Generator[YAMLDocument, None, None]:
486+
"""A context manager to read a YAML file."""
471487
with yaml_path.open(mode="r") as f:
472488
try:
473489
yaml_document = _get_yaml_document(f, guess_indent=guess_indent)
474490
except YAMLError as err:
475491
msg = f"Error reading '{yaml_path}':\n{err}"
476492
raise YAMLDecodeError(msg) from None
477493

478-
start_empty = not yaml_document.content
479494
yield yaml_document
480-
if start_empty and not yaml_document.content:
481-
# No change
482-
return
483-
yaml_document.roundtripper.dump(yaml_document.content, stream=yaml_path)
484495

485496

486497
def _get_yaml_document(

src/usethis/_integrations/pre_commit/hooks.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@
66
from usethis._console import box_print, tick_print
77
from usethis._integrations.file.yaml.update import update_ruamel_yaml_map
88
from usethis._integrations.pre_commit.dump import pre_commit_fancy_dump
9-
from usethis._integrations.pre_commit.io_ import edit_pre_commit_config_yaml
9+
from usethis._integrations.pre_commit.io_ import (
10+
edit_pre_commit_config_yaml,
11+
read_pre_commit_config_yaml,
12+
)
1013
from usethis._integrations.pre_commit.schema import (
1114
HookDefinition,
1215
Language,
@@ -232,7 +235,7 @@ def get_hook_ids() -> list[str]:
232235
if not path.exists():
233236
return []
234237

235-
with edit_pre_commit_config_yaml() as doc:
238+
with read_pre_commit_config_yaml() as doc:
236239
return extract_hook_ids(doc.model)
237240

238241

src/usethis/_integrations/pre_commit/io_.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
from usethis._config import usethis_config
1111
from usethis._console import tick_print
12-
from usethis._integrations.file.yaml.io_ import edit_yaml
12+
from usethis._integrations.file.yaml.io_ import edit_yaml, read_yaml
1313
from usethis._integrations.pre_commit.schema import JsonSchemaForPreCommitConfigYaml
1414
from usethis.errors import FileConfigError
1515

@@ -60,6 +60,18 @@ def edit_pre_commit_config_yaml() -> Generator[PreCommitConfigYAMLDocument, None
6060
_validate_config(doc.content)
6161

6262

63+
@contextmanager
64+
def read_pre_commit_config_yaml() -> Generator[PreCommitConfigYAMLDocument, None, None]:
65+
"""A context manager to read '.pre-commit-config.yaml'."""
66+
name = ".pre-commit-config.yaml"
67+
path = usethis_config.cpd() / name
68+
69+
with read_yaml(path) as doc:
70+
config = _validate_config(doc.content)
71+
yield PreCommitConfigYAMLDocument(content=doc.content, model=config)
72+
_validate_config(doc.content)
73+
74+
6375
def _validate_config(ruamel_content: YAMLLiteral) -> JsonSchemaForPreCommitConfigYaml:
6476
if isinstance(ruamel_content, CommentedMap) and not ruamel_content:
6577
ruamel_content = CommentedMap({"repos": []})

tests/usethis/_integrations/ci/bitbucket/test_bitbucket_io_.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from usethis._integrations.ci.bitbucket.io_ import (
66
BitbucketPipelinesYAMLConfigError,
77
edit_bitbucket_pipelines_yaml,
8+
read_bitbucket_pipelines_yaml,
89
)
910
from usethis._test import change_cwd
1011

@@ -147,3 +148,24 @@ def test_invalid_contents(self, tmp_path: Path):
147148
edit_bitbucket_pipelines_yaml() as _,
148149
):
149150
pass
151+
152+
153+
class TestReadBitbucketPipelinesYAML:
154+
def test_quote_style_preserved(self, tmp_path: Path):
155+
# Arrange
156+
content_str = """\
157+
pipelines:
158+
default:
159+
- step:
160+
script:
161+
- 'echo'
162+
"""
163+
164+
(tmp_path / "bitbucket-pipelines.yml").write_text(content_str)
165+
166+
# Act
167+
with change_cwd(tmp_path), read_bitbucket_pipelines_yaml():
168+
pass
169+
170+
# Assert
171+
assert (tmp_path / "bitbucket-pipelines.yml").read_text() == content_str

tests/usethis/_integrations/pre_commit/test_pre_commit_io_.py

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
from pathlib import Path
22

3-
from usethis._integrations.pre_commit.io_ import edit_pre_commit_config_yaml
3+
from usethis._integrations.pre_commit.io_ import (
4+
edit_pre_commit_config_yaml,
5+
read_pre_commit_config_yaml,
6+
)
47
from usethis._test import change_cwd
58

69

@@ -54,3 +57,22 @@ def test_empty_valid_but_unchanged(self, tmp_path: Path):
5457

5558
# Assert
5659
assert (tmp_path / ".pre-commit-config.yaml").read_text() == ""
60+
61+
62+
class TestReadPreCommitConfigYAML:
63+
def test_quote_style_preserved(self, tmp_path: Path):
64+
# Arrange
65+
content_str = """\
66+
repos:
67+
- repo: 'https://github.com/abravalheri/validate-pyproject'
68+
rev: 'v0.23'
69+
"""
70+
71+
(tmp_path / ".pre-commit-config.yaml").write_text(content_str)
72+
73+
# Act
74+
with change_cwd(tmp_path), read_pre_commit_config_yaml():
75+
pass
76+
77+
# Assert
78+
assert (tmp_path / ".pre-commit-config.yaml").read_text() == content_str

0 commit comments

Comments
 (0)