Paul Eggert wrote:
> I was testing all modules that depend on c99, this way:
> 
>    ./gnulib-tool -h --create-testdir --dir foo $(cd modules && grep -lx 
> 'c99' $(git ls-files))

The libunistring maintenance also needs a way to find all modules that depend
on a particular module. I've missed a corresponding gnulib-tool option for a
long time. This patch adds it. So that you can now write

     ./gnulib-tool -h --create-testdir --dir foo $(./gnulib-tool 
--extract-dependents c99)


2024-07-28  Bruno Haible  <br...@clisp.org>

        gnulib-tool.py: New options --extract-[recursive-]dependents.
        * pygnulib/GLInfo.py (GLInfo.usage): Document --extract-dependents and
        --extract-recursive-dependents options.
        * pygnulib/GLModuleSystem.py (GLModule.getDependenciesRecursively): Move
        method.
        (getDependents, getDependentsRecursively): New methods.
        * pygnulib/main.py (main): Add support for --extract-dependents and
        --extract-recursive-dependents.

diff --git a/pygnulib/GLInfo.py b/pygnulib/GLInfo.py
index 9ed3ea61d4..83659b1c1f 100644
--- a/pygnulib/GLInfo.py
+++ b/pygnulib/GLInfo.py
@@ -134,6 +134,8 @@ def usage(self) -> str:
        gnulib-tool --extract-filelist module
        gnulib-tool --extract-dependencies module
        gnulib-tool --extract-recursive-dependencies module
+       gnulib-tool --extract-dependents module
+       gnulib-tool --extract-recursive-dependents module
        gnulib-tool --extract-autoconf-snippet module
        gnulib-tool --extract-automake-snippet module
        gnulib-tool --extract-include-directive module
@@ -175,6 +177,11 @@ def usage(self) -> str:
       --extract-recursive-dependencies  extract the dependencies of the module
                                         and its dependencies, recursively, all
                                         together, but without the conditions
+      --extract-dependents         list the modules which depend on the given
+                                   module directly.  This is also known as the
+                                   "reverse dependencies".
+      --extract-recursive-dependents  list the modules which depend on the 
given
+                                      module directly or indirectly
       --extract-autoconf-snippet   extract the snippet for configure.ac
       --extract-automake-snippet   extract the snippet for library makefile
       --extract-include-directive  extract the #include directive
diff --git a/pygnulib/GLModuleSystem.py b/pygnulib/GLModuleSystem.py
index 248d9ec800..e713b60fc9 100644
--- a/pygnulib/GLModuleSystem.py
+++ b/pygnulib/GLModuleSystem.py
@@ -23,6 +23,7 @@
 import sys
 import hashlib
 import subprocess as sp
