From 40b90946a7ea04ee0ebd56c24529453c28baf56a Mon Sep 17 00:00:00 2001 From: pwws <9303779+pwws@users.noreply.github.com> Date: Fri, 7 May 2021 06:07:31 +0200 Subject: [PATCH] bugfix: Allow using staticmethods as jobs (#1458) --- rq/job.py | 2 +- rq/utils.py | 34 +++++++++++++++++++++++++++++++--- tests/fixtures.py | 6 ++++++ tests/test_job.py | 8 ++++++++ 4 files changed, 46 insertions(+), 4 deletions(-) diff --git a/rq/job.py b/rq/job.py index 2296b9a..ea612c7 100644 --- a/rq/job.py +++ b/rq/job.py @@ -108,7 +108,7 @@ class Job(object): job._instance = func.__self__ job._func_name = func.__name__ elif inspect.isfunction(func) or inspect.isbuiltin(func): - job._func_name = '{0}.{1}'.format(func.__module__, func.__name__) + job._func_name = '{0}.{1}'.format(func.__module__, func.__qualname__) elif isinstance(func, string_types): job._func_name = as_text(func) elif not inspect.isclass(func) and hasattr(func, '__call__'): # a callable class instance diff --git a/rq/utils.py b/rq/utils.py index 573c4aa..2686f52 100644 --- a/rq/utils.py +++ b/rq/utils.py @@ -125,9 +125,37 @@ class ColorizingStreamHandler(logging.StreamHandler): def import_attribute(name): """Return an attribute from a dotted path name (e.g. "path.to.func").""" - module_name, attribute = name.rsplit('.', 1) - module = importlib.import_module(module_name) - return getattr(module, attribute) + name_bits = name.split('.') + module_name_bits, attribute_bits = name_bits[:-1], [name_bits[-1]] + module = None + # When the attribute we look for is a staticmethod, module name in its + # dotted path is not the last-before-end word + # E.g.: package_a.package_b.module_a.ClassA.my_static_method + # Thus we remove the bits from the end of the name until we can import it + while len(module_name_bits): + try: + module_name = '.'.join(module_name_bits) + module = importlib.import_module(module_name) + break + except ModuleNotFoundError: + attribute_bits.insert(0, module_name_bits.pop()) + + if module is None: + raise ValueError(f'Invalid attribute name: {name}') + + attribute_name = '.'.join(attribute_bits) + if hasattr(module, attribute_name): + return getattr(module, attribute_name) + + # staticmethods + attribute_name = attribute_bits.pop() + attribute_owner_name = '.'.join(attribute_bits) + attribute_owner = getattr(module, attribute_owner_name) + + if not hasattr(attribute_owner, attribute_name): + raise ValueError(f'Invalid attribute name: {name}') + + return getattr(attribute_owner, attribute_name) def utcnow(): diff --git a/tests/fixtures.py b/tests/fixtures.py index 82e98bc..dd2218a 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -143,6 +143,12 @@ class UnicodeStringObject(object): return u'é' +class ClassWithAStaticMethod(object): + @staticmethod + def static_method(): + return u"I'm a static method" + + with Connection(): @job(queue='default') def decorated_job(x, y): diff --git a/tests/test_job.py b/tests/test_job.py index a65ca86..f50b03e 100644 --- a/tests/test_job.py +++ b/tests/test_job.py @@ -772,6 +772,14 @@ class TestJob(RQTestCase): self.assertIsNotNone(job.get_call_string()) job.perform() + def test_create_job_from_static_method(self): + """test creating jobs with static method""" + queue = Queue(connection=self.testconn) + + job = queue.enqueue(fixtures.ClassWithAStaticMethod.static_method) + self.assertIsNotNone(job.get_call_string()) + job.perform() + def test_create_job_with_ttl_should_have_ttl_after_enqueued(self): """test creating jobs with ttl and checks if get_jobs returns it properly [issue502]""" queue = Queue(connection=self.testconn)