Update Links in for Excel Spreadsheet Using Python - python

I am running simulations in Python that generate output that need to be directly consumed by a modeler in their excel workbooks. I have generated code that will directly output my data into their excel spreadsheet template. The code I have generated to output the data directly to their template is fine, but the problem I am running into is that the modeler has a series of workbooks that are "linked" together. If I insert my data into their spreadsheet, the links to that workbook do no update unless the user physically opens the workbook to "Edit Links" -> "Update Values". If there was one workbook, then the user can simply open the workbook with no problem. In reality, there will be over 100 workbooks that need the links updated. Unfortunately, there is nothing I can do to change the modeler's approach in linking workbooks -- the only thing I can do is accommodate their approach.
My goal is to create a Python solution that will allow me to 1) Generate the simulated Data, 2) Insert my generated data into the modeler's workbook, and 3) Update all of the links between workbooks. Ultimately, in order to be streamlined, I want to be able to do all three in one end-to-end python program. I have solved (1) and (2), and I have a solution for (3) that almost works. I have generated the following functional script:
from win32com.client import Dispatch
import pandas as pd
from openpyxl import load_workbook
import os
import time
def run_macro(workbook_name, vba_sub, com_instance):
wb = com_instance.workbooks.open(workbook_name)
wb.RefreshAll()
xl_module = wb.VBProject.VBComponents.Add(1)
xl_module.CodeModule.AddFromString(vba_sub.strip())
com_instance.Application.Run('UpdateLinkValues')
wb.Save()
wb.Close()
return True
def main():
dir_root = ("C:\\Model_Spreadsheets")
vba_sub = \
'''
sub UpdateLinkValues()
Application.AskToUpdateLinks = False
ActiveWorkbook.UpdateLink Name:=ActiveWorkbook.LinkSources
end sub
'''
xl_app = Dispatch("Excel.Application")
xl_app.Visible = False
xl_app.DisplayAlerts = False
for root, dirs, files in os.walk(dir_root):
for fn in files:
if fn.endswith(".xlsx") and fn[0] is not "~":
run_macro(os.path.join(root, fn), vba_sub, xl_app)
xl_app.Quit()
if __name__ == "__main__":
main()
This script is really close to the correct solution I am looking for, but I run into a VBA error seemingly 'randomly':
run-time error '1004' method 'updatelink' method of object '_workbook' failed
This error does appear each time I try to run this script, but it does not occur for the same workbook each time -- sometimes, it occurs on the first workbook, sometimes on the 15th, etc...
I have an option to debug in VBA, and the only way that I can continue on to the next workbook is if I change the macro to
sub UpdateLinkValues()
Application.AskToUpdateLinks = False
end sub
if I run this macro and exit debug, the program will continue to run until it encounters the same error again. My first thought was that maybe there is a timing issue between me opening the workbook and trying to run the macro. A workaround that I have found is that I can change the macro and the app visibility:
vba_sub = \
'''
sub UpdateLinkValues()
Application.AskToUpdateLinks = False
end sub
'''
and
xl_app.Visible = True
This works fine, but I am not a fan of having each of the workbooks open and close because it takes a long time. My question is, does anyone know why this run-time error is coming up -- with a solution? Or perhaps, does anyone know how to intercept this run-time error in Python as an exception? If I can intercept this error as an exception in python, then I could use my alternative solution for those particulars workbooks.
Thanks in advance!

Consider having Python directly run the method UpdateLink with the COM objects you initialize, namely the xl_app and wb objects. No need to build a macro in each workbook and then call it.
Below UpdateLink() is wrapped in a try/except/finally block in case workbook has no links as LinkSources will return an Empty value, raising a COM exception, the very error you receive:
run-time error '1004' method 'updatelink' method of object '_workbook'
failed
Also be sure to uninitialize objects (a good best practice in VBA too: Set wb = Nothing) after use to free CPU resources else they remain as background processes until garbage collection.
def run_macro(workbook_name, com_instance):
wb = com_instance.workbooks.open(workbook_name)
com_instance.AskToUpdateLinks = False
try:
wb.UpdateLink(Name=wb.LinkSources())
except Exception as e:
print(e)
finally:
wb.Close(True)
wb = None
return True
def main():
dir_root = ("C:\\Model_Spreadsheets")
xl_app = Dispatch("Excel.Application")
xl_app.Visible = False
xl_app.DisplayAlerts = False
for root, dirs, files in os.walk(dir_root):
for fn in files:
if fn.endswith(".xlsx") and fn[0] is not "~":
run_macro(os.path.join(root, fn), xl_app)
xl_app.Quit()
xl = None
Aside - though VBA ships by default with Excel and MS Office applications, it is actually a separate component. To check, under Tools \ References in VBA IDE, you will see VBA is the first checked item, nothing built-in. In fact, VBA does exactly what you are doing in Python: making a COM interface to the Excel Object Library. So in a sense VBA is just as related to Excel and Python is!

