pandas-dev/pandas · #71328

ENH: warn when to_json/to_xml compression is ignored without a path (GH#32326)

jbrockmendel · merged Oct 25, 20266 files · 59 + / 7 −
doc/source/whatsnew/v3.2.0.rst1 + / 0 −
@@ -32,6 +32,7 @@ Other enhancements - :func:`read_csv` with the ``"c"`` and ``"python"`` engines now accepts a :class:`DatetimeTZDtype` in ``dtype`` (:issue:`24542`) - :meth:`.DataFrameGroupBy.rank` and :meth:`.SeriesGroupBy.rank` now accept a ``numeric_only`` argument, matching :meth:`DataFrame.rank` (:issue:`44438`) - :meth:`DataFrame.to_csv` and other writers now accept an ``os.PathLike`` such as :class:`pathlib.Path` for the ``archive_name`` entry of a zip or tar ``compression`` dict (:issue:`31934`)+- :meth:`DataFrame.to_json`, :meth:`Series.to_json` and :meth:`DataFrame.to_xml` now warn when a compression method is requested but no path is given, since the returned string is not compressed, and raise for an unrecognized ``compression`` as they do with a path (:issue:`32326`) - :meth:`Series.groupby` now accepts ``as_index=False``, returning aggregation results as a :class:`DataFrame` with the group labels as columns (:issue:`36507`) - Added :meth:`Categorical.to_dense` and :meth:`Series.cat.to_dense` to get the values with the dtype of the categories, including extension dtypes (:issue:`26410`) - Added :meth:`DatetimeIndex.set_freq` and :meth:`TimedeltaIndex.set_freq`, which return a new index with the given ``freq`` instead of modifying it in place (:issue:`61094`)
pandas/io/common.py25 + / 6 −
@@ -354,11 +354,9 @@ def _get_filepath_or_buffer(     compression_method = infer_compression(filepath_or_buffer, compression_method)      # GH21227 internal compression is not used for non-binary handles.-    if compression_method and hasattr(filepath_or_buffer, "write") and "b" not in mode:-        warnings.warn(-            "compression has no effect when passing a non-binary object as input.",-            RuntimeWarning,-            stacklevel=find_stack_level(),+    if hasattr(filepath_or_buffer, "write") and "b" not in mode:+        warn_unused_compression(+            compression_method, "passing a non-binary object as input"         )         compression_method = None @@ -554,7 +552,7 @@ def file_path_to_url(path: str) -> str:   def get_compression_method(-    compression: CompressionOptions,+    compression: str | CompressionDict | None, ) -> tuple[str | None, CompressionDict]:     """     Simplifies a compression argument to a compression method string and@@ -659,6 +657,27 @@ def infer_compression(     raise ValueError(msg)  +def warn_unused_compression(+    compression: str | CompressionDict | None,+    reason: str = "no path or buffer is given; the returned string is not compressed",+) -> None:+    """+    Warn that ``compression`` has no effect because of ``reason``.++    An unrecognized compression method still raises, as it would if it were used.+    """+    compression_method, _ = get_compression_method(compression)+    if compression_method is None or compression_method == "infer":+        return+    # the path is only consulted for "infer"; here this only validates the method+    infer_compression("", compression_method)+    warnings.warn(+        f"compression has no effect when {reason}.",+        RuntimeWarning,+        stacklevel=find_stack_level(),+    )++ def check_parent_directory(path: Path | str) -> None:     """     Check if parent directory of a file exists, raise OSError if it does not
pandas/io/formats/xml.py5 + / 1 −
@@ -18,7 +18,10 @@ from pandas.core.dtypes.common import is_list_like from pandas.core.dtypes.missing import isna -from pandas.io.common import get_handle+from pandas.io.common import (+    get_handle,+    warn_unused_compression,+) from pandas.io.xml import get_data_from_filepath  if TYPE_CHECKING:@@ -354,6 +357,7 @@ def write_output(self) -> str | None:             return None          else:+            warn_unused_compression(self.compression)             return xml_doc.decode(self.encoding).rstrip()  
pandas/io/json/_json.py2 + / 0 −
@@ -67,6 +67,7 @@     get_handle,     is_potential_multi_index,     stringify_path,+    warn_unused_compression, ) from pandas.io.json._normalize import convert_to_line_delimits from pandas.io.json._table_schema import (@@ -269,6 +270,7 @@ def to_json(         ) as handles:             handles.handle.write(s)     else:+        warn_unused_compression(compression)         return s     return None 
pandas/tests/io/json/test_compression.py16 + / 0 −
@@ -81,6 +81,22 @@ def test_write_unsupported_compression_type(temp_file):         df.to_json(temp_file, compression="unsupported")  +def test_to_json_compression_without_path():+    # GH#32326+    df = pd.DataFrame({"A": [1]})+    msg = "compression has no effect when no path or buffer is given"+    with tm.assert_produces_warning(RuntimeWarning, match=msg):+        result = df.to_json(compression="gzip")+    assert result == df.to_json()+    with tm.assert_produces_warning(RuntimeWarning, match=msg):+        df["A"].to_json(compression={"method": "gzip", "mtime": 1})+    with tm.assert_produces_warning(None):+        df.to_json(compression="infer")+        df.to_json(compression=None)+    with pytest.raises(ValueError, match="Unrecognized compression type: unsupported"):+        df.to_json(compression="unsupported")++ def test_read_unsupported_compression_type(temp_file):     msg = "Unrecognized compression type: unsupported"     with pytest.raises(ValueError, match=msg):
pandas/tests/io/xml/test_to_xml.py10 + / 0 −
@@ -1375,6 +1375,16 @@ def test_unsupported_compression(parser, geom_df, temp_file):         geom_df.to_xml(path, parser=parser, compression="7z")  +def test_compression_without_path(parser, geom_df):+    # GH#32326+    msg = "compression has no effect when no path or buffer is given"+    with tm.assert_produces_warning(RuntimeWarning, match=msg):+        result = geom_df.to_xml(parser=parser, compression="gzip")+    assert result == geom_df.to_xml(parser=parser)+    with pytest.raises(ValueError, match="Unrecognized compression type"):+        geom_df.to_xml(parser=parser, compression="7z")++ # STORAGE OPTIONS