Skip to content

Commit 00fa954

Browse files
authored
Merge pull request #213 from SwayamInSync/np_ld_tests
ENH: Support arbitrary-length Python ints in QuadPrecision constructor
2 parents a63dc48 + 27555fb commit 00fa954

File tree

2 files changed

+180
-22
lines changed

2 files changed

+180
-22
lines changed

quaddtype/numpy_quaddtype/src/scalar.c

Lines changed: 58 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,52 @@ QuadPrecision_raw_new(QuadBackendType backend)
4141
return new;
4242
}
4343

44+
static QuadPrecisionObject *
45+
quad_from_py_int(PyObject *py_int, QuadBackendType backend, QuadPrecisionObject *self_to_cleanup)
46+
{
47+
int overflow = 0;
48+
long long lval = PyLong_AsLongLongAndOverflow(py_int, &overflow);
49+
50+
if (overflow != 0) {
51+
// Integer is too large, convert to string and recursively call QuadPrecision_from_object
52+
PyObject *str_obj = PyObject_Str(py_int);
53+
if (str_obj == NULL) {
54+
if (self_to_cleanup) {
55+
Py_DECREF(self_to_cleanup);
56+
}
57+
return NULL;
58+
}
59+
60+
QuadPrecisionObject *result = QuadPrecision_from_object(str_obj, backend);
61+
Py_DECREF(str_obj);
62+
if (self_to_cleanup) {
63+
Py_DECREF(self_to_cleanup); // discard the default one
64+
}
65+
return result;
66+
}
67+
else if (lval == -1 && PyErr_Occurred()) {
68+
if (self_to_cleanup) {
69+
Py_DECREF(self_to_cleanup);
70+
}
71+
return NULL;
72+
}
73+
74+
// No overflow, use the integer value directly
75+
QuadPrecisionObject *self = self_to_cleanup ? self_to_cleanup : QuadPrecision_raw_new(backend);
76+
if (!self) {
77+
return NULL;
78+
}
79+
80+
if (backend == BACKEND_SLEEF) {
81+
self->value.sleef_value = Sleef_cast_from_int64q1(lval);
82+
}
83+
else {
84+
self->value.longdouble_value = (long double)lval;
85+
}
86+
return self;
87+
88+
}
89+
4490
QuadPrecisionObject *
4591
QuadPrecision_from_object(PyObject *value, QuadBackendType backend)
4692
{
@@ -76,16 +122,10 @@ QuadPrecision_from_object(PyObject *value, QuadBackendType backend)
76122
Py_DECREF(self);
77123
return NULL;
78124
}
79-
long long lval = PyLong_AsLongLong(py_int);
80-
Py_DECREF(py_int);
81125

82-
if (backend == BACKEND_SLEEF) {
83-
self->value.sleef_value = Sleef_cast_from_int64q1(lval);
84-
}
85-
else {
86-
self->value.longdouble_value = (long double)lval;
87-
}
88-
return self;
126+
QuadPrecisionObject *result = quad_from_py_int(py_int, backend, self);
127+
Py_DECREF(py_int);
128+
return result;
89129
}
90130
// Try as boolean
91131
else if (PyArray_IsScalar(value, Bool)) {
@@ -94,9 +134,16 @@ QuadPrecision_from_object(PyObject *value, QuadBackendType backend)
94134
Py_DECREF(self);
95135
return NULL;
96136
}
137+
138+
// Booleans are always 0 or 1, so no overflow check needed
97139
long long lval = PyLong_AsLongLong(py_int);
98140
Py_DECREF(py_int);
99141

