Skip to content

Standardize coding style in DSPy #7885

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 15 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 2 additions & 11 deletions .github/workflows/run_tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,19 +31,10 @@ jobs:
uv venv .venv
echo "${{ github.workspace }}/.venv/bin" >> $GITHUB_PATH
- name: Install dependencies
run: uv sync --dev -p .venv
run: uv sync --dev -p .venv --extra dev
- name: Ruff Fix Attempt
id: ruff_fix
uses: chartboost/ruff-action@v1
with:
args: check --fix-only --diff --exit-non-zero-on-fix
continue-on-error: true
- name: Fail Workflow if Ruff Fix Failed
if: steps.ruff_fix.outcome == 'failure'
run: |
echo "Ruff fix failed, failing the workflow."
echo "Please run 'ruff check . --fix-only' locally and push the changes."
exit 1
run: ruff check --fix-only --diff --exit-non-zero-on-fix

test:
name: Run Tests
Expand Down
60 changes: 11 additions & 49 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,65 +2,27 @@ default_language_version:
python: python3.9

default_stages: [pre-commit]
default_install_hook_types: [pre-commit, commit-msg]
default_install_hook_types: [pre-commit]

repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.1.11
hooks:
- id: ruff
args: [--fix]
- id: ruff-format

- repo: https://github.com/timothycrosley/isort
rev: 5.12.0
- repo: local
hooks:
- id: isort
args:
[
"--profile=black",
"--py=39",
"--line-length=120",
"--multi-line=3",
"--trailing-comma",
"--force-grid-wrap=0",
"--use-parentheses",
"--ensure-newline-before-comments",
"--project=CORE,src,config,preprocess,train,transform,main,model",
]
- id: ruff-check
name: ruff (lint)
entry: ruff
language: system
types_or: [python, pyi]
files: ^(dspy|tests)/.*\.py$
exclude: ^(dspy/__metadata__\.py|dspy/retrieve/.*\.py|tests/reliability/.*\.py|tests/retrieve/.*\.py)$
args: [check, --fix-only]

- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.1.0
rev: v5.0.0
hooks:
- id: check-yaml
args: ["--allow-multiple-documents", "--unsafe"]
- id: end-of-file-fixer
- id: trailing-whitespace
- id: check-docstring-first
- id: check-toml
- id: check-added-large-files
args: ["--maxkb=1024"]
- id: requirements-txt-fixer
- id: check-merge-conflict
- id: debug-statements
- id: pretty-format-json
args:
- "--autofix"
- "--indent=2"

- repo: local
hooks:
- id: validate-commit-msg
name: Commit Message is Valid
language: pygrep
entry: ^(break|build|ci|docs|feat|fix|perf|refactor|style|test|ops|hotfix|release|maint|init|enh|revert)\([\w,\.,\-,\(,\),\/]+\)(!?)(:)\s{1}([\w,\W,:]+)
stages: [commit-msg]
args: [--negate]

- repo: https://github.com/pre-commit/mirrors-prettier
rev: v3.0.3
hooks:
- id: prettier
additional_dependencies:
- prettier@2.1.2
- "@prettier/plugin-xml@0.12.0"
44 changes: 41 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,17 +35,55 @@ For minor changes (simple bug fixes or documentation fixes), feel free to open a
To make code changes, fork the repository and set up your local development environment following the
instructions in the "Environment Setup" section below.

### Step 3. Create a Pull Request
### Step 3 Commit Your Code and Run Autoformatting

