]> Savannah Git Hosting - gnulib.git/commitdiff
read-only modules; module cache; various fixes
authorDmitry Selyutin <ghostmansd@gmail.com>
Sat, 13 Jan 2018 15:27:57 +0000 (18:27 +0300)
committerDmitry Selyutin <ghostmansd@gmail.com>
Sat, 13 Jan 2018 15:42:02 +0000 (18:42 +0300)
Since gnulib-tool does not modify modules, it is OK to initialize each
module only once. Moreover, since most of the time is spent upon
executing transitive closures and each transitive closure stores all
modules into different lists, it is also a good idea to calculate the
hash only once for read-only modules. These simple additions make
gnulib-tool much faster than before, since each module is initialized
only once-per-run and has a constant hash value.

For example, an invocation of pygnulib.py inside wget2 takes about 0.5
seconds until it completes gnulib-comp.m4 generation; previously it used
to take up to 8 seconds. Needless to say that performing the same task
is much slower when using the good old gnulib-tool: in my experiments
it takes about a minute to complete gnulib-comp.m4 generation.

Some of the TransitiveClosure methods considered useful enough to be
also present as a part of Database class. The refactoring also revealed
that conditional modules computation was implemented incorrectly; this
code was fixed. Moreover, this part of functionality was moved into the
__init__ section, which also saves several milliseconds since it is not
necessary to check whether conditional modules table exists upon each
conditional() invocation.

pygnulib.py
pygnulib/generator.py
pygnulib/module.py
pygnulib/vfs.py

index 77c33214e3523e9f8c4d5795d4eabe38409a5db3..fa32be283f82d4e1d708f855b1ac93c7d03a5ed8 100755 (executable)
@@ -73,7 +73,7 @@ TP_RSYNC_URI = "translationproject.org::tp/latest/"
 
 def list_hook(gnulib, *args, **kwargs):
     (_, _) = (args, kwargs)
-    for module in sorted(gnulib.modules(full=False)):
+    for module in sorted(gnulib.modules()):
         print(module.name, file=sys.stdout)
     return os.EX_OK
 
@@ -85,7 +85,7 @@ def extract_hook(program, gnulib, mode, namespace, *args, **kwargs):
     mode += "s" if mode == "maintainer" else ""
     for module in namespace["modules"]:
         try:
-            module = gnulib.module(module, full=True)
+            module = gnulib.module(module)
             print(module[mode], file=sys.stdout)
         except UnknownModuleError as error:
             print("{0}:".format(program), "warning:", error, file=sys.stderr)
index e5c89dd03579c4a577bdab8a19653eea07fc7077..519011a4225eb3a9b072d5d960665cd264cb2e1c 100644 (file)
@@ -318,7 +318,6 @@ class AutoconfMultisnippetGenerator(BaseGenerator):
     def __iter__(self):
         config = self.__config
         database = self.__database
-        base_table = database.base_table
         modules = self.__modules
         macro_prefix = self.__macro_prefix
 
@@ -339,12 +338,12 @@ class AutoconfMultisnippetGenerator(BaseGenerator):
         conditional = set()
         unconditional = set()
         for dependency in modules:
-            if base_table.conditional(dependency):
+            if database.conditional(dependency):
                 conditional.add(dependency)
             else:
                 unconditional.add(dependency)
-        conditional = sorted(set(conditional))
-        unconditional = sorted(set(unconditional))
+        conditional = sorted(conditional)
+        unconditional = sorted(unconditional)
 
         # Emit the autoconf code for the unconditional modules.
         for module in unconditional:
@@ -370,36 +369,30 @@ class AutoconfMultisnippetGenerator(BaseGenerator):
             for line in AutoconfSnippetGenerator(**arguments):
                 yield "      {}".format(line)
             yield "      {}=true".format(shellvar)
