Skip to content

Commit 92fb52b

Browse files
committed
Python 3.14 support
1 parent c5d5402 commit 92fb52b

9 files changed

Lines changed: 51 additions & 52 deletions

File tree

.github/workflows/ci.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@ jobs:
1111
timeout-minutes: 20
1212
runs-on: ubuntu-latest
1313
strategy:
14-
max-parallel: 5
14+
max-parallel: 6
1515
matrix:
16-
python-version: ['3.9', '3.10', '3.11', '3.12', '3.13']
16+
python-version: ['3.9', '3.10', '3.11', '3.12', '3.13', '3.14']
1717

1818
steps:
1919
- name: checkout

pywb/recorder/test/test_recorder.py

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ def test_record_warc_1(self):
150150

151151
resp = self._test_warc_write(recorder_app, 'httpbin.org', '/get?foo=bar')
152152
assert b'HTTP/1.1 200 OK' in resp.body
153-
assert b'"foo": "bar"' in resp.body
153+
assert b'"foo":"bar"' in resp.body.replace(b' ', b'')
154154

155155
self._test_all_warcs('/warcs/', 1)
156156

@@ -160,7 +160,7 @@ def test_record_warc_2(self):
160160

161161
resp = self._test_warc_write(recorder_app, 'httpbin.org', '/get?foo=bar')
162162
assert b'HTTP/1.1 200 OK' in resp.body
163-
assert b'"foo": "bar"' in resp.body
163+
assert b'"foo":"bar"' in resp.body.replace(b' ', b'')
164164

165165
self._test_all_warcs('/warcs/', 2)
166166

@@ -262,7 +262,7 @@ def test_record_skip_wrong_coll(self):
262262

263263
resp = self._test_warc_write(recorder_app, 'httpbin.org', '/get?foo=bar')
264264
assert b'HTTP/1.1 200 OK' in resp.body
265-
assert b'"foo": "bar"' in resp.body
265+
assert b'"foo":"bar"' in resp.body.replace(b' ', b'')
266266

267267
self._test_all_warcs('/warcs/', 2)
268268

@@ -279,7 +279,7 @@ def test_record_param_user_coll(self):
279279
resp = self._test_warc_write(recorder_app, 'httpbin.org', '/user-agent',
280280
'&param.recorder.user=USER&param.recorder.coll=COLL')
281281

282-
assert '"user-agent": "{0}"'.format(UA) in resp.text
282+
assert '"user-agent":"{0}"'.format(UA.replace(' ', '')) in resp.text.replace(' ', '')
283283
#assert b'HTTP/1.1 200 OK' in resp.body
284284
#assert b'"foo": "bar"' in resp.body
285285

@@ -312,12 +312,12 @@ def test_record_param_user_coll_same_dir(self):
312312
resp = self._test_warc_write(recorder_app, 'httpbin.org',
313313
'/get?foo=bar', '&param.recorder.user=USER2&param.recorder.coll=COLL2')
314314
assert b'HTTP/1.1 200 OK' in resp.body
315-
assert b'"foo": "bar"' in resp.body
315+
assert b'"foo":"bar"' in resp.body.replace(b' ', b'')
316316

317317
resp = self._test_warc_write(recorder_app, 'httpbin.org',
318318
'/get?foo=bar', '&param.recorder.user=USER2&param.recorder.coll=COLL3')
319319
assert b'HTTP/1.1 200 OK' in resp.body
320-
assert b'"foo": "bar"' in resp.body
320+
assert b'"foo":"bar"' in resp.body.replace(b' ', b'')
321321

322322
self._test_all_warcs('/warcs2', 2)
323323

@@ -334,7 +334,7 @@ def test_record_param_user_coll_revisit(self):
334334
resp = self._test_warc_write(recorder_app, 'httpbin.org', '/user-agent',
335335
'&param.recorder.user=USER&param.recorder.coll=COLL')
336336

337-
assert '"user-agent": "{0}"'.format(UA) in resp.text
337+
assert '"user-agent":"{0}"'.format(UA.replace(' ', '')) in resp.text.replace(' ', '')
338338
#assert b'HTTP/1.1 200 OK' in resp.body
339339
#assert b'"foo": "bar"' in resp.body
340340

