Skip to content

Commit a7b2318

Browse files
committed
[py3-compat] Use six for hybrid py2/py3 compatibility
1 parent 98674f8 commit a7b2318

File tree

4 files changed

+73
-44
lines changed

4 files changed

+73
-44
lines changed

.github/workflows/runtests.yml

+20-11
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,23 @@ jobs:
1414
- run: git config --global user.email runtest@localhost
1515
- run: nox --non-interactive --error-on-missing-interpreter --session runtests -- --git-default-branch=master
1616

17-
# runtests-py3:
18-
# runs-on: ubuntu-latest
19-
# steps:
20-
# - uses: wntrblm/[email protected]
21-
# with:
22-
# python-versions: "3.7"
23-
# - uses: actions/checkout@v4
24-
# - run: git config --global user.name runtest
25-
# - run: git config --global user.email runtest@localhost
26-
# - run: git config --global init.defaultBranch main
27-
# - run: nox --non-interactive --error-on-missing-interpreter --session runtests
17+
runtests-py3:
18+
runs-on: ubuntu-latest
19+
strategy:
20+
matrix:
21+
python-version:
22+
- "3.7"
23+
# - "3.8"
24+
# - "3.9"
25+
# - "3.10"
26+
# - "3.11"
27+
- "3.12"
28+
steps:
29+
- uses: wntrblm/[email protected]
30+
with:
31+
python-versions: ${{ matrix.python-version }}
32+
- uses: actions/checkout@v4
33+
- run: git config --global user.name runtest
34+
- run: git config --global user.email runtest@localhost
35+
- run: git config --global init.defaultBranch main
36+
- run: nox --non-interactive --error-on-missing-interpreter --session runtests

runtests.py

+41-28
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,10 @@
1010
"""
1111
from __future__ import print_function
1212

13+
# Python3 imports are commented out, six is used to offer py2+py3 compatibility
1314
import argparse
14-
import BaseHTTPServer
15-
import ConfigParser
15+
# from configparser import ConfigParser
16+
# from http.server import BaseHTTPRequestHandler, HTTPServer
1617
import json
1718
import os
1819
import random
@@ -26,7 +27,12 @@
2627
import time
2728
import traceback
2829
import unittest
29-
import urlparse
30+
# from urllib.parse import parse_qs, urlparse
31+
32+
33+
from six.moves.BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer
34+
from six.moves.configparser import ConfigParser
35+
from six.moves.urllib.parse import parse_qs, urlparse
3036

3137
from lxml import html
3238

@@ -35,6 +41,7 @@
3541
from trac.util.translation import _
3642

3743
import requests
44+
import six
3845

3946

4047
GIT = 'test-git-foo'
@@ -83,6 +90,9 @@ def git_check_output(*args, **kwargs):
8390
as a string.
8491
"""
8592
repo = kwargs.pop('repo', None)
93+
kwargs.setdefault('text', True)
94+
if six.PY2:
95+
del kwargs['text']
8696

8797
if repo is None:
8898
cmdargs = ["git"] + list(args)
@@ -136,9 +146,12 @@ def createTracEnvironment(cls, **kwargs):
136146
subprocess.check_output([TRAC_ADMIN_BIN, d(ENV), 'permission',
137147
'add', 'anonymous', 'TRAC_ADMIN'])
138148

139-
conf = ConfigParser.ConfigParser()
140-
with open(d(CONF), 'rb') as fp:
141-
conf.readfp(fp)
149+
conf = ConfigParser()
150+
with open(d(CONF), 'r') as fp:
151+
if six.PY2:
152+
conf.readfp(fp)
153+
else:
154+
conf.read_file(fp)
142155

143156
conf.add_section('components')
144157
conf.set('components', 'trac.versioncontrol.web_ui.browser.BrowserModule', 'disabled')
@@ -210,7 +223,7 @@ def createTracEnvironment(cls, **kwargs):
210223
conf.set('trac', 'permission_policies',
211224
'GitHubPolicy, %s' % old_permission_policies)
212225

213-
with open(d(CONF), 'wb') as fp:
226+
with open(d(CONF), 'w') as fp:
214227
conf.write(fp)
215228

216229
with open(d(HTDIGEST), 'w') as fp:
@@ -269,7 +282,7 @@ def makeGitCommit(repo, path, content, message='edit', branch=None):
269282

270283
if branch != GIT_DEFAULT_BRANCH:
271284
git_check_output('checkout', branch, repo=repo)
272-
with open(d(repo, path), 'wb') as fp:
285+
with open(d(repo, path), 'w') as fp:
273286
fp.write(content)
274287
git_check_output('add', path, repo=repo)
275288
git_check_output('commit', '-m', message, repo=repo)
@@ -406,11 +419,11 @@ def testLogin(self):
406419
response = requests.get(u('github/login'), allow_redirects=False)
407420
self.assertEqual(response.status_code, 302)
408421

