consync 2.3.1__tar.gz → 2.4.0__tar.gz
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.
- {consync-2.3.1 → consync-2.4.0}/PKG-INFO +1 -1
- {consync-2.3.1 → consync-2.4.0}/consync/__init__.py +1 -1
- {consync-2.3.1 → consync-2.4.0}/consync/config.py +16 -0
- {consync-2.3.1 → consync-2.4.0}/consync/models.py +3 -0
- {consync-2.3.1 → consync-2.4.0}/consync/sync.py +37 -0
- {consync-2.3.1 → consync-2.4.0}/pyproject.toml +1 -1
- {consync-2.3.1 → consync-2.4.0}/tests/test_arrays.py +1 -0
- {consync-2.3.1 → consync-2.4.0}/tests/test_audit_sync.py +4 -0
- {consync-2.3.1 → consync-2.4.0}/tests/test_cli.py +2 -1
- {consync-2.3.1 → consync-2.4.0}/tests/test_comprehensive_sync.py +12 -1
- {consync-2.3.1 → consync-2.4.0}/tests/test_embedded.py +1 -0
- {consync-2.3.1 → consync-2.4.0}/tests/test_safety.py +144 -0
- {consync-2.3.1 → consync-2.4.0}/.github/CODEOWNERS +0 -0
- {consync-2.3.1 → consync-2.4.0}/.github/copilot-instructions.md +0 -0
- {consync-2.3.1 → consync-2.4.0}/.github/dependabot.yml +0 -0
- {consync-2.3.1 → consync-2.4.0}/.github/workflows/ci.yml +0 -0
- {consync-2.3.1 → consync-2.4.0}/.github/workflows/codeql.yml +0 -0
- {consync-2.3.1 → consync-2.4.0}/.github/workflows/publish.yml +0 -0
- {consync-2.3.1 → consync-2.4.0}/.github/workflows/release.yml +0 -0
- {consync-2.3.1 → consync-2.4.0}/.gitignore +0 -0
- {consync-2.3.1 → consync-2.4.0}/CLAUDE.md +0 -0
- {consync-2.3.1 → consync-2.4.0}/CONTRIBUTING.md +0 -0
- {consync-2.3.1 → consync-2.4.0}/FAQ.md +0 -0
- {consync-2.3.1 → consync-2.4.0}/LICENSE +0 -0
- {consync-2.3.1 → consync-2.4.0}/README.md +0 -0
- {consync-2.3.1 → consync-2.4.0}/SECURITY.md +0 -0
- {consync-2.3.1 → consync-2.4.0}/TODO.md +0 -0
- {consync-2.3.1 → consync-2.4.0}/assets/demo.gif +0 -0
- {consync-2.3.1 → consync-2.4.0}/assets/demo.tape +0 -0
- {consync-2.3.1 → consync-2.4.0}/consync/backup.py +0 -0
- {consync-2.3.1 → consync-2.4.0}/consync/cli.py +0 -0
- {consync-2.3.1 → consync-2.4.0}/consync/hooks.py +0 -0
- {consync-2.3.1 → consync-2.4.0}/consync/lock.py +0 -0
- {consync-2.3.1 → consync-2.4.0}/consync/logging_config.py +0 -0
- {consync-2.3.1 → consync-2.4.0}/consync/parsers/__init__.py +0 -0
- {consync-2.3.1 → consync-2.4.0}/consync/parsers/c_header.py +0 -0
- {consync-2.3.1 → consync-2.4.0}/consync/parsers/c_struct_table.py +0 -0
- {consync-2.3.1 → consync-2.4.0}/consync/parsers/csv_parser.py +0 -0
- {consync-2.3.1 → consync-2.4.0}/consync/parsers/json_parser.py +0 -0
- {consync-2.3.1 → consync-2.4.0}/consync/parsers/toml_parser.py +0 -0
- {consync-2.3.1 → consync-2.4.0}/consync/parsers/xlsx.py +0 -0
- {consync-2.3.1 → consync-2.4.0}/consync/precision.py +0 -0
- {consync-2.3.1 → consync-2.4.0}/consync/renderers/__init__.py +0 -0
- {consync-2.3.1 → consync-2.4.0}/consync/renderers/c_header.py +0 -0
- {consync-2.3.1 → consync-2.4.0}/consync/renderers/c_struct_table.py +0 -0
- {consync-2.3.1 → consync-2.4.0}/consync/renderers/csharp.py +0 -0
- {consync-2.3.1 → consync-2.4.0}/consync/renderers/csv_renderer.py +0 -0
- {consync-2.3.1 → consync-2.4.0}/consync/renderers/json_renderer.py +0 -0
- {consync-2.3.1 → consync-2.4.0}/consync/renderers/python_const.py +0 -0
- {consync-2.3.1 → consync-2.4.0}/consync/renderers/rust_const.py +0 -0
- {consync-2.3.1 → consync-2.4.0}/consync/renderers/verilog.py +0 -0
- {consync-2.3.1 → consync-2.4.0}/consync/renderers/vhdl.py +0 -0
- {consync-2.3.1 → consync-2.4.0}/consync/state.py +0 -0
- {consync-2.3.1 → consync-2.4.0}/consync/validators.py +0 -0
- {consync-2.3.1 → consync-2.4.0}/consync/watcher.py +0 -0
- {consync-2.3.1 → consync-2.4.0}/examples/fpga/.consync.yaml +0 -0
- {consync-2.3.1 → consync-2.4.0}/examples/fpga/design_params.csv +0 -0
- {consync-2.3.1 → consync-2.4.0}/examples/hardware/.consync.yaml +0 -0
- {consync-2.3.1 → consync-2.4.0}/examples/hardware/constants.csv +0 -0
- {consync-2.3.1 → consync-2.4.0}/examples/multilang/.consync.yaml +0 -0
- {consync-2.3.1 → consync-2.4.0}/examples/multilang/constants.json +0 -0
- {consync-2.3.1 → consync-2.4.0}/npm/.npmrc +0 -0
- {consync-2.3.1 → consync-2.4.0}/npm/LICENSE +0 -0
- {consync-2.3.1 → consync-2.4.0}/npm/README.md +0 -0
- {consync-2.3.1 → consync-2.4.0}/npm/bin/consync.js +0 -0
- {consync-2.3.1 → consync-2.4.0}/npm/package.json +0 -0
- {consync-2.3.1 → consync-2.4.0}/npm/scripts/install.js +0 -0
- {consync-2.3.1 → consync-2.4.0}/tests/__init__.py +0 -0
- {consync-2.3.1 → consync-2.4.0}/tests/test_bidirectional.py +0 -0
- {consync-2.3.1 → consync-2.4.0}/tests/test_c_struct_table.py +0 -0
- {consync-2.3.1 → consync-2.4.0}/tests/test_parsers.py +0 -0
- {consync-2.3.1 → consync-2.4.0}/tests/test_precision.py +0 -0
- {consync-2.3.1 → consync-2.4.0}/tests/test_renderers.py +0 -0
- {consync-2.3.1 → consync-2.4.0}/tests/test_sync.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: consync
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.4.0
|
|
4
4
|
Summary: Bidirectional sync between spreadsheets and source code constants — with full decimal precision.
|
|
5
5
|
Project-URL: Homepage, https://github.com/naveenkumarbaskaran/consync
|
|
6
6
|
Project-URL: Repository, https://github.com/naveenkumarbaskaran/consync
|
|
@@ -140,6 +140,18 @@ def _parse_mapping(raw: dict[str, Any], config_dir: Path) -> MappingConfig:
|
|
|
140
140
|
if key not in parser_options and key in raw:
|
|
141
141
|
parser_options[key] = raw[key]
|
|
142
142
|
|
|
143
|
+
# protect_target is mandatory when direction is not 'both'
|
|
144
|
+
protect_target_raw = raw.get("protect_target")
|
|
145
|
+
if direction != SyncDirection.BOTH:
|
|
146
|
+
if protect_target_raw is None:
|
|
147
|
+
raise ValueError(
|
|
148
|
+
f"Mapping '{source}' → '{target}': 'protect_target' is required "
|
|
149
|
+
f"when direction is '{raw.get('direction', 'source_to_target')}' (not 'both'). "
|
|
150
|
+
f"Set protect_target: true to make the destination file read-only after sync, "
|
|
151
|
+
f"or protect_target: false to allow manual edits."
|
|
152
|
+
)
|
|
153
|
+
protect_target = bool(protect_target_raw) if protect_target_raw is not None else False
|
|
154
|
+
|
|
143
155
|
return MappingConfig(
|
|
144
156
|
source=source,
|
|
145
157
|
target=target,
|
|
@@ -155,6 +167,7 @@ def _parse_mapping(raw: dict[str, Any], config_dir: Path) -> MappingConfig:
|
|
|
155
167
|
output_style=raw.get("output_style", "const"),
|
|
156
168
|
static_const=raw.get("static_const", False),
|
|
157
169
|
typed_ints=raw.get("typed_ints", True),
|
|
170
|
+
protect_target=protect_target,
|
|
158
171
|
parser_options=parser_options,
|
|
159
172
|
renderer_options=raw.get("renderer_options") or {},
|
|
160
173
|
validators=raw.get("validators") or {},
|
|
@@ -241,10 +254,13 @@ mappings:
|
|
|
241
254
|
# - source: params.csv # Will be created from your existing code
|
|
242
255
|
# target: legacy/params.h # Already exists
|
|
243
256
|
# direction: target_to_source # First sync creates the CSV from the .h
|
|
257
|
+
# protect_target: true # REQUIRED when direction is not 'both'
|
|
244
258
|
|
|
245
259
|
# Optional:
|
|
246
260
|
# prefix: "" # Prefix all constant names (e.g., "HW_")
|
|
247
261
|
# uppercase_names: true # Force UPPER_CASE names in output
|
|
262
|
+
# protect_target: true # Make destination file read-only after sync
|
|
263
|
+
# # (mandatory when direction is not 'both')
|
|
248
264
|
|
|
249
265
|
# Global settings:
|
|
250
266
|
# state_file: .consync.state.json # Track sync state (gitignore this)
|
|
@@ -90,6 +90,9 @@ class MappingConfig:
|
|
|
90
90
|
output_style: str = "const" # "const" | "define" — #define vs const declaration
|
|
91
91
|
static_const: bool = False # emit "static const" to avoid linker duplicates
|
|
92
92
|
typed_ints: bool = True # use uint32_t/int32_t instead of plain int/double for integers
|
|
93
|
+
# File protection — prevents manual edits to the destination file
|
|
94
|
+
# Mandatory when direction is not 'both'.
|
|
95
|
+
protect_target: bool = False # make target read-only after sync (source_to_target)
|
|
93
96
|
# Parser/renderer options (format-specific kwargs)
|
|
94
97
|
parser_options: dict = field(default_factory=dict) # passed to parser as **kwargs
|
|
95
98
|
renderer_options: dict = field(default_factory=dict) # passed to renderer as **kwargs
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import logging
|
|
6
|
+
import os
|
|
7
|
+
import stat
|
|
6
8
|
from dataclasses import dataclass
|
|
7
9
|
from enum import Enum
|
|
8
10
|
from pathlib import Path
|
|
@@ -169,7 +171,9 @@ def _sync_one(
|
|
|
169
171
|
+ (f" (+{len(validation.errors)-1} more)" if len(validation.errors) > 1 else ""),
|
|
170
172
|
)
|
|
171
173
|
backup_file(target_path, backup_dir=config_dir / ".consync" / "backups")
|
|
174
|
+
_unprotect_before_write(target_path, mapping)
|
|
172
175
|
_render_file(constants, target_path, mapping.target_format, mapping)
|
|
176
|
+
_protect_after_write(target_path, mapping)
|
|
173
177
|
result = SyncResult.SYNCED_SOURCE_TO_TARGET
|
|
174
178
|
logger.info(
|
|
175
179
|
"Synced %s → %s (%d constants)",
|
|
@@ -187,7 +191,9 @@ def _sync_one(
|
|
|
187
191
|
+ (f" (+{len(validation.errors)-1} more)" if len(validation.errors) > 1 else ""),
|
|
188
192
|
)
|
|
189
193
|
backup_file(source_path, backup_dir=config_dir / ".consync" / "backups")
|
|
194
|
+
_unprotect_before_write(source_path, mapping)
|
|
190
195
|
_render_file(constants, source_path, mapping.source_format, mapping)
|
|
196
|
+
_protect_after_write(source_path, mapping)
|
|
191
197
|
result = SyncResult.SYNCED_TARGET_TO_SOURCE
|
|
192
198
|
logger.info(
|
|
193
199
|
"Synced %s → %s (%d constants)",
|
|
@@ -389,6 +395,37 @@ def _render_file(constants: list, filepath: Path, format_name: str, mapping: Map
|
|
|
389
395
|
renderer(constants, filepath, config=mapping)
|
|
390
396
|
|
|
391
397
|
|
|
398
|
+
def _unprotect_before_write(filepath: Path, mapping: MappingConfig) -> None:
|
|
399
|
+
"""Temporarily make a protected file writable before consync writes to it.
|
|
400
|
+
|
|
401
|
+
Only acts if protect_target is enabled and the file exists and is read-only.
|
|
402
|
+
"""
|
|
403
|
+
if not mapping.protect_target:
|
|
404
|
+
return
|
|
405
|
+
if not filepath.exists():
|
|
406
|
+
return
|
|
407
|
+
current = filepath.stat().st_mode
|
|
408
|
+
if not (current & stat.S_IWUSR):
|
|
409
|
+
filepath.chmod(current | stat.S_IWUSR)
|
|
410
|
+
logger.debug("Temporarily made %s writable for sync.", filepath.name)
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
def _protect_after_write(filepath: Path, mapping: MappingConfig) -> None:
|
|
414
|
+
"""Make the destination file read-only after sync writes it.
|
|
415
|
+
|
|
416
|
+
Removes the owner write bit so the user cannot accidentally edit the
|
|
417
|
+
file that is managed by consync. Only acts when protect_target is True.
|
|
418
|
+
"""
|
|
419
|
+
if not mapping.protect_target:
|
|
420
|
+
return
|
|
421
|
+
if not filepath.exists():
|
|
422
|
+
return
|
|
423
|
+
current = filepath.stat().st_mode
|
|
424
|
+
readonly = current & ~(stat.S_IWUSR | stat.S_IWGRP | stat.S_IWOTH)
|
|
425
|
+
filepath.chmod(readonly)
|
|
426
|
+
logger.info("Protected %s (read-only). Use 'consync sync' to update.", filepath.name)
|
|
427
|
+
|
|
428
|
+
|
|
392
429
|
def _validate_if_needed(constants: list, mapping: MappingConfig):
|
|
393
430
|
"""Run validation hooks if configured. Returns ValidationResult or None."""
|
|
394
431
|
if not mapping.validators:
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "consync"
|
|
7
|
-
version = "2.
|
|
7
|
+
version = "2.4.0"
|
|
8
8
|
description = "Bidirectional sync between spreadsheets and source code constants — with full decimal precision."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = {text = "MIT"}
|
|
@@ -495,6 +495,7 @@ static const MotorParam_t params[3] = {
|
|
|
495
495
|
target: motor_params.xlsx
|
|
496
496
|
format: c_struct_table
|
|
497
497
|
direction: target_to_source
|
|
498
|
+
protect_target: false
|
|
498
499
|
parser_options:
|
|
499
500
|
variant: all
|
|
500
501
|
"""))
|
|
@@ -508,6 +509,7 @@ static const MotorParam_t params[3] = {
|
|
|
508
509
|
target: motor_params.xlsx
|
|
509
510
|
format: c_struct_table
|
|
510
511
|
direction: source_to_target
|
|
512
|
+
protect_target: false
|
|
511
513
|
parser_options:
|
|
512
514
|
variant: all
|
|
513
515
|
"""))
|
|
@@ -526,6 +528,7 @@ static const MotorParam_t params[3] = {
|
|
|
526
528
|
target: motor_params.xlsx
|
|
527
529
|
format: c_struct_table
|
|
528
530
|
direction: target_to_source
|
|
531
|
+
protect_target: false
|
|
529
532
|
parser_options:
|
|
530
533
|
variant: all
|
|
531
534
|
"""))
|
|
@@ -567,6 +570,7 @@ static const Param_t tbl[2] = {
|
|
|
567
570
|
target: p.xlsx
|
|
568
571
|
format: c_struct_table
|
|
569
572
|
direction: source_to_target
|
|
573
|
+
protect_target: false
|
|
570
574
|
"""))
|
|
571
575
|
# First sync to create xlsx
|
|
572
576
|
sync(config_path=config)
|
|
@@ -22,6 +22,7 @@ def project_dir(tmp_path):
|
|
|
22
22
|
- source: data.csv
|
|
23
23
|
target: out.h
|
|
24
24
|
direction: source_to_target
|
|
25
|
+
protect_target: false
|
|
25
26
|
precision: 17
|
|
26
27
|
header_guard: TEST_H
|
|
27
28
|
"""))
|
|
@@ -90,4 +91,4 @@ class TestVersionCommand:
|
|
|
90
91
|
def test_version(self, runner):
|
|
91
92
|
result = runner.invoke(main, ["--version"])
|
|
92
93
|
assert result.exit_code == 0
|
|
93
|
-
assert "2.
|
|
94
|
+
assert "2.4.0" in result.output
|
|
@@ -129,7 +129,8 @@ C_STRUCT_TABLE_VARIANTS = textwrap.dedent("""\
|
|
|
129
129
|
|
|
130
130
|
|
|
131
131
|
def create_config(tmp_path, source_name, target_name, direction="source_to_target",
|
|
132
|
-
source_format="", target_format="", parser_options=None
|
|
132
|
+
source_format="", target_format="", parser_options=None,
|
|
133
|
+
protect_target=False):
|
|
133
134
|
"""Create a .consync.yaml config file."""
|
|
134
135
|
cfg = f"""\
|
|
135
136
|
mappings:
|
|
@@ -137,6 +138,9 @@ mappings:
|
|
|
137
138
|
target: {target_name}
|
|
138
139
|
direction: {direction}
|
|
139
140
|
"""
|
|
141
|
+
# protect_target is mandatory when direction is not 'both'
|
|
142
|
+
if direction != "both":
|
|
143
|
+
cfg += f" protect_target: {'true' if protect_target else 'false'}\n"
|
|
140
144
|
if source_format:
|
|
141
145
|
cfg += f" source_format: {source_format}\n"
|
|
142
146
|
if target_format:
|
|
@@ -403,12 +407,15 @@ class TestOneWaySourceToTarget:
|
|
|
403
407
|
- source: params.csv
|
|
404
408
|
target: params.h
|
|
405
409
|
direction: source_to_target
|
|
410
|
+
protect_target: false
|
|
406
411
|
- source: params.csv
|
|
407
412
|
target: params.json
|
|
408
413
|
direction: source_to_target
|
|
414
|
+
protect_target: false
|
|
409
415
|
- source: params.csv
|
|
410
416
|
target: params.py
|
|
411
417
|
direction: source_to_target
|
|
418
|
+
protect_target: false
|
|
412
419
|
""")
|
|
413
420
|
config = tmp_path / ".consync.yaml"
|
|
414
421
|
config.write_text(cfg_text)
|
|
@@ -1059,6 +1066,7 @@ class TestFormatRendering:
|
|
|
1059
1066
|
- source: params.csv
|
|
1060
1067
|
target: params.h
|
|
1061
1068
|
direction: source_to_target
|
|
1069
|
+
protect_target: false
|
|
1062
1070
|
output_style: define
|
|
1063
1071
|
""")
|
|
1064
1072
|
config = tmp_path / ".consync.yaml"
|
|
@@ -1076,6 +1084,7 @@ class TestFormatRendering:
|
|
|
1076
1084
|
- source: params.csv
|
|
1077
1085
|
target: params.h
|
|
1078
1086
|
direction: source_to_target
|
|
1087
|
+
protect_target: false
|
|
1079
1088
|
output_style: const
|
|
1080
1089
|
""")
|
|
1081
1090
|
config = tmp_path / ".consync.yaml"
|
|
@@ -1093,6 +1102,7 @@ class TestFormatRendering:
|
|
|
1093
1102
|
- source: params.csv
|
|
1094
1103
|
target: params.v
|
|
1095
1104
|
direction: source_to_target
|
|
1105
|
+
protect_target: false
|
|
1096
1106
|
module_name: motor_params
|
|
1097
1107
|
""")
|
|
1098
1108
|
config = tmp_path / ".consync.yaml"
|
|
@@ -1111,6 +1121,7 @@ class TestFormatRendering:
|
|
|
1111
1121
|
- source: params.csv
|
|
1112
1122
|
target: params.cs
|
|
1113
1123
|
direction: source_to_target
|
|
1124
|
+
protect_target: false
|
|
1114
1125
|
namespace: Motor.Config
|
|
1115
1126
|
""")
|
|
1116
1127
|
config = tmp_path / ".consync.yaml"
|
|
@@ -414,6 +414,7 @@ mappings:
|
|
|
414
414
|
- source: data.csv
|
|
415
415
|
target: out.h
|
|
416
416
|
direction: source_to_target
|
|
417
|
+
protect_target: false
|
|
417
418
|
""")
|
|
418
419
|
source = tmp_path / "data.csv"
|
|
419
420
|
source.write_text(source_content)
|
|
@@ -470,6 +471,7 @@ mappings:
|
|
|
470
471
|
- source: data.csv
|
|
471
472
|
target: out.h
|
|
472
473
|
direction: source_to_target
|
|
474
|
+
protect_target: false
|
|
473
475
|
validators:
|
|
474
476
|
X:
|
|
475
477
|
min: 0
|
|
@@ -494,6 +496,7 @@ mappings:
|
|
|
494
496
|
- source: data.csv
|
|
495
497
|
target: out.h
|
|
496
498
|
direction: source_to_target
|
|
499
|
+
protect_target: false
|
|
497
500
|
validators:
|
|
498
501
|
X:
|
|
499
502
|
min: 0
|
|
@@ -506,3 +509,144 @@ mappings:
|
|
|
506
509
|
reports = sync(config_path=str(config))
|
|
507
510
|
|
|
508
511
|
assert reports[0].result == SyncResult.SYNCED_SOURCE_TO_TARGET
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
# ============================================================
|
|
515
|
+
# protect_target Tests
|
|
516
|
+
# ============================================================
|
|
517
|
+
|
|
518
|
+
class TestProtectTarget:
|
|
519
|
+
"""Tests for the protect_target option that makes destination files read-only."""
|
|
520
|
+
|
|
521
|
+
def test_config_requires_protect_target_for_s2t(self, tmp_path):
|
|
522
|
+
"""Config loader rejects source_to_target without protect_target."""
|
|
523
|
+
from consync.config import load_config
|
|
524
|
+
|
|
525
|
+
config = tmp_path / ".consync.yaml"
|
|
526
|
+
config.write_text("""\
|
|
527
|
+
mappings:
|
|
528
|
+
- source: data.csv
|
|
529
|
+
target: out.h
|
|
530
|
+
direction: source_to_target
|
|
531
|
+
""")
|
|
532
|
+
(tmp_path / "data.csv").write_text("Name,Value\nX,1\n")
|
|
533
|
+
|
|
534
|
+
with pytest.raises(ValueError, match="protect_target.*required"):
|
|
535
|
+
load_config(config)
|
|
536
|
+
|
|
537
|
+
def test_config_requires_protect_target_for_t2s(self, tmp_path):
|
|
538
|
+
"""Config loader rejects target_to_source without protect_target."""
|
|
539
|
+
from consync.config import load_config
|
|
540
|
+
|
|
541
|
+
config = tmp_path / ".consync.yaml"
|
|
542
|
+
config.write_text("""\
|
|
543
|
+
mappings:
|
|
544
|
+
- source: data.csv
|
|
545
|
+
target: out.h
|
|
546
|
+
direction: target_to_source
|
|
547
|
+
protect_target: false
|
|
548
|
+
""")
|
|
549
|
+
# This should load fine (protect_target is set)
|
|
550
|
+
cfg = load_config(config)
|
|
551
|
+
assert cfg.mappings[0].protect_target is False
|
|
552
|
+
|
|
553
|
+
def test_config_optional_for_both_direction(self, tmp_path):
|
|
554
|
+
"""Config loader does NOT require protect_target when direction is 'both'."""
|
|
555
|
+
from consync.config import load_config
|
|
556
|
+
|
|
557
|
+
config = tmp_path / ".consync.yaml"
|
|
558
|
+
config.write_text("""\
|
|
559
|
+
mappings:
|
|
560
|
+
- source: data.csv
|
|
561
|
+
target: out.h
|
|
562
|
+
direction: both
|
|
563
|
+
""")
|
|
564
|
+
# Should load without error
|
|
565
|
+
cfg = load_config(config)
|
|
566
|
+
assert cfg.mappings[0].protect_target is False
|
|
567
|
+
|
|
568
|
+
def test_protect_target_true_makes_file_readonly(self, tmp_path):
|
|
569
|
+
"""When protect_target is True, target file is set read-only after sync."""
|
|
570
|
+
import stat
|
|
571
|
+
from consync.sync import sync, SyncResult
|
|
572
|
+
|
|
573
|
+
config = tmp_path / ".consync.yaml"
|
|
574
|
+
config.write_text("""\
|
|
575
|
+
mappings:
|
|
576
|
+
- source: data.csv
|
|
577
|
+
target: out.h
|
|
578
|
+
direction: source_to_target
|
|
579
|
+
protect_target: true
|
|
580
|
+
""")
|
|
581
|
+
source = tmp_path / "data.csv"
|
|
582
|
+
source.write_text("Name,Value\nX,42\n")
|
|
583
|
+
|
|
584
|
+
os.chdir(tmp_path)
|
|
585
|
+
reports = sync(config_path=str(config))
|
|
586
|
+
assert reports[0].result == SyncResult.SYNCED_SOURCE_TO_TARGET
|
|
587
|
+
|
|
588
|
+
target = tmp_path / "out.h"
|
|
589
|
+
assert target.exists()
|
|
590
|
+
mode = target.stat().st_mode
|
|
591
|
+
assert not (mode & stat.S_IWUSR), "Target should be read-only (no owner write bit)"
|
|
592
|
+
assert not (mode & stat.S_IWGRP), "Target should be read-only (no group write bit)"
|
|
593
|
+
assert not (mode & stat.S_IWOTH), "Target should be read-only (no other write bit)"
|
|
594
|
+
|
|
595
|
+
def test_protect_target_false_leaves_file_writable(self, tmp_path):
|
|
596
|
+
"""When protect_target is False, target file stays writable."""
|
|
597
|
+
import stat
|
|
598
|
+
from consync.sync import sync, SyncResult
|
|
599
|
+
|
|
600
|
+
config = tmp_path / ".consync.yaml"
|
|
601
|
+
config.write_text("""\
|
|
602
|
+
mappings:
|
|
603
|
+
- source: data.csv
|
|
604
|
+
target: out.h
|
|
605
|
+
direction: source_to_target
|
|
606
|
+
protect_target: false
|
|
607
|
+
""")
|
|
608
|
+
source = tmp_path / "data.csv"
|
|
609
|
+
source.write_text("Name,Value\nX,42\n")
|
|
610
|
+
|
|
611
|
+
os.chdir(tmp_path)
|
|
612
|
+
reports = sync(config_path=str(config))
|
|
613
|
+
assert reports[0].result == SyncResult.SYNCED_SOURCE_TO_TARGET
|
|
614
|
+
|
|
615
|
+
target = tmp_path / "out.h"
|
|
616
|
+
mode = target.stat().st_mode
|
|
617
|
+
assert mode & stat.S_IWUSR, "Target should remain writable"
|
|
618
|
+
|
|
619
|
+
def test_protect_target_resync_works(self, tmp_path):
|
|
620
|
+
"""Protected (read-only) file can still be updated by subsequent sync."""
|
|
621
|
+
import stat
|
|
622
|
+
from consync.sync import sync, SyncResult
|
|
623
|
+
|
|
624
|
+
config = tmp_path / ".consync.yaml"
|
|
625
|
+
config.write_text("""\
|
|
626
|
+
mappings:
|
|
627
|
+
- source: data.csv
|
|
628
|
+
target: out.h
|
|
629
|
+
direction: source_to_target
|
|
630
|
+
protect_target: true
|
|
631
|
+
""")
|
|
632
|
+
source = tmp_path / "data.csv"
|
|
633
|
+
source.write_text("Name,Value\nX,42\n")
|
|
634
|
+
|
|
635
|
+
os.chdir(tmp_path)
|
|
636
|
+
sync(config_path=str(config))
|
|
637
|
+
target = tmp_path / "out.h"
|
|
638
|
+
assert not (target.stat().st_mode & stat.S_IWUSR)
|
|
639
|
+
|
|
640
|
+
content_v1 = target.read_text()
|
|
641
|
+
|
|
642
|
+
# Update source and sync again — should work despite file being read-only
|
|
643
|
+
source.write_text("Name,Value\nX,99\n")
|
|
644
|
+
reports = sync(config_path=str(config))
|
|
645
|
+
assert reports[0].result == SyncResult.SYNCED_SOURCE_TO_TARGET
|
|
646
|
+
|
|
647
|
+
content_v2 = target.read_text()
|
|
648
|
+
assert content_v2 != content_v1
|
|
649
|
+
assert "99" in content_v2
|
|
650
|
+
|
|
651
|
+
# File should be read-only again
|
|
652
|
+
assert not (target.stat().st_mode & stat.S_IWUSR)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|