Coverage for utilities/deprecation.py: 41%

111 statements  

« prev     ^ index     » next       coverage.py v7.11.0, created at 2025-11-16 22:49 +1300

1""" 

2Deprecation Utilities 

3===================== 

4 

5Define deprecation management utilities for the Colour library. 

6""" 

7 

8from __future__ import annotations 

9 

10import sys 

11import typing 

12from dataclasses import dataclass 

13from importlib import import_module 

14from operator import attrgetter 

15 

16if typing.TYPE_CHECKING: 

17 from colour.hints import Any, ModuleType 

18 

19from colour.utilities import MixinDataclassIterable, attest, optional, usage_warning 

20 

21__author__ = "Colour Developers" 

22__copyright__ = "Copyright 2013 Colour Developers" 

23__license__ = "BSD-3-Clause - https://opensource.org/licenses/BSD-3-Clause" 

24__maintainer__ = "Colour Developers" 

25__email__ = "colour-developers@colour-science.org" 

26__status__ = "Production" 

27 

28__all__ = [ 

29 "ObjectRenamed", 

30 "ObjectRemoved", 

31 "ObjectFutureRename", 

32 "ObjectFutureRemove", 

33 "ObjectFutureAccessChange", 

34 "ObjectFutureAccessRemove", 

35 "ModuleAPI", 

36 "ArgumentRenamed", 

37 "ArgumentRemoved", 

38 "ArgumentFutureRename", 

39 "ArgumentFutureRemove", 

40 "get_attribute", 

41 "build_API_changes", 

42 "handle_arguments_deprecation", 

43] 

44 

45 

46@dataclass(frozen=True) 

47class ObjectRenamed(MixinDataclassIterable): 

48 """ 

49 Represent an object that has been renamed in the API. 

50 

51 Parameters 

52 ---------- 

53 name 

54 Object name that has been changed. 

55 new_name 

56 New object name. 

57 """ 

58 

59 name: str 

60 new_name: str 

61 

62 def __str__(self) -> str: 

63 """ 

64 Return a formatted string representation of the class. 

65 

66 Returns 

67 ------- 

68 :class:`str` 

69 Formatted string representation. 

70 """ 

71 

72 return f'"{self.name}" object has been renamed to "{self.new_name}".' 

73 

74 

75@dataclass(frozen=True) 

76class ObjectRemoved(MixinDataclassIterable): 

77 """ 

78 Represent an object that has been removed from the API. 

79 

80 Parameters 

81 ---------- 

82 name 

83 Object name that has been removed. 

84 """ 

85 

86 name: str 

87 

88 def __str__(self) -> str: 

89 """ 

90 Return a formatted string representation of the class. 

91 

92 Returns 

93 ------- 

94 :class:`str` 

95 Formatted string representation. 

96 """ 

97 

98 return f'"{self.name}" object has been removed from the API.' 

99 

100 

101@dataclass(frozen=True) 

102class ObjectFutureRename(MixinDataclassIterable): 

103 """ 

104 Represent an object that will be renamed in a future release. 

105 

106 Parameters 

107 ---------- 

108 name 

109 Object name that will change in a future release. 

110 new_name 

111 New object name. 

112 """ 

113 

114 name: str 

115 new_name: str 

116 

117 def __str__(self) -> str: 

118 """ 

119 Return a formatted string representation of the class. 

120 

121 Returns 

122 ------- 

123 :class:`str` 

124 Formatted string representation. 

125 """ 

126 

127 return ( 

128 f'"{self.name}" object is deprecated and will be renamed to ' 

129 f'"{self.new_name}" in a future release.' 

130 ) 

131 

132 

133@dataclass(frozen=True) 

134class ObjectFutureRemove(MixinDataclassIterable): 

135 """ 

136 Represent an object that will be removed in a future release. 

137 

138 Parameters 

139 ---------- 

140 name 

141 Object name that will be removed in a future release. 

142 """ 

143 

144 name: str 

145 

146 def __str__(self) -> str: 

147 """ 

148 Return a formatted string representation of the class. 

149 

150 Returns 

151 ------- 

152 :class:`str` 

153 Formatted string representation. 

154 """ 

155 

