Skip to content

Commit

Permalink
gnulib-tool.py: New options --extract-[recursive-]dependents.
Browse files Browse the repository at this point in the history
* 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.
  • Loading branch information
bhaible committed Jul 28, 2024
1 parent 8f2cda7 commit e0de8e4
Show file tree
Hide file tree
Showing 4 changed files with 169 additions and 27 deletions.
11 changes: 11 additions & 0 deletions ChangeLog
Original file line number Diff line number Diff line change
@@ -1,3 +1,14 @@
2024-07-28 Bruno Haible <[email protected]>

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.

2024-07-28 Bruno Haible <[email protected]>

gnulib-tool.py: Avoid adding specific modules to a testdir.
Expand Down
7 changes: 7 additions & 0 deletions pygnulib/GLInfo.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
129 changes: 102 additions & 27 deletions pygnulib/GLModuleSystem.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -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.'''
Expand Down Expand Up @@ -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', '')
Expand Down
49 changes: 49 additions & 0 deletions pygnulib/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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:
Expand Down

0 comments on commit e0de8e4

Please sign in to comment.