Skip to content

Commit aaeeb36

Browse files
committed
Merge branch 'master' of github.com:pantsbuild/pants into pyenv
[ci skip-rust] [ci skip-build-wheels]
2 parents 12537b0 + 56b2562 commit aaeeb36

File tree

10 files changed

+135
-238
lines changed

10 files changed

+135
-238
lines changed

3rdparty/python/constraints.txt

+5-7
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Generated by build-support/bin/generate_lockfile.sh on Wed Oct 7 12:00:29 PDT 2020
1+
# Generated by build-support/bin/generate_lockfile.sh on Tue Oct 20 15:16:58 MST 2020
22
ansicolors==1.1.8
33
attrs==20.2.0
44
beautifulsoup4==4.6.3
@@ -10,8 +10,7 @@ dataclasses==0.6
1010
fasteners==0.15
1111
idna==2.10
1212
importlib-metadata==2.0.0
13-
iniconfig==1.0.1
14-
libcst==0.3.12
13+
iniconfig==1.1.1
1514
monotonic==1.5
1615
more-itertools==8.5.0
1716
mypy==0.782
@@ -31,12 +30,11 @@ pytest==6.0.2
3130
PyYAML==5.3.1
3231
requests==2.24.0
3332
setproctitle==1.1.10
34-
setuptools==50.3.0
33+
setuptools==50.3.2
3534
six==1.15.0
3635
toml==0.10.1
3736
typed-ast==1.4.1
3837
typing-extensions==3.7.4.2
39-
typing-inspect==0.6.0
40-
urllib3==1.25.10
38+
urllib3==1.25.11
4139
www-authenticate==0.9.2
42-
zipp==3.3.0
40+
zipp==3.3.1

3rdparty/python/requirements.txt

-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ ansicolors==1.1.8
22
beautifulsoup4>=4.6.0,<4.7
33
dataclasses==0.6
44
fasteners==0.15.0
5-
libcst>=0.3.12,<0.4
65

76
# The MyPy requirement should be maintained in lockstep with the requirement the Pants repo uses
87
# for the mypy task since it configures custom MyPy plugins. That requirement can be found via:
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,21 @@
11
# Copyright 2020 Pants project contributors (see CONTRIBUTORS.md).
22
# Licensed under the Apache License, Version 2.0 (see LICENSE).
33

4-
import logging
4+
import ast as ast3
55
import re
6+
import sys
67
from dataclasses import dataclass
7-
from typing import Optional, Set, Union
8+
from typing import Optional, Set, Tuple
89

9-
import libcst as cst
1010
from typed_ast import ast27
11-
from typing_extensions import Protocol
1211

1312
from pants.util.memo import memoized_property
1413
from pants.util.ordered_set import FrozenOrderedSet
1514
from pants.util.strutil import ensure_text
1615

17-
logger = logging.getLogger(__name__)
16+
17+
class ImportParseError(ValueError):
18+
pass
1819

1920

2021
@dataclass(frozen=True)
@@ -33,56 +34,41 @@ class ParsedPythonImports:
3334
def all_imports(self) -> FrozenOrderedSet[str]:
3435
return FrozenOrderedSet(sorted([*self.explicit_imports, *self.inferred_imports]))
3536

36-
@classmethod
37-
def empty(cls) -> "ParsedPythonImports":
38-
return cls(FrozenOrderedSet(), FrozenOrderedSet())
39-
40-
41-
class VisitorInterface(Protocol):
42-
explicit_imports: Set[str]
43-
inferred_imports: Set[str]
4437

