Repository: celery/celery

Issue:
Tasks created from classmethods no longer work
## Checklist

**Version info:**

* celery 4.0.2 (latentcall)
* kombu 4.0.2 
* py 2.7.13
* billiard 3.5.0.2

**Verified against celery/master:**  Yes

## Steps to reproduce

Save as reproducer.py and then run with ```celery -A reproducer worker```

```
from celery import Celery, task, Task

app = Celery('hello', broker='qpid://localhost/')


class ReproducerClass:

    @classmethod
    def task_classmethod(cls):
        return 'hello world'

    @staticmethod
    def task_staticmethod():
        return 'hello world'


# this works
statictask = task(ReproducerClass.task_staticmethod, base=Task)
# this blows up
classtask = task(ReproducerClass.task_classmethod, base=Task)

```


## Expected behavior

class method task will function as a normal task, as it did in Celery 3.1.x

## Actual behavior

```
Traceback (most recent call last):
  File "/usr/bin/celery", line 11, in <module>
    sys.exit(main())
  File "/usr/lib/python2.7/site-packages/celery/__main__.py", line 14, in main
    _main()
  File "/usr/lib/python2.7/site-packages/celery/bin/celery.py", line 326, in main
    cmd.execute_from_commandline(argv)
  File "/usr/lib/python2.7/site-packages/celery/bin/celery.py", line 488, in execute_from_commandline
    super(CeleryCommand, self).execute_from_commandline(argv)))
  File "/usr/lib/python2.7/site-packages/celery/bin/base.py", line 281, in execute_from_commandline
    return self.handle_argv(self.prog_name, argv[1:])
  File "/usr/lib/python2.7/site-packages/celery/bin/celery.py", line 480, in handle_argv
    return self.execute(command, argv)
  File "/usr/lib/python2.7/site-packages/celery/bin/celery.py", line 412, in execute
    ).run_from_argv(self.prog_name, argv[1:], command=argv[0])
  File "/usr/lib/python2.7/site-packages/celery/bin/worker.py", line 221, in run_from_argv
    return self(*args, **options)
  File "/usr/lib/python2.7/site-packages/celery/bin/base.py", line 244, in __call__
    ret = self.run(*args, **kwargs)
  File "/usr/lib/python2.7/site-packages/celery/bin/worker.py", line 255, in run
    **kwargs)
  File "/usr/lib/python2.7/site-packages/celery/worker/worker.py", line 95, in __init__
    self.on_before_init(**kwargs)
  File "/usr/lib/python2.7/site-packages/celery/apps/worker.py", line 97, in on_before_init
    trace.setup_worker_optimizations(self.app, self.hostname)
  File "/usr/lib/python2.7/site-packages/celery/app/trace.py", line 568, in setup_worker_optimizations
    app.finalize()
  File "/usr/lib/python2.7/site-packages/celery/app/base.py", line 512, in finalize
    _announce_app_finalized(self)
  File "/usr/lib/python2.7/site-packages/celery/_state.py", line 52, in _announce_app_finalized
    callback(app)
  File "/usr/lib/python2.7/site-packages/celery/app/base.py", line 412, in cons
    return app._task_from_fun(fun, **opts)
  File "/usr/lib/python2.7/site-packages/celery/app/base.py", line 453, in _task_from_fun
    '__header__': staticmethod(head_from_fun(fun, bound=bind)),
  File "/usr/lib/python2.7/site-packages/celery/utils/functional.py", line 275, in head_from_fun
    fun_args=_argsfromspec(getfullargspec(fun)),
  File "/usr/lib/python2.7/site-packages/vine/five.py", line 350, in getfullargspec
    s = _getargspec(fun)
  File "/usr/lib64/python2.7/inspect.py", line 815, in getargspec
    raise TypeError('{!r} is not a Python function'.format(func))
TypeError: <method-wrapper '__call__' of instancemethod object at 0x7f6ba1d7b410> is not a Python function
```

