Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added a node to keep track of files #647

Merged
merged 2 commits into from
Nov 8, 2023
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
1 change: 1 addition & 0 deletions src/sisl/nodes/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from .context import SISL_NODES_CONTEXT, NodeContext, temporal_context
from .file_nodes import FileNode
from .node import Node
from .utils import nodify_module
from .workflow import Workflow
176 changes: 176 additions & 0 deletions src/sisl/nodes/file_nodes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
from pathlib import Path

try:
import watchdog.events
import watchdog.observers

Check warning on line 5 in src/sisl/nodes/file_nodes.py

View check run for this annotation

Codecov / codecov/patch

src/sisl/nodes/file_nodes.py#L5

Added line #L5 was not covered by tests

WATCHDOG_IMPORTED = True

Check warning on line 7 in src/sisl/nodes/file_nodes.py

View check run for this annotation

Codecov / codecov/patch

src/sisl/nodes/file_nodes.py#L7

Added line #L7 was not covered by tests
except ImportError:
WATCHDOG_IMPORTED = False

from sisl.messages import warn

from .node import Node

if WATCHDOG_IMPORTED:

class CallbackHandler(watchdog.events.PatternMatchingEventHandler):
def __init__(self, callback_obj, *args, **kwargs):
super().__init__(*args, **kwargs)
self.callback_obj = callback_obj

Check warning on line 20 in src/sisl/nodes/file_nodes.py

View check run for this annotation

Codecov / codecov/patch

src/sisl/nodes/file_nodes.py#L17-L20

Added lines #L17 - L20 were not covered by tests

def _run_method(self, method_name, event):

Check notice

Code scanning / CodeQL

Explicit returns mixed with implicit (fall through) returns Note

Mixing implicit and explicit returns may indicate an error as implicit returns always return None.
if not hasattr(self.callback_obj, "matches_path"):
return
if not self.callback_obj.matches_path(event.src_path):
return

Check warning on line 26 in src/sisl/nodes/file_nodes.py

View check run for this annotation

Codecov / codecov/patch

src/sisl/nodes/file_nodes.py#L22-L26

Added lines #L22 - L26 were not covered by tests

func = getattr(self.callback_obj, method_name, None)
if callable(func):
return func(event)

Check warning on line 30 in src/sisl/nodes/file_nodes.py

View check run for this annotation

Codecov / codecov/patch

src/sisl/nodes/file_nodes.py#L28-L30

Added lines #L28 - L30 were not covered by tests

def on_modified(self, event):
self._run_method("on_file_modified", event)

Check warning on line 33 in src/sisl/nodes/file_nodes.py

View check run for this annotation

Codecov / codecov/patch

src/sisl/nodes/file_nodes.py#L32-L33

Added lines #L32 - L33 were not covered by tests

def on_created(self, event):
self._run_method("on_file_created", event)

Check warning on line 36 in src/sisl/nodes/file_nodes.py

View check run for this annotation

Codecov / codecov/patch

src/sisl/nodes/file_nodes.py#L35-L36

Added lines #L35 - L36 were not covered by tests

def on_moved(self, event):
self._run_method("on_file_moved", event)

Check warning on line 39 in src/sisl/nodes/file_nodes.py

View check run for this annotation

Codecov / codecov/patch

src/sisl/nodes/file_nodes.py#L38-L39

Added lines #L38 - L39 were not covered by tests

def on_deleted(self, event):
self._run_method("on_file_deleted", event)

Check warning on line 42 in src/sisl/nodes/file_nodes.py

View check run for this annotation

Codecov / codecov/patch

src/sisl/nodes/file_nodes.py#L41-L42

Added lines #L41 - L42 were not covered by tests


class FileNode(Node):
"""Node that listens to changes to a given file.

If the file changes, the node will be marked as outdated and the signal
will be propagated to all downstream nodes.

Therefore, if autoupdate is enabled at some point down the tree, an
update will be triggered.

This node requires `watchdog` to be installed in order to be fully
functional. Otherwise, it will not fail but it won't do anything.

Parameters
----------
path :
Path to the file to watch.

Examples
--------

.. code-block:: python
from sisl.nodes import Node, FileNode
import time

# Write something to file
with open("testfile", "w") as f:
f.write("HELLO")

# Create a FileNode
n = FileNode("testfile")

# Define a file reader node that prints the contents of the file
@Node.from_func
def print_contents(path):
print("printing contents...")
with open(path, "r") as f:
print(f.read())

# And initialize it by passing the FileNode as an input
printer = print_contents(path=n)

print("---RUNNING NODE")

# Run the printer node
printer.get()

print("--- SET AUTOMATIC UPDATING")

# Now set it to automatically update on changes to upstream inputs
printer.context.update(lazy=False)

print("--- APPENDING TO FILE")

# Append to the file which will trigger the update.
with open("testfile", "a") as f:
f.write("\nBYE")

# Give some time for the printer to react before exiting
time.sleep(1)

This should give the following output:

.. code-block::

---RUNNING NODE
printing contents...
HELLO
--- SET AUTOMATIC UPDATING
--- APPENDING TO FILE
printing contents...
HELLO
BYE

"""

def setup(self, *args, **kwargs):
super().setup(*args, **kwargs)

self._setup_observer()

def _setup_observer(self):
"""Sets up the watchdog observer."""
if not WATCHDOG_IMPORTED:
warn(
"Watchdog is not installed. {self.__class__.__name__} will not be able to detect changes in files."
)
else:
# Watchdog watches directories so we should watch the parent
parent_path = Path(self.inputs["path"]).parent

