Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions pdbpp_hijack_pdb.pth
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
# Doc: https://docs.python.org/3/library/site.html
# Code: https://github.com/blueyed/cpython/blob/1b293b600/Lib/site.py#L148-L187
#
# See `setup.py` for a similar setup for editable installs.
#
# Add our "_pdbpp_path_hack" to the beginning of `sys.path` if:
# 1. the environment variable PDBPP_HIJACK_PDB is trueish (as an int), defaults to 1.
# i.e. PDBPP_HIJACK_PDB=0 can be used to disable it.
Expand Down
76 changes: 71 additions & 5 deletions setup.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,20 @@
import os.path
from collections.abc import Mapping
from pathlib import Path
from typing import TYPE_CHECKING

from setuptools import setup
from setuptools.command.build_py import build_py
from setuptools.command.editable_wheel import _encode_pth, _StaticPth, editable_wheel

readme_path = os.path.join(os.path.dirname(__file__), "README.rst")
changelog_path = os.path.join(os.path.dirname(__file__), "CHANGELOG")
if TYPE_CHECKING:
from setuptools.command.editable_wheel import (
Distribution,
EditableStrategy,
WheelFile,
)

readme_path = Path(__file__).parent / "README.rst"
changelog_path = Path(__file__).parent / "CHANGELOG"

with open(readme_path, encoding="utf-8") as fh:
readme = fh.read()
Expand All @@ -14,6 +24,58 @@
long_description = readme + "\n\n" + changelog


class CustomEditablePth(_StaticPth):
"""Inserts a custom module (path_hack_name) as the first element of sys.path"""

def __init__(
self, path_hack_name: str, dist: Distribution, name: str, src_dir: Path
) -> None:
super().__init__(dist, name, [src_dir])
self.path_hack = src_dir / path_hack_name

def __call__(
self, wheel: WheelFile, files: list[str], mapping: Mapping[str, str]
) -> None:
assert all([p.resolve().exists() for p in self.path_entries]), (
"module path does not exist"
)

path_hack = f"import sys; sys.path.insert(0, \"{self.path_hack.resolve()}\") if int(os.environ.get('PDBPP_HIJACK_PDB', 1)) else None"
contents = _encode_pth(
"\n".join(
[
path_hack,
self.path_entries[0].resolve().as_posix(),
]
),
)

wheel.writestr(f"__editable__.{self.name}.pth", contents)


class editable_install_with_pth_file(editable_wheel):
"""custom editable_wheel install so that `pip install -e .` also hijacks pdb with pdbpp"""

_path_hack_name: str = "_pdbpp_path_hack" # must match name of module in `./src/`

def _select_strategy(
self,
name: str,
tag: str,
build_lib: str | Path,
) -> EditableStrategy:
# note: this requires src-layout
src_dir = self.package_dir.get("", ".")

src_dir = Path(self.project_dir, src_dir)
return CustomEditablePth(
self._path_hack_name,
self.distribution,
name,
src_dir,
)


class build_py_with_pth_file(build_py):
"""Include the .pth file for this project, in the generated wheel."""

Expand All @@ -24,13 +86,17 @@ def run(self):

self.copy_file(
self.pth_file,
os.path.join(self.build_lib, self.pth_file),
Path(self.build_lib, self.pth_file),
preserve_mode=0,
)
print(f"build_py_with_pth_file: include {self.pth_file} in wheel")


setup(
cmdclass={"build_py": build_py_with_pth_file},
cmdclass={
"editable_wheel": editable_install_with_pth_file,
"build_py": build_py_with_pth_file,
},
platforms=[
"unix",
"linux",
Expand Down
6 changes: 0 additions & 6 deletions testing/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -212,9 +212,3 @@ def import_mock(name, *args):
yield m

return cm


def skip_with_missing_pth_file():
pth = os.path.join(sysconfig.get_path("purelib"), "pdbpp_hijack_pdb.pth")
if not os.path.exists(pth):
pytest.skip(f"Missing pth file ({pth}), editable install?")
80 changes: 77 additions & 3 deletions testing/test_integration.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
import glob
import importlib
import re
import subprocess
import sys
import sysconfig
from os import spawnl
from pathlib import Path
from shutil import which
from textwrap import dedent

import pexpect
import pytest

from .conftest import skip_with_missing_pth_file


def test_integration(pytester, readline_param):
with (pytester.path / "test_file.py").open("w") as fh:
Expand Down Expand Up @@ -60,7 +64,6 @@ def test_ipython(testdir):
- `up` used to crash due to conflicting `hidden_frames` attribute/method.
"""
pytest.importorskip("IPython")
skip_with_missing_pth_file()

child = testdir.spawn(
f"{sys.executable} -m IPython --colors=nocolor --simple-prompt",
Expand All @@ -75,3 +78,74 @@ def test_ipython(testdir):
child.sendeof()
child.sendline("y")
assert child.wait() == 0


def test_hijacking():
"""make sure that _pdbpp_path_hack is one of the first items in sys.path"""

spec = importlib.util.find_spec("pdbpp")
if spec is None or spec.origin is None:
return False

package_path = Path(spec.origin).parent

if package_path.parts[-1] == "src":
# editable install
path_entry = sys.path[2]
elif package_path.parts[-1] == "site-packages":
# full install
path_entry = sys.path[1]
else:
pytest.fail("Unknown install location/method?")

stdlib_path = sysconfig.get_path("stdlib")

path_hack_found = False
for path_entry in sys.path:
if "_pdbpp_path_hack" in path_entry:
path_hack_found = True

if path_entry != stdlib_path:
continue

if path_hack_found:
break

pytest.fail("stdlib path is not hijacked")


def test_editable_install_pth(tmp_path):
"""Make sure that running `pip install -e .` installs the .pth file for pdb hijacking"""

has_uv = which("uv")
if has_uv: # uv is faster in creating venvs
venv_command = f"uv venv --python={sys.executable} --seed {tmp_path}" # seed the env with pip
else:
venv_command = f"{sys.executable} -m venv {tmp_path}"

subprocess.check_call(venv_command.split())

python = tmp_path / "bin/python"

pdbpp_root = Path(__file__).parent.parent

install_command = f"pip install --no-deps -e {pdbpp_root}"
if has_uv:
install_command = f"uv {install_command} --python={python}"
else:
install_command = f"{python} -m {install_command}"

subprocess.check_call(install_command.split())

pths: list[Path] = list(
glob.glob((tmp_path / "lib/*/site-packages/__editable__.pdbpp*.pth").as_posix())
)

assert len(pths) == 1

editable_pth = Path(pths[0])
pth_contents = editable_pth.read_text()

pattern = r"sys.path.insert\(0, \".*_pdbpp_path_hack\"\)"
result = re.search(pattern, pth_contents)
assert result is not None
5 changes: 0 additions & 5 deletions testing/test_pdb.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,6 @@
import pdbpp
from pdbpp import DefaultConfig, Pdb, StringIO

from .conftest import skip_with_missing_pth_file

pygments_major, pygments_minor, _ = pygments_version.split(".")


Expand Down Expand Up @@ -5704,9 +5702,6 @@ def test_python_m_pdb_usage():

@pytest.mark.parametrize("PDBPP_HIJACK_PDB", (1, 0))
def test_python_m_pdb_uses_pdbpp_and_env(PDBPP_HIJACK_PDB, monkeypatch, tmpdir):
if PDBPP_HIJACK_PDB:
skip_with_missing_pth_file()

monkeypatch.setenv("PDBPP_HIJACK_PDB", str(PDBPP_HIJACK_PDB))

f = tmpdir.ensure("test.py")
Expand Down