blob: b4f8d50d473aa3810bec9a4242373c7fcb2a27c2 [file] [log] [blame]
'''
This module provides utilities to get the absolute filenames so that we can be sure that:
- The case of a file will match the actual file in the filesystem (otherwise breakpoints won't be hit).
- Providing means for the user to make path conversions when doing a remote debugging session in
one machine and debugging in another.
To do that, the PATHS_FROM_ECLIPSE_TO_PYTHON constant must be filled with the appropriate paths.
@note:
in this context, the server is where your python process is running
and the client is where eclipse is running.
E.g.:
If the server (your python process) has the structure
/user/projects/my_project/src/package/module1.py
and the client has:
c:\my_project\src\package\module1.py
the PATHS_FROM_ECLIPSE_TO_PYTHON would have to be:
PATHS_FROM_ECLIPSE_TO_PYTHON = [(r'c:\my_project\src', r'/user/projects/my_project/src')]
@note: DEBUG_CLIENT_SERVER_TRANSLATION can be set to True to debug the result of those translations
@note: the case of the paths is important! Note that this can be tricky to get right when one machine
uses a case-independent filesystem and the other uses a case-dependent filesystem (if the system being
debugged is case-independent, 'normcase()' should be used on the paths defined in PATHS_FROM_ECLIPSE_TO_PYTHON).
@note: all the paths with breakpoints must be translated (otherwise they won't be found in the server)
@note: to enable remote debugging in the target machine (pydev extensions in the eclipse installation)
import pydevd;pydevd.settrace(host, stdoutToServer, stderrToServer, port, suspend)
see parameter docs on pydevd.py
@note: for doing a remote debugging session, all the pydevd_ files must be on the server accessible
through the PYTHONPATH (and the PATHS_FROM_ECLIPSE_TO_PYTHON only needs to be set on the target
machine for the paths that'll actually have breakpoints).
'''
from pydevd_constants import * #@UnusedWildImport
import os.path
import sys
import traceback
basename = os.path.basename
exists = os.path.exists
join = os.path.join
try:
rPath = os.path.realpath #@UndefinedVariable
except:
# jython does not support os.path.realpath
# realpath is a no-op on systems without islink support
rPath = os.path.abspath
#defined as a list of tuples where the 1st element of the tuple is the path in the client machine
#and the 2nd element is the path in the server machine.
#see module docstring for more details.
PATHS_FROM_ECLIPSE_TO_PYTHON = []
#example:
#PATHS_FROM_ECLIPSE_TO_PYTHON = [
#(normcase(r'd:\temp\temp_workspace_2\test_python\src\yyy\yyy'),
# normcase(r'd:\temp\temp_workspace_2\test_python\src\hhh\xxx'))]
DEBUG_CLIENT_SERVER_TRANSLATION = False
#caches filled as requested during the debug session
NORM_FILENAME_CONTAINER = {}
NORM_FILENAME_AND_BASE_CONTAINER = {}
NORM_FILENAME_TO_SERVER_CONTAINER = {}
NORM_FILENAME_TO_CLIENT_CONTAINER = {}
pycharm_os = None
def normcase(file):
global pycharm_os
if pycharm_os == 'UNIX':
return file
else:
return os.path.normcase(file)
def _NormFile(filename):
try:
return NORM_FILENAME_CONTAINER[filename]
except KeyError:
r = normcase(rPath(filename))
#cache it for fast access later
ind = r.find('.zip')
if ind == -1:
ind = r.find('.egg')
if ind != -1:
ind+=4
zip_path = r[:ind]
if r[ind] == "!":
ind+=1
inner_path = r[ind:]
if inner_path.startswith('/'):
inner_path = inner_path[1:]
r = zip_path + "/" + inner_path
NORM_FILENAME_CONTAINER[filename] = r
return r
ZIP_SEARCH_CACHE = {}
def exists(file):
if os.path.exists(file):
return file
ind = file.find('.zip')
if ind == -1:
ind = file.find('.egg')
if ind != -1:
ind+=4
zip_path = file[:ind]
if file[ind] == "!":
ind+=1
inner_path = file[ind:]
try:
zip = ZIP_SEARCH_CACHE[zip_path]
except KeyError:
try:
import zipfile
zip = zipfile.ZipFile(zip_path, 'r')
ZIP_SEARCH_CACHE[zip_path] = zip
except :
return None
try:
if inner_path.startswith('/'):
inner_path = inner_path[1:]
info = zip.getinfo(inner_path)
return zip_path + "/" + inner_path
except KeyError:
return None
return None
#Now, let's do a quick test to see if we're working with a version of python that has no problems
#related to the names generated...
try:
try:
code = rPath.func_code
except AttributeError:
code = rPath.__code__
if not exists(_NormFile(code.co_filename)):
sys.stderr.write('-------------------------------------------------------------------------------\n')
sys.stderr.write('pydev debugger: CRITICAL WARNING: This version of python seems to be incorrectly compiled (internal generated filenames are not absolute)\n')
sys.stderr.write('pydev debugger: The debugger may still function, but it will work slower and may miss breakpoints.\n')
sys.stderr.write('pydev debugger: Related bug: http://bugs.python.org/issue1666807\n')
sys.stderr.write('-------------------------------------------------------------------------------\n')
sys.stderr.flush()
NORM_SEARCH_CACHE = {}
initial_norm_file = _NormFile
def _NormFile(filename): #Let's redefine _NormFile to work with paths that may be incorrect
try:
return NORM_SEARCH_CACHE[filename]
except KeyError:
ret = initial_norm_file(filename)
if not exists(ret):
#We must actually go on and check if we can find it as if it was a relative path for some of the paths in the pythonpath
for path in sys.path:
ret = initial_norm_file(join(path, filename))
if exists(ret):
break
else:
sys.stderr.write('pydev debugger: Unable to find real location for: %s\n' % (filename,))
ret = filename
NORM_SEARCH_CACHE[filename] = ret
return ret
except:
#Don't fail if there's something not correct here -- but at least print it to the user so that we can correct that
traceback.print_exc()
if PATHS_FROM_ECLIPSE_TO_PYTHON:
#Work on the client and server slashes.
eclipse_sep = None
python_sep = None
for eclipse_prefix, server_prefix in PATHS_FROM_ECLIPSE_TO_PYTHON:
if eclipse_sep is not None and python_sep is not None:
break
if eclipse_sep is None:
for c in eclipse_prefix:
if c in ('/', '\\'):
eclipse_sep = c
break
if python_sep is None:
for c in server_prefix:
if c in ('/', '\\'):
python_sep = c
break
#If they're the same or one of them cannot be determined, just make it all None.
if eclipse_sep == python_sep or eclipse_sep is None or python_sep is None:
eclipse_sep = python_sep = None
#only setup translation functions if absolutely needed!
def NormFileToServer(filename):
#Eclipse will send the passed filename to be translated to the python process
#So, this would be 'NormFileFromEclipseToPython'
try:
return NORM_FILENAME_TO_SERVER_CONTAINER[filename]
except KeyError:
#used to translate a path from the client to the debug server
translated = normcase(filename)
for eclipse_prefix, server_prefix in PATHS_FROM_ECLIPSE_TO_PYTHON:
if translated.startswith(eclipse_prefix):
if DEBUG_CLIENT_SERVER_TRANSLATION:
sys.stderr.write('pydev debugger: replacing to server: %s\n' % (translated,))
translated = translated.replace(eclipse_prefix, server_prefix)
if DEBUG_CLIENT_SERVER_TRANSLATION:
sys.stderr.write('pydev debugger: sent to server: %s\n' % (translated,))
break
else:
if DEBUG_CLIENT_SERVER_TRANSLATION:
sys.stderr.write('pydev debugger: to server: unable to find matching prefix for: %s in %s\n' % \
(translated, [x[0] for x in PATHS_FROM_ECLIPSE_TO_PYTHON]))
#Note that when going to the server, we do the replace first and only later do the norm file.
if eclipse_sep is not None:
translated = translated.replace(eclipse_sep, python_sep)
translated = _NormFile(translated)
NORM_FILENAME_TO_SERVER_CONTAINER[filename] = translated
return translated
def NormFileToClient(filename):
#The result of this method will be passed to eclipse
#So, this would be 'NormFileFromPythonToEclipse'
try:
return NORM_FILENAME_TO_CLIENT_CONTAINER[filename]
except KeyError:
#used to translate a path from the debug server to the client
translated = _NormFile(filename)
for eclipse_prefix, python_prefix in PATHS_FROM_ECLIPSE_TO_PYTHON:
if translated.startswith(python_prefix):
if DEBUG_CLIENT_SERVER_TRANSLATION:
sys.stderr.write('pydev debugger: replacing to client: %s\n' % (translated,))
translated = translated.replace(python_prefix, eclipse_prefix)
if DEBUG_CLIENT_SERVER_TRANSLATION:
sys.stderr.write('pydev debugger: sent to client: %s\n' % (translated,))
break
else:
if DEBUG_CLIENT_SERVER_TRANSLATION:
sys.stderr.write('pydev debugger: to client: unable to find matching prefix for: %s in %s\n' % \
(translated, [x[1] for x in PATHS_FROM_ECLIPSE_TO_PYTHON]))
if eclipse_sep is not None:
translated = translated.replace(python_sep, eclipse_sep)
#The resulting path is not in the python process, so, we cannot do a _NormFile here,
#only at the beginning of this method.
NORM_FILENAME_TO_CLIENT_CONTAINER[filename] = translated
return translated
else:
#no translation step needed (just inline the calls)
NormFileToClient = _NormFile
NormFileToServer = _NormFile
def GetFileNameAndBaseFromFile(f):
try:
return NORM_FILENAME_AND_BASE_CONTAINER[f]
except KeyError:
filename = _NormFile(f)
base = basename(filename)
NORM_FILENAME_AND_BASE_CONTAINER[f] = filename, base
return filename, base
def GetFilenameAndBase(frame):
#This one is just internal (so, does not need any kind of client-server translation)
f = frame.f_code.co_filename
if f is not None and f.startswith('build/bdist.'):
# files from eggs in Python 2.7 have paths like build/bdist.linux-x86_64/egg/<path-inside-egg>
f = frame.f_globals['__file__']
if f.endswith('.pyc'):
f = f[:-1]
return GetFileNameAndBaseFromFile(f)
def set_pycharm_os(os):
global pycharm_os
pycharm_os = os