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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tinycwrap
3
- Version: 0.0.1
3
+ Version: 0.0.2
4
4
  Summary: Lightweight C-to-Python wrapper generator using CFFI and NumPy
5
5
  Author: TinyCWrap Contributors
6
6
  Requires-Python: >=3.9
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "tinycwrap"
7
- version = "0.0.1"
7
+ version = "0.0.2"
8
8
  description = "Lightweight C-to-Python wrapper generator using CFFI and NumPy"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -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))
@@ -6,6 +6,6 @@ from .cmodule import CModule
6
6
  try:
7
7
  __version__ = version("tinycwrap")
8
8
  except PackageNotFoundError:
9
- __version__ = "0.0.1"
9
+ __version__ = "0.0.2"
10
10
 
11
11
  __all__ = ["CModule", "__version__"]
@@ -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._cdef is None)
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
- print(f"[CModule] Compiling: {' '.join(cmd)}")
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
- print(f"[CModule] Loaded {so_path}")
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 = True) -> str:
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
- try:
345
- src = self._c_path.read_text(encoding="utf8")
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._c_path.read_text(encoding="utf8")
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
- try:
564
- c_source_text = self._c_path.read_text(encoding="utf8")
565
- except OSError:
566
- c_source_text = None
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.empty({int(a.array_len)}, dtype=base_dtype)"
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.empty(int({expr_py}), dtype=base_dtype)"
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.empty_like(ref_arr, dtype=base_dtype)",
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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tinycwrap
3
- Version: 0.0.1
3
+ Version: 0.0.2
4
4
  Summary: Lightweight C-to-Python wrapper generator using CFFI and NumPy
5
5
  Author: TinyCWrap Contributors
6
6
  Requires-Python: >=3.9
@@ -2,6 +2,7 @@ README.md
2
2
  pyproject.toml
3
3
  tests/test_examples.py
4
4
  tests/test_geom.py
5
+ tests/test_path.py
5
6
  tinycwrap/__init__.py
6
7
  tinycwrap/cmodule.py
7
8
  tinycwrap/parsing.py
File without changes
File without changes