nebius/SWE-rebench-V2-PRs huggingface dataset:
Repository: stanfordnlp/dspy
Pull request #8001 (2025-03-24 05:14:34)

Fix a bug in Teleprompter.get_params
Fix the bug in `get-params` method and change the test to capture the issue.

TomeHirata: Since this is a hotfix for an existing broken method, let me quickly merge it.

Patch:
diff --git a/dspy/teleprompt/teleprompt.py b/dspy/teleprompt/teleprompt.py
index c9003719..3c27226f 100644
--- a/dspy/teleprompt/teleprompt.py
+++ b/dspy/teleprompt/teleprompt.py
@@ -27,4 +27,4 @@ class Teleprompter:
         Returns:
             The parameters of the teleprompter.
         """
-        return self.__dict__()
+        return self.__dict__


Test patch:
diff --git a/tests/teleprompt/test_teleprompt.py b/tests/teleprompt/test_teleprompt.py
index a3117394..df183285 100644
--- a/tests/teleprompt/test_teleprompt.py
+++ b/tests/teleprompt/test_teleprompt.py
@@ -9,9 +9,6 @@ class DummyTeleprompter(Teleprompter):
     def compile(self, student, *, trainset, teacher=None, valset=None, **kwargs):
         return student
 
-    def get_params(self):
-        return self.__dict__
-
 
 def test_get_params():
     teleprompter = DummyTeleprompter(param1=1, param2="test")
Repository: stanfordnlp/dspy
Pull request #8087 (2025-04-20 19:49:49)

Fix Pydantic Deprecation warning
Currently, there is this deprecation warning message:

```
/opt/venv/lib/python3.12/site-packages/dspy/adapters/json_adapter.py:198: PydanticDeprecatedSince20: The `schema` method is deprecated; use `model_json_schema` instead. Deprecated in Pydantic V2.0 to be removed in V3.0. See Pydantic V2 Migration Guide at https://errors.pydantic.dev/2.11/migration/

  schema = Model.schema()
```

Updated the file with the warning


Patch:
diff --git a/dspy/adapters/json_adapter.py b/dspy/adapters/json_adapter.py
index 3f833f8b..d0bbc17c 100644
--- a/dspy/adapters/json_adapter.py
+++ b/dspy/adapters/json_adapter.py
@@ -195,7 +195,7 @@ def _get_structured_outputs_response_format(signature: SignatureMeta) -> type[py
     Model = pydantic.create_model("DSPyProgramOutputs", **fields, __config__=type("Config", (), {"extra": "forbid"}))
 
     # Generate the initial schema.
-    schema = Model.schema()
+    schema = Model.model_json_schema()
 
     # Remove any DSPy-specific metadata.
     for prop in schema.get("properties", {}).values():
diff --git a/dspy/primitives/tool.py b/dspy/primitives/tool.py
index 3e9a7182..8b3d1fa9 100644
--- a/dspy/primitives/tool.py
+++ b/dspy/primitives/tool.py
@@ -119,7 +119,7 @@ class Tool:
                 v_json_schema = self._resolve_pydantic_schema(v)
                 args[k] = v_json_schema
             else:
-                args[k] = TypeAdapter(v).json_schema() or "Any"
+                args[k] = TypeAdapter(v).json_schema()
             if default_values[k] is not inspect.Parameter.empty:
                 args[k]["default"] = default_values[k]
             if arg_desc and k in arg_desc:
@@ -137,7 +137,8 @@ class Tool:
                 raise ValueError(f"Arg {k} is not in the tool's args.")
             try:
                 instance = v.model_dump() if hasattr(v, "model_dump") else v
-                if not self.args[k] == "Any":
+                type_str = self.args[k].get("type")
+                if type_str is not None and type_str != "Any":
                     validate(instance=instance, schema=self.args[k])
             except ValidationError as e:
                 raise ValueError(f"Arg {k} is invalid: {e.message}")


Test patch:
diff --git a/tests/primitives/test_tool.py b/tests/primitives/test_tool.py
index 6b49f24f..02e8170b 100644
--- a/tests/primitives/test_tool.py
+++ b/tests/primitives/test_tool.py
@@ -150,3 +150,12 @@ def test_invalid_function_call():
 def test_parameter_desc():
     tool = Tool(dummy_function, arg_desc={"x": "The x parameter"})
     assert tool.args["x"]["description"] == "The x parameter"
+
+
+def test_tool_with_default_args_without_type_hints():
+    def foo(x=100):
+        return x
+
+    tool = Tool(foo)
+    assert tool.args["x"]["default"] == 100
+    assert not hasattr(tool.args["x"], "type")
Repository: stanfordnlp/dspy
Pull request #8102 (2025-04-24 04:42:42)

Support kwargs in dspy Tool
Allow using callables that accept arbitrary keyword args.

```python
def a(**kwargs):
    """arguments are c:int and d:int"""
    return kwargs.get("c", 1) * kwargs.get("d", 1)

tool = dspy.Tool(a)
tool(c=5, d=4)
```


Patch:
diff --git a/dspy/primitives/tool.py b/dspy/primitives/tool.py
index 94817397..9c8e388e 100644
--- a/dspy/primitives/tool.py
+++ b/dspy/primitives/tool.py
@@ -57,6 +57,7 @@ class Tool:
         self.args = args
         self.arg_types = arg_types
         self.arg_desc = arg_desc
+        self.has_kwargs = False
 
         self._parse_function(func, arg_desc)
 
@@ -129,6 +130,7 @@ class Tool:
         self.desc = self.desc or desc
         self.args = self.args or args
         self.arg_types = self.arg_types or arg_types
+        self.has_kwargs = any([param.kind == param.VAR_KEYWORD for param in sig.parameters.values()])
 
     def _parse_args(self, **kwargs):
         parsed_kwargs = {}
@@ -147,6 +149,9 @@ class Tool:
     def __call__(self, **kwargs):
         for k, v in kwargs.items():
             if k not in self.args:
+                if self.has_kwargs:
+                    # If the tool has kwargs, skip validation for unknown args
+                    continue
                 raise ValueError(f"Arg {k} is not in the tool's args.")
             try:
                 instance = v.model_dump() if hasattr(v, "model_dump") else v


Test patch:
diff --git a/tests/primitives/test_tool.py b/tests/primitives/test_tool.py
index ce6b3f44..02488597 100644
--- a/tests/primitives/test_tool.py
+++ b/tests/primitives/test_tool.py
@@ -193,3 +193,12 @@ def test_tool_call_parses_nested_list_of_pydantic_model():
 
     result = tool(**args)
     assert result == [[DummyModel(field1="hello", field2=123)]]
+
+
+def test_tool_call_kwarg():
+    def fn(x: int, **kwargs):
+        return kwargs
+    tool = Tool(fn)
+
+    assert tool(x=1, y=2, z=3) == {"y": 2, "z": 3}
+
Repository: stanfordnlp/dspy
Pull request #542 (2024-03-04 05:56:01)

bug(dspy): Move ensemble import to teleprompt
Adds ensemble to the teleprompt imports. Solves #533 


Patch:
diff --git a/dspy/teleprompt/__init__.py b/dspy/teleprompt/__init__.py
index 61425e5b..a1088f2d 100644
--- a/dspy/teleprompt/__init__.py
+++ b/dspy/teleprompt/__init__.py
@@ -1,4 +1,5 @@
 from .bootstrap import *
+from .ensemble import *
 from .finetune import *
 from .knn_fewshot import *
 from .random_search import *


Test patch:
diff --git a/tests/teleprompt/test_ensemble.py b/tests/teleprompt/test_ensemble.py
index 292176af..54ca2708 100644
--- a/tests/teleprompt/test_ensemble.py
+++ b/tests/teleprompt/test_ensemble.py
@@ -1,6 +1,6 @@
 import pytest
 import dspy
-from dspy.teleprompt.ensemble import Ensemble
+from dspy.teleprompt import Ensemble
 
 
 class MockProgram(dspy.Module):
Repository: stanfordnlp/dspy
Pull request #987 (2024-05-07 12:14:44)

Remove common indentations in signature according to Python Docstring convention
* Remove common indentations like it is done in docstring tools
* Fixes https://github.com/stanfordnlp/dspy/issues/986


Patch:
diff --git a/dspy/signatures/signature.py b/dspy/signatures/signature.py
index d10f7171..3ad35c31 100644
--- a/dspy/signatures/signature.py
+++ b/dspy/signatures/signature.py
@@ -1,4 +1,5 @@
 import ast
+import inspect
 import re
 import types
 import typing
@@ -88,7 +89,7 @@ class SignatureMeta(type(BaseModel)):
 
     @property
     def instructions(cls) -> str:
-        return getattr(cls, "__doc__", "")
+        return inspect.cleandoc(getattr(cls, "__doc__", ""))
 
     def with_instructions(cls, instructions: str) -> Type["Signature"]:
         return Signature(cls.fields, instructions)
@@ -217,9 +218,9 @@ def ensure_signature(signature: Union[str, Type[Signature]], instructions=None)
 
 
 def make_signature(
-    signature: Union[str, Dict[str, Tuple[type, FieldInfo]]],
-    instructions: str = None,
-    signature_name: str = "StringSignature",
+        signature: Union[str, Dict[str, Tuple[type, FieldInfo]]],
+        instructions: str = None,
+        signature_name: str = "StringSignature",
 ) -> Type[Signature]:
     """Create a new Signature type with the given fields and instructions.
 


