Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 26 additions & 1 deletion spotfire/data_function.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import types
import typing
import re
import warnings

from spotfire import sbdf, _utils

Expand All @@ -25,6 +26,7 @@


_COLUMN_METADATA_TRUNCATE_THRESHOLD = 80000
_MAX_WARNINGS = 100


def _bad_string(str_: typing.Any) -> bool:
Expand Down Expand Up @@ -220,15 +222,19 @@ def write(self, globals_dict: _Globals, debug_fn: _LogFunction) -> None:

class AnalyticResult:
"""Represents the results of evaluating an AnalyticSpec object."""
# pylint: disable=too-many-instance-attributes
std_err_out: typing.Optional[str]
warnings: list[str]
summary: typing.Optional[str]
_exc_info: _ExceptionInfo
_debug_log: typing.Optional[str]

def __init__(self, capture: _OutputCapture) -> None:
self.success = True
self.has_stderr = False
self.has_warnings = False
self.std_err_out = None
self.warnings = []
self.summary = None
self._exc_info = (None, None, None)
self._capture = capture
Expand Down Expand Up @@ -413,9 +419,25 @@ def _execute_script(self, compiled_script: types.CodeType, result: AnalyticResul
if self.analytic_type == "script":
# noinspection PyBroadException
try:
exec(compiled_script, self.globals)
with warnings.catch_warnings(record=True) as caught_warnings:
exec(compiled_script, self.globals)
except BaseException:
result.fail_with_exception(sys.exc_info())
Comment thread
vrane-tibco marked this conversation as resolved.
Comment thread
vrane-tibco marked this conversation as resolved.
if caught_warnings:
result.has_warnings = True
total = len(caught_warnings)
if total > _MAX_WARNINGS:
result.warnings = [
warnings.formatwarning(w.message, w.category, w.filename, w.lineno, w.line)
for w in caught_warnings[:_MAX_WARNINGS - 1]
]
result.warnings.append(f"... and {total - _MAX_WARNINGS + 1} more warnings (truncated)\n")
else:
result.warnings = [
warnings.formatwarning(w.message, w.category, w.filename, w.lineno, w.line)
for w in caught_warnings
]
if not result.success:
return
elif self.analytic_type == "aggregationScript":
self.debug("aggregation scripts are not supported")
Expand Down Expand Up @@ -489,6 +511,9 @@ def _create_summary(self, result: AnalyticResult) -> None:
result.has_stderr = True
buf.write("\nStandard error:\n")
buf.write(result.std_err_out)
if result.has_warnings:
buf.write("\nWarnings:\n")

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Do we need more stronger protocol identification ? If I print warnings in the log/print(Warnings:) will it create problem ?

@vrane-tibco vrane-tibco Jul 17, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

This is added to show warnings at client when enable debug logging for data function, along with other debug logs warning also get displayed. Python service or client glue code will retrieve warnings from AnalyticResult.warnings (field in AnalyticResult class)
We may remove this "Warnings:" from debug logging once client starts showing warnings for python data functions.

buf.writelines(result.warnings)
if self.debug_enabled:
debug_log = result.get_debug_log()
if debug_log:
Expand Down
2 changes: 1 addition & 1 deletion spotfire/test/files/data_function/table_metadata.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@

Standard error:
Warnings:
<data_function>:3: UserWarning: Pandas doesn't allow columns to be created via a new attribute name - see https://pandas.pydata.org/pandas-docs/stable/indexing.html#attribute-access
7 changes: 7 additions & 0 deletions spotfire/test/files/data_function/warning_per_row.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@

Warnings:
<data_function>:4: UserWarning: bad value at row 0
<data_function>:4: UserWarning: bad value at row 1
<data_function>:4: UserWarning: bad value at row 2
<data_function>:4: UserWarning: bad value at row 3
<data_function>:4: UserWarning: bad value at row 4
2 changes: 1 addition & 1 deletion spotfire/test/files/data_function/warning_pysrv79.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@

Standard error:
Warnings:
<data_function>:4: Warning: This is a Warning
<data_function>:5: UserWarning: This is a UserWarning
<data_function>:6: DeprecationWarning: This is a DeprecationWarning
Expand Down
44 changes: 41 additions & 3 deletions spotfire/test/test_data_function.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ class DataFunctionTest(unittest.TestCase):
"""Unit tests for public functions in 'spotfire.data_function' module."""
# pylint: disable=too-many-branches, too-many-statements, too-many-arguments, too-many-public-methods

def _run_analytic(self, script, inputs, outputs, success, expected_result, spec_adjust=None) -> None:
def _run_analytic(self, script, inputs, outputs, success, expected_result, spec_adjust=None,
has_warnings=None, has_stderr=None) -> None:
"""Run a full pass through the analytic protocol, and compare the output to the expected value."""
# pylint: disable=too-many-positional-arguments,protected-access,too-many-locals
with _utils.TempFiles() as temp_files:
Expand Down Expand Up @@ -106,6 +107,10 @@ def _run_analytic(self, script, inputs, outputs, success, expected_result, spec_
if not actual_result.success:
print("test: data function has failed")
self.assertEqual(actual_result.success, success)
if has_warnings is not None:
self.assertEqual(actual_result.has_warnings, has_warnings)
if has_stderr is not None:
self.assertEqual(actual_result.has_stderr, has_stderr)
print("test: done evaluating spec")

for output in output_spec:
Expand Down Expand Up @@ -249,7 +254,7 @@ def test_exception_pysrv78(self):
print("But not this.")""", {}, {}, False, expected)

def test_warning_pysrv79(self):
"""Test that warnings are returned."""
"""Test that warnings are returned separately from stderr."""
expected = _PythonVersionedExpectedValue("warning_pysrv79")
self._run_analytic("""import warnings
warnings.simplefilter("always")
Expand All @@ -264,7 +269,40 @@ def test_warning_pysrv79(self):
warn("This is a ImportWarning", ImportWarning)
warn("This is a UnicodeWarning", UnicodeWarning)
warn("This is a BytesWarning", BytesWarning)
warn("This is a ResourceWarning", ResourceWarning)""", {}, {}, True, expected)
warn("This is a ResourceWarning", ResourceWarning)""", {}, {}, True, expected,
has_warnings=True, has_stderr=False)


def test_warning_per_row(self):
"""Test that unique per-row warnings are each captured individually."""
in1_df = pd.DataFrame({"a": pd.array([1, 2, 3, 4, 5], dtype="Int64")})
expected = _PythonVersionedExpectedValue("warning_per_row")
Comment thread
vrane-tibco marked this conversation as resolved.
self._run_analytic("""import warnings
warnings.simplefilter("always")
for idx, row in in1.iterrows():
warnings.warn(f"bad value at row {idx}")
output = in1""", {"in1": in1_df}, {"output": in1_df}, True, expected,
has_warnings=True, has_stderr=False)

def test_warning_truncated(self):
"""Test that warnings are truncated to _MAX_WARNINGS."""
spec = datafn.AnalyticSpec("script", [], [], """import warnings
warnings.simplefilter("always")
for i in range(150):
warnings.warn(f"warning {i}")""")
result = spec.evaluate()
self.assertTrue(result.has_warnings)
self.assertEqual(len(result.warnings), 100)
self.assertIn("51 more warnings (truncated)", result.warnings[-1])

def test_warning_suppressed(self):
"""Test that warnings.simplefilter('ignore') suppresses warning capture."""
self._run_analytic("""import warnings
warnings.simplefilter("ignore")
import pandas as pd
df = pd.DataFrame({"x": [1, 2, 3]})
df.new_col = [4, 5, 6]""", {}, {}, True, None,
has_warnings=False, has_stderr=False)

def test_stderr_pysrv116(self):
"""Test that stdout is returned correctly with stderr"""
Expand Down
6 changes: 4 additions & 2 deletions spotfire/test/test_sbdf.py
Original file line number Diff line number Diff line change
Expand Up @@ -445,7 +445,8 @@ def test_numpy_datetime_resolution(self):
}
for resolution, timestamp in inputs.items():
with self.subTest(resolution=resolution):
array = np.array([[0], [timestamp]]).astype(f"datetime64[{resolution}]")
array: np.ndarray = np.array([[0], [timestamp]])
array = array.astype(f"datetime64[{resolution}]")
dataframe = pd.DataFrame(array, columns=["x"])
df2 = self._roundtrip_dataframe(dataframe)
val = df2.at[1, 'x']
Expand All @@ -462,7 +463,8 @@ def test_numpy_timedelta_resolution(self):
}
for resolution, timestamp in inputs.items():
with self.subTest(resolution=resolution):
array = np.array([[0], [timestamp]]).astype(f"timedelta64[{resolution}]")
array: np.ndarray = np.array([[0], [timestamp]])
array = array.astype(f"timedelta64[{resolution}]")
dataframe = pd.DataFrame(array, columns=["x"])
df2 = self._roundtrip_dataframe(dataframe)
val = df2.at[1, 'x']
Expand Down
Loading