From 5d72550d2c9d01078dbddf240b25b3cf1c3f321d Mon Sep 17 00:00:00 2001 From: Tomas Babej Date: Tue, 13 Jun 2023 18:24:46 -0400 Subject: [PATCH 1/5] utils: Add helper method to remove files --- datafiles/utils.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/datafiles/utils.py b/datafiles/utils.py index d7b310f5..5f7a7a9a 100644 --- a/datafiles/utils.py +++ b/datafiles/utils.py @@ -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}" From 9d9fe8eed141a2886ee9552c511c0bd890fe6ea5 Mon Sep 17 00:00:00 2001 From: Tomas Babej Date: Tue, 13 Jun 2023 18:57:59 -0400 Subject: [PATCH 2/5] Mapper: Support renaming the file during save() if required --- datafiles/mapper.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/datafiles/mapper.py b/datafiles/mapper.py index af90d80b..d9810163 100644 --- a/datafiles/mapper.py +++ b/datafiles/mapper.py @@ -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: @@ -264,6 +264,17 @@ 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 attributes that are involved in the path were changed + file_rename_required = False + original_path = self.path + 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( @@ -279,6 +290,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 file_rename_required: + remove(original_path) self.modified = False From 381d1de5f40c7799dfcdd7e3e86ac358e22395bb Mon Sep 17 00:00:00 2001 From: Tomas Babej Date: Tue, 13 Jun 2023 21:18:26 -0400 Subject: [PATCH 3/5] Permeate the rename flag throughout the framework --- datafiles/config.py | 3 +++ datafiles/decorators.py | 2 ++ datafiles/mapper.py | 3 +++ datafiles/model.py | 11 ++++++++++- 4 files changed, 18 insertions(+), 1 deletion(-) diff --git a/datafiles/config.py b/datafiles/config.py index 5fb52e27..1279fdc0 100644 --- a/datafiles/config.py +++ b/datafiles/config.py @@ -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: @@ -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 diff --git a/datafiles/decorators.py b/datafiles/decorators.py index 3bc12874..c4458d6f 100644 --- a/datafiles/decorators.py +++ b/datafiles/decorators.py @@ -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.""" @@ -36,6 +37,7 @@ def decorator(cls=None): manual=manual, defaults=defaults, infer=infer, + rename=rename, ) return decorator diff --git a/datafiles/mapper.py b/datafiles/mapper.py index d9810163..226ecb50 100644 --- a/datafiles/mapper.py +++ b/datafiles/mapper.py @@ -28,6 +28,7 @@ def __init__( manual: bool, defaults: bool, infer: bool, + rename: bool, root: Optional[Mapper] = None, ) -> None: assert manual is not None @@ -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 @@ -319,6 +321,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, diff --git a/datafiles/model.py b/datafiles/model.py index 580de27d..54816da5 100644 --- a/datafiles/model.py +++ b/datafiles/model.py @@ -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") @@ -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 From 07bc2b047cca552df879cde6b2f526e34f051cba Mon Sep 17 00:00:00 2001 From: Tomas Babej Date: Tue, 13 Jun 2023 21:18:49 -0400 Subject: [PATCH 4/5] mapper: Gate the file renaming behaviour behind rename=True --- datafiles/mapper.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/datafiles/mapper.py b/datafiles/mapper.py index 226ecb50..f3c439ae 100644 --- a/datafiles/mapper.py +++ b/datafiles/mapper.py @@ -266,16 +266,20 @@ 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 attributes that are involved in the path were changed + # 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 - 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._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: @@ -292,7 +296,7 @@ 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 file_rename_required: + if self._rename and file_rename_required: remove(original_path) self.modified = False From c47ca620fbcdc00897387763e794572e4af00501 Mon Sep 17 00:00:00 2001 From: Tomas Babej Date: Tue, 13 Jun 2023 21:19:11 -0400 Subject: [PATCH 5/5] tests: Adjust mapper fixture to set rename flag --- datafiles/tests/test_mapper.py | 1 + 1 file changed, 1 insertion(+) diff --git a/datafiles/tests/test_mapper.py b/datafiles/tests/test_mapper.py index be754aa5..50a9fcf0 100644 --- a/datafiles/tests/test_mapper.py +++ b/datafiles/tests/test_mapper.py @@ -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():