Repository: intake/intake
Pull request #501 (2020-05-30 21:02:20)

FIX: entry property behavior when no entry
 - Catalogs are DataSource sub-classes
 - they now have an entry property that raises NoEntry which is a
   AttributeError sub-class
 - in the Catalog base class there is  a __getattr__ defined which
   gets triggered if attribute access raises AttributeError
 - there is code that tries calling self.entry and catches the NoEntry
   to handle the failure case
 - but __getattr__ catches it first and converts it to a normal
   AttributeError which escapes the try/except in configure_new

This adds an extra special case to the __getattr__ code to raise the
expected attribute and deprecates obj.entry.  It is only used a few
places internally which were replaced with checking
`self._entry is None`.


Patch:
diff --git a/intake/catalog/base.py b/intake/catalog/base.py
index 56ea955..2c46b10 100644
--- a/intake/catalog/base.py
+++ b/intake/catalog/base.py
@@ -21,7 +21,7 @@ import msgpack
 from ..auth.base import BaseClientAuth, AuthenticationFailure
 from .remote import RemoteCatalogEntry
 from .utils import flatten, reload_on_change, RemoteCatalogError
-from ..source.base import DataSource
+from ..source.base import DataSource, NoEntry
 from ..compat import unpack_kwargs, pack_kwargs
 logger = logging.getLogger('intake')
 
@@ -329,6 +329,10 @@ class Catalog(DataSource):
         return "<Intake catalog: %s>" % self.name
 
     def __getattr__(self, item):
+        # we need this special case here because the (deprecated) entry
+        # property on the base class
+        if item == 'entry':
+            raise NoEntry("Source was not made from a catalog entry")
         if not item.startswith('_'):
             # Fall back to __getitem__.
             try:
diff --git a/intake/source/base.py b/intake/source/base.py
index 86c5458..335d90a 100644
--- a/intake/source/base.py
+++ b/intake/source/base.py
@@ -11,6 +11,7 @@ from yaml import dump
 
 from .cache import make_caches
 from ..utils import make_path_posix, DictSerialiseMixin, pretty_describe
+import warnings
 
 
 class Schema(dict):
@@ -233,6 +234,7 @@ class DataSource(DictSerialiseMixin):
 
     @property
     def entry(self):
+        warnings.warn("direct access to the entry is deprecated", stacklevel=2)
         if self._entry is None:
             raise NoEntry("Source was not made from a catalog entry")
         return self._entry
@@ -248,11 +250,11 @@ class DataSource(DictSerialiseMixin):
         the original entry definition in a catalog **if** this source was originally
         created from a catalog.
         """
-        try:
-            obj = self.entry(**kwargs)
-            obj._entry = self.entry
+        if self._entry is not None:
+            obj = self._entry(**kwargs)
+            obj._entry = self._entry
             return obj
-        except NoEntry:  # no entry
+        else:
             kw = self._captured_init_kwargs.copy()
             kw.update(kwargs)
             return type(self)(*self._captured_init_args, **kw)
@@ -260,12 +262,18 @@ class DataSource(DictSerialiseMixin):
     __call__ = get = configure_new  # compatibility aliases
 
     def describe(self):
-        return self.entry.describe()
+        if self._entry is None:
+            raise NoEntry("Source was not made from a catalog entry")
+
+        return self._entry.describe()
 
     @property
     def gui(self):
         """Source GUI, with parameter selection and plotting"""
-        return self.entry.gui
+        if self._entry is None:
+            raise NoEntry("Source was not made from a catalog entry")
+
+        return self._entry.gui
 
     def close(self):
         """Close open resources corresponding to this data source."""


Test patch:
diff --git a/intake/catalog/tests/test_core.py b/intake/catalog/tests/test_core.py
new file mode 100644
index 0000000..e340bff
--- /dev/null
+++ b/intake/catalog/tests/test_core.py
@@ -0,0 +1,7 @@
+from intake.catalog.base import Catalog
+
+
+def test_no_entry():
+    cat = Catalog()
+    cat2 = cat.configure_new()
+    assert isinstance(cat2, Catalog)
diff --git a/intake/gui/source/tests/test_gui.py b/intake/gui/source/tests/test_gui.py
index fc4f541..593e782 100644
--- a/intake/gui/source/tests/test_gui.py
+++ b/intake/gui/source/tests/test_gui.py
@@ -14,6 +14,10 @@ def gui(sources1):
     return SourceGUI(sources=sources1)
 
 
+def test_gui_attribute(sources1):
+    assert sources1[0].gui
+
+
 def test_gui(gui, sources1):
     assert gui.select.items == sources1
     assert gui.sources == [sources1[0]]