From eea288a84694b15d74c84b919756e751b0115e15 Mon Sep 17 00:00:00 2001 From: slowglow <71831690+slowglow@users.noreply.github.com> Date: Wed, 15 Nov 2023 12:35:02 +0900 Subject: [PATCH 1/9] [FIX]: Attempt creation of symlink on Windows and handle exceptions. If symlinks are not enabled on Windows, issue a guiding message. --- signac/linked_view.py | 43 ++++++++++++++++++++++++++++++++----------- 1 file changed, 32 insertions(+), 11 deletions(-) diff --git a/signac/linked_view.py b/signac/linked_view.py index ab273a721..57b9f6fae 100644 --- a/signac/linked_view.py +++ b/signac/linked_view.py @@ -38,21 +38,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" @@ -85,8 +79,35 @@ 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") & (err.winerror == 1314): + print("-----------------------------------------------------------------") + print(err.strerror) + print(" ") + print("You likely don't have permission to create Windows symlinks.") + print("To enable the creation of symlinks on Windows you need") + print("to enable 'Developer mode' (requires administrative rights).") + print(" ") + print("To enable 'Developer mode':") + print(" 1. Go to 'Settings'.") + print(" 2. In the search bar type 'Use developer features'") + print(" 3. Enable the item 'Developer mode'.") + print("The details may vary between different versions of Windows.") + print("") + print("If you use Python >=3.8 that's all there is to it.") + print("For Python <3.8, a few more steps are required.") + print("It gets more involved if your Windows is Home edition.") + print("For more details check:") + print("https://www.scivision.dev/windows-symbolic-link-permission-enable") + print("-----------------------------------------------------------------") + print(" ") + raise err.with_traceback(sys.exc_info()[2]) return links From 61215a61408a378f66d8d83000ffb1d0ad4938b9 Mon Sep 17 00:00:00 2001 From: slowglow <71831690+slowglow@users.noreply.github.com> Date: Wed, 15 Nov 2023 13:01:09 +0900 Subject: [PATCH 2/9] Update contributors.yaml --- contributors.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/contributors.yaml b/contributors.yaml index 5ba517c87..a611392c7 100644 --- a/contributors.yaml +++ b/contributors.yaml @@ -138,4 +138,8 @@ contributors: family-names: Kadar given-names: Alain affiliation: "University of Michigan" + - + family-names: Stoimenov + given-names: Boyko + affiliation: "JTEKT Corp." ... From a6887b4f0fc344b3e48934ac6ac75e030ea37dd1 Mon Sep 17 00:00:00 2001 From: slowglow <71831690+slowglow@users.noreply.github.com> Date: Wed, 15 Nov 2023 13:03:54 +0900 Subject: [PATCH 3/9] Update changelog.txt --- changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.txt b/changelog.txt index e5b1f2ad2..40ffc7106 100644 --- a/changelog.txt +++ b/changelog.txt @@ -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 --------------------- From e9ecf3666c2ac25b8a8b243059b919f989b8cbd1 Mon Sep 17 00:00:00 2001 From: slowglow <71831690+slowglow@users.noreply.github.com> Date: Wed, 15 Nov 2023 13:18:37 +0900 Subject: [PATCH 4/9] Update linked_view.py Change of wording of the message: - removed mention of Python <3.8, which is not supported. - removed reference to external sites, which may go down in the future. --- signac/linked_view.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/signac/linked_view.py b/signac/linked_view.py index 57b9f6fae..28adbb148 100644 --- a/signac/linked_view.py +++ b/signac/linked_view.py @@ -98,13 +98,7 @@ def create_linked_view(project, prefix=None, job_ids=None, path=None): print(" 1. Go to 'Settings'.") print(" 2. In the search bar type 'Use developer features'") print(" 3. Enable the item 'Developer mode'.") - print("The details may vary between different versions of Windows.") - print("") - print("If you use Python >=3.8 that's all there is to it.") - print("For Python <3.8, a few more steps are required.") - print("It gets more involved if your Windows is Home edition.") - print("For more details check:") - print("https://www.scivision.dev/windows-symbolic-link-permission-enable") + print("The details for Home edition and between Windows versions may vary.") print("-----------------------------------------------------------------") print(" ") raise err.with_traceback(sys.exc_info()[2]) From 64415ea739fc13ed605357239e2c05b428cda691 Mon Sep 17 00:00:00 2001 From: Bradley Dice Date: Wed, 15 Nov 2023 10:21:15 -0600 Subject: [PATCH 5/9] Update linked_view.py --- signac/linked_view.py | 33 ++++++++++++++++++--------------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/signac/linked_view.py b/signac/linked_view.py index 28adbb148..66211b88a 100644 --- a/signac/linked_view.py +++ b/signac/linked_view.py @@ -7,6 +7,7 @@ import logging import os import sys +import textwrap from itertools import chain from ._utility import _mkdir_p @@ -86,21 +87,23 @@ def create_linked_view(project, prefix=None, job_ids=None, path=None): _check_directory_structure_validity(links.keys()) _update_view(prefix, links) except OSError as err: - if (sys.platform == "win32") & (err.winerror == 1314): - print("-----------------------------------------------------------------") - print(err.strerror) - print(" ") - print("You likely don't have permission to create Windows symlinks.") - print("To enable the creation of symlinks on Windows you need") - print("to enable 'Developer mode' (requires administrative rights).") - print(" ") - print("To enable 'Developer mode':") - print(" 1. Go to 'Settings'.") - print(" 2. In the search bar type 'Use developer features'") - print(" 3. Enable the item 'Developer mode'.") - print("The details for Home edition and between Windows versions may vary.") - print("-----------------------------------------------------------------") - print(" ") + 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 From 637d44ffa6413fa2b1e7e715ac44cff3574eb620 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 15 Nov 2023 16:21:58 +0000 Subject: [PATCH 6/9] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- signac/linked_view.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/signac/linked_view.py b/signac/linked_view.py index 66211b88a..816d79032 100644 --- a/signac/linked_view.py +++ b/signac/linked_view.py @@ -88,11 +88,13 @@ def create_linked_view(project, prefix=None, job_ids=None, path=None): _update_view(prefix, links) except OSError as err: if sys.platform == "win32" and err.winerror == 1314: - print(textwrap.dedent(f"""\ + 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). @@ -103,7 +105,8 @@ def create_linked_view(project, prefix=None, job_ids=None, path=None): The details for Home edition and between Windows versions may vary. ----------------------------------------------------------------- """ - )) + ) + ) raise err.with_traceback(sys.exc_info()[2]) return links From f1b4e24c009e3f47bc389d4ab36dae6a140ed251 Mon Sep 17 00:00:00 2001 From: Bradley Dice Date: Wed, 15 Nov 2023 10:27:57 -0600 Subject: [PATCH 7/9] Update linked_view.py --- signac/linked_view.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/signac/linked_view.py b/signac/linked_view.py index 816d79032..ad941cef7 100644 --- a/signac/linked_view.py +++ b/signac/linked_view.py @@ -91,7 +91,7 @@ def create_linked_view(project, prefix=None, job_ids=None, path=None): print( textwrap.dedent( f"""\ - ----------------------------------------------------------------- + ------------------------------------------------------------------- Error: {err.strerror} @@ -103,7 +103,7 @@ def create_linked_view(project, prefix=None, job_ids=None, path=None): 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. - ----------------------------------------------------------------- + ------------------------------------------------------------------- """ ) ) From 1e61ca5ce3c622eb1a2338685230161ca694e6c7 Mon Sep 17 00:00:00 2001 From: Bradley Dice Date: Wed, 15 Nov 2023 10:53:01 -0600 Subject: [PATCH 8/9] Update tests. --- tests/test_project.py | 62 +++++++++++++++++++++++++++++-------------- tests/test_shell.py | 13 ++++----- 2 files changed, 47 insertions(+), 28 deletions(-) diff --git a/tests/test_project.py b/tests/test_project.py index 565a2d5bc..e6c727656 100644 --- a/tests/test_project.py +++ b/tests/test_project.py @@ -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 @@ -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 @@ -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) @@ -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""" @@ -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) @@ -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) @@ -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) @@ -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) @@ -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) @@ -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) @@ -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) @@ -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) @@ -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) @@ -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) @@ -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) @@ -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, ): @@ -2028,7 +2050,7 @@ 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() @@ -2036,7 +2058,7 @@ def test_create_linked_view_heterogeneous_schema_problematic(self): 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") @@ -2044,7 +2066,7 @@ def test_create_linked_view_with_slash_raises_error(self): 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 = [" ", "*", "~"] statepoints = [ @@ -2055,7 +2077,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) @@ -2268,7 +2290,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 diff --git a/tests/test_shell.py b/tests/test_shell.py index 073223e71..695fdada1 100644 --- a/tests/test_shell.py +++ b/tests/test_shell.py @@ -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." @@ -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()) @@ -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() @@ -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() @@ -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() From 4f3c12b827e51c4cb3f7da2ee2748c27dfd6e2d2 Mon Sep 17 00:00:00 2001 From: Bradley Dice Date: Wed, 15 Nov 2023 10:55:30 -0600 Subject: [PATCH 9/9] Ignore test for * in symlink names on Windows. --- tests/test_project.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/test_project.py b/tests/test_project.py index e6c727656..da546ede0 100644 --- a/tests/test_project.py +++ b/tests/test_project.py @@ -2068,7 +2068,9 @@ def test_create_linked_view_with_slash_raises_error(self): @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 ]