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

Support renaming files if the filename attributes were modified #304

Draft
wants to merge 5 commits into
base: main
Choose a base branch
from
Draft
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
3 changes: 3 additions & 0 deletions datafiles/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ class Meta:
datafile_manual: bool = False
datafile_defaults: bool = False
datafile_infer: bool = False
datafile_rename: bool = False


def load(obj) -> Meta:
Expand All @@ -30,5 +31,7 @@ def load(obj) -> Meta:
meta.datafile_defaults = obj.Meta.datafile_defaults
with suppress(AttributeError):
meta.datafile_infer = obj.Meta.datafile_infer
with suppress(AttributeError):
meta.datafile_rename = obj.Meta.datafile_rename

return meta
2 changes: 2 additions & 0 deletions datafiles/decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ def datafile(
manual: bool = Meta.datafile_manual,
defaults: bool = Meta.datafile_defaults,
infer: bool = Meta.datafile_infer,
rename: bool = Meta.datafile_rename,
**kwargs,
):
"""Synchronize a data class to the specified path."""
Expand All @@ -36,6 +37,7 @@ def decorator(cls=None):
manual=manual,
defaults=defaults,
infer=infer,
rename=rename,
)

return decorator
Expand Down
22 changes: 21 additions & 1 deletion datafiles/mapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from . import config, formats, hooks
from .converters import Converter, List, map_type, resolve
from .types import Missing, Trilean
from .utils import display, get_default_field_value, recursive_update, write
from .utils import display, get_default_field_value, recursive_update, remove, write


class Mapper:
Expand All @@ -28,6 +28,7 @@ def __init__(
manual: bool,
defaults: bool,
infer: bool,
rename: bool,
root: Optional[Mapper] = None,
) -> None:
assert manual is not None
Expand All @@ -40,6 +41,7 @@ def __init__(
self.attrs = attrs
self._pattern = pattern
self._manual = manual
self._rename = rename
self.defaults = defaults
self._infer = infer
self._last_load = 0.0
Expand Down Expand Up @@ -264,6 +266,21 @@ def save(self, *, include_default_values: Trilean = None, _log=True) -> None:
self._root.save(include_default_values=include_default_values, _log=_log)
return

# Determine whether the expected filepath of the file has changed (which
# happens as a result of modifying attributes that compose the filename).
# Note that this behaviour is gated behind rename=True flag.
file_rename_required = False
original_path = self.path

if self._rename:
with hooks.disabled(): # hooks have to be disabled to prevent infinite loop
if "path" in self.__dict__:
del self.__dict__["path"] # invalidate the cached property

# This call of self.path updates the value since the cache is invalidated
if self.path != original_path:
file_rename_required = True

if self.path:
if self.exists and self._frozen:
raise dataclasses.FrozenInstanceError(
Expand All @@ -279,6 +296,8 @@ def save(self, *, include_default_values: Trilean = None, _log=True) -> None:
text = self._get_text(include_default_values=include_default_values)

write(self.path, text, display=True)
if self._rename and file_rename_required:
remove(original_path)

self.modified = False

Expand Down Expand Up @@ -306,6 +325,7 @@ def create_mapper(obj, root=None) -> Mapper:
attrs=attrs or {},
pattern=pattern,
manual=meta.datafile_manual,
rename=meta.datafile_rename,
defaults=meta.datafile_defaults,
infer=meta.datafile_infer,
root=root,
Expand Down
11 changes: 10 additions & 1 deletion datafiles/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,14 @@ def objects(cls) -> Manager: # pylint: disable=no-self-argument


def create_model(
cls, *, attrs=None, manual=None, pattern=None, defaults=None, infer=None
cls,
*,
attrs=None,
manual=None,
pattern=None,
defaults=None,
infer=None,
rename=None,
):
"""Patch model attributes on to an existing dataclass."""
log.debug(f"Converting {cls} to a datafile model")
Expand All @@ -68,6 +75,8 @@ def create_model(
m.datafile_defaults = defaults
if not hasattr(cls, "Meta") and infer is not None:
m.datafile_infer = infer
if not hasattr(cls, "Meta") and rename is not None:
m.datafile_rename = rename

cls.Meta = m

Expand Down
1 change: 1 addition & 0 deletions datafiles/tests/test_mapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ def mapper():
manual=Meta.datafile_manual,
defaults=Meta.datafile_defaults,
infer=Meta.datafile_infer,
rename=Meta.datafile_rename,
)

def describe_path():
Expand Down
8 changes: 8 additions & 0 deletions datafiles/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,14 @@ def read(filename: str, *, display=False) -> str:
return text


def remove(filename_or_path: Union[str, Path]) -> None:
"""Remove a given file, if it exists."""
filepath = Path(filename_or_path)
if filepath.exists():
filepath.unlink()
log.debug("Removed filepath: %s", str(filepath))


def display(path: Path, data: Dict) -> None:
"""Display data read from a file."""
message = f"Data from file: {path}"
Expand Down