How do you simulate completing a QFileDialog using PyQt and QTest? - python

I want to automate the testing of a GUI. In particular I want to test the "save" option from the file menu. I.e. when the save button is clicked the data from the fields in the UI are collected and then written to a json file.
The problem is, when the QFileDialog pops up asking for the user to enter the name of the file to save to, I cant get a handle on the dialog to continue testing. How do you automate this bit? I am not sure how to get a handle on the QFileDialog.
def hdl_save_report_as(self):
try:
self.dialog.setDefaultSuffix('json')
save_file, _ = self.dialog.getSaveFileName(caption="Save Report", filter="JSON Files (*.json)")
if save_file:
score = self.parent.main_tab_view.get_fields()
self.ui_model.report_path = save_file
with open(save_file, 'w') as f:
json.dump(score, f, indent=4)
except Exception as e:
result = dlg.message_dialog("Exception", "We ran into an error!", QMessageBox.Warning, e)
print(e)
def test_save_report(self):
self.main_window.menu.bt_save_file.trigger()
self.main_window.menu.dialog.selectFile('a_test_report.json')
## save_dialog = QApplication.activeModalWidget()
# save_dialog = QApplication.activeWindow()
# children = save_dialog.findChildren()
# active_line_edit = None
# confirm_button = None
# for c in children:
# if type(c) is QLineEdit:
# if c.hasFocus():
# active_line_edit = QLineEdit(c)
# # if type(c) is QPushButton:
# # if
# active_line_edit.setText("a_test_report")
self.assertTrue(os.path.exists(os.path.join(os.getcwd(), 'a_test_report.json')))
I have tried a few different approaches, is there a standard way to do this? Hopefully I have missed something obvious.

From what I can see and as explained in another post it is not possible to simulate interaction with the special QFileDialogs like getSaveFileName or getOpenFileName.
My approach is now managing the files in setup function like
def setUp(self):
self.test_report = "a_test_report.json"
# Remove an existing test report
if os.path.exists(self.test_report):
os.remove(self.test_report)
self.assertFalse(os.path.exists(os.path.join(os.getcwd(), self.test_report)))
# Create a file to test reading / writing
with open(self.test_report, 'w') as f:
f.write("")
self.assertTrue(os.path.exists(os.path.join(os.getcwd(), self.test_report)))
self.assertTrue(os.stat(self.test_report).st_size == 0)
self.main_window = MainWindow()
And testing like
def test_save_action(self):
# Trigger the save button - check if the file was overwritten with new info
self.main_window.menu.bt_save_file.trigger()
self.assertTrue(os.stat(self.test_report).st_size > 0)
Although it seems it is not possible to test a "Save As" function.

Related

Pre-select multiple files in a QFileDialog

