Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ADD] prefer-env-translation: Add new check for odoo v18.0 #516

Merged
merged 1 commit into from
Jan 14, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
118 changes: 67 additions & 51 deletions README.md

Large diffs are not rendered by default.

5 changes: 3 additions & 2 deletions src/pylint_odoo/checkers/custom_logging.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from astroid import builder, exceptions as astroid_exceptions, nodes
from pylint.checkers import logging

from .. import misc
from .odoo_addons import OdooAddons
from .odoo_base_checker import OdooBaseChecker

Expand Down Expand Up @@ -67,9 +68,9 @@ def add_message(self, msgid, *args, **kwargs):
return super().add_message(msgid, *args, **kwargs)

def visit_call(self, node):
if not isinstance(node.func, nodes.Name):
name = OdooAddons.get_func_name(node.func)
if name not in misc.TRANSLATION_METHODS:
return
name = node.func.name
with config_logging_modules(self.linter, ("odoo",)):
self._check_log_method(node, name)

Expand Down
17 changes: 14 additions & 3 deletions src/pylint_odoo/checkers/odoo_addons.py
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,11 @@
"deprecated-odoo-model-method",
CHECK_DESCRIPTION,
),
"W8161": (
"Better using self.env._ More info at https://github.com/odoo/odoo/pull/174844",
"prefer-env-translation",
CHECK_DESCRIPTION,
),
}

DFTL_MANIFEST_REQUIRED_KEYS = ["license"]
Expand Down Expand Up @@ -569,6 +574,7 @@ class OdooAddons(OdooBaseChecker, BaseChecker):
"odoo_maxversion": "13.0",
},
"no-raise-unlink": {"odoo_minversion": "15.0"},
"prefer-env-translation": {"odoo_minversion": "18.0"},
}

def __init__(self, linter: PyLinter):
Expand Down Expand Up @@ -789,6 +795,7 @@ def _get_assignation_nodes(self, node):
"method-inverse",
"method-search",
"no-write-in-compute",
"prefer-env-translation",
"print-used",
"renamed-field-parameter",
"sql-injection",
Expand Down Expand Up @@ -870,8 +877,7 @@ def visit_call(self, node):
self.odoo_computes.add(method_name)
if (
isinstance(argument_aux, nodes.Call)
and isinstance(argument_aux.func, nodes.Name)
and argument_aux.func.name == "_"
and self.get_func_name(argument_aux.func) in misc.TRANSLATION_METHODS
):
self.add_message("translation-field", node=argument_aux)
index += 1
Expand Down Expand Up @@ -942,7 +948,12 @@ def visit_call(self, node):
self.add_message("translation-required", node=node, args=("message_post", keyword, as_string))

# Call _(...) with variables into the term to be translated
if isinstance(node.func, nodes.Name) and node.func.name == "_" and node.args:
if self.get_func_name(node.func) in misc.TRANSLATION_METHODS and node.args:
# "_" -> isinstance(node.func, nodes.Name)
# "self.env._" -> isinstance(node.func, nodes.Attribute)
if isinstance(node.func, nodes.Name) and node.func.as_string() in misc.TRANSLATION_METHODS:
self.add_message("prefer-env-translation", node=node)

wrong = ""
right = ""
arg = node.args[0]
Expand Down
1 change: 1 addition & 0 deletions src/pylint_odoo/misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"18.0",
]
DFTL_MANIFEST_VERSION_FORMAT = r"({valid_odoo_versions})\.\d+\.\d+\.\d+$"
TRANSLATION_METHODS = ("_", "_lt")


class StringParseError(TypeError):
Expand Down
195 changes: 195 additions & 0 deletions testing/resources/test_repo/broken_module/models/broken_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@
from odoo import fields, models, _
from odoo.exceptions import UserError
from odoo import exceptions
from odoo.tools.translate import LazyTranslate


# Relatives import for odoo addons
from odoo.addons.broken_module import broken_model as broken_model1
Expand All @@ -61,6 +63,7 @@
import itertools
from itertools import groupby

_lt = LazyTranslate(__name__)
other_field = fields.Char()


Expand Down Expand Up @@ -402,6 +405,134 @@ def my_method1(self, variable1):
self.message_post(_('Double method _ and lstrtip %s').lstrip() % (variable1,)) # TODO: Emit message for this case
return error_msg

def my_method11(self, variable1):
# Shouldn't show error of field-argument-translate
self.my_method2(self.env._('hello world'))

