Skip to content

Commit 07eb621

Browse files
syntronadeas31
andauthored
OMCPath (#317)
* [OMCPath] add class * [OMCPath] add implementation using OMC via sendExpression() * [OMCPath] add pytest (only docker at the moment) * [OMCPath] TODO items * [test_OMCPath] mypy fix * [test_OMCPath] fix end of file * [test_OMCPath] define test using OMCSessionZMQ() locally * add TODO - need to check Python versions * not working: 3.10 * working: 3.12 * [test_OMCPath] activate docker based on test_docker * [OMCPath] add more functionality and docstrings * [OMCPath] remove TODO entries * [OMCPath] define limited compatibility for Python < 3.12 * use modified pathlib.Path as OMCPath * [OMCSEssionZMQ] use OMCpath * [OMCSessionZMQ] create a tempdir using omcpath_tempdir() * [OMCPath] fix mypy * [OMCPath] add warning message for Python < 3.12 * [OMCPath] try to make mypy happy ... * [test_OMCPath] only for Python >= 3.12 * [test_OMCPath] update test * [OMCPath._omc_resolve] use sendExpression() with parsed=False * this is scripting output and, thus, it cannot be parsed * [test_OMCPath] cleanup; use the same code for local OMC and docker based OMC * [test_OMCPath] define test for WSL * [test_OMCPath] use omcpath_tempdir() instead of hard-coded tempdir definition * [OMCPath] spelling fix see commit ID: aa74b36 - [OMCPath] add more functionality and docstrings * [OMCPath] implementation version 3 * differentiate between * Python >= 3.12 uses OMCPath based on OMC for filesystem operation * Python < 3.12 uses a pathlib.Path based implementation which is limited to OMCProcessLocal * [OMCSession*] fix flake8 (PyCharm likes the empty lines) * [OMCSessionZMQ] more generic definiton for omcpath_tempdir() * [OMCPathCompatibility] mypy on github ... * [OMCPathCompatibility] improve log messages * [test_OMCPath] update * [OMCPathReal] align exists() to the definition used in pathlib * [test_OMCPath] fix error error: "unlink" of "OMCPathReal" does not return a value (it only ever returns None) [func-returns-value] --------- Co-authored-by: Adeel Asghar <[email protected]>
1 parent db102c3 commit 07eb621

File tree

2 files changed

+309
-0
lines changed

2 files changed

+309
-0
lines changed

OMPython/OMCSession.py

Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,191 @@ def getClassNames(self, className=None, recursive=False, qualified=False, sort=F
271271
return self._ask(question='getClassNames', opt=opt)
272272

273273

274+
class OMCPathReal(pathlib.PurePosixPath):
275+
"""
276+
Implementation of a basic Path object which uses OMC as backend. The connection to OMC is provided via a
277+
OMCSessionZMQ session object.
278+
"""
279+
280+
def __init__(self, *path, session: OMCSessionZMQ) -> None:
281+
super().__init__(*path)
282+
self._session = session
283+
284+
def with_segments(self, *pathsegments):
285+
"""
286+
Create a new OMCPath object with the given path segments.
287+
288+
The original definition of Path is overridden to ensure session is set.
289+
"""
290+
return type(self)(*pathsegments, session=self._session)
291+
292+
def is_file(self, *, follow_symlinks=True) -> bool:
293+
"""
294+
Check if the path is a regular file.
295+
"""
296+
return self._session.sendExpression(f'regularFileExists("{self.as_posix()}")')
297+
298+
def is_dir(self, *, follow_symlinks=True) -> bool:
299+
"""
300+
Check if the path is a directory.
301+
"""
302+
return self._session.sendExpression(f'directoryExists("{self.as_posix()}")')
303+
304+
def read_text(self, encoding=None, errors=None, newline=None) -> str:
305+
"""
306+
Read the content of the file represented by this path as text.
307+
308+
The additional arguments `encoding`, `errors` and `newline` are only defined for compatibility with Path()
309+
definition.
310+
"""
311+
return self._session.sendExpression(f'readFile("{self.as_posix()}")')
312+
313+
def write_text(self, data: str, encoding=None, errors=None, newline=None):
314+
"""
315+
Write text data to the file represented by this path.
316+
317+
The additional arguments `encoding`, `errors`, and `newline` are only defined for compatibility with Path()
318+
definitions.
319+
"""
320+
if not isinstance(data, str):
321+
raise TypeError('data must be str, not %s' %
322+
data.__class__.__name__)
323+
324+
return self._session.sendExpression(f'writeFile("{self.as_posix()}", "{data}", false)')
325+
326+
def mkdir(self, mode=0o777, parents=False, exist_ok=False):
327+
"""
328+
Create a directory at the path represented by this OMCPath object.
329+
330+
The additional arguments `mode`, and `parents` are only defined for compatibility with Path() definitions.
331+
"""
332+
if self.is_dir() and not exist_ok:
333+
raise FileExistsError(f"Directory {self.as_posix()} already exists!")
334+
335+
return self._session.sendExpression(f'mkdir("{self.as_posix()}")')
336+
337+
def cwd(self):
338+
"""
339+
Returns the current working directory as an OMCPath object.
340+
"""
341+
cwd_str = self._session.sendExpression('cd()')
342+
return OMCPath(cwd_str, session=self._session)
343+
344+
def unlink(self, missing_ok: bool = False) -> None:
345+
"""
346+
Unlink (delete) the file or directory represented by this path.
347+
"""
348+
res = self._session.sendExpression(f'deleteFile("{self.as_posix()}")')
349+
if not res and not missing_ok:
350+
raise FileNotFoundError(f"Cannot delete file {self.as_posix()} - it does not exists!")
351+
352+
def resolve(self, strict: bool = False):
353+
"""
354+
Resolve the path to an absolute path. This is done based on available OMC functions.
355+
"""
356+
if strict and not (self.is_file() or self.is_dir()):
357+
raise OMCSessionException(f"Path {self.as_posix()} does not exist!")
358+
359+
if self.is_file():
360+
omcpath = self._omc_resolve(self.parent.as_posix()) / self.name
361+
elif self.is_dir():
362+
omcpath = self._omc_resolve(self.as_posix())
363+
else:
364+
raise OMCSessionException(f"Path {self.as_posix()} is neither a file nor a directory!")
365+
366+
return omcpath
367+
368+
def _omc_resolve(self, pathstr: str):
369+
"""
370+
Internal function to resolve the path of the OMCPath object using OMC functions *WITHOUT* changing the cwd
371+
within OMC.
372+
"""
373+
expression = ('omcpath_cwd := cd(); '
374+
f'omcpath_check := cd("{pathstr}"); ' # check requested pathstring
375+
'cd(omcpath_cwd)')
376+
377+
try:
378+
result = self._session.sendExpression(command=expression, parsed=False)
379+
result_parts = result.split('\n')
380+
pathstr_resolved = result_parts[1]
381+
pathstr_resolved = pathstr_resolved[1:-1] # remove quotes
382+
383+
omcpath_resolved = self._session.omcpath(pathstr_resolved)
384+
except OMCSessionException as ex:
385+
raise OMCSessionException(f"OMCPath resolve failed for {pathstr}!") from ex
386+
387+
if not omcpath_resolved.is_file() and not omcpath_resolved.is_dir():
388+
raise OMCSessionException(f"OMCPath resolve failed for {pathstr} - path does not exist!")
389+
390+
return omcpath_resolved
391+
392+
def absolute(self):
393+
"""
394+
Resolve the path to an absolute path. This is done by calling resolve() as it is the best we can do
395+
using OMC functions.
396+
"""
397+
return self.resolve(strict=True)
398+
399+
def exists(self, follow_symlinks=True) -> bool:
400+
"""
401+
Semi replacement for pathlib.Path.exists().
402+
"""
403+
return self.is_file() or self.is_dir()
404+
405+
def size(self) -> int:
406+
"""
407+
Get the size of the file in bytes - this is an extra function and the best we can do using OMC.
408+
"""
409+
if not self.is_file():
410+
raise OMCSessionException(f"Path {self.as_posix()} is not a file!")
411+
412+
res = self._session.sendExpression(f'stat("{self.as_posix()}")')
413+
if res[0]:
414+
return int(res[1])
415+
416+
raise OMCSessionException(f"Error reading file size for path {self.as_posix()}!")
417+
418+
419+
if sys.version_info < (3, 12):
420+
421+
class OMCPathCompatibility(pathlib.Path):
422+
"""
423+
Compatibility class for OMCPath in Python < 3.12. This allows to run all code which uses OMCPath (mainly
424+
ModelicaSystem) on these Python versions. There is one remaining limitation: only OMCProcessLocal will work as
425+
OMCPathCompatibility is based on the standard pathlib.Path implementation.
426+
"""
427+
428+
# modified copy of pathlib.Path.__new__() definition
429+
def __new__(cls, *args, **kwargs):
430+
logger.warning("Python < 3.12 - using a version of class OMCPath "
431+
"based on pathlib.Path for local usage only.")
432+
433+
if cls is OMCPathCompatibility:
434+
cls = OMCPathCompatibilityWindows if os.name == 'nt' else OMCPathCompatibilityPosix
435+
self = cls._from_parts(args)
436+
if not self._flavour.is_supported:
437+
raise NotImplementedError("cannot instantiate %r on your system"
438+
% (cls.__name__,))
439+
return self
440+
441+
def size(self) -> int:
442+
"""
443+
Needed compatibility function to have the same interface as OMCPathReal
444+
"""
445+
return self.stat().st_size
446+
447+
class OMCPathCompatibilityPosix(pathlib.PosixPath, OMCPathCompatibility):
448+
pass
449+
450+
class OMCPathCompatibilityWindows(pathlib.WindowsPath, OMCPathCompatibility):
451+
pass
452+
453+
OMCPath = OMCPathCompatibility
454+
455+
else:
456+
OMCPath = OMCPathReal
457+
458+
274459
class OMCSessionZMQ:
275460

276461
def __init__(
@@ -325,6 +510,52 @@ def __del__(self):
325510

326511
self.omc_zmq = None
327512

513+
def omcpath(self, *path) -> OMCPath:
514+
"""
515+
Create an OMCPath object based on the given path segments and the current OMC session.
516+
"""
517+
518+
# fallback solution for Python < 3.12; a modified pathlib.Path object is used as OMCPath replacement
519+
if sys.version_info < (3, 12):
520+
if isinstance(self.omc_process, OMCProcessLocal):
521+
# noinspection PyArgumentList
522+
return OMCPath(*path)
523+
else:
524+
raise OMCSessionException("OMCPath is supported for Python < 3.12 only if OMCProcessLocal is used!")
525+
else:
526+
return OMCPath(*path, session=self)
527+
528+
def omcpath_tempdir(self, tempdir_base: Optional[OMCPath] = None) -> OMCPath:
529+
"""
530+
Get a temporary directory using OMC. It is our own implementation as non-local usage relies on OMC to run all
531+
filesystem related access.
532+
"""
533+
names = [str(uuid.uuid4()) for _ in range(100)]
534+
535+
if tempdir_base is None:
536+
# fallback solution for Python < 3.12; a modified pathlib.Path object is used as OMCPath replacement
537+
if sys.version_info < (3, 12):
538+
tempdir_str = tempfile.gettempdir()
539+
else:
540+
tempdir_str = self.sendExpression("getTempDirectoryPath()")
541+
tempdir_base = self.omcpath(tempdir_str)
542+
543+
tempdir: Optional[OMCPath] = None
544+
for name in names:
545+
# create a unique temporary directory name
546+
tempdir = tempdir_base / name
547+
548+
if tempdir.exists():
549+
continue
550+
551+
tempdir.mkdir(parents=True, exist_ok=False)
552+
break
553+
554+
if tempdir is None or not tempdir.is_dir():
555+
raise OMCSessionException("Cannot create a temporary directory!")
556+
557+
return tempdir
558+
328559
def execute(self, command: str):
329560
warnings.warn("This function is depreciated and will be removed in future versions; "
330561
"please use sendExpression() instead", DeprecationWarning, stacklevel=2)

tests/test_OMCPath.py

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import sys
2+
import OMPython
3+
import pytest
4+
5+
skip_on_windows = pytest.mark.skipif(
6+
sys.platform.startswith("win"),
7+
reason="OpenModelica Docker image is Linux-only; skipping on Windows.",
8+
)
9+
10+
skip_python_older_312 = pytest.mark.skipif(
11+
sys.version_info < (3, 12),
12+
reason="OMCPath(non-local) only working for Python >= 3.12.",
13+
)
14+
15+
16+
def test_OMCPath_OMCSessionZMQ():
17+
om = OMPython.OMCSessionZMQ()
18+
19+
_run_OMCPath_checks(om)
20+
21+
del om
22+
23+
24+
def test_OMCPath_OMCProcessLocal():
25+
omp = OMPython.OMCProcessLocal()
26+
om = OMPython.OMCSessionZMQ(omc_process=omp)
27+
28+
_run_OMCPath_checks(om)
29+
30+
del om
31+
32+
33+
@skip_on_windows
34+
@skip_python_older_312
35+
def test_OMCPath_OMCProcessDocker():
36+
omcp = OMPython.OMCProcessDocker(docker="openmodelica/openmodelica:v1.25.0-minimal")
37+
om = OMPython.OMCSessionZMQ(omc_process=omcp)
38+
assert om.sendExpression("getVersion()") == "OpenModelica 1.25.0"
39+
40+
_run_OMCPath_checks(om)
41+
42+
del omcp
43+
del om
44+
45+
46+
@pytest.mark.skip(reason="Not able to run WSL on github")
47+
@skip_python_older_312
48+
def test_OMCPath_OMCProcessWSL():
49+
omcp = OMPython.OMCProcessWSL(
50+
wsl_omc='omc',
51+
wsl_user='omc',
52+
timeout=30.0,
53+
)
54+
om = OMPython.OMCSessionZMQ(omc_process=omcp)
55+
56+
_run_OMCPath_checks(om)
57+
58+
del omcp
59+
del om
60+
61+
62+
def _run_OMCPath_checks(om: OMPython.OMCSessionZMQ):
63+
p1 = om.omcpath_tempdir()
64+
p2 = p1 / 'test'
65+
p2.mkdir()
66+
assert p2.is_dir()
67+
p3 = p2 / '..' / p2.name / 'test.txt'
68+
assert p3.is_file() is False
69+
assert p3.write_text('test')
70+
assert p3.is_file()
71+
assert p3.size() > 0
72+
p3 = p3.resolve().absolute()
73+
assert str(p3) == str((p2 / 'test.txt').resolve().absolute())
74+
assert p3.read_text() == "test"
75+
assert p3.is_file()
76+
assert p3.parent.is_dir()
77+
p3.unlink()
78+
assert p3.is_file() is False

0 commit comments

Comments
 (0)