142+
if (lval == -1 && PyErr_Occurred()) {
143+
Py_DECREF(self);
144+
return NULL;
145+
}
146+
100147
if (backend == BACKEND_SLEEF) {
101148
self->value.sleef_value = Sleef_cast_from_int64q1(lval);
102149
}
@@ -145,7 +192,7 @@ QuadPrecision_from_object(PyObject *value, QuadBackendType backend)
145192
self->value.longdouble_value = (long double)dval;
146193
}
147194
}
148-
else if (PyUnicode_CheckExact(value)) {
195+
else if (PyUnicode_Check(value)) {
149196
const char *s = PyUnicode_AsUTF8(value);
150197
char *endptr = NULL;
151198
if (backend == BACKEND_SLEEF) {
@@ -161,18 +208,7 @@ QuadPrecision_from_object(PyObject *value, QuadBackendType backend)
161208
}
162209
}
163210
else if (PyLong_Check(value)) {
164-
long long val = PyLong_AsLongLong(value);
165-
if (val == -1 && PyErr_Occurred()) {
166-
PyErr_SetString(PyExc_OverflowError, "Overflow Error, value out of range");
167-
Py_DECREF(self);
168-
return NULL;
169-
}
170-
if (backend == BACKEND_SLEEF) {
171-
self->value.sleef_value = Sleef_cast_from_int64q1(val);
172-
}
173-
else {
174-
self->value.longdouble_value = (long double)val;
175-
}
211+
return quad_from_py_int(value, backend, self);
176212
}
177213
else if (Py_TYPE(value) == &QuadPrecision_Type) {
178214
Py_DECREF(self); // discard the default one

quaddtype/tests/test_quaddtype.py

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,66 @@ def test_create_scalar_simple():
1616
assert isinstance(QuadPrecision(1), QuadPrecision)
1717

1818

19+
@pytest.mark.parametrize("int_val", [
20+
# Very large integers that exceed long double range
21+
2 ** 1024,
22+
2 ** 2048,
23+
10 ** 308,
24+
10 ** 4000,
25+
# Edge cases
26+
0,
27+
1,
28+
-1,
29+
# Negative large integers
30+
-(2 ** 1024),
31+
])
32+
def test_create_scalar_from_large_int(int_val):
33+
"""Test that QuadPrecision can handle very large integers beyond long double range.
34+
35+
This test ensures that integers like 2**1024, which overflow standard long double,
36+
are properly converted via string representation to QuadPrecision without raising
37+
overflow errors. The conversion should match the string-based conversion.
38+
"""
39+
# Convert large int to QuadPrecision
40+
result = QuadPrecision(int_val)
41+
assert isinstance(result, QuadPrecision)
42+
43+
# String conversion should give the same result
44+
str_val = str(int_val)
45+
result_from_str = QuadPrecision(str_val)
46+
47+
# Both conversions should produce the same value
48+
# (can be inf==inf on some platforms for very large values)
49+
assert result == result_from_str
50+
51+
# For zero and small values, verify exact conversion
52+
if int_val == 0:
53+
assert float(result) == 0.0
54+
elif abs(int_val) == 1:
55+
assert float(result) == float(int_val)
56+
57+
58+
def test_create_scalar_from_int_with_broken_str():
59+
"""Test that QuadPrecision handles errors when __str__ fails on large integers.
60+
61+
This test checks the error handling path in scalar.c where PyObject_Str(py_int)
62+
returns NULL. We simulate this by subclassing int with a __str__ method
63+
that raises an exception.
64+
"""
65+
class BrokenInt(int):
66+
def __str__(self):
67+
raise RuntimeError("Intentionally broken __str__ method")
68+
69+
# Create an instance with a value that will overflow long long (> 2**63 - 1)
70+
# This triggers the string conversion path in quad_from_py_int
71+
broken_int = BrokenInt(2 ** 1024)
72+
73+
# When PyLong_AsLongLongAndOverflow returns overflow,
74+
# it tries to convert to string, which should fail and propagate the error
75+
with pytest.raises(RuntimeError, match="Intentionally broken __str__ method"):
76+
QuadPrecision(broken_int)
77+
78+
1979
class TestQuadPrecisionArrayCreation:
2080
"""Test suite for QuadPrecision array creation from sequences and arrays."""
2181

@@ -248,6 +308,68 @@ def test_string_roundtrip():
248308
)
249309

250310

311+
def test_string_subclass_parsing():
312+
"""Test that QuadPrecision handles string subclasses correctly.
313+
314+
This tests the PyUnicode_Check path in scalar.c lines 195-209,
315+
verifying that string subclasses work and that parsing errors
316+
are properly handled.
317+
"""
318+
class MyString(str):
319+
"""A custom string subclass"""
320+
pass
321+
322+
# Test valid string subclass - should parse correctly
323+
valid_str = MyString("3.14159265358979323846")
324+
result = QuadPrecision(valid_str)
325+
assert isinstance(result, QuadPrecision)
326+
expected = QuadPrecision("3.14159265358979323846")
327+
assert result == expected
328+
329+
# Test with scientific notation
330+
sci_str = MyString("1.23e-100")
331+
result = QuadPrecision(sci_str)
332+
assert isinstance(result, QuadPrecision)
333+
334+
# Test with negative value
335+
neg_str = MyString("-42.5")
336+
result = QuadPrecision(neg_str)
337+
assert float(result) == -42.5
338+
339+
# Test invalid string - should raise ValueError
340+
invalid_str = MyString("not a number")
341+
with pytest.raises(ValueError, match="Unable to parse string to QuadPrecision"):
342+
QuadPrecision(invalid_str)
343+
344+
# Test partially valid string (has trailing garbage)
345+
partial_str = MyString("3.14abc")
346+
with pytest.raises(ValueError, match="Unable to parse string to QuadPrecision"):
347+
QuadPrecision(partial_str)
348+
349+
# Test empty string
350+
empty_str = MyString("")
351+
with pytest.raises(ValueError, match="Unable to parse string to QuadPrecision"):
352+
QuadPrecision(empty_str)
353+
354+
# Test string with leading garbage
355+
leading_garbage = MyString("abc3.14")
356+
with pytest.raises(ValueError, match="Unable to parse string to QuadPrecision"):
357+
QuadPrecision(leading_garbage)
358+
359+
# Test special values
360+
inf_str = MyString("inf")
361+
result = QuadPrecision(inf_str)
362+
assert np.isinf(float(result))
363+
364+
neg_inf_str = MyString("-inf")
365+
result = QuadPrecision(neg_inf_str)
366+
assert np.isinf(float(result)) and float(result) < 0
367+
368+
nan_str = MyString("nan")
369+
result = QuadPrecision(nan_str)
370+
assert np.isnan(float(result))
371+
372+
251373
@pytest.mark.parametrize("name,expected", [("pi", np.pi), ("e", np.e), ("log2e", np.log2(np.e)), ("log10e", np.log10(np.e)), ("ln2", np.log(2.0)), ("ln10", np.log(10.0))])
252374
def test_math_constant(name, expected):
253375
assert isinstance(getattr(numpy_quaddtype, name), QuadPrecision)

0 commit comments

Comments
 (0)