nighthawk-python 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,346 @@
1
+ from __future__ import annotations
2
+
3
+ import ast
4
+
5
+ from .blocks import (
6
+ _joined_string_is_natural_sentinel,
7
+ _joined_string_scan_text,
8
+ _validate_joined_string_bindings_do_not_span_formatted_values,
9
+ extract_bindings,
10
+ extract_program,
11
+ is_natural_sentinel,
12
+ )
13
+
14
+
15
+ class NaturalTransformer(ast.NodeTransformer):
16
+ def __init__(self, *, captured_name_tuple: tuple[str, ...]) -> None:
17
+ super().__init__()
18
+ self._captured_name_tuple = captured_name_tuple
19
+ self._return_annotation_stack: list[ast.expr | None] = []
20
+ self._binding_name_to_type_expression_stack: list[dict[str, ast.expr]] = []
21
+ self._is_async_function_stack: list[bool] = []
22
+ self._loop_depth = 0
23
+
24
+ def _visit_function_like(self, node: ast.FunctionDef | ast.AsyncFunctionDef) -> ast.AST:
25
+ self._return_annotation_stack.append(node.returns)
26
+ self._binding_name_to_type_expression_stack.append(self._collect_binding_name_to_type_expression(node))
27
+ self._is_async_function_stack.append(isinstance(node, ast.AsyncFunctionDef))
28
+ saved_loop_depth = self._loop_depth
29
+ self._loop_depth = 0
30
+ try:
31
+ if node.body:
32
+ first_statement = node.body[0]
33
+ if (
34
+ isinstance(first_statement, ast.Expr)
35
+ and isinstance(first_statement.value, ast.Constant)
36
+ and isinstance(first_statement.value.value, str)
37
+ ):
38
+ docstring_text = first_statement.value.value
39
+ if is_natural_sentinel(docstring_text):
40
+ program = extract_program(docstring_text)
41
+ input_bindings, output_bindings = extract_bindings(program)
42
+ return_annotation = self._current_return_annotation_expression()
43
+ binding_types_dict_expression = self._current_binding_types_dict_expression(output_bindings)
44
+ injected = build_runtime_call_and_assignments(
45
+ ast.Constant(program),
46
+ input_bindings,
47
+ output_bindings,
48
+ binding_types_dict_expression,
49
+ return_annotation,
50
+ is_in_loop=self._loop_depth > 0,
51
+ is_async_function=self._is_async_function_stack[-1],
52
+ )
53
+
54
+ # Preserve user-source location: the injected runtime call and its
55
+ # subsequent assignments should point at the Natural docstring
56
+ # sentinel line (the opening triple-quote line).
57
+ sentinel_location = ast.copy_location(ast.Pass(), first_statement)
58
+ sentinel_location.end_lineno = sentinel_location.lineno
59
+ sentinel_location.end_col_offset = sentinel_location.col_offset
60
+
61
+ injected_with_location = [ast.copy_location(statement, sentinel_location) for statement in injected]
62
+
63
+ body_without_docstring = node.body[1:]
64
+ node.body = injected_with_location + body_without_docstring
65
+
66
+ node = self.generic_visit(node) # type: ignore[assignment]
67
+
68
+ if self._captured_name_tuple:
69
+ anchor_name = "__nh_cell_anchor__"
70
+ name_to_cell_name = "__nh_name_to_cell__"
71
+
72
+ anchor_body: list[ast.stmt] = [
73
+ ast.Return(
74
+ value=ast.Tuple(
75
+ elts=[ast.Name(id=name, ctx=ast.Load()) for name in self._captured_name_tuple],
76
+ ctx=ast.Load(),
77
+ )
78
+ )
79
+ ]
80
+
81
+ anchor_function = ast.FunctionDef(
82
+ name=anchor_name,
83
+ args=ast.arguments(
84
+ posonlyargs=[],
85
+ args=[],
86
+ kwonlyargs=[],
87
+ kw_defaults=[],
88
+ defaults=[],
89
+ ),
90
+ body=anchor_body,
91
+ decorator_list=[],
92
+ returns=None,
93
+ type_comment=None,
94
+ )
95
+
96
+ freevars_expression = ast.Attribute(
97
+ value=ast.Attribute(value=ast.Name(id=anchor_name, ctx=ast.Load()), attr="__code__", ctx=ast.Load()),
98
+ attr="co_freevars",
99
+ ctx=ast.Load(),
100
+ )
101
+
102
+ closure_expression = ast.BoolOp(
103
+ op=ast.Or(),
104
+ values=[
105
+ ast.Attribute(value=ast.Name(id=anchor_name, ctx=ast.Load()), attr="__closure__", ctx=ast.Load()),
106
+ ast.Tuple(elts=[], ctx=ast.Load()),
107
+ ],
108
+ )
109
+
110
+ name_to_cell_value = ast.Call(
111
+ func=ast.Name(id="dict", ctx=ast.Load()),
112
+ args=[
113
+ ast.Call(
114
+ func=ast.Name(id="zip", ctx=ast.Load()),
115
+ args=[freevars_expression, closure_expression],
116
+ keywords=[],
117
+ )
118
+ ],
119
+ keywords=[],
120
+ )
121
+
122
+ name_to_cell_assign = ast.Assign(
123
+ targets=[ast.Name(id=name_to_cell_name, ctx=ast.Store())],
124
+ value=name_to_cell_value,
125
+ )
126
+
127
+ with_statement = ast.With(
128
+ items=[
129
+ ast.withitem(
130
+ context_expr=ast.Call(
131
+ func=ast.Name(id="__nh_python_cell_scope__", ctx=ast.Load()),
132
+ args=[ast.Name(id=name_to_cell_name, ctx=ast.Load())],
133
+ keywords=[],
134
+ ),
135
+ optional_vars=None,
136
+ )
137
+ ],
138
+ body=node.body,
139
+ type_comment=None,
140
+ )
141
+
142
+ node.body = [anchor_function, name_to_cell_assign, with_statement]
143
+
144
+ return node
145
+ finally:
146
+ self._is_async_function_stack.pop()
147
+ self._binding_name_to_type_expression_stack.pop()
148
+ self._return_annotation_stack.pop()
149
+ self._loop_depth = saved_loop_depth
150
+
151
+ def visit_FunctionDef(self, node: ast.FunctionDef) -> ast.AST:
152
+ return self._visit_function_like(node)
153
+
154
+ def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef) -> ast.AST: # noqa: N802
155
+ return self._visit_function_like(node)
156
+
157
+ def visit_For(self, node: ast.For) -> ast.AST:
158
+ self._loop_depth += 1
159
+ try:
160
+ return self.generic_visit(node)
161
+ finally:
162
+ self._loop_depth -= 1
163
+
164
+ def visit_AsyncFor(self, node: ast.AsyncFor) -> ast.AST:
165
+ self._loop_depth += 1
166
+ try:
167
+ return self.generic_visit(node)
168
+ finally:
169
+ self._loop_depth -= 1
170
+
171
+ def visit_While(self, node: ast.While) -> ast.AST:
172
+ self._loop_depth += 1
173
+ try:
174
+ return self.generic_visit(node)
175
+ finally:
176
+ self._loop_depth -= 1
177
+
178
+ def visit_Expr(self, node: ast.Expr) -> ast.AST | list[ast.stmt]:
179
+ self.generic_visit(node)
180
+ value = node.value
181
+
182
+ if isinstance(value, ast.Constant) and isinstance(value.value, str):
183
+ text = value.value
184
+ if is_natural_sentinel(text):
185
+ program = extract_program(text)
186
+ input_bindings, output_bindings = extract_bindings(program)
187
+ return_annotation = self._current_return_annotation_expression()
188
+ binding_types_dict_expression = self._current_binding_types_dict_expression(output_bindings)
189
+ is_in_loop = self._loop_depth > 0
190
+ statements = build_runtime_call_and_assignments(
191
+ ast.Constant(program),
192
+ input_bindings,
193
+ output_bindings,
194
+ binding_types_dict_expression,
195
+ return_annotation,
196
+ is_in_loop=is_in_loop,
197
+ is_async_function=self._is_async_function_stack[-1] if self._is_async_function_stack else False,
198
+ )
199
+
200
+ sentinel_location = ast.copy_location(ast.Pass(), node)
201
+ sentinel_location.end_lineno = sentinel_location.lineno
202
+ sentinel_location.end_col_offset = sentinel_location.col_offset
203
+
204
+ return [ast.copy_location(statement, sentinel_location) for statement in statements] # type: ignore[return-value]
205
+
206
+ if isinstance(value, ast.JoinedStr) and _joined_string_is_natural_sentinel(value):
207
+ _validate_joined_string_bindings_do_not_span_formatted_values(value)
208
+ return_annotation = self._current_return_annotation_expression()
209
+ is_in_loop = self._loop_depth > 0
210
+
211
+ scan_text = _joined_string_scan_text(value, formatted_value_placeholder="")
212
+ program = extract_program(scan_text)
213
+ input_bindings, output_bindings = extract_bindings(program)
214
+ binding_types_dict_expression = self._current_binding_types_dict_expression(output_bindings)
215
+
216
+ extracted_program_call = ast.Call(
217
+ func=ast.Name(id="__nh_extract_program__", ctx=ast.Load()),
218
+ args=[value],
219
+ keywords=[],
220
+ )
221
+
222
+ statements = build_runtime_call_and_assignments(
223
+ extracted_program_call,
224
+ input_bindings,
225
+ output_bindings,
226
+ binding_types_dict_expression,
227
+ return_annotation,
228
+ is_in_loop=is_in_loop,
229
+ is_async_function=self._is_async_function_stack[-1] if self._is_async_function_stack else False,
230
+ )
231
+
232
+ sentinel_location = ast.copy_location(ast.Pass(), node)
233
+ sentinel_location.end_lineno = sentinel_location.lineno
234
+ sentinel_location.end_col_offset = sentinel_location.col_offset
235
+
236
+ return [ast.copy_location(statement, sentinel_location) for statement in statements] # type: ignore[return-value]
237
+
238
+ return node
239
+
240
+ def _current_return_annotation_expression(self) -> ast.expr:
241
+ if not self._return_annotation_stack:
242
+ return ast.Name(id="object", ctx=ast.Load())
243
+ annotation = self._return_annotation_stack[-1]
244
+ if annotation is None:
245
+ return ast.Name(id="object", ctx=ast.Load())
246
+ return annotation
247
+
248
+ def _collect_binding_name_to_type_expression(
249
+ self,
250
+ node: ast.FunctionDef | ast.AsyncFunctionDef,
251
+ ) -> dict[str, ast.expr]:
252
+ binding_name_to_type_expression: dict[str, ast.expr] = {}
253
+
254
+ for argument in [
255
+ *node.args.posonlyargs,
256
+ *node.args.args,
257
+ *node.args.kwonlyargs,
258
+ ]:
259
+ if argument.annotation is not None:
260
+ binding_name_to_type_expression[argument.arg] = argument.annotation
261
+
262
+ for statement in node.body:
263
+ if isinstance(statement, ast.AnnAssign):
264
+ target = statement.target
265
+ if isinstance(target, ast.Name):
266
+ binding_name_to_type_expression[target.id] = statement.annotation
267
+
268
+ return binding_name_to_type_expression
269
+
270
+ def _current_binding_types_dict_expression(self, binding_names: tuple[str, ...]) -> ast.expr:
271
+ if not binding_names:
272
+ return ast.Dict(keys=[], values=[])
273
+
274
+ binding_name_to_type_expression: dict[str, ast.expr] = {}
275
+ if self._binding_name_to_type_expression_stack:
276
+ binding_name_to_type_expression = self._binding_name_to_type_expression_stack[-1]
277
+
278
+ keys: list[ast.expr | None] = []
279
+ values: list[ast.expr] = []
280
+ for name in binding_names:
281
+ keys.append(ast.Constant(name))
282
+ values.append(binding_name_to_type_expression.get(name, ast.Name(id="object", ctx=ast.Load())))
283
+
284
+ return ast.Dict(keys=keys, values=values)
285
+
286
+
287
+ def build_runtime_call_and_assignments(
288
+ natural_program_expression: ast.expr,
289
+ input_binding_names: tuple[str, ...],
290
+ output_binding_names: tuple[str, ...],
291
+ binding_types_dict_expression: ast.expr,
292
+ return_annotation: ast.expr,
293
+ *,
294
+ is_in_loop: bool,
295
+ is_async_function: bool,
296
+ ) -> list[ast.stmt]:
297
+ # Build the runner method call (the only part requiring hand-built AST
298
+ # because natural_program_expression, binding_types_dict_expression, and
299
+ # return_annotation are dynamic AST nodes from the source).
300
+ method_name = "run_step_async" if is_async_function else "run_step"
301
+ call_expression = ast.Call(
302
+ func=ast.Attribute(
303
+ value=ast.Name(id="__nighthawk_runner__", ctx=ast.Load()),
304
+ attr=method_name,
305
+ ctx=ast.Load(),
306
+ ),
307
+ args=[
308
+ natural_program_expression,
309
+ ast.List(elts=[ast.Constant(name) for name in input_binding_names], ctx=ast.Load()),
310
+ ast.List(elts=[ast.Constant(name) for name in output_binding_names], ctx=ast.Load()),
311
+ binding_types_dict_expression,
312
+ return_annotation,
313
+ ast.Constant(is_in_loop),
314
+ ],
315
+ keywords=[],
316
+ )
317
+ envelope_value: ast.expr = ast.Await(value=call_expression) if is_async_function else call_expression
318
+
319
+ # Parse template for the common envelope-unpacking structure.
320
+ statements: list[ast.stmt] = ast.parse('__nh_envelope__ = None\n__nh_bindings__ = __nh_envelope__["bindings"]\n').body
321
+ # Replace the None placeholder with the actual call expression.
322
+ statements[0].value = envelope_value # type: ignore[attr-defined]
323
+
324
+ # Add binding commit assignments (dynamic per output binding).
325
+ for name in output_binding_names:
326
+ statements.extend(ast.parse(f'if "{name}" in __nh_bindings__:\n {name} = __nh_bindings__["{name}"]\n').body)
327
+
328
+ # Add outcome extraction and dispatch.
329
+ statements.extend(ast.parse('__nh_step_outcome__ = __nh_envelope__["step_outcome"]').body)
330
+
331
+ outcome_source = (
332
+ 'if __nh_step_outcome__ is not None:\n if __nh_step_outcome__.kind == "return":\n return __nh_envelope__["return_value"]\n'
333
+ )
334
+ if is_in_loop:
335
+ outcome_source += (
336
+ ' if __nh_step_outcome__.kind == "break":\n break\n if __nh_step_outcome__.kind == "continue":\n continue\n'
337
+ )
338
+ statements.extend(ast.parse(outcome_source).body)
339
+
340
+ return statements
341
+
342
+
343
+ def transform_module_ast(module: ast.Module, *, captured_name_tuple: tuple[str, ...] = ()) -> ast.Module:
344
+ module = NaturalTransformer(captured_name_tuple=captured_name_tuple).visit(module) # type: ignore[assignment]
345
+ ast.fix_missing_locations(module)
346
+ return module
File without changes
@@ -0,0 +1,50 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import contextvars
5
+ import inspect
6
+ import threading
7
+ from collections.abc import Awaitable, Callable, Coroutine
8
+ from typing import Any, cast
9
+
10
+
11
+ def run_coroutine_synchronously(coroutine_call: Callable[[], Coroutine[Any, Any, Any]]) -> Any:
12
+ try:
13
+ asyncio.get_running_loop()
14
+ except RuntimeError:
15
+ return asyncio.run(coroutine_call())
16
+
17
+ execution_context = contextvars.copy_context()
18
+ result_container: dict[str, Any] = {}
19
+ exception_container: dict[str, BaseException] = {}
20
+
21
+ def run_coroutine_call_in_thread() -> None:
22
+ try:
23
+ result_container["result"] = execution_context.run(lambda: asyncio.run(coroutine_call()))
24
+ except BaseException as exception:
25
+ exception_container["exception"] = exception
26
+
27
+ thread = threading.Thread(
28
+ target=run_coroutine_call_in_thread,
29
+ name="nighthawk-sync-bridge",
30
+ )
31
+ thread.start()
32
+ thread.join()
33
+
34
+ exception = exception_container.get("exception")
35
+ if exception is not None:
36
+ raise exception.with_traceback(exception.__traceback__)
37
+
38
+ return result_container["result"]
39
+
40
+
41
+ def run_awaitable_value_synchronously(value: object) -> object:
42
+ if not inspect.isawaitable(value):
43
+ return value
44
+
45
+ typed_awaitable_value = cast(Awaitable[Any], value)
46
+
47
+ async def _await_value() -> Any:
48
+ return await typed_awaitable_value
49
+
50
+ return run_coroutine_synchronously(_await_value)