ipython 9.3.0__py3-none-any.whl → 9.4.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.
IPython/core/debugger.py CHANGED
@@ -133,6 +133,7 @@ from contextlib import contextmanager
133
133
  from functools import lru_cache
134
134
 
135
135
  from IPython import get_ipython
136
+ from IPython.core.debugger_backport import PdbClosureBackport
136
137
  from IPython.utils import PyColorize
137
138
  from IPython.utils.PyColorize import TokenStream
138
139
 
@@ -140,9 +141,18 @@ from typing import TYPE_CHECKING
140
141
  from types import FrameType
141
142
 
142
143
  # We have to check this directly from sys.argv, config struct not yet available
143
- from pdb import Pdb as OldPdb
144
+ from pdb import Pdb as _OldPdb
144
145
  from pygments.token import Token
145
146
 
147
+
148
+ if sys.version_info < (3, 13):
149
+
150
+ class OldPdb(PdbClosureBackport, _OldPdb):
151
+ pass
152
+
153
+ else:
154
+ OldPdb = _OldPdb
155
+
146
156
  if TYPE_CHECKING:
147
157
  # otherwise circular import
148
158
  from IPython.core.interactiveshell import InteractiveShell
@@ -0,0 +1,206 @@
1
+ """
2
+ The code in this module is a backport of cPython changes in Pdb
3
+ that were introduced in Python 3.13 by gh-83151: Make closure work on pdb
4
+ https://github.com/python/cpython/pull/111094.
5
+ This file should be removed once IPython drops supports for Python 3.12.
6
+
7
+ The only changes are:
8
+ - reformatting by darker (black) formatter
9
+ - addition of type-ignore comments to satisfy mypy
10
+
11
+ Copyright (c) 2001 Python Software Foundation; All Rights Reserved
12
+
13
+ PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2
14
+ --------------------------------------------
15
+
16
+ 1. This LICENSE AGREEMENT is between the Python Software Foundation
17
+ ("PSF"), and the Individual or Organization ("Licensee") accessing and
18
+ otherwise using this software ("Python") in source or binary form and
19
+ its associated documentation.
20
+
21
+ 2. Subject to the terms and conditions of this License Agreement, PSF hereby
22
+ grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce,
23
+ analyze, test, perform and/or display publicly, prepare derivative works,
24
+ distribute, and otherwise use Python alone or in any derivative version,
25
+ provided, however, that PSF's License Agreement and PSF's notice of copyright,
26
+ i.e., "Copyright (c) 2001 Python Software Foundation; All Rights Reserved"
27
+ are retained in Python alone or in any derivative version prepared by Licensee.
28
+
29
+ 3. In the event Licensee prepares a derivative work that is based on
30
+ or incorporates Python or any part thereof, and wants to make
31
+ the derivative work available to others as provided herein, then
32
+ Licensee hereby agrees to include in any such work a brief summary of
33
+ the changes made to Python.
34
+
35
+ 4. PSF is making Python available to Licensee on an "AS IS"
36
+ basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR
37
+ IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND
38
+ DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS
39
+ FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT
40
+ INFRINGE ANY THIRD PARTY RIGHTS.
41
+
42
+ 5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON
43
+ FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS
44
+ A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON,
45
+ OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
46
+
47
+ 6. This License Agreement will automatically terminate upon a material
48
+ breach of its terms and conditions.
49
+
50
+ 7. Nothing in this License Agreement shall be deemed to create any
51
+ relationship of agency, partnership, or joint venture between PSF and
52
+ Licensee. This License Agreement does not grant permission to use PSF
53
+ trademarks or trade name in a trademark sense to endorse or promote
54
+ products or services of Licensee, or any third party.
55
+
56
+ 8. By copying, installing or otherwise using Python, Licensee
57
+ agrees to be bound by the terms and conditions of this License
58
+ Agreement.
59
+ """
60
+
61
+ import sys
62
+ import types
63
+ import codeop
64
+ import textwrap
65
+ from types import CodeType
66
+
67
+
68
+ class PdbClosureBackport:
69
+ def _exec_in_closure(self, source, globals, locals): # type: ignore[no-untyped-def]
70
+ """Run source code in closure so code object created within source
71
+ can find variables in locals correctly
72
+ returns True if the source is executed, False otherwise
73
+ """
74
+
75
+ # Determine if the source should be executed in closure. Only when the
76
+ # source compiled to multiple code objects, we should use this feature.
77
+ # Otherwise, we can just raise an exception and normal exec will be used.
78
+
79
+ code = compile(source, "<string>", "exec")
80
+ if not any(isinstance(const, CodeType) for const in code.co_consts):
81
+ return False
82
+
83
+ # locals could be a proxy which does not support pop
84
+ # copy it first to avoid modifying the original locals
85
+ locals_copy = dict(locals)
86
+
87
+ locals_copy["__pdb_eval__"] = {"result": None, "write_back": {}}
88
+
89
+ # If the source is an expression, we need to print its value
90
+ try:
91
+ compile(source, "<string>", "eval")
92
+ except SyntaxError:
93
+ pass
94
+ else:
95
+ source = "__pdb_eval__['result'] = " + source
96
+
97
+ # Add write-back to update the locals
98
+ source = (
99
+ "try:\n"
100
+ + textwrap.indent(source, " ")
101
+ + "\n"
102
+ + "finally:\n"
103
+ + " __pdb_eval__['write_back'] = locals()"
104
+ )
105
+
106
+ # Build a closure source code with freevars from locals like:
107
+ # def __pdb_outer():
108
+ # var = None
109
+ # def __pdb_scope(): # This is the code object we want to execute
110
+ # nonlocal var
111
+ # <source>
112
+ # return __pdb_scope.__code__
113
+ source_with_closure = (
114
+ "def __pdb_outer():\n"
115
+ + "\n".join(f" {var} = None" for var in locals_copy)
116
+ + "\n"
117
+ + " def __pdb_scope():\n"
118
+ + "\n".join(f" nonlocal {var}" for var in locals_copy)
119
+ + "\n"
120
+ + textwrap.indent(source, " ")
121
+ + "\n"
122
+ + " return __pdb_scope.__code__"
123
+ )
124
+
125
+ # Get the code object of __pdb_scope()
126
+ # The exec fills locals_copy with the __pdb_outer() function and we can call
127
+ # that to get the code object of __pdb_scope()
128
+ ns = {}
129
+ try:
130
+ exec(source_with_closure, {}, ns)
131
+ except Exception:
132
+ return False
133
+ code = ns["__pdb_outer"]()
134
+
135
+ cells = tuple(types.CellType(locals_copy.get(var)) for var in code.co_freevars)
136
+
137
+ try:
138
+ exec(code, globals, locals_copy, closure=cells)
139
+ except Exception:
140
+ return False
141
+
142
+ # get the data we need from the statement
143
+ pdb_eval = locals_copy["__pdb_eval__"]
144
+
145
+ # __pdb_eval__ should not be updated back to locals
146
+ pdb_eval["write_back"].pop("__pdb_eval__")
147
+
148
+ # Write all local variables back to locals
149
+ locals.update(pdb_eval["write_back"])
150
+ eval_result = pdb_eval["result"]
151
+ if eval_result is not None:
152
+ print(repr(eval_result))
153
+
154
+ return True
155
+
156
+ def default(self, line): # type: ignore[no-untyped-def]
157
+ if line[:1] == "!":
158
+ line = line[1:].strip()
159
+ locals = self.curframe_locals
160
+ globals = self.curframe.f_globals
161
+ try:
162
+ buffer = line
163
+ if (
164
+ code := codeop.compile_command(line + "\n", "<stdin>", "single")
165
+ ) is None:
166
+ # Multi-line mode
167
+ with self._disable_command_completion():
168
+ buffer = line
169
+ continue_prompt = "... "
170
+ while (
171
+ code := codeop.compile_command(buffer, "<stdin>", "single")
172
+ ) is None:
173
+ if self.use_rawinput:
174
+ try:
175
+ line = input(continue_prompt)
176
+ except (EOFError, KeyboardInterrupt):
177
+ self.lastcmd = ""
178
+ print("\n")
179
+ return
180
+ else:
181
+ self.stdout.write(continue_prompt)
182
+ self.stdout.flush()
183
+ line = self.stdin.readline()
184
+ if not len(line):
185
+ self.lastcmd = ""
186
+ self.stdout.write("\n")
187
+ self.stdout.flush()
188
+ return
189
+ else:
190
+ line = line.rstrip("\r\n")
191
+ buffer += "\n" + line
192
+ save_stdout = sys.stdout
193
+ save_stdin = sys.stdin
194
+ save_displayhook = sys.displayhook
195
+ try:
196
+ sys.stdin = self.stdin
197
+ sys.stdout = self.stdout
198
+ sys.displayhook = self.displayhook
199
+ if not self._exec_in_closure(buffer, globals, locals):
200
+ exec(code, globals, locals)
201
+ finally:
202
+ sys.stdout = save_stdout
203
+ sys.stdin = save_stdin
204
+ sys.displayhook = save_displayhook
205
+ except:
206
+ self._error_exc()
@@ -252,13 +252,23 @@ class ExecutionInfo:
252
252
  Stores information about what is going to happen.
