CoolFace
Apppublic

Aluode/PerceptionLabPortable

sourceHugging Faceupdated 9mo agoView on Hugging Face
0likes
sdist.py522 linesDownload Raw Back to command
1"""distutils.command.sdist2 3Implements the Distutils 'sdist' command (create a source distribution)."""4 5from __future__ import annotations6 7import os8import sys9from collections.abc import Callable10from distutils import archive_util, dir_util, file_util11from distutils._log import log12from glob import glob13from itertools import filterfalse14from typing import ClassVar15 16from ..core import Command17from ..errors import DistutilsOptionError, DistutilsTemplateError18from ..filelist import FileList19from ..text_file import TextFile20from ..util import convert_path21 22 23def show_formats():24    """Print all possible values for the 'formats' option (used by25    the "--help-formats" command-line option).26    """27    from ..archive_util import ARCHIVE_FORMATS28    from ..fancy_getopt import FancyGetopt29 30    formats = sorted(31        ("formats=" + format, None, ARCHIVE_FORMATS[format][2])32        for format in ARCHIVE_FORMATS.keys()33    )34    FancyGetopt(formats).print_help("List of available source distribution formats:")35 36 37class sdist(Command):38    description = "create a source distribution (tarball, zip file, etc.)"39 40    def checking_metadata(self) -> bool:41        """Callable used for the check sub-command.42 43        Placed here so user_options can view it"""44        return self.metadata_check45 46    user_options = [47        ('template=', 't', "name of manifest template file [default: MANIFEST.in]"),48        ('manifest=', 'm', "name of manifest file [default: MANIFEST]"),49        (50            'use-defaults',51            None,52            "include the default file set in the manifest "53            "[default; disable with --no-defaults]",54        ),55        ('no-defaults', None, "don't include the default file set"),56        (57            'prune',58            None,59            "specifically exclude files/directories that should not be "60            "distributed (build tree, RCS/CVS dirs, etc.) "61            "[default; disable with --no-prune]",62        ),63        ('no-prune', None, "don't automatically exclude anything"),64        (65            'manifest-only',66            'o',67            "just regenerate the manifest and then stop (implies --force-manifest)",68        ),69        (70            'force-manifest',71            'f',72            "forcibly regenerate the manifest and carry on as usual. "73            "Deprecated: now the manifest is always regenerated.",74        ),75        ('formats=', None, "formats for source distribution (comma-separated list)"),76        (77            'keep-temp',78            'k',79            "keep the distribution tree around after creating " + "archive file(s)",80        ),81        (82            'dist-dir=',83            'd',84            "directory to put the source distribution archive(s) in [default: dist]",85        ),86        (87            'metadata-check',88            None,89            "Ensure that all required elements of meta-data "90            "are supplied. Warn if any missing. [default]",91        ),92        (93            'owner=',94            'u',95            "Owner name used when creating a tar file [default: current user]",96        ),97        (98            'group=',99            'g',100            "Group name used when creating a tar file [default: current group]",101        ),102    ]103 104    boolean_options: ClassVar[list[str]] = [105        'use-defaults',106        'prune',107        'manifest-only',108        'force-manifest',109        'keep-temp',110        'metadata-check',111    ]112 113    help_options: ClassVar[list[tuple[str, str | None, str, Callable[[], object]]]] = [114        ('help-formats', None, "list available distribution formats", show_formats),115    ]116 117    negative_opt: ClassVar[dict[str, str]] = {118        'no-defaults': 'use-defaults',119        'no-prune': 'prune',120    }121 122    sub_commands = [('check', checking_metadata)]123 124    READMES: ClassVar[tuple[str, ...]] = ('README', 'README.txt', 'README.rst')125 126    def initialize_options(self):127        # 'template' and 'manifest' are, respectively, the names of128        # the manifest template and manifest file.129        self.template = None130        self.manifest = None131 132        # 'use_defaults': if true, we will include the default file set133        # in the manifest134        self.use_defaults = True135        self.prune = True136 137        self.manifest_only = False138        self.force_manifest = False139 140        self.formats = ['gztar']141        self.keep_temp = False142        self.dist_dir = None143 144        self.archive_files = None145        self.metadata_check = True146        self.owner = None147        self.group = None148 149    def finalize_options(self) -> None:150        if self.manifest is None:151            self.manifest = "MANIFEST"152        if self.template is None:153            self.template = "MANIFEST.in"154 155        self.ensure_string_list('formats')156 157        bad_format = archive_util.check_archive_formats(self.formats)158        if bad_format:159            raise DistutilsOptionError(f"unknown archive format '{bad_format}'")160 161        if self.dist_dir is None:162            self.dist_dir = "dist"163 164    def run(self) -> None:165        # 'filelist' contains the list of files that will make up the166        # manifest167        self.filelist = FileList()168 169        # Run sub commands170        for cmd_name in self.get_sub_commands():171            self.run_command(cmd_name)172 173        # Do whatever it takes to get the list of files to process174        # (process the manifest template, read an existing manifest,175        # whatever).  File list is accumulated in 'self.filelist'.176        self.get_file_list()177 178        # If user just wanted us to regenerate the manifest, stop now.179        if self.manifest_only:180            return181 182        # Otherwise, go ahead and create the source distribution tarball,183        # or zipfile, or whatever.184        self.make_distribution()185 186    def get_file_list(self) -> None:187        """Figure out the list of files to include in the source188        distribution, and put it in 'self.filelist'.  This might involve189        reading the manifest template (and writing the manifest), or just190        reading the manifest, or just using the default file set -- it all191        depends on the user's options.192        """193        # new behavior when using a template:194        # the file list is recalculated every time because195        # even if MANIFEST.in or setup.py are not changed196        # the user might have added some files in the tree that197        # need to be included.198        #199        #  This makes --force the default and only behavior with templates.200        template_exists = os.path.isfile(self.template)201        if not template_exists and self._manifest_is_not_generated():202            self.read_manifest()203            self.filelist.sort()204            self.filelist.remove_duplicates()205            return206 207        if not template_exists:208            self.warn(209                ("manifest template '%s' does not exist " + "(using default file list)")210                % self.template211            )212        self.filelist.findall()213 214        if self.use_defaults:215            self.add_defaults()216 217        if template_exists:218            self.read_template()219 220        if self.prune:221            self.prune_file_list()222 223        self.filelist.sort()224        self.filelist.remove_duplicates()225        self.write_manifest()226 227    def add_defaults(self) -> None:228        """Add all the default files to self.filelist:229          - README or README.txt230          - setup.py231          - tests/test*.py and test/test*.py232          - all pure Python modules mentioned in setup script233          - all files pointed by package_data (build_py)234          - all files defined in data_files.235          - all files defined as scripts.236          - all C sources listed as part of extensions or C libraries237            in the setup script (doesn't catch C headers!)238        Warns if (README or README.txt) or setup.py are missing; everything239        else is optional.240        """241        self._add_defaults_standards()242        self._add_defaults_optional()243        self._add_defaults_python()244        self._add_defaults_data_files()245        self._add_defaults_ext()246        self._add_defaults_c_libs()247        self._add_defaults_scripts()248 249    @staticmethod250    def _cs_path_exists(fspath):251        """252        Case-sensitive path existence check253 254        >>> sdist._cs_path_exists(__file__)255        True256        >>> sdist._cs_path_exists(__file__.upper())257        False258        """259        if not os.path.exists(fspath):260            return False261        # make absolute so we always have a directory262        abspath = os.path.abspath(fspath)263        directory, filename = os.path.split(abspath)264        return filename in os.listdir(directory)265 266    def _add_defaults_standards(self):267        standards = [self.READMES, self.distribution.script_name]268        for fn in standards:269            if isinstance(fn, tuple):270                alts = fn271                got_it = False272                for fn in alts:273                    if self._cs_path_exists(fn):274                        got_it = True275                        self.filelist.append(fn)276                        break277 278                if not got_it:279                    self.warn(280                        "standard file not found: should have one of " + ', '.join(alts)281                    )282            else:283                if self._cs_path_exists(fn):284                    self.filelist.append(fn)285                else:286                    self.warn(f"standard file '{fn}' not found")287 288    def _add_defaults_optional(self):289        optional = ['tests/test*.py', 'test/test*.py', 'setup.cfg']290        for pattern in optional:291            files = filter(os.path.isfile, glob(pattern))292            self.filelist.extend(files)293 294    def _add_defaults_python(self):295        # build_py is used to get:296        #  - python modules297        #  - files defined in package_data298        build_py = self.get_finalized_command('build_py')299 300        # getting python files301        if self.distribution.has_pure_modules():302            self.filelist.extend(build_py.get_source_files())303 304        # getting package_data files305        # (computed in build_py.data_files by build_py.finalize_options)306        for _pkg, src_dir, _build_dir, filenames in build_py.data_files:307            for filename in filenames:308                self.filelist.append(os.path.join(src_dir, filename))309 310    def _add_defaults_data_files(self):311        # getting distribution.data_files312        if self.distribution.has_data_files():313            for item in self.distribution.data_files:314                if isinstance(item, str):315                    # plain file316                    item = convert_path(item)317                    if os.path.isfile(item):318                        self.filelist.append(item)319                else:320                    # a (dirname, filenames) tuple321                    dirname, filenames = item322                    for f in filenames:323                        f = convert_path(f)324                        if os.path.isfile(f):325                            self.filelist.append(f)326 327    def _add_defaults_ext(self):328        if self.distribution.has_ext_modules():329            build_ext = self.get_finalized_command('build_ext')330            self.filelist.extend(build_ext.get_source_files())331 332    def _add_defaults_c_libs(self):333        if self.distribution.has_c_libraries():334            build_clib = self.get_finalized_command('build_clib')335            self.filelist.extend(build_clib.get_source_files())336 337    def _add_defaults_scripts(self):338        if self.distribution.has_scripts():339            build_scripts = self.get_finalized_command('build_scripts')340            self.filelist.extend(build_scripts.get_source_files())341 342    def read_template(self) -> None:343        """Read and parse manifest template file named by self.template.344 345        (usually "MANIFEST.in") The parsing and processing is done by346        'self.filelist', which updates itself accordingly.347        """348        log.info("reading manifest template '%s'", self.template)349        template = TextFile(350            self.template,351            strip_comments=True,352            skip_blanks=True,353            join_lines=True,354            lstrip_ws=True,355            rstrip_ws=True,356            collapse_join=True,357        )358 359        try:360            while True:361                line = template.readline()362                if line is None:  # end of file363                    break364 365                try:366                    self.filelist.process_template_line(line)367                # the call above can raise a DistutilsTemplateError for368                # malformed lines, or a ValueError from the lower-level369                # convert_path function370                except (DistutilsTemplateError, ValueError) as msg:371                    self.warn(372                        f"{template.filename}, line {int(template.current_line)}: {msg}"373                    )374        finally:375            template.close()376 377    def prune_file_list(self) -> None:378        """Prune off branches that might slip into the file list as created379        by 'read_template()', but really don't belong there:380          * the build tree (typically "build")381          * the release tree itself (only an issue if we ran "sdist"382            previously with --keep-temp, or it aborted)383          * any RCS, CVS, .svn, .hg, .git, .bzr, _darcs directories384        """385        build = self.get_finalized_command('build')386        base_dir = self.distribution.get_fullname()387 388        self.filelist.exclude_pattern(None, prefix=os.fspath(build.build_base))389        self.filelist.exclude_pattern(None, prefix=base_dir)390 391        if sys.platform == 'win32':392            seps = r'/|\\'393        else:394            seps = '/'395 396        vcs_dirs = ['RCS', 'CVS', r'\.svn', r'\.hg', r'\.git', r'\.bzr', '_darcs']397        vcs_ptrn = r'(^|{})({})({}).*'.format(seps, '|'.join(vcs_dirs), seps)398        self.filelist.exclude_pattern(vcs_ptrn, is_regex=True)399 400    def write_manifest(self) -> None:401        """Write the file list in 'self.filelist' (presumably as filled in402        by 'add_defaults()' and 'read_template()') to the manifest file403        named by 'self.manifest'.404        """405        if self._manifest_is_not_generated():406            log.info(407                f"not writing to manually maintained manifest file '{self.manifest}'"408            )409            return410 411        content = self.filelist.files[:]412        content.insert(0, '# file GENERATED by distutils, do NOT edit')413        self.execute(414            file_util.write_file,415            (self.manifest, content),416            f"writing manifest file '{self.manifest}'",417        )418 419    def _manifest_is_not_generated(self):420        # check for special comment used in 3.1.3 and higher421        if not os.path.isfile(self.manifest):422            return False423 424        with open(self.manifest, encoding='utf-8') as fp:425            first_line = next(fp)426        return first_line != '# file GENERATED by distutils, do NOT edit\n'427 428    def read_manifest(self) -> None:429        """Read the manifest file (named by 'self.manifest') and use it to430        fill in 'self.filelist', the list of files to include in the source431        distribution.432        """433        log.info("reading manifest file '%s'", self.manifest)434        with open(self.manifest, encoding='utf-8') as lines:435            self.filelist.extend(436                # ignore comments and blank lines437                filter(None, filterfalse(is_comment, map(str.strip, lines)))438            )439 440    def make_release_tree(self, base_dir, files) -> None:441        """Create the directory tree that will become the source442        distribution archive.  All directories implied by the filenames in443        'files' are created under 'base_dir', and then we hard link or copy444        (if hard linking is unavailable) those files into place.445        Essentially, this duplicates the developer's source tree, but in a446        directory named after the distribution, containing only the files447        to be distributed.448        """449        # Create all the directories under 'base_dir' necessary to450        # put 'files' there; the 'mkpath()' is just so we don't die451        # if the manifest happens to be empty.452        self.mkpath(base_dir)453        dir_util.create_tree(base_dir, files, dry_run=self.dry_run)454 455        # And walk over the list of files, either making a hard link (if456        # os.link exists) to each one that doesn't already exist in its457        # corresponding location under 'base_dir', or copying each file458        # that's out-of-date in 'base_dir'.  (Usually, all files will be459        # out-of-date, because by default we blow away 'base_dir' when460        # we're done making the distribution archives.)461 462        if hasattr(os, 'link'):  # can make hard links on this system463            link = 'hard'464            msg = f"making hard links in {base_dir}..."465        else:  # nope, have to copy466            link = None467            msg = f"copying files to {base_dir}..."468 469        if not files:470            log.warning("no files to distribute -- empty manifest?")471        else:472            log.info(msg)473        for file in files:474            if not os.path.isfile(file):475                log.warning("'%s' not a regular file -- skipping", file)476            else:477                dest = os.path.join(base_dir, file)478                self.copy_file(file, dest, link=link)479 480        self.distribution.metadata.write_pkg_info(base_dir)481 482    def make_distribution(self) -> None:483        """Create the source distribution(s).  First, we create the release484        tree with 'make_release_tree()'; then, we create all required485        archive files (according to 'self.formats') from the release tree.486        Finally, we clean up by blowing away the release tree (unless487        'self.keep_temp' is true).  The list of archive files created is488        stored so it can be retrieved later by 'get_archive_files()'.489        """490        # Don't warn about missing meta-data here -- should be (and is!)491        # done elsewhere.492        base_dir = self.distribution.get_fullname()493        base_name = os.path.join(self.dist_dir, base_dir)494 495        self.make_release_tree(base_dir, self.filelist.files)496        archive_files = []  # remember names of files we create497        # tar archive must be created last to avoid overwrite and remove498        if 'tar' in self.formats:499            self.formats.append(self.formats.pop(self.formats.index('tar')))500 501        for fmt in self.formats:502            file = self.make_archive(503                base_name, fmt, base_dir=base_dir, owner=self.owner, group=self.group504            )505            archive_files.append(file)506            self.distribution.dist_files.append(('sdist', '', file))507 508        self.archive_files = archive_files509 510        if not self.keep_temp:511            dir_util.remove_tree(base_dir, dry_run=self.dry_run)512 513    def get_archive_files(self):514        """Return the list of archive files created when the command515        was run, or None if the command hasn't run yet.516        """517        return self.archive_files518 519 520def is_comment(line: str) -> bool:521    return line.startswith('#')522