Skip to content
Merged
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
6 changes: 3 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.9", "3.14"]
python-version: ["3.10", "3.14"]

steps:
- uses: actions/checkout@v4
Expand All @@ -37,7 +37,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.9", "3.14"]
python-version: ["3.10", "3.14"]

steps:
- uses: actions/checkout@v4
Expand All @@ -63,7 +63,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
tests-command: ["test", "test-sqlalchemy14"]

services:
Expand Down
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,24 @@
# Changelog

## [1.0.3]

15/11/2025

- Improve project description.
- Remove support for Python 3.9.
- Update type hint syntax to be compatible with Python 3.10.
- Update pytest to 9.*.

## [1.0.2]

15/11/2025

- Fix issus with sqlalchemy-utils being pinned too strictly.
- Fix tox issues.
- Remove print statements from tests.
- Fix issues with type hints.

## [1.0.0]

- Version 1.0.0.
- This version breaks compatibility with any previous versions of the library.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ ty:
# requirements

install-reqs:
uv pip install -e ."[dev,lint,test]"
uv pip install -U -e ."[dev,lint,test]"

install-tox-uv:
uv tool install tox --with tox-uv
Expand Down
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
# SQLAlchemy Diff

A tool for comparing SQLAlchemy models.
A tool for comparing database schemas using SQLAlchemy.

## Requirements

- Python 3.9 or higher (supports 3.9, 3.10, 3.11, 3.12, 3.13, 3.14)
- Python 3.10 or higher (supports 3.10, 3.11, 3.12, 3.13, 3.14)
- SQLAlchemy >= 1.4
- sqlalchemy-utils ~= 0.41.2

Expand Down Expand Up @@ -100,7 +100,7 @@ class MyCustomInspector(BaseInspector, DiffMixin):
# Set to False if it operates at table level (like columns, indexes)
db_level = False

