diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 79003f1b..50de60bf 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,14 +1,6 @@ repos: - - repo: local - hooks: - - id: pylint - name: pylint - entry: poetry run pylint - language: system - exclude: ^migrations/ - types: [python] - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v2.1.0 + rev: v4.6.0 hooks: - id: check-added-large-files - id: check-json @@ -23,45 +15,38 @@ repos: ] exclude: 'k8s/secret.json' - id: trailing-whitespace + - repo: local hooks: - - id: mypy - name: mypy - entry: poetry run mypy - language: system - types: [python] - - repo: local - hooks: - - id: bandit - name: bandit - entry: poetry run bandit - args: [-lll, --recursive] - language: system - - repo: local - hooks: - - id: detect-secrets - name: detect-secrets - entry: poetry run detect-secrets-hook - args: ['--baseline', '.secrets.baseline'] - exclude: "poetry.lock" - language: system - - repo: https://github.com/asottile/seed-isort-config - rev: v1.9.3 - hooks: - - id: seed-isort-config - - repo: local - hooks: - - id: isort - name: isort - entry: poetry run isort - args: ["--recursive", "--settings-path", "./pyproject.toml", "."] - language: system - types: [python] - - repo: local - hooks: - - id: black - name: black - entry: poetry run black - args: ["--config","./pyproject.toml", "."] - types: [python] - language: system + - id: lint + name: lint + entry: bin/lint.sh + args: [lint] + language: system + types: [python] + + - id: mypy + name: mypy + entry: bin/lint.sh + args: [mypy] + language: system + types: [python] + + - id: bandit + name: bandit + entry: bin/lint.sh + args: [bandit] + language: system + + - id: detect-secrets + name: detect-secrets + entry: bin/lint.sh + args: [detect-secrets] + language: system + + - id: format + name: format + entry: bin/lint.sh + args: [format] + language: system + types: [python] diff --git a/Makefile b/Makefile index 8e0b05bb..a90a92df 100644 --- a/Makefile +++ b/Makefile @@ -21,7 +21,7 @@ help: @echo " build - build docker containers" @echo " db-only - run PostgreSQL server" @echo " lint - lint check for code" - @echo " format - run formatters (black, isort), fix in place" + @echo " format - run formatter, fix in place" @echo " setup - (re)create the database" @echo " shell - open a shell in the web container" @echo " start - run the API service" @@ -53,8 +53,7 @@ lint: $(INSTALL_STAMP) .PHONY: format format: $(INSTALL_STAMP) - bin/lint.sh black --fix - bin/lint.sh isort --fix + bin/lint.sh format --fix .PHONY: db-only db-only: .env diff --git a/bin/lint.sh b/bin/lint.sh index 65fba6ea..ba299f72 100755 --- a/bin/lint.sh +++ b/bin/lint.sh @@ -6,76 +6,76 @@ POETRY_RUN="poetry run" CURRENT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) BASE_DIR="$(dirname "$CURRENT_DIR")" -bandit () { - $POETRY_RUN bandit -lll --recursive "${BASE_DIR}" --exclude "${BASE_DIR}/poetry.lock,${BASE_DIR}/.venv,${BASE_DIR}/.mypy,${BASE_DIR}/build" -} -black () { - $POETRY_RUN black ${check:+--check} "${BASE_DIR}" -} -detect_secrets () { - # Scan only files fixed into the repo, omit poetry.lock - FILES_TO_SCAN=`git ls-tree --full-tree -r --name-only HEAD | grep -v poetry.lock` - $POETRY_RUN detect-secrets-hook $FILES_TO_SCAN --baseline .secrets.baseline -} -isort () { - $POETRY_RUN isort ${check:+--check-only} "${BASE_DIR}" -} -pylint () { - $POETRY_RUN pylint "${BASE_DIR}/ctms" "${BASE_DIR}/tests" -} -mypy () { - $POETRY_RUN mypy "${BASE_DIR}/ctms" -} + +BANDIT_CMD="$POETRY_RUN bandit -lll --recursive ${BASE_DIR} --exclude ${BASE_DIR}/poetry.lock,${BASE_DIR}/.venv,${BASE_DIR}/.mypy,${BASE_DIR}/build" + +FORMAT_CMD="$POETRY_RUN ruff format $BASE_DIR" + +# Scan only files fixed into the repo, omit poetry.lock +DETECT_SECRETS_FILES="$(git ls-tree --full-tree -r --name-only HEAD | grep -v poetry.lock)" +DETECT_SECRETS_CMD="$POETRY_RUN detect-secrets-hook $DETECT_SECRETS_FILES --baseline .secrets.baseline" + +LINT_CMD="$POETRY_RUN ruff check $BASE_DIR" + +MYPY_CMD="$POETRY_RUN mypy ${BASE_DIR}/ctms" + + all () { - echo "running black" - black - echo "running isort" - isort + echo "running format (check only)" + $FORMAT_CMD + echo "running lint" + $LINT_CMD echo "running mypy" - mypy - echo "running pylint" - pylint + $MYPY_CMD echo "running bandit" - bandit + $BANDIT_CMD echo "running detect_secrets" - detect_secrets + $DETECT_SECRETS_CMD } usage () { - echo "Usage: bin/lint.sh [OPTION]" - echo " run linting checks" - echo "Options": + echo "Usage: bin/lint.sh [subcommand] [--fix]" + echo " run linting checks, and optionally fix in place (if available)" + echo "Subcommand": echo " bandit" - echo " black [--fix]" echo " detect-secrets" - echo " isort [--fix]" + echo " format" + echo " lint" echo " mypy" - echo " pylint" } -subcommand=''; -check="true" -if [ -z $1 ]; then +if [ -z "$1" ]; then all else subcommand=$1; shift case $subcommand in - "black" | "isort") - case $1 in - "--fix") - check="" - ;; - esac - case $subcommand in - "isort") isort;; - "black") black;; - esac - ;; - - "pylint") pylint;; - "mypy") mypy;; - "bandit") bandit;; - "detect-secrets") detect_secrets;; - *) usage;; + "format") + if [ -n "$1" ] && [ "$1" != "--fix" ]; then + usage + else + check_flag="--check" + [ "$1" = "--fix" ] && check_flag="" + $FORMAT_CMD ${check_flag:-} + fi + ;; + "lint") + if [ -n "$1" ] && [ "$1" != "--fix" ]; then + usage + else + $LINT_CMD ${1:-} + fi + ;; + "mypy") + $MYPY_CMD + ;; + "bandit") + $BANDIT_CMD + ;; + "detect-secrets") + $DETECT_SECRETS_CMD + ;; + *) + usage + ;; esac fi diff --git a/ctms/backport_legacy_waitlists.py b/ctms/backport_legacy_waitlists.py index 512a713a..bab24759 100644 --- a/ctms/backport_legacy_waitlists.py +++ b/ctms/backport_legacy_waitlists.py @@ -8,7 +8,7 @@ from .schemas import RelayWaitlistInSchema, VpnWaitlistInSchema, WaitlistInSchema -def format_legacy_vpn_relay_waitlist_input( +def format_legacy_vpn_relay_waitlist_input( # noqa: PLR0912,PLR0915 db: Session, email_id: UUID4, input_data, schema_class, metrics: Optional[Dict] ): """ @@ -100,42 +100,38 @@ def format_legacy_vpn_relay_waitlist_input( to_update.append( WaitlistInSchema(name=waitlist.name, subscribed=False) ) - else: - # `relay_waitlist` field was specified. - if input_relay_newsletters: - # We are subscribing to a `relay-*-waitlist` newsletter. - # We want to keep the other Relay waitlists already subscribed. - for waitlist in relay_waitlists: - to_update.append( - WaitlistInSchema( - name=waitlist.name, - subscribed=waitlist.subscribed, - fields=waitlist.fields, - ) - ) - for newsletter in input_relay_newsletters: - name = newsletter["name"].replace("-waitlist", "") - to_update.append( - WaitlistInSchema( - name=name, fields={"geo": parsed_relay.geo} - ) + elif input_relay_newsletters: + # We are subscribing to a `relay-*-waitlist` newsletter. + # We want to keep the other Relay waitlists already subscribed. + for waitlist in relay_waitlists: + to_update.append( + WaitlistInSchema( + name=waitlist.name, + subscribed=waitlist.subscribed, + fields=waitlist.fields, ) - elif len(relay_waitlists) == 0: - # We are subscribing to the `relay` waitlist for the first time. + ) + for newsletter in input_relay_newsletters: + name = newsletter["name"].replace("-waitlist", "") to_update.append( - WaitlistInSchema(name="relay", fields={"geo": parsed_relay.geo}) + WaitlistInSchema(name=name, fields={"geo": parsed_relay.geo}) ) - else: - # `relay_waitlist` was specified but without newsletter, hence update geo field - # of all subscribed Relay waitlists. - for waitlist in relay_waitlists: - to_update.append( - WaitlistInSchema( - name=waitlist.name, - source=waitlist.source, - fields={"geo": parsed_relay.geo}, - ) + elif len(relay_waitlists) == 0: + # We are subscribing to the `relay` waitlist for the first time. + to_update.append( + WaitlistInSchema(name="relay", fields={"geo": parsed_relay.geo}) + ) + else: + # `relay_waitlist` was specified but without newsletter, hence update geo field + # of all subscribed Relay waitlists. + for waitlist in relay_waitlists: + to_update.append( + WaitlistInSchema( + name=waitlist.name, + source=waitlist.source, + fields={"geo": parsed_relay.geo}, ) + ) if to_update: formatted["waitlists"] = [wl.model_dump() for wl in to_update] diff --git a/ctms/bin/client_credentials.py b/ctms/bin/client_credentials.py index 6efa9cb2..7bb13949 100755 --- a/ctms/bin/client_credentials.py +++ b/ctms/bin/client_credentials.py @@ -86,7 +86,7 @@ def print_new_credentials( ) -def main(db, settings, test_args=None): +def main(db, settings, test_args=None): # noqa: PLR0912 """ Process the command line and create or update client credentials diff --git a/ctms/bin/delete_bulk.py b/ctms/bin/delete_bulk.py index 8cb9bd50..92bcbf0f 100755 --- a/ctms/bin/delete_bulk.py +++ b/ctms/bin/delete_bulk.py @@ -79,4 +79,4 @@ def delete(obj, email, email_file): if __name__ == "__main__": - cli() # pylint: disable=no-value-for-parameter + cli() diff --git a/ctms/crud.py b/ctms/crud.py index a2c37fc0..e1cdc9af 100644 --- a/ctms/crud.py +++ b/ctms/crud.py @@ -47,7 +47,7 @@ def ping(db: Session): try: db.execute(text("SELECT 1")) return True - except Exception as exc: # pylint:disable = broad-exception-caught + except Exception as exc: logger.exception(exc) return False @@ -119,15 +119,14 @@ def get_all_contacts_from_ids(db, email_ids): def get_bulk_query(start_time, end_time, after_email_uuid, mofo_relevant): filters = [ - # pylint: disable-next=comparison-with-callable Email.update_timestamp >= start_time, - Email.update_timestamp < end_time, # pylint: disable=comparison-with-callable + Email.update_timestamp < end_time, Email.email_id != after_email_uuid, ] if mofo_relevant is False: filters.append( or_( - Email.mofo == None, # pylint: disable=C0121 + Email.mofo == None, Email.mofo.has(mofo_relevant=mofo_relevant), ) ) @@ -487,7 +486,7 @@ def _update_orm(orm: Base, update_dict: dict): setattr(orm, key, value) -def update_contact( +def update_contact( # noqa: PLR0912 db: Session, email: Email, update_data: dict, metrics: Optional[Dict] ) -> None: """Update an existing contact using a sparse update dictionary""" @@ -510,15 +509,14 @@ def update_contact( if existing: db.delete(existing) setattr(email, group_name, None) + elif existing is None: + new = creator(db, email_id, schema(**update_data[group_name])) + setattr(email, group_name, new) else: - if existing is None: - new = creator(db, email_id, schema(**update_data[group_name])) - setattr(email, group_name, new) - else: - _update_orm(existing, update_data[group_name]) - if schema.model_validate(existing).is_default(): - db.delete(existing) - setattr(email, group_name, None) + _update_orm(existing, update_data[group_name]) + if schema.model_validate(existing).is_default(): + db.delete(existing) + setattr(email, group_name, None) update_data = format_legacy_vpn_relay_waitlist_input( db, email_id, update_data, dict, metrics @@ -585,7 +583,7 @@ def get_active_api_client_ids(db: Session) -> List[str]: def update_api_client_last_access(db: Session, api_client: ApiClient): - api_client.last_access = func.now() # pylint: disable=not-callable + api_client.last_access = func.now() db.add(api_client) diff --git a/ctms/exception_capture.py b/ctms/exception_capture.py index 31208197..d01d29c8 100644 --- a/ctms/exception_capture.py +++ b/ctms/exception_capture.py @@ -24,7 +24,7 @@ def init_sentry(): sentry_debug = False version_info = config.get_version() - # pylint: disable=abstract-class-instantiated + sentry_sdk.init( release=version_info["version"], debug=sentry_debug, diff --git a/ctms/metrics.py b/ctms/metrics.py index 3be58ce8..82679496 100644 --- a/ctms/metrics.py +++ b/ctms/metrics.py @@ -74,13 +74,12 @@ def set_metrics(metrics: Any) -> None: - global METRICS # pylint: disable=global-statement + global METRICS # noqa: PLW0603 METRICS = metrics -# pylint: disable-next=unnecessary-lambda-assignment get_metrics_registry = lambda: METRICS_REGISTRY -get_metrics = lambda: METRICS # pylint: disable=unnecessary-lambda-assignment +get_metrics = lambda: METRICS oauth2_scheme = OAuth2ClientCredentials(tokenUrl="token") token_scheme = HTTPBasic(auto_error=False) diff --git a/ctms/models.py b/ctms/models.py index e822a9d2..dfffc526 100644 --- a/ctms/models.py +++ b/ctms/models.py @@ -24,31 +24,29 @@ class Base(DeclarativeBase): pass -class CaseInsensitiveComparator( - Comparator -): # pylint: disable=abstract-method,too-many-ancestors +class CaseInsensitiveComparator(Comparator): def __eq__(self, other): return func.lower(self.__clause_element__()) == func.lower(other) class TimestampMixin: @declared_attr - def create_timestamp(cls): # pylint: disable=no-self-argument + def create_timestamp(cls): return mapped_column( TIMESTAMP(timezone=True), nullable=False, - server_default=func.now(), # pylint: disable=not-callable + server_default=func.now(), ) @declared_attr - def update_timestamp(cls): # pylint: disable=no-self-argument + def update_timestamp(cls): return mapped_column( TIMESTAMP(timezone=True), nullable=False, - server_default=func.now(), # pylint: disable=not-callable + server_default=func.now(), # server_onupdate would be nice to use here, but it's not supported # by Postgres - onupdate=func.now(), # pylint: disable=not-callable + onupdate=func.now(), ) @@ -87,7 +85,7 @@ def primary_email_insensitive(self): return self.primary_email.lower() @primary_email_insensitive.comparator - def primary_email_insensitive_comparator(cls): # pylint: disable=no-self-argument + def primary_email_insensitive_comparator(cls): return CaseInsensitiveComparator(cls.primary_email) # Indexes @@ -158,7 +156,7 @@ def fxa_primary_email_insensitive(self): @fxa_primary_email_insensitive.comparator def fxa_primary_email_insensitive_comparator( cls, - ): # pylint: disable=no-self-argument + ): return CaseInsensitiveComparator(cls.primary_email) # Indexes diff --git a/ctms/routers/contacts.py b/ctms/routers/contacts.py index c45f1b78..e8d0d988 100644 --- a/ctms/routers/contacts.py +++ b/ctms/routers/contacts.py @@ -216,7 +216,7 @@ def create_ctms_contact( try: create_contact(db, email_id, contact, get_metrics()) db.commit() - except Exception as e: # pylint:disable = W0703 + except Exception as e: db.rollback() if isinstance(e, IntegrityError): raise HTTPException(status_code=409, detail="Contact already exists") from e @@ -258,7 +258,7 @@ def create_or_update_ctms_contact( try: create_or_update_contact(db, email_id, contact, get_metrics()) db.commit() - except Exception as e: # pylint:disable = W0703 + except Exception as e: db.rollback() if isinstance(e, IntegrityError): raise HTTPException( @@ -305,7 +305,7 @@ def partial_update_ctms_contact( try: db.commit() - except Exception as e: # pylint:disable = W0703 + except Exception as e: db.rollback() if isinstance(e, IntegrityError): raise HTTPException( diff --git a/ctms/schemas/bulk.py b/ctms/schemas/bulk.py index 208be7fc..8010414a 100644 --- a/ctms/schemas/bulk.py +++ b/ctms/schemas/bulk.py @@ -55,7 +55,7 @@ def mofo_relevant_must_not_be_blank(cls, value): after: Optional[str] = Field(default=None, validate_default=True) @field_validator("after", mode="before") - def after_must_be_base64_decodable(cls, value): # pylint: disable=no-self-argument + def after_must_be_base64_decodable(cls, value): if value in BLANK_VALS: return None # Default try: diff --git a/ctms/schemas/contact.py b/ctms/schemas/contact.py index cb5d3dab..a6d91bd1 100644 --- a/ctms/schemas/contact.py +++ b/ctms/schemas/contact.py @@ -291,7 +291,7 @@ def set_default_mofo(cls, value): @model_validator(mode="before") @classmethod - def legacy_waitlists(cls, values): # pylint: disable=no-self-argument + def legacy_waitlists(cls, values): # Show computed fields in response for retro-compatibility. values["vpn_waitlist"] = VpnWaitlistSchema() values["relay_waitlist"] = RelayWaitlistSchema() @@ -301,7 +301,7 @@ def legacy_waitlists(cls, values): # pylint: disable=no-self-argument continue if isinstance(waitlist, dict): # TODO: figure out why dict from `response_model` decorators param in app.py) - waitlist = WaitlistSchema(**waitlist) + waitlist = WaitlistSchema(**waitlist) # noqa: PLW2901 if waitlist.name == "vpn": values["vpn_waitlist"] = VpnWaitlistSchema( geo=waitlist.fields.get("geo"), diff --git a/ctms/schemas/waitlist.py b/ctms/schemas/waitlist.py index 935e940f..c3ebbd4d 100644 --- a/ctms/schemas/waitlist.py +++ b/ctms/schemas/waitlist.py @@ -115,7 +115,7 @@ class WaitlistTableSchema(WaitlistTimestampedSchema): model_config = ConfigDict(extra="forbid") -def CountryField(): # pylint:disable = invalid-name +def CountryField(): return Field( default=None, max_length=100, @@ -124,7 +124,7 @@ def CountryField(): # pylint:disable = invalid-name ) -def PlatformField(): # pylint:disable = invalid-name +def PlatformField(): return Field( default=None, max_length=100, @@ -134,7 +134,7 @@ def PlatformField(): # pylint:disable = invalid-name def validate_waitlist_newsletters( - contact: Union["ContactInBase", "ContactPatchSchema"] + contact: Union["ContactInBase", "ContactPatchSchema"], ): """ This helper validates that when subscribing to `relay-*-waitlist` diff --git a/poetry.lock b/poetry.lock index 8bfd27af..07add3a6 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. [[package]] name = "alabaster" @@ -132,17 +132,6 @@ files = [ [package.extras] tests = ["mypy (>=0.800)", "pytest", "pytest-asyncio"] -[[package]] -name = "astroid" -version = "3.2.4" -description = "An abstract syntax tree for Python with inference support." -optional = false -python-versions = ">=3.8.0" -files = [ - {file = "astroid-3.2.4-py3-none-any.whl", hash = "sha256:413658a61eeca6202a59231abb473f932038fbcbf1666587f66d482083413a25"}, - {file = "astroid-3.2.4.tar.gz", hash = "sha256:0e14202810b30da1b735827f78f5157be2bbd4a7a59b7707ca0bfc2fb4c0063a"}, -] - [[package]] name = "babel" version = "2.15.0" @@ -192,50 +181,6 @@ test = ["beautifulsoup4 (>=4.8.0)", "coverage (>=4.5.4)", "fixtures (>=3.0.0)", toml = ["tomli (>=1.1.0)"] yaml = ["PyYAML"] -[[package]] -name = "black" -version = "24.4.2" -description = "The uncompromising code formatter." -optional = false -python-versions = ">=3.8" -files = [ - {file = "black-24.4.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dd1b5a14e417189db4c7b64a6540f31730713d173f0b63e55fabd52d61d8fdce"}, - {file = "black-24.4.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e537d281831ad0e71007dcdcbe50a71470b978c453fa41ce77186bbe0ed6021"}, - {file = "black-24.4.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eaea3008c281f1038edb473c1aa8ed8143a5535ff18f978a318f10302b254063"}, - {file = "black-24.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:7768a0dbf16a39aa5e9a3ded568bb545c8c2727396d063bbaf847df05b08cd96"}, - {file = "black-24.4.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:257d724c2c9b1660f353b36c802ccece186a30accc7742c176d29c146df6e474"}, - {file = "black-24.4.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bdde6f877a18f24844e381d45e9947a49e97933573ac9d4345399be37621e26c"}, - {file = "black-24.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e151054aa00bad1f4e1f04919542885f89f5f7d086b8a59e5000e6c616896ffb"}, - {file = "black-24.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:7e122b1c4fb252fd85df3ca93578732b4749d9be076593076ef4d07a0233c3e1"}, - {file = "black-24.4.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:accf49e151c8ed2c0cdc528691838afd217c50412534e876a19270fea1e28e2d"}, - {file = "black-24.4.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:88c57dc656038f1ab9f92b3eb5335ee9b021412feaa46330d5eba4e51fe49b04"}, - {file = "black-24.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be8bef99eb46d5021bf053114442914baeb3649a89dc5f3a555c88737e5e98fc"}, - {file = "black-24.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:415e686e87dbbe6f4cd5ef0fbf764af7b89f9057b97c908742b6008cc554b9c0"}, - {file = "black-24.4.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:bf10f7310db693bb62692609b397e8d67257c55f949abde4c67f9cc574492cc7"}, - {file = "black-24.4.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:98e123f1d5cfd42f886624d84464f7756f60ff6eab89ae845210631714f6db94"}, - {file = "black-24.4.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:48a85f2cb5e6799a9ef05347b476cce6c182d6c71ee36925a6c194d074336ef8"}, - {file = "black-24.4.2-cp38-cp38-win_amd64.whl", hash = "sha256:b1530ae42e9d6d5b670a34db49a94115a64596bc77710b1d05e9801e62ca0a7c"}, - {file = "black-24.4.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:37aae07b029fa0174d39daf02748b379399b909652a806e5708199bd93899da1"}, - {file = "black-24.4.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:da33a1a5e49c4122ccdfd56cd021ff1ebc4a1ec4e2d01594fef9b6f267a9e741"}, - {file = "black-24.4.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef703f83fc32e131e9bcc0a5094cfe85599e7109f896fe8bc96cc402f3eb4b6e"}, - {file = "black-24.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:b9176b9832e84308818a99a561e90aa479e73c523b3f77afd07913380ae2eab7"}, - {file = "black-24.4.2-py3-none-any.whl", hash = "sha256:d36ed1124bb81b32f8614555b34cc4259c3fbc7eec17870e8ff8ded335b58d8c"}, - {file = "black-24.4.2.tar.gz", hash = "sha256:c872b53057f000085da66a19c55d68f6f8ddcac2642392ad3a355878406fbd4d"}, -] - -[package.dependencies] -click = ">=8.0.0" -mypy-extensions = ">=0.4.3" -packaging = ">=22.0" -pathspec = ">=0.9.0" -platformdirs = ">=2" - -[package.extras] -colorama = ["colorama (>=0.4.3)"] -d = ["aiohttp (>=3.7.4)", "aiohttp (>=3.7.4,!=3.9.0)"] -jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] -uvloop = ["uvloop (>=0.15.2)"] - [[package]] name = "cachetools" version = "5.4.0" @@ -589,21 +534,6 @@ requests = "*" gibberish = ["gibberish-detector"] word-list = ["pyahocorasick"] -[[package]] -name = "dill" -version = "0.3.8" -description = "serialize all of Python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "dill-0.3.8-py3-none-any.whl", hash = "sha256:c36ca9ffb54365bdd2f8eb3eff7d2a21237f8452b57ace88b1ac615b7e815bd7"}, - {file = "dill-0.3.8.tar.gz", hash = "sha256:3ebe3c479ad625c4553aca177444d89b486b1d84982eeacded644afc0cf797ca"}, -] - -[package.extras] -graph = ["objgraph (>=1.7.2)"] -profile = ["gprof2dot (>=2022.7.29)"] - [[package]] name = "distlib" version = "0.3.8" @@ -1023,20 +953,6 @@ files = [ {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, ] -[[package]] -name = "isort" -version = "5.13.2" -description = "A Python utility / library to sort Python imports." -optional = false -python-versions = ">=3.8.0" -files = [ - {file = "isort-5.13.2-py3-none-any.whl", hash = "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6"}, - {file = "isort-5.13.2.tar.gz", hash = "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109"}, -] - -[package.extras] -colors = ["colorama (>=0.4.6)"] - [[package]] name = "jinja2" version = "3.1.4" @@ -1324,17 +1240,6 @@ files = [ {file = "MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b"}, ] -[[package]] -name = "mccabe" -version = "0.7.0" -description = "McCabe checker, plugin for flake8" -optional = false -python-versions = ">=3.6" -files = [ - {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, - {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, -] - [[package]] name = "mdurl" version = "0.1.2" @@ -1445,17 +1350,6 @@ bcrypt = ["bcrypt (>=3.1.0)"] build-docs = ["cloud-sptheme (>=1.10.1)", "sphinx (>=1.6)", "sphinxcontrib-fulltoc (>=1.2.0)"] totp = ["cryptography"] -[[package]] -name = "pathspec" -version = "0.12.1" -description = "Utility library for gitignore style pattern matching of file paths." -optional = false -python-versions = ">=3.8" -files = [ - {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, - {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, -] - [[package]] name = "pbr" version = "6.0.0" @@ -1745,61 +1639,22 @@ files = [ [package.extras] windows-terminal = ["colorama (>=0.4.6)"] -[[package]] -name = "pylint" -version = "3.2.6" -description = "python code static checker" -optional = false -python-versions = ">=3.8.0" -files = [ - {file = "pylint-3.2.6-py3-none-any.whl", hash = "sha256:03c8e3baa1d9fb995b12c1dbe00aa6c4bcef210c2a2634374aedeb22fb4a8f8f"}, - {file = "pylint-3.2.6.tar.gz", hash = "sha256:a5d01678349454806cff6d886fb072294f56a58c4761278c97fb557d708e1eb3"}, -] - -[package.dependencies] -astroid = ">=3.2.4,<=3.3.0-dev0" -colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} -dill = {version = ">=0.3.7", markers = "python_version >= \"3.12\""} -isort = ">=4.2.5,<5.13.0 || >5.13.0,<6" -mccabe = ">=0.6,<0.8" -platformdirs = ">=2.2.0" -tomlkit = ">=0.10.1" - -[package.extras] -spelling = ["pyenchant (>=3.2,<4.0)"] -testutils = ["gitpython (>3)"] - -[[package]] -name = "pylint-pytest" -version = "1.1.8" -description = "A Pylint plugin to suppress pytest-related false positives." -optional = false -python-versions = ">=3.6" -files = [ - {file = "pylint_pytest-1.1.8-py3-none-any.whl", hash = "sha256:8a532c1709c161406b8459bfc414c57a32a4ab488efd4bc97a22a21cb235331f"}, - {file = "pylint_pytest-1.1.8.tar.gz", hash = "sha256:5c862c88870aa8eb1b376df48ab9e820b5bdadfe37d965aa9ac567a6305955ca"}, -] - -[package.dependencies] -pylint = ">=2" -pytest = ">=4.6,<=8.2.0" - [[package]] name = "pytest" -version = "8.2.0" +version = "8.3.2" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.8" files = [ - {file = "pytest-8.2.0-py3-none-any.whl", hash = "sha256:1733f0620f6cda4095bbf0d9ff8022486e91892245bb9e7d5542c018f612f233"}, - {file = "pytest-8.2.0.tar.gz", hash = "sha256:d507d4482197eac0ba2bae2e9babf0672eb333017bcedaa5fb1a3d42c1174b3f"}, + {file = "pytest-8.3.2-py3-none-any.whl", hash = "sha256:4ba08f9ae7dcf84ded419494d229b48d0903ea6407b030eaec46df5e6a73bba5"}, + {file = "pytest-8.3.2.tar.gz", hash = "sha256:c132345d12ce551242c87269de812483f5bcc87cdbb4722e48487ba194f9fdce"}, ] [package.dependencies] colorama = {version = "*", markers = "sys_platform == \"win32\""} iniconfig = "*" packaging = "*" -pluggy = ">=1.5,<2.0" +pluggy = ">=1.5,<2" [package.extras] dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] @@ -1999,6 +1854,33 @@ files = [ [package.dependencies] pyasn1 = ">=0.1.3" +[[package]] +name = "ruff" +version = "0.5.5" +description = "An extremely fast Python linter and code formatter, written in Rust." +optional = false +python-versions = ">=3.7" +files = [ + {file = "ruff-0.5.5-py3-none-linux_armv6l.whl", hash = "sha256:605d589ec35d1da9213a9d4d7e7a9c761d90bba78fc8790d1c5e65026c1b9eaf"}, + {file = "ruff-0.5.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:00817603822a3e42b80f7c3298c8269e09f889ee94640cd1fc7f9329788d7bf8"}, + {file = "ruff-0.5.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:187a60f555e9f865a2ff2c6984b9afeffa7158ba6e1eab56cb830404c942b0f3"}, + {file = "ruff-0.5.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fe26fc46fa8c6e0ae3f47ddccfbb136253c831c3289bba044befe68f467bfb16"}, + {file = "ruff-0.5.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4ad25dd9c5faac95c8e9efb13e15803cd8bbf7f4600645a60ffe17c73f60779b"}, + {file = "ruff-0.5.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f70737c157d7edf749bcb952d13854e8f745cec695a01bdc6e29c29c288fc36e"}, + {file = "ruff-0.5.5-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:cfd7de17cef6ab559e9f5ab859f0d3296393bc78f69030967ca4d87a541b97a0"}, + {file = "ruff-0.5.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a09b43e02f76ac0145f86a08e045e2ea452066f7ba064fd6b0cdccb486f7c3e7"}, + {file = "ruff-0.5.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d0b856cb19c60cd40198be5d8d4b556228e3dcd545b4f423d1ad812bfdca5884"}, + {file = "ruff-0.5.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3687d002f911e8a5faf977e619a034d159a8373514a587249cc00f211c67a091"}, + {file = "ruff-0.5.5-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:ac9dc814e510436e30d0ba535f435a7f3dc97f895f844f5b3f347ec8c228a523"}, + {file = "ruff-0.5.5-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:af9bdf6c389b5add40d89b201425b531e0a5cceb3cfdcc69f04d3d531c6be74f"}, + {file = "ruff-0.5.5-py3-none-musllinux_1_2_i686.whl", hash = "sha256:d40a8533ed545390ef8315b8e25c4bb85739b90bd0f3fe1280a29ae364cc55d8"}, + {file = "ruff-0.5.5-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:cab904683bf9e2ecbbe9ff235bfe056f0eba754d0168ad5407832928d579e7ab"}, + {file = "ruff-0.5.5-py3-none-win32.whl", hash = "sha256:696f18463b47a94575db635ebb4c178188645636f05e934fdf361b74edf1bb2d"}, + {file = "ruff-0.5.5-py3-none-win_amd64.whl", hash = "sha256:50f36d77f52d4c9c2f1361ccbfbd09099a1b2ea5d2b2222c586ab08885cf3445"}, + {file = "ruff-0.5.5-py3-none-win_arm64.whl", hash = "sha256:3191317d967af701f1b73a31ed5788795936e423b7acce82a2b63e26eb3e89d6"}, + {file = "ruff-0.5.5.tar.gz", hash = "sha256:cc5516bdb4858d972fbc31d246bdb390eab8df1a26e2353be2dbc0c2d7f5421a"}, +] + [[package]] name = "sentry-sdk" version = "2.11.0" @@ -2373,17 +2255,6 @@ docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphi tests = ["freezegun (>=0.2.8)", "pretend", "pytest (>=6.0)", "pytest-asyncio (>=0.17)", "simplejson"] typing = ["mypy (>=1.4)", "rich", "twisted"] -[[package]] -name = "tomlkit" -version = "0.13.0" -description = "Style preserving TOML library" -optional = false -python-versions = ">=3.8" -files = [ - {file = "tomlkit-0.13.0-py3-none-any.whl", hash = "sha256:7075d3042d03b80f603482d69bf0c8f345c2b30e41699fd8883227f89972b264"}, - {file = "tomlkit-0.13.0.tar.gz", hash = "sha256:08ad192699734149f5b97b45f1f18dad7eb1b6d16bc72ad0c2335772650d7b72"}, -] - [[package]] name = "types-python-dateutil" version = "2.9.0.20240316" @@ -2697,4 +2568,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.12.1" -content-hash = "f47cfc6fe43c0c5b42c0ea4bae2104b97e94eb0f68885f15d768bacb57bac961" +content-hash = "e51302f9def969e6ccadaec1eb136fd2ae0c35b3e49eb71389845376668cb1ce" diff --git a/pyproject.toml b/pyproject.toml index e8cdf197..aeb3fb3c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,58 +38,30 @@ pydantic-settings = "^2.3.4" pre-commit = "^3.8.0" sphinx = "^7.4.7" pytest = "^8.2.0" -isort = {extras = ["pyproject"], version = "^5.13.2"} -black = "^24.4.2" coverage = {extras = ["toml"], version = "^7.6"} mypy = "^1.11" types-python-dateutil = "^2.9.0" detect-secrets = "^1.5.0" bandit = "^1.7.9" SQLAlchemy-Utils = "^0.41.2" -pylint = "^3.2.6" -pylint-pytest = "^1.1.8" types-requests = "^2.32.0" factory-boy = "^3.3.0" pytest-factoryboy = "^2.7.0" backoff = "^2.2.1" httpx = "^0.27.0" +ruff = "^0.5.5" [tool.pytest.ini_options] testpaths = [ "tests/unit", ] -[tool.pylint] - [tool.pylint.'MESSAGES CONTROL'] - disable = [ - "R0801", # sqla has a lot of duplicate code and it is work to "fix" this - "C0301", # if black is happy with the line-length, let's not mess with it - "R0903", # this is triggered by pydantic's config and is really just a taste thing anyway - "R0913", # we have too-many-arguments in too many places to easily fix - "R0912", # Same idea as above, let's leave it since it is a matter of taste - "W0511", # Let us have our TODOs - "W0613", # sometimes unused arguments are nice for symmetry and also api methods - "R0914", # Leave me alone, pylint. I can have as many locals as I want - - "C0114", # mozilla-it/ctms-api#100 - "C0115", # mozilla-it/ctms-api#100 - "C0116", # mozilla-it/ctms-api#100 - ] - [tool.pylint.REPORTS] - output-format = 'colorized' - [tool.pylint.FORMAT] - good-names = 'e,i,f,db,SessionLocal' # Normally pylint objects to these due to not being snake_case - [tool.pylint.TYPECHECK] - generated-members='alembic.*' - [tool.pylint.MASTER] - extension-pkg-whitelist='pydantic' - load-plugins='pylint_pytest' - ignore='third_party' - ignore-patterns = "migrations/.*" # Not worth fixing yet - -[tool.isort] -profile = "black" +[tool.ruff] +target-version = "py312" +[tool.ruff.lint] +select = ["PL", "I"] +ignore = ["PLR2004", "PLR0913"] [tool.coverage] # https://github.com/nedbat/coveragepy diff --git a/tests/factories/models.py b/tests/factories/models.py index 62c52ead..a6714562 100644 --- a/tests/factories/models.py +++ b/tests/factories/models.py @@ -8,9 +8,6 @@ from ctms.database import ScopedSessionLocal -# Pylint complains that we don't override `evaluate` in the class below, but -# neither does the class that we're subclassing from, hence the disable. -# pylint: disable-next=abstract-method class RelatedFactoryVariableList(factory.RelatedFactoryList): """allows overriding ``size`` during factory usage, e.g. ParentFactory(list_factory__size=4) diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py index 0acdade7..4c900e2c 100644 --- a/tests/unit/conftest.py +++ b/tests/unit/conftest.py @@ -87,7 +87,7 @@ def engine(pytestconfig): else: # Assume the regular database was passed, create a new test database test_db_url = str( - PostgresDsn.build( # pylint: disable=no-member + PostgresDsn.build( scheme=parsed_db_url.scheme, username=parsed_db_url.username, password=parsed_db_url.password, @@ -114,7 +114,6 @@ def engine(pytestconfig): cfg = alembic_config.Config(os.path.join(APP_FOLDER, "alembic.ini")) - # pylint: disable-next=unsupported-assignment-operation cfg.attributes["connection"] = test_engine alembic_command.upgrade(cfg, "head") @@ -624,18 +623,15 @@ def _check_written(field, getter, result_list=False): results is None ), f"{email_id} has field `{field}` but it is _default_ and it should _not_ have been written to db" else: - assert ( - results - ), f"{email_id} has field `{field}` and it should have been written to db" + assert results, f"{email_id} has field `{field}` and it should have been written to db" + elif result_list: + assert ( + results == [] + ), f"{email_id} does not have field `{field}` and it should _not_ have been written to db" else: - if result_list: - assert ( - results == [] - ), f"{email_id} does not have field `{field}` and it should _not_ have been written to db" - else: - assert ( - results is None - ), f"{email_id} does not have field `{field}` and it should _not_ have been written to db" + assert ( + results is None + ), f"{email_id} does not have field `{field}` and it should _not_ have been written to db" if check_written: _check_written("amo", get_amo_by_email_id) @@ -701,16 +697,15 @@ def _check_written(field, getter): results = getter(dbsession, written_id) if sample.model_dump().get(field) and code in {200, 201}: if field in fields_not_written or field in new_default_fields: - assert results is None or ( - isinstance(results, list) and len(results) == 0 + assert ( + results is None + or (isinstance(results, list) and len(results) == 0) ), f"{sample_email_id} has field `{field}` but it is _default_ and it should _not_ have been written to db" else: - assert ( - results - ), f"{sample_email_id} has field `{field}` and it should have been written to db" + assert results, f"{sample_email_id} has field `{field}` and it should have been written to db" else: - assert results is None or ( - isinstance(results, list) and len(results) == 0 + assert ( + results is None or (isinstance(results, list) and len(results) == 0) ), f"{sample_email_id} does not have field `{field}` and it should _not_ have been written to db" if check_written: diff --git a/tests/unit/test_backport_legacy_waitlists.py b/tests/unit/test_backport_legacy_waitlists.py index f82b49da..7e9a8b64 100644 --- a/tests/unit/test_backport_legacy_waitlists.py +++ b/tests/unit/test_backport_legacy_waitlists.py @@ -89,7 +89,6 @@ def test_relay_waitlist_unsubscribed_on_newsletter_unsubscribed( def test_relay_waitlist_unsubscribed_on_all_newsletters_unsubscribed( dbsession, email_factory, waitlist_factory ): - email = email_factory() waitlist_factory(name="relay-phone-masking", fields={"geo": "es"}, email=email) dbsession.commit() diff --git a/tests/unit/test_crud.py b/tests/unit/test_crud.py index b4a76e99..2a7c92b8 100644 --- a/tests/unit/test_crud.py +++ b/tests/unit/test_crud.py @@ -1,6 +1,5 @@ """Test database operations""" -# pylint: disable=too-many-lines from datetime import datetime, timedelta, timezone from uuid import uuid4 diff --git a/tests/unit/test_log.py b/tests/unit/test_log.py index b5e4e099..0f79f17e 100644 --- a/tests/unit/test_log.py +++ b/tests/unit/test_log.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- """Tests for logging helpers""" + from unittest.mock import patch import pytest