diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 5ffee75..cb70731 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -44,7 +44,7 @@ jobs: run: | inv ci.prepare - name: Run checks ${{ matrix.python-version }} - run: inv git.run-hooks + run: inv pre-commit.run-hooks - name: Upload results to coveralls run: | pip install coveralls diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 597e5aa..7d06026 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,6 +14,37 @@ repos: - id: isort name: isort (python) + - repo: https://github.com/pycqa/flake8 + rev: 6.1.0 + hooks: + - id: flake8 + exclude: node_modules|migrations|scripts|.venv|__init__.py + additional_dependencies: [ + # A flake8 plugin that checks django code style. + # https://github.com/rocioar/flake8-django + flake8-django, + # required by flake8-django + django, + django_extensions, + # A plugin for Flake8 finding likely bugs and design problems in your program. + # https://github.com/PyCQA/flake8-bugbear + flake8-bugbear, + # A flake8 plugin checking common style issues or inconsistencies with pytest-based tests. + # https://github.com/m-burst/flake8-pytest-style + flake8-pytest-style, + # A flake8 plugin that warn about backslashes usage. + # https://github.com/wemake-services/flake8-broken-line + flake8-broken-line, + # A simple module that adds an extension for the fantastic pydocstyle tool to flake8. + # https://github.com/PyCQA/flake8-docstrings + flake8-docstrings, + # McCabe complexity checker. + # https://github.com/PyCQA/mccabe + mccabe, + # A flake8 plug-in loading the configuration from pyproject.toml + flake8-pyproject, + ] + - repo: https://github.com/asottile/add-trailing-comma rev: v2.4.0 hooks: @@ -44,20 +75,23 @@ repos: types: [ file ] stages: [ push ] - - id: linters - name: run linters - entry: inv linters.all + - id: tests + name: run tests + entry: + inv python.run + " -m coverage run --source import_export_extensions + --omit import_export_extensions/migrations -m pytest -v" language: system pass_filenames: false types: [ python ] stages: [ push ] - - id: tests - name: run tests - entry: inv tests.run-ci + - id: mypy + name: mypy + entry: inv mypy.run language: system pass_filenames: false - types: [ python ] + types: [ file ] stages: [ push ] - id: package_installation_verify diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index f4a3b95..3526ded 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -80,8 +80,7 @@ Ready to contribute? Here's how to set up `django-import-export-extensions` for 5. When you're done making changes, check that your changes pass flake8 and the tests:: - $ inv tests.run - $ inv linters.all + $ inv pre-commit.run-hooks 6. Commit your changes and push your branch to GitHub:: diff --git a/poetry.lock b/poetry.lock index 1521c36..a2ffde1 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1063,26 +1063,6 @@ files = [ {file = "imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a"}, ] -[[package]] -name = "importlib-metadata" -version = "6.8.0" -description = "Read metadata from Python packages" -category = "dev" -optional = false -python-versions = ">=3.8" -files = [ - {file = "importlib_metadata-6.8.0-py3-none-any.whl", hash = "sha256:3ebb78df84a805d7698245025b975d9d67053cd94c79245ba4b3eb694abe68bb"}, - {file = "importlib_metadata-6.8.0.tar.gz", hash = "sha256:dbace7892d8c0c4ac1ad096662232f831d4e64f4c4545bd53016a3e9d4654743"}, -] - -[package.dependencies] -zipp = ">=0.5" - -[package.extras] -docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -perf = ["ipython"] -testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)", "pytest-ruff"] - [[package]] name = "inflection" version = "0.5.1" @@ -1159,7 +1139,6 @@ prompt-toolkit = ">=3.0.41,<3.1.0" pygments = ">=2.4.0" stack-data = "*" traitlets = ">=5" -typing-extensions = {version = "*", markers = "python_version < \"3.10\""} [package.extras] all = ["black", "curio", "docrepr", "exceptiongroup", "ipykernel", "ipyparallel", "ipywidgets", "matplotlib", "matplotlib (!=3.2.0)", "nbconvert", "nbformat", "notebook", "numpy (>=1.22)", "pandas", "pickleshare", "pytest (<7)", "pytest (<7.1)", "pytest-asyncio (<0.22)", "qtconsole", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "stack-data", "testpath", "trio", "typing-extensions"] @@ -1263,7 +1242,6 @@ files = [ [package.dependencies] amqp = ">=5.1.1,<6.0.0" -typing-extensions = {version = "*", markers = "python_version < \"3.10\""} vine = "*" [package.extras] @@ -2204,6 +2182,25 @@ files = [ {file = "rpds_py-0.13.2.tar.gz", hash = "sha256:f8eae66a1304de7368932b42d801c67969fd090ddb1a7a24f27b435ed4bed68f"}, ] +[[package]] +name = "saritasa-invocations" +version = "0.10.0" +description = "Collection of invoke commands used by Saritasa" +category = "dev" +optional = false +python-versions = ">=3.10,<4.0" +files = [ + {file = "saritasa_invocations-0.10.0-py3-none-any.whl", hash = "sha256:9aa78d5cd1f02682638a5ba7641182ba304ab9f56dec05ad2d8606eec31049d6"}, + {file = "saritasa_invocations-0.10.0.tar.gz", hash = "sha256:185a54ad499ded5f3147797def1809c16c072c8575ebbf65ffb9d614ba0aff64"}, +] + +[package.dependencies] +invoke = ">=2,<3" +rich = ">=13,<14" + +[package.extras] +env-settings = ["python-decouple (>=3,<4)"] + [[package]] name = "setuptools" version = "69.0.2" @@ -2275,7 +2272,6 @@ babel = ">=2.9" colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} docutils = ">=0.18.1,<0.21" imagesize = ">=1.3" -importlib-metadata = {version = ">=4.8", markers = "python_version < \"3.10\""} Jinja2 = ">=3.0" packaging = ">=21.0" Pygments = ">=2.14" @@ -2591,6 +2587,21 @@ files = [ {file = "types_python_dateutil-2.8.19.14-py3-none-any.whl", hash = "sha256:f977b8de27787639986b4e28963263fd0e5158942b3ecef91b9335c130cb1ce9"}, ] +[[package]] +name = "types-requests" +version = "2.31.0.10" +description = "Typing stubs for requests" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "types-requests-2.31.0.10.tar.gz", hash = "sha256:dc5852a76f1eaf60eafa81a2e50aefa3d1f015c34cf0cba130930866b1b22a92"}, + {file = "types_requests-2.31.0.10-py3-none-any.whl", hash = "sha256:b32b9a86beffa876c0c3ac99a4cd3b8b51e973fb8e3bd4e0a6bb32c7efad80fc"}, +] + +[package.dependencies] +urllib3 = ">=2" + [[package]] name = "typing-extensions" version = "4.8.0" @@ -2856,23 +2867,7 @@ files = [ {file = "xlwt-1.3.0.tar.gz", hash = "sha256:c59912717a9b28f1a3c2a98fd60741014b06b043936dcecbc113eaaada156c88"}, ] -[[package]] -name = "zipp" -version = "3.17.0" -description = "Backport of pathlib-compatible object wrapper for zip files" -category = "dev" -optional = false -python-versions = ">=3.8" -files = [ - {file = "zipp-3.17.0-py3-none-any.whl", hash = "sha256:0e923e726174922dce09c53c59ad483ff7bbb8e572e00c7f7c46b88556409f31"}, - {file = "zipp-3.17.0.tar.gz", hash = "sha256:84e64a1c28cf7e91ed2078bb8cc8c259cb19b76942096c8d7b84947690cabaf0"}, -] - -[package.extras] -docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"] -testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy (>=0.9.1)", "pytest-ruff"] - [metadata] lock-version = "2.0" -python-versions = ">=3.9, <3.12" -content-hash = "4fd6ae8958c2149b0b7f44f0265e641e2211fe627fa877695c747389598720e4" +python-versions = ">=3.10, <3.12" +content-hash = "e060032f2fa2d332a49c2f9dde92d213f493bdf06e22ede11320f48050eb4a05" diff --git a/provision/celery.py b/provision/celery.py deleted file mode 100644 index 2ed1e98..0000000 --- a/provision/celery.py +++ /dev/null @@ -1,10 +0,0 @@ -from invoke import task - - -@task -def run(context): - """Start celery worker.""" - context.run( - "celery --app tests.celery_app:app " - "worker --beat --scheduler=django --loglevel=info", - ) diff --git a/provision/ci.py b/provision/ci.py index 00e4dc8..f7855c6 100644 --- a/provision/ci.py +++ b/provision/ci.py @@ -1,22 +1,14 @@ ############################################################################## # Commands used in ci for code validation ############################################################################## +import saritasa_invocations from invoke import task -from . import common, docker, project - @task def prepare(context): """Prepare ci environment for check.""" - common.success("Preparing CI") - docker.up(context) - set_up_hosts(context) - project.install_requirements(context) - - -def set_up_hosts(context): - """Add services to hosts.""" - common.success("Setting up hosts") - context.run("echo \"127.0.0.1 postgres\" | sudo tee -a /etc/hosts") - context.run("echo \"127.0.0.1 redis\" | sudo tee -a /etc/hosts") + saritasa_invocations.print_success("Preparing CI") + saritasa_invocations.docker.up(context) + saritasa_invocations.github_actions.set_up_hosts(context) + saritasa_invocations.poetry.install(context) diff --git a/provision/common.py b/provision/common.py deleted file mode 100644 index b405a21..0000000 --- a/provision/common.py +++ /dev/null @@ -1,17 +0,0 @@ -import rich -from rich.panel import Panel - - -def success(msg): - """Print success message.""" - return rich.print(Panel(msg, style="green bold")) - - -def warn(msg): - """Print warning message.""" - return rich.print(Panel(msg, style="yellow bold")) - - -def error(msg): - """Print error message.""" - return rich.print(Panel(msg, style="red bold")) diff --git a/provision/django.py b/provision/django.py deleted file mode 100644 index 4ad4746..0000000 --- a/provision/django.py +++ /dev/null @@ -1,140 +0,0 @@ -############################################################################## -# Django commands and stuff -############################################################################## -from invoke import FailingResponder, Failure, Responder, task - -from . import common, docker, start - - -def wait_for_database(context): - """Ensure that database is up and ready to accept connections. - - Function called just once during subsequent calls of management commands. - - """ - if hasattr(wait_for_database, "_called"): - return - docker.up(context) - start.run_python( - context, - " ".join(["tests/manage.py", "wait_for_database", "--stable 0"]), - ) - wait_for_database._called = True - - -@task -def manage(context, command, watchers=()): - """Run ``manage.py`` command. - - This command also handle starting of required services and waiting DB to - be ready. - - Args: - context: Invoke context - command: Manage command - watchers: Automated responders to command - - """ - wait_for_database(context) - return start.run_python( - context, - " ".join(["tests/manage.py", command]), - watchers=watchers, - ) - - -@task -def makemigrations(context): - """Run makemigrations command and chown created migrations.""" - common.success("Django: Make migrations") - manage(context, "makemigrations") - - -@task -def check_new_migrations(context): - """Check if there is new migrations or not.""" - common.success("Checking migrations") - manage(context, "makemigrations --check --dry-run") - - -@task -def migrate(context): - """Run ``migrate`` command.""" - common.success("Django: Apply migrations") - manage(context, "migrate") - - -@task -def resetdb(context, apply_migrations=True): - """Reset database to initial state (including test DB).""" - common.success("Reset database to its initial state") - manage(context, "drop_test_database --noinput") - manage(context, "reset_db -c --noinput") - if not apply_migrations: - return - makemigrations(context) - migrate(context) - createsuperuser(context) - - -@task -def createsuperuser( - context, - email="root@root.com", - username="root", - password="root", -): - """Create superuser.""" - common.success("Create superuser") - responder_email = FailingResponder( - pattern=r"Email address: ", - response=email + "\n", - sentinel="That Email address is already taken.", - ) - responder_user_name = Responder( - pattern=r"Username: ", - response=username + "\n", - ) - responder_password = Responder( - pattern=r"(Password: )|(Password \(again\): )", - response=password + "\n", - ) - - try: - manage( - context, - command="createsuperuser", - watchers=[ - responder_email, - responder_user_name, - responder_password, - ], - ) - except Failure: - common.warn("Superuser with that email already exists. Skipped.") - - -@task -def run(context): - """Run development web-server.""" - common.success("Running web app") - manage(context, "runserver_plus 0.0.0.0:8000") - - -@task -def shell(context, params=""): - """Shortcut for manage.py shell_plus command. - - Additional params available here: - https://django-extensions.readthedocs.io/en/latest/shell_plus.html - - """ - common.success("Entering Django Shell") - manage(context, f"shell_plus --ipython {params}") - - -@task -def dbshell(context): - """Open postgresql shell with credentials from either local or dev env.""" - common.success("Entering DB shell") - manage(context, "dbshell") diff --git a/provision/docker.py b/provision/docker.py deleted file mode 100644 index 1b3ce95..0000000 --- a/provision/docker.py +++ /dev/null @@ -1,96 +0,0 @@ -from invoke import UnexpectedExit, task - -from . import common - -############################################################################## -# Containers start stop commands -############################################################################## - -MAIN_CONTAINERS = ( - "postgres", - "redis", -) - - -def stop_all_containers(context): - """Shortcut for stopping ALL running docker containers.""" - context.run("docker stop $(docker ps -q)") - - -def up_containers( - context, - containers: tuple[str, ...], - detach=True, - stop_others=True, - **kwargs, -): - """Bring up containers and run them. - - Add `d` kwarg to run them in background. - - Args: - context: Invoke context - containers: Name of containers to start - detach: To run them in background - stop_others: Stop ALL other containers in case of errors during `up`. - Usually this happens when containers from other project uses the - same ports, for example, Postgres and redis. - - Raises: - UnexpectedExit: when `up` command wasn't successful - - """ - if containers: - common.success(f"Bring up {', '.join(containers)} containers") - else: - common.success("Bring up all containers") - up_cmd = ( - f"docker compose up " - f"{'-d ' if detach else ''}" - f"{' '.join(containers)}" - ) - try: - context.run(up_cmd) - except UnexpectedExit as exception: - if not stop_others: - raise exception - stop_all_containers(context) - context.run(up_cmd) - - -def stop_containers(context, containers): - """Stop containers.""" - common.success(f"Stopping {' '.join(containers)} containers ") - cmd = f"docker compose stop {' '.join(containers)}" - context.run(cmd) - - -@task -def up(context): - """Bring up main containers and start them.""" - up_containers( - context, - containers=MAIN_CONTAINERS, - detach=True, - ) - - -@task -def stop(context): - """Stop main containers.""" - stop_containers( - context, - containers=MAIN_CONTAINERS, - ) - - -@task -def clear(context): - """Stop and remove all containers defined in docker-compose. - - Also remove images. - - """ - common.success("Clearing docker compose") - context.run("docker compose rm -f") - context.run("docker compose down -v --rmi all --remove-orphans") diff --git a/provision/git.py b/provision/git.py deleted file mode 100644 index 5bbdd68..0000000 --- a/provision/git.py +++ /dev/null @@ -1,32 +0,0 @@ -from invoke import task - -from . import common - - -@task -def setup(context): - """Set up git for working.""" - pre_commit(context) - context.run("git config --add merge.ff false") - context.run("git config --add pull.ff only") - - -@task -def pre_commit(context): - """Install git hooks via pre-commit.""" - common.success("Setting up pre-commit") - hooks = " ".join( - f"--hook-type {hook}" for hook in ( - "pre-commit", - "pre-push", - "commit-msg", - ) - ) - context.run(f"pre-commit install {hooks}") - - -@task -def run_hooks(context): - """Install git hooks.""" - common.success("Running git hooks") - context.run("pre-commit run --hook-stage push --all-files") diff --git a/provision/linters.py b/provision/linters.py deleted file mode 100644 index 5638a52..0000000 --- a/provision/linters.py +++ /dev/null @@ -1,56 +0,0 @@ -from invoke import Exit, UnexpectedExit, task - -from . import common - -############################################################################## -# Linters -############################################################################## - -DEFAULT_FOLDERS = "import_export_extensions tests" - - -@task -def isort(context, path=DEFAULT_FOLDERS, params=""): - """Command to fix imports formatting.""" - common.success("Linters: ISort running") - return context.run(command=f"isort {path} {params}") - - -@task -def isort_check(context, path=DEFAULT_FOLDERS): - """Command to fix imports formatting.""" - return isort(context, path=path, params="--check-only") - - -@task -def flake8(context, path=DEFAULT_FOLDERS): - """Run `flake8` linter.""" - common.success("Linters: Flake8 running") - return context.run(command=f"flake8 {path}") - - -@task -def mypy(context, path=DEFAULT_FOLDERS): - """Run `mypy` linter.""" - common.success("Linters: Mypy running") - return context.run( - command=f"mypy --install-types --non-interactive {path}", - ) - - -@task -def all(context, path=DEFAULT_FOLDERS): - """Run all linters""" - common.success("Linters: running all linters") - linters = (isort_check, flake8, mypy) - failed = [] - for linter in linters: - try: - linter(context, path) - except UnexpectedExit: - failed.append(linter.__name__) - if failed: - common.error( - f"Linters failed: {', '.join(map(str.capitalize, failed))}", - ) - raise Exit(code=1) diff --git a/provision/project.py b/provision/project.py index d66048b..038aad1 100644 --- a/provision/project.py +++ b/provision/project.py @@ -1,7 +1,6 @@ +import saritasa_invocations from invoke import task -from . import common, django, docker, git, linters, tests - ############################################################################## # Build project locally @@ -9,35 +8,12 @@ @task def init(context, clean=False): """Prepare env for working with project.""" - common.success("Setting up git config") - git.setup(context) - common.success("Initial assembly of all dependencies") - install_tools(context) - install_requirements(context) + saritasa_invocations.print_success("Setting up git config") + saritasa_invocations.git.setup(context) + saritasa_invocations.print_success("Initial assembly of all dependencies") + saritasa_invocations.poetry.install(context) if clean: - docker.clear(context) - django.migrate(context) - tests.run(context) - linters.all(context) - django.createsuperuser(context) - - -############################################################################## -# Manage dependencies -############################################################################## -@task -def install_tools(context): - """Install shell/cli dependencies, and tools needed to install requirements - - Define your dependencies here, for example: - local("sudo npm -g install ngrok") - - """ - context.run("pip install --upgrade setuptools pip pip-tools wheel") - - -@task -def install_requirements(context): - """Install local development requirements""" - common.success("Install dependencies with poetry") - context.run("poetry install --sync") + saritasa_invocations.docker.clear(context) + saritasa_invocations.django.migrate(context) + saritasa_invocations.pytest.run(context) + saritasa_invocations.django.createsuperuser(context) diff --git a/provision/start.py b/provision/start.py deleted file mode 100644 index 0c50cea..0000000 --- a/provision/start.py +++ /dev/null @@ -1,17 +0,0 @@ -############################################################################## -# Run commands -############################################################################## - -def run_python(context, command: str, watchers=()): - """Run command using local python interpreter.""" - return context.run( - " ".join(["python3", command]), - watchers=watchers, - ) - -def run_coverage(context, command: str, watchers=()): - """Run command using coverage.""" - return context.run( - " ".join(["coverage run", command]), - watchers=watchers, - ) diff --git a/provision/tests.py b/provision/tests.py deleted file mode 100644 index ac38d50..0000000 --- a/provision/tests.py +++ /dev/null @@ -1,26 +0,0 @@ -from invoke import task - -from . import common, start - - -@task -def run(context, params=""): - """Run django tests with ``extra`` args for ``p`` tests. - - `p` means `params` - extra args for tests - python -m pytest - - """ - common.success("Tests running") - return start.run_python(context, f"-m pytest {params}") - - -@task -def run_ci(context): - """Run tests in github actions.""" - start.run_coverage( - context, - "--source import_export_extensions " - "--omit import_export_extensions/migrations " - "-m pytest -v", - ) diff --git a/pyproject.toml b/pyproject.toml index 9e75c30..dc7caa7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,7 +24,7 @@ classifiers = [ ] [tool.poetry.dependencies] -python = ">=3.9, <3.12" +python = ">=3.10, <3.12" django-import-export = "^3.3.3" celery = "^5.3.6" redis = "^5.0.1" @@ -51,6 +51,7 @@ flake8-pyproject = "^1.2.3" flake8 = "^6.1.0" mccabe = "^0.7.0" mypy = "^1.7.1" +types-requests = "^2.31.0.10" [tool.poetry.group.test.dependencies] django-probes = "^1.7.0" @@ -67,11 +68,10 @@ sphinx-rtd-theme = "^2.0.0" watchdog = "^3.0.0" [tool.poetry.group.local.dependencies] -# saritasa-invocations = "^0.10.0" toml = "^0.10.2" pre-commit = "^3.5.0" cruft = "^2.15.0" -invoke = "^2.2.0" +saritasa-invocations = "^0.10.0" [build-system] requires = ["poetry-core"] diff --git a/tasks.py b/tasks.py index fee6c13..443352f 100644 --- a/tasks.py +++ b/tasks.py @@ -1,24 +1,42 @@ +import saritasa_invocations from invoke import Collection -from provision import celery, ci, django, docker, git, linters, project, tests +from provision import ci, project ns = Collection( - celery, + saritasa_invocations.celery, ci, - django, - docker, - linters, + saritasa_invocations.django, + saritasa_invocations.docker, project, - tests, - git, + saritasa_invocations.pytest, + saritasa_invocations.poetry, + saritasa_invocations.git, + saritasa_invocations.pre_commit, + saritasa_invocations.mypy, + saritasa_invocations.python, ) # Configurations for run command ns.configure( - dict( - run=dict( - pty=True, - echo=True, + { + "run": { + "pty": True, + "echo": True, + }, + "saritasa_invocations": saritasa_invocations.Config( + project_name="django-import-export-extensions", + celery=saritasa_invocations.CelerySettings( + app="tests.celery_app:app", + ), + django=saritasa_invocations.DjangoSettings( + manage_file_path="tests/manage.py", + settings_path="tests.settings", + apps_path="tests", + ), + github_actions=saritasa_invocations.GitHubActionsSettings( + hosts=("postgres", "redis"), + ), ), - ), + }, )