You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

191 lines
6.6 KiB

  1. # -*- coding: utf-8 -*-
  2. """upload_docs
  3. Implements a Distutils 'upload_docs' subcommand (upload documentation to
  4. PyPI's pythonhosted.org).
  5. """
  6. from base64 import standard_b64encode
  7. from distutils import log
  8. from distutils.errors import DistutilsOptionError
  9. from distutils.command.upload import upload
  10. import os
  11. import socket
  12. import zipfile
  13. import tempfile
  14. import sys
  15. import shutil
  16. from setuptools.compat import httplib, urlparse, unicode, iteritems, PY3
  17. from pkg_resources import iter_entry_points
  18. errors = 'surrogateescape' if PY3 else 'strict'
  19. # This is not just a replacement for byte literals
  20. # but works as a general purpose encoder
  21. def b(s, encoding='utf-8'):
  22. if isinstance(s, unicode):
  23. return s.encode(encoding, errors)
  24. return s
  25. class upload_docs(upload):
  26. description = 'Upload documentation to PyPI'
  27. user_options = [
  28. ('repository=', 'r',
  29. "url of repository [default: %s]" % upload.DEFAULT_REPOSITORY),
  30. ('show-response', None,
  31. 'display full response text from server'),
  32. ('upload-dir=', None, 'directory to upload'),
  33. ]
  34. boolean_options = upload.boolean_options
  35. def has_sphinx(self):
  36. if self.upload_dir is None:
  37. for ep in iter_entry_points('distutils.commands', 'build_sphinx'):
  38. return True
  39. sub_commands = [('build_sphinx', has_sphinx)]
  40. def initialize_options(self):
  41. upload.initialize_options(self)
  42. self.upload_dir = None
  43. self.target_dir = None
  44. def finalize_options(self):
  45. upload.finalize_options(self)
  46. if self.upload_dir is None:
  47. if self.has_sphinx():
  48. build_sphinx = self.get_finalized_command('build_sphinx')
  49. self.target_dir = build_sphinx.builder_target_dir
  50. else:
  51. build = self.get_finalized_command('build')
  52. self.target_dir = os.path.join(build.build_base, 'docs')
  53. else:
  54. self.ensure_dirname('upload_dir')
  55. self.target_dir = self.upload_dir
  56. self.announce('Using upload directory %s' % self.target_dir)
  57. def create_zipfile(self, filename):
  58. zip_file = zipfile.ZipFile(filename, "w")
  59. try:
  60. self.mkpath(self.target_dir) # just in case
  61. for root, dirs, files in os.walk(self.target_dir):
  62. if root == self.target_dir and not files:
  63. raise DistutilsOptionError(
  64. "no files found in upload directory '%s'"
  65. % self.target_dir)
  66. for name in files:
  67. full = os.path.join(root, name)
  68. relative = root[len(self.target_dir):].lstrip(os.path.sep)
  69. dest = os.path.join(relative, name)
  70. zip_file.write(full, dest)
  71. finally:
  72. zip_file.close()
  73. def run(self):
  74. # Run sub commands
  75. for cmd_name in self.get_sub_commands():
  76. self.run_command(cmd_name)
  77. tmp_dir = tempfile.mkdtemp()
  78. name = self.distribution.metadata.get_name()
  79. zip_file = os.path.join(tmp_dir, "%s.zip" % name)
  80. try:
  81. self.create_zipfile(zip_file)
  82. self.upload_file(zip_file)
  83. finally:
  84. shutil.rmtree(tmp_dir)
  85. def upload_file(self, filename):
  86. f = open(filename, 'rb')
  87. content = f.read()
  88. f.close()
  89. meta = self.distribution.metadata
  90. data = {
  91. ':action': 'doc_upload',
  92. 'name': meta.get_name(),
  93. 'content': (os.path.basename(filename), content),
  94. }
  95. # set up the authentication
  96. credentials = b(self.username + ':' + self.password)
  97. credentials = standard_b64encode(credentials)
  98. if PY3:
  99. credentials = credentials.decode('ascii')
  100. auth = "Basic " + credentials
  101. # Build up the MIME payload for the POST data
  102. boundary = '--------------GHSKFJDLGDS7543FJKLFHRE75642756743254'
  103. sep_boundary = b('\n--') + b(boundary)
  104. end_boundary = sep_boundary + b('--')
  105. body = []
  106. for key, values in iteritems(data):
  107. title = '\nContent-Disposition: form-data; name="%s"' % key
  108. # handle multiple entries for the same name
  109. if not isinstance(values, list):
  110. values = [values]
  111. for value in values:
  112. if type(value) is tuple:
  113. title += '; filename="%s"' % value[0]
  114. value = value[1]
  115. else:
  116. value = b(value)
  117. body.append(sep_boundary)
  118. body.append(b(title))
  119. body.append(b("\n\n"))
  120. body.append(value)
  121. if value and value[-1:] == b('\r'):
  122. body.append(b('\n')) # write an extra newline (lurve Macs)
  123. body.append(end_boundary)
  124. body.append(b("\n"))
  125. body = b('').join(body)
  126. self.announce("Submitting documentation to %s" % (self.repository),
  127. log.INFO)
  128. # build the Request
  129. # We can't use urllib2 since we need to send the Basic
  130. # auth right with the first request
  131. schema, netloc, url, params, query, fragments = \
  132. urlparse(self.repository)
  133. assert not params and not query and not fragments
  134. if schema == 'http':
  135. conn = httplib.HTTPConnection(netloc)
  136. elif schema == 'https':
  137. conn = httplib.HTTPSConnection(netloc)
  138. else:
  139. raise AssertionError("unsupported schema " + schema)
  140. data = ''
  141. try:
  142. conn.connect()
  143. conn.putrequest("POST", url)
  144. content_type = 'multipart/form-data; boundary=%s' % boundary
  145. conn.putheader('Content-type', content_type)
  146. conn.putheader('Content-length', str(len(body)))
  147. conn.putheader('Authorization', auth)
  148. conn.endheaders()
  149. conn.send(body)
  150. except socket.error as e:
  151. self.announce(str(e), log.ERROR)
  152. return
  153. r = conn.getresponse()
  154. if r.status == 200:
  155. self.announce('Server response (%s): %s' % (r.status, r.reason),
  156. log.INFO)
  157. elif r.status == 301:
  158. location = r.getheader('Location')
  159. if location is None:
  160. location = 'https://pythonhosted.org/%s/' % meta.get_name()
  161. self.announce('Upload successful. Visit %s' % location,
  162. log.INFO)
  163. else:
  164. self.announce('Upload failed (%s): %s' % (r.status, r.reason),
  165. log.ERROR)
  166. if self.show_response:
  167. print('-' * 75, r.read(), '-' * 75)