diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index ab47f3a..627ddd1 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -16,7 +16,7 @@ jobs: strategy: matrix: python-version: - - "3.7" + - "3.9" - "3.12" DB: - "sqlite" @@ -72,17 +72,17 @@ jobs: --health-interval 30s --health-timeout 15s --health-retries 5 - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v5 - name: Install Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} - name: Install Project run: | python -m pip install --upgrade pip - pip install --upgrade poetry + pip install --upgrade poetry poetry install - name: install oracle dependencies if: ${{ matrix.DB == 'oracle' }} @@ -111,17 +111,15 @@ jobs: lint: runs-on: ubuntu-latest steps: - - name: Black Validation - uses: psf/black@stable + - uses: actions/checkout@v5 + - uses: astral-sh/ruff-action@v3 with: - version: "23.3.0" # Last version which can be used in py3.7 - options: "--check --verbose" - - uses: actions/checkout@v3 - - name: ruff-action - uses: chartboost/ruff-action@v1 + args: "--version" + - run: ruff check --fix + - run: ruff format generate-coverage-report: - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 needs: [test-sqlalchemy-history] steps: - name: Coveralls diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index 2c3588f..31fb905 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -9,7 +9,7 @@ jobs: - name: Checkout uses: actions/checkout@v2 - name: Setup Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v6 with: python-version: 3.9 - name: Install Dependencies diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3e07f8f..355dbec 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -42,8 +42,9 @@ git checkout -b add-issue-num # Run tests locally DB=sqlite poetry run pytest -# Lint -poetry run black . +# Lint & Format +poetry run ruff format . +poetry run ruff check --fix . ``` - Add commit for your changes with message title and message description brifly explaining the approach - Keep commit message title 72 characters diff --git a/docs/README.md b/docs/README.md index 7c53b21..77e3361 100644 --- a/docs/README.md +++ b/docs/README.md @@ -7,7 +7,7 @@ An auditing extension for sqlalchemy which keeps a track of the history of your ## Features -- Supports sqlalchemy 2+ and python 3.7+ +- Supports sqlalchemy 2+ and python 3.9+ - Tracks history for inserts, deletes, and updates - Does not store updates which don't change anything - Supports alembic migrations diff --git a/pyproject.toml b/pyproject.toml index 80f6a42..a2e37c0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,14 +20,13 @@ classifiers=[ ] [tool.poetry.dependencies] -python = "^3.7" +python = "^3.9" SQLAlchemy = ">=2" SQLAlchemy-Utils = ">=0.30.12" cached-property = "*" -[tool.poetry.dev-dependencies] -black = "*" +[tool.poetry.group.dev.dependencies] ruff = "*" pre-commit = "2.21.0" @@ -41,11 +40,9 @@ pymssql = ">=2.2.0" cx-Oracle = "8.3.0" pytest-cov = "*" - -[tool.black] -# Set line-length lower than 120 as black will go 10% above if needed +[tool.ruff] line-length = 110 -target-version = ['py37'] +target-version = 'py39' [tool.coverage.run] dynamic_context = "test_function" diff --git a/sqlalchemy_history/builder.py b/sqlalchemy_history/builder.py index 8071c0b..c63b243 100644 --- a/sqlalchemy_history/builder.py +++ b/sqlalchemy_history/builder.py @@ -1,5 +1,5 @@ """Builder Module Detects and builds version class for models and version tables collected during instrument - phase by the manager +phase by the manager """ from copy import copy diff --git a/sqlalchemy_history/expression_reflector.py b/sqlalchemy_history/expression_reflector.py index 322d08d..5e9f658 100644 --- a/sqlalchemy_history/expression_reflector.py +++ b/sqlalchemy_history/expression_reflector.py @@ -1,5 +1,5 @@ -"""This is ExpressionReflector used for generating expression queries. -""" +"""This is ExpressionReflector used for generating expression queries.""" + import sqlalchemy as sa from sqlalchemy.sql.expression import bindparam diff --git a/sqlalchemy_history/fetcher.py b/sqlalchemy_history/fetcher.py index 4294b92..b6c18e0 100644 --- a/sqlalchemy_history/fetcher.py +++ b/sqlalchemy_history/fetcher.py @@ -1,5 +1,5 @@ -"""Fetcher Module helps traverse across versions for a given versioned object. -""" +"""Fetcher Module helps traverse across versions for a given versioned object.""" + import operator import sqlalchemy as sa from sqlalchemy_utils import get_primary_keys, identity diff --git a/sqlalchemy_history/manager.py b/sqlalchemy_history/manager.py index b0107ef..a440d53 100644 --- a/sqlalchemy_history/manager.py +++ b/sqlalchemy_history/manager.py @@ -6,6 +6,7 @@ and the actual versioning to UnitOfWork class. """ + from functools import wraps import sqlalchemy as sa diff --git a/sqlalchemy_history/model_builder.py b/sqlalchemy_history/model_builder.py index 31ca21f..6695671 100644 --- a/sqlalchemy_history/model_builder.py +++ b/sqlalchemy_history/model_builder.py @@ -1,5 +1,5 @@ -"""Model Builder module build Versioned Models -""" +"""Model Builder module build Versioned Models""" + from copy import copy import sqlalchemy as sa from sqlalchemy.ext.declarative import declared_attr diff --git a/sqlalchemy_history/operation.py b/sqlalchemy_history/operation.py index fea54fd..efc3653 100644 --- a/sqlalchemy_history/operation.py +++ b/sqlalchemy_history/operation.py @@ -1,5 +1,4 @@ -"""Operations module contains Operation Class. -""" +"""Operations module contains Operation Class.""" from copy import copy @@ -90,7 +89,12 @@ def add_update(self, target): del state_copy[rel_key] if state_copy: - self.add(Operation(target, Operation.UPDATE)) + if target in self: + # If already in current transaction and some event hook did a update + # prior to commit hook, continue with operation type as it is + self.add(Operation(target, self[self.format_key(target)].type)) + else: + self.add(Operation(target, Operation.UPDATE)) def add_delete(self, target): self.add(Operation(target, Operation.DELETE)) diff --git a/sqlalchemy_history/plugins/transaction_changes.py b/sqlalchemy_history/plugins/transaction_changes.py index bccfa56..b885cde 100644 --- a/sqlalchemy_history/plugins/transaction_changes.py +++ b/sqlalchemy_history/plugins/transaction_changes.py @@ -21,6 +21,7 @@ 233678 Article ================ ================= """ + import sqlalchemy as sa from sqlalchemy_history.plugins.base import Plugin diff --git a/sqlalchemy_history/relationship_builder.py b/sqlalchemy_history/relationship_builder.py index 28de44a..3610d4f 100644 --- a/sqlalchemy_history/relationship_builder.py +++ b/sqlalchemy_history/relationship_builder.py @@ -1,6 +1,7 @@ """Relationship Builder builds and manages relations between versioned model built by builder - module for versioned package +module for versioned package """ + import sqlalchemy as sa from sqlalchemy_history.exc import ClassNotVersioned diff --git a/sqlalchemy_history/reverter.py b/sqlalchemy_history/reverter.py index 7f5fbcd..dca6dd1 100644 --- a/sqlalchemy_history/reverter.py +++ b/sqlalchemy_history/reverter.py @@ -1,5 +1,5 @@ -"""Reverter Reverts. -""" +"""Reverter Reverts.""" + import sqlalchemy as sa from sqlalchemy_history.operation import Operation from sqlalchemy_history.utils import versioned_column_properties, parent_class diff --git a/sqlalchemy_history/table_builder.py b/sqlalchemy_history/table_builder.py index 3b02eba..3ba7bd2 100644 --- a/sqlalchemy_history/table_builder.py +++ b/sqlalchemy_history/table_builder.py @@ -1,5 +1,5 @@ -"""Table Builder Builds versioned table. -""" +"""Table Builder Builds versioned table.""" + import sqlalchemy as sa from sqlalchemy.sql.sqltypes import Enum @@ -34,7 +34,7 @@ def reflect_column(self, column): if column_copy.name == self.option("transaction_column_name"): column_copy.nullable = False if isinstance(column_copy.type, Enum): - column_copy.type.name = 'history_' + column_copy.type.name + column_copy.type.name = "history_" + column_copy.type.name if not column_copy.primary_key: column_copy.nullable = True diff --git a/sqlalchemy_history/transaction.py b/sqlalchemy_history/transaction.py index 4347374..0088f4d 100644 --- a/sqlalchemy_history/transaction.py +++ b/sqlalchemy_history/transaction.py @@ -1,5 +1,4 @@ -"""Transaction model makes transactions for history tables -""" +"""Transaction model makes transactions for history tables""" from collections import OrderedDict import datetime @@ -113,7 +112,8 @@ def __repr__(self): ) return "" % ", ".join( ( - "%s=%r" % (field, value) if not isinstance(value, int) + "%s=%r" % (field, value) + if not isinstance(value, int) # We want the following line to ensure that longs get # shown without the ugly L suffix on python 2.x # versions diff --git a/sqlalchemy_history/unit_of_work.py b/sqlalchemy_history/unit_of_work.py index 84e78fe..b3d3889 100644 --- a/sqlalchemy_history/unit_of_work.py +++ b/sqlalchemy_history/unit_of_work.py @@ -1,19 +1,19 @@ -"""UnitOfWork module tracks all unit of transaction needed to be done to track history models trnasactions -""" +"""UnitOfWork module tracks all unit of transaction needed to be done to track history models transactions""" from copy import copy import sqlalchemy as sa from sqlalchemy_utils import get_primary_keys, identity + from sqlalchemy_history.operation import Operations +from sqlalchemy_history.schema import update_end_tx_column from sqlalchemy_history.utils import ( end_tx_column_name, - version_class, is_session_modified, tx_column_name, + version_class, versioned_column_properties, ) -from sqlalchemy_history.schema import update_end_tx_column class UnitOfWork(object): diff --git a/tests/builders/test_table_builder.py b/tests/builders/test_table_builder.py index 824cb60..59c7079 100644 --- a/tests/builders/test_table_builder.py +++ b/tests/builders/test_table_builder.py @@ -93,9 +93,7 @@ class Article(self.Model): last_update = sa.Column( sa.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False ) - enum_col = sa.Column( - sa.Enum('TYPE_A', 'TYPE_B', name='test_enum') - ) + enum_col = sa.Column(sa.Enum("TYPE_A", "TYPE_B", name="test_enum")) self.Article = Article @@ -139,12 +137,10 @@ class Article(self.Model): sa.Integer, sa.Sequence(f"{__tablename__}_seq", start=1), autoincrement=True, primary_key=True ) - enum_col = sa.Column( - sa.Enum('TYPE_A', 'TYPE_B', name='test_enum') - ) + enum_col = sa.Column(sa.Enum("TYPE_A", "TYPE_B", name="test_enum")) self.Article = Article def test_name_enums(self): version_model = version_class(self.Article) - assert version_model.enum_col.type.name == 'history_test_enum' \ No newline at end of file + assert version_model.enum_col.type.name == "history_test_enum" diff --git a/tests/reported_bugs/test_bug_141_after_flush_postexec_op_type_issue.py b/tests/reported_bugs/test_bug_141_after_flush_postexec_op_type_issue.py new file mode 100644 index 0000000..5cbedc3 --- /dev/null +++ b/tests/reported_bugs/test_bug_141_after_flush_postexec_op_type_issue.py @@ -0,0 +1,45 @@ +import sqlalchemy as sa +from copy import copy + +from tests import TestCase +from sqlalchemy_history import version_class + + +class TestBug141(TestCase): + # ref: https://github.com/corridor/sqlalchemy-history/issues/141 + def create_models(self): + class Author(self.Model): + __tablename__ = "author" + __versioned__ = copy(self.options) + + id = sa.Column( + sa.Integer, sa.Sequence(f"{__tablename__}_seq", start=1), autoincrement=True, primary_key=True + ) + name = sa.Column(sa.Unicode(255)) + + self.Author = Author + + def test_add_record(self): + author = self.Author(name="Author 1") + + @sa.event.listens_for(self.session, "after_flush_postexec") + def after_flush_postexec(session, flush_context): + if author.name != "yoyoyoyoyo": + author.name = "yoyoyoyoyo" + + self.session.add(author) + self.session.commit() + + versioned_objs = self.session.query(version_class(self.Author)).all() + assert len(versioned_objs) == 1 + assert versioned_objs[0].operation_type == 0 + assert versioned_objs[0].name == "yoyoyoyoyo" + author.name = "sdfeoinfe" + self.session.add(author) + self.session.commit() + versioned_objs = self.session.query(version_class(self.Author)).all() + assert len(versioned_objs) == 2 + assert versioned_objs[0].operation_type == 0 + assert versioned_objs[1].operation_type == 1 + assert versioned_objs[0].name == versioned_objs[1].name == "yoyoyoyoyo" + sa.event.remove(self.session, "after_flush_postexec", after_flush_postexec) diff --git a/tests/reported_bugs/test_bug_27_datetime_insertion_issue.py b/tests/reported_bugs/test_bug_27_datetime_insertion_issue.py index 7967090..ec87277 100644 --- a/tests/reported_bugs/test_bug_27_datetime_insertion_issue.py +++ b/tests/reported_bugs/test_bug_27_datetime_insertion_issue.py @@ -20,7 +20,7 @@ def create_models(self): sa.DateTime, nullable=False, server_default=sa.func.current_timestamp(), - default=lambda: datetime.datetime.now(datetime.timezone.utc) + default=lambda: datetime.datetime.now(datetime.timezone.utc), ), ) diff --git a/tests/schema/test_update_end_transaction_id.py b/tests/schema/test_update_end_transaction_id.py index 0b7e72d..825d720 100644 --- a/tests/schema/test_update_end_transaction_id.py +++ b/tests/schema/test_update_end_transaction_id.py @@ -25,7 +25,7 @@ def create_models(self): sa.DateTime, nullable=False, server_default=sa.func.current_timestamp(), - default=lambda: datetime.datetime.now(datetime.timezone.utc) + default=lambda: datetime.datetime.now(datetime.timezone.utc), ), ) diff --git a/tests/test_exotic_operation_combos.py b/tests/test_exotic_operation_combos.py index 11f388b..d2fab17 100644 --- a/tests/test_exotic_operation_combos.py +++ b/tests/test_exotic_operation_combos.py @@ -1,6 +1,3 @@ -import os -from pytest import mark - from sqlalchemy_history.operation import Operation from tests import TestCase, create_test_cases @@ -40,10 +37,6 @@ def test_insert_deleted_and_flushed_object(self): assert article2.versions[0].operation_type == Operation.INSERT assert article2.versions[1].operation_type == Operation.UPDATE - # Ref for mssql: https://github.com/sqlalchemy/sqlalchemy/discussions/8829 - @mark.skipif( - os.environ.get("DB") == "mssql", reason="mssql does not support changing the IDENTITY column" - ) def test_replace_deleted_object_with_update(self): article = self.Article() article.name = "Some article" @@ -58,7 +51,7 @@ def test_replace_deleted_object_with_update(self): self.session.delete(article) self.session.flush() - article2.id = article.id + article2.name = article.name self.session.commit() assert article2.versions.count() == 2 assert article2.versions[0].operation_type == Operation.INSERT diff --git a/tests/test_transaction.py b/tests/test_transaction.py index 9895301..30c8de7 100644 --- a/tests/test_transaction.py +++ b/tests/test_transaction.py @@ -45,11 +45,13 @@ def test_changed_entities(self): } def test_transaction_issued_at(self): - time.sleep(1) + time.sleep(1) self.article.name = "Some article 2" self.session.add(self.article) self.session.commit() - assert self.article.versions[0].transaction.issued_at != self.article.versions[1].transaction.issued_at + assert ( + self.article.versions[0].transaction.issued_at != self.article.versions[1].transaction.issued_at + ) # Check that the tests pass without TransactionChangesPlugin diff --git a/tests/utils/test_parent_table.py b/tests/utils/test_parent_table.py index 7724ebf..d81d47b 100644 --- a/tests/utils/test_parent_table.py +++ b/tests/utils/test_parent_table.py @@ -1,4 +1,3 @@ - import datetime import pytest import sqlalchemy as sa @@ -23,7 +22,7 @@ def create_models(self): sa.DateTime, nullable=False, server_default=sa.func.current_timestamp(), - default=lambda: datetime.datetime.now(datetime.timezone.utc) + default=lambda: datetime.datetime.now(datetime.timezone.utc), ), ) diff --git a/tests/utils/test_version_table.py b/tests/utils/test_version_table.py index 105a2c0..55e8520 100644 --- a/tests/utils/test_version_table.py +++ b/tests/utils/test_version_table.py @@ -23,7 +23,7 @@ def create_models(self): sa.DateTime, nullable=False, server_default=sa.func.current_timestamp(), - default=lambda: datetime.datetime.now(datetime.timezone.utc) + default=lambda: datetime.datetime.now(datetime.timezone.utc), ), ) diff --git a/tox.ini b/tox.ini index a9e4733..c402cbc 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py37 +envlist = py39 [testenv] commands = pip install -e ".[test]"