modutils.py 23 KB

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