Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Derive from path #28

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
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
176 changes: 50 additions & 126 deletions pyiron_snippets/files.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
from __future__ import annotations

from pathlib import Path
from pathlib import Path, PosixPath, WindowsPath
import shutil
import sys

# Determine the correct base class based on the platform
BasePath = WindowsPath if sys.platform == "win32" else PosixPath


def delete_files_and_directories_recursively(path):
Expand Down Expand Up @@ -38,78 +42,38 @@ def categorize_folder_items(folder_path):
return results


def _resolve_directory_and_path(
file_name: str,
directory: DirectoryObject | str | None = None,
default_directory: str = ".",
):
"""
Internal routine to separate the file name and the directory in case
file name is given in absolute path etc.
"""
path = Path(file_name)
file_name = path.name
if path.is_absolute():
if directory is not None:
raise ValueError(
"You cannot set `directory` when `file_name` is an absolute path"
)
# If absolute path, take that of new_file_name regardless of the
# name of directory
directory = str(path.parent)
else:
if directory is None:
# If directory is not given, take default directory
directory = default_directory
else:
# If the directory is given, use it as the main path and append
# additional path if given in new_file_name
if isinstance(directory, DirectoryObject):
directory = directory.path
directory = directory / path.parent
if not isinstance(directory, DirectoryObject):
directory = DirectoryObject(directory)
return file_name, directory


class DirectoryObject:
def __init__(self, directory: str | Path | DirectoryObject):
if isinstance(directory, str):
self.path = Path(directory)
elif isinstance(directory, Path):
self.path = directory
elif isinstance(directory, DirectoryObject):
self.path = directory.path
self.create()
class DirectoryObject(BasePath):
def __new__(cls, *args, **kwargs):
# Create an instance of PosixPath or WindowsPath depending on the platform
instance = super().__new__(cls, *args, **kwargs)
instance.create()
return instance

def create(self):
self.path.mkdir(parents=True, exist_ok=True)
self.mkdir(parents=True, exist_ok=True)

def delete(self, only_if_empty: bool = False):
if self.is_empty() or not only_if_empty:
delete_files_and_directories_recursively(self.path)
delete_files_and_directories_recursively(self)

def list_content(self):
return categorize_folder_items(self.path)
return categorize_folder_items(self)

def __len__(self):
return sum([len(cc) for cc in self.list_content().values()])

def __repr__(self):
return f"DirectoryObject(directory='{self.path}')\n{self.list_content()}"

def get_path(self, file_name):
return self.path / file_name
return f"DirectoryObject(directory='{self}')\n{self.list_content()}"

def file_exists(self, file_name):
return self.get_path(file_name).is_file()
return self.joinpath(file_name).is_file()

def write(self, file_name, content, mode="w"):
with self.get_path(file_name).open(mode=mode) as f:
with self.joinpath(file_name).open(mode=mode) as f:
f.write(content)

def create_subdirectory(self, path):
return DirectoryObject(self.path / path)
return DirectoryObject(self.joinpath(path))

def create_file(self, file_name):
return FileObject(file_name, self)
Expand All @@ -119,7 +83,7 @@ def is_empty(self) -> bool:

def remove_files(self, *files: str):
for file in files:
path = self.get_path(file)
path = self.joinpath(file)
if path.is_file():
path.unlink()

Expand All @@ -128,90 +92,50 @@ class NoDestinationError(ValueError):
"""A custom error for when neither a new file name nor new location are provided"""


class FileObject:
def __init__(self, file_name: str, directory: DirectoryObject = None):
self._file_name, self.directory = _resolve_directory_and_path(
file_name=file_name, directory=directory, default_directory="."
)

@property
def file_name(self):
return self._file_name

