liblaf-cherries 0.4.3__py3-none-any.whl → 0.5.1__py3-none-any.whl

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 (49) hide show
  1. liblaf/cherries/__init__.py +1 -0
  2. liblaf/cherries/__init__.pyi +5 -7
  3. liblaf/cherries/_entrypoint.py +11 -10
  4. liblaf/cherries/_version.py +2 -2
  5. liblaf/cherries/config/__init__.py +1 -0
  6. liblaf/cherries/config/__init__.pyi +16 -4
  7. liblaf/cherries/config/_config.py +1 -2
  8. liblaf/cherries/{paths → config/asset}/__init__.py +1 -0
  9. liblaf/cherries/config/asset/__init__.pyi +34 -0
  10. liblaf/cherries/config/asset/_meta.py +98 -0
  11. liblaf/cherries/config/asset/_registry.py +25 -0
  12. liblaf/cherries/config/asset/resolvers/__init__.py +4 -0
  13. liblaf/cherries/config/asset/resolvers/__init__.pyi +5 -0
  14. liblaf/cherries/config/asset/resolvers/_abc.py +17 -0
  15. liblaf/cherries/config/asset/resolvers/_series.py +18 -0
  16. liblaf/cherries/config/asset/resolvers/_vtk.py +33 -0
  17. liblaf/cherries/core/__init__.py +1 -0
  18. liblaf/cherries/core/__init__.pyi +15 -3
  19. liblaf/cherries/core/_impl.py +20 -7
  20. liblaf/cherries/core/_plugin.py +53 -50
  21. liblaf/cherries/core/_run.py +22 -52
  22. liblaf/cherries/core/_spec.py +10 -11
  23. liblaf/cherries/core/_utils.py +44 -1
  24. liblaf/cherries/meta/__init__.py +1 -0
  25. liblaf/cherries/meta/_git.py +3 -3
  26. liblaf/cherries/meta/_name.py +3 -3
  27. liblaf/cherries/path_utils/__init__.py +4 -0
  28. liblaf/cherries/{paths → path_utils}/__init__.pyi +2 -3
  29. liblaf/cherries/{paths → path_utils}/_convert.py +1 -1
  30. liblaf/cherries/path_utils/_path.py +43 -0
  31. liblaf/cherries/plugins/__init__.py +1 -0
  32. liblaf/cherries/plugins/comet.py +16 -9
  33. liblaf/cherries/plugins/dvc.py +1 -1
  34. liblaf/cherries/plugins/git_.py +17 -21
  35. liblaf/cherries/plugins/local.py +3 -3
  36. liblaf/cherries/plugins/logging.py +2 -2
  37. liblaf/cherries/profiles/__init__.py +1 -0
  38. liblaf/cherries/profiles/_factory.py +2 -3
  39. liblaf/cherries/utils/__init__.py +1 -0
  40. {liblaf_cherries-0.4.3.dist-info → liblaf_cherries-0.5.1.dist-info}/METADATA +2 -2
  41. liblaf_cherries-0.5.1.dist-info/RECORD +56 -0
  42. liblaf/cherries/config/_asset.py +0 -92
  43. liblaf/cherries/paths/_path.py +0 -70
  44. liblaf_cherries-0.4.3.dist-info/RECORD +0 -48
  45. /liblaf/cherries/core/{typed.py → typing.py} +0 -0
  46. /liblaf/cherries/{paths → path_utils}/_special.py +0 -0
  47. /liblaf/cherries/{typed.py → typing.py} +0 -0
  48. {liblaf_cherries-0.4.3.dist-info → liblaf_cherries-0.5.1.dist-info}/WHEEL +0 -0
  49. {liblaf_cherries-0.4.3.dist-info → liblaf_cherries-0.5.1.dist-info}/licenses/LICENSE +0 -0
@@ -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, paths, 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,17 +15,16 @@ from .core import (
15
15
  log_parameter,
16
16
  log_parameters,
17
17
  )