@@ -387,7 +387,7 @@ def test_record_param_user_coll_skip(self):
387387
resp = self._test_warc_write(recorder_app, 'httpbin.org', '/user-agent',
388388
'&param.recorder.user=USER&param.recorder.coll=COLL')
389389

390-
assert '"user-agent": "{0}"'.format(UA) in resp.text
390+
assert '"user-agent":"{0}"'.format(UA.replace(' ', '')) in resp.text.replace(' ', '')
391391
#assert b'HTTP/1.1 200 OK' in resp.body
392392
#assert b'"foo": "bar"' in resp.body
393393
self._test_all_warcs('/warcs/USER/COLL/', 2)
@@ -409,7 +409,7 @@ def test_record_param_user_coll_write_dupe_no_revisit(self):
409409
resp = self._test_warc_write(recorder_app, 'httpbin.org',
410410
'/get?foo=bar', '&param.recorder.user=USER&param.recorder.coll=COLL')
411411
assert b'HTTP/1.1 200 OK' in resp.body
412-
assert b'"foo": "bar"' in resp.body
412+
assert b'"foo":"bar"' in resp.body.replace(b' ', b'')
413413

414414
self._test_all_warcs('/warcs/USER/COLL/', 3)
415415

@@ -432,7 +432,7 @@ def test_record_file_warc_keep_open(self):
432432

433433
resp = self._test_warc_write(recorder_app, 'httpbin.org', '/get?foo=bar')
434434
assert b'HTTP/1.1 200 OK' in resp.body
435-
assert b'"foo": "bar"' in resp.body
435+
assert b'"foo":"bar"' in resp.body.replace(b' ', b'')
436436

437437
assert os.path.isfile(path)
438438
assert len(writer.fh_cache) == 1
@@ -455,15 +455,15 @@ def test_record_multiple_writes_keep_open(self):
455455
'/get?foo=bar', '&param.recorder.coll=FOO')
456456

457457
assert b'HTTP/1.1 200 OK' in resp.body
458-
assert b'"foo": "bar"' in resp.body
458+
assert b'"foo":"bar"' in resp.body.replace(b' ', b'')
459459

460460

461461
# Second Record
462462
resp = self._test_warc_write(recorder_app, 'httpbin.org',
463463
'/get?boo=far', '&param.recorder.coll=FOO')
464464

465465
assert b'HTTP/1.1 200 OK' in resp.body
466-
assert b'"boo": "far"' in resp.body
466+
assert b'"boo":"far"' in resp.body.replace(b' ', b'')
467467

468468
self._test_all_warcs('/warcs/FOO/', 1)
469469

@@ -523,14 +523,14 @@ def test_record_multiple_writes_rollover_idle(self):
523523
'/get?foo=bar', '&param.recorder.coll=GOO')
524524

525525
assert b'HTTP/1.1 200 OK' in resp.body
526-
assert b'"foo": "bar"' in resp.body
526+
assert b'"foo":"bar"' in resp.body.replace(b' ', b'')
527527

528528
# Second Record
529529
resp = self._test_warc_write(recorder_app, 'httpbin.org',
530530
'/get?boo=far', '&param.recorder.coll=GOO')
531531

532532
assert b'HTTP/1.1 200 OK' in resp.body
533-
assert b'"boo": "far"' in resp.body
533+
assert b'"boo":"far"' in resp.body.replace(b' ', b'')
534534

535535
self._test_all_warcs('/warcs/GOO/', 1)
536536

@@ -542,7 +542,7 @@ def test_record_multiple_writes_rollover_idle(self):
542542
'/get?goo=bar', '&param.recorder.coll=GOO')
543543

544544
assert b'HTTP/1.1 200 OK' in resp.body
545-
assert b'"goo": "bar"' in resp.body
545+
assert b'"goo":"bar"' in resp.body.replace(b' ', b'')
546546

547547
self._test_all_warcs('/warcs/GOO/', 2)
548548

