tinycwrap 0.0.2__tar.gz → 0.0.4__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.2
3
+ Version: 0.0.4
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.2"
7
+ version = "0.0.4"
8
8
  description = "Lightweight C-to-Python wrapper generator using CFFI and NumPy"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -27,12 +27,18 @@ def test_circle_points_length_contract(cg):
27
27
  assert pts.shape == (101,)
28
28
 
29
29
 
30
+
30
31
  def test_return_struct_array_member(cg):
31
32
  seg = cg.G2DSegment()
32
33
  seg.type = 1
33
34
  seg.data = [0.0, 0.0, 1.0, 1.0, 2.0, 2.0, 3.0, 3.0]
34
35
  assert seg.type == 1
35
36
  np.testing.assert_allclose(seg.data, [0.0, 0.0, 1.0, 1.0, 2.0, 2.0, 3.0, 3.0])
37
+ seg= cg.G2DSegment(type=2, data=[9.0]*8)
38
+ assert seg.type == 2
39
+ np.testing.assert_allclose(seg.data, [9.0]*8)
40
+
41
+
36
42
 
37
43
  def test_geom2d_return_strct(cg):
38
44
  seg = cg.geom2d_line_segment_from_start_length(0.1, 0.2, 0.5, 0.4, 0.3)
@@ -52,3 +58,12 @@ def test_docstring_contains_contract(cg):
52
58
  doc = cg.geom2d_rectangle_to_path.__doc__
53
59
  assert "Contract: len(out_segments)=4" in doc
54
60
  assert "Auto-wrapped C function `geom2d_rectangle_to_path`." in doc
61
+
62
+
63
+ def test_polygon_length(cg):
64
+ pts = cg.G2DPoints(len_points=3)
65
+ pts.points[0]["x"] = 0.0; pts.points[0]["y"] = 0.0
66
+ pts.points[1]["x"] = 1.0; pts.points[1]["y"] = 0.0
67
+ pts.points[2]["x"] = 1.0; pts.points[2]["y"] = 1.0
68
+ length = cg.geom2d_polygon_length(pts.points)
69
+ assert length == 2.0
@@ -0,0 +1,54 @@
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))
24
+
25
+
26
+ def test_path_struct_pointer_array(cp):
27
+ path = cp.G2DPath(len_segments=3)
28
+ assert path.len_segments == 3
29
+ assert isinstance(path.segments, np.ndarray)
30
+ assert path.segments.shape == (3,)
31
+ assert path.segments.dtype == cp.G2DSegment.dtype
32
+
33
+
34
+ def test_return_class_path(cp):
35
+ path=cp.G2DPath(len_segments=1)
36
+ cp.geom2d_path_from_circle(1.0, path)
37
+ assert path.len_segments == 1
38
+ assert path.segments.shape == (1,)
39
+ seg = path.segments[0]
40
+ assert seg['type'] == 1 # circle segment
41
+ np.testing.assert_allclose(seg['data'][:3], [0.0, 0.0, 1.0]) # cx,cy,radius
42
+
43
+ def test_build_pointer_from_data(cp):
44
+ segments=cp.geom2d_rectangle_to_path(2.0, 3.0)
45
+ assert segments.shape == (4,)
46
+ path=cp.G2DPath(segments=segments, len_segments=len(segments))
47
+ assert path.len_segments == 4
48
+ assert path.segments.shape == (4,)
49
+
50
+ def test_repr_pointer_array(cp):
51
+ path = cp.G2DPath(len_segments=2)
52
+ repr_str = repr(path)
53
+ assert "segments=<segments* 0x" in repr_str
54
+ assert "len_segments=2" in repr_str
@@ -6,6 +6,6 @@ from .cmodule import CModule
6
6
  try:
7
7
  __version__ = version("tinycwrap")
8
8
  except PackageNotFoundError:
9
- __version__ = "0.0.2"
9
+ __version__ = "0.0.4"
10
10
 
11
11
  __all__ = ["CModule", "__version__"]
@@ -462,8 +462,19 @@ class CModule:
462
462
  for sname, sspec in self._struct_specs.items():
463
463
  try:
464
464
  dtype_fields = []
465
+ pointer_meta = []
465
466
  for f in sspec.fields:
