Repository: praw-dev/praw

Issue:
TypeError on login: cannot make memory view
I can no longer login using PRAW 3.3.0.

Using username/password:

```
Traceback (most recent call last):
  File "countdown.py", line 25, in update_countdown
    reddit.login(username, password)
  File "<decorator-gen-49>", line 2, in login
  File "/usr/lib/python2.7/site-packages/praw/decorators.py", line 75, in wrap
    return function(*args, **kwargs)
  File "/usr/lib/python2.7/site-packages/praw/__init__.py", line 1418, in login
    self.request_json(self.config['login'], data=data)
  File "<decorator-gen-8>", line 2, in request_json
  File "/usr/lib/python2.7/site-packages/praw/decorators.py", line 113, in raise_api_exceptions
    return_value = function(*args, **kwargs)
  File "/usr/lib/python2.7/site-packages/praw/__init__.py", line 612, in request_json
    retry_on_error=retry_on_error)
```

Using OAuth:

```
Traceback (most recent call last):
  File "countdown.py", line 25, in update_countdown
    reddit.refresh_access_information(access_info)
  File "/usr/lib/python2.7/site-packages/praw/__init__.py", line 1311, in get_access_information
    retval = super(AuthenticatedReddit, self).get_access_information(code)
  File "<decorator-gen-9>", line 2, in get_access_information
  File "/usr/lib/python2.7/site-packages/praw/decorators.py", line 285, in require_oauth
    return function(*args, **kwargs)
  File "/usr/lib/python2.7/site-packages/praw/__init__.py", line 675, in get_access_information
    retval = self._handle_oauth_request(data)
  File "/usr/lib/python2.7/site-packages/praw/__init__.py", line 650, in _handle_oauth_request
    response = self._request(url, auth=auth, data=data, raw_response=True)
```

The rest of the traceback is common to both methods:

```
  File "/usr/lib/python2.7/site-packages/praw/__init__.py", line 444, in _request
    response = handle_redirect()
  File "/usr/lib/python2.7/site-packages/praw/__init__.py", line 425, in handle_redirect
    verify=self.http.validate_certs, **kwargs)
  File "/usr/lib/python2.7/site-packages/praw/handlers.py", line 136, in wrapped
    return function(cls, **kwargs)
  File "/usr/lib/python2.7/site-packages/praw/handlers.py", line 57, in wrapped
    return function(cls, **kwargs)
  File "/usr/lib/python2.7/site-packages/praw/handlers.py", line 103, in request
    allow_redirects=False, verify=verify)
  File "/usr/lib/python2.7/site-packages/requests/sessions.py", line 576, in send
    r = adapter.send(request, **kwargs)
  File "/usr/lib/python2.7/site-packages/requests/adapters.py", line 370, in send
    timeout=timeout
  File "/usr/lib/python2.7/site-packages/requests/packages/urllib3/connectionpool.py", line 559, in urlopen
    body=body, headers=headers)
  File "/usr/lib/python2.7/site-packages/requests/packages/urllib3/connectionpool.py", line 353, in _make_request
    conn.request(method, url, **httplib_request_kw)
  File "/usr/lib64/python2.7/httplib.py", line 1053, in request
    self._send_request(method, url, body, headers)
  File "/usr/lib64/python2.7/httplib.py", line 1093, in _send_request
    self.endheaders(body)
  File "/usr/lib64/python2.7/httplib.py", line 1049, in endheaders
    self._send_output(message_body)
  File "/usr/lib64/python2.7/httplib.py", line 893, in _send_output
    self.send(msg)
  File "/usr/lib64/python2.7/httplib.py", line 869, in send
    self.sock.sendall(data)
  File "/usr/lib/python2.7/site-packages/requests/packages/urllib3/contrib/pyopenssl.py", line 216, in sendall
    data = memoryview(data)
TypeError: cannot make memory view because object does not have the buffer interface
```



Discussion:
After some digging it seems to me that this is related to kennethreitz/requests#2818 (and shazow/urllib3#717).

Can you use pip to see what version of requests you are using?

