From 20760defdc116974f96d6ec1ddbe7e40b64511d7 Mon Sep 17 00:00:00 2001 From: liyang Date: Fri, 1 Nov 2024 12:51:27 +0800 Subject: [PATCH 1/3] feat: introduce HFPath --- megfile/__init__.py | 2 + megfile/config.py | 3 + megfile/fsspec.py | 537 ++++++++++++++++++++++++++++++++++++++++++++ megfile/hf_path.py | 45 ++++ 4 files changed, 587 insertions(+) create mode 100644 megfile/fsspec.py create mode 100644 megfile/hf_path.py diff --git a/megfile/__init__.py b/megfile/__init__.py index b766be7a..017b589e 100644 --- a/megfile/__init__.py +++ b/megfile/__init__.py @@ -65,6 +65,7 @@ is_hdfs, ) from megfile.hdfs_path import HdfsPath +from megfile.hf_path import HFPath from megfile.http import ( http_exists, http_getmtime, @@ -396,6 +397,7 @@ "hdfs_makedirs", "S3Path", "FSPath", + "HFPath", "HttpPath", "HttpsPath", "StdioPath", diff --git a/megfile/config.py b/megfile/config.py index 7ad5d57b..4934e71c 100644 --- a/megfile/config.py +++ b/megfile/config.py @@ -60,5 +60,8 @@ SFTP_MAX_RETRY_TIMES = int( os.getenv("MEGFILE_SFTP_MAX_RETRY_TIMES") or DEFAULT_MAX_RETRY_TIMES ) +HF_MAX_RETRY_TIMES = int( + os.getenv("MEGFILE_HF_MAX_RETRY_TIMES") or DEFAULT_MAX_RETRY_TIMES +) HTTP_AUTH_HEADERS = ("Authorization", "Www-Authenticate", "Cookie", "Cookie2") diff --git a/megfile/fsspec.py b/megfile/fsspec.py new file mode 100644 index 00000000..5080fecc --- /dev/null +++ b/megfile/fsspec.py @@ -0,0 +1,537 @@ +import io +import os +from datetime import datetime +from typing import IO, BinaryIO, Callable, Iterator, List, Optional, Tuple + +import fsspec + +from megfile.errors import _create_missing_ok_generator +from megfile.interfaces import ContextIterator, FileEntry, PathLike, StatResult, URIPath + + +def _parse_is_link(info): + if "islink" in info: # LocalFileSystem + return info["islink"] + return info["type"] == "link" # SFTPFileSystem + + +def _to_timestamp(data): + if isinstance(data, datetime): + return data.timestamp() + return data + + +def _parse_ctime(info): + if "created" in info: # LocalFileSystem + return _to_timestamp(info["created"]) + if "time" in info: # SFTPFileSystem + return _to_timestamp(info["time"]) + return None + + +def _parse_mtime(info): + if "mtime" in info: # LocalFileSystem & SFTPFileSystem + return _to_timestamp(info["mtime"]) + if "last_commit" in info and "date" in info["last_commit"]: # HfFileSystem + return _to_timestamp(info["last_commit"]["date"]) + if "LastModified" in info: # S3FileSystem + return _to_timestamp(info["LastModified"]) + return None + + +def _make_stat(info): + return StatResult( + islnk=_parse_is_link(info), + isdir=info["type"] == "directory", + size=info["size"], + ctime=_parse_ctime(info), + mtime=_parse_mtime(info), + extra=info, + ) + + +def _make_entry(filesystem, info): + return FileEntry( + name=os.path.basename(info["name"]), + path=filesystem.unstrip_protocol(info["name"]), + stat=_make_stat(info), + ) + + +class BaseFSSpecPath(URIPath): + protocol: str + filesystem: fsspec.AbstractFileSystem + + def __init__(self, path: PathLike, *other_paths: PathLike): + super().__init__(path, *other_paths) + + def exists(self, followlinks: bool = False) -> bool: + """ + Test if the path exists + + :param followlinks: False if regard symlink as file, else True + :returns: True if the path exists, else False + + """ + return self.filesystem.exists(self.path_without_protocol) + + def getmtime(self, follow_symlinks: bool = False) -> float: + """ + Get last-modified time of the file on the given path (in Unix timestamp format). + + If the path is an existent directory, + return the latest modified time of all file in it. + + :returns: last-modified time + """ + return self.filesystem.modified(self.path_without_protocol) + + def getsize(self, follow_symlinks: bool = False) -> int: + """ + Get file size on the given file path (in bytes). + + If the path in a directory, return the sum of all file size in it, + including file in subdirectories (if exist). + + The result excludes the size of directory itself. In other words, + return 0 Byte on an empty directory path. + + :returns: File size + + """ + return self.filesystem.size(self.path_without_protocol) + + def glob( + self, + pattern, + recursive: bool = True, + missing_ok: bool = True, + followlinks: bool = False, + ) -> List["BaseFSSpecPath"]: + """Return path list in ascending alphabetical order, + in which path matches glob pattern + + 1. If doesn't match any path, return empty list + Notice: ``glob.glob`` in standard library returns ['a/'] instead of + empty list when pathname is like `a/**`, recursive is True and directory 'a' + doesn't exist. fs_glob behaves like ``glob.glob`` in standard library under + such circumstance. + 2. No guarantee that each path in result is different, which means: + Assume there exists a path `/a/b/c/b/d.txt` + use path pattern like `/**/b/**/*.txt` to glob, + the path above will be returned twice + 3. `**` will match any matched file, directory, symlink and '' by default, + when recursive is `True` + 4. fs_glob returns same as glob.glob(pathname, recursive=True) in + ascending alphabetical order. + 5. Hidden files (filename stars with '.') will not be found in the result + + :param pattern: Glob the given relative pattern in the directory represented + by this path + :param recursive: If False, `**` will not search directory recursively + :param missing_ok: If False and target path doesn't match any file, + raise FileNotFoundError + :returns: A list contains paths match `pathname` + """ + return list( + self.iglob( + pattern=pattern, + recursive=recursive, + missing_ok=missing_ok, + followlinks=followlinks, + ) + ) + + def glob_stat( + self, + pattern, + recursive: bool = True, + missing_ok: bool = True, + followlinks: bool = False, + ) -> Iterator[FileEntry]: + """Return a list contains tuples of path and file stat, + in ascending alphabetical order, in which path matches glob pattern + + 1. If doesn't match any path, return empty list + Notice: ``glob.glob`` in standard library returns ['a/'] instead of + empty list when pathname is like `a/**`, recursive is True and + directory 'a' doesn't exist. fsspec_glob behaves like ``glob.glob`` in + standard library under such circumstance. + 2. No guarantee that each path in result is different, which means: + Assume there exists a path `/a/b/c/b/d.txt` + use path pattern like `/**/b/**/*.txt` to glob, + the path above will be returned twice + 3. `**` will match any matched file, directory, symlink and '' by default, + when recursive is `True` + 4. fs_glob returns same as glob.glob(pathname, recursive=True) in + ascending alphabetical order. + 5. Hidden files (filename stars with '.') will not be found in the result + + :param pattern: Glob the given relative pattern in the directory represented + by this path + :param recursive: If False, `**` will not search directory recursively + :param missing_ok: If False and target path doesn't match any file, + raise FileNotFoundError + :returns: A list contains tuples of path and file stat, + in which paths match `pathname` + """ + + def create_generator(): + for info in self.filesystem.find( + self.path_without_protocol, withdirs=True, detail=True + ).values(): + yield _make_entry(self.filesystem, info) + + return _create_missing_ok_generator( + create_generator(), + missing_ok, + FileNotFoundError("No match any file: %r" % self.path_with_protocol), + ) + + def iglob( + self, + pattern, + recursive: bool = True, + missing_ok: bool = True, + followlinks: bool = False, + ) -> Iterator["BaseFSSpecPath"]: + """Return path iterator in ascending alphabetical order, + in which path matches glob pattern + + 1. If doesn't match any path, return empty list + Notice: ``glob.glob`` in standard library returns ['a/'] instead of + empty list when pathname is like `a/**`, recursive is True and + directory 'a' doesn't exist. fs_glob behaves like ``glob.glob`` in + standard library under such circumstance. + 2. No guarantee that each path in result is different, which means: + Assume there exists a path `/a/b/c/b/d.txt` + use path pattern like `/**/b/**/*.txt` to glob, + the path above will be returned twice + 3. `**` will match any matched file, directory, symlink and '' by default, + when recursive is `True` + 4. fs_glob returns same as glob.glob(pathname, recursive=True) in + ascending alphabetical order. + 5. Hidden files (filename stars with '.') will not be found in the result + + :param pattern: Glob the given relative pattern in the directory represented + by this path + :param recursive: If False, `**` will not search directory recursively + :param missing_ok: If False and target path doesn't match any file, + raise FileNotFoundError + :returns: An iterator contains paths match `pathname` + """ + for file_entry in self.glob_stat( + pattern=pattern, + recursive=recursive, + missing_ok=missing_ok, + followlinks=followlinks, + ): + yield self.from_path(file_entry.path) + + def is_dir(self, followlinks: bool = False) -> bool: + """ + Test if a path is directory + + .. note:: + + The difference between this function and ``os.path.isdir`` is that + this function regard symlink as file + + :param followlinks: False if regard symlink as file, else True + :returns: True if the path is a directory, else False + + """ + return self.filesystem.isdir(self.path_without_protocol) + + def is_file(self, followlinks: bool = False) -> bool: + """ + Test if a path is file + + .. note:: + + The difference between this function and ``os.path.isfile`` is that + this function regard symlink as file + + :param followlinks: False if regard symlink as file, else True + :returns: True if the path is a file, else False + + """ + return self.filesystem.isfile(self.path_without_protocol) + + def listdir(self, followlinks: bool = False) -> List[str]: + """ + Get all contents of given fsspec path. + The result is in ascending alphabetical order. + + :returns: All contents have in the path in ascending alphabetical order + """ + entries = list(self.scandir(followlinks=followlinks)) + return sorted([entry.name for entry in entries]) + + def iterdir(self, followlinks: bool = False) -> Iterator["BaseFSSpecPath"]: + """ + Get all contents of given fsspec path. + The result is in ascending alphabetical order. + + :returns: All contents have in the path in ascending alphabetical order + """ + for path in self.listdir(followlinks=followlinks): + yield self.joinpath(path) + + def load(self) -> BinaryIO: + """Read all content on specified path and write into memory + + User should close the BinaryIO manually + + :returns: Binary stream + """ + with self.open(mode="rb") as f: + data = f.read() + return io.BytesIO(data) + + def save(self, file_object: BinaryIO): + """Write the opened binary stream to path + If parent directory of path doesn't exist, it will be created. + + :param file_object: stream to be read + """ + with self.open(mode="wb") as output: + output.write(file_object.read()) + + def mkdir(self, mode=0o777, parents: bool = False, exist_ok: bool = False): + """ + make a directory with fsspec, including parent directory. + If there exists a file on the path, raise FileExistsError + + :param mode: If mode is given, it is combined with the process’ umask value to + determine the file mode and access flags. + :param parents: If parents is true, any missing parents of this path + are created as needed; If parents is false (the default), + a missing parent raises FileNotFoundError. + :param exist_ok: If False and target directory exists, raise FileExistsError + + :raises: FileExistsError + """ + if self.exists(): + if not exist_ok: + raise FileExistsError(f"File exists: '{self.path_with_protocol}'") + return + return self.filesystem.mkdir(self.path_without_protocol, create_parents=parents) + + def rmdir(self): + """ + Remove this directory. The directory must be empty. + """ + return self.filesystem.rmdir(self._real_path) + + def realpath(self) -> str: + """Return the real path of given path + + :returns: Real path of given path + """ + return self.resolve().path_with_protocol + + def copy( + self, + dst_path: PathLike, + callback: Optional[Callable[[int], None]] = None, + followlinks: bool = False, + overwrite: bool = True, + ): + """ + Copy the file to the given destination path. + + :param dst_path: The destination path to copy the file to. + :param callback: An optional callback function that takes an integer parameter + and is called periodically during the copy operation to report the number + of bytes copied. + :param followlinks: Whether to follow symbolic links when copying directories. + :raises IsADirectoryError: If the source is a directory. + :raises OSError: If there is an error copying the file. + """ + return self.filesystem.copy( + self.path_without_protocol, dst_path, recursive=False + ) + + def sync( + self, + dst_path: PathLike, + followlinks: bool = False, + force: bool = False, + overwrite: bool = True, + ): + """Copy file/directory on src_url to dst_url + + :param dst_url: Given destination path + :param followlinks: False if regard symlink as file, else True + :param force: Sync file forcible, do not ignore same files, + priority is higher than 'overwrite', default is False + :param overwrite: whether or not overwrite file when exists, default is True + """ + return self.filesystem.copy( + self.path_without_protocol, dst_path, recursive=True + ) + + def rename(self, dst_path: PathLike, overwrite: bool = True) -> "BaseFSSpecPath": + """ + rename file with fsspec + + :param dst_path: Given destination path + :param overwrite: whether or not overwrite file when exists + """ + self.filesystem.mv(self.path_without_protocol, dst_path, recursive=False) + + def move(self, dst_path: PathLike, overwrite: bool = True) -> "BaseFSSpecPath": + """ + move file/directory with fsspec + + :param dst_path: Given destination path + :param overwrite: whether or not overwrite file when exists + """ + self.filesystem.mv(self.path_without_protocol, dst_path, recursive=True) + + def unlink(self, missing_ok: bool = False) -> None: + """ + Remove the file with fsspec + + :param missing_ok: if False and target file not exists, raise FileNotFoundError + """ + if missing_ok and not self.exists(): + return + self.filesystem.rm(self.path_without_protocol, recursive=False) + + def remove(self, missing_ok: bool = False) -> None: + """ + Remove the file or directory with fsspec + + :param missing_ok: if False and target file/directory not exists, + raise FileNotFoundError + """ + if missing_ok and not self.exists(): + return + self.filesystem.rm(self.path_without_protocol, recursive=True) + + def scan(self, missing_ok: bool = True, followlinks: bool = False) -> Iterator[str]: + """ + Iteratively traverse only files in given directory, in alphabetical order. + Every iteration on generator yields a path string. + + If path is a file path, yields the file only + If path is a non-existent path, return an empty generator + If path is a bucket path, return all file paths in the bucket + + :param missing_ok: If False and there's no file in the directory, + raise FileNotFoundError + :returns: A file path generator + """ + scan_stat_iter = self.scan_stat(missing_ok=missing_ok, followlinks=followlinks) + + for file_entry in scan_stat_iter: + yield file_entry.path + + def scan_stat( + self, missing_ok: bool = True, followlinks: bool = False + ) -> Iterator[FileEntry]: + """ + Iteratively traverse only files in given directory, in alphabetical order. + Every iteration on generator yields a tuple of path string and file stat + + :param missing_ok: If False and there's no file in the directory, + raise FileNotFoundError + :returns: A file path generator + """ + + def create_generator(): + for info in self.filesystem.find( + self.path_without_protocol, withdirs=False, detail=True + ).values(): + yield _make_entry(self.filesystem, info) + + return _create_missing_ok_generator( + create_generator(), + missing_ok, + FileNotFoundError("No match any file in: %r" % self.path_with_protocol), + ) + + def scandir(self) -> Iterator[FileEntry]: + """ + Get all content of given file path. + + :returns: An iterator contains all contents have prefix path + """ + if not self.exists(): + raise FileNotFoundError("No such directory: %r" % self.path_with_protocol) + + if not self.is_dir(): + raise NotADirectoryError("Not a directory: %r" % self.path_with_protocol) + + def create_generator(): + for info in self.filesystem.ls(self.path_without_protocol, detail=True): + yield _make_entry(self.filesystem, info) + + return ContextIterator(create_generator()) + + def stat(self, follow_symlinks=True) -> StatResult: + """ + Get StatResult of file with fsspec, including file size and mtime, + referring to fs_getsize and fs_getmtime + + :returns: StatResult + """ + return _make_stat(self.filesystem.info(self.path_without_protocol)) + + def walk( + self, followlinks: bool = False + ) -> Iterator[Tuple[str, List[str], List[str]]]: + """ + Generate the file names in a directory tree by walking the tree top-down. + For each directory in the tree rooted at directory path (including path itself), + it yields a 3-tuple (root, dirs, files). + + - root: a string of current path + - dirs: name list of subdirectories (excluding '.' and '..' if they exist) + in 'root'. The list is sorted by ascending alphabetical order + - files: name list of non-directory files (link is regarded as file) in 'root'. + The list is sorted by ascending alphabetical order + + If path not exists, or path is a file (link is regarded as file), + return an empty generator + + .. note:: + + Be aware that setting ``followlinks`` to True can lead to infinite recursion + if a link points to a parent directory of itself. fs_walk() does not keep + track of the directories it visited already. + + :param followlinks: False if regard symlink as file, else True + :returns: A 3-tuple generator + """ + if not self.exists(followlinks=followlinks): + return + + if self.is_file(followlinks=followlinks): + return + + for root, dirs, files in self.filesystem.walk(self.path_without_protocol): + yield self.from_path(root).path_with_protocol, dirs, files + + def open( + self, + mode: str = "r", + buffering=-1, + encoding: Optional[str] = None, + errors: Optional[str] = None, + **kwargs, + ) -> IO: + """Open a file on the path. + + :param mode: Mode to open file + :param buffering: buffering is an optional integer used to + set the buffering policy. + :param encoding: encoding is the name of the encoding used to decode or encode + the file. This should only be used in text mode. + :param errors: errors is an optional string that specifies how encoding and + decoding errors are to be handled—this cannot be used in binary mode. + :returns: File-Like object + """ + return self.filesystem.open(self.path_without_protocol, mode) diff --git a/megfile/hf_path.py b/megfile/hf_path.py new file mode 100644 index 00000000..bb48dc6b --- /dev/null +++ b/megfile/hf_path.py @@ -0,0 +1,45 @@ +from logging import getLogger as get_logger + +from megfile.config import HF_MAX_RETRY_TIMES +from megfile.errors import http_should_retry, patch_method +from megfile.fsspec import BaseFSSpecPath +from megfile.smart import SmartPath +from megfile.utils import cached_classproperty + +_logger = get_logger(__name__) + +MAX_RETRIES = HF_MAX_RETRY_TIMES + + +def _patch_huggingface_session(): + from huggingface_hub.utils._http import UniqueRequestIdAdapter + + def after_callback(response, *args, **kwargs): + if response.status_code in (429, 500, 502, 503, 504): + response.raise_for_status() + return response + + def before_callback(method, url, **kwargs): + _logger.debug( + "send http request: %s %r, with parameters: %s", method, url, kwargs + ) + + UniqueRequestIdAdapter.send = patch_method( # pyre-ignore[16] + UniqueRequestIdAdapter.send, # pytype: disable=attribute-error + max_retries=MAX_RETRIES, + after_callback=after_callback, + before_callback=before_callback, + should_retry=http_should_retry, + ) + + +@SmartPath.register +class HFPath(BaseFSSpecPath): + protocol = "hf" + + @cached_classproperty + def filesystem(self): + from huggingface_hub import HfFileSystem + + _patch_huggingface_session() + return HfFileSystem() From 4d65b59e185372cae5367d83504f74b36c51b91d Mon Sep 17 00:00:00 2001 From: liyang Date: Wed, 20 Nov 2024 12:52:38 +0800 Subject: [PATCH 2/3] make fsspec optional --- megfile/fsspec.py | 7 +++++-- requirements-fsspec.txt | 2 ++ 2 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 requirements-fsspec.txt diff --git a/megfile/fsspec.py b/megfile/fsspec.py index 5080fecc..9df40c16 100644 --- a/megfile/fsspec.py +++ b/megfile/fsspec.py @@ -3,7 +3,10 @@ from datetime import datetime from typing import IO, BinaryIO, Callable, Iterator, List, Optional, Tuple -import fsspec +try: + import fsspec +except ImportError: # pragma: no cover + fsspec = None from megfile.errors import _create_missing_ok_generator from megfile.interfaces import ContextIterator, FileEntry, PathLike, StatResult, URIPath @@ -60,7 +63,7 @@ def _make_entry(filesystem, info): class BaseFSSpecPath(URIPath): protocol: str - filesystem: fsspec.AbstractFileSystem + filesystem: 'fsspec.AbstractFileSystem' def __init__(self, path: PathLike, *other_paths: PathLike): super().__init__(path, *other_paths) diff --git a/requirements-fsspec.txt b/requirements-fsspec.txt new file mode 100644 index 00000000..9b82b33c --- /dev/null +++ b/requirements-fsspec.txt @@ -0,0 +1,2 @@ +fsspec +huggingface_hub \ No newline at end of file From 7db4103cdd571579d6c359529872255cd4a8880a Mon Sep 17 00:00:00 2001 From: liyang Date: Wed, 20 Nov 2024 12:59:29 +0800 Subject: [PATCH 3/3] make lint happy --- megfile/fsspec.py | 12 ++++++------ tests/test_smart_path.py | 5 ++++- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/megfile/fsspec.py b/megfile/fsspec.py index 9df40c16..021d668c 100644 --- a/megfile/fsspec.py +++ b/megfile/fsspec.py @@ -63,7 +63,7 @@ def _make_entry(filesystem, info): class BaseFSSpecPath(URIPath): protocol: str - filesystem: 'fsspec.AbstractFileSystem' + filesystem: "fsspec.AbstractFileSystem" def __init__(self, path: PathLike, *other_paths: PathLike): super().__init__(path, *other_paths) @@ -375,7 +375,7 @@ def sync( self.path_without_protocol, dst_path, recursive=True ) - def rename(self, dst_path: PathLike, overwrite: bool = True) -> "BaseFSSpecPath": + def rename(self, dst_path: PathLike, overwrite: bool = True): """ rename file with fsspec @@ -384,7 +384,7 @@ def rename(self, dst_path: PathLike, overwrite: bool = True) -> "BaseFSSpecPath" """ self.filesystem.mv(self.path_without_protocol, dst_path, recursive=False) - def move(self, dst_path: PathLike, overwrite: bool = True) -> "BaseFSSpecPath": + def move(self, dst_path: PathLike, overwrite: bool = True): """ move file/directory with fsspec @@ -393,7 +393,7 @@ def move(self, dst_path: PathLike, overwrite: bool = True) -> "BaseFSSpecPath": """ self.filesystem.mv(self.path_without_protocol, dst_path, recursive=True) - def unlink(self, missing_ok: bool = False) -> None: + def unlink(self, missing_ok: bool = False): """ Remove the file with fsspec @@ -403,7 +403,7 @@ def unlink(self, missing_ok: bool = False) -> None: return self.filesystem.rm(self.path_without_protocol, recursive=False) - def remove(self, missing_ok: bool = False) -> None: + def remove(self, missing_ok: bool = False): """ Remove the file or directory with fsspec @@ -456,7 +456,7 @@ def create_generator(): FileNotFoundError("No match any file in: %r" % self.path_with_protocol), ) - def scandir(self) -> Iterator[FileEntry]: + def scandir(self, followlinks: bool = False) -> Iterator[FileEntry]: """ Get all content of given file path. diff --git a/tests/test_smart_path.py b/tests/test_smart_path.py index 51b93607..883d3ae8 100644 --- a/tests/test_smart_path.py +++ b/tests/test_smart_path.py @@ -12,6 +12,7 @@ ) from megfile.fs_path import FSPath from megfile.hdfs_path import HdfsPath +from megfile.hf_path import HFPath from megfile.http_path import HttpPath, HttpsPath from megfile.interfaces import Access from megfile.s3_path import S3Path @@ -58,7 +59,7 @@ def s3_empty_client(mocker): def test_register_result(): - assert len(SmartPath._registered_protocols) == 7 + assert len(SmartPath._registered_protocols) == 8 assert S3Path.protocol in SmartPath._registered_protocols assert FSPath.protocol in SmartPath._registered_protocols assert HttpPath.protocol in SmartPath._registered_protocols @@ -66,6 +67,7 @@ def test_register_result(): assert StdioPath.protocol in SmartPath._registered_protocols assert SftpPath.protocol in SmartPath._registered_protocols assert HdfsPath.protocol in SmartPath._registered_protocols + assert HFPath.protocol in SmartPath._registered_protocols assert SmartPath._registered_protocols[S3Path.protocol] == S3Path assert SmartPath._registered_protocols[FSPath.protocol] == FSPath @@ -73,6 +75,7 @@ def test_register_result(): assert SmartPath._registered_protocols[HttpsPath.protocol] == HttpsPath assert SmartPath._registered_protocols[StdioPath.protocol] == StdioPath assert SmartPath._registered_protocols[HdfsPath.protocol] == HdfsPath + assert SmartPath._registered_protocols[HFPath.protocol] == HFPath assert SmartPath.from_uri(FS_TEST_ABSOLUTE_PATH) == SmartPath(FS_TEST_ABSOLUTE_PATH)