expand_modules.py 6.9 KB

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