litellm/tests/code_coverage_tests/check_spanattributes_value_usage.py
yuneng-jiang 6a0d03914c
test: drop the cwd-relative sys.path.insert calls from the test suite (#37802)
* test: drop the cwd-relative sys.path.insert calls from the test suite

TQ003 stands at 1,077 across 1,058 files, and 1,015 of them are the same shape:
sys.path.insert(0, os.path.abspath("../..")) and its deeper siblings. The
argument resolves against the working directory rather than the file, so from
the repo root, where every job runs pytest, it inserts the directory two levels
above the checkout. It has never pointed at litellm. The package is installed
into the environment anyway, which is what actually makes the import work, and
what the rule's message has said all along.

Removing them leaves 1,634 imports of sys and os with no remaining reference,
and those go too, except where another test module imports the name back out of
the file. The rest of TQ003 is 62 call sites that resolve against __file__ or a
variable, which are a different question and are left alone.

Collection is identical either way: 45,871 tests and the same 51 pre-existing
collection errors before and after, and ruff reports no new undefined name.

* test: drop the duplicate imports the sys.path sweep exposed to F811

* test(pre-call-utils): restore the os import the new bedrock tests need
2026-08-22 09:25:58 -07:00

193 lines
7.1 KiB
Python

"""
SpanAttributes Value Usage Checker
This script ensures that all SpanAttributes enum references in the OpenTelemetry integration
are properly accessed with the .value property. This is important because:
1. Without .value, the enum object itself is used instead of its string value
2. This can cause type errors or unexpected behavior in OpenTelemetry exporters
3. It's a consistent pattern that should be followed for all enum usage
Example of correct usage:
span.set_attribute(key=SpanAttributes.LLM_USER.value, value="user123")
Example of incorrect usage:
span.set_attribute(key=SpanAttributes.LLM_USER, value="user123")
The script checks both through AST parsing (for accurate code analysis) and regex
(for backup coverage) to find any violations.
Usage:
python tests/code_coverage_tests/check_spanattributes_value_usage.py
python tests/code_coverage_tests/check_spanattributes_value_usage.py --debug
"""
import argparse
import ast
import os
import re
from typing import List, Tuple
# Add parent directory to path so we can import litellm
import litellm
class SpanAttributesUsageChecker(ast.NodeVisitor):
"""
Checks if SpanAttributes is used without .value when setting attributes in safe_set_attribute calls
and other attribute setting methods in opentelemetry.py.
This is important to ensure consistent enum value access and prevent type errors
when sending data to OpenTelemetry exporters.
"""
def __init__(self, debug=False):
self.violations = []
self.debug = debug
def visit_Call(self, node):
# Check if this is a call to safe_set_attribute or set_attribute
if isinstance(node.func, ast.Attribute) and node.func.attr in [
"safe_set_attribute",
"set_attribute",
]:
# Look for the 'key' parameter
for keyword in node.keywords:
if keyword.arg == "key":
# Check if the value is a SpanAttributes member without .value
if (
isinstance(keyword.value, ast.Attribute)
and isinstance(keyword.value.value, ast.Name)
and keyword.value.value.id == "SpanAttributes"
):
# Get the source code for this attribute
try:
attr_source = ast.unparse(keyword.value)
if not attr_source.endswith(".value"):
if self.debug:
print(
f"AST found violation: {node.lineno}: {attr_source}"
)
self.violations.append(
(node.lineno, f"{attr_source} used without .value")
)
except AttributeError:
# For Python < 3.9, ast.unparse doesn't exist
# Fallback to our best guess
if keyword.value.attr != "value" and not hasattr(
keyword.value, "value"
):
violation_msg = f"SpanAttributes.{keyword.value.attr} used without .value"
if self.debug:
print(
f"AST found violation: {node.lineno}: {violation_msg}"
)
self.violations.append((node.lineno, violation_msg))
# Continue the visit
self.generic_visit(node)
def check_file(file_path: str, debug: bool = False) -> List[Tuple[int, str]]:
"""
Analyze a Python file to check for SpanAttributes usage without .value
Args:
file_path: Path to the Python file to check
debug: Whether to print debug information
Returns:
List of (line_number, message) tuples identifying violations
"""
with open(file_path, "r") as file:
content = file.read()
# First try AST parsing for accurate code structure analysis
try:
tree = ast.parse(content)
checker = SpanAttributesUsageChecker(debug=debug)
checker.visit(tree)
violations = checker.violations
# Also do a regex check for backup/extra coverage
# This catches cases that might be missed by AST parsing
# Split content into lines for more precise analysis
lines = content.splitlines()
for i, line in enumerate(lines, 1):
# Skip lines that contain ".value" after "SpanAttributes."
# This prevents false positives for correct usage
if re.search(r"SpanAttributes\.[A-Z_][A-Z0-9_]*\.value", line):
if debug:
print(f"Line {i} skipped - contains .value: {line.strip()}")
continue
# Pattern: Looking for "key=SpanAttributes.ENUM_NAME" without .value at the end
pattern = r"key\s*=\s*SpanAttributes\.[A-Z_][A-Z0-9_]*(?!\.value)"
match = re.search(pattern, line)
if match:
# Check if this violation was already found by AST
if not any(i == line_num for line_num, _ in violations):
if debug:
print(f"Regex found violation: {i}: {match.group(0)}")
violations.append(
(i, f"SpanAttributes used without .value: {match.group(0)}")
)
return violations
except SyntaxError:
print(f"Syntax error in {file_path}")
return []
def main():
"""
Main function to run the SpanAttributes usage check on the OpenTelemetry integration file.
Exits with code 1 if violations are found, 0 otherwise.
"""
parser = argparse.ArgumentParser(
description="Check for SpanAttributes used without .value"
)
parser.add_argument("--debug", action="store_true", help="Enable debug output")
args = parser.parse_args()
# Path to the OpenTelemetry integration file
target_file = os.path.join("litellm", "integrations", "opentelemetry.py")
if not os.path.exists(target_file):
# Try alternate path for local development
target_file = os.path.join(
"..", "..", "litellm", "integrations", "opentelemetry.py"
)
if not os.path.exists(target_file):
print(f"Error: Could not find file at {target_file}")
exit(1)
violations = check_file(target_file, debug=args.debug)
if violations:
print(
f"Found {len(violations)} SpanAttributes without .value in {target_file}:"
)
# Sort violations by line number for better readability
violations.sort(key=lambda x: x[0])
for line, message in violations:
print(f" Line {line}: {message}")
print(
"\nDirect enum reference can cause errors. Always use .value with SpanAttributes enums."
)
exit(1)
else:
print(f"All SpanAttributes are used correctly with .value in {target_file}")
exit(0)
if __name__ == "__main__":
main()