consync 0.1.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.
- consync/__init__.py +9 -0
- consync/backup.py +188 -0
- consync/cli.py +372 -0
- consync/config.py +200 -0
- consync/hooks.py +81 -0
- consync/lock.py +118 -0
- consync/logging_config.py +273 -0
- consync/models.py +104 -0
- consync/parsers/__init__.py +40 -0
- consync/parsers/c_header.py +96 -0
- consync/parsers/csv_parser.py +133 -0
- consync/parsers/json_parser.py +138 -0
- consync/parsers/toml_parser.py +74 -0
- consync/parsers/xlsx.py +116 -0
- consync/precision.py +148 -0
- consync/renderers/__init__.py +49 -0
- consync/renderers/c_header.py +222 -0
- consync/renderers/csharp.py +174 -0
- consync/renderers/csv_renderer.py +46 -0
- consync/renderers/json_renderer.py +71 -0
- consync/renderers/python_const.py +84 -0
- consync/renderers/rust_const.py +90 -0
- consync/renderers/verilog.py +89 -0
- consync/renderers/vhdl.py +94 -0
- consync/state.py +76 -0
- consync/sync.py +458 -0
- consync/validators.py +233 -0
- consync/watcher.py +176 -0
- consync-0.1.0.dist-info/METADATA +590 -0
- consync-0.1.0.dist-info/RECORD +33 -0
- consync-0.1.0.dist-info/WHEEL +4 -0
- consync-0.1.0.dist-info/entry_points.txt +2 -0
- consync-0.1.0.dist-info/licenses/LICENSE +21 -0
consync/precision.py
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"""Precision-preserving number formatting for IEEE 754 doubles.
|
|
2
|
+
|
|
3
|
+
The core problem: hardware constants have 15-17 significant digits.
|
|
4
|
+
Excel stores them as IEEE 754 doubles. When writing to C/Verilog/Python,
|
|
5
|
+
we must preserve ALL significant digits to guarantee round-trip fidelity:
|
|
6
|
+
|
|
7
|
+
parse(format(x)) == x
|
|
8
|
+
|
|
9
|
+
Default: 17 significant digits (maximum for double precision).
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import math
|
|
15
|
+
from decimal import Decimal
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def format_float(value: float, precision: int = 17) -> str:
|
|
19
|
+
"""Format a float preserving significant digits.
|
|
20
|
+
|
|
21
|
+
Uses Python's `g` format which automatically chooses between
|
|
22
|
+
fixed-point and scientific notation for best readability.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
value: The float to format.
|
|
26
|
+
precision: Number of significant digits (1-21). Default 17.
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
String representation with full precision.
|
|
30
|
+
|
|
31
|
+
Examples:
|
|
32
|
+
>>> format_float(1.20029384729384)
|
|
33
|
+
'1.20029384729384'
|
|
34
|
+
>>> format_float(0.00000047832940)
|
|
35
|
+
'4.783294e-07'
|
|
36
|
+
>>> format_float(100029.4829183)
|
|
37
|
+
'100029.4829183'
|
|
38
|
+
>>> format_float(4700.0)
|
|
39
|
+
'4700'
|
|
40
|
+
"""
|
|
41
|
+
if precision < 1:
|
|
42
|
+
precision = 1
|
|
43
|
+
if precision > 21:
|
|
44
|
+
precision = 21
|
|
45
|
+
|
|
46
|
+
if math.isnan(value):
|
|
47
|
+
return "NAN"
|
|
48
|
+
if math.isinf(value):
|
|
49
|
+
return "INF" if value > 0 else "-INF"
|
|
50
|
+
|
|
51
|
+
formatted = f"{value:.{precision}g}"
|
|
52
|
+
|
|
53
|
+
# Ensure no trailing zeros that add false precision,
|
|
54
|
+
# but keep at least one digit after decimal for floats
|
|
55
|
+
return formatted
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def format_c_double(value: float, precision: int = 17) -> str:
|
|
59
|
+
"""Format a float for C/C++ const double declaration.
|
|
60
|
+
|
|
61
|
+
Always produces a string that, when parsed by a C compiler,
|
|
62
|
+
yields the exact same IEEE 754 bit pattern.
|
|
63
|
+
|
|
64
|
+
Examples:
|
|
65
|
+
>>> format_c_double(4.7832940e-07)
|
|
66
|
+
'4.7832940000000000e-07'
|
|
67
|
+
>>> format_c_double(4700.0)
|
|
68
|
+
'4700'
|
|
69
|
+
>>> format_c_double(1.20029384729384)
|
|
70
|
+
'1.20029384729384'
|
|
71
|
+
"""
|
|
72
|
+
return format_float(value, precision)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def format_scientific(value: float, precision: int = 17) -> str:
|
|
76
|
+
"""Always use scientific notation (for Verilog/VHDL real parameters).
|
|
77
|
+
|
|
78
|
+
Examples:
|
|
79
|
+
>>> format_scientific(4700.0)
|
|
80
|
+
'4.7e+03'
|
|
81
|
+
>>> format_scientific(0.00000047832940)
|
|
82
|
+
'4.783294e-07'
|
|
83
|
+
"""
|
|
84
|
+
if precision < 1:
|
|
85
|
+
precision = 1
|
|
86
|
+
return f"{value:.{precision}e}"
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def format_fixed(value: float, decimal_places: int = 14) -> str:
|
|
90
|
+
"""Fixed-point notation with explicit decimal places.
|
|
91
|
+
|
|
92
|
+
Examples:
|
|
93
|
+
>>> format_fixed(1.20029384729384, 14)
|
|
94
|
+
'1.20029384729384'
|
|
95
|
+
"""
|
|
96
|
+
return f"{value:.{decimal_places}f}".rstrip("0").rstrip(".")
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def parse_number(text: str) -> float | int:
|
|
100
|
+
"""Parse a numeric string, preserving int vs float distinction.
|
|
101
|
+
|
|
102
|
+
Handles scientific notation, underscores (Rust/Verilog style),
|
|
103
|
+
and common suffixes.
|
|
104
|
+
|
|
105
|
+
Examples:
|
|
106
|
+
>>> parse_number("4.7832940e-07")
|
|
107
|
+
4.783294e-07
|
|
108
|
+
>>> parse_number("4700")
|
|
109
|
+
4700
|
|
110
|
+
>>> parse_number("1_000_000")
|
|
111
|
+
1000000
|
|
112
|
+
>>> parse_number("0xFF")
|
|
113
|
+
255
|
|
114
|
+
"""
|
|
115
|
+
text = text.strip().replace("_", "")
|
|
116
|
+
|
|
117
|
+
# Hex literals
|
|
118
|
+
if text.lower().startswith("0x"):
|
|
119
|
+
return int(text, 16)
|
|
120
|
+
|
|
121
|
+
# Binary literals
|
|
122
|
+
if text.lower().startswith("0b"):
|
|
123
|
+
return int(text, 2)
|
|
124
|
+
|
|
125
|
+
# Try int first
|
|
126
|
+
try:
|
|
127
|
+
val = int(text)
|
|
128
|
+
return val
|
|
129
|
+
except ValueError:
|
|
130
|
+
pass
|
|
131
|
+
|
|
132
|
+
# Then float
|
|
133
|
+
return float(text)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def significant_digits(value: float) -> int:
|
|
137
|
+
"""Count the significant digits in a float's representation.
|
|
138
|
+
|
|
139
|
+
Uses Decimal to get the shortest representation that round-trips.
|
|
140
|
+
"""
|
|
141
|
+
if value == 0:
|
|
142
|
+
return 1
|
|
143
|
+
d = Decimal(str(value))
|
|
144
|
+
# sign, digits, exponent
|
|
145
|
+
_, digits, _ = d.as_tuple()
|
|
146
|
+
# Strip trailing zeros
|
|
147
|
+
stripped = str(int("".join(str(d) for d in digits))).rstrip("0")
|
|
148
|
+
return len(stripped) if stripped else 1
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Renderer registry — maps format names to renderer functions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Callable
|
|
7
|
+
|
|
8
|
+
from consync.models import Constant
|
|
9
|
+
|
|
10
|
+
# Type for renderer functions: (constants, filepath, **kwargs) -> None
|
|
11
|
+
RendererFunc = Callable[..., None]
|
|
12
|
+
|
|
13
|
+
_REGISTRY: dict[str, RendererFunc] = {}
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def register(name: str):
|
|
17
|
+
"""Decorator to register a renderer function."""
|
|
18
|
+
def decorator(func: RendererFunc) -> RendererFunc:
|
|
19
|
+
_REGISTRY[name] = func
|
|
20
|
+
return func
|
|
21
|
+
return decorator
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def get_renderer(format_name: str) -> RendererFunc:
|
|
25
|
+
"""Get a renderer by format name."""
|
|
26
|
+
if format_name not in _REGISTRY:
|
|
27
|
+
available = ", ".join(sorted(_REGISTRY.keys()))
|
|
28
|
+
raise ValueError(
|
|
29
|
+
f"Unknown renderer format '{format_name}'. Available: {available}"
|
|
30
|
+
)
|
|
31
|
+
return _REGISTRY[format_name]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def list_renderers() -> list[str]:
|
|
35
|
+
"""List all registered renderer format names."""
|
|
36
|
+
return sorted(_REGISTRY.keys())
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# Import all renderer modules to trigger registration
|
|
40
|
+
from consync.renderers import ( # noqa: E402, F401
|
|
41
|
+
c_header,
|
|
42
|
+
csharp,
|
|
43
|
+
csv_renderer,
|
|
44
|
+
python_const,
|
|
45
|
+
rust_const,
|
|
46
|
+
verilog,
|
|
47
|
+
vhdl,
|
|
48
|
+
json_renderer,
|
|
49
|
+
)
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
"""C/C++ header renderer — generates const declarations or #define macros.
|
|
2
|
+
|
|
3
|
+
Supports:
|
|
4
|
+
- `const double` (default) or `#define` output style
|
|
5
|
+
- `static const` for multi-TU safety
|
|
6
|
+
- Typed integers: uint32_t / int32_t / uint16_t / int16_t / uint8_t / int8_t
|
|
7
|
+
- Array constants: static const int X[] = {1, 2, 3};
|
|
8
|
+
- Configurable precision for doubles/floats
|
|
9
|
+
|
|
10
|
+
Output examples:
|
|
11
|
+
|
|
12
|
+
Style "const" (default):
|
|
13
|
+
static const uint32_t R_PULLUP = 4706U; /* Ohm | I2C pull-up */
|
|
14
|
+
static const double R_SENSE = 1.9999999999910001; /* Ohm | Sense R */
|
|
15
|
+
static const int THRESHOLDS[] = {50, 100, 150, 200, 250}; /* bar | Brake thresholds */
|
|
16
|
+
|
|
17
|
+
Style "define":
|
|
18
|
+
#define R_PULLUP (4706U) /* Ohm | I2C pull-up */
|
|
19
|
+
#define R_SENSE (1.9999999999910001) /* Ohm | Sense R */
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
from datetime import datetime
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
|
|
27
|
+
from consync.models import Constant, MappingConfig
|
|
28
|
+
from consync.precision import format_c_double
|
|
29
|
+
from consync.renderers import register
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _c_type_for_int(value: int, typed_ints: bool) -> tuple[str, str]:
|
|
33
|
+
"""Determine C type and suffix for an integer value.
|
|
34
|
+
|
|
35
|
+
Returns (type_name, literal_suffix).
|
|
36
|
+
"""
|
|
37
|
+
if not typed_ints:
|
|
38
|
+
return ("int", "")
|
|
39
|
+
|
|
40
|
+
if value < 0:
|
|
41
|
+
if -128 <= value <= 127:
|
|
42
|
+
return ("int8_t", "")
|
|
43
|
+
elif -32768 <= value <= 32767:
|
|
44
|
+
return ("int16_t", "")
|
|
45
|
+
elif -2147483648 <= value <= 2147483647:
|
|
46
|
+
return ("int32_t", "")
|
|
47
|
+
else:
|
|
48
|
+
return ("int64_t", "LL")
|
|
49
|
+
else:
|
|
50
|
+
if value <= 255:
|
|
51
|
+
return ("uint8_t", "U")
|
|
52
|
+
elif value <= 65535:
|
|
53
|
+
return ("uint16_t", "U")
|
|
54
|
+
elif value <= 4294967295:
|
|
55
|
+
return ("uint32_t", "U")
|
|
56
|
+
else:
|
|
57
|
+
return ("uint64_t", "ULL")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _c_type_for_float(value: float) -> str:
|
|
61
|
+
"""Determine C float type — always double for full precision."""
|
|
62
|
+
return "double"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@register("c_header")
|
|
66
|
+
def render_c_header(
|
|
67
|
+
constants: list[Constant],
|
|
68
|
+
filepath: str | Path,
|
|
69
|
+
*,
|
|
70
|
+
config: MappingConfig | None = None,
|
|
71
|
+
**kwargs,
|
|
72
|
+
) -> None:
|
|
73
|
+
"""Render constants as a C/C++ header file.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
constants: Constants to write.
|
|
77
|
+
filepath: Output .h file path.
|
|
78
|
+
config: Mapping config (for precision, header_guard, output_style, etc.)
|
|
79
|
+
"""
|
|
80
|
+
filepath = Path(filepath)
|
|
81
|
+
precision = config.precision if config else kwargs.get("precision", 17)
|
|
82
|
+
source_name = config.source if config else kwargs.get("source", "unknown")
|
|
83
|
+
output_style = (config.output_style if config else kwargs.get("output_style", "const")).lower()
|
|
84
|
+
static = config.static_const if config else kwargs.get("static_const", False)
|
|
85
|
+
typed_ints = config.typed_ints if config else kwargs.get("typed_ints", True)
|
|
86
|
+
|
|
87
|
+
# Determine header guard
|
|
88
|
+
guard = ""
|
|
89
|
+
if config and config.header_guard:
|
|
90
|
+
guard = config.header_guard
|
|
91
|
+
else:
|
|
92
|
+
guard = filepath.stem.upper().replace("-", "_").replace(".", "_") + "_H"
|
|
93
|
+
|
|
94
|
+
# Prefix handling
|
|
95
|
+
prefix = config.prefix if config else kwargs.get("prefix", "")
|
|
96
|
+
|
|
97
|
+
# Calculate alignment
|
|
98
|
+
names = [prefix + c.name for c in constants]
|
|
99
|
+
name_width = max((len(n) for n in names), default=20) + 2
|
|
100
|
+
|
|
101
|
+
# Qualifier for const style
|
|
102
|
+
qualifier = "static const" if static else "const"
|
|
103
|
+
|
|
104
|
+
# Include for stdint types
|
|
105
|
+
needs_stdint = typed_ints and any(
|
|
106
|
+
isinstance(c.value, int) or
|
|
107
|
+
(isinstance(c.value, list) and c.value and isinstance(c.value[0], int))
|
|
108
|
+
for c in constants
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
# Build header
|
|
112
|
+
lines = [
|
|
113
|
+
f"/* AUTO-GENERATED by consync — do not edit by hand.",
|
|
114
|
+
f" * Source: {source_name}",
|
|
115
|
+
f" * Synced: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}",
|
|
116
|
+
f" * Regenerate: consync sync",
|
|
117
|
+
f" */",
|
|
118
|
+
f"",
|
|
119
|
+
f"#ifndef {guard}",
|
|
120
|
+
f"#define {guard}",
|
|
121
|
+
f"",
|
|
122
|
+
]
|
|
123
|
+
|
|
124
|
+
if needs_stdint and output_style == "const":
|
|
125
|
+
lines.append("#include <stdint.h>")
|
|
126
|
+
lines.append("")
|
|
127
|
+
|
|
128
|
+
# Build constant declarations
|
|
129
|
+
for c in constants:
|
|
130
|
+
name = prefix + c.name
|
|
131
|
+
if config and config.uppercase_names:
|
|
132
|
+
name = name.upper()
|
|
133
|
+
|
|
134
|
+
# Build comment
|
|
135
|
+
parts = [p for p in (c.unit, c.description) if p]
|
|
136
|
+
comment = f" /* {' | '.join(parts)} */" if parts else ""
|
|
137
|
+
|
|
138
|
+
# === Array values ===
|
|
139
|
+
if isinstance(c.value, list):
|
|
140
|
+
_render_array_const(lines, c, name, qualifier, typed_ints, precision, comment)
|
|
141
|
+
continue
|
|
142
|
+
|
|
143
|
+
# === Scalar values ===
|
|
144
|
+
# Format value and determine type
|
|
145
|
+
if isinstance(c.value, int):
|
|
146
|
+
c_type, suffix = _c_type_for_int(c.value, typed_ints)
|
|
147
|
+
val_str = str(c.value) + suffix
|
|
148
|
+
else:
|
|
149
|
+
c_type = _c_type_for_float(c.value)
|
|
150
|
+
val_str = format_c_double(c.value, precision)
|
|
151
|
+
suffix = ""
|
|
152
|
+
|
|
153
|
+
# Emit line based on output style
|
|
154
|
+
if output_style == "define":
|
|
155
|
+
lines.append(f"#define {name:<{name_width}} ({val_str}){comment}")
|
|
156
|
+
else:
|
|
157
|
+
type_width = max(len(_get_c_type(c2, typed_ints)) for c2 in constants
|
|
158
|
+
if not isinstance(c2.value, list)) if any(
|
|
159
|
+
not isinstance(c2.value, list) for c2 in constants) else len(c_type)
|
|
160
|
+
lines.append(
|
|
161
|
+
f"{qualifier} {c_type:<{type_width}} {name:<{name_width}} = {val_str};{comment}"
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
lines += ["", f"#endif /* {guard} */", ""]
|
|
165
|
+
|
|
166
|
+
filepath.parent.mkdir(parents=True, exist_ok=True)
|
|
167
|
+
filepath.write_text("\n".join(lines), encoding="utf-8")
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _get_c_type(c: "Constant", typed_ints: bool) -> str:
|
|
171
|
+
"""Get the C type string for a scalar constant."""
|
|
172
|
+
if isinstance(c.value, int):
|
|
173
|
+
return _c_type_for_int(c.value, typed_ints)[0]
|
|
174
|
+
return _c_type_for_float(c.value)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _render_array_const(
|
|
178
|
+
lines: list[str],
|
|
179
|
+
c: "Constant",
|
|
180
|
+
name: str,
|
|
181
|
+
qualifier: str,
|
|
182
|
+
typed_ints: bool,
|
|
183
|
+
precision: int,
|
|
184
|
+
comment: str,
|
|
185
|
+
) -> None:
|
|
186
|
+
"""Render an array constant as: static const int X[] = {1, 2, 3};"""
|
|
187
|
+
from consync.models import ConstantType
|
|
188
|
+
|
|
189
|
+
arr = c.value
|
|
190
|
+
if not arr:
|
|
191
|
+
return
|
|
192
|
+
|
|
193
|
+
first = arr[0]
|
|
194
|
+
if isinstance(first, int):
|
|
195
|
+
# Use typed ints based on max value in array
|
|
196
|
+
max_val = max(arr)
|
|
197
|
+
min_val = min(arr)
|
|
198
|
+
if min_val < 0:
|
|
199
|
+
c_type = _c_type_for_int(min_val, typed_ints)[0]
|
|
200
|
+
# Ensure type fits max too
|
|
201
|
+
if typed_ints:
|
|
202
|
+
c_type2 = _c_type_for_int(max_val, typed_ints)[0]
|
|
203
|
+
# Pick the wider signed type
|
|
204
|
+
if "64" in c_type or "64" in c_type2:
|
|
205
|
+
c_type = "int64_t"
|
|
206
|
+
elif "32" in c_type or "32" in c_type2:
|
|
207
|
+
c_type = "int32_t"
|
|
208
|
+
elif "16" in c_type or "16" in c_type2:
|
|
209
|
+
c_type = "int16_t"
|
|
210
|
+
else:
|
|
211
|
+
c_type = _c_type_for_int(max_val, typed_ints)[0]
|
|
212
|
+
vals = ", ".join(str(v) for v in arr)
|
|
213
|
+
elif isinstance(first, float):
|
|
214
|
+
c_type = "double"
|
|
215
|
+
vals = ", ".join(format_c_double(v, precision) for v in arr)
|
|
216
|
+
else:
|
|
217
|
+
# String arrays → not directly supported in C, skip
|
|
218
|
+
return
|
|
219
|
+
|
|
220
|
+
lines.append(
|
|
221
|
+
f"{qualifier} {c_type} {name}[] = {{{vals}}};{comment}"
|
|
222
|
+
)
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
"""C# renderer — generates a static class with typed constants.
|
|
2
|
+
|
|
3
|
+
Output example:
|
|
4
|
+
// AUTO-GENERATED by consync — do not edit by hand.
|
|
5
|
+
namespace HardwareConstants
|
|
6
|
+
{
|
|
7
|
+
public static class Constants
|
|
8
|
+
{
|
|
9
|
+
/// <summary>Current sense resistor (Ohm)</summary>
|
|
10
|
+
public const double R_SENSE = 1.9999999999910001;
|
|
11
|
+
|
|
12
|
+
/// <summary>I2C pull-up resistor (Ohm)</summary>
|
|
13
|
+
public const uint R_PULLUP = 4706;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from datetime import datetime
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
|
|
23
|
+
from consync.models import Constant, MappingConfig
|
|
24
|
+
from consync.precision import format_float
|
|
25
|
+
from consync.renderers import register
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _csharp_type(value: int | float | str, typed_ints: bool = True) -> tuple[str, str]:
|
|
29
|
+
"""Determine C# type and optional suffix for a value.
|
|
30
|
+
|
|
31
|
+
Returns (type_name, literal_suffix).
|
|
32
|
+
"""
|
|
33
|
+
if isinstance(value, int):
|
|
34
|
+
if not typed_ints:
|
|
35
|
+
return ("int", "")
|
|
36
|
+
if value < 0:
|
|
37
|
+
if -2147483648 <= value <= 2147483647:
|
|
38
|
+
return ("int", "")
|
|
39
|
+
else:
|
|
40
|
+
return ("long", "L")
|
|
41
|
+
else:
|
|
42
|
+
if value <= 2147483647:
|
|
43
|
+
return ("int", "")
|
|
44
|
+
elif value <= 4294967295:
|
|
45
|
+
return ("uint", "U")
|
|
46
|
+
else:
|
|
47
|
+
return ("ulong", "UL")
|
|
48
|
+
elif isinstance(value, float):
|
|
49
|
+
return ("double", "")
|
|
50
|
+
else:
|
|
51
|
+
return ("string", "")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@register("csharp")
|
|
55
|
+
def render_csharp(
|
|
56
|
+
constants: list[Constant],
|
|
57
|
+
filepath: str | Path,
|
|
58
|
+
*,
|
|
59
|
+
config: MappingConfig | None = None,
|
|
60
|
+
**kwargs,
|
|
61
|
+
) -> None:
|
|
62
|
+
"""Render constants as a C# static class.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
constants: Constants to write.
|
|
66
|
+
filepath: Output .cs file path.
|
|
67
|
+
config: Mapping config (for precision, namespace, etc.)
|
|
68
|
+
"""
|
|
69
|
+
filepath = Path(filepath)
|
|
70
|
+
precision = config.precision if config else kwargs.get("precision", 17)
|
|
71
|
+
source_name = config.source if config else kwargs.get("source", "unknown")
|
|
72
|
+
namespace = (config.namespace if config else kwargs.get("namespace", "")) or "Constants"
|
|
73
|
+
class_name = (config.module_name if config else kwargs.get("module_name", "")) or "HwConstants"
|
|
74
|
+
typed_ints = config.typed_ints if config else kwargs.get("typed_ints", True)
|
|
75
|
+
prefix = config.prefix if config else kwargs.get("prefix", "")
|
|
76
|
+
|
|
77
|
+
lines = [
|
|
78
|
+
f"// AUTO-GENERATED by consync — do not edit by hand.",
|
|
79
|
+
f"// Source: {source_name}",
|
|
80
|
+
f"// Synced: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}",
|
|
81
|
+
f"// Regenerate: consync sync",
|
|
82
|
+
f"",
|
|
83
|
+
f"namespace {namespace}",
|
|
84
|
+
f"{{",
|
|
85
|
+
f" public static class {class_name}",
|
|
86
|
+
f" {{",
|
|
87
|
+
]
|
|
88
|
+
|
|
89
|
+
for c in constants:
|
|
90
|
+
name = prefix + c.name
|
|
91
|
+
if config and config.uppercase_names:
|
|
92
|
+
name = name.upper()
|
|
93
|
+
|
|
94
|
+
# XML doc comment
|
|
95
|
+
doc_parts = [p for p in (c.description, f"({c.unit})" if c.unit else "") if p]
|
|
96
|
+
if doc_parts:
|
|
97
|
+
doc = " ".join(doc_parts)
|
|
98
|
+
lines.append(f" /// <summary>{doc}</summary>")
|
|
99
|
+
|
|
100
|
+
# === Array values ===
|
|
101
|
+
if isinstance(c.value, list):
|
|
102
|
+
_render_csharp_array(lines, c, name, typed_ints, precision)
|
|
103
|
+
lines.append("")
|
|
104
|
+
continue
|
|
105
|
+
|
|
106
|
+
# === Scalar values ===
|
|
107
|
+
cs_type, suffix = _csharp_type(c.value, typed_ints)
|
|
108
|
+
|
|
109
|
+
# Format value
|
|
110
|
+
if isinstance(c.value, int):
|
|
111
|
+
val_str = str(c.value) + suffix
|
|
112
|
+
elif isinstance(c.value, float):
|
|
113
|
+
val_str = format_float(c.value, precision)
|
|
114
|
+
else:
|
|
115
|
+
val_str = f'"{c.value}"'
|
|
116
|
+
|
|
117
|
+
lines.append(f" public const {cs_type} {name} = {val_str};")
|
|
118
|
+
lines.append("")
|
|
119
|
+
|
|
120
|
+
# Remove trailing blank line before closing braces
|
|
121
|
+
if lines and lines[-1] == "":
|
|
122
|
+
lines.pop()
|
|
123
|
+
|
|
124
|
+
lines += [
|
|
125
|
+
f" }}",
|
|
126
|
+
f"}}",
|
|
127
|
+
"",
|
|
128
|
+
]
|
|
129
|
+
|
|
130
|
+
filepath.parent.mkdir(parents=True, exist_ok=True)
|
|
131
|
+
filepath.write_text("\n".join(lines), encoding="utf-8")
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _render_csharp_array(
|
|
135
|
+
lines: list[str],
|
|
136
|
+
c: "Constant",
|
|
137
|
+
name: str,
|
|
138
|
+
typed_ints: bool,
|
|
139
|
+
precision: int,
|
|
140
|
+
) -> None:
|
|
141
|
+
"""Render an array constant as: public static readonly int[] X = { 1, 2, 3 };"""
|
|
142
|
+
arr = c.value
|
|
143
|
+
if not arr:
|
|
144
|
+
return
|
|
145
|
+
|
|
146
|
+
first = arr[0]
|
|
147
|
+
if isinstance(first, int):
|
|
148
|
+
cs_type = "int"
|
|
149
|
+
if typed_ints:
|
|
150
|
+
max_val = max(arr)
|
|
151
|
+
min_val = min(arr)
|
|
152
|
+
if min_val < 0:
|
|
153
|
+
if min_val >= -2147483648 and max_val <= 2147483647:
|
|
154
|
+
cs_type = "int"
|
|
155
|
+
else:
|
|
156
|
+
cs_type = "long"
|
|
157
|
+
else:
|
|
158
|
+
if max_val <= 2147483647:
|
|
159
|
+
cs_type = "int"
|
|
160
|
+
elif max_val <= 4294967295:
|
|
161
|
+
cs_type = "uint"
|
|
162
|
+
else:
|
|
163
|
+
cs_type = "ulong"
|
|
164
|
+
vals = ", ".join(str(v) for v in arr)
|
|
165
|
+
elif isinstance(first, float):
|
|
166
|
+
cs_type = "double"
|
|
167
|
+
vals = ", ".join(format_float(v, precision) for v in arr)
|
|
168
|
+
else:
|
|
169
|
+
cs_type = "string"
|
|
170
|
+
vals = ", ".join(f'"{v}"' for v in arr)
|
|
171
|
+
|
|
172
|
+
lines.append(
|
|
173
|
+
f" public static readonly {cs_type}[] {name} = {{ {vals} }};"
|
|
174
|
+
)
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""CSV renderer — writes constants to a CSV file."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import csv
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from consync.models import Constant, MappingConfig
|
|
9
|
+
from consync.renderers import register
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@register("csv")
|
|
13
|
+
def render_csv(
|
|
14
|
+
constants: list[Constant],
|
|
15
|
+
filepath: str | Path,
|
|
16
|
+
*,
|
|
17
|
+
config: MappingConfig | None = None,
|
|
18
|
+
**kwargs,
|
|
19
|
+
) -> None:
|
|
20
|
+
"""Render constants as a CSV file with headers Name, Value, Unit, Description."""
|
|
21
|
+
filepath = Path(filepath)
|
|
22
|
+
filepath.parent.mkdir(parents=True, exist_ok=True)
|
|
23
|
+
|
|
24
|
+
delimiter = ","
|
|
25
|
+
if filepath.suffix.lower() == ".tsv":
|
|
26
|
+
delimiter = "\t"
|
|
27
|
+
|
|
28
|
+
with filepath.open("w", newline="", encoding="utf-8") as f:
|
|
29
|
+
writer = csv.writer(f, delimiter=delimiter)
|
|
30
|
+
writer.writerow(["Name", "Value", "Unit", "Description"])
|
|
31
|
+
for c in constants:
|
|
32
|
+
# Format value: arrays use pipe separator, floats get precision
|
|
33
|
+
if isinstance(c.value, list):
|
|
34
|
+
if c.value and isinstance(c.value[0], float):
|
|
35
|
+
from consync.precision import format_float
|
|
36
|
+
precision = config.precision if config else 17
|
|
37
|
+
val_str = "|".join(format_float(v, precision) for v in c.value)
|
|
38
|
+
else:
|
|
39
|
+
val_str = "|".join(str(v) for v in c.value)
|
|
40
|
+
elif isinstance(c.value, float):
|
|
41
|
+
from consync.precision import format_float
|
|
42
|
+
precision = config.precision if config else 17
|
|
43
|
+
val_str = format_float(c.value, precision)
|
|
44
|
+
else:
|
|
45
|
+
val_str = str(c.value)
|
|
46
|
+
writer.writerow([c.name, val_str, c.unit or "", c.description or ""])
|