466
- base_dtype = numpy_dtype_for_base_type(f.base_type)
467
+ try:
468
+ base_dtype = numpy_dtype_for_base_type(f.base_type)
469
+ except TypeError:
470
+ base_dtype = None
471
+ if f.is_pointer:
472
+ dtype_fields.append((f.name, np.uintp))
473
+ pointer_meta.append(f)
474
+ if base_dtype is None:
475
+ continue
476
+ if base_dtype is None:
477
+ continue
467
478
  if f.array_len:
468
479
  dtype_fields.append((f.name, (base_dtype, (f.array_len,))))
469
480
  else:
@@ -472,19 +483,75 @@ class CModule:
472
483
  except TypeError:
473
484
  continue
474
485
 
475
- def make_struct_class(spec, dtype):
476
- slots = ("_data",)
486
+ def make_struct_class(spec, dtype, _struct_dtypes=self._struct_dtypes):
487
+ slots = ("_data", "_children")
477
488
 
478
489
  def __init__(self, **kwargs):
479
490
  data = np.zeros((), dtype=dtype)
491
+ children = {}
492
+ # handle pointer+len convention
493
+ for f in spec.fields:
494
+ if f.is_pointer and f.name in dtype.names:
495
+ len_field = f"len_{f.name}"
496
+ target_dtype = _struct_dtypes.get(f.base_type)
497
+ provided = kwargs.get(f.name)
498
+ if provided is not None and target_dtype is not None:
499
+ arr = np.ascontiguousarray(provided, dtype=target_dtype)
500
+ children[f.name] = arr
501
+ data[f.name] = arr.ctypes.data
502
+ if len_field in dtype.names and len_field not in kwargs:
503
+ data[len_field] = len(arr)
504
+ elif len_field in kwargs and target_dtype is not None:
505
+ llen = int(kwargs[len_field])
506
+ arr = np.zeros(llen, dtype=target_dtype)
507
+ children[f.name] = arr
508
+ data[f.name] = arr.ctypes.data
509
+ data[len_field] = llen
510
+ elif provided is not None:
511
+ arr = np.ascontiguousarray(provided)
512
+ children[f.name] = arr
513
+ data[f.name] = arr.ctypes.data
514
+ if len_field in dtype.names and len_field not in kwargs:
515
+ data[len_field] = len(arr)
516
+ elif target_dtype is not None and len_field in dtype.names:
517
+ llen = int(kwargs.get(len_field, 1))
518
+ arr = np.zeros(llen, dtype=target_dtype)
519
+ children[f.name] = arr
520
+ data[f.name] = arr.ctypes.data
521
+ data[len_field] = llen
480
522
  for k in dtype.names:
481
523
  if k in kwargs:
482
- data[k] = kwargs[k]
524
+ val = kwargs[k]
525
+ field_info = dtype.fields[k][0]
526
+ if field_info.shape != ():
527
+ arr = np.asarray(val, dtype=field_info.base)
528
+ if arr.shape != field_info.shape:
529
+ raise ValueError(f"Field {k} expects shape {field_info.shape}, got {arr.shape}")
530
+ np.copyto(data[k], arr)
531
+ else:
532
+ if k in children:
533
+ data[k] = children[k].ctypes.data
534
+ elif isinstance(val, np.ndarray):
535
+ if val.dtype.kind == "V" and val.shape != ():
536
+ arr = np.ascontiguousarray(val)
537
+ data[k] = arr.ctypes.data
538
+ if f"len_{k}" in dtype.names and f"len_{k}" not in kwargs:
539
+ data[f"len_{k}"] = len(arr)
540
+ else:
541
+ data[k] = val
542
+ else:
543
+ data[k] = val
483
544
  object.__setattr__(self, "_data", data)
545
+ object.__setattr__(self, "_children", children)
484
546
 
485
547
  def __repr__(self):
486
548
  parts_list = []
549
+ pointer_lengths = getattr(self, "_pointer_lengths", {})
487
550
  for name in dtype.names:
551
+ if name in pointer_lengths:
552
+ ptr_val = int(self._data[name])
553
+ parts_list.append(f"{name}=<{name}* 0x{ptr_val:x}>")
554
+ continue
488
555
  val = self._data[name]
489
556
  if val.shape == ():
490
557
  parts_list.append(f"{name}={val.item()!r}")
@@ -502,13 +569,16 @@ class CModule:
502
569
  "zeros": staticmethod(
503
570
  lambda n, _dtype=dtype: np.zeros(n, dtype=_dtype)
504
571
  ),
