canvas.py 50 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461
  1. # -*- coding: utf-8 -*-
  2. """Composing task work-flows.
  3. .. seealso:
  4. You should import these from :mod:`celery` and not this module.
  5. """
  6. from __future__ import absolute_import, unicode_literals
  7. import sys
  8. from collections import MutableSequence, deque
  9. from copy import deepcopy
  10. from functools import partial as _partial, reduce
  11. from operator import itemgetter
  12. from itertools import chain as _chain
  13. from kombu.utils.functional import fxrange, reprcall
  14. from kombu.utils.objects import cached_property
  15. from kombu.utils.uuid import uuid
  16. from vine import barrier
  17. from celery._state import current_app
  18. from celery.five import python_2_unicode_compatible
  19. from celery.local import try_import
  20. from celery.result import GroupResult
  21. from celery.utils import abstract
  22. from celery.utils.functional import (
  23. maybe_list, is_list, _regen, regen, chunks as _chunks,
  24. )
  25. from celery.utils.text import truncate
  26. __all__ = [
  27. 'Signature', 'chain', 'xmap', 'xstarmap', 'chunks',
  28. 'group', 'chord', 'signature', 'maybe_signature',
  29. ]
  30. PY3 = sys.version_info[0] == 3
  31. # json in Python 2.7 borks if dict contains byte keys.
  32. JSON_NEEDS_UNICODE_KEYS = PY3 and not try_import('simplejson')
  33. def _shorten_names(task_name, s):
  34. # type: (str, str) -> str
  35. """Remove repeating module names from string.
  36. Arguments:
  37. task_name (str): Task name (full path including module),
  38. to use as the basis for removing module names.
  39. s (str): The string we want to work on.
  40. Example:
  41. >>> _shorten_names(
  42. ... 'x.tasks.add',
  43. ... 'x.tasks.add(2, 2) | x.tasks.add(4) | x.tasks.mul(8)',
  44. ... )
  45. 'x.tasks.add(2, 2) | add(4) | mul(8)'
  46. """
  47. # This is used by repr(), to remove repeating module names.
  48. # extract the module part of the task name
  49. module = str(task_name).rpartition('.')[0] + '.'
  50. # find the first occurance of the module name in the string.
  51. index = s.find(module)
  52. if index >= 0:
  53. s = ''.join([
  54. # leave the first occurance of the module name untouched.
  55. s[:index + len(module)],
  56. # strip seen module name from the rest of the string.
  57. s[index + len(module):].replace(module, ''),
  58. ])
  59. return s
  60. class _getitem_property(object):
  61. """Attribute -> dict key descriptor.
  62. The target object must support ``__getitem__``,
  63. and optionally ``__setitem__``.
  64. Example:
  65. >>> from collections import defaultdict
  66. >>> class Me(dict):
  67. ... deep = defaultdict(dict)
  68. ...
  69. ... foo = _getitem_property('foo')
  70. ... deep_thing = _getitem_property('deep.thing')
  71. >>> me = Me()
  72. >>> me.foo
  73. None
  74. >>> me.foo = 10
  75. >>> me.foo
  76. 10
  77. >>> me['foo']
  78. 10
  79. >>> me.deep_thing = 42
  80. >>> me.deep_thing
  81. 42
  82. >>> me.deep
  83. defaultdict(<type 'dict'>, {'thing': 42})
  84. """
  85. def __init__(self, keypath, doc=None):
  86. path, _, self.key = keypath.rpartition('.')
  87. self.path = path.split('.') if path else None
  88. self.__doc__ = doc
  89. def _path(self, obj):
  90. return (reduce(lambda d, k: d[k], [obj] + self.path) if self.path
  91. else obj)
  92. def __get__(self, obj, type=None):
  93. if obj is None:
  94. return type
  95. return self._path(obj).get(self.key)
  96. def __set__(self, obj, value):
  97. self._path(obj)[self.key] = value
  98. def maybe_unroll_group(g):
  99. """Unroll group with only one member."""
  100. # Issue #1656
  101. try:
  102. size = len(g.tasks)
  103. except TypeError:
  104. try:
  105. size = g.tasks.__length_hint__()
  106. except (AttributeError, TypeError):
  107. return g
  108. else:
  109. return list(g.tasks)[0] if size == 1 else g
  110. else:
  111. return g.tasks[0] if size == 1 else g
  112. def task_name_from(task):
  113. return getattr(task, 'name', task)
  114. def _upgrade(fields, sig):
  115. """Used by custom signatures in .from_dict, to keep common fields."""
  116. sig.update(chord_size=fields.get('chord_size'))
  117. return sig
  118. def _seq_concat_item(seq, item):
  119. """Return copy of sequence seq with item added.
  120. Returns:
  121. Sequence: if seq is a tuple, the result will be a tuple,
  122. otherwise it depends on the implementation of ``__add__``.
  123. """
  124. return seq + (item,) if isinstance(seq, tuple) else seq + [item]
  125. def _seq_concat_seq(a, b):
  126. """Concatenate two sequences: ``a + b``.
  127. Returns:
  128. Sequence: The return value will depend on the largest sequence
  129. - if b is larger and is a tuple, the return value will be a tuple.
  130. - if a is larger and is a list, the return value will be a list,
  131. """
  132. # find the type of the largest sequence
  133. prefer = type(max([a, b], key=len))
  134. # convert the smallest list to the type of the largest sequence.
  135. if not isinstance(a, prefer):
  136. a = prefer(a)
  137. if not isinstance(b, prefer):
  138. b = prefer(b)
  139. return a + b
  140. @abstract.CallableSignature.register
  141. @python_2_unicode_compatible
  142. class Signature(dict):
  143. """Task Signature.
  144. Class that wraps the arguments and execution options
  145. for a single task invocation.
  146. Used as the parts in a :class:`group` and other constructs,
  147. or to pass tasks around as callbacks while being compatible
  148. with serializers with a strict type subset.
  149. Signatures can also be created from tasks:
  150. - Using the ``.signature()`` method that has the same signature
  151. as ``Task.apply_async``:
  152. .. code-block:: pycon
  153. >>> add.signature(args=(1,), kwargs={'kw': 2}, options={})
  154. - or the ``.s()`` shortcut that works for star arguments:
  155. .. code-block:: pycon
  156. >>> add.s(1, kw=2)
  157. - the ``.s()`` shortcut does not allow you to specify execution options
  158. but there's a chaning `.set` method that returns the signature:
  159. .. code-block:: pycon
  160. >>> add.s(2, 2).set(countdown=10).set(expires=30).delay()
  161. Note:
  162. You should use :func:`~celery.signature` to create new signatures.
  163. The ``Signature`` class is the type returned by that function and
  164. should be used for ``isinstance`` checks for signatures.
  165. See Also:
  166. :ref:`guide-canvas` for the complete guide.
  167. Arguments:
  168. task (Task, str): Either a task class/instance, or the name of a task.
  169. args (Tuple): Positional arguments to apply.
  170. kwargs (Dict): Keyword arguments to apply.
  171. options (Dict): Additional options to :meth:`Task.apply_async`.
  172. Note:
  173. If the first argument is a :class:`dict`, the other
  174. arguments will be ignored and the values in the dict will be used
  175. instead::
  176. >>> s = signature('tasks.add', args=(2, 2))
  177. >>> signature(s)
  178. {'task': 'tasks.add', args=(2, 2), kwargs={}, options={}}
  179. """
  180. TYPES = {}
  181. _app = _type = None
  182. @classmethod
  183. def register_type(cls, subclass, name=None):
  184. cls.TYPES[name or subclass.__name__] = subclass
  185. return subclass
  186. @classmethod
  187. def from_dict(cls, d, app=None):
  188. typ = d.get('subtask_type')
  189. if typ:
  190. target_cls = cls.TYPES[typ]
  191. if target_cls is not cls:
  192. return target_cls.from_dict(d, app=app)
  193. return Signature(d, app=app)
  194. def __init__(self, task=None, args=None, kwargs=None, options=None,
  195. type=None, subtask_type=None, immutable=False,
  196. app=None, **ex):
  197. self._app = app
  198. if isinstance(task, dict):
  199. super(Signature, self).__init__(task) # works like dict(d)
  200. else:
  201. # Also supports using task class/instance instead of string name.
  202. try:
  203. task_name = task.name
  204. except AttributeError:
  205. task_name = task
  206. else:
  207. self._type = task
  208. super(Signature, self).__init__(
  209. task=task_name, args=tuple(args or ()),
  210. kwargs=kwargs or {},
  211. options=dict(options or {}, **ex),
  212. subtask_type=subtask_type,
  213. immutable=immutable,
  214. chord_size=None,
  215. )
  216. def __call__(self, *partial_args, **partial_kwargs):
  217. """Call the task directly (in the current process)."""
  218. args, kwargs, _ = self._merge(partial_args, partial_kwargs, None)
  219. return self.type(*args, **kwargs)
  220. def delay(self, *partial_args, **partial_kwargs):
  221. """Shortcut to :meth:`apply_async` using star arguments."""
  222. return self.apply_async(partial_args, partial_kwargs)
  223. def apply(self, args=(), kwargs={}, **options):
  224. """Call task locally.
  225. Same as :meth:`apply_async` but executed the task inline instead
  226. of sending a task message.
  227. """
  228. # For callbacks: extra args are prepended to the stored args.
  229. args, kwargs, options = self._merge(args, kwargs, options)
  230. return self.type.apply(args, kwargs, **options)
  231. def apply_async(self, args=(), kwargs={}, route_name=None, **options):
  232. """Apply this task asynchronously.
  233. Arguments:
  234. args (Tuple): Partial args to be prepended to the existing args.
  235. kwargs (Dict): Partial kwargs to be merged with existing kwargs.
  236. options (Dict): Partial options to be merged
  237. with existing options.
  238. Returns:
  239. ~@AsyncResult: promise of future evaluation.
  240. See also:
  241. :meth:`~@Task.apply_async` and the :ref:`guide-calling` guide.
  242. """
  243. try:
  244. _apply = self._apply_async
  245. except IndexError: # pragma: no cover
  246. # no tasks for chain, etc to find type
  247. return
  248. # For callbacks: extra args are prepended to the stored args.
  249. if args or kwargs or options:
  250. args, kwargs, options = self._merge(args, kwargs, options)
  251. else:
  252. args, kwargs, options = self.args, self.kwargs, self.options
  253. # pylint: disable=too-many-function-args
  254. # Borks on this, as it's a property
  255. return _apply(args, kwargs, **options)
  256. def _merge(self, args=(), kwargs={}, options={}, force=False):
  257. if self.immutable and not force:
  258. return (self.args, self.kwargs,
  259. dict(self.options, **options) if options else self.options)
  260. return (tuple(args) + tuple(self.args) if args else self.args,
  261. dict(self.kwargs, **kwargs) if kwargs else self.kwargs,
  262. dict(self.options, **options) if options else self.options)
  263. def clone(self, args=(), kwargs={}, **opts):
  264. """Create a copy of this signature.
  265. Arguments:
  266. args (Tuple): Partial args to be prepended to the existing args.
  267. kwargs (Dict): Partial kwargs to be merged with existing kwargs.
  268. options (Dict): Partial options to be merged with
  269. existing options.
  270. """
  271. # need to deepcopy options so origins links etc. is not modified.
  272. if args or kwargs or opts:
  273. args, kwargs, opts = self._merge(args, kwargs, opts)
  274. else:
  275. args, kwargs, opts = self.args, self.kwargs, self.options
  276. s = Signature.from_dict({'task': self.task, 'args': tuple(args),
  277. 'kwargs': kwargs, 'options': deepcopy(opts),
  278. 'subtask_type': self.subtask_type,
  279. 'chord_size': self.chord_size,
  280. 'immutable': self.immutable}, app=self._app)
  281. s._type = self._type
  282. return s
  283. partial = clone
  284. def freeze(self, _id=None, group_id=None, chord=None,
  285. root_id=None, parent_id=None):
  286. """Finalize the signature by adding a concrete task id.
  287. The task won't be called and you shouldn't call the signature
  288. twice after freezing it as that'll result in two task messages
  289. using the same task id.
  290. Returns:
  291. ~@AsyncResult: promise of future evaluation.
  292. """
  293. # pylint: disable=redefined-outer-name
  294. # XXX chord is also a class in outer scope.
  295. opts = self.options
  296. try:
  297. tid = opts['task_id']
  298. except KeyError:
  299. tid = opts['task_id'] = _id or uuid()
  300. if root_id:
  301. opts['root_id'] = root_id
  302. if parent_id:
  303. opts['parent_id'] = parent_id
  304. if 'reply_to' not in opts:
  305. opts['reply_to'] = self.app.oid
  306. if group_id:
  307. opts['group_id'] = group_id
  308. if chord:
  309. opts['chord'] = chord
  310. # pylint: disable=too-many-function-args
  311. # Borks on this, as it's a property.
  312. return self.AsyncResult(tid)
  313. _freeze = freeze
  314. def replace(self, args=None, kwargs=None, options=None):
  315. """Replace the args, kwargs or options set for this signature.
  316. These are only replaced if the argument for the section is
  317. not :const:`None`.
  318. """
  319. s = self.clone()
  320. if args is not None:
  321. s.args = args
  322. if kwargs is not None:
  323. s.kwargs = kwargs
  324. if options is not None:
  325. s.options = options
  326. return s
  327. def set(self, immutable=None, **options):
  328. """Set arbitrary execution options (same as ``.options.update(…)``).
  329. Returns:
  330. Signature: This is a chaining method call
  331. (i.e., it will return ``self``).
  332. """
  333. if immutable is not None:
  334. self.set_immutable(immutable)
  335. self.options.update(options)
  336. return self
  337. def set_immutable(self, immutable):
  338. self.immutable = immutable
  339. def _with_list_option(self, key):
  340. items = self.options.setdefault(key, [])
  341. if not isinstance(items, MutableSequence):
  342. items = self.options[key] = [items]
  343. return items
  344. def append_to_list_option(self, key, value):
  345. items = self._with_list_option(key)
  346. if value not in items:
  347. items.append(value)
  348. return value
  349. def extend_list_option(self, key, value):
  350. items = self._with_list_option(key)
  351. items.extend(maybe_list(value))
  352. def link(self, callback):
  353. """Add callback task to be applied if this task succeeds.
  354. Returns:
  355. Signature: the argument passed, for chaining
  356. or use with :func:`~functools.reduce`.
  357. """
  358. return self.append_to_list_option('link', callback)
  359. def link_error(self, errback):
  360. """Add callback task to be applied on error in task execution.
  361. Returns:
  362. Signature: the argument passed, for chaining
  363. or use with :func:`~functools.reduce`.
  364. """
  365. return self.append_to_list_option('link_error', errback)
  366. def on_error(self, errback):
  367. """Version of :meth:`link_error` that supports chaining.
  368. on_error chains the original signature, not the errback so::
  369. >>> add.s(2, 2).on_error(errback.s()).delay()
  370. calls the ``add`` task, not the ``errback`` task, but the
  371. reverse is true for :meth:`link_error`.
  372. """
  373. self.link_error(errback)
  374. return self
  375. def flatten_links(self):
  376. """Return a recursive list of dependencies.
  377. "unchain" if you will, but with links intact.
  378. """
  379. return list(_chain.from_iterable(_chain(
  380. [[self]],
  381. (link.flatten_links()
  382. for link in maybe_list(self.options.get('link')) or [])
  383. )))
  384. def __or__(self, other):
  385. if isinstance(self, group):
  386. if isinstance(other, group):
  387. # group() | group() -> single group
  388. return group(_chain(self.tasks, other.tasks), app=self.app)
  389. # group() | task -> chord
  390. return chord(self, body=other, app=self._app)
  391. elif isinstance(other, group):
  392. # unroll group with one member
  393. other = maybe_unroll_group(other)
  394. if isinstance(self, chain):
  395. # chain | group() -> chain
  396. sig = self.clone()
  397. sig.tasks.append(other)
  398. return sig
  399. # task | group() -> chain
  400. return chain(self, other, app=self.app)
  401. if not isinstance(self, chain) and isinstance(other, chain):
  402. # task | chain -> chain
  403. return chain(
  404. _seq_concat_seq((self,), other.tasks), app=self._app)
  405. elif isinstance(other, chain):
  406. # chain | chain -> chain
  407. sig = self.clone()
  408. if isinstance(sig.tasks, tuple):
  409. sig.tasks = list(sig.tasks)
  410. sig.tasks.extend(other.tasks)
  411. return sig
  412. elif isinstance(self, chord):
  413. # chord | task -> attach to body
  414. sig = self.clone()
  415. sig.body = sig.body | other
  416. return sig
  417. elif isinstance(other, Signature):
  418. if isinstance(self, chain):
  419. if isinstance(self.tasks[-1], group):
  420. # CHAIN [last item is group] | TASK -> chord
  421. sig = self.clone()
  422. sig.tasks[-1] = chord(
  423. sig.tasks[-1], other, app=self._app)
  424. return sig
  425. elif isinstance(self.tasks[-1], chord):
  426. # CHAIN [last item is chord] -> chain with chord body.
  427. sig = self.clone()
  428. sig.tasks[-1].body = sig.tasks[-1].body | other
  429. return sig
  430. else:
  431. # chain | task -> chain
  432. return chain(
  433. _seq_concat_item(self.tasks, other), app=self._app)
  434. # task | task -> chain
  435. return chain(self, other, app=self._app)
  436. return NotImplemented
  437. def election(self):
  438. type = self.type
  439. app = type.app
  440. tid = self.options.get('task_id') or uuid()
  441. with app.producer_or_acquire(None) as P:
  442. props = type.backend.on_task_call(P, tid)
  443. app.control.election(tid, 'task', self.clone(task_id=tid, **props),
  444. connection=P.connection)
  445. return type.AsyncResult(tid)
  446. def reprcall(self, *args, **kwargs):
  447. args, kwargs, _ = self._merge(args, kwargs, {}, force=True)
  448. return reprcall(self['task'], args, kwargs)
  449. def __deepcopy__(self, memo):
  450. memo[id(self)] = self
  451. return dict(self)
  452. def __invert__(self):
  453. return self.apply_async().get()
  454. def __reduce__(self):
  455. # for serialization, the task type is lazily loaded,
  456. # and not stored in the dict itself.
  457. return signature, (dict(self),)
  458. def __json__(self):
  459. return dict(self)
  460. def __repr__(self):
  461. return self.reprcall()
  462. if JSON_NEEDS_UNICODE_KEYS: # pragma: no cover
  463. def items(self):
  464. for k, v in dict.items(self):
  465. yield k.decode() if isinstance(k, bytes) else k, v
  466. @property
  467. def name(self):
  468. # for duck typing compatibility with Task.name
  469. return self.task
  470. @cached_property
  471. def type(self):
  472. return self._type or self.app.tasks[self['task']]
  473. @cached_property
  474. def app(self):
  475. return self._app or current_app
  476. @cached_property
  477. def AsyncResult(self):
  478. try:
  479. return self.type.AsyncResult
  480. except KeyError: # task not registered
  481. return self.app.AsyncResult
  482. @cached_property
  483. def _apply_async(self):
  484. try:
  485. return self.type.apply_async
  486. except KeyError:
  487. return _partial(self.app.send_task, self['task'])
  488. id = _getitem_property('options.task_id', 'Task UUID')
  489. parent_id = _getitem_property('options.parent_id', 'Task parent UUID.')
  490. root_id = _getitem_property('options.root_id', 'Task root UUID.')
  491. task = _getitem_property('task', 'Name of task.')
  492. args = _getitem_property('args', 'Positional arguments to task.')
  493. kwargs = _getitem_property('kwargs', 'Keyword arguments to task.')
  494. options = _getitem_property('options', 'Task execution options.')
  495. subtask_type = _getitem_property('subtask_type', 'Type of signature')
  496. chord_size = _getitem_property(
  497. 'chord_size', 'Size of chord (if applicable)')
  498. immutable = _getitem_property(
  499. 'immutable', 'Flag set if no longer accepts new arguments')
  500. @Signature.register_type
  501. @python_2_unicode_compatible
  502. class chain(Signature):
  503. """Chain tasks together.
  504. Each tasks follows one another,
  505. by being applied as a callback of the previous task.
  506. Note:
  507. If called with only one argument, then that argument must
  508. be an iterable of tasks to chain: this allows us
  509. to use generator expressions.
  510. Example:
  511. This is effectively :math:`((2 + 2) + 4)`:
  512. .. code-block:: pycon
  513. >>> res = chain(add.s(2, 2), add.s(4))()
  514. >>> res.get()
  515. 8
  516. Calling a chain will return the result of the last task in the chain.
  517. You can get to the other tasks by following the ``result.parent``'s:
  518. .. code-block:: pycon
  519. >>> res.parent.get()
  520. 4
  521. Using a generator expression:
  522. .. code-block:: pycon
  523. >>> lazy_chain = chain(add.s(i) for i in range(10))
  524. >>> res = lazy_chain(3)
  525. Arguments:
  526. *tasks (Signature): List of task signatures to chain.
  527. If only one argument is passed and that argument is
  528. an iterable, then that'll be used as the list of signatures
  529. to chain instead. This means that you can use a generator
  530. expression.
  531. Returns:
  532. ~celery.chain: A lazy signature that can be called to apply the first
  533. task in the chain. When that task succeeed the next task in the
  534. chain is applied, and so on.
  535. """
  536. tasks = _getitem_property('kwargs.tasks', 'Tasks in chain.')
  537. @classmethod
  538. def from_dict(cls, d, app=None):
  539. tasks = d['kwargs']['tasks']
  540. if tasks:
  541. if isinstance(tasks, tuple): # aaaargh
  542. tasks = d['kwargs']['tasks'] = list(tasks)
  543. # First task must be signature object to get app
  544. tasks[0] = maybe_signature(tasks[0], app=app)
  545. return _upgrade(d, chain(tasks, app=app, **d['options']))
  546. def __init__(self, *tasks, **options):
  547. tasks = (regen(tasks[0]) if len(tasks) == 1 and is_list(tasks[0])
  548. else tasks)
  549. Signature.__init__(
  550. self, 'celery.chain', (), {'tasks': tasks}, **options
  551. )
  552. self._use_link = options.pop('use_link', None)
  553. self.subtask_type = 'chain'
  554. self._frozen = None
  555. def __call__(self, *args, **kwargs):
  556. if self.tasks:
  557. return self.apply_async(args, kwargs)
  558. def clone(self, *args, **kwargs):
  559. to_signature = maybe_signature
  560. s = Signature.clone(self, *args, **kwargs)
  561. s.kwargs['tasks'] = [
  562. to_signature(sig, app=self._app, clone=True)
  563. for sig in s.kwargs['tasks']
  564. ]
  565. return s
  566. def apply_async(self, args=(), kwargs={}, **options):
  567. # python is best at unpacking kwargs, so .run is here to do that.
  568. app = self.app
  569. if app.conf.task_always_eager:
  570. return self.apply(args, kwargs, **options)
  571. return self.run(args, kwargs, app=app, **(
  572. dict(self.options, **options) if options else self.options))
  573. def run(self, args=(), kwargs={}, group_id=None, chord=None,
  574. task_id=None, link=None, link_error=None, publisher=None,
  575. producer=None, root_id=None, parent_id=None, app=None, **options):
  576. # pylint: disable=redefined-outer-name
  577. # XXX chord is also a class in outer scope.
  578. app = app or self.app
  579. use_link = self._use_link
  580. if use_link is None and app.conf.task_protocol == 1:
  581. use_link = True
  582. args = (tuple(args) + tuple(self.args)
  583. if args and not self.immutable else self.args)
  584. if self._frozen:
  585. tasks, results = self._frozen
  586. else:
  587. tasks, results = self.prepare_steps(
  588. args, self.tasks, root_id, parent_id, link_error, app,
  589. task_id, group_id, chord,
  590. )
  591. if results:
  592. if link:
  593. tasks[0].extend_list_option('link', link)
  594. first_task = tasks.pop()
  595. # chain option may already be set, resulting in
  596. # "multiple values for keyword argument 'chain'" error.
  597. # Issue #3379.
  598. options['chain'] = tasks if not use_link else None
  599. first_task.apply_async(**options)
  600. return results[0]
  601. def freeze(self, _id=None, group_id=None, chord=None,
  602. root_id=None, parent_id=None):
  603. # pylint: disable=redefined-outer-name
  604. # XXX chord is also a class in outer scope.
  605. _, results = self._frozen = self.prepare_steps(
  606. self.args, self.tasks, root_id, parent_id, None,
  607. self.app, _id, group_id, chord, clone=False,
  608. )
  609. return results[0]
  610. def prepare_steps(self, args, tasks,
  611. root_id=None, parent_id=None, link_error=None, app=None,
  612. last_task_id=None, group_id=None, chord_body=None,
  613. clone=True, from_dict=Signature.from_dict):
  614. app = app or self.app
  615. # use chain message field for protocol 2 and later.
  616. # this avoids pickle blowing the stack on the recursion
  617. # required by linking task together in a tree structure.
  618. # (why is pickle using recursion? or better yet why cannot python
  619. # do tail call optimization making recursion actually useful?)
  620. use_link = self._use_link
  621. if use_link is None and app.conf.task_protocol == 1:
  622. use_link = True
  623. steps = deque(tasks)
  624. steps_pop = steps.pop
  625. steps_extend = steps.extend
  626. prev_task = None
  627. prev_res = None
  628. tasks, results = [], []
  629. i = 0
  630. while steps:
  631. task = steps_pop()
  632. is_first_task, is_last_task = not steps, not i
  633. if not isinstance(task, abstract.CallableSignature):
  634. task = from_dict(task, app=app)
  635. if isinstance(task, group):
  636. task = maybe_unroll_group(task)
  637. # first task gets partial args from chain
  638. if clone:
  639. task = task.clone(args) if is_first_task else task.clone()
  640. elif is_first_task:
  641. task.args = tuple(args) + tuple(task.args)
  642. if isinstance(task, chain):
  643. # splice the chain
  644. steps_extend(task.tasks)
  645. continue
  646. if isinstance(task, group) and prev_task:
  647. # automatically upgrade group(...) | s to chord(group, s)
  648. # for chords we freeze by pretending it's a normal
  649. # signature instead of a group.
  650. tasks.pop()
  651. results.pop()
  652. task = chord(
  653. task, body=prev_task,
  654. task_id=prev_res.task_id, root_id=root_id, app=app,
  655. )
  656. if is_last_task:
  657. # chain(task_id=id) means task id is set for the last task
  658. # in the chain. If the chord is part of a chord/group
  659. # then that chord/group must synchronize based on the
  660. # last task in the chain, so we only set the group_id and
  661. # chord callback for the last task.
  662. res = task.freeze(
  663. last_task_id,
  664. root_id=root_id, group_id=group_id, chord=chord_body,
  665. )
  666. else:
  667. res = task.freeze(root_id=root_id)
  668. i += 1
  669. if prev_task:
  670. if use_link:
  671. # link previous task to this task.
  672. task.link(prev_task)
  673. if prev_res and not prev_res.parent:
  674. prev_res.parent = res
  675. if link_error:
  676. for errback in maybe_list(link_error):
  677. task.link_error(errback)
  678. tasks.append(task)
  679. results.append(res)
  680. prev_task, prev_res = task, res
  681. if isinstance(task, chord):
  682. # If the task is a chord, and the body is a chain
  683. # the chain has already been prepared, and res is
  684. # set to the last task in the callback chain.
  685. # We need to change that so that it points to the
  686. # group result object.
  687. node = res
  688. while node.parent:
  689. node = node.parent
  690. prev_res = node
  691. return tasks, results
  692. def apply(self, args=(), kwargs={}, **options):
  693. last, fargs = None, args
  694. for task in self.tasks:
  695. res = task.clone(fargs).apply(
  696. last and (last.get(),), **dict(self.options, **options))
  697. res.parent, last, fargs = last, res, None
  698. return last
  699. @property
  700. def app(self):
  701. app = self._app
  702. if app is None:
  703. try:
  704. app = self.tasks[0]._app
  705. except LookupError:
  706. pass
  707. return app or current_app
  708. def __repr__(self):
  709. if not self.tasks:
  710. return '<{0}@{1:#x}: empty>'.format(
  711. type(self).__name__, id(self))
  712. return _shorten_names(
  713. self.tasks[0]['task'],
  714. ' | '.join(repr(t) for t in self.tasks))
  715. class _basemap(Signature):
  716. _task_name = None
  717. _unpack_args = itemgetter('task', 'it')
  718. @classmethod
  719. def from_dict(cls, d, app=None):
  720. return _upgrade(
  721. d, cls(*cls._unpack_args(d['kwargs']), app=app, **d['options']),
  722. )
  723. def __init__(self, task, it, **options):
  724. Signature.__init__(
  725. self, self._task_name, (),
  726. {'task': task, 'it': regen(it)}, immutable=True, **options
  727. )
  728. def apply_async(self, args=(), kwargs={}, **opts):
  729. # need to evaluate generators
  730. task, it = self._unpack_args(self.kwargs)
  731. return self.type.apply_async(
  732. (), {'task': task, 'it': list(it)},
  733. route_name=task_name_from(self.kwargs.get('task')), **opts
  734. )
  735. @Signature.register_type
  736. @python_2_unicode_compatible
  737. class xmap(_basemap):
  738. """Map operation for tasks.
  739. Note:
  740. Tasks executed sequentially in process, this is not a
  741. parallel operation like :class:`group`.
  742. """
  743. _task_name = 'celery.map'
  744. def __repr__(self):
  745. task, it = self._unpack_args(self.kwargs)
  746. return '[{0}(x) for x in {1}]'.format(
  747. task.task, truncate(repr(it), 100))
  748. @Signature.register_type
  749. @python_2_unicode_compatible
  750. class xstarmap(_basemap):
  751. """Map operation for tasks, using star arguments."""
  752. _task_name = 'celery.starmap'
  753. def __repr__(self):
  754. task, it = self._unpack_args(self.kwargs)
  755. return '[{0}(*x) for x in {1}]'.format(
  756. task.task, truncate(repr(it), 100))
  757. @Signature.register_type
  758. class chunks(Signature):
  759. """Partition of tasks in n chunks."""
  760. _unpack_args = itemgetter('task', 'it', 'n')
  761. @classmethod
  762. def from_dict(cls, d, app=None):
  763. return _upgrade(
  764. d, chunks(*cls._unpack_args(
  765. d['kwargs']), app=app, **d['options']),
  766. )
  767. def __init__(self, task, it, n, **options):
  768. Signature.__init__(
  769. self, 'celery.chunks', (),
  770. {'task': task, 'it': regen(it), 'n': n},
  771. immutable=True, **options
  772. )
  773. def __call__(self, **options):
  774. return self.apply_async(**options)
  775. def apply_async(self, args=(), kwargs={}, **opts):
  776. return self.group().apply_async(
  777. args, kwargs,
  778. route_name=task_name_from(self.kwargs.get('task')), **opts
  779. )
  780. def group(self):
  781. # need to evaluate generators
  782. task, it, n = self._unpack_args(self.kwargs)
  783. return group((xstarmap(task, part, app=self._app)
  784. for part in _chunks(iter(it), n)),
  785. app=self._app)
  786. @classmethod
  787. def apply_chunks(cls, task, it, n, app=None):
  788. return cls(task, it, n, app=app)()
  789. def _maybe_group(tasks, app):
  790. if isinstance(tasks, dict):
  791. tasks = signature(tasks, app=app)
  792. if isinstance(tasks, (group, chain)):
  793. tasks = tasks.tasks
  794. elif isinstance(tasks, abstract.CallableSignature):
  795. tasks = [tasks]
  796. else:
  797. tasks = [signature(t, app=app) for t in tasks]
  798. return tasks
  799. @Signature.register_type
  800. @python_2_unicode_compatible
  801. class group(Signature):
  802. """Creates a group of tasks to be executed in parallel.
  803. A group is lazy so you must call it to take action and evaluate
  804. the group.
  805. Note:
  806. If only one argument is passed, and that argument is an iterable
  807. then that'll be used as the list of tasks instead: this
  808. allows us to use ``group`` with generator expressions.
  809. Example:
  810. >>> lazy_group = group([add.s(2, 2), add.s(4, 4)])
  811. >>> promise = lazy_group() # <-- evaluate: returns lazy result.
  812. >>> promise.get() # <-- will wait for the task to return
  813. [4, 8]
  814. Arguments:
  815. *tasks (Signature): A list of signatures that this group will call.
  816. If there's only one argument, and that argument is an iterable,
  817. then that'll define the list of signatures instead.
  818. **options (Any): Execution options applied to all tasks
  819. in the group.
  820. Returns:
  821. ~celery.group: signature that when called will then call all of the
  822. tasks in the group (and return a :class:`GroupResult` instance
  823. that can be used to inspect the state of the group).
  824. """
  825. tasks = _getitem_property('kwargs.tasks', 'Tasks in group.')
  826. @classmethod
  827. def from_dict(cls, d, app=None):
  828. return _upgrade(
  829. d, group(d['kwargs']['tasks'], app=app, **d['options']),
  830. )
  831. def __init__(self, *tasks, **options):
  832. if len(tasks) == 1:
  833. tasks = tasks[0]
  834. if isinstance(tasks, group):
  835. tasks = tasks.tasks
  836. if not isinstance(tasks, _regen):
  837. tasks = regen(tasks)
  838. Signature.__init__(
  839. self, 'celery.group', (), {'tasks': tasks}, **options
  840. )
  841. self.subtask_type = 'group'
  842. def __call__(self, *partial_args, **options):
  843. return self.apply_async(partial_args, **options)
  844. def skew(self, start=1.0, stop=None, step=1.0):
  845. it = fxrange(start, stop, step, repeatlast=True)
  846. for task in self.tasks:
  847. task.set(countdown=next(it))
  848. return self
  849. def apply_async(self, args=(), kwargs=None, add_to_parent=True,
  850. producer=None, link=None, link_error=None, **options):
  851. if link is not None:
  852. raise TypeError('Cannot add link to group: use a chord')
  853. if link_error is not None:
  854. raise TypeError(
  855. 'Cannot add link to group: do that on individual tasks')
  856. app = self.app
  857. if app.conf.task_always_eager:
  858. return self.apply(args, kwargs, **options)
  859. if not self.tasks:
  860. return self.freeze()
  861. options, group_id, root_id = self._freeze_gid(options)
  862. tasks = self._prepared(self.tasks, [], group_id, root_id, app)
  863. p = barrier()
  864. results = list(self._apply_tasks(tasks, producer, app, p,
  865. args=args, kwargs=kwargs, **options))
  866. result = self.app.GroupResult(group_id, results, ready_barrier=p)
  867. p.finalize()
  868. # - Special case of group(A.s() | group(B.s(), C.s()))
  869. # That is, group with single item that's a chain but the
  870. # last task in that chain is a group.
  871. #
  872. # We cannot actually support arbitrary GroupResults in chains,
  873. # but this special case we can.
  874. if len(result) == 1 and isinstance(result[0], GroupResult):
  875. result = result[0]
  876. parent_task = app.current_worker_task
  877. if add_to_parent and parent_task:
  878. parent_task.add_trail(result)
  879. return result
  880. def apply(self, args=(), kwargs={}, **options):
  881. app = self.app
  882. if not self.tasks:
  883. return self.freeze() # empty group returns GroupResult
  884. options, group_id, root_id = self._freeze_gid(options)
  885. tasks = self._prepared(self.tasks, [], group_id, root_id, app)
  886. return app.GroupResult(group_id, [
  887. sig.apply(args=args, kwargs=kwargs, **options) for sig, _ in tasks
  888. ])
  889. def set_immutable(self, immutable):
  890. for task in self.tasks:
  891. task.set_immutable(immutable)
  892. def link(self, sig):
  893. # Simply link to first task
  894. sig = sig.clone().set(immutable=True)
  895. return self.tasks[0].link(sig)
  896. def link_error(self, sig):
  897. sig = sig.clone().set(immutable=True)
  898. return self.tasks[0].link_error(sig)
  899. def _prepared(self, tasks, partial_args, group_id, root_id, app,
  900. CallableSignature=abstract.CallableSignature,
  901. from_dict=Signature.from_dict,
  902. isinstance=isinstance, tuple=tuple):
  903. for task in tasks:
  904. if isinstance(task, CallableSignature):
  905. # local sigs are always of type Signature, and we
  906. # clone them to make sure we don't modify the originals.
  907. task = task.clone()
  908. else:
  909. # serialized sigs must be converted to Signature.
  910. task = from_dict(task, app=app)
  911. if isinstance(task, group):
  912. # needs yield_from :(
  913. unroll = task._prepared(
  914. task.tasks, partial_args, group_id, root_id, app,
  915. )
  916. for taskN, resN in unroll:
  917. yield taskN, resN
  918. else:
  919. if partial_args and not task.immutable:
  920. task.args = tuple(partial_args) + tuple(task.args)
  921. yield task, task.freeze(group_id=group_id, root_id=root_id)
  922. def _apply_tasks(self, tasks, producer=None, app=None, p=None,
  923. add_to_parent=None, chord=None,
  924. args=None, kwargs=None, **options):
  925. # pylint: disable=redefined-outer-name
  926. # XXX chord is also a class in outer scope.
  927. app = app or self.app
  928. with app.producer_or_acquire(producer) as producer:
  929. for sig, res in tasks:
  930. sig.apply_async(producer=producer, add_to_parent=False,
  931. chord=sig.options.get('chord') or chord,
  932. args=args, kwargs=kwargs,
  933. **options)
  934. # adding callback to result, such that it will gradually
  935. # fulfill the barrier.
  936. #
  937. # Using barrier.add would use result.then, but we need
  938. # to add the weak argument here to only create a weak
  939. # reference to the object.
  940. if p and not p.cancelled and not p.ready:
  941. p.size += 1
  942. res.then(p, weak=True)
  943. yield res # <-- r.parent, etc set in the frozen result.
  944. def _freeze_gid(self, options):
  945. # remove task_id and use that as the group_id,
  946. # if we don't remove it then every task will have the same id...
  947. options = dict(self.options, **options)
  948. options['group_id'] = group_id = (
  949. options.pop('task_id', uuid()))
  950. return options, group_id, options.get('root_id')
  951. def freeze(self, _id=None, group_id=None, chord=None,
  952. root_id=None, parent_id=None):
  953. # pylint: disable=redefined-outer-name
  954. # XXX chord is also a class in outer scope.
  955. opts = self.options
  956. try:
  957. gid = opts['task_id']
  958. except KeyError:
  959. gid = opts['task_id'] = uuid()
  960. if group_id:
  961. opts['group_id'] = group_id
  962. if chord:
  963. opts['chord'] = chord
  964. root_id = opts.setdefault('root_id', root_id)
  965. parent_id = opts.setdefault('parent_id', parent_id)
  966. new_tasks = []
  967. # Need to unroll subgroups early so that chord gets the
  968. # right result instance for chord_unlock etc.
  969. results = list(self._freeze_unroll(
  970. new_tasks, group_id, chord, root_id, parent_id,
  971. ))
  972. if isinstance(self.tasks, MutableSequence):
  973. self.tasks[:] = new_tasks
  974. else:
  975. self.tasks = new_tasks
  976. return self.app.GroupResult(gid, results)
  977. _freeze = freeze
  978. def _freeze_unroll(self, new_tasks, group_id, chord, root_id, parent_id):
  979. # pylint: disable=redefined-outer-name
  980. # XXX chord is also a class in outer scope.
  981. stack = deque(self.tasks)
  982. while stack:
  983. task = maybe_signature(stack.popleft(), app=self._app).clone()
  984. if isinstance(task, group):
  985. stack.extendleft(task.tasks)
  986. else:
  987. new_tasks.append(task)
  988. yield task.freeze(group_id=group_id,
  989. chord=chord, root_id=root_id,
  990. parent_id=parent_id)
  991. def __iter__(self):
  992. return iter(self.tasks)
  993. def __repr__(self):
  994. if self.tasks:
  995. return _shorten_names(
  996. self.tasks[0]['task'],
  997. 'group({0.tasks!r})'.format(self))
  998. return 'group(<empty>)'
  999. def __len__(self):
  1000. return len(self.tasks)
  1001. @property
  1002. def app(self):
  1003. app = self._app
  1004. if app is None:
  1005. try:
  1006. app = self.tasks[0].app
  1007. except LookupError:
  1008. pass
  1009. return app if app is not None else current_app
  1010. @Signature.register_type
  1011. @python_2_unicode_compatible
  1012. class chord(Signature):
  1013. r"""Barrier synchronization primitive.
  1014. A chord consists of a header and a body.
  1015. The header is a group of tasks that must complete before the callback is
  1016. called. A chord is essentially a callback for a group of tasks.
  1017. The body is applied with the return values of all the header
  1018. tasks as a list.
  1019. Example:
  1020. The chord:
  1021. .. code-block:: pycon
  1022. >>> res = chord([add.s(2, 2), add.s(4, 4)])(sum_task.s())
  1023. is effectively :math:`\Sigma ((2 + 2) + (4 + 4))`:
  1024. .. code-block:: pycon
  1025. >>> res.get()
  1026. 12
  1027. """
  1028. @classmethod
  1029. def from_dict(cls, d, app=None):
  1030. args, d['kwargs'] = cls._unpack_args(**d['kwargs'])
  1031. return _upgrade(d, cls(*args, app=app, **d))
  1032. @staticmethod
  1033. def _unpack_args(header=None, body=None, **kwargs):
  1034. # Python signatures are better at extracting keys from dicts
  1035. # than manually popping things off.
  1036. return (header, body), kwargs
  1037. def __init__(self, header, body=None, task='celery.chord',
  1038. args=(), kwargs={}, app=None, **options):
  1039. Signature.__init__(
  1040. self, task, args,
  1041. dict(kwargs=kwargs, header=_maybe_group(header, app),
  1042. body=maybe_signature(body, app=app)), app=app, **options
  1043. )
  1044. self.subtask_type = 'chord'
  1045. def __call__(self, body=None, **options):
  1046. return self.apply_async((), {'body': body} if body else {}, **options)
  1047. def freeze(self, _id=None, group_id=None, chord=None,
  1048. root_id=None, parent_id=None):
  1049. # pylint: disable=redefined-outer-name
  1050. # XXX chord is also a class in outer scope.
  1051. if not isinstance(self.tasks, group):
  1052. self.tasks = group(self.tasks, app=self.app)
  1053. header_result = self.tasks.freeze(
  1054. parent_id=parent_id, root_id=root_id, chord=self.body)
  1055. bodyres = self.body.freeze(_id, root_id=root_id)
  1056. # we need to link the body result back to the group result,
  1057. # but the body may actually be a chain,
  1058. # so find the first result without a parent
  1059. node = bodyres
  1060. seen = set()
  1061. while node:
  1062. if node.id in seen:
  1063. raise RuntimeError('Recursive result parents')
  1064. seen.add(node.id)
  1065. if node.parent is None:
  1066. node.parent = header_result
  1067. break
  1068. node = node.parent
  1069. self.id = self.tasks.id
  1070. return bodyres
  1071. def apply_async(self, args=(), kwargs={}, task_id=None,
  1072. producer=None, publisher=None, connection=None,
  1073. router=None, result_cls=None, **options):
  1074. kwargs = kwargs or {}
  1075. args = (tuple(args) + tuple(self.args)
  1076. if args and not self.immutable else self.args)
  1077. body = kwargs.pop('body', None) or self.kwargs['body']
  1078. kwargs = dict(self.kwargs['kwargs'], **kwargs)
  1079. body = body.clone(**options)
  1080. app = self._get_app(body)
  1081. tasks = (self.tasks.clone() if isinstance(self.tasks, group)
  1082. else group(self.tasks, app=app))
  1083. if app.conf.task_always_eager:
  1084. return self.apply(args, kwargs,
  1085. body=body, task_id=task_id, **options)
  1086. return self.run(tasks, body, args, task_id=task_id, **options)
  1087. def apply(self, args=(), kwargs={}, propagate=True, body=None, **options):
  1088. body = self.body if body is None else body
  1089. tasks = (self.tasks.clone() if isinstance(self.tasks, group)
  1090. else group(self.tasks, app=self.app))
  1091. return body.apply(
  1092. args=(tasks.apply(args, kwargs).get(propagate=propagate),),
  1093. )
  1094. def _traverse_tasks(self, tasks, value=None):
  1095. stack = deque(list(tasks))
  1096. while stack:
  1097. task = stack.popleft()
  1098. if isinstance(task, group):
  1099. stack.extend(task.tasks)
  1100. else:
  1101. yield task if value is None else value
  1102. def __length_hint__(self):
  1103. return sum(self._traverse_tasks(self.tasks, 1))
  1104. def run(self, header, body, partial_args, app=None, interval=None,
  1105. countdown=1, max_retries=None, eager=False,
  1106. task_id=None, **options):
  1107. app = app or self._get_app(body)
  1108. group_id = header.options.get('task_id') or uuid()
  1109. root_id = body.options.get('root_id')
  1110. body.chord_size = self.__length_hint__()
  1111. options = dict(self.options, **options) if options else self.options
  1112. if options:
  1113. options.pop('task_id', None)
  1114. body.options.update(options)
  1115. results = header.freeze(
  1116. group_id=group_id, chord=body, root_id=root_id).results
  1117. bodyres = body.freeze(task_id, root_id=root_id)
  1118. parent = app.backend.apply_chord(
  1119. header, partial_args, group_id, body,
  1120. interval=interval, countdown=countdown,
  1121. options=options, max_retries=max_retries,
  1122. result=results)
  1123. bodyres.parent = parent
  1124. return bodyres
  1125. def clone(self, *args, **kwargs):
  1126. s = Signature.clone(self, *args, **kwargs)
  1127. # need to make copy of body
  1128. try:
  1129. s.kwargs['body'] = maybe_signature(s.kwargs['body'], clone=True)
  1130. except (AttributeError, KeyError):
  1131. pass
  1132. return s
  1133. def link(self, callback):
  1134. self.body.link(callback)
  1135. return callback
  1136. def link_error(self, errback):
  1137. self.body.link_error(errback)
  1138. return errback
  1139. def set_immutable(self, immutable):
  1140. # changes mutability of header only, not callback.
  1141. for task in self.tasks:
  1142. task.set_immutable(immutable)
  1143. def __repr__(self):
  1144. if self.body:
  1145. if isinstance(self.body, chain):
  1146. return _shorten_names(
  1147. self.body.tasks[0]['task'],
  1148. '({0} | {1!r})'.format(
  1149. self.body.tasks[0].reprcall(self.tasks),
  1150. chain(self.body.tasks[1:], app=self._app),
  1151. ),
  1152. )
  1153. return _shorten_names(
  1154. self.body['task'], self.body.reprcall(self.tasks))
  1155. return '<chord without body: {0.tasks!r}>'.format(self)
  1156. @cached_property
  1157. def app(self):
  1158. return self._get_app(self.body)
  1159. def _get_app(self, body=None):
  1160. app = self._app
  1161. if app is None:
  1162. try:
  1163. tasks = self.tasks.tasks # is a group
  1164. except AttributeError:
  1165. tasks = self.tasks
  1166. app = tasks[0]._app
  1167. if app is None and body is not None:
  1168. app = body._app
  1169. return app if app is not None else current_app
  1170. tasks = _getitem_property('kwargs.header', 'Tasks in chord header.')
  1171. body = _getitem_property('kwargs.body', 'Body task of chord.')
  1172. def signature(varies, *args, **kwargs):
  1173. """Create new signature.
  1174. - if the first argument is a signature already then it's cloned.
  1175. - if the first argument is a dict, then a Signature version is returned.
  1176. Returns:
  1177. Signature: The resulting signature.
  1178. """
  1179. app = kwargs.get('app')
  1180. if isinstance(varies, dict):
  1181. if isinstance(varies, abstract.CallableSignature):
  1182. return varies.clone()
  1183. return Signature.from_dict(varies, app=app)
  1184. return Signature(varies, *args, **kwargs)
  1185. subtask = signature # XXX compat
  1186. def maybe_signature(d, app=None, clone=False):
  1187. """Ensure obj is a signature, or None.
  1188. Arguments:
  1189. d (Optional[Union[abstract.CallableSignature, Mapping]]):
  1190. Signature or dict-serialized signature.
  1191. app (celery.Celery):
  1192. App to bind signature to.
  1193. clone (bool):
  1194. If d' is already a signature, the signature
  1195. will be cloned when this flag is enabled.
  1196. Returns:
  1197. Optional[abstract.CallableSignature]
  1198. """
  1199. if d is not None:
  1200. if isinstance(d, abstract.CallableSignature):
  1201. if clone:
  1202. d = d.clone()
  1203. elif isinstance(d, dict):
  1204. d = signature(d)
  1205. if app is not None:
  1206. d._app = app
  1207. return d
  1208. maybe_subtask = maybe_signature # XXX compat