diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml new file mode 100644 index 0000000..eb4f0c6 --- /dev/null +++ b/.github/workflows/pre-commit.yml @@ -0,0 +1,33 @@ +# Generated from: +# https://github.com/zopefoundation/meta/tree/master/config/zope-product +name: pre-commit + +on: + pull_request: + push: + branches: + - master + # Allow to run this workflow manually from the Actions tab + workflow_dispatch: + +env: + FORCE_COLOR: 1 + +jobs: + pre-commit: + name: linting + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: 3.x + - uses: pre-commit/action@v3.0.1 + with: + extra_args: --all-files --show-diff-on-failure + env: + PRE_COMMIT_COLOR: always + - uses: pre-commit-ci/lite-action@v1.0.2 + if: always() + with: + msg: Apply pre-commit code formatting diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 8f22689..83d4560 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -17,16 +17,17 @@ jobs: fail-fast: false matrix: os: - - ["ubuntu", "ubuntu-20.04"] + - ["ubuntu", "ubuntu-latest"] config: # [Python version, tox env] - - ["3.9", "lint"] - - ["3.7", "py37"] - - ["3.8", "py38"] - - ["3.9", "py39"] - - ["3.10", "py310"] - - ["3.11", "py311"] - - ["3.9", "coverage"] + - ["3.11", "release-check"] + - ["3.8", "py38"] + - ["3.9", "py39"] + - ["3.10", "py310"] + - ["3.11", "py311"] + - ["3.12", "py312"] + - ["3.13", "py313"] + - ["3.11", "coverage"] runs-on: ${{ matrix.os[1] }} if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name @@ -34,21 +35,23 @@ jobs: steps: - name: "Configure PostgreSQL" run: | + sudo apt install postgresql sudo mkdir -p /usr/local/pgsql/data sudo chown postgres /usr/local/pgsql/data - sudo su - postgres -c '/usr/lib/postgresql/14/bin/initdb -D /usr/local/pgsql/data' + sudo su - postgres -c '/usr/lib/postgresql/16/bin/initdb -D /usr/local/pgsql/data' sudo su - postgres -c 'echo "max_prepared_transactions=10" >> /usr/local/pgsql/data/postgresql.conf' sudo su - postgres -c 'cat /usr/local/pgsql/data/postgresql.conf' - sudo su - postgres -c '/usr/lib/postgresql/14/bin/pg_ctl -D /usr/local/pgsql/data -l logfile start' - sudo su - postgres -c '/usr/lib/postgresql/14/bin/createdb zope_sqlalchemy_tests' - sudo su - postgres -c '/usr/lib/postgresql/14/bin/psql -l' - - uses: actions/checkout@v3 + sudo su - postgres -c '/usr/lib/postgresql/16/bin/pg_ctl -D /usr/local/pgsql/data -l logfile start' + sudo su - postgres -c '/usr/lib/postgresql/16/bin/createdb zope_sqlalchemy_tests' + sudo su - postgres -c '/usr/lib/postgresql/16/bin/psql -l' + - uses: actions/checkout@v4 - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.config[0] }} + allow-prereleases: true - name: Pip cache - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ~/.cache/pip key: ${{ runner.os }}-pip-${{ matrix.config[0] }}-${{ hashFiles('setup.*', 'tox.ini') }} @@ -60,8 +63,12 @@ jobs: python -m pip install --upgrade pip pip install tox - name: Test + if: ${{ !startsWith(runner.os, 'Mac') }} run: | tox -f ${{ matrix.config[1] }} + - name: Test (macOS) + if: ${{ startsWith(runner.os, 'Mac') }} + run: tox -e ${{ matrix.config[1] }}-universal2 - name: Coverage if: matrix.config[1] == 'coverage' run: | diff --git a/.meta.toml b/.meta.toml index 5d4a399..ab058da 100644 --- a/.meta.toml +++ b/.meta.toml @@ -2,7 +2,7 @@ # https://github.com/zopefoundation/meta/tree/master/config/zope-product [meta] template = "zope-product" -commit-id = "1dc6b9e7" +commit-id = "85622de1" [python] with-pypy = false @@ -10,15 +10,16 @@ with-sphinx-doctests = false with-windows = false with-future-python = false with-macos = false +with-docs = false [coverage] fail-under = 65 [tox] additional-envlist = [ - "py{37,38,39}-sqlalchemy11", - "py{37,38,39,310}-sqlalchemy{12,13}", - "py{37,38,39,310,311}-sqlalchemy{14,20}", + "py{38,39}-sqlalchemy11", + "py{38,39,310}-sqlalchemy{12,13}", + "py{38,39,310,311,312,313}-sqlalchemy{14,20}", ] testenv-deps = [ "sqlalchemy11: SQLAlchemy==1.1.*", diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..8d0156c --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,28 @@ +# Generated from: +# https://github.com/zopefoundation/meta/tree/master/config/zope-product +minimum_pre_commit_version: '3.6' +repos: + - repo: https://github.com/pycqa/isort + rev: "5.13.2" + hooks: + - id: isort + - repo: https://github.com/hhatto/autopep8 + rev: "v2.3.1" + hooks: + - id: autopep8 + args: [--in-place, --aggressive, --aggressive] + - repo: https://github.com/asottile/pyupgrade + rev: v3.17.0 + hooks: + - id: pyupgrade + args: [--py38-plus] + - repo: https://github.com/isidentical/teyit + rev: 0.4.3 + hooks: + - id: teyit + - repo: https://github.com/PyCQA/flake8 + rev: "7.1.1" + hooks: + - id: flake8 + additional_dependencies: + - flake8-debugger == 4.1.2 diff --git a/CHANGES.rst b/CHANGES.rst index 48a0a1a..fe1607e 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -4,6 +4,10 @@ Changes 3.2 (unreleased) ---------------- +- Add support for Python 3.12, 3.13. + +- Drop support for Python 3.7. + - SQLAlchemy's versions 2.0.32 up to 2.0.35 run into dead locks when running the tests on Python 3.11+, so excluding them from the list of supported versions. diff --git a/MANIFEST.in b/MANIFEST.in index 6c35d80..4ea108d 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -5,6 +5,7 @@ include *.rst include *.txt include buildout.cfg include tox.ini +include .pre-commit-config.yaml recursive-include src *.py include github_actions.cfg diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..edd83ca --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,22 @@ +# +# Generated from: +# https://github.com/zopefoundation/meta/tree/master/config/zope-product + +[build-system] +requires = ["setuptools<74"] +build-backend = "setuptools.build_meta" + +[tool.coverage] +[tool.coverage.run] +branch = true +source = ["zope.sqlalchemy"] + +[tool.coverage.report] +fail_under = 65 +precision = 2 +ignore_errors = true +show_missing = true +exclude_lines = ["pragma: no cover", "pragma: nocover", "except ImportError:", "raise NotImplementedError", "if __name__ == '__main__':", "self.fail", "raise AssertionError", "raise unittest.Skip"] + +[tool.coverage.html] +directory = "parts/htmlcov" diff --git a/setup.cfg b/setup.cfg index 66b1ca8..a7bfdb4 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,7 +1,5 @@ # Generated from: # https://github.com/zopefoundation/meta/tree/master/config/zope-product -[bdist_wheel] -universal = 0 [flake8] doctests = 1 @@ -17,7 +15,7 @@ ignore = force_single_line = True combine_as_imports = True sections = FUTURE,STDLIB,THIRDPARTY,ZOPE,FIRSTPARTY,LOCALFOLDER -known_third_party = six, docutils, pkg_resources, pytz +known_third_party = docutils, pkg_resources, pytz known_zope = known_first_party = default_section = ZOPE diff --git a/setup.py b/setup.py index 192365f..070a175 100644 --- a/setup.py +++ b/setup.py @@ -5,6 +5,34 @@ tests_require = ['zope.testing'] +sqlalchemy_versions = ','.join([ + '>=1.1', + '!=1.4.0', + '!=1.4.1', + '!=1.4.2', + '!=1.4.3', + '!=1.4.4', + '!=1.4.5', + '!=1.4.6', + '!=2.0.19', # Tests run into a deadlock on Python 3.12 from here on + '!=2.0.20', + '!=2.0.21', + '!=2.0.22', + '!=2.0.23', + '!=2.0.24', + '!=2.0.25', + '!=2.0.26', + '!=2.0.27', + '!=2.0.28', + '!=2.0.29', + '!=2.0.30', + '!=2.0.31', + '!=2.0.32', + '!=2.0.33', + '!=2.0.34', + '!=2.0.35', + '!=2.0.36', +]) setup( name='zope.sqlalchemy', @@ -16,14 +44,12 @@ namespace_packages=['zope'], test_suite='zope.sqlalchemy.tests.test_suite', author='Laurence Rowe', - author_email='laurence@lrowe.co.uk', url='https://github.com/zopefoundation/zope.sqlalchemy', description="Minimal Zope/SQLAlchemy transaction integration", long_description=( open(os.path.join('src', 'zope', 'sqlalchemy', 'README.rst')).read() + - "\n\n" + - open('CHANGES.rst').read()), + "\n\n" + open('CHANGES.rst').read()), license='ZPL 2.1', keywords='zope zope3 sqlalchemy', classifiers=[ @@ -35,20 +61,21 @@ "License :: OSI Approved :: Zope Public License", "Programming Language :: Python", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", "Programming Language :: Python :: Implementation :: CPython", "Topic :: Database", "Topic :: Software Development :: Libraries :: Python Modules", ], - python_requires='>=3.7', + python_requires='>=3.8', install_requires=[ 'packaging', 'setuptools', - 'SQLAlchemy>=1.1,!=1.4.0,!=1.4.1,!=1.4.2,!=1.4.3,!=1.4.4,!=1.4.5,!=1.4.6,!=2.0.32,!=2.0.33,!=2.0.34,!=2.0.35', # noqa: E501 line too long + f'SQLAlchemy{sqlalchemy_versions}', 'transaction>=1.6.0', 'zope.interface>=3.6.0', ], diff --git a/src/zope/sqlalchemy/tests.py b/src/zope/sqlalchemy/tests.py index 5083924..43ed276 100644 --- a/src/zope/sqlalchemy/tests.py +++ b/src/zope/sqlalchemy/tests.py @@ -265,7 +265,7 @@ def testMarkUnknownSession(self): DummyDataManager(key="dummy.first") session = Session() mark_changed(session) - self.assertTrue(session in zope.sqlalchemy.datamanager._SESSION_STATE) + self.assertIn(session, zope.sqlalchemy.datamanager._SESSION_STATE) def testAbortBeforeCommit(self): # Simulate what happens in a conflict error @@ -399,18 +399,27 @@ def testSavepoint(self): s1 = t.savepoint() session.add(User(id=1, firstname="udo", lastname="juergens")) session.flush() - self.assertTrue(len(query.all()) == 1, - "Users table should have one row") + self.assertEqual( + len(query.all()), + 1, + "Users table should have one row" + ) s2 = t.savepoint() session.add(User(id=2, firstname="heino", lastname="n/a")) session.flush() - self.assertTrue(len(query.all()) == 2, - "Users table should have two rows") + self.assertEqual( + len(query.all()), + 2, + "Users table should have two rows" + ) s2.rollback() - self.assertTrue(len(query.all()) == 1, - "Users table should have one row") + self.assertEqual( + len(query.all()), + 1, + "Users table should have one row" + ) s1.rollback() self.assertFalse(query.all(), "Users table should be empty") @@ -771,12 +780,16 @@ def testRetry(self): tm1, tm2, s1, s2 = self.tm1, self.tm2, self.s1, self.s2 # make sure we actually start a session. tm1.begin() - self.assertTrue( - len(s1.query(User).all()) == 1, "Users table should have one row" + self.assertEqual( + len(s1.query(User).all()), + 1, + "Users table should have one row" ) tm2.begin() - self.assertTrue( - len(s2.query(User).all()) == 1, "Users table should have one row" + self.assertEqual( + len(s2.query(User).all()), + 1, + "Users table should have one row" ) s1.query(User).delete() if SA_GE_20: @@ -800,15 +813,19 @@ def testRetryThread(self): tm1, tm2, s1, s2 = self.tm1, self.tm2, self.s1, self.s2 # make sure we actually start a session. tm1.begin() - self.assertTrue( - len(s1.query(User).all()) == 1, "Users table should have one row" + self.assertEqual( + len(s1.query(User).all()), + 1, + "Users table should have one row" ) tm2.begin() s2.connection().execute(sql.text( "SET TRANSACTION ISOLATION LEVEL SERIALIZABLE" )) - self.assertTrue( - len(s2.query(User).all()) == 1, "Users table should have one row" + self.assertEqual( + len(s2.query(User).all()), + 1, + "Users table should have one row" ) s1.query(User).delete() raised = False @@ -875,14 +892,14 @@ def tearDownReadMe(test): def test_suite(): import doctest from unittest import TestSuite - from unittest import makeSuite + from unittest import defaultTestLoader optionflags = doctest.NORMALIZE_WHITESPACE | doctest.ELLIPSIS suite = TestSuite() - suite.addTest(makeSuite(ZopeSQLAlchemyTests)) - suite.addTest(makeSuite(MultipleEngineTests)) + suite.addTest(defaultTestLoader.loadTestsFromTestCase(ZopeSQLAlchemyTests)) + suite.addTest(defaultTestLoader.loadTestsFromTestCase(MultipleEngineTests)) if TEST_DSN.startswith("postgres") or TEST_DSN.startswith("oracle"): - suite.addTest(makeSuite(RetryTests)) + suite.addTest(defaultTestLoader.loadTestsFromTestCase(RetryTests)) # examples in docs are only correct for SQLAlchemy >=1.4 if parse_version(sqlalchemy_version) >= parse_version('1.4.0'): diff --git a/tox.ini b/tox.ini index 3ab3475..91a610e 100644 --- a/tox.ini +++ b/tox.ini @@ -3,27 +3,31 @@ [tox] minversion = 3.18 envlist = + release-check lint - py37 py38 py39 py310 py311 + py312 + py313 coverage - py{37,38,39}-sqlalchemy11 - py{37,38,39,310}-sqlalchemy{12,13} - py{37,38,39,310,311}-sqlalchemy{14,20} + py{38,39}-sqlalchemy11 + py{38,39,310}-sqlalchemy{12,13} + py{38,39,310,311,312,313}-sqlalchemy{14,20} [testenv] skip_install = true deps = - zc.buildout >= 3.0.1 + setuptools <74 + zc.buildout >= 3.1 wheel > 0.37 sqlalchemy11: SQLAlchemy==1.1.* sqlalchemy12: SQLAlchemy==1.2.* sqlalchemy13: SQLAlchemy==1.3.* sqlalchemy14: SQLAlchemy==1.4.* sqlalchemy20: SQLAlchemy==2.0.* +setenv = commands_pre = !sqlalchemy20: sh -c 'if [ '{env:CI:false}' = 'true' ]; then {envbindir}/buildout -nc {toxinidir}/github_actions.cfg buildout:directory={envdir} buildout:develop={toxinidir} ; fi' !sqlalchemy20: sh -c 'if [ '{env:CI:false}' != 'true' ]; then {envbindir}/buildout -nc {toxinidir}/postgres.cfg buildout:directory={envdir} buildout:develop={toxinidir} ; fi' @@ -41,34 +45,46 @@ passenv = allowlist_externals = sh -[testenv:lint] +[testenv:setuptools-latest] basepython = python3 +deps = + git+https://github.com/pypa/setuptools.git\#egg=setuptools + zc.buildout >= 3.1 + wheel > 0.37 + sqlalchemy11: SQLAlchemy==1.1.* + sqlalchemy12: SQLAlchemy==1.2.* + sqlalchemy13: SQLAlchemy==1.3.* + sqlalchemy14: SQLAlchemy==1.4.* + sqlalchemy20: SQLAlchemy==2.0.* + + +[testenv:release-check] +description = ensure that the distribution is ready to release +basepython = python3 +skip_install = true +deps = + setuptools <74 + twine + build + check-manifest + check-python-versions >= 0.20.0 + wheel commands_pre = - mkdir -p {toxinidir}/parts/flake8 -allowlist_externals = - mkdir commands = - isort --check-only --diff {toxinidir}/src {toxinidir}/setup.py - flake8 {toxinidir}/src {toxinidir}/setup.py check-manifest - check-python-versions -deps = - check-manifest - check-python-versions - flake8 - isort - # Useful flake8 plugins that are Python and Plone specific: - flake8-coding - flake8-debugger - mccabe + check-python-versions --only setup.py,tox.ini,.github/workflows/tests.yml + python -m build --sdist --no-isolation + twine check dist/* -[testenv:isort-apply] +[testenv:lint] +description = This env runs all linters configured in .pre-commit-config.yaml basepython = python3 -commands_pre = +skip_install = true deps = - isort + pre-commit +commands_pre = commands = - isort {toxinidir}/src {toxinidir}/setup.py [] + pre-commit run --all-files --show-diff-on-failure [testenv:coverage] basepython = python3 @@ -78,27 +94,9 @@ allowlist_externals = mkdir deps = {[testenv]deps} - coverage + coverage[toml] commands = mkdir -p {toxinidir}/parts/htmlcov - coverage run {envdir}/bin/test {posargs:-cv} + coverage run {envbindir}/test {posargs:-cv} coverage html - coverage report -m --fail-under=65 - -[coverage:run] -branch = True -source = zope.sqlalchemy - -[coverage:report] -precision = 2 -exclude_lines = - pragma: no cover - pragma: nocover - except ImportError: - raise NotImplementedError - if __name__ == '__main__': - self.fail - raise AssertionError - -[coverage:html] -directory = parts/htmlcov + coverage report