Repository: yt-project/yt

Issue:
disk data object should do more error checking on inputs
As reported on the mailing list:

https://mail.python.org/mm3/archives/list/yt-users@python.org/thread/6JOJM5X7RSVXJI3U2GWXEPDGOSOHTMK4/

If you do something like this:

```python
import yt
ds = yt.load('IsolatedGalaxy/galaxy0030/galaxy0030')
disk = ds.disk(ds.domain_center, [0, 0, 1], ds.domain_center, (20, 'kpc'))
disk['density']
```

You will get a rather unfriendly error coming from the cython selection code:

```python traceback
~/Documents/yt-git-fixes/yt/data_objects/data_containers.py in __getitem__(self, key)
    280                 return self.field_data[f]
    281             else:
--> 282                 self.get_data(f)
    283         # fi.units is the unit expression string. We depend on the registry
    284         # hanging off the dataset to define this unit object.

~/Documents/yt-git-fixes/yt/data_objects/data_containers.py in get_data(self, fields)
   1264     def get_data(self, fields=None):
   1265         if self._current_chunk is None:
-> 1266             self.index._identify_base_chunk(self)
   1267         if fields is None: return
   1268         nfields = []

~/Documents/yt-git-fixes/yt/geometry/grid_geometry_handler.py in _identify_base_chunk(self, dobj)
    291             dobj._chunk_info[0] = weakref.proxy(dobj)
    292         elif getattr(dobj, "_grids", None) is None:
--> 293             gi = dobj.selector.select_grids(self.grid_left_edge,
    294                                             self.grid_right_edge,
    295                                             self.grid_levels)

~/Documents/yt-git-fixes/yt/data_objects/data_containers.py in selector(self)
   1216             self._selector = compose_selector(self, self._data_source.selector, sclass(self))
   1217         else:
-> 1218             self._selector = sclass(self)
   1219         return self._selector
   1220

~/Documents/yt-git-fixes/yt/geometry/selection_routines.pyx in yt.geometry.selection_routines.DiskSelector.__init__()
```

We should raise an error inside the disk initializer. We should also make sure that all of the inputs are sanitized by the time the initializer is finished.


Discussion:

Resolving patch:
diff --git a/yt/data_objects/selection_data_containers.py b/yt/data_objects/selection_data_containers.py
--- a/yt/data_objects/selection_data_containers.py
+++ b/yt/data_objects/selection_data_containers.py
@@ -18,13 +18,11 @@
 
 from yt.data_objects.data_containers import \
     YTSelectionContainer0D, YTSelectionContainer1D, \
-    YTSelectionContainer2D, YTSelectionContainer3D
-from yt.funcs import \
-    ensure_list, \
-    iterable, \
-    validate_width_tuple, \
-    fix_length, \
-    fix_axis
+    YTSelectionContainer2D, YTSelectionContainer3D, YTSelectionContainer
+from yt.data_objects.static_output import Dataset
+from yt.funcs import ensure_list, iterable, validate_width_tuple,\
+    fix_length, fix_axis, validate_3d_array, validate_float,\
+    validate_iterable, validate_object
 from yt.geometry.selection_routines import \
     points_in_cells
 from yt.units.yt_array import \
@@ -587,6 +585,16 @@ class YTDisk(YTSelectionContainer3D):
     _con_args = ('center', '_norm_vec', 'radius', 'height')
     def __init__(self, center, normal, radius, height, fields=None,
                  ds=None, field_parameters=None, data_source=None):
+
+        validate_3d_array(center)
+        validate_3d_array(normal)
+        validate_float(radius)
+        validate_float(height)
+        validate_iterable(fields)
+        validate_object(field_parameters, dict)
+        validate_object(ds, Dataset)
+        validate_object(data_source, YTSelectionContainer)
+
         YTSelectionContainer3D.__init__(self, center, ds,
                                         field_parameters, data_source)
         self._norm_vec = np.array(normal)/np.sqrt(np.dot(normal,normal))
diff --git a/yt/funcs.py b/yt/funcs.py
--- a/yt/funcs.py
+++ b/yt/funcs.py
@@ -1169,3 +1169,65 @@ def handle_mks_cgs(values, field_units):
     except YTEquivalentDimsError as e:
         values = values.to_equivalent(e.new_units, e.base)
     return values
