Skip to content

Commit 0734881

Browse files
authored
Add string compilation + grouping utils (#48)
# Motivation <!-- Why is this change necessary? --> # Content <!-- Please include a summary of the change --> # Testing <!-- How was the change tested? --> # Please check the following before marking your PR as ready for review - [ ] I have added tests for my changes - [ ] I have updated the documentation or added new documentation as needed - [ ] I have read and agree to the [Contributor License Agreement](../CLA.md)
1 parent a463c50 commit 0734881

37 files changed

+944
-23
lines changed

src/codegen/cli/codemod/convert.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ def convert_to_cli(input: str, language: str, name: str) -> str:
77
# from app.codemod.compilation.models.context import CodemodContext
88
#from app.codemod.compilation.models.pr_options import PROptions
99
10-
from graph_sitter import {codebase_type}
10+
from codegen.sdk import {codebase_type}
1111
1212
context: Any
1313

src/codegen/cli/utils/count_functions.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
# from app.codemod.compilation.models.context import CodemodContext
77
# from app.codemod.compilation.models.pr_options import PROptions
8-
# from graph_sitter import PyCodebaseType
8+
# from codegen.sdk import PyCodebaseType
99

1010
# context: CodemodContext
1111

src/codegen/git/__init__.py

Whitespace-only changes.

src/codegen/git/configs/constants.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,5 @@
33
CODEGEN_BOT_NAME = "codegen-bot"
44
CODEGEN_BOT_EMAIL = "[email protected]"
55
CODEOWNERS_FILEPATHS = [".github/CODEOWNERS", "CODEOWNERS", "docs/CODEOWNERS"]
6+
HIGHSIDE_REMOTE_NAME = "highside"
7+
LOWSIDE_REMOTE_NAME = "lowside"
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
from __future__ import annotations
2+
3+
import logging
4+
from typing import Any
5+
6+
from pydantic import BaseModel, Field
7+
8+
from codegen.git.models.pull_request_context import PullRequestContext
9+
10+
logger = logging.getLogger(__name__)
11+
12+
13+
class CodemodContext(BaseModel):
14+
CODEMOD_ID: int | None = None
15+
CODEMOD_LINK: str | None = None
16+
CODEMOD_AUTHOR: str | None = None
17+
TEMPLATE_ARGS: dict[str, Any] = Field(default_factory=dict)
18+
19+
# TODO: add fields for version
20+
# CODEMOD_VERSION_ID: int | None = None
21+
# CODEMOD_VERSION_AUTHOR: str | None = None
22+
23+
PULL_REQUEST: PullRequestContext | None = None
24+
25+
@classmethod
26+
def _render_template(cls, template_schema: dict[str, str], template_values: dict[str, Any]) -> dict[str, Any]:
27+
template_data: dict[str, Any] = {}
28+
for var_name, var_value in template_values.items():
29+
var_type = template_schema.get(var_name)
30+
31+
if var_type == "list":
32+
template_data[var_name] = [str(v).strip() for v in var_value.split(",")]
33+
else:
34+
template_data[var_name] = str(var_value)
35+
return template_data
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
from pydantic import BaseModel
2+
3+
4+
class GithubNamedUserContext(BaseModel):
5+
"""Represents a GitHub user parsed from a webhook payload"""
6+
7+
login: str
8+
email: str | None = None
9+
10+
@classmethod
11+
def from_payload(cls, payload: dict) -> "GithubNamedUserContext":
12+
return cls(login=payload.get("login"), email=payload.get("email"))

src/codegen/git/models/pr_options.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
from pydantic import BaseModel
2+
3+
from codegen.utils.decorators.docs import apidoc
4+
5+
6+
@apidoc
7+
class PROptions(BaseModel):
8+
"""Options for generating a PR."""
9+
10+
title: str | None = None
11+
body: str | None = None
12+
labels: list[str] | None = None # TODO: not used until we add labels to GithubPullRequestModel
13+
force_push_head_branch: bool | None = None
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
from pydantic import BaseModel
2+
3+
4+
class PRPartContext(BaseModel):
5+
"""Represents a GitHub pull request part parsed from a webhook payload"""
6+
7+
ref: str
8+
sha: str
9+
10+
@classmethod
11+
def from_payload(cls, payload: dict) -> "PRPartContext":
12+
return cls(ref=payload.get("ref"), sha=payload.get("sha"))
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
from pydantic import BaseModel
2+
3+
from codegen.git.models.github_named_user_context import GithubNamedUserContext
4+
from codegen.git.models.pr_part_context import PRPartContext
5+
from codegen.git.schemas.github import GithubType
6+
7+
8+
class PullRequestContext(BaseModel):
9+
"""Represents a GitHub pull request"""
10+
11+
id: int
12+
url: str
13+
html_url: str
14+
number: int
15+
state: str
16+
title: str
17+
user: GithubNamedUserContext
18+
body: str
19+
draft: bool
20+
head: PRPartContext
21+
base: PRPartContext
22+
merged: bool | None
23+
merged_by: dict | None
24+
additions: int | None
25+
deletions: int | None
26+
changed_files: int | None
27+
github_type: GithubType | None = None
28+
webhook_data: dict | None = None
29+
30+
@classmethod
31+
def from_payload(cls, webhook_payload: dict) -> "PullRequestContext":
32+
webhook_data = webhook_payload.get("pull_request", {})
33+
return cls(
34+
id=webhook_data.get("id"),
35+
url=webhook_data.get("url"),
36+
html_url=webhook_data.get("html_url"),
37+
number=webhook_data.get("number"),
38+
state=webhook_data.get("state"),
39+
title=webhook_data.get("title"),
40+
user=GithubNamedUserContext.from_payload(webhook_data.get("user", {})),
41+
body=webhook_data.get("body"),
42+
draft=webhook_data.get("draft"),
43+
head=PRPartContext.from_payload(webhook_data.get("head", {})),
44+
base=PRPartContext.from_payload(webhook_data.get("base", {})),
45+
merged=webhook_data.get("merged"),
46+
merged_by=webhook_data.get("merged_by", {}),
47+
additions=webhook_data.get("additions"),
48+
deletions=webhook_data.get("deletions"),
49+
changed_files=webhook_data.get("changed_files"),
50+
github_type=GithubType.from_url(webhook_data.get("html_url")),
51+
webhook_data=webhook_data,
52+
)

src/codegen/git/repo_operator/repo_operator.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
from codegen.git.schemas.enums import CheckoutResult, FetchResult
1919
from codegen.git.schemas.repo_config import BaseRepoConfig
2020
from codegen.utils.performance.stopwatch_utils import stopwatch
21-
from codegen.utils.time_utils import humanize_duration
21+
from codegen.utils.performance.time_utils import humanize_duration
2222

2323
logger = logging.getLogger(__name__)
2424

src/codegen/gscli/generate/runner_imports.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,10 @@
1212
import plotly
1313
""".strip()
1414
CODEGEN_IMPORTS = """
15-
from app.codemod.compilation.models.context import CodemodContext
16-
from app.codemod.compilation.models.github_named_user_context import GithubNamedUserContext
17-
from app.codemod.compilation.models.pr_part_context import PRPartContext
18-
from app.codemod.compilation.models.pull_request_context import PullRequestContext
15+
from codegen.git.models.codemod_context import CodemodContext
16+
from codegen.git.models.github_named_user_context import GithubNamedUserContext
17+
from codegen.git.models.pr_part_context import PRPartContext
18+
from codegen.git.models.pull_request_context import PullRequestContext
1919
"""
2020
# TODO: these should also be made public (i.e. included in the docs site)
2121
GS_PRIVATE_IMPORTS = """

src/codegen/gscli/generate/utils.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,11 @@ def generate_builtins_file(path_to_builtins: str, language_type: LanguageType):
3232
# This file is auto-generated, do not modify manually
3333
3434
{{all_imports}}
35-
from app.codemod.compilation.models.context import CodemodContext
36-
from app.codemod.compilation.models.pr_options import PROptions
37-
from app.codemod.compilation.models.github_named_user_context import GithubNamedUserContext
38-
from app.codemod.compilation.models.pr_part_context import PRPartContext
39-
from app.codemod.compilation.models.pull_request_context import PullRequestContext
35+
from codegen.git.models.codemod_context import CodemodContext
36+
from codegen.git.models.pr_options import PROptions
37+
from codegen.git.models.github_named_user_context import GithubNamedUserContext
38+
from codegen.git.models.pr_part_context import PRPartContext
39+
from codegen.git.models.pull_request_context import PullRequestContext
4040
from codegen.sdk.codebase.flagging.code_flag import MessageType as MessageType
4141
4242
{"\n".join(inspect.getsource(codebase).splitlines()[-2:])}

src/codegen/sdk/codebase/flagging/flags.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,11 @@
11
from dataclasses import dataclass, field
2-
from typing import TYPE_CHECKING
32

43
from codegen.sdk.codebase.flagging.code_flag import CodeFlag
54
from codegen.sdk.codebase.flagging.enums import MessageType
5+
from codegen.sdk.codebase.flagging.group import Group
66
from codegen.sdk.core.interfaces.editable import Editable
77
from codegen.utils.decorators.docs import noapidoc
88

9-
if TYPE_CHECKING:
10-
from app.codemod.types import Group
11-
129

1310
@dataclass
1411
class Flags:
@@ -69,7 +66,7 @@ def set_find_mode(self, find_mode: bool) -> None:
6966
self._find_mode = find_mode
7067

7168
@noapidoc
72-
def set_active_group(self, group: "Group") -> None:
69+
def set_active_group(self, group: Group) -> None:
7370
"""Will only fix these flags."""
7471
# TODO - flesh this out more with Group datatype and GroupBy
7572
self._active_group = group.flags
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
from dataclasses import dataclass
2+
3+
from dataclasses_json import dataclass_json
4+
5+
from codegen.sdk.codebase.flagging.code_flag import CodeFlag
6+
from codegen.sdk.codebase.flagging.groupers.enums import GroupBy
7+
8+
DEFAULT_GROUP_ID = 0
9+
10+
11+
@dataclass_json
12+
@dataclass
13+
class Group:
14+
group_by: GroupBy
15+
segment: str
16+
flags: list[CodeFlag] | None = None
17+
id: int = DEFAULT_GROUP_ID
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
from codegen.git.repo_operator.remote_repo_operator import RemoteRepoOperator
2+
from codegen.sdk.codebase.flagging.code_flag import CodeFlag
3+
from codegen.sdk.codebase.flagging.group import Group
4+
from codegen.sdk.codebase.flagging.groupers.base_grouper import BaseGrouper
5+
from codegen.sdk.codebase.flagging.groupers.enums import GroupBy
6+
7+
8+
class AllGrouper(BaseGrouper):
9+
"""Group all flags into one group."""
10+
11+
type: GroupBy = GroupBy.ALL
12+
13+
@staticmethod
14+
def create_all_groups(flags: list[CodeFlag], repo_operator: RemoteRepoOperator | None = None) -> list[Group]:
15+
return [Group(group_by=GroupBy.ALL, segment="all", flags=flags)] if flags else []
16+
17+
@staticmethod
18+
def create_single_group(flags: list[CodeFlag], segment: str, repo_operator: RemoteRepoOperator | None = None) -> Group:
19+
if segment != "all":
20+
raise ValueError(f"❌ Invalid segment for AllGrouper: {segment}. Only 'all' is a valid segment.")
21+
return Group(group_by=GroupBy.ALL, segment=segment, flags=flags)
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import logging
2+
3+
from codegen.git.repo_operator.remote_repo_operator import RemoteRepoOperator
4+
from codegen.sdk.codebase.flagging.code_flag import CodeFlag
5+
from codegen.sdk.codebase.flagging.group import Group
6+
from codegen.sdk.codebase.flagging.groupers.base_grouper import BaseGrouper
7+
from codegen.sdk.codebase.flagging.groupers.enums import GroupBy
8+
9+
logger = logging.getLogger(__name__)
10+
11+
12+
class AppGrouper(BaseGrouper):
13+
"""Group flags by segment=app.
14+
Ex: apps/profile.
15+
"""
16+
17+
type: GroupBy = GroupBy.APP
18+
19+
@staticmethod
20+
def create_all_groups(flags: list[CodeFlag], repo_operator: RemoteRepoOperator | None = None) -> list[Group]:
21+
unique_apps = list({"/".join(flag.filepath.split("/")[:3]) for flag in flags})
22+
groups = []
23+
for idx, app in enumerate(unique_apps):
24+
matches = [f for f in flags if f.filepath.startswith(app)]
25+
if len(matches) > 0:
26+
groups.append(Group(id=idx, group_by=GroupBy.APP, segment=app, flags=matches))
27+
return groups
28+
29+
@staticmethod
30+
def create_single_group(flags: list[CodeFlag], segment: str, repo_operator: RemoteRepoOperator | None = None) -> Group:
31+
segment_flags = [f for f in flags if f.filepath.startswith(segment)]
32+
if len(segment_flags) == 0:
33+
logger.warning(f"🤷‍♀️ No flags found for APP segment: {segment}")
34+
return Group(group_by=GroupBy.APP, segment=segment, flags=segment_flags)
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
from codegen.git.repo_operator.remote_repo_operator import RemoteRepoOperator
2+
from codegen.sdk.codebase.flagging.code_flag import CodeFlag
3+
from codegen.sdk.codebase.flagging.group import Group
4+
from codegen.sdk.codebase.flagging.groupers.enums import GroupBy
5+
6+
7+
class BaseGrouper:
8+
"""Base class of all groupers.
9+
Children of this class should include in their doc string:
10+
- a short desc of what the segment format is. ex: for FileGrouper the segment is a filename
11+
"""
12+
13+
type: GroupBy
14+
15+
def __init__(self) -> None:
16+
if type is None:
17+
raise ValueError("Must set type in BaseGrouper")
18+
19+
@staticmethod
20+
def create_all_groups(flags: list[CodeFlag], repo_operator: RemoteRepoOperator | None = None) -> list[Group]:
21+
raise NotImplementedError("Must implement create_all_groups in BaseGrouper")
22+
23+
@staticmethod
24+
def create_single_group(flags: list[CodeFlag], segment: str, repo_operator: RemoteRepoOperator | None = None) -> Group:
25+
"""TODO: handle the case when 0 flags are passed in"""
26+
raise NotImplementedError("Must implement create_single_group in BaseGrouper")
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
from codegen.git.repo_operator.remote_repo_operator import RemoteRepoOperator
2+
from codegen.sdk.codebase.flagging.code_flag import CodeFlag
3+
from codegen.sdk.codebase.flagging.group import Group
4+
from codegen.sdk.codebase.flagging.groupers.base_grouper import BaseGrouper
5+
from codegen.sdk.codebase.flagging.groupers.enums import GroupBy
6+
7+
DEFAULT_CHUNK_SIZE = 5
8+
9+
10+
class CodeownerGrouper(BaseGrouper):
11+
"""Group flags by CODEOWNERS.
12+
13+
Parses .github/CODEOWNERS and groups by each possible codeowners
14+
15+
Segment should be either a github username or github team name.
16+
"""
17+
18+
type: GroupBy = GroupBy.CODEOWNER
19+
20+
@staticmethod
21+
def create_all_groups(flags: list[CodeFlag], repo_operator: RemoteRepoOperator | None = None) -> list[Group]:
22+
owner_to_group: dict[str, Group] = {}
23+
no_owner_group = Group(group_by=GroupBy.CODEOWNER, segment="@no-owner", flags=[])
24+
for idx, flag in enumerate(flags):
25+
flag_owners = repo_operator.codeowners_parser.of(flag.filepath) # TODO: handle codeowners_parser could be null
26+
if not flag_owners:
27+
no_owner_group.flags.append(flag)
28+
continue
29+
# NOTE: always use the first owner. ex if the line is /dir @team1 @team2 then use team1
30+
flag_owner = flag_owners[0][1]
31+
group = owner_to_group.get(flag_owner, Group(id=idx, group_by=GroupBy.CODEOWNER, segment=flag_owner, flags=[]))
32+
group.flags.append(flag)
33+
owner_to_group[flag_owner] = group
34+
35+
no_owner_group.id = len(owner_to_group)
36+
return [*list(owner_to_group.values()), no_owner_group]
37+
38+
@staticmethod
39+
def create_single_group(flags: list[CodeFlag], segment: str, repo_operator: RemoteRepoOperator | None = None) -> Group:
40+
raise NotImplementedError("TODO: implement single group creation")
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
from codegen.sdk.codebase.flagging.groupers.all_grouper import AllGrouper
2+
from codegen.sdk.codebase.flagging.groupers.app_grouper import AppGrouper
3+
from codegen.sdk.codebase.flagging.groupers.codeowner_grouper import CodeownerGrouper
4+
from codegen.sdk.codebase.flagging.groupers.file_chunk_grouper import FileChunkGrouper
5+
from codegen.sdk.codebase.flagging.groupers.file_grouper import FileGrouper
6+
from codegen.sdk.codebase.flagging.groupers.instance_grouper import InstanceGrouper
7+
8+
ALL_GROUPERS = [
9+
AllGrouper,
10+
AppGrouper,
11+
CodeownerGrouper,
12+
FileChunkGrouper,
13+
FileGrouper,
14+
InstanceGrouper,
15+
]
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
from enum import StrEnum
2+
3+
4+
class GroupBy(StrEnum):
5+
ALL = "all"
6+
APP = "app"
7+
CODEOWNER = "codeowner"
8+
FILE = "file"
9+
FILE_CHUNK = "file_chunk"
10+
HOT_COLD = "hot_cold"
11+
INSTANCE = "instance"

0 commit comments

Comments
 (0)