-            try:
-                for (dependency, condition) in sorted(base_table.dependencies(demander)):
-                    if base_table.conditional(dependency):
-                        shellfunc = dependency.shell_function(macro_prefix)
-                        if condition is not None:
-                            yield "      if {}; then".format(condition)
-                            yield "        {}".format(shellfunc)
-                            yield "      fi"
-                        else:
-                            yield "      {}".format(shellfunc)
-            except KeyError:
-                pass
+            for (dependency, condition) in sorted(database.dependencies(demander)):
+                if database.conditional(dependency):
+                    shellfunc = dependency.shell_function(macro_prefix)
+                    if condition is not None:
+                        yield "      if {}; then".format(condition)
+                        yield "        {}".format(shellfunc)
+                        yield "      fi"
+                    else:
+                        yield "      {}".format(shellfunc)
             yield "    fi"
             yield "  }"
 
         # Emit the dependencies from the unconditional to the conditional modules.
         for demander in unconditional:
-            try:
-                for (dependency, condition) in sorted(base_table.dependencies(demander)):
-                    if dependency in modules and base_table.conditional(dependency):
-                        condname = dependency.conditional_name(macro_prefix)
-                        shellfunc = dependency.shell_function(macro_prefix)
-                        if condition is not None:
-                            yield "  if {}; then".format(condition)
-                            yield "    {}".format(shellfunc)
-                            yield "  fi"
-                        else:
-                            yield "  {}".format(shellfunc)
-            except KeyError:
-                pass
+            for (dependency, condition) in sorted(database.dependencies(demander)):
+                if dependency in modules and database.conditional(dependency):
+                    condname = dependency.conditional_name(macro_prefix)
+                    shellfunc = dependency.shell_function(macro_prefix)
+                    if condition is not None:
+                        yield "  if {}; then".format(condition)
+                        yield "    {}".format(shellfunc)
+                        yield "  fi"
+                    else:
+                        yield "  {}".format(shellfunc)
 
         # Define the Automake conditionals.
         yield "  m4_pattern_allow([^{}_GNULIB_ENABLED_])".format(macro_prefix)
@@ -1034,7 +1027,7 @@ class GnulibCompGenerator(BaseGenerator):
             yield "  # Code from module {}:".format(module.name)
             lines = module.early_autoconf_snippet.split("\n")
             for line in filter(lambda line: line.strip(), lines):
-                yield line
+                yield "  {}".format(line)
         yield "])"
         yield ""
         yield "# This macro should be invoked from $configure_ac, in the section"
index f3bfbc7dbd843bcbbc384b8cf59155dfd2253f2a..83fc144cd7099baa094eae31f9e58094245e8090 100644 (file)
@@ -24,33 +24,52 @@ _ITERABLES = frozenset((list, tuple, set, frozenset, type({}.keys()), type({}.va
 
 
 class BaseModule:
-    """gnulib generic module"""
+    """base module"""
     _TABLE = {
         "description"                  : (0x00, str, "Description"),
         "comment"                      : (0x01, str, "Comment"),
-        "status"                       : (0x02, frozenset, "Status"),
+        "status"                       : (0x02, tuple, "Status"),
         "notice"                       : (0x03, str, "Notice"),
         "applicability"                : (0x04, str, "Applicability"),
-        "files"                        : (0x05, frozenset, "Files"),
-        "dependencies"                 : (0x06, frozenset, "Depends-on"),
+        "files"                        : (0x05, tuple, "Files"),
+        "dependencies"                 : (0x06, tuple, "Depends-on"),
         "early_autoconf_snippet"       : (0x07, str, "configure.ac-early"),
         "autoconf_snippet"             : (0x08, str, "configure.ac"),
         "conditional_automake_snippet" : (0x09, str, "Makefile.am"),
         "include_directive"            : (0x0A, str, "Include"),
         "link_directive"               : (0x0B, str, "Link"),
-        "licenses"                     : (0x0C, frozenset, "License"),
-        "maintainers"                  : (0x0D, frozenset, "Maintainer"),
+        "licenses"                     : (0x0C, tuple, "License"),
+        "maintainers"                  : (0x0D, tuple, "Maintainer"),
         # unconditional_automake_snippet
         # automake_snippet
     }
-    _LIB_SOURCES = _re.compile(r"^lib_SOURCES\s*\+\=\s*(.*?)$", _re.S | _re.M)
+    _PROPERTIES = {
+        "name",
+        "description",
+        "comment",
+        "status",
+        "notice",
+        "applicability",
+        "files",
+        "dependencies",
+        "early_autoconf_snippet",
+        "autoconf_snippet",
+        "conditional_automake_snippet",
+        "unconditional_automake_snippet",
+        "automake_snippet",
+        "include_directive",
+        "link_directive",
+        "licenses",
+        "maintainers",
+    }
+    __LIB_SOURCES = _re.compile(r"^lib_SOURCES\s*\+\=\s*(.*?)$", _re.S | _re.M)
 
 
     def __init__(self, name, **kwargs):
         _type_assert("name", name, str)
         if "licenses" in kwargs:
             licenses = set()
-            for license in kwargs.get("licenses", frozenset()):
+            for license in kwargs.get("licenses", tuple()):
                 _type_assert("license", license, str)
                 licenses.add(license)
             kwargs["licenses"] = licenses
@@ -79,42 +98,65 @@ class BaseModule:
 
 
     def __hash__(self):
-        return hash(self.__name) ^ _json.dumps(self.__table, sort_keys=True)
+        return hash(self.__name) ^ hash(_json.dumps(dict(self.__table), sort_keys=True))
 
 
     def __getitem__(self, key):
         _type_assert("key", key, str)
-        if key not in BaseModule._TABLE:
+        if key not in BaseModule._PROPERTIES:
             key = key.replace("-", "_")
-            if key not in BaseModule._TABLE:
+            if key not in BaseModule._PROPERTIES:
                 raise KeyError(repr(key))
         return getattr(self, key)
 
 
     def __setitem__(self, key, value):
         _type_assert("key", key, str)
-        if key not in BaseModule._TABLE:
+        if key not in BaseModule._PROPERTIES:
             key = key.replace("-", "_")
-            if key not in BaseModule._TABLE:
+            if key not in BaseModule._PROPERTIES:
                 raise KeyError(repr(key))
         return setattr(self, key, value)
 
 
     @property
     def package(self):
-        """gnulib-compatible module package"""
-        result = ""
-        for (key, (_, typeid, field)) in sorted(BaseModule._TABLE.items(), key=lambda k: k[1][0]):
-            field += ":\n"
-            if typeid in _ITERABLES:
-                value = "\n".join(self.__table[key])
-            else:
-                value = self.__table[key]
-            if value:
-                result += field
-                result += value
-                result += "\n\n" if value else "\n"
-        return result.strip() + "\n"
+        """gnulib-compatible module textual representation"""
+        def _package():
+            yield "Description:"
+            yield self.__table["description"]
+            yield "Comment:"
+            yield self.__table["comment"]
+            yield "Status:"
+            for status in sorted(self.__table["status"]):
+                yield status
+            yield "Notice:"
+            yield self.__table["notice"]
+            yield "Applicability:"
+            yield self.__table["applicability"]
+            yield "Files:"
+            for file in sorted(self.__table["files"]):
+                yield file
+            yield "Depends-on:"
+            for (module, condition) in self.__table["dependencies"]:
+                yield "{}    {}".format(module, condition)
+            yield "configure.ac-early:"
+            yield self.__table["early_autoconf_snippet"]
+            yield "configure.ac:"
+            yield self.__table["autoconf_snippet"]
+            yield "Makefile.am:"
+            yield self.__table["conditional_automake_snippet"]
+            yield "Include:"
+            yield self.__table["include_directive"]
+            yield "Link:"
+            yield self.__table["link_directive"]
+            yield "License:"
+            for license in sorted(self.__table["licenses"]):
+                yield license
+            yield "Maintainer:"
+            for maintainer in self.__table["maintainers"]:
+                yield maintainer
+        return "\n".join(_package())
 
 
     @property
@@ -162,7 +204,7 @@ class BaseModule:
         for item in value:
             _type_assert("status", item, str)
             result.add(item)
-        self.__table["status"] = frozenset(result)
+        self.__table["status"] = tuple(result)
 
 
     @property
@@ -220,7 +262,7 @@ class BaseModule:
     @property
     def files(self):
         """file dependencies iterator (set of strings)"""
-        return frozenset(self.__table["files"])
+        return self.__table["files"]
 
     @files.setter
     def files(self, value):
@@ -229,7 +271,7 @@ class BaseModule:
         for item in value:
             _type_assert("file", item, str)
             result.add(item)
-        self.__table["files"] = frozenset(result)
+        self.__table["files"] = tuple(result)
 
 
     @property
@@ -252,7 +294,7 @@ class BaseModule:
             if not condition:
                 condition = None
             entries.add((module, condition))
-        return frozenset(entries)
+        return tuple(entries)
 
     @dependencies.setter
     def dependencies(self, value):
@@ -262,7 +304,7 @@ class BaseModule:
             _type_assert("name", name, str)
             _type_assert("condition", condition, str)
             result.add((name, condition))
-        self.__table["dependencies"] = frozenset(result)
+        self.__table["dependencies"] = tuple(result)
 
 
     @property
@@ -296,14 +338,11 @@ class BaseModule:
     def conditional_automake_snippet(self, value):
         _type_assert("conditional_automake_snippet", value, str)
         self.__table["conditional_automake_snippet"] = value
-        self.__table.pop("unconditional_automake_snippet", None)
 
 
     @property
     def unconditional_automake_snippet(self):
         """Makefile.am snippet that must stay outside of Automake conditionals"""
-        if "unconditional_automake_snippet" in self.__table:
-            return self.__table["unconditional_automake_snippet"]
         result = ""
         files = self.files
         if self.name.endswith("-tests"):
@@ -317,13 +356,13 @@ class BaseModule:
         lib_SOURCES = False
         lines = list(snippet.splitlines())
         for (index, line) in enumerate(lines):
-            if BaseModule._LIB_SOURCES.findall(line):
+            if BaseModule.__LIB_SOURCES.findall(line):
                 (first, last) = (index, index)
                 while line.endswith("\\"):
                     line = lines[last]
                     last += 1
                 lines = list(lines)[first:(last + 1)]
-                lines[0] = BaseModule._LIB_SOURCES.sub("\\1", lines[0])
+                lines[0] = BaseModule.__LIB_SOURCES.sub("\\1", lines[0])
                 lib_SOURCES = True
                 break
         lines = tuple(lines) if lib_SOURCES else ()
@@ -362,7 +401,6 @@ class BaseModule:
         buildaux_files = tuple(_os.path.join(prefix, file[len("build-aux/"):]) for file in buildaux_files)
         if buildaux_files:
             result += ("EXTRA_DIST += {}".format(" ".join(sorted(buildaux_files))) + "\n")
-        self.__table["unconditional_automake_snippet"] = result
         return result
 
 
@@ -400,7 +438,7 @@ class BaseModule:
     @property
     def licenses(self):
         """licenses set"""
-        return frozenset(self.__table["licenses"])
+        return self.__table["licenses"]
 
     @licenses.setter
     def licenses(self, value):
@@ -409,7 +447,7 @@ class BaseModule:
         for item in value:
             _type_assert("license", item, str)
             result.add(value)
-        self.__table["licenses"] = frozenset(result)
+        self.__table["licenses"] = tuple(result)
 
 
     @property
@@ -424,7 +462,7 @@ class BaseModule:
         for item in value:
             _type_assert("maintainer", item, str)
             result.add(item)
-        self.__table["maintainers"] = frozenset(result)
+        self.__table["maintainers"] = tuple(result)
 
 
     def shell_variable(self, macro_prefix="gl"):
@@ -459,16 +497,17 @@ class BaseModule:
         return self.__table.items()
 
 
-    def keys(self):
+    @classmethod
+    def keys(cls):
         """a set-like object providing a view on module keys"""
-        for key in BaseModule._TABLE.keys():
+        for key in BaseModule._PROPERTIES:
             yield key
 
 
     def values(self):
         """a set-like object providing a view on module values"""
-        for key in BaseModule._TABLE.keys():
-            yield self[value]
+        for key in BaseModule._PROPERTIES:
+            yield self[key]
 
 
     def __lt__(self, value):
@@ -479,43 +518,49 @@ class BaseModule:
         return self.__lt__(value) or self.__eq__(value)
 
     def __eq__(self, value):
-        return hash(self) == hash(value)
+        _type_assert("value", value, BaseModule)
+        if self.name != value.name:
+            return False
+        for key in BaseModule._PROPERTIES:
+            if self[key] != value[key]:
+                return False
+        return True
 
     def __ne__(self, value):
         return not self.__eq__(value)
 
     def __ge__(self, value):
-        return value.__le__(self)
+        return not value.__le__(self)
 
     def __gt__(self, value):
-        return value.__lt__(self)
+        return not value.__lt__(self)
 
 
 
 class _DummyModuleMeta(type):
     __INSTANCE = None
-    __TABLE = {
+    __PROPERTIES = {
         "description": "A dummy file, to make sure the library is non-empty.",
         "comment": "",
-        "status": frozenset(),
+        "status": tuple(),
         "notice": "",
         "applicability": "main",
-        "files": frozenset({"lib/dummy.c"}),
-        "dependencies": frozenset(),
+        "files": tuple({"lib/dummy.c"}),
+        "dependencies": tuple(),
         "early_autoconf_snippet": "",
         "autoconf_snippet": "",
         "include_directive": "",
         "link_directive": "",
-        "licenses": frozenset({"public domain"}),
-        "maintainers": frozenset({"all"}),
+        "licenses": tuple({"public domain"}),
+        "maintainers": tuple({"all"}),
         "automake_snippet": "lib_SOURCES += dummy.c",
         "conditional_automake_snippet": "lib_SOURCES += dummy.c",
         "unconditional_automake_snippet": "",
     }
 
-    def __new__(cls, name, parents, attributes):
-        self = super().__new__(cls, name, parents, attributes)
-        for (key, value) in _DummyModuleMeta.__TABLE.items():
+    def __new__(mcs, name, parents, attributes):
+        self = super().__new__(mcs, name, parents, attributes)
+        for (key, value) in _DummyModuleMeta.__PROPERTIES.items():
             setattr(self, key, property(lambda self, value=value: value))
         return self
 
@@ -526,70 +571,63 @@ class _DummyModuleMeta(type):
 
 
 class DummyModule(BaseModule, metaclass=_DummyModuleMeta):
+    """dummy module singleton"""
     def __init__(self):
         super().__init__(name="dummy")
 
 
 
 class FileModule(BaseModule):
-    """gnulib module text file"""
+    """text-based module"""
     _TABLE = {_value[2]:(_value[1], _key) for (_key, _value) in BaseModule._TABLE.items()}
     _FIELDS = [_field for (_, _, _field) in BaseModule._TABLE.values()]
     _PATTERN = _re.compile("({}):".format("|".join(_FIELDS)))
 
 
-    def __init__(self, path, mode="r", name=None, **kwargs):
+    def __init__(self, path, name=None, **kwargs):
+        _type_assert("path", path, str)
+        _type_assert("name", name, str)
+        self.__path = path
+        with _codecs.open(path, "rb", "UTF-8") as stream:
+            match = FileModule._PATTERN.split(stream.read())[1:]
         table = {}
-        if name is None:
-            name = _os.path.basename(path)
-        if mode not in ("r", "w", "rw"):
-            raise ValueError("illegal mode: {}".format(mode))
-        if mode == "r":
-            with _codecs.open(path, "rb", "UTF-8") as stream:
-                match = FileModule._PATTERN.split(stream.read())[1:]
-            for (group, value) in zip(match[::2], match[1::2]):
-                (typeid, key) = FileModule._TABLE[group]
-                if typeid in _ITERABLES:
-                    lines = []
-                    for line in value.splitlines():
-                        if line.strip() and not line.startswith("#"):
-                            lines.append(line)
-                    table[key] = typeid(lines)
-                else:
-                    table[key] = value.strip()
-            if "licenses" not in table:
-                table["licenses"] = ["GPL"]
-            if table["licenses"] == "LGPLv3+ or GPLv2":
-                table["licenses"] = ["GPLv2, LGPLv3+"]
-            self.__stream = None
-        elif mode == "w":
-            self.__stream = _codecs.open(path, "w+", "UTF-8")
-        elif mode == "rw":
-            super().__init__(name)
-            self.__init__(path, "r")
-            self.__stream = _codecs.open(path, "w+", "UTF-8")
-        else:
-            raise ValueError("illegal mode: {}".format(mode))
-
+        for (group, value) in zip(match[::2], match[1::2]):
+            (typeid, key) = FileModule._TABLE[group]
+            if typeid in _ITERABLES:
+                lines = []
+                for line in value.splitlines():
+                    if line.strip() and not line.startswith("#"):
+                        lines.append(line)
+                table[key] = typeid(lines)
+            else:
+                table[key] = value.strip()
         for (key, value) in kwargs.items():
             table[key] = value
         super().__init__(name, **table)
 
 
-    def close(self):
-        """Close the underlying stream and write data into the file."""
-        if self.__stream:
-            self.__stream.truncate(0)
-            self.__stream.write(str(self))
-            self.__stream.close()
 
+class GnulibModule(FileModule):
+    """read-only gnulib standard module"""
+    def __init__(self, path, name):
+        super().__init__(path=path, name=name)
+        self.__hash = super().__hash__()
 
-    def __enter__(self):
-        return self
 
+    def __setattr__(self, key, value):
+        if key in BaseModule._PROPERTIES:
+            raise AttributeError("can't set property")
+        super().__setattr__(key, value)
 
-    def __exit__(self, exctype, excval, exctrace):
-        self.close()
+
+    def __hash__(self):
+        return self.__hash
+
+
+    def __eq__(self, other):
+        if isinstance(other, GnulibModule):
+            return hash(self) == hash(other)
+        return super().__eq__(other)
 
 
 
@@ -620,25 +658,26 @@ class TransitiveClosure:
                     raise TypeError("module: pygnulib.module.BaseModule expected")
             return module
 
-        handled = set()
-        conditional = set()
-        unconditional = set()
+        def _update(demander, dependency, condition):
+            demanders[demander][dependency] = condition
+            dependencies[dependency][demander] = condition
+            current.add(dependency)
+
+        current = set()
+        previous = set()
         demanders = _collections.defaultdict(dict)
         dependencies = _collections.defaultdict(dict)
-        modules = {lookup(module) for module in config.modules}
-        for dependency in modules:
-            unconditional.add(dependency)
-            dependencies[dependency][None] = None
-        while modules:
-            for demander in set(modules):
+        for module in config.modules:
+            dependency = lookup(module)
+            _update(None, dependency, None)
+        while current != previous:
+            previous.update(current)
+            for demander in previous:
                 if tests and not demander.name.endswith("-tests"):
                     try:
                         dependency = lookup("{}-tests".format(demander.name))
                         if not _exclude(dependency):
-                            demanders[None][dependency] = None
-                            dependencies[dependency][None] = None
-                            modules.add(dependency)
-                            dependency = demander
+                            _update(None, dependency, None)
                     except _UnknownModuleError:
                         pass # ignore non-existent tests
                 for (dependency, condition) in demander.dependencies:
@@ -650,27 +689,37 @@ class TransitiveClosure:
                         #   configure: error: conditional "..." was never defined.
                         # because automake 1.11.1 does not handle nested conditionals
                         # correctly. As a workaround, make the module unconditional.
-                        demanders[None][dependency] = None
-                        dependencies[dependency][None] = None
-                        unconditional.add(dependency)
+                        _update(None, dependency, None)
                     elif not _exclude(dependency):
-                        if condition is not None:
-                            conditional.add(dependency)
-                        elif demander in conditional:
-                            conditional.add(dependency)
-                        else:
-                            conditional.discard(dependency)
-                            unconditional.add(dependency)
-                        demanders[demander][dependency] = None
-                        dependencies[dependency][demander] = condition
-                    modules.add(dependency)
-                handled.add(demander)
-            modules.difference_update(handled)
+                        _update(demander, dependency, condition)
 
         self.__lookup = _lookup
         self.__demanders = demanders
         self.__dependencies = dependencies
-        self.__conditional = frozenset(conditional)
+
+        conditional = set()
+        unconditional = set()
+        for (dependency, demanders) in self.__dependencies.items():
+            for (demander, _) in demanders.items():
+                if demander is None:
+                    unconditional.add(demander)
+                    break
+        previous = set()
+        current = set(unconditional)
+        while previous != current:
+            previous.update(current)
+            for demander in previous:
+                dependencies = self.__demanders.get(demander, {})
+                for (dependency, condition) in dependencies.items():
+                    if condition is not None:
+                        conditional.add(dependency)
+                    elif demander in conditional:
+                        conditional.add(dependency)
+                    else:
+                        conditional.discard(dependency)
+                        unconditional.add(dependency)
+                current.add(demander)
+        self.__conditional = conditional
 
 
     def __iter__(self):
@@ -755,23 +804,19 @@ class TransitiveClosure:
 
 
     def demanders(self, module):
-        """For each demander which requires the module yield the demander and condition."""
+        """For each demander which requires the module yield the demander and the corresponding condition."""
         module = self.__lookup(module)
-        if module not in self.__dependencies:
-            fmt = "dependency {} not found"
-            raise KeyError(fmt.format(module))
-        for (demander, condition) in self.__dependencies.get(module, {}).items():
-            yield (demander, condition)
+        if module in self.__dependencies:
+            for (demander, condition) in self.__dependencies.get(module, {}).items():
+                yield (demander, condition)
 
 
     def dependencies(self, module):
-        """For each dependency of this module yield the dependency and the condition."""
+        """For each dependency of the module yield this dependency and the corresponding condition."""
         module = self.__lookup(module)
-        if module not in self.__demanders:
-            fmt = "demander {} not found"
-            raise KeyError(fmt.format(module))
-        for (dependency, condition) in self.__demanders.get(module, {}).items():
-            yield (dependency, condition)
+        if module in self.__demanders:
+            for (dependency, condition) in self.__demanders.get(module, {}).items():
+                yield (dependency, condition)
 
 
 
@@ -816,14 +861,14 @@ class Database:
                         return True
             return False
 
-        base_table = TransitiveClosure(lookup, config, False)
-        full_table = TransitiveClosure(lookup, config, True)
-        main_modules = set(base_table)
+        base_closure = TransitiveClosure(lookup, config, False)
+        full_closure = TransitiveClosure(lookup, config, True)
+        main_modules = set(base_closure)
         explicit_modules = set()
-        for module in full_table:
+        for module in full_closure:
             if module.name in config.modules:
                 explicit_modules.add(module)
-        final_modules = set(full_table) if config.tests else main_modules
+        final_modules = set(full_closure) if config.tests else main_modules
         test_modules = (final_modules - set(filter(_applicability, main_modules)))
         libtests = _libtests(test_modules)
         if _dummy(main_modules):
@@ -838,8 +883,7 @@ class Database:
             test_files.add(file)
 
         self.__libtests = libtests
-        self.__base_table = base_table
-        self.__full_table = full_table
+        self.__closure = full_closure
         self.__main_modules = tuple(sorted(main_modules))
         self.__test_modules = tuple(sorted(test_modules))
         self.__final_modules = tuple(sorted(final_modules))
@@ -848,22 +892,32 @@ class Database:
         self.__test_files = tuple(sorted(test_files))
 
 
-    @property
-    def base_table(self):
+    def conditional(self, module):
         """
-        The transitive closure table of the specified modules, ignoring tests modules.
-        An iteration yields a (dependency, demander, condition) on each step.
+        Test whether module is a conditional dependency.
+        Note that this check also takes all parent modules into account.
+        Any module with an unconditional demander is also unconditional.
         """
-        return self.__base_table
+        return self.__closure.conditional(module)
 
 
-    @property
-    def full_table(self):
+    def unconditional(self, module):
         """
-        The full transitive closure table of the specified modules.
-        An iteration yields a (dependency, demander, condition) on each step.
+        Test whether module is an unconditional dependency.
+        Note that this check also takes all parent modules into account.
+        Any module with an unconditional demander is also unconditional.
         """
-        return self.__full_table
+        return not self.conditional(module)
+
+
+    def demanders(self, module):
+        """For each demander which requires the module yield the demander and the corresponding condition."""
+        return self.__closure.demanders(module)
+
+
+    def dependencies(self, module):
+        """For each dependency of the module yield this dependency and the corresponding condition."""
+        return self.__closure.dependencies(module)
 
 
     @property
index dfb0fd57fb4f90abc6b762ba6a009c576d51140f..a0dc16fc3e55d9b603728921f8acb90a86e390b8 100644 (file)
@@ -14,9 +14,8 @@ import subprocess as _sp
 
 from .error import type_assert as _type_assert
 from .error import UnknownModuleError as _UnknownModuleError
-from .module import BaseModule as _BaseModule
 from .module import DummyModule as _DummyModule
-from .module import FileModule as _FileModule
+from .module import GnulibModule as _GnulibModule
 
 
 
@@ -292,6 +291,7 @@ class GnulibGitVFS(BaseVFS):
 
     def __init__(self, prefix, **table):
         super().__init__(prefix, **table)
+        self.__cache = {}
         if not _os.path.exists(self.absolute):
             raise FileNotFoundError(self.absolute)
         if not _os.path.isdir(self.absolute):
@@ -300,20 +300,23 @@ class GnulibGitVFS(BaseVFS):
             raise TypeError("{} is not a gnulib repository".format(prefix))
 
 
-    def module(self, name, full=True):
-        """obtain gnulib module by name"""
+    def module(self, name):
+        """instantiate a module"""
         _type_assert("name", name, str)
-        _type_assert("full", full, bool)
-        if name in GnulibGitVFS._EXCLUDE:
-            raise ValueError("illegal module name")
+        if name in self.__cache:
+            return self.__cache[name]
         path = _os.path.join(self.absolute, self["modules"], name)
         try:
-            return _FileModule(path, name=name) if full else _BaseModule(name)
+            if name != "dummy":
+                self.__cache[name] = _GnulibModule(path=path, name=name)
+            else:
+                self.__cache[name] = _DummyModule()
+            return self.__cache[name]
         except FileNotFoundError:
             raise _UnknownModuleError(name)
 
 
-    def modules(self, full=True):
+    def modules(self):
         """iterate over all available modules"""
         prefix = _os.path.join(self.absolute, self["modules"])
         for root, _, files in _os.walk(prefix):
@@ -329,4 +332,4 @@ class GnulibGitVFS(BaseVFS):
             for name in names:
                 path = _os.path.join(root, name)
                 name = path[len(prefix) + 1:]
-                yield self.module(name, full)
+                yield self.module(name)