From 72d219a24cd90c3d1a54fb31818ea898e93a79cd Mon Sep 17 00:00:00 2001 From: Jacob Oscarson Date: Wed, 15 May 2013 15:40:35 +0200 Subject: [PATCH 01/40] Writes an optional specified PID file on startup --- rq/scripts/__init__.py | 4 ++++ rq/scripts/rqworker.py | 6 ++++++ 2 files changed, 10 insertions(+) diff --git a/rq/scripts/__init__.py b/rq/scripts/__init__.py index fc31d09..6b45403 100644 --- a/rq/scripts/__init__.py +++ b/rq/scripts/__init__.py @@ -42,6 +42,10 @@ def setup_default_arguments(args, settings): if args.socket is None and socket: args.socket = socket + pid = settings.get('PID_FILE', False) + if args.pid is None and pid: + args.pid = pid + if args.db is None: args.db = settings.get('REDIS_DB', 0) diff --git a/rq/scripts/rqworker.py b/rq/scripts/rqworker.py index 29505ca..24aa714 100755 --- a/rq/scripts/rqworker.py +++ b/rq/scripts/rqworker.py @@ -27,6 +27,8 @@ def parse_args(): parser.add_argument('--verbose', '-v', action='store_true', default=False, help='Show more output') parser.add_argument('--quiet', '-q', action='store_true', default=False, help='Show less output') parser.add_argument('--sentry-dsn', action='store', default=None, metavar='URL', help='Report exceptions to this Sentry DSN') + parser.add_argument('--pid', '-i', action='store', default=None, + help='Write PID to this file') parser.add_argument('queues', nargs='*', help='The queues to listen on (default: \'default\')') return parser.parse_args() @@ -65,6 +67,10 @@ def main(): args.sentry_dsn = settings.get('SENTRY_DSN', os.environ.get('SENTRY_DSN', None)) + if args.pid: + with open(os.path.expanduser(args.pid), "w") as fp: + fp.write(str(os.getpid())) + setup_loghandlers_from_args(args) setup_redis(args) From a0d46c93f375c38ccd55828d1f0645c25c20cb3e Mon Sep 17 00:00:00 2001 From: Devi Date: Mon, 3 Jun 2013 21:46:25 +0530 Subject: [PATCH 02/40] take REDIS_URL from settings if exists --- rq/scripts/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/rq/scripts/__init__.py b/rq/scripts/__init__.py index fc31d09..6c567ac 100644 --- a/rq/scripts/__init__.py +++ b/rq/scripts/__init__.py @@ -30,6 +30,9 @@ def read_config_file(module): def setup_default_arguments(args, settings): """ Sets up args from settings or defaults """ + if args.url is None: + args.url = settings.get('REDIS_URL', 'redis://localhost:6379/0') + if args.host is None: args.host = settings.get('REDIS_HOST', 'localhost') From 17be89674448d4898f4a576b6c1f98865dc8566c Mon Sep 17 00:00:00 2001 From: Justin Unwin Date: Mon, 10 Jun 2013 22:51:49 +0200 Subject: [PATCH 03/40] Fix a problem caused when a nonstandard stdout is defined and not properly implemented --- rq/utils.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/rq/utils.py b/rq/utils.py index 5fa4940..c5d50cf 100644 --- a/rq/utils.py +++ b/rq/utils.py @@ -69,8 +69,11 @@ class _Colorizer(object): self.codes["darkyellow"] = self.codes["brown"] self.codes["fuscia"] = self.codes["fuchsia"] self.codes["white"] = self.codes["bold"] - self.notty = not sys.stdout.isatty() + try: + self.notty = not sys.stdout.isatty() + except: + self.notty = True def reset_color(self): return self.codes["reset"] From af04545bbff8024222d9736cf2d03df0b6149614 Mon Sep 17 00:00:00 2001 From: Justin Unwin Date: Tue, 11 Jun 2013 00:07:53 +0200 Subject: [PATCH 04/40] Ok i am being lazy using the try catch --- rq/utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rq/utils.py b/rq/utils.py index c5d50cf..44bbe65 100644 --- a/rq/utils.py +++ b/rq/utils.py @@ -70,9 +70,9 @@ class _Colorizer(object): self.codes["fuscia"] = self.codes["fuchsia"] self.codes["white"] = self.codes["bold"] - try: + if hasattr(sys.stdout, "isatty"): self.notty = not sys.stdout.isatty() - except: + else: self.notty = True def reset_color(self): From 5cfbae61a9dc28b1fe330e8b0c7158dda96bbaf0 Mon Sep 17 00:00:00 2001 From: oniltonmaciel Date: Sun, 16 Jun 2013 17:38:25 -0400 Subject: [PATCH 05/40] Replaced limit by length and start by offset Replaced limit by length and start by offset to remove a possible ambiguity --- rq/queue.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/rq/queue.py b/rq/queue.py index a7a82bc..a418a4e 100644 --- a/rq/queue.py +++ b/rq/queue.py @@ -74,17 +74,17 @@ class Queue(object): return None return job - def get_job_ids(self, start=0, limit=-1): + def get_job_ids(self, offset=0, length=-1): """Returns a slice of job IDs in the queue.""" - if limit >= 0: - end = start + limit + if length >= 0: + end = start + length else: - end = limit + end = lenth return self.connection.lrange(self.key, start, end) - def get_jobs(self, start=0, limit=-1): + def get_jobs(self, offset=0, length=-1): """Returns a slice of jobs in the queue.""" - job_ids = self.get_job_ids(start, limit) + job_ids = self.get_job_ids(start, length) return compact([self.safe_fetch_job(job_id) for job_id in job_ids]) @property From 97de8ea3ccc122e06a8871f7e281db790379e13f Mon Sep 17 00:00:00 2001 From: Onilton Maciel Date: Sun, 16 Jun 2013 17:45:04 -0400 Subject: [PATCH 06/40] Fixed typos and errors found. Tests passing now --- rq/queue.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/rq/queue.py b/rq/queue.py index a418a4e..759c485 100644 --- a/rq/queue.py +++ b/rq/queue.py @@ -76,15 +76,16 @@ class Queue(object): def get_job_ids(self, offset=0, length=-1): """Returns a slice of job IDs in the queue.""" + start = offset if length >= 0: - end = start + length + end = offset + length else: - end = lenth + end = length return self.connection.lrange(self.key, start, end) def get_jobs(self, offset=0, length=-1): """Returns a slice of jobs in the queue.""" - job_ids = self.get_job_ids(start, length) + job_ids = self.get_job_ids(offset, length) return compact([self.safe_fetch_job(job_id) for job_id in job_ids]) @property From 3afc32f08ab4b73dfd937870e73073d756711b5c Mon Sep 17 00:00:00 2001 From: Onilton Maciel Date: Sun, 16 Jun 2013 18:11:03 -0400 Subject: [PATCH 07/40] End calculation in get_jobs_ids in fixed. Length is respected --- rq/queue.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rq/queue.py b/rq/queue.py index 759c485..d08311c 100644 --- a/rq/queue.py +++ b/rq/queue.py @@ -78,7 +78,7 @@ class Queue(object): """Returns a slice of job IDs in the queue.""" start = offset if length >= 0: - end = offset + length + end = offset + (length - 1) else: end = length return self.connection.lrange(self.key, start, end) From 995554cdd281bceff07d2dc75305670d56a02c40 Mon Sep 17 00:00:00 2001 From: Jacob Oscarson Date: Mon, 17 Jun 2013 10:25:16 +0200 Subject: [PATCH 08/40] Removed alternative parameters for PID file creation --- rq/scripts/__init__.py | 4 ---- rq/scripts/rqworker.py | 2 +- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/rq/scripts/__init__.py b/rq/scripts/__init__.py index 6b45403..fc31d09 100644 --- a/rq/scripts/__init__.py +++ b/rq/scripts/__init__.py @@ -42,10 +42,6 @@ def setup_default_arguments(args, settings): if args.socket is None and socket: args.socket = socket - pid = settings.get('PID_FILE', False) - if args.pid is None and pid: - args.pid = pid - if args.db is None: args.db = settings.get('REDIS_DB', 0) diff --git a/rq/scripts/rqworker.py b/rq/scripts/rqworker.py index 24aa714..b7e58fe 100755 --- a/rq/scripts/rqworker.py +++ b/rq/scripts/rqworker.py @@ -27,7 +27,7 @@ def parse_args(): parser.add_argument('--verbose', '-v', action='store_true', default=False, help='Show more output') parser.add_argument('--quiet', '-q', action='store_true', default=False, help='Show less output') parser.add_argument('--sentry-dsn', action='store', default=None, metavar='URL', help='Report exceptions to this Sentry DSN') - parser.add_argument('--pid', '-i', action='store', default=None, + parser.add_argument('--pid', action='store', default=None, help='Write PID to this file') parser.add_argument('queues', nargs='*', help='The queues to listen on (default: \'default\')') From 5505291818a9e610ea0d91a1e1be2ed84c1228a4 Mon Sep 17 00:00:00 2001 From: Jacob Oscarson Date: Mon, 17 Jun 2013 10:39:13 +0200 Subject: [PATCH 09/40] Polished help message for PID file specification --- rq/scripts/rqworker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rq/scripts/rqworker.py b/rq/scripts/rqworker.py index b7e58fe..d4c0040 100755 --- a/rq/scripts/rqworker.py +++ b/rq/scripts/rqworker.py @@ -28,7 +28,7 @@ def parse_args(): parser.add_argument('--quiet', '-q', action='store_true', default=False, help='Show less output') parser.add_argument('--sentry-dsn', action='store', default=None, metavar='URL', help='Report exceptions to this Sentry DSN') parser.add_argument('--pid', action='store', default=None, - help='Write PID to this file') + help='Write the process ID number to a file at the specified path') parser.add_argument('queues', nargs='*', help='The queues to listen on (default: \'default\')') return parser.parse_args() From c4e7c1799471bd521e409145099094e8e5bd49c0 Mon Sep 17 00:00:00 2001 From: Devi Date: Mon, 17 Jun 2013 14:49:53 +0530 Subject: [PATCH 10/40] deprecate use of host/db/port options for Redis --- rq/scripts/__init__.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/rq/scripts/__init__.py b/rq/scripts/__init__.py index 6c567ac..6fc89e4 100644 --- a/rq/scripts/__init__.py +++ b/rq/scripts/__init__.py @@ -1,5 +1,6 @@ import importlib import redis +from warnings import warn from rq import use_connection @@ -31,7 +32,12 @@ def read_config_file(module): def setup_default_arguments(args, settings): """ Sets up args from settings or defaults """ if args.url is None: - args.url = settings.get('REDIS_URL', 'redis://localhost:6379/0') + args.url = settings.get('REDIS_URL') + + if (args.host or args.port or args.socket or args.db or args.password): + warn('Host, port, db, password options for Redis will not be \ + supported in future versions of RQ. \ + Please use `REDIS_URL` or `--url` instead.', DeprecationWarning) if args.host is None: args.host = settings.get('REDIS_HOST', 'localhost') From 2e9fa1d0a70dbfe4e96d310b77aad0e565fce3d9 Mon Sep 17 00:00:00 2001 From: Vincent Driessen Date: Mon, 17 Jun 2013 08:48:57 +0200 Subject: [PATCH 11/40] Release 0.3.8. --- CHANGES.md | 2 +- rq/version.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 93cc7bc..2d55158 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,5 @@ ### 0.3.8 -(not yet released) +(June 17th, 2013) - `rqworker` and `rqinfo` have a `--url` argument to connect to a Redis url. diff --git a/rq/version.py b/rq/version.py index 9aaa604..6cf0c32 100644 --- a/rq/version.py +++ b/rq/version.py @@ -1 +1 @@ -VERSION = '0.3.8-dev' +VERSION = '0.3.8' From f9897ea920beacbe4f6b881e6a5645c1376297c1 Mon Sep 17 00:00:00 2001 From: Wojciech Bederski Date: Tue, 21 May 2013 17:35:18 +0200 Subject: [PATCH 12/40] register_sentry breaks failed queue when func cannot be imported --- tests/test_sentry.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 tests/test_sentry.py diff --git a/tests/test_sentry.py b/tests/test_sentry.py new file mode 100644 index 0000000..3fa4737 --- /dev/null +++ b/tests/test_sentry.py @@ -0,0 +1,29 @@ +from tests import RQTestCase +from rq import Queue, Worker, get_failed_queue +from rq.contrib.sentry import register_sentry + + +class FakeSentry(object): + def captureException(self, *args, **kwds): + pass # we cannot check this, because worker forks + + +class TestSentry(RQTestCase): + + def test_work_fails(self): + """Non importable jobs should be put on the failed queue event with sentry""" + q = Queue() + failed_q = get_failed_queue() + + # Action + q.enqueue('_non.importable.job') + self.assertEquals(q.count, 1) + + w = Worker([q]) + register_sentry(FakeSentry(), w) + + w.work(burst=True) + + # Postconditions + self.assertEquals(failed_q.count, 1) + self.assertEquals(q.count, 0) From 0198916856bdb416f68bbae4637064d2f3cab5d8 Mon Sep 17 00:00:00 2001 From: Selwin Ong Date: Tue, 30 Jul 2013 19:31:51 +0700 Subject: [PATCH 13/40] Fixes an issue where register_sentry breaks when logging jobs with unimportable function. --- rq/contrib/sentry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rq/contrib/sentry.py b/rq/contrib/sentry.py index abaef72..4b776d1 100644 --- a/rq/contrib/sentry.py +++ b/rq/contrib/sentry.py @@ -7,7 +7,7 @@ def register_sentry(client, worker): exc_info=exc_info, extra={ 'job_id': job.id, - 'func': job.func, + 'func': job.func_name, 'args': job.args, 'kwargs': job.kwargs, 'description': job.description, From 610c26d8168ce2bf7a0d2abb52fab2a2f2f693c4 Mon Sep 17 00:00:00 2001 From: Alex Morega Date: Mon, 5 Aug 2013 13:47:15 +0300 Subject: [PATCH 14/40] run tests on py33 --- .travis.yml | 1 + tox.ini | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index e51ca34..e56fe48 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,6 +2,7 @@ language: python python: - "2.6" - "2.7" + - "3.3" - "pypy" install: - if [[ $TRAVIS_PYTHON_VERSION == 2.6 ]]; then pip install -r py26-requirements.txt; fi diff --git a/tox.ini b/tox.ini index fbf798c..af0281a 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist=py26,py27,pypy +envlist=py26,py27,py33,pypy [testenv] commands=py.test From a75ea0d693d2be2e030b366b62c88f6bc01413d1 Mon Sep 17 00:00:00 2001 From: Alex Morega Date: Mon, 5 Aug 2013 14:06:59 +0300 Subject: [PATCH 15/40] changes by python-modernize --- rq/compat/dictconfig.py | 32 ++++++++++++++++---------------- rq/job.py | 4 ++-- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/rq/compat/dictconfig.py b/rq/compat/dictconfig.py index 345ffb8..803b7cd 100644 --- a/rq/compat/dictconfig.py +++ b/rq/compat/dictconfig.py @@ -245,7 +245,7 @@ class BaseConfigurator(object): def configure_custom(self, config): """Configure an object with a user-supplied factory.""" c = config.pop('()') - if not hasattr(c, '__call__') and hasattr(types, 'ClassType') and type(c) != types.ClassType: + if not hasattr(c, '__call__') and type(c) != type: c = self.resolve(c) props = config.pop('.', None) # Check for valid identifiers @@ -296,21 +296,21 @@ class DictConfigurator(BaseConfigurator): level = handler_config.get('level', None) if level: handler.setLevel(_checkLevel(level)) - except StandardError, e: + except Exception as e: raise ValueError('Unable to configure handler ' '%r: %s' % (name, e)) loggers = config.get('loggers', EMPTY_DICT) for name in loggers: try: self.configure_logger(name, loggers[name], True) - except StandardError, e: + except Exception as e: raise ValueError('Unable to configure logger ' '%r: %s' % (name, e)) root = config.get('root', None) if root: try: self.configure_root(root, True) - except StandardError, e: + except Exception as e: raise ValueError('Unable to configure root ' 'logger: %s' % e) else: @@ -325,7 +325,7 @@ class DictConfigurator(BaseConfigurator): try: formatters[name] = self.configure_formatter( formatters[name]) - except StandardError, e: + except Exception as e: raise ValueError('Unable to configure ' 'formatter %r: %s' % (name, e)) # Next, do filters - they don't refer to anything else, either @@ -333,7 +333,7 @@ class DictConfigurator(BaseConfigurator): for name in filters: try: filters[name] = self.configure_filter(filters[name]) - except StandardError, e: + except Exception as e: raise ValueError('Unable to configure ' 'filter %r: %s' % (name, e)) @@ -346,7 +346,7 @@ class DictConfigurator(BaseConfigurator): handler = self.configure_handler(handlers[name]) handler.name = name handlers[name] = handler - except StandardError, e: + except Exception as e: raise ValueError('Unable to configure handler ' '%r: %s' % (name, e)) # Next, do loggers - they refer to handlers and filters @@ -385,7 +385,7 @@ class DictConfigurator(BaseConfigurator): existing.remove(name) try: self.configure_logger(name, loggers[name]) - except StandardError, e: + except Exception as e: raise ValueError('Unable to configure logger ' '%r: %s' % (name, e)) @@ -408,7 +408,7 @@ class DictConfigurator(BaseConfigurator): if root: try: self.configure_root(root) - except StandardError, e: + except Exception as e: raise ValueError('Unable to configure root ' 'logger: %s' % e) finally: @@ -420,7 +420,7 @@ class DictConfigurator(BaseConfigurator): factory = config['()'] # for use in exception handler try: result = self.configure_custom(config) - except TypeError, te: + except TypeError as te: if "'format'" not in str(te): raise #Name of parameter changed from fmt to format. @@ -450,7 +450,7 @@ class DictConfigurator(BaseConfigurator): for f in filters: try: filterer.addFilter(self.config['filters'][f]) - except StandardError, e: + except Exception as e: raise ValueError('Unable to add filter %r: %s' % (f, e)) def configure_handler(self, config): @@ -459,14 +459,14 @@ class DictConfigurator(BaseConfigurator): if formatter: try: formatter = self.config['formatters'][formatter] - except StandardError, e: + except Exception as e: raise ValueError('Unable to set formatter ' '%r: %s' % (formatter, e)) level = config.pop('level', None) filters = config.pop('filters', None) if '()' in config: c = config.pop('()') - if not hasattr(c, '__call__') and hasattr(types, 'ClassType') and type(c) != types.ClassType: + if not hasattr(c, '__call__') and type(c) != type: c = self.resolve(c) factory = c else: @@ -476,7 +476,7 @@ class DictConfigurator(BaseConfigurator): 'target' in config: try: config['target'] = self.config['handlers'][config['target']] - except StandardError, e: + except Exception as e: raise ValueError('Unable to set target handler ' '%r: %s' % (config['target'], e)) elif issubclass(klass, logging.handlers.SMTPHandler) and\ @@ -489,7 +489,7 @@ class DictConfigurator(BaseConfigurator): kwargs = dict([(k, config[k]) for k in config if valid_ident(k)]) try: result = factory(**kwargs) - except TypeError, te: + except TypeError as te: if "'stream'" not in str(te): raise #The argument name changed from strm to stream @@ -511,7 +511,7 @@ class DictConfigurator(BaseConfigurator): for h in handlers: try: logger.addHandler(self.config['handlers'][h]) - except StandardError, e: + except Exception as e: raise ValueError('Unable to add handler %r: %s' % (h, e)) def common_logger_config(self, logger, config, incremental=False): diff --git a/rq/job.py b/rq/job.py index a4772a4..b3370b5 100644 --- a/rq/job.py +++ b/rq/job.py @@ -26,7 +26,7 @@ def unpickle(pickled_string): """ try: obj = loads(pickled_string) - except (StandardError, UnpicklingError) as e: + except (Exception, UnpicklingError) as e: raise UnpickleError('Could not unpickle.', pickled_string, e) return obj @@ -76,7 +76,7 @@ class Job(object): assert isinstance(kwargs, dict), '%r is not a valid kwargs dict.' % (kwargs,) job = cls(connection=connection) if inspect.ismethod(func): - job._instance = func.im_self + job._instance = func.__self__ job._func_name = func.__name__ elif inspect.isfunction(func) or inspect.isbuiltin(func): job._func_name = '%s.%s' % (func.__module__, func.__name__) From 2e517001a9c03487413f123f4f2a37eb9a7f2b91 Mon Sep 17 00:00:00 2001 From: Alex Morega Date: Mon, 5 Aug 2013 14:11:01 +0300 Subject: [PATCH 16/40] pass on arguments to py.test --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index af0281a..bb92511 100644 --- a/tox.ini +++ b/tox.ini @@ -2,7 +2,7 @@ envlist=py26,py27,py33,pypy [testenv] -commands=py.test +commands=py.test [] deps=pytest [testenv:py26] From a3b5ce5e46122c84a06ab1b36c20df17a87eac52 Mon Sep 17 00:00:00 2001 From: Alex Morega Date: Mon, 5 Aug 2013 14:24:06 +0300 Subject: [PATCH 17/40] accomodate py3 imports and builtins --- rq/compat/__init__.py | 11 +++++++++++ rq/compat/dictconfig.py | 3 ++- rq/decorators.py | 3 ++- rq/job.py | 8 ++++++-- rq/local.py | 5 ++++- rq/queue.py | 4 ++-- tests/test_job.py | 5 ++++- 7 files changed, 31 insertions(+), 8 deletions(-) diff --git a/rq/compat/__init__.py b/rq/compat/__init__.py index a10576f..e08e4c0 100644 --- a/rq/compat/__init__.py +++ b/rq/compat/__init__.py @@ -39,3 +39,14 @@ else: opfunc.__doc__ = getattr(int, opname).__doc__ setattr(cls, opname, opfunc) return cls + + +PY2 = sys.version_info[0] < 3 + +if PY2: + string_types = (str, unicode) + text_type = unicode + +else: + string_types = (str,) + text_type = str diff --git a/rq/compat/dictconfig.py b/rq/compat/dictconfig.py index 803b7cd..c314afb 100644 --- a/rq/compat/dictconfig.py +++ b/rq/compat/dictconfig.py @@ -21,6 +21,7 @@ import logging.handlers import re import sys import types +from rq.compat import string_types IDENTIFIER = re.compile('^[a-z_][a-z0-9_]*$', re.I) @@ -230,7 +231,7 @@ class BaseConfigurator(object): isinstance(value, tuple): value = ConvertingTuple(value) value.configurator = self - elif isinstance(value, basestring): # str for py3k + elif isinstance(value, string_types): # str for py3k m = self.CONVERT_PATTERN.match(value) if m: d = m.groupdict() diff --git a/rq/decorators.py b/rq/decorators.py index 4d57c90..d57b6cc 100644 --- a/rq/decorators.py +++ b/rq/decorators.py @@ -2,6 +2,7 @@ from functools import wraps from .queue import Queue from .connections import resolve_connection from .worker import DEFAULT_RESULT_TTL +from rq.compat import string_types class job(object): @@ -26,7 +27,7 @@ class job(object): def __call__(self, f): @wraps(f) def delay(*args, **kwargs): - if isinstance(self.queue, basestring): + if isinstance(self.queue, string_types): queue = Queue(name=self.queue, connection=self.connection) else: queue = self.queue diff --git a/rq/job.py b/rq/job.py index b3370b5..61dd247 100644 --- a/rq/job.py +++ b/rq/job.py @@ -2,10 +2,14 @@ import importlib import inspect import times from uuid import uuid4 -from cPickle import loads, dumps, UnpicklingError +try: + from cPickle import loads, dumps, UnpicklingError +except ImportError: # noqa + from pickle import loads, dumps, UnpicklingError # noqa from .local import LocalStack from .connections import resolve_connection from .exceptions import UnpickleError, NoSuchJobError +from rq.compat import text_type def enum(name, *sequential, **named): @@ -194,7 +198,7 @@ class Job(object): first time the ID is requested. """ if self._id is None: - self._id = unicode(uuid4()) + self._id = text_type(uuid4()) return self._id def set_id(self, value): diff --git a/rq/local.py b/rq/local.py index e789c81..555a6d1 100644 --- a/rq/local.py +++ b/rq/local.py @@ -17,7 +17,10 @@ except ImportError: # noqa try: from thread import get_ident # noqa except ImportError: # noqa - from dummy_thread import get_ident # noqa + try: + from _thread import get_ident # noqa + except ImportError: # noqa + from dummy_thread import get_ident # noqa def release_local(local): diff --git a/rq/queue.py b/rq/queue.py index d08311c..f3b5f92 100644 --- a/rq/queue.py +++ b/rq/queue.py @@ -3,7 +3,7 @@ from .connections import resolve_connection from .job import Job, Status from .exceptions import (NoSuchJobError, UnpickleError, InvalidJobOperationError, DequeueTimeout) -from .compat import total_ordering +from .compat import total_ordering, string_types def get_failed_queue(connection=None): @@ -154,7 +154,7 @@ class Queue(object): * A string, representing the location of a function (must be meaningful to the import context of the workers) """ - if not isinstance(f, basestring) and f.__module__ == '__main__': + if not isinstance(f, string_types) and f.__module__ == '__main__': raise ValueError( 'Functions from the __main__ module cannot be processed ' 'by workers.') diff --git a/tests/test_job.py b/tests/test_job.py index 8f59472..3135178 100644 --- a/tests/test_job.py +++ b/tests/test_job.py @@ -3,7 +3,10 @@ from datetime import datetime from tests import RQTestCase from tests.fixtures import Number, some_calculation, say_hello, access_self from tests.helpers import strip_milliseconds -from cPickle import loads +try: + from cPickle import loads +except ImportError: + from pickle import loads from rq.job import Job, get_current_job from rq.exceptions import NoSuchJobError, UnpickleError from rq.queue import Queue From 6eeee85cc3f34a1f8b3edebbe358d59ba0b8e4c8 Mon Sep 17 00:00:00 2001 From: Alex Morega Date: Mon, 5 Aug 2013 14:54:06 +0300 Subject: [PATCH 18/40] remove backwards compat for custom properties --- rq/job.py | 40 ---------------------------------------- 1 file changed, 40 deletions(-) diff --git a/rq/job.py b/rq/job.py index 61dd247..6871419 100644 --- a/rq/job.py +++ b/rq/job.py @@ -399,44 +399,4 @@ class Job(object): return hash(self.id) - # Backwards compatibility for custom properties - def __getattr__(self, name): # noqa - import warnings - warnings.warn( - "Getting custom properties from the job instance directly " - "will be unsupported as of RQ 0.4. Please use the meta dict " - "to store all custom variables. So instead of this:\n\n" - "\tjob.foo\n\n" - "Use this:\n\n" - "\tjob.meta['foo']\n", - SyntaxWarning) - try: - return self.__dict__['meta'][name] # avoid recursion - except KeyError: - return getattr(super(Job, self), name) - - def __setattr__(self, name, value): - # Ignore the "private" fields - private_attrs = set(['origin', '_func_name', 'ended_at', - 'description', '_args', 'created_at', 'enqueued_at', 'connection', - '_result', 'result', 'timeout', '_kwargs', 'exc_info', '_id', - 'data', '_instance', 'result_ttl', '_status', 'status', 'meta']) - - if name in private_attrs: - object.__setattr__(self, name, value) - return - - import warnings - warnings.warn( - "Setting custom properties on the job instance directly will " - "be unsupported as of RQ 0.4. Please use the meta dict to " - "store all custom variables. So instead of this:\n\n" - "\tjob.foo = 'bar'\n\n" - "Use this:\n\n" - "\tjob.meta['foo'] = 'bar'\n", - SyntaxWarning) - - self.__dict__['meta'][name] = value - - _job_stack = LocalStack() From 670a4e2a4ebdb6be97ff9776f53cf70446013fa5 Mon Sep 17 00:00:00 2001 From: Vincent Driessen Date: Thu, 14 Feb 2013 15:53:58 +0100 Subject: [PATCH 19/40] Python 3 chokes on this one. Whatever man. --- tests/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/__init__.py b/tests/__init__.py index 688c11f..ac66204 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -53,7 +53,7 @@ class RQTestCase(unittest.TestCase): cls.testconn = testconn # Shut up logging - logging.disable("ERROR") + logging.disable(logging.ERROR) def setUp(self): # Flush beforewards (we like our hygiene) From 8d61d3bf26e84bfa5c1c6f4c212b3eb944e9b792 Mon Sep 17 00:00:00 2001 From: Alex Morega Date: Mon, 5 Aug 2013 15:14:07 +0300 Subject: [PATCH 20/40] port string handling to py3 Redis uses byte values for everything. We save queue names and job IDs as unicode. So we need to convert every time we get data from redis. --- rq/compat/__init__.py | 19 +++++++++++++++++++ rq/job.py | 22 +++++++++++----------- rq/queue.py | 16 +++++++++------- rq/worker.py | 3 ++- tests/test_job.py | 18 +++++++++--------- tests/test_queue.py | 6 ++++-- tests/test_worker.py | 2 +- 7 files changed, 55 insertions(+), 31 deletions(-) diff --git a/rq/compat/__init__.py b/rq/compat/__init__.py index e08e4c0..9ce6e4e 100644 --- a/rq/compat/__init__.py +++ b/rq/compat/__init__.py @@ -47,6 +47,25 @@ if PY2: string_types = (str, unicode) text_type = unicode + def as_text(v): + return v + + def decode_redis_hash(h): + return h + else: string_types = (str,) text_type = str + + def as_text(v): + if v is None: + return None + elif isinstance(v, bytes): + return v.decode('ascii') + elif isinstance(v, str): + return v + else: + raise ValueError('Unknown type %r' % type(v)) + + def decode_redis_hash(h): + return dict((as_text(k), h[k]) for k in h) diff --git a/rq/job.py b/rq/job.py index 6871419..6bf9189 100644 --- a/rq/job.py +++ b/rq/job.py @@ -9,7 +9,7 @@ except ImportError: # noqa from .local import LocalStack from .connections import resolve_connection from .exceptions import UnpickleError, NoSuchJobError -from rq.compat import text_type +from rq.compat import text_type, decode_redis_hash, as_text def enum(name, *sequential, **named): @@ -98,7 +98,7 @@ class Job(object): return self._func_name def _get_status(self): - self._status = self.connection.hget(self.key, 'status') + self._status = as_text(self.connection.hget(self.key, 'status')) return self._status def _set_status(self, status): @@ -210,7 +210,7 @@ class Job(object): @classmethod def key_for(cls, job_id): """The Redis key that is used to store job hash under.""" - return 'rq:job:%s' % (job_id,) + return b'rq:job:' + job_id.encode('ascii') @property def key(self): @@ -259,7 +259,7 @@ class Job(object): Will raise a NoSuchJobError if no corresponding Redis key exists. """ key = self.key - obj = self.connection.hgetall(key) + obj = decode_redis_hash(self.connection.hgetall(key)) if len(obj) == 0: raise NoSuchJobError('No such job: %s' % (key,)) @@ -267,7 +267,7 @@ class Job(object): if date_str is None: return None else: - return times.to_universal(date_str) + return times.to_universal(as_text(date_str)) try: self.data = obj['data'] @@ -279,16 +279,16 @@ class Job(object): except UnpickleError: if not safe: raise - self.created_at = to_date(obj.get('created_at')) - self.origin = obj.get('origin') - self.description = obj.get('description') - self.enqueued_at = to_date(obj.get('enqueued_at')) - self.ended_at = to_date(obj.get('ended_at')) + self.created_at = to_date(as_text(obj.get('created_at'))) + self.origin = as_text(obj.get('origin')) + self.description = as_text(obj.get('description')) + self.enqueued_at = to_date(as_text(obj.get('enqueued_at'))) + self.ended_at = to_date(as_text(obj.get('ended_at'))) self._result = unpickle(obj.get('result')) if obj.get('result') else None # noqa self.exc_info = obj.get('exc_info') self.timeout = int(obj.get('timeout')) if obj.get('timeout') else None self.result_ttl = int(obj.get('result_ttl')) if obj.get('result_ttl') else None # noqa - self._status = obj.get('status') if obj.get('status') else None + self._status = as_text(obj.get('status') if obj.get('status') else None) self.meta = unpickle(obj.get('meta')) if obj.get('meta') else {} def save(self, pipeline=None): diff --git a/rq/queue.py b/rq/queue.py index f3b5f92..3f9cd96 100644 --- a/rq/queue.py +++ b/rq/queue.py @@ -3,7 +3,7 @@ from .connections import resolve_connection from .job import Job, Status from .exceptions import (NoSuchJobError, UnpickleError, InvalidJobOperationError, DequeueTimeout) -from .compat import total_ordering, string_types +from .compat import total_ordering, string_types, as_text def get_failed_queue(connection=None): @@ -27,8 +27,9 @@ class Queue(object): connection = resolve_connection(connection) def to_queue(queue_key): - return cls.from_queue_key(queue_key, connection=connection) - return map(to_queue, connection.keys('%s*' % prefix)) + return cls.from_queue_key(as_text(queue_key), + connection=connection) + return list(map(to_queue, connection.keys('%s*' % prefix))) @classmethod def from_queue_key(cls, queue_key, connection=None): @@ -81,7 +82,8 @@ class Queue(object): end = offset + (length - 1) else: end = length - return self.connection.lrange(self.key, start, end) + return [as_text(job_id) for job_id in + self.connection.lrange(self.key, start, end)] def get_jobs(self, offset=0, length=-1): """Returns a slice of jobs in the queue.""" @@ -116,7 +118,7 @@ class Queue(object): self.connection.rename(self.key, COMPACT_QUEUE) while True: - job_id = self.connection.lpop(COMPACT_QUEUE) + job_id = as_text(self.connection.lpop(COMPACT_QUEUE)) if job_id is None: break if Job.exists(job_id, self.connection): @@ -204,7 +206,7 @@ class Queue(object): def pop_job_id(self): """Pops a given job ID from this Redis queue.""" - return self.connection.lpop(self.key) + return as_text(self.connection.lpop(self.key)) @classmethod def lpop(cls, queue_keys, timeout, connection=None): @@ -274,7 +276,7 @@ class Queue(object): result = cls.lpop(queue_keys, timeout, connection=connection) if result is None: return None - queue_key, job_id = result + queue_key, job_id = map(as_text, result) queue = cls.from_queue_key(queue_key, connection=connection) try: job = Job.fetch(job_id, connection=connection) diff --git a/rq/worker.py b/rq/worker.py index 8f1da27..4717c38 100644 --- a/rq/worker.py +++ b/rq/worker.py @@ -21,6 +21,7 @@ from .logutils import setup_loghandlers from .exceptions import NoQueueError, UnpickleError, DequeueTimeout from .timeouts import death_penalty_after from .version import VERSION +from rq.compat import text_type green = make_colorizer('darkgreen') yellow = make_colorizer('darkyellow') @@ -431,7 +432,7 @@ class Worker(object): if rv is None: self.log.info('Job OK') else: - self.log.info('Job OK, result = %s' % (yellow(unicode(rv)),)) + self.log.info('Job OK, result = %s' % (yellow(text_type(rv)),)) if result_ttl == 0: self.log.info('Result discarded immediately.') diff --git a/tests/test_job.py b/tests/test_job.py index 3135178..522f1a6 100644 --- a/tests/test_job.py +++ b/tests/test_job.py @@ -78,7 +78,7 @@ class TestJob(RQTestCase): # Saving creates a Redis hash self.assertEquals(self.testconn.exists(job.key), False) job.save() - self.assertEquals(self.testconn.type(job.key), 'hash') + self.assertEquals(self.testconn.type(job.key), b'hash') # Saving writes pickled job data unpickled_data = loads(self.testconn.hget(job.key, 'data')) @@ -108,15 +108,15 @@ class TestJob(RQTestCase): job.save() expected_date = strip_milliseconds(job.created_at) - stored_date = self.testconn.hget(job.key, 'created_at') + stored_date = self.testconn.hget(job.key, 'created_at').decode('ascii') self.assertEquals( times.to_universal(stored_date), expected_date) # ... and no other keys are stored - self.assertItemsEqual( + self.assertEqual( self.testconn.hkeys(job.key), - ['created_at']) + [b'created_at']) def test_persistence_of_typical_jobs(self): """Storing typical jobs.""" @@ -124,15 +124,15 @@ class TestJob(RQTestCase): job.save() expected_date = strip_milliseconds(job.created_at) - stored_date = self.testconn.hget(job.key, 'created_at') + stored_date = self.testconn.hget(job.key, 'created_at').decode('ascii') self.assertEquals( times.to_universal(stored_date), expected_date) # ... and no other keys are stored - self.assertItemsEqual( - self.testconn.hkeys(job.key), - ['created_at', 'data', 'description']) + self.assertEqual( + sorted(self.testconn.hkeys(job.key)), + [b'created_at', b'data', b'description']) def test_store_then_fetch(self): """Store, then fetch.""" @@ -172,7 +172,7 @@ class TestJob(RQTestCase): # equivalent to a worker not having the most up-to-date source code # and unable to import the function) data = self.testconn.hget(job.key, 'data') - unimportable_data = data.replace('say_hello', 'shut_up') + unimportable_data = data.replace(b'say_hello', b'shut_up') self.testconn.hset(job.key, 'data', unimportable_data) job.refresh() diff --git a/tests/test_queue.py b/tests/test_queue.py index 0385e0a..a6dabb2 100644 --- a/tests/test_queue.py +++ b/tests/test_queue.py @@ -107,7 +107,9 @@ class TestQueue(RQTestCase): # Inspect data inside Redis q_key = 'rq:queue:default' self.assertEquals(self.testconn.llen(q_key), 1) - self.assertEquals(self.testconn.lrange(q_key, 0, -1)[0], job_id) + self.assertEquals( + self.testconn.lrange(q_key, 0, -1)[0].decode('ascii'), + job_id) def test_enqueue_sets_metadata(self): """Enqueueing job onto queues modifies meta data.""" @@ -258,7 +260,7 @@ class TestFailedQueue(RQTestCase): job.save() get_failed_queue().quarantine(job, Exception('Some fake error')) # noqa - self.assertItemsEqual(Queue.all(), [get_failed_queue()]) # noqa + self.assertEqual(Queue.all(), [get_failed_queue()]) # noqa self.assertEquals(get_failed_queue().count, 1) get_failed_queue().requeue(job.id) diff --git a/tests/test_worker.py b/tests/test_worker.py index d9e2fe5..d1b2632 100644 --- a/tests/test_worker.py +++ b/tests/test_worker.py @@ -57,7 +57,7 @@ class TestWorker(RQTestCase): job = Job.create(func=div_by_zero, args=(3,)) job.save() data = self.testconn.hget(job.key, 'data') - invalid_data = data.replace('div_by_zero', 'nonexisting_job') + invalid_data = data.replace(b'div_by_zero', b'nonexisting_job') assert data != invalid_data self.testconn.hset(job.key, 'data', invalid_data) From 5b630b1e22f6c7109d6fd880e4d30f9d87e7d2c7 Mon Sep 17 00:00:00 2001 From: Alex Morega Date: Mon, 5 Aug 2013 15:51:04 +0300 Subject: [PATCH 21/40] port rqinfo to py3 --- rq/scripts/rqinfo.py | 16 ++++++++-------- rq/worker.py | 6 +++--- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/rq/scripts/rqinfo.py b/rq/scripts/rqinfo.py index df16eb2..a8d6f35 100755 --- a/rq/scripts/rqinfo.py +++ b/rq/scripts/rqinfo.py @@ -101,9 +101,9 @@ def show_workers(args): for w in ws: worker_queues = filter_queues(w.queue_names()) if not args.raw: - print '%s %s: %s' % (w.name, state_symbol(w.state), ', '.join(worker_queues)) + print('%s %s: %s' % (w.name, state_symbol(w.state), ', '.join(worker_queues))) else: - print 'worker %s %s %s' % (w.name, w.state, ','.join(worker_queues)) + print('worker %s %s %s' % (w.name, w.state, ','.join(worker_queues))) else: # Create reverse lookup table queues = dict([(q, []) for q in qs]) @@ -119,21 +119,21 @@ def show_workers(args): queues_str = ", ".join(sorted(map(lambda w: '%s (%s)' % (w.name, state_symbol(w.state)), queues[q]))) else: queues_str = '–' - print '%s %s' % (pad(q.name + ':', max_qname + 1), queues_str) + print('%s %s' % (pad(q.name + ':', max_qname + 1), queues_str)) if not args.raw: - print '%d workers, %d queues' % (len(ws), len(qs)) + print('%d workers, %d queues' % (len(ws), len(qs))) def show_both(args): show_queues(args) if not args.raw: - print '' + print('') show_workers(args) if not args.raw: - print '' + print('') import datetime - print 'Updated: %s' % datetime.datetime.now() + print('Updated: %s' % datetime.datetime.now()) def parse_args(): @@ -186,5 +186,5 @@ def main(): print(e) sys.exit(1) except KeyboardInterrupt: - print + print() sys.exit(0) diff --git a/rq/worker.py b/rq/worker.py index 4717c38..3ba4250 100644 --- a/rq/worker.py +++ b/rq/worker.py @@ -21,7 +21,7 @@ from .logutils import setup_loghandlers from .exceptions import NoQueueError, UnpickleError, DequeueTimeout from .timeouts import death_penalty_after from .version import VERSION -from rq.compat import text_type +from rq.compat import text_type, as_text green = make_colorizer('darkgreen') yellow = make_colorizer('darkyellow') @@ -68,7 +68,7 @@ class Worker(object): if connection is None: connection = get_current_connection() reported_working = connection.smembers(cls.redis_workers_keys) - workers = [cls.find_by_key(key, connection) for key in + workers = [cls.find_by_key(as_text(key), connection) for key in reported_working] return compact(workers) @@ -91,7 +91,7 @@ class Worker(object): name = worker_key[len(prefix):] worker = cls([], name, connection=connection) - queues = connection.hget(worker.key, 'queues') + queues = as_text(connection.hget(worker.key, 'queues')) worker._state = connection.hget(worker.key, 'state') or '?' if queues: worker.queues = [Queue(queue, connection=connection) From 328e7611d39ad989435e3fa6c1aa32879185c515 Mon Sep 17 00:00:00 2001 From: Alex Morega Date: Wed, 7 Aug 2013 00:10:54 +0300 Subject: [PATCH 22/40] use utf-8 instead of ascii --- rq/compat/__init__.py | 2 +- rq/job.py | 2 +- tests/test_job.py | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/rq/compat/__init__.py b/rq/compat/__init__.py index 9ce6e4e..ac9b7a9 100644 --- a/rq/compat/__init__.py +++ b/rq/compat/__init__.py @@ -61,7 +61,7 @@ else: if v is None: return None elif isinstance(v, bytes): - return v.decode('ascii') + return v.decode('utf-8') elif isinstance(v, str): return v else: diff --git a/rq/job.py b/rq/job.py index 6bf9189..42653e0 100644 --- a/rq/job.py +++ b/rq/job.py @@ -210,7 +210,7 @@ class Job(object): @classmethod def key_for(cls, job_id): """The Redis key that is used to store job hash under.""" - return b'rq:job:' + job_id.encode('ascii') + return b'rq:job:' + job_id.encode('utf-8') @property def key(self): diff --git a/tests/test_job.py b/tests/test_job.py index 522f1a6..0f376ec 100644 --- a/tests/test_job.py +++ b/tests/test_job.py @@ -108,7 +108,7 @@ class TestJob(RQTestCase): job.save() expected_date = strip_milliseconds(job.created_at) - stored_date = self.testconn.hget(job.key, 'created_at').decode('ascii') + stored_date = self.testconn.hget(job.key, 'created_at').decode('utf-8') self.assertEquals( times.to_universal(stored_date), expected_date) @@ -124,7 +124,7 @@ class TestJob(RQTestCase): job.save() expected_date = strip_milliseconds(job.created_at) - stored_date = self.testconn.hget(job.key, 'created_at').decode('ascii') + stored_date = self.testconn.hget(job.key, 'created_at').decode('utf-8') self.assertEquals( times.to_universal(stored_date), expected_date) From d418fbe9a881dd83aacc9d1420d6e52a601b7f51 Mon Sep 17 00:00:00 2001 From: Vincent Driessen Date: Tue, 20 Aug 2013 11:16:37 +0200 Subject: [PATCH 23/40] Remove accidentally committed file. --- .gitignore | 10 ++++++---- dist/rq-0.3.6.tar.gz | Bin 38622 -> 0 bytes 2 files changed, 6 insertions(+), 4 deletions(-) delete mode 100644 dist/rq-0.3.6.tar.gz diff --git a/.gitignore b/.gitignore index 2f5bd47..321839e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,7 @@ *.pyc -dump.rdb -/*.egg-info -.env -/.tox/ +*.egg-info + +/dump.rdb +/.env +/.tox +/dist diff --git a/dist/rq-0.3.6.tar.gz b/dist/rq-0.3.6.tar.gz deleted file mode 100644 index 25a27f4a7f4757c3df1888cebcbd89dd04ce290c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 38622 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 G_yPcU4|B%= From 75267e671ab24d6bbcbe2fb5745c5235eee2a898 Mon Sep 17 00:00:00 2001 From: Vincent Driessen Date: Tue, 20 Aug 2013 11:17:05 +0200 Subject: [PATCH 24/40] Add release notes. --- CHANGES.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index 2d55158..f74dc5e 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,11 @@ +### 0.3.9 +(August 20th, 2013) + +- Python 3 compatibility (Thanks, Alex!) + +- Minor bug fix where Sentry would break when func cannot be imported + + ### 0.3.8 (June 17th, 2013) From 004cf44af65c9a10b08793fc35b0df4f4adf13e6 Mon Sep 17 00:00:00 2001 From: Vincent Driessen Date: Tue, 20 Aug 2013 11:17:32 +0200 Subject: [PATCH 25/40] Bump version to 0.3.9. --- rq/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rq/version.py b/rq/version.py index 6cf0c32..a7e5d0d 100644 --- a/rq/version.py +++ b/rq/version.py @@ -1 +1 @@ -VERSION = '0.3.8' +VERSION = '0.3.9' From 998ff1a1abb991d298ad803f33234e51a5eed0c5 Mon Sep 17 00:00:00 2001 From: Alex Morega Date: Tue, 20 Aug 2013 13:01:10 +0300 Subject: [PATCH 26/40] fix reading of version in py3 --- setup.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/setup.py b/setup.py index 957a5ba..6a77f76 100644 --- a/setup.py +++ b/setup.py @@ -10,9 +10,9 @@ from setuptools import setup, find_packages def get_version(): basedir = os.path.dirname(__file__) with open(os.path.join(basedir, 'rq/version.py')) as f: - VERSION = None - exec(f.read()) - return VERSION + locals = {} + exec(f.read(), locals) + return locals['VERSION'] raise RuntimeError('No version info found.') From 49c7cf0af7730e88630534aa80a96f436d3829fd Mon Sep 17 00:00:00 2001 From: Vincent Driessen Date: Tue, 20 Aug 2013 12:06:43 +0200 Subject: [PATCH 27/40] Release 0.3.10. --- CHANGES.md | 6 ++++++ rq/version.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index f74dc5e..0de7d30 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,9 @@ +### 0.3.10 +(August 20th, 2013) + +- Bug fix in setup.py + + ### 0.3.9 (August 20th, 2013) diff --git a/rq/version.py b/rq/version.py index a7e5d0d..9802ebb 100644 --- a/rq/version.py +++ b/rq/version.py @@ -1 +1 @@ -VERSION = '0.3.9' +VERSION = '0.3.10' From 10bda9684d8b1af534b7ea6b4d9dad482b67af66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=2E=20=C4=B0brahim=20G=C3=BCng=C3=B6r?= Date: Wed, 21 Aug 2013 17:51:14 +0300 Subject: [PATCH 28/40] Pass description parameter to job constructor in order to distinguish job names in queue.jobs or in rq-dashboard. Add related test case. --- rq/job.py | 4 ++-- rq/queue.py | 8 +++++--- tests/test_job.py | 14 ++++++++++++++ 3 files changed, 21 insertions(+), 5 deletions(-) diff --git a/rq/job.py b/rq/job.py index 42653e0..fdc4b57 100644 --- a/rq/job.py +++ b/rq/job.py @@ -68,7 +68,7 @@ class Job(object): # Job construction @classmethod def create(cls, func, args=None, kwargs=None, connection=None, - result_ttl=None, status=None): + result_ttl=None, status=None, description=None): """Creates a new Job instance for the given function, arguments, and keyword arguments. """ @@ -88,7 +88,7 @@ class Job(object): job._func_name = func job._args = args job._kwargs = kwargs - job.description = job.get_call_string() + job.description = description or job.get_call_string() job.result_ttl = result_ttl job._status = status return job diff --git a/rq/queue.py b/rq/queue.py index 3f9cd96..427b665 100644 --- a/rq/queue.py +++ b/rq/queue.py @@ -129,7 +129,7 @@ class Queue(object): """Pushes a job ID on the corresponding Redis queue.""" self.connection.rpush(self.key, job_id) - def enqueue_call(self, func, args=None, kwargs=None, timeout=None, result_ttl=None): # noqa + def enqueue_call(self, func, args=None, kwargs=None, description=None, timeout=None, result_ttl=None): # noqa """Creates a job to represent the delayed function call and enqueues it. @@ -138,7 +138,7 @@ class Queue(object): contain options for RQ itself. """ timeout = timeout or self._default_timeout - job = Job.create(func, args, kwargs, connection=self.connection, + job = Job.create(func, args, kwargs, description=description, connection=self.connection, result_ttl=result_ttl, status=Status.QUEUED) return self.enqueue_job(job, timeout=timeout) @@ -164,15 +164,17 @@ class Queue(object): # Detect explicit invocations, i.e. of the form: # q.enqueue(foo, args=(1, 2), kwargs={'a': 1}, timeout=30) timeout = None + description=None result_ttl = None if 'args' in kwargs or 'kwargs' in kwargs: assert args == (), 'Extra positional arguments cannot be used when using explicit args and kwargs.' # noqa timeout = kwargs.pop('timeout', None) + description = kwargs.pop('description', None) args = kwargs.pop('args', None) result_ttl = kwargs.pop('result_ttl', None) kwargs = kwargs.pop('kwargs', None) - return self.enqueue_call(func=f, args=args, kwargs=kwargs, + return self.enqueue_call(func=f, args=args, kwargs=kwargs, description=description, timeout=timeout, result_ttl=result_ttl) def enqueue_job(self, job, timeout=None, set_meta_data=True): diff --git a/tests/test_job.py b/tests/test_job.py index 0f376ec..899fbce 100644 --- a/tests/test_job.py +++ b/tests/test_job.py @@ -203,6 +203,20 @@ class TestJob(RQTestCase): job_from_queue = Job.fetch(job.id, connection=self.testconn) self.assertEqual(job.result_ttl, None) + def test_description_is_persisted(self): + """Ensure that job's custom description is set properly""" + description = 'Say hello!' + job = Job.create(func=say_hello, args=('Lionel',), description=description) + job.save() + job_from_queue = Job.fetch(job.id, connection=self.testconn) + self.assertEqual(job.description, description) + + # Ensure job description is constructed from function call string + job = Job.create(func=say_hello, args=('Lionel',)) + job.save() + job_from_queue = Job.fetch(job.id, connection=self.testconn) + self.assertEqual(job.description, job.get_call_string()) + def test_job_access_within_job_function(self): """The current job is accessible within the job function.""" # Executing the job function from outside of RQ throws an exception From 536e8a89cf4cfd18c5002b724e66e98001907961 Mon Sep 17 00:00:00 2001 From: Vincent Driessen Date: Fri, 23 Aug 2013 15:10:47 +0200 Subject: [PATCH 29/40] Fixes for Python 3. --- rq/scripts/rqinfo.py | 4 ++-- rq/scripts/rqworker.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/rq/scripts/rqinfo.py b/rq/scripts/rqinfo.py index a8d6f35..73c1a7c 100755 --- a/rq/scripts/rqinfo.py +++ b/rq/scripts/rqinfo.py @@ -44,7 +44,7 @@ def state_symbol(state): def show_queues(args): if len(args.queues): - qs = map(Queue, args.queues) + qs = list(map(Queue, args.queues)) else: qs = Queue.all() @@ -79,7 +79,7 @@ def show_queues(args): def show_workers(args): if len(args.queues): - qs = map(Queue, args.queues) + qs = list(map(Queue, args.queues)) def any_matching_queue(worker): def queue_matches(q): diff --git a/rq/scripts/rqworker.py b/rq/scripts/rqworker.py index d4c0040..d23d176 100755 --- a/rq/scripts/rqworker.py +++ b/rq/scripts/rqworker.py @@ -77,7 +77,7 @@ def main(): cleanup_ghosts() try: - queues = map(Queue, args.queues) + queues = list(map(Queue, args.queues)) w = Worker(queues, name=args.name) # Should we configure Sentry? From 5d0f91e542b354c28ce19367b1df255f93a28caf Mon Sep 17 00:00:00 2001 From: Vincent Driessen Date: Fri, 23 Aug 2013 15:11:58 +0200 Subject: [PATCH 30/40] Release 0.3.11. --- CHANGES.md | 6 ++++++ rq/version.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 0de7d30..7c7662d 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,9 @@ +### 0.3.11 +(August 23th, 2013) + +- Some more fixes in command line scripts for Python 3 + + ### 0.3.10 (August 20th, 2013) diff --git a/rq/version.py b/rq/version.py index 9802ebb..efdcdac 100644 --- a/rq/version.py +++ b/rq/version.py @@ -1 +1 @@ -VERSION = '0.3.10' +VERSION = '0.3.11' From 57ea6203d1f4a3c1e0abc1f410fc5ab8544aba83 Mon Sep 17 00:00:00 2001 From: Vincent Driessen Date: Mon, 26 Aug 2013 10:13:12 +0200 Subject: [PATCH 31/40] Let's not change positional argument order. This may break other people's programs. --- rq/queue.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rq/queue.py b/rq/queue.py index 427b665..8d78be2 100644 --- a/rq/queue.py +++ b/rq/queue.py @@ -129,7 +129,8 @@ class Queue(object): """Pushes a job ID on the corresponding Redis queue.""" self.connection.rpush(self.key, job_id) - def enqueue_call(self, func, args=None, kwargs=None, description=None, timeout=None, result_ttl=None): # noqa + def enqueue_call(self, func, args=None, kwargs=None, timeout=None, + result_ttl=None, description=None): """Creates a job to represent the delayed function call and enqueues it. From 90fcb6c9d0fce43bd70b580bf024d47b3ef98712 Mon Sep 17 00:00:00 2001 From: Vincent Driessen Date: Mon, 26 Aug 2013 10:13:34 +0200 Subject: [PATCH 32/40] PEP8ify. --- rq/queue.py | 7 +++---- tests/test_job.py | 32 ++++++++++++++++---------------- 2 files changed, 19 insertions(+), 20 deletions(-) diff --git a/rq/queue.py b/rq/queue.py index 8d78be2..2404eea 100644 --- a/rq/queue.py +++ b/rq/queue.py @@ -158,14 +158,13 @@ class Queue(object): meaningful to the import context of the workers) """ if not isinstance(f, string_types) and f.__module__ == '__main__': - raise ValueError( - 'Functions from the __main__ module cannot be processed ' - 'by workers.') + raise ValueError('Functions from the __main__ module cannot be processed ' + 'by workers.') # Detect explicit invocations, i.e. of the form: # q.enqueue(foo, args=(1, 2), kwargs={'a': 1}, timeout=30) timeout = None - description=None + description = None result_ttl = None if 'args' in kwargs or 'kwargs' in kwargs: assert args == (), 'Extra positional arguments cannot be used when using explicit args and kwargs.' # noqa diff --git a/tests/test_job.py b/tests/test_job.py index 899fbce..01e181c 100644 --- a/tests/test_job.py +++ b/tests/test_job.py @@ -88,9 +88,9 @@ class TestJob(RQTestCase): """Fetching jobs.""" # Prepare test self.testconn.hset('rq:job:some_id', 'data', - "(S'tests.fixtures.some_calculation'\nN(I3\nI4\nt(dp1\nS'z'\nI2\nstp2\n.") # noqa + "(S'tests.fixtures.some_calculation'\nN(I3\nI4\nt(dp1\nS'z'\nI2\nstp2\n.") self.testconn.hset('rq:job:some_id', 'created_at', - "2012-02-07 22:13:24+0000") + '2012-02-07 22:13:24+0000') # Fetch returns a job job = Job.fetch('some_id') @@ -110,13 +110,13 @@ class TestJob(RQTestCase): expected_date = strip_milliseconds(job.created_at) stored_date = self.testconn.hget(job.key, 'created_at').decode('utf-8') self.assertEquals( - times.to_universal(stored_date), - expected_date) + times.to_universal(stored_date), + expected_date) # ... and no other keys are stored self.assertEqual( - self.testconn.hkeys(job.key), - [b'created_at']) + self.testconn.hkeys(job.key), + [b'created_at']) def test_persistence_of_typical_jobs(self): """Storing typical jobs.""" @@ -126,13 +126,13 @@ class TestJob(RQTestCase): expected_date = strip_milliseconds(job.created_at) stored_date = self.testconn.hget(job.key, 'created_at').decode('utf-8') self.assertEquals( - times.to_universal(stored_date), - expected_date) + times.to_universal(stored_date), + expected_date) # ... and no other keys are stored self.assertEqual( - sorted(self.testconn.hkeys(job.key)), - [b'created_at', b'data', b'description']) + sorted(self.testconn.hkeys(job.key)), + [b'created_at', b'data', b'description']) def test_store_then_fetch(self): """Store, then fetch.""" @@ -195,12 +195,12 @@ class TestJob(RQTestCase): """Ensure that job's result_ttl is set properly""" job = Job.create(func=say_hello, args=('Lionel',), result_ttl=10) job.save() - job_from_queue = Job.fetch(job.id, connection=self.testconn) + Job.fetch(job.id, connection=self.testconn) self.assertEqual(job.result_ttl, 10) job = Job.create(func=say_hello, args=('Lionel',)) job.save() - job_from_queue = Job.fetch(job.id, connection=self.testconn) + Job.fetch(job.id, connection=self.testconn) self.assertEqual(job.result_ttl, None) def test_description_is_persisted(self): @@ -208,13 +208,13 @@ class TestJob(RQTestCase): description = 'Say hello!' job = Job.create(func=say_hello, args=('Lionel',), description=description) job.save() - job_from_queue = Job.fetch(job.id, connection=self.testconn) + Job.fetch(job.id, connection=self.testconn) self.assertEqual(job.description, description) # Ensure job description is constructed from function call string job = Job.create(func=say_hello, args=('Lionel',)) job.save() - job_from_queue = Job.fetch(job.id, connection=self.testconn) + Job.fetch(job.id, connection=self.testconn) self.assertEqual(job.description, job.get_call_string()) def test_job_access_within_job_function(self): @@ -254,12 +254,12 @@ class TestJob(RQTestCase): """Test that jobs and results are expired properly.""" job = Job.create(func=say_hello) job.save() - + # Jobs with negative TTLs don't expire job.cleanup(ttl=-1) self.assertEqual(self.testconn.ttl(job.key), -1) - # Jobs with positive TTLs are eventually deleted + # Jobs with positive TTLs are eventually deleted job.cleanup(ttl=100) self.assertEqual(self.testconn.ttl(job.key), 100) From 1274b091159cb46efb2d7889f9ca6ac7c297cfc0 Mon Sep 17 00:00:00 2001 From: Vincent Driessen Date: Mon, 26 Aug 2013 10:17:34 +0200 Subject: [PATCH 33/40] Use constants in tests, instead of calling more functions. --- tests/test_job.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/tests/test_job.py b/tests/test_job.py index 01e181c..d25f60f 100644 --- a/tests/test_job.py +++ b/tests/test_job.py @@ -205,17 +205,16 @@ class TestJob(RQTestCase): def test_description_is_persisted(self): """Ensure that job's custom description is set properly""" - description = 'Say hello!' - job = Job.create(func=say_hello, args=('Lionel',), description=description) + job = Job.create(func=say_hello, args=('Lionel',), description=u'Say hello!') job.save() Job.fetch(job.id, connection=self.testconn) - self.assertEqual(job.description, description) + self.assertEqual(job.description, u'Say hello!') # Ensure job description is constructed from function call string job = Job.create(func=say_hello, args=('Lionel',)) job.save() Job.fetch(job.id, connection=self.testconn) - self.assertEqual(job.description, job.get_call_string()) + self.assertEqual(job.description, "tests.fixtures.say_hello('Lionel')") def test_job_access_within_job_function(self): """The current job is accessible within the job function.""" From 60dba7c1061f8b8d2286f64c7727f899d801dfdd Mon Sep 17 00:00:00 2001 From: Vincent Driessen Date: Mon, 26 Aug 2013 10:21:48 +0200 Subject: [PATCH 34/40] Update release notes. --- CHANGES.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index 7c7662d..7cae844 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,10 @@ +### 0.3.12 +(not released yet) + +- Ability to provide a custom job description (instead of using the default + function invocation hint). Thanks, İbrahim. + + ### 0.3.11 (August 23th, 2013) From a29661907484a638ac56ffc71b30c6a4e9ccc146 Mon Sep 17 00:00:00 2001 From: Sylvain Zimmer Date: Fri, 30 Aug 2013 01:44:46 +0200 Subject: [PATCH 35/40] Split Job.dump() and Job.save() --- rq/job.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/rq/job.py b/rq/job.py index fdc4b57..274b631 100644 --- a/rq/job.py +++ b/rq/job.py @@ -291,11 +291,8 @@ class Job(object): self._status = as_text(obj.get('status') if obj.get('status') else None) self.meta = unpickle(obj.get('meta')) if obj.get('meta') else {} - def save(self, pipeline=None): - """Persists the current job instance to its corresponding Redis key.""" - key = self.key - connection = pipeline if pipeline is not None else self.connection - + def dump(self): + """Returns a serialization of the current job instance""" obj = {} obj['created_at'] = times.format(self.created_at or times.now(), 'UTC') @@ -322,7 +319,14 @@ class Job(object): if self.meta: obj['meta'] = dumps(self.meta) - connection.hmset(key, obj) + return obj + + def save(self, pipeline=None): + """Persists the current job instance to its corresponding Redis key.""" + key = self.key + connection = pipeline if pipeline is not None else self.connection + + connection.hmset(key, self.dump()) def cancel(self): """Cancels the given job, which will prevent the job from ever being @@ -379,13 +383,13 @@ class Job(object): - If it's a positive number, set the job to expire in X seconds. - If result_ttl is negative, don't set an expiry to it (persist forever) - """ + """ if ttl == 0: self.cancel() elif ttl > 0: connection = pipeline if pipeline is not None else self.connection connection.expire(self.key, ttl) - + def __str__(self): return '' % (self.id, self.description) From 1b558704d390579b4aab6e571b5e49d8236939fc Mon Sep 17 00:00:00 2001 From: Vincent Driessen Date: Mon, 2 Sep 2013 22:51:26 +0200 Subject: [PATCH 36/40] Fix: COMPACT_QUEUE should be a unique key. This fixes #230. Thanks, @sylvinus. --- rq/queue.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/rq/queue.py b/rq/queue.py index 2404eea..e5b3b04 100644 --- a/rq/queue.py +++ b/rq/queue.py @@ -1,4 +1,6 @@ import times +import uuid + from .connections import resolve_connection from .job import Job, Status from .exceptions import (NoSuchJobError, UnpickleError, @@ -114,7 +116,7 @@ class Queue(object): """Removes all "dead" jobs from the queue by cycling through it, while guarantueeing FIFO semantics. """ - COMPACT_QUEUE = 'rq:queue:_compact' + COMPACT_QUEUE = 'rq:queue:_compact:{0}'.format(uuid.uuid4()) self.connection.rename(self.key, COMPACT_QUEUE) while True: From 12b6b3200e83636538a6e85d88b2ae02d3ec8999 Mon Sep 17 00:00:00 2001 From: Vincent Driessen Date: Mon, 2 Sep 2013 22:54:49 +0200 Subject: [PATCH 37/40] Update changelog. --- CHANGES.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index 7cae844..7578083 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,6 +4,9 @@ - Ability to provide a custom job description (instead of using the default function invocation hint). Thanks, İbrahim. +- Temporary key for the compact queue is now randomly generated, which should + avoid name clashes for concurrent compact actions. + ### 0.3.11 (August 23th, 2013) From 537476b488dc8ad91c105907f04b9939f5f206b0 Mon Sep 17 00:00:00 2001 From: Vincent Driessen Date: Mon, 2 Sep 2013 23:02:24 +0200 Subject: [PATCH 38/40] PEP8ify. --- rq/job.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/rq/job.py b/rq/job.py index 274b631..a168738 100644 --- a/rq/job.py +++ b/rq/job.py @@ -4,7 +4,7 @@ import times from uuid import uuid4 try: from cPickle import loads, dumps, UnpicklingError -except ImportError: # noqa +except ImportError: # noqa from pickle import loads, dumps, UnpicklingError # noqa from .local import LocalStack from .connections import resolve_connection @@ -16,8 +16,9 @@ def enum(name, *sequential, **named): values = dict(zip(sequential, range(len(sequential))), **named) return type(name, (), values) -Status = enum('Status', QUEUED='queued', FINISHED='finished', FAILED='failed', - STARTED='started') +Status = enum('Status', + QUEUED='queued', FINISHED='finished', FAILED='failed', + STARTED='started') def unpickle(pickled_string): @@ -287,7 +288,7 @@ class Job(object): self._result = unpickle(obj.get('result')) if obj.get('result') else None # noqa self.exc_info = obj.get('exc_info') self.timeout = int(obj.get('timeout')) if obj.get('timeout') else None - self.result_ttl = int(obj.get('result_ttl')) if obj.get('result_ttl') else None # noqa + self.result_ttl = int(obj.get('result_ttl')) if obj.get('result_ttl') else None # noqa self._status = as_text(obj.get('status') if obj.get('status') else None) self.meta = unpickle(obj.get('meta')) if obj.get('meta') else {} @@ -354,7 +355,6 @@ class Job(object): assert self.id == _job_stack.pop() return self._result - def get_ttl(self, default_ttl=None): """Returns ttl for a job that determines how long a job and its result will be persisted. In the future, this method will also be responsible @@ -390,7 +390,6 @@ class Job(object): connection = pipeline if pipeline is not None else self.connection connection.expire(self.key, ttl) - def __str__(self): return '' % (self.id, self.description) From 4d92079694f33ae14b4ce15e78db06cbb3badda0 Mon Sep 17 00:00:00 2001 From: Vincent Driessen Date: Mon, 2 Sep 2013 23:05:18 +0200 Subject: [PATCH 39/40] PEP8ify. --- rq/connections.py | 14 ++++++-------- rq/decorators.py | 6 +++--- rq/exceptions.py | 1 + rq/local.py | 7 ++++--- rq/timeouts.py | 2 +- rq/utils.py | 7 +++---- rq/worker.py | 38 ++++++++++++++++---------------------- 7 files changed, 34 insertions(+), 41 deletions(-) diff --git a/rq/connections.py b/rq/connections.py index f4a72e4..ee07070 100644 --- a/rq/connections.py +++ b/rq/connections.py @@ -18,8 +18,8 @@ def Connection(connection=None): finally: popped = pop_connection() assert popped == connection, \ - 'Unexpected Redis connection was popped off the stack. ' \ - 'Check your Redis connection setup.' + 'Unexpected Redis connection was popped off the stack. ' \ + 'Check your Redis connection setup.' def push_connection(redis): @@ -37,7 +37,7 @@ def use_connection(redis=None): use of use_connection() and stacked connection contexts. """ assert len(_connection_stack) <= 1, \ - 'You should not mix Connection contexts with use_connection().' + 'You should not mix Connection contexts with use_connection().' release_local(_connection_stack) if redis is None: @@ -61,13 +61,11 @@ def resolve_connection(connection=None): connection = get_current_connection() if connection is None: - raise NoRedisConnectionException( - 'Could not resolve a Redis connection.') + raise NoRedisConnectionException('Could not resolve a Redis connection.') return connection _connection_stack = LocalStack() -__all__ = ['Connection', - 'get_current_connection', 'push_connection', 'pop_connection', - 'use_connection'] +__all__ = ['Connection', 'get_current_connection', 'push_connection', + 'pop_connection', 'use_connection'] diff --git a/rq/decorators.py b/rq/decorators.py index d57b6cc..b433904 100644 --- a/rq/decorators.py +++ b/rq/decorators.py @@ -4,10 +4,10 @@ from .connections import resolve_connection from .worker import DEFAULT_RESULT_TTL from rq.compat import string_types -class job(object): +class job(object): def __init__(self, queue, connection=None, timeout=None, - result_ttl=DEFAULT_RESULT_TTL): + result_ttl=DEFAULT_RESULT_TTL): """A decorator that adds a ``delay`` method to the decorated function, which in turn creates a RQ job when called. Accepts a required ``queue`` argument that can be either a ``Queue`` instance or a string @@ -32,6 +32,6 @@ class job(object): else: queue = self.queue return queue.enqueue_call(f, args=args, kwargs=kwargs, - timeout=self.timeout, result_ttl=self.result_ttl) + timeout=self.timeout, result_ttl=self.result_ttl) f.delay = delay return f diff --git a/rq/exceptions.py b/rq/exceptions.py index 982a580..25e4f0e 100644 --- a/rq/exceptions.py +++ b/rq/exceptions.py @@ -15,5 +15,6 @@ class UnpickleError(Exception): super(UnpickleError, self).__init__(message, inner_exception) self.raw_data = raw_data + class DequeueTimeout(Exception): pass diff --git a/rq/local.py b/rq/local.py index 555a6d1..61f896f 100644 --- a/rq/local.py +++ b/rq/local.py @@ -13,13 +13,13 @@ # current thread ident. try: from greenlet import getcurrent as get_ident -except ImportError: # noqa +except ImportError: # noqa try: from thread import get_ident # noqa - except ImportError: # noqa + except ImportError: # noqa try: from _thread import get_ident # noqa - except ImportError: # noqa + except ImportError: # noqa from dummy_thread import get_ident # noqa @@ -119,6 +119,7 @@ class LocalStack(object): def _get__ident_func__(self): return self._local.__ident_func__ + def _set__ident_func__(self, value): # noqa object.__setattr__(self._local, '__ident_func__', value) __ident_func__ = property(_get__ident_func__, _set__ident_func__) diff --git a/rq/timeouts.py b/rq/timeouts.py index d26528a..f1e1848 100644 --- a/rq/timeouts.py +++ b/rq/timeouts.py @@ -33,7 +33,7 @@ class death_penalty_after(object): def handle_death_penalty(self, signum, frame): raise JobTimeoutException('Job exceeded maximum timeout ' - 'value (%d seconds).' % self._timeout) + 'value (%d seconds).' % self._timeout) def setup_death_penalty(self): """Sets up an alarm signal and a signal handler that raises diff --git a/rq/utils.py b/rq/utils.py index 44bbe65..8219c9f 100644 --- a/rq/utils.py +++ b/rq/utils.py @@ -16,8 +16,7 @@ def gettermsize(): def ioctl_GWINSZ(fd): try: import fcntl, termios, struct # noqa - cr = struct.unpack('hh', fcntl.ioctl(fd, termios.TIOCGWINSZ, - '1234')) + cr = struct.unpack('hh', fcntl.ioctl(fd, termios.TIOCGWINSZ, '1234')) except: return None return cr @@ -53,7 +52,7 @@ class _Colorizer(object): self.codes["overline"] = esc + "06m" dark_colors = ["black", "darkred", "darkgreen", "brown", "darkblue", - "purple", "teal", "lightgray"] + "purple", "teal", "lightgray"] light_colors = ["darkgray", "red", "green", "yellow", "blue", "fuchsia", "turquoise", "white"] @@ -139,7 +138,7 @@ class ColorizingStreamHandler(logging.StreamHandler): def __init__(self, exclude=None, *args, **kwargs): self.exclude = exclude - if is_python_version((2,6)): + if is_python_version((2, 6)): logging.StreamHandler.__init__(self, *args, **kwargs) else: super(ColorizingStreamHandler, self).__init__(*args, **kwargs) diff --git a/rq/worker.py b/rq/worker.py index 3ba4250..64f6501 100644 --- a/rq/worker.py +++ b/rq/worker.py @@ -43,9 +43,9 @@ def iterable(x): def compact(l): return [x for x in l if x is not None] -_signames = dict((getattr(signal, signame), signame) \ - for signame in dir(signal) \ - if signame.startswith('SIG') and '_' not in signame) +_signames = dict((getattr(signal, signame), signame) + for signame in dir(signal) + if signame.startswith('SIG') and '_' not in signame) def signal_name(signum): @@ -68,8 +68,8 @@ class Worker(object): if connection is None: connection = get_current_connection() reported_working = connection.smembers(cls.redis_workers_keys) - workers = [cls.find_by_key(as_text(key), connection) for key in - reported_working] + workers = [cls.find_by_key(as_text(key), connection) + for key in reported_working] return compact(workers) @classmethod @@ -95,13 +95,12 @@ class Worker(object): worker._state = connection.hget(worker.key, 'state') or '?' if queues: worker.queues = [Queue(queue, connection=connection) - for queue in queues.split(',')] + for queue in queues.split(',')] return worker - def __init__(self, queues, name=None, - default_result_ttl=DEFAULT_RESULT_TTL, connection=None, - exc_handler=None, default_worker_ttl=DEFAULT_WORKER_TTL): # noqa + default_result_ttl=DEFAULT_RESULT_TTL, connection=None, + exc_handler=None, default_worker_ttl=DEFAULT_WORKER_TTL): # noqa if connection is None: connection = get_current_connection() self.connection = connection @@ -193,9 +192,8 @@ class Worker(object): self.log.debug('Registering birth of worker %s' % (self.name,)) if self.connection.exists(self.key) and \ not self.connection.hexists(self.key, 'death'): - raise ValueError( - 'There exists an active worker named \'%s\' ' - 'already.' % (self.name,)) + raise ValueError('There exists an active worker named \'%s\' ' + 'already.' % (self.name,)) key = self.key now = time.time() queues = ','.join(self.queue_names()) @@ -304,8 +302,8 @@ class Worker(object): qnames = self.queue_names() self.procline('Listening on %s' % ','.join(qnames)) self.log.info('') - self.log.info('*** Listening on %s...' % \ - green(', '.join(qnames))) + self.log.info('*** Listening on %s...' % + green(', '.join(qnames))) timeout = None if burst else max(1, self.default_worker_ttl - 60) try: result = self.dequeue_job_and_maintain_ttl(timeout) @@ -324,7 +322,7 @@ class Worker(object): # Use the public setter here, to immediately update Redis job.status = Status.STARTED self.log.info('%s: %s (%s)' % (green(queue.name), - blue(job.description), job.id)) + blue(job.description), job.id)) self.connection.expire(self.key, (job.timeout or 180) + 60) self.fork_and_perform_job(job) @@ -336,19 +334,17 @@ class Worker(object): self.register_death() return did_perform_work - def dequeue_job_and_maintain_ttl(self, timeout): while True: try: return Queue.dequeue_any(self.queues, timeout, - connection=self.connection) + connection=self.connection) except DequeueTimeout: pass self.log.debug('Sending heartbeat to prevent worker timeout.') self.connection.expire(self.key, self.default_worker_ttl) - def fork_and_perform_job(self, job): """Spawns a work horse to perform the actual work and passes it a job. The worker will wait for the work horse and make sure it executes @@ -443,12 +439,10 @@ class Worker(object): return True - def handle_exception(self, job, *exc_info): """Walks the exception handler stack to delegate exception handling.""" - exc_string = ''.join( - traceback.format_exception_only(*exc_info[:2]) + - traceback.format_exception(*exc_info)) + exc_string = ''.join(traceback.format_exception_only(*exc_info[:2]) + + traceback.format_exception(*exc_info)) self.log.error(exc_string) for handler in reversed(self._exc_handlers): From eb5bb6329c573bd4df65554ddac96543fa11370f Mon Sep 17 00:00:00 2001 From: Vincent Driessen Date: Mon, 2 Sep 2013 23:12:01 +0200 Subject: [PATCH 40/40] PEP8ify. --- rq/scripts/__init__.py | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/rq/scripts/__init__.py b/rq/scripts/__init__.py index 6fc89e4..575a8cb 100644 --- a/rq/scripts/__init__.py +++ b/rq/scripts/__init__.py @@ -6,27 +6,28 @@ from rq import use_connection def add_standard_arguments(parser): parser.add_argument('--config', '-c', default=None, - help='Module containing RQ settings.') + help='Module containing RQ settings.') parser.add_argument('--url', '-u', default=None, - help='URL describing Redis connection details. ' - 'Overrides other connection arguments if supplied.') + help='URL describing Redis connection details. ' + 'Overrides other connection arguments if supplied.') parser.add_argument('--host', '-H', default=None, - help='The Redis hostname (default: localhost)') + help='The Redis hostname (default: localhost)') parser.add_argument('--port', '-p', default=None, - help='The Redis portnumber (default: 6379)') + help='The Redis portnumber (default: 6379)') parser.add_argument('--db', '-d', type=int, default=None, - help='The Redis database (default: 0)') + help='The Redis database (default: 0)') parser.add_argument('--password', '-a', default=None, - help='The Redis password (default: None)') + help='The Redis password (default: None)') parser.add_argument('--socket', '-s', default=None, - help='The Redis Unix socket') + help='The Redis Unix socket') + def read_config_file(module): """Reads all UPPERCASE variables defined in the given module file.""" settings = importlib.import_module(module) return dict([(k, v) - for k, v in settings.__dict__.items() - if k.upper() == k]) + for k, v in settings.__dict__.items() + if k.upper() == k]) def setup_default_arguments(args, settings): @@ -35,9 +36,9 @@ def setup_default_arguments(args, settings): args.url = settings.get('REDIS_URL') if (args.host or args.port or args.socket or args.db or args.password): - warn('Host, port, db, password options for Redis will not be \ - supported in future versions of RQ. \ - Please use `REDIS_URL` or `--url` instead.', DeprecationWarning) + warn('Host, port, db, password options for Redis will not be ' + 'supported in future versions of RQ. ' + 'Please use `REDIS_URL` or `--url` instead.', DeprecationWarning) if args.host is None: args.host = settings.get('REDIS_HOST', 'localhost') @@ -63,5 +64,5 @@ def setup_redis(args): redis_conn = redis.StrictRedis.from_url(args.url) else: redis_conn = redis.StrictRedis(host=args.host, port=args.port, db=args.db, - password=args.password, unix_socket_path=args.socket) + password=args.password, unix_socket_path=args.socket) use_connection(redis_conn)