When a "choose files" dialog is displayed I want to pre-select files in a project which are already configured as being "part of" that project, so the user can select new files OR unselect existing (i.e. previously chosen) files.
This answer suggests multiple selection should be possible.
For this MRE, please make 3 files and put them in a suitable ref_dir:
from PyQt5 import QtWidgets
import sys
class Window(QtWidgets.QWidget):
def __init__(self):
super(Window, self).__init__()
self.button = QtWidgets.QPushButton('Test', self)
self.button.clicked.connect(self.handle_button)
layout = QtWidgets.QVBoxLayout(self)
layout.addWidget(self.button)
def handle_button(self):
options = QtWidgets.QFileDialog.Options()
options |= QtWidgets.QFileDialog.DontUseNativeDialog
ref_dir = 'D:\\temp'
files_list = ['file1.txt', 'file2.txt', 'file3.txt']
fd = QtWidgets.QFileDialog(None, 'Choose project files', ref_dir, '(*.txt)')
fd.setFileMode(QtWidgets.QFileDialog.ExistingFiles)
fd.setOptions(options)
# fd.setVisible(True)
for file in files_list:
print(f'selecting file |{file}|')
fd.selectFile(file)
string_list = fd.exec()
print(f'string list {string_list}')
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
window = Window()
window.show()
sys.exit(app.exec_())
Unfortunately, despite ExistingFiles having been chosen as the file mode, I find that it is only the last file selected which has the selection... but I want all three to be selected when the dialog is displayed.
I tried experimenting with setVisible to see whether the multiple selection could be achieved somehow after the dialog is displayed, but this didn't work.
Since a non-native file dialog is being used, we can access its child widgets to control its behavior.
At first I thought about using the selection model of the item views, but this won't update the line edit, which is responsible of checking if the files exist and enabling the Ok button in that case; considering this, the obvious solution is to directly update the line edit instead:
def handle_button(self):
# ...
existing = []
for file in files_list:
if fd.directory().exists(file):
existing.append('"{}"'.format(file))
lineEdit = fd.findChild(QtWidgets.QLineEdit, 'fileNameEdit')
lineEdit.setText(' '.join(existing))
if fd.exec():
print('string list {}'.format(fd.selectedFiles()))
The only drawback of this approach is that the fileSelected and filesSelected signals are not sent.
Musicamante's answer was very, very helpful, in particular showing that the selection is in fact triggered by filling the QLE with path strings.
But in fact there is a fatal flaw when the purpose is as I have stated: unfortunately, if you try to deselect the final selected file in a directory, actually this name is not then removed from the QLE. And in fact, if the QLE is set to blank this disables the "Choose" button. All this is by design: the function of a QFileDialog is either to "open" or to "save", not to "modify".
But I did find a solution, which involves finding the QListView which lists the files in the directory, and then using a signal on its selection model.
Another thing this caters for is what happens when you change directory: obviously, you then want the selection to be updated on the basis of the project's files as found (or not found) inside that directory. I've in fact changed the text of the "choose" button to show that "modification" is the name of the game.
fd = QtWidgets.QFileDialog(app.get_main_window(), 'Modify project files', start_directory, '(*.docx)')
fd.setFileMode(QtWidgets.QFileDialog.ExistingFiles)
fd.setViewMode(QtWidgets.QFileDialog.List)
fd.setLabelText(QtWidgets.QFileDialog.Reject, '&Cancel')
fd.setLabelText(QtWidgets.QFileDialog.Accept, '&Modify')
fd.setOptions(options)
file_name_line_edit = fd.findChild(QtWidgets.QLineEdit, 'fileNameEdit')
list_view = fd.findChild(QtWidgets.QListView, 'listView')
# utility to cope with all permutations of backslashes and forward slashes in path strings:
def split_file_path_str(path_str):
dir_path_str, filename = ntpath.split(path_str)
return dir_path_str, (filename or ntpath.basename(dir_path_str))
fd.displayed_dir = None
sel_model = list_view.selectionModel()
def sel_changed():
if not fd.displayed_dir:
return
selected_file_paths_in_shown_dir = []
sel_col_0s = sel_model.selectedRows()
for sel_col_0 in sel_col_0s:
file_path_str = os.path.join(fd.displayed_dir, sel_col_0.data())
selected_file_paths_in_shown_dir.append(file_path_str)
already_included = file_path_str in self.files_list
if not already_included:
fd.project_files_in_shown_dir.append(file_path_str)
# now find if there are any project files which are now NOT selected
for project_file_path_str in fd.project_files_in_shown_dir:
if project_file_path_str not in selected_file_paths_in_shown_dir:
fd.project_files_in_shown_dir.remove(project_file_path_str)
sel_model.selectionChanged.connect(sel_changed)
def file_dlg_dir_entered(displayed_dir):
displayed_dir = os.path.normpath(displayed_dir)
# this is set to None to prevent unwanted selection processing triggered by setText(...) below
fd.displayed_dir = None
fd.project_files_in_shown_dir = []
existing = []
for file_path_str in self.files_list:
dir_path_str, filename = split_file_path_str(file_path_str)
if dir_path_str == displayed_dir:
existing.append(f'"{file_path_str}"')
fd.project_files_in_shown_dir.append(file_path_str)
file_name_line_edit.setText(' '.join(existing))
fd.displayed_dir = displayed_dir
fd.directoryEntered.connect(file_dlg_dir_entered)
# set the initially displayed directory...
file_dlg_dir_entered(start_directory)
if fd.exec():
# for each file, if not present in self.files_list, add to files list and make self dirty
for project_file_in_shown_dir in fd.project_files_in_shown_dir:
if project_file_in_shown_dir not in self.files_list:
self.files_list.append(project_file_in_shown_dir)
# also add to list widget...
app.get_main_window().ui.files_list.addItem(project_file_in_shown_dir)
if not self.is_dirty():
self.toggle_dirty()
# but we also have to make sure that a file has not been UNselected...
docx_files_in_start_dir = [f for f in os.listdir(fd.displayed_dir) if os.path.isfile(os.path.join(fd.displayed_dir, f)) and os.path.splitext(f)[1] == '.docx' ]
for docx_file_in_start_dir in docx_files_in_start_dir:
docx_file_path_str = os.path.join(fd.displayed_dir, docx_file_in_start_dir)
if docx_file_path_str in self.files_list and docx_file_path_str not in fd.project_files_in_shown_dir:
self.files_list.remove(docx_file_path_str)
list_widget = app.get_main_window().ui.files_list
item_for_removal = list_widget.findItems(docx_file_path_str, QtCore.Qt.MatchExactly)[0]
list_widget.takeItem(list_widget.row(item_for_removal))
if not self.is_dirty():
self.toggle_dirty()
After a few hours of playing around, here's a pretty good way to programatically pre-select multiple files in a QFileDialog:
from pathlib import Path
from PyQt5.QtCore import QItemSelectionModel
from PyQt5.QtWidgets import QFileDialog, QListView
p_files = Path('/path/to/your/files')
dlg = QFileDialog(
directory=str(p_files),
options=QFileDialog.DontUseNativeDialog)
# get QListView which controls item selection
file_view = dlg.findChild(QListView, 'listView')
# filter files which we want to select based on any condition (eg only .txt files)
# anything will work here as long as you get a list of Path objects or just str filepaths
sel_files = [p for p in p_files.iterdir() if p.suffix == '.txt']
# get selection model (QItemSelectionModel)
sel_model = file_view.selectionModel()
for p in sel_files:
# get idx (QModelIndex) from model() (QFileSystemModel) using str of Path obj
idx = sel_model.model().index(str(p))
# set the active selection using each QModelIndex
# IMPORTANT - need to include the selection type
# see dir(QItemSelectionModel) for all options
sel_model.select(idx, QItemSelectionModel.Select | QItemSelectionModel.Rows)
dlg.exec_()
dlg.selectedFiles()
>>> ['list.txt', 'of.txt', 'selected.txt', 'files.txt']

