Coverage for /usr/lib/python3/dist-packages/scipy/_lib/_uarray/_backend.py: 37%

163 statements  

« prev     ^ index     » next       coverage.py v7.9.1, created at 2025-06-14 15:55 +0200

1import typing 

2import types 

3import inspect 

4import functools 

5from . import _uarray 

6import copyreg 

7import pickle 

8import contextlib 

9 

10from ._uarray import ( # type: ignore 

11 BackendNotImplementedError, 

12 _Function, 

13 _SkipBackendContext, 

14 _SetBackendContext, 

15 _BackendState, 

16) 

17 

18__all__ = [ 

19 "set_backend", 

20 "set_global_backend", 

21 "skip_backend", 

22 "register_backend", 

23 "determine_backend", 

24 "determine_backend_multi", 

25 "clear_backends", 

26 "create_multimethod", 

27 "generate_multimethod", 

28 "_Function", 

29 "BackendNotImplementedError", 

30 "Dispatchable", 

31 "wrap_single_convertor", 

32 "wrap_single_convertor_instance", 

33 "all_of_type", 

34 "mark_as", 

35 "set_state", 

36 "get_state", 

37 "reset_state", 

38 "_BackendState", 

39 "_SkipBackendContext", 

40 "_SetBackendContext", 

41] 

42 

43ArgumentExtractorType = typing.Callable[..., typing.Tuple["Dispatchable", ...]] 

44ArgumentReplacerType = typing.Callable[ 

45 [typing.Tuple, typing.Dict, typing.Tuple], typing.Tuple[typing.Tuple, typing.Dict] 

46] 

47 

48def unpickle_function(mod_name, qname, self_): 

49 import importlib 

50 

51 try: 

52 module = importlib.import_module(mod_name) 

53 qname = qname.split(".") 

54 func = module 

55 for q in qname: 

56 func = getattr(func, q) 

57 

58 if self_ is not None: 

59 func = types.MethodType(func, self_) 

60 

61 return func 

62 except (ImportError, AttributeError) as e: 

63 from pickle import UnpicklingError 

64 

65 raise UnpicklingError from e 

66 

67 

68def pickle_function(func): 

69 mod_name = getattr(func, "__module__", None) 

70 qname = getattr(func, "__qualname__", None) 

71 self_ = getattr(func, "__self__", None) 

72 

73 try: 

74 test = unpickle_function(mod_name, qname, self_) 

75 except pickle.UnpicklingError: 

76 test = None 

77 

78 if test is not func: 

79 raise pickle.PicklingError( 

80 f"Can't pickle {func}: it's not the same object as {test}" 

81 ) 

82 

83 return unpickle_function, (mod_name, qname, self_) 

84 

85 

86def pickle_state(state): 

87 return _uarray._BackendState._unpickle, state._pickle() 

88 

89 

90def pickle_set_backend_context(ctx): 

91 return _SetBackendContext, ctx._pickle() 

92 

93 

94def pickle_skip_backend_context(ctx): 

95 return _SkipBackendContext, ctx._pickle() 

96 

97 

98copyreg.pickle(_Function, pickle_function) 

99copyreg.pickle(_uarray._BackendState, pickle_state) 

100copyreg.pickle(_SetBackendContext, pickle_set_backend_context) 

101copyreg.pickle(_SkipBackendContext, pickle_skip_backend_context) 

102 

103 

104def get_state(): 

105 """ 

106 Returns an opaque object containing the current state of all the backends. 

107 

108 Can be used for synchronization between threads/processes. 

109 

110 See Also 

111 -------- 

112 set_state 

113 Sets the state returned by this function. 

114 """ 

115 return _uarray.get_state() 

116 

117 

118@contextlib.contextmanager 

119def reset_state(): 

120 """ 

121 Returns a context manager that resets all state once exited. 

122 

123 See Also 

124 -------- 

125 set_state 

126 Context manager that sets the backend state. 

127 get_state 

128 Gets a state to be set by this context manager. 

129 """ 

