Aluode/PerceptionLabPortable
0
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 