Skip to content

fix: validate pyarrow columns in stage input checks#2156

Closed
nightcityblade wants to merge 1 commit into
NVIDIA-NeMo:mainfrom
nightcityblade:fix/issue-2151
Closed

fix: validate pyarrow columns in stage input checks#2156
nightcityblade wants to merge 1 commit into
NVIDIA-NeMo:mainfrom
nightcityblade:fix/issue-2151

Conversation

@nightcityblade

Copy link
Copy Markdown
Contributor

Description

closes #2151

  • teach ProcessingStage.validate_input() to recognize column names on table-like task data instead of relying only on hasattr()
  • cover the regression with focused DocumentBatch tests using PyArrow tables

Usage

from nemo_curator.tasks import DocumentBatch
import pyarrow as pa

batch = DocumentBatch(task_id="batch", dataset_name="dataset", data=pa.table({"text": ["hello"]}))
assert stage.validate_input(batch)

Checklist

  • I am familiar with the Contributing Guide.
  • New or Existing tests cover these changes.
  • The documentation is up to date with these changes.

Validation run locally:

  • python3 -m ruff check nemo_curator/stages/base.py tests/stages/common/test_base.py
  • python3 -m pytest --noconftest tests/stages/common/test_base.py -q (blocked in this environment: pyarrow is not installed; the full test setup also requires ray via tests/conftest.py)

@nightcityblade
nightcityblade requested a review from a team as a code owner July 2, 2026 15:08
@nightcityblade
nightcityblade requested review from oyilmaz-nvidia and removed request for a team July 2, 2026 15:08
@copy-pr-bot

copy-pr-bot Bot commented Jul 2, 2026

Copy link
Copy Markdown

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

@greptile-apps

greptile-apps Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes a regression where ProcessingStage.validate_input() used bare hasattr() to check for required data columns, which always returned False for PyArrow tables (columns are not exposed as Python attributes). A new _has_data_attr static method is introduced that explicitly checks pa.Table.column_names and pd.DataFrame.columns before falling back to hasattr.

  • nemo_curator/stages/base.py: Replaces the inline hasattr(task.data, attr) check with _has_data_attr(), which handles PyArrow tables via column_names and pandas DataFrames via columns in addition to the original attribute-lookup path.
  • tests/stages/common/test_base.py: Adds TestProcessingStageValidateInput with six cases covering PyArrow, pandas, and plain Python objects for both positive and negative paths.

Confidence Score: 5/5

Safe to merge — the fix is narrow, directly targets the reported false-negative, and does not touch any execution or scheduling paths.

The change is a self-contained guard function replacing a single hasattr call. Both PyArrow and pandas branches are tested, the logic is straightforward, and there are no side effects outside of validate_input.

No files require special attention; the two observations are minor and do not affect correctness of the fix.

Important Files Changed

Filename Overview
nemo_curator/stages/base.py Adds _has_data_attr static method that correctly checks pa.Table.column_names and pd.DataFrame.columns before falling back to hasattr; the isinstance guards contain redundant hasattr sub-checks that are safe but unnecessary.
tests/stages/common/test_base.py Adds PyArrow and pandas branch tests; the pandas acceptance test exercises the first hasattr path rather than the isinstance(pd.DataFrame) branch, leaving that block untested for positive cases.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A["_has_data_attr(data, attr)"] --> B{"hasattr(data, attr)?"}
    B -- Yes --> C["return True"]
    B -- No --> D{"isinstance(data, pa.Table)?"}
    D -- Yes --> E["attr in data.column_names"]
    E --> F["return result"]
    D -- No --> G{"isinstance(data, pd.DataFrame)?"}
    G -- Yes --> H["attr in data.columns"]
    H --> I["return result"]
    G -- No --> J["return False"]
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
    A["_has_data_attr(data, attr)"] --> B{"hasattr(data, attr)?"}
    B -- Yes --> C["return True"]
    B -- No --> D{"isinstance(data, pa.Table)?"}
    D -- Yes --> E["attr in data.column_names"]
    E --> F["return result"]
    D -- No --> G{"isinstance(data, pd.DataFrame)?"}
    G -- Yes --> H["attr in data.columns"]
    H --> I["return result"]
    G -- No --> J["return False"]
Loading

Reviews (6): Last reviewed commit: "fix: remove unreachable pyarrow schema f..." | Re-trigger Greptile

Comment thread nemo_curator/stages/base.py Outdated
Comment on lines +168 to +169
if hasattr(data, "columns"):
return attr in data.columns

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 columns branch is ambiguous for PyArrow-like types

For pa.Table, data.columns returns a list of ChunkedArray objects, not strings — so attr in data.columns would perform object-identity comparison and always return False for a plain string attribute name. This branch works today only because pa.Table also exposes column_names, which is checked first. If another table type (e.g. a future columnar format) has columns returning arrays but lacks column_names, this branch would silently misreport all columns as missing. A brief inline comment or guard (e.g. checking that the first element is a string) would make the intent and the ordering dependency explicit.

Comment on lines +279 to +298
class TestProcessingStageValidateInput:
def test_validate_input_accepts_pyarrow_columns(self):
stage = RequiredColumnStage()
batch = DocumentBatch(
task_id="batch",
dataset_name="dataset",
data=pa.table({"text": ["hello"]}),
)

assert stage.validate_input(batch) is True

def test_validate_input_rejects_missing_pyarrow_columns(self):
stage = RequiredColumnStage()
batch = DocumentBatch(
task_id="batch",
dataset_name="dataset",
data=pa.table({"other": ["hello"]}),
)

assert stage.validate_input(batch) is False

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Missing pandas DataFrame coverage