253
253
  """
254
254
  raw_cell = None
255
+ transformed_cell = None
255
256
  store_history = False
256
257
  silent = False
257
258
  shell_futures = True
258
259
  cell_id = None
259
260
 
260
- def __init__(self, raw_cell, store_history, silent, shell_futures, cell_id):
261
+ def __init__(
262
+ self,
263
+ raw_cell,
264
+ store_history,
265
+ silent,
266
+ shell_futures,
267
+ cell_id,
268
+ transformed_cell=None,
269
+ ):
261
270
  self.raw_cell = raw_cell
271
+ self.transformed_cell = transformed_cell
262
272
  self.store_history = store_history
263
273
  self.silent = silent
264
274
  self.shell_futures = shell_futures
@@ -269,12 +279,18 @@ class ExecutionInfo:
269
279
  raw_cell = (
270
280
  (self.raw_cell[:50] + "..") if len(self.raw_cell) > 50 else self.raw_cell
271
281
  )
282
+ transformed_cell = (
283
+ (self.transformed_cell[:50] + "..")
284
+ if self.transformed_cell and len(self.transformed_cell) > 50
285
+ else self.transformed_cell
286
+ )
272
287
  return (
273
- '<%s object at %x, raw_cell="%s" store_history=%s silent=%s shell_futures=%s cell_id=%s>'
288
+ '<%s object at %x, raw_cell="%s" transformed_cell="%s" store_history=%s silent=%s shell_futures=%s cell_id=%s>'
274
289
  % (
275
290
  name,
276
291
  id(self),
277
292
  raw_cell,
293
+ transformed_cell,
278
294
  self.store_history,
279
295
  self.silent,
280
296
  self.shell_futures,
@@ -3156,14 +3172,18 @@ class InteractiveShell(SingletonConfigurable):
3156
3172
  except BaseException as e:
3157
3173
  try:
3158
3174
  info = ExecutionInfo(
3159
- raw_cell, store_history, silent, shell_futures, cell_id
3175
+ raw_cell,
3176
+ store_history,
3177
+ silent,
3178
+ shell_futures,
3179
+ cell_id,
3180
+ transformed_cell=transformed_cell,
3160
3181
  )
3161
3182
  result = ExecutionResult(info)
3162
3183
  result.error_in_exec = e
3163
3184
  self.showtraceback(running_compiled_code=True)
3164
3185
  except:
3165
3186
  pass
3166
-
3167
3187
  return result
3168
3188
 
3169
3189
  def should_run_async(
@@ -3248,7 +3268,14 @@ class InteractiveShell(SingletonConfigurable):
3248
3268
 
3249
3269
  .. versionadded:: 7.0
3250
3270
  """
3251
- info = ExecutionInfo(raw_cell, store_history, silent, shell_futures, cell_id)
3271
+ info = ExecutionInfo(
3272
+ raw_cell,
3273
+ store_history,
3274
+ silent,
3275
+ shell_futures,
3276
+ cell_id,
3277
+ transformed_cell=transformed_cell,
3278
+ )
3252
3279
  result = ExecutionResult(info)
