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 <th...@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-le...@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/HL3P7CXZX3U5SMNIJODL45BE6E72MWTI/
> 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-change-the-world/>
_______________________________________________
Python-ideas mailing list -- python-ideas@python.org
To unsubscribe send an email to python-ideas-le...@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/LQL4VRXWWDYRYCOHQYWV4GMZO542HV6E/
Code of Conduct: http://python.org/psf/codeofconduct/

Reply via email to