Skip to content
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

Open
wants to merge 8 commits into
base: master
Choose a base branch
from

Conversation

ZLLentz
Copy link
Member

@ZLLentz ZLLentz commented Nov 27, 2024

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 returns True or False 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:

  • Correctly check if QImage.save succeeded or failed
  • If it failed, do a best-effort check for simple issues
  • If it failed, ask the user to try a different filename and re-open the dialog
  • Set the default directory to the user's home area, which is guaranteed to be writeable, unlike the current working directory
  • fileName -> filename because the naming was irritating me

@@ -17,6 +17,7 @@

import sys
import os
import os.path

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?

Copy link
Member Author

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.

Copy link

@tangkong tangkong Dec 2, 2024

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.

@ZLLentz ZLLentz marked this pull request as ready for review December 2, 2024 19:22
camviewer_ui_impl.py Outdated Show resolved Hide resolved
# 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
Copy link

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)

Copy link
Member Author

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)

Copy link
Member Author

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

Copy link

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?

Copy link
Member Author

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.

camviewer_ui_impl.py Outdated Show resolved Hide resolved
"Images (*.npy *.jpg *.png *.bmp *.pgm *.tif)",
)[0]
if fileName == "":
if filename == "":

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?

Copy link
Member Author

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".

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)?

Copy link
Member Author

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.

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

Copy link
Member Author

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.

Copy link
Member Author

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)

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.

Copy link
Member Author

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.

Copy link
Member Author

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(

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?

Copy link
Member Author

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.

Copy link
Member Author

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

# 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):

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?

Copy link
Member Author

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.

https://doc.qt.io/qt-5/qimage.html#save

Copy link
Member Author

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.

@ZLLentz
Copy link
Member Author

ZLLentz commented Dec 3, 2024

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.

@ZLLentz
Copy link
Member Author

ZLLentz commented Dec 3, 2024

The diff is somewhat confusing now because it's more like a rewrite than a tweak, but I think this is cleaner than before!

camviewer_ui_impl.py Outdated Show resolved Hide resolved
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants