-
Notifications
You must be signed in to change notification settings - Fork 25
Expand file tree
/
Copy pathgenerate_wrapper.py
More file actions
executable file
·3241 lines (2965 loc) · 137 KB
/
Copy pathgenerate_wrapper.py
File metadata and controls
executable file
·3241 lines (2965 loc) · 137 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#!/usr/bin/env python
##Copyright 2008-2026 Thomas Paviot (tpaviot@gmail.com)
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
###########
# imports #
###########
import argparse
import configparser
import datetime
import glob
import hashlib # to compute md5 function signatures
import keyword # to prevent using python language keywords
import logging
from operator import itemgetter
import os
import platform
import re
import subprocess
import sys
import time
import CppHeaderParser
from _modules import OCCT_MODULES, TOOLKITS
from _exclusions import (
ENUMS_TO_EXLUDE,
HXX_TO_EXCLUDE_FROM_BEING_INCLUDED,
HXX_TO_EXCLUDE_FROM_CPPPARSER,
NCOLLECTION_WRAPPED_CLASSES,
NODEFAULTCTOR,
STANDARD_INTEGER_TYPEDEF,
TEMPLATES_TO_EXCLUDE,
TYPEDEF_TO_EXCLUDE,
)
from _swig_templates import (
BREPALGOAPI_HEADER,
BREPTOOLS_WRITE_READ_FROM_STRING,
BREPTOOLS_WRITE_READ_FROM_STRING_PYI,
BVH_HEADER_TEMPLATE,
BYREF_ENUM_TEMPLATE,
GETSTATE_TEMPLATE,
GRAPHIC3D_DEFINE_HEADER,
HARRAY1_TEMPLATE,
HARRAY1_TEMPLATE_PYI,
HARRAY2_TEMPLATE,
HARRAY2_TEMPLATE_PYI,
HASH_TOPODS_SHAPE_TEMPLATE,
HSEQUENCE_TEMPLATE,
HSEQUENCE_TEMPLATE_PYI,
LICENSE_HEADER,
MATH_HEADER_TEMPLATE,
NCOLLECTION_ARRAY1_EXTEND_TEMPLATE_PYI,
NCOLLECTION_DATAMAP_EXTEND_TEMPLATE,
NCOLLECTION_HEADER_TEMPLATE,
NCOLLECTION_LIST_EXTEND_TEMPLATE,
NCOLLECTION_LIST_EXTEND_TEMPLATE_PYI,
NCOLLECTION_SEQUENCE_EXTEND_TEMPLATE,
NCOLLECTION_SEQUENCE_EXTEND_TEMPLATE_PYI,
NUMPY_INIT_TEMPLATE,
PRS3D_HEADER_TEMPLATE,
SETSTATE_TEMPLATE,
SHAPE_ANALYSIS_FREE_BOUNDS_TEMPLATE,
SHAPE_ANALYSIS_FREE_BOUNDS_TEMPLATE_PYI,
STANDARD_TRANSIENT_OPERATORS_TEMPLATE,
TEMPLATE_DUMPJSON,
TEMPLATE_DUMPJSON_PYI,
TEMPLATE_GETTER_PYI,
TEMPLATE_GETTER_SETTER,
TEMPLATE_INITFROMJSON,
TEMPLATE_INITFROMJSON_PYI,
TEMPLATE_SETTER_PYI,
TEMPLATE__EQ__,
TEMPLATE__IADD__,
TEMPLATE__IMUL__,
TEMPLATE__ISUB__,
TEMPLATE__ITRUEDIV__,
TEMPLATE__NE__,
TIMESTAMP_TEMPLATE,
TOPODS_CLASS,
TOPODS_CLASS_PYI,
TOPODS_SHAPE_PICKLE_TEMPLATE,
WIN_PRAGMAS,
)
##############################################
# Load configuration file and setup settings #
##############################################
# the PYTHONOCC_GENERATOR_CONFIG environment variable, if set, overrides the
# wrapper_generator.conf next to this script (used by the CI)
DEFAULT_CONFIG_PATH = os.environ.get("PYTHONOCC_GENERATOR_CONFIG") or os.path.join(
os.path.dirname(os.path.abspath(__file__)), "wrapper_generator.conf"
)
def load_config(config_path):
"""Read wrapper_generator.conf and set the module-level path settings.
Called once at import time with the default path (so the helper functions
are usable from the unit tests), then again from main() if --config is
given. No filesystem check is done here, see check_paths().
"""
global PYTHONOCC_VERSION, OCCT_INCLUDE_DIR, PYTHONOCC_CORE_PATH
global COMMON_OUTPUT_PATH, SWIG_OUTPUT_PATH, HEADERS_OUTPUT_PATH
config = configparser.ConfigParser()
if not config.read(config_path, encoding="utf8"):
raise FileNotFoundError(f"Configuration file {config_path} not found.")
# pythonocc version
PYTHONOCC_VERSION = config.get("pythonocc-core", "version")
# oce headers location
OCCT_INCLUDE_DIR = config.get("OCCT", "include_dir")
# swig output path
PYTHONOCC_CORE_PATH = config.get("pythonocc-core", "path")
swig_files_path = os.path.join(PYTHONOCC_CORE_PATH, "src", "SWIG_files")
COMMON_OUTPUT_PATH = os.path.join(swig_files_path, "common")
SWIG_OUTPUT_PATH = os.path.join(swig_files_path, "wrapper")
HEADERS_OUTPUT_PATH = os.path.join(swig_files_path, "headers")
def check_paths():
"""Fail early if the OCCT headers are missing, and create the output
directories the generator writes into."""
if not os.path.isdir(OCCT_INCLUDE_DIR):
raise FileNotFoundError(f"OCCT include dir {OCCT_INCLUDE_DIR} not found.")
for output_path in (SWIG_OUTPUT_PATH, HEADERS_OUTPUT_PATH, COMMON_OUTPUT_PATH):
os.makedirs(output_path, exist_ok=True)
load_config(DEFAULT_CONFIG_PATH)
GENERATE_SWIG_FILES = (
True # if set to False, skip .i generator, to avoid recompile everything
)
def setup_logging():
"""Log both to stdout and to ${SWIG_OUTPUT_PATH}/generator.log, which is
emptied at each run. Must be called after check_paths()."""
log_formatter = logging.Formatter("[%(levelname)-5.5s] %(message)s")
log = logging.getLogger()
log.setLevel(logging.INFO)
file_handler = logging.FileHandler(
os.path.join(SWIG_OUTPUT_PATH, "generator.log"), mode="w", encoding="utf8"
)
file_handler.setFormatter(log_formatter)
log.addHandler(file_handler)
console_handler = logging.StreamHandler(sys.stdout)
console_handler.setFormatter(log_formatter)
log.addHandler(console_handler)
####################
# Global variables #
####################
DOC_URL = "https://dev.opencascade.org/doc/occt-7.9.0/refman/html"
class GeneratorState:
"""Mutable state shared across the generation pipeline.
Replaces a dozen module-level globals so the dependencies between passes
are explicit. A single instance, ``state`` below, is the only authority.
"""
def __init__(self):
# set by ModuleWrapper.__init__ for the module currently being wrapped
self.current_module = None
# python modules the current module imports (transitive deps).
# Reassigned per module; check_dependency() and process_typedefs()
# append to it.
self.python_module_dependency = []
# Like above but for additional headers; reset at every module via
# reset_header_depency().
self.header_dependency = []
# occt-800: built lazily by scan_typedef_aliases() — a mapping from
# canonical template form (e.g. "NCollection_HArray1<gp_Pnt2d>") to
# the typedef alias (e.g. "TColgp_HArray1OfPnt2d") that pythonocc
# actually wraps. Many OCCT 8.0 headers replaced typedef names with
# the canonical template form in their function signatures, but the
# typedef alias is what carries the SWIG type tag, so we have to
# rewrite back.
self.harray_typedef_rewrites = []
# All enums seen so far; populated by process_enums().
self.all_enums = []
# Enums passed/returned by reference; need a SWIG-specific template.
self.all_byref_enums = []
# HArray1/HArray2/HSequence registries: name -> base type. Populated
# both from DEFINE_HARRAY{1,2}/DEFINE_HSEQUENCE macros and from
# `typedef NCollection_HArrayN<X> Y;` aliases scanned upfront.
self.all_harray1 = {}
self.all_harray2 = {}
self.all_hsequence = {}
# Classes that need %wrap_handle / %make_alias.
self.all_standard_handles = []
self.all_standard_transients = ["Standard_Transient"]
# since SWIG 4.1.1, static functions can no longer be called as free
# functions; we emit deprecation shims for the old name.
self.deprecated_static_functions = []
# statistics
self.nb_total_classes = 0
self.nb_total_methods = 0
state = GeneratorState()
def get_log_header():
"""returns a header to be appended to the SWIG file
Useful for development
"""
os_name = f"{platform.system()} {platform.architecture()[0]} {platform.release()}"
# the generator may be run from a tarball, without git available
try:
generator_git_revision = (
subprocess.check_output(
["git", "rev-parse", "--short", "HEAD"],
cwd=os.path.dirname(os.path.abspath(__file__)),
stderr=subprocess.DEVNULL,
)
.strip()
.decode("utf8")
)
except (OSError, subprocess.CalledProcessError):
generator_git_revision = "unknown"
# find the OCC VERSION targeted by the wrapper
# the OCCT version is available from the Standard_Version.hxx header
# e.g. define OCC_VERSION_COMPLETE "7.4.0"
standard_version_header = os.path.join(OCCT_INCLUDE_DIR, "Standard_Version.hxx")
occ_version = "unknown"
if os.path.isfile(standard_version_header):
with open(standard_version_header, "r", encoding="utf8") as f:
file_lines = f.readlines()
for file_line in file_lines:
if file_line.startswith("#define OCC_VERSION_COMPLETE"):
occ_version = file_line.split('"')[1].strip()
return TIMESTAMP_TEMPLATE.substitute(
{
"GITREVISION": generator_git_revision,
"OS": os_name,
"OCCTVERSION": occ_version,
"DATE": f"{datetime.datetime.now()}",
}
)
def get_log_footer(elapsed_seconds):
return """
#################################################
SWIG interface file generation completed in {:.2f}s
#################################################
""".format(
elapsed_seconds
)
def reset_header_depency():
state.header_dependency = ["TColgp", "TColStd", "TCollection", "Storage"]
def check_is_persistent(class_name):
"""
Checks, whether a class belongs to the persistent classes (and not to the transient ones)
"""
return any(
class_name.startswith(occ_module)
for occ_module in [
"PFunction",
"PDataStd",
"PPrsStd",
"PDF",
"PDocStd",
"PDataXtd",
"PNaming",
"PCDM_Document",
]
)
def filter_header_list(header_list, exclusion_list):
"""From a header list, remove hxx to HXX_TO_EXCLUDE
The files to be excluded are specified in the exclusion list
"""
for header_to_remove in exclusion_list:
if os.path.join(OCCT_INCLUDE_DIR, header_to_remove) in header_list:
header_list.remove(os.path.join(OCCT_INCLUDE_DIR, header_to_remove))
# remove platform dependent files
# this is done to have the same SWIG files on every platform:
# wnt specific (WNT_*, OSD_WNT), linux (X11, XWD) and osx (Cocoa).
# Match on the basename only, and case-sensitively, so that neither the
# include dir path nor names such as Storage_StreamUnknownTypeError
# ("unknowntype" contains "wnt") are caught by accident.
platform_markers = ("WNT", "X11", "XWD", "Cocoa")
header_list = [
x
for x in header_list
if not any(marker in os.path.basename(x) for marker in platform_markers)
]
return header_list
def case_sensitive_glob(wildcard):
"""
Case sensitive glob for Windows.
Designed for handling of GEOM and Geom modules
This function makes the difference between GEOM_* and Geom_* under Windows
"""
flist = glob.glob(wildcard)
pattern = wildcard.split("*")[0]
return [file_ for file_ in flist if pattern in file_]
def get_all_module_headers(module_name):
"""Returns a list with all header names"""
mh = case_sensitive_glob(os.path.join(OCCT_INCLUDE_DIR, f"{module_name}.hxx"))
mh += case_sensitive_glob(os.path.join(OCCT_INCLUDE_DIR, f"{module_name}_*.hxx"))
mh = filter_header_list(mh, HXX_TO_EXCLUDE_FROM_BEING_INCLUDED)
return sorted(map(os.path.basename, mh))
def check_has_related_handle(class_name):
"""For a given class :
Check if a header exists.
"""
if check_is_persistent(class_name):
return False
filename = os.path.join(OCCT_INCLUDE_DIR, f"Handle_{class_name}.hxx")
other_possible_filename = filename
if class_name.startswith("Graphic3d"):
other_possible_filename = os.path.join(
OCCT_INCLUDE_DIR, f"{class_name}_Handle.hxx"
)
return (
os.path.exists(filename)
or os.path.exists(other_possible_filename)
or need_handle(class_name)
)
def need_handle(class_name):
"""Returns True if the current parsed class needs an
Handle to be defined. This is useful when headers define
handles but no header"""
# @TODO what about DEFINE_RTTI ?
return (
class_name in state.all_standard_handles
or class_name in state.all_standard_transients
)
_DEFINE_STANDARD_HANDLE_RE = re.compile(
r"DEFINE_STANDARD_HANDLE[\s]*\([\w\s]+,+[\w\s]+\)"
)
# occt-800: many classes that derive from Standard_Transient no longer carry
# a DEFINE_STANDARD_HANDLE; they only have DEFINE_STANDARD_RTTI{,_INLINE,EXT}
# (C, Parent). Treat that as an implicit handle declaration so
# check_has_related_handle picks them up. DEFINE_DERIVED_ATTRIBUTE marks XCAF
# shape tools and similar classes as TDataStd_GenericEmpty subtypes.
_DEFINE_RTTI_RE = re.compile(
r"DEFINE_(?:STANDARD_RTTI(?:_INLINE|EXT)?|DERIVED_ATTRIBUTE)\s*"
r"\(\s*([\w]+)\s*,\s*[\w:]+\s*\)"
)
_DEFINE_HARRAY1_RE = re.compile(r"DEFINE_HARRAY1[\s]*\([\w\s]+,+[\w\s]+\)")
_DEFINE_HARRAY2_RE = re.compile(r"DEFINE_HARRAY2[\s]*\([\w\s]+,+[\w\s]+\)")
_DEFINE_HSEQUENCE_RE = re.compile(r"DEFINE_HSEQUENCE[\s]*\([\w\s]+,+[\w\s]+\)")
# Strip Standard_DEPRECATED("...") and Standard_DEPRECATED_STD("...") entirely
# (with the parens), so they disappear from the source rather than leaving a
# dangling //comment. OCCT 8.0 places these attributes mid-declaration (e.g.
# inside `using ... = X;`) which would otherwise produce malformed C++.
_STANDARD_DEPRECATED_RE = re.compile(
r"Standard_DEPRECATED(?:_STD|_WARNING)?\s*\(\s*"
r'(?:".*?(?:\\"|[^"])*?"(?:\s*".*?(?:\\"|[^"])*?")*)\s*\)'
)
_USING_ALIAS_RE = re.compile(r"\busing\s+([A-Za-z_]\w*)\s*=\s*([^;]+);")
_HANDLE_PARENS_RE = re.compile(r"Handle[\s]*\([\w\s]*\)")
def _collect_handle_macros(header_content):
"""Populate state.all_standard_handles with names declared via DEFINE_STANDARD_HANDLE
or one of the DEFINE_STANDARD_RTTI* / DEFINE_DERIVED_ATTRIBUTE macros."""
for match in _DEFINE_STANDARD_HANDLE_RE.findall(header_content):
state.all_standard_handles.append(match.split("(")[1].split(",")[0])
for match in _DEFINE_RTTI_RE.findall(header_content):
if match not in state.all_standard_handles:
state.all_standard_handles.append(match)
def _collect_harray_macros(header_content):
"""Populate state.all_harray1/2 and state.all_hsequence from DEFINE_HARRAY{1,2} /
DEFINE_HSEQUENCE macros."""
for regex, store, label in (
(_DEFINE_HARRAY1_RE, state.all_harray1, "HARRAY1"),
(_DEFINE_HARRAY2_RE, state.all_harray2, "HARRAY2"),
(_DEFINE_HSEQUENCE_RE, state.all_hsequence, "HSEQUENCE"),
):
for match in regex.findall(header_content):
typename = match.split("(")[1].split(",")[0]
base_typename = match.split(",")[1].split(")")[0]
logging.info("Found %s definition %s:%s", label, typename, base_typename)
store[typename] = base_typename.strip()
def _comment_out_macros(header_content):
"""Replace macros that confuse CppHeaderParser by their //commented form."""
for token in (
"DEFINE_STANDARD_HANDLE",
"DEFINE_STANDARD_RTTIEXT",
"DEFINE_STANDARD_RTTI_INLINE",
"NCOLLECTION_HSEQUENCE",
):
header_content = header_content.replace(token, f"//{token}")
# Standard_DEPRECATED("...") form: drop the whole macro+parens first, then
# //comment any bare leftover identifier. Order matters.
header_content = _STANDARD_DEPRECATED_RE.sub("", header_content)
for token in (
"Standard_DEPRECATED",
"DECLARE_TOBJOCAF_PERSISTENCE",
"DEFINE_DERIVED_ATTRIBUTE",
):
header_content = header_content.replace(token, f"//{token}")
return header_content
def _strip_macros(header_content):
"""Drop attribute macros that prevent CppHeaderParser from working."""
for token in ("DEFINE_STANDARD_ALLOC", "Standard_EXPORT", "Standard_NODISCARD"):
header_content = header_content.replace(token, "")
return header_content
def _rewrite_handle_parens(header_content):
"""Rewrite legacy `Handle(X)` syntax to `opencascade::handle<X>`."""
for match in _HANDLE_PARENS_RE.findall(header_content):
# matches are of the form ['Handle(Graphic3d_Structure)',
# 'Handle(Graphic3d_DataStructureManager)']
normalized = match.replace(" ", "")
class_name = normalized.split("Handle(")[1].split(")")[0]
if class_name == "" or not class_name[0].isupper():
continue
header_content = header_content.replace(
normalized, f"opencascade::handle<{class_name}>"
)
return header_content
def _convert_using_to_typedef(header_content):
"""occt-800: rewrite simple C++11 `using X = Y;` aliases into classic
`typedef Y X;` so the typedef pipeline picks them up. Many OCCT 8.0
headers (GCE2d_MakeEllipse, ...) became `using` aliases for renamed
classes. Skip template aliases (RHS contains '<'): they would produce SWIG
%template instantiations against templates we don't expose."""
def _replace(match):
rhs = match.group(2).strip()
if "<" in rhs:
return match.group(0)
return f"typedef {rhs} {match.group(1)};"
return _USING_ALIAS_RE.sub(_replace, header_content)
def adapt_header_file(header_content):
"""Pre-process an OCCT header so CppHeaderParser can parse it.
- Skips OCCT 8.0 deprecated alias headers entirely.
- Collects DEFINE_STANDARD_HANDLE / DEFINE_STANDARD_RTTI* / DEFINE_HARRAY*
/ DEFINE_HSEQUENCE declarations into the corresponding global registries.
- Strips or //comments out macros that the parser cannot handle.
- Normalizes `occ::handle` and `Handle(X)` to `opencascade::handle<X>`.
- Rewrites simple `using X = Y;` aliases to `typedef Y X;`.
"""
if ("Deprecated alias to moved class" in header_content) or (
"Alias to moved class" in header_content
):
return ""
_collect_handle_macros(header_content)
_collect_harray_macros(header_content)
header_content = _comment_out_macros(header_content)
header_content = _strip_macros(header_content)
# occ::handle must be normalized before using-to-typedef (a `using X =
# occ::handle<Z>;` is template-aliased and intentionally skipped by the
# rewrite); Handle(X) rewriting comes last because it introduces template
# syntax that earlier passes don't expect.
header_content = header_content.replace("occ::handle", "opencascade::handle")
header_content = _convert_using_to_typedef(header_content)
header_content = _rewrite_handle_parens(header_content)
return header_content
def parse_header(header_filename):
"""Use CppHeaderParser module to parse header_filename"""
with open(header_filename, "r", encoding="utf-8") as header_content:
adapted_header_content = adapt_header_file(header_content.read())
try:
cpp_header = CppHeaderParser.CppHeader(adapted_header_content, "string")
except CppHeaderParser.CppParseError as e:
error_message = f"Error: cannot parse {header_filename}\n"
error_message += f"Reason: {e}"
raise RuntimeError(error_message) from e
return cpp_header
def filter_typedefs(typedef_dict):
"""Remove some strange thing that generated SWIG
errors
"""
if "{" in typedef_dict:
del typedef_dict["{"]
if ":" in typedef_dict:
del typedef_dict[":"]
for key in list(typedef_dict):
if key in TYPEDEF_TO_EXCLUDE:
del typedef_dict[key]
continue
# remove typedefs that ends with function callbacks
if key.endswith("Function"):
logging.info("Skip typedef %s because ends with 'Function'", key)
del typedef_dict[key]
continue
# remove typedefs tha ends with _fp (means function pointer?)
if key.endswith("_fp"):
logging.info("Skip typedef %s because ends with '_fp'", key)
del typedef_dict[key]
continue
# remove typedefs tha ends with Func (function pointer)
if key.endswith("Func"):
logging.info("Skip typedef %s because ends with 'Func'", key)
del typedef_dict[key]
continue
# occt-800: skip pointer typedefs (e.g. typedef NCollection_List<X>* Plos)
# SWIG cannot generate %template(...) Foo<X>*; with a pointer
if typedef_dict[key].rstrip().endswith("*"):
logging.info("Skip typedef %s because target is a pointer type", key)
del typedef_dict[key]
for key in list(typedef_dict):
typedef_dict[key] = typedef_dict[key].replace(" ::", "::")
typedef_dict[key] = typedef_dict[key].replace(" , ", ", ")
return typedef_dict
def get_type_for_ncollection_array(ncollection_array: str) -> str:
"""input : NCollection_Array1<Standard_Real>
output : Standard_Real
"""
return ncollection_array.split("<")[1].split(">")[0].strip()
def process_templates_from_typedefs(list_of_typedefs):
""" """
wrapper_str = "/* templates */\n"
pyi_str = ""
for t in list_of_typedefs:
template_name = t[1].replace(" ", "")
template_type = t[0]
if "unsigned" not in template_type and "const" not in template_type:
template_type = template_type.replace(" ", "")
# we must include
if not (
template_type.endswith("::Iterator") or template_type.endswith("::Type")
): # it's not an iterator
wrap_template = all(
forbidden_template not in template_type
for forbidden_template in TEMPLATES_TO_EXCLUDE
)
if template_name in TEMPLATES_TO_EXCLUDE:
continue
# sometimes the template name is weird (parenthesis, comma etc.)
# don't consider this
if "_" not in template_name:
wrap_template = False
# del typedef_dict[key]
if wrap_template:
# wrapper_str += f"%template({template_name}) {template_type};\n"
# if a NCollection_Array1, extend this template to benefit from pythonic methods
# All "Array1" classes are considered as python arrays
# TODO : it should be a good thing to use decorators here, to avoid code duplication
basetype_hint = adapt_type_for_hint(
get_type_for_ncollection_array(template_type)
)
if "NCollection_Array1" in template_type:
# in this cas, we use the Array1ExtendIter(T) macro by default
# if the NCollection_Array1 involves Standard_Integer or Standard_Real
# then the NCollection_Array1 can be wrapped as a numpy array and the
# macro Array1NumpyTemplate is used.
base_type = template_type[:-1].split("NCollection_Array1<")[1]
# occt-800 typedefs use plain `double`/`int`/`float` instead
# of the Standard_* aliases; treat both forms identically
if base_type in ("Standard_ShortReal", "float"):
wrapper_str += "%apply (float* IN_ARRAY1, int DIM1) { (float* numpyArray1, int nRows1) };\n"
wrapper_str += "%apply (float* ARGOUT_ARRAY1, int DIM1) { (float* numpyArray1Argout, int nRows1Argout) };\n"
wrapper_str += f"Array1NumpyTemplate({template_name}, float, {base_type})\n"
elif base_type in ("Standard_Real", "double"):
wrapper_str += "%apply (double* IN_ARRAY1, int DIM1) { (double* numpyArray1, int nRows1) };\n"
wrapper_str += "%apply (double* ARGOUT_ARRAY1, int DIM1) { (double* numpyArray1Argout, int nRows1Argout) };\n"
wrapper_str += f"Array1NumpyTemplate({template_name}, double, {base_type})\n"
elif base_type in ("Standard_Integer", "int"):
wrapper_str += "%apply (long long* IN_ARRAY1, int DIM1) { (long long* numpyArray1, int nRows1) };\n"
wrapper_str += "%apply (long long* ARGOUT_ARRAY1, int DIM1) { (long long* numpyArray1Argout, int nRows1Argout) };\n"
wrapper_str += f"Array1NumpyTemplate({template_name}, long long, {base_type})\n"
elif base_type == "Poly_Triangle":
wrapper_str += "%apply (long long* IN_ARRAY2, int DIM1, int DIM2) { (long long* numpyArray2, int nRows2, int nDims2) };\n"
wrapper_str += "%apply (long long* ARGOUT_ARRAY1, int DIM1) { (long long* numpyArray2Argout, int aSizeArgout) };\n"
wrapper_str += f"Array1OfTriaNumpyTemplate({template_name}, Poly_Triangle)\n\n"
# 2D elements, i.e. that provides X() and Y() methods
elif base_type in ["gp_XY", "gp_Vec2d", "gp_Pnt2d", "gp_Dir2d"]:
wrapper_str += "%apply (double* IN_ARRAY2, int DIM1, int DIM2) { (double* numpyArray2, int nRows2, int nDims2) };\n"
wrapper_str += "%apply (double* ARGOUT_ARRAY1, int DIM1) { (double* numpyArray2Argout, int aSizeArgout) };\n"
wrapper_str += (
f"Array1Of2DNumpyTemplate({template_name}, {base_type})\n"
)
# 3D elements, i.e. that provides X(), Y() and Z() methods
elif base_type in ["gp_XYZ", "gp_Vec", "gp_Pnt", "gp_Dir"]:
wrapper_str += "%apply (double* IN_ARRAY2, int DIM1, int DIM2) { (double* numpyArray2, int nRows2, int nDims2) };\n"
wrapper_str += "%apply (double* ARGOUT_ARRAY1, int DIM1) { (double* numpyArray2Argout, int aSizeArgout) };\n"
wrapper_str += (
f"Array1Of3DNumpyTemplate({template_name}, {base_type})\n"
)
else: # no numpy support
wrapper_str += f"%template({template_name}) {template_type};\n"
wrapper_str += f"Array1ExtendIter({base_type})\n\n"
pyi_str += NCOLLECTION_ARRAY1_EXTEND_TEMPLATE_PYI.substitute(
{
"NCollection_Array1_Template_Instanciation": template_name,
"Type_T": f"{basetype_hint}",
}
)
elif "NCollection_Array2" in template_type:
# same than NCollection_Array1
base_type = template_type.split("NCollection_Array2<")[1].split(
">"
)[0]
# occt-800: typedefs use plain `double`/`int`/`float`
if base_type in ("Standard_ShortReal", "float"):
wrapper_str += "%apply (float* IN_ARRAY2, int DIM1, int DIM2) { (float* numpyArray2, int nRows2, int nCols2) };\n"
wrapper_str += "%apply (float* ARGOUT_ARRAY1, int DIM1) { (float* numpyArray2Argout, int aSizeArgout) };\n"
wrapper_str += f"Array2NumpyTemplate({template_name}, float, {base_type})\n"
elif base_type in ("Standard_Real", "double"):
wrapper_str += "%apply (double* IN_ARRAY2, int DIM1, int DIM2) { (double* numpyArray2, int nRows2, int nCols2) };\n"
wrapper_str += "%apply (double* ARGOUT_ARRAY1, int DIM1) { (double* numpyArray2Argout, int aSizeArgout) };\n"
wrapper_str += f"Array2NumpyTemplate({template_name}, double, {base_type})\n"
elif base_type in ("Standard_Integer", "int"):
wrapper_str += "%apply (long long* IN_ARRAY2, int DIM1, int DIM2) { (long long* numpyArray2, int nRows2, int nCols2) };\n"
wrapper_str += "%apply (long long* ARGOUT_ARRAY1, int DIM1) { (long long* numpyArray2Argout, int aSizeArgout) };\n"
wrapper_str += f"Array2NumpyTemplate({template_name}, long long, {base_type})\n"
# 2D elements
elif base_type in ["gp_XY", "gp_Vec2d", "gp_Pnt2d", "gp_Dir2d"]:
wrapper_str += "%apply (double* IN_ARRAY3, int DIM1, int DIM2, int DIM3) { (double* numpyArray3, int nRows3, int nCols3, int nDims3) };\n"
wrapper_str += "%apply (double* ARGOUT_ARRAY1, int DIM1) { (double* numpyArray3Argout, int aSizeArgout) };\n"
wrapper_str += (
f"Array2Of2DNumpyTemplate({template_name}, {base_type})\n"
)
# 3D elements
elif base_type in ["gp_XYZ", "gp_Vec", "gp_Pnt", "gp_Dir"]:
wrapper_str += "%apply (double* IN_ARRAY3, int DIM1, int DIM2, int DIM3) { (double* numpyArray3, int nRows3, int nCols3, int nDims3) };\n"
wrapper_str += "%apply (double* ARGOUT_ARRAY1, int DIM1) { (double* numpyArray3Argout, int aSizeArgout) };\n"
wrapper_str += (
f"Array2Of3DNumpyTemplate({template_name}, {base_type})\n"
)
else:
wrapper_str += f"%template({template_name}) {template_type};\n"
elif "NCollection_List" in template_type:
wrapper_str += f"%template({template_name}) {template_type};\n"
# derive the matching ListIterator typedef name from the
# list typedef (TopTools_ListOfShape -> TopTools_ListIteratorOfListOfShape)
list_iter_name = template_name.replace(
"ListOf", "ListIteratorOfListOf", 1
)
wrapper_str += NCOLLECTION_LIST_EXTEND_TEMPLATE.substitute(
{
"NCollection_List_Template_Instanciation": template_type,
"NCollection_ListIterator_Name": list_iter_name,
}
)
pyi_str += NCOLLECTION_LIST_EXTEND_TEMPLATE_PYI.substitute(
{
"NCollection_List_Template_Instanciation": template_name,
"Type_T": f"{basetype_hint}",
}
)
elif "NCollection_Sequence" in template_type:
wrapper_str += f"%template({template_name}) {template_type};\n"
wrapper_str += NCOLLECTION_SEQUENCE_EXTEND_TEMPLATE.substitute(
{"NCollection_Sequence_Template_Instanciation": template_type}
)
pyi_str += NCOLLECTION_SEQUENCE_EXTEND_TEMPLATE_PYI.substitute(
{
"NCollection_Sequence_Template_Instanciation": template_name,
"Type_T": f"{basetype_hint}",
}
)
elif "NCollection_DataMap" in template_type:
# NCollection_Datamap is similar to a Python dict,
# it's a (key, value) store. Defined as
# template < class TheKeyType,
# class TheItemType,
# class Hasher = NCollection_DefaultHasher<TheKeyType> >
# some occt methods return such an object, but the iterator can't be accessed
# through Python. Se we extend this class with a Keys() method that iterates over
# NCollection_DataMap keys and returns a Python list of key objects.
# Note : works for standard_Integer keys only so far
# occt-800: ignore Items()/KeyValues() returning ItemsView<...>
# which is non-default-constructible and cannot be wrapped by SWIG
wrapper_str += f"%ignore {template_type}::Items;\n"
wrapper_str += f"%ignore {template_type}::KeyValues;\n"
wrapper_str += f"%template({template_name}) {template_type};\n"
if "<Standard_Integer" in template_type or "<int" in template_type:
wrapper_str += NCOLLECTION_DATAMAP_EXTEND_TEMPLATE.substitute(
{
"NCollection_DataMap_Template_Instanciation": template_type,
"NCollection_DataMap_Template_Name": template_name,
}
)
elif (
"NCollection_IndexedMap" in template_type
or "NCollection_IndexedDataMap" in template_type
):
# occt-800: NCollection_IndexedMap/IndexedDataMap expose
# IndexedItems()/Items()/KeyValues() returning a non-default-
# constructible View<...>. SWIG cannot wrap them.
wrapper_str += f"%ignore {template_type}::Items;\n"
wrapper_str += f"%ignore {template_type}::KeyValues;\n"
wrapper_str += f"%ignore {template_type}::IndexedItems;\n"
# occt-800rc5 bug: Contained() references a non-existent
# IndexedDataMapNode::Key field (should be Key1)
wrapper_str += f"%ignore {template_type}::Contained;\n"
wrapper_str += f"%template({template_name}) {template_type};\n"
elif (
template_type.startswith("NCollection_HArray1<")
or template_type.startswith("NCollection_HArray2<")
or template_type.startswith("NCollection_HSequence<")
):
# occt-800: NCollection_HArray1/HArray2/HSequence are now
# plain template classes deriving from Standard_Transient.
# Register the typedef -> ALL_HARRAY{1,2}/HSEQUENCE so that
# process_handles emits %wrap_handle and process_harrayN
# emits the fake class definition + %make_alias (the same
# path used in OCCT 7.9 with the DEFINE_HARRAY1 macro).
inner = template_type.split("<", 1)[1].rsplit(">", 1)[0].strip()
if template_type.startswith("NCollection_HArray1<"):
state.all_harray1[template_name] = (
f"NCollection_Array1<{inner}>"
)
elif template_type.startswith("NCollection_HArray2<"):
state.all_harray2[template_name] = (
f"NCollection_Array2<{inner}>"
)
else:
state.all_hsequence[template_name] = (
f"NCollection_Sequence<{inner}>"
)
else:
wrapper_str += f"%template({template_name}) {template_type};\n"
elif (
template_name.endswith("Iter") or "_ListIteratorOf" in template_name
): # it's a lst iterator, we use another way to wrap the template
# #%template(TopTools_ListIteratorOfListOfShape) NCollection_TListIterator<TopTools_ListOfShape>;
if "IteratorOf" in template_name:
if "::handle" not in template_type:
typ = (template_type.split("<")[1]).split(">")[0]
else:
h_typ = (template_type.split("<")[2]).split(">")[0]
typ = f"opencascade::handle<{h_typ}>"
else: # template_name.endswith("Iter") — guaranteed by the elif above
typ = template_name.split("Iter")[0]
wrapper_str += (
f"%template({template_name}) NCollection_TListIterator<{typ}>;\n"
)
wrapper_str += "/* end templates declaration */\n"
return wrapper_str, pyi_str
def adapt_type_for_hint_typedef(typedef_type_str):
typedef_type_str = typedef_type_str.replace(" *", "")
typedef_type_str = typedef_type_str.replace("&OutValue", "")
typedef_type_str = typedef_type_str.replace("class", "")
if "char" in typedef_type_str or "Char" in typedef_type_str:
typedef_type_str = "str"
if (
"_int" in typedef_type_str
or " int" in typedef_type_str
or " long" in typedef_type_str
):
typedef_type_str = "int"
if "double" in typedef_type_str:
typedef_type_str = "float"
if (
"void" in typedef_type_str
or "VOID" in typedef_type_str
and "avoid" not in typedef_type_str
):
typedef_type_str = "None"
if "GUID" in typedef_type_str:
typedef_type_str = "str"
if "size_t" in typedef_type_str:
typedef_type_str = "int"
if "struct" in typedef_type_str:
typedef_type_str = "int"
return typedef_type_str
def str_in(list_of_patterns, a_string):
"""a utility function that returns True if any of the item
of the list patterns is in the a_string"""
return any(patt in a_string for patt in list_of_patterns)
def process_typedefs(typedefs_dict):
"""Take a typedef dictionary and returns a SWIG definition string"""
templates_str = ""
typedef_pyi_str = "" # NewTypes related to typedef aliases
# pythoncode for typedef aliases, to be inserted at the end of the swig interface file
typedef_aliases_str = "/* class aliases */\n%pythoncode {\n"
typedef_str = "/* typedefs */\n"
templates = []
# careful, there might be some strange things returned by CppHeaderParser
# they should not be taken into account
filtered_typedef_dict = filter_typedefs(typedefs_dict)
# we check if there is any type def type that relies on an opencascade::handle
# if this is the case, we must add the corresponding python module
# as a dependency otherwise it leads to a runtime issue
for template_type in filtered_typedef_dict.values():
if "opencascade::handle" in template_type: # we must add a PYTHON DEPENDENCY
if template_type.count("<") == 2:
h_typ = (template_type.split("<")[2]).split(">")[0]
elif template_type.count("<") == 1:
h_typ = (template_type.split("<")[1]).split(">")[0]
else:
logging.warning(
"This template type cannot be handled: %s", template_type
)
continue
module = h_typ.split("_")[0]
if module != state.current_module:
# need to be added to the list of dependent object
if (module not in state.python_module_dependency) and (
is_module(module)
):
state.python_module_dependency.append(module)
sorted_list_of_typedefs = sorted(filtered_typedef_dict.keys())
for typedef_value in sorted_list_of_typedefs:
# some occttype defs are actually templated classes,
# for instance
# typedef NCollection_Array1<Standard_Real> TColStd_Array1OfReal;
# this must be wrapped as a typedef but rather as an instaicated class
# the good way to proceed is:
# %{include "NCollection_Array1.hxx"}
# %template(TColStd_Array1OfReal) NCollection_Array1<Standard_Real>;
# we then check if > or < are in the typedef string then we process it.
typedef_type = filtered_typedef_dict[typedef_value]
typedef_str += f"typedef {typedef_type} {typedef_value};\n"
#
# Check if the typedef is a template
#
if str_in(["<", ">"], f"{typedef_type}"):
templates.append([typedef_type, typedef_value])
#
# Check if it's just a class alias
#
elif not str_in(["*", ":", " ", "Standard"], f"{typedef_type}"):
# we create the alias in python
# e.g.
# BRepOffsetAPI_= BRepAlgoAPI_Cut
# only if the type is a module class (exclude char, Standard_Real etc.)
#
typedef_module_name = typedef_type.split("_")[0]
if is_module(typedef_module_name):
if state.current_module == typedef_module_name:
typedef_aliases_str += f"{typedef_value}={typedef_type}\n"
else:
typedef_aliases_str += f"{typedef_value}=OCC.Core.{typedef_module_name}.{typedef_type}\n"
check_dependency(typedef_type.split()[0])
# Define a new type, only for aliases
type_to_define = typedef_type
match_1 = [
"<",
":",
"struct",
"union",
")",
"NCollection_Array1",
"NCollection_List",
"NCollection_DataMap",
"NCollection_Sequence",
]
if (
all(match not in type_to_define for match in match_1)
and type_to_define is not None
and ")" not in typedef_value
):
type_to_define = adapt_type_for_hint_typedef(type_to_define)
typedef_pyi_str += (
f'\n{typedef_value} = NewType("{typedef_value}", {type_to_define})'
)
elif (
")" not in typedef_value
and "(" not in typedef_value
and ":" not in typedef_value
and "NCollection_Array1" not in type_to_define
and "NCollection_List" not in type_to_define
and "NCollection_DataMap" not in type_to_define
and "NCollection_Sequence" not in type_to_define
):
typedef_pyi_str += "\n# the following typedef cannot be wrapped as is"
typedef_pyi_str += f'\n{typedef_value} = NewType("{typedef_value}", Any)'
typedef_pyi_str += "\n"
typedef_str += "/* end typedefs declaration */\n\n"
# then we process templates
# at this stage, we get a list as follows
templates_def, templates_pyi = process_templates_from_typedefs(templates)
templates_str += templates_def
templates_str += "\n"
# close aliases
typedef_aliases_str += "}\n"
return (
templates_str + typedef_str,
typedef_pyi_str + templates_pyi,
typedef_aliases_str,
)
def adapt_enum_value(enum_value):
"""Take for example Graphic3d_TextureSetBits.hxx
//! Standard texture units combination bits.
enum Graphic3d_TextureSetBits
{
Graphic3d_TextureSetBits_NONE = 0,
Graphic3d_TextureSetBits_BaseColor = (unsigned int )(1 << int(Graphic3d_TextureUnit_BaseColor)),
Graphic3d_TextureSetBits_Emissive = (unsigned int )(1 << int(Graphic3d_TextureUnit_Emissive)),
Graphic3d_TextureSetBits_Occlusion = (unsigned int )(1 << int(Graphic3d_TextureUnit_Occlusion)),
Graphic3d_TextureSetBits_Normal = (unsigned int )(1 << int(Graphic3d_TextureUnit_Normal)),
Graphic3d_TextureSetBits_MetallicRoughness = (unsigned int )(1 << int(Graphic3d_TextureUnit_MetallicRoughness)),
};
The values (unsigned int )(1 << int(Graphic3d_TextureUnit_BaseColor)) cannot be processed as is by SWIG.
We transform them to Graphic3d_TextureUnit_BaseColor
"""
if isinstance(enum_value, int) or "int (" not in enum_value:
return enum_value
return enum_value.split("int ( ")[1].split(")")[0].strip()
def process_enums(enums_list):
"""Take an enum list and generate a compliant SWIG string
Then create a python class that mimics the enum
for instance, from the TopAbs_Orientation.hxx header, we have
enum TopAbs_Orientation
{
TopAbs_FORWARD,
TopAbs_REVERSED,
TopAbs_INTERNAL,
TopAbs_EXTERNAL
};
In SWIG, this will be wrapped in the interface file as
enum TopAbs_Orientation {
TopAbs_FORWARD = 0,
TopAbs_REVERSED = 1,
TopAbs_INTERNAL = 2,
TopAbs_EXTERNAL = 3,
};
However, python does not know anything about TopAbs_Orientation, he only knows TopAbs_FORWARD
So we also create a python class that mimics the enum and let python know about the TopAbs_Orientation type
%pythoncode {
class TopAbs_Orientation:
TopAbs_FORWARD = 0
TopAbs_REVERSED = 1
TopAbs_INTERNAL = 2
TopAbs_EXTERNAL = 3
}