PageRenderTime 51ms CodeModel.GetById 15ms RepoModel.GetById 1ms app.codeStats 0ms

/src/flake8/utils.py

https://gitlab.com/mdomke/flake8
Python | 317 lines | 232 code | 16 blank | 69 comment | 14 complexity | 273a7d3d216f36c473153a115257995d MD5 | raw file
  1. """Utility methods for flake8."""
  2. import collections
  3. import fnmatch as _fnmatch
  4. import inspect
  5. import io
  6. import os
  7. import platform
  8. import re
  9. import sys
  10. DIFF_HUNK_REGEXP = re.compile(r'^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@.*$')
  11. def parse_comma_separated_list(value):
  12. # type: (Union[Sequence[str], str]) -> List[str]
  13. """Parse a comma-separated list.
  14. :param value:
  15. String or list of strings to be parsed and normalized.
  16. :returns:
  17. List of values with whitespace stripped.
  18. :rtype:
  19. list
  20. """
  21. if not value:
  22. return []
  23. if not isinstance(value, (list, tuple)):
  24. value = value.split(',')
  25. return [item.strip() for item in value]
  26. def normalize_paths(paths, parent=os.curdir):
  27. # type: (Union[Sequence[str], str], str) -> List[str]
  28. """Parse a comma-separated list of paths.
  29. :returns:
  30. The normalized paths.
  31. :rtype:
  32. [str]
  33. """
  34. return [normalize_path(p, parent)
  35. for p in parse_comma_separated_list(paths)]
  36. def normalize_path(path, parent=os.curdir):
  37. # type: (str, str) -> str
  38. """Normalize a single-path.
  39. :returns:
  40. The normalized path.
  41. :rtype:
  42. str
  43. """
  44. # NOTE(sigmavirus24): Using os.path.sep and os.path.altsep allow for
  45. # Windows compatibility with both Windows-style paths (c:\\foo\bar) and
  46. # Unix style paths (/foo/bar).
  47. separator = os.path.sep
  48. # NOTE(sigmavirus24): os.path.altsep may be None
  49. alternate_separator = os.path.altsep or ''
  50. if separator in path or (alternate_separator and
  51. alternate_separator in path):
  52. path = os.path.abspath(os.path.join(parent, path))
  53. return path.rstrip(separator + alternate_separator)
  54. def stdin_get_value():
  55. # type: () -> str
  56. """Get and cache it so plugins can use it."""
  57. cached_value = getattr(stdin_get_value, 'cached_stdin', None)
  58. if cached_value is None:
  59. stdin_value = sys.stdin.read()
  60. if sys.version_info < (3, 0):
  61. cached_type = io.BytesIO
  62. else:
  63. cached_type = io.StringIO
  64. stdin_get_value.cached_stdin = cached_type(stdin_value)
  65. cached_value = stdin_get_value.cached_stdin
  66. return cached_value.getvalue()
  67. def parse_unified_diff(diff=None):
  68. # type: (str) -> List[str]
  69. """Parse the unified diff passed on stdin.
  70. :returns:
  71. dictionary mapping file names to sets of line numbers
  72. :rtype:
  73. dict
  74. """
  75. # Allow us to not have to patch out stdin_get_value
  76. if diff is None:
  77. diff = stdin_get_value()
  78. number_of_rows = None
  79. current_path = None
  80. parsed_paths = collections.defaultdict(set)
  81. for line in diff.splitlines():
  82. if number_of_rows:
  83. # NOTE(sigmavirus24): Below we use a slice because stdin may be
  84. # bytes instead of text on Python 3.
  85. if line[:1] != '-':
  86. number_of_rows -= 1
  87. # We're in the part of the diff that has lines starting with +, -,
  88. # and ' ' to show context and the changes made. We skip these
  89. # because the information we care about is the filename and the
  90. # range within it.
  91. # When number_of_rows reaches 0, we will once again start
  92. # searching for filenames and ranges.
  93. continue
  94. # NOTE(sigmavirus24): Diffs that we support look roughly like:
  95. # diff a/file.py b/file.py
  96. # ...
  97. # --- a/file.py
  98. # +++ b/file.py
  99. # Below we're looking for that last line. Every diff tool that
  100. # gives us this output may have additional information after
  101. # ``b/file.py`` which it will separate with a \t, e.g.,
  102. # +++ b/file.py\t100644
  103. # Which is an example that has the new file permissions/mode.
  104. # In this case we only care about the file name.
  105. if line[:3] == '+++':
  106. current_path = line[4:].split('\t', 1)[0]
  107. # NOTE(sigmavirus24): This check is for diff output from git.
  108. if current_path[:2] == 'b/':
  109. current_path = current_path[2:]
  110. # We don't need to do anything else. We have set up our local
  111. # ``current_path`` variable. We can skip the rest of this loop.
  112. # The next line we will see will give us the hung information
  113. # which is in the next section of logic.
  114. continue
  115. hunk_match = DIFF_HUNK_REGEXP.match(line)
  116. # NOTE(sigmavirus24): pep8/pycodestyle check for:
  117. # line[:3] == '@@ '
  118. # But the DIFF_HUNK_REGEXP enforces that the line start with that
  119. # So we can more simply check for a match instead of slicing and
  120. # comparing.
  121. if hunk_match:
  122. (row, number_of_rows) = [
  123. 1 if not group else int(group)
  124. for group in hunk_match.groups()
  125. ]
  126. parsed_paths[current_path].update(
  127. range(row, row + number_of_rows)
  128. )
  129. # We have now parsed our diff into a dictionary that looks like:
  130. # {'file.py': set(range(10, 16), range(18, 20)), ...}
  131. return parsed_paths
  132. def is_windows():
  133. # type: () -> bool
  134. """Determine if we're running on Windows.
  135. :returns:
  136. True if running on Windows, otherwise False
  137. :rtype:
  138. bool
  139. """
  140. return os.name == 'nt'
  141. def can_run_multiprocessing_on_windows():
  142. # type: () -> bool
  143. """Determine if we can use multiprocessing on Windows.
  144. :returns:
  145. True if the version of Python is modern enough, otherwise False
  146. :rtype:
  147. bool
  148. """
  149. is_new_enough_python27 = sys.version_info >= (2, 7, 11)
  150. is_new_enough_python3 = sys.version_info > (3, 2)
  151. return is_new_enough_python27 or is_new_enough_python3
  152. def is_using_stdin(paths):
  153. # type: (List[str]) -> bool
  154. """Determine if we're going to read from stdin.
  155. :param list paths:
  156. The paths that we're going to check.
  157. :returns:
  158. True if stdin (-) is in the path, otherwise False
  159. :rtype:
  160. bool
  161. """
  162. return '-' in paths
  163. def _default_predicate(*args):
  164. return False
  165. def filenames_from(arg, predicate=None):
  166. # type: (str, callable) -> Generator
  167. """Generate filenames from an argument.
  168. :param str arg:
  169. Parameter from the command-line.
  170. :param callable predicate:
  171. Predicate to use to filter out filenames. If the predicate
  172. returns ``True`` we will exclude the filename, otherwise we
  173. will yield it. By default, we include every filename
  174. generated.
  175. :returns:
  176. Generator of paths
  177. """
  178. if predicate is None:
  179. predicate = _default_predicate
  180. if predicate(arg):
  181. return
  182. if os.path.isdir(arg):
  183. for root, sub_directories, files in os.walk(arg):
  184. if predicate(root):
  185. sub_directories[:] = []
  186. continue
  187. # NOTE(sigmavirus24): os.walk() will skip a directory if you
  188. # remove it from the list of sub-directories.
  189. for directory in sub_directories:
  190. joined = os.path.join(root, directory)
  191. if predicate(directory) or predicate(joined):
  192. sub_directories.remove(directory)
  193. for filename in files:
  194. joined = os.path.join(root, filename)
  195. if predicate(joined) or predicate(filename):
  196. continue
  197. yield joined
  198. else:
  199. yield arg
  200. def fnmatch(filename, patterns, default=True):
  201. # type: (str, List[str], bool) -> bool
  202. """Wrap :func:`fnmatch.fnmatch` to add some functionality.
  203. :param str filename:
  204. Name of the file we're trying to match.
  205. :param list patterns:
  206. Patterns we're using to try to match the filename.
  207. :param bool default:
  208. The default value if patterns is empty
  209. :returns:
  210. True if a pattern matches the filename, False if it doesn't.
  211. ``default`` if patterns is empty.
  212. """
  213. if not patterns:
  214. return default
  215. return any(_fnmatch.fnmatch(filename, pattern) for pattern in patterns)
  216. def parameters_for(plugin):
  217. # type: (flake8.plugins.manager.Plugin) -> Dict[str, bool]
  218. """Return the parameters for the plugin.
  219. This will inspect the plugin and return either the function parameters
  220. if the plugin is a function or the parameters for ``__init__`` after
  221. ``self`` if the plugin is a class.
  222. :param plugin:
  223. The internal plugin object.
  224. :type plugin:
  225. flake8.plugins.manager.Plugin
  226. :returns:
  227. A dictionary mapping the parameter name to whether or not it is
  228. required (a.k.a., is positional only/does not have a default).
  229. :rtype:
  230. dict([(str, bool)])
  231. """
  232. func = plugin.plugin
  233. is_class = not inspect.isfunction(func)
  234. if is_class: # The plugin is a class
  235. func = plugin.plugin.__init__
  236. if sys.version_info < (3, 3):
  237. argspec = inspect.getargspec(func)
  238. start_of_optional_args = len(argspec[0]) - len(argspec[-1] or [])
  239. parameter_names = argspec[0]
  240. parameters = collections.OrderedDict([
  241. (name, position < start_of_optional_args)
  242. for position, name in enumerate(parameter_names)
  243. ])
  244. else:
  245. parameters = collections.OrderedDict([
  246. (parameter.name, parameter.default is parameter.empty)
  247. for parameter in inspect.signature(func).parameters.values()
  248. if parameter.kind == parameter.POSITIONAL_OR_KEYWORD
  249. ])
  250. if is_class:
  251. parameters.pop('self', None)
  252. return parameters
  253. def get_python_version():
  254. """Find and format the python implementation and version.
  255. :returns:
  256. Implementation name, version, and platform as a string.
  257. :rtype:
  258. str
  259. """
  260. # The implementation isn't all that important.
  261. try:
  262. impl = platform.python_implementation() + " "
  263. except AttributeError: # Python 2.5
  264. impl = ''
  265. return '%s%s on %s' % (impl, platform.python_version(), platform.system())