• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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