From 2bf876309658e98a716469a63ceb3fc919185741 Mon Sep 17 00:00:00 2001 From: "Thomas S." <2565098+thomass-dev@users.noreply.github.com> Date: Tue, 10 Feb 2026 13:26:41 +0100 Subject: [PATCH 1/9] feat(skore/project)!: Enforce mode in a dedicated parameter --- .../getting_started/plot_getting_started.py | 2 +- skore/src/skore/_login.py | 2 +- skore/src/skore/project/project.py | 87 ++++++++++--------- skore/tests/unit/project/test_project.py | 42 ++++----- 4 files changed, 68 insertions(+), 65 deletions(-) diff --git a/examples/getting_started/plot_getting_started.py b/examples/getting_started/plot_getting_started.py index 7b3279587e..6186e15b03 100644 --- a/examples/getting_started/plot_getting_started.py +++ b/examples/getting_started/plot_getting_started.py @@ -341,7 +341,7 @@ os.environ["SKORE_WORKSPACE"] = temp_dir.name # sphinx_gallery_end_ignore -project = skore.Project("german_credit_classification") +project = skore.Project(mode="local", name="german_credit_classification") # %% # We store our reports with descriptive keys: diff --git a/skore/src/skore/_login.py b/skore/src/skore/_login.py index eb99a066aa..d1149f3382 100644 --- a/skore/src/skore/_login.py +++ b/skore/src/skore/_login.py @@ -7,7 +7,7 @@ logger = getLogger(__name__) -def login(*, mode: Literal["local", "hub"] = "hub", **kwargs): +def login(*, mode: Literal["hub", "local"] = "hub", **kwargs): """ Login to the storage backend. diff --git a/skore/src/skore/project/project.py b/skore/src/skore/project/project.py index aeb50cbffe..aa57e7d9a5 100644 --- a/skore/src/skore/project/project.py +++ b/skore/src/skore/project/project.py @@ -10,6 +10,8 @@ from skore._sklearn.types import MLTask from skore.project._summary import Summary +Mode = Literal["hub", "local"] + class Project: r""" @@ -23,20 +25,20 @@ class Project: insert a key-report pair into the project, to obtain the metadata/metrics of the inserted reports and to get a specific report by its id. - Two mutually exclusive modes are available and can be configured using the ``name`` + Two mutually exclusive modes are available and can be configured using the ``mode`` parameter of the constructor: .. rubric:: Hub mode - If the ``name`` takes the form of the URI ``hub:///``, the project - is configured to the ``hub`` mode to communicate with the ``skore hub``. + The project is configured to the ``hub`` mode to communicate with the ``skore hub``. - A workspace is a ``skore hub`` concept that must be configured on the ``skore hub`` - interface. It represents an isolated entity managing users, projects, and - resources. It can be a company, organization, or team that operates - independently within the system. + In this mode, the ``name`` takes the form ``/``, where the + workspace is a ``skore hub`` concept that must be configured on the ``skore hub`` + interface. It represents an isolated entity managing users, projects, and resources. + It can be a company, organization, or team that operates independently within the + system. - In this mode, you must have an account to the ``skore hub`` and must be + Also in this mode, you must have an account to the ``skore hub`` and must be authorized to the specified workspace. You must also be authenticated beforehand, by calling the ``skore.login()`` function at the top of your script. @@ -60,12 +62,9 @@ class Project: Parameters ---------- name : str - The name of the project: - - - if the ``name`` takes the form of the URI ``hub:///``, the - project is configured to communicate with the ``skore hub``, - - otherwise, the project is configured to communicate with a local storage, on - the user machine. + The name of the project. + mode : {"hub", "local"} + The mode of the project. **kwargs : dict Extra keyword arguments passed to the project, depending on its mode. @@ -75,9 +74,9 @@ class Project: Attributes ---------- name : str - The name of the project, extrapolated from the ``name`` parameter. - mode : str - The mode of the project, extrapolated from the ``name`` parameter. + The name of the project. + mode : {"hub", "local"} + The mode of the project. ml_task : MLTask The ML task of the project; unset until a first report is put. @@ -121,7 +120,7 @@ class Project: >>> from skore import Project >>> >>> tmpdir = TemporaryDirectory().name - >>> local_project = Project("my-xp", workspace=Path(tmpdir)) + >>> local_project = Project(mode="local", name="my-xp", workspace=Path(tmpdir)) Put reports in the project. @@ -140,39 +139,38 @@ class Project: Create a summary view to investigate persisted reports' metadata/metrics. """ - __HUB_NAME_PATTERN = re.compile(r"hub://(?P[^/]+)/(?P.+)") + __HUB_NAME_PATTERN = re.compile(r"(?P[^/]+)/(?P.+)") @staticmethod - def __setup_plugin(name: str) -> tuple[Literal["local", "hub"], str, Any, dict]: + def __setup_plugin(mode: Mode, name: str) -> tuple[Any, dict]: PLUGINS = entry_points(group="skore.plugins.project") - mode: Literal["local", "hub"] - if match := re.match(Project.__HUB_NAME_PATTERN, name): - mode = "hub" - name = match["name"] - parameters = {"workspace": match["workspace"], "name": name} + if mode == "hub": + if not (match := re.match(Project.__HUB_NAME_PATTERN, name)): + raise ValueError( + f'In hub mode, the name must be formatted as "/" ' + f"(found {name})" + ) + + parameters = {"workspace": match["workspace"], "name": match["name"]} else: - mode = "local" parameters = {"name": name} if mode not in PLUGINS.names: raise ValueError(f"Unknown mode `{mode}`. Please install `skore[{mode}]`.") - return mode, name, PLUGINS[mode].load(), parameters + return PLUGINS[mode].load(), parameters - def __init__(self, name: str, **kwargs): + def __init__(self, name: str, *, mode: Mode, **kwargs): r""" Initialize a project. Parameters ---------- name : str - The name of the project: - - - if the ``name`` takes the form of the URI ``hub:///``, - the project is configured to communicate with the ``skore hub``, - - otherwise, the project is configured to communicate with a local storage, - on the user machine. + The name of the project. + mode : {"hub", "local"} + The mode of the project. **kwargs : dict Extra keyword arguments passed to the project, depending on its mode. @@ -189,7 +187,10 @@ def __init__(self, name: str, **kwargs): - on Linux, usually ``${HOME}/.cache/skore``, - on macOS, usually ``${HOME}/Library/Caches/skore``. """ - mode, name, plugin, parameters = Project.__setup_plugin(name) + if mode not in ("hub", "local"): + raise ValueError(f'`mode` must be "hub" or "local" (found {mode})') + + plugin, parameters = Project.__setup_plugin(mode, name) self.__mode = mode self.__name = name @@ -281,19 +282,16 @@ def __repr__(self) -> str: # noqa: D105 return self.__project.__repr__() @staticmethod - def delete(name: str, **kwargs): + def delete(name: str, *, mode: Mode, **kwargs): r""" Delete a project. Parameters ---------- name : str - The name of the project: - - - if the ``name`` takes the form of the URI ``hub:///``, - the project is configured to communicate with the ``skore hub``, - - otherwise, the project is configured to communicate with a local storage, - on the user machine. + The name of the project. + mode : {"hub", "local"} + The mode of the project. **kwargs : dict Extra keyword arguments passed to the project, depending on its mode. @@ -310,6 +308,9 @@ def delete(name: str, **kwargs): - on Linux, usually ``${HOME}/.cache/skore``, - on macOS, usually ``${HOME}/Library/Caches/skore``. """ - _, _, plugin, parameters = Project.__setup_plugin(name) + if mode not in ("hub", "local"): + raise ValueError(f'`mode` must be "hub" or "local" (found {mode})') + + plugin, parameters = Project.__setup_plugin(mode, name) return plugin.delete(**(kwargs | parameters)) diff --git a/skore/tests/unit/project/test_project.py b/skore/tests/unit/project/test_project.py index d3efd1c18d..0437f22ed4 100644 --- a/skore/tests/unit/project/test_project.py +++ b/skore/tests/unit/project/test_project.py @@ -93,7 +93,7 @@ def cv_regression() -> CrossValidationReport: class TestProject: def test_init_local(self, FakeLocalProject): - project = Project("", workspace="") + project = Project(mode="local", name="", workspace="") assert isinstance(project, Project) assert project._Project__mode == "local" @@ -115,14 +115,14 @@ def test_init_local_unknown_plugin(self, monkeypatch, tmp_path): ValueError, match=escape("Unknown mode `local`. Please install `skore[local]`."), ): - Project("") + Project(mode="local", name="") def test_init_hub(self, FakeHubProject): - project = Project("hub:///") + project = Project(mode="hub", name="/") assert isinstance(project, Project) assert project._Project__mode == "hub" - assert project._Project__name == "" + assert project._Project__name == "/" assert FakeHubProject.called assert not FakeHubProject.call_args.args assert FakeHubProject.call_args.kwargs == { @@ -140,7 +140,7 @@ def test_init_hub_unknown_plugin(self, monkeypatch, tmp_path): ValueError, match=escape("Unknown mode `hub`. Please install `skore[hub]`."), ): - Project("hub:///") + Project(mode="hub", name="/") def test_init_exception_wrong_ml_task(self, monkeypatch): """If the underlying Project implementation contains reports with @@ -173,15 +173,17 @@ def test_init_exception_wrong_ml_task(self, monkeypatch): "Got ML tasks " ) with raises(RuntimeError, match=err_msg): - Project("", workspace="") + Project(mode="local", name="", workspace="") def test_mode(self): - assert Project("").mode == "local" - assert Project("hub:///").mode == "hub" + assert Project(mode="local", name="").mode == "local" + assert Project(mode="hub", name="/").mode == "hub" def test_name(self): - assert Project("").name == "" - assert Project("hub:///").name == "" + assert Project(mode="local", name="").name == "" + assert ( + Project(mode="hub", name="/").name == "/" + ) @mark.parametrize( "report", @@ -192,7 +194,7 @@ def test_name(self): ) def test_put(self, report, FakeLocalProject, request): report = request.getfixturevalue(report) - project = Project("") + project = Project(mode="local", name="") project.put("", report) @@ -206,13 +208,13 @@ def test_put(self, report, FakeLocalProject, request): def test_put_exception(self): with raises(TypeError, match="Key must be a string"): - Project("").put(None, "") + Project(mode="local", name="").put(None, "") with raises(TypeError, match="Report must be `EstimatorReport` or"): - Project("").put("", "") + Project(mode="local", name="").put("", "") def test_put_exception_wrong_ml_task(self, regression, classification): - project = Project("", workspace="") + project = Project(mode="local", name="", workspace="") project.put("classification", classification) assert project.ml_task == "binary-classification" @@ -224,7 +226,7 @@ def test_put_exception_wrong_ml_task(self, regression, classification): project.put("regression", regression) def test_get(self, FakeLocalProject): - project = Project("") + project = Project(mode="local", name="") project.get("") @@ -234,7 +236,7 @@ def test_get(self, FakeLocalProject): assert not project._Project__project.get.call_args.kwargs def test_summarize(self): - project = Project("") + project = Project(mode="local", name="") project._Project__project.summarize.return_value = [ { "learner": "", @@ -288,7 +290,7 @@ def test_summarize_with_skore_local_project(self, monkeypatch, tmpdir): y_test=y_test, ) - project = Project("", workspace=Path(r"{tmpdir}")) + project = Project(mode="local", name="", workspace=Path(r"{tmpdir}")) project.put("", regression) project.summarize() """ @@ -300,11 +302,11 @@ def test_summarize_with_skore_local_project(self, monkeypatch, tmpdir): execution_result.raise_error() def test_repr(self): - project = Project("") + project = Project(mode="local", name="") assert repr(project) == repr(project._Project__project) def test_delete_local(self, FakeLocalProject): - Project.delete("", workspace="") + Project.delete(mode="local", name="", workspace="") assert not FakeLocalProject.called assert FakeLocalProject.delete.called @@ -315,7 +317,7 @@ def test_delete_local(self, FakeLocalProject): } def test_delete_hub(self, FakeHubProject): - Project.delete("hub:///") + Project.delete(mode="hub", name="/") assert not FakeHubProject.called assert FakeHubProject.delete.called From c2f0737b9ddef6f826e91ebdb5d7a425c41c197c Mon Sep 17 00:00:00 2001 From: "Thomas S." <2565098+thomass-dev@users.noreply.github.com> Date: Mon, 16 Feb 2026 11:19:55 +0100 Subject: [PATCH 2/9] fix docstr --- skore/src/skore/project/project.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/skore/src/skore/project/project.py b/skore/src/skore/project/project.py index aa57e7d9a5..2f2e9b8138 100644 --- a/skore/src/skore/project/project.py +++ b/skore/src/skore/project/project.py @@ -30,17 +30,17 @@ class Project: .. rubric:: Hub mode - The project is configured to the ``hub`` mode to communicate with the ``skore hub``. + The project is configured to communicate with the ``skore hub``. - In this mode, the ``name`` takes the form ``/``, where the - workspace is a ``skore hub`` concept that must be configured on the ``skore hub`` - interface. It represents an isolated entity managing users, projects, and resources. - It can be a company, organization, or team that operates independently within the - system. + In this mode, the ``name`` is expected to be of the form ``/``, + where the workspace is a ``skore hub`` concept that must be configured on the + ``skore hub`` interface. It represents an isolated entity managing users, projects, + and resources. It can be a company, organization, or team that operates + independently within the system. - Also in this mode, you must have an account to the ``skore hub`` and must be - authorized to the specified workspace. You must also be authenticated beforehand, - by calling the ``skore.login()`` function at the top of your script. + Note: Using Project in ``hub`` mode requires an account on ``skore hub``, with + access rights to the specified workspace. Authentication to ``skore hub`` is done by + running ``skore.login()`` before instantiating the Project. .. rubric:: Local mode From adb96f3af5687c3ebf01efe6f1279464a0e505ae Mon Sep 17 00:00:00 2001 From: "Thomas S." <2565098+thomass-dev@users.noreply.github.com> Date: Mon, 16 Feb 2026 14:30:19 +0100 Subject: [PATCH 3/9] =?UTF-8?q?Refactor:=20add=20a=20dedicated=20`project.?= =?UTF-8?q?plugin`=20and=20`project.types`=C2=A0=20modules?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../getting_started/plot_getting_started.py | 2 +- skore/src/skore/__init__.py | 4 +-- .../skore/{project => _project}/__init__.py | 4 --- .../skore/{project => _project}/_summary.py | 7 +++-- .../skore/{project => _project}/_widget.py | 0 .../skore/{_login.py => _project/login.py} | 15 ++++------ skore/src/skore/_project/plugin.py | 19 ++++++++++++ .../skore/{project => _project}/project.py | 30 +++++++++---------- skore/src/skore/_project/types.py | 4 +++ skore/tests/unit/{ => project}/test_login.py | 0 skore/tests/unit/project/test_project.py | 10 +++---- skore/tests/unit/project/test_summary.py | 6 ++-- skore/tests/unit/project/test_widget.py | 2 +- 13 files changed, 58 insertions(+), 45 deletions(-) rename skore/src/skore/{project => _project}/__init__.py (50%) rename skore/src/skore/{project => _project}/_summary.py (97%) rename skore/src/skore/{project => _project}/_widget.py (100%) rename skore/src/skore/{_login.py => _project/login.py} (82%) create mode 100644 skore/src/skore/_project/plugin.py rename skore/src/skore/{project => _project}/project.py (93%) create mode 100644 skore/src/skore/_project/types.py rename skore/tests/unit/{ => project}/test_login.py (100%) diff --git a/examples/getting_started/plot_getting_started.py b/examples/getting_started/plot_getting_started.py index 6186e15b03..7b3279587e 100644 --- a/examples/getting_started/plot_getting_started.py +++ b/examples/getting_started/plot_getting_started.py @@ -341,7 +341,7 @@ os.environ["SKORE_WORKSPACE"] = temp_dir.name # sphinx_gallery_end_ignore -project = skore.Project(mode="local", name="german_credit_classification") +project = skore.Project("german_credit_classification") # %% # We store our reports with descriptive keys: diff --git a/skore/src/skore/__init__.py b/skore/src/skore/__init__.py index 4e84d6e091..be6c125893 100644 --- a/skore/src/skore/__init__.py +++ b/skore/src/skore/__init__.py @@ -9,7 +9,8 @@ from skore._config import config_context, get_config, set_config from skore._externals._sklearn_compat import parse_version -from skore._login import login +from skore._project.login import login +from skore._project.project import Project from skore._sklearn import ( ComparisonReport, ConfusionMatrixDisplay, @@ -32,7 +33,6 @@ ) from skore._utils._patch import setup_jupyter_display from skore._utils._show_versions import show_versions -from skore.project import Project # Configure jupyter display for VS Code compatibility setup_jupyter_display() diff --git a/skore/src/skore/project/__init__.py b/skore/src/skore/_project/__init__.py similarity index 50% rename from skore/src/skore/project/__init__.py rename to skore/src/skore/_project/__init__.py index cadbd8144c..5294755b04 100644 --- a/skore/src/skore/project/__init__.py +++ b/skore/src/skore/_project/__init__.py @@ -1,5 +1 @@ """Alias top level function and class of the project submodule.""" - -from skore.project.project import Project - -__all__ = ["Project"] diff --git a/skore/src/skore/project/_summary.py b/skore/src/skore/_project/_summary.py similarity index 97% rename from skore/src/skore/project/_summary.py rename to skore/src/skore/_project/_summary.py index a699243b07..f133d4f2a7 100644 --- a/skore/src/skore/project/_summary.py +++ b/skore/src/skore/_project/_summary.py @@ -6,13 +6,12 @@ from pandas import Categorical, DataFrame, Index, MultiIndex, RangeIndex -from skore import ComparisonReport -from skore.project._widget import ModelExplorerWidget +from skore._project._widget import ModelExplorerWidget if TYPE_CHECKING: from typing import Literal - from skore import CrossValidationReport, EstimatorReport + from skore import ComparisonReport, CrossValidationReport, EstimatorReport class Summary(DataFrame): @@ -90,6 +89,8 @@ def reports( return_as : {"list", "comparison"}, default="list" In what form the reports should be returned. """ + from skore import ComparisonReport + if self.empty: return [] diff --git a/skore/src/skore/project/_widget.py b/skore/src/skore/_project/_widget.py similarity index 100% rename from skore/src/skore/project/_widget.py rename to skore/src/skore/_project/_widget.py diff --git a/skore/src/skore/_login.py b/skore/src/skore/_project/login.py similarity index 82% rename from skore/src/skore/_login.py rename to skore/src/skore/_project/login.py index d1149f3382..b7a1f5af51 100644 --- a/skore/src/skore/_login.py +++ b/skore/src/skore/_project/login.py @@ -1,13 +1,14 @@ """Configure storage backend credentials.""" -from importlib.metadata import entry_points from logging import getLogger -from typing import Literal + +from skore._project import plugin +from skore._project.types import ProjectMode logger = getLogger(__name__) -def login(*, mode: Literal["hub", "local"] = "hub", **kwargs): +def login(*, mode: ProjectMode = "hub", **kwargs): """ Login to the storage backend. @@ -55,12 +56,6 @@ def login(*, mode: Literal["hub", "local"] = "hub", **kwargs): logger.debug("Login to local storage.") return - mode = "hub" - plugins = entry_points(group="skore.plugins.login") - - if mode not in plugins.names: - raise ValueError(f"Unknown mode `{mode}`. Please install `skore[{mode}]`.") - logger.debug("Login to hub storage.") - return plugins[mode].load()(**kwargs) + return plugin.get(group="skore.plugins.login", mode="hub")(**kwargs) diff --git a/skore/src/skore/_project/plugin.py b/skore/src/skore/_project/plugin.py new file mode 100644 index 0000000000..ae6a6b3c6a --- /dev/null +++ b/skore/src/skore/_project/plugin.py @@ -0,0 +1,19 @@ +from importlib.metadata import entry_points +from typing import get_args + +from skore._project.types import PluginGroup, ProjectMode + +GROUPS = get_args(PluginGroup) +MODES = get_args(ProjectMode) + + +def get(*, group: PluginGroup, mode: ProjectMode): + assert group in GROUPS, f"`group` must be in {GROUPS} (found {group})" + assert mode in MODES, f"`mode` must be in {MODES} (found {mode})" + + plugins = entry_points(group=group) + + if mode not in plugins.names: + raise ValueError(f"Unknown mode `{mode}`. Please install `skore[{mode}]`.") + + return plugins[mode].load() diff --git a/skore/src/skore/project/project.py b/skore/src/skore/_project/project.py similarity index 93% rename from skore/src/skore/project/project.py rename to skore/src/skore/_project/project.py index 2f2e9b8138..c4b86dba01 100644 --- a/skore/src/skore/project/project.py +++ b/skore/src/skore/_project/project.py @@ -3,14 +3,15 @@ from __future__ import annotations import re -from importlib.metadata import entry_points -from typing import Any, Literal +from typing import TYPE_CHECKING, Any -from skore import CrossValidationReport, EstimatorReport +from skore._project import plugin +from skore._project._summary import Summary +from skore._project.types import ProjectMode from skore._sklearn.types import MLTask -from skore.project._summary import Summary -Mode = Literal["hub", "local"] +if TYPE_CHECKING: + from skore import CrossValidationReport, EstimatorReport class Project: @@ -142,9 +143,7 @@ class Project: __HUB_NAME_PATTERN = re.compile(r"(?P[^/]+)/(?P.+)") @staticmethod - def __setup_plugin(mode: Mode, name: str) -> tuple[Any, dict]: - PLUGINS = entry_points(group="skore.plugins.project") - + def __setup_plugin(mode: ProjectMode, name: str) -> tuple[Any, dict]: if mode == "hub": if not (match := re.match(Project.__HUB_NAME_PATTERN, name)): raise ValueError( @@ -156,12 +155,9 @@ def __setup_plugin(mode: Mode, name: str) -> tuple[Any, dict]: else: parameters = {"name": name} - if mode not in PLUGINS.names: - raise ValueError(f"Unknown mode `{mode}`. Please install `skore[{mode}]`.") - - return PLUGINS[mode].load(), parameters + return plugin.get(group="skore.plugins.project", mode=mode), parameters - def __init__(self, name: str, *, mode: Mode, **kwargs): + def __init__(self, name: str, *, mode: ProjectMode = "local", **kwargs): r""" Initialize a project. @@ -169,7 +165,7 @@ def __init__(self, name: str, *, mode: Mode, **kwargs): ---------- name : str The name of the project. - mode : {"hub", "local"} + mode : {"hub", "local"}, default "local" The mode of the project. **kwargs : dict Extra keyword arguments passed to the project, depending on its mode. @@ -235,6 +231,8 @@ def put(self, key: str, report: EstimatorReport | CrossValidationReport): TypeError If the combination of parameters are not valid. """ + from skore import CrossValidationReport, EstimatorReport + if not isinstance(key, str): raise TypeError(f"Key must be a string (found '{type(key)}')") @@ -282,7 +280,7 @@ def __repr__(self) -> str: # noqa: D105 return self.__project.__repr__() @staticmethod - def delete(name: str, *, mode: Mode, **kwargs): + def delete(name: str, *, mode: ProjectMode = "local", **kwargs): r""" Delete a project. @@ -290,7 +288,7 @@ def delete(name: str, *, mode: Mode, **kwargs): ---------- name : str The name of the project. - mode : {"hub", "local"} + mode : {"hub", "local"}, default "local" The mode of the project. **kwargs : dict Extra keyword arguments passed to the project, depending on its mode. diff --git a/skore/src/skore/_project/types.py b/skore/src/skore/_project/types.py new file mode 100644 index 0000000000..b6fd89d2ba --- /dev/null +++ b/skore/src/skore/_project/types.py @@ -0,0 +1,4 @@ +from typing import Literal + +ProjectMode = Literal["hub", "local"] +PluginGroup = Literal["skore.plugins.project", "skore.plugins.login"] diff --git a/skore/tests/unit/test_login.py b/skore/tests/unit/project/test_login.py similarity index 100% rename from skore/tests/unit/test_login.py rename to skore/tests/unit/project/test_login.py diff --git a/skore/tests/unit/project/test_project.py b/skore/tests/unit/project/test_project.py index 0437f22ed4..5cbd944499 100644 --- a/skore/tests/unit/project/test_project.py +++ b/skore/tests/unit/project/test_project.py @@ -9,7 +9,7 @@ from sklearn.model_selection import train_test_split from skore import CrossValidationReport, EstimatorReport, Project -from skore.project._summary import Summary +from skore._project._summary import Summary class FakeEntryPoint(EntryPoint): @@ -36,7 +36,7 @@ def FakeHubProject(): @fixture(autouse=True) def monkeypatch_entrypoints(monkeypatch, FakeLocalProject, FakeHubProject): monkeypatch.setattr( - "skore.project.project.entry_points", + "skore._project.plugin.entry_points", lambda **kwargs: EntryPoints( [ FakeEntryPoint( @@ -108,7 +108,7 @@ def test_init_local(self, FakeLocalProject): def test_init_local_unknown_plugin(self, monkeypatch, tmp_path): monkeypatch.undo() monkeypatch.setattr( - "skore.project.project.entry_points", lambda **kwargs: EntryPoints([]) + "skore._project.plugin.entry_points", lambda **kwargs: EntryPoints([]) ) with raises( @@ -133,7 +133,7 @@ def test_init_hub(self, FakeHubProject): def test_init_hub_unknown_plugin(self, monkeypatch, tmp_path): monkeypatch.undo() monkeypatch.setattr( - "skore.project.project.entry_points", lambda **kwargs: EntryPoints([]) + "skore._project.plugin.entry_points", lambda **kwargs: EntryPoints([]) ) with raises( @@ -156,7 +156,7 @@ def test_init_exception_wrong_ml_task(self, monkeypatch): project_factory = Mock(return_value=project) monkeypatch.setattr( - "skore.project.project.entry_points", + "skore._project.plugin.entry_points", lambda **kwargs: EntryPoints( [ FakeEntryPoint( diff --git a/skore/tests/unit/project/test_summary.py b/skore/tests/unit/project/test_summary.py index cd2dfc3ca4..d79aee1bd2 100644 --- a/skore/tests/unit/project/test_summary.py +++ b/skore/tests/unit/project/test_summary.py @@ -8,8 +8,8 @@ from sklearn.linear_model import LinearRegression, LogisticRegression from sklearn.model_selection import train_test_split +from skore._project._summary import Summary from skore._sklearn import ComparisonReport, CrossValidationReport, EstimatorReport -from skore.project._summary import Summary @fixture @@ -207,7 +207,7 @@ def test_reports_filter_true( ] monkeypatch.setattr( - "skore.project._summary.Summary._query_string_selection", + "skore._project._summary.Summary._query_string_selection", lambda self: "ml_task == 'regression'", ) @@ -240,7 +240,7 @@ def test_reports_filter_false( ] monkeypatch.setattr( - "skore.project._summary.Summary._query_string_selection", + "skore._project._summary.Summary._query_string_selection", lambda self: "ml_task == 'regression'", ) diff --git a/skore/tests/unit/project/test_widget.py b/skore/tests/unit/project/test_widget.py index 9d4db40f76..092837d4c6 100644 --- a/skore/tests/unit/project/test_widget.py +++ b/skore/tests/unit/project/test_widget.py @@ -5,7 +5,7 @@ import plotly.graph_objects as go import pytest -from skore.project._widget import ModelExplorerWidget +from skore._project._widget import ModelExplorerWidget @pytest.fixture From 410a72951b5470ba611d7861967be36b047a85ba Mon Sep 17 00:00:00 2001 From: "Thomas S." <2565098+thomass-dev@users.noreply.github.com> Date: Mon, 16 Feb 2026 15:03:17 +0100 Subject: [PATCH 4/9] fix after merge --- skore/tests/unit/project/test_summary.py | 2 +- skore/tests/unit/project/test_widget.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/skore/tests/unit/project/test_summary.py b/skore/tests/unit/project/test_summary.py index c0b9141e9b..f639a84c9d 100644 --- a/skore/tests/unit/project/test_summary.py +++ b/skore/tests/unit/project/test_summary.py @@ -436,7 +436,7 @@ def test_repr_mimebundle_fallback_without_jupyter_deps( ): """Without Jupyter deps, Summary shows table and warns instead of widget.""" monkeypatch.setattr( - "skore.project._summary._jupyter_dependencies_available", + "skore._project._summary._jupyter_dependencies_available", lambda: False, ) project = FakeProject( diff --git a/skore/tests/unit/project/test_widget.py b/skore/tests/unit/project/test_widget.py index e13cd3c7e5..595f6e79e7 100644 --- a/skore/tests/unit/project/test_widget.py +++ b/skore/tests/unit/project/test_widget.py @@ -383,7 +383,7 @@ def test_model_explorer_widget_no_dataset_for_task(metadata, capsys): def test_model_explorer_widget_requires_jupyter_deps(metadata, monkeypatch): """ModelExplorerWidget raises ImportError when Jupyter deps are not installed.""" monkeypatch.setattr( - "skore.project._widget._jupyter_dependencies_available", + "skore._project._widget._jupyter_dependencies_available", lambda: False, ) with pytest.raises(ImportError, match="pip install skore"): From 638c975474ba236c5cff7f28a73020af55606bff Mon Sep 17 00:00:00 2001 From: "Thomas S." <2565098+thomass-dev@users.noreply.github.com> Date: Mon, 16 Feb 2026 15:10:54 +0100 Subject: [PATCH 5/9] fix documentation referencing hub:// --- sphinx/user_guide/project.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/sphinx/user_guide/project.rst b/sphinx/user_guide/project.rst index 7913f7e906..6790bf8add 100644 --- a/sphinx/user_guide/project.rst +++ b/sphinx/user_guide/project.rst @@ -7,10 +7,10 @@ Storing data science artifacts .. currentmodule:: skore `skore` provides a :class:`Project` class to store data science artifacts. The storage -is either local or remote, based on the value passed to the parameter `name` at the -initialization. When `name` is set to the form of the URI `hub:///`, -the project is configured to the `hub` mode to communicate with the `skore hub`. -Refer to the documentation of :class:`Project` for the detailed API. +is either local or remote, based on the value passed to the parameter `mode` at the +initialization. When `mode` is set to `hub`, the project is configured to communicate +with the `skore hub`. Refer to the documentation of :class:`Project` for the detailed +API. Once a project is created, store :class:`EstimatorReport` via the method :meth:`Project.put`. From a15f2ae6c65dce60fdd8ea478577cf2767f15207 Mon Sep 17 00:00:00 2001 From: "Thomas S." <2565098+thomass-dev@users.noreply.github.com> Date: Mon, 16 Feb 2026 15:11:59 +0100 Subject: [PATCH 6/9] fix imports in test after renaming --- skore/tests/unit/project/test_login.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/skore/tests/unit/project/test_login.py b/skore/tests/unit/project/test_login.py index ea43b14ac7..0f6b43e096 100644 --- a/skore/tests/unit/project/test_login.py +++ b/skore/tests/unit/project/test_login.py @@ -20,7 +20,7 @@ def load(self): def test_login_local(monkeypatch, FakeLogin): monkeypatch.setattr( - "skore._login.entry_points", + "skore._project.plugin.entry_points", lambda **kwargs: EntryPoints( [ FakeEntryPoint( @@ -41,7 +41,7 @@ def test_login_local(monkeypatch, FakeLogin): def test_login_hub(monkeypatch, FakeLogin): monkeypatch.setattr( - "skore._login.entry_points", + "skore._project.plugin.entry_points", lambda **kwargs: EntryPoints( [ FakeEntryPoint( @@ -63,7 +63,9 @@ def test_login_hub(monkeypatch, FakeLogin): def test_login_hub_without_plugin(monkeypatch): - monkeypatch.setattr("skore._login.entry_points", lambda **kwargs: EntryPoints([])) + monkeypatch.setattr( + "skore._project.plugin.entry_points", lambda **kwargs: EntryPoints([]) + ) from skore import login From b7b9a07c51ee2bb831bb98c9ecdc6b6c8562f389 Mon Sep 17 00:00:00 2001 From: "Thomas S." <2565098+thomass-dev@users.noreply.github.com> Date: Mon, 16 Feb 2026 15:48:23 +0100 Subject: [PATCH 7/9] remove unecessary `the` --- skore/src/skore/_project/project.py | 6 +++--- sphinx/user_guide/project.rst | 5 ++--- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/skore/src/skore/_project/project.py b/skore/src/skore/_project/project.py index b2f3c6f5b8..644f15c0a3 100644 --- a/skore/src/skore/_project/project.py +++ b/skore/src/skore/_project/project.py @@ -30,10 +30,10 @@ class Project: .. rubric:: Hub mode - The project is configured to communicate with the ``skore hub``. + The project is configured to communicate with ``skore hub``. - In this mode, the ``name`` is expected to be of the form ``/``, - where the workspace is a ``skore hub`` concept that must be configured on the + In this mode, ``name`` is expected to be of the form ``/``, where + the workspace is a ``skore hub`` concept that must be configured on the ``skore hub`` interface. It represents an isolated entity managing users, projects, and resources. It can be a company, organization, or team that operates independently within the system. diff --git a/sphinx/user_guide/project.rst b/sphinx/user_guide/project.rst index 6790bf8add..55bfceb5cc 100644 --- a/sphinx/user_guide/project.rst +++ b/sphinx/user_guide/project.rst @@ -7,10 +7,9 @@ Storing data science artifacts .. currentmodule:: skore `skore` provides a :class:`Project` class to store data science artifacts. The storage -is either local or remote, based on the value passed to the parameter `mode` at the +is either local or remote, based on the value passed to the parameter `mode` at initialization. When `mode` is set to `hub`, the project is configured to communicate -with the `skore hub`. Refer to the documentation of :class:`Project` for the detailed -API. +with `skore hub`. Refer to the documentation of :class:`Project` for the detailed API. Once a project is created, store :class:`EstimatorReport` via the method :meth:`Project.put`. From ea1cc73088f5e571ccde042930dcd579cac7a6bc Mon Sep 17 00:00:00 2001 From: "Thomas S." <2565098+thomass-dev@users.noreply.github.com> Date: Mon, 16 Feb 2026 16:21:40 +0100 Subject: [PATCH 8/9] Add plugin docstr --- skore/src/skore/_project/plugin.py | 41 ++++++++++++++++++++++++++++-- 1 file changed, 39 insertions(+), 2 deletions(-) diff --git a/skore/src/skore/_project/plugin.py b/skore/src/skore/_project/plugin.py index ae6a6b3c6a..1a37ce3502 100644 --- a/skore/src/skore/_project/plugin.py +++ b/skore/src/skore/_project/plugin.py @@ -1,5 +1,7 @@ +"""Tools used to interact with ``skore`` plugin.""" + from importlib.metadata import entry_points -from typing import get_args +from typing import Any, get_args from skore._project.types import PluginGroup, ProjectMode @@ -7,7 +9,42 @@ MODES = get_args(ProjectMode) -def get(*, group: PluginGroup, mode: ProjectMode): +def get(*, group: PluginGroup, mode: ProjectMode) -> Any: + """ + Load and return a ``skore`` plugin implementation for the given group and mode. + + There are currently two types of plugins allowed: + - the classes implementing the ``Project`` API, registered under the + ``skore.plugins.project`` group, + - the functions used to login, registered under the ``skore.plugins.login`` group. + + This function uses internally the python entry points mechanism: each package + compatible with ``skore`` could expose its own plugins, as long as they are + registered in the right groups and comply with APIs. + + It is usually used to retrieve the plugins exposed by ``skore-local-project`` + or ``skore-hub-project``. + + Parameters + ---------- + group : PluginGroup + The group of plugin to search for. Must be one of: + - "skore.plugins.project" + - "skore.plugins.login" + + mode : ProjectMode + The project mode used to select the plugin implementation. + Must be one of: + - "hub" + - "local" + + Returns + ------- + Any + The loaded plugin object corresponding to the given group and mode. + The exact return type depends on the registered plugin implementation: class or + function. + """ assert group in GROUPS, f"`group` must be in {GROUPS} (found {group})" assert mode in MODES, f"`mode` must be in {MODES} (found {mode})" From 8ae814d144a13caaea6d0b123d4ccea966f67af0 Mon Sep 17 00:00:00 2001 From: "Thomas S." <2565098+thomass-dev@users.noreply.github.com> Date: Mon, 16 Feb 2026 16:35:44 +0100 Subject: [PATCH 9/9] factorize mode test --- skore/src/skore/_project/project.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/skore/src/skore/_project/project.py b/skore/src/skore/_project/project.py index 644f15c0a3..0251480149 100644 --- a/skore/src/skore/_project/project.py +++ b/skore/src/skore/_project/project.py @@ -151,8 +151,10 @@ def __setup_plugin(mode: ProjectMode, name: str) -> tuple[Any, dict]: ) parameters = {"workspace": match["workspace"], "name": match["name"]} - else: + elif mode == "local": parameters = {"name": name} + else: + raise ValueError(f'`mode` must be "hub" or "local" (found {mode})') return plugin.get(group="skore.plugins.project", mode=mode), parameters @@ -182,9 +184,6 @@ def __init__(self, name: str, *, mode: ProjectMode = "local", **kwargs): - on Linux, usually ``${HOME}/.cache/skore``, - on macOS, usually ``${HOME}/Library/Caches/skore``. """ - if mode not in ("hub", "local"): - raise ValueError(f'`mode` must be "hub" or "local" (found {mode})') - plugin, parameters = Project.__setup_plugin(mode, name) self.__mode = mode @@ -307,9 +306,6 @@ def delete(name: str, *, mode: ProjectMode = "local", **kwargs): - on Linux, usually ``${HOME}/.cache/skore``, - on macOS, usually ``${HOME}/Library/Caches/skore``. """ - if mode not in ("hub", "local"): - raise ValueError(f'`mode` must be "hub" or "local" (found {mode})') - plugin, parameters = Project.__setup_plugin(mode, name) return plugin.delete(**(kwargs | parameters))