Repository: Textualize/textual
Pull request: method for reloading single node of DirectoryTree
**Please review the following checklist.**

- [x] Docstrings on all new or modified functions / classes 
- [ ] Updated documentation
- [x] Updated CHANGELOG.md (where appropriate)

New method `DirectoryTree.reload_node` allows reloading the content of a single directory.

fixes #2757


Problem statement:
Add a method to `DirectoryTree` that allows for reloading an individual node
The question cropped up on Discord and seems like a useful thing to have: the ability to say "clear out and reload this specific node" when using `DirectoryTree` -- a developer may know that a specific directory has been modified and they want to refresh the content.

Hints from issue discussion:
(This issue is reserved for the [EuroPython 2023 sprint](https://ep2023.europython.eu/sprints). If you are not participating in the sprint, please refrain from working on this issue. Thanks!)

The [`DirectoryTree` widget](https://textual.textualize.io/widgets/directory_tree/) loads its directory upon instantiation and only once, so it displays static data.

The widget should grow a method called something like`reload_node` and that method should do what @davep describes above.
It should clear whatever is under that node and it should load it again.

Good luck and have fun!
I'd like to work on this one. Thanks!

Commit message: method for reloading single node of DirectoryTree

Patch to review:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ec1c24b51b..c31d93afde 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
 - Fixed a crash when a `SelectionList` had a prompt wider than itself https://github.com/Textualize/textual/issues/2900
 - Fixed a bug where `Click` events were bubbling up from `Switch` widgets https://github.com/Textualize/textual/issues/2366
 - Fixed a crash when using empty CSS variables https://github.com/Textualize/textual/issues/1849
+- Added `DirectoryTree.reload_node` method https://github.com/Textualize/textual/issues/2757
 
 ## [0.30.0] - 2023-07-17
 
diff --git a/src/textual/widgets/_directory_tree.py b/src/textual/widgets/_directory_tree.py
index 8ea138a57b..4ac3ba9e06 100644
--- a/src/textual/widgets/_directory_tree.py
+++ b/src/textual/widgets/_directory_tree.py
@@ -146,6 +146,56 @@ def reload(self) -> None:
         # loading up.
         self._add_to_load_queue(self.root)
 
+    def clear_subtree(self, node: TreeNode[DirEntry]) -> DirectoryTree:
+        """Clear all nodes under the given node.
+
+        Returns:
+            The `Tree` instance (the subtree below the node).
+        """
+        self._line_cache.clear()
+        self._tree_lines_cached = None
+        node_label = node._label
+        node_data = node.data
+        node = TreeNode(
+            self,
+            None,
+            self._new_id(),
+            node_label,
+            node_data,
+            expanded=True,
+        )
+        self._updates += 1
+        self.refresh()
+        return self
+
+    def reset_subtree(
+        self, node: TreeNode[DirEntry], label: TextType, data: DirEntry | None = None
+    ) -> DirectoryTree:
+        """Clear the subtree and reset the given node.
+
+        Args:
+            label: The label for the node.
+            data: Optional data for the node.
+
+        Returns:
+            The `Tree` instance (the subtree below the node).
+        """
+        self.clear_subtree(node)
+        node.label = label
+        node.data = data
+        return self
+
+    def reload_node(self, node: TreeNode[DirEntry]) -> None:
+        """Reload the given node's contents.
+
+        Args:
+            node: The node to reload.
+        """
+        self.reset_subtree(
+            node, str(node.data.path.name), DirEntry(self.PATH(node.data.path))
+        )
+        self._add_to_load_queue(node)
+
     def validate_path(self, path: str | Path) -> Path:
         """Ensure that the path is of the `Path` type.
 
diff --git a/tests/tree/test_directory_tree_reload_node.py b/tests/tree/test_directory_tree_reload_node.py
new file mode 100644
index 0000000000..fee70d170d
--- /dev/null
+++ b/tests/tree/test_directory_tree_reload_node.py
@@ -0,0 +1,79 @@
+from rich.text import Text
+
+from textual.app import App, ComposeResult
+from textual.widgets import DirectoryTree
+
+
+class MyDirectoryTree(DirectoryTree):
+    pass
+
+
+class DirectoryTreeReloadApp(App[None]):
+    """DirectoryTree reloading test app."""
+
+    def __init__(self, path):
+        super().__init__()
+        self._tmp_path = path
+
+    def compose(self) -> ComposeResult:
+        yield MyDirectoryTree(self._tmp_path)
+
+
+async def test_directory_tree_reload_node(tmp_path) -> None:
+    """Reloading a node of a directory tree should display newly created file inside the directory."""
+
+    RELOADED_DIRECTORY = "parentdir"
+    NOT_RELOADED_DIRECTORY = "otherdir"
+    FILE1_NAME = "log.txt"
+    FILE1_CONTENT = "file1_content"
+    FILE2_NAME = "hello.txt"
+    FILE2_CONTENT = "file2_content"
+    NOT_RELOADED_FILE3_NAME = "demo.txt"
+    NOT_RELOADED_FILE3_CONTENT = "file3_content"
+    NOT_RELOADED_FILE4_NAME = "unseen.txt"
+    NOT_RELOADED_FILE4_CONTENT = "file4_content"
+
+    reloaded_dir = tmp_path / RELOADED_DIRECTORY
+    reloaded_dir.mkdir()
+    non_reloaded_dir = tmp_path / NOT_RELOADED_DIRECTORY
+    non_reloaded_dir.mkdir()
+    file1 = reloaded_dir / FILE1_NAME
+    file1.write_text(FILE1_CONTENT)
+    file3 = non_reloaded_dir / NOT_RELOADED_FILE3_NAME
+    file3.write_text(NOT_RELOADED_FILE3_CONTENT)
+    async with DirectoryTreeReloadApp(tmp_path).run_test() as pilot:
+        await pilot.pause()
+        tree = pilot.app.query_one(MyDirectoryTree)
+        assert len(tree.root.children) == 2
+        node = tree.root.children[1]
+        assert node.label == Text(RELOADED_DIRECTORY)
+        node.expand()
+        await pilot.pause()
+        assert len(node.children) == 1
+        assert node.children[0].label == Text(FILE1_NAME)
+        unaffected_node = tree.root.children[0]
+        unaffected_node.expand()
+        await pilot.pause()
+        assert len(unaffected_node.children) == 1
+        assert unaffected_node.children[0].label == Text(NOT_RELOADED_FILE3_NAME)
+
+        file2 = reloaded_dir / FILE2_NAME
+        file2.write_text(FILE2_CONTENT)
+        file4 = non_reloaded_dir / NOT_RELOADED_FILE4_NAME
+        file4.write_text(NOT_RELOADED_FILE4_CONTENT)
+        tree.reload_node(node)
+
+        await pilot.pause()
+        assert len(tree.root.children) == 2
+        node = tree.root.children[1]
+        assert node.label == Text(RELOADED_DIRECTORY)
+        node.collapse()
+        node.expand()
+        assert len(node.children) == 2
+        assert [child.label for child in node.children] == [
+            Text(filename) for filename in sorted({FILE1_NAME, FILE2_NAME})
+        ]
+        unaffected_node.collapse()
+        unaffected_node.expand()
+        assert len(unaffected_node.children) == 1
+        assert unaffected_node.children[0].label == Text(NOT_RELOADED_FILE3_NAME)


Code review comments:
path: tests/tree/test_directory_tree_reload_node.py
text: @user1:
This test looks great!

I'm just going to ask if we can avoid writing inside the files after creating them.
It saves us just a little bit of time! ⏩

diff_hunk: @@ -0,0 +1,79 @@
+from rich.text import Text
+
+from textual.app import App, ComposeResult
+from textual.widgets import DirectoryTree
+
+
+class MyDirectoryTree(DirectoryTree):
path: tests/tree/test_directory_tree_reload_node.py
text: @user1:
You can use `DirectoryTree` directly! 😃

diff_hunk: @@ -146,6 +146,56 @@ def reload(self) -> None:
         # loading up.
         self._add_to_load_queue(self.root)
 
+    def clear_subtree(self, node: TreeNode[DirEntry]) -> DirectoryTree:
+        """Clear all nodes under the given node.
+
+        Returns:
+            The `Tree` instance (the subtree below the node).
path: src/textual/widgets/_directory_tree.py
text: @user1:
You say "the subtree below the node", which doesn't sound right to me.
If you are returning `self`, you are returning the root tree.
If you return `node`, you are returning the root of the subtree that was cleared.
So, the docstring needs to be touched up.