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

Python wrapper entry cache adjustment #354

Merged
merged 3 commits into from
Jan 29, 2025

Conversation

andrewwhitehead
Copy link
Contributor

From the Python library documentation it appears that the native lru_cache decorator could result in caching of Entry instances longer than necessary. This PR switches to a custom cache implementation at the EntryList level which should avoid any undesired behaviour.

@ff137

Copy link
Contributor

@ff137 ff137 left a comment

Choose a reason for hiding this comment

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

I think it's very much worthwhile to add a unit test that asserts caching works as expected.

GPT helped me generate the following -- maybe it's helpful inspiration, but obviously just auto-generated and requires more attention to detail:

import unittest
from unittest.mock import patch, MagicMock
from aries_askar.bindings.handle import EntryListHandle

class TestEntryCache(unittest.TestCase):
    @patch('aries_askar.bindings.lib.Lib')
    def test_get_category_caching(self, mock_lib):
        # Setup the mock for Lib().invoke
        mock_invoke = MagicMock()
        # Define return values for different indices
        mock_invoke.side_effect = [
            'Category1',  # For index 0
            'Category2',  # For index 1
        ]
        mock_lib_instance = mock_lib.return_value
        mock_lib_instance.invoke.return_value = None  # Assuming invoke modifies the StrBuffer in place

        # Initialize EntryListHandle
        handle = EntryListHandle(value=123)

        with patch('aries_askar.bindings.lib.StrBuffer') as MockStrBuffer:
            # Mock StrBuffer to return the desired string
            mock_str_buffer_instance = MockStrBuffer.return_value
            mock_str_buffer_instance.__str__.return_value = mock_invoke()

            # First call with index 0
            category1_first = handle.get_category(0)
            self.assertEqual(category1_first, 'Category1')
            mock_lib_instance.invoke.assert_called_with(
                "askar_entry_list_get_category",
                (EntryListHandle, int, unittest.mock.ANY),
                handle,
                0,
                unittest.mock.ANY,
            )

            # Reset mock to track subsequent calls
            mock_lib_instance.invoke.reset_mock()

            # Second call with index 0 (should use cache)
            category1_second = handle.get_category(0)
            self.assertEqual(category1_second, 'Category1')
            mock_lib_instance.invoke.assert_not_called()

            # Call with a new index 1
            mock_str_buffer_instance.__str__.return_value = mock_invoke()  # Update return value
            category2 = handle.get_category(1)
            self.assertEqual(category2, 'Category2')
            mock_lib_instance.invoke.assert_called_with(
                "askar_entry_list_get_category",
                (EntryListHandle, int, unittest.mock.ANY),
                handle,
                1,
                unittest.mock.ANY,
            )

            # Reset mock to track subsequent calls
            mock_lib_instance.invoke.reset_mock()

            # Call again with index 1 (should use cache)
            category2_second = handle.get_category(1)
            self.assertEqual(category2_second, 'Category2')
            mock_lib_instance.invoke.assert_not_called()

if __name__ == '__main__':
    unittest.main()

Comment on lines 64 to 70
cache[ckey] = res
if isinstance(res, dict):
# make sure the cached copy is not mutated
res = dict(res)
return res
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this correct? If res is a dict, it's cast to a dict?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The dict constructor creates a new instance by iterating through the old instance's entries, but I changed it to a deep copy anyway in case there's nested structure.

Copy link
Contributor

Choose a reason for hiding this comment

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

Aah I see now -- that modifying res downstream would modify the same object stored in cache. Was a lil confusing at first

@andrewwhitehead
Copy link
Contributor Author

I've added a unit test that should cover the entry_cache expectations, thanks.

Copy link
Contributor

@ff137 ff137 left a comment

Choose a reason for hiding this comment

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

Nice 👍

@swcurran swcurran merged commit dba65c0 into openwallet-foundation:main Jan 29, 2025
28 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