feat: add path completion, LCP-based suggestions, and validator tests
- Refactored `FalyxCompleter` to support longest common prefix (LCP) completions by default. - Added `_ensure_quote` helper to auto-quote completions containing spaces/tabs. - Integrated `_yield_lcp_completions` for consistent completion insertion logic. - Added `_suggest_paths()` helper to dynamically suggest filesystem paths for arguments of type `Path`. - Integrated path completion into `suggest_next()` for both positional and flagged arguments. - Updated `argument_examples.py` to include a `--path` argument (`Path | None`), demonstrating file path completion. - Enabled `CompleteStyle.COLUMN` for tab-completion menu formatting in interactive sessions. - Improved bottom bar docstring formatting with fenced code block examples. - Added safeguard to `word_validator` to reject `"N"` since it’s reserved for `yes_no_validator`. - Improved help panel rendering for commands (using `Padding` + `Panel`). - Added full test coverage for: - `FalyxCompleter` and LCP behavior (`tests/test_completer/`) - All validators (`tests/test_validators/`) - Bumped version to 0.1.80.
This commit is contained in:
42
tests/test_validators/test_command_validator.py
Normal file
42
tests/test_validators/test_command_validator.py
Normal file
@ -0,0 +1,42 @@
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
import pytest
|
||||
from prompt_toolkit.document import Document
|
||||
from prompt_toolkit.validation import ValidationError
|
||||
|
||||
from falyx.validators import CommandValidator
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_command_validator_validates_command():
|
||||
fake_falyx = AsyncMock()
|
||||
fake_falyx.get_command.return_value = (False, object(), (), {})
|
||||
validator = CommandValidator(fake_falyx, "Invalid!")
|
||||
|
||||
await validator.validate_async(Document("valid"))
|
||||
fake_falyx.get_command.assert_awaited_once()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_command_validator_rejects_invalid_command():
|
||||
fake_falyx = AsyncMock()
|
||||
fake_falyx.get_command.return_value = (False, None, (), {})
|
||||
validator = CommandValidator(fake_falyx, "Invalid!")
|
||||
|
||||
with pytest.raises(ValidationError):
|
||||
await validator.validate_async(Document("not_a_command"))
|
||||
|
||||
with pytest.raises(ValidationError):
|
||||
await validator.validate_async(Document(""))
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_command_validator_is_preview():
|
||||
fake_falyx = AsyncMock()
|
||||
fake_falyx.get_command.return_value = (True, None, (), {})
|
||||
validator = CommandValidator(fake_falyx, "Invalid!")
|
||||
|
||||
await validator.validate_async(Document("?preview_command"))
|
||||
fake_falyx.get_command.assert_awaited_once_with(
|
||||
"?preview_command", from_validate=True
|
||||
)
|
24
tests/test_validators/test_int_range_validator.py
Normal file
24
tests/test_validators/test_int_range_validator.py
Normal file
@ -0,0 +1,24 @@
|
||||
import pytest
|
||||
from prompt_toolkit.document import Document
|
||||
from prompt_toolkit.validation import ValidationError
|
||||
|
||||
from falyx.validators import int_range_validator
|
||||
|
||||
|
||||
def test_int_range_validator_accepts_valid_numbers():
|
||||
validator = int_range_validator(1, 10)
|
||||
for valid in ["1", "5", "10"]:
|
||||
validator.validate(Document(valid))
|
||||
|
||||
|
||||
@pytest.mark.parametrize("invalid", ["0", "11", "5.5", "hello", "-1", ""])
|
||||
def test_int_range_validator_rejects_invalid(invalid):
|
||||
validator = int_range_validator(1, 10)
|
||||
with pytest.raises(ValidationError):
|
||||
validator.validate(Document(invalid))
|
||||
|
||||
|
||||
def test_int_range_validator_edge_cases():
|
||||
validator = int_range_validator(1, 10)
|
||||
for valid in ["1", "10"]:
|
||||
validator.validate(Document(valid))
|
18
tests/test_validators/test_key_validator.py
Normal file
18
tests/test_validators/test_key_validator.py
Normal file
@ -0,0 +1,18 @@
|
||||
import pytest
|
||||
from prompt_toolkit.document import Document
|
||||
from prompt_toolkit.validation import ValidationError
|
||||
|
||||
from falyx.validators import key_validator
|
||||
|
||||
|
||||
def test_key_validator_accepts_valid_keys():
|
||||
validator = key_validator(["A", "B", "Z"])
|
||||
for valid in ["A", "B", "Z"]:
|
||||
validator.validate(Document(valid))
|
||||
|
||||
|
||||
@pytest.mark.parametrize("invalid", ["Y", "D", "C", "", "1", "AB", "ZB"])
|
||||
def test_key_validator_rejects_invalid(invalid):
|
||||
validator = key_validator(["A", "B", "Z"])
|
||||
with pytest.raises(ValidationError):
|
||||
validator.validate(Document(invalid))
|
73
tests/test_validators/test_multi_index_validator.py
Normal file
73
tests/test_validators/test_multi_index_validator.py
Normal file
@ -0,0 +1,73 @@
|
||||
import pytest
|
||||
from prompt_toolkit.document import Document
|
||||
from prompt_toolkit.validation import ValidationError
|
||||
|
||||
from falyx.validators import MultiIndexValidator
|
||||
|
||||
|
||||
def test_multi_index_validator_accepts_valid_indices():
|
||||
validator = MultiIndexValidator(
|
||||
1, 5, number_selections=3, separator=",", allow_duplicates=False, cancel_key="C"
|
||||
)
|
||||
for valid in ["1,2,3", "2,3,4", "1,4,5"]:
|
||||
validator.validate(Document(valid))
|
||||
|
||||
|
||||
def test_multi_index_validator_rejects_invalid_indices():
|
||||
validator = MultiIndexValidator(
|
||||
1, 5, number_selections=3, separator=",", allow_duplicates=False, cancel_key="C"
|
||||
)
|
||||
with pytest.raises(ValidationError):
|
||||
validator.validate(Document("A,!,F"))
|
||||
with pytest.raises(ValidationError):
|
||||
validator.validate(Document("0,6,7"))
|
||||
with pytest.raises(ValidationError):
|
||||
validator.validate(Document("1,2,2"))
|
||||
|
||||
|
||||
def test_multi_index_validator_rejects_invalid_number_of_selections():
|
||||
validator = MultiIndexValidator(
|
||||
1, 5, number_selections=3, separator=",", allow_duplicates=False, cancel_key="C"
|
||||
)
|
||||
with pytest.raises(ValidationError):
|
||||
validator.validate(Document("1,2"))
|
||||
with pytest.raises(ValidationError):
|
||||
validator.validate(Document("1,2,3,4"))
|
||||
validator = MultiIndexValidator(
|
||||
1, 5, number_selections=1, separator=",", allow_duplicates=False, cancel_key="C"
|
||||
)
|
||||
validator.validate(Document("1"))
|
||||
with pytest.raises(ValidationError):
|
||||
validator.validate(Document("2,3"))
|
||||
|
||||
|
||||
def test_multi_index_validator_cancel_key():
|
||||
validator = MultiIndexValidator(
|
||||
1, 5, number_selections=3, separator=",", allow_duplicates=False, cancel_key="C"
|
||||
)
|
||||
validator.validate(Document("C"))
|
||||
|
||||
|
||||
def test_multi_index_validator_cancel_alone():
|
||||
validator = MultiIndexValidator(
|
||||
1, 5, number_selections=3, separator=",", allow_duplicates=False, cancel_key="C"
|
||||
)
|
||||
with pytest.raises(ValidationError):
|
||||
validator.validate(Document("1,C"))
|
||||
|
||||
|
||||
def test_multi_index_validator_empty_input():
|
||||
validator = MultiIndexValidator(
|
||||
1, 5, number_selections=3, separator=",", allow_duplicates=False, cancel_key="C"
|
||||
)
|
||||
with pytest.raises(ValidationError):
|
||||
validator.validate(Document(""))
|
||||
|
||||
|
||||
def test_multi_index_validator_error_message_for_duplicates():
|
||||
validator = MultiIndexValidator(
|
||||
1, 5, number_selections=3, separator=",", allow_duplicates=False, cancel_key="C"
|
||||
)
|
||||
with pytest.raises(ValidationError) as e:
|
||||
validator.validate(Document("1,1,2"))
|
||||
assert "Duplicate selection" in str(e.value)
|
105
tests/test_validators/test_multi_key_validator.py
Normal file
105
tests/test_validators/test_multi_key_validator.py
Normal file
@ -0,0 +1,105 @@
|
||||
import pytest
|
||||
from prompt_toolkit.document import Document
|
||||
from prompt_toolkit.validation import ValidationError
|
||||
|
||||
from falyx.validators import MultiKeyValidator
|
||||
|
||||
|
||||
def test_multi_key_validator_accepts_valid_keys():
|
||||
validator = MultiKeyValidator(
|
||||
["A", "B", "C"],
|
||||
number_selections=2,
|
||||
separator=",",
|
||||
allow_duplicates=False,
|
||||
cancel_key="X",
|
||||
)
|
||||
for valid in ["A,B", "B,C", "A,C"]:
|
||||
validator.validate(Document(valid))
|
||||
|
||||
|
||||
def test_multi_key_validator_rejects_invalid_keys():
|
||||
validator = MultiKeyValidator(
|
||||
["A", "B", "C"],
|
||||
number_selections=2,
|
||||
separator=",",
|
||||
allow_duplicates=False,
|
||||
cancel_key="X",
|
||||
)
|
||||
with pytest.raises(ValidationError):
|
||||
validator.validate(Document("D,E,F"))
|
||||
with pytest.raises(ValidationError):
|
||||
validator.validate(Document("A,B,A"))
|
||||
with pytest.raises(ValidationError):
|
||||
validator.validate(Document("A,B,C,D"))
|
||||
|
||||
|
||||
def test_multi_key_validator_rejects_invalid_number_of_selections():
|
||||
validator = MultiKeyValidator(
|
||||
["A", "B", "C"],
|
||||
number_selections=2,
|
||||
separator=",",
|
||||
allow_duplicates=False,
|
||||
cancel_key="X",
|
||||
)
|
||||
with pytest.raises(ValidationError):
|
||||
validator.validate(Document("A")) # Not enough selections
|
||||
with pytest.raises(ValidationError):
|
||||
validator.validate(Document("A,B,C")) # Too many selections
|
||||
validator = MultiKeyValidator(
|
||||
["A", "B", "C"],
|
||||
number_selections=1,
|
||||
separator=",",
|
||||
allow_duplicates=False,
|
||||
cancel_key="X",
|
||||
)
|
||||
validator.validate(Document("A")) # Exactly one selection is valid
|
||||
with pytest.raises(ValidationError):
|
||||
validator.validate(Document("B,C")) # Too many selections
|
||||
|
||||
|
||||
def test_multi_key_validator_cancel_key():
|
||||
validator = MultiKeyValidator(
|
||||
["A", "B", "C"],
|
||||
number_selections=2,
|
||||
separator=",",
|
||||
allow_duplicates=False,
|
||||
cancel_key="X",
|
||||
)
|
||||
validator.validate(Document("X"))
|
||||
|
||||
|
||||
def test_multi_key_validator_cancel_alone():
|
||||
validator = MultiKeyValidator(
|
||||
["A", "B", "C"],
|
||||
number_selections=2,
|
||||
separator=",",
|
||||
allow_duplicates=False,
|
||||
cancel_key="X",
|
||||
)
|
||||
with pytest.raises(ValidationError):
|
||||
validator.validate(Document("A,X"))
|
||||
|
||||
|
||||
def test_multi_key_validator_empty_input():
|
||||
validator = MultiKeyValidator(
|
||||
["A", "B", "C"],
|
||||
number_selections=2,
|
||||
separator=",",
|
||||
allow_duplicates=False,
|
||||
cancel_key="X",
|
||||
)
|
||||
with pytest.raises(ValidationError):
|
||||
validator.validate(Document(""))
|
||||
|
||||
|
||||
def test_multi_key_validator_error_message_for_duplicates():
|
||||
validator = MultiKeyValidator(
|
||||
["A", "B", "C"],
|
||||
number_selections=2,
|
||||
separator=",",
|
||||
allow_duplicates=False,
|
||||
cancel_key="X",
|
||||
)
|
||||
with pytest.raises(ValidationError) as e:
|
||||
validator.validate(Document("A,A,B"))
|
||||
assert "Duplicate selection" in str(e.value)
|
29
tests/test_validators/test_word_validator.py
Normal file
29
tests/test_validators/test_word_validator.py
Normal file
@ -0,0 +1,29 @@
|
||||
import pytest
|
||||
from prompt_toolkit.document import Document
|
||||
from prompt_toolkit.validation import ValidationError
|
||||
|
||||
from falyx.validators import word_validator
|
||||
|
||||
|
||||
def test_word_validator_accepts_valid_words():
|
||||
validator = word_validator("apple")
|
||||
validator.validate(Document("apple"))
|
||||
validator.validate(Document("N"))
|
||||
|
||||
|
||||
def test_word_validator_accepts_case_insensitive():
|
||||
validator = word_validator("banana")
|
||||
validator.validate(Document("BANANA"))
|
||||
validator.validate(Document("banana"))
|
||||
|
||||
|
||||
def test_word_validator_rejects_n():
|
||||
with pytest.raises(ValueError):
|
||||
word_validator("N")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("invalid", ["yes", "no", "maybe", "", "1"])
|
||||
def test_word_validator_rejects_invalid(invalid):
|
||||
validator = word_validator("apple")
|
||||
with pytest.raises(ValidationError):
|
||||
validator.validate(Document(invalid))
|
18
tests/test_validators/test_words_validator.py
Normal file
18
tests/test_validators/test_words_validator.py
Normal file
@ -0,0 +1,18 @@
|
||||
import pytest
|
||||
from prompt_toolkit.document import Document
|
||||
from prompt_toolkit.validation import ValidationError
|
||||
|
||||
from falyx.validators import words_validator
|
||||
|
||||
|
||||
def test_words_validator_accepts_valid_words():
|
||||
validator = words_validator(["hello", "world", "falyx"])
|
||||
for valid in ["hello", "world", "falyx"]:
|
||||
validator.validate(Document(valid)) # should not raise
|
||||
|
||||
|
||||
@pytest.mark.parametrize("invalid", ["yes", "no", "maybe", "", "1"])
|
||||
def test_words_validator_rejects_invalid(invalid):
|
||||
validator = words_validator(["hello", "world", "falyx"])
|
||||
with pytest.raises(ValidationError):
|
||||
validator.validate(Document(invalid))
|
18
tests/test_validators/test_yes_no_validator.py
Normal file
18
tests/test_validators/test_yes_no_validator.py
Normal file
@ -0,0 +1,18 @@
|
||||
import pytest
|
||||
from prompt_toolkit.document import Document
|
||||
from prompt_toolkit.validation import ValidationError
|
||||
|
||||
from falyx.validators import yes_no_validator
|
||||
|
||||
|
||||
def test_yes_no_validator_accepts_y_and_n():
|
||||
validator = yes_no_validator()
|
||||
for valid in ["Y", "y", "N", "n"]:
|
||||
validator.validate(Document(valid))
|
||||
|
||||
|
||||
@pytest.mark.parametrize("invalid", ["yes", "no", "maybe", "", "1"])
|
||||
def test_yes_no_validator_rejects_invalid(invalid):
|
||||
validator = yes_no_validator()
|
||||
with pytest.raises(ValidationError):
|
||||
validator.validate(Document(invalid))
|
Reference in New Issue
Block a user