Skip to content

Commit 78b5cea

Browse files
authored
Merge pull request #24 from mutating/develop
0.0.20
2 parents e00c24e + 0d04691 commit 78b5cea

21 files changed

Lines changed: 2477 additions & 422 deletions

README.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -366,6 +366,28 @@ some_slot['non_existent_key']()
366366
#> run the slot default function
367367
```
368368

369+
When a slot or selection should resolve to exactly one callable candidate, prefer `.one` to manual collection checks. It works on slots and on selections returned by `[...]` or `pop()`:
370+
371+
```python
372+
@slot
373+
def sum_slot(a, b) -> list[int]:
374+
...
375+
376+
@sum_slot.plugin
377+
def sum_plugin(a, b) -> int:
378+
return a + b
379+
380+
selected_from_slot = sum_slot.one
381+
selected_by_name = sum_slot['sum_plugin'].one
382+
383+
print(selected_from_slot(1, 2))
384+
#> [3]
385+
print(selected_by_name(1, 2))
386+
#> [3]
387+
```
388+
389+
`.one` returns a callable selection; it does not call it. The arguments above are passed to that returned selection. For `sum_slot.one`, the selection contains the only plugin registered in the slot; for `sum_slot['sum_plugin'].one`, the only plugin in that selection. If no plugin matches but the slot body is non-empty, that body is used as fallback. Otherwise, or if there is more than one candidate, `pristan.errors.OneResolutionError` is raised.
390+
369391
You can use the [`len()`](https://docs.python.org/3/library/functions.html#len) function to find out how many plugins you have:
370392

371393
```python

docs/plans/1.md

Lines changed: 312 additions & 0 deletions
Large diffs are not rendered by default.

pristan/common_types.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,11 +49,15 @@ def __bool__(self) -> bool: ...
4949
def __len__(self) -> int: ...
5050

5151

52-
class SlotSelectionProtocol(BaseSlotViewProtocol[SlotParameters, SlotCallResultCovariant, PluginResultCovariant], Protocol[SlotParameters, SlotCallResultCovariant, PluginResultCovariant]):
53-
pass
52+
class SlotSelectionProtocol(BaseSlotViewProtocol[SlotParameters, SlotCallResultCovariant, PluginResultCovariant], Protocol[SlotParameters, SlotCallResultCovariant, PluginResultCovariant]): # pragma: no cover
53+
@property
54+
def one(self) -> 'SlotSelectionProtocol[SlotParameters, SlotCallResultCovariant, PluginResultCovariant]': ...
5455

5556

5657
class SlotProtocol(BaseSlotViewProtocol[SlotParameters, SlotCallResultCovariant, PluginResult], Protocol[SlotParameters, SlotCallResultCovariant, PluginResult]): # pragma: no cover
58+
@property
59+
def one(self) -> SlotSelectionProtocol[SlotParameters, SlotCallResultCovariant, PluginResult]: ...
60+
5761
@overload
5862
def plugin(self, plugin_function_or_name: Optional[str] = None, unique: bool = False, engine: Optional[Union[List[str], str]] = None, run_once: bool = False) -> Callable[[Callable[SlotParameters, PluginResult]], Callable[SlotParameters, PluginResult]]: ...
5963

