From 68545497c906905d5bd687957ebb9fec820b6d46 Mon Sep 17 00:00:00 2001 From: Min RK Date: Mon, 26 May 2025 13:37:56 +0200 Subject: [PATCH 1/5] validate inputs to is_hidden inputs must be absolute. Raise ValueError instead of possibly incorrect result when input is invalid --- jupyter_core/paths.py | 54 ++++++++++++++++++++++++++++--------------- 1 file changed, 35 insertions(+), 19 deletions(-) diff --git a/jupyter_core/paths.py b/jupyter_core/paths.py index d11a949..c05d4fb 100644 --- a/jupyter_core/paths.py +++ b/jupyter_core/paths.py @@ -433,7 +433,7 @@ def exists(path: str) -> bool: return True -def is_file_hidden_win(abs_path: str, stat_res: Optional[Any] = None) -> bool: +def is_file_hidden_win(abs_path: str | Path, stat_res: Optional[Any] = None) -> bool: """Is a file hidden? This only checks the file itself; it should be called in combination with @@ -449,7 +449,8 @@ def is_file_hidden_win(abs_path: str, stat_res: Optional[Any] = None) -> bool: The result of calling stat() on abs_path. If not passed, this function will call stat() internally. """ - if Path(abs_path).name.startswith("."): + abs_path = Path(abs_path) + if abs_path.name.startswith("."): return True if stat_res is None: @@ -494,12 +495,13 @@ def is_file_hidden_posix(abs_path: str, stat_res: Optional[Any] = None) -> bool: The result of calling stat() on abs_path. If not passed, this function will call stat() internally. """ - if Path(abs_path).name.startswith("."): + abs_path = Path(abs_path) + if abs_path.name.startswith("."): return True if stat_res is None or stat.S_ISLNK(stat_res.st_mode): try: - stat_res = Path(abs_path).stat() + stat_res = abs_path.stat() except OSError as e: if e.errno == errno.ENOENT: return False @@ -524,7 +526,7 @@ def is_file_hidden_posix(abs_path: str, stat_res: Optional[Any] = None) -> bool: is_file_hidden = is_file_hidden_posix -def is_hidden(abs_path: str, abs_root: str = "") -> bool: +def is_hidden(abs_path: str | Path, abs_root: str | Path = "") -> bool: """Is a file hidden or contained in a hidden directory? This will start with the rightmost path element and work backwards to the @@ -538,42 +540,56 @@ def is_hidden(abs_path: str, abs_root: str = "") -> bool: Parameters ---------- - abs_path : unicode + abs_path : str or Path The absolute path to check for hidden directories. - abs_root : unicode + abs_root : str or Path The absolute path of the root directory in which hidden directories should be checked for. """ - abs_path = os.path.normpath(abs_path) - abs_root = os.path.normpath(abs_root) + abs_path = Path(os.path.normpath(abs_path)) + if abs_root: + abs_root = Path(os.path.normpath(abs_root)) + else: + abs_root = abs_path.root if abs_path == abs_root: + # root itself is never hidden return False + # check that arguments are valid + if not abs_path.is_absolute(): + _msg = f"{abs_path=} is not absolute. abs_path must be absolute." + raise ValueError(_msg) + if not abs_root.is_absolute(): + _msg = f"{abs_root=} is not absolute. abs_root must be absolute." + raise ValueError(_msg) + if not abs_path.is_relative_to(abs_root): + _msg = ( + f"{abs_path=} is not a subdirectory of {abs_root=}. abs_path must be within abs_root." + ) + raise ValueError(_msg) + if is_file_hidden(abs_path): return True - if not abs_root: - abs_root = abs_path.split(os.sep, 1)[0] + os.sep - inside_root = abs_path[len(abs_root) :] - if any(part.startswith(".") for part in Path(inside_root).parts): + relative_path = abs_path.relative_to(abs_root) + if any(part.startswith(".") for part in relative_path.parts): return True # check UF_HIDDEN on any location up to root. # is_file_hidden() already checked the file, so start from its parent dir - path = str(Path(abs_path).parent) - while path and path.startswith(abs_root) and path != abs_root: - if not Path(path).exists(): - path = str(Path(path).parent) + for parent in abs_path.parents: + if not parent.exists(): continue + if parent == abs_root: + break try: # may fail on Windows junctions - st = os.lstat(path) + st = parent.lstat() except OSError: return True if getattr(st, "st_flags", 0) & UF_HIDDEN: return True - path = str(Path(path).parent) return False From 8cad96b69b13a7c612f8cc0162cb81555747d5bb Mon Sep 17 00:00:00 2001 From: Min RK Date: Mon, 26 May 2025 14:03:04 +0200 Subject: [PATCH 2/5] path.root is a str --- jupyter_core/paths.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jupyter_core/paths.py b/jupyter_core/paths.py index c05d4fb..e623f66 100644 --- a/jupyter_core/paths.py +++ b/jupyter_core/paths.py @@ -550,7 +550,7 @@ def is_hidden(abs_path: str | Path, abs_root: str | Path = "") -> bool: if abs_root: abs_root = Path(os.path.normpath(abs_root)) else: - abs_root = abs_path.root + abs_root = Path(abs_path.root) if abs_path == abs_root: # root itself is never hidden From 28b453d20f602779d12d5fdaeb24cee99f65290f Mon Sep 17 00:00:00 2001 From: Min RK Date: Mon, 26 May 2025 14:07:09 +0200 Subject: [PATCH 3/5] missing Path in is_file_hidden --- jupyter_core/paths.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jupyter_core/paths.py b/jupyter_core/paths.py index e623f66..4388539 100644 --- a/jupyter_core/paths.py +++ b/jupyter_core/paths.py @@ -479,7 +479,7 @@ def is_file_hidden_win(abs_path: str | Path, stat_res: Optional[Any] = None) -> return False -def is_file_hidden_posix(abs_path: str, stat_res: Optional[Any] = None) -> bool: +def is_file_hidden_posix(abs_path: str | Path, stat_res: Optional[Any] = None) -> bool: """Is a file hidden? This only checks the file itself; it should be called in combination with From 0d44430f880042ea2b4b72b6fbd873c33639fe9b Mon Sep 17 00:00:00 2001 From: Min RK Date: Mon, 26 May 2025 14:12:21 +0200 Subject: [PATCH 4/5] exercise relative paths to is_hidden --- tests/test_paths.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/test_paths.py b/tests/test_paths.py index 0782792..8453afa 100644 --- a/tests/test_paths.py +++ b/tests/test_paths.py @@ -465,6 +465,19 @@ def test_is_hidden(): assert is_hidden(subdir78, root) +@pytest.mark.parametrize( + ("abs_path", "abs_root"), + [ + ("relative.py", "/absolute"), + ("/absolute/path.py", "relative"), + ("/absolute/path.py", "/absolute/not/parent"), + ], +) +def test_is_hidden_invalid(abs_path, abs_root): + with pytest.raises(ValueError, match="abs"): + is_hidden(abs_path, abs_root) + + @pytest.mark.skipif( not ( sys.platform == "win32" From d285450df8bfbfe8e61168d903fcbbb4fc31b359 Mon Sep 17 00:00:00 2001 From: Min RK Date: Mon, 26 May 2025 14:18:07 +0200 Subject: [PATCH 5/5] get last parent for default root Windows is more complex since it combines root and drive to be absolute cast to list because parents[-1] is new in 3.10 --- jupyter_core/paths.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jupyter_core/paths.py b/jupyter_core/paths.py index 4388539..f133223 100644 --- a/jupyter_core/paths.py +++ b/jupyter_core/paths.py @@ -550,7 +550,7 @@ def is_hidden(abs_path: str | Path, abs_root: str | Path = "") -> bool: if abs_root: abs_root = Path(os.path.normpath(abs_root)) else: - abs_root = Path(abs_path.root) + abs_root = list(abs_path.parents)[-1] if abs_path == abs_root: # root itself is never hidden