From ecea1586e65c45907522d71a4d3197e26ceecd74 Mon Sep 17 00:00:00 2001 From: Dmitry Selyutin <ghostmansd@gmail.com> Date: Sun, 4 Feb 2018 13:13:13 +0300 Subject: [PATCH] simplify properties; improve performance --- pygnulib.py | 104 +++-- pygnulib/config.py | 920 ++++++++++++++++-------------------------- pygnulib/error.py | 19 - pygnulib/generator.py | 281 +++++-------- pygnulib/misc.py | 175 ++++++++ pygnulib/module.py | 889 +++++++++++++++++++++------------------- pygnulib/parser.py | 4 + pygnulib/tools.py | 9 - pygnulib/vfs.py | 97 +++-- 9 files changed, 1225 insertions(+), 1273 deletions(-) create mode 100644 pygnulib/misc.py diff --git a/pygnulib.py b/pygnulib.py index defa62650a..1636a6a4d1 100755 --- a/pygnulib.py +++ b/pygnulib.py @@ -116,15 +116,7 @@ def extract_hook(program, gnulib, mode, namespace, *args, **kwargs): def import_hook(script, gnulib, namespace, explicit, verbosity, options, *args, **kwargs): (_, _) = (args, kwargs) config = BaseConfig(**namespace) - try: - cache = CachedConfig(root=config.root) - except FileNotFoundError: - cache = BaseConfig(**config) - cache.files = set() - cache.ac_version = "2.59" - for key in {"ac_version", "files"}: - if key not in namespace: - config[key] = cache[key] + cache = CachedConfig(root=config.root, gnulib_comp=False, gnulib_cache=False) database = Database(gnulib.module, config) # Print some information about modules. @@ -155,7 +147,7 @@ def import_hook(script, gnulib, namespace, explicit, verbosity, options, *args, # Determine license incompatibilities, if any. incompatibilities = set() - if config.licenses & {"LGPLv2", "LGPLv2+", "LGPLv3", "LGPLv3+"}: + if set(config.licenses) & {"LGPLv2", "LGPLv2+", "LGPLv3", "LGPLv3+"}: acceptable = IGNORED_LICENSES | config.licenses for (name, licenses) in ((module.name, module.licenses) for module in main): if not (acceptable & licenses): @@ -176,7 +168,13 @@ def import_hook(script, gnulib, namespace, explicit, verbosity, options, *args, print("\n".join(" " + line for line in notice.splitlines()), file=sys.stdout) # Determine the final file list. - files = (set(database.main_files) | set(database.test_files)) + test_files = set() + main_files = set(database.main_files) + for file in database.test_files: + if file.startswith("lib/"): + file = "tests=lib/{}".format(file[len("lib/"):]) + test_files.add(file) + files = (main_files | test_files) if verbosity >= 0: print("File list:", file=sys.stdout) for file in sorted(files): @@ -188,24 +186,6 @@ def import_hook(script, gnulib, namespace, explicit, verbosity, options, *args, else: print(" ", file, file=sys.stdout, sep="") - table = {} - overrides = [] - project = BaseVFS(config.root, **table) - overrides = {BaseVFS(override, **table) for override in config.overrides} - for (name, key) in SUBSTITUTION.items(): - project[name] = config[key] if key else "" - for override in overrides: - for (name, key) in SUBSTITUTION.items(): - override[name] = config[key] if key else "" - - table = {} - old_files = set(cache.files) - if "m4/gnulib-tool.m4" in project: - old_files |= set(["m4/gnulib-tool.m4"]) - for (tbl_key, cfg_key) in SUBSTITUTION.items(): - table[tbl_key] = cache[cfg_key] if cfg_key else "" - new_files = frozenset(files | set(["m4/gnulib-tool.m4"])) - dry_run = options["dry_run"] gnulib_copymode = config.copymode local_copymode = config.local_copymode @@ -249,11 +229,29 @@ def import_hook(script, gnulib, namespace, explicit, verbosity, options, *args, transfer_file(local, src_vfs, src_name, dst_vfs, dst_name) print(fmt.format(file=dst_name), file=sys.stdout) + # Adjust the VFS mappings. + overrides = [] + project = BaseVFS(config.root) + overrides = {BaseVFS(override) for override in config.overrides} + for (alias, key) in SUBSTITUTION.items(): + path = config[key] if key else "" + project[alias] = path + for override in overrides: + override[alias] = path + gnulib["tests=lib"] = "lib" + + # Determine all relevant file lists. + old_files = set(cache.files) + if "m4/gnulib-tool.m4" in project: + old_files |= set(["m4/gnulib-tool.m4"]) + new_files = (files | set(["m4/gnulib-tool.m4"])) + # First the files that are in old_files, but not in new_files. # Then the files that are in new_files, but not in old_files. # Then the files that are in new_files and in old_files. - removed_files = {file for file in old_files if file not in new_files} - added_files = {file for file in new_files if file not in old_files} + origin = lambda file: ("lib/" + file[len("tests=lib/"):]) if file.startswith("tests=lib/") else file + removed_files = set(map(origin, old_files)).difference(map(origin, new_files)) + added_files = new_files.difference(old_files) kept_files = (old_files & new_files) for file in sorted(removed_files): remove_file(project, file) @@ -299,8 +297,15 @@ def import_hook(script, gnulib, namespace, explicit, verbosity, options, *args, # Generate the contents of library makefile. path = os.path.join(config.source_base, config.makefile_name) with tempfile.NamedTemporaryFile("w", encoding="UTF-8", delete=False) as tmp: - modules = database.main_modules - for line in lib_makefile(path, config, explicit, database, modules, mkedits, False): + arguments = { + "path": path, + "config": config, + "explicit": explicit, + "modules": database.main_modules, + "mkedits": mkedits, + "testing": False, + } + for line in lib_makefile(**arguments): print(line, file=tmp) (src, dst) = (tmp.name, path) present = vfs_exists(project, dst) @@ -346,7 +351,7 @@ def import_hook(script, gnulib, namespace, explicit, verbosity, options, *args, action = update_file if present else add_file action(False, None, src, project, dst, present) os.unlink(tmp.name) - # Fetch PO files. + po_root = os.path.join(project.absolute, project["po"]) fmt = ("{} gnulib PO files from " + TP_URL) print(fmt.format("Fetching", "Fetch")[dry_run], file=sys.stdout) @@ -382,7 +387,7 @@ def import_hook(script, gnulib, namespace, explicit, verbosity, options, *args, # Create m4/gnulib-cache.m4. with tempfile.NamedTemporaryFile("w", encoding="UTF-8", delete=False) as tmp: - for line in gnulib_cache(config): + for line in gnulib_cache(config, explicit): print(line, file=tmp) (src, dst) = (tmp.name, "m4/gnulib-cache.m4") present = vfs_exists(project, dst) @@ -394,7 +399,13 @@ def import_hook(script, gnulib, namespace, explicit, verbosity, options, *args, # Create m4/gnulib-comp.m4. with tempfile.NamedTemporaryFile("w", encoding="UTF-8", delete=False) as tmp: - for line in gnulib_comp(config, explicit, database, True): + arguments = { + "config": config, + "explicit": explicit, + "database": database, + "subdirs": True, + } + for line in gnulib_comp(**arguments): print(line, file=tmp) (src, dst) = (tmp.name, "m4/gnulib-comp.m4") present = vfs_exists(project, dst) @@ -408,8 +419,16 @@ def import_hook(script, gnulib, namespace, explicit, verbosity, options, *args, if config.tests: path = os.path.join(config.tests_base, config.makefile_name) with tempfile.NamedTemporaryFile("w", encoding="UTF-8", delete=False) as tmp: - modules = database.test_modules - for line in tests_makefile(path, config, explicit, database, modules, mkedits, False): + arguments = { + "path": path, + "config": config, + "explicit": explicit, + "modules": database.test_modules, + "mkedits": mkedits, + "testing": False, + "libtests": database.libtests, + } + for line in tests_makefile(**arguments): print(line, file=tmp) (src, dst) = (tmp.name, path) present = vfs_exists(project, dst) @@ -418,7 +437,6 @@ def import_hook(script, gnulib, namespace, explicit, verbosity, options, *args, action = update_file if present else add_file action(False, None, src, project, dst, present) os.unlink(tmp.name) - return os.EX_OK @@ -464,7 +482,6 @@ HOOKS = { def main(script, gnulib, program, arguments, environ): gnulib = GnulibGitVFS(gnulib) - gnulib["tests=lib"] = "lib" parser = CommandLineParser(program) try: (namespace, mode, verbosity, options) = parser.parse(arguments) @@ -485,9 +502,9 @@ def main(script, gnulib, program, arguments, environ): "verbosity": verbosity, "options": options, } - for (action, callback) in HOOKS.items(): + for (action, hook) in HOOKS.items(): if mode.startswith(action): - return callback(**kwargs) + return hook(**kwargs) return os.EX_SOFTWARE @@ -501,13 +518,12 @@ if __name__ == "__main__": environ = dict(os.environ) try: result = main(script, gnulib, program, arguments, environ) - except BaseException as error: + except StopIteration as error: with codecs.open(log, "wb", "UTF-8") as stream: program = repr(program) if " " in program else program arguments = " ".join(repr(arg) if " " in arg else arg for arg in arguments) print(traceback.format_exc(), file=stream) print("COMMAND:", program, arguments, file=stream) - print("VERSION:", gnulib, file=stream) typeid = type(error) module = typeid.__module__ name = typeid.__name__ diff --git a/pygnulib/config.py b/pygnulib/config.py index 71c04cb322..ab7586c336 100644 --- a/pygnulib/config.py +++ b/pygnulib/config.py @@ -7,14 +7,15 @@ import codecs as _codecs import os as _os import re as _re - -from collections import OrderedDict as _OrderedDict from distutils import version as _version - -from .error import type_assert as _type_assert from .error import AutoconfVersionError as _AutoconfVersionError from .error import M4BaseMismatchError as _M4BaseMismatchError +from .misc import Property as _Property +from .misc import PathProperty as _PathProperty +from .misc import BitwiseProperty as _BitwiseProperty +from .misc import StringListProperty as _StringListProperty +from .misc import PathListProperty as _PathListProperty @@ -22,9 +23,6 @@ def _compile(regex): return _re.compile(regex, _re.S | _re.M) -_ITERABLES = frozenset((list, tuple, set, frozenset, type({}.keys()), type({}.values()))) - - LGPLv2_LICENSE = frozenset({"LGPLv2", "LGPLv2+"}) LGPLv3_LICENSE = frozenset({"LGPLv2+", "LGPLv3", "LGPLv3+", "LGPL"}) @@ -41,630 +39,418 @@ OTHER_LICENSES = frozenset({ -KEYS = frozenset({ - "root", - "overrides", - "source_base", - "m4_base", - "po_base", - "doc_base", - "tests_base", - "auxdir", - "libname", - "makefile_name", - "macro_prefix", - "po_domain", - "witness_c_macro", - "licenses", - "ac_version", - "ac_file", - "modules", - "avoids", - "files", - "copymode", - "local_copymode", - "tests", - "obsolete", - "cxx_tests", - "longrunning_tests", - "privileged_tests", - "unportable_tests", - "libtool", - "conditionals", - "copyrights", - "gnumake", - "single_configure", - "vc_files", - "all_tests", -}) - - - class BaseConfig: """gnulib generic configuration""" - _TABLE = { + __slots__ = ("__options", "__flags", "__active") + + + __PROPERTIES = { "root" : ".", - "overrides" : set(), + "overrides" : tuple(), "source_base" : "lib", "m4_base" : "m4", - "po_base" : "po", + "po_base" : "", "doc_base" : "doc", "tests_base" : "tests", - "auxdir" : "", + "auxdir" : "build-aux", "libname" : "libgnu", "makefile_name" : "Makefile.am", "macro_prefix" : "gl", "po_domain" : "", "witness_c_macro" : "", - "licenses" : set(), + "licenses" : tuple(), "ac_version" : "2.59", "ac_file" : "configure.ac", - "modules" : set(), - "avoids" : set(), - "files" : set(), + "modules" : tuple(), + "avoids" : tuple(), + "files" : tuple(), "copymode" : None, "local_copymode" : None, "tests" : False, - "obsolete" : False, "cxx_tests" : False, "longrunning_tests" : False, "privileged_tests" : False, "unportable_tests" : False, + "all_tests" : False, + "obsolete" : False, "libtool" : False, - "conditionals" : True, + "conditionals" : False, "copyrights" : False, "gnumake" : False, "single_configure" : False, "vc_files" : False, } + __OPTIONS = { + "root", + "overrides", + "source_base", + "m4_base", + "po_base", + "doc_base", + "tests_base", + "auxdir", + "libname", + "makefile_name", + "macro_prefix", + "po_domain", + "witness_c_macro", + "licenses", + "ac_version", + "ac_file", + "modules", + "avoids", + "files", + "copymode", + "local_copymode", + } + __FLAGS = { + "tests", + "cxx_tests", + "longrunning_tests", + "privileged_tests", + "unportable_tests", + "all_tests", + "obsolete", + "libtool", + "conditionals", + "copyrights", + "gnumake", + "single_configure", + "vc_files", + } - class _Option: - """gnulib configuration options""" - Tests = (1 << 0) - Obsolete = (1 << 1) - CXX = (1 << 2) - Longrunning = (1 << 3) - Privileged = (1 << 4) - Unportable = (1 << 5) - Libtool = (1 << 6) - Conditionals = (1 << 7) - Copyrights = (1 << 8) - GNUMake = (1 << 9) - SingeConfigure = (1 << 10) - AllTests = (Obsolete | Tests | CXX | Longrunning | Privileged | Unportable) - - - def __init__(self, **kwargs): - self.__options = 0 - self.__table = {} - for (key, value) in BaseConfig._TABLE.items(): - self[key] = kwargs.get(key, value) + def __init__(self, root, **kwargs): + if not isinstance(root, str): + raise TypeError("root: str expected") + if not root: + raise ValueError("root: empty path") + root = _os.path.normpath(root) + self.__active = {} + self.__flags = 0 + self.__options = {} + for key in BaseConfig.__OPTIONS: + value = BaseConfig.__PROPERTIES[key] + self.__set_option_pure(key, value) + for key in BaseConfig.__FLAGS: + state = BaseConfig.__PROPERTIES[key] + mask = getattr(self.__class__, key).mask + self.__set_flags_pure(mask, state) + self.__set_option("root", root) - def __repr__(self): - module = self.__class__.__module__ - name = self.__class__.__name__ - return "{}.{}{}".format(module, name, repr(self.__table)) + for (key, value) in kwargs.items(): + setattr(self, key, value) def __enter__(self): return self - def __exit__(self, exctype, excval, exctrace): - pass - - - @property - def root(self): - """target directory""" - return self.__table["root"] - - @root.setter - def root(self, value): - _type_assert("root", value, str) - if not value: - raise ValueError("non-empty path not allowed") - self.__table["root"] = _os.path.normpath(value) - - - @property - def overrides(self): - """local override directories""" - return self.__table["overrides"] - - @overrides.setter - def overrides(self, value): - _type_assert("overrides", value, _ITERABLES) - result = list() - for item in value: - _type_assert("override", item, str) - result.append(_os.path.normpath(item)) - result = _OrderedDict.fromkeys(result) - self.__table["overrides"] = tuple(result) - - - @property - def source_base(self): - """directory relative to ROOT where source code is placed; defaults to 'lib'""" - return self.__table["source_base"] - - @source_base.setter - def source_base(self, value): - _type_assert("source_base", value, str) - value = _os.path.normpath(value) - if _os.path.isabs(value): - raise ValueError("source_base cannot be an absolute path") - self.__table["source_base"] = _os.path.normpath(value) if value else "lib" - - - @property - def m4_base(self): - """directory relative to ROOT where *.m4 macros are placed; defaults to 'm4'""" - return self.__table["m4_base"] - - @m4_base.setter - def m4_base(self, value): - _type_assert("m4_base", value, str) - value = _os.path.normpath(value) - if _os.path.isabs(value): - raise ValueError("m4_base cannot be an absolute path") - self.__table["m4_base"] = _os.path.normpath(value) if value else "m4" - - - @property - def po_base(self): - """directory relative to ROOT where *.po files are placed; defaults to 'po'""" - return self.__table["po_base"] - - @po_base.setter - def po_base(self, value): - _type_assert("po_base", value, str) - value = _os.path.normpath(value) - if _os.path.isabs(value): - raise ValueError("po_base cannot be an absolute path") - self.__table["po_base"] = _os.path.normpath(value) if value else "po" - - - @property - def doc_base(self): - """directory relative to ROOT where doc files are placed; defaults to 'doc'""" - return self.__table["doc_base"] - - @doc_base.setter - def doc_base(self, value): - _type_assert("doc_base", value, str) - value = _os.path.normpath(value) - if _os.path.isabs(value): - raise ValueError("doc_base cannot be an absolute path") - self.__table["doc_base"] = _os.path.normpath(value) if value else "doc" - - - @property - def tests_base(self): - """directory relative to ROOT where unit tests are placed; defaults to 'tests'""" - return self.__table["tests_base"] - - @tests_base.setter - def tests_base(self, value): - _type_assert("tests_base", value, str) - value = _os.path.normpath(value) - if _os.path.isabs(value): - raise ValueError("tests_base cannot be an absolute path") - self.__table["tests_base"] = _os.path.normpath(value) if value else "tests" - - - @property - def auxdir(self): - """directory relative to ROOT where auxiliary build tools are placed""" - return self.__table["auxdir"] - - @auxdir.setter - def auxdir(self, value): - _type_assert("auxdir", value, str) - value = _os.path.normpath(value) - if _os.path.isabs(value): - raise ValueError("auxdir cannot be an absolute path") - self.__table["auxdir"] = _os.path.normpath(value) if value else "build-aux" - - - @property - def libname(self): - """library name; defaults to 'libgnu'""" - return self.__table["libname"] - - @libname.setter - def libname(self, value): - _type_assert("libname", value, str) - self.__table["libname"] = value if value else "libgnu" - - - @property - def makefile_name(self): - """name of makefile in automake syntax in the source-base and tests-base directories""" - return self.__table["makefile_name"] - - @makefile_name.setter - def makefile_name(self, value): - _type_assert("makefile_name", value, str) - value = _os.path.normpath(value) - if _os.path.isabs(value): - raise ValueError("makefile_name cannot be an absolute path") - self.__table["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.__table["macro_prefix"] - - @macro_prefix.setter - def macro_prefix(self, value): - _type_assert("macro_prefix", value, str) - self.__table["macro_prefix"] = value - - - @property - def po_domain(self): - """the prefix of the i18n domain""" - return self.__table["po_domain"] - - @po_domain.setter - def po_domain(self, value): - _type_assert("po_domain", value, str) - self.__table["po_domain"] = value - - - @property - def witness_c_macro(self): - """the C macro that is defined when the sources are compiled or used""" - return self.__table["witness_c_macro"] - - @witness_c_macro.setter - def witness_c_macro(self, value): - _type_assert("witness_c_macro", value, str) - self.__table["witness_c_macro"] = value - - - @property - def licenses(self): - """abort if modules aren't available under the LGPL; also modify license template""" - return self.__table["licenses"] - - @licenses.setter - def licenses(self, value): - _type_assert("licenses", value, _ITERABLES) - result = set() - for item in value: - _type_assert("license", item, str) - result.add(item) - self.__table["licenses"] = frozenset(result) - - - @property - def ac_version(self): - """minimal supported autoconf version""" - return self.__table["ac_version"] - - @ac_version.setter - def ac_version(self, value): - _type_assert("ac_version", value, str) - if _version.LooseVersion(value) < _version.LooseVersion("2.59"): - raise _AutoconfVersionError("2.59") - self.__table["ac_version"] = value - + def __repr__(self): + module = self.__class__.__module__ + name = self.__class__.__name__ + table = ", ".join(f"{key}={value}" for (key, value) in self.items()) + return f"{module}.{name}{{{table}}}" - @property - def ac_file(self): - """autoconf file (usually configure.ac or configure.in)""" - return self.__table["ac_file"] - @ac_file.setter - def ac_file(self, value): - _type_assert("ac_file", value, str) - self.__table["ac_file"] = value + def __getitem__(self, key): + if key not in BaseConfig.__PROPERTIES: + key = key.replace("-", "_") + if key not in BaseConfig.__PROPERTIES: + raise KeyError("unsupported option: {0}".format(key)) + return getattr(self, key) - @property - def modules(self): - """list of modules""" - return self.__table["modules"] + def __setitem__(self, key, value): + if key not in BaseConfig.__PROPERTIES: + key = key.replace("_", "-") + if key not in BaseConfig.__PROPERTIES: + raise KeyError("unsupported option: {0}".format(key)) + return setattr(self, key, value) - @modules.setter - def modules(self, value): - _type_assert("modules", value, _ITERABLES) - result = set() - for item in value: - _type_assert("module", item, str) - result.add(item) - self.__table["modules"] = frozenset(result) + def __get_option(self, key): + return self.__options[key] - @property - def avoids(self): - """list of modules to avoid""" - return self.__table["avoids"] + def __set_option_pure(self, key, value): + self.__options[key] = value - @avoids.setter - def avoids(self, value): - _type_assert("avoids", value, _ITERABLES) - result = set() - for item in value: - _type_assert("avoid", item, str) - result.add(item) - self.__table["avoids"] = frozenset(result) + def __set_option(self, key, value): + self.__active[key] = True + return self.__set_option_pure(key, value) - @property - def files(self): - """list of files to be processed""" - return self.__table["files"] + def __get_flags(self, mask): + return self.__flags & mask - @files.setter - def files(self, value): - _type_assert("files", value, _ITERABLES) - result = set() - for item in value: - _type_assert("file", item, str) - result.add(item) - self.__table["files"] = frozenset(result) + def __set_flags_pure(self, mask, state): + if state: + self.__flags |= mask + else: + self.__flags &= ~mask + + def __set_flags(self, mask, state): + key = {getattr(BaseConfig, key).mask:key for key in BaseConfig.__FLAGS}[mask] + self.__active[key] = True + return self.__set_flags_pure(mask, state) + + + root = _PathProperty( + fget=lambda self: self.__get_option("root"), + fset=lambda self, path: self.__set_option("root", path), + doc="target directory (root project directory)", + ) + overrides = _PathListProperty( + sorted=False, + unique=True, + fget=lambda self: self.__get_option("overrides"), + fset=lambda self, paths: self.__set_option("overrides", paths), + doc="local override directories", + ) + source_base = _PathProperty( + fget=lambda self: self.__get_option("source_base"), + fset=lambda self, path: self.__set_option("source_base", path), + doc="directory relative to ROOT where source code is placed; defaults to 'lib'", + ) + m4_base = _PathProperty( + fget=lambda self: self.__get_option("m4_base"), + fset=lambda self, path: self.__set_option("m4_base", path), + doc="directory relative to ROOT where *.m4 macros are placed; defaults to 'm4'", + ) + po_base = _Property( + fget=lambda self: self.__get_option("po_base"), + fset=lambda self, path: self.__set_option("po_base", path), + check=lambda value: isinstance(value, str), + doc="directory relative to ROOT where *.po files are placed; defaults to 'po'", + ) + doc_base = _PathProperty( + fget=lambda self: self.__get_option("doc_base"), + fset=lambda self, path: self.__set_option("doc_base", path), + doc="directory relative to ROOT where doc files are placed; defaults to 'doc'", + ) + tests_base = _PathProperty( + fget=lambda self: self.__get_option("tests_base"), + fset=lambda self, path: self.__set_option("tests_base", path), + doc="directory relative to ROOT where unit tests are placed; defaults to 'tests'", + ) + auxdir = _PathProperty( + fget=lambda self: self.__get_option("auxdir"), + fset=lambda self, path: self.__set_option("auxdir", path), + doc="directory relative to ROOT where auxiliary build tools are placed; defaults to 'build-aux'", + ) + libname = _Property( + fget=lambda self: self.__get_option("libname"), + fset=lambda self, name: self.__set_option("libname", name), + check=lambda value: isinstance(value, str) and value, + doc="library name; defaults to 'libgnu'", + ) + makefile_name = _Property( + fget=lambda self: self.__get_option("makefile_name"), + fset=lambda self, name: self.__set_option("makefile_name", name), + check=lambda value: isinstance(value, str) and value, + doc="name of makefile in automake syntax in the source-base and tests-base directories", + ) + macro_prefix = _Property( + fget=lambda self: self.__get_option("macro_prefix"), + fset=lambda self, name: self.__set_option("macro_prefix", name), + check=lambda value: isinstance(value, str) and value, + doc="the prefix of the macros 'gl_EARLY' and 'gl_INIT'; defaults to 'gl'", + ) + po_domain = _Property( + fget=lambda self: self.__get_option("po_domain"), + fset=lambda self, name: self.__set_option("po_domain", name), + check=lambda value: value is None or isinstance(value, str), + doc="the prefix of the i18n domain", + ) + witness_c_macro = _Property( + fget=lambda self: self.__get_option("witness_c_macro"), + fset=lambda self, name: self.__set_option("witness_c_macro", name), + check=lambda value: isinstance(value, str) and (value == "") or not value[0].isdigit(), + doc="the C macro that is defined when the sources are compiled or used", + ) + modules = _StringListProperty( + sorted=True, + unique=True, + fget=lambda self: self.__get_option("modules"), + fset=lambda self, name: self.__set_option("modules", name), + doc="sorted set of the desired modules", + ) + avoids = _StringListProperty( + sorted=True, + unique=True, + fget=lambda self: self.__get_option("avoids"), + fset=lambda self, name: self.__set_option("avoids", name), + doc="sorted set of the modules that must be avoided", + ) + files = _PathListProperty( + sorted=True, + unique=True, + fget=lambda self: self.__get_option("files"), + fset=lambda self, name: self.__set_option("files", name), + doc="a list of files to be processed", + ) + copymode = _Property( + fget=lambda self: self.__get_option("copymode"), + fset=lambda self, mode: self.__set_option("copymode", mode), + check=lambda value: value in {None, "hardlink", "symlink"}, + doc="file copy mode ('symlink', 'hardlink' or None)", + ) + local_copymode = _Property( + fget=lambda self: self.__get_option("copymode"), + fset=lambda self, mode: self.__set_option("copymode", mode), + check=lambda value: value in {None, "hardlink", "symlink"}, + doc="file copy mode for local directory ('symlink', 'hardlink' or None)", + ) + ac_file = _PathProperty( + fget=lambda self: self.__get_option("ac_file"), + fset=lambda self, path: self.__set_option("ac_file", path), + doc="autoconf file (usually configure.ac or configure.in)", + ) + ac_version = _Property( + fget=lambda self: self.__get_option("ac_version"), + fset=lambda self, version: self.__set_option("ac_version", str(_version.LooseVersion(version))), + doc="minimal supported autoconf version", + ) + licenses = _StringListProperty( + sorted=True, + unique=True, + fget=lambda self: self.__get_option("licenses"), + fset=lambda self, name: self.__set_option("licenses", name), + doc="acceptable licenses for modules", + ) + tests = _BitwiseProperty( + mask=(1 << 0), + fget=lambda self, mask: bool(self.__get_flags(mask)), + fset=lambda self, mask, state: self.__set_flags(mask, state), + doc="include unit tests for the included modules?", + ) + obsolete = _BitwiseProperty( + mask=(1 << 1), + fget=lambda self, mask: bool(self.__get_flags(mask)), + fset=lambda self, mask, state: self.__set_flags(mask, state), + doc="include obsolete modules when they occur among the modules?", + ) + cxx_tests = _BitwiseProperty( + mask=(1 << 2), + fget=lambda self, mask: bool(self.__get_flags(mask)), + fset=lambda self, mask, state: self.__set_flags(mask, state), + doc="include even unit tests for C++ interoperability?", + ) + longrunning_tests = _BitwiseProperty( + mask=(1 << 3), + fget=lambda self, mask: bool(self.__get_flags(mask)), + fset=lambda self, mask, state: self.__set_flags(mask, state), + doc="include even unit tests for C++ interoperability?", + ) + privileged_tests = _BitwiseProperty( + mask=(1 << 4), + fget=lambda self, mask: bool(self.__get_flags(mask)), + fset=lambda self, mask, state: self.__set_flags(mask, state), + doc="include even unit tests that require root privileges?", + ) + unportable_tests = _BitwiseProperty( + mask=(1 << 5), + fget=lambda self, mask: bool(self.__get_flags(mask)), + fset=lambda self, mask, state: self.__set_flags(mask, state), + doc="include even unit tests that fail on some platforms?", + ) + mask = _BitwiseProperty( + mask=(obsolete.mask | cxx_tests.mask | longrunning_tests.mask | privileged_tests.mask | unportable_tests.mask), + fget=lambda self, mask: self.__get_flags(mask), + doc="configuration acceptibility mask", + ) + all_tests = _BitwiseProperty( + mask=(tests.mask | cxx_tests.mask | longrunning_tests.mask | privileged_tests.mask | unportable_tests.mask), + fget=lambda self, mask: bool(self.__get_flags(mask) == mask), + fset=lambda self, mask, state: self.__set_flags(mask, state), + doc="include all kinds of unit tests?", + ) + libtool = _BitwiseProperty( + mask=(1 << 6), + fget=lambda self, mask: bool(self.__get_flags(mask)), + fset=lambda self, mask, state: self.__set_flags(mask, state), + doc="use libtool rules?", + ) + conditionals = _BitwiseProperty( + mask=(1 << 7), + fget=lambda self, mask: bool(self.__get_flags(mask)), + fset=lambda self, mask, state: self.__set_flags(mask, state), + doc="support conditional dependencies (may save configure time and object code)?", + ) + copyrights = _BitwiseProperty( + mask=(1 << 8), + fget=lambda self, mask: bool(self.__get_flags(mask)), + fset=lambda self, mask, state: self.__set_flags(mask, state), + doc="update the license copyright text?", + ) + gnumake = _BitwiseProperty( + mask=(1 << 9), + fget=lambda self, mask: bool(self.__get_flags(mask)), + fset=lambda self, mask, state: self.__set_flags(mask, state), + doc="output for GNU Make instead of for the default Automake?", + ) + single_configure = _BitwiseProperty( + mask=(1 << 10), + fget=lambda self, mask: bool(self.__get_flags(mask)), + fset=lambda self, mask, state: self.__set_flags(mask, state), + doc="generate a single configure file?", + ) + vc_files = _BitwiseProperty( + mask=(1 << 11), + fget=lambda self, mask: bool(self.__get_flags(mask)), + fset=lambda self, mask, state: self.__set_flags(mask, state), + doc="update version control related files?", + ) @property def include_guard_prefix(self): """include guard prefix""" - prefix = self.__table["macro_prefix"].upper() - default = BaseConfig._TABLE["macro_prefix"].upper() - return "GL" if prefix == default else "GL_{0}".format(prefix) - - - @property - def copymode(self): - """file copy mode ('symlink', 'hardlink' or None)""" - return self.__table["copymode"] - - @copymode.setter - def copymode(self, value): - if value not in frozenset({None, "symlink", "hardlink"}): - raise ValueError("copymode: None, 'symlink' or 'hardlink'") - self.__table["copymode"] = value - - - @property - def local_copymode(self): - """file copy mode for local directory ('symlink', 'hardlink' or None)""" - return self.__table["local_copymode"] - - @local_copymode.setter - def local_copymode(self, value): - if value not in frozenset({None, "symlink", "hardlink"}): - raise ValueError("local_copymode: None, 'symlink' or 'hardlink'") - self.__table["local_copymode"] = value + prefix = self.macro_prefix.upper() + return "GL" if prefix == "GL" else f"GL_{prefix}" - @property - def tests(self): - """include unit tests for the included modules?""" - return bool(self.__options & BaseConfig._Option.Tests) - - @tests.setter - def tests(self, value): - _type_assert("tests", value, bool) - if value: - self.__options |= BaseConfig._Option.Tests - else: - self.__options &= ~BaseConfig._Option.Tests - - - @property - def obsolete(self): - """include obsolete modules when they occur among the modules?""" - return bool(self.__options & BaseConfig._Option.Obsolete) - - @obsolete.setter - def obsolete(self, value): - _type_assert("obsolete", value, bool) - if value: - self.__options |= BaseConfig._Option.Obsolete - else: - self.__options &= ~BaseConfig._Option.Obsolete - - - @property - def cxx_tests(self): - """include even unit tests for C++ interoperability?""" - return bool(self.__options & BaseConfig._Option.CXX) - - @cxx_tests.setter - def cxx_tests(self, value): - _type_assert("cxx_tests", value, bool) - if value: - self.__options |= BaseConfig._Option.CXX - else: - self.__options &= ~BaseConfig._Option.CXX - - - @property - def longrunning_tests(self): - """include even unit tests that are long-runners?""" - return bool(self.__options & BaseConfig._Option.Longrunning) - - @longrunning_tests.setter - def longrunning_tests(self, value): - _type_assert("longrunning_tests", value, bool) - if value: - self.__options |= BaseConfig._Option.Longrunning - else: - self.__options &= ~BaseConfig._Option.Longrunning - - - @property - def privileged_tests(self): - """include even unit tests that require root privileges?""" - return bool(self.__options & BaseConfig._Option.Privileged) - - @privileged_tests.setter - def privileged_tests(self, value): - _type_assert("privileged_tests", value, bool) - if value: - self.__options |= BaseConfig._Option.Privileged - else: - self.__options &= ~BaseConfig._Option.Privileged - - - @property - def unportable_tests(self): - """include even unit tests that fail on some platforms?""" - return bool(self.__options & BaseConfig._Option.Unportable) - - @unportable_tests.setter - def unportable_tests(self, value): - _type_assert("unportable_tests", value, bool) - if value: - self.__options |= BaseConfig._Option.Unportable - else: - self.__options &= ~BaseConfig._Option.Unportable - - - @property - def all_tests(self): - """include all kinds of problematic unit tests?""" - return (self.__options & BaseConfig._Option.AllTests) == BaseConfig._Option.AllTests - - @all_tests.setter - def all_tests(self, value): - if value: - self.__options |= BaseConfig._Option.AllTests - else: - self.__options &= BaseConfig._Option.AllTests - - - @property - def libtool(self): - """use libtool rules?""" - return bool(self.__options & BaseConfig._Option.Libtool) - - @libtool.setter - def libtool(self, value): - _type_assert("libtool", value, bool) - if value: - self.__options |= BaseConfig._Option.Libtool - else: - self.__options &= ~BaseConfig._Option.Libtool - - - @property - def conditionals(self): - """support conditional dependencies (may save configure time and object code)?""" - return bool(self.__options & BaseConfig._Option.Conditionals) - - @conditionals.setter - def conditionals(self, value): - _type_assert("conditionals", value, bool) - if value: - self.__options |= BaseConfig._Option.Conditionals - else: - self.__options &= ~BaseConfig._Option.Conditionals - - - @property - def copyrights(self): - """update the license copyright text?""" - return bool(self.__options & BaseConfig._Option.Copyrights) - - @copyrights.setter - def copyrights(self, value): - _type_assert("copyrights", value, bool) - if value: - self.__options |= BaseConfig._Option.Copyrights - else: - self.__options &= ~BaseConfig._Option.Copyrights - - - @property - def gnumake(self): - """update the license copyright text?""" - return bool(self.__options & BaseConfig._Option.GNUMake) - - @gnumake.setter - def gnumake(self, value): - _type_assert("gnumake", value, bool) - if value: - self.__options |= BaseConfig._Option.GNUMake - else: - self.__options &= ~BaseConfig._Option.GNUMake - - - @property - def single_configure(self): - """generate a single configure file?""" - return bool(self.__options & BaseConfig._Option.SingeConfigure) - - @single_configure.setter - def single_configure(self, value): - _type_assert("single_configure", value, bool) - if value: - self.__options |= BaseConfig._Option.SingeConfigure - else: - self.__options &= ~BaseConfig._Option.SingeConfigure - - - @property - def vc_files(self): - """update version control related files?""" - return self.__table["vc_files"] - - @vc_files.setter - def vc_files(self, value): - _type_assert("vc_files", value, bool) - self.__table["vc_files"] = value - - - def __getitem__(self, key): - if key not in KEYS: + def explicit(self, key): + """Determine if option is set to a non-default value.""" + if key not in BaseConfig.__PROPERTIES: key = key.replace("-", "_") - if key not in KEYS: - raise KeyError("unsupported option: {0}".format(key)) - return getattr(self, key) - - - def __setitem__(self, key, value): - if key not in KEYS: - key = key.replace("_", "-") - if key not in KEYS: + if key not in BaseConfig.__PROPERTIES: raise KeyError("unsupported option: {0}".format(key)) - return setattr(self, key, value) + return self.__active.get(key, False) def items(self): """a set-like object providing a view on configuration items""" - return self.__table.items() + for key in BaseConfig.__PROPERTIES: + value = self[key] + yield (key, value) def keys(self): """a set-like object providing a view on configuration keys""" - return self.__table.keys() + return iter(BaseConfig.__PROPERTIES) def values(self): """a set-like object providing a view on configuration values""" - return self.__table.values() + for key in BaseConfig.__PROPERTIES: + yield self[key] class CachedConfig(BaseConfig): """gnulib cached configuration""" - _COMMENTS = _compile(r"((?:(?:#)|(?:^dnl\s+)|(?:\s+dnl\s+)).*?)$") - _AUTOCONF = { + __slots__ = () + + + __COMMENTS = _compile(r"((?:(?:#)|(?:^dnl\s+)|(?:\s+dnl\s+)).*?)$") + __AUTOCONF = { "ac_version" : _compile(r"AC_PREREQ\(\[(.*?)\]\)"), "auxdir" : _compile(r"AC_CONFIG_AUX_DIR\(\[(.*?)\]\)$"), "libtool" : _compile(r"A[CM]_PROG_LIBTOOL") } - _GNULIB_CACHE = { + __GNULIB_CACHE = { "overrides" : (list, _compile(r"gl_LOCAL_DIR\(\[(.*?)\]\)")), "libtool" : (bool, _compile(r"gl_LIBTOOL\(\[(.*?)\]\)")), "conditionals" : (bool, _compile(r"gl_CONDITIONAL_DEPENDENCIES\(\[(.*?)\]\)")), @@ -692,26 +478,34 @@ class CachedConfig(BaseConfig): } - def __init__(self, root, ac_file=None, **kwargs): + def __init__(self, root, gnulib_comp=False, gnulib_cache=False, ac_file=None, **kwargs): if ac_file is None: ac_file = "configure.ac" - _type_assert("ac_file", ac_file, str) + if not isinstance(ac_file, str): + raise TypeError("ac_file: str expected") ac_path = _os.path.join(root, ac_file) if not _os.path.exists(ac_path): ac_file = "configure.in" ac_path = _os.path.join(root, ac_file) - ac_path = _os.path.normpath(ac_path) super().__init__(root=root, **kwargs) self.__autoconf(ac_path, kwargs) - self.__gnulib_cache(kwargs) - self.__gnulib_comp(kwargs) + try: + self.__gnulib_cache(kwargs) + except FileNotFoundError: + if gnulib_cache: + raise + try: + self.__gnulib_comp(kwargs) + except FileNotFoundError: + if gnulib_comp: + raise def __autoconf(self, ac_path, explicit): with _codecs.open(ac_path, "rb", "UTF-8") as stream: - data = CachedConfig._COMMENTS.sub("", stream.read()) - for (key, pattern) in CachedConfig._AUTOCONF.items(): + data = CachedConfig.__COMMENTS.sub("", stream.read()) + for (key, pattern) in CachedConfig.__AUTOCONF.items(): match = pattern.findall(data) if match and key not in explicit: self[key] = match[-1] @@ -722,8 +516,8 @@ class CachedConfig(BaseConfig): if not _os.path.exists(path): raise FileNotFoundError(path) with _codecs.open(path, "rb", "UTF-8") as stream: - data = CachedConfig._COMMENTS.sub("", stream.read()) - for (key, (typeid, pattern)) in CachedConfig._GNULIB_CACHE.items(): + data = CachedConfig.__COMMENTS.sub("", stream.read()) + for (key, (typeid, pattern)) in CachedConfig.__GNULIB_CACHE.items(): match = pattern.findall(data) if match and key not in explicit: if key == "licenses": @@ -749,7 +543,7 @@ class CachedConfig(BaseConfig): if not _os.path.exists(path): raise FileNotFoundError(path) with _codecs.open(path, "rb", "UTF-8") as stream: - data = CachedConfig._COMMENTS.sub("", stream.read()) + data = CachedConfig.__COMMENTS.sub("", stream.read()) pattern = _compile(r"AC_DEFUN\(\[{0}_FILE_LIST\], \[(.*?)\]\)".format(macro_prefix)) match = pattern.findall(data) if match and "files" not in explicit: diff --git a/pygnulib/error.py b/pygnulib/error.py index 933d28732b..1c667fb4fb 100644 --- a/pygnulib/error.py +++ b/pygnulib/error.py @@ -4,25 +4,6 @@ -def type_assert(key, value, types): - """panic if value has a type different than mentioned in types""" - typeset = [] - if isinstance(types, type): - types = [types] - types = tuple(types) - if not isinstance(value, types): - for typeid in types: - module = typeid.__module__ - name = typeid.__name__ - if module == "builtins": - typeset += [name] - else: - typeset += [module + "." + name] - typeset = "{0}".format(", ".join(typeset)) - raise TypeError("{0}: {1} expected".format(key, typeset)) - - - class AutoconfVersionError(Exception): """minimum supported autoconf version mismatch""" def __init__(self, version): diff --git a/pygnulib/generator.py b/pygnulib/generator.py index 8b2be1c3c4..274abcca79 100644 --- a/pygnulib/generator.py +++ b/pygnulib/generator.py @@ -11,7 +11,6 @@ import subprocess as _sp from datetime import datetime as _datetime -from .error import type_assert as _type_assert from .config import BaseConfig as _BaseConfig from .module import BaseModule as _BaseModule from .module import Database as _Database @@ -98,7 +97,6 @@ __PO_MAKE_VARS = ( def po_make_vars(config, **override): """Generate PO Makefile parameterization.""" - _type_assert("config", config, _BaseConfig) config = _BaseConfig(**config) for (key, value) in override.items(): config[key] = value @@ -122,7 +120,6 @@ def po_make_vars(config, **override): def POTFILES(config, files, **override): """Generate file list to be passed to xgettext.""" - _type_assert("config", config, _BaseConfig) config = _BaseConfig(**config) for (key, value) in override.items(): config[key] = value @@ -138,30 +135,17 @@ def POTFILES(config, files, **override): def autoconf_snippet(config, module, toplevel, no_libtool, no_gettext, **override): """ - Generate autoconf snippet for a standalone module. - - <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 - """ - _type_assert("config", config, _BaseConfig) - _type_assert("module", module, _BaseModule) - _type_assert("toplevel", toplevel, bool) - _type_assert("no_libtool", no_libtool, bool) - _type_assert("no_gettext", no_gettext, bool) + Generate autoconf snippet for a standalone module.""" config = _BaseConfig(**config) for (key, value) in override.items(): config[key] = value - libtool = config.libtool and not no_libtool gettext = not no_gettext if module.name not in ("gnumakefile", "maintainer-makefile") or toplevel: snippet = module.autoconf_snippet include_guard_prefix = config.include_guard_prefix snippet.replace(r"${gl_include_guard_prefix}", include_guard_prefix) - if not config.libtool: + if no_libtool: table = ( (r"$gl_cond_libtool", "false"), (r"gl_libdeps", "gltests_libdeps"), @@ -169,7 +153,7 @@ def autoconf_snippet(config, module, toplevel, no_libtool, no_gettext, **overrid ) for (src, dst) in table: snippet = snippet.replace(src, dst) - if not gettext: + if no_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) @@ -180,7 +164,7 @@ def autoconf_snippet(config, module, toplevel, no_libtool, no_gettext, **overrid lines = filter(lambda line: line.strip(), snippet.split("\n")) for line in lines: yield line - if module.name == "alloca" and libtool: + if module.name == "alloca" and config.libtool and not no_libtool: yield "changequote(,)dnl" yield "LTALLOCA=`echo \"$ALLOCA\" | sed -e 's/\\.[^.]* /.lo /g;s/\\.[^.]*$/.lo/'`" yield "changequote([, ])dnl" @@ -188,23 +172,8 @@ def autoconf_snippet(config, module, toplevel, no_libtool, no_gettext, **overrid -def autoconf_snippet_sequence(config, database, modules, toplevel, no_libtool, no_gettext, **override): - """ - Generate an autoconf snippet for multiple modules. - - <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 - [macro_prefix] the prefix of the macros 'gl_EARLY' and 'gl_INIT' - """ - _type_assert("config", config, _BaseConfig) - _type_assert("database", database, _Database) - _type_assert("modules", modules, _ITERABLES) - _type_assert("toplevel", toplevel, bool) - _type_assert("no_libtool", no_libtool, bool) - _type_assert("no_gettext", no_gettext, bool) +def autoconf_snippets(config, database, modules, toplevel, no_libtool, no_gettext, **override): + """Generate an autoconf snippet for multiple modules.""" config = _BaseConfig(**config) for (key, value) in override.items(): config[key] = value @@ -326,12 +295,7 @@ __INIT_MACRO_HEADER = ( ) def init_macro_header(config, **override): - """ - Generate the first few statements of the gl_INIT macro. - - [macro_prefix] the prefix of the macros 'gl_EARLY' and 'gl_INIT' - """ - _type_assert("config", config, _BaseConfig) + """Generate the first few statements of the gl_INIT macro.""" config = _BaseConfig(**config) for (key, value) in override.items(): config[key] = value @@ -382,7 +346,6 @@ __INIT_MACRO_FOOTER = ( def init_macro_footer(config, **override): """Generate the last few statements of the gl_INIT macro.""" - _type_assert("config", config, _BaseConfig) config = _BaseConfig(**config) for (key, value) in override.items(): config[key] = value @@ -429,7 +392,6 @@ def init_macro_done(config, **override): [macro_prefix] the prefix of the macros 'gl_EARLY' and 'gl_INIT' """ - _type_assert("config", config, _BaseConfig) config = _BaseConfig(**config) for (key, value) in override.items(): config[key] = value @@ -441,7 +403,7 @@ def init_macro_done(config, **override): __COMMAND_LINE_PATHS = ( ("libname", lambda k, v, d: f"--lib={v}"), - ("source_base", lambda k, v, d: f"--source_base={v}"), + ("source_base", lambda k, v, d: f"--source-base={v}"), ("m4_base", lambda k, v, d: f"--m4-base={v}"), ("po_base", lambda k, v, d: f"--po-base={v}" if k in d else None), ("doc_base", lambda k, v, d: f"--doc-base={v}"), @@ -457,21 +419,18 @@ __COMMAND_LINE_TESTS = ( ("all_tests", lambda v: "--with-all-tests" if v else None), ) __COMMAND_LINE_MISC = ( - ("conditionals", lambda k, v, d: ("--no", "--")[v] + "conditional-dependencies"), - ("libtool", lambda k, v, d: ("--no", "--")[v] + "libtool"), + ("makefile_name", lambda k, v, d: f"--makefile-name={v}"), + ("conditionals", lambda k, v, d: ("--no-", "--")[v] + "conditional-dependencies"), + ("libtool", lambda k, v, d: ("--no-", "--")[v] + "libtool"), ("macro_prefix", lambda k, v, d: f"--macro-prefix={v}"), ("gnumake", lambda k, v, d: "--gnu-make" if v else None), - ("makefile_name", lambda k, v, d: f"--makefile-name={v}"), ("po_domain", lambda k, v, d: f"--po-domain={v}" if k in d else None), ("witness_c_macro", lambda k, v, d: f"--witness-c-macro={v}" if k in d else None), - ("vc_files", lambda k, v, d: ("--no", "--")[v] + "vc-files" if k in d else None), + ("vc_files", lambda k, v, d: ("--no-", "--")[v] + "vc-files" if k in d else None), ) def command_line(config, explicit, **override): """Generate gnulib command-line invocation.""" - _type_assert("config", config, _BaseConfig) - _type_assert("explicit", explicit, _ITERABLES) - yield "gnulib-tool --import" for path in config.overrides: yield f"--local-dir={path}" @@ -487,7 +446,7 @@ def command_line(config, explicit, **override): yield option for module in config.avoids: yield "--avoid={module.name}" - if config.licenses in _LGPL: + if frozenset(config.licenses) in _LGPL: lgpl = _LGPL[config.licenses] if lgpl != "yes": yield f"--lgpl={lgpl}" @@ -498,8 +457,8 @@ def command_line(config, explicit, **override): option = hook(key, value, explicit) if option is not None: yield option - for module in sorted(config.modules): - yield "{}".format(module) + for module in config.modules: + yield f"{module}" @@ -545,23 +504,14 @@ def _lib_makefile_callback(conditionals, gnumake): return callbacks[conditionals][gnumake] -def lib_makefile(path, config, explicit, database, modules, mkedits, testing, **override): +def lib_makefile(path, config, explicit, modules, mkedits, testing, **override): """Generate library Makefile.am file.""" - _type_assert("path", path, str) - _type_assert("config", config, _BaseConfig) - _type_assert("explicit", explicit, _ITERABLES) - _type_assert("database", database, _Database) - _type_assert("modules", modules, _ITERABLES) - _type_assert("mkedits", mkedits, _ITERABLES) - _type_assert("testing", testing, bool) - date = _datetime.now() - kwargs = { - "libname": config.libname, - "macro_prefix": config.macro_prefix, - "libext": "la" if config.libtool else "a", - "perhaps_LT": "LT" if config.libtool else "", - } + libname = config.libname + po_domain = config.po_domain + macro_prefix = config.macro_prefix + libext = "la" if config.libtool else "a" + perhaps_LT = "LT" if config.libtool else "" assign = "+=" if config.gnumake or "makefile_name" in explicit else "=" eliminate_LDFLAGS = True if config.libtool else False @@ -573,7 +523,7 @@ def lib_makefile(path, config, explicit, database, modules, mkedits, testing, ** yield "## DO NOT EDIT! GENERATED AUTOMATICALLY!" yield "## Process this file with automake to produce Makefile.in." - yield "# Copyright (C) 2002-{} Free Software Foundation, Inc.".format(date.year) + yield f"# Copyright (C) 2002-{date.year} Free Software Foundation, Inc." for line in __DISCLAIMER[1:]: yield line yield "" @@ -584,14 +534,14 @@ def lib_makefile(path, config, explicit, database, modules, mkedits, testing, ** # similarly, the IRIX 6.5 awk fails if a line has length >= 3072. actioncmd = " ".join(command_line(config, explicit)) if len(actioncmd) <= 3000: - yield "# Reproduce by: {}".format(actioncmd) + yield f"# Reproduce by: {actioncmd}" yield "" callback = _lib_makefile_callback(config.conditionals, config.gnumake) def _snippet(): lines = [] subdirs = False - for module in database.main_modules: + for module in modules: if module.test: continue conditional = module.conditional_automake_snippet @@ -599,21 +549,21 @@ def lib_makefile(path, config, explicit, database, modules, mkedits, testing, ** conditional = conditional.replace("lib_LTLIBRARIES", "lib%_LTLIBRARIES") if eliminate_LDFLAGS: conditional = __MAKEFILE_LDFLAGS.sub("", conditional) - conditional = __MAKEFILE_LIBNAME.sub("{libname}_{libext}_\\1".format(**kwargs), conditional) + conditional = __MAKEFILE_LIBNAME.sub(f"{libname}_{libext}_\\1", conditional) conditional = conditional.replace("lib%_LIBRARIES", "lib_LIBRARIES") conditional = conditional.replace("lib%_LTLIBRARIES", "lib_LTLIBRARIES") if transform_check_PROGRAMS: conditional = conditional.replace("check_PROGRAMS", "noinst_PROGRAMS") conditional = conditional.replace(r"${gl_include_guard_prefix}", config.include_guard_prefix) unconditional = module.unconditional_automake_snippet.format(auxdir=config.auxdir) - unconditional = __MAKEFILE_LIBNAME.sub("{libname}_{libext}_\\1".format(**kwargs), unconditional) + unconditional = __MAKEFILE_LIBNAME.sub(f"{libname}_{libext}_\\1", unconditional) if (conditional + unconditional).strip(): - lines.append("## begin gnulib module {}".format(module.name)) + lines.append(f"## begin gnulib module {module.name}") if module.name == "alloca": - lines.append("{libname}_{libext}_LIBADD += @{perhaps_LT}ALLOCA@".format(**kwargs)) - lines.append("{libname}_{libext}_DEPENDENCIES += @{perhaps_LT}ALLOCA@".format(**kwargs)) + lines.append(f"{libname}_{libext}_LIBADD += @{perhaps_LT}ALLOCA@") + lines.append(f"{libname}_{libext}_DEPENDENCIES += @{perhaps_LT}ALLOCA@") lines += list(callback(module, conditional, unconditional, config.macro_prefix)) - lines.append("## end gnulib module {}".format(module.name)) + lines.append(f"## end gnulib module {module.name}") lines.append("") subdirs |= any(__MAKEFILE_SUBDIRS.match(file) for file in module.files) return (subdirs, lines) @@ -640,7 +590,7 @@ def lib_makefile(path, config, explicit, database, modules, mkedits, testing, ** yield "EXTRA_DIST =" yield "BUILT_SOURCES =" yield "SUFFIXES =" - yield "MOSTLYCLEANFILES {} core *.stackdump".format(assign) + yield f"MOSTLYCLEANFILES {assign} core *.stackdump" if "makefile_name" not in explicit: yield "MOSTLYCLEANDIRS =" yield "CLEANFILES =" @@ -661,7 +611,7 @@ def lib_makefile(path, config, explicit, database, modules, mkedits, testing, ** else: yield "== gnulib-tool GNU Make output failed as follows ==" for line in stderr.splitlines(): - yield "# stderr: {}".format(line) + yield f"# stderr: {line}" yield "# End of GNU Make output." else: yield "# No GNU Make output." @@ -677,11 +627,11 @@ def lib_makefile(path, config, explicit, database, modules, mkedits, testing, ** )) if "makefile_name" not in explicit: yield "" - yield "AM_CPPFLAGS ={}".format(cppflags) + yield f"AM_CPPFLAGS ={cppflags}" yield "AM_CFLAGS =" elif "".join(cppflags): yield "" - yield "AM_CPPFLAGS +={}".format(cppflags) + yield f"AM_CPPFLAGS +={cppflags}" yield "" snippet = "\n".join(lines) @@ -694,36 +644,35 @@ def lib_makefile(path, config, explicit, database, modules, mkedits, testing, ** # installation location for the library. Don't confuse automake by saying # it should not be installed. # By default, the generated library should not be installed. - regex = "^[a-zA-Z0-9_]*_{perhaps_LT}LIBRARIES\\s*\\+?\\=\\s*{libname}\\.{libext}$" - pattern = _re.compile(regex.format(**kwargs), _re.S) + pattern = _re.compile(f"^[a-zA-Z0-9_]*_{perhaps_LT}LIBRARIES\\s*\\+?\\=\\s*{libname}\\.{libext}$", _re.S) if not pattern.findall(snippet): - yield "noinst_{perhaps_LT}LIBRARIES += {libname}.{libext}".format(**kwargs) + yield f"noinst_{perhaps_LT}LIBRARIES += {libname}.{libext}" yield "" - yield "{libname}_{libext}_SOURCES =".format(**kwargs) + yield f"{libname}_{libext}_SOURCES =" # Here we use $(LIBOBJS), not @LIBOBJS@. The value is the same. However, # automake during its analysis looks for $(LIBOBJS), not for @LIBOBJS@. - yield "{libname}_{libext}_LIBADD = $({macro_prefix}_{perhaps_LT}LIBOBJS)".format(**kwargs) - yield "{libname}_{libext}_DEPENDENCIES = $({macro_prefix}_{perhaps_LT}LIBOBJS)".format(**kwargs) - yield "EXTRA_{libname}_{libext}_SOURCES =".format(**kwargs) + yield f"{libname}_{libext}_LIBADD = $({macro_prefix}_{perhaps_LT}LIBOBJS)" + yield f"{libname}_{libext}_DEPENDENCIES = $({macro_prefix}_{perhaps_LT}LIBOBJS)" + yield f"EXTRA_{libname}_{libext}_SOURCES =" if config.libtool: - yield "{libname}_{libext}_LDFLAGS = $(AM_LDFLAGS)".format(**kwargs) - yield "{libname}_{libext}_LDFLAGS += -no-undefined".format(**kwargs) + yield f"{libname}_{libext}_LDFLAGS = $(AM_LDFLAGS)" + yield f"{libname}_{libext}_LDFLAGS += -no-undefined" # Synthesize an ${libname}_${libext}_LDFLAGS augmentation by combining # the link dependencies of all modules. def _directives(modules): - directives = (module.link_directive for module in sorted(modules)) - for directive in filter(lambda directive: directive.strip(), directives): - index = directive.find("when linking with libtool") - if index != -1: - directive = directive[:index].strip(" ") - yield directive - for directive in _directives(database.main_modules): - yield "{libname}_{libext}_LDFLAGS += {directive}".format(directive=directive, **kwargs) + for module in modules: + for directive in module.link_directives: + index = directive.find("when linking with libtool") + if index != -1: + directive = directive[:index].strip(" ") + yield directive + for directive in sorted(set(_directives(modules))): + yield f"{libname}_{libext}_LDFLAGS += {directive}" yield "" if "po_base" in explicit: - yield "AM_CPPFLAGS += -DDEFAULT_TEXT_DOMAIN=\\\"{}-gnulib\\\"".format(config.po_domain) + yield f"AM_CPPFLAGS += -DDEFAULT_TEXT_DOMAIN=\\\"{po_domain}-gnulib\\\"" yield "" for line in lines: @@ -747,24 +696,17 @@ def _tests_makefile_callback(gnumake): yield snippet def _gnumake(module, snippet): - yield "ifeq (,$(OMIT_GNULIB_MODULE_{}))".format(module.name) + yield f"ifeq (,$(OMIT_GNULIB_MODULE_{module.name}))" yield "" yield snippet yield "endif" return _gnumake if gnumake else _automake -def tests_makefile(path, config, explicit, database, modules, mkedits, testing): +def tests_makefile(path, config, explicit, modules, mkedits, testing, libtests): """Generate tests Makefile.am file.""" - _type_assert("path", path, str) - _type_assert("config", config, _BaseConfig) - _type_assert("explicit", explicit, _ITERABLES) - _type_assert("database", database, _Database) - _type_assert("modules", modules, _ITERABLES) - _type_assert("mkedits", mkedits, _ITERABLES) - _type_assert("testing", testing, bool) - - if testing and not config.single_configure: + single_configure = config.single_configure + if testing and not single_configure: modules = sorted(filter(lambda module: module.test, modules)) date = _datetime.now() @@ -775,12 +717,6 @@ def tests_makefile(path, config, explicit, database, modules, mkedits, testing): witness_c_macro = config.witness_c_macro tests_base_inverse = "/".join(".." for _ in config.tests_base.split(_os.path.sep)) (libname, libext) = (config.libname, "la" if config.libtool else "a") - kwargs = { - "libname": config.libname, - "macro_prefix": config.macro_prefix, - "libext": "la" if config.libtool else "a", - "perhaps_LT": "LT" if config.libtool else "", - } assign = "+=" if config.gnumake or "makefile_name" in explicit else "=" eliminate_LDFLAGS = True if config.libtool else False @@ -792,7 +728,7 @@ def tests_makefile(path, config, explicit, database, modules, mkedits, testing): yield "## DO NOT EDIT! GENERATED AUTOMATICALLY!" yield "## Process this file with automake to produce Makefile.in." - yield "# Copyright (C) 2002-{} Free Software Foundation, Inc.".format(date.year) + yield f"# Copyright (C) 2002-{date.year} Free Software Foundation, Inc." for line in __DISCLAIMER[1:]: yield line yield "" @@ -815,14 +751,14 @@ def tests_makefile(path, config, explicit, database, modules, mkedits, testing): if transform_check_PROGRAMS: snippet = snippet.replace("check_PROGRAMS", "noinst_PROGRAMS") snippet = snippet.replace(r"${gl_include_guard_prefix}", config.include_guard_prefix) - if database.libtests and module.name == "alloca": + if libtests and module.name == "alloca": lines += ["libtests_a_LIBADD += @ALLOCA@"] lines += ["libtests_a_DEPENDENCIES += @ALLOCA@"] subdirs |= any(__MAKEFILE_SUBDIRS.match(file) for file in module.files) if snippet.strip(): - lines.append("## begin gnulib module {}".format(module.name)) + lines.append(f"## begin gnulib module {module.name}") lines += list(callback(module, snippet)) - lines.append("## end gnulib module {}".format(module.name)) + lines.append(f"## end gnulib module {module.name}") lines.append("") if module.longrunning_test: longrunning += lines @@ -856,7 +792,7 @@ def tests_makefile(path, config, explicit, database, modules, mkedits, testing): yield "EXTRA_PROGRAMS =" yield "noinst_HEADERS =" yield "noinst_LIBRARIES =" - if database.libtests: + if libtests: if testing: yield "noinst_LIBRARIES += libtests.a" else: @@ -906,12 +842,12 @@ def tests_makefile(path, config, explicit, database, modules, mkedits, testing): # module whose dependency to 'progname' is voluntarily omitted). # The LIBTESTS_LIBDEPS can be passed to the linker once or twice, it does # not matter. - local_ldadd_before = " libtests.a" if database.libtests else "" - local_ldadd_after = " libtests.a $(LIBTESTS_LIBDEPS)" if database.libtests else "" + local_ldadd_before = " libtests.a" if libtests else "" + local_ldadd_after = " libtests.a $(LIBTESTS_LIBDEPS)" if libtests else "" yield f"LDADD ={local_ldadd_before} {tests_base_inverse}/{source_base}/{libname}.{libext}{local_ldadd_after}" yield "" - if database.libtests: + if libtests: yield "libtests_a_SOURCES =" # Here we use $(LIBOBJS), not @LIBOBJS@. The value is the same. However, # automake during its analysis looks for $(LIBOBJS), not for @LIBOBJS@. @@ -954,16 +890,14 @@ __GNULIB_CACHE_OPTIONS = ( ("privileged", "gl_WITH_PRIVILEGED_TESTS"), ) -def gnulib_cache(config): +def gnulib_cache(config, explicit): """ Generate gnulib-cache.m4 file. """ - _type_assert("config", config, _BaseConfig) - date = _datetime.now() yield "## DO NOT EDIT! GENERATED AUTOMATICALLY!" yield "## Process this file with automake to produce Makefile.in." - yield "# Copyright (C) 2002-{} Free Software Foundation, Inc.".format(date.year) + yield f"# Copyright (C) 2002-{date.year} Free Software Foundation, Inc." for line in __DISCLAIMER: yield line yield "#" @@ -974,10 +908,13 @@ def gnulib_cache(config): yield "" yield "" yield "# Specification in the form of a command-line invocation:" - yield "gl_LOCAL_DIR([$relative_local_gnulib_path])" + yield "# " + " ".join(command_line(config, explicit)) + yield "" + yield "# Specification in the form of a few gnulib-tool.m4 macro invocations:" + yield "gl_LOCAL_DIR([{}])".format(" ".join(config.overrides)) yield "gl_MODULES([" for module in sorted(config.modules): - yield " {}".format(module) + yield f" {module}" yield "])" for key in ("obsolete", "cxx_tests", "longrunning_tests", "privileged_tests", "unportable_tests"): if config[key]: @@ -985,35 +922,32 @@ def gnulib_cache(config): if config.all_tests: yield "gl_WITH_ALL_TESTS" yield "gl_AVOID([{}])".format(" ".join(sorted(config.avoids))) - yield "gl_SOURCE_BASE([{}])".format(config.source_base) - yield "gl_M4_BASE([{}])".format(config.m4_base) - yield "gl_PO_BASE([{}])".format(config.po_base) - yield "gl_DOC_BASE([{}])".format(config.doc_base) - yield "gl_TESTS_BASE([{}])".format(config.tests_base) + yield f"gl_SOURCE_BASE([{config.source_base}])" + yield f"gl_M4_BASE([{config.m4_base}])" + yield f"gl_PO_BASE([{config.po_base}])" + yield f"gl_DOC_BASE([{config.doc_base}])" + yield f"gl_TESTS_BASE([{config.tests_base}])" if config.tests: yield "gl_WITH_TESTS" yield "gl_LIB([{}])".format(config.libname) - if config.licenses in _LGPL: + if frozenset(config.licenses) in _LGPL: lgpl = _LGPL[config.licenses] yield "gl_LGPL([{}])".format(lgpl) if lgpl != "yes" else "gl_LGPL" - yield "gl_MAKEFILE_NAME([{}])".format(config.makefile_name) + yield f"gl_MAKEFILE_NAME([{config.makefile_name}])" if config.conditionals: yield "gl_CONDITIONAL_DEPENDENCIES" if config.libtool: yield "gl_LIBTOOL" - yield "gl_MACRO_PREFIX([{}])".format(config.macro_prefix) - yield "gl_PO_DOMAIN([{}])".format(config.po_domain) - yield "gl_WITNESS_C_MACRO([{}])".format(config.witness_c_macro) + yield f"gl_MACRO_PREFIX([{config.macro_prefix}])" + yield f"gl_PO_DOMAIN([{config.po_domain}])" + yield f"gl_WITNESS_C_MACRO([{config.witness_c_macro}])" if config.vc_files: - yield "gl_VC_FILES([{}])".format(" ".join(sorted(config.vc_files))) + yield "gl_VC_FILES([{}])".format(" ".join(config.vc_files)) def gnulib_comp(config, explicit, database, subdirs, **override): """gnulib-comp.m4 generator""" - _type_assert("config", config, _BaseConfig) - _type_assert("explicit", explicit, _ITERABLES) - _type_assert("database", database, _Database) config = _BaseConfig(**config) for (key, value) in override.items(): config[key] = value @@ -1022,8 +956,9 @@ def gnulib_comp(config, explicit, database, subdirs, **override): test_modules = database.test_modules date = _datetime.now() + ac_file = config.ac_file yield "# DO NOT EDIT! GENERATED AUTOMATICALLY!" - yield "# Copyright (C) 2002-{} Free Software Foundation, Inc.".format(date.year) + yield f"# Copyright (C) 2002-{date.year} Free Software Foundation, Inc." for line in __DISCLAIMER[1:]: yield line @@ -1035,10 +970,10 @@ def gnulib_comp(config, explicit, database, subdirs, **override): yield "# other built files." yield "" yield "" - yield "# This macro should be invoked from {}, in the section".format(config.ac_file) + yield f"# This macro should be invoked from {ac_file}, in the section" yield "# \"Checks for programs\", right after AC_PROG_CC, and certainly before" yield "# any checks for libraries, header files, types and library functions." - yield "AC_DEFUN([{}_EARLY],".format(config.macro_prefix) + yield f"AC_DEFUN([{macro_prefix}_EARLY]," yield "[" yield " m4_pattern_forbid([^gl_[A-Z]])dnl the gnulib macro namespace" yield " m4_pattern_allow([^gl_ES$])dnl a valid locale name" @@ -1053,15 +988,15 @@ def gnulib_comp(config, explicit, database, subdirs, **override): if not config.gnumake and subdirs: yield " AC_REQUIRE([AM_PROG_CC_C_O])" for module in database.final_modules: - yield " # Code from module {}:".format(module.name) + yield f" # Code from module {module.name}:" lines = module.early_autoconf_snippet.split("\n") for line in filter(lambda line: line.strip(), lines): - yield " {}".format(line) + yield f" {line}" yield "])" yield "" - yield "# This macro should be invoked from {}, in the section".format(config.ac_file) + yield f"# This macro should be invoked from {ac_file}, in the section" yield "# \"Check for header files, types and library functions\"." - yield "AC_DEFUN([{}_INIT],".format(macro_prefix) + yield f"AC_DEFUN([{macro_prefix}_INIT]," yield "[" if config.libtool: yield " AM_CONDITIONAL([GL_COND_LIBTOOL], [true])" @@ -1071,13 +1006,13 @@ def gnulib_comp(config, explicit, database, subdirs, **override): yield " gl_cond_libtool=false" yield " gl_libdeps=" yield " gl_ltlibdeps=" - yield " gl_m4_base='{}'".format(config.m4_base) + yield f" gl_m4_base='{config.m4_base}'" for line in init_macro_header(config, macro_prefix=macro_prefix): yield line - yield " gl_source_base='{}'".format(config.source_base) + yield f" gl_source_base='{config.source_base}'" if "witness_c_macro" in explicit: - yield " m4_pushdef([gl_MODULE_INDICATOR_CONDITION], [{}])".format(config.witness_c_macro) - for line in autoconf_snippet_sequence(config, database, main_modules, True, False, True, macro_prefix=macro_prefix): + yield f" m4_pushdef([gl_MODULE_INDICATOR_CONDITION], [{config.witness_c_macro}])" + for line in autoconf_snippets(config, database, main_modules, True, False, True, macro_prefix=macro_prefix): yield line if "witness_c_macro" in explicit: yield " m4_popdef([gl_MODULE_INDICATOR_CONDITION])" @@ -1088,13 +1023,13 @@ def gnulib_comp(config, explicit, database, subdirs, **override): yield " gltests_ltlibdeps=" for line in init_macro_header(config, macro_prefix=(macro_prefix + "tests")): yield line - yield " gl_source_base='{}'".format(config.tests_base) + yield f" gl_source_base='{config.tests_base}'" # Define a tests witness macro that depends on the package. # PACKAGE is defined by AM_INIT_AUTOMAKE, PACKAGE_TARNAME is defined by AC_INIT. # See <http://lists.gnu.org/archive/html/automake/2009-05/msg00145.html>. yield "changequote(,)dnl" yield "".join(( - " {}tests_WITNESS=IN_`".format(macro_prefix), + f" {macro_prefix}tests_WITNESS=IN_`", "echo \"${PACKAGE-$PACKAGE_TARNAME}\"", " | ", "LC_ALL=C tr abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLMNOPQRSTUVWXYZ", @@ -1103,10 +1038,10 @@ def gnulib_comp(config, explicit, database, subdirs, **override): "`_GNULIB_TESTS", )) yield "changequote([, ])dnl" - yield " AC_SUBST([{}tests_WITNESS])".format(macro_prefix) - yield " gl_module_indicator_condition=${}tests_WITNESS".format(macro_prefix) + yield f" AC_SUBST([{macro_prefix}tests_WITNESS])" + yield f" gl_module_indicator_condition=${macro_prefix}tests_WITNESS" yield " m4_pushdef([gl_MODULE_INDICATOR_CONDITION], [$gl_module_indicator_condition])" - for line in autoconf_snippet_sequence(config, database, test_modules, True, True, True, macro_prefix=macro_prefix): + for line in autoconf_snippets(config, database, test_modules, True, True, True, macro_prefix=macro_prefix): yield line yield " m4_popdef([gl_MODULE_INDICATOR_CONDITION])" for line in init_macro_footer(config, macro_prefix=(macro_prefix + "tests")): @@ -1116,10 +1051,10 @@ def gnulib_comp(config, explicit, database, subdirs, **override): # created using libtool, because libtool already handles the dependencies. if not config.libtool: libname = config.libname.upper() - yield " {}_LIBDEPS=\"$gl_libdeps\"".format(libname) - yield " AC_SUBST([{}_LIBDEPS])".format(libname) - yield " {}_LTLIBDEPS=\"$gl_ltlibdeps\"".format(libname) - yield " AC_SUBST([{}_LTLIBDEPS])".format(libname) + yield f" {libname}_LIBDEPS=\"$gl_libdeps\"" + yield f" AC_SUBST([{libname}_LIBDEPS])" + yield f" {libname}_LTLIBDEPS=\"$gl_ltlibdeps\"" + yield f" AC_SUBST([{libname}_LTLIBDEPS])" if database.libtests: yield " LIBTESTS_LIBDEPS=\"$gltests_libdeps\"" yield " AC_SUBST([LIBTESTS_LIBDEPS])" @@ -1131,7 +1066,13 @@ def gnulib_comp(config, explicit, database, subdirs, **override): yield "" yield "# This macro records the list of files which have been installed by" yield "# gnulib-tool and may be removed by future gnulib-tool invocations." - yield "AC_DEFUN([{}_FILE_LIST], [".format(macro_prefix) - for file in sorted(set(database.main_files + database.test_files)): - yield " {}".format(file) + yield f"AC_DEFUN([{macro_prefix}_FILE_LIST], [" + test_files = set() + main_files = set(database.main_files) + for file in database.test_files: + if file.startswith("lib/"): + file = ("tests=lib/" + file[len("lib/"):]) + test_files.add(file) + for file in sorted(main_files | test_files): + yield f" {file}" yield "])" diff --git a/pygnulib/misc.py b/pygnulib/misc.py new file mode 100644 index 0000000000..275ac55bc3 --- /dev/null +++ b/pygnulib/misc.py @@ -0,0 +1,175 @@ +#!/usr/bin/python +# encoding: UTF-8 +"""miscellaneous tools and traits""" + + + +import os as _os +from collections import OrderedDict as _OrderedDict + + + +class Property(property): + """generic property""" + @staticmethod + def __fget(*args, **kwargs): + raise AttributeError("unreadable attribute") + + @staticmethod + def __fset(*args, **kwargs): + raise AttributeError("can't set attribute") + + + def __init__(self, fget=None, fset=None, doc=None, check=None): + """ + doc : any arbitrary string + fget : function to get the value + fset : function to set the value + + def fget(self): + return self.value + + def fset(self, value): + self.value = value + """ + fget = Property.__fget if fget is None else fget + fset = Property.__fset if fset is None else fset + # if not callable(fget): + # raise TypeError("fget: callable expected") + # if not callable(fset): + # raise TypeError("fset: callable expected") + super().__init__(fget=fget, fset=fset, doc=doc) + self.__check = check + self.__fget = fget + self.__fset = fset + self.__doc__ = doc + + + def __set__(self, obj, val): + if self.__check: + if not self.__check(val): + raise ValueError("value check failed") + return super().__set__(obj, val) + + +class PathProperty(Property): + """file system path property""" + def __init__(self, fget=None, fset=None, doc=None): + """ + doc : any arbitrary string + fget : function to get the value + fset : function to set the value + + def fget(self): + return self.path + + def fset(self, path): + self.path = path + """ + super().__init__(fget=fget, fset=fset, doc=doc) + + + def __set__(self, obj, val): + if not isinstance(val, str): + raise TypeError("value: str expected") + if not val: + raise ValueError("value: empty path") + val = _os.path.normpath(val) + return super().__set__(obj, val) + + + +class BitwiseProperty(Property): + """bitwise flag property""" + def __init__(self, mask, fget=None, fset=None, doc=None): + """ + mask : int bitwise mask + doc : any arbitrary string + fget : function to get the value + fset : function to set the value + + def fget(self, mask): + return bool(self.mask & mask) + + def fset(self, mask, state): + if state: + self.mask |= mask + else: + self.mask &= ~mask + """ + if not isinstance(mask, int): + raise TypeError("mask: int expected") + + def _fget(obj): + return self.__fget(obj, self.__mask) + + def _fset(obj, state): + return self.__fset(obj, self.__mask, state) + + super().__init__(doc=doc, fget=_fget, fset=_fset) + self.__mask = mask + self.__fget = fget + self.__fset = fset + + + @property + def mask(self): + return self.__mask + + + def __set__(self, obj, val): + if not isinstance(val, bool): + raise TypeError("value: bool expected") + return super().__set__(obj, val) + + + +class StringListProperty(Property): + """string sequence property""" + __TYPES = (list, tuple, set, frozenset, type({}.keys()), type({}.values())) + + def __init__(self, sorted=False, unique=False, fget=None, fset=None, doc=None): + if not isinstance(sorted, bool): + raise TypeError("sorted: bool expected") + if not isinstance(unique, bool): + raise TypeError("unique: bool expected") + super().__init__(fget=fget, fset=fset, doc=doc) + self.__sorted = sorted + self.__unique = unique + + + def __set__(self, obj, val): + if not isinstance(val, StringListProperty.__TYPES): + raise TypeError("value: iterable expected") + res = list() + for item in val: + if not isinstance(item, str): + raise TypeError("item: str expected") + res.append(item) + res = tuple(_OrderedDict.fromkeys(res) if self.__unique else res) + val = tuple(sorted(res) if self.__sorted else res) + return super().__set__(obj, val) + + + +class PathListProperty(StringListProperty): + """path sequence property""" + __TYPES = (list, tuple, set, frozenset, type({}.keys()), type({}.values())) + + + def __init__(self, sorted=False, unique=False, fget=None, fset=None, doc=None): + super().__init__(sorted=sorted, unique=unique, fget=fget, fset=fset, doc=doc) + + + def __set__(self, obj, val): + if not isinstance(val, PathListProperty.__TYPES): + raise TypeError("value: iterable expected") + res = list() + for item in val: + if not isinstance(item, str): + raise TypeError("item: str expected") + if not item: + raise TypeError("item: empty path") + item = _os.path.normpath(item) + res.append(item) + return super().__set__(obj, res) diff --git a/pygnulib/module.py b/pygnulib/module.py index 7e15427fe7..40a782e27e 100644 --- a/pygnulib/module.py +++ b/pygnulib/module.py @@ -11,39 +11,44 @@ import hashlib as _hashlib import json as _json import os as _os import re as _re +import sys as _sys -from .error import type_assert as _type_assert from .error import UnknownModuleError as _UnknownModuleError from .config import BaseConfig as _BaseConfig - - - -_ITERABLES = frozenset((list, tuple, set, frozenset, type({}.keys()), type({}.values()))) +from .misc import Property as _Property +from .misc import PathProperty as _PathProperty +from .misc import StringListProperty as _StringListProperty +from .misc import PathListProperty as _PathListProperty class BaseModule: """base module""" - _TABLE = { - "description" : (0x00, str, "Description"), - "comment" : (0x01, str, "Comment"), - "status" : (0x02, tuple, "Status"), - "notice" : (0x03, str, "Notice"), - "applicability" : (0x04, str, "Applicability"), - "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, tuple, "License"), - "maintainers" : (0x0D, tuple, "Maintainer"), - # unconditional_automake_snippet - # automake_snippet + __slots__ = ("__options", "__flags") + + + __PROPERTIES = { + "name" : None, + "description" : "", + "comment" : "", + "status" : "", + "notice" : "", + "applicability" : "", + "files" : "", + "dependencies" : "", + "early_autoconf_snippet" : "", + "autoconf_snippet" : "", + "conditional_automake_snippet" : "", + "unconditional_automake_snippet" : None, + "automake_snippet" : None, + "include_directives" : "", + "link_directives" : "", + "licenses" : "", + "maintainers" : "", + "test" : "", } - _PROPERTIES = { + __OPTIONS = { "name", "description", "comment", @@ -57,289 +62,267 @@ class BaseModule: "conditional_automake_snippet", "unconditional_automake_snippet", "automake_snippet", - "include_directive", - "link_directive", + "include_directives", + "link_directives", "licenses", "maintainers", "test", } + __FLAGS = {} __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", tuple()): - _type_assert("license", license, str) - licenses.add(license) - kwargs["licenses"] = licenses - if "maintainers" not in kwargs: - kwargs["maintainers"] = {"all"} - self.__name = name - self.__table = _collections.OrderedDict() - for (key, (_, typeid, _)) in BaseModule._TABLE.items(): - self.__table[key] = typeid(kwargs.get(key, typeid())) - - - def __repr__(self): - return "<" + self.__name + ">" + if not isinstance(name, str): + raise TypeError("name: str expected") + if not name.strip(): + raise ValueError("name: invalid name") + + self.__flags = 0 + self.__options = {} + for key in BaseModule.__OPTIONS: + value = BaseModule.__PROPERTIES[key] + if value is not None: + self.__set_option(key, value) + for key in BaseModule.__FLAGS: + state = BaseModule.__PROPERTIES[key] + mask = getattr(self.__class__, key).mask + self.__set_flags(mask, state) + self.__set_option("name", name) - - def __str__(self): - return "<" + self.__name + ">" + for (key, value) in kwargs.items(): + setattr(self, key, value) def __enter__(self): return self - def __exit__(self, exctype, excval, exctrace): - pass + def __hash__(self): + return hash(_json.dumps(dict(self.__options), sort_keys=True)) - def __hash__(self): - return hash(self.__name) ^ hash(_json.dumps(dict(self.__table), sort_keys=True)) + def __repr__(self): + module = self.__class__.__module__ + name = self.__class__.__name__ + return f"{module}.{name}[{self.name}]" def __getitem__(self, key): - _type_assert("key", key, str) - if key not in BaseModule._PROPERTIES: + if key not in BaseModule.__PROPERTIES: key = key.replace("-", "_") - if key not in BaseModule._PROPERTIES: + 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._PROPERTIES: + if key not in BaseModule.__PROPERTIES: key = key.replace("-", "_") - if key not in BaseModule._PROPERTIES: + if key not in BaseModule.__PROPERTIES: raise KeyError(repr(key)) return setattr(self, key, value) + def __get_option(self, key): + return self.__options[key] + + def __set_option(self, key, value): + self.__options[key] = value + + @property - def gnulib(self): + def gnulib_package(self): """gnulib-compatible module textual representation""" def _gnulib(): yield "Description:" - yield self.__table["description"] + yield self.description yield "Comment:" - yield self.__table["comment"] + yield self.comment yield "Status:" - for status in sorted(self.__table["status"]): + for status in sorted(self.status): yield status yield "Notice:" - yield self.__table["notice"] + yield self.notice yield "Applicability:" - yield self.__table["applicability"] + yield self.applicability yield "Files:" - for file in sorted(self.__table["files"]): + for file in self.files: yield file yield "Depends-on:" - for (module, condition) in self.__table["dependencies"]: + for (module, condition) in self.dependencies: yield "{} {}".format(module, condition) yield "configure.ac-early:" - yield self.__table["early_autoconf_snippet"] + yield self.early_autoconf_snippet yield "configure.ac:" - yield self.__table["autoconf_snippet"] + yield self.autoconf_snippet yield "Makefile.am:" - yield self.__table["conditional_automake_snippet"] + yield self.conditional_automake_snippet yield "Include:" - yield self.__table["include_directive"] + for include in self.include_directives: + yield include yield "Link:" - yield self.__table["link_directive"] + for link in self.link_directives: + yield link yield "License:" - for license in sorted(self.__table["licenses"]): + for license in self.licenses: yield license yield "Maintainer:" - for maintainer in self.__table["maintainers"]: + for maintainer in maintainers: yield maintainer return "\n".join(_gnulib()) - @property - def name(self): - """name""" - return self.__name - - @name.setter - def name(self, value): - _type_assert("name", value, str) - self.__name = value - - - @property - def description(self): - """description""" - return self.__table["description"] - - @description.setter - def description(self, value): - _type_assert("description", value, str) - self.__table["description"] = value - - - @property - def comment(self): - """comment""" - return self.__table["comment"] - - @comment.setter - def comment(self, value): - _type_assert("comment", value, str) - self.__table["comment"] = value - - - @property - def status(self): - """status""" - return self.__table["status"] - - @status.setter - def status(self, value): - _type_assert("status", value, _ITERABLES) - result = set() - for item in value: - _type_assert("status", item, str) - result.add(item) - self.__table["status"] = tuple(result) - - - @property - def obsolete(self): - """module is obsolete?""" - return "obsolete" in self.status - - @property - def cxx_test(self): - """module is a C++ test?""" - return "c++-test" in self.status - - @property - def longrunning_test(self): - """module is a C++ test?""" - return "longrunning-test" in self.status - - @property - def privileged_test(self): - """module is a privileged test?""" - return "privileged-test" in self.status - - @property - def unportable_test(self): - """module is a unportable test?""" - return "unportable-test" in self.status - - - @property - def notice(self): - """notice""" - return self.__table["notice"] - - @notice.setter - def notice(self, value): - _type_assert("notice", value, str) - self.__table["notice"] = value - - - @property - def applicability(self): - """applicability (usually "main" or "tests")""" - return self.__table["applicability"] - - @applicability.setter - def applicability(self, value): - _type_assert("applicability", value, str) - if value not in ("all", "main", "tests"): - raise ValueError("applicability: \"main\", \"tests\" or \"all\"") - self.__table["applicability"] = value - - - @property - def files(self): - """file dependencies iterator (set of strings)""" - return self.__table["files"] - - @files.setter - def files(self, value): - _type_assert("files", value, _ITERABLES) - result = set() - for item in value: - _type_assert("file", item, str) - result.add(item) - self.__table["files"] = tuple(result) - - - @property + name = _Property( + fget=lambda self: self.__get_option("name"), + fset=lambda self, string: self.__set_option("name", string), + check=lambda value: isinstance(value, str) and value, + doc="name", + ) + description = _Property( + fget=lambda self: self.__get_option("description"), + fset=lambda self, string: self.__set_option("description", string), + check=lambda value: isinstance(value, str), + doc="description", + ) + comment = _Property( + fget=lambda self: self.__get_option("comment"), + fset=lambda self, string: self.__set_option("comment", string), + check=lambda value: isinstance(value, str), + doc="comment", + ) + status = _StringListProperty( + sorted=True, + unique=True, + fget=lambda self: self.__get_option("comment"), + fset=lambda self, string: self.__set_option("comment", string), + doc="status list", + ) + obsolete = _Property( + fget=lambda self: "obsolete" in self.status, + doc="module is obsolete?", + ) + cxx_test = _Property( + fget=lambda self: "c++-test" in self.status, + doc="module is a C++ test?", + ) + longrunning_test = _Property( + fget=lambda self: "longrunning-test" in self.status, + doc="module is a longrunning test?", + ) + privileged_test = _Property( + fget=lambda self: "privileged-test" in self.status, + doc="module is a privileged test?", + ) + unportable_test = _Property( + fget=lambda self: "unportable-test" in self.status, + doc="module is an unportable test?", + ) + mask = _Property( + fget=lambda self: ((0, (1 << 0))[self.obsolete] + |(0, (1 << 1))[self.cxx_test] + |(0, (1 << 2))[self.longrunning_test] + |(0, (1 << 3))[self.privileged_test] + |(0, (1 << 4))[self.unportable_test]), + doc="module acceptibility mask", + ) + notice = _Property( + fget=lambda self: self.__get_option("notice"), + fset=lambda self, string: self.__set_option("notice", string), + check=lambda value: isinstance(value, str), + doc="module notice or disclaimer", + ) + applicability = _Property( + fget=lambda self: self.__get_option("applicability"), + fset=lambda self, string: self.__set_option("applicability", string), + check=lambda value: isinstance(value, str) and value in {"main", "tests", "all"}, + doc="applicability ('main', 'tests' or 'all')", + ) + files = _PathListProperty( + sorted=True, + unique=True, + fget=lambda self: self.__get_option("files"), + fset=lambda self, string: self.__set_option("files", string), + doc="file dependencies", + ) + early_autoconf_snippet = _Property( + fget=lambda self: self.__get_option("early_autoconf_snippet"), + fset=lambda self, string: self.__set_option("early_autoconf_snippet", string), + check=lambda value: isinstance(value, str), + doc="early configure.ac snippet", + ) + autoconf_snippet = _Property( + fget=lambda self: self.__get_option("autoconf_snippet"), + fset=lambda self, string: self.__set_option("autoconf_snippet", string), + check=lambda value: isinstance(value, str), + doc="configure.ac snippet", + ) + conditional_automake_snippet = _Property( + fget=lambda self: self.__get_option("conditional_automake_snippet"), + fset=lambda self, string: self.__set_option("conditional_automake_snippet", string), + check=lambda value: isinstance(value, str), + doc="configure.ac snippet", + ) + automake_snippet = _Property( + fget=lambda self: "\n".join((self.conditional_automake_snippet, self.unconditional_automake_snippet)), + doc="full automake snippet (conditional + unconditional parts)", + ) + include_directives = _StringListProperty( + fget=lambda self: self.__get_option("include_directives"), + fset=lambda self, string: self.__set_option("include_directives", string), + doc="include directive", + ) + link_directives = _StringListProperty( + fget=lambda self: self.__get_option("link_directives"), + fset=lambda self, string: self.__set_option("link_directives", string), + doc="link directive", + ) + licenses = _StringListProperty( + sorted=True, + unique=True, + fget=lambda self: self.__get_option("licenses"), + fset=lambda self, name: self.__set_option("licenses", name), + doc="acceptable licenses for modules", + ) + maintainers = _StringListProperty( + sorted=False, + unique=True, + fget=lambda self: self.__get_option("maintainers"), + fset=lambda self, name: self.__set_option("maintainers", name), + doc="module maintainers list", + ) + test = _Property( + fget=lambda self: self.__get_option("test"), + fset=lambda self, name: self.__set_option("test", name), + check=lambda value: isinstance(value, bool), + doc="module is a test?", + ) + + + @_Property def dependencies(self): """dependencies iterator (name, condition)""" - entries = set() - for line in self.__table["dependencies"]: - line = line.replace("\t", " ").strip() - index = line.find(" ") - if index == -1: - module = line - condition = "" - else: - module = line[:index].strip() - condition = line[index:].strip() - if condition.startswith("["): - condition = condition[1:] - if condition.endswith("]"): - condition = condition[:-1] - if not condition: - condition = None - entries.add((module, condition)) - return tuple(entries) + return self.__options["dependencies"] @dependencies.setter def dependencies(self, value): - _type_assert("files", value, _ITERABLES) - result = set() - for (name, condition) in value: - _type_assert("name", name, str) - _type_assert("condition", condition, str) - result.add((name, condition)) - self.__table["dependencies"] = tuple(result) - - - @property - def early_autoconf_snippet(self): - """early configure.ac snippet""" - return self.__table["early_autoconf_snippet"] - - @early_autoconf_snippet.setter - def early_autoconf_snippet(self, value): - _type_assert("early_autoconf_snippet", value, str) - self.__table["early_autoconf_snippet"] = value - - - @property - def autoconf_snippet(self): - """configure.ac snippet""" - return self.__table["autoconf_snippet"] - - @autoconf_snippet.setter - def autoconf_snippet(self, value): - _type_assert("autoconf_snippet", value, str) - self.__table["autoconf_snippet"] = value - - - @property - def conditional_automake_snippet(self): - """Makefile.am snippet that can be put inside Automake conditionals""" - return self.__table["conditional_automake_snippet"] - - @conditional_automake_snippet.setter - def conditional_automake_snippet(self, value): - _type_assert("conditional_automake_snippet", value, str) - self.__table["conditional_automake_snippet"] = value - - - @property + result = [] + types = (list, tuple, set, frozenset, type({}.keys()), type({}.values())) + if not isinstance(value, types): + raise TypeError("value: iterable expected") + for item in value: + if not isinstance(value, (list, tuple)): + raise TypeError("item: pair expected") + (module, condition) = item + if not isinstance(module, str): + raise TypeError("module: str expected") + if condition is not None and not isinstance(condition, str): + raise TypeError("condition: str or None expected") + result.append((module, condition)) + self.__options["dependencies"] = tuple(result) + + + @_Property def unconditional_automake_snippet(self): """Makefile.am snippet that must stay outside of Automake conditionals""" result = "" @@ -403,78 +386,6 @@ class BaseModule: return result - @property - def automake_snippet(self): - """full automake snippet (conditional + unconditional parts)""" - return "\n".join((self.conditional_automake_snippet, self.unconditional_automake_snippet)) - - - @property - def include_directive(self): - """include directive""" - value = self.__table["include_directive"] - if value.startswith("<") or value.startswith("\""): - return "#include {0}".format(value) - return self.__table["include_directive"] - - @include_directive.setter - def include_directive(self, value): - _type_assert("include_directive", value, str) - self.__table["include_directive"] = value - - - @property - def link_directive(self): - """link directive""" - return self.__table["link_directive"] - - @link_directive.setter - def link_directive(self, value): - _type_assert("link_directive", value, str) - self.__table["link_directive"] = value - - - @property - def licenses(self): - """licenses set""" - return self.__table["licenses"] - - @licenses.setter - def licenses(self, value): - _type_assert("licenses", value, _ITERABLES) - result = set() - for item in value: - _type_assert("license", item, str) - result.add(value) - self.__table["licenses"] = tuple(result) - - - @property - def maintainers(self): - """maintainers""" - return self.__table["maintainers"] - - @maintainers.setter - def maintainers(self, value): - _type_assert("maintainers", value, _ITERABLES) - result = set() - for item in value: - _type_assert("maintainer", item, str) - result.add(item) - self.__table["maintainers"] = tuple(result) - - - @property - def test(self): - """module is tests-related?""" - return self.__table["test"] - - @test.setter - def test(self, value): - _type_assert("test", value, bool) - self.__table["test"] = value - - 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 @@ -504,37 +415,40 @@ class BaseModule: def items(self): """a set-like object providing a view on module items""" - for key in BaseModule._PROPERTIES: + for key in BaseModule.__PROPERTIES: yield (key, self[key]) + @classmethod def keys(self): """a set-like object providing a view on module keys""" - for (key, _) in self.items(): + for key in BaseModule.__PROPERTIES: yield key def values(self): """a set-like object providing a view on module values""" - for (_, value) in self.items(): - yield value + for key in BaseModule.__PROPERTIES: + yield self[key] def __lt__(self, value): - _type_assert("value", value, BaseModule) - return self.name < value.name + if value is not None: + return self.name < value.name + return False def __le__(self, value): return self.__lt__(value) or self.__eq__(value) def __eq__(self, value): - _type_assert("value", value, BaseModule) - if self.name != value.name: - return False - for key in BaseModule._PROPERTIES: - if self[key] != value[key]: + if value is not None: + if self.name != value.name: return False - return True + for key in BaseModule.__PROPERTIES: + if self[key] != value[key]: + return False + return True + return False def __ne__(self, value): return not self.__eq__(value) @@ -547,6 +461,108 @@ class BaseModule: +class FileModule(BaseModule): + """text-based module""" + __slots__ = ("__path") + + + __DEPENDENCY = _re.compile(r"(\S+)(?:\s+(\[.*?\]))?$", _re.M) + __STRING = lambda text: text.strip() + __MULTILINE = lambda text: tuple(filter( + lambda line: line.strip() and not line.strip().startswith("#"), + [line.strip() for line in text.strip().splitlines()], + )) + __INCLUDE_DIRECTIVES = lambda text: tuple(filter( + lambda line: line.strip(), + [line.strip() for line in text.strip().splitlines()], + )) + __DEPENDENCIES = lambda text: FileModule.__DEPENDENCY.findall("\n".join(FileModule.__MULTILINE(text))) + __MAINTAINERS = lambda text: tuple(filter( + lambda line: line.strip() and not line.strip().startswith("#"), + {line.strip() for line in text.split((",", "\n")["\n" in text.strip()])}, + )) + __TABLE = { + "Description": ( + "description", + __STRING, + ), + "Comment": ( + "comment", + __STRING, + ), + "Status": ( + "status", + __MULTILINE, + ), + "Notice": ( + "notice", + __STRING, + ), + "Applicability": ( + "applicability", + __STRING, + ), + "Files": ( + "files", + __MULTILINE, + ), + "Depends-on": ( + "dependencies", + __DEPENDENCIES, + ), + "configure.ac-early": ( + "early_autoconf_snippet", + __STRING, + ), + "configure.ac": ( + "autoconf_snippet", + __STRING, + ), + "Makefile.am": ( + "conditional_automake_snippet", + __STRING, + ), + "Include": ( + "include_directives", + __INCLUDE_DIRECTIVES, + ), + "Link": ( + "link_directives", + __MULTILINE, + ), + "License": ( + "licenses", + __MULTILINE, + ), + "Maintainer": ( + "maintainers", + __MAINTAINERS, + ), + } + __PATTERN = _re.compile("({}):".format("|".join(__TABLE))) + + + path = _Property( + fget=lambda self: self.__path, + doc="module file path", + ) + + + def __init__(self, path, name, **kwargs): + if not isinstance(path, str): + raise TypeError("path: str expected") + if not isinstance(name, str): + raise TypeError("name: str expected") + with _codecs.open(path, "rb", "UTF-8") as stream: + match = FileModule.__PATTERN.split(stream.read())[1:] + for (group, text) in zip(match[::2], match[1::2]): + (key, hook) = FileModule.__TABLE[group] + kwargs.setdefault(key, hook(text)) + super().__init__(name=name, **kwargs) + self.__path = path + + + class _DummyModuleMeta(type): __INSTANCE = None __PROPERTIES = { @@ -559,8 +575,8 @@ class _DummyModuleMeta(type): "dependencies": tuple(), "early_autoconf_snippet": "", "autoconf_snippet": "", - "include_directive": "", - "link_directive": "", + "include_directives": "", + "link_directives": "", "licenses": tuple({"public domain"}), "maintainers": tuple({"all"}), "automake_snippet": "lib_SOURCES += dummy.c", @@ -568,15 +584,18 @@ class _DummyModuleMeta(type): "unconditional_automake_snippet": "", } + def __new__(mcs, name, parents, attributes): - cls = super().__new__(mcs, name, parents, attributes) for (key, value) in _DummyModuleMeta.__PROPERTIES.items(): - setattr(cls, key, property(lambda cls, value=value: value)) - return cls + fget=lambda self, value=value: value + doc = getattr(BaseModule, key).__doc__ + attributes[key] = _Property(fget=fget, doc=doc) + return super().__new__(mcs, name, parents, attributes) + def __call__(cls, *args, **kwargs): if _DummyModuleMeta.__INSTANCE is None: - _DummyModuleMeta.__INSTANCE = super(_DummyModuleMeta, cls).__call__(*args, **kwargs) + _DummyModuleMeta.__INSTANCE = super().__call__(*args, **kwargs) return _DummyModuleMeta.__INSTANCE @@ -587,47 +606,50 @@ class DummyModule(BaseModule, metaclass=_DummyModuleMeta): -class FileModule(BaseModule): - """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))) +class _GnulibModuleMeta(type): + def __new__(mcs, name, parents, attributes): + for key in BaseModule.keys(): + if key in attributes: + continue + fget = lambda self, key=key: self.__getitem__(key) + doc = getattr(BaseModule, key).__doc__ + attributes[key] = _Property(fget=fget, doc=doc) + return super().__new__(mcs, name, parents, attributes) - 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 = {} - 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) +class GnulibModule(FileModule, metaclass=_GnulibModuleMeta): + """read-only gnulib standard module""" + __slots__ = ("__cache", "__hash", "__mask", "__path", "__test") + __OBSOLETE = (1 << 0) + __CXX_TEST = (1 << 1) + __LONGRUNNING_TEST = (1 << 2) + __PRIVILEGED_TEST = (1 << 3) + __UNPORTABLE_TEST = (1 << 4) + -class GnulibModule(FileModule): - """read-only gnulib standard module""" def __init__(self, path, name): - super().__init__(path=path, name=name) + super(FileModule, self).__init__(name=name) + try: + module = FileModule(path=path, name=name, test=name.endswith("-tests")) + except FileNotFoundError: + raise _UnknownModuleError(name) + self.__cache = {_sys.intern(k):v for (k,v) in module.items()} self.__hash = super().__hash__() + self.__mask = module.mask + self.__path = module.path + self.__test = module.test - def __setattr__(self, key, value): - if key in BaseModule._PROPERTIES: - raise AttributeError("can't set property") - super().__setattr__(key, value) + def __repr__(self): + module = self.__class__.__module__ + name = self.__class__.__name__ + return f"{module}.{name}{{{self.name}}}" + + + def __getitem__(self, key): + return self.__cache[key] def __hash__(self): @@ -636,41 +658,63 @@ class GnulibModule(FileModule): def __eq__(self, other): if isinstance(other, GnulibModule): - return hash(self) == hash(other) + return self.__hash == hash(other) return super().__eq__(other) - @property + obsolete = _Property( + fget=lambda self: bool(self.__mask & GnulibModule.__OBSOLETE), + doc="module is obsolete?", + ) + cxx_test = _Property( + fget=lambda self: bool(self.__mask & GnulibModule.__CXX_TEST), + doc="module is a C++ test?", + ) + longrunning_test = _Property( + fget=lambda self: bool(self.__mask & GnulibModule.__LONGRUNNING_TEST), + doc="module is a longrunning test?", + ) + privileged_test = _Property( + fget=lambda self: bool(self.__mask & GnulibModule.__PRIVILEGED_TEST), + doc="module is a privileged test?", + ) + unportable_test = _Property( + fget=lambda self: bool(self.__mask & GnulibModule.__UNPORTABLE_TEST), + doc="module is an unportable test?", + ) + mask = _Property( + fget=lambda self: self.__mask, + doc="module acceptibility mask", + ) + test = _Property( + fget=lambda self: self.name.endswith("-tests"), + doc="module is tests-related?", + ) + path = _Property( + fget=lambda self: self.__path, + doc="module file path", + ) + + + @_Property def applicability(self): - """applicability (usually "main" or "tests")""" + """WAGH applicability (usually "main" or "tests")""" default = "tests" if self.test else "main" - result = super().applicability - return result if result else default - - - @property - def test(self): - """module is tests-related?""" - return self.name.endswith("-tests") + current = self.__cache["applicability"] + return current if current else default class TransitiveClosure: """transitive closure table""" - def __init__(self, lookup, config, tests): + def __init__(self, lookup, modules, mask, gnumake, tests=False): if not callable(lookup): raise TypeError("lookup: callable expected") - _type_assert("config", config, _BaseConfig) - _type_assert("tests", tests, bool) + demanders = _collections.defaultdict(dict) + dependencies = _collections.defaultdict(dict) def _exclude(module): - return any(( - (not config.obsolete and module.obsolete), - (not config.cxx_tests and module.cxx_test), - (not config.longrunning_tests and module.longrunning_test), - (not config.privileged_tests and module.privileged_test), - (not config.unportable_tests and module.unportable_test), - )) + return mask != module.mask def _lookup(module): if not (module is None or isinstance(module, BaseModule)): @@ -687,26 +731,31 @@ class TransitiveClosure: dependencies[dependency][demander] = condition current.add(dependency) + testdb = {} + mapping = {} current = set() previous = set() - demanders = _collections.defaultdict(dict) - dependencies = _collections.defaultdict(dict) - for module in config.modules: + for module in modules: dependency = lookup(module) _update(None, dependency, None) while current != previous: previous.update(current) for demander in previous: - if tests and not demander.test: + if tests and not demander.test and testdb.get(demander.name, None) is None: try: - dependency = lookup("{}-tests".format(demander.name)) + name = (demander.name + "-tests") + path = (demander.path + "-tests") + dependency = GnulibModule(name=name, path=path) if not _exclude(dependency): - _update(None, dependency, None) + _update(demander, dependency, None) + testdb[demander.name] = True + else: + testdb[demander.name] = False except _UnknownModuleError: - pass # ignore non-existent tests + testdb[demander.name] = False for (dependency, condition) in demander.dependencies: dependency = lookup(dependency) - if config.gnumake and condition and condition.startswith("if "): + if gnumake and condition and condition.startswith("if "): # A module whose Makefile.am snippet contains a reference to an # automake conditional. If we were to use it conditionally, we # would get an error @@ -724,10 +773,11 @@ class TransitiveClosure: conditional = set() unconditional = set() for (dependency, demanders) in self.__dependencies.items(): - for (demander, _) in demanders.items(): + for demander in demanders: if demander is None: - unconditional.add(demander) + unconditional.add(dependency) break + previous = set() current = set(unconditional) while previous != current: @@ -786,14 +836,9 @@ class TransitiveClosure: demanders = _collections.defaultdict(dict) dependencies = _collections.defaultdict(dict) collection = _ast.literal_eval(string) - _type_assert("collection", collection, dict) for key in collection: - _type_assert("key", key, str) value = collection[key] - _type_assert("value", value, dict) for (subkey, subvalue) in value.items(): - _type_assert("key", key, str) - _type_assert("dict", value, dict) (dependency, demander, condition) = (key, subkey, subvalue) dependency = self.__lookup(dependency) demander = self.__lookup(demander) @@ -852,7 +897,10 @@ class Database: def __init__(self, lookup, config): if not callable(lookup): raise TypeError("lookup: callable expected") - _type_assert("config", config, _BaseConfig) + + mask = config.mask + gnumake = config.gnumake + lookup = lambda module, lookup=lookup: module if isinstance(module, BaseModule) else lookup(module) def _applicability(module): return module.applicability in ({"main", "all"}, {"main"})[config.tests] @@ -885,26 +933,23 @@ class Database: return True return False - base_closure = TransitiveClosure(lookup, config, False) - full_closure = TransitiveClosure(lookup, config, True) + # Perform a transitive closure for modules from the configuration. + # The result of this transitive closure is a set of main modules. + explicit_modules = {lookup(module) for module in config.modules} + base_closure = TransitiveClosure(lookup, explicit_modules, mask, gnumake) + full_closure = TransitiveClosure(lookup, set(base_closure), mask, gnumake, True) + + # Once the full transitive closure is completed, populate the database. main_modules = set(base_closure) - explicit_modules = set() - for module in full_closure: - if module.name in config.modules: - explicit_modules.add(module) final_modules = set(full_closure) if config.tests else main_modules - test_modules = (final_modules - set(filter(_applicability, main_modules))) + test_modules = (final_modules - set(filter(_applicability, sorted(main_modules)))) libtests = _libtests(test_modules) if _dummy(main_modules): main_modules.add(DummyModule()) if _dummy(test_modules) and libtests: test_modules.add(DummyModule()) main_files = _files(main_modules) - test_files = set() - for file in _files(test_modules): - if file.startswith("lib/"): - file = ("tests=lib/" + file[len("lib/"):]) - test_files.add(file) + test_files = _files(test_modules) self.__libtests = libtests self.__closure = full_closure @@ -916,6 +961,14 @@ class Database: self.__test_files = tuple(sorted(test_files)) + def __iter__(self): + def _iter(): + for dependency in self.__closure: + for (demander, condition) in self.__closure.demanders(dependency): + yield (dependency, demander, condition) + return iter(sorted(_iter())) + + def conditional(self, module): """ Test whether module is a conditional dependency. @@ -936,12 +989,12 @@ class Database: def demanders(self, module): """For each demander which requires the module yield the demander and the corresponding condition.""" - return self.__closure.demanders(module) + return sorted(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) + return sorted(self.__closure.dependencies(module)) @property diff --git a/pygnulib/parser.py b/pygnulib/parser.py index 0ef96a3ee0..055798ecc5 100644 --- a/pygnulib/parser.py +++ b/pygnulib/parser.py @@ -647,6 +647,7 @@ class CommandLine: "specify the library name; defaults to 'libgnu'", ), "action": _Option, + "dest": "libname", "metavar": "LIBRARY", }), (["--source-base"], { @@ -942,7 +943,10 @@ class CommandLine: fmt = "--{0}: expected at least one argument" self.__parser.error(fmt.format(mode)) verbosity = namespace.pop("verbosity", 0) + namespace.pop("no_changelog", None) + options = dict(namespace) + namespace.setdefault("root", ".") options.setdefault("dry_run", False) namespace["overrides"] = list(reversed(namespace.get("overrides", []))) return (namespace, mode, verbosity, options) diff --git a/pygnulib/tools.py b/pygnulib/tools.py index a169393c0a..aff7742aaf 100644 --- a/pygnulib/tools.py +++ b/pygnulib/tools.py @@ -4,13 +4,9 @@ -import os as _os import subprocess as _sp -from .error import type_assert as _type_assert - - class _PipeMeta(type): __INSTANCE = None @@ -29,11 +25,6 @@ class Pipe(metaclass=_PipeMeta): class Executable: """command-line program or script""" def __init__(self, name, path=None, encoding=None): - _type_assert("name", name, str) - if path is not None: - _type_assert("path", path, str) - if encoding is not None: - _type_assert("encoding", encoding, str) self.__name = name self.__path = path self.__encoding = encoding diff --git a/pygnulib/vfs.py b/pygnulib/vfs.py index 537cea2e98..bcd55f572a 100644 --- a/pygnulib/vfs.py +++ b/pygnulib/vfs.py @@ -12,7 +12,6 @@ import tempfile as _tempfile import subprocess as _sp -from .error import type_assert as _type_assert from .error import UnknownModuleError as _UnknownModuleError from .module import DummyModule as _DummyModule from .module import GnulibModule as _GnulibModule @@ -21,19 +20,21 @@ from .module import GnulibModule as _GnulibModule class BaseVFS: """gnulib generic virtual file system""" - def __init__(self, prefix, **table): - _type_assert("prefix", prefix, str) + __slots__ = ("__origin", "__root", "__table") + + + def __init__(self, origin, **table): self.__table = {} for (key, value) in table.items(): - _type_assert(key, value, str) self.__table[key] = _os.path.normpath(value) - self.__prefix = prefix + self.__origin = _os.path.normpath(origin) + self.__root = _os.path.abspath(origin) def __repr__(self): module = self.__class__.__module__ name = self.__class__.__name__ - return "{}.{}{{{}}}".format(module, name, repr(self.__prefix)) + return "{}.{}{{{}}}".format(module, name, repr(self.__origin)) def __enter__(self): @@ -45,15 +46,13 @@ class BaseVFS: def __contains__(self, name): - path = _os.path.normpath(name) if _os.path.isabs(name): raise ValueError("name must be a relative path") - path = _os.path.join(self.absolute, self[name]) + path = _os.path.join(self.__root, self[name]) return _os.path.exists(path) def __getitem__(self, name): - _type_assert("name", name, str) parts = [] replaced = False name = _os.path.normpath(name) @@ -67,13 +66,11 @@ class BaseVFS: part = self.__table[part] replaced = True parts += [part] - name = _os.path.sep.join(parts) - return _os.path.normpath(name) + return _os.path.sep.join(parts) def __setitem__(self, src, dst): for name in (src, dst): - _type_assert("name", name, str) if _os.path.isabs(name): raise ValueError("name cannot be an absoule path") src = _os.path.normpath(src) @@ -82,15 +79,15 @@ class BaseVFS: @property - def relative(self): - """BaseVFS VFS name""" - return self.__prefix + def origin(self): + """origin VFS path""" + return self.__origin @property - def absolute(self): + def root(self): """absolute VFS path""" - return _os.path.abspath(self.__prefix) + return self.__root @@ -114,8 +111,6 @@ def lookup(name, primary, secondary, patch="patch"): NOTE: It is up to the caller to unlink files obtained after dynamic patching. """ - _type_assert("primary", primary, BaseVFS) - _type_assert("secondary", secondary, BaseVFS) if name in secondary: return (secondary, name) diff = "{}.diff".format(name) @@ -141,14 +136,14 @@ def lookup(name, primary, secondary, patch="patch"): def mkdir(root, name): """Create a leaf directory and all intermediate ones recursively.""" root = BaseVFS(".") if root is None else root - path = name if _os.path.isabs(name) else _os.path.join(root.absolute, root[name]) + path = name if _os.path.isabs(name) else _os.path.join(root.root, root[name]) _os.makedirs(root[name], exist_ok=True) def backup(root, name): """Backup the given file.""" root = BaseVFS(".") if root is None else root - original_path = _os.path.join(root.absolute, root[name]) + original_path = _os.path.join(root.root, root[name]) backup_path = "{}~".format(original_path) try: _os.unlink(backup_path) @@ -163,9 +158,9 @@ def compare(lhs_root, lhs_name, rhs_root, rhs_name): rhs_root = BaseVFS(".") if rhs_root is None else rhs_root (lhs_path, rhs_path) = (lhs_name, rhs_name) if not _os.path.isabs(lhs_name): - lhs_path = _os.path.join(lhs_root.absolute, lhs_root[lhs_name]) + lhs_path = _os.path.join(lhs_root.root, lhs_root[lhs_name]) if not _os.path.isabs(rhs_name): - rhs_path = _os.path.join(rhs_root.absolute, rhs_root[rhs_name]) + rhs_path = _os.path.join(rhs_root.root, rhs_root[rhs_name]) return _filecmp.cmp(lhs_path, rhs_path, shallow=False) @@ -181,9 +176,9 @@ def copy(src_root, src_name, dst_root, dst_name): mkdir(dst_root, _os.path.dirname(dst_name)) (src_path, dst_path) = (src_name, dst_name) if not _os.path.isabs(src_name): - src_path = _os.path.join(src_root.absolute, src_root[src_name]) + src_path = _os.path.join(src_root.root, src_root[src_name]) if not _os.path.isabs(dst_name): - dst_path = _os.path.join(dst_root.absolute, dst_root[dst_name]) + dst_path = _os.path.join(dst_root.root, dst_root[dst_name]) with _codecs.open(src_path, "rb") as istream: with _codecs.open(dst_path, "wb") as ostream: while 1: @@ -196,7 +191,7 @@ def copy(src_root, src_name, dst_root, dst_name): def exists(root, name): """Check whether the given file exists.""" root = BaseVFS(".") if root is None else root - path = name if _os.path.isabs(name) else _os.path.join(root.absolute, root[name]) + path = name if _os.path.isabs(name) else _os.path.join(root.root, root[name]) return _os.path.exists(path) @@ -212,9 +207,9 @@ def hardlink(src_root, src_name, dst_root, dst_name): mkdir(dst_root, _os.path.dirname(dst_name)) (src_path, dst_path) = (src_name, dst_name) if not _os.path.isabs(src_name): - src_path = _os.path.join(src_root.absolute, src_root[src_name]) + src_path = _os.path.join(src_root.root, src_root[src_name]) if not _os.path.isabs(dst_name): - dst_path = _os.path.join(dst_root.absolute, dst_root[dst_name]) + dst_path = _os.path.join(dst_root.root, dst_root[dst_name]) _os.link(src_path, dst_path) @@ -229,16 +224,16 @@ def move(src_root, src_name, dst_root, dst_name): mkdir(dst_root, _os.path.dirname(dst_name)) (src_path, dst_path) = (src_name, dst_name) if not _os.path.isabs(src_name): - src_path = _os.path.join(src_root.absolute, src_root[src_name]) + src_path = _os.path.join(src_root.root, src_root[src_name]) if not _os.path.isabs(dst_name): - dst_path = _os.path.join(dst_root.absolute, dst_root[dst_name]) + dst_path = _os.path.join(dst_root.root, dst_root[dst_name]) _os.rename(src_path, dst_path) def iostream(root, name, mode="r", encoding=None): """Open file and return a stream. Raise IOError upon failure.""" root = BaseVFS(".") if root is None else root - path = name if _os.path.isabs(name) else _os.path.join(root.absolute, root[name]) + path = name if _os.path.isabs(name) else _os.path.join(root.root, root[name]) return _codecs.open(path, mode, encoding) @@ -246,7 +241,7 @@ def readlink(root, name): """Obtain the path to which the symbolic link points.""" root = BaseVFS(".") if root is None else root mkdir(root, _os.path.dirname(name)) - path = name if _os.path.isabs(name) else _os.path.join(root.absolute, root[name]) + path = name if _os.path.isabs(name) else _os.path.join(root.root, root[name]) return _os.readlink(path) @@ -262,16 +257,16 @@ def symlink(src_root, src_name, dst_root, dst_name, relative=True): if not relative: (src_path, dst_path) = (src_name, dst_name) if not _os.path.isabs(src_name): - src_path = _os.path.join(src_root.absolute, src_root[src_name]) + src_path = _os.path.join(src_root.root, src_root[src_name]) if not _os.path.isabs(dst_name): - dst_path = _os.path.join(dst_root.absolute, dst_root[dst_name]) + dst_path = _os.path.join(dst_root.root, dst_root[dst_name]) else: - src_path = _os.path.join(src_root.relative, src_root[src_name]) - dst_path = _os.path.join(dst_root.relative, dst_root[dst_name]) + src_path = _os.path.join(src_root.origin, src_root[src_name]) + dst_path = _os.path.join(dst_root.origin, dst_root[dst_name]) prefix = _os.path.relpath(_os.path.dirname(src_path), _os.path.dirname(dst_path)) suffix = _os.path.basename(src_root[src_name]) src_path = _os.path.join(prefix, suffix) - dst_path = _os.path.join(dst_root.absolute, dst_root[dst_name]) + dst_path = _os.path.join(dst_root.root, dst_root[dst_name]) _os.symlink(src_path, dst_path) @@ -279,13 +274,16 @@ def unlink(root, name, backup=True): """Unlink a file, backing it up if necessary.""" root = BaseVFS(".") if root is None else root mkdir(root, _os.path.dirname(name)) - path = name if _os.path.isabs(name) else _os.path.join(root.absolute, root[name]) + path = name if _os.path.isabs(name) else _os.path.join(root.root, root[name]) _os.unlink(path) class GnulibGitVFS(BaseVFS): """gnulib git repository""" + __slots__ = ("__cache", "__modules") + + _EXCLUDE = { "." : str.startswith, "~" : str.endswith, @@ -299,23 +297,23 @@ class GnulibGitVFS(BaseVFS): } - def __init__(self, prefix, **table): - super().__init__(prefix, **table) + def __init__(self, origin): + super().__init__(origin=origin) self.__cache = {"dummy": _DummyModule()} - if not _os.path.exists(self.absolute): - raise FileNotFoundError(self.absolute) - if not _os.path.isdir(self.absolute): - raise NotADirectoryError(self.absolute) - if not _os.path.isdir(_os.path.join(self.absolute, ".git")): + self.__modules = _os.path.join(self.root, "modules") + if not _os.path.exists(self.root): + raise FileNotFoundError(self.root) + if not _os.path.isdir(self.root): + raise NotADirectoryError(self.root) + if not _os.path.isdir(_os.path.join(self.root, ".git")): raise TypeError("{} is not a gnulib repository".format(prefix)) def module(self, name): """instantiate a module""" - _type_assert("name", name, str) if name in self.__cache: return self.__cache[name] - path = _os.path.join(self.absolute, self["modules"], name) + path = _os.path.join(self.__modules, name) try: self.__cache[name] = _GnulibModule(path=path, name=name) return self.__cache[name] @@ -325,8 +323,7 @@ class GnulibGitVFS(BaseVFS): def modules(self): """iterate over all available modules""" - prefix = _os.path.join(self.absolute, self["modules"]) - for root, _, files in _os.walk(prefix): + for root, _, files in _os.walk(self.__modules): names = [] for name in files: exclude = False @@ -338,5 +335,5 @@ class GnulibGitVFS(BaseVFS): names += [name] for name in names: path = _os.path.join(root, name) - name = path[len(prefix) + 1:] + name = path[len(self.__modules) + 1:] yield self.module(name) -- 2.39.5