5
5
6
6
import sys
7
7
import warnings
8
- from argparse import Action
9
8
from functools import wraps
10
9
from types import ModuleType
11
- from typing import Any , Callable
10
+ from typing import TYPE_CHECKING
12
11
13
- from packaging .version import Version , parse
12
+ if TYPE_CHECKING :
13
+ from argparse import Action
14
+ from typing import Any , Callable
15
+
16
+ from packaging .version import Version
14
17
15
18
from . import __version__
16
19
@@ -22,17 +25,52 @@ class DeprecatedError(RuntimeError):
22
25
# inspired by deprecation (https://deprecation.readthedocs.io/en/latest/) and
23
26
# CPython's warnings._deprecated
24
27
class DeprecationHandler :
25
- _version : Version
28
+ _version : str | None
29
+ _version_tuple : tuple [int , ...] | None
30
+ _version_object : Version | None
26
31
27
- def __init__ (self , version : Version | str ):
32
+ def __init__ (self , version : str ):
28
33
"""Factory to create a deprecation handle for the specified version.
29
34
30
35
:param version: The version to compare against when checking deprecation statuses.
31
36
"""
37
+ self ._version = version
38
+ # Try to parse the version string as a simple tuple[int, ...] to avoid
39
+ # packaging.version import and costlier version comparisons.
40
+ self ._version_tuple = self ._get_version_tuple (version )
41
+ self ._version_object = None
42
+
43
+ @staticmethod
44
+ def _get_version_tuple (version : str ) -> tuple [int , ...] | None :
45
+ """Return version as non-empty tuple of ints if possible, else None.
46
+
47
+ :param version: Version string to parse.
48
+ """
32
49
try :
33
- self ._version = parse (version )
34
- except TypeError :
35
- self ._version = parse ("0.0.0.dev0+placeholder" )
50
+ return tuple (int (part ) for part in version .strip ().split ("." )) or None
51
+ except (AttributeError , ValueError ):
52
+ return None
53
+
54
+ def _version_less_than (self , version : str ) -> bool :
55
+ """Test whether own version is less than the given version.
56
+
57
+ :param version: Version string to compare against.
58
+ """
59
+ if self ._version_tuple :
60
+ if version_tuple := self ._get_version_tuple (version ):
61
+ return self ._version_tuple < version_tuple
62
+
63
+ # If self._version or version could not be represented by a simple
64
+ # tuple[int, ...], do a more elaborate version parsing and comparison.
65
+ # Avoid this import otherwise to reduce import time for conda activate.
66
+ from packaging .version import parse
67
+
68
+ if self ._version_object is None :
69
+ try :
70
+ self ._version_object = parse (self ._version )
71
+ except TypeError :
72
+ self ._version_object = parse ("0.0.0.dev0+placeholder" )
73
+ return self ._version_object < parse (version )
36
74
37
75
def __call__ (
38
76
self ,
@@ -281,16 +319,33 @@ def _get_module(self, stack: int) -> tuple[ModuleType, str]:
281
319
:param stack: The stacklevel increment.
282
320
:return: The module and module name.
283
321
"""
284
- import inspect # expensive
285
-
286
322
try :
287
323
frame = sys ._getframe (2 + stack )
288
- module = inspect .getmodule (frame )
289
- if module is not None :
290
- return (module , module .__name__ )
291
324
except IndexError :
292
325
# IndexError: 2 + stack is out of range
293
326
pass
327
+ else :
328
+ # Shortcut finding the module by manually inspecting loaded modules.
329
+ try :
330
+ filename = frame .f_code .co_filename
331
+ except AttributeError :
332
+ # AttributeError: frame.f_code.co_filename is undefined
333
+ pass
334
+ else :
335
+ for module in sys .modules .values ():
336
+ if not isinstance (module , ModuleType ):
337
+ continue
338
+ if not hasattr (module , "__file__" ):
339
+ continue
340
+ if module .__file__ == filename :
341
+ return (module , module .__name__ )
342
+
343
+ # If above failed, do an expensive import and costly getmodule call.
344
+ import inspect
345
+
346
+ module = inspect .getmodule (frame )
347
+ if module is not None :
348
+ return (module , module .__name__ )
294
349
295
350
raise DeprecatedError ("unable to determine the calling module" )
296
351
@@ -309,14 +364,11 @@ def _generate_message(
309
364
:param addendum: Additional messaging. Useful to indicate what to do instead.
310
365
:return: The warning category (if applicable) and the message.
311
366
"""
312
- deprecate_version = parse (deprecate_in )
313
- remove_version = parse (remove_in )
314
-
315
367
category : type [Warning ] | None
316
- if self ._version < deprecate_version :
368
+ if self ._version_less_than ( deprecate_in ) :
317
369
category = PendingDeprecationWarning
318
370
warning = f"is pending deprecation and will be removed in { remove_in } ."
319
- elif self ._version < remove_version :
371
+ elif self ._version_less_than ( remove_in ) :
320
372
category = DeprecationWarning
321
373
warning = f"is deprecated and will be removed in { remove_in } ."
322
374
else :
0 commit comments