Initial commit

This commit is contained in:
IRBorisov 2024-06-07 20:41:20 +03:00
commit 16f0811ef3
47 changed files with 2895 additions and 0 deletions

16
.gitignore vendored Normal file
View File

@ -0,0 +1,16 @@
.vscode
.vs
*.user
*.aps
~*
*.pch.tmp
*.clang.pch
*.dll
__pycache__
build/
whl/
venv/
output/
*egg-info

40
VBAMake.txt Normal file
View File

@ -0,0 +1,40 @@
# == Properties Section ==
# configuration properties
# use .ini format to define properties
# mandatory properties: name, artifact_home, source_home
id = PyRunner
name = PyRunner
description = Инструменты взаимодействия с Python
artifact_home = PyRunner
source_home = PyRunner
install_home = \\fs1.concept.ru\projects\10 Автоматизация деятельности\02 Офисная автоматизация\80 PyRunner
%%
# === Build section ===
# Available commands:
# build LOCAL_MANIFEST
# copy LOCAL_SOURCE -> [LOCAL_ARTIFACT]
# save_as LOCAL_ARTIFACT -> LOCAL_ARTIFACT
# run LOCAL_SOURCE.bat
copy script\DeployServer.ps1 -> script\DeployServer.ps1
copy src\test\data -> test
run script\Build.ps1
run script\DeployLocal.ps1
copy src\vbatopydll\dll -> src\vbatopydll\dll
copy src\vbatopy -> src\vbatopy
build script\PythonManager.txt
%%
# === Install section ==
# Available commands:
# install LOCAL_ARTIFACT -> [INSTALL_PATH]
# add_template LOCAL_ARTIFACT -> [LOCAL_TEMPLATE]
# run LOCAL_ARTIFACT.bat <- [PARAMETERS]
# run APPLICATION <- [PARAMETERS]
run script\DeployServer.ps1
install PythonManager.xlsm

41
script/Build.ps1 Normal file
View File

@ -0,0 +1,41 @@
# ====== Build Python ==========
Set-Location $PSScriptRoot\..\src\vbatopy
Write-Information 'Prepare Python environemnt...'
$packageName = 'vbatopy'
$output = "$PSScriptRoot\..\output"
$python = '.\venv\Scripts\python.exe'
if (-not (Test-Path -Path $python -PathType Leaf)) {
& 'python' -m venv .\venv
& $python -m pip install -r requirements.txt
& $python -m pip install -r requirements-build.txt
}
if (Test-Path -Path $output\$packageName) {
Remove-Item $output\$packageName -Recurse -Force
}
& $python -m build --outdir=$output\$packageName
# ======= Build CPP ===========
Set-Location $PSScriptRoot\..\src\vbatopydll
Write-Information 'Building cpp libraries...'
$solution = 'vbatopy.sln'
# TODO: find MSBuild instead - Implement in ConceptDeploy
$msbuild32 = 'C:\Program Files\Microsoft Visual Studio\2022\Community\Msbuild\Current\Bin\amd64\MSBuild.exe'
$msbuild64 = 'C:\Program Files\Microsoft Visual Studio\2022\Community\Msbuild\Current\Bin\MSBuild.exe'
if (-not (Test-Path -Path $msbuild32 -PathType Leaf) -or -not (Test-Path -Path $msbuild64 -PathType Leaf)) {
Write-Error "Missing MSBuild, please correct MSBuild path in $MyInvocation.MyCommand.Name"
Exit 1
}
& $msbuild32 @($solution, '/target:Clean', '/target:Build', '/property:Configuration=Release', '/property:Platform=Win32') | Write-Verbose
if ($LASTEXITCODE -ne 0) {
Write-Error 'Failed to build x86 dll...'
}
& $msbuild64 @($solution, '/target:Clean', '/target:Build', '/property:Configuration=Release', '/property:Platform=x64') | Write-Verbose
if ($LASTEXITCODE -ne 0) {
Write-Error 'Failed to build x64 dll...'
}

12
script/DeployServer.ps1 Normal file
View File

@ -0,0 +1,12 @@
# # Deploy VBAtoPY package to server storage and distribution
Set-Variable -Name PACKAGE_NAME -Value 'vbatopy' -Option Constant
Set-Variable -Name PROJECT_ROOT -Value "$PSScriptRoot\.." -Option Constant
Import-Module -Name "ConceptDeploy" -Force
# ======= Distribute Python ==========
Push-PythonPackage -PackageName $PACKAGE_NAME -PackageRoot $PROJECT_ROOT
# ======= Distribute Dll ==========
Push-DLL -dllName 'vbatopy-connector32' -dllPath "$PROJECT_ROOT\output\dll"
Push-DLL -dllName 'vbatopy-connector64' -dllPath "$PROJECT_ROOT\output\dll"

68
script/PythonManager.txt Normal file
View File

@ -0,0 +1,68 @@
# == Properties Section ==
# configuration properties
# use .ini format to define properties
# mandatory properties: name, artifact
name = PythonManager.xlsm
artifact = PythonManager.xlsm
%%
# === Imports Section ===
# Hierarchy of folders and files
# Use Tabulator to mark next level in hierarchy
# All folders are nested into SharedHome path
api
API_Python.cls
API_UserInteraction.cls
ex_Python.bas
ex_WinAPI.bas
utility
API_Config.cls
API_JSON.cls
API_Timer.cls
ex_VBA.bas
dev
DevTester.bas
%%
# === Source Code Section ==
# Hierarchy of folders and files
# Use Tabulator to mark next level in hierarchy
# All folders are nested into SourceHome path
src
manager
DevHelper.bas
Declarations.bas
Main.bas
MainImpl.bas
z_UIRibbon.bas
z_UIMessages.bas
test
s_Python.cls
%%
# ===== UI Section =======
# Pairs of path to UI elements, use " -> " delimiter
# First component is a path relative to SourceHome\ui folders
# Second component is internal path inside project file
.rels -> _rels\.rels
customUI.xml -> customUI\customUI.xml
%%
# === References Section ===
# List dependencies in one of the formats
# global : GLOBAL_NAME
# guid : {REGISTERED_GUID}
# file : PATH_TO_LIBRARY
global : Scripting
global : ADODB
global : Shell32

View File

@ -0,0 +1,4 @@
Attribute VB_Name = "Declarations"
Option Private Module
Option Explicit

30
src/manager/DevHelper.bas Normal file
View File

@ -0,0 +1,30 @@
Attribute VB_Name = "DevHelper"
'Option Private Module
Option Explicit
Public Function Dev_PrepareSkeleton()
' Do nothing
End Function
Public Sub Dev_ManualRunTest()
Dim sSuite$: sSuite = "s_Python"
Dim sTest$: sTest = "t_StartServer"
Dim sMsg$: sMsg = Dev_RunTestDebug(sSuite, sTest)
Debug.Print sMsg
Call MsgBox(sMsg)
End Sub
Public Function Dev_GetTestSuite(sName$) As Object
Select Case sName
Case "s_Python": Set Dev_GetTestSuite = New s_Python
' Case "s_ParseDate": Set Dev_GetTestSuite = New s_ParseDate
End Select
End Function
Public Function Dev_GetTestFolder() As String
Static sFolder$
If sFolder = vbNullString Then
sFolder = ThisWorkbook.Path & "\test"
End If
Dev_GetTestFolder = sFolder
End Function

78
src/manager/Main.bas Normal file
View File

