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