45-
46-
def parse_file(*, filename: str, content: str, module_name: str) -> Optional[VisitorInterface]:
47-
"""Parse the file for python imports, and return a visitor with the imports it found."""
48-
# Parse all python 3 code with libCST. We parse assuming python 3 goes first, because we assume
49-
# most user code will be python 3.
50-
# TODO(#10921): identify the appropriate interpreter version beforehand!
38+
def parse_file(*, filename: str, content: str) -> Optional[Tuple]:
5139
try:
52-
# NB: Since all python 3 code is forwards-compatible with the 3.8 parser, and the import
53-
# syntax remains unchanged, we are safely able to use the 3.8 parser for parsing imports.
54-
# TODO(#10922): Support parsing python 3.9/3.10 with libCST!
55-
config = cst.PartialParserConfig(python_version="3.8")
56-
cst_tree = cst.parse_module(content, config=config)
57-
completed_cst_visitor = _CSTVisitor.visit_tree(cst_tree, module_name=module_name)
58-
return completed_cst_visitor
59-
except cst.ParserSyntaxError as e:
60-
# NB: When the python 3 ast visitor fails to parse python 2 syntax, it raises a
61-
# ParserSyntaxError. This may also occur when the file contains invalid python code. If we
62-
# successfully parse a python 2 file with a python 3 parser, that should not change the
63-
# imports we calculate.
64-
logger.debug(f"Failed to parse {filename} with python 3.8 libCST parser: {e}")
65-
66-
try:
67-
py27_tree = ast27.parse(content, filename=filename)
68-
completed_ast27_visitor = _Py27AstVisitor.visit_tree(py27_tree, module_name=module_name)
69-
return completed_ast27_visitor
70-
except SyntaxError as e:
71-
logger.debug(f"Failed to parse {filename} with python 2.7 typed-ast parser: {e}")
72-
73-
return None
40+
# NB: The Python 3 ast is generally backwards-compatible with earlier versions. The only
41+
# breaking change is `async` `await` becoming reserved keywords in Python 3.7 (deprecated
42+
# in 3.6). If the std-lib fails to parse, we could use typed-ast to try parsing with a
43+
# target version of Python 3.5, but we don't because Python 3.5 is almost EOL and has very
44+
# low usage.
45+
# We will also fail to parse Python 3.8 syntax if Pants is run with Python 3.6 or 3.7.
46+
# There is no known workaround for this, beyond users changing their `./pants` script to
47+
# always use >= 3.8.
48+
tree = ast3.parse(content, filename=filename)
49+
visitor_cls = _Py3AstVisitor if sys.version_info[:2] < (3, 8) else _Py38AstVisitor
50+
return tree, visitor_cls
51+
except SyntaxError:
52+
try:
53+
py27_tree = ast27.parse(content, filename=filename)
54+
return py27_tree, _Py27AstVisitor
55+
except SyntaxError:
56+
return None
7457

7558

7659
def find_python_imports(*, filename: str, content: str, module_name: str) -> ParsedPythonImports:
77-
completed_visitor = parse_file(filename=filename, content=content, module_name=module_name)
60+
parse_result = parse_file(filename=filename, content=content)
7861
# If there were syntax errors, gracefully early return. This is more user friendly than
7962
# propagating the exception. Dependency inference simply won't be used for that file, and
8063
# it'll be up to the tool actually being run (e.g. Pytest or Flake8) to error.
81-
if completed_visitor is None:
82-
return ParsedPythonImports.empty()
64+
if parse_result is None:
65+
return ParsedPythonImports(FrozenOrderedSet(), FrozenOrderedSet())
66+
tree, ast_visitor_cls = parse_result
67+
ast_visitor = ast_visitor_cls(module_name)
68+
ast_visitor.visit(tree)
8369
return ParsedPythonImports(
84-
explicit_imports=FrozenOrderedSet(sorted(completed_visitor.explicit_imports)),
85-
inferred_imports=FrozenOrderedSet(sorted(completed_visitor.inferred_imports)),
70+
explicit_imports=FrozenOrderedSet(sorted(ast_visitor.explicit_imports)),
71+
inferred_imports=FrozenOrderedSet(sorted(ast_visitor.inferred_imports)),
8672
)
8773

8874

@@ -91,77 +77,42 @@ def find_python_imports(*, filename: str, content: str, module_name: str) -> Par
9177
_INFERRED_IMPORT_REGEX = re.compile(r"^([a-z_][a-z_\d]*\.){2,}[a-zA-Z_]\w*$")
9278

