]> Savannah Git Hosting - gnulib.git/commitdiff
[python] python gnulib API development branch
authorDmitry Selyutin <ghostmansd@gmail.com>
Fri, 8 Sep 2017 20:35:13 +0000 (23:35 +0300)
committerDmitry Selyutin <ghostmansd@gmail.com>
Fri, 8 Sep 2017 20:35:13 +0000 (23:35 +0300)
pygnulib/config.py [new file with mode: 0644]
pygnulib/error.py [new file with mode: 0644]
pygnulib/filesystem.py [new file with mode: 0644]
pygnulib/generator.py [new file with mode: 0644]
pygnulib/module.py [new file with mode: 0644]

diff --git a/pygnulib/config.py b/pygnulib/config.py
new file mode 100644 (file)
index 0000000..ae84b98
--- /dev/null
@@ -0,0 +1,1170 @@
+#!/usr/bin/python
+# encoding: UTF-8
+
+
+import argparse
+import codecs
+import os
+import re
+
+from .error import AutoconfVersionError
+
+
+class Config:
+    """gnulib generic configuration"""
+    _TABLE_ = {
+        "root"              : "",
+        "local_dir"         : "",
+        "source_base"       : "lib",
+        "m4_base"           : "m4",
+        "po_base"           : "po",
+        "doc_base"          : "doc",
+        "tests_base"        : "tests",
+        "auxdir"            : "",
+        "lib"               : "libgnu",
+        "makefile_name"     : "Makefile.am",
+        "macro_prefix"      : "gl",
+        "po_domain"         : "",
+        "witness_c_macro"   : "",
+        "lgpl"              : 0,
+        "tests"             : False,
+        "obsolete"          : False,
+        "cxx_tests"         : False,
+        "longrunning_tests" : False,
+        "privileged_tests"  : False,
+        "unportable_tests"  : False,
+        "all_tests"         : False,
+        "libtool"           : False,
+        "conddeps"          : False,
+        "vc_files"          : False,
+        "autoconf"          : 2.59,
+        "modules"           : [],
+        "avoid"             : [],
+        "files"             : [],
+    }
+
+
+    def __init__(self, **kwargs):
+        self._table_ = dict()
+        for key in Config._TABLE_:
+            self._table_[key] = Config._TABLE_[key]
+        for key, value in kwargs.items():
+            self[key] = value
+
+
+    def __repr__(self):
+        return repr(self._table_)
+
+
+    def __iter__(self):
+        for key in Config._TABLE_:
+            value = self[key]
+            yield key, value
+
+
+    @property
+    def source_base(self):
+        """directory relative to ROOT where source code is placed; defaults to 'lib'"""
+        return self["source_base"]
+
+    @source_base.setter
+    def source_base(self, value):
+        self["source_base"] = value
+
+
+    @property
+    def m4_base(self):
+        """directory relative to ROOT where *.m4 macros are placed; defaults to 'm4'"""
+        return self["m4_base"]
+
+    @m4_base.setter
+    def m4_base(self, value):
+        self["m4_base"] = value
+
+
+    @property
+    def po_base(self):
+        """directory relative to ROOT where *.po files are placed; defaults to 'po'"""
+        return self["po_base"]
+
+    @po_base.setter
+    def po_base(self, value):
+        self["po_base"] = value
+
+
+    @property
+    def doc_base(self):
+        """directory relative to ROOT where doc files are placed; defaults to 'doc'"""
+        return self["doc_base"]
+
+    @doc_base.setter
+    def doc_base(self, value):
+        self["doc_base"] = value
+
+
+    @property
+    def tests_base(self):
+        """directory relative to ROOT where unit tests are placed; defaults to 'tests'"""
+        return self["tests_base"]
+
+    @tests_base.setter
+    def tests_base(self, value):
+        self["tests_base"] = value
+
+
+    @property
+    def auxdir(self):
+        """directory relative to ROOT where auxiliary build tools are placed"""
+        return self["auxdir"]
+
+    @auxdir.setter
+    def auxdir(self, value):
+        self["auxdir"] = value
+
+
+    @property
+    def lib(self):
+        """library name; defaults to 'libgnu'"""
+        return self["lib"]
+
+    @lib.setter
+    def lib(self, value):
+        self["lib"] = value
+
+
+    @property
+    def makefile_name(self):
+        """name of makefile in automake syntax in the source-base and tests-base directories"""
+        return self["makefile_name"]
+
+    @makefile_name.setter
+    def makefile_name(self, value):
+        self["makefile_name"] = value
+
+
+    @property
+    def macro_prefix(self):
+        """
+        the prefix of the macros 'gl_EARLY' and 'gl_INIT' (default is 'gl');
+        the change of this parameter also affects include_guard_prefix parameter
+        """
+        return self["macro_prefix"]
+
+    @macro_prefix.setter
+    def macro_prefix(self, value):
+        self["macro_prefix"] = value
+
+
+    @property
+    def po_domain(self):
+        """the prefix of the i18n domain"""
+        return self["po_domain"]
+
+    @po_domain.setter
+    def po_domain(self, value):
+        self["po_domain"] = value
+
+
+    @property
+    def witness_c_macro(self):
+        """the C macro that is defined when the sources are compiled or used"""
+        return self["witness_c_macro"]
+
+    @witness_c_macro.setter
+    def witness_c_macro(self, value):
+        self["witness_c_macro"] = value
+
+
+    @property
+    def lgpl(self):
+        """abort if modules aren't available under the LGPL; also modify license template"""
+        return self["lgpl"]
+
+    @lgpl.setter
+    def lgpl(self, value):
+        if value is None:
+            value = 0
+        if value not in [0, 2, 3]:
+            raise TypeError("lgpl option must be either None or integral version (2 or 3)")
+        self["lgpl"] = value
+
+
+    @property
+    def tests(self):
+        """include unit tests for the included modules"""
+        return self["tests"]
+
+    @tests.setter
+    def tests(self, value):
+        self["tests"] = value
+
+
+    @property
+    def obsolete(self):
+        """include obsolete modules when they occur among the modules"""
+        return self["obsolete"]
+
+    @obsolete.setter
+    def obsolete(self, value):
+        self["obsolete"] = value
+
+
+    @property
+    def cxx_tests(self):
+        """include even unit tests for C++ interoperability"""
+        return self["cxx_tests"]
+
+    @cxx_tests.setter
+    def cxx_tests(self, value):
+        self["cxx_tests"] = value
+
+
+    @property
+    def longrunning_tests(self):
+        """include even unit tests that are long-runners"""
+        return self["longrunning_tests"]
+
+    @longrunning_tests.setter
+    def longrunning_tests(self, value):
+        self["longrunning_tests"] = value
+
+
+    @property
+    def privileged_tests(self):
+        """include even unit tests that require root privileges"""
+        return self["privileged_tests"]
+
+    @privileged_tests.setter
+    def privileged_tests(self, value):
+        self["privileged_tests"] = value
+
+
+    @property
+    def unportable_tests(self):
+        """include even unit tests that fail on some platforms"""
+        return self["unportable_tests"]
+
+    @unportable_tests.setter
+    def unportable_tests(self, value):
+        self["unportable_tests"] = value
+
+
+    @property
+    def all_tests(self):
+        """include all kinds of problematic unit tests"""
+        result = True
+        result &= self.tests
+        result &= self.cxx_tests
+        result &= self.privileged_tests
+        result &= self.unportable_tests
+        result &= self.longrunning_tests
+        return result
+
+    @all_tests.setter
+    def all_tests(self, value):
+        self.tests = value
+        self.cxx_tests = value
+        self.privileged_tests = value
+        self.unportable_tests = value
+        self.longrunning_tests = value
+
+
+    @property
+    def libtool(self):
+        """use libtool rules"""
+        return self["libtool"]
+
+    @libtool.setter
+    def libtool(self, value):
+        self["libtool"] = value
+
+
+    @property
+    def conddeps(self):
+        """support conditional dependencies (may save configure time and object code)"""
+        return self["conddeps"]
+
+    @conddeps.setter
+    def conddeps(self, value):
+        self["conddeps"] = value
+
+
+    @property
+    def vc_files(self):
+        """update version control related files"""
+        return self["vc_files"]
+
+    @vc_files.setter
+    def vc_files(self, value):
+        self["vc_files"] = value
+
+
+    @property
+    def autoconf(self):
+        """autoconf version"""
+        return self["autoconf"]
+
+    @autoconf.setter
+    def autoconf(self, value):
+        self["autoconf"] = value
+
+
+    @property
+    def modules(self):
+        """list of modules"""
+        return self["modules"]
+
+    @modules.setter
+    def modules(self, value):
+        self["modules"] = tuple(value)
+
+
+    @property
+    def avoid(self):
+        """list of modules to avoid"""
+        return self["avoid"]
+
+    @avoid.setter
+    def avoid(self, value):
+        self["avoid"] = tuple(value)
+
+
+    @property
+    def files(self):
+        """list of files to be processed"""
+        return self["files"]
+
+    @files.setter
+    def files(self, value):
+        self["files"] = list(value)
+
+
+    @property
+    def include_guard_prefix(self):
+        """include guard prefix"""
+        prefix = self["macro_prefix"].upper()
+        default = Config._TABLE_["macro_prefix"]
+        return "GL_%s" % prefix if prefix == default else "GL"
+
+
+    def __getitem__(self, key):
+        if key not in Config._TABLE_:
+            key = key.replace("-", "_")
+            if key not in Config._TABLE_:
+                raise KeyError("unsupported option: %r" % key)
+        return self._table_[key]
+
+
+    def __setitem__(self, key, value):
+        if key not in Config._TABLE_:
+            key = key.replace("_", "-")
+            if key not in Config._TABLE_:
+                raise KeyError("unsupported option: %r" % key)
+        key = key.replace("-", "_")
+        typeid = type(Config._TABLE_[key])
+        if key == "lgpl":
+            if value not in [None, 2, 3]:
+                raise TypeError("lgpl option must be either None or integral version (2 or 3)")
+        elif key == "autoconf":
+            if value < 2.59:
+                raise AutoconfVersionError(2.59)
+        elif not isinstance(value, typeid):
+            raise TypeError("%r option must be of %r type" % (key, typeid))
+        self._table_[key] = value
+
+
+    def items(self):
+        """a set-like object providing a view on configuration items"""
+        return self._table_.items()
+
+
+    def keys(self):
+        """a set-like object providing a view on configuration keys"""
+        return self._table_.keys()
+
+
+    def values(self):
+        """a set-like object providing a view on configuration values"""
+        return self._table_.values()
+
+
+class Cache(Config):
+    """gnulib cached configuration"""
+    _AUTOCONF_ = {
+        "autoconf" : re.compile(".*AC_PREREQ\\(\\[(.*?)\\]\\)", re.S | re.M),
+        "auxdir"   : re.compile("^AC_CONFIG_AUX_DIR\\(\\[(.*?)\\]\\)$", re.S | re.M),
+        "libtool"  : re.compile("A[CM]_PROG_LIBTOOL", re.S | re.M)
+    }
+    _GNULIB_CACHE_ = {
+        "libtool"           : (bool, "gl_LIBTOOL"),
+        "conddeps"          : (bool, "gl_CONDITIONAL_DEPENDENCIES"),
+        "vc_files"          : (bool, "gl_VC_FILES"),
+        "tests"             : (bool, "gl_WITH_TESTS"),
+        "obsolete"          : (bool, "gl_WITH_OBSOLETE"),
+        "cxx_tests"         : (bool, "gl_WITH_CXX_TESTS"),
+        "longrunning_tests" : (bool, "gl_WITH_LONGRUNNING_TESTS"),
+        "privileged_tests"  : (bool, "gl_WITH_PRIVILEGED_TESTS"),
+        "unportable_tests"  : (bool, "gl_WITH_UNPORTABLE_TESTS"),
+        "all_tests"         : (bool, "gl_WITH_ALL_TESTS"),
+        "local_dir"         : (str, "gl_LOCAL_DIR"),
+        "source_base"       : (str, "gl_SOURCE_BASE"),
+        "m4_base"           : (str, "gl_M4_BASE"),
+        "po_base"           : (str, "gl_PO_BASE"),
+        "doc_base"          : (str, "gl_DOC_BASE"),
+        "tests_base"        : (str, "gl_TESTS_BASE"),
+        "makefile_name"     : (str, "gl_MAKEFILE_NAME"),
+        "macro_prefix"      : (str, "gl_MACRO_PREFIX"),
+        "po_domain"         : (str, "gl_PO_DOMAIN"),
+        "witness_c_macro"   : (str, "gl_WITNESS_C_MACRO"),
+        "lib"               : (str, "gl_LIB"),
+        "modules"           : (list, "gl_MODULES"),
+        "avoid"             : (list, "gl_AVOID"),
+        "lgpl"              : (str, "gl_LGPL"),
+    }
+    _GNULIB_CACHE_BOOL_ = []
+    _GNULIB_CACHE_STR_ = []
+    _GNULIB_CACHE_LIST_ = []
+    for _key_, (_typeid_, _) in _GNULIB_CACHE_.items():
+        if _typeid_ is bool:
+            _GNULIB_CACHE_BOOL_ += [_key_]
+        elif _typeid_ is str:
+            _GNULIB_CACHE_STR_ += [_key_]
+        else:
+            _GNULIB_CACHE_LIST_ += [_key_]
+    _GNULIB_CACHE_PATTERN_ = re.compile("^(gl_.*?)\\(\\[(.*?)\\]\\)$", re.S | re.M)
+
+
+    def __init__(self, root, autoconf=None, **kwargs):
+        if not isinstance(root, str):
+            raise TypeError("root must be of 'str' type")
+        super().__init__(**kwargs)
+        self._autoconf_(root, autoconf)
+        self._gnulib_cache_(root)
+        self._gnulib_comp_(root)
+
+    def _autoconf_(self, root, autoconf):
+        if not autoconf:
+            autoconf = os.path.join(root, "configure.ac")
+            if not os.path.exists(autoconf):
+                autoconf = os.path.join(root, "configure.in")
+        if not os.path.isabs(autoconf):
+            autoconf = os.path.join(root, autoconf)
+        autoconf = os.path.normpath(autoconf)
+        with codecs.open(autoconf, "rb", "UTF-8") as stream:
+            data = stream.read()
+        for key, pattern in Cache._AUTOCONF_.items():
+            match = pattern.findall(data)
+            if not match:
+                continue
+            if key == "autoconf":
+                self["autoconf"] = sorted(set([float(_.strip()) for _ in match if _.strip()]))[-1]
+            else:
+                self[key] = match[-1]
+
+    def _gnulib_cache_(self, root):
+        m4base = self["m4-base"]
+        gnulib_cache = os.path.join(root, m4base, "gnulib-cache.m4")
+        if os.path.exists(gnulib_cache):
+            with codecs.open(gnulib_cache, "rb", "UTF-8") as stream:
+                data = stream.read()
+            for key in Cache._GNULIB_CACHE_BOOL_:
+                (_, macro) = Cache._GNULIB_CACHE_[key]
+                if key in data:
+                    self[key] = True
+            match = dict(Cache._GNULIB_CACHE_PATTERN_.findall(data))
+            for key in Cache._GNULIB_CACHE_STR_:
+                (_, macro) = Cache._GNULIB_CACHE_[key]
+                if macro in match:
+                    self[key] = match[macro].strip()
+            for key in Cache._GNULIB_CACHE_LIST_:
+                (_, macro) = Cache._GNULIB_CACHE_[key]
+                if macro in match:
+                    self[key] = [_.strip() for _ in match[macro].split("\n") if _.strip()]
+
+    def _gnulib_comp_(self, root):
+        m4base = self["m4-base"]
+        gnulib_comp = os.path.join(root, m4base, "gnulib-comp.m4")
+        if os.path.exists(gnulib_comp):
+            with codecs.open(gnulib_comp, "rb", "UTF-8") as stream:
+                data = stream.read()
+            regex = "AC_DEFUN\\(\\[%s_FILE_LIST\\], \\[(.*?)\\]\\)" % self["macro-prefix"]
+            pattern = re.compile(regex, re.S | re.M)
+            match = pattern.findall(data)
+            if match:
+                self.files = [_.strip() for _ in match[-1].split("\n") if _.strip()]
+
+
+
+class CommandLine(Config):
+    """gnulib-tool command line configuration"""
+    _LIST_ = (1 << 0)
+    _FIND_ = (1 << 1)
+    _IMPORT_ = (1 << 2)
+    _ADD_IMPORT_ = (1 << 3)
+    _REMOVE_IMPORT_ = (1 << 4)
+    _UPDATE_ = (1 << 5)
+    _TEST_DIRECTORY_ = (1 << 6)
+    _MEGA_TEST_DIRECTORY_ = (1 << 7)
+    _TEST_ = (1 << 8)
+    _MEGA_TEST_ = (1 << 9)
+    _COPY_FILE_ = (1 << 10)
+    _EXTRACT_DESCRIPTION_ = (1 << 11)
+    _EXTRACT_COMMENT_ = (1 << 12)
+    _EXTRACT_STATUS_ = (1 << 13)
+    _EXTRACT_NOTICE_ = (1 << 14)
+    _EXTRACT_APPLICABILITY_ = (1 << 15)
+    _EXTRACT_FILELIST_ = (1 << 16)
+    _EXTRACT_DEPENDENCIES_ = (1 << 17)
+    _EXTRACT_AUTOCONF_SNIPPET_ = (1 << 18)
+    _EXTRACT_AUTOMAKE_SNIPPET_ = (1 << 19)
+    _EXTRACT_INCLUDE_DIRECTIVE_ = (1 << 20)
+    _EXTRACT_LINK_DIRECTIVE_ = (1 << 21)
+    _EXTRACT_LICENSE_ = (1 << 22)
+    _EXTRACT_MAINTAINER_ = (1 << 23)
+    _EXTRACT_TESTS_MODULE_ = (1 << 24)
+    _ANY_IMPORT_ = _IMPORT_ | _ADD_IMPORT_ | _REMOVE_IMPORT_ | _UPDATE_
+    _ANY_TEST_ = _TEST_ | _MEGA_TEST_ | _TEST_DIRECTORY_ | _MEGA_TEST_DIRECTORY_
+    _ALL_ = _LIST_ | _FIND_ | _ANY_IMPORT_ | _ANY_TEST_
+    _MODES_ = (
+        (_LIST_, "list", ""),
+        (_FIND_, "find", "filename"),
+        (_IMPORT_, "import", "[module1 ... moduleN]"),
+        (_ADD_IMPORT_, "add-import", "[module1 ... moduleN]"),
+        (_REMOVE_IMPORT_, "remove-import", "[module1 ... moduleN]"),
+        (_UPDATE_, "update", ""),
+        (_TEST_DIRECTORY_, "testdir", "--dir=directory [module1 ... moduleN]"),
+        (_MEGA_TEST_DIRECTORY_, "megatestdir", "--dir=directory [module1 ... moduleN]"),
+        (_TEST_, "test", "--dir=directory [module1 ... moduleN]"),
+        (_MEGA_TEST_, "megatest", "--dir=directory [module1 ... moduleN]"),
+        (_EXTRACT_DESCRIPTION_, "extract-description", "module"),
+        (_EXTRACT_COMMENT_, "extract-comment", "module"),
+        (_EXTRACT_STATUS_, "extract-status", "module"),
+        (_EXTRACT_NOTICE_, "extract-notice", "module"),
+        (_EXTRACT_APPLICABILITY_, "extract-applicability", "module"),
+        (_EXTRACT_FILELIST_, "extract-filelist", "module"),
+        (_EXTRACT_DEPENDENCIES_, "extract-dependencies", "module"),
+        (_EXTRACT_AUTOCONF_SNIPPET_, "extract-autoconf-snippet", "module"),
+        (_EXTRACT_AUTOMAKE_SNIPPET_, "extract-automake-snippet", "module"),
+        (_EXTRACT_INCLUDE_DIRECTIVE_, "extract-include-directive", "module"),
+        (_EXTRACT_LINK_DIRECTIVE_, "extract-link-directive", "module"),
+        (_EXTRACT_LICENSE_, "extract-license", "module"),
+        (_EXTRACT_MAINTAINER_, "extract-maintainer", "module"),
+        (_EXTRACT_TESTS_MODULE_, "extract-tests-module", "module"),
+        (_COPY_FILE_, "copy", "file [destination]"),
+    )
+
+
+    class _AvoidAction_(argparse.Action):
+        def __call__(self, parser, namespace, value, option=None):
+            values = getattr(namespace, self.dest)
+            values += value
+
+
+    class _VerboseAction_(argparse.Action):
+        def __call__(self, parser, namespace, value, option=None):
+            value = getattr(namespace, self.dest)
+            verbose = option in ("-v", "--verbose")
+            value += +1 if verbose else -1
+            setattr(namespace, self.dest, value)
+
+
+    # section0: (section0_modes, (([section0_option0, section0_optionN], **section0_kwargs)))
+    # sectionN: (sectionN_modes, (([sectionN_option0, sectionN_optionN], **sectionN_kwargs)))
+    #
+    # for (name, flags, arguments) in sections:
+    #     for argument in arguments:
+    #         (options, kwargs) = argument
+    _SECTIONS_ = (
+        (
+            "Operation modes",
+            None,
+            (
+                (["-l", "--list"], {
+                    "help": (
+                        "print the available module names",
+                    ),
+                    "dest": "mode",
+                    "default": None,
+                    "action": "store_const",
+                    "const": _LIST_,
+                }),
+                (["-f", "--find"], {
+                    "help": (
+                        "find the modules which contain the specified file",
+                    ),
+                    "dest": "mode",
+                    "default": None,
+                    "action": "store_const",
+                    "const": _FIND_,
+                }),
+                (["-i", "--import"], {
+                    "help": (
+                        "import the given modules into the current package",
+                    ),
+                    "dest": "mode",
+                    "default": None,
+                    "action": "store_const",
+                    "const": _IMPORT_,
+                }),
+                (["-a", "--add-import"], {
+                    "help": (
+                        "augment the list of imports from gnulib into the",
+                        "current package, by adding the given modules;",
+                        "if no modules are specified, update the current",
+                        "package from the current gnulib",
+                    ),
+                    "dest": "mode",
+                    "default": None,
+                    "action": "store_const",
+                    "const": _ADD_IMPORT_,
+                }),
+                (["-r", "--remove-import"], {
+                    "help": (
+                        "reduce the list of imports from gnulib into the",
+                        "current package, by removing the given modules",
+                    ),
+                    "dest": "mode",
+                    "default": None,
+                    "action": "store_const",
+                    "const": _REMOVE_IMPORT_,
+                }),
+                (["-u", "--update"], {
+                    "help": (
+                        "update the current package, restore files omitted",
+                        "from version control",
+                    ),
+                    "dest": "mode",
+                    "default": None,
+                    "action": "store_const",
+                    "const": _UPDATE_,
+                }),
+
+                (["--extract-description"], {
+                    "help": (
+                        "extract the description",
+                    ),
+                    "dest": "mode",
+                    "default": None,
+                    "action": "store_const",
+                    "const": _EXTRACT_DESCRIPTION_,
+                }),
+                (["--extract-comment"], {
+                    "help": (
+                        "extract the comment",
+                    ),
+                    "dest": "mode",
+                    "default": None,
+                    "action": "store_const",
+                    "const": _EXTRACT_COMMENT_,
+                }),
+                (["--extract-status"], {
+                    "help": (
+                        "extract the status (obsolete etc.)",
+                    ),
+                    "dest": "mode",
+                    "default": None,
+                    "action": "store_const",
+                    "const": _EXTRACT_STATUS_,
+                }),
+                (["--extract-notice"], {
+                    "help": (
+                        "extract the notice or banner",
+                    ),
+                    "dest": "mode",
+                    "default": None,
+                    "action": "store_const",
+                    "const": _EXTRACT_NOTICE_,
+                }),
+                (["--extract-applicability"], {
+                    "help": (
+                        "extract the applicability",
+                    ),
+                    "dest": "mode",
+                    "default": None,
+                    "action": "store_const",
+                    "const": _EXTRACT_APPLICABILITY_,
+                }),
+                (["--extract-filelist"], {
+                    "help": (
+                        "extract the list of files",
+                    ),
+                    "dest": "mode",
+                    "default": None,
+                    "action": "store_const",
+                    "const": _EXTRACT_FILELIST_,
+                }),
+                (["--extract-dependencies"], {
+                    "help": (
+                        "extract the dependencies",
+                    ),
+                    "dest": "mode",
+                    "default": None,
+                    "action": "store_const",
+                    "const": _EXTRACT_DEPENDENCIES_,
+                }),
+                (["--extract-autoconf-snippet"], {
+                    "help": (
+                        "extract the snippet for configure.ac",
+                    ),
+                    "dest": "mode",
+                    "default": None,
+                    "action": "store_const",
+                    "const": _EXTRACT_AUTOCONF_SNIPPET_,
+                }),
+                (["--extract-automake-snippet"], {
+                    "help": (
+                        "extract the snippet for library makefile",
+                    ),
+                    "dest": "mode",
+                    "default": None,
+                    "action": "store_const",
+                    "const": _EXTRACT_AUTOMAKE_SNIPPET_,
+                }),
+                (["--extract-include-directive"], {
+                    "help": (
+                        "extract the #include directive",
+                    ),
+                    "dest": "mode",
+                    "default": None,
+                    "action": "store_const",
+                    "const": _EXTRACT_INCLUDE_DIRECTIVE_,
+                }),
+                (["--extract-link-directive"], {
+                    "help": (
+                        "extract the linker directive",
+                    ),
+                    "dest": "mode",
+                    "default": None,
+                    "action": "store_const",
+                    "const": _EXTRACT_LINK_DIRECTIVE_,
+                }),
+                (["--extract-license"], {
+                    "help": (
+                        "report the license terms of the source files",
+                        "under lib/",
+                    ),
+                    "dest": "mode",
+                    "default": None,
+                    "action": "store_const",
+                    "const": _EXTRACT_LICENSE_,
+                }),
+                (["--extract-maintainer"], {
+                    "help": (
+                        "report the maintainer(s) inside gnulib",
+                    ),
+                    "dest": "mode",
+                    "default": None,
+                    "action": "store_const",
+                    "const": _EXTRACT_MAINTAINER_,
+                }),
+                (["--extract-tests-module"], {
+                    "help": (
+                        "report the unit test module, if it exists",
+                    ),
+                    "dest": "mode",
+                    "default": None,
+                    "action": "store_const",
+                    "const": _EXTRACT_TESTS_MODULE_,
+                }),
+
+                (["--copy-file"], {
+                    "help": (
+                        "copy a file that is not part of any module",
+                    ),
+                    "dest": "mode",
+                    "default": None,
+                    "action": "store_const",
+                    "const": _COPY_FILE_,
+                }),
+            ),
+        ),
+
+
+        (
+            "General options",
+            _ALL_,
+            (
+                (["--dir"], {
+                    "help": (
+                        "specify the target directory; on --import,",
+                        "this specifies where your configure.ac",
+                        "can be found",
+                    ),
+                    "dest": "root",
+                    "default": ".",
+                    "metavar": "DIRECTORY",
+                }),
+                (["--local-dir"], {
+                    "help": (
+                        "pecify a local override directory where to look",
+                        "up files before looking in gnulib's directory",
+                    ),
+                    "dest": "local_dir",
+                    "default": ".",
+                    "nargs": 1,
+                    "metavar": "DIRECTORY",
+                }),
+                (["-v", "--verbose"], {
+                    "help": (
+                        "increase verbosity; may be repeated",
+                    ),
+                    "dest": "verbosity",
+                    "action": _VerboseAction_,
+                    "default": 0,
+                    "nargs": 0,
+                }),
+                (["-q", "--quiet"], {
+                    "help": (
+                        "decrease verbosity; may be repeated",
+                    ),
+                    "dest": "verbosity",
+                    "action": _VerboseAction_,
+                    "default": 0,
+                    "nargs": 0,
+                }),
+            ),
+        ),
+
+
+        (
+            "Options for --import, --add/remove-import,\n"
+            "            --create-[mega]testdir, --[mega]test",
+            (_ANY_IMPORT_ & ~_UPDATE_) | _ANY_TEST_,
+            (
+                (["--with-tests"], {
+                    "help": (
+                        "include unit tests for the included modules",
+                    ),
+                    "dest": "tests",
+                    "action": "store_true",
+                    "default": False,
+                }),
+                (["--with-obsolete"], {
+                    "help": (
+                        "include obsolete modules when they occur among the",
+                        "dependencies; by default, dependencies to obsolete",
+                        "modules are ignored",
+                    ),
+                    "dest": "obsolete",
+                    "action": "store_true",
+                    "default": False,
+                }),
+                (["--with-c++-tests"], {
+                    "help": (
+                        "include even unit tests for C++ interoperability",
+                    ),
+                    "dest": "cxx_tests",
+                    "action": "store_true",
+                    "default": False,
+                }),
+                (["--with-longrunning-tests"], {
+                    "help": (
+                        "include even unit tests that are long-runners",
+                    ),
+                    "dest": "longrunning_tests",
+                    "action": "store_true",
+                    "default": False,
+                }),
+                (["--with-privileged-tests"], {
+                    "help": (
+                        "include even unit tests that require root",
+                        "privileges",
+                    ),
+                    "dest": "privileged_tests",
+                    "action": "store_true",
+                    "default": False,
+                }),
+                (["--with-unportable-tests"], {
+                    "help": (
+                        "include even unit tests that fail on some platforms",
+                    ),
+                    "dest": "unportable_tests",
+                    "action": "store_true",
+                    "default": False,
+                }),
+                (["--with-all-tests"], {
+                    "help": (
+                        "include all kinds of problematic unit tests",
+                    ),
+                    "dest": "all_tests",
+                    "action": "store_true",
+                    "default": False,
+                }),
+                (["--avoid"], {
+                    "help": (
+                        "avoid including the given MODULE; useful if you",
+                        "have code that provides equivalent functionality",
+                        "this option can be repeated",
+                    ),
+                    "action": _AvoidAction_,
+                    "nargs": 1,
+                    "default": [],
+                    "metavar": "MODULE",
+                }),
+                (["--conditional-dependencies"], {
+                    "help": (
+                        "support conditional dependencies (may save configure",
+                        "time and object code)",
+                    ),
+                    "action": "store_true",
+                    "default": False,
+                    "dest": "conddeps",
+                }),
+                (["--no-conditional-dependencies"], {
+                    "help": (
+                        "don't use conditional dependencies",
+                    ),
+                    "action": "store_false",
+                    "default": False,
+                    "dest": "conddeps",
+                }),
+                (["--libtool"], {
+                    "help": (
+                        "use libtool rules",
+                    ),
+                    "action": "store_true",
+                    "default": False,
+                    "dest": "libtool",
+                }),
+                (["--no-libtool"], {
+                    "help": (
+                        "don't use libtool rules",
+                    ),
+                    "action": "store_false",
+                    "default": False,
+                    "dest": "libtool",
+                }),
+            ),
+        ),
+
+
+        (
+            "Options for --import, --add/remove-import",
+            (_ANY_IMPORT_ & ~_UPDATE_),
+            (
+                (["--lib"], {
+                    "help": (
+                        "specify the library name; defaults to 'libgnu'",
+                    ),
+                    "default": "libgnu",
+                    "metavar": "LIBRARY",
+                }),
+                (["--source-base"], {
+                    "help": (
+                        "directory relative to --dir where source code is",
+                        "placed (default \"lib\")",
+                    ),
+                    "default": "lib",
+                    "metavar": "DIRECTORY",
+                }),
+                (["--m4-base"], {
+                    "help": (
+                        "directory relative to --dir where *.m4 macros are",
+                        "placed (default \"m4\")",
+                    ),
+                    "default": "m4",
+                    "metavar": "DIRECTORY",
+                }),
+                (["--po-base"], {
+                    "help": (
+                        "directory relative to --dir where *.po files are",
+                        "placed (default \"po\")",
+                    ),
+                    "default": "po",
+                    "metavar": "DIRECTORY",
+                }),
+                (["--doc-base"], {
+                    "help": (
+                        "directory relative to --dir where doc files are",
+                        "placed (default \"doc\")",
+                    ),
+                    "default": "doc",
+                    "metavar": "DIRECTORY",
+                }),
+                (["--tests-base"], {
+                    "help": (
+                        "directory relative to --dir where unit tests are",
+                        "placed (default \"tests\")",
+                    ),
+                    "default": "tests",
+                    "metavar": "DIRECTORY",
+                }),
+                (["--aux-dir"], {
+                    "help": (
+                        "directory relative to --dir where auxiliary build",
+                        "tools are placed (default comes from configure.ac);",
+                    ),
+                    "dest": "auxdir",
+                    "default": "",
+                    "metavar": "DIRECTORY",
+                }),
+                (["--lgpl"], {
+                    "help": (
+                        "abort if modules aren't available under the LGPL;",
+                        "also modify license template from GPL to LGPL;",
+                        "the version number of the LGPL can be specified;",
+                        "the default is currently LGPLv3.",
+                    ),
+                    "choices": (2, 3),
+                    "type": int,
+                    "metavar": "[=2|=3]",
+                }),
+                (["--makefile-name"], {
+                    "help": (
+                        "name of makefile in automake syntax in the",
+                        "source-base and tests-base directories",
+                        "(default \"Makefile.am\")",
+                    ),
+                    "default": "Makefile.am",
+                    "metavar": "NAME",
+                }),
+                (["--macro-prefix"], {
+                    "help": (
+                        "specify the prefix of the macros 'gl_EARLY' and",
+                        "'gl_INIT'; default is 'gl'",
+                    ),
+                    "default": "gl",
+                    "metavar": "PREFIX",
+                }),
+                (["--po-domain"], {
+                    "help": (
+                        "specify the prefix of the i18n domain; usually use",
+                        "the package name; a suffix '-gnulib' is appended",
+                    ),
+                    "default": "",
+                    "metavar": "NAME",
+                }),
+                (["--witness-c-macro"], {
+                    "help": (
+                        "specify the C macro that is defined when the",
+                        "sources in this directory are compiled or used",
+                    ),
+                    "default": "",
+                    "metavar": "NAME",
+                }),
+                (["--vc-files"], {
+                    "help": (
+                        "update version control related files",
+                        "(.gitignore and/or .cvsignore)",
+                    ),
+                    "action": "store_true",
+                    "default": False,
+                    "dest": "vc_files",
+                }),
+                (["--no-vc-files"], {
+                    "help": (
+                        "don't update version control related files",
+                        "(.gitignore and/or .cvsignore)",
+                    ),
+                    "action": "store_false",
+                    "default": False,
+                    "dest": "libtool",
+                }),
+                (["--no-changelog"], {
+                    "help": (
+                        "don't update or create ChangeLog files;",
+                        "this option is currently deprecated",
+                    ),
+                    "default": None,
+                    "action": "store_const",
+                    "const": None,
+                }),
+            ),
+        ),
+    )
+
+
+    def _usage_(self):
+        iterable = iter(CommandLine._MODES_)
+        (_, cmd, args) = next(iterable)
+        fmt = (" --{cmd}" + (" {args}" if args else ""))
+        lines = ["usage: {program}" + fmt.format(cmd=cmd, args=args)]
+        for (_, cmd, args) in iterable:
+            fmt = (" --{cmd}" + (" {args}" if args else ""))
+            lines += ["       {program}" + fmt.format(cmd=cmd, args=args)]
+        lines += ["", ""]
+        return "\n".join(lines).format(program=self._program_)
+
+
+    def _help_(self):
+        lines = [""]
+        for (name, _, args) in CommandLine._SECTIONS_:
+            offset = -1
+            lines += ["", "%s:" % name, ""]
+            for arg in args:
+                (options, kwargs) = arg
+                options = ", ".join(options)
+                if "metavar" in kwargs:
+                    options += (" " + kwargs["metavar"])
+                length = len(options)
+                if length > offset:
+                    offset = length
+            fmt1 = "      %-{0}s    %s".format(offset)
+            fmt2 = "      " + " " * offset + "    %s"
+            for arg in args:
+                (options, kwargs) = arg
+                options = ", ".join(options)
+                if "metavar" in kwargs:
+                    sep = "" if "choices" in kwargs else "="
+                    options += ("%s%s" % (sep, kwargs["metavar"]))
+                description = iter(kwargs["help"])
+                line = next(description)
+                lines += [fmt1 % (options, line)]
+                for line in description:
+                    lines += [fmt2 % line]
+            lines += [""]
+        return self._usage_()[:-1] + "\n".join(lines)
+
+
+    def __init__(self, program, argv, **kwargs):
+        super().__init__(**kwargs)
+
+        exclude = {}
+        parser = argparse.ArgumentParser(prog=program, allow_abbrev=False)
+        modes = parser.add_mutually_exclusive_group()
+        for (options, kwargs) in CommandLine._SECTIONS_[0][2]:
+            for (flag, cmd, _) in CommandLine._MODES_:
+                if "--%s" % cmd not in options:
+                    continue
+                exclude[flag] = [options, []]
+                modes.add_argument(*options, **kwargs)
+                for (_, flags, xargs) in CommandLine._SECTIONS_[1:]:
+                    if not flags & flag:
+                        for xarg in xargs:
+                            (options, kwargs) = xarg
+                            exclude[flag][1] += options
+                break
+
+        for (_, _, args) in CommandLine._SECTIONS_[1:]:
+            for arg in args:
+                (options, kwargs) = arg
+                parser.add_argument(*options, **kwargs)
+        parser.add_argument("modules", nargs="+", metavar="module1 ... moduleN")
+
+        self._program_ = os.path.basename(program)
+        parser.format_usage = self._usage_
+        parser.format_help = self._help_
+
+        namespace = vars(parser.parse_args(argv))
+        self._mode_ = namespace.pop("mode")
+        if self._mode_ is None:
+            parser.error("no operating mode selected")
+        self._verbosity_ = namespace.pop("verbosity")
+
+        (mode, conflict) = exclude[self._mode_]
+        conflict = set(conflict).intersection(argv)
+        if conflict:
+            conflict = list(conflict)[0]
+            fmt = "argument {0}: not allowed with argument {1}"
+            parser.error(fmt.format(conflict, "/".join(mode)))
+        _ = namespace.pop("no_changelog", None)
+        for (key, value) in namespace.items():
+            self[key] = value
+
+
+    @property
+    def mode(self):
+        for (flag, cmd, _) in CommandLine._MODES_:
+            if flag == self._mode_:
+                return cmd
+        return ""
diff --git a/pygnulib/error.py b/pygnulib/error.py
new file mode 100644 (file)
index 0000000..d618df0
--- /dev/null
@@ -0,0 +1,54 @@
+#!/usr/bin/python
+# encoding: UTF-8
+
+
+
+class AutoconfVersionError(Exception):
+    """minimum supported autoconf version mismatch"""
+    def __init__(self, version):
+        fmt = "minimum supported autoconf version is %f"
+        message = fmt % version
+        super().__init__(message)
+
+
+
+class M4BaseMismatchError(Exception):
+    """<gnulib-comp.m4> is expected to contain gl_M4_BASE([m4base])"""
+    def __init__(self, m4_base):
+        fmt = "<gnulib-comp.m4> is expected to contain gl_M4_BASE([%s])"
+        message = fmt % m4_base
+        super().__init__(message)
+
+
+
+class ConditionalDependenciesUnavailableError(Exception):
+    """conditional dependencies are not supported with tests"""
+    def __init__(self):
+        message = "conditional dependencies are not supported with tests"
+        super().__init__(message)
+
+
+
+class IncompatibleLicenseError(Exception):
+    """incompatible licenses on modules"""
+    def __init__(self, modules):
+        fmt = "incompatible licenses on modules: %r"
+        message = fmt % modules
+        super().__init__(message)
+
+
+
+class EmptyFileListError(Exception):
+    """cannot process empty file list"""
+    def __init__(self):
+        message = "cannot process empty file list"
+        super().__init__(message)
+
+
+
+class UnknownLicenseError(Exception):
+    """module lacks a license"""
+    def __init__(self, module):
+        fmt = "module lacks a license: %r"
+        message = fmt % module
+        super().__init__(message)
diff --git a/pygnulib/filesystem.py b/pygnulib/filesystem.py
new file mode 100644 (file)
index 0000000..3d5721f
--- /dev/null
@@ -0,0 +1,114 @@
+#!/usr/bin/python
+# encoding: UTF-8
+
+
+
+import os
+
+from .config import Config
+from .module import Module
+from .module import FileModule
+
+
+
+class Directory:
+    """gnulib generic virtual file system"""
+    _SUBST_ = {
+        "build-aux" : "aux-dir",
+        "doc"       : "doc-base",
+        "lib"       : "source-base",
+        "m4"        : "m4-base",
+        "tests"     : "tests-base",
+        "tests=lib" : "tests-base",
+        "po"        : "po-base",
+    }
+
+
+    def __init__(self, root, config):
+        if not isinstance(root, str):
+            raise TypeError("root must be of 'str' type")
+        if not isinstance(config, Config):
+            raise TypeError("config must be of 'Config' type")
+        if not os.path.exists(root):
+            raise FileNotFoundError(root)
+        if not os.path.isdir(root):
+            raise NotADirectoryError(root)
+        self._config_ = config
+        self._root_ = os.path.realpath(root)
+
+
+    def __getitem__(self, name):
+        """retrieve the canonical path of the specified file name"""
+        parts = []
+        replaced = False
+        if not isinstance(name, str):
+            raise TypeError("name must be of 'str' type")
+        path = os.path.normpath(name)
+        if os.path.isabs(path):
+            raise ValueError("name must be a relative path")
+        for part in path.split(os.path.sep):
+            if part == "..":
+                parts += [part]
+                continue
+            if not replaced:
+                for old, new in Directory._SUBST_.items():
+                    if part == old:
+                        part = self._config_[new]
+                        replaced = True
+            parts += [part]
+        path = os.path.sep.join([self._root_] + parts)
+        if not os.path.exists(path):
+            raise FileNotFoundError(name)
+        return path
+
+
+
+class Git(Directory):
+    """gnulib Git-based virtual file system"""
+    _EXCLUDE_ = {
+        "."                 : str.startswith,
+        "~"                 : str.endswith,
+        "-tests"            : str.endswith,
+        "CVS"               : str.startswith,
+        "ChangeLog"         : str.__eq__,
+        "COPYING"           : str.__eq__,
+        "README"            : str.__eq__,
+        "TEMPLATE"          : str.__eq__,
+        "TEMPLATE-TESTS"    : str.__eq__,
+        "TEMPLATE-EXTENDED" : str.__eq__,
+    }
+
+
+    def __init__(self, root, config):
+        if not os.path.isdir(root):
+            raise FileNotFoundError(root)
+        super().__init__(root, config)
+        if not os.path.isdir(os.path.join(self._root_, ".git")):
+            raise TypeError("%r is not a gnulib repository")
+
+
+    def module(self, name, full=True):
+        """instantiate gnulib module by its name"""
+        if name in Git._EXCLUDE_:
+            raise ValueError("illegal module name")
+        path = os.path.join(self["modules"], name)
+        return FileModule(path, name=name) if full else Module(name)
+
+
+    def modules(self, full=True):
+        """iterate over all available modules"""
+        prefix = self["modules"]
+        for root, _, files in os.walk(prefix):
+            names = []
+            for name in files:
+                exclude = False
+                for key, method in Git._EXCLUDE_.items():
+                    if method(name, key):
+                        exclude = True
+                        break
+                if not exclude:
+                    names += [name]
+            for name in names:
+                path = os.path.join(root, name)
+                name = path[len(prefix) + 1:]
+                yield self.module(name, full)
diff --git a/pygnulib/generator.py b/pygnulib/generator.py
new file mode 100644 (file)
index 0000000..fef2b49
--- /dev/null
@@ -0,0 +1,442 @@
+#!/usr/bin/python
+# encoding: UTF-8
+
+
+
+import os
+
+from .config import Config
+from .module import Module
+
+
+
+class Generator:
+    """gnulib file content generator"""
+    _TEMPLATE_ = (
+        "## DO NOT EDIT! GENERATED AUTOMATICALLY!",
+        "#",
+        "# This file is free software; you can redistribute it and/or modify",
+        "# it under the terms of the GNU General Public License as published by",
+        "# the Free Software Foundation; either version 3 of the License, or",
+        "# (at your option) any later version.",
+        "#",
+        "# This file is distributed in the hope that it will be useful,",
+        "# but WITHOUT ANY WARRANTY; without even the implied warranty of",
+        "# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the",
+        "# GNU General Public License for more details.",
+        "#",
+        "# You should have received a copy of the GNU General Public License",
+        "# along with this file.  If not, see <http://www.gnu.org/licenses/>.",
+        "#",
+        "# As a special exception to the GNU General Public License,",
+        "# this file may be distributed as part of a program that",
+        "# contains a configuration script generated by Autoconf, under",
+        "# the same distribution terms as the rest of that program.",
+        "#",
+        "# Generated by gnulib-tool.",
+    )
+
+    def __repr__(self):
+        return "pygnulib.generator.Generator"
+
+    def __str__(self):
+        return "\n".join([_ for _ in self])
+
+    def __iter__(self):
+        for line in Generator._TEMPLATE_:
+            yield line
+
+
+
+class POMakefile(Generator):
+    """PO Makefile parameterization"""
+    _TEMPLATE_ = (
+        "# These options get passed to xgettext.",
+        "XGETTEXT_OPTIONS = \\",
+        "  --keyword=_ --flag=_:1:pass-c-format \\",
+        "  --keyword=N_ --flag=N_:1:pass-c-format \\",
+        "  --keyword='proper_name:1,\"This is a proper name." # comma omitted
+        " See the gettext manual, section Names.\"' \\",
+        "  --keyword='proper_name_utf8:1,\"This is a proper name." # comma omitted
+        " See the gettext manual, section Names.\"' \\",
+        "  --flag=error:3:c-format --flag=error_at_line:5:c-format",
+        "",
+        "# This is the copyright holder that gets inserted into the header of the",
+        "# $(DOMAIN).pot file.  gnulib is copyrighted by the FSF.",
+        "COPYRIGHT_HOLDER = Free Software Foundation, Inc.",
+        "",
+        "# This is the email address or URL to which the translators shall report",
+        "# bugs in the untranslated strings:",
+        "# - Strings which are not entire sentences, see the maintainer guidelines",
+        "#   in the GNU gettext documentation, section 'Preparing Strings'.",
+        "# - Strings which use unclear terms or require additional context to be",
+        "#   understood.",
+        "# - Strings which make invalid assumptions about notation of date, time or",
+        "#   money.",
+        "# - Pluralisation problems.",
+        "# - Incorrect English spelling.",
+        "# - Incorrect formatting.",
+        "# It can be your email address, or a mailing list address where translators",
+        "# can write to without being subscribed, or the URL of a web page through",
+        "# which the translators can contact you.",
+        "MSGID_BUGS_ADDRESS = bug-gnulib@gnu.org",
+        "",
+        "# This is the list of locale categories, beyond LC_MESSAGES, for which the",
+        "# message catalogs shall be used.  It is usually empty.",
+        "EXTRA_LOCALE_CATEGORIES =",
+        "",
+        "# This tells whether the $(DOMAIN).pot file contains messages with an 'msgctxt'",
+        "# context.  Possible values are \"yes\" and \"no\".  Set this to yes if the",
+        "# package uses functions taking also a message context, like pgettext(), or",
+        "# if in $(XGETTEXT_OPTIONS) you define keywords with a context argument.",
+        "USE_MSGCTXT = no"
+    )
+    def __init__(self, config):
+        if not isinstance(config, Config):
+            raise TypeError("config must be of pygnulib.Config type")
+        self._config_ = config
+        super().__init__()
+
+
+    @property
+    def po_base(self):
+        """directory relative to ROOT where *.po files are placed; defaults to 'po'"""
+        return self._config_.po_base
+
+
+    @property
+    def po_domain(self):
+        """the prefix of the i18n domain"""
+        return self._config_.po_domain
+
+
+    def __repr__(self):
+        fmt = "pygnulib.generator.POMakefile(po_base=%r, po_domain=%r)"
+        return fmt % (self.po_base, self.po_domain)
+
+
+    def __iter__(self):
+        for line in super().__iter__():
+            yield line
+        yield "# Usually the message domain is the same as the package name."
+        yield "# But here it has a '-gnulib' suffix."
+        yield "DOMAIN = %s-gnulib" % self.po_domain
+        yield ""
+        yield "# These two variables depend on the location of this directory."
+        yield "subdir = %s" % self.po_domain
+        yield "top_subdir = %s" % "/".join([".." for _ in self.po_base.split(os.path.sep)])
+        for line in POMakefile._TEMPLATE_:
+            yield line
+
+
+
+class POTFILES(Generator):
+    """file list to be passed to xgettext"""
+    def __init__(self, config, files):
+        if not isinstance(config, Config):
+            raise TypeError("config must be of pygnulib.Config type")
+        super().__init__()
+        self._config_ = config
+        self._files_ = tuple(files)
+
+
+    @property
+    def files(self):
+        """list of files"""
+        return tuple(self.files)
+
+
+    def __repr__(self):
+        fmt = "pygnulib.generator.POTFILES(files=%r)"
+        return fmt % self.files
+
+
+    def __iter__(self):
+        for line in super().__iter__():
+            yield line
+        yield "# List of files which contain translatable strings."
+        for file in [_ for _ in self.files if _.startswith("lib/")]:
+            yield os.path.join(self._config_.source_base, file[4:])
+
+
+
+class AutoconfSnippet(Generator):
+    """autoconf snippet generator for standalone module"""
+    def __init__(self, config, module, toplevel, no_libtool, no_gettext):
+        """
+        config: gnulib configuration
+        module: gnulib module instance
+        toplevel: make a subordinate use of gnulib if False
+        no_libtool: disable libtool (regardless of configuration)
+        no_gettext: disable AM_GNU_GETTEXT invocations if True
+        """
+        if not isinstance(config, Config):
+            raise TypeError("config must be of pygnulib.config.Config type")
+        if not isinstance(module, Module):
+            raise TypeError("module must be of pygnulib.module.Module type")
+        if not isinstance(toplevel, bool):
+            raise TypeError("toplevel must be of bool type")
+        if not isinstance(no_libtool, bool):
+            raise TypeError("no_libtool must be of bool type")
+        if not isinstance(no_gettext, bool):
+            raise TypeError("no_gettext must be of bool type")
+        super().__init__()
+        self._config_ = config
+        self._module_ = module
+        self._toplevel_ = toplevel
+        self._no_libtool_ = no_libtool
+        self._no_gettext_ = no_gettext
+
+
+    @property
+    def toplevel(self):
+        """top level indicator; subordinate use of pygnulib"""
+        return self._toplevel_
+
+
+    @property
+    def libtool(self):
+        """libtool switch, disabling libtool configuration parameter"""
+        return self._config_.libtool and not self._no_libtool_
+
+
+    @property
+    def gettext(self):
+        """gettext switch, disabling AM_GNU_GETTEXT invocations"""
+        return not self._no_gettext_
+
+
+    def __repr__(self):
+        flags = []
+        if self.toplevel:
+            flags += ["toplevel"]
+        if self.libtool:
+            flags += ["libtool"]
+        if self.gettext:
+            flags += ["gettext"]
+        fmt = "pygnulib.generator.AutoconfSnippet(include_guard_prefix=%r, flags=%s)"
+        include_guard_prefix = self._config_.include_guard_prefix
+        return fmt % (include_guard_prefix, "|".join(flags))
+
+
+    def __iter__(self):
+        module = self._module_
+        if module.name not in ("gnumakefile", "maintainer-makefile") or self.toplevel:
+            snippet = module.configure_ac_snippet
+            include_guard_prefix = self._config_.include_guard_prefix
+            snippet.replace(r"${gl_include_guard_prefix}", include_guard_prefix)
+            if not self.libtool:
+                table = (
+                    (r"$gl_cond_libtool", "false"),
+                    ("gl_libdeps", "gltests_libdeps"),
+                    ("gl_ltlibdeps", "gltests_ltlibdeps"),
+                )
+                for (src, dst) in table:
+                    snippet = snippet.replace(src, dst)
+            if not self.gettext:
+                src = "AM_GNU_GETTEXT([external])"
+                dst = "dnl you must add AM_GNU_GETTEXT([external]) or similar to configure.ac.'"
+                snippet = snippet.replace(src, dst)
+            lines = (_ for _ in snippet.split("\n") if _)
+            for line in lines:
+                yield line
+            if module.name == "alloca" and self.libtool:
+                yield "changequote(,)dnl"
+                yield "LTALLOCA=`echo \"$ALLOCA\" | sed -e 's/\\.[^.]* /.lo /g;s/\\.[^.]*$/.lo/'`"
+                yield "changequote([, ])dnl"
+                yield "AC_SUBST([LTALLOCA])"
+
+
+
+class InitMacro(Generator):
+    """basic gl_INIT macro generator"""
+    def __init__(self, config, macro_prefix=None):
+        """
+        config: gnulib configuration
+        macro_prefix: macro prefix; if None, consider configuration
+        """
+        if not isinstance(config, Config):
+            raise TypeError("config must be of pygnulib.config.Config type")
+        if macro_prefix is None:
+            macro_prefix = config.macro_prefix
+        if not isinstance(macro_prefix, str):
+            raise TypeError("macro_prefix must be of str type")
+        self._macro_prefix_ = macro_prefix
+
+
+    @property
+    def macro_prefix(self):
+        """the prefix of the macros 'gl_EARLY' and 'gl_INIT'"""
+        return self._macro_prefix_
+
+
+    def __repr__(self):
+        fmt = "pygnulib.generator.InitMacro(macro_prefix=%r)"
+        return fmt % self.macro_prefix
+
+
+
+class InitMacroHeader(InitMacro):
+    """the first few statements of the gl_INIT macro"""
+    def __init__(self, config, macro_prefix=None):
+        """
+        config: gnulib configuration
+        macro_prefix: macro prefix; if None, consider configuration
+        """
+        super().__init__(config=config, macro_prefix=macro_prefix)
+
+
+    def __repr__(self):
+        fmt = "pygnulib.generator.InitMacroHeader(macro_prefix=%r)"
+        return fmt % self.macro_prefix
+
+
+    def __iter__(self):
+        # Overriding AC_LIBOBJ and AC_REPLACE_FUNCS has the effect of storing
+        # platform-dependent object files in ${macro_prefix_arg}_LIBOBJS instead
+        # of LIBOBJS. The purpose is to allow several gnulib instantiations under
+        # a single configure.ac file. (AC_CONFIG_LIBOBJ_DIR does not allow this
+        # flexibility).
+        # Furthermore it avoids an automake error like this when a Makefile.am
+        # that uses pieces of gnulib also uses $(LIBOBJ):
+        #   automatically discovered file `error.c' should not be explicitly
+        #   mentioned.
+        yield "  m4_pushdef([AC_LIBOBJ], m4_defn([%s_LIBOBJ]))" % self.macro_prefix
+        yield "  m4_pushdef([AC_REPLACE_FUNCS], m4_defn([%s_REPLACE_FUNCS]))" % self.macro_prefix
+
+        # Overriding AC_LIBSOURCES has the same purpose of avoiding the automake
+        # error when a Makefile.am that uses pieces of gnulib also uses $(LIBOBJ):
+        #   automatically discovered file `error.c' should not be explicitly
+        #   mentioned
+        # We let automake know about the files to be distributed through the
+        # EXTRA_lib_SOURCES variable.
+        yield "  m4_pushdef([AC_LIBSOURCES], m4_defn([%s_LIBSOURCES]))" % self.macro_prefix
+
+        # Create data variables for checking the presence of files that are
+        # mentioned as AC_LIBSOURCES arguments. These are m4 variables, not shell
+        # variables, because we want the check to happen when the configure file is
+        # created, not when it is run. ${macro_prefix_arg}_LIBSOURCES_LIST is the
+        # list of files to check for. ${macro_prefix_arg}_LIBSOURCES_DIR is the
+        # subdirectory in which to expect them.
+        yield "  m4_pushdef([%s_LIBSOURCES_LIST], [])" % self.macro_prefix
+        yield "  m4_pushdef([%s_LIBSOURCES_DIR], [])" % self.macro_prefix
+        yield "  gl_COMMON"
+
+
+
+class InitMacroFooter(InitMacro):
+    """the last few statements of the gl_INIT macro"""
+    _TEMPLATE_ = (
+        "  m4_ifval({macro_prefix}_LIBSOURCES_LIST, [",
+        "    m4_syscmd([test ! -d ]m4_defn([{macro_prefix}_LIBSOURCES_DIR])[ ||",
+        "      for gl_file in ]{macro_prefix}_LIBSOURCES_LIST[ ; do",
+        "        if test ! -r ]m4_defn([{macro_prefix}_LIBSOURCES_DIR])[/$gl_file ; then",
+        "          echo \"missing file ]m4_defn([{macro_prefix}_LIBSOURCES_DIR])[/$gl_file\" >&2",
+        "          exit 1",
+        "        fi",
+        "      done])dnl",
+        "      m4_if(m4_sysval, [0], [],",
+        "        [AC_FATAL([expected source file, required through AC_LIBSOURCES, not found])])",
+        "  ])",
+        "  m4_popdef([{macro_prefix}_LIBSOURCES_DIR])",
+        "  m4_popdef([{macro_prefix}_LIBSOURCES_LIST])",
+        "  m4_popdef([AC_LIBSOURCES])",
+        "  m4_popdef([AC_REPLACE_FUNCS])",
+        "  m4_popdef([AC_LIBOBJ])",
+        "  AC_CONFIG_COMMANDS_PRE([",
+        "    {macro_prefix}_libobjs=",
+        "    {macro_prefix}_ltlibobjs=",
+        "    if test -n \"${macro_prefix}_LIBOBJS\"; then",
+        "      # Remove the extension.",
+        "      sed_drop_objext='s/\\.o$//;s/\\.obj$//'",
+        "      for i in `for i in ${macro_prefix}_LIBOBJS; " # comma omitted
+        "do echo \"$i\"; done | sed -e \"$sed_drop_objext\" | sort | uniq`; do",
+        "        {macro_prefix}_libobjs=\"${macro_prefix}_libobjs $i.$ac_objext\"",
+        "        {macro_prefix}_ltlibobjs=\"${macro_prefix}_ltlibobjs $i.lo\"",
+        "      done",
+        "    fi",
+        "    AC_SUBST([{macro_prefix}_LIBOBJS], [${macro_prefix}_libobjs])",
+        "    AC_SUBST([{macro_prefix}_LTLIBOBJS], [${macro_prefix}_ltlibobjs])",
+        "  ])",
+    )
+
+
+    def __init__(self, config, macro_prefix=None):
+        """
+        config: gnulib configuration
+        macro_prefix: macro prefix; if None, consider configuration
+        """
+        super().__init__(config=config, macro_prefix=macro_prefix)
+
+
+    def __repr__(self):
+        fmt = "pygnulib.generator.InitMacroFooter(macro_prefix=%r)"
+        return fmt % self.macro_prefix
+
+
+    def __iter__(self):
+        # Check the presence of files that are mentioned as AC_LIBSOURCES
+        # arguments. The check is performed only when autoconf is run from the
+        # directory where the configure.ac resides; if it is run from a different
+        # directory, the check is skipped.
+        for line in InitMacroFooter._TEMPLATE_:
+            yield line.format(macro_prefix=self.macro_prefix)
+
+
+
+class InitMacroDone(InitMacro):
+    """few statements AFTER the gl_INIT macro"""
+    _TEMPLATE_ = (
+        "",
+        "# Like AC_LIBOBJ, except that the module name goes",
+        "# into {macro_prefix}_LIBOBJS instead of into LIBOBJS.",
+        "AC_DEFUN([{macro_prefix}_LIBOBJ], [",
+        "  AS_LITERAL_IF([$1], [{macro_prefix}_LIBSOURCES([$1.c])])dnl",
+        "  {macro_prefix}_LIBOBJS=\"${macro_prefix}_LIBOBJS $1.$ac_objext\"",
+        "])",
+        "",
+        "# Like AC_REPLACE_FUNCS, except that the module name goes",
+        "# into {macro_prefix}_LIBOBJS instead of into LIBOBJS.",
+        "AC_DEFUN([{macro_prefix}_REPLACE_FUNCS], [",
+        "  m4_foreach_w([gl_NAME], [$1], [AC_LIBSOURCES(gl_NAME[.c])])dnl",
+        "  AC_CHECK_FUNCS([$1], , [{macro_prefix}_LIBOBJ($ac_func)])",
+        "])",
+        "",
+        "# Like AC_LIBSOURCES, except the directory where the source file is",
+        "# expected is derived from the gnulib-tool parameterization,",
+        "# and alloca is special cased (for the alloca-opt module).",
+        "# We could also entirely rely on EXTRA_lib..._SOURCES.",
+        "AC_DEFUN([{macro_prefix}_LIBSOURCES], [",
+        "  m4_foreach([_gl_NAME], [$1], [",
+        "    m4_if(_gl_NAME, [alloca.c], [], [",
+        "      m4_define([{macro_prefix}_LIBSOURCES_DIR], [{source_base}])",
+        "      m4_append([{macro_prefix}_LIBSOURCES_LIST], _gl_NAME, [ ])",
+        "    ])",
+        "  ])",
+        "])",
+    )
+
+
+    def __init__(self, config, source_base=None, macro_prefix=None):
+        if source_base is None:
+            source_base = config.source_base
+        if not isinstance(source_base, str):
+            raise TypeError("source_base must be of str type")
+        super().__init__(config=config, macro_prefix=macro_prefix)
+        self._source_base_ = source_base
+
+
+    @property
+    def source_base(self):
+        """directory relative to ROOT where source code is placed; defaults to 'lib'"""
+        return self._source_base_
+
+
+    def __repr__(self):
+        fmt = "pygnulib.generator.InitMacroDone(source_base=%r, macro_prefix=%r)"
+        return fmt % (self.source_base, self.macro_prefix)
+
+
+    def __iter__(self):
+        for line in InitMacroDone._TEMPLATE_:
+            yield line.format(source_base=self._source_base_, macro_prefix=self._macro_prefix_)
diff --git a/pygnulib/module.py b/pygnulib/module.py
new file mode 100644 (file)
index 0000000..75aad88
--- /dev/null
@@ -0,0 +1,416 @@
+#!/usr/bin/python
+# encoding: UTF-8
+
+
+
+import codecs
+import collections
+import hashlib
+import os
+import re
+
+
+
+class Module:
+    """gnulib generic module"""
+    _TABLE_ = {
+        "description"                : (0x00, str, "Description"),
+        "comment"                    : (0x01, str, "Comment"),
+        "status"                     : (0x02, str, "Status"),
+        "notice"                     : (0x03, str, "Notice"),
+        "applicability"              : (0x04, str, "Applicability"),
+        "files"                      : (0x05, list, "Files"),
+        "dependencies"               : (0x06, list, "Depends-on"),
+        "early_configure_ac_snippet" : (0x07, str, "configure.ac-early"),
+        "configure_ac_snippet"       : (0x08, str, "configure.ac"),
+        "makefile_am_snippet"        : (0x09, str, "Makefile.am"),
+        "include"                    : (0x0A, list, "Include"),
+        "link"                       : (0x0B, list, "Link"),
+        "license"                    : (0x0C, str, "License"),
+        "maintainers"                : (0x0D, list, "Maintainer"),
+    }
+    _PATTERN_DEPENDENCIES_ = re.compile("^(\\S+)(?:\\s+(.+))*$")
+    _PATTERN_INCLUDE_ = re.compile("^[\\<\"]([A-Za-z0-9/\\-_]+\\.h)[\\>\"](?:\\s+.*^)*$")
+
+
+    def __init__(self, name, **kwargs):
+        if not isinstance(name, str):
+            raise TypeError("name must be of 'str' type")
+        self._name_ = name
+        self._table_ = {"maintainers": ["all"]}
+        for key in Module._TABLE_:
+            self._table_[key] = ""
+        for key, value in kwargs.items():
+            self._table_[key] = value
+
+
+    @property
+    def name(self):
+        """name"""
+        return self._name_
+
+    @name.setter
+    def name(self, value):
+        if not isinstance(value, str):
+            raise TypeError("'str' type is expected")
+        self._name_ = value
+
+
+    @property
+    def description(self):
+        """description"""
+        return self._table_["description"]
+
+    @description.setter
+    def description(self, value):
+        if not isinstance(value, str):
+            raise TypeError("'str' type is expected")
+        self._table_["description"] = value
+
+
+    @property
+    def comment(self):
+        """comment"""
+        return self._table_["comment"]
+
+    @comment.setter
+    def comment(self, value):
+        if not isinstance(value, str):
+            raise TypeError("'str' type is expected")
+        self._table_["comment"] = value
+
+
+    @property
+    def status(self):
+        """status"""
+        return self._table_["status"]
+
+    @status.setter
+    def status(self, value):
+        if not isinstance(value, str):
+            raise TypeError("'str' type is expected")
+        self._table_["status"] = value
+
+
+    @property
+    def notice(self):
+        """notice"""
+        return self._table_["notice"]
+
+    @notice.setter
+    def notice(self, value):
+        if not isinstance(value, str):
+            raise TypeError("'str' type is expected")
+        self._table_["notice"] = value
+
+
+    @property
+    def applicability(self):
+        """applicability (usually "main" or "tests")"""
+        default = "main" if self.name.endswith("-tests") else "tests"
+        current = self._table_["applicability"]
+        return current if current.strip() else default
+
+    @applicability.setter
+    def applicability(self, value):
+        if not isinstance(value, str):
+            raise TypeError("'str' type is expected")
+        self._table_["applicability"] = value
+
+
+    @property
+    def files(self):
+        """file dependencies iterator (set of strings)"""
+        for file in self._table_["files"]:
+            yield file
+
+    @files.setter
+    def files(self, iterable):
+        if isinstance(iterable, (bytes, str)) \
+        or not isinstance(iterable, collections.Iterable):
+            raise TypeError("iterable of strings is expected")
+        result = set()
+        for item in iterable:
+            if not isinstance(item, str):
+                raise TypeError("iterable of strings is expected")
+            result.update([item])
+        self._table_["files"] = result
+
+
+    @property
+    def dependencies(self):
+        """dependencies iterator (name, condition)"""
+        for entry in self._table_["dependencies"]:
+            yield Module._PATTERN_DEPENDENCIES_.findall(entry)[0]
+
+    @dependencies.setter
+    def dependencies(self, iterable):
+        error = TypeError("iterable of pairs (name, condition) is expected")
+        if isinstance(iterable, (bytes, str)) \
+        or not isinstance(iterable, collections.Iterable):
+            raise error
+        result = set()
+        try:
+            for pair in iterable:
+                (name, condition) = pair
+                if not isinstance(name, str) \
+                or not isinstance(condition, str):
+                    raise error
+                result.update([(name, condition)])
+        except ValueError:
+            raise error
+        self._table_["dependencies"] = result
+
+
+    @property
+    def early_configure_ac_snippet(self):
+        """early configure.ac snippet"""
+        return self._table_["early_configure_ac_snippet"]
+
+    @early_configure_ac_snippet.setter
+    def early_configure_ac_snippet(self, value):
+        if not isinstance(value, str):
+            raise TypeError("'str' type is expected")
+        self._table_["early_configure_ac_snippet"] = value
+
+
+    @property
+    def configure_ac_snippet(self):
+        """configure.ac snippet"""
+        return self._table_["configure_ac_snippet"]
+
+    @configure_ac_snippet.setter
+    def configure_ac_snippet(self, value):
+        if not isinstance(value, str):
+            raise TypeError("'str' type is expected")
+        self._table_["configure_ac_snippet"] = value
+
+
+    @property
+    def makefile_am_snippet(self):
+        """Makefile.am snippet"""
+        return self._table_["makefile_am_snippet"]
+
+    @makefile_am_snippet.setter
+    def makefile_am_snippet(self, value):
+        if not isinstance(value, str):
+            raise TypeError("'str' type is expected")
+        self._table_["makefile_am_snippet"] = value
+
+
+    @property
+    def include(self):
+        """include files iterator (header, comment)"""
+        for entry in self._table_["include"]:
+            match = Module._PATTERN_INCLUDE_.findall(entry)
+            yield match[0] if match else entry
+
+    @include.setter
+    def include(self, iterable):
+        error = TypeError("iterable of pairs (header, comment) is expected")
+        if isinstance(iterable, (bytes, str)) \
+        or not isinstance(iterable, collections.Iterable):
+            raise error
+        result = set()
+        try:
+            for pair in iterable:
+                (header, comment) = pair
+                if not isinstance(header, str) \
+                or not isinstance(comment, str):
+                    raise error
+                result.update([(header, comment)])
+        except ValueError:
+            raise error
+        self._table_["include"] = result
+
+
+    @property
+    def link(self):
+        """linkage iterator (string)"""
+        for entry in self._table_["link"]:
+            yield entry
+
+    @link.setter
+    def link(self, iterable):
+        if isinstance(iterable, (bytes, str)) \
+        or not isinstance(iterable, collections.Iterable):
+            raise TypeError("iterable of strings is expected")
+        result = set()
+        for item in iterable:
+            if not isinstance(item, str):
+                raise TypeError("iterable of strings is expected")
+            result.update([item])
+        self._table_["link"] = result
+
+
+    @property
+    def license(self):
+        """license"""
+        return self._table_["license"]
+
+    @license.setter
+    def license(self, value):
+        if not isinstance(value, str):
+            raise TypeError("'str' type is expected")
+        self._table_["license"] = value
+
+
+    @property
+    def maintainers(self):
+        """maintainers iterator (maintainer)"""
+        for entry in self._table_["maintainers"]:
+            yield entry
+
+    @maintainers.setter
+    def maintainers(self, iterable):
+        if isinstance(iterable, (bytes, str)) \
+        or not isinstance(iterable, collections.Iterable):
+            raise TypeError("iterable of strings is expected")
+        result = set()
+        for item in iterable:
+            if not isinstance(item, str):
+                raise TypeError("iterable of strings is expected")
+            result.update([item])
+        self._table_["maintainers"] = result
+
+
+    def shell_variable(self, macro_prefix="gl"):
+        """Get the name of the shell variable set to true once m4 macros have been executed."""
+        module = self.name
+        if len(module) != len(module.encode()):
+            module = (module + "\n").encode("UTF-8")
+            module = hashlib.md5(module).hexdigest()
+        return "%s_gnulib_enabled_%s" % (macro_prefix, module)
+
+
+    def shell_function(self, macro_prefix="gl"):
+        """Get the name of the shell function containing the m4 macros."""
+        module = self.name
+        if len(module) != len(module.encode()):
+            module = (module + "\n").encode("UTF-8")
+            module = hashlib.md5(module).hexdigest()
+        return "func_%s_gnulib_m4code_%s" % (macro_prefix, module)
+
+
+    def conditional_name(self, macro_prefix="gl"):
+        """Get the automake conditional name."""
+        module = self.name
+        if len(module) != len(module.encode()):
+            module = (module + "\n").encode("UTF-8")
+            module = hashlib.md5(module).hexdigest()
+        return "%s_GNULIB_ENABLED_%s" % (macro_prefix, module)
+
+
+    def __hash__(self):
+        return hash(str(self))
+
+
+    def __repr__(self):
+        return self._name_
+
+
+    def __str__(self):
+        result = ""
+        for key, (_, typeid, field) in sorted(Module._TABLE_.items(), key=lambda k: k[1][0]):
+            field += ":\n"
+            if typeid is list:
+                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"
+
+
+    def __lt__(self, value):
+        return self.name < value.name
+
+    def __le__(self, value):
+        return self.__lt__(value) or self.__eq__(value)
+
+    def __eq__(self, value):
+        return self.name == value.name
+
+    def __ne__(self, value):
+        return not self.__eq__(value)
+
+    def __ge__(self, value):
+        return value.__le__(self)
+
+    def __gt__(self, value):
+        return value.__lt__(self)
+
+
+
+class FileModule(Module):
+    """gnulib module text file"""
+    _TABLE_ = {
+        "Description"        : (str, "description"),
+        "Comment"            : (str, "comment"),
+        "Status"             : (str, "status"),
+        "Notice"             : (str, "notice"),
+        "Applicability"      : (str, "applicability"),
+        "Files"              : (list, "files"),
+        "Depends-on"         : (list, "dependencies"),
+        "configure.ac-early" : (str, "early_configure_ac_snippet"),
+        "configure.ac"       : (str, "configure_ac_snippet"),
+        "Makefile.am"        : (str, "makefile_am_snippet"),
+        "Include"            : (list, "include"),
+        "Link"               : (list, "link"),
+        "License"            : (str, "license"),
+        "Maintainer"         : (list, "maintainers"),
+    }
+    _FIELDS_ = [field for (_, _, field) in Module._TABLE_.values()]
+    _PATTERN_ = re.compile("(%s):" % "|".join(_FIELDS_))
+
+    def __init__(self, path, mode="r", name=None, **kwargs):
+        if name is None:
+            name = os.path.basename(path)
+        if mode not in ("r", "w", "rw"):
+            raise ValueError("illegal mode: %r" % mode)
+        if mode == "r":
+            super().__init__(name)
+            with codecs.open(path, "rb", "UTF-8") as stream:
+                data = ""
+                for line in stream:
+                    line = line.strip("\n")
+                    if line.startswith("#") \
+                    or (line.startswith("/*") and line.endswith("*/")):
+                        continue
+                    data += (line + "\n")
+            match = FileModule._PATTERN_.split(data)[1:]
+            for (group, value) in zip(match[::2], match[1::2]):
+                (typeid, key) = FileModule._TABLE_[group]
+                if typeid is list:
+                    self._table_[key] = [_ for _ in "".join(value).split("\n") if _.strip()]
+                else:
+                    self._table_[key] = value.strip()
+            self._stream_ = None
+        elif mode == "w":
+            super().__init__(name)
+            self._stream_ = codecs.open(path, "w+", "UTF-8")
+        elif mode == "rw":
+            self.__init__(path, "r")
+            self._stream_ = codecs.open(path, "w+", "UTF-8")
+        else:
+            raise ValueError("invalid mode: %r" % mode)
+        for key, value in kwargs.items():
+            self._table_[key] = value
+
+
+    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()
+
+
+    def __enter__(self):
+        return self
+
+
+    def __exit__(self, exctype, excval, exctrace):
+        self.close()