Repository: dateutil/dateutil (Python)

Issue:
Passing `None` to parser raises `AttributeError`
If `None` is passed to the parser, I would expect either a `ValueError` or a `TypeError` to be raised, but instead `AttributeError` is raised because it tries to call `read` on the object. With `datetime.datetime.strptime`, if you pass it anything other than a string type, it raises a `TypeError`. I think we may want to do something similar. Certainly raising `AttributeError` violates principle of least surprise.

Since [timelex](https://github.com/dateutil/dateutil/blob/master/dateutil/parser.py#L67) is already using explicit type checks, we may want to change those two `if` statements to an `if`, `elif`, `else` triplet. I'm also open to possible solutions that would use duck typing.

``` python
from dateutil.parser import parse
parse(None)
```

Traceback:

```
dateutil/parser.py in _parse(self, timestr, dayfirst, yearfirst, fuzzy, fuzzy_with_tokens)
 ...
dateutil/parser.py in get_token(self)
     80                 nextchar = self.charstack.pop(0)
     81             else:
---> 82                 nextchar = self.instream.read(1)
     83                 while nextchar == '\x00':
     84                     nextchar = self.instream.read(1)

AttributeError: 'NoneType' object has no attribute 'read'
```



Hints:

Patch:
diff --git a/dateutil/parser.py b/dateutil/parser.py
--- a/dateutil/parser.py
+++ b/dateutil/parser.py
@@ -56,6 +56,10 @@ def __init__(self, instream):
         if isinstance(instream, text_type):
             instream = StringIO(instream)
 
+        if getattr(instream, 'read', None) is None:
+            raise TypeError('Parser must be a string or character stream, not '
+                            '{itype}'.format(itype=instream.__class__.__name__))
+
         self.instream = instream
         self.charstack = []
         self.tokenstack = []


Test patch:
diff --git a/dateutil/test/test_parser.py b/dateutil/test/test_parser.py
--- a/dateutil/test/test_parser.py
+++ b/dateutil/test/test_parser.py
@@ -7,7 +7,9 @@
 from dateutil.tz import tzoffset
 from dateutil.parser import *
 
+import six
 from six import assertRaisesRegex, PY3
+from six.moves import StringIO
 
 class ParserTest(unittest.TestCase):
 
@@ -30,6 +32,74 @@ def testEmptyString(self):
         with self.assertRaises(ValueError):
             parse('')
 
+    def testNone(self):
+        with self.assertRaises(TypeError):
+            parse(None)
+
+    def testInvalidType(self):
+        with self.assertRaises(TypeError):
+            parse(13)
+
+    def testDuckTyping(self):
+        # We want to support arbitrary classes that implement the stream
+        # interface.
+
+        class StringPassThrough(object):
+            def __init__(self, stream):
+                self.stream = stream
+
+            def read(self, *args, **kwargs):
+                return self.stream.read(*args, **kwargs)
+
+
+        dstr = StringPassThrough(StringIO('2014 January 19'))
+
+        self.assertEqual(parse(dstr), datetime(2014, 1, 19))
+
+    def testParseStream(self):
+        dstr = StringIO('2014 January 19')
+
+        self.assertEqual(parse(dstr), datetime(2014, 1, 19))
+
+    def testParseStr(self):
+        self.assertEqual(parse(self.str_str),
+                         parse(self.uni_str))
+
+    def testParserParseStr(self):
+        from dateutil.parser import parser
+
+        self.assertEqual(parser().parse(self.str_str),
+                         parser().parse(self.uni_str))
+
+    def testParseUnicodeWords(self):
+
+        class rus_parserinfo(parserinfo):
+            MONTHS = [("янв", "Январь"),
+                      ("фев", "Февраль"),
+                      ("мар", "Март"),
+                      ("апр", "Апрель"),
+                      ("май", "Май"),
+                      ("июн", "Июнь"),
+                      ("июл", "Июль"),
+                      ("авг", "Август"),
+                      ("сен", "Сентябрь"),
+                      ("окт", "Октябрь"),
+                      ("ноя", "Ноябрь"),
+                      ("дек", "Декабрь")]
+
+        self.assertEqual(parse('10 Сентябрь 2015 10:20',
+                               parserinfo=rus_parserinfo()),
+                         datetime(2015, 9, 10, 10, 20))
+
+    def testParseWithNulls(self):
+        # This relies on the from __future__ import unicode_literals, because
+        # explicitly specifying a unicode literal is a syntax error in Py 3.2
+        # May want to switch to u'...' if we ever drop Python 3.2 support.
+        pstring = '\x00\x00August 29, 1924'
+
+        self.assertEqual(parse(pstring),
+                         datetime(1924, 8, 29))
+
     def testDateCommandFormat(self):
         self.assertEqual(parse("Thu Sep 25 10:36:28 BRST 2003",
                                tzinfos=self.tzinfos),
@@ -738,45 +808,6 @@ class myparserinfo(parserinfo):
         dt = myparser.parse("01/Foo/2007")
         self.assertEqual(dt, datetime(2007, 1, 1))
 
-    def testParseStr(self):
-        self.assertEqual(parse(self.str_str),
-                         parse(self.uni_str))
-
-    def testParserParseStr(self):
-        from dateutil.parser import parser
-
-        self.assertEqual(parser().parse(self.str_str),
-                         parser().parse(self.uni_str))
-
-    def testParseUnicodeWords(self):
-
-        class rus_parserinfo(parserinfo):
-            MONTHS = [("янв", "Январь"),
-                      ("фев", "Февраль"),
-                      ("мар", "Март"),
-                      ("апр", "Апрель"),
-                      ("май", "Май"),
-                      ("июн", "Июнь"),
-                      ("июл", "Июль"),
-                      ("авг", "Август"),
-                      ("сен", "Сентябрь"),
-                      ("окт", "Октябрь"),
-                      ("ноя", "Ноябрь"),
-                      ("дек", "Декабрь")]
-
-        self.assertEqual(parse('10 Сентябрь 2015 10:20',
-                               parserinfo=rus_parserinfo()),
-                         datetime(2015, 9, 10, 10, 20))
-
-    def testParseWithNulls(self):
-        # This relies on the from __future__ import unicode_literals, because
-        # explicitly specifying a unicode literal is a syntax error in Py 3.2
-        # May want to switch to u'...' if we ever drop Python 3.2 support.
-        pstring = '\x00\x00August 29, 1924'
-
-        self.assertEqual(parse(pstring),
-                         datetime(1924, 8, 29))
-
     def testNoYearFirstNoDayFirst(self):
         dtstr = '090107'