# Message post with new translation function
self.message_post(subject=self.env._('Subject translatable'),
body=self.env._('Body translatable'))
self.message_post(self.env._('Body translatable'),
self.env._('Subject translatable'))
self.message_post(self.env._('Body translatable'),
subject=self.env._('Subject translatable'))
self.message_post(self.env._('A CDR has been recovered for %s') % (variable1,))
self.message_post(self.env._('A CDR has been recovered for %s') % variable1)
self.message_post(self.env._('Var {a}').format(a=variable1))
self.message_post(self.env._('Var %(variable)s') % {'variable': variable1})
self.message_post(subject=self.env._('Subject translatable'),
body=self.env._('Body translatable %s') % variable1)
self.message_post(subject=self.env._('Subject translatable %(variable)s') %
{'variable': variable1},
message_type='notification')
self.message_post(self.env._('Body translatable'),
self.env._('Subject translatable {a}').format(a=variable1))
self.message_post(self.env._('Body translatable %s') % variable1,
self.env._('Subject translatable %(variable)s') %
{'variable': variable1})
self.message_post('<p>%s</p>' % self.env._('Body translatable'))
self.message_post(body='<p>%s</p>' % self.env._('Body translatable'))

# translation new function with variables in the term
variable2 = variable1
self.message_post(self.env._('Variable not translatable: %s' % variable1))
self.message_post(self.env._('Variables not translatable: %s, %s' % (
variable1, variable2)))
self.message_post(body=self.env._('Variable not translatable: %s' % variable1))
self.message_post(body=self.env._('Variables not translatable: %s %s' % (
variable1, variable2)))
error_msg = self.env._('Variable not translatable: %s' % variable1)
error_msg = self.env._('Variables not translatable: %s, %s' % (
variable1, variable2))
error_msg = self.env._('Variable not translatable: {}'.format(variable1))
error_msg = self.env._('Variables not translatable: {}, {variable2}'.format(
variable1, variable2=variable2))

# string with parameters without name
# so you can't change the order in the translation
self.env._('%s %d') % ('hello', 3)
self.env._('%s %s') % ('hello', 'world')
self.env._('{} {}').format('hello', 3)
self.env._('{} {}').format('hello', 'world')

# Valid cases
self.env._('%(strname)s') % {'strname': 'hello'}
self.env._('%(strname)s %(intname)d') % {'strname': 'hello', 'intname': 3}
self.env._('%s') % 'hello'
self.env._('%d') % 3
self.env._('{}').format('hello')
self.env._('{}').format(3)

# It raised exception but it was already fixed
msg = "Invalid not _ method %s".lstrip() % "value"
# It should emit message but binop.left is showing "lstrip" only instead of "_"
self.message_post(self.env._('Double method _ and lstrtip %s').lstrip() % (variable1,)) # TODO: Emit message for this case
return error_msg

def my_method111(self, variable1):
# Shouldn't show error of field-argument-translate
self.my_method2(_lt('hello world'))

# Message post with new translation function
self.message_post(subject=_lt('Subject translatable'),
body=_lt('Body translatable'))
self.message_post(_lt('Body translatable'),
_lt('Subject translatable'))
self.message_post(_lt('Body translatable'),
subject=_lt('Subject translatable'))
self.message_post(_lt('A CDR has been recovered for %s') % (variable1,))
self.message_post(_lt('A CDR has been recovered for %s') % variable1)
self.message_post(_lt('Var {a}').format(a=variable1))
self.message_post(_lt('Var %(variable)s') % {'variable': variable1})
self.message_post(subject=_lt('Subject translatable'),
body=_lt('Body translatable %s') % variable1)
self.message_post(subject=_lt('Subject translatable %(variable)s') %
{'variable': variable1},
message_type='notification')
self.message_post(_lt('Body translatable'),
_lt('Subject translatable {a}').format(a=variable1))
self.message_post(_lt('Body translatable %s') % variable1,
_lt('Subject translatable %(variable)s') %
{'variable': variable1})
self.message_post('<p>%s</p>' % _lt('Body translatable'))
self.message_post(body='<p>%s</p>' % _lt('Body translatable'))

