Skip to content

Commit

Permalink
Attempt to work around bug PY-40661 in PyCharm
Browse files Browse the repository at this point in the history
Fixes #5
  • Loading branch information
tonyroberts committed Sep 22, 2022
1 parent 9004753 commit 32b5c00
Show file tree
Hide file tree
Showing 3 changed files with 213 additions and 72 deletions.
148 changes: 79 additions & 69 deletions pyxll_pycharm/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
Requires:
- PyXLL >= 5.0.0
- PyCharm Professional
- Python >= 3.7
To install this package use::
Expand All @@ -16,7 +17,9 @@
[PYCHARM]
port = 5000
suspend = 0
fix_pydevd_import = 1
"""
from ._import_pydevd import use_pycharm_pydevd
from pyxll import get_config
import pkg_resources
import ctypes
Expand All @@ -25,23 +28,21 @@

_log = logging.getLogger(__name__)


_MB_YESNO = 0x04
_MB_OK = 0x0
_IDYES = 0x6


#
def connect_to_pycharm(*args):
"""Connect to the remote PyCharm debugger."""
# Defer importing pydevd until it's actually needed as it will conflict with using
# other debuggers such as VS Code.
import pydevd_pycharm
import pydevd

# Get the settings from the config
port = 5000
suspend = False
stdout_to_server = True
stderr_to_server = True
fix_pydevd_import = True

cfg = get_config()
if cfg.has_option("PYCHARM", "port"):
Expand All @@ -68,74 +69,83 @@ def connect_to_pycharm(*args):
except (ValueError, TypeError):
_log.error("Unexpected value for PYCHARM.stderr_to_server.")

# If the debugger is not already running ask the user if they have started the debug server
if not pydevd.connected:
result = ctypes.windll.user32.MessageBoxA(
0,
b"Have you started the PyCharm remote debugger on port %d?" % port,
b"PyCharm Remote Debug",
_MB_YESNO)

if result != _IDYES:
ctypes.windll.user32.MessageBoxA(
if cfg.has_option("PYCHARM", "fix_pydevd_import"):
try:
fix_pydevd_import = bool(int(cfg.get("PYCHARM", "fix_pydevd_import")))
except (ValueError, TypeError):
_log.error("Unexpected value for PYCHARM.fix_pydevd_import.")

# Import pydevd_pycharm and pydevd, working around issues with multiple versions
# of pydevd being installed (e.g. when using ipython and debugpy).
with use_pycharm_pydevd(fix_pydevd_import) as (pydevd_pycharm, pydevd):
# If the debugger is not already running ask the user if they have started the debug server
if not pydevd.connected:
result = ctypes.windll.user32.MessageBoxA(
0,
b"Please start the PyCharm remote debugger on port %d first." % port,
b"Have you started the PyCharm remote debugger on port %d?" % port,
b"PyCharm Remote Debug",
_MB_OK)
return
else:
# The debugger is already running so check if the user has restarted the debug server
result = ctypes.windll.user32.MessageBoxA(
0,
b"The PyCharm debugger was already connected.\n" +
b"Have you re-started the PyCharm remote debugger on port %d?" % port,
b"PyCharm Remote Debug",
_MB_YESNO)

if result != _IDYES:
ctypes.windll.user32.MessageBoxA(
_MB_YESNO)

if result != _IDYES:
ctypes.windll.user32.MessageBoxA(
0,
b"Please start the PyCharm remote debugger on port %d first." % port,
b"PyCharm Remote Debug",
_MB_OK)
return
else:
# The debugger is already running so check if the user has restarted the debug server
result = ctypes.windll.user32.MessageBoxA(
0,
b"Please re-start the PyCharm remote debugger on port %d first." % port,
b"The PyCharm debugger was already connected.\n" +
b"Have you re-started the PyCharm remote debugger on port %d?" % port,
b"PyCharm Remote Debug",
_MB_OK)
return

# Call stoptrace (this sets pydevd.connected to False)
_log.debug("Disconnecting from the PyCharm debugger...")
pydevd.stoptrace()

# Undo the stdout/stderr redirection (not strictly necessary!)
if hasattr(sys, "_pydevd_out_buffer_") and hasattr(sys, "stdout_original"):
sys.stdout = sys.stdout_original
del sys._pydevd_out_buffer_

if hasattr(sys, "_pydevd_err_buffer_") and hasattr(sys, "stderr_original"):
sys.stderr = sys.stderr_original
del sys._pydevd_err_buffer_

# End the debugging session and kill all the pydevd threads
pydb = pydevd.get_global_debugger()
pydb.finish_debugging_session()
pydevd.kill_all_pydev_threads()

# Wait for all the pydevd threads to end
_log.debug("Waiting for the pydevd threads to finish.")
threads = list(pydevd.PyDBDaemonThread.created_pydb_daemon_threads.keys())
for thread in threads:
thread.join(timeout=1.0)
if thread.is_alive():
raise RuntimeError("Timed out waiting for pydevd thread to terminate.")

# Connect to the remote debugger
_log.debug("Connecting to the PyCharm debugger...")
pydevd_pycharm.settrace("localhost",
port=port,
suspend=suspend,
stdoutToServer=stdout_to_server,
stderrToServer=stderr_to_server)

# Reset excepthook to the default to avoid a PyCharm bug
sys.excepthook = sys.__excepthook__
_MB_YESNO)

if result != _IDYES:
ctypes.windll.user32.MessageBoxA(
0,
b"Please re-start the PyCharm remote debugger on port %d first." % port,
b"PyCharm Remote Debug",
_MB_OK)
return

# Call stoptrace (this sets pydevd.connected to False)
_log.debug("Disconnecting from the PyCharm debugger...")
pydevd.stoptrace()

# Undo the stdout/stderr redirection (not strictly necessary!)
if hasattr(sys, "_pydevd_out_buffer_") and hasattr(sys, "stdout_original"):
sys.stdout = sys.stdout_original
del sys._pydevd_out_buffer_

if hasattr(sys, "_pydevd_err_buffer_") and hasattr(sys, "stderr_original"):
sys.stderr = sys.stderr_original
del sys._pydevd_err_buffer_

# End the debugging session and kill all the pydevd threads
pydb = pydevd.get_global_debugger()
pydb.finish_debugging_session()
pydevd.kill_all_pydev_threads()

# Wait for all the pydevd threads to end
_log.debug("Waiting for the pydevd threads to finish.")
threads = list(pydevd.PyDBDaemonThread.created_pydb_daemon_threads.keys())
for thread in threads:
thread.join(timeout=1.0)
if thread.is_alive():
raise RuntimeError("Timed out waiting for pydevd thread to terminate.")

# Connect to the remote debugger
_log.debug("Connecting to the PyCharm debugger...")
pydevd_pycharm.settrace("localhost",
port=port,
suspend=suspend,
stdoutToServer=stdout_to_server,
stderrToServer=stderr_to_server)

# Reset excepthook to the default to avoid a PyCharm bug
sys.excepthook = sys.__excepthook__

ctypes.windll.user32.MessageBoxA(
0,
Expand Down
129 changes: 129 additions & 0 deletions pyxll_pycharm/_import_pydevd.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
from os.path import expandvars
from itertools import chain
from glob import glob
from pathlib import Path
from functools import lru_cache
import contextlib
import logging
import sys

_log = logging.getLogger(__name__)


@contextlib.contextmanager
def _restore_sys_modules():
# Take a copy of sys.modules and sys.path
sys_modules = dict(sys.modules)
sys_path = list(sys.path)

yield

# Restore sys.modules and sys.path
sys.modules.clear()
sys.modules.update(sys_modules)
sys.path.clear()
sys.path.extend(sys_path)


def _path_is_relative_to(a, b):
# Path.is_relative_to is new in Python 3.9
try:
Path(a).relative_to(b)
return Path(a).relative_to(b)
except ValueError:
return False


@lru_cache(maxsize=2)
def _import_pydevd(apply_fix=True):
"""Imports pydevd_pycharm and pydevd from the pydevd_pycharm package.
Manipulates sys.modules and sys.path to try and import the right packages.
This works around issues with debugpy also being installed.
See https://youtrack.jetbrains.com/issue/PY-40661/pydevd-pycharm-conflicts-with-pydevd-package.
"""
# If apply_fix is False then simply import and return the modules
if not apply_fix:
import pydevd_pycharm
import pydevd

pydevd_pycharm_dir = Path(pydevd_pycharm.__file__).parent
pydevd_dir = Path(pydevd.__file__).parent
if pydevd_pycharm_dir != pydevd_dir:
_log.warning("Potential pydevd version conflict found. " +
f"Try uninstalling pydevd from '{pydevd_dir}' and reinstall pydevd_pycharm.")

return pydevd_pycharm, pydevd

try:
# Try importing pydevd_pycharm from the default sys.path first
import pydevd_pycharm
except ImportError:
# If that fails look for PyCharm and add it to sys.path
_log.debug("pydevd_pycharm not found on sys.path. Looking for PyCharm install...")
pydevd_eggs = list(reversed(sorted(chain.from_iterable(
glob(expandvars(f"{env_var}\\JetBrains\\PyCharm*\\debug-eggs\\pydevd-pycharm.egg"), recursive=True)
for env_var in ("${ProgramFiles}", "${ProgramFiles(x86)}")))))

if pydevd_eggs:
pydevd_egg = pydevd_eggs[0]
_log.debug(f"Found pydevd-pycharm in PyCharm install: {pydevd_egg}")
sys.path.insert(0, pydevd_egg)

# Try to import pydevd_pycharm again (sys.path may have changed)
import pydevd_pycharm

# Next import pydev
import pydevd

# pydevd should be distributed as part of pydevd_pycharm
pydevd_pycharm_dir = Path(pydevd_pycharm.__file__).parent
pydevd_dir = Path(pydevd.__file__).parent
if pydevd_pycharm_dir != pydevd_dir:
_log.debug(f"Attempting to work around incompatible version of pydevd found: {pydevd.__file__}")

# Remove all the existing pydevd modules from sys.modules
pydev_modules = set()
for name, module in sys.modules.items():
if ((name.startswith("pydev") or name.startswith("_pydev"))
and (_path_is_relative_to(module.__file__, pydevd_dir) or
_path_is_relative_to(module.__file__, pydevd_pycharm_dir))):
pydev_modules.add(name)

for name in pydev_modules:
del sys.modules[name]

# Re-import pydevd_pycharm and pydevd with the PyCharm path appearing first on sys.path
sys.path.insert(0, str(pydevd_pycharm_dir))
import pydevd_pycharm
import pydevd

# Check what we've imported is now correct
pydevd_pycharm_dir = Path(pydevd_pycharm.__file__).parent
pydevd_dir = Path(pydevd.__file__).parent
if pydevd_pycharm_dir != pydevd_dir:
_log.warning("Potential pydevd version conflict found. " +
f"Try uninstalling pydevd from '{pydevd_dir}' and reinstall pydevd_pycharm.")

return pydevd_pycharm, pydevd


@contextlib.contextmanager
def use_pycharm_pydevd(apply_fix=True):
"""Context manager that imports pydevd_pycharm and pydevd
and ensures that sys.path includes the pycharm_pydevd path
for any late imports.
"""
if not apply_fix:
yield _import_pydevd(apply_fix)
return

with _restore_sys_modules():
pydevd_pycharm, pydevd = _import_pydevd(apply_fix)

# Make sure that pydevd_pycharm.__file__ is first on the path
if apply_fix:
pycharm_dir = str(Path(pydevd_pycharm.__file__).parent)
if sys.path[0] != pycharm_dir:
sys.path.insert(0, sys.path.dirname(pydevd_pycharm.__file__))

yield pydevd_pycharm, pydevd
8 changes: 5 additions & 3 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
PyCharm debugging support for PyXLL.
Requires:
- Python >= 3.7
- PyXLL >= 5.0.0
- PyCharm Professional
Expand Down Expand Up @@ -31,7 +32,7 @@
description="Adds PyCharm debugging support to PyXLL.",
long_description=long_description,
long_description_content_type='text/markdown',
version="0.2.1",
version="0.3.0",
packages=find_packages(),
include_package_data=True,
package_data={
Expand All @@ -45,7 +46,7 @@
"Tracker": "https://github.com/pyxll/pyxll-pycharm/issues",
},
classifiers=[
"Programming Language :: Python",
"Programming Language :: Python :: 3 :: Only",
"License :: OSI Approved :: MIT License",
"Operating System :: Microsoft :: Windows"
],
Expand All @@ -58,5 +59,6 @@
install_requires=[
"pyxll >= 5.0.0",
"pydevd-pycharm"
]
],
python_requires=">=3.7"
)

0 comments on commit 32b5c00

Please sign in to comment.