From a3c1386b0f613ff6b12a3940b2e9ba5187f070f3 Mon Sep 17 00:00:00 2001 From: Steven Loria Date: Sun, 19 Jan 2025 00:26:02 -0500 Subject: [PATCH 1/2] Update ruff config to match marshmallow and pre-commit auto-update --- .pre-commit-config.yaml | 4 ++-- pyproject.toml | 51 +++++++++++++++++++++++++++++++++-------- 2 files changed, 44 insertions(+), 11 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d50b916..b4a8dac 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,12 +2,12 @@ ci: autoupdate_schedule: monthly repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.8.6 + rev: v0.9.2 hooks: - id: ruff - id: ruff-format - repo: https://github.com/python-jsonschema/check-jsonschema - rev: 0.30.0 + rev: 0.31.0 hooks: - id: check-github-workflows - id: check-readthedocs diff --git a/pyproject.toml b/pyproject.toml index 9a4948a..2fd0174 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -48,7 +48,7 @@ include = ["docs/", "tests/", "CHANGELOG.rst", "CONTRIBUTING.rst", "tox.ini"] exclude = ["docs/_build/"] [tool.ruff] -src = ["src"] +src = ["src", "tests"] fix = true show-fixes = true output-format = "full" @@ -57,14 +57,47 @@ output-format = "full" docstring-code-format = true [tool.ruff.lint] -ignore = ["E203", "E266", "E501", "E731"] -select = [ - "B", # flake8-bugbear - "E", # pycodestyle error - "F", # pyflakes - "I", # isort - "UP", # pyupgrade - "W", # pycodestyle warning +# use all checks available in ruff except the ones explicitly ignored below +select = ["ALL"] +ignore = [ + "A005", # "module {name} shadows a Python standard-library module" + "ANN", # let mypy handle annotation checks + "ARG", # unused arguments are common w/ interfaces + "COM", # let formatter take care commas + "C901", # don't enforce complexity level + "D", # don't require docstrings + "DTZ007", # ignore false positives due to https://github.com/astral-sh/ruff/issues/1306 + "E501", # leave line-length enforcement to formatter + "EM", # allow string messages in exceptions + "FIX", # allow "FIX" comments in code + "INP001", # allow Python files outside of packages + "N806", # allow uppercase variable names for variables that are classes + "PERF203", # allow try-except within loops + "PLR0913", # "Too many arguments" + "PLR0912", # "Too many branches" + "PLR2004", # "Magic value used in comparison" + "PTH", # don't require using pathlib instead of os + "RUF012", # allow mutable class variables + "SIM102", # Sometimes nested ifs are more readable than if...and... + "SIM105", # "Use `contextlib.suppress(...)` instead of `try`-`except`-`pass`" + "SIM108", # sometimes if-else is more readable than a ternary + "TD", # allow TODO comments to be whatever we want + "TRY003", # allow long messages passed to exceptions + "TRY004", # allow ValueError for invalid argument types +] + +[tool.ruff.lint.per-file-ignores] +"tests/*" = [ + "ARG", # unused arguments are fine in tests + "C408", # allow dict() instead of dict literal + "DTZ", # allow naive datetimes + "FBT003", # allow boolean positional argument + "N803", # fixture names might be uppercase + "PLR0915", # allow lots of statements + "PT007", # ignore false positives due to https://github.com/astral-sh/ruff/issues/14743 + "PT011", # don't require match when using pytest.raises + "S", # allow asserts + "SIM117", # allow nested with statements because it's more readable sometimes ] [tool.mypy] From 8352d64ef0ff726cc964a4eae970ec154de2052b Mon Sep 17 00:00:00 2001 From: Steven Loria Date: Sun, 19 Jan 2025 00:36:28 -0500 Subject: [PATCH 2/2] Address all lint errors --- docs/conf.py | 2 +- pyproject.toml | 7 +++++-- src/marshmallow_sqlalchemy/__init__.py | 12 ++++++------ src/marshmallow_sqlalchemy/convert.py | 17 +++++++---------- src/marshmallow_sqlalchemy/fields.py | 2 +- .../load_instance_mixin.py | 6 ++++-- src/marshmallow_sqlalchemy/schema.py | 13 +++++++------ tests/conftest.py | 10 ++++------ tests/test_conversion.py | 3 +-- 9 files changed, 36 insertions(+), 36 deletions(-) mode change 100755 => 100644 docs/conf.py diff --git a/docs/conf.py b/docs/conf.py old mode 100755 new mode 100644 index 3a666f4..eb24f79 --- a/docs/conf.py +++ b/docs/conf.py @@ -25,7 +25,7 @@ master_doc = "index" project = "marshmallow-sqlalchemy" -copyright = "Steven Loria and contributors" +copyright = "Steven Loria and contributors" # noqa: A001 version = release = importlib.metadata.version("marshmallow-sqlalchemy") diff --git a/pyproject.toml b/pyproject.toml index 2fd0174..55d9d65 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -63,24 +63,26 @@ ignore = [ "A005", # "module {name} shadows a Python standard-library module" "ANN", # let mypy handle annotation checks "ARG", # unused arguments are common w/ interfaces - "COM", # let formatter take care commas "C901", # don't enforce complexity level + "COM", # let formatter take care commas "D", # don't require docstrings "DTZ007", # ignore false positives due to https://github.com/astral-sh/ruff/issues/1306 "E501", # leave line-length enforcement to formatter "EM", # allow string messages in exceptions "FIX", # allow "FIX" comments in code "INP001", # allow Python files outside of packages + "N804", # metaclass methods aren't properly handled by this rule "N806", # allow uppercase variable names for variables that are classes "PERF203", # allow try-except within loops - "PLR0913", # "Too many arguments" "PLR0912", # "Too many branches" + "PLR0913", # "Too many arguments" "PLR2004", # "Magic value used in comparison" "PTH", # don't require using pathlib instead of os "RUF012", # allow mutable class variables "SIM102", # Sometimes nested ifs are more readable than if...and... "SIM105", # "Use `contextlib.suppress(...)` instead of `try`-`except`-`pass`" "SIM108", # sometimes if-else is more readable than a ternary + "SLF001", # allow private member access "TD", # allow TODO comments to be whatever we want "TRY003", # allow long messages passed to exceptions "TRY004", # allow ValueError for invalid argument types @@ -92,6 +94,7 @@ ignore = [ "C408", # allow dict() instead of dict literal "DTZ", # allow naive datetimes "FBT003", # allow boolean positional argument + "N802", # allow uppercasing in test names, e.g. test_convert_TSVECTOR "N803", # fixture names might be uppercase "PLR0915", # allow lots of statements "PT007", # ignore false positives due to https://github.com/astral-sh/ruff/issues/14743 diff --git a/src/marshmallow_sqlalchemy/__init__.py b/src/marshmallow_sqlalchemy/__init__.py index e346201..f1afdf5 100644 --- a/src/marshmallow_sqlalchemy/__init__.py +++ b/src/marshmallow_sqlalchemy/__init__.py @@ -15,15 +15,15 @@ ) __all__ = [ - "SQLAlchemySchema", + "ModelConversionError", + "ModelConverter", "SQLAlchemyAutoSchema", - "SQLAlchemySchemaOpts", "SQLAlchemyAutoSchemaOpts", + "SQLAlchemySchema", + "SQLAlchemySchemaOpts", "auto_field", - "ModelConverter", - "fields_for_model", - "property2field", "column2field", - "ModelConversionError", "field_for", + "fields_for_model", + "property2field", ] diff --git a/src/marshmallow_sqlalchemy/convert.py b/src/marshmallow_sqlalchemy/convert.py index e73bce1..dc46462 100644 --- a/src/marshmallow_sqlalchemy/convert.py +++ b/src/marshmallow_sqlalchemy/convert.py @@ -3,7 +3,6 @@ import functools import inspect import uuid -from collections.abc import Iterable from typing import ( TYPE_CHECKING, Any, @@ -25,14 +24,16 @@ from marshmallow import fields, validate from sqlalchemy.dialects import mssql, mysql, postgresql from sqlalchemy.orm import SynonymProperty -from sqlalchemy.types import TypeEngine from .exceptions import ModelConversionError from .fields import Related, RelatedList if TYPE_CHECKING: + from collections.abc import Iterable + from sqlalchemy.ext.declarative import DeclarativeMeta from sqlalchemy.orm import MapperProperty + from sqlalchemy.types import TypeEngine PropertyOrColumn = MapperProperty | sa.Column @@ -136,8 +137,7 @@ def __init__(self, schema_cls: type[ma.Schema] | None = None): def type_mapping(self) -> dict[type, type[fields.Field]]: if self.schema_cls: return self.schema_cls.TYPE_MAPPING - else: - return ma.Schema.TYPE_MAPPING + return ma.Schema.TYPE_MAPPING def fields_for_model( self, @@ -349,14 +349,13 @@ def field_for( converted_prop = self.property2field( prop, # To satisfy type checking, need to pass a literal bool - instance=True if instance else False, + instance=True if instance else False, # noqa: SIM210 field_class=field_class, **kwargs, ) if remote_with_local_multiplicity: return RelatedList(converted_prop, **{**self.get_base_kwargs(), **kwargs}) - else: - return converted_prop + return converted_prop def _get_field_name(self, prop_or_column: PropertyOrColumn) -> str: return prop_or_column.key @@ -478,9 +477,7 @@ def _should_exclude_field( key = self._get_field_name(column) if fields and key not in fields: return True - if exclude and key in exclude: - return True - return False + return bool(exclude and key in exclude) def get_base_kwargs(self): return {"validate": [], "metadata": {}} diff --git a/src/marshmallow_sqlalchemy/fields.py b/src/marshmallow_sqlalchemy/fields.py index f72ddc6..2db28ca 100644 --- a/src/marshmallow_sqlalchemy/fields.py +++ b/src/marshmallow_sqlalchemy/fields.py @@ -88,7 +88,7 @@ def transient(self): def _serialize(self, value, attr, obj): ret = {prop.key: getattr(value, prop.key, None) for prop in self.related_keys} - return ret if len(ret) > 1 else list(ret.values())[0] + return ret if len(ret) > 1 else next(iter(ret.values())) def _deserialize(self, value, *args, **kwargs): """Deserialize a serialized value to a model instance. diff --git a/src/marshmallow_sqlalchemy/load_instance_mixin.py b/src/marshmallow_sqlalchemy/load_instance_mixin.py index 532e88e..0353d62 100644 --- a/src/marshmallow_sqlalchemy/load_instance_mixin.py +++ b/src/marshmallow_sqlalchemy/load_instance_mixin.py @@ -8,8 +8,7 @@ from __future__ import annotations -from collections.abc import Iterable, Mapping -from typing import Any, Generic, TypeVar, cast +from typing import TYPE_CHECKING, Any, Generic, TypeVar, cast import marshmallow as ma from sqlalchemy.ext.declarative import DeclarativeMeta @@ -18,6 +17,9 @@ from .fields import get_primary_keys +if TYPE_CHECKING: + from collections.abc import Iterable, Mapping + _ModelType = TypeVar("_ModelType", bound=DeclarativeMeta) diff --git a/src/marshmallow_sqlalchemy/schema.py b/src/marshmallow_sqlalchemy/schema.py index 244155f..ba7b934 100644 --- a/src/marshmallow_sqlalchemy/schema.py +++ b/src/marshmallow_sqlalchemy/schema.py @@ -1,16 +1,18 @@ from __future__ import annotations -from typing import Any, cast +from typing import TYPE_CHECKING, Any, cast import sqlalchemy as sa from marshmallow.fields import Field from marshmallow.schema import Schema, SchemaMeta, SchemaOpts -from sqlalchemy.ext.declarative import DeclarativeMeta from .convert import ModelConverter from .exceptions import IncorrectSchemaTypeError from .load_instance_mixin import LoadInstanceMixin, _ModelType +if TYPE_CHECKING: + from sqlalchemy.ext.declarative import DeclarativeMeta + # This isn't really a field; it's a placeholder for the metaclass. # This should be considered private API. @@ -42,10 +44,9 @@ def create_field( model = self.model or schema_opts.model if model: return converter.field_for(model, column_name, **self.field_kwargs) - else: - table = self.table if self.table is not None else schema_opts.table - column = getattr(cast(sa.Table, table).columns, column_name) - return converter.column2field(column, **self.field_kwargs) + table = self.table if self.table is not None else schema_opts.table + column = getattr(cast(sa.Table, table).columns, column_name) + return converter.column2field(column, **self.field_kwargs) # This field should never be bound to a schema. # If this method is called, it's probably because the schema is not a SQLAlchemySchema. diff --git a/tests/conftest.py b/tests/conftest.py index 4a64975..64b44ec 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -29,8 +29,6 @@ class AnotherInteger(sa.Integer): """Use me to test if MRO works like we want""" - pass - class AnotherText(sa.types.TypeDecorator): """Use me to test if MRO and `impl` virtual type works like we want""" @@ -38,19 +36,19 @@ class AnotherText(sa.types.TypeDecorator): impl = sa.UnicodeText -@pytest.fixture() +@pytest.fixture def Base() -> type: return declarative_base() -@pytest.fixture() +@pytest.fixture def engine(): engine = sa.create_engine("sqlite:///:memory:", echo=False, future=True) yield engine engine.dispose() -@pytest.fixture() +@pytest.fixture def session(Base, models, engine): Session = sessionmaker(bind=engine) Base.metadata.create_all(bind=engine) @@ -75,7 +73,7 @@ class Models: Keyword: type[DeclarativeMeta] -@pytest.fixture() +@pytest.fixture def models(Base: type) -> Models: # models adapted from https://github.com/wtforms/wtforms-sqlalchemy/blob/master/tests/tests.py student_course = sa.Table( diff --git a/tests/test_conversion.py b/tests/test_conversion.py index 01ec7ef..e3e4499 100644 --- a/tests/test_conversion.py +++ b/tests/test_conversion.py @@ -158,7 +158,7 @@ def make_property(*column_args, **column_kwargs): class TestPropertyFieldConversion: - @pytest.fixture() + @pytest.fixture def converter(self): return ModelConverter() @@ -185,7 +185,6 @@ class MySchema(Schema): (sa.Date, fields.Date), (sa.DateTime, fields.DateTime), (sa.Boolean, fields.Bool), - (sa.Boolean, fields.Bool), (sa.Float, fields.Float), (sa.SmallInteger, fields.Int), (sa.Interval, fields.TimeDelta),