1 |
commit: b09cd1265ba60e15dbb549d18cc10b9b11e424f7 |
2 |
Author: Michał Górny <mgorny <AT> gentoo <DOT> org> |
3 |
AuthorDate: Mon Jan 31 22:08:19 2022 +0000 |
4 |
Commit: Michał Górny <mgorny <AT> gentoo <DOT> org> |
5 |
CommitDate: Mon Jan 31 22:27:52 2022 +0000 |
6 |
URL: https://gitweb.gentoo.org/repo/gentoo.git/commit/?id=b09cd126 |
7 |
|
8 |
dev-python/cherrypy: Add pypy3 love |
9 |
|
10 |
Signed-off-by: Michał Górny <mgorny <AT> gentoo.org> |
11 |
|
12 |
dev-python/cherrypy/cherrypy-18.6.1.ebuild | 9 +- |
13 |
.../files/cherrypy-18.6.1-close-files.patch | 416 +++++++++++++++++++++ |
14 |
2 files changed, 423 insertions(+), 2 deletions(-) |
15 |
|
16 |
diff --git a/dev-python/cherrypy/cherrypy-18.6.1.ebuild b/dev-python/cherrypy/cherrypy-18.6.1.ebuild |
17 |
index a039a64ffe1e..d1177e6159ad 100644 |
18 |
--- a/dev-python/cherrypy/cherrypy-18.6.1.ebuild |
19 |
+++ b/dev-python/cherrypy/cherrypy-18.6.1.ebuild |
20 |
@@ -1,9 +1,9 @@ |
21 |
-# Copyright 1999-2021 Gentoo Authors |
22 |
+# Copyright 1999-2022 Gentoo Authors |
23 |
# Distributed under the terms of the GNU General Public License v2 |
24 |
|
25 |
EAPI=8 |
26 |
|
27 |
-PYTHON_COMPAT=( python3_{8..10} ) |
28 |
+PYTHON_COMPAT=( python3_{8..10} pypy3 ) |
29 |
inherit distutils-r1 |
30 |
|
31 |
MY_P="CherryPy-${PV}" |
32 |
@@ -40,6 +40,11 @@ BDEPEND=" |
33 |
distutils_enable_tests pytest |
34 |
|
35 |
python_prepare_all() { |
36 |
+ local PATCHES=( |
37 |
+ # https://github.com/cherrypy/cherrypy/pull/1946 |
38 |
+ "${FILESDIR}"/${P}-close-files.patch |
39 |
+ ) |
40 |
+ |
41 |
sed -r -e '/(pytest-sugar|pytest-cov)/ d' \ |
42 |
-i setup.py || die |
43 |
|
44 |
|
45 |
diff --git a/dev-python/cherrypy/files/cherrypy-18.6.1-close-files.patch b/dev-python/cherrypy/files/cherrypy-18.6.1-close-files.patch |
46 |
new file mode 100644 |
47 |
index 000000000000..478d717dfb8c |
48 |
--- /dev/null |
49 |
+++ b/dev-python/cherrypy/files/cherrypy-18.6.1-close-files.patch |
50 |
@@ -0,0 +1,416 @@ |
51 |
+From 94a2cc036203c6da55174ef3b105c0c875bbc79f Mon Sep 17 00:00:00 2001 |
52 |
+From: =?UTF-8?q?Micha=C5=82=20G=C3=B3rny?= <mgorny@g.o> |
53 |
+Date: Mon, 31 Jan 2022 22:25:34 +0100 |
54 |
+Subject: [PATCH] Use context managers to close files properly and fix tests on |
55 |
+ PyPy |
56 |
+ |
57 |
+Use context managers (`with`) to ensure that all open files are closed |
58 |
+correctly. This resolves resource leaks and test failures with PyPy3.7. |
59 |
+ |
60 |
+The code prior to this change used four approaches for closing files: |
61 |
+ |
62 |
+1. Using a context manager (`with` clause). |
63 |
+ |
64 |
+2. Using a try/finally clause. |
65 |
+ |
66 |
+3. Closing the file in the same scope (unreliable: file object can leak |
67 |
+ on exception). |
68 |
+ |
69 |
+4. Not closing open files at all. |
70 |
+ |
71 |
+The last point is a real problem for PyPy since it does not GC |
72 |
+unreachable objects as aggressively as CPython does. While leaving |
73 |
+a function scope on CPython causes the file objects private to it to |
74 |
+be destroyed (and therefore closed), in PyPy they can stay dangling |
75 |
+for some time. When combines with buffered writes, this means that |
76 |
+writes can still remain pending after returning from function. |
77 |
+ |
78 |
+Using a context manager is a simple, consistent way to ensure that |
79 |
+the file object is closed once it is no longer needed. In turn, this |
80 |
+guarantees that all pending writes will be performed upon function |
81 |
+return and the code won't be hiting race conditions between writing |
82 |
+a file and reading it afterwards. |
83 |
+--- |
84 |
+ cherrypy/_cperror.py | 3 ++- |
85 |
+ cherrypy/_cpmodpy.py | 5 +---- |
86 |
+ cherrypy/lib/auth_digest.py | 13 ++++++------ |
87 |
+ cherrypy/lib/covercp.py | 40 ++++++++++++++++++------------------ |
88 |
+ cherrypy/lib/reprconf.py | 5 +---- |
89 |
+ cherrypy/lib/sessions.py | 10 ++------- |
90 |
+ cherrypy/process/plugins.py | 3 ++- |
91 |
+ cherrypy/test/helper.py | 3 ++- |
92 |
+ cherrypy/test/logtest.py | 33 ++++++++++++++++------------- |
93 |
+ cherrypy/test/modfastcgi.py | 5 +---- |
94 |
+ cherrypy/test/modfcgid.py | 5 +---- |
95 |
+ cherrypy/test/modpy.py | 5 +---- |
96 |
+ cherrypy/test/modwsgi.py | 5 +---- |
97 |
+ cherrypy/test/test_core.py | 5 ++--- |
98 |
+ cherrypy/test/test_states.py | 11 +++++----- |
99 |
+ 15 files changed, 67 insertions(+), 84 deletions(-) |
100 |
+ |
101 |
+diff --git a/cherrypy/_cperror.py b/cherrypy/_cperror.py |
102 |
+index 4e727682..ebf1dcf6 100644 |
103 |
+--- a/cherrypy/_cperror.py |
104 |
++++ b/cherrypy/_cperror.py |
105 |
+@@ -532,7 +532,8 @@ def get_error_page(status, **kwargs): |
106 |
+ return result |
107 |
+ else: |
108 |
+ # Load the template from this path. |
109 |
+- template = io.open(error_page, newline='').read() |
110 |
++ with io.open(error_page, newline='') as f: |
111 |
++ template = f.read() |
112 |
+ except Exception: |
113 |
+ e = _format_exception(*_exc_info())[-1] |
114 |
+ m = kwargs['message'] |
115 |
+diff --git a/cherrypy/_cpmodpy.py b/cherrypy/_cpmodpy.py |
116 |
+index 0e608c48..a08f0ed9 100644 |
117 |
+--- a/cherrypy/_cpmodpy.py |
118 |
++++ b/cherrypy/_cpmodpy.py |
119 |
+@@ -339,11 +339,8 @@ LoadModule python_module modules/mod_python.so |
120 |
+ } |
121 |
+ |
122 |
+ mpconf = os.path.join(os.path.dirname(__file__), 'cpmodpy.conf') |
123 |
+- f = open(mpconf, 'wb') |
124 |
+- try: |
125 |
++ with open(mpconf, 'wb') as f: |
126 |
+ f.write(conf_data) |
127 |
+- finally: |
128 |
+- f.close() |
129 |
+ |
130 |
+ response = read_process(self.apache_path, '-k start -f %s' % mpconf) |
131 |
+ self.ready = True |
132 |
+diff --git a/cherrypy/lib/auth_digest.py b/cherrypy/lib/auth_digest.py |
133 |
+index fbb5df64..981e9a5d 100644 |
134 |
+--- a/cherrypy/lib/auth_digest.py |
135 |
++++ b/cherrypy/lib/auth_digest.py |
136 |
+@@ -101,13 +101,12 @@ def get_ha1_file_htdigest(filename): |
137 |
+ """ |
138 |
+ def get_ha1(realm, username): |
139 |
+ result = None |
140 |
+- f = open(filename, 'r') |
141 |
+- for line in f: |
142 |
+- u, r, ha1 = line.rstrip().split(':') |
143 |
+- if u == username and r == realm: |
144 |
+- result = ha1 |
145 |
+- break |
146 |
+- f.close() |
147 |
++ with open(filename, 'r') as f: |
148 |
++ for line in f: |
149 |
++ u, r, ha1 = line.rstrip().split(':') |
150 |
++ if u == username and r == realm: |
151 |
++ result = ha1 |
152 |
++ break |
153 |
+ return result |
154 |
+ |
155 |
+ return get_ha1 |
156 |
+diff --git a/cherrypy/lib/covercp.py b/cherrypy/lib/covercp.py |
157 |
+index 3e219713..005fafa5 100644 |
158 |
+--- a/cherrypy/lib/covercp.py |
159 |
++++ b/cherrypy/lib/covercp.py |
160 |
+@@ -334,26 +334,26 @@ class CoverStats(object): |
161 |
+ yield '</body></html>' |
162 |
+ |
163 |
+ def annotated_file(self, filename, statements, excluded, missing): |
164 |
+- source = open(filename, 'r') |
165 |
+- buffer = [] |
166 |
+- for lineno, line in enumerate(source.readlines()): |
167 |
+- lineno += 1 |
168 |
+- line = line.strip('\n\r') |
169 |
+- empty_the_buffer = True |
170 |
+- if lineno in excluded: |
171 |
+- template = TEMPLATE_LOC_EXCLUDED |
172 |
+- elif lineno in missing: |
173 |
+- template = TEMPLATE_LOC_NOT_COVERED |
174 |
+- elif lineno in statements: |
175 |
+- template = TEMPLATE_LOC_COVERED |
176 |
+- else: |
177 |
+- empty_the_buffer = False |
178 |
+- buffer.append((lineno, line)) |
179 |
+- if empty_the_buffer: |
180 |
+- for lno, pastline in buffer: |
181 |
+- yield template % (lno, cgi.escape(pastline)) |
182 |
+- buffer = [] |
183 |
+- yield template % (lineno, cgi.escape(line)) |
184 |
++ with open(filename, 'r') as source: |
185 |
++ buffer = [] |
186 |
++ for lineno, line in enumerate(source.readlines()): |
187 |
++ lineno += 1 |
188 |
++ line = line.strip('\n\r') |
189 |
++ empty_the_buffer = True |
190 |
++ if lineno in excluded: |
191 |
++ template = TEMPLATE_LOC_EXCLUDED |
192 |
++ elif lineno in missing: |
193 |
++ template = TEMPLATE_LOC_NOT_COVERED |
194 |
++ elif lineno in statements: |
195 |
++ template = TEMPLATE_LOC_COVERED |
196 |
++ else: |
197 |
++ empty_the_buffer = False |
198 |
++ buffer.append((lineno, line)) |
199 |
++ if empty_the_buffer: |
200 |
++ for lno, pastline in buffer: |
201 |
++ yield template % (lno, cgi.escape(pastline)) |
202 |
++ buffer = [] |
203 |
++ yield template % (lineno, cgi.escape(line)) |
204 |
+ |
205 |
+ @cherrypy.expose |
206 |
+ def report(self, name): |
207 |
+diff --git a/cherrypy/lib/reprconf.py b/cherrypy/lib/reprconf.py |
208 |
+index 3976652e..76381d7b 100644 |
209 |
+--- a/cherrypy/lib/reprconf.py |
210 |
++++ b/cherrypy/lib/reprconf.py |
211 |
+@@ -163,11 +163,8 @@ class Parser(configparser.ConfigParser): |
212 |
+ # fp = open(filename) |
213 |
+ # except IOError: |
214 |
+ # continue |
215 |
+- fp = open(filename) |
216 |
+- try: |
217 |
++ with open(filename) as fp: |
218 |
+ self._read(fp, filename) |
219 |
+- finally: |
220 |
+- fp.close() |
221 |
+ |
222 |
+ def as_dict(self, raw=False, vars=None): |
223 |
+ """Convert an INI file to a dictionary""" |
224 |
+diff --git a/cherrypy/lib/sessions.py b/cherrypy/lib/sessions.py |
225 |
+index 5b3328f2..0f56a4fa 100644 |
226 |
+--- a/cherrypy/lib/sessions.py |
227 |
++++ b/cherrypy/lib/sessions.py |
228 |
+@@ -516,11 +516,8 @@ class FileSession(Session): |
229 |
+ if path is None: |
230 |
+ path = self._get_file_path() |
231 |
+ try: |
232 |
+- f = open(path, 'rb') |
233 |
+- try: |
234 |
++ with open(path, 'rb') as f: |
235 |
+ return pickle.load(f) |
236 |
+- finally: |
237 |
+- f.close() |
238 |
+ except (IOError, EOFError): |
239 |
+ e = sys.exc_info()[1] |
240 |
+ if self.debug: |
241 |
+@@ -531,11 +528,8 @@ class FileSession(Session): |
242 |
+ def _save(self, expiration_time): |
243 |
+ assert self.locked, ('The session was saved without being locked. ' |
244 |
+ "Check your tools' priority levels.") |
245 |
+- f = open(self._get_file_path(), 'wb') |
246 |
+- try: |
247 |
++ with open(self._get_file_path(), 'wb') as f: |
248 |
+ pickle.dump((self._data, expiration_time), f, self.pickle_protocol) |
249 |
+- finally: |
250 |
+- f.close() |
251 |
+ |
252 |
+ def _delete(self): |
253 |
+ assert self.locked, ('The session deletion without being locked. ' |
254 |
+diff --git a/cherrypy/process/plugins.py b/cherrypy/process/plugins.py |
255 |
+index 2a9952de..e96fb1ce 100644 |
256 |
+--- a/cherrypy/process/plugins.py |
257 |
++++ b/cherrypy/process/plugins.py |
258 |
+@@ -436,7 +436,8 @@ class PIDFile(SimplePlugin): |
259 |
+ if self.finalized: |
260 |
+ self.bus.log('PID %r already written to %r.' % (pid, self.pidfile)) |
261 |
+ else: |
262 |
+- open(self.pidfile, 'wb').write(ntob('%s\n' % pid, 'utf8')) |
263 |
++ with open(self.pidfile, 'wb') as f: |
264 |
++ f.write(ntob('%s\n' % pid, 'utf8')) |
265 |
+ self.bus.log('PID %r written to %r.' % (pid, self.pidfile)) |
266 |
+ self.finalized = True |
267 |
+ start.priority = 70 |
268 |
+diff --git a/cherrypy/test/helper.py b/cherrypy/test/helper.py |
269 |
+index c1ca4535..cae49533 100644 |
270 |
+--- a/cherrypy/test/helper.py |
271 |
++++ b/cherrypy/test/helper.py |
272 |
+@@ -505,7 +505,8 @@ server.ssl_private_key: r'%s' |
273 |
+ |
274 |
+ def get_pid(self): |
275 |
+ if self.daemonize: |
276 |
+- return int(open(self.pid_file, 'rb').read()) |
277 |
++ with open(self.pid_file, 'rb') as f: |
278 |
++ return int(f.read()) |
279 |
+ return self._proc.pid |
280 |
+ |
281 |
+ def join(self): |
282 |
+diff --git a/cherrypy/test/logtest.py b/cherrypy/test/logtest.py |
283 |
+index 344be987..112bdc25 100644 |
284 |
+--- a/cherrypy/test/logtest.py |
285 |
++++ b/cherrypy/test/logtest.py |
286 |
+@@ -97,7 +97,8 @@ class LogCase(object): |
287 |
+ |
288 |
+ def emptyLog(self): |
289 |
+ """Overwrite self.logfile with 0 bytes.""" |
290 |
+- open(self.logfile, 'wb').write('') |
291 |
++ with open(self.logfile, 'wb') as f: |
292 |
++ f.write('') |
293 |
+ |
294 |
+ def markLog(self, key=None): |
295 |
+ """Insert a marker line into the log and set self.lastmarker.""" |
296 |
+@@ -105,10 +106,11 @@ class LogCase(object): |
297 |
+ key = str(time.time()) |
298 |
+ self.lastmarker = key |
299 |
+ |
300 |
+- open(self.logfile, 'ab+').write( |
301 |
+- b'%s%s\n' |
302 |
+- % (self.markerPrefix, key.encode('utf-8')) |
303 |
+- ) |
304 |
++ with open(self.logfile, 'ab+') as f: |
305 |
++ f.write( |
306 |
++ b'%s%s\n' |
307 |
++ % (self.markerPrefix, key.encode('utf-8')) |
308 |
++ ) |
309 |
+ |
310 |
+ def _read_marked_region(self, marker=None): |
311 |
+ """Return lines from self.logfile in the marked region. |
312 |
+@@ -122,20 +124,23 @@ class LogCase(object): |
313 |
+ logfile = self.logfile |
314 |
+ marker = marker or self.lastmarker |
315 |
+ if marker is None: |
316 |
+- return open(logfile, 'rb').readlines() |
317 |
++ with open(logfile, 'rb') as f: |
318 |
++ return f.readlines() |
319 |
+ |
320 |
+ if isinstance(marker, str): |
321 |
+ marker = marker.encode('utf-8') |
322 |
+ data = [] |
323 |
+ in_region = False |
324 |
+- for line in open(logfile, 'rb'): |
325 |
+- if in_region: |
326 |
+- if line.startswith(self.markerPrefix) and marker not in line: |
327 |
+- break |
328 |
+- else: |
329 |
+- data.append(line) |
330 |
+- elif marker in line: |
331 |
+- in_region = True |
332 |
++ with open(logfile, 'rb') as f: |
333 |
++ for line in f: |
334 |
++ if in_region: |
335 |
++ if (line.startswith(self.markerPrefix) |
336 |
++ and marker not in line): |
337 |
++ break |
338 |
++ else: |
339 |
++ data.append(line) |
340 |
++ elif marker in line: |
341 |
++ in_region = True |
342 |
+ return data |
343 |
+ |
344 |
+ def assertInLog(self, line, marker=None): |
345 |
+diff --git a/cherrypy/test/modfastcgi.py b/cherrypy/test/modfastcgi.py |
346 |
+index 79ec3d18..0c6d01e2 100644 |
347 |
+--- a/cherrypy/test/modfastcgi.py |
348 |
++++ b/cherrypy/test/modfastcgi.py |
349 |
+@@ -112,15 +112,12 @@ class ModFCGISupervisor(helper.LocalWSGISupervisor): |
350 |
+ fcgiconf = os.path.join(curdir, fcgiconf) |
351 |
+ |
352 |
+ # Write the Apache conf file. |
353 |
+- f = open(fcgiconf, 'wb') |
354 |
+- try: |
355 |
++ with open(fcgiconf, 'wb') as f: |
356 |
+ server = repr(os.path.join(curdir, 'fastcgi.pyc'))[1:-1] |
357 |
+ output = self.template % {'port': self.port, 'root': curdir, |
358 |
+ 'server': server} |
359 |
+ output = output.replace('\r\n', '\n') |
360 |
+ f.write(output) |
361 |
+- finally: |
362 |
+- f.close() |
363 |
+ |
364 |
+ result = read_process(APACHE_PATH, '-k start -f %s' % fcgiconf) |
365 |
+ if result: |
366 |
+diff --git a/cherrypy/test/modfcgid.py b/cherrypy/test/modfcgid.py |
367 |
+index d101bd67..ea373004 100644 |
368 |
+--- a/cherrypy/test/modfcgid.py |
369 |
++++ b/cherrypy/test/modfcgid.py |
370 |
+@@ -101,15 +101,12 @@ class ModFCGISupervisor(helper.LocalSupervisor): |
371 |
+ fcgiconf = os.path.join(curdir, fcgiconf) |
372 |
+ |
373 |
+ # Write the Apache conf file. |
374 |
+- f = open(fcgiconf, 'wb') |
375 |
+- try: |
376 |
++ with open(fcgiconf, 'wb') as f: |
377 |
+ server = repr(os.path.join(curdir, 'fastcgi.pyc'))[1:-1] |
378 |
+ output = self.template % {'port': self.port, 'root': curdir, |
379 |
+ 'server': server} |
380 |
+ output = ntob(output.replace('\r\n', '\n')) |
381 |
+ f.write(output) |
382 |
+- finally: |
383 |
+- f.close() |
384 |
+ |
385 |
+ result = read_process(APACHE_PATH, '-k start -f %s' % fcgiconf) |
386 |
+ if result: |
387 |
+diff --git a/cherrypy/test/modpy.py b/cherrypy/test/modpy.py |
388 |
+index 7c288d2c..024453e9 100644 |
389 |
+--- a/cherrypy/test/modpy.py |
390 |
++++ b/cherrypy/test/modpy.py |
391 |
+@@ -107,13 +107,10 @@ class ModPythonSupervisor(helper.Supervisor): |
392 |
+ if not os.path.isabs(mpconf): |
393 |
+ mpconf = os.path.join(curdir, mpconf) |
394 |
+ |
395 |
+- f = open(mpconf, 'wb') |
396 |
+- try: |
397 |
++ with open(mpconf, 'wb') as f: |
398 |
+ f.write(self.template % |
399 |
+ {'port': self.port, 'modulename': modulename, |
400 |
+ 'host': self.host}) |
401 |
+- finally: |
402 |
+- f.close() |
403 |
+ |
404 |
+ result = read_process(APACHE_PATH, '-k start -f %s' % mpconf) |
405 |
+ if result: |
406 |
+diff --git a/cherrypy/test/modwsgi.py b/cherrypy/test/modwsgi.py |
407 |
+index da7d240b..24c72684 100644 |
408 |
+--- a/cherrypy/test/modwsgi.py |
409 |
++++ b/cherrypy/test/modwsgi.py |
410 |
+@@ -109,14 +109,11 @@ class ModWSGISupervisor(helper.Supervisor): |
411 |
+ if not os.path.isabs(mpconf): |
412 |
+ mpconf = os.path.join(curdir, mpconf) |
413 |
+ |
414 |
+- f = open(mpconf, 'wb') |
415 |
+- try: |
416 |
++ with open(mpconf, 'wb') as f: |
417 |
+ output = (self.template % |
418 |
+ {'port': self.port, 'testmod': modulename, |
419 |
+ 'curdir': curdir}) |
420 |
+ f.write(output) |
421 |
+- finally: |
422 |
+- f.close() |
423 |
+ |
424 |
+ result = read_process(APACHE_PATH, '-k start -f %s' % mpconf) |
425 |
+ if result: |
426 |
+diff --git a/cherrypy/test/test_core.py b/cherrypy/test/test_core.py |
427 |
+index 6fde3a97..42460b3f 100644 |
428 |
+--- a/cherrypy/test/test_core.py |
429 |
++++ b/cherrypy/test/test_core.py |
430 |
+@@ -586,9 +586,8 @@ class CoreRequestHandlingTest(helper.CPWebCase): |
431 |
+ def testFavicon(self): |
432 |
+ # favicon.ico is served by staticfile. |
433 |
+ icofilename = os.path.join(localDir, '../favicon.ico') |
434 |
+- icofile = open(icofilename, 'rb') |
435 |
+- data = icofile.read() |
436 |
+- icofile.close() |
437 |
++ with open(icofilename, 'rb') as icofile: |
438 |
++ data = icofile.read() |
439 |
+ |
440 |
+ self.getPage('/favicon.ico') |
441 |
+ self.assertBody(data) |
442 |
+diff --git a/cherrypy/test/test_states.py b/cherrypy/test/test_states.py |
443 |
+index 28dd6510..d59a4d87 100644 |
444 |
+--- a/cherrypy/test/test_states.py |
445 |
++++ b/cherrypy/test/test_states.py |
446 |
+@@ -424,11 +424,12 @@ test_case_name: "test_signal_handler_unsubscribe" |
447 |
+ p.join() |
448 |
+ |
449 |
+ # Assert the old handler ran. |
450 |
+- log_lines = list(open(p.error_log, 'rb')) |
451 |
+- assert any( |
452 |
+- line.endswith(b'I am an old SIGTERM handler.\n') |
453 |
+- for line in log_lines |
454 |
+- ) |
455 |
++ with open(p.error_log, 'rb') as f: |
456 |
++ log_lines = list(f) |
457 |
++ assert any( |
458 |
++ line.endswith(b'I am an old SIGTERM handler.\n') |
459 |
++ for line in log_lines |
460 |
++ ) |
461 |
+ |
462 |
+ |
463 |
+ def test_safe_wait_INADDR_ANY(): # pylint: disable=invalid-name |
464 |
+-- |
465 |
+2.35.1 |
466 |
+ |