From 959059fb5e7e031d96da693455e6f969fb3d0c7b Mon Sep 17 00:00:00 2001 From: Olivier Dony Date: Tue, 29 Mar 2022 10:14:40 +0000 Subject: [PATCH 01/13] [FIX] _vendor: fix compatibility with werkzeug 2.0+ As of werkzeug 2.0, the `posixemulation` compatibility layer for atomic rename operations is abandoned[1]. In the mean time an atomic, cross-platform file renaming function was introduced in the stdlib, as of Python 3.3: `os.replace()`. By using `os.replace()` instead of `posixemulation.rename()`, we can ensure compatibility with versions 0.x, 1.x and 2.x of werkzeug. We've always required Python 3.5+ since the P3 support, so `os.replace()` is always available. This is a follow-up of the work for supporting werkzeug 1.x [2] References: [1] https://github.com/pallets/werkzeug/pull/1790 [2] vendoring of werkzeug.sessions: odoo/odoo#45931 Part-of: odoo/odoo#91927 --- odoo/tools/_vendor/sessions.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/odoo/tools/_vendor/sessions.py b/odoo/tools/_vendor/sessions.py index e64a8865e21..5bdbc818e99 100644 --- a/odoo/tools/_vendor/sessions.py +++ b/odoo/tools/_vendor/sessions.py @@ -20,14 +20,13 @@ import os import re import tempfile from hashlib import sha1 -from os import path +from os import path, replace as rename from pickle import dump from pickle import HIGHEST_PROTOCOL from pickle import load from time import time from werkzeug.datastructures import CallbackDict -from werkzeug.posixemulation import rename _logger = logging.getLogger(__name__) _sha1_re = re.compile(r"^[a-f0-9]{40}$") From 65c8814a2f53908dce0a2010892092daac44c48d Mon Sep 17 00:00:00 2001 From: Christophe Monniez Date: Thu, 14 Apr 2022 12:45:07 +0000 Subject: [PATCH 02/13] [FIX] various: replace deprecated currentThread method CurrentThread is now really deprecated in Python 3.10 ... Time to change. Part-of: odoo/odoo#91927 --- addons/crm/models/crm_lead.py | 2 +- addons/crm/models/crm_team.py | 2 +- addons/crm/models/crm_team_member.py | 2 +- addons/event/models/event_mail.py | 2 +- addons/hr/models/res_config_settings.py | 2 +- addons/mail/models/mail_mail.py | 2 +- addons/mail/models/mail_thread.py | 2 +- addons/mass_mailing/models/mailing.py | 2 +- addons/partner_autocomplete/models/res_company.py | 2 +- addons/sale_timesheet/models/product.py | 4 ++-- addons/sms/models/sms_sms.py | 4 ++-- addons/stock_sms/models/stock_picking.py | 4 ++-- addons/website/models/theme_models.py | 2 +- odoo/__init__.py | 2 +- odoo/addons/base/models/ir_mail_server.py | 2 +- odoo/addons/base/models/ir_module.py | 2 +- odoo/addons/base/tests/test_mail.py | 2 +- odoo/modules/loading.py | 4 ++-- odoo/service/server.py | 14 +++++++------- odoo/tests/loader.py | 4 ++-- odoo/tools/cache.py | 2 +- odoo/tools/translate.py | 2 +- 22 files changed, 33 insertions(+), 33 deletions(-) diff --git a/addons/crm/models/crm_lead.py b/addons/crm/models/crm_lead.py index a43de43ebe1..249c007a77d 100644 --- a/addons/crm/models/crm_lead.py +++ b/addons/crm/models/crm_lead.py @@ -2156,7 +2156,7 @@ class Lead(models.Model): # - avoid blocking the table for too long with a too big transaction transactions_count, transactions_failed_count = 0, 0 cron_update_lead_start_date = datetime.now() - auto_commit = not getattr(threading.currentThread(), 'testing', False) + auto_commit = not getattr(threading.current_thread(), 'testing', False) for probability, probability_lead_ids in probability_leads.items(): for lead_ids_current in tools.split_every(PLS_UPDATE_BATCH_STEP, probability_lead_ids): transactions_count += 1 diff --git a/addons/crm/models/crm_team.py b/addons/crm/models/crm_team.py index c0f0d5bce8e..3e622756edb 100644 --- a/addons/crm/models/crm_team.py +++ b/addons/crm/models/crm_team.py @@ -445,7 +445,7 @@ class Team(models.Model): BUNDLE_HOURS_DELAY = int(self.env['ir.config_parameter'].sudo().get_param('crm.assignment.delay', default=0)) BUNDLE_COMMIT_SIZE = int(self.env['ir.config_parameter'].sudo().get_param('crm.assignment.commit.bundle', 100)) - auto_commit = not getattr(threading.currentThread(), 'testing', False) + auto_commit = not getattr(threading.current_thread(), 'testing', False) # leads max_create_dt = self.env.cr.now() - datetime.timedelta(hours=BUNDLE_HOURS_DELAY) diff --git a/addons/crm/models/crm_team_member.py b/addons/crm/models/crm_team_member.py index bf6f44ae37f..d25787d8d50 100644 --- a/addons/crm/models/crm_team_member.py +++ b/addons/crm/models/crm_team_member.py @@ -154,7 +154,7 @@ class TeamMember(models.Model): leads_done_ids = set() counter = 0 # auto-commit except in testing mode - auto_commit = not getattr(threading.currentThread(), 'testing', False) + auto_commit = not getattr(threading.current_thread(), 'testing', False) commit_bundle_size = int(self.env['ir.config_parameter'].sudo().get_param('crm.assignment.commit.bundle', 100)) while population: counter += 1 diff --git a/addons/event/models/event_mail.py b/addons/event/models/event_mail.py index 2272f030c33..78e2c736942 100644 --- a/addons/event/models/event_mail.py +++ b/addons/event/models/event_mail.py @@ -242,7 +242,7 @@ You receive this email because you are: self.invalidate_cache() self._warn_template_error(scheduler, e) else: - if autocommit and not getattr(threading.currentThread(), 'testing', False): + if autocommit and not getattr(threading.current_thread(), 'testing', False): self.env.cr.commit() return True diff --git a/addons/hr/models/res_config_settings.py b/addons/hr/models/res_config_settings.py index 457188e9bb8..5308b036a14 100644 --- a/addons/hr/models/res_config_settings.py +++ b/addons/hr/models/res_config_settings.py @@ -23,7 +23,7 @@ class ResConfigSettings(models.TransientModel): @api.constrains('module_hr_presence', 'hr_presence_control_email', 'hr_presence_control_ip') def _check_advanced_presence(self): - test_mode = self.env.registry.in_test_mode() or getattr(threading.currentThread(), 'testing', False) + test_mode = self.env.registry.in_test_mode() or getattr(threading.current_thread(), 'testing', False) if self.env.context.get('install_mode', False) or test_mode: return diff --git a/addons/mail/models/mail_mail.py b/addons/mail/models/mail_mail.py index 87defd3147b..9d01695ac35 100644 --- a/addons/mail/models/mail_mail.py +++ b/addons/mail/models/mail_mail.py @@ -197,7 +197,7 @@ class MailMail(models.Model): res = None try: # auto-commit except in testing mode - auto_commit = not getattr(threading.currentThread(), 'testing', False) + auto_commit = not getattr(threading.current_thread(), 'testing', False) res = self.browse(ids).send(auto_commit=auto_commit) except Exception: _logger.exception("Failed processing mail queue") diff --git a/addons/mail/models/mail_thread.py b/addons/mail/models/mail_thread.py index 44298548b4d..cd8a21fdd06 100644 --- a/addons/mail/models/mail_thread.py +++ b/addons/mail/models/mail_thread.py @@ -2480,7 +2480,7 @@ class MailThread(models.AbstractModel): # 2. do not send emails immediately if the registry is not loaded, # to prevent sending email during a simple update of the database # using the command-line. - test_mode = getattr(threading.currentThread(), 'testing', False) + test_mode = getattr(threading.current_thread(), 'testing', False) if force_send and len(emails) < recipients_max and (not self.pool._init or test_mode): # unless asked specifically, send emails after the transaction to # avoid side effects due to emails being sent while the transaction fails diff --git a/addons/mass_mailing/models/mailing.py b/addons/mass_mailing/models/mailing.py index f52a2c75581..5cad182f4a3 100644 --- a/addons/mass_mailing/models/mailing.py +++ b/addons/mass_mailing/models/mailing.py @@ -978,7 +978,7 @@ class MassMailing(models.Model): extra_context = mailing._get_mass_mailing_context() composer = composer.with_context(active_ids=res_ids, **extra_context) # auto-commit except in testing mode - auto_commit = not getattr(threading.currentThread(), 'testing', False) + auto_commit = not getattr(threading.current_thread(), 'testing', False) composer._action_send_mail(auto_commit=auto_commit) mailing.write({ 'state': 'done', diff --git a/addons/partner_autocomplete/models/res_company.py b/addons/partner_autocomplete/models/res_company.py index 86c6e0c9bf7..ecaec41dda5 100644 --- a/addons/partner_autocomplete/models/res_company.py +++ b/addons/partner_autocomplete/models/res_company.py @@ -27,7 +27,7 @@ class ResCompany(models.Model): @api.model_create_multi def create(self, vals_list): res = super().create(vals_list) - if not getattr(threading.currentThread(), 'testing', False): + if not getattr(threading.current_thread(), 'testing', False): res.iap_enrich_auto() return res diff --git a/addons/sale_timesheet/models/product.py b/addons/sale_timesheet/models/product.py index ce0eff95383..3f9d5fc8c91 100644 --- a/addons/sale_timesheet/models/product.py +++ b/addons/sale_timesheet/models/product.py @@ -104,7 +104,7 @@ class ProductTemplate(models.Model): def write(self, vals): # timesheet product can't be archived - test_mode = getattr(threading.currentThread(), 'testing', False) or self.env.registry.in_test_mode() + test_mode = getattr(threading.current_thread(), 'testing', False) or self.env.registry.in_test_mode() if not test_mode and 'active' in vals and not vals['active']: time_product = self.env.ref('sale_timesheet.time_product') if time_product.product_tmpl_id in self: @@ -138,7 +138,7 @@ class ProductProduct(models.Model): def write(self, vals): # timesheet product can't be archived - test_mode = getattr(threading.currentThread(), 'testing', False) or self.env.registry.in_test_mode() + test_mode = getattr(threading.current_thread(), 'testing', False) or self.env.registry.in_test_mode() if not test_mode and 'active' in vals and not vals['active']: time_product = self.env.ref('sale_timesheet.time_product') if time_product in self: diff --git a/addons/sms/models/sms_sms.py b/addons/sms/models/sms_sms.py index 9dd9d0dbacd..ec2b58e0c37 100644 --- a/addons/sms/models/sms_sms.py +++ b/addons/sms/models/sms_sms.py @@ -96,7 +96,7 @@ class SmsSms(models.Model): for batch_ids in self._split_batch(): self.browse(batch_ids)._send(unlink_failed=unlink_failed, unlink_sent=unlink_sent, raise_exception=raise_exception) # auto-commit if asked except in testing mode - if auto_commit is True and not getattr(threading.currentThread(), 'testing', False): + if auto_commit is True and not getattr(threading.current_thread(), 'testing', False): self._cr.commit() def resend_failed(self): @@ -145,7 +145,7 @@ class SmsSms(models.Model): res = None try: # auto-commit except in testing mode - auto_commit = not getattr(threading.currentThread(), 'testing', False) + auto_commit = not getattr(threading.current_thread(), 'testing', False) res = self.browse(ids).send(unlink_failed=False, unlink_sent=True, auto_commit=auto_commit, raise_exception=False) except Exception: _logger.exception("Failed processing SMS queue") diff --git a/addons/stock_sms/models/stock_picking.py b/addons/stock_sms/models/stock_picking.py index 846af19271f..554a160fa56 100644 --- a/addons/stock_sms/models/stock_picking.py +++ b/addons/stock_sms/models/stock_picking.py @@ -23,7 +23,7 @@ class Picking(models.Model): is_delivery = picking.company_id.stock_move_sms_validation \ and picking.picking_type_id.code == 'outgoing' \ and (picking.partner_id.mobile or picking.partner_id.phone) - if is_delivery and not getattr(threading.currentThread(), 'testing', False) \ + if is_delivery and not getattr(threading.current_thread(), 'testing', False) \ and not self.env.registry.in_test_mode() \ and not picking.company_id.has_received_warning_stock_sms \ and picking.company_id.stock_move_sms_validation: @@ -52,7 +52,7 @@ class Picking(models.Model): def _send_confirmation_email(self): super(Picking, self)._send_confirmation_email() - if not self.env.context.get('skip_sms') and not getattr(threading.currentThread(), 'testing', False) and not self.env.registry.in_test_mode(): + if not self.env.context.get('skip_sms') and not getattr(threading.current_thread(), 'testing', False) and not self.env.registry.in_test_mode(): pickings = self.filtered(lambda p: p.company_id.stock_move_sms_validation and p.picking_type_id.code == 'outgoing' and (p.partner_id.mobile or p.partner_id.phone)) for picking in pickings: # Sudo as the user has not always the right to read this sms template. diff --git a/addons/website/models/theme_models.py b/addons/website/models/theme_models.py index f03921c3641..21bd5011c20 100644 --- a/addons/website/models/theme_models.py +++ b/addons/website/models/theme_models.py @@ -346,7 +346,7 @@ class IrUiView(models.Model): # During a theme module update, theme views' copies receiving an arch # update should not be considered as `arch_updated`, as this is not a # user made change. - test_mode = getattr(threading.currentThread(), 'testing', False) + test_mode = getattr(threading.current_thread(), 'testing', False) if not (test_mode or self.pool._init): return super().write(vals) no_arch_updated_views = other_views = self.env['ir.ui.view'] diff --git a/odoo/__init__.py b/odoo/__init__.py index 8038761aecc..1b2bcf8fb8b 100644 --- a/odoo/__init__.py +++ b/odoo/__init__.py @@ -100,7 +100,7 @@ def registry(database_name=None): """ if database_name is None: import threading - database_name = threading.currentThread().dbname + database_name = threading.current_thread().dbname return modules.registry.Registry(database_name) #---------------------------------------------------------- diff --git a/odoo/addons/base/models/ir_mail_server.py b/odoo/addons/base/models/ir_mail_server.py index 5fe2af31701..897c5fb88d5 100644 --- a/odoo/addons/base/models/ir_mail_server.py +++ b/odoo/addons/base/models/ir_mail_server.py @@ -711,7 +711,7 @@ class IrMailServer(models.Model): Can be overridden in tests after mocking the SMTP lib to test in depth the outgoing mail server. """ - return getattr(threading.currentThread(), 'testing', False) or self.env.registry.in_test_mode() + return getattr(threading.current_thread(), 'testing', False) or self.env.registry.in_test_mode() def _neutralize(self): super()._neutralize() diff --git a/odoo/addons/base/models/ir_module.py b/odoo/addons/base/models/ir_module.py index 0e4a5e1cf75..8bdd01e9565 100644 --- a/odoo/addons/base/models/ir_module.py +++ b/odoo/addons/base/models/ir_module.py @@ -578,7 +578,7 @@ class Module(models.Model): } def _button_immediate_function(self, function): - if getattr(threading.currentThread(), 'testing', False): + if getattr(threading.current_thread(), 'testing', False): raise RuntimeError( "Module operations inside tests are not transactional and thus forbidden.\n" "If you really need to perform module operations to test a specific behavior, it " diff --git a/odoo/addons/base/tests/test_mail.py b/odoo/addons/base/tests/test_mail.py index a2defe8668a..5cd017f75ee 100644 --- a/odoo/addons/base/tests/test_mail.py +++ b/odoo/addons/base/tests/test_mail.py @@ -512,6 +512,6 @@ class TestEmailMessage(TransactionCase): msg['References'] = '<345227342212345.1596730777.324691772483620-example-30453-other.reference@test-123.example.com>' smtp = FakeSMTP() - self.patch(threading.currentThread(), 'testing', False) + self.patch(threading.current_thread(), 'testing', False) self.env['ir.mail_server'].send_email(msg, smtp_session=smtp) self.assertTrue(smtp.email_sent) diff --git a/odoo/modules/loading.py b/odoo/modules/loading.py index 5d18e5347fc..3a9da98231e 100644 --- a/odoo/modules/loading.py +++ b/odoo/modules/loading.py @@ -62,7 +62,7 @@ def load_data(cr, idref, mode, kind, package): filename = None try: if kind in ('demo', 'test'): - threading.currentThread().testing = True + threading.current_thread().testing = True for filename in _get_files_of_kind(kind): _logger.info("loading %s/%s", package.name, filename) noupdate = False @@ -71,7 +71,7 @@ def load_data(cr, idref, mode, kind, package): tools.convert_file(cr, package.name, filename, idref, mode, noupdate, kind) finally: if kind in ('demo', 'test'): - threading.currentThread().testing = False + threading.current_thread().testing = False return bool(filename) diff --git a/odoo/service/server.py b/odoo/service/server.py index b7e3fd6f1ba..e27666102d3 100644 --- a/odoo/service/server.py +++ b/odoo/service/server.py @@ -123,7 +123,7 @@ class RequestHandler(werkzeug.serving.WSGIRequestHandler): self.timeout = 5 # flag the current thread as handling a http request super(RequestHandler, self).setup() - me = threading.currentThread() + me = threading.current_thread() me.name = 'odoo.service.http.request.%s' % (me.ident,) @@ -350,7 +350,7 @@ class CommonServer(object): class ThreadedServer(CommonServer): def __init__(self, app): super(ThreadedServer, self).__init__(app) - self.main_thread_id = threading.currentThread().ident + self.main_thread_id = threading.current_thread().ident # Variable keeping track of the number of calls to the signal handler defined # below. This variable is monitored by ``quit_on_signals()``. self.quit_signals_received = 0 @@ -385,7 +385,7 @@ class ThreadedServer(CommonServer): memory = memory_info(psutil.Process(os.getpid())) if config['limit_memory_soft'] and memory > config['limit_memory_soft']: _logger.warning('Server memory limit (%s) reached.', memory) - self.limits_reached_threads.add(threading.currentThread()) + self.limits_reached_threads.add(threading.current_thread()) for thread in threading.enumerate(): if not thread.daemon or getattr(thread, 'type', None) == 'cron': @@ -448,7 +448,7 @@ class ThreadedServer(CommonServer): _logger.debug('cron%d polling for jobs', number) for db_name, registry in registries.d.items(): if registry.ready: - thread = threading.currentThread() + thread = threading.current_thread() thread.start_time = time.time() try: ir_cron._process_jobs(db_name) @@ -525,7 +525,7 @@ class ThreadedServer(CommonServer): # Manually join() all threads before calling sys.exit() to allow a second signal # to trigger _force_quit() in case some non-daemon threads won't exit cleanly. # threading.Thread.join() should not mask signals (at least in python 2.5). - me = threading.currentThread() + me = threading.current_thread() _logger.debug('current thread: %r', me) for thread in threading.enumerate(): _logger.debug('process %r (%r)', thread, thread.isDaemon()) @@ -1217,7 +1217,7 @@ def _reexec(updated_modules=None): def load_test_file_py(registry, test_file): from odoo.tests.common import OdooSuite - threading.currentThread().testing = True + threading.current_thread().testing = True try: test_path, _ = os.path.splitext(os.path.abspath(test_file)) for mod in [m for m in get_modules() if '%s%s%s' % (os.path.sep, m, os.path.sep) in test_file]: @@ -1233,7 +1233,7 @@ def load_test_file_py(registry, test_file): _logger.error('%s: at least one error occurred in a test', test_file) return finally: - threading.currentThread().testing = False + threading.current_thread().testing = False def preload_registries(dbnames): """ Preload a registries, possibly run a test file.""" diff --git a/odoo/tests/loader.py b/odoo/tests/loader.py index 7c57be1d93e..9e9117c8833 100644 --- a/odoo/tests/loader.py +++ b/odoo/tests/loader.py @@ -72,12 +72,12 @@ def run_suite(suite, module_name=None): # avoid dependency hell from ..modules import module module.current_test = module_name - threading.currentThread().testing = True + threading.current_thread().testing = True results = OdooTestResult() suite(results) - threading.currentThread().testing = False + threading.current_thread().testing = False module.current_test = None return results diff --git a/odoo/tools/cache.py b/odoo/tools/cache.py index dd37b0cf60f..662cd0d0653 100644 --- a/odoo/tools/cache.py +++ b/odoo/tools/cache.py @@ -201,7 +201,7 @@ def log_ormcache_stats(sig=None, frame=None): from odoo.modules.registry import Registry import threading - me = threading.currentThread() + me = threading.current_thread() me_dbname = getattr(me, 'dbname', 'n/a') for dbname, reg in sorted(Registry.registries.d.items()): diff --git a/odoo/tools/translate.py b/odoo/tools/translate.py index ca11767dc62..fa3f8d5bd62 100644 --- a/odoo/tools/translate.py +++ b/odoo/tools/translate.py @@ -361,7 +361,7 @@ class GettextAlias(object): def _get_db(self): # find current DB based on thread/worker db name (see netsvc) - db_name = getattr(threading.currentThread(), 'dbname', None) + db_name = getattr(threading.current_thread(), 'dbname', None) if db_name: return odoo.sql_db.db_connect(db_name) From 22448d76da049cfecdf9b90507d5194285c10bb8 Mon Sep 17 00:00:00 2001 From: Christophe Monniez Date: Thu, 14 Apr 2022 12:48:37 +0000 Subject: [PATCH 03/13] [FIX] requirements: adapt for ubuntu Jammy Part-of: odoo/odoo#91927 --- requirements.txt | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/requirements.txt b/requirements.txt index 46bd76127c7..29b0285c151 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,16 +6,19 @@ ebaysdk==2.1.5 freezegun==0.3.11; python_version < '3.8' freezegun==0.3.15; python_version >= '3.8' gevent==1.5.0 ; python_version == '3.7' -gevent==20.9.0 ; python_version >= '3.8' +gevent==20.9.0 ; python_version > '3.7' and python_version <= '3.9' +gevent==21.8.0 ; python_version > '3.9' # (Jammy) greenlet==0.4.15 ; python_version == '3.7' -greenlet==0.4.17 ; python_version > '3.7' +greenlet==0.4.17 ; python_version > '3.7' and python_version <= '3.9' +greenlet==1.1.2 ; python_version > '3.9' # (Jammy) idna==2.8 Jinja2==2.11.3 # min version = 2.10.1 (Focal - with security backports) libsass==0.18.0 lxml==4.6.5 # min version = 4.5.0 (Focal - with security backports) MarkupSafe==1.1.0 num2words==0.5.6 -ofxparse==0.19 +ofxparse==0.19; python_version <= '3.9' +ofxparse==0.21; python_version > '3.9' # (Jammy) passlib==1.7.3 # min version = 1.7.2 (Focal with security backports) Pillow==9.0.1 # min version = 7.0.0 (Focal with security backports) polib==1.1.0 @@ -37,7 +40,8 @@ reportlab==3.5.59 # version < 3.5.54 are not compatible with Pillow 8.1.2 and 3. requests==2.25.1 # versions < 2.25 aren't compatible w/ urllib3 1.26. Bullseye = 2.25.1. min version = 2.22.0 (Focal) urllib3==1.26.5 # indirect / min version = 1.25.8 (Focal with security backports) vobject==0.9.6.1 -Werkzeug==0.16.1 +Werkzeug==0.16.1 ; python_version <= '3.9' +Werkzeug==2.0.2 ; python_version > '3.9' # (Jammy) xlrd==1.1.0; python_version < '3.8' xlrd==1.2.0; python_version >= '3.8' XlsxWriter==1.1.2 From f629cf5feee3e77ed6b9316cc7fbd217420d7a9e Mon Sep 17 00:00:00 2001 From: Christophe Monniez Date: Thu, 14 Apr 2022 08:08:06 +0000 Subject: [PATCH 04/13] [FIX] safe_eval: add new py 3.10 GEN_START opcode Part-of: odoo/odoo#91927 --- odoo/tools/safe_eval.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/odoo/tools/safe_eval.py b/odoo/tools/safe_eval.py index 59f3617d359..02c0b31423b 100644 --- a/odoo/tools/safe_eval.py +++ b/odoo/tools/safe_eval.py @@ -86,6 +86,8 @@ _EXPR_OPCODES = _CONST_OPCODES.union(to_opcodes([ # specialised comparisons 'IS_OP', 'CONTAINS_OP', 'DICT_MERGE', 'DICT_UPDATE', + # Basically used in any "generator literal" + 'GEN_START', # added in 3.10 but already removed from 3.11. ])) - _BLACKLIST _SAFE_OPCODES = _EXPR_OPCODES.union(to_opcodes([ From 27a7d4822f103c95874fefc5ca1fd17621bdd621 Mon Sep 17 00:00:00 2001 From: Christophe Monniez Date: Wed, 13 Apr 2022 12:32:28 +0000 Subject: [PATCH 05/13] [FIX] server: replace deprecated daemon getter/setter Since Python 3.5 the isDaemon and setDaemon methods can be replaced by a property. As it's now deprecated in Python 3.10, the Time has come. [0] https://docs.python.org/3.5/library/threading.html#threading.Thread.setDaemon Part-of: odoo/odoo#91927 --- odoo/service/server.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/odoo/service/server.py b/odoo/service/server.py index e27666102d3..e93875e4f28 100644 --- a/odoo/service/server.py +++ b/odoo/service/server.py @@ -290,7 +290,7 @@ class FSWatcherInotify(FSWatcherBase): def start(self): self.started = True self.thread = threading.Thread(target=self.run, name="odoo.service.autoreload.watcher") - self.thread.setDaemon(True) + self.thread.daemon = True self.thread.start() def stop(self): @@ -472,7 +472,7 @@ class ThreadedServer(CommonServer): def target(): self.cron_thread(i) t = threading.Thread(target=target, name="odoo.service.cron.cron%d" % i) - t.setDaemon(True) + t.daemon = True t.type = 'cron' t.start() _logger.debug("cron%d started!" % i) @@ -483,7 +483,7 @@ class ThreadedServer(CommonServer): def http_spawn(self): t = threading.Thread(target=self.http_thread, name="odoo.service.httpd") - t.setDaemon(True) + t.daemon = True t.start() def start(self, stop=False): @@ -528,8 +528,8 @@ class ThreadedServer(CommonServer): me = threading.current_thread() _logger.debug('current thread: %r', me) for thread in threading.enumerate(): - _logger.debug('process %r (%r)', thread, thread.isDaemon()) - if (thread != me and not thread.isDaemon() and thread.ident != self.main_thread_id and + _logger.debug('process %r (%r)', thread, thread.daemon) + if (thread != me and not thread.daemon and thread.ident != self.main_thread_id and thread not in self.limits_reached_threads): while thread.is_alive() and (time.time() - stop_time) < 1: # We wait for requests to finish, up to 1 second. From 082a5a1a9868a10612f7c9cdb946e8a50d51154f Mon Sep 17 00:00:00 2001 From: Christophe Monniez Date: Wed, 13 Apr 2022 11:26:23 +0000 Subject: [PATCH 06/13] [FIX] report: replace deprecated LooseVersion method With this commit, the LooseVersion function comes from distutils which is deprecated [0] in Python 3.10 is replaced by parse_version. [0]: https://peps.python.org/pep-0632/ Part-of: odoo/odoo#91927 --- odoo/addons/base/models/ir_actions_report.py | 7 +++---- odoo/addons/test_lint/tests/test_pylint.py | 7 +++---- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/odoo/addons/base/models/ir_actions_report.py b/odoo/addons/base/models/ir_actions_report.py index 529a774a3df..d826f332dde 100644 --- a/odoo/addons/base/models/ir_actions_report.py +++ b/odoo/addons/base/models/ir_actions_report.py @@ -6,7 +6,7 @@ from odoo import api, fields, models, tools, SUPERUSER_ID, _ from odoo.exceptions import UserError, AccessError from odoo.tools.safe_eval import safe_eval, time from odoo.tools.misc import find_in_path -from odoo.tools import config, is_html_empty +from odoo.tools import config, is_html_empty, parse_version from odoo.http import request from odoo.osv.expression import NEGATIVE_TERM_OPERATORS, FALSE_DOMAIN @@ -21,7 +21,6 @@ import json from lxml import etree from contextlib import closing -from distutils.version import LooseVersion from reportlab.graphics.barcode import createBarcodeDrawing from PyPDF2 import PdfFileWriter, PdfFileReader, utils from collections import OrderedDict @@ -69,12 +68,12 @@ else: match = re.search(b'([0-9.]+)', out) if match: version = match.group(0).decode('ascii') - if LooseVersion(version) < LooseVersion('0.12.0'): + if parse_version(version) < parse_version('0.12.0'): _logger.info('Upgrade Wkhtmltopdf to (at least) 0.12.0') wkhtmltopdf_state = 'upgrade' else: wkhtmltopdf_state = 'ok' - if LooseVersion(version) >= LooseVersion('0.12.2'): + if parse_version(version) >= parse_version('0.12.2'): wkhtmltopdf_dpi_zoom_ratio = True if config['workers'] == 1: diff --git a/odoo/addons/test_lint/tests/test_pylint.py b/odoo/addons/test_lint/tests/test_pylint.py index a5b11305ee6..b41bf6578ae 100644 --- a/odoo/addons/test_lint/tests/test_pylint.py +++ b/odoo/addons/test_lint/tests/test_pylint.py @@ -7,7 +7,6 @@ try: except ImportError: pylint = None import subprocess -from distutils.version import LooseVersion import os from os.path import join import sys @@ -53,10 +52,10 @@ class TestPyLint(TransactionCase): def test_pylint(self): if pylint is None: self._skip_test('please install pylint') - required_pylint_version = LooseVersion('1.6.4') + required_pylint_version = tools.parse_version('1.6.4') if sys.version_info >= (3, 6): - required_pylint_version = LooseVersion('1.7.0') - if LooseVersion(getattr(pylint, '__version__', '0.0.1')) < required_pylint_version: + required_pylint_version = tools.parse_version('1.7.0') + if tools.parse_version(getattr(pylint, '__version__', '0.0.1')) < required_pylint_version: self._skip_test('please upgrade pylint to >= %s' % required_pylint_version) paths = [tools.config['root_path']] From ba1724732c23ae3a79182d525fc07633894244ef Mon Sep 17 00:00:00 2001 From: Christophe Monniez Date: Wed, 13 Apr 2022 09:59:41 +0000 Subject: [PATCH 07/13] [FIX] core: ignore invalid escape sequence in 3.10 Since Python 3.10, the invalid escape sequence warning use quotes to display the invalid sequence. Because of that, the filter does not catch it anymore. With this commit, they are caught in all supported versions. Part-of: odoo/odoo#91927 --- odoo/netsvc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/odoo/netsvc.py b/odoo/netsvc.py index c9b4cd36818..dc54d336d70 100644 --- a/odoo/netsvc.py +++ b/odoo/netsvc.py @@ -128,7 +128,7 @@ def init_logger(): warnings.simplefilter('default', category=DeprecationWarning) # ignore deprecation warnings from invalid escape (there's a ton and it's # pretty likely a super low-value signal) - warnings.filterwarnings('ignore', r'^invalid escape sequence \\.', category=DeprecationWarning) + warnings.filterwarnings('ignore', r'^invalid escape sequence \'?\\.', category=DeprecationWarning) # recordsets are both sequence and set so trigger warning despite no issue warnings.filterwarnings('ignore', r'^Sampling from a set', category=DeprecationWarning, module='odoo') # ignore a bunch of warnings we can't really fix ourselves From e2ff2fd29bba2c124dcc5f4012ddcc7d23e20f80 Mon Sep 17 00:00:00 2001 From: Christophe Monniez Date: Mon, 25 Apr 2022 09:06:55 +0000 Subject: [PATCH 08/13] [FIX] link_tracker, pos_mercury: use unescape from html As werkzeug.utils.unescape is deprecated in werkzeug 2.0 and the markupsafe version of unescape just uses the built-in html.unescape [0], we can use directly the same built-in method. [0] pallets/markupsafe@c35603a9037d3dd679732089ecd1084abc6e8cb7 Part-of: odoo/odoo#91927 --- addons/link_tracker/models/mail_render_mixin.py | 7 ++++--- addons/pos_mercury/models/pos_mercury_transaction.py | 5 +++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/addons/link_tracker/models/mail_render_mixin.py b/addons/link_tracker/models/mail_render_mixin.py index 2c7fa9c5e09..f7ff2d4548b 100644 --- a/addons/link_tracker/models/mail_render_mixin.py +++ b/addons/link_tracker/models/mail_render_mixin.py @@ -4,7 +4,8 @@ import re import markupsafe -from werkzeug import urls, utils +from html import unescape +from werkzeug import urls from odoo import api, models, tools @@ -45,7 +46,7 @@ class MailRenderMixin(models.AbstractModel): continue label = (match[3] or '').strip() - create_vals = dict(link_tracker_vals, url=utils.unescape(long_url), label=utils.unescape(label)) + create_vals = dict(link_tracker_vals, url=unescape(long_url), label=unescape(label)) link = self.env['link.tracker'].search_or_create(create_vals) if link.short_url: # `str` manipulation required to support replacing "&" characters, common in urls @@ -75,7 +76,7 @@ class MailRenderMixin(models.AbstractModel): if blacklist and any(item in parsed.path for item in blacklist): continue - create_vals = dict(link_tracker_vals, url=utils.unescape(original_url)) + create_vals = dict(link_tracker_vals, url=unescape(original_url)) link = self.env['link.tracker'].search_or_create(create_vals) if link.short_url: # Ensures we only replace the same link and not a subpart of a longer one, multiple times if applicable diff --git a/addons/pos_mercury/models/pos_mercury_transaction.py b/addons/pos_mercury/models/pos_mercury_transaction.py index 7ce0811d714..bb1ca421c0b 100644 --- a/addons/pos_mercury/models/pos_mercury_transaction.py +++ b/addons/pos_mercury/models/pos_mercury_transaction.py @@ -4,7 +4,8 @@ from datetime import date, timedelta import requests -import werkzeug + +from html import unescape from odoo import models, api, service from odoo.tools.translate import _ @@ -69,7 +70,7 @@ class MercuryTransaction(models.Model): try: r = requests.post(url, data=xml_transaction, headers=headers, timeout=65) r.raise_for_status() - response = werkzeug.utils.unescape(r.content.decode()) + response = unescape(r.content.decode()) except Exception: response = "timeout" From 1824f32f58dbfd450e868208708a43382da65431 Mon Sep 17 00:00:00 2001 From: Xavier-Do Date: Mon, 25 Apr 2022 09:26:57 +0000 Subject: [PATCH 09/13] [FIX] http: use vendored user agent parser Part-of: odoo/odoo#91927 --- odoo/http.py | 2 + odoo/tools/_vendor/useragents.py | 204 +++++++++++++++++++++++++++++++ 2 files changed, 206 insertions(+) create mode 100644 odoo/tools/_vendor/useragents.py diff --git a/odoo/http.py b/odoo/http.py index 12e13cb8873..98fdbe1a2b9 100644 --- a/odoo/http.py +++ b/odoo/http.py @@ -160,6 +160,7 @@ from .tools.geoipresolver import GeoIPResolver from .tools.func import filter_kwargs, lazy_property from .tools.mimetypes import guess_mimetype from .tools._vendor import sessions +from .tools._vendor.useragents import UserAgent _logger = logging.getLogger(__name__) @@ -1752,6 +1753,7 @@ class Application: return response(environ, start_response) httprequest = werkzeug.wrappers.Request(environ) + httprequest.user_agent_class = UserAgent # use vendored userAgent since it will be removed in 2.1 httprequest.parameter_storage_class = ( werkzeug.datastructures.ImmutableOrderedMultiDict) request = Request(httprequest) diff --git a/odoo/tools/_vendor/useragents.py b/odoo/tools/_vendor/useragents.py new file mode 100644 index 00000000000..7627ccc973b --- /dev/null +++ b/odoo/tools/_vendor/useragents.py @@ -0,0 +1,204 @@ +# -*- coding: utf-8 -*- +""" + werkzeug.useragents + ~~~~~~~~~~~~~~~~~~~ + + This module provides a helper to inspect user agent strings. This module + is far from complete but should work for most of the currently available + browsers. + + + :copyright: 2007 Pallets + :license: BSD-3-Clause + + This package was vendored in odoo in order to prevent errors with werkzeug 2.1 +""" +import re + + +class UserAgentParser(object): + """A simple user agent parser. Used by the `UserAgent`.""" + + platforms = ( + ("cros", "chromeos"), + ("iphone|ios", "iphone"), + ("ipad", "ipad"), + (r"darwin|mac|os\s*x", "macos"), + ("win", "windows"), + (r"android", "android"), + ("netbsd", "netbsd"), + ("openbsd", "openbsd"), + ("freebsd", "freebsd"), + ("dragonfly", "dragonflybsd"), + ("(sun|i86)os", "solaris"), + (r"x11|lin(\b|ux)?", "linux"), + (r"nintendo\s+wii", "wii"), + ("irix", "irix"), + ("hp-?ux", "hpux"), + ("aix", "aix"), + ("sco|unix_sv", "sco"), + ("bsd", "bsd"), + ("amiga", "amiga"), + ("blackberry|playbook", "blackberry"), + ("symbian", "symbian"), + ) + browsers = ( + ("googlebot", "google"), + ("msnbot", "msn"), + ("yahoo", "yahoo"), + ("ask jeeves", "ask"), + (r"aol|america\s+online\s+browser", "aol"), + ("opera", "opera"), + ("edge", "edge"), + ("chrome|crios", "chrome"), + ("seamonkey", "seamonkey"), + ("firefox|firebird|phoenix|iceweasel", "firefox"), + ("galeon", "galeon"), + ("safari|version", "safari"), + ("webkit", "webkit"), + ("camino", "camino"), + ("konqueror", "konqueror"), + ("k-meleon", "kmeleon"), + ("netscape", "netscape"), + (r"msie|microsoft\s+internet\s+explorer|trident/.+? rv:", "msie"), + ("lynx", "lynx"), + ("links", "links"), + ("Baiduspider", "baidu"), + ("bingbot", "bing"), + ("mozilla", "mozilla"), + ) + + _browser_version_re = r"(?:%s)[/\sa-z(]*(\d+[.\da-z]+)?" + _language_re = re.compile( + r"(?:;\s*|\s+)(\b\w{2}\b(?:-\b\w{2}\b)?)\s*;|" + r"(?:\(|\[|;)\s*(\b\w{2}\b(?:-\b\w{2}\b)?)\s*(?:\]|\)|;)" + ) + + def __init__(self): + self.platforms = [(b, re.compile(a, re.I)) for a, b in self.platforms] + self.browsers = [ + (b, re.compile(self._browser_version_re % a, re.I)) + for a, b in self.browsers + ] + + def __call__(self, user_agent): + for platform, regex in self.platforms: # noqa: B007 + match = regex.search(user_agent) + if match is not None: + break + else: + platform = None + for browser, regex in self.browsers: # noqa: B007 + match = regex.search(user_agent) + if match is not None: + version = match.group(1) + break + else: + browser = version = None + match = self._language_re.search(user_agent) + if match is not None: + language = match.group(1) or match.group(2) + else: + language = None + return platform, browser, version, language + + +class UserAgent(object): + """Represents a user agent. Pass it a WSGI environment or a user agent + string and you can inspect some of the details from the user agent + string via the attributes. The following attributes exist: + + .. attribute:: string + + the raw user agent string + + .. attribute:: platform + + the browser platform. The following platforms are currently + recognized: + + - `aix` + - `amiga` + - `android` + - `blackberry` + - `bsd` + - `chromeos` + - `dragonflybsd` + - `freebsd` + - `hpux` + - `ipad` + - `iphone` + - `irix` + - `linux` + - `macos` + - `netbsd` + - `openbsd` + - `sco` + - `solaris` + - `symbian` + - `wii` + - `windows` + + .. attribute:: browser + + the name of the browser. The following browsers are currently + recognized: + + - `aol` * + - `ask` * + - `baidu` * + - `bing` * + - `camino` + - `chrome` + - `edge` + - `firefox` + - `galeon` + - `google` * + - `kmeleon` + - `konqueror` + - `links` + - `lynx` + - `mozilla` + - `msie` + - `msn` + - `netscape` + - `opera` + - `safari` + - `seamonkey` + - `webkit` + - `yahoo` * + + (Browsers marked with a star (``*``) are crawlers.) + + .. attribute:: version + + the version of the browser + + .. attribute:: language + + the language of the browser + """ + + _parser = UserAgentParser() + + def __init__(self, environ_or_string): + if isinstance(environ_or_string, dict): + environ_or_string = environ_or_string.get("HTTP_USER_AGENT", "") + self.string = environ_or_string + self.platform, self.browser, self.version, self.language = self._parser( + environ_or_string + ) + + def to_header(self): + return self.string + + def __str__(self): + return self.string + + def __nonzero__(self): + return bool(self.browser) + + __bool__ = __nonzero__ + + def __repr__(self): + return "<%s %r/%s>" % (self.__class__.__name__, self.browser, self.version) From 482057043ff4b5319b943bed0d9a1f7b09ae3620 Mon Sep 17 00:00:00 2001 From: Xavier-Do Date: Mon, 25 Apr 2022 10:01:40 +0000 Subject: [PATCH 10/13] [FIX] http, web: use Response instead of BaseResponse Part-of: odoo/odoo#91927 --- odoo/http.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/odoo/http.py b/odoo/http.py index 98fdbe1a2b9..77132fda7bd 100644 --- a/odoo/http.py +++ b/odoo/http.py @@ -889,7 +889,7 @@ class Response(werkzeug.wrappers.Response): _logger.warning("%s returns an HTTPException instead of raising it.", fname) raise result - if isinstance(result, werkzeug.wrappers.BaseResponse): + if isinstance(result, werkzeug.wrappers.Response): response = cls.force_type(result) response.set_default() return response From 567721d136d947e6065f07e67a8f144f8dcfa48f Mon Sep 17 00:00:00 2001 From: Christophe Monniez Date: Mon, 25 Apr 2022 11:15:48 +0000 Subject: [PATCH 11/13] [FIX] populate: force integer for randint random.randrange needs integer values since python 3.10. As randint uses randrange, we cast the seconds into integer. Part-of: odoo/odoo#91927 --- odoo/tools/populate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/odoo/tools/populate.py b/odoo/tools/populate.py index 44e654fa0ee..7986a4827dc 100644 --- a/odoo/tools/populate.py +++ b/odoo/tools/populate.py @@ -177,5 +177,5 @@ def randdatetime(*, base_date=None, relative_before=None, relative_after=None, s seconds_after = relative_after and ((base_date + relative_after) - base_date).total_seconds() or 0 def get_rand_datetime(random=None, **kwargs): - return base_date + relativedelta(seconds=random.randint(seconds_before, seconds_after)) + return base_date + relativedelta(seconds=random.randint(int(seconds_before), int(seconds_after))) return compute(get_rand_datetime, seed=seed) From d2b7a601bac69d4890d8a5a99ff2ada24a712466 Mon Sep 17 00:00:00 2001 From: Christophe Monniez Date: Mon, 16 May 2022 10:00:59 +0000 Subject: [PATCH 12/13] [FIX] test_crm_full: bump query count This query count test fails deterministically on runbot when built with Ubuntu Jammy. After some investigations, it appears that this query count is very indeterministic and will be fixed in #91386. Until that, the expected count is bumped. Part-of: odoo/odoo#91927 --- addons/test_crm_full/tests/test_performance.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/test_crm_full/tests/test_performance.py b/addons/test_crm_full/tests/test_performance.py index 5d23d83864e..85527cd8f6c 100644 --- a/addons/test_crm_full/tests/test_performance.py +++ b/addons/test_crm_full/tests/test_performance.py @@ -42,7 +42,7 @@ class TestCrmPerformance(CrmPerformanceCase): country_be = self.env.ref('base.be') lang_be_id = self.env['res.lang']._lang_get_id('fr_BE') - with freeze_time(self.reference_now), self.assertQueryCount(user_sales_leads=243): # 235, sometimes +6 + with freeze_time(self.reference_now), self.assertQueryCount(user_sales_leads=244): # 235, sometimes +6 self.env.cr._now = self.reference_now # force create_date to check schedulers crm_values = [ {'country_id': country_be.id, From 8f52bdf9f4c031fce33403379d237812d5f6f162 Mon Sep 17 00:00:00 2001 From: Julien Castiaux Date: Thu, 19 May 2022 10:52:57 +0000 Subject: [PATCH 13/13] [FIX] test_http: unreliable tests Part-of: odoo/odoo#91927 --- odoo/addons/test_http/tests/test_http.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/odoo/addons/test_http/tests/test_http.py b/odoo/addons/test_http/tests/test_http.py index d916a6bc3ee..533c05d1cfc 100644 --- a/odoo/addons/test_http/tests/test_http.py +++ b/odoo/addons/test_http/tests/test_http.py @@ -1,5 +1,6 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. +import html import json from unittest.mock import patch from urllib.parse import urlparse @@ -380,7 +381,7 @@ class TestHttpModels(TestHttpBase): milky_way = self.env.ref('test_http.milky_way') res = self.url_open(f'/test_http/{milky_way.id}/9999') # unknown gate self.assertEqual(res.status_code, 400) - self.assertIn("The goa'uld destroyed the gate", res.text) + self.assertIn("The goa'uld destroyed the gate", html.unescape(res.text)) @tagged('post_install', '-at_install') @@ -510,6 +511,7 @@ class TestHttpEnsureDb(TestHttpBase): self.assertEqual(new_session.uid, None) # follow redirection + self.opener.cookies['session_id'] = new_session.sid res = self.multidb_url_open('/test_http/ensure_db') res.raise_for_status() self.assertEqual(res.status_code, 200)