connor-makowski / type_enforced

A pure python type enforcer for annotations. Enforce types in python functions and methods.
MIT License
45 stars 2 forks source link

WithSubclasses breaks with late binding #43

Closed perkinslr closed 1 day ago

perkinslr commented 2 weeks ago

Create a function like

class bar: ...

@type_enforced.Enforcer()
def foo(b: WithSubclasses(bar)): ..

Then in a later imported file, create a subclass of the target type

class baz(bar): ...

If you try to call foo(baz()) you get an error. This is because WithSubclasses immediately looks through the list of subclasses, rather than doing so when the function in question is called.

Additionally, abstract base classes are unsupported because it uses this approach. A simpler approach would be to make WithSubclasses be a higher priority check that uses isinstance to defer the actual check to at runtime. Unfortunately I don't see a trivial way to fix this.

connor-makowski commented 3 days ago

Thanks for creating an issue @perkinslr.

I do not see an immediate clean fix for this issue as well. The reason we do immediate checking for subclasses at initialization instead of at function call time is for performance reasons.

You could always push the validation to run time using a custom constraint. I might be able to generalize this with a few caveats.

In your Foo class definition, you could do:

import type_enforced
from type_enforced.utils import WithSubclasses, GenericConstraint

class Bar:
    pass

# Using a generic constraint runs the constraint at call time
BarWithSubclass = GenericConstraint({
    'bar_with_subclass':lambda x: type(x) in WithSubclasses(Bar)
})

@type_enforced.Enforcer
def foo(x: BarWithSubclass) -> None:
    pass

The challenge with this is that Constraints are not validated in the same way as types. First types are validated and then constraints are validated. If you want to pass other type checks with this constraint for foo(x) it may have other implications.

connor-makowski commented 3 days ago

You could also define your foo function after importing baz which would then work as is. I understand that this is not necessarily ideal so the above custom constraint would be a more generic solution.

connor-makowski commented 1 day ago

Delayed the callable until inital check in 1.7.0

Closing with: https://github.com/connor-makowski/type_enforced/commit/34e1dd297eb7a5b1e781d03c47e8c9908c1e0f3a