expand_modules.py 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185
  1. # Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
  2. # For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE
  3. # Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt
  4. from __future__ import annotations
  5. import os
  6. import sys
  7. from collections.abc import Sequence
  8. from pathlib import Path
  9. from re import Pattern
  10. from astroid import modutils
  11. from pylint.typing import ErrorDescriptionDict, ModuleDescriptionDict
  12. def _modpath_from_file(filename: str, is_namespace: bool, path: list[str]) -> list[str]:
  13. def _is_package_cb(inner_path: str, parts: list[str]) -> bool:
  14. return modutils.check_modpath_has_init(inner_path, parts) or is_namespace
  15. return modutils.modpath_from_file_with_callback( # type: ignore[no-any-return]
  16. filename, path=path, is_package_cb=_is_package_cb
  17. )
  18. def discover_package_path(modulepath: str, source_roots: Sequence[str]) -> str:
  19. """Discover package path from one its modules and source roots."""
  20. dirname = os.path.realpath(os.path.expanduser(modulepath))
  21. if not os.path.isdir(dirname):
  22. dirname = os.path.dirname(dirname)
  23. # Look for a source root that contains the module directory
  24. for source_root in source_roots:
  25. source_root = os.path.realpath(os.path.expanduser(source_root))
  26. if os.path.commonpath([source_root, dirname]) in [dirname, source_root]:
  27. return source_root
  28. # Fall back to legacy discovery by looking for __init__.py upwards as
  29. # it's the only way given that source root was not found or was not provided
  30. while True:
  31. if not os.path.exists(os.path.join(dirname, "__init__.py")):
  32. return dirname
  33. old_dirname = dirname
  34. dirname = os.path.dirname(dirname)
  35. if old_dirname == dirname:
  36. return os.getcwd()
  37. def _is_in_ignore_list_re(element: str, ignore_list_re: list[Pattern[str]]) -> bool:
  38. """Determines if the element is matched in a regex ignore-list."""
  39. return any(file_pattern.match(element) for file_pattern in ignore_list_re)
  40. def _is_ignored_file(
  41. element: str,
  42. ignore_list: list[str],
  43. ignore_list_re: list[Pattern[str]],
  44. ignore_list_paths_re: list[Pattern[str]],
  45. ) -> bool:
  46. element = os.path.normpath(element)
  47. basename = Path(element).absolute().name
  48. return (
  49. basename in ignore_list
  50. or _is_in_ignore_list_re(basename, ignore_list_re)
  51. or _is_in_ignore_list_re(element, ignore_list_paths_re)
  52. )
  53. # pylint: disable = too-many-locals, too-many-statements
  54. def expand_modules(
  55. files_or_modules: Sequence[str],
  56. source_roots: Sequence[str],
  57. ignore_list: list[str],
  58. ignore_list_re: list[Pattern[str]],
  59. ignore_list_paths_re: list[Pattern[str]],
  60. ) -> tuple[dict[str, ModuleDescriptionDict], list[ErrorDescriptionDict]]:
  61. """Take a list of files/modules/packages and return the list of tuple
  62. (file, module name) which have to be actually checked.
  63. """
  64. result: dict[str, ModuleDescriptionDict] = {}
  65. errors: list[ErrorDescriptionDict] = []
  66. path = sys.path.copy()
  67. for something in files_or_modules:
  68. basename = os.path.basename(something)
  69. if _is_ignored_file(
  70. something, ignore_list, ignore_list_re, ignore_list_paths_re
  71. ):
  72. result[something] = {
  73. "path": something,
  74. "name": "",
  75. "isarg": False,
  76. "basepath": something,
  77. "basename": "",
  78. "isignored": True,
  79. }
  80. continue
  81. module_package_path = discover_package_path(something, source_roots)
  82. additional_search_path = [".", module_package_path, *path]
  83. if os.path.exists(something):
  84. # this is a file or a directory
  85. try:
  86. modname = ".".join(
  87. modutils.modpath_from_file(something, path=additional_search_path)
  88. )
  89. except ImportError:
  90. modname = os.path.splitext(basename)[0]
  91. if os.path.isdir(something):
  92. filepath = os.path.join(something, "__init__.py")
  93. else:
  94. filepath = something
  95. else:
  96. # suppose it's a module or package
  97. modname = something
  98. try:
  99. filepath = modutils.file_from_modpath(
  100. modname.split("."), path=additional_search_path
  101. )
  102. if filepath is None:
  103. continue
  104. except ImportError as ex:
  105. errors.append({"key": "fatal", "mod": modname, "ex": ex})
  106. continue
  107. filepath = os.path.normpath(filepath)
  108. modparts = (modname or something).split(".")
  109. try:
  110. spec = modutils.file_info_from_modpath(
  111. modparts, path=additional_search_path
  112. )
  113. except ImportError:
  114. # Might not be acceptable, don't crash.
  115. is_namespace = not os.path.exists(filepath)
  116. is_directory = os.path.isdir(something)
  117. else:
  118. is_namespace = modutils.is_namespace(spec)
  119. is_directory = modutils.is_directory(spec)
  120. if not is_namespace:
  121. default: ModuleDescriptionDict = {
  122. "path": filepath,
  123. "name": modname,
  124. "isarg": True,
  125. "basepath": filepath,
  126. "basename": modname,
  127. "isignored": False,
  128. }
  129. result.setdefault(filepath, default)["isarg"] = True
  130. has_init = (
  131. modparts[-1] != "__init__" and os.path.basename(filepath) == "__init__.py"
  132. )
  133. if has_init or is_namespace or is_directory:
  134. for subfilepath in modutils.get_module_files(
  135. os.path.dirname(filepath) or ".", ignore_list, list_all=is_namespace
  136. ):
  137. subfilepath = os.path.normpath(subfilepath)
  138. if filepath == subfilepath:
  139. continue
  140. if _is_ignored_file(
  141. subfilepath, ignore_list, ignore_list_re, ignore_list_paths_re
  142. ):
  143. result[subfilepath] = {
  144. "path": subfilepath,
  145. "name": "",
  146. "isarg": False,
  147. "basepath": subfilepath,
  148. "basename": "",
  149. "isignored": True,
  150. }
  151. continue
  152. modpath = _modpath_from_file(
  153. subfilepath, is_namespace, path=additional_search_path
  154. )
  155. submodname = ".".join(modpath)
  156. # Preserve arg flag if module is also explicitly given.
  157. isarg = subfilepath in result and result[subfilepath]["isarg"]
  158. result[subfilepath] = {
  159. "path": subfilepath,
  160. "name": submodname,
  161. "isarg": isarg,
  162. "basepath": filepath,
  163. "basename": modname,
  164. "isignored": False,
  165. }
  166. return result, errors