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 # TODO
- Fix MatrixForbidden when switching rooms with Alt+numbers
- Verify PCN include_builtin works under QRC
- PCN docstrings - PCN docstrings
- PCN error handling - PCN error handling
- PCN documentation
- Room display name not updated when someone removes theirs - Room display name not updated when someone removes theirs
- Fix right margin of own `<image url>\n<image url>` messages - Fix right margin of own `<image url>\n<image url>` messages

View File

@ -2,6 +2,7 @@ import re
import textwrap import textwrap
from collections import OrderedDict from collections import OrderedDict
from collections.abc import MutableMapping from collections.abc import MutableMapping
from contextlib import suppress
from dataclasses import dataclass, field from dataclasses import dataclass, field
from operator import attrgetter from operator import attrgetter
from pathlib import Path from pathlib import Path
@ -33,6 +34,7 @@ class Section(MutableMapping):
root: Optional["Section"] = None root: Optional["Section"] = None
parent: Optional["Section"] = None parent: Optional["Section"] = None
builtins_path: Path = BUILTINS_DIR builtins_path: Path = BUILTINS_DIR
included: List[Path] = field(default_factory=list)
globals: GlobalsDict = field(init=False) globals: GlobalsDict = field(init=False)
_edited: Dict[str, Any] = field(init=False, default_factory=dict) _edited: Dict[str, Any] = field(init=False, default_factory=dict)
@ -223,6 +225,8 @@ class Section(MutableMapping):
def deep_merge(self, section2: "Section") -> None: def deep_merge(self, section2: "Section") -> None:
self.included += section2.included
for key in section2: for key in section2:
if key in self.sections and key in section2.sections: if key in self.sections and key in section2.sections:
self.globals.data.update(section2.globals.data) self.globals.data.update(section2.globals.data)
@ -249,14 +253,26 @@ class Section(MutableMapping):
def include_file(self, path: Union[Path, str]) -> None: 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 path = self.source_path.parent / path
with suppress(ValueError):
self.included.remove(path)
self.included.append(path)
self.deep_merge(Section.from_file(path)) self.deep_merge(Section.from_file(path))
def include_builtin(self, relative_path: Union[Path, str]) -> None: 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]: def as_dict(self, _section: Optional["Section"] = None) -> Dict[str, Any]:
@ -339,6 +355,13 @@ class Section(MutableMapping):
return changes 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 @classmethod
def from_source_code( def from_source_code(

View File

@ -35,8 +35,10 @@ class UserFile:
create_missing: ClassVar[bool] = True create_missing: ClassVar[bool] = True
backend: "Backend" = field(repr=False) backend: "Backend" = field(repr=False)
filename: str = field() filename: str = field()
parent: Optional["UserFile"] = None
children: Dict[Path, "UserFile"] = field(default_factory=dict)
data: Any = field(init=False, default_factory=dict) data: Any = field(init=False, default_factory=dict)
_need_write: bool = field(init=False, default=False) _need_write: bool = field(init=False, default=False)
@ -46,15 +48,12 @@ class UserFile:
_writer: Optional[asyncio.Future] = field(init=False, default=None) _writer: Optional[asyncio.Future] = field(init=False, default=None)
def __post_init__(self) -> None: def __post_init__(self) -> None:
try: if self.path.exists():
text_data = self.path.read_text() text = self.path.read_text()
except FileNotFoundError: self.data, self._need_write = self.deserialized(text)
else:
self.data = self.default_data self.data = self.default_data
self._need_write = self.create_missing 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._reader = asyncio.ensure_future(self._start_reader())
self._writer = asyncio.ensure_future(self._start_writer()) self._writer = asyncio.ensure_future(self._start_writer())
@ -99,12 +98,26 @@ class UserFile:
if self._writer: if self._writer:
self._writer.cancel() self._writer.cancel()
for child in self.children.values():
child.stop_watching()
async def set_data(self, data: Any) -> None: async def set_data(self, data: Any) -> None:
"""Set `data` and call `save()`, conveniance method for QML.""" """Set `data` and call `save()`, conveniance method for QML."""
self.data = data self.data = data
self.save() 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: async def _start_reader(self) -> None:
"""Disk reader coroutine, watches for file changes to update `data`.""" """Disk reader coroutine, watches for file changes to update `data`."""
@ -123,13 +136,7 @@ class UserFile:
ignored += 1 ignored += 1
continue continue
async with aiopen(self.path) as file: await self.update_from_file()
text = await file.read()
self.data, save = self.deserialized(text)
if save:
self.save()
self._mtime = mtime self._mtime = mtime
elif change[0] == Change.deleted: elif change[0] == Change.deleted:
@ -140,6 +147,12 @@ class UserFile:
if changes and ignored < len(changes): if changes and ignored < len(changes):
UserFileChanged(type(self), self.qml_data) 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(): while not self.path.exists():
# Prevent error spam after file gets deleted # Prevent error spam after file gets deleted
await asyncio.sleep(0.5) await asyncio.sleep(0.5)
@ -147,7 +160,6 @@ class UserFile:
except Exception as err: # noqa except Exception as err: # noqa
LoopException(str(err), err, traceback.format_exc().rstrip()) LoopException(str(err), err, traceback.format_exc().rstrip())
async def _start_writer(self) -> None: async def _start_writer(self) -> None:
"""Disk writer coroutine, update the file with a 1 second cooldown.""" """Disk writer coroutine, update the file with a 1 second cooldown."""
@ -261,6 +273,12 @@ class PCNFile(MappingFile):
create_missing = False create_missing = False
path_override: Optional[Path] = None
@property
def path(self) -> Path:
return self.path_override or super().path
@property @property
def write_path(self) -> Path: def write_path(self) -> Path:
"""Full path of file where programatically-done edits are stored.""" """Full path of file where programatically-done edits are stored."""
@ -281,6 +299,22 @@ class PCNFile(MappingFile):
if self.write_path.exists(): if self.write_path.exists():
edits = self.write_path.read_text() 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))) return (root, root.deep_merge_edits(json.loads(edits)))
def serialized(self) -> str: def serialized(self) -> str: