Python Excel copy cell style from one to another openpyxl - python

I am struggling with the following using openpyxl version 3.0.7
I want to copy the style of one cell to another. That means, background colour, font, etc.
However, I don't know how I can do that.
My initial idea was basically to do
sheet["E1"].font = sheet["D1"].font.
That bit shoots out TypeError: unhashable type: 'StyleProxy'.
Using just .style doesn't really do all that much so it's not suitable.
I found a few solutions online like the one from here. However, I don't know how I can apply it to my specific needs seeing as I struggle to even transfer the font from one cell to another.

Shortly after typing this out, I found the solution.
from copy import copy
wb = openpyxl.load_workbook('C:\\path...\\')
sheet = wb["Name of the worksheet"]
sheet["E1"].font = copy(sheet["D1"].font)
sheet["E1"].border = copy(sheet["D1"].border)
sheet["E1"].fill = copy(sheet["D1"].fill)
sheet["E1"].number_format = copy(sheet["D1"].number_format)
sheet["E1"].protection = copy(sheet["D1"].protection)
sheet["E1"].alignment = copy(sheet["D1"].alignment)
The only thing left to do would be to do this in a loop. That would be achievable by doing something like
for i in range(....):
sheet["E" + str(i)].font= copy(sheet["D" +str(i)].font)
etc.

Related

How to eval all conditional formatting instances that apply to a cell ? (without rewriting excel formula parser in python)

After trying to use openpyxl to try to know which styles is applied in order to get the the actual background color of a cell after the conditional formatting has been applied and realized that I would have to write a formula parser (and it makes no sense to re-write excel and I would have to deal with chained formula cell values, etc).
I am now reaching the PyUno interface to get access via a libreoffice instance running headless and reaching the XSheetConditionalEntry object trough the PyOO interface.
Looks that I have reached the exact same place, I have the cell and the formula; but no way of knowing which of the conditional formatting styles applies or not:
def processFile(filename):
soffice = subprocess.Popen(officeCommand, shell=True)
desktop = pyoo.Desktop(pipe='hello')
doc = desktop.open_spreadsheet(filename)
sheet = doc.sheets['STOP FS 2023']
cell = sheet[5,24]
cellUno = cell._get_target()
print(f"{cellUno.getPropertyValue('CellBackColor')=}")
print(f"{cellUno.getPropertyValue('CellStyle')=}")
for currentConditionalFormat in cellUno.getPropertyValue('ConditionalFormat'):
print(f"{currentConditionalFormat.getStyleName()=}")
print(f"{currentConditionalFormat.getOperator()=}")
getting the following results
cellUno.getPropertyValue('CellBackColor')=-1
cellUno.getPropertyValue('CellStyle')='Default'
currentConditionalFormat.getStyleName()='ConditionalStyle_4'
currentConditionalFormat.getOperator()=<Enum instance com.sun.star.sheet.ConditionOperator ('BETWEEN')>
currentConditionalFormat.getStyleName()='ConditionalStyle_3'
currentConditionalFormat.getOperator()=<Enum instance com.sun.star.sheet.ConditionOperator ('NONE')>
currentConditionalFormat.getStyleName()='ConditionalStyle_2'
currentConditionalFormat.getOperator()=<Enum instance com.sun.star.sheet.ConditionOperator ('NONE')>
currentConditionalFormat.getStyleName()='ConditionalStyle_1'
currentConditionalFormat.getOperator()=<Enum instance com.sun.star.sheet.ConditionOperator ('NONE')>
The style that is being applied is the ConditoinalStyle_3
This post has helped a bit but it is intended to work inside of a macro, and looks like heir forum sign up is broken, as I would would have tried to ask the same question over there.

Is there a method to check if a portion of text inside a xlsx cell is bold?

