Skip to content

Commit b16734a

Browse files
authored
Improve clr.accepts/returns (#1449)
* Correct types reported by clr.accepts/returns * Improve type checks in clr.accepts/returns * Normalize error message of clr.accepts * Fix failing test
1 parent 3db37aa commit b16734a

3 files changed

Lines changed: 121 additions & 47 deletions

File tree

Directory.Build.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@
117117
</PropertyGroup>
118118

119119
<ItemGroup>
120-
<Using Include="Microsoft.Scripting.Runtime.NotNullAttribute" Alias="NotNoneAttribute" />
120+
<Using Include="Microsoft.Scripting.Runtime.NotNullAttribute" Alias="NotNone" />
121121
</ItemGroup>
122122

123123
<!-- Release -->

Src/IronPython/Runtime/ClrModule.cs

Lines changed: 49 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
using System.Collections;
2020
using System.Collections.Generic;
2121
using System.IO;
22+
using System.Linq;
2223
using System.Reflection;
2324
using System.Runtime.CompilerServices;
2425
using System.Runtime.InteropServices;
@@ -592,7 +593,7 @@ public static PythonType StrongBox {
592593
/// </summary>
593594
/// <param name="types"></param>
594595
/// <returns></returns>
595-
public static object accepts(params object[] types) {
596+
public static object accepts([NotNone] params object[] types) {
596597
return new ArgChecker(types);
597598
}
598599

@@ -615,10 +616,10 @@ public static object Self() {
615616
/// Decorator for verifying the arguments to a function are of a specified type.
616617
/// </summary>
617618
public class ArgChecker {
618-
private readonly object[] expected;
619+
private readonly PythonType[] expected;
619620

620-
public ArgChecker(object[] prms) {
621-
expected = prms;
621+
public ArgChecker([NotNone] object[] types) {
622+
expected = types.Select(t => t.ToPythonType()).ToArray();
622623
}
623624

624625
#region ICallableWithCodeContext Members
@@ -638,16 +639,16 @@ public object Call(CodeContext context, object func) {
638639
/// then calls the original function.
639640
/// </summary>
640641
public class RuntimeArgChecker : PythonTypeSlot {
641-
private readonly object[] _expected;
642+
private readonly PythonType[] _expected;
642643
private readonly object _func;
643644
private readonly object _inst;
644645

645-
public RuntimeArgChecker(object function, object[] expectedArgs) {
646+
public RuntimeArgChecker([NotNone] object function, [NotNone] PythonType[] expectedArgs) {
646647
_expected = expectedArgs;
647648
_func = function;
648649
}
649650

650-
public RuntimeArgChecker(object instance, object function, object[] expectedArgs)
651+
public RuntimeArgChecker(object instance, [NotNone] object function, [NotNone] PythonType[] expectedArgs)
651652
: this(function, expectedArgs) {
652653
_inst = instance;
653654
}
@@ -661,11 +662,12 @@ private void ValidateArgs(object[] args) {
661662

662663
// no need to validate self... the method should handle it.
663664
for (int i = start; i < args.Length + start; i++) {
664-
PythonType dt = DynamicHelpers.GetPythonType(args[i - start]);
665-
666-
PythonType expct = _expected[i] as PythonType;
667-
if (dt != _expected[i] && !dt.IsSubclassOf(expct)) {
668-
throw PythonOps.AssertionError("argument {0} has bad value (got {1}, expected {2})", i, dt, _expected[i]);
665+
object arg = args[i - start];
666+
PythonType expct = _expected[i];
667+
if (!IsInstanceOf(arg, expct)) {
668+
throw PythonOps.AssertionError(
669+
"argument {0} has bad value (expected {1}, got {2})",
670+
i, expct.Name, PythonOps.GetPythonTypeName(arg));
669671
}
670672
}
671673
}
@@ -714,10 +716,10 @@ public object Call(CodeContext context, [ParamDictionary]IDictionary<object, obj
714716
/// Decorator for verifying the return type of functions.
715717
/// </summary>
716718
public class ReturnChecker {
717-
public object retType;
719+
public PythonType retType;
718720

719721
public ReturnChecker(object returnType) {
720-
retType = returnType;
722+
retType = returnType.ToPythonType();
721723
}
722724

723725
#region ICallableWithCodeContext Members
@@ -735,30 +737,25 @@ public object Call(CodeContext context, object func) {
735737
/// validates the return type is of a specified type.
736738
/// </summary>
737739
public class RuntimeReturnChecker : PythonTypeSlot {
738-
private readonly object _retType;
740+
private readonly PythonType _retType;
739741
private readonly object _func;
740742
private readonly object _inst;
741743

742-
public RuntimeReturnChecker(object function, object expectedReturn) {
744+
public RuntimeReturnChecker([NotNone] object function, [NotNone] PythonType expectedReturn) {
743745
_retType = expectedReturn;
744746
_func = function;
745747
}
746748

747-
public RuntimeReturnChecker(object instance, object function, object expectedReturn)
749+
public RuntimeReturnChecker(object instance, [NotNone] object function, [NotNone] PythonType expectedReturn)
748750
: this(function, expectedReturn) {
749751
_inst = instance;
750752
}
751753

752754
private void ValidateReturn(object ret) {
753-
// we return void...
754-
if (ret == null && _retType == null) return;
755-
756-
PythonType dt = DynamicHelpers.GetPythonType(ret);
757-
if (dt != _retType) {
758-
PythonType expct = _retType as PythonType;
759-
760-
if (!dt.IsSubclassOf(expct))
761-
throw PythonOps.AssertionError("bad return value returned (expected {0}, got {1})", _retType, dt);
755+
if (!IsInstanceOf(ret, _retType)) {
756+
throw PythonOps.AssertionError(
757+
"bad return value returned (expected {0}, got {1})",
758+
_retType.Name, PythonOps.GetPythonTypeName(ret));
762759
}
763760
}
764761

@@ -811,6 +808,32 @@ public object Call(CodeContext context, [ParamDictionary]IDictionary<object, obj
811808
#endregion
812809
}
813810

811+
private static PythonType ToPythonType(this object obj) {
812+
return obj switch {
813+
PythonType pt => pt,
814+
Type t => DynamicHelpers.GetPythonTypeFromType(t),
815+
null => TypeCache.Null,
816+
_ => throw PythonOps.TypeErrorForTypeMismatch("type", obj)
817+
};
818+
}
819+
820+
private static bool IsInstanceOf(object obj, PythonType pt) {
821+
// See also PythonOps.IsInstance
822+
var objType = DynamicHelpers.GetPythonType(obj);
823+
824+
if (objType == pt) {
825+
return true;
826+
}
827+
828+
// PEP 237: int/long unification
829+
// https://github.com/IronLanguages/ironpython3/issues/52
830+
if (pt == TypeCache.BigInteger && obj is int) {
831+
return true;
832+
}
833+
834+
return pt.__subclasscheck__(objType);
835+
}
836+
814837
/// <summary>
815838
/// returns the result of dir(o) as-if "import clr" has not been performed.
816839
/// </summary>

Tests/test_function.py

Lines changed: 71 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
import unittest
66

7-
from iptest import IronPythonTestCase, is_cli, is_mono, is_netcoreapp, is_posix, big, run_test, skipUnlessIronPython
7+
from iptest import IronPythonTestCase, is_cli, is_mono, is_netcoreapp, is_posix, big, clr_int_types, run_test, skipUnlessIronPython
88
from types import FunctionType, MethodType
99

1010
global init
@@ -560,19 +560,29 @@ def foo(x):
560560
return x
561561

562562
self.assertEqual(foo('abc'), 'abc')
563-
self.assertRaises(AssertionError, foo, 2)
564-
self.assertRaises(AssertionError, foo, big(2))
565-
self.assertRaises(AssertionError, foo, 2.0)
566-
self.assertRaises(AssertionError, foo, True)
563+
self.assertRaisesMessage(AssertionError, "argument 0 has bad value (expected str, got int)", foo, 2)
564+
self.assertRaisesMessage(AssertionError, "argument 0 has bad value (expected str, got int)", foo, big(2))
565+
self.assertRaisesMessage(AssertionError, "argument 0 has bad value (expected str, got float)", foo, 2.0)
566+
self.assertRaisesMessage(AssertionError, "argument 0 has bad value (expected str, got bool)", foo, True)
567+
568+
@clr.accepts(int)
569+
def foo(x):
570+
return x
571+
572+
self.assertEqual(foo(1), 1)
573+
self.assertEqual(foo(big(1)), 1)
574+
self.assertEqual(foo(True), True)
575+
self.assertRaisesMessage(AssertionError, "argument 0 has bad value (expected int, got str)", foo, 'abc')
576+
self.assertRaisesMessage(AssertionError, "argument 0 has bad value (expected int, got float)", foo, 2.0)
567577

568578
@clr.accepts(str, bool)
569579
def foo(x, y):
570580
return x, y
571581

572582
self.assertEqual(foo('abc', True), ('abc', True))
573-
self.assertRaises(AssertionError, foo, ('abc',2))
574-
self.assertRaises(AssertionError, foo, ('abc',big(2)))
575-
self.assertRaises(AssertionError, foo, ('abc',2.0))
583+
self.assertRaisesMessage(AssertionError, "argument 1 has bad value (expected bool, got int)", foo, 'abc', 2)
584+
self.assertRaisesMessage(AssertionError, "argument 1 has bad value (expected bool, got int)", foo, 'abc', big(2))
585+
self.assertRaisesMessage(AssertionError, "argument 1 has bad value (expected bool, got float)", foo, 'abc', 2.0)
576586

577587

578588
class bar:
@@ -583,21 +593,25 @@ def foo(self, x):
583593

584594
a = bar()
585595
self.assertEqual(a.foo('xyz'), 'xyz')
586-
self.assertRaises(AssertionError, a.foo, 2)
587-
self.assertRaises(AssertionError, a.foo, big(2))
588-
self.assertRaises(AssertionError, a.foo, 2.0)
589-
self.assertRaises(AssertionError, a.foo, True)
596+
self.assertRaisesMessage(AssertionError, "argument 1 has bad value (expected str, got int)", a.foo, 2)
597+
self.assertRaisesMessage(AssertionError, "argument 1 has bad value (expected str, got int)", a.foo, big(2))
598+
self.assertRaisesMessage(AssertionError, "argument 1 has bad value (expected str, got float)", a.foo, 2.0)
599+
self.assertRaisesMessage(AssertionError, "argument 1 has bad value (expected str, got bool)", a.foo, True)
590600

591601
@clr.returns(str)
592602
def foo(x):
593603
return x
594604

595-
596605
self.assertEqual(foo('abc'), 'abc')
597-
self.assertRaises(AssertionError, foo, 2)
598-
self.assertRaises(AssertionError, foo, big(2))
599-
self.assertRaises(AssertionError, foo, 2.0)
600-
self.assertRaises(AssertionError, foo, True)
606+
self.assertRaisesMessage(AssertionError, "bad return value returned (expected str, got int)", foo, 2)
607+
self.assertRaisesMessage(AssertionError, "bad return value returned (expected str, got int)", foo, big(2))
608+
self.assertRaisesMessage(AssertionError, "bad return value returned (expected str, got float)", foo, 2.0)
609+
self.assertRaisesMessage(AssertionError, "bad return value returned (expected str, got bool)", foo, True)
610+
611+
with self.assertRaisesMessage(TypeError, "expected type, got int"):
612+
@clr.accepts(0)
613+
def foo(x): pass
614+
601615

602616
@clr.accepts(bool)
603617
@clr.returns(str)
@@ -607,15 +621,52 @@ def foo(x):
607621

608622
self.assertEqual(foo(True), 'True')
609623

610-
self.assertRaises(AssertionError, foo, 2)
611-
self.assertRaises(AssertionError, foo, big(2))
612-
self.assertRaises(AssertionError, foo, False)
624+
self.assertRaisesMessage(AssertionError, "argument 0 has bad value (expected bool, got int)", foo, 2)
625+
self.assertRaisesMessage(AssertionError, "argument 0 has bad value (expected bool, got int)", foo, big(2))
626+
self.assertRaisesMessage(AssertionError, "bad return value returned (expected str, got int)", foo, False)
627+
613628

614629
@clr.returns(None)
615630
def foo(): pass
616631

617632
self.assertEqual(foo(), None)
618633

634+
@clr.returns(type(None))
635+
def foo(): pass
636+
637+
self.assertEqual(foo(), None)
638+
639+
@clr.returns(None)
640+
def foo():
641+
return 1
642+
643+
self.assertRaisesMessage(AssertionError, "bad return value returned (expected NoneType, got int)", foo)
644+
645+
with self.assertRaisesMessage(TypeError, "expected type, got int"):
646+
@clr.returns(0)
647+
def foo(): pass
648+
649+
import System
650+
for t in clr_int_types:
651+
@clr.accepts(t)
652+
def foo(x):
653+
return x
654+
655+
self.assertRaisesMessage(AssertionError, f"""argument 0 has bad value (expected {str(t).split("'")[1]}, got int)""", foo, big(0))
656+
if t != System.Int32:
657+
self.assertRaisesMessage(AssertionError, f"""argument 0 has bad value (expected {str(t).split("'")[1]}, got int)""", foo, 0)
658+
659+
@clr.accepts(System.IConvertible)
660+
@clr.returns(System.IConvertible)
661+
def foo(x):
662+
return x
663+
664+
self.assertEqual(foo(1), 1)
665+
self.assertEqual(foo(True), True)
666+
self.assertEqual(foo('abc'), 'abc')
667+
self.assertRaisesMessage(AssertionError, "argument 0 has bad value (expected IConvertible, got int)", foo, big(1))
668+
self.assertRaisesMessage(AssertionError, "argument 0 has bad value (expected IConvertible, got Guid)", foo, System.Guid.Empty)
669+
619670
def test_error_message(self):
620671
try:
621672
repr()

0 commit comments

Comments
 (0)