409-
redirect_url = urlparse.urlparse(response.headers['Location'])
422+
redirect_url = urlparse(response.headers['Location'])
410423
self.assertEqual(redirect_url.scheme, 'https')
411424
self.assertEqual(redirect_url.netloc, 'github.com')
412425
self.assertEqual(redirect_url.path, '/login/oauth/authorize')
413-
params = urlparse.parse_qs(redirect_url.query, keep_blank_values=True)
426+
params = parse_qs(redirect_url.query, keep_blank_values=True)
414427
state = params['state'][0] # this is a random value
415428
self.assertEqual(params, {
416429
'client_id': ['01234567890123456789'],
@@ -490,7 +503,7 @@ def setUpClass(cls):
490503
cls.trac_env_broken = trac_env_broken
491504
cls.trac_env_broken_api = trac_env_broken_api
492505

493-
with open(d(SECRET), 'wb') as fp:
506+
with open(d(SECRET), 'w') as fp:
494507
fp.write('98765432109876543210')
495508

496509

@@ -505,11 +518,11 @@ def testLoginWithReqEmail(self):
505518
response = requests.get(u('github/login'), allow_redirects=False)
506519
self.assertEqual(response.status_code, 302)
507520

508-
redirect_url = urlparse.urlparse(response.headers['Location'])
521+
redirect_url = urlparse(response.headers['Location'])
509522
self.assertEqual(redirect_url.scheme, 'https')
510523
self.assertEqual(redirect_url.netloc, 'github.com')
511524
self.assertEqual(redirect_url.path, '/login/oauth/authorize')
512-
params = urlparse.parse_qs(redirect_url.query, keep_blank_values=True)
525+
params = parse_qs(redirect_url.query, keep_blank_values=True)
513526
state = params['state'][0] # this is a random value
514527
self.assertEqual(params, {
515528
'client_id': ['01234567890123456789'],
@@ -527,11 +540,11 @@ def loginAndVerifyClientId(self, expected_client_id):
527540
response = requests.get(u('github/login'), allow_redirects=False)
528541
self.assertEqual(response.status_code, 302)
529542

530-
redirect_url = urlparse.urlparse(response.headers['Location'])
543+
redirect_url = urlparse(response.headers['Location'])
531544
self.assertEqual(redirect_url.scheme, 'https')
532545
self.assertEqual(redirect_url.netloc, 'github.com')
533546
self.assertEqual(redirect_url.path, '/login/oauth/authorize')
534-
params = urlparse.parse_qs(redirect_url.query, keep_blank_values=True)
547+
params = parse_qs(redirect_url.query, keep_blank_values=True)
535548
state = params['state'][0] # this is a random value
536549
self.assertEqual(params, {
537550
'client_id': [expected_client_id],
@@ -636,8 +649,8 @@ def attemptValidOauth(self, testenv, callback, **kwargs):
636649
self.assertEqual(response.status_code, 302)
637650

638651
# Extract the state from the redirect
639-
redirect_url = urlparse.urlparse(response.headers['Location'])
640-
params = urlparse.parse_qs(redirect_url.query, keep_blank_values=True)
652+
redirect_url = urlparse(response.headers['Location'])
653+
params = parse_qs(redirect_url.query, keep_blank_values=True)
641654
state = params['state'][0] # this is a random value
642655
response = session.get(
643656
u('github/oauth'),
@@ -1095,13 +1108,13 @@ class GitHubPostCommitHookWithUpdateHookTests(TracGitHubTests):
10951108

10961109
@classmethod
10971110
def createUpdateHook(cls):
1098-
with open(d(UPDATEHOOK), 'wb') as fp:
1111+
with open(d(UPDATEHOOK), 'w') as fp:
10991112
# simple shell script to echo back all input
11001113
fp.write("""#!/bin/sh\nexec cat""")
11011114
os.fchmod(fp.fileno(), 0o755)
11021115

11031116
def createFailingUpdateHook(cls):
1104-
with open(d(UPDATEHOOK), 'wb') as fp:
1117+
with open(d(UPDATEHOOK), 'w') as fp:
11051118
fp.write("""#!/bin/sh\nexit 1""")
11061119
os.fchmod(fp.fileno(), 0o755)
11071120

@@ -1160,7 +1173,7 @@ class GitHubPostCommitHookWithCacheTests(GitHubPostCommitHookTests):
11601173
cached_git = True
11611174

11621175

1163-
class GitHubAPIMock(BaseHTTPServer.BaseHTTPRequestHandler):
1176+
class GitHubAPIMock(BaseHTTPRequestHandler):
11641177
def log_message(self, format, *args):
11651178
# Visibly differentiate GitHub API mock logging from tracd logs
11661179
sys.stderr.write("%s [%s] %s\n" %
@@ -1221,7 +1234,7 @@ def do_GET(self):
12211234
self.send_header("Content-Type", contenttype)
12221235
self.end_headers()
12231236

1224-
self.wfile.write(json.dumps(answer))
1237+
self.wfile.write(json.dumps(answer, ensure_ascii=True).encode('ascii'))
12251238

12261239
def do_POST(self):
12271240
md = self.server.mockdata
@@ -1239,9 +1252,9 @@ def do_POST(self):
12391252
chunk = self.rfile.read(chunk_size)
12401253
if not chunk:
12411254
break
1242-
L.append(chunk)
1255+
L.append(chunk.decode('ascii'))
12431256
size_remaining -= len(L[-1])
1244-
args = urlparse.parse_qs(''.join(L))
1257+
args = parse_qs(''.join(L))
12451258

12461259
retcode = 404
12471260
answer = {}
@@ -1255,7 +1268,7 @@ def do_POST(self):
12551268
self.send_response(retcode)
12561269
self.send_header("Content-Type", contenttype)
12571270
self.end_headers()
1258-
self.wfile.write(json.dumps(answer))
1271+
self.wfile.write(json.dumps(answer, ensure_ascii=True).encode('ascii'))
12591272

12601273

12611274
class TracContext(object):
@@ -1836,7 +1849,7 @@ def test_014_hook_membership_event_add_team(self):
18361849
self.assertGreater(len(data), 0, "No groups returned after update")
18371850
self.assertIn(users[0]["login"], data,
18381851
"User %s expected after update, but not present" % users[0]["login"])
1839-
self.assertItemsEqual(
1852+
six.assertCountEqual(self,
18401853
data[users[0]["login"]],
18411854
(u"github-%s-justice-league" % self.organization, u"github-%s" % self.organization),
18421855
"User %s does not have expected groups after update" % users[0]["login"])
@@ -1901,7 +1914,7 @@ def test_015_hook_membership_event_add_member(self):
19011914
self.assertGreater(len(data), 0, "No groups returned after update")
19021915
self.assertIn(users[1]["login"], data,
19031916
"User %s expected after update, but not present" % users[1]["login"])
1904-
self.assertItemsEqual(
1917+
six.assertCountEqual(self,
19051918
data[users[1]["login"]],
19061919
(u"github-%s-justice-league" % self.organization, u"github-%s" % self.organization),
19071920
"User %s does not have expected groups after update" % users[1]["login"])
@@ -2113,7 +2126,7 @@ def updateMockData(md, retcode=None, contenttype=None, answers=None,
21132126
JSON-encoded and returned for requests to the paths.
21142127
:param postcallback: A callback function called for the next POST requests.
21152128
Arguments are the requested path and a dict of POST
2116-
data as returned by `urlparse.parse_qs()`. The
2129+
data as returned by `parse_qs()`. The
21172130
callback should return a tuple `(retcode, answer)`
21182131
where `retcode` is the HTTP return code and `answer`
21192132
will be JSON-encoded and sent to the client. Note that
@@ -2148,7 +2161,7 @@ def apiMockServer(port, mockdata):
21482161
be JSON-encoded and returned. Use `updateMockData()` to
21492162
update the contents of the mockdata dict.
21502163
"""
2151-
httpd = BaseHTTPServer.HTTPServer(('127.0.0.1', port), GitHubAPIMock)
2164+
httpd = HTTPServer(('127.0.0.1', port), GitHubAPIMock)
21522165
# Make mockdata available to server
21532166
httpd.mockdata = mockdata
21542167
httpd.serve_forever()

setup.py

+3
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@
1818
namespace_packages=['tracext'],
1919
platforms='all',
2020
license='BSD',
21+
install_requires=[
22+
'six==1.16.0',
23+
],
2124
extras_require={'oauth': ['requests_oauthlib >= 0.5']},
2225
entry_points={'trac.plugins': [
2326
'github.browser = tracext.github:GitHubBrowser',

tracext/github/__init__.py

+9-5
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@
2929
from trac.web.auth import LoginModule
3030
from trac.web.chrome import add_warning
3131

32+
import six
33+
3234
def _config_secret(value):
3335
if re.match(r'[A-Z_]+', value):
3436
return os.environ.get(value, '')
@@ -330,9 +332,11 @@ def __init__(self, api, env, fullname):
330332
self.api = api
331333
self.env = env
332334
self.name = fullname
333-
# _fullname needs to be a string, not a unicode string, otherwise the
334-
# cache object won't convert it into a hash.
335-
self._fullname = fullname.encode('utf-8')
335+
self._fullname = fullname
336+
if six.PY2:
337+
# Trac's @cached for py2 does an isinstance(..., str) so we need a native
338+
# string type
339+
self._fullname = fullname.encode('utf-8')
336340
# next try: immediately
337341
self._next_update = datetime.now() - timedelta(seconds=10)
338342
self._cached_result = self._apiresult_error()
@@ -656,10 +660,10 @@ def github_api(self, url, *args):
656660
additional positional arguments.
657661
"""
658662
import requests
659-
import urllib
663+
from six.moves.urllib.parse import quote
660664

661665
github_api_url = os.environ.get("TRAC_GITHUB_API_URL", "https://api.github.com/")
662-
formatted_url = github_api_url + url.format(*(urllib.quote(str(x)) for x in args))
666+
formatted_url = github_api_url + url.format(*(quote(str(x)) for x in args))
663667
access_token = _config_secret(self.access_token)
664668
self.log.debug("Hitting GitHub API endpoint %s with user %s", formatted_url, self.username) # pylint: disable=no-member
665669
results = []

0 commit comments

Comments
 (0)