|
1 ############################################################################# |
|
2 # |
|
3 # Copyright (c) 2004-2009 Zope Corporation and Contributors. |
|
4 # All Rights Reserved. |
|
5 # |
|
6 # This software is subject to the provisions of the Zope Public License, |
|
7 # Version 2.0 (ZPL). A copy of the ZPL should accompany this distribution. |
|
8 # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED |
|
9 # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED |
|
10 # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS |
|
11 # FOR A PARTICULAR PURPOSE. |
|
12 # |
|
13 ############################################################################## |
|
14 """Various test-support utility functions |
|
15 |
|
16 $Id: testing.py 116197 2010-09-04 00:02:53Z gary $ |
|
17 """ |
|
18 |
|
19 import BaseHTTPServer |
|
20 import errno |
|
21 import logging |
|
22 import os |
|
23 import pkg_resources |
|
24 import random |
|
25 import re |
|
26 import shutil |
|
27 import socket |
|
28 import subprocess |
|
29 import sys |
|
30 import tempfile |
|
31 import textwrap |
|
32 import threading |
|
33 import time |
|
34 import urllib2 |
|
35 |
|
36 import zc.buildout.buildout |
|
37 import zc.buildout.easy_install |
|
38 from zc.buildout.rmtree import rmtree |
|
39 |
|
40 fsync = getattr(os, 'fsync', lambda fileno: None) |
|
41 is_win32 = sys.platform == 'win32' |
|
42 |
|
43 setuptools_location = pkg_resources.working_set.find( |
|
44 pkg_resources.Requirement.parse('setuptools')).location |
|
45 |
|
46 def cat(dir, *names): |
|
47 path = os.path.join(dir, *names) |
|
48 if (not os.path.exists(path) |
|
49 and is_win32 |
|
50 and os.path.exists(path+'-script.py') |
|
51 ): |
|
52 path = path+'-script.py' |
|
53 print open(path).read(), |
|
54 |
|
55 def ls(dir, *subs): |
|
56 if subs: |
|
57 dir = os.path.join(dir, *subs) |
|
58 names = os.listdir(dir) |
|
59 names.sort() |
|
60 for name in names: |
|
61 if os.path.isdir(os.path.join(dir, name)): |
|
62 print 'd ', |
|
63 elif os.path.islink(os.path.join(dir, name)): |
|
64 print 'l ', |
|
65 else: |
|
66 print '- ', |
|
67 print name |
|
68 |
|
69 def mkdir(*path): |
|
70 os.mkdir(os.path.join(*path)) |
|
71 |
|
72 def remove(*path): |
|
73 path = os.path.join(*path) |
|
74 if os.path.isdir(path): |
|
75 shutil.rmtree(path) |
|
76 else: |
|
77 os.remove(path) |
|
78 |
|
79 def rmdir(*path): |
|
80 shutil.rmtree(os.path.join(*path)) |
|
81 |
|
82 def write(dir, *args): |
|
83 path = os.path.join(dir, *(args[:-1])) |
|
84 f = open(path, 'w') |
|
85 f.write(args[-1]) |
|
86 f.flush() |
|
87 fsync(f.fileno()) |
|
88 f.close() |
|
89 |
|
90 ## FIXME - check for other platforms |
|
91 MUST_CLOSE_FDS = not sys.platform.startswith('win') |
|
92 |
|
93 def system(command, input=''): |
|
94 env = dict(os.environ) |
|
95 env['COLUMNS'] = '80' |
|
96 p = subprocess.Popen(command, |
|
97 shell=True, |
|
98 stdin=subprocess.PIPE, |
|
99 stdout=subprocess.PIPE, |
|
100 stderr=subprocess.PIPE, |
|
101 close_fds=MUST_CLOSE_FDS, |
|
102 env=env, |
|
103 ) |
|
104 i, o, e = (p.stdin, p.stdout, p.stderr) |
|
105 if input: |
|
106 i.write(input) |
|
107 i.close() |
|
108 result = o.read() + e.read() |
|
109 o.close() |
|
110 e.close() |
|
111 return result |
|
112 |
|
113 def call_py(interpreter, cmd, flags=None): |
|
114 if sys.platform == 'win32': |
|
115 args = ['"%s"' % arg for arg in (interpreter, flags, cmd) if arg] |
|
116 args.insert(-1, '"-c"') |
|
117 return system('"%s"' % ' '.join(args)) |
|
118 else: |
|
119 cmd = repr(cmd) |
|
120 return system( |
|
121 ' '.join(arg for arg in (interpreter, flags, '-c', cmd) if arg)) |
|
122 |
|
123 def get(url): |
|
124 return urllib2.urlopen(url).read() |
|
125 |
|
126 def _runsetup(setup, executable, *args): |
|
127 if os.path.isdir(setup): |
|
128 setup = os.path.join(setup, 'setup.py') |
|
129 d = os.path.dirname(setup) |
|
130 |
|
131 args = [zc.buildout.easy_install._safe_arg(arg) |
|
132 for arg in args] |
|
133 args.insert(0, '-q') |
|
134 env = dict(os.environ) |
|
135 if executable == sys.executable: |
|
136 env['PYTHONPATH'] = setuptools_location |
|
137 # else pass an executable that has setuptools! See testselectingpython.py. |
|
138 args.append(env) |
|
139 |
|
140 here = os.getcwd() |
|
141 try: |
|
142 os.chdir(d) |
|
143 os.spawnle(os.P_WAIT, executable, |
|
144 zc.buildout.easy_install._safe_arg(executable), |
|
145 setup, *args) |
|
146 if os.path.exists('build'): |
|
147 rmtree('build') |
|
148 finally: |
|
149 os.chdir(here) |
|
150 |
|
151 def sdist(setup, dest): |
|
152 _runsetup(setup, sys.executable, 'sdist', '-d', dest, '--formats=zip') |
|
153 |
|
154 def bdist_egg(setup, executable, dest): |
|
155 _runsetup(setup, executable, 'bdist_egg', '-d', dest) |
|
156 |
|
157 def sys_install(setup, dest): |
|
158 _runsetup(setup, sys.executable, 'install', '--install-purelib', dest, |
|
159 '--record', os.path.join(dest, '__added_files__'), |
|
160 '--single-version-externally-managed') |
|
161 |
|
162 def find_python(version): |
|
163 e = os.environ.get('PYTHON%s' % version) |
|
164 if e is not None: |
|
165 return e |
|
166 if is_win32: |
|
167 e = '\Python%s%s\python.exe' % tuple(version.split('.')) |
|
168 if os.path.exists(e): |
|
169 return e |
|
170 else: |
|
171 cmd = 'python%s -c "import sys; print sys.executable"' % version |
|
172 p = subprocess.Popen(cmd, |
|
173 shell=True, |
|
174 stdin=subprocess.PIPE, |
|
175 stdout=subprocess.PIPE, |
|
176 stderr=subprocess.STDOUT, |
|
177 close_fds=MUST_CLOSE_FDS) |
|
178 i, o = (p.stdin, p.stdout) |
|
179 i.close() |
|
180 e = o.read().strip() |
|
181 o.close() |
|
182 if os.path.exists(e): |
|
183 return e |
|
184 cmd = 'python -c "import sys; print \'%s.%s\' % sys.version_info[:2]"' |
|
185 p = subprocess.Popen(cmd, |
|
186 shell=True, |
|
187 stdin=subprocess.PIPE, |
|
188 stdout=subprocess.PIPE, |
|
189 stderr=subprocess.STDOUT, |
|
190 close_fds=MUST_CLOSE_FDS) |
|
191 i, o = (p.stdin, p.stdout) |
|
192 i.close() |
|
193 e = o.read().strip() |
|
194 o.close() |
|
195 if e == version: |
|
196 cmd = 'python -c "import sys; print sys.executable"' |
|
197 p = subprocess.Popen(cmd, |
|
198 shell=True, |
|
199 stdin=subprocess.PIPE, |
|
200 stdout=subprocess.PIPE, |
|
201 stderr=subprocess.STDOUT, |
|
202 close_fds=MUST_CLOSE_FDS) |
|
203 i, o = (p.stdin, p.stdout) |
|
204 i.close() |
|
205 e = o.read().strip() |
|
206 o.close() |
|
207 if os.path.exists(e): |
|
208 return e |
|
209 |
|
210 raise ValueError( |
|
211 "Couldn't figure out the executable for Python %(version)s.\n" |
|
212 "Set the environment variable PYTHON%(version)s to the location\n" |
|
213 "of the Python %(version)s executable before running the tests." |
|
214 % {'version': version}) |
|
215 |
|
216 def wait_until(label, func, *args, **kw): |
|
217 if 'timeout' in kw: |
|
218 kw = dict(kw) |
|
219 timeout = kw.pop('timeout') |
|
220 else: |
|
221 timeout = 30 |
|
222 deadline = time.time()+timeout |
|
223 while time.time() < deadline: |
|
224 if func(*args, **kw): |
|
225 return |
|
226 time.sleep(0.01) |
|
227 raise ValueError('Timed out waiting for: '+label) |
|
228 |
|
229 def get_installer_values(): |
|
230 """Get the current values for the easy_install module. |
|
231 |
|
232 This is necessary because instantiating a Buildout will force the |
|
233 Buildout's values on the installer. |
|
234 |
|
235 Returns a dict of names-values suitable for set_installer_values.""" |
|
236 names = ('default_versions', 'download_cache', 'install_from_cache', |
|
237 'prefer_final', 'include_site_packages', |
|
238 'allowed_eggs_from_site_packages', 'use_dependency_links', |
|
239 'allow_picked_versions', 'always_unzip' |
|
240 ) |
|
241 values = {} |
|
242 for name in names: |
|
243 values[name] = getattr(zc.buildout.easy_install, name)() |
|
244 return values |
|
245 |
|
246 def set_installer_values(values): |
|
247 """Set the given values on the installer.""" |
|
248 for name, value in values.items(): |
|
249 getattr(zc.buildout.easy_install, name)(value) |
|
250 |
|
251 def make_buildout(executable=None): |
|
252 """Make a buildout that uses this version of zc.buildout.""" |
|
253 # Create a basic buildout.cfg to avoid a warning from buildout. |
|
254 open('buildout.cfg', 'w').write( |
|
255 "[buildout]\nparts =\n" |
|
256 ) |
|
257 # Get state of installer defaults so we can reinstate them (instantiating |
|
258 # a Buildout will force the Buildout's defaults on the installer). |
|
259 installer_values = get_installer_values() |
|
260 # Use the buildout bootstrap command to create a buildout |
|
261 config = [ |
|
262 ('buildout', 'log-level', 'WARNING'), |
|
263 # trick bootstrap into putting the buildout develop egg |
|
264 # in the eggs dir. |
|
265 ('buildout', 'develop-eggs-directory', 'eggs'), |
|
266 ] |
|
267 if executable is not None: |
|
268 config.append(('buildout', 'executable', executable)) |
|
269 zc.buildout.buildout.Buildout( |
|
270 'buildout.cfg', config, |
|
271 user_defaults=False, |
|
272 ).bootstrap([]) |
|
273 # Create the develop-eggs dir, which didn't get created the usual |
|
274 # way due to the trick above: |
|
275 os.mkdir('develop-eggs') |
|
276 # Reinstate the default values of the installer. |
|
277 set_installer_values(installer_values) |
|
278 |
|
279 def buildoutSetUp(test): |
|
280 |
|
281 test.globs['__tear_downs'] = __tear_downs = [] |
|
282 test.globs['register_teardown'] = register_teardown = __tear_downs.append |
|
283 |
|
284 installer_values = get_installer_values() |
|
285 register_teardown( |
|
286 lambda: set_installer_values(installer_values) |
|
287 ) |
|
288 |
|
289 here = os.getcwd() |
|
290 register_teardown(lambda: os.chdir(here)) |
|
291 |
|
292 handlers_before_set_up = logging.getLogger().handlers[:] |
|
293 def restore_root_logger_handlers(): |
|
294 root_logger = logging.getLogger() |
|
295 for handler in root_logger.handlers[:]: |
|
296 root_logger.removeHandler(handler) |
|
297 for handler in handlers_before_set_up: |
|
298 root_logger.addHandler(handler) |
|
299 register_teardown(restore_root_logger_handlers) |
|
300 |
|
301 base = tempfile.mkdtemp('buildoutSetUp') |
|
302 base = os.path.realpath(base) |
|
303 register_teardown(lambda base=base: rmtree(base)) |
|
304 |
|
305 old_home = os.environ.get('HOME') |
|
306 os.environ['HOME'] = os.path.join(base, 'bbbBadHome') |
|
307 def restore_home(): |
|
308 if old_home is None: |
|
309 del os.environ['HOME'] |
|
310 else: |
|
311 os.environ['HOME'] = old_home |
|
312 register_teardown(restore_home) |
|
313 |
|
314 base = os.path.join(base, '_TEST_') |
|
315 os.mkdir(base) |
|
316 |
|
317 tmp = tempfile.mkdtemp('buildouttests') |
|
318 register_teardown(lambda: rmtree(tmp)) |
|
319 |
|
320 zc.buildout.easy_install.default_index_url = 'file://'+tmp |
|
321 os.environ['buildout-testing-index-url'] = ( |
|
322 zc.buildout.easy_install.default_index_url) |
|
323 os.environ.pop('PYTHONPATH', None) |
|
324 |
|
325 def tmpdir(name): |
|
326 path = os.path.join(base, name) |
|
327 mkdir(path) |
|
328 return path |
|
329 |
|
330 sample = tmpdir('sample-buildout') |
|
331 |
|
332 os.chdir(sample) |
|
333 make_buildout() |
|
334 |
|
335 def start_server(path): |
|
336 port, thread = _start_server(path, name=path) |
|
337 url = 'http://localhost:%s/' % port |
|
338 register_teardown(lambda: stop_server(url, thread)) |
|
339 return url |
|
340 |
|
341 def make_py(initialization=''): |
|
342 """Returns paths to new executable and to its site-packages. |
|
343 """ |
|
344 buildout = tmpdir('executable_buildout') |
|
345 site_packages_dir = os.path.join(buildout, 'site-packages') |
|
346 mkdir(site_packages_dir) |
|
347 old_wd = os.getcwd() |
|
348 os.chdir(buildout) |
|
349 make_buildout() |
|
350 # Normally we don't process .pth files in extra-paths. We want to |
|
351 # in this case so that we can test with setuptools system installs |
|
352 # (--single-version-externally-managed), which use .pth files. |
|
353 initialization = ( |
|
354 ('import sys\n' |
|
355 'import site\n' |
|
356 'known_paths = set(sys.path)\n' |
|
357 'site_packages_dir = %r\n' |
|
358 'site.addsitedir(site_packages_dir, known_paths)\n' |
|
359 ) % (site_packages_dir,)) + initialization |
|
360 initialization = '\n'.join( |
|
361 ' ' + line for line in initialization.split('\n')) |
|
362 install_develop( |
|
363 'zc.recipe.egg', os.path.join(buildout, 'develop-eggs')) |
|
364 install_develop( |
|
365 'z3c.recipe.scripts', os.path.join(buildout, 'develop-eggs')) |
|
366 write('buildout.cfg', textwrap.dedent('''\ |
|
367 [buildout] |
|
368 parts = py |
|
369 include-site-packages = false |
|
370 exec-sitecustomize = false |
|
371 |
|
372 [py] |
|
373 recipe = z3c.recipe.scripts |
|
374 interpreter = py |
|
375 initialization = |
|
376 %(initialization)s |
|
377 extra-paths = %(site-packages)s |
|
378 eggs = setuptools |
|
379 ''') % { |
|
380 'initialization': initialization, |
|
381 'site-packages': site_packages_dir}) |
|
382 system(os.path.join(buildout, 'bin', 'buildout')) |
|
383 os.chdir(old_wd) |
|
384 return ( |
|
385 os.path.join(buildout, 'bin', 'py'), site_packages_dir) |
|
386 |
|
387 test.globs.update(dict( |
|
388 sample_buildout = sample, |
|
389 ls = ls, |
|
390 cat = cat, |
|
391 mkdir = mkdir, |
|
392 rmdir = rmdir, |
|
393 remove = remove, |
|
394 tmpdir = tmpdir, |
|
395 write = write, |
|
396 system = system, |
|
397 call_py = call_py, |
|
398 get = get, |
|
399 cd = (lambda *path: os.chdir(os.path.join(*path))), |
|
400 join = os.path.join, |
|
401 sdist = sdist, |
|
402 bdist_egg = bdist_egg, |
|
403 start_server = start_server, |
|
404 buildout = os.path.join(sample, 'bin', 'buildout'), |
|
405 wait_until = wait_until, |
|
406 make_py = make_py |
|
407 )) |
|
408 |
|
409 def buildoutTearDown(test): |
|
410 for f in test.globs['__tear_downs']: |
|
411 f() |
|
412 |
|
413 class Server(BaseHTTPServer.HTTPServer): |
|
414 |
|
415 def __init__(self, tree, *args): |
|
416 BaseHTTPServer.HTTPServer.__init__(self, *args) |
|
417 self.tree = os.path.abspath(tree) |
|
418 |
|
419 __run = True |
|
420 def serve_forever(self): |
|
421 while self.__run: |
|
422 self.handle_request() |
|
423 |
|
424 def handle_error(self, *_): |
|
425 self.__run = False |
|
426 |
|
427 class Handler(BaseHTTPServer.BaseHTTPRequestHandler): |
|
428 |
|
429 Server.__log = False |
|
430 |
|
431 def __init__(self, request, address, server): |
|
432 self.__server = server |
|
433 self.tree = server.tree |
|
434 BaseHTTPServer.BaseHTTPRequestHandler.__init__( |
|
435 self, request, address, server) |
|
436 |
|
437 def do_GET(self): |
|
438 if '__stop__' in self.path: |
|
439 raise SystemExit |
|
440 |
|
441 if self.path == '/enable_server_logging': |
|
442 self.__server.__log = True |
|
443 self.send_response(200) |
|
444 return |
|
445 |
|
446 if self.path == '/disable_server_logging': |
|
447 self.__server.__log = False |
|
448 self.send_response(200) |
|
449 return |
|
450 |
|
451 path = os.path.abspath(os.path.join(self.tree, *self.path.split('/'))) |
|
452 if not ( |
|
453 ((path == self.tree) or path.startswith(self.tree+os.path.sep)) |
|
454 and |
|
455 os.path.exists(path) |
|
456 ): |
|
457 self.send_response(404, 'Not Found') |
|
458 #self.send_response(200) |
|
459 out = '<html><body>Not Found</body></html>' |
|
460 #out = '\n'.join(self.tree, self.path, path) |
|
461 self.send_header('Content-Length', str(len(out))) |
|
462 self.send_header('Content-Type', 'text/html') |
|
463 self.end_headers() |
|
464 self.wfile.write(out) |
|
465 return |
|
466 |
|
467 self.send_response(200) |
|
468 if os.path.isdir(path): |
|
469 out = ['<html><body>\n'] |
|
470 names = os.listdir(path) |
|
471 names.sort() |
|
472 for name in names: |
|
473 if os.path.isdir(os.path.join(path, name)): |
|
474 name += '/' |
|
475 out.append('<a href="%s">%s</a><br>\n' % (name, name)) |
|
476 out.append('</body></html>\n') |
|
477 out = ''.join(out) |
|
478 self.send_header('Content-Length', str(len(out))) |
|
479 self.send_header('Content-Type', 'text/html') |
|
480 else: |
|
481 out = open(path, 'rb').read() |
|
482 self.send_header('Content-Length', len(out)) |
|
483 if path.endswith('.egg'): |
|
484 self.send_header('Content-Type', 'application/zip') |
|
485 elif path.endswith('.gz'): |
|
486 self.send_header('Content-Type', 'application/x-gzip') |
|
487 elif path.endswith('.zip'): |
|
488 self.send_header('Content-Type', 'application/x-gzip') |
|
489 else: |
|
490 self.send_header('Content-Type', 'text/html') |
|
491 self.end_headers() |
|
492 |
|
493 self.wfile.write(out) |
|
494 |
|
495 def log_request(self, code): |
|
496 if self.__server.__log: |
|
497 print '%s %s %s' % (self.command, code, self.path) |
|
498 |
|
499 def _run(tree, port): |
|
500 server_address = ('localhost', port) |
|
501 httpd = Server(tree, server_address, Handler) |
|
502 httpd.serve_forever() |
|
503 |
|
504 def get_port(): |
|
505 for i in range(10): |
|
506 port = random.randrange(20000, 30000) |
|
507 s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) |
|
508 try: |
|
509 try: |
|
510 s.connect(('localhost', port)) |
|
511 except socket.error: |
|
512 return port |
|
513 finally: |
|
514 s.close() |
|
515 raise RuntimeError, "Can't find port" |
|
516 |
|
517 def _start_server(tree, name=''): |
|
518 port = get_port() |
|
519 thread = threading.Thread(target=_run, args=(tree, port), name=name) |
|
520 thread.setDaemon(True) |
|
521 thread.start() |
|
522 wait(port, up=True) |
|
523 return port, thread |
|
524 |
|
525 def start_server(tree): |
|
526 return _start_server(tree)[0] |
|
527 |
|
528 def stop_server(url, thread=None): |
|
529 try: |
|
530 urllib2.urlopen(url+'__stop__') |
|
531 except Exception: |
|
532 pass |
|
533 if thread is not None: |
|
534 thread.join() # wait for thread to stop |
|
535 |
|
536 def wait(port, up): |
|
537 addr = 'localhost', port |
|
538 for i in range(120): |
|
539 time.sleep(0.25) |
|
540 try: |
|
541 s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) |
|
542 s.connect(addr) |
|
543 s.close() |
|
544 if up: |
|
545 break |
|
546 except socket.error, e: |
|
547 if e[0] not in (errno.ECONNREFUSED, errno.ECONNRESET): |
|
548 raise |
|
549 s.close() |
|
550 if not up: |
|
551 break |
|
552 else: |
|
553 if up: |
|
554 raise |
|
555 else: |
|
556 raise SystemError("Couln't stop server") |
|
557 |
|
558 def install(project, destination): |
|
559 if not isinstance(destination, basestring): |
|
560 destination = os.path.join(destination.globs['sample_buildout'], |
|
561 'eggs') |
|
562 |
|
563 dist = pkg_resources.working_set.find( |
|
564 pkg_resources.Requirement.parse(project)) |
|
565 if dist.location.endswith('.egg'): |
|
566 destination = os.path.join(destination, |
|
567 os.path.basename(dist.location), |
|
568 ) |
|
569 if os.path.isdir(dist.location): |
|
570 shutil.copytree(dist.location, destination) |
|
571 else: |
|
572 shutil.copyfile(dist.location, destination) |
|
573 else: |
|
574 # copy link |
|
575 open(os.path.join(destination, project+'.egg-link'), 'w' |
|
576 ).write(dist.location) |
|
577 |
|
578 def install_develop(project, destination): |
|
579 if not isinstance(destination, basestring): |
|
580 destination = os.path.join(destination.globs['sample_buildout'], |
|
581 'develop-eggs') |
|
582 |
|
583 dist = pkg_resources.working_set.find( |
|
584 pkg_resources.Requirement.parse(project)) |
|
585 open(os.path.join(destination, project+'.egg-link'), 'w' |
|
586 ).write(dist.location) |
|
587 |
|
588 def _normalize_path(match): |
|
589 path = match.group(1) |
|
590 if os.path.sep == '\\': |
|
591 path = path.replace('\\\\', '/') |
|
592 if path.startswith('\\'): |
|
593 path = path[1:] |
|
594 return '/' + path.replace(os.path.sep, '/') |
|
595 |
|
596 if sys.platform == 'win32': |
|
597 sep = r'[\\/]' # Windows uses both sometimes. |
|
598 else: |
|
599 sep = re.escape(os.path.sep) |
|
600 normalize_path = ( |
|
601 re.compile( |
|
602 r'''[^'" \t\n\r!]+%(sep)s_[Tt][Ee][Ss][Tt]_%(sep)s([^"' \t\n\r]+)''' |
|
603 % dict(sep=sep)), |
|
604 _normalize_path, |
|
605 ) |
|
606 |
|
607 normalize_endings = re.compile('\r\n'), '\n' |
|
608 |
|
609 normalize_script = ( |
|
610 re.compile('(\n?)- ([a-zA-Z_.-]+)-script.py\n- \\2.exe\n'), |
|
611 '\\1- \\2\n') |
|
612 |
|
613 normalize_egg_py = ( |
|
614 re.compile('-py\d[.]\d(-\S+)?.egg'), |
|
615 '-pyN.N.egg', |
|
616 ) |