66import code
77import getpass
88import logging
9+ import os
910import sys
1011import threading
1112import traceback
1213from asyncio import to_thread
1314from functools import update_wrapper
1415from linecache import cache as line_cache
16+ from pathlib import Path
1517from typing import TYPE_CHECKING , cast
1618
1719from pygments import highlight
@@ -124,6 +126,77 @@ def info(
124126 if callable (set_status ):
125127 set_status (self .status )
126128
129+ def init_venv (self ) -> None :
130+ """Add the current venv to sys.path so we can import modules from it.
131+
132+ This isn't perfect: it doesn't use the Python interpreter with which the
133+ virtualenv was built, and it ignores the --no-site-packages option. A warning
134+ will appear suggesting the user installs euporie in the virtualenv or to use a
135+ Jupyter kernel, but for many cases, it probably works well enough.
136+
137+ Adapted from :py:mod:`IPython`.
138+ """
139+ # Check if we are in a virtual environment
140+ if "VIRTUAL_ENV" not in os .environ :
141+ return
142+ elif os .environ ["VIRTUAL_ENV" ] == "" :
143+ log .warning (
144+ "The virtual environment path is set to '': "
145+ "please check if this is intentional."
146+ )
147+ return
148+
149+ # Follow sys.executable symlink trail, recoding all paths along the way
150+ # We need to check all of these
151+ paths = [Path (sys .executable )]
152+ while (path := paths [- 1 ]).is_symlink ():
153+ new_path = path .readlink ()
154+ if not new_path .is_absolute ():
155+ new_path = path .parent / new_path
156+ paths .append (new_path )
157+
158+ # Get the venv path
159+ p_venv = Path (os .environ ["VIRTUAL_ENV" ]).resolve ()
160+ # In Cygwin paths like "c:\..." and '\cygdrive\c\...' are possible
161+ if len (p_venv .parts ) > 2 and p_venv .parts [1 ] == "cygdrive" :
162+ drive_name = p_venv .parts [2 ]
163+ p_venv = (drive_name + ":/" ) / Path (* p_venv .parts [3 :])
164+
165+ # Check if the executable is already inside or has access to the virtualenv
166+ if any (p_venv == p .parents [1 ].resolve () for p in paths ):
167+ return
168+
169+ # Locate the site-packages of the virtual environment
170+ if sys .platform == "win32" :
171+ virtual_env = str (Path (os .environ ["VIRTUAL_ENV" ], "Lib" , "site-packages" ))
172+ else :
173+ import re
174+
175+ virtual_env_path = Path (
176+ os .environ ["VIRTUAL_ENV" ], "lib" , "python{}.{}" , "site-packages"
177+ )
178+ p_ver = sys .version_info [:2 ]
179+
180+ # Predict version from py[thon]-x.x in the $VIRTUAL_ENV
181+ re_m = re .search (r"\bpy(?:thon)?([23])\.(\d+)\b" , os .environ ["VIRTUAL_ENV" ])
182+ if re_m :
183+ predicted_path = Path (str (virtual_env_path ).format (* re_m .groups ()))
184+ if predicted_path .exists ():
185+ p_ver = re_m .groups ()
186+
187+ virtual_env = str (virtual_env_path ).format (* p_ver )
188+
189+ if self .kernel_tab .app .config .warn_venv :
190+ log .warning ("Attempting to work in virtualenv %r." , virtual_env )
191+ log .warning (
192+ "If you encounter problems, please install euporie inside the virtual "
193+ "environment, or use a Jupyter kernel."
194+ )
195+ import site
196+
197+ sys .path .insert (0 , virtual_env )
198+ site .addsitedir (virtual_env )
199+
127200 async def start_async (self ) -> None :
128201 """Start the local interpreter."""
129202 self .error = None
@@ -134,6 +207,7 @@ async def start_async(self) -> None:
134207 set_execution_count := self .default_callbacks .get ("set_execution_count" )
135208 ):
136209 set_execution_count (self .execution_count )
210+ self .init_venv ()
137211
138212 @property
139213 def spec (self ) -> dict [str , str ]:
@@ -177,7 +251,7 @@ def showtraceback(
177251 colored_traceback = highlight (
178252 traceback_text ,
179253 Python3TracebackLexer (),
180- Terminal256Formatter (style = get_app () .config .syntax_theme ),
254+ Terminal256Formatter (style = self . kernel_tab . app .config .syntax_theme ),
181255 ).rstrip ()
182256
183257 # Send the error through the callback
0 commit comments