Repository: pandas-dev/pandas

Issue:
Proper checks on nrows and chunksize for read_csv
From #15755 (e.g. [this comment](https://github.com/pandas-dev/pandas/pull/15756#issuecomment-288108170)).

Current status:
- there is a check on ``nrows``, which ensure floats are converted to int with no loss of precision, but not that the resulting/passed value is non-negative
- there is no check on ``chunksize``, so that a ``pd.concat`` based on ``chunksize=0`` will enter an infinite loop

I guess we want to ensure ``chunksize > 0`` and ``nrows >= 0``.


Discussion:

Resolving patch:
diff --git a/pandas/io/parsers.py b/pandas/io/parsers.py
--- a/pandas/io/parsers.py
+++ b/pandas/io/parsers.py
@@ -344,24 +344,34 @@
 """ % (_parser_params % (_fwf_widths, ''))
 
 
-def _validate_nrows(nrows):
+def _validate_integer(name, val, min_val=0):
     """
-    Checks whether the 'nrows' parameter for parsing is either
+    Checks whether the 'name' parameter for parsing is either
     an integer OR float that can SAFELY be cast to an integer
     without losing accuracy. Raises a ValueError if that is
     not the case.
+
+    Parameters
+    ----------
+    name : string
+        Parameter name (used for error reporting)
+    val : int or float
+        The value to check
+    min_val : int
+        Minimum allowed value (val < min_val will result in a ValueError)
     """
-    msg = "'nrows' must be an integer"
+    msg = "'{name:s}' must be an integer >={min_val:d}".format(name=name,
+                                                               min_val=min_val)
 
-    if nrows is not None:
-        if is_float(nrows):
-            if int(nrows) != nrows:
+    if val is not None:
+        if is_float(val):
+            if int(val) != val:
                 raise ValueError(msg)
-            nrows = int(nrows)
-        elif not is_integer(nrows):
+            val = int(val)
+        elif not (is_integer(val) and val >= min_val):
             raise ValueError(msg)
 
-    return nrows
+    return val
 
 
 def _read(filepath_or_buffer, kwds):
@@ -383,8 +393,8 @@ def _read(filepath_or_buffer, kwds):
 
     # Extract some of the arguments (pass chunksize on).
     iterator = kwds.get('iterator', False)
-    chunksize = kwds.get('chunksize', None)
-    nrows = _validate_nrows(kwds.get('nrows', None))
+    chunksize = _validate_integer('chunksize', kwds.get('chunksize', None), 1)
+    nrows = _validate_integer('nrows', kwds.get('nrows', None))
 
     # Create the parser.
     parser = TextFileReader(filepath_or_buffer, **kwds)


Test patch:
diff --git a/pandas/tests/io/parser/common.py b/pandas/tests/io/parser/common.py
--- a/pandas/tests/io/parser/common.py
+++ b/pandas/tests/io/parser/common.py
@@ -384,7 +384,7 @@ def test_read_nrows(self):
         df = self.read_csv(StringIO(self.data1), nrows=3.0)
         tm.assert_frame_equal(df, expected)
 
-        msg = "must be an integer"
+        msg = r"'nrows' must be an integer >=0"
 
         with tm.assertRaisesRegexp(ValueError, msg):
             self.read_csv(StringIO(self.data1), nrows=1.2)
@@ -392,6 +392,9 @@ def test_read_nrows(self):
         with tm.assertRaisesRegexp(ValueError, msg):
             self.read_csv(StringIO(self.data1), nrows='foo')
 
+        with tm.assertRaisesRegexp(ValueError, msg):
+            self.read_csv(StringIO(self.data1), nrows=-1)
+
     def test_read_chunksize(self):
         reader = self.read_csv(StringIO(self.data1), index_col=0, chunksize=2)
         df = self.read_csv(StringIO(self.data1), index_col=0)
@@ -402,6 +405,18 @@ def test_read_chunksize(self):
         tm.assert_frame_equal(chunks[1], df[2:4])
         tm.assert_frame_equal(chunks[2], df[4:])
 
+        # with invalid chunksize value:
+        msg = r"'chunksize' must be an integer >=1"
+
+        with tm.assertRaisesRegexp(ValueError, msg):
+            self.read_csv(StringIO(self.data1), chunksize=1.3)
+
+        with tm.assertRaisesRegexp(ValueError, msg):
+            self.read_csv(StringIO(self.data1), chunksize='foo')
+
+        with tm.assertRaisesRegexp(ValueError, msg):
+            self.read_csv(StringIO(self.data1), chunksize=0)
+
     def test_read_chunksize_and_nrows(self):
 
         # gh-15755