通過python的import hooks實現像引用代碼一樣使用設定檔

來源:互聯網
上載者:User

偶然看到網上的一篇文章,Treating configuration as code with Python’s import hooks(http://www.taricorp.net/2012/treating-configuration-as-code-with-pythons-import-hook),藉助python的import hooks來實現一種新穎的讀取設定檔資訊的方法,感覺這種想法很不錯。其中展現了imp、sys.meta_path、sys.modules的應用,值得一讀。
現將其轉載,期望更多的人能看到。

Treating configuration as code with Python’s import hooksPosted on 28/08/2012 by tariRationale

I was reading up on web frameworks available when programming in Haskell earlier today, and I liked the use of domain-specific languages (DSLs) within frameworks such as the routing syntax in Yesod. Compared to how routes are specified in Django (as a similar example that I’m already familiar with), the DSL is both easier to read (because it doesn’t need to be valid code in the hosting language) and faster (since it ends up getting compiled into the application as properly executable code).

A pattern I find myself using rather often in Python projects is to have a small module (usually called config) that encapsulates an INI-style configuration file. It feels like an ugly solution though, since it generally just exports a ConfigParser instance. Combined with consideration of DSLs in Haskell, that got me thinking: what if there were an easier way that made INI configuration files act like Python source such that they could just be imported and have the contents of the file exposed as simple Python types (thus hiding some unnecessary complexity)?

Implementation

I was aware of Python’s import hook mechanisms, so I figured that it should be a good way to approach this problem, and it ended up being a good excuse to learn more about the import hook mechanism. Thus, the following code provides a way to expose INI-style configuration as Python modules. It should be compatible with Python 3 after changing the import of ConfigParser on line 1 to configparser, but I only tested it on Python 2.7.

import ConfigParser, imp, os, sys class INILoader(object):    def __init__(self, prefix):        self.prefix = prefix     def load_module(self, name):        if name in sys.modules:            return sys.modules[name]         module = imp.new_module(name)        if name == self.prefix:            # 'from config import foo' gets config then config.foo,            # so we need a dummy package.            module.__package__ = name            module.__path__ = []            module.__file__ = __file__        else:            # Try to find a .ini file            module.__package__, _, fname = name.rpartition('.')            fname += '.ini'            module.__file__ = fname            if not os.path.isfile(fname):                raise ImportError("Could not find a .ini file matching " + name)            else:                load_ini_module(fname, module)         sys.modules[name] = module        return module     def find_module(self, name, path=None):        if name.startswith(self.prefix):            return self        else:            return None def load_ini_module(f, m):    """Load ini-style file ``f`` into module ``m``."""    cp = ConfigParser.SafeConfigParser()    cp.read(f)    for section in cp.sections():        setattr(m, section, dict(cp.items(section))) def init(package='config'):    """Install the ini import hook for the given virtual package name."""    sys.meta_path.append(INILoader(package))

Most of this code should be fairly easy to follow. The magic of the import hook itself is all in the INILoader class, and exactly how that works is specified in PEP 302.

Usage

So how do you use this? Basically, you must simply run init(), then any imports from the specified package (config by default) will be resolved from an .ini file rather than an actual Python module. Sections in a file are exposed as dictionaries under the module.

An example is much more informative than the preceding short description, so here’s one. I put the code on my Python path as INIImport.py and created foo.ini with the following contents:

[cat]sound=meow[dog]sound=woof[cow]sound=moo

It has three sections, each describing an animal. Now I load up a Python console and use it:

>>> import INIImport>>> INIImport.init()>>> from config import foo>>> foo.cat{'sound': 'meow'}>>> foo.dog['sound']'woof'

This has the same semantics as a normal Python module, so it can be reloaded or aliased just like any other module:

>>> import config.foo>>> foo == config.fooTrue>>> reload(config.foo)<module 'config.foo' from 'foo.ini'>

The ability to reload this module is particularly handy, because my normal configuration module approach doesn’t provide an easy way to reload the file.

Improvements, Limitations

Some addition improvements come to mind if I were to release this experiment as production-quality code. Notably, additional path manipulations for finding .ini files would be useful, such as taking a path argument to init(), supplying a set of directories to search within. Having a way to remove the import hook that it installs would also be good, and straightforward to implement. There’s no way to get all the sections in the module, so it would also be useful to export the sections somehow– perhaps by having the module support the mapping protocol (so all the sections could be retrieved with module.items(), for example).

The main limitation of this scheme is that it has no way to determine the desired type of loaded configuration values, so everything is a string. This is a typical limitation when using the ConfigParser module, but compared to a more robust configuration scheme such as defining objects in a Python file (such as Django does), this might be an annoying loss of expressiveness. The values can always be coerced to the required type when retrieving them, but that’s a bit of unnecessary extra code in whatever uses the configuration.

It may also be useful to provide a way to write configuration back to a file when modifying a config module, but my simplistic implementation makes no attempt at such. Doing so would not be terribly difficult, just involving some wrapper objects to handle member assignment for sections and items, then providing a mechanism for saving the current values back to the original file.

Postlude

This made for an interesting experiment, and it should be a handy example for how to implement import hooks in Python. You may use this code freely within your own work, but I’d appreciate if you leave a note here that it was useful, and link back to this post.

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.