Skip to content

Commit 0575fce

Browse files
abncrobinso
authored andcommitted
Use requests instead of pycurl
pycurl doesn't work on python3, and python-requests should do everything we need.
1 parent cacd541 commit 0575fce

4 files changed

Lines changed: 80 additions & 148 deletions

File tree

bin/bugzilla

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@ import socket
2323
import sys
2424
import tempfile
2525

26-
from urllib2 import HTTPError
2726
from xmlrpclib import Fault, ProtocolError
27+
from requests.exceptions import SSLError
2828

2929
import bugzilla
3030

@@ -1173,7 +1173,7 @@ if __name__ == '__main__':
11731173
log.debug("", exc_info=True)
11741174
print("\nConnection lost/failed: %s" % str(e))
11751175
sys.exit(2)
1176-
except (Fault, HTTPError):
1176+
except (Fault, bugzilla.BugzillaError):
11771177
e = sys.exc_info()[1]
11781178
log.debug("", exc_info=True)
11791179
print("\nServer error: %s" % str(e))
@@ -1184,16 +1184,13 @@ if __name__ == '__main__':
11841184
print("\nInvalid server response: %d %s" % (e.errcode, e.errmsg))
11851185

11861186
# Give SSL recommendations
1187-
import pycurl
1188-
sslerrcodes = [getattr(pycurl, ename) for ename in dir(pycurl) if
1189-
ename.startswith("E_SSL")]
1190-
if e.errcode in sslerrcodes:
1187+
if isinstance(e, SSLError):
11911188
print("\nIf you trust the remote server, you can work "
11921189
"around this error with:\n"
11931190
" bugzilla --nosslverify ...")
11941191

11951192
# Detect redirect
1196-
redir = (e.headers and e.headers.getheader("location", 0) or None)
1193+
redir = (e.headers and 'location' in e.headers)
11971194
if redir:
11981195
print("\nServer was attempting a redirect. Try: "
11991196
" bugzilla --bugzilla %s ..." % redir)

bugzilla/base.py

Lines changed: 73 additions & 138 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,11 @@
1515

1616
from ConfigParser import SafeConfigParser
1717
from cookielib import LoadError, LWPCookieJar, MozillaCookieJar
18-
from urllib2 import Request, HTTPError, build_opener
18+
from io import BytesIO
1919
from urlparse import urlparse, parse_qsl
20-
from StringIO import StringIO
2120
from xmlrpclib import Binary, Fault, ProtocolError, ServerProxy, Transport
2221

23-
import pycurl
22+
import requests
2423

2524
from bugzilla import __version__, log
2625
from bugzilla.bug import _Bug, _User
@@ -63,20 +62,6 @@ def _detect_filetype(fname):
6362
return None
6463

6564

66-
def _decode_rfc2231_value(val):
67-
# BUG WORKAROUND: decode_header doesn't work unless there's whitespace
68-
# around the encoded string (see http://bugs.python.org/issue1079)
69-
from email import utils
70-
from email import header
71-
72-
# pylint: disable=W1401
73-
# Anomolous backslash in string
74-
val = utils.ecre.sub(' \g<0> ', val)
75-
val = val.strip('"')
76-
return ''.join(f[0].decode(f[1] or 'us-ascii')
77-
for f in header.decode_header(val))
78-
79-
8065
def _build_cookiejar(cookiefile):
8166
cj = MozillaCookieJar(cookiefile)
8267
if cookiefile is None:
@@ -110,134 +95,86 @@ def _build_cookiejar(cookiefile):
11095
return retcj
11196

11297

113-
def _check_http_error(uri, request_body, response_data):
114-
# This pulls some of the guts from urllib to give us HTTP error
115-
# code checking. Wrap it all in try/except incase this breaks in
116-
# the future, it's only for error handling.
117-
try:
118-
import httplib
119-
import urllib
120-
121-
class FakeSocket(StringIO):
122-
def makefile(self, *args, **kwarg):
123-
ignore = args
124-
ignore = kwarg
125-
return self
126-
127-
httpresp = httplib.HTTPResponse(FakeSocket(response_data))
128-
httpresp.begin()
129-
resp = urllib.addinfourl(FakeSocket(response_data), httpresp.msg, uri)
130-
resp.code = httpresp.status
131-
resp.msg = httpresp.reason
132-
133-
req = Request(uri)
134-
req.add_data(request_body)
135-
opener = build_opener()
136-
137-
for handler in opener.handlers:
138-
if hasattr(handler, "http_response"):
139-
handler.http_response(req, resp)
140-
except HTTPError:
141-
raise
142-
except:
143-
pass
144-
98+
class RequestsTransport(Transport):
99+
user_agent = 'Python/Bugzilla'
145100