3253
3280
 
3254
3281
  if (not raw_cell) or raw_cell.isspace():
@@ -20,6 +20,7 @@ import shlex
20
20
  import sys
21
21
  import time
22
22
  import timeit
23
+ import signal
23
24
  from typing import Dict, Any
24
25
  from ast import (
25
26
  Assign,
@@ -1257,8 +1258,15 @@ class ExecutionMagics(Magics):
1257
1258
  if return_result:
1258
1259
  return timeit_result
1259
1260
 
1260
- @skip_doctest
1261
1261
  @no_var_expand
1262
+ @magic_arguments.magic_arguments()
1263
+ @magic_arguments.argument(
1264
+ "--no-raise-error",
1265
+ action="store_true",
1266
+ dest="no_raise_error",
1267
+ help="If given, don't re-raise exceptions",
1268
+ )
1269
+ @skip_doctest
1262
1270
  @needs_local_scope
1263
1271
  @line_cell_magic
1264
1272
  @output_can_be_silenced
@@ -1329,9 +1337,34 @@ class ExecutionMagics(Magics):
1329
1337
  Wall time: 0.00 s
1330
1338
  Compiler : 0.78 s
1331
1339
  """
1332
- # fail immediately if the given expression can't be compiled
1340
+ line_present = False
1341
+ # Try to parse --no-raise-error if present, else ignore unrecognized args
1342
+ try:
1343
+ args = magic_arguments.parse_argstring(self.time, line)
1344
+ except UsageError as e:
1345
+ # Only ignore UsageError if caused by unrecognized arguments
1346
+ # We'll manually check for --no-raise-error and remove it from line
1347
+ line_present = True
1348
+
1349
+ # Check if --no-raise-error is present
1350
+ no_raise_error = "--no-raise-error" in line
1351
+
1352
+ if no_raise_error:
1353
+ # Remove --no-raise-error while preserving the rest of the line structure
1354
+ line = re.sub(r"\s*--no-raise-error\s*", " ", line).strip()
1355
+ # Clean up any double spaces
1356
+ line = re.sub(r"\s+", " ", line)
1357
+
1358
+ class Args:
1359
+ def __init__(self, no_raise_error):
1360
+ self.no_raise_error = no_raise_error
1361
+
1362
+ args = Args(no_raise_error)
1363
+ else:
1364
+ if not hasattr(args, "no_raise_error"):
1365
+ args.no_raise_error = False
1333
1366
 
1334
- if line and cell:
1367
+ if line_present and cell:
1335
1368
  raise UsageError("Can't use statement directly after '%%time'!")
1336
1369
 
1337
1370
  if cell:
@@ -1376,13 +1409,24 @@ class ExecutionMagics(Magics):
1376
1409
  wtime = time.time
1377
1410
  # time execution
1378
1411
  wall_st = wtime()
1412
+ # Track whether to propagate exceptions or exit
1413
+ exit_on_interrupt = False
1414
+ interrupt_occured = False
1415
+ captured_exception = None
1416
+
1379
1417
  if mode == "eval":
1380
1418
  st = clock2()
1381
1419
  try:
1382
1420
  out = eval(code, glob, local_ns)
1383
- except Exception:
1384
- self.shell.showtraceback()
1385
- return
1421
+ except KeyboardInterrupt as e:
1422
+ captured_exception = e
1423
+ interrupt_occured = True
1424
+ exit_on_interrupt = True
1425
+ except Exception as e:
1426
+ captured_exception = e
1427
+ interrupt_occured = True
1428
+ if not args.no_raise_error:
1429
+ exit_on_interrupt = True
1386
1430
  end = clock2()
1387
1431
  else:
1388
1432
  st = clock2()
@@ -1393,11 +1437,16 @@ class ExecutionMagics(Magics):
1393
1437
  if expr_val is not None:
1394
1438
  code_2 = self.shell.compile(expr_val, source, 'eval')
1395
1439
  out = eval(code_2, glob, local_ns)
1396
- except Exception:
1397
- self.shell.showtraceback()
1398
- return
1440
+ except KeyboardInterrupt as e:
1441
+ captured_exception = e
1442
+ interrupt_occured = True
1443
+ exit_on_interrupt = True
1444
+ except Exception as e:
1445
+ captured_exception = e
1446
+ interrupt_occured = True
1447
+ if not args.no_raise_error:
1448
+ exit_on_interrupt = True
1399
1449
  end = clock2()
1400
-
1401
1450
  wall_end = wtime()
1402
1451
  # Compute actual times and report
1403
1452
  wall_time = wall_end - wall_st
@@ -1416,6 +1465,10 @@ class ExecutionMagics(Magics):
1416
1465
  print(f"Compiler : {_format_time(tc)}")
1417
1466
  if tp > tp_min:
1418
1467
  print(f"Parser : {_format_time(tp)}")
1468
+ if interrupt_occured:
1469
+ if exit_on_interrupt and captured_exception:
1470
+ raise captured_exception
1471
+ return
1419
1472
  return out
1420
1473
 
1421
1474
  @skip_doctest
@@ -1514,7 +1567,7 @@ class ExecutionMagics(Magics):
1514
1567
  default="",
1515
1568
  nargs="?",
1516
1569
  help="""
1517
-
1570
+
1518
1571
  The name of the variable in which to store output.
1519
1572
  This is a ``utils.io.CapturedIO`` object with stdout/err attributes
1520
1573
  for the text of the captured output.
@@ -361,6 +361,9 @@ class NamespaceMagics(Magics):
361
361
  - For numpy arrays, a summary with shape, number of
362
362
  elements, typecode and size in memory.
363
363
 
364
+ - For objects that have shape attribute, primarily dataframe and series like
365
+ objects, will print the shape.
366
+
364
367
  - Everything else: a string representation, snipping their middle if
365
368
  too long.
366
369
 
@@ -372,11 +375,17 @@ class NamespaceMagics(Magics):
372
375
 
373
376
  In [2]: beta = 'test'
374
377
 
375
- In [3]: %whos
378
+ In [3]: df = pd.DataFrame({"a": range(10), "b": range(10,20)})
379
+
380
+ In [4]: s = df["a"]
381
+
382
+ In [5]: %whos
376
383
  Variable Type Data/Info
377
384
  --------------------------------
378
385
  alpha int 123
379
386
  beta str test
387
+ df DataFrame Shape: (10, 2)
388
+ s Series Shape: (10, )
380
389
  """
381
390
 
382
391
  varnames = self.who_ls(parameter_s)
@@ -441,9 +450,7 @@ class NamespaceMagics(Magics):
441
450
  Mb = 1048576 # kb**2
442
451
  for vname,var,vtype in zip(varnames,varlist,typelist):
443
452
  print(vformat.format(vname, vtype, varwidth=varwidth, typewidth=typewidth), end=' ')
444
- if vtype in seq_types:
445
- print("n="+str(len(var)))
446
- elif vtype == ndarray_type:
453
+ if vtype == ndarray_type:
447
454
  vshape = str(var.shape).replace(',','').replace(' ','x')[1:-1]
448
455
  if vtype==ndarray_type:
449
456
  # numpy
@@ -458,7 +465,14 @@ class NamespaceMagics(Magics):
458
465
  if vbytes < Mb:
459
466
  print('(%s kb)' % (vbytes/kb,))
460
467
  else:
461
- print('(%s Mb)' % (vbytes/Mb,))
468
+ print("(%s Mb)" % (vbytes / Mb,))
469
+ elif hasattr(var, "shape"):
470
+ # Useful for DataFrames and Series
471
+ # Ought to work for both pandas and polars
472
+ print(f"Shape: {var.shape}")
473
+ elif hasattr(var, "__len__"):
474
+ ## types that can be used in len function
475
+ print(var if isinstance(var, str) else f"n={len(var)}")
462
476
  else:
463
477
  try:
464
478
  vstr = str(var)
@@ -11,6 +11,7 @@ import os
11
11
  import signal
12
12
  import sys
13
13
  import time
14
+ from codecs import getincrementaldecoder
14
15
  from subprocess import CalledProcessError
15
16
  from threading import Thread
16
17
 
@@ -227,15 +228,21 @@ class ScriptMagics(Magics):
227
228
  return await stream.read(e.consumed)
228
229
 
229
230
  async def _handle_stream(stream, stream_arg, file_object):
231
+ should_break = False
232
+ decoder = getincrementaldecoder("utf-8")(errors="replace")
230
233
  while True:
231
- chunk = (await _readchunk(stream)).decode("utf8", errors="replace")
234
+ chunk = decoder.decode(await _readchunk(stream))
232
235
  if not chunk:
233
236
  break
237
+ chunk = decoder.decode("", final=True)
238
+ should_break = True
234
239
  if stream_arg:
235
240
  self.shell.user_ns[stream_arg] = chunk
236
241
  else:
237
242
  file_object.write(chunk)
238
243
  file_object.flush()
244
+ if should_break:
245
+ break
239
246
 
240
247
  async def _stream_communicate(process, cell):
241
248
  process.stdin.write(cell)
IPython/core/release.py CHANGED
@@ -16,7 +16,7 @@
16
16
  # release. 'dev' as a _version_extra string means this is a development
17
17
  # version
18
18
  _version_major = 9
19
- _version_minor = 3
19
+ _version_minor = 4
20
20
  _version_patch = 0
21
21
  _version_extra = ".dev"
22
22
  # _version_extra = "b2"
@@ -73,6 +73,30 @@ The following magic commands are provided:
73
73
 
74
74
  Mark module 'foo' to not be autoreloaded.
75
75
 
76
+ Import Conflict Resolution
77
+ ==========================
78
+
79
+ In ``%autoreload 3`` mode, the extension tracks ``from X import Y`` style imports
80
+ and intelligently resolves conflicts when the same name is imported multiple ways.
81
+
82
+ Import tracking occurs after successful code execution, ensuring that only valid
83
+ imports are tracked. This approach handles edge cases such as:
84
+
85
+ - Importing a name that doesn't initially exist in a module, then adding that name
86
+ to the module and importing it again
87
+ - Conflicts between aliased imports (``from X import Y as Z``) and direct imports
88
+ (``from X import Z``)
89
+
90
+ When conflicts occur:
91
+
92
+ - If you first do ``from X import Y as Z`` then later ``from X import Z``,
93
+ the extension will switch to reloading ``Z`` instead of ``Y`` under the name ``Z``.
94
+
95
+ - Similarly, if you first do ``from X import Z`` then later ``from X import Y as Z``,
96
+ the extension will switch to reloading ``Y`` as ``Z`` instead of the original ``Z``.
97
+
98
+ - The most recent successful import always takes precedence in conflict resolution.
99
+
76
100
  Caveats
77
101
  =======
78
102
 
@@ -127,6 +151,7 @@ __skip_doctest__ = True
127
151
  # Imports
128
152
  # -----------------------------------------------------------------------------
129
153
 
154
+ import ast
130
155
  import os
131
156
  import sys
132
157
  import traceback
@@ -171,6 +196,9 @@ class ModuleReloader:
171
196
  # Deduper reloader
172
197
  self.deduper_reloader = DeduperReloader()
173
198
 
199
+ # Persistent import tracker for from-imports
200
+ self.import_from_tracker = ImportFromTracker({}, {})
201
+
174
202
  # Cache module modification times
175
203
  self.check(check_all=True, do_reload=False)
176
204
 
@@ -193,6 +221,10 @@ class ModuleReloader:
193
221
  pass
194
222
  self.modules[module_name] = True
195
223
 
224
+ def clear_import_tracker(self):
225
+ """Clear the persistent import tracker state"""
226
+ self.import_from_tracker = ImportFromTracker({}, {})
227
+
196
228
  def aimport_module(self, module_name):
197
229
  """Import a module, and mark it reloadable
198
230
 
@@ -237,7 +269,7 @@ class ModuleReloader:
237
269
 
238
270
  return py_filename, pymtime
239
271
 
240
- def check(self, check_all=False, do_reload=True):
272
+ def check(self, check_all=False, do_reload=True, execution_info=None):
241
273
  """Check whether some modules need to be reloaded."""
242
274
 
243
275
  if not self.enabled and not check_all:
@@ -248,6 +280,10 @@ class ModuleReloader:
248
280
  else:
249
281
  modules = list(self.modules.keys())
250
282
 
283
+ # Use the persistent import_from_tracker
284
+ import_from_tracker = (
285
+ self.import_from_tracker if self.import_from_tracker.imports_froms else None
286
+ )
251
287
  for modname in modules:
252
288
  m = sys.modules.get(modname, None)
253
289
 
@@ -275,7 +311,13 @@ class ModuleReloader:
275
311
  self._report(f"Reloading '{modname}'.")
276
312
  try:
277
313
  if self.autoload_obj:
278
- superreload(m, reload, self.old_objects, self.shell)
314
+ superreload(
315
+ m,
316
+ reload,
317
+ self.old_objects,
318
+ self.shell,
319
+ import_from_tracker=import_from_tracker,
320
+ )
279
321
  # if not using autoload, check if deduperreload is viable for this module
280
322
  elif self.deduper_reloader.maybe_reload_module(m):
281
323
  pass
@@ -427,6 +469,59 @@ mod_attrs = [
427
469
  ]
428
470
 
429
471
 
472
+ class ImportFromTracker:
473
+ def __init__(self, imports_froms: dict, symbol_map: dict):
474
+ self.imports_froms = imports_froms
475
+ # symbol_map maps original_name -> list of resolved_names
476
+ self.symbol_map = {}
477
+ if symbol_map:
478
+ for module_name, mappings in symbol_map.items():
479
+ self.symbol_map[module_name] = {}
480
+ for original_name, resolved_names in mappings.items():
481
+ if isinstance(resolved_names, list):
482
+ self.symbol_map[module_name][original_name] = resolved_names[:]
483
+ else:
484
+ self.symbol_map[module_name][original_name] = [resolved_names]
485
+ else:
486
+ self.symbol_map = symbol_map or {}
487
+
488
+ def add_import(
489
+ self, module_name: str, original_name: str, resolved_name: str
490
+ ) -> None:
491
+ """Add an import, handling conflicts with existing imports.
492
+
493
+ This method is called after successful code execution, so we know the import is valid.
494
+ """
495
+ if module_name not in self.imports_froms:
496
+ self.imports_froms[module_name] = []
497
+ if module_name not in self.symbol_map:
498
+ self.symbol_map[module_name] = {}
499
+
500
+ # Check if there's already a different mapping for the same resolved_name from a different original_name
501
+ # We need to remove any conflicting mappings
502
+ for orig_name, res_names in list(self.symbol_map[module_name].items()):
503
+ if resolved_name in res_names and orig_name != original_name:
504
+ # Remove the conflicting resolved_name from the other original_name's list
505
+ res_names.remove(resolved_name)
506
+ if (
507
+ not res_names
508
+ ): # If the list is now empty, remove the original_name entirely
509
+ if orig_name in self.imports_froms[module_name]:
510
+ self.imports_froms[module_name].remove(orig_name)
511
+ del self.symbol_map[module_name][orig_name]
512
+
513
+ # Add the new mapping
514
+ if original_name not in self.imports_froms[module_name]:
515
+ self.imports_froms[module_name].append(original_name)
516
+
517
+ if original_name not in self.symbol_map[module_name]:
518
+ self.symbol_map[module_name][original_name] = []
519
+
520
+ # Add the resolved_name if it's not already in the list
521
+ if resolved_name not in self.symbol_map[module_name][original_name]:
522
+ self.symbol_map[module_name][original_name].append(resolved_name)
523
+
524
+
430
525
  def append_obj(module, d, name, obj, autoload=False):
431
526
  in_module = hasattr(obj, "__module__") and obj.__module__ == module.__name__
432
527
  if autoload:
@@ -445,7 +540,9 @@ def append_obj(module, d, name, obj, autoload=False):
445
540
  return True
446
541
 
447
542
 
448
- def superreload(module, reload=reload, old_objects=None, shell=None):
543
+ def superreload(
544
+ module, reload=reload, old_objects=None, shell=None, import_from_tracker=None
545
+ ):
449
546
  """Enhanced version of the builtin reload function.
450
547
 
451
548
  superreload remembers objects previously in the module, and
@@ -486,18 +583,34 @@ def superreload(module, reload=reload, old_objects=None, shell=None):
486
583
  module.__dict__.update(old_dict)
487
584
  raise
488
585
 
489
- # iterate over all objects and update functions & classes
490
586
  for name, new_obj in list(module.__dict__.items()):
491
587
  key = (module.__name__, name)
492
588
  if key not in old_objects:
493
589
  # here 'shell' acts both as a flag and as an output var
590
+ imports_froms = (
591
+ import_from_tracker.imports_froms if import_from_tracker else None
592
+ )
593
+ symbol_map = import_from_tracker.symbol_map if import_from_tracker else None
494
594
  if (
495
595
  shell is None
496
596
  or name == "Enum"
497
597
  or not append_obj(module, old_objects, name, new_obj, True)
598
+ or (
599
+ imports_froms
600
+ and module.__name__ in imports_froms
601
+ and "*" not in imports_froms[module.__name__]
602
+ and name not in imports_froms[module.__name__]
603
+ )
498
604
  ):
499
605
  continue
500
- shell.user_ns[name] = new_obj
606
+
607
+ # Handle symbol mapping - now supporting multiple resolved names per original name
608
+ if symbol_map and name in symbol_map.get(module.__name__, {}):
609
+ resolved_names = symbol_map.get(module.__name__, {})[name]
610
+ for resolved_name in resolved_names:
611
+ shell.user_ns[resolved_name] = new_obj
612
+ else:
613
+ shell.user_ns[name] = new_obj
501
614
 
502
615
  new_refs = []
503
616
  for old_ref in old_objects[key]:
@@ -727,6 +840,9 @@ class AutoreloadMagics(Magics):
727
840
  self.shell.push({top_name: top_module})
728
841
 
729
842
  def pre_run_cell(self, info):
843
+ # Store the execution info for later use in post_execute_hook
844
+ self._last_execution_info = info
845
+
730
846
  if self._reloader.enabled:
731
847
  try:
732
848
  self._reloader.check()
@@ -734,7 +850,20 @@ class AutoreloadMagics(Magics):
734
850
  pass
735
851
 
736
852
  def post_execute_hook(self):
737
- """Cache the modification times of any modules imported in this execution"""
853
+ """Cache the modification times of any modules imported in this execution and track imports"""
854
+
855
+ # Track imports from the recently executed code if autoreload 3 is enabled
856
+ if self._reloader.enabled and self._reloader.autoload_obj:
857
+ # Use the stored execution info
858
+ if (
859
+ hasattr(self, "_last_execution_info")
860
+ and self._last_execution_info
861
+ and self._last_execution_info.transformed_cell
862
+ ):
863
+ self._track_imports_from_code(
864
+ self._last_execution_info.transformed_cell
865
+ )
866
+
738
867
  newly_loaded_modules = set(sys.modules) - self.loaded_modules
739
868
  for modname in newly_loaded_modules:
740
869
  _, pymtime = self._reloader.filename_and_mtime(sys.modules[modname])
@@ -743,6 +872,36 @@ class AutoreloadMagics(Magics):
743
872
 
744
873
  self.loaded_modules.update(newly_loaded_modules)
745
874
 
875
+ def _track_imports_from_code(self, code: str) -> None:
876
+ """Track import statements from executed code"""
877
+ try:
878
+ tree = ast.parse(code)
879
+
880
+ for node in ast.walk(tree):
881
+ # Handle "from X import Y" style imports
882
+ if isinstance(node, ast.ImportFrom):
883
+ mod = node.module
884
+
885
+ # Skip relative imports that don't have a module name
886
+ if mod is None:
887
+ continue
888
+
889
+ for name in node.names:
890
+ # name.name is going to be actual name that we want to import from module
891
+ # name.asname is Z in the case of from X import Y as Z
892
+ # we should update Z in the shell in this situation, so track it too.
893
+ original_name = name.name
894
+ resolved_name = name.asname if name.asname else name.name
895
+
896
+ # Since the code executed successfully, we know this import is valid
897
+ self._reloader.import_from_tracker.add_import(
898
+ mod, original_name, resolved_name
899
+ )
900
+ except (SyntaxError, ValueError):
901
+ # If there's a syntax error, skip import tracking
902
+ # (though this shouldn't happen since the code already executed successfully)
903
+ pass
904
+
746
905
 
747
906
  def load_ipython_extension(ip):
748
907
  """Load the extension in IPython."""
@@ -432,16 +432,7 @@ class DeduperReloader(DeduperReloaderPatchingMixin):
432
432
  func_code = "class __autoreload_class__:\n" + textwrap.indent(
433
433
  func_code, " "
434
434
  )
435
- global_env = namespace_to_check.__dict__
436
- if hasattr(to_patch_to, "__globals__"):
437
- global_env = to_patch_to.__globals__
438
- elif isinstance(to_patch_to, property):
439
- if to_patch_to.fget is not None:
440
- global_env = to_patch_to.fget.__globals__
441
- elif to_patch_to.fset is not None:
442
- global_env = to_patch_to.fset.__globals__
443
- elif to_patch_to.fdel is not None:
444
- global_env = to_patch_to.fdel.__globals__
435
+ global_env = ns.__dict__
445
436
  if not isinstance(global_env, dict):
446
437
  global_env = dict(global_env)
447
438
  exec(func_code, global_env, local_env) # type: ignore[arg-type]
IPython/utils/_sysinfo.py CHANGED
@@ -1,2 +1,2 @@
1
1
  # GENERATED BY setup.py
2
- commit = "e87cb2a40"
2
+ commit = "67f7d8b8b"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ipython
3
- Version: 9.3.0
3
+ Version: 9.4.0
4
4
  Summary: IPython: Productive Interactive Computing
5
5
  Author: The IPython Development Team
6
6
  Author-email: ipython-dev@python.org
@@ -13,7 +13,8 @@ IPython/core/compilerop.py,sha256=tA8xHh10gp85brI2OYmvl7kW0TgDghdKbzmZE7nS4sw,69
13
13
  IPython/core/completer.py,sha256=xczIOWv2i6fKjt2neeSLCeMvBMPIV-OQbE7ggBQB_PU,129815
14
14
  IPython/core/completerlib.py,sha256=C_1uFwR4eiqIsemMRbluMQV1WJ3qSfnGxO01PsGSpr8,12641
15
15
  IPython/core/crashhandler.py,sha256=8-kyI6aNkqbaB_lBlbNKAOFv34HDBCpLggMfiu4oIDg,8747
16
- IPython/core/debugger.py,sha256=I3LA4HQFEP5cy-kbbRvJqlLlSgVHSWBbp0cbPs3nU8M,41806
16
+ IPython/core/debugger.py,sha256=ZVvFEIby6pBAd1Cv5MfZ8EX3lY3il-v2eT2Z4JwubD8,41991
17
+ IPython/core/debugger_backport.py,sha256=qP96tVbfH7wpOFIjhuFyFVVM1zIgCGbP1AyQzOBfw28,8216
17
18
  IPython/core/display.py,sha256=wQgVFY_U1O-a-jJSLb00nJ9m2w9-NsBhEBGnQUcWUd0,41129
18
19
  IPython/core/display_functions.py,sha256=hlj1gXXrcIQU_ita03dHFesltOCViP1N3RcoLtLuyFI,12407
19
20
  IPython/core/display_trap.py,sha256=r9AeMqllLicJvY8JfrGTQMkyxz37QT7X_RwwXxNk7R0,2185
@@ -30,7 +31,7 @@ IPython/core/history.py,sha256=5hmGOe1H93tCtWj_d8HdFixDCr0kdEqahGJ-zUo03RQ,41183
30
31
  IPython/core/historyapp.py,sha256=5H38INsWXRacscKz_5PQHYrEnHEayDtc1D1qcSyHRBU,5847
31
32
  IPython/core/hooks.py,sha256=xBWTZqycxZi97yj01IFc-SoJBzV5B73IoDHbAAlKUpQ,5193
32
33
  IPython/core/inputtransformer2.py,sha256=7sRleytrcAbp5PZMOrDw59MjUAGXg5BbaRbPkSW83-I,28909
33
- IPython/core/interactiveshell.py,sha256=nFDmQXdbYO85nExVC_OhCwyvu1mtImI7RNsWxSNo7dw,159075
34
+ IPython/core/interactiveshell.py,sha256=LYa9QAz74ljvILU-UIymV-VsXZBczrDegSftUZ24-pM,159746
34
35
  IPython/core/latex_symbols.py,sha256=DzFecvqWVSsdN7vWAsp0mlYAHRDQKfZGAmvuDUh0M-s,30127
35
36
  IPython/core/logger.py,sha256=Iwe4xKMmxEdvSwHYPMfsTWkmdaqVCgvZT3R3I3qTmrU,8436
36
37
  IPython/core/macro.py,sha256=OhvXWNhLe393rI2wTpMgbUVHWSnmC_ycHiYqzqSHXZU,1726
@@ -44,7 +45,7 @@ IPython/core/prefilter.py,sha256=JHQ3feaD4bhoBDqZcEgmlDjQ2sfRXC1DNjgJhpaMU7E,257
44
45
  IPython/core/profileapp.py,sha256=bFMFIyehxeF9pDUtxw_6D3b0nxeqsupKTe7XhH7GMkc,10711
45
46
  IPython/core/profiledir.py,sha256=-vjOa1I_UajMZJblJRYXh16Y0RaAUn5a2swQBsw2qEU,8459
46
47
  IPython/core/pylabtools.py,sha256=LfNV9xCJ3flCfJXmv1NaCRYj9jZDtHAQ5oSEHWo3Gmg,17376
47
- IPython/core/release.py,sha256=7T2aiCX5S_TTkk9TYSdLgJR90mt0IAtGO8eUJ6DNNNE,1505
48
+ IPython/core/release.py,sha256=ecwU1SOd6rB87PaUvxEoHsxQCLFEAtebo2_pvVgvA0A,1505
48
49
  IPython/core/shellapp.py,sha256=oZIzj_sqIXrN3qyyhinZ1gLXvFviKYHkmS4H3wVEb74,19307
49
50
  IPython/core/splitinput.py,sha256=bAX1puQjvYB-otJyqiqeOhWj6dooWuQeNVx2YdaKQs8,5006
50
51
  IPython/core/tbtools.py,sha256=X4iB5zKAT2y4TK1R9l3d3kiW5htrzKn3qxalFFe2xzI,16880
@@ -58,21 +59,21 @@ IPython/core/magics/basic.py,sha256=uFkd-gTzlSVkNDSu8Rg4fbS_IK2raEhx1SYA6kWZ4hg,
58
59
  IPython/core/magics/code.py,sha256=h_dho9niPvtf_IpoOZf5GAD6CYbT0EQGsfLfutyX-7I,28144
59
60
  IPython/core/magics/config.py,sha256=QBL5uY7m-Q7C46mO3q1Yio9s73w1TnI9y__j5E-j44Y,4881
60
61
  IPython/core/magics/display.py,sha256=STRq66GlZwcvFyBxbkqslclpP_s9LnqD0ew9Z3S4-Jo,3130
61
- IPython/core/magics/execution.py,sha256=4DAQBezVAwtKiHaCvX14cKsfzhJF2xh8IFKRkC4XrUo,62408
62
+ IPython/core/magics/execution.py,sha256=YKsyU9cUxAyAquyWVzw5iNH7h-OY-UYHRm4eJoz5XKI,64512
62
63
  IPython/core/magics/extension.py,sha256=Jj6OlkM71PS0j1HfEMDc-jU2Exwo9Ff_K0nD7e_W4N0,2477
63
64
  IPython/core/magics/history.py,sha256=Aw9gBzK4AJbe-gvRdMW7n-_zxxHuMyHvHJtRDuCwwug,12629
64
65
  IPython/core/magics/logging.py,sha256=VuDiF5QZrgzTT7Lr1NkkMCtUM1EHoGCw2pYlKsSQc4Q,6867
65
- IPython/core/magics/namespace.py,sha256=2jAK2L7_NH58Ibk_YnHoQI1IYIrto9SZTMfmps0e1mo,24825
66
+ IPython/core/magics/namespace.py,sha256=uqWM77PbbpOYvQOYMqCwJJrnJwXVIw4abju9Q7cxas0,25446
66
67
  IPython/core/magics/osm.py,sha256=mNlHBS8ZEMf687nDpLGYKj6aC-i_r7d9iUTFKw6WtY4,30695
67
68
  IPython/core/magics/packaging.py,sha256=5m2pt1bkosMdiQxB46hDdD4KisfPJpNnebCqMnygodE,6030
68
69
  IPython/core/magics/pylab.py,sha256=BBRczZ0khoZB5NPipgvVtOWYpdkLatB9mQFYzyg-vpg,6624
69
- IPython/core/magics/script.py,sha256=o0kBs9SpbR0MLOmVMh5e3r7gTFuH5U4yKQ37yBadtaI,13286
70
+ IPython/core/magics/script.py,sha256=zv-BwLsqAeVsgwCEnO3mr4HoSM6aFz6eaH_KPmM2uJM,13570
70
71
  IPython/core/profile/README_STARTUP,sha256=Y47GtYQkWy6pdzkqylUNL6eBSZMUIRGwTxXPUF_BBBc,371
71
72
  IPython/extensions/__init__.py,sha256=V4vSllFd18CVUvDTbXLYqfwTFmPXUiQZGCs5LX2IliE,78
72
- IPython/extensions/autoreload.py,sha256=_qU6PNtAm93J2UqtD3TMA68rj9wUWJPXfqb_VidrOCM,24571
73
+ IPython/extensions/autoreload.py,sha256=XnNxSE8d4cxvBGyvz2YJwySRsIOZe6WXRT9TUPWBd88,31724
73
74
  IPython/extensions/storemagic.py,sha256=ths8PLtGmYZAYaibRuS1QetLm1Xu1soyGwehBjayByg,8168
74
75
  IPython/extensions/deduperreload/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
75
- IPython/extensions/deduperreload/deduperreload.py,sha256=jh4sa6b9w2zO4PQRUtPwAFtVUlSJwPKRAQg4q_csh9s,24680
76
+ IPython/extensions/deduperreload/deduperreload.py,sha256=5BWnzqfoScQ3a1RTN3wZpxKGXVU_Oxo7P4rQoJfRrI0,24134
76
77
  IPython/extensions/deduperreload/deduperreload_patching.py,sha256=xOaws3UV5KmaAK8yZ3qtQO45w_5Ntkt_qZTcZ1psszY,4934
77
78
  IPython/external/__init__.py,sha256=-EQHbuUnBe1RS1_CwaLGzNSZQsCJsrxHW_r15smvVW0,126
78
79
  IPython/external/pickleshare.py,sha256=Zs0Hq8IXbf51RNFCr1AFxZKtlhfPJMM3b1WD4sNB670,9974
@@ -142,7 +143,7 @@ IPython/utils/_process_emscripten.py,sha256=lGLQb2IgmanNtb502KflfuKIhgOF119Ji3cw
142
143
  IPython/utils/_process_posix.py,sha256=aOEtguhS3vdWngBpws1XQURO8Ozqd5gRiCk9VLky6tA,7502
143
144
  IPython/utils/_process_win32.py,sha256=Pcf6ZiqMbqDT79edzegE_AX3D367UtE8bbhT41no54A,6775
144
145
  IPython/utils/_process_win32_controller.py,sha256=hi2eR7mLbl3TTMCVbgps85GppxdtYbhOYK_l13WvYaM,21343
145
- IPython/utils/_sysinfo.py,sha256=08M3HsNfrqEo6YkNSl9dN69UHyrECgtABfkCQUrexqk,45
146
+ IPython/utils/_sysinfo.py,sha256=dBl2xAoPkJnjAen7HGklkADaXbyQjQc9sk00mQOwoCA,45
146
147
  IPython/utils/capture.py,sha256=h5yL5Lxq8bgO1SFpoNDYjEi6mh1IW_2X9CE7vOsUxE4,5137
147
148
  IPython/utils/coloransi.py,sha256=CML-SkzLa7oaIK1qypb3uwcfPXDeKHxZQiMJ0IWvUY0,293
148
149
  IPython/utils/contexts.py,sha256=w5_uXc0WTU3KKV1kcCW9A0_Mz5mGRoeGWMq_P_eo-Dg,1610
@@ -174,11 +175,11 @@ IPython/utils/text.py,sha256=6s-y4KvDmnJxLs0urf5D-1auZGSnj2xmXgQ-9jVu0N8,18788
174
175
  IPython/utils/timing.py,sha256=nND-ZUBkHWfYevvbRG-YfOSIFczz_epzMqWK5PH6nqA,4275
175
176
  IPython/utils/tokenutil.py,sha256=x6KQ6ZCGOY7j5GQcr7byJRZSBFgyBcfkTiLtjxkl9f8,6552
176
177
  IPython/utils/wildcard.py,sha256=6EEc3OEYp-IuSoidL6nwpaHg--GxnzbAJTmFiz77CNE,4612
177
- ipython-9.3.0.data/data/share/man/man1/ipython.1,sha256=PVdQP2hHmHyUEwzLOPcgavnCe9jTDVrM1jKZt4cnF_Q,2058
178
- ipython-9.3.0.dist-info/licenses/COPYING.rst,sha256=NBr8vXKYh7cEb-e5j8T07f867Y048G7v2bMGcPBD3xc,1639
179
- ipython-9.3.0.dist-info/licenses/LICENSE,sha256=4OOQdI7UQKuJPKHxNaiKkgqvVAnbuQpbQnx1xeUSaPs,1720
180
- ipython-9.3.0.dist-info/METADATA,sha256=PLMXmc_ptvho0DnNgFp4AFEqUxL3ikB9OaYqtJbgfzw,4431
181
- ipython-9.3.0.dist-info/WHEEL,sha256=aCYgKU6ypALRDRz4OvLmTVj8mlMrXZlQylkgDKtOmfQ,90
182
- ipython-9.3.0.dist-info/entry_points.txt,sha256=z5BEEohWgg0SHdgdeNABf4T3fu-lr9W6F_bWOQHLdVs,83
183
- ipython-9.3.0.dist-info/top_level.txt,sha256=PKjvHtNCBZ9EHTmd2mwJ1J_k3j0F6D1lTFzIcJFFPEU,8
184
- ipython-9.3.0.dist-info/RECORD,,
178
+ ipython-9.4.0.data/data/share/man/man1/ipython.1,sha256=PVdQP2hHmHyUEwzLOPcgavnCe9jTDVrM1jKZt4cnF_Q,2058
179
+ ipython-9.4.0.dist-info/licenses/COPYING.rst,sha256=NBr8vXKYh7cEb-e5j8T07f867Y048G7v2bMGcPBD3xc,1639
180
+ ipython-9.4.0.dist-info/licenses/LICENSE,sha256=4OOQdI7UQKuJPKHxNaiKkgqvVAnbuQpbQnx1xeUSaPs,1720
181
+ ipython-9.4.0.dist-info/METADATA,sha256=d4dTcreyYD_4S0OE3JSqst9kT1pRCroTCNNwdInBiZY,4431
182
+ ipython-9.4.0.dist-info/WHEEL,sha256=_2vT4RZnosDS5yjNeAMuEbJY3SAaKsQTuZHmdWSC-aI,90
183
+ ipython-9.4.0.dist-info/entry_points.txt,sha256=z5BEEohWgg0SHdgdeNABf4T3fu-lr9W6F_bWOQHLdVs,83
184
+ ipython-9.4.0.dist-info/top_level.txt,sha256=PKjvHtNCBZ9EHTmd2mwJ1J_k3j0F6D1lTFzIcJFFPEU,8
185
+ ipython-9.4.0.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (9.3.0)
2
+ Generator: setuptools (9.4.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5