Skip to content

Commit 3a0b736

Browse files
author
Burak Yigit Kaya
committed
Add Python 3 support
1 parent e624385 commit 3a0b736

File tree

4 files changed

+150
-84
lines changed

4 files changed

+150
-84
lines changed

phabricator/__init__.py

Lines changed: 41 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -14,21 +14,23 @@
1414
except:
1515
__version__ = 'unknown'
1616

17+
import collections
1718
import copy
1819
import hashlib
19-
import httplib
2020
import json
2121
import os.path
2222
import re
2323
import socket
2424
import time
25-
import urllib
26-
import urlparse
2725

28-
from collections import defaultdict
26+
from ._compat import (
27+
MutableMapping, iteritems, string_types, httplib, urlparse, urlencode,
28+
)
29+
2930

3031
__all__ = ['Phabricator']
3132

33+
3234
# Default phabricator interfaces
3335
INTERFACES = json.loads(open(os.path.join(os.path.dirname(__file__), 'interfaces.json'), 'r').read())
3436

@@ -79,11 +81,11 @@
7981
'pair': tuple,
8082

8183
# str types
82-
'str': basestring,
83-
'string': basestring,
84-
'phid': basestring,
85-
'guids': basestring,
86-
'type': basestring,
84+
'str': string_types,
85+
'string': string_types,
86+
'phid': string_types,
87+
'guids': string_types,
88+
'type': string_types,
8789
}
8890

8991
STR_RE = re.compile(r'([a-zA-Z_]+)')
@@ -108,9 +110,9 @@ def map_param_type(param_type):
108110
sub_match = STR_RE.match(sub_type)
109111
sub_type = sub_match.group(0).lower()
110112

111-
return [PARAM_TYPE_MAP.setdefault(sub_type, basestring)]
113+
return [PARAM_TYPE_MAP.setdefault(sub_type, string_types)]
112114

113-
return PARAM_TYPE_MAP.setdefault(main_type, basestring)
115+
return PARAM_TYPE_MAP.setdefault(main_type, string_types)
114116

115117

