From 9911d6163ffc1ced798583c30571d787565db9b9 Mon Sep 17 00:00:00 2001 From: Alvaro Frias Date: Thu, 16 Oct 2025 14:10:04 -0300 Subject: [PATCH 1/4] add __call__ to Use Signed-off-by: Alvaro Frias --- schema/__init__.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/schema/__init__.py b/schema/__init__.py index c27ee78..0b8134b 100644 --- a/schema/__init__.py +++ b/schema/__init__.py @@ -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) From d369c4ee5afe45cfa5eca14ee1e1153aa39fc07f Mon Sep 17 00:00:00 2001 From: Alvaro Frias Date: Thu, 16 Oct 2025 14:10:27 -0300 Subject: [PATCH 2/4] add test Signed-off-by: Alvaro Frias --- test_mypy_use_callable.py | 190 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 190 insertions(+) create mode 100644 test_mypy_use_callable.py diff --git a/test_mypy_use_callable.py b/test_mypy_use_callable.py new file mode 100644 index 0000000..5c3af85 --- /dev/null +++ b/test_mypy_use_callable.py @@ -0,0 +1,190 @@ +"""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 subprocess +import sys +import tempfile +from pathlib import Path +import shutil +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(f"✓ 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(f"✓ 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( + f"✓ 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(f"✓ 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() From 1f393bcc150d975c891c6819c9d8dac213b74114 Mon Sep 17 00:00:00 2001 From: Alvaro Frias Date: Thu, 16 Oct 2025 14:17:14 -0300 Subject: [PATCH 3/4] update ci Signed-off-by: Alvaro Frias --- .github/workflows/python-test.yml | 2 +- tox.ini | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/python-test.yml b/.github/workflows/python-test.yml index 3b3b580..c6b865e 100644 --- a/.github/workflows/python-test.yml +++ b/.github/workflows/python-test.yml @@ -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 diff --git a/tox.ini b/tox.ini index 74f2d71..f83a301 100644 --- a/tox.ini +++ b/tox.ini @@ -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 @@ -32,3 +34,4 @@ deps = pytest pytest-cov coverage mock + mypy From 45aedd05d29555844783e799f5e1a765b7b8636c Mon Sep 17 00:00:00 2001 From: Alvaro Frias Date: Thu, 16 Oct 2025 14:35:17 -0300 Subject: [PATCH 4/4] pre-commit fix Signed-off-by: Alvaro Frias --- test_mypy_use_callable.py | 94 +++++++++++++++++++++------------------ 1 file changed, 51 insertions(+), 43 deletions(-) diff --git a/test_mypy_use_callable.py b/test_mypy_use_callable.py index 5c3af85..5d6c69a 100644 --- a/test_mypy_use_callable.py +++ b/test_mypy_use_callable.py @@ -3,24 +3,26 @@ 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 shutil + 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() + 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') + __import__("mypy") return True except ImportError: return False @@ -28,30 +30,31 @@ def _can_import_mypy(): # 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") + 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 = ''' + 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: + 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], + [sys.executable, "-m", "mypy", "--no-error-summary", temp_file], capture_output=True, - text=True) + text=True, + ) # Check that the specific error about Use is NOT present output = result.stdout + result.stderr @@ -59,10 +62,11 @@ def test_mypy_use_with_and_no_error(): # 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}") + assert ( + error_pattern.lower() not in output.lower() + ), f"Found 'incompatible type Use' error in mypy output:\n{output}" - print(f"✓ mypy does not report 'incompatible type Use' error") + print("✓ mypy does not report 'incompatible type Use' error") finally: Path(temp_file).unlink(missing_ok=True) @@ -70,31 +74,32 @@ def test_mypy_use_with_and_no_error(): def test_mypy_use_with_or_no_error(): """Test that mypy doesn't report 'incompatible type Use' error with Or.""" - test_code = ''' + 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: + 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], + [sys.executable, "-m", "mypy", "--no-error-summary", temp_file], capture_output=True, - text=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}") + assert ( + error_pattern.lower() not in output.lower() + ), f"Found 'incompatible type Use' error in mypy output:\n{output}" - print(f"✓ mypy does not report 'incompatible type Use' error with Or") + print("✓ mypy does not report 'incompatible type Use' error with Or") finally: Path(temp_file).unlink(missing_ok=True) @@ -102,7 +107,7 @@ def test_mypy_use_with_or_no_error(): def test_mypy_use_with_and_complex_no_error(): """Test complex And/Or combinations with Use don't produce mypy errors.""" - test_code = ''' + test_code = """ from schema import And, Or, Use, Schema, Optional # Complex cases that used to fail @@ -113,28 +118,27 @@ def test_mypy_use_with_and_complex_no_error(): "name": Use(str.title), Optional("email"): Use(str.lower), }) -''' +""" - with tempfile.NamedTemporaryFile(mode='w', suffix='.py', - delete=False) as f: + 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], + [sys.executable, "-m", "mypy", "--no-error-summary", temp_file], capture_output=True, - text=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}") + assert ( + error_pattern.lower() not in output.lower() + ), f"Found 'incompatible type Use' error in mypy output:\n{output}" - print( - f"✓ mypy does not report 'incompatible type Use' error in complex schemas" - ) + print("✓ mypy does not report 'incompatible type Use' error in complex schemas") finally: Path(temp_file).unlink(missing_ok=True) @@ -142,7 +146,7 @@ def test_mypy_use_with_and_complex_no_error(): def test_mypy_use_callable_recognized(): """Test that Use is recognized as callable by mypy.""" - test_code = ''' + test_code = """ from schema import Use use_int = Use(int) @@ -150,26 +154,27 @@ def test_mypy_use_callable_recognized(): # 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: + 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], + [sys.executable, "-m", "mypy", "--no-error-summary", temp_file], capture_output=True, - text=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}") + assert ( + "not callable" not in output.lower() + ), f"mypy thinks Use is not callable:\n{output}" - print(f"✓ mypy recognizes Use as callable") + print("✓ mypy recognizes Use as callable") finally: Path(temp_file).unlink(missing_ok=True) @@ -177,7 +182,7 @@ def test_mypy_use_callable_recognized(): if __name__ == "__main__": if not is_mypy_available(): - print("mypy is not available in the environment.") + 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) @@ -188,3 +193,6 @@ def test_mypy_use_callable_recognized(): 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.")