From 66e1ab01910477a66540d5baf1273d0a07610e24 Mon Sep 17 00:00:00 2001 From: Vishal Shenoy Date: Mon, 27 Jan 2025 14:50:06 -0800 Subject: [PATCH 1/8] . --- run_format_example.py | 89 +++++++++ sqlalchemy_1.6_to_2.0/input_repo/database.py | 4 +- sqlalchemy_1.6_to_2.0/input_repo/models.py | 1 + sqlalchemy_1.6_to_2.0/output_repo/database.py | 11 -- sqlalchemy_1.6_to_2.0/output_repo/main.py | 101 ---------- sqlalchemy_1.6_to_2.0/output_repo/models.py | 32 ---- sqlalchemy_1.6_to_2.0/output_repo/schemas.py | 31 ---- sqlalchemy_1.6_to_2.0/run.py | 173 ++++++++++++++++++ 8 files changed, 266 insertions(+), 176 deletions(-) create mode 100644 run_format_example.py delete mode 100644 sqlalchemy_1.6_to_2.0/output_repo/database.py delete mode 100644 sqlalchemy_1.6_to_2.0/output_repo/main.py delete mode 100644 sqlalchemy_1.6_to_2.0/output_repo/models.py delete mode 100644 sqlalchemy_1.6_to_2.0/output_repo/schemas.py create mode 100644 sqlalchemy_1.6_to_2.0/run.py diff --git a/run_format_example.py b/run_format_example.py new file mode 100644 index 0000000..939bf53 --- /dev/null +++ b/run_format_example.py @@ -0,0 +1,89 @@ +import codegen +from codegen import Codebase +from codegen.sdk.core.detached_symbols.function_call import FunctionCall + + +@codegen.function("useSuspenseQuery-to-useSuspenseQueries") +def run(codebase: Codebase): + """Convert useSuspenseQuery calls to useSuspenseQueries in a React codebase. + + This codemod: + 1. Finds all files containing useSuspenseQuery + 2. Adds the necessary import statement + 3. Converts multiple useSuspenseQuery calls to a single useSuspenseQueries call + """ + # Import statement for useSuspenseQueries + import_str = "import { useQuery, useSuspenseQueries } from '@tanstack/react-query'" + + # Track statistics + files_modified = 0 + functions_modified = 0 + + # Iterate through all files in the codebase + for file in codebase.files: + if "useSuspenseQuery" not in file.source: + continue + + print(f"Processing {file.filepath}") + # Add the import statement + file.add_import_from_import_string(import_str) + file_modified = False + + # Iterate through all functions in the file + for function in file.functions: + if "useSuspenseQuery" not in function.source: + continue + + results = [] # Store left-hand side of assignments + queries = [] # Store query arguments + old_statements = [] # Track statements to replace + + # Find useSuspenseQuery assignments + for stmt in function.code_block.assignment_statements: + if not isinstance(stmt.right, FunctionCall): + continue + + fcall = stmt.right + if fcall.name != "useSuspenseQuery": + continue + + old_statements.append(stmt) + results.append(stmt.left.source) + queries.append(fcall.args[0].value.source) + + # Convert to useSuspenseQueries if needed + if old_statements: + new_query = f"const [{', '.join(results)}] = useSuspenseQueries({{queries: [{', '.join(queries)}]}})" + print( + f"Converting useSuspenseQuery to useSuspenseQueries in {function.name}" + ) + + # Print the diff + print("\nOriginal code:") + for stmt in old_statements: + print(f"- {stmt.source}") + print("\nNew code:") + print(f"+ {new_query}") + print("-" * 50) + + # Replace old statements with new query + for stmt in old_statements: + stmt.edit(new_query) + + functions_modified += 1 + file_modified = True + + if file_modified: + files_modified += 1 + + print("\nModification complete:") + print(f"Files modified: {files_modified}") + print(f"Functions modified: {functions_modified}") + + +if __name__ == "__main__": + print("Initializing codebase...") + codebase = Codebase.from_repo("deepfence/ThreatMapper") + + print("Running codemod...") + run(codebase) diff --git a/sqlalchemy_1.6_to_2.0/input_repo/database.py b/sqlalchemy_1.6_to_2.0/input_repo/database.py index c07234d..0bfd0aa 100644 --- a/sqlalchemy_1.6_to_2.0/input_repo/database.py +++ b/sqlalchemy_1.6_to_2.0/input_repo/database.py @@ -3,7 +3,9 @@ from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import sessionmaker -SQLALCHEMY_DATABASE_URL = "postgresql://user:password@localhost/dbname" # Change to your database URL +SQLALCHEMY_DATABASE_URL = ( + "postgresql://user:password@localhost/dbname" # Change to your database URL +) engine = create_engine(SQLALCHEMY_DATABASE_URL) SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) diff --git a/sqlalchemy_1.6_to_2.0/input_repo/models.py b/sqlalchemy_1.6_to_2.0/input_repo/models.py index cd1a9a8..f733b83 100644 --- a/sqlalchemy_1.6_to_2.0/input_repo/models.py +++ b/sqlalchemy_1.6_to_2.0/input_repo/models.py @@ -2,6 +2,7 @@ from sqlalchemy.orm import relationship, backref from database import Base + class Publisher(Base): __tablename__ = "publishers" diff --git a/sqlalchemy_1.6_to_2.0/output_repo/database.py b/sqlalchemy_1.6_to_2.0/output_repo/database.py deleted file mode 100644 index c606b3b..0000000 --- a/sqlalchemy_1.6_to_2.0/output_repo/database.py +++ /dev/null @@ -1,11 +0,0 @@ -# database.py -from sqlalchemy import create_engine -from sqlalchemy.orm import DeclarativeBase -from sqlalchemy.orm import sessionmaker - -SQLALCHEMY_DATABASE_URL = "postgresql://user:password@localhost/dbname" # Change to your database URL - -engine = create_engine(SQLALCHEMY_DATABASE_URL) -SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) - -Base = declarative_base() diff --git a/sqlalchemy_1.6_to_2.0/output_repo/main.py b/sqlalchemy_1.6_to_2.0/output_repo/main.py deleted file mode 100644 index 1c62632..0000000 --- a/sqlalchemy_1.6_to_2.0/output_repo/main.py +++ /dev/null @@ -1,101 +0,0 @@ -# main.py -from fastapi import FastAPI, Depends, HTTPException -from sqlalchemy.orm import Session -import models, schemas -from database import SessionLocal, engine -from typing import List - -# Initialize the app and create database tables -app = FastAPI() -models.Base.metadata.create_all(bind=engine) - -# Dependency for the database session -def get_db(): - db = SessionLocal() - try: - yield db - finally: - db.close() - -# CRUD Operations - -@app.post("/books/", response_model=schemas.Book) -def create_book(book: schemas.BookCreate, db: Session = Depends(get_db)): - db_book = models.Book(**book.dict()) - db.add(db_book) - db.commit() - db.refresh(db_book) - return db_book - -@app.get("/books/", response_model=List[schemas.Book]) -def read_books(skip: int = 0, limit: int = 10, db: Session = Depends(get_db)): - books = db.query()(models.Book).offset(skip).limit(limit).scalars().all() - return books - -@app.get("/books/{book_id}", response_model=schemas.Book) -def read_book(book_id: int, db: Session = Depends(get_db)): - book = db.query()(models.Book).where(models.Book.id == book_id).first() - if book is None: - raise HTTPException(status_code=404, detail="Book not found") - return book - -@app.put("/books/{book_id}", response_model=schemas.Book) -def update_book(book_id: int, book: schemas.BookCreate, db: Session = Depends(get_db)): - db_book = db.query()(models.Book).where(models.Book.id == book_id).first() - if db_book is None: - raise HTTPException(status_code=404, detail="Book not found") - for key, value in book.dict().items(): - setattr(db_book, key, value) - db.commit() - db.refresh(db_book) - return db_book - -@app.delete("/books/{book_id}", response_model=schemas.Book) -def delete_book(book_id: int, db: Session = Depends(get_db)): - db_book = db.query()(models.Book).where(models.Book.id == book_id).first() - if db_book is None: - raise HTTPException(status_code=404, detail="Book not found") - db.delete(db_book) - db.commit() - return db_book - -@app.post("/publishers/", response_model=schemas.Publisher) -def create_publisher(publisher: schemas.PublisherCreate, db: Session = Depends(get_db)): - db_publisher = models.Publisher(**publisher.dict()) - db.add(db_publisher) - db.commit() - db.refresh(db_publisher) - return db_publisher - -@app.get("/publishers/", response_model=List[schemas.Publisher]) -def read_publishers(skip: int = 0, limit: int = 10, db: Session = Depends(get_db)): - publishers = db.query()(models.Publisher).offset(skip).limit(limit).scalars().all() - return publishers - -@app.get("/publishers/{publisher_id}", response_model=schemas.Publisher) -def read_publisher(publisher_id: int, db: Session = Depends(get_db)): - publisher = db.query()(models.Publisher).where(models.Publisher.id == publisher_id).first() - if not publisher: - raise HTTPException(status_code=404, detail="Publisher not found") - return publisher - -@app.put("/publishers/{publisher_id}", response_model=schemas.Publisher) -def update_publisher(publisher_id: int, publisher: schemas.PublisherCreate, db: Session = Depends(get_db)): - db_publisher = db.query()(models.Publisher).where(models.Publisher.id == publisher_id).first() - if not db_publisher: - raise HTTPException(status_code=404, detail="Publisher not found") - for key, value in publisher.dict().items(): - setattr(db_publisher, key, value) - db.commit() - db.refresh(db_publisher) - return db_publisher - -@app.delete("/publishers/{publisher_id}", response_model=schemas.Publisher) -def delete_publisher(publisher_id: int, db: Session = Depends(get_db)): - db_publisher = db.query()(models.Publisher).where(models.Publisher.id == publisher_id).first() - if not db_publisher: - raise HTTPException(status_code=404, detail="Publisher not found") - db.delete(db_publisher) - db.commit() - return db_publisher - diff --git a/sqlalchemy_1.6_to_2.0/output_repo/models.py b/sqlalchemy_1.6_to_2.0/output_repo/models.py deleted file mode 100644 index 25ad484..0000000 --- a/sqlalchemy_1.6_to_2.0/output_repo/models.py +++ /dev/null @@ -1,32 +0,0 @@ -from typing import List, Optional -from sqlalchemy import Column, Integer, String, ForeignKey -from sqlalchemy.orm import relationship, Mapped, mapped_column -from database import Base - -class Publisher(Base): - __tablename__ = "publishers" - - id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True) - name: Mapped[str] = mapped_column(String, unique=True, index=True) - books: Mapped[List["Book"]] = relationship( - "Book", - back_populates="publisher", - lazy='selectin' - ) - -class Book(Base): - __tablename__ = "books" - - id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True) - title: Mapped[str] = mapped_column(String, index=True) - author: Mapped[str] = mapped_column(String, index=True) - description: Mapped[Optional[str]] = mapped_column(String, nullable=True) - publisher_id: Mapped[Optional[int]] = mapped_column( - Integer, - ForeignKey("publishers.id"), - nullable=True - ) - publisher: Mapped[Optional["Publisher"]] = relationship( - "Publisher", - back_populates="books" - ) \ No newline at end of file diff --git a/sqlalchemy_1.6_to_2.0/output_repo/schemas.py b/sqlalchemy_1.6_to_2.0/output_repo/schemas.py deleted file mode 100644 index daf4fb9..0000000 --- a/sqlalchemy_1.6_to_2.0/output_repo/schemas.py +++ /dev/null @@ -1,31 +0,0 @@ -from pydantic import BaseModel -from typing import List, Optional - -class PublisherBase(BaseModel): - name: str - -class PublisherCreate(PublisherBase): - pass - -class Publisher(PublisherBase): - id: int - books: List["Book"] = [] - - class Config: - orm_mode = True - -class BookBase(BaseModel): - title: str - author: str - description: str - publisher_id: Optional[int] - -class BookCreate(BookBase): - pass - -class Book(BookBase): - id: int - publisher: Optional[Publisher] - - class Config: - orm_mode = True diff --git a/sqlalchemy_1.6_to_2.0/run.py b/sqlalchemy_1.6_to_2.0/run.py new file mode 100644 index 0000000..00e7125 --- /dev/null +++ b/sqlalchemy_1.6_to_2.0/run.py @@ -0,0 +1,173 @@ +import codegen +from codegen import Codebase + + +@codegen.function("sqlalchemy-1.6-to-2.0") +def run(codebase: Codebase): + """Migrate SQLAlchemy code from 1.6 to 2.0 style. + + Updates: + 1. Base class inheritance and imports + 2. Relationship definitions (backref -> back_populates) + 3. Query syntax to 2.0 style + 4. Configuration and lazy loading defaults + """ + changes_by_file = {} + + for file in codebase.files: + print(f"šŸ” Processing {file.filepath}") + + changes = [] + for updater in [ + update_base_class, + update_relationships, + update_query_syntax, + update_configurations, + ]: + if file_changes := updater(file): + changes.extend(file_changes) + + if changes: + changes_by_file[file.filepath] = changes + + # Print summary + print("\nšŸ“ Changes made:") + for filepath, changes in changes_by_file.items(): + print(f"\n{filepath}:") + for change in changes: + print(f"{change}") + + +def update_base_class(file): + """Update Base class inheritance and imports.""" + changes = [] + + # Update imports + if any("Base" in cls.parent_class_names for cls in file.classes): + file.add_import_from_import_string("from sqlalchemy.orm import DeclarativeBase") + changes.append( + "- from sqlalchemy.ext.declarative import declarative_base\n+ from sqlalchemy.orm import DeclarativeBase" + ) + + for imp in file.imports: + if imp.symbol_name == "declarative_base": + imp.remove() + + # Update Base classes + for cls in file.classes: + if cls.name == "Base": + cls.set_parent_class("DeclarativeBase") + changes.append("- class Base(object):\n+ class Base(DeclarativeBase):") + elif "Base" in cls.parent_class_names and not any( + c.name == "Base" for c in file.classes + ): + cls.insert_before("\nclass Base(DeclarativeBase):\n pass\n") + changes.append("+ class Base(DeclarativeBase):\n+ pass") + + return changes if changes else None + + +def update_relationships(file): + """Modernize relationship definitions.""" + changes = [] + + def process_relationship(symbol): + for call in symbol.function_calls: + if call.name != "relationship": + continue + + arg_names = [arg.name for arg in call.args if arg.name] + if "backref" in arg_names: + backref_arg = next(arg for arg in call.args if arg.name == "backref") + old_value = f'relationship("{call.args[0].value.source}", backref="{backref_arg.value.source}"' + call.set_kwarg("back_populates", backref_arg.value.source) + backref_arg.remove() + new_value = f'relationship("{call.args[0].value.source}", back_populates="{backref_arg.value.source}"' + changes.append(f"- {old_value}\n+ {new_value}") + elif "back_populates" not in arg_names: + old_value = f'relationship("{call.args[0].value.source}"' + call.set_kwarg("back_populates", "None") + new_value = ( + f'relationship("{call.args[0].value.source}", back_populates=None' + ) + changes.append(f"- {old_value}\n+ {new_value}") + + for item in [ + *file.functions, + *[m for c in file.classes for m in c.methods], + *[a for c in file.classes for a in c.attributes], + ]: + process_relationship(item) + + return changes if changes else None + + +def update_query_syntax(file): + """Update to SQLAlchemy 2.0 query style.""" + changes = [] + + query_updates = { + "query": "select", + "filter": "where", + "all": "scalars().all", + "first": "scalar_one_or_none", + } + + def process_queries(function): + for call in function.function_calls: + if new_name := query_updates.get(call.name): + old_name = call.name + call.set_name(new_name) + changes.append(f"- db.session.{old_name}()\n+ db.session.{new_name}()") + + for item in [*file.functions, *[m for c in file.classes for m in c.methods]]: + process_queries(item) + + return changes if changes else None + + +def update_configurations(file): + """Update engine, session, and relationship configurations.""" + changes = [] + + # Update engine and session config + for call in file.function_calls: + if call.name == "create_engine" and not any( + arg.name == "future" for arg in call.args + ): + old_call = "create_engine(url)" + call.set_kwarg("future", "True") + call.set_kwarg("pool_pre_ping", "True") + changes.append( + f"- {old_call}\n+ create_engine(url, future=True, pool_pre_ping=True)" + ) + elif call.name == "sessionmaker" and not any( + arg.name == "future" for arg in call.args + ): + old_call = "sessionmaker(bind=engine)" + call.set_kwarg("future", "True") + changes.append(f"- {old_call}\n+ sessionmaker(bind=engine, future=True)") + elif call.name == "relationship" and not any( + arg.name == "lazy" for arg in call.args + ): + old_call = f'relationship("{call.args[0].value.source}"' + call.set_kwarg("lazy", '"select"') + changes.append(f'- {old_call}\n+ {old_call}, lazy="select")') + + # Update Pydantic configs + for cls in file.classes: + if cls.name == "Config": + for attr in cls.attributes: + if attr.name == "orm_mode": + old_attr = "orm_mode = True" + attr.set_name("from_attributes") + attr.set_value("True") + changes.append(f"- {old_attr}\n+ from_attributes = True") + + return changes if changes else None + + +if __name__ == "__main__": + print("\nInitializing codebase...") + codebase = Codebase("./input_repo") + run(codebase) From 4302c53ed47c827ea2222a05e797519dd18fee7d Mon Sep 17 00:00:00 2001 From: Vishal Shenoy Date: Mon, 27 Jan 2025 14:59:26 -0800 Subject: [PATCH 2/8] . --- golden_readme.md | 135 +++++++++++++++++++++++++++++++++ run_format_example.py | 89 ---------------------- sqlalchemy_1.6_to_2.0/guide.md | 96 ----------------------- 3 files changed, 135 insertions(+), 185 deletions(-) create mode 100644 golden_readme.md delete mode 100644 run_format_example.py delete mode 100644 sqlalchemy_1.6_to_2.0/guide.md diff --git a/golden_readme.md b/golden_readme.md new file mode 100644 index 0000000..77150d9 --- /dev/null +++ b/golden_readme.md @@ -0,0 +1,135 @@ + FreezeGun to TimeMachine Migration Example + +This example demonstrates how to use Codegen to automatically migrate test code from FreezeGun to TimeMachine for time mocking. The migration script makes this process simple by handling all the tedious manual updates automatically. + +## How the Migration Script Works + +The script (`run.py`) automates the entire migration process in a few key steps: + +1. **Codebase Loading** + ```python + codebase = Codebase.from_repo( + "getmoto/moto", commit="786a8ada7ed0c7f9d8b04d49f24596865e4b7901") + ``` + - Loads your codebase into Codegen's intelligent code analysis engine + - Provides a simple SDK for making codebase-wide changes + - Supports specific commit targeting for version control + +2. **Test File Detection** + ```python + if "tests" not in file.filepath: + continue + ``` + - Automatically identifies test files using Codegen's file APIs + - Skips non-test files to avoid unnecessary processing + - Focuses changes where time mocking is most commonly used + +3. **Import Updates** + ```python + for imp in file.imports: + if imp.symbol_name and 'freezegun' in imp.source: + if imp.name == 'freeze_time': + imp.edit('from time_machine import travel') + ``` + - Uses Codegen's import analysis to find and update imports + - Handles both direct and aliased imports + - Preserves import structure and formatting + +4. **Function Call Transformation** + ```python + for fcall in file.function_calls: + if 'freeze_time' not in fcall.source: + continue + # Transform freeze_time to travel with tick=False + ``` + - Uses Codegen's function call analysis to find all usages + - Adds required TimeMachine parameters + - Maintains existing arguments and formatting + +## Why This Makes Migration Easy + +1. **Zero Manual Updates** + - Codegen SDK handles all the file searching and updating + - No tedious copy-paste work + +2. **Consistent Changes** + - Codegen ensures all transformations follow the same patterns + - Maintains code style consistency + +3. **Safe Transformations** + - Codegen validates changes before applying them + - Easy to review and revert if needed + +## Common Migration Patterns + +### Decorator Usage +```python +# FreezeGun +@freeze_time("2023-01-01") +def test_function(): + pass + +# Automatically converted to: +@travel("2023-01-01", tick=False) +def test_function(): + pass +``` + +### Context Manager Usage +```python +# FreezeGun +with freeze_time("2023-01-01"): + # test code + +# Automatically converted to: +with travel("2023-01-01", tick=False): + # test code +``` + +### Moving Time Forward +```python +# FreezeGun +freezer = freeze_time("2023-01-01") +freezer.start() +freezer.move_to("2023-01-02") +freezer.stop() + +# Automatically converted to: +traveller = travel("2023-01-01", tick=False) +traveller.start() +traveller.shift(datetime.timedelta(days=1)) +traveller.stop() +``` + +## Key Differences to Note + +1. **Tick Parameter** + - TimeMachine requires explicit tick behavior configuration + - Script automatically adds `tick=False` to match FreezeGun's default behavior + +2. **Time Movement** + - FreezeGun uses `move_to()` with datetime strings + - TimeMachine uses `shift()` with timedelta objects + +3. **Return Values** + - FreezeGun's decorator returns the freezer object + - TimeMachine's decorator returns a traveller object + +## Running the Migration + +```bash +# Install Codegen +pip install codegen +# Run the migration +python run.py +``` + +## Learn More + +- [TimeMachine Documentation](https://github.com/adamchainz/time-machine) +- [FreezeGun Documentation](https://github.com/spulec/freezegun) +- [Codegen Documentation](https://docs.codegen.com) + +## Contributing + +Feel free to submit issues and enhancement requests! \ No newline at end of file diff --git a/run_format_example.py b/run_format_example.py deleted file mode 100644 index 939bf53..0000000 --- a/run_format_example.py +++ /dev/null @@ -1,89 +0,0 @@ -import codegen -from codegen import Codebase -from codegen.sdk.core.detached_symbols.function_call import FunctionCall - - -@codegen.function("useSuspenseQuery-to-useSuspenseQueries") -def run(codebase: Codebase): - """Convert useSuspenseQuery calls to useSuspenseQueries in a React codebase. - - This codemod: - 1. Finds all files containing useSuspenseQuery - 2. Adds the necessary import statement - 3. Converts multiple useSuspenseQuery calls to a single useSuspenseQueries call - """ - # Import statement for useSuspenseQueries - import_str = "import { useQuery, useSuspenseQueries } from '@tanstack/react-query'" - - # Track statistics - files_modified = 0 - functions_modified = 0 - - # Iterate through all files in the codebase - for file in codebase.files: - if "useSuspenseQuery" not in file.source: - continue - - print(f"Processing {file.filepath}") - # Add the import statement - file.add_import_from_import_string(import_str) - file_modified = False - - # Iterate through all functions in the file - for function in file.functions: - if "useSuspenseQuery" not in function.source: - continue - - results = [] # Store left-hand side of assignments - queries = [] # Store query arguments - old_statements = [] # Track statements to replace - - # Find useSuspenseQuery assignments - for stmt in function.code_block.assignment_statements: - if not isinstance(stmt.right, FunctionCall): - continue - - fcall = stmt.right - if fcall.name != "useSuspenseQuery": - continue - - old_statements.append(stmt) - results.append(stmt.left.source) - queries.append(fcall.args[0].value.source) - - # Convert to useSuspenseQueries if needed - if old_statements: - new_query = f"const [{', '.join(results)}] = useSuspenseQueries({{queries: [{', '.join(queries)}]}})" - print( - f"Converting useSuspenseQuery to useSuspenseQueries in {function.name}" - ) - - # Print the diff - print("\nOriginal code:") - for stmt in old_statements: - print(f"- {stmt.source}") - print("\nNew code:") - print(f"+ {new_query}") - print("-" * 50) - - # Replace old statements with new query - for stmt in old_statements: - stmt.edit(new_query) - - functions_modified += 1 - file_modified = True - - if file_modified: - files_modified += 1 - - print("\nModification complete:") - print(f"Files modified: {files_modified}") - print(f"Functions modified: {functions_modified}") - - -if __name__ == "__main__": - print("Initializing codebase...") - codebase = Codebase.from_repo("deepfence/ThreatMapper") - - print("Running codemod...") - run(codebase) diff --git a/sqlalchemy_1.6_to_2.0/guide.md b/sqlalchemy_1.6_to_2.0/guide.md deleted file mode 100644 index 62b2e61..0000000 --- a/sqlalchemy_1.6_to_2.0/guide.md +++ /dev/null @@ -1,96 +0,0 @@ -# Guide: Migrating from SQLAlchemy 1.6 to 2.0 with Codegen - -This guide walks you through the steps to migrate your codebase from SQLAlchemy 1.6 to 2.0 using Codegen. Follow along to modernize your imports, relationships, and query syntax while ensuring compatibility with SQLAlchemy 2.0. Each step includes a direct link to the appropriate codemod for easy implementation. - ---- - -## šŸŽ‰ Overview of Changes - -The migration focuses on these key updates: - -1. **Import Adjustments** - Aligns your code with the updated SQLAlchemy 2.0 module structure. - [Run the Import Codemod](https://www.codegen.sh/search/6506?skillType=codemod) - -2. **Relationship Updates** - Refines relationship definitions by replacing `backref` with `back_populates` for explicitness and better readability. - [Run the Relationship Codemod](https://www.codegen.sh/search/6510?skillType=codemod) - -3. **Query Syntax Modernization** - Updates queries to leverage the latest syntax like `select()` and `where()`, removing deprecated methods. - [Run the Query Syntax Codemod](https://www.codegen.sh/search/6508?skillType=codemod) - -4. **Relationship Lazy Loading** - SQLAlchemy 2.0 introduces a new `lazy` parameter for relationship definitions. Update your relationships to use the new `lazy` parameter for improved performance. - [Run the Relationship Lazy Loading Codemod](https://www.codegen.sh/search/6512?skillType=codemod) - -5. **Type Annotations** - SQLAlchemy 2.0 has improved type annotation support. Update your models to include type hints for better IDE support and runtime type checking. - - - Add type annotations to model attributes and relationships - - Leverage SQLAlchemy's typing module for proper type hints - - Enable better IDE autocompletion and type checking - - [Run the Type Annotations Codemod](https://www.codegen.sh/search/4645?skillType=codemod) - ---- - -## How to Migrate - -### Step 1: Update Imports - -SQLAlchemy 2.0 introduces a refined import structure. Use the import codemod to: - -- Replace wildcard imports (`*`) with explicit imports for better clarity. -- Update `declarative_base` to `DeclarativeBase`. - -šŸ‘‰ [Run the Import Codemod](https://www.codegen.sh/search/6506?skillType=codemod) - ---- - -### Step 2: Refactor Relationships - -In SQLAlchemy 2.0, relationships require more explicit definitions. This includes: - -- Transitioning from `backref` to `back_populates` for consistency. -- Explicitly specifying `back_populates` for all relationship definitions. - -šŸ‘‰ [Run the Relationship Codemod](https://www.codegen.sh/search/6510?skillType=codemod) - ---- - -### Step 3: Modernize Query Syntax - -The query API has been revamped in SQLAlchemy 2.0. Key updates include: - -- Switching to `select()` and `where()` for query construction. -- Replacing any deprecated methods with their modern equivalents. - -šŸ‘‰ [Run the Query Syntax Codemod](https://www.codegen.sh/search/6508?skillType=codemod) - ---- - -### Step 4: Update Relationship Lazy Loading - -SQLAlchemy 2.0 introduces a new `lazy` parameter for relationship definitions. Update your relationships to use the new `lazy` parameter for improved performance. - -šŸ‘‰ [Run the Relationship Lazy Loading Codemod](https://www.codegen.sh/search/6512?skillType=codemod) - ---- - -### Step 5: Add Type Annotations -SQLAlchemy 2.0 has improved type annotation support. Update your models to include type hints for better IDE support and runtime type checking. - -- Add type annotations to model attributes and relationships -- Leverage SQLAlchemy's typing module for proper type hints -- Enable better IDE autocompletion and type checking - -šŸ‘‰ [Run the Type Annotations Codemod](https://www.codegen.sh/search/4645?skillType=codemod) - ---- - -## Need Help? - -If you encounter issues or have specific edge cases not addressed by the codemods, reach out to the Codegen support team or visit the [Codegen Documentation](https://www.codegen.sh/docs) for detailed guidance. - -Start your SQLAlchemy 2.0 migration today and enjoy the benefits of a cleaner, modern codebase! From 74e43debffe490e7da11be1d59dd3a93b58f631c Mon Sep 17 00:00:00 2001 From: Vishal Shenoy Date: Mon, 27 Jan 2025 15:05:13 -0800 Subject: [PATCH 3/8] . --- golden_readme.md | 135 -------------------------- sqlalchemy_1.6_to_2.0/README.md | 18 ++-- sqlalchemy_type_annotations/README.md | 22 +++++ 3 files changed, 29 insertions(+), 146 deletions(-) delete mode 100644 golden_readme.md diff --git a/golden_readme.md b/golden_readme.md deleted file mode 100644 index 77150d9..0000000 --- a/golden_readme.md +++ /dev/null @@ -1,135 +0,0 @@ - FreezeGun to TimeMachine Migration Example - -This example demonstrates how to use Codegen to automatically migrate test code from FreezeGun to TimeMachine for time mocking. The migration script makes this process simple by handling all the tedious manual updates automatically. - -## How the Migration Script Works - -The script (`run.py`) automates the entire migration process in a few key steps: - -1. **Codebase Loading** - ```python - codebase = Codebase.from_repo( - "getmoto/moto", commit="786a8ada7ed0c7f9d8b04d49f24596865e4b7901") - ``` - - Loads your codebase into Codegen's intelligent code analysis engine - - Provides a simple SDK for making codebase-wide changes - - Supports specific commit targeting for version control - -2. **Test File Detection** - ```python - if "tests" not in file.filepath: - continue - ``` - - Automatically identifies test files using Codegen's file APIs - - Skips non-test files to avoid unnecessary processing - - Focuses changes where time mocking is most commonly used - -3. **Import Updates** - ```python - for imp in file.imports: - if imp.symbol_name and 'freezegun' in imp.source: - if imp.name == 'freeze_time': - imp.edit('from time_machine import travel') - ``` - - Uses Codegen's import analysis to find and update imports - - Handles both direct and aliased imports - - Preserves import structure and formatting - -4. **Function Call Transformation** - ```python - for fcall in file.function_calls: - if 'freeze_time' not in fcall.source: - continue - # Transform freeze_time to travel with tick=False - ``` - - Uses Codegen's function call analysis to find all usages - - Adds required TimeMachine parameters - - Maintains existing arguments and formatting - -## Why This Makes Migration Easy - -1. **Zero Manual Updates** - - Codegen SDK handles all the file searching and updating - - No tedious copy-paste work - -2. **Consistent Changes** - - Codegen ensures all transformations follow the same patterns - - Maintains code style consistency - -3. **Safe Transformations** - - Codegen validates changes before applying them - - Easy to review and revert if needed - -## Common Migration Patterns - -### Decorator Usage -```python -# FreezeGun -@freeze_time("2023-01-01") -def test_function(): - pass - -# Automatically converted to: -@travel("2023-01-01", tick=False) -def test_function(): - pass -``` - -### Context Manager Usage -```python -# FreezeGun -with freeze_time("2023-01-01"): - # test code - -# Automatically converted to: -with travel("2023-01-01", tick=False): - # test code -``` - -### Moving Time Forward -```python -# FreezeGun -freezer = freeze_time("2023-01-01") -freezer.start() -freezer.move_to("2023-01-02") -freezer.stop() - -# Automatically converted to: -traveller = travel("2023-01-01", tick=False) -traveller.start() -traveller.shift(datetime.timedelta(days=1)) -traveller.stop() -``` - -## Key Differences to Note - -1. **Tick Parameter** - - TimeMachine requires explicit tick behavior configuration - - Script automatically adds `tick=False` to match FreezeGun's default behavior - -2. **Time Movement** - - FreezeGun uses `move_to()` with datetime strings - - TimeMachine uses `shift()` with timedelta objects - -3. **Return Values** - - FreezeGun's decorator returns the freezer object - - TimeMachine's decorator returns a traveller object - -## Running the Migration - -```bash -# Install Codegen -pip install codegen -# Run the migration -python run.py -``` - -## Learn More - -- [TimeMachine Documentation](https://github.com/adamchainz/time-machine) -- [FreezeGun Documentation](https://github.com/spulec/freezegun) -- [Codegen Documentation](https://docs.codegen.com) - -## Contributing - -Feel free to submit issues and enhancement requests! \ No newline at end of file diff --git a/sqlalchemy_1.6_to_2.0/README.md b/sqlalchemy_1.6_to_2.0/README.md index ad50ddd..7e208b9 100644 --- a/sqlalchemy_1.6_to_2.0/README.md +++ b/sqlalchemy_1.6_to_2.0/README.md @@ -1,12 +1,10 @@ # SQLAlchemy 1.6 to 2.0 Migration Example -[![Documentation](https://img.shields.io/badge/docs-docs.codegen.com-blue)](https://docs.codegen.com/tutorials/sqlalchemy-1.6-to-2.0) - This example demonstrates how to use Codegen to automatically migrate SQLAlchemy 1.6 code to the new 2.0-style query interface. For a complete walkthrough, check out our [tutorial](https://docs.codegen.com/tutorials/sqlalchemy-1.6-to-2.0). -## What This Example Does +## How the Migration Script Works -The migration script handles four key transformations: +The codemod script handles four key transformations: 1. **Convert Query to Select** ```python @@ -18,6 +16,7 @@ The migration script handles four key transformations: select(User).where(User.name == 'john') ).scalars().all() ``` + This transformation replaces the legacy Query interface with the new Select-based API, providing better type safety and consistency. 2. **Update Session Execution** ```python @@ -29,6 +28,7 @@ The migration script handles four key transformations: users = session.execute(select(User)).scalars().all() first_user = session.execute(select(User)).scalars().first() ``` + Session execution is updated to use the new execute() method, which provides clearer separation between SQL construction and execution. 3. **Modernize ORM Relationships** ```python @@ -42,6 +42,7 @@ The migration script handles four key transformations: class Address(Base): user = relationship("User", back_populates="addresses") ``` + Relationships are modernized to use explicit back_populates instead of backref, making bidirectional relationships more maintainable and explicit. 4. **Add Type Annotations** ```python @@ -59,6 +60,7 @@ The migration script handles four key transformations: name: Mapped[str] = mapped_column() addresses: Mapped[List["Address"]] = relationship() ``` + Type annotations are added using SQLAlchemy 2.0's Mapped[] syntax, enabling better IDE support and runtime type checking. ## Running the Example @@ -70,13 +72,7 @@ pip install codegen python run.py ``` -The script will process all Python files in the `repo-before` directory and apply the transformations in the correct order. - -## Understanding the Code - -- `run.py` - The migration script -- `repo-before/` - Sample SQLAlchemy 1.6 application to migrate -- `guide.md` - Additional notes and explanations +The script will process all Python files in the `input_repo` directory and apply the transformations in the correct order. ## Learn More diff --git a/sqlalchemy_type_annotations/README.md b/sqlalchemy_type_annotations/README.md index 776211e..9847411 100644 --- a/sqlalchemy_type_annotations/README.md +++ b/sqlalchemy_type_annotations/README.md @@ -127,6 +127,28 @@ class Book(Base): ) ``` +## Key Differences to Note + +1. **Import Changes** + - New imports required: `from typing import List, Optional` + - `Column` import is replaced with `mapped_column` + - New `Mapped` type wrapper is required + +2. **Column Definition Syntax** + - Old: `column_name = Column(type, **kwargs)` + - New: `column_name: Mapped[type] = mapped_column(**kwargs)` + - Type is moved from constructor to type annotation + +3. **Relationship Changes** + - `backref` parameter is deprecated in favor of explicit `back_populates` + - Relationships require type hints with `Mapped[List["Model"]]` or `Mapped[Optional["Model"]]` + - Forward references use string literals for model names + +4. **Nullable Handling** + - Nullable fields must use `Optional[type]` in type annotation + - Both the type annotation and `nullable=True` parameter are required + - Example: `field: Mapped[Optional[str]] = mapped_column(nullable=True)` + ## Running the Migration ```bash From 8cfa61bc5be4e1b75b702420f3b8d1dcdf1007d6 Mon Sep 17 00:00:00 2001 From: christinewangcw <146775704+christinewangcw@users.noreply.github.com> Date: Mon, 27 Jan 2025 23:22:00 +0000 Subject: [PATCH 4/8] Automated pre-commit update --- sqlalchemy_1.6_to_2.0/input_repo/models.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sqlalchemy_1.6_to_2.0/input_repo/models.py b/sqlalchemy_1.6_to_2.0/input_repo/models.py index f733b83..d87655f 100644 --- a/sqlalchemy_1.6_to_2.0/input_repo/models.py +++ b/sqlalchemy_1.6_to_2.0/input_repo/models.py @@ -1,5 +1,5 @@ -from sqlalchemy import Column, Integer, String, ForeignKey, Index -from sqlalchemy.orm import relationship, backref +from sqlalchemy import Column, Integer, String, ForeignKey +from sqlalchemy.orm import relationship from database import Base From 9faf75f26e4f75c33a5061c42b3f2dca8e380dad Mon Sep 17 00:00:00 2001 From: Vishal Shenoy Date: Mon, 27 Jan 2025 15:37:32 -0800 Subject: [PATCH 5/8] whoops --- sqlalchemy_1.6_to_2.0/README.md | 2 +- sqlalchemy_type_annotations/README.md | 170 ++++++++++---------------- 2 files changed, 65 insertions(+), 107 deletions(-) diff --git a/sqlalchemy_1.6_to_2.0/README.md b/sqlalchemy_1.6_to_2.0/README.md index 7e208b9..a64ece8 100644 --- a/sqlalchemy_1.6_to_2.0/README.md +++ b/sqlalchemy_1.6_to_2.0/README.md @@ -1,6 +1,6 @@ # SQLAlchemy 1.6 to 2.0 Migration Example -This example demonstrates how to use Codegen to automatically migrate SQLAlchemy 1.6 code to the new 2.0-style query interface. For a complete walkthrough, check out our [tutorial](https://docs.codegen.com/tutorials/sqlalchemy-1.6-to-2.0). +This codemod demonstrates how to use Codegen to automatically migrate SQLAlchemy 1.6 code to the new 2.0-style query interface. For a complete walkthrough, check out our [tutorial](https://docs.codegen.com/tutorials/sqlalchemy-1.6-to-2.0). ## How the Migration Script Works diff --git a/sqlalchemy_type_annotations/README.md b/sqlalchemy_type_annotations/README.md index 9847411..8481be6 100644 --- a/sqlalchemy_type_annotations/README.md +++ b/sqlalchemy_type_annotations/README.md @@ -1,78 +1,50 @@ # Enhance SQLAlchemy Type Annotations -This codemod demonstrates how to automatically add type annotations to SQLAlchemy models in your Python codebase. The migration script makes this process simple by handling all the tedious manual updates automatically. - -## How the Migration Script Works - -The script automates the entire migration process in a few key steps: - -1. **Model Detection and Analysis** - ```python - codebase = Codebase.from_repo("your/repo") - for file in codebase.files: - if "models" not in file.filepath: - continue - ``` - - Automatically identifies SQLAlchemy model files - - Analyzes model structure and relationships - - Determines required type annotations - -2. **Type Annotation Updates** - ```python - for column in model.columns: - if isinstance(column, Column): - column.edit(to_mapped_column(column)) - ``` - - Converts Column definitions to typed Mapped columns - - Handles nullable fields with Optional types - - Preserves existing column configurations - -3. **Relationship Transformations** - ```python - for rel in model.relationships: - if isinstance(rel, relationship): - rel.edit(to_typed_relationship(rel)) - ``` - - Updates relationship definitions with proper typing - - Converts backref to back_populates - - Adds List/Optional type wrappers as needed - -## Common Migration Patterns - -### Column Definitions -```python -# Before +This codemod demonstrates how to automatically add type annotations to SQLAlchemy models in your Python codebase. This conversion improves code maintainability and IDE support by adding proper type hints to model attributes and relationships. + +This primarily leverages two APIs: +- [`codebase.files`](/api-reference/Codebase#files) for finding relevant files +- [`file.edit(...)`](/api-reference/Codebase#files) for updating model definitions + +## What This Example Does + +The codemod handles three key transformations: + +1. **Convert Column Definitions to Mapped Types** +```python python +# From: id = Column(Integer, primary_key=True) name = Column(String) -# After +# To: id: Mapped[int] = mapped_column(primary_key=True) name: Mapped[str] = mapped_column() ``` -### Nullable Fields -```python -# Before -description = Column(String, nullable=True) +2. **Update Relationship Definitions** +```python python + # From: +addresses = relationship("Address", backref="user") -# After -description: Mapped[Optional[str]] = mapped_column(nullable=True) +# To: +addresses: Mapped[List["Address"]] = relationship(back_populates="user") ``` -### Relationships -```python -# Before -addresses = relationship("Address", backref="user") +3. **Update Relationship Definitions** +```python python +# From: +description = Column(String, nullable=True) -# After -addresses: Mapped[List["Address"]] = relationship(back_populates="user") +# To: +description: Mapped[Optional[str]] = mapped_column(nullable=True) ``` -## Complete Example +## Example Implementation -### Before Migration -```python -from sqlalchemy import Column, Integer, String, ForeignKey +For a complete example of the transformation, see: + +```python python +from sqlalchemy import Column, Integer, String, ForeignKey, Index from sqlalchemy.orm import relationship, backref from database import Base @@ -83,6 +55,7 @@ class Publisher(Base): name = Column(String, unique=True, index=True) books = relationship("Book", backref="publisher") + class Book(Base): __tablename__ = "books" @@ -93,31 +66,34 @@ class Book(Base): publisher_id = Column(Integer, ForeignKey("publishers.id")) ``` -### After Migration -```python +And its transformed version: + +```python python from typing import List, Optional -from sqlalchemy import ForeignKey -from sqlalchemy.orm import Mapped, mapped_column, relationship +from sqlalchemy import Column, Integer, String, ForeignKey +from sqlalchemy.orm import relationship, Mapped, mapped_column from database import Base class Publisher(Base): __tablename__ = "publishers" - id: Mapped[int] = mapped_column(primary_key=True, index=True) - name: Mapped[str] = mapped_column(unique=True, index=True) + id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True) + name: Mapped[str] = mapped_column(String, unique=True, index=True) books: Mapped[List["Book"]] = relationship( "Book", - back_populates="publisher" + back_populates="publisher", + lazy='selectin' ) class Book(Base): __tablename__ = "books" - id: Mapped[int] = mapped_column(primary_key=True, index=True) - title: Mapped[str] = mapped_column(index=True) - author: Mapped[str] = mapped_column(index=True) - description: Mapped[Optional[str]] = mapped_column(nullable=True) + id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True) + title: Mapped[str] = mapped_column(String, index=True) + author: Mapped[str] = mapped_column(String, index=True) + description: Mapped[Optional[str]] = mapped_column(String, nullable=True) publisher_id: Mapped[Optional[int]] = mapped_column( + Integer, ForeignKey("publishers.id"), nullable=True ) @@ -127,43 +103,25 @@ class Book(Base): ) ``` -## Key Differences to Note - -1. **Import Changes** - - New imports required: `from typing import List, Optional` - - `Column` import is replaced with `mapped_column` - - New `Mapped` type wrapper is required - -2. **Column Definition Syntax** - - Old: `column_name = Column(type, **kwargs)` - - New: `column_name: Mapped[type] = mapped_column(**kwargs)` - - Type is moved from constructor to type annotation - -3. **Relationship Changes** - - `backref` parameter is deprecated in favor of explicit `back_populates` - - Relationships require type hints with `Mapped[List["Model"]]` or `Mapped[Optional["Model"]]` - - Forward references use string literals for model names - -4. **Nullable Handling** - - Nullable fields must use `Optional[type]` in type annotation - - Both the type annotation and `nullable=True` parameter are required - - Example: `field: Mapped[Optional[str]] = mapped_column(nullable=True)` - -## Running the Migration - -```bash -# Install Codegen -pip install codegen -# Run the migration -python run.py -``` +## Running the Transformation -## Learn More +1. Install the codegen package: + ```bash + pip install codegen + ``` -- [SQLAlchemy 2.0 Documentation](https://docs.sqlalchemy.org/en/20/) -- [SQLAlchemy Type Annotations Guide](https://docs.sqlalchemy.org/en/20/orm/typing.html) -- [Codegen Documentation](https://docs.codegen.com) +2. Run the codemod: + ```bash + python3 run.py + ``` -## Contributing +This will: +1. Initialize the codebase from the target repository +2. Find and process files containing `useSuspenseQuery` +3. Apply the transformations +4. Print detailed information to the terminal, including: + - Files being processed + - Before/after diffs for each transformation + - Summary statistics of modified files and functions -Feel free to submit issues and enhancement requests! \ No newline at end of file +The script will output progress and changes to the terminal as it runs, allowing you to review each transformation in real-time. \ No newline at end of file From 10a29566f532d9af056617a36ce017039511bc6f Mon Sep 17 00:00:00 2001 From: Vishal Shenoy Date: Mon, 27 Jan 2025 17:27:54 -0800 Subject: [PATCH 6/8] get_diff --- sqlalchemy_1.6_to_2.0/run.py | 49 +++++++----------------------------- 1 file changed, 9 insertions(+), 40 deletions(-) diff --git a/sqlalchemy_1.6_to_2.0/run.py b/sqlalchemy_1.6_to_2.0/run.py index 00e7125..64f7478 100644 --- a/sqlalchemy_1.6_to_2.0/run.py +++ b/sqlalchemy_1.6_to_2.0/run.py @@ -12,42 +12,27 @@ def run(codebase: Codebase): 3. Query syntax to 2.0 style 4. Configuration and lazy loading defaults """ - changes_by_file = {} - for file in codebase.files: print(f"šŸ” Processing {file.filepath}") - changes = [] for updater in [ update_base_class, update_relationships, update_query_syntax, update_configurations, ]: - if file_changes := updater(file): - changes.extend(file_changes) - - if changes: - changes_by_file[file.filepath] = changes + updater(file) - # Print summary + # Print changes using codebase diff print("\nšŸ“ Changes made:") - for filepath, changes in changes_by_file.items(): - print(f"\n{filepath}:") - for change in changes: - print(f"{change}") + print(codebase.get_diff()) def update_base_class(file): """Update Base class inheritance and imports.""" - changes = [] - # Update imports if any("Base" in cls.parent_class_names for cls in file.classes): file.add_import_from_import_string("from sqlalchemy.orm import DeclarativeBase") - changes.append( - "- from sqlalchemy.ext.declarative import declarative_base\n+ from sqlalchemy.orm import DeclarativeBase" - ) for imp in file.imports: if imp.symbol_name == "declarative_base": @@ -57,14 +42,8 @@ def update_base_class(file): for cls in file.classes: if cls.name == "Base": cls.set_parent_class("DeclarativeBase") - changes.append("- class Base(object):\n+ class Base(DeclarativeBase):") - elif "Base" in cls.parent_class_names and not any( - c.name == "Base" for c in file.classes - ): + elif "Base" in cls.parent_class_names and not any(c.name == "Base" for c in file.classes): cls.insert_before("\nclass Base(DeclarativeBase):\n pass\n") - changes.append("+ class Base(DeclarativeBase):\n+ pass") - - return changes if changes else None def update_relationships(file): @@ -87,9 +66,7 @@ def process_relationship(symbol): elif "back_populates" not in arg_names: old_value = f'relationship("{call.args[0].value.source}"' call.set_kwarg("back_populates", "None") - new_value = ( - f'relationship("{call.args[0].value.source}", back_populates=None' - ) + new_value = f'relationship("{call.args[0].value.source}", back_populates=None' changes.append(f"- {old_value}\n+ {new_value}") for item in [ @@ -132,24 +109,16 @@ def update_configurations(file): # Update engine and session config for call in file.function_calls: - if call.name == "create_engine" and not any( - arg.name == "future" for arg in call.args - ): + if call.name == "create_engine" and not any(arg.name == "future" for arg in call.args): old_call = "create_engine(url)" call.set_kwarg("future", "True") call.set_kwarg("pool_pre_ping", "True") - changes.append( - f"- {old_call}\n+ create_engine(url, future=True, pool_pre_ping=True)" - ) - elif call.name == "sessionmaker" and not any( - arg.name == "future" for arg in call.args - ): + changes.append(f"- {old_call}\n+ create_engine(url, future=True, pool_pre_ping=True)") + elif call.name == "sessionmaker" and not any(arg.name == "future" for arg in call.args): old_call = "sessionmaker(bind=engine)" call.set_kwarg("future", "True") changes.append(f"- {old_call}\n+ sessionmaker(bind=engine, future=True)") - elif call.name == "relationship" and not any( - arg.name == "lazy" for arg in call.args - ): + elif call.name == "relationship" and not any(arg.name == "lazy" for arg in call.args): old_call = f'relationship("{call.args[0].value.source}"' call.set_kwarg("lazy", '"select"') changes.append(f'- {old_call}\n+ {old_call}, lazy="select")') From 2dc4b52954c21e9b279b3273245210efa226b328 Mon Sep 17 00:00:00 2001 From: Vishal Shenoy Date: Mon, 27 Jan 2025 18:08:48 -0800 Subject: [PATCH 7/8] . --- sqlalchemy_1.6_to_2.0/README.md | 59 +++++++++++++++------------------ 1 file changed, 27 insertions(+), 32 deletions(-) diff --git a/sqlalchemy_1.6_to_2.0/README.md b/sqlalchemy_1.6_to_2.0/README.md index a64ece8..886599b 100644 --- a/sqlalchemy_1.6_to_2.0/README.md +++ b/sqlalchemy_1.6_to_2.0/README.md @@ -6,61 +6,56 @@ This codemod demonstrates how to use Codegen to automatically migrate SQLAlchemy The codemod script handles four key transformations: -1. **Convert Query to Select** +1. **Update Base Class and Imports** ```python # From: - session.query(User).filter_by(name='john').all() + from sqlalchemy.ext.declarative import declarative_base + Base = declarative_base() # To: - session.execute( - select(User).where(User.name == 'john') - ).scalars().all() + from sqlalchemy.orm import DeclarativeBase + class Base(DeclarativeBase): + pass ``` - This transformation replaces the legacy Query interface with the new Select-based API, providing better type safety and consistency. + Updates the Base class to use the new DeclarativeBase style. -2. **Update Session Execution** +2. **Modernize ORM Relationships** ```python # From: - users = session.query(User).all() - first_user = session.query(User).first() + class User(Base): + addresses = relationship("Address", backref="user") # To: - users = session.execute(select(User)).scalars().all() - first_user = session.execute(select(User)).scalars().first() + class User(Base): + addresses = relationship("Address", back_populates="user") ``` - Session execution is updated to use the new execute() method, which provides clearer separation between SQL construction and execution. + Relationships are modernized to use explicit back_populates instead of backref. If no back reference is specified, it defaults to None. -3. **Modernize ORM Relationships** +3. **Update Query Method Names** ```python # From: - class User(Base): - addresses = relationship("Address", backref="user") + db.session.query(User).filter(User.name == 'john').all() + db.session.query(User).first() # To: - class User(Base): - addresses = relationship("Address", back_populates="user", use_list=True) - class Address(Base): - user = relationship("User", back_populates="addresses") + db.session.select(User).where(User.name == 'john').scalars().all() + db.session.select(User).scalar_one_or_none() ``` - Relationships are modernized to use explicit back_populates instead of backref, making bidirectional relationships more maintainable and explicit. + Updates query method names to their 2.0 equivalents: `query` → `select`, `filter` → `where`, `all` → `scalars().all`, and `first` → `scalar_one_or_none`. -4. **Add Type Annotations** +4. **Update Configurations** ```python # From: - class User(Base): - __tablename__ = "users" - id = Column(Integer, primary_key=True) - name = Column(String) - addresses = relationship("Address") + create_engine(url) + sessionmaker(bind=engine) + relationship("Address") # To: - class User(Base): - __tablename__ = "users" - id: Mapped[int] = mapped_column(primary_key=True) - name: Mapped[str] = mapped_column() - addresses: Mapped[List["Address"]] = relationship() + create_engine(url, future=True, pool_pre_ping=True) + sessionmaker(bind=engine, future=True) + relationship("Address", lazy="select") ``` - Type annotations are added using SQLAlchemy 2.0's Mapped[] syntax, enabling better IDE support and runtime type checking. + Adds future-compatible configurations and default lazy loading settings. Also updates Pydantic configs from `orm_mode` to `from_attributes`. ## Running the Example From e3ef046aea9aef88c39a2e10a0c4678341db6590 Mon Sep 17 00:00:00 2001 From: Vishal Shenoy Date: Mon, 27 Jan 2025 18:09:50 -0800 Subject: [PATCH 8/8] . --- sqlalchemy_1.6_to_2.0/run.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/sqlalchemy_1.6_to_2.0/run.py b/sqlalchemy_1.6_to_2.0/run.py index 64f7478..4bdefac 100644 --- a/sqlalchemy_1.6_to_2.0/run.py +++ b/sqlalchemy_1.6_to_2.0/run.py @@ -76,8 +76,6 @@ def process_relationship(symbol): ]: process_relationship(item) - return changes if changes else None - def update_query_syntax(file): """Update to SQLAlchemy 2.0 query style.""" @@ -100,8 +98,6 @@ def process_queries(function): for item in [*file.functions, *[m for c in file.classes for m in c.methods]]: process_queries(item) - return changes if changes else None - def update_configurations(file): """Update engine, session, and relationship configurations.""" @@ -133,8 +129,6 @@ def update_configurations(file): attr.set_value("True") changes.append(f"- {old_attr}\n+ from_attributes = True") - return changes if changes else None - if __name__ == "__main__": print("\nInitializing codebase...")