Coverage for utilities/deprecation.py: 41%

1"""

2 Deprecation Utilities

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

4

5 Define deprecation management utilities for the Colour library.

6"""

7

8 from __future__ import annotations

9

10 import sys

11 import typing

12 from dataclasses import dataclass

13 from importlib import import_module

14 from operator import attrgetter

15

16 if typing.TYPE_CHECKING:

17 from colour.hints import Any, ModuleType

18

19 from 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)

47 class 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)

76 class 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)

102 class 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)

134 class 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)

163 class 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)

196 class 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)

223 class 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)

252 class 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)

278 class 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)

311 class 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

339 class 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

420 def 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

455 def 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', \

497 new_name='module.object_1_new_name'),

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

499 new_name='module.object_2_new_name'),

500 'object_3_access': ObjectFutureAccessChange(\

501 access='module.object_3_access', \

502 new_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(\

506 name='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

532 def 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