pywb/warcserver/test/test_handlers.py

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ def test_live_resource(self):
166166
assert resp.headers['Memento-Datetime'] != ''
167167

168168
assert b'HTTP/1.1 200 OK' in resp.body
169-
assert b'"foo": "bar"' in resp.body
169+
assert b'"foo":"bar"' in resp.body.replace(b' ', b'')
170170

171171
assert 'ResErrors' not in resp.headers
172172

@@ -182,7 +182,7 @@ def test_live_post_resource(self):
182182
assert resp.headers['Memento-Datetime'] != ''
183183

184184
assert b'HTTP/1.1 200 OK' in resp.body
185-
assert b'"foo": "bar"' in resp.body
185+
assert b'"foo":"bar"' in resp.body.replace(b' ', b'')
186186

187187
assert 'ResErrors' not in resp.headers
188188

@@ -294,7 +294,7 @@ def test_agg_live_postreq(self):
294294
assert resp.headers['Memento-Datetime'] != ''
295295

296296
assert b'HTTP/1.1 200 OK' in resp.body
297-
assert b'"foo": "bar"' in resp.body
297+
assert b'"foo":"bar"' in resp.body.replace(b' ', b'')
298298

299299
#assert json.loads(resp.headers['ResErrors']) == {"rhiz": "NotFoundException('https://webarchives.rhizome.org/vvork/http://httpbin.org/get?foo=bar',)"}
300300
assert "NotFoundException('https://webarchives.rhizome.org/vvork/" in json.loads(resp.headers['ResErrors'])['rhiz']
@@ -320,9 +320,10 @@ def test_agg_post_resolve_postreq(self):
320320
assert resp.headers['Memento-Datetime'] != ''
321321

322322
assert b'HTTP/1.1 200 OK' in resp.body
323-
assert b'"foo": "bar"' in resp.body
324-
assert b'"test": "abc"' in resp.body
325-
assert b'"url": "http://httpbin.org/post"' in resp.body
323+
compact = resp.body.replace(b' ', b'')
324+
assert b'"foo":"bar"' in compact
325+
assert b'"test":"abc"' in compact
326+
assert b'"url":"http://httpbin.org/post"' in compact
326327

327328
assert 'ResErrors' not in resp.headers
328329

@@ -338,9 +339,10 @@ def test_agg_post_resolve_fallback(self):
338339
assert resp.headers['Link'] == MementoUtils.make_link('http://httpbin.org/post', 'original')
339340

340341
assert b'HTTP/1.1 200 OK' in resp.body
341-
assert b'"foo": "bar"' in resp.body
342-
assert b'"test": "abc"' in resp.body
343-
assert b'"url": "http://httpbin.org/post"' in resp.body
342+
compact = resp.body.replace(b' ', b'')
343+
assert b'"foo":"bar"' in compact
344+
assert b'"test":"abc"' in compact
345+
assert b'"url":"http://httpbin.org/post"' in compact
344346

345347
assert 'ResErrors' not in resp.headers
346348

@@ -509,5 +511,3 @@ def test_error_invalid(self):
509511

510512
assert resp.json == {'message': "Internal Error: 'list' object is not callable"}
511513
assert resp.text == resp.headers['ResErrors']
512-
513-

requirements.txt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,13 @@ jinja2>=3.1.2
77
surt>=0.3.1
88
brotlipy
99
pyyaml
10-
werkzeug==2.2.3
10+
werkzeug==2.2.3; python_version<"3.14"
11+
werkzeug==2.3.8; python_version>="3.14"
1112
webencodings
1213
legacy-cgi; python_version>="3.13"
1314
gevent==22.10.2; python_version<"3.8"
1415
gevent==23.9.0.post1; python_version>="3.8" and python_version<"3.13"
15-
gevent==24.10.1; python_version>="3.13"
16+
gevent==25.4.1; python_version>="3.13"
1617
greenlet>=2.0.2,<3.0; python_version<"3.12"
1718
greenlet==3.2.4; python_version>="3.12.0rc0"
1819
webassets==2.0

