Skip to content

Commit 5d25894

Browse files
committed
Add support for PEP701
* fstrings are broken into several distinct token in py3.12, reattach them together as a singular string to preserve previous behavior. Closes: PyCQA#646 Signed-off-by: Alfred Wingate <[email protected]>
1 parent f78f194 commit 5d25894

File tree

3 files changed

+59
-0
lines changed

3 files changed

+59
-0
lines changed

docs/release_notes.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,14 @@ Release Notes
44
**pydocstyle** version numbers follow the
55
`Semantic Versioning <http://semver.org/>`_ specification.
66

7+
8+
Current development version
9+
---------------------------
10+
11+
Bug Fixes
12+
13+
* Add support for PEP-701 fixing fstring parsing in python3.12 (#656).
14+
715
6.3.0 - January 17th, 2023
816
--------------------------
917

src/pydocstyle/parser.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -463,6 +463,7 @@ def leapfrog(self, kind, value=None):
463463
return
464464
self.stream.move()
465465

466+
466467
def parse_docstring(self):
467468
"""Parse a single docstring and return its value."""
468469
self.log.debug("parsing docstring, token is %s", self.current)
@@ -479,6 +480,27 @@ def parse_docstring(self):
479480
)
480481
self.stream.move()
481482
return docstring
483+
if (sys.version_info.major, sys.version_info.minor) >= (
484+
3,
485+
12,
486+
) and self.current.kind == tk.FSTRING_START:
487+
def fstring(string):
488+
"""Recursively parse fstring tokens to output it as one string."""
489+
while self.current.kind != tk.FSTRING_END:
490+
self.stream.move()
491+
string += self.current.value
492+
if self.current.kind == tk.FSTRING_START:
493+
string = fstring(string)
494+
self.stream.move()
495+
string += self.current.value
496+
return string
497+
# Reattach fstring tokens together into a string to deal with PEP 701 in python3.12
498+
start = self.current.start[0]
499+
string = fstring(self.current.value)
500+
end = self.current.end[0]
501+
docstring = Docstring(string, start, end)
502+
self.stream.move()
503+
return docstring
482504
return None
483505

484506
def parse_decorators(self):

src/tests/parser_test.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,35 @@ def do_something(pos_param0, pos_param1, kw_param0="default"):
114114
assert str(function) == 'in public function `do_something`'
115115

116116

117+
def test_nested_fstring():
118+
"""Test parsing fstring with nested fstrings."""
119+
parser = Parser()
120+
code = CodeSnippet("""\
121+
def do_something(pos_param0, pos_param1, kw_param0="default"):
122+
f\"""Do something. {f"This is a nested fstring."}\"""
123+
return None
124+
""")
125+
module = parser.parse(code, 'file_path')
126+
assert module.is_public
127+
assert module.dunder_all is None
128+
129+
function, = module.children
130+
assert function.name == 'do_something'
131+
assert function.decorators == []
132+
assert function.children == []
133+
assert function.docstring == 'f"""Do something. {f"This is a nested fstring."}"""'
134+
assert function.docstring.start == 2
135+
assert function.docstring.end == 2
136+
assert function.kind == 'function'
137+
assert function.parent == module
138+
assert function.start == 1
139+
assert function.end == 3
140+
assert function.error_lineno == 2
141+
assert function.source == code.getvalue()
142+
assert function.is_public
143+
assert str(function) == 'in public function `do_something`'
144+
145+
117146
def test_decorated_function():
118147
"""Test parsing of a simple function with a decorator."""
119148
parser = Parser()

0 commit comments

Comments
 (0)