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

Fix crash when using f-strings #653

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions docs/release_notes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@ Release Notes
**pydocstyle** version numbers follow the
`Semantic Versioning <http://semver.org/>`_ specification.

Current Development Version
---------------------------

Bug Fixes

* Fix a crash when f-strings are used as docstrings (#653).

6.3.0 - January 17th, 2023
--------------------------

Expand Down
33 changes: 21 additions & 12 deletions src/pydocstyle/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,15 @@

__all__ = ('check',)

FSTRING_REGEX = re(r'^([rR]?)[fF]')


def eval_docstring(docstring):
"""Safely evaluate docstring."""
if FSTRING_REGEX.match(str(docstring)):
return ""
return ast.literal_eval(docstring)


def check_for(kind, terminal=False):
def decorator(f):
Expand Down Expand Up @@ -241,7 +250,7 @@ def check_docstring_empty(self, definition, docstring):
NOTE: This used to report as D10X errors.

"""
if docstring and is_blank(ast.literal_eval(docstring)):
if docstring and is_blank(eval_docstring(docstring)):
return violations.D419()

@check_for(Definition)
Expand All @@ -253,7 +262,7 @@ def check_one_liners(self, definition, docstring):

"""
if docstring:
lines = ast.literal_eval(docstring).split('\n')
lines = eval_docstring(docstring).split('\n')
if len(lines) > 1:
non_empty_lines = sum(1 for l in lines if not is_blank(l))
if non_empty_lines == 1:
Expand Down Expand Up @@ -329,7 +338,7 @@ def check_blank_after_summary(self, definition, docstring):

"""
if docstring:
lines = ast.literal_eval(docstring).strip().split('\n')
lines = eval_docstring(docstring).strip().split('\n')
if len(lines) > 1:
post_summary_blanks = list(map(is_blank, lines[1:]))
blanks_count = sum(takewhile(bool, post_summary_blanks))
Expand Down Expand Up @@ -382,7 +391,7 @@ def check_newline_after_last_paragraph(self, definition, docstring):
if docstring:
lines = [
l
for l in ast.literal_eval(docstring).split('\n')
for l in eval_docstring(docstring).split('\n')
if not is_blank(l)
]
if len(lines) > 1:
Expand All @@ -393,7 +402,7 @@ def check_newline_after_last_paragraph(self, definition, docstring):
def check_surrounding_whitespaces(self, definition, docstring):
"""D210: No whitespaces allowed surrounding docstring text."""
if docstring:
lines = ast.literal_eval(docstring).split('\n')
lines = eval_docstring(docstring).split('\n')
if (
lines[0].startswith(' ')
or len(lines) == 1
Expand Down Expand Up @@ -421,7 +430,7 @@ def check_multi_line_summary_start(self, definition, docstring):
"ur'''",
]

lines = ast.literal_eval(docstring).split('\n')
lines = eval_docstring(docstring).split('\n')
if len(lines) > 1:
first = docstring.split("\n")[0].strip().lower()
if first in start_triple:
Expand All @@ -443,7 +452,7 @@ def check_triple_double_quotes(self, definition, docstring):

'''
if docstring:
if '"""' in ast.literal_eval(docstring):
if '"""' in eval_docstring(docstring):
# Allow ''' quotes if docstring contains """, because
# otherwise """ quotes could not be expressed inside
# docstring. Not in PEP 257.
Expand Down Expand Up @@ -487,7 +496,7 @@ def _check_ends_with(docstring, chars, violation):

"""
if docstring:
summary_line = ast.literal_eval(docstring).strip().split('\n')[0]
summary_line = eval_docstring(docstring).strip().split('\n')[0]
if not summary_line.endswith(chars):
return violation(summary_line[-1])

Expand Down Expand Up @@ -526,7 +535,7 @@ def check_imperative_mood(self, function, docstring): # def context
and not function.is_test
and not function.is_property(self.property_decorators)
):
stripped = ast.literal_eval(docstring).strip()
stripped = eval_docstring(docstring).strip()
if stripped:
first_word = strip_non_alphanumeric(stripped.split()[0])
check_word = first_word.lower()
Expand All @@ -552,7 +561,7 @@ def check_no_signature(self, function, docstring): # def context

"""
if docstring:
first_line = ast.literal_eval(docstring).strip().split('\n')[0]
first_line = eval_docstring(docstring).strip().split('\n')[0]
if function.name + '(' in first_line.replace(' ', ''):
return violations.D402()

Expand All @@ -564,7 +573,7 @@ def check_capitalized(self, function, docstring):

"""
if docstring:
first_word = ast.literal_eval(docstring).split()[0]
first_word = eval_docstring(docstring).split()[0]
if first_word == first_word.upper():
return
for char in first_word:
Expand Down Expand Up @@ -596,7 +605,7 @@ def check_starts_with_this(self, function, docstring):
if not docstring:
return

stripped = ast.literal_eval(docstring).strip()
stripped = eval_docstring(docstring).strip()
if not stripped:
return

Expand Down
11 changes: 10 additions & 1 deletion src/tests/test_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -1584,4 +1584,13 @@ def test_match_considers_basenames_for_path_args(env):
# env.invoke calls pydocstyle with full path to test_a.py
out, _, code = env.invoke(target='test_a.py')
assert '' == out
assert code == 0
assert code == 0

def test_fstring(env):
"""Test that f-strings do not cause a crash."""
env.write_config(select='D')
with env.open("test.py", 'wt') as fobj:
fobj.write('''f"bar {123}"''')
_, err, code = env.invoke(args="-v")
assert code == 1
assert "ValueError" not in err