Skip to content
Merged
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
16 changes: 14 additions & 2 deletions pyhealth/datasets/base_dataset.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import logging
import os
import pickle
import posixpath
from abc import ABC
from pathlib import Path
from typing import Dict, Iterator, Iterable, List, Optional, Any, Callable
Expand Down Expand Up @@ -106,10 +107,21 @@ def is_url(path: str) -> bool:


def clean_path(path: str) -> str:
"""Clean a path string."""
"""Clean a path string.

URL paths are normalised as POSIX paths on every OS; local paths are
expanded and resolved.

Examples:
>>> from pyhealth.datasets.base_dataset import clean_path
>>> clean_path("https://example.org/a/b/../c.csv")
'https://example.org/a/c.csv'
"""
if is_url(path):
parsed = urlparse(path)
cleaned_path = os.path.normpath(parsed.path)
# A URL path is POSIX whatever the host OS: os.path.normpath would
# rewrite the separators as backslashes on Windows.
cleaned_path = posixpath.normpath(parsed.path)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[low] Empty URL path becomes '/.' after normalisation

For a URL with no path, such as "https://example.org", parsed.path is "" and posixpath.normpath("") returns ".", so clean_path returns "https://example.org/.". This already happens on Linux and macOS and predates this PR, and current callers always pass a path after the root, so it is not reached in practice. A fix would return the path unchanged when parsed.path is empty, for example: cleaned_path = posixpath.normpath(parsed.path) if parsed.path else "". It does not block the merge.

# Rebuild the full URL
return urlunparse(parsed._replace(path=cleaned_path))
else:
Expand Down
45 changes: 45 additions & 0 deletions tests/core/test_clean_path.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import ntpath
import unittest
from unittest.mock import patch

from pyhealth.datasets import base_dataset
from pyhealth.datasets.base_dataset import clean_path

# (input URL, expected output). A URL path is POSIX whatever the host OS.
URL_CASES = [
(
"https://storage.googleapis.com/pyhealth/Synthetic_MIMIC-III/PATIENTS.csv.gz",
"https://storage.googleapis.com/pyhealth/Synthetic_MIMIC-III/PATIENTS.csv.gz",
),
(
"https://storage.googleapis.com/pyhealth/Synthetic_MIMIC-III//PATIENTS.csv",
"https://storage.googleapis.com/pyhealth/Synthetic_MIMIC-III/PATIENTS.csv",
),
(
"https://example.org/a/b/../c.csv",
"https://example.org/a/c.csv",
),
(
"s3://bucket/key/file.csv.gz",
"s3://bucket/key/file.csv.gz",
),
]


class TestCleanPathURL(unittest.TestCase):
def test_url_path_is_normalised_as_posix(self):
for url, expected in URL_CASES:
with self.subTest(url=url):
self.assertEqual(clean_path(url), expected)

def test_url_path_is_unaffected_by_windows_os_path(self):
# Reproduce the Windows behaviour on any OS: ntpath.normpath rewrites
# "/" as "\\", which must not leak into a URL.
with patch.object(base_dataset.os, "path", ntpath):
for url, expected in URL_CASES:
with self.subTest(url=url):
self.assertEqual(clean_path(url), expected)


if __name__ == "__main__":
unittest.main()
Loading