1"""setuptools.command.egg_info 2 3Create a distribution's .egg-info directory and contents""" 4 5from distutils.filelist import FileList as _FileList 6from distutils.errors import DistutilsInternalError 7from distutils.util import convert_path 8from distutils import log 9import distutils.errors 10import distutils.filelist 11import functools 12import os 13import re 14import sys 15import io 16import warnings 17import time 18import collections 19 20from .._importlib import metadata 21from .. import _entry_points 22 23from setuptools import Command 24from setuptools.command.sdist import sdist 25from setuptools.command.sdist import walk_revctrl 26from setuptools.command.setopt import edit_config 27from setuptools.command import bdist_egg 28from pkg_resources import ( 29 Requirement, safe_name, parse_version, 30 safe_version, to_filename) 31import setuptools.unicode_utils as unicode_utils 32from setuptools.glob import glob 33 34from setuptools.extern import packaging 35from setuptools.extern.jaraco.text import yield_lines 36from setuptools import SetuptoolsDeprecationWarning 37 38 39def translate_pattern(glob): # noqa: C901 # is too complex (14) # FIXME 40 """ 41 Translate a file path glob like '*.txt' in to a regular expression. 42 This differs from fnmatch.translate which allows wildcards to match 43 directory separators. It also knows about '**/' which matches any number of 44 directories. 45 """ 46 pat = '' 47 48 # This will split on '/' within [character classes]. This is deliberate. 49 chunks = glob.split(os.path.sep) 50 51 sep = re.escape(os.sep) 52 valid_char = '[^%s]' % (sep,) 53 54 for c, chunk in enumerate(chunks): 55 last_chunk = c == len(chunks) - 1 56 57 # Chunks that are a literal ** are globstars. They match anything. 58 if chunk == '**': 59 if last_chunk: 60 # Match anything if this is the last component 61 pat += '.*' 62 else: 63 # Match '(name/)*' 64 pat += '(?:%s+%s)*' % (valid_char, sep) 65 continue # Break here as the whole path component has been handled 66 67 # Find any special characters in the remainder 68 i = 0 69 chunk_len = len(chunk) 70 while i < chunk_len: 71 char = chunk[i] 72 if char == '*': 73 # Match any number of name characters 74 pat += valid_char + '*' 75 elif char == '?': 76 # Match a name character 77 pat += valid_char 78 elif char == '[': 79 # Character class 80 inner_i = i + 1 81 # Skip initial !/] chars 82 if inner_i < chunk_len and chunk[inner_i] == '!': 83 inner_i = inner_i + 1 84 if inner_i < chunk_len and chunk[inner_i] == ']': 85 inner_i = inner_i + 1 86 87 # Loop till the closing ] is found 88 while inner_i < chunk_len and chunk[inner_i] != ']': 89 inner_i = inner_i + 1 90 91 if inner_i >= chunk_len: 92 # Got to the end of the string without finding a closing ] 93 # Do not treat this as a matching group, but as a literal [ 94 pat += re.escape(char) 95 else: 96 # Grab the insides of the [brackets] 97 inner = chunk[i + 1:inner_i] 98 char_class = '' 99 100 # Class negation 101 if inner[0] == '!': 102 char_class = '^' 103 inner = inner[1:] 104 105 char_class += re.escape(inner) 106 pat += '[%s]' % (char_class,) 107 108 # Skip to the end ] 109 i = inner_i 110 else: 111 pat += re.escape(char) 112 i += 1 113 114 # Join each chunk with the dir separator 115 if not last_chunk: 116 pat += sep 117 118 pat += r'\Z' 119 return re.compile(pat, flags=re.MULTILINE | re.DOTALL) 120 121 122class InfoCommon: 123 tag_build = None 124 tag_date = None 125 126 @property 127 def name(self): 128 return safe_name(self.distribution.get_name()) 129 130 def tagged_version(self): 131 return safe_version(self._maybe_tag(self.distribution.get_version())) 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 version if self.vtags and version.endswith(self.vtags) 140 else version + self.vtags 141 ) 142 143 def tags(self): 144 version = '' 145 if self.tag_build: 146 version += self.tag_build 147 if self.tag_date: 148 version += time.strftime("-%Y%m%d") 149 return version 150 vtags = property(tags) 151 152 153class egg_info(InfoCommon, Command): 154 description = "create a distribution's .egg-info directory" 155 156 user_options = [ 157 ('egg-base=', 'e', "directory containing .egg-info directories" 158 " (default: top of the source tree)"), 159 ('tag-date', 'd', "Add date stamp (e.g. 20050528) to version number"), 160 ('tag-build=', 'b', "Specify explicit tag to add to version number"), 161 ('no-date', 'D', "Don't include date stamp [default]"), 162 ] 163 164 boolean_options = ['tag-date'] 165 negative_opt = { 166 'no-date': 'tag-date', 167 } 168 169 def initialize_options(self): 170 self.egg_base = None 171 self.egg_name = None 172 self.egg_info = None 173 self.egg_version = None 174 self.broken_egg_info = False 175 176 #################################### 177 # allow the 'tag_svn_revision' to be detected and 178 # set, supporting sdists built on older Setuptools. 179 @property 180 def tag_svn_revision(self): 181 pass 182 183 @tag_svn_revision.setter 184 def tag_svn_revision(self, value): 185 pass 186 #################################### 187 188 def save_version_info(self, filename): 189 """ 190 Materialize the value of date into the 191 build tag. Install build keys in a deterministic order 192 to avoid arbitrary reordering on subsequent builds. 193 """ 194 egg_info = collections.OrderedDict() 195 # follow the order these keys would have been added 196 # when PYTHONHASHSEED=0 197 egg_info['tag_build'] = self.tags() 198 egg_info['tag_date'] = 0 199 edit_config(filename, dict(egg_info=egg_info)) 200 201 def finalize_options(self): 202 # Note: we need to capture the current value returned 203 # by `self.tagged_version()`, so we can later update 204 # `self.distribution.metadata.version` without 205 # repercussions. 206 self.egg_name = self.name 207 self.egg_version = self.tagged_version() 208 parsed_version = parse_version(self.egg_version) 209 210 try: 211 is_version = isinstance(parsed_version, packaging.version.Version) 212 spec = "%s==%s" if is_version else "%s===%s" 213 Requirement(spec % (self.egg_name, self.egg_version)) 214 except ValueError as e: 215 raise distutils.errors.DistutilsOptionError( 216 "Invalid distribution name or version syntax: %s-%s" % 217 (self.egg_name, self.egg_version) 218 ) from e 219 220 if self.egg_base is None: 221 dirs = self.distribution.package_dir 222 self.egg_base = (dirs or {}).get('', os.curdir) 223 224 self.ensure_dirname('egg_base') 225 self.egg_info = to_filename(self.egg_name) + '.egg-info' 226 if self.egg_base != os.curdir: 227 self.egg_info = os.path.join(self.egg_base, self.egg_info) 228 if '-' in self.egg_name: 229 self.check_broken_egg_info() 230 231 # Set package version for the benefit of dumber commands 232 # (e.g. sdist, bdist_wininst, etc.) 233 # 234 self.distribution.metadata.version = self.egg_version 235 236 # If we bootstrapped around the lack of a PKG-INFO, as might be the 237 # case in a fresh checkout, make sure that any special tags get added 238 # to the version info 239 # 240 pd = self.distribution._patched_dist 241 if pd is not None and pd.key == self.egg_name.lower(): 242 pd._version = self.egg_version 243 pd._parsed_version = parse_version(self.egg_version) 244 self.distribution._patched_dist = None 245 246 def write_or_delete_file(self, what, filename, data, force=False): 247 """Write `data` to `filename` or delete if empty 248 249 If `data` is non-empty, this routine is the same as ``write_file()``. 250 If `data` is empty but not ``None``, this is the same as calling 251 ``delete_file(filename)`. If `data` is ``None``, then this is a no-op 252 unless `filename` exists, in which case a warning is issued about the 253 orphaned file (if `force` is false), or deleted (if `force` is true). 254 """ 255 if data: 256 self.write_file(what, filename, data) 257 elif os.path.exists(filename): 258 if data is None and not force: 259 log.warn( 260 "%s not set in setup(), but %s exists", what, filename 261 ) 262 return 263 else: 264 self.delete_file(filename) 265 266 def write_file(self, what, filename, data): 267 """Write `data` to `filename` (if not a dry run) after announcing it 268 269 `what` is used in a log message to identify what is being written 270 to the file. 271 """ 272 log.info("writing %s to %s", what, filename) 273 data = data.encode("utf-8") 274 if not self.dry_run: 275 f = open(filename, 'wb') 276 f.write(data) 277 f.close() 278 279 def delete_file(self, filename): 280 """Delete `filename` (if not a dry run) after announcing it""" 281 log.info("deleting %s", filename) 282 if not self.dry_run: 283 os.unlink(filename) 284 285 def run(self): 286 self.mkpath(self.egg_info) 287 os.utime(self.egg_info, None) 288 for ep in metadata.entry_points(group='egg_info.writers'): 289 self.distribution._install_dependencies(ep) 290 writer = ep.load() 291 writer(self, ep.name, os.path.join(self.egg_info, ep.name)) 292 293 # Get rid of native_libs.txt if it was put there by older bdist_egg 294 nl = os.path.join(self.egg_info, "native_libs.txt") 295 if os.path.exists(nl): 296 self.delete_file(nl) 297 298 self.find_sources() 299 300 def find_sources(self): 301 """Generate SOURCES.txt manifest file""" 302 manifest_filename = os.path.join(self.egg_info, "SOURCES.txt") 303 mm = manifest_maker(self.distribution) 304 mm.manifest = manifest_filename 305 mm.run() 306 self.filelist = mm.filelist 307 308 def check_broken_egg_info(self): 309 bei = self.egg_name + '.egg-info' 310 if self.egg_base != os.curdir: 311 bei = os.path.join(self.egg_base, bei) 312 if os.path.exists(bei): 313 log.warn( 314 "-" * 78 + '\n' 315 "Note: Your current .egg-info directory has a '-' in its name;" 316 '\nthis will not work correctly with "setup.py develop".\n\n' 317 'Please rename %s to %s to correct this problem.\n' + '-' * 78, 318 bei, self.egg_info 319 ) 320 self.broken_egg_info = self.egg_info 321 self.egg_info = bei # make it work for now 322 323 324class FileList(_FileList): 325 # Implementations of the various MANIFEST.in commands 326 327 def process_template_line(self, line): 328 # Parse the line: split it up, make sure the right number of words 329 # is there, and return the relevant words. 'action' is always 330 # defined: it's the first word of the line. Which of the other 331 # three are defined depends on the action; it'll be either 332 # patterns, (dir and patterns), or (dir_pattern). 333 (action, patterns, dir, dir_pattern) = self._parse_template_line(line) 334 335 action_map = { 336 'include': self.include, 337 'exclude': self.exclude, 338 'global-include': self.global_include, 339 'global-exclude': self.global_exclude, 340 'recursive-include': functools.partial( 341 self.recursive_include, dir, 342 ), 343 'recursive-exclude': functools.partial( 344 self.recursive_exclude, dir, 345 ), 346 'graft': self.graft, 347 'prune': self.prune, 348 } 349 log_map = { 350 'include': "warning: no files found matching '%s'", 351 'exclude': ( 352 "warning: no previously-included files found " 353 "matching '%s'" 354 ), 355 'global-include': ( 356 "warning: no files found matching '%s' " 357 "anywhere in distribution" 358 ), 359 'global-exclude': ( 360 "warning: no previously-included files matching " 361 "'%s' found anywhere in distribution" 362 ), 363 'recursive-include': ( 364 "warning: no files found matching '%s' " 365 "under directory '%s'" 366 ), 367 'recursive-exclude': ( 368 "warning: no previously-included files matching " 369 "'%s' found under directory '%s'" 370 ), 371 'graft': "warning: no directories found matching '%s'", 372 'prune': "no previously-included directories found matching '%s'", 373 } 374 375 try: 376 process_action = action_map[action] 377 except KeyError: 378 raise DistutilsInternalError( 379 "this cannot happen: invalid action '{action!s}'". 380 format(action=action), 381 ) 382 383 # OK, now we know that the action is valid and we have the 384 # right number of words on the line for that action -- so we 385 # can proceed with minimal error-checking. 386 387 action_is_recursive = action.startswith('recursive-') 388 if action in {'graft', 'prune'}: 389 patterns = [dir_pattern] 390 extra_log_args = (dir, ) if action_is_recursive else () 391 log_tmpl = log_map[action] 392 393 self.debug_print( 394 ' '.join( 395 [action] + 396 ([dir] if action_is_recursive else []) + 397 patterns, 398 ) 399 ) 400 for pattern in patterns: 401 if not process_action(pattern): 402 log.warn(log_tmpl, pattern, *extra_log_args) 403 404 def _remove_files(self, predicate): 405 """ 406 Remove all files from the file list that match the predicate. 407 Return True if any matching files were removed 408 """ 409 found = False 410 for i in range(len(self.files) - 1, -1, -1): 411 if predicate(self.files[i]): 412 self.debug_print(" removing " + self.files[i]) 413 del self.files[i] 414 found = True 415 return found 416 417 def include(self, pattern): 418 """Include files that match 'pattern'.""" 419 found = [f for f in glob(pattern) if not os.path.isdir(f)] 420 self.extend(found) 421 return bool(found) 422 423 def exclude(self, pattern): 424 """Exclude files that match 'pattern'.""" 425 match = translate_pattern(pattern) 426 return self._remove_files(match.match) 427 428 def recursive_include(self, dir, pattern): 429 """ 430 Include all files anywhere in 'dir/' that match the pattern. 431 """ 432 full_pattern = os.path.join(dir, '**', pattern) 433 found = [f for f in glob(full_pattern, recursive=True) 434 if not os.path.isdir(f)] 435 self.extend(found) 436 return bool(found) 437 438 def recursive_exclude(self, dir, pattern): 439 """ 440 Exclude any file anywhere in 'dir/' that match the pattern. 441 """ 442 match = translate_pattern(os.path.join(dir, '**', pattern)) 443 return self._remove_files(match.match) 444 445 def graft(self, dir): 446 """Include all files from 'dir/'.""" 447 found = [ 448 item 449 for match_dir in glob(dir) 450 for item in distutils.filelist.findall(match_dir) 451 ] 452 self.extend(found) 453 return bool(found) 454 455 def prune(self, dir): 456 """Filter out files from 'dir/'.""" 457 match = translate_pattern(os.path.join(dir, '**')) 458 return self._remove_files(match.match) 459 460 def global_include(self, pattern): 461 """ 462 Include all files anywhere in the current directory that match the 463 pattern. This is very inefficient on large file trees. 464 """ 465 if self.allfiles is None: 466 self.findall() 467 match = translate_pattern(os.path.join('**', pattern)) 468 found = [f for f in self.allfiles if match.match(f)] 469 self.extend(found) 470 return bool(found) 471 472 def global_exclude(self, pattern): 473 """ 474 Exclude all files anywhere that match the pattern. 475 """ 476 match = translate_pattern(os.path.join('**', pattern)) 477 return self._remove_files(match.match) 478 479 def append(self, item): 480 if item.endswith('\r'): # Fix older sdists built on Windows 481 item = item[:-1] 482 path = convert_path(item) 483 484 if self._safe_path(path): 485 self.files.append(path) 486 487 def extend(self, paths): 488 self.files.extend(filter(self._safe_path, paths)) 489 490 def _repair(self): 491 """ 492 Replace self.files with only safe paths 493 494 Because some owners of FileList manipulate the underlying 495 ``files`` attribute directly, this method must be called to 496 repair those paths. 497 """ 498 self.files = list(filter(self._safe_path, self.files)) 499 500 def _safe_path(self, path): 501 enc_warn = "'%s' not %s encodable -- skipping" 502 503 # To avoid accidental trans-codings errors, first to unicode 504 u_path = unicode_utils.filesys_decode(path) 505 if u_path is None: 506 log.warn("'%s' in unexpected encoding -- skipping" % path) 507 return False 508 509 # Must ensure utf-8 encodability 510 utf8_path = unicode_utils.try_encode(u_path, "utf-8") 511 if utf8_path is None: 512 log.warn(enc_warn, path, 'utf-8') 513 return False 514 515 try: 516 # accept is either way checks out 517 if os.path.exists(u_path) or os.path.exists(utf8_path): 518 return True 519 # this will catch any encode errors decoding u_path 520 except UnicodeEncodeError: 521 log.warn(enc_warn, path, sys.getfilesystemencoding()) 522 523 524class manifest_maker(sdist): 525 template = "MANIFEST.in" 526 527 def initialize_options(self): 528 self.use_defaults = 1 529 self.prune = 1 530 self.manifest_only = 1 531 self.force_manifest = 1 532 533 def finalize_options(self): 534 pass 535 536 def run(self): 537 self.filelist = FileList() 538 if not os.path.exists(self.manifest): 539 self.write_manifest() # it must exist so it'll get in the list 540 self.add_defaults() 541 if os.path.exists(self.template): 542 self.read_template() 543 self.add_license_files() 544 self.prune_file_list() 545 self.filelist.sort() 546 self.filelist.remove_duplicates() 547 self.write_manifest() 548 549 def _manifest_normalize(self, path): 550 path = unicode_utils.filesys_decode(path) 551 return path.replace(os.sep, '/') 552 553 def write_manifest(self): 554 """ 555 Write the file list in 'self.filelist' to the manifest file 556 named by 'self.manifest'. 557 """ 558 self.filelist._repair() 559 560 # Now _repairs should encodability, but not unicode 561 files = [self._manifest_normalize(f) for f in self.filelist.files] 562 msg = "writing manifest file '%s'" % self.manifest 563 self.execute(write_file, (self.manifest, files), msg) 564 565 def warn(self, msg): 566 if not self._should_suppress_warning(msg): 567 sdist.warn(self, msg) 568 569 @staticmethod 570 def _should_suppress_warning(msg): 571 """ 572 suppress missing-file warnings from sdist 573 """ 574 return re.match(r"standard file .*not found", msg) 575 576 def add_defaults(self): 577 sdist.add_defaults(self) 578 self.filelist.append(self.template) 579 self.filelist.append(self.manifest) 580 rcfiles = list(walk_revctrl()) 581 if rcfiles: 582 self.filelist.extend(rcfiles) 583 elif os.path.exists(self.manifest): 584 self.read_manifest() 585 586 if os.path.exists("setup.py"): 587 # setup.py should be included by default, even if it's not 588 # the script called to create the sdist 589 self.filelist.append("setup.py") 590 591 ei_cmd = self.get_finalized_command('egg_info') 592 self.filelist.graft(ei_cmd.egg_info) 593 594 def add_license_files(self): 595 license_files = self.distribution.metadata.license_files or [] 596 for lf in license_files: 597 log.info("adding license file '%s'", lf) 598 pass 599 self.filelist.extend(license_files) 600 601 def prune_file_list(self): 602 build = self.get_finalized_command('build') 603 base_dir = self.distribution.get_fullname() 604 self.filelist.prune(build.build_base) 605 self.filelist.prune(base_dir) 606 sep = re.escape(os.sep) 607 self.filelist.exclude_pattern(r'(^|' + sep + r')(RCS|CVS|\.svn)' + sep, 608 is_regex=1) 609 610 def _safe_data_files(self, build_py): 611 """ 612 The parent class implementation of this method 613 (``sdist``) will try to include data files, which 614 might cause recursion problems when 615 ``include_package_data=True``. 616 617 Therefore, avoid triggering any attempt of 618 analyzing/building the manifest again. 619 """ 620 if hasattr(build_py, 'get_data_files_without_manifest'): 621 return build_py.get_data_files_without_manifest() 622 623 warnings.warn( 624 "Custom 'build_py' does not implement " 625 "'get_data_files_without_manifest'.\nPlease extend command classes" 626 " from setuptools instead of distutils.", 627 SetuptoolsDeprecationWarning 628 ) 629 return build_py.get_data_files() 630 631 632def write_file(filename, contents): 633 """Create a file with the specified name and write 'contents' (a 634 sequence of strings without line terminators) to it. 635 """ 636 contents = "\n".join(contents) 637 638 # assuming the contents has been vetted for utf-8 encoding 639 contents = contents.encode("utf-8") 640 641 with open(filename, "wb") as f: # always write POSIX-style manifest 642 f.write(contents) 643 644 645def write_pkg_info(cmd, basename, filename): 646 log.info("writing %s", filename) 647 if not cmd.dry_run: 648 metadata = cmd.distribution.metadata 649 metadata.version, oldver = cmd.egg_version, metadata.version 650 metadata.name, oldname = cmd.egg_name, metadata.name 651 652 try: 653 # write unescaped data to PKG-INFO, so older pkg_resources 654 # can still parse it 655 metadata.write_pkg_info(cmd.egg_info) 656 finally: 657 metadata.name, metadata.version = oldname, oldver 658 659 safe = getattr(cmd.distribution, 'zip_safe', None) 660 661 bdist_egg.write_safety_flag(cmd.egg_info, safe) 662 663 664def warn_depends_obsolete(cmd, basename, filename): 665 if os.path.exists(filename): 666 log.warn( 667 "WARNING: 'depends.txt' is not used by setuptools 0.6!\n" 668 "Use the install_requires/extras_require setup() args instead." 669 ) 670 671 672def _write_requirements(stream, reqs): 673 lines = yield_lines(reqs or ()) 674 675 def append_cr(line): 676 return line + '\n' 677 lines = map(append_cr, lines) 678 stream.writelines(lines) 679 680 681def write_requirements(cmd, basename, filename): 682 dist = cmd.distribution 683 data = io.StringIO() 684 _write_requirements(data, dist.install_requires) 685 extras_require = dist.extras_require or {} 686 for extra in sorted(extras_require): 687 data.write('\n[{extra}]\n'.format(**vars())) 688 _write_requirements(data, extras_require[extra]) 689 cmd.write_or_delete_file("requirements", filename, data.getvalue()) 690 691 692def write_setup_requirements(cmd, basename, filename): 693 data = io.StringIO() 694 _write_requirements(data, cmd.distribution.setup_requires) 695 cmd.write_or_delete_file("setup-requirements", filename, data.getvalue()) 696 697 698def write_toplevel_names(cmd, basename, filename): 699 pkgs = dict.fromkeys( 700 [ 701 k.split('.', 1)[0] 702 for k in cmd.distribution.iter_distribution_names() 703 ] 704 ) 705 cmd.write_file("top-level names", filename, '\n'.join(sorted(pkgs)) + '\n') 706 707 708def overwrite_arg(cmd, basename, filename): 709 write_arg(cmd, basename, filename, True) 710 711 712def write_arg(cmd, basename, filename, force=False): 713 argname = os.path.splitext(basename)[0] 714 value = getattr(cmd.distribution, argname, None) 715 if value is not None: 716 value = '\n'.join(value) + '\n' 717 cmd.write_or_delete_file(argname, filename, value, force) 718 719 720def write_entries(cmd, basename, filename): 721 eps = _entry_points.load(cmd.distribution.entry_points) 722 defn = _entry_points.render(eps) 723 cmd.write_or_delete_file('entry points', filename, defn, True) 724 725 726def get_pkg_info_revision(): 727 """ 728 Get a -r### off of PKG-INFO Version in case this is an sdist of 729 a subversion revision. 730 """ 731 warnings.warn( 732 "get_pkg_info_revision is deprecated.", EggInfoDeprecationWarning) 733 if os.path.exists('PKG-INFO'): 734 with io.open('PKG-INFO') as f: 735 for line in f: 736 match = re.match(r"Version:.*-r(\d+)\s*$", line) 737 if match: 738 return int(match.group(1)) 739 return 0 740 741 742class EggInfoDeprecationWarning(SetuptoolsDeprecationWarning): 743 """Deprecated behavior warning for EggInfo, bypassing suppression.""" 744