From 7f80afc4002a1614dbb4beedf41a548507feeed3 Mon Sep 17 00:00:00 2001 From: Swen Kooij Date: Sun, 8 Jun 2025 20:51:23 +0200 Subject: [PATCH] Adjust dependencies to work with Python 3.6 all the way to Python 3.13 --- .circleci/config.yml | 30 +-- .gitignore | 1 + README.md | 16 +- .../migrations/patched_autodetector.py | 4 +- psqlextra/backend/schema.py | 6 +- psqlextra/models/view.py | 4 +- psqlextra/partitioning/plan.py | 2 +- psqlextra/query.py | 6 +- psqlextra/settings.py | 3 +- pyproject.toml | 60 ++++- requirements-all.txt | 4 +- requirements-test.txt | 3 + settings.py | 20 +- setup.cfg | 1 - setup.py | 217 +++++------------- ...ent_command_partition_auto_confirm[y].json | 1 + ...t_command_partition_auto_confirm[yes].json | 1 + ...mmand_partition_confirm_no[capital_n].json | 1 + ...mand_partition_confirm_no[capital_no].json | 1 + ...ement_command_partition_confirm_no[n].json | 1 + ...ment_command_partition_confirm_no[no].json | 1 + ...ommand_partition_confirm_no[title_no].json | 1 + ...mand_partition_confirm_yes[capital_y].json | 1 + ...nd_partition_confirm_yes[capital_yes].json | 1 + ...ment_command_partition_confirm_yes[y].json | 1 + ...nt_command_partition_confirm_yes[yes].json | 1 + ...nagement_command_partition_dry_run[d].json | 1 + ...gement_command_partition_dry_run[dry].json | 1 + tests/snapshots/__init__.py | 0 .../snap_test_management_command_partition.py | 34 --- tests/test_introspect.py | 106 ++++++--- tests/test_make_migrations.py | 4 +- tests/test_management_command_partition.py | 32 ++- tests/test_manager.py | 6 +- tests/test_on_conflict.py | 8 +- tox.ini | 3 +- 36 files changed, 293 insertions(+), 290 deletions(-) create mode 100644 requirements-test.txt create mode 100644 tests/__snapshots__/test_management_command_partition/test_management_command_partition_auto_confirm[y].json create mode 100644 tests/__snapshots__/test_management_command_partition/test_management_command_partition_auto_confirm[yes].json create mode 100644 tests/__snapshots__/test_management_command_partition/test_management_command_partition_confirm_no[capital_n].json create mode 100644 tests/__snapshots__/test_management_command_partition/test_management_command_partition_confirm_no[capital_no].json create mode 100644 tests/__snapshots__/test_management_command_partition/test_management_command_partition_confirm_no[n].json create mode 100644 tests/__snapshots__/test_management_command_partition/test_management_command_partition_confirm_no[no].json create mode 100644 tests/__snapshots__/test_management_command_partition/test_management_command_partition_confirm_no[title_no].json create mode 100644 tests/__snapshots__/test_management_command_partition/test_management_command_partition_confirm_yes[capital_y].json create mode 100644 tests/__snapshots__/test_management_command_partition/test_management_command_partition_confirm_yes[capital_yes].json create mode 100644 tests/__snapshots__/test_management_command_partition/test_management_command_partition_confirm_yes[y].json create mode 100644 tests/__snapshots__/test_management_command_partition/test_management_command_partition_confirm_yes[yes].json create mode 100644 tests/__snapshots__/test_management_command_partition/test_management_command_partition_dry_run[d].json create mode 100644 tests/__snapshots__/test_management_command_partition/test_management_command_partition_dry_run[dry].json delete mode 100644 tests/snapshots/__init__.py delete mode 100644 tests/snapshots/snap_test_management_command_partition.py diff --git a/.circleci/config.yml b/.circleci/config.yml index 6e3b4e81..a00cc61e 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -55,7 +55,7 @@ jobs: steps: - checkout - install-dependencies: - extra: test + extra: dev, test - run-tests: pyversion: 36 @@ -67,7 +67,7 @@ jobs: steps: - checkout - install-dependencies: - extra: test + extra: dev, test - run-tests: pyversion: 37 @@ -79,7 +79,7 @@ jobs: steps: - checkout - install-dependencies: - extra: test + extra: dev, test - run-tests: pyversion: 38 @@ -91,7 +91,7 @@ jobs: steps: - checkout - install-dependencies: - extra: test + extra: dev, test - run-tests: pyversion: 39 @@ -103,7 +103,7 @@ jobs: steps: - checkout - install-dependencies: - extra: test + extra: dev, test - run-tests: pyversion: 310 @@ -115,9 +115,14 @@ jobs: steps: - checkout - install-dependencies: - extra: test + extra: dev, test, test-report - run-tests: pyversion: 311 + - store_test_results: + path: reports + - run: + name: Upload coverage report + command: coveralls test-python312: executor: @@ -128,7 +133,7 @@ jobs: steps: - checkout - install-dependencies: - extra: test + extra: dev, test - run-tests: pyversion: 312 @@ -141,14 +146,9 @@ jobs: steps: - checkout - install-dependencies: - extra: test + extra: dev, test - run-tests: pyversion: 313 - - store_test_results: - path: reports - - run: - name: Upload coverage report - command: coveralls analysis: executor: @@ -157,10 +157,10 @@ jobs: steps: - checkout - install-dependencies: - extra: analysis, test + extra: dev, analysis, test - run: name: Verify - command: python setup.py verify + command: poe verify publish: executor: diff --git a/.gitignore b/.gitignore index 97ebaa67..63d6378d 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,7 @@ reports/ *.egg-info/ pip-wheel-metadata/ dist/ +build/ # Ignore stupid .DS_Store .DS_Store diff --git a/README.md b/README.md index eeab68d8..a26731de 100644 --- a/README.md +++ b/README.md @@ -59,9 +59,11 @@ With seamless we mean that any features we add will work truly seamlessly. You s ## Working with the code ### Prerequisites -* PostgreSQL 10 or newer. -* Django 2.0 or newer (including 3.x, 4.x). -* Python 3.6 or newer. +* PostgreSQL 14 or newer. +* Django 5.x or newer. +* Python 3.11 or newer. + +These are just for local development. CI for code analysis etc runs against these. Tests will pass on all Python, Django and PostgreSQL versions documented. Linting, formatting and type-checking the code might not work on other Python and/or Django versions. ### Getting started @@ -86,16 +88,16 @@ With seamless we mean that any features we add will work truly seamlessly. You s 4. Install the development/test dependencies: - λ pip install .[test] .[analysis] + λ pip install -r requirements-test.txt 5. Run the tests: - λ tox + λ poe test 6. Run the benchmarks: - λ py.test -c pytest-benchmark.ini + λ poe benchmark 7. Auto-format code, sort imports and auto-fix linting errors: - λ python setup.py fix + λ poe fix diff --git a/psqlextra/backend/migrations/patched_autodetector.py b/psqlextra/backend/migrations/patched_autodetector.py index e5ba8938..07f9e528 100644 --- a/psqlextra/backend/migrations/patched_autodetector.py +++ b/psqlextra/backend/migrations/patched_autodetector.py @@ -37,8 +37,8 @@ class AddOperationHandler: """Handler for when operations are being added to a new migration. - This is where we intercept operations such as - :see:CreateModel to replace it with our own. + This is where we intercept operations such as :see:CreateModel to + replace it with our own. """ def __init__(self, autodetector, app_label, args, kwargs): diff --git a/psqlextra/backend/schema.py b/psqlextra/backend/schema.py index c7253549..22acc075 100644 --- a/psqlextra/backend/schema.py +++ b/psqlextra/backend/schema.py @@ -558,9 +558,9 @@ def replace_materialized_view_model(self, model: Type[Model]) -> None: This is used to alter the backing query of a materialized view. - Replacing a materialized view is a lot trickier than a normal view. - For normal views we can use `CREATE OR REPLACE VIEW`, but for - materialized views, we have to create the new view, copy all + Replacing a materialized view is a lot trickier than a normal + view. For normal views we can use `CREATE OR REPLACE VIEW`, but + for materialized views, we have to create the new view, copy all indexes and constraints and drop the old one. This operation is atomic as it runs in a transaction. diff --git a/psqlextra/models/view.py b/psqlextra/models/view.py index b19f88c8..d24ed5a0 100644 --- a/psqlextra/models/view.py +++ b/psqlextra/models/view.py @@ -54,8 +54,8 @@ def _view_query_as_sql_with_params( When copying the meta options from the model, we convert any from the above to a raw SQL query with bind parameters. We do - this is because it is what the SQL driver understands and - we can easily serialize it into a migration. + this is because it is what the SQL driver understands and we can + easily serialize it into a migration. """ # might be a callable to support delayed imports diff --git a/psqlextra/partitioning/plan.py b/psqlextra/partitioning/plan.py index 3fcac44d..301b4241 100644 --- a/psqlextra/partitioning/plan.py +++ b/psqlextra/partitioning/plan.py @@ -54,7 +54,7 @@ def apply(self, using: Optional[str]) -> None: def print(self) -> None: """Prints this model plan to the terminal in a readable format.""" - print(f"{self.config.model.__name__}:") + print(f"{self.config.model.__name__}: ") for partition in self.deletions: print(" - %s" % partition.name()) diff --git a/psqlextra/query.py b/psqlextra/query.py index 5dd1cdb3..ca1d2226 100644 --- a/psqlextra/query.py +++ b/psqlextra/query.py @@ -73,9 +73,9 @@ def annotate(self, **annotations) -> "Self": # type: ignore[valid-type, overrid name of an existing field on the model as the alias name. This version of the function does allow that. - This is done by temporarily renaming the fields in order to avoid the - check for conflicts that the base class does. - We rename all fields instead of the ones that already exist because + This is done by temporarily renaming the fields in order to + avoid the check for conflicts that the base class does. We + rename all fields instead of the ones that already exist because the annotations are stored in an OrderedDict. Renaming only the conflicts will mess up the order. """ diff --git a/psqlextra/settings.py b/psqlextra/settings.py index 6f75c779..b6061766 100644 --- a/psqlextra/settings.py +++ b/psqlextra/settings.py @@ -16,7 +16,8 @@ def postgres_set_local( The effect is undone when the context manager exits. - See https://www.postgresql.org/docs/current/runtime-config-client.html + See + https://www.postgresql.org/docs/current/runtime-config-client.html for an overview of all available options. """ diff --git a/pyproject.toml b/pyproject.toml index fb35b3b4..cb27ce10 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ exclude = ''' | .env | env | venv - | tests/snapshots + | tests/__snapshots__ )/ ) ''' @@ -25,3 +25,61 @@ ignore_missing_imports = true [tool.django-stubs] django_settings_module = "settings" + +[tool.poe.tasks] +_autoflake = "python3 -m autoflake --remove-all -i -r setup.py psqlextra tests" +_autopep8 = "autopep8 -i -r setup.py psqlextra tests" +_isort_setup_py = "isort setup.py" +_isort_psqlextra = "isort psqlextra" +_isort_tests = "isort tests" +_isort_verify_setup_py = "isort -c setup.py" +_isort_verify_psqlextra = "isort -c psqlextra" +_isort_verify_tests = "isort -c tests" + +[tool.poe.tasks.lint] +cmd = "python3 -m flake8 --builtin=__version__ setup.py psqlextra tests" +help = "Lints all the code." + +[tool.poe.tasks.lint_fix] +sequence = ["_autoflake", "_autopep8"] +help = "Auto-fixes linter errors." + +[tool.poe.tasks.lint_types] +cmd = "mypy --package psqlextra --pretty --show-error-codes" +help = "Type-checks the code." + +[tool.poe.tasks.format] +cmd = "black setup.py psqlextra tests" +help = "Auto-formats the code." + +[tool.poe.tasks.format_verify] +cmd = "black --check setup.py psqlextra tests" +help = "Verifies that the code was formatted properly." + +[tool.poe.tasks.format_docstrings] +cmd = "docformatter -r -i ." +help = "Auto-formats doc strings." + +[tool.poe.tasks.format_docstrings_verify] +cmd = "docformatter -r -c ." +help = "Verifies all doc strings are properly formatted." + +[tool.poe.tasks.sort_imports] +sequence = ["_isort_setup_py", "_isort_psqlextra", "_isort_tests"] +help = "Auto-sorts the imports." + +[tool.poe.tasks.sort_imports_verify] +sequence = ["_isort_verify_setup_py", "_isort_verify_psqlextra", "_isort_verify_tests"] +help = "Verifies that the imports are properly sorted." + +[tool.poe.tasks.fix] +sequence = ["format", "format_docstrings", "sort_imports", "lint_fix", "lint", "lint_types"] +help = "Automatically format code and fix linting errors." + +[tool.poe.tasks.verify] +sequence = ["format_verify", "format_docstrings_verify", "sort_imports_verify", "lint", "lint_types"] +help = "Automatically format code and fix linting errors." + +[tool.poe.tasks.test] +cmd = "pytest --cov=psqlextra --cov-report=term --cov-report=xml:reports/xml --cov-report=html:reports/html --junitxml=reports/junit/tests.xml --reuse-db -vv" +help = "Runs all the tests." diff --git a/requirements-all.txt b/requirements-all.txt index 8b6a1b6c..c7ae18d2 100644 --- a/requirements-all.txt +++ b/requirements-all.txt @@ -1,4 +1,6 @@ -e . --e .[local] +-e .[dev] +-e .[test] +-e .[test-report] -e .[analysis] -e .[docs] diff --git a/requirements-test.txt b/requirements-test.txt new file mode 100644 index 00000000..bd31d78f --- /dev/null +++ b/requirements-test.txt @@ -0,0 +1,3 @@ +-e . +-e .[dev] +-e .[test] diff --git a/settings.py b/settings.py index 7266ccb4..7ece1712 100644 --- a/settings.py +++ b/settings.py @@ -1,4 +1,20 @@ -import dj_database_url +import os + +from urllib.parse import urlparse + + +def _parse_db_url(url: str): + parsed_url = urlparse(url) + + return { + 'ENGINE': 'django.db.backends.postgresql', + 'NAME': (parsed_url.path or '').strip('/') or "postgres", + 'HOST': parsed_url.hostname or None, + 'PORT': parsed_url.port or None, + 'USER': parsed_url.username or None, + 'PASSWORD': parsed_url.password or None, + } + DEBUG = True TEMPLATE_DEBUG = True @@ -8,7 +24,7 @@ TEST_RUNNER = 'django.test.runner.DiscoverRunner' DATABASES = { - 'default': dj_database_url.config(default='postgres:///psqlextra'), + 'default': _parse_db_url(os.environ.get('DATABASE_URL', 'postgres:///psqlextra')), } DATABASES['default']['ENGINE'] = 'tests.psqlextra_test_backend' diff --git a/setup.cfg b/setup.cfg index 65713eaa..ecb84153 100644 --- a/setup.cfg +++ b/setup.cfg @@ -9,4 +9,3 @@ lines_between_types=1 include_trailing_comma=True known_third_party=pytest,freezegun float_to_top=true -skip_glob=tests/snapshots/*.py diff --git a/setup.py b/setup.py index 8aa2ccb9..047d3613 100644 --- a/setup.py +++ b/setup.py @@ -1,35 +1,9 @@ -import distutils.cmd import os -import subprocess from setuptools import find_packages, setup exec(open("psqlextra/_version.py").read()) - -class BaseCommand(distutils.cmd.Command): - user_options = [] - - def initialize_options(self): - pass - - def finalize_options(self): - pass - - -def create_command(text, commands): - """Creates a custom setup.py command.""" - - class CustomCommand(BaseCommand): - description = text - - def run(self): - for cmd in commands: - subprocess.check_call(cmd) - - return CustomCommand - - with open( os.path.join(os.path.dirname(__file__), "README.md"), encoding="utf-8" ) as readme: @@ -38,7 +12,7 @@ def run(self): setup( name="django-postgres-extra", - version=__version__, + version=__version__, # noqa packages=find_packages(exclude=["tests"]), package_data={"psqlextra": ["py.typed"]}, include_package_data=True, @@ -49,7 +23,16 @@ def run(self): url="https://github.com/SectorLabs/django-postgres-extra", author="Sector Labs", author_email="open-source@sectorlabs.ro", - keywords=["django", "postgres", "extra", "hstore", "ltree"], + keywords=[ + "django", + "postgres", + "extra", + "hstore", + "upsert", + "partioning", + "materialized", + "view", + ], classifiers=[ "Environment :: Web Environment", "Framework :: Django", @@ -72,154 +55,62 @@ def run(self): "python-dateutil>=2.8.0,<=3.0.0", ], extras_require={ + # Python 3.6 - Python 3.13 ':python_version <= "3.6"': ["dataclasses"], - "docs": [ - "Sphinx==8.2.3", - "sphinx-rtd-theme==3.0.2", - "docutils==0.21.2", - "Jinja2==3.1.6", + "dev": [ + "poethepoet==0.34.0; python_version >= '3.9'", + "poethepoet==0.30.0; python_version >= '3.8' and python_version < '3.9'", + "poethepoet==0.19.0; python_version >= '3.7' and python_version < '3.8'", + "poethepoet==0.13.1; python_version >= '3.6' and python_version < '3.7'", ], "test": [ "psycopg2==2.9.10; python_version >= '3.8'", "psycopg2==2.9.9; python_version >= '3.7' and python_version < '3.8'", "psycopg2==2.9.8; python_version >= '3.6' and python_version < '3.7'", - "dj-database-url==0.5.0", - "pytest==6.2.5", - "pytest-benchmark==3.4.1", - "pytest-django==4.4.0", - "pytest-cov==3.0.0", - "pytest-lazy-fixture==0.6.3", - "pytest-freezegun==0.4.2", - "tox==3.24.4", - "freezegun==1.1.0", - "coveralls==3.3.0", - "snapshottest==0.6.0", + "types-psycopg2==2.9.21.20250516; python_version >= '3.9'", + "types-psycopg2==2.9.8; python_version >= '3.6' and python_version < '3.9'", + "pytest==8.4.0; python_version > '3.8'", + "pytest==7.0.1; python_version <= '3.8'", + "pytest-benchmark==5.1.0; python_version > '3.8'", + "pytest-benchmark==3.4.1; python_version <= '3.8'", + "pytest-django==4.11.1; python_version > '3.7'", + "pytest-django==4.5.2; python_version <= '3.7'", + "pytest-cov==6.1.1; python_version > '3.8'", + "pytest-cov==4.0.0; python_version <= '3.8'", + "coverage==7.8.2; python_version > '3.8'", + "coverage==7.6.1; python_version >= '3.8' and python_version <= '3.8'", + "coverage==6.2; python_version <= '3.7'", + "tox==4.26.0; python_version > '3.8'", + "tox==3.28.0; python_version <= '3.8'", + "freezegun==1.5.2; python_version > '3.7'", + "freezegun==1.2.2; python_version <= '3.7'", + "syrupy==4.9.1; python_version >= '3.9'", + "syrupy==2.3.1; python_version <= '3.8'", ], + # Python 3.11 assumed from below + "test-report": ["coveralls==4.0.1"], "analysis": [ "black==22.3.0", - "flake8==4.0.1", - "autoflake==1.4", - "autopep8==1.6.0", - "isort==5.10.0", - "docformatter==1.4", - "mypy==1.2.0; python_version > '3.6'", - "mypy==0.971; python_version <= '3.6'", - "django-stubs==4.2.7; python_version > '3.6'", - "django-stubs==1.9.0; python_version <= '3.6'", - "typing-extensions==4.5.0; python_version > '3.6'", - "typing-extensions==4.1.0; python_version <= '3.6'", - "types-dj-database-url==1.3.0.0", - "types-python-dateutil==2.8.19.12", + "flake8==7.2.0", + "autoflake==2.3.1", + "autopep8==2.3.2", + "isort==6.0.1", + "docformatter==1.7.7", + "mypy==1.16.0", + "django-stubs==4.2.7", + "typing-extensions==4.14.0", + "types-dj-database-url==1.3.0.4", + "types-python-dateutil==2.9.0.20250516", + ], + "docs": [ + "Sphinx==8.2.3", + "sphinx-rtd-theme==3.0.2", + "docutils==0.21.2", + "Jinja2==3.1.6", ], "publish": [ "build==0.7.0", "twine==3.7.1", ], }, - cmdclass={ - "lint": create_command( - "Lints the code", - [ - [ - "flake8", - "--builtin=__version__", - "setup.py", - "psqlextra", - "tests", - ] - ], - ), - "lint_fix": create_command( - "Lints the code", - [ - [ - "autoflake", - "--remove-all", - "-i", - "-r", - "setup.py", - "psqlextra", - "tests", - ], - ["autopep8", "-i", "-r", "setup.py", "psqlextra", "tests"], - ], - ), - "lint_types": create_command( - "Type-checks the code", - [ - [ - "mypy", - "--package", - "psqlextra", - "--pretty", - "--show-error-codes", - ], - ], - ), - "format": create_command( - "Formats the code", [["black", "setup.py", "psqlextra", "tests"]] - ), - "format_verify": create_command( - "Checks if the code is auto-formatted", - [["black", "--check", "setup.py", "psqlextra", "tests"]], - ), - "format_docstrings": create_command( - "Auto-formats doc strings", [["docformatter", "-r", "-i", "."]] - ), - "format_docstrings_verify": create_command( - "Verifies that doc strings are properly formatted", - [["docformatter", "-r", "-c", "."]], - ), - "sort_imports": create_command( - "Automatically sorts imports", - [ - ["isort", "setup.py"], - ["isort", "psqlextra"], - ["isort", "tests"], - ], - ), - "sort_imports_verify": create_command( - "Verifies all imports are properly sorted.", - [ - ["isort", "-c", "setup.py"], - ["isort", "-c", "psqlextra"], - ["isort", "-c", "tests"], - ], - ), - "fix": create_command( - "Automatically format code and fix linting errors", - [ - ["python", "setup.py", "format"], - ["python", "setup.py", "format_docstrings"], - ["python", "setup.py", "sort_imports"], - ["python", "setup.py", "lint_fix"], - ["python", "setup.py", "lint"], - ["python", "setup.py", "lint_types"], - ], - ), - "verify": create_command( - "Verifies whether the code is auto-formatted and has no linting errors", - [ - ["python", "setup.py", "format_verify"], - ["python", "setup.py", "format_docstrings_verify"], - ["python", "setup.py", "sort_imports_verify"], - ["python", "setup.py", "lint"], - ["python", "setup.py", "lint_types"], - ], - ), - "test": create_command( - "Runs all the tests", - [ - [ - "pytest", - "--cov=psqlextra", - "--cov-report=term", - "--cov-report=xml:reports/xml", - "--cov-report=html:reports/html", - "--junitxml=reports/junit/tests.xml", - "--reuse-db", - ] - ], - ), - }, ) diff --git a/tests/__snapshots__/test_management_command_partition/test_management_command_partition_auto_confirm[y].json b/tests/__snapshots__/test_management_command_partition/test_management_command_partition_auto_confirm[y].json new file mode 100644 index 00000000..664538ac --- /dev/null +++ b/tests/__snapshots__/test_management_command_partition/test_management_command_partition_auto_confirm[y].json @@ -0,0 +1 @@ +"test: \n - tobedeleted\n + tobecreated\n\n1 partitions will be deleted\n1 partitions will be created\nOperations applied.\n" diff --git a/tests/__snapshots__/test_management_command_partition/test_management_command_partition_auto_confirm[yes].json b/tests/__snapshots__/test_management_command_partition/test_management_command_partition_auto_confirm[yes].json new file mode 100644 index 00000000..664538ac --- /dev/null +++ b/tests/__snapshots__/test_management_command_partition/test_management_command_partition_auto_confirm[yes].json @@ -0,0 +1 @@ +"test: \n - tobedeleted\n + tobecreated\n\n1 partitions will be deleted\n1 partitions will be created\nOperations applied.\n" diff --git a/tests/__snapshots__/test_management_command_partition/test_management_command_partition_confirm_no[capital_n].json b/tests/__snapshots__/test_management_command_partition/test_management_command_partition_confirm_no[capital_n].json new file mode 100644 index 00000000..f1c2aa68 --- /dev/null +++ b/tests/__snapshots__/test_management_command_partition/test_management_command_partition_confirm_no[capital_n].json @@ -0,0 +1 @@ +"test: \n - tobedeleted\n + tobecreated\n\n1 partitions will be deleted\n1 partitions will be created\nDo you want to proceed? (y/N) Operation aborted.\n" diff --git a/tests/__snapshots__/test_management_command_partition/test_management_command_partition_confirm_no[capital_no].json b/tests/__snapshots__/test_management_command_partition/test_management_command_partition_confirm_no[capital_no].json new file mode 100644 index 00000000..f1c2aa68 --- /dev/null +++ b/tests/__snapshots__/test_management_command_partition/test_management_command_partition_confirm_no[capital_no].json @@ -0,0 +1 @@ +"test: \n - tobedeleted\n + tobecreated\n\n1 partitions will be deleted\n1 partitions will be created\nDo you want to proceed? (y/N) Operation aborted.\n" diff --git a/tests/__snapshots__/test_management_command_partition/test_management_command_partition_confirm_no[n].json b/tests/__snapshots__/test_management_command_partition/test_management_command_partition_confirm_no[n].json new file mode 100644 index 00000000..f1c2aa68 --- /dev/null +++ b/tests/__snapshots__/test_management_command_partition/test_management_command_partition_confirm_no[n].json @@ -0,0 +1 @@ +"test: \n - tobedeleted\n + tobecreated\n\n1 partitions will be deleted\n1 partitions will be created\nDo you want to proceed? (y/N) Operation aborted.\n" diff --git a/tests/__snapshots__/test_management_command_partition/test_management_command_partition_confirm_no[no].json b/tests/__snapshots__/test_management_command_partition/test_management_command_partition_confirm_no[no].json new file mode 100644 index 00000000..f1c2aa68 --- /dev/null +++ b/tests/__snapshots__/test_management_command_partition/test_management_command_partition_confirm_no[no].json @@ -0,0 +1 @@ +"test: \n - tobedeleted\n + tobecreated\n\n1 partitions will be deleted\n1 partitions will be created\nDo you want to proceed? (y/N) Operation aborted.\n" diff --git a/tests/__snapshots__/test_management_command_partition/test_management_command_partition_confirm_no[title_no].json b/tests/__snapshots__/test_management_command_partition/test_management_command_partition_confirm_no[title_no].json new file mode 100644 index 00000000..f1c2aa68 --- /dev/null +++ b/tests/__snapshots__/test_management_command_partition/test_management_command_partition_confirm_no[title_no].json @@ -0,0 +1 @@ +"test: \n - tobedeleted\n + tobecreated\n\n1 partitions will be deleted\n1 partitions will be created\nDo you want to proceed? (y/N) Operation aborted.\n" diff --git a/tests/__snapshots__/test_management_command_partition/test_management_command_partition_confirm_yes[capital_y].json b/tests/__snapshots__/test_management_command_partition/test_management_command_partition_confirm_yes[capital_y].json new file mode 100644 index 00000000..530f6bdb --- /dev/null +++ b/tests/__snapshots__/test_management_command_partition/test_management_command_partition_confirm_yes[capital_y].json @@ -0,0 +1 @@ +"test: \n - tobedeleted\n + tobecreated\n\n1 partitions will be deleted\n1 partitions will be created\nDo you want to proceed? (y/N) Operations applied.\n" diff --git a/tests/__snapshots__/test_management_command_partition/test_management_command_partition_confirm_yes[capital_yes].json b/tests/__snapshots__/test_management_command_partition/test_management_command_partition_confirm_yes[capital_yes].json new file mode 100644 index 00000000..530f6bdb --- /dev/null +++ b/tests/__snapshots__/test_management_command_partition/test_management_command_partition_confirm_yes[capital_yes].json @@ -0,0 +1 @@ +"test: \n - tobedeleted\n + tobecreated\n\n1 partitions will be deleted\n1 partitions will be created\nDo you want to proceed? (y/N) Operations applied.\n" diff --git a/tests/__snapshots__/test_management_command_partition/test_management_command_partition_confirm_yes[y].json b/tests/__snapshots__/test_management_command_partition/test_management_command_partition_confirm_yes[y].json new file mode 100644 index 00000000..530f6bdb --- /dev/null +++ b/tests/__snapshots__/test_management_command_partition/test_management_command_partition_confirm_yes[y].json @@ -0,0 +1 @@ +"test: \n - tobedeleted\n + tobecreated\n\n1 partitions will be deleted\n1 partitions will be created\nDo you want to proceed? (y/N) Operations applied.\n" diff --git a/tests/__snapshots__/test_management_command_partition/test_management_command_partition_confirm_yes[yes].json b/tests/__snapshots__/test_management_command_partition/test_management_command_partition_confirm_yes[yes].json new file mode 100644 index 00000000..530f6bdb --- /dev/null +++ b/tests/__snapshots__/test_management_command_partition/test_management_command_partition_confirm_yes[yes].json @@ -0,0 +1 @@ +"test: \n - tobedeleted\n + tobecreated\n\n1 partitions will be deleted\n1 partitions will be created\nDo you want to proceed? (y/N) Operations applied.\n" diff --git a/tests/__snapshots__/test_management_command_partition/test_management_command_partition_dry_run[d].json b/tests/__snapshots__/test_management_command_partition/test_management_command_partition_dry_run[d].json new file mode 100644 index 00000000..6b67fa96 --- /dev/null +++ b/tests/__snapshots__/test_management_command_partition/test_management_command_partition_dry_run[d].json @@ -0,0 +1 @@ +"test: \n - tobedeleted\n + tobecreated\n\n1 partitions will be deleted\n1 partitions will be created\n" diff --git a/tests/__snapshots__/test_management_command_partition/test_management_command_partition_dry_run[dry].json b/tests/__snapshots__/test_management_command_partition/test_management_command_partition_dry_run[dry].json new file mode 100644 index 00000000..6b67fa96 --- /dev/null +++ b/tests/__snapshots__/test_management_command_partition/test_management_command_partition_dry_run[dry].json @@ -0,0 +1 @@ +"test: \n - tobedeleted\n + tobecreated\n\n1 partitions will be deleted\n1 partitions will be created\n" diff --git a/tests/snapshots/__init__.py b/tests/snapshots/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/tests/snapshots/snap_test_management_command_partition.py b/tests/snapshots/snap_test_management_command_partition.py deleted file mode 100644 index 1cac2227..00000000 --- a/tests/snapshots/snap_test_management_command_partition.py +++ /dev/null @@ -1,34 +0,0 @@ -# -*- coding: utf-8 -*- -# snapshottest: v1 - https://goo.gl/zC4yUc -from __future__ import unicode_literals - -from snapshottest import GenericRepr, Snapshot - - -snapshots = Snapshot() - -snapshots['test_management_command_partition_auto_confirm[--yes] 1'] = GenericRepr("CaptureResult(out='test:\\n - tobedeleted\\n + tobecreated\\n\\n1 partitions will be deleted\\n1 partitions will be created\\nOperations applied.\\n', err='')") - -snapshots['test_management_command_partition_auto_confirm[-y] 1'] = GenericRepr("CaptureResult(out='test:\\n - tobedeleted\\n + tobecreated\\n\\n1 partitions will be deleted\\n1 partitions will be created\\nOperations applied.\\n', err='')") - -snapshots['test_management_command_partition_confirm_no[NO] 1'] = GenericRepr("CaptureResult(out='test:\\n - tobedeleted\\n + tobecreated\\n\\n1 partitions will be deleted\\n1 partitions will be created\\nDo you want to proceed? (y/N) Operation aborted.\\n', err='')") - -snapshots['test_management_command_partition_confirm_no[N] 1'] = GenericRepr("CaptureResult(out='test:\\n - tobedeleted\\n + tobecreated\\n\\n1 partitions will be deleted\\n1 partitions will be created\\nDo you want to proceed? (y/N) Operation aborted.\\n', err='')") - -snapshots['test_management_command_partition_confirm_no[No] 1'] = GenericRepr("CaptureResult(out='test:\\n - tobedeleted\\n + tobecreated\\n\\n1 partitions will be deleted\\n1 partitions will be created\\nDo you want to proceed? (y/N) Operation aborted.\\n', err='')") - -snapshots['test_management_command_partition_confirm_no[n] 1'] = GenericRepr("CaptureResult(out='test:\\n - tobedeleted\\n + tobecreated\\n\\n1 partitions will be deleted\\n1 partitions will be created\\nDo you want to proceed? (y/N) Operation aborted.\\n', err='')") - -snapshots['test_management_command_partition_confirm_no[no] 1'] = GenericRepr("CaptureResult(out='test:\\n - tobedeleted\\n + tobecreated\\n\\n1 partitions will be deleted\\n1 partitions will be created\\nDo you want to proceed? (y/N) Operation aborted.\\n', err='')") - -snapshots['test_management_command_partition_confirm_yes[YES] 1'] = GenericRepr("CaptureResult(out='test:\\n - tobedeleted\\n + tobecreated\\n\\n1 partitions will be deleted\\n1 partitions will be created\\nDo you want to proceed? (y/N) Operations applied.\\n', err='')") - -snapshots['test_management_command_partition_confirm_yes[Y] 1'] = GenericRepr("CaptureResult(out='test:\\n - tobedeleted\\n + tobecreated\\n\\n1 partitions will be deleted\\n1 partitions will be created\\nDo you want to proceed? (y/N) Operations applied.\\n', err='')") - -snapshots['test_management_command_partition_confirm_yes[y] 1'] = GenericRepr("CaptureResult(out='test:\\n - tobedeleted\\n + tobecreated\\n\\n1 partitions will be deleted\\n1 partitions will be created\\nDo you want to proceed? (y/N) Operations applied.\\n', err='')") - -snapshots['test_management_command_partition_confirm_yes[yes] 1'] = GenericRepr("CaptureResult(out='test:\\n - tobedeleted\\n + tobecreated\\n\\n1 partitions will be deleted\\n1 partitions will be created\\nDo you want to proceed? (y/N) Operations applied.\\n', err='')") - -snapshots['test_management_command_partition_dry_run[--dry] 1'] = GenericRepr("CaptureResult(out='test:\\n - tobedeleted\\n + tobecreated\\n\\n1 partitions will be deleted\\n1 partitions will be created\\n', err='')") - -snapshots['test_management_command_partition_dry_run[-d] 1'] = GenericRepr("CaptureResult(out='test:\\n - tobedeleted\\n + tobecreated\\n\\n1 partitions will be deleted\\n1 partitions will be created\\n', err='')") diff --git a/tests/test_introspect.py b/tests/test_introspect.py index 5e5a9ffc..bf50d3f1 100644 --- a/tests/test_introspect.py +++ b/tests/test_introspect.py @@ -1,4 +1,5 @@ import django +import freezegun import pytest from django.contrib.postgres.fields import ArrayField @@ -51,13 +52,14 @@ def mocked_model_foreign_keys( @pytest.fixture -def mocked_model_varying_fields_instance(freezer, mocked_model_varying_fields): - return mocked_model_varying_fields.objects.create( - title="hello world", - updated_at=timezone.now(), - content={"a": 1}, - items=["a", "b"], - ) +def mocked_model_varying_fields_instance(mocked_model_varying_fields): + with freezegun.freeze_time("2020-1-1 12:00:00.0"): + return mocked_model_varying_fields.objects.create( + title="hello world", + updated_at=timezone.now(), + content={"a": 1}, + items=["a", "b"], + ) @pytest.fixture @@ -78,17 +80,22 @@ def models_from_cursor_wrapper_single(): reason=django_31_skip_reason, ) @pytest.mark.parametrize( - "models_from_cursor_wrapper", + "models_from_cursor_wrapper_name", [ - pytest.lazy_fixture("models_from_cursor_wrapper_multiple"), - pytest.lazy_fixture("models_from_cursor_wrapper_single"), + "models_from_cursor_wrapper_multiple", + "models_from_cursor_wrapper_single", ], ) def test_models_from_cursor_applies_converters( + request, mocked_model_varying_fields, mocked_model_varying_fields_instance, - models_from_cursor_wrapper, + models_from_cursor_wrapper_name, ): + models_from_cursor_wrapper = request.getfixturevalue( + models_from_cursor_wrapper_name + ) + with connection.cursor() as cursor: cursor.execute( *mocked_model_varying_fields.objects.all().query.sql_with_params() @@ -114,17 +121,22 @@ def test_models_from_cursor_applies_converters( reason=django_31_skip_reason, ) @pytest.mark.parametrize( - "models_from_cursor_wrapper", + "models_from_cursor_wrapper_name", [ - pytest.lazy_fixture("models_from_cursor_wrapper_multiple"), - pytest.lazy_fixture("models_from_cursor_wrapper_single"), + "models_from_cursor_wrapper_multiple", + "models_from_cursor_wrapper_single", ], ) def test_models_from_cursor_handles_field_order( + request, mocked_model_varying_fields, mocked_model_varying_fields_instance, - models_from_cursor_wrapper, + models_from_cursor_wrapper_name, ): + models_from_cursor_wrapper = request.getfixturevalue( + models_from_cursor_wrapper_name + ) + with connection.cursor() as cursor: cursor.execute( f'SELECT content, items, id, title, updated_at FROM "{mocked_model_varying_fields._meta.db_table}"', @@ -151,17 +163,22 @@ def test_models_from_cursor_handles_field_order( reason=django_31_skip_reason, ) @pytest.mark.parametrize( - "models_from_cursor_wrapper", + "models_from_cursor_wrapper_name", [ - pytest.lazy_fixture("models_from_cursor_wrapper_multiple"), - pytest.lazy_fixture("models_from_cursor_wrapper_single"), + "models_from_cursor_wrapper_multiple", + "models_from_cursor_wrapper_single", ], ) def test_models_from_cursor_handles_partial_fields( + request, mocked_model_varying_fields, mocked_model_varying_fields_instance, - models_from_cursor_wrapper, + models_from_cursor_wrapper_name, ): + models_from_cursor_wrapper = request.getfixturevalue( + models_from_cursor_wrapper_name + ) + with connection.cursor() as cursor: cursor.execute( f'SELECT id FROM "{mocked_model_varying_fields._meta.db_table}"', @@ -183,15 +200,19 @@ def test_models_from_cursor_handles_partial_fields( reason=django_31_skip_reason, ) @pytest.mark.parametrize( - "models_from_cursor_wrapper", + "models_from_cursor_wrapper_name", [ - pytest.lazy_fixture("models_from_cursor_wrapper_multiple"), - pytest.lazy_fixture("models_from_cursor_wrapper_single"), + "models_from_cursor_wrapper_multiple", + "models_from_cursor_wrapper_single", ], ) def test_models_from_cursor_handles_null( - mocked_model_varying_fields, models_from_cursor_wrapper + request, mocked_model_varying_fields, models_from_cursor_wrapper_name ): + models_from_cursor_wrapper = request.getfixturevalue( + models_from_cursor_wrapper_name + ) + instance = mocked_model_varying_fields.objects.create() with connection.cursor() as cursor: @@ -214,17 +235,22 @@ def test_models_from_cursor_handles_null( reason=django_31_skip_reason, ) @pytest.mark.parametrize( - "models_from_cursor_wrapper", + "models_from_cursor_wrapper_name", [ - pytest.lazy_fixture("models_from_cursor_wrapper_multiple"), - pytest.lazy_fixture("models_from_cursor_wrapper_single"), + "models_from_cursor_wrapper_multiple", + "models_from_cursor_wrapper_single", ], ) def test_models_from_cursor_foreign_key( + request, mocked_model_single_field, mocked_model_foreign_keys, - models_from_cursor_wrapper, + models_from_cursor_wrapper_name, ): + models_from_cursor_wrapper = request.getfixturevalue( + models_from_cursor_wrapper_name + ) + instance = mocked_model_foreign_keys.objects.create( varying_fields=None, single_field=mocked_model_single_field.objects.create(name="test"), @@ -254,18 +280,23 @@ def test_models_from_cursor_foreign_key( reason=django_31_skip_reason, ) @pytest.mark.parametrize( - "models_from_cursor_wrapper", + "models_from_cursor_wrapper_name", [ - pytest.lazy_fixture("models_from_cursor_wrapper_multiple"), - pytest.lazy_fixture("models_from_cursor_wrapper_single"), + "models_from_cursor_wrapper_multiple", + "models_from_cursor_wrapper_single", ], ) def test_models_from_cursor_related_fields( + request, mocked_model_varying_fields, mocked_model_single_field, mocked_model_foreign_keys, - models_from_cursor_wrapper, + models_from_cursor_wrapper_name, ): + models_from_cursor_wrapper = request.getfixturevalue( + models_from_cursor_wrapper_name + ) + instance = mocked_model_foreign_keys.objects.create( varying_fields=mocked_model_varying_fields.objects.create( title="test", updated_at=timezone.now() @@ -321,21 +352,26 @@ def test_models_from_cursor_related_fields( reason=django_31_skip_reason, ) @pytest.mark.parametrize( - "models_from_cursor_wrapper", + "models_from_cursor_wrapper_name", [ - pytest.lazy_fixture("models_from_cursor_wrapper_multiple"), - pytest.lazy_fixture("models_from_cursor_wrapper_single"), + "models_from_cursor_wrapper_multiple", + "models_from_cursor_wrapper_single", ], ) @pytest.mark.parametrize( "selected", [True, False], ids=["selected", "not_selected"] ) def test_models_from_cursor_related_fields_optional( + request, mocked_model_varying_fields, mocked_model_foreign_keys, - models_from_cursor_wrapper, + models_from_cursor_wrapper_name, selected, ): + models_from_cursor_wrapper = request.getfixturevalue( + models_from_cursor_wrapper_name + ) + instance = mocked_model_foreign_keys.objects.create( varying_fields=mocked_model_varying_fields.objects.create( title="test", updated_at=timezone.now() diff --git a/tests/test_make_migrations.py b/tests/test_make_migrations.py index 6f63a0d6..a843b6eb 100644 --- a/tests/test_make_migrations.py +++ b/tests/test_make_migrations.py @@ -208,7 +208,9 @@ def test_make_migration_field_operations_view_models( def test_autodetect_fk_issue(fake_app, method): """Test whether Django can perform ForeignKey optimization. - Fixes https://github.com/SectorLabs/django-postgres-extra/issues/123 for Django >= 2.2 + Fixes + https://github.com/SectorLabs/django-postgres-extra/issues/123 + for Django >= 2.2 """ meta_options = {"app_label": fake_app.name} partitioning_options = {"method": method, "key": "artist_id"} diff --git a/tests/test_management_command_partition.py b/tests/test_management_command_partition.py index 6e305fb9..c621cf15 100644 --- a/tests/test_management_command_partition.py +++ b/tests/test_management_command_partition.py @@ -6,6 +6,7 @@ from django.db import models from django.test import override_settings +from syrupy.extensions.json import JSONSnapshotExtension from psqlextra.backend.introspection import ( PostgresIntrospectedPartitionTable, @@ -20,6 +21,11 @@ from .fake_model import define_fake_partitioned_model +@pytest.fixture +def snapshot(snapshot): + return snapshot.use_extension(JSONSnapshotExtension) + + @pytest.fixture def fake_strategy(): strategy = create_autospec(PostgresPartitioningStrategy) @@ -88,12 +94,12 @@ def _run(*args): command.add_arguments(parser) command.handle(**vars(parser.parse_args(args))) - return capsys.readouterr() + return capsys.readouterr().out return _run -@pytest.mark.parametrize("args", ["-d", "--dry"]) +@pytest.mark.parametrize("args", ["-d", "--dry"], ids=["d", "dry"]) def test_management_command_partition_dry_run( args, snapshot, run, fake_model, fake_partitioning_manager ): @@ -101,7 +107,7 @@ def test_management_command_partition_dry_run( create/delete partitions.""" config = fake_partitioning_manager.find_config_for_model(fake_model) - snapshot.assert_match(run(args)) + assert run(args) == snapshot() config.strategy.createable_partition.create.assert_not_called() config.strategy.createable_partition.delete.assert_not_called() @@ -109,7 +115,7 @@ def test_management_command_partition_dry_run( config.strategy.deleteable_partition.delete.assert_not_called() -@pytest.mark.parametrize("args", ["-y", "--yes"]) +@pytest.mark.parametrize("args", ["-y", "--yes"], ids=["y", "yes"]) def test_management_command_partition_auto_confirm( args, snapshot, run, fake_model, fake_partitioning_manager ): @@ -117,7 +123,7 @@ def test_management_command_partition_auto_confirm( creating/deleting partitions.""" config = fake_partitioning_manager.find_config_for_model(fake_model) - snapshot.assert_match(run(args)) + assert run(args) == snapshot config.strategy.createable_partition.create.assert_called_once() config.strategy.createable_partition.delete.assert_not_called() @@ -125,7 +131,11 @@ def test_management_command_partition_auto_confirm( config.strategy.deleteable_partition.delete.assert_called_once() -@pytest.mark.parametrize("answer", ["y", "Y", "yes", "YES"]) +@pytest.mark.parametrize( + "answer", + ["y", "Y", "yes", "YES"], + ids=["y", "capital_y", "yes", "capital_yes"], +) def test_management_command_partition_confirm_yes( answer, monkeypatch, snapshot, run, fake_model, fake_partitioning_manager ): @@ -135,7 +145,7 @@ def test_management_command_partition_confirm_yes( config = fake_partitioning_manager.find_config_for_model(fake_model) monkeypatch.setattr("builtins.input", lambda _: answer) - snapshot.assert_match(run()) + assert run() == snapshot config.strategy.createable_partition.create.assert_called_once() config.strategy.createable_partition.delete.assert_not_called() @@ -143,7 +153,11 @@ def test_management_command_partition_confirm_yes( config.strategy.deleteable_partition.delete.assert_called_once() -@pytest.mark.parametrize("answer", ["n", "N", "no", "No", "NO"]) +@pytest.mark.parametrize( + "answer", + ["n", "N", "no", "No", "NO"], + ids=["n", "capital_n", "no", "title_no", "capital_no"], +) def test_management_command_partition_confirm_no( answer, monkeypatch, snapshot, run, fake_model, fake_partitioning_manager ): @@ -153,7 +167,7 @@ def test_management_command_partition_confirm_no( config = fake_partitioning_manager.find_config_for_model(fake_model) monkeypatch.setattr("builtins.input", lambda _: answer) - snapshot.assert_match(run()) + assert run() == snapshot config.strategy.createable_partition.create.assert_not_called() config.strategy.createable_partition.delete.assert_not_called() diff --git a/tests/test_manager.py b/tests/test_manager.py index 0fbe2a52..f68dd20a 100644 --- a/tests/test_manager.py +++ b/tests/test_manager.py @@ -34,10 +34,8 @@ def test_manager_backend_set(databases): def test_manager_backend_not_set(): - """Tests whether creating a new instance of - :see:PostgresManager fails if no database - has `psqlextra.backend` configured - as its ENGINE.""" + """Tests whether creating a new instance of :see:PostgresManager fails if + no database has `psqlextra.backend` configured as its ENGINE.""" with override_settings( DATABASES={"default": {"ENGINE": "django.db.backends.postgresql"}} diff --git a/tests/test_on_conflict.py b/tests/test_on_conflict.py index 7f3f5ab8..b7cf0024 100644 --- a/tests/test_on_conflict.py +++ b/tests/test_on_conflict.py @@ -179,11 +179,11 @@ def test_on_conflict_outdated_model(conflict_action): """Tests whether insert properly handles fields that are in the database but not on the model. - This happens if somebody manually modified the database - to add a column that is not present in the model. + This happens if somebody manually modified the database to add a + column that is not present in the model. - This should be handled properly by ignoring the column - returned by the database. + This should be handled properly by ignoring the column returned by + the database. """ model = get_fake_model( diff --git a/tox.ini b/tox.ini index 963f9d31..697d1c44 100644 --- a/tox.ini +++ b/tox.ini @@ -23,8 +23,9 @@ deps = psycopg29: psycopg2[binary]~=2.9 psycopg31: psycopg[binary]~=3.1 psycopg32: psycopg[binary]~=3.2 + .[dev] .[test] setenv = DJANGO_SETTINGS_MODULE=settings passenv = DATABASE_URL -commands = python setup.py test +commands = poe test