Commits

masklinn committed d6d0e7b

Add variadics to parseArgs, move utils docs around, add doc for PY_call

Comments (0)

Files changed (6)

     :maxdepth: 2
 
     types
+    utility
     differences
 
 Usage
               evaluation
     :rtype: :class:`py.object`
 
-Utility functions
-+++++++++++++++++
-
-These are functions provided to implement ``py`` objects which can be
-used within a ``py.js`` evaluation, they're essentially ``py.js``'s
-version of the Python C API. They are prefixed with ``PY_``
-
-.. function:: py.PY_parseArgs(arguments, format)
-
-    Arguments parser converting from the :ref:`user-defined calling
-    conventions <types-methods-python-call>` to a JS object mapping
-    argument names to values. It serves the same role as
-    `PyArg_ParseTupleAndKeywords`_.
-
-    :param arguments: array-like objects holding the args and kwargs
-                      passed to the callable, generally the
-                      ``arguments`` of the caller.
-
-    :param format: mapping declaration to the actual arguments of the
-                   function. A javascript array composed of three
-                   possible types of elements:
-
-                   * The literal string ``'*'`` marks all following
-                     parameters as keyword-only, regardless of them
-                     having a default value or not [#kwonly]_
-
-                   * A string defines a required parameter, accessible
-                     positionally or through keyword
-
-                   * A pair of ``[String, py.object]`` defines an
-                     optional parameter and its default value.
-
-    :returns: a javascript object mapping argument names to values
-
-    :raises: ``TypeError`` if the provided arguments don't match the
-             format
-
-.. class:: py.PY_def(fn)
-
-    Type wrapping javascript functions into py.js callables. The
-    wrapped function follows :ref:`the py.js calling conventions
-    <types-methods-python-call>`
-
-    :param Function fn: the javascript function to wrap
-    :returns: a callable py.js object
-
 Implementation details
 ----------------------
 
 accessing an invalid key on a ``dict``, ``py.js`` will throw
 ``Error("KeyError: 'foo'")``.
 
-.. [#kwonly] Python 2, which py.js currently implements, does not
-             support Python-level keyword-only parameters (it can be
-             done through the C-API), but it seemed neat and easy
-             enough so there.
-
 .. _Python Data Model: http://docs.python.org/reference/datamodel.html
 
-.. _PyArg_ParseTupleAndKeywords:
-    http://docs.python.org/c-api/arg.html#PyArg_ParseTupleAndKeywords
+Utility functions for interacting with ``py.js`` objects
+========================================================
+
+Essentially the ``py.js`` version of the Python C API, these functions
+are used to implement new ``py.js`` types or to interact with existing
+ones.
+
+They are prefixed with ``PY_``.
+
+.. function:: py.PY_call(callable[, args][, kwargs])
+
+    Call an arbitrary python-level callable from javascript.
+
+    :param callable: A ``py.js`` callable object (broadly speaking,
+                     either a class or an object with a ``__call__``
+                     method)
+
+    :param args: javascript Array of :class:`py.object`, used as
+                 positional arguments to ``callable``
+
+    :param kwargs: javascript Object mapping names to
+                   :class:`py.object`, used as named arguments to
+                   ``callable``
+
+    :returns: nothing or :class:`py.object`
+
+.. function:: py.PY_parseArgs(arguments, format)
+
+    Arguments parser converting from the :ref:`user-defined calling
+    conventions <types-methods-python-call>` to a JS object mapping
+    argument names to values. It serves the same role as
+    `PyArg_ParseTupleAndKeywords`_.
+
+    ::
+
+        var args = py.PY_parseArgs(
+            arguments, ['foo', 'bar', ['baz', 3], ['qux', "foo"]]);
+
+    roughly corresponds to the argument spec:
+
+    .. code-block:: python
+
+        def func(foo, bar, baz=3, qux="foo"):
+            pass
+
+    .. note:: a significant difference is that "default values" will
+              be re-evaluated at each call, since they are within the
+              function.
+
+    :param arguments: array-like objects holding the args and kwargs
+                      passed to the callable, generally the
+                      ``arguments`` of the caller.
+
+    :param format: mapping declaration to the actual arguments of the
+                   function. A javascript array composed of five
+                   possible types of elements:
+
+                   * The literal string ``'*'`` marks all following
+                     parameters as keyword-only, regardless of them
+                     having a default value or not [#kwonly]_. Can
+                     only be present once in the parameters list.
+
+                   * A string prefixed by ``*``, marks the positional
+                     variadic parameter for the function: gathers all
+                     provided positional arguments left and makes all
+                     following parameters keyword-only
+                     [#star-args]_. ``*args`` is incompatible with
+                     ``*``.
+
+                   * A string prefixed with ``**``, marks the
+                     positional keyword variadic parameter for the
+                     function: gathers all provided keyword arguments
+                     left and closes the argslist. If present, this
+                     must be the last parameter of the format list.
+
+                   * A string defines a required parameter, accessible
+                     positionally or through keyword
+
+                   * A pair of ``[String, py.object]`` defines an
+                     optional parameter and its default value.
+
+                   For simplicity, when not using optional parameters
+                   it is possible to use a simple string as the format
+                   (using space-separated elements). The string will
+                   be split on whitespace and processed as a normal
+                   format array.
+
+    :returns: a javascript object mapping argument names to values
+
+    :raises: ``TypeError`` if the provided arguments don't match the
+             format
+
+.. class:: py.PY_def(fn)
+
+    Type wrapping javascript functions into py.js callables. The
+    wrapped function follows :ref:`the py.js calling conventions
+    <types-methods-python-call>`
+
+    :param Function fn: the javascript function to wrap
+    :returns: a callable py.js object
+
+.. [#kwonly] Python 2, which py.js currently implements, does not
+             support Python-level keyword-only parameters (it can be
+             done through the C-API), but it seemed neat and easy
+             enough so there.
+
+.. [#star-args] due to this and contrary to Python 2, py.js allows
+                arguments other than ``**kwargs`` to follow ``*args``.
+
+.. _PyArg_ParseTupleAndKeywords:
+    http://docs.python.org/c-api/arg.html#PyArg_ParseTupleAndKeywords
 
     py.PY_parseArgs = function PY_parseArgs(argument, format) {
         var out = {};
-        var args = argument[0], kwargs = argument[1];
+        var args = argument[0];
+        var kwargs = {};
+        for (var k in argument[1]) {
+            kwargs[k] = argument[1][k];
+        }
+        if (typeof format === 'string') {
+            format = format.split(/\s+/);
+        }
         var name = function (spec) {
             if (typeof spec === 'string') {
                 return spec;
                 "TypeError: unknown format specification " +
                     JSON.stringify(spec));
         };
-        
+        // TODO: ensure all format arg names are actual names?
         for(var i=0; i<args.length; ++i) {
             var spec = format[i];
             // spec list ended, or specs switching to keyword-only
                     "TypeError: function takes exactly " + (i-1) +
                     " positional arguments (" + args.length +
                     " given")
+            } else if(/^\*\w/.test(spec)) {
+                // *args, final
+                out[name(spec.slice(1))] = args.slice(i);
+                break;
             }
 
             out[name(spec)] = args[i];
         }
-        for (var kwarg in kwargs) {
-            if (!kwargs.hasOwnProperty(kwarg)) { continue; }
+        for(var j=i; j<format.length; ++j) {
+            var spec = format[j];
+            var n = name(spec);
 
-            // Error out if already matched via positional
-            if (kwarg in out) {
+            if (n in out) {
                 throw new Error(
                     "TypeError: function got multiple values " + 
                     "for keyword argument '" + kwarg + "'");
             }
-            // Look for spec matching kwarg, start after positional
-            // arguments as we already checked there
-            var expected = false;
-            for(var j=i; j<format.length; ++j) {
-                var spec = format[j];
-                if (name(spec) === kwarg) {
-                    expected = true; break;
-                }
+            if (/^\*\*\w/.test(n)) {
+                // **kwarg
+                out[n.slice(2)] = kwargs;
+                kwargs = {};
+                break;
             }
-            if (!expected) {
-                throw new Error(
-                    "TypeError: function got an unexpected keyword argument '"
-                    + kwarg + "'");
+            if (n in kwargs) {
+                out[n] = kwargs[n];
+                // Remove from args map
+                delete kwargs[n];
             }
-
-            out[kwarg] = kwargs[kwarg];
+        }
+        // Ensure all keyword arguments were consumed
+        for (var key in kwargs) {
+            throw new Error(
+                "TypeError: function got an unexpected keyword argument '"
+                    + key + "'");
         }
 
-        // Fixup args count if there's a kwonly flag
+        // Fixup args count if there's a kwonly flag (or an *args)
         var kwonly = 0;
         for(var k = 0; k < format.length; ++k) {
-            if (format[k] === '*') { kwonly = 1; break; }
+            if (/^\*/.test(format[k])) { kwonly = 1; break; }
         }
         // Check that all required arguments have been matched, add
         // optional values
         for(var k = 0; k < format.length; ++k) {
             var spec = format[k], n = name(spec);
-            // keyword only or matched argument
-            if (n === '*' || n in out) { continue; }
+            // kwonly, va_arg or matched argument
+            if (/^\*/.test(n) || n in out) { continue; }
             // Unmatched required argument
             if (!(spec instanceof Array)) {
                 throw new Error(

test/helpers/args.js

+var py = require('../../lib/py.js'),
+expect = require('expect.js');
+
+var ev = function (str, context) {
+    return py.evaluate(py.parse(py.tokenize(str)), context);
+};
+
+describe("Arguments parser", function () {
+    it('should return an object', function () {
+        expect(py.PY_parseArgs([[], {}], []))
+            .to.be.an(Object);
+    });
+    it('should assert the number of arguments', function () {
+        expect(function () {
+            py.PY_parseArgs([[1, 2], {}], []);
+        }).to.throwException(/^TypeError/);
+        expect(function () {
+            py.PY_parseArgs([[], {a: 3}], []);
+        }).to.throwException(/^TypeError/);
+        expect(function () {
+            py.PY_parseArgs([[], {}], ['a']);
+        }).to.throwException(/^TypeError/);
+        expect(function () {
+            py.PY_parseArgs([[], {}], ['*', 'a']);
+        }).to.throwException(/^TypeError/);
+    });
+    it('should prevent arguments conflicts', function () {
+        expect(function () {
+            py.PY_parseArgs([[1], {foo: 1}], ['foo']);
+        }).to.throwException(/^TypeError/);
+    });
+    it('should not require optional arguments', function () {
+        var val = py.float.fromJSON(3);
+        expect(py.PY_parseArgs([[], {}], [['foo', val]]))
+            .to.eql({
+                foo: val
+            });
+        expect(py.PY_parseArgs([[], {}], ['*', ['foo', val]]))
+            .to.eql({
+                foo: val
+            });
+    });
+    it('should override defaults', function () {
+        var def = py.float.fromJSON(3);
+        var val = py.float.fromJSON(4);
+
+        expect(py.PY_parseArgs([[val], {}], [['foo', def]]))
+            .to.eql({
+                foo: val
+            });
+        expect(py.PY_parseArgs([[], {foo: val}], [['foo', def]]))
+            .to.eql({
+                foo: val
+            });
+    });
+    it('should allow star-args', function () {
+        expect(py.PY_parseArgs(
+            [[1, 2, 3, 4], {baz: 5}], 'foo *bar baz'))
+            .to.eql({
+                foo: 1,
+                bar: [2, 3, 4],
+                baz: 5
+            });
+    });
+    it('should make arguments following star-args keyword-only', function () {
+        expect(py.PY_parseArgs([[1, 2, 3], {baz: 4, qux: 5}],
+                               ['foo', '*bar', 'baz', 'qux', ['quux', 42]]))
+            .to.eql({
+                foo: 1,
+                bar: [2, 3],
+                baz: 4,
+                qux: 5,
+                quux: 42
+            });
+        expect(function () {
+            py.PY_parseArgs([[1, 2, 3], {}], 'foo *bar baz');
+        }).to.throwException(/^TypeError/);
+    });
+    it('should support **kw args', function () {
+        expect(py.PY_parseArgs([[1, 2, 3], {baz: 4, qux: 5, quux: 6}],
+                               'foo *bar baz **qux'))
+            .to.eql({
+                foo: 1,
+                bar: [2, 3],
+                baz: 4,
+                qux: {qux: 5, quux: 6}
+            });
+    });
+    it('should support a string argspec', function () {
+        expect(py.PY_parseArgs([[1, 2, 3], {qux: 4}], 'foo bar baz qux'))
+            .to.eql({
+                foo: 1,
+                bar: 2,
+                baz: 3,
+                qux: 4
+            });
+    });
+});

test/helpers/call.js

+var py = require('../../lib/py.js'),
+    expect = require('expect.js');
+
+var ev = function (str, context) {
+    return py.evaluate(py.parse(py.tokenize(str)), context);
+};
     return py.evaluate(py.parse(py.tokenize(str)), context);
 };
 
-describe('Utility functions', function () {
-    describe("Arguments parser", function () {
-        it('should return an object', function () {
-            expect(py.PY_parseArgs([[], {}], []))
-                .to.be.an(Object);
-        });
-        it('should assert the number of arguments', function () {
-            expect(function () {
-                py.PY_parseArgs([[1, 2], {}], []);
-            }).to.throwException(/^TypeError/);
-            expect(function () {
-                py.PY_parseArgs([[], {a: 3}], []);
-            }).to.throwException(/^TypeError/);
-            expect(function () {
-                py.PY_parseArgs([[], {}], ['a']);
-            }).to.throwException(/^TypeError/);
-            expect(function () {
-                py.PY_parseArgs([[], {}], ['*', 'a']);
-            }).to.throwException(/^TypeError/);
-        });
-        it('should prevent arguments conflicts', function () {
-            expect(function () {
-                py.PY_parseArgs([[1], {foo: 1}], ['foo']);
-            }).to.throwException(/^TypeError/);
-        });
-        it('should not require optional arguments', function () {
-            var val = py.float.fromJSON(3);
-            expect(py.PY_parseArgs([[], {}], [['foo', val]]))
-                .to.eql({
-                    foo: val
-                });
-            expect(py.PY_parseArgs([[], {}], ['*', ['foo', val]]))
-                .to.eql({
-                    foo: val
-                });
-        });
-        it('should override defaults', function () {
-            var def = py.float.fromJSON(3);
-            var val = py.float.fromJSON(4);
-
-            expect(py.PY_parseArgs([[val], {}], [['foo', def]]))
-                .to.eql({
-                    foo: val
-                });
-            expect(py.PY_parseArgs([[], {foo: val}], [['foo', def]]))
-                .to.eql({
-                    foo: val
-                });
-        });
-    });
-});
-
 describe('Literals', function () {
     describe('Number', function () {
         it('should have the right type', function () {