1- # Copyright 2021-2022 MONAI Consortium
1+ # Copyright 2021-2026 MONAI Consortium
22# Licensed under the Apache License, Version 2.0 (the "License");
33# you may not use this file except in compliance with the License.
44# You may obtain a copy of the License at
1010# limitations under the License.
1111
1212import inspect
13+ import re
1314import runpy
1415import sys
1516import warnings
17+ from functools import lru_cache
1618from importlib import import_module
19+ from importlib .metadata import distributions
1720from pathlib import Path
1821from typing import TYPE_CHECKING , Any , Callable , Dict , List , Optional , Tuple , Type , Union
1922
20- import pkg_resources
23+
24+ def _normalize_project_name (name : str ) -> str :
25+ """Normalize project name for lookup (PEP 503).
26+
27+ PEP 503 normalizes by lowercasing and replacing any run of [-_.]
28+ with a single hyphen, so distribution key matching works for names
29+ containing dots or mixed separators.
30+ """
31+ return re .sub (r"[-_.]+" , "-" , name .lower ())
32+
33+
34+ @lru_cache (maxsize = 1 )
35+ def _get_working_set () -> Dict [str , Any ]:
36+ """Build a dict of distribution name -> dist-like object using importlib.metadata.
37+
38+ Cached so we do not rescan on every call. First-discovered distribution
39+ is kept for each normalized name when multiple distributions match.
40+ """
41+ result : Dict [str , Any ] = {}
42+ for d in distributions ():
43+ key = _normalize_project_name (d .name )
44+ if key not in result :
45+ result [key ] = _DistributionAdapter (d )
46+ return result
47+
48+
49+ class _DistributionAdapter :
50+ """Adapter so importlib.metadata Distribution can be used like pkg_resources Distribution."""
51+
52+ def __init__ (self , dist : Any ) -> None :
53+ self ._dist = dist
54+ path = getattr (dist , "path" , getattr (dist , "_path" , None ))
55+ self ._path = Path (path ).resolve () if path else None
56+
57+ @property
58+ def key (self ) -> str :
59+ return _normalize_project_name (self ._dist .name )
60+
61+ @property
62+ def egg_info (self ) -> Optional [Path ]:
63+ return self ._path
64+
65+ @property
66+ def module_path (self ) -> str :
67+ if self ._path :
68+ return str (self ._path .parent )
69+ return ""
70+
71+ def requires (self ) -> List [str ]:
72+ return list (self ._dist .requires or [])
73+
2174
2275if TYPE_CHECKING :
2376 from monai .deploy .core import Application
@@ -295,9 +348,9 @@ def __init__(self, *_args, **kwargs):
295348
296349
297350def is_dist_editable (project_name : str ) -> bool :
298- distributions : Dict = { v . key : v for v in pkg_resources . working_set }
299- dist : Any = distributions .get (project_name )
300- if not hasattr (dist , "egg_info" ):
351+ working_set = _get_working_set ()
352+ dist : Any = working_set .get (_normalize_project_name ( project_name ) )
353+ if not hasattr (dist , "egg_info" ) or dist . egg_info is None :
301354 return False
302355 egg_info = Path (dist .egg_info )
303356 if egg_info .is_dir ():
@@ -314,15 +367,15 @@ def is_dist_editable(project_name: str) -> bool:
314367 try :
315368 if data ["dir_info" ]["editable" ]:
316369 return True
317- except KeyError :
370+ except ( KeyError , TypeError ) :
318371 pass
319372 return False
320373
321374
322375def dist_module_path (project_name : str ) -> str :
323- distributions : Dict = { v . key : v for v in pkg_resources . working_set }
324- dist : Any = distributions .get (project_name )
325- if hasattr (dist , "egg_info" ):
376+ working_set = _get_working_set ()
377+ dist : Any = working_set .get (_normalize_project_name ( project_name ) )
378+ if hasattr (dist , "egg_info" ) and dist . egg_info is not None :
326379 egg_info = Path (dist .egg_info )
327380 if egg_info .is_dir () and egg_info .suffix == ".dist-info" :
328381 if (egg_info / "direct_url.json" ).exists ():
@@ -336,7 +389,7 @@ def dist_module_path(project_name: str) -> str:
336389 file_url = data ["url" ]
337390 if file_url .startswith ("file://" ):
338391 return str (file_url [7 :])
339- except KeyError :
392+ except ( KeyError , TypeError , AttributeError ) :
340393 pass
341394
342395 if hasattr (dist , "module_path" ):
@@ -345,17 +398,14 @@ def dist_module_path(project_name: str) -> str:
345398
346399
347400def is_module_installed (project_name : str ) -> bool :
348- distributions : Dict = {v .key : v for v in pkg_resources .working_set }
349- dist : Any = distributions .get (project_name )
350- if dist :
351- return True
352- else :
353- return False
401+ working_set = _get_working_set ()
402+ dist : Any = working_set .get (_normalize_project_name (project_name ))
403+ return dist is not None
354404
355405
356406def dist_requires (project_name : str ) -> List [str ]:
357- distributions : Dict = { v . key : v for v in pkg_resources . working_set }
358- dist : Any = distributions .get (project_name )
407+ working_set = _get_working_set ()
408+ dist : Any = working_set .get (_normalize_project_name ( project_name ) )
359409 if hasattr (dist , "requires" ):
360410 return [str (req ) for req in dist .requires ()]
361411 return []
0 commit comments