📜 ⬆️ ⬇️

Antipattern settings.py



Habrapitonam hello!

From time to time I come across development patterns that exist not because they solve a problem well, but because it is done in the popular framework X, therefore, many people think that this is good.
')
Now I want to celebrate the “all settings - in settings.py” pattern. It is clear that he gained popularity thanks to Django. I now and again meet in projects that are not tied to this framework the same story: a large code base, small, pretty unrelated components, and here you are: all together from arbitrary places climb into the magic nedomodul settings for their constants.

So, why is this approach in my opinion disgusting.


Problems with cascading settings



In real-life projects, as a rule, you need at least three sets of settings: to drive a project to localhost, to launch unittest, and to turn everything on combat servers. In this case, most of the settings usually coincide in all cases, and some are different.

For example, you use MongoDB as storage. In general, you need to my_project to it on localhost and use a DB called my_project . However, to launch unittest, you need to take DB with a different name so as not to hurt the combat data: say, unittests . And in the case of production, you need to connect not to localhost, but to a well-defined IP, to a server sent under Mong.

So how, depending on the external settings.MONGODB_ADDRESS of settings.py should take on different values? Usually a voodoo-construction at the end consisting of __import__ , __dict__ , vars() , try/except ImportError , which tries to add and block the namespace with all the giblets of another module like settings_local.py.

The fact that it is additionally necessary to load _local.py is specified either by a hardcode or through an environment variable. In any case, so that the same unit tests, for example, enable their settings only at the time of launch, you have to dance with a tambourine and break the Zen of Python: Explicit is better than implicit.

In addition, this solution involves another problem, described below.

Executable code



To store settings in the form of an executable py-code is creepy. In fact, the entire pattern, apparently, originally appeared as a supposedly simple and elegant solution: “Why do we need some cfg parsers if you can do everything right on the python? And there are more opportunities! ” In scenarios a little harder than a trivial solution turns sideways. Consider, for example, such a snippet:

 # settings.py BASE_PATH = os.path.dirname(__file__) PROJECT_HOSTNAME = 'localhost' SOME_JOB_COMMAND = '%s/bin/do_job.py -H %s' % (BASE_PATH, PROJECT_HOSTNAME) # settings_production.py PROJECT_HOSTNAME = 'my-project.ru' 


Do you understand what the problem is? The fact that we have overlapped the value of PROJECT_HOSTNAME absolutely on the drum for the total value of SOME_JOB_COMMAND . We could grit our teeth with copying the definition of SOME_JOB_COMMAND after overlapping, but even this is not possible: BASE_PATH is in another module. Copy-paste it too? Is it too?

I'm not saying that the executable code as a configuration can simply lead to the difficult to ImportError when the application starts in a new environment.

Therefore, I am sure that flies should be separately, cutlets separately: the basic values ​​in the text file, calculated in the py-module.

High-coupling



A good project is one project that can be disassembled into small cubes, and each cube is put on github as a full-fledged open-source project.

When everything is the same, but with one BUT: “be so kind as to have settings.py in the root of the project and so that it contains the settings FOO_BAR , FOO_BAZ and FOO_QUX ” it looks like something absurd, isn't it? And when something sounds ridiculous, it usually means that there are situations in which this nonsense is auditioned.

In our case, the example does not force itself to invent for a long time. Let our application work with the VKontakte API, and we have something like VKontakteProfileCache , which uses settings.VK_API_KEY and settings.VK_API_SECRET in the forehead. Well used and enjoyed, and then again, and our project should start working immediately with several VKontakte applications. And everything, VKontakteProfileCache designed so that it works only with one pair of credentials.

Therefore, slimmer and more expedient to never ever access the settings module directly. Let all consumers accept the necessary settings through the parameters of the constructor, through the dependency injection framework, whatever you like, but not directly. And let the concrete settings let them pull out the very lowest level, like code in if __name__ == '__main__' . And how could he take them - his personal problems. With this approach, unit-testing is also extremely simplified: with what settings you need to run, with those we create.

Possible Solution



So, the “settings.py” pattern was covered with mud. I feel better, thank you. Now about a possible solution. I have used a similar approach in several projects and find it convenient and devoid of the problems listed.

Settings stored in text ini-style files. For parsing, we use ConfigObj : it has richer possibilities in comparison with the standard ConfigParser, in particular, it is very easy to make cascades with it.

In the project, we create the base settings file default_settings.cfg with all possible settings and their values ​​with a reasonable default.

We create the utils.config module with functions like configure_from_files() , configure_for_unittests() , which return an object with settings for different situations. configure_from_files() organizes a cascading file search: default_settings.cfg , ~/.my-project.cfg , /etc/my-project.cfg and probably somewhere else. It all depends on the project.

The calculated settings are evaluated by the last step of the assembly of the configuration object.

The module itself is only used for launching processes or tests. All classes interested in the settings receive ready-made values ​​via injections, that is, they do not communicate with the settings directly. In fact, it is not always convenient when the darkness is still better to transfer the entire configuration object, but this does not negate the fact that it needs to be transmitted - no appeal to the forehead.

Perhaps I have written too much about such a “trifle” as settings. But if at least someone, after reading, thinks about blindly copying a far from perfect approach to something and makes it better, simpler, more fun, I will consider the mission of this post accomplished.

Source: https://habr.com/ru/post/115893/


All Articles