Watch files included from PCN files (auto-reload)

This commit is contained in:
miruka 2021-01-04 03:31:26 -04:00
parent 99e2be650b
commit 199ec7646b
3 changed files with 76 additions and 22 deletions

View File

@ -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 `<image url>\n<image url>` messages

View File

@ -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(

View File

@ -37,6 +37,8 @@ class UserFile:
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: