From 58d224677bfe9f617050868cd0253092ee57db0e Mon Sep 17 00:00:00 2001 From: Alberto Miranda Date: Fri, 20 May 2022 15:21:22 +0200 Subject: [PATCH] Namespaces considered when formatting keywords --- genopts/genopts.py | 315 +++++++++++++++++++++++++++++---------------- 1 file changed, 201 insertions(+), 114 deletions(-) diff --git a/genopts/genopts.py b/genopts/genopts.py index f0859b2..e7284fc 100755 --- a/genopts/genopts.py +++ b/genopts/genopts.py @@ -18,6 +18,7 @@ # SPDX-License-Identifier: GPL-3.0-or-later import argparse +import string import sys from abc import ABC, abstractmethod from dataclasses import dataclass @@ -30,50 +31,6 @@ from cerberus.errors import ValidationError as CerberusValidationError VERSION = "0.1.0" -TEMPLATES: Dict[str, str] = { - 'schema': """\ -#ifndef {header_guard} -#define {header_guard} - -#include - -#include "file_options/file_options.hpp" -#include "parsers.hpp" -#include "keywords.hpp" -#include "defaults.hpp" - -namespace fs = std::filesystem; - -{namespace_begin} -using file_options::converter; -using file_options::declare_file; -using file_options::declare_group; -using file_options::declare_list; -using file_options::declare_option; -using file_options::file_schema; -using file_options::opt_type; -using file_options::sec_type; - -const file_schema valid_options = declare_file({{ - {sections} -}}); -{namespace_end} - -#endif /* {header_guard} */ -""", - - 'keywords': """\ -#ifndef {header_guard} -#define {header_guard} - -{namespace_begin} -{section_keywords} -{option_keywords} -{namespace_end} - -#endif /* {header_guard} */ -"""} - # Validation schema for a file_option in OPT_DESC_FILE OPTION_SCHEMA = { 'type': 'dict', @@ -190,36 +147,64 @@ class ConfigError(ValidationError): class Namespace(ABC): - namespace: Iterable[str] + separator: str = '::' + namespace: List[str] def __init__(self, namespace: str): - self.namespace = namespace.split('::') + self.namespace = namespace.split(Namespace.separator) + + def __contains__(self, item) -> bool: + return item.namespace[0:len(self.namespace)] == self.namespace + + def __sub__(self, other): + nn = [n for n in self.namespace if n not in other.namespace] + return type(self)(Namespace.separator.join(nn)) @abstractmethod - def begin(self) -> str: + def __format__(self, format_spec) -> str: return NotImplemented @abstractmethod - def end(self) -> str: + def __repr__(self) -> str: return NotImplemented class Cxx14Namespace(Namespace): - def begin(self) -> str: - return '\n'.join(f'namespace {n} {{' for n in self.namespace) + def __format__(self, format_spec): + # {:B} formats the opening statement of a namespace + if format_spec == "B": + return '\n'.join(f'namespace {n} {{' for n in self.namespace) + # {:E} formats the opening statement of a namespace + if format_spec == "E": + return '\n'.join(f'}} // namespace {n}' for n in reversed(self.namespace)) + # no specifier just formats the namespace as a name + return Namespace.separator.join(n for n in self.namespace) + + def __repr__(self) -> str: + def add_quotes(s): + return f"'{s}'" - def end(self) -> str: - return '\n'.join(f'}} // namespace {n}' for n in reversed(self.namespace)) + return f"Cxx14Namespace(namespace={', '.join([add_quotes(n) for n in self.namespace])})" class Cxx17Namespace(Namespace): - def begin(self) -> str: - return f"namespace {'::'.join(self.namespace)} {{" + def __format__(self, format_spec): + # {:B} formats the opening statement of a namespace + if format_spec == "B": + return f"namespace {Namespace.separator.join(self.namespace)} {{" + # {:E} formats the opening statement of a namespace + if format_spec == "E": + return f"}} // namespace {Namespace.separator.join(self.namespace)}" + # no specifier just formats the namespace as a name + return Namespace.separator.join(n for n in self.namespace) - def end(self) -> str: - return f"}} // namespace {'::'.join(self.namespace)}" + def __repr__(self) -> str: + def add_quotes(s): + return f"'{s}'" + + return f"Cxx17Namespace(namespace={', '.join([add_quotes(n) for n in self.namespace])})" class NamespaceFactory: @@ -268,8 +253,8 @@ class OutputConfig: class Config: output_lang: str - schema_config: OutputConfig - keywords_config: OutputConfig + schema: OutputConfig + keywords: OutputConfig keys: Iterable[str] = ['schema', 'keywords'] def __init__(self, @@ -277,23 +262,74 @@ class Config: schema_config: OutputConfig, keywords_config: OutputConfig): self.output_lang = output_lang - self.schema_config = schema_config - self.keywords_config = keywords_config + self.schema = schema_config + self.keywords = keywords_config def __repr__(self): return f"Config(" \ f"output_lang='{self.output_lang}', " \ - f"schema_config='{self.schema_config}', " \ - f"keywords_config={self.keywords_config})" + f"schema='{self.schema}', " \ + f"keywords={self.keywords})" -class Option(ABC): +class ContextMeta(type): + _instances = {} + + def __call__(cls, *args, **kwargs): + if cls not in cls._instances: + instance = super(ContextMeta, cls).__call__(*args, **kwargs) + cls._instances[cls] = instance + return cls._instances[cls] + + +class FormattingContext(metaclass=ContextMeta): + _current_namespace: Namespace = None + + @property + def current_namespace(self) -> Namespace: + return self._current_namespace + + @current_namespace.setter + def current_namespace(self, value): + self._current_namespace = value + + +class Keyword: name: str + namespace: Namespace + + def __init__(self, name: str, namespace: Namespace): + self.name = name + self.namespace = namespace + + def __format__(self, format_spec): + + # {D} formats a keyword definition + if format_spec == 'D': + return f"constexpr static auto {self.name} {{\"{self.name}\"}};" + + # {I} formats a keyword instantiation + if format_spec == 'I': + ctx = FormattingContext() + ns = self.namespace + if self.namespace in ctx.current_namespace: + ns -= ctx.current_namespace + + return f"{ns}{Namespace.separator}{self.name}" + + return self.name + + def __str__(self) -> str: + return self.name + + +class Option(ABC): + keyword: Keyword type: str required: bool - def __init__(self, name: str, type: str, required: bool): - self.name = name + def __init__(self, keyword: Keyword, type: str, required: bool): + self.keyword = keyword self.type = type self.required = required @@ -310,20 +346,22 @@ class ConvertibleOption(Option): converter: Optional[str] template: str = """\ declare_option<{type}>( - keywords::{name}, + {keyword}, opt_type::{required}, converter<{type}>({converter})) """ - def __init__(self, name: str, type: str, required: bool, converter: Optional[str] = None): - super().__init__(name, type, required) + def __init__(self, keyword: Keyword, type: str, required: bool, converter: Optional[str] = None): + super().__init__(keyword, type, required) self.converter = converter def __repr__(self) -> str: - return f"Option(name='{self.name}', type='{self.type}', required={self.required}, converter={self.converter})" + return f"Option(keyword='{self.keyword}', type='{self.type}', required={self.required}, " \ + f"converter={self.converter})" def __str__(self) -> str: - return self.template.format(name=self.name, type=self.type, + return self.template.format(keyword=f"{self.keyword:I}", + type=self.type, required="mandatory" if self.required else "optional", converter=self.converter) @@ -331,37 +369,37 @@ declare_option<{type}>( class NonConvertibleOption(Option): template: str = """\ declare_option<{type}>( - keywords::{name}, + {keyword}, opt_type::{required}) """ - def __init__(self, name: str, type: str, required: bool): - super().__init__(name, type, required) + def __init__(self, keyword: Keyword, type: str, required: bool): + super().__init__(keyword, type, required) def __repr__(self) -> str: - return f"Option(name='{self.name}', type='{self.type}', required={self.required})" + return f"Option(keyword='{self.keyword}', type='{self.type}', required={self.required})" def __str__(self) -> str: - return self.template.format(name=self.name, type=self.type, + return self.template.format(keyword=self.keyword, type=self.type, required="mandatory" if self.required else "optional") class OptionFactory: @staticmethod - def create_option(name: str, type: str, required: bool, converter: Optional[str] = None) -> Option: + def create_option(keyword: Keyword, type: str, required: bool, converter: Optional[str] = None) -> Option: if converter: - return ConvertibleOption(name, type, required, converter) + return ConvertibleOption(keyword, type, required, converter) else: - return NonConvertibleOption(name, type, required) + return NonConvertibleOption(keyword, type, required) class Section: - name: str + keyword: Keyword required: bool options: Iterable[Option] template: str = """\ declare_section( - keywords::{name}, + {keyword}, sec_type::{required}, declare_group( {options} @@ -369,32 +407,22 @@ declare_section( ) """ - def __init__(self, name: str, required: bool, options: Iterable[Option]): - self.name = name + def __init__(self, keyword: Keyword, required: bool, options: Iterable[Option]): + self.keyword = keyword self.required = required self.options = options def __repr__(self) -> str: - return f"Section(name='{self.name}', options={self.options})" + return f"Section(keyword='{self.keyword}', options={self.options})" def __str__(self) -> str: return self.template.format( - name=self.name, + keyword=f"{self.keyword:I}", required="mandatory" if self.required else "optional", options=",\n".join(str(opt) for opt in self.options) ) -class Keyword: - name: str - - def __init__(self, name: str): - self.name = name - - def __str__(self) -> str: - return f"constexpr static auto {self.name} {{\"{self.name}\"}};" - - class Generator: data: Any config: Config @@ -404,6 +432,7 @@ class Generator: def __init__(self, config: Config, desc_file: Path): self.sections = [] self.keywords_by_section = {} + self.ctx = FormattingContext() with open(desc_file, "r") as f: self.data = yaml.safe_load(f) @@ -419,53 +448,100 @@ class Generator: for opt in s['options']: options.append( - OptionFactory.create_option(opt['name'], + OptionFactory.create_option(Keyword(opt['name'], config.keywords.namespace), opt['type'], opt['required'], opt.get('converter', None))) if section_name not in self.keywords_by_section: self.keywords_by_section[section_name] = [] - self.keywords_by_section[section_name].append(Keyword(opt['name'])) + self.keywords_by_section[section_name].append(Keyword(opt['name'], config.keywords.namespace)) self.sections.append( - Section(s['name'], + Section(Keyword(s['name'], config.keywords.namespace), s['required'], options)) def get_schema(self) -> str: - cfg = self.config.schema_config + + template = """\ +#ifndef {header_guard} +#define {header_guard} + +#include + +#include "file_options/file_options.hpp" +#include "parsers.hpp" +#include "keywords.hpp" +#include "defaults.hpp" + +namespace fs = std::filesystem; + +{namespace_begin} +using file_options::converter; +using file_options::declare_file; +using file_options::declare_group; +using file_options::declare_list; +using file_options::declare_option; +using file_options::file_schema; +using file_options::opt_type; +using file_options::sec_type; + +const file_schema valid_options = declare_file({{ + {sections} +}}); +{namespace_end} + +#endif /* {header_guard} */ +""" + + cfg = self.config.schema copyright_notice = cfg.copyright_text if copyright_notice: copyright_notice += "\n" + self.ctx.current_namespace = self.config.schema.namespace + return (copyright_notice + - TEMPLATES['schema'].format( + template.format( header_guard=cfg.header_guard, - namespace_begin=cfg.namespace.begin() + "\n", - namespace_end="\n" + cfg.namespace.end(), - sections='\n'.join(str(s) for s in self.sections), + namespace_begin=f"{cfg.namespace:B}\n", + namespace_end=f"\n{cfg.namespace:E}", + sections='\n'.join(str(s) for s in self.sections) )) def get_keywords(self) -> str: - section_keywords = [Keyword(s.name) for s in self.sections] + template = """\ +#ifndef {header_guard} +#define {header_guard} - cfg = self.config.keywords_config +{namespace_begin} +{section_keywords} +{option_keywords} +{namespace_end} + +#endif /* {header_guard} */ +""" + + cfg = self.config.keywords + section_keywords = [s.keyword for s in self.sections] copyright_notice = cfg.copyright_text if copyright_notice: copyright_notice += "\n" + self.ctx.current_namespace = self.config.keywords.namespace + return (copyright_notice + - TEMPLATES['keywords'].format( + template.format( header_guard=cfg.header_guard, - namespace_begin=cfg.namespace.begin() + "\n", - namespace_end="\n" + cfg.namespace.end(), + namespace_begin=f"{cfg.namespace:B}\n", + namespace_end=f"\n{cfg.namespace:E}", section_keywords="// section names\n" + - "\n".join(str(k) for k in section_keywords) + "\n", + "\n".join(f"{k:D}" for k in section_keywords) + "\n", option_keywords='\n'.join(f"// option names for '{sn}' section\n" + - '\n'.join(str(k) for k in self.keywords_by_section[sn.name]) + '\n'.join(f"{k:D}" for k in self.keywords_by_section[sn.name]) for sn in section_keywords) )) @@ -590,8 +666,19 @@ def parse_args(args) -> argparse.Namespace: return parser.parse_args(args) -def main(args = None): +class PluralFormatter(string.Formatter): + def format_field(self, value, format_spec): + if format_spec.startswith('plural,'): + words = format_spec.split(',') + if value == 1: + return words[1] + else: + return words[2] + else: + return super().format_field(value, format_spec) + +def main(args=None): if not args: args = sys.argv[1:] @@ -607,8 +694,8 @@ def main(args = None): if args.force_console: print(hpp_schema) else: - cfg.schema_config.output_path.header.write_text(hpp_schema) - print(f"Written {cfg.schema_config.output_path.header}") + cfg.schema.output_path.header.write_text(hpp_schema) + print(f"Written {cfg.schema.output_path.header}") if args.write_keywords: hpp_keywords = g.get_keywords() @@ -616,8 +703,8 @@ def main(args = None): if args.force_console: print(hpp_keywords) else: - cfg.keywords_config.output_path.header.write_text(hpp_keywords) - print(f"Written {cfg.keywords_config.output_path.header}") + cfg.keywords.output_path.header.write_text(hpp_keywords) + print(f"Written {cfg.keywords.output_path.header}") except ConfigError as e: print(e, file=sys.stderr) -- GitLab