Skip to content

Commit 8a4179c

Browse files
author
John Lyu
committed
fixup! feat: Add filesystem-only caching with global refresh support
1 parent 0303b3f commit 8a4179c

File tree

5 files changed

+45
-259
lines changed

5 files changed

+45
-259
lines changed

docs/usage/caching.md

Lines changed: 7 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ your-project/
1919
├── docs/
2020
├── mkdocs.yml
2121
└── .markdown-exec-cache/
22-
├── my-plot.cache # Custom ID cache
2322
└── abc123def456.cache # Hash-based cache files
2423
```
2524

@@ -44,20 +43,6 @@ print(f"Executed at: {time.time()}")
4443

4544
The cache is automatically invalidated when the code or execution options change.
4645

47-
### Custom Cache IDs
48-
49-
For more control, use a custom cache ID (string value). This is useful for expensive operations where you want explicit control over cache invalidation:
50-
51-
````md exec="1" source="tabbed-left" tabs="Markdown|Rendered"
52-
```python exec="yes" cache="my-plot"
53-
import matplotlib.pyplot as plt
54-
# Expensive plot generation...
55-
print("Generated plot")
56-
```
57-
````
58-
59-
The cache file will be stored as `.markdown-exec-cache/my-plot.cache`.
60-
6146
### Cache Invalidation
6247

6348
The cache is automatically invalidated when the code content or execution options change (a new hash is computed). **Stale cache files** — from code blocks that have been removed or changed — are cleaned up automatically:
@@ -85,16 +70,6 @@ This is useful for:
8570

8671
## Clearing Cache
8772

88-
### Delete Specific Cache Entry
89-
90-
Remove the cache file for a specific custom ID:
91-
92-
```bash
93-
rm .markdown-exec-cache/my-custom-id.cache
94-
```
95-
96-
### Clear All Cache
97-
9873
Remove the entire cache directory:
9974

10075
```bash
@@ -136,57 +111,24 @@ rm -rf .markdown-exec-cache/
136111

137112
### Choosing Cache Type
138113

139-
- **`cache="yes"`** (hash-based):
140-
141-
- Automatically invalidated when code changes
142-
- Great for development and production
143-
- No manual cache management needed
144-
145-
- **`cache="custom-id"`** (custom ID):
146-
147-
- Use for expensive operations where you want explicit control
148-
- Easier to identify and manage specific cache files
149-
- Requires manual invalidation or `refresh="yes"` when code changes
114+
Use `cache="yes"` for all caching needs. The cache is automatically invalidated when the code or execution options change — no manual cache management needed.
150115

151116
### Cache Invalidation Strategy
152117

153-
**For hash-based caching (`cache="yes"`):**
118+
Cache is automatically invalidated when code or options change — no manual intervention needed. To force re-execution of all cached blocks, use:
154119

155-
- Cache is automatically invalidated when code or options change
156-
- No manual intervention needed
157-
158-
**For custom ID caching (`cache="custom-id"`):**
159-
160-
1. **Change the ID** when you want to force re-execution:
161-
162-
```markdown
163-
cache="my-plot-v2" # Changed from my-plot
164-
```
165-
166-
1. **Use refresh temporarily**:
167-
168-
```markdown
169-
cache="my-plot" refresh="yes" # Remove refresh="yes" after update
170-
```
171-
172-
1. **Use global refresh** for all caches:
173-
174-
```bash
175-
MARKDOWN_EXEC_CACHE_REFRESH=1 mkdocs build
176-
```
177-
178-
1. **Clear cache directory** before important builds:
120+
```bash
121+
MARKDOWN_EXEC_CACHE_REFRESH=1 mkdocs build
122+
```
179123

