liblaf-cherries 0.5.0__tar.gz → 0.5.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. {liblaf_cherries-0.5.0 → liblaf_cherries-0.5.2}/PKG-INFO +4 -3
  2. {liblaf_cherries-0.5.0 → liblaf_cherries-0.5.2}/pyproject.toml +3 -2
  3. {liblaf_cherries-0.5.0 → liblaf_cherries-0.5.2}/src/liblaf/cherries/__init__.py +1 -0
  4. {liblaf_cherries-0.5.0 → liblaf_cherries-0.5.2}/src/liblaf/cherries/__init__.pyi +3 -3
  5. {liblaf_cherries-0.5.0 → liblaf_cherries-0.5.2}/src/liblaf/cherries/_entrypoint.py +9 -8
  6. {liblaf_cherries-0.5.0 → liblaf_cherries-0.5.2}/src/liblaf/cherries/_version.py +2 -2
  7. {liblaf_cherries-0.5.0 → liblaf_cherries-0.5.2}/src/liblaf/cherries/config/__init__.py +1 -0
  8. {liblaf_cherries-0.5.0 → liblaf_cherries-0.5.2}/src/liblaf/cherries/config/__init__.pyi +16 -6
  9. liblaf_cherries-0.5.2/src/liblaf/cherries/config/_config.py +5 -0
  10. {liblaf_cherries-0.5.0/src/liblaf/cherries/meta → liblaf_cherries-0.5.2/src/liblaf/cherries/config/asset}/__init__.py +1 -0
  11. liblaf_cherries-0.5.2/src/liblaf/cherries/config/asset/__init__.pyi +34 -0
  12. liblaf_cherries-0.5.2/src/liblaf/cherries/config/asset/_meta.py +98 -0
  13. liblaf_cherries-0.5.2/src/liblaf/cherries/config/asset/_registry.py +25 -0
  14. {liblaf_cherries-0.5.0/src/liblaf/cherries/core → liblaf_cherries-0.5.2/src/liblaf/cherries/config/asset/resolvers}/__init__.py +1 -0
  15. liblaf_cherries-0.5.2/src/liblaf/cherries/config/asset/resolvers/__init__.pyi +5 -0
  16. liblaf_cherries-0.5.2/src/liblaf/cherries/config/asset/resolvers/_abc.py +17 -0
  17. liblaf_cherries-0.5.2/src/liblaf/cherries/config/asset/resolvers/_series.py +18 -0
  18. liblaf_cherries-0.5.2/src/liblaf/cherries/config/asset/resolvers/_vtk.py +33 -0
  19. liblaf_cherries-0.5.2/src/liblaf/cherries/core/__init__.py +4 -0
  20. {liblaf_cherries-0.5.0 → liblaf_cherries-0.5.2}/src/liblaf/cherries/core/__init__.pyi +15 -3
  21. {liblaf_cherries-0.5.0 → liblaf_cherries-0.5.2}/src/liblaf/cherries/core/_impl.py +20 -7
  22. liblaf_cherries-0.5.2/src/liblaf/cherries/core/_plugin.py +99 -0
  23. {liblaf_cherries-0.5.0 → liblaf_cherries-0.5.2}/src/liblaf/cherries/core/_run.py +17 -36
  24. {liblaf_cherries-0.5.0 → liblaf_cherries-0.5.2}/src/liblaf/cherries/core/_spec.py +10 -11
  25. liblaf_cherries-0.5.2/src/liblaf/cherries/core/_utils.py +63 -0
  26. liblaf_cherries-0.5.2/src/liblaf/cherries/meta/__init__.py +4 -0
  27. {liblaf_cherries-0.5.0 → liblaf_cherries-0.5.2}/src/liblaf/cherries/meta/_git.py +3 -3
  28. {liblaf_cherries-0.5.0 → liblaf_cherries-0.5.2}/src/liblaf/cherries/meta/_name.py +3 -3
  29. liblaf_cherries-0.5.2/src/liblaf/cherries/path_utils/__init__.py +4 -0
  30. liblaf_cherries-0.5.2/src/liblaf/cherries/plugins/__init__.py +4 -0
  31. {liblaf_cherries-0.5.0 → liblaf_cherries-0.5.2}/src/liblaf/cherries/plugins/comet.py +4 -4
  32. {liblaf_cherries-0.5.0 → liblaf_cherries-0.5.2}/src/liblaf/cherries/plugins/git_.py +12 -10
  33. {liblaf_cherries-0.5.0 → liblaf_cherries-0.5.2}/src/liblaf/cherries/plugins/logging.py +2 -2
  34. liblaf_cherries-0.5.2/src/liblaf/cherries/profiles/__init__.py +4 -0
  35. {liblaf_cherries-0.5.0 → liblaf_cherries-0.5.2}/src/liblaf/cherries/profiles/_factory.py +2 -3
  36. liblaf_cherries-0.5.2/src/liblaf/cherries/utils/__init__.py +4 -0
  37. liblaf_cherries-0.5.0/src/liblaf/cherries/config/_asset.py +0 -115
  38. liblaf_cherries-0.5.0/src/liblaf/cherries/config/_config.py +0 -6
  39. liblaf_cherries-0.5.0/src/liblaf/cherries/core/_plugin.py +0 -96
  40. liblaf_cherries-0.5.0/src/liblaf/cherries/core/_utils.py +0 -20
  41. liblaf_cherries-0.5.0/src/liblaf/cherries/pathutils/__init__.py +0 -3
  42. liblaf_cherries-0.5.0/src/liblaf/cherries/plugins/__init__.py +0 -3
  43. liblaf_cherries-0.5.0/src/liblaf/cherries/profiles/__init__.py +0 -3
  44. liblaf_cherries-0.5.0/src/liblaf/cherries/utils/__init__.py +0 -3
  45. {liblaf_cherries-0.5.0 → liblaf_cherries-0.5.2}/.gitignore +0 -0
  46. {liblaf_cherries-0.5.0 → liblaf_cherries-0.5.2}/LICENSE +0 -0
  47. {liblaf_cherries-0.5.0 → liblaf_cherries-0.5.2}/README.md +0 -0
  48. {liblaf_cherries-0.5.0 → liblaf_cherries-0.5.2}/src/liblaf/cherries/_version.pyi +0 -0
  49. /liblaf_cherries-0.5.0/src/liblaf/cherries/core/typed.py → /liblaf_cherries-0.5.2/src/liblaf/cherries/core/typing.py +0 -0
  50. {liblaf_cherries-0.5.0 → liblaf_cherries-0.5.2}/src/liblaf/cherries/meta/__init__.pyi +0 -0
  51. {liblaf_cherries-0.5.0/src/liblaf/cherries/pathutils → liblaf_cherries-0.5.2/src/liblaf/cherries/path_utils}/__init__.pyi +0 -0
  52. {liblaf_cherries-0.5.0/src/liblaf/cherries/pathutils → liblaf_cherries-0.5.2/src/liblaf/cherries/path_utils}/_convert.py +0 -0
  53. {liblaf_cherries-0.5.0/src/liblaf/cherries/pathutils → liblaf_cherries-0.5.2/src/liblaf/cherries/path_utils}/_path.py +0 -0
  54. {liblaf_cherries-0.5.0/src/liblaf/cherries/pathutils → liblaf_cherries-0.5.2/src/liblaf/cherries/path_utils}/_special.py +0 -0
  55. {liblaf_cherries-0.5.0 → liblaf_cherries-0.5.2}/src/liblaf/cherries/plugins/__init__.pyi +0 -0
  56. {liblaf_cherries-0.5.0 → liblaf_cherries-0.5.2}/src/liblaf/cherries/plugins/dvc.py +0 -0
  57. {liblaf_cherries-0.5.0 → liblaf_cherries-0.5.2}/src/liblaf/cherries/plugins/local.py +0 -0
  58. {liblaf_cherries-0.5.0 → liblaf_cherries-0.5.2}/src/liblaf/cherries/profiles/__init__.pyi +0 -0
  59. {liblaf_cherries-0.5.0 → liblaf_cherries-0.5.2}/src/liblaf/cherries/profiles/_abc.py +0 -0
  60. {liblaf_cherries-0.5.0 → liblaf_cherries-0.5.2}/src/liblaf/cherries/profiles/_default.py +0 -0
  61. {liblaf_cherries-0.5.0 → liblaf_cherries-0.5.2}/src/liblaf/cherries/profiles/_playground.py +0 -0
  62. {liblaf_cherries-0.5.0 → liblaf_cherries-0.5.2}/src/liblaf/cherries/py.typed +0 -0
  63. {liblaf_cherries-0.5.0 → liblaf_cherries-0.5.2}/src/liblaf/cherries/typing.py +0 -0
  64. {liblaf_cherries-0.5.0 → liblaf_cherries-0.5.2}/src/liblaf/cherries/utils/__init__.pyi +0 -0
  65. {liblaf_cherries-0.5.0 → liblaf_cherries-0.5.2}/src/liblaf/cherries/utils/_functools.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: liblaf-cherries
