diff options
author | gram <git@orsinium.dev> | 2023-02-28 11:18:17 +0100 |
---|---|---|
committer | gram <git@orsinium.dev> | 2023-02-28 11:18:17 +0100 |
commit | 2e02490fa12406d1753d83be8f56a25edcc23c55 (patch) | |
tree | 0733da095332d8bb6c56681d78e86467c50c69b5 |
init core logic
-rw-r--r-- | owners/__init__.py | 0 | ||||
-rw-r--r-- | owners/__main__.py | 0 | ||||
-rw-r--r-- | owners/_cli.py | 0 | ||||
-rw-r--r-- | owners/_owners.py | 62 | ||||
-rw-r--r-- | owners/_rule.py | 42 | ||||
-rw-r--r-- | owners/_section.py | 38 |
6 files changed, 142 insertions, 0 deletions
diff --git a/owners/__init__.py b/owners/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/owners/__init__.py diff --git a/owners/__main__.py b/owners/__main__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/owners/__main__.py diff --git a/owners/_cli.py b/owners/_cli.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/owners/_cli.py diff --git a/owners/_owners.py b/owners/_owners.py new file mode 100644 index 0000000..cd57d62 --- /dev/null +++ b/owners/_owners.py @@ -0,0 +1,62 @@ +from __future__ import annotations + +from dataclasses import dataclass +from functools import cached_property +from pathlib import Path + +from ._section import Section +from ._rule import Rule + + +@dataclass(frozen=True) +class CodeOwners: + root: Path + + @cached_property + def file_path(self) -> Path: + paths = [ + self.root / 'CODEOWNERS', + self.root / 'docs' / 'CODEOWNERS', + self.root / '.gitlab' / 'CODEOWNERS', + ] + for path in paths: + if path.exists(): + return path + raise FileNotFoundError('cannot find CODEOWNERS') + + @cached_property + def sections(self) -> tuple[Section, ...]: + sections: list[Section] = [] + section_name = '' + rules: list[Rule] = [] + with self.file_path.open('r', encoding='utf8') as stream: + for line in stream: + line = line.strip() + + # comment line + if not line or line.startswith('#'): + continue + is_section = line.startswith(('^[', '[')) + + # rule line + if not is_section: + rules.append(Rule(root=self.root, raw=line)) + continue + + # section line + if rules: + sections.append(Section( + root=self.root, + raw=section_name, + rules=tuple(rules) + )) + section_name = line + + return tuple(sections) + + def find_rule(self, path: Path) -> Rule | None: + for section in self.sections: + rule = section.find_rule(path) + if rule is not None: + return rule + return None diff --git a/owners/_rule.py b/owners/_rule.py new file mode 100644 index 0000000..c900d0e --- /dev/null +++ b/owners/_rule.py @@ -0,0 +1,42 @@ +from __future__ import annotations + +from dataclasses import dataclass +from functools import cached_property +from pathlib import Path + + +@dataclass(frozen=True) +class Rule: + root: Path + raw: str + + @cached_property + def raw_path(self) -> str: + return self.raw.split()[0] + + @cached_property + def paths(self) -> tuple[Path, ...]: + raw = self.raw_path.lstrip('/') + if '*' in raw: + paths = self.root.glob(raw) + return tuple(path.absolute() for path in paths) + path = Path(raw) + if not path.exists(): + return tuple() + return (path,) + + @cached_property + def owners(self) -> tuple[str, ...]: + without_comments = self.raw.split('#')[0] + return tuple(without_comments.split()[1:]) + + def includes(self, path: Path) -> bool: + """Check if the given path is included in the rule. + """ + path = path.absolute() + if path in self.paths: + return True + for parent in path.parents: + if parent in self.paths: + return True + return False diff --git a/owners/_section.py b/owners/_section.py new file mode 100644 index 0000000..c5f85bc --- /dev/null +++ b/owners/_section.py @@ -0,0 +1,38 @@ +from __future__ import annotations + +from dataclasses import dataclass +from functools import cached_property +from pathlib import Path +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from ._rule import Rule + + +@dataclass(frozen=True) +class Section: + root: Path + raw: str + rules: tuple[Rule, ...] + + @cached_property + def name(self) -> str | None: + raw = self.raw.strip() + without_comments = raw.split('#')[0] + if not raw: + return None + return without_comments.lstrip('^[').rstrip(']') + + @cached_property + def required(self) -> bool: + return not self.raw.lstrip().startswith('^') + + def find_rule(self, path: Path) -> Rule | None: + path = path.absolute() + for rule in self.rules: + if path in rule.paths: + return rule + for rule in self.rules: + if rule.includes(path): + return rule + return None |