def inspect(self, engine: Engine, ignore_specs: Optional[list] = None) -> dict:
def inspect(self, engine: Engine, ignore_specs: list[IgnoreSpecType] | None = None) -> dict:
"""
Inspect the database and return structured data.

Expand Down Expand Up @@ -161,7 +161,7 @@ class SequencesInspector(BaseInspector, DiffMixin):
key = "sequences"
db_level = True

def inspect(self, engine: Engine, ignore_specs: Optional[list] = None) -> dict:
def inspect(self, engine: Engine, ignore_specs: list[IgnoreSpecType] | None = None) -> dict:
ignore_clauses = self._filter_ignorers(ignore_specs)
inspector = self._get_inspector(engine)

Expand Down
11 changes: 5 additions & 6 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
[project]
name = "sqlalchemy-diff"
version = "1.0.2"
version = "1.0.3"
authors = [
{ name = "Fabrizio Romano", email = "gianchub@gmail.com" },
{ name = "Mark McArdle", email = "m.mc4rdle@gmail.com" },
]
description = "A tool for comparing SQLAlchemy models"
description = "A tool for comparing database schemas using SQLAlchemy"
readme = "README.md"
license-files = ["LICEN[CS]E*"]
requires-python = ">=3.9"
requires-python = ">=3.10"
classifiers = [
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
Expand Down Expand Up @@ -46,7 +45,7 @@ lint = [
"ty",
]
test = [
"pytest~=8.4.2",
"pytest~=9.0.1",
"pytest-cov~=7.0.0",
"psycopg2-binary",
"pytest-xdist>=3.8.0",
Expand Down Expand Up @@ -82,7 +81,7 @@ line-length = 100

[tool.ruff]
line-length = 100
target-version = "py39"
target-version = "py310"

[tool.ruff.format]
# Like Black, use double quotes for strings.
Expand Down
14 changes: 7 additions & 7 deletions src/sqlalchemydiff/comparer.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import logging
from collections.abc import Iterable
from copy import deepcopy
from typing import Any, Optional
from typing import Any

from sqlalchemy.engine import Engine

Expand Down Expand Up @@ -104,8 +104,8 @@ def from_params(
cls,
db_one_uri: str,
db_two_uri: str,
db_one_params: Optional[dict[str, Any]] = None,
db_two_params: Optional[dict[str, Any]] = None,
db_one_params: dict[str, Any] | None = None,
db_two_params: dict[str, Any] | None = None,
):
db_one_params = db_one_params or {}
db_two_params = db_two_params or {}
Expand All @@ -118,8 +118,8 @@ def compare(
self,
one_alias: str = "one",
two_alias: str = "two",
ignores: Optional[list[str]] = None,
ignore_inspectors: Optional[Iterable[str]] = None,
ignores: list[str] | None = None,
ignore_inspectors: Iterable[str] | None = None,
):
ignore_specs = self.ignore_spec_factory_class().create_specs(register, ignores)

Expand All @@ -139,7 +139,7 @@ def compare(
return self.compare_result_class(result, one_alias=one_alias, two_alias=two_alias)

def _filter_inspectors(
self, ignore_inspectors: Optional[set[str]]
self, ignore_inspectors: set[str] | None
) -> list[tuple[str, type[BaseInspector]]]:
if not ignore_inspectors:
ignore_inspectors = set()
Expand All @@ -152,7 +152,7 @@ def _filter_inspectors(

def _get_db_info(
self, ignore_specs: list[IgnoreSpecType], inspector: BaseInspector, engine: Engine
) -> Optional[dict]:
) -> dict | None:
try:
return inspector.inspect(engine, ignore_specs)
except InspectorNotSupported as e:
Expand Down
5 changes: 2 additions & 3 deletions src/sqlalchemydiff/inspection/base.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import abc
import inspect as stdlib_inspect
from typing import Optional

from sqlalchemy import inspect
from sqlalchemy.engine import Engine
Expand Down Expand Up @@ -59,7 +58,7 @@ def __init__(self, one_alias: str = "one", two_alias: str = "two"):

@abc.abstractmethod
def inspect(
self, engine: Engine, ignore_specs: Optional[list[IgnoreSpecType]] = None
self, engine: Engine, ignore_specs: list[IgnoreSpecType] | None = None
) -> dict: ... # pragma: no cover

@abc.abstractmethod
Expand All @@ -68,7 +67,7 @@ def diff(self, one: dict, two: dict) -> dict: ... # pragma: no cover
@abc.abstractmethod
def _is_supported(self, inspector: Inspector) -> bool: ... # pragma: no cover

def _filter_ignorers(self, specs: Optional[list[IgnoreSpecType]]) -> IgnoreClauses:
def _filter_ignorers(self, specs: list[IgnoreSpecType] | None) -> IgnoreClauses:
tables, enums, clauses = [], [], []

for spec in specs or []:
Expand Down
14 changes: 6 additions & 8 deletions src/sqlalchemydiff/inspection/ignore.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
from dataclasses import dataclass, field
from typing import NamedTuple, Optional, Union
from typing import NamedTuple


class TableIgnoreSpec(NamedTuple):
table_name: str
inspector_key: Optional[str] = None
object_name: Optional[str] = None
inspector_key: str | None = None
object_name: str | None = None


class EnumIgnoreSpec(NamedTuple):
name: str


IgnoreSpecType = Union[TableIgnoreSpec, EnumIgnoreSpec]
IgnoreSpecType = TableIgnoreSpec | EnumIgnoreSpec


class IgnoreSpecFactory:
Expand All @@ -29,9 +29,7 @@ class IgnoreSpecFactory:
separator = "."

@classmethod
def create_specs(
cls, register: dict, ignores: Optional[list[str]] = None
) -> list[IgnoreSpecType]:
def create_specs(cls, register: dict, ignores: list[str] | None = None) -> list[IgnoreSpecType]:
if not ignores:
return []

Expand Down Expand Up @@ -64,6 +62,6 @@ class IgnoreClauses:
enums: list[str] = field(default_factory=list)
clauses: list[IgnoreSpecType] = field(default_factory=list)

def is_clause(self, table_name: str, inspector_key: str, object_name: Optional[str]) -> bool:
def is_clause(self, table_name: str, inspector_key: str, object_name: str | None) -> bool:
clause = TableIgnoreSpec(table_name, inspector_key, object_name)
return clause in self.clauses
22 changes: 11 additions & 11 deletions src/sqlalchemydiff/inspection/inspectors.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Optional, cast
from typing import cast

from sqlalchemy.engine import Engine

Expand All @@ -14,11 +14,11 @@ class TablesInspector(BaseInspector, DiffMixin):
key = "tables"
db_level = True

def inspect(self, engine: Engine, ignore_specs: Optional[list[IgnoreSpecType]] = None) -> dict:
def inspect(self, engine: Engine, ignore_specs: list[IgnoreSpecType] | None = None) -> dict:
ignore_clauses = self._filter_ignorers(ignore_specs)
inspector = self._get_inspector(engine)

def get_comment(table_name: str) -> Optional[str]:
def get_comment(table_name: str) -> str | None:
try:
return inspector.get_table_comment(table_name)["text"]
except NotImplementedError:
Expand All @@ -30,7 +30,7 @@ def get_comment(table_name: str) -> Optional[str]:
if table_name not in ignore_clauses.tables
}

def _format_table(self, table_name: str, comment: Optional[str] = None) -> dict:
def _format_table(self, table_name: str, comment: str | None = None) -> dict:
return {
"name": table_name,
"comment": comment or "",
Expand All @@ -48,7 +48,7 @@ class ColumnsInspector(BaseInspector, DiffMixin):

key = "columns"

def inspect(self, engine: Engine, ignore_specs: Optional[list[IgnoreSpecType]] = None) -> dict:
def inspect(self, engine: Engine, ignore_specs: list[IgnoreSpecType] | None = None) -> dict:
ignore_clauses = self._filter_ignorers(ignore_specs)

inspector = self._get_inspector(engine)
Expand Down Expand Up @@ -90,7 +90,7 @@ class PrimaryKeysInspector(BaseInspector, DiffMixin):

key = "primary_keys"

def inspect(self, engine: Engine, ignore_specs: Optional[list[IgnoreSpecType]] = None) -> dict:
def inspect(self, engine: Engine, ignore_specs: list[IgnoreSpecType] | None = None) -> dict:
ignore_clauses = self._filter_ignorers(ignore_specs)

inspector = self._get_inspector(engine)
Expand Down Expand Up @@ -121,7 +121,7 @@ class ForeignKeysInspector(BaseInspector, DiffMixin):

key = "foreign_keys"

def inspect(self, engine: Engine, ignore_specs: Optional[list[IgnoreSpecType]] = None) -> dict:
def inspect(self, engine: Engine, ignore_specs: list[IgnoreSpecType] | None = None) -> dict:
ignore_clauses = self._filter_ignorers(ignore_specs)

inspector = self._get_inspector(engine)
Expand Down Expand Up @@ -155,7 +155,7 @@ class IndexesInspector(BaseInspector, DiffMixin):

key = "indexes"

def inspect(self, engine: Engine, ignore_specs: Optional[list[IgnoreSpecType]] = None) -> dict:
def inspect(self, engine: Engine, ignore_specs: list[IgnoreSpecType] | None = None) -> dict:
ignore_clauses = self._filter_ignorers(ignore_specs)
inspector = self._get_inspector(engine)
table_names = inspector.get_table_names()
Expand Down Expand Up @@ -183,7 +183,7 @@ class UniqueConstraintsInspector(BaseInspector, DiffMixin):

key = "unique_constraints"

def inspect(self, engine: Engine, ignore_specs: Optional[list[IgnoreSpecType]] = None) -> dict:
def inspect(self, engine: Engine, ignore_specs: list[IgnoreSpecType] | None = None) -> dict:
ignore_clauses = self._filter_ignorers(ignore_specs)
inspector = self._get_inspector(engine)
table_names = inspector.get_table_names()
Expand Down Expand Up @@ -220,7 +220,7 @@ class CheckConstraintsInspector(BaseInspector, DiffMixin):

key = "check_constraints"

def inspect(self, engine: Engine, ignore_specs: Optional[list[IgnoreSpecType]] = None) -> dict:
def inspect(self, engine: Engine, ignore_specs: list[IgnoreSpecType] | None = None) -> dict:
ignore_clauses = self._filter_ignorers(ignore_specs)
inspector = self._get_inspector(engine)
table_names = inspector.get_table_names()
Expand Down Expand Up @@ -250,7 +250,7 @@ class EnumsInspector(BaseInspector, DiffMixin):
db_level = True

def inspect(
self, engine: Engine, ignore_specs: Optional[list[IgnoreSpecType]] = None
self, engine: Engine, ignore_specs: list[IgnoreSpecType] | None = None
) -> list[dict]:
inspector = self._get_inspector(engine)

Expand Down
9 changes: 4 additions & 5 deletions tests/models/models_one.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
# ty: ignore[invalid-assignment]
import datetime
import enum
from typing import Optional

from sqlalchemy import (
CheckConstraint,
Expand Down Expand Up @@ -36,8 +35,8 @@ class Employee(Base):
age: Mapped[int] = Column(Integer, nullable=False, default=21)
ssn: Mapped[str] = Column(Unicode(30), nullable=False)
number_of_pets: Mapped[int] = Column(Integer, default=1, nullable=False)
title: Mapped[Optional[Title]] = Column(Enum(Title, native_enum=True))
department: Mapped[Optional[str]] = Column( # ty: ignore[invalid-assignment]
title: Mapped[Title | None] = Column(Enum(Title, native_enum=True))
department: Mapped[str | None] = Column( # ty: ignore[invalid-assignment]
Enum("Product", "Engineering", "Sales", native_enum=False)
)

Expand Down Expand Up @@ -77,7 +76,7 @@ class Role(Base):

employees: Mapped[list["Employee"]] = relationship("Employee", back_populates="role")

role_type: Mapped[Optional[str]] = Column(Enum("Permanent", "Contractor", name="role_type"))
role_type: Mapped[str | None] = Column(Enum("Permanent", "Contractor", name="role_type"))


class Skill(Base):
Expand All @@ -87,7 +86,7 @@ class Skill(Base):
}

slug: Mapped[str] = Column(String(50), primary_key=True)
description: Mapped[Optional[str]] = Column(Unicode(100), nullable=True)
description: Mapped[str | None] = Column(Unicode(100), nullable=True)

employee: Mapped[int] = Column(
Integer, ForeignKey("employees.id", name="fk_skills_employees"), nullable=False
Expand Down
Loading