Repository: praw-dev/praw

Issue:
inspect.getargspec() is deprecated and will be removed in Python 3.6
When using PRAW with the recently released Python 3.5 a warning shows:

```
DeprecationWarning: inspect.getargspec() is deprecated, use inspect.signature() instead
```

[Python's documentation](https://docs.python.org/3/library/inspect.html#inspect.getargspec) on `inspect.getargspec()` says:

> Deprecated since version 3.0: Use signature() and Signature Object, which provide a better introspecting API for callables. This function will be removed in Python 3.6.

In the file [praw/decorators.py](https://github.com/praw-dev/praw/blob/master/praw/decorators.py) the function `inspect.getargspec()` is used twice, to get a list of a function arguments' names, both lines are identical:

```
func_args = inspect.getargspec(function).args
```

This can easily be made compatible with Python 3.6 by changing the two lines to:

```
func_args = list(inspect.signature(function).parameters.keys())
```

which, to my knowledge, produces the exact same list of arguments' names so it _should_ work with the rest of the code.

**But**, the problem is that the `inspect.signature()` function, added in Python 3.0, wasn't backported to Python 2.7 and thus isn't supported by Python 2.

That's the limit of my skills I'm afraid. I'm not a programmer experienced enough to know what to do in such a case, I don't know how PRAW can be made compatible with both python 2.7 and 3.6, so instead of a pull request I can only open a new issue.



Discussion:
> But, the problem is that the inspect.signature() function, added in Python 3.0, wasn't backported to Python 2.7 and thus isn't supported by Python 2.

Using the `six` compatability library, the use of Python 2/3 can be determined via the boolean `six.PY(x)` where x is 2 or 3, and the function used can be different per that check. Or the function from 2.7 can just be imported as the one with the 3 name with a `try except` block. But both function should be double checked to produce an equivalent result.

`inspect.getargspec(function).args` and `list(inspect.signature(function).parameters.keys())` are not, in fact, equivalent. The former returns a list of positional arguments while the latter returns the names of all arguments. The list comprehension `[parameter.name for parameter in inspect.signature(function).parameters.values() if parameter.kind == parameter.POSITIONAL_OR_KEYWORD]` gives you what you want for functions with *args or **kwargs.

@macioosch thanks for reporting, and @jaccarmac thanks for the information. Would either of you like to make a PR for fixing this such that the function also works 3.5+?

@bboe I'll do it right now since I have the time.

@jaccarmac I believe your mistaken? I just tested it:

```
>>> def foo(thing1, thing2, thing3, thing4=None, thing5=None):
...     print('\n'.join([str(thing1), str(thing2), str(thing3), str(thing4), str(thing5)]))
...     return str(thing5)
... 
>>> foo('bar', 'biz', 'bang')
bar
biz
bang
None
None
'None'
>>> inspect.getargspec(foo).args['thing1', 'thing2', 'thing3', 'thing4', 'thing5']
>>> list(inspect.signature(foo).parameters.keys())['thing1', 'thing2', 'thing3', 'thing4', 'thing5']
>>> 
```

They both return _all_ arguments, unless I'm misunderstanding what a positional argument is.

Edit: I see what @jaccarmac was talking about now, whoops. If there's a `*args` or `**kwargs` those get returned in the signarure.


Resolving patch:
diff --git a/praw/decorator_helpers.py b/praw/decorator_helpers.py
--- a/praw/decorator_helpers.py
+++ b/praw/decorator_helpers.py
@@ -1,4 +1,5 @@
 """Internal helper functions used by praw.decorators."""
+import inspect
 from requests.compat import urljoin
 import six
 import sys
@@ -23,3 +24,15 @@ def _is_mod_of_all(user, subreddit):
     mod_subs = user.get_cached_moderated_reddits()
     subs = six.text_type(subreddit).lower().split('+')
     return all(sub in mod_subs for sub in subs)
+
+
+def _make_func_args(function):
+    if six.PY3 and not hasattr(sys, 'pypy_version_info'):
+        # CPython3 uses inspect.signature(), not inspect.getargspec()
+        # see #551 and #541 for more info
+        func_items = inspect.signature(function).parameters.items()
+        func_args = [name for name, param in func_items
+                     if param.kind == param.POSITIONAL_OR_KEYWORD]
+    else:
+        func_args = inspect.getargspec(function).args
+    return func_args
diff --git a/praw/decorators.py b/praw/decorators.py
--- a/praw/decorators.py
+++ b/praw/decorators.py
@@ -24,11 +24,14 @@
 from __future__ import print_function, unicode_literals
 
 import decorator
-import inspect
 import six
 import sys
 from functools import wraps
-from praw.decorator_helpers import _get_captcha, _is_mod_of_all
+from praw.decorator_helpers import (
+    _get_captcha,
+    _is_mod_of_all,
+    _make_func_args
+)
 from praw import errors
 from warnings import simplefilter, warn
 
@@ -48,7 +51,7 @@ def alias_function(function, class_name):
     """
     @wraps(function)
     def wrapped(self, *args, **kwargs):
-        func_args = inspect.getargspec(function).args
+        func_args = _make_func_args(function)
         if 'subreddit' in func_args and func_args.index('subreddit') != 1:
             # Only happens for search
             kwargs['subreddit'] = self
@@ -164,9 +167,9 @@ def require_captcha(function, *args, **kwargs):
                 # *args currently contains a None where the captcha answer
                 # needs to go. If we put the captcha in the **kwargs,
                 # we get a TypeError for having two values of the same param.
-                func_args = inspect.getargspec(function)
-                if 'captcha' in func_args.args:
-                    captcha_index = func_args.args.index('captcha')
+                func_args = _make_func_args(function)
+                if 'captcha' in func_args:
+                    captcha_index = func_args.index('captcha')
                     args = list(args)
                     args[captcha_index] = captcha_answer
                 else:


Test patch:
diff --git a/tests/test_decorators.py b/tests/test_decorators.py
--- a/tests/test_decorators.py
+++ b/tests/test_decorators.py
@@ -1,6 +1,7 @@
 from __future__ import print_function, unicode_literals
 
 import unittest
+from praw.decorator_helpers import _make_func_args
 from praw.decorators import restrict_access
 
 
@@ -8,3 +9,15 @@ class DecoratorTest(unittest.TestCase):
     def test_require_access_failure(self):
         self.assertRaises(TypeError, restrict_access, scope=None,
                           oauth_only=True)
+
+    def test_make_func_args(self):
+        def foo(arg1, arg2, arg3):
+            pass
+
+        def bar(arg1, arg2, arg3, *args, **kwargs):
+            pass
+
+        arglist = ['arg1', 'arg2', 'arg3']
+
+        self.assertEqual(_make_func_args(foo), arglist)
+        self.assertEqual(_make_func_args(bar), arglist)