CoolFace
Apppublic

Aluode/PerceptionLabPortable

sourceHugging Faceupdated 9mo agoView on Hugging Face
0likes
egg_info.py719 linesDownload Raw Back to command
1"""setuptools.command.egg_info2 3Create a distribution's .egg-info directory and contents"""4 5import functools6import os7import re8import sys9import time10from collections.abc import Callable11 12import packaging13import packaging.requirements14import packaging.version15 16import setuptools.unicode_utils as unicode_utils17from setuptools import Command18from setuptools.command import bdist_egg19from setuptools.command.sdist import sdist, walk_revctrl20from setuptools.command.setopt import edit_config21from setuptools.glob import glob22 23from .. import _entry_points, _normalization24from .._importlib import metadata25from ..warnings import SetuptoolsDeprecationWarning26from . import _requirestxt27 28import distutils.errors29import distutils.filelist30from distutils import log31from distutils.errors import DistutilsInternalError32from distutils.filelist import FileList as _FileList33from distutils.util import convert_path34 35PY_MAJOR = f'{sys.version_info.major}.{sys.version_info.minor}'36 37 38def translate_pattern(glob):  # noqa: C901  # is too complex (14)  # FIXME39    """40    Translate a file path glob like '*.txt' in to a regular expression.41    This differs from fnmatch.translate which allows wildcards to match42    directory separators. It also knows about '**/' which matches any number of43    directories.44    """45    pat = ''46 47    # This will split on '/' within [character classes]. This is deliberate.48    chunks = glob.split(os.path.sep)49 50    sep = re.escape(os.sep)51    valid_char = f'[^{sep}]'52 53    for c, chunk in enumerate(chunks):54        last_chunk = c == len(chunks) - 155 56        # Chunks that are a literal ** are globstars. They match anything.57        if chunk == '**':58            if last_chunk:59                # Match anything if this is the last component60                pat += '.*'61            else:62                # Match '(name/)*'63                pat += f'(?:{valid_char}+{sep})*'64            continue  # Break here as the whole path component has been handled65 66        # Find any special characters in the remainder67        i = 068        chunk_len = len(chunk)69        while i < chunk_len:70            char = chunk[i]71            if char == '*':72                # Match any number of name characters73                pat += valid_char + '*'74            elif char == '?':75                # Match a name character76                pat += valid_char77            elif char == '[':78                # Character class79                inner_i = i + 180                # Skip initial !/] chars81                if inner_i < chunk_len and chunk[inner_i] == '!':82                    inner_i = inner_i + 183                if inner_i < chunk_len and chunk[inner_i] == ']':84                    inner_i = inner_i + 185 86                # Loop till the closing ] is found87                while inner_i < chunk_len and chunk[inner_i] != ']':88                    inner_i = inner_i + 189 90                if inner_i >= chunk_len:91                    # Got to the end of the string without finding a closing ]92                    # Do not treat this as a matching group, but as a literal [93                    pat += re.escape(char)94                else:95                    # Grab the insides of the [brackets]96                    inner = chunk[i + 1 : inner_i]97                    char_class = ''98 99                    # Class negation100                    if inner[0] == '!':101                        char_class = '^'102                        inner = inner[1:]103 104                    char_class += re.escape(inner)105                    pat += f'[{char_class}]'106 107                    # Skip to the end ]108                    i = inner_i109            else:110                pat += re.escape(char)111            i += 1112 113        # Join each chunk with the dir separator114        if not last_chunk:115            pat += sep116 117    pat += r'\Z'118    return re.compile(pat, flags=re.MULTILINE | re.DOTALL)119 120 121class InfoCommon:122    tag_build = None123    tag_date = None124 125    @property126    def name(self):127        return _normalization.safe_name(self.distribution.get_name())128 129    def tagged_version(self):130        tagged = self._maybe_tag(self.distribution.get_version())131        return _normalization.safe_version(tagged)132 133    def _maybe_tag(self, version):134        """135        egg_info may be called more than once for a distribution,136        in which case the version string already contains all tags.137        """138        return (139            version140            if self.vtags and self._already_tagged(version)141            else version + self.vtags142        )143 144    def _already_tagged(self, version: str) -> bool:145        # Depending on their format, tags may change with version normalization.146        # So in addition the regular tags, we have to search for the normalized ones.147        return version.endswith(self.vtags) or version.endswith(self._safe_tags())148 149    def _safe_tags(self) -> str:150        # To implement this we can rely on `safe_version` pretending to be version 0151        # followed by tags. Then we simply discard the starting 0 (fake version number)152        try:153            return _normalization.safe_version(f"0{self.vtags}")[1:]154        except packaging.version.InvalidVersion:155            return _normalization.safe_name(self.vtags.replace(' ', '.'))156 157    def tags(self) -> str:158        version = ''159        if self.tag_build:160            version += self.tag_build161        if self.tag_date:162            version += time.strftime("%Y%m%d")163        return version164 165    vtags = property(tags)166 167 168class egg_info(InfoCommon, Command):169    description = "create a distribution's .egg-info directory"170 171    user_options = [172        (173            'egg-base=',174            'e',175            "directory containing .egg-info directories"176            " [default: top of the source tree]",177        ),178        ('tag-date', 'd', "Add date stamp (e.g. 20050528) to version number"),179        ('tag-build=', 'b', "Specify explicit tag to add to version number"),180        ('no-date', 'D', "Don't include date stamp [default]"),181    ]182 183    boolean_options = ['tag-date']184    negative_opt = {185        'no-date': 'tag-date',186    }187 188    def initialize_options(self):189        self.egg_base = None190        self.egg_name = None191        self.egg_info = None192        self.egg_version = None193        self.ignore_egg_info_in_manifest = False194 195    ####################################196    # allow the 'tag_svn_revision' to be detected and197    # set, supporting sdists built on older Setuptools.198    @property199    def tag_svn_revision(self) -> None:200        pass201 202    @tag_svn_revision.setter203    def tag_svn_revision(self, value):204        pass205 206    ####################################207 208    def save_version_info(self, filename) -> None:209        """210        Materialize the value of date into the211        build tag. Install build keys in a deterministic order212        to avoid arbitrary reordering on subsequent builds.213        """214        # follow the order these keys would have been added215        # when PYTHONHASHSEED=0216        egg_info = dict(tag_build=self.tags(), tag_date=0)217        edit_config(filename, dict(egg_info=egg_info))218 219    def finalize_options(self) -> None:220        # Note: we need to capture the current value returned221        # by `self.tagged_version()`, so we can later update222        # `self.distribution.metadata.version` without223        # repercussions.224        self.egg_name = self.name225        self.egg_version = self.tagged_version()226        parsed_version = packaging.version.Version(self.egg_version)227 228        try:229            is_version = isinstance(parsed_version, packaging.version.Version)230            spec = "%s==%s" if is_version else "%s===%s"231            packaging.requirements.Requirement(spec % (self.egg_name, self.egg_version))232        except ValueError as e:233            raise distutils.errors.DistutilsOptionError(234                f"Invalid distribution name or version syntax: {self.egg_name}-{self.egg_version}"235            ) from e236 237        if self.egg_base is None:238            dirs = self.distribution.package_dir239            self.egg_base = (dirs or {}).get('', os.curdir)240 241        self.ensure_dirname('egg_base')242        self.egg_info = _normalization.filename_component(self.egg_name) + '.egg-info'243        if self.egg_base != os.curdir:244            self.egg_info = os.path.join(self.egg_base, self.egg_info)245 246        # Set package version for the benefit of dumber commands247        # (e.g. sdist, bdist_wininst, etc.)248        #249        self.distribution.metadata.version = self.egg_version250 251    def _get_egg_basename(self, py_version=PY_MAJOR, platform=None):252        """Compute filename of the output egg. Private API."""253        return _egg_basename(self.egg_name, self.egg_version, py_version, platform)254 255    def write_or_delete_file(self, what, filename, data, force: bool = False) -> None:256        """Write `data` to `filename` or delete if empty257 258        If `data` is non-empty, this routine is the same as ``write_file()``.259        If `data` is empty but not ``None``, this is the same as calling260        ``delete_file(filename)`.  If `data` is ``None``, then this is a no-op261        unless `filename` exists, in which case a warning is issued about the262        orphaned file (if `force` is false), or deleted (if `force` is true).263        """264        if data:265            self.write_file(what, filename, data)266        elif os.path.exists(filename):267            if data is None and not force:268                log.warn("%s not set in setup(), but %s exists", what, filename)269                return270            else:271                self.delete_file(filename)272 273    def write_file(self, what, filename, data) -> None:274        """Write `data` to `filename` (if not a dry run) after announcing it275 276        `what` is used in a log message to identify what is being written277        to the file.278        """279        log.info("writing %s to %s", what, filename)280        data = data.encode("utf-8")281        if not self.dry_run:282            f = open(filename, 'wb')283            f.write(data)284            f.close()285 286    def delete_file(self, filename) -> None:287        """Delete `filename` (if not a dry run) after announcing it"""288        log.info("deleting %s", filename)289        if not self.dry_run:290            os.unlink(filename)291 292    def run(self) -> None:293        # Pre-load to avoid iterating over entry-points while an empty .egg-info294        # exists in sys.path. See pypa/pyproject-hooks#206295        writers = list(metadata.entry_points(group='egg_info.writers'))296 297        self.mkpath(self.egg_info)298        try:299            os.utime(self.egg_info, None)300        except OSError as e:301            msg = f"Cannot update time stamp of directory '{self.egg_info}'"302            raise distutils.errors.DistutilsFileError(msg) from e303        for ep in writers:304            writer = ep.load()305            writer(self, ep.name, os.path.join(self.egg_info, ep.name))306 307        # Get rid of native_libs.txt if it was put there by older bdist_egg308        nl = os.path.join(self.egg_info, "native_libs.txt")309        if os.path.exists(nl):310            self.delete_file(nl)311 312        self.find_sources()313 314    def find_sources(self) -> None:315        """Generate SOURCES.txt manifest file"""316        manifest_filename = os.path.join(self.egg_info, "SOURCES.txt")317        mm = manifest_maker(self.distribution)318        mm.ignore_egg_info_dir = self.ignore_egg_info_in_manifest319        mm.manifest = manifest_filename320        mm.run()321        self.filelist = mm.filelist322 323 324class FileList(_FileList):325    # Implementations of the various MANIFEST.in commands326 327    def __init__(328        self, warn=None, debug_print=None, ignore_egg_info_dir: bool = False329    ) -> None:330        super().__init__(warn, debug_print)331        self.ignore_egg_info_dir = ignore_egg_info_dir332 333    def process_template_line(self, line) -> None:334        # Parse the line: split it up, make sure the right number of words335        # is there, and return the relevant words.  'action' is always336        # defined: it's the first word of the line.  Which of the other337        # three are defined depends on the action; it'll be either338        # patterns, (dir and patterns), or (dir_pattern).339        (action, patterns, dir, dir_pattern) = self._parse_template_line(line)340 341        action_map: dict[str, Callable] = {342            'include': self.include,343            'exclude': self.exclude,344            'global-include': self.global_include,345            'global-exclude': self.global_exclude,346            'recursive-include': functools.partial(347                self.recursive_include,348                dir,349            ),350            'recursive-exclude': functools.partial(351                self.recursive_exclude,352                dir,353            ),354            'graft': self.graft,355            'prune': self.prune,356        }357        log_map = {358            'include': "warning: no files found matching '%s'",359            'exclude': ("warning: no previously-included files found matching '%s'"),360            'global-include': (361                "warning: no files found matching '%s' anywhere in distribution"362            ),363            'global-exclude': (364                "warning: no previously-included files matching "365                "'%s' found anywhere in distribution"366            ),367            'recursive-include': (368                "warning: no files found matching '%s' under directory '%s'"369            ),370            'recursive-exclude': (371                "warning: no previously-included files matching "372                "'%s' found under directory '%s'"373            ),374            'graft': "warning: no directories found matching '%s'",375            'prune': "no previously-included directories found matching '%s'",376        }377 378        try:379            process_action = action_map[action]380        except KeyError:381            msg = f"Invalid MANIFEST.in: unknown action {action!r} in {line!r}"382            raise DistutilsInternalError(msg) from None383 384        # OK, now we know that the action is valid and we have the385        # right number of words on the line for that action -- so we386        # can proceed with minimal error-checking.387 388        action_is_recursive = action.startswith('recursive-')389        if action in {'graft', 'prune'}:390            patterns = [dir_pattern]391        extra_log_args = (dir,) if action_is_recursive else ()392        log_tmpl = log_map[action]393 394        self.debug_print(395            ' '.join(396                [action] + ([dir] if action_is_recursive else []) + patterns,397            )398        )399        for pattern in patterns:400            if not process_action(pattern):401                log.warn(log_tmpl, pattern, *extra_log_args)402 403    def _remove_files(self, predicate):404        """405        Remove all files from the file list that match the predicate.406        Return True if any matching files were removed407        """408        found = False409        for i in range(len(self.files) - 1, -1, -1):410            if predicate(self.files[i]):411                self.debug_print(" removing " + self.files[i])412                del self.files[i]413                found = True414        return found415 416    def include(self, pattern):417        """Include files that match 'pattern'."""418        found = [f for f in glob(pattern) if not os.path.isdir(f)]419        self.extend(found)420        return bool(found)421 422    def exclude(self, pattern):423        """Exclude files that match 'pattern'."""424        match = translate_pattern(pattern)425        return self._remove_files(match.match)426 427    def recursive_include(self, dir, pattern):428        """429        Include all files anywhere in 'dir/' that match the pattern.430        """431        full_pattern = os.path.join(dir, '**', pattern)432        found = [f for f in glob(full_pattern, recursive=True) if not os.path.isdir(f)]433        self.extend(found)434        return bool(found)435 436    def recursive_exclude(self, dir, pattern):437        """438        Exclude any file anywhere in 'dir/' that match the pattern.439        """440        match = translate_pattern(os.path.join(dir, '**', pattern))441        return self._remove_files(match.match)442 443    def graft(self, dir):444        """Include all files from 'dir/'."""445        found = [446            item447            for match_dir in glob(dir)448            for item in distutils.filelist.findall(match_dir)449        ]450        self.extend(found)451        return bool(found)452 453    def prune(self, dir):454        """Filter out files from 'dir/'."""455        match = translate_pattern(os.path.join(dir, '**'))456        return self._remove_files(match.match)457 458    def global_include(self, pattern):459        """460        Include all files anywhere in the current directory that match the461        pattern. This is very inefficient on large file trees.462        """463        if self.allfiles is None:464            self.findall()465        match = translate_pattern(os.path.join('**', pattern))466        found = [f for f in self.allfiles if match.match(f)]467        self.extend(found)468        return bool(found)469 470    def global_exclude(self, pattern):471        """472        Exclude all files anywhere that match the pattern.473        """474        match = translate_pattern(os.path.join('**', pattern))475        return self._remove_files(match.match)476 477    def append(self, item) -> None:478        if item.endswith('\r'):  # Fix older sdists built on Windows479            item = item[:-1]480        path = convert_path(item)481 482        if self._safe_path(path):483            self.files.append(path)484 485    def extend(self, paths) -> None:486        self.files.extend(filter(self._safe_path, paths))487 488    def _repair(self):489        """490        Replace self.files with only safe paths491 492        Because some owners of FileList manipulate the underlying493        ``files`` attribute directly, this method must be called to494        repair those paths.495        """496        self.files = list(filter(self._safe_path, self.files))497 498    def _safe_path(self, path):499        enc_warn = "'%s' not %s encodable -- skipping"500 501        # To avoid accidental trans-codings errors, first to unicode502        u_path = unicode_utils.filesys_decode(path)503        if u_path is None:504            log.warn(f"'{path}' in unexpected encoding -- skipping")505            return False506 507        # Must ensure utf-8 encodability508        utf8_path = unicode_utils.try_encode(u_path, "utf-8")509        if utf8_path is None:510            log.warn(enc_warn, path, 'utf-8')511            return False512 513        try:514            # ignore egg-info paths515            is_egg_info = ".egg-info" in u_path or b".egg-info" in utf8_path516            if self.ignore_egg_info_dir and is_egg_info:517                return False518            # accept is either way checks out519            if os.path.exists(u_path) or os.path.exists(utf8_path):520                return True521        # this will catch any encode errors decoding u_path522        except UnicodeEncodeError:523            log.warn(enc_warn, path, sys.getfilesystemencoding())524 525 526class manifest_maker(sdist):527    template = "MANIFEST.in"528 529    def initialize_options(self) -> None:530        self.use_defaults = True531        self.prune = True532        self.manifest_only = True533        self.force_manifest = True534        self.ignore_egg_info_dir = False535 536    def finalize_options(self) -> None:537        pass538 539    def run(self) -> None:540        self.filelist = FileList(ignore_egg_info_dir=self.ignore_egg_info_dir)541        if not os.path.exists(self.manifest):542            self.write_manifest()  # it must exist so it'll get in the list543        self.add_defaults()544        if os.path.exists(self.template):545            self.read_template()546        self.add_license_files()547        self._add_referenced_files()548        self.prune_file_list()549        self.filelist.sort()550        self.filelist.remove_duplicates()551        self.write_manifest()552 553    def _manifest_normalize(self, path):554        path = unicode_utils.filesys_decode(path)555        return path.replace(os.sep, '/')556 557    def write_manifest(self) -> None:558        """559        Write the file list in 'self.filelist' to the manifest file560        named by 'self.manifest'.561        """562        self.filelist._repair()563 564        # Now _repairs should encodability, but not unicode565        files = [self._manifest_normalize(f) for f in self.filelist.files]566        msg = f"writing manifest file '{self.manifest}'"567        self.execute(write_file, (self.manifest, files), msg)568 569    def warn(self, msg) -> None:570        if not self._should_suppress_warning(msg):571            sdist.warn(self, msg)572 573    @staticmethod574    def _should_suppress_warning(msg):575        """576        suppress missing-file warnings from sdist577        """578        return re.match(r"standard file .*not found", msg)579 580    def add_defaults(self) -> None:581        sdist.add_defaults(self)582        self.filelist.append(self.template)583        self.filelist.append(self.manifest)584        rcfiles = list(walk_revctrl())585        if rcfiles:586            self.filelist.extend(rcfiles)587        elif os.path.exists(self.manifest):588            self.read_manifest()589 590        if os.path.exists("setup.py"):591            # setup.py should be included by default, even if it's not592            # the script called to create the sdist593            self.filelist.append("setup.py")594 595        ei_cmd = self.get_finalized_command('egg_info')596        self.filelist.graft(ei_cmd.egg_info)597 598    def add_license_files(self) -> None:599        license_files = self.distribution.metadata.license_files or []600        for lf in license_files:601            log.info("adding license file '%s'", lf)602        self.filelist.extend(license_files)603 604    def _add_referenced_files(self):605        """Add files referenced by the config (e.g. `file:` directive) to filelist"""606        referenced = getattr(self.distribution, '_referenced_files', [])607        # ^-- fallback if dist comes from distutils or is a custom class608        for rf in referenced:609            log.debug("adding file referenced by config '%s'", rf)610        self.filelist.extend(referenced)611 612    def _safe_data_files(self, build_py):613        """614        The parent class implementation of this method615        (``sdist``) will try to include data files, which616        might cause recursion problems when617        ``include_package_data=True``.618 619        Therefore, avoid triggering any attempt of620        analyzing/building the manifest again.621        """622        if hasattr(build_py, 'get_data_files_without_manifest'):623            return build_py.get_data_files_without_manifest()624 625        SetuptoolsDeprecationWarning.emit(626            "`build_py` command does not inherit from setuptools' `build_py`.",627            """628            Custom 'build_py' does not implement 'get_data_files_without_manifest'.629            Please extend command classes from setuptools instead of distutils.630            """,631            see_url="https://peps.python.org/pep-0632/",632            # due_date not defined yet, old projects might still do it?633        )634        return build_py.get_data_files()635 636 637def write_file(filename, contents) -> None:638    """Create a file with the specified name and write 'contents' (a639    sequence of strings without line terminators) to it.640    """641    contents = "\n".join(contents)642 643    # assuming the contents has been vetted for utf-8 encoding644    contents = contents.encode("utf-8")645 646    with open(filename, "wb") as f:  # always write POSIX-style manifest647        f.write(contents)648 649 650def write_pkg_info(cmd, basename, filename) -> None:651    log.info("writing %s", filename)652    if not cmd.dry_run:653        metadata = cmd.distribution.metadata654        metadata.version, oldver = cmd.egg_version, metadata.version655        metadata.name, oldname = cmd.egg_name, metadata.name656 657        try:658            metadata.write_pkg_info(cmd.egg_info)659        finally:660            metadata.name, metadata.version = oldname, oldver661 662        safe = getattr(cmd.distribution, 'zip_safe', None)663 664        bdist_egg.write_safety_flag(cmd.egg_info, safe)665 666 667def warn_depends_obsolete(cmd, basename, filename) -> None:668    """669    Unused: left to avoid errors when updating (from source) from <= 67.8.670    Old installations have a .dist-info directory with the entry-point671    ``depends.txt = setuptools.command.egg_info:warn_depends_obsolete``.672    This may trigger errors when running the first egg_info in build_meta.673    TODO: Remove this function in a version sufficiently > 68.674    """675 676 677# Export API used in entry_points678write_requirements = _requirestxt.write_requirements679write_setup_requirements = _requirestxt.write_setup_requirements680 681 682def write_toplevel_names(cmd, basename, filename) -> None:683    pkgs = dict.fromkeys([684        k.split('.', 1)[0] for k in cmd.distribution.iter_distribution_names()685    ])686    cmd.write_file("top-level names", filename, '\n'.join(sorted(pkgs)) + '\n')687 688 689def overwrite_arg(cmd, basename, filename) -> None:690    write_arg(cmd, basename, filename, True)691 692 693def write_arg(cmd, basename, filename, force: bool = False) -> None:694    argname = os.path.splitext(basename)[0]695    value = getattr(cmd.distribution, argname, None)696    if value is not None:697        value = '\n'.join(value) + '\n'698    cmd.write_or_delete_file(argname, filename, value, force)699 700 701def write_entries(cmd, basename, filename) -> None:702    eps = _entry_points.load(cmd.distribution.entry_points)703    defn = _entry_points.render(eps)704    cmd.write_or_delete_file('entry points', filename, defn, True)705 706 707def _egg_basename(egg_name, egg_version, py_version=None, platform=None):708    """Compute filename of the output egg. Private API."""709    name = _normalization.filename_component(egg_name)710    version = _normalization.filename_component(egg_version)711    egg = f"{name}-{version}-py{py_version or PY_MAJOR}"712    if platform:713        egg += f"-{platform}"714    return egg715 716 717class EggInfoDeprecationWarning(SetuptoolsDeprecationWarning):718    """Deprecated behavior warning for EggInfo, bypassing suppression."""719 
Aluode/PerceptionLabPortable · CoolFace