diff --git a/bears/general/OutdatedDependencyBear.py b/bears/general/OutdatedDependencyBear.py new file mode 100644 index 0000000000..bf13dc53f5 --- /dev/null +++ b/bears/general/OutdatedDependencyBear.py @@ -0,0 +1,68 @@ +from distutils.version import LooseVersion +from coalib.bears.LocalBear import LocalBear +from coalib.results.Result import Result +from dependency_management.requirements.PipRequirement import ( + parse_requirements_file, get_latest_version +) +from dependency_management.requirements.NpmRequirement import ( + npm_outdated_command +) + + +class OutdatedDependencyBear(LocalBear): + LANGUAGES = {'All'} + AUTHORS = {'The coala developers'} + AUTHORS_EMAILS = {'coala-devel@googlegroups.com'} + LICENSE = 'AGPL-3.0' + + def run(self, filename, file, requirement_type: str,): + """ + Checks for the outdated dependencies in a project. + + :param requirement_type: + One of the requirement types supported by coala's package manager. + :param requirements_file: + Requirements file can be specified to look for the requirements. + """ + requirement_types = ['pip', 'npm'] + + if requirement_type not in requirement_types: + raise ValueError('Currently the bear only supports {} as ' + 'requirement_type.' + .format(', '.join( + type for type in requirement_types))) + + message = ('The requirement {} with version {} is not ' + 'pinned to its latest version {}.') + + if requirement_type == 'pip': + data = parse_requirements_file(file) + + for req in data: + latest_ver = get_latest_version(req['package_name']) + print(latest_ver) + if LooseVersion(req['version']) < LooseVersion(latest_ver): + yield Result.from_values(origin=self, + message=message.format( + req['package_name'], + req['version'], + latest_ver), + file=filename, + line=req['line_number']+1, + end_line=req['line_number']+1, + ) + + elif requirement_type == 'npm': + data = npm_outdated_command() + + for req in data: + latest_ver = data[req]['latest'] + + if LooseVersion(data[req]['wanted']) < LooseVersion(latest_ver): + yield Result.from_values(origin=self, + message=message.format( + req, + data[req]['wanted'], + latest_ver), + file=filename, + ) diff --git a/tests/general/OutdatedDependencyBearTest.py b/tests/general/OutdatedDependencyBearTest.py new file mode 100644 index 0000000000..97375f36eb --- /dev/null +++ b/tests/general/OutdatedDependencyBearTest.py @@ -0,0 +1,75 @@ +import unittest.mock +from queue import Queue + +from bears.general.OutdatedDependencyBear import OutdatedDependencyBear +from coalib.testing.LocalBearTestHelper import LocalBearTestHelper +from coalib.results.Result import Result +from coalib.settings.Section import Section +from coalib.settings.Setting import Setting + + +test_file = """ +foo==1.0 +bar~=2.0 +""" + + +class OutdatedDependencyBearTest(LocalBearTestHelper): + + def setUp(self): + self.section = Section('') + self.uut = OutdatedDependencyBear(self.section, Queue()) + + @unittest.mock.patch('bears.general.OutdatedDependencyBear.' + 'get_latest_version') + def test_pip_requirement_type(self, _mock): + self.section.append(Setting('requirement_type', 'pip')) + _mock.return_value = '3.0' + message = ('The requirement {} with version {} is not ' + 'pinned to its latest version 3.0.') + self.check_results(self.uut, + test_file.splitlines(True), + [Result.from_values( + origin='OutdatedDependencyBear', + message=message.format('foo', '1.0'), + file='default', + line=2, end_line=2, + ), + Result.from_values( + origin='OutdatedDependencyBear', + message=message.format('bar', '2.0'), + file='default', + line=3, end_line=3, + )], + filename='default', + ) + + @unittest.mock.patch('bears.general.OutdatedDependencyBear.' + 'npm_outdated_command') + def test_npm_requirement_type(self, _mock): + self.section.append(Setting('requirement_type', 'npm')) + _mock.return_value = {'foo': {'wanted': '1.0', 'latest': '3.0'}, + 'bar': {'wanted': '2.0', 'latest': '3.0'}} + message = ('The requirement {} with version {} is not ' + 'pinned to its latest version 3.0.') + self.check_results(self.uut, + test_file.splitlines(True), + [Result.from_values( + origin='OutdatedDependencyBear', + message=message.format('foo', '1.0'), + file='default', + ), + Result.from_values( + origin='OutdatedDependencyBear', + message=message.format('bar', '2.0'), + file='default', + )], + filename='default', + ) + + def test_requirement_type_value_error(self): + self.section.append(Setting('requirement_type', 'blabla')) + error = ('ValueError: Currently the bear only supports pip, npm as ' + 'requirement_type.') + with self.assertRaisesRegex(AssertionError, error): + self.check_validity(self.uut, [], filename='default')