-
-
Notifications
You must be signed in to change notification settings - Fork 32.2k
gh-111495: Add tests for PyNumber C API #111996
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
38 commits
Select commit
Hold shift + click to select a range
cbca658
gh-111495: Add tests for PyNumber C API
skirpichev 0d95d57
Remove inaccessible code (PyLong_AsSsize_t raises OverflowError)
skirpichev 7b8adb4
Drop checks for broken float subclasses (like for PyNumber_Long in 31…
skirpichev 4665f25
+ tests with sets for intersection/union/etc
skirpichev 9e14905
Use macroses
skirpichev c0f4051
Merge branch 'main' into capi-number-tests
skirpichev 3a7a4c4
Merge branch 'main' into capi-number-tests
skirpichev 788e9c2
Ternary ops (currently only pow/ipow) don't use __r*__ dunders
skirpichev 8cf4429
More tests
skirpichev 91c68a7
Merge branch 'main' into capi-number-tests
skirpichev 9f6fd15
More tests
skirpichev 9f5b7a1
Use BINARY_FUNC macro for some remaining ops
skirpichev 2675738
Add UNARY_FUNC macro to define unary PyNumber_* functions
skirpichev 2bc6bc7
Fix typo
skirpichev 49d673b
Make last argument optional for PyNumber_Power/InPlacePower
skirpichev b2dda89
More tests
skirpichev b86d2eb
Remove number_check() from _testcapi/abstract.c, move tests
skirpichev 0fbbba9
address review:
skirpichev bcf0c20
Merge branch 'main' into capi-number-tests
skirpichev 9aae51b
some cleanup for support classes
skirpichev d052280
+ cleanup
skirpichev c04679c
use instead generic tests for unary functions
skirpichev efe4aab
+1
skirpichev 4d96d35
use instead generic tests for binary functions + misc tests
skirpichev b00365e
Merge branch 'main' into capi-number-tests
skirpichev 74abc5d
+1
skirpichev d62b802
Merge branch 'main' into capi-number-tests
skirpichev 7ba5bb0
Merge branch 'master' into capi-number-tests
skirpichev 76aac6f
Merge branch 'master' into capi-number-tests
skirpichev de8f7f3
Include only basic tests for unary/binary ops (with builtin types)
skirpichev bd3435b
Merge branch 'master' into capi-number-tests
skirpichev ab08430
address review: sort imports
skirpichev 0bccf1c
address review: split test_misc()
skirpichev abde55b
Update Lib/test/test_capi/test_number.py
skirpichev f4be9c5
address review: use import_helper for _testbuffer
skirpichev 86df16c
fix typo
skirpichev c620c72
drop support classes with __trunc__ dunder
skirpichev 556b23b
address review: skip two test if no ndarray
skirpichev File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,335 @@ | ||
import itertools | ||
import operator | ||
import sys | ||
import unittest | ||
import warnings | ||
|
||
from test.support import cpython_only, import_helper | ||
|
||
_testcapi = import_helper.import_module('_testcapi') | ||
from _testcapi import PY_SSIZE_T_MAX, PY_SSIZE_T_MIN | ||
|
||
try: | ||
from _testbuffer import ndarray | ||
except ImportError: | ||
ndarray = None | ||
|
||
NULL = None | ||
|
||
class BadDescr: | ||
def __get__(self, obj, objtype=None): | ||
raise RuntimeError | ||
|
||
class WithDunder: | ||
def _meth(self, *args): | ||
if self.val: | ||
return self.val | ||
if self.exc: | ||
raise self.exc | ||
@classmethod | ||
def with_val(cls, val): | ||
obj = super().__new__(cls) | ||
obj.val = val | ||
obj.exc = None | ||
setattr(cls, cls.methname, cls._meth) | ||
return obj | ||
|
||
@classmethod | ||
def with_exc(cls, exc): | ||
obj = super().__new__(cls) | ||
obj.val = None | ||
obj.exc = exc | ||
setattr(cls, cls.methname, cls._meth) | ||
return obj | ||
|
||
class HasBadAttr: | ||
def __new__(cls): | ||
obj = super().__new__(cls) | ||
setattr(cls, cls.methname, BadDescr()) | ||
return obj | ||
|
||
|
||
class IndexLike(WithDunder): | ||
methname = '__index__' | ||
|
||
class IntLike(WithDunder): | ||
methname = '__int__' | ||
|
||
class FloatLike(WithDunder): | ||
methname = '__float__' | ||
|
||
|
||
def subclassof(base): | ||
return type(base.__name__ + 'Subclass', (base,), {}) | ||
|
||
|
||
class SomeError(Exception): | ||
pass | ||
|
||
class OtherError(Exception): | ||
pass | ||
|
||
|
||
class CAPITest(unittest.TestCase): | ||
def test_check(self): | ||
# Test PyNumber_Check() | ||
check = _testcapi.number_check | ||
|
||
self.assertTrue(check(1)) | ||
self.assertTrue(check(IndexLike.with_val(1))) | ||
self.assertTrue(check(IntLike.with_val(99))) | ||
self.assertTrue(check(0.5)) | ||
self.assertTrue(check(FloatLike.with_val(4.25))) | ||
self.assertTrue(check(1+2j)) | ||
|
||
self.assertFalse(check([])) | ||
self.assertFalse(check("abc")) | ||
self.assertFalse(check(object())) | ||
self.assertFalse(check(NULL)) | ||
|
||
def test_unary_ops(self): | ||
methmap = {'__neg__': _testcapi.number_negative, # PyNumber_Negative() | ||
'__pos__': _testcapi.number_positive, # PyNumber_Positive() | ||
'__abs__': _testcapi.number_absolute, # PyNumber_Absolute() | ||
'__invert__': _testcapi.number_invert} # PyNumber_Invert() | ||
|
||
for name, func in methmap.items(): | ||
# Generic object, has no tp_as_number structure | ||
self.assertRaises(TypeError, func, object()) | ||
|
||
# C-API function accepts NULL | ||
self.assertRaises(SystemError, func, NULL) | ||
|
||
# Behave as corresponding unary operation | ||
op = getattr(operator, name) | ||
for x in [0, 42, -1, 3.14, 1+2j]: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you move There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmm, I would prefer this be more local. |
||
try: | ||
op(x) | ||
except TypeError: | ||
self.assertRaises(TypeError, func, x) | ||
else: | ||
self.assertEqual(func(x), op(x)) | ||
|
||
def test_binary_ops(self): | ||
methmap = {'__add__': _testcapi.number_add, # PyNumber_Add() | ||
'__sub__': _testcapi.number_subtract, # PyNumber_Subtract() | ||
'__mul__': _testcapi.number_multiply, # PyNumber_Multiply() | ||
'__matmul__': _testcapi.number_matrixmultiply, # PyNumber_MatrixMultiply() | ||
'__floordiv__': _testcapi.number_floordivide, # PyNumber_FloorDivide() | ||
'__truediv__': _testcapi.number_truedivide, # PyNumber_TrueDivide() | ||
'__mod__': _testcapi.number_remainder, # PyNumber_Remainder() | ||
'__divmod__': _testcapi.number_divmod, # PyNumber_Divmod() | ||
'__lshift__': _testcapi.number_lshift, # PyNumber_Lshift() | ||
'__rshift__': _testcapi.number_rshift, # PyNumber_Rshift() | ||
'__and__': _testcapi.number_and, # PyNumber_And() | ||
'__xor__': _testcapi.number_xor, # PyNumber_Xor() | ||
'__or__': _testcapi.number_or, # PyNumber_Or() | ||
'__pow__': _testcapi.number_power, # PyNumber_Power() | ||
'__iadd__': _testcapi.number_inplaceadd, # PyNumber_InPlaceAdd() | ||
'__isub__': _testcapi.number_inplacesubtract, # PyNumber_InPlaceSubtract() | ||
'__imul__': _testcapi.number_inplacemultiply, # PyNumber_InPlaceMultiply() | ||
'__imatmul__': _testcapi.number_inplacematrixmultiply, # PyNumber_InPlaceMatrixMultiply() | ||
'__ifloordiv__': _testcapi.number_inplacefloordivide, # PyNumber_InPlaceFloorDivide() | ||
'__itruediv__': _testcapi.number_inplacetruedivide, # PyNumber_InPlaceTrueDivide() | ||
'__imod__': _testcapi.number_inplaceremainder, # PyNumber_InPlaceRemainder() | ||
'__ilshift__': _testcapi.number_inplacelshift, # PyNumber_InPlaceLshift() | ||
'__irshift__': _testcapi.number_inplacershift, # PyNumber_InPlaceRshift() | ||
'__iand__': _testcapi.number_inplaceand, # PyNumber_InPlaceAnd() | ||
'__ixor__': _testcapi.number_inplacexor, # PyNumber_InPlaceXor() | ||
'__ior__': _testcapi.number_inplaceor, # PyNumber_InPlaceOr() | ||
'__ipow__': _testcapi.number_inplacepower, # PyNumber_InPlacePower() | ||
} | ||
|
||
for name, func in methmap.items(): | ||
cases = [0, 42, 3.14, -1, 123, 1+2j] | ||
|
||
# Generic object, has no tp_as_number structure | ||
for x in cases: | ||
self.assertRaises(TypeError, func, object(), x) | ||
self.assertRaises(TypeError, func, x, object()) | ||
|
||
# Behave as corresponding binary operation | ||
op = getattr(operator, name, divmod) | ||
for x, y in itertools.combinations(cases, 2): | ||
try: | ||
op(x, y) | ||
except (TypeError, ValueError, ZeroDivisionError) as exc: | ||
self.assertRaises(exc.__class__, func, x, y) | ||
else: | ||
self.assertEqual(func(x, y), op(x, y)) | ||
|
||
# CRASHES func(NULL, object()) | ||
# CRASHES func(object(), NULL) | ||
|
||
@unittest.skipIf(ndarray is None, "needs _testbuffer") | ||
def test_misc_add(self): | ||
# PyNumber_Add(), PyNumber_InPlaceAdd() | ||
add = _testcapi.number_add | ||
inplaceadd = _testcapi.number_inplaceadd | ||
|
||
# test sq_concat/sq_inplace_concat slots | ||
a, b, r = [1, 2], [3, 4], [1, 2, 3, 4] | ||
self.assertEqual(add(a, b), r) | ||
self.assertEqual(a, [1, 2]) | ||
self.assertRaises(TypeError, add, ndarray([1], (1,)), 2) | ||
a, b, r = [1, 2], [3, 4], [1, 2, 3, 4] | ||
self.assertEqual(inplaceadd(a, b), r) | ||
self.assertEqual(a, r) | ||
self.assertRaises(TypeError, inplaceadd, ndarray([1], (1,)), 2) | ||
|
||
@unittest.skipIf(ndarray is None, "needs _testbuffer") | ||
def test_misc_multiply(self): | ||
# PyNumber_Multiply(), PyNumber_InPlaceMultiply() | ||
multiply = _testcapi.number_multiply | ||
inplacemultiply = _testcapi.number_inplacemultiply | ||
|
||
# test sq_repeat/sq_inplace_repeat slots | ||
a, b, r = [1], 2, [1, 1] | ||
self.assertEqual(multiply(a, b), r) | ||
self.assertEqual((a, b), ([1], 2)) | ||
self.assertEqual(multiply(b, a), r) | ||
self.assertEqual((a, b), ([1], 2)) | ||
self.assertEqual(multiply([1], -1), []) | ||
self.assertRaises(TypeError, multiply, ndarray([1], (1,)), 2) | ||
self.assertRaises(TypeError, multiply, [1], 0.5) | ||
self.assertRaises(OverflowError, multiply, [1], PY_SSIZE_T_MAX + 1) | ||
skirpichev marked this conversation as resolved.
Show resolved
Hide resolved
|
||
self.assertRaises(MemoryError, multiply, [1, 2], PY_SSIZE_T_MAX//2 + 1) | ||
a, b, r = [1], 2, [1, 1] | ||
self.assertEqual(inplacemultiply(a, b), r) | ||
self.assertEqual((a, b), (r, 2)) | ||
a = [1] | ||
self.assertEqual(inplacemultiply(b, a), r) | ||
self.assertEqual((a, b), ([1], 2)) | ||
self.assertRaises(TypeError, inplacemultiply, ndarray([1], (1,)), 2) | ||
self.assertRaises(OverflowError, inplacemultiply, [1], PY_SSIZE_T_MAX + 1) | ||
self.assertRaises(MemoryError, inplacemultiply, [1, 2], PY_SSIZE_T_MAX//2 + 1) | ||
|
||
def test_misc_power(self): | ||
# PyNumber_Power() | ||
power = _testcapi.number_power | ||
|
||
class HasPow(WithDunder): | ||
methname = '__pow__' | ||
|
||
# ternary op | ||
self.assertEqual(power(4, 11, 5), pow(4, 11, 5)) | ||
self.assertRaises(TypeError, power, 4, 11, 1.25) | ||
self.assertRaises(TypeError, power, 4, 11, HasPow.with_val(NotImplemented)) | ||
self.assertRaises(TypeError, power, 4, 11, object()) | ||
|
||
@cpython_only | ||
def test_rshift_print(self): | ||
# This tests correct syntax hint for py2 redirection (>>). | ||
rshift = _testcapi.number_rshift | ||
|
||
with self.assertRaises(TypeError) as context: | ||
skirpichev marked this conversation as resolved.
Show resolved
Hide resolved
|
||
rshift(print, 42) | ||
self.assertIn('Did you mean "print(<message>, ' | ||
'file=<output_stream>)"?', str(context.exception)) | ||
with self.assertRaises(TypeError) as context: | ||
rshift(max, sys.stderr) | ||
self.assertNotIn('Did you mean ', str(context.exception)) | ||
with self.assertRaises(TypeError) as context: | ||
rshift(1, "spam") | ||
|
||
def test_long(self): | ||
# Test PyNumber_Long() | ||
long = _testcapi.number_long | ||
|
||
self.assertEqual(long(42), 42) | ||
self.assertEqual(long(1.25), 1) | ||
self.assertEqual(long("42"), 42) | ||
self.assertEqual(long(b"42"), 42) | ||
self.assertEqual(long(bytearray(b"42")), 42) | ||
self.assertEqual(long(memoryview(b"42")), 42) | ||
self.assertEqual(long(IndexLike.with_val(99)), 99) | ||
self.assertEqual(long(IntLike.with_val(99)), 99) | ||
|
||
self.assertRaises(TypeError, long, IntLike.with_val(1.0)) | ||
with warnings.catch_warnings(): | ||
warnings.simplefilter("error", DeprecationWarning) | ||
self.assertRaises(DeprecationWarning, long, IntLike.with_val(True)) | ||
with self.assertWarns(DeprecationWarning): | ||
self.assertEqual(long(IntLike.with_val(True)), 1) | ||
self.assertRaises(RuntimeError, long, IntLike.with_exc(RuntimeError)) | ||
|
||
self.assertRaises(TypeError, long, 1j) | ||
self.assertRaises(TypeError, long, object()) | ||
self.assertRaises(SystemError, long, NULL) | ||
|
||
def test_float(self): | ||
# Test PyNumber_Float() | ||
float_ = _testcapi.number_float | ||
|
||
self.assertEqual(float_(1.25), 1.25) | ||
self.assertEqual(float_(123), 123.) | ||
self.assertEqual(float_("1.25"), 1.25) | ||
|
||
self.assertEqual(float_(FloatLike.with_val(4.25)), 4.25) | ||
self.assertEqual(float_(IndexLike.with_val(99)), 99.0) | ||
self.assertEqual(float_(IndexLike.with_val(-1)), -1.0) | ||
|
||
self.assertRaises(TypeError, float_, FloatLike.with_val(687)) | ||
with warnings.catch_warnings(): | ||
warnings.simplefilter("error", DeprecationWarning) | ||
self.assertRaises(DeprecationWarning, float_, FloatLike.with_val(subclassof(float)(4.25))) | ||
with self.assertWarns(DeprecationWarning): | ||
self.assertEqual(float_(FloatLike.with_val(subclassof(float)(4.25))), 4.25) | ||
self.assertRaises(RuntimeError, float_, FloatLike.with_exc(RuntimeError)) | ||
|
||
self.assertRaises(TypeError, float_, IndexLike.with_val(1.25)) | ||
self.assertRaises(OverflowError, float_, IndexLike.with_val(2**2000)) | ||
|
||
self.assertRaises(TypeError, float_, 1j) | ||
self.assertRaises(TypeError, float_, object()) | ||
skirpichev marked this conversation as resolved.
Show resolved
Hide resolved
|
||
self.assertRaises(SystemError, float_, NULL) | ||
|
||
def test_index(self): | ||
# Test PyNumber_Index() | ||
index = _testcapi.number_index | ||
|
||
self.assertEqual(index(11), 11) | ||
|
||
with warnings.catch_warnings(): | ||
warnings.simplefilter("error", DeprecationWarning) | ||
self.assertRaises(DeprecationWarning, index, IndexLike.with_val(True)) | ||
with self.assertWarns(DeprecationWarning): | ||
self.assertEqual(index(IndexLike.with_val(True)), 1) | ||
self.assertRaises(TypeError, index, IndexLike.with_val(1.0)) | ||
self.assertRaises(RuntimeError, index, IndexLike.with_exc(RuntimeError)) | ||
|
||
self.assertRaises(TypeError, index, 1.25) | ||
self.assertRaises(TypeError, index, "42") | ||
self.assertRaises(TypeError, index, object()) | ||
skirpichev marked this conversation as resolved.
Show resolved
Hide resolved
|
||
self.assertRaises(SystemError, index, NULL) | ||
|
||
def test_tobase(self): | ||
# Test PyNumber_ToBase() | ||
tobase = _testcapi.number_tobase | ||
|
||
self.assertEqual(tobase(10, 2), bin(10)) | ||
self.assertEqual(tobase(11, 8), oct(11)) | ||
self.assertEqual(tobase(16, 10), str(16)) | ||
self.assertEqual(tobase(13, 16), hex(13)) | ||
|
||
self.assertRaises(SystemError, tobase, NULL, 2) | ||
skirpichev marked this conversation as resolved.
Show resolved
Hide resolved
|
||
self.assertRaises(SystemError, tobase, 2, 3) | ||
self.assertRaises(TypeError, tobase, 1.25, 2) | ||
self.assertRaises(TypeError, tobase, "42", 2) | ||
|
||
def test_asssizet(self): | ||
# Test PyNumber_AsSsize_t() | ||
asssizet = _testcapi.number_asssizet | ||
|
||
for n in [*range(-6, 7), PY_SSIZE_T_MIN, PY_SSIZE_T_MAX]: | ||
self.assertEqual(asssizet(n, OverflowError), n) | ||
self.assertEqual(asssizet(PY_SSIZE_T_MAX+10, NULL), PY_SSIZE_T_MAX) | ||
self.assertEqual(asssizet(PY_SSIZE_T_MIN-10, NULL), PY_SSIZE_T_MIN) | ||
|
||
self.assertRaises(OverflowError, asssizet, PY_SSIZE_T_MAX + 10, OverflowError) | ||
self.assertRaises(RuntimeError, asssizet, PY_SSIZE_T_MAX + 10, RuntimeError) | ||
self.assertRaises(SystemError, asssizet, NULL, TypeError) | ||
|
||
|
||
if __name__ == "__main__": | ||
unittest.main() |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.