572
+ "_pointer_lengths": {f.name: f"len_{f.name}" for f in spec.fields if f.is_pointer},
505
573
  }
506
574
 
507
575
  for fname in dtype.names:
508
576
  field_info = dtype.fields[fname][0]
509
577
  is_scalar = field_info.shape == ()
510
578
 
511
- def getter(self, fname=fname, is_scalar=is_scalar):
579
+ def getter(self, fname=fname, is_scalar=is_scalar, field_info=field_info):
580
+ if fname in getattr(self, "_children", {}):
581
+ return self._children[fname]
512
582
  val = self._data[fname]
513
583
  return val.item() if is_scalar else val
514
584
 
@@ -519,6 +589,11 @@ class CModule:
519
589
  field_info=field_info,
520
590
  is_scalar=is_scalar,
521
591
  ):
592
+ if fname in getattr(self, "_children", {}):
593
+ arr = np.ascontiguousarray(value, dtype=self._children[fname].dtype)
594
+ self._children[fname] = arr
595
+ self._data[fname] = arr.ctypes.data
596
+ return
522
597
  if is_scalar:
523
598
  self._data[fname] = value
524
599
  else:
@@ -594,20 +669,26 @@ class CModule:
594
669
  struct_names = set(self._struct_specs.keys())
595
670
  params: list[str] = []
596
671
  len_params: list[str] = []
672
+ optional_seen = False
597
673
  for a in fspec.args:
598
674
  if a.is_array_out and a.name.lower().startswith("out"):
599
675
  params.append(f"{a.name}=None")
676
+ optional_seen = True
600
677
  elif a.is_length_param:
601
678
  len_params.append(f"{a.name}=None")
679
+ optional_seen = True
602
680
  elif (
603
681
  a.is_pointer
604
682
  and a.base_type in struct_names
605
683
  and not a.is_const
606
- and a.name.lower().startswith("out")
607
684
  ):
608
685
  params.append(f"{a.name}=None")
686
+ optional_seen = True
609
687
  else:
610
- params.append(a.name)
688
+ if optional_seen:
689
+ params.append(f"{a.name}=None")
690
+ else:
691
+ params.append(a.name)
611
692
 
612
693
  params.extend(len_params)
613
694
  signature = ", ".join(params)
@@ -725,11 +806,10 @@ class CModule:
725
806
  if a.is_array_in:
726
807
  const_prefix = "const " if a.is_const else ""
727
808
  if a.base_type in struct_names:
728
- cls_expr = f"_struct_classes['{a.base_type}']"
729
809
  dtype_expr = f"_struct_dtypes['{a.base_type}']"
730
810
  pre_lines += [
731
- f" if isinstance({a.name}, {cls_expr}):",
732
- f" arr_{a.name} = {a.name}._data",
811
+ f" if hasattr({a.name}, '_data') and getattr({a.name}, '_data', None) is not None and getattr({a.name}, '_data').dtype == {dtype_expr}:",
812
+ f" arr_{a.name} = getattr({a.name}, '_data')",
733
813
  " else:",
734
814
  f" arr_{a.name} = np.ascontiguousarray({a.name}, dtype={dtype_expr})",
735
815
  f" ptr_{a.name} = _self._ffi.cast('{const_prefix}{a.base_type} *', _self._ffi.from_buffer(arr_{a.name}))",
@@ -834,11 +914,10 @@ class CModule:
834
914
  )
835
915
  elif a.is_pointer and a.base_type in struct_names:
836
916
  dtype_expr = f"_struct_dtypes['{a.base_type}']"
837
- cls_expr = f"_struct_classes['{a.base_type}']"
838
917
  if a.is_const:
