Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions _codeql_detected_source_root
9 changes: 7 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[build-system]
requires = ["uv_build>=0.9,<0.10"]
build-backend = "uv_build"
requires = ["setuptools>=61.0", "wheel"]
build-backend = "setuptools.build_meta"

[project]
name = "foamlib"
Expand Down Expand Up @@ -119,3 +119,8 @@ extend-ignore = [

[tool.ruff.lint.pydocstyle]
convention = "pep257"

[tool.setuptools]
ext-modules = [
{name = "foamlib._c._skip", sources = ["src/foamlib/_c/skip.c"]}
]
1 change: 1 addition & 0 deletions src/foamlib/_c/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""C extension modules for foamlib."""
29 changes: 29 additions & 0 deletions src/foamlib/_c/_skip.pyi
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
"""Type stubs for the _skip C extension module."""

Check failure on line 1 in src/foamlib/_c/_skip.pyi

View workflow job for this annotation

GitHub Actions / lint

Ruff (PYI021)

src/foamlib/_c/_skip.pyi:1:1: PYI021 Docstrings should not be included in stubs

Check failure on line 1 in src/foamlib/_c/_skip.pyi

View workflow job for this annotation

GitHub Actions / lint

Ruff (PYI021)

src/foamlib/_c/_skip.pyi:1:1: PYI021 Docstrings should not be included in stubs

def skip(

Check failure on line 3 in src/foamlib/_c/_skip.pyi

View workflow job for this annotation

GitHub Actions / lint

Ruff (PYI048)

src/foamlib/_c/_skip.pyi:3:5: PYI048 Function body must contain exactly one statement

Check failure on line 3 in src/foamlib/_c/_skip.pyi

View workflow job for this annotation

GitHub Actions / lint

Ruff (PYI048)

src/foamlib/_c/_skip.pyi:3:5: PYI048 Function body must contain exactly one statement
contents: bytes | bytearray,
pos: int,
*,
newline_ok: bool = True,
) -> int:
"""
Skip whitespace and comments in OpenFOAM file content.

Check failure on line 11 in src/foamlib/_c/_skip.pyi

View workflow job for this annotation

GitHub Actions / lint

Ruff (W293)

src/foamlib/_c/_skip.pyi:11:1: W293 Blank line contains whitespace

Check failure on line 11 in src/foamlib/_c/_skip.pyi

View workflow job for this annotation

GitHub Actions / lint

Ruff (W293)

src/foamlib/_c/_skip.pyi:11:1: W293 Blank line contains whitespace
This function skips over whitespace characters (space, tab, newline, etc.) and
both types of comments:
- Line comments starting with // (until the end of the line)
- Block comments enclosed in /* */

Check failure on line 16 in src/foamlib/_c/_skip.pyi

View workflow job for this annotation

GitHub Actions / lint

Ruff (W293)

src/foamlib/_c/_skip.pyi:16:1: W293 Blank line contains whitespace

Check failure on line 16 in src/foamlib/_c/_skip.pyi

View workflow job for this annotation

GitHub Actions / lint

Ruff (W293)

src/foamlib/_c/_skip.pyi:16:1: W293 Blank line contains whitespace
Args:
contents: The byte content to parse (bytes or bytearray).
pos: The starting position in the content.
newline_ok: Whether newlines should be considered whitespace (default: True).
When False, the function will stop at newline characters.

Check failure on line 22 in src/foamlib/_c/_skip.pyi

View workflow job for this annotation

GitHub Actions / lint

Ruff (W293)

src/foamlib/_c/_skip.pyi:22:1: W293 Blank line contains whitespace

Check failure on line 22 in src/foamlib/_c/_skip.pyi

View workflow job for this annotation

GitHub Actions / lint

Ruff (W293)

src/foamlib/_c/_skip.pyi:22:1: W293 Blank line contains whitespace
Returns:
The new position after skipping whitespace and comments.

Check failure on line 25 in src/foamlib/_c/_skip.pyi

View workflow job for this annotation

GitHub Actions / lint

Ruff (W293)

src/foamlib/_c/_skip.pyi:25:1: W293 Blank line contains whitespace

Check failure on line 25 in src/foamlib/_c/_skip.pyi

View workflow job for this annotation

GitHub Actions / lint

Ruff (W293)

src/foamlib/_c/_skip.pyi:25:1: W293 Blank line contains whitespace
Raises:
FoamFileDecodeError: If an unclosed block comment is encountered.
"""

Check failure on line 28 in src/foamlib/_c/_skip.pyi

View workflow job for this annotation

GitHub Actions / lint

Ruff (PYI021)

src/foamlib/_c/_skip.pyi:9:5: PYI021 Docstrings should not be included in stubs

Check failure on line 28 in src/foamlib/_c/_skip.pyi

View workflow job for this annotation

GitHub Actions / lint

Ruff (PYI021)

src/foamlib/_c/_skip.pyi:9:5: PYI021 Docstrings should not be included in stubs
...

Check failure on line 29 in src/foamlib/_c/_skip.pyi

View workflow job for this annotation

GitHub Actions / lint

Ruff (PIE790)

src/foamlib/_c/_skip.pyi:29:5: PIE790 Unnecessary `...` literal

Check failure on line 29 in src/foamlib/_c/_skip.pyi

View workflow job for this annotation

GitHub Actions / lint

Ruff (PIE790)

src/foamlib/_c/_skip.pyi:29:5: PIE790 Unnecessary `...` literal
187 changes: 187 additions & 0 deletions src/foamlib/_c/skip.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
#define PY_SSIZE_T_CLEAN
#include <Python.h>

/* Whitespace lookup tables */
static int is_whitespace[256] = {0};
static int is_whitespace_no_newline[256] = {0};

/* Initialize whitespace lookup tables */
static void init_whitespace_tables(void) {
static int initialized = 0;
if (initialized) return;

/* Whitespace characters: space, newline, tab, carriage return, form feed, vertical tab */
const char *ws = " \n\t\r\f\v";
for (const char *c = ws; *c; c++) {
is_whitespace[(unsigned char)*c] = 1;
is_whitespace_no_newline[(unsigned char)*c] = 1;
}
is_whitespace_no_newline['\n'] = 0;

initialized = 1;
}

/*
* Skip whitespace and comments in OpenFOAM file content.
* Returns the new position after skipping whitespace and comments.
*
* Arguments:
* contents: bytes or bytearray object
* pos: starting position (int)
* newline_ok: whether newlines are considered whitespace (bool, default True)
*
* Returns:
* New position (int)
*/
static PyObject *
skip(PyObject *self, PyObject *args, PyObject *kwargs)
{
PyObject *contents_obj;
Py_ssize_t pos;
int newline_ok = 1;

static char *kwlist[] = {"contents", "pos", "newline_ok", NULL};

if (!PyArg_ParseTupleAndKeywords(args, kwargs, "On|p", kwlist,
&contents_obj, &pos, &newline_ok)) {
return NULL;
}

/* Initialize whitespace tables */
init_whitespace_tables();

/* Get buffer from bytes or bytearray */
Py_buffer buffer;
if (PyObject_GetBuffer(contents_obj, &buffer, PyBUF_SIMPLE) < 0) {
return NULL;
}

const unsigned char *contents = (const unsigned char *)buffer.buf;
Py_ssize_t len = buffer.len;

/* Choose whitespace table based on newline_ok */
const int *ws_table = newline_ok ? is_whitespace : is_whitespace_no_newline;

/* Skip whitespace and comments */
while (pos < len) {
/* Skip whitespace */
while (pos < len && ws_table[contents[pos]]) {
pos++;
}

/* Check for comments */
if (pos + 1 >= len) {
break;
}

unsigned char next1 = contents[pos];
unsigned char next2 = contents[pos + 1];

/* Handle // comments */
if (next1 == '/' && next2 == '/') {
pos += 2;
while (pos < len) {
if (contents[pos] == '\n') {
if (newline_ok) {
pos++;
}
break;
}
if (contents[pos] == '\\' && pos + 1 < len && contents[pos + 1] == '\n') {
pos++;
}
pos++;
}
continue;
}

/* Handle block comments */
if (next1 == '/' && next2 == '*') {
pos += 2;
/* Find closing */
int found = 0;
while (pos + 1 < len) {
if (contents[pos] == '*' && contents[pos + 1] == '/') {
pos += 2;
found = 1;
break;
}
pos++;
}
if (!found) {
/* Raise FoamFileDecodeError */
PyBuffer_Release(&buffer);

/* Import the exception class */
PyObject *exceptions_module = PyImport_ImportModule("foamlib._files._parsing.exceptions");
if (!exceptions_module) {
return NULL;
}

PyObject *FoamFileDecodeError_class = PyObject_GetAttrString(exceptions_module, "FoamFileDecodeError");
Py_DECREF(exceptions_module);
if (!FoamFileDecodeError_class) {
return NULL;
}

/* Create exception with keyword argument */
PyObject *kwargs_exc = Py_BuildValue("{s:s}", "expected", "*/");
PyObject *args_exc = Py_BuildValue("(On)", contents_obj, len);

if (!kwargs_exc || !args_exc) {
Py_XDECREF(kwargs_exc);
Py_XDECREF(args_exc);
Py_DECREF(FoamFileDecodeError_class);
return NULL;
}

PyObject *exc = PyObject_Call(FoamFileDecodeError_class, args_exc, kwargs_exc);
Py_DECREF(kwargs_exc);
Py_DECREF(args_exc);

if (exc) {
PyErr_SetObject(FoamFileDecodeError_class, exc);
Py_DECREF(exc);
}
Py_DECREF(FoamFileDecodeError_class);
return NULL;
}
continue;
}

/* No more whitespace or comments */
break;
}

PyBuffer_Release(&buffer);
return PyLong_FromSsize_t(pos);
}

/* Module method definitions */
static PyMethodDef skip_methods[] = {
{"skip", (PyCFunction)skip, METH_VARARGS | METH_KEYWORDS,
"Skip whitespace and comments in OpenFOAM file content.\n\n"
"Args:\n"
" contents: bytes or bytearray object\n"
" pos: starting position (int)\n"
" newline_ok: whether newlines are considered whitespace (bool, default True)\n\n"
"Returns:\n"
" New position (int)"},
{NULL, NULL, 0, NULL}
};

/* Module definition */
static struct PyModuleDef skip_module = {
PyModuleDef_HEAD_INIT,
"_skip",
"C extension module for skipping whitespace and comments in OpenFOAM files",
-1,
skip_methods
};

/* Module initialization */
PyMODINIT_FUNC
PyInit__skip(void)
{
return PyModule_Create(&skip_module);
}
47 changes: 3 additions & 44 deletions src/foamlib/_files/_parsing/_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from types import EllipsisType
from typing import Generic, Literal, TypeVar, overload

from foamlib._c._skip import skip as _skip
from foamlib._files._util import add_to_mapping

if sys.version_info >= (3, 11):
Expand Down Expand Up @@ -78,50 +79,8 @@ def make_fatal(self) -> FoamFileDecodeError:
return FoamFileDecodeError(self._contents, self.pos, expected=self._expected)


def _skip(
contents: bytes | bytearray,
pos: int,
*,
newline_ok: bool = True,
) -> int:
is_whitespace = _IS_WHITESPACE if newline_ok else _IS_WHITESPACE_NO_NEWLINE

with contextlib.suppress(IndexError):
while True:
while is_whitespace[contents[pos]]:
pos += 1

next1 = contents[pos]
next2 = contents[pos + 1]

if next1 == ord("/") and next2 == ord("/"):
pos += 2
while True:
if contents[pos] == ord("\n"):
if newline_ok:
pos += 1
break
if contents[pos] == ord("\\"):
with contextlib.suppress(IndexError):
if contents[pos + 1] == ord("\n"):
pos += 1
pos += 1
continue
pos += 1
continue

if next1 == ord("/") and next2 == ord("*"):
if (pos := contents.find(b"*/", pos + 2)) == -1:
raise FoamFileDecodeError(
contents,
len(contents),
expected="*/",
)
pos += 2
continue
break

return pos
# Python implementation of _skip has been moved to C extension module.
# The C extension (foamlib._c._skip.skip) is imported at the top of this file.


def _expect(contents: bytes | bytearray, pos: int, expected: bytes | bytearray) -> int:
Expand Down
85 changes: 85 additions & 0 deletions tests/test_c_extensions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
"""Tests for C extension modules."""

import pytest
from foamlib._c._skip import skip
from foamlib._files._parsing.exceptions import FoamFileDecodeError


def test_skip_whitespace() -> None:
"""Test skipping various whitespace characters."""
assert skip(b" hello", 0) == 3
assert skip(b"\t\t\thello", 0) == 3
assert skip(b"\n\n\nhello", 0) == 3
assert skip(b" \t\n\r\f\vhello", 0) == 6
assert skip(b"hello", 0) == 0


def test_skip_line_comment() -> None:
"""Test skipping // comments."""
assert skip(b"// comment\nhello", 0) == 11
assert skip(b"//comment\nhello", 0) == 10
assert skip(b" // comment\nhello", 0) == 13
# Line comment with backslash continuation
assert skip(b"// comment\\\n more\nhello", 0) == 19


def test_skip_block_comment() -> None:
"""Test skipping /* */ comments."""
assert skip(b"/* comment */hello", 0) == 13
assert skip(b"/*comment*/hello", 0) == 11
assert skip(b" /* comment */ hello", 0) == 17
# Multi-line block comment
assert skip(b"/* comment\nmore\nlines */hello", 0) == 24


def test_skip_mixed_whitespace_and_comments() -> None:
"""Test skipping mixed whitespace and comments."""
assert skip(b" /* comment */ // line\nhello", 0) == 25
assert skip(b"// comment\n /* block */ hello", 0) == 26


def test_skip_newline_ok_parameter() -> None:
"""Test the newline_ok parameter."""
# With newline_ok=True (default), newlines are skipped
assert skip(b" \n hello", 0, newline_ok=True) == 5
# With newline_ok=False, newlines are not skipped
assert skip(b" \n hello", 0, newline_ok=False) == 2
# Line comments with newline_ok=False
result = skip(b"// comment\nhello", 0, newline_ok=False)
assert result == 10 # Stops at the newline


def test_skip_no_content_to_skip() -> None:
"""Test when there's nothing to skip."""
assert skip(b"hello", 0) == 0
assert skip(b"123", 0) == 0


def test_skip_at_end_of_buffer() -> None:
"""Test skipping at the end of the buffer."""
assert skip(b"hello ", 5) == 8
assert skip(b"hello", 5) == 5


def test_skip_incomplete_block_comment() -> None:
"""Test that incomplete block comments raise an error."""
with pytest.raises(FoamFileDecodeError):
skip(b"/* incomplete comment", 0)


def test_skip_with_bytearray() -> None:
"""Test that the skip function works with bytearray as well as bytes."""
ba = bytearray(b" /* comment */ hello")
assert skip(ba, 0) == 17


def test_skip_multiple_comments() -> None:
"""Test skipping multiple consecutive comments."""
assert skip(b"/* c1 */ /* c2 */ /* c3 */hello", 0) == 26
assert skip(b"// c1\n// c2\n// c3\nhello", 0) == 18


def test_skip_empty_comment() -> None:
"""Test skipping empty comments."""
assert skip(b"/**/hello", 0) == 4
assert skip(b"//\nhello", 0) == 3
Loading