diff --git a/bson/__init__.py b/bson/__init__.py index acac701ab..8e8f8fd3e 100644 --- a/bson/__init__.py +++ b/bson/__init__.py @@ -572,7 +572,7 @@ def _encode_code(name, value, dummy, opts): """Encode bson.code.Code.""" cstring = _make_c_string(value) cstrlen = len(cstring) - if not value.scope: + if value.scope is None: return b"\x0D" + name + _PACK_INT(cstrlen) + cstring scope = _dict_to_bson(value.scope, False, opts, False) full_length = _PACK_INT(8 + cstrlen + len(scope)) diff --git a/bson/_cbsonmodule.c b/bson/_cbsonmodule.c index aea7387b2..3c29aefad 100644 --- a/bson/_cbsonmodule.c +++ b/bson/_cbsonmodule.c @@ -809,7 +809,7 @@ static int _write_element_to_buffer(PyObject* self, buffer_t buffer, return 0; } - if (!PyDict_Size(scope)) { + if (scope == Py_None) { Py_DECREF(scope); *(buffer_get_buffer(buffer) + type_byte) = 0x0D; return write_string(buffer, value); diff --git a/bson/code.py b/bson/code.py index 8a9c55824..1c8b30acf 100644 --- a/bson/code.py +++ b/bson/code.py @@ -16,7 +16,7 @@ """ import collections -from bson.py3compat import string_type +from bson.py3compat import string_type, PY3, text_type class Code(str): @@ -32,12 +32,19 @@ class Code(str): the `scope` dictionary. :Parameters: - - `code`: string containing JavaScript code to be evaluated + - `code`: A string containing JavaScript code to be evaluated or another + instance of Code. In the latter case, the scope of `code` becomes this + Code's :attr:`scope`. - `scope` (optional): dictionary representing the scope in which `code` should be evaluated - a mapping from identifiers (as - strings) to values + strings) to values. Defaults to ``None``. This is applied after any + scope associated with a given `code` above. - `**kwargs` (optional): scope variables can also be passed as - keyword arguments + keyword arguments. These are applied after `scope` and `code`. + + ..versionchanged:: 3.4 + The default value for :attr:`scope` is ``None`` instead of ``{}``. + """ _type_marker = 13 @@ -47,19 +54,29 @@ class Code(str): raise TypeError("code must be an " "instance of %s" % (string_type.__name__)) - self = str.__new__(cls, code) + if not PY3 and isinstance(code, text_type): + self = str.__new__(cls, code.encode('utf8')) + else: + self = str.__new__(cls, code) try: self.__scope = code.scope except AttributeError: - self.__scope = {} + self.__scope = None if scope is not None: if not isinstance(scope, collections.Mapping): raise TypeError("scope must be an instance of dict") - self.__scope.update(scope) + if self.__scope is not None: + self.__scope.update(scope) + else: + self.__scope = scope - self.__scope.update(kwargs) + if kwargs: + if self.__scope is not None: + self.__scope.update(kwargs) + else: + self.__scope = kwargs return self diff --git a/bson/json_util.py b/bson/json_util.py index 448b61dea..8a2e86afe 100644 --- a/bson/json_util.py +++ b/bson/json_util.py @@ -409,6 +409,8 @@ def default(obj, json_options=DEFAULT_JSON_OPTIONS): if isinstance(obj, Timestamp): return {"$timestamp": SON([("t", obj.time), ("i", obj.inc)])} if isinstance(obj, Code): + if obj.scope is None: + return SON([('$code', str(obj))]) return SON([('$code', str(obj)), ('$scope', obj.scope)]) if isinstance(obj, Binary): return SON([ diff --git a/doc/changelog.rst b/doc/changelog.rst index 95580615c..c99cb892d 100644 --- a/doc/changelog.rst +++ b/doc/changelog.rst @@ -10,7 +10,11 @@ Highlights include: - Finer control over JSON encoding/decoding with :class:`~bson.json_util.JSONOptions`. +- Allow :class:`~bson.code.Code` objects to have a scope of ``None``, signifying + no scope. Also allow encoding Code objects with an empty scope (i.e. ``{}``). + .. warning:: Starting in PyMongo 3.4, :attr:`~bson.code.Code.scope` may return + ``None``, as the default scope is ``None`` instead of ``{}``. Issues Resolved ............... diff --git a/test/test_bson.py b/test/test_bson.py index dc78acae7..aac5beba6 100644 --- a/test/test_bson.py +++ b/test/test_bson.py @@ -391,6 +391,11 @@ class TestBSON(unittest.TestCase): b"=\x00\x00\x00\x0f$field\x000\x00\x00\x00\x1f\x00" b"\x00\x00return function(){ return x; }\x00\t\x00" b"\x00\x00\x08x\x00\x00\x00\x00") + unicode_empty_scope = Code(u"function(){ return 'héllo';}", {}) + self.assertEqual(BSON.encode({'$field': unicode_empty_scope}), + b"8\x00\x00\x00\x0f$field\x00+\x00\x00\x00\x1e\x00" + b"\x00\x00function(){ return 'h\xc3\xa9llo';}\x00\x05" + b"\x00\x00\x00\x00\x00") a = ObjectId(b"\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0A\x0B") self.assertEqual(BSON.encode({"oid": a}), b"\x16\x00\x00\x00\x07\x6F\x69\x64\x00\x00\x01\x02" diff --git a/test/test_code.py b/test/test_code.py index 787903161..f06a84346 100644 --- a/test/test_code.py +++ b/test/test_code.py @@ -1,3 +1,5 @@ +# -*- coding: utf-8 -*- +# # Copyright 2009-2015 MongoDB, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -46,19 +48,26 @@ class TestCode(unittest.TestCase): self.assertTrue(a_code.endswith("world")) self.assertTrue(isinstance(a_code, Code)) self.assertFalse(isinstance(a_string, Code)) - self.assertEqual(a_code.scope, {}) - a_code.scope["my_var"] = 5 - self.assertEqual(a_code.scope, {"my_var": 5}) + self.assertIsNone(a_code.scope) + with_scope = Code('hello world', {'my_var': 5}) + self.assertEqual({'my_var': 5}, with_scope.scope) + empty_scope = Code('hello world', {}) + self.assertEqual({}, empty_scope.scope) + another_scope = Code(with_scope, {'new_var': 42}) + self.assertEqual(str(with_scope), str(another_scope)) + self.assertEqual({'new_var': 42, 'my_var': 5}, another_scope.scope) + # No error. + Code(u'héllø world¡') def test_repr(self): - c = Code("hello world") + c = Code("hello world", {}) self.assertEqual(repr(c), "Code('hello world', {})") c.scope["foo"] = "bar" self.assertEqual(repr(c), "Code('hello world', {'foo': 'bar'})") c = Code("hello world", {"blah": 3}) self.assertEqual(repr(c), "Code('hello world', {'blah': 3})") c = Code("\x08\xFF") - self.assertEqual(repr(c), "Code(%s, {})" % (repr("\x08\xFF"),)) + self.assertEqual(repr(c), "Code(%s, None)" % (repr("\x08\xFF"),)) def test_equality(self): b = Code("hello") @@ -67,14 +76,14 @@ class TestCode(unittest.TestCase): self.assertEqual(c, Code("hello", {"foo": 5})) self.assertNotEqual(c, Code("hello", {"foo": 6})) self.assertEqual(b, Code("hello")) - self.assertEqual(b, Code("hello", {})) + self.assertEqual(b, Code("hello", None)) self.assertNotEqual(b, Code("hello ")) self.assertNotEqual("hello", Code("hello")) # Explicitly test inequality self.assertFalse(c != Code("hello", {"foo": 5})) self.assertFalse(b != Code("hello")) - self.assertFalse(b != Code("hello", {})) + self.assertFalse(b != Code("hello", None)) def test_hash(self): self.assertRaises(TypeError, hash, Code("hello world")) diff --git a/test/test_json_util.py b/test/test_json_util.py index ce8a419d9..9dbb28cec 100644 --- a/test/test_json_util.py +++ b/test/test_json_util.py @@ -314,6 +314,10 @@ class TestJsonUtil(unittest.TestCase): # Check order. self.assertEqual('{"$code": "return z", "$scope": {"z": 2}}', res) + no_scope = Code('function() {}') + self.assertEqual( + '{"$code": "function() {}"}', json_util.dumps(no_scope)) + def test_undefined(self): jsn = '{"name": {"$undefined": true}}' self.assertIsNone(json_util.loads(jsn)['name'])