Watch files included from PCN files (auto-reload)
This commit is contained in:
parent
99e2be650b
commit
199ec7646b
@ -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
|
||||
|
@ -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(
|
||||
|
@ -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:
|
||||
|
Loading…
Reference in New Issue
Block a user