Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/python-package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:
fail-fast: false
matrix:
os: ["windows-latest", "ubuntu-latest", "macos-latest"]
python-version: ["3.12", "3.13", "3.14"]
python-version: ["3.13", "3.14"]

runs-on: ${{matrix.os}}

Expand Down
15 changes: 3 additions & 12 deletions entangled/commands/reset.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,9 @@
without actually writing out to source files.
"""

from ..io import TransactionMode, transaction
from ..errors.user import UserError
from ..interface import Document
from ..io import TransactionMode
from .main import main

import logging
from .tangle import do_tangle


@main.command(short_help="Reset the file database.")
Expand All @@ -20,10 +17,4 @@ def reset():
Resets the file database. This performs a tangle without actually
writing output to the files, but updating the database as if we were.
"""
doc = Document()
mode = TransactionMode.RESETDB

with transaction(mode) as t:
doc.load(t)
doc.tangle(t)
t.clear_orphans()
do_tangle(mode=TransactionMode.RESETDB)
7 changes: 3 additions & 4 deletions entangled/commands/sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,10 @@ class Action(Enum):


def sync_action(doc: Document) -> Action:
fs = FileCache()
input_file_list = doc.input_files(fs)
input_file_list = doc.input_files()

with filedb(readonly=True) as db:
changed = set(db.changed_files(fs))
with filedb(readonly=True, fs=doc.context.fs) as db:
changed = set(db.changed_files(doc.context.fs))

if not all(f in db for f in input_file_list):
return Action.TANGLE
Expand Down
27 changes: 21 additions & 6 deletions entangled/commands/tangle.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@

from .main import main

from ..config import AnnotationMethod
from ..io import transaction, TransactionMode
from ..config import AnnotationMethod, Config
from ..io import AbstractFileCache, FileCache, transaction, TransactionMode
from ..errors.user import UserError
from ..interface import Document
from ..interface import Context, Document


@main.command()
Expand All @@ -14,20 +14,35 @@
@click.option("-f", "--force", is_flag=True, help="force overwriting existing files")
@click.option("-s", "--show", is_flag=True, help="only show what would happen")
def tangle(*, annotate: AnnotationMethod | None = None, force: bool = False, show: bool = False):
"""Tangle codes from the documentation."""
if show:
mode = TransactionMode.SHOW
elif force:
mode = TransactionMode.FORCE
else:
mode = TransactionMode.FAIL

doc = Document()
do_tangle(annotate=annotate, mode=mode, skip_post_tangle=False)


def do_tangle(*,
annotate: AnnotationMethod | None = None,
mode: TransactionMode = TransactionMode.FAIL,
fs: AbstractFileCache | None = None,
skip_post_tangle: bool = True):
"""Tangle codes from the documentation."""

with transaction(mode) as t:
if fs is None:
fs = FileCache()

doc = Document(context=Context(fs=fs))

with transaction(mode, fs=fs) as t:
doc.load(t)
doc.tangle(t, annotate)
t.clear_orphans()

if skip_post_tangle:
return