18
- from .paths import (
18
+ from .path_utils import (
19
19
  as_os_path,
20
20
  as_path,
21
21
  as_posix,
22
22
  data,
23
23
  entrypoint,
24
24
  exp_dir,
25
- git_root,
26
- git_root_safe,
27
25
  params,
28
26
  path,
27
+ project_dir,
29
28
  src,
30
29
  )
31
30
 
@@ -43,8 +42,6 @@ __all__ = [
43
42
  "end",
44
43
  "entrypoint",
45
44
  "exp_dir",
46
- "git_root",
47
- "git_root_safe",
48
45
  "input",
49
46
  "log_asset",
50
47
  "log_input",
@@ -59,8 +56,9 @@ __all__ = [
59
56
  "output",
60
57
  "params",
61
58
  "path",
62
- "paths",
59
+ "path_utils",
63
60
  "plugins",
61
+ "project_dir",
64
62
  "run",
65
63
  "src",
66
64
  "start",
@@ -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(
@@ -40,8 +41,8 @@ def start(
40
41
  ) -> core.Run:
41
42
  run: core.Run = profiles.factory(profile).init()
42
43
  run.start()
43
- run.log_other("cherries.entrypoint", run.entrypoint.relative_to(run.root_dir))
44
- run.log_other("cherries.exp_dir", run.exp_dir.relative_to(run.root_dir))
44
+ run.log_other("cherries.entrypoint", run.entrypoint.relative_to(run.project_dir))
45
+ run.log_other("cherries.exp_dir", run.exp_dir.relative_to(run.project_dir))
45
46
  run.log_other("cherries.start_time", run.start_time)
46
47
  return run
47
48
 
@@ -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.4.3'
32
- __version_tuple__ = version_tuple = (0, 4, 3)
31
+ __version__ = version = '0.5.1'
32
+ __version_tuple__ = version_tuple = (0, 5, 1)
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,7 +1,14 @@
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,
@@ -9,13 +16,18 @@ from ._asset import (
9
16
  model_dump_without_assets,
10
17
  output,
11
18
  )
12
- from ._config import BaseConfig
13
19
 
14
20
  __all__ = [
15
21
  "AssetKind",
22
+ "AssetResolver",
23
+ "AssetResolverRegistry",
24
+ "AssetResolverSeries",
25
+ "AssetResolverVtk",
16
26
  "BaseConfig",
27
+ "Extra",
17
28
  "MetaAsset",
18
- "PathGenerator",
29
+ "asset",
30
+ "asset_resolver_registry",
19
31
  "get_assets",
20
32
  "get_inputs",
21
33
  "get_outputs",
@@ -2,5 +2,4 @@ import pydantic_settings as ps
2
2
 
3
3
 
4
4
  class BaseConfig(ps.BaseSettings):
5
- model_config = ps.SettingsConfigDict(cli_parse_args=True)
6
- # TODO: add support for config files
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())
@@ -0,0 +1,4 @@
1
+ import lazy_loader as lazy
2
+
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
@@ -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 ._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)
@@ -1,30 +1,29 @@
1
- from collections.abc import Mapping, MutableMapping, Sequence
1
+ import math
2
+ import operator
3
+ from collections.abc import Mapping, Sequence
2
4
  from typing import Any, Self
3
5
 
4
6
  import attrs
7
+ import cachetools
5
8
  import networkx as nx
9
+ from loguru import logger
6
10
 
7
- from ._impl import ImplInfo, get_impl_info
8
- from ._spec import SpecInfo, collect_specs
9
- from .typed import MethodName
11
+ from ._impl import ImplInfo, collect_impls, get_impl_info
12
+ from .typing import MethodName, PluginId
10
13
 
11
14
 
12
15
  @attrs.define
13
16
  class Plugin:
14
- plugins: dict[str, "Plugin"] = attrs.field(factory=dict, kw_only=True)
17
+ plugins: dict[PluginId, "Plugin"] = attrs.field(factory=dict, kw_only=True)
15
18
 
16
19
  _plugin_parent: Self | None = attrs.field(default=None, kw_only=True)
17
- _sort_plugins_cache: MutableMapping[MethodName, Sequence["Plugin"]] = attrs.field(
18
- factory=dict, init=False
20
+ _cache_sort_plugins: cachetools.Cache[MethodName, Sequence["Plugin"]] = attrs.field(
21
+ factory=lambda: cachetools.Cache(math.inf), init=False
19
22
  )
20
23
 
21
- @classmethod
22
- def plugin_id_cls(cls) -> str:
23
- return cls.__name__
24
-
25
24
  @property
26
25
  def plugin_id(self) -> str:
27
- return self.plugin_id_cls()
26
+ return type(self).__name__
28
27
 
29
28
  @property
30
29
  def plugin_root(self) -> Self:
@@ -47,50 +46,54 @@ class Plugin:
47
46
  return []
48
47
  results: list[Any] = []
49
48
  for plugin in plugins:
50
- result: Any = getattr(plugin, method)(*args, **kwargs)
51
- if result is None:
52
- continue
53
- if first_result:
54
- return result
55
- results.append(result)
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)
56
61
  return results
57
62
 
58
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)
59
67
  plugin._plugin_parent = self # noqa: SLF001
60
68
  self.plugins[plugin.plugin_id] = plugin
61
69
 
62
- def _plugins_prepare(self) -> None:
63
- specs: dict[str, SpecInfo] = collect_specs(self)
64
- for method in specs:
65
- self._sort_plugins_cache[method] = self._plugins_sort(
66
- method, refresh_cache=True
67
- )
70
+ def _plugins_sort_cache_key(self, method: MethodName) -> MethodName:
71
+ return method
68
72
 
69
- def _plugins_sort(
70
- self, method: str, *, refresh_cache: bool = False
71
- ) -> Sequence["Plugin"]:
72
- if refresh_cache or method not in self._sort_plugins_cache:
73
- plugin_infos: dict[str, ImplInfo] = {
74
- plugin_id: info
75
- for plugin_id, plugin in self.plugins.items()
76
- if (info := get_impl_info(getattr(plugin, method, None))) is not None
77
- }
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
+ }
78
82
 
79
- def key_fn(node: str) -> int:
80
- return plugin_infos[node].priority
83
+ def key_fn(node: str) -> int:
84
+ return plugin_infos[node].priority
81
85
 
82
- graph = nx.DiGraph()
83
- for plugin_id, impl_info in plugin_infos.items():
84
- graph.add_node(plugin_id)
85
- for after in impl_info.after:
86
- if after in plugin_infos:
87
- graph.add_edge(after, plugin_id)
88
- for before in impl_info.before:
89
- if before in plugin_infos:
90
- graph.add_edge(plugin_id, before)
91
- self._sort_plugins_cache[method] = tuple(
92
- plugin
93
- for plugin_id in nx.lexicographical_topological_sort(graph, key=key_fn)
94
- if (plugin := self.plugins.get(plugin_id)) is not None
95
- )
96
- return self._sort_plugins_cache[method]
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
+ )