Skip to content

Commit 80d4bf5

Browse files
authored
ci: pre-commit hooks for static analysis (#510)
* chore: add pre-commit for ruff & black * docs: add pre-commit setup instructions to CONTRIBUTING.md * chore: update ruff configuration in pyproject.toml * when you run pre-commit run --all-files, it should actually format the files instead of just reporting what would change. After the formatting is done, you should see the changes in git status and be able to commit them. * Remove black; will fight over eachother with ruff * run ruff on pre-existing issues * chore(pyproject.toml): remove black, update dirctories * ci: remove black-formatter.yaml * ci: add static-analysis.yaml running pre-commit hook * build: add ruff to development dependencies
1 parent a0d85c5 commit 80d4bf5

42 files changed

Lines changed: 288 additions & 479 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/black-formatter.yml

Lines changed: 0 additions & 29 deletions
This file was deleted.
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
name: Static analysis
2+
3+
# on PR and push to main
4+
on:
5+
push:
6+
branches:
7+
- main
8+
pull_request:
9+
paths:
10+
- '**/*.py'
11+
12+
permissions:
13+
contents: read
14+
15+
# Limit concurrency by workflow/branch combination.
16+
#
17+
# For pull request builds, pushing additional changes to the
18+
# branch will cancel prior in-progress and pending builds.
19+
#
20+
# For builds triggered on a branch push, additional changes
21+
# will wait for prior builds to complete before starting.
22+
#
23+
# https://docs.github.com/en/actions/using-jobs/using-concurrency
24+
concurrency:
25+
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
26+
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
27+
28+
jobs:
29+
pre-commit-checks:
30+
name: Pre-commit checks
31+
runs-on: ubuntu-latest
32+
33+
steps:
34+
- uses: actions/checkout@v4
35+
with:
36+
persist-credentials: false
37+
fetch-depth: 0
38+
39+
- name: Set up Python
40+
uses: actions/setup-python@v5
41+
id: setup_python
42+
with:
43+
python-version: "3.11.10"
44+
45+
- name: UV Cache
46+
# Manually cache the uv cache directory
47+
# until setup-python supports it:
48+
# https://github.com/actions/setup-python/issues/822
49+
uses: actions/cache@v4
50+
id: cache-uv
51+
with:
52+
path: ~/.cache/uv
53+
key: uvcache-${{ runner.os }}-${{ steps.setup_python.outputs.python-version }}-${{ hashFiles('pyproject.toml') }}
54+
55+
- name: Install packages
56+
run: |
57+
python -m pip install -U uv pre-commit
58+
uv pip install --upgrade --system -e .[dev]
59+
60+
- name: Run pre-commit
61+
run: |
62+
pre-commit run --show-diff-on-failure --color=always --all-files

.pre-commit-config.yaml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
repos:
2+
- repo: https://github.com/astral-sh/ruff-pre-commit
3+
rev: "v0.2.1"
4+
hooks:
5+
- id: ruff
6+
args: [--fix, --exit-non-zero-on-fix]
7+
- id: ruff-format

CONTRIBUTING.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,18 @@ Even if you're not ready to contribute code, we'd love to hear your thoughts. Dr
7777
.\venv\Scripts\activate # Windows
7878
```
7979

80+
3. **Pre-commit Setup**:
81+
We use pre-commit hooks to automatically format and lint code. Set them up with:
82+
```bash
83+
pip install pre-commit
84+
pre-commit install
85+
```
86+
87+
That's it! The hooks will run automatically when you commit. To manually check all files:
88+
```bash
89+
pre-commit run --all-files
90+
```
91+
8092
## Testing
8193

8294
We use a comprehensive testing stack to ensure code quality and reliability. Our testing framework includes pytest and several specialized testing tools.

agentops/cli.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,7 @@ def main():
66
parser = argparse.ArgumentParser(description="AgentOps CLI")
77
subparsers = parser.add_subparsers(dest="command")
88

9-
timetravel_parser = subparsers.add_parser(
10-
"timetravel", help="Time Travel Debugging commands", aliases=["tt"]
11-
)
9+
timetravel_parser = subparsers.add_parser("timetravel", help="Time Travel Debugging commands", aliases=["tt"])
1210
timetravel_parser.add_argument(
1311
"branch_name",
1412
type=str,

agentops/client.py

Lines changed: 12 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,7 @@ def __init__(self):
4444
api_key=os.environ.get("AGENTOPS_API_KEY"),
4545
parent_key=os.environ.get("AGENTOPS_PARENT_KEY"),
4646
endpoint=os.environ.get("AGENTOPS_API_ENDPOINT"),
47-
env_data_opt_out=os.environ.get(
48-
"AGENTOPS_ENV_DATA_OPT_OUT", "False"
49-
).lower()
50-
== "true",
47+
env_data_opt_out=os.environ.get("AGENTOPS_ENV_DATA_OPT_OUT", "False").lower() == "true",
5148
)
5249

5350
def configure(
@@ -106,9 +103,7 @@ def initialize(self) -> Union[Session, None]:
106103

107104
if session:
108105
for agent_args in self._pre_init_queue["agents"]:
109-
session.create_agent(
110-
name=agent_args["name"], agent_id=agent_args["agent_id"]
111-
)
106+
session.create_agent(name=agent_args["name"], agent_id=agent_args["agent_id"])
112107
self._pre_init_queue["agents"] = []
113108

114109
return session
@@ -141,9 +136,7 @@ def add_tags(self, tags: List[str]) -> None:
141136

142137
session = self._safe_get_session()
143138
if session is None:
144-
return logger.warning(
145-
"Could not add tags. Start a session by calling agentops.start_session()."
146-
)
139+
return logger.warning("Could not add tags. Start a session by calling agentops.start_session().")
147140

148141
session.add_tags(tags=tags)
149142

@@ -162,9 +155,7 @@ def set_tags(self, tags: List[str]) -> None:
162155
session = self._safe_get_session()
163156

164157
if session is None:
165-
return logger.warning(
166-
"Could not set tags. Start a session by calling agentops.start_session()."
167-
)
158+
return logger.warning("Could not set tags. Start a session by calling agentops.start_session().")
168159
else:
169160
session.set_tags(tags=tags)
170161

@@ -198,9 +189,7 @@ def record(self, event: Union[Event, ErrorEvent]) -> None:
198189

199190
session = self._safe_get_session()
200191
if session is None:
201-
return logger.error(
202-
"Could not record event. Start a session by calling agentops.start_session()."
203-
)
192+
return logger.error("Could not record event. Start a session by calling agentops.start_session().")
204193
session.record(event)
205194

206195
def start_session(
@@ -244,9 +233,7 @@ def start_session(
244233

245234
if self._pre_init_queue["agents"] and len(self._pre_init_queue["agents"]) > 0:
246235
for agent_args in self._pre_init_queue["agents"]:
247-
session.create_agent(
248-
name=agent_args["name"], agent_id=agent_args["agent_id"]
249-
)
236+
session.create_agent(name=agent_args["name"], agent_id=agent_args["agent_id"])
250237
self._pre_init_queue["agents"] = []
251238

252239
self._sessions.append(session)
@@ -277,9 +264,7 @@ def end_session(
277264
if is_auto_end and self._config.skip_auto_end_session:
278265
return
279266

280-
token_cost = session.end_session(
281-
end_state=end_state, end_state_reason=end_state_reason, video=video
282-
)
267+
token_cost = session.end_session(end_state=end_state, end_state_reason=end_state_reason, video=video)
283268

284269
return token_cost
285270

@@ -299,9 +284,7 @@ def create_agent(
299284
# if no session passed, assume single session
300285
session = self._safe_get_session()
301286
if session is None:
302-
self._pre_init_queue["agents"].append(
303-
{"name": name, "agent_id": agent_id}
304-
)
287+
self._pre_init_queue["agents"].append({"name": name, "agent_id": agent_id})
305288
else:
306289
session.create_agent(name=name, agent_id=agent_id)
307290

@@ -326,9 +309,7 @@ def signal_handler(signum, frame):
326309
"""
327310
signal_name = "SIGINT" if signum == signal.SIGINT else "SIGTERM"
328311
logger.info("%s detected. Ending session...", signal_name)
329-
self.end_session(
330-
end_state="Fail", end_state_reason=f"Signal {signal_name} detected"
331-
)
312+
self.end_session(end_state="Fail", end_state_reason=f"Signal {signal_name} detected")
332313
sys.exit(0)
333314

334315
def handle_exception(exc_type, exc_value, exc_traceback):
@@ -341,9 +322,7 @@ def handle_exception(exc_type, exc_value, exc_traceback):
341322
exc_traceback (TracebackType): A traceback object encapsulating the call stack at the
342323
point where the exception originally occurred.
343324
"""
344-
formatted_traceback = "".join(
345-
traceback.format_exception(exc_type, exc_value, exc_traceback)
346-
)
325+
formatted_traceback = "".join(traceback.format_exception(exc_type, exc_value, exc_traceback))
347326

348327
for session in self._sessions:
349328
session.end_session(
@@ -376,13 +355,7 @@ def add_pre_init_warning(self, message: str):
376355
# replaces the session currently stored with a specific session_id, with a new session
377356
def _update_session(self, session: Session):
378357
self._sessions[
379-
self._sessions.index(
380-
[
381-
sess
382-
for sess in self._sessions
383-
if sess.session_id == session.session_id
384-
][0]
385-
)
358+
self._sessions.index([sess for sess in self._sessions if sess.session_id == session.session_id][0])
386359
] = session
387360

388361
def _safe_get_session(self) -> Optional[Session]:
@@ -392,9 +365,7 @@ def _safe_get_session(self) -> Optional[Session]:
392365
return self._sessions[0]
393366

394367
if len(self._sessions) > 1:
395-
calling_function = inspect.stack()[
396-
2
397-
].function # Using index 2 because we have a wrapper at index 1
368+
calling_function = inspect.stack()[2].function # Using index 2 because we have a wrapper at index 1
398369
return logger.warning(
399370
f"Multiple sessions detected. You must use session.{calling_function}(). More info: https://docs.agentops.ai/v1/concepts/core-concepts#session-management"
400371
)

agentops/decorators.py

Lines changed: 8 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,7 @@ async def async_wrapper(*args, session: Optional[Session] = None, **kwargs):
4545
arg_names = list(func_args.keys())
4646
# Get default values
4747
arg_values = {
48-
name: func_args[name].default
49-
for name in arg_names
50-
if func_args[name].default is not inspect._empty
48+
name: func_args[name].default for name in arg_names if func_args[name].default is not inspect._empty
5149
}
5250
# Update with positional arguments
5351
arg_values.update(dict(zip(arg_names, args)))
@@ -111,9 +109,7 @@ def sync_wrapper(*args, session: Optional[Session] = None, **kwargs):
111109
arg_names = list(func_args.keys())
112110
# Get default values
113111
arg_values = {
114-
name: func_args[name].default
115-
for name in arg_names
116-
if func_args[name].default is not inspect._empty
112+
name: func_args[name].default for name in arg_names if func_args[name].default is not inspect._empty
117113
}
118114
# Update with positional arguments
119115
arg_values.update(dict(zip(arg_names, args)))
@@ -191,9 +187,7 @@ async def async_wrapper(*args, session: Optional[Session] = None, **kwargs):
191187
arg_names = list(func_args.keys())
192188
# Get default values
193189
arg_values = {
194-
name: func_args[name].default
195-
for name in arg_names
196-
if func_args[name].default is not inspect._empty
190+
name: func_args[name].default for name in arg_names if func_args[name].default is not inspect._empty
197191
}
198192
# Update with positional arguments
199193
arg_values.update(dict(zip(arg_names, args)))
@@ -257,9 +251,7 @@ def sync_wrapper(*args, session: Optional[Session] = None, **kwargs):
257251
arg_names = list(func_args.keys())
258252
# Get default values
259253
arg_values = {
260-
name: func_args[name].default
261-
for name in arg_names
262-
if func_args[name].default is not inspect._empty
254+
name: func_args[name].default for name in arg_names if func_args[name].default is not inspect._empty
263255
}
264256
# Update with positional arguments
265257
arg_values.update(dict(zip(arg_names, args)))
@@ -338,20 +330,17 @@ def new_init(self, *args, **kwargs):
338330
session=session,
339331
)
340332
except AttributeError as e:
341-
Client().add_pre_init_warning(
342-
f"Failed to track an agent {name} with the @track_agent decorator."
343-
)
344-
logger.warning(
345-
"Failed to track an agent with the @track_agent decorator."
346-
)
333+
Client().add_pre_init_warning(f"Failed to track an agent {name} with the @track_agent decorator.")
334+
logger.warning("Failed to track an agent with the @track_agent decorator.")
347335
original_init(self, *args, **kwargs)
348336

349337
obj.__init__ = new_init
350338

351339
elif inspect.isfunction(obj):
352340
obj.agent_ops_agent_id = str(uuid4()) # type: ignore
353341
Client().create_agent(
354-
name=obj.agent_ops_agent_name, agent_id=obj.agent_ops_agent_id # type: ignore
342+
name=obj.agent_ops_agent_name,
343+
agent_id=obj.agent_ops_agent_id, # type: ignore
355344
)
356345

357346
else:

agentops/helpers.py

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99

1010
from .log_config import logger
1111
from uuid import UUID
12-
from importlib.metadata import version
1312

1413

1514
def get_ISO_time():
@@ -38,7 +37,9 @@ def filter_dict(obj):
3837
k: (
3938
filter_dict(v)
4039
if isinstance(v, (dict, list)) or is_jsonable(v)
41-
else str(v) if isinstance(v, UUID) else ""
40+
else str(v)
41+
if isinstance(v, UUID)
42+
else ""
4243
)
4344
for k, v in obj.items()
4445
}
@@ -47,7 +48,9 @@ def filter_dict(obj):
4748
(
4849
filter_dict(x)
4950
if isinstance(x, (dict, list)) or is_jsonable(x)
50-
else str(x) if isinstance(x, UUID) else ""
51+
else str(x)
52+
if isinstance(x, UUID)
53+
else ""
5154
)
5255
for x in obj
5356
]
@@ -85,9 +88,7 @@ def remove_unwanted_items(value):
8588
"""Recursively remove self key and None/... values from dictionaries so they aren't serialized"""
8689
if isinstance(value, dict):
8790
return {
88-
k: remove_unwanted_items(v)
89-
for k, v in value.items()
90-
if v is not None and v is not ... and k != "self"
91+
k: remove_unwanted_items(v) for k, v in value.items() if v is not None and v is not ... and k != "self"
9192
}
9293
elif isinstance(value, list):
9394
return [remove_unwanted_items(item) for item in value]
@@ -106,9 +107,7 @@ def check_call_stack_for_agent_id() -> Union[UUID, None]:
106107
# We stop looking up the stack at main because after that we see global variables
107108
if var == "__main__":
108109
return None
109-
if hasattr(var, "agent_ops_agent_id") and getattr(
110-
var, "agent_ops_agent_id"
111-
):
110+
if hasattr(var, "agent_ops_agent_id") and getattr(var, "agent_ops_agent_id"):
112111
logger.debug(
113112
"LLM call from agent named: %s",
114113
getattr(var, "agent_ops_agent_name"),
@@ -141,7 +140,7 @@ def check_agentops_update():
141140

142141
if not latest_version == current_version:
143142
logger.warning(
144-
f" WARNING: agentops is out of date. Please update with the command: 'pip install --upgrade agentops'"
143+
" WARNING: agentops is out of date. Please update with the command: 'pip install --upgrade agentops'"
145144
)
146145
except Exception as e:
147146
logger.debug(f"Failed to check for updates: {e}")

0 commit comments

Comments
 (0)