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/