Skip to content

Commit 30ed559

Browse files
committed
chore(scopes): split and sync configuration schema
This changes split the configuration codes and sync it from the engine. This will allow to build a CI job to sync the ci/scopes/config/scopes.py file. Change-Id: Ib976077362efebc9f3d1548e48cbad370372ecd3
1 parent 0442aa2 commit 30ed559

File tree

4 files changed

+133
-59
lines changed

4 files changed

+133
-59
lines changed

mergify_cli/ci/scopes/cli.py

Lines changed: 5 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@
66

77
import click
88
import pydantic
9-
import yaml
109

1110
from mergify_cli import utils
1211
from mergify_cli.ci.scopes import base_detector
1312
from mergify_cli.ci.scopes import changed_files
13+
from mergify_cli.ci.scopes import config
1414
from mergify_cli.ci.scopes import exceptions
1515

1616

@@ -19,65 +19,11 @@
1919

2020

2121
SCOPE_PREFIX = "scope_"
22-
SCOPE_NAME_RE = r"^[A-Za-z0-9_-]+$"
23-
24-
25-
class ConfigInvalidError(exceptions.ScopesError):
26-
pass
27-
28-
29-
ScopeName = typing.Annotated[
30-
str,
31-
pydantic.StringConstraints(pattern=SCOPE_NAME_RE, min_length=1),
32-
]
33-
34-
35-
class FileFilters(pydantic.BaseModel):
36-
include: tuple[str, ...] = pydantic.Field(default_factory=lambda: ("**/*",))
37-
exclude: tuple[str, ...] = pydantic.Field(default_factory=tuple)
38-
39-
40-
class SourceFiles(pydantic.BaseModel):
41-
files: dict[ScopeName, FileFilters]
42-
43-
44-
class SourceManual(pydantic.BaseModel):
45-
manual: None
46-
47-
48-
class ScopesConfig(pydantic.BaseModel):
49-
model_config = pydantic.ConfigDict(extra="forbid")
50-
51-
source: SourceFiles | SourceManual | None = None
52-
merge_queue_scope: str | None = "merge-queue"
53-
54-
55-
class Config(pydantic.BaseModel):
56-
model_config = pydantic.ConfigDict(extra="ignore")
57-
58-
scopes: ScopesConfig
59-
60-
@classmethod
61-
def from_dict(cls, data: dict[str, typing.Any] | typing.Any) -> Config: # noqa: ANN401
62-
try:
63-
return cls.model_validate(data)
64-
except pydantic.ValidationError as e:
65-
raise ConfigInvalidError(e)
66-
67-
@classmethod
68-
def from_yaml(cls, path: str) -> Config:
69-
with pathlib.Path(path).open(encoding="utf-8") as f:
70-
try:
71-
data = yaml.safe_load(f) or {}
72-
except yaml.YAMLError as e:
73-
raise ConfigInvalidError(e)
74-
75-
return cls.from_dict(data)
7622

7723

7824
def match_scopes(
7925
files: abc.Iterable[str],
80-
filters: dict[ScopeName, FileFilters],
26+
filters: dict[config.ScopeName, config.FileFilters],
8127
) -> tuple[set[str], dict[str, list[str]]]:
8228
scopes_hit: set[str] = set()
8329
per_scope: dict[str, list[str]] = {s: [] for s in filters}
@@ -142,7 +88,7 @@ def load_from_file(cls, filename: str) -> DetectedScope:
14288

14389

14490
def detect(config_path: str) -> DetectedScope:
145-
cfg = Config.from_yaml(config_path)
91+
cfg = config.Config.from_yaml(config_path)
14692
base = base_detector.detect()
14793

