From 3a322a023b0f53486d8ca0b9f1adb50c14b7b37e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edgar=20Ram=C3=ADrez=20Mondrag=C3=B3n?= Date: Mon, 25 Sep 2023 19:42:16 -0600 Subject: [PATCH 1/8] feat: Implement custom types for `ipaddr` and `uuid` extensions --- pyproject.toml | 10 ++ src/sqlean_driver/__init__.py | 64 +++++++++++ src/sqlean_driver/custom_types.py | 182 ++++++++++++++++++++++++++++++ tests/test_types.py | 157 ++++++++++++++++++++++++++ 4 files changed, 413 insertions(+) create mode 100644 src/sqlean_driver/custom_types.py create mode 100644 tests/test_types.py diff --git a/pyproject.toml b/pyproject.toml index bdde63a..dd68c0e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,6 +44,7 @@ dependencies = [ "greenlet>=3.0.0a1", "sqlalchemy>=1.4", "sqlean-py>=0.21.5.1", + 'typing-extensions; python_version < "3.10"', ] [project.optional-dependencies] dev = [ @@ -205,11 +206,17 @@ select = [ "RUF", # Ruff-specific rules ] target-version = "py38" +unfixable = [ + "ERA", +] [tool.ruff.isort] known-first-party = ["sqlean_driver"] required-imports = ["from __future__ import annotations"] +[tool.ruff.flake8-annotations] +allow-star-arg-any = true + [tool.ruff.flake8-import-conventions] banned-from = ["typing"] @@ -219,6 +226,9 @@ typing = "t" [tool.ruff.flake8-tidy-imports] ban-relative-imports = "all" +[tool.ruff.pep8-naming] +ignore-names = ["visit_*"] + [tool.ruff.per-file-ignores] # Tests can use magic values, assertions, and relative imports "tests/**/*" = ["PLR2004", "S101", "TID252", "ANN201"] diff --git a/src/sqlean_driver/__init__.py b/src/sqlean_driver/__init__.py index 6650f6b..b9eb42c 100644 --- a/src/sqlean_driver/__init__.py +++ b/src/sqlean_driver/__init__.py @@ -3,24 +3,88 @@ from __future__ import annotations import typing as t +import uuid from importlib.metadata import version +import sqlalchemy.types as sqltypes +from sqlalchemy.dialects.sqlite.base import SQLiteTypeCompiler from sqlalchemy.dialects.sqlite.pysqlite import SQLiteDialect_pysqlite +from sqlalchemy.sql.functions import GenericFunction + +from sqlean_driver.custom_types import UUID if t.TYPE_CHECKING: from types import ModuleType from sqlalchemy.engine.url import URL + from sqlalchemy.sql.type_api import TypeEngine + from sqlean_driver.custom_types import IPAddress, IPNetwork __version__ = version(__package__) +class SQLeanTypeCompiler(SQLiteTypeCompiler): + """A type compiler for SQLite that uses sqlean.py as the DBAPI.""" + + def visit_INET(self, type_: TypeEngine[IPAddress], **kwa: t.Any) -> str: # noqa: ARG002, PLR6301, E501 + """Visit an INET node.""" + return "INET" + + def visit_CIDR(self, type_: TypeEngine[IPNetwork], **kw: t.Any) -> str: # noqa: ARG002, PLR6301 + """Visit a CIDR nodes.""" + return "CIDR" + + def visit_UUID(self, type_: TypeEngine[uuid.UUID], **kw: t.Any) -> str: # noqa: ARG002, PLR6301 + """Visit a UUID node.""" + return "UUID" + + +class uuid4(GenericFunction[uuid.UUID]): # noqa: N801 + """Generates a version 4 (random) UUID as a string. + + Aliased as gen_random_uuid() for PostgreSQL compatibility. + """ + + name = "uuid4" + type = UUID() # noqa: A003 + inherit_cache = True + + +class gen_random_uuid(uuid4): # noqa: N801 + """Generates a version 4 (random) UUID as a string.""" + + name = "gen_random_uuid" + + +class uuid_str(GenericFunction[uuid.UUID]): # noqa: N801 + """Converts a UUID `X` into a well-formed UUID string. + + `X` can be either a string or a blob. + """ + + name = "uuid_str" + type = UUID() # noqa: A003 + inherit_cache = True + + +class uuid_blob(GenericFunction[bytes]): # noqa: N801 + """Converts a UUID `X` into a well-formed UUID string. + + `X` can be either a string or a blob. + """ + + name = "uuid_blob" + type = sqltypes.BLOB() # noqa: A003 + inherit_cache = True + + class SQLeanDialect(SQLiteDialect_pysqlite): """A dialect for SQLite that uses sqlean.py as the DBAPI.""" driver = "sqlean" supports_statement_cache = True + type_compiler_cls = SQLeanTypeCompiler @classmethod def dbapi(cls) -> ModuleType: # type: ignore[override] diff --git a/src/sqlean_driver/custom_types.py b/src/sqlean_driver/custom_types.py new file mode 100644 index 0000000..7b60719 --- /dev/null +++ b/src/sqlean_driver/custom_types.py @@ -0,0 +1,182 @@ +"""Custom SQLAlchemy types.""" + +from __future__ import annotations + +import ipaddress +import typing as t +import uuid + +import sqlalchemy.types as sqltypes +from sqlalchemy.sql.functions import GenericFunction + +if t.TYPE_CHECKING: + import sys + + if sys.version_info < (3, 10): + from typing_extensions import TypeAlias + else: + from typing import TypeAlias # noqa: ICN003 + + from sqlalchemy.engine.interfaces import Dialect + from sqlalchemy.sql.type_api import _BindProcessorType, _ResultProcessorType + + +IPAddress: TypeAlias = t.Union[ipaddress.IPv4Address, ipaddress.IPv6Address] +IPNetwork: TypeAlias = t.Union[ipaddress.IPv4Network, ipaddress.IPv6Network] + + +def none_or_str(value: t.Any | None) -> str | None: # noqa: ANN401 + """Return the value or None.""" + return str(value) if value is not None else None + + +def none_or_ip_interface( + value: t.Any | None, # noqa: ANN401 +) -> ipaddress.IPv4Interface | ipaddress.IPv6Interface | None: + """Return the value or None.""" + return ipaddress.ip_interface(value) if value is not None else None + + +def none_or_ip_network( + value: t.Any | None, # noqa: ANN401 +) -> ipaddress.IPv4Network | ipaddress.IPv6Network | None: + """Return the value or None.""" + return ipaddress.ip_network(value) if value is not None else None + + +def none_or_uuid( + value: t.Any | None, # noqa: ANN401 +) -> uuid.UUID | None: + """Return the value or None.""" + return uuid.UUID(value) if value is not None else None + + +class INET(sqltypes.TypeEngine[IPAddress]): + """An INET type.""" + + __visit_name__ = "INET" + + def bind_processor( + self, # noqa: PLR6301 + _dialect: Dialect, + ) -> _BindProcessorType[IPAddress] | None: + """Return a bind processor.""" + return none_or_str + + def result_processor( + self, # noqa: PLR6301 + _dialect: Dialect, + _coltype: object, + ) -> _ResultProcessorType[IPAddress] | None: + """Return a result processor.""" + return none_or_ip_interface + + class Comparator( + sqltypes.Indexable.Comparator[IPAddress], + sqltypes.Concatenable.Comparator[IPAddress], + ): + """Comparator for the INET type.""" + + def ipfamily(self) -> _IPAddrIPFamilyFunction: + """Return the IP family.""" + return _IPAddrIPFamilyFunction(self.expr) # type: ignore[no-untyped-call] + + def iphost(self) -> _IPAddrIPHostFunction: + """Return the IP host.""" + return _IPAddrIPHostFunction(self.expr) # type: ignore[no-untyped-call] + + def ipmasklen(self) -> _IPAddrIPMaskLenFunction: + """Return the IP mask length.""" + return _IPAddrIPMaskLenFunction(self.expr) # type: ignore[no-untyped-call] + + def ipnetwork(self) -> _IPAddrIPNetworkFunction: + """Return the IP network.""" + return _IPAddrIPNetworkFunction(self.expr) # type: ignore[no-untyped-call] + + def ipcontains(self, other: IPAddress | str) -> _IPAddrIPContainsFunction: + """Return whether the IP address contains another IP address.""" + return _IPAddrIPContainsFunction(self.expr, other) # type: ignore[no-untyped-call] + + comparator_factory = Comparator + + +class CIDR(sqltypes.TypeEngine[IPNetwork]): + """A CIDR type.""" + + __visit_name__ = "CIDR" + + def bind_processor( + self, # noqa: PLR6301 + _dialect: Dialect, + ) -> _BindProcessorType[IPNetwork] | None: + """Return a bind processor.""" + return none_or_str + + def result_processor( + self, # noqa: PLR6301 + _dialect: Dialect, + _coltype: object, + ) -> _ResultProcessorType[IPNetwork] | None: + """Return a result processor.""" + return none_or_ip_network + + +class UUID(sqltypes.TypeEngine[uuid.UUID]): + """A UUID type.""" + + __visit_name__ = "UUID" + + def bind_processor( + self, # noqa: PLR6301 + _dialect: Dialect, + ) -> _BindProcessorType[uuid.UUID] | None: + """Return a bind processor.""" + return none_or_str + + def result_processor( + self, # noqa: PLR6301 + _dialect: Dialect, + _coltype: object, + ) -> _ResultProcessorType[uuid.UUID] | None: + """Return a result processor.""" + return none_or_uuid + + +class _IPAddrIPFamilyFunction(GenericFunction[int]): + """Returns the family of a specified IP address.""" + + name = "ipfamily" + type = sqltypes.Integer() # noqa: A003 + inherit_cache = True + + +class _IPAddrIPHostFunction(GenericFunction[str]): + """Returns the host part of an IP address.""" + + name = "iphost" + type = sqltypes.String() # noqa: A003 + inherit_cache = True + + +class _IPAddrIPMaskLenFunction(GenericFunction[int]): + """Returns the prefix length of an IP address.""" + + name = "ipmasklen" + type = sqltypes.Integer() # noqa: A003 + inherit_cache = True + + +class _IPAddrIPNetworkFunction(GenericFunction[IPNetwork]): + """Returns the network part of an IP address.""" + + name = "ipnetwork" + type = CIDR() # noqa: A003 + inherit_cache = True + + +class _IPAddrIPContainsFunction(GenericFunction[bool]): + """Returns whether an IP address contains another IP address.""" + + name = "ipcontains" + type = sqltypes.Boolean() # noqa: A003 + inherit_cache = True diff --git a/tests/test_types.py b/tests/test_types.py new file mode 100644 index 0000000..cf28668 --- /dev/null +++ b/tests/test_types.py @@ -0,0 +1,157 @@ +"""Test the custom types.""" + +from __future__ import annotations + +import ipaddress +import typing as t +import uuid + +import pytest +from sqlalchemy import Column, Integer, MetaData, Table, create_engine, func, select + +from sqlean_driver.custom_types import INET, UUID + +if t.TYPE_CHECKING: + from sqlalchemy import Select + +metadata = MetaData() +table = Table( + "table", + metadata, + Column("id", Integer, primary_key=True), + Column("ip", INET), + Column("uuid_col", UUID), +) + + +@pytest.mark.parametrize( + ("data", "query", "expected"), + [ + pytest.param( + [{"ip": None}], + select(table.c.ip, table.c.ip.ipnetwork()), + (None, None), + id="nullable", + ), + pytest.param( + [{"ip": ipaddress.IPv4Network("192.168.1.1")}], + select(func.ipfamily(table.c.ip), table.c.ip.ipfamily()), + (4, 4), + id="ipfamily", + ), + pytest.param( + [{"ip": ipaddress.IPv6Interface("2001:db8::123/64")}], + select(func.iphost(table.c.ip), table.c.ip.iphost()), + ("2001:db8::123", "2001:db8::123"), + id="iphost", + ), + pytest.param( + [{"ip": ipaddress.IPv4Interface("192.168.16.12/24")}], + select(func.ipmasklen(table.c.ip), table.c.ip.ipmasklen()), + (24, 24), + id="ipmasklen", + ), + pytest.param( + [{"ip": ipaddress.IPv4Interface("192.168.16.12/24")}], + select(func.ipnetwork(table.c.ip), table.c.ip.ipnetwork()), + ( + ipaddress.IPv4Network("192.168.16.0/24"), + ipaddress.IPv4Network("192.168.16.0/24"), + ), + id="ipnetwork", + ), + pytest.param( + [{"ip": ipaddress.IPv4Interface("192.168.16.0/24")}], + select( + func.ipcontains(table.c.ip, "192.168.16.3"), + table.c.ip.ipcontains("192.168.16.3"), + ), + (True, True), + id="ipcontains_lhs", + ), + pytest.param( + [{"ip": ipaddress.IPv4Interface("192.168.16.3")}], + select( + func.ipcontains("192.168.16.0/24", table.c.ip), + ), + (True,), + id="ipcontains_rhs", + ), + ], +) +def test_ipaddr_types( + data: list[dict[str, t.Any]], + query: Select[t.Any], + expected: tuple[t.Any, ...], +) -> None: + """Test that the types work.""" + engine = create_engine("sqlite+sqlean:///:memory:?extensions=ipaddr") + metadata.create_all(engine) + with engine.connect() as conn: + conn.execute(table.insert(), data) + result = conn.execute(query) + assert result.fetchone() == expected + + +@pytest.mark.parametrize( + ("data", "query", "expected"), + [ + pytest.param( + [{"uuid_col": None}], + select(table.c.uuid_col), + (None,), + id="nullable", + ), + ], +) +def test_uuid_types( + data: list[dict[str, t.Any]], + query: Select[t.Any], + expected: tuple[t.Any, ...], +) -> None: + """Test that the types work.""" + engine = create_engine("sqlite+sqlean:///:memory:?extensions=uuid") + metadata.create_all(engine) + with engine.connect() as conn: + conn.execute(table.insert(), data) + result = conn.execute(query) + assert result.fetchone() == expected + + +def test_function_uuid4() -> None: + """Test that the function works.""" + engine = create_engine("sqlite+sqlean:///:memory:?extensions=uuid") + metadata.create_all(engine) + with engine.connect() as conn: + result = conn.execute(select(func.uuid4())) + row = result.fetchone() + assert row is not None + assert isinstance(row[0], uuid.UUID) + + +def test_function_uuid_str() -> None: + """Test that the function works.""" + engine = create_engine("sqlite+sqlean:///:memory:?extensions=uuid") + metadata.create_all(engine) + with engine.connect() as conn: + result = conn.execute(select(func.uuid_str("8d144638-3baf-4901-a554-b541142c152b"))) + row = result.fetchone() + assert row is not None + assert row[0] == uuid.UUID("8d144638-3baf-4901-a554-b541142c152b") + + +def test_function_uuid_blob() -> None: + """Test that the function works.""" + engine = create_engine("sqlite+sqlean:///:memory:?extensions=uuid") + metadata.create_all(engine) + with engine.connect() as conn: + result = conn.execute( + select( + func.uuid_blob("8d144638-3baf-4901-a554-b541142c152b"), + func.uuid_blob(func.uuid4()), + ), + ) + row = result.fetchone() + assert row is not None + assert isinstance(row[0], bytes) + assert isinstance(row[1], bytes) From a52b2433db9422a9cee37c40c94780b36da67ad2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edgar=20Ram=C3=ADrez=20Mondrag=C3=B3n?= Date: Mon, 25 Sep 2023 23:05:23 -0600 Subject: [PATCH 2/8] Adjust for SQLAlchemy 1 --- pyproject.toml | 1 + src/sqlean_driver/__init__.py | 2 +- src/sqlean_driver/custom_types.py | 8 ++++++-- tests/test_types.py | 4 ++-- 4 files changed, 10 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index dd68c0e..88cc312 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -110,6 +110,7 @@ skip-string-normalization = true ignore = [ "ANN101", # missing-type-self "ANN102", # missing-type-cls + "FIX002", # line-contains-todo ] line-length = 100 select = [ diff --git a/src/sqlean_driver/__init__.py b/src/sqlean_driver/__init__.py index b9eb42c..bde8b3b 100644 --- a/src/sqlean_driver/__init__.py +++ b/src/sqlean_driver/__init__.py @@ -84,7 +84,7 @@ class SQLeanDialect(SQLiteDialect_pysqlite): driver = "sqlean" supports_statement_cache = True - type_compiler_cls = SQLeanTypeCompiler + type_compiler = SQLeanTypeCompiler @classmethod def dbapi(cls) -> ModuleType: # type: ignore[override] diff --git a/src/sqlean_driver/custom_types.py b/src/sqlean_driver/custom_types.py index 7b60719..a5beeaa 100644 --- a/src/sqlean_driver/custom_types.py +++ b/src/sqlean_driver/custom_types.py @@ -71,9 +71,13 @@ def result_processor( """Return a result processor.""" return none_or_ip_interface + # TODO(edgarrmondragon): Add missing type parameters: + # > sqltypes.Indexable.Comparator[IPAddress] + # > sqltypes.Concatenable.Comparator[IPAddress] + # https://github.com/edgarrmondragon/sqlean-driver/issues/37 class Comparator( - sqltypes.Indexable.Comparator[IPAddress], - sqltypes.Concatenable.Comparator[IPAddress], + sqltypes.Indexable.Comparator, # type: ignore[type-arg] + sqltypes.Concatenable.Comparator, # type: ignore[type-arg] ): """Comparator for the INET type.""" diff --git a/tests/test_types.py b/tests/test_types.py index cf28668..dfb6eff 100644 --- a/tests/test_types.py +++ b/tests/test_types.py @@ -87,7 +87,7 @@ def test_ipaddr_types( """Test that the types work.""" engine = create_engine("sqlite+sqlean:///:memory:?extensions=ipaddr") metadata.create_all(engine) - with engine.connect() as conn: + with engine.connect() as conn, conn.begin(): conn.execute(table.insert(), data) result = conn.execute(query) assert result.fetchone() == expected @@ -112,7 +112,7 @@ def test_uuid_types( """Test that the types work.""" engine = create_engine("sqlite+sqlean:///:memory:?extensions=uuid") metadata.create_all(engine) - with engine.connect() as conn: + with engine.connect() as conn, conn.begin(): conn.execute(table.insert(), data) result = conn.execute(query) assert result.fetchone() == expected From d1ad417cf215568c376f6625dce31d3e605fa831 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edgar=20Ram=C3=ADrez=20Mondrag=C3=B3n?= Date: Mon, 25 Sep 2023 23:11:43 -0600 Subject: [PATCH 3/8] Make linter happy --- pyproject.toml | 2 +- src/sqlean_driver/__init__.py | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 88cc312..c0b07e0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -99,7 +99,7 @@ dependencies = ["black>=23.1.0", "mypy>=1.0.0", "ruff>=0.0.243"] [tool.hatch.envs.lint.scripts] style = ["ruff check {args:.}", "black --check --diff {args:.}"] style-gh = ["ruff check {args:.} --format github", "black --check --diff {args:.}"] -fmt = ["black {args:.}", "ruff --fix {args:.}", "style"] +fmt = ["black {args:.}", "ruff check --fix {args:.}", "style"] [tool.black] target-version = ["py38"] diff --git a/src/sqlean_driver/__init__.py b/src/sqlean_driver/__init__.py index bde8b3b..342f651 100644 --- a/src/sqlean_driver/__init__.py +++ b/src/sqlean_driver/__init__.py @@ -27,7 +27,11 @@ class SQLeanTypeCompiler(SQLiteTypeCompiler): """A type compiler for SQLite that uses sqlean.py as the DBAPI.""" - def visit_INET(self, type_: TypeEngine[IPAddress], **kwa: t.Any) -> str: # noqa: ARG002, PLR6301, E501 + def visit_INET( + self, # noqa: PLR6301 + type_: TypeEngine[IPAddress], # noqa: ARG002 + **kw: t.Any, # noqa: ARG002 + ) -> str: """Visit an INET node.""" return "INET" From ab473efb8204867bc09c8e93bfc0b6dbaa1232c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edgar=20Ram=C3=ADrez=20Mondrag=C3=B3n?= Date: Tue, 26 Sep 2023 07:50:05 -0600 Subject: [PATCH 4/8] Add Windows XFAIL --- tests/test_types.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/test_types.py b/tests/test_types.py index dfb6eff..1a9b563 100644 --- a/tests/test_types.py +++ b/tests/test_types.py @@ -3,6 +3,7 @@ from __future__ import annotations import ipaddress +import sys import typing as t import uuid @@ -24,6 +25,10 @@ ) +@pytest.mark.xfail( + sys.platform == "win32", + reason="'ipaddr' extension not available on Windows", +) @pytest.mark.parametrize( ("data", "query", "expected"), [ From a752861ea5d20be83e52aea59c41be1f4c818204 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edgar=20Ram=C3=ADrez=20Mondrag=C3=B3n?= Date: Tue, 26 Sep 2023 14:00:44 -0600 Subject: [PATCH 5/8] Test CIDR directly --- pyproject.toml | 8 +++++--- tests/test_types.py | 45 +++++++++++++++++++++++++++++++++------------ 2 files changed, 38 insertions(+), 15 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index c0b07e0..2ae4fea 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,11 +44,11 @@ dependencies = [ "greenlet>=3.0.0a1", "sqlalchemy>=1.4", "sqlean-py>=0.21.5.1", - 'typing-extensions; python_version < "3.10"', ] [project.optional-dependencies] dev = [ "pytest", + 'typing-extensions; python_version < "3.10"', ] [project.urls] Changelog = "https://github.com/edgarrmondragon/sqlean-driver/blob/main/CHANGELOG.md" @@ -64,10 +64,10 @@ source = "vcs" [tool.hatch.envs.test] dependencies = [ "coverage[toml]>=6.5", - "pytest", "pytest-github-actions-annotate-failures", "sqlalchemy=={matrix:sqlalchemy:2}.*", ] +features = ["dev"] matrix-name-format = "{variable}_{value}" [tool.hatch.envs.test.env-vars] SQLALCHEMY_WARN_20 = "1" @@ -89,7 +89,8 @@ xml = "coverage xml" report = ["coverage combine", "coverage report --show-missing"] [tool.hatch.envs.typing] -dependencies = ["mypy>=1.0.0", "pytest"] +dependencies = ["mypy>=1.0.0"] +features = ["dev"] [tool.hatch.envs.typing.scripts] check = "mypy --strict --install-types --non-interactive {args:src/sqlean_driver tests}" @@ -209,6 +210,7 @@ select = [ target-version = "py38" unfixable = [ "ERA", + "F401", ] [tool.ruff.isort] diff --git a/tests/test_types.py b/tests/test_types.py index 1a9b563..360d43c 100644 --- a/tests/test_types.py +++ b/tests/test_types.py @@ -2,25 +2,34 @@ from __future__ import annotations -import ipaddress import sys import typing as t import uuid +from ipaddress import IPv4Interface, IPv4Network, IPv6Interface import pytest -from sqlalchemy import Column, Integer, MetaData, Table, create_engine, func, select +from sqlalchemy import ( + Column, + Integer, + MetaData, + Table, + create_engine, + func, + select, +) -from sqlean_driver.custom_types import INET, UUID +from sqlean_driver.custom_types import CIDR, INET, UUID if t.TYPE_CHECKING: from sqlalchemy import Select metadata = MetaData() table = Table( - "table", + "test_table", metadata, Column("id", Integer, primary_key=True), Column("ip", INET), + Column("cidr", CIDR), Column("uuid_col", UUID), ) @@ -39,34 +48,46 @@ id="nullable", ), pytest.param( - [{"ip": ipaddress.IPv4Network("192.168.1.1")}], + [{"cidr": None}], + select(table.c.cidr), + (None,), + id="nullable_cidr", + ), + pytest.param( + [{"cidr": IPv4Network("192.168.16.3/32")}], + select(table.c.cidr), + (IPv4Network("192.168.16.3/32"),), + id="cidr", + ), + pytest.param( + [{"ip": IPv4Network("192.168.1.1")}], select(func.ipfamily(table.c.ip), table.c.ip.ipfamily()), (4, 4), id="ipfamily", ), pytest.param( - [{"ip": ipaddress.IPv6Interface("2001:db8::123/64")}], + [{"ip": IPv6Interface("2001:db8::123/64")}], select(func.iphost(table.c.ip), table.c.ip.iphost()), ("2001:db8::123", "2001:db8::123"), id="iphost", ), pytest.param( - [{"ip": ipaddress.IPv4Interface("192.168.16.12/24")}], + [{"ip": IPv4Interface("192.168.16.12/24")}], select(func.ipmasklen(table.c.ip), table.c.ip.ipmasklen()), (24, 24), id="ipmasklen", ), pytest.param( - [{"ip": ipaddress.IPv4Interface("192.168.16.12/24")}], + [{"ip": IPv4Interface("192.168.16.12/24")}], select(func.ipnetwork(table.c.ip), table.c.ip.ipnetwork()), ( - ipaddress.IPv4Network("192.168.16.0/24"), - ipaddress.IPv4Network("192.168.16.0/24"), + IPv4Network("192.168.16.0/24"), + IPv4Network("192.168.16.0/24"), ), id="ipnetwork", ), pytest.param( - [{"ip": ipaddress.IPv4Interface("192.168.16.0/24")}], + [{"ip": IPv4Interface("192.168.16.0/24")}], select( func.ipcontains(table.c.ip, "192.168.16.3"), table.c.ip.ipcontains("192.168.16.3"), @@ -75,7 +96,7 @@ id="ipcontains_lhs", ), pytest.param( - [{"ip": ipaddress.IPv4Interface("192.168.16.3")}], + [{"ip": IPv4Interface("192.168.16.3")}], select( func.ipcontains("192.168.16.0/24", table.c.ip), ), From 6f3e0a410a45c7f2d1459e4ac638b0b17cdaffea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edgar=20Ram=C3=ADrez=20Mondrag=C3=B3n?= Date: Wed, 10 Jan 2024 23:19:15 -0600 Subject: [PATCH 6/8] Remove unused `type: ignore` comments --- src/sqlean_driver/custom_types.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/sqlean_driver/custom_types.py b/src/sqlean_driver/custom_types.py index 716f439..ce21f73 100644 --- a/src/sqlean_driver/custom_types.py +++ b/src/sqlean_driver/custom_types.py @@ -83,23 +83,23 @@ class Comparator( def ipfamily(self) -> _IPAddrIPFamilyFunction: """Return the IP family.""" - return _IPAddrIPFamilyFunction(self.expr) # type: ignore[no-untyped-call] + return _IPAddrIPFamilyFunction(self.expr) def iphost(self) -> _IPAddrIPHostFunction: """Return the IP host.""" - return _IPAddrIPHostFunction(self.expr) # type: ignore[no-untyped-call] + return _IPAddrIPHostFunction(self.expr) def ipmasklen(self) -> _IPAddrIPMaskLenFunction: """Return the IP mask length.""" - return _IPAddrIPMaskLenFunction(self.expr) # type: ignore[no-untyped-call] + return _IPAddrIPMaskLenFunction(self.expr) def ipnetwork(self) -> _IPAddrIPNetworkFunction: """Return the IP network.""" - return _IPAddrIPNetworkFunction(self.expr) # type: ignore[no-untyped-call] + return _IPAddrIPNetworkFunction(self.expr) def ipcontains(self, other: IPAddress | str) -> _IPAddrIPContainsFunction: """Return whether the IP address contains another IP address.""" - return _IPAddrIPContainsFunction(self.expr, other) # type: ignore[no-untyped-call] + return _IPAddrIPContainsFunction(self.expr, other) comparator_factory = Comparator From 7dd3578ee44fe8462119d44b68fd377632963345 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edgar=20Ram=C3=ADrez-Mondrag=C3=B3n?= Date: Thu, 1 Feb 2024 23:29:43 -0600 Subject: [PATCH 7/8] Update --- .pre-commit-config.yaml | 4 ++-- src/sqlean_driver/__init__.py | 6 +++--- src/sqlean_driver/custom_types.py | 10 +++++----- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 70cd724..9bebaf6 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -18,7 +18,7 @@ repos: - id: trailing-whitespace - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.1.11 + rev: v0.1.14 hooks: - id: ruff name: Ruff lint @@ -28,6 +28,6 @@ repos: entry: ruff format - repo: https://github.com/tox-dev/pyproject-fmt - rev: "1.5.3" + rev: "1.6.0" hooks: - id: pyproject-fmt diff --git a/src/sqlean_driver/__init__.py b/src/sqlean_driver/__init__.py index efba862..87697f6 100644 --- a/src/sqlean_driver/__init__.py +++ b/src/sqlean_driver/__init__.py @@ -51,7 +51,7 @@ class uuid4(GenericFunction[uuid.UUID]): # noqa: N801 """ name = "uuid4" - type = UUID() # noqa: A003 + type = UUID() inherit_cache = True @@ -68,7 +68,7 @@ class uuid_str(GenericFunction[uuid.UUID]): # noqa: N801 """ name = "uuid_str" - type = UUID() # noqa: A003 + type = UUID() inherit_cache = True @@ -79,7 +79,7 @@ class uuid_blob(GenericFunction[bytes]): # noqa: N801 """ name = "uuid_blob" - type = sqltypes.BLOB() # noqa: A003 + type = sqltypes.BLOB() inherit_cache = True diff --git a/src/sqlean_driver/custom_types.py b/src/sqlean_driver/custom_types.py index ce21f73..830d25f 100644 --- a/src/sqlean_driver/custom_types.py +++ b/src/sqlean_driver/custom_types.py @@ -150,7 +150,7 @@ class _IPAddrIPFamilyFunction(GenericFunction[int]): """Returns the family of a specified IP address.""" name = "ipfamily" - type = sqltypes.Integer() # noqa: A003 + type = sqltypes.Integer() inherit_cache = True @@ -158,7 +158,7 @@ class _IPAddrIPHostFunction(GenericFunction[str]): """Returns the host part of an IP address.""" name = "iphost" - type = sqltypes.String() # noqa: A003 + type = sqltypes.String() inherit_cache = True @@ -166,7 +166,7 @@ class _IPAddrIPMaskLenFunction(GenericFunction[int]): """Returns the prefix length of an IP address.""" name = "ipmasklen" - type = sqltypes.Integer() # noqa: A003 + type = sqltypes.Integer() inherit_cache = True @@ -174,7 +174,7 @@ class _IPAddrIPNetworkFunction(GenericFunction[IPNetwork]): """Returns the network part of an IP address.""" name = "ipnetwork" - type = CIDR() # noqa: A003 + type = CIDR() inherit_cache = True @@ -182,5 +182,5 @@ class _IPAddrIPContainsFunction(GenericFunction[bool]): """Returns whether an IP address contains another IP address.""" name = "ipcontains" - type = sqltypes.Boolean() # noqa: A003 + type = sqltypes.Boolean() inherit_cache = True From e51dbbe11712e54aac1e35590e5171e7622de110 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 10 Jul 2024 03:12:38 +0000 Subject: [PATCH 8/8] chore: Fix lint errors --- pyproject.toml | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 41e15af..5edbe6e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -59,7 +59,7 @@ optional-dependencies.testing = [ ] optional-dependencies.typing = [ "mypy>=1", - 'typing-extensions; python_version < "3.10"', + "typing-extensions; python_version<'3.10'", ] urls.Changelog = "https://github.com/edgarrmondragon/sqlean-driver/blob/main/CHANGELOG.md" urls.Documentation = "https://github.com/edgarrmondragon/sqlean-driver#readme" @@ -241,15 +241,11 @@ lint.select = [ "W", # pycodestyle (warning) "YTT", # flake8-2020 ] -lint.unfixable = [ - "ERA", - "F401", -] lint.ignore = [ "ANN101", # missing-type-self "ANN102", # missing-type-cls "COM812", # missing-trailing-comma - "FIX002", # line-contains-todo + "FIX002", # line-contains-todo "ISC001", # single-line-implicit-string-concatenation ] lint.per-file-ignores."tests/**/*" = [ @@ -258,20 +254,26 @@ lint.per-file-ignores."tests/**/*" = [ "S101", "TID252", ] +lint.unfixable = [ + "ERA", + "F401", +] +lint.flake8-annotations.allow-star-arg-any = true + lint.flake8-import-conventions.banned-from = [ "typing", ] lint.flake8-import-conventions.extend-aliases.typing = "t" lint.flake8-tidy-imports.ban-relative-imports = "all" -lint.pep8-naming.ignore-names = ["visit_*"] lint.isort.known-first-party = [ "sqlean_driver", ] lint.isort.required-imports = [ "from __future__ import annotations", ] -lint.flake8-annotations.allow-star-arg-any = true - +lint.pep8-naming.ignore-names = [ + "visit_*", +] # Tests can use magic values, assertions, and relative imports lint.pydocstyle.convention = "google" lint.preview = true