diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6eb7e389..0f65d6af 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,7 +10,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] + python-version: ["3.10", "3.11", "3.12", "3.13", "3.14", "3.15-dev"] numpy: [0, 1] os: [ubuntu-latest, macos-latest, windows-latest, macos-14] steps: @@ -46,7 +46,7 @@ jobs: - name: run flake8 run: flake8 - name: run ty - if: matrix.os != 'windows-latest' + if: matrix.os != 'windows-latest' && matrix.python-version == '3.14' run: | pip install uv uv run ty check sqlite_utils diff --git a/docs/cli-reference.rst b/docs/cli-reference.rst index 6231dbf2..c3bf7a08 100644 --- a/docs/cli-reference.rst +++ b/docs/cli-reference.rst @@ -117,15 +117,15 @@ See :ref:`cli_query`. --tsv Output TSV --no-headers Omit CSV headers -t, --table Output as a formatted table - --fmt TEXT Table format - one of asciidoc, double_grid, - double_outline, fancy_grid, fancy_outline, github, - grid, heavy_grid, heavy_outline, html, jira, - latex, latex_booktabs, latex_longtable, latex_raw, - mediawiki, mixed_grid, mixed_outline, moinmoin, - orgtbl, outline, pipe, plain, presto, pretty, - psql, rounded_grid, rounded_outline, rst, simple, - simple_grid, simple_outline, textile, tsv, - unsafehtml, youtrack + --fmt TEXT Table format - one of asciidoc, colon_grid, + double_grid, double_outline, fancy_grid, + fancy_outline, github, grid, heavy_grid, + heavy_outline, html, jira, latex, latex_booktabs, + latex_longtable, latex_raw, mediawiki, mixed_grid, + mixed_outline, moinmoin, orgtbl, outline, pipe, + plain, presto, pretty, psql, rounded_grid, + rounded_outline, rst, simple, simple_grid, + simple_outline, textile, tsv, unsafehtml, youtrack --json-cols Detect JSON cols and output them as JSON, not escaped strings -r, --raw Raw output, first column of first row @@ -186,15 +186,15 @@ See :ref:`cli_memory`. --tsv Output TSV --no-headers Omit CSV headers -t, --table Output as a formatted table - --fmt TEXT Table format - one of asciidoc, double_grid, - double_outline, fancy_grid, fancy_outline, github, - grid, heavy_grid, heavy_outline, html, jira, - latex, latex_booktabs, latex_longtable, latex_raw, - mediawiki, mixed_grid, mixed_outline, moinmoin, - orgtbl, outline, pipe, plain, presto, pretty, - psql, rounded_grid, rounded_outline, rst, simple, - simple_grid, simple_outline, textile, tsv, - unsafehtml, youtrack + --fmt TEXT Table format - one of asciidoc, colon_grid, + double_grid, double_outline, fancy_grid, + fancy_outline, github, grid, heavy_grid, + heavy_outline, html, jira, latex, latex_booktabs, + latex_longtable, latex_raw, mediawiki, mixed_grid, + mixed_outline, moinmoin, orgtbl, outline, pipe, + plain, presto, pretty, psql, rounded_grid, + rounded_outline, rst, simple, simple_grid, + simple_outline, textile, tsv, unsafehtml, youtrack --json-cols Detect JSON cols and output them as JSON, not escaped strings -r, --raw Raw output, first column of first row @@ -425,14 +425,15 @@ See :ref:`cli_search`. --tsv Output TSV --no-headers Omit CSV headers -t, --table Output as a formatted table - --fmt TEXT Table format - one of asciidoc, double_grid, - double_outline, fancy_grid, fancy_outline, github, - grid, heavy_grid, heavy_outline, html, jira, latex, - latex_booktabs, latex_longtable, latex_raw, mediawiki, - mixed_grid, mixed_outline, moinmoin, orgtbl, outline, - pipe, plain, presto, pretty, psql, rounded_grid, - rounded_outline, rst, simple, simple_grid, - simple_outline, textile, tsv, unsafehtml, youtrack + --fmt TEXT Table format - one of asciidoc, colon_grid, + double_grid, double_outline, fancy_grid, fancy_outline, + github, grid, heavy_grid, heavy_outline, html, jira, + latex, latex_booktabs, latex_longtable, latex_raw, + mediawiki, mixed_grid, mixed_outline, moinmoin, orgtbl, + outline, pipe, plain, presto, pretty, psql, + rounded_grid, rounded_outline, rst, simple, + simple_grid, simple_outline, textile, tsv, unsafehtml, + youtrack --json-cols Detect JSON cols and output them as JSON, not escaped strings --load-extension TEXT Path to SQLite extension, with optional :entrypoint @@ -690,14 +691,15 @@ See :ref:`cli_tables`. --tsv Output TSV --no-headers Omit CSV headers -t, --table Output as a formatted table - --fmt TEXT Table format - one of asciidoc, double_grid, - double_outline, fancy_grid, fancy_outline, github, - grid, heavy_grid, heavy_outline, html, jira, latex, - latex_booktabs, latex_longtable, latex_raw, mediawiki, - mixed_grid, mixed_outline, moinmoin, orgtbl, outline, - pipe, plain, presto, pretty, psql, rounded_grid, - rounded_outline, rst, simple, simple_grid, - simple_outline, textile, tsv, unsafehtml, youtrack + --fmt TEXT Table format - one of asciidoc, colon_grid, + double_grid, double_outline, fancy_grid, fancy_outline, + github, grid, heavy_grid, heavy_outline, html, jira, + latex, latex_booktabs, latex_longtable, latex_raw, + mediawiki, mixed_grid, mixed_outline, moinmoin, orgtbl, + outline, pipe, plain, presto, pretty, psql, + rounded_grid, rounded_outline, rst, simple, + simple_grid, simple_outline, textile, tsv, unsafehtml, + youtrack --json-cols Detect JSON cols and output them as JSON, not escaped strings --columns Include list of columns for each table @@ -731,14 +733,15 @@ See :ref:`cli_views`. --tsv Output TSV --no-headers Omit CSV headers -t, --table Output as a formatted table - --fmt TEXT Table format - one of asciidoc, double_grid, - double_outline, fancy_grid, fancy_outline, github, - grid, heavy_grid, heavy_outline, html, jira, latex, - latex_booktabs, latex_longtable, latex_raw, mediawiki, - mixed_grid, mixed_outline, moinmoin, orgtbl, outline, - pipe, plain, presto, pretty, psql, rounded_grid, - rounded_outline, rst, simple, simple_grid, - simple_outline, textile, tsv, unsafehtml, youtrack + --fmt TEXT Table format - one of asciidoc, colon_grid, + double_grid, double_outline, fancy_grid, fancy_outline, + github, grid, heavy_grid, heavy_outline, html, jira, + latex, latex_booktabs, latex_longtable, latex_raw, + mediawiki, mixed_grid, mixed_outline, moinmoin, orgtbl, + outline, pipe, plain, presto, pretty, psql, + rounded_grid, rounded_outline, rst, simple, + simple_grid, simple_outline, textile, tsv, unsafehtml, + youtrack --json-cols Detect JSON cols and output them as JSON, not escaped strings --columns Include list of columns for each view @@ -777,15 +780,15 @@ See :ref:`cli_rows`. --tsv Output TSV --no-headers Omit CSV headers -t, --table Output as a formatted table - --fmt TEXT Table format - one of asciidoc, double_grid, - double_outline, fancy_grid, fancy_outline, github, - grid, heavy_grid, heavy_outline, html, jira, - latex, latex_booktabs, latex_longtable, latex_raw, - mediawiki, mixed_grid, mixed_outline, moinmoin, - orgtbl, outline, pipe, plain, presto, pretty, - psql, rounded_grid, rounded_outline, rst, simple, - simple_grid, simple_outline, textile, tsv, - unsafehtml, youtrack + --fmt TEXT Table format - one of asciidoc, colon_grid, + double_grid, double_outline, fancy_grid, + fancy_outline, github, grid, heavy_grid, + heavy_outline, html, jira, latex, latex_booktabs, + latex_longtable, latex_raw, mediawiki, mixed_grid, + mixed_outline, moinmoin, orgtbl, outline, pipe, + plain, presto, pretty, psql, rounded_grid, + rounded_outline, rst, simple, simple_grid, + simple_outline, textile, tsv, unsafehtml, youtrack --json-cols Detect JSON cols and output them as JSON, not escaped strings --load-extension TEXT Path to SQLite extension, with optional @@ -817,14 +820,15 @@ See :ref:`cli_triggers`. --tsv Output TSV --no-headers Omit CSV headers -t, --table Output as a formatted table - --fmt TEXT Table format - one of asciidoc, double_grid, - double_outline, fancy_grid, fancy_outline, github, - grid, heavy_grid, heavy_outline, html, jira, latex, - latex_booktabs, latex_longtable, latex_raw, mediawiki, - mixed_grid, mixed_outline, moinmoin, orgtbl, outline, - pipe, plain, presto, pretty, psql, rounded_grid, - rounded_outline, rst, simple, simple_grid, - simple_outline, textile, tsv, unsafehtml, youtrack + --fmt TEXT Table format - one of asciidoc, colon_grid, + double_grid, double_outline, fancy_grid, fancy_outline, + github, grid, heavy_grid, heavy_outline, html, jira, + latex, latex_booktabs, latex_longtable, latex_raw, + mediawiki, mixed_grid, mixed_outline, moinmoin, orgtbl, + outline, pipe, plain, presto, pretty, psql, + rounded_grid, rounded_outline, rst, simple, + simple_grid, simple_outline, textile, tsv, unsafehtml, + youtrack --json-cols Detect JSON cols and output them as JSON, not escaped strings --load-extension TEXT Path to SQLite extension, with optional :entrypoint @@ -856,14 +860,15 @@ See :ref:`cli_indexes`. --tsv Output TSV --no-headers Omit CSV headers -t, --table Output as a formatted table - --fmt TEXT Table format - one of asciidoc, double_grid, - double_outline, fancy_grid, fancy_outline, github, - grid, heavy_grid, heavy_outline, html, jira, latex, - latex_booktabs, latex_longtable, latex_raw, mediawiki, - mixed_grid, mixed_outline, moinmoin, orgtbl, outline, - pipe, plain, presto, pretty, psql, rounded_grid, - rounded_outline, rst, simple, simple_grid, - simple_outline, textile, tsv, unsafehtml, youtrack + --fmt TEXT Table format - one of asciidoc, colon_grid, + double_grid, double_outline, fancy_grid, fancy_outline, + github, grid, heavy_grid, heavy_outline, html, jira, + latex, latex_booktabs, latex_longtable, latex_raw, + mediawiki, mixed_grid, mixed_outline, moinmoin, orgtbl, + outline, pipe, plain, presto, pretty, psql, + rounded_grid, rounded_outline, rst, simple, + simple_grid, simple_outline, textile, tsv, unsafehtml, + youtrack --json-cols Detect JSON cols and output them as JSON, not escaped strings --load-extension TEXT Path to SQLite extension, with optional :entrypoint diff --git a/docs/cli.rst b/docs/cli.rst index a6081609..415e4c28 100644 --- a/docs/cli.rst +++ b/docs/cli.rst @@ -257,6 +257,7 @@ Available ``--fmt`` options are: .. ]]] - ``asciidoc`` +- ``colon_grid`` - ``double_grid`` - ``double_outline`` - ``fancy_grid`` diff --git a/pyproject.toml b/pyproject.toml index a50a3a8e..5bfa4922 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,7 +33,7 @@ dependencies = [ [dependency-groups] dev = [ - "black>=24.1.1", + "black>=26.3.1", "cogapp", "hypothesis", "pytest", @@ -47,7 +47,9 @@ dev = [ # flake8 "flake8", "flake8-pyproject", - "ty", + "ty>=0.0.37", + # For stable cog: + "tabulate>=0.10.0", ] docs = [ "beanbag-docutils>=2.0", diff --git a/sqlite_utils/cli.py b/sqlite_utils/cli.py index 9b9ee20e..5844dfc0 100644 --- a/sqlite_utils/cli.py +++ b/sqlite_utils/cli.py @@ -1,7 +1,7 @@ import base64 from typing import Any import click -from click_default_group import DefaultGroup # type: ignore +from click_default_group import DefaultGroup from datetime import datetime, timezone import hashlib import pathlib @@ -42,7 +42,6 @@ TypeTracker, ) - CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"]) @@ -223,7 +222,7 @@ def _iter(): else: items = db.table_names(fts4=fts4, fts5=fts5) for name in items: - row = [name] + row: list[Any] = [name] if counts: row.append(method(name).count) if columns: @@ -2122,8 +2121,9 @@ def _execute_query( cursor = [[cursor.rowcount]] else: headers = [c[0] for c in cursor.description] + cursor_or_rows: Any = cursor if raw: - row = cursor.fetchone() # type: ignore[union-attr] + row = cursor_or_rows.fetchone() data = row[0] if row else None if isinstance(data, bytes): sys.stdout.buffer.write(data) @@ -2911,8 +2911,7 @@ def _analyze(db, tables, columns, save, common_limit=10, no_most=False, no_least ) details = ( ( - textwrap.dedent( - """ + textwrap.dedent(""" {table}.{column}: ({i}/{total}) Total rows: {total_rows} @@ -2920,8 +2919,7 @@ def _analyze(db, tables, columns, save, common_limit=10, no_most=False, no_least Blank rows: {num_blank} Distinct values: {num_distinct}{most_common_rendered}{least_common_rendered} - """ - ) + """) .strip() .format( i=i + 1, @@ -2968,8 +2966,7 @@ def uninstall(packages, yes): def _generate_convert_help(): - help = textwrap.dedent( - """ + help = textwrap.dedent(""" Convert columns using Python code you supply. For example: \b @@ -2982,8 +2979,7 @@ def _generate_convert_help(): Use "-" for CODE to read Python code from standard input. The following common operations are available as recipe functions: - """ - ).strip() + """).strip() recipe_names = [ n for n in dir(recipes) @@ -2997,15 +2993,13 @@ def _generate_convert_help(): name, str(inspect.signature(fn)), textwrap.dedent(fn.__doc__.rstrip()) ) help += "\n\n" - help += textwrap.dedent( - """ + help += textwrap.dedent(""" You can use these recipes like so: \b sqlite-utils convert my.db mytable mycolumn \\ 'r.jsonsplit(value, delimiter=":")' - """ - ).strip() + """).strip() return help diff --git a/sqlite_utils/db.py b/sqlite_utils/db.py index aacdc893..ed3fc7af 100644 --- a/sqlite_utils/db.py +++ b/sqlite_utils/db.py @@ -15,6 +15,7 @@ import contextlib import datetime import decimal +import importlib import inspect import itertools import json @@ -22,7 +23,7 @@ import pathlib import re import secrets -from sqlite_fts4 import rank_bm25 # type: ignore +from sqlite_fts4 import rank_bm25 import textwrap from typing import ( cast, @@ -43,7 +44,7 @@ from sqlite_utils.plugins import pm try: - from sqlite_dump import iterdump # type: ignore[import-not-found] + iterdump = importlib.import_module("sqlite_dump").iterdump except ImportError: iterdump = None @@ -82,15 +83,17 @@ def quote_identifier(identifier: str) -> str: return '"{}"'.format(identifier.replace('"', '""')) +pd: Any = None try: - import pandas as pd # type: ignore + pd = importlib.import_module("pandas") except ImportError: - pd = None # type: ignore + pd = None +np: Any = None try: - import numpy as np # type: ignore + np = importlib.import_module("numpy") except ImportError: - np = None # type: ignore + np = None Column = namedtuple( "Column", ("cid", "name", "type", "notnull", "default_value", "is_pk") @@ -190,7 +193,10 @@ class Default: DEFAULT = Default() -COLUMN_TYPE_MAPPING = { +Tracer = Callable[[str, Optional[Union[Sequence[Any], Dict[str, Any]]]], None] + + +COLUMN_TYPE_MAPPING: Dict[Any, str] = { float: "REAL", int: "INTEGER", bool: "INTEGER", @@ -339,7 +345,7 @@ def __init__( memory_name: Optional[str] = None, recreate: bool = False, recursive_triggers: bool = True, - tracer: Optional[Callable] = None, + tracer: Optional[Tracer] = None, use_counts_table: bool = False, execute_plugins: bool = True, use_old_upsert: bool = False, @@ -375,8 +381,8 @@ def __init__( self.conn = sqlite3.connect(str(filename_or_conn)) else: assert not recreate, "recreate cannot be used with connections, only paths" - self.conn = filename_or_conn - self._tracer = tracer + self.conn = cast(sqlite3.Connection, filename_or_conn) + self._tracer: Optional[Tracer] = tracer if recursive_triggers: self.execute("PRAGMA recursive_triggers=on;") self._registered_functions: set = set() @@ -421,7 +427,7 @@ def ensure_autocommit_off(self) -> Generator[None, None, None]: @contextlib.contextmanager def tracer( - self, tracer: Optional[Callable[[str, Optional[Sequence]], None]] = None + self, tracer: Optional[Tracer] = None ) -> Generator["Database", None, None]: """ Context manager to temporarily set a tracer function - all executed SQL queries will @@ -439,7 +445,7 @@ def tracer( :param tracer: Callable accepting ``sql`` and ``parameters`` arguments """ prev_tracer = self._tracer - self._tracer = tracer or print + self._tracer = tracer or cast(Tracer, print) try: yield self finally: @@ -2275,12 +2281,10 @@ def create_index( "{}_{}".format(index_name, suffix) if suffix else index_name ) sql = ( - textwrap.dedent( - """ + textwrap.dedent(""" CREATE {unique}INDEX {if_not_exists}{index_name} ON {table_name} ({columns}); - """ - ) + """) .strip() .format( index_name=quote_identifier(created_index_name), @@ -2475,8 +2479,7 @@ def enable_counts(self) -> None: See :ref:`python_api_cached_table_counts` for details. """ sql = ( - textwrap.dedent( - """ + textwrap.dedent(""" {create_counts_table} CREATE TRIGGER IF NOT EXISTS {trigger_insert} AFTER INSERT ON {table} BEGIN @@ -2501,8 +2504,7 @@ def enable_counts(self) -> None: ); END; INSERT OR REPLACE INTO _counts VALUES ({table_quoted}, (select count(*) from {table})); - """ - ) + """) .strip() .format( create_counts_table=_COUNTS_TABLE_CREATE_SQL.format( @@ -2554,14 +2556,12 @@ def enable_fts( :param replace: Should any existing FTS index for this table be replaced by the new one? """ create_fts_sql = ( - textwrap.dedent( - """ + textwrap.dedent(""" CREATE VIRTUAL TABLE {table_fts} USING {fts_version} ( {columns},{tokenize} content={table} ) - """ - ) + """) .strip() .format( table=quote_identifier(self.name), @@ -2599,8 +2599,7 @@ def enable_fts( table = quote_identifier(self.name) table_fts = quote_identifier(self.name + "_fts") triggers = ( - textwrap.dedent( - """ + textwrap.dedent(""" CREATE TRIGGER {table_ai} AFTER INSERT ON {table} BEGIN INSERT INTO {table_fts} (rowid, {columns}) VALUES (new.rowid, {new_cols}); END; @@ -2611,8 +2610,7 @@ def enable_fts( INSERT INTO {table_fts} ({table_fts}, rowid, {columns}) VALUES('delete', old.rowid, {old_cols}); INSERT INTO {table_fts} (rowid, {columns}) VALUES (new.rowid, {new_cols}); END; - """ - ) + """) .strip() .format( table=table, @@ -2637,12 +2635,10 @@ def populate_fts(self, columns: Iterable[str]) -> "Table": """ columns_quoted = ", ".join(quote_identifier(c) for c in columns) sql = ( - textwrap.dedent( - """ + textwrap.dedent(""" INSERT INTO {table_fts} (rowid, {columns}) SELECT rowid, {columns} FROM {table}; - """ - ) + """) .strip() .format( table=quote_identifier(self.name), @@ -2659,17 +2655,11 @@ def disable_fts(self) -> "Table": if fts_table: self.db[fts_table].drop() # Now delete the triggers that related to that table - sql = ( - textwrap.dedent( - """ + sql = textwrap.dedent(""" SELECT name FROM sqlite_master WHERE type = 'trigger' AND (sql LIKE '% INSERT INTO [{}]%' OR sql LIKE '% INSERT INTO "{}"%') - """ - ) - .strip() - .format(fts_table, fts_table) - ) + """).strip().format(fts_table, fts_table) trigger_names = [] for row in self.db.execute(sql).fetchall(): trigger_names.append(row[0]) @@ -2695,8 +2685,7 @@ def rebuild_fts(self) -> "Table": def detect_fts(self) -> Optional[str]: "Detect if table has a corresponding FTS virtual table and return it" - sql = textwrap.dedent( - """ + sql = textwrap.dedent(""" SELECT name FROM sqlite_master WHERE rootpage = 0 AND ( @@ -2707,8 +2696,7 @@ def detect_fts(self) -> Optional[str]: AND sql LIKE '%VIRTUAL TABLE%USING FTS%' ) ) - """ - ).strip() + """).strip() args = { "like": '%VIRTUAL TABLE%USING FTS%content="{}"%'.format(self.name), "like2": '%VIRTUAL TABLE%USING FTS%content="{}"%'.format(self.name), @@ -2724,13 +2712,9 @@ def optimize(self) -> "Table": "Run the ``optimize`` operation against the associated full-text search index table." fts_table = self.detect_fts() if fts_table is not None: - self.db.execute( - """ + self.db.execute(""" INSERT INTO {table} ({table}) VALUES ("optimize"); - """.strip().format( - table=quote_identifier(fts_table) - ) - ) + """.strip().format(table=quote_identifier(fts_table))) return self def search_sql( @@ -2768,8 +2752,7 @@ def search_sql( ) fts_table_quoted = quote_identifier(fts_table) virtual_table_using = self.db.table(fts_table).virtual_table_using - sql = textwrap.dedent( - """ + sql = textwrap.dedent(""" with {original} as ( select rowid, @@ -2786,8 +2769,7 @@ def search_sql( order by {order_by} {limit_offset} - """ - ).strip() + """).strip() if virtual_table_using == "FTS5": rank_implementation = "{}.rank".format(fts_table_quoted) else: @@ -3517,7 +3499,7 @@ def insert_all( raise ValueError( "When using list-based iteration, the first yielded value must be a list of column name strings" ) - column_names = list(first_record) + column_names = cast(List[str], list(first_record)) all_columns = column_names num_columns = len(column_names) # Get the actual first data record @@ -3559,7 +3541,8 @@ def insert_all( chunk_as_dicts = [dict(zip(column_names, row)) for row in chunk] column_types = suggest_column_types(chunk_as_dicts) else: - column_types = suggest_column_types(chunk) # type: ignore[arg-type] + dict_chunk = cast(List[Dict[str, Any]], chunk) + column_types = suggest_column_types(dict_chunk) if extracts: for col in extracts: if col in column_types: @@ -3586,14 +3569,14 @@ def insert_all( all_columns.insert(0, hash_id) else: all_columns_set: Set[str] = set() - for record in chunk: - all_columns_set.update(record.keys()) # type: ignore[union-attr] + for record in cast(List[Dict[str, Any]], chunk): + all_columns_set.update(record.keys()) all_columns = list(sorted(all_columns_set)) if hash_id: all_columns.insert(0, hash_id) else: if not list_mode: - for record in chunk: + for record in cast(List[Dict[str, Any]], chunk): all_columns += [ column for column in record if column not in all_columns ] @@ -3791,6 +3774,7 @@ def lookup( :param strict: Boolean, apply STRICT mode if creating the table. """ assert isinstance(lookup_values, dict) + assert pk is not None if extra_values is not None: assert isinstance(extra_values, dict) combined_values = dict(lookup_values) @@ -3810,7 +3794,7 @@ def lookup( ) ) try: - return rows[0][pk] # type: ignore[index] + return rows[0][pk] except IndexError: return self.insert( combined_values, diff --git a/sqlite_utils/utils.py b/sqlite_utils/utils.py index 05e9a511..0ca98fc9 100644 --- a/sqlite_utils/utils.py +++ b/sqlite_utils/utils.py @@ -3,6 +3,7 @@ import csv import enum import hashlib +import importlib import io import itertools import json @@ -21,6 +22,7 @@ Set, Tuple, Type, + TYPE_CHECKING, TypeVar, Union, cast, @@ -30,22 +32,26 @@ from . import recipes -try: - import pysqlite3 as sqlite3 # type: ignore[import-not-found] # noqa: F401 - from pysqlite3 import dbapi2 # type: ignore[import-not-found] # noqa: F401 +if TYPE_CHECKING: + import sqlite3 # noqa: F401 + from sqlite3 import dbapi2 # noqa: F401 OperationalError = dbapi2.OperationalError -except ImportError: +else: try: - import sqlean as sqlite3 # type: ignore[import-not-found] # noqa: F401 - from sqlean import dbapi2 # type: ignore[import-not-found] # noqa: F401 - + sqlite3 = importlib.import_module("pysqlite3") + dbapi2 = importlib.import_module("pysqlite3.dbapi2") OperationalError = dbapi2.OperationalError except ImportError: - import sqlite3 # noqa: F401 - from sqlite3 import dbapi2 # noqa: F401 + try: + sqlite3 = importlib.import_module("sqlean") + dbapi2 = importlib.import_module("sqlean.dbapi2") + OperationalError = dbapi2.OperationalError + except ImportError: + import sqlite3 # noqa: F401 + from sqlite3 import dbapi2 # noqa: F401 - OperationalError = dbapi2.OperationalError + OperationalError = dbapi2.OperationalError SPATIALITE_PATHS = ( @@ -60,7 +66,7 @@ ORIGINAL_CSV_FIELD_SIZE_LIMIT = csv.field_size_limit() # Type alias for row dictionaries - values can be various SQLite-compatible types -RowValue = Union[None, int, float, str, bytes, bool] +RowValue = Union[None, int, float, str, bytes, bool, List[str]] Row = Dict[str, RowValue] T = TypeVar("T") @@ -284,7 +290,7 @@ def _extra_key_strategy( else: extras_value = row.pop(None) row_out = cast(Row, row) - row_out[extras_key] = extras_value # type: ignore[assignment] + row_out[extras_key] = cast(RowValue, extras_value) yield row_out @@ -464,14 +470,14 @@ def get_tests(cls) -> List[str]: def test_integer(self, value: object) -> bool: try: - int(value) # type: ignore + int(cast(Any, value)) return True except (ValueError, TypeError): return False def test_float(self, value: object) -> bool: try: - float(value) # type: ignore[arg-type] + float(cast(Any, value)) return True except (ValueError, TypeError): return False diff --git a/tests/conftest.py b/tests/conftest.py index 3990d76e..6fd35b85 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -42,14 +42,12 @@ def fresh_db(): @pytest.fixture def existing_db(): database = Database(memory=True) - database.executescript( - """ + database.executescript(""" CREATE TABLE foo (text TEXT); INSERT INTO foo (text) values ("one"); INSERT INTO foo (text) values ("two"); INSERT INTO foo (text) values ("three"); - """ - ) + """) return database diff --git a/tests/test_analyze_tables.py b/tests/test_analyze_tables.py index 4618eff1..a2ce585d 100644 --- a/tests/test_analyze_tables.py +++ b/tests/test_analyze_tables.py @@ -143,10 +143,7 @@ def db_to_analyze_path(db_to_analyze, tmpdir): def test_analyze_table(db_to_analyze_path): result = CliRunner().invoke(cli.cli, ["analyze-tables", db_to_analyze_path]) - assert ( - result.output.strip() - == ( - """ + assert result.output.strip() == (""" stuff.id: (1/3) Total rows: 8 @@ -179,9 +176,7 @@ def test_analyze_table(db_to_analyze_path): Most common: 5: 5 - 3: 4""" - ).strip() - ) + 3: 4""").strip() def test_analyze_table_save(db_to_analyze_path): diff --git a/tests/test_cli.py b/tests/test_cli.py index 40c3595c..40b36854 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -967,12 +967,9 @@ def test_query_json_with_json_cols(db_path): result = CliRunner().invoke( cli.cli, [db_path, "select id, name, friends from dogs"] ) - assert ( - r""" + assert r""" [{"id": 1, "name": "Cleo", "friends": "[{\"name\": \"Pancakes\"}, {\"name\": \"Bailey\"}]"}] - """.strip() - == result.output.strip() - ) + """.strip() == result.output.strip() # With --json-cols: result = CliRunner().invoke( cli.cli, [db_path, "select id, name, friends from dogs", "--json-cols"] @@ -1998,12 +1995,10 @@ def test_search_quote(tmpdir): def test_indexes(tmpdir): db_path = str(tmpdir / "test.db") db = Database(db_path) - db.conn.executescript( - """ + db.conn.executescript(""" create table Gosh (c1 text, c2 text, c3 text); create index Gosh_idx on Gosh(c2, c3 desc); - """ - ) + """) result = CliRunner().invoke( cli.cli, ["indexes", str(db_path)], @@ -2094,16 +2089,12 @@ def test_triggers(tmpdir, extra_args, expected): pk="id", ) db["counter"].insert({"count": 1}) - db.conn.execute( - textwrap.dedent( - """ + db.conn.execute(textwrap.dedent(""" CREATE TRIGGER blah AFTER INSERT ON articles BEGIN UPDATE counter SET count = count + 1; END - """ - ) - ) + """)) args = ["triggers", db_path] if extra_args: args.extend(extra_args) diff --git a/tests/test_cli_convert.py b/tests/test_cli_convert.py index 387b3181..443e72c6 100644 --- a/tests/test_cli_convert.py +++ b/tests/test_cli_convert.py @@ -371,16 +371,14 @@ def test_convert_multi_complex_column_types(fresh_db_and_path): ], pk="id", ) - code = textwrap.dedent( - """ + code = textwrap.dedent(""" if value == 1: return {"is_str": "", "is_float": 1.2, "is_int": None} elif value == 2: return {"is_float": 1, "is_int": 12} elif value == 3: return {"is_bytes": b"blah"} - """ - ) + """) result = CliRunner().invoke( cli.cli, [ diff --git a/tests/test_create.py b/tests/test_create.py index ea09c459..b1a6ad1f 100644 --- a/tests/test_create.py +++ b/tests/test_create.py @@ -20,7 +20,6 @@ import pytest import uuid - try: import pandas as pd # type: ignore except ImportError: diff --git a/tests/test_default_value.py b/tests/test_default_value.py index 9ffdb144..c594c9f5 100644 --- a/tests/test_default_value.py +++ b/tests/test_default_value.py @@ -1,6 +1,5 @@ import pytest - EXAMPLES = [ ("TEXT DEFAULT 'foo'", "'foo'", "'foo'"), ("TEXT DEFAULT 'foo)'", "'foo)'", "'foo)'"), diff --git a/tests/test_duplicate.py b/tests/test_duplicate.py index 552f697c..28961d2e 100644 --- a/tests/test_duplicate.py +++ b/tests/test_duplicate.py @@ -5,14 +5,12 @@ def test_duplicate(fresh_db): # Create table using native Sqlite statement: - fresh_db.execute( - """CREATE TABLE "table1" ( + fresh_db.execute("""CREATE TABLE "table1" ( "text_col" TEXT, "real_col" REAL, "int_col" INTEGER, "bool_col" INTEGER, - "datetime_col" TEXT)""" - ) + "datetime_col" TEXT)""") # Insert one row of mock data: dt = datetime.datetime.now() data = { diff --git a/tests/test_extract.py b/tests/test_extract.py index 435c1aea..d24c5974 100644 --- a/tests/test_extract.py +++ b/tests/test_extract.py @@ -126,9 +126,7 @@ def test_extract_rowid_table(fresh_db): ' "common_name_latin_name_id" INTEGER REFERENCES "common_name_latin_name"("id")\n' ")" ) - assert ( - fresh_db.execute( - """ + assert fresh_db.execute(""" select tree.name, common_name_latin_name.common_name, @@ -136,10 +134,7 @@ def test_extract_rowid_table(fresh_db): from tree join common_name_latin_name on tree.common_name_latin_name_id = common_name_latin_name.id - """ - ).fetchall() - == [("Tree 1", "Palm", "Arecaceae")] - ) + """).fetchall() == [("Tree 1", "Palm", "Arecaceae")] def test_reuse_lookup_table(fresh_db): diff --git a/tests/test_introspect.py b/tests/test_introspect.py index ab61c158..4ff3f772 100644 --- a/tests/test_introspect.py +++ b/tests/test_introspect.py @@ -109,13 +109,11 @@ def test_table_repr(fresh_db): def test_indexes(fresh_db): - fresh_db.executescript( - """ + fresh_db.executescript(""" create table Gosh (c1 text, c2 text, c3 text); create index Gosh_c1 on Gosh(c1); create index Gosh_c2c3 on Gosh(c2, c3); - """ - ) + """) assert [ Index( seq=0, @@ -130,13 +128,11 @@ def test_indexes(fresh_db): def test_xindexes(fresh_db): - fresh_db.executescript( - """ + fresh_db.executescript(""" create table Gosh (c1 text, c2 text, c3 text); create index Gosh_c1 on Gosh(c1); create index Gosh_c2c3 on Gosh(c2, c3 desc); - """ - ) + """) assert fresh_db["Gosh"].xindexes == [ XIndex( name="Gosh_c2c3", diff --git a/tests/test_transform.py b/tests/test_transform.py index e763a6cd..5eb501db 100644 --- a/tests/test_transform.py +++ b/tests/test_transform.py @@ -638,15 +638,13 @@ def test_transform_with_indexes_errors(fresh_db, transform_params): def test_transform_with_unique_constraint_implicit_index(fresh_db): dogs = fresh_db["dogs"] # Create a table with a UNIQUE constraint on 'name', which creates an implicit index - fresh_db.execute( - """ + fresh_db.execute(""" CREATE TABLE dogs ( id INTEGER PRIMARY KEY, name TEXT UNIQUE, age INTEGER ); - """ - ) + """) dogs.insert({"id": 1, "name": "Cleo", "age": 5}) # Attempt to transform the table without modifying 'name'