Using openpyxl we can properly check if a cell is fully bold/not bold, but we cannot work with richtext so having two words, one bolded and one not, will make the check fail.
This can be done correctly with xlrd, but it doesn't support xlsx files. Converting from xlsx to xls is risky, especially in my use case, since I have a big file with many languages and I think i could lose information.
How can I check if cells substrings are bold inside a xlsx file?
TL;DR: not yet, but upcoming openpyxl v3.1 will be able to satisfy your request.
I took a quick tour through the late 2022 state of python-excel affair with respect to this very feature, which relies on being able to manage Rich Text objects as cell contents:
pylightxl was new to me, so I quickly browsed the project for a bit. But as the name indicates, it is geared towards a small, maintainable feature set. Only cell values are in scope, formatting is intentionally skipped.
xlrd supports rich text, though only legacy .xls, as you pointed out in your question already.
xlsxwriter luckily is able to construct cells containing rich text with mixed formatting (example), but unfortunately only deals with writing files.
openpyxl finally currently does not support rich text... but: with the upcoming release 3.1, it will, thanks to merge request !409 that was completed earlier this year.
example, using openpyxl 3.0.10:
have this sample:
then use:
import openpyxl
test_file = "test_spreadsheet.xlsx"
obj = openpyxl.load_workbook(test_file)
sheet_ref = obj.active
cell1 = sheet_ref.cell(row = 3, column = 2)
cell_font1 = cell1.font
print(cell_font1.b)
cell = sheet_ref.cell(row = 4, column = 2)
cell_font = cell.font
print(cell_font.b)
result:
$ python test.py
False
True
so you could build it yourself:
def is_text_in_cell_bold(cell_obj):
if (cell_obj.font.b):
return True
return False

Chart objects in xlwings

The code snippet came from the official documentation of xlwings here and it is the setup for my question.
import xlwings as xw
sht = xw.Book().sheets[0]
sht.range('A1').value = [['Foo1', 'Foo2'], [1, 2]]
chart = sht.charts.add()
chart.set_source_data(sht.range('A1').expand())
chart.chart_type = 'line'
chart.name
Running print(chart.api) outputs the tuple below.
(<xlwings._xlwindows.COMRetryObjectWrapper at 0x1fcd60c9a90>, <xlwings._xlwindows.COMRetryObjectWrapper at 0x1fcd60c9f28>)
If I want to use the api attribute to do some basic chart manipulation like remove the legend and add a title, it only works if I do it to chart.api[1]. For instance the code below works fine. It removes the chart legend and adds a title.
chart.api[1].HasLegend = 0
chart.api[1].SetElement(2)
chart.api[1].ChartTitle.Text = 'A title'
However, anything I do to chart.api[0] yields an error, (for instance print(chart.api[0].HasLegend) yields an error). I can't understand what kind of object this is or how it is useful. I can't find anything regarding this in the official documentation.
Finally my question is: what is the object at the index 0 above? Please, help me grok what it is.
There is another post that addresses your question about the object at the index 0.
set chart name in Xlwings
The expression chart.api returns a tuple with two COM wrappers. I'm
not really sure why there are two COM wrappers, but it seems that you
need the second one to access the chart. Hence the use of chart.api[1]
here.

Possible to insert row at specific position with python-docx?

