feat: support dtype(ir.Value) and ir.Value[<dtype>]
This allows for pleasant UX such as
- ibis.dtype(ibis.ir.StringValue)
- ibis.dtype(ibis.ir.IntegerValue["!uint32"])
class Users(ibis.Table):
name: ibis.ir.StringColumn
age: ibis.ir.IntegerColumn["uint8"]
def my_api(users: Users):
# Runtime coercion of types works as expected!
users = ibis.cast(users, Users)
# IDE type hints work too!
reveal_type(users.age) # Its an IntegerColumn!
I also adjusted the ibis.dtype() function so that the nullable kwarg defaults to None, which means "Don't mess with the nullability of something that is already a dtype". I can put this into a separate PR if desired, but I think this is a good change to make anyways.
The current behavior of dtype(nonnullable_dtype, nullable=True) returning the original input, still nonnullable, is a footgun.
This is effectively adding a Protocol to ibis, where anything that has a .__dtype__ attribute, ibis will prefer to get the datatype from that attribute. I think this is in general a good idea. I am thinking we could do this for other ibis APIs as well, eg ibis.schema() will first look for a .__schema__ attribute and prefer that if it exists. Not sure if there are other places where this would make sense.
It might be a good idea to make these attributes to be better namespaced, eg __ibis_dtype__ and __ibis_schema__.
@deepyaman Do you have any thoughts here?