Skip to content

feat: [CG-10650] codebase.codeowners interface #290

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 19 commits into from
Feb 11, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions ruff.toml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ extend-generics = [
"codegen.sdk.core.assignment.Assignment",
"codegen.sdk.core.class_definition.Class",
"codegen.sdk.core.codebase.Codebase",
"codegen.sdk.core.codeowner.CodeOwner",
"codegen.sdk.core.dataclasses.usage.Usage",
"codegen.sdk.core.dataclasses.usage.UsageType",
"codegen.sdk.core.dataclasses.usage.UsageKind",
Expand Down
13 changes: 13 additions & 0 deletions src/codegen/sdk/core/codebase.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import re
from collections.abc import Generator
from contextlib import contextmanager
from functools import cached_property
from pathlib import Path
from typing import TYPE_CHECKING, Generic, Literal, TypeVar, Unpack, overload

Expand Down Expand Up @@ -36,6 +37,7 @@
from codegen.sdk.codebase.span import Span
from codegen.sdk.core.assignment import Assignment
from codegen.sdk.core.class_definition import Class
from codegen.sdk.core.codeowner import CodeOwner
from codegen.sdk.core.detached_symbols.code_block import CodeBlock
from codegen.sdk.core.detached_symbols.parameter import Parameter
from codegen.sdk.core.directory import Directory
Expand Down Expand Up @@ -257,6 +259,17 @@ def files(self, *, extensions: list[str] | Literal["*"] | None = None) -> list[T
# Sort files alphabetically
return sort_editables(files, alphabetical=True, dedupe=False)

@cached_property
def codeowners(self) -> list["CodeOwner[TSourceFile]"]:
"""List all CodeOnwers in the codebase.

Returns:
list[CodeOwners]: A list of CodeOwners objects in the codebase.
"""
if self.G.codeowners_parser is None:
return []
return CodeOwner.from_parser(self.G.codeowners_parser, lambda *args, **kwargs: self.files(*args, **kwargs))

@property
def directories(self) -> list[TDirectory]:
"""List all directories in the codebase.
Expand Down
97 changes: 97 additions & 0 deletions src/codegen/sdk/core/codeowner.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import logging
from collections.abc import Iterable, Iterator
from typing import Callable, Generic, Literal

from codeowners import CodeOwners as CodeOwnersParser

from codegen.sdk._proxy import proxy_property
from codegen.sdk.core.interfaces.has_symbols import (
FilesParam,
HasSymbols,
TClass,
TFile,
TFunction,
TGlobalVar,
TImport,
TImportStatement,
TSymbol,
)
from codegen.sdk.core.utils.cache_utils import cached_generator
from codegen.shared.decorators.docs import apidoc, noapidoc

logger = logging.getLogger(__name__)


@apidoc
class CodeOwner(
HasSymbols[TFile, TSymbol, TImportStatement, TGlobalVar, TClass, TFunction, TImport],
Generic[TFile, TSymbol, TImportStatement, TGlobalVar, TClass, TFunction, TImport],
):
"""CodeOwner is a class that represents a code owner in a codebase.

It is used to iterate over all files that are owned by a specific owner.

Attributes:
owner_type: The type of the owner (USERNAME, TEAM, EMAIL).
owner_value: The value of the owner.
files_source: A callable that returns an iterable of all files in the codebase.
"""

owner_type: Literal["USERNAME", "TEAM", "EMAIL"]
owner_value: str
files_source: Callable[FilesParam, Iterable[TFile]]

def __init__(
self,
files_source: Callable[FilesParam, Iterable[TFile]],
owner_type: Literal["USERNAME", "TEAM", "EMAIL"],
owner_value: str,
):
self.owner_type = owner_type
self.owner_value = owner_value
self.files_source = files_source

Check failure on line 52 in src/codegen/sdk/core/codeowner.py

View workflow job for this annotation

GitHub Actions / mypy

error: Incompatible types in assignment (expression has type "Callable[FilesParam, Iterable[TFile]]", variable has type "Callable[FilesParam, Iterable[TFile]]") [assignment]

@classmethod
def from_parser(
cls,
parser: CodeOwnersParser,
file_source: Callable[FilesParam, Iterable[TFile]],
) -> list["CodeOwner"]:
"""Create a list of CodeOwner objects from a CodeOwnersParser.

Args:
parser (CodeOwnersParser): The CodeOwnersParser to use.
file_source (Callable[FilesParam, Iterable[TFile]]): A callable that returns an iterable of all files in the codebase.

Returns:
list[CodeOwner]: A list of CodeOwner objects.
"""
codeowners = []

Check failure on line 69 in src/codegen/sdk/core/codeowner.py

View workflow job for this annotation

GitHub Actions / mypy

error: Need type annotation for "codeowners" (hint: "codeowners: list[<type>] = ...") [var-annotated]
for _, _, owners, _, _ in parser.paths:
for owner_label, owner_value in owners:
codeowners.append(CodeOwner(file_source, owner_label, owner_value))
return codeowners

@cached_generator(maxsize=16)

Check failure on line 75 in src/codegen/sdk/core/codeowner.py

View workflow job for this annotation

GitHub Actions / mypy

error: Argument 1 has incompatible type "Callable[[CodeOwner[TFile, TSymbol, TImportStatement, TGlobalVar, TClass, TFunction, TImport], VarArg(Any), KwArg(Any)], Iterable[TFile]]"; expected "Callable[[CodeOwner[TFile, TSymbol, TImportStatement, TGlobalVar, TClass, TFunction, TImport], VarArg(Any), KwArg(Any)], Iterator[Never]]" [arg-type]
@noapidoc
def files_generator(self, *args: FilesParam.args, **kwargs: FilesParam.kwargs) -> Iterable[TFile]:

Check failure on line 77 in src/codegen/sdk/core/codeowner.py

View workflow job for this annotation

GitHub Actions / mypy

error: ParamSpec "FilesParam" is unbound [valid-type]
for source_file in self.files_source(*args, **kwargs):
# Filter files by owner value
if self.owner_value in source_file.owners:
yield source_file

@proxy_property
def files(self, *args: FilesParam.args, **kwargs: FilesParam.kwargs) -> Iterable[TFile]:
"""Recursively iterate over all files in the codebase that are owned by the current code owner."""
return self.files_generator(*args, **kwargs)

@property
def name(self) -> str:
"""The name of the code owner."""
return self.owner_value

def __iter__(self) -> Iterator[TFile]:
return iter(self.files_generator())

def __repr__(self) -> str:
return f"CodeOwner(owner_type={self.owner_type}, owner_value={self.owner_value})"
145 changes: 38 additions & 107 deletions src/codegen/sdk/core/directory.py
Original file line number Diff line number Diff line change
@@ -1,43 +1,30 @@
import logging
import os
from itertools import chain
from collections.abc import Iterator
from pathlib import Path
from typing import TYPE_CHECKING, Generic, Self, TypeVar

from codegen.shared.decorators.docs import apidoc, py_noapidoc

if TYPE_CHECKING:
from codegen.sdk.core.assignment import Assignment
from codegen.sdk.core.class_definition import Class
from codegen.sdk.core.file import File
from codegen.sdk.core.function import Function
from codegen.sdk.core.import_resolution import Import, ImportStatement
from codegen.sdk.core.symbol import Symbol
from codegen.sdk.typescript.class_definition import TSClass
from codegen.sdk.typescript.export import TSExport
from codegen.sdk.typescript.file import TSFile
from codegen.sdk.typescript.function import TSFunction
from codegen.sdk.typescript.import_resolution import TSImport
from codegen.sdk.typescript.statements.import_statement import TSImportStatement
from codegen.sdk.typescript.symbol import TSSymbol

import logging
from typing import Generic, Self

from codegen.sdk.core.interfaces.has_symbols import (
HasSymbols,
TClass,
TFile,
TFunction,
TGlobalVar,
TImport,
TImportStatement,
TSymbol,
)
from codegen.sdk.core.utils.cache_utils import cached_generator
from codegen.shared.decorators.docs import apidoc, noapidoc

logger = logging.getLogger(__name__)


TFile = TypeVar("TFile", bound="File")
TSymbol = TypeVar("TSymbol", bound="Symbol")
TImportStatement = TypeVar("TImportStatement", bound="ImportStatement")
TGlobalVar = TypeVar("TGlobalVar", bound="Assignment")
TClass = TypeVar("TClass", bound="Class")
TFunction = TypeVar("TFunction", bound="Function")
TImport = TypeVar("TImport", bound="Import")

TSGlobalVar = TypeVar("TSGlobalVar", bound="Assignment")


@apidoc
class Directory(Generic[TFile, TSymbol, TImportStatement, TGlobalVar, TClass, TFunction, TImport]):
class Directory(
HasSymbols[TFile, TSymbol, TImportStatement, TGlobalVar, TClass, TFunction, TImport],
Generic[TFile, TSymbol, TImportStatement, TGlobalVar, TClass, TFunction, TImport],
):
"""Directory representation for codebase.

GraphSitter abstraction of a file directory that can be used to look for files and symbols within a specific directory.
Expand All @@ -58,7 +45,7 @@ def __init__(self, path: Path, dirpath: str, parent: Self | None):
self.path = path
self.dirpath = dirpath
self.parent = parent
self.items = dict()
self.items = {}

def __iter__(self):
return iter(self.items.values())
Expand Down Expand Up @@ -126,62 +113,13 @@ def _get_subdirectories(directory: Directory):
_get_subdirectories(self)
return subdirectories

@property
def symbols(self) -> list[TSymbol]:
"""Get a recursive list of all symbols in the directory and its subdirectories."""
return list(chain.from_iterable(f.symbols for f in self.files))

@property
def import_statements(self) -> list[TImportStatement]:
"""Get a recursive list of all import statements in the directory and its subdirectories."""
return list(chain.from_iterable(f.import_statements for f in self.files))

@property
def global_vars(self) -> list[TGlobalVar]:
"""Get a recursive list of all global variables in the directory and its subdirectories."""
return list(chain.from_iterable(f.global_vars for f in self.files))

@property
def classes(self) -> list[TClass]:
"""Get a recursive list of all classes in the directory and its subdirectories."""
return list(chain.from_iterable(f.classes for f in self.files))

@property
def functions(self) -> list[TFunction]:
"""Get a recursive list of all functions in the directory and its subdirectories."""
return list(chain.from_iterable(f.functions for f in self.files))

@property
@py_noapidoc
def exports(self: "Directory[TSFile, TSSymbol, TSImportStatement, TSGlobalVar, TSClass, TSFunction, TSImport]") -> "list[TSExport]":
"""Get a recursive list of all exports in the directory and its subdirectories."""
return list(chain.from_iterable(f.exports for f in self.files))

@property
def imports(self) -> list[TImport]:
"""Get a recursive list of all imports in the directory and its subdirectories."""
return list(chain.from_iterable(f.imports for f in self.files))

def get_symbol(self, name: str) -> TSymbol | None:
"""Get a symbol by name in the directory and its subdirectories."""
return next((s for s in self.symbols if s.name == name), None)

def get_import_statement(self, name: str) -> TImportStatement | None:
"""Get an import statement by name in the directory and its subdirectories."""
return next((s for s in self.import_statements if s.name == name), None)

def get_global_var(self, name: str) -> TGlobalVar | None:
"""Get a global variable by name in the directory and its subdirectories."""
return next((s for s in self.global_vars if s.name == name), None)

def get_class(self, name: str) -> TClass | None:
"""Get a class by name in the directory and its subdirectories."""
return next((s for s in self.classes if s.name == name), None)

def get_function(self, name: str) -> TFunction | None:
"""Get a function by name in the directory and its subdirectories."""
return next((s for s in self.functions if s.name == name), None)
@noapidoc
@cached_generator()
def files_generator(self) -> Iterator[TFile]:
"""Yield files recursively from the directory."""
yield from self.files

# Directory-specific methods
def add_file(self, file: TFile) -> None:
"""Add a file to the directory."""
rel_path = os.path.relpath(file.file_path, self.dirpath)
Expand All @@ -202,18 +140,12 @@ def get_file(self, filename: str, ignore_case: bool = False) -> TFile | None:
from codegen.sdk.core.file import File

if ignore_case:
return next((f for name, f in self.items.items() if name.lower() == filename.lower() and isinstance(f, File)), None)
return next(
(f for name, f in self.items.items() if name.lower() == filename.lower() and isinstance(f, File)),
None,
)
return self.items.get(filename, None)

@py_noapidoc
def get_export(self: "Directory[TSFile, TSSymbol, TSImportStatement, TSGlobalVar, TSClass, TSFunction, TSImport]", name: str) -> "TSExport | None":
"""Get an export by name in the directory and its subdirectories (supports only typescript)."""
return next((s for s in self.exports if s.name == name), None)

def get_import(self, name: str) -> TImport | None:
"""Get an import by name in the directory and its subdirectories."""
return next((s for s in self.imports if s.name == name), None)

def add_subdirectory(self, subdirectory: Self) -> None:
"""Add a subdirectory to the directory."""
rel_path = os.path.relpath(subdirectory.dirpath, self.dirpath)
Expand All @@ -230,23 +162,22 @@ def remove_subdirectory_by_path(self, subdirectory_path: str) -> None:
del self.items[rel_path]

def get_subdirectory(self, subdirectory_name: str) -> Self | None:
"""Get a subdirectory by its path relative to the directory."""
"""Get a subdirectory by its name (relative to the directory)."""
return self.items.get(subdirectory_name, None)

def remove(self) -> None:
"""Remove the directory and all its files and subdirectories."""
for f in self.files:
f.remove()

def update_filepath(self, new_filepath: str) -> None:
"""Update the filepath of the directory."""
"""Update the filepath of the directory and its contained files."""
old_path = self.dirpath
new_path = new_filepath

for file in self.files:
new_file_path = os.path.join(new_path, os.path.relpath(file.file_path, old_path))
file.update_filepath(new_file_path)

def remove(self) -> None:
"""Remove all the files in the files container."""
for f in self.files:
f.remove()

def rename(self, new_name: str) -> None:
"""Rename the directory."""
parent_dir, _ = os.path.split(self.dirpath)
Expand Down
Loading