14894
scopes_hit: set[str]
@@ -153,11 +99,11 @@ def detect(config_path: str) -> DetectedScope:
15399
all_scopes = set()
154100
scopes_hit = set()
155101
per_scope = {}
156-
elif isinstance(source, SourceFiles):
102+
elif isinstance(source, config.SourceFiles):
157103
changed = changed_files.git_changed_files(base.ref)
158104
all_scopes = set(source.files.keys())
159105
scopes_hit, per_scope = match_scopes(changed, source.files)
160-
elif isinstance(source, SourceManual):
106+
elif isinstance(source, config.SourceManual):
161107
msg = "source `manual` has been set, scopes must be send with `scopes-send` or API"
162108
raise exceptions.ScopesError(msg)
163109
else:
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
from mergify_cli.ci.scopes import exceptions
2+
from mergify_cli.ci.scopes.config.root import Config
3+
from mergify_cli.ci.scopes.config.root import ConfigInvalidError
4+
from mergify_cli.ci.scopes.config.scopes import FileFilters
5+
from mergify_cli.ci.scopes.config.scopes import ScopeName
6+
from mergify_cli.ci.scopes.config.scopes import Scopes
7+
from mergify_cli.ci.scopes.config.scopes import SourceFiles
8+
from mergify_cli.ci.scopes.config.scopes import SourceManual
9+
10+
11+
__all__ = [
12+
"Config",
13+
"ConfigInvalidError",
14+
"FileFilters",
15+
"ScopeName",
16+
"Scopes",
17+
"SourceFiles",
18+
"SourceManual",
19+
"exceptions",
20+
]
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import pathlib
2+
import typing
3+
4+
import pydantic
5+
import yaml
6+
7+
from mergify_cli.ci.scopes import exceptions
8+
from mergify_cli.ci.scopes.config.scopes import Scopes
9+
10+
11+
class ConfigInvalidError(exceptions.ScopesError):
12+
pass
13+
14+
15+
class Config(pydantic.BaseModel):
16+
model_config = pydantic.ConfigDict(extra="ignore")
17+
18+
scopes: Scopes
19+
20+
@classmethod
21+
def from_dict(
22+
cls,
23+
data: dict[str, typing.Any] | typing.Any, # noqa: ANN401
24+
) -> typing.Self:
25+
try:
26+
return cls.model_validate(data)
27+
except pydantic.ValidationError as e:
28+
raise ConfigInvalidError(e)
29+
30+
@classmethod
31+
def from_yaml(cls, path: str) -> typing.Self:
32+
with pathlib.Path(path).open(encoding="utf-8") as f:
33+
try:
34+
data = yaml.safe_load(f) or {}
35+
except yaml.YAMLError as e:
36+
raise ConfigInvalidError(e)
37+
38+
return cls.from_dict(data)
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
from __future__ import annotations
2+
3+
import typing
4+
5+
import pydantic
6+
7+
8+
SCOPE_NAME_RE = r"^[A-Za-z0-9_-]+$"
9+
10+
11+
ScopeName = typing.Annotated[
12+
str,
13+
pydantic.StringConstraints(pattern=SCOPE_NAME_RE, min_length=1),
14+
]
15+
16+
17+
class FileFilters(pydantic.BaseModel):
18+
include: tuple[str, ...] = pydantic.Field(
19+
default_factory=lambda: ("**/*",),
20+
description=(
21+
"Glob patterns of files to include for this scope. "
22+
"Empty means 'include everything' before exclusions. "
23+
"Examples: ('src/**/*.py', 'Makefile')"
24+
),
25+
)
26+
exclude: tuple[str, ...] = pydantic.Field(
27+
default_factory=tuple,
28+
description=(
29+
"Glob patterns of files to exclude from this scope. "
30+
"Evaluated after `include` and takes precedence. "
31+
"Examples: ('**/tests/**', '*.md')"
32+
),
33+
)
34+
35+
36+
class SourceFiles(pydantic.BaseModel):
37+
files: dict[ScopeName, FileFilters] = pydantic.Field(
38+
description=(
39+
"Mapping of scope name to its file filters. "
40+
"A file belongs to a scope if it matches the scope's `include` "
41+
"patterns and not its `exclude` patterns."
42+
),
43+
)
44+
45+
46+
class SourceManual(pydantic.BaseModel):
47+
manual: None = pydantic.Field(
48+
description="Scopes are manually sent via API or `mergify scopes-send`",
49+
)
50+
51+
52+
class Scopes(pydantic.BaseModel):
53+
model_config = pydantic.ConfigDict(extra="ignore")
54+
55+
source: SourceFiles | SourceManual | None = pydantic.Field(
56+
default=None,
57+
description=(
58+
"Where scopes come from. "
59+
"`files` uses file-pattern rules (`gha-mergify-ci-scopes` must have been setup on your pull request); "
60+
"`manual` uses scopes sent via API or `mergify scopes-send`; "
61+
"`None` disables scoping."
62+
),
63+
)
64+
merge_queue_scope: str | None = pydantic.Field(
65+
default="merge-queue",
66+
description=(
67+
"Optional scope name automatically applied to merge queue PRs. "
68+
"Set to `None` to disable."
69+
),
70+
)

0 commit comments

Comments
 (0)