diff --git a/core/src/main/java/org/python/core/ArgParser.java b/core/src/main/java/org/python/core/ArgParser.java
index 652ed2c2e..563be1680 100644
--- a/core/src/main/java/org/python/core/ArgParser.java
+++ b/core/src/main/java/org/python/core/ArgParser.java
@@ -5,7 +5,6 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
-import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.StringJoiner;
@@ -25,26 +24,78 @@
* in a Python implementation, and arranges them into an array. This
* array is either created by the parser, or designated by the
* caller. The parser may therefore be used to prepare arguments for
- * a pure a Java method (or {@code MethodHandle}) accepting an
- * array, or to insert them as initial values in an interpreter
- * frame ({@code PyFrame}).
+ * a pure a Java method (or {@code MethodHandle}) that accepts an
+ * array, or to insert arguments as initial values of local
+ * variables in an an optimised interpreter frame ({@link PyFrame}).
*
* The fields of the parser that determine the acceptable numbers of
* positional arguments and their names are essentially those of a
* {@code code} object ({@link PyCode}). Defaults are provided
* values that mirror the defaults built into a {@code function}
- * object ({@code PyFunction}).
+ * object ({@link PyFunction}).
+ *
+ * Consider for example a function that in Python would have the
+ * function definition:
+ * def func(a, b, c=3, d=4, /, e=5, f=6, *aa, g=7, h, i=9, **kk):
+ * pass
+ * This could be described by a constructor call and
+ * modifiers:
+ * String[] names = {"a", "b", "c", "d", "e", "f", "g", "h", "i",
+ * "aa", "kk"};
+ * ArgParser ap = new ArgParser("func", names,
+ * names.length - 2, 4, 3, true, true) //
+ * .defaults(3, 4, 5, 6) //
+ * .kwdefaults(7, null, 9);
+ * Note that "aa" and "kk" are at the end of the parameter
+ * names. (This is how a CPython frame is laid out.)
+ *
+ * Defaults are provided, after the parser has been constructed, as
+ * values corresponding to parameter names, when right-justified in
+ * the space to which they apply. (See diagram below.) Both the
+ * positional and keyword defaults are given by position in this
+ * formulation. The {@link #kwdefaults(Object...)} call is allowed
+ * to supply {@code null} values at positions it does not define.
+ *
+ * When parsed to an array, the layout of the argument values, in
+ * relation to fields of the parser will be as follows.
+ *
+ * A Python {@code frame}
+ *
+ * names
+ * a
+ * b
+ * c
+ * d
+ * e
+ * f
+ * g
+ * h
+ * i
+ * aa
+ * kk
+ *
+ *
+ * layout
+ * posOnly
+ *
+ * kwOnly
+ *
+ *
+ *
+ * defaults
+ * kwdefaults
+ *
+ *
*
* The most readable way of specifying a parser (although one that
* is a little costly to construct) is to list the parameters as
* they would be declared in Python, including the furniture that
* marks up the positional-only, keyword-only, positional varargs,
* and keyword varargs. This is the API offered by
- * {@link #fromSignature(String, String...)} but it only features in
- * unit tests.
- *
- * In practice, we construct the parser with a complex of arguments
- * derived by inspection of the method signature.
+ * {@link #fromSignature(String, String...)}. In practice we only
+ * use this in unit tests. For serious applications we construct the
+ * {@code ArgParser} with a complex of arguments derived by
+ * inspection of the Java or Python signature.
*/
class ArgParser {
@@ -76,16 +127,19 @@ class ArgParser {
* Names of parameters that could be satisfied by position or
* keyword, including the collector parameters. Elements are
* guaranteed to be interned, and not {@code null} or empty. The
- * length of this array is the number of named parameters:
+ * array must name all the parameters, of which there are:
* {@code argcount + kwonlyargcount
+ (hasVarArgs() ? 1 : 0) + (hasVarKeywords() ? 1 : 0)}
+ *
+ * It is often is longer since it suits us to re-use an array that
+ * names all the local variables of a frame.
*/
/*
- * Here and elsewhere we use the same names as the CPython code,
- * even though it tends to say "argument" when it could mean that or
- * "parameter". In comments and documentation "positional parameter"
- * means a parameter eligible to be satisfied by an argument given
- * by position.
+ * Here and elsewhere we use the same field names as the CPython
+ * code, even though it tends to say "argument" when it could mean
+ * that or "parameter". In comments and documentation
+ * "positional parameter" means a parameter eligible to be satisfied
+ * by an argument given by position.
*/
final String[] argnames;
@@ -142,40 +196,45 @@ class ArgParser {
final int varKeywordsIndex;
/**
- * Create a parser, for a named function, with defined numbers of
- * positional-only and keyword-only parameters, and naming the
- * parameters. Parameters that may only be satisfied by arguments
- * given by position need not be named. ("" is acceptable in the
- * names array.)
+ * Construct a parser for a named function, with defined numbers of
+ * positional-only and keyword-only parameters, and parameter names
+ * in an array prepared by client code.
*
- * Overflow of positional and/or keyword arguments into a
- * {@code tuple} or {@code dict} may also be allowed. For example, a
- * function that in Python would have the signature with the
- * function definition:
+ * The array of names is used in-place (not copied). The client code
+ * must therefore ensure that it cannot be modified after the parser
+ * has been constructed.
+ *
+ * The array of names may be longer than is necessary: the caller
+ * specifies how much of the array should be treated as regular
+ * parameter names, and whether zero, one or two further elements
+ * will name collectors for excess positional or keyword arguments.
+ * The rest of the elements will not be examined by the parser. The
+ * motivation for this design is to permit efficient construction
+ * when the the array of names is the local variable names in a
+ * Python {@code code} object.
+ *
+ * @param name of the function
+ * @param names of the parameters including any collectors (varargs)
+ * @param regargcount number of regular (non-collector) parameters
+ * @param posOnly number of positional-only parameters
+ * @param kwOnly number of keyword-only parameters
+ * @param varargs whether there is positional collector
+ * @param varkw whether there is a keywords collector
+ */
+ ArgParser(String name, String[] names, int regargcount, int posOnly, int kwOnly,
+ boolean varargs, boolean varkw) {
+ this(name, ScopeKind.TYPE, MethodKind.STATIC, names, regargcount, posOnly, kwOnly, varargs,
+ varkw);
+ }
+
+ /**
+ * Construct a parser from descriptive parameters that may be
+ * derived from a the annotated declarations ({@link Exposed}
+ * methods) that appear in type and module definitions written in
+ * Java. For;
* def func(a, b, c=3, d=4, /, e=5, f=6, *aa, g=7, h, i=9, **kk):
* pass
- * # func.__defaults__ == (3, 4, 5, 6)
- * # func.__kwdefaults__ == {'g': 7, 'i': 9}
- * would be described by a constructor call:
- * private static ArgParser parser =
- * new ArgParser("func", "aa", "kk", 4, 3, //
- * "a", "b", "c", "d", "e", "f", "g", "h", "i")
- * .defaults(3, 4, 5, 6) //
- * .kwdefaults(7, null, 9);
- *
- * Note that "aa" and "kk" are given separately, not amongst
- * the parameter names. In the parsing result array, they will be at
- * the end. (This is how a CPython frame is laid out.)
- *
- * Defaults are provided, after the parser has been constructed, as
- * values corresponding to parameter names, when right-justified in
- * the space to which they apply. (See diagram below.) Both the
- * positional and keyword defaults are given by position in this
- * formulation. The {@link #kwdefaults(Object...)} call is allowed
- * to supply {@code null} values at positions it does not define.
- *
- * When parsed to an array, the layout of the argument values, in
- * relation to fields of the parser will be as follows.
+ *
The constructor arguments should specify this layout:
*
* A Python {@code frame}
*
@@ -197,6 +256,8 @@ class ArgParser {
* posOnly
*
* kwOnly
+ * varargs
+ * varkw
*
*
*
@@ -205,150 +266,37 @@ class ArgParser {
*
*
*
- * @param name of the function
- * @param varargs name of the positional collector or {@code null}
- * @param varkw name of the keywords collector or {@code null}
- * @param posOnly number of positional-only parameters
- * @param kwdOnly number of keyword-only parameters
- * @param names of the (non-collector) parameters
- */
- ArgParser(String name, String varargs, String varkw, int posOnly, int kwdOnly,
- String... names) {
- this(name, ScopeKind.TYPE, MethodKind.STATIC, varargs, varkw, posOnly, kwdOnly, names);
- }
-
- /**
* @param name of the function
* @param scopeKind whether module, etc.
* @param methodKind whether static, etc.
- * @param varargs name of the positional collector or {@code null}
- * @param varkw name of the keywords collector or {@code null}
- * @param posOnly number of positional-only parameters
- * @param kwdOnly number of keyword-only parameters
- * @param names of the (non-collector) parameters
- */
- ArgParser(String name, ScopeKind scopeKind, MethodKind methodKind, String varargs, String varkw,
- int posOnly, int kwdOnly, String... names) {
-
- // Name of function
- this.name = name;
- this.methodKind = methodKind;
- this.scopeKind = scopeKind;
-
- // Total parameter count *except* possible varargs, varkwargs
- int N = names.length;
- this.regargcount = N;
-
- // Fill in other cardinal points
- this.posonlyargcount = posOnly;
- this.kwonlyargcount = kwdOnly;
- this.argcount = N - kwdOnly;
-
- // There may be positional and/or keyword collectors
- this.varArgsIndex = varargs != null ? N++ : -1;
- this.varKeywordsIndex = varkw != null ? N++ : -1;
-
- // Make a new array of the names, including the collectors.
- String[] argnames = this.argnames = interned(names, N);
-
- if (varargs != null)
- argnames[varArgsIndex] = varargs.intern();
- if (varkw != null)
- argnames[varKeywordsIndex] = varkw.intern();
-
- // Check for empty names
- for (int i = posOnly; i < N; i++) {
- if (argnames[i].length() == 0) {
- // We found a "" name beyond positional only.
- throw new InterpreterError(MISPLACED_EMPTY, name, argnames.toString());
- }
- }
-
- assert argnames.length == argcount + kwonlyargcount + (hasVarArgs() ? 1 : 0)
- + (hasVarKeywords() ? 1 : 0);
- }
-
- /**
- * Construct a parser for a named function, with defined numbers of
- * positional-only and keyword-only parameters, and parameter names
- * in an array prepared by client code.
- *
- * The capabilities of this parser, are exactly the same as one
- * defined by
- * {@link #ArgParser(String, String, String, int, int, String...)}.
- * The parser in the example there may be generated by:
- * String[] names = {"a", "b", "c", "d", "e", "f", "g", "h", "i",
- * "aa", "kk"};
- * ArgParser ap = new ArgParser("func", true, true, 4, 3, names,
- * names.length - 2) //
- * .defaults(3, 4, 5, 6) //
- * .kwdefaults(7, null, 9);
- * The differences allow the array of names to be used
- * in-place (not copied). The client code must therefore ensure that
- * it cannot be modified after the parser has been constructed.
- *
- * The array of names may be longer than is necessary: the caller
- * specifies how much of the array should be treated as regular
- * parameter names, and whether zero, one or two further elements
- * will name collectors for excess positional or keyword arguments.
- * The rest of the elements will not be examined by the parser. The
- * motivation for this design is to permit efficient construction
- * when the the array of names is the local variable names in a
- * Python {@code code} object.
- *
- * @param name of the function
- * @param varargs whether there is positional collector
- * @param varkw whether there is a keywords collector
- * @param posOnly number of positional-only parameters
- * @param kwdOnly number of keyword-only parameters
* @param names of the parameters including any collectors (varargs)
- * @param count number of regular (non-collector) parameters
- */
- ArgParser(String name, boolean varargs, boolean varkw, int posOnly, int kwdOnly, String[] names,
- int count) {
- this(name, ScopeKind.TYPE, MethodKind.STATIC, varargs, varkw, posOnly, kwdOnly, names,
- count);
- }
-
- /**
- * Construct a parser from descriptive parameters that may be
- * derived from a the annotated declarations ({@link Exposed}
- * methods) that appear in type and module definitions written in
- * Java.
- *
- * @param name of the function
- * @param scopeKind whether module, etc.
- * @param methodKind whether static, etc.
+ * @param regargcount number of regular (non-collector) parameters
+ * @param posOnly number of positional-only parameters
+ * @param kwOnly number of keyword-only parameters
* @param varargs whether there is positional collector
* @param varkw whether there is a keywords collector
- * @param posOnly number of positional-only parameters
- * @param kwdOnly number of keyword-only parameters
- * @param names of the parameters including any collectors (varargs)
- * @param count number of regular (non-collector) parameters
*/
- ArgParser(String name, ScopeKind scopeKind, MethodKind methodKind, boolean varargs,
- boolean varkw, int posOnly, int kwdOnly, String[] names, int count) {
+ ArgParser(String name, ScopeKind scopeKind, MethodKind methodKind, String[] names,
+ int regargcount, int posOnly, int kwOnly, boolean varargs, boolean varkw) {
// Name of function
this.name = name;
this.methodKind = methodKind;
this.scopeKind = scopeKind;
+ this.argnames = names;
// Total parameter count *except* possible varargs, varkwargs
- int N = Math.min(count, names.length);
+ int N = Math.min(regargcount, names.length);
this.regargcount = N;
this.posonlyargcount = posOnly;
- this.kwonlyargcount = kwdOnly;
- this.argcount = N - kwdOnly;
+ this.kwonlyargcount = kwOnly;
+ this.argcount = N - kwOnly;
// There may be positional and/or keyword collectors
this.varArgsIndex = varargs ? N++ : -1;
this.varKeywordsIndex = varkw ? N++ : -1;
- // Make a new array of the names, including the collectors.
- this.argnames = interned(names, N);
-
- assert argnames.length == argcount + kwonlyargcount + (hasVarArgs() ? 1 : 0)
+ assert argnames.length >= argcount + kwonlyargcount + (hasVarArgs() ? 1 : 0)
+ (hasVarKeywords() ? 1 : 0);
}
@@ -412,11 +360,35 @@ static ArgParser fromSignature(String name, String... decl) {
// Total parameter count *except* possible varargs, varkwargs
int N = args.size();
+
+ /*
+ * If there was no "/" or "*", all are positional arguments. This is
+ * consistent with the output of inspect.signature, where e.g.
+ * inspect.signature(exec) is (source, globals=None, locals=None,
+ * /).
+ */
if (posCount == 0) { posCount = N; }
+ // Number of regular arguments (not *, **)
+ int regArgCount = N;
+ int kwOnly = N - posCount;
+
+ // Add any *args to the names
+ if (varargs != null) {
+ args.add(varargs);
+ N++;
+ }
+
+ // Add any **kwargs to the names
+ if (varkw != null) {
+ args.add(varkw);
+ N++;
+ }
+
String[] names = N == 0 ? NO_STRINGS : args.toArray(new String[N]);
- return new ArgParser(name, ScopeKind.TYPE, MethodKind.STATIC, varargs, varkw, posOnly,
- N - posCount, names);
+
+ return new ArgParser(name, ScopeKind.TYPE, MethodKind.STATIC, names, regArgCount, posOnly,
+ kwOnly, varargs != null, varkw != null);
}
/**
@@ -522,21 +494,6 @@ String textSignature() {
return sj.toString();
}
- /**
- * Return a copy of a {@code String} array in which every element is
- * interned, in a new , possibly larger, array. We intern the
- * strings to enable the fast path in keyword argument processing.
- *
- * @param a to intern
- * @param N size of array to return
- * @return array of equivalent interned strings
- */
- private String[] interned(String[] a, int N) {
- String[] s = new String[Math.max(N, a.length)];
- for (int i = 0; i < a.length; i++) { s[i] = a[i].intern(); }
- return s;
- }
-
/**
* Return i th positional parameter name and default value if
* available. Helper to {@link #sigString()}.
@@ -633,26 +590,23 @@ Object[] parse(PyTuple args, PyDict kwargs) {
return a;
}
- private static final String MISPLACED_EMPTY =
- "Misplaced empty keyword in ArgParser spec for %s %s";
-
/**
- * Provide the positional defaults. If L values are provided, they
- * correspond to {@code arg[max-L] ... arg[max-1]}, where
- * {@code max} is the index of the first keyword-only parameter, or
- * the number of parameters if there are no keyword-only parameters.
- * The minimum number of positional arguments will then be
- * {@code max-L}.
+ * Provide the positional defaults. * The {@code ArgParser} keeps a
+ * reference to this array, so that subsequent changes to it will
+ * affect argument parsing. (Concurrent access to the array and
+ * parser is a client issue.)
+ *
+ * If L values are provided, they correspond to
+ * {@code arg[max-L] ... arg[max-1]}, where {@code max} is the index
+ * of the first keyword-only parameter, or the number of parameters
+ * if there are no keyword-only parameters. The minimum number of
+ * positional arguments will then be {@code max-L}.
*
* @param values replacement positional defaults (or {@code null})
* @return {@code this}
*/
ArgParser defaults(Object... values) {
- if (values == null || values.length == 0) {
- defaults = null;
- } else {
- defaults = Arrays.copyOf(values, values.length);
- }
+ defaults = values;
checkShape();
return this;
}
@@ -682,18 +636,17 @@ ArgParser kwdefaults(Object... values) {
}
/**
- * Provide the keyword-only defaults, perhaps as a {@code dict}.
+ * Provide the keyword-only defaults, perhaps as a {@code dict}. The
+ * {@code ArgParser} keeps a reference to this map, so that
+ * subsequent changes to it will affect argument parsing, as
+ * required for a Python {@link PyFunction function}. (Concurrent
+ * access to the mapping and parser is a client issue.)
*
* @param kwd replacement keyword defaults (or {@code null})
* @return {@code this}
*/
ArgParser kwdefaults(Map kwd) {
- if (kwd == null || kwd.isEmpty())
- kwdefaults = null;
- else {
- kwdefaults = new HashMap<>();
- kwdefaults.putAll(kwd);
- }
+ kwdefaults = kwd;
checkShape();
return this;
}
@@ -706,6 +659,7 @@ ArgParser kwdefaults(Map kwd) {
* number of parameters.
*/
private void checkShape() {
+ // XXX This may be too fussy, given that Python function is not
final int N = argcount;
final int L = defaults == null ? 0 : defaults.length;
final int K = kwonlyargcount;
@@ -781,12 +735,12 @@ void setPositionalArguments(PyTuple args) {
* positional or keyword defaults to make up the shortfall.
*
* @param stack positional and keyword arguments
- * @param start position of arguments in the array
+ * @param pos position of arguments in the array
* @param nargs number of positional arguments
*/
- void setPositionalArguments(Object[] stack, int start, int nargs) {
+ void setPositionalArguments(Object[] stack, int pos, int nargs) {
int n = Math.min(nargs, argcount);
- for (int i = 0, j = start; i < n; i++)
+ for (int i = 0, j = pos; i < n; i++)
setLocal(i, stack[j++]);
}
@@ -868,12 +822,13 @@ void setKeywordArguments(PyDict kwargs) {
* @param kwnames keywords used in the call (or {@code **kwargs})
*/
void setKeywordArguments(Object[] stack, int kwstart, String[] kwnames) {
- /*
- * Create a dictionary for the excess keyword parameters, and insert
- * it in the local variables at the proper position.
- */
+
PyDict kwdict = null;
if (varKeywordsIndex >= 0) {
+ /*
+ * Create a dictionary for the excess keyword parameters, and insert
+ * it in the local variables at the proper position.
+ */
kwdict = Py.dict();
setLocal(varKeywordsIndex, kwdict);
}
@@ -906,9 +861,6 @@ void setKeywordArguments(Object[] stack, int kwstart, String[] kwnames) {
throw new TypeError(MULTIPLE_VALUES, name, key);
}
}
-
- if (varKeywordsIndex >= 0) { setLocal(varKeywordsIndex, kwdict); }
-
}
/**
@@ -1226,8 +1178,7 @@ class ArrayFrameWrapper extends FrameWrapper {
* intended use is that {@code start = 1} allows space for a
* {@code self} reference not in the argument list. The capacity of
* the array, between the start index and the end, must be
- * sufficient to hold the parse result. The destination array must
- * be sufficient to hold the parse result and may be larger, e.g. to
+ * sufficient to hold the parse result may be larger, e.g. to
* accommodate other local variables.
*
* @param vars destination array
@@ -1259,6 +1210,12 @@ void setPositionalArguments(PyTuple argsTuple) {
int n = Math.min(argsTuple.value.length, argcount);
System.arraycopy(argsTuple.value, 0, vars, start, n);
}
+
+ @Override
+ void setPositionalArguments(Object[] stack, int pos, int nargs) {
+ int n = Math.min(nargs, argcount);
+ System.arraycopy(stack, pos, vars, start, n);
+ }
}
/**
@@ -1335,11 +1292,12 @@ void parseToFrame(FrameWrapper frame, Object[] stack, int start, int nargs, Stri
*/
// Set parameters from the positional arguments in the call.
- frame.setPositionalArguments(stack, start, nargs);
+ if (nargs > 0) { frame.setPositionalArguments(stack, start, nargs); }
// Set parameters from the keyword arguments in the call.
- if (nkwargs > 0)
+ if (varKeywordsIndex >= 0 || nkwargs > 0) {
frame.setKeywordArguments(stack, start + nargs, kwnames);
+ }
if (nargs > argcount) {
@@ -1376,7 +1334,7 @@ void parseToFrame(FrameWrapper frame, Object[] stack, int start, int nargs, Stri
*
* @param frame to populate with argument values
* @param args all arguments, positional then keyword
- * @param kwnames of keyword arguments
+ * @param kwnames of keyword arguments (or {@code null})
*/
void parseToFrame(FrameWrapper frame, Object[] args, String[] kwnames) {
@@ -1393,11 +1351,12 @@ void parseToFrame(FrameWrapper frame, Object[] args, String[] kwnames) {
*/
// Set parameters from the positional arguments in the call.
- frame.setPositionalArguments(args, 0, nargs);
+ if (nargs > 0) { frame.setPositionalArguments(args, 0, nargs); }
// Set parameters from the keyword arguments in the call.
- if (nkwargs > 0)
+ if (varKeywordsIndex >= 0 || nkwargs > 0) {
frame.setKeywordArguments(args, nargs, kwnames);
+ }
if (nargs > argcount) {
diff --git a/core/src/main/java/org/python/core/BuiltinsModule.java b/core/src/main/java/org/python/core/BuiltinsModule.java
new file mode 100644
index 000000000..87dc896fa
--- /dev/null
+++ b/core/src/main/java/org/python/core/BuiltinsModule.java
@@ -0,0 +1,185 @@
+// Copyright (c)2023 Jython Developers.
+// Licensed to PSF under a contributor agreement.
+package org.python.core;
+
+import java.lang.invoke.MethodHandles;
+import java.util.Iterator;
+
+import org.python.core.Exposed.Default;
+import org.python.core.Exposed.DocString;
+import org.python.core.Exposed.KeywordOnly;
+import org.python.core.Exposed.Name;
+import org.python.core.Exposed.PositionalCollector;
+import org.python.core.Exposed.PythonStaticMethod;
+
+/**
+ * The {@code builtins} module is definitely called "builtins".
+ *
+ * Although it is fully a module, the {@link BuiltinsModule} lives
+ * in the {@code core} package because it needs privileged access to
+ * the core implementation that extension modules do not.
+ */
+class BuiltinsModule extends JavaModule {
+
+ private static final ModuleDef DEFINITION = new ModuleDef("builtins", MethodHandles.lookup());
+
+ /** Construct an instance of the {@code builtins} module. */
+ BuiltinsModule() {
+ super(DEFINITION);
+
+ // This list is taken from CPython bltinmodule.c
+ add("None", Py.None);
+ // add("Ellipsis", Py.Ellipsis);
+ add("NotImplemented", Py.NotImplemented);
+ add("False", Py.False);
+ add("True", Py.True);
+ add("bool", PyBool.TYPE);
+ // add("memoryview", PyMemoryView.TYPE);
+ // add("bytearray", PyByteArray.TYPE);
+ add("bytes", PyBytes.TYPE);
+ // add("classmethod", PyClassMethod.TYPE);
+ // add("complex", PyComplex.TYPE);
+ add("dict", PyDict.TYPE);
+ // add("enumerate", PyEnum.TYPE);
+ // add("filter", PyFilter.TYPE);
+ add("float", PyFloat.TYPE);
+ // add("frozenset", PyFrozenSet.TYPE);
+ // add("property", PyProperty.TYPE);
+ add("int", PyLong.TYPE);
+ add("list", PyList.TYPE);
+ // add("map", PyMap.TYPE);
+ add("object", PyBaseObject.TYPE);
+ // add("range", PyRange.TYPE);
+ // add("reversed", PyReversed.TYPE);
+ // add("set", PySet.TYPE);
+ add("slice", PySlice.TYPE);
+ // add("staticmethod", PyStaticMethod.TYPE);
+ add("str", PyUnicode.TYPE);
+ // add("super", PySuper.TYPE);
+ add("tuple", PyTuple.TYPE);
+ add("type", PyType.TYPE);
+ // add("zip", PyZip.TYPE);
+ }
+
+ @PythonStaticMethod
+ @DocString("Return the absolute value of the argument.")
+ static Object abs(Object x) throws Throwable { return PyNumber.absolute(x); }
+
+ @PythonStaticMethod
+ @DocString("Return the number of items in a container.")
+ static Object len(Object v) throws Throwable { return PySequence.size(v); }
+
+ /**
+ * Implementation of {@code max()}.
+ *
+ * @param arg1 a first argument or iterable of arguments
+ * @param args contains other positional arguments
+ * @param key function
+ * @param dflt to return when iterable is empty
+ * @return {@code max} result or {@code dflt}
+ * @throws Throwable from calling {@code key} or comparison
+ */
+ @PythonStaticMethod(positionalOnly = false)
+ @DocString("Return the largest item in an iterable"
+ + " or the largest of two or more arguments.")
+ // Simplified version of max()
+ static Object max(Object arg1, @KeywordOnly @Default("None") Object key,
+ @Name("default") @Default("None") Object dflt, @PositionalCollector PyTuple args)
+ throws Throwable {
+ // @PositionalCollector has to be last.
+ return minmax(arg1, args, key, dflt, Comparison.GT);
+ }
+
+ /**
+ * Implementation of {@code min()}.
+ *
+ * @param arg1 a first argument or iterable of arguments
+ * @param args contains other positional arguments
+ * @param key function
+ * @param dflt to return when iterable is empty
+ * @return {@code min} result or {@code dflt}
+ * @throws Throwable from calling {@code key} or comparison
+ */
+ @PythonStaticMethod(positionalOnly = false)
+ @DocString("Return the smallest item in an iterable"
+ + " or the smallest of two or more arguments.")
+ // Simplified version of min()
+ static Object min(Object arg1, @KeywordOnly @Default("None") Object key,
+ @Name("default") @Default("None") Object dflt, @PositionalCollector PyTuple args)
+ throws Throwable {
+ // @PositionalCollector has to be last.
+ return minmax(arg1, args, key, dflt, Comparison.LT);
+ }
+
+ /**
+ * Implementation of both
+ * {@link #min(Object, Object, Object, PyTuple) min()} and
+ * {@link #max(Object, Object, Object, PyTuple) max()}.
+ *
+ * @param arg1 a first argument or iterable of arguments
+ * @param args contains other positional arguments
+ *
+ * @param key function
+ * @param dflt to return when iterable is empty
+ * @param op {@code LT} for {@code min} and {@code GT} for
+ * {@code max}.
+ * @return min or max result as appropriate
+ * @throws Throwable from calling {@code op} or {@code key}
+ */
+ // Compare CPython min_max in Python/bltinmodule.c
+ private static Object minmax(Object arg1, PyTuple args, Object key, Object dflt, Comparison op)
+ throws Throwable {
+
+ int n = args.size();
+ Object result;
+ Iterator others;
+ assert key != null;
+
+ if (n > 0) {
+ /*
+ * Positional mode: arg1 is the first value, args contains the other
+ * values to compare
+ */
+ result = key == Py.None ? arg1 : Callables.callFunction(key, arg1);
+ others = args.iterator();
+ if (dflt != Py.None) {
+ String name = op == Comparison.LT ? "min" : "max";
+ throw new TypeError(DEFAULT_WITHOUT_ITERABLE, name);
+ }
+
+ } else {
+ // Single iterable argument of the values to compare
+ result = null;
+ // XXX define PySequence.iterable like PyMapping.map?
+ others = PySequence.fastList(arg1, null).iterator();
+ }
+
+ // Now we can get on with the comparison
+ while (others.hasNext()) {
+ Object item = others.next();
+ if (key != Py.None) { item = Callables.callFunction(key, item); }
+ if (result == null) {
+ result = item;
+ } else if (Abstract.richCompareBool(item, result, op)) { result = item; }
+ }
+
+ // result may be null if the single iterable argument is empty
+ if (result != null) {
+ return result;
+ } else if (dflt != Py.None) {
+ assert dflt != null;
+ return dflt;
+ } else {
+ String name = op == Comparison.LT ? "min" : "max";
+ throw new ValueError("%s() arg is an empty sequence", name);
+ }
+ }
+
+ private static final String DEFAULT_WITHOUT_ITERABLE =
+ "Cannot specify a default for %s() with multiple positional arguments";
+
+ @PythonStaticMethod
+ @DocString("Return the canonical string representation of the object.\n"
+ + "For many object types, including most builtins, eval(repr(obj)) == obj.")
+ static Object repr(Object obj) throws Throwable { return Abstract.repr(obj); }
+}
diff --git a/core/src/main/java/org/python/core/CPython38Frame.java b/core/src/main/java/org/python/core/CPython38Frame.java
index 143365abb..10c591155 100644
--- a/core/src/main/java/org/python/core/CPython38Frame.java
+++ b/core/src/main/java/org/python/core/CPython38Frame.java
@@ -1,4 +1,4 @@
-// Copyright (c)2022 Jython Developers.
+// Copyright (c)2023 Jython Developers.
// Licensed to PSF under a contributor agreement.
package org.python.core;
@@ -155,11 +155,21 @@ Object eval() {
try {
locals.put(name, s[--sp]);
} catch (NullPointerException npe) {
- throw new SystemError("no locals found when storing '%s'", name);
+ throw noLocals("storing", name);
}
oparg = 0;
break;
+ case Opcode.DELETE_NAME:
+ name = names[oparg | opword & 0xff];
+ oparg = 0;
+ try {
+ locals.remove(name);
+ } catch (NullPointerException npe) {
+ throw noLocals("deleting", name);
+ }
+ break;
+
case Opcode.BUILD_MAP:
// k1 | v1 | ... | kN | vN | -> | map |
// -------------------------^sp -------^sp
@@ -355,7 +365,7 @@ Object eval() {
* regular method in it. {@code CALL_METHOD} will detect and use
* this optimised form if the first element is not {@code null}.
*
- * @param obj of whichg the callable is an attribute
+ * @param obj of which the callable is an attribute
* @param name of callable attribute
* @param offset in stack at which to place results
* @throws AttributeError ifthe named attribute does not exist
@@ -463,4 +473,15 @@ private void getMethod(Object obj, String name, int offset) throws AttributeErro
// All the look-ups and descriptors came to nothing :(
throw Abstract.noAttributeError(obj, name);
}
+
+ /**
+ * Generate error to throw when we cannot access locals.
+ *
+ * @param action "loading", "storing" or "deleting"
+ * @param name variable name
+ * @return
+ */
+ private static SystemError noLocals(String action, String name) {
+ return new SystemError("no locals found when %s '%s'", name);
+ }
}
diff --git a/core/src/main/java/org/python/core/Exposer.java b/core/src/main/java/org/python/core/Exposer.java
index c072579f8..e1fb1e554 100644
--- a/core/src/main/java/org/python/core/Exposer.java
+++ b/core/src/main/java/org/python/core/Exposer.java
@@ -36,6 +36,7 @@
import org.python.core.Exposed.PositionalOnly;
import org.python.core.Exposed.PythonMethod;
import org.python.core.Exposed.PythonStaticMethod;
+import org.python.core.ModuleDef.MethodDef;
/**
* An object for tabulating the attributes of classes that define
@@ -67,6 +68,25 @@ protected Exposer() {
/** @return which {@link ScopeKind} of {@code Exposer} is this? */
abstract ScopeKind kind();
+ /**
+ * On behalf of the given module defined in Java, build a
+ * description of the attributes discovered by introspection of the
+ * class provided.
+ *
+ * Attributes are identified by annotations. (See {@link Exposed}.)
+ *
+ * @param definingClass to introspect for members
+ * @return exposure result
+ * @throws InterpreterError on errors of definition
+ */
+ static ModuleExposer exposeModule(Class> definingClass) throws InterpreterError {
+ // Create an instance of Exposer to hold specs, type, etc.
+ ModuleExposer exposer = new ModuleExposer();
+ // Let the exposer control the logic
+ exposer.expose(definingClass);
+ return exposer;
+ }
+
/**
* On behalf of the given type defined in Java, build a description
* of the attributes discovered by introspection of the class (or
@@ -617,14 +637,35 @@ private boolean isDefined() {
ArgParser getParser() {
if (parser == null && parameterNames != null
&& parameterNames.length >= posonlyargcount) {
- parser = new ArgParser(name, scopeKind, methodKind, varArgsIndex >= 0,
- varKeywordsIndex >= 0, posonlyargcount, kwonlyargcount, parameterNames,
- regargcount);
+ parser = new ArgParser(name, scopeKind, methodKind, parameterNames, regargcount,
+ posonlyargcount, kwonlyargcount, varArgsIndex >= 0, varKeywordsIndex >= 0);
parser.defaults(defaults).kwdefaults(kwdefaults);
}
return parser;
}
+ /**
+ * Produce a method definition from this specification that
+ * references a method handle on the (single) defining method and
+ * the parser created from this specification. This is used in the
+ * construction of a module defined in Java (a {@link ModuleDef}).
+ *
+ * @param lookup authorisation to access methods
+ * @return corresponding method definition
+ * @throws InterpreterError on lookup prohibited
+ */
+ MethodDef getMethodDef(Lookup lookup) throws InterpreterError {
+ assert methods.size() == 1;
+ Method m = methods.get(0);
+ MethodHandle mh;
+ try {
+ mh = lookup.unreflect(m);
+ } catch (IllegalAccessException e) {
+ throw cannotGetHandle(m, e);
+ }
+ return new MethodDef(getParser(), mh);
+ }
+
/**
* Add a method implementation. (A test that the signature is
* acceptable follows when we construct the {@link PyMethodDescr}.)
@@ -1126,9 +1167,9 @@ static class MethodSpec extends CallableSpec {
@Override
PyMethodDescr asAttribute(PyType objclass, Lookup lookup) throws InterpreterError {
- ArgParser ap = new ArgParser(name, scopeKind, MethodKind.INSTANCE, varArgsIndex >= 0,
- varKeywordsIndex >= 0, posonlyargcount, kwonlyargcount, parameterNames,
- regargcount);
+ ArgParser ap = new ArgParser(name, scopeKind, MethodKind.INSTANCE, parameterNames,
+ regargcount, posonlyargcount, kwonlyargcount, varArgsIndex >= 0,
+ varKeywordsIndex >= 0);
ap.defaults(defaults).kwdefaults(kwdefaults);
// Methods have self + this many args:
@@ -1166,7 +1207,7 @@ static class StaticMethodSpec extends CallableSpec {
StaticMethodSpec(String name, ScopeKind scopeKind) { super(name, scopeKind); }
@Override
- PyJavaMethod asAttribute(PyType objclass, Lookup lookup) {
+ PyJavaFunction asAttribute(PyType objclass, Lookup lookup) {
// TODO Auto-generated method stub
return null;
}
diff --git a/core/src/main/java/org/python/core/Interpreter.java b/core/src/main/java/org/python/core/Interpreter.java
index 1fe0bb078..a4f58b17d 100644
--- a/core/src/main/java/org/python/core/Interpreter.java
+++ b/core/src/main/java/org/python/core/Interpreter.java
@@ -1,3 +1,5 @@
+// Copyright (c)2023 Jython Developers.
+// Licensed to PSF under a contributor agreement.
package org.python.core;
import org.python.base.InterpreterError;
@@ -29,10 +31,9 @@ class Interpreter {
/** Create a new {@code Interpreter}. */
Interpreter() {
- // builtinsModule = new BuiltinsModule();
- // builtinsModule.init();
+ builtinsModule = new BuiltinsModule();
+ builtinsModule.exec();
// addModule(builtinsModule);
- builtinsModule = null;
}
void addModule(PyModule m) {
diff --git a/core/src/main/java/org/python/core/JavaModule.java b/core/src/main/java/org/python/core/JavaModule.java
new file mode 100644
index 000000000..14d5dda04
--- /dev/null
+++ b/core/src/main/java/org/python/core/JavaModule.java
@@ -0,0 +1,42 @@
+// Copyright (c)2023 Jython Developers.
+// Licensed to PSF under a contributor agreement.
+package org.python.core;
+
+/** Common mechanisms for all Python modules defined in Java. */
+public abstract class JavaModule extends PyModule {
+
+ final ModuleDef definition;
+
+ /**
+ * Construct the base {@code JavaModule}, saving the module
+ * definition, which is normally created during static
+ * initialisation of the concrete class defining the module. In
+ * terms of PEP 489 phases, the constructor performs the
+ * {@code Py_mod_create}. We defer filling the module dictionary
+ * from the definition and other sources until {@link #exec()} is
+ * called.
+ *
+ * @param definition of the module
+ */
+ protected JavaModule(ModuleDef definition) {
+ super(definition.name);
+ this.definition = definition;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * In the case of a {@code JavaModule}, the base implementation
+ * mines the method definitions from the {@link #definition}. The
+ * module should extend this method, that is call
+ * {@code super.exec()} to add boilerplate and the methods, then add
+ * other definitions (typically constants) to the module namespace
+ * with {@link #add(String, Object) #add(String, Object)}. In terms
+ * of PEP 489 phases, this is the {@code Py_mod_exec} phase.
+ */
+ @Override
+ void exec() {
+ super.exec();
+ definition.addMembers(this);
+ }
+}
diff --git a/core/src/main/java/org/python/core/MethodDescriptor.java b/core/src/main/java/org/python/core/MethodDescriptor.java
index 760a77926..5c652bf6e 100644
--- a/core/src/main/java/org/python/core/MethodDescriptor.java
+++ b/core/src/main/java/org/python/core/MethodDescriptor.java
@@ -9,7 +9,7 @@
* Java. This class provides some common behaviour and support
* methods that would otherwise be duplicated. This is also home to
* some static methods in support of both sub-classes and other
- * callable objects (e.g. {@link PyJavaMethod}).
+ * callable objects (e.g. {@link PyJavaFunction}).
*/
abstract class MethodDescriptor extends Descriptor implements FastCall {
diff --git a/core/src/main/java/org/python/core/MethodSignature.java b/core/src/main/java/org/python/core/MethodSignature.java
index 5766cdb96..7767e415c 100644
--- a/core/src/main/java/org/python/core/MethodSignature.java
+++ b/core/src/main/java/org/python/core/MethodSignature.java
@@ -17,7 +17,7 @@
/**
* The {@code enum MethodSignature} enumerates the method signatures
* for which an optimised implementation is possible. Sub-classes of
- * {@link PyJavaMethod} and {@link PyMethodDescr} correspond to
+ * {@link PyJavaFunction} and {@link PyMethodDescr} correspond to
* these values. It is not required that each value have a distinct
* optimised sub-class. This {@code enum} is used internally to
* choose between these sub-classes.
diff --git a/core/src/main/java/org/python/core/ModuleDef.java b/core/src/main/java/org/python/core/ModuleDef.java
new file mode 100644
index 000000000..9e3518ea6
--- /dev/null
+++ b/core/src/main/java/org/python/core/ModuleDef.java
@@ -0,0 +1,157 @@
+// Copyright (c)2022 Jython Developers.
+// Licensed to PSF under a contributor agreement.
+package org.python.core;
+
+import java.lang.invoke.MethodHandle;
+import java.lang.invoke.MethodHandles.Lookup;
+
+/**
+ * A {@code ModuleDef} is a definition from which instances of a module
+ * may be made. It stands in relation to the Java classes that define
+ * Python modules, somewhat in the way a Python {@code type} object
+ * stands in relation to the Java classes that define Python objects.
+ *
+ * What we most often encounter as "a module", a Python source file, is
+ * actually just a definition from which a module object may be made.
+ * This happens once in each interpreter where the module is
+ * imported. A distinct object, with mutable state, represents that
+ * module in each interpreter. There must therefore be a factory object
+ * that has access to the definition of the module, but is able to
+ * instantiate it (equivalent to executing the body of a module defined
+ * in Python). A {@code ModuleDef} is that factory.
+ *
+ * This initialisation cannot be identified with the static
+ * initialisation of the Java class, since that cannot be repeated, but
+ * must happen per instance. It is useful, however, to have an
+ * intermediate cache of the results of processing the defining Java
+ * class once statically initialised.
+ */
+public class ModuleDef {
+ // Compare CPython PyModuleDef
+
+ /** Name of the module. */
+ final String name;
+
+ /** The Java class defining instances of the module. */
+ final Class> definingClass;
+
+ /**
+ * Definitions for the members that appear in the dictionary of
+ * instances of the module named. Instances receive members by copy,
+ * by binding to the module instance (descriptors), or by reference
+ * (if immutable).
+ */
+ private final MethodDef[] methods;
+
+ /**
+ * Create a definition for the module, largely by introspection on
+ * the class and by forming {@code MethodHandle}s on discovered
+ * attributes.
+ *
+ * @param name of the module (e.g. "sys" or "math")
+ * @param lookup authorises access to the defining class.
+ */
+ ModuleDef(String name, Lookup lookup) {
+ this.name = name;
+ this.definingClass = lookup.lookupClass();
+ ModuleExposer exposer = Exposer.exposeModule(definingClass);
+ this.methods = exposer.getMethodDefs(lookup);
+ // XXX ... and for fields.
+ // XXX ... and for types defined in the module maybe? :o
+ }
+
+ /**
+ * Get the method definitions. This method is provided for test use
+ * only. It isn't safe as for public use.
+ *
+ * @return the method definitions
+ */
+ MethodDef[] getMethods() { return methods; }
+
+ /**
+ * Add members defined here to the dictionary of a module instance.
+ *
+ * @param module to populate
+ */
+ void addMembers(JavaModule module) {
+ PyDict d = module.dict;
+ for (MethodDef md : methods) {
+ // Create function by binding to the module
+ PyJavaFunction func = PyJavaFunction.fromParser(
+ md.argParser, md.handle, module, this.name);
+ d.put(md.argParser.name, func);
+ }
+ }
+
+ /**
+ * A {@code MethodDef} describes a built-in function or method as it
+ * is declared in a Java module. It holds an argument parser and a
+ * handle for calling the method.
+ *
+ * Recall that a module definition may have multiple instances. The
+ * {@code MethodDef} represents the method between the definition of
+ * the module (exposure as a {@link ModuleDef}) and the creation of
+ * actual {@link JavaModule} instances.
+ *
+ * When a method is declared in Java as an instance method of the
+ * module, the {@code MethodDef} that describes it discounts the
+ * {@code self} argument. The {@link PyJavaFunction} created from it
+ * binds the module instance that is its target, so that it is is
+ * correct for a call to that {@code PyJavaFunction}. This is
+ * consistent with CPython.
+ */
+ // Compare CPython struct PyMethodDef
+ static class MethodDef {
+
+ /*
+ * The object here is only superficially similar to the CPython
+ * PyMethodDef: it is not used as a member of descriptors or
+ * methods; extension writers do not declare instances of them.
+ * Instead, we reify the argument information from the
+ * declaration in Java, and associated annotations. In CPython,
+ * this knowledge is present at run-time in the structure of the
+ * code generated by Argument Clinic, incompletely in the flags
+ * of the PyMethodDef, and textually in the signature that
+ * begins the documentation string. We do it by holding an
+ * ArgParser.
+ */
+
+ /**
+ * An argument parser constructed with this {@code MethodDef}
+ * from the description of the signature. Full information on
+ * the signature is available from this structure, and it is
+ * available to parse the arguments to a standard
+ * {@code (Object[], String[])} call. (In simple sub-classes it
+ * is only used to generate error messages once simple checks
+ * fail.)
+ */
+ final ArgParser argParser;
+
+ /**
+ * A handle to the implementation of the function or method.
+ * This is generated by reflecting the same object that
+ * {@link #argParser} describes.
+ */
+ // CPython PyMethodDef: ml_meth
+ final MethodHandle handle;
+
+ /**
+ * Create a {@link MethodDef} of the given kind from the
+ * {@link ArgParser} provided.
+ *
+ * @param argParser parser defining the method
+ * @param meth method handle prepared by sub-class
+ */
+ MethodDef(ArgParser argParser, MethodHandle meth) {
+ this.argParser = argParser;
+ assert meth != null;
+ this.handle = meth;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("%s[%s]", getClass().getSimpleName(),
+ argParser);
+ }
+ }
+}
diff --git a/core/src/main/java/org/python/core/ModuleExposer.java b/core/src/main/java/org/python/core/ModuleExposer.java
new file mode 100644
index 000000000..277f3b586
--- /dev/null
+++ b/core/src/main/java/org/python/core/ModuleExposer.java
@@ -0,0 +1,88 @@
+// Copyright (c)2022 Jython Developers.
+// Licensed to PSF under a contributor agreement.
+package org.python.core;
+
+import java.lang.invoke.MethodHandles.Lookup;
+import java.lang.reflect.Method;
+
+import org.python.core.Exposed.PythonMethod;
+import org.python.core.Exposed.PythonStaticMethod;
+import org.python.base.InterpreterError;
+import org.python.core.ModuleDef.MethodDef;
+
+/**
+ * A {@code ModuleExposer} provides access to the attributes of a module
+ * defined in Java (a built-in or extension module). These are primarily
+ * the {@link MethodDef}s derived from annotated methods in the defining
+ * class. It is normally obtained by a call to
+ * {@link Exposer#exposeModule(Class)}.
+ */
+class ModuleExposer extends Exposer {
+
+ /**
+ * Construct the {@code ModuleExposer} instance for a particular
+ * module.
+ */
+ ModuleExposer() {}
+
+ /**
+ * Build the result from the defining class.
+ *
+ * @param definingClass to scan for definitions
+ */
+ void expose(Class> definingClass) {
+ // Scan the defining class for definitions
+ scanJavaMethods(definingClass);
+ // XXX ... and for fields.
+ // XXX ... and for types defined in the module maybe? :o
+ }
+
+ @Override
+ ScopeKind kind() { return ScopeKind.MODULE; }
+
+ /**
+ * From the methods discovered by introspection of the class, return
+ * an array of {@link MethodDef}s. This array will normally be part
+ * of a {@link ModuleDef} from which the dictionary of each instance
+ * of the module will be created.
+ *
+ * A {@link MethodDef} relies on {@code MethodHandle}, so a lookup
+ * object must be provided with the necessary access to the defining
+ * class.
+ *
+ * @param lookup authorisation to access methods
+ * @return method definitions
+ * @throws InterpreterError on lookup prohibited
+ */
+ MethodDef[] getMethodDefs(Lookup lookup) throws InterpreterError {
+ MethodDef[] a = new MethodDef[methodSpecs.size()];
+ int i = 0;
+ for (CallableSpec ms : methodSpecs) {
+ a[i++] = ms.getMethodDef(lookup);
+ }
+ return a;
+ }
+
+ /**
+ * For a Python module defined in Java, add to {@link specs}, the
+ * methods found in the given defining class and annotated for
+ * exposure.
+ *
+ * @param definingClass to introspect for definitions
+ * @throws InterpreterError on duplicates or unsupported types
+ */
+ @Override
+ void scanJavaMethods(Class> definingClass)
+ throws InterpreterError {
+
+ // Collect exposed functions (Java methods)
+ for (Method m : definingClass.getDeclaredMethods()) {
+ PythonMethod a =
+ m.getDeclaredAnnotation(PythonMethod.class);
+ if (a != null) { addMethodSpec(m, a); }
+ PythonStaticMethod sm =
+ m.getDeclaredAnnotation(PythonStaticMethod.class);
+ if (sm != null) { addStaticMethodSpec(m, sm); }
+ }
+ }
+}
diff --git a/core/src/main/java/org/python/core/PyFrame.java b/core/src/main/java/org/python/core/PyFrame.java
index 250904e8f..a6966e817 100644
--- a/core/src/main/java/org/python/core/PyFrame.java
+++ b/core/src/main/java/org/python/core/PyFrame.java
@@ -172,6 +172,8 @@ protected PyFrame(Interpreter interpreter, C code, PyDict globals, Object locals
*/
this.locals = PyMapping.map(locals);
}
+ // Fix up the builtins module dictionary (simplified)
+ this.builtins = interpreter.builtinsModule.getDict();
}
/**
diff --git a/core/src/main/java/org/python/core/PyJavaMethod.java b/core/src/main/java/org/python/core/PyJavaFunction.java
similarity index 90%
rename from core/src/main/java/org/python/core/PyJavaMethod.java
rename to core/src/main/java/org/python/core/PyJavaFunction.java
index 2b861dcd4..08e2a93e4 100644
--- a/core/src/main/java/org/python/core/PyJavaMethod.java
+++ b/core/src/main/java/org/python/core/PyJavaFunction.java
@@ -6,16 +6,18 @@
import java.lang.invoke.MethodHandles;
import java.util.List;
-import org.python.core.ArgumentError.Mode;
import org.python.base.InterpreterError;
import org.python.base.MethodKind;
+import org.python.core.ArgumentError.Mode;
+import org.python.core.Exposed.Getter;
+import org.python.core.Exposed.Member;
/**
* The Python {@code builtin_function_or_method} object. Java
* sub-classes represent either a built-in function or a built-in
* method bound to a particular object.
*/
-public abstract class PyJavaMethod implements CraftedPyObject, FastCall {
+public abstract class PyJavaFunction implements CraftedPyObject, FastCall {
/** The type of Python object this class implements. */
static final PyType TYPE = PyType.fromSpec( //
@@ -35,19 +37,20 @@ public abstract class PyJavaMethod implements CraftedPyObject, FastCall {
* ({@code object} or {@code type}). A function obtained from a
* module may be a method bound to an instance of that module.
*/
+ @Member("__self__")
final Object self;
/**
* A Java {@code MethodHandle} that implements the function or bound
* method. The type of this handle varies according to the sub-class
- * of {@code PyJavaMethod}, but it is definitely "prepared" to
+ * of {@code PyJavaFunction}, but it is definitely "prepared" to
* accept {@code Object.class} instances or arrays, not the actual
* parameter types of the method definition in Java.
*/
final MethodHandle handle;
/**
- * An argument parser supplied to this {@code PyJavaMethod} at
+ * An argument parser supplied to this {@code PyJavaFunction} at
* construction, from Java reflection of the definition in Java and
* from annotations on it. Full information on the signature is
* available from this structure, and it is available to parse the
@@ -72,7 +75,7 @@ public abstract class PyJavaMethod implements CraftedPyObject, FastCall {
* method)
* @param module name of the module supplying the definition
*/
- protected PyJavaMethod(ArgParser argParser, MethodHandle handle, Object self, String module) {
+ protected PyJavaFunction(ArgParser argParser, MethodHandle handle, Object self, String module) {
this.argParser = argParser;
this.handle = handle;
this.self = self;
@@ -80,7 +83,7 @@ protected PyJavaMethod(ArgParser argParser, MethodHandle handle, Object self, St
}
/**
- * Construct a {@code PyJavaMethod} from an {@link ArgParser} and
+ * Construct a {@code PyJavaFunction} from an {@link ArgParser} and
* {@code MethodHandle} for the implementation method. The arguments
* described by the parser do not include "self". This is the
* factory we use to create a function in a module.
@@ -94,7 +97,8 @@ protected PyJavaMethod(ArgParser argParser, MethodHandle handle, Object self, St
* @return A method descriptor supporting the signature
*/
// Compare CPython PyCFunction_NewEx in methodobject.c
- static PyJavaMethod fromParser(ArgParser ap, MethodHandle method, Object self, String module) {
+ static PyJavaFunction fromParser(ArgParser ap, MethodHandle method, Object self,
+ String module) {
/*
* Note this is a recommendation on the assumption all optimisations
* are supported. The actual choice is made in the switch statement.
@@ -130,11 +134,11 @@ static PyJavaMethod fromParser(ArgParser ap, MethodHandle method, Object self, S
}
/**
- * Construct a {@code PyJavaMethod} from a {@link PyMethodDescr} and
- * optional object to bind. The {@link PyMethodDescr} provides the
- * parser and unbound prepared {@code MethodHandle}. The arguments
- * described by the parser do not include "self". This is the
- * factory that supports descriptor {@code __get__}.
+ * Construct a {@code PyJavaFunction} from a {@link PyMethodDescr}
+ * and optional object to bind. The {@link PyMethodDescr} provides
+ * the parser and unbound prepared {@code MethodHandle}. The
+ * arguments described by the parser do not include "self". This is
+ * the factory that supports descriptor {@code __get__}.
*
* @param descr descriptor being bound
* @param self object to which bound (or {@code null} if a static
@@ -145,7 +149,7 @@ static PyJavaMethod fromParser(ArgParser ap, MethodHandle method, Object self, S
* @throws Throwable on other errors while chasing the MRO
*/
// Compare CPython PyCFunction_NewEx in methodobject.c
- static PyJavaMethod from(PyMethodDescr descr, Object self) throws TypeError, Throwable {
+ static PyJavaFunction from(PyMethodDescr descr, Object self) throws TypeError, Throwable {
ArgParser ap = descr.argParser;
assert ap.methodKind == MethodKind.INSTANCE;
MethodHandle handle = descr.getHandle(self).bindTo(self);
@@ -179,45 +183,18 @@ protected Object __repr__() throws Throwable {
Object __call__(Object[] args, String[] names) throws TypeError, Throwable {
try {
- if (names != null && names.length != 0) {
- return call(args, names);
- } else {
- int n = args.length;
- switch (n) {
- // case 0 (an error) handled by default clause
- case 1:
- return call(args[0]);
- case 2:
- return call(args[0], args[1]);
- case 3:
- return call(args[0], args[1], args[2]);
- case 4:
- return call(args[0], args[1], args[2], args[3]);
- default:
- return call(args);
- }
- }
+ // It is *not* worth unpacking the array here
+ return call(args, names);
} catch (ArgumentError ae) {
throw typeError(ae, args, names);
}
}
- /*
- * A simplified __call__ used in the narrative. To use, rename this
- * to __call__, rename the real __call__ to something else, and
- * force fromParser() and from() always to select General as the
- * implementation type.
- */
- Object simple__call__(Object[] args, String[] names) throws TypeError, Throwable {
- Object[] frame = argParser.parse(args, names);
- return handle.invokeExact(frame);
- }
-
// exposed methods -----------------------------------------------
/** @return name of the function or method */
// Compare CPython meth_get__name__ in methodobject.c
- // @Exposed.Getter
+ @Getter
String __name__() { return argParser.name; }
// plumbing ------------------------------------------------------
@@ -255,7 +232,7 @@ public TypeError typeError(ArgumentError ae, Object[] args, String[] names) {
* The implementation may have any signature allowed by
* {@link ArgParser}.
*/
- private static class General extends PyJavaMethod {
+ private static class General extends PyJavaFunction {
/**
* Construct a method object, identifying the implementation by a
@@ -302,7 +279,7 @@ public Object call(Object[] args, String[] names) throws TypeError, Throwable {
* @ImplNote Sub-classes must define {@link #call(Object[])}: the
* default definition in {@link FastCall} is not enough.
*/
- private static abstract class AbstractPositional extends PyJavaMethod {
+ private static abstract class AbstractPositional extends PyJavaFunction {
/** Default values of the trailing arguments. */
protected final Object[] d;
@@ -340,7 +317,22 @@ public Object call(Object[] args, String[] names) throws TypeError, Throwable {
public Object call(Object[] args) throws TypeError, Throwable {
// Make sure we find out if this is missing
throw new InterpreterError(
- "Sub-classes of AbstractPositional " + "must define call(Object[])");
+ "Sub-classes of AbstractPositional must define call(Object[])");
+ }
+
+ // Save some indirection by specialising to positional
+ @Override
+ Object __call__(Object[] args, String[] names) throws TypeError, Throwable {
+ try {
+ if (names == null || names.length == 0) {
+ // It is *not* worth unpacking the array here
+ return call(args);
+ } else {
+ throw new ArgumentError(Mode.NOKWARGS);
+ }
+ } catch (ArgumentError ae) {
+ throw typeError(ae, args, names);
+ }
}
}
@@ -498,7 +490,7 @@ public Object call(Object[] a) throws ArgumentError, TypeError, Throwable {
int n = a.length, k;
if (n == 3) {
// Number of arguments matches number of parameters
- return handle.invokeExact(a[0], a[1], a[3]);
+ return handle.invokeExact(a[0], a[1], a[2]);
} else if ((k = n - min) >= 0) {
if (n == 2) {
return handle.invokeExact(a[0], a[1], d[k]);
@@ -514,7 +506,7 @@ public Object call(Object[] a) throws ArgumentError, TypeError, Throwable {
@Override
public Object call() throws Throwable {
- if (min == 0) { return handle.invokeExact(d[0], d[1], d[3]); }
+ if (min == 0) { return handle.invokeExact(d[0], d[1], d[2]); }
throw new ArgumentError(min, max);
}
diff --git a/core/src/main/java/org/python/core/PyMethodDescr.java b/core/src/main/java/org/python/core/PyMethodDescr.java
index 867dbcfcc..7f5cf6244 100644
--- a/core/src/main/java/org/python/core/PyMethodDescr.java
+++ b/core/src/main/java/org/python/core/PyMethodDescr.java
@@ -17,7 +17,7 @@
* from Python. A {@code PyMethodDescr} is a callable object itself,
* and provides binding behaviour through
* {@link #__get__(Object, PyType) __get__}, which usually creates a
- * {@link PyJavaMethod}.
+ * {@link PyJavaFunction}.
*
* It suits us to sub-class {@code PyMethodDescr} to express the
* multiplicity of implementations and to respond to the signature
@@ -70,8 +70,8 @@ abstract class PyMethodDescr extends MethodDescriptor {
/**
* Deduced method signature (useful to have cached when constructing
- * a {@link PyJavaMethod}). Note that this is allowed to differ from
- * {@link MethodSignature#fromParser(ArgParser)
+ * a {@link PyJavaFunction}). Note that this is allowed to differ
+ * from {@link MethodSignature#fromParser(ArgParser)
* MethodSignature.fromParser(argParser)}.
*/
final MethodSignature signature;
@@ -376,7 +376,7 @@ Object simple__call__(Object[] args, String[] names) throws TypeError, Throwable
/**
* Return the described method, bound to {@code obj} as its "self"
* argument, or if {@code obj==null}, return this descriptor. In the
- * non-null case, {@code __get__} returns a {@link PyJavaMethod}.
+ * non-null case, {@code __get__} returns a {@link PyJavaFunction}.
* Calling the returned object invokes the same Java method as this
* descriptor, with {@code obj} as first argument, and other
* arguments to the call appended.
@@ -396,7 +396,7 @@ Object __get__(Object obj, PyType type) throws TypeError, Throwable {
else {
// Return a callable binding the method and the target
check(obj);
- return PyJavaMethod.from(this, obj);
+ return PyJavaFunction.from(this, obj);
}
}
diff --git a/core/src/main/java/org/python/core/PyModule.java b/core/src/main/java/org/python/core/PyModule.java
index b22c7f274..b660f51d5 100644
--- a/core/src/main/java/org/python/core/PyModule.java
+++ b/core/src/main/java/org/python/core/PyModule.java
@@ -1,13 +1,10 @@
+// Copyright (c)2023 Jython Developers.
+// Licensed to PSF under a contributor agreement.
package org.python.core;
import java.lang.invoke.MethodHandles;
-import java.util.Map;
-/**
- * The Python {@code module} object.
- *
- * Stop-gap implementation to satisfy use elsewhere in the project.
- */
+/** The Python {@code module} object. */
public class PyModule implements CraftedPyObject, DictPyObject {
/** The type of Python object this class implements. */
@@ -16,10 +13,10 @@ public class PyModule implements CraftedPyObject, DictPyObject {
protected final PyType type;
- /** Name of this module. **/
+ /** Name of this module. Not {@code null}. **/
final String name;
- /** Dictionary (globals) of this module. **/
+ /** Dictionary (globals) of this module. Not {@code null}. **/
final PyDict dict;
/**
@@ -42,12 +39,40 @@ public class PyModule implements CraftedPyObject, DictPyObject {
*/
PyModule(String name) { this(TYPE, name); }
+ /**
+ * Initialise the module instance. The main action will be to add
+ * entries to {@link #dict}. These become the members (globals) of
+ * the module.
+ */
+ void exec() {}
+
@Override
public PyType getType() { return type; }
+ /**
+ * The global dictionary of a module instance. This is always a
+ * Python {@code dict} and never {@code null}.
+ *
+ * @return The globals of this module
+ */
@Override
- public Map getDict() { return dict; }
+ public PyDict getDict() { return dict; }
@Override
public String toString() { return String.format("", name); }
+
+ /**
+ * Add a type by name to the dictionary.
+ *
+ * @param t the type
+ */
+ void add(PyType t) { dict.put(t.getName(), t); }
+
+ /**
+ * Add an object by name to the module dictionary.
+ *
+ * @param name to use as key
+ * @param o value for key
+ */
+ void add(String name, Object o) { dict.put(name, o); }
}
diff --git a/core/src/main/java/org/python/core/PyType.java b/core/src/main/java/org/python/core/PyType.java
index 1e0ac018c..a1ca89af5 100644
--- a/core/src/main/java/org/python/core/PyType.java
+++ b/core/src/main/java/org/python/core/PyType.java
@@ -16,6 +16,7 @@
import java.util.Map;
import org.python.base.InterpreterError;
+import org.python.core.Exposed.Getter;
import org.python.core.Slot.Signature;
/**
@@ -624,6 +625,7 @@ protected void updateAfterSetAttr(String name) {
*
* @return name of this type
*/
+ @Getter("__name__")
public String getName() { return name; }
/**
diff --git a/core/src/main/java/org/python/core/PyUnicode.java b/core/src/main/java/org/python/core/PyUnicode.java
index 84fdcf6cc..0751d4aeb 100644
--- a/core/src/main/java/org/python/core/PyUnicode.java
+++ b/core/src/main/java/org/python/core/PyUnicode.java
@@ -1,4 +1,4 @@
-// Copyright (c)2021 Jython Developers.
+// Copyright (c)2023 Jython Developers.
// Licensed to PSF under a contributor agreement.
package org.python.core;
@@ -289,7 +289,7 @@ final static PyObject new(PyNewWrapper new_, boolean init, PyType subtype,
private static Object __repr__(Object self) {
try {
// XXX make encode_UnicodeEscape (if needed) take a delegate
- return "u" + encode_UnicodeEscape(convertToString(self), true);
+ return encode_UnicodeEscape(convertToString(self), true);
} catch (NoConversion nc) {
throw Abstract.impossibleArgumentError("str", self);
}
diff --git a/core/src/test/java/org/python/core/ArgParserTest.java b/core/src/test/java/org/python/core/ArgParserTest.java
index b9bbd985f..fe0a00534 100644
--- a/core/src/test/java/org/python/core/ArgParserTest.java
+++ b/core/src/test/java/org/python/core/ArgParserTest.java
@@ -1,9 +1,13 @@
+// Copyright (c)2022 Jython Developers.
+// Licensed to PSF under a contributor agreement.
package org.python.core;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
+import java.util.List;
+
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
@@ -208,58 +212,105 @@ void throws_when_arg_missing() {
}
@Nested
- @DisplayName("Example 1 from the Javadoc")
- class FromJavadoc1 extends Standard {
-
- ArgParser ap = //
- new ArgParser("func", "aa", "kk", //
- 4, 3, //
- "a", "b", "c", "d", "e", "f", "g", "h", "i")//
- .defaults(3, 4, 5, 6) //
- .kwdefaults(77, null, 99);
- private String SIG = "func(a, b, c=3, d=4, /, e=5, f=6, *aa, g=77, h, i=99, **kk)";
+ @DisplayName("A parser for a positional collector")
+ class PositionalCollector extends Standard {
+
+ ArgParser ap = ArgParser.fromSignature("func", "*aa");
@Override
@Test
void has_expected_fields() {
assertEquals("func", ap.name);
- assertEquals(11, ap.argnames.length);
- assertEquals(6, ap.argcount);
- assertEquals(4, ap.posonlyargcount);
- assertEquals(3, ap.kwonlyargcount);
- assertEquals(9, ap.regargcount);
- assertEquals(9, ap.varArgsIndex);
- assertEquals(10, ap.varKeywordsIndex);
+ assertEquals(1, ap.argnames.length);
+ assertEquals(0, ap.argcount);
+ assertEquals(0, ap.posonlyargcount);
+ assertEquals(0, ap.kwonlyargcount);
+ assertEquals(0, ap.regargcount);
+ assertEquals(0, ap.varArgsIndex);
+ assertEquals(-1, ap.varKeywordsIndex);
}
@Override
@Test
void parses_classic_args() {
- PyTuple args = Py.tuple(10, 20, 30);
+ PyTuple args = Py.tuple(1, 2, 3);
PyDict kwargs = Py.dict();
- kwargs.put("g", 70);
- kwargs.put("h", 80);
- PyTuple expectedTuple = PyTuple.EMPTY;
- PyDict expectedDict = Py.dict();
- Object[] expected =
- new Object[] {10, 20, 30, 4, 5, 6, 70, 80, 99, expectedTuple, expectedDict};
+ Object[] frame = ap.parse(args, kwargs);
+ assertEquals(1, frame.length);
+ assertEquals(List.of(1, 2, 3), frame[0]);
+ }
+
+ @Test
+ void throws_on_keyword() {
+ PyTuple args = Py.tuple(1);
+ PyDict kwargs = Py.dict();
+ kwargs.put("c", 3);
+ assertThrows(TypeError.class, () -> ap.parse(args, kwargs));
+ }
+
+ @Override
+ @Test
+ void has_expected_toString() { assertEquals("func(*aa)", ap.toString()); }
+ }
+
+ @Nested
+ @DisplayName("A parser for a keyword collector")
+ class KeywordCollector extends Standard {
+
+ ArgParser ap = ArgParser.fromSignature("func", "**kk");
+
+ @Override
+ @Test
+ void has_expected_fields() {
+ assertEquals("func", ap.name);
+ assertEquals(1, ap.argnames.length);
+ assertEquals(0, ap.argcount);
+ assertEquals(0, ap.posonlyargcount);
+ assertEquals(0, ap.kwonlyargcount);
+ assertEquals(0, ap.regargcount);
+ assertEquals(-1, ap.varArgsIndex);
+ assertEquals(0, ap.varKeywordsIndex);
+ }
+
+ @Override
+ @Test
+ void parses_classic_args() {
+ PyTuple args = Py.tuple();
+ PyDict kwargs = Py.dict();
+ kwargs.put("b", 2);
+ kwargs.put("c", 3);
+ kwargs.put("a", 1);
Object[] frame = ap.parse(args, kwargs);
- assertArrayEquals(expected, frame);
+ assertEquals(1, frame.length);
+ PyDict kk = (PyDict)frame[0];
+ assertEquals(1, kk.get("a"));
+ assertEquals(2, kk.get("b"));
+ assertEquals(3, kk.get("c"));
+ }
+
+ @Test
+ void throws_on_positional() {
+ PyTuple args = Py.tuple(1);
+ PyDict kwargs = Py.dict();
+ kwargs.put("b", 2);
+ kwargs.put("c", 3);
+ kwargs.put("a", 1);
+ assertThrows(TypeError.class, () -> ap.parse(args, kwargs));
}
@Override
@Test
- void has_expected_toString() { assertEquals(SIG, ap.toString()); }
+ void has_expected_toString() { assertEquals("func(**kk)", ap.toString()); }
}
@Nested
- @DisplayName("Example 2 from the Javadoc")
- class FromJavadoc2 extends Standard {
+ @DisplayName("Example from the Javadoc")
+ class FromJavadoc extends Standard {
String[] names = {"a", "b", "c", "d", "e", "f", "g", "h", "i", "aa", "kk"};
- ArgParser ap = new ArgParser("func", true, true, 4, 3, names, names.length - 2) //
+ ArgParser ap = new ArgParser("func", names, names.length - 2, 4, 3, true, true) //
.defaults(3, 4, 5, 6) //
.kwdefaults(77, null, 99);
private String SIG = "func(a, b, c=3, d=4, /, e=5, f=6, *aa, g=77, h, i=99, **kk)";
diff --git a/core/src/test/java/org/python/core/BuiltinsModuleTest.java b/core/src/test/java/org/python/core/BuiltinsModuleTest.java
new file mode 100644
index 000000000..188417495
--- /dev/null
+++ b/core/src/test/java/org/python/core/BuiltinsModuleTest.java
@@ -0,0 +1,112 @@
+package org.python.core;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNotSame;
+import static org.junit.jupiter.api.Assertions.assertSame;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+
+/**
+ * This is a test of instantiating and using the {@code builtins}
+ * module, which has a special place in the Python interpreter as the
+ * name space. Many built-in types and functions are named there for use
+ * by the Python interpreter and it is effectively implicitly imported.
+ */
+@DisplayName("The builtins module")
+class BuiltinsModuleTest extends UnitTestSupport {
+
+ static final String FILE = "BuiltinsModuleTest.java";
+
+ @Test
+ @DisplayName("exists on an interepreter")
+ @SuppressWarnings("static-method")
+ void existsOnInterpreter() {
+ Interpreter interp = new Interpreter();
+ PyModule builtins = interp.builtinsModule;
+ assertNotNull(builtins);
+ }
+
+ @Test
+ @DisplayName("has independent instances")
+ @SuppressWarnings("static-method")
+ void canBeInstantiated() {
+ Interpreter interp1 = new Interpreter();
+ Interpreter interp2 = new Interpreter();
+ // Look up an arbitrary function in each interpreter
+ PyJavaFunction abs1 = (PyJavaFunction)interp1.getBuiltin("abs");
+ assertSame(abs1.self, interp1.builtinsModule);
+ PyJavaFunction abs2 = (PyJavaFunction)interp2.getBuiltin("abs");
+ assertSame(abs2.self, interp2.builtinsModule);
+ // Each module provides distinct function objects
+ assertNotSame(abs1, abs2);
+ // builtins module instances are distinct
+ assertNotSame(interp1.builtinsModule, interp2.builtinsModule);
+ }
+
+ @Nested
+ @DisplayName("provides expected function ...")
+ class TestFunctions {
+ Interpreter interp;
+ PyDict globals;
+ /* BuiltinsModule? */ PyModule builtins;
+
+ @BeforeEach
+ void setup() {
+ interp = new Interpreter();
+ globals = Py.dict();
+ builtins = interp.builtinsModule;
+ }
+
+
+ @Test
+ @DisplayName("abs")
+ void testAbs() throws Throwable {
+ Object f = Abstract.getAttr(builtins, "abs");
+ Object r = Callables.callFunction(f, -5.0);
+ assertEquals(5.0, r);
+ }
+
+
+ @Test
+ @DisplayName("len")
+ void testLen() throws Throwable {
+ Object f = Abstract.getAttr(builtins, "len");
+ Object r = Callables.callFunction(f, "hello");
+ assertEquals(5, r);
+ }
+
+ @Test
+ @DisplayName("max")
+ void testMax() throws Throwable {
+ Object f = Abstract.getAttr(builtins, "max");
+ Object r = Callables.callFunction(f, 4, 4.2, 5.0, 6);
+ assertEquals(6, r);
+ r = Callables.callFunction(f, Py.tuple(4, 4.2, 5.0, 6));
+ assertEquals(6, r);
+ }
+
+ @Test
+ @DisplayName("min")
+ void testMin() throws Throwable {
+ Object f = Abstract.getAttr(builtins, "min");
+ Object r = Callables.callFunction(f, 4, 5.0, 6, 4.2);
+ assertEquals(4, r);
+ r = Callables.callFunction(f, Py.tuple(4, 5.0, 6, 4.2));
+ assertEquals(4, r);
+ }
+
+ @Test
+ @DisplayName("repr")
+ void testRepr() throws Throwable {
+ Object f = Abstract.getAttr(builtins, "repr");
+ assertEquals("123", Callables.callFunction(f, 123));
+ assertEquals("'spam'", Callables.callFunction(f, "spam"));
+ // XXX implement None.__repr__
+ // assertEquals("None", Callables.callFunction(f, Py.None));
+ }
+ }
+}
diff --git a/core/src/test/java/org/python/core/CPython38CodeTest.java b/core/src/test/java/org/python/core/CPython38CodeTest.java
index 32063b249..52a20a621 100644
--- a/core/src/test/java/org/python/core/CPython38CodeTest.java
+++ b/core/src/test/java/org/python/core/CPython38CodeTest.java
@@ -1,3 +1,5 @@
+// Copyright (c)2023 Jython Developers.
+// Licensed to PSF under a contributor agreement.
package org.python.core;
import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -117,11 +119,14 @@ void co_consts() {
@SuppressWarnings("static-method")
@DisplayName("We can execute ...")
@ParameterizedTest(name = "{0}.py")
- @ValueSource(strings = {"load_store_name", "unary_op", "binary_op", "call_method_builtin"})
+ @ValueSource(strings = {"load_store_name", "unary_op", "binary_op", "call_method_builtin",
+ "builtins_module"})
void executeSimple(String name) {
CPython38Code code = readCode(name);
+ Interpreter interp = new Interpreter();
PyDict globals = new PyDict();
- code.createFrame(null, globals, globals).eval();
+ PyFrame> f = code.createFrame(interp, globals, globals);
+ f.eval();
assertExpectedVariables(readResultDict(name), globals);
}
diff --git a/core/src/test/java/org/python/core/ModuleExposerMethodTest.java b/core/src/test/java/org/python/core/ModuleExposerMethodTest.java
new file mode 100644
index 000000000..db73c9fbc
--- /dev/null
+++ b/core/src/test/java/org/python/core/ModuleExposerMethodTest.java
@@ -0,0 +1,686 @@
+// Copyright (c)2023 Jython Developers.
+// Licensed to PSF under a contributor agreement.
+package org.python.core;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import java.lang.invoke.MethodHandles;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+
+import org.python.core.Exposed.PositionalOnly;
+import org.python.core.Exposed.PythonMethod;
+import org.python.core.Exposed.PythonStaticMethod;
+import org.python.base.MethodKind;
+
+/**
+ * Test that functions exposed by a Python module defined in
+ * Java, using the scheme of annotations defined in {@link Exposed},
+ * result in {@link PyJavaFunction} objects with characteristics
+ * that correspond to the definition.
+ *
+ * The first test in each case is to examine the fields in the
+ * parser that attaches to the {@link ModuleDef.MethodDef}. Then we
+ * call the function using the {@code __call__} special method, and
+ * using our "Java call" signatures.
+ *
+ * There is a nested test suite for each signature pattern.
+ */
+@DisplayName("A method exposed by a module")
+class ModuleExposerMethodTest {
+
+ /**
+ * Nested test classes implement these as standard. A base class
+ * here is just a way to describe the tests once that we repeat in
+ * each nested case.
+ */
+ abstract static class Standard {
+
+ // Working variables for the tests
+ /** The module we create. */
+ final PyModule module;
+ /** The function to examine or call. */
+ PyJavaFunction func;
+ /** The parser in the function we examine. */
+ ArgParser ap;
+ /** The expected result of calling the function */
+ Object[] exp;
+
+ Standard() {
+ this.module = new ExampleModule();
+ this.module.exec();
+ }
+
+ /**
+ * A parser attached to the function object should have field values
+ * that correctly reflect the signature and annotations in the
+ * defining class.
+ */
+ abstract void has_expected_fields();
+
+ /**
+ * Call the function using the {@code __call__} special method with
+ * arguments correct for the function's specification. The function
+ * should obtain the correct result (and not throw).
+ *
+ * @throws Throwable unexpectedly
+ */
+ abstract void supports__call__() throws Throwable;
+
+ /**
+ * Call the function using the {@code __call__} special method with
+ * arguments correct for the function's specification, and
+ * explicitly zero or more keywords. The function should obtain the
+ * correct result (and not throw).
+ *
+ * @throws Throwable unexpectedly
+ */
+ abstract void supports_keywords() throws Throwable;
+
+ /**
+ * Call the function using the {@code __call__} special method and
+ * an unexpected keyword: where none is expected, for a positional
+ * argument, or simply an unacceptable name. The function should
+ * throw {@link TypeError}.
+ *
+ * @throws Throwable unexpectedly
+ */
+ abstract void raises_TypeError_on_unexpected_keyword() throws Throwable;
+
+ /**
+ * Call the function using the Java call interface with arguments
+ * correct for the function's specification. The function should
+ * obtain the correct result (and not throw).
+ *
+ * @throws Throwable unexpectedly
+ */
+ abstract void supports_java_call() throws Throwable;
+
+ /**
+ * Check that the fields of the parser match expectations for a
+ * method with no collector parameters and a certain number of
+ * positional-only parameters.
+ *
+ * @param kind static or instance
+ * @param name of method
+ * @param count of parameters
+ * @param posonlycount count of positional-only parameters
+ */
+ void no_collector(MethodKind kind, String name, int count, int posonlycount) {
+ assertEquals(name, ap.name);
+ assertEquals(kind, ap.methodKind);
+ assertEquals(count, ap.argnames.length);
+ assertEquals(count, ap.argcount);
+ assertEquals(posonlycount, ap.posonlyargcount);
+ assertEquals(0, ap.kwonlyargcount);
+ assertEquals(count, ap.regargcount);
+ assertEquals(-1, ap.varArgsIndex);
+ assertEquals(-1, ap.varKeywordsIndex);
+ }
+
+ /**
+ * Check that the fields of the parser match expectations for a
+ * static method with no collector parameters and a certain number
+ * of positional-only parameters.
+ *
+ * @param name of method
+ * @param count of parameters
+ * @param posonly count of positional-only parameters
+ */
+ void no_collector_static(String name, int count, int posonly) {
+ no_collector(MethodKind.STATIC, name, count, posonly);
+ }
+
+ /**
+ * Check that the fields of the parser match expectations for a
+ * instance method with no collector parameters and a certain number
+ * of positional-only parameters.
+ *
+ * @param name of method
+ * @param count of parameters
+ * @param posonly count of positional-only parameters
+ */
+ void no_collector_instance(String name, int count, int posonly) {
+ no_collector(MethodKind.INSTANCE, name, count, posonly);
+ }
+
+ /**
+ * Check the result of a call against {@link #exp}. The reference
+ * rtesult is the same throughout a given sub-class test.
+ *
+ * @param result of call
+ */
+ void check_result(PyTuple result) { assertArrayEquals(exp, result.value); }
+ }
+
+ /**
+ * A Python module definition that exhibits a range of method
+ * signatures explored in the tests.
+ */
+ static class ExampleModule extends JavaModule {
+
+ static final ModuleDef DEF = new ModuleDef("example", MethodHandles.lookup());
+
+ ExampleModule() { super(DEF); }
+
+ /**
+ * See {@link StaticNoParams}: no parameters are allowed.
+ */
+ @PythonStaticMethod
+ static void f0() {}
+
+ /**
+ * See {@link NoParams}: no parameters are allowed.
+ */
+ @PythonMethod
+ void m0() {}
+
+ /**
+ * See {@link StaticOneParam}: the parameter is positional-only as a
+ * result of the default exposure.
+ *
+ * @param a positional arg
+ * @return the arg (tuple)
+ */
+ @PythonStaticMethod
+ static PyTuple f1(double a) { return Py.tuple(a); }
+
+ /**
+ * See {@link OneParam}: the parameter is positional-only as a
+ * result of the default exposure.
+ *
+ * @param a positional arg
+ * @return the arg (tuple)
+ */
+ @PythonMethod
+ PyTuple m1(double a) { return Py.tuple(this, a); }
+
+ /**
+ * See {@link StaticDefaultPositionalParams}: the parameters are
+ * positional-only as a result of the default exposure.
+ *
+ * @param a positional arg
+ * @param b positional arg
+ * @param c positional arg
+ * @return the args
+ */
+ @PythonStaticMethod
+ static PyTuple f3(int a, String b, Object c) { return Py.tuple(a, b, c); }
+
+ /**
+ * See {@link DefaultPositionalParams}: the parameters are
+ * positional-only as a result of the default exposure.
+ *
+ * @param a positional arg
+ * @param b positional arg
+ * @param c positional arg
+ * @return the args
+ */
+ @PythonMethod
+ PyTuple m3(int a, String b, Object c) { return Py.tuple(this, a, b, c); }
+
+ /**
+ * See {@link StaticPositionalOrKeywordParams}: the parameters are
+ * positional-or-keyword but none are positional-only.
+ *
+ * @param a positional-or-keyword arg
+ * @param b positional-or-keyword arg
+ * @param c positional-or-keyword arg
+ * @return the args
+ */
+ @PythonStaticMethod(positionalOnly = false)
+ static PyTuple f3pk(int a, String b, Object c) { return Py.tuple(a, b, c); }
+
+ /**
+ * See {@link PositionalOrKeywordParams}: the parameters are
+ * positional-or-keyword but none are positional-only.
+ *
+ * @param a positional-or-keyword arg
+ * @param b positional-or-keyword arg
+ * @param c positional-or-keyword arg
+ * @return the args
+ */
+ @PythonMethod(positionalOnly = false)
+ PyTuple m3pk(int a, String b, Object c) { return Py.tuple(this, a, b, c); }
+
+ /**
+ * See {@link SomePositionalOnlyParams}: two parameters are
+ * positional-only as a result of an annotation.
+ *
+ * @param a positional arg
+ * @param b positional arg
+ * @param c positional-or-keyword arg
+ * @return the args
+ */
+ @PythonStaticMethod
+ static PyTuple f3p2(int a, @PositionalOnly String b, Object c) { return Py.tuple(a, b, c); }
+
+ /**
+ * See {@link StaticSomePositionalOnlyParams}: two parameters are
+ * positional-only as a result of an annotation.
+ *
+ * @param a positional arg
+ * @param b positional arg
+ * @param c positional-or-keyword arg
+ * @return the args
+ */
+ @PythonMethod
+ PyTuple m3p2(int a, @PositionalOnly String b, Object c) { return Py.tuple(this, a, b, c); }
+ }
+
+ /** {@link ExampleModule#m0()} accepts no arguments. */
+ @Nested
+ @DisplayName("with no parameters")
+ class NoParams extends Standard {
+
+ @BeforeEach
+ void setup() throws AttributeError, Throwable {
+ // func = module.m0
+ func = (PyJavaFunction)Abstract.getAttr(module, "m0");
+ ap = func.argParser;
+ }
+
+ @Override
+ @Test
+ void has_expected_fields() { no_collector_instance("m0", 0, 0); }
+
+ @Override
+ @Test
+ void supports__call__() throws Throwable {
+ // We call func()
+ Object[] args = {};
+
+ // The method is declared void (which means return None)
+ Object r = func.__call__(args, null);
+ assertEquals(Py.None, r);
+ }
+
+ /** Keywords must be empty. */
+ @Override
+ @Test
+ void supports_keywords() throws Throwable {
+ // We call func()
+ Object[] args = {};
+ String[] names = {};
+
+ // The method is declared void (which means return None)
+ Object r = func.__call__(args, names);
+ assertEquals(Py.None, r);
+ }
+
+ @Override
+ @Test
+ void raises_TypeError_on_unexpected_keyword() {
+ // We call func(c=3)
+ Object[] args = {3};
+ String[] names = {"c"}; // Nothing expected
+
+ assertThrows(TypeError.class, () -> func.__call__(args, names));
+ }
+
+ @Override
+ @Test
+ void supports_java_call() throws Throwable {
+ // We call func()
+ // The method is declared void (which means return None)
+ Object r = func.call();
+ assertEquals(Py.None, r);
+ }
+ }
+
+ /** {@link ExampleModule#f0()} accepts no arguments. */
+ @Nested
+ @DisplayName("static, with no parameters")
+ class StaticNoParams extends NoParams {
+
+ @Override
+ @BeforeEach
+ void setup() throws AttributeError, Throwable {
+ // func = module.f0
+ func = (PyJavaFunction)Abstract.getAttr(module, "f0");
+ ap = func.argParser;
+ }
+
+ @Override
+ @Test
+ void has_expected_fields() { no_collector_static("f0", 0, 0); }
+ }
+
+ /**
+ * {@link ExampleModule#m1(double)} accepts one argument that
+ * must be given by position.
+ */
+ @Nested
+ @DisplayName("with one positional-only parameter")
+ class OneParam extends Standard {
+
+ @BeforeEach
+ void setup() throws AttributeError, Throwable {
+ // func = module.m1
+ func = (PyJavaFunction)Abstract.getAttr(module, "m1");
+ ap = func.argParser;
+ exp = new Object[] {module, 42.0};
+ }
+
+ @Override
+ @Test
+ void has_expected_fields() { no_collector_instance("m1", 1, 1); }
+
+ @Override
+ @Test
+ void supports__call__() throws Throwable {
+ // We call func(42.0)
+ Object[] args = {42.0};
+ // The method reports its arguments as a tuple
+ PyTuple r = (PyTuple)func.__call__(args, null);
+ check_result(r);
+ }
+
+ @Override
+ @Test
+ void supports_keywords() throws Throwable {
+ // We call func(42.0)
+ Object[] args = {42.0};
+ String[] names = {};
+ // The method reports its arguments as a tuple
+ PyTuple r = (PyTuple)func.__call__(args, names);
+ check_result(r);
+ }
+
+ @Override
+ @Test
+ void raises_TypeError_on_unexpected_keyword() {
+ // We call func(42.0, a=5)
+ Object[] args = {42.0, 5};
+ String[] names = {"a"};
+
+ assertThrows(TypeError.class, () -> func.__call__(args, names));
+ }
+
+ @Override
+ @Test
+ void supports_java_call() throws Throwable {
+ // We call func(42.0)
+ PyTuple r = (PyTuple)func.call(42.0);
+ check_result(r);
+ }
+ }
+
+ /**
+ * {@link ExampleModule#f1(double)} accepts one argument that
+ * must be given by position.
+ */
+ @Nested
+ @DisplayName("static, with one positional-only parameter")
+ class StaticOneParam extends OneParam {
+
+ @Override
+ @BeforeEach
+ void setup() throws AttributeError, Throwable {
+ // func = module.f1
+ func = (PyJavaFunction)Abstract.getAttr(module, "f1");
+ ap = func.argParser;
+ exp = new Object[] {42.0};
+ }
+
+ @Override
+ @Test
+ void has_expected_fields() { no_collector_static("f1", 1, 1); }
+ }
+
+ /**
+ * {@link ExampleModule#m3(int, String, Object)} accepts 3 arguments
+ * that must be given by position.
+ */
+ @Nested
+ @DisplayName("with positional-only parameters by default")
+ class DefaultPositionalParams extends Standard {
+
+ @BeforeEach
+ void setup() throws AttributeError, Throwable {
+ // func = module.m3
+ func = (PyJavaFunction)Abstract.getAttr(module, "m3");
+ ap = func.argParser;
+ exp = new Object[] {module, 1, "2", 3};
+ }
+
+ @Override
+ @Test
+ void has_expected_fields() { no_collector_instance("m3", 3, 3); }
+
+ @Override
+ @Test
+ void supports__call__() throws Throwable {
+ // We call func(1, '2', 3)
+ Object[] args = {1, "2", 3};
+ // The method reports its arguments as a tuple
+ PyTuple r = (PyTuple)func.__call__(args, null);
+ check_result(r);
+ }
+
+ @Override
+ @Test
+ void supports_keywords() throws Throwable {
+ // We call func(1, '2', 3)
+ Object[] args = {1, "2", 3};
+ String[] names = {};
+ // The method reports its arguments as a tuple
+ PyTuple r = (PyTuple)func.__call__(args, names);
+ check_result(r);
+ }
+
+ @Override
+ @Test
+ void raises_TypeError_on_unexpected_keyword() {
+ // We call func(1, '2', c=3)
+ Object[] args = {1, "2", 3};
+ String[] names = {"c"};
+
+ assertThrows(TypeError.class, () -> func.__call__(args, names));
+ }
+
+ @Override
+ @Test
+ void supports_java_call() throws Throwable {
+ // We call func(1, '2', 3)
+ PyTuple r = (PyTuple)func.call(1, "2", 3);
+ check_result(r);
+ }
+ }
+
+ /**
+ * {@link ExampleModule#f3(int, String, Object)} accepts 3 arguments
+ * that must be given by position.
+ */
+ @Nested
+ @DisplayName("static, with positional-only parameters by default")
+ class StaticDefaultPositionalParams extends DefaultPositionalParams {
+
+ @Override
+ @BeforeEach
+ void setup() throws AttributeError, Throwable {
+ // func = module.f3
+ func = (PyJavaFunction)Abstract.getAttr(module, "f3");
+ ap = func.argParser;
+ exp = new Object[] {1, "2", 3};
+ }
+
+ @Override
+ @Test
+ void has_expected_fields() { no_collector_static("f3", 3, 3); }
+ }
+
+ /**
+ * {@link ExampleModule#m3pk(int, String, Object)} accepts 3
+ * arguments that may be given by position or keyword.
+ */
+ @Nested
+ @DisplayName("with positional-or-keyword parameters")
+ class PositionalOrKeywordParams extends Standard {
+
+ @BeforeEach
+ void setup() throws AttributeError, Throwable {
+ // func = module.m3pk
+ func = (PyJavaFunction)Abstract.getAttr(module, "m3pk");
+ ap = func.argParser;
+ exp = new Object[] {module, 1, "2", 3};
+ }
+
+ @Override
+ @Test
+ void has_expected_fields() { no_collector_instance("m3pk", 3, 0); }
+
+ @Override
+ @Test
+ void supports__call__() throws Throwable {
+ // We call func(1, '2', 3)
+ Object[] args = {1, "2", 3};
+ String[] names = {};
+ PyTuple r = (PyTuple)func.__call__(args, names);
+ check_result(r);
+ }
+
+ /** Supply second and third arguments by keyword. */
+ @Override
+ @Test
+ void supports_keywords() throws Throwable {
+ // We call func(1, c=3, b='2')
+ Object[] args = {1, 3, "2"};
+ String[] names = {"c", "b"};
+ PyTuple r = (PyTuple)func.__call__(args, names);
+ check_result(r);
+ }
+
+ /** Get the wrong keyword. */
+ @Override
+ @Test
+ void raises_TypeError_on_unexpected_keyword() throws Throwable {
+ // We call func(1, c=3, b='2', x=4)
+ Object[] args = {1, 3, "2", 4};
+ String[] names = {"c", "b", /* unknown */"x"};
+ assertThrows(TypeError.class, () -> func.__call__(args, names));
+ }
+
+ @Override
+ @Test
+ void supports_java_call() throws Throwable {
+ PyTuple r = (PyTuple)func.call(1, "2", 3);
+ check_result(r);
+ }
+ }
+
+ /**
+ * {@link ExampleModule#f3pk(int, String, Object)} accepts 3
+ * arguments that may be given by position or keyword.
+ */
+ @Nested
+ @DisplayName("static, with positional-or-keyword parameters")
+ class StaticPositionalOrKeywordParams extends PositionalOrKeywordParams {
+
+ @Override
+ @BeforeEach
+ void setup() throws AttributeError, Throwable {
+ // func = module.f3pk
+ func = (PyJavaFunction)Abstract.getAttr(module, "f3pk");
+ ap = func.argParser;
+ exp = new Object[] {1, "2", 3};
+ }
+
+ @Override
+ @Test
+ void has_expected_fields() { no_collector_static("f3pk", 3, 0); }
+
+ }
+
+ /**
+ * {@link ExampleModule#m3p2(int, String, Object)} accepts 3
+ * arguments, two of which may be given by position only, and the
+ * last by either position or keyword.
+ */
+ @Nested
+ @DisplayName("with two positional-only parameters")
+ class SomePositionalOnlyParams extends Standard {
+
+ @BeforeEach
+ void setup() throws AttributeError, Throwable {
+ // func = module.m3p2
+ func = (PyJavaFunction)Abstract.getAttr(module, "m3p2");
+ ap = func.argParser;
+ exp = new Object[] {module, 1, "2", 3};
+ }
+
+ @Override
+ @Test
+ void has_expected_fields() { no_collector_instance("m3p2", 3, 2); }
+
+ @Override
+ @Test
+ void supports__call__() throws Throwable {
+ // We call func(1, '2', 3)
+ Object[] args = {1, "2", 3};
+ String[] names = {};
+
+ // The method just parrots its arguments as a tuple
+ PyTuple r = (PyTuple)func.__call__(args, names);
+ check_result(r);
+ }
+
+ /** Supply third argument by keyword. */
+ @Override
+ @Test
+ void supports_keywords() throws Throwable {
+ // We call func(1, '2', c=3)
+ Object[] args = {1, "2", 3};
+ String[] names = {"c"};
+
+ // The method reports its arguments as a tuple
+ PyTuple r = (PyTuple)func.__call__(args, names);
+ check_result(r);
+ }
+
+ @Override
+ @Test
+ void raises_TypeError_on_unexpected_keyword() throws Throwable {
+ // We call func(1, c=3, b='2')
+ Object[] args = {1, 3, "2"};
+ String[] names = {"c", /* positional */"b"};
+ assertThrows(TypeError.class, () -> func.__call__(args, names));
+ }
+
+ @Override
+ @Test
+ void supports_java_call() throws Throwable {
+ // The method reports its arguments as a tuple
+ PyTuple r = (PyTuple)func.call(1, "2", 3);
+ check_result(r);
+ }
+ }
+
+ /**
+ * {@link ExampleModule#f3p2(int, String, Object)} accepts 3
+ * arguments, two of which may be given by position only, and the
+ * last by either position or keyword.
+ */
+ @Nested
+ @DisplayName("static, with two positional-only parameters")
+ class StaticSomePositionalOnlyParams extends SomePositionalOnlyParams {
+
+ @Override
+ @BeforeEach
+ void setup() throws AttributeError, Throwable {
+ // func = module.f3p2
+ func = (PyJavaFunction)Abstract.getAttr(module, "f3p2");
+ ap = func.argParser;
+ exp = new Object[] {1, "2", 3};
+ }
+
+ @Override
+ @Test
+ void has_expected_fields() { no_collector_static("f3p2", 3, 2); }
+ }
+}
diff --git a/core/src/test/java/org/python/core/ModuleExposerTest.java b/core/src/test/java/org/python/core/ModuleExposerTest.java
new file mode 100644
index 000000000..dc1713965
--- /dev/null
+++ b/core/src/test/java/org/python/core/ModuleExposerTest.java
@@ -0,0 +1,220 @@
+// Copyright (c)2022 Jython Developers.
+// Licensed to PSF under a contributor agreement.
+package org.python.core;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.lang.invoke.MethodHandle;
+import java.lang.invoke.MethodHandles;
+import java.lang.invoke.MethodHandles.Lookup;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeMap;
+import java.util.TreeSet;
+
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+
+import org.python.core.Exposed.PositionalOnly;
+import org.python.core.Exposed.PythonMethod;
+import org.python.core.Exposed.PythonStaticMethod;
+import org.python.core.ModuleDef.MethodDef;
+
+/**
+ * Test that a Python module defined in Java, using the scheme of
+ * annotations defined in {@link Exposed}, can be processed correctly by
+ * a {@link Exposer} to a {@link ModuleDef}. This tests a large part of
+ * the exposure mechanism.
+ *
+ * The class used in the test {@link FakeModule} is not actually a
+ * {@link PyModule}, but we go through the actions of the
+ * {@link ModuleExposer} so we can examine the intermediate results.
+ */
+@DisplayName("For a module exposed from a Java definition")
+class ModuleExposerTest extends UnitTestSupport {
+
+ /**
+ * This class is not actually a Python module definition, but is
+ * annotated as if it were. We will test whether the
+ * {@link MethodDef}s are created as expected. We'll also act on it
+ * to produce a dictionary as if it were a real module.
+ */
+ static class FakeModule {
+
+ static final Lookup LOOKUP = MethodHandles.lookup();
+
+ // Signature: ()
+ @PythonStaticMethod
+ static void f0() {}
+
+ // Signature: ($module, /)
+ @PythonMethod
+ void m0() {}
+
+ // Signature: (a)
+ @PythonStaticMethod
+ static PyTuple f1(double a) {return Py.tuple(a);}
+
+ // Signature: ($module, a, /)
+ @PythonMethod
+ @SuppressWarnings("static-method")
+ PyTuple m1(double a) {return Py.tuple(a);}
+
+ // Signature: (a, b, c, /)
+ @PythonStaticMethod
+ static PyTuple f3(int a, String b, Object c) {
+ return Py.tuple(a, b, c);
+ }
+
+ // Signature: ($module, a, b, c, /)
+ @PythonMethod
+ @SuppressWarnings("static-method")
+ PyTuple m3(int a, String b, Object c) {
+ return Py.tuple(a, b, c);
+ }
+
+ // Signature: (/, a, b, c)
+ @PythonStaticMethod(positionalOnly = false)
+ static PyTuple f3pk(int a, String b, Object c) {
+ return Py.tuple(a, b, c);
+ }
+
+ // Signature: ($module, /, a, b, c)
+ @PythonMethod(positionalOnly = false)
+ @SuppressWarnings("static-method")
+ PyTuple m3pk(int a, String b, Object c) {
+ return Py.tuple(a, b, c);
+ }
+
+ // Signature: (a, b, /, c)
+ @PythonStaticMethod
+ static PyTuple f3p2(int a, @PositionalOnly String b, Object c) {
+ return Py.tuple(a, b, c);
+ }
+
+ // Signature: ($module, a, b, /, c)
+ @PythonMethod
+ @SuppressWarnings("static-method")
+ PyTuple m3p2(int a, @PositionalOnly String b, Object c) {
+ return Py.tuple(a, b, c);
+ }
+ }
+
+ @Nested
+ @DisplayName("calling the Exposer")
+ class TestExposer {
+
+ @Test
+ @DisplayName("produces a ModuleExposer")
+ void getExposer() {
+ ModuleExposer exposer =
+ Exposer.exposeModule(FakeModule.class);
+ assertNotNull(exposer);
+ }
+
+ @Test
+ @DisplayName("finds the expected methods")
+ void getMethodDefs() {
+ ModuleExposer exposer =
+ Exposer.exposeModule(FakeModule.class);
+ MethodDef[] mdArray =
+ exposer.getMethodDefs(FakeModule.LOOKUP);
+ checkMethodDefArray(mdArray);
+ }
+ }
+
+ @Nested
+ @DisplayName("constructing a ModuleDef")
+ class TestDefinition {
+
+ @Test
+ @DisplayName("produces a MethodDef array")
+ void createMethodDef() {
+ ModuleDef def = new ModuleDef("example", FakeModule.LOOKUP);
+ checkMethodDefArray(def.getMethods());
+ }
+ }
+
+ @Nested
+ @DisplayName("a module instance")
+ class TestInstance {
+
+ @Test
+ @DisplayName("has expected method signatures")
+ void hasMethods() {
+ /*
+ * As FakeModule is not a PyModule, we must work a bit
+ * harder to take care of things normally automatic. Make a
+ * ModuleDef to hold the MethodDefs from the Exposer.
+ */
+ ModuleDef def = new ModuleDef("example", FakeModule.LOOKUP);
+ // An instance of the "module" to bind in PyJavaMethods
+ FakeModule fake = new FakeModule();
+ // A map to stand in for the module dictionary to hold them
+ Map dict = new HashMap<>();
+ // Which we now fill ...
+ for (MethodDef md : def.getMethods()) {
+ ArgParser ap = md.argParser;
+ MethodHandle mh = md.handle;
+ PyJavaFunction m =
+ PyJavaFunction.fromParser(ap, mh, fake, def.name);
+ dict.put(md.argParser.name, m);
+ }
+ // And here we check what's in it
+ checkMethodSignatures(dict);
+ }
+ }
+
+ private static void checkMethodDefArray(MethodDef[] defs) {
+ assertNotNull(defs);
+
+ Map mds = new TreeMap<>();
+ for (MethodDef def : defs) { mds.put(def.argParser.name, def); }
+
+ Set expected = new TreeSet<>();
+ expected.addAll(List.of( //
+ "f0", "f1", "f3", "f3pk", "f3p2", //
+ "m0", "m1", "m3", "m3pk", "m3p2"));
+
+ assertEquals(expected, mds.keySet(), "contains expected names");
+ }
+
+ private static void
+ checkMethodSignatures(Map dict) {
+ assertNotNull(dict);
+
+ checkSignature(dict, "f0()");
+ checkSignature(dict, "m0($module, /)");
+ checkSignature(dict, "f1(a, /)");
+ checkSignature(dict, "m1($module, a, /)");
+ checkSignature(dict, "f3(a, b, c, /)");
+ checkSignature(dict, "m3($module, a, b, c, /)");
+ checkSignature(dict, "f3pk(a, b, c)");
+ checkSignature(dict, "m3pk($module, /, a, b, c)");
+ checkSignature(dict, "f3p2(a, b, /, c)");
+ checkSignature(dict, "m3p2($module, a, b, /, c)");
+ }
+
+ /**
+ * Check that a method with the expected signature is in the
+ * dictionary.
+ *
+ * @param dict dictionary
+ * @param spec signature
+ */
+ private static void checkSignature(Map dict,
+ String spec) {
+ int k = spec.indexOf('(');
+ assertTrue(k > 0);
+ String name = spec.substring(0, k);
+ String expect = spec.substring(k);
+ PyJavaFunction pjf = (PyJavaFunction)dict.get(name);
+ assertEquals(expect, pjf.argParser.textSignature());
+ }
+
+}
diff --git a/core/src/test/java/org/python/core/TypeExposerMethodTest.java b/core/src/test/java/org/python/core/TypeExposerMethodTest.java
index f19986c3a..d06c6cbcf 100644
--- a/core/src/test/java/org/python/core/TypeExposerMethodTest.java
+++ b/core/src/test/java/org/python/core/TypeExposerMethodTest.java
@@ -46,7 +46,7 @@ abstract static class Standard {
/** The object on which to invoke the method. */
Object obj;
/** The function to examine or call (bound to {@code obj}). */
- PyJavaMethod func;
+ PyJavaFunction func;
/** The parser we examine. */
ArgParser ap;
/** The expected result of calling the method. */
@@ -157,7 +157,7 @@ void setup(String name, Object o) throws AttributeError, Throwable {
descr = (PyMethodDescr)PyType.of(o).lookup(name);
ap = descr.argParser;
obj = o;
- func = (PyJavaMethod)Abstract.getAttr(obj, name);
+ func = (PyJavaFunction)Abstract.getAttr(obj, name);
}
/**
diff --git a/core/src/test/pythonExample/builtins_module.py b/core/src/test/pythonExample/builtins_module.py
new file mode 100644
index 000000000..043601492
--- /dev/null
+++ b/core/src/test/pythonExample/builtins_module.py
@@ -0,0 +1,28 @@
+# builtins_module.py
+#
+# The focus of this test is the way the interpreter resolves names
+# in the builtins dictionary (after local and global namespaces).
+# This happens in opcodes LOAD_NAME and LOAD_GLOBAL.
+
+# Access sample objects from the builtins module implicitly
+# Opcode is LOAD_NAME
+
+int_name = int.__name__
+max_name = max.__name__
+
+# Call functions to prove we can
+# Opcode is LOAD_NAME
+ai = abs(-42)
+af = abs(-41.9)
+
+
+# Sometimes __builtins__ is not the builtins module. Find it with:
+bi = max.__self__
+
+# Check explicit attribute access to the (real) builtins module
+bi_int_name = bi.int.__name__
+bi_max_name = bi.max.__name__
+
+
+# Not marshallable
+del bi