Skip to content

Commit

Permalink
Improve Python 2 only syntax detection (GH-2592)
Browse files Browse the repository at this point in the history
* Improve Python 2 only syntax detection

First of all this fixes a mistake I made in Python 2 deprecation PR
using token.* to check for print/exec statements. Turns out that
for nodes with a type value higher than 256 its numeric type isn't
guaranteed to be constant. Using syms.* instead fixes this.

Also add support for the following cases:

    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

* Add octal support, more test cases, and fixup long ints

Co-authored-by: Jelle Zijlstra <jelle.zijlstra@gmail.com>

Co-authored-by: Jelle Zijlstra <jelle.zijlstra@gmail.com>
  • Loading branch information
ichard26 and JelleZijlstra committed Nov 12, 2021
1 parent f297c46 commit 0753d99
Show file tree
Hide file tree
Showing 6 changed files with 157 additions and 6 deletions.
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)
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

0 comments on commit 0753d99

Please sign in to comment.