-
-
Notifications
You must be signed in to change notification settings - Fork 122
Feat: add the to_dict() #1288
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
Open
rodrigobnogueira
wants to merge
26
commits into
aio-libs:master
Choose a base branch
from
rodrigobnogueira:feat/to-dict
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Feat: add the to_dict() #1288
Changes from 23 commits
Commits
Show all changes
26 commits
Select commit
Hold shift + click to select a range
cbde09a
Fix memory leak in pop() when key is not found
daa9f58
feat: Implement to_dict() method
7a243d8
Add comprehensive psleak test suite
15000a6
Enhance psleak tests with 100KB tolerance and 1M iterations
2adb465
Add to_dict leak tests for CIMultiDict and MultiDictProxy
6f29b13
Remove psleak tests (moved to fix/psleak-expansion branch)
86be9e1
Add to_dict feature with comprehensive tests and isolated leak check
d064ebe
Integrate isolated leak test for to_dict into CI suite
77b606d
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] 4455db9
Revert unrelated changes to hashtable.h and pop tests
8d46e65
Restore multidict_pop.py to leak tests (was accidentally removed)
ed6d373
Fix CI linting (mypy/clang-format) and restore update leak test
42e3605
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] 02a1a35
Merge branch 'master' into feat/to-dict
rodrigobnogueira 20c411c
Add changelog entry and fix remaining clang-format issues
8d429f7
Fix PyPy compatibility: use psutil instead of tracemalloc
f2a9cb7
Merge branch 'master' into feat/to-dict
rodrigobnogueira 31b7e77
Merge branch 'master' into feat/to-dict
rodrigobnogueira c710e1d
test: add exact type hints to to_dict tests to fix MyPy coverage
06e097d
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] 636e308
Merge branch 'master' into feat/to-dict
rodrigobnogueira 9b704c8
tests: strict type annotations for test_to_dict for 100% coveralls pr…
9ab8624
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] d58c07f
Resolve formatting conflicts in test_to_dict
1cb0497
tests: Use Optional instead of | Union syntax for Python 3.9 compat
a5920ec
tests: add pragma no cover to DictFactory Protocol
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| Added ``to_dict()`` method to export multidict contents as a standard dict with value lists. (#783) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| """Memory leak test for to_dict().""" | ||
|
|
||
| import gc | ||
| import os | ||
|
|
||
| import psutil | ||
| from multidict import MultiDict | ||
|
|
||
|
|
||
| process = psutil.Process(os.getpid()) | ||
|
|
||
|
|
||
| def trim_ram() -> None: | ||
| gc.collect() | ||
|
|
||
|
|
||
| def get_memory_usage() -> int: | ||
| memory_info = process.memory_info() | ||
| return memory_info.rss // (1024 * 1024) | ||
|
|
||
|
|
||
| def test_to_dict_leak() -> None: | ||
| for _ in range(100): | ||
| d = MultiDict([("a", 1), ("b", 2)]) | ||
| d.to_dict() | ||
| trim_ram() | ||
|
|
||
| mem_before = get_memory_usage() | ||
| for _ in range(1_000_000): | ||
| d = MultiDict([("a", 1), ("b", 2)]) | ||
| d.to_dict() | ||
| trim_ram() | ||
| mem_after = get_memory_usage() | ||
|
|
||
| growth = mem_after - mem_before | ||
| assert growth < 50, f"Memory grew by {growth} MB, possible leak" | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| test_to_dict_leak() | ||
| print("PASSED: No memory leak detected in to_dict()") |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,180 @@ | ||
| """Test to_dict functionality for all multidict types.""" | ||
|
|
||
| from typing import Type, Iterable | ||
| from multidict import MultiMapping | ||
|
|
||
|
|
||
| from typing import Protocol | ||
|
|
||
|
|
||
| from multidict import MultiDict, CIMultiDict, MultiDictProxy, CIMultiDictProxy | ||
|
|
||
|
|
||
| class MultidictModule(Protocol): | ||
| MultiDict: Type[MultiDict[object]] | ||
| CIMultiDict: Type[CIMultiDict[object]] | ||
| MultiDictProxy: Type[MultiDictProxy[object]] | ||
| CIMultiDictProxy: Type[CIMultiDictProxy[object]] | ||
|
|
||
|
|
||
| class DictFactory(Protocol): | ||
| def __call__( | ||
| self, arg: Iterable[tuple[str, object]] | None = None | ||
| ) -> MultiMapping[object]: ... | ||
|
|
||
|
|
||
| import pytest | ||
|
|
||
| from multidict import CIMultiDict, CIMultiDictProxy, MultiDict, MultiDictProxy | ||
|
|
||
|
|
||
| class BaseToDictTests: | ||
| """Base tests for to_dict() method, inherited by all multidict type tests.""" | ||
|
|
||
| def test_to_dict_simple(self, cls: DictFactory) -> None: | ||
| """Test basic conversion with unique keys.""" | ||
| d = cls([("a", 1), ("b", 2)]) | ||
| result = d.to_dict() | ||
| assert result == {"a": [1], "b": [2]} | ||
|
|
||
| def test_to_dict_multi_values(self, cls: DictFactory) -> None: | ||
| """Test grouping multiple values under the same key.""" | ||
| d = cls([("a", 1), ("b", 2), ("a", 3)]) | ||
| result = d.to_dict() | ||
| assert result == {"a": [1, 3], "b": [2]} | ||
|
|
||
| def test_to_dict_empty(self, cls: DictFactory) -> None: | ||
| """Test conversion of an empty multidict.""" | ||
| d = cls() | ||
| result = d.to_dict() | ||
| assert result == {} | ||
|
|
||
| def test_to_dict_returns_new_dict(self, cls: DictFactory) -> None: | ||
| """Test that each call returns a new dictionary instance.""" | ||
| d = cls([("a", 1)]) | ||
| result1 = d.to_dict() | ||
| result2 = d.to_dict() | ||
| assert result1 == result2 | ||
| assert result1 is not result2 | ||
|
|
||
| def test_to_dict_list_is_fresh(self, cls: DictFactory) -> None: | ||
| """Test that value lists are independent between calls.""" | ||
| d = cls([("a", 1)]) | ||
| result1 = d.to_dict() | ||
| result2 = d.to_dict() | ||
| assert result1["a"] is not result2["a"] | ||
|
|
||
| def test_to_dict_order_preservation(self, cls: DictFactory) -> None: | ||
| """Test that value lists maintain insertion order.""" | ||
| d = cls([("x", 3), ("x", 1), ("x", 2)]) | ||
| result = d.to_dict() | ||
| assert result["x"] == [3, 1, 2] | ||
|
|
||
| def test_to_dict_large_data(self, cls: DictFactory) -> None: | ||
| """Test to_dict with a large number of entries for performance.""" | ||
| items = [(f"key{i % 100}", i) for i in range(10000)] | ||
| d = cls(items) | ||
| result = d.to_dict() | ||
| assert len(result) == 100 | ||
| assert all(len(v) == 100 for v in result.values()) | ||
|
|
||
| def test_to_dict_mixed_value_types(self, cls: DictFactory) -> None: | ||
| """Test to_dict with mixed value types (str, int) to verify generic _V.""" | ||
| d = cls([("a", 1), ("a", "two"), ("b", 3.14)]) | ||
| result = d.to_dict() | ||
| assert result["a"] == [1, "two"] | ||
| assert result["b"] == [3.14] | ||
|
|
||
|
|
||
| class TestMultiDictToDict(BaseToDictTests): | ||
| """Tests for MultiDict.to_dict().""" | ||
|
|
||
| @pytest.fixture | ||
| def cls(self, multidict_module: MultidictModule) -> Type[MultiDict[object]]: | ||
| return multidict_module.MultiDict | ||
|
|
||
|
|
||
| class TestCIMultiDictToDict(BaseToDictTests): | ||
| """Tests for CIMultiDict.to_dict().""" | ||
|
|
||
| @pytest.fixture | ||
| def cls(self, multidict_module: MultidictModule) -> Type[CIMultiDict[object]]: | ||
| return multidict_module.CIMultiDict | ||
|
|
||
| def test_to_dict_case_insensitive_grouping(self, cls: DictFactory) -> None: | ||
| """Test that case variants are grouped under the same key.""" | ||
| d = cls([("A", 1), ("a", 2), ("B", 3)]) | ||
| result = d.to_dict() | ||
| assert len(result) == 2 | ||
| assert "A" in result or "a" in result | ||
| assert "B" in result or "b" in result | ||
| key_a = "A" if "A" in result else "a" | ||
| key_b = "B" if "B" in result else "b" | ||
| assert result[key_a] == [1, 2] | ||
| assert result[key_b] == [3] | ||
|
|
||
|
|
||
| class TestMultiDictProxyToDict(BaseToDictTests): | ||
| """Tests for MultiDictProxy.to_dict().""" | ||
|
|
||
| @pytest.fixture | ||
| def cls(self, multidict_module: MultidictModule) -> DictFactory: | ||
| def make_proxy( | ||
| arg: Iterable[tuple[str, object]] | None = None, | ||
| ) -> MultiMapping[object]: | ||
| md: MultiDict[object] = ( | ||
| multidict_module.MultiDict(arg) if arg else multidict_module.MultiDict() | ||
| ) | ||
| return multidict_module.MultiDictProxy(md) | ||
|
|
||
| return make_proxy | ||
|
|
||
| def test_to_dict_proxy_mutation_isolation( | ||
| self, cls: DictFactory, multidict_module: MultidictModule | ||
| ) -> None: | ||
| """Test that modifying returned dict does not affect the proxy.""" | ||
| md: MultiDict[object] = multidict_module.MultiDict([("a", 1)]) | ||
| proxy: MultiMapping[object] = multidict_module.MultiDictProxy(md) | ||
| result = proxy.to_dict() | ||
| result["a"].append(999) | ||
| assert proxy.getall("a") == [1] | ||
|
|
||
|
|
||
| class TestCIMultiDictProxyToDict(BaseToDictTests): | ||
| """Tests for CIMultiDictProxy.to_dict().""" | ||
|
|
||
| @pytest.fixture | ||
| def cls(self, multidict_module: MultidictModule) -> DictFactory: | ||
| def make_proxy( | ||
| arg: Iterable[tuple[str, object]] | None = None, | ||
| ) -> MultiMapping[object]: | ||
| md: CIMultiDict[object] = ( | ||
| multidict_module.CIMultiDict(arg) | ||
| if arg | ||
| else multidict_module.CIMultiDict() | ||
| ) | ||
| return multidict_module.CIMultiDictProxy(md) | ||
|
|
||
| return make_proxy | ||
|
|
||
| def test_to_dict_case_insensitive_grouping(self, cls: DictFactory) -> None: | ||
| """Test that case variants are grouped under the same key.""" | ||
| d = cls([("A", 1), ("a", 2), ("B", 3)]) | ||
| result = d.to_dict() | ||
| assert len(result) == 2 | ||
| assert "A" in result or "a" in result | ||
| assert "B" in result or "b" in result | ||
| key_a = "A" if "A" in result else "a" | ||
| key_b = "B" if "B" in result else "b" | ||
| assert result[key_a] == [1, 2] | ||
| assert result[key_b] == [3] | ||
|
|
||
| def test_to_dict_proxy_mutation_isolation( | ||
| self, cls: DictFactory, multidict_module: MultidictModule | ||
| ) -> None: | ||
| """Test that modifying returned dict does not affect the proxy.""" | ||
| md: CIMultiDict[object] = multidict_module.CIMultiDict([("a", 1)]) | ||
| proxy: MultiMapping[object] = multidict_module.CIMultiDictProxy(md) | ||
| result = proxy.to_dict() | ||
| result["a"].append(999) | ||
| assert proxy.getall("a") == [1] | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.