9379

94-
class _Py27AstVisitor(ast27.NodeVisitor):
80+
class _BaseAstVisitor:
9581
def __init__(self, module_name: str) -> None:
9682
self._module_parts = module_name.split(".")
9783
self.explicit_imports: Set[str] = set()
9884
self.inferred_imports: Set[str] = set()
9985

100-
@classmethod
101-
def visit_tree(cls, tree: ast27.AST, module_name: str) -> "_Py27AstVisitor":
102-
visitor = cls(module_name)
103-
visitor.visit(tree)
104-
return visitor
105-
106-
def _maybe_add_inferred_import(self, s: str) -> None:
86+
def maybe_add_inferred_import(self, s: str) -> None:
10787
if _INFERRED_IMPORT_REGEX.match(s):
10888
self.inferred_imports.add(s)
10989

110-
def visit_Import(self, node: ast27.Import) -> None:
90+
def visit_Import(self, node) -> None:
11191
for alias in node.names:
11292
self.explicit_imports.add(alias.name)
11393

114-
def visit_ImportFrom(self, node: ast27.ImportFrom) -> None:
115-
rel_module = [] if node.module is None else [node.module]
116-
relative_level = 0 if node.level is None else node.level
117-
abs_module = ".".join(self._module_parts[0:-relative_level] + rel_module)
94+
def visit_ImportFrom(self, node) -> None:
95+
rel_module = node.module
96+
abs_module = ".".join(
97+
self._module_parts[0 : -node.level] + ([] if rel_module is None else [rel_module])
98+
)
11899
for alias in node.names:
119100
self.explicit_imports.add(f"{abs_module}.{alias.name}")
120101

121-
def visit_Str(self, node: ast27.Str) -> None:
122-
val = ensure_text(node.s)
123-
self._maybe_add_inferred_import(val)
124-
125102

126-
class _CSTVisitor(cst.CSTVisitor):
127-
def __init__(self, module_name: str) -> None:
128-
self._module_parts = module_name.split(".")
129-
self.explicit_imports: Set[str] = set()
130-
self.inferred_imports: Set[str] = set()
103+
class _Py27AstVisitor(ast27.NodeVisitor, _BaseAstVisitor):
104+
def visit_Str(self, node) -> None:
105+
val = ensure_text(node.s)
106+
self.maybe_add_inferred_import(val)
131107

132-
@classmethod
133-
def visit_tree(cls, tree: cst.Module, module_name: str) -> "_CSTVisitor":
134-
visitor = cls(module_name)
135-
tree.visit(visitor)
136-
return visitor
137108

138-
def _maybe_add_inferred_import(self, s: Union[str, bytes]) -> None:
139-
if isinstance(s, bytes):
140-
return
141-
if _INFERRED_IMPORT_REGEX.match(s):
142-
self.inferred_imports.add(s)
109+
class _Py3AstVisitor(ast3.NodeVisitor, _BaseAstVisitor):
110+
def visit_Str(self, node) -> None:
111+
self.maybe_add_inferred_import(node.s)
143112

144-
def _flatten_attribute_or_name(self, node: cst.BaseExpression) -> str:
145-
if isinstance(node, cst.Name):
146-
return node.value
147-
if not isinstance(node, cst.Attribute):
148-
raise TypeError(f"Unrecognized cst.BaseExpression subclass: {node}")
149-
inner = self._flatten_attribute_or_name(node.value)
150-
return f"{inner}.{node.attr.value}"
151113