156 return ( 

157 f'"{self.name}" object is deprecated and will be removed in ' 

158 f"a future release." 

159 ) 

160 

161 

162@dataclass(frozen=True) 

163class ObjectFutureAccessChange(MixinDataclassIterable): 

164 """ 

165 Represent an object whose access pattern will change in a future 

166 release. 

167 

168 Parameters 

169 ---------- 

170 access 

171 Object access that will change in a future release. 

172 new_access 

173 New object access pattern. 

174 """ 

175 

176 access: str 

177 new_access: str 

178 

179 def __str__(self) -> str: 

180 """ 

181 Return a formatted string representation of the class. 

182 

183 Returns 

184 ------- 

185 :class:`str` 

186 Formatted string representation. 

187 """ 

188 

189 return ( 

190 f'"{self.access}" object access is deprecated and will change ' 

191 f'to "{self.new_access}" in a future release.' 

192 ) 

193 

194 

195@dataclass(frozen=True) 

196class ObjectFutureAccessRemove(MixinDataclassIterable): 

197 """ 

198 Represent an object whose access will be removed in a future release. 

199 be removed in a future release. 

200 

201 Parameters 

202 ---------- 

203 name 

204 Object name whose access will be removed in a future release. 

205 """ 

206 

207 name: str 

208 

209 def __str__(self) -> str: 

210 """ 

211 Return a formatted string representation of the class. 

212 

213 Returns 

214 ------- 

215 :class:`str` 

216 Formatted string representation. 

217 """ 

218 

219 return f'"{self.name}" object access will be removed in a future release.' 

220 

221 

222@dataclass(frozen=True) 

223class ArgumentRenamed(MixinDataclassIterable): 

224 """ 

225 Represent an argument that has been renamed in the API. 

226 

227 Parameters 

228 ---------- 

229 name 

230 Argument name that has been changed. 

231 new_name 

232 New argument name. 

233 """ 

234 

235 name: str 

236 new_name: str 

237 

238 def __str__(self) -> str: 

239 """ 

240 Return a formatted string representation of the class. 

241 

242 Returns 

243 ------- 

244 :class:`str` 

245 Formatted string representation. 

246 """ 

247 

248 return f'"{self.name}" argument has been renamed to "{self.new_name}".' 

249 

250 

251@dataclass(frozen=True) 

252class ArgumentRemoved(MixinDataclassIterable): 

253 """ 

254 Represent an argument that has been removed from the API. 

255 

256 Parameters 

257 ---------- 

258 name 

259 Argument name that has been removed. 

260 """ 

261 

262 name: str 

263 

264 def __str__(self) -> str: 

265 """ 

266 Return a formatted string representation of the class. 

267 

268 Returns 

269 ------- 

270 :class:`str` 

271 Formatted string representation. 

272 """ 

273 

274 return f'"{self.name}" argument has been removed from the API.' 

275 

276 

277@dataclass(frozen=True) 

278class ArgumentFutureRename(MixinDataclassIterable): 

279 """ 

280 Represent an argument that will be renamed in a future release. 

281 change in a future release. 

282 

283 Parameters 

284 ---------- 

285 name 

286 Argument name that will change in a future release. 

287 new_name 

288 New argument name. 

289 """ 

290 

291 name: str 

292 new_name: str 

293 

294 def __str__(self) -> str: 

295 """ 

296 Return a formatted string representation of the class. 

297 

298 Returns 

299 ------- 

300 :class:`str` 

301 Formatted string representation. 

302 """ 

303 

304 return ( 

305 f'"{self.name}" argument is deprecated and will be renamed to ' 

306 f'"{self.new_name}" in a future release.' 

307 ) 

308 

309 

310@dataclass(frozen=True) 

311class ArgumentFutureRemove(MixinDataclassIterable): 

312 """ 

313 Represent an argument that will be removed in a future release. 

314 

315 Parameters 

316 ---------- 

317 name 

318 Argument name that will be removed in a future release. 

319 """ 

320 

321 name: str 

322 

323 def __str__(self) -> str: 

324 """ 

325 Return a formatted string representation of the class. 

326 

327 Returns 

328 ------- 

329 :class:`str` 

330 Formatted string representation. 

331 """ 

332 

