# HG changeset patch # User Franz Glasner # Date 1736509604 -3600 # Node ID e081b6ee55706dbea5970032789274e053e9167e # Parent 804a823c63f51537419b5af6e3ce6b6e6a762100 treesum.py now runs on Python3.4 also: use a workaround for its missing byte % formatting. No extra module is required for it to run using sha SHA and SHA-2 family of digests. diff -r 804a823c63f5 -r e081b6ee5570 cutils/treesum.py --- a/cutils/treesum.py Fri Jan 10 11:38:31 2025 +0100 +++ b/cutils/treesum.py Fri Jan 10 12:46:44 2025 +0100 @@ -341,11 +341,13 @@ if not handle_root_logical and os.path.islink(root): linktgt = util.fsencode(os.readlink(root)) linkdgst = algorithm[0]() - linkdgst.update(b"%d:%s," % (len(linktgt), linktgt)) + linkdgst.update( + util.interpolate_bytes(b"%d:%s,", len(linktgt), linktgt)) dir_dgst = algorithm[0]() dir_dgst.update(b"1:L,") dir_dgst.update( - b"%d:%s," % (len(linkdgst.digest()), linkdgst.digest())) + util.interpolate_bytes( + b"%d:%s,", len(linkdgst.digest()), linkdgst.digest())) if size_only: outfp.write( format_bsd_line( @@ -375,13 +377,15 @@ if fso.is_symlink and not follow_directory_symlinks: linktgt = util.fsencode(os.readlink(fso.path)) linkdgst = algorithm[0]() - linkdgst.update(b"%d:%s," % (len(linktgt), linktgt)) - dir_dgst.update(b"1:S,%d:%s," - % (len(fso.fsname), fso.fsname)) + linkdgst.update( + util.interpolate_bytes( + b"%d:%s,", len(linktgt), linktgt)) + dir_dgst.update(util.interpolate_bytes( + b"1:S,%d:%s,", len(fso.fsname), fso.fsname)) # no mtime and no mode for symlinks - dir_dgst.update( - b"%d:%s," - % (len(linkdgst.digest()), linkdgst.digest())) + dir_dgst.update(util.interpolate_bytes( + b"%d:%s,", + len(linkdgst.digest()), linkdgst.digest())) opath = "/".join(top) + "/" + fso.name if top else fso.name if size_only: outfp.write( @@ -403,21 +407,25 @@ # fetch from dir_digests dgst, dsz = dir_digests[top + (fso.name,)] dir_size += dsz - dir_dgst.update(b"1:d,%d:%s," % (len(fso.fsname), fso.fsname)) - dir_dgst.update(b"%d:%s," % (len(dgst), dgst)) + dir_dgst.update(util.interpolate_bytes( + b"1:d,%d:%s,", len(fso.fsname), fso.fsname)) + dir_dgst.update(util.interpolate_bytes( + b"%d:%s,", len(dgst), dgst)) if with_metadata_full_mode: modestr = normalized_mode_str(fso.stat.st_mode) if not isinstance(modestr, bytes): modestr = modestr.encode("ascii") - dir_dgst.update(b"8:fullmode,%d:%s," - % (len(modestr), modestr)) + dir_dgst.update(util.interpolate_bytes( + b"8:fullmode,%d:%s,", len(modestr), modestr)) elif with_metadata_mode: modestr = normalized_compatible_mode_str(fso.stat.st_mode) if not isinstance(modestr, bytes): modestr = modestr.encode("ascii") - dir_dgst.update(b"4:mode,%d:%s," % (len(modestr), modestr)) + dir_dgst.update(util.interpolate_bytes( + b"4:mode,%d:%s,", len(modestr), modestr)) else: - dir_dgst.update(b"1:f,%d:%s," % (len(fso.fsname), fso.fsname)) + dir_dgst.update(util.interpolate_bytes( + b"1:f,%d:%s,", len(fso.fsname), fso.fsname)) dir_size += fso.stat.st_size if with_metadata_mtime: mtime = datetime.datetime.utcfromtimestamp( @@ -425,22 +433,25 @@ mtime = mtime.isoformat("T") + "Z" if not isinstance(mtime, bytes): mtime = mtime.encode("ascii") - dir_dgst.update(b"5:mtime,%d:%s," % (len(mtime), mtime)) + dir_dgst.update(util.interpolate_bytes( + b"5:mtime,%d:%s,", len(mtime), mtime)) if with_metadata_full_mode: modestr = normalized_mode_str(fso.stat.st_mode) if not isinstance(modestr, bytes): modestr = modestr.encode("ascii") - dir_dgst.update(b"8:fullmode,%d:%s," - % (len(modestr), modestr)) + dir_dgst.update(util.interpolate_bytes( + b"8:fullmode,%d:%s,", len(modestr), modestr)) elif with_metadata_mode: modestr = normalized_compatible_mode_str(fso.stat.st_mode) if not isinstance(modestr, bytes): modestr = modestr.encode("ascii") - dir_dgst.update(b"4:mode,%d:%s," % (len(modestr), modestr)) + dir_dgst.update(util.interpolate_bytes( + b"4:mode,%d:%s,", len(modestr), modestr)) if not size_only: dgst = digest.compute_digest_file( algorithm[0], fso.path, use_mmap=use_mmap) - dir_dgst.update(b"%d:%s," % (len(dgst), dgst)) + dir_dgst.update(util.interpolate_bytes( + b"%d:%s,", len(dgst), dgst)) opath = "/".join(top) + "/" + fso.name if top else fso.name if size_only: outfp.write( @@ -496,23 +507,23 @@ what = what.encode("ascii") if what == b"TIMESTAMP": assert filename is None - return b"TIMESTAMP = %d%s" % (value, ls) + return util.interpolate_bytes(b"TIMESTAMP = %d%s", value, ls) if what in (b"ISOTIMESTAMP", b"FLAGS", b"VERSION"): assert filename is None if not isinstance(value, bytes): value = value.encode("ascii") - return b"%s = %s%s" % (what, value, ls) + return util.interpolate_bytes(b"%s = %s%s", what, value, ls) assert filename is not None if what == b"COMMENT": if not isinstance(filename, bytes): filename = filename.encode("utf-8") - return b"COMMENT (%s)%s" % (filename, ls) + return util.interpolate_bytes(b"COMMENT (%s)%s", filename, ls) if not isinstance(filename, bytes): filename = util.fsencode(filename) if what == b"SIZE": - return b"SIZE (%s) = %d%s" % (filename, size, ls) + return util.interpolate_bytes(b"SIZE (%s) = %d%s", filename, size, ls) if value is None: - return b"%s (%s)%s" % (what, filename, ls) + return util.interpolate_bytes(b"%s (%s)%s", what, filename, ls) if use_base64: value = base64.b64encode(value) else: @@ -520,9 +531,11 @@ if filename != b"./@": filename = util.normalize_filename(filename, True) if size is None: - return b"%s (%s) = %s%s" % (what, filename, value, ls) + return util.interpolate_bytes( + b"%s (%s) = %s%s", what, filename, value, ls) else: - return b"%s (%s) = %s,%d%s" % (what, filename, value, size, ls) + return util.interpolate_bytes( + b"%s (%s) = %s,%d%s", what, filename, value, size, ls) if __name__ == "__main__": diff -r 804a823c63f5 -r e081b6ee5570 cutils/util/__init__.py --- a/cutils/util/__init__.py Fri Jan 10 11:38:31 2025 +0100 +++ b/cutils/util/__init__.py Fri Jan 10 12:46:44 2025 +0100 @@ -8,6 +8,7 @@ """ __all__ = ["PY2", + "PY35", "normalize_filename", "argv2algo", "algotag2algotype", @@ -26,6 +27,7 @@ PY2 = sys.version_info[0] < 3 +PY35 = sys.version_info[:2] >= (3, 5) def default_algotag(): @@ -191,3 +193,33 @@ if isinstance(what, bytes): return what return os.fsencode(what) + + +def interpolate_bytes(formatstr, *values): + """Interpolate byte strings also on Python 3.4. + + :param bytes formatstr: + :param values: params for interpolation: may *not* contain Unicode strings + :rvalue: the formatted octet + :rtype: bytes + + """ + assert isinstance(formatstr, bytes) + # Python 3.5+ or Python2 know how to interpolate byte strings + if PY35 or PY2: + return formatstr % values + # Workaround with a Latin-1 dance + tformatstr = formatstr.decode("latin1") + tvalues = [] + for v in values: + if PY2: + if isinstance(v, unicode): # noqa: F821 undefined name 'unicode' + assert False + else: + if isinstance(v, str): + assert False + if isinstance(v, bytes): + tvalues.append(v.decode("latin1")) + else: + tvalues.append(v) + return (tformatstr % tuple(tvalues)).encode("latin1")