Skip to content

Commit 4733a57

Browse files
authored
0.7.3 (#61)
* Get richer information for ImproperUseError: #60 * Add VarnameException and VarnameWarning as root for all varname-defined exceptions and warnings. * Add mypy check pre-commit * Add mypy check to CI
1 parent d3182fd commit 4733a57

File tree

6 files changed

+103
-41
lines changed

6 files changed

+103
-41
lines changed

.github/workflows/build.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,12 @@ jobs:
2525
run: |
2626
python -m pip install --upgrade pip
2727
python -m pip install pylint=='2.8.*'
28+
python -m pip install mypy
2829
python -m pip install poetry
2930
poetry config virtualenvs.create false
3031
poetry install -v
32+
- name: Run mypy check
33+
run: mypy -p varname
3134
- name: Run pylint
3235
run: pylint varname
3336
- name: Test with pytest

.pre-commit-config.yaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,14 @@ repos:
4040
language: system
4141
files: pyproject.toml
4242
pass_filenames: false
43+
- id: mypycheck
44+
name: Type checking by mypy
45+
entry: mypy
46+
language: system
47+
files: ^pipda/.+$
48+
pass_filenames: false
49+
types: [python]
50+
args: [-p, varname]
4351
- id: pytest
4452
name: Run pytest
4553
entry: pytest

varname/core.py

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
get_argument_sources,
1616
get_function_called_argname,
1717
parse_argname_subscript,
18+
rich_exc_message,
1819
ArgSourceType,
1920
VarnameRetrievingError,
2021
ImproperUseError,
@@ -104,13 +105,13 @@ class instantiation.
104105

105106
# Skip one more frame, as it is supposed to be called
106107
# inside another function
107-
node = get_node(frame + 1, ignore, raise_exc=raise_exc)
108-
if not node:
108+
refnode = get_node(frame + 1, ignore, raise_exc=raise_exc)
109+
if not refnode:
109110
if raise_exc:
110111
raise VarnameRetrievingError("Unable to retrieve the ast node.")
111112
return None
112113

113-
node = lookfor_parent_assign(node, strict=strict)
114+
node = lookfor_parent_assign(refnode, strict=strict)
114115
if not node:
115116
if strict and not strict_given:
116117
warnings.warn(
@@ -134,7 +135,7 @@ class instantiation.
134135
msg = "Expression is not the direct RHS of an assignment."
135136
else:
136137
msg = "Expression is not part of an assignment."
137-
raise ImproperUseError(msg)
138+
raise ImproperUseError(rich_exc_message(msg, refnode))
138139
return None
139140

140141
if isinstance(node, ast.Assign):
@@ -161,7 +162,11 @@ class instantiation.
161162

162163
if len(names) > 1:
163164
raise ImproperUseError(
164-
f"Expect a single variable on left-hand side, got {len(names)}."
165+
rich_exc_message(
166+
"Expect a single variable on left-hand side, "
167+
f"got {len(names)}.",
168+
refnode,
169+
)
165170
)
166171

167172
return names[0]
@@ -539,8 +544,12 @@ def argname2(
539544
If False, `asttokens` is required to retrieve the source.
540545
541546
Returns:
542-
Scalar string if
547+
The argument source when no more_args passed, otherwise a tuple of
548+
argument sources
543549
550+
Raises:
551+
VarnameRetrievingError: When the ast node where the function is called
552+
cannot be retrieved
544553
"""
545554
ignore_list = IgnoreList.create(
546555
ignore,
@@ -579,7 +588,7 @@ def argname2(
579588
vars_only=vars_only,
580589
pos_only=False,
581590
)
582-
except Exception as err: # pragma: no cover
591+
except Exception as err: # pragma: no cover
583592
# find a test case?
584593
raise VarnameRetrievingError(
585594
"Have you specified the right `frame`?"

varname/helpers.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,7 @@ def debug(
184184
values = (var, *more_vars)
185185
name_and_values = [
186186
f"{var_name}{sep}{value!r}" if repr else f"{var_name}{sep}{value}"
187-
for var_name, value in zip(var_names, values) # type: ignore
187+
for var_name, value in zip(var_names, values) # type: ignore
188188
]
189189

190190
if merge:

varname/ignore.py

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
debug_ignore_frame,
4242
)
4343

44+
4445
class IgnoreElem(ABC):
4546
"""An element of the ignore list"""
4647

@@ -66,7 +67,7 @@ def subclass_init(
6667

6768
# save it for __repr__
6869
cls.attrs = attrs
69-
cls.__init__ = subclass_init # type: ignore
70+
cls.__init__ = subclass_init # type: ignore
7071

7172
def _post_init(self) -> None:
7273
"""Setups after __init__"""
@@ -127,7 +128,7 @@ def _post_init(self) -> None:
127128
# pylint: disable=attribute-defined-outside-init
128129

129130
# Path object will turn into str here
130-
self.dirname = path.realpath(self.dirname) # type: str
131+
self.dirname = path.realpath(self.dirname) # type: str
131132

132133
if not self.dirname.endswith(path.sep):
133134
self.dirname = f"{self.dirname}{path.sep}"
@@ -264,23 +265,23 @@ def create_ignore_elem(ignore_elem: IgnoreElemType) -> IgnoreElem:
264265
return (
265266
IgnoreDirname(ignore_elem) # type: ignore
266267
if path.isdir(ignore_elem)
267-
else IgnoreFilename(ignore_elem) # type: ignore
268+
else IgnoreFilename(ignore_elem) # type: ignore
268269
)
269270
if hasattr(ignore_elem, "__code__"):
270-
return IgnoreFunction(ignore_elem) # type: ignore
271+
return IgnoreFunction(ignore_elem) # type: ignore
271272
if not isinstance(ignore_elem, tuple) or len(ignore_elem) != 2:
272273
raise ValueError(f"Unexpected ignore item: {ignore_elem!r}")
273274
# is tuple and len == 2
274275
if hasattr(ignore_elem[0], "__code__") and isinstance(ignore_elem[1], int):
275-
return IgnoreDecorated(*ignore_elem) # type: ignore
276+
return IgnoreDecorated(*ignore_elem) # type: ignore
276277
# otherwise, the second element should be qualname
277278
if not isinstance(ignore_elem[1], str):
278279
raise ValueError(f"Unexpected ignore item: {ignore_elem!r}")
279280

280281
if isinstance(ignore_elem[0], ModuleType):
281-
return IgnoreModuleQualname(*ignore_elem) # type: ignore
282+
return IgnoreModuleQualname(*ignore_elem) # type: ignore
282283
if isinstance(ignore_elem[0], (Path, str)):
283-
return IgnoreFilenameQualname(*ignore_elem) # type: ignore
284+
return IgnoreFilenameQualname(*ignore_elem) # type: ignore
284285
if ignore_elem[0] is None:
285286
return IgnoreOnlyQualname(*ignore_elem)
286287

@@ -316,24 +317,22 @@ def create(
316317
ignore = [ignore]
317318

318319
ignore_list = [
319-
IgnoreStdlib( # type: ignore
320+
IgnoreStdlib( # type: ignore
320321
sysconfig.get_python_lib(standard_lib=True)
321322
)
322-
] # type: List[IgnoreElem]
323+
] # type: List[IgnoreElem]
323324
if ignore_varname:
324325
ignore_list.append(create_ignore_elem(sys.modules[__package__]))
325326
if ignore_lambda:
326327
ignore_list.append(create_ignore_elem((None, "*<lambda>")))
327328
for ignore_elem in ignore:
328329
ignore_list.append(create_ignore_elem(ignore_elem))
329330

330-
return cls(ignore_list) # type: ignore
331+
return cls(ignore_list) # type: ignore
331332

332333
def __init__(self, ignore_list: List[IgnoreElemType]) -> None:
333334
self.ignore_list = ignore_list
334-
debug_ignore_frame(
335-
'>>> IgnoreList initiated <<<'
336-
)
335+
debug_ignore_frame(">>> IgnoreList initiated <<<")
337336

338337
def nextframe_to_check(
339338
self, frame_no: int, frameinfos: List[inspect.FrameInfo]
@@ -352,7 +351,7 @@ def nextframe_to_check(
352351
A number for Next `N`th frame to check. 0 if no frame matched.
353352
"""
354353
for ignore_elem in self.ignore_list:
355-
matched = ignore_elem.match(frame_no, frameinfos) # type: ignore
354+
matched = ignore_elem.match(frame_no, frameinfos) # type: ignore
356355
if matched and isinstance(ignore_elem, IgnoreDecorated):
357356
debug_ignore_frame(
358357
f"Ignored by {ignore_elem!r}", frameinfos[frame_no]
@@ -406,4 +405,4 @@ def get_frame(self, frame_no: int) -> FrameType:
406405

407406
raise VarnameRetrievingError from exc
408407

409-
return None # pragma: no cover
408+
return None # pragma: no cover

varname/utils.py

Lines changed: 61 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,10 @@
4444

4545
if sys.version_info >= (3, 8):
4646
ASSIGN_TYPES = (ast.Assign, ast.AnnAssign, ast.NamedExpr)
47-
AssignType = Union[ASSIGN_TYPES]
48-
else: # pragma: no cover
47+
AssignType = Union[ASSIGN_TYPES] # type: ignore
48+
else: # pragma: no cover
4949
ASSIGN_TYPES = (ast.Assign, ast.AnnAssign)
50-
AssignType = Union[ASSIGN_TYPES]
50+
AssignType = Union[ASSIGN_TYPES] # type: ignore
5151

5252
MODULE_IGNORE_ID_NAME = "__varname_ignore_id__"
5353

@@ -62,29 +62,37 @@ class config: # pylint: disable=invalid-name
6262
debug = False
6363

6464

65-
class VarnameRetrievingError(Exception):
65+
class VarnameException(Exception):
66+
"""Root exception for all varname exceptions"""
67+
68+
69+
class VarnameRetrievingError(VarnameException):
6670
"""When failed to retrieve the varname"""
6771

6872

69-
class QualnameNonUniqueError(Exception):
73+
class QualnameNonUniqueError(VarnameException):
7074
"""When a qualified name is used as an ignore element but references to
7175
multiple objects in a module"""
7276

7377

74-
class NonVariableArgumentError(Exception):
78+
class NonVariableArgumentError(VarnameException):
7579
"""When vars_only is True but try to retrieve name of
7680
a non-variable argument"""
7781

7882

79-
class ImproperUseError(Exception):
83+
class ImproperUseError(VarnameException):
8084
"""When varname() is improperly used"""
8185

8286

83-
class MaybeDecoratedFunctionWarning(Warning):
87+
class VarnameWarning(Warning):
88+
"""Root warning for all varname warnings"""
89+
90+
91+
class MaybeDecoratedFunctionWarning(VarnameWarning):
8492
"""When a suspecious decorated function used as ignore function directly"""
8593

8694

87-
class MultiTargetAssignmentWarning(Warning):
95+
class MultiTargetAssignmentWarning(VarnameWarning):
8896
"""When varname tries to retrieve variable name in
8997
a multi-target assignment"""
9098

@@ -120,13 +128,14 @@ def get_node(
120128
return get_node_by_frame(frameobj, raise_exc)
121129

122130

123-
def get_node_by_frame(
124-
frame: FrameType, raise_exc: bool = True
125-
) -> ast.AST:
131+
def get_node_by_frame(frame: FrameType, raise_exc: bool = True) -> ast.AST:
126132
"""Get the node by frame, raise errors if possible"""
127133
exect = Source.executing(frame)
128134

129135
if exect.node:
136+
# attach the frame for better exception message
137+
# (ie. where ImproperUseError happens)
138+
exect.node.__frame__ = frame
130139
return exect.node
131140

132141
if exect.source.text and exect.source.tree and raise_exc:
@@ -257,10 +266,7 @@ def check_qualname_by_source(
257266
)
258267

259268

260-
def debug_ignore_frame(
261-
msg: str,
262-
frameinfo: inspect.FrameInfo = None
263-
) -> None:
269+
def debug_ignore_frame(msg: str, frameinfo: inspect.FrameInfo = None) -> None:
264270
"""Print the debug message for a given frame info object
265271
266272
Args:
@@ -415,9 +421,9 @@ def parse_argname_subscript(node: ast.Subscript):
415421
if not isinstance(name, ast.Name):
416422
raise ValueError(f"Expect {ast.dump(name)} to be a variable.")
417423

418-
subscript = node.slice # type: ast.AST
424+
subscript = node.slice # type: ast.AST
419425
if isinstance(subscript, ast.Index):
420-
subscript = subscript.value # pragma: no cover
426+
subscript = subscript.value # pragma: no cover
421427
if not isinstance(subscript, (ast.Str, ast.Num, ast.Constant)):
422428
raise ValueError(f"Expect {ast.dump(subscript)} to be a constant.")
423429

@@ -428,3 +434,40 @@ def parse_argname_subscript(node: ast.Subscript):
428434
subscript = getattr(subscript, "s", subscript) # ast.Str
429435

430436
return name.id, subscript
437+
438+
439+
def rich_exc_message(msg: str, node: ast.AST) -> str:
440+
"""Attach the source code from the node to message to
441+
get a rich message for exceptions
442+
443+
If package 'rich' is not install or 'node.__frame__' doesn't exist, fall
444+
to plain message (with basic information), otherwise show a better message
445+
with full information
446+
"""
447+
frame = node.__frame__ # type: FrameType
448+
lineno = node.lineno # type: int
449+
col_offset = node.col_offset # type: int
450+
filename = frame.f_code.co_filename # type: str
451+
lines, startlineno = inspect.getsourcelines(frame)
452+
line_range = (startlineno + 1, startlineno + len(lines) + 1)
453+
if startlineno == 0:
454+
# wired shift
455+
lineno -= 1 # pragma: no cover
456+
457+
linenos = tuple(map(str, range(*line_range))) # type: Tuple[str, ...]
458+
lineno_width = max(map(len, linenos)) # type: int
459+
hiline = lineno - startlineno # type: int
460+
codes = [] # type: List[str]
461+
for i, lno in enumerate(linenos):
462+
lno = lno.ljust(lineno_width)
463+
if i == hiline:
464+
codes.append(f" > | {lno} {lines[i]}")
465+
codes.append(f" | {' ' * (lineno_width + col_offset + 2)}^\n")
466+
else:
467+
codes.append(f" | {lno} {lines[i]}")
468+
469+
return (
470+
f"{msg}\n\n"
471+
f" {filename}:{lineno+1}:{col_offset+1}\n"
472+
f"{''.join(codes)}\n"
473+
)

0 commit comments

Comments
 (0)