Verified Commit 58d22467 authored by Alberto Miranda's avatar Alberto Miranda ♨️
Browse files

Namespaces considered when formatting keywords

parent 61efca7e
Loading
Loading
Loading
Loading
+201 −114
Original line number Diff line number Diff line
@@ -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 <filesystem>

#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:
    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)

    def end(self) -> str:
        # {: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}'"

        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 <filesystem>

#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}

{namespace_begin}
{section_keywords}
{option_keywords}
{namespace_end}

#endif /* {header_guard} */
"""

        cfg = self.config.keywords_config
        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)