130 with set_state(get_state()): 

131 yield 

132 

133 

134@contextlib.contextmanager 

135def set_state(state): 

136 """ 

137 A context manager that sets the state of the backends to one returned by :obj:`get_state`. 

138 

139 See Also 

140 -------- 

141 get_state 

142 Gets a state to be set by this context manager. 

143 """ 

144 old_state = get_state() 

145 _uarray.set_state(state) 

146 try: 

147 yield 

148 finally: 

149 _uarray.set_state(old_state, True) 

150 

151 

152def create_multimethod(*args, **kwargs): 

153 """ 

154 Creates a decorator for generating multimethods. 

155 

156 This function creates a decorator that can be used with an argument 

157 extractor in order to generate a multimethod. Other than for the 

158 argument extractor, all arguments are passed on to 

159 :obj:`generate_multimethod`. 

160 

161 See Also 

162 -------- 

163 generate_multimethod 

164 Generates a multimethod. 

165 """ 

166 

167 def wrapper(a): 

168 return generate_multimethod(a, *args, **kwargs) 

169 

170 return wrapper 

171 

172 

173def generate_multimethod( 

174 argument_extractor: ArgumentExtractorType, 

175 argument_replacer: ArgumentReplacerType, 

176 domain: str, 

177 default: typing.Optional[typing.Callable] = None, 

178): 

179 """ 

180 Generates a multimethod. 

181 

182 Parameters 

183 ---------- 

184 argument_extractor : ArgumentExtractorType 

185 A callable which extracts the dispatchable arguments. Extracted arguments 

186 should be marked by the :obj:`Dispatchable` class. It has the same signature 

187 as the desired multimethod. 

188 argument_replacer : ArgumentReplacerType 

189 A callable with the signature (args, kwargs, dispatchables), which should also 

190 return an (args, kwargs) pair with the dispatchables replaced inside the args/kwargs. 

191 domain : str 

192 A string value indicating the domain of this multimethod. 

193 default: Optional[Callable], optional 

194 The default implementation of this multimethod, where ``None`` (the default) specifies 

195 there is no default implementation. 

196 

197 Examples 

198 -------- 

199 In this example, ``a`` is to be dispatched over, so we return it, while marking it as an ``int``. 

200 The trailing comma is needed because the args have to be returned as an iterable. 

201 

202 >>> def override_me(a, b): 

203 ... return Dispatchable(a, int), 

204 

205 Next, we define the argument replacer that replaces the dispatchables inside args/kwargs with the 

206 supplied ones. 

207 

208 >>> def override_replacer(args, kwargs, dispatchables): 

209 ... return (dispatchables[0], args[1]), {} 

210 

211 Next, we define the multimethod. 

212 

213 >>> overridden_me = generate_multimethod( 

214 ... override_me, override_replacer, "ua_examples" 

215 ... ) 

216 

217 Notice that there's no default implementation, unless you supply one. 

218 

219 >>> overridden_me(1, "a") 

220 Traceback (most recent call last): 

221 ... 

222 uarray.BackendNotImplementedError: ... 

223 

224 >>> overridden_me2 = generate_multimethod( 

225 ... override_me, override_replacer, "ua_examples", default=lambda x, y: (x, y) 

226 ... ) 

227 >>> overridden_me2(1, "a") 

228 (1, 'a') 

229 

230 See Also 

231 -------- 

232 uarray 

233 See the module documentation for how to override the method by creating backends. 

234 """ 

235 kw_defaults, arg_defaults, opts = get_defaults(argument_extractor) 

236 ua_func = _Function( 

237 argument_extractor, 

238 argument_replacer, 

239 domain, 

240 arg_defaults, 

241 kw_defaults, 

242 default, 

243 ) 

244 

245 return functools.update_wrapper(ua_func, argument_extractor) 

