Skip to content

Commit cfaee2a

Browse files
committed
Merge branch 'main' of https://github.com/codeflash-ai/codeflash into updated-vsc-extension
2 parents 6af7d4c + d2170e0 commit cfaee2a

File tree

8 files changed

+617
-56
lines changed

8 files changed

+617
-56
lines changed

codeflash/api/cfapi.py

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -45,13 +45,31 @@ def make_cfapi_request(
4545
cfapi_headers = {"Authorization": f"Bearer {get_codeflash_api_key()}"}
4646
if extra_headers:
4747
cfapi_headers.update(extra_headers)
48-
if method.upper() == "POST":
49-
json_payload = json.dumps(payload, indent=None, default=pydantic_encoder)
50-
cfapi_headers["Content-Type"] = "application/json"
51-
response = requests.post(url, data=json_payload, headers=cfapi_headers, timeout=60)
52-
else:
53-
response = requests.get(url, headers=cfapi_headers, timeout=60)
54-
return response
48+
try:
49+
if method.upper() == "POST":
50+
json_payload = json.dumps(payload, indent=None, default=pydantic_encoder)
51+
cfapi_headers["Content-Type"] = "application/json"
52+
response = requests.post(url, data=json_payload, headers=cfapi_headers, timeout=60)
53+
else:
54+
response = requests.get(url, headers=cfapi_headers, timeout=60)
55+
response.raise_for_status()
56+
return response # noqa: TRY300
57+
except requests.exceptions.HTTPError:
58+
# response may be either a string or JSON, so we handle both cases
59+
error_message = ""
60+
try:
61+
json_response = response.json()
62+
if "error" in json_response:
63+
error_message = json_response["error"]
64+
elif "message" in json_response:
65+
error_message = json_response["message"]
66+
except (ValueError, TypeError):
67+
error_message = response.text
68+
69+
logger.error(
70+
f"CF_API_Error:: making request to Codeflash API (url: {url}, method: {method}, status {response.status_code}): {error_message}"
71+
)
72+
return response
5573

5674

5775
@lru_cache(maxsize=1)
@@ -165,10 +183,7 @@ def is_github_app_installed_on_repo(owner: str, repo: str) -> bool:
165183
:return: The response object.
166184
"""
167185
response = make_cfapi_request(endpoint=f"/is-github-app-installed?repo={repo}&owner={owner}", method="GET")
168-
if not response.ok or response.text != "true":
169-
logger.error(f"Error: {response.text}")
170-
return False
171-
return True
186+
return response.ok and response.text == "true"
172187

173188

174189
def get_blocklisted_functions() -> dict[str, set[str]] | dict[str, Any]:

codeflash/cli_cmds/cli.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -137,9 +137,6 @@ def process_pyproject_config(args: Namespace) -> Namespace:
137137
assert Path(args.benchmarks_root).is_dir(), (
138138
f"--benchmarks-root {args.benchmarks_root} must be a valid directory"
139139
)
140-
assert Path(args.benchmarks_root).resolve().is_relative_to(Path(args.tests_root).resolve()), (
141-
f"--benchmarks-root {args.benchmarks_root} must be a subdirectory of --tests-root {args.tests_root}"
142-
)
143140
if env_utils.get_pr_number() is not None:
144141
import git
145142

codeflash/code_utils/code_replacer.py

Lines changed: 87 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
import isort
99
import libcst as cst
10-
import libcst.matchers as m
10+
from libcst.metadata import PositionProvider
1111

1212
from codeflash.cli_cmds.console import logger
1313
from codeflash.code_utils.code_extractor import add_global_assignments, add_needed_imports_from_module
@@ -37,6 +37,55 @@ def normalize_code(code: str) -> str:
3737
return ast.unparse(normalize_node(ast.parse(code)))
3838

3939

40+
class AddRequestArgument(cst.CSTTransformer):
41+
METADATA_DEPENDENCIES = (PositionProvider,)
42+
43+
def leave_FunctionDef(self, original_node: cst.FunctionDef, updated_node: cst.FunctionDef) -> cst.FunctionDef:
44+
# Matcher for '@fixture' or '@pytest.fixture'
45+
for decorator in original_node.decorators:
46+
dec = decorator.decorator
47+
48+
if isinstance(dec, cst.Call):
49+
func_name = ""
50+
if isinstance(dec.func, cst.Attribute) and isinstance(dec.func.value, cst.Name):
51+
if dec.func.attr.value == "fixture" and dec.func.value.value == "pytest":
52+
func_name = "pytest.fixture"
53+
elif isinstance(dec.func, cst.Name) and dec.func.value == "fixture":
54+
func_name = "fixture"
55+
56+
if func_name:
57+
for arg in dec.args:
58+
if (
59+
arg.keyword
60+
and arg.keyword.value == "autouse"
61+
and isinstance(arg.value, cst.Name)
62+
and arg.value.value == "True"
63+
):
64+
args = updated_node.params.params
65+
arg_names = {arg.name.value for arg in args}
66+
67+
# Skip if 'request' is already present
68+
if "request" in arg_names:
69+
return updated_node
70+
71+
# Create a new 'request' param
72+
request_param = cst.Param(name=cst.Name("request"))
73+
74+
# Add 'request' as the first argument (after 'self' or 'cls' if needed)
75+
if args:
76+
first_arg = args[0].name.value
77+
if first_arg in {"self", "cls"}:
78+
new_params = [args[0], request_param] + list(args[1:]) # noqa: RUF005
79+
else:
80+
new_params = [request_param] + list(args) # noqa: RUF005
81+
else:
82+
new_params = [request_param]
83+
84+
new_param_list = updated_node.params.with_changes(params=new_params)
85+
return updated_node.with_changes(params=new_param_list)
86+
return updated_node
87+
88+
4089
class PytestMarkAdder(cst.CSTTransformer):
4190
"""Transformer that adds pytest marks to test functions."""
4291

@@ -106,41 +155,51 @@ def _create_pytest_mark(self) -> cst.Decorator:
106155
class AutouseFixtureModifier(cst.CSTTransformer):
107156
def leave_FunctionDef(self, original_node: cst.FunctionDef, updated_node: cst.FunctionDef) -> cst.FunctionDef:
108157
# Matcher for '@fixture' or '@pytest.fixture'
109-
fixture_decorator_func = m.Name("fixture") | m.Attribute(value=m.Name("pytest"), attr=m.Name("fixture"))
110-
111158
for decorator in original_node.decorators:
112-
if m.matches(
113-
decorator,
114-
m.Decorator(
115-
decorator=m.Call(
116-
func=fixture_decorator_func, args=[m.Arg(value=m.Name("True"), keyword=m.Name("autouse"))]
117-
)
118-
),
119-
):
120-
# Found a matching fixture with autouse=True
121-
122-
# 1. The original body of the function will become the 'else' block.
123-
# updated_node.body is an IndentedBlock, which is what cst.Else expects.
124-
else_block = cst.Else(body=updated_node.body)
125-
126-
# 2. Create the new 'if' block that will exit the fixture early.
127-
if_test = cst.parse_expression('request.node.get_closest_marker("codeflash_no_autouse")')
128-
yield_statement = cst.parse_statement("yield")
129-
if_body = cst.IndentedBlock(body=[yield_statement])
130-
131-
# 3. Construct the full if/else statement.
132-
new_if_statement = cst.If(test=if_test, body=if_body, orelse=else_block)
133-
134-
# 4. Replace the entire function's body with our new single statement.
135-
return updated_node.with_changes(body=cst.IndentedBlock(body=[new_if_statement]))
159+
dec = decorator.decorator
160+
161+
if isinstance(dec, cst.Call):
162+
func_name = ""
163+
if isinstance(dec.func, cst.Attribute) and isinstance(dec.func.value, cst.Name):
164+
if dec.func.attr.value == "fixture" and dec.func.value.value == "pytest":
165+
func_name = "pytest.fixture"
166+
elif isinstance(dec.func, cst.Name) and dec.func.value == "fixture":
167+
func_name = "fixture"
168+
169+
if func_name:
170+
for arg in dec.args:
171+
if (
172+
arg.keyword
173+
and arg.keyword.value == "autouse"
174+
and isinstance(arg.value, cst.Name)
175+
and arg.value.value == "True"
176+
):
177+
# Found a matching fixture with autouse=True
178+
179+
# 1. The original body of the function will become the 'else' block.
180+
# updated_node.body is an IndentedBlock, which is what cst.Else expects.
181+
else_block = cst.Else(body=updated_node.body)
182+
183+
# 2. Create the new 'if' block that will exit the fixture early.
184+
if_test = cst.parse_expression('request.node.get_closest_marker("codeflash_no_autouse")')
185+
yield_statement = cst.parse_statement("yield")
186+
if_body = cst.IndentedBlock(body=[yield_statement])
187+
188+
# 3. Construct the full if/else statement.
189+
new_if_statement = cst.If(test=if_test, body=if_body, orelse=else_block)
190+
191+
# 4. Replace the entire function's body with our new single statement.
192+
return updated_node.with_changes(body=cst.IndentedBlock(body=[new_if_statement]))
136193
return updated_node
137194

138195

139196
def disable_autouse(test_path: Path) -> str:
140197
file_content = test_path.read_text(encoding="utf-8")
141198
module = cst.parse_module(file_content)
199+
add_request_argument = AddRequestArgument()
142200
disable_autouse_fixture = AutouseFixtureModifier()
143-
modified_module = module.visit(disable_autouse_fixture)
201+
modified_module = module.visit(add_request_argument)
202+
modified_module = modified_module.visit(disable_autouse_fixture)
144203
test_path.write_text(modified_module.code, encoding="utf-8")
145204
return file_content
146205

codeflash/tracer.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from __future__ import annotations
1313

1414
import contextlib
15+
import datetime
1516
import importlib.machinery
1617
import io
1718
import json
@@ -81,6 +82,7 @@ def __init__(
8182
config_file_path: Path | None = None,
8283
max_function_count: int = 256,
8384
timeout: int | None = None, # seconds
85+
command: str | None = None,
8486
) -> None:
8587
"""Use this class to trace function calls.
8688
@@ -91,6 +93,7 @@ def __init__(
9193
:param max_function_count: Maximum number of times to trace one function
9294
:param timeout: Timeout in seconds for the tracer, if the traced code takes more than this time, then tracing
9395
stops and normal execution continues. If this is None then no timeout applies
96+
:param command: The command that initiated the tracing (for metadata storage)
9497
"""
9598
if functions is None:
9699
functions = []
@@ -148,6 +151,9 @@ def __init__(
148151
assert "test_framework" in self.config, "Please specify 'test-framework' in pyproject.toml config file"
149152
self.t = self.timer()
150153

154+
# Store command information for metadata table
155+
self.command = command if command else " ".join(sys.argv)
156+
151157
def __enter__(self) -> None:
152158
if self.disable:
153159
return
@@ -174,6 +180,22 @@ def __enter__(self) -> None:
174180
"CREATE TABLE function_calls(type TEXT, function TEXT, classname TEXT, filename TEXT, "
175181
"line_number INTEGER, last_frame_address INTEGER, time_ns INTEGER, args BLOB)"
176182
)
183+
184+
# Create metadata table to store command information
185+
cur.execute("CREATE TABLE metadata(key TEXT PRIMARY KEY, value TEXT)")
186+
187+
# Store command metadata
188+
cur.execute("INSERT INTO metadata VALUES (?, ?)", ("command", self.command))
189+
cur.execute("INSERT INTO metadata VALUES (?, ?)", ("program_name", self.file_being_called_from))
190+
cur.execute(
191+
"INSERT INTO metadata VALUES (?, ?)",
192+
("functions_filter", json.dumps(self.functions) if self.functions else None),
193+
)
194+
cur.execute(
195+
"INSERT INTO metadata VALUES (?, ?)",
196+
("timestamp", datetime.datetime.now(datetime.timezone.utc).isoformat()),
197+
)
198+
cur.execute("INSERT INTO metadata VALUES (?, ?)", ("project_root", str(self.project_root)))
177199
console.rule("Codeflash: Traced Program Output Begin", style="bold blue")
178200
frame = sys._getframe(0) # Get this frame and simulate a call to it # noqa: SLF001
179201
self.dispatch["call"](self, frame, 0)
@@ -842,6 +864,7 @@ def main() -> ArgumentParser:
842864
max_function_count=args.max_function_count,
843865
timeout=args.tracer_timeout,
844866
config_file_path=args.codeflash_config,
867+
command=" ".join(sys.argv),
845868
).runctx(code, globs, None)
846869

847870
except BrokenPipeError as exc:

codeflash/version.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
# These version placeholders will be replaced by poetry-dynamic-versioning during `poetry build`.
2-
__version__ = "0.14.0"
3-
__version_tuple__ = (0, 14, 0)
2+
__version__ = "0.14.3"
3+
__version_tuple__ = (0, 14, 3)

docs/docs/configuration.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ tests-root = "tests"
1616
test-framework = "pytest"
1717
formatter-cmds = ["black $file"]
1818
# optional configuration
19+
benchmarks-root = "tests/benchmarks" # Required when running with --benchmark
1920
ignore-paths = ["my_module/build/"]
2021
pytest-cmd = "pytest"
2122
disable-imports-sorting = false
@@ -29,6 +30,7 @@ Required Options:
2930
- `test-framework`: The test framework you use for your project. Codeflash supports `pytest` and `unittest`.
3031

3132
Optional Configuration:
33+
- `benchmarks-root`: The directory where your benchmarks are located. Codeflash will use this directory to discover existing benchmarks. Note that this option is required when running with `--benchmark`.
3234
- `ignore-paths`: A list of paths withing the `module-root` to ignore when optimizing code. Codeflash will not optimize code in these paths. Useful for ignoring build directories or other generated code. You can also leave this empty if not needed.
3335
- `pytest-cmd`: The command to run your tests. Defaults to `pytest`. You can specify extra commandline arguments here for pytest.
3436
- `formatter-cmds`: The command line to run your code formatter or linter. Defaults to `["black $file"]`. In the command line `$file` refers to the current file being optimized. The assumption with using tools here is that they overwrite the same file and returns a zero exit code. You can also specify multiple tools here that run in a chain as a toml array. You can also disable code formatting by setting this to `["disabled"]`.

docs/docs/optimizing-with-codeflash/benchmarking.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ sidebar_position: 5
33
---
44
# Using Benchmarks
55

6-
Codeflash is able the determine the impact of an optimization on predefined benchmarks, when used in benchmark mode.
6+
Codeflash is able to determine the impact of an optimization on predefined benchmarks, when used in benchmark mode.
77

88
Benchmark mode is an easy way for users to define workflows that are performance-critical and need to be optimized.
99
For example, if a user has an important function that requires minimal latency, the user can define a benchmark for that function.
@@ -13,7 +13,7 @@ Codeflash will then calculate the impact (if any) of any optimization on the per
1313

1414
1. **Create a benchmarks root**
1515

16-
Create a directory for benchmarks. This directory must be a sub directory of your tests directory.
16+
Create a directory for benchmarks if it does not already exist.
1717

1818
In your pyproject.toml, add the path to the 'benchmarks-root' section.
1919
```yaml

0 commit comments

Comments
 (0)