+
+def validate_3d_array(obj):
+    if not iterable(obj) or len(obj) != 3:
+        raise TypeError("Expected an array of size (3,), received '%s' of "
+                        "length %s" % (str(type(obj)).split("'")[1], len(obj)))
+
+def validate_float(obj):
+    """Validates if the passed argument is a float value.
+
+    Raises an exception if `obj` is a single float value
+    or a YTQunatity of size 1.
+
+    Parameters
+    ----------
+    obj : Any
+        Any argument which needs to be checked for a single float value.
+
+    Raises
+    ------
+    TypeError
+        Raised if `obj` is not a single float value or YTQunatity
+
+    Examples
+    --------
+    >>> validate_float(1)
+    >>> validate_float(1.50)
+    >>> validate_float(YTQuantity(1,"cm"))
+    >>> validate_float((1,"cm"))
+    >>> validate_float([1, 1, 1])
+    Traceback (most recent call last):
+    ...
+    TypeError: Expected a numeric value (or size-1 array), received 'list' of length 3
+
+    >>> validate_float([YTQuantity(1, "cm"), YTQuantity(2,"cm")])
+    Traceback (most recent call last):
+    ...
+    TypeError: Expected a numeric value (or size-1 array), received 'list' of length 2
+    """
+    if isinstance(obj, tuple):
+        if len(obj) != 2 or not isinstance(obj[0], numeric_type)\
+                or not isinstance(obj[1], string_types):
+            raise TypeError("Expected a numeric value (or tuple of format "
+                            "(float, String)), received an inconsistent tuple "
+                            "'%s'." % str(obj))
+        else:
+            return
+    if iterable(obj) and (len(obj) != 1 or not isinstance(obj[0], numeric_type)):
+        raise TypeError("Expected a numeric value (or size-1 array), "
+                        "received '%s' of length %s"
+                        % (str(type(obj)).split("'")[1], len(obj)))
+
+
+def validate_iterable(obj):
+    if obj is not None and not iterable(obj):
+        raise TypeError("Expected an iterable object,"
+                        " received '%s'" % str(type(obj)).split("'")[1])
+
+def validate_object(obj, data_type):
+    if obj is not None and not isinstance(obj, data_type):
+        raise TypeError("Expected an object of '%s' type, received '%s'"
+                        % (str(data_type).split("'")[1],
+                        str(type(obj)).split("'")[1]))


Test patch:
diff --git a/yt/data_objects/tests/test_disks.py b/yt/data_objects/tests/test_disks.py
new file mode 100644
--- /dev/null
+++ b/yt/data_objects/tests/test_disks.py
@@ -0,0 +1,51 @@
+from nose.tools import assert_raises
+from numpy.testing import assert_equal
+
+from yt import YTQuantity
+from yt.testing import fake_random_ds
+
+
+def test_bad_disk_input():
+    # Fixes 1768
+    ds = fake_random_ds(16)
+
+    # Test invalid 3d array
+    with assert_raises(TypeError) as ex:
+        ds.disk(ds.domain_center, [0, 0, 1, 1], (10, 'kpc'), (20, 'kpc'))
+    desired = ("Expected an array of size (3,), received 'list' of length 4")
+    assert_equal(str(ex.exception), desired)
+
+    # Test invalid float
+    with assert_raises(TypeError) as ex:
+        ds.disk(ds.domain_center, [0, 0, 1], ds.domain_center, (20, 'kpc'))
+    desired = ("Expected a numeric value (or size-1 array),"
+               " received 'yt.units.yt_array.YTArray' of length 3")
+    assert_equal(str(ex.exception), desired)
+
+    # Test invalid float
+    with assert_raises(TypeError) as ex:
+        ds.disk(ds.domain_center, [0, 0, 1], (10, 10), (20, 'kpc'))
+    desired = ("Expected a numeric value (or tuple of format (float, String)),"
+               " received an inconsistent tuple '(10, 10)'.")
+    assert_equal(str(ex.exception), desired)
+
+    # Test invalid iterable
+    with assert_raises(TypeError) as ex:
+        ds.disk(ds.domain_center, [0, 0, 1], (10, 'kpc'),
+                (20, 'kpc'), fields=YTQuantity(1, 'kpc'))
+    desired = ("Expected an iterable object, received"
+               " 'yt.units.yt_array.YTQuantity'")
+    assert_equal(str(ex.exception), desired)
+
+    # Test invalid object
+    with assert_raises(TypeError) as ex:
+        ds.disk(ds.domain_center, [0, 0, 1], (10, 'kpc'),
+                (20, 'kpc'), ds=ds.all_data())
+    desired = ("Expected an object of 'yt.data_objects.static_output.Dataset' "
+               "type, received "
+               "'yt.data_objects.selection_data_containers.YTRegion'")
+    assert_equal(str(ex.exception), desired)
+
+    # Test valid disk
+    ds.disk(ds.domain_center, [0, 0, 1], (10, 'kpc'), (20, 'kpc'))
+    ds.disk(ds.domain_center, [0, 0, 1], 10, (20, 'kpc'))