codekingpro/portable-devtools
115k
1from __future__ import annotations2 3import contextlib4import os5import re6from collections.abc import Iterator7from itertools import chain8from typing import ClassVar9 10from .._importlib import metadata11from ..dist import Distribution12from .build import _ORIGINAL_SUBCOMMANDS13 14import distutils.command.sdist as orig15from distutils import log16 17_default_revctrl = list18 19 20def walk_revctrl(dirname='') -> Iterator:21 """Find all files under revision control"""22 for ep in metadata.entry_points(group='setuptools.file_finders'):23 yield from ep.load()(dirname)24 25 26class sdist(orig.sdist):27 """Smart sdist that finds anything supported by revision control"""28 29 user_options = [30 ('formats=', None, "formats for source distribution (comma-separated list)"),31 (32 'keep-temp',33 'k',34 "keep the distribution tree around after creating archive file(s)",35 ),36 (37 'dist-dir=',38 'd',39 "directory to put the source distribution archive(s) in [default: dist]",40 ),41 (42 'owner=',43 'u',44 "Owner name used when creating a tar file [default: current user]",45 ),46 (47 'group=',48 'g',49 "Group name used when creating a tar file [default: current group]",50 ),51 ]52 53 distribution: Distribution # override distutils.dist.Distribution with setuptools.dist.Distribution54 negative_opt: ClassVar[dict[str, str]] = {}55 56 README_EXTENSIONS = ['', '.rst', '.txt', '.md']57 READMES = tuple(f'README{ext}' for ext in README_EXTENSIONS)58 59 def run(self) -> None:60 self.run_command('egg_info')61 ei_cmd = self.get_finalized_command('egg_info')62 self.filelist = ei_cmd.filelist63 self.filelist.append(os.path.join(ei_cmd.egg_info, 'SOURCES.txt'))64 self.check_readme()65 66 # Run sub commands67 for cmd_name in self.get_sub_commands():68 self.run_command(cmd_name)69 70 self.make_distribution()71 72 dist_files = getattr(self.distribution, 'dist_files', [])73 for file in self.archive_files:74 data = ('sdist', '', file)75 if data not in dist_files:76 dist_files.append(data)77 78 def initialize_options(self) -> None:79 orig.sdist.initialize_options(self)80 81 def make_distribution(self) -> None:82 """83 Workaround for #51684 """85 with self._remove_os_link():86 orig.sdist.make_distribution(self)87 88 @staticmethod89 @contextlib.contextmanager90 def _remove_os_link():91 """92 In a context, remove and restore os.link if it exists93 """94 95 class NoValue:96 pass97 98 orig_val = getattr(os, 'link', NoValue)99 try:100 del os.link101 except Exception:102 pass103 try:104 yield105 finally:106 if orig_val is not NoValue:107 os.link = orig_val108 109 def add_defaults(self) -> None:110 super().add_defaults()111 self._add_defaults_build_sub_commands()112 113 def _add_defaults_optional(self):114 super()._add_defaults_optional()115 if os.path.isfile('pyproject.toml'):116 self.filelist.append('pyproject.toml')117 118 def _add_defaults_python(self):119 """getting python files"""120 if self.distribution.has_pure_modules():121 build_py = self.get_finalized_command('build_py')122 self.filelist.extend(build_py.get_source_files())123 self._add_data_files(self._safe_data_files(build_py))124 125 def _add_defaults_build_sub_commands(self):126 build = self.get_finalized_command("build")127 missing_cmds = set(build.get_sub_commands()) - _ORIGINAL_SUBCOMMANDS128 # ^-- the original built-in sub-commands are already handled by default.129 cmds = (self.get_finalized_command(c) for c in missing_cmds)130 files = (c.get_source_files() for c in cmds if hasattr(c, "get_source_files"))131 self.filelist.extend(chain.from_iterable(files))132 133 def _safe_data_files(self, build_py):134 """135 Since the ``sdist`` class is also used to compute the MANIFEST136 (via :obj:`setuptools.command.egg_info.manifest_maker`),137 there might be recursion problems when trying to obtain the list of138 data_files and ``include_package_data=True`` (which in turn depends on139 the files included in the MANIFEST).140 141 To avoid that, ``manifest_maker`` should be able to overwrite this142 method and avoid recursive attempts to build/analyze the MANIFEST.143 """144 return build_py.data_files145 146 def _add_data_files(self, data_files):147 """148 Add data files as found in build_py.data_files.149 """150 self.filelist.extend(151 os.path.join(src_dir, name)152 for _, src_dir, _, filenames in data_files153 for name in filenames154 )155 156 def _add_defaults_data_files(self):157 try:158 super()._add_defaults_data_files()159 except TypeError:160 log.warn("data_files contains unexpected objects")161 162 def prune_file_list(self) -> None:163 super().prune_file_list()164 # Prevent accidental inclusion of test-related cache dirs at the project root165 sep = re.escape(os.sep)166 self.filelist.exclude_pattern(r"^(\.tox|\.nox|\.venv)" + sep, is_regex=True)167 168 def check_readme(self) -> None:169 for f in self.READMES:170 if os.path.exists(f):171 return172 else:173 self.warn(174 "standard file not found: should have one of " + ', '.join(self.READMES)175 )176 177 def make_release_tree(self, base_dir, files) -> None:178 orig.sdist.make_release_tree(self, base_dir, files)179 180 # Save any egg_info command line options used to create this sdist181 dest = os.path.join(base_dir, 'setup.cfg')182 if hasattr(os, 'link') and os.path.exists(dest):183 # unlink and re-copy, since it might be hard-linked, and184 # we don't want to change the source version185 os.unlink(dest)186 self.copy_file('setup.cfg', dest)187 188 self.get_finalized_command('egg_info').save_version_info(dest)189 190 def _manifest_is_not_generated(self):191 # check for special comment used in 2.7.1 and higher192 if not os.path.isfile(self.manifest):193 return False194 195 with open(self.manifest, 'rb') as fp:196 first_line = fp.readline()197 return first_line != b'# file GENERATED by distutils, do NOT edit\n'198 199 def read_manifest(self) -> None:200 """Read the manifest file (named by 'self.manifest') and use it to201 fill in 'self.filelist', the list of files to include in the source202 distribution.203 """204 log.info("reading manifest file '%s'", self.manifest)205 manifest = open(self.manifest, 'rb')206 for bytes_line in manifest:207 # The manifest must contain UTF-8. See #303.208 try:209 line = bytes_line.decode('UTF-8')210 except UnicodeDecodeError:211 log.warn(f"{line!r} not UTF-8 decodable -- skipping")212 continue213 # ignore comments and blank lines214 line = line.strip()215 if line.startswith('#') or not line:216 continue217 self.filelist.append(line)218 manifest.close()219 