From: Sebastián Ramírez Date: Mon, 23 Oct 2023 07:34:50 +0000 (+0400) Subject: 👷 Move to Ruff and add pre-commit (#661) X-Git-Tag: 0.0.9~41 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=065fcdc8280ab333a0e165eec9124b727d2a92e8;p=thirdparty%2Ffastapi%2Fsqlmodel.git 👷 Move to Ruff and add pre-commit (#661) * 👷 Add pre-commit * 🔧 Add pyproject.toml config for Ruff * ➕ Replace isort, flake8, autoflake with Ruff * 🔨 Update lint and format scripts * 🎨 Format with Ruff * 🔧 Update Poetry config --- diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..9f7085f7 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,35 @@ +# See https://pre-commit.com for more information +# See https://pre-commit.com/hooks.html for more hooks +default_language_version: + python: python3.10 +repos: +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.4.0 + hooks: + - id: check-added-large-files + - id: check-toml + - id: check-yaml + args: + - --unsafe + - id: end-of-file-fixer + - id: trailing-whitespace +- repo: https://github.com/asottile/pyupgrade + rev: v3.7.0 + hooks: + - id: pyupgrade + args: + - --py3-plus + - --keep-runtime-typing +- repo: https://github.com/charliermarsh/ruff-pre-commit + rev: v0.0.275 + hooks: + - id: ruff + args: + - --fix +- repo: https://github.com/psf/black + rev: 23.3.0 + hooks: + - id: black +ci: + autofix_commit_msg: 🎨 [pre-commit.ci] Auto format from pre-commit.com hooks + autoupdate_commit_msg: ⬆ [pre-commit.ci] pre-commit autoupdate diff --git a/pyproject.toml b/pyproject.toml index e4027271..73d8b3ac 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,10 +35,9 @@ SQLAlchemy = ">=1.4.17,<=1.4.41" pydantic = "^1.8.2" sqlalchemy2-stubs = {version = "*", allow-prereleases = true} -[tool.poetry.dev-dependencies] +[tool.poetry.group.dev.dependencies] pytest = "^7.0.1" mypy = "0.971" -flake8 = "^5.0.4" black = "^22.10.0" mkdocs = "^1.2.1" mkdocs-material = "^8.1.4" @@ -48,8 +47,7 @@ mdx-include = "^1.4.1" coverage = {extras = ["toml"], version = "^6.2"} fastapi = "^0.68.1" requests = "^2.26.0" -autoflake = "^1.4" -isort = "^5.9.3" +ruff = "^0.1.1" [build-system] requires = ["poetry-core"] @@ -75,27 +73,19 @@ exclude_lines = [ "if TYPE_CHECKING:", ] -[tool.isort] -profile = "black" -known_third_party = ["sqlmodel"] -skip_glob = [ - "sqlmodel/__init__.py", - ] - - [tool.mypy] # --strict disallow_any_generics = true -disallow_subclassing_any = true -disallow_untyped_calls = true +disallow_subclassing_any = true +disallow_untyped_calls = true disallow_untyped_defs = true -disallow_incomplete_defs = true -check_untyped_defs = true -disallow_untyped_decorators = true +disallow_incomplete_defs = true +check_untyped_defs = true +disallow_untyped_decorators = true no_implicit_optional = true -warn_redundant_casts = true +warn_redundant_casts = true warn_unused_ignores = true -warn_return_any = true +warn_return_any = true implicit_reexport = false strict_equality = true # --strict end @@ -104,4 +94,23 @@ strict_equality = true module = "sqlmodel.sql.expression" warn_unused_ignores = false -# invalidate CI cache: 1 +[tool.ruff] +select = [ + "E", # pycodestyle errors + "W", # pycodestyle warnings + "F", # pyflakes + "I", # isort + "C", # flake8-comprehensions + "B", # flake8-bugbear +] +ignore = [ + "E501", # line too long, handled by black + "B008", # do not perform function calls in argument defaults + "C901", # too complex +] + +[tool.ruff.per-file-ignores] +# "__init__.py" = ["F401"] + +[tool.ruff.isort] +known-third-party = ["sqlmodel", "sqlalchemy", "pydantic", "fastapi"] diff --git a/scripts/format.sh b/scripts/format.sh index 0d456398..b6aebd10 100755 --- a/scripts/format.sh +++ b/scripts/format.sh @@ -1,6 +1,5 @@ #!/bin/sh -e set -x -autoflake --remove-all-unused-imports --recursive --remove-unused-variables --in-place sqlmodel docs_src tests --exclude=__init__.py -black sqlmodel tests docs_src -isort sqlmodel tests docs_src +ruff sqlmodel tests docs_src scripts --fix +black sqlmodel tests docs_src scripts diff --git a/scripts/lint.sh b/scripts/lint.sh index 4191d90f..b328e3d9 100755 --- a/scripts/lint.sh +++ b/scripts/lint.sh @@ -4,6 +4,5 @@ set -e set -x mypy sqlmodel -flake8 sqlmodel tests docs_src +ruff sqlmodel tests docs_src scripts black sqlmodel tests docs_src --check -isort sqlmodel tests docs_src scripts --check-only diff --git a/sqlmodel/__init__.py b/sqlmodel/__init__.py index 720aa8c9..3aa6e0d2 100644 --- a/sqlmodel/__init__.py +++ b/sqlmodel/__init__.py @@ -5,12 +5,12 @@ from sqlalchemy.engine import create_mock_engine as create_mock_engine from sqlalchemy.engine import engine_from_config as engine_from_config from sqlalchemy.inspection import inspect as inspect from sqlalchemy.schema import BLANK_SCHEMA as BLANK_SCHEMA +from sqlalchemy.schema import DDL as DDL from sqlalchemy.schema import CheckConstraint as CheckConstraint from sqlalchemy.schema import Column as Column from sqlalchemy.schema import ColumnDefault as ColumnDefault from sqlalchemy.schema import Computed as Computed from sqlalchemy.schema import Constraint as Constraint -from sqlalchemy.schema import DDL as DDL from sqlalchemy.schema import DefaultClause as DefaultClause from sqlalchemy.schema import FetchedValue as FetchedValue from sqlalchemy.schema import ForeignKey as ForeignKey @@ -23,6 +23,14 @@ from sqlalchemy.schema import Sequence as Sequence from sqlalchemy.schema import Table as Table from sqlalchemy.schema import ThreadLocalMetaData as ThreadLocalMetaData from sqlalchemy.schema import UniqueConstraint as UniqueConstraint +from sqlalchemy.sql import LABEL_STYLE_DEFAULT as LABEL_STYLE_DEFAULT +from sqlalchemy.sql import ( + LABEL_STYLE_DISAMBIGUATE_ONLY as LABEL_STYLE_DISAMBIGUATE_ONLY, +) +from sqlalchemy.sql import LABEL_STYLE_NONE as LABEL_STYLE_NONE +from sqlalchemy.sql import ( + LABEL_STYLE_TABLENAME_PLUS_COL as LABEL_STYLE_TABLENAME_PLUS_COL, +) from sqlalchemy.sql import alias as alias from sqlalchemy.sql import all_ as all_ from sqlalchemy.sql import and_ as and_ @@ -48,14 +56,6 @@ from sqlalchemy.sql import insert as insert from sqlalchemy.sql import intersect as intersect from sqlalchemy.sql import intersect_all as intersect_all from sqlalchemy.sql import join as join -from sqlalchemy.sql import LABEL_STYLE_DEFAULT as LABEL_STYLE_DEFAULT -from sqlalchemy.sql import ( - LABEL_STYLE_DISAMBIGUATE_ONLY as LABEL_STYLE_DISAMBIGUATE_ONLY, -) -from sqlalchemy.sql import LABEL_STYLE_NONE as LABEL_STYLE_NONE -from sqlalchemy.sql import ( - LABEL_STYLE_TABLENAME_PLUS_COL as LABEL_STYLE_TABLENAME_PLUS_COL, -) from sqlalchemy.sql import lambda_stmt as lambda_stmt from sqlalchemy.sql import lateral as lateral from sqlalchemy.sql import literal as literal @@ -85,55 +85,53 @@ from sqlalchemy.sql import values as values from sqlalchemy.sql import within_group as within_group from sqlalchemy.types import ARRAY as ARRAY from sqlalchemy.types import BIGINT as BIGINT -from sqlalchemy.types import BigInteger as BigInteger from sqlalchemy.types import BINARY as BINARY from sqlalchemy.types import BLOB as BLOB from sqlalchemy.types import BOOLEAN as BOOLEAN -from sqlalchemy.types import Boolean as Boolean from sqlalchemy.types import CHAR as CHAR from sqlalchemy.types import CLOB as CLOB from sqlalchemy.types import DATE as DATE -from sqlalchemy.types import Date as Date from sqlalchemy.types import DATETIME as DATETIME -from sqlalchemy.types import DateTime as DateTime from sqlalchemy.types import DECIMAL as DECIMAL -from sqlalchemy.types import Enum as Enum from sqlalchemy.types import FLOAT as FLOAT -from sqlalchemy.types import Float as Float from sqlalchemy.types import INT as INT from sqlalchemy.types import INTEGER as INTEGER -from sqlalchemy.types import Integer as Integer -from sqlalchemy.types import Interval as Interval from sqlalchemy.types import JSON as JSON -from sqlalchemy.types import LargeBinary as LargeBinary from sqlalchemy.types import NCHAR as NCHAR from sqlalchemy.types import NUMERIC as NUMERIC -from sqlalchemy.types import Numeric as Numeric from sqlalchemy.types import NVARCHAR as NVARCHAR -from sqlalchemy.types import PickleType as PickleType from sqlalchemy.types import REAL as REAL from sqlalchemy.types import SMALLINT as SMALLINT +from sqlalchemy.types import TEXT as TEXT +from sqlalchemy.types import TIME as TIME +from sqlalchemy.types import TIMESTAMP as TIMESTAMP +from sqlalchemy.types import VARBINARY as VARBINARY +from sqlalchemy.types import VARCHAR as VARCHAR +from sqlalchemy.types import BigInteger as BigInteger +from sqlalchemy.types import Boolean as Boolean +from sqlalchemy.types import Date as Date +from sqlalchemy.types import DateTime as DateTime +from sqlalchemy.types import Enum as Enum +from sqlalchemy.types import Float as Float +from sqlalchemy.types import Integer as Integer +from sqlalchemy.types import Interval as Interval +from sqlalchemy.types import LargeBinary as LargeBinary +from sqlalchemy.types import Numeric as Numeric +from sqlalchemy.types import PickleType as PickleType from sqlalchemy.types import SmallInteger as SmallInteger from sqlalchemy.types import String as String -from sqlalchemy.types import TEXT as TEXT from sqlalchemy.types import Text as Text -from sqlalchemy.types import TIME as TIME from sqlalchemy.types import Time as Time -from sqlalchemy.types import TIMESTAMP as TIMESTAMP from sqlalchemy.types import TypeDecorator as TypeDecorator from sqlalchemy.types import Unicode as Unicode from sqlalchemy.types import UnicodeText as UnicodeText -from sqlalchemy.types import VARBINARY as VARBINARY -from sqlalchemy.types import VARCHAR as VARCHAR -# Extensions and modifications of SQLAlchemy in SQLModel +# From SQLModel, modifications of SQLAlchemy or equivalents of Pydantic from .engine.create import create_engine as create_engine +from .main import Field as Field +from .main import Relationship as Relationship +from .main import SQLModel as SQLModel from .orm.session import Session as Session -from .sql.expression import select as select from .sql.expression import col as col +from .sql.expression import select as select from .sql.sqltypes import AutoString as AutoString - -# Export SQLModel specifics (equivalent to Pydantic) -from .main import SQLModel as SQLModel -from .main import Field as Field -from .main import Relationship as Relationship diff --git a/sqlmodel/main.py b/sqlmodel/main.py index 7dec60dd..d5a73024 100644 --- a/sqlmodel/main.py +++ b/sqlmodel/main.py @@ -26,15 +26,24 @@ from typing import ( from pydantic import BaseConfig, BaseModel from pydantic.errors import ConfigError, DictError -from pydantic.fields import SHAPE_SINGLETON +from pydantic.fields import SHAPE_SINGLETON, ModelField, Undefined, UndefinedType from pydantic.fields import FieldInfo as PydanticFieldInfo -from pydantic.fields import ModelField, Undefined, UndefinedType from pydantic.main import ModelMetaclass, validate_model from pydantic.typing import NoArgAnyCallable, resolve_annotations from pydantic.utils import ROOT_KEY, Representation -from sqlalchemy import Boolean, Column, Date, DateTime +from sqlalchemy import ( + Boolean, + Column, + Date, + DateTime, + Float, + ForeignKey, + Integer, + Interval, + Numeric, + inspect, +) from sqlalchemy import Enum as sa_Enum -from sqlalchemy import Float, ForeignKey, Integer, Interval, Numeric, inspect from sqlalchemy.orm import RelationshipProperty, declared_attr, registry, relationship from sqlalchemy.orm.attributes import set_attribute from sqlalchemy.orm.decl_api import DeclarativeMeta @@ -305,9 +314,9 @@ class SQLModelMetaclass(ModelMetaclass, DeclarativeMeta): config_registry = cast(registry, config_registry) # If it was passed by kwargs, ensure it's also set in config new_cls.__config__.registry = config_table - setattr(new_cls, "_sa_registry", config_registry) - setattr(new_cls, "metadata", config_registry.metadata) - setattr(new_cls, "__abstract__", True) + setattr(new_cls, "_sa_registry", config_registry) # noqa: B010 + setattr(new_cls, "metadata", config_registry.metadata) # noqa: B010 + setattr(new_cls, "__abstract__", True) # noqa: B010 return new_cls # Override SQLAlchemy, allow both SQLAlchemy and plain Pydantic models @@ -320,7 +329,7 @@ class SQLModelMetaclass(ModelMetaclass, DeclarativeMeta): # triggers an error base_is_table = False for base in bases: - config = getattr(base, "__config__") + config = getattr(base, "__config__") # noqa: B009 if config and getattr(config, "table", False): base_is_table = True break @@ -351,7 +360,7 @@ class SQLModelMetaclass(ModelMetaclass, DeclarativeMeta): rel_kwargs["back_populates"] = rel_info.back_populates if rel_info.link_model: ins = inspect(rel_info.link_model) - local_table = getattr(ins, "local_table") + local_table = getattr(ins, "local_table") # noqa: B009 if local_table is None: raise RuntimeError( "Couldn't find the secondary table for " @@ -430,7 +439,7 @@ def get_column_from_field(field: ModelField) -> Column: # type: ignore # Override derived nullability if the nullable property is set explicitly # on the field if hasattr(field.field_info, "nullable"): - field_nullable = getattr(field.field_info, "nullable") + field_nullable = getattr(field.field_info, "nullable") # noqa: B009 if field_nullable != Undefined: nullable = field_nullable args = []