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