Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve Python 2 only syntax detection #2592

Merged
merged 3 commits into from Nov 12, 2021
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGES.md
@@ -1,5 +1,12 @@
# Change Log

## _Unreleased_

### _Black_

- Warn about Python 2 deprecation in more cases by improving Python 2 only syntax
detection (#2592)

## 21.10b0

### _Black_
Expand Down
36 changes: 33 additions & 3 deletions src/black/__init__.py
Expand Up @@ -1132,8 +1132,17 @@ def get_features_used(node: Node) -> Set[Feature]: # noqa: C901
features.add(Feature.F_STRINGS)

elif n.type == token.NUMBER:
if "_" in n.value: # type: ignore
assert isinstance(n, Leaf)
if "_" in n.value:
features.add(Feature.NUMERIC_UNDERSCORES)
elif n.value.endswith(("L", "l")):
# Python 2: 10L
features.add(Feature.LONG_INT_LITERAL)
elif len(n.value) >= 2 and n.value[0] == "0" and n.value[1].isdigit():
# Python 2: 0123; 00123; ...
if not all(char == "0" for char in n.value):
# although we don't want to match 0000 or similar
features.add(Feature.OCTAL_INT_LITERAL)

elif n.type == token.SLASH:
if n.parent and n.parent.type in {
Expand Down Expand Up @@ -1171,10 +1180,31 @@ def get_features_used(node: Node) -> Set[Feature]: # noqa: C901
if argch.type in STARS:
features.add(feature)

elif n.type == token.PRINT_STMT:
# Python 2 only features (for its deprecation) except for integers, see above
elif n.type == syms.print_stmt:
features.add(Feature.PRINT_STMT)
elif n.type == token.EXEC_STMT:
elif n.type == syms.exec_stmt:
features.add(Feature.EXEC_STMT)
elif n.type == syms.tfpdef:
# def set_position((x, y), value):
# ...
features.add(Feature.AUTOMATIC_PARAMETER_UNPACKING)
elif n.type == syms.except_clause:
# try:
# ...
# except Exception, err:
# ...
if len(n.children) >= 4:
if n.children[-2].type == token.COMMA:
features.add(Feature.COMMA_STYLE_EXCEPT)
elif n.type == syms.raise_stmt:
# raise Exception, "msg"
if len(n.children) >= 4:
if n.children[-2].type == token.COMMA:
features.add(Feature.COMMA_STYLE_RAISE)
ichard26 marked this conversation as resolved.
Show resolved Hide resolved
elif n.type == token.BACKQUOTE:
# `i'm surprised this ever existed`
features.add(Feature.BACKQUOTE_REPR)

return features

Expand Down
12 changes: 12 additions & 0 deletions src/black/mode.py
Expand Up @@ -44,13 +44,25 @@ class Feature(Enum):
# temporary for Python 2 deprecation
PRINT_STMT = 200
EXEC_STMT = 201
AUTOMATIC_PARAMETER_UNPACKING = 202
COMMA_STYLE_EXCEPT = 203
COMMA_STYLE_RAISE = 204
LONG_INT_LITERAL = 205
OCTAL_INT_LITERAL = 206
BACKQUOTE_REPR = 207


VERSION_TO_FEATURES: Dict[TargetVersion, Set[Feature]] = {
TargetVersion.PY27: {
Feature.ASYNC_IDENTIFIERS,
Feature.PRINT_STMT,
Feature.EXEC_STMT,
Feature.AUTOMATIC_PARAMETER_UNPACKING,
Feature.COMMA_STYLE_EXCEPT,
Feature.COMMA_STYLE_RAISE,
Feature.LONG_INT_LITERAL,
Feature.OCTAL_INT_LITERAL,
Feature.BACKQUOTE_REPR,
},
TargetVersion.PY33: {Feature.UNICODE_LITERALS, Feature.ASYNC_IDENTIFIERS},
TargetVersion.PY34: {Feature.UNICODE_LITERALS, Feature.ASYNC_IDENTIFIERS},
Expand Down
3 changes: 0 additions & 3 deletions src/blib2to3/pgen2/token.py
Expand Up @@ -74,9 +74,6 @@
COLONEQUAL: Final = 59
N_TOKENS: Final = 60
NT_OFFSET: Final = 256
# temporary for Python 2 deprecation
PRINT_STMT: Final = 316
EXEC_STMT: Final = 288
# --end constants--

tok_name: Final[Dict[int, str]] = {}
Expand Down
90 changes: 90 additions & 0 deletions tests/data/python2_detection.py
@@ -0,0 +1,90 @@
# This uses a similar construction to the decorators.py test data file FYI.

print "hello, world!"

###

exec "print('hello, world!')"

###

def set_position((x, y), value):
pass

###

try:
pass
except Exception, err:
pass

###

raise RuntimeError, "I feel like crashing today :p"

###

`wow_these_really_did_exist`

###

10L

###

10l

###

0123

# output

print("hello python three!")

###

exec("I'm not sure if you can use exec like this but that's not important here!")

###

try:
pass
except make_exception(1, 2):
pass

###

try:
pass
except Exception as err:
pass

###

raise RuntimeError(make_msg(1, 2))

###

raise RuntimeError("boom!",)

###

def set_position(x, y, value):
pass

###

10

###

0

###

000

###

0o12
15 changes: 15 additions & 0 deletions tests/test_black.py
Expand Up @@ -2017,6 +2017,7 @@ def test_get_sources_with_stdin_filename_and_force_exclude(self) -> None:
)


@pytest.mark.python2
@pytest.mark.parametrize("explicit", [True, False], ids=["explicit", "autodetection"])
def test_python_2_deprecation_with_target_version(explicit: bool) -> None:
args = [
Expand All @@ -2032,6 +2033,20 @@ def test_python_2_deprecation_with_target_version(explicit: bool) -> None:
assert "DEPRECATION: Python 2 support will be removed" in result.stderr


@pytest.mark.python2
def test_python_2_deprecation_autodetection_extended() -> None:
# this test has a similar construction to test_get_features_used_decorator
python2, non_python2 = read_data("python2_detection")
for python2_case in python2.split("###"):
node = black.lib2to3_parse(python2_case)
assert black.detect_target_versions(node) == {TargetVersion.PY27}, python2_case
for non_python2_case in non_python2.split("###"):
node = black.lib2to3_parse(non_python2_case)
assert black.detect_target_versions(node) != {
TargetVersion.PY27
}, non_python2_case


with open(black.__file__, "r", encoding="utf-8") as _bf:
black_source_lines = _bf.readlines()

Expand Down