@@ -73,6 +73,12 @@ def run_stubtest(
7373 # TODO: Maybe find a way to cache these in CI
7474 dists_to_install = [dist_req , get_mypy_req ()]
7575 dists_to_install .extend (requirements .external_pkgs ) # Internal requirements are added to MYPYPATH
76+
77+ # Since the "gdb" Python package is available only inside GDB, it is not
78+ # possible to install it through pip, so stub tests cannot install it.
79+ if dist_name == "gdb" :
80+ dists_to_install [:] = dists_to_install [1 :]
81+
7682 pip_cmd = [pip_exe , "install" , * dists_to_install ]
7783 try :
7884 subprocess .run (pip_cmd , check = True , capture_output = True )
@@ -118,6 +124,10 @@ def run_stubtest(
118124 if not setup_uwsgi_stubtest_command (dist , venv_dir , stubtest_cmd ):
119125 return False
120126
127+ if dist_name == "gdb" :
128+ if not setup_gdb_stubtest_command (venv_dir , stubtest_cmd ):
129+ return False
130+
121131 try :
122132 subprocess .run (stubtest_cmd , env = stubtest_env , check = True , capture_output = True )
123133 except subprocess .CalledProcessError as e :
@@ -153,6 +163,88 @@ def run_stubtest(
153163 return True
154164
155165
166+ def setup_gdb_stubtest_command (venv_dir : Path , stubtest_cmd : list [str ]) -> bool :
167+ """
168+ Use wrapper scripts to run stubtest inside gdb.
169+ The wrapper script is used to pass the arguments to the gdb script.
170+ """
171+ if sys .platform == "win32" :
172+ print_error ("gdb is not supported on Windows" )
173+ return False
174+
175+ try :
176+ gdb_version = subprocess .check_output (["gdb" , "--version" ], text = True , stderr = subprocess .STDOUT )
177+ except FileNotFoundError :
178+ print_error ("gdb is not installed" )
179+ return False
180+ if "Python scripting is not supported in this copy of GDB" in gdb_version :
181+ print_error ("Python scripting is not supported in this copy of GDB" )
182+ return False
183+
184+ gdb_script = venv_dir / "gdb_stubtest.py"
185+ wrapper_script = venv_dir / "gdb_wrapper.py"
186+ gdb_script_contents = dedent (
187+ f"""
188+ import json
189+ import os
190+ import site
191+ import sys
192+ import traceback
193+
194+ from glob import glob
195+
196+ # Add the venv site-packages to sys.path. gdb doesn't use the virtual environment.
197+ # Taken from https://github.com/pwndbg/pwndbg/blob/83d8d95b576b749e888f533ce927ad5a77fb957b/gdbinit.py#L37
198+ site_pkgs_path = glob(os.path.join({ str (venv_dir )!r} , "lib/*/site-packages"))[0]
199+ site.addsitedir(site_pkgs_path)
200+
201+ exit_code = 1
202+ try:
203+ # gdb wraps stdout and stderr without a .fileno
204+ # colored output in mypy tries to access .fileno()
205+ sys.stdout.fileno = sys.__stdout__.fileno
206+ sys.stderr.fileno = sys.__stderr__.fileno
207+
208+ from mypy.stubtest import main
209+
210+ sys.argv = json.loads(os.environ.get("STUBTEST_ARGS"))
211+ exit_code = main()
212+ except Exception:
213+ traceback.print_exc()
214+ finally:
215+ gdb.execute(f"quit {{exit_code}}")
216+ """
217+ )
218+ gdb_script .write_text (gdb_script_contents )
219+
220+ wrapper_script_contents = dedent (
221+ f"""
222+ import json
223+ import os
224+ import subprocess
225+ import sys
226+
227+ stubtest_env = os.environ | {{"STUBTEST_ARGS": json.dumps(sys.argv)}}
228+ gdb_cmd = [
229+ "gdb",
230+ "--quiet",
231+ "--nx",
232+ "--batch",
233+ "--command",
234+ { str (gdb_script )!r} ,
235+ ]
236+ r = subprocess.run(gdb_cmd, env=stubtest_env)
237+ sys.exit(r.returncode)
238+ """
239+ )
240+ wrapper_script .write_text (wrapper_script_contents )
241+
242+ # replace "-m mypy.stubtest" in stubtest_cmd with the path to our wrapper script
243+ assert stubtest_cmd [1 :3 ] == ["-m" , "mypy.stubtest" ]
244+ stubtest_cmd [1 :3 ] = [str (wrapper_script )]
245+ return True
246+
247+
156248def setup_uwsgi_stubtest_command (dist : Path , venv_dir : Path , stubtest_cmd : list [str ]) -> bool :
157249 """Perform some black magic in order to run stubtest inside uWSGI.
158250
0 commit comments