333 return ( 

334 f'"{self.name}" argument is deprecated and will be removed in ' 

335 f"a future release." 

336 ) 

337 

338 

339class ModuleAPI: 

340 """ 

341 Define a class enabling customisation of module attribute access with 

342 built-in deprecation management functionality. 

343 

344 Parameters 

345 ---------- 

346 module 

347 Module for which to customise attribute access behaviour. 

348 

349 Methods 

350 ------- 

351 - :meth:`~colour.utilities.ModuleAPI.__init__` 

352 - :meth:`~colour.utilities.ModuleAPI.__getattr__` 

353 - :meth:`~colour.utilities.ModuleAPI.__dir__` 

354 

355 Examples 

356 -------- 

357 >>> import sys 

358 >>> sys.modules["colour"] = ModuleAPI(sys.modules["colour"]) 

359 ... # doctest: +SKIP 

360 """ 

361 

362 def __init__(self, module: ModuleType, changes: dict | None = None) -> None: 

363 self._module = module 

364 self._changes = optional(changes, {}) 

365 

366 def __getattr__(self, attribute: str) -> Any: 

367 """ 

368 Return the specified attribute value while handling deprecation. 

369 

370 Parameters 

371 ---------- 

372 attribute 

373 Attribute name. 

374 

375 Returns 

376 ------- 

377 :class:`object` 

378 Attribute value. 

379 

380 Raises 

381 ------ 

382 AttributeError 

383 If the attribute is not defined. 

384 """ 

385 

386 change = self._changes.get(attribute) 

387 

388 if change is not None: 

389 if not isinstance(change, ObjectRemoved): 

390 usage_warning(str(change)) 

391 

392 return ( 

393 getattr(self._module, attribute) 

394 if isinstance(change, ObjectFutureRemove) 

395 else get_attribute(change.values[1]) 

396 ) 

397 

398 raise AttributeError(str(change)) 

399 

400 return getattr(self._module, attribute) 

401 

402 def __dir__(self) -> list: 

403 """ 

404 Return the list of names in the module local scope filtered according 

405 to the changes. 

406 

407 Returns 

408 ------- 

409 :class:`list` 

410 Filtered list of names in the module local scope. 

411 """ 

412 

413 return [ 

414 attribute 

415 for attribute in dir(self._module) 

416 if attribute not in self._changes 

417 ] 

418 

419 

420def get_attribute(attribute: str) -> Any: 

421 """ 

422 Retrieve the value of the specified attribute from its namespace. 

423 

424 Parameters 

425 ---------- 

426 attribute 

427 Attribute to retrieve, ``attribute`` must have a namespace 

428 module, e.g., *colour.models.oetf_inverse_BT2020*. 

429 

430 Returns 

431 ------- 

432 :class:`object` 

433 Retrieved attribute value. 

434 

435 Examples 

436 -------- 

437 >>> get_attribute("colour.models.oetf_inverse_BT2020") # doctest: +ELLIPSIS 

438 <function oetf_inverse_BT2020 at 0x...> 

439 """ 

440 

441 attest("." in attribute, '"{0}" attribute has no namespace!') 

442 

443 module_name, attribute = attribute.rsplit(".", 1) 

444 

445 module = optional(sys.modules.get(module_name), import_module(module_name)) 

446 

447 attest( 

448 module is not None, 

449 f'"{module_name}" module does not exists or cannot be imported!', 

450 ) 

451 

452 return attrgetter(attribute)(module) 

453 

454 

455def build_API_changes(changes: dict) -> dict: 

