util.py 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114
  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. from __future__ import annotations
  5. import pathlib
  6. import sys
  7. from functools import lru_cache
  8. from importlib._bootstrap_external import _NamespacePath # type: ignore[attr-defined]
  9. from importlib.util import _find_spec_from_path # type: ignore[attr-defined]
  10. from astroid.const import IS_PYPY
  11. if sys.version_info >= (3, 11):
  12. from importlib.machinery import NamespaceLoader
  13. else:
  14. from importlib._bootstrap_external import _NamespaceLoader as NamespaceLoader
  15. @lru_cache(maxsize=4096)
  16. def is_namespace(modname: str) -> bool:
  17. from astroid.modutils import ( # pylint: disable=import-outside-toplevel
  18. EXT_LIB_DIRS,
  19. STD_LIB_DIRS,
  20. )
  21. STD_AND_EXT_LIB_DIRS = STD_LIB_DIRS.union(EXT_LIB_DIRS)
  22. if modname in sys.builtin_module_names:
  23. return False
  24. found_spec = None
  25. # find_spec() attempts to import parent packages when given dotted paths.
  26. # That's unacceptable here, so we fallback to _find_spec_from_path(), which does
  27. # not, but requires instead that each single parent ('astroid', 'nodes', etc.)
  28. # be specced from left to right.
  29. processed_components = []
  30. last_submodule_search_locations: _NamespacePath | None = None
  31. for component in modname.split("."):
  32. processed_components.append(component)
  33. working_modname = ".".join(processed_components)
  34. try:
  35. # Both the modname and the path are built iteratively, with the
  36. # path (e.g. ['a', 'a/b', 'a/b/c']) lagging the modname by one
  37. found_spec = _find_spec_from_path(
  38. working_modname, path=last_submodule_search_locations
  39. )
  40. except AttributeError:
  41. return False
  42. except ValueError:
  43. if modname == "__main__":
  44. return False
  45. try:
  46. # .pth files will be on sys.modules
  47. # __spec__ is set inconsistently on PyPy so we can't really on the heuristic here
  48. # See: https://foss.heptapod.net/pypy/pypy/-/issues/3736
  49. # Check first fragment of modname, e.g. "astroid", not "astroid.interpreter"
  50. # because of cffi's behavior
  51. # See: https://github.com/pylint-dev/astroid/issues/1776
  52. mod = sys.modules[processed_components[0]]
  53. return (
  54. mod.__spec__ is None
  55. and getattr(mod, "__file__", None) is None
  56. and hasattr(mod, "__path__")
  57. and not IS_PYPY
  58. )
  59. except KeyError:
  60. return False
  61. except AttributeError:
  62. # Workaround for "py" module
  63. # https://github.com/pytest-dev/apipkg/issues/13
  64. return False
  65. except KeyError:
  66. # Intermediate steps might raise KeyErrors
  67. # https://github.com/python/cpython/issues/93334
  68. # TODO: update if fixed in importlib
  69. # For tree a > b > c.py
  70. # >>> from importlib.machinery import PathFinder
  71. # >>> PathFinder.find_spec('a.b', ['a'])
  72. # KeyError: 'a'
  73. # Repair last_submodule_search_locations
  74. if last_submodule_search_locations:
  75. last_item = last_submodule_search_locations[-1]
  76. # e.g. for failure example above, add 'a/b' and keep going
  77. # so that find_spec('a.b.c', path=['a', 'a/b']) succeeds
  78. assumed_location = pathlib.Path(last_item) / component
  79. last_submodule_search_locations.append(str(assumed_location))
  80. continue
  81. # Update last_submodule_search_locations for next iteration
  82. if found_spec and found_spec.submodule_search_locations:
  83. # But immediately return False if we can detect we are in stdlib
  84. # or external lib (e.g site-packages)
  85. if any(
  86. any(
  87. str(location).startswith(lib_dir)
  88. for lib_dir in STD_AND_EXT_LIB_DIRS
  89. )
  90. for location in found_spec.submodule_search_locations
  91. ):
  92. return False
  93. last_submodule_search_locations = found_spec.submodule_search_locations
  94. return (
  95. found_spec is not None
  96. and found_spec.submodule_search_locations is not None
  97. and found_spec.origin is None
  98. and (
  99. found_spec.loader is None or isinstance(found_spec.loader, NamespaceLoader)
  100. )
  101. )