839
918
  out_lines += [
840
- f" if isinstance({a.name}, {cls_expr}):",
841
- f" arr_{a.name} = {a.name}._data",
919
+ f" if hasattr({a.name}, '_data') and getattr({a.name}, '_data', None) is not None and getattr({a.name}, '_data').dtype == {dtype_expr}:",
920
+ f" arr_{a.name} = getattr({a.name}, '_data')",
842
921
  " else:",
843
922
  f" arr_{a.name} = np.ascontiguousarray({a.name}, dtype={dtype_expr})",
844
923
  f" ptr_{a.name} = _self._ffi.cast('{a.base_type} *', _self._ffi.from_buffer(arr_{a.name}))",
@@ -846,33 +925,18 @@ class CModule:
846
925
  else:
847
926
  out_lines += [
848
927
  f" if {a.name} is None:",
849
- ]
850
- if a.array_len is not None:
851
- out_lines += [
852
- f" arr_{a.name} = np.zeros({int(a.array_len)}, dtype={dtype_expr})"
853
- ]
854
- elif a.name in contract_map:
855
- expr_py = _expr_py(contract_map[a.name])
856
- out_lines += [
857
- f" arr_{a.name} = np.zeros(int({expr_py}), dtype={dtype_expr})"
858
- ]
859
- elif a.array_len is None and a.name not in contract_map:
860
- out_lines += [
861
- f" arr_{a.name} = np.zeros((), dtype={dtype_expr})"
862
- ]
863
- else:
864
- out_lines += [
865
- f" raise ValueError('{fspec.name}: provide {a.name} or a Contract for its length')"
866
- ]
867
- out_lines += [
868
- f" elif isinstance({a.name}, {cls_expr}):",
928
+ f" obj_{a.name} = _struct_classes['{a.base_type}']()",
929
+ f" arr_{a.name} = obj_{a.name}._data",
930
+ f" elif hasattr({a.name}, '_data') and getattr({a.name}, '_data', None) is not None and getattr({a.name}, '_data').dtype == {dtype_expr}:",
931
+ f" obj_{a.name} = {a.name}",
869
932
  f" arr_{a.name} = {a.name}._data",
870
933
  " else:",
871
934
  f" arr_{a.name} = np.ascontiguousarray({a.name}, dtype={dtype_expr})",
935
+ f" obj_{a.name} = None",
872
936
  f" ptr_{a.name} = _self._ffi.cast('{a.base_type} *', _self._ffi.from_buffer(arr_{a.name}))",
873
937
  ]
874
938
  output_names.append(a.name)
875
- output_vars.append(f"arr_{a.name}")
939
+ output_vars.append(f"obj_{a.name} if obj_{a.name} is not None else arr_{a.name}")
876
940
  arg_call_args[idx] = f"ptr_{a.name}"
877
941
  elif a.is_scalar and not a.is_length_param and arg_call_args[idx] is None:
878
942
  scalar_lines.append(f" {a.name} = {a.name}")
@@ -200,10 +200,6 @@ def _parse_structs_with_pycparser(cdef: str) -> dict[str, StructSpec]:
200
200
  for decl in struct.decls:
201
201
  ctype, is_ptr, is_const, arr_len = _ctype_from_decl(decl.type)
202
202
  base = base_type_from_ctype(ctype)
203
- try:
204
- numpy_dtype_for_base_type(base)
205
- except TypeError:
206
- continue
207
203
  fields.append(
208
204
  StructField(
209
205
  name=decl.name,
@@ -311,7 +307,7 @@ def _parse_structs_regex(cdef: str) -> dict[str, StructSpec]:
311
307
  if not line:
312
308
  continue
313
309
  line = re.sub(r"/\*.*?\*/", "", line).strip()
314
- m_field = re.match(r"(.+?)\s+([A-Za-z_]\w*)(\s*\[(\d+)\])?$", line)
310
+ m_field = re.match(r"(.+?\*?)\s*([A-Za-z_]\w*)(\s*\[(\d+)\])?$", line)
315
311
  if not m_field:
316
312
  continue
317
313
  raw_ctype = m_field.group(1).strip()
@@ -320,10 +316,6 @@ def _parse_structs_regex(cdef: str) -> dict[str, StructSpec]:
320
316
  is_pointer = "*" in raw_ctype
321
317
  is_const = "const" in raw_ctype
322
318
  base = base_type_from_ctype(raw_ctype)
323
- try:
324
- numpy_dtype_for_base_type(base)
325
- except TypeError:
326
- continue
327
319
  fields.append(
328
320
  StructField(
329
321
  name=fname,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tinycwrap
3
- Version: 0.0.2
3
+ Version: 0.0.4
4
4
  Summary: Lightweight C-to-Python wrapper generator using CFFI and NumPy
5
5
  Author: TinyCWrap Contributors
6
6
  Requires-Python: >=3.9
@@ -1,23 +0,0 @@
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))
File without changes
File without changes