@property
def path(self):
return self.directory.path / Path(self._file_name)
class FileObject(BasePath):
def __new__(cls, file_name: str, directory: DirectoryObject = None):
# Resolve the full path of the file
if directory is None:
full_path = Path(file_name)
else:
if isinstance(directory, str):
directory = DirectoryObject(directory)
full_path = directory.joinpath(file_name)
instance = super().__new__(cls, full_path)
return instance

def write(self, content, mode="x"):
self.directory.write(file_name=self.file_name, content=content, mode=mode)
with self.open(mode=mode) as f:
f.write(content)

def read(self, mode="r"):
with open(self.path, mode=mode) as f:
with self.open(mode=mode) as f:
return f.read()

def is_file(self):
return self.directory.file_exists(self.file_name)
return self.exists() and super().is_file()

def delete(self):
self.path.unlink()

def __str__(self):
return str(self.path.absolute())

def _resolve_directory_and_path(
self,
file_name: str,
directory: DirectoryObject | str | None = None,
default_directory: str = ".",
):
"""
Internal routine to separate the file name and the directory in case
file name is given in absolute path etc.
"""
path = Path(file_name)
file_name = path.name
if path.is_absolute():
# If absolute path, take that of new_file_name regardless of the
# name of directory
directory = str(path.parent)
else:
if directory is None:
# If directory is not given, take default directory
directory = default_directory
else:
# If the directory is given, use it as the main path and append
# additional path if given in new_file_name
if isinstance(directory, DirectoryObject):
directory = directory.path
directory = directory / path.parent
if not isinstance(directory, DirectoryObject):
directory = DirectoryObject(directory)
return file_name, directory
self.unlink()

def copy(
self,
new_file_name: str | None = None,
directory: DirectoryObject | str | None = None,
):
"""
Copy an existing file to a new location.
Args:
new_file_name (str): New file name. You can also set
an absolute path (in which case `directory` will be ignored)
directory (DirectoryObject): Directory. If None, the same
directory is used
Returns:
(FileObject): file object of the new file
"""
if new_file_name is None and directory is None:
raise NoDestinationError(
"Either new file name or directory must be specified"
)

if new_file_name is None:
if directory is None:
raise NoDestinationError(
"Either new file name or directory must be specified"
)
new_file_name = self.file_name
file_name, directory = self._resolve_directory_and_path(
new_file_name, directory, default_directory=self.directory.path
)
new_file = FileObject(file_name, directory.path)
shutil.copy(str(self.path), str(new_file.path))
return new_file
new_file_name = self.name

if directory is None:
directory = self.parent
elif isinstance(directory, str):
directory = DirectoryObject(directory)

new_file = directory.joinpath(new_file_name)
shutil.copy(str(self), str(new_file))
return FileObject(new_file_name, DirectoryObject(directory))
59 changes: 26 additions & 33 deletions tests/unit/test_files.py
Original file line number Diff line number Diff line change
@@ -1,60 +1,56 @@
import unittest
from pyiron_snippets.files import DirectoryObject, FileObject
from pathlib import Path
import platform
from platform import system

from pyiron_snippets.files import DirectoryObject, FileObject

class TestFiles(unittest.TestCase):
def setUp(self):
self.directory = DirectoryObject("test")

def tearDown(self):
self.directory.delete()
if self.directory.exists():
self.directory.delete()

def test_directory_instantiation(self):
directory = DirectoryObject(Path("test"))
self.assertEqual(directory.path, self.directory.path)
self.assertEqual(directory, self.directory)
directory = DirectoryObject(self.directory)
self.assertEqual(directory.path, self.directory.path)
self.assertEqual(directory, self.directory)

def test_file_instantiation(self):
self.assertEqual(
FileObject("test.txt", self.directory).path,
FileObject("test.txt", "test").path,
FileObject("test.txt", self.directory),
FileObject("test.txt", "test"),
msg="DirectoryObject and str must give the same object"
)
self.assertEqual(
FileObject("test/test.txt").path,
FileObject("test.txt", "test").path,
msg="File path not same as directory path"
FileObject("test/test.txt"),
FileObject("test.txt", "test"),
msg="File path not the same as directory path"
)

