Skip to content

Commit 4eeddf7

Browse files
committed
feat: worked out how to handle files between C and Python
1 parent f2d3fad commit 4eeddf7

File tree

11 files changed

+574
-126
lines changed

11 files changed

+574
-126
lines changed

poetry.lock

Lines changed: 93 additions & 93 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ pytest = "^7.2.1"
1515
pytest-cov = "^4.0.0"
1616
richbench = "^1.0.3"
1717
igraph = "^0.10.6"
18-
stimulus = { git = "https://github.com/igraph/stimulus.git", tag = "0.14.1" }
18+
stimulus = { git = "https://github.com/igraph/stimulus.git", tag = "0.15.0" }
1919

2020
[tool.poetry.group.doc.dependencies]
2121
mkdocs-material = "^9.1.21"

src/codegen/internal_functions.py.in

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import annotations
22

3+
from contextlib import ExitStack
34
from ctypes import c_char_p, c_int
45
from typing import Any, Iterable, Optional, TYPE_CHECKING
56

@@ -10,6 +11,7 @@ from .types import (
1011
BoolArray,
1112
EdgeLike,
1213
EdgeSelector,
14+
FileLike,
1315
IntArray,
1416
MatrixLike,
1517
MatrixIntLike,

src/codegen/internal_lib.py.in

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
# fmt: off
22

3-
from ctypes import cdll, c_char_p, c_double, c_int, c_size_t, c_void_p, POINTER
3+
from ctypes import cdll, c_char_p, c_double, c_int, c_size_t, c_void_p, CDLL, POINTER
44
from ctypes.util import find_library
5+
from platform import system
56
from typing import Any
67

78
from .errors import handle_igraph_error_t
89
from .types import (
910
FILE,
11+
FilePtr,
1012
igraph_attribute_combination_t,
1113
igraph_attribute_table_t,
1214
igraph_bool_t,
@@ -59,8 +61,35 @@ def _load_igraph_c_library():
5961
return lib
6062

6163

64+
def _load_libc():
65+
"""Imports the C standard library using `ctypes`."""
66+
if system() == "Windows":
67+
return CDLL("msvcrt.dll", use_errno=True)
68+
elif system() == "Darwin":
69+
return CDLL("libc.dylib", use_errno=True)
70+
elif system() == "Linux":
71+
return CDLL("libc.so.6", use_errno=True)
72+
else:
73+
raise RuntimeError("Cannot import C standard library on this platform")
74+
75+
76+
_libc: Any = _load_libc()
6277
_lib: Any = _load_igraph_c_library()
6378

79+
# Standard libc functions
80+
81+
fclose = _libc.fclose
82+
fclose.restype = int
83+
fclose.argtypes = [POINTER(FILE)]
84+
85+
fflush = _libc.fflush
86+
fflush.restype = int
87+
fflush.argtypes = [POINTER(FILE)]
88+
89+
fdopen = _libc.fdopen
90+
fdopen.restype = POINTER(FILE)
91+
fdopen.argtypes = [c_int, c_char_p]
92+
6493
# Vector type
6594

6695
igraph_vector_init = _lib.igraph_vector_init

src/codegen/types.yaml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,20 @@ BIPARTITE_TYPES:
233233
INOUT: "%C% = iterable_to_igraph_vector_bool_t(%I%) if %I% is not None else None"
234234
OUT: "%C% = _VectorBool.create(0)"
235235

236+
INFILE:
237+
PY_TYPE: FileLike
238+
PY_RETURN_TYPE: c_int
239+
INCONV:
240+
IN: '%C% = %S%.enter_context(any_to_file_ptr(%I%, "r"))'
241+
FLAGS: stack
242+
243+
OUTFILE:
244+
PY_TYPE: FileLike
245+
PY_RETURN_TYPE: c_int
246+
INCONV:
247+
IN: '%C% = %S%.enter_context(any_to_file_ptr(%I%, "w"))'
248+
FLAGS: stack
249+
236250
ADJACENCY_MODE:
237251
PY_TYPE: AdjacencyMode
238252
INCONV:

src/igraph_ctypes/_internal/conversion.py

Lines changed: 69 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,15 @@
44

55
import numpy as np
66

7-
from ctypes import addressof, memmove, POINTER
8-
from typing import Any, Iterable, Optional, Sequence, TYPE_CHECKING
7+
from contextlib import contextmanager
8+
from ctypes import addressof, get_errno, memmove, POINTER
9+
from os import strerror
10+
from typing import Any, IO, Iterable, Iterator, Optional, Sequence, TYPE_CHECKING
911

1012
from .enums import MatrixStorage
1113
from .lib import (
14+
fdopen,
15+
fflush,
1216
igraph_es_all,
1317
igraph_es_none,
1418
igraph_es_vector_copy,
@@ -60,6 +64,7 @@
6064
BoolArray,
6165
EdgeLike,
6266
EdgeSelector,
67+
FilePtr,
6368
IntArray,
6469
MatrixLike,
6570
MatrixIntLike,
@@ -86,6 +91,7 @@
8691

8792

8893
__all__ = (
94+
"any_to_file_ptr",
8995
"any_to_igraph_bool_t",
9096
"bytes_to_str",
9197
"edgelike_to_igraph_integer_t",
@@ -137,6 +143,67 @@
137143
)
138144

139145

146+
@contextmanager
147+
def any_to_file_ptr(obj: Any, mode: str) -> Iterator[Optional[FilePtr]]:
148+
"""Converts an arbitrary Python object to an open file pointer in the C
149+
layer, using the following rules:
150+
151+
- ``None`` is returned as is.
152+
153+
- Integers are treated as file handles and a low-level ``fdopen()`` call
154+
from the C standard library will be used to convert them into a ``FILE*``
155+
pointer.
156+
157+
- File-like objects with a ``fileno()`` method will be converted into a
158+
file handle and then they will be treated the same way as integers above.
159+
160+
- Anything else is forwarded to ``open()`` to convert them into a file-like
161+
object. They will then be treated as any other file-like object. The
162+
created object will be _closed_ automatically when the context manager
163+
exits.
164+
"""
165+
if obj is None:
166+
yield None
167+
return
168+
169+
handle: int
170+
fp: IO[Any] | None = None
171+
file_ptr: Optional[FilePtr] = None
172+
173+
if isinstance(obj, int):
174+
handle = obj
175+
elif hasattr(obj, "fileno") and callable(obj.fileno):
176+
# Flush pending writes first to ensure that they do not get mixed up
177+
# with the ones performed by igraph's C core
178+
if hasattr(obj, "flush") and callable(obj.flush):
179+
obj.flush()
180+
handle = obj.fileno()
181+
else:
182+
fp = open(obj, mode)
183+
try:
184+
if hasattr(fp, "fileno") and callable(fp.fileno):
185+
handle = fp.fileno()
186+
else:
187+
raise TypeError("open() returned an object without a file handle")
188+
except Exception:
189+
fp.close()
190+
fp = None
191+
raise
192+
193+
try:
194+
file_ptr = fdopen(
195+
handle, mode.encode("ascii") if isinstance(mode, str) else mode
196+
)
197+
if not file_ptr:
198+
errno = get_errno()
199+
raise OSError(errno, strerror(errno))
200+
yield file_ptr
201+
fflush(file_ptr)
202+
finally:
203+
if fp is not None:
204+
fp.close() # takes care of fclose() on file_ptr
205+
206+
140207
def any_to_igraph_bool_t(obj: Any) -> igraph_bool_t:
141208
"""Converts an arbitrary Python object to an igraph boolean by taking its
142209
truth value.

0 commit comments

Comments
 (0)