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: find and replace functionality #198

Merged
merged 30 commits into from
Sep 5, 2023

Conversation

tangkong
Copy link
Contributor

@tangkong tangkong commented Aug 21, 2023

Description

  • Implements atef-specific find and replace functionality
  • adds a basic find-and-replace widget
  • finish up / clean up widget button functionality

Motivation and Context

This stems from a request for templated checkouts, which boils down to having find-and-replace functionality. A naive find-and-replace on the json could touch file/field names, and is rather difficult to inspect.

My intention is to make the future "fill template" step / tool wrap this functionality, possibly with some extra visualization or streamlining buttons. A sample checkout could be run through this tool and and "filled" as is with this tool, I imagine

(In reality I implemented the simple find-and-replace helpers, tried making it more robust, and a week later ended up here.)

Creating a step that fills templates mid-checkout is a possible next step, though we'd have to consider expanding each template step on preparation and inserting it into the tree. Inspection of the substeps of that filled template would be un-inspectable unless we expand in edit-mode, and in that case we're basically creating a bunch of filled template checkouts separately.

How Has This Been Tested?

Some unit tests have been added, more need to be...

Where Has This Been Documented?

This PR

Screenshots

image

image

image

Pre-merge checklist

  • Code works interactively
  • Code contains descriptive docstrings, including context and API
  • New/changed functions and methods are covered in the test suite where possible
  • Code has been checked for threading issues (no blocking tasks in GUI thread)
  • Test suite passes locally
  • Test suite passes on GitHub Actions
  • Ran docs/pre-release-notes.sh and created a pre-release documentation page
  • Pre-release docs include context, functional descriptions, and contributors as appropriate

@tangkong tangkong marked this pull request as ready for review August 29, 2023 23:11
@tangkong
Copy link
Contributor Author

This is basically feature complete as far as I care for. There was a good amount of gui-ing here so I'm hoping for some adversarial clicking while I write comments / unit tests.

@tangkong tangkong requested review from ZLLentz and klauer August 30, 2023 15:51
@tangkong tangkong changed the title WIP/ENH: find and replace functionality ENH: find and replace functionality Aug 30, 2023
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clickly clicky seemed to work as I used this and it seems very powerful in a way that I think will be well-appreciated by the users. A few thoughts:

  • Subbing into a big structure can sometimes show a bit too much info. Is there any way we can truncate this so we only show the bit that changed?
  • Is there any simple way we can automatically refresh when I add a new edit? I got confused for a moment at first before realizing that I needed to click the refresh button on the left.

image

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, also: after saving the file, it seems like this whole template window goes blank. I think that will scare some people. I wonder if we should automatically close this and open the file that was just saved?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Subbing into a big structure can sometimes show a bit too much info. Is there any way we can truncate this so we only show the bit that changed?

I gave this my best effort, but I wonder if I can do any better. Currently it shows the deepest dataclass, which I thought was needed to give the change any context at all. But I suppose now that I have {object}.{attr} on the left I could just show the {attr} on the right

Is there any simple way we can automatically refresh when I add a new edit? I got confused for a moment at first before realizing that I needed to click the refresh button on the left.

This is a perpetual issue I have. I always try to hook more editingFinished signals, but find them to either not catch all the cases I'd want or trigger too often. I'll see what I can do here

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've made the previews show just the bit that changed (the last part of the path), as well as gave the user some notice that their file was saved and that they can open it.

I'll give a bit more information in the details list if the user hasn't picked an edit as well

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What would be really neat is if you could preview the checkout bits that changed, maybe even just a color-coded tree of sorts?
image

Might be neat if the tooltip could show an ASCII tree for the key:
image
Something to the effect of:

configs[0]
 - by_attr...

(Weighing an ascii tooltip tree up against the rest of atef functionality... totally top priority, yeah?)

Copy link
Contributor Author

@tangkong tangkong Aug 31, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did think about this, but punted it never to look back. I think it's honestly the ideal visualization, and might not be too much more work, the more I think about it...

This is definitely worth a follow up issue though

Copy link
Member

@ZLLentz ZLLentz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm very positive on this. Pending Ken's review I think this is good enough to merge.

preview_file = copy.deepcopy(self.data.target)

try:
_ = data.apply(target=preview_file)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there some special reason to hold on to this reference?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah I dropped my train of thought here. FindReplaceAction.apply catches errors now, so I need that value to decide whether or not application was successful

Comment on lines +853 to +863
refresh_button = self.button_box.button(QtWidgets.QDialogButtonBox.Retry)
refresh_button.clicked.connect(self.refresh_paths)
refresh_button.setText('')
refresh_button.setToolTip('refresh edit details')
refresh_button.setIcon(qta.icon('ei.refresh'))

apply_button = self.button_box.button(QtWidgets.QDialogButtonBox.Apply)
apply_button.clicked.connect(self.apply_edits)
apply_button.setText('')
apply_button.setToolTip('apply all changes')
apply_button.setIcon(qta.icon('ei.check'))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The lack of self.<thing> = <thing> here confused me, but then I realized that the button_box must be tracking them.


