Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes14kdownloads
egg_info.py717 linesDownload Raw Back to command
1"""setuptools.command.egg_info2 3Create a distribution's .egg-info directory and contents"""4 5from __future__ import annotations6 7import functools8import os9import re10import sys11import time12from collections.abc import Callable13 14import packaging15import packaging.requirements16import packaging.version17 18import setuptools.unicode_utils as unicode_utils19from setuptools import Command20from setuptools.command import bdist_egg21from setuptools.command.sdist import sdist, walk_revctrl22from setuptools.command.setopt import edit_config23from setuptools.glob import glob24 25from .. import _entry_points, _normalization26from .._importlib import metadata27from ..warnings import SetuptoolsDeprecationWarning28from . import _requirestxt29 30import distutils.errors31import distutils.filelist32from distutils import log33from distutils.errors import DistutilsInternalError34from distutils.filelist import FileList as _FileList35from distutils.util import convert_path36 37PY_MAJOR = f'{sys.version_info.major}.{sys.version_info.minor}'38 39 40def translate_pattern(glob):  # noqa: C901  # is too complex (14)  # FIXME41    """42    Translate a file path glob like '*.txt' in to a regular expression.43    This differs from fnmatch.translate which allows wildcards to match44    directory separators. It also knows about '**/' which matches any number of45    directories.46    """47    pat = ''48 49    # This will split on '/' within [character classes]. This is deliberate.50    chunks = glob.split(os.path.sep)51 52    sep = re.escape(os.sep)53    valid_char = f'[^{sep}]'54 55    for c, chunk in enumerate(chunks):56        last_chunk = c == len(chunks) - 157 58        # Chunks that are a literal ** are globstars. They match anything.59        if chunk == '**':60            if last_chunk:61                # Match anything if this is the last component62                pat += '.*'63            else:64                # Match '(name/)*'65                pat += f'(?:{valid_char}+{sep})*'66            continue  # Break here as the whole path component has been handled67 68        # Find any special characters in the remainder69        i = 070        chunk_len = len(chunk)71        while i < chunk_len:72            char = chunk[i]73            if char == '*':74                # Match any number of name characters75                pat += valid_char + '*'76            elif char == '?':77                # Match a name character78                pat += valid_char79            elif char == '[':80                # Character class81                inner_i = i + 182                # Skip initial !/] chars83                if inner_i < chunk_len and chunk[inner_i] == '!':84                    inner_i = inner_i + 185                if inner_i < chunk_len and chunk[inner_i] == ']':86                    inner_i = inner_i + 187 88                # Loop till the closing ] is found89                while inner_i < chunk_len and chunk[inner_i] != ']':90                    inner_i = inner_i + 191 92                if inner_i >= chunk_len:93                    # Got to the end of the string without finding a closing ]94                    # Do not treat this as a matching group, but as a literal [95                    pat += re.escape(char)96                else:97                    # Grab the insides of the [brackets]98                    inner = chunk[i + 1 : inner_i]99                    char_class = ''100 101                    # Class negation102                    if inner[0] == '!':103                        char_class = '^'104                        inner = inner[1:]105 106                    char_class += re.escape(inner)107                    pat += f'[{char_class}]'108 109                    # Skip to the end ]110                    i = inner_i111            else:112                pat += re.escape(char)113            i += 1114 115        # Join each chunk with the dir separator116        if not last_chunk:117            pat += sep118 119    pat += r'\Z'120    return re.compile(pat, flags=re.MULTILINE | re.DOTALL)121 122 123class InfoCommon:124    tag_build = None125    tag_date = None126 127    @property128    def name(self):129        return _normalization.safe_name(self.distribution.get_name())130 131    def tagged_version(self):132        tagged = self._maybe_tag(self.distribution.get_version())133        return _normalization.safe_version(tagged)134 135    def _maybe_tag(self, version):136        """137        egg_info may be called more than once for a distribution,138        in which case the version string already contains all tags.139        """140        return (141            version142            if self.vtags and self._already_tagged(version)143            else version + self.vtags144        )145 146    def _already_tagged(self, version: str) -> bool:147        # Depending on their format, tags may change with version normalization.148        # So in addition the regular tags, we have to search for the normalized ones.149        return version.endswith((self.vtags, self._safe_tags()))150 151    def _safe_tags(self) -> str:152        # To implement this we can rely on `safe_version` pretending to be version 0153        # followed by tags. Then we simply discard the starting 0 (fake version number)154        try:155            return _normalization.safe_version(f"0{self.vtags}")[1:]156        except packaging.version.InvalidVersion:157            return _normalization.safe_name(self.vtags.replace(' ', '.'))158 159    def tags(self) -> str:160        version = ''161        if self.tag_build:162            version += self.tag_build163        if self.tag_date:164            version += time.strftime("%Y%m%d")165        return version166 167    vtags = property(tags)168 169 170class egg_info(InfoCommon, Command):171    description = "create a distribution's .egg-info directory"172 173    user_options = [174        (175            'egg-base=',176            'e',177            "directory containing .egg-info directories"178            " [default: top of the source tree]",179        ),180        ('tag-date', 'd', "Add date stamp (e.g. 20050528) to version number"),181        ('tag-build=', 'b', "Specify explicit tag to add to version number"),182        ('no-date', 'D', "Don't include date stamp [default]"),183    ]184 185    boolean_options = ['tag-date']186    negative_opt = {187        'no-date': 'tag-date',188    }189 190    def initialize_options(self):191        self.egg_base = None192        self.egg_name = None193        self.egg_info = None194        self.egg_version = None195        self.ignore_egg_info_in_manifest = False196 197    ####################################198    # allow the 'tag_svn_revision' to be detected and199    # set, supporting sdists built on older Setuptools.200    @property201    def tag_svn_revision(self) -> int | None:202        pass203 204    @tag_svn_revision.setter205    def tag_svn_revision(self, value) -> None:206        pass207 208    ####################################209 210    def save_version_info(self, filename) -> None:211        """212        Materialize the value of date into the213        build tag. Install build keys in a deterministic order214        to avoid arbitrary reordering on subsequent builds.215        """216        # follow the order these keys would have been added217        # when PYTHONHASHSEED=0218        egg_info = dict(tag_build=self.tags(), tag_date=0)219        edit_config(filename, dict(egg_info=egg_info))220 221    def finalize_options(self) -> None:222        # Note: we need to capture the current value returned223        # by `self.tagged_version()`, so we can later update224        # `self.distribution.metadata.version` without225        # repercussions.226        self.egg_name = self.name227        self.egg_version = self.tagged_version()228        parsed_version = packaging.version.Version(self.egg_version)229 230        try:231            is_version = isinstance(parsed_version, packaging.version.Version)232            spec = "%s==%s" if is_version else "%s===%s"233            packaging.requirements.Requirement(spec % (self.egg_name, self.egg_version))234        except ValueError as e:235            raise distutils.errors.DistutilsOptionError(236                f"Invalid distribution name or version syntax: {self.egg_name}-{self.egg_version}"237            ) from e238 239        if self.egg_base is None:240            dirs = self.distribution.package_dir241            self.egg_base = (dirs or {}).get('', os.curdir)242 243        self.ensure_dirname('egg_base')244        self.egg_info = _normalization.filename_component(self.egg_name) + '.egg-info'245        if self.egg_base != os.curdir:246            self.egg_info = os.path.join(self.egg_base, self.egg_info)247 248        # Set package version for the benefit of dumber commands249        # (e.g. sdist, bdist_wininst, etc.)250        #251        self.distribution.metadata.version = self.egg_version252 253    def _get_egg_basename(self, py_version=PY_MAJOR, platform=None):254        """Compute filename of the output egg. Private API."""255        return _egg_basename(self.egg_name, self.egg_version, py_version, platform)256 257    def write_or_delete_file(self, what, filename, data, force: bool = False) -> None:258        """Write `data` to `filename` or delete if empty259 260        If `data` is non-empty, this routine is the same as ``write_file()``.261        If `data` is empty but not ``None``, this is the same as calling262        ``delete_file(filename)`.  If `data` is ``None``, then this is a no-op263        unless `filename` exists, in which case a warning is issued about the264        orphaned file (if `force` is false), or deleted (if `force` is true).265        """266        if data:267            self.write_file(what, filename, data)268        elif os.path.exists(filename):269            if data is None and not force:270                log.warn("%s not set in setup(), but %s exists", what, filename)271                return272            else:273                self.delete_file(filename)274 275    def write_file(self, what, filename, data) -> None:276        """Write `data` to `filename` (if not a dry run) after announcing it277 278        `what` is used in a log message to identify what is being written279        to the file.280        """281        log.info("writing %s to %s", what, filename)282        data = data.encode("utf-8")283        f = open(filename, 'wb')284        f.write(data)285        f.close()286 287    def delete_file(self, filename) -> None:288        """Delete `filename` (if not a dry run) after announcing it"""289        log.info("deleting %s", filename)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        item = item.removesuffix('\r')  # Fix older sdists built on Windows479        path = convert_path(item)480 481        if self._safe_path(path):482            self.files.append(path)483 484    def extend(self, paths) -> None:485        self.files.extend(filter(self._safe_path, paths))486 487    def _repair(self):488        """489        Replace self.files with only safe paths490 491        Because some owners of FileList manipulate the underlying492        ``files`` attribute directly, this method must be called to493        repair those paths.494        """495        self.files = list(filter(self._safe_path, self.files))496 497    def _safe_path(self, path):498        enc_warn = "'%s' not %s encodable -- skipping"499 500        # To avoid accidental trans-codings errors, first to unicode501        u_path = unicode_utils.filesys_decode(path)502        if u_path is None:503            log.warn(f"'{path}' in unexpected encoding -- skipping")504            return False505 506        # Must ensure utf-8 encodability507        utf8_path = unicode_utils.try_encode(u_path, "utf-8")508        if utf8_path is None:509            log.warn(enc_warn, path, 'utf-8')510            return False511 512        try:513            # ignore egg-info paths514            is_egg_info = ".egg-info" in u_path or b".egg-info" in utf8_path515            if self.ignore_egg_info_dir and is_egg_info:516                return False517            # accept is either way checks out518            if os.path.exists(u_path) or os.path.exists(utf8_path):519                return True520        # this will catch any encode errors decoding u_path521        except UnicodeEncodeError:522            log.warn(enc_warn, path, sys.getfilesystemencoding())523 524 525class manifest_maker(sdist):526    template = "MANIFEST.in"527 528    def initialize_options(self) -> None:529        self.use_defaults = True530        self.prune = True531        self.manifest_only = True532        self.force_manifest = True533        self.ignore_egg_info_dir = False534 535    def finalize_options(self) -> None:536        pass537 538    def run(self) -> None:539        self.filelist = FileList(ignore_egg_info_dir=self.ignore_egg_info_dir)540        if not os.path.exists(self.manifest):541            self.write_manifest()  # it must exist so it'll get in the list542        self.add_defaults()543        if os.path.exists(self.template):544            self.read_template()545        self.add_license_files()546        self._add_referenced_files()547        self.prune_file_list()548        self.filelist.sort()549        self.filelist.remove_duplicates()550        self.write_manifest()551 552    def _manifest_normalize(self, path):553        path = unicode_utils.filesys_decode(path)554        return path.replace(os.sep, '/')555 556    def write_manifest(self) -> None:557        """558        Write the file list in 'self.filelist' to the manifest file559        named by 'self.manifest'.560        """561        self.filelist._repair()562 563        # Now _repairs should encodability, but not unicode564        files = [self._manifest_normalize(f) for f in self.filelist.files]565        msg = f"writing manifest file '{self.manifest}'"566        self.execute(write_file, (self.manifest, files), msg)567 568    def warn(self, msg) -> None:569        if not self._should_suppress_warning(msg):570            sdist.warn(self, msg)571 572    @staticmethod573    def _should_suppress_warning(msg):574        """575        suppress missing-file warnings from sdist576        """577        return re.match(r"standard file .*not found", msg)578 579    def add_defaults(self) -> None:580        sdist.add_defaults(self)581        self.filelist.append(self.template)582        self.filelist.append(self.manifest)583        rcfiles = list(walk_revctrl())584        if rcfiles:585            self.filelist.extend(rcfiles)586        elif os.path.exists(self.manifest):587            self.read_manifest()588 589        if os.path.exists("setup.py"):590            # setup.py should be included by default, even if it's not591            # the script called to create the sdist592            self.filelist.append("setup.py")593 594        ei_cmd = self.get_finalized_command('egg_info')595        self.filelist.graft(ei_cmd.egg_info)596 597    def add_license_files(self) -> None:598        license_files = self.distribution.metadata.license_files or []599        for lf in license_files:600            log.info("adding license file '%s'", lf)601        self.filelist.extend(license_files)602 603    def _add_referenced_files(self):604        """Add files referenced by the config (e.g. `file:` directive) to filelist"""605        referenced = getattr(self.distribution, '_referenced_files', [])606        # ^-- fallback if dist comes from distutils or is a custom class607        for rf in referenced:608            log.debug("adding file referenced by config '%s'", rf)609        self.filelist.extend(referenced)610 611    def _safe_data_files(self, build_py):612        """613        The parent class implementation of this method614        (``sdist``) will try to include data files, which615        might cause recursion problems when616        ``include_package_data=True``.617 618        Therefore, avoid triggering any attempt of619        analyzing/building the manifest again.620        """621        if hasattr(build_py, 'get_data_files_without_manifest'):622            return build_py.get_data_files_without_manifest()623 624        SetuptoolsDeprecationWarning.emit(625            "`build_py` command does not inherit from setuptools' `build_py`.",626            """627            Custom 'build_py' does not implement 'get_data_files_without_manifest'.628            Please extend command classes from setuptools instead of distutils.629            """,630            see_url="https://peps.python.org/pep-0632/",631            # due_date not defined yet, old projects might still do it?632        )633        return build_py.get_data_files()634 635 636def write_file(filename, contents) -> None:637    """Create a file with the specified name and write 'contents' (a638    sequence of strings without line terminators) to it.639    """640    contents = "\n".join(contents)641 642    # assuming the contents has been vetted for utf-8 encoding643    contents = contents.encode("utf-8")644 645    with open(filename, "wb") as f:  # always write POSIX-style manifest646        f.write(contents)647 648 649def write_pkg_info(cmd, basename, filename) -> None:650    log.info("writing %s", filename)651    metadata = cmd.distribution.metadata652    metadata.version, oldver = cmd.egg_version, metadata.version653    metadata.name, oldname = cmd.egg_name, metadata.name654 655    try:656        metadata.write_pkg_info(cmd.egg_info)657    finally:658        metadata.name, metadata.version = oldname, oldver659 660    safe = getattr(cmd.distribution, 'zip_safe', None)661 662    bdist_egg.write_safety_flag(cmd.egg_info, safe)663 664 665def warn_depends_obsolete(cmd, basename, filename) -> None:666    """667    Unused: left to avoid errors when updating (from source) from <= 67.8.668    Old installations have a .dist-info directory with the entry-point669    ``depends.txt = setuptools.command.egg_info:warn_depends_obsolete``.670    This may trigger errors when running the first egg_info in build_meta.671    TODO: Remove this function in a version sufficiently > 68.672    """673 674 675# Export API used in entry_points676write_requirements = _requirestxt.write_requirements677write_setup_requirements = _requirestxt.write_setup_requirements678 679 680def write_toplevel_names(cmd, basename, filename) -> None:681    pkgs = dict.fromkeys([682        k.split('.', 1)[0] for k in cmd.distribution.iter_distribution_names()683    ])684    cmd.write_file("top-level names", filename, '\n'.join(sorted(pkgs)) + '\n')685 686 687def overwrite_arg(cmd, basename, filename) -> None:688    write_arg(cmd, basename, filename, True)689 690 691def write_arg(cmd, basename, filename, force: bool = False) -> None:692    argname = os.path.splitext(basename)[0]693    value = getattr(cmd.distribution, argname, None)694    if value is not None:695        value = '\n'.join(value) + '\n'696    cmd.write_or_delete_file(argname, filename, value, force)697 698 699def write_entries(cmd, basename, filename) -> None:700    eps = _entry_points.load(cmd.distribution.entry_points)701    defn = _entry_points.render(eps)702    cmd.write_or_delete_file('entry points', filename, defn, True)703 704 705def _egg_basename(egg_name, egg_version, py_version=None, platform=None):706    """Compute filename of the output egg. Private API."""707    name = _normalization.filename_component(egg_name)708    version = _normalization.filename_component(egg_version)709    egg = f"{name}-{version}-py{py_version or PY_MAJOR}"710    if platform:711        egg += f"-{platform}"712    return egg713 714 715class EggInfoDeprecationWarning(SetuptoolsDeprecationWarning):716    """Deprecated behavior warning for EggInfo, bypassing suppression."""717 
codekingpro/portable-devtools · Team Ai