wizards/source/scriptforge/SF_Array.xba | 8 wizards/source/scriptforge/SF_PythonHelper.xba | 118 ++++----- wizards/source/scriptforge/python/scriptforge.py | 279 +++++++++++++---------- 3 files changed, 224 insertions(+), 181 deletions(-)
New commits: commit 0bbe5b4f2a7797bfd05fdbde316857fceaf84d77 Author: Jean-Pierre Ledure <j...@ledure.be> AuthorDate: Sat Aug 19 17:30:25 2023 +0200 Commit: Jean-Pierre Ledure <j...@ledure.be> CommitDate: Sun Aug 20 10:48:48 2023 +0200 ScriptForge - Refactor Python <=> Basic protocol Incremental changes had made the code in SF_PythonHelper.xba scriptforge.py (which manage the protocol between the Python user scripts and the ScriptForge services implemented in Basic) less readable and efficient. Next features have been reviewed: - dict instances may be passed as arguments and returned. Conversions are done on-th-fly. - dates may be passed as arguments and returned. Conversions are done on-th-fly. The lists of hardcoded methods have been removed. - 2D arrays in standard modules may be passed as arguments and returned. Conversions are done on-th-fly. The lists of hardcoded methods have been removed. - The hardcoded list of methods requiring a post- processing has been reduced. - Methods in standard modules, when not returning an array, are also executed with CallByName(). - The unused 'localProperties' attribute in Python services has been removed. - Flags about arrays in standard modules have been adapted in filesystem.Files() filesystem.SubFolders() session.UnoMethods() session.UnoProperties() string.SplitNotQuoted() string.Wrap() ui.Documents() - The platform.UserData property became a usual property. Dictionaries are admitted as arguments and return values. As a consquence next functions are supported in Python as well: session.GetPDFExportOpetions() session.SetPDFExportOptions() document, calc, writer.CustomProperties document, calc, writer.DocumentProperties These changes require an update of the help pages. Non-regression tests were run with success. All changes are transparent for existing scripts. Change-Id: Iae7a1f5090c590209cd3cb2314c919c44736eba9 Reviewed-on: https://gerrit.libreoffice.org/c/core/+/155860 Reviewed-by: Jean-Pierre Ledure <j...@ledure.be> Tested-by: Jenkins diff --git a/wizards/source/scriptforge/SF_Array.xba b/wizards/source/scriptforge/SF_Array.xba index 49bdab14770a..54110cde9352 100644 --- a/wizards/source/scriptforge/SF_Array.xba +++ b/wizards/source/scriptforge/SF_Array.xba @@ -849,7 +849,6 @@ REM ---------------------------------------------------------------------------- Public Function ImportFromCSVFile(Optional ByRef FileName As Variant _ , Optional ByVal Delimiter As Variant _ , Optional ByVal DateFormat As Variant _ - , Optional ByVal _IsoDate As Variant _ ) As Variant ''' Import the data contained in a comma-separated values (CSV) file ''' The comma may be replaced by any character @@ -866,7 +865,6 @@ Public Function ImportFromCSVFile(Optional ByRef FileName As Variant _ ''' The dash (-) may be replaced by a dot (.), a slash (/) or a space ''' Other date formats will be ignored ''' If "" (default), dates will be considered as strings -''' _IsoDate: when True, the execution is initiated from Python, do not convert dates to Date variables. Internal use only ''' Returns: ''' A 2D-array with each row corresponding with a single record read in the file ''' and each column corresponding with a field of the record @@ -892,7 +890,9 @@ Dim vItem As Variant ' Individual item in the output array Dim iPosition As Integer ' Date position in individual item Dim iYear As Integer, iMonth As Integer, iDay As Integer ' Date components +Dim bIsoDate As Boolean ' When True, do not convert dates to Date variables Dim i As Long + Const cstItemsLimit = 250000 ' Maximum number of admitted items Const cstThisSub = "Array.ImportFromCSVFile" Const cstSubArgs = "FileName, [Delimiter="",""], [DateFormat=""""]" @@ -903,7 +903,6 @@ Const cstSubArgs = "FileName, [Delimiter="",""], [DateF Check: If IsMissing(Delimiter) Or IsEmpty(Delimiter) Then Delimiter = "," If IsMissing(DateFormat) Or IsEmpty(DateFormat) Then DateFormat = "" - If IsMissing(_IsoDate) Or IsEmpty(_IsoDate) Then _IsoDate = False If SF_Utils._EnterFunction(cstThisSub, cstSubArgs) Then If Not SF_Utils._ValidateFile(FileName, "FileName") Then GoTo Finally If Not SF_Utils._Validate(Delimiter, "Delimiter", V_STRING) Then GoTo Finally @@ -912,6 +911,7 @@ Check: If Len(Delimiter) = 0 Then Delimiter = "," Try: + bIsoDate = _SF_.TriggeredByPython ' Dates are not converted ' Counts the lines present in the file to size the final array ' Very beneficial for large files, better than multiple ReDims ' Small overhead for small files @@ -947,7 +947,7 @@ Try: iPosition = InStr(DateFormat, "MM") : iMonth = CInt(Mid(sItem, iPosition, 2)) iPosition = InStr(DateFormat, "DD") : iDay = CInt(Mid(sItem, iPosition, 2)) vItem = DateSerial(iYear, iMonth, iDay) - If _IsoDate Then vItem = SF_Utils._CDateToIso(vItem) ' Called from Python + If bIsoDate Then vItem = SF_Utils._CDateToIso(vItem) ' Called from Python Else vItem = sItem End If diff --git a/wizards/source/scriptforge/SF_PythonHelper.xba b/wizards/source/scriptforge/SF_PythonHelper.xba index 58a841d4dc26..b611dbfd0d7e 100644 --- a/wizards/source/scriptforge/SF_PythonHelper.xba +++ b/wizards/source/scriptforge/SF_PythonHelper.xba @@ -565,6 +565,8 @@ Public Function _PythonDispatcher(ByRef BasicObject As Variant _ ''' The invocation of the method can be a Property Get, Property Let or a usual call ''' NB: arguments and return values must not be 2D arrays ''' The implementation intends to be as AGNOSTIC as possible in terms of objects nature and methods called +''' The method returns the value effectively returned by the called component, +''' completed with additional metadata. The whole is packaged in a 1D array. ''' Args: ''' BasicObject: a module or a class instance - May also be the reserved string: "SF_Services" ''' CallType: one of the constants applicable to a CallByName statement + optional protocol flags @@ -597,8 +599,10 @@ Dim sServiceName As String ' Alias of BasicObject.ServiceName Dim bBasicClass As Boolean ' True when BasicObject is a class Dim sLibrary As String ' Library where the object belongs to Dim bUno As Boolean ' Return value is a UNO object +Dim bDict As Boolean ' Return value is a Basic SF_Dictionary class instance +Dim oDict As Object ' SF_Dictionary class instance Dim oObjDesc As Object ' _ObjectDescriptor type -Dim iDims As Integer ' # of dims of vReturn +Dim iDims As Integer ' # of dims of vReturn when array Dim sess As Object : Set sess = ScriptForge.SF_Session Dim i As Long, j As Long @@ -609,6 +613,8 @@ Const cstNoArgs = "+++NOARGS+++", cstSymEmpty = "+++EMPTY+++" ' Determines the CallType Const vbGet = 2, vbLet = 4, vbMethod = 1, vbSet = 8 ' Protocol flags +Const cstPost = 16 ' Requires a hardcoded post-processing +Const cstDictArg = 32 ' May contain a Dictionary argument Const cstDateArg = 64 ' May contain a date argument Const cstDateRet = 128 ' Return value can be a date Const cstUno = 256 ' Return value can be a UNO object @@ -616,8 +622,8 @@ Const cstArgArray = 512 ' Any argument can be a 2D array Const cstRetArray = 1024 ' Return value can be an array Const cstObject = 2048 ' 1st argument is a Basic object when numeric Const cstHardCode = 4096 ' Method must not be executed with CallByName() -' Object nature in returned array -Const objMODULE = 1, objCLASS = 2, objUNO = 3 +' Returned object nature +Const objMODULE = 1, objCLASS = 2, objDICT = 3, objUNO = 4 Check: If SF_Utils._ErrorHandling() Then On Local Error GoTo Catch @@ -626,7 +632,10 @@ Check: ' Ignore Null basic objects (Null = Null or Nothing) If IsNull(BasicObject) Or IsEmpty(BasicObject) Then GoTo Catch - ' Reinterpret arguments one by one into vArgs, convert UNO date/times and conventional NoArgs/Empty/Null/Missing values + ' Reinterpret arguments one by one into vArgs + ' - convert UNO dates/times + ' - identify conventional NoArgs/Empty/Null/Missing constants + ' - convert arrays of property values into DictionarY iNbArgs = -1 vArgs = Array() @@ -642,7 +651,7 @@ Check: If vArg < 0 Or Not IsArray(_SF_.PythonStorage) Then GoTo Catch If vArg > UBound(_SF_.PythonStorage) Then GoTo Catch vArg = _SF_.PythonStorage(vArg) - ' Is argument a symbolic constant for Null, Empty, ... , or a date? + ' Is argument a symbolic constant for Null, Empty, ... , or a date, or a dictionary ? ElseIf VarType(vArg) = V_STRING Then If Len(vArg) = 0 Then ElseIf vArg = cstSymEmpty Then @@ -654,6 +663,16 @@ Check: End If ElseIf VarType(vArg) = V_OBJECT Then If ( CallType And cstDateArg ) = cstDateArg Then vArg = CDateFromUnoDateTime(vArg) + ElseIf ( CallType And cstDictArg ) = cstDictArg Then + If IsArray(vArg) Then + If UBound(vArg) >= 0 Then + If sess.UnoObjectType(vArg(0)) = "com.sun.star.beans.PropertyValue" Then + Set oDict = CreateScriptService("ScriptForge.Dictionary") + oDict.ImportFromPropertyValues(vArg, Overwrite := True) + vArg = oDict + End If + End If + End If End If iNbArgs = iNbArgs + 1 @@ -669,9 +688,12 @@ Try: ' (2) Python has tuples and tuple of tuples, not 2D arrays ' (3) Passing 2D arrays through a script provider always transform it into a sequence of sequences ' (4) The CallByName function takes exclusive control on the targeted object up to its exit - ' 1. Methods in usual modules are called by ExecuteBasicScript() except if they use a ParamArray + ' (5) A script provider returns a Basic Date variable as Empty + ' RULES: + ' 1. All methods in any module are invoked with CallByName ' 2. Properties in any service are got and set with obj.GetProperty/SetProperty(...) - ' 3. Methods in class modules are invoked with CallByName + ' EXCEPTIONS: + ' 3. Methods in usual modules are called by ExecuteBasicScript() when they manipulate arrays ' 4. Methods in class modules using a 2D array or returning arrays, or methods using ParamArray, ''' are hardcoded as exceptions or are not implemented ' 5. Due to constraint (4), a predefined list of method calls must be hardcoded to avoid blocking use of CallByName @@ -706,7 +728,7 @@ Try: sObjectType = vBasicObject.ObjectType bBasicClass = ( Left(sObjectType, 3) <> "SF_" ) End If - + ' Implement dispatching strategy Case V_INTEGER If BasicObject < 0 Or Not IsArray(_SF_.PythonStorage) Then GoTo Catch @@ -718,41 +740,9 @@ Try: ' Basic modules have type = "SF_*" bBasicClass = ( Left(sObjectType, 3) <> "SF_" ) sLibrary = Split(sServiceName, ".")(0) - - ' Methods in standard modules returning/passing a date are hardcoded as exceptions - If Not bBasicClass And ((CallType And vbMethod) = vbMethod) _ - And (((CallType And cstDateRet) = cstDateRet) Or ((CallType And cstDateArg) = cstDateArg)) Then - Select Case sServiceName - Case "ScriptForge.FileSystem" - If Script = "GetFileModified" Then vReturn = SF_FileSystem.GetFileModified(vArgs(0)) - Case "ScriptForge.Region" - Select Case Script - Case "DSTOffset" : vReturn = SF_Region.DSTOffset(vArgs(0), vArgs(1), vArgs(2)) - Case "LocalDateTime" : vReturn = SF_Region.LocalDateTime(vArgs(0), vArgs(1), vArgs(2)) - Case "UTCDateTime" : vReturn = SF_Region.UTCDateTime(vArgs(0), vArgs(1), vArgs(2)) - Case "UTCNow" : vReturn = SF_Region.UTCNow(vArgs(0), vArgs(1)) - Case Else - End Select - End Select - ' Methods in usual modules using a 2D array or returning arrays are hardcoded as exceptions - ElseIf Not bBasicClass And _ - (((CallType And vbMethod) + (CallType And cstArgArray)) = vbMethod + cstArgArray Or _ - ((CallType And vbMethod) + (CallType And cstRetArray)) = vbMethod + cstRetArray) Then - ' Not service related - If Script = "Methods" Then - vReturn = vBasicObject.Methods() - ElseIf Script = "Properties" Then - vReturn = vBasicObject.Properties() - Else - Select Case sServiceName - Case "ScriptForge.Array" - If Script = "ImportFromCSVFile" Then vReturn = SF_Array.ImportFromCSVFile(vArgs(0), vArgs(1), vArgs(2), True) - End Select - End If - - ' Methods in usual modules are called by ExecuteBasicScript() except if they use a ParamArray - ElseIf Not bBasicClass And (CallType And vbMethod) = vbMethod Then + ' Methods in standard modules are called by ExecuteBasicScript() when arrays are returned + If Not bBasicClass And (CallType And vbMethod) = vbMethod And (CallType And cstRetArray) = cstRetArray Then sScript = sLibrary & "." & sObjectType & "." & Script ' Force validation in targeted function, not in ExecuteBasicScript() _SF_.StackLevel = -1 @@ -768,13 +758,13 @@ Try: Case 7 : vReturn = sess.ExecuteBasicScript(, sScript, vArgs(0), vArgs(1), vArgs(2), vArgs(3), vArgs(4), vArgs(5), vArgs(6), vArgs(7)) End Select _SF_.StackLevel = 0 - + ' Properties in any service are got and set with obj.GetProperty/SetProperty(...) ElseIf (CallType And vbGet) = vbGet Then ' In some cases (Calc ...) GetProperty may have an argument If UBound(vArgs) < 0 Then vReturn = vBasicObject.GetProperty(Script) Else vReturn = vBasicObject.GetProperty(Script, vArgs(0)) ElseIf (CallType And vbLet) = vbLet Then vReturn = vBasicObject.SetProperty(Script, vArgs(0)) - + ' Methods in class modules using a 2D array or returning arrays are hardcoded as exceptions. Bug #138155 ElseIf ((CallType And vbMethod) + (CallType And cstArgArray)) = vbMethod + cstArgArray Or _ ((CallType And vbMethod) + (CallType And cstRetArray)) = vbMethod + cstRetArray Then @@ -830,23 +820,18 @@ Try: End Select End Select End If - - ' Methods in class modules may better not be executed with CallByName() + + ' Specific methods in class modules may better not be executed with CallByName() because they do not return immediately ElseIf bBasicClass And ((CallType And vbMethod) + (CallType And cstHardCode)) = vbMethod + cstHardCode Then Select Case sServiceName Case "SFDialogs.Dialog" Select Case Script - Case "Activate" : vReturn = vBasicObject.Activate() - Case "Center" - If UBound(vArgs) < 0 Then vReturn = vBasicObject.Center() Else vReturn = vBasicObject.Center(vArgs(0)) - Case "EndExecute" : vReturn = vBasicObject.EndExecute(vArgs(0)) Case "Execute" : vReturn = vBasicObject.Execute(vArgs(0)) - Case "Resize" : vReturn = vBasicObject.Resize(vArgs(0), vArgs(1), vArgs(2), vArgs(3)) End Select End Select - ' Methods in class modules are invoked with CallByName - ElseIf bBasicClass And ((CallType And vbMethod) = vbMethod) Then + ' Methods in all modules are invoked with CallByName + ElseIf ((CallType And vbMethod) = vbMethod) Then Select Case UBound(vArgs) ' Dirty alternatives to process usual and ParamArray cases ' But, up to ... how many ? @@ -896,11 +881,11 @@ Try: End If ' Post processing - If Script = "Dispose" Then - ' Special case: Dispose() must update the cache for class objects created in Python scripts - Set _SF_.PythonStorage(BasicObject) = Nothing - ElseIf sServiceName = "ScriptForge.Platform" And Script = "UserData" Then - vReturn = vReturn.ConvertToPropertyValues() + If (CallType And cstPost) = cstPost Then + If Script = "Dispose" Then + ' Special case: Dispose() must update the cache for class objects created in Python scripts + Set _SF_.PythonStorage(BasicObject) = Nothing + End If End If Case Else End Select @@ -909,6 +894,7 @@ Try: vReturnArray = Array() ' Distinguish: Basic object ' UNO object + ' Dictionary ' Array ' Scalar If IsArray(vReturn) Then @@ -941,15 +927,25 @@ Try: Set vReturnArray(0) = vReturn Else ReDim vReturnArray(0 To 5) - vReturnArray(0) = _SF_._AddToPythonSTorage(vReturn) + bDict = ( vReturn.ObjectType = "DICTIONARY" ) + If bDict Then + vReturnArray(0) = vReturn.ConvertToPropertyValues() + Else + vReturnArray(0) = _SF_._AddToPythonSTorage(vReturn) + End If End If vReturnArray(1) = V_OBJECT - vReturnArray(2) = Iif(bUno, objUNO, Iif(bBasicClass, objCLASS, objMODULE)) + Select Case True + Case bUno : vReturnArray(2) = objUNO + Case bDICT : vReturnArray(2) = objDICT + Case bBasicClass : vReturnArray(2) = objCLASS + Case Else : vReturnArray(2) = objMODULE + End Select If Not bUno Then vReturnArray(3) = vReturn.ObjectType vReturnArray(4) = vReturn.ServiceName vReturnArray(5) = "" - If vReturn.ObjectType <> "SF_CalcReference" Then ' Calc references are implemented as a Type ... End Type data structure + If vReturn.ObjectType <> "SF_CalcReference" And Not bDict Then ' Calc references are implemented as a Type ... End Type data structure If SF_Array.Contains(vReturn.Properties(), "Name", SortOrder := "ASC") Then vReturnArray(5) = vReturn.Name End If End If diff --git a/wizards/source/scriptforge/python/scriptforge.py b/wizards/source/scriptforge/python/scriptforge.py index e978c30b4304..b235a791f87d 100644 --- a/wizards/source/scriptforge/python/scriptforge.py +++ b/wizards/source/scriptforge/python/scriptforge.py @@ -28,27 +28,27 @@ By collecting most-demanded document operations in a set of easy to use, easy to read routines, users can now program document macros with much less hassle and get quicker results. - ScriptForge abundant methods are organized in reusable modules that cleanly isolate Basic/Python programming - language constructs from ODF document content accesses and user interface(UI) features. + The use of the ScriptForge interfaces in user scripts hides the complexity of the usual UNO interfaces. + However it does not replace them. At the opposite their coexistence is ensured. + Indeed, ScriptForge provides a number of shortcuts to key UNO objects. The scriptforge.py module - - implements a protocol between Python (user) scripts and the ScriptForge Basic library - - contains the interfaces (classes and attributes) to be used in Python user scripts - to run the services implemented in the standard libraries shipped with LibreOffice + - describes the interfaces (classes and attributes) to be used in Python user scripts + to run the services implemented in the standard modules shipped with LibreOffice + - implements a protocol between those interfaces and, when appropriate, the corresponding ScriptForge + Basic libraries implementing the requested services. Usage: - When Python and LibreOffice run in the same process (usual case): either - from scriptforge import * # or, better ... + When Python and LibreOffice run in the same process (usual case): from scriptforge import CreateScriptService When Python and LibreOffice are started in separate processes, - LibreOffice being started from console ... (example for Linux with port = 2021) - ./soffice --accept='socket,host=localhost,port=2021;urp;' - then use next statement: - from scriptforge import * # or, better ... + LibreOffice being started from console ... (example for Linux with port = 2023) + ./soffice --accept='socket,host=localhost,port=2023;urp;' + then use next statements: from scriptforge import CreateScriptService, ScriptForge - ScriptForge(hostname = 'localhost', port = 2021) + ScriptForge(hostname = 'localhost', port = 2023) Specific documentation about the use of ScriptForge from Python scripts: https://help.libreoffice.org/latest/en-US/text/sbasic/shared/03/sf_intro.html?DbPAR=BASIC @@ -80,24 +80,29 @@ class _Singleton(type): class ScriptForge(object, metaclass = _Singleton): """ - The ScriptForge (singleton) class encapsulates the core of the ScriptForge run-time + The ScriptForge class encapsulates the core of the ScriptForge run-time - Bridge with the LibreOffice process - Implementation of the inter-language protocol with the Basic libraries - Identification of the available services interfaces - Dispatching of services - Coexistence with UNO - It embeds the Service class that manages the protocol with Basic + The class may be instantiated several times. Only the first instance will be retained ("Singleton"). """ # ######################################################################### # Class attributes # ######################################################################### + # Inter-process parameters hostname = '' port = 0 - componentcontext = None - scriptprovider = None - SCRIPTFORGEINITDONE = False + + componentcontext = None # com.sun.star.uno.XComponentContext + scriptprovider = None # com.sun.star.script.provider.XScriptProvider + SCRIPTFORGEINITDONE = False # When True, an instance of the class exists + + servicesdispatcher = None # com.sun.star.script.provider.XScript to 'basicdispatcher' constant + serviceslist = {} # Dictionary of all available services # ######################################################################### # Class constants @@ -105,17 +110,18 @@ class ScriptForge(object, metaclass = _Singleton): library = 'ScriptForge' Version = '7.6' # Actual version number # - # Basic dispatcher for Python scripts + # Basic dispatcher for Python scripts (@scope#library.module.function) basicdispatcher = '@application#ScriptForge.SF_PythonHelper._PythonDispatcher' # Python helper functions module - pythonhelpermodule = 'ScriptForgeHelper.py' + pythonhelpermodule = 'ScriptForgeHelper.py' # Preset in production mode, + # might be changed (by devs only) in test mode # # VarType() constants V_EMPTY, V_NULL, V_INTEGER, V_LONG, V_SINGLE, V_DOUBLE = 0, 1, 2, 3, 4, 5 V_CURRENCY, V_DATE, V_STRING, V_OBJECT, V_BOOLEAN = 6, 7, 8, 9, 11 V_VARIANT, V_ARRAY, V_ERROR, V_UNO = 12, 8192, -1, 16 - # Object types - objMODULE, objCLASS, objUNO = 1, 2, 3 + # Types of objects returned from Basic + objMODULE, objCLASS, objDICT, objUNO = 1, 2, 3, 4 # Special argument symbols cstSymEmpty, cstSymNull, cstSymMissing = '+++EMPTY+++', '+++NULL+++', '+++MISSING+++' # Predefined references for services implemented as standard Basic modules @@ -132,7 +138,8 @@ class ScriptForge(object, metaclass = _Singleton): def __init__(self, hostname = '', port = 0): """ Because singleton, constructor is executed only once while Python active - Arguments are mandatory when Python and LibreOffice run in separate processes + Both arguments are mandatory when Python and LibreOffice run in separate processes + Otherwise both arguments must be left out. :param hostname: probably 'localhost' :param port: port number """ @@ -194,57 +201,63 @@ class ScriptForge(object, metaclass = _Singleton): @classmethod def InvokeSimpleScript(cls, script, *args): """ - Create a UNO object corresponding with the given Python or Basic script - The execution is done with the invoke() method applied on the created object + Low-level script execution via the script provider protocol: + Create a UNO object corresponding with the given Python or Basic script + The execution is done with the invoke() method applied on the created object Implicit scope: Either - "application" a shared library (BASIC) - "share" a library of LibreOffice Macros (PYTHON) + "application" a shared library (BASIC) + "share" a module within LibreOffice Macros (PYTHON) :param script: Either [@][scope#][library.]module.method - Must not be a class module or method [@] means that the targeted method accepts ParamArray arguments (Basic only) [scope#][directory/]module.py$method - Must be a method defined at module level - :return: the value returned by the invoked script, or an error if the script was not found + :return: the value returned by the invoked script without interpretation + An error is raised when the script is not found. """ - # The frequently called PythonDispatcher in the ScriptForge Basic library is cached to privilege performance - if cls.servicesdispatcher is not None and script == ScriptForge.basicdispatcher: - xscript = cls.servicesdispatcher - fullscript = script - paramarray = True - # Build the URI specification described in - # https://wiki.documentfoundation.org/Documentation/DevGuide/Scripting_Framework#Scripting_Framework_URI_Specification - elif len(script) > 0: + def ParseScript(_script): # Check ParamArray arguments - paramarray = False - if script[0] == '@': - script = script[1:] - paramarray = True + _paramarray = False + if _script[0] == '@': + _script = _script[1:] + _paramarray = True scope = '' - if '#' in script: - scope, script = script.split('#') - if '.py$' in script.lower(): # Python + if '#' in _script: + scope, _script = _script.split('#') + if '.py$' in _script.lower(): # Python if len(scope) == 0: scope = 'share' # Default for Python # Provide an alternate helper script depending on test context - if script.startswith(cls.pythonhelpermodule) and hasattr(cls, 'pythonhelpermodule2'): - script = cls.pythonhelpermodule2 + script[len(cls.pythonhelpermodule):] - if '#' in script: - scope, script = script.split('#') - uri = 'vnd.sun.star.script:{0}?language=Python&location={1}'.format(script, scope) + if _script.startswith(cls.pythonhelpermodule) and hasattr(cls, 'pythonhelpermodule2'): + _script = cls.pythonhelpermodule2 + _script[len(cls.pythonhelpermodule):] + if '#' in _script: + scope, _script = _script.split('#') + uri = 'vnd.sun.star.script:{0}?language=Python&location={1}'.format(_script, scope) else: # Basic if len(scope) == 0: scope = 'application' # Default for Basic lib = '' - if len(script.split('.')) < 3: + if len(_script.split('.')) < 3: lib = cls.library + '.' # Default library = ScriptForge - uri = 'vnd.sun.star.script:{0}{1}?language=Basic&location={2}'.format(lib, script, scope) + uri = 'vnd.sun.star.script:{0}{1}?language=Basic&location={2}'.format(lib, _script, scope) # Get the script object - fullscript = ('@' if paramarray else '') + scope + ':' + script + _fullscript = ('@' if _paramarray else '') + scope + ':' + _script try: - xscript = cls.scriptprovider.getScript(uri) + _xscript = cls.scriptprovider.getScript(uri) # com.sun.star.script.provider.XScript except Exception: raise RuntimeError( - 'The script \'{0}\' could not be located in your LibreOffice installation'.format(script)) + 'The script \'{0}\' could not be located in your LibreOffice installation'.format(_script)) + return _paramarray, _fullscript, _xscript + + # The frequently called PythonDispatcher in the ScriptForge Basic library is cached to privilege performance + if cls.servicesdispatcher is not None and script == ScriptForge.basicdispatcher: + xscript = cls.servicesdispatcher + fullscript = script + paramarray = True + # Parse the 'script' argument and build the URI specification described in + # https://wiki.documentfoundation.org/Documentation/DevGuide/Scripting_Framework#Scripting_Framework_URI_Specification + elif len(script) > 0: + paramarray, fullscript, xscript = ParseScript(script) else: # Should not happen return None @@ -265,33 +278,57 @@ class ScriptForge(object, metaclass = _Singleton): @classmethod def InvokeBasicService(cls, basicobject, flags, method, *args): """ - Execute a given Basic script and interpret its result + High-level script execution via the ScriptForge inter-language protocol: + To be used for all service methods having their implementation in the Basic world + Substitute dictionary arguments by sets of UNO property values + Execute the given Basic method on a class instance + Interpret its result This method has as counterpart the ScriptForge.SF_PythonHelper._PythonDispatcher() Basic method - :param basicobject: a Service subclass + :param basicobject: a SFServices subclass instance + The real object is cached in a Basic Global variable and identified by its reference :param flags: see the vb* and flg* constants in the SFServices class :param method: the name of the method or property to invoke, as a string :param args: the arguments of the method. Symbolic cst* constants may be necessary :return: The invoked Basic counterpart script (with InvokeSimpleScript()) will return a tuple - [0] The returned value - scalar, object reference or a tuple - [1] The Basic VarType() of the returned value - Null, Empty and Nothing have different vartypes but return all None to Python + [0/Value] The returned value - scalar, object reference, UNO object or a tuple + [1/VarType] The Basic VarType() of the returned value + Null, Empty and Nothing have own vartypes but return all None to Python Additionally, when [0] is a tuple: - [2] Number of dimensions in Basic + [2/Dims] Number of dimensions of the original Basic array Additionally, when [0] is a UNO or Basic object: - [2] Module (1), Class instance (2) or UNO (3) - [3] The object's ObjectType - [4] The object's ServiceName - [5] The object's name + [2/Class] Basic module (1), Basic class instance (2), Dictionary (3), UNO object (4) + Additionally, when [0] is a Basic object: + [3/Type] The object's ObjectType + [4/Service] The object's ServiceName + [5/Name] The object's name When an error occurs Python receives None as a scalar. This determines the occurrence of a failure The method returns either - the 0th element of the tuple when scalar, tuple or UNO object - - a new Service() object or one of its subclasses otherwise + - a new SFServices() object or one of its subclasses otherwise """ # Constants script = ScriptForge.basicdispatcher cstNoArgs = '+++NOARGS+++' cstValue, cstVarType, cstDims, cstClass, cstType, cstService, cstName = 0, 1, 2, 2, 3, 4, 5 + def ConvertDictArgs(): + """ + Convert dictionaries in arguments to sets of property values + """ + argslist = list(args) + for i in range(len(args)): + arg = argslist[i] + if isinstance(arg, dict): + argdict = arg + if not isinstance(argdict, SFScriptForge.SF_Dictionary): + argdict = CreateScriptService('ScriptForge.Dictionary', arg) + argslist[i] = argdict.ConvertToPropertyValues() + return tuple(argslist) + + # + # Intercept dictionary arguments + if flags & SFServices.flgDictArg == SFServices.flgDictArg: # Bits comparison + args = ConvertDictArgs() # # Run the basic script # The targeted script has a ParamArray argument. Do not change next 4 lines except if you know what you do ! @@ -307,9 +344,21 @@ class ScriptForge(object, metaclass = _Singleton): raise RuntimeError("The execution of the method '" + method + "' failed. Execution stops.") # # Analyze the returned tuple - if returntuple[cstVarType] == ScriptForge.V_OBJECT and len(returntuple) > cstClass: # Avoid Nothing + # Distinguish: + # A Basic object to be mapped onto a new Python class instance + # A UNO object + # A set of property values to be returned as a dict() + # An array, tuple or tuple of tuples + # A scalar or Nothing + returnvalue = returntuple[cstValue] + if returntuple[cstVarType] == ScriptForge.V_OBJECT and len(returntuple) > cstClass: # Skip Nothing if returntuple[cstClass] == ScriptForge.objUNO: pass + elif returntuple[cstClass] == ScriptForge.objDICT: + dico = CreateScriptService('ScriptForge.Dictionary') + if not isinstance(returnvalue, uno.ByteSequence): # if array not empty + dico.ImportFromPropertyValues(returnvalue, overwrite = True) + return dico else: # Create the new class instance of the right subclass of SFServices() servname = returntuple[cstService] @@ -318,18 +367,17 @@ class ScriptForge(object, metaclass = _Singleton): raise RuntimeError("The service '" + servname + "' is not available in Python. Execution stops.") subcls = cls.serviceslist[servname] if subcls is not None: - return subcls(returntuple[cstValue], returntuple[cstType], returntuple[cstClass], - returntuple[cstName]) + return subcls(returnvalue, returntuple[cstType], returntuple[cstClass], returntuple[cstName]) elif returntuple[cstVarType] >= ScriptForge.V_ARRAY: # Intercept empty array - if isinstance(returntuple[cstValue], uno.ByteSequence): + if isinstance(returnvalue, uno.ByteSequence): return () elif returntuple[cstVarType] == ScriptForge.V_DATE: - dat = SFScriptForge.SF_Basic.CDateFromUnoDateTime(returntuple[cstValue]) + dat = SFScriptForge.SF_Basic.CDateFromUnoDateTime(returnvalue) return dat else: # All other scalar values pass - return returntuple[cstValue] + return returnvalue @staticmethod def SetAttributeSynonyms(): @@ -429,8 +477,6 @@ class SFServices(object): Conventionally, camel-cased and lower-cased synonyms are supported where relevant a dictionary named 'serviceproperties' with keys = (proper-cased) property names and value = boolean True = editable, False = read-only - a list named 'localProperties' reserved to properties for internal use - e.g. oDlg.Controls() is a method that uses '_Controls' to hold the list of available controls When forceGetProperty = False # Standard behaviour read-only serviceproperties are buffered in Python after their 1st get request to Basic @@ -449,7 +495,8 @@ class SFServices(object): """ # Python-Basic protocol constants and flags vbGet, vbLet, vbMethod, vbSet = 2, 4, 1, 8 # CallByName constants - flgPost = 32 # The method or the property implies a hardcoded post-processing + flgPost = 16 # The method or the property implies a hardcoded post-processing + flgDictArg = 32 # Invoked service method may contain a dict argument flgDateArg = 64 # Invoked service method may contain a date argument flgDateRet = 128 # Invoked service method can return a date flgArrayArg = 512 # 1st argument can be a 2D array @@ -476,14 +523,12 @@ class SFServices(object): """ Trivial initialization of internal properties If the subclass has its own __init()__ method, a call to this one should be its first statement. - Afterwards localProperties should be filled with the list of its own properties """ self.objectreference = reference # the index in the Python storage where the Basic object is stored - self.objecttype = objtype # ('SF_String', 'DICTIONARY', ...) + self.objecttype = objtype # ('SF_String', 'TIMER', ...) self.classmodule = classmodule # Module (1), Class instance (2) self.name = name # '' when no name self.internal = False # True to exceptionally allow assigning a new value to a read-only property - self.localProperties = [] # the properties reserved for internal use (often empty) def __getattr__(self, name): """ @@ -495,7 +540,7 @@ class SFServices(object): if name in self.propertysynonyms: # Reset real name if argument provided in lower or camel case name = self.propertysynonyms[name] if self.serviceimplementation == 'basic': - if name in ('serviceproperties', 'localProperties', 'internal_attributes', 'propertysynonyms', + if name in ('serviceproperties', 'internal_attributes', 'propertysynonyms', 'forceGetProperty'): pass elif name in self.serviceproperties: @@ -519,10 +564,10 @@ class SFServices(object): Management of __dict__ is automatically done in the final usual object.__setattr__ method """ if self.serviceimplementation == 'basic': - if name in ('serviceproperties', 'localProperties', 'internal_attributes', 'propertysynonyms', + if name in ('serviceproperties', 'internal_attributes', 'propertysynonyms', 'forceGetProperty'): pass - elif name[0:2] == '__' or name in self.internal_attributes or name in self.localProperties: + elif name[0:2] == '__' or name in self.internal_attributes: pass elif name in self.serviceproperties or name in self.propertysynonyms: if name in self.propertysynonyms: # Reset real name if argument provided in lower or camel case @@ -534,9 +579,9 @@ class SFServices(object): return else: raise AttributeError( - "type object '" + self.objecttype + "' has no editable property '" + name + "'") + "object of type '" + self.objecttype + "' has no editable property '" + name + "'") else: - raise AttributeError("type object '" + self.objecttype + "' has no property '" + name + "'") + raise AttributeError("object of type '" + self.objecttype + "' has no property '" + name + "'") object.__setattr__(self, name, value) return @@ -547,7 +592,7 @@ class SFServices(object): def Dispose(self): if self.serviceimplementation == 'basic': if self.objectreference >= len(ScriptForge.servicesmodules): # Do not dispose predefined module objects - self.ExecMethod(self.vbMethod, 'Dispose') + self.ExecMethod(self.vbMethod + self.flgPost, 'Dispose') self.objectreference = -1 def ExecMethod(self, flags = 0, methodname = '', *args): @@ -593,6 +638,8 @@ class SFServices(object): if isinstance(value, datetime.datetime): value = SFScriptForge.SF_Basic.CDateToUnoDateTime(value) flag += self.flgDateArg + elif isinstance(value, dict): + flag += self.flgDictArg if repr(type(value)) == "<class 'pyuno'>": flag += self.flgUno return self.EXEC(self.objectreference, flag, propertyname, value) @@ -1128,7 +1175,7 @@ class SFScriptForge: return self.ExecMethod(self.vbMethod, 'FileExists', filename) def Files(self, foldername, filter = '', includesubfolders = False): - return self.ExecMethod(self.vbMethod, 'Files', foldername, filter, includesubfolders) + return self.ExecMethod(self.vbMethod + self.flgArrayRet, 'Files', foldername, filter, includesubfolders) def FolderExists(self, foldername): return self.ExecMethod(self.vbMethod, 'FolderExists', foldername) @@ -1186,7 +1233,8 @@ class SFScriptForge: return self.ExecMethod(self.vbMethod, 'PickFolder', defaultfolder, freetext) def SubFolders(self, foldername, filter = '', includesubfolders = False): - return self.ExecMethod(self.vbMethod, 'SubFolders', foldername, filter, includesubfolders) + return self.ExecMethod(self.vbMethod + self.flgArrayRet, 'SubFolders', foldername, + filter, includesubfolders) @classmethod def _ConvertFromUrl(cls, filename): @@ -1304,15 +1352,6 @@ class SFScriptForge: def PythonVersion(self): return self.SIMPLEEXEC(self.py, 'PythonVersion') - @property - def UserData(self): - props = self.GetProperty('UserData') # is an array of property values - if len(props) == 0: - return dict() - dico = CreateScriptService('Dictionary') - dico.ImportFromPropertyValues(props, overwrite = True) - return dico - # ######################################################################### # SF_Region CLASS # ######################################################################### @@ -1457,6 +1496,9 @@ class SFScriptForge: def ExecutePythonScript(cls, scope = '', script = '', *args): return cls.SIMPLEEXEC(scope + '#' + script, *args) + def GetPDFExportOptions(self): + return self.ExecMethod(self.vbMethod, 'GetPDFExportOptions') + def HasUnoMethod(self, unoobject, methodname): return self.ExecMethod(self.vbMethod, 'HasUnoMethod', unoobject, methodname) @@ -1474,14 +1516,17 @@ class SFScriptForge: def SendMail(self, recipient, cc = '', bcc = '', subject = '', body = '', filenames = '', editmessage = True): return self.ExecMethod(self.vbMethod, 'SendMail', recipient, cc, bcc, subject, body, filenames, editmessage) + def SetPDFExportOptions(self, pdfoptions): + return self.ExecMethod(self.vbMethod + self.flgDictArg, 'SetPDFExportOptions', pdfoptions) + def UnoObjectType(self, unoobject): return self.ExecMethod(self.vbMethod, 'UnoObjectType', unoobject) def UnoMethods(self, unoobject): - return self.ExecMethod(self.vbMethod, 'UnoMethods', unoobject) + return self.ExecMethod(self.vbMethod + self.flgArrayRet, 'UnoMethods', unoobject) def UnoProperties(self, unoobject): - return self.ExecMethod(self.vbMethod, 'UnoProperties', unoobject) + return self.ExecMethod(self.vbMethod + self.flgArrayRet, 'UnoProperties', unoobject) def WebService(self, uri): return self.ExecMethod(self.vbMethod, 'WebService', uri) @@ -1531,10 +1576,11 @@ class SFScriptForge: return self.ExecMethod(self.vbMethod, 'IsUrl', inputstr) def SplitNotQuoted(self, inputstr, delimiter = ' ', occurrences = 0, quotechar = '"'): - return self.ExecMethod(self.vbMethod, 'SplitNotQuoted', inputstr, delimiter, occurrences, quotechar) + return self.ExecMethod(self.vbMethod + self.flgArrayRet, 'SplitNotQuoted', inputstr, delimiter, + occurrences, quotechar) def Wrap(self, inputstr, width = 70, tabsize = 8): - return self.ExecMethod(self.vbMethod, 'Wrap', inputstr, width, tabsize) + return self.ExecMethod(self.vbMethod + self.flgArrayRet, 'Wrap', inputstr, width, tabsize) # ######################################################################### # SF_TextStream CLASS @@ -1647,8 +1693,6 @@ class SFScriptForge: def ActiveWindow(self): return self.ExecMethod(self.vbMethod, 'ActiveWindow') - activeWindow, activewindow = ActiveWindow, ActiveWindow - def Activate(self, windowname = ''): return self.ExecMethod(self.vbMethod, 'Activate', windowname) @@ -1660,7 +1704,7 @@ class SFScriptForge: return self.ExecMethod(self.vbMethod, 'CreateDocument', documenttype, templatefile, hidden) def Documents(self): - return self.ExecMethod(self.vbMethod, 'Documents') + return self.ExecMethod(self.vbMethod + self.flgArrayRet, 'Documents') def GetDocument(self, windowname = ''): return self.ExecMethod(self.vbMethod, 'GetDocument', windowname) @@ -1868,13 +1912,13 @@ class SFDialogs: # Methods potentially executed while the dialog is in execution require the flgHardCode flag def Activate(self): - return self.ExecMethod(self.vbMethod + self.flgHardCode, 'Activate') + return self.ExecMethod(self.vbMethod, 'Activate') def Center(self, parent = ScriptForge.cstSymMissing): parentclasses = (SFDocuments.SF_Document, SFDocuments.SF_Base, SFDocuments.SF_Calc, SFDocuments.SF_Writer, SFDialogs.SF_Dialog) parentobj = parent.objectreference if isinstance(parent, parentclasses) else parent - return self.ExecMethod(self.vbMethod + self.flgObject + self.flgHardCode, 'Center', parentobj) + return self.ExecMethod(self.vbMethod + self.flgObject, 'Center', parentobj) def CloneControl(self, sourcename, controlname, left = 1, top = 1): return self.ExecMethod(self.vbMethod, 'CloneControl', sourcename, controlname, left, top) @@ -1981,7 +2025,7 @@ class SFDialogs: return self.ExecMethod(self.vbMethod, 'CreateTreeControl', controlname, place, border) def EndExecute(self, returnvalue): - return self.ExecMethod(self.vbMethod + self.flgHardCode, 'EndExecute', returnvalue) + return self.ExecMethod(self.vbMethod, 'EndExecute', returnvalue) def Execute(self, modal = True): return self.ExecMethod(self.vbMethod + self.flgHardCode, 'Execute', modal) @@ -1994,7 +2038,7 @@ class SFDialogs: return self.ExecMethod(self.vbMethod, 'OrderTabs', tabslist, start, increment) def Resize(self, left = -99999, top = -99999, width = -1, height = -1): - return self.ExecMethod(self.vbMethod + self.flgHardCode, 'Resize', left, top, width, height) + return self.ExecMethod(self.vbMethod, 'Resize', left, top, width, height) def SetPageManager(self, pilotcontrols = '', tabcontrols = '', wizardcontrols = '', lastpage = 0): return self.ExecMethod(self.vbMethod, 'SetPageManager', pilotcontrols, tabcontrols, wizardcontrols, @@ -2117,10 +2161,11 @@ class SFDocuments: serviceimplementation = 'basic' servicename = 'SFDocuments.Document' servicesynonyms = ('document', 'sfdocuments.document') - serviceproperties = dict(Description = True, DocumentType = False, ExportFilters = False, FileSystem = False, - ImportFilters = False, IsBase = False, IsCalc = False, IsDraw = False, - IsFormDocument = False, IsImpress = False, IsMath = False, IsWriter = False, - Keywords = True, Readonly = False, Subject = True, Title = True, XComponent = False) + serviceproperties = dict(CustomProperties = True, Description = True, DocumentProperties = False, + DocumentType = False, ExportFilters = False, FileSystem = False, ImportFilters = False, + IsBase = False, IsCalc = False, IsDraw = False, IsFormDocument = False, + IsImpress = False, IsMath = False, IsWriter = False, Keywords = True, Readonly = False, + Subject = True, Title = True, XComponent = False) # Force for each property to get its value from Basic - due to intense interactivity with user forceGetProperty = True @@ -2242,11 +2287,12 @@ class SFDocuments: serviceimplementation = 'basic' servicename = 'SFDocuments.Calc' servicesynonyms = ('calc', 'sfdocuments.calc') - serviceproperties = dict(CurrentSelection = True, Sheets = False, - Description = True, DocumentType = False, ExportFilters = False, FileSystem = False, - ImportFilters = False, IsBase = False, IsCalc = False, IsDraw = False, - IsFormDocument = False, IsImpress = False, IsMath = False, IsWriter = False, - Keywords = True, Readonly = False, Subject = True, Title = True, XComponent = False) + serviceproperties = dict(CurrentSelection = True, CustomProperties = True, Description = True, + DocumentProperties = False, DocumentType = False, ExportFilters = False, + FileSystem = False, ImportFilters = False, IsBase = False, IsCalc = False, + IsDraw = False, IsFormDocument = False, IsImpress = False, IsMath = False, + IsWriter = False, Keywords = True, Readonly = False, Sheets = False, Subject = True, + Title = True, XComponent = False) # Force for each property to get its value from Basic - due to intense interactivity with user forceGetProperty = True @@ -2637,10 +2683,11 @@ class SFDocuments: serviceimplementation = 'basic' servicename = 'SFDocuments.Writer' servicesynonyms = ('writer', 'sfdocuments.writer') - serviceproperties = dict(Description = True, DocumentType = False, ExportFilters = False, FileSystem = False, - ImportFilters = False, IsBase = False, IsCalc = False, IsDraw = False, - IsFormDocument = False, IsImpress = False, IsMath = False, IsWriter = False, - Keywords = True, Readonly = False, Subject = True, Title = True, XComponent = False) + serviceproperties = dict(CustomProperties = True, Description = True, DocumentProperties = False, + DocumentType = False, ExportFilters = False, FileSystem = False, ImportFilters = False, + IsBase = False, IsCalc = False, IsDraw = False, IsFormDocument = False, + IsImpress = False, IsMath = False, IsWriter = False, Keywords = True, Readonly = False, + Subject = True, Title = True, XComponent = False) # Force for each property to get its value from Basic - due to intense interactivity with user forceGetProperty = True