diff --git a/libcst/_batched_visitor.py b/libcst/_batched_visitor.py index d853738f..62f60012 100644 --- a/libcst/_batched_visitor.py +++ b/libcst/_batched_visitor.py @@ -3,7 +3,6 @@ # This source code is licensed under the MIT license found in the # LICENSE file in the root directory of this source tree. -import inspect from typing import ( Callable, cast, @@ -44,20 +43,19 @@ def get_visitors(self) -> Mapping[str, VisitorMethod]: excluding all empty stubs. """ - methods = inspect.getmembers( - self, - lambda m: ( - inspect.ismethod(m) - and (m.__name__.startswith("visit_") or m.__name__.startswith("leave_")) - and not getattr(m, "_is_no_op", False) - ), - ) + methods = { + name: method + for name in dir(self) + if name.startswith(("visit_", "leave_")) + and callable(method := getattr(self, name)) + and not getattr(method, "_is_no_op", False) + } # TODO: verify all visitor methods reference valid node classes. # for name, __ in methods: # ... - return dict(methods) + return methods def visit_batched( diff --git a/libcst/tests/test_batched_visitor.py b/libcst/tests/test_batched_visitor.py index 9009847c..cbc89d45 100644 --- a/libcst/tests/test_batched_visitor.py +++ b/libcst/tests/test_batched_visitor.py @@ -70,3 +70,20 @@ def leave_If(self, original_node: cst.If) -> None: self.assertEqual( object.__getattribute__(if_, "whitespace_before_test"), mock.leave_If() ) + + def test_no_property_access(self) -> None: + mock = Mock() + + class Batchable(BatchableCSTVisitor): + @property + def evil_property(self) -> str: + mock.evil_property() + return "evil" + + def visit_Module(self, node: cst.Module) -> None: + mock.visit_Module() + + _ = visit_batched(parse_module("if True: pass"), [Batchable()]) + + mock.visit_Module.assert_called_once() + mock.evil_property.assert_not_called()