mirror of
https://github.com/lancedb/lancedb.git
synced 2026-11-13 14:16:54 +00:00
feat(python)!: rename the @udf decorator to @function (#4837)
The client registers Functions, but the decorator that packages one was called `udf` and produced a `UdfDefinition`. Rename them to `function` and `FunctionDefinition`, and say "function" instead of "UDF" in the docstrings, error messages and docs around them. `udf` and `UdfDefinition` stay as deprecated aliases that emit a `DeprecationWarning`. The source packager strips either spelling of the decorator, so `@udf` and `@function` produce the same artifact and digest, and the wire contract is unchanged. DataFusion and Lance UDF types, and Geneva's `@udf` in the integration docs, are unrelated and untouched.
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
|
||||
|
||||
import os
|
||||
import warnings
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from datetime import timedelta
|
||||
from typing import Dict, Optional, Union, Any, List, Iterable, Literal, TYPE_CHECKING
|
||||
@@ -39,12 +40,13 @@ from .functions import (
|
||||
FunctionArtifactRequest as FunctionArtifactRequest,
|
||||
FunctionApplication as FunctionApplication,
|
||||
FunctionBinding as FunctionBinding,
|
||||
FunctionDefinition as FunctionDefinition,
|
||||
FunctionRegistrationRequest as FunctionRegistrationRequest,
|
||||
FunctionVersion as FunctionVersion,
|
||||
HuggingFaceModel as HuggingFaceModel,
|
||||
PythonRuntimeSpec as PythonRuntimeSpec,
|
||||
RefreshColumnResult as RefreshColumnResult,
|
||||
UdfDefinition as UdfDefinition,
|
||||
function as function,
|
||||
udf as udf,
|
||||
)
|
||||
from .secrets import EnvVarSecret as EnvVarSecret
|
||||
@@ -77,6 +79,7 @@ from .catalog import (
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from lance.blob import BlobType as BlobType
|
||||
from .functions import UdfDefinition as UdfDefinition
|
||||
|
||||
|
||||
def __getattr__(name: str):
|
||||
@@ -85,6 +88,13 @@ def __getattr__(name: str):
|
||||
|
||||
globals()["BlobType"] = BlobType
|
||||
return BlobType
|
||||
if name == "UdfDefinition":
|
||||
from . import functions
|
||||
|
||||
warnings.warn(
|
||||
functions.UDF_DEFINITION_DEPRECATION, DeprecationWarning, stacklevel=2
|
||||
)
|
||||
return functions.FunctionDefinition
|
||||
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
||||
|
||||
|
||||
|
||||
+12
-12
@@ -58,7 +58,7 @@ from lance_namespace.errors import NamespaceNotEmptyError, TableNotFoundError
|
||||
|
||||
from . import __version__
|
||||
from ._lancedb import _append_doc_url, connect as lancedb_connect # type: ignore
|
||||
from .functions import FunctionVersion, UdfDefinition
|
||||
from .functions import FunctionVersion, FunctionDefinition
|
||||
from .graph import (
|
||||
AsyncPropertyGraph,
|
||||
EdgeTable,
|
||||
@@ -958,12 +958,12 @@ class DBConnection(EnforceOverrides):
|
||||
|
||||
def create_function(
|
||||
self,
|
||||
definition: UdfDefinition,
|
||||
definition: FunctionDefinition,
|
||||
*,
|
||||
secrets: Optional[Sequence[EnvVarSecret]] = None,
|
||||
namespace_path: Optional[List[str]] = None,
|
||||
) -> FunctionVersion:
|
||||
"""Build and register a scalar Python UDF, then return its version.
|
||||
"""Build and register a scalar Python function, then return its version.
|
||||
|
||||
The server builds the OCI image and registers the completed artifact.
|
||||
This is the blocking counterpart of :meth:`create_function_async`.
|
||||
@@ -971,8 +971,8 @@ class DBConnection(EnforceOverrides):
|
||||
|
||||
Parameters
|
||||
----------
|
||||
definition : UdfDefinition
|
||||
A callable decorated with [udf][lancedb.udf].
|
||||
definition : FunctionDefinition
|
||||
A callable decorated with [function][lancedb.function].
|
||||
secrets : sequence of EnvVarSecret, optional
|
||||
One [EnvVarSecret][lancedb.secrets.EnvVarSecret] per credential the
|
||||
Function needs, each naming a Secret and the environment variable
|
||||
@@ -1002,12 +1002,12 @@ class DBConnection(EnforceOverrides):
|
||||
|
||||
def create_function_async(
|
||||
self,
|
||||
definition: UdfDefinition,
|
||||
definition: FunctionDefinition,
|
||||
*,
|
||||
secrets: Optional[Sequence[EnvVarSecret]] = None,
|
||||
namespace_path: Optional[List[str]] = None,
|
||||
) -> Job[FunctionVersion]:
|
||||
"""Submit a scalar Python UDF for building and registration.
|
||||
"""Submit a scalar Python function for building and registration.
|
||||
|
||||
The server-side job builds the OCI image, then registers the completed
|
||||
artifact. The returned job has type ``query`` and the SQL query UUID as
|
||||
@@ -2284,7 +2284,7 @@ class LanceDBConnection(DBConnection):
|
||||
@override
|
||||
def create_function_async(
|
||||
self,
|
||||
definition: UdfDefinition,
|
||||
definition: FunctionDefinition,
|
||||
*,
|
||||
secrets: Optional[Sequence[EnvVarSecret]] = None,
|
||||
namespace_path: Optional[List[str]] = None,
|
||||
@@ -3573,12 +3573,12 @@ class AsyncConnection(object):
|
||||
|
||||
async def create_function_async(
|
||||
self,
|
||||
definition: UdfDefinition,
|
||||
definition: FunctionDefinition,
|
||||
*,
|
||||
secrets: Optional[Sequence[EnvVarSecret]] = None,
|
||||
namespace_path: Optional[List[str]] = None,
|
||||
) -> AsyncJob[FunctionVersion]:
|
||||
"""Submit a scalar Python UDF for building and registration.
|
||||
"""Submit a scalar Python function for building and registration.
|
||||
|
||||
The server-side job builds the OCI image, then registers the completed
|
||||
artifact. The returned job has type ``query`` and the SQL query UUID as
|
||||
@@ -3589,8 +3589,8 @@ class AsyncConnection(object):
|
||||
the namespace the Function is registered in; None or an empty list is
|
||||
the root. LanceDB Embedded connections raise ``NotImplementedError``.
|
||||
"""
|
||||
if not isinstance(definition, UdfDefinition):
|
||||
raise TypeError("create_function_async requires a @udf definition")
|
||||
if not isinstance(definition, FunctionDefinition):
|
||||
raise TypeError("create_function_async requires a @function definition")
|
||||
request = definition.bind_secrets(secrets)
|
||||
inner = await self._inner.create_function_async(
|
||||
request.to_canonical_json(), namespace_path
|
||||
|
||||
@@ -35,6 +35,7 @@ from typing import (
|
||||
Callable,
|
||||
Literal,
|
||||
Optional,
|
||||
TYPE_CHECKING,
|
||||
Union,
|
||||
get_args,
|
||||
get_origin,
|
||||
@@ -333,7 +334,7 @@ class HuggingFaceModel(_RemoteValue):
|
||||
snapshot before initialization, so the model can load without a revision
|
||||
argument or a Hugging Face Hub request::
|
||||
|
||||
@udf(
|
||||
@function(
|
||||
pip=["sentence-transformers==3.3.1"],
|
||||
models=[HuggingFaceModel(
|
||||
repo="sentence-transformers/all-MiniLM-L6-v2",
|
||||
@@ -482,8 +483,8 @@ class FunctionVersion(_RemoteValue):
|
||||
|
||||
``source_digest`` identifies the registered source artifact, independently
|
||||
of the built ``image``. Compare it with
|
||||
``udf_def.registration_request.artifact.digest`` to check whether a local
|
||||
[UdfDefinition][lancedb.functions.UdfDefinition] has the same source.
|
||||
``definition.registration_request.artifact.digest`` to check whether a local
|
||||
[FunctionDefinition][lancedb.functions.FunctionDefinition] has the same source.
|
||||
It is ``None`` when the server does not provide a source digest.
|
||||
|
||||
``gpu_class`` is the operator-defined GPU pool fixed into this Function
|
||||
@@ -639,7 +640,7 @@ def _initialization_value(name: str, value: Any) -> Any:
|
||||
|
||||
|
||||
class FunctionRegistrationRequest(_RemoteValue):
|
||||
"""Stable remote registration envelope produced by :func:`udf`.
|
||||
"""Stable remote registration envelope produced by :func:`function`.
|
||||
|
||||
Credential values deliberately have no field here. The only secret-shaped
|
||||
thing a client sends is ``secret_bindings``: the name of a Secret the
|
||||
@@ -1375,11 +1376,15 @@ def _infer_signature(
|
||||
return FunctionSignature(inputs=tuple(inputs), output=output)
|
||||
|
||||
|
||||
def _is_udf_decorator(node: ast.expr) -> bool:
|
||||
# Both spellings strip from packaged source; `udf` is the deprecated alias.
|
||||
_DECORATOR_NAMES = frozenset({"function", "udf"})
|
||||
|
||||
|
||||
def _is_function_decorator(node: ast.expr) -> bool:
|
||||
if isinstance(node, ast.Call):
|
||||
node = node.func
|
||||
return (isinstance(node, ast.Name) and node.id == "udf") or (
|
||||
isinstance(node, ast.Attribute) and node.attr == "udf"
|
||||
return (isinstance(node, ast.Name) and node.id in _DECORATOR_NAMES) or (
|
||||
isinstance(node, ast.Attribute) and node.attr in _DECORATOR_NAMES
|
||||
)
|
||||
|
||||
|
||||
@@ -1444,7 +1449,7 @@ def _module_references(module_source: str) -> set[str]:
|
||||
visit(child, found)
|
||||
|
||||
found: set[str] = set()
|
||||
module = symtable.symtable(module_source, "<udf>", "exec")
|
||||
module = symtable.symtable(module_source, "<function>", "exec")
|
||||
for symbol in module.get_symbols():
|
||||
if (
|
||||
symbol.is_referenced()
|
||||
@@ -1517,7 +1522,7 @@ def _definition_node(
|
||||
)
|
||||
except (OSError, TypeError) as error:
|
||||
raise ValueError(
|
||||
f"@udf requires inspectable Python source for {target.__qualname__!r}"
|
||||
f"@function requires inspectable Python source for {target.__qualname__!r}"
|
||||
) from error
|
||||
module = ast.parse(source)
|
||||
is_class = inspect.isclass(target)
|
||||
@@ -1532,15 +1537,16 @@ def _definition_node(
|
||||
]
|
||||
if len(definitions) != 1 or isinstance(definitions[0], ast.AsyncFunctionDef):
|
||||
raise ValueError(
|
||||
"@udf source must contain exactly one "
|
||||
"@function source must contain exactly one "
|
||||
+ ("class" if is_class else "synchronous function")
|
||||
)
|
||||
definition = definitions[0]
|
||||
if entry:
|
||||
if any(
|
||||
not _is_udf_decorator(decorator) for decorator in definition.decorator_list
|
||||
not _is_function_decorator(decorator)
|
||||
for decorator in definition.decorator_list
|
||||
):
|
||||
raise ValueError("@udf cannot package additional Python decorators")
|
||||
raise ValueError("@function cannot package additional Python decorators")
|
||||
definition.decorator_list = []
|
||||
return definition
|
||||
|
||||
@@ -1551,7 +1557,7 @@ def _definition_namespace(target: Any) -> Mapping[str, Any]:
|
||||
module = sys.modules.get(target.__module__)
|
||||
if module is None:
|
||||
raise ValueError(
|
||||
f"@udf cannot resolve the module of {target.__qualname__!r}"
|
||||
f"@function cannot resolve the module of {target.__qualname__!r}"
|
||||
)
|
||||
namespace = vars(module)
|
||||
environment = namespace.get("__builtins__", builtins)
|
||||
@@ -1566,7 +1572,7 @@ def _definition_namespace(target: Any) -> Mapping[str, Any]:
|
||||
# The artifact runs under the standard builtins; only the exact mapping is
|
||||
# provably equivalent (a subclass or copy can change lookups and hooks).
|
||||
if environment is not vars(builtins):
|
||||
raise ValueError("@udf cannot package a non-standard builtins environment")
|
||||
raise ValueError("@function cannot package a non-standard builtins environment")
|
||||
return namespace
|
||||
|
||||
|
||||
@@ -1580,7 +1586,7 @@ def _is_self_reference(target: Any, namespace: Mapping[str, Any], name: str) ->
|
||||
return True
|
||||
# The decorator's own result is the one wrapper known to behave like
|
||||
# `target`; any other binding may differ from the packaged definition.
|
||||
return type(bound) is UdfDefinition and bound._function is target
|
||||
return type(bound) is FunctionDefinition and bound._function is target
|
||||
|
||||
|
||||
def _packaged_by_source(value: Any) -> bool:
|
||||
@@ -1593,7 +1599,7 @@ def _packaged_by_source(value: Any) -> bool:
|
||||
qualname = value.__qualname__
|
||||
if "<" in qualname or "." in qualname:
|
||||
raise ValueError(
|
||||
f"@udf cannot package {qualname!r}: only module-level functions and "
|
||||
f"@function cannot package {qualname!r}: only module-level functions and "
|
||||
"classes travel with a Function, not lambdas, closures, or nested "
|
||||
"definitions"
|
||||
)
|
||||
@@ -1689,7 +1695,7 @@ class _SourcePackager:
|
||||
definition = _definition_node(target, entry=entry)
|
||||
if not inspect.isclass(target) and inspect.getclosurevars(target).nonlocals:
|
||||
raise ValueError(
|
||||
"@udf cannot package functions that capture closure values"
|
||||
"@function cannot package functions that capture closure values"
|
||||
)
|
||||
namespace = _definition_namespace(target)
|
||||
reader_name = (
|
||||
@@ -1702,7 +1708,7 @@ class _SourcePackager:
|
||||
dynamic = _namespace_acquisition(definition, references)
|
||||
if dynamic:
|
||||
raise ValueError(
|
||||
f"@udf cannot package dynamic namespace access: {dynamic!r}"
|
||||
f"@function cannot package dynamic namespace access: {dynamic!r}"
|
||||
)
|
||||
self._warn_unshipped_direct_imports(definition)
|
||||
self._packaged.add(id(target))
|
||||
@@ -1714,9 +1720,9 @@ class _SourcePackager:
|
||||
if name == target.__name__:
|
||||
if not _is_self_reference(target, namespace, name):
|
||||
raise ValueError(
|
||||
f"@udf cannot package {name!r}: the module binds that name "
|
||||
"to another value, which the artifact's own definition "
|
||||
"would shadow"
|
||||
f"@function cannot package {name!r}: the module binds that "
|
||||
"name to another value, which the artifact's own "
|
||||
"definition would shadow"
|
||||
)
|
||||
continue
|
||||
if name in namespace:
|
||||
@@ -1725,7 +1731,7 @@ class _SourcePackager:
|
||||
unresolved.append(name)
|
||||
if unresolved:
|
||||
raise ValueError(
|
||||
f"@udf source contains unresolved global names: {unresolved!r}"
|
||||
f"@function source contains unresolved global names: {unresolved!r}"
|
||||
)
|
||||
return source
|
||||
|
||||
@@ -1771,7 +1777,7 @@ class _SourcePackager:
|
||||
claimed = self._names.setdefault(name, meaning)
|
||||
if claimed != meaning:
|
||||
raise ValueError(
|
||||
f"@udf cannot package {name!r}: packaged definitions bind that "
|
||||
f"@function cannot package {name!r}: packaged definitions bind that "
|
||||
"name to different values"
|
||||
)
|
||||
|
||||
@@ -1779,8 +1785,8 @@ class _SourcePackager:
|
||||
if _packaged_by_source(value):
|
||||
if value.__name__ != name:
|
||||
raise ValueError(
|
||||
f"@udf packages {value.__qualname__!r} by source, so it must be "
|
||||
f"referenced by its own name, not {name!r}"
|
||||
f"@function packages {value.__qualname__!r} by source, so it "
|
||||
f"must be referenced by its own name, not {name!r}"
|
||||
)
|
||||
if id(value) not in self._packaged:
|
||||
self._definitions.append(self._definition(value, entry=False))
|
||||
@@ -1799,7 +1805,8 @@ class _SourcePackager:
|
||||
if isinstance(value, types.ModuleType):
|
||||
if value.__name__.split(".")[0] in _NAMESPACE_MODULES:
|
||||
raise ValueError(
|
||||
f"@udf cannot package dynamic namespace access: {value.__name__!r}"
|
||||
"@function cannot package dynamic namespace access: "
|
||||
f"{value.__name__!r}"
|
||||
)
|
||||
try:
|
||||
imported = importlib.import_module(value.__name__)
|
||||
@@ -1854,7 +1861,7 @@ class _SourcePackager:
|
||||
return
|
||||
self._warned_imports.add(top)
|
||||
warnings.warn(
|
||||
f"@udf source imports {module_name!r} from {locations[0]}, a local "
|
||||
f"@function source imports {module_name!r} from {locations[0]}, a local "
|
||||
"module the Function's environment may not be able to import; ship "
|
||||
f"it with code=[{top}] or declare the package that provides it in "
|
||||
"pip/conda",
|
||||
@@ -2070,10 +2077,10 @@ def _class_call(cls: type) -> Callable[..., Any]:
|
||||
return call
|
||||
|
||||
|
||||
class UdfDefinition:
|
||||
class FunctionDefinition:
|
||||
"""A Python callable prepared for remote Function registration.
|
||||
|
||||
Instances are created with :func:`udf`. Calling an instance is ordinary
|
||||
Instances are created with :func:`function`. Calling an instance is ordinary
|
||||
local Python: a decorated function is called directly, while a decorated
|
||||
class is constructed using its initialization fields and the returned
|
||||
instance is called with each input. Reuse the instance and call its
|
||||
@@ -2114,7 +2121,9 @@ class UdfDefinition:
|
||||
if not is_class and (
|
||||
not inspect.isfunction(function) or inspect.iscoroutinefunction(function)
|
||||
):
|
||||
raise TypeError("@udf requires a synchronous Python function or a class")
|
||||
raise TypeError(
|
||||
"@function requires a synchronous Python function or a class"
|
||||
)
|
||||
function_name = name or function.__name__
|
||||
if not _FUNCTION_NAME.fullmatch(function_name):
|
||||
raise ValueError(f"invalid Function name: {function_name!r}")
|
||||
@@ -2181,7 +2190,7 @@ class UdfDefinition:
|
||||
if is_class:
|
||||
if "<" in function.__qualname__ or "." in function.__qualname__:
|
||||
raise ValueError(
|
||||
"@udf classes must be defined at module level; nested classes "
|
||||
"@function classes must be defined at module level; nested classes "
|
||||
"would capture their enclosing scope"
|
||||
)
|
||||
signature = _infer_signature(
|
||||
@@ -2327,12 +2336,12 @@ class UdfDefinition:
|
||||
|
||||
|
||||
@overload
|
||||
def udf(function: Callable[..., Any]) -> UdfDefinition: ...
|
||||
def function(target: Callable[..., Any]) -> FunctionDefinition: ...
|
||||
|
||||
|
||||
@overload
|
||||
def udf(
|
||||
function: None = None,
|
||||
def function(
|
||||
target: None = None,
|
||||
*,
|
||||
name: Optional[str] = None,
|
||||
input_schema: Optional[pa.Schema] = None,
|
||||
@@ -2350,11 +2359,11 @@ def udf(
|
||||
image: Optional[str] = None,
|
||||
code: Sequence[types.ModuleType] = (),
|
||||
models: Sequence[HuggingFaceModel] = (),
|
||||
) -> Callable[[Callable[..., Any]], UdfDefinition]: ...
|
||||
) -> Callable[[Callable[..., Any]], FunctionDefinition]: ...
|
||||
|
||||
|
||||
def udf(
|
||||
function: Optional[Callable[..., Any]] = None,
|
||||
def function(
|
||||
target: Optional[Callable[..., Any]] = None,
|
||||
*,
|
||||
name: Optional[str] = None,
|
||||
input_schema: Optional[pa.Schema] = None,
|
||||
@@ -2511,7 +2520,7 @@ def udf(
|
||||
files are not copied. To use a package that includes required data
|
||||
resources, install it with ``pip=`` or ``conda=`` and remove that
|
||||
package from ``code=`` so the copied Python package cannot shadow the
|
||||
installed distribution. ``@udf`` warns when it skips files from a
|
||||
installed distribution. ``@function`` warns when it skips files from a
|
||||
package in ``code=``.
|
||||
models : sequence of HuggingFaceModel, optional
|
||||
Immutable Hugging Face repository snapshots prepared in the worker
|
||||
@@ -2523,7 +2532,7 @@ def udf(
|
||||
|
||||
Returns
|
||||
-------
|
||||
UdfDefinition
|
||||
FunctionDefinition
|
||||
A callable definition accepted by
|
||||
:meth:`lancedb.db.DBConnection.create_function`,
|
||||
:meth:`lancedb.db.AsyncConnection.create_function_async` and
|
||||
@@ -2531,14 +2540,14 @@ def udf(
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> from lancedb import udf
|
||||
>>> @udf(pip=["numpy==2.2.0"])
|
||||
>>> from lancedb import function
|
||||
>>> @function(pip=["numpy==2.2.0"])
|
||||
... def score(value: float) -> float:
|
||||
... return value * 2
|
||||
>>> score(1.5)
|
||||
3.0
|
||||
>>> import pyarrow as pa
|
||||
>>> @udf(
|
||||
>>> @function(
|
||||
... input_schema=pa.schema([pa.field("value", pa.int64(), nullable=False)]),
|
||||
... output_schema=pa.int64(),
|
||||
... )
|
||||
@@ -2546,14 +2555,14 @@ def udf(
|
||||
... return value
|
||||
>>> batch_identity(pa.array([1, 2, 3])).to_pylist()
|
||||
[1, 2, 3]
|
||||
>>> @udf(pip=["cupy-cuda12x"], gpu=True, gpu_class="a10g")
|
||||
>>> @function(pip=["cupy-cuda12x"], gpu=True, gpu_class="a10g")
|
||||
... def gpu_score(value: int) -> int:
|
||||
... return value * 2
|
||||
>>> gpu_score.registration_request.runtime.gpu
|
||||
True
|
||||
>>> gpu_score.registration_request.runtime.gpu_class
|
||||
'a10g'
|
||||
>>> @udf
|
||||
>>> @function
|
||||
... class scale:
|
||||
... def __init__(self, factor: float, offset: float = 0.0):
|
||||
... self.factor, self.offset = factor, offset
|
||||
@@ -2570,7 +2579,7 @@ def udf(
|
||||
>>> [field.name for field in scale.registration_request.signature.initialization]
|
||||
['factor', 'offset']
|
||||
>>> import lancedb
|
||||
>>> @udf(
|
||||
>>> @function(
|
||||
... input_schema=pa.schema([lancedb.blob("image", nullable=False)]),
|
||||
... output_schema=lancedb.blob("copy", nullable=False),
|
||||
... )
|
||||
@@ -2611,8 +2620,8 @@ def udf(
|
||||
``blob_mode="bytes"`` cannot fetch payloads inside lists.
|
||||
"""
|
||||
|
||||
def decorate(target: Any) -> UdfDefinition:
|
||||
return UdfDefinition(
|
||||
def decorate(target: Any) -> FunctionDefinition:
|
||||
return FunctionDefinition(
|
||||
target,
|
||||
name=name,
|
||||
input_schema=input_schema,
|
||||
@@ -2632,9 +2641,9 @@ def udf(
|
||||
models=tuple(models),
|
||||
)
|
||||
|
||||
if function is None:
|
||||
if target is None:
|
||||
return decorate
|
||||
return decorate(function)
|
||||
return decorate(target)
|
||||
|
||||
|
||||
__all__ = [
|
||||
@@ -2660,6 +2669,31 @@ __all__ = [
|
||||
"PythonAdapterSpec",
|
||||
"PythonRuntimeSpec",
|
||||
"RefreshColumnResult",
|
||||
"UdfDefinition",
|
||||
"udf",
|
||||
"FunctionDefinition",
|
||||
]
|
||||
|
||||
|
||||
def udf(*args: Any, **kwargs: Any) -> Any:
|
||||
"""Deprecated alias of :func:`function`."""
|
||||
warnings.warn(
|
||||
"lancedb.udf is deprecated; use lancedb.function",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
return function(*args, **kwargs)
|
||||
|
||||
|
||||
UDF_DEFINITION_DEPRECATION = (
|
||||
"lancedb.UdfDefinition is deprecated; use lancedb.FunctionDefinition"
|
||||
)
|
||||
|
||||
|
||||
def __getattr__(name: str) -> Any:
|
||||
if name == "UdfDefinition":
|
||||
warnings.warn(UDF_DEFINITION_DEPRECATION, DeprecationWarning, stacklevel=2)
|
||||
return FunctionDefinition
|
||||
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
UdfDefinition = FunctionDefinition
|
||||
|
||||
@@ -38,7 +38,7 @@ import pyarrow as pa
|
||||
from ..common import DATA
|
||||
from lancedb.listing import Listing, NamespaceListing, TableListing
|
||||
from ..db import DBConnection, LOOP
|
||||
from ..functions import FunctionVersion, UdfDefinition
|
||||
from ..functions import FunctionVersion, FunctionDefinition
|
||||
from ..graph import EdgeTable, NodeTable, PropertyGraph
|
||||
from ..job import AsyncJob, Job
|
||||
from ..sql import Query as SqlQuery
|
||||
@@ -968,7 +968,7 @@ class RemoteDBConnection(DBConnection):
|
||||
@override
|
||||
def create_function_async(
|
||||
self,
|
||||
definition: UdfDefinition,
|
||||
definition: FunctionDefinition,
|
||||
*,
|
||||
secrets: Optional[Sequence[EnvVarSecret]] = None,
|
||||
namespace_path: Optional[List[str]] = None,
|
||||
|
||||
@@ -10,7 +10,7 @@ import lancedb
|
||||
import numpy as np
|
||||
import pyarrow as pa
|
||||
import pytest
|
||||
from lancedb import LanceDBConnection, udf
|
||||
from lancedb import LanceDBConnection, function
|
||||
from lancedb.index import FTS
|
||||
|
||||
# TODO: setup integ test mark and script
|
||||
@@ -98,7 +98,7 @@ def test_function_execution_controls_through_sql_and_materialized_view():
|
||||
view_name = f"batch_probe_view_{suffix}"
|
||||
registered_version = None
|
||||
|
||||
@udf(
|
||||
@function(
|
||||
name=function_name,
|
||||
input_schema=pa.schema([pa.field("value", pa.int64(), nullable=False)]),
|
||||
output_schema=pa.int64(),
|
||||
|
||||
@@ -32,11 +32,11 @@ from lancedb.functions import (
|
||||
PythonRuntimeSpec,
|
||||
SecretBinding,
|
||||
SecretReference,
|
||||
UdfDefinition,
|
||||
FunctionDefinition,
|
||||
_canonical_arrow_type,
|
||||
_exact_arrow_type,
|
||||
_GRAMMAR_PRIMITIVES,
|
||||
udf,
|
||||
function,
|
||||
)
|
||||
from lancedb.secrets import EnvVarSecret
|
||||
from lancedb.remote.db import RemoteDBConnection
|
||||
@@ -62,7 +62,7 @@ FUNCTION_VERSION = json.loads(
|
||||
)["version"]
|
||||
|
||||
|
||||
@udf(
|
||||
@function(
|
||||
pip=["numpy>=2"],
|
||||
env={"MODE": "test"},
|
||||
python_version="3.12",
|
||||
@@ -71,7 +71,7 @@ def normalize_score(value: float) -> float:
|
||||
return value / 100.0
|
||||
|
||||
|
||||
@udf(
|
||||
@function(
|
||||
pip=["openai==3.7.0"],
|
||||
env={"MODE": "test"},
|
||||
python_version="3.12",
|
||||
@@ -80,8 +80,8 @@ def analyze_caption(caption: str) -> str:
|
||||
return caption.strip()
|
||||
|
||||
|
||||
def test_scalar_udf_matches_shared_registration_golden_and_remains_callable():
|
||||
assert isinstance(normalize_score, UdfDefinition)
|
||||
def test_scalar_function_matches_shared_registration_golden_and_remains_callable():
|
||||
assert isinstance(normalize_score, FunctionDefinition)
|
||||
assert normalize_score(25.0) == 0.25
|
||||
assert (
|
||||
normalize_score.registration_request.to_canonical_json()
|
||||
@@ -96,13 +96,13 @@ def test_scalar_udf_matches_shared_registration_golden_and_remains_callable():
|
||||
}
|
||||
|
||||
|
||||
def test_udf_declares_pinned_hugging_face_models():
|
||||
def test_function_declares_pinned_hugging_face_models():
|
||||
model = HuggingFaceModel(
|
||||
repo="sentence-transformers/all-MiniLM-L6-v2",
|
||||
revision="1110a243fdf4706b3f48f1d95db1a4f5529b4d41",
|
||||
)
|
||||
|
||||
@udf(pip=["sentence-transformers==3.3.1"], models=[model])
|
||||
@function(pip=["sentence-transformers==3.3.1"], models=[model])
|
||||
def embed_text(value: str) -> list[float]:
|
||||
return [1.0]
|
||||
|
||||
@@ -117,19 +117,19 @@ def test_udf_declares_pinned_hugging_face_models():
|
||||
assert "models" not in undeclared
|
||||
|
||||
with pytest.raises(ValueError, match="once"):
|
||||
udf(name="duplicate_model", models=[model, model])(lambda value: value)
|
||||
function(name="duplicate_model", models=[model, model])(lambda value: value)
|
||||
with pytest.raises(ValueError, match="40-character"):
|
||||
HuggingFaceModel(repo="owner/model", revision="main")
|
||||
with pytest.raises(ValueError, match="reserve"):
|
||||
udf(
|
||||
function(
|
||||
name="reserved_model_env",
|
||||
models=[model],
|
||||
env={"HF_HOME": "/tmp/model"},
|
||||
)(lambda value: value)
|
||||
|
||||
|
||||
def test_udf_execution_controls_match_shared_registration_golden():
|
||||
controlled = udf(
|
||||
def test_function_execution_controls_match_shared_registration_golden():
|
||||
controlled = function(
|
||||
pip=["numpy>=2"],
|
||||
env={"MODE": "test"},
|
||||
python_version="3.12",
|
||||
@@ -144,8 +144,8 @@ def test_udf_execution_controls_match_shared_registration_golden():
|
||||
)
|
||||
|
||||
|
||||
def test_udf_execution_controls_apply_to_vectorized_functions():
|
||||
@udf(
|
||||
def test_function_execution_controls_apply_to_vectorized_functions():
|
||||
@function(
|
||||
input_schema=pa.schema([pa.field("value", pa.int64(), nullable=False)]),
|
||||
output_schema=pa.int64(),
|
||||
batch_size=3,
|
||||
@@ -171,28 +171,28 @@ def test_udf_execution_controls_apply_to_vectorized_functions():
|
||||
({"row_timeout_seconds": True}, "row_timeout_seconds"),
|
||||
],
|
||||
)
|
||||
def test_udf_execution_controls_require_their_positive_ranges(options, field):
|
||||
def test_function_execution_controls_require_their_positive_ranges(options, field):
|
||||
def identity(value: int) -> int:
|
||||
return value
|
||||
|
||||
with pytest.raises(ValueError, match=field):
|
||||
udf(**options)(identity)
|
||||
function(**options)(identity)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"name", ["examples.image_info", "image-info", "image_info.", "image_info-"]
|
||||
)
|
||||
def test_udf_rejects_function_names_with_periods_or_hyphens(name):
|
||||
def test_function_rejects_function_names_with_periods_or_hyphens(name):
|
||||
with pytest.raises(ValueError, match="invalid Function name"):
|
||||
|
||||
@udf(name=name)
|
||||
@function(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 test_function_accepts_function_names_with_letters_digits_and_underscores(name):
|
||||
@function(name=name)
|
||||
def image_info(value: int) -> int:
|
||||
return value
|
||||
|
||||
@@ -200,7 +200,7 @@ def test_udf_accepts_function_names_with_letters_digits_and_underscores(name):
|
||||
assert image_info(1) == 1
|
||||
|
||||
|
||||
def test_secret_bound_udf_matches_its_shared_registration_golden():
|
||||
def test_secret_bound_function_matches_its_shared_registration_golden():
|
||||
assert analyze_caption(" hello ") == "hello"
|
||||
bound = analyze_caption.bind_secrets(
|
||||
[EnvVarSecret(secret_name="openai-prod", env_variable="OPENAI_API_KEY")]
|
||||
@@ -446,15 +446,15 @@ def test_a_secret_name_admits_what_a_namespace_name_does():
|
||||
)
|
||||
|
||||
|
||||
def _main_udf_source(
|
||||
def _main_function_source(
|
||||
*, threshold: int = 20, input_annotation: str = "int", comparison: str = ">="
|
||||
) -> str:
|
||||
return (
|
||||
"from __future__ import annotations\n"
|
||||
"from lancedb.functions import udf\n"
|
||||
"from lancedb.functions import function\n"
|
||||
f"THRESHOLD = {threshold}\n"
|
||||
"\n"
|
||||
"@udf\n"
|
||||
"@function\n"
|
||||
f"def label(value: {input_annotation}) -> str:\n"
|
||||
f" return 'big' if value {comparison} THRESHOLD else 'small'\n"
|
||||
"\n"
|
||||
@@ -463,7 +463,7 @@ def _main_udf_source(
|
||||
)
|
||||
|
||||
|
||||
def _run_main_udf(path: Path, source: str) -> dict:
|
||||
def _run_main_function(path: Path, source: str) -> dict:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(source)
|
||||
result = subprocess.run(
|
||||
@@ -475,15 +475,15 @@ def _run_main_udf(path: Path, source: str) -> dict:
|
||||
return json.loads(result.stdout)
|
||||
|
||||
|
||||
def test_main_udf_registration_identity_is_stable_across_processes_and_paths(
|
||||
def test_main_function_registration_identity_is_stable_across_processes_and_paths(
|
||||
tmp_path,
|
||||
):
|
||||
source = _main_udf_source()
|
||||
source = _main_function_source()
|
||||
original_path = tmp_path / "original" / "job.py"
|
||||
moved_path = tmp_path / "moved" / "renamed_job.py"
|
||||
|
||||
original_runs = [_run_main_udf(original_path, source) for _ in range(2)]
|
||||
moved_run = _run_main_udf(moved_path, source)
|
||||
original_runs = [_run_main_function(original_path, source) for _ in range(2)]
|
||||
moved_run = _run_main_function(moved_path, source)
|
||||
|
||||
assert len({run["artifact"]["digest"] for run in [*original_runs, moved_run]}) == 1
|
||||
assert all(
|
||||
@@ -492,15 +492,15 @@ def test_main_udf_registration_identity_is_stable_across_processes_and_paths(
|
||||
)
|
||||
assert original_runs[0] == original_runs[1] == moved_run
|
||||
|
||||
body_change = _run_main_udf(
|
||||
tmp_path / "changes" / "body.py", _main_udf_source(comparison=">")
|
||||
body_change = _run_main_function(
|
||||
tmp_path / "changes" / "body.py", _main_function_source(comparison=">")
|
||||
)
|
||||
global_change = _run_main_udf(
|
||||
tmp_path / "changes" / "global.py", _main_udf_source(threshold=21)
|
||||
global_change = _run_main_function(
|
||||
tmp_path / "changes" / "global.py", _main_function_source(threshold=21)
|
||||
)
|
||||
annotation_change = _run_main_udf(
|
||||
annotation_change = _run_main_function(
|
||||
tmp_path / "changes" / "annotation.py",
|
||||
_main_udf_source(input_annotation="float"),
|
||||
_main_function_source(input_annotation="float"),
|
||||
)
|
||||
|
||||
baseline = original_runs[0]
|
||||
@@ -524,11 +524,13 @@ def _run_packaged(definition, *args):
|
||||
"""Execute the shipped artifact in a fresh namespace, as a worker would."""
|
||||
source = base64.b64decode(definition.registration_request.artifact.content.data)
|
||||
namespace: dict = {}
|
||||
exec(compile(source, "<udf>", "exec"), namespace)
|
||||
exec(compile(source, "<function>", "exec"), namespace)
|
||||
return namespace[definition.registration_request.artifact.entrypoint](*args)
|
||||
|
||||
|
||||
def _execute_registered_array_udf_v1(request: dict, batch: pa.RecordBatch) -> pa.Array:
|
||||
def _execute_registered_array_function_v1(
|
||||
request: dict, batch: pa.RecordBatch
|
||||
) -> pa.Array:
|
||||
"""Exercise adapter-v1 annotation dispatch over a registered artifact."""
|
||||
assert request["artifact"]["adapter"] == {
|
||||
"kind": "scalar_to_arrow_batch",
|
||||
@@ -542,7 +544,7 @@ def _execute_registered_array_udf_v1(request: dict, batch: pa.RecordBatch) -> pa
|
||||
"RecordBatch": pa.RecordBatch,
|
||||
"typing": typing,
|
||||
}
|
||||
exec(compile(source, "<registered-udf>", "exec"), namespace)
|
||||
exec(compile(source, "<registered-function>", "exec"), namespace)
|
||||
function = namespace[request["artifact"]["entrypoint"]]
|
||||
parameters = tuple(inspect.signature(function).parameters.values())
|
||||
annotations = typing.get_type_hints(function)
|
||||
@@ -555,8 +557,8 @@ def _execute_registered_array_udf_v1(request: dict, batch: pa.RecordBatch) -> pa
|
||||
return result
|
||||
|
||||
|
||||
def test_udf_conda_environment():
|
||||
@udf(conda=["scipy", "numpy"], conda_channels=["conda-forge", "defaults"])
|
||||
def test_function_conda_environment():
|
||||
@function(conda=["scipy", "numpy"], conda_channels=["conda-forge", "defaults"])
|
||||
def halve(value: float) -> float:
|
||||
return value / 2
|
||||
|
||||
@@ -570,13 +572,13 @@ def test_udf_conda_environment():
|
||||
assert "channels" not in pip_request["runtime"]["environment"]
|
||||
|
||||
with pytest.raises(ValueError, match="not both"):
|
||||
udf(name="both", pip=["numpy"], conda=["numpy"])(lambda value: value)
|
||||
function(name="both", pip=["numpy"], conda=["numpy"])(lambda value: value)
|
||||
with pytest.raises(ValueError, match="requires conda"):
|
||||
udf(name="channels", conda_channels=["conda-forge"])(lambda value: value)
|
||||
function(name="channels", conda_channels=["conda-forge"])(lambda value: value)
|
||||
|
||||
|
||||
def test_udf_apt_system_packages():
|
||||
@udf(pip=["torchcodec"], apt=["libavutil58", "libavcodec60", "libavutil58"])
|
||||
def test_function_apt_system_packages():
|
||||
@function(pip=["torchcodec"], apt=["libavutil58", "libavcodec60", "libavutil58"])
|
||||
def decode(value: float) -> float:
|
||||
return value
|
||||
|
||||
@@ -587,7 +589,7 @@ def test_udf_apt_system_packages():
|
||||
"system_packages": ["libavcodec60", "libavutil58"],
|
||||
}
|
||||
|
||||
@udf(conda=["numpy"], apt=["libsndfile1"])
|
||||
@function(conda=["numpy"], apt=["libsndfile1"])
|
||||
def conda_decode(value: float) -> float:
|
||||
return value
|
||||
|
||||
@@ -596,14 +598,14 @@ def test_udf_apt_system_packages():
|
||||
pip_request = json.loads(normalize_score.registration_request.to_canonical_json())
|
||||
assert "system_packages" not in pip_request["runtime"]["environment"]
|
||||
with pytest.raises(ValueError, match="trimmed"):
|
||||
udf(name="padded", apt=[" libsndfile1"])(lambda value: value)
|
||||
function(name="padded", apt=[" libsndfile1"])(lambda value: value)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("packages", [{"pip": ["numpy"]}, {"conda": ["numpy"]}])
|
||||
def test_udf_image(packages):
|
||||
def test_function_image(packages):
|
||||
image = "ghcr.io/acme/embedder@sha256:" + "a" * 64
|
||||
|
||||
@udf(image=image, **packages)
|
||||
@function(image=image, **packages)
|
||||
def embed(value: float) -> float:
|
||||
return value
|
||||
|
||||
@@ -616,13 +618,13 @@ def test_udf_image(packages):
|
||||
@pytest.mark.parametrize(
|
||||
"image", ["ghcr.io/acme/embedder:latest", "embedder@sha256:" + "a" * 64, ""]
|
||||
)
|
||||
def test_udf_image_requires_digest_reference(image):
|
||||
def test_function_image_requires_digest_reference(image):
|
||||
with pytest.raises(ValueError, match="sha256"):
|
||||
udf(name="pinned", image=image)(lambda value: value)
|
||||
function(name="pinned", image=image)(lambda value: value)
|
||||
|
||||
|
||||
def test_udf_gpu_marker_uses_gpu_runtime():
|
||||
@udf(pip=["cupy-cuda12x"], gpu=True, gpu_class="a10g")
|
||||
def test_function_gpu_marker_uses_gpu_runtime():
|
||||
@function(pip=["cupy-cuda12x"], gpu=True, gpu_class="a10g")
|
||||
def double_on_gpu(value: int) -> int:
|
||||
return value * 2
|
||||
|
||||
@@ -631,7 +633,7 @@ def test_udf_gpu_marker_uses_gpu_runtime():
|
||||
assert request["runtime"]["gpu"] is True
|
||||
assert request["runtime"]["gpu_class"] == "a10g"
|
||||
|
||||
@udf(pip=["pyarrow"])
|
||||
@function(pip=["pyarrow"])
|
||||
def cpu_function(value: int) -> int:
|
||||
return value
|
||||
|
||||
@@ -647,12 +649,12 @@ def test_udf_gpu_marker_uses_gpu_runtime():
|
||||
|
||||
for invalid in [None, 0, 1, -1, 1.5, "", "true", "1", "H100"]:
|
||||
with pytest.raises(ValueError, match="gpu must be a boolean"):
|
||||
udf(name="invalid_gpu", gpu=invalid)(identity)
|
||||
function(name="invalid_gpu", gpu=invalid)(identity)
|
||||
for invalid in ["H100", "a_b", "-a10g", "a10g-", "a" * 64]:
|
||||
with pytest.raises(ValueError, match="gpu_class"):
|
||||
udf(name="invalid_gpu_class", gpu=True, gpu_class=invalid)(identity)
|
||||
function(name="invalid_gpu_class", gpu=True, gpu_class=invalid)(identity)
|
||||
with pytest.raises(ValueError, match="requires gpu=True"):
|
||||
udf(name="gpu_class_without_gpu", gpu_class="a10g")(identity)
|
||||
function(name="gpu_class_without_gpu", gpu_class="a10g")(identity)
|
||||
|
||||
base_runtime = {
|
||||
"kind": "python_v2",
|
||||
@@ -692,8 +694,8 @@ def test_unknown_runtime_discards_payload_before_known_field_validation():
|
||||
assert runtime.to_canonical_json() == '{"kind":"python_v3"}'
|
||||
|
||||
|
||||
def test_udf_packages_attribute_access_and_body_imports():
|
||||
@udf
|
||||
def test_function_packages_attribute_access_and_body_imports():
|
||||
@function
|
||||
def word_norm(body: str) -> float:
|
||||
import numpy as np
|
||||
|
||||
@@ -706,14 +708,14 @@ def test_udf_packages_attribute_access_and_body_imports():
|
||||
assert _run_packaged(word_norm, "aa bb") == pytest.approx(8**0.5)
|
||||
|
||||
|
||||
def test_udf_packages_module_globals_and_global_caches():
|
||||
@udf
|
||||
def test_function_packages_module_globals_and_global_caches():
|
||||
@function
|
||||
def label(value: int) -> str:
|
||||
return "big" if value >= THRESHOLD else "small"
|
||||
|
||||
assert _run_packaged(label, 21) == "big"
|
||||
|
||||
@udf
|
||||
@function
|
||||
def cached(value: int) -> int:
|
||||
global _CACHE
|
||||
if _CACHE is None:
|
||||
@@ -723,16 +725,16 @@ def test_udf_packages_module_globals_and_global_caches():
|
||||
assert _run_packaged(cached, 2) == 42
|
||||
|
||||
|
||||
def test_udf_annotations_are_not_runtime_names():
|
||||
@udf
|
||||
def test_function_annotations_are_not_runtime_names():
|
||||
@function
|
||||
def identity(value: date) -> date:
|
||||
return value
|
||||
|
||||
assert _run_packaged(identity, date(2026, 8, 25)) == date(2026, 8, 25)
|
||||
|
||||
|
||||
def test_udf_nested_scopes_resolve_lexically():
|
||||
@udf
|
||||
def test_function_nested_scopes_resolve_lexically():
|
||||
@function
|
||||
def score(value: int) -> int:
|
||||
offset = 2
|
||||
|
||||
@@ -744,7 +746,7 @@ def test_udf_nested_scopes_resolve_lexically():
|
||||
assert _run_packaged(score, 3) == 5
|
||||
|
||||
|
||||
def test_udf_resolves_module_globals_before_builtins(tmp_path):
|
||||
def test_function_resolves_module_globals_before_builtins(tmp_path):
|
||||
module_path = tmp_path / "shadowing_udfs.py"
|
||||
module_path.write_text(
|
||||
"max = 7\n"
|
||||
@@ -765,10 +767,10 @@ def test_udf_resolves_module_globals_before_builtins(tmp_path):
|
||||
spec.loader.exec_module(module)
|
||||
|
||||
# The module's `max = 7` is what the interpreter would use, so it ships.
|
||||
assert _run_packaged(udf(module.uses_literal_shadow), 1) == 8
|
||||
assert _run_packaged(function(module.uses_literal_shadow), 1) == 8
|
||||
# A callable global cannot ship; it must not be silently swapped for the builtin.
|
||||
with pytest.raises(TypeError, match="unsupported global value of type function"):
|
||||
udf(module.uses_callable_shadow)
|
||||
function(module.uses_callable_shadow)
|
||||
|
||||
|
||||
def test_canonical_arrow_type_prefers_the_compact_grammar():
|
||||
@@ -801,8 +803,8 @@ def test_canonical_arrow_type_prefers_the_compact_grammar():
|
||||
_canonical_arrow_type(outside)
|
||||
|
||||
|
||||
def test_udf_nested_annotations_are_postponed_in_the_artifact():
|
||||
@udf
|
||||
def test_function_nested_annotations_are_postponed_in_the_artifact():
|
||||
@function
|
||||
def score(value: int) -> int:
|
||||
def identity(item: date) -> date:
|
||||
return item
|
||||
@@ -813,8 +815,8 @@ def test_udf_nested_annotations_are_postponed_in_the_artifact():
|
||||
assert _run_packaged(score, 3) == 3
|
||||
|
||||
|
||||
def test_udf_ships_globals_the_body_deletes():
|
||||
@udf
|
||||
def test_function_ships_globals_the_body_deletes():
|
||||
@function
|
||||
def clear(value: int) -> int:
|
||||
global _CACHE
|
||||
del _CACHE
|
||||
@@ -823,7 +825,7 @@ def test_udf_ships_globals_the_body_deletes():
|
||||
assert _run_packaged(clear, 3) == 3
|
||||
|
||||
|
||||
def test_udf_rejects_a_module_global_that_does_not_import_as_itself(tmp_path):
|
||||
def test_function_rejects_a_module_global_that_does_not_import_as_itself(tmp_path):
|
||||
module_path = tmp_path / "fake_module_udfs.py"
|
||||
module_path.write_text(
|
||||
"import types\n"
|
||||
@@ -837,10 +839,10 @@ def test_udf_rejects_a_module_global_that_does_not_import_as_itself(tmp_path):
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
with pytest.raises(TypeError, match="does not import as 'numpy'"):
|
||||
udf(module.score)
|
||||
function(module.score)
|
||||
|
||||
|
||||
def test_udf_rejects_a_module_level_namespace_alias(tmp_path):
|
||||
def test_function_rejects_a_module_level_namespace_alias(tmp_path):
|
||||
module_path = tmp_path / "aliasing_udfs.py"
|
||||
module_path.write_text(
|
||||
"import builtins as b\n"
|
||||
@@ -853,7 +855,7 @@ def test_udf_rejects_a_module_level_namespace_alias(tmp_path):
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
with pytest.raises(ValueError, match="dynamic namespace access"):
|
||||
udf(module.score)
|
||||
function(module.score)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -866,7 +868,7 @@ def test_udf_rejects_a_module_level_namespace_alias(tmp_path):
|
||||
"sys.modules[__name__].THRESHOLD",
|
||||
],
|
||||
)
|
||||
def test_udf_rejects_dynamic_namespace_access(access):
|
||||
def test_function_rejects_dynamic_namespace_access(access):
|
||||
namespace: dict = {}
|
||||
exec(
|
||||
f"def score(value: int) -> int:\n return value + {access}\n",
|
||||
@@ -886,9 +888,11 @@ def _package_from_text(source: str, module_globals: dict | None = None):
|
||||
import tempfile
|
||||
|
||||
directory = tempfile.mkdtemp()
|
||||
path = Path(directory) / "generated_udf_module.py"
|
||||
path = Path(directory) / "generated_function_module.py"
|
||||
path.write_text(source)
|
||||
spec = importlib.util.spec_from_file_location(f"generated_udf_{id(source)}", path)
|
||||
spec = importlib.util.spec_from_file_location(
|
||||
f"generated_function_{id(source)}", path
|
||||
)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
if module_globals:
|
||||
module.__dict__.update(module_globals)
|
||||
@@ -898,10 +902,10 @@ def _package_from_text(source: str, module_globals: dict | None = None):
|
||||
for value in vars(module).values()
|
||||
if callable(value) and getattr(value, "__module__", None) == module.__name__
|
||||
]
|
||||
return udf(functions[0])
|
||||
return function(functions[0])
|
||||
|
||||
|
||||
def test_udf_rejects_a_non_standard_builtins_environment():
|
||||
def test_function_rejects_a_non_standard_builtins_environment():
|
||||
def score(value: int) -> int:
|
||||
return len([1]) + value
|
||||
|
||||
@@ -916,7 +920,7 @@ def test_udf_rejects_a_non_standard_builtins_environment():
|
||||
patched.__annotations__ = score.__annotations__
|
||||
assert patched(3) == 102
|
||||
with pytest.raises(ValueError, match="non-standard builtins environment"):
|
||||
udf(patched)
|
||||
function(patched)
|
||||
|
||||
class ReportingDict(dict): # reports standard entries, resolves differently
|
||||
def __missing__(self, key):
|
||||
@@ -928,7 +932,7 @@ def test_udf_rejects_a_non_standard_builtins_environment():
|
||||
disguised.__annotations__ = score.__annotations__
|
||||
assert disguised(3) == 102
|
||||
with pytest.raises(ValueError, match="non-standard builtins environment"):
|
||||
udf(disguised)
|
||||
function(disguised)
|
||||
|
||||
hooked = types.FunctionType(
|
||||
score.__code__,
|
||||
@@ -937,10 +941,10 @@ def test_udf_rejects_a_non_standard_builtins_environment():
|
||||
)
|
||||
hooked.__annotations__ = score.__annotations__
|
||||
with pytest.raises(ValueError, match="non-standard builtins environment"):
|
||||
udf(hooked)
|
||||
function(hooked)
|
||||
|
||||
|
||||
def test_udf_recursion_versus_a_rebound_module_name(tmp_path):
|
||||
def test_function_recursion_versus_a_rebound_module_name(tmp_path):
|
||||
module_path = tmp_path / "rebound_udfs.py"
|
||||
module_path.write_text(
|
||||
"def fact(value: int) -> int:\n"
|
||||
@@ -952,20 +956,20 @@ def test_udf_recursion_versus_a_rebound_module_name(tmp_path):
|
||||
spec = importlib.util.spec_from_file_location("rebound_udfs", module_path)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
assert _run_packaged(udf(module.fact), 5) == 120
|
||||
assert _run_packaged(function(module.fact), 5) == 120
|
||||
raw = module.score
|
||||
module.score = 10
|
||||
with pytest.raises(ValueError, match="binds that name to another value"):
|
||||
udf(raw)
|
||||
function(raw)
|
||||
# A wrapper that merely exposes __wrapped__ is not the function.
|
||||
module.score = functools.wraps(raw)(lambda value: 41)
|
||||
with pytest.raises(ValueError, match="binds that name to another value"):
|
||||
udf(raw)
|
||||
function(raw)
|
||||
# The decorator's own result is; a subclass of it is not.
|
||||
module.fact = udf(module.fact)
|
||||
module.fact = function(module.fact)
|
||||
assert _run_packaged(module.fact, 4) == 24
|
||||
|
||||
class Twisted(UdfDefinition):
|
||||
class Twisted(FunctionDefinition):
|
||||
def __call__(self, *args, **kwargs):
|
||||
return 41
|
||||
|
||||
@@ -980,7 +984,7 @@ def test_udf_recursion_versus_a_rebound_module_name(tmp_path):
|
||||
python_version=None,
|
||||
)
|
||||
with pytest.raises(ValueError, match="binds that name to another value"):
|
||||
udf(raw_fact)
|
||||
function(raw_fact)
|
||||
|
||||
|
||||
def test_canonical_arrow_type_uses_exact_json_for_list_child_properties():
|
||||
@@ -1051,12 +1055,12 @@ def _shadows_missing_in_a_lambda(value: int) -> int:
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"function",
|
||||
"target",
|
||||
[_calls_missing, _shadows_missing_in_a_comprehension, _shadows_missing_in_a_lambda],
|
||||
)
|
||||
def test_udf_rejects_a_truly_unresolved_global(function):
|
||||
def test_function_rejects_a_truly_unresolved_global(target):
|
||||
with pytest.raises(ValueError, match=r"unresolved global names: \['missing'\]"):
|
||||
udf(function)
|
||||
function(target)
|
||||
|
||||
|
||||
def _arrow_type_from_golden(spec: dict) -> pa.DataType:
|
||||
@@ -1111,7 +1115,7 @@ def test_explicit_arrow_schema_is_deterministic():
|
||||
nullable=False,
|
||||
)
|
||||
|
||||
@udf(input_schema=input_schema, output_schema=output_schema)
|
||||
@function(input_schema=input_schema, output_schema=output_schema)
|
||||
def explicit(value):
|
||||
return [value, value, value]
|
||||
|
||||
@@ -1123,7 +1127,7 @@ def test_explicit_arrow_schema_is_deterministic():
|
||||
|
||||
|
||||
def test_blob_fields_use_the_scalar_function_semantic_type():
|
||||
@udf(
|
||||
@function(
|
||||
input_schema=pa.schema([lancedb.blob("image", nullable=False)]),
|
||||
output_schema=lancedb.blob("result", nullable=False),
|
||||
)
|
||||
@@ -1137,12 +1141,12 @@ def test_blob_fields_use_the_scalar_function_semantic_type():
|
||||
|
||||
|
||||
def test_blob_input_access_is_inferred_from_annotations():
|
||||
@udf
|
||||
@function
|
||||
def read_blob(video: BlobReader, timestamp_s: float) -> bytes:
|
||||
video.seek(int(timestamp_s))
|
||||
return video.read(4)
|
||||
|
||||
@udf
|
||||
@function
|
||||
def read_bytes(video: bytes) -> int:
|
||||
return len(video)
|
||||
|
||||
@@ -1159,11 +1163,11 @@ def test_blob_input_access_is_inferred_from_annotations():
|
||||
|
||||
|
||||
def test_nullable_blob_reader_and_explicit_blob_bytes():
|
||||
@udf
|
||||
@function
|
||||
def nullable(video: Optional[BlobReader]) -> int:
|
||||
return 0 if video is None else video.size()
|
||||
|
||||
@udf(
|
||||
@function(
|
||||
input_schema=pa.schema([lancedb.blob("video", nullable=False)]),
|
||||
output_schema=pa.field("size", pa.int64(), nullable=False),
|
||||
)
|
||||
@@ -1183,7 +1187,7 @@ def test_blob_reader_rejects_an_incompatible_explicit_schema():
|
||||
return video.size()
|
||||
|
||||
with pytest.raises(TypeError, match="requires a Blob V2 field"):
|
||||
udf(
|
||||
function(
|
||||
read,
|
||||
input_schema=pa.schema([pa.field("video", pa.int64(), nullable=False)]),
|
||||
output_schema=pa.field("size", pa.int64(), nullable=False),
|
||||
@@ -1191,7 +1195,7 @@ def test_blob_reader_rejects_an_incompatible_explicit_schema():
|
||||
|
||||
|
||||
def test_whole_named_struct_function_can_include_a_blob_result_field():
|
||||
@udf(
|
||||
@function(
|
||||
input_schema=pa.schema([lancedb.blob("image", nullable=False)]),
|
||||
output_schema=pa.field(
|
||||
"payload",
|
||||
@@ -1245,7 +1249,7 @@ def test_struct_blob_signature_fields_preserve_exact_metadata_and_nullability():
|
||||
nullable=False,
|
||||
)
|
||||
|
||||
@udf(input_schema=pa.schema([nested_input]), output_schema=nested_output)
|
||||
@function(input_schema=pa.schema([nested_input]), output_schema=nested_output)
|
||||
def copy_payload(payload):
|
||||
return payload
|
||||
|
||||
@@ -1289,7 +1293,7 @@ def test_struct_blob_signature_supports_multiple_struct_levels():
|
||||
nullable=False,
|
||||
)
|
||||
|
||||
@udf(
|
||||
@function(
|
||||
input_schema=pa.schema([recursive]),
|
||||
output_schema=pa.field("size", pa.int64(), nullable=False),
|
||||
)
|
||||
@@ -1310,7 +1314,7 @@ def test_struct_blob_signature_supports_multiple_struct_levels():
|
||||
],
|
||||
)
|
||||
def test_blob_output_accepts_list_ancestors(data_type):
|
||||
@udf(
|
||||
@function(
|
||||
input_schema=pa.schema([lancedb.blob("video", nullable=False)]),
|
||||
output_schema=pa.field("frames", data_type, nullable=False),
|
||||
)
|
||||
@@ -1335,7 +1339,7 @@ def test_blob_output_accepts_list_ancestors(data_type):
|
||||
def test_blob_output_rejects_map_and_fixed_size_list_ancestors(data_type):
|
||||
with pytest.raises(TypeError, match="nested under collection types"):
|
||||
|
||||
@udf(
|
||||
@function(
|
||||
input_schema=pa.schema([pa.field("id", pa.int64(), nullable=False)]),
|
||||
output_schema=pa.field("frames", data_type, nullable=False),
|
||||
)
|
||||
@@ -1346,7 +1350,7 @@ def test_blob_output_rejects_map_and_fixed_size_list_ancestors(data_type):
|
||||
def test_blob_output_named_struct_accepts_list_ancestor():
|
||||
frames_type = pa.list_(lancedb.blob("item", nullable=False))
|
||||
|
||||
@udf(
|
||||
@function(
|
||||
input_schema=pa.schema([pa.field("id", pa.int64(), nullable=False)]),
|
||||
output_schema=pa.schema(
|
||||
[
|
||||
@@ -1384,7 +1388,7 @@ def test_blob_signature_rejects_collection_ancestors(data_type):
|
||||
match="Blob v2 fields nested under collection types are not supported",
|
||||
):
|
||||
|
||||
@udf(
|
||||
@function(
|
||||
input_schema=pa.schema([pa.field("value", data_type, nullable=False)]),
|
||||
output_schema=pa.field("size", pa.int64(), nullable=False),
|
||||
)
|
||||
@@ -1411,7 +1415,7 @@ def test_blob_signature_rejects_collection_below_a_struct():
|
||||
match="Blob v2 fields nested under collection types are not supported",
|
||||
):
|
||||
|
||||
@udf(
|
||||
@function(
|
||||
input_schema=pa.schema([nested]),
|
||||
output_schema=pa.field("size", pa.int64(), nullable=False),
|
||||
)
|
||||
@@ -1420,7 +1424,7 @@ def test_blob_signature_rejects_collection_below_a_struct():
|
||||
|
||||
|
||||
def test_blob_fields_support_vectorized_pyarrow_arrays():
|
||||
@udf(
|
||||
@function(
|
||||
input_schema=pa.schema([lancedb.blob("image", nullable=True)]),
|
||||
output_schema=lancedb.blob("result", nullable=False),
|
||||
)
|
||||
@@ -1433,7 +1437,7 @@ def test_blob_fields_support_vectorized_pyarrow_arrays():
|
||||
values = pa.array([b"large blob", b"", None], type=pa.large_binary())
|
||||
request = json.loads(copy_blobs.registration_request.to_canonical_json())
|
||||
batch = pa.RecordBatch.from_arrays([values], names=["image"])
|
||||
result = _execute_registered_array_udf_v1(request, batch)
|
||||
result = _execute_registered_array_function_v1(request, batch)
|
||||
assert isinstance(result, pa.LargeBinaryArray)
|
||||
assert result.to_pylist() == [b"large blob", b"", b""]
|
||||
|
||||
@@ -1454,7 +1458,7 @@ def test_blob_fields_support_vectorized_pyarrow_arrays():
|
||||
|
||||
|
||||
def test_documented_blob_array_identity_executes_packaged_artifact():
|
||||
@udf(
|
||||
@function(
|
||||
input_schema=pa.schema([lancedb.blob("image", nullable=False)]),
|
||||
output_schema=lancedb.blob("copy", nullable=False),
|
||||
)
|
||||
@@ -1463,7 +1467,7 @@ def test_documented_blob_array_identity_executes_packaged_artifact():
|
||||
|
||||
values = pa.array([b"large blob", b""], type=pa.large_binary())
|
||||
request = json.loads(copy_blobs.registration_request.to_canonical_json())
|
||||
result = _execute_registered_array_udf_v1(
|
||||
result = _execute_registered_array_function_v1(
|
||||
request,
|
||||
pa.RecordBatch.from_arrays([values], names=["image"]),
|
||||
)
|
||||
@@ -1471,7 +1475,7 @@ def test_documented_blob_array_identity_executes_packaged_artifact():
|
||||
|
||||
|
||||
def test_named_struct_function_can_include_a_blob_result_field():
|
||||
@udf(
|
||||
@function(
|
||||
input_schema=pa.schema([lancedb.blob("image", nullable=False)]),
|
||||
output_schema=pa.schema(
|
||||
[
|
||||
@@ -1492,7 +1496,7 @@ def test_named_struct_function_can_include_a_blob_result_field():
|
||||
|
||||
|
||||
def test_named_struct_function_preserves_nullable_result_fields():
|
||||
@udf(
|
||||
@function(
|
||||
input_schema=pa.schema([pa.field("value", pa.int64(), nullable=False)]),
|
||||
output_schema=pa.schema(
|
||||
[
|
||||
@@ -1510,7 +1514,7 @@ def test_named_struct_function_preserves_nullable_result_fields():
|
||||
("failure_code", False),
|
||||
]
|
||||
|
||||
@udf(
|
||||
@function(
|
||||
input_schema=pa.schema([pa.field("value", pa.int64(), nullable=False)]),
|
||||
output_schema=pa.schema(
|
||||
[
|
||||
@@ -1540,7 +1544,7 @@ def test_metadata_marked_blob_field_uses_the_semantic_type():
|
||||
metadata={"ARROW:extension:name": "lance.blob.v2"},
|
||||
)
|
||||
|
||||
@udf(
|
||||
@function(
|
||||
input_schema=pa.schema([metadata_blob]),
|
||||
output_schema=pa.field("size", pa.int64(), nullable=False),
|
||||
)
|
||||
@@ -1560,7 +1564,7 @@ def test_blob_marker_rejects_invalid_storage_layout():
|
||||
|
||||
with pytest.raises(TypeError, match="requires a supported Blob storage layout"):
|
||||
|
||||
@udf(
|
||||
@function(
|
||||
input_schema=pa.schema([malformed]),
|
||||
output_schema=pa.field("size", pa.int64(), nullable=False),
|
||||
)
|
||||
@@ -1587,7 +1591,7 @@ def test_nested_non_blob_extension_is_not_silently_unwrapped():
|
||||
)
|
||||
with pytest.raises(TypeError, match="unsupported Arrow type"):
|
||||
|
||||
@udf(
|
||||
@function(
|
||||
input_schema=pa.schema([nested]),
|
||||
output_schema=pa.field("result", pa.int64(), nullable=False),
|
||||
)
|
||||
@@ -1599,7 +1603,7 @@ def test_explicit_large_utf8_schemas_use_the_canonical_function_name():
|
||||
input_schema = pa.schema([pa.field("text", pa.large_string(), nullable=True)])
|
||||
output_schema = pa.field("result", pa.large_string(), nullable=False)
|
||||
|
||||
@udf(input_schema=input_schema, output_schema=output_schema)
|
||||
@function(input_schema=input_schema, output_schema=output_schema)
|
||||
def preserve(text):
|
||||
return text
|
||||
|
||||
@@ -1625,7 +1629,7 @@ def test_explicit_large_utf8_schemas_use_the_canonical_function_name():
|
||||
def test_large_binary_schemas_use_the_canonical_function_name():
|
||||
payload = pa.field("data", pa.large_binary(), nullable=True)
|
||||
|
||||
@udf(
|
||||
@function(
|
||||
input_schema=pa.schema([payload]),
|
||||
output_schema=pa.field("result", pa.large_binary(), nullable=False),
|
||||
)
|
||||
@@ -1696,7 +1700,7 @@ def test_nested_struct_output_uses_canonical_exact_json():
|
||||
]
|
||||
)
|
||||
|
||||
@udf(
|
||||
@function(
|
||||
input_schema=pa.schema([pa.field("text", pa.string(), nullable=False)]),
|
||||
output_schema=pa.field("analysis", analysis, nullable=False),
|
||||
)
|
||||
@@ -1746,31 +1750,31 @@ def test_nested_struct_output_uses_canonical_exact_json():
|
||||
def test_annotation_and_explicit_schema_validation_fail_closed():
|
||||
with pytest.raises(TypeError, match="missing Function annotations"):
|
||||
|
||||
@udf
|
||||
@function
|
||||
def missing(value):
|
||||
return value
|
||||
|
||||
with pytest.raises(TypeError, match="unsupported Function annotation"):
|
||||
|
||||
@udf
|
||||
@function
|
||||
def unsupported(value: set[str]) -> str:
|
||||
return ""
|
||||
|
||||
with pytest.raises(ValueError, match="output must be non-nullable"):
|
||||
|
||||
@udf
|
||||
@function
|
||||
def nullable_output(value: int) -> Optional[int]:
|
||||
return value
|
||||
|
||||
with pytest.raises(TypeError, match=r"missing Function annotations: \['return'\]"):
|
||||
|
||||
@udf(input_schema=pa.schema([pa.field("value", pa.int64())]))
|
||||
@function(input_schema=pa.schema([pa.field("value", pa.int64())]))
|
||||
def partial_schema(value):
|
||||
return value
|
||||
|
||||
with pytest.raises(ValueError, match="exactly match callable parameters"):
|
||||
|
||||
@udf(
|
||||
@function(
|
||||
input_schema=pa.schema([pa.field("other", pa.int64())]),
|
||||
output_schema=pa.int64(),
|
||||
)
|
||||
@@ -1779,7 +1783,7 @@ def test_annotation_and_explicit_schema_validation_fail_closed():
|
||||
|
||||
with pytest.raises(ValueError, match="output must be non-nullable"):
|
||||
|
||||
@udf(
|
||||
@function(
|
||||
input_schema=pa.schema([pa.field("value", pa.int64())]),
|
||||
output_schema=pa.field("result", pa.int64(), nullable=True),
|
||||
)
|
||||
@@ -1792,7 +1796,7 @@ def test_annotation_and_explicit_schema_validation_fail_closed():
|
||||
]:
|
||||
with pytest.raises(TypeError, match="unsupported Arrow type"):
|
||||
|
||||
@udf(
|
||||
@function(
|
||||
input_schema=pa.schema([pa.field("value", pa.int64())]),
|
||||
output_schema=pa.schema([invalid_field]),
|
||||
)
|
||||
@@ -1801,7 +1805,7 @@ def test_annotation_and_explicit_schema_validation_fail_closed():
|
||||
|
||||
with pytest.raises(TypeError, match="unsupported Arrow type"):
|
||||
|
||||
@udf(
|
||||
@function(
|
||||
input_schema=pa.schema(
|
||||
[pa.field("value", pa.int64(), metadata={"k": "v"})]
|
||||
),
|
||||
@@ -1812,7 +1816,7 @@ def test_annotation_and_explicit_schema_validation_fail_closed():
|
||||
|
||||
with pytest.raises(TypeError, match="unsupported Arrow type"):
|
||||
|
||||
@udf(
|
||||
@function(
|
||||
input_schema=pa.schema([pa.field("value", pa.int64())]),
|
||||
output_schema=pa.field(
|
||||
"result", pa.int64(), nullable=False, metadata={"k": "v"}
|
||||
@@ -1824,7 +1828,7 @@ def test_annotation_and_explicit_schema_validation_fail_closed():
|
||||
struct_type = pa.struct([pa.field("value", pa.int64(), nullable=False)])
|
||||
with pytest.raises(TypeError, match="unsupported Arrow type"):
|
||||
|
||||
@udf(
|
||||
@function(
|
||||
input_schema=pa.schema([pa.field("value", pa.int64())]),
|
||||
output_schema=pa.field(
|
||||
"result", struct_type, nullable=False, metadata={"k": "v"}
|
||||
@@ -1848,7 +1852,7 @@ def test_annotation_and_explicit_schema_validation_fail_closed():
|
||||
]:
|
||||
with pytest.raises(TypeError, match="schema metadata"):
|
||||
|
||||
@udf(input_schema=input_schema, output_schema=output_schema)
|
||||
@function(input_schema=input_schema, output_schema=output_schema)
|
||||
def schema_metadata(value):
|
||||
return value
|
||||
|
||||
@@ -2093,8 +2097,8 @@ def test_building_a_binding_contacts_no_server():
|
||||
assert state["requests"] == []
|
||||
|
||||
|
||||
def test_vectorized_blob_udf_executes_packaged_artifact_and_publishes(tmp_path):
|
||||
@udf(
|
||||
def test_vectorized_blob_function_executes_packaged_artifact_and_publishes(tmp_path):
|
||||
@function(
|
||||
input_schema=pa.schema([lancedb.blob("image", nullable=True)]),
|
||||
output_schema=lancedb.blob("copy", nullable=False),
|
||||
)
|
||||
@@ -2135,7 +2139,7 @@ def test_vectorized_blob_udf_executes_packaged_artifact_and_publishes(tmp_path):
|
||||
)
|
||||
hydrated = source.fetch_blobs("image", input_hits)
|
||||
assert isinstance(hydrated, pa.LargeBinaryArray)
|
||||
result = _execute_registered_array_udf_v1(
|
||||
result = _execute_registered_array_function_v1(
|
||||
registration,
|
||||
pa.RecordBatch.from_arrays([hydrated], names=["image"]),
|
||||
)
|
||||
@@ -2181,7 +2185,7 @@ def test_blob_list_output_fresh_artifact_survives_local_reopen(
|
||||
input_type = physical_list(pa.int64())
|
||||
item = lancedb.blob("item", nullable=True)
|
||||
|
||||
@udf(
|
||||
@function(
|
||||
input_schema=pa.schema([pa.field("frames", input_type, nullable=False)]),
|
||||
output_schema=pa.field("frames", declared_list(item), nullable=False),
|
||||
)
|
||||
@@ -2199,7 +2203,7 @@ def test_blob_list_output_fresh_artifact_survives_local_reopen(
|
||||
item_field = declared["fields"][0]
|
||||
assert item_field["metadata"]["ARROW:extension:name"] == "lance.blob.v2"
|
||||
|
||||
result = _execute_registered_array_udf_v1(
|
||||
result = _execute_registered_array_function_v1(
|
||||
request,
|
||||
pa.RecordBatch.from_arrays(
|
||||
[pa.array([[0], [1]], type=input_type)],
|
||||
@@ -2264,11 +2268,11 @@ def test_a_function_namespace_path_must_be_a_list_of_segments():
|
||||
|
||||
|
||||
def test_one_explicit_schema_infers_the_other_from_annotations():
|
||||
@udf(input_schema=pa.schema([pa.field("value", pa.int64())]))
|
||||
@function(input_schema=pa.schema([pa.field("value", pa.int64())]))
|
||||
def explicit_input(value) -> float:
|
||||
return float(value or 0)
|
||||
|
||||
@udf(output_schema=pa.field("result", pa.float32(), nullable=False))
|
||||
@function(output_schema=pa.field("result", pa.float32(), nullable=False))
|
||||
def explicit_output(value: Optional[int]):
|
||||
return float(value or 0)
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ import pyarrow as pa
|
||||
import pytest
|
||||
|
||||
from lancedb import col
|
||||
from lancedb.functions import FunctionVersion, udf
|
||||
from lancedb.functions import FunctionDefinition, FunctionVersion, function
|
||||
|
||||
FIXTURES = (
|
||||
Path(__file__).parents[3]
|
||||
@@ -63,7 +63,7 @@ def helper_package(tmp_path, monkeypatch):
|
||||
return write
|
||||
|
||||
|
||||
@udf
|
||||
@function
|
||||
class scale:
|
||||
def __init__(
|
||||
self,
|
||||
@@ -114,11 +114,11 @@ def test_class_function_declares_inputs_output_and_initialization():
|
||||
assert scale.registration_request.artifact.entrypoint == "scale"
|
||||
source = artifact(scale).decode()
|
||||
assert source.startswith("from __future__ import annotations\n")
|
||||
assert "class scale:" in source and "@udf" not in source
|
||||
assert "class scale:" in source and "@function" not in source
|
||||
|
||||
|
||||
def test_function_without_initialization_keeps_its_wire_form():
|
||||
@udf
|
||||
@function
|
||||
def double(value: int) -> int:
|
||||
return value * 2
|
||||
|
||||
@@ -147,11 +147,11 @@ def test_batch_annotations_require_explicit_schemas(annotation, kind, helper_pac
|
||||
"""
|
||||
module = helper_package("fx_batch_annotations", {"__init__.py": source})
|
||||
with pytest.raises(TypeError, match="input_schema and output_schema"):
|
||||
udf(module.identity)
|
||||
function(module.identity)
|
||||
|
||||
|
||||
def test_array_batch_annotations_accept_explicit_schemas():
|
||||
@udf(
|
||||
@function(
|
||||
input_schema=pa.schema([pa.field("value", pa.int64(), nullable=False)]),
|
||||
output_schema=pa.int64(),
|
||||
)
|
||||
@@ -168,7 +168,7 @@ def test_array_batch_annotations_accept_explicit_schemas():
|
||||
assert identity(values).equals(values)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("class_udf", [False, True])
|
||||
@pytest.mark.parametrize("class_function", [False, True])
|
||||
@pytest.mark.parametrize("postponed", [False, True])
|
||||
@pytest.mark.parametrize(
|
||||
("annotation", "nullable"),
|
||||
@@ -185,13 +185,13 @@ def test_array_batch_annotations_accept_explicit_schemas():
|
||||
],
|
||||
)
|
||||
def test_blob_reader_annotations_resolve_in_the_artifact(
|
||||
helper_package, monkeypatch, class_udf, postponed, annotation, nullable
|
||||
helper_package, monkeypatch, class_function, postponed, annotation, nullable
|
||||
):
|
||||
definition = (
|
||||
f"class read_blob:\n"
|
||||
f" def __call__(self, video: {annotation}) -> int:\n"
|
||||
f" return 0\n"
|
||||
if class_udf
|
||||
if class_function
|
||||
else f"def read_blob(video: {annotation}) -> int:\n return 0\n"
|
||||
)
|
||||
module = helper_package(
|
||||
@@ -202,9 +202,9 @@ def test_blob_reader_annotations_resolve_in_the_artifact(
|
||||
+ "import lancedb\n"
|
||||
"from typing import Optional\n"
|
||||
"from lancedb import BlobReader, BlobReader as Reader\n"
|
||||
"from lancedb.functions import udf\n"
|
||||
"from lancedb.functions import function\n"
|
||||
"MaybeReader = Optional[Reader]\n\n"
|
||||
f"@udf\n{definition}"
|
||||
f"@function\n{definition}"
|
||||
)
|
||||
},
|
||||
)
|
||||
@@ -221,7 +221,7 @@ def test_blob_reader_annotations_resolve_in_the_artifact(
|
||||
namespace = {}
|
||||
exec(compile(artifact(module.read_blob), "user_function.py", "exec"), namespace)
|
||||
packaged = namespace["read_blob"]
|
||||
if class_udf:
|
||||
if class_function:
|
||||
packaged = packaged.__call__
|
||||
assert get_type_hints(packaged) == {
|
||||
"video": Optional[worker.BlobReader] if nullable else worker.BlobReader,
|
||||
@@ -237,11 +237,11 @@ def test_blob_reader_annotation_does_not_shadow_body_globals(
|
||||
{
|
||||
"__init__.py": """
|
||||
from lancedb import BlobReader
|
||||
from lancedb.functions import udf
|
||||
from lancedb.functions import function
|
||||
|
||||
_lance_blob_reader = 7
|
||||
|
||||
@udf
|
||||
@function
|
||||
def read_blob(video: BlobReader) -> int:
|
||||
return _lance_blob_reader
|
||||
"""
|
||||
@@ -280,14 +280,14 @@ def test_class_function_rejects_unsupported_initialization(init, message):
|
||||
namespace,
|
||||
)
|
||||
with pytest.raises((TypeError, ValueError), match=message):
|
||||
udf(namespace["bad"])
|
||||
function(namespace["bad"])
|
||||
|
||||
|
||||
def test_class_function_requires_call_and_a_plain_close():
|
||||
with pytest.raises(TypeError, match="synchronous __call__"):
|
||||
udf(NoCall)
|
||||
function(NoCall)
|
||||
with pytest.raises(TypeError, match="close must be a method taking no"):
|
||||
udf(BadClose)
|
||||
function(BadClose)
|
||||
|
||||
|
||||
def test_nested_class_and_closure_are_rejected_with_the_reason():
|
||||
@@ -299,13 +299,13 @@ def test_nested_class_and_closure_are_rejected_with_the_reason():
|
||||
return nested
|
||||
|
||||
with pytest.raises(ValueError, match="defined at module level"):
|
||||
udf(build())
|
||||
function(build())
|
||||
|
||||
factor = 3
|
||||
|
||||
with pytest.raises(ValueError, match="capture closure values"):
|
||||
|
||||
@udf
|
||||
@function
|
||||
def closes_over(value: int) -> int:
|
||||
return value * factor
|
||||
|
||||
@@ -372,10 +372,10 @@ def test_code_packages_ship_as_a_bundle_and_version_with_their_source(
|
||||
"""
|
||||
import fx_helpers
|
||||
from fx_helpers.rate import TokenBucket
|
||||
from lancedb import udf
|
||||
from lancedb import function
|
||||
|
||||
|
||||
@udf(code=[fx_helpers])
|
||||
@function(code=[fx_helpers])
|
||||
class limited:
|
||||
def __init__(self, tpm: int):
|
||||
self.limiter = TokenBucket(tpm)
|
||||
@@ -445,7 +445,7 @@ def test_code_package_cache_artifacts_do_not_warn(helper_package):
|
||||
with warnings.catch_warnings(record=True) as caught:
|
||||
warnings.simplefilter("always")
|
||||
|
||||
@udf(code=[cache])
|
||||
@function(code=[cache])
|
||||
def cache_only(value: int) -> int:
|
||||
return value
|
||||
|
||||
@@ -468,7 +468,7 @@ def test_code_package_nonidentifier_resource_dir_warns_at_boundary(helper_packag
|
||||
with warnings.catch_warnings(record=True) as caught:
|
||||
warnings.simplefilter("always")
|
||||
|
||||
@udf(code=[resources])
|
||||
@function(code=[resources])
|
||||
def resource_boundary(value: int) -> int:
|
||||
return value
|
||||
|
||||
@@ -495,7 +495,7 @@ def test_code_rejects_submodules_duplicates_and_non_modules(helper_package):
|
||||
]:
|
||||
with pytest.raises((TypeError, ValueError), match=message):
|
||||
|
||||
@udf(code=code)
|
||||
@function(code=code)
|
||||
def f(value: int) -> int:
|
||||
return value
|
||||
|
||||
@@ -514,10 +514,10 @@ def test_local_module_outside_code_warns_at_registration(tmp_path, helper_packag
|
||||
textwrap.dedent(
|
||||
f"""
|
||||
from fx_local import normalize
|
||||
from lancedb import udf
|
||||
from lancedb import function
|
||||
|
||||
|
||||
@udf({options})
|
||||
@function({options})
|
||||
def {name}(value: int) -> int:
|
||||
return normalize(value)
|
||||
"""
|
||||
@@ -551,10 +551,10 @@ def test_local_module_imported_in_function_body_warns_at_registration(
|
||||
(tmp_path / f"{name}.py").write_text(
|
||||
textwrap.dedent(
|
||||
f"""
|
||||
from lancedb import udf
|
||||
from lancedb import function
|
||||
|
||||
|
||||
@udf({options})
|
||||
@function({options})
|
||||
def {name}(value: int) -> int:
|
||||
import fx_body_local
|
||||
|
||||
@@ -610,20 +610,20 @@ import pyarrow as pa
|
||||
|
||||
import fx_mmlb
|
||||
from fx_mmlb.embedding import OpenAIEmbedder
|
||||
from lancedb import udf
|
||||
from lancedb import function
|
||||
|
||||
TEXT = pa.schema([pa.field("text", pa.string())])
|
||||
EMBEDDING = pa.list_(pa.field("item", pa.float64(), nullable=False))
|
||||
|
||||
|
||||
@udf(code=[fx_mmlb], input_schema=TEXT, output_schema=EMBEDDING)
|
||||
@function(code=[fx_mmlb], input_schema=TEXT, output_schema=EMBEDDING)
|
||||
class openai_embedding_batch(OpenAIEmbedder):
|
||||
def __call__(self, text: pa.Array) -> pa.Array:
|
||||
item = pa.field("item", pa.float64(), nullable=False)
|
||||
return pa.array(self.embed(text.to_pylist()), pa.list_(item))
|
||||
|
||||
|
||||
@udf(code=[fx_mmlb])
|
||||
@function(code=[fx_mmlb])
|
||||
class openai_embedding(OpenAIEmbedder):
|
||||
def __call__(self, text: str) -> list[float]:
|
||||
return self.embed([text])[0]
|
||||
@@ -695,7 +695,7 @@ def test_main_definitions_travel_by_source(tmp_path):
|
||||
import math
|
||||
from dataclasses import dataclass
|
||||
|
||||
from lancedb import udf
|
||||
from lancedb import function
|
||||
|
||||
SCALE = 2
|
||||
|
||||
@@ -714,7 +714,7 @@ def test_main_definitions_travel_by_source(tmp_path):
|
||||
return helper(value) * self.settings.factor
|
||||
|
||||
|
||||
@udf
|
||||
@function
|
||||
class scaled(Base):
|
||||
def __init__(self, factor: int):
|
||||
self.settings = Settings(factor)
|
||||
@@ -750,11 +750,11 @@ def test_main_lambdas_are_rejected(tmp_path):
|
||||
script.write_text(
|
||||
textwrap.dedent(
|
||||
"""
|
||||
from lancedb import udf
|
||||
from lancedb import function
|
||||
|
||||
double = lambda value: value * 2
|
||||
|
||||
@udf
|
||||
@function
|
||||
def uses_lambda(value: int) -> int:
|
||||
return double(value)
|
||||
"""
|
||||
@@ -765,3 +765,33 @@ def test_main_lambdas_are_rejected(tmp_path):
|
||||
)
|
||||
assert result.returncode != 0
|
||||
assert "not lambdas, closures, or nested definitions" in result.stderr
|
||||
|
||||
|
||||
def test_udf_alias_warns_and_packages_like_function(helper_package):
|
||||
def source(decorator: str) -> str:
|
||||
return f"""
|
||||
import lancedb
|
||||
from lancedb import function, udf
|
||||
|
||||
@{decorator}
|
||||
def double(value: int) -> int:
|
||||
return value * 2
|
||||
"""
|
||||
|
||||
current = helper_package("fx_alias_current", {"__init__.py": source("function")})
|
||||
expected = current.double.registration_request.artifact.digest
|
||||
for name, decorator in (("fx_alias_bare", "udf"), ("fx_alias_attr", "lancedb.udf")):
|
||||
with pytest.warns(DeprecationWarning, match="lancedb.udf is deprecated"):
|
||||
module = helper_package(name, {"__init__.py": source(decorator)})
|
||||
assert isinstance(module.double, FunctionDefinition)
|
||||
assert "udf" not in artifact(module.double).decode()
|
||||
assert module.double.registration_request.artifact.digest == expected
|
||||
|
||||
|
||||
def test_udf_definition_alias_warns():
|
||||
with pytest.warns(DeprecationWarning, match="UdfDefinition is deprecated"):
|
||||
from lancedb import UdfDefinition
|
||||
assert UdfDefinition is FunctionDefinition
|
||||
with pytest.warns(DeprecationWarning, match="UdfDefinition is deprecated"):
|
||||
from lancedb.functions import UdfDefinition as module_alias
|
||||
assert module_alias is FunctionDefinition
|
||||
|
||||
Reference in New Issue
Block a user