246 

247 

248def set_backend(backend, coerce=False, only=False): 

249 """ 

250 A context manager that sets the preferred backend. 

251 

252 Parameters 

253 ---------- 

254 backend 

255 The backend to set. 

256 coerce 

257 Whether or not to coerce to a specific backend's types. Implies ``only``. 

258 only 

259 Whether or not this should be the last backend to try. 

260 

261 See Also 

262 -------- 

263 skip_backend: A context manager that allows skipping of backends. 

264 set_global_backend: Set a single, global backend for a domain. 

265 """ 

266 try: 

267 return backend.__ua_cache__["set", coerce, only] 

268 except AttributeError: 

269 backend.__ua_cache__ = {} 

270 except KeyError: 

271 pass 

272 

273 ctx = _SetBackendContext(backend, coerce, only) 

274 backend.__ua_cache__["set", coerce, only] = ctx 

275 return ctx 

276 

277 

278def skip_backend(backend): 

279 """ 

280 A context manager that allows one to skip a given backend from processing 

281 entirely. This allows one to use another backend's code in a library that 

282 is also a consumer of the same backend. 

283 

284 Parameters 

285 ---------- 

286 backend 

287 The backend to skip. 

288 

289 See Also 

290 -------- 

291 set_backend: A context manager that allows setting of backends. 

292 set_global_backend: Set a single, global backend for a domain. 

293 """ 

294 try: 

295 return backend.__ua_cache__["skip"] 

296 except AttributeError: 

297 backend.__ua_cache__ = {} 

298 except KeyError: 

299 pass 

300 

301 ctx = _SkipBackendContext(backend) 

302 backend.__ua_cache__["skip"] = ctx 

303 return ctx 

304 

305 

306def get_defaults(f): 

307 sig = inspect.signature(f) 

308 kw_defaults = {} 

309 arg_defaults = [] 

310 opts = set() 

311 for k, v in sig.parameters.items(): 

312 if v.default is not inspect.Parameter.empty: 

313 kw_defaults[k] = v.default 

314 if v.kind in ( 

315 inspect.Parameter.POSITIONAL_ONLY, 

316 inspect.Parameter.POSITIONAL_OR_KEYWORD, 

317 ): 

318 arg_defaults.append(v.default) 

319 opts.add(k) 

320 

321 return kw_defaults, tuple(arg_defaults), opts 

322 

323 

324def set_global_backend(backend, coerce=False, only=False, *, try_last=False): 

325 """ 

326 This utility method replaces the default backend for permanent use. It 

327 will be tried in the list of backends automatically, unless the 

328 ``only`` flag is set on a backend. This will be the first tried 

329 backend outside the :obj:`set_backend` context manager. 

330 

331 Note that this method is not thread-safe. 

332 

333 .. warning:: 

334 We caution library authors against using this function in 

335 their code. We do *not* support this use-case. This function 

336 is meant to be used only by users themselves, or by a reference 

337 implementation, if one exists. 

338 

339 Parameters 

340 ---------- 

341 backend 

342 The backend to register. 

343 coerce : bool 

344 Whether to coerce input types when trying this backend. 

345 only : bool 

346 If ``True``, no more backends will be tried if this fails. 

347 Implied by ``coerce=True``. 

348 try_last : bool 

349 If ``True``, the global backend is tried after registered backends. 

350 

351 See Also 

352 -------- 

353 set_backend: A context manager that allows setting of backends. 

354 skip_backend: A context manager that allows skipping of backends. 

355 """ 

356 _uarray.set_global_backend(backend, coerce, only, try_last) 

357 

358 

359def register_backend(backend): 

360 """ 

361 This utility method sets registers backend for permanent use. It 

362 will be tried in the list of backends automatically, unless the 

363 ``only`` flag is set on a backend. 

364 

365 Note that this method is not thread-safe. 

366 

367 Parameters 

368 ---------- 

369 backend 

370 The backend to register. 

371 """ 

