Skip to content

Commit

Permalink
make "chameleon zope context wrapping" more faithfull (#873)
Browse files Browse the repository at this point in the history
* faithful chameleon-zope context wrapper

* make flake8 happy

* rework chameleon/zope context integration

* fixed Python 3 incompatibilty
  • Loading branch information
dataflake committed Jul 10, 2020
1 parent 97f0d95 commit ba252d4
Show file tree
Hide file tree
Showing 3 changed files with 154 additions and 63 deletions.
3 changes: 3 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ https://github.com/zopefoundation/Zope/blob/4.x/CHANGES.rst
5.0a3 (unreleased)
------------------


- Make "chameleon-zope context wrapping" more faithful

- Let "unicode conflict resolution" work for all templates (not just
``ZopePageTemplate``).

Expand Down
106 changes: 61 additions & 45 deletions src/Products/PageTemplates/engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
import ast
import logging
import re
from collections.abc import Mapping
from weakref import ref

from chameleon.astutil import Static
from chameleon.astutil import Symbol
Expand All @@ -36,7 +38,6 @@

from .Expressions import PathIterator
from .Expressions import SecureModuleImporter
from .Expressions import ZopeContext
from .interfaces import IZopeAwareEngine


Expand Down Expand Up @@ -146,74 +147,87 @@ def _compile_zt_expr(type, expression, engine=None, econtext=None):
_compile_zt_expr_node = Static(Symbol(_compile_zt_expr))


class _C2ZContextWrapper(ZopeContext):
"""Behaves like "zope" context with vars from "chameleon" context."""
def __init__(self, c_context, attrs):
self.__c_context = c_context
self.__z_context = c_context["__zt_context__"]
self.__attrs = attrs
# map context class to context wrapper class
_context_class_registry = {}

# delegate to ``__c_context``
@property
def vars(self):
return self

def _with_vars_from_chameleon(context):
"""prepare *context* to get its ``vars`` from ``chameleon``."""
cc = context.__class__
wc = _context_class_registry.get(cc)
if wc is None:
class ContextWrapper(_C2ZContextWrapperBase, cc):
pass

wc = _context_class_registry[cc] = ContextWrapper
context.__class__ = wc
return ref(context)


class Name2KeyError(Mapping):
# auxiliary class to convert ``chameleon``'s ``NameError``
# into ``KeyError``
def __init__(self, mapping):
self.data = mapping

def __getitem__(self, key):
try:
return self.__c_context.__getitem__(key)
except NameError: # Exception for missing key
if key == "attrs":
return self.__attrs
return self.data[key]
except NameError:
raise KeyError(key)

def __iter__(self):
return iter(self.data)

def __len__(self):
return len(self.data)


class _C2ZContextWrapperBase(object):
"""Behaves like "zope" context with vars from "chameleon" context.
It is assumed that an instance holds the current ``chameleon``
context in its attribute ``_c_context``.
"""
@property
def vars(self):
return Name2KeyError(self._c_context)

# delegate to `_c_context`
def getValue(self, name, default=None):
try:
return self[name]
except KeyError:
return self._c_context[name]
except NameError:
return default

get = getValue

def setLocal(self, name, value):
self.__c_context.setLocal(name, value)
self._c_context.setLocal(name, value)

def setGlobal(self, name, value):
self.__c_context.setGlobal(name, value)

# delegate reading ``dict`` methods to ``c_context``
# Note: some "global"s might be missing
# Note: we do not expect that modifying ``dict`` methods are used
def __iter__(self):
return iter(self.__c_context)

def keys(self):
return self.__c_context.keys()

def values(self):
return self.__c_context.values()

def items(self):
return self.__c_context.items()

def __len__(self):
return len(self.__c_context)

def __contains__(self, k):
return k in self.__c_context
self._c_context.setGlobal(name, value)

# unsupported methods
def beginScope(self, *args, **kw):
"""will not work as the scope is controlled by ``chameleon``."""
raise NotImplementedError()

endScope = beginScope
setContext = beginScope
setSourceFile = beginScope
setPosition = beginScope
setRepeat = beginScope

# work around bug in ``zope.tales.tales.Context``
def getDefault(self):
return self.contexts["default"]


# delegate all else to ``__z_context``
def __getattr__(self, attr):
return getattr(self.__z_context, attr)
def _C2ZContextWrapper(c_context, attrs):
c_context["attrs"] = attrs
zt_context = c_context["__zt_context__"]()
zt_context._c_context = c_context
return zt_context


_c_context_2_z_context_node = Static(Symbol(_C2ZContextWrapper))
Expand Down Expand Up @@ -322,9 +336,11 @@ def __call__(self, context, macros, tal=True, **options):
# and evaluation
# unused for ``chameleon.tales`` expressions
kwargs["__zt_engine__"] = self.engine
kwargs["__zt_context__"] = context
kwargs["__zt_context__"] = _with_vars_from_chameleon(context)

template = self.template
# ensure ``chameleon`` ``default`` representation
context.setContext("default", DEFAULT_MARKER)
kwargs["default"] = DEFAULT_MARKER

return template.render(**kwargs)
Expand Down
108 changes: 90 additions & 18 deletions src/Products/PageTemplates/tests/testC2ZContext.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,51 +16,57 @@

from zope.tales.tales import Context

from ..engine import DEFAULT_MARKER
from ..engine import _C2ZContextWrapper
from ..engine import _context_class_registry
from ..engine import _with_vars_from_chameleon as wrap


class C2ZContextTests(unittest.TestCase):
def setUp(self):
self.c_context = c_context = Scope()
c_context["__zt_context__"] = Context(None, {})
z_context = Context(None, {})
z_context.setContext("default", DEFAULT_MARKER)
c_context["__zt_context__"] = wrap(z_context)
self.z_context = _C2ZContextWrapper(c_context, None)

def test_elementary_functions(self):
c = self.z_context
cv = c.vars
c.setLocal("a", "A")
c.setLocal("b", "B")
self.assertEqual(c["a"], "A")
self.assertEqual(cv["a"], "A")
self.assertEqual(c.get("b"), "B")
self.assertIsNone(c.get("c"))
with self.assertRaises(KeyError):
c["c"]
self.assertEqual(sorted(c.keys()), ["__zt_context__", "a", "b"])
self.assertEqual(sorted(c), ["__zt_context__", "a", "b"])
vs = c.values()
cv["c"]
self.assertEqual(sorted(cv.keys()),
["__zt_context__", "a", "attrs", "b"])
self.assertEqual(sorted(cv), ["__zt_context__", "a", "attrs", "b"])
vs = cv.values()
for v in ("A", "B"):
self.assertIn(v, vs)
its = c.items()
its = cv.items()
for k in "ab":
self.assertIn((k, k.capitalize()), its)
self.assertIn("a", c)
self.assertNotIn("c", c)
self.assertEqual(len(c), 3)
# templates typically use ``vars`` as ``dict`` instead of API methods
self.assertIs(c.vars, c)
self.assertIn("a", cv)
self.assertNotIn("c", cv)
self.assertEqual(len(cv), 4)

def test_setGlobal(self):
top_context = self.z_context
top_context = self.z_context.vars
c_context = self.c_context.copy() # context push
c = _C2ZContextWrapper(c_context, None) # local ``zope`` context
cv = c.vars
c.setLocal("a", "A")
self.assertIn("a", c)
self.assertIn("a", cv)
self.assertNotIn("a", top_context)
c.setGlobal("b", "B")
# the following (commented code) line fails due to
# "https://github.com/malthe/chameleon/issues/305"
# self.assertIn("b", c)
self.assertIn("b", top_context)
self.assertEqual(c["b"], "B")
self.assertEqual(cv["b"], "B")
self.assertEqual(top_context["b"], "B")
# Note: "https://github.com/malthe/chameleon/issues/305":
# ``dict`` methods are unreliable in presence of global variables
Expand All @@ -69,7 +75,8 @@ def test_setGlobal(self):
def test_unimplemented(self):
c = self.z_context
cd = Context.__dict__
for m in ("beginScope", "endScope", "setSourceFile", "setPosition"):
for m in ("beginScope", "endScope", "setSourceFile", "setPosition",
"setRepeat"):
self.assertIn(m, cd) # check against spelling errors
with self.assertRaises(NotImplementedError):
getattr(c, m)()
Expand All @@ -80,6 +87,71 @@ def test_attribute_delegation(self):

def test_attrs(self):
c = self.z_context
self.assertIsNone(c["attrs"])
self.assertIsNone(c.vars["attrs"])
c.setLocal("attrs", "hallo")
self.assertEqual(c["attrs"], "hallo")
self.assertEqual(c.vars["attrs"], "hallo")

def test_faithful_wrapping(self):
class MyContextBase(object):
var = None
var2 = None

def get_vars(self):
return self.vars

class MyContext(MyContextBase):
var = None

def set(self, v):
self.attr = v

def get_vars(self):
return super(MyContext, self).get_vars()

def my_get(self, k):
return self.vars[k]

def override_var(self):
self.var = "var"

c_context = self.c_context
my_context = MyContext()
my_context.var2 = "var2"
c_context["__zt_context__"] = wrap(my_context)
zc = _C2ZContextWrapper(c_context, None)
# attributes
# -- via method
zc.set("attr")
self.assertEqual(zc.attr, "attr")
# -- via wrapper
zc.wattr = "wattr"
self.assertEqual(zc.wattr, "wattr")
# correct ``vars``; including ``super``
self.assertEqual(zc.get_vars(), zc.vars)
# correct subscription
zc.setLocal("a", "a")
self.assertEqual(zc.my_get("a"), "a")
# correct attribute access
my_context.my_attr = "my_attr"
self.assertEqual(zc.my_attr, "my_attr")
# override class variable
self.assertIsNone(zc.var)
zc.override_var()
self.assertEqual(zc.var, "var")
# instance variable over class variable
self.assertEqual(zc.var2, "var2")

def test_context_class_registry(self):
class MyContext(object):
pass

class_regs = len(_context_class_registry)
wc1 = wrap(MyContext())()
self.assertEqual(len(_context_class_registry), class_regs + 1)
wc2 = wrap(MyContext())()
self.assertEqual(len(_context_class_registry), class_regs + 1)
self.assertIs(wc1.__class__, wc2.__class__)

def test_default(self):
c = self.z_context
self.assertIs(c.getDefault(), DEFAULT_MARKER)

0 comments on commit ba252d4

Please sign in to comment.