146-
class _CURLTransport(Transport):
147101
def __init__(self, url, cookiejar,
148102
sslverify=True, sslcafile=None, debug=0):
103+
# pylint: disable=W0231
104+
# pylint does not handle multiple import of Transport well
149105
if hasattr(Transport, "__init__"):
150106
Transport.__init__(self, use_datetime=False)
151107

152108
self.verbose = debug
109+
self._cookiejar = cookiejar
153110

154111
# transport constructor needs full url too, as xmlrpc does not pass
155112
# scheme to request
156113
self.scheme = urlparse(url)[0]
157114
if self.scheme not in ["http", "https"]:
158115
raise Exception("Invalid URL scheme: %s (%s)" % (self.scheme, url))
159116

160-
self.c = pycurl.Curl()
161-
self.c.setopt(pycurl.POST, 1)
162-
self.c.setopt(pycurl.CONNECTTIMEOUT, 30)
163-
self.c.setopt(pycurl.HTTPHEADER, [
164-
"Content-Type: text/xml",
165-
])
166-
self.c.setopt(pycurl.VERBOSE, debug)
167-
168-
self.set_cookiejar(cookiejar)
117+
self.use_https = self.scheme == 'https'
169118

170-
# ssl settings
171-
if self.scheme == "https":
172-
# override curl built-in ca file setting
173-
if sslcafile is not None:
174-
self.c.setopt(pycurl.CAINFO, sslcafile)
175-
176-
# disable ssl verification
177-
if not sslverify:
178-
self.c.setopt(pycurl.SSL_VERIFYPEER, 0)
179-
self.c.setopt(pycurl.SSL_VERIFYHOST, 0)
119+
self.request_defaults = {
120+
'cert': sslcafile if self.use_https else None,
121+
'cookies': cookiejar,
122+
'verify': sslverify,
123+
'headers': {
124+
'Content-Type': 'text/xml',
125+
'User-Agent': self.user_agent,
126+
}
127+
}
180128

181-
def set_cookiejar(self, cj):
182-
self.c.setopt(pycurl.COOKIEFILE, cj.filename or "")
183-
self.c.setopt(pycurl.COOKIEJAR, cj.filename or "")
129+
def parse_response(self, response):
130+
""" Parse XMLRPC response """
131+
parser, unmarshaller = self.getparser()
132+
parser.feed(response.text.encode('utf-8'))
133+
parser.close()
134+
return unmarshaller.close()
184135

185-
def get_cookies(self):
186-
return self.c.getinfo(pycurl.INFO_COOKIELIST)
136+
def _request_helper(self, url, request_body):
137+
"""
138+
A helper method to assist in making a request and provide a parsed
139+
response.
140+
"""
141+
try:
142+
response = requests.post(
143+
url, data=request_body, **self.request_defaults)
187144

188-
def _open_helper(self, url, request_body):
189-
self.c.setopt(pycurl.URL, url)
190-
self.c.setopt(pycurl.POSTFIELDS, request_body)
145+
# We expect utf-8 from the server
146+
response.encoding = 'UTF-8'
191147

192-
b = StringIO()
193-
headers = StringIO()
194-
self.c.setopt(pycurl.WRITEFUNCTION, b.write)
195-
self.c.setopt(pycurl.HEADERFUNCTION, headers.write)
148+
# update/set any cookies
149+
for cookie in response.cookies:
150+
self._cookiejar.set_cookie(cookie)
196151

197-
try:
198-
m = pycurl.CurlMulti()
199-
m.add_handle(self.c)
200-
while True:
201-
if m.perform()[0] == -1:
202-
continue
203-
num, ok, err = m.info_read()
204-
ignore = num
152+
if self._cookiejar.filename is not None:
153+
# Save is required only if we have a filename
154+
self._cookiejar.save()
205155