456 """ 

457 Build effective API changes from specified API changes mapping. 

458 

459 Parameters 

460 ---------- 

461 changes 

462 Dictionary of desired API changes. 

463 

464 Returns 

465 ------- 

466 :class:`dict` 

467 API changes 

468 

469 Examples 

470 -------- 

471 >>> from pprint import pprint 

472 >>> changes = { 

473 ... "ObjectRenamed": [ 

474 ... [ 

475 ... "module.object_1_name", 

476 ... "module.object_1_new_name", 

477 ... ] 

478 ... ], 

479 ... "ObjectFutureRename": [ 

480 ... [ 

481 ... "module.object_2_name", 

482 ... "module.object_2_new_name", 

483 ... ] 

484 ... ], 

485 ... "ObjectFutureAccessChange": [ 

486 ... [ 

487 ... "module.object_3_access", 

488 ... "module.sub_module.object_3_new_access", 

489 ... ] 

490 ... ], 

491 ... "ObjectRemoved": ["module.object_4_name"], 

492 ... "ObjectFutureRemove": ["module.object_5_name"], 

493 ... "ObjectFutureAccessRemove": ["module.object_6_access"], 

494 ... } 

495 >>> pprint(build_API_changes(changes)) # doctest: +SKIP 

496 {'object_1_name': ObjectRenamed(name='module.object_1_name', \ 

497new_name='module.object_1_new_name'), 

498 'object_2_name': ObjectFutureRename(name='module.object_2_name', \ 

499new_name='module.object_2_new_name'), 

500 'object_3_access': ObjectFutureAccessChange(\ 

501access='module.object_3_access', \ 

502new_access='module.sub_module.object_3_new_access'), 

503 'object_4_name': ObjectRemoved(name='module.object_4_name'), 

504 'object_5_name': ObjectFutureRemove(name='module.object_5_name'), 

505 'object_6_access': ObjectFutureAccessRemove(\ 

506name='module.object_6_access')} 

507 """ 

508 

509 for rename_type in ( 

510 ObjectRenamed, 

511 ObjectFutureRename, 

512 ObjectFutureAccessChange, 

513 ArgumentRenamed, 

514 ArgumentFutureRename, 

515 ): 

516 for change in changes.pop(rename_type.__name__, []): 

517 changes[change[0].split(".")[-1]] = rename_type(*change) 

518 

519 for remove_type in ( 

520 ObjectRemoved, 

521 ObjectFutureRemove, 

522 ObjectFutureAccessRemove, 

523 ArgumentRemoved, 

524 ArgumentFutureRemove, 

525 ): 

526 for change in changes.pop(remove_type.__name__, []): 

527 changes[change.split(".")[-1]] = remove_type(change) 

528 

529 return changes 

530 

531 

532def handle_arguments_deprecation(changes: dict, **kwargs: Any) -> dict: 

533 """ 

534 Handle argument deprecation according to the specified API changes 

535 mapping. 

536 

537 Parameters 

538 ---------- 

539 changes 

540 Dictionary of specified API changes defining how arguments should 

541 be handled during deprecation. 

542 

543 Other Parameters 

544 ---------------- 

545 kwargs 

546 Keyword arguments to process for deprecation handling. 

547 

548 Returns 

549 ------- 

550 :class:`dict` 

551 Processed keyword arguments with deprecation rules applied. 

552 

553 Examples 

554 -------- 

555 >>> changes = { 

556 ... "ArgumentRenamed": [ 

557 ... [ 

558 ... "argument_1_name", 

559 ... "argument_1_new_name", 

560 ... ] 

561 ... ], 

562 ... "ArgumentFutureRename": [ 

563 ... [ 

564 ... "argument_2_name", 

565 ... "argument_2_new_name", 

566 ... ] 

567 ... ], 

568 ... "ArgumentRemoved": ["argument_3_name"], 

569 ... "ArgumentFutureRemove": ["argument_4_name"], 

570 ... } 

571 >>> handle_arguments_deprecation( 

572 ... changes, 

573 ... argument_1_name=True, 

574 ... argument_2_name=True, 

575 ... argument_4_name=True, 

576 ... ) 

577 ... # doctest: +SKIP 

578 {'argument_4_name': True, 'argument_1_new_name': True, \ 

579'argument_2_new_name': True} 

580 """ 

581 

582 changes = build_API_changes(changes) 

583 

584 for kwarg in kwargs.copy(): 

585 change = changes.get(kwarg) 

586 

587 if change is None: 

588 continue 

589 

590 if not isinstance(change, ArgumentRemoved): 

591 usage_warning(str(change)) 

592 

593 if isinstance(change, ArgumentFutureRemove): 

594 continue 

595 kwargs[change.values[1]] = kwargs.pop(kwarg) 

596 else: 

597 kwargs.pop(kwarg) 

598 usage_warning(str(change)) 

599 

600 return kwargs