3 Provides the FileList class, used for poking about the filesystem
4 and building lists of files.
7 # created 2000/07/17, Rene Liebscher (as template.py)
8 # most parts taken from commands/sdist.py
9 # renamed 2000/07/29 (to filelist.py) and officially added to
10 # the Distutils source, Greg Ward
18 from distutils
.util
import convert_path
19 from distutils
.errors
import DistutilsTemplateError
, DistutilsInternalError
20 from distutils
import log
24 """A list of files built by on exploring the filesystem and filtered by
25 applying various patterns to what we find there.
29 directory from which files will be taken -- only used if
30 'allfiles' not supplied to constructor
32 list of filenames currently being built/filtered/manipulated
34 complete list of files under consideration (ie. without any
41 # ignore argument to FileList, but keep them for backwards
47 def set_allfiles (self
, allfiles
):
48 self
.allfiles
= allfiles
50 def findall (self
, dir=os
.curdir
):
51 self
.allfiles
= findall(dir)
53 def debug_print (self
, msg
):
54 """Print 'msg' to stdout if the global DEBUG (taken from the
55 DISTUTILS_DEBUG environment variable) flag is true.
57 from distutils
.debug
import DEBUG
61 # -- List-like methods ---------------------------------------------
63 def append (self
, item
):
64 self
.files
.append(item
)
66 def extend (self
, items
):
67 self
.files
.extend(items
)
70 # Not a strict lexical sort!
71 sortable_files
= map(os
.path
.split
, self
.files
)
74 for sort_tuple
in sortable_files
:
75 self
.files
.append(apply(os
.path
.join
, sort_tuple
))
78 # -- Other miscellaneous utility methods ---------------------------
80 def remove_duplicates (self
):
81 # Assumes list has been sorted!
82 for i
in range(len(self
.files
) - 1, 0, -1):
83 if self
.files
[i
] == self
.files
[i
- 1]:
87 # -- "File template" methods ---------------------------------------
89 def _parse_template_line (self
, line
):
90 words
= string
.split(line
)
93 patterns
= dir = dir_pattern
= None
95 if action
in ('include', 'exclude',
96 'global-include', 'global-exclude'):
98 raise DistutilsTemplateError
, \
99 "'%s' expects <pattern1> <pattern2> ..." % action
101 patterns
= map(convert_path
, words
[1:])
103 elif action
in ('recursive-include', 'recursive-exclude'):
105 raise DistutilsTemplateError
, \
106 "'%s' expects <dir> <pattern1> <pattern2> ..." % action
108 dir = convert_path(words
[1])
109 patterns
= map(convert_path
, words
[2:])
111 elif action
in ('graft', 'prune'):
113 raise DistutilsTemplateError
, \
114 "'%s' expects a single <dir_pattern>" % action
116 dir_pattern
= convert_path(words
[1])
119 raise DistutilsTemplateError
, "unknown action '%s'" % action
121 return (action
, patterns
, dir, dir_pattern
)
123 # _parse_template_line ()
126 def process_template_line (self
, line
):
128 # Parse the line: split it up, make sure the right number of words
129 # is there, and return the relevant words. 'action' is always
130 # defined: it's the first word of the line. Which of the other
131 # three are defined depends on the action; it'll be either
132 # patterns, (dir and patterns), or (dir_pattern).
133 (action
, patterns
, dir, dir_pattern
) = self
._parse
_template
_line
(line
)
135 # OK, now we know that the action is valid and we have the
136 # right number of words on the line for that action -- so we
137 # can proceed with minimal error-checking.
138 if action
== 'include':
139 self
.debug_print("include " + string
.join(patterns
))
140 for pattern
in patterns
:
141 if not self
.include_pattern(pattern
, anchor
=1):
142 log
.warn("warning: no files found matching '%s'",
145 elif action
== 'exclude':
146 self
.debug_print("exclude " + string
.join(patterns
))
147 for pattern
in patterns
:
148 if not self
.exclude_pattern(pattern
, anchor
=1):
149 log
.warn(("warning: no previously-included files "
150 "found matching '%s'"), pattern
)
152 elif action
== 'global-include':
153 self
.debug_print("global-include " + string
.join(patterns
))
154 for pattern
in patterns
:
155 if not self
.include_pattern(pattern
, anchor
=0):
156 log
.warn(("warning: no files found matching '%s' " +
157 "anywhere in distribution"), pattern
)
159 elif action
== 'global-exclude':
160 self
.debug_print("global-exclude " + string
.join(patterns
))
161 for pattern
in patterns
:
162 if not self
.exclude_pattern(pattern
, anchor
=0):
163 log
.warn(("warning: no previously-included files matching "
164 "'%s' found anywhere in distribution"),
167 elif action
== 'recursive-include':
168 self
.debug_print("recursive-include %s %s" %
169 (dir, string
.join(patterns
)))
170 for pattern
in patterns
:
171 if not self
.include_pattern(pattern
, prefix
=dir):
172 log
.warn(("warngin: no files found matching '%s' " +
173 "under directory '%s'"),
176 elif action
== 'recursive-exclude':
177 self
.debug_print("recursive-exclude %s %s" %
178 (dir, string
.join(patterns
)))
179 for pattern
in patterns
:
180 if not self
.exclude_pattern(pattern
, prefix
=dir):
181 log
.warn(("warning: no previously-included files matching "
182 "'%s' found under directory '%s'"),
185 elif action
== 'graft':
186 self
.debug_print("graft " + dir_pattern
)
187 if not self
.include_pattern(None, prefix
=dir_pattern
):
188 log
.warn("warning: no directories found matching '%s'",
191 elif action
== 'prune':
192 self
.debug_print("prune " + dir_pattern
)
193 if not self
.exclude_pattern(None, prefix
=dir_pattern
):
194 log
.warn(("no previously-included directories found " +
195 "matching '%s'"), dir_pattern
)
197 raise DistutilsInternalError
, \
198 "this cannot happen: invalid action '%s'" % action
200 # process_template_line ()
203 # -- Filtering/selection methods -----------------------------------
205 def include_pattern (self
, pattern
,
206 anchor
=1, prefix
=None, is_regex
=0):
207 """Select strings (presumably filenames) from 'self.files' that
208 match 'pattern', a Unix-style wildcard (glob) pattern. Patterns
209 are not quite the same as implemented by the 'fnmatch' module: '*'
210 and '?' match non-special characters, where "special" is platform-
211 dependent: slash on Unix; colon, slash, and backslash on
212 DOS/Windows; and colon on Mac OS.
214 If 'anchor' is true (the default), then the pattern match is more
215 stringent: "*.py" will match "foo.py" but not "foo/bar.py". If
216 'anchor' is false, both of these will match.
218 If 'prefix' is supplied, then only filenames starting with 'prefix'
219 (itself a pattern) and ending with 'pattern', with anything in between
220 them, will match. 'anchor' is ignored in this case.
222 If 'is_regex' is true, 'anchor' and 'prefix' are ignored, and
223 'pattern' is assumed to be either a string containing a regex or a
224 regex object -- no translation is done, the regex is just compiled
227 Selected strings will be added to self.files.
229 Return 1 if files are found.
232 pattern_re
= translate_pattern(pattern
, anchor
, prefix
, is_regex
)
233 self
.debug_print("include_pattern: applying regex r'%s'" %
236 # delayed loading of allfiles list
237 if self
.allfiles
is None:
240 for name
in self
.allfiles
:
241 if pattern_re
.search(name
):
242 self
.debug_print(" adding " + name
)
243 self
.files
.append(name
)
251 def exclude_pattern (self
, pattern
,
252 anchor
=1, prefix
=None, is_regex
=0):
253 """Remove strings (presumably filenames) from 'files' that match
254 'pattern'. Other parameters are the same as for
255 'include_pattern()', above.
256 The list 'self.files' is modified in place.
257 Return 1 if files are found.
260 pattern_re
= translate_pattern(pattern
, anchor
, prefix
, is_regex
)
261 self
.debug_print("exclude_pattern: applying regex r'%s'" %
263 for i
in range(len(self
.files
)-1, -1, -1):
264 if pattern_re
.search(self
.files
[i
]):
265 self
.debug_print(" removing " + self
.files
[i
])
276 # ----------------------------------------------------------------------
279 def findall (dir = os
.curdir
):
280 """Find all files under 'dir' and return the list of full filenames
283 from stat
import ST_MODE
, S_ISREG
, S_ISDIR
, S_ISLNK
292 names
= os
.listdir(dir)
295 if dir != os
.curdir
: # avoid the dreaded "./" syndrome
296 fullname
= os
.path
.join(dir, name
)
300 # Avoid excess stat calls -- just one will do, thank you!
301 stat
= os
.stat(fullname
)
304 list.append(fullname
)
305 elif S_ISDIR(mode
) and not S_ISLNK(mode
):
311 def glob_to_re (pattern
):
312 """Translate a shell-like glob pattern to a regular expression; return
313 a string containing the regex. Differs from 'fnmatch.translate()' in
314 that '*' does not match "special characters" (which are
317 pattern_re
= fnmatch
.translate(pattern
)
319 # '?' and '*' in the glob pattern become '.' and '.*' in the RE, which
320 # IMHO is wrong -- '?' and '*' aren't supposed to match slash in Unix,
321 # and by extension they shouldn't match such "special characters" under
322 # any OS. So change all non-escaped dots in the RE to match any
323 # character except the special characters.
324 # XXX currently the "special characters" are just slash -- i.e. this is
326 pattern_re
= re
.sub(r
'(^|[^\\])\.', r
'\1[^/]', pattern_re
)
332 def translate_pattern (pattern
, anchor
=1, prefix
=None, is_regex
=0):
333 """Translate a shell-like wildcard pattern to a compiled regular
334 expression. Return the compiled regex. If 'is_regex' true,
335 then 'pattern' is directly compiled to a regex (if it's a string)
336 or just returned as-is (assumes it's a regex object).
339 if type(pattern
) is StringType
:
340 return re
.compile(pattern
)
345 pattern_re
= glob_to_re(pattern
)
349 if prefix
is not None:
350 prefix_re
= (glob_to_re(prefix
))[0:-1] # ditch trailing $
351 pattern_re
= "^" + os
.path
.join(prefix_re
, ".*" + pattern_re
)
352 else: # no prefix -- respect anchor flag
354 pattern_re
= "^" + pattern_re
356 return re
.compile(pattern_re
)
358 # translate_pattern ()