self.actions.append(action)

# this works, but is smelly and bad. Access parent table signals
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the canonical way to do this would be to put a signal on this class, emit it here, and connect it to the parent's parent's signals at some point in the parent's parent, rather than parent twicing. I think this is OK as-is though.

Copy link
Contributor Author

@tangkong tangkong Aug 31, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding a signal on this row that the table widget expects would force me to change the other instances of this table-row-combo to match (and also have that signal), which I felt wasn't worth it at this juncture. I totally agree though, this isn't quite how things should be done.

@@ -367,13 +374,39 @@ def clear_results(self, *args, **kwargs):

current_tree.update_statuses()

def find_replace(self, *args, **kwargs):
""" find and replace in the current tree. Open a find-replace widget """
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't notice this while testing- makes me want to go back and click around again.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is really nifty

I wonder if anyone will get confused by the interface though- it wasn't obvious to me at first that this wasn't doing a find/replace in the same window, rather it is creating a whole new version that I could choose to edit and save seperately

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, it is really cool but I also felt the same regarding the end user's expectations with the dialog.

"Apply All" seems like it should be replacing it in the file, but it's just doing the changes on an in-memory version until you click 'save as' (... right?)
Maybe the dialog should be something like:

"Choose input file: "
"Choose output file: "
(Make edits in this section)
Reload / Preview and Verify / Save

Copy link
Contributor

@klauer klauer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The functionality here is awesome! Nicely done. I think this will serve the end user rather well.

Some thoughts, suggestions, nitpicks, and musings as per usual. Nothing at a level that requires holding this PR up any longer. Sorry it took me so long to get around to reviewing!

item: Any,
match: Callable,
parent: List[Tuple[Any, Any]] = []
) -> Generator:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
) -> Generator:
) -> Generator[List[Tuple[Any, Any]], None, None]:

self.update_replace_fn()

self.change_list.clear()
self.match_paths = walk_find_match(self.orig_file, self._match_fn)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Careful - you exhaust this below so self.match_paths isn't useful
It'd be better to listify it here, probably, even if just due to the dataclass change comment below on 454

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What would be really neat is if you could preview the checkout bits that changed, maybe even just a color-coded tree of sorts?
image

Might be neat if the tooltip could show an ASCII tree for the key:
image
Something to the effect of:

configs[0]
 - by_attr...

(Weighing an ascii tooltip tree up against the rest of atef functionality... totally top priority, yeah?)

return
elif not edit_row_widget.get_details_rows():
l_item = QtWidgets.QListWidgetItem(
'provide search and replace text to show details...'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, it wasn't clear to me why the "edit details" weren't showing:
image
I think this should probably read "Click on edit in the left list to see details; if nothing shows here there is no match" or something to that effect

refresh_button.setToolTip('refresh edit details')
refresh_button.setIcon(qta.icon('ei.refresh'))

apply_button = self.button_box.button(QtWidgets.QDialogButtonBox.Apply)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I click 'apply all' after having changed a device in the template, I'd have expected to see the device/signal list above change:
image

data = deserialize(ProcedureFile, self._original_json)
except ValidationError:
logger.error('failed to open file as either active '
'or passive checkout')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should probably raise and clear original_json
At the very least data is unbound in this except clause

self,
*args,
filepath: Optional[str] = None,
window: Optional[Any] = None,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably can do better than Any here

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TIL typing.TYPE_CHECKING was a thing. circular imports be gone!

def __init__(
self,
*args,
data=None,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

data unused?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My TableWidgetWithAddRow expected its row widgets to be DataWidgets, all of which have a data kwarg. I probably could have made a dataclass to represent an "Edit" but apparently past me didn't feel like it 🤷

"""
Update the title. Will be the name and the number of unapplied edits
"""
file_name = os.path.basename(self.fp)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dealing with None is annoying with respect to the static analyzer but I find it to be good practice - even if you're 100% sure this doesn't get called when fp is None - to guard for it anyway. A future refactor you may forget about it and then get confused why basename(None) is getting called

self.toggle_button = QtWidgets.QToolButton(text=text)
self.toggle_button.setCheckable(True)
self.toggle_button.setChecked(False)
self.toggle_button.setStyleSheet("QToolButton { border: none; }")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aside: we should probably move to making an atef stylesheet at some point and move these things there (atef.qss, oh yeah!)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if that'd fix some of the weird osx rendering differences 🤔 A good idea, but I'm not volunteering right now... 👀

@tangkong
Copy link
Contributor Author

tangkong commented Sep 5, 2023

I think I've addressed all the comments that fit as add-ons to this PR, and made issues for the rest. I'd like to merge this and move on, unless there are any objections

@klauer
Copy link
Contributor

klauer commented Sep 5, 2023

Merge at will!

@tangkong tangkong merged commit 91c4076 into pcdshub:master Sep 5, 2023
9 checks passed
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.

3 participants