def validate_entry(entry_name: str, sorted_packages: List[str], lenient_prefix=False): """ Validate if this entry name can be used. It currently checks for: 1) If the package name is defined. (This can be turn off by setting `lenient_prefix` to True. 2) If the entry name have at least 3 segments. Args: entry_name: The name to be validated. sorted_packages: The package names that are allowed. lenient_prefix: Whether we enforce that the entry must follow the pre-defined package name. Returns: """ if not lenient_prefix: for package_name in sorted_packages: if entry_name.startswith(package_name): break else: # None of the package name matches. raise InvalidIdentifierException( f"Entry name [{entry_name}] does not start with any predefined " f"packages, please define the packages by using " f"`additional_prefixes` in the ontology. Or you can use the " f"default prefix 'ft.onto'.") entry_splits = entry_name.split(".") for e in entry_splits: if not e.isidentifier(): raise InvalidIdentifierException( f"The entry name segment {e} is not a valid python identifier." ) if len(entry_splits) < 3: raise InvalidIdentifierException( f"We currently require each entry to contains at least 3 levels, " f"which corresponds to the directory name, the file (module) name," f"the entry class name. There are only {len(entry_splits)}" f"levels in [{entry_name}].")
def validate_entry(entry_name: str, sorted_packages: List[str]) -> str: for package_name in sorted_packages: if entry_name.startswith(package_name): matched_package = package_name break else: # None of the package name matches. raise InvalidIdentifierException( f"Entry name [{entry_name}] does not start with any predefined " f"packages, please define the packages by using " f"`additional_prefixes` in the ontology. Or you can use the " f"default prefix 'ft.onto'.") entry_splits = entry_name.split('.') if len(entry_splits) < 3: raise InvalidIdentifierException( f"We currently require each entry to contains at least 3 levels, " f"which corresponds to the directory name, the file (module) name," f"the entry class name. There are only {len(entry_splits)}" f"levels in [{entry_name}].") return matched_package
def parse_schema( self, schema: Dict, source_json_file: str, merged_schema: List[Dict], merged_prefixes: List[str], lenient_prefix=False, merged_entry_tree: Optional[EntryTree] = None, ): r"""Generates ontology code for a parsed schema extracted from a json config. Appends entry code to the corresponding module. Creates a new module file if module is generated for the first time. Args: schema: Ontology dictionary extracted from a json config. source_json_file: Path of the source json file. merged_schema: The merged schema is used to remember all definitions during parsing. merged_prefixes: To remember all prefixes encountered during parsing. lenient_prefix: Whether to remove the constraint on the prefix set. merged_entry_tree: an EntryTree type object and if it's not`None` then after running this function, all the entries from ontology specification file would be parsed into a tree structure with parent and children entries to represent the relationship. Returns: Modules to be imported by dependencies of the current ontology. """ entry_definitions: List[Dict] = schema[SchemaKeywords.definitions] merged_schema.extend(entry_definitions) if SchemaKeywords.prefixes in schema: merged_prefixes.extend(schema[SchemaKeywords.prefixes]) allowed_packages = set( schema.get(SchemaKeywords.prefixes, []) + [DEFAULT_PREFIX]) sorted_prefixes = analyze_packages(allowed_packages) file_desc = file_header( schema.get(SchemaKeywords.description, ""), schema.get(SchemaKeywords.ontology_name, ""), ) for definition in entry_definitions: raw_entry_name = definition[SchemaKeywords.entry_name] validate_entry(raw_entry_name, sorted_prefixes, lenient_prefix) if raw_entry_name in self.allowed_types_tree: warnings.warn( f"Class {raw_entry_name} already present in the " f"ontology, will be overridden.", DuplicateEntriesWarning, ) self.allowed_types_tree[raw_entry_name] = set() # Add the entry definition to the import managers. # This time adding to the root manager so everyone can access it # if needed, but they will only appear in the import list when # requested. # Entry class should be added to the imports before the attributes # to be able to used as the attribute type for the same entry. self.import_managers.root.add_object_to_import(raw_entry_name) # Get various parts of the entry name. en = EntryName(raw_entry_name) entry_item, properties = self.parse_entry(en, definition) # Add it as a defining object. self.import_managers.get( en.module_name).add_defining_objects(raw_entry_name) # Get or set module writer only if the ontology to be generated # is not already installed. if source_json_file not in self.exclude_from_writing: module_writer = self.module_writers.get(en.module_name) module_writer.set_description(file_desc) module_writer.source_file = source_json_file # Add entry item to the writer. module_writer.add_entry(en, entry_item) # Adding entry attributes to the allowed types for validation. for property_name in properties: # Check if the name is allowed. if not property_name.isidentifier(): raise InvalidIdentifierException( f"The property name: {property_name} is not a valid " f"python identifier.") if property_name in self.allowed_types_tree[en.class_name]: warnings.warn( f"Attribute type for the entry {en.class_name} " f"and the attribute {property_name} already present in " f"the ontology, will be overridden", DuplicatedAttributesWarning, ) self.allowed_types_tree[en.class_name].add(property_name) # populate the entry tree based on information if merged_entry_tree is not None: curr_entry_name = en.class_name parent_entry_name = definition["parent_entry"] curr_entry_attributes = self.allowed_types_tree[en.class_name] merged_entry_tree.add_node(curr_entry_name, parent_entry_name, curr_entry_attributes)
def parse_entry(self, entry_name: EntryName, schema: Dict) -> Tuple[EntryDefinition, List[str]]: """ Args: entry_name: Object holds various name form of the entry. schema: Dictionary containing specifications for an entry. Returns: extracted entry information: entry package string, entry filename, entry class entry_name, generated entry code and entry attribute names. """ this_manager = self.import_managers.get(entry_name.module_name) # Determine the parent entry of this entry. parent_entry: str = schema[SchemaKeywords.parent_entry] if parent_entry.startswith(TOP_MOST_MODULE_NAME): raise ParentEntryNotSupportedException( f"The parent entry {parent_entry} cannot be directly inherited," f" please inherit a type from {top.__name__} or your own" f" ontology.") if not this_manager.is_imported(parent_entry): raise ParentEntryNotDeclaredException( f"The parent entry {parent_entry} is not declared. It is " f"neither in the base entries nor in custom entries. " f"Please check them ontology specification, and make sure the " f"entry is defined before this.") base_entry: Optional[str] = self.find_base_entry( entry_name.class_name, parent_entry) if base_entry is None: raise OntologySpecError( f"Cannot find the base entry for entry " f"{entry_name.class_name} and {parent_entry}") if base_entry not in self.top_init_args: raise ParentEntryNotSupportedException( f"Cannot add {entry_name.class_name} to the ontology as " f"it's parent entry {parent_entry} is not supported. This is " f"likely that the entries are not inheriting the allowed types." ) # Take the property definitions of this entry. properties: List[Dict] = schema.get(SchemaKeywords.attributes, []) this_manager = self.import_managers.get(entry_name.module_name) # Validate if the parent entry is present. if not this_manager.is_known_name(parent_entry): raise ParentEntryNotDeclaredException( f"Cannot add {entry_name.class_name} to the ontology as " f"it's parent entry {parent_entry} is not present " f"in the ontology.") parent_entry_use_name = this_manager.get_name_to_use(parent_entry) property_items, property_names = [], [] for prop_schema in properties: # TODO: add test prop_name = prop_schema["name"] if prop_name in RESERVED_ATTRIBUTE_NAMES: raise InvalidIdentifierException( f"The attribute name {prop_name} is reserved and cannot be " f"used, please consider changed the name. The list of " f"reserved name strings are " f"{RESERVED_ATTRIBUTE_NAMES}") property_names.append(prop_schema["name"]) property_items.append(self.parse_property(entry_name, prop_schema)) # For special classes that requires a constraint. core_bases: Set[str] = self.top_to_core_entries[base_entry] entry_constraint_keys: Dict[str, str] = {} if any(item == "BaseLink" for item in core_bases): entry_constraint_keys = DEFAULT_CONSTRAINTS_KEYS["BaseLink"] elif any(item == "BaseGroup" for item in core_bases): entry_constraint_keys = DEFAULT_CONSTRAINTS_KEYS["BaseGroup"] class_att_items: List[ClassTypeDefinition] = [] for schema_key, class_key in entry_constraint_keys.items(): if schema_key in schema: constraint_type_ = schema[schema_key] constraint_type_name = this_manager.get_name_to_use( constraint_type_) if constraint_type_name is None: raise TypeNotDeclaredException( f"The type {constraint_type_} is not defined but it is " f"specified in {schema_key} of the definition of " f"{schema['entry_name']}. Please define them before " f"this entry type.") # TODO: cannot handle constraints that contain self-references. # self_ref = entry_name.class_name == constraint_type_ class_att_items.append( ClassTypeDefinition(class_key, constraint_type_name)) # TODO: Can assign better object type to Link and Group objects custom_init_arg_str: str = self.construct_init(entry_name, base_entry) entry_item = EntryDefinition( name=entry_name.name, class_type=parent_entry_use_name, init_args=custom_init_arg_str, properties=property_items, class_attributes=class_att_items, description=schema.get(SchemaKeywords.description, None), ) return entry_item, property_names