As you can see, the entire traceback is within Celery, leaving no indication of where the problem might be.  After debugging, we found that the deficiency is [here](https://github.com/celery/celery/blob/1992cb07f1d4de44ab80ff7ee8ab93614517d7ae/celery/utils/functional.py#L269):

Here is the exerpt:

```
    if not inspect.isfunction(fun) and hasattr(fun, '__call__'):
        name, fun = fun.__class__.__name__, fun.__call__
    else:
        name = fun.__name__
````

If you mess around with the following code in REPL or ipython, we can figure out what's going on.

```
In [1]: class Test:
    @classmethod
    def cmtest(cls):
        print "cmtest"
    @staticmethod
    def smtest():
        print "smtest"
   ...:         

In [2]: import inspect

In [3]: inspect.isfunction(Test.cmtest)
Out[3]: False

In [4]: inspect.isfunction(Test.smtest)
Out[4]: True

In [5]: Test.cmtest.__call__
Out[5]: <method-wrapper '__call__' of instancemethod object at 0x7fce55138730>

In [6]: Test.smtest.__call__
Out[6]: <method-wrapper '__call__' of function object at 0x7fce5393f758>
```

The staticmethod is taking the "else" branch, while the classmethod is taking the "if" branch.  That's about as far down the rabbit hole as I wanted to go.  Nevertheless, it's a regression from Celery 3.1.x behavior.
  

Task fail to classmethod   in version 4.0.0
## Checklist

- [x] I have included the output of ``celery -A proj report`` in the issue.
      (if you are not able to do this, then at least specify the Celery
       version affected).
- [x] I have verified that the issue exists against the `master` branch of Celery.

## Steps to reproduce
I use this command `celery -A base worker -Q mail_remind,sms_remind -l INFO`
```
class PushBullet(object):
    headers = config.PushBullet.headers
    phone_id = config.PushBullet.phone_id
    pc_id = config.PushBullet.pc_id
    push_url = config.PushBullet.push_url

    @classmethod
    @app.task
    def send_sms_to_phone(cls, title='', body='', device_iden=phone_id):
        if title or body:
            data = {
                "type": "note",
                "title": title,
                "body": body,
                "device_iden": device_iden
            }
           
           # This is requests.post
            post(cls.push_url, cls.headers, data)
```

And then I call it just like this

`PushBullet.send_sms_to_phone.apply_async(args=(title, body))`

and it raise an error. 
## Expected behavior

When I call it like this `PushBullet.send_sms_to_phone(title, body)` is OK.

## Actual behavior
```
[2016-11-19 11:24:27,591: ERROR/PoolWorker-6] Task base.sms_util.send_sms_to_phone[87afdb95-06b8-4eb4-bf52-91afa9913b2c] raised unexpected: AttributeError("'str' object has no attribute 'push_url'",)
Traceback (most recent call last):
  File "/Users/wyx/project/message/.env/lib/python3.5/site-packages/celery/app/trace.py", line 368, in trace_task
    R = retval = fun(*args, **kwargs)
  File "/Users/wyx/project/message/.env/lib/python3.5/site-packages/celery/app/trace.py", line 623, in __protected_call__
    return self.run(*args, **kwargs)
  File "/Users/wyx/project/message/base/sms_util.py", line 48, in send_sms_to_phone
    post(cls.push_url, cls.headers, data)
AttributeError: 'str' object has no attribute 'push_url'
```



Discussion:
Just realized that an issue for this already exists: #3609 
Did this work in 3.1?  I have a hard time seeing how this can work, considering that @app.task doesn't return a function, it returns a class instance that happens to be callable.
I came across this independently and didn't notice this issue.  I actually get an entirely different traceback, but the same results: classmethod tasks no longer work.

More info in #3863 

Resolving patch:
diff --git a/celery/utils/functional.py b/celery/utils/functional.py
--- a/celery/utils/functional.py
+++ b/celery/utils/functional.py
@@ -266,7 +266,11 @@ def head_from_fun(fun, bound=False, debug=False):
     # in pure-Python.  Instead we use exec to create a new function
     # with an empty body, meaning it has the same performance as
     # as just calling a function.
-    if not inspect.isfunction(fun) and hasattr(fun, '__call__'):
+    is_function = inspect.isfunction(fun)
+    is_callable = hasattr(fun, '__call__')
+    is_method = inspect.ismethod(fun)
+
+    if not is_function and is_callable and not is_method:
         name, fun = fun.__class__.__name__, fun.__call__
     else:
         name = fun.__name__


Test patch:
diff --git a/t/integration/test_canvas.py b/t/integration/test_canvas.py
--- a/t/integration/test_canvas.py
+++ b/t/integration/test_canvas.py
@@ -30,6 +30,8 @@ def test_complex_chain(self, manager):
 
     @flaky
     def test_group_chord_group_chain(self, manager):
+        from celery.five import bytes_if_py2
+
         if not manager.app.conf.result_backend.startswith('redis'):
             raise pytest.skip('Requires redis result backend.')
         redis_connection = StrictRedis()
@@ -40,11 +42,17 @@ def test_group_chord_group_chain(self, manager):
 
         result = (before | connect | after).delay()
         result.get(timeout=TIMEOUT)
-        redis_messages = redis_connection.lrange('redis-echo', 0, -1)
-        assert set(['before 0', 'before 1', 'before 2']) == \
-            set(redis_messages[:3])
-        assert redis_messages[3] == 'connect'
-        assert set(redis_messages[4:]) == set(['after 0', 'after 1'])
+        redis_messages = list(map(
+            bytes_if_py2,
+            redis_connection.lrange('redis-echo', 0, -1)
+        ))
+        before_items = \
+            set(map(bytes_if_py2, (b'before 0', b'before 1', b'before 2')))
+        after_items = set(map(bytes_if_py2, (b'after 0', b'after 1')))
+
+        assert set(redis_messages[:3]) == before_items
+        assert redis_messages[3] == b'connect'
+        assert set(redis_messages[4:]) == after_items
         redis_connection.delete('redis-echo')
 
     @flaky
diff --git a/t/unit/utils/test_functional.py b/t/unit/utils/test_functional.py
--- a/t/unit/utils/test_functional.py
+++ b/t/unit/utils/test_functional.py
@@ -205,6 +205,18 @@ def test_from_fun_forced_kwargs(self):
         g(a=1, b=2)
         g(a=1, b=2, c=3)
 
+    def test_classmethod(self):
+        class A(object):
+            @classmethod
+            def f(cls, x):
+                return x
+
+        fun = head_from_fun(A.f, bound=False)
+        assert fun(A, 1) == 1
+
+        fun = head_from_fun(A.f, bound=True)
+        assert fun(1) == 1
+
 
 class test_fun_takes_argument: