Skip to content
This repository was archived by the owner on Mar 27, 2019. It is now read-only.

Commit a5d5807

Browse files
authored
Read requirements.txt at the root of the repository (#45)
1 parent ba96703 commit a5d5807

File tree

20 files changed

+192
-18
lines changed

20 files changed

+192
-18
lines changed

Pipfile

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ name = "pypi"
88
[packages]
99

1010
jedi = {git = "git://github.com/sourcegraph/jedi.git", editable = true, ref = "9a3e7256df2e6099207fd7289141885ec17ebec7"}
11+
requirements = {git = "git://github.com/sourcegraph/requirements-parser.git", editable = true, ref = "69f1a9cb916b2995843c3ea9b988da46c9dd65c7"}
1112
opentracing = "*"
1213
lightstep = "*"
1314

Pipfile.lock

+11-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

langserver/fetch.py

+5-3
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,12 @@
1010
log = logging.getLogger(__name__)
1111

1212

13-
def fetch_dependency(module_name: str, install_path: str):
13+
def fetch_dependency(module_name: str, specifier: str, install_path: str):
1414
"""
1515
Shells out to PIP in order to download and unzip the named package into the specified path. This method only runs
1616
`pip download`, NOT `pip install`, so it's presumably safe.
1717
:param module_name: the name of the package to download
18+
:param specifier: the version specifier for the package
1819
:param install_path: the path in which to install the downloaded package
1920
"""
2021
with tempfile.TemporaryDirectory() as download_folder:
@@ -23,9 +24,9 @@ def fetch_dependency(module_name: str, install_path: str):
2324

2425
index_url = os.environ.get('INDEX_URL')
2526
if index_url is not None:
26-
result = pip.main(["download", "--no-deps", "-i", index_url, "-d", download_folder, module_name])
27+
result = pip.main(["download", "--no-deps", "-i", index_url, "-d", download_folder, module_name+specifier])
2728
else:
28-
result = pip.main(["download", "--no-deps", "-d", download_folder, module_name])
29+
result = pip.main(["download", "--no-deps", "-d", download_folder, module_name+specifier])
2930
if result != pip.status_codes.SUCCESS:
3031
log.error("Unable to fetch package %s", module_name)
3132
return
@@ -45,3 +46,4 @@ def fetch_dependency(module_name: str, install_path: str):
4546
result = subprocess.run(["tar", "-C", install_path, "-xjf", thing_abs])
4647
else:
4748
log.warning("Unrecognized package file: %s", thing, exc_info=True)
49+

langserver/requirements_parser.py

+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
from requirements import parse
2+
3+
def parse_requirements(req_path, file_system):
4+
"""
5+
Parses the pip requirements file located at req_path. Returns a map of package names
6+
to their version specifiers.
7+
8+
:param req_path: the path to the pip requirements file. Throws a FileNotFound or a FileException if
9+
req_path is not valid
10+
11+
:param file_system: the file system to use to open the requirements file @ req_path and any other
12+
recursive calls.
13+
14+
Known limitations:
15+
16+
- All requirements files with that use the '--find-links', '--index-url', '--extra-index-url'
17+
or '--no-index' flags are ignored.
18+
19+
- All requirements that don't use a version specifier (e.x. django>=1.5 ) are ignored.
20+
"""
21+
req_string = file_system.open(req_path)
22+
requirements = parse(req_string, current_path = req_path, file_system = file_system)
23+
return { req.name:req.specs for req in requirements if req.specifier}
24+
25+
def get_version_specifier_for_pkg(pkg, pkg_specifiers_map):
26+
"""
27+
Returns the specifier string to use for a given requirement. If
28+
pkg has no corresponding entry in the pkg_specifiers_map, a string
29+
representing that any version is allowed is returned.
30+
31+
:param pkg: the name of the package to get the version specifier for
32+
:param pkg_specifiers_map: a map of packages to their respective version specifiers
33+
from a parsed requirements file
34+
"""
35+
36+
specifier_strs = []
37+
for spec in pkg_specifiers_map.get(pkg, [""]):
38+
specifier_strs.append("".join(spec))
39+
40+
return ",".join(specifier_strs)

langserver/workspace.py

+23-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
from .config import GlobalConfig
2-
from .fs import FileSystem, LocalFileSystem
2+
from .fs import FileSystem, LocalFileSystem, FileException
33
from .imports import get_imports
44
from .fetch import fetch_dependency
5+
from .requirements_parser import parse_requirements, get_version_specifier_for_pkg
56
from typing import Dict, Set, List
67

78
import logging
@@ -272,7 +273,8 @@ def find_external_module(self, qualified_name: str) -> Module:
272273
if package_name not in self.fetched:
273274
self.indexing_lock.acquire()
274275
self.fetched.add(package_name)
275-
fetch_dependency(package_name, self.PACKAGES_PATH)
276+
specifier = self.get_ext_pkg_version_specifier(package_name)
277+
fetch_dependency(package_name, specifier, self.PACKAGES_PATH)
276278
self.index_external_modules()
277279
self.indexing_lock.release()
278280
the_module = self.dependencies.get(qualified_name, None)
@@ -281,6 +283,25 @@ def find_external_module(self, qualified_name: str) -> Module:
281283
else:
282284
return the_module
283285

286+
def get_ext_pkg_version_specifier(self, package_name):
287+
"""
288+
Gets the version specifier to use after parsing the project's requirements file.
289+
290+
(See limitations and caveats in .requirements_parser.parse_requirements()
291+
and .requirements_parser.get_version_specifier_for_pkg()).
292+
293+
If a requirements file isn't found at the root of the repo, or if there was an error parsing it,
294+
a string representing that any version is allowed is returned.
295+
"""
296+
pkg_specifiers_map = {}
297+
try:
298+
pkg_specifiers_map = parse_requirements("requirements.txt", self.fs)
299+
except (FileException, FileNotFoundError) as e:
300+
log.warning("error parsing requirements file for {}, err: {}".format(self.PROJECT_ROOT, e))
301+
pass
302+
303+
return get_version_specifier_for_pkg(package_name, pkg_specifiers_map)
304+
284305
def index_external_modules(self):
285306
for path in os.listdir(self.PACKAGES_PATH):
286307
if path not in self.indexed_folders:

test/repos/dep_versioning/.DS_Store

-8 KB
Binary file not shown.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[packages]
2+
3+
testfarhan = ">0.3,<0.5"

test/repos/dep_versioning_between/Pipfile.lock

+38
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
testfarhan>0.3,<0.5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[packages]
2+
3+
testfarhan = ">0.3,<0.6,!=0.5"

test/repos/dep_versioning_between_multiple/Pipfile.lock

+38
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
testfarhan>0.3,<0.6,!=0.5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from testfarhan.farhantest import testfunc
2+
3+
print(testfunc())
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
testfarhan==0.1
+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from testfarhan.farhantest import testfunc
2+
3+
print(testfunc())
+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from testfarhan.farhantest import testfunc
2+
3+
print(testfunc())

test/test_dep_versioning.py

+18-12
Original file line numberDiff line numberDiff line change
@@ -3,22 +3,28 @@
33
import pytest
44

55

6-
@pytest.fixture()
7-
def workspace():
8-
workspace = Harness("repos/dep_versioning")
9-
workspace.initialize("repos/dep_versioning?" + str(uuid.uuid4()))
10-
yield workspace
11-
workspace.exit()
6+
@pytest.fixture(params=[
7+
# tuples of the repo for the test, along
8+
# with the expected doc_string for the hover
9+
# in that repo
10+
("repos/dep_versioning_fixed", "this is version 0.1"),
11+
("repos/dep_versioning_between", "this is version 0.4"),
12+
("repos/dep_versioning_between_multiple", "this is version 0.4"),
13+
("repos/dep_versioning_none", "this is version 0.6")
14+
])
15+
def test_data(request):
16+
repo_path, expected_doc_string = request.param
1217

18+
workspace = Harness(repo_path)
19+
workspace.initialize(repo_path + str(uuid.uuid4()))
20+
yield (workspace, expected_doc_string)
1321

14-
"""
15-
This test should pass as long as we do not fetch results from the correct version of dependencies .
16-
Once we *do*, this test should be updated so the hover returns 'this is version 0.1'.
17-
"""
22+
workspace.exit()
1823

1924

2025
class TestDependencyVersioning:
21-
def test_dep_version(self, workspace):
26+
def test_dep_download_specified_version(self, test_data):
27+
workspace, expected_doc_string = test_data
2228
uri = "file:///test.py"
2329
character, line = 6, 2
2430
result = workspace.hover(uri, line, character)
@@ -28,6 +34,6 @@ def test_dep_version(self, workspace):
2834
'language': 'python',
2935
'value': 'def testfunc()'
3036
},
31-
'this is version 0.2'
37+
expected_doc_string
3238
]
3339
}

0 commit comments

Comments
 (0)