Related

AttributeError: 'NoneType' object has no attribute 'Worksheets' in python win32com.client

def messageToCal(path, calculatorName,argList, eventName, fucName,PCBName):
# location of LED calculator
loc = path+calculatorName
#print loc
# open excel, run macro called "external_Run" with argList
xls=win32com.client.Dispatch("Excel.Application")
wb = xls.Workbooks.Open(Filename=loc)
try:
xls.Application.Run("external_Run",argList)
except Exception as e:
print "--------------- ERROR ------------------"
print(e)
print "=> No data was found, please check your input file"
raise
#xls.Visible = True
# disable asking dialog when close excel
xls.DisplayAlerts = False
# export first sheet (macro output) to csv
w=wb.Worksheets(1)
w.SaveAs(path +'#'+eventName+'_'+fucName+'_'+PCBName.replace(".", "")+'_' +str(argList[3])+ '.csv',6)
xls.Application.Quit()
del xls
While running this code it throws error as
**w=wb.Worksheets(1)
AttributeError: 'NoneType' object has no attribute 'Worksheets'**
Previously it was running fine. Suddenly it throwing the error.
Something is causing your code to fail to open the workbook correctly. This is causing wb to be set to None. That is why you are getting an AttributeError when attempting to access wb.Worksheets(1).
If nothing in the code has changed, it's likely that something in your spreadsheet is causing the error. You'll want to verify that the spreadsheet is in the correct location in addition to looking at any recent changes to the spreadsheet to determine what the actual cause of the problem is.
There is no problem with the Excel sheet, it's just that your code opens up excel when you run your code, but never closes it i.e. with wb.close() in your case.
If you open the Task Manager and click on more details you will see Excel running in the background even after the program has ended.
Yes, you can close it through the task manager each time after running the program, to work with that Excel file again.
Also, if you close it that way and open the file through Excel you will see that Excel provides the history (which will be the exact same data) of that file, as the program was closed abruptly before.
As I mentioned above you can use wb.close() and close your workbook through code. Although it will give you an error, but won't affect the outcome of your program in any noticeable way. I applied this patch and it all worked fine(ignoring the unharmful error).

use xlwings to call Python UDFs

I'm trying to call two Python UDFs in excel via xlwings, i saw the following error message:
Make sure that this workbook contains the xlwings module and you are trusting access to the VBA project object module(Options).
Macro setting is already enabled, the workbook also referenced to xlwings. The xlwings version in Anaconda is 0.20.0, i think this is the latest version, but i can only see "import Python UDFs" button under xlwings Add-in settings, nothing else, which is different from the Add-in settings i saw in some videos. I'm using Jupyter Notebook, my python code is saved in "M:\SolverFunction.ipynb". I also saved the code in .py extension with the same name in the same directory, not sure if i can just use .ipynb extension directly. Below is my function settings in VBA, can someone please check if everything is correct:
PYTHON_WIN = "C:\Program Files\Anaconda3\pythonw.exe"
PYTHON_MAC = ""
PYTHON_FROZEN = ThisWorkbook.Path & "\build\exe.win32-2.7"
PYTHONPATH = "M:\"
UDF_MODULES = "SolverFunction"
UDF_DEBUG_SERVER = False
LOG_FILE = ""
SHOW_LOG = True
OPTIMIZED_CONNECTION = False
when i run the macro Run_Python_Function, it shows SyntaxError:
Syntax Error in Macro
I need to call two python UDFs,don't really know how to fix this error, there is no error in the python code itself.
Another question: in the python code i use 'xw.Book' to call the existing sheet
wb = xw.Book(r'M:\SolverFunction.xlsm')
If the sheet is saved in another directory with another name, say "C:\Desktop\DVA Totem Submission\xyz.xlsm", except modifying the code
wb = xw.Book(r'C:\Desktop\DVA Totem Submission\xyz.xlsm')
do i need to change the function settings in VBA as well?
Thanks,

XLwings - Can't get Excel application to quit even with kill() method

I have searched and cannot seem to find the answer to my issue. I’m hoping someone can help.
Here is a slimmed down version of my code. I have it within a unit test TestCase class. I open three books, one of which is an xlsm file (wb2), do some processing, and then save the wb2 file with another name and close all three workbooks.
What's happening is that all three workbooks are closing, but there still remains an instance of Excel open. Like, an empty shell with nothing in it. I searched and found the kill() method, but that is not killing it (whether I use self.xw.App().kill() or just xw.App().kill()). Also, I print out all of the open apps and it returns an empty list. So, I don't know what this instance remaining is all about and why it's not getting killed. BTW, I am using Excel 365. Thanks in advance.
import xlwings as xw
class TestClass(unittest.TestCase):
def test_xltest(self):
#Open 3 separate workbooks
self.wb1 = xw.Book(workbook1)
self.wb2 = xw.Book(workbook2)
self.wb3 = xw.Book(workbook3)
#Do some processing here
def tearDown(self):
self.wb1.close()
self.wb2.save(self.newDirectoryPath)
self.wb2.close()
self.wb3.close()
print (xw.apps)
self.xw.App().kill()
if __name__ == "__main__":
unittest.main()