pristan/components/slot.py

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
Generator,
1515
Generic,
1616
List,
17+
NoReturn,
1718
Optional,
1819
Tuple,
1920
TypeVar,
@@ -37,12 +38,16 @@
3738
)
3839
from pristan.components.plugin import Plugin
3940
from pristan.components.plugins_group import PluginsGroup
40-
from pristan.components.slot_caller import CallerWithPlugins, SlotCaller
41+
from pristan.components.slot_caller import (
42+
CallerWithPlugins,
43+
SlotCaller,
44+
)
4145
from pristan.components.slot_code_representer import SlotCodeRepresenter
4246
from pristan.components.slot_code_representer import sentinel as return_type_sentinel
4347
from pristan.errors import (
4448
EntrypointLoadingError,
4549
ExplicitNameRequiredError,
50+
OneResolutionError,
4651
PrimadonnaPluginError,
4752
PristanException,
4853
StrangeTypeAnnotationError,
@@ -109,6 +114,24 @@ def __bool__(self) -> bool:
109114
self._load_entrypoints()
110115
return bool(self.backed_caller)
111116

117+
@property
118+
def one(self) -> CallerWithPlugins[PluginResult]:
119+
self._load_entrypoints()
120+
snapshot = CallerWithPlugins(self.caller, list(self.plugins.plugins))
121+
if not snapshot:
122+
raise OneResolutionError(f'Slot "{self.slot_name}" has no registered plugins and its body is empty.')
123+
if len(snapshot) > 1:
124+
raise OneResolutionError(f'Slot "{self.slot_name}" has {len(snapshot)} registered plugins, so .one cannot choose one.')
125+
return snapshot
126+
127+
@one.setter
128+
def one(self, value: Any) -> NoReturn: # noqa: ARG002
129+
raise AttributeError('Attribute ".one" is read-only.')
130+
131+
@one.deleter
132+
def one(self) -> NoReturn:
133+
raise AttributeError('Attribute ".one" is read-only.')
134+
112135
def __iter__(self) -> Generator[PluginProtocol[SlotParameters, PluginResult], None, None]:
113136
self._load_entrypoints()
114137
yield from self.plugins

pristan/components/slot_caller.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import Any, Dict, Generator, Generic, List, Type, Union
1+
from typing import Any, Dict, Generator, Generic, List, NoReturn, Type, Union
22

33
from denial import InnerNoneType
44
from printo import repred
@@ -13,6 +13,7 @@
1313
from pristan.components.plugins_group import PluginsGroup
1414
from pristan.components.slot_code_representer import SlotCodeRepresenter
1515
from pristan.components.slot_code_representer import sentinel as return_type_sentinel
16+
from pristan.errors import OneResolutionError
1617

1718

1819
@repred
@@ -78,3 +79,19 @@ def __bool__(self) -> bool:
7879

7980
def __len__(self) -> int:
8081
return len(self.plugins)
82+
83+
@property
84+
def one(self) -> 'CallerWithPlugins[PluginResult]':
85+
if not self:
86+
raise OneResolutionError(f'Selection from slot "{self.caller.slot_name}" has no selected plugins and the slot body is empty.')
87+
if len(self) > 1:
88+
raise OneResolutionError(f'Selection from slot "{self.caller.slot_name}" has {len(self)} selected plugins, so .one cannot choose one.')
89+
return self
90+
91+
@one.setter
92+
def one(self, value: Any) -> NoReturn: # noqa: ARG002
93+
raise AttributeError('Attribute ".one" is read-only.')
94+
95+
@one.deleter
96+
def one(self) -> NoReturn:
97+
raise AttributeError('Attribute ".one" is read-only.')

pristan/errors.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,7 @@ class CannotGetVersionsError(PristanException):
2828

2929
class NumberOfCallsError(PristanException):
3030
...
31+
32+
33+
class OneResolutionError(PristanException):
34+
...

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "pristan"
7-
version = "0.0.19"
7+
version = "0.0.20"
88
authors = [{ name = "Evgeniy Blinov", email = "zheni-b@yandex.ru" }]
99
description = "Function-based plugin system with respect to typing"
1010
readme = "README.md"
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from tests.smokes.demo.simple_slots import simple_slot_3
2+
3+
4+
@simple_slot_3.plugin('name')
5+
def plugin() -> int:
6+
return 1
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from tests.smokes.demo.simple_slots import simple_custom_one_slot
2+
3+
4+
@simple_custom_one_slot.plugin('name2')
5+
def plugin() -> int:
6+
return 8
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from tests.smokes.demo.simple_slots import simple_slot_5
2+
3+
4+
@simple_slot_5.plugin('name')
5+
def plugin() -> int:
6+
return 1

0 commit comments

Comments
 (0)