Path: blob/main/test/lib/python3.9/site-packages/setuptools/command/egg_info.py
4799 views
"""setuptools.command.egg_info12Create a distribution's .egg-info directory and contents"""34from distutils.filelist import FileList as _FileList5from distutils.errors import DistutilsInternalError6from distutils.util import convert_path7from distutils import log8import distutils.errors9import distutils.filelist10import functools11import os12import re13import sys14import io15import warnings16import time17import collections1819from .._importlib import metadata20from .. import _entry_points2122from setuptools import Command23from setuptools.command.sdist import sdist24from setuptools.command.sdist import walk_revctrl25from setuptools.command.setopt import edit_config26from setuptools.command import bdist_egg27from pkg_resources import (28Requirement, safe_name, parse_version,29safe_version, to_filename)30import setuptools.unicode_utils as unicode_utils31from setuptools.glob import glob3233from setuptools.extern import packaging34from setuptools.extern.jaraco.text import yield_lines35from setuptools import SetuptoolsDeprecationWarning363738def translate_pattern(glob): # noqa: C901 # is too complex (14) # FIXME39"""40Translate a file path glob like '*.txt' in to a regular expression.41This differs from fnmatch.translate which allows wildcards to match42directory separators. It also knows about '**/' which matches any number of43directories.44"""45pat = ''4647# This will split on '/' within [character classes]. This is deliberate.48chunks = glob.split(os.path.sep)4950sep = re.escape(os.sep)51valid_char = '[^%s]' % (sep,)5253for c, chunk in enumerate(chunks):54last_chunk = c == len(chunks) - 15556# Chunks that are a literal ** are globstars. They match anything.57if chunk == '**':58if last_chunk:59# Match anything if this is the last component60pat += '.*'61else:62# Match '(name/)*'63pat += '(?:%s+%s)*' % (valid_char, sep)64continue # Break here as the whole path component has been handled6566# Find any special characters in the remainder67i = 068chunk_len = len(chunk)69while i < chunk_len:70char = chunk[i]71if char == '*':72# Match any number of name characters73pat += valid_char + '*'74elif char == '?':75# Match a name character76pat += valid_char77elif char == '[':78# Character class79inner_i = i + 180# Skip initial !/] chars81if inner_i < chunk_len and chunk[inner_i] == '!':82inner_i = inner_i + 183if inner_i < chunk_len and chunk[inner_i] == ']':84inner_i = inner_i + 18586# Loop till the closing ] is found87while inner_i < chunk_len and chunk[inner_i] != ']':88inner_i = inner_i + 18990if 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 [93pat += re.escape(char)94else:95# Grab the insides of the [brackets]96inner = chunk[i + 1:inner_i]97char_class = ''9899# Class negation100if inner[0] == '!':101char_class = '^'102inner = inner[1:]103104char_class += re.escape(inner)105pat += '[%s]' % (char_class,)106107# Skip to the end ]108i = inner_i109else:110pat += re.escape(char)111i += 1112113# Join each chunk with the dir separator114if not last_chunk:115pat += sep116117pat += r'\Z'118return re.compile(pat, flags=re.MULTILINE | re.DOTALL)119120121class InfoCommon:122tag_build = None123tag_date = None124125@property126def name(self):127return safe_name(self.distribution.get_name())128129def tagged_version(self):130return safe_version(self._maybe_tag(self.distribution.get_version()))131132def _maybe_tag(self, version):133"""134egg_info may be called more than once for a distribution,135in which case the version string already contains all tags.136"""137return (138version if self.vtags and self._already_tagged(version)139else version + self.vtags140)141142def _already_tagged(self, version: str) -> bool:143# Depending on their format, tags may change with version normalization.144# So in addition the regular tags, we have to search for the normalized ones.145return version.endswith(self.vtags) or version.endswith(self._safe_tags())146147def _safe_tags(self) -> str:148# To implement this we can rely on `safe_version` pretending to be version 0149# followed by tags. Then we simply discard the starting 0 (fake version number)150return safe_version(f"0{self.vtags}")[1:]151152def tags(self) -> str:153version = ''154if self.tag_build:155version += self.tag_build156if self.tag_date:157version += time.strftime("-%Y%m%d")158return version159vtags = property(tags)160161162class egg_info(InfoCommon, Command):163description = "create a distribution's .egg-info directory"164165user_options = [166('egg-base=', 'e', "directory containing .egg-info directories"167" (default: top of the source tree)"),168('tag-date', 'd', "Add date stamp (e.g. 20050528) to version number"),169('tag-build=', 'b', "Specify explicit tag to add to version number"),170('no-date', 'D', "Don't include date stamp [default]"),171]172173boolean_options = ['tag-date']174negative_opt = {175'no-date': 'tag-date',176}177178def initialize_options(self):179self.egg_base = None180self.egg_name = None181self.egg_info = None182self.egg_version = None183self.broken_egg_info = False184185####################################186# allow the 'tag_svn_revision' to be detected and187# set, supporting sdists built on older Setuptools.188@property189def tag_svn_revision(self):190pass191192@tag_svn_revision.setter193def tag_svn_revision(self, value):194pass195####################################196197def save_version_info(self, filename):198"""199Materialize the value of date into the200build tag. Install build keys in a deterministic order201to avoid arbitrary reordering on subsequent builds.202"""203egg_info = collections.OrderedDict()204# follow the order these keys would have been added205# when PYTHONHASHSEED=0206egg_info['tag_build'] = self.tags()207egg_info['tag_date'] = 0208edit_config(filename, dict(egg_info=egg_info))209210def finalize_options(self):211# Note: we need to capture the current value returned212# by `self.tagged_version()`, so we can later update213# `self.distribution.metadata.version` without214# repercussions.215self.egg_name = self.name216self.egg_version = self.tagged_version()217parsed_version = parse_version(self.egg_version)218219try:220is_version = isinstance(parsed_version, packaging.version.Version)221spec = "%s==%s" if is_version else "%s===%s"222Requirement(spec % (self.egg_name, self.egg_version))223except ValueError as e:224raise distutils.errors.DistutilsOptionError(225"Invalid distribution name or version syntax: %s-%s" %226(self.egg_name, self.egg_version)227) from e228229if self.egg_base is None:230dirs = self.distribution.package_dir231self.egg_base = (dirs or {}).get('', os.curdir)232233self.ensure_dirname('egg_base')234self.egg_info = to_filename(self.egg_name) + '.egg-info'235if self.egg_base != os.curdir:236self.egg_info = os.path.join(self.egg_base, self.egg_info)237if '-' in self.egg_name:238self.check_broken_egg_info()239240# Set package version for the benefit of dumber commands241# (e.g. sdist, bdist_wininst, etc.)242#243self.distribution.metadata.version = self.egg_version244245# If we bootstrapped around the lack of a PKG-INFO, as might be the246# case in a fresh checkout, make sure that any special tags get added247# to the version info248#249pd = self.distribution._patched_dist250if pd is not None and pd.key == self.egg_name.lower():251pd._version = self.egg_version252pd._parsed_version = parse_version(self.egg_version)253self.distribution._patched_dist = None254255def write_or_delete_file(self, what, filename, data, force=False):256"""Write `data` to `filename` or delete if empty257258If `data` is non-empty, this routine is the same as ``write_file()``.259If `data` is empty but not ``None``, this is the same as calling260``delete_file(filename)`. If `data` is ``None``, then this is a no-op261unless `filename` exists, in which case a warning is issued about the262orphaned file (if `force` is false), or deleted (if `force` is true).263"""264if data:265self.write_file(what, filename, data)266elif os.path.exists(filename):267if data is None and not force:268log.warn(269"%s not set in setup(), but %s exists", what, filename270)271return272else:273self.delete_file(filename)274275def write_file(self, what, filename, data):276"""Write `data` to `filename` (if not a dry run) after announcing it277278`what` is used in a log message to identify what is being written279to the file.280"""281log.info("writing %s to %s", what, filename)282data = data.encode("utf-8")283if not self.dry_run:284f = open(filename, 'wb')285f.write(data)286f.close()287288def delete_file(self, filename):289"""Delete `filename` (if not a dry run) after announcing it"""290log.info("deleting %s", filename)291if not self.dry_run:292os.unlink(filename)293294def run(self):295self.mkpath(self.egg_info)296os.utime(self.egg_info, None)297for ep in metadata.entry_points(group='egg_info.writers'):298self.distribution._install_dependencies(ep)299writer = ep.load()300writer(self, ep.name, os.path.join(self.egg_info, ep.name))301302# Get rid of native_libs.txt if it was put there by older bdist_egg303nl = os.path.join(self.egg_info, "native_libs.txt")304if os.path.exists(nl):305self.delete_file(nl)306307self.find_sources()308309def find_sources(self):310"""Generate SOURCES.txt manifest file"""311manifest_filename = os.path.join(self.egg_info, "SOURCES.txt")312mm = manifest_maker(self.distribution)313mm.manifest = manifest_filename314mm.run()315self.filelist = mm.filelist316317def check_broken_egg_info(self):318bei = self.egg_name + '.egg-info'319if self.egg_base != os.curdir:320bei = os.path.join(self.egg_base, bei)321if os.path.exists(bei):322log.warn(323"-" * 78 + '\n'324"Note: Your current .egg-info directory has a '-' in its name;"325'\nthis will not work correctly with "setup.py develop".\n\n'326'Please rename %s to %s to correct this problem.\n' + '-' * 78,327bei, self.egg_info328)329self.broken_egg_info = self.egg_info330self.egg_info = bei # make it work for now331332333class FileList(_FileList):334# Implementations of the various MANIFEST.in commands335336def process_template_line(self, line):337# Parse the line: split it up, make sure the right number of words338# is there, and return the relevant words. 'action' is always339# defined: it's the first word of the line. Which of the other340# three are defined depends on the action; it'll be either341# patterns, (dir and patterns), or (dir_pattern).342(action, patterns, dir, dir_pattern) = self._parse_template_line(line)343344action_map = {345'include': self.include,346'exclude': self.exclude,347'global-include': self.global_include,348'global-exclude': self.global_exclude,349'recursive-include': functools.partial(350self.recursive_include, dir,351),352'recursive-exclude': functools.partial(353self.recursive_exclude, dir,354),355'graft': self.graft,356'prune': self.prune,357}358log_map = {359'include': "warning: no files found matching '%s'",360'exclude': (361"warning: no previously-included files found "362"matching '%s'"363),364'global-include': (365"warning: no files found matching '%s' "366"anywhere in distribution"367),368'global-exclude': (369"warning: no previously-included files matching "370"'%s' found anywhere in distribution"371),372'recursive-include': (373"warning: no files found matching '%s' "374"under directory '%s'"375),376'recursive-exclude': (377"warning: no previously-included files matching "378"'%s' found under directory '%s'"379),380'graft': "warning: no directories found matching '%s'",381'prune': "no previously-included directories found matching '%s'",382}383384try:385process_action = action_map[action]386except KeyError:387raise DistutilsInternalError(388"this cannot happen: invalid action '{action!s}'".389format(action=action),390)391392# OK, now we know that the action is valid and we have the393# right number of words on the line for that action -- so we394# can proceed with minimal error-checking.395396action_is_recursive = action.startswith('recursive-')397if action in {'graft', 'prune'}:398patterns = [dir_pattern]399extra_log_args = (dir, ) if action_is_recursive else ()400log_tmpl = log_map[action]401402self.debug_print(403' '.join(404[action] +405([dir] if action_is_recursive else []) +406patterns,407)408)409for pattern in patterns:410if not process_action(pattern):411log.warn(log_tmpl, pattern, *extra_log_args)412413def _remove_files(self, predicate):414"""415Remove all files from the file list that match the predicate.416Return True if any matching files were removed417"""418found = False419for i in range(len(self.files) - 1, -1, -1):420if predicate(self.files[i]):421self.debug_print(" removing " + self.files[i])422del self.files[i]423found = True424return found425426def include(self, pattern):427"""Include files that match 'pattern'."""428found = [f for f in glob(pattern) if not os.path.isdir(f)]429self.extend(found)430return bool(found)431432def exclude(self, pattern):433"""Exclude files that match 'pattern'."""434match = translate_pattern(pattern)435return self._remove_files(match.match)436437def recursive_include(self, dir, pattern):438"""439Include all files anywhere in 'dir/' that match the pattern.440"""441full_pattern = os.path.join(dir, '**', pattern)442found = [f for f in glob(full_pattern, recursive=True)443if not os.path.isdir(f)]444self.extend(found)445return bool(found)446447def recursive_exclude(self, dir, pattern):448"""449Exclude any file anywhere in 'dir/' that match the pattern.450"""451match = translate_pattern(os.path.join(dir, '**', pattern))452return self._remove_files(match.match)453454def graft(self, dir):455"""Include all files from 'dir/'."""456found = [457item458for match_dir in glob(dir)459for item in distutils.filelist.findall(match_dir)460]461self.extend(found)462return bool(found)463464def prune(self, dir):465"""Filter out files from 'dir/'."""466match = translate_pattern(os.path.join(dir, '**'))467return self._remove_files(match.match)468469def global_include(self, pattern):470"""471Include all files anywhere in the current directory that match the472pattern. This is very inefficient on large file trees.473"""474if self.allfiles is None:475self.findall()476match = translate_pattern(os.path.join('**', pattern))477found = [f for f in self.allfiles if match.match(f)]478self.extend(found)479return bool(found)480481def global_exclude(self, pattern):482"""483Exclude all files anywhere that match the pattern.484"""485match = translate_pattern(os.path.join('**', pattern))486return self._remove_files(match.match)487488def append(self, item):489if item.endswith('\r'): # Fix older sdists built on Windows490item = item[:-1]491path = convert_path(item)492493if self._safe_path(path):494self.files.append(path)495496def extend(self, paths):497self.files.extend(filter(self._safe_path, paths))498499def _repair(self):500"""501Replace self.files with only safe paths502503Because some owners of FileList manipulate the underlying504``files`` attribute directly, this method must be called to505repair those paths.506"""507self.files = list(filter(self._safe_path, self.files))508509def _safe_path(self, path):510enc_warn = "'%s' not %s encodable -- skipping"511512# To avoid accidental trans-codings errors, first to unicode513u_path = unicode_utils.filesys_decode(path)514if u_path is None:515log.warn("'%s' in unexpected encoding -- skipping" % path)516return False517518# Must ensure utf-8 encodability519utf8_path = unicode_utils.try_encode(u_path, "utf-8")520if utf8_path is None:521log.warn(enc_warn, path, 'utf-8')522return False523524try:525# accept is either way checks out526if os.path.exists(u_path) or os.path.exists(utf8_path):527return True528# this will catch any encode errors decoding u_path529except UnicodeEncodeError:530log.warn(enc_warn, path, sys.getfilesystemencoding())531532533class manifest_maker(sdist):534template = "MANIFEST.in"535536def initialize_options(self):537self.use_defaults = 1538self.prune = 1539self.manifest_only = 1540self.force_manifest = 1541542def finalize_options(self):543pass544545def run(self):546self.filelist = FileList()547if not os.path.exists(self.manifest):548self.write_manifest() # it must exist so it'll get in the list549self.add_defaults()550if os.path.exists(self.template):551self.read_template()552self.add_license_files()553self.prune_file_list()554self.filelist.sort()555self.filelist.remove_duplicates()556self.write_manifest()557558def _manifest_normalize(self, path):559path = unicode_utils.filesys_decode(path)560return path.replace(os.sep, '/')561562def write_manifest(self):563"""564Write the file list in 'self.filelist' to the manifest file565named by 'self.manifest'.566"""567self.filelist._repair()568569# Now _repairs should encodability, but not unicode570files = [self._manifest_normalize(f) for f in self.filelist.files]571msg = "writing manifest file '%s'" % self.manifest572self.execute(write_file, (self.manifest, files), msg)573574def warn(self, msg):575if not self._should_suppress_warning(msg):576sdist.warn(self, msg)577578@staticmethod579def _should_suppress_warning(msg):580"""581suppress missing-file warnings from sdist582"""583return re.match(r"standard file .*not found", msg)584585def add_defaults(self):586sdist.add_defaults(self)587self.filelist.append(self.template)588self.filelist.append(self.manifest)589rcfiles = list(walk_revctrl())590if rcfiles:591self.filelist.extend(rcfiles)592elif os.path.exists(self.manifest):593self.read_manifest()594595if os.path.exists("setup.py"):596# setup.py should be included by default, even if it's not597# the script called to create the sdist598self.filelist.append("setup.py")599600ei_cmd = self.get_finalized_command('egg_info')601self.filelist.graft(ei_cmd.egg_info)602603def add_license_files(self):604license_files = self.distribution.metadata.license_files or []605for lf in license_files:606log.info("adding license file '%s'", lf)607pass608self.filelist.extend(license_files)609610def prune_file_list(self):611build = self.get_finalized_command('build')612base_dir = self.distribution.get_fullname()613self.filelist.prune(build.build_base)614self.filelist.prune(base_dir)615sep = re.escape(os.sep)616self.filelist.exclude_pattern(r'(^|' + sep + r')(RCS|CVS|\.svn)' + sep,617is_regex=1)618619def _safe_data_files(self, build_py):620"""621The parent class implementation of this method622(``sdist``) will try to include data files, which623might cause recursion problems when624``include_package_data=True``.625626Therefore, avoid triggering any attempt of627analyzing/building the manifest again.628"""629if hasattr(build_py, 'get_data_files_without_manifest'):630return build_py.get_data_files_without_manifest()631632warnings.warn(633"Custom 'build_py' does not implement "634"'get_data_files_without_manifest'.\nPlease extend command classes"635" from setuptools instead of distutils.",636SetuptoolsDeprecationWarning637)638return build_py.get_data_files()639640641def write_file(filename, contents):642"""Create a file with the specified name and write 'contents' (a643sequence of strings without line terminators) to it.644"""645contents = "\n".join(contents)646647# assuming the contents has been vetted for utf-8 encoding648contents = contents.encode("utf-8")649650with open(filename, "wb") as f: # always write POSIX-style manifest651f.write(contents)652653654def write_pkg_info(cmd, basename, filename):655log.info("writing %s", filename)656if not cmd.dry_run:657metadata = cmd.distribution.metadata658metadata.version, oldver = cmd.egg_version, metadata.version659metadata.name, oldname = cmd.egg_name, metadata.name660661try:662# write unescaped data to PKG-INFO, so older pkg_resources663# can still parse it664metadata.write_pkg_info(cmd.egg_info)665finally:666metadata.name, metadata.version = oldname, oldver667668safe = getattr(cmd.distribution, 'zip_safe', None)669670bdist_egg.write_safety_flag(cmd.egg_info, safe)671672673def warn_depends_obsolete(cmd, basename, filename):674if os.path.exists(filename):675log.warn(676"WARNING: 'depends.txt' is not used by setuptools 0.6!\n"677"Use the install_requires/extras_require setup() args instead."678)679680681def _write_requirements(stream, reqs):682lines = yield_lines(reqs or ())683684def append_cr(line):685return line + '\n'686lines = map(append_cr, lines)687stream.writelines(lines)688689690def write_requirements(cmd, basename, filename):691dist = cmd.distribution692data = io.StringIO()693_write_requirements(data, dist.install_requires)694extras_require = dist.extras_require or {}695for extra in sorted(extras_require):696data.write('\n[{extra}]\n'.format(**vars()))697_write_requirements(data, extras_require[extra])698cmd.write_or_delete_file("requirements", filename, data.getvalue())699700701def write_setup_requirements(cmd, basename, filename):702data = io.StringIO()703_write_requirements(data, cmd.distribution.setup_requires)704cmd.write_or_delete_file("setup-requirements", filename, data.getvalue())705706707def write_toplevel_names(cmd, basename, filename):708pkgs = dict.fromkeys(709[710k.split('.', 1)[0]711for k in cmd.distribution.iter_distribution_names()712]713)714cmd.write_file("top-level names", filename, '\n'.join(sorted(pkgs)) + '\n')715716717def overwrite_arg(cmd, basename, filename):718write_arg(cmd, basename, filename, True)719720721def write_arg(cmd, basename, filename, force=False):722argname = os.path.splitext(basename)[0]723value = getattr(cmd.distribution, argname, None)724if value is not None:725value = '\n'.join(value) + '\n'726cmd.write_or_delete_file(argname, filename, value, force)727728729def write_entries(cmd, basename, filename):730eps = _entry_points.load(cmd.distribution.entry_points)731defn = _entry_points.render(eps)732cmd.write_or_delete_file('entry points', filename, defn, True)733734735def get_pkg_info_revision():736"""737Get a -r### off of PKG-INFO Version in case this is an sdist of738a subversion revision.739"""740warnings.warn(741"get_pkg_info_revision is deprecated.", EggInfoDeprecationWarning)742if os.path.exists('PKG-INFO'):743with io.open('PKG-INFO') as f:744for line in f:745match = re.match(r"Version:.*-r(\d+)\s*$", line)746if match:747return int(match.group(1))748return 0749750751class EggInfoDeprecationWarning(SetuptoolsDeprecationWarning):752"""Deprecated behavior warning for EggInfo, bypassing suppression."""753754755