diff --git a/calculate/utils/package.py b/calculate/utils/package.py index e442e70..c65bbb2 100644 --- a/calculate/utils/package.py +++ b/calculate/utils/package.py @@ -10,7 +10,6 @@ from pyparsing import Literal, Regex, Word, nums, alphanums,\ from jinja2 import PackageLoader, Environment from calculate.utils.tools import Singleton import hashlib -import operator class PackageError(Exception): @@ -42,169 +41,233 @@ class PackageNotFound(Exception): class Version: + _suffix_order = {'alpha': 0, 'beta': 1, 'pre': 2, + 'rc': 3, 'no': 4, 'p': 5} + '''Класс для работы со значениями версий.''' def __init__(self, version_value=None): if version_value is None: - self._version_string = '-1' - self._version_value = [-1] + self._string = '-1' + self._value = [-1] + self._literal = None + self._suffix = [(4, 0)] + self._revision = 0 elif isinstance(version_value, Version): - self._version_string = version_value._version_string - self._version_value = version_value._version_value + self._string = version_value._string + self._value = version_value._value + self._literal = version_value._literal + self._suffix = version_value._suffix + self._revision = version_value._revision else: value = self._get_version_value(version_value) - if not value: + if value is None: raise VersionError( "Can't initialize Version object using '{0}'" " value with type {1}".format(version_value, type(version_value))) - if isinstance(version_value, str): - self._version_string = version_value.strip('-') - else: - self._version_string = str(version_value) - - self._version_value = value + self._string = value['string'] + self._value = value['value'] + self._literal = value['literal'] + self._suffix = value['suffix'] + self._revision = value['revision'] def _get_version_value(self, version): '''Вспомогательный метод для получения значения версии, представленного в виде списка.''' if isinstance(version, Version): - return version._version_value + version_value = {'string': version._string, + 'value': version._value, + 'literal': version._literal, + 'suffix': version._suffix, + 'revision': version._revision} elif isinstance(version, int): - version_value = [str(version)] + version_value = {'string': str(int), + 'value': [version], + 'literal': '', + 'suffix': [(4, 0)], + 'revision': 0} elif isinstance(version, float): - version_value = [] + version_list = [] version = str(version).split('.') for version_part in version: - version_value.append(int(version_part.strip())) + version_list.append(int(version_part.strip())) + version_value = {'string': str(version), + 'value': version_list, + 'literal': '', + 'suffix': (4, 0), + 'revision': 0} elif isinstance(version, str): - version = version.strip('-') - version_value = [] - - if '-' in version: - version = version.split('-')[0] - if '_' in version: - version = version.split('_')[0] - - for version_part in version.split('.'): - if version_part.isdigit(): - version_part = int(version_part) - version_value.append(version_part) - else: - return False - else: - return False - return version_value + parse_result = PackageAtomParser.version_regex.search( + version.strip('-')) + if not parse_result: + return + result_dict = parse_result.groupdict() + version_value = {'string': version} + + version_list = [] + for version_part in result_dict['value'].split('.'): + version_list.append(int(version_part.strip('-'))) + version_value['value'] = version_list + + # Парсим литерал, если он есть. + version_value['literal'] = result_dict['literal'] or '' + + # Парсим всю совокупность имеющихся суффиксов. + print('result dict:', result_dict) + suffixes = result_dict['suffix'] + print('suffixes:', suffixes) + suffix_list = [] + if suffixes is not None: + suffixes = suffixes.strip('_') + suffixes = suffixes.split('_') + for suffix in suffixes: + result = re.search(r'([^\d]+)(\d+)?', suffix) + suffix_list.append((self._suffix_order[result.group(1)], + int(result.group(2) or 0))) + else: + suffix_list = [(self._suffix_order['no'], 0)] + version_value['suffix'] = suffix_list - def _use_compare_operation(self, compare_operator, other_value): - '''Вспомогательный метод для реализации различных операций сравнения. - ''' - version_value = self._version_value[:] + # Парсим ревизию. + if parse_result['revision'] is not None: + version_value['revision'] = int( + parse_result['revision'].strip('-r')) + else: + version_value['revision'] = 0 + else: + return - other_value_length = len(other_value) - version_value_length = len(version_value) + return version_value - if other_value_length < version_value_length: - for counter in range(version_value_length - other_value_length): - other_value.append(0) - elif version_value_length < other_value_length: - for counter in range(other_value_length - version_value_length): - version_value.append(0) + def _compare_lists(self, lversion, rversion, filler=0): + '''Метод для сравнения двух списков, даже если если они не одинаковы. + Возвращает 0, если списки равны, 1 если lversion больше, -1 если + lversion меньше.''' + if lversion == rversion: + return 0 - for lvalue, rvalue in zip(version_value, other_value): + for index in range(0, max(len(lversion), len(rversion))): + lvalue = lversion[index] if len(lversion) > index else filler + rvalue = rversion[index] if len(rversion) > index else filler if lvalue == rvalue: continue - if compare_operator(lvalue, rvalue): - return True - else: - return False - else: - if (compare_operator != operator.lt and - compare_operator != operator.gt and - compare_operator != operator.ne): - return True + if lvalue > rvalue: + return 1 else: - return False + return -1 + return 0 def __lt__(self, other): '''Перегрузка x < y.''' - other_value = self._get_version_value(other) - if not other_value: + other_version = self._get_version_value(other) + if other_version is None: raise VersionError( "Unable to compare Version object with the '{0}'" " value of '{1}' type".format(other, type(other))) + cmp_res = self._compare_lists(self._value, other_version['value']) + if cmp_res != 0: + return cmp_res < 0 + + if self._literal != other_version['literal']: + return self._literal < other_version['literal'] + + cmp_res = self._compare_lists(self._suffix, + other_version['suffix'], + filler=(4, 0)) + if cmp_res != 0: + return cmp_res < 0 - return self._use_compare_operation(operator.lt, other_value) + return self._revision < other_version['revision'] def __le__(self, other): '''Перегрузка x <= y.''' - other_value = self._get_version_value(other) - if not other_value: + other_version = self._get_version_value(other) + if other_version is None: raise VersionError( "Unable to compare Version object with the '{0}'" " value of '{1}' type".format(other, type(other))) - - return self._use_compare_operation(operator.le, other_value) + cmp_res = self._compare_lists(self._value, other_version['value']) + if cmp_res != 0: + return cmp_res < 0 + if self._literal != other_version['literal']: + return self._literal < other_version['literal'] + cmp_res = self._compare_lists(self._suffix, + other_version['suffix'], + filler=(4, 0)) + if cmp_res != 0: + return cmp_res < 0 + return self._revision <= other_version['revision'] def __eq__(self, other): '''Перегрузка x == y.''' - other_value = self._get_version_value(other) - if not other_value: + other_version = self._get_version_value(other) + if other_version is None: raise VersionError( "Unable to compare Version object with the '{0}'" " value of '{1}' type".format(other, type(other))) - - return self._use_compare_operation(operator.eq, other_value) + cmp_res = self._compare_lists(self._value, + other_version['value']) + if cmp_res != 0: + return False + if self._literal != other_version['literal']: + return False + cmp_res = self._compare_lists(self._suffix, + other_version['suffix'], + filler=(4, 0)) + if cmp_res != 0: + return False + return self._revision == other_version['revision'] def __ne__(self, other): '''Перегрузка x != y.''' - other_value = self._get_version_value(other) - if not other_value: - raise VersionError( - "Unable to compare Version object with the '{0}'" - " value of '{1}' type".format(other, type(other))) - - return self._use_compare_operation(operator.ne, other_value) + return not self.__eq__(other) def __gt__(self, other): '''Перегрузка x > y.''' - other_value = self._get_version_value(other) - if not other_value: - raise VersionError( - "Unable to compare Version object with the '{0}'" - " value of '{1}' type".format(other, type(other))) - - return self._use_compare_operation(operator.gt, other_value) + return not self.__le__(other) def __ge__(self, other): '''Перегрузка x >= y.''' - other_value = self._get_version_value(other) - if not other_value: - raise VersionError( - "Unable to compare Version object with the '{0}'" - " value of '{1}' type".format(other, type(other))) - - return self._use_compare_operation(operator.ge, other_value) + return not self.__lt__(other) def __hash__(self): - return hash(self._version_string) + return hash(self._string) def __repr__(self): - return ''.format(self._version_string) + return ''.format(self._string) def __str__(self): - return self._version_string + return self._string def __bool__(self): - if self._version_value == [-1]: + if self._value == [-1]: return False else: return True + def __rshift__(self, other: tuple) -> bool: + "Проверка нахождения значения переменной в указанном диапазоне." + if (not isinstance(other, tuple) or len(other) != 2 + or not isinstance(other[0], str) or not isinstance(other[1], str)): + raise VersionError("Versions range must be tuple of two strings," + f" not '{type(other)}'") + + lequal = other[0].startswith('=') + lversion = Version(other[0].strip('=')) + + requal = other[0].startswith('=') + rversion = Version(other[1].strip('=')) + + return (((lequal and self >= lversion) + or (not lequal and self > lversion)) + and ((requal and self <= rversion) + or (not requal and self < rversion))) + class ContentsParser(metaclass=Singleton): def __init__(self): @@ -328,15 +391,13 @@ class PackageAtomName: def __eq__(self, other): if isinstance(other, PackageAtomName): - return (self._package_directory == - other._package_directory) + return self._package_directory == other._package_directory else: return False def __ne__(self, other): if isinstance(other, PackageAtomName): - return (self._package_directory != - other._package_directory) + return self._package_directory != other._package_directory else: return False @@ -361,8 +422,14 @@ NonePackage = PackageAtomName({'pkg_path': None, 'version': None}) class PackageAtomParser: '''Класс для парсинга параметра package, его проверки, а также определения принадлежности файла пакету.''' + _value = r'(?P\d+(\.\d+)*)' + _literal = r'(?P[a-z])?' + _suffix = r'(?P(_(pre|p|beta|alpha|rc)(\d+)?)+)?' + _revision = r'(?P-r\d+)?' + _version_pattern = _value + _literal + _suffix + _revision + package_name_pattern =\ - r'(?P\D[\w\d]*(\-\D[\w\d]*)*)(?P-\d[^\s,\[:]*)?' + fr'(?P\D[\w\d]*(\-\D[\w\d]*)*)(?P-{_version_pattern})?' atom_name_pattern = r'''(?P[^\s/]*)/ {0} @@ -372,6 +439,7 @@ class PackageAtomParser: atom_regex = re.compile(atom_name_pattern, re.VERBOSE) package_name_regex = re.compile(package_name_pattern) + version_regex = re.compile(_version_pattern) def __init__(self, pkg_path='/var/db/pkg', chroot_path='/'): @@ -443,7 +511,7 @@ class PackageAtomParser: # Используем glob-паттерн для поиска. if self._atom_dictionary['version'] is not None: full_name = self._atom_dictionary['name'] + '-' +\ - self._atom_dictionary['version']._version_string + self._atom_dictionary['version']._string else: full_name = self._atom_dictionary['name'] diff --git a/tests/utils/test_package.py b/tests/utils/test_package.py index 27d16ac..ad28a79 100644 --- a/tests/utils/test_package.py +++ b/tests/utils/test_package.py @@ -22,20 +22,35 @@ class TestContents: assert version_1 < version_2 def test_if_two_Version_objects_compared_using_le_operation_and_the_left_version_value_is_less_than_or_equal_to_the_right_version__the_result_of_the_comparing_would_be_True(self): - version_1 = Version('3.7.2-r1') + version_1 = Version('3.9.0_beta') version_2 = Version('3.9') version_3 = Version('3.9.0') assert (version_1 <= version_2 and version_2 <= version_3) + def test_if_two_Version_objects_compared_using_lt_operation_both_versions_has_suffixes_and_the_left_version_value_is_less_than_or_equal_to_the_right_version__the_result_of_the_comparing_would_be_True(self): + version_1 = Version('13.9_alpha') + version_2 = Version('13.9.0_beta') + assert version_1 < version_2 + def test_if_two_Version_objects_compared_using_gt_operation_and_the_left_version_value_is_less_than_the_right_version__the_result_of_the_comparing_would_be_True(self): version_1 = Version('3.5.6-r1') - version_2 = Version('2.5.6') + version_2 = Version('2.5.6-r2') + assert version_1 > version_2 + + def test_if_two_Version_objects_compared_using_gt_operation_both_of_the_versions_have_revision_values_and_the_same_version_number_and_the_right_version_value_is_less_than_the_left_version__the_result_of_the_comparing_would_be_True(self): + version_1 = Version('3.5.6-r2') + version_2 = Version('3.5.6-r1') + assert version_1 > version_2 + + def test_if_two_Version_objects_compared_using_gt_operation_both_of_them_have_p_suffix_and_the_right_version_value_is_less_than_left_version__the_result_of_the_comparing_would_be_True(self): + version_1 = Version('2.5.6_p2-r1') + version_2 = Version('2.5.6_p1-r1') assert version_1 > version_2 def test_if_two_Version_objects_compared_using_ge_operation_and_the_left_version_value_is_less_than_or_equal_to_the_right_version__the_result_of_the_comparing_would_be_True(self): version_1 = Version('13.0') version_2 = Version('12.7.6-r1') - assert (version_1 >= version_2 and version_2 >= '12.7.6') + assert version_1 >= version_2 and version_2 >= '12.7.6' def test_if_two_Version_objects_compared_using_ne_operation_and_the_left_version_value_is_less_than_or_equal_to_the_right_version__the_result_of_the_comparing_would_be_True(self): version_1 = Version('13.0') @@ -45,6 +60,43 @@ class TestContents: version_1 = Version('13.1.0') assert (version_1 == '13.1') + def test_if_version_object_is_checked_to_be_in_the_range_and_the_version_is_in_the_range_indeed__the_operation_returns_True(self): + version = Version('5.1.2') + assert version >> ('5.1.0', '6.1.12') + + def test_if_version_object_is_checked_to_be_in_the_range_between_two_versions_values_the_first_one_is_included_and_the_version_is_in_this_range_indeed__the_operation_returns_True(self): + version = Version('5.1.2') + assert version >> ('=5.1.2', '6.1.12') + + def test_i_am_too_lazy_to_come_up_with_the_name_for_this_test(self): + version = Version('5.1.2') + assert not version >> ('5.1.2', '6.1.12') + + def test_the_same_situation(self): + version = Version('4.1') + assert not version >> ('5.1.2', '6.1.12') + + def test_if_ContentsParser_is_used_for_parsing_of_a_correct_contents_file_text_and_then_is_used_to_get_text_of_the_source_text__the_source_text_and_the_parser_s_output_will_be_the_same(self): + parser = ContentsParser() + contents_text = '''dir /usr +dir /usr/bin +sym /usr/bin/rview -> vim 1573538053 +sym /usr/bin/rvim -> vim 1573538053 +obj /usr/bin/vim 30acc0f256e11c1ecdb1bd80b688d238 1573538056 +sym /usr/bin/vimdiff -> vim 1573538053 +dir /etc +dir /etc/test_dir_1 +''' + output_dict = parser.parse(contents_text) + print('DICT:') + pprint(output_dict) + + output_text = parser.render(output_dict) + print('TEXT:') + print(output_text) + + assert output_text == contents_text + def test_if_PackageContents_object_initialized_by_existing_package__it_contains_dictionary_of_items_from_contents_file(self): result = OrderedDict({ '/usr': @@ -240,24 +292,3 @@ sym /etc/test_dir_2/symlink -> file_2.cfg {} with pytest.raises(PackageNotFound): file_package = package_atom.get_file_package('/etc/shadow') print('package = {}'.format(file_package)) - - def test_parser(self): - parser = ContentsParser() - contents_text = '''dir /usr -dir /usr/bin -sym /usr/bin/rview -> vim 1573538053 -sym /usr/bin/rvim -> vim 1573538053 -obj /usr/bin/vim 30acc0f256e11c1ecdb1bd80b688d238 1573538056 -sym /usr/bin/vimdiff -> vim 1573538053 -dir /etc -dir /etc/test_dir_1 -''' - output_dict = parser.parse(contents_text) - print('DICT:') - pprint(output_dict) - - output_text = parser.render(output_dict) - print('TEXT:') - print(output_text) - - assert output_text == contents_text