Running Libreoffice BASIC macro from python

I've a macro in LibreOffice BASIC and I want to run it from my python program. I've found some threads in which they use this code:
import os
import win32com.client
if os.path.exists("excelsheet.xlsm"):
xl=win32com.client.Dispatch("Excel.Application")
xl.Workbooks.Open(Filename="C:\Full Location\To\excelsheet.xlsm", ReadOnly=1)
xl.Application.Run("excelsheet.xlsm!modulename.macroname")
## xl.Application.Save() # if you want to save then uncomment this line and change delete the ", ReadOnly=1" part from the open function.
xl.Application.Quit() # Comment this out if your excel script closes
del xl
But this is for windows Excell program and I want for the LibreOffice program. Is it possible to do this?
Thanks :)
My preferred way is to put the python script in the Scripts/python subfolder of your LibreOffice user directory. Then add this function at the bottom:
def call_basic_macro():
document = XSCRIPTCONTEXT.getDocument()
frame = document.getCurrentController().getFrame()
ctx = XSCRIPTCONTEXT.getComponentContext()
dispatcher = ctx.ServiceManager.createInstanceWithContext(
'com.sun.star.frame.DispatchHelper', ctx)
url = document.getURL()
macro_call = ('macro:///Standard.Module1.Macro1("%s")' % url)
dispatcher.executeDispatch(frame, macro_call, "", 0, ())
g_exported_scripts=call_basic_macro,
Now run the python script from Writer by going to Tools -> Macros -> Run Macro. Expand My Macros and select the name of the script.
Another way which seems closer to your Excel example is to start a listening instance of LibreOffice with a system call:
start soffice -accept=socket,host=0,port=2002;urp;
I typically do that part in a shell script (batch file on Windows) rather than python. Then in python, get the document context from the instance:
import uno
localContext = uno.getComponentContext()
After that, the code would look similar to what is above. Note that with this approach on Windows, the python.exe included with LibreOffice must be used in order to load the uno module.
A third way is to simply do a system call:
soffice "macro:///Standard.Module1.Macro1()"
For more on this third approach, see https://forum.openoffice.org/en/forum/viewtopic.php?f=20&t=8232.

Interaction between open and dispatched excel processes, win32com

I am using the win32com (Python 2.7 (Anaconda) in LiClipse) to start separate instances of excel...
class ExcelDocument(object):
"""Excel class
"""
def __init__(self, xlDocIn, make_visible=False):
"""Open spreadsheet"""
self.excelApp = DispatchEx('Excel.Application')
I then do bits and bobs to the excel document (using MS Office 2013), including opening another document with DispatchEx...
objExcel1 = ExcelDocument(PATH_TABLE,False)
objExcel1.update_sheets()
...
objExcel2 = ExcelDocument(PATH_BACKG, False)
Trying to assign a second ExceLDocument class will crash the script.
If I change init to
def __init__(self, xlDocIn, make_visible=False):
"""Open spreadsheet"""
try:
self.excelApp = GetActiveObject('Excel.Application')
except:
self.excelApp = DispatchEx('Excel.Application')
the script runs fine.
If i have an excel file open when I run the script either
a. the script will crash upon editing the open file.
b. the open excel file will close when these lines of code are executed...
def close(self):
"""Close spreadsheet resources"""
self.excelApp.DisplayAlerts = True
self.workbook.Saved = 0
self.workbook.Close(SaveChanges=0)
self.excelApp.Visible = 0
Is there a reason I cannot call DispatchEx the second time, as my script should run in the background and not interrupt the any open excel files?
You should design your script assuming there can only be one instance of Excel running, although it can contain multiple workbooks. If you use GetObject (instead of GetActiveObject) then win32com will return a handle to the existing app if one exists, or start the app if it doesn't. So you won't need the if/else.
Also what this means in terms of design is that you should have a way of tracking which workbooks get opened, and close only those, so that the final state of the Excel application is the same as when you started script. So you would have one instance of ExcelDocument per workbook, each using GetObject, and each one closing the workbook that it represents. Before you create the first ExcelDocument, save GetActiveObject so your script knows if it should close the app on exit.
Basically:
activeXlApp = win32com.client.GetActiveObject('Excel.Application')
objExcel1 = ExcelDocument(PATH_TABLE,False) # uses GetObject()
objExcel1.update_sheets()
...
objExcel2 = ExcelDocument(PATH_BACKG, False)
if activeXlApp is not None:
activeXlApp.Close()

Categories

Resources