36
36
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
37
37
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
38
38
# SOFTWARE.
39
- import os
40
39
from importlib import invalidate_caches
41
- from io import StringIO
42
- from pathlib import Path
43
- from string import Formatter
44
40
45
41
import gc
42
+ import os
43
+ import shutil
46
44
import sys
45
+ import unittest
46
+ from copy import deepcopy
47
+ from io import StringIO
48
+ from pathlib import Path
49
+ from string import Formatter
47
50
48
51
DIR = Path (__file__ ).parent .absolute ()
49
52
@@ -81,23 +84,14 @@ def unhandled_error_compare_with_message(x, y):
81
84
else :
82
85
return x == y
83
86
84
- class CPyExtTestCase ():
85
87
86
- def setUpClass (self ):
87
- for typ in type (self ).mro ():
88
- for k , v in typ .__dict__ .items ():
89
- if k .startswith ("test_" ):
90
- modname = k .replace ("test_" , "" )
91
- if k .startswith ("test_graalpython_" ):
92
- if not GRAALPYTHON :
93
- continue
94
- else :
95
- modname = k .replace ("test_graalpython_" , "" )
96
- self .compile_module (modname )
88
+ class CPyExtTestCase (unittest .TestCase ):
89
+ pass
97
90
98
91
99
92
compiled_registry = set ()
100
93
94
+
101
95
def ccompile (self , name , check_duplicate_name = True ):
102
96
from distutils .core import setup , Extension
103
97
from distutils .sysconfig import get_config_var
@@ -115,19 +109,21 @@ def ccompile(self, name, check_duplicate_name=True):
115
109
m .update (block )
116
110
cur_checksum = m .hexdigest ()
117
111
112
+ build_dir = DIR / 'build' / name
113
+
118
114
# see if there is already a checksum file
119
- checksum_file = DIR / f'{ name } { EXT_SUFFIX } .sha256'
115
+ checksum_file = build_dir / f'{ name } { EXT_SUFFIX } .sha256'
120
116
available_checksum = ""
121
117
if checksum_file .exists ():
122
118
# read checksum file
123
119
with open (checksum_file , "r" ) as f :
124
120
available_checksum = f .readline ()
125
121
126
122
# note, the suffix is already a string like '.so'
127
- lib_file = DIR / f'{ name } { EXT_SUFFIX } '
123
+ lib_file = build_dir / f'{ name } { EXT_SUFFIX } '
128
124
129
125
if check_duplicate_name and available_checksum != cur_checksum and name in compiled_registry :
130
- print (f"\n \n WARNING: module with name '{ name } ' was already compiled, but with different source code. "
126
+ raise RuntimeError (f"\n \n Module with name '{ name } ' was already compiled, but with different source code. "
131
127
"Have you accidentally used the same name for two different CPyExtType, CPyExtHeapType, "
132
128
"or similar helper calls? Modules with same name can sometimes confuse the import machinery "
133
129
"and cause all sorts of trouble.\n " )
@@ -138,17 +134,28 @@ def ccompile(self, name, check_duplicate_name=True):
138
134
# Note: It could be that the C source file's checksum didn't change but someone
139
135
# manually deleted the shared library file.
140
136
if available_checksum != cur_checksum or not lib_file .exists ():
141
- module = Extension (name , sources = [str (source_file )])
142
- verbosity = '--verbose' if sys .flags .verbose else '--quiet'
143
- args = [verbosity , 'build' , 'install_lib' , '-f' , f'--install-dir={ DIR } ' , 'clean' ]
144
- setup (
145
- script_name = 'setup' ,
146
- script_args = args ,
147
- name = name ,
148
- version = '1.0' ,
149
- description = '' ,
150
- ext_modules = [module ]
151
- )
137
+ os .makedirs (build_dir , exist_ok = True )
138
+ # MSVC linker doesn't like absolute paths in some parameters, so just run from the build dir
139
+ old_cwd = os .getcwd ()
140
+ os .chdir (build_dir )
141
+ try :
142
+ shutil .copy (source_file , '.' )
143
+ module = Extension (name , sources = [source_file .name ])
144
+ args = [
145
+ '--verbose' if sys .flags .verbose else '--quiet' ,
146
+ 'build' ,
147
+ 'install_lib' , '-f' , '--install-dir=.' ,
148
+ ]
149
+ setup (
150
+ script_name = 'setup' ,
151
+ script_args = args ,
152
+ name = name ,
153
+ version = '1.0' ,
154
+ description = '' ,
155
+ ext_modules = [module ]
156
+ )
157
+ finally :
158
+ os .chdir (old_cwd )
152
159
153
160
# write new checksum
154
161
with open (checksum_file , "w" ) as f :
@@ -164,6 +171,8 @@ def ccompile(self, name, check_duplicate_name=True):
164
171
if GRAALPYTHON :
165
172
file_not_empty (lib_file )
166
173
174
+ return str (build_dir )
175
+
167
176
168
177
def file_not_empty (path ):
169
178
for i in range (3 ):
@@ -340,13 +349,13 @@ def file_not_empty(path):
340
349
"""
341
350
342
351
343
- class CPyExtFunction () :
352
+ class CPyExtFunction :
344
353
345
354
def __init__ (self , pfunc , parameters , template = c_template , cmpfunc = None , stderr_validator = None , ** kwargs ):
346
355
self .template = template
347
356
self .pfunc = pfunc
348
357
self .parameters = parameters
349
- kwargs [ "name" ] = kwargs [ "name" ] if "name" in kwargs else None
358
+ kwargs . setdefault ( "name" , None )
350
359
self .name = kwargs ["name" ]
351
360
if "code" in kwargs :
352
361
kwargs ["customcode" ] = kwargs ["code" ]
@@ -401,12 +410,28 @@ def _insert(self, d, name, default_value):
401
410
def __repr__ (self ):
402
411
return "<CPyExtFunction %s>" % self .name
403
412
404
- def test (self ):
405
- sys .path .insert (0 , str (DIR ))
413
+ def __set_name__ (self , owner , name ):
414
+ if self .name :
415
+ raise RuntimeError (f"{ type (self )} already assigned to a test suite. Use copy() method to duplicate a test" )
416
+ self .name = name .removeprefix ('test_' )
417
+ self .__name__ = name
418
+ self .__qualname__ = f'{ owner .__qualname__ } .{ name } '
419
+
420
+ def copy (self ):
421
+ inst = deepcopy (self )
422
+ inst .name = None
423
+ return inst
424
+
425
+ @property
426
+ def __code__ (self ):
427
+ return type (self ).__call__ .__code__
428
+
429
+ def __call__ (self ):
406
430
try :
407
- cmodule = __import__ (self .name )
408
- finally :
409
- sys .path .pop (0 )
431
+ self .create_module (self .name )
432
+ cmodule = compile_module_from_file (self .name )
433
+ except Exception as e :
434
+ raise RuntimeError (f"{ self .__qualname__ } : Failed to create module" ) from e
410
435
ctest = getattr (cmodule , "test_%s" % self .name )
411
436
cargs = self .parameters ()
412
437
pargs = self .parameters ()
@@ -416,7 +441,7 @@ def test(self):
416
441
sys .stderr = StringIO ()
417
442
try :
418
443
cresult = ctest (cargs [i ])
419
- except BaseException as e :
444
+ except Exception as e :
420
445
cresult = e
421
446
else :
422
447
if self .stderr_validator :
@@ -430,20 +455,13 @@ def test(self):
430
455
except BaseException as e :
431
456
presult = e
432
457
433
- if not self .cmpfunc :
434
- assert cresult == presult , ( "%r == %r in %s(%s)" % ( cresult , presult , self . name , pargs [ i ]) )
458
+ if self .cmpfunc :
459
+ success = self . cmpfunc ( cresult , presult )
435
460
else :
436
- assert self .cmpfunc (cresult , presult ), ("%r == %r in %s(%s)" % (cresult , presult , self .name , pargs [i ]))
461
+ success = cresult == presult
462
+ assert success , f"{ self .__qualname__ } : C result not equal to python reference implementation result.\n \t Arguments: { pargs [i ]!r} \n \t Expected result: { presult !r} \n \t Actual C result: { cresult !r} "
437
463
gc .collect ()
438
464
439
- def __get__ (self , instance , typ = None ):
440
- if typ is None :
441
- return self
442
- else :
443
- CPyExtFunction .test .__name__ = self .name
444
- CPyExtFunction .test .__qualname__ = f'{ CPyExtFunction .__name__ } .test_{ self .name } '
445
- return self .test
446
-
447
465
448
466
class CPyExtFunctionOutVars (CPyExtFunction ):
449
467
'''
@@ -509,27 +527,28 @@ def get_value(self, key, args, kwds):
509
527
return Formatter .get_value (key , args , kwds )
510
528
511
529
512
- def _compile_module (c_source , name ):
530
+ def compile_module_from_string (c_source : str , name : str ):
513
531
source_file = DIR / f'{ name } .c'
514
532
with open (source_file , "wb" , buffering = 0 ) as f :
515
533
f .write (bytes (c_source , 'utf-8' ))
516
- # ensure file was really written
517
- try :
518
- stat_result = os .stat (source_file )
519
- if stat_result [6 ] == 0 :
520
- raise SystemError ("empty source file %s" % (source_file ,))
521
- except FileNotFoundError :
522
- raise SystemError ("source file %s not available" % (source_file ,))
523
- ccompile (None , name )
524
- sys .path .insert (0 , str (DIR ))
534
+ return compile_module_from_file (name )
535
+
536
+
537
+ def compile_module_from_file (module_name : str ):
538
+ install_dir = ccompile (None , module_name )
539
+ sys .path .insert (0 , install_dir )
525
540
try :
526
- cmodule = __import__ (name )
541
+ cmodule = __import__ (module_name )
527
542
finally :
528
543
sys .path .pop (0 )
529
544
return cmodule
530
545
531
546
532
547
def CPyExtType (name , code = '' , ** kwargs ):
548
+ kwargs ['set_base_code' ] = ''
549
+ # We set tp_base later, because MSVC doesn't see the usual &PySomething_Type expressions as constants
550
+ if tp_base := kwargs .get ('tp_base' ):
551
+ kwargs ['set_base_code' ] = f'{ name } Type.tp_base = { tp_base } ;'
533
552
mod_template = """
534
553
static PyModuleDef {name}module = {{
535
554
PyModuleDef_HEAD_INIT,
@@ -546,6 +565,7 @@ def CPyExtType(name, code='', **kwargs):
546
565
if (m == NULL)
547
566
return NULL;
548
567
568
+ {set_base_code}
549
569
{ready_code}
550
570
if (PyType_Ready(&{name}Type) < 0)
551
571
return NULL;
@@ -564,7 +584,7 @@ def CPyExtType(name, code='', **kwargs):
564
584
kwargs .setdefault ("post_ready_code" , "" )
565
585
c_source = type_decl + UnseenFormatter ().format (mod_template , ** kwargs )
566
586
567
- cmodule = _compile_module (c_source , name )
587
+ cmodule = compile_module_from_string (c_source , name )
568
588
return getattr (cmodule , name )
569
589
570
590
@@ -690,7 +710,7 @@ def CPyExtTypeDecl(name, code='', **kwargs):
690
710
{name}_methods, /* tp_methods */
691
711
{name}_members, /* tp_members */
692
712
{name}_getset, /* tp_getset */
693
- {tp_base}, /* tp_base */
713
+ 0, /* set later */ /* tp_base */
694
714
{tp_dict}, /* tp_dict */
695
715
{tp_descr_get}, /* tp_descr_get */
696
716
{tp_descr_set}, /* tp_descr_set */
@@ -781,7 +801,7 @@ def CPyExtHeapType(name, bases=(object), code='', slots=None, **kwargs):
781
801
kwargs .setdefault ("ready_code" , "" )
782
802
kwargs .setdefault ("post_ready_code" , "" )
783
803
code = UnseenFormatter ().format (template , ** kwargs )
784
- mod = _compile_module (code , name )
804
+ mod = compile_module_from_string (code , name )
785
805
return mod .create (bases )
786
806
787
807
0 commit comments