Read labels separately from a function that generates a set of buttons

I have the following development that I am working on with the Tkinter, ElementTree and Pandas modules in Python:
from tkinter import *
import xml.etree.ElementTree as ET
import pandas as pd
file_xml = ET.parse('example1.xml')
rootXML = file_xml.getroot()
root = Tk()
root.title("Graphical Analysis of Signals of a XML")
root.geometry("1024x820")
root.pack_propagate(False)
root.config(bd=15)
# Functions to open xml file
def open_file():
try:
global temxml, xml_Name
xml_Name = str(easygui.fileopenbox(title='Select xml file', default='*.xml'))
if str(os.path.abspath(xml_Name)) != os.path.join(os.path.abspath(os.getcwd()), os.path.basename(xml_Name)):
menssage.set("Opened xml file: ", xml_Name)
child_4_separate(os.path.basename(str(tempxml)))
else:
child_4_separate(os.path.basename(str(xml_Name)))
except FileNotFoundError:
print('XML file was not loaded.')
# Function to display buttons and choose the Child_4 to be plotted
def child_4_separate(xml_Name):
print("File: ", xml_Name)
file_xml = ET.parse(xml_Name)
data_xml = [
{
"Name": signal.attrib["Name"],
"Id": signal.attrib["Id"],
} for signal in file_xml.findall(".//Child_4")
]
# print(data_xml)
for i in data_xml:
print(i)
id_tc = i.get('Id')
dict_tc = str(i.values()).replace('dict_values([\'', '')
name_tc = dict_tc.replace('\'])', '')
Button(root, text=f"TC> {name_tc}", command=transfor_data_atri_child_4(xml_Name, id_tc)).pack()
# Function to transform xml file to DataFrame
def transfor_data_atri_child_4(rootXML, id_tc):
print("File: ", rootXML)
print("id_tc: ", id_tc)
What I'm trying to do is that every time I click a button, the child_4_separate (xml_Name) function it goes to the transform_data_atri_child_4 (rootXML, id_tc) function with a single id number and does not fetch me everything like it does in the last print that I show below, this is to be able to manipulate them separately.
I share the XML file in this link example1.xml because of how long it is.
I don't know if I need another for inside or what I need, because when trying another for inside the already existing for in the child_4_separate (xml_Name) function it is repeating it many times and it is not what I want, but simply that redirect to the following function with the two parameters that I am indicating, but separately; help me please! Beforehand, thank you very much!
Just for possible searches or related problems later, I share the solution:
Take the value command = lambda x = xml_Name, y = id_tc: transform_data_atri_child_4 (x, y) in the button attributes and it worked, my function is like this:
# Function to display buttons and choose the Child_4 to be plotted
def child_4_separate(xml_Name):
# print("File: ", xml_Name)
file_xml = ET.parse(xml_Name)
data_xml = [
{
"Name": signal.attrib["Name"],
"Id": signal.attrib["Id"],
} for signal in file_xml.findall(".//Child_4")
]
# print(data_xml)
for i in data_xml:
id_tc = i.get('Id')
dict_tc = str(i.values()).replace('dict_values([\'', '')
name_tc = dict_tc.replace('\'])', '')
Button(root, text=f"TC> {name_tc}", command=lambda x=xml_Name, y=id_tc: transfor_data_atri_child_4(x, y)).pack()
I appreciate the solution to #Sujay. Happy codification everyone!!

