{ "info": { "author": "Nathaniel J. Smith", "author_email": "njs@pobox.com", "bugtrack_url": null, "classifiers": [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: BSD License", "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.6", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.2", "Programming Language :: Python :: 3.3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5" ], "description": "metamodule - Useful tools and gee-whiz tricks for defining Python APIs\n======================================================================\n\nIn Python, writing a *metaclass* lets you create new kinds of class\nobjects whose behaviour you can control.\n\nBy analogy (and bit of abuse of English), writing a *metamodule* lets\nyou create module objects with customized behaviour.\n\n``metamodule.py`` is a single-file, permissively-licensed Python\nlibrary that makes it easy and safe to use custom module subtypes as\nthe public interface for your library. For example, ordinarily in\nPython it's easy to issue a deprecation warning when someone calls a\ndeprecated function (``mymodule.foo()``), but it's very difficult to\nissue a deprecation warning when someone accesses a deprecated\nconstant (``mymodule.FOO``). Another commonly-requested (though\nsomewhat dangerous) feature is the ability to delay importing a\nsubmodule until the first time it's accessed\n(``mymodule.submodule.subfunction()``). With metamodule, these are\nboth easy to solve: we just give ``mymodule`` a custom ``__getattr__``\nmethod that does what we want. (And in fact, you don't even need to\nwrite this ``__getattr__`` -- metamodule includes an implementation\nthat provides both of the above features out-of-the-box.)\n\n\nExample / documentation\n=======================\n\nIn the source directory of this project, try starting a Python REPL\nand running::\n\n >>> import examplepkg\n\n``examplepkg`` is a module object::\n\n >>> import types\n >>> isinstance(examplepkg, types.ModuleType)\n True\n\nBut it's not a regular module object; it's a custom subclass::\n\n >>> examplepkg\n \n\nAnd this subclass has superpowers::\n\n # Automatically loads the submodule on first access:\n >>> examplepkg.submodule.subattr\n ... submodule loading ...\n 'look ma no import'\n\n # Imports are cached so future usage is just as fast as regular access:\n >>> examplepkg.submodule.subattr\n 'look ma no import'\n\n # Accessing this attribute triggers a warning:\n >>> examplepkg.a\n __main__:1: FutureWarning: 'a' attribute will become 2 in next release\n 1\n\n # But regular attributes continue to work fine, with no speed penalty:\n >>> examplepkg.b\n 2\n\n # reload() works fine (except on CPython 3.3, which is buggy)\n >>> import imp\n >>> imp.reload(examplepkg)\n \n\n # And functions defined in the package use the same globals dict\n # as the package itself. (On py2 replace .__globals__ with .func_globals)\n >>> examplepkg.__dict__ is examplepkg.f.__globals__\n True\n\nTo accomplish this, all we had to do was put the following code at the\ntop of ``examplepkg/__init__.py``::\n\n # WARNING: this should be placed at the *very top* of your module,\n # *before* you import any code that might recursively re-import\n # your package.\n import metamodule\n metamodule.install(__name__)\n del metamodule\n\n # Any strings in this set name submodules that will be lazily imported:\n # NB: you probably shouldn't use this unless you have a real,\n # specific need for it, since it can cause import errors and other\n # side-effects to appear at weird and confusing places.\n __auto_import__.add(\"submodule\")\n\n # Attributes that we want to warn users about:\n __warn_on_access__[\"a\"] = (\n # Attribute value\n 1,\n # Warning issued when attribute is accessed\n FutureWarning(\"'a' attribute will become 2 in next release\"))\n\nYou can also define your own ``ModuleType`` subclass and pass it as\nthe second argument to ``metamodule.install``. Your class can do\nanything you can regularly do with a Python class -- define special\nmethods like ``__getattribute__``, use properties, have a custom\n``__repr__``, whatever you want. Note that your class instance's\n``__dict__`` will be the module globals dict, so assigning to\n``self.foo`` is equivalent to creating a global variable in your\nmodule named ``foo``, and vice-versa.\n\nThe one thing to watch out for is that your class's ``__init__`` will\n*not* be called -- instead, you should define a method\n``__metamodule_init__`` which will be called immediately after your\nmetamodule class is installed.\n\n\nVersions supported\n==================\n\nMetamodule is currently tested against:\n\n* CPython 2.6, 2.7\n* CPython 3.2, 3.3, 3.4, and pre-releases of 3.5\n\nI suspect it will *work* on pretty much every version of CPython that\nhas a working ``ctypes``, I just don't have convenient access to older\nversions to test.\n\nAs far as I know we do not yet support PyPy, Jython, etc., but we will\nas soon they catch up with Python 3.5 and start allowing ``__class__``\nassignment on module objects.\n\n\nHow it works\n============\n\nPython has always allowed these kinds of tricks to some extent, via\nthe mechanism of assigning a new object to\n``sys.modules[\"mymodule\"]``; this object can then have whatever\nbehaviour you like. This can work well, but the end result is that you\nhave two different objects that both represent the same module: your\noriginal module object (which owns the ``globals()`` namespace where\nyour module code executes), and your custom object. Depending on the\nrelative order of the assignment to ``sys.modules`` and imports of\nsubmodules, you can end up with different pieces of code in the same\nprogram thinking that ``mymodule`` refers to one or the other of these\nobjects. If they don't share the same ``__dict__``, then their\nnamespaces can get out of sync; alternatively, if they *do* share the\nsame ``__dict__``, then this means that your custom object can't\nsubclass ``ModuleType`` (module objects don't allow reassignment of\ntheir ``__dict__`` attribute), which breaks ``reload()``. All in all\nit's a bit of a mess. It's possible to write correct code using this\nmethod, if you are extremely careful -- for example `apipkg\n`_ is a somewhat similar library\nuses this approach, but to keep things workable it requires that your\nlibrary's public interface be defined *entirely* by apipkg\ncalls. There's no easy way to take a legacy Python package and\nincrementally switch it over to using apipkg.\n\nThe key feature that metamodule provides is: it makes it easy to set\nup ``sys.modules[\"mymodule\"]`` so that it is both (a) an instance of a\nclass that you control, so you can have arbitrary properties etc.,\nAND (b) a regular subclass of ``ModuleType`` with your\n``__init__.py``'s ``globals()`` as its ``__dict__`` attribute, so that\nyou can continue using the usual Python approach to defining your\nAPI.\n\nThis combination makes it easy and safe to transition an existing\nlibrary to using metamodule -- just add a call to\n``metamodule.install`` at the top of your ``__init__.py``, and nothing\nat all will change, except that you can now start taking advantage of\nyour new superpowers at your leisure.\n\nHow do we do it? On CPython 3.5 and later, this is easy: metamodule\nuses ``__class__`` assignment on module objects (a feature that was\nadded to CPython explicitly to support this usage).\n\nOn CPython 3.4 and earlier, it uses ``ctypes`` hacks. These are ugly,\nbut safe so long as no one goes back in time and changes the internal\nmemory layout of module objects on old, already-released versions of\nPython. (Which is not going to happen.) Basically, we instantiate a\nnew object of the specified ``ModuleType`` subclass, and then we use\nsome arcane knowledge of how these objects are laid out in order to\nswap the guts of your original module and the new object. Then we\nassign the new object into ``sys.modules``. This preserves the key\ninvariant that at any given point there's exactly one module that owns\nyour globals dict, and it's in ``sys.modules``. It does, however, mean\nthat things will go horribly wrong if you call ``metamodule.install``\n*after* someone else has already imported your module. So unless you\nonly want to support Python 3.5+, then make sure to call\n``metamodule.install`` right at the top of your module definition\nfile.\n\nThese two tricks together let us safely support all versions of\nCPython, and as alternative implementations like PyPy catch up with\nCPython 3.5 in supporting ``__class__`` assignment, we'll support\nthose too.\n\n\nChange history\n==============\n\n1.1:\n\n* When looking up ``__metamodule_init__``, go straight to the class\n without checking the instance. This makes our behavior more\n consistent with regular ``__init__``, and avoids accidentally\n triggering ``__getattr__``. (Thanks to Antony Lee for the report +\n fix.)\n\n1.0:\n\n* First public release.", "description_content_type": null, "docs_url": null, "download_url": "", "downloads": { "last_day": -1, "last_month": -1, "last_week": -1 }, "home_page": "https://github.com/njsmith/metamodule", "keywords": "", "license": "2-clause BSD", "maintainer": "", "maintainer_email": "", "name": "metamodule", "package_url": "https://pypi.org/project/metamodule/", "platform": "UNKNOWN", "project_url": "https://pypi.org/project/metamodule/", "project_urls": { "Homepage": "https://github.com/njsmith/metamodule" }, "release_url": "https://pypi.org/project/metamodule/1.1/", "requires_dist": null, "requires_python": "", "summary": "A tiny Python module for taking control of your library's public API.", "version": "1.1" }, "last_serial": 2159912, "releases": { "0.0.0-dev": [], "1.0": [ { "comment_text": "", "digests": { "md5": "51956fb7714ed979021e1f3f1d81f27f", "sha256": "fb42534f389328448127e3a900f18d01489104cdf16164cc961d6cf3a320002b" }, "downloads": -1, "filename": "metamodule-1.0-py2.py3-none-any.whl", "has_sig": false, "md5_digest": "51956fb7714ed979021e1f3f1d81f27f", "packagetype": "bdist_wheel", "python_version": "py2.py3", "requires_python": null, "size": 10896, "upload_time": "2015-07-22T06:07:32", "url": "https://files.pythonhosted.org/packages/6f/67/5b3f6cd50fb7359a92cb53b08d7d7227480db2f494c78583617963200a76/metamodule-1.0-py2.py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "003a5e63b288c5adbbd75c14eb857f7f", "sha256": "ea56688a6913ace7fde6ddc5a5b43fdb86aecddeeaf470b123caa164f160f5c7" }, "downloads": -1, "filename": "metamodule-1.0.zip", "has_sig": false, "md5_digest": "003a5e63b288c5adbbd75c14eb857f7f", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 18481, "upload_time": "2015-07-22T06:07:35", "url": "https://files.pythonhosted.org/packages/e0/19/f9c41954b5d4d7e6f3403a6333b905b9a2ffef66eadcc4588370c02446b4/metamodule-1.0.zip" } ], "1.1": [ { "comment_text": "", "digests": { "md5": "32399aca2b6658726030ead1bc0cebd3", "sha256": "f80b0e13ec03686aa96b6dc1f87d2a70decf8f72fb083e0d2463a7be5a94a395" }, "downloads": -1, "filename": "metamodule-1.1-py2.py3-none-any.whl", "has_sig": false, "md5_digest": "32399aca2b6658726030ead1bc0cebd3", "packagetype": "bdist_wheel", "python_version": "py2.py3", "requires_python": null, "size": 11557, "upload_time": "2016-06-10T03:20:10", "url": "https://files.pythonhosted.org/packages/e4/3f/3c768b05a787def31ecaecdfef2a201aa73cd80ab66867743fc315716cd6/metamodule-1.1-py2.py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "a999eda32f99ef2b8370b798faf2d5d6", "sha256": "04ad47e60fddcb6407ceb838dc9b39a8ecf990a66ffb5f56cd228b8942ec7218" }, "downloads": -1, "filename": "metamodule-1.1.zip", "has_sig": false, "md5_digest": "a999eda32f99ef2b8370b798faf2d5d6", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 19461, "upload_time": "2016-06-10T03:20:14", "url": "https://files.pythonhosted.org/packages/32/c9/5ff83663523a98ee39ca78f71dd1e8d3cdf62b69ad933a778a6bc7596ead/metamodule-1.1.zip" } ] }, "urls": [ { "comment_text": "", "digests": { "md5": "32399aca2b6658726030ead1bc0cebd3", "sha256": "f80b0e13ec03686aa96b6dc1f87d2a70decf8f72fb083e0d2463a7be5a94a395" }, "downloads": -1, "filename": "metamodule-1.1-py2.py3-none-any.whl", "has_sig": false, "md5_digest": "32399aca2b6658726030ead1bc0cebd3", "packagetype": "bdist_wheel", "python_version": "py2.py3", "requires_python": null, "size": 11557, "upload_time": "2016-06-10T03:20:10", "url": "https://files.pythonhosted.org/packages/e4/3f/3c768b05a787def31ecaecdfef2a201aa73cd80ab66867743fc315716cd6/metamodule-1.1-py2.py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "a999eda32f99ef2b8370b798faf2d5d6", "sha256": "04ad47e60fddcb6407ceb838dc9b39a8ecf990a66ffb5f56cd228b8942ec7218" }, "downloads": -1, "filename": "metamodule-1.1.zip", "has_sig": false, "md5_digest": "a999eda32f99ef2b8370b798faf2d5d6", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 19461, "upload_time": "2016-06-10T03:20:14", "url": "https://files.pythonhosted.org/packages/32/c9/5ff83663523a98ee39ca78f71dd1e8d3cdf62b69ad933a778a6bc7596ead/metamodule-1.1.zip" } ] }