diff --git a/.gitignore b/.gitignore index e3a67ef..150293c 100644 --- a/.gitignore +++ b/.gitignore @@ -10,5 +10,6 @@ build dist *.pyc .pytest_cache/ +.ruff_cache/ # dump.rdb is created by redis-server, needed to run tests dump.rdb diff --git a/.isort.cfg b/.isort.cfg deleted file mode 100644 index ba2778d..0000000 --- a/.isort.cfg +++ /dev/null @@ -1,6 +0,0 @@ -[settings] -multi_line_output=3 -include_trailing_comma=True -force_grid_wrap=0 -use_parentheses=True -line_length=88 diff --git a/MANIFEST.in b/MANIFEST.in index af16441..42b4eb4 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,6 +1,6 @@ recursive-include docs * recursive-include tests * -include pytest.ini AUTHORS.rst CODE_OF_CONDUCT.md pyproject.toml LICENSE README.rst tox.ini .coveragerc .isort.cfg .readthedocs.yaml +include pytest.ini AUTHORS.rst CODE_OF_CONDUCT.md pyproject.toml LICENSE README.rst tox.ini .coveragerc .readthedocs.yaml prune docs/_build global-exclude *.pyc prune __pycache__ diff --git a/pyproject.toml b/pyproject.toml index 63c8fb3..f60887f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,17 +1,27 @@ -[tool.black] -line-length = 88 -target-version = ['py37', 'py38', 'py39', 'py310', 'py311'] -include = '\.pyi?$' -exclude = ''' -/( - \.eggs - | \.git - | \.hg - | \.mypy_cache - | \.tox - | \.venv - | _build - | build - | dist -)/ -''' +[tool.ruff] +extend-exclude = [ + "__pycache__", +] + +[tool.ruff.lint] +select = [ + # pycodestyle + "E", "W", + # flake8 + "F", + # isort + "I", + # pytest style + "PT", + # eradicate commented code + "ERA", + # ruff lint + "RUF", +] +ignore = [ + # `format` will wrap lines. + "E501", +] + +[tool.ruff.lint.isort] +known-first-party = ["dockerflow"] diff --git a/src/dockerflow/django/middleware.py b/src/dockerflow/django/middleware.py index 8b20936..7208ce4 100644 --- a/src/dockerflow/django/middleware.py +++ b/src/dockerflow/django/middleware.py @@ -1,6 +1,7 @@ import logging import re import time +import typing import urllib import uuid @@ -16,7 +17,7 @@ class DockerflowMiddleware(MiddlewareMixin): https://github.com/mozilla-services/Dockerflow/blob/main/docs/mozlog.md """ - viewpatterns = [ + viewpatterns: typing.ClassVar = [ (re.compile(r"/__version__/?$"), views.version), (re.compile(r"/__heartbeat__/?$"), views.heartbeat), (re.compile(r"/__lbheartbeat__/?$"), views.lbheartbeat), diff --git a/src/dockerflow/logging.py b/src/dockerflow/logging.py index 081d808..567b64b 100644 --- a/src/dockerflow/logging.py +++ b/src/dockerflow/logging.py @@ -8,6 +8,7 @@ import socket import sys import traceback +import typing class SafeJSONEncoder(json.JSONEncoder): @@ -32,7 +33,7 @@ class JsonLogFormatter(logging.Formatter): LOGGING_FORMAT_VERSION = "2.0" # Map from Python logging to Syslog severity levels - SYSLOG_LEVEL_MAP = { + SYSLOG_LEVEL_MAP: typing.ClassVar = { 50: 2, # CRITICAL 40: 3, # ERROR 30: 4, # WARNING @@ -43,7 +44,7 @@ class JsonLogFormatter(logging.Formatter): # Syslog level to use when/if python level isn't found in map DEFAULT_SYSLOG_LEVEL = 7 - EXCLUDED_LOGRECORD_ATTRS = set( + EXCLUDED_LOGRECORD_ATTRS: typing.ClassVar = set( ( "args", "asctime", diff --git a/tests/conftest.py b/tests/conftest.py index ec3ae02..6cd8e79 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -6,7 +6,7 @@ import dockerflow.checks.registry -@pytest.fixture +@pytest.fixture() def version_content(): """ as documented on https://github.com/mozilla-services/Dockerflow/blob/main/docs/version_object.md @@ -20,6 +20,6 @@ def version_content(): @pytest.fixture(autouse=True) -def clear_checks(): +def _clear_checks(): yield dockerflow.checks.registry.clear_checks() diff --git a/tests/core/test_logging.py b/tests/core/test_logging.py index 1ac702f..54d2b71 100644 --- a/tests/core/test_logging.py +++ b/tests/core/test_logging.py @@ -15,7 +15,7 @@ @pytest.fixture() -def reset_logging(): +def _reset_logging(): logging.shutdown() reload(logging) @@ -31,7 +31,8 @@ def assert_records(records): return details -def test_initialization_from_ini(reset_logging, caplog, tmpdir): +@pytest.mark.usefixtures("_reset_logging") +def test_initialization_from_ini(caplog, tmpdir): ini_content = textwrap.dedent( """ [loggers] @@ -229,7 +230,5 @@ def test_ignore_json_message(caplog): } } } -""".replace( - "\\", "\\\\" - ) +""".replace("\\", "\\\\") ) # HACK: Fix escaping for easy copy/paste diff --git a/tests/django/test_django.py b/tests/django/test_django.py index 5464e97..593798b 100644 --- a/tests/django/test_django.py +++ b/tests/django/test_django.py @@ -20,19 +20,19 @@ @pytest.fixture(autouse=True) -def reset_checks(): +def _reset_checks(): yield registry.registered_checks = set() registry.deployment_checks = set() @pytest.fixture(autouse=True) -def setup_request_summary_logger(dockerflow_middleware): +def _setup_request_summary_logger(dockerflow_middleware): dockerflow_middleware.summary_logger.addHandler(logging.NullHandler()) dockerflow_middleware.summary_logger.setLevel(logging.INFO) -@pytest.fixture +@pytest.fixture() def dockerflow_middleware(): return DockerflowMiddleware(get_response=HttpResponse()) @@ -55,7 +55,7 @@ def test_version_missing(dockerflow_middleware, mocker, rf): assert response.status_code == 404 -@pytest.mark.django_db +@pytest.mark.django_db() def test_heartbeat(client, settings): response = client.get("/__heartbeat__") assert response.status_code == 200 @@ -73,7 +73,7 @@ def test_heartbeat(client, settings): assert content.get("details") is None -@pytest.mark.django_db +@pytest.mark.django_db() def test_heartbeat_debug(client, settings): settings.DOCKERFLOW_CHECKS = [ "tests.django.django_checks.warning", @@ -89,7 +89,7 @@ def test_heartbeat_debug(client, settings): assert content["details"] -@pytest.mark.django_db +@pytest.mark.django_db() def test_heartbeat_silenced(client, settings): settings.DOCKERFLOW_CHECKS = [ "tests.django.django_checks.warning", @@ -107,8 +107,9 @@ def test_heartbeat_silenced(client, settings): assert "error" not in content["details"] -@pytest.mark.django_db -def test_heartbeat_logging(dockerflow_middleware, reset_checks, rf, settings, caplog): +@pytest.mark.django_db() +@pytest.mark.usefixtures("_reset_checks") +def test_heartbeat_logging(dockerflow_middleware, rf, settings, caplog): request = rf.get("/__heartbeat__") settings.DOCKERFLOW_CHECKS = [ "tests.django.django_checks.warning", @@ -123,7 +124,7 @@ def test_heartbeat_logging(dockerflow_middleware, reset_checks, rf, settings, ca assert ("WARNING", "tests.checks.W001: some warning") in logged -@pytest.mark.django_db +@pytest.mark.django_db() def test_lbheartbeat_makes_no_db_queries(dockerflow_middleware, rf): queries = CaptureQueriesContext(connection) request = rf.get("/__lbheartbeat__") @@ -133,7 +134,7 @@ def test_lbheartbeat_makes_no_db_queries(dockerflow_middleware, rf): assert len(queries) == 0 -@pytest.mark.django_db +@pytest.mark.django_db() def test_redis_check(client, settings): settings.DOCKERFLOW_CHECKS = ["dockerflow.django.checks.check_redis_connected"] checks.register() @@ -152,7 +153,7 @@ def assert_log_record(request, record, errno=0, level=logging.INFO): assert isinstance(record.t, int) -@pytest.fixture +@pytest.fixture() def dockerflow_request(rf): return rf.get("/", HTTP_USER_AGENT="dockerflow/tests", HTTP_ACCEPT_LANGUAGE="tlh") @@ -248,7 +249,7 @@ def test_check_database_connected_misconfigured(mocker): assert errors[0].id == health.ERROR_MISCONFIGURED_DATABASE -@pytest.mark.django_db +@pytest.mark.django_db() def test_check_database_connected_unsuable(mocker): mocker.patch("django.db.connection.is_usable", return_value=False) errors = checks.check_database_connected([]) @@ -256,7 +257,7 @@ def test_check_database_connected_unsuable(mocker): assert errors[0].id == health.ERROR_UNUSABLE_DATABASE -@pytest.mark.django_db +@pytest.mark.django_db() def test_check_database_connected_success(mocker): errors = checks.check_database_connected([]) assert errors == [] @@ -272,7 +273,7 @@ def test_check_migrations_applied_cannot_check_migrations(exception, mocker): assert errors[0].id == health.INFO_CANT_CHECK_MIGRATIONS -@pytest.mark.django_db +@pytest.mark.django_db() def test_check_migrations_applied_unapplied_migrations(mocker): mock_loader = mocker.patch("django.db.migrations.loader.MigrationLoader") mock_loader.return_value.applied_migrations = ["spam", "eggs"] @@ -306,7 +307,7 @@ def test_check_migrations_applied_unapplied_migrations(mocker): @pytest.mark.parametrize( - "exception,error", + ("exception", "error"), [ (redis.ConnectionError, health.ERROR_CANNOT_CONNECT_REDIS), (NotImplementedError, health.ERROR_MISSING_REDIS_CLIENT), diff --git a/tests/fastapi/test_fastapi.py b/tests/fastapi/test_fastapi.py index 4c51da9..a514f9a 100644 --- a/tests/fastapi/test_fastapi.py +++ b/tests/fastapi/test_fastapi.py @@ -20,12 +20,12 @@ def create_app(): return app -@pytest.fixture +@pytest.fixture() def app(): return create_app() -@pytest.fixture +@pytest.fixture() def client(app): return TestClient(app) @@ -69,7 +69,7 @@ def test_mozlog_failure(client, mocker, caplog): "dockerflow.fastapi.views.get_version", side_effect=ValueError("crash") ) - with pytest.raises(ValueError): + with pytest.raises(expected_exception=ValueError): client.get("/__version__") record = caplog.records[0] diff --git a/tests/flask/migrations/env.py b/tests/flask/migrations/env.py index b9641bc..58e16bb 100755 --- a/tests/flask/migrations/env.py +++ b/tests/flask/migrations/env.py @@ -4,7 +4,7 @@ from logging.config import fileConfig from alembic import context # no:qa -from flask import current_app # noqa +from flask import current_app from sqlalchemy import engine_from_config, pool # this is the Alembic Config object, which provides @@ -24,7 +24,7 @@ # other values from the config, defined by the needs of env.py, # can be acquired: -# my_important_option = config.get_main_option("my_important_option") +# my_important_option = config.get_main_option("my_important_option") # noqa # ... etc. diff --git a/tests/flask/test_flask.py b/tests/flask/test_flask.py index ac8f09a..857b6b4 100644 --- a/tests/flask/test_flask.py +++ b/tests/flask/test_flask.py @@ -34,7 +34,7 @@ def load_user(user_id): return MockUser(user_id) -@pytest.fixture +@pytest.fixture() def app(): app = Flask("dockerflow") app.secret_key = "super sekrit" @@ -48,31 +48,31 @@ def client(app): return app.test_client() -@pytest.fixture +@pytest.fixture() def dockerflow(app): return Dockerflow(app) @pytest.fixture() -def setup_request_summary_logger(dockerflow): +def _setup_request_summary_logger(dockerflow): dockerflow.summary_logger.addHandler(logging.NullHandler()) dockerflow.summary_logger.setLevel(logging.INFO) -@pytest.fixture +@pytest.fixture() def db(app): app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite://" app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False return SQLAlchemy(app) -@pytest.fixture +@pytest.fixture() def migrate(app, db): test_migrations = os.path.join(os.path.dirname(__file__), "migrations") return Migrate(app, db, directory=test_migrations) -@pytest.fixture +@pytest.fixture() def redis_store(app): return FlaskRedis.from_custom_provider(FakeStrictRedis, app) @@ -122,7 +122,6 @@ def version_callback(path): def test_heartbeat(app, dockerflow): - # app.debug = True response = app.test_client().get("/__heartbeat__") assert response.status_code == 200 @@ -283,7 +282,8 @@ def assert_log_record(record, errno=0, level=logging.INFO): headers = {"User-Agent": "dockerflow/tests", "Accept-Language": "tlh"} -def test_request_summary(caplog, app, client, setup_request_summary_logger): +@pytest.mark.usefixtures("_setup_request_summary_logger") +def test_request_summary(caplog, app, client): caplog.set_level(logging.INFO) with app.test_request_context("/"): client.get("/", headers=headers) @@ -297,7 +297,8 @@ def test_request_summary(caplog, app, client, setup_request_summary_logger): assert getattr(request, "uid", None) is None -def test_request_summary_querystring(caplog, app, client, setup_request_summary_logger): +@pytest.mark.usefixtures("_setup_request_summary_logger") +def test_request_summary_querystring(caplog, app, client): app.config["DOCKERFLOW_SUMMARY_LOG_QUERYSTRING"] = True caplog.set_level(logging.INFO) with app.test_request_context("/"): @@ -498,7 +499,7 @@ def test_check_migrations_applied_unapplied_migrations(mocker, app, db, migrate) @pytest.mark.parametrize( - "exception,error", + ("exception", "error"), [ (redis.ConnectionError, health.ERROR_CANNOT_CONNECT_REDIS), (redis.RedisError, health.ERROR_REDIS_EXCEPTION), diff --git a/tests/requirements/lint.txt b/tests/requirements/lint.txt index dd54e5a..eb4e60a 100644 --- a/tests/requirements/lint.txt +++ b/tests/requirements/lint.txt @@ -1,5 +1,3 @@ -flake8 -flake8-black -flake8-isort +ruff twine check-manifest diff --git a/tests/sanic/test_sanic.py b/tests/sanic/test_sanic.py index 14ea640..c07cde7 100644 --- a/tests/sanic/test_sanic.py +++ b/tests/sanic/test_sanic.py @@ -51,7 +51,7 @@ async def fake_redis(*args, **kw): return FakeRedis(*args, **kw) -@pytest.fixture(scope="function") +@pytest.fixture() def app(): app = Sanic(f"dockerflow-{uuid.uuid4().hex}") @@ -64,24 +64,24 @@ async def root(request): return app -@pytest.fixture +@pytest.fixture() def dockerflow(app): return Dockerflow(app) @pytest.fixture() -def setup_request_summary_logger(dockerflow): +def _setup_request_summary_logger(dockerflow): dockerflow.summary_logger.addHandler(logging.NullHandler()) dockerflow.summary_logger.setLevel(logging.INFO) -@pytest.fixture +@pytest.fixture() def dockerflow_redis(app): app.config["REDIS"] = {"address": "redis://:password@localhost:6379/0"} return Dockerflow(app, redis=SanicRedis(app)) -@pytest.fixture +@pytest.fixture() def test_client(app): return SanicTestClient(app) @@ -210,7 +210,7 @@ def test_redis_check(dockerflow_redis, mocker, test_client): @pytest.mark.parametrize( - "error,messages", + ("error", "messages"), [ ( "connection", @@ -251,16 +251,16 @@ def assert_log_record(caplog, errno=0, level=logging.INFO, rid=None, t=int, path headers = {"User-Agent": "dockerflow/tests", "Accept-Language": "tlh"} -def test_request_summary(caplog, setup_request_summary_logger, test_client): +@pytest.mark.usefixtures("_setup_request_summary_logger") +def test_request_summary(caplog, test_client): request, _ = test_client.get(headers=headers) assert isinstance(request.ctx.start_timestamp, float) assert request.ctx.id is not None assert_log_record(caplog, rid=request.ctx.id) -def test_request_summary_querystring( - app, caplog, setup_request_summary_logger, test_client -): +@pytest.mark.usefixtures("_setup_request_summary_logger") +def test_request_summary_querystring(app, caplog, test_client): app.config["DOCKERFLOW_SUMMARY_LOG_QUERYSTRING"] = True _, _ = test_client.get("/?x=شكر", headers=headers) records = [r for r in caplog.records if r.name == "request.summary"] @@ -281,9 +281,8 @@ def exception_raiser(request): assert record.getMessage() == "exception message" -def test_request_summary_failed_request( - app, caplog, setup_request_summary_logger, test_client -): +@pytest.mark.usefixtures("_setup_request_summary_logger") +def test_request_summary_failed_request(app, caplog, test_client): @app.middleware def hostile_callback(request): # simulating resetting request changes diff --git a/tox.ini b/tox.ini index e76aa0d..c8fd79c 100644 --- a/tox.ini +++ b/tox.ini @@ -59,12 +59,8 @@ pip_pre = false basepython = python3.8 deps = -rtests/requirements/lint.txt commands = - flake8 src/dockerflow tests/ + ruff check src/ tests/ + ruff format src/ tests/ check-manifest -v python setup.py sdist twine check dist/* - -[flake8] -exclude = - .tox -ignore=E501,E127,E128,E124