Uploading files using Browse Button in Jupyter and Using/Saving them

I came across this snippet for uploading files in Jupyter however I don't know how to save this file on the machine that executes the code or how to show the first 5 lines of the uploaded file. Basically I am looking for proper commands for accessing the file after it has been uploaded:
import io
from IPython.display import display
import fileupload
def _upload():
_upload_widget = fileupload.FileUploadWidget()
def _cb(change):
decoded = io.StringIO(change['owner'].data.decode('utf-8'))
filename = change['owner'].filename
print('Uploaded `{}` ({:.2f} kB)'.format(
filename, len(decoded.read()) / 2 **10))
_upload_widget.observe(_cb, names='data')
display(_upload_widget)
_upload()
_cb is called when the upload finishes. As described in the comment above, you can write to a file there, or store it in a variable. For example:
from IPython.display import display
import fileupload
uploader = fileupload.FileUploadWidget()
def _handle_upload(change):
w = change['owner']
with open(w.filename, 'wb') as f:
f.write(w.data)
print('Uploaded `{}` ({:.2f} kB)'.format(
w.filename, len(w.data) / 2**10))
uploader.observe(_handle_upload, names='data')
display(uploader)
After the upload has finished, you can access the filename as:
uploader.filename
I am working on ML with Jupyter notebook, and I was looking for a solution to select the local files containing the datasets by browsing amongst the local file system. Although, the question here refers more to uploading than selecting a file. I am putting here a snippet that I found here because when I was looking for a solution for my particular case, the result of the search took me several times to here.
import os
import ipywidgets as widgets
class FileBrowser(object):
def __init__(self):
self.path = os.getcwd()
self._update_files()
def _update_files(self):
self.files = list()
self.dirs = list()
if(os.path.isdir(self.path)):
for f in os.listdir(self.path):
ff = os.path.join(self.path, f)
if os.path.isdir(ff):
self.dirs.append(f)
else:
self.files.append(f)
def widget(self):
box = widgets.VBox()
self._update(box)
return box
def _update(self, box):
def on_click(b):
if b.description == '..':
self.path = os.path.split(self.path)[0]
else:
self.path = os.path.join(self.path, b.description)
self._update_files()
self._update(box)
buttons = []
if self.files:
button = widgets.Button(description='..', background_color='#d0d0ff')
button.on_click(on_click)
buttons.append(button)
for f in self.dirs:
button = widgets.Button(description=f, background_color='#d0d0ff')
button.on_click(on_click)
buttons.append(button)
for f in self.files:
button = widgets.Button(description=f)
button.on_click(on_click)
buttons.append(button)
box.children = tuple([widgets.HTML("<h2>%s</h2>" % (self.path,))] + buttons)
And to use it:
f = FileBrowser()
f.widget()
# <interact with widget, select a path>
# in a separate cell:
f.path # returns the selected path
4 years later this remains an interesting question, though Fileupload has slightly changed and belongs to ipywidgets....
Here is some demo that shows how to get the file/files after the button click and reset the button to get more files....
from ipywidgets import FileUpload
def on_upload_change(change):
if not change.new:
return
up = change.owner
for filename,data in up.value.items():
print(f'writing [{filename}] to ./')
with open(filename, 'wb') as f:
f.write(data['content'])
up.value.clear()
up._counter = 0
upload_btn = FileUpload()
upload_btn.observe(on_upload_change, names='_counter')
upload_btn
And here is a "debug" version that shows what is going on / why things work...
from ipywidgets import FileUpload
def on_upload_change(change):
if change.new==0:
print ('cleared')
return
up = change.owner
print (type(up.value))
for filename,data in up.value.items():
print('==========================================================================================')
print(filename)
for k,v in data['metadata'].items():
print(f' -{k:13}:[{v}]')
print(f' -content len :[{len(data["content"])}]')
print('==========================================================================================')
up.value.clear()
up._counter = 0
upload_btn = FileUpload()
upload_btn.observe(on_upload_change, names='_counter')
upload_btn
I stumbled into this thread ~2 years late. For those still confused about how to work with the fileupload widget I have built off of the excellent answer posted by minrk with some other usage examples below.
from IPython.display import display
import fileupload
uploader = fileupload.FileUploadWidget()
def _handle_upload(change):
w = change['owner']
with open(w.filename, 'wb') as f:
f.write(w.data)
print('Uploaded `{}` ({:.2f} kB)'.format(
w.filename, len(w.data) / 2**10))
uploader.observe(_handle_upload, names='data')
display(uploader)
From the widget documentation:
class FileUploadWidget(ipywidgets.DOMWidget):
'''File Upload Widget.
This widget provides file upload using `FileReader`.
'''
_view_name = traitlets.Unicode('FileUploadView').tag(sync=True)
_view_module = traitlets.Unicode('fileupload').tag(sync=True)
label = traitlets.Unicode(help='Label on button.').tag(sync=True)
filename = traitlets.Unicode(help='Filename of `data`.').tag(sync=True)
data_base64 = traitlets.Unicode(help='File content, base64 encoded.'
).tag(sync=True)
data = traitlets.Bytes(help='File content.')
def __init__(self, label="Browse", *args, **kwargs):
super(FileUploadWidget, self).__init__(*args, **kwargs)
self._dom_classes += ('widget_item', 'btn-group')
self.label = label
def _data_base64_changed(self, *args):
self.data = base64.b64decode(self.data_base64.split(',', 1)[1])
Get the data in bytestring format:
uploader.data
Get the data in a regular utf-8 string:
datastr= str(uploader.data,'utf-8')
Make a new pandas dataframe from the utf-8 string (e.g. from a .csv input):
import pandas as pd
from io import StringIO
datatbl = StringIO(datastr)
newdf = pd.read_table(datatbl,sep=',',index_col=None)
You have to enable the file upload option in your code, to enable the browse button to appear in your notebook.
Run the following
!jupyter nbextension enable fileupload --user --py