DocumentBatch.data is typed as pa.Table | pd.DataFrame, and pd.DataFrame takes a different code path through _has_data_attr (it lacks column_names, so it falls through to the columns branch). The new test class only exercises the PyArrow path. Adding parallel test_validate_input_accepts_pandas_columns / test_validate_input_rejects_missing_pandas_columns cases would ensure that both union branches stay correct and guard against a future regression in the columns fallback.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 let's try to make sure the test coverage here reaches every possible block of _has_data_attr please.

@svcnvidia-nemo-ci svcnvidia-nemo-ci added the waiting-on-maintainers Waiting on maintainers to respond label Jul 4, 2026
@sarahyurick

Copy link
Copy Markdown
Contributor

/ok to test 0a69c05

@sarahyurick

Copy link
Copy Markdown
Contributor

/ok to test 5a2e951

Comment thread nemo_curator/stages/base.py Outdated
if hasattr(data, attr):
return True

if hasattr(data, "column_names"):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should it look more like

Suggested change
if hasattr(data, "column_names"):
if isinstance(data, pa.Table) and hasattr(data, "column_names"):

?

Comment thread nemo_curator/stages/base.py Outdated
if hasattr(data, "column_names"):
return attr in data.column_names

if hasattr(data, "columns"):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if hasattr(data, "columns"):
if isinstance(data, pd.DataFrame) and hasattr(data, "columns"):

?

Comment thread nemo_curator/stages/base.py Outdated
if hasattr(data, "columns"):
return attr in data.columns

if hasattr(data, "schema") and hasattr(data.schema, "names"):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if hasattr(data, "schema") and hasattr(data.schema, "names"):
if isinstance(data, pa.Table) and hasattr(data, "schema") and hasattr(data.schema, "names"):

?

Comment thread nemo_curator/stages/base.py Outdated
return attr in data.columns

if hasattr(data, "schema") and hasattr(data.schema, "names"):
return attr in data.schema.names

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this block supposed to cover the empty table case? Like what conditions would you expect for it to get all the way down here?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bumping this comment. @nightcityblade should this condition be removed or is there a case where the function could enter this block?

Comment thread tests/stages/common/test_base.py Outdated
def test_validate_input_accepts_pyarrow_columns(self):
stage = RequiredColumnStage()
batch = DocumentBatch(
task_id="batch",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make sure you have the latest version of main:

Suggested change
task_id="batch",

we no longer pass task_id directly to create a DocumentBatch.

Comment thread tests/stages/common/test_base.py Outdated
def test_validate_input_rejects_missing_pyarrow_columns(self):
stage = RequiredColumnStage()
batch = DocumentBatch(
task_id="batch",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
task_id="batch",

Comment on lines +279 to +298
class TestProcessingStageValidateInput:
def test_validate_input_accepts_pyarrow_columns(self):
stage = RequiredColumnStage()
batch = DocumentBatch(
task_id="batch",
dataset_name="dataset",
data=pa.table({"text": ["hello"]}),
)

assert stage.validate_input(batch) is True

def test_validate_input_rejects_missing_pyarrow_columns(self):
stage = RequiredColumnStage()
batch = DocumentBatch(
task_id="batch",
dataset_name="dataset",
data=pa.table({"other": ["hello"]}),
)

assert stage.validate_input(batch) is False

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 let's try to make sure the test coverage here reaches every possible block of _has_data_attr please.

@nightcityblade

Copy link
Copy Markdown
Contributor Author

Pushed a follow-up commit for the review notes. I narrowed the fallback checks to concrete / cases, updated the construction to match current , and added coverage for the pandas path plus the explicit-attribute/unknown-attribute branches.\n\nLocal verification: All checks passed! passed. I also attempted the focused test module, but the local environment here is missing repo test dependencies ( from , plus package deps needed to import end-to-end), so I wasn’t able to run that pytest target locally before pushing.

@nightcityblade

Copy link
Copy Markdown
Contributor Author

Pushed a follow-up commit for the review notes. I narrowed the fallback checks to concrete pa.Table / pd.DataFrame cases, updated the DocumentBatch construction to match current main, and added coverage for the pandas path plus the explicit-attribute/unknown-attribute branches.

Local verification: python3 -m ruff check nemo_curator/stages/base.py tests/stages/common/test_base.py passed. I also attempted the focused test module, but the local environment here is missing repo test dependencies (ray from tests/conftest.py, plus package deps needed to import nemo_curator end-to-end), so I wasn’t able to run that pytest target locally before pushing.

@nightcityblade

Copy link
Copy Markdown
Contributor Author

Addressed the latest review note: I removed the unreachable fallback and pushed a follow-up commit, since for we already return through the branch.\n\nLocal verification: passed. I also retried
no tests ran in 0.00s, but the local environment here still fails at import time because is not installed via .

@nightcityblade

Copy link
Copy Markdown
Contributor Author

Addressed the latest review note: I removed the unreachable schema.names fallback and pushed a follow-up commit, since for pa.Table we already return through the column_names branch.

Local verification: python3 -m ruff check nemo_curator/stages/base.py tests/stages/common/test_base.py passed. I also retried pytest tests/stages/common/test_base.py -q, but the local environment here still fails at import time because ray is not installed via tests/conftest.py.

Signed-off-by: nightcityblade <nightcityblade@gmail.com>
@nightcityblade

Copy link
Copy Markdown
Contributor Author

Pushed a follow-up force-push to add the missing DCO sign-off to the latest commit, so the DCO check should be able to clear on the updated head.

@greptile-apps

greptile-apps Bot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Too many files changed for review. (1719 files found, 100 file limit)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ProcessingStage input validation fails when the input task is DocumentBatch and the data type is pyarrow

3 participants