canvas.py 48 KB

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