How to Use the safe_eval Module in Odoo

If you read the odoo’s source code, sometimes you will find some codes that use safe_eval module. Have you wonder, what is safe_eval ? And usually used for what ?

If we read the odoo’s source code, actually, safe_eval is just an implementation of python’s built-in eval function/module. Of course with some modifications.

If you don’t know, the eval module is used to process expressions with a string data type, so that they can be read as mathematical operations or even as a valid python code. For example, let’s say we have a variable with the string data type like the code below.

my_string = '100 + 5'

By using the eval module, we can process the my_string variable which is a string variable, so that it can be processed as a valid mathematical operation. Please try the code below.

my_string = '100 + 5'

value = eval(my_string)
print(value)

The value variable should be 105 now, the result of the 100 + 5 operation.

The eval module can also process expressions that do not have an explicit value. Like the code below.

a = 100
b = 5
my_string = 'a + b'

value = eval(my_string)

eval will automatically search for variables with the name of a and b in the global scope, then eval will calculate the my_string expressions. If we don’t want to write variables in the global scope, we can also include the list of variable names in a dictionary, like in the code below.

my_string = 'a + b'

value = eval(my_string, {'a': 100, 'b': 5})

As I wrote earlier, the safe_eval module actually is just the implementation of the Python eval built-in module with some modifications. Therefore, the code above can also be written using the safe_eval module, like the code below.

from odoo.tools.safe_eval import safe_eval

my_string = 'a + b'

value = safe_eval(my_string, {'a': 100, 'b': 5})

So what is the difference between python’s eval module and odoo’s safe_eval module?

The safe_eval module will blacklist some expressions, but when we execute the same expressions with eval it is cause no problems. Please see the list of safe_eval blacklisted expressions at odoo’s source code. The first expression which safe_eval blacklisted was the import expression. For example, suppose we have an expression like this.

my_string = "__import__('odoo').tools.float_round(a/b,pricision)"

The above expression, if passed to the eval module will not cause an error, and will return the correct value. Please try the code below.

eval_value = eval(my_string, {'a': 15, 'b': 2, 'pricision': 0})

But if passed to the safe_eval module

safe_eval_value = safe_eval(my_string, {'a': 15, 'b': 2, 'pricision': 0})

it will cause an error like the image below.

An error message when using odoo safe_eval

Apart from blacklisting the expressions that include the import code, if we read the odoo’s source code, actually safe_eval also restrict other expressions. But I haven’t found any sample code related to it, so I can’t discuss it at the moment. If I find the sample code in the future, I will update this article.

We can also change the safe_eval mode. By default when we call the safe_eval, odoo will execute the python’s eval module. But we can replace eval with another module, for example, with the exec module which is also another python built-in module. Look at the code below.

my_string = "c = a + b"
my_value = {'c': 1}
print('my_value==before==', my_value)
# my_value==before== {'c': 1}

safe_eval(my_string, {'a': 4, 'b': 7}, my_value, mode="exec", nocopy=True)

print('my_value==after==', my_value)
# my_value==after== {'c': 11}

In case you didn’t know, if we pass the c = a + b expression into the eval module, it will trigger an error, because c = a + b is not a valid expression for eval, some programmer usually call the c = a + b as not an expression, but a statement. But if the c = a + b statement is passed to the exec module, it will be processed without causing an error. But the exec module does not return value, therefore we need to enter the third argument in safe_eval, namely the locals_dict argument which in the above example the value is the my_value variable, to store the calculated result. You need to remember that when we call the safe_eval module with exec mode we must set the value of the nocopy argument to True, otherwise the value of the my_value variable will not change.

Furthermore, in odoo modules, what is safe_eval usually used for?

The first use, safe_eval is usually used to evaluate some string domains. For example in the pos_loyalty module in odoo 14 enterprise. Or in the dynamic_print_access_right module that I created to limit the print button access rights dynamically.

In the dynamic_print_access_right module I have a field with the name of condition which has data type of Char, where in the view I render the field with a widget derived from the odoo domain widget. This widget allows users to write domains easily, where user-written domains will still be saved as Char.

Odoo widget domain

Since the domain is stored as Char/String in the database, of course we can’t pass it directly to the search method or any other method retaled to the model. We have to change it to a valid domain whose the data type is a List, we can achieve this by using the safe_eval module, as I do in the my dynamic_print_access_right module. But, odoo itself is inconsistent when it comes to converting strings into valid domains, for example in the coupon module, they use the ast module which is also a python built-in module.

Another use of safe_eval in the odoo module is when they use it to calculate the Profit and Loss report or Balance Sheet in odoo enterprise. Please pay attention to the Income block formula in the Accounting >> Configuration >> Financial Reports >> Profit and Loss menu below.

Odoo income formula at profit and loss

Pay attention to the Formulas field in the image above which has a value of OPINC + OIN. If you have access to odoo enterprise, please see the formula.py file in the account_reports module. In that file the OPINC + OIN expression will be processed with safe_eval, but how odoo writes the code in this module, I think is quite interesting. Because when odoo calls safe_eval the OPINC and OIN values don’t exist yet, unlike the code below.

safe_eval('OPINC + OIN', {'OPINC': 100, 'OIN': 200})

But the value of those two variables will be processed dynamically. To illustrate, suppose we have an expression like this.

my_string = 'S00006 + S00007'

Let’s say S00006 and S00007 are Sales Order numbers in our database. By using safe_eval we will make the values of those variables will be searched and calculated dynamically, so if we change the expression to P00006 * P00008 and want the data to be retrieved from the Purchase Order, we don’t need to change our code.

First, let’s create a class that inherits to Dictionary.

class DataSet(dict):

    def __init__(self, model, field_to_search, field_to_calculate):
        super().__init__()
        self.model = model
        self.field_to_search = field_to_search
        self.field_to_calculate = field_to_calculate

    def __getitem__(self, item):
        record = self.model.search([(self.field_to_search,'=',item)],limit=1)
        if record:
            return getattr(record, self.field_to_calculate)
        else:
            return 0

Then when we call the safe_eval we can use the DataSet class above as an argument, like the code below.

my_string = 'S00006 + S00007'

value = safe_eval(my_string,DataSet(self.env['sale.order'],'name','amount_total'), nocopy=True)

When safe_eval tries to get the value of S00006 expression, safe_eval will trigger the Dictionary __getitem__ method. That’s why we override this method in order to return a dynamic value. If we want to use safe_eval for other purposes, for example to get the subtotal value of Purchase Order, we can change it easily, like the code below.

my_string = 'P00006 + P00007'

value = safe_eval(my_string,DataSet(self.env['purchase.order'],'name','amount_untaxed'), nocopy=True)

Please remember, when we use the dynamic method on safe_eval like the previouse code, we must set the value of the nocopy argument to True. But if we use the eval it is not necessary.

That’s all I can write about the safe_eval. Actually, I haven’t found any sample code that makes safe_eval is better and should be the first choice over the eval. But I try to always use the safe_eval to evaluate expressions where the user can input them freely. With the safe word in its name, and it is proven that safe_eval will blacklist some expression, I hope when I use the safe_eval the application that I create can be more secure.

Related Article

Leave a Reply