mirror of
https://github.com/lancedb/lancedb.git
synced 2026-11-13 14:16:54 +00:00
fix(python): name unsupported Function globals in capture errors (#4938)
Function source packaging rejected unsupported global values without identifying the variable or explaining the supported captures. Include the global name even for a nested tuple failure, describe the supported literal values and remedy, and document the capture boundary. Adds function/class regression cases for Arrow types, nested tuples, dictionaries and non-finite floats, plus a supported nested literal control. New serialization support remains separate. Formatting applied; no local builds or tests were run. CI will execute the regression coverage.
This commit is contained in:
@@ -1428,19 +1428,21 @@ def _is_function_decorator(node: ast.expr) -> bool:
|
||||
)
|
||||
|
||||
|
||||
def _literal_source(value: Any) -> str:
|
||||
def _literal_source(value: Any, name: str) -> str:
|
||||
if value is None or type(value) in (bool, int, str, bytes):
|
||||
return repr(value)
|
||||
if type(value) is float and math.isfinite(value):
|
||||
return repr(value)
|
||||
if type(value) is tuple:
|
||||
children = ", ".join(_literal_source(child) for child in value)
|
||||
children = ", ".join(_literal_source(child, name) for child in value)
|
||||
if len(value) == 1:
|
||||
children += ","
|
||||
return f"({children})"
|
||||
raise TypeError(
|
||||
"Function source references an unsupported global value of type "
|
||||
f"{type(value).__name__}"
|
||||
f"{type(value).__name__} in {name!r}. Only None, bool, int, str, bytes, "
|
||||
"finite float, and tuples of these literals can be captured. "
|
||||
"Define this value inside the function or class body instead."
|
||||
)
|
||||
|
||||
|
||||
@@ -1875,7 +1877,7 @@ class _SourcePackager:
|
||||
if imported is value:
|
||||
self._require_importable(module_name)
|
||||
return f"from {module_name} import {qualname} as {name}"
|
||||
return f"{name} = {_literal_source(value)}"
|
||||
return f"{name} = {_literal_source(value, name)}"
|
||||
|
||||
def _require_importable(self, module_name: str) -> None:
|
||||
"""Warn about an import the Function's environment may not be able to
|
||||
@@ -2486,8 +2488,8 @@ def function(
|
||||
- modules, and classes and functions importable from an installed
|
||||
package, become imports; the package must be declared in ``pip`` or
|
||||
``conda``;
|
||||
- literals (``None``, booleans, numbers, strings, bytes, tuples of them)
|
||||
are inlined;
|
||||
- literals (``None``, ``bool``, ``int``, ``str``, ``bytes``, finite
|
||||
``float``, and tuples containing only these literals) are inlined;
|
||||
- functions and classes defined in ``__main__``, such as notebook cells,
|
||||
are packaged by source, together with what they reference;
|
||||
- modules and packages listed in ``code`` are shipped as Python source and
|
||||
@@ -2505,6 +2507,9 @@ def function(
|
||||
image. Warnings follow normal Python warning filters and may be promoted
|
||||
to errors. Declare every third-party import in ``pip`` or ``conda`` even
|
||||
when it happens to be installed on the registration host.
|
||||
Other global values, including Arrow types and schemas, lists, dictionaries,
|
||||
sets, and NumPy arrays, cannot be captured as literals. Define them inside
|
||||
the function or class body instead.
|
||||
Closures, lambdas, nested definitions, mutable module-level
|
||||
objects, code that reaches the module namespace another way
|
||||
(``globals()``/``eval``, ``sys.modules``, ``builtins``), and a
|
||||
|
||||
@@ -127,6 +127,60 @@ def test_function_without_initialization_keeps_its_wire_form():
|
||||
assert request["artifact"]["kind"] == "python_callable"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("kind", ["function", "class"])
|
||||
@pytest.mark.parametrize(
|
||||
("global_value", "value_type"),
|
||||
[
|
||||
("pa.struct([('label', pa.string())])", "StructType"),
|
||||
("(1, (pa.int64(),))", "DataType"),
|
||||
("{'label': 1}", "dict"),
|
||||
("float('inf')", "float"),
|
||||
],
|
||||
)
|
||||
def test_unsupported_global_names_value_and_remedy(
|
||||
helper_package, kind, global_value, value_type
|
||||
):
|
||||
definition = (
|
||||
"class uses_global:\n"
|
||||
" def __call__(self, value: int) -> int:\n"
|
||||
" return value if CAPTURED_VALUE else 0\n"
|
||||
if kind == "class"
|
||||
else "def uses_global(value: int) -> int:\n"
|
||||
" return value if CAPTURED_VALUE else 0\n"
|
||||
)
|
||||
module = helper_package(
|
||||
"fx_unsupported_global",
|
||||
{
|
||||
"__init__.py": f"import pyarrow as pa\nCAPTURED_VALUE = {global_value}\n"
|
||||
+ definition
|
||||
},
|
||||
)
|
||||
with pytest.raises(TypeError) as error:
|
||||
function(module.uses_global)
|
||||
message = str(error.value)
|
||||
assert "CAPTURED_VALUE" in message
|
||||
assert value_type in message
|
||||
assert "finite float" in message
|
||||
assert "inside the function or class body" in message
|
||||
|
||||
|
||||
def test_supported_nested_literal_global_still_packages(helper_package):
|
||||
module = helper_package(
|
||||
"fx_literal_global",
|
||||
{
|
||||
"__init__.py": "CAPTURED_VALUE = (None, True, 1, 'label', "
|
||||
"b'bytes', (1.5,))\n"
|
||||
"def uses_global(value: int) -> int:\n"
|
||||
" return value + CAPTURED_VALUE[2]\n"
|
||||
},
|
||||
)
|
||||
definition = function(module.uses_global)
|
||||
namespace = {}
|
||||
exec(compile(artifact(definition), "user_function.py", "exec"), namespace)
|
||||
assert namespace["uses_global"](3) == 4
|
||||
assert namespace["CAPTURED_VALUE"] == module.CAPTURED_VALUE
|
||||
|
||||
|
||||
@pytest.mark.parametrize("annotation", ["Array", "RecordBatch"])
|
||||
@pytest.mark.parametrize("kind", ["function", "class"])
|
||||
def test_batch_annotations_require_explicit_schemas(annotation, kind, helper_package):
|
||||
|
||||
Reference in New Issue
Block a user