We follow the [Google Python Style Guide](https://google.github.io/styleguide/pyguide.html) and use `ruff` for both linting and formatting. To ensure consistent code quality, we use pre-commit hooks that automatically check and fix common issues.


First you need to set up the pre-commit hooks (do this once after cloning the repository):

```shell
pre-commit install
```

Then stage and commit your changes. When you run `git commit`, the pre-commit hook will be
automatically run.

```shell
git add .
git commit -m "your commit message"
```

If the hooks make any changes, you'll need to stage and commit those changes as well.

You can also run the hooks manually:

- Check staged files only:

```shell
pre-commit run
```

- Check specific files:

```shell
pre-commit run --files path/to/file1.py path/to/file2.py
```

Please ensure all pre-commit checks pass before creating your pull request. If you're unsure about any
formatting issues, feel free to commit your changes and let the pre-commit hooks fix them automatically.

### Step 4. Create a Pull Request

Once your changes are ready, open a pull request from your branch in your fork to the main branch in the
[DSPy repo](https://github.com/stanfordnlp/dspy).

### Step 4. Code Review
### Step 5. Code Review

Once your PR is up and passes all CI tests, we will assign reviewers to review the code. There may be
several rounds of comments and code changes before the pull request gets approved by the reviewer.

### Step 5. Merging
### Step 6. Merging

Once the pull request is approved, a team member will take care of merging.

Expand Down
2 changes: 1 addition & 1 deletion dspy/adapters/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from dspy.adapters.chat_adapter import ChatAdapter
from dspy.adapters.json_adapter import JSONAdapter
from dspy.adapters.two_step_adapter import TwoStepAdapter
from dspy.adapters.types import History, Image, Audio, BaseType
from dspy.adapters.types import Audio, BaseType, History, Image

__all__ = [
"Adapter",
Expand Down
2 changes: 1 addition & 1 deletion dspy/adapters/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -352,4 +352,4 @@ def parse(self, signature: Type[Signature], completion: str) -> dict[str, Any]:
Returns:
A dictionary of the output fields.
"""
raise NotImplementedError
raise NotImplementedError
4 changes: 2 additions & 2 deletions dspy/adapters/types/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from dspy.adapters.types.history import History
from dspy.adapters.types.image import Image
from dspy.adapters.types.audio import Audio
from dspy.adapters.types.base_type import BaseType
from dspy.adapters.types.history import History
from dspy.adapters.types.image import Image

__all__ = ["History", "Image", "Audio", "BaseType"]
4 changes: 2 additions & 2 deletions dspy/adapters/types/audio.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ def validate_input(cls, values: Any) -> Any:
"""
if isinstance(values, cls):
return {"data": values.data, "format": values.format}
return encode_audio(values)
return encode_audio(values)

@classmethod
def from_url(cls, url: str) -> "Audio":
Expand Down Expand Up @@ -142,4 +142,4 @@ def encode_audio(audio: Union[str, bytes, dict, "Audio", Any], sampling_rate: in
encoded = base64.b64encode(audio).decode("utf-8")
return {"data": encoded, "format": format}
else:
raise ValueError(f"Unsupported type for encode_audio: {type(audio)}")
raise ValueError(f"Unsupported type for encode_audio: {type(audio)}")
2 changes: 1 addition & 1 deletion dspy/dsp/utils/dpr.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ def __init__(self, **kwargs):
annotators: None or empty set (only tokenizes).
"""
self._regexp = regex.compile(
"(%s)|(%s)" % (self.ALPHA_NUM, self.NON_WS), # noqa: UP031
"(%s)|(%s)" % (self.ALPHA_NUM, self.NON_WS),
flags=regex.IGNORECASE + regex.UNICODE + regex.MULTILINE,
)
if len(kwargs.get("annotators", {})) > 0:
Expand Down
54 changes: 28 additions & 26 deletions dspy/signatures/signature.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@ class MySignature(dspy.Signature):
import importlib
import inspect
import re
import sys
import types
import typing
import sys
from copy import deepcopy
from typing import Any, Dict, Optional, Tuple, Type, Union

Expand All @@ -41,7 +41,7 @@ class SignatureMeta(type(BaseModel)):
def __call__(cls, *args, **kwargs):
if cls is Signature:
# We don't create an actual Signature instance, instead, we create a new Signature class.
custom_types = kwargs.pop('custom_types', None)
custom_types = kwargs.pop("custom_types", None)

if custom_types is None and args and isinstance(args[0], str):
custom_types = cls._detect_custom_types_from_caller(args[0])
Expand All @@ -52,76 +52,78 @@ def __call__(cls, *args, **kwargs):
@staticmethod
def _detect_custom_types_from_caller(signature_str):
"""Detect custom types from the caller's frame based on the signature string.

Note: This method relies on Python's frame introspection which has some limitations:
1. May not work in all Python implementations (e.g., compiled with optimizations)
2. Looks up a limited number of frames in the call stack
3. Cannot find types that are imported but not in the caller's namespace
For more reliable custom type resolution, explicitly provide types using the

For more reliable custom type resolution, explicitly provide types using the
`custom_types` parameter when creating a Signature.
"""

# Extract potential type names from the signature string, including dotted names
# Match both simple types like 'MyType' and dotted names like 'Module.Type'
type_pattern = r':\s*([A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)*)'
type_pattern = r":\s*([A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)*)"
type_names = re.findall(type_pattern, signature_str)
if not type_names:
return None

# Get type references from caller frames by walking the stack
found_types = {}

needed_types = set()
dotted_types = {}

for type_name in type_names:
parts = type_name.split('.')
parts = type_name.split(".")
base_name = parts[0]

if base_name not in typing.__dict__ and base_name not in __builtins__:
if len(parts) > 1:
dotted_types[type_name] = base_name
needed_types.add(base_name)
else:
needed_types.add(type_name)

if not needed_types:
return None

frame = None
try:
frame = sys._getframe(1) # Start one level up (skip this function)

max_frames = 100
frame_count = 0

while frame and needed_types and frame_count < max_frames:
frame_count += 1

for type_name in list(needed_types):
if type_name in frame.f_locals:
found_types[type_name] = frame.f_locals[type_name]
needed_types.remove(type_name)
elif frame.f_globals and type_name in frame.f_globals:
found_types[type_name] = frame.f_globals[type_name]
needed_types.remove(type_name)

# If we found all needed types, stop looking
if not needed_types:
break

frame = frame.f_back

if needed_types and frame_count >= max_frames:
import logging

logging.getLogger("dspy").warning(
f"Reached maximum frame search depth ({max_frames}) while looking for types: {needed_types}. "
"Consider providing custom_types explicitly to Signature."
)
except (AttributeError, ValueError):
# Handle environments where frame introspection is not available
import logging

logging.getLogger("dspy").debug(
"Frame introspection failed while trying to resolve custom types. "
"Consider providing custom_types explicitly to Signature."
Expand All @@ -132,7 +134,7 @@ def _detect_custom_types_from_caller(signature_str):

return found_types or None

def __new__(mcs, signature_name, bases, namespace, **kwargs): # noqa: N804
def __new__(mcs, signature_name, bases, namespace, **kwargs):
# At this point, the orders have been swapped already.
field_order = [name for name, value in namespace.items() if isinstance(value, FieldInfo)]
# Set `str` as the default type for all fields
Expand Down Expand Up @@ -284,7 +286,7 @@ def delete(cls, name) -> Type["Signature"]:
fields = dict(cls.fields)

if name in fields:
del fields[name]
del fields[name] # noqa: RUF051
else:
raise ValueError(f"Field `{name}` not found in `{cls.__name__}`.")

Expand Down Expand Up @@ -398,11 +400,11 @@ def make_signature(
"question": (str, InputField()),
"answer": (str, OutputField())
})

# Using custom types
class MyType:
pass

sig3 = make_signature("input: MyType -> output", custom_types={"MyType": MyType})
```
"""
Expand All @@ -411,7 +413,7 @@ class MyType:
if custom_types:
names = dict(typing.__dict__)
names.update(custom_types)

fields = _parse_signature(signature, names) if isinstance(signature, str) else signature

# Validate the fields, this is important because we sometimes forget the
Expand Down Expand Up @@ -549,15 +551,15 @@ def resolve_name(type_name: str):
if isinstance(node, ast.Attribute):
base = _parse_type_node(node.value, names)
attr_name = node.attr

if hasattr(base, attr_name):
return getattr(base, attr_name)

if isinstance(node.value, ast.Name):
full_name = f"{node.value.id}.{attr_name}"
if full_name in names:
return names[full_name]

raise ValueError(f"Unknown attribute: {attr_name} on {base}")

if isinstance(node, ast.Subscript):
Expand Down
2 changes: 1 addition & 1 deletion dspy/teleprompt/grpo.py
Original file line number Diff line number Diff line change
Expand Up @@ -298,7 +298,7 @@ def compile(
num_student_predictors = len(student.predictors())

logging.info("Preparing the teacher program(s)... We will ensure that the provided programs have the same program structure as the student program.")
if isinstance(teacher, list) and len(teacher) == 0 or teacher is None:
if (isinstance(teacher, list) and len(teacher) == 0) or teacher is None:
teacher = student
teachers = teacher if isinstance(teacher, list) else [teacher]
for t in teachers:
Expand Down
Loading