| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
Sorry, something went wrong.
|
One thing to note is that the handling of Record isn't quite what I'd like it to be yet. Currently, a user could do something like this: class MyRecord(asyncpg.Record):
id: int
name: str
created_at: datetime.datetime
r: MyRecord = ...
reveal_type(r.id) # intThe user will only use this class for type checking and can use attribute access with type checking, but not lookup notation. A mypy plugin can probably be written to handle the lookup notation (TypedDict has something similar), so this isn't a big issue. The bigger issue is the following will be an error: records: typing.List[MyRecord] = await conn.fetch('SELECT ... from ...')It's an error because fetch() is typed to return List[Record], which doesn't match List[MyRecord] (List is invariant, so it has to match exactly). There are two options:
record: MyRecord = await conn.fetchrow('SELECT ... FROM ...')
records: typing.List[MyRecord] = await conn.fetch('SELECT ... FROM ...')
cursor: asyncpg.cursor.CursorFactory[MyRecord] = await conn.cursor('SELECT ... FROM ...')
record2: asyncpg.Record = await conn.fetchrow('SELECT ... FROM ...')
records2: typing.List[asyncpg.Record] = await conn.fetch('SELECT ... FROM ...')
cursor2: asyncpg.cursor.CursorFactory[asyncpg.Record] = await conn.cursor('SELECT ... FROM ...')
# the variables below have the same types as the last code block,
# but the types are inferred instead
record = await conn.fetch('SELECT ... FROM ...', record_class=MyRecord)
records = await conn.fetch('SELECT ... FROM ...', record_class=MyRecord)
cursor = await conn.cursor('SELECT ... FROM ...', record_class=MyRecord)
record2 = await conn.fetchrow('SELECT ... FROM ...')
records2 = await conn.fetch('SELECT ... FROM ...')
cursor2 = await conn.cursor('SELECT ... FROM ...')Note that while mypy will see record as a MyRecord, it will be an instance of Record at runtime because record_class is only used to infer the type of the return. I have the second option coded locally, but I wanted to check if adding that extra record_class parameter for type-checking purposes is OK. |
Sorry, something went wrong.
|
There's a third option: lie about the return type and say it's a Sequence[Record] instead. I don't think the results of fetch() are mutated that often (why would you?) so this shouldn't be much of an issue. |
Sorry, something went wrong.
|
Typing it as Sequence[Record] kind of works, but you'd still need a cast to go from Sequence[Record] to Sequence[MyRecord]. Combining Sequence with option 2 is better than List (since Sequence is covariant), but it still requires adding type information for all of your variables storing the results from functions returning Record. |
Sorry, something went wrong.
Right. Well, my concern with the record_class parameter is that it could be very confusing to unknowing users. If we named it record_class, then one might rightfully assume that the returned records will be instances of that class, when, in fact, they wouldn't be. You'd mentioned that a plugin is necessary to handle subscript access, so perhaps said plugin can also deal with the fetch calls (via a function hook). The plugin would only need to check that the lval type is a covariant sequence of Record and adjust the return type of fetch() to it to make mypy happy. |
Sorry, something went wrong.
I understand the concern and share your concern. Would record_type be less confusing?
I'll have to do some digging to see if this is possible. If it is, I agree that it would probably be the best solution. |
Sorry, something went wrong.
|
I've done quite a bit more work on the mypy plugin and have the following working: import asyncpg
import datetime
import typing
import typing_extensions
class MyRecord(asyncpg.Record):
foo: int
bar: typing.Optional[str]
class MyOtherRecord(MyRecord):
baz: datetime.datetime
async def main() -> None:
conn = await asyncpg.connect(...)
m = typing.cast(MyRecord, await conn.fetchrow('SELECT foo, bar FROM records'))
o = typing.cast(MyOtherRecord, await conn.fetchrow('SELECT foo, bar, baz FROM other_records'))
key = 'baz'
fkey: typing_extensions.Final = 'baz'
reveal_type(m['foo']) # int
reveal_type(m['bar']) # Optional[str]
reveal_type(m['baz']) # error: "MyRecord" has no key 'baz'
reveal_type(m[0]) # int
reveal_type(m[1]) # Optional[str]
reveal_type(m[2]) # error: "MyRecord" has no index 2
reveal_type(m.get('foo')) # int
reveal_type(m.get('bar')) # Optional[str]
reveal_type(m.get('baz')) # error: "MyRecord" has no key 'baz'
reveal_type(m.get('baz', 1)) # Literal[1]
reveal_type(m.get(key, 1)) # Union[Any, int]
reveal_type(m.get(fkey, 1)) # Literal[1]
reveal_type(o['foo']) # int
reveal_type(o['bar']) # Optional[str]
reveal_type(o['baz']) # datetime
reveal_type(o[0]) # int
reveal_type(o[1]) # Optional[str]
reveal_type(o[2]) # datetime
reveal_type(o.get('foo')) # int
reveal_type(o.get('bar')) # Optional[str]
reveal_type(o.get('baz')) # datetime
reveal_type(o.get('baz', 1)) # datetime
reveal_type(o.get(key, 1)) # Union[Any, int]
reveal_type(o.get(fkey, 1)) # datetimeBased on the implementation of asyncpg.Record, I believe I have the typing for __getitem__() and get() correct. I tried to get the typings for Record to be as similar to TypedDict as possible (given the implementation differences). You'll notice that when the key can be determined by the type system, get() with a default argument is deterministic (ex. o.get('baz', 1) and o.get(fkey, 1)), otherwise it returns a Union. One thing I'd like to possibly try is to come up with a metaclass that would act like a typing_extensions.Protocol with runtime checking so instanceof() could be used to determine if a Record matched. At this time, I haven't attempted it. I also did a lot of poking around to try and get the plugin to set the return type based on the variable it is being assigned to, but I don't see a way that it's possible. This means we're left with two options:
stmt = typing.cast(asyncpg.prepared_stmt.PreparedStatement[MyRecord], await conn.prepare('SELECT ... FROM ...'))
reveal_type(stmt) # asyncpg.prepared_stmt.PreparedStatement[MyRecord]
stmt = await conn.prepare('SELECT ... FROM ...', return_type=MyRecord)
reveal_type(stmt) # asyncpg.prepared_stmt.PreparedStatement[MyRecord]There's also a possible third option if the Protocol-like class actually works: require users to use isinstance() to narrow the type: stmt = await conn.prepare('SELECT ... FROM ...')
reveal_type(stmt) # asyncpg.prepared_stmt.PreparedStatement[asyncpg.protocol.protocol.Record]
record = await stmt.fetchrow(...)
reveal_type(record) # Optional[asyncpg.protocol.protocol.Record]
assert isinstance(record, MyRecord)
reveal_type(record) # MyRecordThe third option completely depends on whether the Protocol-like class is feasible, and would also do runtime checking (which would check if Record.keys() has all of the keys of the subclass). I would imagine the runtime checking would be a performance hit. |
Sorry, something went wrong.
|
Awesome work on the plugin, @bryanforbes! Thanks!
I'm still a bit uneasy with adding unused parameters just for the typing purpose. That said, if we had record_class= actually make fetch() and friends return instances of that class, that would be a great solution. There's actually #40, which quite a few people requested before. |
Sorry, something went wrong.
|
@elprans Thanks! There are still some places in the code where # type: ignore is being used. I've updated them to use the specific code(s) that can be used to ignore them (so if new type errors arise, mypy doesn't ignore those as well). Some of them can be ignored. For example, anywhere TypedDict is being used to ensure a dictionary has the right shape, mypy will complain when using ** on it. Others indicate a possible issue with the code that I wasn't sure how to fix. I'll list those here (using the latest commit I've just pushed):
With regards to the unused parameter, I completely understand your concern. I also think that making fetch() and friends return the instances of that class would be a good solution. I can look into that, but my experience with cpython is fairly limited (I've used cython a long long time ago, so it's much more familiar). Let me know how you'd like to proceed. |
Sorry, something went wrong.
Add the new `record_class` parameter to the `create_pool()` and `connect()` functions, as well as to the `cursor()`, `prepare()`, `fetch()` and `fetchrow()` connection methods. This not only allows adding custom functionality to the returned objects, but also assists with typing (see #577 for discussion). Fixes: #40.
Add the new `record_class` parameter to the `create_pool()` and `connect()` functions, as well as to the `cursor()`, `prepare()`, `fetch()` and `fetchrow()` connection methods. This not only allows adding custom functionality to the returned objects, but also assists with typing (see #577 for discussion). Fixes: #40.
Add the new `record_class` parameter to the `create_pool()` and `connect()` functions, as well as to the `cursor()`, `prepare()`, `fetch()` and `fetchrow()` connection methods. This not only allows adding custom functionality to the returned objects, but also assists with typing (see #577 for discussion). Fixes: #40.
There was a problem hiding this comment.
With regards to the unused parameter, I completely understand your concern. I also think that making fetch() and friends return the instances of that class would be a good solution.
I took this upon myself to implement in #559.
I did a cursory review of the PR and left a few comments. Most importantly, I think we should bite the bullet and use the PEP 526 syntax for type annotations instead of comments. Python 3.5 is rapidly going out of fashion, and I'd love to drop a bunch of hacks we already have to support it.
Thanks again for working on this!
Sorry, something went wrong.
| low, high = port_range | ||
|
|
||
| port = low | ||
| port = low # type: typing.Optional[int] |
There was a problem hiding this comment.
Python 3.5 would be EOL'd in September and I think we should just drop support for it and use proper annotation syntax everywhere.
Sorry, something went wrong.
There was a problem hiding this comment.
I've updated the PR to switch to 3.6 type annotations and removed 3.5 from CI
Sorry, something went wrong.
| self._aborted = True | ||
| self._protocol.abort() | ||
| self._protocol = None | ||
| self._protocol = None # type: ignore[assignment] |
There was a problem hiding this comment.
Perhaps a cleaner solution would be to assign a sentinel instance of a Protocol-like object, e.g. DeadProtocol that raises an error on any attribute access.
Sorry, something went wrong.
Add the new `record_class` parameter to the `create_pool()` and `connect()` functions, as well as to the `cursor()`, `prepare()`, `fetch()` and `fetchrow()` connection methods. This not only allows adding custom functionality to the returned objects, but also assists with typing (see #577 for discussion). Fixes: #40.
Add the new `record_class` parameter to the `create_pool()` and `connect()` functions, as well as to the `cursor()`, `prepare()`, `fetch()` and `fetchrow()` connection methods. This not only allows adding custom functionality to the returned objects, but also assists with typing (see #577 for discussion). Fixes: #40.
Add the new `record_class` parameter to the `create_pool()` and `connect()` functions, as well as to the `cursor()`, `prepare()`, `fetch()` and `fetchrow()` connection methods. This not only allows adding custom functionality to the returned objects, but also assists with typing (see #577 for discussion). Fixes: #40.
Add the new `record_class` parameter to the `create_pool()` and `connect()` functions, as well as to the `cursor()`, `prepare()`, `fetch()` and `fetchrow()` connection methods. This not only allows adding custom functionality to the returned objects, but also assists with typing (see #577 for discussion). Fixes: #40.
|
@bryanforbes PR #599 was merged! I don't know if it was a blocker for this PR or not (by reading the discussion here I think it was). Thanks for this PR, really, looking forward to it! 😄 |
Sorry, something went wrong.
|
@victoraugustolls I'll rebase and update this PR today |
Sorry, something went wrong.
|
@victoraugustolls I finished the rebase, but there's an issue with Python 3.6 and ConnectionMeta + Generic related to python/typing#449. I'll poke around with it this weekend and try to get it working. |
Sorry, something went wrong.
|
Thanks for the update @bryanforbes ! I will try and search for something that can help |
Sorry, something went wrong.
|
@victoraugustolls I was able to come up with a solution for the metaclass issue in Python 3.6 (that won't affect performance on 3.7+), but I'd like to work on some tests before taking this PR out of draft. Feel free to review the code, though. |
Sorry, something went wrong.
|
@victoraugustolls I started writing unit tests last night, however they use py.test and pytest-mypy-plugins to allow testing the output for both errors and revealed types. However, although py.test is a dev extra, I noticed you aren't using it to run the test suite. Will it be a problem to use py.test to only run the mypy unit tests? |
Sorry, something went wrong.
|
@bryanforbes I don't think we need more than running mypy like we do flake8 in test__sourcecode.py for the main regression testsuite. Here's an example |
Sorry, something went wrong.
|
Hi @bryanforbes ! I'm not a maintainer, but thanks for asking! I agree with @elprans and running mypy should be enough. If mypy is happy it should be fine! |
Sorry, something went wrong.
|
Is there anything blocking this pr? I would be willing to help out if needed |
Sorry, something went wrong.
|
Yeah I think it should be merged. Maybe it is still not perfect, but it likely will work for majority of people and any bugs will come out when people start using. It's been 3 years this PR has been open and rebasing it constantly just adds unnecessary overhead. |
Sorry, something went wrong.
|
Is there someone we have to ping to get this merged? I would be willing to help out wherever is needed |
Sorry, something went wrong.
I saw @elprans initially commenting here with suggestions, and @1st1 in another issue commenting that type annotations would be welcomed. Hopefully that could bring attention to this PR. |
Sorry, something went wrong.
|
Hi! When you plan to merge this proposal to main? |
Sorry, something went wrong.
|
Hi @bryanforbes, could you please resolve conflicts. I have no permissions to do that @elprans I see that this work could be continued even after this PR has been merged. |
Sorry, something went wrong.
|
I'll try to get some time to resolve conflicts this week |
Sorry, something went wrong.
Add typings to the project and check the project using mypy. `PostgresMessage` and `PoolConnectionProxy` were broken out into their own files to make it easier to add typing via stub (pyi) files. Since they are metaclasses which generate dynamic objects, we can't type them directly in their python module.
|
@elprans I've updated the typings to match what I have in asyncpg-stubs. Please give this another review when you have time. |
Sorry, something went wrong.
|
@elprans would it help you review this to break this into individual PRs? There's a lot here. |
Sorry, something went wrong.
Yes, absolutely. |
Sorry, something went wrong.
Ok. I'll take care of that this week. |
Sorry, something went wrong.
Been there: https://meta.stackexchange.com/questions/19478/the-many-memes-of-meta/19514#19514 😅 Are there any updates to this? |
Sorry, something went wrong.
|
@bryanforbes is there still work on this? I found asyncpg-stubs package and tried to use it. It works, although it has some (not bugs, but inconsistencies with asyncpg). For example it makes Connection and Pool types as generics (so one can specify a custom record) this is probably how it should be, but then mypy demands me to specify the type, and when I do then code refuses to run, because asyncpg can't handle square brackets next to types. I really wish asyncpg would natively support types so we wouldn't have this type of inconsistencies. |
Sorry, something went wrong.
You can workaround this with something like: from typing import TYPE_CHECKING
from asyncpg import Pool, Record as BaseRecord
if TYPE_CHECKING:
Record = BaseRecord[Record] # you can also specify a custom `Record` class
else:
Record = BaseRecord
# etc.This is a great workaround when some stub library changes some interface which is incompatible with runtime environment. |
Sorry, something went wrong.
oh wow, this is a great trick. I'll install it back and try it, though I would still love if the types could be added to the base package, as this would eliminate need for things like that. |
Sorry, something went wrong.
|
sigh I just noticed that were some initial typing added to 0.30.0 which made me excited, but that turned out to be only adding types to some less popular calls (I'm mostly interested in Connect and Pool), doesn't have py.typed file so mypy doesn't use the types and async-stubs doesn't work with that version so I had to downgrade :( Is there a way to use 0.30.0 with asyncpg-stubs or I have to chose one over the other? @elprans @bryanforbes If there are types in asyncpg-stubs already that seem to work, why we are breaking changes into parts and have several months for release instead just putting them all there. The types should not affect how the code executes. |
Sorry, something went wrong.
|
You can also fix errors with generics only in stubs but not in runtime with from __future__ import annotations |
Sorry, something went wrong.
| Back | FazBrowse Home | New Git URL |
This is a work in progress and still produces errors when type checking the codebase. I added type hints to the Python files where it was feasible and updated the code of the methods to be type safe. In two places (pool.py and exceptions/_base.py), adding type hints to the code itself would not work because of the dynamic nature of the code in those modules. There may also be places where I'm making wrong assumptions about the code (especially in the cython code), so a thorough review would be very welcome. Lastly, I did not add typings for asyncpg._testbase since that seems to be an internal testing module.
Some of the remaining problems that mypy finds may be bugs in the code or they may be that mypy is being overly strict:
References #569, #387