Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 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
2 changes: 2 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ New features

Changes
-------
- The :meth: `DataOp.skb.full_report` method now deletes reports created with
``output_dir=None`` after 7 days. :pr:`1657` by :user: `Simon Dierickx <simon.dierickx>`.
- The :func: `tabular_pipeline` uses a :class:`SquashingScaler` instead of a
:class:`StandardScaler` for centering and scaling numerical features
when linear models are used.
Expand Down
8 changes: 3 additions & 5 deletions skrub/_data_ops/_inspection.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,11 +80,9 @@ def node_report(data_op, mode="preview", environment=None, **report_kwargs):
def _get_output_dir(output_dir, overwrite):
now = datetime.datetime.now().strftime("%Y-%m-%dT%H%M%S")
if output_dir is None:
output_dir = (
datasets.get_data_dir()
/ "execution_reports"
/ f"full_data_op_report_{now}_{random_string()}"
)
output_base_dir = datasets.get_data_dir() / "execution_reports"
output_dir = output_base_dir / f"full_data_op_report_{now}_{random_string()}"
_utils.prune_folder(output_base_dir)
else:
output_dir = Path(output_dir).expanduser().resolve()
if output_dir.exists():
Expand Down
14 changes: 12 additions & 2 deletions skrub/_data_ops/_skrub_namespace.py
Original file line number Diff line number Diff line change
Expand Up @@ -1472,7 +1472,15 @@ def full_report(
This creates a report showing the computation graph, and for each
intermediate computation, some information (the line of code where it
was defined, the time it took to run, and more) and a display of the
intermediate result (or error).
intermediate result (or error). By default, the report is stored in
a timestamped subdirectory of the skrub data folder.

.. note::
When this function is invoked reports starting with ``full_data_op_report_``
that are stored in the skrub data folder are automatically deleted after 7 days.
This is to avoid accumulating too many reports over time. If you want to keep
specific reports, please specify an output directory.


Parameters
----------
Expand All @@ -1486,7 +1494,9 @@ def full_report(

output_dir : str or Path or None (default=None)
Directory where to store the report. If ``None``, a timestamped
subdirectory will be created in the skrub data directory.
subdirectory will be created in the skrub data directory. Note
that the reports created with ``output_dir=None`` are automatically
deleted after 7 days.

overwrite : bool (default=False)
What to do if the output directory already exists. If
Expand Down
26 changes: 26 additions & 0 deletions skrub/_data_ops/_utils.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import enum
import os
import shutil
import time
import traceback
import warnings

from joblib.externals import cloudpickle

Expand Down Expand Up @@ -62,3 +66,25 @@ def format_exception(e):
def format_exception_only(e):
"""compatibility for python < 3.10"""
return traceback.format_exception_only(type(e), e)


def prune_folder(path: str):
if not os.path.exists(path):
return
time_threshold = time.time() - 7 * 24 * 3600 # 7 days ago

folders = (

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

nitpick: the rest of skrub uses the (newer) pathlib module for those kind of filesystem manipulation rather than os, so for consistency it would be great to use it here, especially since the path you get from _get_output_dir is already a pathlib.Path

for example you can get the mtime with stat, the Path object representing the directory has iterdir() and glob() methods, you can join with the / operator or joinpath, etc.

also maybe we could name folders directories instead? since we use the term 'directory' rather than 'folder' for other variables (same for the function name prune_folder)

finally why do you prefer 2 separate loops for finding the directories and then removing them?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

totally agree with the variable naming — I’ll change it, and I’ll switch to using the pathlib module as well. As for the two separate loops: I initially used them for debugging purposes so I could easily print all the folders that were selected. But maybe its better to and even more readable to put it all in one loop. I will change it!

(f, os.path.getmtime(os.path.join(path, f)))

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

All tests are passing except the codecov (not enough test coverage). In the test cov report these lines (line 73-77) are not covered. Maybe because assignment of the variable is only done on line 73.

for f in os.listdir(path)
if os.path.isdir(os.path.join(path, f)) and f.startswith("full_data_op_report_")
)
for dir_path, dir_time in folders:
try:
if dir_time < time_threshold:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

i guess the test could be outside the try block

shutil.rmtree(os.path.join(path, dir_path))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I wonder if we should have an even stricter check of the name (eg check with a regex there is a timestamp) just to be extra sure we don't erase something we shouldn't ... but I guess "full_data_op_report_" is already very specific 🤔

except Exception as e:
warnings.warn(
"Skrub wants to delete an old folder in the skrub data folder: "
f"Could not delete {dir_path}:\n"
+ "".join(format_exception_only(e))
)
Empty file.
61 changes: 61 additions & 0 deletions skrub/_data_ops/tests/test_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import os
import tempfile
import time

import pytest

from skrub._data_ops import _utils


def test_prune_folder_with_standard_name_dirs():
eight_days_ago = time.time() - 8 * 24 * 3600
with tempfile.TemporaryDirectory() as tmpdir:
for i in range(3):
dirname = os.path.join(tmpdir, f"full_data_op_report_{i}")
os.mkdir(dirname)
if i < 2:
Comment thread
dierickxsimon marked this conversation as resolved.
Outdated
os.utime(dirname, (eight_days_ago, eight_days_ago))

assert len(os.listdir(tmpdir)) == 3

_utils.prune_folder(tmpdir)

remaining_items = os.listdir(tmpdir)
assert len(remaining_items) == 1


def test_prune_folder_with_nonstandard_name_dirs():
eight_days_ago = time.time() - 8 * 24 * 3600
with tempfile.TemporaryDirectory() as tmpdir:
dirname = os.path.join(tmpdir, "other_report")
os.mkdir(dirname)
os.utime(dirname, (eight_days_ago, eight_days_ago))
Comment thread
dierickxsimon marked this conversation as resolved.
Outdated

assert len(os.listdir(tmpdir)) == 1

_utils.prune_folder(tmpdir)

remaining_items = os.listdir(tmpdir)
assert len(remaining_items) == 1
Comment thread
dierickxsimon marked this conversation as resolved.
Outdated
assert remaining_items[0] == "other_report"


def test_prune_folder_catch_exception(monkeypatch):
eight_days_ago = time.time() - 8 * 24 * 3600

with tempfile.TemporaryDirectory() as tmpdir:
dirname = os.path.join(tmpdir, "full_data_op_report_test")
os.mkdir(dirname)
os.utime(dirname, (eight_days_ago, eight_days_ago))

def mock_rmtree(path, *args, **kwargs):
raise OSError("Cannot delete folder")

monkeypatch.setattr("shutil.rmtree", mock_rmtree)

assert len(os.listdir(tmpdir)) == 1

with pytest.warns(UserWarning, match="Could not delete"):
_utils.prune_folder(tmpdir)

monkeypatch.undo()