Skip to content

Commit 59d839c

Browse files
rustyconoverclaude
andcommitted
Add pytest-cov and improve error messages with actionable guidance
- Add pytest-cov to dev dependencies for coverage reporting - Improve ValueError/RuntimeError messages across the codebase to include actionable guidance: what went wrong, why, and how to fix it - Files updated: arguments.py, function.py, table_function.py, table_in_out_function.py, worker.py 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 3dac957 commit 59d839c

7 files changed

Lines changed: 151 additions & 11 deletions

File tree

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ requires-python = ">=3.12.4"
77
dependencies = ["click", "pyarrow", "structlog", "platformdirs"]
88

99
[project.optional-dependencies]
10-
dev = ["mypy", "pyarrow-stubs", "pytest", "pytest-mypy", "pytest-ruff", "pytest-xdist", "ruff"]
10+
dev = ["mypy", "pyarrow-stubs", "pytest", "pytest-cov", "pytest-mypy", "pytest-ruff", "pytest-xdist", "ruff"]
1111

1212
[project.scripts]
1313
vgi-client = "vgi.client.cli:main"

uv.lock

Lines changed: 90 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

vgi/arguments.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -518,7 +518,12 @@ def __get__(
518518

519519
# Instance access - parse and cache
520520
if self._name is None:
521-
raise RuntimeError("Arg descriptor was not properly initialized")
521+
raise RuntimeError(
522+
"Arg descriptor was not properly initialized. "
523+
"This typically means the descriptor was accessed before __set_name__ "
524+
"was called. Ensure Arg is used as a class attribute, not instantiated "
525+
"dynamically."
526+
)
522527

523528
if self._name not in obj.__dict__:
524529
obj.__dict__[self._name] = self._resolve(obj)
@@ -528,7 +533,12 @@ def _resolve(self, obj: object) -> ArgT:
528533
"""Parse argument from obj.invocation.arguments and validate."""
529534
invocation = getattr(obj, "invocation", None)
530535
if invocation is None:
531-
raise RuntimeError("Object does not have 'invocation' attribute")
536+
raise RuntimeError(
537+
f"Cannot resolve Arg '{self._name}': object {type(obj).__name__} does "
538+
f"not have an 'invocation' attribute. Arg descriptors can only be used "
539+
f"on classes that have an 'invocation' attribute (e.g., "
540+
f"TableInOutFunction, TableFunctionGenerator)."
541+
)
532542
arguments = invocation.arguments
533543

534544
if self.default is _MISSING:

vgi/function.py

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -944,7 +944,13 @@ def process(self, batch: pa.RecordBatch) -> OutputGenerator:
944944
945945
"""
946946
if self.init_identifier is None:
947-
raise ValueError("init_identifier must be set before storing state")
947+
raise ValueError(
948+
"init_identifier must be set before storing state. "
949+
"This is typically set automatically during perform_init() or "
950+
"retrieve_init(). Ensure your function calls super().perform_init() "
951+
"in perform_init(), or that the worker correctly calls "
952+
"retrieve_init() for secondary workers."
953+
)
948954
self.state_storage.store(
949955
self.init_identifier,
950956
os.getpid(),
@@ -977,7 +983,13 @@ def finalize(self) -> OutputGenerator:
977983
978984
"""
979985
if self.init_identifier is None:
980-
raise ValueError("init_identifier must be set before collecting states")
986+
raise ValueError(
987+
"init_identifier must be set before collecting states. "
988+
"This is typically set automatically during perform_init() or "
989+
"retrieve_init(). Ensure your function calls super().perform_init() "
990+
"in perform_init(), or that the worker correctly calls "
991+
"retrieve_init() for secondary workers."
992+
)
981993
state_bytes_list = self.state_storage.collect_and_delete(self.init_identifier)
982994
return [state_class.deserialize(data) for data in state_bytes_list]
983995

@@ -1008,7 +1020,11 @@ def perform_init(self, init_input: pa.RecordBatch) -> GlobalInitResult:
10081020
10091021
"""
10101022
if self.init_identifier is None:
1011-
raise ValueError("init_identifier must be set before enqueuing work")
1023+
raise ValueError(
1024+
"init_identifier must be set before enqueuing work. "
1025+
"Call enqueue_work() after perform_init() has completed, typically "
1026+
"at the end of your perform_init() override or in setup()."
1027+
)
10121028
return self.state_storage.enqueue_work(self.init_identifier, work_items)
10131029

10141030
def dequeue_work(self) -> bytes | None:
@@ -1038,5 +1054,11 @@ def process(self) -> OutputGenerator:
10381054
10391055
"""
10401056
if self.init_identifier is None:
1041-
raise ValueError("init_identifier must be set before dequeuing work")
1057+
raise ValueError(
1058+
"init_identifier must be set before dequeuing work. "
1059+
"This is typically set automatically during perform_init() or "
1060+
"retrieve_init(). Ensure your function calls super().perform_init() "
1061+
"in perform_init(), or that the worker correctly calls "
1062+
"retrieve_init() for secondary workers."
1063+
)
10421064
return self.state_storage.dequeue_work(self.init_identifier)

vgi/table_function.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -383,7 +383,12 @@ def perform_init(self, init_input: pa.RecordBatch) -> vgi.function.GlobalInitRes
383383
def retrieve_init(self, init_input: vgi.function.GlobalInitResult) -> None:
384384
"""Retrieve and store init data from the storage."""
385385
if init_input.global_init_identifier is None:
386-
raise ValueError("global_init_identifier is required but was None")
386+
raise ValueError(
387+
"global_init_identifier is required but was None. "
388+
"This indicates the GlobalInitResult was not properly initialized. "
389+
"Ensure perform_init() returns a GlobalInitResult with a valid "
390+
"identifier."
391+
)
387392
self.init_identifier = init_input.global_init_identifier
388393
self.init_data = GlobalStateInitInput.deserialize_bytes(
389394
self.init_storage.get(self.init_identifier)

vgi/table_in_out_function.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -554,7 +554,13 @@ def __init__(
554554
"""Initialize the function with invocation data and logger."""
555555
super().__init__(invocation=invocation, logger=logger)
556556
if invocation.in_out_function_input_schema is None:
557-
raise ValueError("Function requires a non-null input schema")
557+
raise ValueError(
558+
f"{type(self).__name__} requires an input schema, but none was "
559+
f"provided. TableInOutGeneratorFunction processes input batches and "
560+
f"requires in_out_function_input_schema to be set in the Invocation. "
561+
f"If your function generates output without input, inherit from "
562+
f"TableFunctionGenerator instead."
563+
)
558564

559565
@property
560566
def input_schema(self) -> pa.Schema:

vgi/worker.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -295,7 +295,11 @@ def _process_batches(
295295
296296
"""
297297
if invocation.global_init_identifier is None:
298-
raise ValueError("global_init_identifier is required but was None")
298+
raise ValueError(
299+
"global_init_identifier is required but was None. "
300+
"This is an internal protocol error - the worker should have set "
301+
"global_init_identifier after perform_init() completed successfully."
302+
)
299303
generator = instance.run()
300304
next(generator) # Prime the run() generator
301305

@@ -474,7 +478,10 @@ def run(self) -> None:
474478
else:
475479
raise TypeError(
476480
f"Unsupported function type: {type(instance).__name__}. "
477-
f"Expected TableInOutGeneratorFunction or TableFunctionGenerator."
481+
f"Functions must inherit from TableInOutGeneratorFunction (for "
482+
f"functions that process input batches) or TableFunctionGenerator "
483+
f"(for functions that generate output without input). "
484+
f"See vgi.table_in_out_function and vgi.table_function modules."
478485
)
479486

480487
fn_log.info(

0 commit comments

Comments
 (0)