@@ -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+
274459class 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 )
0 commit comments