3
- Version: 0.5.0
3
+ Version: 0.5.2
4
4
  Summary: 🍒 Sweet experiment tracking with Comet, DVC, and Git integration.
5
5
  Project-URL: Changelog, https://github.com/liblaf/cherries/blob/main/CHANGELOG.md
6
6
  Project-URL: Documentation, https://cherries.readthedocs.io/
@@ -32,12 +32,13 @@ Classifier: Topic :: Utilities
32
32
  Classifier: Typing :: Typed
33
33
  Requires-Python: >=3.12
34
34
  Requires-Dist: attrs<26,>=25
35
+ Requires-Dist: cachetools<7,>=6
35
36
  Requires-Dist: comet-ml<4,>=3
36
- Requires-Dist: dvc[all]<4,>=3
37
+ Requires-Dist: dvc[ssh]<4,>=3
37
38
  Requires-Dist: environs<15,>=14
38
39
  Requires-Dist: gitpython<4,>=3
39
40
  Requires-Dist: lazy-loader<0.5,>=0.4
40
- Requires-Dist: liblaf-grapes<5,>=4
41
+ Requires-Dist: liblaf-grapes<6,>=5
41
42
  Requires-Dist: loguru<0.8,>=0.7
42
43
  Requires-Dist: networkx<4,>=3
