From 3826ec4b19df46f28dafe413c875f37033844035 Mon Sep 17 00:00:00 2001 From: mrcork Date: Mon, 28 Sep 2020 14:28:59 +0800 Subject: [PATCH 1/6] fix static tp_names as per cpython and fix some getters and setters --- src/abstract.js | 30 +++++++++++++----------------- src/bytes.js | 2 +- src/function.js | 15 +++++++++++++-- src/generic.js | 2 +- src/type.js | 22 +++++++++++++++++++--- 5 files changed, 47 insertions(+), 24 deletions(-) diff --git a/src/abstract.js b/src/abstract.js index 196507d9aa..f9f41fea22 100644 --- a/src/abstract.js +++ b/src/abstract.js @@ -23,8 +23,17 @@ Sk.abstr = {}; */ Sk.abstr.typeName = function (obj) { if (obj != null && obj.tp$name !== undefined) { - return obj.tp$name; + let name = obj.hp$name; + if (name !== undefined) { + return name; + } + name = obj.tp$name; + if (name.includes(".")) { + name = name.slice(name.lastIndexOf(".") + 1); + } + return name; } else { + Sk.asserts.fail(obj + " passed to typeName"); return ""; } }; @@ -47,15 +56,13 @@ const binop_name_to_symbol = { }; function binop_type_error(v, w, name) { - const vtypename = Sk.abstr.typeName(v); - const wtypename = Sk.abstr.typeName(w); - throw new Sk.builtin.TypeError("unsupported operand type(s) for " + binop_name_to_symbol[name] + ": '" + vtypename + "' and '" + wtypename + "'"); + throw new Sk.builtin.TypeError("unsupported operand type(s) for " + binop_name_to_symbol[name] + ": '" + v.tp$name + "' and '" + w.tp$name + "'"); }; function biniop_type_error(v, w, name) { const vtypename = Sk.abstr.typeName(v); const wtypename = Sk.abstr.typeName(w); - throw new Sk.builtin.TypeError("unsupported operand type(s) for " + binop_name_to_symbol[name] + "=: '" + vtypename + "' and '" + wtypename + "'"); + throw new Sk.builtin.TypeError("unsupported operand type(s) for " + binop_name_to_symbol[name] + "=: '" + v.tp$name + "' and '" + w.tp$name + "'"); }; const uop_name_to_symbol = { @@ -64,8 +71,7 @@ const uop_name_to_symbol = { Invert: "~", }; function unop_type_error(v, name) { - var vtypename = Sk.abstr.typeName(v); - throw new Sk.builtin.TypeError("bad operand type for unary " + uop_name_to_symbol[name] + ": '" + vtypename + "'"); + throw new Sk.builtin.TypeError("bad operand type for unary " + uop_name_to_symbol[name] + ": '" + v.tp$name + "'"); }; /** @@ -1186,13 +1192,6 @@ Sk.abstr.buildNativeClass = function (typename, options) { /**@type {FunctionConstructor} */ let typeobject = options.constructor; - let mod; - if (typename.includes(".")) { - // you should define the module like "collections.defaultdict" for static classes - const mod_typename = typename.split("."); - typename = mod_typename.pop(); - mod = mod_typename.join("."); - } // set the prototypical chains for inheritance Sk.abstr.setUpInheritance(typename, typeobject, options.base, options.meta); @@ -1214,9 +1213,6 @@ Sk.abstr.buildNativeClass = function (typename, options) { Sk.abstr.setUpGetSets(typeobject, options.getsets); Sk.abstr.setUpClassMethods(typeobject, options.classmethods); - if (mod !== undefined) { - type_proto.__module__ = new Sk.builtin.str(mod); - } const proto = options.proto || {}; Object.entries(proto).forEach(([p, val]) => { diff --git a/src/bytes.js b/src/bytes.js index f478db545e..9e82848f01 100644 --- a/src/bytes.js +++ b/src/bytes.js @@ -115,7 +115,7 @@ Sk.builtin.bytes = Sk.abstr.buildNativeClass("bytes", { }); return Sk.misceval.chain(r, () => new Sk.builtin.bytes(source)); } - throw new Sk.builtin.TypeError("cannot convert '" + Sk.abstr.typeName(source) + "' object into bytes"); + throw new Sk.builtin.TypeError("cannot convert '" + Sk.abstr.typeName(pySource) + "' object into bytes"); }, $r() { let num; diff --git a/src/function.js b/src/function.js index e14e320a3d..a1830e8acb 100644 --- a/src/function.js +++ b/src/function.js @@ -35,7 +35,7 @@ Sk.builtin.func = Sk.abstr.buildNativeClass("function", { this.$name = (code.co_name && code.co_name.v) || code.name || ""; this.$d = Sk.builtin.dict ? new Sk.builtin.dict() : undefined; this.$doc = code.$doc; - this.$module = (Sk.globals && Sk.globals["__name__"]) || Sk.builtin.none.none$; + this.$module = (Sk.globals && Sk.globals["__name__"]); this.$qualname = (code.co_qualname && code.co_qualname.v) || this.$name; if (closure2 !== undefined) { @@ -127,9 +127,20 @@ Sk.builtin.func = Sk.abstr.buildNativeClass("function", { }, __doc__: { $get() { - return new Sk.builtin.str(this.$doc); + return this.$doc || Sk.builtin.none.none$; }, + $set(value) { + this.$doc = value; + } }, + __module__: { + $get() { + return this.$module || Sk.builtin.none.none$; + }, + $set(value) { + this.$module = value; + } + } }, proto: { $memoiseFlags() { diff --git a/src/generic.js b/src/generic.js index f527bc417f..f1cb621031 100644 --- a/src/generic.js +++ b/src/generic.js @@ -274,7 +274,7 @@ Sk.generic.getSetDict = { }, $set(value) { if (value === undefined) { - this.$d = new Sk.builtin.dict(); + throw new Sk.builtin.TypeError("cannot delete __dict__"); } else if (value instanceof Sk.builtin.dict) { this.$d = value; } else { diff --git a/src/type.js b/src/type.js index c38a57f2b2..0466b99e73 100644 --- a/src/type.js +++ b/src/type.js @@ -324,6 +324,7 @@ function setUpKlass($name, klass, bases, meta) { tp$bases: { value: bases, writable: true }, tp$mro: { value: null, writable: true }, hp$type: { value: true, writable: true }, + hp$name: { value: $name, writable: true} }); klass_proto.tp$mro = klass.$buildMRO(); @@ -572,7 +573,15 @@ Sk.builtin.type.prototype.tp$getsets = { }, __name__: { $get() { - return new Sk.builtin.str(this.prototype.tp$name); + let name = this.prototype.hp$name; + if (name !== undefined) { + return new Sk.builtin.str(name); + } + name = this.prototype.tp$name; + if (name.includes(".")) { + name = name.slice(name.lastIndexOf(".") + 1); + } + return new Sk.builtin.str(name); }, $set(value) { check_special_type_attr(this, value, Sk.builtin.str.$name); @@ -581,7 +590,7 @@ Sk.builtin.type.prototype.tp$getsets = { "can only assign string to " + this.prototype.tp$name + ".__name__, not '" + Sk.abstr.typeName(value) + "'" ); } - this.prototype.tp$name = value.$jsstr(); + this.prototype.hp$name = this.prototype.tp$name = value.$jsstr(); }, }, __module__: { @@ -590,6 +599,9 @@ Sk.builtin.type.prototype.tp$getsets = { if (mod && !(mod.ob$type === Sk.builtin.getset_descriptor)) { return mod; } + if (this.tp$name.includes(".")) { + return new Sk.builtin.str(this.tp$name.slice(0, this.tp$name.lastIndexOf("."))); + } return new Sk.builtin.str("builtins"); }, $set(value) { @@ -658,7 +670,11 @@ const subtype_dict_getset_description = { if (dict_descr !== undefined) { return dict_descr.tp$descr_set(this, value); } - return Sk.generic.getSetDict.$set.call(this, value); + if (value === undefined) { + this.$d = new Sk.builtin.dict([]); + } else { + return Sk.generic.getSetDict.$set.call(this, value); + } }, $doc: "dictionary for instance variables (if defined)", $name: "__dict__", From 918980983192f09b1dbaeee25e265deea21f7de5 Mon Sep 17 00:00:00 2001 From: mrcork Date: Mon, 28 Sep 2020 14:29:08 +0800 Subject: [PATCH 2/6] reduce only available in py2 mode --- src/builtindict.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/builtindict.js b/src/builtindict.js index 98a6f6d626..e7bca8d03e 100644 --- a/src/builtindict.js +++ b/src/builtindict.js @@ -24,7 +24,6 @@ Sk.builtins = { "hasattr" : null, "id" : null, - "reduce" : new Sk.builtin.func(Sk.builtin.reduce), "sorted" : null, "any" : null, "all" : null, @@ -477,6 +476,7 @@ Sk.setupObjects = function (py3) { Sk.builtins["map"] = Sk.builtin.map_; Sk.builtins["zip"] = Sk.builtin.zip_; Sk.builtins["range"] = Sk.builtin.range_; + delete Sk.builtins["reduce"]; delete Sk.builtins["xrange"]; delete Sk.builtins["StandardError"]; delete Sk.builtins["unicode"]; @@ -519,6 +519,15 @@ Sk.setupObjects = function (py3) { null, "builtins" ); + Sk.builtins["reduce"] = new Sk.builtin.sk_method( + { + $meth: Sk.builtin.reduce, + $name: "reduce", + $flags: { MinArgs: 2, MaxArgs: 3 }, + }, + null, + "builtins" + ); Sk.builtins["filter"] = new Sk.builtin.func(Sk.builtin.filter); Sk.builtins["map"] = new Sk.builtin.func(Sk.builtin.map); Sk.builtins["zip"] = new Sk.builtin.func(Sk.builtin.zip); From e032e5d6d1e0683c1dd4c5b40bc4d9ae53b44ab6 Mon Sep 17 00:00:00 2001 From: mrcork Date: Mon, 28 Sep 2020 14:29:18 +0800 Subject: [PATCH 3/6] object tostring --- src/object.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/object.js b/src/object.js index 773cabd712..8d8eddf57f 100644 --- a/src/object.js +++ b/src/object.js @@ -156,7 +156,9 @@ Sk.builtin.object = Sk.abstr.buildNativeClass("object", { }, proto: /**@lends {Sk.builtin.object.prototype}*/ { valueOf: Object.prototype.valueOf, - toString: Object.prototype.toString, + toString() { + return this.tp$str().v; + }, hasOwnProperty: Object.prototype.hasOwnProperty, hp$type: undefined, // private method used for error messages From 655e36f0f9c4bf1e8d92d9900e0fa320c34fff60 Mon Sep 17 00:00:00 2001 From: mrcork Date: Mon, 28 Sep 2020 14:31:01 +0800 Subject: [PATCH 4/6] keyword array helper functions --- src/abstract.js | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/abstract.js b/src/abstract.js index f9f41fea22..67e6a6dfd1 100644 --- a/src/abstract.js +++ b/src/abstract.js @@ -527,6 +527,23 @@ Sk.abstr.mappingUnpackIntoKeywordArray = function (jsArray, pyMapping, pyCodeObj ); }; +Sk.abstr.keywordArrayFromPyDict = function (dict) { + const arr = []; + dict.$items().forEach(([key, val]) => { + if (!Sk.builtin.checkString(key)) { + throw new Sk.builtin.TypeError("keywords must be strings"); + } + arr.push(key.$jsstr()); + arr.push(val); + }); + return arr; +}; + +Sk.abstr.keywordArrayToPyDict = function (kwarray) { + kwarray = kwarray.map((x, i) => i % 2 ? x : new Sk.builtin.str(x)); + return new Sk.builtin.dict(kwarray); +}; + /** * * @function From ac8c02c3eeb0f87e153ac4aed0cc713cb886fa8c Mon Sep 17 00:00:00 2001 From: mrcork Date: Mon, 28 Sep 2020 14:31:22 +0800 Subject: [PATCH 5/6] functools implementation with tests --- src/lib/functools.js | 458 +++++++++++++ src/lib/functools.py | 1 - test/unit3/test_functools.py | 1167 ++++++++++++++++++++++++++++++++++ test/unit3/test_itertools.py | 2 +- 4 files changed, 1626 insertions(+), 2 deletions(-) create mode 100644 src/lib/functools.js delete mode 100644 src/lib/functools.py create mode 100644 test/unit3/test_functools.py diff --git a/src/lib/functools.js b/src/lib/functools.js new file mode 100644 index 0000000000..8b30e6272f --- /dev/null +++ b/src/lib/functools.js @@ -0,0 +1,458 @@ +function $builtinmodule() { + const functools = { + __name__: new Sk.builtin.str("functools"), + __all__: new Sk.builtin.list( + [ + "update_wrapper", + "wraps", + "WRAPPER_ASSIGNMENTS", + "WRAPPER_UPDATES", + "total_ordering", + "cmp_to_key", + "lru_cache" /**@todo lru_cache */, + "reduce", + "TopologicalSorter" /**@todo TopologicalSorter */, + "CycleError" /**@todo CycleError */, + "partial", + "partialmethod", + "singledispatch" /**@todo singledispatch */, + "singledispatchmethod" /**@todo singledispatchmethod */, + "cached_property" /**@todo cached_property */, + ].map((x) => new Sk.builtin.str(x)) + ), + WRAPPER_ASSIGNMENTS: new Sk.builtin.tuple( + ["__module__", "__name__", "__qualname__", "__doc__" /*"__annotations__"*/].map((x) => new Sk.builtin.str(x)) + ), + WRAPPER_UPDATES: new Sk.builtin.tuple([new Sk.builtin.str("__dict__")]), + + /**@todo */ + lru_cache: proxyFail("lru_cace"), + TopologicalSorter: proxyFail("TopologicalSorter"), + CycleError: proxyFail("CycleError"), + singledispatch: proxyFail("singledispatch"), + singledispatchmethod: proxyFail("singledispatchmethod"), + cached_property: proxyFail("cached_property"), + }; + + function proxyFail(_name) { + return new Sk.builtin.func(function () { + throw new Sk.builtin.NotImplementedError(_name + " is not yet implemented in skulpt"); + }); + } + + /********** Partial *************/ + + function partial_adjust_args_kwargs(args, kwargs) { + args = this.arg_arr.concat(args); + if (kwargs) { + kwargs = Sk.abstr.keywordArrayToPyDict(kwargs); + const kwargs1 = this.kwdict.dict$copy(); + kwargs1.dict$merge(kwargs); + kwargs = Sk.abstr.keywordArrayFromPyDict(kwargs1); + } else { + kwargs = Sk.abstr.keywordArrayFromPyDict(this.kwdict); + } + return { args: args, kwargs: kwargs }; + } + + function partial_new(args, kwargs) { + if (args.length < 1) { + throw new Sk.builtin.TypeError("type 'partial' takes at least 1 argument"); + } + let func = args.shift(); + let pargs, pkwdict; + if (func instanceof this.sk$builtinBase) { + const part = func; + func = part.fn; + pargs = part.arg_arr; + pkwdict = part.kwdict; + } + this.check$func(func); + if (pargs) { + args = pargs.concat(args); + } + kwargs = kwargs || []; + let kwdict = Sk.abstr.keywordArrayToPyDict(kwargs); + if (pkwdict) { + const copy = pkwdict.dict$copy(); + copy.dict$merge(kwdict); + kwdict = copy; + } + if (this.sk$builtinBase === this.constructor) { + return new this.constructor(func, args, kwdict); + } else { + // for subclassing + const instance = new this.constructor(); + this.sk$builtinBase.call(instance, func, args, kwdict); + return instance; + } + } + + function partial_repr() { + if (this.in$repr) { + return new Sk.builtin.str("..."); + } + this.in$repr = true; + const arglist = [Sk.misceval.objectRepr(this.fn)]; + this.arg_arr.forEach((arg) => { + arglist.push(Sk.misceval.objectRepr(arg)); + }); + this.kwdict.$items().forEach(([key, val]) => { + arglist.push(key.toString() + "=" + Sk.misceval.objectRepr(val)); + }); + this.in$repr = false; + + /** @todo this.tp$name should actually include functools here since it's a static type */ + return new Sk.builtin.str(this.tp$name + "(" + arglist.join(", ") + ")"); + } + + functools.partial = Sk.abstr.buildNativeClass("functools.partial", { + constructor: function partial(func, args, kwdict) { + this.fn = func; + this.arg_arr = args; + this.arg_tup = new Sk.builtin.tuple(args); + this.kwdict = kwdict; + this.in$repr = false; + this.$d = new Sk.builtin.dict([]); + }, + slots: { + tp$new: partial_new, + tp$call(args, kwargs) { + ({ args, kwargs } = this.adj$args_kws(args, kwargs)); + return this.fn.tp$call(args, kwargs); + }, + tp$doc: "partial(func, *args, **keywords) - new function with partial application\n of the given arguments and keywords.\n", + $r: partial_repr, + tp$getattr: Sk.generic.getAttr, + tp$setattr: Sk.generic.setAttr, + }, + getsets: { + func: { + $get() { + return this.fn; + }, + $doc: "function object to use in future partial calls", + }, + args: { + $get() { + return this.arg_tup; + }, + $doc: "tuple of arguments to future partial calls", + }, + keywords: { + $get() { + return this.kwdict; + }, + $doc: "dictionary of keyword arguments to future partial calls", + }, + __dict__: Sk.generic.getSetDict, + }, + methods: { + // __reduce__: {}, + // __setstate__: {} + }, + proto: { + adj$args_kws: partial_adjust_args_kwargs, + check$func(func) { + if (!Sk.builtin.checkCallable(func)) { + throw new Sk.builtin.TypeError("the first argument must be callable"); + } + }, + }, + }); + + /********** Partial Method *************/ + + functools.partialmethod = Sk.abstr.buildNativeClass("functools.partialmethod", { + constructor: function partialmethod(func, args, kwdict) { + this.fn = func; + this.arg_arr = args; + this.arg_tup = new Sk.builtin.tuple(args); + this.kwdict = kwdict; + }, + slots: { + tp$new: partial_new, + tp$doc: + "Method descriptor with partial application of the given arguments\n and keywords.\n\n Supports wrapping existing descriptors and handles non-descriptor\n callables as instance methods.\n ", + $r: partial_repr, + tp$descr_get(obj, obtype) { + let res; + if (this.fn.tp$descr_get) { + const new_func = this.fn.tp$descr_get(obj, obtype); + if (new_func !== this.fn) { + if (!Sk.builtin.checkCallable(new_func)) { + throw new Sk.builtin.TypeError("type 'partial' requires a callable"); + } + res = new functools.partial(new_func, this.arg_arr.slice(0), this.kwdict.dict$copy()); + const __self__ = Sk.abstr.lookupSpecial(new_func, this.str$self); + if (__self__ !== undefined) { + res.tp$setattr(this.str$self, __self__); + } + } + } + if (res === undefined) { + res = this.make$unbound().tp$descr_get(obj, obtype); + } + return res; + }, + }, + methods: { + _make_unbound_method: { + $meth() { + return this.make$unbound(); + }, + $flags: { NoArgs: true }, + }, + }, + getsets: { + func: { + $get() { + return this.fn; + }, + $doc: "function object to use in future partial calls", + }, + args: { + $get() { + return this.arg_tup; + }, + $doc: "tuple of arguments to future partial calls", + }, + keywords: { + $get() { + return this.kwdict; + }, + $doc: "dictionary of keyword arguments to future partial calls", + }, + __dict__: Sk.generic.getSetDict, + }, + proto: { + str$self: new Sk.builtin.str("__self__"), + make$unbound() { + const self = this; + function _method(args, kwargs) { + const cls_or_self = args.shift(); + ({ args, kwargs } = self.adj$args_kws(args, kwargs)); + args.unshift(cls_or_self); + return Sk.misceval.callsimOrSuspendArray(self.fn, args, kwargs); + } + _method.co_fastcall = true; + return new Sk.builtin.func(_method); + }, + adj$args_kws: partial_adjust_args_kwargs, + check$func(func) { + if (!Sk.builtin.checkCallable(func) && func.tp$descr_get === undefined) { + throw new Sk.builtin.TypeError(Sk.misceval.objectRepr(func) + " is not callable or a descriptor"); + } + }, + }, + }); + + /********** Total Ordering *************/ + + const js_opname_to_py = { + __lt__: Sk.builtin.str.$lt, + __le__: Sk.builtin.str.$le, + __gt__: Sk.builtin.str.$gt, + __ge__: Sk.builtin.str.$ge, + }; + + function from_slot(op_name, get_res) { + const pyName = js_opname_to_py[op_name]; + function compare_slot(self, other) { + let op_result = Sk.misceval.callsimArray(self.tp$getattr(pyName), [other]); + if (op_result === Sk.builtin.NotImplemented.NotImplemented$) { + return op_result; + } + op_result = Sk.misceval.isTrue(op_result); + return new Sk.builtin.bool(get_res(op_result, self, other)); + } + compare_slot.co_name = pyName; + return compare_slot; + } + + const _gt_from_lt = from_slot("__lt__", (op_result, self, other) => !op_result && Sk.misceval.richCompareBool(self, other, "NotEq")); + const _le_from_lt = from_slot("__lt__", (op_result, self, other) => op_result || Sk.misceval.richCompareBool(self, other, "Eq")); + const _ge_from_lt = from_slot("__lt__", (op_result) => !op_result); + const _ge_from_le = from_slot("__le__", (op_result, self, other) => !op_result || Sk.misceval.richCompareBool(self, other, "Eq")); + const _lt_from_le = from_slot("__le__", (op_result, self, other) => op_result && Sk.misceval.richCompareBool(self, other, "NotEq")); + const _gt_from_le = from_slot("__le__", (op_result) => !op_result); + const _lt_from_gt = from_slot("__gt__", (op_result, self, other) => !op_result && Sk.misceval.richCompareBool(self, other, "NotEq")); + const _ge_from_gt = from_slot("__gt__", (op_result, self, other) => op_result || Sk.misceval.richCompareBool(self, other, "Eq")); + const _le_from_gt = from_slot("__gt__", (op_result) => !op_result); + const _le_from_ge = from_slot("__ge__", (op_result, self, other) => !op_result || Sk.misceval.richCompareBool(self, other, "Eq")); + const _gt_from_ge = from_slot("__ge__", (op_result, self, other) => op_result && Sk.misceval.richCompareBool(self, other, "NotEq")); + const _lt_from_ge = from_slot("__ge__", (op_result) => !op_result); + + const pyFunc = Sk.builtin.func; + + const _convert = { + __lt__: { __gt__: new pyFunc(_gt_from_lt), __le__: new pyFunc(_le_from_lt), __ge__: new pyFunc(_ge_from_lt) }, + __le__: { __ge__: new pyFunc(_ge_from_le), __lt__: new pyFunc(_lt_from_le), __gt__: new pyFunc(_gt_from_le) }, + __gt__: { __lt__: new pyFunc(_lt_from_gt), __ge__: new pyFunc(_ge_from_gt), __le__: new pyFunc(_le_from_gt) }, + __ge__: { __le__: new pyFunc(_le_from_ge), __gt__: new pyFunc(_gt_from_ge), __lt__: new pyFunc(_lt_from_ge) }, + }; + + const op_name_short = { + __lt__: "ob$lt", + __le__: "ob$le", + __gt__: "ob$gt", + __ge__: "ob$ge", + }; + + function total_ordering(cls) { + const roots = []; + if (!Sk.builtin.checkClass(cls)) { + throw new Sk.builtin.TypeError("total ordering only supported for type objects not '" + Sk.abstr.typeName(cls) + "'"); + } + Object.keys(_convert).forEach((key) => { + const shortcut = op_name_short[key]; + if (cls.prototype[shortcut] !== Sk.builtin.object.prototype[shortcut]) { + roots.push(key); + } + }); + if (!roots.length) { + throw new Sk.builtin.ValueError("must define atleast one ordering operation: <, >, <=, >="); + } + const root = roots[0]; + Object.entries(_convert[root]).forEach(([opname, opfunc]) => { + if (!roots.includes(opname)) { + cls.tp$setattr(js_opname_to_py[opname], opfunc); + } + }); + return cls; + } + + /************* KeyWrapper for cmp_to_key *************/ + const zero = new Sk.builtin.int_(0); + + const KeyWrapper = Sk.abstr.buildNativeClass("functools.KeyWrapper", { + constructor: function (cmp, obj) { + this.cmp = cmp; + this.obj = obj; + }, + slots: { + tp$call(args, kwargs) { + const [obj] = Sk.abstr.copyKeywordsToNamedArgs("K", ["obj"], args, kwargs, []); + return new KeyWrapper(this.cmp, obj); + }, + tp$richcompare(other, op) { + if (!(other instanceof KeyWrapper)) { + throw new Sk.builtin.TypeError("other argument must be K instance"); + } + const x = this.obj; + const y = other.obj; + if (!x || !y) { + throw new Sk.builtin.AttributeErrror("object"); + } + const comparison = Sk.misceval.callsimOrSuspendArray(this.cmp, [x, y]); + return Sk.misceval.chain(comparison, (res) => Sk.misceval.richCompareBool(res, zero, op)); + }, + tp$getattr: Sk.generic.getAttr, + tp$hash: Sk.builtin.none.none$, + }, + getsets: { + obj: { + $get() { + return this.obj || Sk.builtin.none.none$; + }, + $set(value) { + this.obj = value; + }, + $doc: "Value wrapped by a key function.", + }, + }, + }); + + const str_update = new Sk.builtin.str("update"); + const __wrapped__ = new Sk.builtin.str("__wrapped__"); + + Sk.abstr.setUpModuleMethods("functools", functools, { + cmp_to_key: { + $meth: function cmp_to_key(mycmp) { + return new KeyWrapper(mycmp); + }, + $flags: { NamedArgs: ["mycmp"], Defaults: [] }, + $doc: "Convert a cmp= function into a key= function.", + $textsig: "($module, cmp, /)", + }, + reduce: { + $meth: function reduce(fun, seq, initializer) { + const iter = Sk.abstr.iter(seq); + let accum_value; + initializer = initializer || iter.tp$iternext(true); + return Sk.misceval.chain( + initializer, + (initial) => { + if (initial === undefined) { + throw new Sk.builtin.TypeError("reduce() of empty sequence with no initial value"); + } + accum_value = initial; + return Sk.misceval.iterFor(iter, (item) => { + return Sk.misceval.chain(Sk.misceval.callsimOrSuspendArray(fun, [accum_value, item]), (res) => { + accum_value = res; + }); + }); + }, + () => { + return accum_value; + } + ); + }, + $flags: { MinArgs: 2, MaxArgs: 3 }, + $doc: + "reduce(function, sequence[, initial]) -> value\n\nApply a function of two arguments cumulatively to the items of a sequence,\nfrom left to right, so as to reduce the sequence to a single value.\nFor example, reduce(lambda x, y: x+y, [1, 2, 3, 4, 5]) calculates\n((((1+2)+3)+4)+5). If initial is present, it is placed before the items\nof the sequence in the calculation, and serves as a default when the\nsequence is empty.", + $textsig: "($module, function, sequence[, initial], /)", + }, + total_ordering: { + $meth: total_ordering, + $flags: { OneArg: true }, + $doc: "Class decorator that fills in missing ordering methods", + }, + + update_wrapper: { + $meth: function update_wrapper(wrapper, wrapped, assigned, updated) { + let it = Sk.abstr.iter(assigned); + let value; + for (let attr = it.tp$iternext(); attr !== undefined; attr = it.tp$iternext()) { + if ((value = wrapped.tp$getattr(attr)) !== undefined) { + wrapper.tp$setattr(attr, value); + } + } + it = Sk.abstr.iter(updated); + for (let attr = it.tp$iternext(); attr !== undefined; attr = it.tp$iternext()) { + value = wrapped.tp$getattr(attr) || new Sk.builtin.dict([]); + const to_update = Sk.abstr.gattr(wrapper, attr); // throw the appropriate error + const update_meth = Sk.abstr.gattr(to_update, str_update); + Sk.misceval.callsimArray(update_meth, [value]); + } + + wrapper.tp$setattr(__wrapped__, wrapped); + return wrapper; + }, + $flags: { + NamedArgs: ["wrapper", "wrapped", "assigned", "updated"], + Defaults: [functools.WRAPPER_ASSIGNMENTS, functools.WRAPPER_UPDATES], + }, + $doc: + "Update a wrapper function to look like the wrapped function\n\n wrapper is the function to be updated\n wrapped is the original function\n assigned is a tuple naming the attributes assigned directly\n from the wrapped function to the wrapper function (defaults to\n functools.WRAPPER_ASSIGNMENTS)\n updated is a tuple naming the attributes of the wrapper that\n are updated with the corresponding attribute from the wrapped\n function (defaults to functools.WRAPPER_UPDATES)\n ", + $textsig: + "($module, /, wrapper, wrapped, assigned=('__module__', '__name__', '__qualname__', '__doc__', '__annotations__'), updated=('__dict__',))", + }, + wraps: { + $meth: function wraps(wrapped, assigned, updated) { + const kwarray = ["wrapped", wrapped, "assigned", assigned, "updated", updated]; + return Sk.misceval.callsimArray(functools.partial, [functools.update_wrapper], kwarray); + }, + $flags: { + NamedArgs: ["wrapped", "assigned", "updated"], + Defaults: [functools.WRAPPER_ASSIGNMENTS, functools.WRAPPER_UPDATES], + }, + $doc: + "Decorator factory to apply update_wrapper() to a wrapper function\n\n Returns a decorator that invokes update_wrapper() with the decorated\n function as the wrapper argument and the arguments to wraps() as the\n remaining arguments. Default arguments are as for update_wrapper().\n This is a convenience function to simplify applying partial() to\n update_wrapper().\n ", + $textsig: + "($module, /, wrapped, assigned=('__module__', '__name__', '__qualname__', '__doc__', '__annotations__'), updated=('__dict__',))", + }, + }); + + return functools; +} diff --git a/src/lib/functools.py b/src/lib/functools.py deleted file mode 100644 index 7a39b03f7f..0000000000 --- a/src/lib/functools.py +++ /dev/null @@ -1 +0,0 @@ -raise NotImplementedError("functools is not yet implemented in Skulpt") diff --git a/test/unit3/test_functools.py b/test/unit3/test_functools.py new file mode 100644 index 0000000000..c3603abb63 --- /dev/null +++ b/test/unit3/test_functools.py @@ -0,0 +1,1167 @@ +# import abc +# import builtins +# import collections +# import collections.abc +import copy +from itertools import permutations, chain +# import pickle +from random import choice +import sys +# from test import support +# import threading +import time +# import typing +import unittest +# import unittest.mock +# import os +# from weakref import proxy +# import contextlib + +# from test.support.script_helper import assert_python_ok + +import functools + +# py_functools = support.import_fresh_module('functools', blocked=['_functools']) +# c_functools = support.import_fresh_module('functools', fresh=['_functools']) +py_functools = functools +c_functools = functools + +# decimal = support.import_fresh_module('decimal', fresh=['_decimal']) + +# @contextlib.contextmanager +# def replaced_module(name, replacement): +# original_module = sys.modules[name] +# sys.modules[name] = replacement +# try: +# yield +# finally: +# sys.modules[name] = original_module + +def capture(*args, **kw): + """capture all positional and keyword arguments""" + return args, kw + + +def signature(part): + """ return the signature of a partial object """ + return (part.func, part.args, part.keywords, part.__dict__) + +class MyTuple(tuple): + pass + +class BadTuple(tuple): + def __add__(self, other): + return list(self) + list(other) + +class MyDict(dict): + pass + + +class TestPartial: + + def test_basic_examples(self): + p = self.partial(capture, 1, 2, a=10, b=20) + self.assertTrue(callable(p)) + self.assertEqual(p(3, 4, b=30, c=40), + ((1, 2, 3, 4), dict(a=10, b=30, c=40))) + p = self.partial(map, lambda x: x*10) + self.assertEqual(list(p([1,2,3,4])), [10, 20, 30, 40]) + + def test_attributes(self): + p = self.partial(capture, 1, 2, a=10, b=20) + # attributes should be readable + self.assertEqual(p.func, capture) + self.assertEqual(p.args, (1, 2)) + self.assertEqual(p.keywords, dict(a=10, b=20)) + + def test_argument_checking(self): + self.assertRaises(TypeError, self.partial) # need at least a func arg + try: + self.partial(2)() + except TypeError: + pass + else: + self.fail('First arg not checked for callability') + + def test_protection_of_callers_dict_argument(self): + # a caller's dictionary should not be altered by partial + def func(a=10, b=20): + return a + d = {'a':3} + p = self.partial(func, a=5) + self.assertEqual(p(**d), 3) + self.assertEqual(d, {'a':3}) + p(b=7) + self.assertEqual(d, {'a':3}) + + def test_kwargs_copy(self): + # Issue #29532: Altering a kwarg dictionary passed to a constructor + # should not affect a partial object after creation + d = {'a': 3} + p = self.partial(capture, **d) + self.assertEqual(p(), ((), {'a': 3})) + d['a'] = 5 + self.assertEqual(p(), ((), {'a': 3})) + + def test_arg_combinations(self): + # exercise special code paths for zero args in either partial + # object or the caller + p = self.partial(capture) + self.assertEqual(p(), ((), {})) + self.assertEqual(p(1,2), ((1,2), {})) + p = self.partial(capture, 1, 2) + self.assertEqual(p(), ((1,2), {})) + self.assertEqual(p(3,4), ((1,2,3,4), {})) + + def test_kw_combinations(self): + # exercise special code paths for no keyword args in + # either the partial object or the caller + p = self.partial(capture) + self.assertEqual(p.keywords, {}) + self.assertEqual(p(), ((), {})) + self.assertEqual(p(a=1), ((), {'a':1})) + p = self.partial(capture, a=1) + self.assertEqual(p.keywords, {'a':1}) + self.assertEqual(p(), ((), {'a':1})) + self.assertEqual(p(b=2), ((), {'a':1, 'b':2})) + # keyword args in the call override those in the partial object + self.assertEqual(p(a=3, b=2), ((), {'a':3, 'b':2})) + + def test_positional(self): + # make sure positional arguments are captured correctly + for args in [(), (0,), (0,1), (0,1,2), (0,1,2,3)]: + p = self.partial(capture, *args) + expected = args + ('x',) + got, empty = p('x') + self.assertTrue(expected == got and empty == {}) + + def test_keyword(self): + # make sure keyword arguments are captured correctly + for a in ['a', 0, None, 3.5]: + p = self.partial(capture, a=a) + expected = {'a':a,'x':None} + empty, got = p(x=None) + self.assertTrue(expected == got and empty == ()) + + def test_no_side_effects(self): + # make sure there are no side effects that affect subsequent calls + p = self.partial(capture, 0, a=1) + args1, kw1 = p(1, b=2) + self.assertTrue(args1 == (0,1) and kw1 == {'a':1,'b':2}) + args2, kw2 = p() + self.assertTrue(args2 == (0,) and kw2 == {'a':1}) + + def test_error_propagation(self): + def f(x, y): + x / y + self.assertRaises(ZeroDivisionError, self.partial(f, 1, 0)) + self.assertRaises(ZeroDivisionError, self.partial(f, 1), 0) + self.assertRaises(ZeroDivisionError, self.partial(f), 1, 0) + self.assertRaises(ZeroDivisionError, self.partial(f, y=0), 1) + + # def test_weakref(self): + # f = self.partial(int, base=16) + # p = proxy(f) + # self.assertEqual(f.func, p.func) + # f = None + # self.assertRaises(ReferenceError, getattr, p, 'func') + + def test_with_bound_and_unbound_methods(self): + data = list(map(str, range(10))) + join = self.partial(str.join, '') + self.assertEqual(join(data), '0123456789') + join = self.partial(''.join) + self.assertEqual(join(data), '0123456789') + + def test_nested_optimization(self): + partial = self.partial + inner = partial(signature, 'asdf') + nested = partial(inner, bar=True) + flat = partial(signature, 'asdf', bar=True) + self.assertEqual(signature(nested), signature(flat)) + + def test_nested_partial_with_attribute(self): + # see issue 25137 + partial = self.partial + + def foo(bar): + return bar + + p = partial(foo, 'first') + p2 = partial(p, 'second') + p2.new_attr = 'spam' + self.assertEqual(p2.new_attr, 'spam') + + def test_repr(self): + args = (object(), object()) + args_repr = ', '.join(repr(a) for a in args) + kwargs = {'a': object(), 'b': object()} + kwargs_reprs = ['a={a!r}, b={b!r}'.format(**kwargs), + 'b={b!r}, a={a!r}'.format(**kwargs)] + if self.partial in (c_functools.partial, py_functools.partial): + name = 'functools.partial' + else: + name = self.partial.__name__ + + f = self.partial(capture) + self.assertEqual(f'{name}({capture!r})', repr(f)) + + f = self.partial(capture, *args) + self.assertEqual(f'{name}({capture!r}, {args_repr})', repr(f)) + + f = self.partial(capture, **kwargs) + self.assertIn(repr(f), + [f'{name}({capture!r}, {kwargs_repr})' + for kwargs_repr in kwargs_reprs]) + + f = self.partial(capture, *args, **kwargs) + self.assertIn(repr(f), + [f'{name}({capture!r}, {args_repr}, {kwargs_repr})' + for kwargs_repr in kwargs_reprs]) + + # def test_recursive_repr(self): + # if self.partial in (c_functools.partial, py_functools.partial): + # name = 'functools.partial' + # else: + # name = self.partial.__name__ + + # f = self.partial(capture) + # f.__setstate__((f, (), {}, {})) + # try: + # self.assertEqual(repr(f), '%s(...)' % (name,)) + # finally: + # f.__setstate__((capture, (), {}, {})) + + # f = self.partial(capture) + # f.__setstate__((capture, (f,), {}, {})) + # try: + # self.assertEqual(repr(f), '%s(%r, ...)' % (name, capture,)) + # finally: + # f.__setstate__((capture, (), {}, {})) + + # f = self.partial(capture) + # f.__setstate__((capture, (), {'a': f}, {})) + # try: + # self.assertEqual(repr(f), '%s(%r, a=...)' % (name, capture,)) + # finally: + # f.__setstate__((capture, (), {}, {})) + + # def test_pickle(self): + # with self.AllowPickle(): + # f = self.partial(signature, ['asdf'], bar=[True]) + # f.attr = [] + # for proto in range(pickle.HIGHEST_PROTOCOL + 1): + # f_copy = pickle.loads(pickle.dumps(f, proto)) + # self.assertEqual(signature(f_copy), signature(f)) + + def test_copy(self): + f = self.partial(signature, ['asdf'], bar=[True]) + f.attr = [] + f_copy = copy.copy(f) + self.assertEqual(signature(f_copy), signature(f)) + self.assertIs(f_copy.attr, f.attr) + self.assertIs(f_copy.args, f.args) + self.assertIs(f_copy.keywords, f.keywords) + + # def test_deepcopy(self): + # f = self.partial(signature, ['asdf'], bar=[True]) + # f.attr = [] + # f_copy = copy.deepcopy(f) + # self.assertEqual(signature(f_copy), signature(f)) + # self.assertIsNot(f_copy.attr, f.attr) + # self.assertIsNot(f_copy.args, f.args) + # self.assertIsNot(f_copy.args[0], f.args[0]) + # self.assertIsNot(f_copy.keywords, f.keywords) + # self.assertIsNot(f_copy.keywords['bar'], f.keywords['bar']) + + # def test_setstate(self): + # f = self.partial(signature) + # f.__setstate__((capture, (1,), dict(a=10), dict(attr=[]))) + + # self.assertEqual(signature(f), + # (capture, (1,), dict(a=10), dict(attr=[]))) + # self.assertEqual(f(2, b=20), ((1, 2), {'a': 10, 'b': 20})) + + # f.__setstate__((capture, (1,), dict(a=10), None)) + + # self.assertEqual(signature(f), (capture, (1,), dict(a=10), {})) + # self.assertEqual(f(2, b=20), ((1, 2), {'a': 10, 'b': 20})) + + # f.__setstate__((capture, (1,), None, None)) + # #self.assertEqual(signature(f), (capture, (1,), {}, {})) + # self.assertEqual(f(2, b=20), ((1, 2), {'b': 20})) + # self.assertEqual(f(2), ((1, 2), {})) + # self.assertEqual(f(), ((1,), {})) + + # f.__setstate__((capture, (), {}, None)) + # self.assertEqual(signature(f), (capture, (), {}, {})) + # self.assertEqual(f(2, b=20), ((2,), {'b': 20})) + # self.assertEqual(f(2), ((2,), {})) + # self.assertEqual(f(), ((), {})) + + # def test_setstate_errors(self): + # f = self.partial(signature) + # self.assertRaises(TypeError, f.__setstate__, (capture, (), {})) + # self.assertRaises(TypeError, f.__setstate__, (capture, (), {}, {}, None)) + # self.assertRaises(TypeError, f.__setstate__, [capture, (), {}, None]) + # self.assertRaises(TypeError, f.__setstate__, (None, (), {}, None)) + # self.assertRaises(TypeError, f.__setstate__, (capture, None, {}, None)) + # self.assertRaises(TypeError, f.__setstate__, (capture, [], {}, None)) + # self.assertRaises(TypeError, f.__setstate__, (capture, (), [], None)) + + # def test_setstate_subclasses(self): + # f = self.partial(signature) + # f.__setstate__((capture, MyTuple((1,)), MyDict(a=10), None)) + # s = signature(f) + # self.assertEqual(s, (capture, (1,), dict(a=10), {})) + # self.assertIs(type(s[1]), tuple) + # self.assertIs(type(s[2]), dict) + # r = f() + # self.assertEqual(r, ((1,), {'a': 10})) + # self.assertIs(type(r[0]), tuple) + # self.assertIs(type(r[1]), dict) + + # f.__setstate__((capture, BadTuple((1,)), {}, None)) + # s = signature(f) + # self.assertEqual(s, (capture, (1,), {}, {})) + # self.assertIs(type(s[1]), tuple) + # r = f(2) + # self.assertEqual(r, ((1, 2), {})) + # self.assertIs(type(r[0]), tuple) + + # def test_recursive_pickle(self): + # with self.AllowPickle(): + # f = self.partial(capture) + # f.__setstate__((f, (), {}, {})) + # try: + # for proto in range(pickle.HIGHEST_PROTOCOL + 1): + # with self.assertRaises(RecursionError): + # pickle.dumps(f, proto) + # finally: + # f.__setstate__((capture, (), {}, {})) + + # f = self.partial(capture) + # f.__setstate__((capture, (f,), {}, {})) + # try: + # for proto in range(pickle.HIGHEST_PROTOCOL + 1): + # f_copy = pickle.loads(pickle.dumps(f, proto)) + # try: + # self.assertIs(f_copy.args[0], f_copy) + # finally: + # f_copy.__setstate__((capture, (), {}, {})) + # finally: + # f.__setstate__((capture, (), {}, {})) + + # f = self.partial(capture) + # f.__setstate__((capture, (), {'a': f}, {})) + # try: + # for proto in range(pickle.HIGHEST_PROTOCOL + 1): + # f_copy = pickle.loads(pickle.dumps(f, proto)) + # try: + # self.assertIs(f_copy.keywords['a'], f_copy) + # finally: + # f_copy.__setstate__((capture, (), {}, {})) + # finally: + # f.__setstate__((capture, (), {}, {})) + + # # Issue 6083: Reference counting bug + # def test_setstate_refcount(self): + # class BadSequence: + # def __len__(self): + # return 4 + # def __getitem__(self, key): + # if key == 0: + # return max + # elif key == 1: + # return tuple(range(1000000)) + # elif key in (2, 3): + # return {} + # raise IndexError + + # f = self.partial(object) + # self.assertRaises(TypeError, f.__setstate__, BadSequence()) + +# @unittest.skipUnless(c_functools, 'requires the C _functools module') +class TestPartialC(TestPartial, unittest.TestCase): + if c_functools: + partial = c_functools.partial + + class AllowPickle: + def __enter__(self): + return self + def __exit__(self, type, value, tb): + return False + + def test_attributes_unwritable(self): + # attributes should not be writable + p = self.partial(capture, 1, 2, a=10, b=20) + self.assertRaises(AttributeError, setattr, p, 'func', map) + self.assertRaises(AttributeError, setattr, p, 'args', (1, 2)) + self.assertRaises(AttributeError, setattr, p, 'keywords', dict(a=1, b=2)) + + p = self.partial(hex) + try: + del p.__dict__ + except TypeError: + pass + else: + self.fail('partial object allowed __dict__ to be deleted') + + def test_manually_adding_non_string_keyword(self): + p = self.partial(capture) + # Adding a non-string/unicode keyword to partial kwargs + p.keywords[1234] = 'value' + r = repr(p) + self.assertIn('1234', r) + self.assertIn("'value'", r) + with self.assertRaises(TypeError): + p() + + def test_keystr_replaces_value(self): + p = self.partial(capture) + + class MutatesYourDict(object): + def __str__(self): + p.keywords[self] = ['sth2'] + return 'astr' + + # Replacing the value during key formatting should keep the original + # value alive (at least long enough). + p.keywords[MutatesYourDict()] = ['sth'] + r = repr(p) + self.assertIn('astr', r) + self.assertIn("['sth']", r) + + +# class TestPartialPy(TestPartial, unittest.TestCase): +# partial = py_functools.partial + +# class AllowPickle: +# def __init__(self): +# self._cm = replaced_module("functools", py_functools) +# def __enter__(self): +# return self._cm.__enter__() +# def __exit__(self, type, value, tb): +# return self._cm.__exit__(type, value, tb) + +if c_functools: + class CPartialSubclass(c_functools.partial): + pass + +# class PyPartialSubclass(py_functools.partial): +# pass + +# @unittest.skipUnless(c_functools, 'requires the C _functools module') +class TestPartialCSubclass(TestPartialC): + if c_functools: + partial = CPartialSubclass + + # partial subclasses are not optimized for nested calls + test_nested_optimization = lambda *args: None + +# class TestPartialPySubclass(TestPartialPy): +# partial = PyPartialSubclass + +class TestPartialMethod(unittest.TestCase): + + class A(object): + nothing = functools.partialmethod(capture) + positional = functools.partialmethod(capture, 1) + keywords = functools.partialmethod(capture, a=2) + both = functools.partialmethod(capture, 3, b=4) + spec_keywords = functools.partialmethod(capture, self=1, func=2) + + nested = functools.partialmethod(positional, 5) + + over_partial = functools.partialmethod(functools.partial(capture, c=6), 7) + + static = functools.partialmethod(staticmethod(capture), 8) + cls = functools.partialmethod(classmethod(capture), d=9) + + a = A() + + def test_arg_combinations(self): + self.assertEqual(self.a.nothing(), ((self.a,), {})) + self.assertEqual(self.a.nothing(5), ((self.a, 5), {})) + self.assertEqual(self.a.nothing(c=6), ((self.a,), {'c': 6})) + self.assertEqual(self.a.nothing(5, c=6), ((self.a, 5), {'c': 6})) + + self.assertEqual(self.a.positional(), ((self.a, 1), {})) + self.assertEqual(self.a.positional(5), ((self.a, 1, 5), {})) + self.assertEqual(self.a.positional(c=6), ((self.a, 1), {'c': 6})) + self.assertEqual(self.a.positional(5, c=6), ((self.a, 1, 5), {'c': 6})) + + self.assertEqual(self.a.keywords(), ((self.a,), {'a': 2})) + self.assertEqual(self.a.keywords(5), ((self.a, 5), {'a': 2})) + self.assertEqual(self.a.keywords(c=6), ((self.a,), {'a': 2, 'c': 6})) + self.assertEqual(self.a.keywords(5, c=6), ((self.a, 5), {'a': 2, 'c': 6})) + + self.assertEqual(self.a.both(), ((self.a, 3), {'b': 4})) + self.assertEqual(self.a.both(5), ((self.a, 3, 5), {'b': 4})) + self.assertEqual(self.a.both(c=6), ((self.a, 3), {'b': 4, 'c': 6})) + self.assertEqual(self.a.both(5, c=6), ((self.a, 3, 5), {'b': 4, 'c': 6})) + + self.assertEqual(self.A.both(self.a, 5, c=6), ((self.a, 3, 5), {'b': 4, 'c': 6})) + + self.assertEqual(self.a.spec_keywords(), ((self.a,), {'self': 1, 'func': 2})) + + def test_nested(self): + self.assertEqual(self.a.nested(), ((self.a, 1, 5), {})) + self.assertEqual(self.a.nested(6), ((self.a, 1, 5, 6), {})) + self.assertEqual(self.a.nested(d=7), ((self.a, 1, 5), {'d': 7})) + self.assertEqual(self.a.nested(6, d=7), ((self.a, 1, 5, 6), {'d': 7})) + + self.assertEqual(self.A.nested(self.a, 6, d=7), ((self.a, 1, 5, 6), {'d': 7})) + + def test_over_partial(self): + self.assertEqual(self.a.over_partial(), ((self.a, 7), {'c': 6})) + self.assertEqual(self.a.over_partial(5), ((self.a, 7, 5), {'c': 6})) + self.assertEqual(self.a.over_partial(d=8), ((self.a, 7), {'c': 6, 'd': 8})) + self.assertEqual(self.a.over_partial(5, d=8), ((self.a, 7, 5), {'c': 6, 'd': 8})) + + self.assertEqual(self.A.over_partial(self.a, 5, d=8), ((self.a, 7, 5), {'c': 6, 'd': 8})) + + def test_bound_method_introspection(self): + obj = self.a + self.assertIs(obj.both.__self__, obj) + self.assertIs(obj.nested.__self__, obj) + self.assertIs(obj.over_partial.__self__, obj) + self.assertIs(obj.cls.__self__, self.A) + self.assertIs(self.A.cls.__self__, self.A) + + def test_unbound_method_retrieval(self): + obj = self.A + self.assertFalse(hasattr(obj.both, "__self__")) + self.assertFalse(hasattr(obj.nested, "__self__")) + self.assertFalse(hasattr(obj.over_partial, "__self__")) + self.assertFalse(hasattr(obj.static, "__self__")) + self.assertFalse(hasattr(self.a.static, "__self__")) + + def test_descriptors(self): + for obj in [self.A, self.a]: + # with self.subTest(obj=obj): + self.assertEqual(obj.static(), ((8,), {})) + self.assertEqual(obj.static(5), ((8, 5), {})) + self.assertEqual(obj.static(d=8), ((8,), {'d': 8})) + self.assertEqual(obj.static(5, d=8), ((8, 5), {'d': 8})) + + self.assertEqual(obj.cls(), ((self.A,), {'d': 9})) + self.assertEqual(obj.cls(5), ((self.A, 5), {'d': 9})) + self.assertEqual(obj.cls(c=8), ((self.A,), {'c': 8, 'd': 9})) + self.assertEqual(obj.cls(5, c=8), ((self.A, 5), {'c': 8, 'd': 9})) + + def test_overriding_keywords(self): + self.assertEqual(self.a.keywords(a=3), ((self.a,), {'a': 3})) + self.assertEqual(self.A.keywords(self.a, a=3), ((self.a,), {'a': 3})) + + def test_invalid_args(self): + with self.assertRaises(TypeError): + class B(object): + method = functools.partialmethod(None, 1) + with self.assertRaises(TypeError): + class B: + method = functools.partialmethod() + with self.assertRaises(TypeError): + class B: + method = functools.partialmethod(func=capture, a=1) + + def test_repr(self): + self.assertEqual(repr(self.A.__dict__['both']), + 'functools.partialmethod({}, 3, b=4)'.format(capture)) + + # def test_abstract(self): + # class Abstract(abc.ABCMeta): + + # @abc.abstractmethod + # def add(self, x, y): + # pass + + # add5 = functools.partialmethod(add, 5) + + # self.assertTrue(Abstract.add.__isabstractmethod__) + # self.assertTrue(Abstract.add5.__isabstractmethod__) + + # for func in [self.A.static, self.A.cls, self.A.over_partial, self.A.nested, self.A.both]: + # self.assertFalse(getattr(func, '__isabstractmethod__', False)) + + # def test_positional_only(self): + # def f(a, b, /): + # return a + b + + # p = functools.partial(f, 1) + # self.assertEqual(p(2), f(1, 2)) + + +class TestUpdateWrapper(unittest.TestCase): + + def check_wrapper(self, wrapper, wrapped, + assigned=functools.WRAPPER_ASSIGNMENTS, + updated=functools.WRAPPER_UPDATES): + # Check attributes were assigned + for name in assigned: + self.assertIs(getattr(wrapper, name), getattr(wrapped, name)) + # Check attributes were updated + for name in updated: + wrapper_attr = getattr(wrapper, name) + wrapped_attr = getattr(wrapped, name) + for key in wrapped_attr: + if name == "__dict__" and key == "__wrapped__": + # __wrapped__ is overwritten by the update code + continue + self.assertIs(wrapped_attr[key], wrapper_attr[key]) + # Check __wrapped__ + self.assertIs(wrapper.__wrapped__, wrapped) + + + def _default_update(self): + def f(a:'This is a new annotation'): + """This is a test""" + pass + f.attr = 'This is also a test' + f.__wrapped__ = "This is a bald faced lie" + f.__doc__ = 'This is a test' # skulpt does not yet parse docstrings + def wrapper(b:'This is the prior annotation'): + pass + functools.update_wrapper(wrapper, f) + return wrapper, f + + def test_default_update(self): + wrapper, f = self._default_update() + self.check_wrapper(wrapper, f) + self.assertIs(wrapper.__wrapped__, f) + self.assertEqual(wrapper.__name__, 'f') + self.assertEqual(wrapper.__qualname__, f.__qualname__) + self.assertEqual(wrapper.attr, 'This is also a test') + # self.assertEqual(wrapper.__annotations__['a'], 'This is a new annotation') + # self.assertNotIn('b', wrapper.__annotations__) + + # @unittest.skipIf(sys.flags.optimize >= 2, + # "Docstrings are omitted with -O2 and above") + def test_default_update_doc(self): + wrapper, f = self._default_update() + self.assertEqual(wrapper.__doc__, 'This is a test') + + def test_no_update(self): + def f(): + """This is a test""" + pass + f.attr = 'This is also a test' + def wrapper(): + pass + functools.update_wrapper(wrapper, f, (), ()) + self.check_wrapper(wrapper, f, (), ()) + self.assertEqual(wrapper.__name__, 'wrapper') + self.assertNotEqual(wrapper.__qualname__, f.__qualname__) + self.assertEqual(wrapper.__doc__, None) + # self.assertEqual(wrapper.__annotations__, {}) + self.assertFalse(hasattr(wrapper, 'attr')) + + def test_selective_update(self): + def f(): + pass + f.attr = 'This is a different test' + f.dict_attr = dict(a=1, b=2, c=3) + def wrapper(): + pass + wrapper.dict_attr = {} + assign = ('attr',) + update = ('dict_attr',) + functools.update_wrapper(wrapper, f, assign, update) + self.check_wrapper(wrapper, f, assign, update) + self.assertEqual(wrapper.__name__, 'wrapper') + self.assertNotEqual(wrapper.__qualname__, f.__qualname__) + self.assertEqual(wrapper.__doc__, None) + self.assertEqual(wrapper.attr, 'This is a different test') + self.assertEqual(wrapper.dict_attr, f.dict_attr) + + def test_missing_attributes(self): + def f(): + pass + def wrapper(): + pass + wrapper.dict_attr = {} + assign = ('attr',) + update = ('dict_attr',) + # Missing attributes on wrapped object are ignored + functools.update_wrapper(wrapper, f, assign, update) + self.assertNotIn('attr', wrapper.__dict__) + self.assertEqual(wrapper.dict_attr, {}) + # Wrapper must have expected attributes for updating + del wrapper.dict_attr + with self.assertRaises(AttributeError): + functools.update_wrapper(wrapper, f, assign, update) + wrapper.dict_attr = 1 + with self.assertRaises(AttributeError): + functools.update_wrapper(wrapper, f, assign, update) + + # @support.requires_docstrings + # @unittest.skipIf(sys.flags.optimize >= 2, + # "Docstrings are omitted with -O2 and above") + def test_builtin_update(self): + # Test for bug #1576241 + def wrapper(): + pass + functools.update_wrapper(wrapper, max) + self.assertEqual(wrapper.__name__, 'max') + self.assertTrue(wrapper.__doc__.startswith('max(')) + # self.assertEqual(wrapper.__annotations__, {}) + + +class TestWraps(TestUpdateWrapper): + + def _default_update(self): + def f(): + """This is a test""" + pass + f.attr = 'This is also a test' + f.__wrapped__ = "This is still a bald faced lie" + f.__doc__ = "This is a test" # skulpt does not yet parse docstrings + @functools.wraps(f) + def wrapper(): + pass + return wrapper, f + + def test_default_update(self): + wrapper, f = self._default_update() + self.check_wrapper(wrapper, f) + self.assertEqual(wrapper.__name__, 'f') + self.assertEqual(wrapper.__qualname__, f.__qualname__) + self.assertEqual(wrapper.attr, 'This is also a test') + + # @unittest.skipIf(sys.flags.optimize >= 2, + # "Docstrings are omitted with -O2 and above") + def test_default_update_doc(self): + wrapper, _ = self._default_update() + self.assertEqual(wrapper.__doc__, 'This is a test') + + def test_no_update(self): + def f(): + """This is a test""" + pass + f.attr = 'This is also a test' + @functools.wraps(f, (), ()) + def wrapper(): + pass + self.check_wrapper(wrapper, f, (), ()) + self.assertEqual(wrapper.__name__, 'wrapper') + self.assertNotEqual(wrapper.__qualname__, f.__qualname__) + self.assertEqual(wrapper.__doc__, None) + self.assertFalse(hasattr(wrapper, 'attr')) + + def test_selective_update(self): + def f(): + pass + f.attr = 'This is a different test' + f.dict_attr = dict(a=1, b=2, c=3) + def add_dict_attr(f): + f.dict_attr = {} + return f + assign = ('attr',) + update = ('dict_attr',) + @functools.wraps(f, assign, update) + @add_dict_attr + def wrapper(): + pass + self.check_wrapper(wrapper, f, assign, update) + self.assertEqual(wrapper.__name__, 'wrapper') + self.assertNotEqual(wrapper.__qualname__, f.__qualname__) + self.assertEqual(wrapper.__doc__, None) + self.assertEqual(wrapper.attr, 'This is a different test') + self.assertEqual(wrapper.dict_attr, f.dict_attr) + + +class TestReduce: + def test_reduce(self): + class Squares: + def __init__(self, max): + self.max = max + self.sofar = [] + + def __len__(self): + return len(self.sofar) + + def __getitem__(self, i): + if not 0 <= i < self.max: raise IndexError + n = len(self.sofar) + while n <= i: + self.sofar.append(n*n) + n += 1 + return self.sofar[i] + def add(x, y): + return x + y + self.assertEqual(self.reduce(add, ['a', 'b', 'c'], ''), 'abc') + self.assertEqual( + self.reduce(add, [['a', 'c'], [], ['d', 'w']], []), + ['a','c','d','w'] + ) + self.assertEqual(self.reduce(lambda x, y: x*y, range(2,8), 1), 5040) + self.assertEqual( + self.reduce(lambda x, y: x*y, range(2,21), 1), + 2432902008176640000 + ) + self.assertEqual(self.reduce(add, Squares(10)), 285) + self.assertEqual(self.reduce(add, Squares(10), 0), 285) + self.assertEqual(self.reduce(add, Squares(0), 0), 0) + self.assertRaises(TypeError, self.reduce) + self.assertRaises(TypeError, self.reduce, 42, 42) + self.assertRaises(TypeError, self.reduce, 42, 42, 42) + self.assertEqual(self.reduce(42, "1"), "1") # func is never called with one item + self.assertEqual(self.reduce(42, "", "1"), "1") # func is never called with one item + self.assertRaises(TypeError, self.reduce, 42, (42, 42)) + self.assertRaises(TypeError, self.reduce, add, []) # arg 2 must not be empty sequence with no initial value + self.assertRaises(TypeError, self.reduce, add, "") + self.assertRaises(TypeError, self.reduce, add, ()) + self.assertRaises(TypeError, self.reduce, add, object()) + + class TestFailingIter: + def __iter__(self): + raise RuntimeError + self.assertRaises(RuntimeError, self.reduce, add, TestFailingIter()) + + self.assertEqual(self.reduce(add, [], None), None) + self.assertEqual(self.reduce(add, [], 42), 42) + + class BadSeq: + def __getitem__(self, index): + raise ValueError + self.assertRaises(ValueError, self.reduce, 42, BadSeq()) + + # Test reduce()'s use of iterators. + def test_iterator_usage(self): + class SequenceClass: + def __init__(self, n): + self.n = n + def __getitem__(self, i): + if 0 <= i < self.n: + return i + else: + raise IndexError + + from operator import add + self.assertEqual(self.reduce(add, SequenceClass(5)), 10) + self.assertEqual(self.reduce(add, SequenceClass(5), 42), 52) + self.assertRaises(TypeError, self.reduce, add, SequenceClass(0)) + self.assertEqual(self.reduce(add, SequenceClass(0), 42), 42) + self.assertEqual(self.reduce(add, SequenceClass(1)), 0) + self.assertEqual(self.reduce(add, SequenceClass(1), 42), 42) + + d = {"one": 1, "two": 2, "three": 3} + self.assertEqual(self.reduce(add, d), "".join(d.keys())) + + +# @unittest.skipUnless(c_functools, 'requires the C _functools module') +class TestReduceC(TestReduce, unittest.TestCase): + if c_functools: + reduce = c_functools.reduce + + +# class TestReducePy(TestReduce, unittest.TestCase): +# reduce = staticmethod(py_functools.reduce) + + +class TestCmpToKey: + + def test_cmp_to_key(self): + def cmp1(x, y): + return (x > y) - (x < y) + key = self.cmp_to_key(cmp1) + self.assertEqual(key(3), key(3)) + self.assertGreater(key(3), key(1)) + self.assertGreaterEqual(key(3), key(3)) + + def cmp2(x, y): + return int(x) - int(y) + key = self.cmp_to_key(cmp2) + self.assertEqual(key(4.0), key('4')) + self.assertLess(key(2), key('35')) + self.assertLessEqual(key(2), key('35')) + self.assertNotEqual(key(2), key('35')) + + def test_cmp_to_key_arguments(self): + def cmp1(x, y): + return (x > y) - (x < y) + key = self.cmp_to_key(mycmp=cmp1) + self.assertEqual(key(obj=3), key(obj=3)) + self.assertGreater(key(obj=3), key(obj=1)) + with self.assertRaises((TypeError, AttributeError)): + key(3) > 1 # rhs is not a K object + with self.assertRaises((TypeError, AttributeError)): + 1 < key(3) # lhs is not a K object + with self.assertRaises(TypeError): + key = self.cmp_to_key() # too few args + with self.assertRaises(TypeError): + key = self.cmp_to_key(cmp1, None) # too many args + key = self.cmp_to_key(cmp1) + with self.assertRaises(TypeError): + key() # too few args + with self.assertRaises(TypeError): + key(None, None) # too many args + + def test_bad_cmp(self): + def cmp1(x, y): + raise ZeroDivisionError + key = self.cmp_to_key(cmp1) + with self.assertRaises(ZeroDivisionError): + key(3) > key(1) + + class BadCmp: + def __lt__(self, other): + raise ZeroDivisionError + def cmp1(x, y): + return BadCmp() + with self.assertRaises(ZeroDivisionError): + key(3) > key(1) + + def test_obj_field(self): + def cmp1(x, y): + return (x > y) - (x < y) + key = self.cmp_to_key(mycmp=cmp1) + self.assertEqual(key(50).obj, 50) + + def test_sort_int(self): + def mycmp(x, y): + return y - x + self.assertEqual(sorted(range(5), key=self.cmp_to_key(mycmp)), + [4, 3, 2, 1, 0]) + + def test_sort_int_str(self): + def mycmp(x, y): + x, y = int(x), int(y) + return (x > y) - (x < y) + values = [5, '3', 7, 2, '0', '1', 4, '10', 1] + values = sorted(values, key=self.cmp_to_key(mycmp)) + self.assertEqual([int(value) for value in values], + [0, 1, 1, 2, 3, 4, 5, 7, 10]) + + def test_hash(self): + def mycmp(x, y): + return y - x + key = self.cmp_to_key(mycmp) + k = key(10) + self.assertRaises(TypeError, hash, k) + # self.assertNotIsInstance(k, collections.abc.Hashable) + + +# @unittest.skipUnless(c_functools, 'requires the C _functools module') +class TestCmpToKeyC(TestCmpToKey, unittest.TestCase): + if c_functools: + cmp_to_key = c_functools.cmp_to_key + + +# class TestCmpToKeyPy(TestCmpToKey, unittest.TestCase): +# cmp_to_key = staticmethod(py_functools.cmp_to_key) + +class TestTotalOrdering(unittest.TestCase): + + def test_total_ordering_lt(self): + @functools.total_ordering + class A: + def __init__(self, value): + self.value = value + def __lt__(self, other): + return self.value < other.value + def __eq__(self, other): + return self.value == other.value + self.assertTrue(A(1) < A(2)) + self.assertTrue(A(2) > A(1)) + self.assertTrue(A(1) <= A(2)) + self.assertTrue(A(2) >= A(1)) + self.assertTrue(A(2) <= A(2)) + self.assertTrue(A(2) >= A(2)) + self.assertFalse(A(1) > A(2)) + + def test_total_ordering_le(self): + @functools.total_ordering + class A: + def __init__(self, value): + self.value = value + def __le__(self, other): + return self.value <= other.value + def __eq__(self, other): + return self.value == other.value + self.assertTrue(A(1) < A(2)) + self.assertTrue(A(2) > A(1)) + self.assertTrue(A(1) <= A(2)) + self.assertTrue(A(2) >= A(1)) + self.assertTrue(A(2) <= A(2)) + self.assertTrue(A(2) >= A(2)) + self.assertFalse(A(1) >= A(2)) + + def test_total_ordering_gt(self): + @functools.total_ordering + class A: + def __init__(self, value): + self.value = value + def __gt__(self, other): + return self.value > other.value + def __eq__(self, other): + return self.value == other.value + self.assertTrue(A(1) < A(2)) + self.assertTrue(A(2) > A(1)) + self.assertTrue(A(1) <= A(2)) + self.assertTrue(A(2) >= A(1)) + self.assertTrue(A(2) <= A(2)) + self.assertTrue(A(2) >= A(2)) + self.assertFalse(A(2) < A(1)) + + def test_total_ordering_ge(self): + @functools.total_ordering + class A: + def __init__(self, value): + self.value = value + def __ge__(self, other): + return self.value >= other.value + def __eq__(self, other): + return self.value == other.value + self.assertTrue(A(1) < A(2)) + self.assertTrue(A(2) > A(1)) + self.assertTrue(A(1) <= A(2)) + self.assertTrue(A(2) >= A(1)) + self.assertTrue(A(2) <= A(2)) + self.assertTrue(A(2) >= A(2)) + self.assertFalse(A(2) <= A(1)) + + def test_total_ordering_no_overwrite(self): + # new methods should not overwrite existing + @functools.total_ordering + class A(int): + pass + self.assertTrue(A(1) < A(2)) + self.assertTrue(A(2) > A(1)) + self.assertTrue(A(1) <= A(2)) + self.assertTrue(A(2) >= A(1)) + self.assertTrue(A(2) <= A(2)) + self.assertTrue(A(2) >= A(2)) + + def test_no_operations_defined(self): + with self.assertRaises(ValueError): + @functools.total_ordering + class A: + pass + + def test_type_error_when_not_implemented(self): + # bug 10042; ensure stack overflow does not occur + # when decorated types return NotImplemented + @functools.total_ordering + class ImplementsLessThan: + def __init__(self, value): + self.value = value + def __eq__(self, other): + if isinstance(other, ImplementsLessThan): + return self.value == other.value + return False + def __lt__(self, other): + if isinstance(other, ImplementsLessThan): + return self.value < other.value + return NotImplemented + + @functools.total_ordering + class ImplementsGreaterThan: + def __init__(self, value): + self.value = value + def __eq__(self, other): + if isinstance(other, ImplementsGreaterThan): + return self.value == other.value + return False + def __gt__(self, other): + if isinstance(other, ImplementsGreaterThan): + return self.value > other.value + return NotImplemented + + @functools.total_ordering + class ImplementsLessThanEqualTo: + def __init__(self, value): + self.value = value + def __eq__(self, other): + if isinstance(other, ImplementsLessThanEqualTo): + return self.value == other.value + return False + def __le__(self, other): + if isinstance(other, ImplementsLessThanEqualTo): + return self.value <= other.value + return NotImplemented + + @functools.total_ordering + class ImplementsGreaterThanEqualTo: + def __init__(self, value): + self.value = value + def __eq__(self, other): + if isinstance(other, ImplementsGreaterThanEqualTo): + return self.value == other.value + return False + def __ge__(self, other): + if isinstance(other, ImplementsGreaterThanEqualTo): + return self.value >= other.value + return NotImplemented + + @functools.total_ordering + class ComparatorNotImplemented: + def __init__(self, value): + self.value = value + def __eq__(self, other): + if isinstance(other, ComparatorNotImplemented): + return self.value == other.value + return False + def __lt__(self, other): + return NotImplemented + + with self.assertRaises(TypeError): + ImplementsLessThan(-1) < 1 + + with self.assertRaises(TypeError): + ImplementsLessThan(0) < ImplementsLessThanEqualTo(0) + + with self.assertRaises(TypeError): + ImplementsLessThan(1) < ImplementsGreaterThan(1) + + with self.assertRaises(TypeError): + ImplementsLessThanEqualTo(2) <= ImplementsLessThan(2) + + with self.assertRaises(TypeError): + ImplementsLessThanEqualTo(3) <= ImplementsGreaterThanEqualTo(3) + + with self.assertRaises(TypeError): + ImplementsGreaterThan(4) > ImplementsGreaterThanEqualTo(4) + + with self.assertRaises(TypeError): + ImplementsGreaterThan(5) > ImplementsLessThan(5) + + with self.assertRaises(TypeError): + ImplementsGreaterThanEqualTo(6) >= ImplementsGreaterThan(6) + + with self.assertRaises(TypeError): + ImplementsGreaterThanEqualTo(7) >= ImplementsLessThanEqualTo(7) + + # with self.subTest("GE when equal"): + # a = ComparatorNotImplemented(8) + # b = ComparatorNotImplemented(8) + # self.assertEqual(a, b) + # with self.assertRaises(TypeError): + # a >= b + + # with self.subTest("LE when equal"): + # a = ComparatorNotImplemented(9) + # b = ComparatorNotImplemented(9) + # self.assertEqual(a, b) + # with self.assertRaises(TypeError): + # a <= b + +# def test_pickle(self): +# for proto in range(pickle.HIGHEST_PROTOCOL + 1): +# for name in '__lt__', '__gt__', '__le__', '__ge__': +# with self.subTest(method=name, proto=proto): +# method = getattr(Orderable_LT, name) +# method_copy = pickle.loads(pickle.dumps(method, proto)) +# self.assertIs(method_copy, method) + +# @functools.total_ordering +# class Orderable_LT: +# def __init__(self, value): +# self.value = value +# def __lt__(self, other): +# return self.value < other.value +# def __eq__(self, other): +# return self.value == other.value + +if __name__ == '__main__': + unittest.main(verbosity=1) diff --git a/test/unit3/test_itertools.py b/test/unit3/test_itertools.py index c13ce56a11..270d916c7b 100644 --- a/test/unit3/test_itertools.py +++ b/test/unit3/test_itertools.py @@ -8,7 +8,7 @@ # import random import copy # import pickle -# from functools import reduce +from functools import reduce import sys # import struct # import threading From 4a474bbf39fff2a71c4e02a7ea2a69aef6739f83 Mon Sep 17 00:00:00 2001 From: mrcork Date: Mon, 28 Sep 2020 15:45:37 +0800 Subject: [PATCH 6/6] add __doc__ --- src/lib/functools.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/lib/functools.js b/src/lib/functools.js index 8b30e6272f..679f9d785b 100644 --- a/src/lib/functools.js +++ b/src/lib/functools.js @@ -1,6 +1,7 @@ function $builtinmodule() { const functools = { __name__: new Sk.builtin.str("functools"), + __doc__: new Sk.builtin.str("Tools for working with functions and callable objects"), __all__: new Sk.builtin.list( [ "update_wrapper",