]>
jfr.im git - dlqueue.git/blob - venv/lib/python3.11/site-packages/pip/_vendor/distlib/manifest.py
1 # -*- coding: utf-8 -*-
3 # Copyright (C) 2012-2013 Python Software Foundation.
4 # See LICENSE.txt and CONTRIBUTORS.txt.
7 Class representing the list of files in a distribution.
9 Equivalent to distutils.filelist, but fixes some problems.
17 from . import DistlibException
18 from .compat
import fsdecode
19 from .util
import convert_path
22 __all__
= ['Manifest']
24 logger
= logging
.getLogger(__name__
)
26 # a \ followed by some spaces + EOL
27 _COLLAPSE_PATTERN
= re
.compile('\\\\w*\n', re
.M
)
28 _COMMENTED_LINE
= re
.compile('#.*?(?=\n)|\n(?=$)', re
.M | re
.S
)
31 # Due to the different results returned by fnmatch.translate, we need
32 # to do slightly different processing for Python 2.7 and 3.2 ... this needed
33 # to be brought in for Python 3.6 onwards.
35 _PYTHON_VERSION
= sys
.version_info
[:2]
37 class Manifest(object):
38 """A list of files built by on exploring the filesystem and filtered by
39 applying various patterns to what we find there.
42 def __init__(self
, base
=None):
44 Initialise an instance.
46 :param base: The base directory to explore under.
48 self
.base
= os
.path
.abspath(os
.path
.normpath(base
or os
.getcwd()))
49 self
.prefix
= self
.base
+ os
.sep
58 """Find all files under the base and set ``allfiles`` to the absolute
59 pathnames of files found.
61 from stat
import S_ISREG
, S_ISDIR
, S_ISLNK
63 self
.allfiles
= allfiles
= []
71 names
= os
.listdir(root
)
74 fullname
= os
.path
.join(root
, name
)
76 # Avoid excess stat calls -- just one will do, thank you!
77 stat
= os
.stat(fullname
)
80 allfiles
.append(fsdecode(fullname
))
81 elif S_ISDIR(mode
) and not S_ISLNK(mode
):
86 Add a file to the manifest.
88 :param item: The pathname to add. This can be relative to the base.
90 if not item
.startswith(self
.prefix
):
91 item
= os
.path
.join(self
.base
, item
)
92 self
.files
.add(os
.path
.normpath(item
))
94 def add_many(self
, items
):
96 Add a list of files to the manifest.
98 :param items: The pathnames to add. These can be relative to the base.
103 def sorted(self
, wantdirs
=False):
105 Return sorted files in directory order
108 def add_dir(dirs
, d
):
110 logger
.debug('add_dir added %s', d
)
112 parent
, _
= os
.path
.split(d
)
113 assert parent
not in ('', '/')
114 add_dir(dirs
, parent
)
116 result
= set(self
.files
) # make a copy!
120 add_dir(dirs
, os
.path
.dirname(f
))
122 return [os
.path
.join(*path_tuple
) for path_tuple
in
123 sorted(os
.path
.split(path
) for path
in result
)]
126 """Clear all collected files."""
130 def process_directive(self
, directive
):
132 Process a directive which either adds some files from ``allfiles`` to
133 ``files``, or removes some files from ``files``.
135 :param directive: The directive to process. This should be in a format
136 compatible with distutils ``MANIFEST.in`` files:
138 http://docs.python.org/distutils/sourcedist.html#commands
140 # Parse the line: split it up, make sure the right number of words
141 # is there, and return the relevant words. 'action' is always
142 # defined: it's the first word of the line. Which of the other
143 # three are defined depends on the action; it'll be either
144 # patterns, (dir and patterns), or (dirpattern).
145 action
, patterns
, thedir
, dirpattern
= self
._parse
_directive
(directive
)
147 # OK, now we know that the action is valid and we have the
148 # right number of words on the line for that action -- so we
149 # can proceed with minimal error-checking.
150 if action
== 'include':
151 for pattern
in patterns
:
152 if not self
._include
_pattern
(pattern
, anchor
=True):
153 logger
.warning('no files found matching %r', pattern
)
155 elif action
== 'exclude':
156 for pattern
in patterns
:
157 found
= self
._exclude
_pattern
(pattern
, anchor
=True)
159 # logger.warning('no previously-included files '
160 # 'found matching %r', pattern)
162 elif action
== 'global-include':
163 for pattern
in patterns
:
164 if not self
._include
_pattern
(pattern
, anchor
=False):
165 logger
.warning('no files found matching %r '
166 'anywhere in distribution', pattern
)
168 elif action
== 'global-exclude':
169 for pattern
in patterns
:
170 found
= self
._exclude
_pattern
(pattern
, anchor
=False)
172 # logger.warning('no previously-included files '
173 # 'matching %r found anywhere in '
174 # 'distribution', pattern)
176 elif action
== 'recursive-include':
177 for pattern
in patterns
:
178 if not self
._include
_pattern
(pattern
, prefix
=thedir
):
179 logger
.warning('no files found matching %r '
180 'under directory %r', pattern
, thedir
)
182 elif action
== 'recursive-exclude':
183 for pattern
in patterns
:
184 found
= self
._exclude
_pattern
(pattern
, prefix
=thedir
)
186 # logger.warning('no previously-included files '
187 # 'matching %r found under directory %r',
190 elif action
== 'graft':
191 if not self
._include
_pattern
(None, prefix
=dirpattern
):
192 logger
.warning('no directories found matching %r',
195 elif action
== 'prune':
196 if not self
._exclude
_pattern
(None, prefix
=dirpattern
):
197 logger
.warning('no previously-included directories found '
198 'matching %r', dirpattern
)
199 else: # pragma: no cover
200 # This should never happen, as it should be caught in
201 # _parse_template_line
202 raise DistlibException(
203 'invalid action %r' % action
)
209 def _parse_directive(self
, directive
):
211 Validate a directive.
212 :param directive: The directive to validate.
213 :return: A tuple of action, patterns, thedir, dir_patterns
215 words
= directive
.split()
216 if len(words
) == 1 and words
[0] not in ('include', 'exclude',
222 # no action given, let's use the default 'include'
223 words
.insert(0, 'include')
226 patterns
= thedir
= dir_pattern
= None
228 if action
in ('include', 'exclude',
229 'global-include', 'global-exclude'):
231 raise DistlibException(
232 '%r expects <pattern1> <pattern2> ...' % action
)
234 patterns
= [convert_path(word
) for word
in words
[1:]]
236 elif action
in ('recursive-include', 'recursive-exclude'):
238 raise DistlibException(
239 '%r expects <dir> <pattern1> <pattern2> ...' % action
)
241 thedir
= convert_path(words
[1])
242 patterns
= [convert_path(word
) for word
in words
[2:]]
244 elif action
in ('graft', 'prune'):
246 raise DistlibException(
247 '%r expects a single <dir_pattern>' % action
)
249 dir_pattern
= convert_path(words
[1])
252 raise DistlibException('unknown action %r' % action
)
254 return action
, patterns
, thedir
, dir_pattern
256 def _include_pattern(self
, pattern
, anchor
=True, prefix
=None,
258 """Select strings (presumably filenames) from 'self.files' that
259 match 'pattern', a Unix-style wildcard (glob) pattern.
261 Patterns are not quite the same as implemented by the 'fnmatch'
262 module: '*' and '?' match non-special characters, where "special"
263 is platform-dependent: slash on Unix; colon, slash, and backslash on
264 DOS/Windows; and colon on Mac OS.
266 If 'anchor' is true (the default), then the pattern match is more
267 stringent: "*.py" will match "foo.py" but not "foo/bar.py". If
268 'anchor' is false, both of these will match.
270 If 'prefix' is supplied, then only filenames starting with 'prefix'
271 (itself a pattern) and ending with 'pattern', with anything in between
272 them, will match. 'anchor' is ignored in this case.
274 If 'is_regex' is true, 'anchor' and 'prefix' are ignored, and
275 'pattern' is assumed to be either a string containing a regex or a
276 regex object -- no translation is done, the regex is just compiled
279 Selected strings will be added to self.files.
281 Return True if files are found.
283 # XXX docstring lying about what the special chars are?
285 pattern_re
= self
._translate
_pattern
(pattern
, anchor
, prefix
, is_regex
)
287 # delayed loading of allfiles list
288 if self
.allfiles
is None:
291 for name
in self
.allfiles
:
292 if pattern_re
.search(name
):
297 def _exclude_pattern(self
, pattern
, anchor
=True, prefix
=None,
299 """Remove strings (presumably filenames) from 'files' that match
302 Other parameters are the same as for 'include_pattern()', above.
303 The list 'self.files' is modified in place. Return True if files are
306 This API is public to allow e.g. exclusion of SCM subdirs, e.g. when
307 packaging source distributions
310 pattern_re
= self
._translate
_pattern
(pattern
, anchor
, prefix
, is_regex
)
311 for f
in list(self
.files
):
312 if pattern_re
.search(f
):
317 def _translate_pattern(self
, pattern
, anchor
=True, prefix
=None,
319 """Translate a shell-like wildcard pattern to a compiled regular
322 Return the compiled regex. If 'is_regex' true,
323 then 'pattern' is directly compiled to a regex (if it's a string)
324 or just returned as-is (assumes it's a regex object).
327 if isinstance(pattern
, str):
328 return re
.compile(pattern
)
332 if _PYTHON_VERSION
> (3, 2):
333 # ditch start and end characters
334 start
, _
, end
= self
._glob
_to
_re
('_').partition('_')
337 pattern_re
= self
._glob
_to
_re
(pattern
)
338 if _PYTHON_VERSION
> (3, 2):
339 assert pattern_re
.startswith(start
) and pattern_re
.endswith(end
)
343 base
= re
.escape(os
.path
.join(self
.base
, ''))
344 if prefix
is not None:
345 # ditch end of pattern character
346 if _PYTHON_VERSION
<= (3, 2):
347 empty_pattern
= self
._glob
_to
_re
('')
348 prefix_re
= self
._glob
_to
_re
(prefix
)[:-len(empty_pattern
)]
350 prefix_re
= self
._glob
_to
_re
(prefix
)
351 assert prefix_re
.startswith(start
) and prefix_re
.endswith(end
)
352 prefix_re
= prefix_re
[len(start
): len(prefix_re
) - len(end
)]
356 if _PYTHON_VERSION
<= (3, 2):
357 pattern_re
= '^' + base
+ sep
.join((prefix_re
,
360 pattern_re
= pattern_re
[len(start
): len(pattern_re
) - len(end
)]
361 pattern_re
= r
'%s%s%s%s.*%s%s' % (start
, base
, prefix_re
, sep
,
363 else: # no prefix -- respect anchor flag
365 if _PYTHON_VERSION
<= (3, 2):
366 pattern_re
= '^' + base
+ pattern_re
368 pattern_re
= r
'%s%s%s' % (start
, base
, pattern_re
[len(start
):])
370 return re
.compile(pattern_re
)
372 def _glob_to_re(self
, pattern
):
373 """Translate a shell-like glob pattern to a regular expression.
375 Return a string containing the regex. Differs from
376 'fnmatch.translate()' in that '*' does not match "special characters"
377 (which are platform-specific).
379 pattern_re
= fnmatch
.translate(pattern
)
381 # '?' and '*' in the glob pattern become '.' and '.*' in the RE, which
382 # IMHO is wrong -- '?' and '*' aren't supposed to match slash in Unix,
383 # and by extension they shouldn't match such "special characters" under
384 # any OS. So change all non-escaped dots in the RE to match any
385 # character except the special characters (currently: just os.sep).
388 # we're using a regex to manipulate a regex, so we need
389 # to escape the backslash twice
391 escaped
= r
'\1[^%s]' % sep
392 pattern_re
= re
.sub(r
'((?<!\\)(\\\\)*)\.', escaped
, pattern_re
)