I'd like to insert a couple of rows in the middle of the table using python-docx. Is there any way to do it? I've tried to use a similar to inserting pictures approach but it didn't work.
If not, I'd appreciate any hint on which module is a better fit for this task. Thanks.
Here is my attempt to mimic the idea for inserting a picture. It's WRONG. 'Run' object has no attribute 'add_row'.
from docx import Document
doc = Document('your docx file')
tables = doc.tables
p = tables[1].rows[4].cells[0].add_paragraph()
r = p.add_run()
r.add_row()
doc.save('test.docx')
The short answer is No. There's no Table.insert_row() method in the API.
A possible approach is to write a so-called "workaround function" that manipulates the underlying XML directly. You can get to any given XML element (e.g. <w:tbl> in this case or perhaps <w:tr>) from it's python-docx proxy object. For example:
tbl = table._tbl
Which gives you a starting point in the XML hierarchy. From there you can create a new element from scratch or by copying and use lxml._Element API calls to place it in the right position in the XML.
It's a little bit of an advanced approach, but probably the simplest option. There are no other Python packages out there that provide a more extensive API as far as I know. The alternative would be to do something in Windows with their COM API or whatever from VBA, possibly IronPython. That would only work at small scale (desktop, not server) running Windows OS.
A search on python-docx workaround function and python-pptx workaround function will find you some examples.
You can insert row to the end of the table and then move it in another position as follows:
from docx import Document
doc = Document('your docx file')
t = doc.tables[0]
row0 = t.rows[0] # for example
row1 = t.rows[-1]
row0._tr.addnext(row1._tr)
Though there isn't a directly usable api to achieve this according to the python-docx documentation, there is a simple solution without using any other libs such as lxml, just use the underlying data structure provided by python-docx, which are CT_Tbl, CT_Row, etc.
These classes do have common methods like addnext, addprevious which can conveniently add element as siblings right after/before current element.
So the problem can be solved as below, (tested on python-docx v0.8.10)
from docx import Document
doc = Document('your docx file')
tables = doc.tables
row = tables[1].rows[4]
tr = row._tr # this is a CT_Row element
for new_tr in build_rows(): # build_rows should return list/iterator of CT_Row instance
tr.addnext(new_tr)
doc.save('test.docx')
this should solve the problem
You can add a row in last position by this way :
from win32com import client
doc = word.Documents.Open(r'yourFile.docx'))
doc = word.ActiveDocument
table = doc.Tables(1) #number of the tab you want to manipulate
table.Rows.Add()
addnext() in lxml.etree seems like will be the better option to use and its working fine, and the only thing is, i cannot set the height of the row, so please provide some answers, if you know!
current_row = table.rows[row_index]
table.rows[row_index].height_rule = WD_ROW_HEIGHT_RULE.AUTO
tbl = table._tbl
border_copied = copy.deepcopy(current_row._tr)
tr = border_copied
current_row._tr.addnext(tr)
I created a video here to demonstrate how to do this because it threw me for a loop the first time.
https://www.youtube.com/watch?v=nhReq_0qqVM
document=Document("MyDocument.docx")
Table = document.table[0]
Table.add_row()
for cells in Table.rows[-1].cells:
cells.text = "test text"
insertion_row = Table.rows[4]._tr
insertion_row.add_next(Table.rows[-1]._tr)
document.save("MyDocument.docx")
The python-docx module doesn't have a method for this, So the best workaround Ive found is to create a new row at the bottom of the table and then use methods from the xml elements to place it in the position it is suppose to be.
This will create a new row with every cell in the row having the value "test text" and we then add that row underneath of our insertion_row.

Style Normal exists already - Python - OpenPyxl

I have looked into many stackoverflow questions but none of them seemed to solve my problem. I am using Python and Openpyxl to fill a whole row with red given a certain condition. I did all the importations necessary :
from openpyxl.styles import PatternFill, NamedStyle, Color
from openpyxl.styles.colors import RED
And my code is the following :
for cell in sheet[i]:
cell.style = NamedStyle(fill=PatternFill(patternType='solid',
fill_type='solid',
fgColor=Color(RED)))
When I ask to print the first occurence of cell it gives me
<Cell 'Divers'.A4>
which is what I am looking for.
However, the following error comes every time : "Style Normal exists already". There is absolutely no cell formatting or style whatsoever in the rest of the code but the Excel file cells are indeed filled with yellow already.
Any idea on how to solve this ? Thanks in advance for any help.
If using a NamedStyle, you're required to pass a name.
red_foreground = NamedStyle(
name="RedForeground",
fill=PatternFill(
patternType='solid',
fill_type='solid',
fgColor=Color(RED)
)
)
Since you're assigning this NamedStyle to more than one cell, it makes sense to register it to your workbook.
wb.add_named_style(red_foreground)
Then you can update it's application to cells, like so:
for cell in sheet[i]:
cell.style = "RedForeground"
Reference:
Creating NamedStyle
NamedStyle Constructor
I also have this problem, and finally found that it was because there were 2 styles, of which had the same name. This is usually caused when you use copy.copy(style). Then after change one of the style.name = 'newname', it will work.
This code would solve already existing named styles.
for index,cur_style in enumerate(excel_workbook._named_styles):
if cur_style.name == 'my_new_style':
excel_workbook._named_styles[index] = my_new_style
my_new_style.bind(excel_workbook)
break
else:
excel_workbook.add_named_style(my_new_style)
However, in your case, you should use some other name than "Normal", because "Normal" is the default named style, just find another name and you can use the code I pasted
There is another way to solve traceback by adding existing styles:
if not 'Style_A' in wb.named_styles:
wb.add_named_style(Style_A)

Categories