From a180e50059ef7cbcce61468f36297617b61d1d69 Mon Sep 17 00:00:00 2001 From: Shaun Smith Date: Sun, 28 May 2017 00:42:35 +0100 Subject: [PATCH] feat: init python plugin from michael's POC --- .gitignore | 3 + .jscsrc | 14 ++ lib/index.js | 55 ++++++ lib/sub-process.js | 24 +++ package.json | 18 ++ plug/distPackage.py | 53 +++++ plug/package.py | 36 ++++ plug/pip_resolve.py | 92 +++++++++ plug/reqPackage.py | 61 ++++++ plug/requirements/__init__.py | 22 +++ plug/requirements/parser.py | 50 +++++ plug/requirements/requirement.py | 181 ++++++++++++++++++ plug/requirements/vcs.py | 30 +++ plug/utils.py | 60 ++++++ test/inspect.test.js | 81 ++++++++ .../requirements.txt | 1 + test/workspaces/pip-app/requirements.txt | 2 + 17 files changed, 783 insertions(+) create mode 100644 .gitignore create mode 100644 .jscsrc create mode 100644 lib/index.js create mode 100644 lib/sub-process.js create mode 100644 package.json create mode 100644 plug/distPackage.py create mode 100644 plug/package.py create mode 100644 plug/pip_resolve.py create mode 100644 plug/reqPackage.py create mode 100644 plug/requirements/__init__.py create mode 100644 plug/requirements/parser.py create mode 100644 plug/requirements/requirement.py create mode 100644 plug/requirements/vcs.py create mode 100644 plug/utils.py create mode 100644 test/inspect.test.js create mode 100644 test/workspaces/pip-app-deps-not-installed/requirements.txt create mode 100644 test/workspaces/pip-app/requirements.txt diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..b84f0c51 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/node_modules/ +__pycache__/ +*.pyc diff --git a/.jscsrc b/.jscsrc new file mode 100644 index 00000000..d28ffd0a --- /dev/null +++ b/.jscsrc @@ -0,0 +1,14 @@ +{ + "preset": "node-style-guide", + "requireCapitalizedComments": null, + "requireEarlyReturn": true, + "requireSpacesInAnonymousFunctionExpression": { + "beforeOpeningCurlyBrace": true, + "beforeOpeningRoundBrace": true + }, + "disallowSpacesInNamedFunctionExpression": { + "beforeOpeningRoundBrace": true + }, + "excludeFiles": ["node_modules/**"], + "disallowSpacesInFunction": null +} diff --git a/lib/index.js b/lib/index.js new file mode 100644 index 00000000..ccdde8de --- /dev/null +++ b/lib/index.js @@ -0,0 +1,55 @@ +var path = require('path'); +var subProcess = require('./sub-process'); + +module.exports = { + inspect: inspect, +}; + +function inspect(root, targetFile, args) { + return Promise.all([ + getMetaData(), + getDependencies(root, targetFile, args), + ]) + .then(function (result) { + return { + plugin: result[0], + package: result[1], + }; + }); +} + +function getMetaData() { + return subProcess.execute('python', ['--version']) + .then(function(res) { + return { + name: 'snyk-python-plugin', + runtime: res.stdout || res.stderr, // `python --version` sends to stderr + }; + }); +} + +function getDependencies(root, targetFile, args) { + return subProcess.execute( + 'python', + buildArgs(root, targetFile, args), + { cwd: root } + ) + .then(function (result) { + return JSON.parse(result.stdout); + }) + .catch(function (stderr) { + if (typeof stderr === 'string' && + stderr.indexOf('Required package missing') !== -1) { + throw new Error('Please run `pip install -r ' + targetFile + '`'); + } else { + throw new Error(stderr); + } + }); +} + +function buildArgs(root, targetFile, extraArgs) { + var args = [path.resolve(__dirname, '../plug/pip_resolve.py')]; + if (targetFile) { args.push(targetFile); } + if (extraArgs) { args.push(extraArgs); } + return args; +} diff --git a/lib/sub-process.js b/lib/sub-process.js new file mode 100644 index 00000000..c98d4dca --- /dev/null +++ b/lib/sub-process.js @@ -0,0 +1,24 @@ +var childProcess = require('child_process'); + +module.exports.execute = function (command, args, options) { + var spawnOptions = { shell: true }; + if (options && options.cwd) { + spawnOptions.cwd = options.cwd; + } + + return new Promise(function (resolve, reject) { + var stdout = ''; + var stderr = ''; + + var proc = childProcess.spawn(command, args, spawnOptions); + proc.stdout.on('data', function (data) { stdout = stdout + data; }); + proc.stderr.on('data', function (data) { stderr = stderr + data; }); + + proc.on('close', function (code) { + if (code !== 0) { + return reject(stdout || stderr); + } + resolve({ stdout: stdout, stderr: stderr }); + }); + }); +}; diff --git a/package.json b/package.json new file mode 100644 index 00000000..23ec2fd3 --- /dev/null +++ b/package.json @@ -0,0 +1,18 @@ +{ + "name": "snyk-python-plugin", + "version": "0.0.0-placeholder", + "description": "Snyk CLI Python plugin", + "main": "lib/index.js", + "scripts": { + "test": "tap `find ./test -name '*.test.js'`", + "lint": "jscs `find ./lib -name '*.js'` -v && jscs `find ./test -name '*.js'` -v" + }, + "author": "snyk.io", + "license": "Apache-2.0", + "devDependencies": { + "jscs": "^3.0.7", + "semantic-release": "^6.3.6", + "sinon": "^2.3.2", + "tap": "^10.3.2" + } +} diff --git a/plug/distPackage.py b/plug/distPackage.py new file mode 100644 index 00000000..4e978a0f --- /dev/null +++ b/plug/distPackage.py @@ -0,0 +1,53 @@ +from package import Package +from reqPackage import ReqPackage + + +class DistPackage(Package): + """Wrapper class for pkg_resources.Distribution instances + :param obj: pkg_resources.Distribution to wrap over + :param req: optional ReqPackage object to associate this + DistPackage with. This is useful for displaying the + tree in reverse + """ + + def __init__(self, obj, req=None): + super(DistPackage, self).__init__(obj) + self.version_spec = None + self.req = req + + def render_as_root(self, frozen): + if not frozen: + return '{0}=={1}'.format(self.project_name, self.version) + else: + return self.__class__.frozen_repr(self._obj) + + def render_as_branch(self, frozen): + assert self.req is not None + if not frozen: + parent_ver_spec = self.req.version_spec + parent_str = self.req.project_name + if parent_ver_spec: + parent_str += parent_ver_spec + return ( + '{0}=={1} [requires: {2}]' + ).format(self.project_name, self.version, parent_str) + else: + return self.render_as_root(frozen) + + def as_requirement(self): + """Return a ReqPackage representation of this DistPackage""" + return ReqPackage(self._obj.as_requirement(), dist=self) + + def as_required_by(self, req): + """Return a DistPackage instance associated to a requirement + This association is necessary for displaying the tree in + reverse. + :param ReqPackage req: the requirement to associate with + :returns: DistPackage instance + """ + return self.__class__(self._obj, req) + + def as_dict(self): + return {'key': self.key, + 'package_name': self.project_name, + 'installed_version': self.version} diff --git a/plug/package.py b/plug/package.py new file mode 100644 index 00000000..0962af02 --- /dev/null +++ b/plug/package.py @@ -0,0 +1,36 @@ +import pip + + +class Package(object): + """Abstract class for wrappers around objects that pip returns. + This class needs to be subclassed with implementations for + `render_as_root` and `render_as_branch` methods. + """ + + def __init__(self, obj): + self._obj = obj + self.project_name = obj.project_name + self.key = obj.key + + def render_as_root(self, frozen): + return NotImplementedError + + def render_as_branch(self, frozen): + return NotImplementedError + + def render(self, parent=None, frozen=False): + if not parent: + return self.render_as_root(frozen) + else: + return self.render_as_branch(frozen) + + @staticmethod + def frozen_repr(obj): + fr = pip.FrozenRequirement.from_dist(obj, []) + return str(fr).strip() + + def __getattr__(self, key): + return getattr(self._obj, key) + + def __repr__(self): + return '<{0}("{1}")>'.format(self.__class__.__name__, self.key) diff --git a/plug/pip_resolve.py b/plug/pip_resolve.py new file mode 100644 index 00000000..5d661426 --- /dev/null +++ b/plug/pip_resolve.py @@ -0,0 +1,92 @@ +import sys +import os +import argparse +import json +import requirements +import pip +import pkg_resources +import utils + + +def create_tree_of_packages_dependencies(dist_tree, packages_names, req_file_path): + """Create packages dependencies tree + :param dict tree: the package tree + :param set packages_names: set of select packages to be shown in the output. + :param req_file_path: the path to requirements.txt file + :rtype: dict + """ + DEPENDENCIES = 'dependencies' + FROM = 'from' + VERSION = 'version' + NAME = 'name' + VERSION_SEPARATOR = '@' + DIR_VERSION = '0.0.0' + PACKAGE_FORMAT_VERSION = 'packageFormatVersion' + + tree = utils.sorted_tree(dist_tree) + nodes = tree.keys() + key_tree = dict((k.key, v) for k, v in tree.items()) + + def get_children(n): return key_tree.get(n.key, []) + packages_as_dist_obj = [ + p for p in nodes if p.key in packages_names or p.project_name in packages_names] + + def create_children_recursive(root_package, key_tree): + children_packages_as_dist = key_tree[root_package[NAME].lower()] + for child_dist in children_packages_as_dist: + child_package = {NAME: child_dist.project_name, VERSION: child_dist.installed_version, + FROM: root_package[FROM] + + [child_dist.key + VERSION_SEPARATOR + + child_dist.installed_version]} + create_children_recursive(child_package, key_tree) + root_package[DEPENDENCIES] = { + child_dist.project_name: child_package} + return root_package + + def create_dir_as_root(): + dir_as_root = { NAME: os.path.basename(os.path.dirname(os.path.abspath(req_file_path))), VERSION: DIR_VERSION, + FROM: [os.path.basename(os.path.dirname(os.path.abspath(req_file_path)))], DEPENDENCIES: {}, + PACKAGE_FORMAT_VERSION: 'pip:0.0.1'} + return dir_as_root + + def create_package_as_root(package, dir_as_root): + package_as_root = {NAME: package.project_name.lower(), VERSION: package._obj._version, + FROM: ["{}{}{}".format(dir_as_root[NAME], VERSION_SEPARATOR, dir_as_root[VERSION])] + + ["{}{}{}".format(package.project_name.lower(), + VERSION_SEPARATOR, package._obj._version)]} + return package_as_root + dir_as_root = create_dir_as_root() + for package in packages_as_dist_obj: + package_as_root = create_package_as_root(package, dir_as_root) + package_tree = create_children_recursive(package_as_root, key_tree) + dir_as_root[DEPENDENCIES][package_as_root[NAME]] = package_tree + return dir_as_root + + +def create_dependencies_tree_by_req_file_path(requirements_file_path): + # get all installed packages + pkgs = pip.get_installed_distributions(local_only=False, skip=[]) + + # get all installed packages's distribution object + dist_index = utils.build_dist_index(pkgs) + + # get all installed distributions tree + dist_tree = utils.construct_tree(dist_index) + + # open the requirements.txt file and create dependencies tree out of it + with open(requirements_file_path, 'r') as requirements_file: + req_list = list(requirements.parse(requirements_file)) + required = [req.name for req in req_list] + installed = [p for p in dist_index] + for r in required: + if r.lower() not in installed: + sys.exit('Required package missing') + package_tree = create_tree_of_packages_dependencies( + dist_tree, [req.name for req in req_list], requirements_file_path) + print(json.dumps(package_tree)) + + +if __name__ == '__main__': + if(not sys.argv[1]): + print('Expecting requirements.txt Path An Argument') + sys.exit(create_dependencies_tree_by_req_file_path(sys.argv[1])) diff --git a/plug/reqPackage.py b/plug/reqPackage.py new file mode 100644 index 00000000..c07b5d74 --- /dev/null +++ b/plug/reqPackage.py @@ -0,0 +1,61 @@ +import pkg_resources +import utils +from package import Package + + +class ReqPackage(Package): + """Wrapper class for Requirements instance + :param obj: The `Requirements` instance to wrap over + :param dist: optional `pkg_resources.Distribution` instance for + this requirement + """ + + UNKNOWN_VERSION = '?' + + def __init__(self, obj, dist=None): + super(ReqPackage, self).__init__(obj) + self.dist = dist + + @property + def version_spec(self): + specs = self._obj.specs + return ','.join([''.join(sp) for sp in specs]) if specs else None + + @property + def installed_version(self): + if not self.dist: + return utils.guess_version(self.key, self.UNKNOWN_VERSION) + return self.dist.version + + def is_conflicting(self): + """If installed version conflicts with required version""" + # unknown installed version is also considered conflicting + if self.installed_version == self.UNKNOWN_VERSION: + return True + ver_spec = (self.version_spec if self.version_spec else '') + req_version_str = '{0}{1}'.format(self.project_name, ver_spec) + req_obj = pkg_resources.Requirement.parse(req_version_str) + return self.installed_version not in req_obj + + def render_as_root(self, frozen): + if not frozen: + return '{0}=={1}'.format(self.project_name, self.installed_version) + elif self.dist: + return self.__class__.frozen_repr(self.dist._obj) + else: + return self.project_name + + def render_as_branch(self, frozen): + if not frozen: + req_ver = self.version_spec if self.version_spec else 'Any' + return ( + '{0} [required: {1}, installed: {2}]' + ).format(self.project_name, req_ver, self.installed_version) + else: + return self.render_as_root(frozen) + + def as_dict(self): + return {'key': self.key, + 'package_name': self.project_name, + 'installed_version': self.installed_version, + 'required_version': self.version_spec} diff --git a/plug/requirements/__init__.py b/plug/requirements/__init__.py new file mode 100644 index 00000000..f1025724 --- /dev/null +++ b/plug/requirements/__init__.py @@ -0,0 +1,22 @@ +from .parser import parse # noqa + +_MAJOR = 0 +_MINOR = 1 +_PATCH = 0 + + +def version_tuple(): + ''' + Returns a 3-tuple of ints that represent the version + ''' + return (_MAJOR, _MINOR, _PATCH) + + +def version(): + ''' + Returns a string representation of the version + ''' + return '%d.%d.%d' % (version_tuple()) + + +__version__ = version() diff --git a/plug/requirements/parser.py b/plug/requirements/parser.py new file mode 100644 index 00000000..024c905f --- /dev/null +++ b/plug/requirements/parser.py @@ -0,0 +1,50 @@ +import os +import warnings + +from .requirement import Requirement + + +def parse(reqstr): + """ + Parse a requirements file into a list of Requirements + + See: pip/req.py:parse_requirements() + + :param reqstr: a string or file like object containing requirements + :returns: a *generator* of Requirement objects + """ + filename = getattr(reqstr, 'name', None) + try: + # Python 2.x compatibility + if not isinstance(reqstr, basestring): + reqstr = reqstr.read() + except NameError: + # Python 3.x only + if not isinstance(reqstr, str): + reqstr = reqstr.read() + + for line in reqstr.splitlines(): + line = line.strip() + if line == '': + continue + elif not line or line.startswith('#'): + # comments are lines that start with # only + continue + elif line.startswith('-r') or line.startswith('--requirement'): + _, new_filename = line.split() + new_file_path = os.path.join(os.path.dirname(filename or '.'), + new_filename) + with open(new_file_path) as f: + for requirement in parse(f): + yield requirement + elif line.startswith('-f') or line.startswith('--find-links') or \ + line.startswith('-i') or line.startswith('--index-url') or \ + line.startswith('--extra-index-url') or \ + line.startswith('--no-index'): + warnings.warn('Private repos not supported. Skipping.') + continue + elif line.startswith('-Z') or line.startswith('--always-unzip'): + warnings.warn('Unused option --always-unzip. Skipping.') + continue + else: + yield Requirement.parse(line) diff --git a/plug/requirements/requirement.py b/plug/requirements/requirement.py new file mode 100644 index 00000000..2fc2bb1d --- /dev/null +++ b/plug/requirements/requirement.py @@ -0,0 +1,181 @@ +from __future__ import unicode_literals +import re +from pkg_resources import Requirement as Req + +from .vcs import VCS, VCS_SCHEMES + + +URI_REGEX = re.compile( + r'^(?Phttps?|file|ftps?)://(?P[^#]+)' + r'(#egg=(?P[^&]+))?$' +) + +VCS_REGEX = re.compile( + r'^(?P{0})://'.format(r'|'.join( + [scheme.replace('+', r'\+') for scheme in VCS_SCHEMES])) + + r'((?P[^/@]+)@)?' + r'(?P[^#@]+)' + r'(@(?P[^#]+))?' + r'(#egg=(?P[^&]+))?$' +) + +# This matches just about everyting +LOCAL_REGEX = re.compile( + r'^((?Pfile)://)?' + r'(?P[^#]+)' + r'(#egg=(?P[^&]+))?$' +) + + +class Requirement(object): + """ + Represents a single requirement + + Typically instances of this class are created with ``Requirement.parse``. + For local file requirements, there's no verification that the file + exists. This class attempts to be *dict-like*. + + See: http://www.pip-installer.org/en/latest/logic.html + + **Members**: + + * ``line`` - the actual requirement line being parsed + * ``editable`` - a boolean whether this requirement is "editable" + * ``local_file`` - a boolean whether this requirement is a local file/path + * ``specifier`` - a boolean whether this requirement used a requirement + specifier (eg. "django>=1.5" or "requirements") + * ``vcs`` - a string specifying the version control system + * ``revision`` - a version control system specifier + * ``name`` - the name of the requirement + * ``uri`` - the URI if this requirement was specified by URI + * ``path`` - the local path to the requirement + * ``extras`` - a list of extras for this requirement + (eg. "mymodule[extra1, extra2]") + * ``specs`` - a list of specs for this requirement + (eg. "mymodule>1.5,<1.6" => [('>', '1.5'), ('<', '1.6')]) + """ + + def __init__(self, line): + # Do not call this private method + self.line = line + self.editable = False + self.local_file = False + self.specifier = False + self.vcs = None + self.name = None + self.uri = None + self.path = None + self.revision = None + self.extras = [] + self.specs = [] + + def __repr__(self): + return ''.format(self.line) + + def __getitem__(self, key): + return getattr(self, key) + + def keys(self): + return self.__dict__.keys() + + @classmethod + def parse_editable(cls, line): + """ + Parses a Requirement from an "editable" requirement which is either + a local project path or a VCS project URI. + + See: pip/req.py:from_editable() + + :param line: an "editable" requirement + :returns: a Requirement instance for the given line + :raises: ValueError on an invalid requirement + """ + + req = cls('-e {0}'.format(line)) + req.editable = True + vcs_match = VCS_REGEX.match(line) + local_match = LOCAL_REGEX.match(line) + + if vcs_match is not None: + groups = vcs_match.groupdict() + req.uri = '{scheme}://{path}'.format(**groups) + req.revision = groups['revision'] + req.name = groups['name'] + for vcs in VCS: + if req.uri.startswith(vcs): + req.vcs = vcs + else: + assert local_match is not None, 'This should match everything' + groups = local_match.groupdict() + req.local_file = True + req.name = groups['name'] + req.path = groups['path'] + + return req + + @classmethod + def parse_line(cls, line): + """ + Parses a Requirement from a non-editable requirement. + + See: pip/req.py:from_line() + + :param line: a "non-editable" requirement + :returns: a Requirement instance for the given line + :raises: ValueError on an invalid requirement + """ + + req = cls(line) + + vcs_match = VCS_REGEX.match(line) + uri_match = URI_REGEX.match(line) + local_match = LOCAL_REGEX.match(line) + + if vcs_match is not None: + groups = vcs_match.groupdict() + req.uri = '{scheme}://{path}'.format(**groups) + req.revision = groups['revision'] + req.name = groups['name'] + for vcs in VCS: + if req.uri.startswith(vcs): + req.vcs = vcs + elif uri_match is not None: + groups = uri_match.groupdict() + req.uri = '{scheme}://{path}'.format(**groups) + req.name = groups['name'] + if groups['scheme'] == 'file': + req.local_file = True + elif '#egg=' in line: + # Assume a local file match + assert local_match is not None, 'This should match everything' + groups = local_match.groupdict() + req.local_file = True + req.name = groups['name'] + req.path = groups['path'] + else: + # This is a requirement specifier. + # Delegate to pkg_resources and hope for the best + req.specifier = True + pkg_req = Req.parse(line) + req.name = pkg_req.unsafe_name + req.extras = list(pkg_req.extras) + req.specs = pkg_req.specs + return req + + @classmethod + def parse(cls, line): + """ + Parses a Requirement from a line of a requirement file. + + :param line: a line of a requirement file + :returns: a Requirement instance for the given line + :raises: ValueError on an invalid requirement + """ + + if line.startswith('-e') or line.startswith('--editable'): + # Editable installs are either a local project path + # or a VCS project URI + return cls.parse_editable( + re.sub(r'^(-e|--editable=?)\s*', '', line)) + + return cls.parse_line(line) diff --git a/plug/requirements/vcs.py b/plug/requirements/vcs.py new file mode 100644 index 00000000..f5317b23 --- /dev/null +++ b/plug/requirements/vcs.py @@ -0,0 +1,30 @@ +from __future__ import unicode_literals + +VCS = [ + 'git', + 'hg', + 'svn', + 'bzr', +] + +VCS_SCHEMES = [ + 'git', + 'git+https', + 'git+ssh', + 'git+git', + 'hg+http', + 'hg+https', + 'hg+static-http', + 'hg+ssh', + 'svn', + 'svn+svn', + 'svn+http', + 'svn+https', + 'svn+ssh', + 'bzr+http', + 'bzr+https', + 'bzr+ssh', + 'bzr+sftp', + 'bzr+ftp', + 'bzr+lp', +] diff --git a/plug/utils.py b/plug/utils.py new file mode 100644 index 00000000..45addd1e --- /dev/null +++ b/plug/utils.py @@ -0,0 +1,60 @@ +from importlib import import_module +from operator import attrgetter +try: + from collections import OrderedDict +except ImportError: + from ordereddict import OrderedDict +from reqPackage import ReqPackage +from distPackage import DistPackage +__version__ = '0.10.1' + + +def build_dist_index(pkgs): + """Build an index pkgs by their key as a dict. + :param list pkgs: list of pkg_resources.Distribution instances + :returns: index of the pkgs by the pkg key + :rtype: dict + """ + return dict((p.key, DistPackage(p)) for p in pkgs) + + +def construct_tree(index): + """Construct tree representation of the pkgs from the index. + The keys of the dict representing the tree will be objects of type + DistPackage and the values will be list of ReqPackage objects. + :param dict index: dist index ie. index of pkgs by their keys + :returns: tree of pkgs and their dependencies + :rtype: dict + """ + return dict((p, [ReqPackage(r, index.get(r.key)) + for r in p.requires()]) + for p in index.values()) + + +def sorted_tree(tree): + """Sorts the dict representation of the tree + The root packages as well as the intermediate packages are sorted + in the alphabetical order of the package names. + :param dict tree: the pkg dependency tree obtained by calling + `construct_tree` function + :returns: sorted tree + :rtype: collections.OrderedDict + """ + return OrderedDict(sorted([(k, sorted(v, key=attrgetter('key'))) + for k, v in tree.items()], + key=lambda kv: kv[0].key)) + + +def guess_version(pkg_key, default='?'): + """Guess the version of a pkg when pip doesn't provide it + :param str pkg_key: key of the package + :param str default: default version to return if unable to find + :returns: version + :rtype: string + """ + try: + m = import_module(pkg_key) + except ImportError: + return default + else: + return getattr(m, '__version__', default) diff --git a/test/inspect.test.js b/test/inspect.test.js new file mode 100644 index 00000000..8af5c6c5 --- /dev/null +++ b/test/inspect.test.js @@ -0,0 +1,81 @@ +var test = require('tap').test; +var path = require('path'); +var plugin = require('../lib'); +var subProcess = require('../lib/sub-process'); + +test('install requirements (may take a while)', function (t) { + chdirWorkspaces('pip-app'); + return subProcess.execute('pip', ['install', '-r', 'requirements.txt']) + .then(function () { + t.pass('installed pip packages'); + }) + .catch(function (error) { + t.bailout(error); + }); +}); + +test('inspect', function (t) { + chdirWorkspaces('pip-app'); + + return plugin.inspect('.', 'requirements.txt') + .then(function (result) { + var pkg = result.package; + + t.test('package', function (t) { + t.ok(pkg, 'package'); + t.equal(pkg.name, 'pip-app', 'name'); + t.equal(pkg.version, '0.0.0', 'version'); + t.equal(pkg.full, 'pip-app@0.0.0', 'version'); + t.equal(pkg.from, ['pip-app@0.0.0'], 'from self'); + t.end(); + }); + + t.test('package dependencies', function (t) { + t.same(pkg.dependencies.django, { + name: 'django', + version: '1.6.1', + from: [ + 'pip-app@0.0.0', + 'django@1.6.1', + ], + }, 'django looks ok'); + t.same(pkg.dependencies.jinja2, { + name: 'jinja2', + version: '2.7.2', + from: [ + 'pip-app@0.0.0', + 'jinja2@2.7.2', + ], + dependencies: { + markupsafe: { + from: [ + 'pip-app@0.0.0', + 'jinja2@2.7.2', + 'markupsafe@0.18', + ], + name: 'markupsafe', + version: '0.18', + }, + }, + }, 'jinja2 looks ok'); + t.end(); + }); + + t.end(); + }); +}); + +test('deps not installed', function (t) { + chdirWorkspaces('pip-app-deps-not-installed'); + return plugin.inspect('.', 'requirements.txt') + .then(function () { + t.fail('should have failed'); + }) + .catch(function (error) { + t.equal(error.message, 'Please run `pip install -r requirements.txt`'); + }); +}); + +function chdirWorkspaces(subdir) { + process.chdir(path.resolve(__dirname, 'workspaces', subdir)); +} diff --git a/test/workspaces/pip-app-deps-not-installed/requirements.txt b/test/workspaces/pip-app-deps-not-installed/requirements.txt new file mode 100644 index 00000000..4c7a38a9 --- /dev/null +++ b/test/workspaces/pip-app-deps-not-installed/requirements.txt @@ -0,0 +1 @@ +awss==0.9.13 diff --git a/test/workspaces/pip-app/requirements.txt b/test/workspaces/pip-app/requirements.txt new file mode 100644 index 00000000..3ed25ef5 --- /dev/null +++ b/test/workspaces/pip-app/requirements.txt @@ -0,0 +1,2 @@ +Jinja2==2.7.2 +Django==1.6.1