diff --git a/superset/charts/data/api.py b/superset/charts/data/api.py index d6ddd595981..3cbeccde271 100644 --- a/superset/charts/data/api.py +++ b/superset/charts/data/api.py @@ -524,8 +524,11 @@ class ChartDataRestApi(ChartRestApi): # return multi-query results bundled as a zip file def _process_data(query_data: Any) -> Any: if result_format == ChartDataResultFormat.CSV: - encoding = app.config["CSV_EXPORT"].get("encoding", "utf-8") - return query_data.encode(encoding) + # CSV data is already encoded to bytes by the query context + # processor, honoring the CSV_EXPORT encoding config. + if isinstance(query_data, str): + encoding = app.config["CSV_EXPORT"].get("encoding", "utf-8") + return query_data.encode(encoding) return query_data files = { diff --git a/superset/common/query_context.py b/superset/common/query_context.py index ed39aff078f..bef870035c0 100644 --- a/superset/common/query_context.py +++ b/superset/common/query_context.py @@ -87,7 +87,7 @@ class QueryContext: self, df: pd.DataFrame, coltypes: list[GenericDataType], - ) -> str | list[dict[str, Any]]: + ) -> str | bytes | list[dict[str, Any]]: return self._processor.get_data(df, coltypes) def get_payload( diff --git a/superset/common/query_context_processor.py b/superset/common/query_context_processor.py index bee3d6eb0df..bd7e8c9ccbe 100644 --- a/superset/common/query_context_processor.py +++ b/superset/common/query_context_processor.py @@ -257,7 +257,7 @@ class QueryContextProcessor: def get_data( self, df: pd.DataFrame, coltypes: list[GenericDataType] - ) -> str | list[dict[str, Any]]: + ) -> str | bytes | list[dict[str, Any]]: if self._query_context.result_format in ChartDataResultFormat.table_like(): include_index = not isinstance(df.index, pd.RangeIndex) columns = list(df.columns) @@ -270,6 +270,11 @@ class QueryContextProcessor: result = csv.df_to_escaped_csv( df, index=include_index, **current_app.config["CSV_EXPORT"] ) + # Encode using the configured CSV_EXPORT encoding (default utf-8) + # so dashboard chart exports honor the same encoding as SQL Lab. + result = result.encode( + current_app.config["CSV_EXPORT"].get("encoding", "utf-8") + ) elif self._query_context.result_format == ChartDataResultFormat.XLSX: excel.apply_column_types(df, coltypes) result = excel.df_to_excel( diff --git a/tests/unit_tests/common/test_query_context_processor.py b/tests/unit_tests/common/test_query_context_processor.py index 6e4acc08b41..0272658c2d6 100644 --- a/tests/unit_tests/common/test_query_context_processor.py +++ b/tests/unit_tests/common/test_query_context_processor.py @@ -107,7 +107,9 @@ def test_get_data_csv(mock_df_to_escaped_csv, processor, mock_query_context): mock_df_to_escaped_csv.return_value = "col1,col2\n1,a\n2,b\n3,c\n" result = processor.get_data(df, coltypes) - assert result == "col1,col2\n1,a\n2,b\n3,c\n" + # CSV output is encoded to bytes using the CSV_EXPORT encoding so dashboard + # chart exports honor the configured encoding (e.g. the utf-8-sig BOM). + assert result == "col1,col2\n1,a\n2,b\n3,c\n".encode("utf-8-sig") mock_df_to_escaped_csv.assert_called_once_with( df, index=False, encoding="utf-8-sig" ) @@ -183,7 +185,7 @@ def test_get_data_empty_dataframe_csv( mock_query_context.result_format = ChartDataResultFormat.CSV mock_df_to_escaped_csv.return_value = "col1,col2\n" result = processor.get_data(df, coltypes) - assert result == "col1,col2\n" + assert result == "col1,col2\n".encode("utf-8-sig") mock_df_to_escaped_csv.assert_called_once_with( df, index=False, encoding="utf-8-sig" )