diff --git a/docs/TODO.md b/docs/TODO.md index 36c6a35e..3ba8285e 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -1,10 +1,7 @@ # TODO -- Fix MatrixForbidden when switching rooms with Alt+numbers -- Verify PCN include_builtin works under QRC - PCN docstrings - PCN error handling -- PCN documentation - Room display name not updated when someone removes theirs - Fix right margin of own `\n` messages diff --git a/src/backend/pcn/section.py b/src/backend/pcn/section.py index 94da29e6..7ed46015 100644 --- a/src/backend/pcn/section.py +++ b/src/backend/pcn/section.py @@ -2,6 +2,7 @@ import re import textwrap from collections import OrderedDict from collections.abc import MutableMapping +from contextlib import suppress from dataclasses import dataclass, field from operator import attrgetter from pathlib import Path @@ -33,6 +34,7 @@ class Section(MutableMapping): root: Optional["Section"] = None parent: Optional["Section"] = None builtins_path: Path = BUILTINS_DIR + included: List[Path] = field(default_factory=list) globals: GlobalsDict = field(init=False) _edited: Dict[str, Any] = field(init=False, default_factory=dict) @@ -223,6 +225,8 @@ class Section(MutableMapping): def deep_merge(self, section2: "Section") -> None: + self.included += section2.included + for key in section2: if key in self.sections and key in section2.sections: self.globals.data.update(section2.globals.data) @@ -249,14 +253,26 @@ class Section(MutableMapping): def include_file(self, path: Union[Path, str]) -> None: - if not Path(path).is_absolute() and self.source_path: + path = Path(path) + + if not path.is_absolute() and self.source_path: path = self.source_path.parent / path + with suppress(ValueError): + self.included.remove(path) + + self.included.append(path) self.deep_merge(Section.from_file(path)) def include_builtin(self, relative_path: Union[Path, str]) -> None: - self.deep_merge(Section.from_file(self.builtins_path / relative_path)) + path = self.builtins_path / relative_path + + with suppress(ValueError): + self.included.remove(path) + + self.included.append(path) + self.deep_merge(Section.from_file(path)) def as_dict(self, _section: Optional["Section"] = None) -> Dict[str, Any]: @@ -339,6 +355,13 @@ class Section(MutableMapping): return changes + @property + def all_includes(self) -> Generator[Path, None, None]: + + yield from self.included + + for sub in self.sections: + yield from self[sub].all_includes @classmethod def from_source_code( diff --git a/src/backend/user_files.py b/src/backend/user_files.py index a199ea2b..fd6cb4fb 100644 --- a/src/backend/user_files.py +++ b/src/backend/user_files.py @@ -35,8 +35,10 @@ class UserFile: create_missing: ClassVar[bool] = True - backend: "Backend" = field(repr=False) - filename: str = field() + backend: "Backend" = field(repr=False) + filename: str = field() + parent: Optional["UserFile"] = None + children: Dict[Path, "UserFile"] = field(default_factory=dict) data: Any = field(init=False, default_factory=dict) _need_write: bool = field(init=False, default=False) @@ -46,15 +48,12 @@ class UserFile: _writer: Optional[asyncio.Future] = field(init=False, default=None) def __post_init__(self) -> None: - try: - text_data = self.path.read_text() - except FileNotFoundError: + if self.path.exists(): + text = self.path.read_text() + self.data, self._need_write = self.deserialized(text) + else: self.data = self.default_data self._need_write = self.create_missing - else: - self.data, save = self.deserialized(text_data) - if save: - self.save() self._reader = asyncio.ensure_future(self._start_reader()) self._writer = asyncio.ensure_future(self._start_writer()) @@ -99,12 +98,26 @@ class UserFile: if self._writer: self._writer.cancel() + for child in self.children.values(): + child.stop_watching() + async def set_data(self, data: Any) -> None: """Set `data` and call `save()`, conveniance method for QML.""" self.data = data self.save() + async def update_from_file(self) -> None: + """Read file at `path`, update `data` and call `save()` if needed.""" + + if not self.path.exists(): + self.data = self.default_data + self._need_write = self.create_missing + return + + async with aiopen(self.path) as file: + self.data, self._need_write = self.deserialized(await file.read()) + async def _start_reader(self) -> None: """Disk reader coroutine, watches for file changes to update `data`.""" @@ -123,13 +136,7 @@ class UserFile: ignored += 1 continue - async with aiopen(self.path) as file: - text = await file.read() - self.data, save = self.deserialized(text) - - if save: - self.save() - + await self.update_from_file() self._mtime = mtime elif change[0] == Change.deleted: @@ -140,6 +147,12 @@ class UserFile: if changes and ignored < len(changes): UserFileChanged(type(self), self.qml_data) + parent = self.parent + while parent: + await parent.update_from_file() + UserFileChanged(type(parent), parent.qml_data) + parent = parent.parent + while not self.path.exists(): # Prevent error spam after file gets deleted await asyncio.sleep(0.5) @@ -147,7 +160,6 @@ class UserFile: except Exception as err: # noqa LoopException(str(err), err, traceback.format_exc().rstrip()) - async def _start_writer(self) -> None: """Disk writer coroutine, update the file with a 1 second cooldown.""" @@ -261,6 +273,12 @@ class PCNFile(MappingFile): create_missing = False + path_override: Optional[Path] = None + + @property + def path(self) -> Path: + return self.path_override or super().path + @property def write_path(self) -> Path: """Full path of file where programatically-done edits are stored.""" @@ -281,6 +299,22 @@ class PCNFile(MappingFile): if self.write_path.exists(): edits = self.write_path.read_text() + includes_now = list(root.all_includes) + + for path, pcn in self.children.copy().items(): + if path not in includes_now: + pcn.stop_watching() + del self.children[path] + + for path in includes_now: + if path not in self.children: + self.children[path] = PCNFile( + self.backend, + filename = path.name, + parent = self, + path_override = path, + ) + return (root, root.deep_merge_edits(json.loads(edits))) def serialized(self) -> str: