diff --git a/bear-languages.yaml b/bear-languages.yaml index b8095b55a9..762d96883e 100644 --- a/bear-languages.yaml +++ b/bear-languages.yaml @@ -329,6 +329,10 @@ RadonBear: - Python - Python 2 - Python 3 +RequirementsCheckBear: + - Python 2 Requirements + - Python 3 Requirements + - Python Requirements RuboCopBear: - Ruby RubyFastererBear: diff --git a/bear-requirements.txt b/bear-requirements.txt index 6a30c370c8..c1517bfa8a 100644 --- a/bear-requirements.txt +++ b/bear-requirements.txt @@ -23,6 +23,7 @@ munkres3~=1.0 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 9f860344ec..7c7a4197db 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/python/requirements/RequirementsCheckBear.py b/bears/python/requirements/RequirementsCheckBear.py new file mode 100644 index 0000000000..e899f05bf4 --- /dev/null +++ b/bears/python/requirements/RequirementsCheckBear.py @@ -0,0 +1,58 @@ +import os.path + +from coalib.bears.GlobalBear import GlobalBear +from coalib.results.Result import Result, RESULT_SEVERITY +from sarge import capture_both + +from dependency_management.requirements.PipRequirement import PipRequirement + + +class RequirementsCheckBear(GlobalBear): + """ + The bear to check and find any conflicting pip dependencies. + """ + LANGUAGES = { + 'Python Requirements', + 'Python 2 Requirements', + 'Python 3 Requirements', + } + REQUIREMENTS = {PipRequirement('pip-tools', '3.8.0')} + AUTHORS = {'The coala developers'} + AUTHORS_EMAILS = {'coala-devel@googlegroups.com'} + LICENSE = 'AGPL-3.0' + + def run(self, require_files: tuple): + """ + :param require_files: + Tuple of requirements files. + """ + data = '' + orig_file = '' + + for req_file in require_files: + if not os.path.isfile(os.path.abspath(req_file)): + raise ValueError('The file \'{}\' doesn\'t exist.' + .format(req_file)) + + with open(req_file) as req: + content = req.read() + if not orig_file: + orig_file = content + else: + data += content + + with open(require_files[0], 'a+') as temp_file: + temp_file.write(data) + + out = capture_both('pip-compile {} -r -n --no-annotate --no-header ' + '--no-index --allow-unsafe'.format(require_files[0])) + + if out.stderr.text and not out.stdout.text: + lines = out.stderr.text.splitlines() + yield Result(self, + message=[s for s in lines if 'Could not' in s][0], + severity=RESULT_SEVERITY.MAJOR, + ) + + with open(require_files[0], 'w+') as temp_file: + temp_file.write(orig_file) diff --git a/tests/python/requirements/RequirementsCheckBearTest.py b/tests/python/requirements/RequirementsCheckBearTest.py new file mode 100644 index 0000000000..9e63902922 --- /dev/null +++ b/tests/python/requirements/RequirementsCheckBearTest.py @@ -0,0 +1,71 @@ +import os +import unittest + +from queue import Queue + +from bears.python.requirements.RequirementsCheckBear import ( + RequirementsCheckBear) +from coalib.results.Result import Result, RESULT_SEVERITY +from coalib.settings.Section import Section +from coalib.settings.Setting import Setting + + +def get_absolute_test_path(file): + return os.path.join(os.path.dirname(__file__), + 'requirements_test_files', file) + + +def read_file(file): + with open(file) as _file: + return _file.read() + + +class RequirementsCheckBearTest(unittest.TestCase): + + def setUp(self): + self.section = Section('') + self.file_dict = {} + self.queue = Queue() + self.test_files = [get_absolute_test_path('conflict.txt'), + get_absolute_test_path('valid.txt')] + self.files = [read_file(_file) for _file in self.test_files] + + def test_conflicted_file(self): + self.section.append(Setting('require_files', self.test_files[0])) + self.uut = RequirementsCheckBear({}, self.section, self.queue) + result = list(self.uut.run_bear_from_section([], {})) + self.assertEqual(result[0], + Result(origin='RequirementsCheckBear', + message=('Could not find a version that ' + 'matches six<=1.11.0,==1.12.0'), + severity=RESULT_SEVERITY.MAJOR)) + self.assertEqual(read_file(self.test_files[0]), self.files[0]) + + def test_valid_file(self): + self.section.append(Setting('require_files', self.test_files[1])) + self.uut = RequirementsCheckBear({}, self.section, self.queue) + result = list(self.uut.run_bear_from_section([], {})) + self.assertEqual(result, []) + self.assertEqual(read_file(self.test_files[1]), self.files[1]) + + def test_multiple_require_files(self): + self.section.append(Setting('require_files', + ','.join(file for file in self.test_files))) + self.uut = RequirementsCheckBear({}, self.section, self.queue) + result = list(self.uut.run_bear_from_section([], {})) + self.assertEqual(result[0], + Result(origin='RequirementsCheckBear', + message=('Could not find a version that ' + 'matches six<=1.11.0,==1.12.0'), + severity=RESULT_SEVERITY.MAJOR)) + self.assertEqual(read_file(self.test_files[0]), self.files[0]) + self.assertEqual(read_file(self.test_files[1]), self.files[1]) + + def test_no_existing_file(self): + invalid_test_file_path = get_absolute_test_path('invalid.txt') + self.section.append(Setting('require_files', invalid_test_file_path)) + self.uut = RequirementsCheckBear({}, self.section, self.queue) + error = ('The file \'{}\' doesn\'t exist.' + .format(invalid_test_file_path)) + with self.assertRaisesRegex(ValueError, error): + list(self.uut.run_bear_from_section([], {})) diff --git a/tests/python/requirements/requirements_test_files/conflict.txt b/tests/python/requirements/requirements_test_files/conflict.txt new file mode 100644 index 0000000000..e13e467611 --- /dev/null +++ b/tests/python/requirements/requirements_test_files/conflict.txt @@ -0,0 +1,2 @@ +six==1.12.0 +six<=1.11.0 diff --git a/tests/python/requirements/requirements_test_files/valid.txt b/tests/python/requirements/requirements_test_files/valid.txt new file mode 100644 index 0000000000..c9913d3630 --- /dev/null +++ b/tests/python/requirements/requirements_test_files/valid.txt @@ -0,0 +1 @@ +pytest~=3.6.4