tinycwrap 0.0.1__tar.gz → 0.0.2__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.
- {tinycwrap-0.0.1 → tinycwrap-0.0.2}/PKG-INFO +1 -1
- {tinycwrap-0.0.1 → tinycwrap-0.0.2}/pyproject.toml +1 -1
- {tinycwrap-0.0.1 → tinycwrap-0.0.2}/tests/test_geom.py +12 -1
- tinycwrap-0.0.2/tests/test_path.py +23 -0
- {tinycwrap-0.0.1 → tinycwrap-0.0.2}/tinycwrap/__init__.py +1 -1
- {tinycwrap-0.0.1 → tinycwrap-0.0.2}/tinycwrap/cmodule.py +93 -18
- {tinycwrap-0.0.1 → tinycwrap-0.0.2}/tinycwrap.egg-info/PKG-INFO +1 -1
- {tinycwrap-0.0.1 → tinycwrap-0.0.2}/tinycwrap.egg-info/SOURCES.txt +1 -0
- {tinycwrap-0.0.1 → tinycwrap-0.0.2}/README.md +0 -0
- {tinycwrap-0.0.1 → tinycwrap-0.0.2}/setup.cfg +0 -0
- {tinycwrap-0.0.1 → tinycwrap-0.0.2}/tests/test_examples.py +0 -0
- {tinycwrap-0.0.1 → tinycwrap-0.0.2}/tinycwrap/parsing.py +0 -0
- {tinycwrap-0.0.1 → tinycwrap-0.0.2}/tinycwrap.egg-info/dependency_links.txt +0 -0
- {tinycwrap-0.0.1 → tinycwrap-0.0.2}/tinycwrap.egg-info/requires.txt +0 -0
- {tinycwrap-0.0.1 → tinycwrap-0.0.2}/tinycwrap.egg-info/top_level.txt +0 -0
|
@@ -40,4 +40,15 @@ def test_geom2d_return_strct(cg):
|
|
|
40
40
|
assert seg.data[0] == 0.1
|
|
41
41
|
assert seg.data[1] == 0.2
|
|
42
42
|
assert np.isclose(seg.data[2], 0.1+0.3*0.5)
|
|
43
|
-
assert np.isclose(seg.data[3], 0.2+0.4*0.3)
|
|
43
|
+
assert np.isclose(seg.data[3], 0.2+0.4*0.3)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def test_geom2d_return_strct_array_member(cg):
|
|
47
|
+
seg = cg.geom2d_rectangle_to_path(1,2)
|
|
48
|
+
assert len(seg) == 4
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def test_docstring_contains_contract(cg):
|
|
52
|
+
doc = cg.geom2d_rectangle_to_path.__doc__
|
|
53
|
+
assert "Contract: len(out_segments)=4" in doc
|
|
54
|
+
assert "Auto-wrapped C function `geom2d_rectangle_to_path`." in doc
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
import pytest
|
|
5
|
+
|
|
6
|
+
from tinycwrap import CModule
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@pytest.fixture(scope="module")
|
|
10
|
+
def cp():
|
|
11
|
+
return CModule(Path("tests/t1/base.c"), Path("tests/t1/path.c"))
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def test_path_get_steps_contract(cp):
|
|
15
|
+
seg = cp.G2DSegment()
|
|
16
|
+
# line from (0,0) to (1,0)
|
|
17
|
+
seg.type = 0
|
|
18
|
+
seg.data = [0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0]
|
|
19
|
+
segments = np.array([seg._data], dtype=cp.G2DSegment.dtype)
|
|
20
|
+
steps = cp.geom2d_path_get_steps(segments, ds_min=0.25)
|
|
21
|
+
expected_len = cp.geom2d_path_get_len_steps(segments, len_segments=len(segments), ds_min=0.25)
|
|
22
|
+
assert len(steps) == expected_len
|
|
23
|
+
assert np.isclose(steps[-1], cp.geom2d_path_get_length(segments, len_segments=1))
|
|
@@ -53,6 +53,7 @@ class CModule:
|
|
|
53
53
|
compiler="gcc",
|
|
54
54
|
compile_args=None,
|
|
55
55
|
reload=True,
|
|
56
|
+
verbose=False,
|
|
56
57
|
):
|
|
57
58
|
"""
|
|
58
59
|
Parameters
|
|
@@ -69,6 +70,8 @@ class CModule:
|
|
|
69
70
|
reload : bool, default True
|
|
70
71
|
If True and running inside IPython, register a pre-run-cell hook to auto-recompile
|
|
71
72
|
when the C source changes.
|
|
73
|
+
verbose : bool, default False
|
|
74
|
+
If True, print compilation and parsing details.
|
|
72
75
|
"""
|
|
73
76
|
if not c_sources:
|
|
74
77
|
raise ValueError("At least one C source path is required")
|
|
@@ -80,6 +83,7 @@ class CModule:
|
|
|
80
83
|
"compile_args": compile_args
|
|
81
84
|
or ["-O3", "-shared", "-fPIC", "-march=native", "-mtune=native"],
|
|
82
85
|
}
|
|
86
|
+
self._verbose = verbose
|
|
83
87
|
|
|
84
88
|
self._ffi = None
|
|
85
89
|
self._lib = None
|
|
@@ -136,12 +140,13 @@ class CModule:
|
|
|
136
140
|
cmd.extend(["-I", str(inc)])
|
|
137
141
|
|
|
138
142
|
# regenerate cdef from source
|
|
139
|
-
self._cdef = self._generate_cdef_from_source(verbose=self.
|
|
143
|
+
self._cdef = self._generate_cdef_from_source(verbose=self._verbose)
|
|
140
144
|
|
|
141
145
|
sources = [str(self._c_path), *(str(p) for p in self._extra_sources)]
|
|
142
146
|
cmd.extend(["-o", str(so_path), *sources])
|
|
143
147
|
|
|
144
|
-
|
|
148
|
+
if self._verbose:
|
|
149
|
+
print(f"[CModule] Compiling: {' '.join(cmd)}")
|
|
145
150
|
subprocess.run(cmd, check=True)
|
|
146
151
|
|
|
147
152
|
ffi = FFI()
|
|
@@ -153,7 +158,8 @@ class CModule:
|
|
|
153
158
|
self._sig = sig
|
|
154
159
|
self._so_path = so_path
|
|
155
160
|
|
|
156
|
-
|
|
161
|
+
if self._verbose:
|
|
162
|
+
print(f"[CModule] Loaded {so_path}")
|
|
157
163
|
|
|
158
164
|
# Re-parse cdef and attach docs from C source, then create wrappers
|
|
159
165
|
self._func_specs = parse_functions_from_cdef(self._cdef)
|
|
@@ -187,6 +193,23 @@ class CModule:
|
|
|
187
193
|
self._so_path = None
|
|
188
194
|
self._compile_and_load()
|
|
189
195
|
|
|
196
|
+
def _debug_specs(self):
|
|
197
|
+
"""Return a pretty string of parsed function specs and contracts (for debugging)."""
|
|
198
|
+
lines: list[str] = []
|
|
199
|
+
for name, spec in self._func_specs.items():
|
|
200
|
+
arg_lines = [f" {a.raw_ctype} {a.name}" for a in spec.args]
|
|
201
|
+
if arg_lines:
|
|
202
|
+
lines.append(f"{name}")
|
|
203
|
+
lines.extend(arg_lines)
|
|
204
|
+
lines.append(f" -> {spec.return_ctype}")
|
|
205
|
+
else:
|
|
206
|
+
lines.append(f"{name}() -> {spec.return_ctype}")
|
|
207
|
+
if spec.contracts:
|
|
208
|
+
for target, expr, is_post in spec.contracts:
|
|
209
|
+
prefix = "Post-Contract" if is_post else "Contract"
|
|
210
|
+
lines.append(f" {prefix}: {target} = {expr}")
|
|
211
|
+
print("\n".join(lines))
|
|
212
|
+
|
|
190
213
|
# ---------- IPython auto-reload hook ------------------------------------
|
|
191
214
|
|
|
192
215
|
def _register_ipython_autoreload(self):
|
|
@@ -230,7 +253,7 @@ class CModule:
|
|
|
230
253
|
|
|
231
254
|
# ---------- 1) auto-generate cdef from C source -------------------------
|
|
232
255
|
|
|
233
|
-
def _generate_cdef_from_source(self, verbose: bool =
|
|
256
|
+
def _generate_cdef_from_source(self, verbose: bool | None = None) -> str:
|
|
234
257
|
"""
|
|
235
258
|
Parse the C file and auto-generate a minimal cdef string.
|
|
236
259
|
|
|
@@ -334,21 +357,21 @@ class CModule:
|
|
|
334
357
|
continue
|
|
335
358
|
cdef_lines.append(line)
|
|
336
359
|
cdef = "\n".join(cdef_lines)
|
|
337
|
-
if verbose
|
|
360
|
+
show = self._verbose if verbose is None else verbose
|
|
361
|
+
if show:
|
|
338
362
|
print("[CModule] Auto-generated cdef:\n" + cdef)
|
|
339
363
|
return cdef
|
|
340
364
|
|
|
341
365
|
# ---------- 2) parse cdef -> FuncSpec/ArgSpec ---------------------------
|
|
342
366
|
|
|
343
367
|
def _attach_docs_from_source(self):
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
except OSError:
|
|
368
|
+
src = self._combined_source()
|
|
369
|
+
if not src:
|
|
347
370
|
return
|
|
348
371
|
|
|
349
372
|
for fname, fspec in self._func_specs.items():
|
|
350
|
-
# we look for: name ( ... ) /* ... */
|
|
351
|
-
pattern = rf"{re.escape(fname)}\s*\([^{{;]*\)\s*/\*(.*?)\*/"
|
|
373
|
+
# we look for: name ( ... ) [optional brace] /* ... */
|
|
374
|
+
pattern = rf"{re.escape(fname)}\s*\([^{{;]*\)\s*\{{?\s*/\*(.*?)\*/"
|
|
352
375
|
m = re.search(pattern, src, flags=re.DOTALL)
|
|
353
376
|
if not m:
|
|
354
377
|
continue
|
|
@@ -550,7 +573,7 @@ class CModule:
|
|
|
550
573
|
wrapper = namespace[fspec.name]
|
|
551
574
|
wrapper.__source__ = src
|
|
552
575
|
try:
|
|
553
|
-
wrapper.__c_source__ = self.
|
|
576
|
+
wrapper.__c_source__ = self._function_source(fspec.name) or self._combined_source()
|
|
554
577
|
except OSError:
|
|
555
578
|
wrapper.__c_source__ = None
|
|
556
579
|
return wrapper
|
|
@@ -560,10 +583,13 @@ class CModule:
|
|
|
560
583
|
Build the Python source string for a wrapper with an explicit signature.
|
|
561
584
|
Keeping this separate allows inspection/debugging of the generated code.
|
|
562
585
|
"""
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
c_source_text =
|
|
586
|
+
src_parts: list[str] = []
|
|
587
|
+
func_text = self._function_source(fspec.name)
|
|
588
|
+
if func_text:
|
|
589
|
+
c_source_text = func_text
|
|
590
|
+
else:
|
|
591
|
+
combined = self._combined_source()
|
|
592
|
+
c_source_text = combined if combined else None
|
|
567
593
|
|
|
568
594
|
struct_names = set(self._struct_specs.keys())
|
|
569
595
|
params: list[str] = []
|
|
@@ -662,11 +688,27 @@ class CModule:
|
|
|
662
688
|
)
|
|
663
689
|
}
|
|
664
690
|
|
|
691
|
+
func_names = set(self._func_specs.keys())
|
|
692
|
+
|
|
665
693
|
def _expr_py(expr: str) -> str:
|
|
666
694
|
expr = expr.strip()
|
|
667
695
|
expr = re.sub(r"len\((\w+)\)", r"len(arr_\1)", expr)
|
|
668
696
|
for name in pointer_scalar_names:
|
|
669
697
|
expr = re.sub(rf"\b{name}\b", f"int(arr_{name}.ravel()[0])", expr)
|
|
698
|
+
if func_names:
|
|
699
|
+
pattern = r"\b(" + "|".join(re.escape(n) for n in func_names) + r")\s*\(([^()]*)\)"
|
|
700
|
+
|
|
701
|
+
def _repl(m):
|
|
702
|
+
fname = m.group(1)
|
|
703
|
+
spec = self._func_specs.get(fname)
|
|
704
|
+
if not spec:
|
|
705
|
+
return m.group(0)
|
|
706
|
+
call_parts = []
|
|
707
|
+
for arg in spec.args:
|
|
708
|
+
call_parts.append(f"{arg.name}={arg.name}")
|
|
709
|
+
return f"_self.{fname}(" + ", ".join(call_parts) + ")"
|
|
710
|
+
|
|
711
|
+
expr = re.sub(pattern, _repl, expr)
|
|
670
712
|
return expr
|
|
671
713
|
|
|
672
714
|
call_args: list[str] = []
|
|
@@ -746,12 +788,12 @@ class CModule:
|
|
|
746
788
|
]
|
|
747
789
|
if a.array_len is not None:
|
|
748
790
|
out_lines += [
|
|
749
|
-
f" arr_{a.name} = np.
|
|
791
|
+
f" arr_{a.name} = np.zeros({int(a.array_len)}, dtype=base_dtype)"
|
|
750
792
|
]
|
|
751
793
|
elif a.name in contract_map:
|
|
752
794
|
expr_py = _expr_py(contract_map[a.name])
|
|
753
795
|
out_lines += [
|
|
754
|
-
f" arr_{a.name} = np.
|
|
796
|
+
f" arr_{a.name} = np.zeros(int({expr_py}), dtype=base_dtype)"
|
|
755
797
|
]
|
|
756
798
|
elif a.array_len is None and a.name not in contract_map:
|
|
757
799
|
out_lines += [
|
|
@@ -761,7 +803,7 @@ class CModule:
|
|
|
761
803
|
out_lines += [
|
|
762
804
|
f" ref_arr = locals().get('arr_{ref_name}', None)",
|
|
763
805
|
" if ref_arr is not None:",
|
|
764
|
-
f" arr_{a.name} = np.
|
|
806
|
+
f" arr_{a.name} = np.zeros_like(ref_arr, dtype=base_dtype)",
|
|
765
807
|
" else:",
|
|
766
808
|
f" raise ValueError('{fspec.name}: provide {a.name} or a Contract for its length')",
|
|
767
809
|
]
|
|
@@ -912,3 +954,36 @@ class CModule:
|
|
|
912
954
|
lines.append(f" return {ret_expr}")
|
|
913
955
|
|
|
914
956
|
return "\n".join(lines)
|
|
957
|
+
|
|
958
|
+
def _combined_source(self) -> str:
|
|
959
|
+
texts: list[str] = []
|
|
960
|
+
for path in [self._c_path, *self._extra_sources]:
|
|
961
|
+
try:
|
|
962
|
+
texts.append(path.read_text(encoding="utf8"))
|
|
963
|
+
except OSError:
|
|
964
|
+
continue
|
|
965
|
+
return "\n".join(texts)
|
|
966
|
+
|
|
967
|
+
def _function_source(self, fname: str) -> str | None:
|
|
968
|
+
"""Return the source (including body) of the given C function if found."""
|
|
969
|
+
src = self._combined_source()
|
|
970
|
+
if not src:
|
|
971
|
+
return None
|
|
972
|
+
pattern = re.compile(
|
|
973
|
+
rf"([\w\s\*\r\n]+{re.escape(fname)}\s*\([^{{;]*\))\s*(?:/\*.*?\*/\s*)?\{{",
|
|
974
|
+
re.DOTALL,
|
|
975
|
+
)
|
|
976
|
+
m = pattern.search(src)
|
|
977
|
+
if not m:
|
|
978
|
+
return None
|
|
979
|
+
brace_start = m.end() - 1
|
|
980
|
+
depth = 0
|
|
981
|
+
for idx in range(brace_start, len(src)):
|
|
982
|
+
ch = src[idx]
|
|
983
|
+
if ch == "{":
|
|
984
|
+
depth += 1
|
|
985
|
+
elif ch == "}":
|
|
986
|
+
depth -= 1
|
|
987
|
+
if depth == 0:
|
|
988
|
+
return src[m.start() : idx + 1]
|
|
989
|
+
return None
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|