for h in doc.context.all_hooks:
h.post_tangle(doc.reference_map)
6 changes: 3 additions & 3 deletions entangled/config/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,11 +83,11 @@ def get_input_files(fs: AbstractFileCache, cfg: Config) -> list[Path]:
Get a sorted list of all input files for this project.
"""
log.debug("watch list: %s; ignoring: %s", cfg.watch_list, cfg.ignore_list)
input_file_list = filter(
input_file_list = sorted(filter(
lambda p: not any(p.match(pat) for pat in cfg.ignore_list),
chain.from_iterable(map(fs.glob, cfg.watch_list)))
chain.from_iterable(map(fs.glob, cfg.watch_list))))
log.debug("input file list %s", input_file_list)
return sorted(input_file_list)
return input_file_list


__all__ = ["Config", "ConfigUpdate", "AnnotationMethod", "Markers", "NamespaceDefault"]
4 changes: 3 additions & 1 deletion entangled/interface/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from dataclasses import dataclass, field
from collections.abc import Generator, Iterable

from ..io import AbstractFileCache, FileCache
from ..config import Config, ConfigUpdate
from ..hooks import HookBase, hooks, create_hook
from ..model import Content, ReferenceMap
Expand All @@ -16,6 +17,7 @@

@dataclass
class Context:
fs: AbstractFileCache = field(default_factory=FileCache)
config: Config = Config()
_hook_states: dict[str, HookBase.State] = field(default_factory=dict)
_hooks: dict[str, HookBase] = field(default_factory=dict)
Expand All @@ -33,7 +35,7 @@ def __post_init__(self):
self._hooks[h] = hook

def __or__(self, update: ConfigUpdate | None) -> Context:
return Context(self.config | update, self._hook_states)
return Context(self.fs, self.config | update, self._hook_states)

@property
def hooks(self) -> list[HookBase]:
Expand Down
6 changes: 3 additions & 3 deletions entangled/interface/document.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,10 @@ def config(self, new_config: Config) -> None:
self.context.config = new_config

def __post_init__(self):
self.config |= read_config(FileCache())
self.config |= read_config(self.context.fs)

def input_files(self, fs: AbstractFileCache):
return get_input_files(fs, self.config)
def input_files(self):
return get_input_files(self.context.fs, self.config)

def source_text(self, path: Path) -> tuple[str, set[PurePath]]:
deps = set()
Expand Down
35 changes: 24 additions & 11 deletions entangled/io/filedb.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from __future__ import annotations
from collections.abc import Generator
from contextlib import contextmanager
from contextlib import contextmanager, nullcontext
import json
from pathlib import Path
from typing import Any
Expand All @@ -16,7 +16,7 @@

from ..version import __version__
from ..utility import ensure_parent
from .virtual import AbstractFileCache
from .virtual import AbstractFileCache, FileCache
from .stat import Stat, hexdigest


Expand Down Expand Up @@ -92,12 +92,16 @@ def new_db() -> FileDB:
return FileDB(__version__, {}, set())


def read_filedb() -> FileDB:
if not FILEDB_PATH.exists():
def read_filedb(fs: AbstractFileCache | None = None) -> FileDB:
if fs is None:
fs = FileCache()

if FILEDB_PATH not in fs:
return new_db()

logging.debug("Reading FileDB")
raw: Any = json.load(open(FILEDB_PATH, "rb")) # pyright: ignore[reportExplicitAny, reportAny]
db_contents = fs[FILEDB_PATH].content
raw: Any = json.loads(db_contents) # pyright: ignore[reportExplicitAny, reportAny]
if raw["version"] != __version__:
raise HelpfulUserError(
f"File database was created with a different version of Entangled ({raw["version"]}).\n" +
Expand All @@ -112,20 +116,29 @@ def read_filedb() -> FileDB:
return db


def write_filedb(db: FileDB):
def write_filedb(db: FileDB, fs: AbstractFileCache | None = None):
if fs is None:
fs = FileCache()

logging.debug("Writing FileDB")
_ = FILEDB_PATH.open("wb").write(msgspec.json.encode(db, order="sorted"))
content = msgspec.json.encode(db, order="sorted").decode(encoding="utf-8")
_ = fs.write(FILEDB_PATH, content)


@contextmanager
def filedb(readonly: bool = False, writeonly: bool = False, virtual: bool = False):
def filedb(readonly: bool = False, writeonly: bool = False, virtual: bool = False, fs: AbstractFileCache | None = None):
if fs is None:
fs = FileCache()

if virtual:
yield new_db()
return

lock = FileLock(ensure_parent(FILEDB_LOCK_PATH))
lock = FileLock(ensure_parent(FILEDB_LOCK_PATH)) if fs.is_for_real() \
else nullcontext()

with lock:
db = read_filedb() if not writeonly else new_db()
db = read_filedb(fs) if not writeonly else new_db()
yield db
if not readonly:
write_filedb(db)
write_filedb(db, fs)
2 changes: 1 addition & 1 deletion entangled/io/transaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,7 @@ def transaction(mode: TransactionMode = TransactionMode.FAIL, fs: AbstractFileCa

with filedb(
writeonly = (mode == TransactionMode.RESETDB),
virtual = isinstance(fs, VirtualFS)) as db:
fs = fs) as db:
if mode == TransactionMode.RESETDB:
db.clear()

Expand Down
15 changes: 14 additions & 1 deletion entangled/io/virtual.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,11 @@ def atomic_write(target: Path, content: str, mode: int | None):


class AbstractFileCache(ABC):
@classmethod
@abstractmethod
def is_for_real(cls) -> bool:
...

@abstractmethod
def __getitem__(self, key: Path) -> FileData:
...
Expand Down Expand Up @@ -73,6 +78,10 @@ def reset(self):
class VirtualFS(AbstractFileCache):
_data: dict[Path, FileData] = field(default_factory=dict)

@classmethod
def is_for_real(cls) -> bool:
return False

def __getitem__(self, key: Path) -> FileData:
return self._data[key]

Expand All @@ -83,7 +92,7 @@ def __delitem__(self, key: Path):
del self._data[key]

def glob(self, pattern: str) -> Iterable[Path]:
return filter(lambda p: p.match(pattern), self._data.keys())
return filter(lambda p: p.full_match(pattern), self._data.keys())

@override
def write(self, key: Path, content: str, mode: int | None = None):
Expand All @@ -108,6 +117,10 @@ class FileCache(AbstractFileCache):
"""
_data: dict[Path, FileData] = field(default_factory=dict)

