codekingpro/portable-devtools
114k
1"""upload_docs2 3Implements a Distutils 'upload_docs' subcommand (upload documentation to4sites other than PyPi such as devpi).5"""6 7from base64 import standard_b64encode8from distutils import log9from distutils.errors import DistutilsOptionError10import os11import zipfile12import tempfile13import shutil14import itertools15import functools16import http.client17import urllib.parse18 19from .._importlib import metadata20from ..warnings import SetuptoolsDeprecationWarning21 22from .upload import upload23 24 25def _encode(s):26 return s.encode('utf-8', 'surrogateescape')27 28 29class upload_docs(upload):30 # override the default repository as upload_docs isn't31 # supported by Warehouse (and won't be).32 DEFAULT_REPOSITORY = 'https://pypi.python.org/pypi/'33 34 description = 'Upload documentation to sites other than PyPi such as devpi'35 36 user_options = [37 (38 'repository=',39 'r',40 "url of repository [default: %s]" % upload.DEFAULT_REPOSITORY,41 ),42 ('show-response', None, 'display full response text from server'),43 ('upload-dir=', None, 'directory to upload'),44 ]45 boolean_options = upload.boolean_options46 47 def has_sphinx(self):48 return bool(49 self.upload_dir is None50 and metadata.entry_points(group='distutils.commands', name='build_sphinx')51 )52 53 sub_commands = [('build_sphinx', has_sphinx)] # type: ignore[list-item] # TODO: Fix in typeshed distutils stubs54 55 def initialize_options(self):56 upload.initialize_options(self)57 self.upload_dir = None58 self.target_dir = None59 60 def finalize_options(self):61 log.warn(62 "Upload_docs command is deprecated. Use Read the Docs "63 "(https://readthedocs.org) instead."64 )65 upload.finalize_options(self)66 if self.upload_dir is None:67 if self.has_sphinx():68 build_sphinx = self.get_finalized_command('build_sphinx')69 self.target_dir = dict(build_sphinx.builder_target_dirs)['html']70 else:71 build = self.get_finalized_command('build')72 self.target_dir = os.path.join(build.build_base, 'docs')73 else:74 self.ensure_dirname('upload_dir')75 self.target_dir = self.upload_dir76 self.announce('Using upload directory %s' % self.target_dir)77 78 def create_zipfile(self, filename):79 zip_file = zipfile.ZipFile(filename, "w")80 try:81 self.mkpath(self.target_dir) # just in case82 for root, dirs, files in os.walk(self.target_dir):83 if root == self.target_dir and not files:84 tmpl = "no files found in upload directory '%s'"85 raise DistutilsOptionError(tmpl % self.target_dir)86 for name in files:87 full = os.path.join(root, name)88 relative = root[len(self.target_dir) :].lstrip(os.path.sep)89 dest = os.path.join(relative, name)90 zip_file.write(full, dest)91 finally:92 zip_file.close()93 94 def run(self):95 SetuptoolsDeprecationWarning.emit(96 "Deprecated command",97 """98 upload_docs is deprecated and will be removed in a future version.99 Instead, use tools like devpi and Read the Docs; or lower level tools like100 httpie and curl to interact directly with your hosting service API.101 """,102 due_date=(2023, 9, 26), # warning introduced in 27 Jul 2022103 )104 105 # Run sub commands106 for cmd_name in self.get_sub_commands():107 self.run_command(cmd_name)108 109 tmp_dir = tempfile.mkdtemp()110 name = self.distribution.metadata.get_name()111 zip_file = os.path.join(tmp_dir, "%s.zip" % name)112 try:113 self.create_zipfile(zip_file)114 self.upload_file(zip_file)115 finally:116 shutil.rmtree(tmp_dir)117 118 @staticmethod119 def _build_part(item, sep_boundary):120 key, values = item121 title = '\nContent-Disposition: form-data; name="%s"' % key122 # handle multiple entries for the same name123 if not isinstance(values, list):124 values = [values]125 for value in values:126 if isinstance(value, tuple):127 title += '; filename="%s"' % value[0]128 value = value[1]129 else:130 value = _encode(value)131 yield sep_boundary132 yield _encode(title)133 yield b"\n\n"134 yield value135 if value and value[-1:] == b'\r':136 yield b'\n' # write an extra newline (lurve Macs)137 138 @classmethod139 def _build_multipart(cls, data):140 """141 Build up the MIME payload for the POST data142 """143 boundary = '--------------GHSKFJDLGDS7543FJKLFHRE75642756743254'144 sep_boundary = b'\n--' + boundary.encode('ascii')145 end_boundary = sep_boundary + b'--'146 end_items = (147 end_boundary,148 b"\n",149 )150 builder = functools.partial(151 cls._build_part,152 sep_boundary=sep_boundary,153 )154 part_groups = map(builder, data.items())155 parts = itertools.chain.from_iterable(part_groups)156 body_items = itertools.chain(parts, end_items)157 content_type = 'multipart/form-data; boundary=%s' % boundary158 return b''.join(body_items), content_type159 160 def upload_file(self, filename):161 with open(filename, 'rb') as f:162 content = f.read()163 meta = self.distribution.metadata164 data = {165 ':action': 'doc_upload',166 'name': meta.get_name(),167 'content': (os.path.basename(filename), content),168 }169 # set up the authentication170 credentials = _encode(self.username + ':' + self.password)171 credentials = standard_b64encode(credentials).decode('ascii')172 auth = "Basic " + credentials173 174 body, ct = self._build_multipart(data)175 176 msg = "Submitting documentation to %s" % (self.repository)177 self.announce(msg, log.INFO)178 179 # build the Request180 # We can't use urllib2 since we need to send the Basic181 # auth right with the first request182 schema, netloc, url, params, query, fragments = urllib.parse.urlparse(183 self.repository184 )185 assert not params and not query and not fragments186 if schema == 'http':187 conn = http.client.HTTPConnection(netloc)188 elif schema == 'https':189 conn = http.client.HTTPSConnection(netloc)190 else:191 raise AssertionError("unsupported schema " + schema)192 193 data = ''194 try:195 conn.connect()196 conn.putrequest("POST", url)197 content_type = ct198 conn.putheader('Content-type', content_type)199 conn.putheader('Content-length', str(len(body)))200 conn.putheader('Authorization', auth)201 conn.endheaders()202 conn.send(body)203 except OSError as e:204 self.announce(str(e), log.ERROR)205 return206 207 r = conn.getresponse()208 if r.status == 200:209 msg = 'Server response (%s): %s' % (r.status, r.reason)210 self.announce(msg, log.INFO)211 elif r.status == 301:212 location = r.getheader('Location')213 if location is None:214 location = 'https://pythonhosted.org/%s/' % meta.get_name()215 msg = 'Upload successful. Visit %s' % location216 self.announce(msg, log.INFO)217 else:218 msg = 'Upload failed (%s): %s' % (r.status, r.reason)219 self.announce(msg, log.ERROR)220 if self.show_response:221 print('-' * 75, r.read(), '-' * 75)222 