-
Notifications
You must be signed in to change notification settings - Fork 127
Draft: new response handling API #464
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
base: main
Are you sure you want to change the base?
Conversation
Modules/LDAPObject.c
Outdated
.name = "_RawLDAPMessage", | ||
.doc = "LDAP Message returned from native code", | ||
.fields = message_fields, | ||
.n_in_sequence = sizeof(message_fields)/sizeof(*message_fields) - 1, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Set this to 2 to preserve backwards compatibility in result
. (And ideally, at the same time, deprecate using result()
as a tuple, along with resultN
, in favor of the named fields).
This would allow old code to continue using type, data = result()
.
Setting this to a value that will grow as more fields are added defeats the purpose of PyStructSequence
– you might as well use a regular object with getset descriptors.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This doesn't feel remotely compatible with LDAPObject.result()
anyway, that's one of the reasons the new API is split into a Connection object (which gets a better request API too in a future PR). Not sure whether to make the result* APIs a (heavyweight) wrapper for this or keep it for now.
Keeping n_in_sequence
automatic is an oversight from initial development, thanks for pointing that out, I'll fix it in the next version. What is your take on the Response subclassing logic? I will probably have to replicate it for ExtendedResponse and intermediates, can you see any conceptual issues with how it is written/is it a total hack?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sadly I won't have time to review the whole PR :(
Could you point to the part I should look at?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Turns out I forgot to add the Python files into the commit so my last comment probably didn't make much sense. I meant the Python bits, this in particular.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
At first glance, that looks more complicated than it needs to be. i don't see much advantage over setting the missing attributes to None (or keeping them missing). Is there an advantage?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The subclassing allows one to expose response-specific methods (Compare result, allow parsing the extended result) and if the user is on 3.10, allow pattern matching, on others, make the code more readable, especially if controls were subjected to the same treatment.
abb123e
to
756fba2
Compare
a35462a
to
90652b1
Compare
6b8f56e
to
12c3b61
Compare
@mistotebe I'm thinking about python-ldap 4.0 release. |
Any feedback is welcome, yes. I've been using this branch elsewhere with some success and the tools added here certainly make porting some of my tools (e.g. syncmonitor) from python-ldap0 an to some extent pldap easier. Don't think I'll be abandoning pldap any time soon (there is no replacement to its stub server implementation for protocol level testing) but definitely closer to dropping python-ldap0 after we get something like this in. There's still a little bit of a kludge in how e.g. searches are represented, so the API might still evolve, and it's only a stepping stone towards:
But it's definitely useful already so happy to start cleaning up or breaking it up starting with bits you consider less contentious and go from there. |
Maybe my project is suitable for this. I am supporting a nice user-friendly API with sync and async, hiding the |
You should definitely be able to make use of the new interfaces and response classes added here to make your own life easier. In terms of request API (and async), yes yours looks somewhat similar to what I wanted, but there is one important difference: any request (not just search) can return any amount of "intermediate" responses (search entry, referral entry, intermediate) and then a final response or exception so a request object should (always?) be iterable. A caller might be able to say they (don't) expect those, they still have a choice to let the whole request finish or they can receive messages one by one and pattern match on the type (SearchResultEntry, SyncInfoNewCookie, generic IntermediateResponse, ...) etc. There is a partial draft of how the async side could look, what you get is a Future and optionally an iterable for the intermediate responses. We can definitely see how to converge those two APIs because in some ways your take is close to what I had in mind and a lot of things I can see there should simplify to "syntactic sugar"/shorthand over the new API. |
Thanks for the explanation. |
Slower? Are we both talking about ldap_result() with
I mean SearchReference, not Result.result == LDAP_REFERRAL. Opting to handle referrals is an option, sure, but the client should have an option to return them (and sometimes that makes sense?).
Often syncrepl control (mode=refreshAndPersist) is the easiest one, or LDAP TXN (RFC 5805). Both exist in OpenLDAP, syncrepl needs the syncprov overlay configured, TXNs need a compatible backend (e.g. back-mdb). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
While I can't see any obvious leaks, the complex control flow around error handling needs careful testing under various failure scenarios. We might need to have an ASAN run here.
And, of course, all tests need to pass consistently before we merge anything.
defaultClass: Optional[type[ExtendedResponse]] = None, | ||
**kwargs) | ||
|
||
The old API is deprecated and will be removed in 4.0. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you agree with my proposal, this version comment can be changed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed, this was based on a discussion we had before the plan you've just outlined below.
@classmethod | ||
def __convert_old_api(cls, responseName_or_msgid=_NOTSET, | ||
encodedResponseValue_or_msgtype=_NOTSET, | ||
controls=None, *, | ||
result=_NOTSET, matcheddn=_NOTSET, message=_NOTSET, | ||
referrals=_NOTSET, name=None, value=None, | ||
defaultClass: Optional[type['ExtendedResult']] = None, | ||
msgid=_NOTSET, msgtype=_NOTSET, | ||
responseName=_NOTSET, encodedResponseValue=_NOTSET, | ||
**kwargs): | ||
""" | ||
Implements both old and new API: | ||
__init__(self, responseName, encodedResponseValue) | ||
and | ||
__init__/__new__(self, msgid, msgtype, controls=None, *, | ||
result, matcheddn, message, referrals, | ||
defaultClass=None, **kwargs) | ||
""" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It looks overly complex, IMO.
Can we split it into two?
Something like
@classmethod
def from_old_api(cls, responseName, encodedResponseValue)
@classmethod
def from_new_api(cls, msgid, msgtype, ...)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not happy about it but ExtendedResponse exists already with a fairly pointless signature but if we ignored it, we'd have to duplicate all the decoding code everywhere else? I hadn't found another way of doing it back then but if you think this will work with __init__
/__new__
, I'll need a little more because I'm still not seeing it...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe we can create a temporary transitional class? Like ExtendedResultOrSomething(ldap.response.ExtendedResult):
with the new implementation? And in 5.0, we can swap it with the existedExtendedResponse
But I don't have a hard opinion here. If this is temporary code that dies in 5.0/6.0 anyway, maybe the current approach is fine?
Okay, I took some time to review the code. This is impressive work and I like it a lot! As discussed, we probably should split breaking and non-breaking code between releases.
For python-ldap 4.0 (as experimental/tech preview), I believe we could include the following stable components that provide immediate value without breaking existing code. We should clearly mark these as experimental in the documentation:
For python-ldap 5.0 (full stable release):
What do you think about the idea/split? |
Thanks for looking at it. Broadly I agree with the plan except that delaying Connection to 5.0 makes all the 4.0 additions moot because it leaves no way of reaching all the new code/classes. How about this?
Does all the mucking with |
12c3b61
to
9e878b4
Compare
9e878b4
to
a40c7d4
Compare
a40c7d4
to
9de48a9
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was thinking about those components as standalone tools as I thought you want smaller scale of that exposed in 4.0.
But yeah, without Connection there's no easy way to actually use any of the new stuff. Your timeline makes more sense.
So 4.0 with experimental/tech preview Connection, 5.0 stable + deprecate LDAPObject, 6.0 remove old stuff - sounds good to me.
About the __new__
pattern - It's clever but honestly it does feel a bit too magical... The main issues I see:
- Readability - When someone calls
Response(msgid, msgtype, ...)
and gets back aSearchEntry
, that's surprising behavior, so it violates the principle of least surprise. The metaclass magic isn't immediately obvious when debugging. - The class-level registry - That
__subclasses
dict could be problematic if someone ever subclasses Response for their own framework. Plus the assert in__init_subclass__
is a bit aggressive (I left a comment below).
Alternative approaches that might be cleaner:
- Explicit factory functions: Just have
response_from_message(msgid, msgtype, ...)
that does the dispatch. Less magical, easier to debug. - A proper factory class: Something like
ResponseFactory.create(msgid, msgtype, ...)
. You could even make it pluggable for custom response types. - Keep new but make it more explicit: Maybe rename Response to ResponseBase or AbstractResponse so it's clear it's not meant to be instantiated directly?
That said, if the pattern works and you're comfortable with it, I'm not too much against that. The automatic subclass registration via __init_subclass__
is actually pretty neat. Maybe just needs better documentation/comments explaining the magic?
if not hasattr(cls, 'msgtype'): | ||
return | ||
c = __class__.__subclasses.setdefault(cls.msgtype, cls) | ||
assert issubclass(cls, c) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the assert
might be a bit too aggressive.
If the API will be used for some CustomSearchEntry(Response) creation and will create two Response subclasses with the same msgtype, then the default SearchEntry is set and assert fails.
It's still not something that should be done usually, but I think it'll be nicer to have something like that here:
if not issubclass(cls, c):
raise TypeError(f"Cannot register {cls} for msgtype ..."
@classmethod | ||
def __convert_old_api(cls, responseName_or_msgid=_NOTSET, | ||
encodedResponseValue_or_msgtype=_NOTSET, | ||
controls=None, *, | ||
result=_NOTSET, matcheddn=_NOTSET, message=_NOTSET, | ||
referrals=_NOTSET, name=None, value=None, | ||
defaultClass: Optional[type['ExtendedResult']] = None, | ||
msgid=_NOTSET, msgtype=_NOTSET, | ||
responseName=_NOTSET, encodedResponseValue=_NOTSET, | ||
**kwargs): | ||
""" | ||
Implements both old and new API: | ||
__init__(self, responseName, encodedResponseValue) | ||
and | ||
__init__/__new__(self, msgid, msgtype, controls=None, *, | ||
result, matcheddn, message, referrals, | ||
defaultClass=None, **kwargs) | ||
""" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe we can create a temporary transitional class? Like ExtendedResultOrSomething(ldap.response.ExtendedResult):
with the new implementation? And in 5.0, we can swap it with the existedExtendedResponse
But I don't have a hard opinion here. If this is temporary code that dies in 5.0/6.0 anyway, maybe the current approach is fine?
Uh oh!
There was an error while loading. Please reload this page.