Skip to content

Commit

Permalink
[FIX]: Attempt creation of symlink on Windows and handle exceptions. (#…
Browse files Browse the repository at this point in the history
…958)

Co-authored-by: Bradley Dice <bdice@bradleydice.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
  • Loading branch information
3 people committed Nov 16, 2023
1 parent 249fcc9 commit 7884ebd
Show file tree
Hide file tree
Showing 5 changed files with 87 additions and 40 deletions.
1 change: 1 addition & 0 deletions changelog.txt
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ Changed
+++++++

- linked views now can contain spaces and other characters except directory separators (#926).
- linked views now can be created on Windows, if 'Developer mode' is enabled (#430).

[2.1.0] -- 2023-07-12
---------------------
Expand Down
4 changes: 4 additions & 0 deletions contributors.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -138,4 +138,8 @@ contributors:
family-names: Kadar
given-names: Alain
affiliation: "University of Michigan"
-
family-names: Stoimenov
given-names: Boyko
affiliation: "JTEKT Corp."
...
43 changes: 32 additions & 11 deletions signac/linked_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import logging
import os
import sys
import textwrap
from itertools import chain

from ._utility import _mkdir_p
Expand Down Expand Up @@ -38,21 +39,15 @@ def create_linked_view(project, prefix=None, job_ids=None, path=None):
Raises
------
OSError
Linked views cannot be created on Windows because
symbolic links are not supported by the platform.
If symbolic links are not enabled on Windows,
linked views cannot be created.
RuntimeError
When state points contain ``os.sep``.
"""
from .import_export import _check_directory_structure_validity, _make_path_function

# Windows does not support the creation of symbolic links.
if sys.platform == "win32":
raise OSError(
"signac cannot create linked views on Windows, because "
"symbolic links are not supported by the platform."
)

if prefix is None:
prefix = "view"

Expand Down Expand Up @@ -85,8 +80,34 @@ def create_linked_view(project, prefix=None, job_ids=None, path=None):
for job in project.find_jobs():
links["./job"] = job.path
assert len(links) < 2
_check_directory_structure_validity(links.keys())
_update_view(prefix, links)

# Updating the view will fail on Windows, if symlinks are not enabled.
# Before re-raising the exception, print a helpful message for the expected error.
try:
_check_directory_structure_validity(links.keys())
_update_view(prefix, links)
except OSError as err:
if sys.platform == "win32" and err.winerror == 1314:
print(
textwrap.dedent(
f"""\
-------------------------------------------------------------------
Error:
{err.strerror}
You may not have permission to create Windows symlinks.
To enable the creation of symlinks on Windows you need
to enable 'Developer mode' (requires administrative rights).
To enable 'Developer mode':
1. Go to 'Settings'.
2. In the search bar type 'Use developer features'.
3. Enable the item 'Developer mode'.
The details for Home edition and between Windows versions may vary.
-------------------------------------------------------------------
"""
)
)
raise err.with_traceback(sys.exc_info()[2])

return links

Expand Down
66 changes: 45 additions & 21 deletions tests/test_project.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Copyright (c) 2017 The Regents of the University of Michigan
# All rights reserved.
# This software is licensed under the BSD 3-Clause License.
import functools
import gzip
import io
import json
Expand Down Expand Up @@ -61,10 +62,31 @@
except ImportError:
NUMPY = False

# Skip linked view tests on Windows
WINDOWS = sys.platform == "win32"


@functools.lru_cache
def _check_symlinks_supported():
"""Check if symlinks are supported on the current platform."""
try:
with TemporaryDirectory() as tmp_dir:
os.symlink(
os.path.realpath(__file__), os.path.join(tmp_dir, "test_symlink")
)
return True
except (NotImplementedError, OSError):
return False


def skip_windows_without_symlinks(test_func):
"""Skip test if platform is Windows and symlinks are not supported."""

return pytest.mark.skipif(
WINDOWS and not _check_symlinks_supported(),
reason="Symbolic links are unsupported on Windows unless in Developer Mode.",
)(test_func)


class TestProjectBase(TestJobBase):
pass

Expand Down Expand Up @@ -188,7 +210,7 @@ def test_no_workspace_warn_on_find(self, caplog):
# constructor: https://bugs.python.org/issue33234
assert len(caplog.records) in (2, 3)

@pytest.mark.skipif(WINDOWS, reason="Symbolic links are unsupported on Windows.")
@skip_windows_without_symlinks
def test_workspace_broken_link_error_on_find(self):
with TemporaryDirectory() as tmp_dir:
project = self.project_class.init_project(path=tmp_dir)
Expand Down Expand Up @@ -1534,7 +1556,7 @@ def test_Schema_repr_methods(self, project_generator, num_jobs):


class TestLinkedViewProject(TestProjectBase):
@pytest.mark.skipif(WINDOWS, reason="Linked views unsupported on Windows.")
@skip_windows_without_symlinks
def test_create_linked_view(self):
def clean(filter=None):
"""Helper function for wiping out views"""
Expand Down Expand Up @@ -1620,7 +1642,7 @@ def clean(filter=None):
src = set(map(lambda j: os.path.realpath(j.path), self.project.find_jobs()))
assert src == dst

@pytest.mark.skipif(WINDOWS, reason="Linked views unsupported on Windows.")
@skip_windows_without_symlinks
def test_create_linked_view_homogeneous_schema_tree(self):
view_prefix = os.path.join(self._tmp_pr, "view")
a_vals = range(10)
Expand Down Expand Up @@ -1650,7 +1672,7 @@ def test_create_linked_view_homogeneous_schema_tree(self):
)
)

@pytest.mark.skipif(WINDOWS, reason="Linked views unsupported on Windows.")
@skip_windows_without_symlinks
def test_create_linked_view_homogeneous_schema_tree_tree(self):
view_prefix = os.path.join(self._tmp_pr, "view")
a_vals = range(10)
Expand Down Expand Up @@ -1680,7 +1702,7 @@ def test_create_linked_view_homogeneous_schema_tree_tree(self):
)
)

@pytest.mark.skipif(WINDOWS, reason="Linked views unsupported on Windows.")
@skip_windows_without_symlinks
def test_create_linked_view_homogeneous_schema_tree_flat(self):
view_prefix = os.path.join(self._tmp_pr, "view")
a_vals = range(10)
Expand All @@ -1707,7 +1729,7 @@ def test_create_linked_view_homogeneous_schema_tree_flat(self):
)
)

@pytest.mark.skipif(WINDOWS, reason="Linked views unsupported on Windows.")
@skip_windows_without_symlinks
def test_create_linked_view_homogeneous_schema_flat_flat(self):
view_prefix = os.path.join(self._tmp_pr, "view")
a_vals = range(10)
Expand All @@ -1734,7 +1756,7 @@ def test_create_linked_view_homogeneous_schema_flat_flat(self):
)
)

@pytest.mark.skipif(WINDOWS, reason="Linked views unsupported on Windows.")
@skip_windows_without_symlinks
def test_create_linked_view_homogeneous_schema_flat_tree(self):
view_prefix = os.path.join(self._tmp_pr, "view")
a_vals = range(10)
Expand Down Expand Up @@ -1769,7 +1791,7 @@ def test_create_linked_view_homogeneous_schema_flat_tree(self):
)
)

@pytest.mark.skipif(WINDOWS, reason="Linked views unsupported on Windows.")
@skip_windows_without_symlinks
def test_create_linked_view_homogeneous_schema_nested(self):
view_prefix = os.path.join(self._tmp_pr, "view")
a_vals = range(2)
Expand Down Expand Up @@ -1801,7 +1823,7 @@ def test_create_linked_view_homogeneous_schema_nested(self):
)
)

@pytest.mark.skipif(WINDOWS, reason="Linked views unsupported on Windows.")
@skip_windows_without_symlinks
def test_create_linked_view_homogeneous_schema_nested_provide_partial_path(self):
view_prefix = os.path.join(self._tmp_pr, "view")
a_vals = range(2)
Expand Down Expand Up @@ -1841,7 +1863,7 @@ def test_create_linked_view_homogeneous_schema_nested_provide_partial_path(self)
)
)

@pytest.mark.skipif(WINDOWS, reason="Linked views unsupported on Windows.")
@skip_windows_without_symlinks
def test_create_linked_view_heterogeneous_disjoint_schema(self):
view_prefix = os.path.join(self._tmp_pr, "view")
a_vals = range(5)
Expand Down Expand Up @@ -1871,7 +1893,7 @@ def test_create_linked_view_heterogeneous_disjoint_schema(self):
os.path.join(view_prefix, "c", sp["c"], "a", str(sp["a"]), "job")
)

@pytest.mark.skipif(WINDOWS, reason="Linked views unsupported on Windows.")
@skip_windows_without_symlinks
def test_create_linked_view_heterogeneous_disjoint_schema_nested(self):
view_prefix = os.path.join(self._tmp_pr, "view")
a_vals = range(2)
Expand Down Expand Up @@ -1902,7 +1924,7 @@ def test_create_linked_view_heterogeneous_disjoint_schema_nested(self):
)
)

@pytest.mark.skipif(WINDOWS, reason="Linked views unsupported on Windows.")
@skip_windows_without_symlinks
def test_create_linked_view_heterogeneous_fizz_schema_flat(self):
view_prefix = os.path.join(self._tmp_pr, "view")
a_vals = range(5)
Expand Down Expand Up @@ -1943,7 +1965,7 @@ def test_create_linked_view_heterogeneous_fizz_schema_flat(self):
)
)

@pytest.mark.skipif(WINDOWS, reason="Linked views unsupported on Windows.")
@skip_windows_without_symlinks
def test_create_linked_view_heterogeneous_schema_nested(self):
view_prefix = os.path.join(self._tmp_pr, "view")
a_vals = range(5)
Expand Down Expand Up @@ -1979,7 +2001,7 @@ def test_create_linked_view_heterogeneous_schema_nested(self):
)
)

@pytest.mark.skipif(WINDOWS, reason="Linked views unsupported on Windows.")
@skip_windows_without_symlinks
def test_create_linked_view_heterogeneous_schema_nested_partial_homogenous_path_provide(
self,
):
Expand Down Expand Up @@ -2028,25 +2050,27 @@ def test_create_linked_view_heterogeneous_schema_nested_partial_homogenous_path_
)
)

@pytest.mark.skipif(WINDOWS, reason="Linked views unsupported on Windows.")
@skip_windows_without_symlinks
def test_create_linked_view_heterogeneous_schema_problematic(self):
self.project.open_job(dict(a=1)).init()
self.project.open_job(dict(a=1, b=1)).init()
view_prefix = os.path.join(self._tmp_pr, "view")
with pytest.raises(RuntimeError):
self.project.create_linked_view(view_prefix)

@pytest.mark.skipif(WINDOWS, reason="Linked views unsupported on Windows.")
@skip_windows_without_symlinks
def test_create_linked_view_with_slash_raises_error(self):
statepoint = {"b": f"bad{os.sep}val"}
view_prefix = os.path.join(self._tmp_pr, "view")
self.project.open_job(statepoint).init()
with pytest.raises(RuntimeError):
self.project.create_linked_view(prefix=view_prefix)

@pytest.mark.skipif(WINDOWS, reason="Linked views unsupported on Windows.")
@skip_windows_without_symlinks
def test_create_linked_view_weird_chars_in_file_name(self):
shell_escaped_chars = [" ", "*", "~"]
shell_escaped_chars = [" ", "~"]
if not WINDOWS:
shell_escaped_chars.append("*")
statepoints = [
{f"a{i}b": 0, "b": f"escaped{i}val"} for i in shell_escaped_chars
]
Expand All @@ -2055,7 +2079,7 @@ def test_create_linked_view_weird_chars_in_file_name(self):
self.project.open_job(sp).init()
self.project.create_linked_view(prefix=view_prefix)

@pytest.mark.skipif(WINDOWS, reason="Linked views unsupported on Windows.")
@skip_windows_without_symlinks
def test_create_linked_view_duplicate_paths(self):
view_prefix = os.path.join(self._tmp_pr, "view")
a_vals = range(2)
Expand Down Expand Up @@ -2268,7 +2292,7 @@ def test_get_job_nested_project_subdir(self):
assert project.get_job(job.fn("test_subdir")) == job
assert signac.get_job(job.fn("test_subdir")) == job

@pytest.mark.skipif(WINDOWS, reason="Symbolic links are unsupported on Windows.")
@skip_windows_without_symlinks
def test_get_job_symlink_other_project(self):
# Test case: Get a job from a symlink in another project workspace
path = self._tmp_dir.name
Expand Down
13 changes: 5 additions & 8 deletions tests/test_shell.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,11 @@
from tempfile import TemporaryDirectory

import pytest
from test_project import _initialize_v1_project
from test_project import WINDOWS, _initialize_v1_project, skip_windows_without_symlinks

import signac
from signac._config import USER_CONFIG_FN, _Config, _load_config, _read_config_file

# Skip linked view tests on Windows
WINDOWS = sys.platform == "win32"


class DummyFile:
"We redirect sys stdout into this file during console tests."
Expand Down Expand Up @@ -154,7 +151,7 @@ def test_document(self):
assert str(key) in out
assert str(value) in out

@pytest.mark.skipif(WINDOWS, reason="Symbolic links are unsupported on Windows.")
@skip_windows_without_symlinks
def test_view_single(self):
"""Check whether command line views work for single job workspaces."""
self.call("python -m signac init".split())
Expand All @@ -170,7 +167,7 @@ def test_view_single(self):
project.open_job(sp).path
)

@pytest.mark.skipif(WINDOWS, reason="Symbolic links are unsupported on Windows.")
@skip_windows_without_symlinks
def test_view(self):
self.call("python -m signac init".split())
project = signac.Project()
Expand All @@ -186,7 +183,7 @@ def test_view(self):
"view/a/{}/job".format(sp["a"])
) == os.path.realpath(project.open_job(sp).path)

@pytest.mark.skipif(WINDOWS, reason="Symbolic links are unsupported on Windows.")
@skip_windows_without_symlinks
def test_view_prefix(self):
self.call("python -m signac init".split())
project = signac.Project()
Expand All @@ -202,7 +199,7 @@ def test_view_prefix(self):
"view/test_dir/a/{}/job".format(sp["a"])
) == os.path.realpath(project.open_job(sp).path)

@pytest.mark.skipif(WINDOWS, reason="Symbolic links are unsupported on Windows.")
@skip_windows_without_symlinks
def test_view_incomplete_path_spec(self):
self.call("python -m signac init".split())
project = signac.Project()
Expand Down

0 comments on commit 7884ebd

Please sign in to comment.