Skip to content

Commit

Permalink
feat: init python plugin from michael's POC
Browse files Browse the repository at this point in the history
  • Loading branch information
darscan committed May 28, 2017
0 parents commit a180e50
Show file tree
Hide file tree
Showing 17 changed files with 783 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
/node_modules/
__pycache__/
*.pyc
14 changes: 14 additions & 0 deletions .jscsrc
Original file line number Diff line number Diff line change
@@ -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
}
55 changes: 55 additions & 0 deletions lib/index.js
Original file line number Diff line number Diff line change
@@ -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;
}
24 changes: 24 additions & 0 deletions lib/sub-process.js
Original file line number Diff line number Diff line change
@@ -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 });
});
});
};
18 changes: 18 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
53 changes: 53 additions & 0 deletions plug/distPackage.py
Original file line number Diff line number Diff line change
@@ -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}
36 changes: 36 additions & 0 deletions plug/package.py
Original file line number Diff line number Diff line change
@@ -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)
92 changes: 92 additions & 0 deletions plug/pip_resolve.py
Original file line number Diff line number Diff line change
@@ -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]))
61 changes: 61 additions & 0 deletions plug/reqPackage.py
Original file line number Diff line number Diff line change
@@ -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}
22 changes: 22 additions & 0 deletions plug/requirements/__init__.py
Original file line number Diff line number Diff line change
@@ -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()
Loading

0 comments on commit a180e50

Please sign in to comment.