diff --git a/docs/changelog.md b/docs/changelog.md index a1d7a617..5ec6a914 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -2,6 +2,8 @@ ## Unreleased +- Add command-line option `-c`/`--code` to typecheck code from + the command line (#685) - Add a `pyanalyze.extensions.EnumName` predicate and infer it as the value for the `.name` attribute on enums. Also fix type inference for enum "properties" on Python 3.11 and up. (#682) diff --git a/pyanalyze/name_check_visitor.py b/pyanalyze/name_check_visitor.py index cfc55a94..36920769 100644 --- a/pyanalyze/name_check_visitor.py +++ b/pyanalyze/name_check_visitor.py @@ -1104,6 +1104,7 @@ def __init__( annotate: bool = False, add_ignores: bool = False, checker: Checker, + is_code_only: bool = False, ) -> None: super().__init__( filename, @@ -1113,6 +1114,7 @@ def __init__( fail_after_first=fail_after_first, verbosity=verbosity, add_ignores=add_ignores, + is_code_only=is_code_only, ) self.checker = checker @@ -1250,6 +1252,26 @@ def _load_module(self) -> Tuple[Optional[types.ModuleType], bool]: if not self.filename: return None, False self.log(logging.INFO, "Checking file", (self.filename, os.getpid())) + if self.is_code_only: + mod_dict = {} + try: + exec(self.contents, mod_dict) + except KeyboardInterrupt: + raise + except BaseException as e: + if self.tree is not None and self.tree.body: + node = self.tree.body[0] + else: + node = None + self.show_error( + node, + f"Failed to execute code due to {e!r}", + error_code=ErrorCode.import_failed, + ) + return None, False + mod = types.ModuleType(self.filename) + mod.__dict__.update(mod_dict) + return mod, False import_paths = self.options.get_value_for(ImportPaths) try: diff --git a/pyanalyze/node_visitor.py b/pyanalyze/node_visitor.py index 0f3545e8..837b8056 100644 --- a/pyanalyze/node_visitor.py +++ b/pyanalyze/node_visitor.py @@ -176,6 +176,7 @@ class BaseNodeVisitor(ast.NodeVisitor): tree: ast.Module all_failures: List[Failure] + is_code_only: bool def __init__( self, @@ -187,6 +188,7 @@ def __init__( verbosity: int = logging.CRITICAL, collect_failures: bool = False, add_ignores: bool = False, + is_code_only: bool = False, ) -> None: """Constructor. @@ -212,6 +214,7 @@ def __init__( self.seen_errors = set() # of (node, error_code) pairs self.add_ignores = add_ignores self.caught_errors = None + self.is_code_only = is_code_only def check(self) -> List[Failure]: """Runs the class's checks on a tree.""" @@ -732,14 +735,32 @@ def _run( @classmethod def _run_on_files_or_all( - cls, files: Optional[Sequence[str]] = None, **kwargs: Any + cls, + files: Optional[Sequence[str]] = None, + code: Optional[str] = None, + **kwargs: Any, ) -> List[Failure]: + if code is not None: + return cls._run_on_code(code, **kwargs) files = files or cls.get_default_directories(**kwargs) if not files: return cls.check_all_files(**kwargs) else: return cls._run_on_files(_get_all_files(files), **kwargs) + @classmethod + def _run_on_code(cls, code: str, **kwargs: Any) -> List[Failure]: + try: + tree = ast.parse(code) + except Exception as e: + print(f"Failed to parse code: {e}", file=sys.stderr) + sys.exit(1) + kwargs.pop("parallel", False) + kwargs.pop("find_unused", False) + kwargs.pop("find_unused_attributes", False) + kwargs.pop("assert_passes", False) + return cls("", code, tree, is_code_only=True, **kwargs).check() + @classmethod def _run_on_files(cls, files: Iterable[str], **kwargs: Any) -> List[Failure]: all_failures = [] @@ -825,6 +846,14 @@ def _get_argument_parser(cls) -> argparse.ArgumentParser: parser.add_argument( "-v", "--verbose", help="Print more information.", action="count" ) + parser.add_argument( + "-c", + "--code", + help=( + "Typecheck the given string. Note that pyanalyze will execute the code" + " before attempting to typecheck it." + ), + ) parser.add_argument( "-f", "--run-fixer",