From bb4b24fa502b6f396eb82faa7d420384bb382b6a Mon Sep 17 00:00:00 2001 From: "Nathan J. Mehl" Date: Fri, 20 Nov 2020 17:44:49 -0500 Subject: [PATCH 1/3] black formatting --- .gitignore | 1 + pydpkg/__init__.py | 250 ++++++++++++++++++++++----------------------- tests/test_dpkg.py | 229 +++++++++++++++++++++-------------------- 3 files changed, 241 insertions(+), 239 deletions(-) diff --git a/.gitignore b/.gitignore index 8e63491..e998998 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ __pycache__ MANIFEST build/ dist/ +.eggs diff --git a/pydpkg/__init__.py b/pydpkg/__init__.py index ea7e37f..015ca3a 100644 --- a/pydpkg/__init__.py +++ b/pydpkg/__init__.py @@ -1,4 +1,3 @@ - """ pydpkg: tools for inspecting dpkg archive files in python without any dependency on libapt """ @@ -23,7 +22,7 @@ import pgpy from arpy import Archive -REQUIRED_HEADERS = ('package', 'version', 'architecture') +REQUIRED_HEADERS = ("package", "version", "architecture") logging.basicConfig() @@ -65,12 +64,12 @@ class DscBadSignatureError(DscError): # pylint: disable=too-many-instance-attributes,too-many-public-methods -class Dpkg(): +class Dpkg: """Class allowing import and manipulation of a debian package file.""" def __init__(self, filename=None, ignore_missing=False, logger=None): - """ Constructor for Dpkg object + """Constructor for Dpkg object :param filename: string :param ignore_missing: bool @@ -79,7 +78,7 @@ def __init__(self, filename=None, ignore_missing=False, logger=None): self.filename = os.path.expanduser(filename) self.ignore_missing = ignore_missing if not isinstance(self.filename, six.string_types): - raise DpkgError('filename argument must be a string') + raise DpkgError("filename argument must be a string") if not os.path.isfile(self.filename): raise DpkgError('filename "%s" does not exist' % filename) self._log = logger or logging.getLogger(__name__) @@ -169,16 +168,16 @@ def fileinfo(self): h_md5 = hashlib.md5() h_sha1 = hashlib.sha1() h_sha256 = hashlib.sha256() - with open(self.filename, 'rb') as dpkg_file: - for chunk in iter(lambda: dpkg_file.read(128), b''): + with open(self.filename, "rb") as dpkg_file: + for chunk in iter(lambda: dpkg_file.read(128), b""): h_md5.update(chunk) h_sha1.update(chunk) h_sha256.update(chunk) self._fileinfo = { - 'md5': h_md5.hexdigest(), - 'sha1': h_sha1.hexdigest(), - 'sha256': h_sha256.hexdigest(), - 'filesize': os.path.getsize(self.filename) + "md5": h_md5.hexdigest(), + "sha1": h_sha1.hexdigest(), + "sha256": h_sha256.hexdigest(), + "filesize": os.path.getsize(self.filename), } return self._fileinfo @@ -188,7 +187,7 @@ def md5(self): :returns: string """ - return self.fileinfo['md5'] + return self.fileinfo["md5"] @property def sha1(self): @@ -196,7 +195,7 @@ def sha1(self): :returns: string """ - return self.fileinfo['sha1'] + return self.fileinfo["sha1"] @property def sha256(self): @@ -204,7 +203,7 @@ def sha256(self): :returns: string """ - return self.fileinfo['sha256'] + return self.fileinfo["sha256"] @property def filesize(self): @@ -212,7 +211,7 @@ def filesize(self): :returns: string """ - return self.fileinfo['filesize'] + return self.fileinfo["filesize"] @property def epoch(self): @@ -266,10 +265,10 @@ def get_header(self, header): def compare_version_with(self, version_str): """Compare my version to an arbitrary version""" - return Dpkg.compare_versions(self.get_header('version'), version_str) + return Dpkg.compare_versions(self.get_header("version"), version_str) @staticmethod - def _force_encoding(obj, encoding='utf-8'): + def _force_encoding(obj, encoding="utf-8"): """Enforce uniform text encoding""" if isinstance(obj, six.string_types): if not isinstance(obj, six.text_type): @@ -279,79 +278,78 @@ def _force_encoding(obj, encoding='utf-8'): def _extract_message(self, ctar): # pathname in the tar could be ./control, or just control # (there would never be two control files...right?) - tar_members = [ - os.path.basename(x.name) for x in ctar.getmembers()] - self._log.debug('got tar members: %s', tar_members) - if 'control' not in tar_members: + tar_members = [os.path.basename(x.name) for x in ctar.getmembers()] + self._log.debug("got tar members: %s", tar_members) + if "control" not in tar_members: raise DpkgMissingControlFile( - 'Corrupt dpkg file: no control file in control.tar.gz') - control_idx = tar_members.index('control') - self._log.debug('got control index: %s', control_idx) + "Corrupt dpkg file: no control file in control.tar.gz" + ) + control_idx = tar_members.index("control") + self._log.debug("got control index: %s", control_idx) # at last! - control_file = ctar.extractfile( - ctar.getmembers()[control_idx]) - self._log.debug('got control file: %s', control_file) + control_file = ctar.extractfile(ctar.getmembers()[control_idx]) + self._log.debug("got control file: %s", control_file) message_body = control_file.read() # py27 lacks email.message_from_bytes, so... if isinstance(message_body, bytes): - message_body = message_body.decode('utf-8') + message_body = message_body.decode("utf-8") message = message_from_string(message_body) - self._log.debug('got control message: %s', message) + self._log.debug("got control message: %s", message) return message def _process_dpkg_file(self, filename): dpkg_archive = Archive(filename) dpkg_archive.read_all_headers() - if b'control.tar.gz' in dpkg_archive.archived_files: - control_archive = dpkg_archive.archived_files[b'control.tar.gz'] + if b"control.tar.gz" in dpkg_archive.archived_files: + control_archive = dpkg_archive.archived_files[b"control.tar.gz"] control_archive_type = "gz" - elif b'control.tar.xz' in dpkg_archive.archived_files: - control_archive = dpkg_archive.archived_files[b'control.tar.xz'] + elif b"control.tar.xz" in dpkg_archive.archived_files: + control_archive = dpkg_archive.archived_files[b"control.tar.xz"] control_archive_type = "xz" else: raise DpkgMissingControlGzipFile( - 'Corrupt dpkg file: no control.tar.gz/xz file in ar archive.') - self._log.debug('found controlgz: %s', control_archive) + "Corrupt dpkg file: no control.tar.gz/xz file in ar archive." + ) + self._log.debug("found controlgz: %s", control_archive) if control_archive_type == "gz": with GzipFile(fileobj=control_archive) as gzf: - self._log.debug('opened gzip control archive: %s', gzf) + self._log.debug("opened gzip control archive: %s", gzf) with tarfile.open(fileobj=io.BytesIO(gzf.read())) as ctar: - self._log.debug('opened tar file: %s', ctar) + self._log.debug("opened tar file: %s", ctar) message = self._extract_message(ctar) else: with lzma.open(control_archive) as xzf: - self._log.debug('opened xz control archive: %s', xzf) + self._log.debug("opened xz control archive: %s", xzf) with tarfile.open(fileobj=io.BytesIO(xzf.read())) as ctar: - self._log.debug('opened tar file: %s', ctar) + self._log.debug("opened tar file: %s", ctar) message = self._extract_message(ctar) for req in REQUIRED_HEADERS: if req not in list(map(str.lower, message.keys())): if self.ignore_missing: - self._log.debug( - 'Header "%s" not found in control message', req) + self._log.debug('Header "%s" not found in control message', req) continue raise DpkgMissingRequiredHeaderError( - 'Corrupt control section; header: "%s" not found' % req) - self._log.debug('all required headers found') + 'Corrupt control section; header: "%s" not found' % req + ) + self._log.debug("all required headers found") for header in message.keys(): - self._log.debug('coercing header to utf8: %s', header) - message.replace_header( - header, self._force_encoding(message[header])) - self._log.debug('all required headers coerced') + self._log.debug("coercing header to utf8: %s", header) + message.replace_header(header, self._force_encoding(message[header])) + self._log.debug("all required headers coerced") return message @staticmethod def get_epoch(version_str): - """ Parse the epoch out of a package version string. + """Parse the epoch out of a package version string. Return (epoch, version); epoch is zero if not found.""" try: # there could be more than one colon, # but we only care about the first - e_index = version_str.index(':') + e_index = version_str.index(":") except ValueError: # no colons means no epoch; that's valid, man return 0, version_str @@ -360,11 +358,11 @@ def get_epoch(version_str): epoch = int(version_str[0:e_index]) except ValueError: raise DpkgVersionError( - 'Corrupt dpkg version %s: epochs can only be ints, and ' - 'epochless versions cannot use the colon character.' % - version_str) + "Corrupt dpkg version %s: epochs can only be ints, and " + "epochless versions cannot use the colon character." % version_str + ) - return epoch, version_str[e_index + 1:] + return epoch, version_str[e_index + 1 :] @staticmethod def get_upstream(version_str): @@ -372,19 +370,19 @@ def get_upstream(version_str): revision and a debian revision, return a tuple of both. If there is no debian revision, return 0 as the second tuple element.""" try: - d_index = version_str.rindex('-') + d_index = version_str.rindex("-") except ValueError: # no hyphens means no debian version, also valid. - return version_str, '0' + return version_str, "0" - return version_str[0:d_index], version_str[d_index+1:] + return version_str[0:d_index], version_str[d_index + 1 :] @staticmethod def split_full_version(version_str): """Split a full version string into epoch, upstream version and debian revision. :param: version_str - :returns: tuple """ + :returns: tuple""" epoch, full_ver = Dpkg.get_epoch(version_str) upstream_rev, debian_rev = Dpkg.get_upstream(full_ver) return epoch, upstream_rev, debian_rev @@ -397,10 +395,10 @@ def get_alphas(revision_str): for i, char in enumerate(revision_str): if char.isdigit(): if i == 0: - return '', revision_str + return "", revision_str return revision_str[0:i], revision_str[i:] # string is entirely alphas - return revision_str, '' + return revision_str, "" @staticmethod def get_digits(revision_str): @@ -408,7 +406,7 @@ def get_digits(revision_str): may be empty) and the remains.""" # If the string is empty, return (0,'') if not revision_str: - return 0, '' + return 0, "" # get the index of the first non-digit for i, char in enumerate(revision_str): if not char.isdigit(): @@ -416,7 +414,7 @@ def get_digits(revision_str): return 0, revision_str return int(revision_str[0:i]), revision_str[i:] # string is entirely digits - return int(revision_str), '' + return int(revision_str), "" @staticmethod def listify(revision_str): @@ -452,9 +450,9 @@ def dstringcmp(a, b): continue # "a tilde sorts before anything, even the end of a part" # (emptyness) - if char == '~': + if char == "~": return -1 - if b[i] == '~': + if b[i] == "~": return 1 # "all the letters sort earlier than all the non-letters" if char.isalpha() and not b[i].isalpha(): @@ -469,12 +467,12 @@ def dstringcmp(a, b): except IndexError: # a is longer than b but otherwise equal, hence greater # ...except for goddamn tildes - if char == '~': + if char == "~": return -1 return 1 # if we get here, a is shorter than b but otherwise equal, hence lesser # ...except for goddamn tildes - if b[len(a)] == '~': + if b[len(a)] == "~": return 1 return -1 @@ -496,8 +494,9 @@ def compare_revision_strings(rev1, rev2): # just in case if not isinstance(item, list2[i].__class__): raise DpkgVersionError( - 'Cannot compare %s to %s, something has gone horribly ' - 'awry.' % (item, list2[i])) + "Cannot compare %s to %s, something has gone horribly " + "awry." % (item, list2[i]) + ) # if the items are equal, next if item == list2[i]: continue @@ -564,9 +563,10 @@ def dstringcmp_key(x): return cmp_to_key(Dpkg.dstringcmp)(x) -class Dsc(): +class Dsc: """Class allowing import and manipulation of a debian source - description (dsc) file.""" + description (dsc) file.""" + def __init__(self, filename=None, logger=None): self.filename = os.path.expanduser(filename) self._dirname = os.path.dirname(self.filename) @@ -594,11 +594,11 @@ def __getattr__(self, attr): :returns: string :raises: AttributeError """ - self._log.debug('grabbing attr: %s', attr) + self._log.debug("grabbing attr: %s", attr) if attr in self.__dict__: return self.__dict__[attr] # handle attributes with dashes :-( - munged = attr.replace('_', '-') + munged = attr.replace("_", "-") # beware: email.Message[nonexistent] returns None not KeyError if munged in self.message: return self.message[munged] @@ -612,7 +612,7 @@ def __getitem__(self, item): :returns: string :raises: KeyError """ - self._log.debug('grabbing item: %s', item) + self._log.debug("grabbing item: %s", item) try: return getattr(self, item) except AttributeError: @@ -631,7 +631,7 @@ def get(self, item, ret=None): @property def message(self): """Return an email.Message object containing the parsed dsc file""" - self._log.debug('accessing message property') + self._log.debug("accessing message property") if self._message is None: self._message = self._process_dsc_file() return self._message @@ -713,8 +713,7 @@ def checksums(self): def validate(self): """Raise exceptions if files are missing or checksums are bad.""" if not self.all_files_present: - raise DscMissingFileError( - [x[0] for x in self._source_files if not x[2]]) + raise DscMissingFileError([x[0] for x in self._source_files if not x[2]]) if not self.all_checksums_correct: raise DscBadChecksumsError(self.corrected_checksums) @@ -722,23 +721,22 @@ def _process_checksums(self): """Walk through the dsc message looking for any keys in the format 'Checksum-hashtype'. Return a nested dictionary in the form {hashtype: {filename: {digest}}}""" - self._log.debug('process_checksums()') + self._log.debug("process_checksums()") sums = {} for key in self.message.keys(): - if key.lower().startswith('checksums'): - hashtype = key.split('-')[1].lower() + if key.lower().startswith("checksums"): + hashtype = key.split("-")[1].lower() # grrrrrr debian :( :( :( - elif key.lower() == 'files': - hashtype = 'md5' + elif key.lower() == "files": + hashtype = "md5" else: continue sums[hashtype] = {} source = self.message[key] - for line in source.split('\n'): + for line in source.split("\n"): if line: # grrr py3-- - digest, _, filename = line.strip().split(' ') - pathname = os.path.abspath( - os.path.join(self._dirname, filename)) + digest, _, filename = line.strip().split(" ") + pathname = os.path.abspath(os.path.join(self._dirname, filename)) sums[hashtype][pathname] = digest return sums @@ -746,37 +744,35 @@ def _internalize_message(self, msg): """Ugh: the dsc message body may not include a Files or Checksums-foo entry for _itself_, which makes for hilarious misadventures up the chain. So, pfeh, we add it.""" - self._log.debug('internalize_message()') + self._log.debug("internalize_message()") base = os.path.basename(self.filename) size = os.path.getsize(self.filename) for key, source in msg.items(): - self._log.debug('processing key: %s', key) - if key.lower().startswith('checksums'): - hashtype = key.split('-')[1].lower() - elif key.lower() == 'files': - hashtype = 'md5' + self._log.debug("processing key: %s", key) + if key.lower().startswith("checksums"): + hashtype = key.split("-")[1].lower() + elif key.lower() == "files": + hashtype = "md5" else: continue found = [] - for line in source.split('\n'): + for line in source.split("\n"): if line: # grrr - found.append(line.strip().split(' ')) + found.append(line.strip().split(" ")) files = [x[2] for x in found] if base not in files: - self._log.debug('dsc file not found in %s: %s', key, base) - self._log.debug('getting hasher for %s', hashtype) + self._log.debug("dsc file not found in %s: %s", key, base) + self._log.debug("getting hasher for %s", hashtype) hasher = getattr(hashlib, hashtype)() - self._log.debug('hashing file') - with open(self.filename, 'rb') as fileobj: + self._log.debug("hashing file") + with open(self.filename, "rb") as fileobj: # pylint: disable=cell-var-from-loop - for chunk in iter(lambda: fileobj.read(1024), b''): + for chunk in iter(lambda: fileobj.read(1024), b""): hasher.update(chunk) - self._log.debug('completed hashing file') - self._log.debug('got %s digest: %s', - hashtype, hasher.hexdigest()) - newline = '\n {0} {1} {2}'.format( - hasher.hexdigest(), size, base) - self._log.debug('new line: %s', newline) + self._log.debug("completed hashing file") + self._log.debug("got %s digest: %s", hashtype, hasher.hexdigest()) + newline = "\n {0} {1} {2}".format(hasher.hexdigest(), size, base) + self._log.debug("new line: %s", newline) msg.replace_header(key, msg[key] + newline) return msg @@ -784,28 +780,29 @@ def _process_dsc_file(self): """Extract the dsc message from a file: parse the dsc body and return an email.Message object. Attempt to extract the RFC822 message from an OpenPGP message if necessary.""" - self._log.debug('process_dsc_file()') - if not self.filename.endswith('.dsc'): + self._log.debug("process_dsc_file()") + if not self.filename.endswith(".dsc"): self._log.debug( - 'File %s does not appear to be a dsc file; pressing ' - 'on but we may experience some turbulence and possibly ' - 'explode.', self.filename) + "File %s does not appear to be a dsc file; pressing " + "on but we may experience some turbulence and possibly " + "explode.", + self.filename, + ) try: self._pgp_message = pgpy.PGPMessage.from_file(self.filename) - self._log.debug('Found pgp signed message') + self._log.debug("Found pgp signed message") msg = message_from_string(self._pgp_message.message) except TypeError as ex: self._log.exception(ex) self._log.fatal( - 'dsc file %s has a corrupt signature: %s', self.filename, ex) + "dsc file %s has a corrupt signature: %s", self.filename, ex + ) raise DscBadSignatureError except IOError as ex: - self._log.fatal('Could not read dsc file "%s": %s', - self.filename, ex) + self._log.fatal('Could not read dsc file "%s": %s', self.filename, ex) raise except (ValueError, pgpy.errors.PGPError) as ex: - self._log.warning('dsc file %s is not signed: %s', - self.filename, ex) + self._log.warning("dsc file %s is not signed: %s", self.filename, ex) with open(self.filename) as fileobj: msg = message_from_file(fileobj) msg = self._internalize_message(msg) @@ -822,21 +819,20 @@ def _process_source_files(self): Also extract the file size from the message lines and fill out the _files dictionary. """ - self._log.debug('process_source_files()') + self._log.debug("process_source_files()") filenames = [] try: - files = self.message['Files'] + files = self.message["Files"] except KeyError: - self._log.fatal('DSC file "%s" does not have a Files section', - self.filename) + self._log.fatal( + 'DSC file "%s" does not have a Files section', self.filename + ) raise - for line in files.split('\n'): + for line in files.split("\n"): if line: - _, size, filename = line.strip().split(' ') - pathname = os.path.abspath( - os.path.join(self._dirname, filename)) - filenames.append( - (pathname, int(size), os.path.isfile(pathname))) + _, size, filename = line.strip().split(" ") + pathname = os.path.abspath(os.path.join(self._dirname, filename)) + filenames.append((pathname, int(size), os.path.isfile(pathname))) return filenames def _validate_checksums(self): @@ -844,14 +840,14 @@ def _validate_checksums(self): dsc file. Check each in turn. If any checksum is invalid, append the correct checksum to a similarly structured dict and return them all at the end.""" - self._log.debug('validate_checksums()') + self._log.debug("validate_checksums()") bad_hashes = defaultdict(lambda: defaultdict(None)) for hashtype, filenames in six.iteritems(self.checksums): for filename, digest in six.iteritems(filenames): hasher = getattr(hashlib, hashtype)() - with open(filename, 'rb') as fileobj: + with open(filename, "rb") as fileobj: # pylint: disable=cell-var-from-loop - for chunk in iter(lambda: fileobj.read(128), b''): + for chunk in iter(lambda: fileobj.read(128), b""): hasher.update(chunk) if hasher.hexdigest() != digest: bad_hashes[hashtype][filename] = hasher.hexdigest() diff --git a/tests/test_dpkg.py b/tests/test_dpkg.py index 2bec9b7..8f5bc46 100644 --- a/tests/test_dpkg.py +++ b/tests/test_dpkg.py @@ -7,8 +7,8 @@ from pydpkg import Dpkg, DpkgVersionError -TEST_DPKG_GZ_FILE = 'testdeb_1:0.0.0-test_all.deb' -TEST_DPKG_XZ_FILE = 'sample_package_xz.deb' +TEST_DPKG_GZ_FILE = "testdeb_1:0.0.0-test_all.deb" +TEST_DPKG_XZ_FILE = "sample_package_xz.deb" class DpkgGzTest(unittest.TestCase): @@ -18,22 +18,22 @@ def setUp(self): def test_get_versions(self): self.assertEqual(self.dpkg.epoch, 1) - self.assertEqual(self.dpkg.upstream_version, '0.0.0') - self.assertEqual(self.dpkg.debian_revision, 'test') + self.assertEqual(self.dpkg.upstream_version, "0.0.0") + self.assertEqual(self.dpkg.debian_revision, "test") def test_get_message_headers(self): - self.assertEqual(self.dpkg.package, 'testdeb') - self.assertEqual(self.dpkg.PACKAGE, 'testdeb') - self.assertEqual(self.dpkg['package'], 'testdeb') - self.assertEqual(self.dpkg['PACKAGE'], 'testdeb') - self.assertEqual(self.dpkg.get('package'), 'testdeb') - self.assertEqual(self.dpkg.get('PACKAGE'), 'testdeb') - self.assertEqual(self.dpkg.get('nonexistent'), None) - self.assertEqual(self.dpkg.get('nonexistent', 'foo'), 'foo') + self.assertEqual(self.dpkg.package, "testdeb") + self.assertEqual(self.dpkg.PACKAGE, "testdeb") + self.assertEqual(self.dpkg["package"], "testdeb") + self.assertEqual(self.dpkg["PACKAGE"], "testdeb") + self.assertEqual(self.dpkg.get("package"), "testdeb") + self.assertEqual(self.dpkg.get("PACKAGE"), "testdeb") + self.assertEqual(self.dpkg.get("nonexistent"), None) + self.assertEqual(self.dpkg.get("nonexistent", "foo"), "foo") def test_missing_header(self): - self.assertRaises(KeyError, self.dpkg.__getitem__, 'xyzzy') - self.assertRaises(AttributeError, self.dpkg.__getattr__, 'xyzzy') + self.assertRaises(KeyError, self.dpkg.__getitem__, "xyzzy") + self.assertRaises(AttributeError, self.dpkg.__getattr__, "xyzzy") def test_message(self): self.assertIsInstance(self.dpkg.message, type(Message())) @@ -46,90 +46,89 @@ def setUp(self): def test_get_versions(self): self.assertEqual(self.dpkg.epoch, 0) - self.assertEqual(self.dpkg.upstream_version, '0.0.1') - self.assertEqual(self.dpkg.debian_revision, '0') + self.assertEqual(self.dpkg.upstream_version, "0.0.1") + self.assertEqual(self.dpkg.debian_revision, "0") def test_get_message_headers(self): - self.assertEqual(self.dpkg.package, 'samplepackage.test') - self.assertEqual(self.dpkg.PACKAGE, 'samplepackage.test') - self.assertEqual(self.dpkg['package'], 'samplepackage.test') - self.assertEqual(self.dpkg['PACKAGE'], 'samplepackage.test') - self.assertEqual(self.dpkg.get('package'), 'samplepackage.test') - self.assertEqual(self.dpkg.get('PACKAGE'), 'samplepackage.test') - self.assertEqual(self.dpkg.get('nonexistent'), None) - self.assertEqual(self.dpkg.get('nonexistent', 'foo'), 'foo') + self.assertEqual(self.dpkg.package, "samplepackage.test") + self.assertEqual(self.dpkg.PACKAGE, "samplepackage.test") + self.assertEqual(self.dpkg["package"], "samplepackage.test") + self.assertEqual(self.dpkg["PACKAGE"], "samplepackage.test") + self.assertEqual(self.dpkg.get("package"), "samplepackage.test") + self.assertEqual(self.dpkg.get("PACKAGE"), "samplepackage.test") + self.assertEqual(self.dpkg.get("nonexistent"), None) + self.assertEqual(self.dpkg.get("nonexistent", "foo"), "foo") def test_missing_header(self): - self.assertRaises(KeyError, self.dpkg.__getitem__, 'xyzzy') - self.assertRaises(AttributeError, self.dpkg.__getattr__, 'xyzzy') + self.assertRaises(KeyError, self.dpkg.__getitem__, "xyzzy") + self.assertRaises(AttributeError, self.dpkg.__getattr__, "xyzzy") def test_message(self): self.assertIsInstance(self.dpkg.message, type(Message())) class DpkgVersionsTest(unittest.TestCase): - def test_get_epoch(self): - self.assertEqual(Dpkg.get_epoch('0'), (0, '0')) - self.assertEqual(Dpkg.get_epoch('0:0'), (0, '0')) - self.assertEqual(Dpkg.get_epoch('1:0'), (1, '0')) - self.assertRaises(DpkgVersionError, Dpkg.get_epoch, '1a:0') + self.assertEqual(Dpkg.get_epoch("0"), (0, "0")) + self.assertEqual(Dpkg.get_epoch("0:0"), (0, "0")) + self.assertEqual(Dpkg.get_epoch("1:0"), (1, "0")) + self.assertRaises(DpkgVersionError, Dpkg.get_epoch, "1a:0") def test_get_upstream(self): - self.assertEqual(Dpkg.get_upstream('00'), ('00', '0')) - self.assertEqual(Dpkg.get_upstream('foo'), ('foo', '0')) - self.assertEqual(Dpkg.get_upstream('foo-bar'), ('foo', 'bar')) - self.assertEqual(Dpkg.get_upstream('foo-bar-baz'), ('foo-bar', 'baz')) + self.assertEqual(Dpkg.get_upstream("00"), ("00", "0")) + self.assertEqual(Dpkg.get_upstream("foo"), ("foo", "0")) + self.assertEqual(Dpkg.get_upstream("foo-bar"), ("foo", "bar")) + self.assertEqual(Dpkg.get_upstream("foo-bar-baz"), ("foo-bar", "baz")) def test_split_full_version(self): - self.assertEqual(Dpkg.split_full_version('00'), (0, '00', '0')) - self.assertEqual(Dpkg.split_full_version('00-00'), (0, '00', '00')) - self.assertEqual(Dpkg.split_full_version('0:0'), (0, '0', '0')) - self.assertEqual(Dpkg.split_full_version('0:0-0'), (0, '0', '0')) - self.assertEqual(Dpkg.split_full_version('0:0.0'), (0, '0.0', '0')) - self.assertEqual(Dpkg.split_full_version('0:0.0-0'), (0, '0.0', '0')) - self.assertEqual(Dpkg.split_full_version('0:0.0-00'), (0, '0.0', '00')) + self.assertEqual(Dpkg.split_full_version("00"), (0, "00", "0")) + self.assertEqual(Dpkg.split_full_version("00-00"), (0, "00", "00")) + self.assertEqual(Dpkg.split_full_version("0:0"), (0, "0", "0")) + self.assertEqual(Dpkg.split_full_version("0:0-0"), (0, "0", "0")) + self.assertEqual(Dpkg.split_full_version("0:0.0"), (0, "0.0", "0")) + self.assertEqual(Dpkg.split_full_version("0:0.0-0"), (0, "0.0", "0")) + self.assertEqual(Dpkg.split_full_version("0:0.0-00"), (0, "0.0", "00")) def test_get_alpha(self): - self.assertEqual(Dpkg.get_alphas(''), ('', '')) - self.assertEqual(Dpkg.get_alphas('0'), ('', '0')) - self.assertEqual(Dpkg.get_alphas('00'), ('', '00')) - self.assertEqual(Dpkg.get_alphas('0a'), ('', '0a')) - self.assertEqual(Dpkg.get_alphas('a'), ('a', '')) - self.assertEqual(Dpkg.get_alphas('a0'), ('a', '0')) + self.assertEqual(Dpkg.get_alphas(""), ("", "")) + self.assertEqual(Dpkg.get_alphas("0"), ("", "0")) + self.assertEqual(Dpkg.get_alphas("00"), ("", "00")) + self.assertEqual(Dpkg.get_alphas("0a"), ("", "0a")) + self.assertEqual(Dpkg.get_alphas("a"), ("a", "")) + self.assertEqual(Dpkg.get_alphas("a0"), ("a", "0")) def test_get_digits(self): - self.assertEqual(Dpkg.get_digits('00'), (0, '')) - self.assertEqual(Dpkg.get_digits('0'), (0, '')) - self.assertEqual(Dpkg.get_digits('0a'), (0, 'a')) - self.assertEqual(Dpkg.get_digits('a'), (0, 'a')) - self.assertEqual(Dpkg.get_digits('a0'), (0, 'a0')) + self.assertEqual(Dpkg.get_digits("00"), (0, "")) + self.assertEqual(Dpkg.get_digits("0"), (0, "")) + self.assertEqual(Dpkg.get_digits("0a"), (0, "a")) + self.assertEqual(Dpkg.get_digits("a"), (0, "a")) + self.assertEqual(Dpkg.get_digits("a0"), (0, "a0")) def test_listify(self): - self.assertEqual(Dpkg.listify('0'), ['', 0]) - self.assertEqual(Dpkg.listify('00'), ['', 0]) - self.assertEqual(Dpkg.listify('0a'), ['', 0, 'a', 0]) - self.assertEqual(Dpkg.listify('a0'), ['a', 0]) - self.assertEqual(Dpkg.listify('a00'), ['a', 0]) - self.assertEqual(Dpkg.listify('a'), ['a', 0]) + self.assertEqual(Dpkg.listify("0"), ["", 0]) + self.assertEqual(Dpkg.listify("00"), ["", 0]) + self.assertEqual(Dpkg.listify("0a"), ["", 0, "a", 0]) + self.assertEqual(Dpkg.listify("a0"), ["a", 0]) + self.assertEqual(Dpkg.listify("a00"), ["a", 0]) + self.assertEqual(Dpkg.listify("a"), ["a", 0]) def test_dstringcmp(self): - self.assertEqual(Dpkg.dstringcmp('~', '.'), -1) - self.assertEqual(Dpkg.dstringcmp('~', 'a'), -1) - self.assertEqual(Dpkg.dstringcmp('a', '.'), -1) - self.assertEqual(Dpkg.dstringcmp('a', '~'), 1) - self.assertEqual(Dpkg.dstringcmp('.', '~'), 1) - self.assertEqual(Dpkg.dstringcmp('.', 'a'), 1) - self.assertEqual(Dpkg.dstringcmp('.', '.'), 0) - self.assertEqual(Dpkg.dstringcmp('0', '0'), 0) - self.assertEqual(Dpkg.dstringcmp('a', 'a'), 0) + self.assertEqual(Dpkg.dstringcmp("~", "."), -1) + self.assertEqual(Dpkg.dstringcmp("~", "a"), -1) + self.assertEqual(Dpkg.dstringcmp("a", "."), -1) + self.assertEqual(Dpkg.dstringcmp("a", "~"), 1) + self.assertEqual(Dpkg.dstringcmp(".", "~"), 1) + self.assertEqual(Dpkg.dstringcmp(".", "a"), 1) + self.assertEqual(Dpkg.dstringcmp(".", "."), 0) + self.assertEqual(Dpkg.dstringcmp("0", "0"), 0) + self.assertEqual(Dpkg.dstringcmp("a", "a"), 0) # taken from # http://www.debian.org/doc/debian-policy/ch-controlfields.html#s-f-Version self.assertEqual( - sorted(['a', '', '~', '~~a', '~~'], - key=Dpkg.dstringcmp_key), - ['~~', '~~a', '~', '', 'a']) + sorted(["a", "", "~", "~~a", "~~"], key=Dpkg.dstringcmp_key), + ["~~", "~~a", "~", "", "a"], + ) def test_compare_revision_strings(self): # note that these are testing a single revision string, not the full @@ -137,69 +136,75 @@ def test_compare_revision_strings(self): # revision onto itself, not an upstream of 0.0.9 and a debian of foo. # equals - self.assertEqual(Dpkg.compare_revision_strings('0', '0'), 0) - self.assertEqual(Dpkg.compare_revision_strings('0', '00'), 0) - self.assertEqual(Dpkg.compare_revision_strings('00.0.9', '0.0.9'), 0) - self.assertEqual(Dpkg.compare_revision_strings('0.00.9-foo', '0.0.9-foo'), 0) - self.assertEqual(Dpkg.compare_revision_strings('0.0.9-1.00foo', '0.0.9-1.0foo'), 0) + self.assertEqual(Dpkg.compare_revision_strings("0", "0"), 0) + self.assertEqual(Dpkg.compare_revision_strings("0", "00"), 0) + self.assertEqual(Dpkg.compare_revision_strings("00.0.9", "0.0.9"), 0) + self.assertEqual(Dpkg.compare_revision_strings("0.00.9-foo", "0.0.9-foo"), 0) + self.assertEqual( + Dpkg.compare_revision_strings("0.0.9-1.00foo", "0.0.9-1.0foo"), 0 + ) # less than - self.assertEqual(Dpkg.compare_revision_strings('0.0.9', '0.0.10'), -1) - self.assertEqual(Dpkg.compare_revision_strings('0.0.9-foo', '0.0.10-foo'), -1) - self.assertEqual(Dpkg.compare_revision_strings('0.0.9-foo', '0.0.10-goo'), -1) - self.assertEqual(Dpkg.compare_revision_strings('0.0.9-foo', '0.0.9-goo'), -1) - self.assertEqual(Dpkg.compare_revision_strings('0.0.10-foo', '0.0.10-goo'), -1) - self.assertEqual(Dpkg.compare_revision_strings('0.0.9-1.0foo', '0.0.9-1.1foo'), -1) + self.assertEqual(Dpkg.compare_revision_strings("0.0.9", "0.0.10"), -1) + self.assertEqual(Dpkg.compare_revision_strings("0.0.9-foo", "0.0.10-foo"), -1) + self.assertEqual(Dpkg.compare_revision_strings("0.0.9-foo", "0.0.10-goo"), -1) + self.assertEqual(Dpkg.compare_revision_strings("0.0.9-foo", "0.0.9-goo"), -1) + self.assertEqual(Dpkg.compare_revision_strings("0.0.10-foo", "0.0.10-goo"), -1) + self.assertEqual( + Dpkg.compare_revision_strings("0.0.9-1.0foo", "0.0.9-1.1foo"), -1 + ) # greater than - self.assertEqual(Dpkg.compare_revision_strings('0.0.10', '0.0.9'), 1) - self.assertEqual(Dpkg.compare_revision_strings('0.0.10-foo', '0.0.9-foo'), 1) - self.assertEqual(Dpkg.compare_revision_strings('0.0.10-foo', '0.0.9-goo'), 1) - self.assertEqual(Dpkg.compare_revision_strings('0.0.9-1.0foo', '0.0.9-1.0bar'), 1) + self.assertEqual(Dpkg.compare_revision_strings("0.0.10", "0.0.9"), 1) + self.assertEqual(Dpkg.compare_revision_strings("0.0.10-foo", "0.0.9-foo"), 1) + self.assertEqual(Dpkg.compare_revision_strings("0.0.10-foo", "0.0.9-goo"), 1) + self.assertEqual( + Dpkg.compare_revision_strings("0.0.9-1.0foo", "0.0.9-1.0bar"), 1 + ) def test_compare_versions(self): # "This [the epoch] is a single (generally small) unsigned integer. # It may be omitted, in which case zero is assumed." - self.assertEqual(Dpkg.compare_versions('0.0.0', '0:0.0.0'), 0) - self.assertEqual(Dpkg.compare_versions('0:0.0.0-foo', '0.0.0-foo'), 0) - self.assertEqual(Dpkg.compare_versions('0.0.0-a', '0:0.0.0-a'), 0) + self.assertEqual(Dpkg.compare_versions("0.0.0", "0:0.0.0"), 0) + self.assertEqual(Dpkg.compare_versions("0:0.0.0-foo", "0.0.0-foo"), 0) + self.assertEqual(Dpkg.compare_versions("0.0.0-a", "0:0.0.0-a"), 0) # "The absence of a debian_revision is equivalent to a debian_revision # of 0." - self.assertEqual(Dpkg.compare_versions('0.0.0', '0.0.0-0'), 0) + self.assertEqual(Dpkg.compare_versions("0.0.0", "0.0.0-0"), 0) # tricksy: - self.assertEqual(Dpkg.compare_versions('0.0.0', '0.0.0-00'), 0) + self.assertEqual(Dpkg.compare_versions("0.0.0", "0.0.0-00"), 0) # combining the above - self.assertEqual(Dpkg.compare_versions('0.0.0-0', '0:0.0.0'), 0) + self.assertEqual(Dpkg.compare_versions("0.0.0-0", "0:0.0.0"), 0) # explicitly equal - self.assertEqual(Dpkg.compare_versions('0.0.0', '0.0.0'), 0) - self.assertEqual(Dpkg.compare_versions('1:0.0.0', '1:0.0.0'), 0) - self.assertEqual(Dpkg.compare_versions('0.0.0-10', '0.0.0-10'), 0) - self.assertEqual(Dpkg.compare_versions('2:0.0.0-1', '2:0.0.0-1'), 0) - self.assertEqual(Dpkg.compare_versions('0:a.0.0-foo', '0:a.0.0-foo'), 0) + self.assertEqual(Dpkg.compare_versions("0.0.0", "0.0.0"), 0) + self.assertEqual(Dpkg.compare_versions("1:0.0.0", "1:0.0.0"), 0) + self.assertEqual(Dpkg.compare_versions("0.0.0-10", "0.0.0-10"), 0) + self.assertEqual(Dpkg.compare_versions("2:0.0.0-1", "2:0.0.0-1"), 0) + self.assertEqual(Dpkg.compare_versions("0:a.0.0-foo", "0:a.0.0-foo"), 0) # less than - self.assertEqual(Dpkg.compare_versions('0.0.0-0', '0:0.0.1'), -1) - self.assertEqual(Dpkg.compare_versions('0.0.0-0', '0:0.0.0-a'), -1) - self.assertEqual(Dpkg.compare_versions('0.0.0-0', '0:0.0.0-1'), -1) - self.assertEqual(Dpkg.compare_versions('0.0.9', '0.0.10'), -1) - self.assertEqual(Dpkg.compare_versions('0.9.0', '0.10.0'), -1) - self.assertEqual(Dpkg.compare_versions('9.0.0', '10.0.0'), -1) + self.assertEqual(Dpkg.compare_versions("0.0.0-0", "0:0.0.1"), -1) + self.assertEqual(Dpkg.compare_versions("0.0.0-0", "0:0.0.0-a"), -1) + self.assertEqual(Dpkg.compare_versions("0.0.0-0", "0:0.0.0-1"), -1) + self.assertEqual(Dpkg.compare_versions("0.0.9", "0.0.10"), -1) + self.assertEqual(Dpkg.compare_versions("0.9.0", "0.10.0"), -1) + self.assertEqual(Dpkg.compare_versions("9.0.0", "10.0.0"), -1) # greater than - self.assertEqual(Dpkg.compare_versions('0.0.1-0', '0:0.0.0'), 1) - self.assertEqual(Dpkg.compare_versions('0.0.0-a', '0:0.0.0-1'), 1) - self.assertEqual(Dpkg.compare_versions('0.0.0-a', '0:0.0.0-0'), 1) - self.assertEqual(Dpkg.compare_versions('0.0.9', '0.0.1'), 1) - self.assertEqual(Dpkg.compare_versions('0.9.0', '0.1.0'), 1) - self.assertEqual(Dpkg.compare_versions('9.0.0', '1.0.0'), 1) + self.assertEqual(Dpkg.compare_versions("0.0.1-0", "0:0.0.0"), 1) + self.assertEqual(Dpkg.compare_versions("0.0.0-a", "0:0.0.0-1"), 1) + self.assertEqual(Dpkg.compare_versions("0.0.0-a", "0:0.0.0-0"), 1) + self.assertEqual(Dpkg.compare_versions("0.0.9", "0.0.1"), 1) + self.assertEqual(Dpkg.compare_versions("0.9.0", "0.1.0"), 1) + self.assertEqual(Dpkg.compare_versions("9.0.0", "1.0.0"), 1) # unicode me harder - self.assertEqual(Dpkg.compare_versions(u'2:0.0.44-1', u'2:0.0.44-nobin'), -1) - self.assertEqual(Dpkg.compare_versions(u'2:0.0.44-nobin', u'2:0.0.44-1'), 1) - self.assertEqual(Dpkg.compare_versions(u'2:0.0.44-1', u'2:0.0.44-1'), 0) + self.assertEqual(Dpkg.compare_versions(u"2:0.0.44-1", u"2:0.0.44-nobin"), -1) + self.assertEqual(Dpkg.compare_versions(u"2:0.0.44-nobin", u"2:0.0.44-1"), 1) + self.assertEqual(Dpkg.compare_versions(u"2:0.0.44-1", u"2:0.0.44-1"), 0) if __name__ == "__main__": From b2669e84a9010021fabfa137c4c535376730132e Mon Sep 17 00:00:00 2001 From: "Nathan J. Mehl" Date: Fri, 20 Nov 2020 17:56:47 -0500 Subject: [PATCH 2/3] Fix tilde comparisons This PR fixes https://github.com/memory/python-dpkg/issues/3, which correctly notes that comparisons of debian versions that share a common root but where the first additional character of the longer version string is a tilde were being handled incorrectly. Also, while we're here: - format with Black - test under python 3.9 - update pep8 test to agree with Black's default line length - add a TODO to refactor the compare_versions function, which now fails pylint's too-many-branches test --- .travis.yml | 5 +++-- README.md | 4 ++-- pydpkg/__init__.py | 16 +++++++++++++--- setup.py | 3 ++- tests/test_dpkg.py | 24 ++++++++++++++++++++++-- 5 files changed, 42 insertions(+), 10 deletions(-) diff --git a/.travis.yml b/.travis.yml index d03213a..e91a7f8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,11 +6,12 @@ python: - "3.6" - "3.7" - "3.8" + - "3.9" before_install: - "pip install -U pip" install: - "pip install -e .[test]" script: - "py.test tests/" - - "pylint pydpkg/" - - "pep8 pydpkg/" + - "pylint -d R0912 pydpkg/" + - "pep8 --max-line-length=90 --ignore=E203 pydpkg/" diff --git a/README.md b/README.md index 959b763..e3d7c8d 100644 --- a/README.md +++ b/README.md @@ -8,8 +8,8 @@ This library can be used to: on platforms that generally lack a native implementation of dpkg 2. compare dpkg version strings, using a pure Python implementation of - the algorithm described at - https://www.debian.org/doc/debian-policy/ch-controlfields.html#s-f-Version + the algorithm described in section 5.6.12 of the debian-policy manual: + https://www.debian.org/doc/debian-policy/ch-controlfields.html#version 3. Parse debian source description (dsc) files, inspect their contents and verify that their source files are present and checksums are diff --git a/pydpkg/__init__.py b/pydpkg/__init__.py index 015ca3a..cd1fc76 100644 --- a/pydpkg/__init__.py +++ b/pydpkg/__init__.py @@ -421,8 +421,8 @@ def listify(revision_str): """Split a revision string into a list of alternating between strings and numbers, padded on either end to always be "str, int, str, int..." and always be of even length. This allows us to trivially implement the - comparison algorithm described at - http://debian.org/doc/debian-policy/ch-controlfields.html#s-f-Version + comparison algorithm described at section 5.6.12 in: + https://www.debian.org/doc/debian-policy/ch-controlfields.html#version """ result = [] while revision_str: @@ -479,8 +479,9 @@ def dstringcmp(a, b): @staticmethod def compare_revision_strings(rev1, rev2): """Compare two debian revision strings as described at - https://www.debian.org/doc/debian-policy/ch-controlfields.html#s-f-Version + https://www.debian.org/doc/debian-policy/ch-controlfields.html#version """ + # FIXME(memory): this function now fails pylint R0912 too-many-branches if rev1 == rev2: return 0 # listify pads results so that we will always be comparing ints to ints @@ -491,6 +492,9 @@ def compare_revision_strings(rev1, rev2): return 0 try: for i, item in enumerate(list1): + # explicitly raise IndexError if we've fallen off the edge of list2 + if i >= len(list2): + raise IndexError # just in case if not isinstance(item, list2[i].__class__): raise DpkgVersionError( @@ -511,8 +515,14 @@ def compare_revision_strings(rev1, rev2): return Dpkg.dstringcmp(item, list2[i]) except IndexError: # rev1 is longer than rev2 but otherwise equal, hence greater + # ...except for goddamn tildes + if list1[len(list2)][0][0] == "~": + return -1 return 1 # rev1 is shorter than rev2 but otherwise equal, hence lesser + # ...except for goddamn tildes + if list2[len(list1)][0][0] == "~": + return 1 return -1 @staticmethod diff --git a/setup.py b/setup.py index 14c70b3..457b946 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,6 @@ from setuptools import setup -__VERSION__ = "1.4.4" +__VERSION__ = "1.5.0" setup( name="pydpkg", @@ -33,6 +33,7 @@ "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", "Programming Language :: Python :: Implementation :: CPython", "Topic :: System :: Archiving :: Packaging", ], diff --git a/tests/test_dpkg.py b/tests/test_dpkg.py index 8f5bc46..38cfb90 100644 --- a/tests/test_dpkg.py +++ b/tests/test_dpkg.py @@ -123,8 +123,8 @@ def test_dstringcmp(self): self.assertEqual(Dpkg.dstringcmp("0", "0"), 0) self.assertEqual(Dpkg.dstringcmp("a", "a"), 0) - # taken from - # http://www.debian.org/doc/debian-policy/ch-controlfields.html#s-f-Version + # taken from section 5.6.12 at: + # https://www.debian.org/doc/debian-policy/ch-controlfields.html#version self.assertEqual( sorted(["a", "", "~", "~~a", "~~"], key=Dpkg.dstringcmp_key), ["~~", "~~a", "~", "", "a"], @@ -192,6 +192,16 @@ def test_compare_versions(self): self.assertEqual(Dpkg.compare_versions("0.0.9", "0.0.10"), -1) self.assertEqual(Dpkg.compare_versions("0.9.0", "0.10.0"), -1) self.assertEqual(Dpkg.compare_versions("9.0.0", "10.0.0"), -1) + self.assertEqual(Dpkg.compare_versions("1.2.3-1~deb7u1", "1.2.3-1"), -1) + self.assertEqual( + Dpkg.compare_versions( + "2.7.4+reloaded2-13ubuntu1", "2.7.4+reloaded2-13+deb9u1" + ), + -1, + ) + self.assertEqual( + Dpkg.compare_versions("2.7.4+reloaded2-13", "2.7.4+reloaded2-13+deb9u1"), -1 + ) # greater than self.assertEqual(Dpkg.compare_versions("0.0.1-0", "0:0.0.0"), 1) @@ -200,6 +210,16 @@ def test_compare_versions(self): self.assertEqual(Dpkg.compare_versions("0.0.9", "0.0.1"), 1) self.assertEqual(Dpkg.compare_versions("0.9.0", "0.1.0"), 1) self.assertEqual(Dpkg.compare_versions("9.0.0", "1.0.0"), 1) + self.assertEqual(Dpkg.compare_versions("1.2.3-1", "1.2.3-1~deb7u1"), 1) + self.assertEqual( + Dpkg.compare_versions( + "2.7.4+reloaded2-13+deb9u1", "2.7.4+reloaded2-13ubuntu1" + ), + 1, + ) + self.assertEqual( + Dpkg.compare_versions("2.7.4+reloaded2-13+deb9u1", "2.7.4+reloaded2-13"), 1 + ) # unicode me harder self.assertEqual(Dpkg.compare_versions(u"2:0.0.44-1", u"2:0.0.44-nobin"), -1) From 6923031cb572364d0c2a782943fb922868944ae6 Mon Sep 17 00:00:00 2001 From: "Nathan J. Mehl" Date: Fri, 20 Nov 2020 18:04:29 -0500 Subject: [PATCH 3/3] oh ffs pylint --- .travis.yml | 2 +- pydpkg/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index e91a7f8..5d60868 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,5 +13,5 @@ install: - "pip install -e .[test]" script: - "py.test tests/" - - "pylint -d R0912 pydpkg/" + - "pylint -d R0912 -d W0511 pydpkg/" - "pep8 --max-line-length=90 --ignore=E203 pydpkg/" diff --git a/pydpkg/__init__.py b/pydpkg/__init__.py index cd1fc76..7ca85e4 100644 --- a/pydpkg/__init__.py +++ b/pydpkg/__init__.py @@ -481,7 +481,7 @@ def compare_revision_strings(rev1, rev2): """Compare two debian revision strings as described at https://www.debian.org/doc/debian-policy/ch-controlfields.html#version """ - # FIXME(memory): this function now fails pylint R0912 too-many-branches + # TODO(memory): this function now fails pylint R0912 too-many-branches if rev1 == rev2: return 0 # listify pads results so that we will always be comparing ints to ints