43
44
  Requires-Dist: pydantic-settings<3,>=2
@@ -34,12 +34,13 @@ classifiers = [
34
34
  ]
35
35
  dependencies = [
36
36
  "attrs>=25,<26",
37
+ "cachetools>=6,<7",
37
38
  "comet-ml>=3,<4",
38
- "dvc[all]>=3,<4",
39
+ "dvc[ssh]>=3,<4",
39
40
  "environs>=14,<15",
40
41
  "gitpython>=3,<4",
41
42
  "lazy-loader>=0.4,<0.5",
42
- "liblaf-grapes>=4,<5",
43
+ "liblaf-grapes>=5,<6",
43
44
  "loguru>=0.7,<0.8",
44
45
  "networkx>=3,<4",
45
46
  "pydantic>=2,<3",
@@ -1,3 +1,4 @@
1
1
  import lazy_loader as lazy
2
2
 
3
3
  __getattr__, __dir__, __all__ = lazy.attach_stub(__name__, __file__)
4
+ del lazy
@@ -1,4 +1,4 @@
1
- from . import config, core, meta, pathutils, plugins
1
+ from . import config, core, meta, path_utils, plugins
2
2
  from ._entrypoint import end, run, start
3
3
  from .config import BaseConfig, input, output # noqa: A004
4
4
  from .core import (
@@ -15,7 +15,7 @@ from .core import (
15
15
  log_parameter,
16
16
  log_parameters,
17
17
  )
18
- from .pathutils import (
18
+ from .path_utils import (
19
19
  as_os_path,
20
20
  as_path,
21
21
  as_posix,
@@ -56,7 +56,7 @@ __all__ = [
56
56
  "output",
57
57
  "params",
58
58
  "path",
59
- "pathutils",
59
+ "path_utils",
60
60
  "plugins",
61
61
  "project_dir",
62
62
  "run",
@@ -9,8 +9,8 @@ from liblaf.cherries import config as _config
9
9
  from liblaf.cherries import core, profiles
10
10
 
11
11
 
12
- def end() -> None:
13
- core.active_run.end()
12
+ def end(*args, **kwargs) -> None:
13
+ core.active_run.end(*args, **kwargs)
14
14
 
15
15
 
16
16
  def run[T](main: Callable[..., T], *, profile: profiles.ProfileLike | None = None) -> T:
@@ -27,12 +27,13 @@ def run[T](main: Callable[..., T], *, profile: profiles.ProfileLike | None = Non
27
27
  run.log_parameters(_config.model_dump_without_assets(config, mode="json"))
28
28
  for path in _config.get_inputs(config):
29
29
  run.log_input(path)
30
- result: T = main(*args, **kwargs)
31
- for config in configs:
32
- for path in _config.get_outputs(config):
33
- run.log_output(path)
34
- run.end()
35
- return result
30
+ try:
31
+ return main(*args, **kwargs)
32
+ finally:
33
+ for config in configs:
34
+ for path in _config.get_outputs(config):
35
+ run.log_output(path)
36
+ run.end()
36
37
 
37
38
 
38
39
  def start(
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
28
28
  commit_id: COMMIT_ID
29
29
  __commit_id__: COMMIT_ID
30
30
 
31
- __version__ = version = '0.5.0'
32
- __version_tuple__ = version_tuple = (0, 5, 0)
31
+ __version__ = version = '0.5.2'
32
+ __version_tuple__ = version_tuple = (0, 5, 2)
33
33
 
34
34
  __commit_id__ = commit_id = None
@@ -1,3 +1,4 @@
1
1
  import lazy_loader as lazy
2
2
 
3
3
  __getattr__, __dir__, __all__ = lazy.attach_stub(__name__, __file__)
4
+ del lazy
@@ -1,27 +1,37 @@
1
- from ._asset import (
1
+ from ._config import BaseConfig
2
+ from .asset import (
2
3
  AssetKind,
4
+ AssetResolver,
5
+ AssetResolverRegistry,
6
+ AssetResolverSeries,
7
+ AssetResolverVtk,
8
+ Extra,
3
9
  MetaAsset,
4
- PathGenerator,
10
+ asset,
11
+ asset_resolver_registry,
5
12
  get_assets,
6
13
  get_inputs,
7
14
  get_outputs,
8
15
  input, # noqa: A004
9
16
  model_dump_without_assets,
10
17
  output,
11
- path_generators,
12
18
  )
13
- from ._config import BaseConfig
14
19
 
15
20
  __all__ = [
16
21
  "AssetKind",
22
+ "AssetResolver",
23
+ "AssetResolverRegistry",
24
+ "AssetResolverSeries",
25
+ "AssetResolverVtk",
17
26
  "BaseConfig",
27
+ "Extra",
18
28
  "MetaAsset",
19
- "PathGenerator",
29
+ "asset",
30
+ "asset_resolver_registry",
20
31
  "get_assets",
21
32
  "get_inputs",
22
33
  "get_outputs",
23
34
  "input",
24
35
  "model_dump_without_assets",
25
36
  "output",
26
- "path_generators",
27
37
  ]
@@ -0,0 +1,5 @@
1
+ import pydantic_settings as ps
2
+
3
+
4
+ class BaseConfig(ps.BaseSettings):
5
+ model_config = ps.SettingsConfigDict()
@@ -1,3 +1,4 @@
1
1
  import lazy_loader as lazy
2
2
 
3
3
  __getattr__, __dir__, __all__ = lazy.attach_stub(__name__, __file__)
4
+ del lazy
@@ -0,0 +1,34 @@
1
+ from . import resolvers
2
+ from ._meta import (
3
+ AssetKind,
4
+ Extra,
5
+ MetaAsset,
6
+ asset,
7
+ get_assets,
8
+ get_inputs,
9
+ get_outputs,
10
+ input, # noqa: A004
11
+ model_dump_without_assets,
12
+ output,
13
+ )
14
+ from ._registry import AssetResolverRegistry, asset_resolver_registry
15
+ from .resolvers import AssetResolver, AssetResolverSeries, AssetResolverVtk
16
+
17
+ __all__ = [
18
+ "AssetKind",
19
+ "AssetResolver",
20
+ "AssetResolverRegistry",
21
+ "AssetResolverSeries",
22
+ "AssetResolverVtk",
23
+ "Extra",
24
+ "MetaAsset",
25
+ "asset",
26
+ "asset_resolver_registry",
27
+ "get_assets",
28
+ "get_inputs",
29
+ "get_outputs",
30
+ "input",
31
+ "model_dump_without_assets",
32
+ "output",
33
+ "resolvers",
34
+ ]
@@ -0,0 +1,98 @@
1
+ import enum
2
+ import os
3
+ from collections.abc import Callable, Generator, Iterable
4
+ from pathlib import Path
5
+ from typing import Any, Literal
6
+
7
+ import attrs
8
+ import pydantic
9
+
10
+ import liblaf.cherries.path_utils as pu
11
+ from liblaf import grapes
12
+ from liblaf.cherries.typing import PathLike
13
+
14
+ from ._registry import asset_resolver_registry
15
+
16
+ type Extra = (
17
+ PathLike
18
+ | Iterable[PathLike]
19
+ | Callable[[PathLike], PathLike | Iterable[PathLike | None] | None]
20
+ | None
21
+ )
22
+
23
+
24
+ class AssetKind(enum.StrEnum):
25
+ INPUT = enum.auto()
26
+ OUTPUT = enum.auto()
27
+
28
+
29
+ @attrs.define
30
+ class MetaAsset:
31
+ kind: AssetKind
32
+ extra: Extra = None
33
+
34
+ def resolve(self, value: Path) -> Generator[Path]:
35
+ if self.extra is None:
36
+ yield from asset_resolver_registry.resolve(value)
37
+ return
38
+ extra: PathLike | Iterable[PathLike | None] | None = (
39
+ self.extra(value) if callable(self.extra) else self.extra # noqa: S610
40
+ )
41
+ for p in grapes.as_iterable(extra, base_type=(str, bytes, os.PathLike)):
42
+ if p is None:
43
+ continue
44
+ yield Path(p)
45
+
46
+
47
+ def asset(path: PathLike, extra: Extra = None, *, kind: AssetKind, **kwargs) -> Path:
48
+ field_info: pydantic.fields.FieldInfo = pydantic.Field(pu.data(path), **kwargs) # pyright: ignore[reportAssignmentType]
49
+ field_info.metadata.append(MetaAsset(kind=kind, extra=extra))
50
+ return field_info # pyright: ignore[reportReturnType]
51
+
52
+
53
+ def get_assets(cfg: pydantic.BaseModel, kind: AssetKind) -> Generator[Path]:
54
+ for name, info in type(cfg).model_fields.items():
55
+ value: Any = getattr(cfg, name)
56
+ if isinstance(value, pydantic.BaseModel):
57
+ yield from get_assets(value, kind)
58
+ for meta in info.metadata:
59
+ if isinstance(meta, MetaAsset) and meta.kind == kind:
60
+ value: Path = Path(value)
61
+ yield value
62
+ yield from meta.resolve(value)
63
+
64
+
65
+ def get_inputs(cfg: pydantic.BaseModel) -> Generator[Path]:
66
+ yield from get_assets(cfg, AssetKind.INPUT)
67
+
68
+
69
+ def get_outputs(cfg: pydantic.BaseModel) -> Generator[Path]:
70
+ yield from get_assets(cfg, AssetKind.OUTPUT)
71
+
72
+
73
+ def input(path: PathLike, extra: Extra = None, **kwargs) -> Path: # noqa: A001
74
+ return asset(path, extra=extra, kind=AssetKind.INPUT, **kwargs)
75
+
76
+
77
+ def model_dump_without_assets(
78
+ model: pydantic.BaseModel,
79
+ *,
80
+ mode: str | Literal["json", "python"] = "json", # noqa: PYI051
81
+ **kwargs,
82
+ ) -> dict[str, Any]:
83
+ data: dict[str, Any] = model.model_dump(mode=mode, **kwargs)
84
+ for name, info in type(model).model_fields.items():
85
+ value: Any = getattr(model, name)
86
+ if isinstance(value, pydantic.BaseModel):
87
+ value = model_dump_without_assets(value)
88
+ for meta in info.metadata:
89
+ if isinstance(meta, MetaAsset):
90
+ del data[name]
91
+ break
92
+ else:
93
+ data[name] = value
94
+ return data
95
+
96
+
97
+ def output(path: PathLike, extra: Extra = None, **kwargs) -> Path:
98
+ return asset(path, extra=extra, kind=AssetKind.OUTPUT, **kwargs)
@@ -0,0 +1,25 @@
1
+ from collections.abc import Generator
2
+ from pathlib import Path
3
+
4
+ import attrs
5
+
6
+ from .resolvers import AssetResolver, AssetResolverSeries, AssetResolverVtk
7
+
8
+
9
+ @attrs.define
10
+ class AssetResolverRegistry:
11
+ _registry: dict[str, AssetResolver] = attrs.field(factory=dict)
12
+
13
+ def register(self, resolver: AssetResolver) -> None:
14
+ self._registry[resolver.id] = resolver
15
+
16
+ def resolve(self, path: Path) -> Generator[Path]:
17
+ for resolver in self._registry.values():
18
+ if not resolver.match(path):
19
+ continue
20
+ yield from resolver.resolve(path)
21
+
22
+
23
+ asset_resolver_registry: AssetResolverRegistry = AssetResolverRegistry()
24
+ asset_resolver_registry.register(AssetResolverSeries())
25
+ asset_resolver_registry.register(AssetResolverVtk())
@@ -1,3 +1,4 @@
1
1
  import lazy_loader as lazy
2
2
 
3
3
  __getattr__, __dir__, __all__ = lazy.attach_stub(__name__, __file__)
4
+ del lazy
@@ -0,0 +1,5 @@
1
+ from ._abc import AssetResolver
2
+ from ._series import AssetResolverSeries
3
+ from ._vtk import AssetResolverVtk
4
+
5
+ __all__ = ["AssetResolver", "AssetResolverSeries", "AssetResolverVtk"]
@@ -0,0 +1,17 @@
1
+ import abc
2
+ from collections.abc import Iterable
3
+ from pathlib import Path
4
+
5
+
6
+ class AssetResolver:
7
+ @property
8
+ def id(self) -> str:
9
+ return type(self).__name__
10
+
11
+ @abc.abstractmethod
12
+ def match(self, path: Path) -> bool:
13
+ raise NotImplementedError
14
+
15
+ @abc.abstractmethod
16
+ def resolve(self, path: Path) -> Iterable[Path]:
17
+ raise NotImplementedError
@@ -0,0 +1,18 @@
1
+ from collections.abc import Generator
2
+ from pathlib import Path
3
+ from typing import override
4
+
5
+ from ._abc import AssetResolver
6
+
7
+
8
+ class AssetResolverSeries(AssetResolver):
9
+ @override
10
+ def match(self, path: Path) -> bool:
11
+ return path.suffix == ".series"
12
+
13
+ @override
14
+ def resolve(self, path: Path) -> Generator[Path]:
15
+ if (folder := path.with_suffix(".d")).exists():
16
+ yield folder
17
+ if (folder := path.with_suffix("")).exists():
18
+ yield folder
@@ -0,0 +1,33 @@
1
+ from collections.abc import Generator
2
+ from pathlib import Path
3
+ from typing import override
4
+
5
+ import attrs
6
+
7
+ from ._abc import AssetResolver
8
+
9
+
10
+ @attrs.define
11
+ class AssetResolverVtk(AssetResolver):
12
+ SUFFIXES: set[str] = attrs.field(
13
+ factory=lambda: {
14
+ ".obj",
15
+ ".ply",
16
+ ".stl",
17
+ ".vti",
18
+ ".vtk",
19
+ ".vtp",
20
+ ".vtr",
21
+ ".vts",
22
+ ".vtu",
23
+ }
24
+ )
25
+
26
+ @override
27
+ def match(self, path: Path) -> bool:
28
+ return path.suffix in self.SUFFIXES
29
+
30
+ @override
31
+ def resolve(self, path: Path) -> Generator[Path]:
32
+ if (landmarks := path.with_suffix(".landmarks.json")).exists():
33
+ yield landmarks
@@ -0,0 +1,4 @@
1
+ import lazy_loader as lazy
2
+
3
+ __getattr__, __dir__, __all__ = lazy.attach_stub(__name__, __file__)
4
+ del lazy
@@ -1,4 +1,4 @@
1
- from ._impl import ImplInfo, get_impl_info, impl
1
+ from ._impl import ImplInfo, collect_impls, get_impl_info, impl
2
2
  from ._plugin import Plugin
3
3
  from ._run import (
4
4
  Run,
@@ -15,17 +15,27 @@ from ._run import (
15
15
  log_parameters,
16
16
  start,
17
17
  )
18
- from ._spec import SpecInfo, spec
19
- from .typed import MethodName, PluginId
18
+ from ._spec import SpecInfo, collect_specs, spec
19
+ from ._utils import (
20
+ PluginCachedProperty,
21
+ PluginProperty,
22
+ plugin_cached_property,
23
+ plugin_property,
24
+ )
25
+ from .typing import MethodName, PluginId
20
26
 
21
27
  __all__ = [
22
28
  "ImplInfo",
23
29
  "MethodName",
24
30
  "Plugin",
31
+ "PluginCachedProperty",
25
32
  "PluginId",
33
+ "PluginProperty",
26
34
  "Run",
27
35
  "SpecInfo",
28
36
  "active_run",
37
+ "collect_impls",
38
+ "collect_specs",
29
39
  "end",
30
40
  "get_impl_info",
31
41
  "impl",
@@ -38,6 +48,8 @@ __all__ = [
38
48
  "log_output",
39
49
  "log_parameter",
40
50
  "log_parameters",
51
+ "plugin_cached_property",
52
+ "plugin_property",
41
53
  "spec",
42
54
  "start",
43
55
  ]
@@ -1,13 +1,13 @@
1
1
  import functools
2
+ import inspect
2
3
  from collections.abc import Callable, Iterable
3
4
  from typing import Any, overload
4
5
 
5
6
  import attrs
6
- import wrapt
7
7
 
8
8
  from liblaf import grapes
9
9
 
10
- from .typed import PluginId
10
+ from .typing import MethodName, PluginId
11
11
 
12
12
 
13
13
  @attrs.define
@@ -43,18 +43,31 @@ def impl(
43
43
  if func is None:
44
44
  return functools.partial(impl, priority=priority, after=after, before=before)
45
45
 
46
- @wrapt.decorator
46
+ info = ImplInfo(after=after, before=before, priority=priority)
47
+
48
+ @grapes.decorator
47
49
  def wrapper(
48
50
  wrapped: Callable, _instance: Any, args: tuple, kwargs: dict[str, Any]
49
51
  ) -> Any:
50
52
  return wrapped(*args, **kwargs)
51
53
 
52
- proxy: Any = wrapper(func) # pyright: ignore[reportCallIssue]
53
- proxy._self_impl = ImplInfo(after=after, before=before, priority=priority) # noqa: SLF001
54
- return proxy
54
+ func = wrapper(func)
55
+ grapes.wrapt_setattr(func, "impl", info)
56
+ return func
57
+
58
+
59
+ def collect_impls(cls: Any) -> dict[MethodName, ImplInfo]:
60
+ if isinstance(cls, type):
61
+ cls = type(cls)
62
+ return {
63
+ name: grapes.wrapt_getattr(method, "impl")
64
+ for name, method in inspect.getmembers(
65
+ cls, lambda m: grapes.wrapt_getattr(m, "impl", None) is not None
66
+ )
67
+ }
55
68
 
56
69
 
57
70
  def get_impl_info(func: Callable | None) -> ImplInfo | None:
58
71
  if func is None:
59
72
  return None
60
- return grapes.unbind_getattr(func, "_self_impl", None)
73
+ return grapes.wrapt_getattr(func, "impl", None)
@@ -0,0 +1,99 @@
1
+ import math
2
+ import operator
3
+ from collections.abc import Mapping, Sequence
4
+ from typing import Any, Self
5
+
6
+ import attrs
7
+ import cachetools
8
+ import networkx as nx
9
+ from loguru import logger
10
+
11
+ from ._impl import ImplInfo, collect_impls, get_impl_info
12
+ from .typing import MethodName, PluginId
13
+
14
+
15
+ @attrs.define
16
+ class Plugin:
17
+ plugins: dict[PluginId, "Plugin"] = attrs.field(factory=dict, kw_only=True)
18
+
19
+ _plugin_parent: Self | None = attrs.field(default=None, kw_only=True)
20
+ _cache_sort_plugins: cachetools.Cache[MethodName, Sequence["Plugin"]] = attrs.field(
21
+ factory=lambda: cachetools.Cache(math.inf), init=False
22
+ )
23
+
24
+ @property
25
+ def plugin_id(self) -> str:
26
+ return type(self).__name__
27
+
28
+ @property
29
+ def plugin_root(self) -> Self:
30
+ if self._plugin_parent is None:
31
+ return self
32
+ return self._plugin_parent.plugin_root
33
+
34
+ def delegate(
35
+ self,
36
+ method: MethodName,
37
+ args: Sequence[Any] = (),
38
+ kwargs: Mapping[str, Any] = {},
39
+ *,
40
+ first_result: bool = False,
41
+ ) -> Any:
42
+ plugins: Sequence[Plugin] = self._plugins_sort(method)
43
+ if not plugins:
44
+ if first_result:
45
+ return None
46
+ return []
47
+ results: list[Any] = []
48
+ for plugin in plugins:
49
+ try:
50
+ result: Any = getattr(plugin, method)(*args, **kwargs)
51
+ except BaseException as e:
52
+ if isinstance(e, (KeyboardInterrupt, SystemExit)):
53
+ raise
54
+ logger.exception("Plugin {}", plugin.plugin_id)
55
+ else:
56
+ if result is None:
57
+ continue
58
+ if first_result:
59
+ return result
60
+ results.append(result)
61
+ return results
62
+
63
+ def register(self, plugin: "Plugin") -> None:
64
+ impls: dict[MethodName, ImplInfo] = collect_impls(plugin)
65
+ for name in impls:
66
+ self._cache_sort_plugins.pop(name, None)
67
+ plugin._plugin_parent = self # noqa: SLF001
68
+ self.plugins[plugin.plugin_id] = plugin
69
+
70
+ def _plugins_sort_cache_key(self, method: MethodName) -> MethodName:
71
+ return method
72
+
73
+ @cachetools.cachedmethod(
74
+ operator.attrgetter("_cache_sort_plugins"), key=_plugins_sort_cache_key
75
+ )
76
+ def _plugins_sort(self, method: str) -> Sequence["Plugin"]:
77
+ plugin_infos: dict[str, ImplInfo] = {
78
+ plugin_id: info
79
+ for plugin_id, plugin in self.plugins.items()
80
+ if (info := get_impl_info(getattr(plugin, method, None))) is not None
81
+ }
82
+
83
+ def key_fn(node: str) -> int:
84
+ return plugin_infos[node].priority
85
+
86
+ graph: nx.DiGraph[str] = nx.DiGraph()
87
+ for plugin_id, impl_info in plugin_infos.items():
88
+ graph.add_node(plugin_id)
89
+ for after in impl_info.after:
90
+ if after in plugin_infos:
91
+ graph.add_edge(after, plugin_id)
92
+ for before in impl_info.before:
93
+ if before in plugin_infos:
94
+ graph.add_edge(plugin_id, before)
95
+ return tuple(
96
+ plugin
97
+ for plugin_id in nx.lexicographical_topological_sort(graph, key=key_fn)
98
+ if (plugin := self.plugins.get(plugin_id)) is not None
99
+ )