13
13
from pathlib import Path
14
14
from typing import Any , ClassVar
15
15
16
+ import attrs
17
+ import pycparser # type: ignore[import-untyped]
18
+ import pycparser .c_ast # type: ignore[import-untyped]
19
+ import pycparser .c_generator # type: ignore[import-untyped]
16
20
from cffi import FFI
17
21
18
22
# ruff: noqa: T201
@@ -204,7 +208,8 @@ def walk_sources(directory: str) -> Iterator[str]:
204
208
extra_link_args .extend (GCC_CFLAGS [tdl_build ])
205
209
206
210
ffi = FFI ()
207
- ffi .cdef (build_sdl .get_cdef ())
211
+ sdl_cdef = build_sdl .get_cdef ()
212
+ ffi .cdef (sdl_cdef )
208
213
for include in includes :
209
214
try :
210
215
ffi .cdef (include .header )
@@ -383,10 +388,10 @@ def write_library_constants() -> None:
383
388
f .write (f"""{ parse_sdl_attrs ("SDL_SCANCODE" , None )[0 ]} \n """ )
384
389
385
390
f .write ("\n # --- SDL keyboard symbols ---\n " )
386
- f .write (f"""{ parse_sdl_attrs ("SDLK " , None )[0 ]} \n """ )
391
+ f .write (f"""{ parse_sdl_attrs ("SDLK_ " , None )[0 ]} \n """ )
387
392
388
393
f .write ("\n # --- SDL keyboard modifiers ---\n " )
389
- f .write ("{}\n _REVERSE_MOD_TABLE = {}\n " .format (* parse_sdl_attrs ("KMOD " , None )))
394
+ f .write ("{}\n _REVERSE_MOD_TABLE = {}\n " .format (* parse_sdl_attrs ("SDL_KMOD " , None )))
390
395
391
396
f .write ("\n # --- SDL wheel ---\n " )
392
397
f .write ("{}\n _REVERSE_WHEEL_TABLE = {}\n " .format (* parse_sdl_attrs ("SDL_MOUSEWHEEL" , all_names )))
@@ -411,5 +416,220 @@ def write_library_constants() -> None:
411
416
Path ("tcod/event.py" ).write_text (event_py , encoding = "utf-8" )
412
417
413
418
419
+ def _fix_reserved_name (name : str ) -> str :
420
+ """Add underscores to reserved Python keywords."""
421
+ assert isinstance (name , str )
422
+ if name in ("def" , "in" ):
423
+ return name + "_"
424
+ return name
425
+
426
+
427
+ @attrs .define (frozen = True )
428
+ class ConvertedParam :
429
+ name : str = attrs .field (converter = _fix_reserved_name )
430
+ hint : str
431
+ original : str
432
+
433
+
434
+ def _type_from_names (names : list [str ]) -> str :
435
+ if not names :
436
+ return ""
437
+ if names [- 1 ] == "void" :
438
+ return "None"
439
+ if names in (["unsigned" , "char" ], ["bool" ]):
440
+ return "bool"
441
+ if names [- 1 ] in ("size_t" , "int" , "ptrdiff_t" ):
442
+ return "int"
443
+ return "Any"
444
+
445
+
446
+ def _param_as_hint (node : pycparser .c_ast .Node , default_name : str ) -> ConvertedParam :
447
+ original = pycparser .c_generator .CGenerator ().visit (node )
448
+ name : str
449
+ names : list [str ]
450
+ match node :
451
+ case pycparser .c_ast .Typename (type = pycparser .c_ast .TypeDecl (type = pycparser .c_ast .IdentifierType (names = names ))):
452
+ # Unnamed type
453
+ return ConvertedParam (default_name , _type_from_names (names ), original )
454
+ case pycparser .c_ast .Decl (
455
+ name = name , type = pycparser .c_ast .TypeDecl (type = pycparser .c_ast .IdentifierType (names = names ))
456
+ ):
457
+ # Named type
458
+ return ConvertedParam (name , _type_from_names (names ), original )
459
+ case pycparser .c_ast .Decl (
460
+ name = name ,
461
+ type = pycparser .c_ast .ArrayDecl (
462
+ type = pycparser .c_ast .TypeDecl (type = pycparser .c_ast .IdentifierType (names = names ))
463
+ ),
464
+ ):
465
+ # Named array
466
+ return ConvertedParam (name , "Any" , original )
467
+ case pycparser .c_ast .Decl (name = name , type = pycparser .c_ast .PtrDecl ()):
468
+ # Named pointer
469
+ return ConvertedParam (name , "Any" , original )
470
+ case pycparser .c_ast .Typename (name = name , type = pycparser .c_ast .PtrDecl ()):
471
+ # Forwarded struct
472
+ return ConvertedParam (name or default_name , "Any" , original )
473
+ case pycparser .c_ast .TypeDecl (type = pycparser .c_ast .IdentifierType (names = names )):
474
+ # Return type
475
+ return ConvertedParam (default_name , _type_from_names (names ), original )
476
+ case pycparser .c_ast .PtrDecl ():
477
+ # Return pointer
478
+ return ConvertedParam (default_name , "Any" , original )
479
+ case pycparser .c_ast .EllipsisParam ():
480
+ # C variable args
481
+ return ConvertedParam ("*__args" , "Any" , original )
482
+ case _:
483
+ raise AssertionError
484
+
485
+
486
+ class DefinitionCollector (pycparser .c_ast .NodeVisitor ): # type: ignore[misc]
487
+ """Gathers functions and names from C headers."""
488
+
489
+ def __init__ (self ) -> None :
490
+ """Initialize the object with empty values."""
491
+ self .functions : list [str ] = []
492
+ """Indented Python function definitions."""
493
+ self .variables : set [str ] = set ()
494
+ """Python variable definitions."""
495
+
496
+ def parse_defines (self , string : str , / ) -> None :
497
+ """Parse C define directives into hinted names."""
498
+ for match in re .finditer (r"#define\s+(\S+)\s+(\S+)\s*" , string ):
499
+ name , value = match .groups ()
500
+ if value == "..." :
501
+ self .variables .add (f"{ name } : Final[int]" )
502
+ else :
503
+ self .variables .add (f"{ name } : Final[Literal[{ value } ]] = { value } " )
504
+
505
+ def visit_Decl (self , node : pycparser .c_ast .Decl ) -> None : # noqa: N802
506
+ """Parse C FFI functions into type hinted Python functions."""
507
+ match node :
508
+ case pycparser .c_ast .Decl (
509
+ type = pycparser .c_ast .FuncDecl (),
510
+ ):
511
+ assert isinstance (node .type .args , pycparser .c_ast .ParamList ), type (node .type .args )
512
+ arg_hints = [_param_as_hint (param , f"arg{ i } " ) for i , param in enumerate (node .type .args .params )]
513
+ return_hint = _param_as_hint (node .type .type , "" )
514
+ if len (arg_hints ) == 1 and arg_hints [0 ].hint == "None" : # Remove void parameter
515
+ arg_hints = []
516
+
517
+ python_params = [f"{ p .name } : { p .hint } " for p in arg_hints ]
518
+ if python_params :
519
+ if arg_hints [- 1 ].name .startswith ("*" ):
520
+ python_params .insert (- 1 , "/" )
521
+ else :
522
+ python_params .append ("/" )
523
+ c_def = pycparser .c_generator .CGenerator ().visit (node )
524
+ python_def = f"""def { node .name } ({ ", " .join (python_params )} ) -> { return_hint .hint } :"""
525
+ self .functions .append (f''' { python_def } \n """{ c_def } """''' )
526
+
527
+ def visit_Enumerator (self , node : pycparser .c_ast .Enumerator ) -> None : # noqa: N802
528
+ """Parse C enums into hinted names."""
529
+ name : str | None
530
+ value : str | int
531
+ match node :
532
+ case pycparser .c_ast .Enumerator (name = name , value = None ):
533
+ self .variables .add (f"{ name } : Final[int]" )
534
+ case pycparser .c_ast .Enumerator (name = name , value = pycparser .c_ast .ID ()):
535
+ self .variables .add (f"{ name } : Final[int]" )
536
+ case pycparser .c_ast .Enumerator (name = name , value = pycparser .c_ast .Constant (value = value )):
537
+ value = int (str (value ).removesuffix ("u" ), base = 0 )
538
+ self .variables .add (f"{ name } : Final[Literal[{ value } ]] = { value } " )
539
+ case pycparser .c_ast .Enumerator (
540
+ name = name , value = pycparser .c_ast .UnaryOp (op = "-" , expr = pycparser .c_ast .Constant (value = value ))
541
+ ):
542
+ value = - int (str (value ).removesuffix ("u" ), base = 0 )
543
+ self .variables .add (f"{ name } : Final[Literal[{ value } ]] = { value } " )
544
+ case pycparser .c_ast .Enumerator (name = name ):
545
+ self .variables .add (f"{ name } : Final[int]" )
546
+ case _:
547
+ raise AssertionError
548
+
549
+
550
+ def write_hints () -> None :
551
+ """Write a custom _libtcod.pyi file from C definitions."""
552
+ function_collector = DefinitionCollector ()
553
+ c = pycparser .CParser ()
554
+
555
+ # Parse SDL headers
556
+ cdef = sdl_cdef
557
+ cdef = cdef .replace ("int..." , "int" )
558
+ cdef = (
559
+ """
560
+ typedef int int8_t;
561
+ typedef int uint8_t;
562
+ typedef int int16_t;
563
+ typedef int uint16_t;
564
+ typedef int int32_t;
565
+ typedef int uint32_t;
566
+ typedef int int64_t;
567
+ typedef int uint64_t;
568
+ typedef int wchar_t;
569
+ typedef int intptr_t;
570
+ """
571
+ + cdef
572
+ )
573
+ cdef = re .sub (r"(typedef enum SDL_PixelFormat).*(SDL_PixelFormat;)" , r"\1 \2" , cdef , flags = re .DOTALL )
574
+ cdef = cdef .replace ("padding[...]" , "padding[]" )
575
+ cdef = cdef .replace ("...;} SDL_TouchFingerEvent;" , "} SDL_TouchFingerEvent;" )
576
+ function_collector .parse_defines (cdef )
577
+ cdef = re .sub (r"\n#define .*" , "" , cdef )
578
+ cdef = re .sub (r"""extern "Python" \{(.*?)\}""" , r"\1" , cdef , flags = re .DOTALL )
579
+ cdef = re .sub (r"//.*" , "" , cdef )
580
+ ast = c .parse (cdef )
581
+ function_collector .visit (ast )
582
+
583
+ # Parse libtcod headers
584
+ cdef = "\n " .join (include .header for include in includes )
585
+ function_collector .parse_defines (cdef )
586
+ cdef = re .sub (r"\n?#define .*" , "" , cdef )
587
+ cdef = re .sub (r"//.*" , "" , cdef )
588
+ cdef = (
589
+ """
590
+ typedef int int8_t;
591
+ typedef int uint8_t;
592
+ typedef int int16_t;
593
+ typedef int uint16_t;
594
+ typedef int int32_t;
595
+ typedef int uint32_t;
596
+ typedef int int64_t;
597
+ typedef int uint64_t;
598
+ typedef int wchar_t;
599
+ typedef int intptr_t;
600
+ typedef int ptrdiff_t;
601
+ typedef int size_t;
602
+ typedef unsigned char bool;
603
+ typedef void* SDL_PropertiesID;
604
+ """
605
+ + cdef
606
+ )
607
+ cdef = re .sub (r"""extern "Python" \{(.*?)\}""" , r"\1" , cdef , flags = re .DOTALL )
608
+ function_collector .visit (c .parse (cdef ))
609
+
610
+ # Write PYI file
611
+ out_functions = """\n \n @staticmethod\n """ .join (sorted (function_collector .functions ))
612
+ out_variables = "\n " .join (sorted (function_collector .variables ))
613
+
614
+ pyi = f"""\
615
+ # Autogenerated with build_libtcod.py
616
+ from typing import Any, Final, Literal
617
+
618
+ # pyi files for CFFI ports are not standard
619
+ # ruff: noqa: A002, ANN401, D402, D403, D415, N801, N802, N803, N815, PLW0211, PYI021
620
+
621
+ class _lib:
622
+ @staticmethod
623
+ { out_functions }
624
+
625
+ { out_variables }
626
+
627
+ lib: _lib
628
+ ffi: Any
629
+ """
630
+ Path ("tcod/_libtcod.pyi" ).write_text (pyi )
631
+
632
+
414
633
if __name__ == "__main__" :
634
+ write_hints ()
415
635
write_library_constants ()
0 commit comments