180-
```bash
181-
rm -rf .markdown-exec-cache/
182-
```
124+
Or [clear the cache directory](#clearing-cache) before the build.
183125

184126
## Examples
185127

186128
### Caching a Matplotlib Plot
187129

188130
````markdown
189-
```python exec="yes" html="yes" cache="population-chart"
131+
```python exec="yes" html="yes" cache="yes"
190132
import matplotlib.pyplot as plt
191133
import io
192134
import base64
@@ -206,17 +148,6 @@ plt.close()
206148
```
207149
````
208150

209-
### Caching API Calls
210-
211-
````markdown
212-
```python exec="yes" cache="github-stars" refresh="no"
213-
import requests
214-
response = requests.get("https://api.github.com/repos/pawamoy/markdown-exec")
215-
stars = response.json()["stargazers_count"]
216-
print(f"⭐ **{stars}** stars on GitHub!")
217-
```
218-
````
219-
220151
## Troubleshooting
221152

222153
### Cache Not Working

src/markdown_exec/_internal/cache.py

Lines changed: 13 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -73,29 +73,25 @@ def _compute_hash(self, code: str, **options: Any) -> str:
7373
)
7474
return hashlib.sha256(cache_key.encode()).hexdigest()
7575

76-
def _get_cache_path(self, cache_id: str) -> Path:
76+
def _get_cache_path(self, cache_key: str) -> Path:
7777
"""Get the filesystem path for a cache entry.
7878
7979
Parameters:
80-
cache_id: The cache identifier (hash or custom ID).
80+
cache_key: The cache key (hash).
8181
8282
Returns:
8383
Path to the cache file.
8484
"""
85-
# Sanitize the cache_id to prevent path traversal
86-
safe_id = "".join(c if c.isalnum() or c in "-_" else "_" for c in cache_id)
87-
return self.cache_dir / f"{safe_id}.cache"
85+
return self.cache_dir / f"{cache_key}.cache"
8886

8987
def get(
9088
self,
91-
cache_id: str | None,
9289
code: str,
9390
**options: Any,
9491
) -> str | None:
9592
"""Retrieve cached output for the given code.
9693
9794
Parameters:
98-
cache_id: Custom cache identifier, or None to use hash-based caching.
9995
code: The source code.
10096
**options: Execution options used for hash computation.
10197
@@ -107,8 +103,7 @@ def get(
107103
_logger.debug("Global cache refresh active, forcing re-execution")
108104
return None
109105

110-
# Determine the cache key
111-
cache_key = self._compute_hash(code, **options) if cache_id is None else cache_id
106+
cache_key = self._compute_hash(code, **options)
112107

113108
# Check filesystem cache
114109
cache_path = self._get_cache_path(cache_key)
@@ -127,21 +122,18 @@ def get(
127122

128123
def set(
129124
self,
130-
cache_id: str | None,
131125
code: str,
132126
output: str,
133127
**options: Any,
134128
) -> None:
135129
"""Store output in cache for the given code.
136130
137131
Parameters:
138-
cache_id: Custom cache identifier, or None to use hash-based caching.
139132
code: The source code.
140133
output: The execution output to cache.
141134
**options: Execution options used for hash computation.
142135
"""
143-
# Determine the cache key
144-
cache_key = self._compute_hash(code, **options) if cache_id is None else cache_id
136+
cache_key = self._compute_hash(code, **options)
145137

146138
# Write to filesystem cache
147139
cache_path = self._get_cache_path(cache_key)
@@ -169,29 +161,14 @@ def cleanup_stale(self) -> None:
169161
except OSError as error:
170162
_logger.warning("Failed to delete stale cache file %s: %s", cache_file, error)
171163

172-
def clear(self, cache_id: str | None = None) -> None:
173-
"""Clear the filesystem cache.
174-
175-
Parameters:
176-
cache_id: Specific cache ID to clear, or None to clear all.
177-
"""
178-
if cache_id is None:
179-
# Clear all cache files
180-
for cache_file in self.cache_dir.glob("*.cache"):
181-
try:
182-
cache_file.unlink()
183-
_logger.debug("Deleted cache file: %s", cache_file)
184-
except OSError as error:
185-
_logger.warning("Failed to delete cache file %s: %s", cache_file, error)
186-
else:
187-
# Clear specific cache file
188-
cache_path = self._get_cache_path(cache_id)
189-
if cache_path.exists():
190-
try:
191-
cache_path.unlink()
192-
_logger.debug("Deleted cache file: %s", cache_path)
193-
except OSError as error:
194-
_logger.warning("Failed to delete cache file %s: %s", cache_path, error)
164+
def clear(self) -> None:
165+
"""Clear all cached files."""
166+
for cache_file in self.cache_dir.glob("*.cache"):
167+
try:
168+
cache_file.unlink()
169+
_logger.debug("Deleted cache file: %s", cache_file)
170+
except OSError as error:
171+
_logger.warning("Failed to delete cache file %s: %s", cache_file, error)
195172

196173

197174
# Global cache manager instance

src/markdown_exec/_internal/formatters/base.py

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ def base_format(
106106
update_toc: bool = True,
107107
workdir: str | None = None,
108108
width: int | None = None,
109-
cache: bool | str = False,
109+
cache: bool = False,
110110
**options: Any,
111111
) -> Markup:
112112
"""Execute code and return HTML.
@@ -130,8 +130,7 @@ def base_format(
130130
update_toc: Whether to include generated headings
131131
into the Markdown table of contents (toc extension).
132132
workdir: The working directory to use for the execution.
133-
cache: Whether to enable caching. If True, uses hash-based caching.
134-
If a string, uses that string as a custom cache ID for cross-build persistence.
133+
cache: Whether to enable caching.
135134
**options: Additional options passed from the formatter.
136135
137136
Returns:
@@ -150,8 +149,6 @@ def base_format(
150149
output = None
151150
if cache:
152151
cache_manager = get_cache_manager()
153-
cache_id = cache if isinstance(cache, str) else None
154-
155152
# Build cache options (exclude cache itself and other non-execution options)
156153
cache_options = {
157154
"language": language,
@@ -163,7 +160,7 @@ def base_format(
163160
"extra": extra,
164161
}
165162

166-
output = cache_manager.get(cache_id, source_input, **cache_options)
163+
output = cache_manager.get(source_input, **cache_options)
167164
if output is not None:
168165
_logger.debug("Using cached output for code block")
169166

@@ -186,7 +183,7 @@ def base_format(
186183

187184
# Cache the output if caching is enabled
188185
if cache:
189-
cache_manager.set(cache_id, source_input, output, **cache_options)
186+
cache_manager.set(source_input, output, **cache_options)
190187

191188
if not output and not source:
192189
return Markup()

src/markdown_exec/_internal/main.py

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -77,13 +77,8 @@ def validator(
7777
workdir_value = inputs.pop("workdir", None)
7878
width_value = int(inputs.pop("width", "0"))
7979

80-
# Handle cache option: can be boolean or custom string ID
8180
cache_value = inputs.pop("cache", "")
82-
cache_enabled = (
83-
_to_bool(cache_value)
84-
if cache_value.lower() in {"yes", "on", "true", "1", "no", "off", "false", "0", ""}
85-
else cache_value
86-
)
81+
cache_enabled = _to_bool(cache_value)
8782

8883
options["id"] = id_value
8984
options["id_prefix"] = id_prefix_value

0 commit comments

Comments
 (0)