CoolFace
Apppublic

Aluode/PerceptionLabPortable

sourceHugging Faceupdated 9mo agoView on Hugging Face
0likes
filelist.py432 linesDownload Raw Back to _distutils
1"""distutils.filelist2 3Provides the FileList class, used for poking about the filesystem4and building lists of files.5"""6 7from __future__ import annotations8 9import fnmatch10import functools11import os12import re13from collections.abc import Iterable14from typing import Literal, overload15 16from ._log import log17from .errors import DistutilsInternalError, DistutilsTemplateError18from .util import convert_path19 20 21class FileList:22    """A list of files built by on exploring the filesystem and filtered by23    applying various patterns to what we find there.24 25    Instance attributes:26      dir27        directory from which files will be taken -- only used if28        'allfiles' not supplied to constructor29      files30        list of filenames currently being built/filtered/manipulated31      allfiles32        complete list of files under consideration (ie. without any33        filtering applied)34    """35 36    def __init__(self, warn: object = None, debug_print: object = None) -> None:37        # ignore argument to FileList, but keep them for backwards38        # compatibility39        self.allfiles: Iterable[str] | None = None40        self.files: list[str] = []41 42    def set_allfiles(self, allfiles: Iterable[str]) -> None:43        self.allfiles = allfiles44 45    def findall(self, dir: str | os.PathLike[str] = os.curdir) -> None:46        self.allfiles = findall(dir)47 48    def debug_print(self, msg: object) -> None:49        """Print 'msg' to stdout if the global DEBUG (taken from the50        DISTUTILS_DEBUG environment variable) flag is true.51        """52        from distutils.debug import DEBUG53 54        if DEBUG:55            print(msg)56 57    # Collection methods58 59    def append(self, item: str) -> None:60        self.files.append(item)61 62    def extend(self, items: Iterable[str]) -> None:63        self.files.extend(items)64 65    def sort(self) -> None:66        # Not a strict lexical sort!67        sortable_files = sorted(map(os.path.split, self.files))68        self.files = []69        for sort_tuple in sortable_files:70            self.files.append(os.path.join(*sort_tuple))71 72    # Other miscellaneous utility methods73 74    def remove_duplicates(self) -> None:75        # Assumes list has been sorted!76        for i in range(len(self.files) - 1, 0, -1):77            if self.files[i] == self.files[i - 1]:78                del self.files[i]79 80    # "File template" methods81 82    def _parse_template_line(self, line):83        words = line.split()84        action = words[0]85 86        patterns = dir = dir_pattern = None87 88        if action in ('include', 'exclude', 'global-include', 'global-exclude'):89            if len(words) < 2:90                raise DistutilsTemplateError(91                    f"'{action}' expects <pattern1> <pattern2> ..."92                )93            patterns = [convert_path(w) for w in words[1:]]94        elif action in ('recursive-include', 'recursive-exclude'):95            if len(words) < 3:96                raise DistutilsTemplateError(97                    f"'{action}' expects <dir> <pattern1> <pattern2> ..."98                )99            dir = convert_path(words[1])100            patterns = [convert_path(w) for w in words[2:]]101        elif action in ('graft', 'prune'):102            if len(words) != 2:103                raise DistutilsTemplateError(104                    f"'{action}' expects a single <dir_pattern>"105                )106            dir_pattern = convert_path(words[1])107        else:108            raise DistutilsTemplateError(f"unknown action '{action}'")109 110        return (action, patterns, dir, dir_pattern)111 112    def process_template_line(self, line: str) -> None:  # noqa: C901113        # Parse the line: split it up, make sure the right number of words114        # is there, and return the relevant words.  'action' is always115        # defined: it's the first word of the line.  Which of the other116        # three are defined depends on the action; it'll be either117        # patterns, (dir and patterns), or (dir_pattern).118        (action, patterns, dir, dir_pattern) = self._parse_template_line(line)119 120        # OK, now we know that the action is valid and we have the121        # right number of words on the line for that action -- so we122        # can proceed with minimal error-checking.123        if action == 'include':124            self.debug_print("include " + ' '.join(patterns))125            for pattern in patterns:126                if not self.include_pattern(pattern, anchor=True):127                    log.warning("warning: no files found matching '%s'", pattern)128 129        elif action == 'exclude':130            self.debug_print("exclude " + ' '.join(patterns))131            for pattern in patterns:132                if not self.exclude_pattern(pattern, anchor=True):133                    log.warning(134                        "warning: no previously-included files found matching '%s'",135                        pattern,136                    )137 138        elif action == 'global-include':139            self.debug_print("global-include " + ' '.join(patterns))140            for pattern in patterns:141                if not self.include_pattern(pattern, anchor=False):142                    log.warning(143                        (144                            "warning: no files found matching '%s' "145                            "anywhere in distribution"146                        ),147                        pattern,148                    )149 150        elif action == 'global-exclude':151            self.debug_print("global-exclude " + ' '.join(patterns))152            for pattern in patterns:153                if not self.exclude_pattern(pattern, anchor=False):154                    log.warning(155                        (156                            "warning: no previously-included files matching "157                            "'%s' found anywhere in distribution"158                        ),159                        pattern,160                    )161 162        elif action == 'recursive-include':163            self.debug_print("recursive-include {} {}".format(dir, ' '.join(patterns)))164            for pattern in patterns:165                if not self.include_pattern(pattern, prefix=dir):166                    msg = "warning: no files found matching '%s' under directory '%s'"167                    log.warning(msg, pattern, dir)168 169        elif action == 'recursive-exclude':170            self.debug_print("recursive-exclude {} {}".format(dir, ' '.join(patterns)))171            for pattern in patterns:172                if not self.exclude_pattern(pattern, prefix=dir):173                    log.warning(174                        (175                            "warning: no previously-included files matching "176                            "'%s' found under directory '%s'"177                        ),178                        pattern,179                        dir,180                    )181 182        elif action == 'graft':183            self.debug_print("graft " + dir_pattern)184            if not self.include_pattern(None, prefix=dir_pattern):185                log.warning("warning: no directories found matching '%s'", dir_pattern)186 187        elif action == 'prune':188            self.debug_print("prune " + dir_pattern)189            if not self.exclude_pattern(None, prefix=dir_pattern):190                log.warning(191                    ("no previously-included directories found matching '%s'"),192                    dir_pattern,193                )194        else:195            raise DistutilsInternalError(196                f"this cannot happen: invalid action '{action}'"197            )198 199    # Filtering/selection methods200    @overload201    def include_pattern(202        self,203        pattern: str,204        anchor: bool = True,205        prefix: str | None = None,206        is_regex: Literal[False] = False,207    ) -> bool: ...208    @overload209    def include_pattern(210        self,211        pattern: str | re.Pattern[str],212        anchor: bool = True,213        prefix: str | None = None,214        *,215        is_regex: Literal[True],216    ) -> bool: ...217    @overload218    def include_pattern(219        self,220        pattern: str | re.Pattern[str],221        anchor: bool,222        prefix: str | None,223        is_regex: Literal[True],224    ) -> bool: ...225    def include_pattern(226        self,227        pattern: str | re.Pattern,228        anchor: bool = True,229        prefix: str | None = None,230        is_regex: bool = False,231    ) -> bool:232        """Select strings (presumably filenames) from 'self.files' that233        match 'pattern', a Unix-style wildcard (glob) pattern.  Patterns234        are not quite the same as implemented by the 'fnmatch' module: '*'235        and '?'  match non-special characters, where "special" is platform-236        dependent: slash on Unix; colon, slash, and backslash on237        DOS/Windows; and colon on Mac OS.238 239        If 'anchor' is true (the default), then the pattern match is more240        stringent: "*.py" will match "foo.py" but not "foo/bar.py".  If241        'anchor' is false, both of these will match.242 243        If 'prefix' is supplied, then only filenames starting with 'prefix'244        (itself a pattern) and ending with 'pattern', with anything in between245        them, will match.  'anchor' is ignored in this case.246 247        If 'is_regex' is true, 'anchor' and 'prefix' are ignored, and248        'pattern' is assumed to be either a string containing a regex or a249        regex object -- no translation is done, the regex is just compiled250        and used as-is.251 252        Selected strings will be added to self.files.253 254        Return True if files are found, False otherwise.255        """256        # XXX docstring lying about what the special chars are?257        files_found = False258        pattern_re = translate_pattern(pattern, anchor, prefix, is_regex)259        self.debug_print(f"include_pattern: applying regex r'{pattern_re.pattern}'")260 261        # delayed loading of allfiles list262        if self.allfiles is None:263            self.findall()264 265        for name in self.allfiles:266            if pattern_re.search(name):267                self.debug_print(" adding " + name)268                self.files.append(name)269                files_found = True270        return files_found271 272    @overload273    def exclude_pattern(274        self,275        pattern: str,276        anchor: bool = True,277        prefix: str | None = None,278        is_regex: Literal[False] = False,279    ) -> bool: ...280    @overload281    def exclude_pattern(282        self,283        pattern: str | re.Pattern[str],284        anchor: bool = True,285        prefix: str | None = None,286        *,287        is_regex: Literal[True],288    ) -> bool: ...289    @overload290    def exclude_pattern(291        self,292        pattern: str | re.Pattern[str],293        anchor: bool,294        prefix: str | None,295        is_regex: Literal[True],296    ) -> bool: ...297    def exclude_pattern(298        self,299        pattern: str | re.Pattern,300        anchor: bool = True,301        prefix: str | None = None,302        is_regex: bool = False,303    ) -> bool:304        """Remove strings (presumably filenames) from 'files' that match305        'pattern'.  Other parameters are the same as for306        'include_pattern()', above.307        The list 'self.files' is modified in place.308        Return True if files are found, False otherwise.309        """310        files_found = False311        pattern_re = translate_pattern(pattern, anchor, prefix, is_regex)312        self.debug_print(f"exclude_pattern: applying regex r'{pattern_re.pattern}'")313        for i in range(len(self.files) - 1, -1, -1):314            if pattern_re.search(self.files[i]):315                self.debug_print(" removing " + self.files[i])316                del self.files[i]317                files_found = True318        return files_found319 320 321# Utility functions322 323 324def _find_all_simple(path):325    """326    Find all files under 'path'327    """328    all_unique = _UniqueDirs.filter(os.walk(path, followlinks=True))329    results = (330        os.path.join(base, file) for base, dirs, files in all_unique for file in files331    )332    return filter(os.path.isfile, results)333 334 335class _UniqueDirs(set):336    """337    Exclude previously-seen dirs from walk results,338    avoiding infinite recursion.339    Ref https://bugs.python.org/issue44497.340    """341 342    def __call__(self, walk_item):343        """344        Given an item from an os.walk result, determine345        if the item represents a unique dir for this instance346        and if not, prevent further traversal.347        """348        base, dirs, files = walk_item349        stat = os.stat(base)350        candidate = stat.st_dev, stat.st_ino351        found = candidate in self352        if found:353            del dirs[:]354        self.add(candidate)355        return not found356 357    @classmethod358    def filter(cls, items):359        return filter(cls(), items)360 361 362def findall(dir: str | os.PathLike[str] = os.curdir):363    """364    Find all files under 'dir' and return the list of full filenames.365    Unless dir is '.', return full filenames with dir prepended.366    """367    files = _find_all_simple(dir)368    if dir == os.curdir:369        make_rel = functools.partial(os.path.relpath, start=dir)370        files = map(make_rel, files)371    return list(files)372 373 374def glob_to_re(pattern):375    """Translate a shell-like glob pattern to a regular expression; return376    a string containing the regex.  Differs from 'fnmatch.translate()' in377    that '*' does not match "special characters" (which are378    platform-specific).379    """380    pattern_re = fnmatch.translate(pattern)381 382    # '?' and '*' in the glob pattern become '.' and '.*' in the RE, which383    # IMHO is wrong -- '?' and '*' aren't supposed to match slash in Unix,384    # and by extension they shouldn't match such "special characters" under385    # any OS.  So change all non-escaped dots in the RE to match any386    # character except the special characters (currently: just os.sep).387    sep = os.sep388    if os.sep == '\\':389        # we're using a regex to manipulate a regex, so we need390        # to escape the backslash twice391        sep = r'\\\\'392    escaped = rf'\1[^{sep}]'393    pattern_re = re.sub(r'((?<!\\)(\\\\)*)\.', escaped, pattern_re)394    return pattern_re395 396 397def translate_pattern(pattern, anchor=True, prefix=None, is_regex=False):398    """Translate a shell-like wildcard pattern to a compiled regular399    expression.  Return the compiled regex.  If 'is_regex' true,400    then 'pattern' is directly compiled to a regex (if it's a string)401    or just returned as-is (assumes it's a regex object).402    """403    if is_regex:404        if isinstance(pattern, str):405            return re.compile(pattern)406        else:407            return pattern408 409    # ditch start and end characters410    start, _, end = glob_to_re('_').partition('_')411 412    if pattern:413        pattern_re = glob_to_re(pattern)414        assert pattern_re.startswith(start) and pattern_re.endswith(end)415    else:416        pattern_re = ''417 418    if prefix is not None:419        prefix_re = glob_to_re(prefix)420        assert prefix_re.startswith(start) and prefix_re.endswith(end)421        prefix_re = prefix_re[len(start) : len(prefix_re) - len(end)]422        sep = os.sep423        if os.sep == '\\':424            sep = r'\\'425        pattern_re = pattern_re[len(start) : len(pattern_re) - len(end)]426        pattern_re = rf'{start}\A{prefix_re}{sep}.*{pattern_re}{end}'427    else:  # no prefix -- respect anchor flag428        if anchor:429            pattern_re = rf'{start}\A{pattern_re[len(start) :]}'430 431    return re.compile(pattern_re)432