-
Notifications
You must be signed in to change notification settings - Fork 14
Expand file tree
/
Copy pathutils.py
More file actions
242 lines (198 loc) · 6.59 KB
/
Copy pathutils.py
File metadata and controls
242 lines (198 loc) · 6.59 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
"""Utilities for inferring required packages and Panel extensions from code."""
import ast
import importlib.util
import traceback
from typing import Any
import sys
import types
# Check for pandas availability once at module level
_PANDAS_AVAILABLE = importlib.util.find_spec("pandas") is not None
def find_extensions(code: str, namespace: dict[str, Any] | None = None) -> list[str]:
"""Infer Panel extensions required for code execution.
Maps common packages/types to their Panel extensions:
- pandas DataFrame/Series -> "tabulator"
- plotly figures -> "plotly"
- bokeh models -> (none, default)
- matplotlib figures -> (none, uses pngpane)
- altair charts -> "vega"
- deck.gl -> "deckgl"
Parameters
----------
code : str
Python code to analyze
namespace : dict[str, Any] | None
Namespace from code execution (optional)
Returns
-------
list[str]
List of required Panel extension names
"""
extensions = []
# Check imports in code
if "plotly" in code:
extensions.append("plotly")
if "altair" in code:
extensions.append("vega")
if "pydeck" in code or "deck" in code:
extensions.append("deckgl")
# Check result type if namespace provided
if namespace is not None and _PANDAS_AVAILABLE:
result = namespace.get("_panel_result")
if result is not None:
import pandas as pd
if isinstance(result, (pd.DataFrame, pd.Series)):
extensions.append("tabulator")
return list(set(extensions)) # deduplicate
def find_requirements(code: str) -> list[str]:
"""Find package requirements from code.
Uses Panel's built-in find_requirements function to detect package dependencies.
Parameters
----------
code : str
Python code to analyze
Returns
-------
list[str]
List of required package names
"""
try:
# Import panel's find_requirements function
from panel.io.mime_render import find_requirements as panel_find_requirements
return panel_find_requirements(code)
except (ImportError, AttributeError):
# Fallback to simple AST-based parsing if panel function not available
try:
tree = ast.parse(code)
except SyntaxError:
return []
imports = set()
for node in ast.walk(tree):
if isinstance(node, ast.Import):
for alias in node.names:
imports.add(alias.name.split(".")[0])
elif isinstance(node, ast.ImportFrom):
if node.module:
imports.add(node.module.split(".")[0])
return list(imports)
def execute_in_module(
code: str,
module_name: str,
*,
cleanup: bool = True,
) -> dict[str, Any]:
"""Execute Python code in a proper module namespace.
Creates a types.ModuleType following Bokeh's pattern, registers it in
sys.modules, executes code, and optionally cleans up. This ensures
Panel decorators (@pn.cache, @pn.depends) and function references work
properly by using module.__dict__ as a single namespace for both globals
and locals.
Parameters
----------
code : str
Python code to execute
module_name : str
Unique name for the module (should be a valid Python identifier)
cleanup : bool, default=True
Whether to remove module from sys.modules after execution.
Set to False if you need to keep the module registered (e.g., for
later eval calls), but remember to clean up manually.
Returns
-------
dict[str, Any]
The module's namespace (module.__dict__) after execution
Raises
------
Exception
Any exception raised during code execution
Notes
-----
This pattern is critical for Panel decorators and code that uses function
introspection or cross-references. It follows Bokeh's CodeRunner pattern.
Examples
--------
>>> namespace = execute_in_module(
... "x = 1\ny = 2\nz = x + y",
... "my_module"
... )
>>> namespace['z']
3
"""
module = types.ModuleType(module_name)
module.__dict__['__file__'] = f"<{module_name}>"
sys.modules[module_name] = module
try:
exec(code, module.__dict__)
return module.__dict__
except Exception:
if cleanup:
sys.modules.pop(module_name, None)
raise
finally:
if cleanup:
sys.modules.pop(module_name, None)
def extract_last_expression(code: str) -> tuple[str, str]:
"""Extract the last expression from code for jupyter method.
Parameters
----------
code : str
Python code
Returns
-------
tuple[str, str]
(statements_code, last_expression_code)
"""
try:
tree = ast.parse(code)
except SyntaxError as e:
raise ValueError(f"Syntax error in code: {e}") from e
if not tree.body:
return "", ""
# Check if last node is an expression
last_node = tree.body[-1]
if isinstance(last_node, ast.Expr):
# Split code into statements and last expression
lines = code.split("\n")
last_line_start = last_node.lineno - 1
last_line_end = last_node.end_lineno if last_node.end_lineno else last_line_start + 1
statements = "\n".join(lines[:last_line_start])
last_expr = "\n".join(lines[last_line_start:last_line_end])
return statements, last_expr
else:
# No expression at end, return all as statements
return code, ""
def get_relative_view_url(id: str) -> str:
"""Generate a relative URL for viewing a visualization by ID.
Parameters
----------
id : str
Visualization ID
Returns
-------
str
Relative URL to view the visualization
"""
return f"./view?id={id}"
def validate_code(code: str) -> str:
"""
Validate Python code by attempting to execute it.
Parameters
----------
code : str
Python code to validate as a string.
Returns
-------
str
An empty string if the code is valid, otherwise the traceback of the error.
"""
try:
execute_in_module(code, module_name="_code_validation", cleanup=True)
except Exception as e:
# Get the traceback but skip the outermost frame (the exec call itself)
if e.__traceback__ is not None:
tb = e.__traceback__.tb_next # Skip the exec() frame
else:
tb = None
traceback_str = "".join(traceback.format_exception(type(e), e, tb))
traceback_str = traceback_str.strip()
return traceback_str
return ""