Skip to content
This repository was archived by the owner on Nov 3, 2023. It is now read-only.

Commit afd0030

Browse files
committed
Safely evaluate literals in case fstring check has been disbaled
1 parent e6a7c82 commit afd0030

File tree

3 files changed

+42
-31
lines changed

3 files changed

+42
-31
lines changed

src/pydocstyle/checker.py

+15-23
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@
22

33
import ast
44
import string
5-
import sys
6-
import textwrap
75
import tokenize as tk
86
from collections import namedtuple
97
from itertools import chain, takewhile
@@ -28,8 +26,10 @@
2826
from .utils import (
2927
common_prefix_length,
3028
is_blank,
29+
is_fstring,
3130
log,
3231
pairwise,
32+
safe_literal_eval,
3333
strip_non_alphanumeric,
3434
)
3535
from .wordlists import IMPERATIVE_BLACKLIST, IMPERATIVE_VERBS, stem
@@ -46,14 +46,6 @@ def decorator(f):
4646
return decorator
4747

4848

49-
_FSTRING_REGEX = re(r'^[rR]?[fF]')
50-
51-
52-
def _is_fstring(docstring):
53-
"""Return True if docstring is an f-string."""
54-
return _FSTRING_REGEX.match(str(docstring))
55-
56-
5749
class ConventionChecker:
5850
"""Checker for PEP 257, NumPy and Google conventions.
5951
@@ -201,7 +193,7 @@ def check_docstring_fstring(self, definition, docstring):
201193
and users may attempt to use them as docstrings. This is an
202194
outright mistake so we issue a specific error code.
203195
"""
204-
if _is_fstring(docstring):
196+
if is_fstring(docstring):
205197
return violations.D303()
206198