if platform.system() == "Windows":
self.assertRaises(ValueError, FileObject, "C:\\test.txt", "test")
else:
self.assertRaises(ValueError, FileObject, "/test.txt", "test")

def test_directory_exists(self):
self.assertTrue(Path("test").exists() and Path("test").is_dir())
self.assertTrue(self.directory.exists() and self.directory.is_dir())

def test_write(self):
self.directory.write(file_name="test.txt", content="something")
self.assertTrue(self.directory.file_exists("test.txt"))
self.assertTrue(
"test/test.txt" in [
ff.replace("\\", "/")
str(ff).replace("\\", "/")
for ff in self.directory.list_content()['file']
]
)
self.assertEqual(len(self.directory), 1)

def test_create_subdirectory(self):
self.directory.create_subdirectory("another_test")
self.assertTrue(Path("test/another_test").exists())
sub_dir = self.directory.create_subdirectory("another_test")
self.assertTrue(sub_dir.exists() and sub_dir.is_dir())

def test_path(self):
f = FileObject("test.txt", self.directory)
self.assertEqual(str(f.path).replace("\\", "/"), "test/test.txt")
self.assertEqual(str(f), str(self.directory.joinpath("test.txt")))

def test_read_and_write(self):
f = FileObject("test.txt", self.directory)
Expand All @@ -76,7 +72,7 @@ def test_is_empty(self):

def test_delete(self):
self.assertTrue(
Path("test").exists() and Path("test").is_dir(),
self.directory.exists() and self.directory.is_dir(),
msg="Sanity check on initial state"
)
self.directory.write(file_name="test.txt", content="something")
Expand All @@ -87,7 +83,7 @@ def test_delete(self):
)
self.directory.delete()
self.assertFalse(
Path("test").exists(),
self.directory.exists(),
msg="Delete should remove the entire directory"
)
self.directory = DirectoryObject("test") # Rebuild it so the tearDown works
Expand Down Expand Up @@ -122,31 +118,28 @@ def test_remove(self):

def test_copy(self):
f = FileObject("test_copy.txt", self.directory)
f.write("sam wrote this wondrful thing")
f.write("sam wrote this wonderful thing")
new_file_1 = f.copy("another_test")
self.assertEqual(new_file_1.read(), "sam wrote this wondrful thing")
self.assertEqual(new_file_1.read(), "sam wrote this wonderful thing")
new_file_2 = f.copy("another_test", ".")
with open("another_test", "r") as file:
txt = file.read()
self.assertEqual(txt, "sam wrote this wondrful thing")
self.assertEqual(txt, "sam wrote this wonderful thing")
new_file_2.delete() # needed because current directory
new_file_3 = f.copy(str(f.path.parent / "another_test"), ".")
self.assertEqual(new_file_1.path.absolute(), new_file_3.path.absolute())
new_file_3 = f.copy(str(f.parent / "another_test"), ".")
self.assertEqual(new_file_1, new_file_3)
new_file_4 = f.copy(directory=".")
with open("test_copy.txt", "r") as file:
txt = file.read()
self.assertEqual(txt, "sam wrote this wondrful thing")
self.assertEqual(txt, "sam wrote this wonderful thing")
new_file_4.delete() # needed because current directory
with self.assertRaises(ValueError):
f.copy()

def test_str(self):
f = FileObject("test_copy.txt", self.directory)
if platform.system() == "Windows":
txt = f"my file: {self.directory.path.absolute()}\\test_copy.txt"
else:
txt = f"my file: {self.directory.path.absolute()}/test_copy.txt"
self.assertEqual(f"my file: {f}", txt)
expected_path = str(self.directory / "test_copy.txt")
self.assertEqual(str(f), expected_path)


if __name__ == '__main__':
Expand Down
Loading