You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
untokenize() fails on tokenize output when a newline is missing #79288
bpo-44667: tokenize.py emits spurious NEWLINE if file ends on a comment without a newline
Note: these values reflect the state of the issue at the time it was migrated and might not reflect the current state.
Show more details
GitHub fields:
assignee=Noneclosed_at=<Date2022-01-19.20:50:24.347>created_at=<Date2018-10-29.22:26:38.314>labels= ['3.7', '3.8']
title='untokenize() fails on tokenize output when a newline is missing'updated_at=<Date2022-01-19.20:52:55.362>user='https://github.com/gpshead'
The same goes for using the documented tokenize API (generate_tokens is not documented):
tokenize.untokenize(tokenize.tokenize(io.BytesIO(b'#').readline))
...
ValueError: start (1,1) precedes previous end (2,0)
untokenize() is no longer able to work on output of generate_tokens() if the input to generate_tokens() did not end in a newline.
Today's workaround: Always append a newline if one is missing to the line that the readline callable passed to tokenize or generate_tokens returns. Very annoying to implement.
which adds a newline token implicitly after comments. Since the input didn't terminate with a '\n', the code to add a newline at the end of input kicks in.
fwiw I think there's more at play here than the newline change. This is the behavior I get on 3.6.5 (before the newline change is applied). # works as expected but check out this input:
>>> t.untokenize(tokenize.generate_tokens(io.StringIO('#').readline))
'#'
>>> t.untokenize(tokenize.generate_tokens(io.StringIO('x=1').readline))
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "D:\Python365\lib\tokenize.py", line 272, in untokenize
self.add_whitespace(start)
File "D:\Python365\lib\tokenize.py", line 234, in add_whitespace
.format(row, col, self.prev_row, self.prev_col))
ValueError: start (1,0) precedes previous end (2,0)
Interesting! I have a 3.6.2 sitting around and cannot reproduce that "x=1" behavior.
I don't know what the behavior _should_ be. It just feels natural that untokenize should be able to round trip anything tokenize or generate_tokens emits without raising an exception.
I'm filing this as the "#" case came up within some existing code we had that happened to effectively test that particular round trip.
Actually nevermind, disregard that, I was just testing it wrong. I think the simplest fix here is to add '#' to the list of characters here so we don't double insert newlines for comments:
It seems to me a bug that if '\n' is not present, tokenize adds both NL and NEWLINE tokens, instead of just one of them. Moreover, both tuples of the double correction look wrong.
If '\n' is present,
TokenInfo(type=56 (NL), string='\n', start=(1, 1), end=(1, 2), line='#\n')
looks correct.
If NL represents a real character, the length 0 string='' in the generated
TokenInfo(type=56 (NL), string='', start=(1, 1), end=(1, 1), line='#'),
seems wrong. I suspect that the idea was to mis-represent NL to avoid '\n' being added by untokenize. In
TokenInfo(type=4 (NEWLINE), string='', start=(1, 1), end=(1, 2), line='')
string='' is mismatched by length = 2-1 = 1. I am inclined to think that the following would be the correct added token, which should untokenize correctly
TokenInfo(type=4 (NEWLINE), string='', start=(1, 1), end=(1, 1), line='')
ast.dump(ast.parse(s)) returns 'Module(body=[])' for both versions of 's', so no help there.
Activity
gpshead commentedon Oct 29, 2018
The behavior change introduced in 3.6.7 and 3.7.1 via https://bugs.python.org/issue33899 has further consequences:
The same goes for using the documented tokenize API (
generate_tokensis not documented):untokenize()is no longer able to work on output ofgenerate_tokens()if the input to generate_tokens() did not end in a newline.Today's workaround: Always append a newline if one is missing to the line that the readline callable passed to tokenize or generate_tokens returns. Very annoying to implement.
ammaraskar commentedon Oct 29, 2018
Looks like this is caused by this line here:
cpython/Lib/tokenize.py
Lines 551 to 558 in b83d917
which adds a newline token implicitly after comments. Since the input didn't terminate with a '\n', the code to add a newline at the end of input kicks in.
ammaraskar commentedon Oct 29, 2018
fwiw I think there's more at play here than the newline change. This is the behavior I get on 3.6.5 (before the newline change is applied). # works as expected but check out this input:
gpshead commentedon Oct 30, 2018
Interesting! I have a 3.6.2 sitting around and cannot reproduce that "x=1" behavior.
I don't know what the behavior _should_ be. It just feels natural that untokenize should be able to round trip anything tokenize or generate_tokens emits without raising an exception.
I'm filing this as the "#" case came up within some existing code we had that happened to effectively test that particular round trip.
ammaraskar commentedon Oct 30, 2018
Actually nevermind, disregard that, I was just testing it wrong. I think the simplest fix here is to add '#' to the list of characters here so we don't double insert newlines for comments:
cpython/Lib/tokenize.py
Line 659 in b83d917
And a test for round tripping a file ending with a comment but no newline will allow that particular branch to be tested.
I'll make a PR this week if no one else gets to it.
serhiy-storchaka commentedon Oct 30, 2018
I am surprised, that removing the newline character adds a token:
terryjreedy commentedon Oct 30, 2018
It seems to me a bug that if '\n' is not present, tokenize adds both NL and NEWLINE tokens, instead of just one of them. Moreover, both tuples of the double correction look wrong.
If '\n' is present,
TokenInfo(type=56 (NL), string='\n', start=(1, 1), end=(1, 2), line='#\n')
looks correct.
If NL represents a real character, the length 0 string='' in the generated
TokenInfo(type=56 (NL), string='', start=(1, 1), end=(1, 1), line='#'),
seems wrong. I suspect that the idea was to mis-represent NL to avoid '\n' being added by untokenize. In
TokenInfo(type=4 (NEWLINE), string='', start=(1, 1), end=(1, 2), line='')
string='' is mismatched by length = 2-1 = 1. I am inclined to think that the following would be the correct added token, which should untokenize correctly
TokenInfo(type=4 (NEWLINE), string='', start=(1, 1), end=(1, 1), line='')
ast.dump(ast.parse(s)) returns 'Module(body=[])' for both versions of 's', so no help there.
iritkatriel commentedon Jan 18, 2022
I am unable to reproduce this on 3.11:
>>> tokenize.untokenize(tokenize.generate_tokens(io.StringIO('#').readline)) '#'terryjreedy commentedon Jan 19, 2022
bpo-44667 tokenize.py emits spurious NEWLINE if file ends on a comment without a newline
Fixed on 3.11, 3.10, 3.9 Aug 2021.
Fix a 3.8 bug and adjust style.