Skip to content

Commit 1bcbeb5

Browse files
s4v4g3filmor
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 f1da55e commit 1bcbeb5

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

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
{
@@ -280,6 +282,22 @@ internal Binding Bind(IntPtr inst, IntPtr args, IntPtr kw, MethodBase info, Meth
280282
{
281283
// loop to find match, return invoker w/ or /wo error
282284
MethodBase[] _methods = null;
285+
286+
var kwargDict = new Dictionary<string, IntPtr>();
287+
if (kw != IntPtr.Zero)
288+
{
289+
var pynkwargs = (int)Runtime.PyDict_Size(kw);
290+
IntPtr keylist = Runtime.PyDict_Keys(kw);
291+
IntPtr valueList = Runtime.PyDict_Values(kw);
292+
for (int i = 0; i < pynkwargs; ++i)
293+
{
294+
var keyStr = Runtime.GetManagedString(Runtime.PyList_GetItem(keylist, i));
295+
kwargDict[keyStr] = Runtime.PyList_GetItem(valueList, i);
296+
}
297+
Runtime.XDecref(keylist);
298+
Runtime.XDecref(valueList);
299+
}
300+
283301
var pynargs = (int)Runtime.PyTuple_Size(args);
284302
var isGeneric = false;
285303
if (info != null)
@@ -303,11 +321,12 @@ internal Binding Bind(IntPtr inst, IntPtr args, IntPtr kw, MethodBase info, Meth
303321
ArrayList defaultArgList;
304322
bool paramsArray;
305323

306-
if (!MatchesArgumentCount(pynargs, pi, out paramsArray, out defaultArgList)) {
324+
if (!MatchesArgumentCount(pynargs, pi, kwargDict, out paramsArray, out defaultArgList))
325+
{
307326
continue;
308327
}
309328
var outs = 0;
310-
var margs = TryConvertArguments(pi, paramsArray, args, pynargs, defaultArgList,
329+
var margs = TryConvertArguments(pi, paramsArray, args, pynargs, kwargDict, defaultArgList,
311330
needsResolution: _methods.Length > 1,
312331
outs: out outs);
313332

@@ -351,19 +370,21 @@ internal Binding Bind(IntPtr inst, IntPtr args, IntPtr kw, MethodBase info, Meth
351370
}
352371

353372
/// <summary>
354-
/// Attempts to convert Python argument tuple into an array of managed objects,
355-
/// that can be passed to a method.
373+
/// Attempts to convert Python positional argument tuple and keyword argument table
374+
/// into an array of managed objects, that can be passed to a method.
356375
/// </summary>
357376
/// <param name="pi">Information about expected parameters</param>
358377
/// <param name="paramsArray"><c>true</c>, if the last parameter is a params array.</param>
359378
/// <param name="args">A pointer to the Python argument tuple</param>
360379
/// <param name="pyArgCount">Number of arguments, passed by Python</param>
380+
/// <param name="kwargDict">Dictionary of keyword argument name to python object pointer</param>
361381
/// <param name="defaultArgList">A list of default values for omitted parameters</param>
362382
/// <param name="needsResolution"><c>true</c>, if overloading resolution is required</param>
363383
/// <param name="outs">Returns number of output parameters</param>
364384
/// <returns>An array of .NET arguments, that can be passed to a method.</returns>
365385
static object[] TryConvertArguments(ParameterInfo[] pi, bool paramsArray,
366386
IntPtr args, int pyArgCount,
387+
Dictionary<string, IntPtr> kwargDict,
367388
ArrayList defaultArgList,
368389
bool needsResolution,
369390
out int outs)
@@ -374,7 +395,10 @@ static object[] TryConvertArguments(ParameterInfo[] pi, bool paramsArray,
374395

375396
for (int paramIndex = 0; paramIndex < pi.Length; paramIndex++)
376397
{
377-
if (paramIndex >= pyArgCount)
398+
var parameter = pi[paramIndex];
399+
bool hasNamedParam = kwargDict.ContainsKey(parameter.Name);
400+
401+
if (paramIndex >= pyArgCount && !hasNamedParam)
378402
{
379403
if (defaultArgList != null)
380404
{
@@ -384,12 +408,19 @@ static object[] TryConvertArguments(ParameterInfo[] pi, bool paramsArray,
384408
continue;
385409
}
386410

387-
var parameter = pi[paramIndex];
388-
IntPtr op = (arrayStart == paramIndex)
389-
// map remaining Python arguments to a tuple since
390-
// the managed function accepts it - hopefully :]
391-
? Runtime.PyTuple_GetSlice(args, arrayStart, pyArgCount)
392-
: Runtime.PyTuple_GetItem(args, paramIndex);
411+
IntPtr op;
412+
if (hasNamedParam)
413+
{
414+
op = kwargDict[parameter.Name];
415+
}
416+
else
417+
{
418+
op = (arrayStart == paramIndex)
419+
// map remaining Python arguments to a tuple since
420+
// the managed function accepts it - hopefully :]
421+
? Runtime.PyTuple_GetSlice(args, arrayStart, pyArgCount)
422+
: Runtime.PyTuple_GetItem(args, paramIndex);
423+
}
393424

394425
bool isOut;
395426
if (!TryConvertArgument(op, parameter.ParameterType, needsResolution, out margs[paramIndex], out isOut))
@@ -505,29 +536,49 @@ static Type TryComputeClrArgumentType(Type parameterType, IntPtr argument, bool
505536
return clrtype;
506537
}
507538

508-
static bool MatchesArgumentCount(int argumentCount, ParameterInfo[] parameters,
539+
static bool MatchesArgumentCount(int positionalArgumentCount, ParameterInfo[] parameters,
540+
Dictionary<string, IntPtr> kwargDict,
509541
out bool paramsArray,
510542
out ArrayList defaultArgList)
511543
{
512544
defaultArgList = null;
513545
var match = false;
514546
paramsArray = false;
515547

516-
if (argumentCount == parameters.Length)
548+
if (positionalArgumentCount == parameters.Length)
517549
{
518550
match = true;
519-
} else if (argumentCount < parameters.Length)
551+
}
552+
else if (positionalArgumentCount < parameters.Length)
520553
{
554+
// every parameter past 'positionalArgumentCount' must have either
555+
// a corresponding keyword argument or a default parameter
521556
match = true;
522557
defaultArgList = new ArrayList();
523-
for (var v = argumentCount; v < parameters.Length; v++) {
524-
if (parameters[v].DefaultValue == DBNull.Value) {
558+
for (var v = positionalArgumentCount; v < parameters.Length; v++)
559+
{
560+
if (kwargDict.ContainsKey(parameters[v].Name))
561+
{
562+
// we have a keyword argument for this parameter,
563+
// no need to check for a default parameter, but put a null
564+
// placeholder in defaultArgList
565+
defaultArgList.Add(null);
566+
}
567+
else if (parameters[v].IsOptional)
568+
{
569+
// IsOptional will be true if the parameter has a default value,
570+
// or if the parameter has the [Optional] attribute specified.
571+
// The GetDefaultValue() extension method will return the value
572+
// to be passed in as the parameter value
573+
defaultArgList.Add(parameters[v].GetDefaultValue());
574+
}
575+
else
576+
{
525577
match = false;
526-
} else {
527-
defaultArgList.Add(parameters[v].DefaultValue);
528578
}
529579
}
530-
} else if (argumentCount > parameters.Length && parameters.Length > 0 &&
580+
}
581+
else if (positionalArgumentCount > parameters.Length && parameters.Length > 0 &&
531582
Attribute.IsDefined(parameters[parameters.Length - 1], typeof(ParamArrayAttribute)))
532583
{
533584
// This is a `foo(params object[] bar)` style method
@@ -722,4 +773,33 @@ internal Binding(MethodBase info, object inst, object[] args, int outs)
722773
this.outs = outs;
723774
}
724775
}
776+
777+
778+
static internal class ParameterInfoExtensions
779+
{
780+
public static object GetDefaultValue(this ParameterInfo parameterInfo)
781+
{
782+
// parameterInfo.HasDefaultValue is preferable but doesn't exist in .NET 4.0
783+
bool hasDefaultValue = (parameterInfo.Attributes & ParameterAttributes.HasDefault) ==
784+
ParameterAttributes.HasDefault;
785+
786+
if (hasDefaultValue)
787+
{
788+
return parameterInfo.DefaultValue;
789+
}
790+
else
791+
{
792+
// [OptionalAttribute] was specified for the parameter.
793+
// See https://stackoverflow.com/questions/3416216/optionalattribute-parameters-default-value
794+
// for rules on determining the value to pass to the parameter
795+
var type = parameterInfo.ParameterType;
796+
if (type == typeof(object))
797+
return Type.Missing;
798+
else if (type.IsValueType)
799+
return Activator.CreateInstance(type);
800+
else
801+
return null;
802+
}
803+
}
804+
}
725805
}

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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy