upload_docs.py 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213
  1. # -*- coding: utf-8 -*-
  2. """upload_docs
  3. Implements a Distutils 'upload_docs' subcommand (upload documentation to
  4. sites other than PyPi such as devpi).
  5. """
  6. from base64 import standard_b64encode
  7. from distutils import log
  8. from distutils.errors import DistutilsOptionError
  9. import os
  10. import socket
  11. import zipfile
  12. import tempfile
  13. import shutil
  14. import itertools
  15. import functools
  16. import http.client
  17. import urllib.parse
  18. import warnings
  19. from .._importlib import metadata
  20. from .. import SetuptoolsDeprecationWarning
  21. from .upload import upload
  22. def _encode(s):
  23. return s.encode('utf-8', 'surrogateescape')
  24. class upload_docs(upload):
  25. # override the default repository as upload_docs isn't
  26. # supported by Warehouse (and won't be).
  27. DEFAULT_REPOSITORY = 'https://pypi.python.org/pypi/'
  28. description = 'Upload documentation to sites other than PyPi such as devpi'
  29. user_options = [
  30. ('repository=', 'r',
  31. "url of repository [default: %s]" % upload.DEFAULT_REPOSITORY),
  32. ('show-response', None,
  33. 'display full response text from server'),
  34. ('upload-dir=', None, 'directory to upload'),
  35. ]
  36. boolean_options = upload.boolean_options
  37. def has_sphinx(self):
  38. return bool(
  39. self.upload_dir is None
  40. and metadata.entry_points(group='distutils.commands', name='build_sphinx')
  41. )
  42. sub_commands = [('build_sphinx', has_sphinx)]
  43. def initialize_options(self):
  44. upload.initialize_options(self)
  45. self.upload_dir = None
  46. self.target_dir = None
  47. def finalize_options(self):
  48. log.warn(
  49. "Upload_docs command is deprecated. Use Read the Docs "
  50. "(https://readthedocs.org) instead.")
  51. upload.finalize_options(self)
  52. if self.upload_dir is None:
  53. if self.has_sphinx():
  54. build_sphinx = self.get_finalized_command('build_sphinx')
  55. self.target_dir = dict(build_sphinx.builder_target_dirs)['html']
  56. else:
  57. build = self.get_finalized_command('build')
  58. self.target_dir = os.path.join(build.build_base, 'docs')
  59. else:
  60. self.ensure_dirname('upload_dir')
  61. self.target_dir = self.upload_dir
  62. self.announce('Using upload directory %s' % self.target_dir)
  63. def create_zipfile(self, filename):
  64. zip_file = zipfile.ZipFile(filename, "w")
  65. try:
  66. self.mkpath(self.target_dir) # just in case
  67. for root, dirs, files in os.walk(self.target_dir):
  68. if root == self.target_dir and not files:
  69. tmpl = "no files found in upload directory '%s'"
  70. raise DistutilsOptionError(tmpl % self.target_dir)
  71. for name in files:
  72. full = os.path.join(root, name)
  73. relative = root[len(self.target_dir):].lstrip(os.path.sep)
  74. dest = os.path.join(relative, name)
  75. zip_file.write(full, dest)
  76. finally:
  77. zip_file.close()
  78. def run(self):
  79. warnings.warn(
  80. "upload_docs is deprecated and will be removed in a future "
  81. "version. Use tools like httpie or curl instead.",
  82. SetuptoolsDeprecationWarning,
  83. )
  84. # Run sub commands
  85. for cmd_name in self.get_sub_commands():
  86. self.run_command(cmd_name)
  87. tmp_dir = tempfile.mkdtemp()
  88. name = self.distribution.metadata.get_name()
  89. zip_file = os.path.join(tmp_dir, "%s.zip" % name)
  90. try:
  91. self.create_zipfile(zip_file)
  92. self.upload_file(zip_file)
  93. finally:
  94. shutil.rmtree(tmp_dir)
  95. @staticmethod
  96. def _build_part(item, sep_boundary):
  97. key, values = item
  98. title = '\nContent-Disposition: form-data; name="%s"' % key
  99. # handle multiple entries for the same name
  100. if not isinstance(values, list):
  101. values = [values]
  102. for value in values:
  103. if isinstance(value, tuple):
  104. title += '; filename="%s"' % value[0]
  105. value = value[1]
  106. else:
  107. value = _encode(value)
  108. yield sep_boundary
  109. yield _encode(title)
  110. yield b"\n\n"
  111. yield value
  112. if value and value[-1:] == b'\r':
  113. yield b'\n' # write an extra newline (lurve Macs)
  114. @classmethod
  115. def _build_multipart(cls, data):
  116. """
  117. Build up the MIME payload for the POST data
  118. """
  119. boundary = '--------------GHSKFJDLGDS7543FJKLFHRE75642756743254'
  120. sep_boundary = b'\n--' + boundary.encode('ascii')
  121. end_boundary = sep_boundary + b'--'
  122. end_items = end_boundary, b"\n",
  123. builder = functools.partial(
  124. cls._build_part,
  125. sep_boundary=sep_boundary,
  126. )
  127. part_groups = map(builder, data.items())
  128. parts = itertools.chain.from_iterable(part_groups)
  129. body_items = itertools.chain(parts, end_items)
  130. content_type = 'multipart/form-data; boundary=%s' % boundary
  131. return b''.join(body_items), content_type
  132. def upload_file(self, filename):
  133. with open(filename, 'rb') as f:
  134. content = f.read()
  135. meta = self.distribution.metadata
  136. data = {
  137. ':action': 'doc_upload',
  138. 'name': meta.get_name(),
  139. 'content': (os.path.basename(filename), content),
  140. }
  141. # set up the authentication
  142. credentials = _encode(self.username + ':' + self.password)
  143. credentials = standard_b64encode(credentials).decode('ascii')
  144. auth = "Basic " + credentials
  145. body, ct = self._build_multipart(data)
  146. msg = "Submitting documentation to %s" % (self.repository)
  147. self.announce(msg, log.INFO)
  148. # build the Request
  149. # We can't use urllib2 since we need to send the Basic
  150. # auth right with the first request
  151. schema, netloc, url, params, query, fragments = \
  152. urllib.parse.urlparse(self.repository)
  153. assert not params and not query and not fragments
  154. if schema == 'http':
  155. conn = http.client.HTTPConnection(netloc)
  156. elif schema == 'https':
  157. conn = http.client.HTTPSConnection(netloc)
  158. else:
  159. raise AssertionError("unsupported schema " + schema)
  160. data = ''
  161. try:
  162. conn.connect()
  163. conn.putrequest("POST", url)
  164. content_type = ct
  165. conn.putheader('Content-type', content_type)
  166. conn.putheader('Content-length', str(len(body)))
  167. conn.putheader('Authorization', auth)
  168. conn.endheaders()
  169. conn.send(body)
  170. except socket.error as e:
  171. self.announce(str(e), log.ERROR)
  172. return
  173. r = conn.getresponse()
  174. if r.status == 200:
  175. msg = 'Server response (%s): %s' % (r.status, r.reason)
  176. self.announce(msg, log.INFO)
  177. elif r.status == 301:
  178. location = r.getheader('Location')
  179. if location is None:
  180. location = 'https://pythonhosted.org/%s/' % meta.get_name()
  181. msg = 'Upload successful. Visit %s' % location
  182. self.announce(msg, log.INFO)
  183. else:
  184. msg = 'Upload failed (%s): %s' % (r.status, r.reason)
  185. self.announce(msg, log.ERROR)
  186. if self.show_response:
  187. print('-' * 75, r.read(), '-' * 75)