@ -0,0 +1,78 @@
Attribute VB_Name = "Main"
Option Explicit
Public Sub RunGlobalConfig()
Dim fso As New Scripting.FileSystemObject
Dim sConfig$: sConfig = ConceptConfigPath
If Not fso.FileExists(sConfig) Then
Call UserInteraction.ShowMessage(EM_MISSING_CONFIG, sConfig)
Exit Sub
End If
Dim oShell As New Shell32.Shell
Call oShell.Open(sConfig)
End Sub
Public Sub RunGenerateWrapper()
Dim sSource$: sSource = UserInteraction.PromptFileFilter(ThisWorkbook.Path & "\", "Ôàéë Python", "*.py", "Âûáåðèòå èñõîäíèê")
If sSource = vbNullString Then _
Exit Sub
Dim fso As New Scripting.FileSystemObject
Dim sDestination$: sDestination = ThisWorkbook.Path & "\" & fso.GetBaseName(sSource) & ".bas"
Dim sResult$: sResult = CreatePythonWrapper(sSource, sDestination)
If sResult <> vbNullString Then _
Call UserInteraction.ShowMessage(IM_WRAPPER_OK, sResult)
End Sub
' ========
Sub BasicTest()
Dim iPython As API_Python: Set iPython = AccessPython
' Call iPython.Py.Exec("print(1)")
Debug.Print Py_double_sum(1, 2)
'Call iPython.UnloadDLL
' Call iPython.KillServer
' Call iPython.ResetCache
' Dim a
' a = iPython.Py.Bool(True)
' Call iPython.KillServer
' Debug.Print Py_hello("World")
' Call AccessPython.KillServer
' Call iPython.Py.ImportModule("test", 2)
' Call AccessPython.ResetCache
End Sub
Public Function Py_hello(pyArg_name)
Dim iPython As API_Python: Set iPython = AccessPython
On Error GoTo FAILED_CALL
Py_hello = iPython.CallFunction("standalone", "hello", Array(pyArg_name))
On Error GoTo 0
Exit Function
FAILED_CALL:
Py_hello = Err.Description
End Function
Public Function Py_double_sum(pyArg_x, pyArg_y)
Dim iPython As API_Python: Set iPython = AccessPython
On Error GoTo FAILED_CALL
Py_double_sum = iPython.CallFunction("standalone", "double_sum", Array(pyArg_x, pyArg_y))
On Error GoTo 0
Exit Function
FAILED_CALL:
Py_double_sum = Err.Description
End Function
Sub TestPythonCall()
Dim theTimer As New API_Timer
Call Py_double_sum(1, 2)
Call theTimer.Start
Dim n&
Dim nTest&
For n = 1 To 1000 Step 1
nTest = Py_double_sum(n, n + 1)
Next n
Debug.Print "Python test: " & theTimer.TimeStr
End Sub

21
src/manager/MainImpl.bas Normal file
View File

@ -0,0 +1,21 @@
Attribute VB_Name = "MainImpl"
Option Private Module
Option Explicit
Public Function CreatePythonWrapper(sSourceModule$, sDestination$) As String
Dim iPython As API_Python: Set iPython = AccessPython
On Error GoTo PROCESS_ERROR
CreatePythonWrapper = iPython.WrapPython(sSourceModule, sDestination)
On Error GoTo 0
Call iPython.KillServer
Exit Function
PROCESS_ERROR:
Debug.Print Err.Description
Call MsgBox(Err.Description & " " & Err.Number, vbCritical, "Error")
Call iPython.KillServer
On Error GoTo 0
End Function

View File

@ -0,0 +1,50 @@
Attribute VB_Name = "z_UIMessages"
' Messaging module
Option Private Module
Option Explicit
Public Enum MsgCode
EM_MISSING_CONFIG
IM_WRAPPER_OK
End Enum
Private g_UI As API_UserInteraction
Public Function UserInteraction() As API_UserInteraction
If g_UI Is Nothing Then _
Set g_UI = New API_UserInteraction
Set UserInteraction = g_UI
End Function
Public Function SetUserInteraction(newUI As API_UserInteraction)
Set g_UI = newUI
End Function
Public Function UIShowMessage(theCode As MsgCode, ParamArray params() As Variant)
Dim unwrapped As Variant: unwrapped = params
unwrapped = FixForwardedParams(unwrapped)
Select Case theCode
Case IM_WRAPPER_OK: Call MsgBox(Fmt("Óñïåøíî ñãåíåðèðîâàí VBA API: {1}", unwrapped), vbInformation)
Case EM_MISSING_CONFIG: Call MsgBox(Fmt("Íå óäàëîñü íàéòè ãëîáàëüíûé êîíôèã: {1}", unwrapped), vbExclamation)
Case Else: Call MsgBox("Íåâåðíûé êîä ñîîáùåíèÿ", vbCritical)
End Select
End Function
Public Function UIAskQuestion(theCode As MsgCode, ParamArray params() As Variant) As Boolean
Dim unwrapped As Variant: unwrapped = params
unwrapped = FixForwardedParams(unwrapped)
Dim answer&: answer = vbNo
Select Case theCode
'Case QM_CODE_DELETE_CONFIRM
' answer = MsgBox("Are you sure you want to delete ALL macros from target file?", vbYesNo + vbQuestion)
Case Else
Call MsgBox("Íåâåðíûé êîä ñîîáùåíèÿ", vbCritical)
End Select
UIAskQuestion = answer = vbYes
End Function

View File

@ -0,0 +1,10 @@
Attribute VB_Name = "z_UIRibbon"
Option Explicit
Option Private Module
Public Sub OnRibbonBtn(iControl As IRibbonControl)
Select Case iControl.ID
Case "GlobalConfig": Call RunGlobalConfig
Case "GenerateWrapper": Call RunGenerateWrapper
End Select
End Sub

View File

@ -0,0 +1,38 @@
def no_return():
pass
def get_42():
return 42
def hello(name):
return f"Hello {name}!"
def double_sum(x, y):
return 2 * (x + y)
def sum_array(x):
return sum(x)
def optional_arg(x=42):
return x
def return_tuple():
return (42, 'test')
def return_list():
return [42, 'test', 11]
def return_2dlist():
return [[1, 2, 42], ['test', 13], [11]]
def return_dict():
return {'a':42, 'b':'test', 'c':11}
def process_dict(arg):
return len(arg)
def extract_range_text(worksheet):
return worksheet.Cells(1,1).Text
def return_range_object(worksheet):
return worksheet.Cells(1,1)

View File

@ -0,0 +1,34 @@
Attribute VB_Name = "testWrap"
Option Private Module
Option Explicit
'Generated code by python_api - changes will be lost with next generation!
Public Sub Py_get_42()
Dim iPython as API_Python: Set iPython = AccessPython
On Error GoTo FAILED_CALL
Py_get_42 = iPython.CallFunction("testWrap", "get_42", Array())
On Error GoTo 0
Exit Sub
FAILED_CALL:
Py_get_42 = Err.Description
End Sub
Public Function Py_get_a_cell(pyArg_worksheet)
Dim iPython as API_Python: Set iPython = AccessPython
On Error GoTo FAILED_CALL
Set Py_get_a_cell = iPython.CallFunctionReturnObject("testWrap", "get_a_cell", Array(pyArg_worksheet))
On Error GoTo 0
Exit Function
FAILED_CALL:
Py_get_a_cell = Err.Description
End Function
Public Function Py_hello(pyArg_name)
Dim iPython as API_Python: Set iPython = AccessPython
On Error GoTo FAILED_CALL
Py_hello = iPython.CallFunction("testWrap", "hello", Array(pyArg_name))
On Error GoTo 0
Exit Function
FAILED_CALL:
Py_hello = Err.Description
End Function

14
src/test/data/testWrap.py Normal file
View File

@ -0,0 +1,14 @@
import vbatopy as vba
@vba.func
def hello(name):
return f"Hello {name}!"
@vba.sub
def get_42():
return 42
@vba.obj
@vba.func
def get_a_cell(worksheet):
return worksheet.Cells(1,1)

305
src/test/s_Python.cls Normal file
View File

@ -0,0 +1,305 @@
VERSION 1.0 CLASS
BEGIN
MultiUse = -1 'True
END
Attribute VB_Name = "s_Python"
Attribute VB_GlobalNameSpace = False
Attribute VB_Creatable = False
Attribute VB_PredeclaredId = False
Attribute VB_Exposed = False
Option Explicit
' TODO:
' Public Function RunPyStandalone(sPyCommand$, Optional sWorkDir$ = "") As Long
' Public Function RunPyFrozen(iPython$, Optional sArgs$ = "") As Long
' Public Function API_Python.Execute(sPyCommand$) As Boolean
' Public Function API_Python.Evaluate(sPyStatement$) As Variant
#If Win64 Then
Private Declare PtrSafe Function GetServer Lib "vbatopy-connector64.dll" (ByRef vResult As Variant) As Long
Private Declare PtrSafe Function KillPythonServer Lib "vbatopy-connector64.dll" Alias "KillServer" () As Long
#Else
Private Declare PtrSafe Function GetServer Lib "vbatopy-connector32.dll" (ByRef vResult As Variant) As Long
Private Declare PtrSafe Function KillPythonServer Lib "vbatopy-connector32.dll" Alias "KillServer" () As Long
#End If
Dim python_ As API_Python
Private Const TEST_FUNCS_FILE = "testFuncs.py"
Private Const TEST_WRAP_SOURCE = "testWrap.py"
Private Const TEST_WRAP_RESULT = "testWrap.bas"
Public Function Setup()
' Mandatory setup function
Set python_ = New API_Python
Call python_.Init(iPython:="python", sModules:=ConceptSourcePath, bDoDebug:=False)
Call python_.LoadDLL
End Function
Public Function Teardown()
' Mandatory teardown function
Call python_.KillServer
Set python_ = Nothing
End Function
Public Function t_StartServer()
On Error GoTo PROPAGATE_ERROR
Call Dev_NewCase("Invalid python")
Dim iTest As New API_Python
Call iTest.Init("invalid_python", ConceptSourcePath)
On Error Resume Next
Call iTest.StartServer
Call Dev_ExpectAnyError
On Error GoTo PROPAGATE_ERROR
Call Dev_NewCase("Missing server module")
Call iTest.Init("python", "", bDoDebug:=False)
On Error Resume Next
Call iTest.StartServer
Call Dev_ExpectAnyError
On Error GoTo PROPAGATE_ERROR
Call Dev_NewCase("Too long command")
Call iTest.Init("python", String$(10000, "A"))
On Error Resume Next
Call iTest.StartServer
Call Dev_ExpectAnyError
On Error GoTo PROPAGATE_ERROR
Call Dev_NewCase("Start valid")
Dim iServer As Variant: Call GetServer(iServer)
Call Dev_ExpectTrue(iServer Is Nothing, "Not started")
Call Dev_ExpectFalse(python_.Validate, "Not started")
Call python_.StartServer
Call GetServer(iServer)
Call Dev_ExpectTrue(python_.Validate, "After start")
Call Dev_ExpectFalse(iServer Is Nothing, "After start")
On Error Resume Next
Call python_.StartServer
Call Dev_ExpectNoError("Double start")
On Error GoTo PROPAGATE_ERROR
Call Dev_NewCase("Double python object")
Call iTest.Init("python", ConceptSourcePath)
On Error Resume Next
Call iTest.StartServer
Call Dev_ExpectNoError
On Error GoTo PROPAGATE_ERROR
Exit Function
PROPAGATE_ERROR:
Call Dev_LogError(Err.Number, Err.Description)
End Function
Public Function t_KillServer()
On Error GoTo PROPAGATE_ERROR
Call Dev_NewCase("Not started")
On Error Resume Next
Call python_.KillServer
Call Dev_ExpectNoError
On Error GoTo PROPAGATE_ERROR
Call Dev_NewCase("Valid kill")
Call python_.StartServer
Call python_.KillServer
Dim iServer As Variant: Call GetServer(iServer)
Call Dev_ExpectTrue(iServer Is Nothing)
Call Dev_ExpectFalse(python_.Validate)
On Error Resume Next
Call python_.KillServer
Call Dev_ExpectNoError("Dobule kill")
On Error GoTo PROPAGATE_ERROR
Call Dev_NewCase("Hijack and kill first")
Call python_.StartServer
Call KillPythonServer
On Error Resume Next
Call python_.KillServer
Call Dev_ExpectNoError
On Error GoTo PROPAGATE_ERROR
Exit Function
PROPAGATE_ERROR:
Call Dev_LogError(Err.Number, Err.Description)
End Function
Public Function t_RunPython()
On Error GoTo PROPAGATE_ERROR
Call Dev_NewCase("Not started")
Call Dev_ExpectFalse(python_.Validate)
Call Dev_ExpectTrue(python_.Execute("print(1)"))
Call Dev_ExpectTrue(python_.Validate)
Call Dev_NewCase("Already started")
Call Dev_ExpectTrue(python_.Execute("print(2)"))
Call Dev_NewCase("Invalid code")
Call Dev_ExpectFalse(python_.Execute("invalid_func_call(42, '42')"))
Call Dev_ExpectFalse(python_.Execute("do_something_illegal42"))
Exit Function
PROPAGATE_ERROR:
Call Dev_LogError(Err.Number, Err.Description)
End Function
Public Function t_LoadModule()
On Error GoTo PROPAGATE_ERROR
Dim sModule$: sModule = Dev_GetTestFolder & "\" & TEST_FUNCS_FILE
Call python_.StartServer
Call Dev_ExpectFalse(python_.LoadModule(""), "Empty input")
Call Dev_NewCase("Missing file")
Call Dev_ExpectFalse(python_.LoadModule(Dev_GetTestFolder & "\missingFile.py"))
Call Dev_NewCase("Valid load")
Call Dev_ExpectTrue(python_.LoadModule(sModule))
Call Dev_ExpectEQ("Hello world!", python_.CallFunction("testFuncs", "hello", Array("world")))
Call Dev_NewCase("Double load")
Call Dev_ExpectTrue(python_.LoadModule(sModule))
Exit Function
PROPAGATE_ERROR:
Call Dev_LogError(Err.Number, Err.Description)
End Function
Public Function t_CallFunctionInvalid()
On Error GoTo PROPAGATE_ERROR
Call python_.StartServer
Call Dev_ExpectTrue(python_.LoadModule(Dev_GetTestFolder & "\" & TEST_FUNCS_FILE))
Call Dev_NewCase("Invalid module")
Call Dev_ExpectLike(python_.CallFunction("invalid_module", "get_42"), "Unexpected Python Error*")
Call Dev_ExpectLike(python_.CallFunction("", "get_42"), "Unexpected Python Error*")
Call Dev_NewCase("Invalid function")
Call Dev_ExpectLike(python_.CallFunction("testFuncs", "invalid_function_name42"), "Unexpected Python Error*")
Call Dev_ExpectLike(python_.CallFunction("testFuncs", ""), "Unexpected Python Error*")
Call Dev_NewCase("Invalid arguments")
Call Dev_ExpectEQ("Hello world!", python_.CallFunction("testFuncs", "hello", Array("world")), "Unexpected Python Error*")
Call Dev_ExpectLike(python_.CallFunction("testFuncs", "hello"), "Unexpected Python Error*", "Missing argument")
Call Dev_ExpectLike(python_.CallFunction("testFuncs", "hello", "world"), "Unexpected Python Error*", "Invalid argument type - string")
Call Dev_ExpectLike(python_.CallFunction("testFuncs", "hello", ThisWorkbook), "Unexpected Python Error*", "Invalid argument type - object")
Call Dev_ExpectLike(python_.CallFunction("testFuncs", "hello", Array("world", "big")), "Unexpected Python Error*", "Invalid argument count")
Exit Function
PROPAGATE_ERROR:
Call Dev_LogError(Err.Number, Err.Description)
End Function
Public Function t_CallFunctionBasic()
On Error GoTo PROPAGATE_ERROR
Call python_.StartServer
Call Dev_ExpectTrue(python_.LoadModule(Dev_GetTestFolder & "\" & TEST_FUNCS_FILE))
Call Dev_NewCase("No return")
Call Dev_ExpectTrue(IsNull(python_.CallFunction("testFuncs", "no_return")))
Call Dev_NewCase("No arguments")
Call Dev_ExpectEQ(42, python_.CallFunction("testFuncs", "get_42"))
Call Dev_NewCase("Strings")
Call Dev_ExpectEQ("Hello world!", python_.CallFunction("testFuncs", "hello", Array("world")))
Call Dev_ExpectEQ("Hello 42!", python_.CallFunction("testFuncs", "hello", Array(42)))
Call Dev_NewCase("Multiple args")
Call Dev_ExpectEQ((13 + 5) * 2, python_.CallFunction("testFuncs", "double_sum", Array(13, 5)))
Call Dev_NewCase("Array arg")
Call Dev_ExpectEQ(42 + 1337 + 1, python_.CallFunction("testFuncs", "sum_array", Array(Array(42, 1337, 1))))
Call Dev_NewCase("Dictionary argument")
Call Dev_ExpectEQ(3, python_.CallFunction("testFuncs", "process_dict", Array(CSet(1, 2, 43))))
Call Dev_NewCase("Optional arg")
Call Dev_ExpectEQ(42, python_.CallFunction("testFuncs", "optional_arg"))
Call Dev_ExpectEQ(1337, python_.CallFunction("testFuncs", "optional_arg", Array(1337)))
Call Dev_NewCase("Multiple return")
Call Dev_ExpectEQ(Array(42, "test"), python_.CallFunction("testFuncs", "return_tuple"))
Call Dev_NewCase("Return list")
Call Dev_ExpectEQ(Array(42, "test", 11), python_.CallFunction("testFuncs", "return_list"))
Call Dev_NewCase("Return 2dlist")
Call Dev_ExpectEQ(Array(Array(1, 2, 42), Array("test", 13), Array(11)), python_.CallFunction("testFuncs", "return_2dlist"))
Call Dev_NewCase("Return dict")
Dim iTest As New Scripting.Dictionary
iTest("a") = 42
iTest("b") = "test"
iTest("c") = 11
Call Dev_ExpectEQ(iTest, python_.CallFunctionReturnObject("testFuncs", "return_dict"))
Exit Function
PROPAGATE_ERROR:
Call Dev_LogError(Err.Number, Err.Description)
End Function
Public Function t_CallFunctionObjects()
On Error GoTo PROPAGATE_ERROR
Call python_.StartServer
Call Dev_ExpectTrue(python_.LoadModule(Dev_GetTestFolder & "\" & TEST_FUNCS_FILE))
Call Dev_NewCase("Access VBA COM from Python")
Dim iCell As Excel.Range: Set iCell = ThisWorkbook.Sheets(1).Cells(1, 1)
iCell = "test"
Call Dev_ExpectEQ("test", python_.CallFunction("testFuncs", "extract_range_text", Array(ThisWorkbook.Sheets(1))))
iCell = ""
Call Dev_NewCase("Return VBA object from Python")
Dim iResult As Object
Set iResult = python_.CallFunctionReturnObject("testFuncs", "return_range_object", Array(ThisWorkbook.Sheets(1)))
iResult = "test"
Call Dev_ExpectEQ("test", iCell.Text)
iCell = ""
Exit Function
PROPAGATE_ERROR:
Call Dev_LogError(Err.Number, Err.Description)
End Function
Public Function t_WrapPython()
On Error GoTo PROPAGATE_ERROR
Dim fso As New Scripting.FileSystemObject
Dim sSource$: sSource = Dev_GetTestFolder & "\" & TEST_WRAP_SOURCE
Dim sDestination$: sDestination = ThisWorkbook.Path & "\" & TEST_WRAP_RESULT
Dim sTest$: sTest = Dev_GetTestFolder & "\" & TEST_WRAP_RESULT
Call python_.StartServer
Call Dev_AssertEQ(sDestination, python_.WrapPython(sSource, sDestination))
Call Dev_ExpectEQ(ReadFile(sTest), ReadFile(sDestination))
Call fso.DeleteFile(sDestination)
Exit Function
PROPAGATE_ERROR:
Call Dev_LogError(Err.Number, Err.Description)
End Function
' =====
Private Function ReadFile(sFile$) As String
Dim adoStream As New ADODB.Stream
adoStream.Charset = "utf-8"
Call adoStream.Open
On Error GoTo ERROR_FILE
Call adoStream.LoadFromFile(sFile)
On Error GoTo 0
ReadFile = adoStream.ReadText
ERROR_FILE:
Call adoStream.Close
On Error GoTo 0
End Function

635
src/vbatopy/.pylintrc Normal file
View File

@ -0,0 +1,635 @@
[MAIN]
# Analyse import fallback blocks. This can be used to support both Python 2 and
# 3 compatible code, which means that the block might have code that exists
# only in one or another interpreter, leading to false positives when analysed.
analyse-fallback-blocks=no
# Clear in-memory caches upon conclusion of linting. Useful if running pylint
# in a server-like mode.
clear-cache-post-run=no
# Load and enable all available extensions. Use --list-extensions to see a list
# all available extensions.
#enable-all-extensions=
# In error mode, messages with a category besides ERROR or FATAL are
# suppressed, and no reports are done by default. Error mode is compatible with
# disabling specific errors.
#errors-only=
# Always return a 0 (non-error) status code, even if lint errors are found.
# This is primarily useful in continuous integration scripts.
#exit-zero=
# A comma-separated list of package or module names from where C extensions may
# be loaded. Extensions are loading into the active Python interpreter and may
# run arbitrary code.
extension-pkg-allow-list=pyconcept
# A comma-separated list of package or module names from where C extensions may
# be loaded. Extensions are loading into the active Python interpreter and may
# run arbitrary code. (This is an alternative name to extension-pkg-allow-list
# for backward compatibility.)
extension-pkg-whitelist=
# Return non-zero exit code if any of these messages/categories are detected,
# even if score is above --fail-under value. Syntax same as enable. Messages
# specified are enabled, while categories only check already-enabled messages.
fail-on=
# Specify a score threshold under which the program will exit with error.
fail-under=10
# Interpret the stdin as a python script, whose filename needs to be passed as
# the module_or_package argument.
#from-stdin=
# Files or directories to be skipped. They should be base names, not paths.
ignore=CVS
# Add files or directories matching the regular expressions patterns to the
# ignore-list. The regex matches against paths and can be in Posix or Windows
# format. Because '\\' represents the directory delimiter on Windows systems,
# it can't be used as an escape character.
ignore-paths=t_.*,.*migrations.*
# Files or directories matching the regular expression patterns are skipped.
# The regex matches against base names, not paths. The default value ignores
# Emacs file locks
ignore-patterns=t_.*?py
# List of module names for which member attributes should not be checked
# (useful for modules/projects where namespaces are manipulated during runtime
# and thus existing member attributes cannot be deduced by static analysis). It
# supports qualified module names, as well as Unix pattern matching.
ignored-modules=
# Python code to execute, usually for sys.path manipulation such as
# pygtk.require().
#init-hook=
# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the
# number of processors available to use, and will cap the count on Windows to
# avoid hangs.
jobs=1
# Control the amount of potential inferred values when inferring a single
# object. This can help the performance when dealing with large functions or
# complex, nested conditions.
limit-inference-results=100
# List of plugins (as comma separated values of python module names) to load,
# usually to register additional checkers.
load-plugins=
# Pickle collected data for later comparisons.
persistent=yes
# Minimum Python version to use for version dependent checks. Will default to
# the version used to run pylint.
py-version=3.9
# Discover python modules and packages in the file system subtree.
recursive=no
# Add paths to the list of the source roots. Supports globbing patterns. The
# source root is an absolute path or a path relative to the current working
# directory used to determine a package namespace for modules located under the
# source root.
source-roots=
# When enabled, pylint would attempt to guess common misconfiguration and emit
# user-friendly hints instead of false-positive error messages.
suggestion-mode=yes
# Allow loading of arbitrary C extensions. Extensions are imported into the
# active Python interpreter and may run arbitrary code.
unsafe-load-any-extension=no
# In verbose mode, extra non-checker-related info will be displayed.
#verbose=
[BASIC]
# Naming style matching correct argument names.
argument-naming-style=snake_case
# Regular expression matching correct argument names. Overrides argument-
# naming-style. If left empty, argument names will be checked with the set
# naming style.
#argument-rgx=
# Naming style matching correct attribute names.
attr-naming-style=snake_case
# Regular expression matching correct attribute names. Overrides attr-naming-
# style. If left empty, attribute names will be checked with the set naming
# style.
#attr-rgx=
# Bad variable names which should always be refused, separated by a comma.
bad-names=foo,
bar,
baz,
toto,
tutu,
tata
# Bad variable names regexes, separated by a comma. If names match any regex,
# they will always be refused
bad-names-rgxs=
# Naming style matching correct class attribute names.
class-attribute-naming-style=any
# Regular expression matching correct class attribute names. Overrides class-
# attribute-naming-style. If left empty, class attribute names will be checked
# with the set naming style.
#class-attribute-rgx=
# Naming style matching correct class constant names.
class-const-naming-style=UPPER_CASE
# Regular expression matching correct class constant names. Overrides class-
# const-naming-style. If left empty, class constant names will be checked with
# the set naming style.
#class-const-rgx=
# Naming style matching correct class names.
class-naming-style=PascalCase
# Regular expression matching correct class names. Overrides class-naming-
# style. If left empty, class names will be checked with the set naming style.
#class-rgx=
# Naming style matching correct constant names.
const-naming-style=UPPER_CASE
# Regular expression matching correct constant names. Overrides const-naming-
# style. If left empty, constant names will be checked with the set naming
# style.
#const-rgx=
# Minimum line length for functions/classes that require docstrings, shorter
# ones are exempt.
docstring-min-length=-1
# Naming style matching correct function names.
function-naming-style=snake_case
# Regular expression matching correct function names. Overrides function-
# naming-style. If left empty, function names will be checked with the set
# naming style.
#function-rgx=
# Good variable names which should always be accepted, separated by a comma.
good-names=i,
j,
k,
ex,
Run,
_
# Good variable names regexes, separated by a comma. If names match any regex,
# they will always be accepted
good-names-rgxs=
# Include a hint for the correct naming format with invalid-name.
include-naming-hint=no
# Naming style matching correct inline iteration names.
inlinevar-naming-style=any
# Regular expression matching correct inline iteration names. Overrides
# inlinevar-naming-style. If left empty, inline iteration names will be checked
# with the set naming style.
#inlinevar-rgx=
# Naming style matching correct method names.
method-naming-style=snake_case
# Regular expression matching correct method names. Overrides method-naming-
# style. If left empty, method names will be checked with the set naming style.
#method-rgx=
# Naming style matching correct module names.
module-naming-style=snake_case
# Regular expression matching correct module names. Overrides module-naming-
# style. If left empty, module names will be checked with the set naming style.
#module-rgx=
# Colon-delimited sets of names that determine each other's naming style when
# the name regexes allow several styles.
name-group=
# Regular expression which should only match function or class names that do
# not require a docstring.
no-docstring-rgx=^_
# List of decorators that produce properties, such as abc.abstractproperty. Add
# to this list to register other decorators that produce valid properties.
# These decorators are taken in consideration only for invalid-name.
property-classes=abc.abstractproperty
# Regular expression matching correct type alias names. If left empty, type
# alias names will be checked with the set naming style.
#typealias-rgx=
# Regular expression matching correct type variable names. If left empty, type
# variable names will be checked with the set naming style.
#typevar-rgx=
# Naming style matching correct variable names.
variable-naming-style=snake_case
# Regular expression matching correct variable names. Overrides variable-
# naming-style. If left empty, variable names will be checked with the set
# naming style.
#variable-rgx=
[CLASSES]
# Warn about protected attribute access inside special methods
check-protected-access-in-special-methods=no
# List of method names used to declare (i.e. assign) instance attributes.
defining-attr-methods=__init__,
__new__,
setUp,
asyncSetUp,
__post_init__
# List of member names, which should be excluded from the protected access
# warning.
exclude-protected=_asdict,_fields,_replace,_source,_make,os._exit
# List of valid names for the first argument in a class method.
valid-classmethod-first-arg=cls
# List of valid names for the first argument in a metaclass class method.
valid-metaclass-classmethod-first-arg=mcs
[DESIGN]
# List of regular expressions of class ancestor names to ignore when counting
# public methods (see R0903)
exclude-too-few-public-methods=
# List of qualified class names to ignore when counting class parents (see
# R0901)
ignored-parents=
# Maximum number of arguments for function / method.
max-args=5
# Maximum number of attributes for a class (see R0902).
max-attributes=7
# Maximum number of boolean expressions in an if statement (see R0916).
max-bool-expr=5
# Maximum number of branch for function / method body.
max-branches=12
# Maximum number of locals for function / method body.
max-locals=15
# Maximum number of parents for a class (see R0901).
max-parents=7
# Maximum number of public methods for a class (see R0904).
max-public-methods=20
# Maximum number of return / yield for function / method body.
max-returns=6
# Maximum number of statements in function / method body.
max-statements=50
# Minimum number of public methods for a class (see R0903).
min-public-methods=2
[EXCEPTIONS]
# Exceptions that will emit a warning when caught.
overgeneral-exceptions=builtins.BaseException,builtins.Exception
[FORMAT]
# Expected format of line ending, e.g. empty (any line ending), LF or CRLF.
expected-line-ending-format=
# Regexp for a line that is allowed to be longer than the limit.
ignore-long-lines=^\s*(# )?<?https?://\S+>?$
# Number of spaces of indent required inside a hanging or continued line.
indent-after-paren=4
# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1
# tab).
indent-string=' '
# Maximum number of characters on a single line.
max-line-length=100
# Maximum number of lines in a module.
max-module-lines=1000
# Allow the body of a class to be on the same line as the declaration if body
# contains single statement.
single-line-class-stmt=no
# Allow the body of an if to be on the same line as the test if there is no
# else.
single-line-if-stmt=no
[IMPORTS]
# List of modules that can be imported at any level, not just the top level
# one.
allow-any-import-level=
# Allow explicit reexports by alias from a package __init__.
allow-reexport-from-package=no
# Allow wildcard imports from modules that define __all__.
allow-wildcard-with-all=no
# Deprecated modules which should not be used, separated by a comma.
deprecated-modules=
# Output a graph (.gv or any supported image format) of external dependencies
# to the given file (report RP0402 must not be disabled).
ext-import-graph=
# Output a graph (.gv or any supported image format) of all (i.e. internal and
# external) dependencies to the given file (report RP0402 must not be
# disabled).
import-graph=
# Output a graph (.gv or any supported image format) of internal dependencies
# to the given file (report RP0402 must not be disabled).
int-import-graph=
# Force import order to recognize a module as part of the standard
# compatibility libraries.
known-standard-library=
# Force import order to recognize a module as part of a third party library.
known-third-party=enchant
# Couples of modules and preferred modules, separated by a comma.
preferred-modules=
[LOGGING]
# The type of string formatting that logging methods do. `old` means using %
# formatting, `new` is for `{}` formatting.
logging-format-style=old
# Logging modules to check that the string format arguments are in logging
# function parameter format.
logging-modules=logging
[MESSAGES CONTROL]
# Only show warnings with the listed confidence levels. Leave empty to show
# all. Valid levels: HIGH, CONTROL_FLOW, INFERENCE, INFERENCE_FAILURE,
# UNDEFINED.
confidence=
# Disable the message, report, category or checker with the given id(s). You
# can either give multiple identifiers separated by comma (,) or put this
# option multiple times (only on the command line, not in the configuration
# file where it should appear only once). You can also use "--disable=all" to
# disable everything first and then re-enable specific checks. For example, if
# you want to run only the similarities checker, you can use "--disable=all
# --enable=similarities". If you want to run only the classes checker, but have
# no Warning level messages displayed, use "--disable=all --enable=classes
# --disable=W".
disable=too-many-public-methods,
invalid-name,
no-else-break,
no-else-continue,
no-else-return,
no-member,
too-many-ancestors,
too-many-return-statements,
too-many-locals,
too-many-instance-attributes,
too-few-public-methods,
unused-argument,
missing-function-docstring,
attribute-defined-outside-init,
ungrouped-imports,
abstract-method
# Enable the message, report, category or checker with the given id(s). You can
# either give multiple identifier separated by comma (,) or put this option
# multiple time (only on the command line, not in the configuration file where
# it should appear only once). See also the "--disable" option for examples.
enable=c-extension-no-member
[METHOD_ARGS]
# List of qualified names (i.e., library.method) which require a timeout
# parameter e.g. 'requests.api.get,requests.api.post'
timeout-methods=requests.api.delete,requests.api.get,requests.api.head,requests.api.options,requests.api.patch,requests.api.post,requests.api.put,requests.api.request
max-line-length=120
[MISCELLANEOUS]
# List of note tags to take in consideration, separated by a comma.
notes=FIXME,
XXX,
TODO
# Regular expression of note tags to take in consideration.
notes-rgx=
[REFACTORING]
# Maximum number of nested blocks for function / method body
max-nested-blocks=5
# Complete name of functions that never returns. When checking for
# inconsistent-return-statements if a never returning function is called then
# it will be considered as an explicit return statement and no message will be
# printed.
never-returning-functions=sys.exit,argparse.parse_error
[REPORTS]
# Python expression which should return a score less than or equal to 10. You
# have access to the variables 'fatal', 'error', 'warning', 'refactor',
# 'convention', and 'info' which contain the number of messages in each
# category, as well as 'statement' which is the total number of statements
# analyzed. This score is used by the global evaluation report (RP0004).
evaluation=max(0, 0 if fatal else 10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10))
# Template used to display messages. This is a python new-style format string
# used to format the message information. See doc for all details.
msg-template=
# Set the output format. Available formats are text, parseable, colorized, json
# and msvs (visual studio). You can also give a reporter class, e.g.
# mypackage.mymodule.MyReporterClass.
#output-format=
# Tells whether to display a full report or only the messages.
reports=no
# Activate the evaluation score.
score=yes
[SIMILARITIES]
# Comments are removed from the similarity computation
ignore-comments=yes
# Docstrings are removed from the similarity computation
ignore-docstrings=yes
# Imports are removed from the similarity computation
ignore-imports=yes
# Signatures are removed from the similarity computation
ignore-signatures=yes
# Minimum lines number of a similarity.
min-similarity-lines=4
[SPELLING]
# Limits count of emitted suggestions for spelling mistakes.
max-spelling-suggestions=4
# Spelling dictionary name. No available dictionaries : You need to install
# both the python package and the system dependency for enchant to work..
spelling-dict=
# List of comma separated words that should be considered directives if they
# appear at the beginning of a comment and should not be checked.
spelling-ignore-comment-directives=fmt: on,fmt: off,noqa:,noqa,nosec,isort:skip,mypy:
# List of comma separated words that should not be checked.
spelling-ignore-words=
# A path to a file that contains the private dictionary; one word per line.
spelling-private-dict-file=
# Tells whether to store unknown words to the private dictionary (see the
# --spelling-private-dict-file option) instead of raising a message.
spelling-store-unknown-words=no
[STRING]
# This flag controls whether inconsistent-quotes generates a warning when the
# character used as a quote delimiter is used inconsistently within a module.
check-quote-consistency=no
# This flag controls whether the implicit-str-concat should generate a warning
# on implicit string concatenation in sequences defined over several lines.
check-str-concat-over-line-jumps=no
[TYPECHECK]
# List of decorators that produce context managers, such as
# contextlib.contextmanager. Add to this list to register other decorators that
# produce valid context managers.
contextmanager-decorators=contextlib.contextmanager
# List of members which are set dynamically and missed by pylint inference
# system, and so shouldn't trigger E1101 when accessed. Python regular
# expressions are accepted.
generated-members=
# Tells whether to warn about missing members when the owner of the attribute
# is inferred to be None.
ignore-none=yes
# This flag controls whether pylint should warn about no-member and similar
# checks whenever an opaque object is returned when inferring. The inference
# can return multiple potential results while evaluating a Python object, but
# some branches might not be evaluated, which results in partial inference. In
# that case, it might be useful to still emit no-member and other checks for
# the rest of the inferred objects.
ignore-on-opaque-inference=yes
# List of symbolic message names to ignore for Mixin members.
ignored-checks-for-mixins=no-member,
not-async-context-manager,
not-context-manager,
attribute-defined-outside-init
# List of class names for which member attributes should not be checked (useful
# for classes with dynamically set attributes). This supports the use of
# qualified names.
ignored-classes=optparse.Values,thread._local,_thread._local,argparse.Namespace
# Show a hint with possible names when a member name was not found. The aspect
# of finding the hint is based on edit distance.
missing-member-hint=yes
# The minimum edit distance a name should have in order to be considered a
# similar match for a missing member name.
missing-member-hint-distance=1
# The total number of similar names that should be taken in consideration when
# showing a hint for a missing member.
missing-member-max-choices=1
# Regex pattern to define which classes are considered mixins.
mixin-class-rgx=.*[Mm]ixin
# List of decorators that change the signature of a decorated function.
signature-mutators=
[VARIABLES]
# List of additional names supposed to be defined in builtins. Remember that
# you should avoid defining new builtins when possible.
additional-builtins=
# Tells whether unused global variables should be treated as a violation.
allow-global-unused-variables=yes
# List of names allowed to shadow builtins
allowed-redefined-builtins=
# List of strings which can identify a callback function by name. A callback
# name must start or end with one of those strings.
callbacks=cb_,
_cb
# A regular expression matching the name of dummy variables (i.e. expected to
# not be used).
dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_
# Argument names that match this expression will be ignored.
ignored-argument-names=_.*|^ignored_|^unused_
# Tells whether we should check for unused import in __init__ files.
init-import=no
# List of qualified module names which can have objects that can redefine
# builtins.
redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io

21
src/vbatopy/LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 CIHT CONCEPT, IRBorisov
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

1
src/vbatopy/README.md Normal file
View File

@ -0,0 +1 @@
Long description goes here

1
src/vbatopy/VERSION Normal file
View File

@ -0,0 +1 @@
1.2.0

7
src/vbatopy/mypy.ini Normal file
View File

@ -0,0 +1,7 @@
# Global options:
[mypy]
warn_return_any = True
warn_unused_configs = True
# Per-module options:

View File

@ -0,0 +1,3 @@
[build-system]
requires = ["setuptools", "wheel"]
build-backend = "setuptools.build_meta"

View File

@ -0,0 +1,4 @@
wheel
pylint
mypy
build

View File

@ -0,0 +1 @@
pywin32>=304

16
src/vbatopy/setup.cfg Normal file
View File

@ -0,0 +1,16 @@
[metadata]
name = vbatopy
version = file: VERSION
author = CIHT CONCEPT, IRBorisov
author_email = iborisov@acconcept.ru
description = Python launcher for VBA
long_description = file: README.md
long_description_content_type = text/markdown
license = MIT
classifiers =
Programming Language :: Python :: 3
[options]
packages = find:
install_requires =
pywin32

4
src/vbatopy/setup.py Normal file
View File

@ -0,0 +1,4 @@
''' vbatopy package installer '''
from setuptools import setup
setup()

View File

@ -0,0 +1,7 @@
from .server import start_server
from .decorators import (
vba_func as func,
vba_sub as sub,
vba_object as obj
)

View File

@ -0,0 +1,58 @@
import pythoncom
from win32com.client import Dispatch
from .python_api import reload_module, preload_module, call_function, wrap_python
# Note: change encoding here if needed. Warning: VBA doesn't support UTF-8 encoded modules!
_windows_cyrillic_encoding = 'cp1251'
class ComPython:
_public_methods_ = ['CallFunction', 'ReloadModule', 'ImportModule', 'WrapPython', 'Eval', 'Exec']
def CallFunction(self, module_name, func_name: str, args=None):
"""Call python function"""
unwrapped_args = None if args is None else tuple(_unwrap_com(arg) for arg in args)
result = call_function(module_name, func_name, unwrapped_args)
return _wrap_com(result)
def ReloadModule(self, module_name: str):
"""Reload imported module"""
return reload_module(module_name)
def ImportModule(self, module_path: str):
"""Load module"""
return preload_module(module_path)
def WrapPython(self, source_module: str, destination_file: str):
""""Create wrapper for decorated python module functions"""
return wrap_python(source_module, destination_file, encoding=_windows_cyrillic_encoding)
def Eval(self, expression: str):
"""Evaluate expression in current context"""
result = eval(expression)
return _wrap_com(result)
def Exec(self, statement: str):
"""Execute statement in current context"""
exec(statement)
def _unwrap_com(com_value):
value_type = type(com_value)
if value_type is pythoncom.TypeIIDs[pythoncom.IID_IDispatch]:
return Dispatch(com_value)
return com_value
def _wrap_com(py_value):
value_type = type(py_value)
if value_type is tuple:
return [element for element in py_value]
elif value_type is dict:
result = Dispatch('Scripting.Dictionary')
for (key, value) in py_value.items():
result.Add(key, value)
return result
return py_value

View File

@ -0,0 +1,95 @@
import functools
import inspect
def vba_func(func):
@functools.wraps(func)
def inner(func):
if not hasattr(func, '__vba__'):
func.__vba__ = describe_function(func)
func.__vba__['sub'] = False
return func
return inner(func)
def vba_sub(func):
@functools.wraps(func)
def inner(func):
if not hasattr(func, '__vba__'):
func.__vba__ = describe_function(func)
func.__vba__['sub'] = True
return func
return inner(func)
def vba_object(func):
@functools.wraps(func)
def inner(func):
func.__vba__['object'] = True
return func
return inner(func)
def describe_function(func):
info = {}
info['name'] = func.__name__
info['sub'] = False
info['object'] = False
(args, argsmap) = _describe_function_args(func)
info['args'] = args
info['argsmap'] = argsmap
return info
def extract_function_description(func):
if hasattr(func, '__vba__'):
return func.__vba__
else:
return describe_function(func)
def _describe_function_args(func):
func_sig = _extract_function_args(func)
nArgs = len(func_sig['args'])
nDefaults = len(func_sig['defaults'])
nRequiredArgs = nArgs - nDefaults
if func_sig['vararg'] and nDefaults > 0:
raise Exception("Not supported: function with both optional and variable length arguments")
args = []
argsmap = {}
for (vpos, vname) in enumerate(func_sig['args']):
arg_info = {
'name': vname,
'pos': vpos,
'vararg': vname == func_sig['vararg'],
'options': {}
}
if vpos >= nRequiredArgs:
arg_info['optional'] = func_sig['defaults'][vpos - nRequiredArgs]
args.append(arg_info)
argsmap[vname] = args[-1]
return (args, argsmap)
def _extract_function_args(func):
signature = inspect.signature(func)
vararg = None
args = []
defaults = []
for arg in signature.parameters.values():
if arg.kind is inspect.Parameter.POSITIONAL_OR_KEYWORD:
args.append(arg.name)
if arg.default is not inspect.Signature.empty:
defaults.append(arg.default)
elif arg.kind is inspect.Parameter.VAR_POSITIONAL:
args.append(arg.name)
vararg = arg.name
else:
raise Exception("Not supported: keyword arguments")
return {
'args': args,
'defaults': defaults,
'vararg': vararg
}

View File

@ -0,0 +1,131 @@
import sys
import os
from importlib import import_module, reload
from .writer import VBAWriter
from .decorators import extract_function_description
def reload_module(module_name: str):
reload(_get_module(module_name))
def preload_module(module_path: str) -> str:
(folder, file) = os.path.split(module_path)
if folder not in sys.path:
sys.path.append(folder)
module_name = os.path.splitext(file)[0]
_get_module(module_name)
return module_name
def call_function(module_name, func_name, args=None):
module = _get_module(module_name)
func = getattr(module, func_name)
if args is not None:
result = func(*args)
else:
result = func()
return result
def wrap_python(source: str, destination: str, encoding: str) -> str:
source_module = preload_module(source)
out_file = open(destination, 'w+', encoding=encoding)
(folder, file) = os.path.split(destination)
vba = VBAWriter(out_file)
vba.line(f'Attribute VB_Name = "{os.path.splitext(file)[0]}"')
vba.line('Option Private Module')
vba.line('Option Explicit')
vba.line("'Generated code by python_api - changes will be lost with next generation!")
_wrap_module(source_module, vba)
out_file.close()
return destination
def _wrap_module(module_name: str, vba: VBAWriter):
module = _get_module(module_name)
for attribute_item in map(lambda attr: getattr(module, attr), dir(module)):
if not hasattr(attribute_item, '__vba__'):
continue
info = extract_function_description(attribute_item)
_wrap_function(module_name, info, vba)
def _wrap_function(module_name, info, vba: VBAWriter):
fname = f"Py_{info['name']}"
ftype = 'Sub' if info['sub'] else 'Function'
(func_sig, vararg) = _generate_function_signature(ftype, fname, info)
args_vba = _wrap_vararg(vararg, info['args'], vba)
vba.line('')
vba.start_block(func_sig)
vba.line("Dim iPython as API_Python: Set iPython = AccessPython")
vba.line("On Error GoTo FAILED_CALL")
if info['object']:
vba.line(f"""Set {fname} = iPython.CallFunctionReturnObject("{module_name}", "{info['name']}", {args_vba})""")
else:
vba.line(f"""{fname} = iPython.CallFunction("{module_name}", "{info['name']}", {args_vba})""")
vba.line("On Error GoTo 0")
vba.line(f"Exit {ftype}")
vba.label("FAILED_CALL")
vba.line(f"{fname} = Err.Description")
vba.end_block('End ' + ftype)
def _wrap_vararg(vararg, args, vba: VBAWriter) -> str:
if vararg == '':
return 'Array(' + ', '.join(arg['name'] for arg in args) + ')'
vba.line("Dim argsArray() As Variant")
non_varargs = [arg['name'] for arg in args if not arg['vararg']]
vba.line("argsArray = Array(%s)" % tuple({', '.join(non_varargs)}))
vba.line(f"ReDim Preserve argsArray(0 to UBound({vararg}) - LBound({vararg}) + {len(non_varargs)})")
vba.line("Dim k&")
vba.line(f"For k = LBound({vararg}) To UBound({vararg})")
vba.line(f"argsArray({len(non_varargs)} + k - LBound({vararg})) = {vararg}(k)")
vba.line("Next k")
return 'argsArray'
def _generate_function_signature(ftype: str, fname: str, info):
args = ''
first = True
vararg = ''
for arg in info['args']:
argname = f"pyArg_{arg['name']}"
arg['name'] = argname
if not first:
args += ', '
if 'optional' in arg:
args += 'Optional '
elif arg['vararg']:
args += 'ParamArray '
vararg = argname
args += argname
if arg['vararg']:
args += '()'
first = False
return (f'Public {ftype} {fname}({args})', vararg)
# Imported module caching
_loaded_modules = {}
def _get_module(module_name):
module_info = _loaded_modules.get(module_name, None)
if module_info is not None:
module = module_info['module']
else:
module = import_module(module_name)
filename = os.path.normcase(module.__file__.lower())
_loaded_modules[module_name] = {'filename': filename, 'module': module}
return module

View File

@ -0,0 +1,66 @@
import logging
import win32api
import win32event
import pythoncom
import pywintypes
import win32com.server.util as server_util
import win32com.server.policy as server_policy
from .comwrappers import ComPython
logger = logging.getLogger(__name__)
def start_server(clsid_str):
"""Start the COM server, clsid_str is the dispatcher object GUID created in DLL module"""
class_id = pywintypes.IID(clsid_str)
_override_com_policy(class_id)
(factory, revokeId) = _register_class_object(class_id)
pythoncom.EnableQuitMessage(win32api.GetCurrentThreadId())
pythoncom.CoInitialize()
pythoncom.CoResumeClassObjects()
_run_python_loop(class_id)
pythoncom.CoRevokeClassObject(revokeId)
pythoncom.CoUninitialize()
def _override_com_policy(class_id):
class WrapPythonPolicy(server_policy.DefaultPolicy):
def _CreateInstance_(self, request_clsid, dispatch_id):
if request_clsid == class_id:
return server_util.wrap(ComPython(), dispatch_id)
else:
return server_policy.DefaultPolicy._CreateInstance_(self, request_clsid, dispatch_id)
server_policy.DefaultPolicy = WrapPythonPolicy
def _register_class_object(class_id):
factory = pythoncom.MakePyFactory(class_id)
revokeId = pythoncom.CoRegisterClassObject(
class_id, factory,
pythoncom.CLSCTX_LOCAL_SERVER,
pythoncom.REGCLS_MULTIPLEUSE | pythoncom.REGCLS_SUSPENDED
)
return (factory, revokeId)
def _run_python_loop(class_id):
msg = f"Python server running, class_id={class_id}"
if logger.hasHandlers():
logger.info(msg)
else:
print(msg)
while True:
rc = win32event.MsgWaitForMultipleObjects(
(), # list of objects
0, # wait for all = False
win32event.INFINITE, # no timeout
win32event.QS_ALLEVENTS # accept any input
)
if rc == win32event.WAIT_OBJECT_0:
if pythoncom.PumpWaitingMessages():
break # wm_quit

View File

@ -0,0 +1,22 @@
class VBAWriter:
''' Class for creating VBA code '''
def __init__(self, out_file):
self._output = out_file
self._indent = 0
def start_block(self, block_header):
self.line(block_header)
self._indent += 1
def end_block(self, block_footer):
self._indent -= 1
self.line(block_footer)
def label(self, label_name):
indent = self._indent
self._indent = 0
self.line(label_name + ':')
self._indent = indent
def line(self, text_line):
self._output.write((' ' * self._indent) + text_line + '\n')

View File

@ -0,0 +1,43 @@
Checks: "*,\
-llvmlibc-implementation-in-namespace,\
-llvmlibc-callee-namespace,\
-llvmlibc-restrict-system-libc-headers,\
-llvm-else-after-return,\
-bugprone-branch-clone,\
-bugprone-suspicious-include,\
-modernize-use-trailing-return-type,\
-modernize-avoid-c-arrays,\
-hicpp-special-member-functions,\
-hicpp-multiway-paths-covered,\
-hicpp-signed-bitwise,\
-hicpp-avoid-c-arrays,\
-hicpp-vararg,\
-llvm-header-guard,\
-google-runtime-references,\
-google-runtime-int,\
-llvm-include-order,\
-fuchsia-overloaded-operator,\
-fuchsia-default-arguments,\
-google-readability-todo,\
-google-global-names-in-headers,\
-readability-redundant-access-specifiers,\
-readability-else-after-return,\
-readability-implicit-bool-conversion,\
-readability-use-anyofallof,\
-performance-no-int-to-ptr,\
-performance-inefficient-string-concatenation,\
-performance-unnecessary-value-param,\
-fuchsia-default-arguments-declarations,\
-fuchsia-trailing-return,\
-fuchsia-multiple-inheritance,\
-fuchsia-default-arguments-calls,\
-misc-non-private-member-variables-in-classes,\
-misc-no-recursion,\
-cppcoreguidelines-pro-type-union-access,\
-cppcoreguidelines-pro-type-reinterpret-cast,\
-cppcoreguidelines-macro-usage,\
-cppcoreguidelines-pro-type-vararg,\
-cppcoreguidelines-special-member-functions,\
-cppcoreguidelines-avoid-c-arrays,\
-cppcoreguidelines-pro-type-cstyle-cast,\
-cppcoreguidelines-non-private-member-variables-in-classes"

View File

@ -0,0 +1,42 @@
#pragma once
namespace vbatopy {
class Server {
public:
// Dispatcher interface is provided by server process through COM CoCreateInstance
IDispatch* dispatcher{ nullptr };
GUID clsid{};
private:
HANDLE jobHandle{};
HANDLE pyProcess{};
HANDLE pyThread{};
bool showConsole{ false };
std::string execCommand{};
Server();
~Server();
public:
static Server& Instance();
void Setup(std::string command, bool showConsole_);
[[nodiscard]] bool CheckStatus() const;
void Start();
void Kill();
void ForceKill();
private:
void ReleaseDispatch();
void ReleaseKillerJob();
void CreateInternalProcess();
void CreateKillerJob();
void ActivateDispatcher();
void SubstituteMacro(std::string& path) const;
};
} // namespace vbatopy

View File

@ -0,0 +1,32 @@
#pragma once
namespace vbatopy::utils {
void ToVariant(const char* str, VARIANT* var);
std::string GUIDToStr(GUID guid);
std::string GetLastErrorMessage();
struct AutoCloseHandle {
HANDLE handle{ nullptr };
explicit AutoCloseHandle(HANDLE handle) {
this->handle = handle;
}
~AutoCloseHandle() {
if (handle != nullptr && handle != INVALID_HANDLE_VALUE) {
CloseHandle(handle);
}
}
HANDLE Release() {
HANDLE res = handle;
handle = nullptr;
return res;
}
};
void ApplyPythonEscaping(std::string& target);
std::string GeneratePythonCommand(std::string pythonExe, std::string sources, GUID clsid);
} // namespace vbatopy::utils

View File

@ -0,0 +1,18 @@
#pragma once
#ifdef VBATOPY_EXPORTS
#define DLLEXPORT __declspec(dllexport)
#else
#define DLLEXPORT __declspec(dllimport)
#endif
DLLEXPORT HRESULT __stdcall GetServer(VARIANT* pyResult);
// Start server using @command argument. $(CLSID) macro will be expanded into dispatcher GUID
DLLEXPORT HRESULT __stdcall StartServer(VARIANT* pyResult, const char* command, int showConsole);
// Start server for @python executable and add @sources to os.path
// Requires: @vbatopy should be available for import
DLLEXPORT HRESULT __stdcall StartPythonServer(VARIANT* pyResult, const char* python, const char* sources, int showConsole);
DLLEXPORT HRESULT __stdcall KillServer();

12
src/vbatopydll/pch.h Normal file
View File

@ -0,0 +1,12 @@
#pragma once
#include <windows.h>
// Note: using std::format increases dll filesize by ~150 KB, but makes code more readable
// Using barebone string concatenation for error messages and cmd construction doesnt impact perfomance
// Consider not using std::format if dll size is critical
#include <format>
#include "utils.h"
#include "server.h"
#include "vbatopy.h"

View File

@ -0,0 +1 @@
#include "pch.h"

View File

@ -0,0 +1,151 @@
#include "pch.h"
namespace vbatopy {
Server::Server() {
if (FAILED(::CoCreateGuid(&clsid))) {
throw std::runtime_error("CoCreateGuid failed.");
}
}
Server::~Server() {
this->ForceKill();
}
Server& Server::Instance() {
static Server py{};
return py;
}
void Server::Setup(std::string command, const bool showConsole_) {
this->Kill();
showConsole = showConsole_;
execCommand = std::move(command);
SubstituteMacro(execCommand);
}
bool Server::CheckStatus() const {
if (dispatcher == nullptr) {
return false;
}
UINT info{};
return dispatcher->GetTypeInfoCount(&info) == S_OK;
}
void Server::Kill() {
ReleaseDispatch();
ReleaseKillerJob();
pyThread = nullptr;
pyProcess = nullptr;
}
void Server::ForceKill() {
ReleaseKillerJob();
dispatcher = nullptr;
pyThread = nullptr;
pyProcess = nullptr;
}
void Server::Start() {
CreateInternalProcess();
utils::AutoCloseHandle hPiThread(pyThread);
utils::AutoCloseHandle hPiProcess(pyProcess);
CreateKillerJob();
utils::AutoCloseHandle hJobAuto(jobHandle);
ActivateDispatcher();
jobHandle = hJobAuto.Release();
}
void Server::ReleaseDispatch() {
if (dispatcher != nullptr) {
dispatcher->Release();
dispatcher = nullptr;
}
}
void Server::ReleaseKillerJob() {
if (jobHandle != nullptr) {
::CloseHandle(jobHandle);
jobHandle = nullptr;
}
}
void Server::SubstituteMacro(std::string& path) const {
static const auto MACRO_LEN = 8U;
const auto position = path.find("$(CLSID)", 0);
if (position == std::string::npos) {
return;
}
path = path.substr(0, position) + utils::GUIDToStr(clsid) + path.substr(position + MACRO_LEN, path.length() - position - MACRO_LEN);
}
void Server::CreateInternalProcess() {
static constexpr auto CMD_INPUT_MAX_LENGTH = 8000U;
const DWORD windowFlags = showConsole ? NORMAL_PRIORITY_CLASS | CREATE_BREAKAWAY_FROM_JOB :
NORMAL_PRIORITY_CLASS | CREATE_BREAKAWAY_FROM_JOB | CREATE_NO_WINDOW;
if (execCommand.length() > CMD_INPUT_MAX_LENGTH) {
throw std::length_error("Start server command is too long");
}
char cmdLine[CMD_INPUT_MAX_LENGTH] = "";
strcat_s(cmdLine, execCommand.c_str());
STARTUPINFOA si;
ZeroMemory(&si, sizeof(si));
si.cb = sizeof(si);
PROCESS_INFORMATION pi;
ZeroMemory(&pi, sizeof(pi));
if (!::CreateProcessA(
nullptr,
cmdLine, // NOLINT(cppcoreguidelines-pro-bounds-array-to-pointer-decay, hicpp-no-array-decay)
nullptr, nullptr, TRUE, windowFlags, nullptr, nullptr,
&si, &pi)) {
throw std::runtime_error(std::format("Could not create Python: {}\nCommand{}", utils::GetLastErrorMessage(), execCommand));
}
pyProcess = pi.hProcess;
pyThread = pi.hThread;
}
void Server::CreateKillerJob() {
utils::AutoCloseHandle hJobAuto{ CreateJobObject(nullptr, nullptr) };
if (hJobAuto.handle == nullptr) {
throw std::runtime_error("CreateJobObject failed: " + utils::GetLastErrorMessage());
}
JOBOBJECT_EXTENDED_LIMIT_INFORMATION jxli;
ZeroMemory(&jxli, sizeof(jxli));
jxli.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
if (!::SetInformationJobObject(hJobAuto.handle, JobObjectExtendedLimitInformation, &jxli, sizeof(jxli))) {
throw std::runtime_error("SetInformationJobObject failed: " + utils::GetLastErrorMessage());
}
if (!::AssignProcessToJobObject(hJobAuto.handle, pyProcess)) {
throw std::runtime_error("AssignProcessToJobObject failed: " + utils::GetLastErrorMessage());
}
jobHandle = hJobAuto.Release();
}
void Server::ActivateDispatcher() {
static constexpr auto RETRY_COUNT = 600U;
static constexpr auto RETRY_PERIOD_MS = 100U;
for (uint16_t attempt = 0; attempt < RETRY_COUNT; ++attempt) {
if (::CoCreateInstance(clsid, nullptr, CLSCTX_LOCAL_SERVER, IID_IDispatch, reinterpret_cast<void**>(&(dispatcher))) != REGDB_E_CLASSNOTREG) {
return;
}
DWORD dwExitCode{};
if (!::GetExitCodeProcess(pyProcess, &dwExitCode)) {
throw std::runtime_error("GetExitCodeProcess failed: " + utils::GetLastErrorMessage());
}
if (dwExitCode != STILL_ACTIVE) {
throw std::runtime_error("Cmd process exited before it was possible to create the interface object.\nCommand: " + execCommand);
}
::Sleep(RETRY_PERIOD_MS);
}
throw std::runtime_error("Could not activate Python COM server");
}
} // namespace vbatopy

View File

@ -0,0 +1,59 @@
#include "pch.h"
namespace vbatopy::utils {
std::string GUIDToStr(const GUID guid) {
return std::format("{{{:08x}-{:04x}-{:04x}-{:02x}{:02x}-{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}}}",
guid.Data1, guid.Data2, guid.Data3,
guid.Data4[0], guid.Data4[1],
guid.Data4[2], guid.Data4[3], guid.Data4[4],
guid.Data4[5], guid.Data4[6], guid.Data4[7]); // NOLINT(cppcoreguidelines-avoid-magic-numbers, readability-magic-numbers)
}
void ToVariant(const char* str, VARIANT* var) {
::VariantClear(var);
int sz = static_cast<int>(strlen(str)) + 1;
auto wideStr = std::make_unique<OLECHAR[]>(sz);
::MultiByteToWideChar(CP_ACP, 0, str, static_cast<int>(sz * sizeof(OLECHAR)), wideStr.get(), sz);
var->vt = VT_BSTR;
var->bstrVal = SysAllocString(wideStr.get());
}
std::string GetLastErrorMessage() {
DWORD dwError = ::GetLastError();
char* buffer{};
if (0 == ::FormatMessageA(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
nullptr, dwError, 0, reinterpret_cast<LPSTR>(&buffer), 0, nullptr)) {
return "Could not get error message: FormatMessage failed.";
}
auto errorMessage = std::string{ buffer };
::LocalFree(buffer);
return errorMessage;
}
void ApplyPythonEscaping(std::string& target) {
std::string result{};
result.reserve(size(target));
for (const auto& c : target) {
if (c == '\\') {
result += R"(\\)";
} else if (c == '&') {
result += "^&";
} else {
result += c;
}
}
if (size(target) != size(result)) {
target = result;
}
}
std::string GeneratePythonCommand(std::string pythonExe, std::string sources, const GUID clsid) {
ApplyPythonEscaping(sources);
return std::format(R"(cmd.exe /C {} -B -c )"
R"("import sys, os; sys.path[0:0]=os.path.normcase(os.path.expandvars(\"{}\")).split(';'); )"
R"(import vbatopy; vbatopy.server.start_server('{}') ")", pythonExe, sources, GUIDToStr(clsid));
}
} // namespace vbatopy::utils

View File

@ -0,0 +1,95 @@
#include "pch.h"
using vbatopy::Server;
// DLL entry point
BOOL WINAPI DllMain(HINSTANCE /*hinstDLL*/, DWORD fdwReason, LPVOID /*lpvReserved*/) {
switch (fdwReason) {
case DLL_PROCESS_ATTACH:
return TRUE;
case DLL_PROCESS_DETACH:
Server::Instance().ForceKill();
return TRUE;
default:
return TRUE;
}
}
DLLEXPORT HRESULT __stdcall GetServer(VARIANT* pyResult) {
try {
::VariantClear(pyResult);
auto& server = Server::Instance();
if (server.CheckStatus()) {
pyResult->vt = VT_DISPATCH;
server.dispatcher->QueryInterface<IDispatch>(&pyResult->pdispVal);
return S_OK;
} else {
pyResult->vt = VT_DISPATCH;
pyResult->pdispVal = nullptr;
return S_OK;
}
} catch (const std::exception& e) {
vbatopy::utils::ToVariant(e.what(), pyResult);
return E_FAIL;
}
}
DLLEXPORT HRESULT __stdcall StartServer(VARIANT* pyResult, const char* command, int showConsole) {
try {
::VariantClear(pyResult);
auto& server = Server::Instance();
if (server.CheckStatus()) {
throw std::runtime_error("Server already running");
}
server.Setup(command, showConsole != 0);
server.Start();
pyResult->vt = VT_DISPATCH;
server.dispatcher->QueryInterface<IDispatch>(&pyResult->pdispVal);
return S_OK;
} catch (const std::exception& e) {
vbatopy::utils::ToVariant(e.what(), pyResult);
return E_FAIL;
}
}
DLLEXPORT HRESULT __stdcall StartPythonServer(VARIANT* pyResult, const char* python, const char* sources, int showConsole) {
try {
::VariantClear(pyResult);
if (python == nullptr || strlen(python) == 0) {
throw std::invalid_argument("Python command missing");
}
auto& server = Server::Instance();
if (server.CheckStatus()) {
throw std::runtime_error("Server already running");
}
server.Setup(vbatopy::utils::GeneratePythonCommand(python, sources, server.clsid), showConsole != 0);
server.Start();
pyResult->vt = VT_DISPATCH;
server.dispatcher->QueryInterface<IDispatch>(&pyResult->pdispVal);
return S_OK;
} catch (const std::exception& e) {
vbatopy::utils::ToVariant(e.what(), pyResult);
return E_FAIL;
}
}
DLLEXPORT HRESULT __stdcall KillServer() {
try {
auto& server = Server::Instance();
server.Kill();
return S_OK;
} catch (const std::exception& /*e*/) {
return E_FAIL;
}
}

View File

@ -0,0 +1,7 @@
LIBRARY
EXPORTS
GetServer
StartPythonServer
StartServer
KillServer

View File

@ -0,0 +1,28 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Express 2013 for Windows Desktop
VisualStudioVersion = 12.0.31101.0
MinimumVisualStudioVersion = 10.0.40219.1
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "vbatopy", "vbatopy.vcxproj", "{2959049E-520D-4930-8408-C578B3302724}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Win32 = Debug|Win32
Debug|x64 = Debug|x64
Release|Win32 = Release|Win32
Release|x64 = Release|x64
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{2959049E-520D-4930-8408-C578B3302724}.Debug|Win32.ActiveCfg = Debug|Win32
{2959049E-520D-4930-8408-C578B3302724}.Debug|Win32.Build.0 = Debug|Win32
{2959049E-520D-4930-8408-C578B3302724}.Debug|x64.ActiveCfg = Debug|x64
{2959049E-520D-4930-8408-C578B3302724}.Debug|x64.Build.0 = Debug|x64
{2959049E-520D-4930-8408-C578B3302724}.Release|Win32.ActiveCfg = Release|Win32
{2959049E-520D-4930-8408-C578B3302724}.Release|Win32.Build.0 = Release|Win32
{2959049E-520D-4930-8408-C578B3302724}.Release|x64.ActiveCfg = Release|x64
{2959049E-520D-4930-8408-C578B3302724}.Release|x64.Build.0 = Release|x64
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
EndGlobal

View File

@ -0,0 +1,209 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" ToolsVersion="14.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup Label="ProjectConfigurations">
<ProjectConfiguration Include="Debug|Win32">
<Configuration>Debug</Configuration>
<Platform>Win32</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Debug|x64">
<Configuration>Debug</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|Win32">
<Configuration>Release</Configuration>
<Platform>Win32</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|x64">
<Configuration>Release</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
</ItemGroup>
<PropertyGroup Label="Globals">
<ProjectGuid>{2959049E-520D-4930-8408-C578B3302724}</ProjectGuid>
<Keyword>Win32Proj</Keyword>
<RootNamespace>vbatopy</RootNamespace>
<WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'" Label="Configuration">
<ConfigurationType>DynamicLibrary</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
<CharacterSet>Unicode</CharacterSet>
<PlatformToolset>v143</PlatformToolset>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'" Label="Configuration">
<ConfigurationType>DynamicLibrary</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
<CharacterSet>Unicode</CharacterSet>
<PlatformToolset>v143</PlatformToolset>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'" Label="Configuration">
<ConfigurationType>DynamicLibrary</ConfigurationType>
<UseDebugLibraries>false</UseDebugLibraries>
<WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>Unicode</CharacterSet>
<PlatformToolset>v143</PlatformToolset>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'" Label="Configuration">
<ConfigurationType>DynamicLibrary</ConfigurationType>
<UseDebugLibraries>false</UseDebugLibraries>
<WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>Unicode</CharacterSet>
<PlatformToolset>v143</PlatformToolset>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<ImportGroup Label="ExtensionSettings">
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'" Label="PropertySheets">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'" Label="PropertySheets">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<PropertyGroup Label="UserMacros" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
<LinkIncremental>true</LinkIncremental>
<OutDir>build\x86\$(Configuration)\dll\</OutDir>
<IntDir>build\x86\$(Configuration)\</IntDir>
<TargetName>vbatopy-connector32</TargetName>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<LinkIncremental>true</LinkIncremental>
<OutDir>build\x64\$(Configuration)\dll\</OutDir>
<IntDir>build\x64\$(Configuration)\</IntDir>
<TargetName>vbatopy-connector64</TargetName>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
<LinkIncremental>false</LinkIncremental>
<OutDir>..\..\output\dll\</OutDir>
<IntDir>build\x86\$(Configuration)\</IntDir>
<TargetName>vbatopy-connector32</TargetName>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<LinkIncremental>false</LinkIncremental>
<OutDir>..\..\output\dll\</OutDir>
<IntDir>build\x64\$(Configuration)\</IntDir>
<TargetName>vbatopy-connector64</TargetName>
</PropertyGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
<ClCompile>
<WarningLevel>Level4</WarningLevel>
<Optimization>Disabled</Optimization>
<AdditionalIncludeDirectories>.\;include;header</AdditionalIncludeDirectories>
<PreprocessorDefinitions>_DEBUG;_WINDOWS;_USRDLL;VBATOPY_EXPORTS;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<RuntimeLibrary>MultiThreadedDebugDLL</RuntimeLibrary>
<LanguageStandard>stdcpplatest</LanguageStandard>
<ConformanceMode>true</ConformanceMode>
<TranslateIncludes>false</TranslateIncludes>
<PrecompiledHeader>Use</PrecompiledHeader>
<PrecompiledHeaderFile>pch.h</PrecompiledHeaderFile>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
<ModuleDefinitionFile>vbatopy.def</ModuleDefinitionFile>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<ClCompile>
<WarningLevel>Level4</WarningLevel>
<Optimization>Disabled</Optimization>
<AdditionalIncludeDirectories>.\;include;header</AdditionalIncludeDirectories>
<PreprocessorDefinitions>_DEBUG;_WINDOWS;_USRDLL;VBATOPY_EXPORTS;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<RuntimeLibrary>MultiThreadedDebugDLL</RuntimeLibrary>
<LanguageStandard>stdcpplatest</LanguageStandard>
<ConformanceMode>true</ConformanceMode>
<TranslateIncludes>false</TranslateIncludes>
<PrecompiledHeader>Use</PrecompiledHeader>
<PrecompiledHeaderFile>pch.h</PrecompiledHeaderFile>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
<ModuleDefinitionFile>vbatopy.def</ModuleDefinitionFile>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
<ClCompile>
<WarningLevel>Level4</WarningLevel>
<Optimization>MaxSpeed</Optimization>
<AdditionalIncludeDirectories>.\;include;header</AdditionalIncludeDirectories>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
<PreprocessorDefinitions>NDEBUG;_WINDOWS;_USRDLL;VBATOPY_EXPORTS;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<RuntimeLibrary>MultiThreadedDLL</RuntimeLibrary>
<LanguageStandard>stdcpplatest</LanguageStandard>
<ConformanceMode>true</ConformanceMode>
<TranslateIncludes>false</TranslateIncludes>
<PrecompiledHeader>Use</PrecompiledHeader>
<PrecompiledHeaderFile>pch.h</PrecompiledHeaderFile>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
<EnableCOMDATFolding>true</EnableCOMDATFolding>
<OptimizeReferences>true</OptimizeReferences>
<ModuleDefinitionFile>vbatopy.def</ModuleDefinitionFile>
</Link>
<PostBuildEvent>
<Command>
</Command>
</PostBuildEvent>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<ClCompile>
<WarningLevel>Level4</WarningLevel>
<Optimization>MaxSpeed</Optimization>
<AdditionalIncludeDirectories>.\;include;header</AdditionalIncludeDirectories>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
<PreprocessorDefinitions>NDEBUG;_WINDOWS;_USRDLL;VBATOPY_EXPORTS;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<RuntimeLibrary>MultiThreadedDLL</RuntimeLibrary>
<LanguageStandard>stdcpplatest</LanguageStandard>
<ConformanceMode>true</ConformanceMode>
<TranslateIncludes>false</TranslateIncludes>
<PrecompiledHeader>Use</PrecompiledHeader>
<PrecompiledHeaderFile>pch.h</PrecompiledHeaderFile>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
<EnableCOMDATFolding>true</EnableCOMDATFolding>
<OptimizeReferences>true</OptimizeReferences>
<ModuleDefinitionFile>vbatopy.def</ModuleDefinitionFile>
</Link>
<PostBuildEvent>
<Command>
</Command>
</PostBuildEvent>
</ItemDefinitionGroup>
<ItemGroup>
<ClCompile Include="src\pch.cpp">
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">Create</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">Create</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">Create</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">Create</PrecompiledHeader>
</ClCompile>
<ClCompile Include="src\server.cpp" />
<ClCompile Include="src\utils.cpp" />
<ClCompile Include="src\vbatopy.cpp" />
</ItemGroup>
<ItemGroup>
<ClInclude Include="header\server.h" />
<ClInclude Include="header\utils.h" />
<ClInclude Include="include\vbatopy.h" />
<ClInclude Include="pch.h" />
</ItemGroup>
<ItemGroup>
<None Include="vbatopy.def" />
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<ImportGroup Label="ExtensionTargets">
</ImportGroup>
</Project>

View File

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<ClCompile Include="src\vbatopy.cpp">
<Filter>src</Filter>
</ClCompile>
<ClCompile Include="src\utils.cpp">
<Filter>src</Filter>
</ClCompile>
<ClCompile Include="src\server.cpp">
<Filter>src</Filter>
</ClCompile>
<ClCompile Include="src\pch.cpp">
<Filter>src</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<Filter Include="src">
<UniqueIdentifier>{874e5ad6-697b-4d2c-a192-cf4b49cb244c}</UniqueIdentifier>
</Filter>
</ItemGroup>
<ItemGroup>
<ClInclude Include="include\vbatopy.h">
<Filter>src</Filter>
</ClInclude>
<ClInclude Include="header\server.h">
<Filter>src</Filter>
</ClInclude>
<ClInclude Include="header\utils.h">
<Filter>src</Filter>
</ClInclude>
<ClInclude Include="pch.h" />
</ItemGroup>
<ItemGroup>
<None Include="vbatopy.def" />
</ItemGroup>
</Project>

305
test/s_Python.cls Normal file
View File

@ -0,0 +1,305 @@
VERSION 1.0 CLASS
BEGIN
MultiUse = -1 'True
END
Attribute VB_Name = "s_Python"
Attribute VB_GlobalNameSpace = False
Attribute VB_Creatable = False
Attribute VB_PredeclaredId = False
Attribute VB_Exposed = False
Option Explicit
' TODO:
' Public Function RunPyStandalone(sPyCommand$, Optional sWorkDir$ = "") As Long
' Public Function RunPyFrozen(iPython$, Optional sArgs$ = "") As Long
' Public Function API_Python.Execute(sPyCommand$) As Boolean
' Public Function API_Python.Evaluate(sPyStatement$) As Variant
#If Win64 Then
Private Declare PtrSafe Function GetServer Lib "vbatopy-connector64.dll" (ByRef vResult As Variant) As Long
Private Declare PtrSafe Function KillPythonServer Lib "vbatopy-connector64.dll" Alias "KillServer" () As Long
#Else
Private Declare PtrSafe Function GetServer Lib "vbatopy-connector32.dll" (ByRef vResult As Variant) As Long
Private Declare PtrSafe Function KillPythonServer Lib "vbatopy-connector32.dll" Alias "KillServer" () As Long
#End If
Dim python_ As API_Python
Private Const TEST_FUNCS_FILE = "testFuncs.py"
Private Const TEST_WRAP_SOURCE = "testWrap.py"
Private Const TEST_WRAP_RESULT = "testWrap.bas"
Public Function Setup()
' Mandatory setup function
Set python_ = New API_Python
Call python_.Init(iPython:=PY_DEFVALUE_INTERPRETER, sModules:="", bDoDebug:=False)
Call python_.LoadDLL
End Function
Public Function Teardown()
' Mandatory teardown function
Call python_.KillServer
Set python_ = Nothing
End Function
Public Function t_StartServer()
On Error GoTo PROPAGATE_ERROR
Call Dev_NewCase("Invalid python")
Dim iTest As New API_Python
Call iTest.Init("invalid_python", "")
On Error Resume Next
Call iTest.StartServer
Call Dev_ExpectAnyError
On Error GoTo PROPAGATE_ERROR
Call Dev_NewCase("Missing server module")
Call iTest.Init("python", "", bDoDebug:=False)
On Error Resume Next
Call iTest.StartServer
Call Dev_ExpectAnyError
On Error GoTo PROPAGATE_ERROR
Call Dev_NewCase("Too long command")
Call iTest.Init("python", String$(10000, "A"))
On Error Resume Next
Call iTest.StartServer
Call Dev_ExpectAnyError
On Error GoTo PROPAGATE_ERROR
Call Dev_NewCase("Start valid")
Dim iServer As Variant: Call GetServer(iServer)
Call Dev_ExpectTrue(iServer Is Nothing, "Not started")
Call Dev_ExpectFalse(python_.Validate, "Not started")
Call python_.StartServer
Call GetServer(iServer)
Call Dev_ExpectTrue(python_.Validate, "After start")
Call Dev_ExpectFalse(iServer Is Nothing, "After start")
On Error Resume Next
Call python_.StartServer
Call Dev_ExpectNoError("Double start")
On Error GoTo PROPAGATE_ERROR
Call Dev_NewCase("Double python object")
Call iTest.Init("python", "")
On Error Resume Next
Call iTest.StartServer
Call Dev_ExpectNoError
On Error GoTo PROPAGATE_ERROR
Exit Function
PROPAGATE_ERROR:
Call Dev_LogError(Err.Number, Err.Description)
End Function
Public Function t_KillServer()
On Error GoTo PROPAGATE_ERROR
Call Dev_NewCase("Not started")
On Error Resume Next
Call python_.KillServer
Call Dev_ExpectNoError
On Error GoTo PROPAGATE_ERROR
Call Dev_NewCase("Valid kill")
Call python_.StartServer
Call python_.KillServer
Dim iServer As Variant: Call GetServer(iServer)
Call Dev_ExpectTrue(iServer Is Nothing)
Call Dev_ExpectFalse(python_.Validate)
On Error Resume Next
Call python_.KillServer
Call Dev_ExpectNoError("Dobule kill")
On Error GoTo PROPAGATE_ERROR
Call Dev_NewCase("Hijack and kill first")
Call python_.StartServer
Call KillPythonServer
On Error Resume Next
Call python_.KillServer
Call Dev_ExpectNoError
On Error GoTo PROPAGATE_ERROR
Exit Function
PROPAGATE_ERROR:
Call Dev_LogError(Err.Number, Err.Description)
End Function
Public Function t_RunPython()
On Error GoTo PROPAGATE_ERROR
Call Dev_NewCase("Not started")
Call Dev_ExpectFalse(python_.Validate)
Call Dev_ExpectTrue(python_.Execute("print(1)"))
Call Dev_ExpectTrue(python_.Validate)
Call Dev_NewCase("Already started")
Call Dev_ExpectTrue(python_.Execute("print(2)"))
Call Dev_NewCase("Invalid code")
Call Dev_ExpectFalse(python_.Execute("invalid_func_call(42, '42')"))
Call Dev_ExpectFalse(python_.Execute("do_something_illegal42"))
Exit Function
PROPAGATE_ERROR:
Call Dev_LogError(Err.Number, Err.Description)
End Function
Public Function t_LoadModule()
On Error GoTo PROPAGATE_ERROR
Dim sModule$: sModule = Dev_GetTestFolder & "\" & TEST_FUNCS_FILE
Call python_.StartServer
Call Dev_ExpectFalse(python_.LoadModule(""), "Empty input")
Call Dev_NewCase("Missing file")
Call Dev_ExpectFalse(python_.LoadModule(Dev_GetTestFolder & "\missingFile.py"))
Call Dev_NewCase("Valid load")
Call Dev_ExpectTrue(python_.LoadModule(sModule))
Call Dev_ExpectEQ("Hello world!", python_.CallFunction("testFuncs", "hello", Array("world")))
Call Dev_NewCase("Double load")
Call Dev_ExpectTrue(python_.LoadModule(sModule))
Exit Function
PROPAGATE_ERROR:
Call Dev_LogError(Err.Number, Err.Description)
End Function
Public Function t_CallFunctionInvalid()
On Error GoTo PROPAGATE_ERROR
Call python_.StartServer
Call Dev_ExpectTrue(python_.LoadModule(Dev_GetTestFolder & "\" & TEST_FUNCS_FILE))
Call Dev_NewCase("Invalid module")
Call Dev_ExpectLike(python_.CallFunction("invalid_module", "get_42"), "Unexpected Python Error*")
Call Dev_ExpectLike(python_.CallFunction("", "get_42"), "Unexpected Python Error*")
Call Dev_NewCase("Invalid function")
Call Dev_ExpectLike(python_.CallFunction("testFuncs", "invalid_function_name42"), "Unexpected Python Error*")
Call Dev_ExpectLike(python_.CallFunction("testFuncs", ""), "Unexpected Python Error*")
Call Dev_NewCase("Invalid arguments")
Call Dev_ExpectEQ("Hello world!", python_.CallFunction("testFuncs", "hello", Array("world")), "Unexpected Python Error*")
Call Dev_ExpectLike(python_.CallFunction("testFuncs", "hello"), "Unexpected Python Error*", "Missing argument")
Call Dev_ExpectLike(python_.CallFunction("testFuncs", "hello", "world"), "Unexpected Python Error*", "Invalid argument type - string")
Call Dev_ExpectLike(python_.CallFunction("testFuncs", "hello", ThisWorkbook), "Unexpected Python Error*", "Invalid argument type - object")
Call Dev_ExpectLike(python_.CallFunction("testFuncs", "hello", Array("world", "big")), "Unexpected Python Error*", "Invalid argument count")
Exit Function
PROPAGATE_ERROR:
Call Dev_LogError(Err.Number, Err.Description)
End Function
Public Function t_CallFunctionBasic()
On Error GoTo PROPAGATE_ERROR
Call python_.StartServer
Call Dev_ExpectTrue(python_.LoadModule(Dev_GetTestFolder & "\" & TEST_FUNCS_FILE))
Call Dev_NewCase("No return")
Call Dev_ExpectTrue(IsNull(python_.CallFunction("testFuncs", "no_return")))
Call Dev_NewCase("No arguments")
Call Dev_ExpectEQ(42, python_.CallFunction("testFuncs", "get_42"))
Call Dev_NewCase("Strings")
Call Dev_ExpectEQ("Hello world!", python_.CallFunction("testFuncs", "hello", Array("world")))
Call Dev_ExpectEQ("Hello 42!", python_.CallFunction("testFuncs", "hello", Array(42)))
Call Dev_NewCase("Multiple args")
Call Dev_ExpectEQ((13 + 5) * 2, python_.CallFunction("testFuncs", "double_sum", Array(13, 5)))
Call Dev_NewCase("Array arg")
Call Dev_ExpectEQ(42 + 1337 + 1, python_.CallFunction("testFuncs", "sum_array", Array(Array(42, 1337, 1))))
Call Dev_NewCase("Dictionary argument")
Call Dev_ExpectEQ(3, python_.CallFunction("testFuncs", "process_dict", Array(CSet(1, 2, 43))))
Call Dev_NewCase("Optional arg")
Call Dev_ExpectEQ(42, python_.CallFunction("testFuncs", "optional_arg"))
Call Dev_ExpectEQ(1337, python_.CallFunction("testFuncs", "optional_arg", Array(1337)))
Call Dev_NewCase("Multiple return")
Call Dev_ExpectEQ(Array(42, "test"), python_.CallFunction("testFuncs", "return_tuple"))
Call Dev_NewCase("Return list")
Call Dev_ExpectEQ(Array(42, "test", 11), python_.CallFunction("testFuncs", "return_list"))
Call Dev_NewCase("Return 2dlist")
Call Dev_ExpectEQ(Array(Array(1, 2, 42), Array("test", 13), Array(11)), python_.CallFunction("testFuncs", "return_2dlist"))
Call Dev_NewCase("Return dict")
Dim iTest As New Scripting.Dictionary
iTest("a") = 42
iTest("b") = "test"
iTest("c") = 11
Call Dev_ExpectEQ(iTest, python_.CallFunctionReturnObject("testFuncs", "return_dict"))
Exit Function
PROPAGATE_ERROR:
Call Dev_LogError(Err.Number, Err.Description)
End Function
Public Function t_CallFunctionObjects()
On Error GoTo PROPAGATE_ERROR
Call python_.StartServer
Call Dev_ExpectTrue(python_.LoadModule(Dev_GetTestFolder & "\" & TEST_FUNCS_FILE))
Call Dev_NewCase("Access VBA COM from Python")
Dim iCell As Excel.Range: Set iCell = ThisWorkbook.Sheets(1).Cells(1, 1)
iCell = "test"
Call Dev_ExpectEQ("test", python_.CallFunction("testFuncs", "extract_range_text", Array(ThisWorkbook.Sheets(1))))
iCell = ""
Call Dev_NewCase("Return VBA object from Python")
Dim iResult As Object
Set iResult = python_.CallFunctionReturnObject("testFuncs", "return_range_object", Array(ThisWorkbook.Sheets(1)))
iResult = "test"
Call Dev_ExpectEQ("test", iCell.Text)
iCell = ""
Exit Function
PROPAGATE_ERROR:
Call Dev_LogError(Err.Number, Err.Description)
End Function
Public Function t_WrapPython()
On Error GoTo PROPAGATE_ERROR
Dim fso As New Scripting.FileSystemObject
Dim sSource$: sSource = Dev_GetTestFolder & "\" & TEST_WRAP_SOURCE
Dim sDestination$: sDestination = ThisWorkbook.Path & "\" & TEST_WRAP_RESULT
Dim sTest$: sTest = Dev_GetTestFolder & "\" & TEST_WRAP_RESULT
Call python_.StartServer
Call Dev_AssertEQ(sDestination, python_.WrapPython(sSource, sDestination))
Call Dev_ExpectEQ(ReadFile(sTest), ReadFile(sDestination))
Call fso.DeleteFile(sDestination)
Exit Function
PROPAGATE_ERROR:
Call Dev_LogError(Err.Number, Err.Description)
End Function
' =====
Private Function ReadFile(sFile$) As String
Dim adoStream As New ADODB.Stream
adoStream.Charset = "utf-8"
Call adoStream.Open
On Error GoTo ERROR_FILE
Call adoStream.LoadFromFile(sFile)
On Error GoTo 0
ReadFile = adoStream.ReadText
ERROR_FILE:
Call adoStream.Close
On Error GoTo 0
End Function

2
ui/.rels Normal file
View File

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="xl/workbook.xml"/><Relationship Id="rId6" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties" Target="docProps/app.xml"/><Relationship Id="rId5" Type="http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties" Target="docProps/core.xml"/><Relationship Id="rId4" Type="http://schemas.microsoft.com/office/2006/relationships/ui/extensibility" Target="customUI/customUI.xml"/></Relationships>

21
ui/customUI.xml Normal file
View File

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<customUI xmlns="http://schemas.microsoft.com/office/2006/01/customui">
<ribbon>
<tabs>
<tab id="PythonManager" label="Python">
<group id="Actions" label="Действия">
<button id="GlobalConfig" size="large"
label="Config"
supertip="Открыть глобальный конфиг"
imageMso="BlogPublishDraft"
onAction="OnRibbonBtn"/>
<button id="GenerateWrapper" size="large"
label="Create Wrapper"
supertip="Создать VBA обертку для модуля Python"
imageMso="ViewFormulaBar"
onAction="OnRibbonBtn"/>
</group>
</tab>
</tabs>
</ribbon>
</customUI>