m2m模型翻译
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

384 lines
14 KiB

6 months ago
  1. # -*- coding: utf-8 -*-
  2. #
  3. # Copyright (C) 2012-2023 Python Software Foundation.
  4. # See LICENSE.txt and CONTRIBUTORS.txt.
  5. #
  6. """
  7. Class representing the list of files in a distribution.
  8. Equivalent to distutils.filelist, but fixes some problems.
  9. """
  10. import fnmatch
  11. import logging
  12. import os
  13. import re
  14. import sys
  15. from . import DistlibException
  16. from .compat import fsdecode
  17. from .util import convert_path
  18. __all__ = ['Manifest']
  19. logger = logging.getLogger(__name__)
  20. # a \ followed by some spaces + EOL
  21. _COLLAPSE_PATTERN = re.compile('\\\\w*\n', re.M)
  22. _COMMENTED_LINE = re.compile('#.*?(?=\n)|\n(?=$)', re.M | re.S)
  23. #
  24. # Due to the different results returned by fnmatch.translate, we need
  25. # to do slightly different processing for Python 2.7 and 3.2 ... this needed
  26. # to be brought in for Python 3.6 onwards.
  27. #
  28. _PYTHON_VERSION = sys.version_info[:2]
  29. class Manifest(object):
  30. """
  31. A list of files built by exploring the filesystem and filtered by applying various
  32. patterns to what we find there.
  33. """
  34. def __init__(self, base=None):
  35. """
  36. Initialise an instance.
  37. :param base: The base directory to explore under.
  38. """
  39. self.base = os.path.abspath(os.path.normpath(base or os.getcwd()))
  40. self.prefix = self.base + os.sep
  41. self.allfiles = None
  42. self.files = set()
  43. #
  44. # Public API
  45. #
  46. def findall(self):
  47. """Find all files under the base and set ``allfiles`` to the absolute
  48. pathnames of files found.
  49. """
  50. from stat import S_ISREG, S_ISDIR, S_ISLNK
  51. self.allfiles = allfiles = []
  52. root = self.base
  53. stack = [root]
  54. pop = stack.pop
  55. push = stack.append
  56. while stack:
  57. root = pop()
  58. names = os.listdir(root)
  59. for name in names:
  60. fullname = os.path.join(root, name)
  61. # Avoid excess stat calls -- just one will do, thank you!
  62. stat = os.stat(fullname)
  63. mode = stat.st_mode
  64. if S_ISREG(mode):
  65. allfiles.append(fsdecode(fullname))
  66. elif S_ISDIR(mode) and not S_ISLNK(mode):
  67. push(fullname)
  68. def add(self, item):
  69. """
  70. Add a file to the manifest.
  71. :param item: The pathname to add. This can be relative to the base.
  72. """
  73. if not item.startswith(self.prefix):
  74. item = os.path.join(self.base, item)
  75. self.files.add(os.path.normpath(item))
  76. def add_many(self, items):
  77. """
  78. Add a list of files to the manifest.
  79. :param items: The pathnames to add. These can be relative to the base.
  80. """
  81. for item in items:
  82. self.add(item)
  83. def sorted(self, wantdirs=False):
  84. """
  85. Return sorted files in directory order
  86. """
  87. def add_dir(dirs, d):
  88. dirs.add(d)
  89. logger.debug('add_dir added %s', d)
  90. if d != self.base:
  91. parent, _ = os.path.split(d)
  92. assert parent not in ('', '/')
  93. add_dir(dirs, parent)
  94. result = set(self.files) # make a copy!
  95. if wantdirs:
  96. dirs = set()
  97. for f in result:
  98. add_dir(dirs, os.path.dirname(f))
  99. result |= dirs
  100. return [os.path.join(*path_tuple) for path_tuple in
  101. sorted(os.path.split(path) for path in result)]
  102. def clear(self):
  103. """Clear all collected files."""
  104. self.files = set()
  105. self.allfiles = []
  106. def process_directive(self, directive):
  107. """
  108. Process a directive which either adds some files from ``allfiles`` to
  109. ``files``, or removes some files from ``files``.
  110. :param directive: The directive to process. This should be in a format
  111. compatible with distutils ``MANIFEST.in`` files:
  112. http://docs.python.org/distutils/sourcedist.html#commands
  113. """
  114. # Parse the line: split it up, make sure the right number of words
  115. # is there, and return the relevant words. 'action' is always
  116. # defined: it's the first word of the line. Which of the other
  117. # three are defined depends on the action; it'll be either
  118. # patterns, (dir and patterns), or (dirpattern).
  119. action, patterns, thedir, dirpattern = self._parse_directive(directive)
  120. # OK, now we know that the action is valid and we have the
  121. # right number of words on the line for that action -- so we
  122. # can proceed with minimal error-checking.
  123. if action == 'include':
  124. for pattern in patterns:
  125. if not self._include_pattern(pattern, anchor=True):
  126. logger.warning('no files found matching %r', pattern)
  127. elif action == 'exclude':
  128. for pattern in patterns:
  129. self._exclude_pattern(pattern, anchor=True)
  130. elif action == 'global-include':
  131. for pattern in patterns:
  132. if not self._include_pattern(pattern, anchor=False):
  133. logger.warning('no files found matching %r '
  134. 'anywhere in distribution', pattern)
  135. elif action == 'global-exclude':
  136. for pattern in patterns:
  137. self._exclude_pattern(pattern, anchor=False)
  138. elif action == 'recursive-include':
  139. for pattern in patterns:
  140. if not self._include_pattern(pattern, prefix=thedir):
  141. logger.warning('no files found matching %r '
  142. 'under directory %r', pattern, thedir)
  143. elif action == 'recursive-exclude':
  144. for pattern in patterns:
  145. self._exclude_pattern(pattern, prefix=thedir)
  146. elif action == 'graft':
  147. if not self._include_pattern(None, prefix=dirpattern):
  148. logger.warning('no directories found matching %r',
  149. dirpattern)
  150. elif action == 'prune':
  151. if not self._exclude_pattern(None, prefix=dirpattern):
  152. logger.warning('no previously-included directories found '
  153. 'matching %r', dirpattern)
  154. else: # pragma: no cover
  155. # This should never happen, as it should be caught in
  156. # _parse_template_line
  157. raise DistlibException(
  158. 'invalid action %r' % action)
  159. #
  160. # Private API
  161. #
  162. def _parse_directive(self, directive):
  163. """
  164. Validate a directive.
  165. :param directive: The directive to validate.
  166. :return: A tuple of action, patterns, thedir, dir_patterns
  167. """
  168. words = directive.split()
  169. if len(words) == 1 and words[0] not in ('include', 'exclude',
  170. 'global-include',
  171. 'global-exclude',
  172. 'recursive-include',
  173. 'recursive-exclude',
  174. 'graft', 'prune'):
  175. # no action given, let's use the default 'include'
  176. words.insert(0, 'include')
  177. action = words[0]
  178. patterns = thedir = dir_pattern = None
  179. if action in ('include', 'exclude',
  180. 'global-include', 'global-exclude'):
  181. if len(words) < 2:
  182. raise DistlibException(
  183. '%r expects <pattern1> <pattern2> ...' % action)
  184. patterns = [convert_path(word) for word in words[1:]]
  185. elif action in ('recursive-include', 'recursive-exclude'):
  186. if len(words) < 3:
  187. raise DistlibException(
  188. '%r expects <dir> <pattern1> <pattern2> ...' % action)
  189. thedir = convert_path(words[1])
  190. patterns = [convert_path(word) for word in words[2:]]
  191. elif action in ('graft', 'prune'):
  192. if len(words) != 2:
  193. raise DistlibException(
  194. '%r expects a single <dir_pattern>' % action)
  195. dir_pattern = convert_path(words[1])
  196. else:
  197. raise DistlibException('unknown action %r' % action)
  198. return action, patterns, thedir, dir_pattern
  199. def _include_pattern(self, pattern, anchor=True, prefix=None,
  200. is_regex=False):
  201. """Select strings (presumably filenames) from 'self.files' that
  202. match 'pattern', a Unix-style wildcard (glob) pattern.
  203. Patterns are not quite the same as implemented by the 'fnmatch'
  204. module: '*' and '?' match non-special characters, where "special"
  205. is platform-dependent: slash on Unix; colon, slash, and backslash on
  206. DOS/Windows; and colon on Mac OS.
  207. If 'anchor' is true (the default), then the pattern match is more
  208. stringent: "*.py" will match "foo.py" but not "foo/bar.py". If
  209. 'anchor' is false, both of these will match.
  210. If 'prefix' is supplied, then only filenames starting with 'prefix'
  211. (itself a pattern) and ending with 'pattern', with anything in between
  212. them, will match. 'anchor' is ignored in this case.
  213. If 'is_regex' is true, 'anchor' and 'prefix' are ignored, and
  214. 'pattern' is assumed to be either a string containing a regex or a
  215. regex object -- no translation is done, the regex is just compiled
  216. and used as-is.
  217. Selected strings will be added to self.files.
  218. Return True if files are found.
  219. """
  220. # XXX docstring lying about what the special chars are?
  221. found = False
  222. pattern_re = self._translate_pattern(pattern, anchor, prefix, is_regex)
  223. # delayed loading of allfiles list
  224. if self.allfiles is None:
  225. self.findall()
  226. for name in self.allfiles:
  227. if pattern_re.search(name):
  228. self.files.add(name)
  229. found = True
  230. return found
  231. def _exclude_pattern(self, pattern, anchor=True, prefix=None,
  232. is_regex=False):
  233. """Remove strings (presumably filenames) from 'files' that match
  234. 'pattern'.
  235. Other parameters are the same as for 'include_pattern()', above.
  236. The list 'self.files' is modified in place. Return True if files are
  237. found.
  238. This API is public to allow e.g. exclusion of SCM subdirs, e.g. when
  239. packaging source distributions
  240. """
  241. found = False
  242. pattern_re = self._translate_pattern(pattern, anchor, prefix, is_regex)
  243. for f in list(self.files):
  244. if pattern_re.search(f):
  245. self.files.remove(f)
  246. found = True
  247. return found
  248. def _translate_pattern(self, pattern, anchor=True, prefix=None,
  249. is_regex=False):
  250. """Translate a shell-like wildcard pattern to a compiled regular
  251. expression.
  252. Return the compiled regex. If 'is_regex' true,
  253. then 'pattern' is directly compiled to a regex (if it's a string)
  254. or just returned as-is (assumes it's a regex object).
  255. """
  256. if is_regex:
  257. if isinstance(pattern, str):
  258. return re.compile(pattern)
  259. else:
  260. return pattern
  261. if _PYTHON_VERSION > (3, 2):
  262. # ditch start and end characters
  263. start, _, end = self._glob_to_re('_').partition('_')
  264. if pattern:
  265. pattern_re = self._glob_to_re(pattern)
  266. if _PYTHON_VERSION > (3, 2):
  267. assert pattern_re.startswith(start) and pattern_re.endswith(end)
  268. else:
  269. pattern_re = ''
  270. base = re.escape(os.path.join(self.base, ''))
  271. if prefix is not None:
  272. # ditch end of pattern character
  273. if _PYTHON_VERSION <= (3, 2):
  274. empty_pattern = self._glob_to_re('')
  275. prefix_re = self._glob_to_re(prefix)[:-len(empty_pattern)]
  276. else:
  277. prefix_re = self._glob_to_re(prefix)
  278. assert prefix_re.startswith(start) and prefix_re.endswith(end)
  279. prefix_re = prefix_re[len(start): len(prefix_re) - len(end)]
  280. sep = os.sep
  281. if os.sep == '\\':
  282. sep = r'\\'
  283. if _PYTHON_VERSION <= (3, 2):
  284. pattern_re = '^' + base + sep.join((prefix_re,
  285. '.*' + pattern_re))
  286. else:
  287. pattern_re = pattern_re[len(start): len(pattern_re) - len(end)]
  288. pattern_re = r'%s%s%s%s.*%s%s' % (start, base, prefix_re, sep,
  289. pattern_re, end)
  290. else: # no prefix -- respect anchor flag
  291. if anchor:
  292. if _PYTHON_VERSION <= (3, 2):
  293. pattern_re = '^' + base + pattern_re
  294. else:
  295. pattern_re = r'%s%s%s' % (start, base, pattern_re[len(start):])
  296. return re.compile(pattern_re)
  297. def _glob_to_re(self, pattern):
  298. """Translate a shell-like glob pattern to a regular expression.
  299. Return a string containing the regex. Differs from
  300. 'fnmatch.translate()' in that '*' does not match "special characters"
  301. (which are platform-specific).
  302. """
  303. pattern_re = fnmatch.translate(pattern)
  304. # '?' and '*' in the glob pattern become '.' and '.*' in the RE, which
  305. # IMHO is wrong -- '?' and '*' aren't supposed to match slash in Unix,
  306. # and by extension they shouldn't match such "special characters" under
  307. # any OS. So change all non-escaped dots in the RE to match any
  308. # character except the special characters (currently: just os.sep).
  309. sep = os.sep
  310. if os.sep == '\\':
  311. # we're using a regex to manipulate a regex, so we need
  312. # to escape the backslash twice
  313. sep = r'\\\\'
  314. escaped = r'\1[^%s]' % sep
  315. pattern_re = re.sub(r'((?<!\\)(\\\\)*)\.', escaped, pattern_re)
  316. return pattern_re