# translation new function with variables in the term
variable2 = variable1
self.message_post(_lt('Variable not translatable: %s' % variable1))
self.message_post(_lt('Variables not translatable: %s, %s' % (
variable1, variable2)))
self.message_post(body=_lt('Variable not translatable: %s' % variable1))
self.message_post(body=_lt('Variables not translatable: %s %s' % (
variable1, variable2)))
error_msg = _lt('Variable not translatable: %s' % variable1)
error_msg = _lt('Variables not translatable: %s, %s' % (
variable1, variable2))
error_msg = _lt('Variable not translatable: {}'.format(variable1))
error_msg = _lt('Variables not translatable: {}, {variable2}'.format(
variable1, variable2=variable2))

# string with parameters without name
# so you can't change the order in the translation
_lt('%s %d') % ('hello', 3)
_lt('%s %s') % ('hello', 'world')
_lt('{} {}').format('hello', 3)
_lt('{} {}').format('hello', 'world')

# Valid cases
_lt('%(strname)s') % {'strname': 'hello'}
_lt('%(strname)s %(intname)d') % {'strname': 'hello', 'intname': 3}
_lt('%s') % 'hello'
_lt('%d') % 3
_lt('{}').format('hello')
_lt('{}').format(3)

# It raised exception but it was already fixed
msg = "Invalid not _ method %s".lstrip() % "value"
# It should emit message but binop.left is showing "lstrip" only instead of "_"
self.message_post(_lt('Double method _ and lstrtip %s').lstrip() % (variable1,)) # TODO: Emit message for this case
return error_msg

def my_method2(self, variable2):
return variable2

Expand Down Expand Up @@ -432,6 +563,18 @@ def my_method7(self):
# Method with translation
raise UserError(_('String with translation'))

def my_method71(self):
user_id = 1
if user_id != 99:
# Method with translation
raise UserError(self.env._('String with translation'))

def my_method72(self):
user_id = 1
if user_id != 99:
# Method with translation
raise UserError(_lt('String with translation'))

def my_method8(self):
user_id = 1
if user_id != 99:
Expand Down Expand Up @@ -476,6 +619,28 @@ def my_method13(self):
raise exceptions.Warning(_(
'String with params format %(p1)s' % {'p1': 'v1'}))

def my_method131(self):
# Shouldn't show error
raise exceptions.Warning(self.env._(
'String with params format {p1}').format(p1='v1'))
raise exceptions.Warning(self.env._(
'String with params format {p1}'.format(p1='v1')))
raise exceptions.Warning(self.env._(
'String with params format %(p1)s') % {'p1': 'v1'})
raise exceptions.Warning(self.env._(
'String with params format %(p1)s' % {'p1': 'v1'}))

def my_method132(self):
# Shouldn't show error
raise exceptions.Warning(_lt(
'String with params format {p1}').format(p1='v1'))
raise exceptions.Warning(_lt(
'String with params format {p1}'.format(p1='v1')))
raise exceptions.Warning(_lt(
'String with params format %(p1)s') % {'p1': 'v1'})
raise exceptions.Warning(_lt(
'String with params format %(p1)s' % {'p1': 'v1'}))

def my_method14(self):
_("String with missing args %s %s", "param1")
_("String with missing kwargs %(param1)s", param2="hola")
Expand All @@ -491,6 +656,36 @@ def my_method14(self):
_("String with correct args %s", "param1")
_("String with correct kwargs %(param1)s", param1="hola")

def my_method141(self):
self.env._("String with missing args %s %s", "param1")
self.env._("String with missing kwargs %(param1)s", param2="hola")
self.env._(f"String with f-interpolation {self.param1}")
self.env._("String unsupported character %y", "param1")
self.env._("format truncated %s%", 'param1')
self.env._("too many args %s", 'param1', 'param2')

self.env._("multi-positional args without placeholders %s %s", 'param1', 'param2')

self.env._("multi-positional args without placeholders {} {}".format('param1', 'param2'))

self.env._("String with correct args %s", "param1")
self.env._("String with correct kwargs %(param1)s", param1="hola")

def my_method142(self):
_lt("String with missing args %s %s", "param1")
_lt("String with missing kwargs %(param1)s", param2="hola")
_lt(f"String with f-interpolation {self.param1}")
_lt("String unsupported character %y", "param1")
_lt("format truncated %s%", 'param1')
_lt("too many args %s", 'param1', 'param2')

_lt("multi-positional args without placeholders %s %s", 'param1', 'param2')

_lt("multi-positional args without placeholders {} {}".format('param1', 'param2'))

_lt("String with correct args %s", "param1")
_lt("String with correct kwargs %(param1)s", param1="hola")

def old_api_method_alias(self, cursor, user, ids, context=None): # old api
pass

Expand Down
Loading
Loading