From 3a6396825b46b5ad0fc93326e0e7a2fbdb8c35c5 Mon Sep 17 00:00:00 2001 From: Auguste Baum Date: Wed, 18 Sep 2024 14:41:35 +0200 Subject: [PATCH 01/17] [squashme] wip --- tests/unit/test_project.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/tests/unit/test_project.py b/tests/unit/test_project.py index 553e08130..aed061384 100644 --- a/tests/unit/test_project.py +++ b/tests/unit/test_project.py @@ -175,3 +175,20 @@ def test_report_layout(project): project.put_report_layout(layout) assert project.get_report_layout() == layout + + +def test_put_several_items(project): + project.put({"a": "foo", "b": "bar"}) + assert project.list_keys() == ["a", "b"] + + +def test_put_several_items_nested(project): + project.put({"a": {"b": "baz"}}) + assert project.list_keys() == ["a"] + assert project.get("a") == {"b": "baz"} + + +def test_put_several_items_error(project): + with pytest.raises(NotImplementedError): + project.put({"a": "foo", "b": (lambda: "unsupported object")}) + assert project.list_keys() == [] From 976ffe7bb39379bb87cc9a48712a822ec7dc77c8 Mon Sep 17 00:00:00 2001 From: Auguste Baum Date: Thu, 19 Sep 2024 10:24:44 +0200 Subject: [PATCH 02/17] [squashme] wip --- src/skore/project.py | 11 +++++++++++ tests/unit/test_project.py | 17 +++++++++++------ 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/src/skore/project.py b/src/skore/project.py index ae8fdde1e..f9343d470 100644 --- a/src/skore/project.py +++ b/src/skore/project.py @@ -40,6 +40,17 @@ def put(self, key: str, value: Any): ) self.put_item(key, object_to_item(value)) + def put_several(self, key_value_pairs: dict[str, Any]): + """Add several values to the Project. + + All values must be valid, or none of them will be added to the Project. + """ + key_item_pairs = { + key: object_to_item(value) for key, value in key_value_pairs.items() + } + for key, item in key_item_pairs.items(): + self.put_item(key, item) + def put_item(self, key: str, item: Item): """Add an Item to the Project.""" self.item_repository.put_item(key, item) diff --git a/tests/unit/test_project.py b/tests/unit/test_project.py index aed061384..0f0fbdad8 100644 --- a/tests/unit/test_project.py +++ b/tests/unit/test_project.py @@ -177,18 +177,23 @@ def test_report_layout(project): assert project.get_report_layout() == layout -def test_put_several_items(project): - project.put({"a": "foo", "b": "bar"}) +def test_put_several(project): + project.put_several({"a": "foo", "b": "bar"}) assert project.list_keys() == ["a", "b"] -def test_put_several_items_nested(project): - project.put({"a": {"b": "baz"}}) +def test_put_several_nested(project): + project.put_several({"a": {"b": "baz"}}) assert project.list_keys() == ["a"] assert project.get("a") == {"b": "baz"} -def test_put_several_items_error(project): +def test_put_several_error(project): with pytest.raises(NotImplementedError): - project.put({"a": "foo", "b": (lambda: "unsupported object")}) + project.put_several({"a": "foo", "b": (lambda: "unsupported object")}) assert project.list_keys() == [] + + +def test_put_several_int_keys(project): + project.put_several({0: "foo", 1: "bar"}) + assert project.list_keys() == ["a", "b"] From 361a27089d7cd05cefb94d655d75d5304bb11674 Mon Sep 17 00:00:00 2001 From: Auguste Baum Date: Thu, 19 Sep 2024 14:53:01 +0200 Subject: [PATCH 03/17] [squashme] wip --- src/skore/project.py | 5 ++++- tests/unit/test_project.py | 16 ++++++++-------- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/skore/project.py b/src/skore/project.py index f9343d470..8b4c0ca42 100644 --- a/src/skore/project.py +++ b/src/skore/project.py @@ -1,5 +1,6 @@ """Define a Project.""" +from functools import singledispatchmethod from pathlib import Path from typing import Any @@ -32,6 +33,7 @@ def __init__( self.item_repository = item_repository self.layout_repository = layout_repository + @singledispatchmethod def put(self, key: str, value: Any): """Add a value to the Project.""" if not isinstance(key, str): @@ -40,7 +42,8 @@ def put(self, key: str, value: Any): ) self.put_item(key, object_to_item(value)) - def put_several(self, key_value_pairs: dict[str, Any]): + @put.register + def put_several(self, key_value_pairs: dict): """Add several values to the Project. All values must be valid, or none of them will be added to the Project. diff --git a/tests/unit/test_project.py b/tests/unit/test_project.py index 0f0fbdad8..5076d0e29 100644 --- a/tests/unit/test_project.py +++ b/tests/unit/test_project.py @@ -178,22 +178,22 @@ def test_report_layout(project): def test_put_several(project): - project.put_several({"a": "foo", "b": "bar"}) + project.put({"a": "foo", "b": "bar"}) + assert project.list_keys() == ["a", "b"] + + +def test_put_several_alias(project): + project.put({"a": "foo", "b": "bar"}) assert project.list_keys() == ["a", "b"] def test_put_several_nested(project): - project.put_several({"a": {"b": "baz"}}) + project.put({"a": {"b": "baz"}}) assert project.list_keys() == ["a"] assert project.get("a") == {"b": "baz"} def test_put_several_error(project): with pytest.raises(NotImplementedError): - project.put_several({"a": "foo", "b": (lambda: "unsupported object")}) + project.put({"a": "foo", "b": (lambda: "unsupported object")}) assert project.list_keys() == [] - - -def test_put_several_int_keys(project): - project.put_several({0: "foo", 1: "bar"}) - assert project.list_keys() == ["a", "b"] From 61f0a10099b4bd58f619cf9873a3b00ebbb8ed80 Mon Sep 17 00:00:00 2001 From: Auguste Baum Date: Thu, 19 Sep 2024 15:29:16 +0200 Subject: [PATCH 04/17] Make it a `TypeError` when `Project.put` receives a non-string key Closes #362 --- src/skore/project.py | 9 ++++----- tests/unit/test_project.py | 15 +++++++++++++++ 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/src/skore/project.py b/src/skore/project.py index 8b4c0ca42..1ec7d6007 100644 --- a/src/skore/project.py +++ b/src/skore/project.py @@ -36,11 +36,7 @@ def __init__( @singledispatchmethod def put(self, key: str, value: Any): """Add a value to the Project.""" - if not isinstance(key, str): - raise KeyTypeError( - f"Key must be a string; '{key}' is of type '{type(key)}'" - ) - self.put_item(key, object_to_item(value)) + self.put_several({key: value}) @put.register def put_several(self, key_value_pairs: dict): @@ -48,6 +44,9 @@ def put_several(self, key_value_pairs: dict): All values must be valid, or none of them will be added to the Project. """ + for key in key_value_pairs: + if not isinstance(key, str): + raise TypeError(f"All keys must be strings; key {key} is {type(key)}") key_item_pairs = { key: object_to_item(value) for key, value in key_value_pairs.items() } diff --git a/tests/unit/test_project.py b/tests/unit/test_project.py index 5076d0e29..924432d5f 100644 --- a/tests/unit/test_project.py +++ b/tests/unit/test_project.py @@ -197,3 +197,18 @@ def test_put_several_error(project): with pytest.raises(NotImplementedError): project.put({"a": "foo", "b": (lambda: "unsupported object")}) assert project.list_keys() == [] + + +def test_put_key_is_a_tuple(project): + with pytest.raises(TypeError): + project.put(("a", "foo"), ("b", "bar")) + + +def test_put_key_is_a_set(project): + with pytest.raises(TypeError): + project.put(set(), "hello") + + +def test_put_key_is_an_int(project): + with pytest.raises(TypeError): + project.put(0, "hello") From 29858977a1a466ce2e0e8f9c827b1eb497eb394b Mon Sep 17 00:00:00 2001 From: Auguste Baum Date: Thu, 19 Sep 2024 15:34:39 +0200 Subject: [PATCH 05/17] [squashme] wip --- src/skore/project.py | 4 +++- tests/unit/test_project.py | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/skore/project.py b/src/skore/project.py index 1ec7d6007..2b6babd5c 100644 --- a/src/skore/project.py +++ b/src/skore/project.py @@ -46,7 +46,9 @@ def put_several(self, key_value_pairs: dict): """ for key in key_value_pairs: if not isinstance(key, str): - raise TypeError(f"All keys must be strings; key {key} is {type(key)}") + raise TypeError( + f"All keys must be strings; key '{key}' is of type '{type(key)}'" + ) key_item_pairs = { key: object_to_item(value) for key, value in key_value_pairs.items() } diff --git a/tests/unit/test_project.py b/tests/unit/test_project.py index 924432d5f..e7b26a00c 100644 --- a/tests/unit/test_project.py +++ b/tests/unit/test_project.py @@ -182,8 +182,8 @@ def test_put_several(project): assert project.list_keys() == ["a", "b"] -def test_put_several_alias(project): - project.put({"a": "foo", "b": "bar"}) +def test_put_several_canonical(project): + project.put_several({"a": "foo", "b": "bar"}) assert project.list_keys() == ["a", "b"] From 49bc9e1a7835821c870a9e613a2dd2982ab3e239 Mon Sep 17 00:00:00 2001 From: Auguste Baum Date: Thu, 19 Sep 2024 17:04:37 +0200 Subject: [PATCH 06/17] loop on values only once --- src/skore/project.py | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/skore/project.py b/src/skore/project.py index 2b6babd5c..05cb1b63e 100644 --- a/src/skore/project.py +++ b/src/skore/project.py @@ -39,20 +39,23 @@ def put(self, key: str, value: Any): self.put_several({key: value}) @put.register - def put_several(self, key_value_pairs: dict): + def put_several(self, key_to_value: dict): """Add several values to the Project. All values must be valid, or none of them will be added to the Project. """ - for key in key_value_pairs: - if not isinstance(key, str): - raise TypeError( - f"All keys must be strings; key '{key}' is of type '{type(key)}'" - ) - key_item_pairs = { - key: object_to_item(value) for key, value in key_value_pairs.items() + + def check_str(key): + if isinstance(key, str): + return key + raise TypeError( + f"All keys must be strings; key '{key}' is of type '{type(key)}'" + ) + + key_to_item = { + check_str(key): object_to_item(value) for key, value in key_to_value.items() } - for key, item in key_item_pairs.items(): + for key, item in key_to_item.items(): self.put_item(key, item) def put_item(self, key: str, item: Item): From fc2242f580405c766797c188d45dc7281034594a Mon Sep 17 00:00:00 2001 From: Auguste Baum Date: Mon, 23 Sep 2024 11:12:54 +0200 Subject: [PATCH 07/17] [squashme] wip --- src/skore/project.py | 33 ++++++++++++++++++++++----------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/src/skore/project.py b/src/skore/project.py index 05cb1b63e..f9dca511d 100644 --- a/src/skore/project.py +++ b/src/skore/project.py @@ -44,19 +44,30 @@ def put_several(self, key_to_value: dict): All values must be valid, or none of them will be added to the Project. """ + errors = [] + + for key, value in key_to_value.items(): + key_error = None + value_error = None + + if not isinstance(key, str): + key_error = TypeError( + f"All keys must be strings; key '{key}' is of type '{type(key)}'" + ) + errors.append(key_error) + + try: + item = object_to_item(value) + except NotImplementedError as e: + value_error = e + errors.append(value_error) + + if key_error or value_error: + continue - def check_str(key): - if isinstance(key, str): - return key - raise TypeError( - f"All keys must be strings; key '{key}' is of type '{type(key)}'" - ) - - key_to_item = { - check_str(key): object_to_item(value) for key, value in key_to_value.items() - } - for key, item in key_to_item.items(): self.put_item(key, item) + + warnings.warn(errors) def put_item(self, key: str, item: Item): """Add an Item to the Project.""" From ce326250e2be66477677e77db57596c1cc2b5122 Mon Sep 17 00:00:00 2001 From: Auguste Baum Date: Mon, 23 Sep 2024 11:59:46 +0200 Subject: [PATCH 08/17] Insert all key-value pairs that can, and warn or raise at the end. --- pyproject.toml | 1 + src/skore/project.py | 62 +++++++++++++++++++++++++++++++------- tests/unit/test_project.py | 28 +++++++++-------- 3 files changed, 68 insertions(+), 23 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index a9ba31cbc..cf89b6d2b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,6 +25,7 @@ dependencies = [ "skops", "uvicorn", ] +requires-python = ">= 3.11" [project.optional-dependencies] test = [ diff --git a/src/skore/project.py b/src/skore/project.py index f9dca511d..ad9b4bfa8 100644 --- a/src/skore/project.py +++ b/src/skore/project.py @@ -1,8 +1,9 @@ """Define a Project.""" +import logging from functools import singledispatchmethod from pathlib import Path -from typing import Any +from typing import Any, Literal from skore.item import ( Item, @@ -17,9 +18,7 @@ from skore.layout import Layout, LayoutRepository from skore.persistence.disk_cache_storage import DirectoryDoesNotExist, DiskCacheStorage - -class KeyTypeError(Exception): - """Key must be a string.""" +logger = logging.getLogger(__name__) class Project: @@ -34,15 +33,41 @@ def __init__( self.layout_repository = layout_repository @singledispatchmethod - def put(self, key: str, value: Any): - """Add a value to the Project.""" - self.put_several({key: value}) + def put(self, key: str, value: Any, on_error: Literal["warn", "raise"] = "warn"): + """Add a value to the Project. + + Parameters + ---------- + key : str + The key to associate with `value` in the Project. Must be a string. + value : Any + The value to associate with `key` in the Project. + on_error : "warn" or "raise" + Upon error (e.g. if the key is not a string), whether to raise an error or + to print a warning. + """ + self.put_several({key: value}, on_error) @put.register - def put_several(self, key_to_value: dict): + def put_several( + self, key_to_value: dict, on_error: Literal["warn", "raise"] = "warn" + ): """Add several values to the Project. - All values must be valid, or none of them will be added to the Project. + All valid key-value pairs will be added to the Project; errors caused + by invalid key-value pairs (e.g. if the key is not a string) will be + collected and shown all at once after all the insertions, either as a + warning or an Exception, depending on the value of `on_error`. + + Parameters + ---------- + key : str + The key to associate with `value` in the Project. Must be a string. + value : Any + The value to associate with `key` in the Project. + on_error : "warn" or "raise" + Upon error (e.g. if the key is not a string), whether to raise an error or + to print a warning. """ errors = [] @@ -66,8 +91,23 @@ def put_several(self, key_to_value: dict): continue self.put_item(key, item) - - warnings.warn(errors) + + if errors: + match on_error: + case "raise": + raise ExceptionGroup( + "Several items could not be inserted in the Project", errors + ) + case "warn": + logger.warning( + "Several items could not be inserted in the Project " + f"due to the following errors: {errors}" + ) + case _: + ValueError( + 'on_error must be set to either "warn" or "raise"; ' + f"got {on_error}" + ) def put_item(self, key: str, item: Item): """Add an Item to the Project.""" diff --git a/tests/unit/test_project.py b/tests/unit/test_project.py index e7b26a00c..5caae21ef 100644 --- a/tests/unit/test_project.py +++ b/tests/unit/test_project.py @@ -14,7 +14,7 @@ from skore.layout import LayoutRepository from skore.layout.layout import LayoutItem, LayoutItemSize from skore.persistence.in_memory_storage import InMemoryStorage -from skore.project import KeyTypeError, Project, ProjectLoadError, load +from skore.project import Project, ProjectLoadError, load @pytest.fixture @@ -138,8 +138,8 @@ def test_put_twice(project): def test_put_int_key(project): - with pytest.raises(KeyTypeError): - project.put(0, "hello") + # Warns that 0 is not a string, but doesn't raise + project.put(0, "hello") assert project.list_keys() == [] @@ -177,12 +177,13 @@ def test_report_layout(project): assert project.get_report_layout() == layout -def test_put_several(project): +def test_put_several_happy_path(project): project.put({"a": "foo", "b": "bar"}) assert project.list_keys() == ["a", "b"] def test_put_several_canonical(project): + """Use `put_several` instead of the `put` alias.""" project.put_several({"a": "foo", "b": "bar"}) assert project.list_keys() == ["a", "b"] @@ -194,21 +195,24 @@ def test_put_several_nested(project): def test_put_several_error(project): - with pytest.raises(NotImplementedError): - project.put({"a": "foo", "b": (lambda: "unsupported object")}) - assert project.list_keys() == [] + """If some key-value pairs are wrong, add all that are valid and print a warning.""" + project.put({"a": "foo", "b": (lambda: "unsupported object")}) + assert project.list_keys() == ["a"] def test_put_key_is_a_tuple(project): - with pytest.raises(TypeError): - project.put(("a", "foo"), ("b", "bar")) + """If key is not a string, warn.""" + project.put(("a", "foo"), ("b", "bar")) + assert project.list_keys() == [] def test_put_key_is_a_set(project): + """Cannot use an unhashable type as a key.""" with pytest.raises(TypeError): project.put(set(), "hello") -def test_put_key_is_an_int(project): - with pytest.raises(TypeError): - project.put(0, "hello") +def test_put_wrong_key_and_value_raise(project): + """When `on_error` is "raise", raise.""" + with pytest.raises(ExceptionGroup): + project.put(0, (lambda: "unsupported object"), on_error="raise") From f90462b76a26566f9926534007555c6bfaeb5717 Mon Sep 17 00:00:00 2001 From: Auguste Baum Date: Mon, 23 Sep 2024 12:06:18 +0200 Subject: [PATCH 09/17] please ruff These are checks that were activated when we set 'requires-python = ">= 3.11"' in pyproject.toml --- src/skore/item/primitive_item.py | 26 ++++++++++----------- src/skore/persistence/abstract_storage.py | 3 ++- src/skore/persistence/disk_cache_storage.py | 3 ++- src/skore/persistence/in_memory_storage.py | 3 ++- 4 files changed, 18 insertions(+), 17 deletions(-) diff --git a/src/skore/item/primitive_item.py b/src/skore/item/primitive_item.py index 21a005301..7793f6b0e 100644 --- a/src/skore/item/primitive_item.py +++ b/src/skore/item/primitive_item.py @@ -10,28 +10,26 @@ from skore.item.item import Item if TYPE_CHECKING: - from typing import Union - - Primitive = Union[ - bool, - float, - int, - str, - list["Primitive"], - tuple["Primitive"], - dict[str | int | float, "Primitive"], - ] + Primitive = ( + bool + | float + | int + | str + | list["Primitive"] + | tuple["Primitive"] + | dict[str | int | float, "Primitive"] + ) def is_primitive(obj: object) -> bool: """Check if the object is a primitive.""" - if isinstance(obj, (bool, float, int, str)): + if isinstance(obj, bool | float | int | str): return True - if isinstance(obj, (list, tuple)): + if isinstance(obj, list | tuple): return all(is_primitive(item) for item in obj) if isinstance(obj, dict): return all( - isinstance(k, (bool, float, int, str)) and is_primitive(v) + isinstance(k, bool | float | int | str) and is_primitive(v) for k, v in obj.items() ) return False diff --git a/src/skore/persistence/abstract_storage.py b/src/skore/persistence/abstract_storage.py index f978a05c6..12b39d7e0 100644 --- a/src/skore/persistence/abstract_storage.py +++ b/src/skore/persistence/abstract_storage.py @@ -1,7 +1,8 @@ """Abstract storage interface.""" from abc import ABC, abstractmethod -from typing import Any, Iterator +from collections.abc import Iterator +from typing import Any class AbstractStorage(ABC): diff --git a/src/skore/persistence/disk_cache_storage.py b/src/skore/persistence/disk_cache_storage.py index 9deeba869..cb7cd2dea 100644 --- a/src/skore/persistence/disk_cache_storage.py +++ b/src/skore/persistence/disk_cache_storage.py @@ -1,7 +1,8 @@ """In-memory storage.""" +from collections.abc import Iterator from pathlib import Path -from typing import Any, Iterator +from typing import Any from diskcache import Cache diff --git a/src/skore/persistence/in_memory_storage.py b/src/skore/persistence/in_memory_storage.py index 3bfd7eb64..73ccf201a 100644 --- a/src/skore/persistence/in_memory_storage.py +++ b/src/skore/persistence/in_memory_storage.py @@ -1,6 +1,7 @@ """In-memory storage.""" -from typing import Any, Iterator +from collections.abc import Iterator +from typing import Any from .abstract_storage import AbstractStorage From 9e37d85610574d916bf6bb396ffdd6ddc1db695f Mon Sep 17 00:00:00 2001 From: Auguste Baum Date: Mon, 23 Sep 2024 15:00:57 +0200 Subject: [PATCH 10/17] Test that warning occurred --- tests/unit/test_project.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/unit/test_project.py b/tests/unit/test_project.py index 5caae21ef..c374742cd 100644 --- a/tests/unit/test_project.py +++ b/tests/unit/test_project.py @@ -137,9 +137,10 @@ def test_put_twice(project): assert project.get("key2") == 5 -def test_put_int_key(project): +def test_put_int_key(project, caplog): # Warns that 0 is not a string, but doesn't raise project.put(0, "hello") + assert len(caplog.record_tuples) == 1 assert project.list_keys() == [] From 97a3ac05ee797bbf0555aa4c9b409c04893ade90 Mon Sep 17 00:00:00 2001 From: Auguste Baum Date: Mon, 23 Sep 2024 15:03:32 +0200 Subject: [PATCH 11/17] Make `on_error` argument docs more precise --- src/skore/project.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/skore/project.py b/src/skore/project.py index ad9b4bfa8..f330d494d 100644 --- a/src/skore/project.py +++ b/src/skore/project.py @@ -42,9 +42,9 @@ def put(self, key: str, value: Any, on_error: Literal["warn", "raise"] = "warn") The key to associate with `value` in the Project. Must be a string. value : Any The value to associate with `key` in the Project. - on_error : "warn" or "raise" + on_error : "warn" or "raise", optional Upon error (e.g. if the key is not a string), whether to raise an error or - to print a warning. + to print a warning. Default is "warn". """ self.put_several({key: value}, on_error) @@ -65,9 +65,9 @@ def put_several( The key to associate with `value` in the Project. Must be a string. value : Any The value to associate with `key` in the Project. - on_error : "warn" or "raise" + on_error : "warn" or "raise", optional Upon error (e.g. if the key is not a string), whether to raise an error or - to print a warning. + to print a warning. Default is "warn". """ errors = [] From 29471273071b7204b8014d10ee3fa1069759eb9d Mon Sep 17 00:00:00 2001 From: Auguste Baum Date: Mon, 23 Sep 2024 15:03:47 +0200 Subject: [PATCH 12/17] if `on_error` is something other than "raise", warn --- src/skore/project.py | 24 +++++++++--------------- 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/src/skore/project.py b/src/skore/project.py index f330d494d..acaac7857 100644 --- a/src/skore/project.py +++ b/src/skore/project.py @@ -93,21 +93,15 @@ def put_several( self.put_item(key, item) if errors: - match on_error: - case "raise": - raise ExceptionGroup( - "Several items could not be inserted in the Project", errors - ) - case "warn": - logger.warning( - "Several items could not be inserted in the Project " - f"due to the following errors: {errors}" - ) - case _: - ValueError( - 'on_error must be set to either "warn" or "raise"; ' - f"got {on_error}" - ) + if on_error == "raise": + raise ExceptionGroup( + "Several items could not be inserted in the Project", errors + ) + + logger.warning( + "Several items could not be inserted in the Project " + f"due to the following errors: {errors}" + ) def put_item(self, key: str, item: Item): """Add an Item to the Project.""" From 5bf8dcb46478526016b735bde82ecf6d3a32a288 Mon Sep 17 00:00:00 2001 From: Auguste Baum Date: Tue, 24 Sep 2024 11:07:32 +0200 Subject: [PATCH 13/17] Revert "please ruff" This reverts commit f90462b76a26566f9926534007555c6bfaeb5717. --- src/skore/item/primitive_item.py | 26 +++++++++++---------- src/skore/persistence/abstract_storage.py | 3 +-- src/skore/persistence/disk_cache_storage.py | 3 +-- src/skore/persistence/in_memory_storage.py | 3 +-- 4 files changed, 17 insertions(+), 18 deletions(-) diff --git a/src/skore/item/primitive_item.py b/src/skore/item/primitive_item.py index 7793f6b0e..21a005301 100644 --- a/src/skore/item/primitive_item.py +++ b/src/skore/item/primitive_item.py @@ -10,26 +10,28 @@ from skore.item.item import Item if TYPE_CHECKING: - Primitive = ( - bool - | float - | int - | str - | list["Primitive"] - | tuple["Primitive"] - | dict[str | int | float, "Primitive"] - ) + from typing import Union + + Primitive = Union[ + bool, + float, + int, + str, + list["Primitive"], + tuple["Primitive"], + dict[str | int | float, "Primitive"], + ] def is_primitive(obj: object) -> bool: """Check if the object is a primitive.""" - if isinstance(obj, bool | float | int | str): + if isinstance(obj, (bool, float, int, str)): return True - if isinstance(obj, list | tuple): + if isinstance(obj, (list, tuple)): return all(is_primitive(item) for item in obj) if isinstance(obj, dict): return all( - isinstance(k, bool | float | int | str) and is_primitive(v) + isinstance(k, (bool, float, int, str)) and is_primitive(v) for k, v in obj.items() ) return False diff --git a/src/skore/persistence/abstract_storage.py b/src/skore/persistence/abstract_storage.py index 12b39d7e0..f978a05c6 100644 --- a/src/skore/persistence/abstract_storage.py +++ b/src/skore/persistence/abstract_storage.py @@ -1,8 +1,7 @@ """Abstract storage interface.""" from abc import ABC, abstractmethod -from collections.abc import Iterator -from typing import Any +from typing import Any, Iterator class AbstractStorage(ABC): diff --git a/src/skore/persistence/disk_cache_storage.py b/src/skore/persistence/disk_cache_storage.py index cb7cd2dea..9deeba869 100644 --- a/src/skore/persistence/disk_cache_storage.py +++ b/src/skore/persistence/disk_cache_storage.py @@ -1,8 +1,7 @@ """In-memory storage.""" -from collections.abc import Iterator from pathlib import Path -from typing import Any +from typing import Any, Iterator from diskcache import Cache diff --git a/src/skore/persistence/in_memory_storage.py b/src/skore/persistence/in_memory_storage.py index 73ccf201a..3bfd7eb64 100644 --- a/src/skore/persistence/in_memory_storage.py +++ b/src/skore/persistence/in_memory_storage.py @@ -1,7 +1,6 @@ """In-memory storage.""" -from collections.abc import Iterator -from typing import Any +from typing import Any, Iterator from .abstract_storage import AbstractStorage From b19ccd3a62574d098c290bdae93adcebf3b6dc1f Mon Sep 17 00:00:00 2001 From: Auguste Baum Date: Tue, 24 Sep 2024 11:11:43 +0200 Subject: [PATCH 14/17] Replace ExceptionGroup with new ProjectPutError --- pyproject.toml | 1 - src/skore/project.py | 9 +++++++-- tests/unit/test_project.py | 4 ++-- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index cf89b6d2b..a9ba31cbc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,7 +25,6 @@ dependencies = [ "skops", "uvicorn", ] -requires-python = ">= 3.11" [project.optional-dependencies] test = [ diff --git a/src/skore/project.py b/src/skore/project.py index acaac7857..4784786fb 100644 --- a/src/skore/project.py +++ b/src/skore/project.py @@ -21,6 +21,10 @@ logger = logging.getLogger(__name__) +class ProjectPutError(Exception): + """One more key-value pairs could not be saved in the Project.""" + + class Project: """A project is a collection of items that are stored in a storage.""" @@ -94,8 +98,9 @@ def put_several( if errors: if on_error == "raise": - raise ExceptionGroup( - "Several items could not be inserted in the Project", errors + raise ProjectPutError( + "Several items could not be inserted in the Project " + f"due to the following errors: {errors}" ) logger.warning( diff --git a/tests/unit/test_project.py b/tests/unit/test_project.py index c374742cd..370cc641a 100644 --- a/tests/unit/test_project.py +++ b/tests/unit/test_project.py @@ -14,7 +14,7 @@ from skore.layout import LayoutRepository from skore.layout.layout import LayoutItem, LayoutItemSize from skore.persistence.in_memory_storage import InMemoryStorage -from skore.project import Project, ProjectLoadError, load +from skore.project import Project, ProjectLoadError, ProjectPutError, load @pytest.fixture @@ -215,5 +215,5 @@ def test_put_key_is_a_set(project): def test_put_wrong_key_and_value_raise(project): """When `on_error` is "raise", raise.""" - with pytest.raises(ExceptionGroup): + with pytest.raises(ProjectPutError): project.put(0, (lambda: "unsupported object"), on_error="raise") From 7498ab9bbb86ba3ccd198395c47a1e947ac1d857 Mon Sep 17 00:00:00 2001 From: Auguste Baum Date: Tue, 24 Sep 2024 11:34:13 +0200 Subject: [PATCH 15/17] Move key check into put_item --- src/skore/project.py | 25 ++++++++----------------- 1 file changed, 8 insertions(+), 17 deletions(-) diff --git a/src/skore/project.py b/src/skore/project.py index 4784786fb..508265ced 100644 --- a/src/skore/project.py +++ b/src/skore/project.py @@ -76,25 +76,11 @@ def put_several( errors = [] for key, value in key_to_value.items(): - key_error = None - value_error = None - - if not isinstance(key, str): - key_error = TypeError( - f"All keys must be strings; key '{key}' is of type '{type(key)}'" - ) - errors.append(key_error) - try: item = object_to_item(value) - except NotImplementedError as e: - value_error = e - errors.append(value_error) - - if key_error or value_error: - continue - - self.put_item(key, item) + self.put_item(key, item) + except (NotImplementedError, TypeError) as e: + errors.append(e) if errors: if on_error == "raise": @@ -110,6 +96,11 @@ def put_several( def put_item(self, key: str, item: Item): """Add an Item to the Project.""" + if not isinstance(key, str): + raise TypeError( + f"All keys must be strings; key '{key}' is of type '{type(key)}'" + ) + self.item_repository.put_item(key, item) def get(self, key: str) -> Any: From 4ad2a4b88fc03eea94d09ac848b6214740eb8d99 Mon Sep 17 00:00:00 2001 From: Auguste Baum Date: Tue, 24 Sep 2024 11:39:58 +0200 Subject: [PATCH 16/17] [squashme] wip --- src/skore/project.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/skore/project.py b/src/skore/project.py index 508265ced..12aa048c9 100644 --- a/src/skore/project.py +++ b/src/skore/project.py @@ -50,7 +50,17 @@ def put(self, key: str, value: Any, on_error: Literal["warn", "raise"] = "warn") Upon error (e.g. if the key is not a string), whether to raise an error or to print a warning. Default is "warn". """ - self.put_several({key: value}, on_error) + try: + item = object_to_item(value) + self.put_item(key, item) + except (NotImplementedError, TypeError) as e: + if on_error == "raise": + raise + + logger.warning( + "Several items could not be inserted in the Project " + f"due to the following error: {e}" + ) @put.register def put_several( @@ -77,8 +87,7 @@ def put_several( for key, value in key_to_value.items(): try: - item = object_to_item(value) - self.put_item(key, item) + self.put(key, value, on_error="raise") except (NotImplementedError, TypeError) as e: errors.append(e) From 0db2bae4d3b827689bf697c8179c3ae36bcc41e8 Mon Sep 17 00:00:00 2001 From: Auguste Baum Date: Tue, 24 Sep 2024 12:21:04 +0200 Subject: [PATCH 17/17] Move logic from `put_several` to `put` - Raise a `ProjectPutError` - When `on_error` is "raise", raise on the first error (rather than collecting all errors at the end). --- src/skore/project.py | 57 ++++++++++++++++++-------------------- tests/unit/test_project.py | 18 ++++++++++-- 2 files changed, 42 insertions(+), 33 deletions(-) diff --git a/src/skore/project.py b/src/skore/project.py index 12aa048c9..9dfd6aa32 100644 --- a/src/skore/project.py +++ b/src/skore/project.py @@ -40,6 +40,9 @@ def __init__( def put(self, key: str, value: Any, on_error: Literal["warn", "raise"] = "warn"): """Add a value to the Project. + If `on_error` is "raise", any error stops the execution. If `on_error` + is "warn" (or anything other than "raise"), a warning is shown instead. + Parameters ---------- key : str @@ -49,16 +52,23 @@ def put(self, key: str, value: Any, on_error: Literal["warn", "raise"] = "warn") on_error : "warn" or "raise", optional Upon error (e.g. if the key is not a string), whether to raise an error or to print a warning. Default is "warn". + + Raises + ------ + ProjectPutError + If the key-value pair cannot be saved properly, and `on_error` is "raise". """ try: item = object_to_item(value) self.put_item(key, item) except (NotImplementedError, TypeError) as e: if on_error == "raise": - raise + raise ProjectPutError( + "Key-value pair could not be inserted in the Project" + ) from e logger.warning( - "Several items could not be inserted in the Project " + "Key-value pair could not be inserted in the Project " f"due to the following error: {e}" ) @@ -68,46 +78,33 @@ def put_several( ): """Add several values to the Project. - All valid key-value pairs will be added to the Project; errors caused - by invalid key-value pairs (e.g. if the key is not a string) will be - collected and shown all at once after all the insertions, either as a - warning or an Exception, depending on the value of `on_error`. + If `on_error` is "raise", the first error stops the execution (so the + later key-value pairs will not be inserted). If `on_error` is "warn" (or + anything other than "raise"), errors do not stop the execution, and are + shown as they come as warnings; all the valid key-value pairs are inserted. Parameters ---------- - key : str - The key to associate with `value` in the Project. Must be a string. - value : Any - The value to associate with `key` in the Project. + key_to_value : dict[str, Any] + The key-value pairs to put in the Project. Keys must be strings. on_error : "warn" or "raise", optional - Upon error (e.g. if the key is not a string), whether to raise an error or + Upon error (e.g. if a key is not a string), whether to raise an error or to print a warning. Default is "warn". - """ - errors = [] + Raises + ------ + ProjectPutError + If a key-value pair in `key_to_value` cannot be saved properly, + and `on_error` is "raise". + """ for key, value in key_to_value.items(): - try: - self.put(key, value, on_error="raise") - except (NotImplementedError, TypeError) as e: - errors.append(e) - - if errors: - if on_error == "raise": - raise ProjectPutError( - "Several items could not be inserted in the Project " - f"due to the following errors: {errors}" - ) - - logger.warning( - "Several items could not be inserted in the Project " - f"due to the following errors: {errors}" - ) + self.put(key, value, on_error=on_error) def put_item(self, key: str, item: Item): """Add an Item to the Project.""" if not isinstance(key, str): raise TypeError( - f"All keys must be strings; key '{key}' is of type '{type(key)}'" + f"Key must be a string; key '{key}' is of type '{type(key)}'" ) self.item_repository.put_item(key, item) diff --git a/tests/unit/test_project.py b/tests/unit/test_project.py index 370cc641a..94c390687 100644 --- a/tests/unit/test_project.py +++ b/tests/unit/test_project.py @@ -189,6 +189,18 @@ def test_put_several_canonical(project): assert project.list_keys() == ["a", "b"] +def test_put_several_some_errors(project, caplog): + project.put( + { + 0: "hello", + 1: "hello", + 2: "hello", + } + ) + assert len(caplog.record_tuples) == 3 + assert project.list_keys() == [] + + def test_put_several_nested(project): project.put({"a": {"b": "baz"}}) assert project.list_keys() == ["a"] @@ -209,11 +221,11 @@ def test_put_key_is_a_tuple(project): def test_put_key_is_a_set(project): """Cannot use an unhashable type as a key.""" - with pytest.raises(TypeError): - project.put(set(), "hello") + with pytest.raises(ProjectPutError): + project.put(set(), "hello", on_error="raise") def test_put_wrong_key_and_value_raise(project): - """When `on_error` is "raise", raise.""" + """When `on_error` is "raise", raise the first error that occurs.""" with pytest.raises(ProjectPutError): project.put(0, (lambda: "unsupported object"), on_error="raise")