Test patch:
diff --git a/tests/signatures/test_signature.py b/tests/signatures/test_signature.py
index d0eb899d..e635455b 100644
--- a/tests/signatures/test_signature.py
+++ b/tests/signatures/test_signature.py
@@ -183,7 +183,8 @@ def test_insantiating2():
 def test_multiline_instructions():
     class MySignature(Signature):
         """First line
-        Second line"""
+        Second line
+            Third line"""
 
         output = OutputField()
 
@@ -195,7 +196,8 @@ def test_multiline_instructions():
 
     assert lm.get_convo(-1) == textwrap.dedent("""\
         First line
-                Second line
+        Second line
+            Third line
         
         ---
Repository: stanfordnlp/dspy
Pull request #990 (2024-05-07 19:38:18)

fix(evaluate): `Evaluate` should not error when `display_table=True`
Obtained an error when `display_table=True` is provided for `Evaluate`. 
See simple reproducible example below:

```py
from dspy.evaluate.evaluate import Evaluate
from dspy.evaluate.metrics import answer_exact_match
from dspy.predict import Predict

def new_example(question, answer):
    """Helper function to create a new example."""
    return dspy.Example(
        question=question,
        answer=answer,
    ).with_inputs("question")

devset = [new_example("What is 1+1?", "2")]

program = Predict("question -> answer")

ev = Evaluate(
    devset=devset,
    metric=answer_exact_match,
    display_table=True,
)
ev(program)  # TypeError: cannot do positional indexing on RangeIndex with these indexers [True] of type bool
```

