modutils.py 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703
  1. # Licensed under the LGPL: https://www.gnu.org/licenses/old-licenses/lgpl-2.1.en.html
  2. # For details: https://github.com/pylint-dev/astroid/blob/main/LICENSE
  3. # Copyright (c) https://github.com/pylint-dev/astroid/blob/main/CONTRIBUTORS.txt
  4. """Python modules manipulation utility functions.
  5. :type PY_SOURCE_EXTS: tuple(str)
  6. :var PY_SOURCE_EXTS: list of possible python source file extension
  7. :type STD_LIB_DIRS: set of str
  8. :var STD_LIB_DIRS: directories where standard modules are located
  9. :type BUILTIN_MODULES: dict
  10. :var BUILTIN_MODULES: dictionary with builtin module names has key
  11. """
  12. from __future__ import annotations
  13. import importlib
  14. import importlib.machinery
  15. import importlib.util
  16. import io
  17. import itertools
  18. import logging
  19. import os
  20. import sys
  21. import sysconfig
  22. import types
  23. import warnings
  24. from collections.abc import Callable, Iterable, Sequence
  25. from contextlib import redirect_stderr, redirect_stdout
  26. from functools import lru_cache
  27. from sys import stdlib_module_names
  28. from astroid.const import IS_JYTHON
  29. from astroid.interpreter._import import spec, util
  30. logger = logging.getLogger(__name__)
  31. if sys.platform.startswith("win"):
  32. PY_SOURCE_EXTS = ("py", "pyw", "pyi")
  33. PY_SOURCE_EXTS_STUBS_FIRST = ("pyi", "pyw", "py")
  34. PY_COMPILED_EXTS = ("dll", "pyd")
  35. else:
  36. PY_SOURCE_EXTS = ("py", "pyi")
  37. PY_SOURCE_EXTS_STUBS_FIRST = ("pyi", "py")
  38. PY_COMPILED_EXTS = ("so",)
  39. # TODO: Adding `platstdlib` is a fix for a workaround in virtualenv. At some point we should
  40. # revisit whether this is still necessary. See https://github.com/pylint-dev/astroid/pull/1323.
  41. STD_LIB_DIRS = {sysconfig.get_path("stdlib"), sysconfig.get_path("platstdlib")}
  42. if os.name == "nt":
  43. STD_LIB_DIRS.add(os.path.join(sys.prefix, "dlls"))
  44. try:
  45. # real_prefix is defined when running inside virtual environments,
  46. # created with the **virtualenv** library.
  47. # Deprecated in virtualenv==16.7.9
  48. # See: https://github.com/pypa/virtualenv/issues/1622
  49. STD_LIB_DIRS.add(os.path.join(sys.real_prefix, "dlls")) # type: ignore[attr-defined]
  50. except AttributeError:
  51. # sys.base_exec_prefix is always defined, but in a virtual environment
  52. # created with the stdlib **venv** module, it points to the original
  53. # installation, if the virtual env is activated.
  54. try:
  55. STD_LIB_DIRS.add(os.path.join(sys.base_exec_prefix, "dlls"))
  56. except AttributeError:
  57. pass
  58. if os.name == "posix":
  59. # Need the real prefix if we're in a virtualenv, otherwise
  60. # the usual one will do.
  61. # Deprecated in virtualenv==16.7.9
  62. # See: https://github.com/pypa/virtualenv/issues/1622
  63. try:
  64. prefix: str = sys.real_prefix # type: ignore[attr-defined]
  65. except AttributeError:
  66. prefix = sys.prefix
  67. def _posix_path(path: str) -> str:
  68. base_python = "python%d.%d" % sys.version_info[:2]
  69. return os.path.join(prefix, path, base_python)
  70. STD_LIB_DIRS.add(_posix_path("lib"))
  71. if sys.maxsize > 2**32:
  72. # This tries to fix a problem with /usr/lib64 builds,
  73. # where systems are running both 32-bit and 64-bit code
  74. # on the same machine, which reflects into the places where
  75. # standard library could be found. More details can be found
  76. # here http://bugs.python.org/issue1294959.
  77. # An easy reproducing case would be
  78. # https://github.com/pylint-dev/pylint/issues/712#issuecomment-163178753
  79. STD_LIB_DIRS.add(_posix_path("lib64"))
  80. EXT_LIB_DIRS = {sysconfig.get_path("purelib"), sysconfig.get_path("platlib")}
  81. BUILTIN_MODULES = dict.fromkeys(sys.builtin_module_names, True)
  82. class NoSourceFile(Exception):
  83. """Exception raised when we are not able to get a python
  84. source file for a precompiled file.
  85. """
  86. def _normalize_path(path: str) -> str:
  87. """Resolve symlinks in path and convert to absolute path.
  88. Note that environment variables and ~ in the path need to be expanded in
  89. advance.
  90. This can be cached by using _cache_normalize_path.
  91. """
  92. return os.path.normcase(os.path.realpath(path))
  93. def _path_from_filename(filename: str, is_jython: bool = IS_JYTHON) -> str:
  94. if not is_jython:
  95. return filename
  96. head, has_pyclass, _ = filename.partition("$py.class")
  97. if has_pyclass:
  98. return head + ".py"
  99. return filename
  100. def _handle_blacklist(
  101. blacklist: Sequence[str], dirnames: list[str], filenames: list[str]
  102. ) -> None:
  103. """Remove files/directories in the black list.
  104. dirnames/filenames are usually from os.walk
  105. """
  106. for norecurs in blacklist:
  107. if norecurs in dirnames:
  108. dirnames.remove(norecurs)
  109. elif norecurs in filenames:
  110. filenames.remove(norecurs)
  111. @lru_cache
  112. def _cache_normalize_path_(path: str) -> str:
  113. return _normalize_path(path)
  114. def _cache_normalize_path(path: str) -> str:
  115. """Normalize path with caching."""
  116. # _module_file calls abspath on every path in sys.path every time it's
  117. # called; on a larger codebase this easily adds up to half a second just
  118. # assembling path components. This cache alleviates that.
  119. if not path: # don't cache result for ''
  120. return _normalize_path(path)
  121. return _cache_normalize_path_(path)
  122. def load_module_from_name(dotted_name: str) -> types.ModuleType:
  123. """Load a Python module from its name.
  124. :type dotted_name: str
  125. :param dotted_name: python name of a module or package
  126. :raise ImportError: if the module or package is not found
  127. :rtype: module
  128. :return: the loaded module
  129. """
  130. try:
  131. return sys.modules[dotted_name]
  132. except KeyError:
  133. pass
  134. # Capture and log anything emitted during import to avoid
  135. # contaminating JSON reports in pylint
  136. with (
  137. redirect_stderr(io.StringIO()) as stderr,
  138. redirect_stdout(io.StringIO()) as stdout,
  139. ):
  140. module = importlib.import_module(dotted_name)
  141. stderr_value = stderr.getvalue()
  142. if stderr_value:
  143. logger.error(
  144. "Captured stderr while importing %s:\n%s", dotted_name, stderr_value
  145. )
  146. stdout_value = stdout.getvalue()
  147. if stdout_value:
  148. logger.info(
  149. "Captured stdout while importing %s:\n%s", dotted_name, stdout_value
  150. )
  151. return module
  152. def load_module_from_modpath(parts: Sequence[str]) -> types.ModuleType:
  153. """Load a python module from its split name.
  154. :param parts:
  155. python name of a module or package split on '.'
  156. :raise ImportError: if the module or package is not found
  157. :return: the loaded module
  158. """
  159. return load_module_from_name(".".join(parts))
  160. def load_module_from_file(filepath: str) -> types.ModuleType:
  161. """Load a Python module from it's path.
  162. :type filepath: str
  163. :param filepath: path to the python module or package
  164. :raise ImportError: if the module or package is not found
  165. :rtype: module
  166. :return: the loaded module
  167. """
  168. modpath = modpath_from_file(filepath)
  169. return load_module_from_modpath(modpath)
  170. def check_modpath_has_init(path: str, mod_path: list[str]) -> bool:
  171. """Check there are some __init__.py all along the way."""
  172. modpath: list[str] = []
  173. for part in mod_path:
  174. modpath.append(part)
  175. path = os.path.join(path, part)
  176. if not _has_init(path):
  177. old_namespace = util.is_namespace(".".join(modpath))
  178. if not old_namespace:
  179. return False
  180. return True
  181. def _is_subpath(path: str, base: str) -> bool:
  182. path = os.path.normcase(os.path.normpath(path))
  183. base = os.path.normcase(os.path.normpath(base))
  184. if not path.startswith(base):
  185. return False
  186. return (len(path) == len(base)) or (path[len(base)] == os.path.sep)
  187. def _get_relative_base_path(filename: str, path_to_check: str) -> list[str] | None:
  188. """Extracts the relative mod path of the file to import from.
  189. Check if a file is within the passed in path and if so, returns the
  190. relative mod path from the one passed in.
  191. If the filename is no in path_to_check, returns None
  192. Note this function will look for both abs and realpath of the file,
  193. this allows to find the relative base path even if the file is a
  194. symlink of a file in the passed in path
  195. Examples:
  196. _get_relative_base_path("/a/b/c/d.py", "/a/b") -> ["c","d"]
  197. _get_relative_base_path("/a/b/c/d.py", "/dev") -> None
  198. """
  199. path_to_check = os.path.normcase(os.path.normpath(path_to_check))
  200. abs_filename = os.path.abspath(filename)
  201. if _is_subpath(abs_filename, path_to_check):
  202. base_path = os.path.splitext(abs_filename)[0]
  203. relative_base_path = base_path[len(path_to_check) :].lstrip(os.path.sep)
  204. return [pkg for pkg in relative_base_path.split(os.sep) if pkg]
  205. real_filename = os.path.realpath(filename)
  206. if _is_subpath(real_filename, path_to_check):
  207. base_path = os.path.splitext(real_filename)[0]
  208. relative_base_path = base_path[len(path_to_check) :].lstrip(os.path.sep)
  209. return [pkg for pkg in relative_base_path.split(os.sep) if pkg]
  210. return None
  211. def modpath_from_file_with_callback(
  212. filename: str,
  213. path: list[str] | None = None,
  214. is_package_cb: Callable[[str, list[str]], bool] | None = None,
  215. ) -> list[str]:
  216. filename = os.path.expanduser(_path_from_filename(filename))
  217. paths_to_check = sys.path.copy()
  218. if path:
  219. paths_to_check = path + paths_to_check
  220. for pathname in itertools.chain(
  221. paths_to_check, map(_cache_normalize_path, paths_to_check)
  222. ):
  223. if not pathname:
  224. continue
  225. modpath = _get_relative_base_path(filename, pathname)
  226. if not modpath:
  227. continue
  228. assert is_package_cb is not None
  229. if is_package_cb(pathname, modpath[:-1]):
  230. return modpath
  231. raise ImportError(
  232. "Unable to find module for {} in {}".format(
  233. filename, ", \n".join(paths_to_check)
  234. )
  235. )
  236. def modpath_from_file(filename: str, path: list[str] | None = None) -> list[str]:
  237. """Get the corresponding split module's name from a filename.
  238. This function will return the name of a module or package split on `.`.
  239. :type filename: str
  240. :param filename: file's path for which we want the module's name
  241. :type Optional[List[str]] path:
  242. Optional list of paths where the module or package should be
  243. searched, additionally to sys.path
  244. :raise ImportError:
  245. if the corresponding module's name has not been found
  246. :rtype: list(str)
  247. :return: the corresponding split module's name
  248. """
  249. return modpath_from_file_with_callback(filename, path, check_modpath_has_init)
  250. def file_from_modpath(
  251. modpath: list[str],
  252. path: Sequence[str] | None = None,
  253. context_file: str | None = None,
  254. ) -> str | None:
  255. return file_info_from_modpath(modpath, path, context_file).location
  256. def file_info_from_modpath(
  257. modpath: list[str],
  258. path: Sequence[str] | None = None,
  259. context_file: str | None = None,
  260. ) -> spec.ModuleSpec:
  261. """Given a mod path (i.e. split module / package name), return the
  262. corresponding file.
  263. Giving priority to source file over precompiled file if it exists.
  264. :param modpath:
  265. split module's name (i.e name of a module or package split
  266. on '.')
  267. (this means explicit relative imports that start with dots have
  268. empty strings in this list!)
  269. :param path:
  270. optional list of path where the module or package should be
  271. searched (use sys.path if nothing or None is given)
  272. :param context_file:
  273. context file to consider, necessary if the identifier has been
  274. introduced using a relative import unresolvable in the actual
  275. context (i.e. modutils)
  276. :raise ImportError: if there is no such module in the directory
  277. :return:
  278. the path to the module's file or None if it's an integrated
  279. builtin module such as 'sys'
  280. """
  281. if context_file is not None:
  282. context: str | None = os.path.dirname(context_file)
  283. else:
  284. context = context_file
  285. if modpath[0] == "xml":
  286. # handle _xmlplus
  287. try:
  288. return _spec_from_modpath(["_xmlplus", *modpath[1:]], path, context)
  289. except ImportError:
  290. return _spec_from_modpath(modpath, path, context)
  291. elif modpath == ["os", "path"]:
  292. # FIXME: currently ignoring search_path...
  293. return spec.ModuleSpec(
  294. name="os.path",
  295. location=os.path.__file__,
  296. type=spec.ModuleType.PY_SOURCE,
  297. )
  298. return _spec_from_modpath(modpath, path, context)
  299. def get_module_part(dotted_name: str, context_file: str | None = None) -> str:
  300. """Given a dotted name return the module part of the name :
  301. >>> get_module_part('astroid.as_string.dump')
  302. 'astroid.as_string'
  303. :param dotted_name: full name of the identifier we are interested in
  304. :param context_file:
  305. context file to consider, necessary if the identifier has been
  306. introduced using a relative import unresolvable in the actual
  307. context (i.e. modutils)
  308. :raise ImportError: if there is no such module in the directory
  309. :return:
  310. the module part of the name or None if we have not been able at
  311. all to import the given name
  312. XXX: deprecated, since it doesn't handle package precedence over module
  313. (see #10066)
  314. """
  315. # os.path trick
  316. if dotted_name.startswith("os.path"):
  317. return "os.path"
  318. parts = dotted_name.split(".")
  319. if context_file is not None:
  320. # first check for builtin module which won't be considered latter
  321. # in that case (path != None)
  322. if parts[0] in BUILTIN_MODULES:
  323. if len(parts) > 2:
  324. raise ImportError(dotted_name)
  325. return parts[0]
  326. # don't use += or insert, we want a new list to be created !
  327. path: list[str] | None = None
  328. starti = 0
  329. if parts[0] == "":
  330. assert (
  331. context_file is not None
  332. ), "explicit relative import, but no context_file?"
  333. path = [] # prevent resolving the import non-relatively
  334. starti = 1
  335. # for all further dots: change context
  336. while starti < len(parts) and parts[starti] == "":
  337. starti += 1
  338. assert (
  339. context_file is not None
  340. ), "explicit relative import, but no context_file?"
  341. context_file = os.path.dirname(context_file)
  342. for i in range(starti, len(parts)):
  343. try:
  344. file_from_modpath(
  345. parts[starti : i + 1], path=path, context_file=context_file
  346. )
  347. except ImportError:
  348. if i < max(1, len(parts) - 2):
  349. raise
  350. return ".".join(parts[:i])
  351. return dotted_name
  352. def get_module_files(
  353. src_directory: str, blacklist: Sequence[str], list_all: bool = False
  354. ) -> list[str]:
  355. """Given a package directory return a list of all available python
  356. module's files in the package and its subpackages.
  357. :param src_directory:
  358. path of the directory corresponding to the package
  359. :param blacklist: iterable
  360. list of files or directories to ignore.
  361. :param list_all:
  362. get files from all paths, including ones without __init__.py
  363. :return:
  364. the list of all available python module's files in the package and
  365. its subpackages
  366. """
  367. files: list[str] = []
  368. for directory, dirnames, filenames in os.walk(src_directory):
  369. if directory in blacklist:
  370. continue
  371. _handle_blacklist(blacklist, dirnames, filenames)
  372. # check for __init__.py
  373. if not list_all and {"__init__.py", "__init__.pyi"}.isdisjoint(filenames):
  374. dirnames[:] = ()
  375. continue
  376. for filename in filenames:
  377. if _is_python_file(filename):
  378. src = os.path.join(directory, filename)
  379. files.append(src)
  380. return files
  381. def get_source_file(
  382. filename: str, include_no_ext: bool = False, prefer_stubs: bool = False
  383. ) -> str:
  384. """Given a python module's file name return the matching source file
  385. name (the filename will be returned identically if it's already an
  386. absolute path to a python source file).
  387. :param filename: python module's file name
  388. :raise NoSourceFile: if no source file exists on the file system
  389. :return: the absolute path of the source file if it exists
  390. """
  391. filename = os.path.abspath(_path_from_filename(filename))
  392. base, orig_ext = os.path.splitext(filename)
  393. orig_ext = orig_ext.lstrip(".")
  394. if orig_ext not in PY_SOURCE_EXTS and os.path.exists(f"{base}.{orig_ext}"):
  395. return f"{base}.{orig_ext}"
  396. for ext in PY_SOURCE_EXTS_STUBS_FIRST if prefer_stubs else PY_SOURCE_EXTS:
  397. source_path = f"{base}.{ext}"
  398. if os.path.exists(source_path):
  399. return source_path
  400. if include_no_ext and not orig_ext and os.path.exists(base):
  401. return base
  402. raise NoSourceFile(filename)
  403. def is_python_source(filename: str | None) -> bool:
  404. """Return: True if the filename is a python source file."""
  405. if not filename:
  406. return False
  407. return os.path.splitext(filename)[1][1:] in PY_SOURCE_EXTS
  408. def is_stdlib_module(modname: str) -> bool:
  409. """Return: True if the modname is in the standard library"""
  410. return modname.split(".")[0] in stdlib_module_names
  411. def module_in_path(modname: str, path: str | Iterable[str]) -> bool:
  412. """Try to determine if a module is imported from one of the specified paths
  413. :param modname: name of the module
  414. :param path: paths to consider
  415. :return:
  416. true if the module:
  417. - is located on the path listed in one of the directory in `paths`
  418. """
  419. modname = modname.split(".")[0]
  420. try:
  421. filename = file_from_modpath([modname])
  422. except ImportError:
  423. # Import failed, we can't check path if we don't know it
  424. return False
  425. if filename is None:
  426. # No filename likely means it's compiled in, or potentially a namespace
  427. return False
  428. filename = _normalize_path(filename)
  429. if isinstance(path, str):
  430. return filename.startswith(_cache_normalize_path(path))
  431. return any(filename.startswith(_cache_normalize_path(entry)) for entry in path)
  432. def is_standard_module(modname: str, std_path: Iterable[str] | None = None) -> bool:
  433. """Try to guess if a module is a standard python module (by default,
  434. see `std_path` parameter's description).
  435. :param modname: name of the module we are interested in
  436. :param std_path: list of path considered has standard
  437. :return:
  438. true if the module:
  439. - is located on the path listed in one of the directory in `std_path`
  440. - is a built-in module
  441. """
  442. warnings.warn(
  443. "is_standard_module() is deprecated. Use, is_stdlib_module() or module_in_path() instead",
  444. DeprecationWarning,
  445. stacklevel=2,
  446. )
  447. modname = modname.split(".")[0]
  448. try:
  449. filename = file_from_modpath([modname])
  450. except ImportError:
  451. # import failed, i'm probably not so wrong by supposing it's
  452. # not standard...
  453. return False
  454. # modules which are not living in a file are considered standard
  455. # (sys and __builtin__ for instance)
  456. if filename is None:
  457. # we assume there are no namespaces in stdlib
  458. return not util.is_namespace(modname)
  459. filename = _normalize_path(filename)
  460. for path in EXT_LIB_DIRS:
  461. if filename.startswith(_cache_normalize_path(path)):
  462. return False
  463. if std_path is None:
  464. std_path = STD_LIB_DIRS
  465. return any(filename.startswith(_cache_normalize_path(path)) for path in std_path)
  466. def is_relative(modname: str, from_file: str) -> bool:
  467. """Return true if the given module name is relative to the given
  468. file name.
  469. :param modname: name of the module we are interested in
  470. :param from_file:
  471. path of the module from which modname has been imported
  472. :return:
  473. true if the module has been imported relatively to `from_file`
  474. """
  475. if not os.path.isdir(from_file):
  476. from_file = os.path.dirname(from_file)
  477. if from_file in sys.path:
  478. return False
  479. return bool(
  480. importlib.machinery.PathFinder.find_spec(
  481. modname.split(".", maxsplit=1)[0], [from_file]
  482. )
  483. )
  484. @lru_cache(maxsize=1024)
  485. def cached_os_path_isfile(path: str | os.PathLike[str]) -> bool:
  486. """A cached version of os.path.isfile that helps avoid repetitive I/O"""
  487. return os.path.isfile(path)
  488. # internal only functions #####################################################
  489. def _spec_from_modpath(
  490. modpath: list[str],
  491. path: Sequence[str] | None = None,
  492. context: str | None = None,
  493. ) -> spec.ModuleSpec:
  494. """Given a mod path (i.e. split module / package name), return the
  495. corresponding spec.
  496. this function is used internally, see `file_from_modpath`'s
  497. documentation for more information
  498. """
  499. assert modpath
  500. location = None
  501. if context is not None:
  502. try:
  503. found_spec = spec.find_spec(modpath, [context])
  504. location = found_spec.location
  505. except ImportError:
  506. found_spec = spec.find_spec(modpath, path)
  507. location = found_spec.location
  508. else:
  509. found_spec = spec.find_spec(modpath, path)
  510. if found_spec.type == spec.ModuleType.PY_COMPILED:
  511. try:
  512. assert found_spec.location is not None
  513. location = get_source_file(found_spec.location)
  514. return found_spec._replace(
  515. location=location, type=spec.ModuleType.PY_SOURCE
  516. )
  517. except NoSourceFile:
  518. return found_spec._replace(location=location)
  519. elif found_spec.type == spec.ModuleType.C_BUILTIN:
  520. # integrated builtin module
  521. return found_spec._replace(location=None)
  522. elif found_spec.type == spec.ModuleType.PKG_DIRECTORY:
  523. assert found_spec.location is not None
  524. location = _has_init(found_spec.location)
  525. return found_spec._replace(location=location, type=spec.ModuleType.PY_SOURCE)
  526. return found_spec
  527. def _is_python_file(filename: str) -> bool:
  528. """Return true if the given filename should be considered as a python file.
  529. .pyc and .pyo are ignored
  530. """
  531. return filename.endswith((".py", ".pyi", ".so", ".pyd", ".pyw"))
  532. @lru_cache(maxsize=1024)
  533. def _has_init(directory: str) -> str | None:
  534. """If the given directory has a valid __init__ file, return its path,
  535. else return None.
  536. """
  537. mod_or_pack = os.path.join(directory, "__init__")
  538. for ext in (*PY_SOURCE_EXTS, "pyc", "pyo"):
  539. if os.path.exists(mod_or_pack + "." + ext):
  540. return mod_or_pack + "." + ext
  541. return None
  542. def is_namespace(specobj: spec.ModuleSpec) -> bool:
  543. return specobj.type == spec.ModuleType.PY_NAMESPACE
  544. def is_directory(specobj: spec.ModuleSpec) -> bool:
  545. return specobj.type == spec.ModuleType.PKG_DIRECTORY
  546. def is_module_name_part_of_extension_package_whitelist(
  547. module_name: str, package_whitelist: set[str]
  548. ) -> bool:
  549. """
  550. Returns True if one part of the module name is in the package whitelist.
  551. >>> is_module_name_part_of_extension_package_whitelist('numpy.core.umath', {'numpy'})
  552. True
  553. """
  554. parts = module_name.split(".")
  555. return any(
  556. ".".join(parts[:x]) in package_whitelist for x in range(1, len(parts) + 1)
  557. )