A short answer: use proxy_tools
The proxy_tools
package attempts to provide @module_property
functionality.
It installs with
pip install proxy_tools
Using a slight modification of @Marein's example, in the_module.py
we put
from proxy_tools import module_property
@module_property
def thing():
print(". ", end='') # Prints ". " on each invocation
return 'hello'
Now from another script, I can do
import the_module
print(the_module.thing)
# . hello
Unexpected behavior
This solution is not without caveats. Namely, the_module.thing
is not a string! It is a proxy_tools.Proxy
object whose special methods have been overridden so that it mimicks a string. Here are some basic tests which illustrate the point:
res = the_module.thing
# [No output!!! Evaluation doesn't occur yet.]
print(type(res))
# <class 'proxy_tools.Proxy'>
print(isinstance(res, str))
# False
print(res)
# . hello
print(res + " there")
# . hello there
print(isinstance(res + "", str))
# . True
print(res.split('e'))
# . ['h', 'llo']
Internally, the original function is stored to the_module.thing._Proxy__local
:
print(res._Proxy__local)
# <function thing at 0x7f729c3bf680>
Further thoughts
Honestly, I'm baffled about why modules don't have this functionality built in. I think the crux of the matter is that the_module
is an instance of the types.ModuleType
class. Setting a "module property" amounts to setting a property on an instance of this class, rather than on the types.ModuleType
class itself. For more details, see this answer.
We can actually implement properties on types.ModuleType
as follows, although the results are not great. We can't directly modify built-in types, but we can curse them:
# python -m pip install forbiddenfruit
from forbiddenfruit import curse
from types import ModuleType
# curse has the same signature as setattr.
curse(ModuleType, "thing2", property(lambda module: f'hi from {module.__name__}'))
This gives us a property which exists over all modules. It's a bit unwieldly, since we break the setting behavior across all modules:
import sys
print(sys.thing2)
# hi from sys
sys.thing2 = 5
# AttributeError: can't set attribute