setup.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ def get_package_data():
127127
"babel-vue-extractor"
128128
],
129129
},
130-
python_requires='>=3.7,<3.14',
130+
python_requires='>=3.7,<3.15',
131131
tests_require=load_requirements("test_requirements.txt"),
132132
cmdclass={'test': PyTest},
133133
test_suite='',
@@ -154,6 +154,7 @@ def get_package_data():
154154
'Programming Language :: Python :: 3.11',
155155
'Programming Language :: Python :: 3.12',
156156
'Programming Language :: Python :: 3.13',
157+
'Programming Language :: Python :: 3.14',
157158
'Topic :: Internet :: Proxy Servers',
158159
'Topic :: Internet :: WWW/HTTP',
159160
'Topic :: Internet :: WWW/HTTP :: WSGI',

tests/test_live_rewriter.py

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -75,15 +75,14 @@ def test_live_live_redirect_2(self, fmod_sl):
7575
def test_live_live_post(self, fmod_sl):
7676
resp = self.post('/live/{0}httpbin.org/post', fmod_sl, {'foo': 'bar', 'test': 'abc'})
7777
assert resp.status_int == 200
78-
resp.charset = 'utf-8'
79-
assert '"foo": "bar"' in resp.text
80-
assert '"test": "abc"' in resp.text
78+
assert resp.json['form']['foo'] == 'bar'
79+
assert resp.json['form']['test'] == 'abc'
8180
assert resp.status_int == 200
8281

8382
def test_live_anchor_encode(self, fmod_sl):
8483
resp = self.get('/live/{0}httpbin.org/get?val=abc%23%23xyz', fmod_sl)
8584
assert 'get?val=abc%23%23xyz"' in resp.text
86-
assert '"val": "abc##xyz"' in resp.text
85+
assert resp.json['args']['val'] == 'abc##xyz'
8786
#assert '"http://httpbin.org/anything/abc##xyz"' in resp.text
8887
assert resp.status_int == 200
8988

@@ -181,7 +180,7 @@ def test_live_video_info(self):
181180

182181
def test_deflate(self, fmod_sl):
183182
resp = self.get('/live/{0}http://httpbin.org/deflate', fmod_sl)
184-
assert b'"deflated": true' in resp.body
183+
assert resp.json['deflated'] is True
185184