372 _uarray.register_backend(backend) 

373 

374 

375def clear_backends(domain, registered=True, globals=False): 

376 """ 

377 This utility method clears registered backends. 

378 

379 .. warning:: 

380 We caution library authors against using this function in 

381 their code. We do *not* support this use-case. This function 

382 is meant to be used only by users themselves. 

383 

384 .. warning:: 

385 Do NOT use this method inside a multimethod call, or the 

386 program is likely to crash. 

387 

388 Parameters 

389 ---------- 

390 domain : Optional[str] 

391 The domain for which to de-register backends. ``None`` means 

392 de-register for all domains. 

393 registered : bool 

394 Whether or not to clear registered backends. See :obj:`register_backend`. 

395 globals : bool 

396 Whether or not to clear global backends. See :obj:`set_global_backend`. 

397 

398 See Also 

399 -------- 

400 register_backend : Register a backend globally. 

401 set_global_backend : Set a global backend. 

402 """ 

403 _uarray.clear_backends(domain, registered, globals) 

404 

405 

406class Dispatchable: 

407 """ 

408 A utility class which marks an argument with a specific dispatch type. 

409 

410 

411 Attributes 

412 ---------- 

413 value 

414 The value of the Dispatchable. 

415 

416 type 

417 The type of the Dispatchable. 

418 

419 Examples 

420 -------- 

421 >>> x = Dispatchable(1, str) 

422 >>> x 

423 <Dispatchable: type=<class 'str'>, value=1> 

424 

425 See Also 

426 -------- 

427 all_of_type 

428 Marks all unmarked parameters of a function. 

429 

430 mark_as 

431 Allows one to create a utility function to mark as a given type. 

432 """ 

433 

434 def __init__(self, value, dispatch_type, coercible=True): 

435 self.value = value 

436 self.type = dispatch_type 

437 self.coercible = coercible 

438 

439 def __getitem__(self, index): 

440 return (self.type, self.value)[index] 

441 

442 def __str__(self): 

443 return "<{}: type={!r}, value={!r}>".format( 

444 type(self).__name__, self.type, self.value 

445 ) 

446 

447 __repr__ = __str__ 

448 

449 

450def mark_as(dispatch_type): 

451 """ 

452 Creates a utility function to mark something as a specific type. 

453 

454 Examples 

455 -------- 

456 >>> mark_int = mark_as(int) 

457 >>> mark_int(1) 

458 <Dispatchable: type=<class 'int'>, value=1> 

459 """ 

460 return functools.partial(Dispatchable, dispatch_type=dispatch_type) 

461 

462 

463def all_of_type(arg_type): 

464 """ 

465 Marks all unmarked arguments as a given type. 

466 

467 Examples 

468 -------- 

469 >>> @all_of_type(str) 

470 ... def f(a, b): 

471 ... return a, Dispatchable(b, int) 

472 >>> f('a', 1) 

473 (<Dispatchable: type=<class 'str'>, value='a'>, <Dispatchable: type=<class 'int'>, value=1>) 

474 """ 

475 

476 def outer(func): 

477 @functools.wraps(func) 

478 def inner(*args, **kwargs): 

479 extracted_args = func(*args, **kwargs) 

480 return tuple( 

481 Dispatchable(arg, arg_type) 

482 if not isinstance(arg, Dispatchable) 

483 else arg 

484 for arg in extracted_args 

485 ) 

486 

487 return inner 

488 

489 return outer 

490 

491 

492def wrap_single_convertor(convert_single): 

493 """ 

494 Wraps a ``__ua_convert__`` defined for a single element to all elements. 

495 If any of them return ``NotImplemented``, the operation is assumed to be 

496 undefined. 

497 

498 Accepts a signature of (value, type, coerce). 

499 """ 

500 

501 @functools.wraps(convert_single) 

502 def __ua_convert__(dispatchables, coerce): 

