8000 Feature/named arg support (#953) · QuantConnect/pythonnet@17bc3f5 · GitHub
[go: up one dir, main page]

Skip to content

Commit 17bc3f5

Browse files
s4v4g3AlexCatarino
authored andcommitted
Feature/named arg support (pythonnet#953)
* Add support for named arguments (pythonnet#849) * Remove kwarg check since it breaks the python-derived CLR class use-case * Add named parameter test cases * Update changelog and authors * Add default params tests
1 parent 5f8d5dc commit 17bc3f5

File tree

5 files changed

+296
-19
lines changed

5 files changed

+296
-19
lines changed

AUTHORS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
- Jeff Reback ([@jreback](https://github.com/jreback))
3636
- Joe Frayne ([@jfrayne](https://github.com/jfrayne))
3737
- Joe Lidbetter ([@jmlidbetter](https://github.com/jmlidbetter))
38+
- Joe Savage ([@s4v4g3](https://github.com/s4v4g3))
3839
- John Burnett ([@johnburnett](https://github.com/johnburnett))
3940
- John Wilkes ([@jbw3](https://github.com/jbw3))
4041
- Luke Stratman ([@lstratman](https://github.com/lstratman))

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ This document follows the conventions laid out in [Keep a CHANGELOG][].
1818
- Removes PyLong_GetMax and PyClass_New when targetting Python3
1919
- Added support for converting python iterators to C# arrays
2020
- Changed usage of obselete function GetDelegateForFunctionPointer(IntPtr, Type) to GetDelegateForFunctionPointer<TDelegate>(IntPtr)
21+
- Added support for kwarg parameters when calling .NET methods from Python
2122

2223
### Fixed
2324

D7AE

src/runtime/methodbinder.cs

Lines changed: 99 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
using System.Collections;
33
using System.Reflection;
44
using System.Text;
5+
using System.Collections.Generic;
6+
using System.Linq;
57

68
namespace Python.Runtime
79
{
@@ -292,6 +294,22 @@ internal Binding Bind(IntPtr inst, IntPtr args, IntPtr kw, MethodBase info, Meth
292294
{
293295
// loop to find match, return invoker w/ or /wo error
294296
MethodBase[] _methods = null;
297+
298+
var kwargDict = new Dictionary<string, IntPtr>();
299+
if (kw != IntPtr.Zero)
300+
{
301+
var pynkwargs = (int)Runtime.PyDict_Size(kw);
302+
IntPtr keylist = Runtime.PyDict_Keys(kw);
303+
IntPtr valueList = Runtime.PyDict_Values(kw);
304+
for (int i = 0; i < pynkwargs; ++i)
305+
{
306+
var keyStr = Runtime.GetManagedString(Runtime.PyList_GetItem(keylist, i));
307+
kwargDict[keyStr] = Runtime.PyList_GetItem(valueList, i);
308+
}
309+
Runtime.XDecref(keylist);
310+
Runtime.XDecref(valueList);
311+
}
312+
295313
var pynargs = (int)Runtime.PyTuple_Size(args);
296314
var isGeneric = false;
297315
if (info != null)
@@ -315,11 +333,12 @@ internal Binding Bind(IntPtr inst, IntPtr args, IntPtr kw, MethodBase info, Meth
315333
ArrayList defaultArgList;
316334
bool paramsArray;
317335

318-
if (!MatchesArgumentCount(pynargs, pi, out paramsArray, out defaultArgList)) {
336+
if (!MatchesArgumentCount(pynargs, pi, kwargDict, out paramsArray, out defaultArgList))
337+
{
319338
continue;
320339
}
321340
var outs = 0;
322-
var margs = TryConvertArguments(pi, paramsArray, args, pynargs, defaultArgList,
341+
var margs = TryConvertArguments(pi, paramsArray, args, pynargs, kwargDict, defaultArgList,
323342
needsResolution: _methods.Length > 1,
324343
outs: out outs);
325344

@@ -363,19 +382,21 @@ internal Binding Bind(IntPtr inst, IntPtr args, IntPtr kw, MethodBase info, Meth
363382
}
364383

365384
/// <summary>
366-
/// Attempts to convert Python argument tuple into an array of managed objects,
367-
/// that can be passed to a method.
385+
/// Attempts to convert Python positional argument tuple and keyword argument table
386+
/// into an array of managed objects, that can be passed to a method.
368387
/// </summary>
369388
/// <param name="pi">Information about expected parameters</param>
370389
/// <param name="paramsArray"><c>true</c>, if the last parameter is a params array.</param>
371390
/// <param name="args">A pointer to the Python argument tuple</param>
372391
/// <param name="pyArgCount">Number of arguments, passed by Python</param>
392+
/// <param name="kwargDict">Dictionary of keyword argument name to python object pointer</param>
373393
/// <param name="defaultArgList">A list of default values for omitted parameters</param>
374394
/// <param name="needsResolution"><c>true</c>, if overloading resolution is required</param>
375395
/// <param name="outs">Returns number of output parameters</param>
376396
/// <returns>An array of .NET arguments, that can be passed to a method.</returns>
377397
static object[] TryConvertArguments(ParameterInfo[] pi, bool paramsArray,
378398
IntPtr args, int pyArgCount,
399+
Dictionary<string, IntPtr> kwargDict,
379400
ArrayList defaultArgList,
380401
bool needsResolution,
381402
out int outs)
@@ -386,7 +407,10 @@ static object[] TryConvertArguments(ParameterInfo[] pi, bool paramsArray,
386407

387408
for (int paramIndex = 0; paramIndex < pi.Length; paramIndex++)
388409
{
389-
if (paramIndex >= pyArgCount)
410+
var parameter = pi[paramIndex];
411+
bool hasNamedParam = kwargDict.ContainsKey(parameter.Name);
412+
413+
if (paramIndex >= pyArgCount && !hasNamedParam)
390414
{
391415
if (defaultArgList != null)
392416
{
@@ -396,12 +420,19 @@ static object[] TryConvertArguments(ParameterInfo[] pi, bool paramsArray,
396420
continue;
397421
}
398422

399-
var parameter = pi[paramIndex];
400-
IntPtr op = (arrayStart == paramIndex)
401-
// map remaining Python arguments to a tuple since
402-
// the managed function accepts it - hopefully :]
403-
? Runtime.PyTuple_GetSlice(args, arrayStart, pyArgCount)
404-
: Runtime.PyTuple_GetItem(args, paramIndex);
423+
IntPtr op;
424+
if (hasNamedParam)
425+
{
426+
op = kwargDict[parameter.Name];
427+
}
428+
else
429+
{
430+
op = (arrayStart == paramIndex)
431+
// map remaining Python arguments to a tuple since
432+
// the managed function accepts it - hopefully :]
433+
? Runtime.PyTuple_GetSlice(args, arrayStart, pyArgCount)
434+
: Runtime.PyTuple_GetItem(args, paramIndex);
435+
}
405436

406437
bool isOut;
407438
if (!TryConvertArgument(op, parameter.ParameterType, needsResolution, out margs[paramIndex], out isOut))
@@ -537,29 +568,49 @@ static Type TryComputeClrArgumentType(Type parameterType, IntPtr argument, bool
537568
return clrtype;
538569
}
539570

540-
static bool MatchesArgumentCount(int argumentCount, ParameterInfo[] parameters,
571+
static bool MatchesArgumentCount(int positionalArgumentCount, ParameterInfo[] parameters,
572+
Dictionary<string, IntPtr> kwargDict,
541573
out bool paramsArray,
542574
out ArrayList defaultArgList)
543575
{
544576
defaultArgList = null;
545577
var match = false;
546578
paramsArray = false;
547579

548-
if (argumentCount == parameters.Length)
580+
if (positionalArgumentCount == parameters.Length)
549581
{
550582
match = true;
551-
} else if (argumentCount < parameters.Length)
583+
}
584+
else if (positionalArgumentCount < parameters.Length)
552585
{
586+
// every parameter past 'positionalArgumentCount' must have either
587+
// a corresponding keyword argument or a default parameter
553588
match = true;
554589
defaultArgList = new ArrayList();
555-
for (var v = argumentCount; v < parameters.Length; v++) {
556-
if (parameters[v].DefaultValue == DBNull.Value) {
590+
for (var v = positionalArgumentCount; v < parameters.Length; v++)
591+
{
592+
if (kwargDict.ContainsKey(parameters[v].Name))
593+
{
594+
// we have a keyword argument for this parameter,
595+
// no need to check for a default parameter, but put a null
596+
// placeholder in defaultArgList
597+
defaultArgList.Add(null);
598+
}
599+
else if (parameters[v].IsOptional)
600+
{
601+
// IsOptional will be true if the parameter has a default value,
602+
// or if the parameter has the [Optional] attribute specified.
603+
// The GetDefaultValue() extension method will return the value
604+
// to be passed in as the parameter value
605+
defaultArgList.Add(parameters[v].GetDefaultValue());
606+
}
607+
else
608+
{
557609
match = false;
558-
} else {
559-
defaultArgList.Add(parameters[v].DefaultValue);
560610
}
561611
}
562-
} else if (argumentCount > parameters.Length && parameters.Length > 0 &&
612+
}
613+
else if (positionalArgumentCount > parameters.Length && parameters.Length > 0 &&
563614
Attribute.IsDefined(parameters[parameters.Length - 1], typeof(ParamArrayAttribute)))
564615
{
565616
// This is a `foo(params object[] bar)` style method
@@ -754,4 +805,33 @@ internal Binding(MethodBase info, object inst, object[] args, int outs)
754805
this.outs = outs;
755806
}
756807
}
808+
809+
810+
static internal class ParameterInfoExtensions
811+
{
812+
public static object GetDefaultValue(this ParameterInfo parameterInfo)
813+
{
814+
// parameterInfo.HasDefaultValue is preferable but doesn't exist in .NET 4.0
815+
bool hasDefaultValue = (parameterInfo.Attributes & ParameterAttributes.HasDefault) ==
816+
ParameterAttributes.HasDefault;
817+
818+
if (hasDefaultValue)
819+
{
820+
return parameterInfo.DefaultValue;
821+
}
822+
else
823+
{
824+
// [OptionalAttribute] was specified for the parameter.
825+
// See https://stackoverflow.com/questions/3416216/optionalattribute-parameters-default-value
826+
// for rules on determining the value to pass to the parameter
827+
var type = parameterInfo.ParameterType;
828+
if (type == typeof(object))
829+
return Type.Missing;
830+
else if (type.IsValueType)
831+
return Activator.CreateInstance(type);
832+
else
833+
return null;
834+
}
835+
}
836+
}
757837
}

src/testing/methodtest.cs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using System.IO;
3+
using System.Runtime.InteropServices;
34

45
namespace Python.Test
56
{
@@ -651,6 +652,38 @@ public static string Casesensitive()
651652
{
652653
return "Casesensitive";
653654
}
655+
656+
public static string DefaultParams(int a=0, int b=0, int c=0, int d=0)
657+
{
658+
return string.Format("{0}{1}{2}{3}", a, b, c, d);
659+
}
660+
661+
public static string OptionalParams([Optional]int a, [Optional]int b, [Optional]int c, [Optional] int d)
662+
{
663+
return string.Format("{0}{1}{2}{3}", a, b, c, d);
664+
}
665+
666+
public static bool OptionalParams_TestMissing([Optional]object a)
667+
{
668+
return a == Type.Missing;
669+
}
670+
671+
public static bool OptionalParams_TestReferenceType([Optional]string a)
672+
{
673+
return a == null;
674+
}
675+
676+
public static string OptionalAndDefaultParams([Optional]int a, [Optional]int b, int c=0, int d=0)
677+
{
678+
return string.Format("{0}{1}{2}{3}", a, b, c, d);
679+
}
680+
681+
public static string OptionalAndDefaultParams2([Optional]int a, [Optional]int b, [Optional, DefaultParameterValue(1)]int c, int d = 2)
682+
{
683+
return string.Format("{0}{1}{2}{3}", a, b, c, d);
684+
}
685+
686+
654687
}
655688

656689

0 commit comments

Comments
 (0)
0