This stems from `isinstance(True, int) == True`. 

This PR contains a proposed fix with updated unit tests. Feel free to re-structure the unit tests as you see fit.


Patch:
diff --git a/dspy/evaluate/evaluate.py b/dspy/evaluate/evaluate.py
index 93524c3c..be071629 100644
--- a/dspy/evaluate/evaluate.py
+++ b/dspy/evaluate/evaluate.py
@@ -218,12 +218,12 @@ class Evaluate:
         result_df = result_df.rename(columns={"correct": metric_name})
 
         if display_table:
-            if isinstance(display_table, int):
-                df_to_display = result_df.head(display_table).copy()
-                truncated_rows = len(result_df) - display_table
-            else:
+            if isinstance(display_table, bool):
                 df_to_display = result_df.copy()
                 truncated_rows = 0
+            else:
+                df_to_display = result_df.head(display_table).copy()
+                truncated_rows = len(result_df) - display_table
 
             styled_df = configure_dataframe_display(df_to_display, metric_name)
 


Test patch:
diff --git a/tests/evaluate/test_evaluate.py b/tests/evaluate/test_evaluate.py
index a0ba9333..faa50953 100644
--- a/tests/evaluate/test_evaluate.py
+++ b/tests/evaluate/test_evaluate.py
@@ -112,11 +112,17 @@ def test_evaluate_call_bad():
     assert score == 0.0
 
 
-def test_evaluate_display_table():
+@pytest.mark.parametrize(
+    "display_table", [True, False, 1]
+)
+def test_evaluate_display_table(display_table):
     devset = [new_example("What is 1+1?", "2")]
+    program = Predict("question -> answer")
     ev = Evaluate(
         devset=devset,
         metric=answer_exact_match,
-        display_table=True,
+        display_table=display_table,
     )
-    assert ev.display_table == True
+    assert ev.display_table == display_table
+
+    ev(program)
Repository: stanfordnlp/dspy
Pull request #1294 (2024-07-19 16:54:03)

Fix conditional in Module.named_parameters() to prevent infinite recursion
The problem is illustrated as follows:

For a module such as the following I added to the tests:
```python3
class DuplicateModule(Module):
    def __init__(self):
        super().__init__()
        self.p0 = dspy.Predict("question -> answer")
        self.p1 = self.p0
```
Running its `named_parameters()` method would result in infinite recursion. This is due to that `Predict` is both a `Module` and a `Parameter`. Thus, according to the original arrangement of the if statements, suppose that `p0` is already visited. Then, by having the `Predict` instance already a member of the `visited` set and that it is a `Module`, the second condition would still execute when `p1` is visited.

I originally discovered this problem when using `assert_transform_module()`, which calls `Module.named_parameters()` at some point.


Patch:
diff --git a/dspy/primitives/module.py b/dspy/primitives/module.py
index 04bb47dd..085dd4a2 100644
--- a/dspy/primitives/module.py
+++ b/dspy/primitives/module.py
@@ -23,10 +23,11 @@ class BaseModule:
         named_parameters = []
 
         def add_parameter(param_name, param_value):
-            if isinstance(param_value, Parameter) and id(param_value) not in visited:
-                visited.add(id(param_value))
-                param_name = postprocess_parameter_name(param_name, param_value)
-                named_parameters.append((param_name, param_value))
+            if isinstance(param_value, Parameter):
+                if id(param_value) not in visited:
+                    visited.add(id(param_value))
+                    param_name = postprocess_parameter_name(param_name, param_value)
+                    named_parameters.append((param_name, param_value))
             
             elif isinstance(param_value, dspy.Module):
                 # When a sub-module is pre-compiled, keep it frozen.


Test patch:
diff --git a/tests/primitives/test_program.py b/tests/primitives/test_program.py
index 219ad525..63a37a75 100644
--- a/tests/primitives/test_program.py
+++ b/tests/primitives/test_program.py
@@ -136,3 +136,15 @@ def test_complex_module_traversal():
     assert (
         found_names == expected_names
     ), f"Missing or extra modules found. Missing: {expected_names-found_names}, Extra: {found_names-expected_names}"
+
+class DuplicateModule(Module):
+    def __init__(self):
+        super().__init__()
+        self.p0 = dspy.Predict("question -> answer")
+        self.p1 = self.p0
+
+def test_named_parameters_duplicate_references():
+   module = DuplicateModule()
+   # Only testing for whether exceptions are thrown or not
+   # As Module.named_parameters() is recursive, this is mainly for catching infinite recursion
+   module.named_parameters()
Repository: stanfordnlp/dspy
Pull request #1730 (2024-10-30 23:50:12)

Add test to validate markdown files referenced in mkdocs.yml
Add test to validate markdown files referenced in mkdocs.yml

This PR adds a test to ensure all markdown files referenced in the navigation section of mkdocs.yml exist in the docs directory. This helps prevent broken links and ensures documentation integrity.

Changes:
- Added new test `test_nav_files_exist` that:
  - Parses mkdocs.yml to extract markdown file paths
  - Verifies each file exists in docs/docs directory
  - Provides clear error message for any missing files

The test helps catch:
- Typos in file paths
- Missing documentation files
- Incorrect file references

Test location: tests/docs/test_mkdocs_links.py




Patch:
diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml
index 37e57034..a98084ba 100644
--- a/docs/mkdocs.yml
+++ b/docs/mkdocs.yml
@@ -32,7 +32,7 @@ nav:
       - ReAct: deep-dive/modules/react.md
       - MultiChainComparison: deep-dive/modules/multi-chain-comparison.md
       - ProgramOfThought: deep-dive/modules/program-of-thought.md
-      - Assertions: deep-dive/modules/assertions.md
+      - Assertions: deep-dive/assertions.md
       - Retreive: deep-dive/modules/retrieve.md
       - Modules Guide: deep-dive/modules/guide.md
     - Metrics and Assertions:


Test patch:
diff --git a/tests/docs/test_mkdocs_links.py b/tests/docs/test_mkdocs_links.py
new file mode 100644
index 00000000..5d5fe7c5
--- /dev/null
+++ b/tests/docs/test_mkdocs_links.py
@@ -0,0 +1,40 @@
+
+
+import pytest
+import os
+
+def test_nav_files_exist():
+    # Read mkdocs.yml
+    docs_dir = os.path.join(os.path.dirname(__file__), '..', '..', 'docs', 'docs')
+    yaml_path = os.path.join(os.path.dirname(__file__), '..', '..', 'docs', 'mkdocs.yml')
+    
+    # Read file and extract nav section
+    with open(yaml_path) as f:
+        content = f.read()
+    
+    # Find nav section
+    nav_start = content.find('nav:')
+    lines = content[nav_start:].split('\n')
+    
+    # Get markdown files
+    md_files = []
+    for line in lines:
+        if '.md' in line:
+            # Extract the markdown filename and clean it up
+            md_file = line.strip().split(':')[-1].strip()
+            # Remove list markers and quotes
+            md_file = md_file.lstrip('- ').strip("'").strip('"')
+            if md_file.endswith('.md'):
+                md_files.append(md_file)
+    
+    # Check if files exist
+    missing = []
+    for file in md_files:
+        if not os.path.exists(os.path.join(docs_dir, file)):
+            missing.append(file)
+    
+    print("\nChecking files in:", docs_dir)
+    print("Found MD files:", md_files)
+    print("Missing files:", missing)
+    
+    assert not missing, f"Missing files: {missing}"
\ No newline at end of file
Repository: go-playground/validator
Pull request #1156 (2023-08-30 02:58:18)

Fix Type called on zero reflect.Value
## Fixes Or Enhances
#1155

Simply added back the check for invalid reflect types before calling `Type` on `reflect.Value`.

**Make sure that you've checked the boxes below before you submit PR:**
- [x] Tests exist or have been written that cover this particular change.

@go-playground/validator-maintainers