503 converted = [] 

504 for d in dispatchables: 

505 c = convert_single(d.value, d.type, coerce and d.coercible) 

506 

507 if c is NotImplemented: 

508 return NotImplemented 

509 

510 converted.append(c) 

511 

512 return converted 

513 

514 return __ua_convert__ 

515 

516 

517def wrap_single_convertor_instance(convert_single): 

518 """ 

519 Wraps a ``__ua_convert__`` defined for a single element to all elements. 

520 If any of them return ``NotImplemented``, the operation is assumed to be 

521 undefined. 

522 

523 Accepts a signature of (value, type, coerce). 

524 """ 

525 

526 @functools.wraps(convert_single) 

527 def __ua_convert__(self, dispatchables, coerce): 

528 converted = [] 

529 for d in dispatchables: 

530 c = convert_single(self, d.value, d.type, coerce and d.coercible) 

531 

532 if c is NotImplemented: 

533 return NotImplemented 

534 

535 converted.append(c) 

536 

537 return converted 

538 

539 return __ua_convert__ 

540 

541 

542def determine_backend(value, dispatch_type, *, domain, only=True, coerce=False): 

543 """Set the backend to the first active backend that supports ``value`` 

544 

545 This is useful for functions that call multimethods without any dispatchable 

546 arguments. You can use :func:`determine_backend` to ensure the same backend 

547 is used everywhere in a block of multimethod calls. 

548 

549 Parameters 

550 ---------- 

551 value 

552 The value being tested 

553 dispatch_type 

554 The dispatch type associated with ``value``, aka 

555 ":ref:`marking <MarkingGlossary>`". 

556 domain: string 

557 The domain to query for backends and set. 

558 coerce: bool 

559 Whether or not to allow coercion to the backend's types. Implies ``only``. 

560 only: bool 

561 Whether or not this should be the last backend to try. 

562 

563 See Also 

564 -------- 

565 set_backend: For when you know which backend to set 

566 

567 Notes 

568 ----- 

569 

570 Support is determined by the ``__ua_convert__`` protocol. Backends not 

571 supporting the type must return ``NotImplemented`` from their 

572 ``__ua_convert__`` if they don't support input of that type. 

573 

574 Examples 

575 -------- 

576 

577 Suppose we have two backends ``BackendA`` and ``BackendB`` each supporting 

578 different types, ``TypeA`` and ``TypeB``. Neither supporting the other type: 

579 

580 >>> with ua.set_backend(ex.BackendA): 

581 ... ex.call_multimethod(ex.TypeB(), ex.TypeB()) 

582 Traceback (most recent call last): 

583 ... 

584 uarray.BackendNotImplementedError: ... 

585 

586 Now consider a multimethod that creates a new object of ``TypeA``, or 

587 ``TypeB`` depending on the active backend. 

588 

589 >>> with ua.set_backend(ex.BackendA), ua.set_backend(ex.BackendB): 

590 ... res = ex.creation_multimethod() 

591 ... ex.call_multimethod(res, ex.TypeA()) 

592 Traceback (most recent call last): 

593 ... 

594 uarray.BackendNotImplementedError: ... 

595 

596 ``res`` is an object of ``TypeB`` because ``BackendB`` is set in the 

597 innermost with statement. So, ``call_multimethod`` fails since the types 

598 don't match. 

599 

600 Instead, we need to first find a backend suitable for all of our objects. 

601 

602 >>> with ua.set_backend(ex.BackendA), ua.set_backend(ex.BackendB): 

603 ... x = ex.TypeA() 

604 ... with ua.determine_backend(x, "mark", domain="ua_examples"): 

605 ... res = ex.creation_multimethod() 

606 ... ex.call_multimethod(res, x) 

607 TypeA 

608 

609 """ 

610 dispatchables = (Dispatchable(value, dispatch_type, coerce),) 

611 backend = _uarray.determine_backend(domain, dispatchables, coerce) 

