Repository: ray-project/ray

Issue:
Actor class static methods are treated as instance methods
Trying to call the staticmethod of ray Actor (class wrapped with @ray.remote) fails because the method awaits the `self` field.

### System information
- **OS Platform and Distribution (e.g., Linux Ubuntu 16.04)**:
- **Ray installed from (source or binary)**: binary (pip)
- **Ray version**: 0.6.4
- **Python version**: 3.6.7
- **Exact command to reproduce**:
```
@ray.remote
class my_class:
    def __init__(self):
        self.a = 1
    
    def get_a(self):
        return self.a
    
    @staticmethod
    def static_sum(a, b):
        return a+b

mc = my_class.remote()
ray.get(mc.static_sum.remote(1, 2))
```
will fail with exception:
```
Exception: Too many arguments were passed to the function 'static_sum'
```
and calling 
```
ray.get(mc.static_sum.remote(1))
```
causes
```
RayTaskError: ray_my_class:static_sum() (pid=28795, host=***)
  File "<ipython-input-64-88df51b11f3a>", line 11, in static_sum
TypeError: unsupported operand type(s) for +: 'my_class' and 'int'
```
<!--
You can obtain the Ray version with

python -c "import ray; print(ray.__version__)"
-->

### Describe the problem
Actor static methods are treated as instance methods.

Additional question: Are there any ways to access the actor property (function wrapped with property decorator) or just instance field? They are not callable and not present in ActorHandle attributes.
e.g.
```
mc.a
```
causes
```
AttributeError: 'ActorHandle' object has no attribute 'a'
```
<!-- Describe the problem clearly here. -->

### Source code / logs
<!-- Include any logs or source code that would be helpful to diagnose the problem. If including tracebacks, please include the full traceback. Large logs and files should be attached. Try to provide a reproducible test case that is the bare minimum necessary to generate the problem. -->



Discussion:
Thanks for reporting this, would you be interested in submitting a patch for this?

Regarding accessing fields of an object, there isn't a way that is exposed right now. We could always expose the builtin methods like `__getattribute__`, which would make it possible. One reason for not making it too easy is that accessing a field feels very "cheap", but in our case it requires remote procedure calls, so isn't particularly cheap.
Thanks, @robertnishihara!

Talking about the patch, are the staticmethods supposed to be treated as remote functions or just ignored with some exception message?

About the accessing the fields: the documentation seems to lack this info. From the first view some people may get the incorrect understanding of ray remote actors.
Up (talking about the last comment)
Up

Resolving patch:
diff --git a/python/ray/actor.py b/python/ray/actor.py
--- a/python/ray/actor.py
+++ b/python/ray/actor.py
@@ -215,12 +215,19 @@ def __init__(self):
         self.method_signatures = {}
         self.actor_method_num_return_vals = {}
         for method_name, method in self.actor_methods:
+            # Whether or not this method requires binding of its first
+            # argument. For class and static methods, we do not want to bind
+            # the first argument, but we do for instance methods
+            is_bound = (ray.utils.is_class_method(method)
+                        or ray.utils.is_static_method(self.modified_class,
+                                                      method_name))
+
             # Print a warning message if the method signature is not
             # supported. We don't raise an exception because if the actor
             # inherits from a class that has a method whose signature we
             # don't support, there may not be much the user can do about it.
             self.method_signatures[method_name] = signature.extract_signature(
-                method, ignore_first=not ray.utils.is_class_method(method))
+                method, ignore_first=not is_bound)
             # Set the default number of return values for this method.
             if hasattr(method, "__ray_num_return_vals__"):
                 self.actor_method_num_return_vals[method_name] = (
diff --git a/python/ray/function_manager.py b/python/ray/function_manager.py
--- a/python/ray/function_manager.py
+++ b/python/ray/function_manager.py
@@ -25,6 +25,7 @@
     binary_to_hex,
     is_function_or_method,
     is_class_method,
+    is_static_method,
     check_oversized_pickle,
     decode,
     ensure_str,
@@ -757,7 +758,9 @@ def actor_method_executor(actor, *args, **kwargs):
 
             # Execute the assigned method and save a checkpoint if necessary.
             try:
-                if is_class_method(method):
+                is_bound = (is_class_method(method)
+                            or is_static_method(type(actor), method_name))
+                if is_bound:
                     method_returns = method(*args, **kwargs)
                 else:
                     method_returns = method(actor, *args, **kwargs)
diff --git a/python/ray/utils.py b/python/ray/utils.py
--- a/python/ray/utils.py
+++ b/python/ray/utils.py
@@ -131,6 +131,21 @@ def is_class_method(f):
     return hasattr(f, "__self__") and f.__self__ is not None
 
 
+def is_static_method(cls, f_name):
+    """Returns whether the class has a static method with the given name.
+
+    Args:
+        cls: The Python class (i.e. object of type `type`) to
+            search for the method in.
+        f_name: The name of the method to look up in this class
+            and check whether or not it is static.
+    """
+    for cls in inspect.getmro(cls):
+        if f_name in cls.__dict__:
+            return isinstance(cls.__dict__[f_name], staticmethod)
+    return False
+
+
 def random_string():
     """Generate a random string to use as an ID.
 


Test patch:
diff --git a/python/ray/tests/test_actor.py b/python/ray/tests/test_actor.py
--- a/python/ray/tests/test_actor.py
+++ b/python/ray/tests/test_actor.py
@@ -212,6 +212,55 @@ def g(self):
     assert ray.get(t.g.remote()) == 3
 
 
+def test_actor_static_attributes(ray_start_regular):
+    class Grandparent:
+        GRANDPARENT = 2
+
+        @staticmethod
+        def grandparent_static():
+            assert Grandparent.GRANDPARENT == 2
+            return 1
+
+    class Parent1(Grandparent):
+        PARENT1 = 6
+
+        @staticmethod
+        def parent1_static():
+            assert Parent1.PARENT1 == 6
+            return 2
+
+        def parent1(self):
+            assert Parent1.PARENT1 == 6
+
+    class Parent2:
+        PARENT2 = 7
+
+        def parent2(self):
+            assert Parent2.PARENT2 == 7
+
+    @ray.remote
+    class TestActor(Parent1, Parent2):
+        X = 3
+
+        @staticmethod
+        def f():
+            assert TestActor.GRANDPARENT == 2
+            assert TestActor.PARENT1 == 6
+            assert TestActor.PARENT2 == 7
+            assert TestActor.X == 3
+            return 4
+
+        def g(self):
+            assert TestActor.GRANDPARENT == 2
+            assert TestActor.PARENT1 == 6
+            assert TestActor.PARENT2 == 7
+            assert TestActor.f() == 4
+            return TestActor.X
+
+    t = TestActor.remote()
+    assert ray.get(t.g.remote()) == 3
+
+
 def test_caching_actors(shutdown_only):
     # Test defining actors before ray.init() has been called.