Sounds like we either need to restrict the version of requests that can be used (set a smaller maximum number), or fix the unicode headers that PRAW sends. The former approach is much simpler.

I used 2.8.0, but it works when I downgraded requests to 2.7.0.


Resolving patch:
diff --git a/praw/__init__.py b/praw/__init__.py
--- a/praw/__init__.py
+++ b/praw/__init__.py
@@ -37,7 +37,8 @@
 from praw.helpers import chunk_sequence, normalize_url
 from praw.internal import (_image_type, _prepare_request,
                            _raise_redirect_exceptions,
-                           _raise_response_exceptions, _to_reddit_list)
+                           _raise_response_exceptions,
+                           _to_reddit_list, _warn_pyopenssl)
 from praw.settings import CONFIG
 from requests import Session
 from requests.compat import urljoin
@@ -306,6 +307,7 @@ class BaseReddit(object):
 
     RETRY_CODES = [502, 503, 504]
     update_checked = False
+    openssl_warned = False
 
     def __init__(self, user_agent, site_name=None, handler=None,
                  disable_update_check=False, **kwargs):
@@ -371,6 +373,11 @@ def _req_error(*_, **__):
             update_check(__name__, __version__)
             BaseReddit.update_checked = True
 
+        # Warn against a potentially incompatible version of pyOpenSSL
+        if not BaseReddit.openssl_warned and self.config.validate_certs:
+            _warn_pyopenssl()
+            BaseReddit.openssl_warned = True
+
         # Initial values
         self._use_oauth = False
 
diff --git a/praw/internal.py b/praw/internal.py
--- a/praw/internal.py
+++ b/praw/internal.py
@@ -30,6 +30,13 @@
                          InvalidSubreddit, OAuthException,
                          OAuthInsufficientScope, OAuthInvalidToken,
                          RedirectException)
+from warnings import warn
+try:
+    from OpenSSL import __version__ as _opensslversion
+    _opensslversionlist = [int(minor) if minor.isdigit() else minor
+                           for minor in _opensslversion.split('.')]
+except ImportError:
+    _opensslversionlist = [0, 15]
 
 MIN_PNG_SIZE = 67
 MIN_JPEG_SIZE = 128
@@ -251,3 +258,14 @@ def _to_reddit_list(arg):
         return six.text_type(arg)
     else:
         return ','.join(six.text_type(a) for a in arg)
+
+
+def _warn_pyopenssl():
+    """Warn the user against faulty versions of pyOpenSSL."""
+    if _opensslversionlist < [0, 15]:  # versions >= 0.15 are fine
+        warn(RuntimeWarning(
+            "pyOpenSSL {0} may be incompatible with praw if validating"
+            "ssl certificates, which is on by default.\nSee https://"
+            "github.com/praw/pull/625 for more information".format(
+                _opensslversion)
+        ))
diff --git a/setup.py b/setup.py
--- a/setup.py
+++ b/setup.py
@@ -14,6 +14,19 @@
           encoding='utf-8') as fp:
     VERSION = re.search("__version__ = '([^']+)'", fp.read()).group(1)
 
+dependencies = ['decorator >=4.0.9, <4.1',
+                'requests >=2.3.0',
+                'six ==1.10',
+                'update_checker >=0.12, <1.0']
+
+try:
+    from OpenSSL import __version__ as opensslversion
+    opensslversion = [int(minor) if minor.isdigit() else minor
+                      for minor in opensslversion.split('.')]
+    if opensslversion < [0, 15]:  # versions less than 0.15 have a regression
+        dependencies.append('pyopenssl >=0.15')
+except ImportError:
+    pass  # open ssl not installed
 
 setup(name=PACKAGE_NAME,
       author='Timothy Mellor',
@@ -38,10 +51,7 @@
                    'reddit\'s API.'),
       entry_points={'console_scripts': [
           'praw-multiprocess = praw.multiprocess:run']},
-      install_requires=['decorator >=4.0.9, <4.1',
-                        'requests >=2.3.0',
-                        'six ==1.10',
-                        'update_checker >=0.12, <1.0'],
+      install_requires=dependencies,
       keywords='reddit api wrapper',
       license='GPLv3',
       long_description=README,


Test patch:
diff --git a/tests/helper.py b/tests/helper.py
--- a/tests/helper.py
+++ b/tests/helper.py
@@ -211,6 +211,31 @@ def assertWarningsRegexp(self, regexp, warning, callable, *args, **kwds):
                 raise AssertionError('Pattern "{0}" not found in'
                                      'any caught warnings'.format(regexp))
 
+    def assertNoWarnings(self, callable, *args, **kwds):
+        """Fail unless no warnings are triggered by callable with
+           arguments args and keyword arguments kwds.
+        """
+        with warnings.catch_warnings(record=True) as warning_list:
+            warnings.simplefilter('always')
+
+            callable(*args, **kwds)
+            if len(warning_list) == 1:
+                wname, warningmsg = (warning_list[0].category.__name__,
+                                     warning_list[0].message.args[0])
+                raise AssertionError("A warning with a category of {0} "
+                                     "and a message of {1} were triggered "
+                                     "by {2}".format(wname, repr(warningmsg),
+                                                     callable.__name__))
+            elif warning_list:
+                wnames = [(w.category.__name__, w.message.args[0])
+                          if w.message.args else (w.category.__name__, "")
+                          for w in warning_list]
+                wnames = ["{}: {}".format(k, repr(v)) for k, v in warnings]
+                raise AssertionError("The following warnings were triggered "
+                                     "by {0}:\n{1}".format(
+                                         callable.__name__,
+                                         "    \n".join(wnames)))
+
 
 class OAuthPRAWTest(PRAWTest):
     def betamax_init(self):
diff --git a/tests/test_internal.py b/tests/test_internal.py
--- a/tests/test_internal.py
+++ b/tests/test_internal.py
@@ -1,8 +1,41 @@
 from __future__ import print_function, unicode_literals
+from six.moves import reload_module
+import sys
+from types import ModuleType
 from praw.internal import _to_reddit_list
 from .helper import PRAWTest, betamax
 
 
+def patch_openpyssl(version):
+    """Wrapper to patch a pyOpenSSL version.
+
+       - Calls the function with praw's internal module as the second argument.
+       - version defines the version string to patch in
+       - Reverts OpenSSL back to it's normal state after the function call if
+         it exists on the system, otherwise, deletes the patched module from
+         sys.modules
+    """
+    def factory(func):
+        def wrapped(obj):
+            try:
+                import OpenSSL as RealOpenSSL
+            except ImportError:
+                RealOpenSSL = None
+            FakeOpenSSL = ModuleType(str('openssl'))
+            FakeOpenSSL.__version__ = version
+            sys.modules['OpenSSL'] = FakeOpenSSL
+            from praw import internal
+            reload_module(internal)
+
+            retval = func(obj, internal)
+            del sys.modules['OpenSSL']
+            if RealOpenSSL is not None:
+                sys.modules['OpenSSL'] = RealOpenSSL
+            return retval
+        return wrapped
+    return factory
+
+
 class InternalTest(PRAWTest):
     def test__to_reddit_list(self):
         output = _to_reddit_list('hello')
@@ -34,3 +67,16 @@ def test__to_reddit_list_with_mix(self):
         obj = self.r.get_subreddit(self.sr)
         output = _to_reddit_list([obj, 'hello'])
         self.assertEqual("{0},{1}".format(self.sr, 'hello'), output)
+
+    @patch_openpyssl('0.14')
+    def test__warn_pyopenssl_invalid_version(self, internal):
+        self.assertWarningsRegexp("0\.14", RuntimeWarning,
+                                  internal._warn_pyopenssl)
+
+    @patch_openpyssl('0.15')
+    def test__warn_pyopenssl_valid_version(self, internal):
+        self.assertNoWarnings(internal._warn_pyopenssl)
+
+    @patch_openpyssl('16.0.1.dev001')
+    def test__warn_pyopenssl_valid_version_dev(self, internal):
+        self.assertNoWarnings(internal._warn_pyopenssl)