coveralls: 
[![Coverage Status](https://coveralls.io/builds/62316383/badge)](https://coveralls.io/builds/62316383)

coverage: 73.849% (+0.006%) from 73.843% when pulling **82faf7721d819bae6e9a71e848160f1d40670442 on MysteriousPotato:main** into **1b40ba0cb450466ab9571f5adff040b141202daa on go-playground:master**.


Patch:
diff --git a/validator.go b/validator.go
index d3ff51d..2cae8f7 100644
--- a/validator.go
+++ b/validator.go
@@ -162,6 +162,10 @@ func (v *validate) traverseField(ctx context.Context, parent reflect.Value, curr
 			}
 		}
 
+		if kind == reflect.Invalid {
+			return
+		}
+
 	case reflect.Struct:
 		isNestedStruct = !current.Type().ConvertibleTo(timeType)
 		// For backward compatibility before struct level validation tags were supported


Test patch:
diff --git a/validator_test.go b/validator_test.go
index d9b715c..1dfa85c 100644
--- a/validator_test.go
+++ b/validator_test.go
@@ -2317,6 +2317,13 @@ func TestSQLValueValidation(t *testing.T) {
 	Equal(t, len(errs.(ValidationErrors)), 2)
 	AssertError(t, errs, "CustomMadeUpStruct.MadeUp", "CustomMadeUpStruct.MadeUp", "MadeUp", "MadeUp", "required")
 	AssertError(t, errs, "CustomMadeUpStruct.OverriddenInt", "CustomMadeUpStruct.OverriddenInt", "OverriddenInt", "OverriddenInt", "gt")
+
+	// Test for empty field on structs without tags
+	type InvalidValuePanicSafetyTest struct {
+		V valuer
+	}
+	errs = validate.Struct(InvalidValuePanicSafetyTest{})
+	Equal(t, errs, nil)
 }
 
 func TestMACValidation(t *testing.T) {
Repository: go-playground/validator
Pull request #1258 (2024-04-26 10:44:46)

Fix ULID should case-insensitive
## Fixes ULID should case-insensitive

@go-playground/validator-maintainers

coveralls: 
[![Coverage Status](https://coveralls.io/builds/67154524/badge)](https://coveralls.io/builds/67154524)

coverage: 74.208%. remained the same
when pulling **71af27584989f758faacd764212e584058b4c819 on akayj:master**
into **a0f74b0fb2a7ae1750c0f0b0a49550d8b6e2e708 on go-playground:master**.


Patch:
diff --git a/regexes.go b/regexes.go
index af98d8d..c67927a 100644
--- a/regexes.go
+++ b/regexes.go
@@ -31,7 +31,7 @@ const (
 	uUID4RFC4122RegexString          = "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$"
 	uUID5RFC4122RegexString          = "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-5[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$"
 	uUIDRFC4122RegexString           = "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$"
-	uLIDRegexString                  = "^[A-HJKMNP-TV-Z0-9]{26}$"
+	uLIDRegexString                  = "^(?i)[A-HJKMNP-TV-Z0-9]{26}$"
 	md4RegexString                   = "^[0-9a-f]{32}$"
 	md5RegexString                   = "^[0-9a-f]{32}$"
 	sha256RegexString                = "^[0-9a-f]{64}$"


Test patch:
diff --git a/validator_test.go b/validator_test.go
index 3b6d263..ab95f84 100644
--- a/validator_test.go
+++ b/validator_test.go
@@ -4338,7 +4338,7 @@ func TestULIDValidation(t *testing.T) {
 	}{
 		{"", false},
 		{"01BX5ZZKBKACT-V9WEVGEMMVRZ", false},
-		{"01bx5zzkbkactav9wevgemmvrz", false},
+		{"01bx5zzkbkactav9wevgemmvrz", true},
 		{"a987Fbc9-4bed-3078-cf07-9141ba07c9f3xxx", false},
 		{"01BX5ZZKBKACTAV9WEVGEMMVRZABC", false},
 		{"01BX5ZZKBKACTAV9WEVGEMMVRZABC", false},
Repository: go-playground/validator
Pull request #1444 (2025-06-27 10:18:23)

fix: make "file://" fail `url` validation
"file://" is not a valid URL since it has no path.

## Fixes Or Enhances

Makes `"file://"` fail the `url` validation instead of passing it.

This is akin to `"http://"` already failing as expected in tests. 

**Make sure that you've checked the boxes below before you submit PR:**
- [x] Tests exist or have been written that cover this particular change.

@go-playground/validator-maintainers

coveralls: 
[![Coverage Status](https://coveralls.io/builds/74369723/badge)](https://coveralls.io/builds/74369723)

coverage: 73.684% (-0.002%) from 73.686%
when pulling **254dc4bea9ec0b39df5487f2601e55e4018eb120 on bfabio:url_file**
into **3fd4678e4a0346d57ac1ebcb81ceb86a87ed59f0 on go-playground:master**.

bfabio: Hi @deankarn, AFAIK they are also URLs: https://url.spec.whatwg.org/#scheme-relative-file-url-string

TIL that they can have a `host` as in `file://localhost/etc/passwd` and `file:///etc/passwd` is just `/etc/passwd` with an empty host.

Patch:
diff --git a/baked_in.go b/baked_in.go
index 0a2eb42..c968ad4 100644
--- a/baked_in.go
+++ b/baked_in.go
@@ -1461,15 +1461,6 @@ func isURI(fl FieldLevel) bool {
 	panic(fmt.Sprintf("Bad field type %s", field.Type()))
 }
 
-// isFileURL is the helper function for validating if the `path` valid file URL as per RFC8089
-func isFileURL(path string) bool {
-	if !strings.HasPrefix(path, "file:/") {
-		return false
-	}
-	_, err := url.ParseRequestURI(path)
-	return err == nil
-}
-
 // isURL is the validation function for validating if the current field's value is a valid URL.
 func isURL(fl FieldLevel) bool {
 	field := fl.Field()
@@ -1483,16 +1474,13 @@ func isURL(fl FieldLevel) bool {
 			return false
 		}
 
-		if isFileURL(s) {
-			return true
-		}
-
 		url, err := url.Parse(s)
 		if err != nil || url.Scheme == "" {
 			return false
 		}
+		isFileScheme := url.Scheme == "file"
 
-		if url.Host == "" && url.Fragment == "" && url.Opaque == "" {
+		if (isFileScheme && (len(url.Path) == 0 || url.Path == "/")) || (!isFileScheme && len(url.Host) == 0 && len(url.Fragment) == 0 && len(url.Opaque) == 0) {
 			return false
 		}
 


Test patch:
diff --git a/validator_test.go b/validator_test.go
index 1273b47..2fcb3ff 100644
--- a/validator_test.go
+++ b/validator_test.go
@@ -8255,7 +8255,9 @@ func TestUrl(t *testing.T) {
 		{"file:///c:/Windows/file.txt", true},
 		{"file://localhost/path/to/file.txt", true},
 		{"file://localhost/c:/WINDOWS/file.txt", true},
-		{"file://", true},
+		{"file:", false},
+		{"file:/", false},
+		{"file://", false},
 		{"file:////remotehost/path/file.txt", true},
 	}