Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/python-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ jobs:
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install pytest
pip install pytest mypy
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi

- name: Run tests
Expand Down
9 changes: 9 additions & 0 deletions schema/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,15 @@ def __init__(
def __repr__(self) -> str:
return f"{self.__class__.__name__}({self._callable!r})"

def __call__(self, data: Any) -> Any:
"""Make Use instances callable by delegating to the wrapped callable.

This allows Use to work properly with And, Or, and other combinators
that expect callable arguments, while maintaining the validate() method
for the validator pattern.
"""
return self._callable(data)

def validate(self, data: Any, **kwargs: Any) -> Any:
try:
return self._callable(data)
Expand Down
198 changes: 198 additions & 0 deletions test_mypy_use_callable.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
"""Test that mypy no longer reports "incompatible type Use" errors.

This test actually runs mypy as a subprocess and verifies that the specific
error about Use being incompatible with And/Or is NOT present.
"""

import shutil
import subprocess
import sys
import tempfile
from pathlib import Path

import pytest


# Check if mypy is available
def is_mypy_available():
"""Check if mypy is available in the environment."""
return shutil.which("mypy") is not None or _can_import_mypy()


def _can_import_mypy():
"""Check if mypy can be imported as a module."""
try:
__import__("mypy")
return True
except ImportError:
return False


# Skip all tests in this module if mypy is not available
pytestmark = pytest.mark.skipif(
not is_mypy_available(), reason="mypy is not available in the environment"
)


def test_mypy_use_with_and_no_error():
"""Test that mypy doesn't report 'incompatible type Use' error with And."""
# Create a temporary test file with the problematic code
test_code = """
from schema import And, Use

# This used to fail with: Argument 2 to "And" has incompatible type "Use"
schema = And(int, Use(int))
"""

with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
f.write(test_code)
temp_file = f.name

try:
# Run mypy on the test file
result = subprocess.run(
[sys.executable, "-m", "mypy", "--no-error-summary", temp_file],
capture_output=True,
text=True,
)

# Check that the specific error about Use is NOT present
output = result.stdout + result.stderr

# The error we're looking for (should NOT be present)
error_pattern = 'incompatible type "Use"'

assert (
error_pattern.lower() not in output.lower()
), f"Found 'incompatible type Use' error in mypy output:\n{output}"

print("✓ mypy does not report 'incompatible type Use' error")

finally:
Path(temp_file).unlink(missing_ok=True)


def test_mypy_use_with_or_no_error():
"""Test that mypy doesn't report 'incompatible type Use' error with Or."""
test_code = """
from schema import Or, Use

# This used to fail with: Argument to "Or" has incompatible type "Use"
schema = Or(Use(int), Use(float))
"""

with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
f.write(test_code)
temp_file = f.name

try:
result = subprocess.run(
[sys.executable, "-m", "mypy", "--no-error-summary", temp_file],
capture_output=True,
text=True,
)

output = result.stdout + result.stderr
error_pattern = 'incompatible type "Use"'

assert (
error_pattern.lower() not in output.lower()
), f"Found 'incompatible type Use' error in mypy output:\n{output}"

print("✓ mypy does not report 'incompatible type Use' error with Or")

finally:
Path(temp_file).unlink(missing_ok=True)


def test_mypy_use_with_and_complex_no_error():
"""Test complex And/Or combinations with Use don't produce mypy errors."""
test_code = """
from schema import And, Or, Use, Schema, Optional

# Complex cases that used to fail
schema1 = And(str, Use(int))
schema2 = Or(int, Use(str))
schema3 = Schema({
"age": And(Use(int), lambda n: 0 <= n <= 120),
"name": Use(str.title),
Optional("email"): Use(str.lower),
})
"""

with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
f.write(test_code)
temp_file = f.name

try:
result = subprocess.run(
[sys.executable, "-m", "mypy", "--no-error-summary", temp_file],
capture_output=True,
text=True,
)

output = result.stdout + result.stderr
error_pattern = 'incompatible type "Use"'

assert (
error_pattern.lower() not in output.lower()
), f"Found 'incompatible type Use' error in mypy output:\n{output}"

print("✓ mypy does not report 'incompatible type Use' error in complex schemas")

finally:
Path(temp_file).unlink(missing_ok=True)


def test_mypy_use_callable_recognized():
"""Test that Use is recognized as callable by mypy."""
test_code = """
from schema import Use

use_int = Use(int)

# This should work - Use should be callable
assert callable(use_int)
result = use_int("123")
"""

with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
f.write(test_code)
temp_file = f.name

try:
result = subprocess.run(
[sys.executable, "-m", "mypy", "--no-error-summary", temp_file],
capture_output=True,
text=True,
)

output = result.stdout + result.stderr

# Should not have any "not callable" errors
assert (
"not callable" not in output.lower()
), f"mypy thinks Use is not callable:\n{output}"

print("✓ mypy recognizes Use as callable")

finally:
Path(temp_file).unlink(missing_ok=True)


if __name__ == "__main__":
if not is_mypy_available():
print("⚠️ mypy is not available in the environment.")
print("These tests require mypy to be installed.")
print("Install with: pip install mypy")
sys.exit(1)

print("Running mypy verification tests...\n")

test_mypy_use_with_and_no_error()
test_mypy_use_with_or_no_error()
test_mypy_use_with_and_complex_no_error()
test_mypy_use_callable_recognized()

print("\n✅ All mypy verification tests passed!")
print("The 'incompatible type Use' error has been fixed.")
3 changes: 3 additions & 0 deletions tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,14 @@ commands = py.test
recreate = true
deps = pytest
mock
mypy


[testenv:py38]
commands = py.test --doctest-glob=README.rst # test documentation
deps = pytest
mock
mypy

[testenv:checks]
basepython=python3
Expand All @@ -32,3 +34,4 @@ deps = pytest
pytest-cov
coverage
mock
mypy