186185
def test_live_origin_and_referrer(self, fmod_sl):
187186
headers = {'Referer': 'http://localhost:80/live/{0}http://example.com/test'.format(fmod_sl),
@@ -200,4 +199,3 @@ def test_live_origin_no_referrer(self, fmod_sl):
200199

201200
assert resp.json['headers']['Origin'] == 'http://httpbin.org'
202201

203-

tests/test_record_dedup.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,12 @@ def test_init_coll(self):
2626

2727
def test_record_1(self):
2828
res = self.testapp.get('/test-dedup/record/mp_/http://httpbin.org/get?A=B', headers={"Referer": "http://httpbin.org/"})
29-
assert '"A": "B"' in res.text
29+
assert res.json['args']['A'] == 'B'
3030

3131
time.sleep(1.2)
3232

3333
res = self.testapp.get('/test-dedup/record/mp_/http://httpbin.org/get?A=B', headers={"Referer": "http://httpbin.org/"})
34-
assert '"A": "B"' in res.text
34+
assert res.json['args']['A'] == 'B'
3535

3636
def test_single_redis_entry(self):
3737
res = self.redis.zrange("pywb:test-dedup:cdxj", 0, -1)

tests/test_record_replay.py

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ def test_init_coll(self):
3636

3737
def test_record_1(self):
3838
res = self.testapp.get('/test/record/mp_/http://httpbin.org/get?A=B')
39-
assert '"A": "B"' in res.text
39+
assert res.json['args']['A'] == 'B'
4040

4141
def test_record_head(self):
4242
res = self.testapp.head('/test/record/mp_/http://httpbin.org/get?A=B')
@@ -48,7 +48,7 @@ def test_replay_1(self, fmod):
4848

4949
fmod_slash = fmod + '/' if fmod else ''
5050
res = self.get('/test/{0}http://httpbin.org/get?A=B', fmod_slash)
51-
assert '"A": "B"' in res.text
51+
assert res.json['args']['A'] == 'B'
5252

5353
def test_replay_head(self, fmod):
5454
fmod_slash = fmod + '/' if fmod else ''
@@ -59,25 +59,25 @@ def test_replay_head(self, fmod):
5959

6060
def test_record_2(self):
6161
res = self.testapp.get('/test2/record/mp_/http://httpbin.org/get?C=D')
62-
assert '"C": "D"' in res.text
62+
assert res.json['args']['C'] == 'D'
6363

6464
def test_replay_2(self, fmod):
6565
self.ensure_empty()
6666

6767
fmod_slash = fmod + '/' if fmod else ''
6868
res = self.get('/test2/{0}http://httpbin.org/get?C=D', fmod_slash)
69-
assert '"C": "D"' in res.text
69+
assert res.json['args']['C'] == 'D'
7070

7171
def test_record_again_1(self):
7272
res = self.testapp.get('/test/record/mp_/http://httpbin.org/get?C=D2')
73-
assert '"C": "D2"' in res.text
73+
assert res.json['args']['C'] == 'D2'
7474

7575
def test_replay_again_1(self, fmod):
7676
self.ensure_empty()
7777

7878
fmod_slash = fmod + '/' if fmod else ''
7979
res = self.get('/test/{0}http://httpbin.org/get?C=D2', fmod_slash)
80-
assert '"C": "D2"' in res.text
80+
assert res.json['args']['C'] == 'D2'
8181

8282
assert len(os.listdir(os.path.join(self.root_dir, '_test_colls', 'test', 'archive'))) == 1
8383

@@ -95,10 +95,10 @@ def test_replay_all_coll(self, fmod):
9595
fmod_slash = fmod + '/' if fmod else ''
9696

9797
res = self.get('/all/{0}http://httpbin.org/get?C=D', fmod_slash)
98-
assert '"C": "D"' in res.text
98+
assert res.json['args']['C'] == 'D'
9999

100100
res = self.get('/all/mp_/http://httpbin.org/get?A=B', fmod_slash)
101-
assert '"A": "B"' in res.text
101+
assert res.json['args']['A'] == 'B'
102102

103103
def test_cdx_all_coll(self):
104104
res = self.testapp.get('/all/cdx?url=http://httpbin.org/get*&output=json')
@@ -164,7 +164,7 @@ def test_init_and_rec(self):
164164
assert os.path.isdir(dir_name)
165165

166166
res = self.testapp.get('/test-new/record/mp_/http://httpbin.org/get?A=B')
167-
assert '"A": "B"' in res.text
167+
assert res.json['args']['A'] == 'B'
168168

169169
names = os.listdir(dir_name)
170170
assert len(names) == 1
@@ -177,7 +177,7 @@ def test_init_and_rec(self):
177177
def test_no_brotli(self):
178178
res = self.testapp.get('/test-new/record/mp_/http://httpbin.org/get?C=D',
179179
headers={'Accept-Encoding': 'gzip, deflate, br'})
180-
assert '"C": "D"' in res.text
180+
assert res.json['args']['C'] == 'D'
181181

182182
with open(self.warc_name, 'rb') as fh:
183183
for record in ArchiveIterator(fh):
@@ -231,4 +231,3 @@ def test_record_new(self):
231231
assert names[0].endswith('.warc.gz')
232232

233233

234-

tox.ini

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ testpaths =
44
tests
55

66
[tox]
7-
envlist = py37, py38, py39, py310, py311, py312, py313
7+
envlist = py37, py38, py39, py310, py311, py312, py313, py314
88

99
[gh-actions]
1010
python =
@@ -13,6 +13,7 @@ python =
1313
3.11: py311
1414
3.12: py312
1515
3.13: py313
16+
3.14: py314
1617

1718
[testenv]
1819
setenv = PYWB_NO_VERIFY_SSL = 1
@@ -24,4 +25,3 @@ deps =
2425
-rextra_requirements.txt
2526
commands =
2627
pytest --cov-config .coveragerc --cov pywb -v --doctest-modules ./pywb/ tests/
27-

0 commit comments

Comments
 (0)