@classmethod
def is_for_real(cls) -> bool:
return True

@override
def __getitem__(self, key: Path) -> FileData:
"""
Expand Down
15 changes: 9 additions & 6 deletions entangled/status.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from collections.abc import Iterable
from .io import filedb, FileCache
from .io import AbstractFileCache, filedb, FileCache
from .config import get_input_files, Config, read_config

from pathlib import Path
Expand All @@ -16,18 +16,21 @@ def safe_glob(pattern: str) -> Iterable[Path]:
return []


def find_watch_dirs():
def find_watch_dirs(fs: AbstractFileCache | None = None):
"""List all directories that contain files that need watching."""
fs = FileCache()
if fs is None:
fs = FileCache()
cfg = Config() | read_config(fs)
input_file_list = get_input_files(fs, cfg)
markdown_dirs = set(p.parent for p in input_file_list)
with filedb(readonly=True) as db:
with filedb(readonly=True, fs=fs) as db:
code_dirs = set(p.parent for p in db.managed_files)
return code_dirs.union(markdown_dirs)


def list_dependent_files():
with filedb(readonly=True) as db:
def list_dependent_files(fs: AbstractFileCache | None = None):
if fs is None:
fs = FileCache()
with filedb(readonly=True, fs=fs) as db:
result = list(db.managed_files)
return result
6 changes: 3 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
[project]
name = "entangled-cli"
version = "2.4.1"
version = "2.4.2"
description = "Literate Programming toolbox"
authors = [{ name = "Johan Hidding", email = "j.hidding@esciencecenter.nl" }]
requires-python = ">=3.12,<4"
requires-python = ">=3.13,<4"
readme = "README.md"
license = "Apache-2.0"
dependencies = [
"filelock>=3.12.0,<4",
"rich>=13.3.5,<14",
"rich>=13,<15",
"tomlkit>=0.12.1,<0.13",
"copier>=9,<10",
"brei>=0.2.3,<0.3",
Expand Down
5 changes: 4 additions & 1 deletion test/commands/test_daemon.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@

from contextlib import chdir

if sys.platform.startswith("win"):
pytest.skip("skipping test on windows until someone tells me how to fix this.", allow_module_level=True)


def wait_for_file(filename, timeout=5):
start_time = time.time()
Expand All @@ -35,7 +38,7 @@ def wait_for_stat_diff(md_stat, filename, timeout=5):
return False


@pytest.mark.timeout(5)
@pytest.mark.timeout(10)
def test_daemon(tmp_path: Path):
with chdir(tmp_path):
configure(debug=True)
Expand Down
26 changes: 26 additions & 0 deletions test/commands/test_reset.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from pathlib import Path
from entangled.commands.tangle import do_tangle
from entangled.io import TransactionMode
from entangled.io.virtual import VirtualFS
import pprint
import json

fs = VirtualFS.from_dict({
"test.md": """
``` {.python file=test.py}
print("Hello, World!")
```
"""
})


def test_issue_83():
do_tangle(fs=fs)
assert Path(".entangled/filedb.json") in fs
assert Path("test.py") in fs
assert "print(\"Hello, World!\")" in fs[Path("test.py")].content
del fs[Path("test.py")]
do_tangle(fs=fs, mode=TransactionMode.RESETDB)
assert Path("test.py") not in fs
do_tangle(fs=fs)
assert Path("test.py") in fs
4 changes: 2 additions & 2 deletions test/io/test_filedb.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,13 @@ def test_stat(example_files: Path):
def test_filedb(example_files: Path):
with chdir(example_files):
fs = FileCache()
with filedb() as db:
with filedb(fs=fs) as db:
for n in "abcd":
db.update(fs, Path(n))

fs.write(Path("d"), "mars")

with filedb() as db:
with filedb(fs=fs) as db:
assert list(db.changed_files(fs)) == [Path("d")]
db.update(fs, Path("d"))
assert list(db.changed_files(fs)) == []
Loading