+import shlex
 from collections import defaultdict
 from typing import Any, ClassVar
 from .constants import (
@@ -330,33 +331,6 @@ def repeatModuleInTests(self) -> bool:
         result = self.name == 'libtextstyle-optional'
         return result
 
-    def getDependenciesRecursively(self) -> str:
-        '''Return a list of recursive dependencies of this module separated
-        by a newline.'''
-        handledmodules = set()
-        inmodules = set()
-        outmodules = set()
-
-        # In order to process every module only once (for speed), process an 
"input
-        # list" of modules, producing an "output list" of modules. During each 
round,
-        # more modules can be queued in the input list. Once a module on the 
input
-        # list has been processed, it is added to the "handled list", so we 
can avoid
-        # to process it again.
-        inmodules.add(self)
-        while len(inmodules) > 0:
-            inmodules_this_round = inmodules
-            inmodules = set()  # Accumulator, queue for next round
-            for module in inmodules_this_round:
-                outmodules.add(module)
-                inmodules = 
inmodules.union(module.getDependenciesWithoutConditions())
-            handledmodules = handledmodules.union(inmodules_this_round)
-            # Remove handledmodules from inmodules.
-            inmodules = inmodules.difference(handledmodules)
-
-        module_names = sorted([ module.name
-                                for module in outmodules ])
-        return lines_to_multiline(module_names)
-
     def getLinkDirectiveRecursively(self) -> str:
         '''Return a list of the link directives of this module separated
         by a newline.'''
@@ -554,6 +528,107 @@ def getDependenciesWithConditions(self) -> 
list[tuple[GLModule, str | None]]:
             self.cache['dependenciesWithCond'] = result
         return self.cache['dependenciesWithCond']
 
+    def getDependenciesRecursively(self) -> str:
+        '''Return a list of recursive dependencies of this module separated
+        by a newline.'''
+        handledmodules = set()
+        inmodules = set()
+        outmodules = set()
+
+        # In order to process every module only once (for speed), process an 
"input
+        # list" of modules, producing an "output list" of modules. During each 
round,
+        # more modules can be queued in the input list. Once a module on the 
input
+        # list has been processed, it is added to the "handled list", so we 
can avoid
+        # to process it again.
+        inmodules.add(self)
+        while len(inmodules) > 0:
+            inmodules_this_round = inmodules
+            inmodules = set()  # Accumulator, queue for next round
+            for module in inmodules_this_round:
+                outmodules.add(module)
+                inmodules = 
inmodules.union(module.getDependenciesWithoutConditions())
+            handledmodules = handledmodules.union(inmodules_this_round)
+            # Remove handledmodules from inmodules.
+            inmodules = inmodules.difference(handledmodules)
+
+        module_names = sorted([ module.name
+                                for module in outmodules ])
+        return lines_to_multiline(module_names)
+
+    def getDependents(self) -> list[GLModule]:
+        '''Return list of dependents (a.k.a. "reverse dependencies"),
+        as a list of GLModule objects.
+        GLConfig: localpath.'''
+        if 'dependents' not in self.cache:
+            localpath = self.config['localpath']
+            # Find a set of module candidates quickly.
+            # TODO: Optimize. This approach is fine for a single getDependents
+            #       invocation, but not for 100 or 1000 of them.
+            # Convert the module name to a POSIX basic regex.
+            # Needs to handle . [ \ * ^ $.
+            regex = self.name.replace('\\', '\\\\').replace('[', 
'\\[').replace('^', '\\^')
+            regex = re.compile(r'([.*$])').sub(r'[\1]', regex)
+            line_regex = '^' + regex
+            # We can't add a '$' to line_regex, because that would fail to 
match
+            # lines that denote conditional dependencies. We could invoke grep
+            # twice, once to search for  line_regex + '$'  and once to search
+            # for   line_regex + [ <TAB>]  but that would be twice as slow.
+            # Read module candidates from gnulib root directory.
+            command = "find modules -type f -print | xargs -n 100 grep -l %s 
/dev/null | sed -e 's,^modules/,,'" % shlex.quote(line_regex)
+            with sp.Popen(command, shell=True, cwd=DIRS['root'], 
stdout=sp.PIPE) as proc:
+                result = proc.stdout.read().decode('UTF-8')
+            # Read module candidates from local directories.
+            if localpath != None and len(localpath) > 0:
+                command = "find modules -type f -print | xargs -n 100 grep -l 
%s /dev/null | sed -e 's,^modules/,,' -e 's,\\.diff$,,'" % 
shlex.quote(line_regex)
+                for localdir in localpath:
+                    with sp.Popen(command, shell=True, cwd=localdir, 
stdout=sp.PIPE) as proc:
+                        result += proc.stdout.read().decode('UTF-8')
+            listing = [ line
+                        for line in result.split('\n')
+                        if line.strip() ]
+            # Remove modules/ prefix from each file name.
+            pattern = re.compile(r'^modules/')
+            listing = [ pattern.sub('', line)
+                        for line in listing ]
+            # Filter out undesired file names.
+            listing = [ line
+                        for line in listing
+                        if self.modulesystem.file_is_module(line) ]
+            candidates = sorted(set(listing))
+            result = []
+            for name in candidates:
+                module = self.modulesystem.find(name)
+                if module:  # Ignore module candidates that don't actually 
exist.
+                    if self in module.getDependenciesWithoutConditions():
+                        result.append(module)
+            self.cache['dependents'] = result
+        return self.cache['dependents']
+
+    def getDependentsRecursively(self) -> str:
+        '''Return a list of recursive dependents of this module,
+        as a list of GLModule objects.'''
+        handledmodules = set()
+        inmodules = set()
+        outmodules = set()
+
+        # In order to process every module only once (for speed), process an 
"input
+        # list" of modules, producing an "output list" of modules. During each 
round,
+        # more modules can be queued in the input list. Once a module on the 
input
+        # list has been processed, it is added to the "handled list", so we 
can avoid
+        # to process it again.
+        inmodules.add(self)
+        while len(inmodules) > 0:
+            inmodules_this_round = inmodules
+            inmodules = set()  # Accumulator, queue for next round
+            for module in inmodules_this_round:
+                outmodules.add(module)
+                inmodules = inmodules.union(module.getDependents())
+            handledmodules = handledmodules.union(inmodules_this_round)
+            # Remove handledmodules from inmodules.
+            inmodules = inmodules.difference(handledmodules)
+
+        return outmodules
+
     def getAutoconfEarlySnippet(self) -> str:
         '''Return autoconf-early snippet.'''
         return self.sections.get('configure.ac-early', '')
diff --git a/pygnulib/main.py b/pygnulib/main.py
index 6953a84917..dbff9dc76a 100644
--- a/pygnulib/main.py
+++ b/pygnulib/main.py
@@ -213,6 +213,14 @@ def main(temp_directory: str) -> None:
                         dest='mode_xrecursive_dependencies',
                         default=None,
                         action='store_true')