Saving and searching data with Tkinter entry boxes

This might be a strange question because I am new to Python.
I am trying to create form in Python which data can be entered into boxes and saved, then opened again. I'm currently using Tkinter to create a Gui which has entry boxes and buttons:
import sys
from tkinter import *
def mstore():
pass
return
def msearch():
file_path = filedialog.askopenfilename()
return
mGui=Tk()
mGui.geometry('450x450+200+200')
mGui.title('Form Test')
#Top
mTitle = Label (mGui,text='Heading Text',bg='white').grid(row=1,column=1)
mDetail = Label (mGui,text='Flavour you can see',bg='white').grid(row=2,column=1)
#Entry Boxes
mFName = Label (mGui,text='Barcode',bg='white').grid(row=3,column=1)
mEntryname = Entry().grid(row=3,column=2)
#Buttons
mSave = Button (mGui,text='Save',bg='white', command = mstore).grid(row=4,column=1)
mSearch = Button (mGui,text='Search',bg='white', command = msearch).grid(row=5,column=1)
mGui.mainloop()
The search was going to be used to open up a file which has been saved before and fill in the boxes with that data, however before that I need help saving the data in a way it will be retrievable - All the information I find is about web-forms. I have also tried saving information with SQLite3 but I found that to not be quite what I was looking for.
Any help/guidance will be appreciated.
Thanks,
Hello Gregulimy!
I have simplified your code and made it do what you want it to do. I have left comments explaining what the code does. If you have any questions about what I have done feel free to ask!
from tkinter import *
def mstore(text):
file = open("file.txt", "w") # Create file.txt
file.write(text) # Write contents of mEntryname to file
file.close() # Closes text file
def msearch():
file = filedialog.askopenfilename() # Stores file directory that user chose
open_file = open(file, 'r') # Opens file user chose
print(open_file.read()) # Displays contents in console
open_file.close() # Closes text file
# Window Creation and Settings
window = Tk()
window.geometry('450x500')
window.title('Form Test')
# Create Widgets
mTitle = Label (window,text='Heading Text',bg='white')
mDetail = Label (window,text='Flavour you can see',bg='white')
mFName = Label (window,text='Barcode',bg='white')
mEntryname = Entry(window)
# Runs mstore function when pressed (passing the contents of the entry box)
mSave = Button (window,text='Save',bg='white', command = lambda: mstore(mEntryname.get()))
# Runs msearch function when pressed
mSearch = Button (window,text='Search',bg='white', command = lambda: msearch())
# Render Widgets
mTitle.pack()
mDetail.pack()
mFName.pack()
mEntryname.pack()
mSave.pack()
mSearch.pack()
window.mainloop()

