diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ce90aa35..567ee005 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,7 +10,10 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest] - python-version: ["3.7", "3.8", "3.9", "3.10"] + python-version: + - "3.8" + - "3.9" + - "3.10" include: - os: macos-latest python-version: "3.10" @@ -36,6 +39,7 @@ jobs: - name: compileall run: | python -We:invalid -m compileall -f -q lib/ etc/; + - name: pytest Mac OS if: ${{ matrix.os == 'macos-latest'}} # json report can't be installed on Py2, and make macos super slow. diff --git a/README.rst b/README.rst index f410db6a..2243a803 100644 --- a/README.rst +++ b/README.rst @@ -236,7 +236,7 @@ Compatibility ------------- Tested with: - - Python 3.7, 3.8, 3.9, 3.10 + - Python 3.8, 3.9, 3.10 - IPython 0.10, 0.11, 0.12, 0.13, 1.0, 1.2, 2.0, 2.1, 2.2, 2.3, 2.4, 3.0, 3.1, 3.2, 4.0., 7.11 (latest) - IPython (text console), IPython Notebook, Spyder diff --git a/lib/python/pyflyby/_autoimp.py b/lib/python/pyflyby/_autoimp.py index 88c9f09c..e2f5a93f 100644 --- a/lib/python/pyflyby/_autoimp.py +++ b/lib/python/pyflyby/_autoimp.py @@ -609,17 +609,13 @@ def visit_FunctionDef(self, node): # scope. # - Store the name in the current scope (but not visibly to # args/decorator_list). - if sys.version_info >= (3, 8): - assert node._fields == ('name', 'args', 'body', 'decorator_list', 'returns', 'type_comment'), node._fields - else: - assert node._fields == ('name', 'args', 'body', 'decorator_list', 'returns'), node._fields + assert node._fields == ('name', 'args', 'body', 'decorator_list', 'returns', 'type_comment'), node._fields with self._NewScopeCtx(include_class_scopes=True): self.visit(node.args) self.visit(node.decorator_list) if node.returns: self.visit(node.returns) - if sys.version_info >= (3, 8): - self._visit_typecomment(node.type_comment) + self._visit_typecomment(node.type_comment) old_in_FunctionDef = self._in_FunctionDef self._in_FunctionDef = True with self._NewScopeCtx(unhide_classdef=True): @@ -671,10 +667,7 @@ def foo(a #type: int self.visit(node) def visit_arguments(self, node): - if sys.version_info >= (3, 8): - assert node._fields == ('posonlyargs', 'args', 'vararg', 'kwonlyargs', 'kw_defaults', 'kwarg', 'defaults'), node._fields - else: - assert node._fields == ('args', 'vararg', 'kwonlyargs', 'kw_defaults', 'kwarg', 'defaults'), node._fields + assert node._fields == ('posonlyargs', 'args', 'vararg', 'kwonlyargs', 'kw_defaults', 'kwarg', 'defaults'), node._fields # Argument/parameter list. Note that the defaults should be # considered "Load"s from the upper scope, and the argument names are # "Store"s in the function scope. @@ -693,8 +686,7 @@ def visit_arguments(self, node): # Store arg names. self.visit(node.args) self.visit(node.kwonlyargs) - if sys.version_info >= (3, 8): - self.visit(node.posonlyargs) + self.visit(node.posonlyargs) # may be None. if node.vararg: self.visit(node.vararg) @@ -807,16 +799,12 @@ def visit_Name(self, node): self._visit_fullname(node.id, node.ctx) def visit_arg(self, node): - if sys.version_info >= (3, 8): - assert node._fields == ('arg', 'annotation', 'type_comment'), node._fields - else: - assert node._fields == ('arg', 'annotation'), node._fields + assert node._fields == ('arg', 'annotation', 'type_comment'), node._fields if node.annotation: self.visit(node.annotation) # Treat it like a Name node would from Python 2 self._visit_fullname(node.arg, ast.Param()) - if sys.version_info >= (3, 8): - self._visit_typecomment(node.type_comment) + self._visit_typecomment(node.type_comment) def visit_Attribute(self, node): name_revparts = [] @@ -1512,8 +1500,7 @@ def find_missing_imports(arg, namespaces): else: return [] # Parse the string into an AST. - kw = {} if sys.version_info < (3, 8) else {'type_comments': True} - node = ast.parse(arg, **kw) # may raise SyntaxError + node = ast.parse(arg, type_comments=True) # may raise SyntaxError # Get missing imports from AST. return _find_missing_imports_in_ast(node, namespaces) elif isinstance(arg, PythonBlock): diff --git a/lib/python/pyflyby/_parse.py b/lib/python/pyflyby/_parse.py index a7de3e3a..7c2dfa9b 100644 --- a/lib/python/pyflyby/_parse.py +++ b/lib/python/pyflyby/_parse.py @@ -24,19 +24,7 @@ from ast import Bytes -if sys.version_info >= (3, 8): - from ast import TypeIgnore, AsyncFunctionDef -else: - - # TypeIgnore, AsyncFunctionDef does not exist on Python 3.7 and before. thus - # we define a dummy TypeIgnore, AsyncFunctionDef just to simplify remaining - # code. - - class TypeIgnore: - pass - - class AsyncFunctionDef: - pass +from ast import TypeIgnore, AsyncFunctionDef def _is_comment_or_blank(line): @@ -104,37 +92,27 @@ def _iter_child_nodes_in_order_internal_1(node): assert node._fields == ("keys", "values") yield list(zip(node.keys, node.values)) elif isinstance(node, (ast.FunctionDef, AsyncFunctionDef)): - if sys.version_info >= (3, 8): - assert node._fields == ( - "name", - "args", - "body", - "decorator_list", - "returns", - "type_comment", - ), node._fields - res = ( - node.type_comment, - node.decorator_list, - node.args, - node.returns, - node.body, - ) - yield res - else: - assert node._fields == ('name', 'args', 'body', 'decorator_list', - 'returns'), node._fields - yield node.decorator_list, node.args, node.returns, node.body + assert node._fields == ( + "name", + "args", + "body", + "decorator_list", + "returns", + "type_comment", + ), node._fields + res = ( + node.type_comment, + node.decorator_list, + node.args, + node.returns, + node.body, + ) + yield res # node.name is a string, not an AST node elif isinstance(node, ast.arguments): - if sys.version_info >= (3, 8): - assert node._fields == ('posonlyargs', 'args', 'vararg', 'kwonlyargs', - 'kw_defaults', 'kwarg', 'defaults'), node._fields - args = node.posonlyargs + node.args - else: - assert node._fields == ('args', 'vararg', 'kwonlyargs', - 'kw_defaults', 'kwarg', 'defaults'), node._fields - args = node.args + assert node._fields == ('posonlyargs', 'args', 'vararg', 'kwonlyargs', + 'kw_defaults', 'kwarg', 'defaults'), node._fields + args = node.posonlyargs + node.args defaults = node.defaults or () num_no_default = len(args) - len(defaults) yield args[:num_no_default] @@ -187,22 +165,10 @@ def _flags_to_try(source, flags, auto_flags, mode): If ``auto_flags`` is True, then yield ``flags`` and ``flags ^ print_function``. """ flags = CompilerFlags(flags) - if sys.version_info >= (3, 8): - if re.search(r"# *type:", source): - flags = flags | CompilerFlags('type_comments') - yield flags - return - if not auto_flags: - yield flags - return - if mode == "eval": - if re.search(r"\bprint\b", source): - flags = flags | CompilerFlags("print_function") - yield flags - return + if re.search(r"# *type:", source): + flags = flags | CompilerFlags('type_comments') yield flags - if re.search(r"\bprint\b", source): - yield flags ^ CompilerFlags("print_function") + return def _parse_ast_nodes(text, flags, auto_flags, mode): @@ -348,14 +314,6 @@ def _annotate_ast_startpos(ast_node, parent_ast_node, minpos, text, flags): """ assert isinstance(ast_node, (ast.AST, str, TypeIgnore)), ast_node - # joined strings and children do not carry a column offset on pre-3.8 - # this prevent reformatting. - # set the column offset to the parent value before 3.8 - if (3, 7) < sys.version_info < (3, 8): - instances = (getattr(ast, "JoinedStr", None), ast.FormattedValue) - if ((isinstance(ast_node, instances) or isinstance(parent_ast_node, instances)) and ast_node.col_offset == -1) or isinstance(ast_node, ast.keyword): - ast_node.col_offset = parent_ast_node.col_offset - # First, traverse child nodes. If the first child node (recursively) is a # multiline string, then we need to transfer its information to this node. # Walk all nodes/fields of the AST. We implement this as a custom @@ -400,9 +358,8 @@ def _annotate_ast_startpos(ast_node, parent_ast_node, minpos, text, flags): if ast_node.col_offset >= 0: # In Python 3.8+, FunctionDef.lineno is the line with the def. To # account for decorators, we need the lineno of the first decorator - if (sys.version_info >= (3, 8) - and isinstance(ast_node, (ast.FunctionDef, ast.ClassDef, AsyncFunctionDef)) - and ast_node.decorator_list): + if (isinstance(ast_node, (ast.FunctionDef, ast.ClassDef, AsyncFunctionDef)) + and ast_node.decorator_list): delta = (ast_node.decorator_list[0].lineno-1, # The col_offset doesn't include the @ ast_node.decorator_list[0].col_offset - 1) @@ -486,9 +443,6 @@ def _annotate_ast_startpos(ast_node, parent_ast_node, minpos, text, flags): for _m in re.finditer("[bBrRuU]*[\"\']", start_line)]) target_str = ast_node.s - if isinstance(target_str, bytes) and sys.version_info[:2] == (3, 7): - target_str = target_str.decode() - # Loop over possible end_linenos. The first one we've identified is the # by far most likely one, but in theory it could be anywhere later in the # file. This could be because of a dastardly concatenated string like @@ -553,18 +507,8 @@ def _annotate_ast_startpos(ast_node, parent_ast_node, minpos, text, flags): candidate_str = _test_parse_string_literal(subtext, flags) if candidate_str is None: continue - if isinstance(candidate_str, bytes) and sys.version_info[:2] == (3, 7): - candidate_str = candidate_str.decode() maybe_fstring = False - try: - if (3, 7) <= sys.version_info <= (3, 8): - potential_start = text.lines[startpos.lineno - 1] - maybe_fstring = ("f'" in potential_start) or ( - 'f"' in potential_start - ) - except IndexError: - pass if target_str == candidate_str and target_str: # Success! @@ -589,17 +533,6 @@ def _annotate_ast_startpos(ast_node, parent_ast_node, minpos, text, flags): for (sq, sp) in startpos_candidates if sp in matched_prefix ] - if (3, 7) <= sys.version_info <= (3, 8): - if len(f_string_candidate_prefixes) == 1: - # we did not find the string but there is one fstring candidate starting it - - ast_node.startpos, ast_node.endpos = f_string_candidate_prefixes[0] - return True - elif isinstance(parent_ast_node, ast.JoinedStr): - self_pos = parent_ast_node.values.index(ast_node) - ast_node.startpos = parent_ast_node.values[self_pos - 1].startpos - ast_node.endpos = parent_ast_node.values[self_pos - 1].endpos - return True raise ValueError("Couldn't find exact position of %s" % (ast.dump(ast_node))) diff --git a/tests/test_autoimp.py b/tests/test_autoimp.py index f9e6415a..9fe0cf55 100644 --- a/tests/test_autoimp.py +++ b/tests/test_autoimp.py @@ -930,9 +930,7 @@ def f(): expected = ['use', 'y'] assert expected == result -@pytest.mark.skipif( - sys.version_info < (3, 8), - reason="Python 3.8+-only syntax.") + def test_find_missing_imports_positional_only_args_1(): code = dedent(""" def func(x, /, y): @@ -1225,11 +1223,6 @@ def test_find_missing_imports_tuple_ellipsis_type_1(): assert expected == result -# Only Python 3.8 includes type comments in the ast, so we only support this -# there (see issue #31). -@pytest.mark.skipif( - sys.version_info < (3, 8), - reason="Python 3.8+-only support.") def test_scan_for_import_issues_type_comment_1(): code = dedent(""" from typing import Sequence @@ -1242,11 +1235,6 @@ def foo(strings # type: Sequence[str] assert missing == [] -# Only Python 3.8 includes type comments in the ast, so we only support this -# there (see issue #31, 171, 174). -@pytest.mark.skipif( - sys.version_info < (3, 8), - reason="Python 3.8+-only support.") def test_scan_for_import_issues_type_comment_2(): code = dedent(""" from typing import Sequence @@ -1259,9 +1247,6 @@ def foo(strings): assert missing == [] -@pytest.mark.skipif( - sys.version_info < (3, 8), - reason="Python 3.8+-only support.") def test_scan_for_import_issues_type_comment_3(): code = dedent(""" def foo(strings): @@ -1273,9 +1258,6 @@ def foo(strings): assert missing == [(1, DottedIdentifier('Sequence'))] -@pytest.mark.skipif( - sys.version_info < (3, 8), - reason="Python 3.8+-only support.") def test_scan_for_import_issues_type_comment_4(): code = dedent(""" from typing import Sequence, Tuple @@ -1287,12 +1269,7 @@ def foo(strings): assert unused == [(2, Import('from typing import Tuple'))] assert missing == [] -# Python 3.8 uses the correct line number for multiline strings (the first -# line), making _annotate_ast_startpos irrelevant. Otherwise, the logic for -# getting this right is too hard. See issue #12. -@pytest.mark.skipif( - sys.version_info < (3, 8), - reason="Python 3.8+-only support.") + def test_scan_for_import_issues_multiline_string_1(): code = dedent(''' x = ( diff --git a/tests/test_interactive.py b/tests/test_interactive.py index d6262a4d..4f338f3e 100644 --- a/tests/test_interactive.py +++ b/tests/test_interactive.py @@ -1039,9 +1039,7 @@ def ipython(template, **kwargs): the template. Assert that the result matches. """ __tracebackhide__ = True - if sys.version_info[:2] >= (3, 8): - # more recent IPython have a different formatting. - template = template.replace("... in ...", "... line ...") + template = template.replace("... in ...", "... line ...") template = dedent(template).strip() input, expected = parse_template(template, clear_tab_completions=_IPYTHON_VERSION>=(7,)) args = kwargs.pop("args", ()) diff --git a/tests/test_parse.py b/tests/test_parse.py index 2fb4b18b..d712eb61 100644 --- a/tests/test_parse.py +++ b/tests/test_parse.py @@ -21,7 +21,6 @@ else: print_function_flag = CompilerFlags.from_int(0x100000) - def test_PythonBlock_FileText_1(): text = FileText( dedent( @@ -496,15 +495,9 @@ def test_PythonBlock_flags_type_comment_1(): """ ).lstrip() ) - if sys.version_info >= (3, 8): - # Includes the type_comments flag - with warnings.catch_warnings(): - warnings.simplefilter("ignore", DeprecationWarning) - assert block.flags == CompilerFlags(0x01000) - else: - with warnings.catch_warnings(): - warnings.simplefilter("ignore", DeprecationWarning) - assert block.flags == CompilerFlags(0x0000) + with warnings.catch_warnings(): + warnings.simplefilter("ignore", DeprecationWarning) + assert block.flags == CompilerFlags(0x01000) def test_PythonBlock_flags_type_comment_fail_transform(): @@ -556,13 +549,13 @@ async def func(self, location: str) -> bytes: """ ) -if sys.version_info >= (3, 8): - examples_transform.append( - # positional only - """ - def f(x, y=None, / , z=None): - pass""" - ) +examples_transform.append( +# positional only +""" +def f(x, y=None, / , z=None): + pass""" + ) + @pytest.mark.parametrize("source", examples_transform) def test_PythonBlock_flags_type_comment_ignore_fails_transform(source): @@ -917,7 +910,6 @@ def test_PythonBlock_compound_statements_1(): assert block.statements == expected -@pytest.mark.skipif(sys.version_info < (3, 8), reason="Does not work pre-3.8") def test_str_lineno_expression(): # Code that used to be in test_interactive. _annotate_ast_startpos does # not work on it because it cannot handle multiline strings that contained @@ -1266,12 +1258,8 @@ def test_PythonStatement_flags_1(): with warnings.catch_warnings(): warnings.simplefilter("ignore", DeprecationWarning) assert s1.block.source_flags == CompilerFlags(0) - if sys.version_info >= (3, 8): - assert s0.block.flags == CompilerFlags("unicode_literals", "division",) - assert s1.block.flags == CompilerFlags("unicode_literals", "division",) - else: - assert s0.block.flags == CompilerFlags("unicode_literals", "division") - assert s1.block.flags == CompilerFlags("unicode_literals", "division") + assert s0.block.flags == CompilerFlags("unicode_literals", "division",) + assert s1.block.flags == CompilerFlags("unicode_literals", "division",) def test_PythonStatement_auto_flags_1():