fix(python): reject invalid punctuation in UDF names (#4724)

Function names containing periods or hyphens passed `@udf` validation
and failed only when registered with the server. Restrict the name
pattern to ASCII letters, digits, and underscores, preserving the
existing first-character rule so invalid names fail when the decorator
runs.

Document the accepted format and add regression coverage for dotted and
hyphenated names, including trailing punctuation, alongside valid name
overrides.

Fixes #4720

Validation:

- The four new invalid-name cases failed before the validator change
because no `ValueError` was raised.
- `cd python && uv run --extra tests pytest
python/tests/test_first_class_function_slice2.py -q`: 86 passed.
- Ruff 0.9.9: `ruff check python` and `ruff format --check python`
passed.
- Repository-root `ruff check .` reports 48 pre-existing errors in
`docs/web-tests/`.

<!-- lance-gatekeeper-fix:v1 agent=5e2441d3d264b685a6cee3c3c21ab048
generation=1 -->

Co-authored-by: Gatefixer <313497061+lancedb-gatefixer[bot]@users.noreply.github.com>
This commit is contained in:
lancedb-gatefixer[bot]
2026-10-28 15:57:48 -07:00
committed by GitHub
parent 5f6f7b591d
commit ca60b402aa
2 changed files with 24 additions and 1 deletions
+3 -1
View File
@@ -674,7 +674,7 @@ class RefreshColumnResult(_RemoteValue):
return self.published_version
_FUNCTION_NAME = re.compile(r"^[A-Za-z_][A-Za-z0-9_.-]*$")
_FUNCTION_NAME = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$")
_FUNCTION_BLOB_V2_TYPE = "blob_v2"
_ARROW_EXTENSION_NAME_KEY = "ARROW:extension:name"
@@ -2106,6 +2106,8 @@ def udf(
The synchronous callable, or the class whose instances are called.
name : str, optional
The remote Function name. Defaults to the callable name.
Must start with an ASCII letter or underscore and contain only ASCII
letters, digits, and underscores.
input_schema : pyarrow.Schema, optional
Explicit input fields in the exact order of the callable parameters.
Must be provided together with ``output_schema``.
@@ -89,6 +89,27 @@ def test_scalar_udf_matches_shared_registration_golden_and_remains_callable():
}
@pytest.mark.parametrize(
"name", ["examples.image_info", "image-info", "image_info.", "image_info-"]
)
def test_udf_rejects_function_names_with_periods_or_hyphens(name):
with pytest.raises(ValueError, match="invalid Function name"):
@udf(name=name)
def image_info(value: int) -> int:
return value
@pytest.mark.parametrize("name", ["image_info", "ImageInfo123", "_image_info", "f"])
def test_udf_accepts_function_names_with_letters_digits_and_underscores(name):
@udf(name=name)
def image_info(value: int) -> int:
return value
assert image_info.registration_request.name == name
assert image_info(1) == 1
def test_secret_bound_udf_matches_its_shared_registration_golden():
assert analyze_caption(" hello ") == "hello"
bound = analyze_caption.bind_secrets(