Skip to content

Commit e8b6e84

Browse files
authored
fix: [OSM-2206] skip unresolved local packages (#255)
1 parent b42e5fc commit e8b6e84

File tree

8 files changed

+80
-25
lines changed

8 files changed

+80
-25
lines changed

lib/dependencies/inspect-implementation.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,11 @@ import * as subProcess from './sub-process';
66
import { DepGraph } from '@snyk/dep-graph';
77
import { buildDepGraph, PartialDepTree } from './build-dep-graph';
88
import { FILENAMES } from '../types';
9-
import { EmptyManifestError, RequiredPackagesMissingError } from '../errors';
9+
import {
10+
EmptyManifestError,
11+
RequiredPackagesMissingError,
12+
UnparsableRequirementError,
13+
} from '../errors';
1014

1115
const returnedTargetFile = (originalTargetFile) => {
1216
const basename = path.basename(originalTargetFile);
@@ -271,6 +275,10 @@ export async function inspectInstalledDeps(
271275

272276
throw new RequiredPackagesMissingError(errMsg);
273277
}
278+
279+
if (error.indexOf('Unparsable requirement line') !== -1) {
280+
throw new UnparsableRequirementError(error);
281+
}
274282
}
275283

276284
throw error;

lib/errors.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
export enum PythonPluginErrorNames {
22
EMPTY_MANIFEST_ERROR = 'EMPTY_MANIFEST_ERROR',
33
REQUIRED_PACKAGES_MISSING_ERROR = 'REQUIRED_PACKAGES_MISSING_ERROR',
4+
UNPARSABLE_REQUIREMENT_ERROR = 'UNPARSABLE_REQUIREMENT_ERROR',
45
}
56

67
export class EmptyManifestError extends Error {
@@ -16,3 +17,10 @@ export class RequiredPackagesMissingError extends Error {
1617
this.name = PythonPluginErrorNames.REQUIRED_PACKAGES_MISSING_ERROR;
1718
}
1819
}
20+
21+
export class UnparsableRequirementError extends Error {
22+
constructor(message: string) {
23+
super(message);
24+
this.name = PythonPluginErrorNames.UNPARSABLE_REQUIREMENT_ERROR;
25+
}
26+
}

pysrc/constants.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,10 @@ def discover(cls, requirements_file_path):
2323
return cls.SETUPTOOLS
2424

2525
return cls.PIP
26+
27+
DEFAULT_OPTIONS = {
28+
"allow_missing":False,
29+
"dev_deps":False,
30+
"only_provenance":False,
31+
"allow_empty":False
32+
}

pysrc/pip_resolve.py

Lines changed: 14 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
import pipfile
1010
import codecs
1111
from operator import le, lt, gt, ge, eq, ne
12-
from constants import DepsManager
12+
from constants import DEFAULT_OPTIONS, DepsManager
1313

1414
import pkg_resources
1515

@@ -349,7 +349,7 @@ def get_requirements_for_setuptools(requirements_file_path):
349349
with open(requirements_file_path, 'r') as f:
350350
setup_py_file_content = f.read()
351351
requirements_data = setup_file.parse_requirements(setup_py_file_content)
352-
req_list = list(requirements.parse(requirements_data))
352+
req_list = [req for req in requirements.parse(requirements_data) if req is not None]
353353

354354
provenance = setup_file.get_provenance(setup_py_file_content)
355355
for req in req_list:
@@ -364,7 +364,7 @@ def get_requirements_for_setuptools(requirements_file_path):
364364
return req_list
365365

366366

367-
def get_requirements_for_pip(requirements_file_path):
367+
def get_requirements_for_pip(requirements_file_path, options):
368368
"""Get requirements for a pip project.
369369
370370
Note:
@@ -381,7 +381,7 @@ def get_requirements_for_pip(requirements_file_path):
381381
encoding = detect_encoding_by_bom(requirements_file_path)
382382

383383
with io.open(requirements_file_path, 'r', encoding=encoding) as f:
384-
req_list = list(requirements.parse(f))
384+
req_list = list(requirements.parse(f, options))
385385

386386
req_list = filter_requirements(req_list)
387387

@@ -409,7 +409,7 @@ def filter_requirements(req_list):
409409
return req_list
410410

411411

412-
def get_requirements_list(requirements_file_path, dev_deps=False):
412+
def get_requirements_list(requirements_file_path, options=DEFAULT_OPTIONS):
413413
"""Retrieves the requirements from the requirements file
414414
415415
The requirements can be retrieved from requirements.txt, Pipfile or setup.py
@@ -423,11 +423,11 @@ def get_requirements_list(requirements_file_path, dev_deps=False):
423423
empty list: if no requirements were found in the requirements file.
424424
"""
425425
if deps_manager is DepsManager.PIPENV:
426-
req_list = get_requirements_for_pipenv(requirements_file_path, dev_deps)
426+
req_list = get_requirements_for_pipenv(requirements_file_path, options.dev_deps)
427427
elif deps_manager is DepsManager.SETUPTOOLS:
428428
req_list = get_requirements_for_setuptools(requirements_file_path)
429429
else:
430-
req_list = get_requirements_for_pip(requirements_file_path)
430+
req_list = get_requirements_for_pip(requirements_file_path, options)
431431

432432
return req_list
433433

@@ -441,10 +441,7 @@ def canonicalize_package_name(name):
441441

442442
def create_dependencies_tree_by_req_file_path(
443443
requirements_file_path,
444-
allow_missing=False,
445-
dev_deps=False,
446-
only_provenance=False,
447-
allow_empty=False
444+
options=DEFAULT_OPTIONS,
448445
):
449446
# TODO: normalise package names before any other processing - this should
450447
# help reduce the amount of `in place` conversions.
@@ -463,7 +460,7 @@ def create_dependencies_tree_by_req_file_path(
463460
dist_tree = utils.construct_tree(dist_index)
464461

465462
# create a list of dependencies from the dependencies file
466-
required = get_requirements_list(requirements_file_path, dev_deps=dev_deps)
463+
required = get_requirements_list(requirements_file_path, options)
467464

468465
# Handle optional dependencies/arbitrary dependencies
469466
optional_dependencies = utils.establish_optional_dependencies(
@@ -475,7 +472,7 @@ def create_dependencies_tree_by_req_file_path(
475472

476473
top_level_provenance_map = {}
477474

478-
if not required and not allow_empty:
475+
if not required and not options.allow_empty:
479476
msg = 'No dependencies detected in manifest.'
480477
sys.exit(msg)
481478
else:
@@ -490,7 +487,7 @@ def create_dependencies_tree_by_req_file_path(
490487
top_level_provenance_map[canonicalize_package_name(r.name)] = r.original_name
491488
if missing_package_names:
492489
msg = 'Required packages missing: ' + (', '.join(missing_package_names))
493-
if allow_missing:
490+
if options.allow_missing:
494491
sys.stderr.write(msg + "\n")
495492
else:
496493
sys.exit(msg)
@@ -501,8 +498,8 @@ def create_dependencies_tree_by_req_file_path(
501498
top_level_requirements,
502499
optional_dependencies,
503500
requirements_file_path,
504-
allow_missing,
505-
only_provenance,
501+
options.allow_missing,
502+
options.only_provenance,
506503
top_level_provenance_map,
507504
)
508505

@@ -542,10 +539,7 @@ def main():
542539

543540
create_dependencies_tree_by_req_file_path(
544541
args.requirements,
545-
allow_missing=args.allow_missing,
546-
dev_deps=args.dev_deps,
547-
only_provenance=args.only_provenance,
548-
allow_empty=args.allow_empty,
542+
args,
549543
)
550544

551545

pysrc/requirements/parser.py

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,16 @@
22

33
import os
44
import warnings
5-
import re
5+
import sys
66

77
from .requirement import Requirement
88

9-
def parse(req_str_or_file):
9+
def parse(req_str_or_file, options={
10+
"allow_missing":False,
11+
"dev_deps":False,
12+
"only_provenance":False,
13+
"allow_empty":False
14+
}):
1015
"""
1116
Parse a requirements file into a list of Requirements
1217
@@ -59,7 +64,7 @@ def parse(req_str_or_file):
5964
new_file_path = os.path.join(os.path.dirname(filename or '.'),
6065
new_filename)
6166
with open(new_file_path) as f:
62-
for requirement in parse(f):
67+
for requirement in parse(f, options):
6368
yield requirement
6469
elif line.startswith('-f') or line.startswith('--find-links') or \
6570
line.startswith('-i') or line.startswith('--index-url') or \
@@ -75,7 +80,13 @@ def parse(req_str_or_file):
7580
line.split()[0])
7681
continue
7782
else:
78-
req = Requirement.parse(line)
83+
try:
84+
req = Requirement.parse(line)
85+
except Exception as e:
86+
if options.allow_missing:
87+
warnings.warn("Skipping line (%s).\n Couldn't process: (%s)." %(line.split()[0], e))
88+
continue
89+
sys.exit('Unparsable requirement line (%s)' %(e))
7990
req.provenance = (
8091
filename,
8192
original_line_idxs[0] + 1,

test/system/inspect.spec.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -740,6 +740,31 @@ describe('inspect', () => {
740740
async () => await inspect('.', FILENAMES.pip.manifest)
741741
).rejects.toThrow('Required packages missing: markupsafe');
742742
});
743+
744+
it('should fail on nonexistent referenced local depedency', async () => {
745+
const workspace = 'pip-app-local-nonexistent-file';
746+
testUtils.chdirWorkspaces(workspace);
747+
testUtils.ensureVirtualenv(workspace);
748+
tearDown = testUtils.activateVirtualenv(workspace);
749+
750+
await expect(inspect('.', FILENAMES.pip.manifest)).rejects.toThrow(
751+
"Unparsable requirement line ([Errno 2] No such file or directory: './lib/nonexistent/setup.py')"
752+
);
753+
});
754+
755+
it('should not fail on nonexistent referenced local depedency when --skip-unresolved', async () => {
756+
const workspace = 'pip-app-local-nonexistent-file';
757+
testUtils.chdirWorkspaces(workspace);
758+
testUtils.ensureVirtualenv(workspace);
759+
tearDown = testUtils.activateVirtualenv(workspace);
760+
761+
const result = await inspect('.', FILENAMES.pip.manifest, {
762+
allowMissing: true,
763+
allowEmpty: true,
764+
});
765+
766+
expect(result.dependencyGraph.toJSON()).not.toEqual({});
767+
});
743768
});
744769

745770
describe('Circular deps', () => {
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
This is a dummy file
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
./lib/nonexistent

0 commit comments

Comments
 (0)