Skip to content

Empty slot selections bypass call-argument validation for empty dict/list bodies #27

Description

@pomponchik

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

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions