Currently, constructors (__init__) take *Like types (eg, StringLike = Union[str, StringExpr]), which was needed to allow literal value shorthand in constructor calls. However, the @init_in_parent provides a translation later that actually narrows the types, __init__ still accepts the *Like types on a call, but internally (seen by __init__) is all *Expr types.
Prior to #96, all constructor parameters should be re-mapped via self.param = self.Parameter(...) and given an explicit type. #96 changes the constructors to infer parameter types based on typing annotations, so for example if we had param as part of the constructor argument list, we could directly use self.param = param. The downside is that param is of type *Like (from the constructor) instead of the more narrow and accurate *Expr (returned by self.Parameter).
This causes a mild issue with generators, since they should only take *Expr types. The current workaround is to expand generators to also take the castable-types in addition to the *Expr types - which isn't completely accurate, but continues to provides convenient lightweight syntax in exchange for a bit of leakiness in the type checking of an advanced feature.
Properly typing init_in_parent
Ideally, one solution would be to define the init_in_parent decorator type, such that the __init__ can be properly defined in terms of *Expr type, and the decorator would provide a wrapped function that takes the *Like type. The problem is that functions (Callables) have contravariant types for the argument list, so it can't capture the ConstraintExpr[...] type parameters (for example, it doesn't accept BoolExpr in place of the ConstraintExpr[...] pattern).
Additionally, having an *Expr in the type signature of __init__, even if the decorator is properly typed, may be confusing, since users might see the __init__ type signature and wonder why a literal type is allowed. However, the @init_in_parent decorator could signpost that, and since the behavior is consistent it's only one additional learning curve item.
Currently, constructors (
__init__) take*Liketypes (eg,StringLike = Union[str, StringExpr]), which was needed to allow literal value shorthand in constructor calls. However, the@init_in_parentprovides a translation later that actually narrows the types,__init__still accepts the*Liketypes on a call, but internally (seen by__init__) is all*Exprtypes.Prior to #96, all constructor parameters should be re-mapped via
self.param = self.Parameter(...)and given an explicit type. #96 changes the constructors to infer parameter types based on typing annotations, so for example if we hadparamas part of the constructor argument list, we could directly useself.param = param. The downside is thatparamis of type*Like(from the constructor) instead of the more narrow and accurate*Expr(returned byself.Parameter).This causes a mild issue with generators, since they should only take
*Exprtypes. The current workaround is to expand generators to also take the castable-types in addition to the*Exprtypes - which isn't completely accurate, but continues to provides convenient lightweight syntax in exchange for a bit of leakiness in the type checking of an advanced feature.Properly typing
init_in_parentIdeally, one solution would be to define the
init_in_parentdecorator type, such that the__init__can be properly defined in terms of*Exprtype, and the decorator would provide a wrapped function that takes the*Liketype. The problem is that functions (Callables) have contravariant types for the argument list, so it can't capture the ConstraintExpr[...] type parameters (for example, it doesn't accept BoolExpr in place of the ConstraintExpr[...] pattern).Additionally, having an *Expr in the type signature of
__init__, even if the decorator is properly typed, may be confusing, since users might see the__init__type signature and wonder why a literal type is allowed. However, the@init_in_parentdecorator could signpost that, and since the behavior is consistent it's only one additional learning curve item.