Skip to content
Open
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
1 change: 1 addition & 0 deletions metaflow/plugins/metadata_providers/local.py
Original file line number Diff line number Diff line change
Expand Up @@ -607,6 +607,7 @@ def _get_metadir(cls, flow_name=None, run_id=None, step_name=None, task_id=None)
def _dump_json_to_file(cls, filepath, data, allow_overwrite=False):
if os.path.isfile(filepath) and not allow_overwrite:
return
f = None
try:
with tempfile.NamedTemporaryFile(
mode="w", dir=os.path.dirname(filepath), delete=False
Expand Down
43 changes: 43 additions & 0 deletions test/unit/test_local_metadata_provider.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
import json
import os

import pytest

from metaflow.plugins.metadata_providers.local import LocalMetadataProvider


Expand Down Expand Up @@ -67,3 +72,41 @@ def filter_for(pattern):
assert filter_for("middle:1,.*") == ["Flow/run/middle/4"]
# and the match-all pattern keeps returning everything
assert len(filter_for(".*")) == len(paths)


def test_dump_json_to_file_reports_the_real_error_when_temp_file_fails(tmp_path):
# _dump_json_to_file opens its temp file *inside* the try, so when the
# NamedTemporaryFile constructor is the thing that fails, `f` is never
# bound and the finally clause raises UnboundLocalError over the top of
# the real OSError. The caller then has no idea what actually went wrong.
Comment on lines +78 to +81

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Comments Describe Fixed Bug

These comments describe the old masking behavior in the present tense, even though f is now initialized before the try. Rephrase them as regression history so future maintainers are not led to believe the current implementation still raises UnboundLocalError. The permission-error test below has the same stale wording.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

target = tmp_path / "missing_dir" / "_self.json"
with pytest.raises(OSError) as excinfo:
LocalMetadataProvider._dump_json_to_file(str(target), {"a": 1})
assert "missing_dir" in str(excinfo.value)


def test_dump_json_to_file_reports_permission_error(tmp_path, mocker):
# The same masking happens for an unwritable metadata directory, which is
# the case a user is most likely to hit (read-only mount, .metaflow owned
# by another account).
mocker.patch(
"metaflow.plugins.metadata_providers.local.tempfile.NamedTemporaryFile",
side_effect=PermissionError(13, "Permission denied"),
)
with pytest.raises(PermissionError):
LocalMetadataProvider._dump_json_to_file(str(tmp_path / "_self.json"), {"a": 1})


def test_dump_json_to_file_writes_and_leaves_no_temp_file(tmp_path):
target = tmp_path / "_self.json"
LocalMetadataProvider._dump_json_to_file(str(target), {"a": 1})
assert json.loads(target.read_text()) == {"a": 1}
assert os.listdir(str(tmp_path)) == ["_self.json"]


def test_dump_json_to_file_does_not_overwrite_by_default(tmp_path):
target = tmp_path / "_self.json"
LocalMetadataProvider._dump_json_to_file(str(target), {"a": 1})
LocalMetadataProvider._dump_json_to_file(str(target), {"a": 2})
assert json.loads(target.read_text()) == {"a": 1}
assert os.listdir(str(tmp_path)) == ["_self.json"]