diff --git a/bear-requirements.txt b/bear-requirements.txt index 8d6a60a46f..70995f09f0 100644 --- a/bear-requirements.txt +++ b/bear-requirements.txt @@ -23,6 +23,7 @@ munkres~=1.1.2 mypy==0.590 nbformat~=4.1 nltk~=3.2 +pip-tools~=3.8.0 proselint~=0.7.0 pycodestyle~=2.2 pydocstyle~=2.0 diff --git a/bear-requirements.yaml b/bear-requirements.yaml index d168d4535c..e2e1d8dbc2 100644 --- a/bear-requirements.yaml +++ b/bear-requirements.yaml @@ -52,6 +52,8 @@ pip_requirements: version: ~=4.1 nltk: version: ~=3.2 + pip-tools: + version: ~=3.8.0 proselint: version: ~=0.7.0 pycodestyle: diff --git a/bears/general/OutdatedDependencyBear.py b/bears/general/OutdatedDependencyBear.py new file mode 100644 index 0000000000..fe7e33ab5e --- /dev/null +++ b/bears/general/OutdatedDependencyBear.py @@ -0,0 +1,57 @@ +from coalib.bears.LocalBear import LocalBear +from coalib.results.Result import Result +from dependency_management.requirements.PipRequirement import PipRequirement +from distutils.version import LooseVersion +from sarge import run, Capture + + +class OutdatedDependencyBear(LocalBear): + LANGUAGES = {'All'} + REQUIREMENTS = {PipRequirement('pip-tools', '3.8.0')} + 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'] + + 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 {}.') + + out = run('pip-compile -n --allow-unsafe {}'.format(filename), + stdout=Capture()) + + data = [line for line in out.stdout.text.splitlines() + if '#' not in line and line] + + for requiremenent in data: + package, version = requiremenent.split('==') + pip_requirement = PipRequirement(package) + latest_ver = pip_requirement.get_latest_version() + line_number = [num for num, line in enumerate(file, 1) + if package in line.lower()] + + if LooseVersion(version) < LooseVersion(latest_ver): + yield Result.from_values(origin=self, + message=message.format( + package, + version, + latest_ver), + file=filename, + line=line_number[0], + end_line=line_number[0], + ) diff --git a/tests/general/OutdatedDependencyBearTest.py b/tests/general/OutdatedDependencyBearTest.py new file mode 100644 index 0000000000..058c4088b6 --- /dev/null +++ b/tests/general/OutdatedDependencyBearTest.py @@ -0,0 +1,75 @@ +import unittest.mock +import sarge +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.' + 'PipRequirement.get_latest_version') + def test_pip_outdated_requirement(self, _mock): + self.section.append(Setting('requirement_type', 'pip')) + _mock.return_value = '3.0' + with unittest.mock.patch('bears.general.OutdatedDependencyBear.' + 'run') as mock: + patched = unittest.mock.Mock(spec=sarge.Pipeline) + patched.stdout = unittest.mock.Mock(spec=sarge.Capture) + patched.stdout.text = 'foo==1.0\nbar==2.0' + mock.return_value = patched + 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.' + 'PipRequirement.get_latest_version') + def test_pip_latest_requirement(self, _mock): + self.section.append(Setting('requirement_type', 'pip')) + _mock.return_value = '1.0' + with unittest.mock.patch('bears.general.OutdatedDependencyBear.' + 'run') as mock: + patched = unittest.mock.Mock(spec=sarge.Pipeline) + patched.stdout = unittest.mock.Mock(spec=sarge.Capture) + patched.stdout.text = 'foo==1.0' + mock.return_value = patched + self.check_results(self.uut, + [test_file.splitlines()[0]], + [], + filename='default') + + def test_requirement_type_value_error(self): + self.section.append(Setting('requirement_type', 'blabla')) + error = ('ValueError: Currently the bear only supports pip as ' + 'requirement_type.') + with self.assertRaisesRegex(AssertionError, error): + self.check_validity(self.uut, [], filename='default')