Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support zope Interfaces #42

Open
perkinslr opened this issue Oct 29, 2024 · 5 comments
Open

Support zope Interfaces #42

perkinslr opened this issue Oct 29, 2024 · 5 comments

Comments

@perkinslr
Copy link

Zope Interfaces are still often used in software using twisted and related technologies. It would be nice if type_enforced supported these interfaces in its type checking.

The critical checker is provided by zope.interface.verify.verifyObject(iface, candidate, tentative), where tentative indicates if objects that just happen to provide the interface are good enough, or if they must explicitly support it.

Note this can sorta be done via GenericConstraints, but those run after the rest of the type checking, so you have to pick between a constraint for an interface, or a basic type. So you cannot do def foo(bar: [int, IBar]):

@connor-makowski
Copy link
Owner

From my perspective, this poses some challenges.

  1. It is important for us to keep type_enforced pure python and without any deps.
  2. The zope interface is fairly non pythonic. If we add support for this, are there other packages we would want to support?

How do you envision support for Zope would look?

@perkinslr
Copy link
Author

With a soft runtime dependency, you can easily check if an argument provides an interface. Initially I thought to include an optional kw argument to Enforcer to tell it to perform interface checking, as the function mentioned above is the only simple way to handle incidental implementations.

However, I understand the desire to avoid adding even a soft dependency, so I found an introspective solution that better fits with type_enforced's existing code. Objects providing an interface have .__provides__ property, which itself can be flattened into a set of interfaces. This set can then be & compared to the set of acceptable_types. So the overall procedure would be to call

if hasattr(obj, "__provides__") and hasattr(obj.__provides__, "flattened"):
    passed_types = set((passed_type, *obj.__provides__.flattened()))
else:
    passed_types = {passed_type}
if not passed_types & set(acceptable_types):
    ...

This could be gated behind a kw argument to Enforcer, to avoid extraneous hasattr calls.

Anyway, that's the "how", the "do we want to" is another matter. I don't know of any other packages that are along similar lines, certainly none that are as wide spread and long standing as zope. (zope is used by twisted, so is indirectly used in lots of enterprise environments). It is probably worth noting that there is an actively maintained mypy plugin for zope to support lint-time type checking.

As for being non-pythonic, the same was said about deferreds and asynchronous programming before asyncio made them popular. Same with the very ability to specify type annotations.

That said, I know what it's like to have drive-by suggestions to increase project scope and complexity, so if you think this is outside your project's scope, I won't argue against that.

@connor-makowski
Copy link
Owner

I started looking into this I ran into a key issue:

Something unexpected is occurring when I try to see if __provides__ is in a I class.

from zope.interface import Interface, implementer
from pprint import pp as print

class IGreeter(Interface):
    def greet(name: str) -> str:
        """Greets the person with the given name."""

@implementer(IGreeter)
class Greeter:
    def greet(self, name: str) -> str:
        return f"Hello, {name}!"

print({
    'hasattr': hasattr(IGreeter, '__provides__'),
    'in_dir': "__provides__" in dir(IGreeter),
    'isinstance': isinstance(IGreeter, Interface),
    'issubclass': issubclass(IGreeter, Interface),
    'dir': dir(IGreeter),
})

Gives:

{'hasattr': False,
 'in_dir': True,
 'isinstance': False,
 'issubclass': True,
 'dir': [
         '_Element__tagged_values',
         '_InterfaceClass__attrs',
         '_InterfaceClass__compute_attrs',
         '_InterfaceMetaClass__module',
         '_ROOT',
         '_Specification__setBases',
         '__adapt__',
         '__bases__',
         '__call__',
         '__class__',
         '__contains__',
         '__delattr__',
         '__dict__',
         '__dir__',
         '__doc__',
         '__eq__',
         '__format__',
         '__ge__',
         '__getattribute__',
         '__getitem__',
         '__getstate__',
         '__gt__',
         '__hash__',
         '__ibmodule__',
         '__identifier__',
         '__implemented__',
         '__init__',
         '__init_subclass__',
         '__iro__',
         '__iter__',
         '__le__',
         '__lt__',
         '__module__',
         '__name__',
         '__ne__',
         '__new__',
         '__or__',
         '__providedBy__',
         '__provides__',
         '__reduce__',
         '__reduce_ex__',
         '__repr__',
         '__ror__',
         '__setattr__',
         '__sizeof__',
         '__slots__',
         '__sro__',
         '__str__',
         '__subclasshook__',
         '__weakref__',
         '_bases',
         '_calculate_sro',
         '_call_conform',
         '_dependents',
         '_do_calculate_ro',
         '_implied',
         '_v_attrs',
         'changed',
         'dependents',
         'direct',
         'extends',
         'get',
         'getBases',
         'getDescriptionFor',
         'getDirectTaggedValue',
         'getDirectTaggedValueTags',
         'getDoc',
         'getName',
         'getTaggedValue',
         'getTaggedValueTags',
         'implementedBy',
         'interfaces',
         'isEqualOrExtendedBy',
         'isOrExtends',
         'names',
         'namesAndDescriptions',
         'providedBy',
         'queryDescriptionFor',
         'queryDirectTaggedValue',
         'queryTaggedValue',
         'setTaggedValue',
         'subscribe',
         'unsubscribe',
         'validateInvariants',
         'weakref'
    ]
}

It seems like the sope interface overrides the dir method but is not actually implementing an attr.

Can you look into that and see if you cant figure out a way to list the IGreeter acceptable classes?

@perkinslr
Copy link
Author

I take it you are trying to expand the list of acceptable_types similar to the way WithSubclasses works? That will have the same limitations as WithSubclasses, but should work. The property IGreeter.dependents is a weakref.WeakKeyDictionary where the keys are sub-interfaces and classes implementing the interface. Extracting the list of classes will require walking the keys and extracting the types. Something like

def extract_acceptable_types(iface: InterfaceClass) -> list[type]:
    types = []
    for entry in iface.dependents:
        if hasattr(entry, "inherit"): # It's a classImplements linker
            types.append(entry.inherit)
        elif hasattr(entry, "_cls"): # It's a directlyProvides linker
            types.append(entry._cls)
        else: # It's an interface inheriting from iface
            types.extend(extract_acceptable_types(entry))
    return types

@connor-makowski
Copy link
Owner

This is starting to seem like it will not be particularly general.

I am now thinking this might be good motivation to allow extending the base type enfoced class for more complex cases like this. Something that is not a constraint. Let me mull this over a bit.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants