0
# File Management
1
2
Dynamic file loading system with live reloading capabilities for development workflows and virtual file management for inline content. This system enables seamless development experiences with hot module replacement and flexible content management.
3
4
## Capabilities
5
6
### FileContents Class
7
8
Watches file changes and emits signals when files are modified, enabling live reloading during development.
9
10
```python { .api }
11
class FileContents:
12
"""
13
Object that watches for file changes and emits a signal when it changes.
14
15
Provides live reloading capabilities by monitoring filesystem changes
16
and automatically updating widget content during development.
17
18
Attributes:
19
changed (Signal): Emitted when file contents change
20
deleted (Signal): Emitted when file is deleted
21
"""
22
23
def __init__(self, path: str | Path, start_thread: bool = True):
24
"""
25
Initialize file watcher for the specified path.
26
27
Parameters:
28
path (str | Path): The file to read and watch for content changes
29
start_thread (bool): Whether to start watching for changes in a separate thread
30
31
Raises:
32
ValueError: If the file does not exist
33
"""
34
35
def watch_in_thread(self):
36
"""
37
Watch for file changes (emitting signals) from a separate thread.
38
39
Starts a background thread that monitors the file for changes
40
and emits the 'changed' signal when modifications are detected.
41
"""
42
43
def stop_thread(self):
44
"""
45
Stops an actively running file watching thread if it exists.
46
47
Cleanly shuts down the background file monitoring thread
48
and releases associated resources.
49
"""
50
51
def watch(self):
52
"""
53
Watch for file changes and emit changed/deleted signal events.
54
55
Blocks indefinitely, yielding change events until the file is deleted.
56
57
Returns:
58
Iterator[tuple[int, str]]: Iterator yielding change events
59
60
Raises:
61
ImportError: If watchfiles package is not installed
62
"""
63
64
def __str__(self) -> str:
65
"""
66
Return current file contents as string.
67
68
Reads and caches file contents, returning the current text.
69
Cache is cleared when file changes are detected.
70
71
Returns:
72
str: Current file contents
73
"""
74
```
75
76
### VirtualFileContents Class
77
78
Stores text file contents in memory with change signals, useful for dynamic content and cell magic integration.
79
80
```python { .api }
81
class VirtualFileContents:
82
"""
83
Stores text file contents in memory and emits a signal when it changes.
84
85
Provides in-memory file simulation with change notifications,
86
useful for dynamic content and integration with IPython cell magics.
87
88
Attributes:
89
changed (Signal): Emitted when contents change
90
contents (str): Current text contents (property)
91
"""
92
93
def __init__(self, contents: str = ""):
94
"""
95
Initialize virtual file with optional initial contents.
96
97
Parameters:
98
contents (str): The initial contents of the file (default: "")
99
"""
100
101
@property
102
def contents(self) -> str:
103
"""
104
Get current file contents.
105
106
Returns:
107
str: Current text contents
108
"""
109
110
@contents.setter
111
def contents(self, value: str):
112
"""
113
Set file contents and emit changed signal.
114
115
Parameters:
116
value (str): New file contents
117
"""
118
119
def __str__(self) -> str:
120
"""
121
Return current contents as string.
122
123
Returns:
124
str: Current file contents
125
"""
126
```
127
128
### File Utilities
129
130
Helper functions for working with file paths and content management.
131
132
```python { .api }
133
def try_file_contents(x) -> FileContents | VirtualFileContents | None:
134
"""
135
Try to coerce an object into a FileContents object.
136
137
Attempts to convert strings, paths, or virtual file references
138
into appropriate FileContents or VirtualFileContents objects.
139
140
Parameters:
141
x: Object to try to coerce (str, Path, etc.)
142
143
Returns:
144
FileContents | VirtualFileContents | None: File contents object or None if conversion fails
145
146
Raises:
147
FileNotFoundError: If the file path exists but file is not found
148
"""
149
150
def try_file_path(x) -> Path | None:
151
"""
152
Try to coerce an object into a pathlib.Path object.
153
154
Handles various input types and validates file path patterns:
155
- Returns None for URLs or multi-line strings
156
- Returns Path for single-line strings with file extensions
157
- Returns existing Path objects unchanged
158
159
Parameters:
160
x: Object to try to coerce into a path
161
162
Returns:
163
Path | None: Path object if x is a valid file path, otherwise None
164
"""
165
```
166
167
### Global Registry
168
169
```python { .api }
170
_VIRTUAL_FILES: weakref.WeakValueDictionary[str, VirtualFileContents]
171
"""
172
Global registry of virtual files.
173
174
Weak reference dictionary that stores virtual file contents
175
by name, automatically cleaning up unused references.
176
"""
177
```
178
179
## Usage Examples
180
181
### File-Based Widget Development
182
183
```python
184
import anywidget
185
186
class DevelopmentWidget(anywidget.AnyWidget):
187
# Files are automatically watched for changes
188
_esm = "./widget.js" # Reloads when widget.js changes
189
_css = "./widget.css" # Reloads when widget.css changes
190
191
# Enable hot module replacement for development
192
import os
193
os.environ["ANYWIDGET_HMR"] = "1"
194
195
widget = DevelopmentWidget()
196
# Edit widget.js or widget.css - changes appear immediately
197
```
198
199
### Manual File Watching
200
201
```python
202
from anywidget._file_contents import FileContents
203
import pathlib
204
205
# Create file watcher
206
js_file = FileContents("./my-widget.js")
207
208
# Connect to change events
209
@js_file.changed.connect
210
def on_js_change(new_contents):
211
print(f"JavaScript updated: {len(new_contents)} characters")
212
213
@js_file.deleted.connect
214
def on_js_deleted():
215
print("JavaScript file was deleted!")
216
217
# Access current contents
218
print(str(js_file)) # Reads current file contents
219
220
# Stop watching when done
221
js_file.stop_thread()
222
```
223
224
### Virtual File Management
225
226
```python
227
from anywidget._file_contents import VirtualFileContents, _VIRTUAL_FILES
228
229
# Create virtual file
230
virtual_js = VirtualFileContents("""
231
function render({ model, el }) {
232
el.innerHTML = "<h1>Dynamic Content</h1>";
233
}
234
export default { render };
235
""")
236
237
# Register in global registry
238
_VIRTUAL_FILES["my-widget"] = virtual_js
239
240
# Connect to change events
241
@virtual_js.changed.connect
242
def on_virtual_change(new_contents):
243
print("Virtual file updated!")
244
245
# Update contents programmatically
246
virtual_js.contents = """
247
function render({ model, el }) {
248
el.innerHTML = "<h1>Updated Dynamic Content</h1>";
249
}
250
export default { render };
251
"""
252
253
# Use in widget
254
import anywidget
255
256
class VirtualWidget(anywidget.AnyWidget):
257
_esm = "my-widget" # References virtual file by name
258
259
widget = VirtualWidget()
260
```
261
262
### Development Workflow with Live Reloading
263
264
```python
265
import anywidget
266
import traitlets as t
267
from pathlib import Path
268
269
# Create development files
270
js_path = Path("./counter.js")
271
css_path = Path("./counter.css")
272
273
js_path.write_text("""
274
function render({ model, el }) {
275
let count = () => model.get("value");
276
let btn = document.createElement("button");
277
btn.innerHTML = `Count: ${count()}`;
278
btn.className = "counter-btn";
279
280
btn.addEventListener("click", () => {
281
model.set("value", count() + 1);
282
});
283
284
model.on("change:value", () => {
285
btn.innerHTML = `Count: ${count()}`;
286
});
287
288
el.appendChild(btn);
289
}
290
export default { render };
291
""")
292
293
css_path.write_text("""
294
.counter-btn {
295
background: #007cba;
296
color: white;
297
border: none;
298
padding: 10px 20px;
299
border-radius: 5px;
300
cursor: pointer;
301
font-size: 16px;
302
}
303
.counter-btn:hover {
304
background: #005a8b;
305
}
306
""")
307
308
class CounterWidget(anywidget.AnyWidget):
309
_esm = js_path # FileContents automatically created
310
_css = css_path # FileContents automatically created
311
312
value = t.Int(0).tag(sync=True)
313
314
# Enable hot reloading
315
import os
316
os.environ["ANYWIDGET_HMR"] = "1"
317
318
widget = CounterWidget()
319
# Now edit counter.js or counter.css files - changes appear immediately in Jupyter
320
```
321
322
### Custom File Content Processing
323
324
```python
325
from anywidget._file_contents import FileContents
326
import json
327
328
class ConfigFileWatcher:
329
def __init__(self, config_path):
330
self.config_file = FileContents(config_path)
331
self.config = {}
332
self._load_config()
333
334
# Watch for changes
335
self.config_file.changed.connect(self._on_config_change)
336
337
def _load_config(self):
338
"""Load and parse configuration"""
339
try:
340
self.config = json.loads(str(self.config_file))
341
except json.JSONDecodeError:
342
self.config = {}
343
print("Warning: Invalid JSON in config file")
344
345
def _on_config_change(self, new_contents):
346
"""Handle configuration file changes"""
347
print("Configuration file updated, reloading...")
348
self._load_config()
349
self._apply_config()
350
351
def _apply_config(self):
352
"""Apply configuration changes"""
353
print(f"Applied config: {self.config}")
354
355
# Usage
356
config_watcher = ConfigFileWatcher("./app-config.json")
357
# Edit app-config.json - changes are automatically detected and processed
358
```
359
360
### Integration with Widget Updates
361
362
```python
363
import anywidget
364
import traitlets as t
365
from anywidget._file_contents import FileContents
366
367
class ConfigurableWidget(anywidget.AnyWidget):
368
_esm = """
369
function render({ model, el }) {
370
let updateDisplay = () => {
371
let config = model.get("config");
372
el.innerHTML = `
373
<div style="
374
background: ${config.background || '#f0f0f0'};
375
color: ${config.color || '#333'};
376
padding: ${config.padding || '10px'};
377
border-radius: ${config.borderRadius || '5px'};
378
">
379
<h3>${config.title || 'Default Title'}</h3>
380
<p>${config.message || 'Default message'}</p>
381
</div>
382
`;
383
};
384
385
model.on("change:config", updateDisplay);
386
updateDisplay();
387
}
388
export default { render };
389
"""
390
391
config = t.Dict({}).tag(sync=True)
392
393
def __init__(self, config_file=None, **kwargs):
394
super().__init__(**kwargs)
395
396
if config_file:
397
self.config_watcher = FileContents(config_file)
398
self.config_watcher.changed.connect(self._update_from_file)
399
self._update_from_file(str(self.config_watcher))
400
401
def _update_from_file(self, contents):
402
"""Update widget config from file contents"""
403
try:
404
import json
405
new_config = json.loads(contents)
406
self.config = new_config
407
except json.JSONDecodeError:
408
print("Warning: Invalid JSON in config file")
409
410
# Create widget with external config file
411
widget = ConfigurableWidget(config_file="./widget-config.json")
412
# Edit widget-config.json to change widget appearance in real-time
413
```