Use a with block to control export (public/private/__all__)

Presently, there is a lot of debate about how to do export control. An alternative suggestion is an export context manager that remains compatible with __all__:

with export():  # Everything declared inside export blocks is exported.
    import math
    import sys as sys_alias
    CONST = 42

def not_exported(): pass  # Outside block, not exported

with export():  # Second block.
    def func(): return CONST
    print(func())  # You can put a non-declaration statement inside, but bad style!

print(__all__)  # 'sys_alias', 'CONST', 'math', 'func'

The export context manager works by snapshotting globals on entry and exit and adding the diff to __all__.

If this is something people are interested in, happy to write up and put on PyPi so people ‘can try before they buy’.

1 Like

There is currently a pole running about visibility signalling Poll: Public/internal handling — what behavior do we want? . There are also plenty of discussions PEP 842 postmortem: how do we protect the standard library? - #44 by danielhollas , PEP 842, PEP 843, PEP 844, and using two namespaces.

I’m using a similar context manager for lazy imports in Python 3.11–14 (see pypi for ”katso”) and in my experience it makes both the intent clear and is straight forward to implement.

Seems a bit fragile, and dependent on a CPython implementation detail (unless you have some way to implement export without sys._getframe). Maybe this is something that needs actual language support - a namespace block. The nearest we currently have is a class, but that comes with a lot of cruft; you can’t just grab every name out of the class and export it. But it comes close:

class exports:
    import math
    import sys as sys_alias
    CONST = 42
__all__ = [x for x in dir(exports) if not x.startswith("_")]

A true namespace could provide the functionality needed.

Katso looks great - haven’t seen it before. :grinning_face:

Yes, using _getframe. Would need to see what other implementations could do. I only use CPython so not familiar with them. Might be in luck, they might have something similar.

Your other suggestion of inferring what should and shouldn’t be exported based on a leading underscore has been tried, e.g. populate_all in the atpublic project. You end up with a complicated algorithm otherwise functions you import from other modules get exported and functions that are meant to by exported but are spelt with an underscore (e.g. NamedTuple) don’t. Also you end up using _getframe!

Right, I’m saying that a class block is far from perfect but is the closest we get without actual language support. Suppose that, instead, we had something that actually captured all assignment, but wasn’t a function or a class:

namespace export:
    import math
    import sys as sys_alias
    CONST = 42
# Calls export(["math", "sys_alias", "CONST"], globals())

This would execute the block of code in the surrounding namespace as normal, but would capture the list of names assigned to within the block, and then call the given callable (in this case, export) with that list, and the enclosing namespace. I don’t think there’s a straight-forward way to do this without some form of language support.

Yeah a namespace is an option, it is mentioned in the pole running currently. A disadvantage is that it doesn’t ‘play nice’ with __all__, something the with export block does. So for example, outside of the export block __all__ can be manually manipulated. People needing to manually managing __all__ is something that derailed PEP 482 (see postmortem).

maybe you should change a name because many scripts used the name export.
maybe decl_public? or a more pythonic name .
request for comments.

also, this would be extremely hard to analyze for type checkers like mypy.
because it made the all from a static list to a dynamic code block,
and it is not clear at all

the current __all__'s contract is based on that “the __all__ is static”,
this make it 100% dynamic (or it is on the opposite meaning of with block),
this is also on the opposite meaning of __all__

What do you mean?

In general, you should be able to assume that, by the time you’re importing the module, it is stable. That’s not the same thing as static, and notably, there are a LOT of ways to dynamically add things to __all__ during the execution of the module. They’re fine.

That aspect of this proposal isn’t an issue. I think it’s a good idea, but just going to be a tad fragile without language support - and that language support isn’t to fix the __all__ side of things :slight_smile:

sorry for that error.
but on the other way, how can we add it?
in builtins.export? that would destory a large amount of scripts using export (it’s the keyword of UNIX Bash), with a weird error like NameError or SyntaxError if it’s a keyword.
In my opinion, the keyword with hints that the code in this block will be executed runtime, that’s not what the static analyze mean.

btw, the human readers and the static analyzers hopes that __all__ is staticly declared.

Hmm, I’ve never had a problem with it as a human reader. It’s usually pretty clear.

sure. __add__ is pretty.
but

with export():
    from somepkg import somefunc
    from os import system

it’s kinda weird.

What top-level module code isn’t executed at runtime?

Why would someone assume code running inside a context manager somehow changes the timing of its execution?

While a technically clever solution. I find the indenting through the context manager distracting. From habit, I expect functions to not be indented. It’s particularly confusing that exported functions have a different indentation level than not-exported functions.

This means that somefunc can be used inside your module (the one you are writing that contains the with export) and is also exported by the module, so that users of your module do not also have to import somefunc as well.

Other proposals in this space have proposed from somepkg export somefunc for this functionality.

I like this feature because it reminds me they are special - however it is definitely a matter of personal taste.

1 Like