From 56c64afbe90e511c103151047e0901f78828c73d Mon Sep 17 00:00:00 2001 From: K4liber Date: Sat, 21 Mar 2026 20:33:49 +0100 Subject: [PATCH] init --- pyproject.toml | 2 +- src/definit/dag/dag.py | 35 +++++++++++++++ src/definit/db/interface.py | 7 +++ src/definit/db/md.py | 54 +++++++++++++++--------- src/definit/definition/definition_key.py | 10 ++++- 5 files changed, 84 insertions(+), 24 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index e05f91b..fd7d3ec 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "definit" -version = "0.5.0" +version = "0.6.0" description = "Hierarchy of precise definitions." readme = "README.md" requires-python = ">=3.12" diff --git a/src/definit/dag/dag.py b/src/definit/dag/dag.py index 319117f..6cc0091 100644 --- a/src/definit/dag/dag.py +++ b/src/definit/dag/dag.py @@ -57,3 +57,38 @@ def visit(node_key: DefinitionKey) -> bool: return False return True + + def get_definition_levels(self) -> list[tuple[Definition, int]]: + """Return the definition levels. + + Each definition is paired with its level in the DAG. + Level 0 corresponds to definitions with no dependencies. + """ + visited: set[DefinitionKey] = set() + added: set[DefinitionKey] = set() + levels: dict[DefinitionKey, int] = {} + definitions_with_levels: list[tuple[Definition, int]] = [] + + def visit(node_key: DefinitionKey) -> int: + if node_key in visited: + return levels.get(node_key, 0) + + visited.add(node_key) + max_level = 0 + + for neighbor in self._edges.get(node_key, []): + neighbor_level = visit(neighbor) + max_level = max(max_level, neighbor_level + 1) + + levels[node_key] = max_level + + if node_key not in added: + definitions_with_levels.append((self._definitions[node_key], max_level)) + added.add(node_key) + + return max_level + + for node_key in self._definitions.keys(): + visit(node_key) + + return definitions_with_levels diff --git a/src/definit/db/interface.py b/src/definit/db/interface.py index a4f21d7..696a6bd 100644 --- a/src/definit/db/interface.py +++ b/src/definit/db/interface.py @@ -8,6 +8,13 @@ class DatabaseAbstract(ABC): + @abstractmethod + def get_dag(self, definition_keys: set[DefinitionKey]) -> DAG: + """ + Get the DAG for a set of definition keys. + """ + ... + @abstractmethod def get_dag_for_definition(self, root: DefinitionKey) -> DAG: """ diff --git a/src/definit/db/md.py b/src/definit/db/md.py index e6550d1..ceef399 100644 --- a/src/definit/db/md.py +++ b/src/definit/db/md.py @@ -43,7 +43,7 @@ def __init__(self, data_md_path: Path, load_cache: bool = False) -> None: def get_dag_for_definition(self, root: DefinitionKey) -> DAG: self._assure_cache_loaded() definitions = {root} - return self._get_dag(definitions=definitions) + return self.get_dag(definition_keys=definitions) def get_index(self, field: Field | None = None) -> set[DefinitionKey]: index: set[DefinitionKey] = set() @@ -67,6 +67,31 @@ def get_definition(self, definition_key: DefinitionKey) -> Definition: parent_definition_key=None, ) + def get_dag(self, definition_keys: set[DefinitionKey]) -> DAG: + dag = DAG() + + for definition_key in definition_keys: + self._update_dag_in_place(definition_key=definition_key, dag=dag) + + definition_keys = {key for key in definition_keys} + dag_definition_keys = {definition.key for definition in dag.nodes} + missing_definition_keys = definition_keys - dag_definition_keys + + if missing_definition_keys: + _logger.warning(f"Following definitions are not a part of DAG: {missing_definition_keys}") + + additional_definition_keys = dag_definition_keys - definition_keys + + if additional_definition_keys: + msg = f"Following definitions are part of DAG but not requested: {additional_definition_keys}" + raise DataParserMdException(msg) + + if not dag.has_dag_structure(): + msg = f"DAG has an invalid structure: {dag.edges}" + raise DataParserMdException(msg) + + return dag + ##### Static methods ##### @staticmethod @@ -88,25 +113,6 @@ def serialize(definitions: list[Definition], db_path: Path) -> None: def _get_full_path(self, absolute_path: Path) -> str: return absolute_path.relative_to(self._definitions_path).as_posix() - def _get_dag(self, definitions: set[DefinitionKey]) -> DAG: - dag = DAG() - - for definition in definitions: - self._update_dag_in_place(definition_key=definition, dag=dag) - - definition_keys = {key for key in definitions} - dag_definition_keys = {definition.key for definition in dag.nodes} - missing_definition_keys = definition_keys - dag_definition_keys - - if missing_definition_keys: - _logger.warning(f"Following definitions are not a part of DAG: {missing_definition_keys}") - - if not dag.has_dag_structure(): - msg = f"DAG has an invalid structure: {dag.edges}" - raise DataParserMdException(msg) - - return dag - def _assure_cache_loaded(self) -> None: if self._definition_uid_to_absolute_path: return # cache already loaded @@ -167,7 +173,13 @@ def _update_dag_in_place( matches = re.findall(r"\[(.*?)\]\((.*?)\)", definition.content) for _, child_uid in matches: - child_definition_key = DefinitionKey.from_full_path(child_uid) + absolute_path = self._definition_uid_to_absolute_path[child_uid] + full_path = self._get_full_path(absolute_path=absolute_path).removesuffix(".md") + child_definition_key = DefinitionKey.from_full_path(full_path=full_path) + + if child_definition_key == definition_key: + continue # Skip self-references + child_definition = self._get_definition( definition_key=child_definition_key, parent_definition_key=definition_key, diff --git a/src/definit/definition/definition_key.py b/src/definit/definition/definition_key.py index be27f80..8dcba9b 100644 --- a/src/definit/definition/definition_key.py +++ b/src/definit/definition/definition_key.py @@ -28,18 +28,24 @@ def from_full_path(full_path: str) -> "DefinitionKey": def get_reference(self, phrase: str | None = None) -> str: if phrase is None: - phrase = self.name + phrase = self._fixed_name return f"[{phrase}]({self.uid})" def get_index_reference(self) -> str: - return f"[{self.name}]({self.full_path})" + return f"[{self._fixed_name}]({self.full_path})" # Internal methods def __hash__(self) -> int: return hash(self.uid) + def __eq__(self, value: object) -> bool: + if not isinstance(value, DefinitionKey): + return NotImplemented + + return self.__hash__() == hash(value) + @cached_property def _fixed_name(self) -> str: return self.name.replace(" ", "_").replace("'", "").replace("-", "_").lower()