Skip to content
This repository has been archived by the owner on Apr 26, 2024. It is now read-only.

_do_execute (database function) could be more precisely typed with better support for ParamSpec #12312

Closed
reivilibre opened this issue Mar 28, 2022 · 7 comments · Fixed by #12666
Labels
T-Task Refactoring, removal, replacement, enabling or disabling functionality, other engineering tasks.

Comments

@reivilibre
Copy link
Contributor

Sprouted from #12311.

Once Mypy supports Concatenate, it could be written as

     def _do_execute(
         self,
         func: Callable[Concatenate[str, P], R],
         sql: str,
         *args: P.args,
         **kwargs: P.kwargs,
     ) -> R:

so long as we also pass kwargs.

@reivilibre reivilibre added T-Task Refactoring, removal, replacement, enabling or disabling functionality, other engineering tasks. Z-Future-Maintenance Things that can't yet be done, but will need cleaning up in a couple of months/releases labels Mar 28, 2022
@clokep
Copy link
Member

clokep commented Mar 28, 2022

Once Mypy supports Concatenate

Which seems to be python/mypy#11847, according to @squahtx

@squahtx
Copy link
Contributor

squahtx commented Mar 28, 2022

related: #11711

@richvdh richvdh added this to the Revisit: Next Month milestone Apr 28, 2022
@squahtx
Copy link
Contributor

squahtx commented Apr 28, 2022

Blocked on #12585

@erikjohnston erikjohnston removed the Z-Future-Maintenance Things that can't yet be done, but will need cleaning up in a couple of months/releases label May 5, 2022
@erikjohnston erikjohnston removed this from the Revisit: Next Month milestone May 5, 2022
@DMRobertson
Copy link
Contributor

DMRobertson commented May 7, 2022

I've just given this a quick try.

I was a bit surprised that we need to include **kwargs: P.kwargs in the outer function definition, but that does seem to be consistent with PEP 612:

A function of type Callable[P, R] can be called with (*args, **kwargs) if and only if args has the type P.args and kwargs has the type P.kwargs, and that those types both originated from the same function declaration.

I was then surprised by the next error from looking at args[0]:

error: Value of type "P.args" is not indexable [index]

def _do_execute(self, func: Callable[..., R], sql: str, *args: Any) -> R:
sql = self._make_sql_one_line(sql)
# TODO(paul): Maybe use 'info' and 'debug' for values?
sql_logger.debug("[SQL] {%s} %s", self.name, sql)
sql = self.database_engine.convert_param_style(sql)
if args:
try:
sql_logger.debug("[SQL values] {%s} %r", self.name, args[0])
except Exception:

This is again consistent with PEP 612:

Inside the function, args has the type P.args, not Tuple[P.args, ...] as would be with a normal annotation (and likewise with the **kwargs)

Given that this is already caught at runtime, I'd be inclined to type-ignore this.

@DMRobertson
Copy link
Contributor

Additionally: This doesn't actually give us a huge amount of type safety. Using a ParamSpec allows us to ensure that the args (and now kwargs) we pass to _do_ execute are suitable for func. In order for this to actually enforce anything, we need meaningful annotations where func is defined and of the arguments where they're passed.

There are four call sites at present, and none of them really have those meaningful annotations AFAICS.

execute and executemany

def execute(self, sql: str, *args: Any) -> None:
self._do_execute(self.txn.execute, sql, *args)
def executemany(self, sql: str, *args: Any) -> None:
self._do_execute(self.txn.executemany, sql, *args)

*args: Any means that we don't get checking here, even though self.txn.execute(many) have annotations.

execute_batch

def execute_batch(self, sql: str, args: Iterable[Iterable[Any]]) -> None:
"""Similar to `executemany`, except `txn.rowcount` will not be correct
afterwards.
More efficient than `executemany` on PostgreSQL
"""
if isinstance(self.database_engine, PostgresEngine):
from psycopg2.extras import execute_batch
self._do_execute(
lambda the_sql: execute_batch(self.txn, the_sql, args), sql

Here the lambda expression has no annotations. Proof by example:

$ cat temp.py
def accepts_string(s: str): print(s)
f = lambda x: accepts_string(x)
reveal_type(f)

2022-05-07 21:38:24 ✔  $ mypy temp.py
temp.py:3: note: Revealed type is "def (x: Any) -> Any"
Success: no issues found in 1 source file

It's also worth noting that the stubs for execute_batch aren't very strict either:

# from reveal_type(execute_batch)
Revealed type is "def (cur: Any, sql: Any, argslist: Any, page_size: builtins.int =)"

This is using the locked version of types-psycopg2, but it's no stricter in the latest release (2.9.13).

execute_values

Very similar to execute_batch.

def execute_values(
self, sql: str, values: Iterable[Iterable[Any]], fetch: bool = True
) -> List[Tuple]:
"""Corresponds to psycopg2.extras.execute_values. Only available when
using postgres.
The `fetch` parameter must be set to False if the query does not return
rows (e.g. INSERTs).
"""
assert isinstance(self.database_engine, PostgresEngine)
from psycopg2.extras import execute_values
return self._do_execute(
lambda the_sql: execute_values(self.txn, the_sql, values, fetch=fetch),
sql,
)

The underlying postgres function has a few more annotations, but not any that would give us more checking.

"def (cur: Any, sql: Any, argslist: Any, template: Union[Any, None] =, page_size: builtins.int =, fetch: builtins.bool =) -> Any

@squahtx
Copy link
Contributor

squahtx commented May 7, 2022

I was then surprised by the next error from looking at args[0]:

error: Value of type "P.args" is not indexable [index]

The next version of mypy will include a fix for this: python/mypy#12668

@DMRobertson
Copy link
Contributor

The next version of mypy will include a fix for this: python/mypy#12668

Good to know. I found python/mypy#11826 by searching, but assumed that the behaviour there was intentional.

DMRobertson pushed a commit that referenced this issue May 7, 2022
I'm not sure this gives us a huge amount of type safety, see this
comment:
#12312 (comment)

In any case, it's a nice bit of practice with `ParamSpec`.
DMRobertson pushed a commit that referenced this issue May 7, 2022
I'm not sure this gives us a huge amount of type safety, see this
comment:
#12312 (comment)

In any case, it's a nice bit of practice with `ParamSpec`.
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
T-Task Refactoring, removal, replacement, enabling or disabling functionality, other engineering tasks.
Projects
None yet
Development

Successfully merging a pull request may close this issue.

6 participants