+    parser.add_argument('--extract-dependents',
+                        dest='mode_xdependents',
+                        default=None,
+                        action='store_true')
+    parser.add_argument('--extract-recursive-dependents',
+                        dest='mode_xrecursive_dependents',
+                        default=None,
+                        action='store_true')
     parser.add_argument('--extract-autoconf-snippet',
                         dest='mode_xautoconf',
                         default=None,
@@ -558,6 +566,9 @@ def main(temp_directory: str) -> None:
         cmdargs.mode_xusability_in_testdir,
         cmdargs.mode_xfilelist,
         cmdargs.mode_xdependencies,
+        cmdargs.mode_xrecursive_dependencies,
+        cmdargs.mode_xdependents,
+        cmdargs.mode_xrecursive_dependents,
         cmdargs.mode_xautoconf,
         cmdargs.mode_xautomake,
         cmdargs.mode_xinclude,
@@ -648,6 +659,12 @@ def main(temp_directory: str) -> None:
     if cmdargs.mode_xrecursive_dependencies != None:
         mode = 'extract-recursive-dependencies'
         modules = list(cmdargs.non_option_arguments)
+    if cmdargs.mode_xdependents != None:
+        mode = 'extract-dependents'
+        modules = list(cmdargs.non_option_arguments)
+    if cmdargs.mode_xrecursive_dependents != None:
+        mode = 'extract-recursive-dependents'
+        modules = list(cmdargs.non_option_arguments)
     if cmdargs.mode_xinclude != None:
         mode = 'extract-include-directive'
         modules = list(cmdargs.non_option_arguments)
@@ -1241,6 +1258,38 @@ def main(temp_directory: str) -> None:
             if module:
                 sys.stdout.write(module.getDependenciesRecursively())
 
+    elif mode == 'extract-dependents':
+        if avoids:
+            message = '%s: *** ' % APP['name']
+            message += 'cannot combine --avoid and --extract-dependents\n'
+            message += '%s: *** Stop.\n' % APP['name']
+            sys.stderr.write(message)
+            sys.exit(1)
+        modulesystem = GLModuleSystem(config)
+        for name in modules:
+            module = modulesystem.find(name)
+            if module:
+                dependents = module.getDependents()
+                dependents_names = sorted([ m.name
+                                            for m in dependents ])
+                sys.stdout.write(lines_to_multiline(dependents_names))
+
+    elif mode == 'extract-recursive-dependents':
+        if avoids:
+            message = '%s: *** ' % APP['name']
+            message += 'cannot combine --avoid and 
--extract-recursive-dependents\n'
+            message += '%s: *** Stop.\n' % APP['name']
+            sys.stderr.write(message)
+            sys.exit(1)
+        modulesystem = GLModuleSystem(config)
+        for name in modules:
+            module = modulesystem.find(name)
+            if module:
+                dependents = module.getDependentsRecursively()
+                dependents_names = sorted([ m.name
+                                            for m in dependents ])
+                sys.stdout.write(lines_to_multiline(dependents_names))
+
     elif mode == 'extract-autoconf-snippet':
         modulesystem = GLModuleSystem(config)
         for name in modules:




Reply via email to