From 7692245551367800aa6be11ede8512c8d571147f Mon Sep 17 00:00:00 2001 From: maximlt Date: Wed, 26 Jul 2023 16:35:50 +0200 Subject: [PATCH 01/11] raise error on Path instantiation if the path is not found --- param/__init__.py | 3 ++- tests/testpathparam.py | 21 +++++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/param/__init__.py b/param/__init__.py index d8be63f9c..2b12208c1 100644 --- a/param/__init__.py +++ b/param/__init__.py @@ -2342,6 +2342,7 @@ def __init__(self, default=Undefined, *, search_paths=Undefined, **params): self.search_paths = search_paths super().__init__(default,**params) + self._validate(self.default) def _resolve(self, path): return resolve_path(path, path_to_file=None, search_paths=self.search_paths) @@ -2354,7 +2355,7 @@ def _validate(self, val): try: self._resolve(val) except OSError as e: - Parameterized(name=f"{self.owner.name}.{self.name}").param.warning('%s',e.args[0]) + raise OSError(e.args[0]) from None def __get__(self, obj, objtype): """ diff --git a/tests/testpathparam.py b/tests/testpathparam.py index bbc8043ff..81889a02b 100644 --- a/tests/testpathparam.py +++ b/tests/testpathparam.py @@ -120,6 +120,13 @@ class B(self.P): with pytest.raises(OSError, match='Path a.txt was not found'): assert b.c is None + def test_notfound_instantiation_raises_error(self): + with pytest.raises( + OSError, + match=r"Path file was not found in the following place\(s\): \['\S+/non/existing/file'\]" + ): + param.Path('non/existing/file') + class TestFilenameParameters(unittest.TestCase): @@ -202,6 +209,13 @@ def test_search_paths(self): assert os.path.isabs(p.c) assert p.c == self.fa + def test_notfound_instantiation_raises_error(self): + with pytest.raises( + OSError, + match=r"File file was not found in the following place\(s\): \['\S+/non/existing/file'\]" + ): + param.Filename('non/existing/file') + class TestFoldernameParameters(unittest.TestCase): @@ -281,3 +295,10 @@ def test_search_paths(self): assert os.path.isdir(p.c) assert os.path.isabs(p.c) assert p.c == self.da + + def test_notfound_instantiation_raises_error(self): + with pytest.raises( + OSError, + match=r"Folder folder was not found in the following place\(s\): \['\S+/non/existing/folder'\]" + ): + param.Foldername('non/existing/folder') From 42b6440d76b7e77e73005a78cb888634a88fb186 Mon Sep 17 00:00:00 2001 From: maximlt Date: Wed, 26 Jul 2023 16:38:05 +0200 Subject: [PATCH 02/11] test that setting a path not found now raises an error --- tests/testpathparam.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tests/testpathparam.py b/tests/testpathparam.py index 81889a02b..33557caa5 100644 --- a/tests/testpathparam.py +++ b/tests/testpathparam.py @@ -127,6 +127,14 @@ def test_notfound_instantiation_raises_error(self): ): param.Path('non/existing/file') + def test_set_notfound(self): + p = self.P() + with pytest.raises( + OSError, + match=r"Path file was not found in the following place\(s\): \['\S+/non/existing/file'\]" + ): + p.a = 'non/existing/file' + class TestFilenameParameters(unittest.TestCase): @@ -216,6 +224,14 @@ def test_notfound_instantiation_raises_error(self): ): param.Filename('non/existing/file') + def test_set_notfound(self): + p = self.P() + with pytest.raises( + OSError, + match=r"File file was not found in the following place\(s\): \['\S+/non/existing/file'\]" + ): + p.a = 'non/existing/file' + class TestFoldernameParameters(unittest.TestCase): @@ -302,3 +318,11 @@ def test_notfound_instantiation_raises_error(self): match=r"Folder folder was not found in the following place\(s\): \['\S+/non/existing/folder'\]" ): param.Foldername('non/existing/folder') + + def test_set_notfound(self): + p = self.P() + with pytest.raises( + OSError, + match=r"Folder folder was not found in the following place\(s\): \['\S+/non/existing/folder'\]" + ): + p.a = 'non/existing/folder' From fdc08f3d5d03e914b47768805ff08603724d7b09 Mon Sep 17 00:00:00 2001 From: maximlt Date: Wed, 26 Jul 2023 21:57:50 +0200 Subject: [PATCH 03/11] add notfound_ok attribute to Path --- param/__init__.py | 26 ++++++++++--- tests/testpathparam.py | 86 ++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 103 insertions(+), 9 deletions(-) diff --git a/param/__init__.py b/param/__init__.py index 2b12208c1..227316c82 100644 --- a/param/__init__.py +++ b/param/__init__.py @@ -2324,23 +2324,28 @@ class Path(Parameter): is None). """ - __slots__ = ['search_paths'] + __slots__ = ['search_paths', 'notfound_ok'] + + _slot_defaults = _dict_update( + Parameter._slot_defaults, notfound_ok=False, + ) @typing.overload def __init__( self, - default=None, *, search_paths=None, + default=None, *, search_paths=None, notfound_ok=False, allow_None=False, doc=None, label=None, precedence=None, instantiate=False, constant=False, readonly=False, pickle_default_value=True, per_instance=True ): ... @_deprecate_positional_args - def __init__(self, default=Undefined, *, search_paths=Undefined, **params): + def __init__(self, default=Undefined, *, search_paths=Undefined, notfound_ok=Undefined, **params): if search_paths is Undefined: search_paths = [] self.search_paths = search_paths + self.notfound_ok = notfound_ok super().__init__(default,**params) self._validate(self.default) @@ -2355,14 +2360,25 @@ def _validate(self, val): try: self._resolve(val) except OSError as e: - raise OSError(e.args[0]) from None + if not self.notfound_ok: + raise OSError(e.args[0]) from None def __get__(self, obj, objtype): """ Return an absolute, normalized path (see resolve_path). """ raw_path = super().__get__(obj,objtype) - return None if raw_path is None else self._resolve(raw_path) + if raw_path is None: + path = None + else: + try: + path = self._resolve(raw_path) + except OSError: + if not self.notfound_ok: + raise + else: + path = raw_path + return path def __getstate__(self): # don't want to pickle the search_paths diff --git a/tests/testpathparam.py b/tests/testpathparam.py index 33557caa5..b9d20c487 100644 --- a/tests/testpathparam.py +++ b/tests/testpathparam.py @@ -29,6 +29,7 @@ class P(param.Parameterized): a = param.Path() b = param.Path(self.fb) c = param.Path('a.txt', search_paths=[tmpdir1]) + d = param.Path(notfound_ok=True) self.P = P @@ -39,6 +40,7 @@ def _check_defaults(self, p): assert p.default is None assert p.allow_None is True assert p.search_paths == [] + assert p.notfound_ok is False def test_defaults_class(self): class P(param.Parameterized): @@ -127,7 +129,7 @@ def test_notfound_instantiation_raises_error(self): ): param.Path('non/existing/file') - def test_set_notfound(self): + def test_set_notfound_raises_error(self): p = self.P() with pytest.raises( OSError, @@ -135,6 +137,30 @@ def test_set_notfound(self): ): p.a = 'non/existing/file' + def test_set_notfound_class_raises_error(self): + with pytest.raises( + OSError, + match=r"Path file was not found in the following place\(s\): \['\S+/non/existing/file'\]" + ): + self.P.a = 'non/existing/file' + + def test_notfoundok_unbound_no_error(self): + p = param.Path('non/existing/file', notfound_ok=True) + assert p.default == 'non/existing/file' + + def test_notfoundok_class_no_error(self): + self.P.d = 'non/existing/file' + assert self.P.d == 'non/existing/file' + + def test_notfoundok_instantiation_no_error(self): + p = self.P(d='non/existing/file') + assert p.d == 'non/existing/file' + + def test_notfoundok_set_no_error(self): + p = self.P() + p.d = 'non/existing/file' + assert p.d == 'non/existing/file' + class TestFilenameParameters(unittest.TestCase): @@ -155,6 +181,7 @@ class P(param.Parameterized): a = param.Filename() b = param.Filename(self.fb) c = param.Filename('a.txt', search_paths=[tmpdir1]) + d = param.Filename(notfound_ok=True) self.P = P @@ -165,6 +192,7 @@ def _check_defaults(self, p): assert p.default is None assert p.allow_None is True assert p.search_paths == [] + assert p.notfound_ok is False def test_defaults_class(self): class P(param.Parameterized): @@ -224,7 +252,14 @@ def test_notfound_instantiation_raises_error(self): ): param.Filename('non/existing/file') - def test_set_notfound(self): + def test_set_notfound_class_raises_error(self): + with pytest.raises( + OSError, + match=r"File file was not found in the following place\(s\): \['\S+/non/existing/file'\]" + ): + self.P.a = 'non/existing/file' + + def test_set_notfound_raises_error(self): p = self.P() with pytest.raises( OSError, @@ -232,6 +267,23 @@ def test_set_notfound(self): ): p.a = 'non/existing/file' + def test_notfoundok_unbound_no_error(self): + p = param.Filename('non/existing/file', notfound_ok=True) + assert p.default == 'non/existing/file' + + def test_notfoundok_class_no_error(self): + self.P.d = 'non/existing/file' + assert self.P.d == 'non/existing/file' + + def test_notfoundok_instantiation_no_error(self): + p = self.P(d='non/existing/file') + assert p.d == 'non/existing/file' + + def test_notfoundok_set_no_error(self): + p = self.P() + p.d = 'non/existing/file' + assert p.d == 'non/existing/file' + class TestFoldernameParameters(unittest.TestCase): @@ -248,7 +300,8 @@ def setUp(self): class P(param.Parameterized): a = param.Foldername() b = param.Foldername(tmpdir1) - c = param.Path('da', search_paths=[tmpdir1]) + c = param.Foldername('da', search_paths=[tmpdir1]) + d = param.Foldername(notfound_ok=True) self.P = P @@ -259,6 +312,7 @@ def _check_defaults(self, p): assert p.default is None assert p.allow_None is True assert p.search_paths == [] + assert p.notfound_ok is False def test_defaults_class(self): class P(param.Parameterized): @@ -319,10 +373,34 @@ def test_notfound_instantiation_raises_error(self): ): param.Foldername('non/existing/folder') - def test_set_notfound(self): + def test_set_notfound_raises_error(self): p = self.P() with pytest.raises( OSError, match=r"Folder folder was not found in the following place\(s\): \['\S+/non/existing/folder'\]" ): p.a = 'non/existing/folder' + + def test_set_notfound_class_raises_error(self): + with pytest.raises( + OSError, + match=r"Folder folder was not found in the following place\(s\): \['\S+/non/existing/folder'\]" + ): + self.P.a = 'non/existing/folder' + + def test_notfoundok_unbound_no_error(self): + p = param.Foldername('non/existing/folder', notfound_ok=True) + assert p.default == 'non/existing/folder' + + def test_notfoundok_class_no_error(self): + self.P.d = 'non/existing/folder' + assert self.P.d == 'non/existing/folder' + + def test_notfoundok_instantiation_no_error(self): + p = self.P(d='non/existing/folder') + assert p.d == 'non/existing/folder' + + def test_notfoundok_set_no_error(self): + p = self.P() + p.d = 'non/existing/folder' + assert p.d == 'non/existing/folder' From 76629aa6bbcbe9c84b97d20dd0ca389518711519 Mon Sep 17 00:00:00 2001 From: maximlt Date: Thu, 27 Jul 2023 07:58:40 +0200 Subject: [PATCH 04/11] adapt paths for windows --- tests/testpathparam.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/tests/testpathparam.py b/tests/testpathparam.py index b9d20c487..e69709ab2 100644 --- a/tests/testpathparam.py +++ b/tests/testpathparam.py @@ -125,7 +125,7 @@ class B(self.P): def test_notfound_instantiation_raises_error(self): with pytest.raises( OSError, - match=r"Path file was not found in the following place\(s\): \['\S+/non/existing/file'\]" + match=r"Path file was not found in the following place\(s\): \['\S+(\\\\|/)non(\\\\|/)existing(\\\\|/)file'\]" ): param.Path('non/existing/file') @@ -133,14 +133,14 @@ def test_set_notfound_raises_error(self): p = self.P() with pytest.raises( OSError, - match=r"Path file was not found in the following place\(s\): \['\S+/non/existing/file'\]" + match=r"Path file was not found in the following place\(s\): \['\S+(\\\\|/)non(\\\\|/)existing(\\\\|/)file'\]" ): p.a = 'non/existing/file' def test_set_notfound_class_raises_error(self): with pytest.raises( OSError, - match=r"Path file was not found in the following place\(s\): \['\S+/non/existing/file'\]" + match=r"Path file was not found in the following place\(s\): \['\S+(\\\\|/)non(\\\\|/)existing(\\\\|/)file'\]" ): self.P.a = 'non/existing/file' @@ -248,14 +248,14 @@ def test_search_paths(self): def test_notfound_instantiation_raises_error(self): with pytest.raises( OSError, - match=r"File file was not found in the following place\(s\): \['\S+/non/existing/file'\]" + match=r"File file was not found in the following place\(s\): \['\S+(\\\\|/)non(\\\\|/)existing(\\\\|/)file'\]" ): param.Filename('non/existing/file') def test_set_notfound_class_raises_error(self): with pytest.raises( OSError, - match=r"File file was not found in the following place\(s\): \['\S+/non/existing/file'\]" + match=r"File file was not found in the following place\(s\): \['\S+(\\\\|/)non(\\\\|/)existing(\\\\|/)file'\]" ): self.P.a = 'non/existing/file' @@ -263,7 +263,7 @@ def test_set_notfound_raises_error(self): p = self.P() with pytest.raises( OSError, - match=r"File file was not found in the following place\(s\): \['\S+/non/existing/file'\]" + match=r"File file was not found in the following place\(s\): \['\S+(\\\\|/)non(\\\\|/)existing(\\\\|/)file'\]" ): p.a = 'non/existing/file' @@ -369,7 +369,7 @@ def test_search_paths(self): def test_notfound_instantiation_raises_error(self): with pytest.raises( OSError, - match=r"Folder folder was not found in the following place\(s\): \['\S+/non/existing/folder'\]" + match=r"Folder folder was not found in the following place\(s\): \['\S+(\\\\|/)non(\\\\|/)existing(\\\\|/)folder'\]" ): param.Foldername('non/existing/folder') @@ -377,14 +377,14 @@ def test_set_notfound_raises_error(self): p = self.P() with pytest.raises( OSError, - match=r"Folder folder was not found in the following place\(s\): \['\S+/non/existing/folder'\]" + match=r"Folder folder was not found in the following place\(s\): \['\S+(\\\\|/)non(\\\\|/)existing(\\\\|/)folder'\]" ): p.a = 'non/existing/folder' def test_set_notfound_class_raises_error(self): with pytest.raises( OSError, - match=r"Folder folder was not found in the following place\(s\): \['\S+/non/existing/folder'\]" + match=r"Folder folder was not found in the following place\(s\): \['\S+(\\\\|/)non(\\\\|/)existing(\\\\|/)folder'\]" ): self.P.a = 'non/existing/folder' From 0b1825b3285f6f2458cd0eb19e00d05472be1f62 Mon Sep 17 00:00:00 2001 From: maximlt Date: Thu, 27 Jul 2023 13:35:02 +0200 Subject: [PATCH 05/11] rename notfound_ok as check_exists with a default of True --- param/__init__.py | 14 +++++++------- tests/testpathparam.py | 18 +++++++++--------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/param/__init__.py b/param/__init__.py index 227316c82..afe73ccd4 100644 --- a/param/__init__.py +++ b/param/__init__.py @@ -2324,28 +2324,28 @@ class Path(Parameter): is None). """ - __slots__ = ['search_paths', 'notfound_ok'] + __slots__ = ['search_paths', 'check_exists'] _slot_defaults = _dict_update( - Parameter._slot_defaults, notfound_ok=False, + Parameter._slot_defaults, check_exists=True, ) @typing.overload def __init__( self, - default=None, *, search_paths=None, notfound_ok=False, + default=None, *, search_paths=None, check_exists=True, allow_None=False, doc=None, label=None, precedence=None, instantiate=False, constant=False, readonly=False, pickle_default_value=True, per_instance=True ): ... @_deprecate_positional_args - def __init__(self, default=Undefined, *, search_paths=Undefined, notfound_ok=Undefined, **params): + def __init__(self, default=Undefined, *, search_paths=Undefined, check_exists=Undefined, **params): if search_paths is Undefined: search_paths = [] self.search_paths = search_paths - self.notfound_ok = notfound_ok + self.check_exists = check_exists super().__init__(default,**params) self._validate(self.default) @@ -2360,7 +2360,7 @@ def _validate(self, val): try: self._resolve(val) except OSError as e: - if not self.notfound_ok: + if self.check_exists: raise OSError(e.args[0]) from None def __get__(self, obj, objtype): @@ -2374,7 +2374,7 @@ def __get__(self, obj, objtype): try: path = self._resolve(raw_path) except OSError: - if not self.notfound_ok: + if self.check_exists: raise else: path = raw_path diff --git a/tests/testpathparam.py b/tests/testpathparam.py index e69709ab2..2e1099c81 100644 --- a/tests/testpathparam.py +++ b/tests/testpathparam.py @@ -29,7 +29,7 @@ class P(param.Parameterized): a = param.Path() b = param.Path(self.fb) c = param.Path('a.txt', search_paths=[tmpdir1]) - d = param.Path(notfound_ok=True) + d = param.Path(check_exists=False) self.P = P @@ -40,7 +40,7 @@ def _check_defaults(self, p): assert p.default is None assert p.allow_None is True assert p.search_paths == [] - assert p.notfound_ok is False + assert p.check_exists is True def test_defaults_class(self): class P(param.Parameterized): @@ -145,7 +145,7 @@ def test_set_notfound_class_raises_error(self): self.P.a = 'non/existing/file' def test_notfoundok_unbound_no_error(self): - p = param.Path('non/existing/file', notfound_ok=True) + p = param.Path('non/existing/file', check_exists=False) assert p.default == 'non/existing/file' def test_notfoundok_class_no_error(self): @@ -181,7 +181,7 @@ class P(param.Parameterized): a = param.Filename() b = param.Filename(self.fb) c = param.Filename('a.txt', search_paths=[tmpdir1]) - d = param.Filename(notfound_ok=True) + d = param.Filename(check_exists=False) self.P = P @@ -192,7 +192,7 @@ def _check_defaults(self, p): assert p.default is None assert p.allow_None is True assert p.search_paths == [] - assert p.notfound_ok is False + assert p.check_exists is True def test_defaults_class(self): class P(param.Parameterized): @@ -268,7 +268,7 @@ def test_set_notfound_raises_error(self): p.a = 'non/existing/file' def test_notfoundok_unbound_no_error(self): - p = param.Filename('non/existing/file', notfound_ok=True) + p = param.Filename('non/existing/file', check_exists=False) assert p.default == 'non/existing/file' def test_notfoundok_class_no_error(self): @@ -301,7 +301,7 @@ class P(param.Parameterized): a = param.Foldername() b = param.Foldername(tmpdir1) c = param.Foldername('da', search_paths=[tmpdir1]) - d = param.Foldername(notfound_ok=True) + d = param.Foldername(check_exists=False) self.P = P @@ -312,7 +312,7 @@ def _check_defaults(self, p): assert p.default is None assert p.allow_None is True assert p.search_paths == [] - assert p.notfound_ok is False + assert p.check_exists is True def test_defaults_class(self): class P(param.Parameterized): @@ -389,7 +389,7 @@ def test_set_notfound_class_raises_error(self): self.P.a = 'non/existing/folder' def test_notfoundok_unbound_no_error(self): - p = param.Foldername('non/existing/folder', notfound_ok=True) + p = param.Foldername('non/existing/folder', check_exists=False) assert p.default == 'non/existing/folder' def test_notfoundok_class_no_error(self): From d92b162f611796d00a4fe0a89ec3ce9367923aa9 Mon Sep 17 00:00:00 2001 From: maximlt Date: Sun, 30 Jul 2023 17:36:54 +0200 Subject: [PATCH 06/11] add more tests --- param/__init__.py | 2 + tests/testpathparam.py | 135 ++++++++++++++++++++++++++++++----------- 2 files changed, 101 insertions(+), 36 deletions(-) diff --git a/param/__init__.py b/param/__init__.py index afe73ccd4..2bc5b39f4 100644 --- a/param/__init__.py +++ b/param/__init__.py @@ -2345,6 +2345,8 @@ def __init__(self, default=Undefined, *, search_paths=Undefined, check_exists=Un search_paths = [] self.search_paths = search_paths + if check_exists is not Undefined and not isinstance(check_exists, bool): + raise ValueError("'check_exists' attribute value must be a boolean") self.check_exists = check_exists super().__init__(default,**params) self._validate(self.default) diff --git a/tests/testpathparam.py b/tests/testpathparam.py index 2e1099c81..b251793ae 100644 --- a/tests/testpathparam.py +++ b/tests/testpathparam.py @@ -16,25 +16,37 @@ def setUp(self): super().setUp() tmpdir1 = tempfile.mkdtemp() + + self.curdir = os.getcwd() + # Chanding the directory to tmpdir1 to test that Path resolves relative + # paths to absolute paths automatically. + os.chdir(tmpdir1) + fa = os.path.join(tmpdir1, 'a.txt') fb = os.path.join(tmpdir1, 'b.txt') + fc = 'c.txt' open(fa, 'w').close() open(fb, 'w').close() + open(fc, 'w').close() self.tmpdir1 = tmpdir1 self.fa = fa self.fb = fb + self.fc = fc class P(param.Parameterized): a = param.Path() b = param.Path(self.fb) c = param.Path('a.txt', search_paths=[tmpdir1]) d = param.Path(check_exists=False) + e = param.Path(self.fc, check_exists=False) + f = param.Path(self.fc) self.P = P def tearDown(self): shutil.rmtree(self.tmpdir1) + os.chdir(self.curdir) def _check_defaults(self, p): assert p.default is None @@ -87,6 +99,25 @@ def test_set_to_None_allowed(self): with pytest.raises(ValueError, match=re.escape(r"'Path' Parameter 'b' does not accept None")): p.b = None + def test_relative_cwd_class(self): + assert os.path.isabs(self.P.f) + + def test_relative_cwd_class_set(self): + self.P.a = self.fc + assert os.path.isabs(self.P.a) + + def test_relative_cwd_inst(self): + assert os.path.isabs(self.P().f) + + def test_relative_cwd_instantiation(self): + p = self.P(a=self.fc) + assert os.path.isabs(p.a) + + def test_relative_cwd_set(self): + p = self.P() + p.a = self.fc + assert os.path.isabs(p.a) + def test_search_paths(self): p = self.P() @@ -97,30 +128,37 @@ def test_search_paths(self): def test_inheritance_behavior(self): - # a = param.Path() - # b = param.Path(self.fb) - # c = param.Path('a.txt', search_paths=[tmpdir1]) - - class B(self.P): - a = param.Path() - b = param.Path() - c = param.Path() - - assert B.a is None - assert B.b == self.fb - # search_paths is empty instead of [tmpdir1] and getting c raises an error - assert B.param.c.search_paths == [] - with pytest.raises(OSError, match='Path a.txt was not found'): - assert B.c is None - - b = B() - - assert b.a is None - assert b.b == self.fb - - assert b.param.c.search_paths == [] - with pytest.raises(OSError, match='Path a.txt was not found'): - assert b.c is None + # Inheritance isn't working great with search_paths and this test + # isn't designed to be run from the tmpdir directory. + startd = os.getcwd() + try: + os.chdir(self.curdir) + # a = param.Path() + # b = param.Path(self.fb) + # c = param.Path('a.txt', search_paths=[tmpdir1]) + + class B(self.P): + a = param.Path() + b = param.Path() + c = param.Path() + + assert B.a is None + assert B.b == self.fb + # search_paths is empty instead of [tmpdir1] and getting c raises an error + assert B.param.c.search_paths == [] + with pytest.raises(OSError, match='Path a.txt was not found'): + assert B.c is None + + b = B() + + assert b.a is None + assert b.b == self.fb + + assert b.param.c.search_paths == [] + with pytest.raises(OSError, match='Path a.txt was not found'): + assert b.c is None + finally: + os.chdir(startd) def test_notfound_instantiation_raises_error(self): with pytest.raises( @@ -144,23 +182,48 @@ def test_set_notfound_class_raises_error(self): ): self.P.a = 'non/existing/file' - def test_notfoundok_unbound_no_error(self): + def test_nonexisting_unbound_no_error(self): p = param.Path('non/existing/file', check_exists=False) assert p.default == 'non/existing/file' - def test_notfoundok_class_no_error(self): + def test_nonexisting_class_no_error(self): self.P.d = 'non/existing/file' assert self.P.d == 'non/existing/file' - def test_notfoundok_instantiation_no_error(self): + def test_nonexisting_instantiation_no_error(self): p = self.P(d='non/existing/file') assert p.d == 'non/existing/file' - def test_notfoundok_set_no_error(self): + def test_nonexisting_set_no_error(self): p = self.P() p.d = 'non/existing/file' assert p.d == 'non/existing/file' + def test_optionalexistence_unbound_no_error(self): + p = param.Path(self.fa, check_exists=False) + assert os.path.isabs(p.default) + + def test_optionalexistence_class_no_error(self): + assert os.path.isabs(self.P.e) + self.P.d = self.fc + assert os.path.isabs(self.P.d) + + def test_optionalexistence_instantiation_no_error(self): + p = self.P(d=self.fc) + assert os.path.isabs(p.d) + + def test_optionalexistence_set_no_error(self): + p = self.P() + p.d = self.fc + assert os.path.isabs(p.d) + + def test_existence_bad_value(self): + with pytest.raises( + ValueError, + match="'check_exists' attribute value must be a boolean" + ): + param.Path(check_exists='wrong_option') + class TestFilenameParameters(unittest.TestCase): @@ -267,19 +330,19 @@ def test_set_notfound_raises_error(self): ): p.a = 'non/existing/file' - def test_notfoundok_unbound_no_error(self): + def test_nonexisting_unbound_no_error(self): p = param.Filename('non/existing/file', check_exists=False) assert p.default == 'non/existing/file' - def test_notfoundok_class_no_error(self): + def test_nonexisting_class_no_error(self): self.P.d = 'non/existing/file' assert self.P.d == 'non/existing/file' - def test_notfoundok_instantiation_no_error(self): + def test_nonexisting_instantiation_no_error(self): p = self.P(d='non/existing/file') assert p.d == 'non/existing/file' - def test_notfoundok_set_no_error(self): + def test_nonexisting_set_no_error(self): p = self.P() p.d = 'non/existing/file' assert p.d == 'non/existing/file' @@ -388,19 +451,19 @@ def test_set_notfound_class_raises_error(self): ): self.P.a = 'non/existing/folder' - def test_notfoundok_unbound_no_error(self): + def test_nonexisting_unbound_no_error(self): p = param.Foldername('non/existing/folder', check_exists=False) assert p.default == 'non/existing/folder' - def test_notfoundok_class_no_error(self): + def test_nonexisting_class_no_error(self): self.P.d = 'non/existing/folder' assert self.P.d == 'non/existing/folder' - def test_notfoundok_instantiation_no_error(self): + def test_nonexisting_instantiation_no_error(self): p = self.P(d='non/existing/folder') assert p.d == 'non/existing/folder' - def test_notfoundok_set_no_error(self): + def test_nonexisting_set_no_error(self): p = self.P() p.d = 'non/existing/folder' assert p.d == 'non/existing/folder' From 14c1394e9f85d1c02d276a70615c48d07d8a4f5f Mon Sep 17 00:00:00 2001 From: maximlt Date: Sun, 30 Jul 2023 18:02:23 +0200 Subject: [PATCH 07/11] improve the docs --- examples/user_guide/Parameter_Types.ipynb | 32 ++++++++++++++++++++--- param/__init__.py | 13 ++++++--- 2 files changed, 39 insertions(+), 6 deletions(-) diff --git a/examples/user_guide/Parameter_Types.ipynb b/examples/user_guide/Parameter_Types.ipynb index 9f4462f50..7806250d3 100644 --- a/examples/user_guide/Parameter_Types.ipynb +++ b/examples/user_guide/Parameter_Types.ipynb @@ -700,7 +700,9 @@ "\n", "A Path can be set to a string specifying the path of a file or folder. In code, the string should be specified in POSIX (UNIX) style, using forward slashes / and starting from / if absolute or some other character if relative. When retrieved, the string will be in the format of the user's operating system. \n", "\n", - "Relative paths are converted to absolute paths by searching for a matching filename on the filesystem. If `search_paths` is provided and not empty, the folders in that list are searched for the given filename, in order, returning the absolute path for the first match found by appending the provided path to the search path. An IOError is raised if the file or folder is not found. If `search_paths` is empty (the default), the file or folder is expected to be in the current working directory.\n", + "Relative paths are converted to absolute paths by searching for a matching filename on the filesystem in the current workding directory (obtained with `os.getcwd()`). If `search_paths` is provided and not empty, the folders in that list are searched for the given filename, in order, returning the absolute path for the first match found by appending the provided path to the search path. An IOError is raised if the file or folder is not found. If `search_paths` is empty (the default), the file or folder is expected to be in the current working directory.\n", + "\n", + "When `check_exists` is set to `False` (default is `True`) the provided path can optionally exist. This is for instance useful to declare an output file path that is meant to be created at a later stage in a process. In the default case the path must exist, on Parameter instantiation and setting.\n", "\n", "Either a file or a folder name is accepted by `param.Path`, while `param.Filename` accepts only file names and `param.Foldername` accepts only folder names." ] @@ -716,6 +718,7 @@ " p = param.Path('Parameter_Types.ipynb')\n", " f = param.Filename('Parameter_Types.ipynb')\n", " d = param.Foldername('lib', search_paths=['/','/usr','/share'])\n", + " o = param.Filename('output.csv', check_exists=False)\n", " \n", "p = P()\n", "p.p" @@ -728,7 +731,7 @@ "metadata": {}, "outputs": [], "source": [ - "p.p='/usr/lib'\n", + "p.p = '/usr/lib'\n", "p.p" ] }, @@ -750,7 +753,7 @@ "outputs": [], "source": [ "with param.exceptions_summarized():\n", - " p.f='/usr/lib'" + " p.f = '/usr/lib'" ] }, { @@ -774,6 +777,29 @@ " p.d = 'Parameter_Types.ipynb'" ] }, + { + "cell_type": "code", + "execution_count": null, + "id": "53225144", + "metadata": {}, + "outputs": [], + "source": [ + "p.o # the output file doesn't exist yet" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "863b9817", + "metadata": {}, + "outputs": [], + "source": [ + "with open(p.o, 'w') as f:\n", + " f.write('Param is awesome!')\n", + "\n", + "p.o # it now exists, getting its value resolve the full file path" + ] + }, { "cell_type": "markdown", "id": "573079ef", diff --git a/param/__init__.py b/param/__init__.py index 2bc5b39f4..8dde75381 100644 --- a/param/__init__.py +++ b/param/__init__.py @@ -2305,12 +2305,11 @@ def __call__(self,path="",**params): class Path(Parameter): - """ - Parameter that can be set to a string specifying the path of a file or folder. + """Parameter that can be set to a string specifying the path of a file or folder. The string should be specified in UNIX style, but it will be returned in the format of the user's operating system. Please use - the Filename or Foldername classes if you require discrimination + the Filename or Foldername Parameters if you require discrimination between the two possibilities. The specified path can be absolute, or relative to either: @@ -2322,6 +2321,14 @@ class Path(Parameter): * any of the paths searched by resolve_path() (if search_paths is None). + + Parameters + ---------- + search_paths : list, default=[os.getcwd()] + List of paths to search the path from + check_exists: boolean, default=True + If True (default) the path must exist on instantiation and set, + otherwise the path can optionally exist. """ __slots__ = ['search_paths', 'check_exists'] From d0296507dc36d7d6a67847e2c5a83e581384219c Mon Sep 17 00:00:00 2001 From: maximlt Date: Sun, 30 Jul 2023 18:27:26 +0200 Subject: [PATCH 08/11] no need to remove the tmp dir --- tests/testpathparam.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/testpathparam.py b/tests/testpathparam.py index b251793ae..b15bb35b1 100644 --- a/tests/testpathparam.py +++ b/tests/testpathparam.py @@ -45,7 +45,6 @@ class P(param.Parameterized): self.P = P def tearDown(self): - shutil.rmtree(self.tmpdir1) os.chdir(self.curdir) def _check_defaults(self, p): From 0abe03f7d805d449d80f84f73f127bb30c058d1e Mon Sep 17 00:00:00 2001 From: maximlt Date: Mon, 31 Jul 2023 00:48:52 +0200 Subject: [PATCH 09/11] very basic validation --- param/__init__.py | 3 +++ tests/testpathparam.py | 4 ++++ 2 files changed, 7 insertions(+) diff --git a/param/__init__.py b/param/__init__.py index 8dde75381..6fcdbcc44 100644 --- a/param/__init__.py +++ b/param/__init__.py @@ -23,6 +23,7 @@ import re import datetime as dt import collections +import pathlib import typing import warnings @@ -2366,6 +2367,8 @@ def _validate(self, val): if not self.allow_None: raise ValueError(f'{_validate_error_prefix(self)} does not accept None') else: + if not isinstance(val, (str, pathlib.Path)): + raise ValueError(f'{_validate_error_prefix(self)} only take str or pathlib.Path types') try: self._resolve(val) except OSError as e: diff --git a/tests/testpathparam.py b/tests/testpathparam.py index b15bb35b1..ff0d4c8b4 100644 --- a/tests/testpathparam.py +++ b/tests/testpathparam.py @@ -82,6 +82,10 @@ def test_no_path_inst(self): p = self.P() assert p.a is None + def test_unsupported_type(self): + with pytest.raises(ValueError): + param.Path(1) + def test_inst_with_path(self): p = self.P(a=self.fa) assert isinstance(p.a, str) From 30b5dfebc531d10f36f705cf132ebddc68ac9df5 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Mon, 31 Jul 2023 11:38:42 +0200 Subject: [PATCH 10/11] Fix typo --- examples/user_guide/Parameter_Types.ipynb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/user_guide/Parameter_Types.ipynb b/examples/user_guide/Parameter_Types.ipynb index 7806250d3..9671648cc 100644 --- a/examples/user_guide/Parameter_Types.ipynb +++ b/examples/user_guide/Parameter_Types.ipynb @@ -700,7 +700,7 @@ "\n", "A Path can be set to a string specifying the path of a file or folder. In code, the string should be specified in POSIX (UNIX) style, using forward slashes / and starting from / if absolute or some other character if relative. When retrieved, the string will be in the format of the user's operating system. \n", "\n", - "Relative paths are converted to absolute paths by searching for a matching filename on the filesystem in the current workding directory (obtained with `os.getcwd()`). If `search_paths` is provided and not empty, the folders in that list are searched for the given filename, in order, returning the absolute path for the first match found by appending the provided path to the search path. An IOError is raised if the file or folder is not found. If `search_paths` is empty (the default), the file or folder is expected to be in the current working directory.\n", + "Relative paths are converted to absolute paths by searching for a matching filename on the filesystem in the current working directory (obtained with `os.getcwd()`). If `search_paths` is provided and not empty, the folders in that list are searched for the given filename, in order, returning the absolute path for the first match found by appending the provided path to the search path. An IOError is raised if the file or folder is not found. If `search_paths` is empty (the default), the file or folder is expected to be in the current working directory.\n", "\n", "When `check_exists` is set to `False` (default is `True`) the provided path can optionally exist. This is for instance useful to declare an output file path that is meant to be created at a later stage in a process. In the default case the path must exist, on Parameter instantiation and setting.\n", "\n", From b28408d49b8130ec1d99551a961b28984fca2967 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Mon, 31 Jul 2023 16:19:40 +0200 Subject: [PATCH 11/11] Update param/__init__.py --- param/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/param/__init__.py b/param/__init__.py index 6fcdbcc44..78c2ce664 100644 --- a/param/__init__.py +++ b/param/__init__.py @@ -2386,7 +2386,7 @@ def __get__(self, obj, objtype): try: path = self._resolve(raw_path) except OSError: - if self.check_exists: + if self.check_exists: raise else: path = raw_path