path-sync 0.2.0__py3-none-any.whl → 0.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- path_sync/__init__.py +10 -1
- path_sync/__main__.py +3 -3
- path_sync/config.py +14 -0
- path_sync/copy.py +4 -0
- path_sync/sections.py +61 -104
- {path_sync-0.2.0.dist-info → path_sync-0.3.0.dist-info}/METADATA +11 -5
- path_sync-0.3.0.dist-info/RECORD +10 -0
- path_sync-0.3.0.dist-info/licenses/LICENSE +21 -0
- path_sync/cmd_boot.py +0 -89
- path_sync/cmd_copy.py +0 -522
- path_sync/cmd_copy_test.py +0 -159
- path_sync/cmd_validate.py +0 -51
- path_sync/conftest.py +0 -15
- path_sync/file_utils.py +0 -7
- path_sync/git_ops.py +0 -188
- path_sync/header.py +0 -80
- path_sync/header_test.py +0 -41
- path_sync/models.py +0 -142
- path_sync/models_test.py +0 -69
- path_sync/sections_test.py +0 -128
- path_sync/typer_app.py +0 -8
- path_sync/validation.py +0 -84
- path_sync/validation_test.py +0 -114
- path_sync/yaml_utils.py +0 -19
- path_sync-0.2.0.dist-info/RECORD +0 -23
- {path_sync-0.2.0.dist-info → path_sync-0.3.0.dist-info}/WHEEL +0 -0
- {path_sync-0.2.0.dist-info → path_sync-0.3.0.dist-info}/entry_points.txt +0 -0
path_sync/sections_test.py
DELETED
|
@@ -1,128 +0,0 @@
|
|
|
1
|
-
import pytest
|
|
2
|
-
|
|
3
|
-
from path_sync import sections
|
|
4
|
-
|
|
5
|
-
JUSTFILE_CONTENT = """\
|
|
6
|
-
# path-sync copy -n python-template
|
|
7
|
-
|
|
8
|
-
# === OK_EDIT ===
|
|
9
|
-
# Custom variables
|
|
10
|
-
|
|
11
|
-
# === DO_NOT_EDIT: path-sync standard ===
|
|
12
|
-
pre-push: lint test
|
|
13
|
-
# === OK_EDIT ===
|
|
14
|
-
|
|
15
|
-
# === DO_NOT_EDIT: path-sync coverage ===
|
|
16
|
-
cov:
|
|
17
|
-
uv run pytest --cov
|
|
18
|
-
# === OK_EDIT ===
|
|
19
|
-
"""
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
def test_parse_sections():
|
|
23
|
-
result = sections.parse_sections(JUSTFILE_CONTENT)
|
|
24
|
-
assert len(result) == 2
|
|
25
|
-
assert result[0].id == "standard"
|
|
26
|
-
assert result[0].content == "pre-push: lint test"
|
|
27
|
-
assert result[1].id == "coverage"
|
|
28
|
-
assert "uv run pytest --cov" in result[1].content
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
def test_parse_sections_no_markers():
|
|
32
|
-
assert sections.parse_sections("just plain content\nno markers") == []
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
def test_parse_sections_nested_error():
|
|
36
|
-
content = """\
|
|
37
|
-
# === DO_NOT_EDIT: path-sync outer ===
|
|
38
|
-
# === DO_NOT_EDIT: path-sync inner ===
|
|
39
|
-
# === OK_EDIT ===
|
|
40
|
-
# === OK_EDIT ===
|
|
41
|
-
"""
|
|
42
|
-
with pytest.raises(ValueError, match="Nested section"):
|
|
43
|
-
sections.parse_sections(content)
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
def test_parse_sections_unclosed_error():
|
|
47
|
-
content = "# === DO_NOT_EDIT: path-sync test ===\nsome content"
|
|
48
|
-
with pytest.raises(ValueError, match="Unclosed section"):
|
|
49
|
-
sections.parse_sections(content)
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
def test_parse_sections_standalone_ok_edit():
|
|
53
|
-
content = "# === OK_EDIT ===\nsome content\n# === OK_EDIT ==="
|
|
54
|
-
assert (
|
|
55
|
-
sections.parse_sections(content) == []
|
|
56
|
-
) # standalone OK_EDIT is valid, ignored
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
def test_has_sections():
|
|
60
|
-
assert sections.has_sections(JUSTFILE_CONTENT)
|
|
61
|
-
assert not sections.has_sections("plain content")
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
def test_wrap_in_default_section():
|
|
65
|
-
result = sections.wrap_in_default_section("content here")
|
|
66
|
-
assert "DO_NOT_EDIT: path-sync default" in result
|
|
67
|
-
assert "content here" in result
|
|
68
|
-
assert result.endswith("# === OK_EDIT ===")
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
def test_extract_sections():
|
|
72
|
-
result = sections.extract_sections(JUSTFILE_CONTENT)
|
|
73
|
-
assert result == {
|
|
74
|
-
"standard": "pre-push: lint test",
|
|
75
|
-
"coverage": "cov:\n uv run pytest --cov",
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
def test_replace_sections_updates_content():
|
|
80
|
-
dest = """\
|
|
81
|
-
# === DO_NOT_EDIT: path-sync standard ===
|
|
82
|
-
old content
|
|
83
|
-
# === OK_EDIT ==="""
|
|
84
|
-
src_sections = {"standard": "new content"}
|
|
85
|
-
result = sections.replace_sections(dest, src_sections)
|
|
86
|
-
assert "new content" in result
|
|
87
|
-
assert "old content" not in result
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
def test_replace_sections_preserves_ok_edit():
|
|
91
|
-
dest = """\
|
|
92
|
-
# custom header
|
|
93
|
-
# === DO_NOT_EDIT: path-sync standard ===
|
|
94
|
-
old
|
|
95
|
-
# === OK_EDIT ===
|
|
96
|
-
# my custom stuff"""
|
|
97
|
-
result = sections.replace_sections(dest, {"standard": "new"})
|
|
98
|
-
assert "# custom header" in result
|
|
99
|
-
assert "# my custom stuff" in result
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
def test_replace_sections_skip():
|
|
103
|
-
dest = """\
|
|
104
|
-
# === DO_NOT_EDIT: path-sync standard ===
|
|
105
|
-
keep this
|
|
106
|
-
# === OK_EDIT ==="""
|
|
107
|
-
result = sections.replace_sections(
|
|
108
|
-
dest, {"standard": "replaced"}, skip_sections=["standard"]
|
|
109
|
-
)
|
|
110
|
-
assert "keep this" in result # skipped sections preserve dest content
|
|
111
|
-
assert "replaced" not in result
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
def test_replace_sections_adds_new():
|
|
115
|
-
dest = "# plain file"
|
|
116
|
-
result = sections.replace_sections(dest, {"newid": "new content"})
|
|
117
|
-
assert "DO_NOT_EDIT: path-sync newid" in result
|
|
118
|
-
assert "new content" in result
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
def test_replace_sections_keeps_dest_only():
|
|
122
|
-
dest = """\
|
|
123
|
-
# === DO_NOT_EDIT: path-sync custom ===
|
|
124
|
-
my custom section
|
|
125
|
-
# === OK_EDIT ==="""
|
|
126
|
-
result = sections.replace_sections(dest, {})
|
|
127
|
-
assert "my custom section" in result # dest-only sections preserved
|
|
128
|
-
assert "DO_NOT_EDIT: path-sync custom" in result
|
path_sync/typer_app.py
DELETED
path_sync/validation.py
DELETED
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import logging
|
|
4
|
-
from pathlib import Path
|
|
5
|
-
|
|
6
|
-
from path_sync import git_ops, header, sections
|
|
7
|
-
|
|
8
|
-
logger = logging.getLogger(__name__)
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
def parse_skip_sections(value: str) -> dict[str, set[str]]:
|
|
12
|
-
"""Parse 'path:section_id,path:section_id' into {path: {section_ids}}."""
|
|
13
|
-
result: dict[str, set[str]] = {}
|
|
14
|
-
for item in value.split(","):
|
|
15
|
-
item = item.strip()
|
|
16
|
-
if not item:
|
|
17
|
-
continue
|
|
18
|
-
if ":" not in item:
|
|
19
|
-
raise ValueError(f"Invalid format '{item}', expected 'path:section_id'")
|
|
20
|
-
path, section_id = item.rsplit(":", 1)
|
|
21
|
-
result.setdefault(path, set()).add(section_id)
|
|
22
|
-
return result
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
def compare_sections(
|
|
26
|
-
baseline_content: str,
|
|
27
|
-
current_content: str,
|
|
28
|
-
skip: set[str],
|
|
29
|
-
) -> list[str]:
|
|
30
|
-
"""Return section IDs with unauthorized changes (modified or removed)."""
|
|
31
|
-
baseline_secs = sections.extract_sections(baseline_content)
|
|
32
|
-
current_secs = sections.extract_sections(current_content)
|
|
33
|
-
|
|
34
|
-
changed: list[str] = []
|
|
35
|
-
for sec_id, baseline_text in baseline_secs.items():
|
|
36
|
-
if sec_id in skip:
|
|
37
|
-
continue
|
|
38
|
-
current_text = current_secs.get(sec_id, "")
|
|
39
|
-
if baseline_text != current_text:
|
|
40
|
-
changed.append(sec_id)
|
|
41
|
-
return changed
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
def validate_no_unauthorized_changes(
|
|
45
|
-
repo_root: Path,
|
|
46
|
-
default_branch: str = "main",
|
|
47
|
-
skip_sections: dict[str, set[str]] | None = None,
|
|
48
|
-
) -> list[str]:
|
|
49
|
-
"""Find files with unauthorized changes in DO_NOT_EDIT sections.
|
|
50
|
-
|
|
51
|
-
Returns 'path:section_id' for section changes or 'path' for full-file.
|
|
52
|
-
"""
|
|
53
|
-
repo = git_ops.get_repo(repo_root)
|
|
54
|
-
base_ref = f"origin/{default_branch}"
|
|
55
|
-
skip = skip_sections or {}
|
|
56
|
-
unauthorized: list[str] = []
|
|
57
|
-
|
|
58
|
-
for path in git_ops.get_changed_files(repo, base_ref):
|
|
59
|
-
if not path.exists():
|
|
60
|
-
continue
|
|
61
|
-
if not header.file_has_header(path):
|
|
62
|
-
continue
|
|
63
|
-
|
|
64
|
-
rel_path = str(path.relative_to(repo_root))
|
|
65
|
-
current_content = path.read_text()
|
|
66
|
-
baseline_content = git_ops.get_file_content_at_ref(repo, path, base_ref)
|
|
67
|
-
|
|
68
|
-
if baseline_content is None:
|
|
69
|
-
continue
|
|
70
|
-
|
|
71
|
-
baseline_has_sections = sections.has_sections(baseline_content)
|
|
72
|
-
current_has_sections = sections.has_sections(current_content)
|
|
73
|
-
|
|
74
|
-
if baseline_has_sections:
|
|
75
|
-
file_skip = skip.get(rel_path, set())
|
|
76
|
-
changed_ids = compare_sections(baseline_content, current_content, file_skip)
|
|
77
|
-
unauthorized.extend(f"{rel_path}:{sid}" for sid in changed_ids)
|
|
78
|
-
elif current_has_sections:
|
|
79
|
-
unauthorized.append(rel_path)
|
|
80
|
-
else:
|
|
81
|
-
if baseline_content != current_content:
|
|
82
|
-
unauthorized.append(rel_path)
|
|
83
|
-
|
|
84
|
-
return sorted(unauthorized)
|
path_sync/validation_test.py
DELETED
|
@@ -1,114 +0,0 @@
|
|
|
1
|
-
from git import Repo
|
|
2
|
-
|
|
3
|
-
from path_sync import validation
|
|
4
|
-
from path_sync.header import get_header_line
|
|
5
|
-
|
|
6
|
-
HEADER = get_header_line(".py", "test-config")
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
def _setup_baseline(repo_path, filename: str, content: str) -> None:
|
|
10
|
-
"""Commit content as baseline on main and create origin/main ref."""
|
|
11
|
-
repo = Repo(repo_path)
|
|
12
|
-
file_path = repo_path / filename
|
|
13
|
-
file_path.write_text(content)
|
|
14
|
-
repo.index.add([filename])
|
|
15
|
-
repo.index.commit("baseline")
|
|
16
|
-
repo.create_head("origin/main", repo.head.commit)
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
def test_modify_ok_edit_passes(tmp_repo):
|
|
20
|
-
baseline = f"""{HEADER}
|
|
21
|
-
# === OK_EDIT ===
|
|
22
|
-
user content
|
|
23
|
-
# === DO_NOT_EDIT: path-sync standard ===
|
|
24
|
-
protected
|
|
25
|
-
# === OK_EDIT ===
|
|
26
|
-
"""
|
|
27
|
-
_setup_baseline(tmp_repo, "test.py", baseline)
|
|
28
|
-
|
|
29
|
-
current = f"""{HEADER}
|
|
30
|
-
# === OK_EDIT ===
|
|
31
|
-
modified user content
|
|
32
|
-
# === DO_NOT_EDIT: path-sync standard ===
|
|
33
|
-
protected
|
|
34
|
-
# === OK_EDIT ===
|
|
35
|
-
"""
|
|
36
|
-
(tmp_repo / "test.py").write_text(current)
|
|
37
|
-
|
|
38
|
-
result = validation.validate_no_unauthorized_changes(tmp_repo, "main")
|
|
39
|
-
assert result == []
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
def test_modify_do_not_edit_fails(tmp_repo):
|
|
43
|
-
baseline = f"""{HEADER}
|
|
44
|
-
# === DO_NOT_EDIT: path-sync standard ===
|
|
45
|
-
protected content
|
|
46
|
-
# === OK_EDIT ===
|
|
47
|
-
"""
|
|
48
|
-
_setup_baseline(tmp_repo, "test.py", baseline)
|
|
49
|
-
|
|
50
|
-
current = f"""{HEADER}
|
|
51
|
-
# === DO_NOT_EDIT: path-sync standard ===
|
|
52
|
-
MODIFIED protected content
|
|
53
|
-
# === OK_EDIT ===
|
|
54
|
-
"""
|
|
55
|
-
(tmp_repo / "test.py").write_text(current)
|
|
56
|
-
|
|
57
|
-
result = validation.validate_no_unauthorized_changes(tmp_repo, "main")
|
|
58
|
-
assert result == ["test.py:standard"]
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
def test_skip_section_passes(tmp_repo):
|
|
62
|
-
baseline = f"""{HEADER}
|
|
63
|
-
# === DO_NOT_EDIT: path-sync coverage ===
|
|
64
|
-
protected
|
|
65
|
-
# === OK_EDIT ===
|
|
66
|
-
"""
|
|
67
|
-
_setup_baseline(tmp_repo, "test.py", baseline)
|
|
68
|
-
|
|
69
|
-
current = f"""{HEADER}
|
|
70
|
-
# === DO_NOT_EDIT: path-sync coverage ===
|
|
71
|
-
MODIFIED
|
|
72
|
-
# === OK_EDIT ===
|
|
73
|
-
"""
|
|
74
|
-
(tmp_repo / "test.py").write_text(current)
|
|
75
|
-
|
|
76
|
-
result = validation.validate_no_unauthorized_changes(
|
|
77
|
-
tmp_repo, "main", skip_sections={"test.py": {"coverage"}}
|
|
78
|
-
)
|
|
79
|
-
assert result == []
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
def test_section_removed_fails(tmp_repo):
|
|
83
|
-
baseline = f"""{HEADER}
|
|
84
|
-
# === DO_NOT_EDIT: path-sync standard ===
|
|
85
|
-
protected
|
|
86
|
-
# === OK_EDIT ===
|
|
87
|
-
"""
|
|
88
|
-
_setup_baseline(tmp_repo, "test.py", baseline)
|
|
89
|
-
|
|
90
|
-
current = f"""{HEADER}
|
|
91
|
-
# no sections anymore
|
|
92
|
-
"""
|
|
93
|
-
(tmp_repo / "test.py").write_text(current)
|
|
94
|
-
|
|
95
|
-
result = validation.validate_no_unauthorized_changes(tmp_repo, "main")
|
|
96
|
-
assert result == ["test.py:standard"]
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
def test_no_sections_full_file_comparison(tmp_repo):
|
|
100
|
-
baseline = f"{HEADER}\noriginal content\n"
|
|
101
|
-
_setup_baseline(tmp_repo, "test.py", baseline)
|
|
102
|
-
|
|
103
|
-
(tmp_repo / "test.py").write_text(f"{HEADER}\nmodified content\n")
|
|
104
|
-
|
|
105
|
-
result = validation.validate_no_unauthorized_changes(tmp_repo, "main")
|
|
106
|
-
assert result == ["test.py"]
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
def test_parse_skip_sections():
|
|
110
|
-
result = validation.parse_skip_sections("justfile:coverage,pyproject.toml:default")
|
|
111
|
-
assert result == {"justfile": {"coverage"}, "pyproject.toml": {"default"}}
|
|
112
|
-
|
|
113
|
-
result = validation.parse_skip_sections("path:a,path:b")
|
|
114
|
-
assert result == {"path": {"a", "b"}}
|
path_sync/yaml_utils.py
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from pathlib import Path
|
|
4
|
-
from typing import TypeVar
|
|
5
|
-
|
|
6
|
-
import yaml
|
|
7
|
-
from pydantic import BaseModel
|
|
8
|
-
|
|
9
|
-
T = TypeVar("T", bound=BaseModel)
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
def load_yaml_model(path: Path, model_type: type[T]) -> T:
|
|
13
|
-
data = yaml.safe_load(path.read_text())
|
|
14
|
-
return model_type.model_validate(data)
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
def dump_yaml_model(model: BaseModel) -> str:
|
|
18
|
-
data = model.model_dump(mode="json", exclude_none=True)
|
|
19
|
-
return yaml.safe_dump(data, default_flow_style=False, sort_keys=False)
|
path_sync-0.2.0.dist-info/RECORD
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
path_sync/__init__.py,sha256=fdUhqFKjflvMkVbUuA_RMMv4am652KWK8N3IVQzj6ok,63
|
|
2
|
-
path_sync/__main__.py,sha256=Hh0Na0BlS0EwBAerhWD4mnaB-oMo6jufFVQBuzNZk3g,296
|
|
3
|
-
path_sync/cmd_boot.py,sha256=hYSrMF9QHVXX5feO2UE3lFSJvV38tEsJaNl0sjU2gbw,2896
|
|
4
|
-
path_sync/cmd_copy.py,sha256=J18euCE2toO88evMqKWb8uwmx2LLnVxNdV86W--PDVg,15654
|
|
5
|
-
path_sync/cmd_copy_test.py,sha256=CLUzHJl_BBWqKkpODBthM4RlxKwF3Wo053EV3Nefqsk,4425
|
|
6
|
-
path_sync/cmd_validate.py,sha256=wcMo_JR2jxFtqaNQt1mk_Mnwy_jPSpFHCXFuzNOphEU,1624
|
|
7
|
-
path_sync/conftest.py,sha256=-iu7W2Bh2aRzTR1x3aMnkWkrVnDMp6ezdh1AFgNcYUE,343
|
|
8
|
-
path_sync/file_utils.py,sha256=5C33qzKFQdwChi5YwUWBujj126t0P6dbGSU_5hWExpE,194
|
|
9
|
-
path_sync/git_ops.py,sha256=1ixXKtseZAWAJP9uJcAb78IGQOmRekriv-n_T1nx0mI,5945
|
|
10
|
-
path_sync/header.py,sha256=2YSCj7ainj5TPFINBHn8Uc2ECu591pZfd7NSOZMX5XA,2293
|
|
11
|
-
path_sync/header_test.py,sha256=R9jwOulSKR70HrFoxYBXUx3DGjdzfB2tZNu9sjL_3rA,1401
|
|
12
|
-
path_sync/models.py,sha256=GR2J8PRtAORltvnL73In6zjdNbkELtUomHdwPHfsWwU,3832
|
|
13
|
-
path_sync/models_test.py,sha256=m9kZbl3CGABrg58owNLx4Aiv5LPvKz0t61o0336J5x8,2052
|
|
14
|
-
path_sync/sections.py,sha256=jzzzt2e-umjY0Ab-d7W-29y7aVVBtf_vEX_abFEdZdo,3681
|
|
15
|
-
path_sync/sections_test.py,sha256=lsApYMe1BqGL7T0sYhbs28VCrDm9dmkq0G3JAvGOlPI,3546
|
|
16
|
-
path_sync/typer_app.py,sha256=lEGMRXql3Se3VbmwAohvpUaL2cbY-RwhPUq8kL7bPbc,177
|
|
17
|
-
path_sync/validation.py,sha256=RP9SWd69mGvB4MxGx4RFJiyIQ-X482Y5h7G7VYAOxiE,2766
|
|
18
|
-
path_sync/validation_test.py,sha256=NdK1JHAtjGIm7m7uVm0fM751ttJhKXrS21gt1eEDhP0,3071
|
|
19
|
-
path_sync/yaml_utils.py,sha256=yj6Bl54EltjLEcVKaiA5Ahb9byT6OUMh0xIEzTsrvnQ,498
|
|
20
|
-
path_sync-0.2.0.dist-info/METADATA,sha256=YXdWMZ1YWX9JVRVjC4qXE0WkoO1z3x7xsAlaEjGc27w,7785
|
|
21
|
-
path_sync-0.2.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
22
|
-
path_sync-0.2.0.dist-info/entry_points.txt,sha256=jTsL0c-9gP-4_Jt3EPgihtpLcwQR0AFAf1AUpD50AlI,54
|
|
23
|
-
path_sync-0.2.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|