Skip to content

Commit cf6bc1d

Browse files
Add compile option to compile project files to bytecode ref #22 (#23)
1 parent b0042c2 commit cf6bc1d

File tree

3 files changed

+53
-13
lines changed

3 files changed

+53
-13
lines changed

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ pip install pythonfmu3
3636
3. Run `pythonfmu3 build` to create the fmu.
3737

3838
```
39-
usage: pythonfmu3 build [-h] -f SCRIPT_FILE [-d DEST] [--doc DOCUMENTATION_FOLDER] [--terminals TERMINALS_FILE] [--no-external-tool]
39+
usage: pythonfmu3 build [-h] -f SCRIPT_FILE [-d DEST] [--doc DOCUMENTATION_FOLDER] [--terminals TERMINALS_FILE] [--compile] [--no-external-tool]
4040
[--no-variable-step] [--interpolate-inputs] [--only-one-per-process] [--handle-state]
4141
[--serialize-state] [--use-memory-management]
4242
[Project files [Project files ...]]
@@ -55,6 +55,7 @@ optional arguments:
5555
Documentation folder to include in the FMU.
5656
--terminals TERMINALS_FILE
5757
Terminals file (terminalsAndIcons.xml) to include in the FMU.
58+
--compile If given, the script file and project files will be compiled to byte code.
5859
--no-external-tool If given, needsExecutionTool=false
5960
--no-variable-step If given, canHandleVariableCommunicationStepSize=false
6061
--interpolate-inputs If given, canInterpolateInputs=true

pythonfmu3/builder.py

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import importlib
44
import itertools
55
import logging
6+
import py_compile
67
import re
78
import shutil
89
import sys
@@ -20,11 +21,29 @@
2021

2122
logger = 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

2431
def 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

3049
def 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(

pythonfmu3/tests/test_builder.py

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,16 +22,18 @@
2222
)
2323

2424

25-
def test_zip_content(tmp_path):
26-
25+
@pytest.mark.parametrize("compile", [False, True])
26+
def test_zip_content(tmp_path, compile):
27+
extension = ".pyc" if compile else ".py"
2728
script_name = "pythonslave.py"
2829
script_file = Path(__file__).parent / "slaves" / script_name
29-
fmu = FmuBuilder.build_FMU(script_file, dest=tmp_path)
30+
fmu = FmuBuilder.build_FMU(script_file, dest=tmp_path, compile=compile)
3031
assert fmu.exists()
3132
assert zipfile.is_zipfile(fmu)
3233

3334
with zipfile.ZipFile(fmu) as files:
3435
names = files.namelist()
36+
script_name = script_name.replace(".py", extension)
3537

3638
assert "modelDescription.xml" in names
3739
assert "/".join(("resources", script_name)) in names
@@ -60,8 +62,11 @@ def test_zip_content(tmp_path):
6062

6163
# Check pythonfmu is embedded
6264
pkg_folder = Path(pythonfmu3.__path__[0])
63-
for f in pkg_folder.rglob("*.py"):
65+
for f in pkg_folder.rglob(f"*.py"):
6466
relative_f = f.relative_to(pkg_folder).as_posix()
67+
# change relative f suffix
68+
if relative_f.endswith(".py"):
69+
relative_f = relative_f.replace(".py", extension)
6570
if "test" not in relative_f:
6671
assert "/".join(("resources", "pythonfmu3", relative_f)) in names
6772

@@ -101,7 +106,6 @@ def build():
101106
project_files.add(full_name.parent)
102107

103108
return FmuBuilder.build_FMU(script_file, dest=tmp_path, project_files=project_files)
104-
105109
fmu = build()
106110
with zipfile.ZipFile(fmu) as files:
107111
names = files.namelist()
@@ -119,7 +123,8 @@ def build():
119123

120124

121125
@pytest.mark.parametrize("pfiles", PROJECT_TEST_CASES)
122-
def test_project_files_containing_script(tmp_path, pfiles):
126+
@pytest.mark.parametrize("compile", [False, True])
127+
def test_project_files_containing_script(tmp_path, pfiles, compile):
123128
orig_script_file = Path(__file__).parent / "slaves/pythonslave.py"
124129
pfiles = map(Path, pfiles)
125130

@@ -139,7 +144,7 @@ def build():
139144
full_name.mkdir(parents=True, exist_ok=True)
140145

141146
return FmuBuilder.build_FMU(
142-
script_file, dest=tmp_path, project_files=[script_file.parent]
147+
script_file, dest=tmp_path, project_files=[script_file.parent], compile=compile
143148
)
144149

145150
fmu = build()

0 commit comments

Comments
 (0)