-
Notifications
You must be signed in to change notification settings - Fork 5
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
ENH: report errors, retry in saving camera image #21
base: master
Are you sure you want to change the base?
Conversation
camviewer_ui_impl.py
Outdated
@@ -17,6 +17,7 @@ | |||
|
|||
import sys | |||
import os | |||
import os.path |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why did you import os.path if you already had os?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was under the mistaken impression that import os
did not expose the utilities in os.path
, because this is how all (most) other modules work. I'll remove the redundant import. I suspect this is a habit I picked up by reading code from other programmers.
There's some interesting implementation details about this I was unaware of, namely that the os
module imports either posixpath
or ntpath
based on your OS and re-exports it as os.path
, injecting it into sys.modules
along the way. Pretty wild behavior, but I guess this is a reasonable way to aim for platform-independent libraries.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The os.path
platform specifics is a TIL for me as well
I'm pretty sure I'd have just tried to access the path
as a submodule of os
, but Zach is right that this is generally not suggested. This access method only works if the parent module imports its submodules. This isn't guaranteed to work for all modules nor is it guaranteed to not change.
camviewer_ui_impl.py
Outdated
# No write permissions? | ||
else: | ||
# This is the most robust way to check if we have permissions | ||
# os.access exists but doesn't quite work fully on NFS filesystems as per the Python docs |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've seen os.stat
mentioned in conjunction with the stat
module, if there's a need for an alternate way? (stackoverflow)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think os.stat
also won't fully encompass the NFS permissions but maybe the accuracy drop will be worth the simpler implementation (not accidentally creating a stub file)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Resolved via simplifying with os.access
to reduce compexity
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Interestingly python suggests the ask-for-forgiveness (try-catch) approach for security reasons we probably don't care about.
Out of curiosity, do you remember what the NFS limitations were?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't remember the details, but in network file systems there can be additional file protections that go beyond the posix uid/guid stuff. I think in practice this isn't used for us.
"Images (*.npy *.jpg *.png *.bmp *.pgm *.tif)", | ||
)[0] | ||
if fileName == "": | ||
if filename == "": |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it intentional to exit if there's no filename provided but retry if there was a saving problem?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's not documented very well (https://doc.qt.io/qt-5/qfiledialog.html) but empty string is returned if the user clicks "cancel".
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this meant for this comment #21 (comment)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, I meant to respond here in this way, but maybe I didn't justify myself fully.
The intention is to exit if the user clicks "cancel", which is signaled by there being no filename. Otherwise the user has no way to change their mind and not save anything.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Feels slightly wrong to throw an exception and have a warning popup if you press cancel, but not sure if it's worth making more user-friendly
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah yeah, you're right- there's a UX problem here. I forgot what came next when responding to this comment since it was pre-existing code. I'll clean this up.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Resolved by making this code path give no message at all. No raise, no pop-up- the UI element that gets us to this point doesn't let you input an empty string, so the only way to be here is to click "X" or "Cancel", so you don't need this confirmation pop-up in any form. Added a comment to clarify intent.
if fileName.lower().endswith(".npy"): | ||
np.save(fileName, self.image) | ||
if filename.lower().endswith(".npy"): | ||
np.save(filename, self.image) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just double checking, but can this fail silently? In the cases you check for if QImage.save fails (no write permission, file already exists, other), can this fail by throwing an exception which would not result in a retry? Also looking at https://github.com/numpy/numpy/blob/v2.1.0/numpy/lib/_npyio_impl.py#L507-L588
I think that this could override an existing file with the same name which shouldn't be possible in case someone tries to use the same filename for different images and ends up overwriting all but the last one.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You're right that I need to be more careful here. The exception being thrown would be "safe" but wouldn't result in a retry, giving inconsistent behavior.
I'll double-check about re-using the same name, but my fuzzy memory is that the file dialog gives you an "are you sure?" sort of error, so if we reach np.save
the user has already confirmed that they want to replace the file.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Resolved by refactoring to make specifically OSError
call the same method that we reach via the QImage.save
checks.
Confirmed that the file select dialog warns the user sufficiently if they pick an already existing filename.
@@ -1151,30 +1151,58 @@ def shutdown(self): | |||
|
|||
def onfileSave(self): | |||
try: | |||
fileName = QFileDialog.getSaveFileName( | |||
filename = QFileDialog.getSaveFileName( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you clicked on the "Save to File" button and then decided not to save anything by exiting the dialog, what happens here? Does getSaveFileName return nothing causing an exception?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It ends up returning an empty string. Qt APIs can be awkward like that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Resolved by removing the exception as stated above
camviewer_ui_impl.py
Outdated
# QImage.save returned False, so the save failed. | ||
# Check for obvious errors, then retry the save | ||
# File already exists? This shouldn't happen due to the qt file dialog. | ||
if os.path.exists(filename): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can QImage make the fail but still return False?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The API docs claims that it cannot, but it's good to be skeptical, especially with pyqt as a middleman between us and the qt docs. Maybe I should also simplify here and be less ambitious in checking some of these things.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Resolved by abandoning this check in favor of large simplification. This at least avoids an incorrect message in the case where QImage would have undefined behavior, and it allows the user to freely override existing filenames if they want to for whatever reason.
In addition to the above, I added a check for invalid file types since it's the only other situation I could find where QImage.save would fail aside from permissions issues. There's a limited, predefined set of image types that are valid. |
The diff is somewhat confusing now because it's more like a rewrite than a tweak, but I think this is cleaner than before! |
Related jira: https://jira.slac.stanford.edu/browse/ECS-6918
This PR's primary purpose is to fix a bug where the image saving dialog always reports success.
The reason for this is that
QImage.save
never raises, instead it returnsTrue
orFalse
to signal if the image was saved properly. The original author of this bit of code assumed that the save function would raise with information about what went wrong.This ends up being somewhat annoying, leaving it to us to figure out why the save might have failed.
Changes implemented here:
QImage.save
succeeded or failed