
Yeah, all the shenanigans with `__all__` make it clear that it's the wrong solution, and we should do something better. Fortunately the PEG parser and its "soft keywords" feature (debuting for match/case in 3.10) makes it much easier to do this. I had thought about this and came up with similar syntax as you did (`export def` etc.) but instead of writing ``` y = 2 export y ``` That's okay, but maybe we can do better, like this? ``` export y = 2 ``` This could also be combined with a type annotation, e.g. ``` export y: int = 2 ``` I'm not sure about the import+export syntax you gave, maybe something like this instead? ``` import export foo import foo, export bar, baz as export babaz ``` Hm, maybe your version is okay too -- just bikeshedding here. :-) You write about auto-populating `__all__`. I am not aware of it ever auto-populating. What are you referring to here? (The behavior that in the absence of `__all__`, everything not starting with `_` is exported, is not auto-population -- it's a default behavior implemented by `import *`, not by the exporting module.) I'm not sure that I would let `export` use the existing `__all__` machinery anyway. Maybe in a module that uses `export` there should be a different rule that disallows importing anything from it that isn't explicitly exported, regardless of what form of import is used (`__all__` *only* affects `import *`). Maybe these ideas should be considered together with lazy import (another thread here). On Fri, Mar 12, 2021 at 3:08 PM Theia Vogel <theia@vgel.me> wrote:
Hi,
I was refactoring some code today and ran into an issue that always bugs me with Python modules. It bugged me enough this time that I spent an hour banging out this potential proposal to add a new contextual keyword. Let me know what you think!
Theia
--------------------------------------------------------------------------------
A typical pattern for a python module is to have an __init__.py that looks something like:
from .foo import ( A, B, C, )
from .bar import ( D, E, )
def baz(): pass
__all__ = [ "A", "B", "C", "D", "E", "baz", ]
This is annoying for a few reasons:
1. It requires name duplication a. It's easy for the top-level imports to get out of sync with __all__, meaning that __all__, instead of being useful for documentation, is actively misleading b. This encourages people to do `from .bar import *`, which screws up many linting tools like flake8, since they can't introspect the names, and also potentially allows definitions that have been deleted to accidentally persist in __all__. 2. Many symbol-renaming tools won't pick up on the names in __all__, as they're strings.
Prior art:
================================================================================
# Rust
Rust distinguishes between "use", which is a private import, "pub use", which is a globally public import, and "pub(crate) use", which is a library-internal import ("crate" is Rust's word for library)
# Javascript
In Javascript modules, there's an "export" keyword:
export function foo() { ... }
And there's a pattern called the "barrel export" that looks similar to a Python import, but additionally exports the imported names:
export * from "./foo"; // re-exports all of foo's definitions
Additionally, a module can be gathered and exported by name, but not in one line:
import * as foo from "./foo"; export { foo };
# Python decorators
People have written utility Python decorators that allow exporting a single function, such as this SO answer: https://stackoverflow.com/a/35710527/1159735
import sys
def export(fn): mod = sys.modules[fn.__module__] if hasattr(mod, '__all__'): mod.__all__.append(fn.__name__) else: mod.__all__ = [fn.__name__] return fn
, which allows you to write:
@export def foo(): pass
# __all__ == ["foo"]
, but this doesn't allow re-exporting imported values.
# Python implicit behavior
Python already has a rule that, if __all__ isn't declared, all non-underscore-prefixed names are automatically exported. This is /ok/, but it's not very explicit (Zen) -- it's easy to accidentally "import sys" instead of "import sys as _sys" -- it makes doing the wrong thing the default state.
Proposal:
================================================================================
Add a contextual keyword "export" that has meaning in three places:
1. Preceding an "import" statement, which directs all names imported by that statement to be added to __all__:
import sys export import .foo export import ( A, B, C, D ) from .bar
# __all__ == ["foo", "A", "B", "C", "D"]
2. Preceding a "def", "async def", or "class" keyword, directing that function or class's name to be added to __all__:
def private(): pass export def foo(): pass export async def async_foo(): pass export class Foo: pass
# __all__ == ["foo", "async_foo", "Foo"]
3. Preceding a bare name at top-level, directing that name to be added to __all__:
x = 1 y = 2 export y
# __all__ == ["y"]
# Big Caveat
For this scheme to work, __all__ needs to not be auto-populated with names. While the behavior is possibly suprising, I think the best way to handle this is to have __all__ not auto-populate if an "export" keyword appears in the file. While this is somewhat-implicit behavior, it seems reasonable to me to expect that if a user uses "export", they are opting in to the new way of managing __all__. Likewise, I think manually assigning __all__ when using "export" should raise an error, as it would overwrite all previous exports and be very confusing. _______________________________________________ Python-ideas mailing list -- python-ideas@python.org To unsubscribe send an email to python-ideas-leave@python.org https://mail.python.org/mailman3/lists/python-ideas.python.org/ Message archived at https://mail.python.org/archives/list/python-ideas@python.org/message/HL3P7C... Code of Conduct: http://python.org/psf/codeofconduct/
-- --Guido van Rossum (python.org/~guido) *Pronouns: he/him **(why is my pronoun here?)* <http://feministing.com/2015/02/03/how-using-they-as-a-singular-pronoun-can-c...>