207199
@check_for(Definition, terminal=True)
@@ -222,7 +214,7 @@ def check_docstring_missing(self, definition, docstring):
222214
not docstring
223215
and definition.is_public
224216
or docstring
225-
and is_blank(docstring, literal_eval=True)
217+
and is_blank(safe_literal_eval(docstring))
226218
):
227219
codes = {
228220
Module: violations.D100,
@@ -252,7 +244,7 @@ def check_one_liners(self, definition, docstring):
252244
253245
"""
254246
if docstring:
255-
lines = ast.literal_eval(docstring).split('\n')
247+
lines = safe_literal_eval(docstring).split('\n')
256248
if len(lines) > 1:
257249
non_empty_lines = sum(1 for l in lines if not is_blank(l))
258250
if non_empty_lines == 1:
@@ -328,7 +320,7 @@ def check_blank_after_summary(self, definition, docstring):
328320
329321
"""
330322
if docstring:
331-
lines = ast.literal_eval(docstring).strip().split('\n')
323+
lines = safe_literal_eval(docstring).strip().split('\n')
332324
if len(lines) > 1:
333325
post_summary_blanks = list(map(is_blank, lines[1:]))
334326
blanks_count = sum(takewhile(bool, post_summary_blanks))
@@ -381,7 +373,7 @@ def check_newline_after_last_paragraph(self, definition, docstring):
381373
if docstring:
382374
lines = [
383375
l
384-
for l in ast.literal_eval(docstring).split('\n')
376+
for l in safe_literal_eval(docstring).split('\n')
385377
if not is_blank(l)
386378
]
387379
if len(lines) > 1:
@@ -392,7 +384,7 @@ def check_newline_after_last_paragraph(self, definition, docstring):
392384
def check_surrounding_whitespaces(self, definition, docstring):
393385
"""D210: No whitespaces allowed surrounding docstring text."""
394386
if docstring:
395-
lines = ast.literal_eval(docstring).split('\n')
387+
lines = safe_literal_eval(docstring).split('\n')
396388
if (
397389
lines[0].startswith(' ')
398390
or len(lines) == 1
@@ -420,7 +412,7 @@ def check_multi_line_summary_start(self, definition, docstring):
420412
"ur'''",
421413
]
422414

423-
lines = ast.literal_eval(docstring).split('\n')
415+
lines = safe_literal_eval(docstring).split('\n')
424416
if len(lines) > 1:
425417
first = docstring.split("\n")[0].strip().lower()
426418
if first in start_triple:
@@ -442,7 +434,7 @@ def check_triple_double_quotes(self, definition, docstring):
442434
443435
'''
444436
if docstring:
445-
if '"""' in ast.literal_eval(docstring):
437+
if '"""' in safe_literal_eval(docstring):
446438
# Allow ''' quotes if docstring contains """, because
447439
# otherwise """ quotes could not be expressed inside
448440
# docstring. Not in PEP 257.
@@ -486,7 +478,7 @@ def _check_ends_with(docstring, chars, violation):
486478
487479
"""
488480
if docstring:
489-
summary_line = ast.literal_eval(docstring).strip().split('\n')[0]
481+
summary_line = safe_literal_eval(docstring).strip().split('\n')[0]
490482
if not summary_line.endswith(chars):
491483
return violation(summary_line[-1])
492484

@@ -521,7 +513,7 @@ def check_imperative_mood(self, function, docstring): # def context
521513
522514
"""
523515
if docstring and not function.is_test:
524-
stripped = ast.literal_eval(docstring).strip()
516+
stripped = safe_literal_eval(docstring).strip()
525517
if stripped:
526518
first_word = strip_non_alphanumeric(stripped.split()[0])
527519
check_word = first_word.lower()
@@ -547,7 +539,7 @@ def check_no_signature(self, function, docstring): # def context
547539
548540
"""
549541
if docstring:
550-
first_line = ast.literal_eval(docstring).strip().split('\n')[0]
542+
first_line = safe_literal_eval(docstring).strip().split('\n')[0]
551543
if function.name + '(' in first_line.replace(' ', ''):
552544
return violations.D402()
553545

@@ -559,7 +551,7 @@ def check_capitalized(self, function, docstring):
559551
560552
"""
561553
if docstring:
562-
first_word = ast.literal_eval(docstring).split()[0]
554+
first_word = safe_literal_eval(docstring).split()[0]
563555
if first_word == first_word.upper():
564556
return
565557
for char in first_word:
@@ -579,7 +571,7 @@ def check_starts_with_this(self, function, docstring):
579571
if not docstring:
580572
return
581573

582-
stripped = ast.literal_eval(docstring).strip()
574+
stripped = safe_literal_eval(docstring).strip()
583575
if not stripped:
584576
return
585577

src/pydocstyle/utils.py

+22-8
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,8 @@
1313
NON_ALPHANUMERIC_STRIP_RE = re.compile(r'[\W_]+')
1414

1515

16-
def is_blank(string: str, literal_eval: bool = False) -> bool:
16+
def is_blank(string: str) -> bool:
1717
"""Return True iff the string contains only whitespaces."""
18-
if literal_eval:
19-
try:
20-
string = ast.literal_eval(string)
21-
except ValueError:
22-
# This happens in case of an fstring
23-
# in which case let's return false
24-
return False
2518
return not string.strip()
2619

2720

@@ -54,3 +47,24 @@ def common_prefix_length(a: str, b: str) -> int:
5447
def strip_non_alphanumeric(string: str) -> str:
5548
"""Strip string from any non-alphanumeric characters."""
5649
return NON_ALPHANUMERIC_STRIP_RE.sub('', string)
50+
51+
52+
FSTRING_REGEX = re.compile(r'^([rR]?)[fF]')
53+
54+
55+
def is_fstring(docstring):
56+
"""Return True if docstring is an f-string."""
57+
return FSTRING_REGEX.match(str(docstring))
58+
59+
60+
def safe_literal_eval(string):
61+
"""Safely evaluate a literal even if it is an fstring."""
62+
try:
63+
return ast.literal_eval(string)
64+
except ValueError:
65+
# In case we hit a value error due to an fstring
66+
# we do a literal eval by subtituting the fstring
67+
# with a normal string.
68+
# We keep the first captured group if any. This includes
69+
# the raw identifiers (r/R) and replace f/F with a blank.
70+
return ast.literal_eval(FSTRING_REGEX.sub(r"\1", string))

src/tests/test_cases/fstrings.py

+5
Original file line numberDiff line numberDiff line change
@@ -50,3 +50,8 @@ def fstring_with_other_errors(arg=1, missing_arg=2):
5050
@D303
5151
def fstring_with_blank_doc_string():
5252
f""" """
53+
54+
55+
@expect("D103: Missing docstring in public function")
56+
def fstring_with_ignores(): # noqa: D303
57+
f""" """

0 commit comments

Comments
 (0)