Skip to content

Commit ed38ea2

Browse files
Pass PTVSD folder name to the new debug adapter executable (#7152)
* Get PR validation going, fix unit tests later * Don't flatten the folder structure too much * Longer comment * Fix service registry unit tests * Add fallback to base PTVSD path * Update ptvsd folder retrieval (cache path) * Fix existing tests * Mock things in setup() function when possible * Don't mix then and await * More tests * Lol undo suite name change * don't call __iter__ directly * Update src/client/debugger/extension/adapter/factory.ts Co-Authored-By: Eric Snow <[email protected]> * Renaming in python scripts (code review) * Add fallback to SpecifierSet * TS code review * Python unit tests + error handling * Move to a folder and fix things * Use latest version of ptvsd * Undo formatting changes * Update packaging version * Add path to pythonfiles * forgot python subfolder * try updating pytest in test-requirements * Add packaging to test requirements * Forgot copyright * Undo pytest version change * Install ptvsd wheels during tests step not compile * yml indentation * Apply suggestions from code review Co-Authored-By: Eric Snow <[email protected]> * More code review changes * more review changes * More review changes * more changes * remove need for ptvsd_version function * Forgot to import re * Fix tests * Apply suggestions from code review Co-Authored-By: Eric Snow <[email protected]> * move path constants to __init__.py * Add safeguard for ptvsd version during dev * Add wheel support for remote debugging * Fix tests * Revert remote debugging changes * Add todo comment * Disable linting for the todo
1 parent 912f5a7 commit ed38ea2

File tree

16 files changed

+535
-150
lines changed

16 files changed

+535
-150
lines changed

build/ci/templates/steps/build_compile.yml

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -36,15 +36,6 @@ steps:
3636
displayName: "pip install requirements"
3737
condition: and(succeeded(), eq(variables['build'], 'true'))
3838
39-
- task: PythonScript@0
40-
displayName: "Install PTVSD wheels"
41-
inputs:
42-
scriptSource: "filePath"
43-
scriptPath: "./pythonFiles/install_ptvsd.py"
44-
arguments: "--ci"
45-
failOnStderr: true
46-
condition: and(succeeded(), eq(variables['build'], 'true'))
47-
4839
- bash: npm run clean
4940
displayName: "Clean"
5041
condition: and(succeeded(), eq(variables['build'], 'true'))

build/ci/templates/test_phases.yml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,15 @@ steps:
138138
displayName: 'pip install ipython requirements'
139139
condition: and(succeeded(), eq(variables['NeedsIPythonReqs'], 'true'))
140140
141+
- task: PythonScript@0
142+
displayName: "Install PTVSD wheels"
143+
inputs:
144+
scriptSource: "filePath"
145+
scriptPath: "./pythonFiles/install_ptvsd.py"
146+
arguments: "--ci"
147+
failOnStderr: true
148+
condition: contains(variables['TestsToRun'], 'testUnitTests')
149+
141150
# Run the Python unit tests in our codebase. Produces a JUnit-style log file that
142151
# will be uploaded after all tests are complete.
143152
#

build/test-requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,4 @@ rope
1515
flask
1616
django
1717
isort
18+
packaging==19.2

pythonFiles/install_ptvsd.py

Lines changed: 25 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -5,50 +5,56 @@
55
import urllib.request
66
import sys
77

8-
ROOT_DIRNAME = path.dirname(path.dirname(path.abspath(__file__)))
9-
REQUIREMENTS_PATH = path.join(ROOT_DIRNAME, "requirements.txt")
10-
PYTHONFILES_PATH = path.join(ROOT_DIRNAME, "pythonFiles", "lib", "python")
8+
ROOT = path.dirname(path.dirname(path.abspath(__file__)))
9+
REQUIREMENTS = path.join(ROOT, "requirements.txt")
10+
PYTHONFILES = path.join(ROOT, "pythonFiles", "lib", "python")
1111
PYPI_PTVSD_URL = "https://pypi.org/pypi/ptvsd/json"
1212

1313

1414
def install_ptvsd():
15-
# If we are in CI use the packaging module installed in PYTHONFILES_PATH.
15+
# If we are in CI use the packaging module installed in PYTHONFILES.
1616
if len(sys.argv) == 2 and sys.argv[1] == "--ci":
17-
sys.path.insert(0, PYTHONFILES_PATH)
17+
sys.path.insert(0, PYTHONFILES)
1818
from packaging.requirements import Requirement
1919

20-
with open(REQUIREMENTS_PATH, "r", encoding="utf-8") as requirements:
21-
for line in requirements:
22-
package_requirement = Requirement(line)
23-
if package_requirement.name != "ptvsd":
24-
continue
25-
requirement_specifier = package_requirement.specifier
26-
ptvsd_version = next(requirement_specifier.__iter__()).version
20+
with open(REQUIREMENTS, "r", encoding="utf-8") as reqsfile:
21+
for line in reqsfile:
22+
pkgreq = Requirement(line)
23+
if pkgreq.name == "ptvsd":
24+
specs = pkgreq.specifier
25+
version = next(iter(specs)).version
26+
break
27+
28+
try:
29+
version
30+
except NameError:
31+
raise Exception("ptvsd requirement not found.")
2732

2833
# Response format: https://warehouse.readthedocs.io/api-reference/json/#project
2934
with urllib.request.urlopen(PYPI_PTVSD_URL) as response:
3035
json_response = json.loads(response.read())
3136
releases = json_response["releases"]
3237

3338
# Release metadata format: https://github.com/pypa/interoperability-peps/blob/master/pep-0426-core-metadata.rst
34-
for wheel_info in releases[ptvsd_version]:
39+
for wheel_info in releases[version]:
3540
# Download only if it's a 3.7 wheel.
3641
if not wheel_info["python_version"].endswith(("37", "3.7")):
3742
continue
3843
filename = wheel_info["filename"].rpartition(".")[0] # Trim the file extension.
39-
ptvsd_path = path.join(PYTHONFILES_PATH, filename)
44+
ptvsd_path = path.join(PYTHONFILES, filename)
4045

4146
with urllib.request.urlopen(wheel_info["url"]) as wheel_response:
4247
wheel_file = BytesIO(wheel_response.read())
43-
# Extract only the contents of the ptvsd subfolder.
44-
prefix = path.join(f"ptvsd-{ptvsd_version}.data", "purelib", "ptvsd")
48+
# Extract only the contents of the purelib subfolder (parent folder of ptvsd),
49+
# since ptvsd files rely on the presence of a 'ptvsd' folder.
50+
prefix = path.join(f"ptvsd-{version}.data", "purelib")
4551

4652
with ZipFile(wheel_file, "r") as wheel:
4753
for zip_info in wheel.infolist():
48-
if not zip_info.filename.startswith(prefix):
49-
continue
54+
# Normalize path for Windows, the wheel folder structure uses forward slashes.
55+
normalized = path.normpath(zip_info.filename)
5056
# Flatten the folder structure.
51-
zip_info.filename = zip_info.filename.split(prefix)[-1]
57+
zip_info.filename = normalized.split(prefix)[-1]
5258
wheel.extract(zip_info, ptvsd_path)
5359

5460

pythonFiles/ptvsd_folder_name.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# Copyright (c) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT License.
3+
4+
import sys
5+
import os.path
6+
7+
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
8+
PYTHONFILES = os.path.join(ROOT, "pythonFiles", "lib", "python")
9+
REQUIREMENTS = os.path.join(ROOT, "requirements.txt")
10+
11+
sys.path.insert(0, PYTHONFILES)
12+
13+
from packaging.requirements import Requirement
14+
from packaging.tags import sys_tags
15+
16+
sys.path.remove(PYTHONFILES)
17+
18+
19+
def ptvsd_folder_name():
20+
"""Return the folder name for the bundled PTVSD wheel compatible with the new debug adapter."""
21+
22+
with open(REQUIREMENTS, "r", encoding="utf-8") as reqsfile:
23+
for line in reqsfile:
24+
pkgreq = Requirement(line)
25+
if pkgreq.name == "ptvsd":
26+
specs = pkgreq.specifier
27+
try:
28+
spec, = specs
29+
version = spec.version
30+
except:
31+
# Fallpack to use base PTVSD path.
32+
print(PYTHONFILES, end="")
33+
return
34+
break
35+
36+
try:
37+
for tag in sys_tags():
38+
folder_name = f"ptvsd-{version}-{tag.interpreter}-{tag.abi}-{tag.platform}"
39+
folder_path = os.path.join(PYTHONFILES, folder_name)
40+
if os.path.exists(folder_path):
41+
print(folder_path, end="")
42+
return
43+
except:
44+
# Fallback to use base PTVSD path no matter the exception.
45+
print(PYTHONFILES, end="")
46+
return
47+
48+
# Default fallback to use base PTVSD path.
49+
print(PYTHONFILES, end="")
50+
51+
52+
if __name__ == "__main__":
53+
ptvsd_folder_name()

pythonFiles/tests/__init__.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,13 @@
11
# Copyright (c) Microsoft Corporation. All rights reserved.
22
# Licensed under the MIT License.
3+
import os.path
4+
5+
TEST_ROOT = os.path.dirname(__file__)
6+
SRC_ROOT = os.path.dirname(TEST_ROOT)
7+
PROJECT_ROOT = os.path.dirname(SRC_ROOT)
8+
IPYTHON_ROOT = os.path.join(SRC_ROOT, "ipython")
9+
TESTING_TOOLS_ROOT = os.path.join(SRC_ROOT, "testing_tools")
10+
DEBUG_ADAPTER_ROOT = os.path.join(SRC_ROOT, "debug_adapter")
11+
12+
PYTHONFILES = os.path.join(SRC_ROOT, "lib", "python")
13+
REQUIREMENTS = os.path.join(PROJECT_ROOT, "requirements.txt")

pythonFiles/tests/__main__.py

Lines changed: 11 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,26 +2,22 @@
22
# Licensed under the MIT License.
33

44
import argparse
5-
import os.path
65
import sys
76

87
import pytest
98

10-
11-
TEST_ROOT = os.path.dirname(__file__)
12-
SRC_ROOT = os.path.dirname(TEST_ROOT)
13-
PROJECT_ROOT = os.path.dirname(SRC_ROOT)
14-
IPYTHON_ROOT = os.path.join(SRC_ROOT, 'ipython')
15-
TESTING_TOOLS_ROOT = os.path.join(SRC_ROOT, 'testing_tools')
9+
from . import DEBUG_ADAPTER_ROOT, IPYTHON_ROOT, SRC_ROOT, TEST_ROOT, TESTING_TOOLS_ROOT
1610

1711

1812
def parse_args():
1913
parser = argparse.ArgumentParser()
2014
# To mark a test as functional: (decorator) @pytest.mark.functional
21-
parser.add_argument('--functional', dest='markers',
22-
action='append_const', const='functional')
23-
parser.add_argument('--no-functional', dest='markers',
24-
action='append_const', const='not functional')
15+
parser.add_argument(
16+
"--functional", dest="markers", action="append_const", const="functional"
17+
)
18+
parser.add_argument(
19+
"--no-functional", dest="markers", action="append_const", const="not functional"
20+
)
2521
args, remainder = parser.parse_known_args()
2622

2723
ns = vars(args)
@@ -32,20 +28,18 @@ def parse_args():
3228
def main(pytestargs, markers=None):
3329
sys.path.insert(1, IPYTHON_ROOT)
3430
sys.path.insert(1, TESTING_TOOLS_ROOT)
31+
sys.path.insert(1, DEBUG_ADAPTER_ROOT)
3532

36-
pytestargs = [
37-
'--rootdir', SRC_ROOT,
38-
TEST_ROOT,
39-
] + pytestargs
33+
pytestargs = ["--rootdir", SRC_ROOT, TEST_ROOT] + pytestargs
4034
for marker in reversed(markers or ()):
4135
pytestargs.insert(0, marker)
42-
pytestargs.insert(0, '-m')
36+
pytestargs.insert(0, "-m")
4337

4438
ec = pytest.main(pytestargs)
4539
return ec
4640

4741

48-
if __name__ == '__main__':
42+
if __name__ == "__main__":
4943
mainkwargs, pytestargs = parse_args()
5044
ec = main(pytestargs, **mainkwargs)
5145
sys.exit(ec)
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Copyright (c) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT License.
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
# Copyright (c) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT License.
3+
import sys
4+
5+
if sys.version_info[:2] != (3, 7):
6+
import unittest
7+
8+
raise unittest.SkipTest("PTVSD wheels shipped for Python 3.7 only")
9+
10+
import os.path
11+
import pytest
12+
import re
13+
14+
from unittest.mock import patch, mock_open
15+
from packaging.tags import sys_tags
16+
from ptvsd_folder_name import ptvsd_folder_name
17+
18+
from .. import PYTHONFILES, REQUIREMENTS
19+
20+
21+
def open_requirements_with_ptvsd():
22+
return patch(
23+
"ptvsd_folder_name.open", mock_open(read_data="jedi==0.15.1\nptvsd==5.0.0")
24+
)
25+
26+
27+
def open_requirements_without_ptvsd():
28+
return patch("ptvsd_folder_name.open", mock_open(read_data="jedi==0.15.1\n"))
29+
30+
31+
class TestPtvsdFolderName:
32+
"""Unit tests for the script retrieving the PTVSD folder name for the PTVSD wheels experiment."""
33+
34+
def test_requirement_exists_folder_exists(self, capsys):
35+
# Return the first constructed folder path as existing.
36+
37+
patcher = patch("os.path.exists")
38+
mock_exists = patcher.start()
39+
mock_exists.side_effect = lambda p: True
40+
tag = next(sys_tags())
41+
folder = "ptvsd-5.0.0-{}-{}-{}".format(tag.interpreter, tag.abi, tag.platform)
42+
43+
with open_requirements_with_ptvsd():
44+
ptvsd_folder_name()
45+
46+
patcher.stop()
47+
expected = os.path.join(PYTHONFILES, folder)
48+
captured = capsys.readouterr()
49+
assert captured.out == expected
50+
51+
def test_ptvsd_requirement_once(self):
52+
reqs = [
53+
line
54+
for line in open(REQUIREMENTS, "r", encoding="utf-8")
55+
if re.match("ptvsd==", line)
56+
]
57+
assert len(reqs) == 1
58+
59+
def test_no_ptvsd_requirement(self, capsys):
60+
with open_requirements_without_ptvsd() as p:
61+
ptvsd_folder_name()
62+
63+
expected = PYTHONFILES
64+
captured = capsys.readouterr()
65+
assert captured.out == expected
66+
67+
def test_no_wheel_folder(self, capsys):
68+
# Return none of of the constructed paths as existing,
69+
# ptvsd_folder_name() should return the path to default ptvsd.
70+
patcher = patch("os.path.exists")
71+
mock_no_exist = patcher.start()
72+
mock_no_exist.side_effect = lambda p: False
73+
74+
with open_requirements_with_ptvsd() as p:
75+
ptvsd_folder_name()
76+
77+
patcher.stop()
78+
expected = PYTHONFILES
79+
captured = capsys.readouterr()
80+
assert captured.out == expected
81+
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# Copyright (c) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT License.
3+
4+
import sys
5+
6+
if sys.version_info[:2] != (3, 7):
7+
import unittest
8+
9+
raise unittest.SkipTest("PTVSD wheels shipped for Python 3.7 only")
10+
11+
import os.path
12+
import pytest
13+
import subprocess
14+
15+
from packaging.requirements import Requirement
16+
from .. import PYTHONFILES, REQUIREMENTS, SRC_ROOT
17+
18+
ARGV = ["python", os.path.join(SRC_ROOT, "ptvsd_folder_name.py")]
19+
PREFIX = "ptvsd=="
20+
21+
with open(REQUIREMENTS, "r", encoding="utf-8") as reqsfile:
22+
for line in reqsfile:
23+
if line.startswith(PREFIX):
24+
VERSION = line[len(PREFIX) :].strip()
25+
break
26+
27+
28+
def ptvsd_paths(*platforms):
29+
paths = set()
30+
for platform in platforms:
31+
folder = "ptvsd-{}-cp37-cp37m-{}".format(VERSION, platform)
32+
paths.add(os.path.join(PYTHONFILES, folder))
33+
return paths
34+
35+
36+
@pytest.mark.functional
37+
class TestPtvsdFolderNameFunctional:
38+
"""Functional tests for the script retrieving the PTVSD folder name for the PTVSD wheels experiment."""
39+
40+
def test_ptvsd_folder_name_nofail(self):
41+
output = subprocess.check_output(ARGV, universal_newlines=True)
42+
assert output != PYTHONFILES
43+
44+
@pytest.mark.skipif(sys.platform != "darwin", reason="macOS functional test")
45+
def test_ptvsd_folder_name_macos(self):
46+
output = subprocess.check_output(ARGV, universal_newlines=True)
47+
assert output in ptvsd_paths("macosx_10_13_x86_64")
48+
49+
@pytest.mark.skipif(sys.platform != "win32", reason="Windows functional test")
50+
def test_ptvsd_folder_name_windows(self):
51+
output = subprocess.check_output(ARGV, universal_newlines=True)
52+
assert output in ptvsd_paths("win32", "win_amd64")
53+
54+
@pytest.mark.skipif(sys.platform != "linux", reason="Linux functional test")
55+
def test_ptvsd_folder_name_linux(self):
56+
output = subprocess.check_output(ARGV, universal_newlines=True)
57+
assert output in ptvsd_paths(
58+
"manylinux1_i686", "manylinux1_x86_64", "manylinux2010_x86_64"
59+
)

requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,4 @@ isort==4.3.21
44
ptvsd==5.0.0a4
55
pyparsing==2.4.0
66
six==1.12.0
7-
packaging==19.1
7+
packaging==19.2

src/client/api.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ export function buildApi(ready: Promise<any>) {
4141
return Promise.reject(ex);
4242
}),
4343
debug: {
44+
// tslint:disable-next-line:no-suspicious-comment
45+
// TODO: Add support for ptvsd wheels experiment, see https://github.com/microsoft/vscode-python/issues/7549
4446
async getRemoteLauncherCommand(host: string, port: number, waitUntilDebuggerAttaches: boolean = true): Promise<string[]> {
4547
return new RemoteDebuggerExternalLauncherScriptProvider().getLauncherArgs({ host, port, waitUntilDebuggerAttaches });
4648
}

0 commit comments

Comments
 (0)