-
-
Notifications
You must be signed in to change notification settings - Fork 335
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
What's the best way to re-export socket/ssl constants? #699
Comments
CC @Zac-HD |
I started wondering how Mypy copes with the stdlib The # Do this for every known CONSTANT, so we minimise reliance on user reports.
# We can *write* this with a trivial script (and keep the script),
# but the point is to have it on disk for static analysis tools.
with contextlib.suppress(ImportError):
from socket import AF_UNIX
with contextlib.suppress(ImportError):
from socket import AF_INET
with contextlib.suppress(ImportError):
from socket import AF_INET6
... # (all the others)
for n in dir(socket):
if n == n.upper() and n not in dir():
# Catches any leftovers, fails in our tests with -Werror, asks for reports
warnings.warn(f'Please report that socket.{n} is not reexported by Trio')
globals[n] = getattr(socket, n) This is obviously rather verbose but it has the exact current behaviour at runtime, minimises risk of developer error, and should be handled well by all static analysis tools. |
My original idea of deleting the attributes and names we do not want seems now rather awkward to me.
for every attribute I do not want to expose. This way a user can just do
adapt all his method calls into coroutines and the trio socket module will be a drop in replacement for the standard library socket. Should in the future we have to implement additional methods (for whatever reason), then this will not change anything user facing. |
@Zac-HD I'd like to add the our current solution is a working one, it was just vscode that did not pick up the upper case symbols, so this might be a vscode problem. In the interactive shell configured with jedi the upper case names seemed to have worked. |
I think in this case jedi or the shell may be cheating and using dynamic runtime data. @Zac-HD As a variation on that idea, what do you think of something like: # trio/_generated/socket_constants.py
from socket import (
AF_INET,
AF_INET6,
...
)
# trio/socket.py
# Fake re-exports for static analysis
if TYPE_CHECKING:
from ._generated.socket_constants import *
# Real re-exports for runtime usage
globals().update({name: value for (name, value) in socket.__dict__.items() if name.isupper()})
# test_socket.py
def test_socket_constant_generator():
real_constants = {name for name in socket.__dict__ if name.isupper()}
our_list = set(open(".../socket-constants.txt").split())
assert our_list.issuperset(real_constants) |
Wouldn't it be kind of annoying for users that |
Yep, that looks good to me. We might need to define
As a general principle of API design I don't like this at all, but in the specific case of sockets consistency is more important. Users expect to use |
I surely will not oppose any of your suggestions, I just find these complicated structures a little bit of an overkill for a straight forward approach. NotImplemented could go into the doc string and would show up in code completion. Also the docs would give this information. |
I just don't understand how implementing |
My vote is for generated, brute force re-export plus a test to catch missing items. I assume we're running tests on each of the relevant platforms. |
I just come from the perspective of someone porting his socket code to trio. Let's assume he somewhere uses |
I see generating seems to get the upper hand, but before the decision is made I want to throw in my 50 cents for the star import again. The huge advantage of this one is it already takes care of any OS/Version dependencies, and static analysis tools pick it up. Also you don't have to deal with the zillions of uppercase names, except for the trio implemented ones. The downside is it imports too much, and exposes everything including blocking functions. This is the only thing we must take care of but this is not a lot of effort. Deletion is one way. Which brings up the second alternative I think we should think of (still my favorite) Implementing (overwriting) all of the attributes. Of course The test is easy to write, just compare the trio.socket attributes with the socket attributes and fail if socket contains something for lowercase attributes that trio.socket is missing. We have therefore 100% compatibility and any code using socket can easily be adapted by alias importing:
Why is this compatibility good? For example This latter approach of implementing all the attributes basically would be somehow like a module inheritance. |
@jmfrank63 Let's split apart the discussion about how we export the socket module API from what it should be. These aren't really related, and generally in my experience, any time you can split apart a discussion into two pieces, you should :-). Regarding implementing the complete socket API: I see your point about porting existing @Zac-HD I just had a random idea. I actually have no idea whether this would work, but maybe you do :-) What would happen if we made an empty I guess a downside would be that not every static analysis tool knows about We'd still have to resync from upstream occasionally, and there wouldn't be any test to prod us to do so. An advantage, particularly for |
Also unsure it would work, but purely for aesthetic reasons I'd prefer to write analysis-friendly code than pair opaque code with extra information. It just seems more robust, as well as nicer for new tools, but I'm well past my actual knowledge now and into taste and speculation. |
You know, maybe the code generation and .pyi stuff and all is making it more complicated than it needs to be. Here's an even simpler version of the explicit import idea:
# TYPE_CHECKING is False at runtime, True during static analysis
if TYPE_CHECKING: # pragma: no cover
# Actual code we maintain as needed (that just happens to never execute).
# If we need to add Python version guards to some imports or something, we can do that
from socket import (
AF_INET,
AF_INET6,
...
)
# Real re-exports for runtime usage
globals().update({name: value for (name, value) in socket.__dict__.items() if name.isupper()}) ...and then in |
It's an improvement on the status quo! Let's just do that, and see how it works 😄 |
In theory I guess we should do So it sounds like for each of the two major approaches, we're now pretty clear on the best way to do it. Explicit, static-only importsWe maintain an explicit list of all the constants that can be exported under any configuration, with a guard so that they don't execute at runtime. At runtime, we continue to do dynamic import of uppercase names. To keep the static list of uppercase symbols up to date, we need a test that checks that static analysis tools like jedi/pylint agree with the runtime symbol list. If the static list gets out of date, then the failure mode is that static analysis tools might miss some symbols, but runtime behavior will continue to be correct. Tools like jedi/pylint should give accurate analyses, except that they might suggest/accept symbols that don't exist on the current platform, but do exist on some other platform (e.g., jedi on Windows will suggest Unix symbols). This seems fine. For the Import * plus static cleanupsWe maintain an explicit list of all the lowercase names that we don't want to import, e.g. as lines like The same code executes at runtime and in static analysis. To keep the static list of lowercase symbols up to date, we need a test that compares our runtime-exported names to a... separately-maintained list of lowercase names we don't want to import, I guess? If the static list gets out of date, then the failure mode is that at runtime we start exporting symbols we didn't mean to. Tools like jedi/pylint should give accurate analyses, to the same extent that they understand the If new socket module names appear, our test will notice, so we'll have a chance to think about whether to wrap them. VerdictBoth of these sound doable, but the static import list seems to be preferred by several folks here, and the failure mode for I think the todo items then are:
|
Any suggestions on the priority as opposed to the other issues of #542? |
Up to you, I think :-) |
Fixed in #730. |
The
socket
andssl
module both define a bunch of UPPERCASE constants, likeAF_INET
andCERT_REQUIRED
. Thetrio.socket
andtrio.ssl
modules want to re-export them. How should we do this?This is challenging because the exact set of constants varies depending on platform (e.g. not every platform has
socket.AF_UNIX
) and Python version. It will probably keep changing in the future. We want our re-exports to be accurate despite this. We don't want to maintain a list of which constants are exported on which platform, and we don't want our re-exports to get stale and out-of-date when the stdlib changes.This motivates our current approach, which is to dynamically query the list of UPPERCASE constants every time trio is imported, and then re-export whatever we find:
trio/trio/socket.py
Lines 50 to 55 in c6dcfdd
This is simple, fast, accurate, it's great! Except... as we discovered in #542, there's another problem: while Python is allegedly a dynamic language, it turns out that static analysis is actually pretty handy and people get grumpy when their static analysis tools don't work. And static analysis tools have absolutely no idea what to make of the code above. So that means pylint and mypy raise errors on code that uses
trio.socket.AF_INET
, jedi and pycharm can't tab-completetrio.ssl.CERT_REQUIRED
, and so on. How can we fix this?Crude way: redundant re-exports
We could keep the current export loop for comprehensiveness, and also manually, redundantly re-export the most-used constants, just so static analysis tools can "see" that these are re-exported:
I guess the criteria for maintaining this list would "add stuff whenever someone complains"?
Possible refinement: wrap the manual re-exports in an
if False:
, so that we don't have to guard against ones that are missing?Clever way:
import *
+ ???@jmfrank63 suggested another approach: we could
from socket import *
, and then manually delete all the lowercase exports that we don't want to re-export:The advantage of this is that if we do it right, then static analysis tools may be able to get a complete and accurate picture of our namespace. The downside is that we have to maintain a list of every non-uppercase symbol in
socket
, and make sure to keep it up-to-date across versions.Questions:
Can actual existing static analysis tools understand
from socket import *
? This isn't a given... the stdlib socket module itself uses dynamic hacks to construct its export list:https://github.com/python/cpython/blob/4b430e5f6954ef4b248e95bfb4087635dcdefc6d/Lib/socket.py#L65
We should check this before committing to anything.
How ugly is would
trio/socket.py
actually end up looking? I guess I can mostly visualize it, but it'd still be good to see what it actually looks like. (E.g. do we end up needing to wrap eachdel
in atry
/except
?)If we accidentally leave anything off of our big list of
del
s, then the failure mode is that Trio starts accidentally exporting some function, potentially a blocking one, and potentially we'll have trouble cleaning it up again later if people start using. So, we want to be very confident that our list is accurate, and stays accurate as the stdlib evolves. How can we do this? Some sort of test? What exactly would this look like?Any other ideas?
We could... make static analysis tools smarter somehow? Sounds hard though, there are too many of them. Any other ideas?
The text was updated successfully, but these errors were encountered: