Is there a better way to duplicate note in note_will_be_added?

What’s the best way to duplicate a Note class instance as a note is being added in the note_will_be_added hook? The following don’t work:

  • duplicate_note = Note(id=note.id, col=note.col), new notes don’t exist in the collection so note.load() fails.
  • duplicate_note = copy.deepcopy(note), fails as it tries to copy some rust backend link

The use case is that I’m modifying the note using values from the note in a sequence of operations, where each operation modifies the note. If I were to just use the same Note instance as the source there will occur undesired effects as the source data is modified during the process.

My current solution is this hacky function:

def duplicate_note(note: Note) -> Note:
    """
    Duplicate a note by creating a new instance and copying the fields.
    Using copy.deepcopy on a Note object does not work, and Note(id=note.id) does not
    work on a new note (where id is 0), thus this utility function.
    """
    new_note = Note(col=note.col, model=note.note_type())

    # Copied code from notes.py _to_backend_note method
    # the method calls hooks.note_will_flush(self) which is not desired here
    # This code may break if the Note class changes in the future.
    backend_note = notes_pb2.Note(
        id=note.id,
        guid=note.guid,
        notetype_id=note.mid,
        mtime_secs=note.mod,
        usn=note.usn,
        tags=note.tags,
        fields=note.fields,
    )
    # Calling internal method that is not part of the public API, so this may break if the
    # Note class changes in the future.
    new_note._load_from_backend_note(backend_note)
    return new_note

I’d like to know if there’s some method that’d be less likely to break in the future.

I’m wondering if adding a duplicate_note method to notes.py would actually be as simple as adding one new internal method that does the copying to notes_pb2.Note only and use that in the duplicate method (exactly as my function above does):

    def _make_backend_note(self) -> notes_pb2.Note:
        return notes_pb2.Note(
            id=self.id,
            guid=self.guid,
            notetype_id=self.mid,
            mtime_secs=self.mod,
            usn=self.usn,
            tags=self.tags,
            fields=self.fields,
        )

    def _to_backend_note(self) -> notes_pb2.Note:
        hooks.note_will_flush(self)
        return self._make_backend_note(self)
    
    def duplicate_note(self) -> Note:
        dupe_note = Note(col=self.col, model=self.note_type())
        dupe_note._load_from_backend_note(self._to_backend_note())
        return dupe_note

I’d be happy to accept a PR that handles this in __deepcopy__, by e.g. nulling out the backend handle prior to copying, then restoring it afterwards so both objects share it.

Not exactly sure how a __deepcopy__ customization is supposed to be done. So, is the general idea that you should use the original deepcopy like this, whenever possible? Without testing this, I guess this ought to be using the default __deepcopy__ on the note by detaching the col ref and then re-attaching it to the original and copy at the end. If this looks like the right path, I’ll test an implementation along these lines on my addon.

    def __deepcopy__(self, memo):
        # Unset col ref so we can deepcopy normally
        col_ref = self.col
        self.col = None

        # Perform default deepcopy
        deepcopy_method = self.__deepcopy__
        self.__deepcopy__ = None
        dupe_note= deepcopy(self, memo)
        self.__deepcopy__ = deepcopy_method
        
        # Bind to dupe_note by types.MethodType, so a subsequent deepcopy on the
        # copy copies it and not the original
        dupe_note.__deepcopy__ = types.MethodType(deepcopy_method.__func__, cp)

        # Reattach col ref to original and dupe
        self.col = col_ref
        dupe_note.col = col_ref

        return dupe_note 

Explict might be better? Something like:

    def __deepcopy__(self, memo):
        from copy import deepcopy
        new = type(self).__new__(type(self))
        memo[id(self)] = new
        for k,v in self.__dict__.items():
            setattr(new, k, v if k=="col" else deepcopy(v, memo))
        return new

FWIW, I would be happy to see us drop the col property from our other objects like Note, and pass it in when required instead, as that makes it more explicit when the collection is being accessed. Doing so without breaking existing add-ons might be tricky though.