Skip to content

🌱 Turn any object into a module 🌱

Ever wanted to call a module directly, or index it? Or just sick of seeing from foo import foo in your examples?

Give your module the awesome power of an object, or maybe just save a little typing, with xmod.

xmod is a tiny library that lets a module to do things that normally only a class could do - handy for modules that "just do one thing".

Example: Make a module callable like a function!

# In your_module.py
import xmod

@xmod
def a_function():
    return 'HERE!!'


# Test at the command line
>>> import your_module
>>> your_module()
HERE!!

Example: Make a module look like a list!?!

# In your_module.py
import xmod

xmod(list(), __name__)

# Test at the command line
>>> import your_module
>>> assert your_module == []
>>> your_module.extend(range(3))
>>> print(your_module)
[0, 1, 2]

API Documentation

xmod(extension=None, name=None, full=None, omit=None, mutable=False)

Extend the system module at name with any Python object.

The original module is replaced in sys.modules by a proxy class which delegates attributes to the original module, and then adds attributes from the extension.

In the most common use case, the extension is a callable and only the __call__ method is delegated, so xmod can also be used as a decorator, both with and without parameters.

Parameters:

Name Type Description Default
extension Optional

The object whose methods and properties extend the namespace. This includes magic methods like call and getitem.

None
name Optional[str]

The name of this symbol in sys.modules. If this is None then xmod will use extension.__module__.

This only needs to be be set if extension is not a function or class defined in the module that's being extended.

If the name argument is given, it should almost certainly be __name__.

None
full Optional[bool]

If False, just add extension as a callable.

If True, extend the module with all members of extension.

If None, the default, add the extension if it's a callable, otherwise extend the module with all members of extension.

None
mutable bool

If True, the attributes on the proxy are mutable and write through to the underlying module. If False, the default, attributes on the proxy cannot be changed.

False
omit Optional[Sequence[str]]

A list of methods not to delegate from the proxy to the extension

If omit is None, it defaults to xmod.OMIT, which seems to work well.

None

Returns:

Type Description
Any

extension, the original item that got decorated

Source code in xmod/xmod.py
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
def xmod(
    extension: Optional = None,
    name: Optional[str] = None,
    full: Optional[bool] = None,
    omit: Optional[Sequence[str]] = None,
    mutable: bool = False
) -> Any:
    """
    Extend the system module at `name` with any Python object.

    The original module is replaced in `sys.modules` by a proxy class
    which delegates attributes to the original module, and then adds
    attributes from the extension.

    In the most common use case, the extension is a callable and only the
    `__call__` method is delegated, so `xmod` can also be used as a
    decorator, both with and without parameters.

    Args:
      extension: The object whose methods and properties extend the namespace.
        This includes magic methods like __call__ and __getitem__.

      name: The name of this symbol in `sys.modules`.  If this is `None`
        then `xmod` will use `extension.__module__`.

        This only needs to be be set if `extension` is _not_ a function or
        class defined in the module that's being extended.

        If the `name` argument is given, it should almost certainly be
        `__name__`.

      full: If `False`, just add extension as a callable.

        If `True`, extend the module with all members of `extension`.

        If `None`, the default, add the extension if it's a callable, otherwise
        extend the module with all members of `extension`.

      mutable: If `True`, the attributes on the proxy are mutable and write
        through to the underlying module.  If `False`, the default, attributes
        on the proxy cannot be changed.

      omit: A list of methods _not_ to delegate from the proxy to the extension

        If `omit` is None, it defaults to `xmod.OMIT`, which seems to
        work well.

    Returns:
        `extension`, the original item that got decorated
    """
    if extension is None:
        # It's a decorator with properties
        return functools.partial(
            xmod, name=name, full=full, omit=omit, mutable=mutable
        )

    def method(f):
        @functools.wraps(f)
        def wrapped(self, *args, **kwargs):
            return f(*args, **kwargs)

        return wrapped

    def mutator(f):
        def fail(*args, **kwargs):
            raise TypeError(f'Class is immutable {args} {kwargs}')

        return method(f) if mutable else fail

    def prop(k):
        return property(
            method(lambda: getattr(extension, k)),
            mutator(lambda v: setattr(extension, k, v)),
            mutator(lambda: delattr(extension, k)),
        )
    name = name or getattr(extension, '__module__', None)
    if not name:
        raise ValueError('`name` parameter must be set')

    module = sys.modules[name]

    def _getattr(k):
        try:
            return getattr(extension, k)
        except AttributeError:
            return getattr(module, k)

    def _setattr(k, v):
        if hasattr(extension, k):
            setattr(extension, k, v)
        else:
            setattr(module, k, v)

    def _delattr(k):
        success = True
        try:
            delattr(extension, k)
        except AttributeError:
            success = False
        try:
            delattr(module, k)
        except AttributeError:
            if not success:
                raise

    members = {
        WRAPPED_ATTRIBUTE: module,
        '__getattr__': method(_getattr),
        '__setattr__': mutator(_setattr),
        '__delattr__': mutator(_delattr),
        '__doc__': getattr(module, '__doc__'),
    }

    if callable(extension):
        members['__call__'] = method(extension)
        members[EXTENSION_ATTRIBUTE] = staticmethod(extension)

    elif full is False:
        raise ValueError('extension must be callable if full is False')

    else:
        members[EXTENSION_ATTRIBUTE] = extension
        full = True

    omit = OMIT if omit is None else set(omit)
    for a in dir(extension) if full else ():
        if a not in omit:
            value = getattr(extension, a)
            is_magic = a.startswith('__') and callable(value)
            if is_magic:
                members[a] = method(value)
            elif False:  # TODO: enable or delete this
                members[a] = prop(a)

    def directory(self):
        return sorted(set(members).union(dir(module)))

    members['__dir__'] = directory

    proxy_class = type(name, (object,), members)
    sys.modules[name] = proxy_class()
    return extension

About this project