Mercurial > hgrepos > Python > apps > py-cutils
changeset 210:1be3af138183
Refactor option handling for configuring symlink handling: now all variations are supported
| author | Franz Glasner <fzglas.hg@dom66.de> |
|---|---|
| date | Thu, 23 Jan 2025 09:16:51 +0100 |
| parents | efbf99bd0910 |
| children | 5bb0b25f8e99 |
| files | cutils/treesum.py |
| diffstat | 1 files changed, 133 insertions(+), 28 deletions(-) [+] |
line wrap: on
line diff
--- a/cutils/treesum.py Wed Jan 22 19:09:58 2025 +0100 +++ b/cutils/treesum.py Thu Jan 23 09:16:51 2025 +0100 @@ -16,6 +16,7 @@ import argparse import base64 import binascii +import collections import datetime import logging import os @@ -63,12 +64,19 @@ help="Put given comment COMMENT into the output as \"COMMENT\". " "Can be given more than once.") gp.add_argument( - "--follow-directory-symlinks", "-l", action="store_true", - dest="follow_directory_symlinks", - help="Follow symbolic links to directories when walking a " - "directory tree. Note that this is different from using " - "\"--logical\" or \"--physical\" for arguments given " - "directly on the command line") + "--follow-directory-symlinks", "-l", action=SymlinkAction, + const="follow-directory-symlinks", + default=FollowSymlinkConfig(False, False, True), + dest="follow_symlinks", + help="""Follow symbolic links to directories when walking a +directory tree. Augments --physical.""") + gp.add_argument( + "--follow-file-symlinks", action=SymlinkAction, + const="follow-file-symlinks", + default=FollowSymlinkConfig(False, False, True), + dest="follow_symlinks", + help="""Follow symbolic links to files when walking a +directory tree. Augments --physical.""") gp.add_argument( "--full-mode", action="store_true", dest="metadata_full_mode", help="Consider all mode bits as returned from stat(2) when " @@ -83,11 +91,13 @@ `normal' prints just whether Python 2 or Python 3 is used, and `none' suppresses the output completely. The default is `normal'.""") gp.add_argument( - "--logical", "-L", dest="logical", action="store_true", - default=None, - help="Follow symbolic links given on command line arguments." - " Note that this is a different setting as to follow symbolic" - " links to directories when traversing a directory tree.") + "--logical", "-L", action=SymlinkAction, dest="follow_symlinks", + const=FollowSymlinkConfig(True, True, True), + help="""Follow symbolic links everywhere: on command line +arguments and -- while walking -- directory and file symbolic links. +Overwrites any other symlink related options +(--physical, no-follow-directory-symlinks, no-follow-file-symlinks, et al.). +""") gp.add_argument( "--minimal", nargs="?", const="", default=None, metavar="TAG", help="Produce minimal output only. If a TAG is given and not " @@ -108,6 +118,18 @@ "generating digests for directories. Digests for files are " "not affected.") gp.add_argument( + "--no-follow-directory-symlinks", action=SymlinkAction, + const="no-follow-directory-symlinks", + dest="follow_symlinks", + help="""Do not follow symbolic links to directories when walking a +directory tree. Augments --logical.""") + gp.add_argument( + "--no-follow-file-symlinks", action=SymlinkAction, + const="no-follow-file-symlinks", + dest="follow_symlinks", + help="""Dont follow symbolic links to files when walking a +directory tree. Augments --logical.""") + gp.add_argument( "--no-mmap", action="store_false", dest="mmap", default=None, help="Dont use mmap.") gp.add_argument( @@ -115,10 +137,13 @@ help="Put the checksum into given file. " "If not given or if it is given as `-' then stdout is used.") gp.add_argument( - "--physical", "-P", dest="logical", action="store_false", - default=None, - help="Do not follow symbolic links given on comment line " - "arguments. This is the default.") + "--physical", "-P", action=SymlinkAction, dest="follow_symlinks", + const=FollowSymlinkConfig(False, False, False), + help="""Do not follow any symbolic links whether they are given +on the command line or when walking the directory tree. +Overwrites any other symlink related options +(--logical, follow-directory-symlinks, follow-file-symlinks, et al.). +This is the default.""") gp.add_argument( "--print-size", action="store_true", help="""Print the size of a file or the accumulated sizes of @@ -249,12 +274,81 @@ return treesum(opts) +FollowSymlinkConfig = collections.namedtuple("FollowSymlinkConfig", + ["command_line", + "directory", + "file"]) + + +class SymlinkAction(argparse.Action): + + """`type' is fixed here. + `dest' is a tuple with three items: + + 1. follow symlinks on the command line + 2. follow directory symlinks while walking + 3. follow file symlinks while walking (not yet implemented) + + """ + + def __init__(self, *args, **kwargs): + if "nargs" in kwargs: + raise ValueError("`nargs' not allowed") + if "type" in kwargs: + raise ValueError("`type' not allowed") + c = kwargs.get("const", None) + if c is None: + raise ValueError("a const value is needed") + if (not isinstance(c, FollowSymlinkConfig) + and c not in ("follow-directory-symlinks", + "no-follow-directory-symlinks", + "follow-file-symlinks", + "no-follow-file-symlinks")): + raise ValueError( + "invalid value for the `const' configuration value") + default = kwargs.get("default", None) + if (default is not None + and not isinstance(default, FollowSymlinkConfig)): + raise TypeError("invalid type for `default'") + kwargs["nargs"] = 0 + super(SymlinkAction, self).__init__(*args, **kwargs) + + def __call__(self, parser, namespace, values, option_string=None): + curval = getattr(namespace, self.dest, None) + if curval is None: + curval = FollowSymlinkConfig(False, False, True) + if isinstance(self.const, FollowSymlinkConfig): + curval = self.const + else: + if self.const == "follow-directory-symlinks": + curval = FollowSymlinkConfig( + curval.command_line, True, curval.file) + elif self.const == "no-follow-directory-symlinks": + curval = FollowSymlinkConfig( + curval.command_line, False, curval.file) + elif self.const == "follow-file-symlinks": + curval = FollowSymlinkConfig( + curval.command_line, curval.directory, True) + elif self.const == "no-follow-file-symlinks": + curval = FollowSymlinkConfig( + curval.command_line, curval.directory, False) + else: + assert False, "Implementation error: not yet implemented" + + # Not following symlinks to files is not yet supported: reset to True + if not curval.file: + curval = FollowSymlinkConfig( + curval.command_line, curval.directory, True) + logging.warning("Coercing options to `follow-file-symlinks'") + setattr(namespace, self.dest, curval) + + def gen_generate_opts(directories=[], algorithm=util.default_algotag(), append_output=False, base64=False, comment=[], - follow_directory_symlinks=False, + follow_symlinks=FollowSymlinkConfig(False, False, False), full_mode=False, generator="normal", logical=None, @@ -266,13 +360,21 @@ print_size=False, size_only=False, utf8=False): + # Not following symlinks to files is not yet supported: reset to True + if not isinstance(follow_symlinks, FollowSymlinkConfig): + raise TypeError("`follow_symlinks' must be a FollowSymlinkConfig") + if not follow_symlinks.file: + follow_symlinks = follow_symlinks._make([follow_symlinks.command_line, + follow_symlinks.directory, + True]) + logging.warning("Coercing to follow-symlinks-file") opts = argparse.Namespace( directories=directories, algorithm=util.argv2algo(algorithm), append_output=append_output, base64=base64, comment=comment, - follow_directory_symlinks=follow_directory_symlinks, + follow_symlinks=follow_symlinks, generator=generator, logical=logical, minimal=minimal, @@ -326,10 +428,9 @@ with out_cm as outfp: for d in opts.directories: - V1DirectoryTreesumGenerator( opts.algorithm, opts.mmap, opts.base64, - opts.logical, opts.follow_directory_symlinks, + opts.follow_symlinks, opts.generator, opts.metadata_mode, opts.metadata_full_mode, @@ -344,7 +445,7 @@ class V1DirectoryTreesumGenerator(object): def __init__(self, algorithm, use_mmap, use_base64, - handle_root_logical, follow_directory_symlinks, + follow_symlinks, with_generator, with_metadata_mode, with_metadata_full_mode, with_metadata_mtime, size_only, print_size, utf8_mode, @@ -353,8 +454,7 @@ self._algorithm = algorithm self._use_mmap = use_mmap self._use_base64 = use_base64 - self._handle_root_logical = handle_root_logical - self._follow_directory_symlinks = follow_directory_symlinks + self._follow_symlinks = follow_symlinks self._with_generator = with_generator self._with_metadata_mode = with_metadata_mode self._with_metadata_full_mode = with_metadata_full_mode @@ -404,10 +504,12 @@ flags.append("with-metadata-mode") if self._with_metadata_mtime: flags.append("with-metadata-mtime") - if self._handle_root_logical: - flags.append("logical") - if self._follow_directory_symlinks: - flags.append("follow-directory-symlinks") + if self._follow_symlinks.command_line: + flags.append("follow-symlinks-commandline") + if self._follow_symlinks.directory: + flags.append("follow-symlinks-directory") + if self._follow_symlinks.file: + flags.append("follow-symlinks-file") if self._size_only: flags.append("size-only") if self._utf8_mode: @@ -444,7 +546,7 @@ "ROOT", None, walk.WalkDirEntry.alt_u8(root), False)) self._outfp.flush() - if not self._handle_root_logical and os.path.islink(root): + if not self._follow_symlinks.command_line and os.path.islink(root): linktgt = walk.WalkDirEntry.from_readlink(os.readlink(root)) linkdgst = self._algorithm[0]() linkdgst.update( @@ -480,6 +582,9 @@ "CRC32", self._outfp.hexcrcdigest(), None, False)) def _generate(self, root, top): + # This is currently always True + assert self._follow_symlinks.file + logging.debug("Handling %s/%r", root, top) path = os.path.join(root, *top) if top else root with walk.ScanDir(path) as dirscan: @@ -493,7 +598,7 @@ dir_tainted = False for fso in fsobjects: if fso.is_dir: - if fso.is_symlink and not self._follow_directory_symlinks: + if fso.is_symlink and not self._follow_symlinks.directory: linktgt = walk.WalkDirEntry.from_readlink( os.readlink(fso.path)) # linktgt = util.fsencode(os.readlink(fso.path)))
