33import importlib
44import itertools
55import logging
6+ import py_compile
67import re
78import shutil
89import sys
2021
2122logger = logging .getLogger (__name__ )
2223
24+ def compile_source_file (source_file : FilePath , clean_existing : bool = False ) -> Path :
25+ compiled_file = source_file .with_suffix (".pyc" )
26+ py_compile .compile (source_file , cfile = compiled_file )
27+ if clean_existing :
28+ source_file .unlink (missing_ok = True )
29+ return compiled_file
2330
2431def get_class_name (file_name : Path ) -> str :
25- with open (str (file_name ), 'r' ) as file :
26- data = file .read ()
27- return re .search (r'class (\w+)\(\s*Fmi3Slave\s*\)\s*:' , data ).group (1 )
32+
33+ if file_name .suffix == ".py" :
34+ with open (str (file_name ), 'r' ) as file :
35+ data = file .read ()
36+ return re .search (r'class (\w+)\(\s*Fmi3Slave\s*\)\s*:' , data ).group (1 )
37+
38+ elif file_name .suffix == ".pyc" :
39+ # For .pyc files, use importlib to load the module and inspect its classes
40+ import importlib .util
41+ spec = importlib .util .spec_from_file_location ("module_name" , file_name )
42+ module = importlib .util .module_from_spec (spec )
43+ spec .loader .exec_module (module )
44+ for name , obj in vars (module ).items ():
45+ if isinstance (obj , type ) and issubclass (obj , Fmi3Slave ) and obj is not Fmi3Slave :
46+ return name
2847
2948
3049def get_model_description (filepath : Path , module_name : str ) -> Tuple [str , Element ]:
@@ -86,7 +105,7 @@ def build_FMU(
86105 raise ValueError (
87106 f"The documentation folder does not exists { documentation_folder !s} "
88107 )
89-
108+
90109 if terminals is not None :
91110 terminals = Path (terminals )
92111 if not terminals .exists ():
@@ -99,11 +118,14 @@ def build_FMU(
99118 )
100119
101120 module_name = script_file .stem
121+ compile_flag = options .get ("compile" , False )
102122
103123 with tempfile .TemporaryDirectory (prefix = "pythonfmu_" ) as tempd :
104124 temp_dir = Path (tempd )
105125 shutil .copy2 (script_file , temp_dir )
106126
127+ if compile_flag :
128+ script_file = compile_source_file (temp_dir / script_file .name , clean_existing = True )
107129 # Embed pythonfmu in the FMU so it does not need to be included
108130 dep_folder = temp_dir / "pythonfmu3"
109131 dep_folder .mkdir ()
@@ -130,6 +152,11 @@ def build_FMU(
130152 else :
131153 shutil .copy2 (file_ , temp_dir )
132154
155+ for f in temp_dir .rglob ("*.py" ):
156+ if f .suffix == ".py" and compile_flag :
157+ compile_source_file (f , clean_existing = True )
158+
159+
133160 model_identifier , xml = get_model_description (
134161 temp_dir .absolute () / script_file .name , module_name
135162 )
@@ -194,7 +221,7 @@ def build_FMU(
194221 terminalsFolder = Path ("terminalsAndIcons" )
195222 relative_f = terminals .relative_to (terminals .parent )
196223 zip_fmu .write (f , arcname = (terminalsFolder / relative_f ))
197-
224+
198225
199226 # Add the model description
200227 xml_str = parseString (tostring (xml , "UTF-8" ))
@@ -240,6 +267,13 @@ def create_command_parser(parser: argparse.ArgumentParser):
240267 default = None
241268 )
242269
270+ parser .add_argument (
271+ "--compile" ,
272+ dest = "compile" ,
273+ help = "If given, the Python script and project files will be compiled to bytecode." ,
274+ action = "store_true"
275+ )
276+
243277 for option in FMI3_MODEL_OPTIONS :
244278 action = "store_false" if option .value else "store_true"
245279 parser .add_argument (
0 commit comments