55import tomllib
66from typing import Optional
77
8+ from multilspy import SyncLanguageServer
9+
810from ...entities .entity import Entity
11+ from ...entities .file import File
912from ..tree_sitter_base import TreeSitterAnalyzer
13+ from .ts_resolver import TreeSitterPythonResolver
1014
1115import tree_sitter_python as tspython
1216from tree_sitter import Language , Node
1317
1418import logging
1519logger = logging .getLogger ('code_graph' )
1620
21+
22+ _RESOLVER_ENV = "CODE_GRAPH_PY_RESOLVER"
23+ _RESOLVER_TREE_SITTER = "tree_sitter"
24+
25+
1726class PythonAnalyzer (TreeSitterAnalyzer ):
1827 entity_node_types = {
1928 'class_definition' : "Class" ,
@@ -26,8 +35,48 @@ class PythonAnalyzer(TreeSitterAnalyzer):
2635
2736 def __init__ (self ) -> None :
2837 super ().__init__ (Language (tspython .language ()))
38+ # Resolver selection: 'tree_sitter' opts into the static project-wide
39+ # resolver (issue #689). Default is the historical jedi/LSP path so
40+ # behaviour is unchanged until explicitly enabled.
41+ resolver_choice = os .environ .get (_RESOLVER_ENV , "" ).strip ().lower ()
42+ if resolver_choice == _RESOLVER_TREE_SITTER :
43+ self ._ts_resolver : Optional [TreeSitterPythonResolver ] = (
44+ TreeSitterPythonResolver (self .language )
45+ )
46+ logger .info ("PythonAnalyzer: tree-sitter static resolver enabled" )
47+ else :
48+ self ._ts_resolver = None
49+
50+ def resolve (
51+ self ,
52+ files : dict [Path , File ],
53+ lsp : SyncLanguageServer ,
54+ file_path : Path ,
55+ path : Path ,
56+ node : Node ,
57+ ) -> list [tuple [File , Node ]]:
58+ """Resolve a name node to ``(File, def_node)`` pairs.
59+
60+ When ``CODE_GRAPH_PY_RESOLVER=tree_sitter`` is set, bypass the LSP
61+ and use the project-wide static resolver. Otherwise fall through to
62+ the default jedi-backed implementation in ``AbstractAnalyzer``.
63+ """
64+ if self ._ts_resolver is not None :
65+ return self ._ts_resolver .resolve (files , file_path , path , node )
66+ return super ().resolve (files , lsp , file_path , path , node )
67+
68+ def needs_lsp (self ) -> bool :
69+ # When the tree-sitter resolver is active we don't touch the LSP, so
70+ # the orchestrator can skip starting one.
71+ return self ._ts_resolver is None
2972
3073 def add_dependencies (self , path : Path , files : list [Path ]):
74+ # When the tree-sitter resolver is active, we resolve statically
75+ # against the in-project files only — installing the project's
76+ # transitive Python deps just to feed jedi adds 10s–10min of
77+ # zero-value pip work. Short-circuit it.
78+ if self ._ts_resolver is not None :
79+ return
3180 if Path (f"{ path } /venv" ).is_dir ():
3281 return
3382 subprocess .run (["python3" , "-m" , "venv" , "venv" ], cwd = str (path ))
0 commit comments