diff --git a/src/datasure/processing/prep.py b/src/datasure/processing/prep.py index 16c4d8cc..d9984d8a 100644 --- a/src/datasure/processing/prep.py +++ b/src/datasure/processing/prep.py @@ -193,7 +193,8 @@ def execute( "action": PrepActions.remove_column.value, "column_names": None, "affected_count": len(existing_columns), - "remaining_count": results.width, + "remaining_rows": results.height, + "remaining_columns": results.width, "value": None, "method": None, "source_columns": existing_columns, @@ -240,7 +241,8 @@ def execute( "action": PrepActions.remove_row.value, "column_names": None, "affected_count": data.height - results.height, - "remaining_count": results.height, + "remaining_rows": results.height, + "remaining_columns": results.width, "value": value, "method": method, "source_columns": source_columns, @@ -442,7 +444,8 @@ def execute( "action": "transform column(s)", "column_names": None, "affected_count": affected_count, - "remaining_count": None, + "remaining_rows": result_data.height, + "remaining_columns": result_data.width, "value": prep_args.value, "method": prep_args.method, "source_columns": source_columns, @@ -738,7 +741,8 @@ def execute(self, data: pl.DataFrame, prep_args: PrepActionResult) -> pl.DataFra "action": PrepActions.add_column.value, "column_names": new_col_name, "affected_count": 1, - "remaining_count": results.width, + "remaining_rows": results.height, + "remaining_columns": results.width, "value": value_spec, "method": method, "source_columns": source_columns, diff --git a/src/datasure/utils/prep_utils.py b/src/datasure/utils/prep_utils.py index b5fc3160..0564c54c 100644 --- a/src/datasure/utils/prep_utils.py +++ b/src/datasure/utils/prep_utils.py @@ -18,6 +18,8 @@ class PrepActionResult: column_names: str | list[str] | None = None affected_count: int | None = None remaining_count: int | None = None + remaining_rows: int | None = None + remaining_columns: int | None = None value: str | list | None = None method: str | None = None source_columns: str | list[str] | None = None @@ -215,6 +217,79 @@ def _pluralize(count: int | None, singular: str, plural: str | None = None) -> s plural = f"{singular}s" return singular if count == 1 else plural + @classmethod + def _format_remaining( + cls, remaining_rows: int | None, remaining_columns: int | None + ) -> str: + """Build the standard 'dataset now has N rows and M columns' clause.""" + parts = [] + if remaining_rows is not None: + parts.append(f"{remaining_rows} {cls._pluralize(remaining_rows, 'row')}") + if remaining_columns is not None: + parts.append( + f"{remaining_columns} {cls._pluralize(remaining_columns, 'column')}" + ) + if not parts: + return "" + return f"Dataset now has {' and '.join(parts)}." + + @classmethod + def _with_remaining(cls, message: str, result: PrepActionResult) -> str: + """Append the standard remaining rows/columns clause to a message.""" + remaining = cls._format_remaining( + result.remaining_rows, result.remaining_columns + ) + return f"{message} {remaining}".rstrip() if remaining else message + + @staticmethod + def _quote_scalar(value: object) -> str: + """Quote string values for display; leave other types as-is.""" + return f'"{value}"' if isinstance(value, str) else str(value) + + @classmethod + def _format_row_condition_detail( + cls, + source_columns: str | list[str] | None, + condition: str | None, + value: object, + ) -> str: + """Build a human-readable 'where ' clause.""" + column_display = cls._format_column_names(source_columns) + if not condition: + return "matching the specified criteria" + + if condition in ( + PrepRowConditions.missing.value, + PrepRowConditions.not_missing.value, + ): + value_text = "" + elif condition in ( + PrepRowConditions.between.value, + PrepRowConditions.not_between.value, + ): + values = value if isinstance(value, list) else [value, value] + value_text = ( + f" {cls._quote_scalar(values[0])} and {cls._quote_scalar(values[1])}" + ) + elif isinstance(value, list): + value_text = " " + ", ".join(cls._quote_scalar(v) for v in value) + elif value is not None: + value_text = f" {cls._quote_scalar(value)}" + else: + value_text = "" + + if column_display: + return f"where {column_display} {condition}{value_text}" + return f"where {condition}{value_text}" + + @staticmethod + def _format_row_index_detail(value: object) -> str: + """Build a human-readable clause describing removed row indexes.""" + if not value: + return "at the specified row index" + items = value if isinstance(value, list) else [value] + return f"at row index {', '.join(str(v) for v in items)}" + # Main Actions @classmethod def transform_columns(cls, result: PrepActionResult) -> str: @@ -223,10 +298,11 @@ def transform_columns(cls, result: PrepActionResult) -> str: row_text = cls._pluralize(result.affected_count, "row") method = result.method or "unknown method" affected_count = result.affected_count or 0 - return ( + message = ( f"✓ Column transformation applied. {column_display} updated using " f"{method}. {affected_count} {row_text} affected." ) + return cls._with_remaining(message, result) @classmethod def add_new_column(cls, result: PrepActionResult) -> str: @@ -236,12 +312,11 @@ def add_new_column(cls, result: PrepActionResult) -> str: if result.source_columns else "specified parameters" ) - column_text = cls._pluralize(result.remaining_count, "column") - return ( + message = ( f"✓ New column {cls._format_column_names(result.column_names)} added. " - f"Created using {result.method} from {source_display}. " - f"Your dataset now has {result.remaining_count} {column_text}." + f"Created using {result.method} from {source_display}." ) + return cls._with_remaining(message, result) @classmethod def remove_columns(cls, result: PrepActionResult) -> str: @@ -258,26 +333,27 @@ def remove_columns(cls, result: PrepActionResult) -> str: else (len(removed_columns) if isinstance(removed_columns, list) else 1) ) column_text = cls._pluralize(column_count, "column") - remaining_text = cls._pluralize(result.remaining_count, "column") column_display = cls._format_column_names(removed_columns) message = ( f"✓ {column_count} {column_text} removed. {column_display} deleted from " - f"your dataset. {result.remaining_count} {remaining_text} remaining." + "your dataset." ) if result.failed_count: message = f"{message} {result.additional_info}" - return message + return cls._with_remaining(message, result) @classmethod def remove_rows(cls, result: PrepActionResult) -> str: """Generate message for removing rows.""" row_text = cls._pluralize(result.affected_count, "row") - remaining_text = cls._pluralize(result.remaining_count, "row") - method_text = result.method if result.method else "specified criteria" - return ( - f"✓ {result.affected_count} {row_text} removed. Deleted rows {method_text}. " - f"{result.remaining_count} {remaining_text} remaining in your dataset." - ) + if result.method == PrepMethods.row_index.value: + detail = cls._format_row_index_detail(result.value) + else: + detail = cls._format_row_condition_detail( + result.source_columns, result.condition, result.value + ) + message = f"✓ {result.affected_count} {row_text} removed {detail}." + return cls._with_remaining(message, result) # Add Column Methods @classmethod diff --git a/tests/utils/test_prep_utils.py b/tests/utils/test_prep_utils.py index d9c17da3..2ad43b8c 100644 --- a/tests/utils/test_prep_utils.py +++ b/tests/utils/test_prep_utils.py @@ -404,6 +404,8 @@ def test_transform_columns_message(self): source_columns=["column1"], affected_count=100, method="trim", + remaining_rows=500, + remaining_columns=12, ) message = PrepConfirmationMessages.transform_columns(result) @@ -412,6 +414,7 @@ def test_transform_columns_message(self): assert '"column1"' in message assert "trim" in message assert "100 rows affected" in message + assert "500 rows and 12 columns" in message def test_transform_columns_message_no_method(self): """Test transform_columns message with no method.""" @@ -432,7 +435,8 @@ def test_add_new_column_message(self): result = PrepActionResult( action="add new column", column_names="new_col", - remaining_count=10, + remaining_rows=200, + remaining_columns=10, method="constant", source_columns=["source_col"], ) @@ -442,14 +446,15 @@ def test_add_new_column_message(self): assert "✓ New column" in message assert '"new_col"' in message assert "constant" in message - assert "10 columns" in message + assert "200 rows and 10 columns" in message def test_add_new_column_message_no_source(self): """Test add_new_column message with no source columns.""" result = PrepActionResult( action="add new column", column_names="new_col", - remaining_count=5, + remaining_rows=5, + remaining_columns=5, method="index", source_columns=None, ) @@ -463,25 +468,29 @@ def test_remove_columns_message(self): result = PrepActionResult( action="remove column(s)", source_columns=["col1", "col2"], - remaining_count=8, + remaining_rows=50, + remaining_columns=8, ) message = PrepConfirmationMessages.remove_columns(result) assert "✓ 2 columns removed" in message assert '"col1", "col2"' in message - assert "8 columns remaining" in message + assert "50 rows and 8 columns" in message def test_remove_columns_message_single_column(self): """Test remove_columns message with single column.""" result = PrepActionResult( - action="remove column(s)", source_columns="single_col", remaining_count=5 + action="remove column(s)", + source_columns="single_col", + remaining_rows=50, + remaining_columns=5, ) message = PrepConfirmationMessages.remove_columns(result) assert "✓ 1 column removed" in message - assert "5 columns remaining" in message + assert "50 rows and 5 columns" in message def test_remove_columns_message_partial_failure(self): """Test remove_columns message notes columns skipped as missing.""" @@ -489,7 +498,8 @@ def test_remove_columns_message_partial_failure(self): action="remove column(s)", source_columns=["col1", "col2"], affected_count=2, - remaining_count=8, + remaining_rows=50, + remaining_columns=8, failed_count=2, additional_info="Columns not found and skipped: ['col3', 'col4']", ) @@ -500,25 +510,67 @@ def test_remove_columns_message_partial_failure(self): assert '"col1", "col2"' in message assert "Columns not found and skipped: ['col3', 'col4']" in message - def test_remove_rows_message(self): - """Test remove_rows message generation.""" + def test_remove_rows_message_by_condition(self): + """Test remove_rows message states the column, condition, and value.""" result = PrepActionResult( action="remove row(s)", affected_count=25, - remaining_count=75, + remaining_rows=75, + remaining_columns=10, method="by condition", + source_columns=["age"], + condition="value is greater than", + value=65, ) message = PrepConfirmationMessages.remove_rows(result) assert "✓ 25 rows removed" in message - assert "by condition" in message - assert "75 rows remaining" in message + assert '"age" value is greater than 65' in message + assert "75 rows and 10 columns" in message + + def test_remove_rows_message_by_condition_between(self): + """Test remove_rows message formats a between-condition value range.""" + result = PrepActionResult( + action="remove row(s)", + affected_count=5, + remaining_rows=95, + remaining_columns=10, + method="by condition", + source_columns=["age"], + condition="value is between", + value=[18, 65], + ) + + message = PrepConfirmationMessages.remove_rows(result) + + assert '"age" value is between 18 and 65' in message - def test_remove_rows_message_no_method(self): - """Test remove_rows message with no method.""" + def test_remove_rows_message_by_index(self): + """Test remove_rows message for row-index removal states the indexes.""" result = PrepActionResult( - action="remove row(s)", affected_count=10, remaining_count=90, method=None + action="remove row(s)", + affected_count=3, + remaining_rows=97, + remaining_columns=10, + method="by row index", + value=["1", "5", "10"], + ) + + message = PrepConfirmationMessages.remove_rows(result) + + assert "✓ 3 rows removed" in message + assert "at row index 1, 5, 10" in message + assert "97 rows and 10 columns" in message + + def test_remove_rows_message_no_condition(self): + """Test remove_rows message with no condition falls back gracefully.""" + result = PrepActionResult( + action="remove row(s)", + affected_count=10, + remaining_rows=90, + remaining_columns=10, + method=None, ) message = PrepConfirmationMessages.remove_rows(result)