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 -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3/lists/python-ideas.python.org/
Message archived at
https://mail.python.org/archives/list/[email protected]/message/HL3P7CXZX3U5SMNIJODL45BE6E72MWTI/
Code of Conduct: http://python.org/psf/codeofconduct/