|
1 | 1 | from check50 import Failure, Missing, Mismatch |
2 | 2 | import re |
3 | 3 | import inspect |
| 4 | +import tokenize |
| 5 | +import types, builtins |
| 6 | +from io import StringIO |
| 7 | + |
4 | 8 |
|
5 | 9 | def check50_assert(src, msg_or_exc=None, cond_type="unknown", left=None, right=None, context=None): |
6 | 10 | """ |
@@ -67,13 +71,27 @@ def check50_assert(src, msg_or_exc=None, cond_type="unknown", left=None, right=N |
67 | 71 | except Exception as e: |
68 | 72 | context[expr_str] = f"[error evaluating: {e}]" |
69 | 73 |
|
70 | | - # produces a string like "var1 = ..., var2 = ..., foo() = ..." |
71 | | - context_str = ", ".join(f"{k} = {repr(v)}" for k, v in (context or {}).items()) |
| 74 | + # filter out modules, functions, and built-ins |
| 75 | + def is_irrelevant_value(v): |
| 76 | + return isinstance(v, (types.ModuleType, types.FunctionType, types.BuiltinFunctionType)) |
| 77 | + |
| 78 | + def is_builtin_name(name): |
| 79 | + return name in dir(builtins) |
72 | 80 |
|
| 81 | + filtered_context = { |
| 82 | + k: v for k, v in context.items() |
| 83 | + if not is_irrelevant_value(v) and not is_builtin_name(k.split("(")[0]) |
| 84 | + } |
| 85 | + |
| 86 | + # produces a string like "var1 = ..., var2 = ..., foo() = ..." |
| 87 | + context_str = ", ".join(f"{k} = {repr(v)}" for k, v in filtered_context.items()) |
| 88 | + else: |
| 89 | + filtered_context = {} |
| 90 | + |
73 | 91 | # Since we've memoized the functions and variables once, now try and |
74 | 92 | # evaluate the conditional by substituting the function calls/vars with |
75 | 93 | # their results |
76 | | - eval_src, eval_context = substitute_expressions(src, context) |
| 94 | + eval_src, eval_context = substitute_expressions(src, filtered_context) |
77 | 95 |
|
78 | 96 | # Merge globals with expression context for evaluation |
79 | 97 | eval_globals = caller_globals.copy() |
@@ -129,18 +147,23 @@ def substitute_expressions(src: str, context: dict) -> tuple[str, dict]: |
129 | 147 | } |
130 | 148 | ``` |
131 | 149 | """ |
132 | | - new_src = src |
133 | | - new_context = {} |
134 | | - |
135 | | - for i, expr in enumerate(sorted(context.keys(), key=len, reverse=True)): |
136 | | - placeholder = f"__expr{i}" |
137 | | - |
138 | | - # Use regex to replace only full matches of expr |
139 | | - # Escape expr if it has special characters (like function calls) |
140 | | - pattern = re.escape(expr) |
141 | | - new_src, count = re.subn(rf'\b{pattern}\b', placeholder, new_src) |
| 150 | + tokens = tokenize.generate_tokens(StringIO(src).readline) |
142 | 151 |
|
143 | | - if count > 0: |
144 | | - new_context[placeholder] = context[expr] |
145 | | - |
146 | | - return new_src, new_context |
| 152 | + new_tokens = [] |
| 153 | + new_context = {} |
| 154 | + placeholder_map = {} |
| 155 | + counter = 0 |
| 156 | + |
| 157 | + for tok_type, tok_string, start, end, line in tokens: |
| 158 | + if tok_string in context: |
| 159 | + if tok_string not in placeholder_map: |
| 160 | + placeholder = f"__expr{counter}" |
| 161 | + placeholder_map[tok_string] = placeholder |
| 162 | + new_context[placeholder] = context[tok_string] |
| 163 | + counter += 1 |
| 164 | + new_tokens.append((tok_type, placeholder)) |
| 165 | + else: |
| 166 | + new_tokens.append((tok_type, tok_string)) |
| 167 | + |
| 168 | + eval_src = tokenize.untokenize(new_tokens) |
| 169 | + return eval_src, new_context |
0 commit comments