Calling a valid-but-empty slot selection can return an empty aggregate without checking whether the call arguments match the parent slot signature.
This is easiest to see with a dict-returning slot whose body is a literal empty dict:
from typing import Dict
from pristan import slot
@slot
def collect(value: int) -> Dict[str, int]:
return {}
selection = collect['missing_plugin']
result = selection()
print(result) # {}
The surprising part is selection(): the parent slot requires value, but no TypeError is raised.
This happens because return {} is treated as an empty body for dict-aggregating slots. The selection has no selected plugins, so the call takes the dict aggregation path over an empty plugin list and returns {} directly. Neither the parent slot function nor any plugin function is called, so Python never gets a chance to reject the missing value argument.
The same shape matters for list-aggregating slots with a literal empty-list body:
from typing import List
from pristan import slot
@slot
def collect(value: int) -> List[int]:
return []
selection = collect['missing_plugin']
result = selection()
print(result) # []
Again, value is required by the slot signature, but the empty selection returns the empty aggregate.
Expected Behavior
Even when a selection has no plugins and the slot body is treated as empty, calling the selection should still validate the call against the slot signature.
For example:
from typing import Dict
import pytest
from pristan import slot
@slot
def collect(value: int) -> Dict[str, int]:
return {}
selection = collect['missing_plugin']
assert selection(1) == {}
with pytest.raises(TypeError):
selection()
The exact error message is less important than the behavior: a call with missing required arguments should not silently return an empty aggregate.
The same principle should apply to too many positional arguments:
with pytest.raises(TypeError):
selection(1, 2)
Important Non-Reproduction Case
The problem does not show up when the selection contains one or more plugins, because the selected plugin function is actually called.
For example, with multiple selected plugins:
from typing import Dict
import pytest
from pristan import slot
@slot
def collect(value: int) -> Dict[str, int]:
return {}
@collect.plugin('plugin')
def first(value: int) -> int:
return value
@collect.plugin('plugin')
def second(value: int) -> int:
return value + 1
selection = collect['plugin']
assert selection(1) == {'plugin': 1, 'plugin-2': 2}
with pytest.raises(TypeError):
selection()
Here selection() raises, because at least one plugin is called and Python checks that the plugin function receives its required value argument.
So the bug is specifically about an empty selection, not about normal plugin dispatch.
Suggested Regression Test
This test currently fails because some_slot['missing']() returns {} instead of raising TypeError. It should start passing once empty selections validate call arguments before returning an empty aggregate.
import pytest
from pristan import slot
def test_empty_dict_selection_validates_slot_call_arguments(folder_slot, dict_type):
@folder_slot(slot)
def some_slot(value: int) -> dict_type:
return {}
selection = some_slot['missing']
assert selection(1) == {}
with pytest.raises(TypeError):
selection()
with pytest.raises(TypeError):
selection(1, 2)
A list-returning companion would catch the same issue for empty list aggregation:
import pytest
from pristan import slot
def test_empty_list_selection_validates_slot_call_arguments(folder_slot, list_type):
@folder_slot(slot)
def some_slot(value: int) -> list_type:
return []
selection = some_slot['missing']
assert selection(1) == []
with pytest.raises(TypeError):
selection()
with pytest.raises(TypeError):
selection(1, 2)
Environment
- Library:
pristan 0.0.22
- OS: macOS (
macOS-10.16-x86_64-i386-64bit, Darwin 24.3.0, x86_64)
- Python:
3.8.10
Calling a valid-but-empty slot selection can return an empty aggregate without checking whether the call arguments match the parent slot signature.
This is easiest to see with a dict-returning slot whose body is a literal empty dict:
The surprising part is
selection(): the parent slot requiresvalue, but noTypeErroris raised.This happens because
return {}is treated as an empty body for dict-aggregating slots. The selection has no selected plugins, so the call takes the dict aggregation path over an empty plugin list and returns{}directly. Neither the parent slot function nor any plugin function is called, so Python never gets a chance to reject the missingvalueargument.The same shape matters for list-aggregating slots with a literal empty-list body:
Again,
valueis required by the slot signature, but the empty selection returns the empty aggregate.Expected Behavior
Even when a selection has no plugins and the slot body is treated as empty, calling the selection should still validate the call against the slot signature.
For example:
The exact error message is less important than the behavior: a call with missing required arguments should not silently return an empty aggregate.
The same principle should apply to too many positional arguments:
Important Non-Reproduction Case
The problem does not show up when the selection contains one or more plugins, because the selected plugin function is actually called.
For example, with multiple selected plugins:
Here
selection()raises, because at least one plugin is called and Python checks that the plugin function receives its requiredvalueargument.So the bug is specifically about an empty selection, not about normal plugin dispatch.
Suggested Regression Test
This test currently fails because
some_slot['missing']()returns{}instead of raisingTypeError. It should start passing once empty selections validate call arguments before returning an empty aggregate.A list-returning companion would catch the same issue for empty list aggregation:
Environment
pristan 0.0.22macOS-10.16-x86_64-i386-64bit, Darwin24.3.0,x86_64)3.8.10