|
1 |
| -import io |
2 |
| -import os |
3 |
| -import hashlib |
4 |
| -import getpass |
5 |
| - |
6 |
| -from base64 import standard_b64encode |
7 |
| - |
8 | 1 | from distutils import log
|
9 | 2 | from distutils.command import upload as orig
|
10 |
| -from distutils.spawn import spawn |
11 |
| - |
12 |
| -from distutils.errors import DistutilsError |
13 | 3 |
|
14 |
| -from setuptools.extern.six.moves.urllib.request import urlopen, Request |
15 |
| -from setuptools.extern.six.moves.urllib.error import HTTPError |
16 |
| -from setuptools.extern.six.moves.urllib.parse import urlparse |
| 4 | +from setuptools.errors import RemovedCommandError |
17 | 5 |
|
18 | 6 |
|
19 | 7 | class upload(orig.upload):
|
20 |
| - """ |
21 |
| - Override default upload behavior to obtain password |
22 |
| - in a variety of different ways. |
23 |
| - """ |
24 |
| - def run(self): |
25 |
| - try: |
26 |
| - orig.upload.run(self) |
27 |
| - finally: |
28 |
| - self.announce( |
29 |
| - "WARNING: Uploading via this command is deprecated, use twine " |
30 |
| - "to upload instead (https://pypi.org/p/twine/)", |
31 |
| - log.WARN |
32 |
| - ) |
| 8 | + """Formerly used to upload packages to PyPI.""" |
33 | 9 |
|
34 |
| - def finalize_options(self): |
35 |
| - orig.upload.finalize_options(self) |
36 |
| - self.username = ( |
37 |
| - self.username or |
38 |
| - getpass.getuser() |
39 |
| - ) |
40 |
| - # Attempt to obtain password. Short circuit evaluation at the first |
41 |
| - # sign of success. |
42 |
| - self.password = ( |
43 |
| - self.password or |
44 |
| - self._load_password_from_keyring() or |
45 |
| - self._prompt_for_password() |
| 10 | + def run(self): |
| 11 | + msg = ( |
| 12 | + "The upload command has been removed, use twine to upload " |
| 13 | + + "instead (https://pypi.org/p/twine)" |
46 | 14 | )
|
47 | 15 |
|
48 |
| - def upload_file(self, command, pyversion, filename): |
49 |
| - # Makes sure the repository URL is compliant |
50 |
| - schema, netloc, url, params, query, fragments = \ |
51 |
| - urlparse(self.repository) |
52 |
| - if params or query or fragments: |
53 |
| - raise AssertionError("Incompatible url %s" % self.repository) |
54 |
| - |
55 |
| - if schema not in ('http', 'https'): |
56 |
| - raise AssertionError("unsupported schema " + schema) |
57 |
| - |
58 |
| - # Sign if requested |
59 |
| - if self.sign: |
60 |
| - gpg_args = ["gpg", "--detach-sign", "-a", filename] |
61 |
| - if self.identity: |
62 |
| - gpg_args[2:2] = ["--local-user", self.identity] |
63 |
| - spawn(gpg_args, |
64 |
| - dry_run=self.dry_run) |
65 |
| - |
66 |
| - # Fill in the data - send all the meta-data in case we need to |
67 |
| - # register a new release |
68 |
| - with open(filename, 'rb') as f: |
69 |
| - content = f.read() |
70 |
| - |
71 |
| - meta = self.distribution.metadata |
72 |
| - |
73 |
| - data = { |
74 |
| - # action |
75 |
| - ':action': 'file_upload', |
76 |
| - 'protocol_version': '1', |
77 |
| - |
78 |
| - # identify release |
79 |
| - 'name': meta.get_name(), |
80 |
| - 'version': meta.get_version(), |
81 |
| - |
82 |
| - # file content |
83 |
| - 'content': (os.path.basename(filename), content), |
84 |
| - 'filetype': command, |
85 |
| - 'pyversion': pyversion, |
86 |
| - 'md5_digest': hashlib.md5(content).hexdigest(), |
87 |
| - |
88 |
| - # additional meta-data |
89 |
| - 'metadata_version': str(meta.get_metadata_version()), |
90 |
| - 'summary': meta.get_description(), |
91 |
| - 'home_page': meta.get_url(), |
92 |
| - 'author': meta.get_contact(), |
93 |
| - 'author_email': meta.get_contact_email(), |
94 |
| - 'license': meta.get_licence(), |
95 |
| - 'description': meta.get_long_description(), |
96 |
| - 'keywords': meta.get_keywords(), |
97 |
| - 'platform': meta.get_platforms(), |
98 |
| - 'classifiers': meta.get_classifiers(), |
99 |
| - 'download_url': meta.get_download_url(), |
100 |
| - # PEP 314 |
101 |
| - 'provides': meta.get_provides(), |
102 |
| - 'requires': meta.get_requires(), |
103 |
| - 'obsoletes': meta.get_obsoletes(), |
104 |
| - } |
105 |
| - |
106 |
| - data['comment'] = '' |
107 |
| - |
108 |
| - if self.sign: |
109 |
| - data['gpg_signature'] = (os.path.basename(filename) + ".asc", |
110 |
| - open(filename+".asc", "rb").read()) |
111 |
| - |
112 |
| - # set up the authentication |
113 |
| - user_pass = (self.username + ":" + self.password).encode('ascii') |
114 |
| - # The exact encoding of the authentication string is debated. |
115 |
| - # Anyway PyPI only accepts ascii for both username or password. |
116 |
| - auth = "Basic " + standard_b64encode(user_pass).decode('ascii') |
117 |
| - |
118 |
| - # Build up the MIME payload for the POST data |
119 |
| - boundary = '--------------GHSKFJDLGDS7543FJKLFHRE75642756743254' |
120 |
| - sep_boundary = b'\r\n--' + boundary.encode('ascii') |
121 |
| - end_boundary = sep_boundary + b'--\r\n' |
122 |
| - body = io.BytesIO() |
123 |
| - for key, value in data.items(): |
124 |
| - title = '\r\nContent-Disposition: form-data; name="%s"' % key |
125 |
| - # handle multiple entries for the same name |
126 |
| - if not isinstance(value, list): |
127 |
| - value = [value] |
128 |
| - for value in value: |
129 |
| - if type(value) is tuple: |
130 |
| - title += '; filename="%s"' % value[0] |
131 |
| - value = value[1] |
132 |
| - else: |
133 |
| - value = str(value).encode('utf-8') |
134 |
| - body.write(sep_boundary) |
135 |
| - body.write(title.encode('utf-8')) |
136 |
| - body.write(b"\r\n\r\n") |
137 |
| - body.write(value) |
138 |
| - body.write(end_boundary) |
139 |
| - body = body.getvalue() |
140 |
| - |
141 |
| - msg = "Submitting %s to %s" % (filename, self.repository) |
142 |
| - self.announce(msg, log.INFO) |
143 |
| - |
144 |
| - # build the Request |
145 |
| - headers = { |
146 |
| - 'Content-type': 'multipart/form-data; boundary=%s' % boundary, |
147 |
| - 'Content-length': str(len(body)), |
148 |
| - 'Authorization': auth, |
149 |
| - } |
150 |
| - |
151 |
| - request = Request(self.repository, data=body, |
152 |
| - headers=headers) |
153 |
| - # send the data |
154 |
| - try: |
155 |
| - result = urlopen(request) |
156 |
| - status = result.getcode() |
157 |
| - reason = result.msg |
158 |
| - except HTTPError as e: |
159 |
| - status = e.code |
160 |
| - reason = e.msg |
161 |
| - except OSError as e: |
162 |
| - self.announce(str(e), log.ERROR) |
163 |
| - raise |
164 |
| - |
165 |
| - if status == 200: |
166 |
| - self.announce('Server response (%s): %s' % (status, reason), |
167 |
| - log.INFO) |
168 |
| - if self.show_response: |
169 |
| - text = getattr(self, '_read_pypi_response', |
170 |
| - lambda x: None)(result) |
171 |
| - if text is not None: |
172 |
| - msg = '\n'.join(('-' * 75, text, '-' * 75)) |
173 |
| - self.announce(msg, log.INFO) |
174 |
| - else: |
175 |
| - msg = 'Upload failed (%s): %s' % (status, reason) |
176 |
| - self.announce(msg, log.ERROR) |
177 |
| - raise DistutilsError(msg) |
178 |
| - |
179 |
| - def _load_password_from_keyring(self): |
180 |
| - """ |
181 |
| - Attempt to load password from keyring. Suppress Exceptions. |
182 |
| - """ |
183 |
| - try: |
184 |
| - keyring = __import__('keyring') |
185 |
| - return keyring.get_password(self.repository, self.username) |
186 |
| - except Exception: |
187 |
| - pass |
188 |
| - |
189 |
| - def _prompt_for_password(self): |
190 |
| - """ |
191 |
| - Prompt for a password on the tty. Suppress Exceptions. |
192 |
| - """ |
193 |
| - try: |
194 |
| - return getpass.getpass() |
195 |
| - except (Exception, KeyboardInterrupt): |
196 |
| - pass |
| 16 | + self.announce("ERROR: " + msg, log.ERROR) |
| 17 | + raise RemovedCommandError(msg) |
0 commit comments