206-
if ok:
207-
m.remove_handle(self.c)
208-
break
209-
if err:
210-
m.remove_handle(self.c)
211-
raise pycurl.error(*err[0][1:])
212-
if m.select(.1) == -1:
213-
# Looks like -1 is passed straight up from select(2)
214-
# While it's not true that this will always be caused
215-
# by SIGINT, it should be the only case we hit
216-
log.debug("pycurl select failed, this likely came from "
217-
"SIGINT, raising")
218-
m.remove_handle(self.c)
219-
raise KeyboardInterrupt
220-
except pycurl.error:
156+
response.raise_for_status()
157+
return self.parse_response(response)
158+
except requests.RequestException:
221159
e = sys.exc_info()[1]
222-
raise ProtocolError(url, e[0], e[1], None)
223-
224-
b.seek(0)
225-
headers.seek(0)
226-
return b, headers
160+
raise ProtocolError(
161+
url, response.status_code, str(e), response.headers)
162+
except Fault:
163+
raise sys.exc_info()[1]
164+
except Exception:
165+
# pylint: disable=W0201
166+
e = BugzillaError(str(sys.exc_info()[1]))
167+
e.__traceback__ = sys.exc_info()[2]
168+
raise e
227169

228170
def request(self, host, handler, request_body, verbose=0):
229171
self.verbose = verbose
230172
url = "%s://%s%s" % (self.scheme, host, handler)
231173

232174
# xmlrpclib fails to escape \r
233-
request_body = request_body.replace('\r', '&#xd;')
234-
235-
body, headers = self._open_helper(url, request_body)
236-
_check_http_error(url, body.getvalue(), headers.getvalue())
237-
238-
return self.parse_response(body)
239-
175+
request_body = request_body.replace(b'\r', b'&#xd;')
240176

177+
return self._request_helper(url, request_body)
241178

242179

243180
class BugzillaError(Exception):
@@ -475,8 +412,8 @@ def connect(self, url=None):
475412
url = self.url
476413
url = self.fix_url(url)
477414

478-
self._transport = _CURLTransport(url, self._cookiejar,
479-
sslverify=self._sslverify)
415+
self._transport = RequestsTransport(
416+
url, self._cookiejar, sslverify=self._sslverify)
480417
self._transport.user_agent = self.user_agent
481418
self._proxy = ServerProxy(url, self._transport)
482419

@@ -1293,29 +1230,27 @@ def attachfile(self, idlist, attachfile, description, **kwargs):
12931230
def openattachment(self, attachid):
12941231
'''Get the contents of the attachment with the given attachment ID.
12951232
Returns a file-like object.'''
1233+
1234+
def get_filename(headers):
1235+
import re
1236+
1237+
match = re.search(
1238+
r'^.*filename="?(.*)"$',
1239+
headers.get('content-disposition', '')
1240+
)
1241+
1242+
# default to attchid if no match was found
1243+
return match.group(1) if match else attachid
1244+
12961245
att_uri = self._attachment_uri(attachid)
12971246

1298-
headers = {}
1299-
ret = StringIO()
1247+
response = requests.get(att_uri, cookies=self._cookiejar, stream=True)
13001248

1301-
def headers_cb(buf):
1302-
if not ":" in buf:
1303-
return
1304-
name, val = buf.split(":", 1)
1305-
headers[name.lower()] = val
1306-
1307-
c = pycurl.Curl()
1308-
c.setopt(pycurl.URL, att_uri)
1309-
c.setopt(pycurl.WRITEFUNCTION, ret.write)
1310-
c.setopt(pycurl.HEADERFUNCTION, headers_cb)
1311-
c.setopt(pycurl.COOKIEFILE, self._cookiejar.filename or "")
1312-
c.perform()
1313-
c.close()
1314-
1315-
disp = headers['content-disposition'].split(';')
1316-
disp.pop(0)
1317-
parms = dict([p.strip().split("=", 1) for p in disp])
1318-
ret.name = _decode_rfc2231_value(parms['filename'])
1249+
ret = BytesIO()
1250+
for chunk in response.iter_content(chunk_size=1024):
1251+
if chunk:
1252+
ret.write(chunk)
1253+
ret.name = get_filename(response.headers)
13191254

13201255
# Hooray, now we have a file-like object with .read() and .name
13211256
ret.seek(0)

python-bugzilla.spec

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ BuildRequires: python-setuptools
1919
BuildRequires: python-setuptools-devel
2020
%endif
2121

22-
BuildRequires: python-pycurl
23-
Requires: python-pycurl
22+
BuildRequires: python-requests
23+
Requires: python-requests
2424

2525
%if ! 0%{?rhel} || 0%{?rhel} >= 6
2626
Requires: python-magic

requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
pycurl
1+
requests

0 commit comments

Comments
 (0)