612 

613 return set_backend(backend, coerce=coerce, only=only) 

614 

615 

616def determine_backend_multi( 

617 dispatchables, *, domain, only=True, coerce=False, **kwargs 

618): 

619 """Set a backend supporting all ``dispatchables`` 

620 

621 This is useful for functions that call multimethods without any dispatchable 

622 arguments. You can use :func:`determine_backend_multi` to ensure the same 

623 backend is used everywhere in a block of multimethod calls involving 

624 multiple arrays. 

625 

626 Parameters 

627 ---------- 

628 dispatchables: Sequence[Union[uarray.Dispatchable, Any]] 

629 The dispatchables that must be supported 

630 domain: string 

631 The domain to query for backends and set. 

632 coerce: bool 

633 Whether or not to allow coercion to the backend's types. Implies ``only``. 

634 only: bool 

635 Whether or not this should be the last backend to try. 

636 dispatch_type: Optional[Any] 

637 The default dispatch type associated with ``dispatchables``, aka 

638 ":ref:`marking <MarkingGlossary>`". 

639 

640 See Also 

641 -------- 

642 determine_backend: For a single dispatch value 

643 set_backend: For when you know which backend to set 

644 

645 Notes 

646 ----- 

647 

648 Support is determined by the ``__ua_convert__`` protocol. Backends not 

649 supporting the type must return ``NotImplemented`` from their 

650 ``__ua_convert__`` if they don't support input of that type. 

651 

652 Examples 

653 -------- 

654 

655 :func:`determine_backend` allows the backend to be set from a single 

656 object. :func:`determine_backend_multi` allows multiple objects to be 

657 checked simultaneously for support in the backend. Suppose we have a 

658 ``BackendAB`` which supports ``TypeA`` and ``TypeB`` in the same call, 

659 and a ``BackendBC`` that doesn't support ``TypeA``. 

660 

661 >>> with ua.set_backend(ex.BackendAB), ua.set_backend(ex.BackendBC): 

662 ... a, b = ex.TypeA(), ex.TypeB() 

663 ... with ua.determine_backend_multi( 

664 ... [ua.Dispatchable(a, "mark"), ua.Dispatchable(b, "mark")], 

665 ... domain="ua_examples" 

666 ... ): 

667 ... res = ex.creation_multimethod() 

668 ... ex.call_multimethod(res, a, b) 

669 TypeA 

670 

671 This won't call ``BackendBC`` because it doesn't support ``TypeA``. 

672 

673 We can also use leave out the ``ua.Dispatchable`` if we specify the 

674 default ``dispatch_type`` for the ``dispatchables`` argument. 

675 

676 >>> with ua.set_backend(ex.BackendAB), ua.set_backend(ex.BackendBC): 

677 ... a, b = ex.TypeA(), ex.TypeB() 

678 ... with ua.determine_backend_multi( 

679 ... [a, b], dispatch_type="mark", domain="ua_examples" 

680 ... ): 

681 ... res = ex.creation_multimethod() 

682 ... ex.call_multimethod(res, a, b) 

683 TypeA 

684 

685 """ 

686 if "dispatch_type" in kwargs: 

687 disp_type = kwargs.pop("dispatch_type") 

688 dispatchables = tuple( 

689 d if isinstance(d, Dispatchable) else Dispatchable(d, disp_type) 

690 for d in dispatchables 

691 ) 

692 else: 

693 dispatchables = tuple(dispatchables) 

694 if not all(isinstance(d, Dispatchable) for d in dispatchables): 

695 raise TypeError("dispatchables must be instances of uarray.Dispatchable") 

696 

697 if len(kwargs) != 0: 

698 raise TypeError(f"Received unexpected keyword arguments: {kwargs}") 

699 

700 backend = _uarray.determine_backend(domain, dispatchables, coerce) 

701 

702 return set_backend(backend, coerce=coerce, only=only)