Skip to content

Commit 84f5d29

Browse files
authored
Merge branch 'main' into docs/connections-incase-typo
2 parents e7553b6 + 63561ce commit 84f5d29

1 file changed

Lines changed: 137 additions & 0 deletions

File tree

scripts/check_new_py_files.py

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
#!/usr/bin/env python3
2+
# Copyright 2026 Google LLC
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
16+
"""Checks that newly-added Python files under src/google/adk/ have a '_' prefix.
17+
18+
ADK is private-by-default: a newly-added Python file under src/google/adk/ must
19+
have a '_'-prefixed basename. To make it public, add the symbol to the package
20+
__init__.py / __all__ instead. See
21+
.agents/skills/adk-style/references/visibility.md.
22+
23+
Newly-added files are detected by diffing the working tree against a baseline
24+
source tree (e.g. an origin/main checkout), so it works in a checkout that has
25+
no local git history:
26+
27+
python scripts/check_new_py_files.py --baseline-dir /path/to/origin-main
28+
29+
Exit codes: 0 = ok, 1 = violation(s) found, 2 = usage/setup error.
30+
"""
31+
32+
from __future__ import annotations
33+
34+
import argparse
35+
import os
36+
import sys
37+
38+
_PACKAGE_RELPATH = os.path.join('src', 'google', 'adk')
39+
40+
_VIOLATION_LINE = "Error: New Python file '{path}' must have a '_' prefix."
41+
42+
_GUIDANCE = (
43+
'All new Python files in src/google/adk/ must be private by default.\n'
44+
'To expose a public interface, use __init__.py and list public symbols'
45+
' in __all__.\n'
46+
'See .agents/skills/adk-style/references/visibility.md for details.'
47+
)
48+
49+
# Subtrees that may exist in the working tree but are intentionally absent from
50+
# the baseline tree; ignore them so the diff does not report them as newly
51+
# added.
52+
_IGNORED_PREFIXES = (
53+
'src/google/adk/internal/',
54+
'src/google/adk/v1/',
55+
'src/google/adk/platform/internal/',
56+
)
57+
58+
59+
def find_py_files(root: str) -> set[str]:
60+
"""Returns root-relative paths of every *.py under <root>/src/google/adk.
61+
62+
Each path includes the src/google/adk/ prefix (e.g.
63+
'src/google/adk/agents/foo.py'). Symlinks are followed so that a src/google/adk
64+
tree assembled from symlinked subdirectories is walked correctly.
65+
"""
66+
package_root = os.path.join(root, _PACKAGE_RELPATH)
67+
found: set[str] = set()
68+
for dirpath, _, filenames in os.walk(package_root, followlinks=True):
69+
for name in filenames:
70+
if name.endswith('.py'):
71+
abs_path = os.path.join(dirpath, name)
72+
found.add(os.path.relpath(abs_path, root))
73+
return found
74+
75+
76+
def _should_check(relpath: str) -> bool:
77+
"""Returns False for paths under an ignored prefix."""
78+
return not any(relpath.startswith(prefix) for prefix in _IGNORED_PREFIXES)
79+
80+
81+
def added_py_files(new_root: str, baseline_root: str) -> set[str]:
82+
"""Returns .py files present in new_root but not in baseline_root.
83+
84+
Paths under _IGNORED_PREFIXES are skipped: they may exist in the working tree
85+
but are intentionally absent from the baseline, so a plain diff would
86+
otherwise report them as newly added.
87+
"""
88+
added = find_py_files(new_root) - find_py_files(baseline_root)
89+
return {path for path in added if _should_check(path)}
90+
91+
92+
def find_violations(added: set[str]) -> list[str]:
93+
"""Returns the sorted added files whose basename does not start with '_'."""
94+
return sorted(
95+
path for path in added if not os.path.basename(path).startswith('_')
96+
)
97+
98+
99+
def _has_package_dir(root: str) -> bool:
100+
return os.path.isdir(os.path.join(root, _PACKAGE_RELPATH))
101+
102+
103+
def _parse_args(argv: list[str]) -> argparse.Namespace:
104+
parser = argparse.ArgumentParser(description=__doc__)
105+
parser.add_argument(
106+
'--baseline-dir',
107+
required=True,
108+
help='Baseline source tree to diff against (an origin/main checkout).',
109+
)
110+
parser.add_argument(
111+
'--new-dir',
112+
default='.',
113+
help='New source tree to check (default: current directory).',
114+
)
115+
return parser.parse_args(argv)
116+
117+
118+
def main(argv: list[str]) -> int:
119+
args = _parse_args(argv)
120+
for label, root in (('baseline', args.baseline_dir), ('new', args.new_dir)):
121+
if not _has_package_dir(root):
122+
print(
123+
f'Error: {label} tree has no {_PACKAGE_RELPATH} directory: {root}',
124+
file=sys.stderr,
125+
)
126+
return 2
127+
128+
violations = find_violations(added_py_files(args.new_dir, args.baseline_dir))
129+
for path in violations:
130+
print(_VIOLATION_LINE.format(path=path), file=sys.stderr)
131+
if violations:
132+
print(_GUIDANCE, file=sys.stderr)
133+
return 1 if violations else 0
134+
135+
136+
if __name__ == '__main__':
137+
sys.exit(main(sys.argv[1:]))

0 commit comments

Comments
 (0)