Add better error reporting for MemoryErrors caused by str->float conversions.
[python.git] / Lib / distutils / command / upload.py
blob3e18aeaad61bf2ec27e6248f99d3701dc1a61e1f
1 """distutils.command.upload
3 Implements the Distutils 'upload' subcommand (upload package to PyPI)."""
4 import os
5 import socket
6 import platform
7 from urllib2 import urlopen, Request, HTTPError
8 from base64 import standard_b64encode
9 import urlparse
10 import cStringIO as StringIO
11 from hashlib import md5
13 from distutils.errors import DistutilsOptionError
14 from distutils.core import PyPIRCCommand
15 from distutils.spawn import spawn
16 from distutils import log
18 class upload(PyPIRCCommand):
20 description = "upload binary package to PyPI"
22 user_options = PyPIRCCommand.user_options + [
23 ('sign', 's',
24 'sign files to upload using gpg'),
25 ('identity=', 'i', 'GPG identity used to sign files'),
28 boolean_options = PyPIRCCommand.boolean_options + ['sign']
30 def initialize_options(self):
31 PyPIRCCommand.initialize_options(self)
32 self.username = ''
33 self.password = ''
34 self.show_response = 0
35 self.sign = False
36 self.identity = None
38 def finalize_options(self):
39 PyPIRCCommand.finalize_options(self)
40 if self.identity and not self.sign:
41 raise DistutilsOptionError(
42 "Must use --sign for --identity to have meaning"
44 config = self._read_pypirc()
45 if config != {}:
46 self.username = config['username']
47 self.password = config['password']
48 self.repository = config['repository']
49 self.realm = config['realm']
51 # getting the password from the distribution
52 # if previously set by the register command
53 if not self.password and self.distribution.password:
54 self.password = self.distribution.password
56 def run(self):
57 if not self.distribution.dist_files:
58 raise DistutilsOptionError("No dist file created in earlier command")
59 for command, pyversion, filename in self.distribution.dist_files:
60 self.upload_file(command, pyversion, filename)
62 def upload_file(self, command, pyversion, filename):
63 # Makes sure the repository URL is compliant
64 schema, netloc, url, params, query, fragments = \
65 urlparse.urlparse(self.repository)
66 if params or query or fragments:
67 raise AssertionError("Incompatible url %s" % self.repository)
69 if schema not in ('http', 'https'):
70 raise AssertionError("unsupported schema " + schema)
72 # Sign if requested
73 if self.sign:
74 gpg_args = ["gpg", "--detach-sign", "-a", filename]
75 if self.identity:
76 gpg_args[2:2] = ["--local-user", self.identity]
77 spawn(gpg_args,
78 dry_run=self.dry_run)
80 # Fill in the data - send all the meta-data in case we need to
81 # register a new release
82 content = open(filename,'rb').read()
83 meta = self.distribution.metadata
84 data = {
85 # action
86 ':action': 'file_upload',
87 'protcol_version': '1',
89 # identify release
90 'name': meta.get_name(),
91 'version': meta.get_version(),
93 # file content
94 'content': (os.path.basename(filename),content),
95 'filetype': command,
96 'pyversion': pyversion,
97 'md5_digest': md5(content).hexdigest(),
99 # additional meta-data
100 'metadata_version' : '1.0',
101 'summary': meta.get_description(),
102 'home_page': meta.get_url(),
103 'author': meta.get_contact(),
104 'author_email': meta.get_contact_email(),
105 'license': meta.get_licence(),
106 'description': meta.get_long_description(),
107 'keywords': meta.get_keywords(),
108 'platform': meta.get_platforms(),
109 'classifiers': meta.get_classifiers(),
110 'download_url': meta.get_download_url(),
111 # PEP 314
112 'provides': meta.get_provides(),
113 'requires': meta.get_requires(),
114 'obsoletes': meta.get_obsoletes(),
116 comment = ''
117 if command == 'bdist_rpm':
118 dist, version, id = platform.dist()
119 if dist:
120 comment = 'built for %s %s' % (dist, version)
121 elif command == 'bdist_dumb':
122 comment = 'built for %s' % platform.platform(terse=1)
123 data['comment'] = comment
125 if self.sign:
126 data['gpg_signature'] = (os.path.basename(filename) + ".asc",
127 open(filename+".asc").read())
129 # set up the authentication
130 auth = "Basic " + standard_b64encode(self.username + ":" +
131 self.password)
133 # Build up the MIME payload for the POST data
134 boundary = '--------------GHSKFJDLGDS7543FJKLFHRE75642756743254'
135 sep_boundary = '\n--' + boundary
136 end_boundary = sep_boundary + '--'
137 body = StringIO.StringIO()
138 for key, value in data.items():
139 # handle multiple entries for the same name
140 if not isinstance(value, list):
141 value = [value]
142 for value in value:
143 if isinstance(value, tuple):
144 fn = ';filename="%s"' % value[0]
145 value = value[1]
146 else:
147 fn = ""
148 value = str(value)
149 body.write(sep_boundary)
150 body.write('\nContent-Disposition: form-data; name="%s"'%key)
151 body.write(fn)
152 body.write("\n\n")
153 body.write(value)
154 if value and value[-1] == '\r':
155 body.write('\n') # write an extra newline (lurve Macs)
156 body.write(end_boundary)
157 body.write("\n")
158 body = body.getvalue()
160 self.announce("Submitting %s to %s" % (filename, self.repository), log.INFO)
162 # build the Request
163 headers = {'Content-type':
164 'multipart/form-data; boundary=%s' % boundary,
165 'Content-length': str(len(body)),
166 'Authorization': auth}
168 request = Request(self.repository, data=body,
169 headers=headers)
170 # send the data
171 try:
172 result = urlopen(request)
173 status = result.getcode()
174 reason = result.msg
175 except socket.error, e:
176 self.announce(str(e), log.ERROR)
177 return
178 except HTTPError, e:
179 status = e.code
180 reason = e.msg
182 if status == 200:
183 self.announce('Server response (%s): %s' % (status, reason),
184 log.INFO)
185 else:
186 self.announce('Upload failed (%s): %s' % (status, reason),
187 log.ERROR)
188 if self.show_response:
189 self.announce('-'*75, result.read(), '-'*75)