Fix for Python V3.7

develop
Andreas Osim 2020-04-14 10:29:10 +02:00
parent ba7b0b26c9
commit 357c96c057
3 changed files with 244 additions and 79 deletions

View File

@ -9,9 +9,12 @@ import textwrap
import uuid import uuid
from datetime import datetime from datetime import datetime
from subprocess import Popen, PIPE from subprocess import Popen, PIPE
from collections import OrderedDict
from odoo import fields, tools from odoo import fields, tools
from odoo.tools.pycompat import string_types, to_text
from odoo.http import request from odoo.http import request
from odoo.modules.module import get_resource_path from odoo.modules.module import get_resource_path
from odoo.addons.base.ir.ir_qweb.qweb import escape
import psycopg2 import psycopg2
from odoo.tools import func, misc from odoo.tools import func, misc
@ -65,7 +68,6 @@ def rjsmin(script):
).strip() ).strip()
return result return result
class AssetError(Exception): class AssetError(Exception):
pass pass
@ -79,17 +81,16 @@ class AssetsBundle(object):
rx_preprocess_imports = re.compile("""(@import\s?['"]([^'"]+)['"](;?))""") rx_preprocess_imports = re.compile("""(@import\s?['"]([^'"]+)['"](;?))""")
rx_css_split = re.compile("\/\*\! ([a-f0-9-]+) \*\/") rx_css_split = re.compile("\/\*\! ([a-f0-9-]+) \*\/")
def __init__(self, name, files, remains, env=None): # remains attribute is depreciated and will remove after v11
def __init__(self, name, files, remains=None, env=None):
self.name = name self.name = name
self.env = request.env if env is None else env self.env = request.env if env is None else env
self.max_css_rules = self.env.context.get('max_css_rules', MAX_CSS_RULES) self.max_css_rules = self.env.context.get('max_css_rules', MAX_CSS_RULES)
self.javascripts = [] self.javascripts = []
self.stylesheets = [] self.stylesheets = []
self.css_errors = [] self.css_errors = []
self.remains = []
self._checksum = None self._checksum = None
self.files = files self.files = files
self.remains = remains
for f in files: for f in files:
if f['atype'] == 'text/sass': if f['atype'] == 'text/sass':
self.stylesheets.append(SassStylesheetAsset(self, url=f['url'], filename=f['filename'], inline=f['content'], media=f['media'])) self.stylesheets.append(SassStylesheetAsset(self, url=f['url'], filename=f['filename'], inline=f['content'], media=f['media']))
@ -100,10 +101,37 @@ class AssetsBundle(object):
elif f['atype'] == 'text/javascript': elif f['atype'] == 'text/javascript':
self.javascripts.append(JavascriptAsset(self, url=f['url'], filename=f['filename'], inline=f['content'])) self.javascripts.append(JavascriptAsset(self, url=f['url'], filename=f['filename'], inline=f['content']))
def to_html(self, sep=None, css=True, js=True, debug=False, async=False, url_for=(lambda url: url)): # depreciated and will remove after v11
def to_html(self, sep=None, css=True, js=True, debug=False, async_load=False, url_for=(lambda url: url), **kw):
if 'async' in kw:
_logger.warning("Using deprecated argument 'async' in to_html call, use 'async_load' instead.")
async_load = kw['async']
nodes = self.to_node(css=css, js=js, debug=debug, async_load=async_load)
if sep is None: if sep is None:
sep = u'\n ' sep = u'\n '
response = [] response = []
for tagName, attributes, content in nodes:
html = u"<%s " % tagName
for name, value in attributes.items():
if value or isinstance(value, string_types):
html += u' %s="%s"' % (name, escape(to_text(value)))
if content is None:
html += u'/>'
else:
html += u'>%s</%s>' % (escape(to_text(content)), tagName)
response.append(html)
return sep + sep.join(response)
def to_node(self, css=True, js=True, debug=False, async_load=False, **kw):
"""
:returns [(tagName, attributes, content)] if the tag is auto close
"""
if 'async' in kw:
_logger.warning("Using deprecated argument 'async' in to_node call, use 'async_load' instead.")
async_load = kw['async']
response = []
if debug == 'assets': if debug == 'assets':
if css and self.stylesheets: if css and self.stylesheets:
is_css_preprocessed, old_attachments = self.is_css_preprocessed() is_css_preprocessed, old_attachments = self.is_css_preprocessed()
@ -111,28 +139,37 @@ class AssetsBundle(object):
self.preprocess_css(debug=debug, old_attachments=old_attachments) self.preprocess_css(debug=debug, old_attachments=old_attachments)
if self.css_errors: if self.css_errors:
msg = '\n'.join(self.css_errors) msg = '\n'.join(self.css_errors)
response.append(JavascriptAsset(self, inline=self.dialog_message(msg)).to_html()) response.append(JavascriptAsset(self, inline=self.dialog_message(msg)).to_node())
response.append(StylesheetAsset(self, url="/web/static/lib/bootstrap/css/bootstrap.css").to_html()) response.append(StylesheetAsset(self, url="/web/static/lib/bootstrap/css/bootstrap.css").to_node())
if not self.css_errors: if not self.css_errors:
for style in self.stylesheets: for style in self.stylesheets:
response.append(style.to_html()) response.append(style.to_node())
if js: if js:
for jscript in self.javascripts: for jscript in self.javascripts:
response.append(jscript.to_html()) response.append(jscript.to_node())
else: else:
if css and self.stylesheets: if css and self.stylesheets:
css_attachments = self.css() or [] css_attachments = self.css() or []
for attachment in css_attachments: for attachment in css_attachments:
response.append(u'<link href="%s" rel="stylesheet"/>' % url_for(attachment.url)) attr = OrderedDict([
["type", "text/css"],
["rel", "stylesheet"],
["href", attachment.url],
])
response.append(("link", attr, None))
if self.css_errors: if self.css_errors:
msg = '\n'.join(self.css_errors) msg = '\n'.join(self.css_errors)
response.append(JavascriptAsset(self, inline=self.dialog_message(msg)).to_html()) response.append(JavascriptAsset(self, inline=self.dialog_message(msg)).to_node())
if js and self.javascripts: if js and self.javascripts:
response.append(u'<script %s type="text/javascript" src="%s"></script>' % (async and u'async="async"' or '', url_for(self.js().url))) attr = OrderedDict([
response.extend(self.remains) ["async", "async" if async_load else None],
["type", "text/javascript"],
["src", self.js().url],
])
response.append(("script", attr, None))
return sep + sep.join(response) return response
@func.lazy_property @func.lazy_property
def last_modified(self): def last_modified(self):
@ -152,17 +189,15 @@ class AssetsBundle(object):
Not really a full checksum. Not really a full checksum.
We compute a SHA1 on the rendered bundle + max linked files last_modified date We compute a SHA1 on the rendered bundle + max linked files last_modified date
""" """
check = u"%s%s%s" % (json.dumps(self.files, sort_keys=True), u",".join(self.remains), self.last_modified) check = u"%s%s" % (json.dumps(self.files, sort_keys=True), self.last_modified)
return hashlib.sha1(check.encode('utf-8')).hexdigest() return hashlib.sha1(check.encode('utf-8')).hexdigest()
def clean_attachments(self, type): def clean_attachments(self, type):
""" Takes care of deleting any outdated ir.attachment records associated to a bundle before """ Takes care of deleting any outdated ir.attachment records associated to a bundle before
saving a fresh one. saving a fresh one.
When `type` is css we need to check that we are deleting a different version (and not *any* When `type` is css we need to check that we are deleting a different version (and not *any*
version) because css may be paginated and, therefore, may produce multiple attachments for version) because css may be paginated and, therefore, may produce multiple attachments for
the same bundle's version. the same bundle's version.
When `type` is js we need to check that we are deleting a different version (and not *any* When `type` is js we need to check that we are deleting a different version (and not *any*
version) because, as one of the creates in `save_attachment` can trigger a rollback, the version) because, as one of the creates in `save_attachment` can trigger a rollback, the
call to `clean_attachments ` is made at the end of the method in order to avoid the rollback call to `clean_attachments ` is made at the end of the method in order to avoid the rollback
@ -278,22 +313,16 @@ class AssetsBundle(object):
(function (message) { (function (message) {
if (window.__assetsBundleErrorSeen) return; if (window.__assetsBundleErrorSeen) return;
window.__assetsBundleErrorSeen = true; window.__assetsBundleErrorSeen = true;
document.addEventListener("DOMContentLoaded", function () { document.addEventListener("DOMContentLoaded", function () {
var alertTimeout = setTimeout(alert.bind(window, message), 0); var alertTimeout = setTimeout(alert.bind(window, message), 0);
if (typeof odoo === "undefined") return; if (typeof odoo === "undefined") return;
odoo.define("AssetsBundle.ErrorMessage", function (require) { odoo.define("AssetsBundle.ErrorMessage", function (require) {
"use strict"; "use strict";
var base = require("web_editor.base"); var base = require("web_editor.base");
var core = require("web.core"); var core = require("web.core");
var Dialog = require("web.Dialog"); var Dialog = require("web.Dialog");
var _t = core._t; var _t = core._t;
clearTimeout(alertTimeout); clearTimeout(alertTimeout);
base.ready().then(function () { base.ready().then(function () {
new Dialog(null, { new Dialog(null, {
title: _t("Style error"), title: _t("Style error"),
@ -314,7 +343,7 @@ class AssetsBundle(object):
outdated = False outdated = False
assets = dict((asset.html_url, asset) for asset in self.stylesheets if isinstance(asset, atype)) assets = dict((asset.html_url, asset) for asset in self.stylesheets if isinstance(asset, atype))
if assets: if assets:
assets_domain = [('url', 'in', list(assets))] assets_domain = [('url', 'in', list(assets.keys()))]
attachments = self.env['ir.attachment'].sudo().search(assets_domain) attachments = self.env['ir.attachment'].sudo().search(assets_domain)
for attachment in attachments: for attachment in attachments:
asset = assets[attachment.url] asset = assets[attachment.url]
@ -474,7 +503,20 @@ class WebAsset(object):
except Exception: except Exception:
raise AssetNotFound("Could not find %s" % self.name) raise AssetNotFound("Could not find %s" % self.name)
# depreciated and will remove after v11
def to_html(self): def to_html(self):
tagName, attributes, content = self.to_node()
html = u"<%s " % tagName
for name, value in attributes.items():
if value or isinstance(value, string_types):
html += u' %s="%s"' % (name, escape(to_text(value)))
if content is None:
html += u'/>'
else:
html += u'>%s</%s>' % (escape(to_text(content)), tagName)
return html
def to_node(self):
raise NotImplementedError() raise NotImplementedError()
@func.lazy_property @func.lazy_property
@ -533,13 +575,19 @@ class JavascriptAsset(WebAsset):
try: try:
return super(JavascriptAsset, self)._fetch_content() return super(JavascriptAsset, self)._fetch_content()
except AssetError as e: except AssetError as e:
return "console.error(%s);" % json.dumps(str(e)) return u"console.error(%s);" % json.dumps(to_text(e))
def to_html(self): def to_node(self):
if self.url: if self.url:
return '<script type="text/javascript" src="%s"></script>' % (self.html_url) return ("script", OrderedDict([
["type", "text/javascript"],
["src", self.html_url],
]), None)
else: else:
return '<script type="text/javascript" charset="utf-8">%s</script>' % self.with_header() return ("script", OrderedDict([
["type", "text/javascript"],
["charset", "utf-8"],
]), self.with_header())
class StylesheetAsset(WebAsset): class StylesheetAsset(WebAsset):
@ -595,13 +643,21 @@ class StylesheetAsset(WebAsset):
content = re.sub(r' *([{}]) *', r'\1', content) content = re.sub(r' *([{}]) *', r'\1', content)
return self.with_header(content) return self.with_header(content)
def to_html(self): def to_node(self):
media = (' media="%s"' % misc.html_escape(self.media)) if self.media else ''
if self.url: if self.url:
href = self.html_url attr = OrderedDict([
return '<link rel="stylesheet" href="%s" type="text/css"%s/>' % (href, media) ["type", "text/css"],
["rel", "stylesheet"],
["href", self.html_url],
["media", escape(to_text(self.media)) if self.media else None]
])
return ("link", attr, None)
else: else:
return '<style type="text/css"%s>%s</style>' % (media, self.with_header()) attr = OrderedDict([
["type", "text/css"],
["media", escape(to_text(self.media)) if self.media else None]
])
return ("style", attr, self.with_header())
class PreprocessedCSS(StylesheetAsset): class PreprocessedCSS(StylesheetAsset):

View File

@ -37,9 +37,7 @@ class IrQWeb(models.AbstractModel, QWeb):
@api.model @api.model
def render(self, id_or_xml_id, values=None, **options): def render(self, id_or_xml_id, values=None, **options):
""" render(id_or_xml_id, values, **options) """ render(id_or_xml_id, values, **options)
Render the template specified by the given name. Render the template specified by the given name.
:param id_or_xml_id: name or etree (see get_template) :param id_or_xml_id: name or etree (see get_template)
:param dict values: template values to be used for rendering :param dict values: template values to be used for rendering
:param options: used to compile the template (the dict available for the rendering is frozen) :param options: used to compile the template (the dict available for the rendering is frozen)
@ -125,35 +123,122 @@ class IrQWeb(models.AbstractModel, QWeb):
if len(el): if len(el):
raise SyntaxError("t-call-assets cannot contain children nodes") raise SyntaxError("t-call-assets cannot contain children nodes")
# self._get_asset(xmlid, options, css=css, js=js, debug=values.get('debug'), async=async, values=values) # nodes = self._get_asset(xmlid, options, css=css, js=js, debug=values.get('debug'), async=async, values=values)
#
# for index, (tagName, t_attrs, content) in enumerate(nodes):
# if index:
# append('\n ')
# append('<')
# append(tagName)
#
# self._post_processing_att(tagName, t_attrs, options)
# for name, value in t_attrs.items():
# if value or isinstance(value, string_types)):
# append(u' ')
# append(name)
# append(u'="')
# append(escape(pycompat.to_text((value)))
# append(u'"')
#
# if not content and tagName in self._void_elements:
# append('/>')
# else:
# append('>')
# if content:
# append(content)
# append('</')
# append(tagName)
# append('>')
#
space = el.getprevious() is not None and el.getprevious().tail or el.getparent().text
sep = u'\n' + space.rsplit('\n').pop()
return [ return [
self._append(ast.Call( ast.Assign(
func=ast.Attribute( targets=[ast.Name(id='nodes', ctx=ast.Store())],
value=ast.Name(id='self', ctx=ast.Load()), value=ast.Call(
attr='_get_asset', func=ast.Attribute(
ctx=ast.Load() value=ast.Name(id='self', ctx=ast.Load()),
attr='_get_asset_nodes',
ctx=ast.Load()
),
args=[
ast.Str(el.get('t-call-assets')),
ast.Name(id='options', ctx=ast.Load()),
],
keywords=[
ast.keyword('css', self._get_attr_bool(el.get('t-css', True))),
ast.keyword('js', self._get_attr_bool(el.get('t-js', True))),
ast.keyword('debug', ast.Call(
func=ast.Attribute(
value=ast.Name(id='values', ctx=ast.Load()),
attr='get',
ctx=ast.Load()
),
args=[ast.Str('debug')],
keywords=[], starargs=None, kwargs=None
)),
ast.keyword('async', self._get_attr_bool(el.get('async', False))),
ast.keyword('values', ast.Name(id='values', ctx=ast.Load())),
],
starargs=None, kwargs=None
)
),
ast.For(
target=ast.Tuple(elts=[
ast.Name(id='index', ctx=ast.Store()),
ast.Tuple(elts=[
ast.Name(id='tagName', ctx=ast.Store()),
ast.Name(id='t_attrs', ctx=ast.Store()),
ast.Name(id='content', ctx=ast.Store())
], ctx=ast.Store())
], ctx=ast.Store()),
iter=ast.Call(
func=ast.Name(id='enumerate', ctx=ast.Load()),
args=[ast.Name(id='nodes', ctx=ast.Load())],
keywords=[],
starargs=None, kwargs=None
), ),
args=[ body=[
ast.Str(el.get('t-call-assets')), ast.If(
ast.Name(id='options', ctx=ast.Load()), test=ast.Name(id='index', ctx=ast.Load()),
], body=[self._append(ast.Str(sep))],
keywords=[ orelse=[]
ast.keyword('css', self._get_attr_bool(el.get('t-css', True))), ),
ast.keyword('js', self._get_attr_bool(el.get('t-js', True))), self._append(ast.Str(u'<')),
ast.keyword('debug', ast.Call( self._append(ast.Name(id='tagName', ctx=ast.Load())),
func=ast.Attribute( ] + self._append_attributes() + [
value=ast.Name(id='values', ctx=ast.Load()), ast.If(
attr='get', test=ast.BoolOp(
ctx=ast.Load() op=ast.And(),
values=[
ast.UnaryOp(ast.Not(), ast.Name(id='content', ctx=ast.Load()), lineno=0, col_offset=0),
ast.Compare(
left=ast.Name(id='tagName', ctx=ast.Load()),
ops=[ast.In()],
comparators=[ast.Attribute(
value=ast.Name(id='self', ctx=ast.Load()),
attr='_void_elements',
ctx=ast.Load()
)]
),
]
), ),
args=[ast.Str('debug')], body=[self._append(ast.Str(u'/>'))],
keywords=[], starargs=None, kwargs=None orelse=[
)), self._append(ast.Str(u'>')),
ast.keyword('async', self._get_attr_bool(el.get('async', False))), ast.If(
ast.keyword('values', ast.Name(id='values', ctx=ast.Load())), test=ast.Name(id='content', ctx=ast.Load()),
body=[self._append(ast.Name(id='content', ctx=ast.Load()))],
orelse=[]
),
self._append(ast.Str(u'</')),
self._append(ast.Name(id='tagName', ctx=ast.Load())),
self._append(ast.Str(u'>')),
]
)
], ],
starargs=None, kwargs=None orelse=[]
)) )
] ]
# for backward compatibility to remove after v10 # for backward compatibility to remove after v10
@ -185,16 +270,34 @@ class IrQWeb(models.AbstractModel, QWeb):
# method called by computing code # method called by computing code
def get_asset_bundle(self, xmlid, files, remains=None, env=None):
return AssetsBundle(xmlid, files, remains=remains, env=env)
# compatibility to remove after v11 - DEPRECATED
@tools.conditional(
'xml' not in tools.config['dev_mode'],
tools.ormcache_context('xmlid', 'options.get("lang", "en_US")', 'css', 'js', 'debug', 'kw.get("async")', 'async_load', keys=("website_id",)),
)
def _get_asset(self, xmlid, options, css=True, js=True, debug=False, async_load=False, values=None, **kw):
if 'async' in kw:
async_load = kw['async']
files, remains = self._get_asset_content(xmlid, options)
asset = self.get_asset_bundle(xmlid, files, remains, env=self.env)
return asset.to_html(css=css, js=js, debug=debug, async_load=async_load, url_for=(values or {}).get('url_for', lambda url: url))
@tools.conditional( @tools.conditional(
# in non-xml-debug mode we want assets to be cached forever, and the admin can force a cache clear # in non-xml-debug mode we want assets to be cached forever, and the admin can force a cache clear
# by restarting the server after updating the source code (or using the "Clear server cache" in debug tools) # by restarting the server after updating the source code (or using the "Clear server cache" in debug tools)
'xml' not in tools.config['dev_mode'], 'xml' not in tools.config['dev_mode'],
tools.ormcache_context('xmlid', 'options.get("lang", "en_US")', 'css', 'js', 'debug', 'async', keys=("website_id",)), tools.ormcache_context('xmlid', 'options.get("lang", "en_US")', 'css', 'js', 'debug', 'kw.get("async")', 'async_load', keys=("website_id",)),
) )
def _get_asset(self, xmlid, options, css=True, js=True, debug=False, async=False, values=None): def _get_asset_nodes(self, xmlid, options, css=True, js=True, debug=False, async_load=False, values=None, **kw):
if 'async' in kw:
async_load = kw['async']
files, remains = self._get_asset_content(xmlid, options) files, remains = self._get_asset_content(xmlid, options)
asset = AssetsBundle(xmlid, files, remains, env=self.env) asset = self.get_asset_bundle(xmlid, files, env=self.env)
return asset.to_html(css=css, js=js, debug=debug, async=async, url_for=(values or {}).get('url_for', lambda url: url)) remains = [node for node in remains if (css and node[0] == 'link') or (js and node[0] != 'link')]
return remains + asset.to_node(css=css, js=js, debug=debug, async_load=async_load)
@tools.ormcache_context('xmlid', 'options.get("lang", "en_US")', keys=("website_id",)) @tools.ormcache_context('xmlid', 'options.get("lang", "en_US")', keys=("website_id",))
def _get_asset_content(self, xmlid, options): def _get_asset_content(self, xmlid, options):
@ -205,6 +308,9 @@ class IrQWeb(models.AbstractModel, QWeb):
env = self.env(context=options) env = self.env(context=options)
def can_aggregate(url):
return not urls.url_parse(url).scheme and not urls.url_parse(url).netloc and not url.startswith('/web/content')
# TODO: This helper can be used by any template that wants to embedd the backend. # TODO: This helper can be used by any template that wants to embedd the backend.
# It is currently necessary because the ir.ui.view bundle inheritance does not # It is currently necessary because the ir.ui.view bundle inheritance does not
# match the module dependency graph. # match the module dependency graph.
@ -218,16 +324,13 @@ class IrQWeb(models.AbstractModel, QWeb):
files = [] files = []
remains = [] remains = []
for el in html.fragments_fromstring(template): for el in html.fragments_fromstring(template):
if isinstance(el, pycompat.string_types): if isinstance(el, html.HtmlElement):
remains.append(pycompat.to_text(el))
elif isinstance(el, html.HtmlElement):
href = el.get('href', '') href = el.get('href', '')
src = el.get('src', '') src = el.get('src', '')
atype = el.get('type') atype = el.get('type')
media = el.get('media') media = el.get('media')
can_aggregate = not urls.url_parse(href).netloc and not href.startswith('/web/content') if can_aggregate(href) and (el.tag == 'style' or (el.tag == 'link' and el.get('rel') == 'stylesheet')):
if el.tag == 'style' or (el.tag == 'link' and el.get('rel') == 'stylesheet' and can_aggregate):
if href.endswith('.sass'): if href.endswith('.sass'):
atype = 'text/sass' atype = 'text/sass'
elif href.endswith('.less'): elif href.endswith('.less'):
@ -237,25 +340,26 @@ class IrQWeb(models.AbstractModel, QWeb):
path = [segment for segment in href.split('/') if segment] path = [segment for segment in href.split('/') if segment]
filename = get_resource_path(*path) if path else None filename = get_resource_path(*path) if path else None
files.append({'atype': atype, 'url': href, 'filename': filename, 'content': el.text, 'media': media}) files.append({'atype': atype, 'url': href, 'filename': filename, 'content': el.text, 'media': media})
elif el.tag == 'script': elif can_aggregate(src) and el.tag == 'script':
atype = 'text/javascript' atype = 'text/javascript'
path = [segment for segment in src.split('/') if segment] path = [segment for segment in href.split('/') if segment]
filename = get_resource_path(*path) if path else None filename = get_resource_path(*path) if path else None
files.append({'atype': atype, 'url': src, 'filename': filename, 'content': el.text, 'media': media}) files.append({'atype': atype, 'url': src, 'filename': filename, 'content': el.text, 'media': media})
else: else:
remains.append(html.tostring(el, encoding='unicode')) remains.append((el.tag, OrderedDict(el.attrib), el.text))
else: else:
try: # the other cases are ignored
remains.append(html.tostring(el, encoding='unicode')) pass
except Exception:
# notYETimplementederror
raise NotImplementedError
return (files, remains) return (files, remains)
def _get_field(self, record, field_name, expression, tagName, field_options, options, values): def _get_field(self, record, field_name, expression, tagName, field_options, options, values):
field = record._fields[field_name] field = record._fields[field_name]
# adds template compile options for rendering fields
field_options['template_options'] = options
# adds generic field options
field_options['tagName'] = tagName field_options['tagName'] = tagName
field_options['expression'] = expression field_options['expression'] = expression
field_options['type'] = field_options.get('widget', field.type) field_options['type'] = field_options.get('widget', field.type)
@ -275,6 +379,9 @@ class IrQWeb(models.AbstractModel, QWeb):
return (attributes, content, inherit_branding or translate) return (attributes, content, inherit_branding or translate)
def _get_widget(self, value, expression, tagName, field_options, options, values): def _get_widget(self, value, expression, tagName, field_options, options, values):
# adds template compile options for rendering fields
field_options['template_options'] = options
field_options['type'] = field_options['widget'] field_options['type'] = field_options['widget']
field_options['tagName'] = tagName field_options['tagName'] = tagName
field_options['expression'] = expression field_options['expression'] = expression

View File

@ -101,6 +101,8 @@ _SAFE_OPCODES = _EXPR_OPCODES.union(set(opmap[x] for x in [
'CALL_FUNCTION_EX', 'CALL_FUNCTION_EX',
# Already in P2 but apparently the first one is used more aggressively in P3 # Already in P2 but apparently the first one is used more aggressively in P3
'CALL_FUNCTION_KW', 'CALL_FUNCTION_VAR', 'CALL_FUNCTION_VAR_KW', 'CALL_FUNCTION_KW', 'CALL_FUNCTION_VAR', 'CALL_FUNCTION_VAR_KW',
# Added in P3.7 https://bugs.python.org/issue26110
'CALL_METHOD', 'LOAD_METHOD',
'GET_ITER', 'FOR_ITER', 'YIELD_VALUE', 'GET_ITER', 'FOR_ITER', 'YIELD_VALUE',
'JUMP_FORWARD', 'JUMP_IF_TRUE', 'JUMP_IF_FALSE', 'JUMP_ABSOLUTE', 'JUMP_FORWARD', 'JUMP_IF_TRUE', 'JUMP_IF_FALSE', 'JUMP_ABSOLUTE',
# New in Python 2.7 - http://bugs.python.org/issue4715 : # New in Python 2.7 - http://bugs.python.org/issue4715 :