Repository: Unidata/siphon

Issue:
Improve catalog dataset string representation 
Currently if we look at the catalog of GOES-16 data:

```python
from datetime import datetime
from siphon.catalog import TDSCatalog

date = datetime.utcnow()
channel = 8
region = 'Mesoscale-2'

cat = TDSCatalog('http://thredds-test.unidata.ucar.edu/thredds/catalog/satellite/goes16/GOES16/'
                 '{}/Channel{:02d}/{:%Y%m%d}/catalog.xml'.format(region, channel, date))
```

and display the most recent datasets:

```python
list(cat.datasets)[-5:]
```

We get a nice representation:
```
['GOES16_Mesoscale-2_20170920_085559_6.19_2km_16.6N_62.2W.nc4',
 'GOES16_Mesoscale-2_20170920_085659_6.19_2km_16.6N_62.2W.nc4',
 'GOES16_Mesoscale-2_20170920_085759_6.19_2km_16.6N_62.2W.nc4',
 'GOES16_Mesoscale-2_20170920_085859_6.19_2km_16.6N_62.2W.nc4',
 'GOES16_Mesoscale-2_20170920_085959_6.19_2km_37.5N_86.3W.nc4']
```

It'd be nice to get that for just printing `cat.datasets[-5:]` as currently we get:

```
[<siphon.catalog.Dataset at 0x116996358>,
 <siphon.catalog.Dataset at 0x1169963c8>,
 <siphon.catalog.Dataset at 0x116996470>,
 <siphon.catalog.Dataset at 0x116996630>,
 <siphon.catalog.Dataset at 0x1169964e0>]
```

Discussion:
jrleeman: Should be ready to go @dopplershift - looks like the code coverage again isn't related.

Patch:
diff --git a/environment.yml b/environment.yml
index b6005bf3..7265b1d7 100755
--- a/environment.yml
+++ b/environment.yml
@@ -13,6 +13,7 @@ dependencies:
   - sphinx-gallery
   - doc8
   - pytest
+  - pytest-catchlog
   - pytest-cov
   - pytest-flake8
   - pytest-runner
diff --git a/siphon/catalog.py b/siphon/catalog.py
index 9fa9f37a..2ef9eb4f 100644
--- a/siphon/catalog.py
+++ b/siphon/catalog.py
@@ -133,6 +133,8 @@ class DatasetCollection(IndexableMapping):
         """Return a string representation of the collection."""
         return str(list(self))
 
+    __repr__ = __str__
+
 
 class TDSCatalog(object):
     """
@@ -237,6 +239,10 @@ class TDSCatalog(object):
 
         self._process_datasets()
 
+    def __str__(self):
+        """Return a string representation of the catalog name."""
+        return str(self.catalog_name)
+
     def _process_dataset(self, element):
         catalog_url = ''
         if 'urlPath' in element.attrib:
@@ -277,6 +283,8 @@ class TDSCatalog(object):
                 return TDSCatalog(latest_cat).datasets[0]
         raise AttributeError('"latest" not available for this catalog')
 
+    __repr__ = __str__
+
 
 class CatalogRef(object):
     """
@@ -312,6 +320,10 @@ class CatalogRef(object):
         href = element_node.attrib['{http://www.w3.org/1999/xlink}href']
         self.href = urljoin(base_url, href)
 
+    def __str__(self):
+        """Return a string representation of the catalog reference."""
+        return str(self.title)
+
     def follow(self):
         """Follow the catalog reference and return a new :class:`TDSCatalog`.
 
@@ -323,6 +335,8 @@ class CatalogRef(object):
         """
         return TDSCatalog(self.href)
 
+    __repr__ = __str__
+
 
 class Dataset(object):
     """
@@ -371,6 +385,10 @@ class Dataset(object):
                 log.warning('Must pass along the catalog URL to resolve '
                             'the latest.xml dataset!')
 
+    def __str__(self):
+        """Return a string representation of the dataset."""
+        return str(self.name)
+
     def resolve_url(self, catalog_url):
         """Resolve the url of the dataset when reading latest.xml.
 
@@ -576,6 +594,8 @@ class Dataset(object):
         except KeyError:
             raise ValueError(service + ' is not available for this dataset')
 
+    __repr__ = __str__
+
 
 class SimpleService(object):
     """Hold information about an access service enabled on a dataset.


Test patch:
diff --git a/siphon/tests/test_catalog.py b/siphon/tests/test_catalog.py
index 9b5497a2..0d850299 100644
--- a/siphon/tests/test_catalog.py
+++ b/siphon/tests/test_catalog.py
@@ -26,6 +26,14 @@ def test_basic():
     assert 'Forecast Model Data' in cat.catalog_refs
 
 
+@recorder.use_cassette('thredds-test-toplevel-catalog')
+def test_catalog_representation():
+    """Test string representation of the catalog object."""
+    url = 'http://thredds-test.unidata.ucar.edu/thredds/catalog.xml'
+    cat = TDSCatalog(url)
+    assert str(cat) == 'Unidata THREDDS Data Server'
+
+
 @recorder.use_cassette('thredds-test-latest-gfs-0p5')
 def test_access():
     """Test catalog parsing of access methods."""
@@ -147,6 +155,16 @@ def test_datasets_str():
                                  "'Latest Collection for NAM CONUS 20km']")
 
 
+@recorder.use_cassette('top_level_20km_rap_catalog')
+def test_datasets_sliced_str():
+    """Test that datasets are printed as expected when sliced."""
+    url = ('http://thredds.ucar.edu/thredds/catalog/grib/NCEP/NAM/'
+           'CONUS_20km/noaaport/catalog.xml')
+    cat = TDSCatalog(url)
+    assert str(cat.datasets[-2:]) == ('[Best NAM CONUS 20km Time Series, '
+                                      'Latest Collection for NAM CONUS 20km]')
+
+
 @recorder.use_cassette('top_level_20km_rap_catalog')
 def test_datasets_nearest_time():
     """Test getting dataset by time using filenames."""
@@ -266,3 +284,11 @@ def test_tds50_catalogref_follow():
     """Test following a catalog ref url on TDS 5."""
     cat = TDSCatalog('http://thredds-test.unidata.ucar.edu/thredds/catalog.xml')
     assert len(cat.catalog_refs[0].follow().catalog_refs) == 59
+
+
+@recorder.use_cassette('top_level_cat')
+def test_catalog_ref_str():
+    """Test that catalog references are properly represented as strings."""
+    url = 'http://thredds.ucar.edu/thredds/catalog.xml'
+    cat = TDSCatalog(url)
+    assert str(cat.catalog_refs[0]) == 'Forecast Model Data'