Check warning on line 133 in src/sisl/nodes/file_nodes.py

View check run for this annotation

Codecov / codecov/patch

src/sisl/nodes/file_nodes.py#L133

Added line #L133 was not covered by tests

self.observer = watchdog.observers.Observer()
handler = CallbackHandler(self)
self.observer.schedule(handler, parent_path)
self.observer.start()

Check warning on line 138 in src/sisl/nodes/file_nodes.py

View check run for this annotation

Codecov / codecov/patch

src/sisl/nodes/file_nodes.py#L135-L138

Added lines #L135 - L138 were not covered by tests

def _update_observer(self):
"""Updates the observer to watch the (possibly) new path."""
if not WATCHDOG_IMPORTED:
return

Check warning on line 143 in src/sisl/nodes/file_nodes.py

View check run for this annotation

Codecov / codecov/patch

src/sisl/nodes/file_nodes.py#L142-L143

Added lines #L142 - L143 were not covered by tests

self.observer.unschedule_all()
parent_path = Path(self.inputs["path"]).parent
self.observer.schedule(CallbackHandler(self), parent_path)

Check warning on line 147 in src/sisl/nodes/file_nodes.py

View check run for this annotation

Codecov / codecov/patch

src/sisl/nodes/file_nodes.py#L145-L147

Added lines #L145 - L147 were not covered by tests

# Methods to interact with the observer
def matches_path(self, path: str):
return Path(path) == Path(self.inputs["path"])

Check warning on line 151 in src/sisl/nodes/file_nodes.py

View check run for this annotation

Codecov / codecov/patch

src/sisl/nodes/file_nodes.py#L151

Added line #L151 was not covered by tests

def on_file_modified(self, event):
self.on_file_change(event)

Check warning on line 154 in src/sisl/nodes/file_nodes.py

View check run for this annotation

Codecov / codecov/patch

src/sisl/nodes/file_nodes.py#L154

Added line #L154 was not covered by tests

def on_file_created(self, event):
self.on_file_change(event)

Check warning on line 157 in src/sisl/nodes/file_nodes.py

View check run for this annotation

Codecov / codecov/patch

src/sisl/nodes/file_nodes.py#L157

Added line #L157 was not covered by tests

def on_file_moved(self, event):
self.on_file_change(event)

Check warning on line 160 in src/sisl/nodes/file_nodes.py

View check run for this annotation

Codecov / codecov/patch

src/sisl/nodes/file_nodes.py#L160

Added line #L160 was not covered by tests

def on_file_deleted(self, event):
self.on_file_change(event)

Check warning on line 163 in src/sisl/nodes/file_nodes.py

View check run for this annotation

Codecov / codecov/patch

src/sisl/nodes/file_nodes.py#L163

Added line #L163 was not covered by tests

def on_file_change(self, event):
self._receive_outdated()

Check warning on line 166 in src/sisl/nodes/file_nodes.py

View check run for this annotation

Codecov / codecov/patch

src/sisl/nodes/file_nodes.py#L166

Added line #L166 was not covered by tests

def update_inputs(self, **inputs):
# We wrap the update_inputs method to update the observer if the path
# changes.
super().update_inputs(**inputs)
if "path" in inputs:
self._update_observer()

Check warning on line 173 in src/sisl/nodes/file_nodes.py

View check run for this annotation

Codecov / codecov/patch

src/sisl/nodes/file_nodes.py#L171-L173

Added lines #L171 - L173 were not covered by tests

def function(self, path: str) -> Path:
return Path(path)
30 changes: 30 additions & 0 deletions src/sisl/nodes/tests/test_file_nodes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import tempfile
import time
from pathlib import Path

import pytest

from sisl.nodes import FileNode


def test_file_node_return():
n = FileNode("test.txt")

assert n.get() == Path("test.txt")


def test_file_node_update():
pytest.importorskip("watchdog")

with tempfile.NamedTemporaryFile("w", delete=False) as f:
n = FileNode(f.name)

Check warning on line 20 in src/sisl/nodes/tests/test_file_nodes.py

View check run for this annotation

Codecov / codecov/patch

src/sisl/nodes/tests/test_file_nodes.py#L19-L20

Added lines #L19 - L20 were not covered by tests

assert n.get() == Path(f.name)

Check warning on line 22 in src/sisl/nodes/tests/test_file_nodes.py

View check run for this annotation

Codecov / codecov/patch

src/sisl/nodes/tests/test_file_nodes.py#L22

Added line #L22 was not covered by tests

assert not n._outdated

Check warning on line 24 in src/sisl/nodes/tests/test_file_nodes.py

View check run for this annotation

Codecov / codecov/patch

src/sisl/nodes/tests/test_file_nodes.py#L24

Added line #L24 was not covered by tests

f.write("test")

Check warning on line 26 in src/sisl/nodes/tests/test_file_nodes.py

View check run for this annotation

Codecov / codecov/patch

src/sisl/nodes/tests/test_file_nodes.py#L26

Added line #L26 was not covered by tests

time.sleep(0.2)

Check warning on line 28 in src/sisl/nodes/tests/test_file_nodes.py

View check run for this annotation

Codecov / codecov/patch

src/sisl/nodes/tests/test_file_nodes.py#L28

Added line #L28 was not covered by tests

assert n._outdated

Check warning on line 30 in src/sisl/nodes/tests/test_file_nodes.py

View check run for this annotation

Codecov / codecov/patch

src/sisl/nodes/tests/test_file_nodes.py#L30

Added line #L30 was not covered by tests
Loading