Attribute VB_Name = "ex_Python" ' ======== Python runner extension ======== ' Shared module version: 20240330 ' Tested in: PythonManager ' Depends on: ex_VBA, ex_WinAPI, API_Config ' Required reference: Scripting Option Private Module Option Explicit ' Switch flag for Debug purposes Private Const SHOW_PYTHON = False Public Const PY_KEY_INTERPRETER = "Python3" Public Const PY_KEY_SOURCE_PATH = "PythonImports" Public Const PY_MODULE_TEXT = "cctext" Public Const PY_MODULE_UTILS = "vbatopy" Public Const PY_DEFVALUE_INTERPRETER = "C:\Tools\Python312-venv\Scripts\python.exe" Public Function AccessPython() As API_Python Static s_Python As API_Python If s_Python Is Nothing Then Set s_Python = New API_Python Call s_Python.Init( _ iPython:=ConceptConfig.GetValue(PY_KEY_INTERPRETER), _ sModules:=OfficeActivePath & ";" & ConceptConfig.GetValue(PY_KEY_SOURCE_PATH), _ bDoDebug:=SHOW_PYTHON) End If Set AccessPython = s_Python End Function Public Function ConceptConfig() As API_Config Dim iConfig As New API_Config Set ConceptConfig = iConfig If Not iConfig.LoadFromFile(ConceptConfigPath()) Then _ Call LoadDefaultConfig(iConfig) End Function ' Runs the Python command, e.g.: Call RunPython("import bar; bar.foo()", ThisWorkbook.Path) Public Function RunPyStandalone(sPyCommand$, Optional sWorkDir$ = vbNullString) As Long Dim iConfig As API_Config: Set iConfig = ConceptConfig Dim iPython$: iPython = iConfig.GetValue(PY_KEY_INTERPRETER) Dim sSource$ If Not sWorkDir = vbNullString Then _ sSource = sWorkDir & ";" sSource = sSource & iConfig.GetValue(PY_KEY_SOURCE_PATH) If sSource <> "" Then _ sSource = sSource & ";" sSource = VBA.Replace(sSource, "&", "^&") sSource = VBA.Replace(sSource, "\", "\\") Dim sChangeDir$: sChangeDir = GenerateCDCommandFor(iPython) Dim sExtendedPyCommand$: sExtendedPyCommand = Fmt( _ "{1} & ""{2}"" -B -c " & _ """import sys, os; sys.path[0:0]=os.path.normcase(os.path.expandvars(\""{3}\"")).split(';'); " & _ "{4}""", _ sChangeDir, iPython, sSource, sPyCommand) RunPyStandalone = PyExecute(sExtendedPyCommand) End Function ' Runs a Python executable that has been frozen by PyInstaller and the like ' Call RunFrozenPython("C:\path\to\frozen_executable.exe", "arg1 arg2") Public Function RunPyFrozen(iPython$, Optional sArgs$ = vbNullString) As Long Dim sChangeDir$: sChangeDir = GenerateCDCommandFor(iPython) Dim sPyCommand$: sPyCommand = Fmt("{1} & ""{2}"" {3}", sChangeDir, iPython, sArgs) RunPyFrozen = PyExecute(sPyCommand) End Function Public Function ConceptConfigPath() As String ConceptConfigPath = VBA.Environ$("USERPROFILE") & "\.concept\concept-options.json" End Function ' ======== Private Function PyExecute(sPyCommand$) As Long Dim sLog$: sLog = GetLogFile() Dim sShellCommand$: sShellCommand = Fmt("cmd.exe /C {1} 2> ""{3}""", _ sPyCommand, sLog) Dim nWindowStyle%: nWindowStyle = IIf(SHOW_PYTHON, 1, 0) Dim iShell As Object: Set iShell = CreateObject("WScript.Shell") Dim nExitCode&: nExitCode = iShell.Run(sShellCommand, nWindowStyle, True) If nExitCode <> 0 Then Call MsgBox(ReadTextFile(sLog), vbCritical) PyExecute = -1 End If On Error Resume Next Call Kill(sLog) On Error GoTo 0 Set iShell = Nothing End Function Private Function LoadDefaultConfig(ByRef iConfig As API_Config) Call iConfig.SetValue(PY_KEY_INTERPRETER, PY_DEFVALUE_INTERPRETER) Call iConfig.SetValue(PY_KEY_SOURCE_PATH, "") End Function Private Function GenerateCDCommandFor(sFile$) As String Dim fso As New Scripting.FileSystemObject GenerateCDCommandFor = Fmt("{1} & cd ""{2}""", VBA.Left(sFile, 2), fso.GetParentFolderName(sFile$)) End Function Private Function GetLogFile() As String GetLogFile = Fmt("{1}\pymanager-{2}.log", VBA.Environ("Temp"), GetCurrentProcessId()) End Function Private Function ReadTextFile(sFile$) As String Dim adoStream As Object: Set adoStream = CreateObject("ADODB.Stream") adoStream.Charset = "utf-8" Call adoStream.Open On Error GoTo ERROR_FILE Call adoStream.LoadFromFile(sFile) On Error GoTo 0 ReadTextFile = adoStream.ReadText ERROR_FILE: Call adoStream.Close End Function