Hello community, here is the log from the commit of package python-libcst for openSUSE:Factory checked in at 2020-09-04 10:58:35 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-libcst (Old) and /work/SRC/openSUSE:Factory/.python-libcst.new.3399 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-libcst" Fri Sep 4 10:58:35 2020 rev:2 rq:829619 version:0.3.10 Changes: -------- --- /work/SRC/openSUSE:Factory/python-libcst/python-libcst.changes 2020-08-25 12:40:11.129430507 +0200 +++ /work/SRC/openSUSE:Factory/.python-libcst.new.3399/python-libcst.changes 2020-09-04 10:59:42.726642648 +0200 @@ -1,0 +2,24 @@ +Tue Aug 25 11:33:22 UTC 2020 - Benjamin Greiner <[email protected]> + +- Update to v0.3.10 + Added + * Handle string annotations in ScopeProvider #373 + * Add is_annotation subtype for Access inreferences. #372 + Updated + * Call pyre query with noninteractive logging #371 + * Replace matchers with explicit visitation in gatherers #366 + * Include missing test data in install #365 + Fixed + * Spaces around walrus operator are not required #368 + * SaveMachedNode now matches with trailing empty wildcards #356 + * Correctly extract wildcard matchers #355 +- clean specfile multibuild +- don't require own package on test flavor +- require dataclasses for Python 3.6 distros +- despite gh#Instagram/LibCST#365 says it's fixed, we still need to + regenerate the code for tests + => don't skip tests which actually pass after codegen +- can use plain unittest again, same as upstream +- define skip_python2 for Leap repositories + +------------------------------------------------------------------- Old: ---- codemod_formatter_error_input.py.txt libcst-0.3.9.tar.gz New: ---- libcst-0.3.10.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-libcst.spec ++++++ --- /var/tmp/diff_new_pack.SAVkFh/_old 2020-09-04 10:59:46.066644438 +0200 +++ /var/tmp/diff_new_pack.SAVkFh/_new 2020-09-04 10:59:46.070644440 +0200 @@ -17,6 +17,7 @@ %{?!python_module:%define python_module() python-%{**} python3-%{**}} +%define skip_python2 1 %global flavor @BUILD_FLAVOR@%{nil} %if "%{flavor}" == "test" %define psuffix -test @@ -26,31 +27,35 @@ %bcond_with test %endif Name: python-libcst%{psuffix} -Version: 0.3.9 +Version: 0.3.10 Release: 0 Summary: Python 3.5+ concrete syntax tree with AST-like properties License: MIT URL: https://github.com/Instagram/LibCST Source: https://files.pythonhosted.org/packages/source/l/libcst/libcst-%{version}.tar.gz -Source1: https://raw.githubusercontent.com/Instagram/LibCST/master/libcst/codemod/tests/codemod_formatter_error_input.py.txt -# all the deps over setuptools are for the generateor fix -BuildRequires: %{python_module black} +# isort needed for the code regeneration, code mod also on non test flavor BuildRequires: %{python_module isort} BuildRequires: %{python_module setuptools} -BuildRequires: %{python_module typing-inspect >= 0.4.0} -BuildRequires: %{python_module typing_extensions >= 3.7.4.2} BuildRequires: fdupes BuildRequires: python-rpm-macros Requires: python-PyYAML >= 5.2 Requires: python-typing-inspect >= 0.4.0 Requires: python-typing_extensions >= 3.7.4.2 BuildArch: noarch +%if %{python_version_nodots} < 37 +Requires: python-dataclasses +%endif %if %{with test} +%if %{python_version_nodots} < 37 +BuildRequires: %{python_module dataclasses} +%endif BuildRequires: %{python_module PyYAML >= 5.2} +# black needed for tests and the code regeneration +BuildRequires: %{python_module black} BuildRequires: %{python_module hypothesis >= 4.36.0} BuildRequires: %{python_module hypothesmith >= 0.0.4} -BuildRequires: %{python_module libcst >= %{version}} -BuildRequires: %{python_module pytest} +BuildRequires: %{python_module typing-inspect >= 0.4.0} +BuildRequires: %{python_module typing_extensions >= 3.7.4.2} %endif %python_subpackages @@ -59,10 +64,10 @@ %prep %setup -q -n libcst-%{version} -cp %{SOURCE1} libcst/codemod/tests/ +# fix executable sed -i 's/"python"/"python3"/' libcst/codemod/tests/test_codemod_cli.py # https://github.com/Instagram/LibCST/issues/331 -[ isort --version | grep "VERSION 4" ] || sed -i 's/"isort", "-y"/"isort"/' libcst/codegen/generate.py +isort --version | grep "VERSION 4" || sed -i 's/"isort", "-y"/"isort"/' libcst/codegen/generate.py # Depends on optional pyre rm \ @@ -73,30 +78,25 @@ # Test result depends on pyre sed -i 's/"error: .* stack:",/"Transformed 1 files successfully.",/' libcst/codemod/tests/test_codemod_cli.py +%if !%{with test} %build %python_build +%endif %install %if !%{with test} %python_install +%python_expand %fdupes %{buildroot}%{$python_sitelib} +%endif + +%if %{with test} +%check %{python_expand # https://github.com/Instagram/LibCST/issues/331 -export PYTHONPATH=%{buildroot}%{$python_sitelib} $python -m libcst.codegen.generate visitors $python -m libcst.codegen.generate return_types $python -m libcst.codegen.generate matchers - -rm -r %{buildroot}%{$python_sitelib}/libcst/tests/ \ - %{buildroot}%{$python_sitelib}/libcst/*/tests/ \ - %{buildroot}%{$python_sitelib}/libcst/*/*/tests/ - -%fdupes %{buildroot}%{$python_sitelib} } -%endif - -%check -%if %{with test} -# the two tests pass in non-multibuild setup, just skip them here -%pytest -k 'not test_codegen_clean_return_types and not test_codegen_clean_visitor_functions' +%pyunittest -v %endif %if !%{with test} ++++++ libcst-0.3.9.tar.gz -> libcst-0.3.10.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libcst-0.3.9/PKG-INFO new/libcst-0.3.10/PKG-INFO --- old/libcst-0.3.9/PKG-INFO 2020-08-07 20:46:05.000000000 +0200 +++ new/libcst-0.3.10/PKG-INFO 2020-08-17 20:25:13.000000000 +0200 @@ -1,6 +1,6 @@ Metadata-Version: 2.1 Name: libcst -Version: 0.3.9 +Version: 0.3.10 Summary: A concrete syntax tree with AST-like properties for Python 3.5, 3.6, 3.7 and 3.8 programs. Home-page: https://github.com/Instagram/LibCST License: MIT diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libcst-0.3.9/libcst/_nodes/expression.py new/libcst-0.3.10/libcst/_nodes/expression.py --- old/libcst-0.3.9/libcst/_nodes/expression.py 2020-08-07 20:39:26.000000000 +0200 +++ new/libcst-0.3.10/libcst/_nodes/expression.py 2020-08-14 21:02:21.000000000 +0200 @@ -3680,19 +3680,6 @@ #: Whitespace after the walrus operator, but before the value. whitespace_after_walrus: BaseParenthesizableWhitespace = SimpleWhitespace.field(" ") - def _validate(self) -> None: - super(NamedExpr, self)._validate() - if ( - self.whitespace_before_walrus.empty - and not self.target._safe_to_use_with_word_operator(ExpressionPosition.LEFT) - ): - raise CSTValidationError("Must have at least one space after target.") - if ( - self.whitespace_after_walrus.empty - and not self.value._safe_to_use_with_word_operator(ExpressionPosition.RIGHT) - ): - raise CSTValidationError("Must have at least one space before value.") - def _visit_and_replace_children(self, visitor: CSTVisitorT) -> "NamedExpr": return NamedExpr( lpar=visit_sequence(self, "lpar", self.lpar, visitor), diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libcst-0.3.9/libcst/_nodes/tests/test_namedexpr.py new/libcst-0.3.10/libcst/_nodes/tests/test_namedexpr.py --- old/libcst-0.3.9/libcst/_nodes/tests/test_namedexpr.py 2020-08-04 22:04:41.000000000 +0200 +++ new/libcst-0.3.10/libcst/_nodes/tests/test_namedexpr.py 2020-08-14 21:02:21.000000000 +0200 @@ -87,6 +87,20 @@ "parser": _parse_statement_force_38, "expected_position": None, }, + { + "node": cst.If( + test=cst.NamedExpr( + target=cst.Name(value="x"), + value=cst.Integer(value="1"), + whitespace_before_walrus=cst.SimpleWhitespace(""), + whitespace_after_walrus=cst.SimpleWhitespace(""), + ), + body=cst.SimpleStatementSuite(body=[cst.Pass()]), + ), + "code": "if x:=1: pass\n", + "parser": _parse_statement_force_38, + "expected_position": None, + }, ) ) def test_valid(self, **kwargs: Any) -> None: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libcst-0.3.9/libcst/_version.py new/libcst-0.3.10/libcst/_version.py --- old/libcst-0.3.9/libcst/_version.py 2020-08-07 20:39:26.000000000 +0200 +++ new/libcst-0.3.10/libcst/_version.py 2020-08-17 18:40:01.000000000 +0200 @@ -4,4 +4,4 @@ # LICENSE file in the root directory of this source tree. -LIBCST_VERSION: str = "0.3.9" +LIBCST_VERSION: str = "0.3.10" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libcst-0.3.9/libcst/codemod/tests/codemod_formatter_error_input.py.txt new/libcst-0.3.10/libcst/codemod/tests/codemod_formatter_error_input.py.txt --- old/libcst-0.3.9/libcst/codemod/tests/codemod_formatter_error_input.py.txt 1970-01-01 01:00:00.000000000 +0100 +++ new/libcst-0.3.10/libcst/codemod/tests/codemod_formatter_error_input.py.txt 2020-08-04 22:04:41.000000000 +0200 @@ -0,0 +1,15 @@ +# Copyright (c) Facebook, Inc. and its affiliates. +# +# This source code is licensed under the MIT license found in the +# LICENSE file in the root directory of this source tree. +# +# pyre-strict + +import subprocess +from contextlib import AsyncExitStack + + +def fun() -> None: + # this is an explicit syntax error to cause formatter error + async with AsyncExitStack() as stack: + stack diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libcst-0.3.9/libcst/codemod/visitors/_gather_comments.py new/libcst-0.3.10/libcst/codemod/visitors/_gather_comments.py --- old/libcst-0.3.9/libcst/codemod/visitors/_gather_comments.py 2020-08-04 22:04:41.000000000 +0200 +++ new/libcst-0.3.10/libcst/codemod/visitors/_gather_comments.py 2020-08-14 21:02:21.000000000 +0200 @@ -7,7 +7,6 @@ from typing import Dict, Pattern, Union import libcst as cst -import libcst.matchers as m from libcst.codemod._context import CodemodContext from libcst.codemod._visitor import ContextAwareVisitor from libcst.metadata import PositionProvider @@ -37,11 +36,21 @@ self._comment_matcher: Pattern[str] = re.compile(comment_regex) - @m.visit(m.EmptyLine(comment=m.DoesNotMatch(None))) - @m.visit(m.TrailingWhitespace(comment=m.DoesNotMatch(None))) - def visit_comment(self, node: Union[cst.EmptyLine, cst.TrailingWhitespace]) -> None: + def visit_EmptyLine(self, node: cst.EmptyLine) -> bool: + if node.comment is not None: + self.handle_comment(node) + return False + + def visit_TrailingWhitespace(self, node: cst.TrailingWhitespace) -> bool: + if node.comment is not None: + self.handle_comment(node) + return False + + def handle_comment( + self, node: Union[cst.EmptyLine, cst.TrailingWhitespace] + ) -> None: comment = node.comment - assert comment is not None # hello, type checker + assert comment is not None # ensured by callsites above if not self._comment_matcher.match(comment.value): return line = self.get_metadata(PositionProvider, comment).start.line diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libcst-0.3.9/libcst/codemod/visitors/_gather_exports.py new/libcst-0.3.10/libcst/codemod/visitors/_gather_exports.py --- old/libcst-0.3.9/libcst/codemod/visitors/_gather_exports.py 2020-08-04 22:04:41.000000000 +0200 +++ new/libcst-0.3.10/libcst/codemod/visitors/_gather_exports.py 2020-08-15 00:06:19.000000000 +0200 @@ -3,13 +3,12 @@ # This source code is licensed under the MIT license found in the # LICENSE file in the root directory of this source tree. # -from typing import Set +from typing import Set, Union -import libcst -import libcst.matchers as m +import libcst as cst from libcst.codemod._context import CodemodContext from libcst.codemod._visitor import ContextAwareVisitor -from libcst.helpers import ensure_type, get_full_name_for_node +from libcst.helpers import get_full_name_for_node class GatherExportsVisitor(ContextAwareVisitor): @@ -32,7 +31,8 @@ def __init__(self, context: CodemodContext) -> None: super().__init__(context) - # Track any re-exported objects in an __all__ reference + # Track any re-exported objects in an __all__ reference and whether + # they're defined or not self.explicit_exported_objects: Set[str] = set() # Presumably at some point in the future it would be useful to grab @@ -43,68 +43,87 @@ # that we have a reasonable place to put implicit objects in the future. # Internal bookkeeping - self._in_assignment: int = 0 - self._in_list: int = 0 + self._is_assigned_export: Set[Union[cst.Tuple, cst.List, cst.Set]] = set() + self._in_assigned_export: Set[Union[cst.Tuple, cst.List, cst.Set]] = set() - def visit_AnnAssign(self, node: libcst.AnnAssign) -> bool: - target = get_full_name_for_node(node.target) - if target == "__all__": - self._in_assignment += 1 - return True + def visit_AnnAssign(self, node: cst.AnnAssign) -> bool: + value = node.value + if value: + if self._handle_assign_target(node.target, value): + return True return False - def leave_AnnAssign(self, original_node: libcst.AnnAssign) -> None: - self._in_assignment -= 1 - - def visit_Assign(self, node: libcst.Assign) -> bool: + def visit_Assign(self, node: cst.Assign) -> bool: for target_node in node.targets: - target = get_full_name_for_node(target_node.target) - if target == "__all__": - self._in_assignment += 1 + if self._handle_assign_target(target_node.target, node.value): return True return False - def leave_Assign(self, original_node: libcst.Assign) -> None: - self._in_assignment -= 1 + def _handle_assign_target( + self, target: cst.BaseExpression, value: cst.BaseExpression + ) -> bool: + target_name = get_full_name_for_node(target) + if target_name == "__all__": + # Assignments such as `__all__ = ["os"]` + # or `__all__ = exports = ["os"]` + if isinstance(value, (cst.List, cst.Tuple, cst.Set)): + self._is_assigned_export.add(value) + return True + elif isinstance(target, cst.Tuple) and isinstance(value, cst.Tuple): + # Assignments such as `__all__, x = ["os"], []` + for element_idx, element_node in enumerate(target.elements): + element_name = get_full_name_for_node(element_node.value) + if element_name == "__all__": + element_value = value.elements[element_idx].value + if isinstance(element_value, (cst.List, cst.Tuple, cst.Set)): + self._is_assigned_export.add(value) + self._is_assigned_export.add(element_value) + return True + return False - def visit_List(self, node: libcst.List) -> bool: - self._in_list += 1 - # Only visit list/set entries when we're in an __all__ - # assignment. We gate also by internal counters, so this - # is simply an optimization. - return self._in_assignment == 1 and self._in_list == 1 - - def leave_List(self, original_node: libcst.List) -> None: - self._in_list -= 1 - - def visit_Tuple(self, node: libcst.Tuple) -> bool: - self._in_list += 1 - # Only visit list/set entries when we're in an __all__ - # assignment. We gate also by internal counters, so this - # is simply an optimization. - return self._in_assignment == 1 and self._in_list == 1 - - def leave_Tuple(self, original_node: libcst.Tuple) -> None: - self._in_list -= 1 - - def visit_Set(self, node: libcst.Set) -> bool: - # Only visit list/set entries when we're in an __all__ - # assignment. We gate also by internal counters, so this - # is simply an optimization. - self._in_list += 1 - return self._in_assignment == 1 and self._in_list == 1 - - def leave_Set(self, original_node: libcst.Set) -> None: - self._in_list -= 1 - - def visit_Element(self, node: libcst.Element) -> bool: - # See if this is a entry that is a string. - extraction = self.extract( - node, m.Element(m.SaveMatchedNode(m.SimpleString(), "string")) - ) - if extraction: - string = ensure_type(extraction["string"], libcst.SimpleString) - self.explicit_exported_objects.add(string.evaluated_value) + def visit_List(self, node: cst.List) -> bool: + if node in self._is_assigned_export: + self._in_assigned_export.add(node) + return True + return False - # Don't need to visit children + def leave_List(self, original_node: cst.List) -> None: + self._is_assigned_export.discard(original_node) + self._in_assigned_export.discard(original_node) + + def visit_Tuple(self, node: cst.Tuple) -> bool: + if node in self._is_assigned_export: + self._in_assigned_export.add(node) + return True return False + + def leave_Tuple(self, original_node: cst.Tuple) -> None: + self._is_assigned_export.discard(original_node) + self._in_assigned_export.discard(original_node) + + def visit_Set(self, node: cst.Set) -> bool: + if node in self._is_assigned_export: + self._in_assigned_export.add(node) + return True + return False + + def leave_Set(self, original_node: cst.Set) -> None: + self._is_assigned_export.discard(original_node) + self._in_assigned_export.discard(original_node) + + def visit_SimpleString(self, node: cst.SimpleString) -> bool: + self._handle_string_export(node) + return False + + def visit_ConcatenatedString(self, node: cst.ConcatenatedString) -> bool: + self._handle_string_export(node) + return False + + def _handle_string_export( + self, node: Union[cst.SimpleString, cst.ConcatenatedString] + ) -> None: + if self._in_assigned_export: + name = node.evaluated_value + if name is None: + return + self.explicit_exported_objects.add(name) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libcst-0.3.9/libcst/codemod/visitors/_gather_string_annotation_names.py new/libcst-0.3.10/libcst/codemod/visitors/_gather_string_annotation_names.py --- old/libcst-0.3.9/libcst/codemod/visitors/_gather_string_annotation_names.py 2020-08-04 22:04:41.000000000 +0200 +++ new/libcst-0.3.10/libcst/codemod/visitors/_gather_string_annotation_names.py 2020-08-14 21:02:21.000000000 +0200 @@ -3,7 +3,7 @@ # This source code is licensed under the MIT license found in the # LICENSE file in the root directory of this source tree. -from typing import Set, Union, cast +from typing import Collection, List, Set, Union, cast import libcst as cst import libcst.matchers as m @@ -13,14 +13,6 @@ FUNCS_CONSIDERED_AS_STRING_ANNOTATIONS = {"typing.TypeVar"} -ANNOTATION_MATCHER: m.BaseMatcherNode = m.Annotation() | m.Call( - metadata=m.MatchMetadataIfTrue( - QualifiedNameProvider, - lambda qualnames: any( - qn.name in FUNCS_CONSIDERED_AS_STRING_ANNOTATIONS for qn in qualnames - ), - ) -) class GatherNamesFromStringAnnotationsVisitor(ContextAwareVisitor): @@ -35,14 +27,45 @@ METADATA_DEPENDENCIES = (QualifiedNameProvider,) - def __init__(self, context: CodemodContext) -> None: + def __init__( + self, + context: CodemodContext, + typing_functions: Collection[str] = FUNCS_CONSIDERED_AS_STRING_ANNOTATIONS, + ) -> None: super().__init__(context) - + self._typing_functions: Collection[str] = typing_functions + self._annotation_stack: List[cst.CSTNode] = [] #: The set of names collected from string literals. self.names: Set[str] = set() - @m.call_if_inside(ANNOTATION_MATCHER) - @m.visit(m.ConcatenatedString()) + def visit_Annotation(self, node: cst.Annotation) -> bool: + self._annotation_stack.append(node) + return True + + def leave_Annotation(self, original_node: cst.Annotation) -> None: + self._annotation_stack.pop() + + def visit_Call(self, node: cst.Call) -> bool: + qnames = self.get_metadata(QualifiedNameProvider, node) + if any(qn.name in self._typing_functions for qn in qnames): + self._annotation_stack.append(node) + return True + return False + + def leave_Call(self, original_node: cst.Call) -> None: + if self._annotation_stack and self._annotation_stack[-1] == original_node: + self._annotation_stack.pop() + + def visit_ConcatenatedString(self, node: cst.ConcatenatedString) -> bool: + if self._annotation_stack: + self.handle_any_string(node) + return False + + def visit_SimpleString(self, node: cst.SimpleString) -> bool: + if self._annotation_stack: + self.handle_any_string(node) + return False + def handle_any_string( self, node: Union[cst.SimpleString, cst.ConcatenatedString] ) -> None: @@ -73,9 +96,3 @@ ) } self.names.update(names) - - @m.call_if_inside(ANNOTATION_MATCHER) - @m.call_if_not_inside(m.ConcatenatedString()) - @m.visit(m.SimpleString()) - def handle_simple_string(self, node: cst.SimpleString) -> None: - self.handle_any_string(node) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libcst-0.3.9/libcst/codemod/visitors/_gather_unused_imports.py new/libcst-0.3.10/libcst/codemod/visitors/_gather_unused_imports.py --- old/libcst-0.3.9/libcst/codemod/visitors/_gather_unused_imports.py 2020-08-04 22:04:41.000000000 +0200 +++ new/libcst-0.3.10/libcst/codemod/visitors/_gather_unused_imports.py 2020-08-14 21:02:21.000000000 +0200 @@ -4,20 +4,23 @@ # LICENSE file in the root directory of this source tree. # -from typing import Iterable, Set, Tuple, Union +from typing import Collection, Iterable, Set, Tuple, Union import libcst as cst -import libcst.matchers as m from libcst.codemod._context import CodemodContext from libcst.codemod._visitor import ContextAwareVisitor from libcst.codemod.visitors._gather_exports import GatherExportsVisitor from libcst.codemod.visitors._gather_string_annotation_names import ( + FUNCS_CONSIDERED_AS_STRING_ANNOTATIONS, GatherNamesFromStringAnnotationsVisitor, ) from libcst.metadata import ProviderT, ScopeProvider from libcst.metadata.scope_provider import _gen_dotted_names +MODULES_IGNORED_BY_DEFAULT = {"__future__"} + + class GatherUnusedImportsVisitor(ContextAwareVisitor): """ Collects all imports from a module not directly used in the same module. @@ -37,9 +40,16 @@ ScopeProvider, ) - def __init__(self, context: CodemodContext) -> None: + def __init__( + self, + context: CodemodContext, + ignored_modules: Collection[str] = MODULES_IGNORED_BY_DEFAULT, + typing_functions: Collection[str] = FUNCS_CONSIDERED_AS_STRING_ANNOTATIONS, + ) -> None: super().__init__(context) + self._ignored_modules: Collection[str] = ignored_modules + self._typing_functions = typing_functions self._string_annotation_names: Set[str] = set() self._exported_names: Set[str] = set() #: Contains a set of (alias, parent_import) pairs that are not used @@ -52,18 +62,27 @@ export_collector = GatherExportsVisitor(self.context) node.visit(export_collector) self._exported_names = export_collector.explicit_exported_objects - annotation_visitor = GatherNamesFromStringAnnotationsVisitor(self.context) + annotation_visitor = GatherNamesFromStringAnnotationsVisitor( + self.context, typing_functions=self._typing_functions + ) node.visit(annotation_visitor) self._string_annotation_names = annotation_visitor.names return True - @m.visit( - m.Import() - | m.ImportFrom( - module=m.DoesNotMatch(m.Name("__future__")), - names=m.DoesNotMatch(m.ImportStar()), - ) - ) + def visit_Import(self, node: cst.Import) -> bool: + self.handle_import(node) + return False + + def visit_ImportFrom(self, node: cst.ImportFrom) -> bool: + module = node.module + if ( + not isinstance(node.names, cst.ImportStar) + and module is not None + and module.value not in self._ignored_modules + ): + self.handle_import(node) + return False + def handle_import(self, node: Union[cst.Import, cst.ImportFrom]) -> None: names = node.names assert not isinstance(names, cst.ImportStar) # hello, type checker diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libcst-0.3.9/libcst/codemod/visitors/tests/test_gather_exports.py new/libcst-0.3.10/libcst/codemod/visitors/tests/test_gather_exports.py --- old/libcst-0.3.9/libcst/codemod/visitors/tests/test_gather_exports.py 2020-08-04 22:04:41.000000000 +0200 +++ new/libcst-0.3.10/libcst/codemod/visitors/tests/test_gather_exports.py 2020-08-15 00:06:19.000000000 +0200 @@ -26,6 +26,10 @@ list_of_str = ["foo", "bar", "baz"] + set_of_str = {"foo", "bar", "baz"} + + tuple_of_str = ("foo", "bar", "baz") + another: List[str] = ["foobar", "foobarbaz"] """ @@ -100,12 +104,34 @@ gatherer = self.gather_exports(code) self.assertEqual(gatherer.explicit_exported_objects, {"bar", "baz"}) - def test_gather_exports_ignore_invalid_3(self) -> None: + def test_gather_exports_ignore_valid_1(self) -> None: + code = """ + from foo import bar + from biz import baz + + __all__ = ["bar", "b""a""z"] + """ + + gatherer = self.gather_exports(code) + self.assertEqual(gatherer.explicit_exported_objects, {"bar", "baz"}) + + def test_gather_exports_ignore_valid_2(self) -> None: + code = """ + from foo import bar + from biz import baz + + __all__, _ = ["bar", "baz"], ["biz"] + """ + + gatherer = self.gather_exports(code) + self.assertEqual(gatherer.explicit_exported_objects, {"bar", "baz"}) + + def test_gather_exports_ignore_valid_3(self) -> None: code = """ from foo import bar from biz import baz - __all__ = ["bar", "baz", "foo""bar"] + __all__ = exported = ["bar", "baz"] """ gatherer = self.gather_exports(code) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libcst-0.3.9/libcst/matchers/_matcher_base.py new/libcst-0.3.10/libcst/matchers/_matcher_base.py --- old/libcst-0.3.9/libcst/matchers/_matcher_base.py 2020-08-04 22:04:41.000000000 +0200 +++ new/libcst-0.3.10/libcst/matchers/_matcher_base.py 2020-08-14 21:02:21.000000000 +0200 @@ -7,7 +7,7 @@ import copy import inspect import re -from dataclasses import fields +from dataclasses import dataclass, fields from enum import Enum, auto from typing import ( Callable, @@ -932,6 +932,34 @@ return cast(_OtherNodeT, _ExtractMatchingNode(matcher, name)) +def _matches_zero_nodes( + matcher: Union[ + BaseMatcherNode, + _BaseWildcardNode, + MatchIfTrue[Callable[[object], bool]], + _BaseMetadataMatcher, + DoNotCareSentinel, + ] +) -> bool: + if isinstance(matcher, AtLeastN) and matcher.n == 0: + return True + if isinstance(matcher, AtMostN): + return True + if isinstance(matcher, _ExtractMatchingNode): + return _matches_zero_nodes(matcher.matcher) + return False + + +@dataclass(frozen=True) +class _SequenceMatchesResult: + sequence_capture: Optional[ + Dict[str, Union[libcst.CSTNode, Sequence[libcst.CSTNode]]] + ] + matched_nodes: Optional[ + Union[libcst.CSTNode, MaybeSentinel, Sequence[libcst.CSTNode]] + ] + + def _sequence_matches( # noqa: C901 nodes: Sequence[Union[MaybeSentinel, libcst.CSTNode]], matchers: Sequence[ @@ -944,30 +972,34 @@ ] ], metadata_lookup: Callable[[meta.ProviderT, libcst.CSTNode], object], -) -> Optional[Dict[str, Union[libcst.CSTNode, Sequence[libcst.CSTNode]]]]: +) -> _SequenceMatchesResult: if not nodes and not matchers: - # Base case, empty lists are alwatys matches - return {} + # Base case, empty lists are always matches + return _SequenceMatchesResult({}, None) if not nodes and matchers: # Base case, we have one or more matcher that wasn't matched - return ( - {} - if all( - (isinstance(m, AtLeastN) and m.n == 0) or isinstance(m, AtMostN) - for m in matchers + if all(_matches_zero_nodes(m) for m in matchers): + return _SequenceMatchesResult( + {m.name: () for m in matchers if isinstance(m, _ExtractMatchingNode)}, + (), ) - else None - ) + else: + return _SequenceMatchesResult(None, None) if nodes and not matchers: # Base case, we have nodes left that don't match any matcher - return None + return _SequenceMatchesResult(None, None) # Recursive case, nodes and matchers LHS matches node = nodes[0] matcher = matchers[0] if isinstance(matcher, DoNotCareSentinel): # We don't care about the value for this node. - return _sequence_matches(nodes[1:], matchers[1:], metadata_lookup) + return _SequenceMatchesResult( + _sequence_matches( + nodes[1:], matchers[1:], metadata_lookup + ).sequence_capture, + node, + ) elif isinstance(matcher, _BaseWildcardNode): if isinstance(matcher, AtMostN): if matcher.n > 0: @@ -977,18 +1009,24 @@ nodes[0], matcher.matcher, metadata_lookup ) if attribute_capture is not None: - sequence_capture = _sequence_matches( + result = _sequence_matches( nodes[1:], [AtMostN(matcher.matcher, n=matcher.n - 1), *matchers[1:]], metadata_lookup, ) - if sequence_capture is not None: - return {**attribute_capture, **sequence_capture} + if result.sequence_capture is not None: + return _SequenceMatchesResult( + {**attribute_capture, **result.sequence_capture}, + (node, *result.matched_nodes), + ) # Finally, assume that this does not match the current node. # Consume the matcher but not the node. - sequence_capture = _sequence_matches(nodes, matchers[1:], metadata_lookup) - if sequence_capture is not None: - return sequence_capture + return _SequenceMatchesResult( + _sequence_matches( + nodes, matchers[1:], metadata_lookup + ).sequence_capture, + (), + ) elif isinstance(matcher, AtLeastN): if matcher.n > 0: # Only match if we can consume one of the matches, since we still @@ -997,13 +1035,17 @@ nodes[0], matcher.matcher, metadata_lookup ) if attribute_capture is not None: - sequence_capture = _sequence_matches( + result = _sequence_matches( nodes[1:], [AtLeastN(matcher.matcher, n=matcher.n - 1), *matchers[1:]], metadata_lookup, ) - if sequence_capture is not None: - return {**attribute_capture, **sequence_capture} + if result.sequence_capture is not None: + return _SequenceMatchesResult( + {**attribute_capture, **result.sequence_capture}, + (node, *result.matched_nodes), + ) + return _SequenceMatchesResult(None, None) else: # First, assume that this does match a node (greedy). # Consume one node since it matched this matcher. @@ -1011,45 +1053,52 @@ nodes[0], matcher.matcher, metadata_lookup ) if attribute_capture is not None: - sequence_capture = _sequence_matches( - nodes[1:], matchers, metadata_lookup - ) - if sequence_capture is not None: - return {**attribute_capture, **sequence_capture} + result = _sequence_matches(nodes[1:], matchers, metadata_lookup) + if result.sequence_capture is not None: + return _SequenceMatchesResult( + {**attribute_capture, **result.sequence_capture}, + (node, *result.matched_nodes), + ) # Now, assume that this does not match the current node. # Consume the matcher but not the node. - sequence_capture = _sequence_matches( - nodes, matchers[1:], metadata_lookup + return _SequenceMatchesResult( + _sequence_matches( + nodes, matchers[1:], metadata_lookup + ).sequence_capture, + (), ) - if sequence_capture is not None: - return sequence_capture else: # There are no other types of wildcard consumers, but we're making # pyre happy with that fact. raise Exception(f"Logic error unrecognized wildcard {type(matcher)}!") elif isinstance(matcher, _ExtractMatchingNode): # See if the raw matcher matches. If it does, capture the sequence we matched and store it. - sequence_capture = _sequence_matches( + result = _sequence_matches( nodes, [matcher.matcher, *matchers[1:]], metadata_lookup ) - if sequence_capture is not None: - return { - # Our own match capture comes first, since we wnat to allow the same - # name later in the sequence to override us. - matcher.name: nodes, - **sequence_capture, - } - return None + if result.sequence_capture is not None: + return _SequenceMatchesResult( + { + # Our own match capture comes first, since we wnat to allow the same + # name later in the sequence to override us. + matcher.name: result.matched_nodes, + **result.sequence_capture, + }, + result.matched_nodes, + ) + return _SequenceMatchesResult(None, None) match_capture = _matches(node, matcher, metadata_lookup) if match_capture is not None: # These values match directly - sequence_capture = _sequence_matches(nodes[1:], matchers[1:], metadata_lookup) - if sequence_capture is not None: - return {**match_capture, **sequence_capture} + result = _sequence_matches(nodes[1:], matchers[1:], metadata_lookup) + if result.sequence_capture is not None: + return _SequenceMatchesResult( + {**match_capture, **result.sequence_capture}, node + ) # Failed recursive case, no match - return None + return _SequenceMatchesResult(None, None) _AttributeValueT = Optional[Union[MaybeSentinel, libcst.CSTNode, str, bool]] @@ -1110,9 +1159,9 @@ for m in matcher.options: if isinstance(m, collections.abc.Sequence): # Should match the sequence of requested nodes - sequence_capture = _sequence_matches(node, m, metadata_lookup) - if sequence_capture is not None: - return sequence_capture + result = _sequence_matches(node, m, metadata_lookup) + if result.sequence_capture is not None: + return result.sequence_capture elif isinstance(m, MatchIfTrue): return {} if matcher.func(node) else None elif isinstance(matcher, AllOf): @@ -1121,10 +1170,10 @@ for m in matcher.options: if isinstance(m, collections.abc.Sequence): # Should match the sequence of requested nodes - sequence_capture = _sequence_matches(node, m, metadata_lookup) - if sequence_capture is None: + result = _sequence_matches(node, m, metadata_lookup) + if result.sequence_capture is None: return None - all_captures = {**all_captures, **sequence_capture} + all_captures = {**all_captures, **result.sequence_capture} elif isinstance(m, MatchIfTrue): return {} if matcher.func(node) else None else: @@ -1150,7 +1199,7 @@ matcher, ), metadata_lookup, - ) + ).sequence_capture # We exhausted our possibilities, there's no match return None diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libcst-0.3.9/libcst/matchers/tests/test_extract.py new/libcst-0.3.10/libcst/matchers/tests/test_extract.py --- old/libcst-0.3.9/libcst/matchers/tests/test_extract.py 2020-08-04 22:04:41.000000000 +0200 +++ new/libcst-0.3.10/libcst/matchers/tests/test_extract.py 2020-08-14 21:02:21.000000000 +0200 @@ -322,6 +322,34 @@ ) self.assertEqual(nodes, {}) + def test_extract_optional_wildcard_head(self) -> None: + expression = cst.parse_expression("[3]") + nodes = m.extract( + expression, + m.List( + elements=[ + m.SaveMatchedNode(m.ZeroOrMore(), "head1"), + m.SaveMatchedNode(m.ZeroOrMore(), "head2"), + m.Element(value=m.Integer(value="3")), + ] + ), + ) + self.assertEqual(nodes, {"head1": (), "head2": ()}) + + def test_extract_optional_wildcard_tail(self) -> None: + expression = cst.parse_expression("[3]") + nodes = m.extract( + expression, + m.List( + elements=[ + m.Element(value=m.Integer(value="3")), + m.SaveMatchedNode(m.ZeroOrMore(), "tail1"), + m.SaveMatchedNode(m.ZeroOrMore(), "tail2"), + ] + ), + ) + self.assertEqual(nodes, {"tail1": (), "tail2": ()}) + def test_extract_optional_wildcard_present(self) -> None: expression = cst.parse_expression("a + b[c], d(e, f * g, h.i.j)") nodes = m.extract( @@ -404,3 +432,25 @@ ), ) self.assertIsNone(nodes) + + def test_extract_sequence_multiple_wildcards(self) -> None: + expression = cst.parse_expression("1, 2, 3, 4") + nodes = m.extract( + expression, + m.Tuple( + elements=( + m.SaveMatchedNode(m.ZeroOrMore(), "head"), + m.SaveMatchedNode(m.Element(value=m.Integer(value="3")), "element"), + m.SaveMatchedNode(m.ZeroOrMore(), "tail"), + ) + ), + ) + tuple_elements = cst.ensure_type(expression, cst.Tuple).elements + self.assertEqual( + nodes, + { + "head": tuple(tuple_elements[:2]), + "element": tuple_elements[2], + "tail": tuple(tuple_elements[3:]), + }, + ) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libcst-0.3.9/libcst/metadata/scope_provider.py new/libcst-0.3.10/libcst/metadata/scope_provider.py --- old/libcst-0.3.9/libcst/metadata/scope_provider.py 2020-08-07 20:39:26.000000000 +0200 +++ new/libcst-0.3.10/libcst/metadata/scope_provider.py 2020-08-15 00:27:00.000000000 +0200 @@ -63,11 +63,14 @@ #: assignment. scope: "Scope" + is_annotation: bool + __assignments: Set["BaseAssignment"] - def __init__(self, node: cst.Name, scope: "Scope") -> None: + def __init__(self, node: cst.Name, scope: "Scope", is_annotation: bool) -> None: self.node = node self.scope = scope + self.is_annotation = is_annotation self.__assignments = set() def __hash__(self) -> int: @@ -640,6 +643,7 @@ self.scope: Scope = GlobalScope() self.__deferred_accesses: List[Tuple[Access, Optional[cst.Attribute]]] = [] self.__top_level_attribute_stack: List[Optional[cst.Attribute]] = [None] + self.__in_annotation: Set[Union[cst.Call, cst.Annotation]] = set() @contextmanager def _new_scope( @@ -695,17 +699,60 @@ def visit_Call(self, node: cst.Call) -> Optional[bool]: self.__top_level_attribute_stack.append(None) + if any( + qn.name == "typing.TypeVar" + for qn in self.scope.get_qualified_names_for(node) + ): + node.func.visit(self) + self.__in_annotation.add(node) + for arg in node.args[1:]: + arg.visit(self) + return False + return True def leave_Call(self, original_node: cst.Call) -> None: self.__top_level_attribute_stack.pop() + self.__in_annotation.discard(original_node) + + def visit_Annotation(self, node: cst.Annotation) -> Optional[bool]: + self.__in_annotation.add(node) + + def leave_Annotation(self, original_node: cst.Annotation) -> None: + self.__in_annotation.discard(original_node) + + def visit_SimpleString(self, node: cst.SimpleString) -> Optional[bool]: + self._handle_string_annotation(node) + return False + + def visit_ConcatenatedString(self, node: cst.ConcatenatedString) -> Optional[bool]: + self._handle_string_annotation(node) + return False + + def _handle_string_annotation( + self, node: Union[cst.SimpleString, cst.ConcatenatedString] + ) -> None: + if self.__in_annotation: + value = node.evaluated_value + if value: + mod = cst.parse_module(value) + mod.visit(self) + + def visit_Subscript(self, node: cst.Subscript) -> Optional[bool]: + if any( + qn.name in ("typing.Literal", "typing_extensions.Literal") + for qn in self.scope.get_qualified_names_for(node.value) + ): + node.value.visit(self) + return False + return True def visit_Name(self, node: cst.Name) -> Optional[bool]: # not all Name have ExpressionContext context = self.provider.get_metadata(ExpressionContextProvider, node, None) if context == ExpressionContext.STORE: self.scope.record_assignment(node.value, node) - elif context in (ExpressionContext.LOAD, ExpressionContext.DEL): - access = Access(node, self.scope) + elif context in (ExpressionContext.LOAD, ExpressionContext.DEL, None): + access = Access(node, self.scope, is_annotation=bool(self.__in_annotation)) self.__deferred_accesses.append( (access, self.__top_level_attribute_stack[-1]) ) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libcst-0.3.9/libcst/metadata/tests/test_scope_provider.py new/libcst-0.3.10/libcst/metadata/tests/test_scope_provider.py --- old/libcst-0.3.9/libcst/metadata/tests/test_scope_provider.py 2020-08-05 02:26:37.000000000 +0200 +++ new/libcst-0.3.10/libcst/metadata/tests/test_scope_provider.py 2020-08-15 00:06:19.000000000 +0200 @@ -1010,6 +1010,59 @@ self.assertEqual(len(set(scopes.values())), 3) + def test_annotation_access(self) -> None: + m, scopes = get_scope_metadata_provider( + """ + from typing import Literal, TypeVar + from a import A, B, C, D, E, F + def x(a: A): + pass + def y(b: "B"): + pass + def z(c: Literal["C"]): + pass + DType = TypeVar("DType", bound=D) + EType = TypeVar("EType", bound="E") + FType = TypeVar("F") + """ + ) + imp = ensure_type( + ensure_type(m.body[1], cst.SimpleStatementLine).body[0], cst.ImportFrom + ) + scope = scopes[imp] + + assignment = list(scope["A"])[0] + self.assertIsInstance(assignment, Assignment) + self.assertEqual(len(assignment.references), 1) + references = list(assignment.references) + self.assertTrue(references[0].is_annotation) + + assignment = list(scope["B"])[0] + self.assertIsInstance(assignment, Assignment) + self.assertEqual(len(assignment.references), 1) + references = list(assignment.references) + self.assertTrue(references[0].is_annotation) + + assignment = list(scope["C"])[0] + self.assertIsInstance(assignment, Assignment) + self.assertEqual(len(assignment.references), 0) + + assignment = list(scope["D"])[0] + self.assertIsInstance(assignment, Assignment) + self.assertEqual(len(assignment.references), 1) + references = list(assignment.references) + self.assertTrue(references[0].is_annotation) + + assignment = list(scope["E"])[0] + self.assertIsInstance(assignment, Assignment) + self.assertEqual(len(assignment.references), 1) + references = list(assignment.references) + self.assertTrue(references[0].is_annotation) + + assignment = list(scope["F"])[0] + self.assertIsInstance(assignment, Assignment) + self.assertEqual(len(assignment.references), 0) + def test_node_of_scopes(self) -> None: m, scopes = get_scope_metadata_provider( """ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libcst-0.3.9/libcst/metadata/type_inference_provider.py new/libcst-0.3.10/libcst/metadata/type_inference_provider.py --- old/libcst-0.3.9/libcst/metadata/type_inference_provider.py 2020-08-04 22:04:41.000000000 +0200 +++ new/libcst-0.3.10/libcst/metadata/type_inference_provider.py 2020-08-14 21:02:21.000000000 +0200 @@ -57,7 +57,7 @@ root_path: Path, paths: List[str], timeout: Optional[int] ) -> Mapping[str, object]: params = ",".join(f"path='{root_path / path}'" for path in paths) - cmd = f'''pyre query "types({params})"''' + cmd = f'''pyre --noninteractive query "types({params})"''' try: stdout, stderr, return_code = run_command(cmd, timeout=timeout) except subprocess.TimeoutExpired as exc: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libcst-0.3.9/libcst.egg-info/PKG-INFO new/libcst-0.3.10/libcst.egg-info/PKG-INFO --- old/libcst-0.3.9/libcst.egg-info/PKG-INFO 2020-08-07 20:46:04.000000000 +0200 +++ new/libcst-0.3.10/libcst.egg-info/PKG-INFO 2020-08-17 20:25:13.000000000 +0200 @@ -1,6 +1,6 @@ Metadata-Version: 2.1 Name: libcst -Version: 0.3.9 +Version: 0.3.10 Summary: A concrete syntax tree with AST-like properties for Python 3.5, 3.6, 3.7 and 3.8 programs. Home-page: https://github.com/Instagram/LibCST License: MIT diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libcst-0.3.9/libcst.egg-info/SOURCES.txt new/libcst-0.3.10/libcst.egg-info/SOURCES.txt --- old/libcst-0.3.9/libcst.egg-info/SOURCES.txt 2020-08-07 20:46:04.000000000 +0200 +++ new/libcst-0.3.10/libcst.egg-info/SOURCES.txt 2020-08-17 20:25:13.000000000 +0200 @@ -192,6 +192,7 @@ libcst/codemod/commands/tests/test_strip_strings_from_types.py libcst/codemod/commands/tests/test_unnecessary_format_string.py libcst/codemod/tests/__init__.py +libcst/codemod/tests/codemod_formatter_error_input.py.txt libcst/codemod/tests/test_cli.py libcst/codemod/tests/test_codemod.py libcst/codemod/tests/test_codemod_cli.py diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libcst-0.3.9/setup.py new/libcst-0.3.10/setup.py --- old/libcst-0.3.9/setup.py 2020-08-04 22:04:41.000000000 +0200 +++ new/libcst-0.3.10/setup.py 2020-08-14 21:02:21.000000000 +0200 @@ -39,7 +39,11 @@ url="https://github.com/Instagram/LibCST", license="MIT", packages=setuptools.find_packages(), - package_data={"libcst": ["py.typed"]}, + package_data={ + "libcst": ["py.typed"], + "libcst.tests.pyre": ["*"], + "libcst.codemod.tests": ["*"], + }, test_suite="libcst", python_requires=">=3.6", install_requires=[
