Skip to content

Commit 88f2a29

Browse files
rustyconoverclaude
andcommitted
Fix Arg[type] subscript type not captured for Arrow type inference
The Arg[str] subscript type was not being captured at runtime, causing argument types to show as "null" in catalog function metadata. Changes: - Add _ArgFactory class that captures the type parameter when Arg[type] is used and creates Arg instances with _type_param set - Add _type_param slot to Arg class to store the captured type - Update extract_argument_specs to use _type_param when determining Arrow type (priority: explicit arrow_type > _type_param > type hint) - Update test to use a custom type not in PYTHON_TO_ARROW 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent cea1d75 commit 88f2a29

3 files changed

Lines changed: 94 additions & 4 deletions

File tree

tests/test_argument_spec.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -490,10 +490,14 @@ class TestExtractArgumentSpecsValidation:
490490
def test_missing_type_hint_warns(self) -> None:
491491
"""Missing type hint and no arrow_type should issue a warning."""
492492

493+
# Use a custom class not in PYTHON_TO_ARROW to trigger warning
494+
class CustomType:
495+
pass
496+
493497
class FunctionWithArg(TableInOutFunction):
494-
count = Arg[int](0) # No type annotation, no arrow_type
498+
count = Arg[CustomType](0) # Type not in PYTHON_TO_ARROW
495499

496-
# Should warn about missing type
500+
# Should warn about missing type (CustomType is not in PYTHON_TO_ARROW)
497501
with pytest.warns(UserWarning, match="Cannot determine Arrow type"):
498502
specs = extract_argument_specs(FunctionWithArg)
499503

vgi/argument_spec.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -344,13 +344,21 @@ class MyFunction(TableInOutFunction):
344344

345345
# Determine Arrow type using priority order:
346346
# 1. Explicit arrow_type on Arg
347-
# 2. Type hint with PYTHON_TO_ARROW mapping
348-
# 3. Default to pa.null() with warning
347+
# 2. Type parameter from Arg[type] subscript (e.g., Arg[str])
348+
# 3. Type hint with PYTHON_TO_ARROW mapping
349+
# 4. Default to pa.null() with warning
349350
arrow_type: pa.DataType
350351
if arg.arrow_type is not None:
351352
arrow_type = arg.arrow_type
352353
elif is_table_input or is_any_type:
353354
arrow_type = pa.null()
355+
elif (
356+
hasattr(arg, "_type_param")
357+
and arg._type_param is not None
358+
and arg._type_param in PYTHON_TO_ARROW
359+
):
360+
# Use type from Arg[type] subscript
361+
arrow_type = PYTHON_TO_ARROW[arg._type_param]
354362
elif hint is not None and hint in PYTHON_TO_ARROW:
355363
arrow_type = PYTHON_TO_ARROW[hint]
356364
else:

vgi/arguments.py

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -486,6 +486,73 @@ def _suggest_similar_choices(self) -> list[Any]:
486486
ArgT = TypeVar("ArgT")
487487

488488

489+
class _ArgFactory:
490+
"""Factory returned by Arg[type] to capture the type parameter.
491+
492+
This allows Arg[str](0) to create an Arg instance with _type_param=str,
493+
which can be used by extract_argument_specs to infer the Arrow type.
494+
"""
495+
496+
__slots__ = ("_type_param",)
497+
498+
def __init__(self, type_param: type) -> None:
499+
self._type_param = type_param
500+
501+
def __call__(
502+
self,
503+
position: int | str,
504+
*,
505+
default: Any = _MISSING,
506+
doc: str = "",
507+
ge: float | int | None = None,
508+
le: float | int | None = None,
509+
gt: float | int | None = None,
510+
lt: float | int | None = None,
511+
choices: Sequence[Any] | None = None,
512+
pattern: str | None = None,
513+
varargs: bool = False,
514+
arrow_type: pa.DataType | None = None,
515+
) -> "Arg[Any]":
516+
"""Create an Arg instance with the captured type parameter."""
517+
arg: Arg[Any] = Arg.__new__(Arg)
518+
# Manually call __init__ logic since we're using __new__
519+
# Validate constraint combinations
520+
if ge is not None and gt is not None:
521+
raise ValueError("Cannot specify both 'ge' and 'gt'")
522+
if le is not None and lt is not None:
523+
raise ValueError("Cannot specify both 'le' and 'lt'")
524+
if varargs:
525+
if isinstance(position, str):
526+
raise ValueError(
527+
"varargs=True requires a positional argument (int), not named"
528+
)
529+
if default is not _MISSING:
530+
raise ValueError(
531+
"varargs=True cannot have a default value "
532+
"(requires at least 1 value)"
533+
)
534+
535+
arg.position = position
536+
arg.default = default
537+
arg.doc = doc
538+
arg.ge = ge
539+
arg.le = le
540+
arg.gt = gt
541+
arg.lt = lt
542+
arg.choices = choices
543+
arg.pattern = pattern
544+
arg.varargs = varargs
545+
arg.arrow_type = arrow_type
546+
arg._name = None
547+
arg._compiled_pattern = None
548+
arg._type_param = self._type_param
549+
550+
if pattern is not None:
551+
arg._compiled_pattern = re.compile(pattern)
552+
553+
return arg
554+
555+
489556
class Arg[ArgT]:
490557
"""Descriptor for declarative argument parsing with optional validation.
491558
@@ -559,6 +626,7 @@ def transform(self, batch):
559626
"arrow_type",
560627
"_name",
561628
"_compiled_pattern",
629+
"_type_param",
562630
)
563631

564632
def __init__(
@@ -629,11 +697,21 @@ def __init__(
629697
self.arrow_type = arrow_type
630698
self._name: str | None = None
631699
self._compiled_pattern: re.Pattern[str] | None = None
700+
self._type_param: type | None = None
632701

633702
# Pre-compile pattern for efficiency
634703
if pattern is not None:
635704
self._compiled_pattern = re.compile(pattern)
636705

706+
def __class_getitem__(cls, item: type) -> "_ArgFactory":
707+
"""Support Arg[type] syntax to capture the type parameter at runtime.
708+
709+
When you write Arg[str](0), this method is called first with item=str,
710+
and returns an _ArgFactory that will create Arg instances with
711+
_type_param set to str.
712+
"""
713+
return _ArgFactory(item)
714+
637715
def __set_name__(self, owner: type, name: str) -> None:
638716
"""Store the attribute name when assigned to a class."""
639717
self._name = name

0 commit comments

Comments
 (0)