152-
def visit_Import(self, node: cst.Import) -> None:
153-
for alias in node.names:
154-
self.explicit_imports.add(self._flatten_attribute_or_name(alias.name))
155-
156-
def visit_ImportFrom(self, node: cst.ImportFrom) -> None:
157-
rel_module = [] if node.module is None else [self._flatten_attribute_or_name(node.module)]
158-
relative_level = len(node.relative)
159-
abs_module = ".".join(self._module_parts[0:-relative_level] + rel_module)
160-
if isinstance(node.names, cst.ImportStar):
161-
self.explicit_imports.add(f"{abs_module}.*")
162-
else:
163-
for alias in node.names:
164-
self.explicit_imports.add(f"{abs_module}.{alias.name.value}")
165-
166-
def visit_SimpleString(self, node: cst.SimpleString) -> None:
167-
self._maybe_add_inferred_import(node.evaluated_value)
114+
class _Py38AstVisitor(ast3.NodeVisitor, _BaseAstVisitor):
115+
# Python 3.8 deprecated the Str node in favor of Constant.
116+
def visit_Constant(self, node) -> None:
117+
if isinstance(node.value, str):
118+
self.maybe_add_inferred_import(node.value)

src/python/pants/backend/python/dependency_inference/import_parser_test.py

+8-41
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
# Copyright 2020 Pants project contributors (see CONTRIBUTORS.md).
22
# Licensed under the Apache License, Version 2.0 (see LICENSE).
33

4-
import logging
4+
import sys
55
from textwrap import dedent
66

7-
from pants.backend.python.dependency_inference.import_parser import (
8-
ParsedPythonImports,
9-
find_python_imports,
10-
)
7+
import pytest
8+
9+
from pants.backend.python.dependency_inference.import_parser import find_python_imports
1110

1211

1312
def test_normal_imports() -> None:
@@ -147,6 +146,10 @@ def test_works_with_python2() -> None:
147146
assert set(imports.inferred_imports) == {"dep.from.bytes", "dep.from.str"}
148147

149148

149+
@pytest.mark.skipif(
150+
sys.version_info[:2] < (3, 8),
151+
reason="Cannot parse Python 3.8 unless Pants is run with Python 3.8.",
152+
)
150153
def test_works_with_python38() -> None:
151154
imports = find_python_imports(
152155
filename="foo.py",
@@ -166,39 +169,3 @@ def test_works_with_python38() -> None:
166169
)
167170
assert set(imports.explicit_imports) == {"demo", "project.demo.Demo"}
168171
assert set(imports.inferred_imports) == {"dep.from.str"}
169-
170-
171-
def test_debug_log_with_python39(caplog) -> None:
172-
"""We can't parse python 3.9 yet, so we want to be able to provide good debug log."""
173-
caplog.set_level(logging.DEBUG)
174-
imports = find_python_imports(
175-
filename="foo.py",
176-
# See https://www.python.org/dev/peps/pep-0614/ for the newly relaxed decorator expressions.
177-
content=dedent(
178-
"""\
179-
@buttons[0].clicked.connect
180-
def spam():
181-
...
182-
"""
183-
),
184-
module_name="project.app",
185-
)
186-
assert imports == ParsedPythonImports.empty()
187-
assert len(caplog.records) == 2
188-
messages = [rec.getMessage() for rec in caplog.records]
189-
190-
cst_parse_error = dedent(
191-
"""\
192-
Failed to parse foo.py with python 3.8 libCST parser: Syntax Error @ 1:9.
193-
Incomplete input. Encountered '[', but expected '(', or 'NEWLINE'.
194-
195-
@buttons[0].clicked.connect
196-
^"""
197-
)
198-
assert cst_parse_error == messages[0]
199-
200-
ast27_parse_error = dedent(
201-
"""\
202-
Failed to parse foo.py with python 2.7 typed-ast parser: invalid syntax (foo.py, line 1)"""
203-
)
204-
assert ast27_parse_error == messages[1]

src/python/pants/backend/python/util_rules/pex.py

+2
Original file line numberDiff line numberDiff line change
@@ -478,6 +478,8 @@ async def create_pex(
478478
"--no-pypi",
479479
*(f"--index={index}" for index in python_repos.indexes),
480480
*(f"--repo={repo}" for repo in python_repos.repos),
481+
"--cache-ttl",
482+
str(python_setup.resolver_http_cache_ttl),
481483
*request.additional_args,
482484
]
483485

0 commit comments

Comments
 (0)