diff --git a/google/cloud/ndb/key.py b/google/cloud/ndb/key.py index d316aa66..906a865f 100644 --- a/google/cloud/ndb/key.py +++ b/google/cloud/ndb/key.py @@ -279,17 +279,9 @@ class Key(object): _hash_value = None def __new__(cls, *path_args, **kwargs): - # Avoid circular import in Python 2.7 - from google.cloud.ndb import context as context_module - _constructor_handle_positional(path_args, kwargs) instance = super(Key, cls).__new__(cls) - # Make sure to pass in the namespace if it's not explicitly set. - if kwargs.get("namespace", UNDEFINED) is UNDEFINED: - context = context_module.get_context() - kwargs["namespace"] = context.get_namespace() - if "reference" in kwargs or "serialized" in kwargs or "urlsafe" in kwargs: ds_key, reference = _parse_from_ref(cls, **kwargs) elif "pairs" in kwargs or "flat" in kwargs: @@ -1319,7 +1311,7 @@ def _parse_from_ref( def _parse_from_args( - pairs=None, flat=None, project=None, app=None, namespace=None, parent=None + pairs=None, flat=None, project=None, app=None, namespace=UNDEFINED, parent=None ): """Construct a key the path (and possibly a parent key). @@ -1344,6 +1336,9 @@ def _parse_from_args( Raises: .BadValueError: If ``parent`` is passed but is not a ``Key``. """ + # Avoid circular import in Python 2.7 + from google.cloud.ndb import context as context_module + flat = _get_path(flat, pairs) _clean_flat_path(flat) @@ -1355,12 +1350,20 @@ def _parse_from_args( parent_ds_key = None if parent is None: project = _project_from_app(app) + if namespace is UNDEFINED: + context = context_module.get_context() + namespace = context.get_namespace() + else: project = _project_from_app(app, allow_empty=True) if not isinstance(parent, Key): raise exceptions.BadValueError( "Expected Key instance, got {!r}".format(parent) ) + + if namespace is UNDEFINED: + namespace = None + # Offload verification of parent to ``google.cloud.datastore.Key()``. parent_ds_key = parent._key diff --git a/google/cloud/ndb/query.py b/google/cloud/ndb/query.py index 6eba8bd3..161ea092 100644 --- a/google/cloud/ndb/query.py +++ b/google/cloud/ndb/query.py @@ -1392,7 +1392,13 @@ def __init__( else: project = ancestor.app() if namespace is not None: - if namespace != ancestor.namespace(): + # if namespace is the empty string, that means default + # namespace, but after a put, if the ancestor is using + # the default namespace, its namespace will be None, + # so skip the test to avoid a false mismatch error. + if namespace == "" and ancestor.namespace() is None: + pass + elif namespace != ancestor.namespace(): raise TypeError("ancestor/namespace mismatch") else: namespace = ancestor.namespace() diff --git a/tests/system/test_query.py b/tests/system/test_query.py index 850d0be8..cdac0ae9 100644 --- a/tests/system/test_query.py +++ b/tests/system/test_query.py @@ -161,6 +161,56 @@ class SomeKind(ndb.Model): assert [entity.foo for entity in results] == [-1, 0, 1, 2, 3, 4] +def test_ancestor_query_with_namespace(client_context, dispose_of, other_namespace): + class Dummy(ndb.Model): + foo = ndb.StringProperty(default="") + + entity1 = Dummy(foo="bar", namespace="xyz") + parent_key = entity1.put() + dispose_of(entity1.key._key) + + entity2 = Dummy(foo="child", parent=parent_key, namespace=None) + entity2.put() + dispose_of(entity2.key._key) + + entity3 = Dummy(foo="childless", namespace="xyz") + entity3.put() + dispose_of(entity3.key._key) + + with client_context.new(namespace=other_namespace).use(): + query = Dummy.query(ancestor=parent_key, namespace="xyz") + results = eventually(query.fetch, length_equals(2)) + + assert results[0].foo == "bar" + assert results[1].foo == "child" + + +def test_ancestor_query_with_default_namespace( + client_context, dispose_of, other_namespace +): + class Dummy(ndb.Model): + foo = ndb.StringProperty(default="") + + entity1 = Dummy(foo="bar", namespace="") + parent_key = entity1.put() + dispose_of(entity1.key._key) + + entity2 = Dummy(foo="child", parent=parent_key) + entity2.put() + dispose_of(entity2.key._key) + + entity3 = Dummy(foo="childless", namespace="") + entity3.put() + dispose_of(entity3.key._key) + + with client_context.new(namespace=other_namespace).use(): + query = Dummy.query(ancestor=parent_key, namespace="") + results = eventually(query.fetch, length_equals(2)) + + assert results[0].foo == "bar" + assert results[1].foo == "child" + + @pytest.mark.usefixtures("client_context") def test_projection(ds_entity): entity_id = test_utils.system.unique_resource_id() diff --git a/tests/unit/test_key.py b/tests/unit/test_key.py index f7e2b150..78624544 100644 --- a/tests/unit/test_key.py +++ b/tests/unit/test_key.py @@ -222,6 +222,22 @@ def test_constructor_with_parent(self): ) assert key._reference is None + @pytest.mark.usefixtures("in_context") + def test_constructor_with_parent_and_namespace(self): + parent = key_module.Key(urlsafe=self.URLSAFE) + key = key_module.Key("Zip", 10, parent=parent, namespace=None) + + assert key._key == google.cloud.datastore.Key( + "Kind", "Thing", "Zip", 10, project="fire" + ) + assert key._reference is None + + @pytest.mark.usefixtures("in_context") + def test_constructor_with_parent_and_mismatched_namespace(self): + parent = key_module.Key(urlsafe=self.URLSAFE) + with pytest.raises(ValueError): + key_module.Key("Zip", 10, parent=parent, namespace="foo") + @pytest.mark.usefixtures("in_context") def test_constructor_with_parent_bad_type(self): parent = mock.sentinel.parent diff --git a/tests/unit/test_query.py b/tests/unit/test_query.py index 7e4966a2..672bce7a 100644 --- a/tests/unit/test_query.py +++ b/tests/unit/test_query.py @@ -1244,6 +1244,13 @@ def test_constructor_with_ancestor_and_namespace(): query = query_module.Query(ancestor=key, namespace="space") assert query.namespace == "space" + @staticmethod + @pytest.mark.usefixtures("in_context") + def test_constructor_with_ancestor_and_default_namespace(): + key = key_module.Key("a", "b", namespace=None) + query = query_module.Query(ancestor=key, namespace="") + assert query.namespace == "" + @staticmethod @pytest.mark.usefixtures("in_context") def test_constructor_with_ancestor_parameterized_thing():