ipython-smart-await 0.1.0__tar.gz → 0.2.0__tar.gz

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.
@@ -24,7 +24,7 @@ jobs:
24
24
  fail-fast: false
25
25
  matrix:
26
26
  os: [ubuntu-latest]
27
- python-version: ["3.10", "3.11", "3.12", "3.13"]
27
+ python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
28
28
  steps:
29
29
  - uses: actions/checkout@v4
30
30
  - uses: actions/setup-python@v5
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ipython-smart-await
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Summary: Auto-await coroutine results (calls, subscripts, item-assignment) in the IPython REPL.
5
5
  Project-URL: Homepage, https://github.com/doronz88/ipython-smart-await
6
6
  Project-URL: Repository, https://github.com/doronz88/ipython-smart-await
@@ -51,6 +51,7 @@ Then, given some async API bound to `p`:
51
51
  p.get_pid() # -> awaited automatically (no `await` needed)
52
52
  a[0] # -> awaited if `a.__getitem__` returns a coroutine
53
53
  a[0] = 7 # -> routed to `a.setindex(0, 7)` (awaited) when `a` has an async `setindex`
54
+ proxy.length # -> awaited if attribute access returns a coroutine (dynamic proxies)
54
55
  ```
55
56
 
56
57
  ### Opting out
@@ -67,8 +68,8 @@ Cells that already use `await` / `async` constructs are left untouched.
67
68
 
68
69
  The extension installs an `ast` transformer (an IPython AST transformer) that wraps:
69
70
 
70
- - **calls** (`foo()`) and **subscript reads** (`a[0]`) in a helper that awaits the result only if
71
- it is a coroutine (non-coroutines pass through unchanged);
71
+ - **calls** (`foo()`), **subscript reads** (`a[0]`), and **attribute reads** (`a.b`) in a helper
72
+ that awaits the result only if it is a coroutine (non-coroutines pass through unchanged);
72
73
  - **single-target subscript assignment** (`a[0] = v`) into a call that routes to an async
73
74
  `setindex(key, value)` when the target exposes one, otherwise a normal item assignment — so
74
75
  dicts, lists, numpy arrays, etc. are unaffected.
@@ -24,6 +24,7 @@ Then, given some async API bound to `p`:
24
24
  p.get_pid() # -> awaited automatically (no `await` needed)
25
25
  a[0] # -> awaited if `a.__getitem__` returns a coroutine
26
26
  a[0] = 7 # -> routed to `a.setindex(0, 7)` (awaited) when `a` has an async `setindex`
27
+ proxy.length # -> awaited if attribute access returns a coroutine (dynamic proxies)
27
28
  ```
28
29
 
29
30
  ### Opting out
@@ -40,8 +41,8 @@ Cells that already use `await` / `async` constructs are left untouched.
40
41
 
41
42
  The extension installs an `ast` transformer (an IPython AST transformer) that wraps:
42
43
 
43
- - **calls** (`foo()`) and **subscript reads** (`a[0]`) in a helper that awaits the result only if
44
- it is a coroutine (non-coroutines pass through unchanged);
44
+ - **calls** (`foo()`), **subscript reads** (`a[0]`), and **attribute reads** (`a.b`) in a helper
45
+ that awaits the result only if it is a coroutine (non-coroutines pass through unchanged);
45
46
  - **single-target subscript assignment** (`a[0] = v`) into a call that routes to an async
46
47
  `setindex(key, value)` when the target exposes one, otherwise a normal item assignment — so
47
48
  dicts, lists, numpy arrays, etc. are unaffected.
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "ipython-smart-await"
7
- version = "0.1.0"
7
+ version = "0.2.0"
8
8
  description = "Auto-await coroutine results (calls, subscripts, item-assignment) in the IPython REPL."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -24,7 +24,7 @@ from typing import Any
24
24
  from IPython.core.interactiveshell import InteractiveShell
25
25
 
26
26
 
27
- __version__ = "0.1.0"
27
+ __version__ = "0.2.0"
28
28
  __all__ = ["load_ipython_extension"]
29
29
 
30
30
 
@@ -80,6 +80,15 @@ class _SmartAwaitTransformer(ast.NodeTransformer):
80
80
  return visited_node
81
81
  return ast.Call(func=ast.Name(_maybe_await.__name__, ctx=ast.Load()), args=[visited_node], keywords=[])
82
82
 
83
+ def visit_Attribute(self, node: ast.Attribute) -> ast.AST:
84
+ visited_node = self.generic_visit(node)
85
+ assert isinstance(visited_node, ast.expr)
86
+ # Only reads yield a (possibly awaitable) value; never wrap an assignment/deletion target.
87
+ # Supports objects that dispatch attribute access to a coroutine (e.g. dynamic proxies).
88
+ if not isinstance(node.ctx, ast.Load):
89
+ return visited_node
90
+ return ast.Call(func=ast.Name(_maybe_await.__name__, ctx=ast.Load()), args=[visited_node], keywords=[])
91
+
83
92
  def visit_Assign(self, node: ast.Assign) -> ast.AST:
84
93
  visited_node = self.generic_visit(node)
85
94
  assert isinstance(visited_node, ast.Assign)
@@ -53,6 +53,10 @@ def test_subscript_read_is_wrapped(ip: InteractiveShell) -> None:
53
53
  assert _rewrite(ip, "a[0]") == "_maybe_await(a[0])"
54
54
 
55
55
 
56
+ def test_attribute_read_is_wrapped(ip: InteractiveShell) -> None:
57
+ assert _rewrite(ip, "a.length") == "_maybe_await(a.length)"
58
+
59
+
56
60
  def test_subscript_assign_is_rewritten(ip: InteractiveShell) -> None:
57
61
  assert _rewrite(ip, "a[0] = 7") == "_setitem(a, 0, 7)"
58
62
 
@@ -91,6 +95,18 @@ def test_subscript_assign_routes_to_setindex(ip: InteractiveShell) -> None:
91
95
  assert a.store == {0: 7}
92
96
 
93
97
 
98
+ def test_attribute_auto_awaited(ip: InteractiveShell) -> None:
99
+ class Proxy:
100
+ def __getattr__(self, name):
101
+ async def _resolve():
102
+ return f"resolved:{name}"
103
+
104
+ return _resolve()
105
+
106
+ ip.user_ns["a"] = Proxy()
107
+ assert ip.run_cell("a.length").result == "resolved:length"
108
+
109
+
94
110
  def test_dict_assign_unaffected(ip: InteractiveShell) -> None:
95
111
  ip.user_ns["d"] = {}
96
112
  assert ip.run_cell("d['k'] = 5").success