55import dataclasses
66import re
77import secrets
8+ import string
9+ from collections .abc import Collection
810from functools import lru_cache
911from importlib .util import find_spec
1012from typing import TypeGuard
@@ -188,6 +190,13 @@ def mask_cell(src: str) -> tuple[str, list[Replacement]]:
188190def create_token (n_chars : int ) -> str :
189191 """Create a randomly generated token that is n_chars characters long."""
190192 assert n_chars > 0
193+ if n_chars == 1 :
194+ return secrets .choice (string .ascii_letters )
195+ if n_chars < 4 :
196+ return "_" + "" .join (
197+ secrets .choice (string .ascii_letters + string .digits + "_" )
198+ for _ in range (n_chars - 1 )
199+ )
191200 n_bytes = max (n_chars // 2 - 1 , 1 )
192201 token = secrets .token_hex (n_bytes )
193202 if len (token ) + 3 > n_chars :
@@ -197,7 +206,7 @@ def create_token(n_chars: int) -> str:
197206 return f'b"{ token } "'
198207
199208
200- def get_token (src : str , magic : str ) -> str :
209+ def get_token (src : str , magic : str , existing_tokens : Collection [ str ] = () ) -> str :
201210 """Return randomly generated token to mask IPython magic with.
202211
203212 For example, if 'magic' was `%matplotlib inline`, then a possible
@@ -209,7 +218,7 @@ def get_token(src: str, magic: str) -> str:
209218 n_chars = len (magic )
210219 token = create_token (n_chars )
211220 counter = 0
212- while token in src :
221+ while token in src or token in existing_tokens :
213222 token = create_token (n_chars )
214223 counter += 1
215224 if counter > 100 :
@@ -271,6 +280,7 @@ def replace_magics(src: str) -> tuple[str, list[Replacement]]:
271280 The replacement, along with the transformed code, are returned.
272281 """
273282 replacements = []
283+ existing_tokens : set [str ] = set ()
274284 magic_finder = MagicFinder ()
275285 magic_finder .visit (ast .parse (src ))
276286 new_srcs = []
@@ -286,8 +296,9 @@ def replace_magics(src: str) -> tuple[str, list[Replacement]]:
286296 offsets_and_magics [0 ].col_offset ,
287297 offsets_and_magics [0 ].magic ,
288298 )
289- mask = get_token (src , magic )
299+ mask = get_token (src , magic , existing_tokens )
290300 replacements .append (Replacement (mask = mask , src = magic ))
301+ existing_tokens .add (mask )
291302 line = line [:col_offset ] + mask
292303 new_srcs .append (line )
293304 return "\n " .join (new_srcs ), replacements
@@ -307,7 +318,9 @@ def unmask_cell(src: str, replacements: list[Replacement]) -> str:
307318 foo = bar
308319 """
309320 for replacement in replacements :
310- src = src .replace (replacement .mask , replacement .src )
321+ if src .count (replacement .mask ) != 1 :
322+ raise NothingChanged
323+ src = src .replace (replacement .mask , replacement .src , 1 )
311324 return src
312325
313326
0 commit comments