""" QL files generation `generate(opts, renderer)` will generate QL classes and manage stub files out of a `yml` schema file. Each class (for example, `Foo`) in the schema triggers: * generation of a `FooBase` class implementation translating all properties into appropriate getters * if not created or already customized, generation of a stub file which defines `Foo` as extending `FooBase`. This can be used to add hand-written code to `Foo`, which requires removal of the `// generated` header comment in that file. All generated base classes actually import these customizations when referencing other classes. Generated files that do not correspond any more to any class in the schema are deleted. Customized stubs are however left behind and must be dealt with by hand. """ import pathlib from dataclasses import dataclass, field from typing import List, ClassVar, Union, Optional import inflection @dataclass class Param: param: str first: bool = False @dataclass class Property: singular: str type: Optional[str] = None tablename: Optional[str] = None tableparams: List[Param] = field(default_factory=list) plural: Optional[str] = None first: bool = False is_optional: bool = False is_predicate: bool = False is_child: bool = False qltest_skip: bool = False def __post_init__(self): if self.tableparams: self.tableparams = [Param(x) for x in self.tableparams] self.tableparams[0].first = True @property def getter(self): return f"get{self.singular}" if not self.is_predicate else self.singular @property def indefinite_getter(self): if self.plural: article = "An" if self.singular[0] in "AEIO" else "A" return f"get{article}{self.singular}" @property def type_is_class(self): return bool(self.type) and self.type[0].isupper() @property def is_repeated(self): return bool(self.plural) @property def is_single(self): return not (self.is_optional or self.is_repeated or self.is_predicate) @dataclass class Class: template: ClassVar = 'ql_class' name: str bases: List[str] = field(default_factory=list) final: bool = False properties: List[Property] = field(default_factory=list) dir: pathlib.Path = pathlib.Path() imports: List[str] = field(default_factory=list) qltest_skip: bool = False qltest_collapse_hierarchy: bool = False qltest_uncollapse_hierarchy: bool = False has_db_id: bool = False ipa: bool = False def __post_init__(self): self.bases = sorted(self.bases) if self.properties: self.properties[0].first = True @property def root(self) -> bool: return not self.bases @property def path(self) -> pathlib.Path: return self.dir / self.name @property def db_id(self) -> Optional[str]: if self.has_db_id: return "@" + inflection.underscore(self.name) @dataclass class Stub: template: ClassVar = 'ql_stub' name: str base_import: str @dataclass class DbClasses: template: ClassVar = 'ql_db' classes: List[Class] = field(default_factory=list) @dataclass class ImportList: template: ClassVar = 'ql_imports' imports: List[str] = field(default_factory=list) @dataclass class GetParentImplementation: template: ClassVar = 'ql_parent' classes: List[Class] = field(default_factory=list) @dataclass class PropertyForTest: getter: str type: Optional[str] = None is_single: bool = False is_predicate: bool = False is_repeated: bool = False @dataclass class ClassTester: template: ClassVar = 'ql_test_class' class_name: str properties: List[PropertyForTest] = field(default_factory=list) @dataclass class PropertyTester: template: ClassVar = 'ql_test_property' class_name: str property: PropertyForTest @dataclass class MissingTestInstructions: template: ClassVar = 'ql_test_missing' class Ipa: @dataclass class Class: is_final: ClassVar = False name: str first: bool = False @dataclass class FinalClass(Class): is_final: ClassVar = True is_ipa_from: ClassVar = False is_ipa_on: ClassVar = False is_db: ClassVar = False @property def is_ipa(self): return self.is_ipa_on or self.is_ipa_from @dataclass class Param: param: str type: str first: bool = False @dataclass class FinalClassIpaFrom(FinalClass): is_ipa_from: ClassVar = True type: str = None @property def params(self): return [Ipa.Param("id", self.type, first=True)] @dataclass class FinalClassIpaOn(FinalClass): is_ipa_on: ClassVar = True params: List["Ipa.Param"] = field(default_factory=list) def __post_init__(self): if self.params: self.params[0].first = True @property def has_params(self) -> bool: return bool(self.params) @dataclass class FinalClassDb(FinalClass): is_db: ClassVar = True subtracted_ipa_types: List["Ipa.Class"] = field(default_factory=list) def subtract_type(self, type: str): self.subtracted_ipa_types.append(Ipa.Class(type, first=not self.subtracted_ipa_types)) @property def has_subtracted_ipa_types(self): return bool(self.subtracted_ipa_types) @property def db_id(self): return "@" + inflection.underscore(self.name) @dataclass class NonFinalClass(Class): derived: List["Ipa.Class"] = field(default_factory=list) def __post_init__(self): self.derived = [Ipa.Class(c) for c in self.derived] if self.derived: self.derived[0].first = True @dataclass class Types: template: ClassVar = "ql_ipa_types" root: str final_classes: List["Ipa.FinalClass"] = field(default_factory=list) non_final_classes: List["Ipa.NonFinalClass"] = field(default_factory=list) def __post_init__(self): if self.final_classes: self.final_classes[0].first = True @dataclass class ConstructorStub: template: ClassVar = "ql_ipa_constructor_stub" cls: Union["Ipa.FinalClassIpaFrom", "Ipa.FinalClassIpaOn"]