116118
def parse_interfaces(interfaces):
@@ -119,9 +121,9 @@ def parse_interfaces(interfaces):
119121
This performs the logic of parsing the non-standard params dict
120122
and then returning a dict Resource can understand
121123
"""
122-
parsed_interfaces = defaultdict(dict)
124+
parsed_interfaces = collections.defaultdict(dict)
123125

124-
for m, d in interfaces.iteritems():
126+
for m, d in iteritems(interfaces):
125127
app, func = m.split('.', 1)
126128

127129
method = parsed_interfaces[app][func] = {}
@@ -133,7 +135,7 @@ def parse_interfaces(interfaces):
133135
method['optional'] = {}
134136
method['required'] = {}
135137

136-
for name, type_info in dict(d['params']).iteritems():
138+
for name, type_info in iteritems(dict(d['params'])):
137139
# Usually in the format: <optionality> <param_type>
138140
info_pieces = type_info.split(' ', 1)
139141

@@ -176,42 +178,29 @@ class InvalidAccessToken(APIError):
176178
pass
177179

178180

179-
class Result(object):
181+
class Result(MutableMapping):
180182
def __init__(self, response):
181183
self.response = response
182184

183-
def __repr__(self):
184-
return '<%s: %s>' % (self.__class__.__name__, repr(self.response))
185-
186-
def __iter__(self):
187-
for r in self.response:
188-
yield r
189-
190185
def __getitem__(self, key):
191186
return self.response[key]
192187

193-
def __getattr__(self, key):
194-
return self.response[key]
188+
__getattr__ = __getitem__
189+
190+
def __setitem__(self, key, value):
191+
self.response[key] = value
195192

196-
def __getstate__(self):
197-
return self.response
193+
def __delitem__(self, key):
194+
del self.response[key]
198195

199-
def __setstate__(self, state):
200-
self.response = state
196+
def __iter__(self):
197+
return iter(self.response)
201198

202199
def __len__(self):
203200
return len(self.response.keys())
204201

205-
def keys(self):
206-
return self.response.keys()
207-
208-
def iteritems(self):
209-
for k, v in self.response.iteritems():
210-
yield k, v
211-
212-
def itervalues(self):
213-
for v in self.response.itervalues():
214-
yield v
202+
def __repr__(self):
203+
return '<%s: %s>' % (type(self).__name__, repr(self.response))
215204

216205

217206
class Resource(object):
@@ -240,17 +229,17 @@ def validate_kwarg(key, target):
240229
# Always allow list
241230
if isinstance(key, list):
242231
return all([validate_kwarg(x, target[0]) for x in key])
243-
return isinstance(key, target)
244-
245-
for k in resource.get('required', []):
246-
if k not in [x.split(':')[0] for x in kwargs.keys()]:
247-
raise ValueError('Missing required argument: %s' % k)
248-
if isinstance(kwargs.get(k), list) and not isinstance(resource['required'][k], list):
249-
raise ValueError('Wrong argument type: %s is not a list' % k)
250-
elif not validate_kwarg(kwargs.get(k), resource['required'][k]):
251-
if isinstance(resource['required'][k], list):
252-
raise ValueError('Wrong arguemnt type: %s is not a list of %ss' % (k, resource['required'][k][0]))
253-
raise ValueError('Wrong arguemnt type: %s is not a %s' % (k, resource['required'][k]))
232+
return isinstance(key, tuple(target) if isinstance(target, list) else target)
233+
234+
for key, val in resource.get('required', {}).items():
235+
if key not in [x.split(':')[0] for x in kwargs.keys()]:
236+
raise ValueError('Missing required argument: %s' % key)
237+
if isinstance(kwargs.get(key), list) and not isinstance(val, list):
238+
raise ValueError('Wrong argument type: %s is not a list' % key)
239+
elif not validate_kwarg(kwargs.get(key), val):
240+
if isinstance(val, list):
241+
raise ValueError('Wrong argument type: %s is not a list of %ss' % (key, val[0]))
242+
raise ValueError('Wrong argument type: %s is not a %s' % (key, val))
254243

255244
conduit = self.api.conduit
256245

@@ -280,7 +269,7 @@ def validate_kwarg(key, target):
280269
'Content-Type': 'application/x-www-form-urlencoded'
281270
}
282271

283-
body = urllib.urlencode({
272+
body = urlencode({
284273
"params": json.dumps(kwargs),
285274
"output": self.api.response_format
286275
})
@@ -355,7 +344,8 @@ def connect(self):
355344
}
356345

357346
def generate_hash(self, token):
358-
return hashlib.sha1(token + self.api.certificate).hexdigest()
347+
source_string = (token + self.api.certificate).encode('utf-8')
348+
return hashlib.sha1(source_string).hexdigest()
359349

360350
def update_interfaces(self):
361351
query = Resource(api=self, method='conduit', endpoint='query')

phabricator/_compat.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import sys
2+
3+
PY3 = sys.version_info[0] >= 3
4+
5+
try:
6+
from collections.abc import MutableMapping
7+
except ImportError:
8+
from collections import MutableMapping
9+
10+
try:
11+
import httplib
12+
except ImportError:
13+
import http.client as httplib
14+
15+
try:
16+
import urlparse
17+
except ImportError:
18+
import urllib.parse as urlparse
19+
20+
try:
21+
from urllib import urlencode
22+
except ImportError:
23+
from urllib.parse import urlencode
24+
25+
if PY3:
26+
str_type = str
27+
string_types = str,
28+
29+
def iteritems(d, **kw):
30+
return iter(d.items(**kw))
31+
else:
32+
str_type = unicode
33+
string_types = basestring,
34+
35+
def iteritems(d, **kw):
36+
return d.iteritems(**kw)

phabricator/tests.py

Lines changed: 53 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,77 +1,100 @@
1+
try:
2+
import unittest2 as unittest
3+
except ImportError:
4+
import unittest
5+
6+
try:
7+
from StringIO import StringIO
8+
except ImportError:
9+
from io import StringIO
10+
11+
try:
12+
import unittest.mock as mock
13+
except ImportError:
14+
import mock
15+
16+
117
import phabricator
2-
import unittest
3-
from StringIO import StringIO
4-
from mock import patch, Mock
18+
519

620
RESPONSES = {
721
'conduit.connect': '{"result":{"connectionID":1759,"sessionKey":"lwvyv7f6hlzb2vawac6reix7ejvjty72svnir6zy","userPHID":"PHID-USER-6ij4rnamb2gsfpdkgmny"},"error_code":null,"error_info":null}',
822
'user.whoami': '{"result":{"phid":"PHID-USER-6ij4rnamz2gxfpbkamny","userName":"testaccount","realName":"Test Account"},"error_code":null,"error_info":null}',
923
'maniphest.find': '{"result":{"PHID-TASK-4cgpskv6zzys6rp5rvrc":{"id":"722","phid":"PHID-TASK-4cgpskv6zzys6rp5rvrc","authorPHID":"PHID-USER-5022a9389121884ab9db","ownerPHID":"PHID-USER-5022a9389121884ab9db","ccPHIDs":["PHID-USER-5022a9389121884ab9db","PHID-USER-ba8aeea1b3fe2853d6bb"],"status":"3","priority":"Needs Triage","title":"Relations should be two-way","description":"When adding a differential revision you can specify Maniphest Tickets to add the relation. However, this doesnt add the relation from the ticket -> the differently.(This was added via the commit message)","projectPHIDs":["PHID-PROJ-358dbc2e601f7e619232","PHID-PROJ-f58a9ac58c333f106a69"],"uri":"https:\/\/secure.phabricator.com\/T722","auxiliary":[],"objectName":"T722","dateCreated":"1325553508","dateModified":"1325618490"}},"error_code":null,"error_info":null}'
1024
}
1125

26+
CERTIFICATE = (
27+
'fdhcq3zsyijnm4h6gmh43zue5umsmng5t4dlwodvmiz4cnc6fl6f'
28+
'zrvjbfg2ftktrcddan7b3xtgmfge2afbrh4uwam6pfxpq5dbkhbl'
29+
'6mgaijdzpq5efw2ynlnjhoeqyh6dakl4yg346gbhabzkcxreu7hc'
30+
'jhw6vo6wwa7ky2sjdk742khlgsakwtme6sr2dfkhlxxkcqw3jngy'
31+
'rq5zj7m6m7hnscuzlzsviawnvg47pe7l4hxiexpbb5k456r'
32+
)
33+
1234
# Protect against local user's .arcrc interference.
1335
phabricator.ARCRC = {}
1436

37+
1538
class PhabricatorTest(unittest.TestCase):
1639
def setUp(self):
1740
self.api = phabricator.Phabricator(username='test', certificate='test', host='http://localhost')
18-
self.api.certificate = "fdhcq3zsyijnm4h6gmh43zue5umsmng5t4dlwodvmiz4cnc6fl6f" + \
19-
"zrvjbfg2ftktrcddan7b3xtgmfge2afbrh4uwam6pfxpq5dbkhbl" + \
20-
"6mgaijdzpq5efw2ynlnjhoeqyh6dakl4yg346gbhabzkcxreu7hc" + \
21-
"jhw6vo6wwa7ky2sjdk742khlgsakwtme6sr2dfkhlxxkcqw3jngy" + \
22-
"rq5zj7m6m7hnscuzlzsviawnvg47pe7l4hxiexpbb5k456r"
41+
self.api.certificate = CERTIFICATE
2342

2443
def test_generate_hash(self):
2544
token = '12345678'
2645
hashed = self.api.generate_hash(token)
27-
self.assertEquals(hashed, 'f8d3bea4e58a2b2967d93d5b307bfa7c693b2e7f')
46+
self.assertEqual(hashed, 'f8d3bea4e58a2b2967d93d5b307bfa7c693b2e7f')
2847

29-
@patch('phabricator.httplib.HTTPConnection')
48+
@mock.patch('phabricator.httplib.HTTPConnection')
3049
def test_connect(self, mock_connection):
31-
mock = mock_connection.return_value = Mock()
32-
mock.getresponse.return_value = StringIO(RESPONSES['conduit.connect'])
50+
mock_obj = mock_connection.return_value = mock.Mock()
51+
mock_obj.getresponse.return_value = StringIO(RESPONSES['conduit.connect'])
3352

3453
api = phabricator.Phabricator(username='test', certificate='test', host='http://localhost')
3554
api.connect()
36-
self.assertTrue('sessionKey' in api.conduit.keys())
37-
self.assertTrue('connectionID' in api.conduit.keys())
55+
keys = api.conduit.keys()
56+
self.assertIn('sessionKey', keys)
57+
self.assertIn('connectionID', keys)
3858

39-
@patch('phabricator.httplib.HTTPConnection')
59+
@mock.patch('phabricator.httplib.HTTPConnection')
4060
def test_user_whoami(self, mock_connection):
41-
mock = mock_connection.return_value = Mock()
42-
mock.getresponse.return_value = StringIO(RESPONSES['user.whoami'])
61+
mock_obj = mock_connection.return_value = mock.Mock()
62+
mock_obj.getresponse.return_value = StringIO(RESPONSES['user.whoami'])
4363

4464
api = phabricator.Phabricator(username='test', certificate='test', host='http://localhost')
4565
api.conduit = True
4666

47-
self.assertEqual('testaccount', api.user.whoami()['userName'])
67+
self.assertEqual(api.user.whoami()['userName'], 'testaccount')
4868

49-
@patch('phabricator.httplib.HTTPConnection')
69+
@mock.patch('phabricator.httplib.HTTPConnection')
5070
def test_maniphest_find(self, mock_connection):
51-
mock = mock_connection.return_value = Mock()
52-
mock.getresponse.return_value = StringIO(RESPONSES['maniphest.find'])
71+
mock_obj = mock_connection.return_value = mock.Mock()
72+
mock_obj.getresponse.return_value = StringIO(RESPONSES['maniphest.find'])
5373

5474
api = phabricator.Phabricator(username='test', certificate='test', host='http://localhost')
5575
api.conduit = True
5676

57-
result = api.maniphest.find(ownerphids=["PHID-USER-5022a9389121884ab9db"])
58-
self.assertEqual(1, len(result))
77+
result = api.maniphest.find(ownerphids=['PHID-USER-5022a9389121884ab9db'])
78+
self.assertEqual(len(result), 1)
5979

6080
# Test iteration
61-
self.assertTrue(isinstance([x for x in result], list))
81+
self.assertIsInstance([x for x in result], list)
6282

6383
# Test getattr
64-
self.assertEqual("3", result["PHID-TASK-4cgpskv6zzys6rp5rvrc"]["status"])
84+
self.assertEqual(result['PHID-TASK-4cgpskv6zzys6rp5rvrc']['status'], '3')
6585

6686
def test_validation(self):
6787
self.api.conduit = True
6888

89+
self.assertRaises(ValueError, self.api.differential.find)
90+
with self.assertRaises(ValueError):
91+
self.api.differential.find(query=1)
92+
with self.assertRaises(ValueError):
93+
self.api.differential.find(query='1')
94+
with self.assertRaises(ValueError):
95+
self.api.differential.find(query='1', guids='1')
6996
with self.assertRaises(ValueError):
70-
self.assertRaises(ValueError, self.api.differential.find())
71-
self.assertRaises(ValueError, self.api.differential.find(query=1))
72-
self.assertRaises(ValueError, self.api.differential.find(query="1"))
73-
self.assertRaises(ValueError, self.api.differential.find(query="1", guids="1"))
74-
self.assertRaises(ValueError, self.api.differential.find(query="1", guids=["1"]))
97+
self.api.differential.find(query='1', guids=['1'])
7598

7699

77100
if __name__ == '__main__':

setup.py

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,17 @@
11
#!/usr/bin/env python
22

3+
import sys
4+
35
from setuptools import setup, find_packages
46

7+
tests_requires = ['pytest']
8+
9+
if sys.version_info[:2] < (2, 7):
10+
tests_requires.append('unittest2')
11+
12+
if sys.version_info[:2] <= (3, 3):
13+
tests_requires.append('mock')
14+
515
setup(
616
name='phabricator',
717
version='0.5.0',
@@ -12,13 +22,20 @@
1222
packages=find_packages(),
1323
zip_safe=False,
1424
test_suite='nose.collector',
15-
install_requires=[''],
16-
tests_require=['nose', 'unittest2', 'mock'],
25+
tests_require=tests_requires,
1726
include_package_data=True,
1827
classifiers=[
1928
'Intended Audience :: Developers',
2029
'Intended Audience :: System Administrators',
2130
'Operating System :: OS Independent',
22-
'Topic :: Software Development'
31+
'Topic :: Software Development',
32+
'Programming Language :: Python',
33+
'Programming Language :: Python :: 2',
34+
'Programming Language :: Python :: 2.6',
35+
'Programming Language :: Python :: 2.7',
36+
'Programming Language :: Python :: 3',
37+
'Programming Language :: Python :: 3.3',
38+
'Programming Language :: Python :: 3.4',
39+
'Programming Language :: Python :: 3.5',
2340
],
2441
)

0 commit comments

Comments
 (0)