PythonWin saving session state

I would like to be able to save my session state within the PythonWin editor (e.g. these three files are opened and positioned in these particular locations within the PythonWin window). I can get handles to each of the child windows within PythonWin using win32gui, as well as the titles of each of the files and the positions/sizes of the windows. I'm unclear though in how to get the full path for the file listed as the child window name (i.e. if child window name is test.py and test.py lives at c:\python\test.py, I don't know how to get c:\python). I was thinking I would write out which files were opened plus their window positions to a small file that I would then call at PythonWin start time for loading.
Any ideas on how to get the full paths to the child window names?
Alternatively if someone already has a more elegant solution for saving session state in PythonWin please pass it along.
Below is the code I'm using right now (thanks to Michal Niklas for the starter code for using win32gui).
import win32gui
import re
MAIN_HWND = 0
def is_win_ok(hwnd, starttext):
s = win32gui.GetWindowText(hwnd)
if s.startswith(starttext):
global MAIN_HWND
MAIN_HWND = hwnd
return None
return 1
def find_main_window(starttxt):
global MAIN_HWND
win32gui.EnumChildWindows(0, is_win_ok, starttxt)
return MAIN_HWND
def winPos(hwnd):
if type(hwnd) == type(1): ( left, top, right, bottom ) = win32gui.GetWindowRect(hwnd)
return "%i, %i, %i, %i" % (left, right, top, bottom)
def winName(hwnd, children):
s = win32gui.GetWindowText(hwnd)
rePy = re.compile(r'[a-zA-Z1-9_ ]*.py')
rePySearch = rePy.search(s)
if rePySearch is not None:
if rePySearch.group()[0:7] != "Running":
s = s + ',' + winPos(hwnd) + '\n'
children.append(s)
return 1
def main():
children = []
main_app = 'PythonWin'
hwnd = win32gui.FindWindow(None, main_app)
if hwnd < 1:
hwnd = find_main_window(main_app)
if hwnd:
win32gui.EnumChildWindows(hwnd, winName, children)
filename = "sessionInfo.txt"
sessionFile = os.path.join(sys.path[0],filename)
fp=open(sessionFile, 'wb')
for i in range(len(children)):
fp.write(children[i])
fp.close()
main()
I could be wrong, but isn't PythonWin written in Python?
Have you tried reading the source to the "Save" command to figure out where it stores its full paths?
(I'd take a look myself, but I haven't used Windows in half a decade)

Categories

Resources