diff --git a/core/docs/changelog/ZO-4627.change b/core/docs/changelog/ZO-4627.change
new file mode 100644
index 0000000000..4a72142368
--- /dev/null
+++ b/core/docs/changelog/ZO-4627.change
@@ -0,0 +1 @@
+ZO-4627: Replace lxml.objectify with plain lxml.etree usage
diff --git a/core/pyproject.toml b/core/pyproject.toml
index 616d84fe8f..6b4f5bc1d6 100644
--- a/core/pyproject.toml
+++ b/core/pyproject.toml
@@ -14,7 +14,6 @@ dependencies = [
"filetype",
"gocept.cache >= 2.1",
"gocept.form[formlib]>=0.7.5", # XXX Should be [ui], but is entrenched
- "gocept.lxml>=0.2.1",
"gocept.runner>0.5.3",
"google-cloud-storage>=2.1.0.dev0",
"grokcore.component",
@@ -208,7 +207,6 @@ deploy = [
zon = [
"gocept.form==0.8.0+py3",
- "gocept.lxml==0.3.0+lxml5", # https://github.com/ZeitOnline/gocept.lxml/tree/py3
"zope.app.locking==3.5.0+py3.1",
"zope.xmlpickle==4.0.0+py3k1", # https://github.com/ZeitOnline/zope.xmlpickle/tree/py3
# We created our own py310 wheels on devpi.zeit.de for these:
diff --git a/core/src/zeit/campus/tests/test_article.py b/core/src/zeit/campus/tests/test_article.py
index 31813d7bc1..aa1226319b 100644
--- a/core/src/zeit/campus/tests/test_article.py
+++ b/core/src/zeit/campus/tests/test_article.py
@@ -24,4 +24,4 @@ def test_topic_should_generate_proper_xml(self):
)
== 1
)
- assert tplink.xml.xpath('//head/topic/label')[0] == 'Moep'
+ assert tplink.xml.xpath('//head/topic/label')[0].text == 'Moep'
diff --git a/core/src/zeit/cms/application.zcml b/core/src/zeit/cms/application.zcml
index cbe91dbd65..11255b2adf 100644
--- a/core/src/zeit/cms/application.zcml
+++ b/core/src/zeit/cms/application.zcml
@@ -16,7 +16,6 @@
-
diff --git a/core/src/zeit/cms/checkout/tests/test_webhook.py b/core/src/zeit/cms/checkout/tests/test_webhook.py
index a8dac5384c..30e9820e4f 100644
--- a/core/src/zeit/cms/checkout/tests/test_webhook.py
+++ b/core/src/zeit/cms/checkout/tests/test_webhook.py
@@ -1,7 +1,7 @@
from unittest import mock
import celery.exceptions
-import lxml.objectify
+import lxml.etree
import plone.testing
import requests.exceptions
@@ -33,7 +33,7 @@ def setUp(self):
)
self.patch = mock.patch(
'zeit.cms.checkout.webhook.HookSource._get_tree',
- side_effect=lambda: lxml.objectify.fromstring(self.config),
+ side_effect=lambda: lxml.etree.fromstring(self.config),
)
self.patch.start()
source = zeit.cms.checkout.webhook.HOOKS.factory
diff --git a/core/src/zeit/cms/configure.zcml b/core/src/zeit/cms/configure.zcml
index d148a16f13..25f27d0ec6 100644
--- a/core/src/zeit/cms/configure.zcml
+++ b/core/src/zeit/cms/configure.zcml
@@ -55,11 +55,13 @@
-
+
+
+
+
+
+
+
+
-
+
-
-
+
-
-
+
diff --git a/core/src/zeit/cms/content/adapter.txt b/core/src/zeit/cms/content/adapter.txt
index da7d0025d7..ad280edefd 100644
--- a/core/src/zeit/cms/content/adapter.txt
+++ b/core/src/zeit/cms/content/adapter.txt
@@ -9,9 +9,9 @@ The xml source adapter adapts IXMLRepresentation to IXMLSource. Let's make sure
processing instructions which are not inside the document tree are preserverd
Create a dummy object:
->>> import lxml.objectify
+>>> import lxml.etree
>>> class XML:
-... xml = lxml.objectify.fromstring('')
+... xml = lxml.etree.fromstring('')
Call the adapter factory -- the PI is still there. We also get the XML
diff --git a/core/src/zeit/cms/content/browser/widget.txt b/core/src/zeit/cms/content/browser/widget.txt
index 8c8a4ee57d..bb7fcc796d 100644
--- a/core/src/zeit/cms/content/browser/widget.txt
+++ b/core/src/zeit/cms/content/browser/widget.txt
@@ -19,11 +19,11 @@ Create a schema:
Create a content object:
->>> import lxml.objectify
+>>> import lxml.etree
>>> import zeit.cms.content.property
>>> @zope.interface.implementer(IContent)
... class Content:
-... xml = lxml.objectify.XML('')
+... xml = lxml.etree.fromstring('')
... snippet = zeit.cms.content.property.Structure('.title')
>>> content = Content()
@@ -54,8 +54,8 @@ Editing sub-nodes
The widget also supports editing subnodes. That is that the data being edited
is not a full tree but a node in a tree.
->>> content.whole_tree = lxml.objectify.XML('')
->>> content.xml = content.whole_tree.editme
+>>> content.whole_tree = lxml.etree.fromstring('')
+>>> content.xml = content.whole_tree.find('editme')
>>> widget.setRenderedValue(content.xml)
>>> widget._getFormValue()
'\r\n \r\n\r\n'
diff --git a/core/src/zeit/cms/content/field.py b/core/src/zeit/cms/content/field.py
index 95f40c0038..c3c130a530 100644
--- a/core/src/zeit/cms/content/field.py
+++ b/core/src/zeit/cms/content/field.py
@@ -1,5 +1,4 @@
import lxml.etree
-import lxml.objectify
import zope.interface
import zope.location.location
import zope.proxy
@@ -8,7 +7,7 @@
import zope.security.checker
import zope.security.proxy
-from zeit.cms.content.util import objectify_soup_fromstring
+from zeit.cms.content.util import etree_soup_fromstring
DEFAULT_MARKER = object()
@@ -25,9 +24,9 @@ class _XMLBase(zope.schema.Field):
def __init__(self, *args, **kw):
tidy_input = kw.pop('tidy_input', False)
if tidy_input:
- self.parse = objectify_soup_fromstring
+ self.parse = etree_soup_fromstring
else:
- self.parse = lxml.objectify.fromstring
+ self.parse = lxml.etree.fromstring
super().__init__(*args, **kw)
def fromUnicode(self, text):
diff --git a/core/src/zeit/cms/content/field.txt b/core/src/zeit/cms/content/field.txt
index 4fd2806b0f..636952ccd7 100644
--- a/core/src/zeit/cms/content/field.txt
+++ b/core/src/zeit/cms/content/field.txt
@@ -5,6 +5,7 @@ Fields
XML Tree
========
+>>> from lxml.builder import E
>>> from zeit.cms.content.field import XMLTree
>>> import zeit.cms.testing
>>> field = XMLTree()
@@ -18,8 +19,8 @@ XML Tree
True
>>> content2 = Content()
->>> content.xml['child'] = 'child'
->>> content2.xml = content.xml['child']
+>>> content.xml.append(E.child('child'))
+>>> content2.xml = content.xml.find('child')
>>> tree2 = field.fromUnicode('MyNewValue')
>>> field.set(content2, tree2)
>>> print(zeit.cms.testing.xmltotext(content.xml))
@@ -31,15 +32,15 @@ True
Replacing node when there are siblings present
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
->>> import lxml.objectify
->>> root = lxml.objectify.E.root()
->>> root.append(lxml.objectify.E.child())
->>> root.append(lxml.objectify.E.child())
+>>> import lxml.builder
+>>> root = lxml.builder.E.root()
+>>> root.append(E.child())
+>>> root.append(E.child())
>>> content = Content()
>>> field = XMLTree()
>>> field.__name__ = 'xml'
->>> field.set(content, root.child)
->>> field.set(content, lxml.objectify.E.new())
+>>> field.set(content, root.find('child'))
+>>> field.set(content, E.new())
>>> print(zeit.cms.testing.xmltotext(root))
@@ -55,4 +56,4 @@ Tidying broken input
>>> tree = field.fromUnicode(
... '')
>>> print(zeit.cms.testing.xmltotext(tree))
-
\ No newline at end of file
+
diff --git a/core/src/zeit/cms/content/interfaces.py b/core/src/zeit/cms/content/interfaces.py
index c32503396c..6ec15133d2 100644
--- a/core/src/zeit/cms/content/interfaces.py
+++ b/core/src/zeit/cms/content/interfaces.py
@@ -374,7 +374,7 @@ class IXMLReference(zope.interface.Interface):
might be references inside the that always use a tag.
(NOTE: These are just examples, not actual zeit.cms policy!)
- Adapting to IXMLReference yields an lxml.objectify tree::
+ Adapting to IXMLReference yields an lxml.etree::
node = zope.component.getAdapter(
content, zeit.cms.content.interfaces.IXMLReference, name='image')
@@ -389,7 +389,7 @@ class IXMLReferenceUpdater(zope.interface.Interface):
def update(xml_node, suppress_errors=False):
"""Update xml_node with data from the content object.
- xml_node: lxml.objectify'ed element
+ xml_node: lxml.etree.Element
"""
diff --git a/core/src/zeit/cms/content/lxmlpickle.py b/core/src/zeit/cms/content/lxmlpickle.py
index d1eab73247..3a87fb4586 100644
--- a/core/src/zeit/cms/content/lxmlpickle.py
+++ b/core/src/zeit/cms/content/lxmlpickle.py
@@ -8,26 +8,30 @@
log = logging.getLogger(__name__)
-def treeFactory(state):
- """Un-Pickle factory."""
+def deserialize(state):
try:
- return lxml.objectify.fromstring(state)
+ return lxml.etree.fromstring(state)
except Exception as e:
log.error('Error during unpickling', exc_info=True)
- return lxml.objectify.fromstring(
- '' % (e, state)
- )
+ return lxml.etree.fromstring('' % (e, state))
-copyreg.constructor(treeFactory)
+copyreg.constructor(deserialize)
-def reduceObjectifiedElement(object):
- """Reduce function for lxml.objectify trees.
+def serialize(obj):
+ """Reduce function for lxml trees.
See http://docs.python.org/lib/pickle-protocol.html for details.
"""
- state = lxml.etree.tostring(object.getroottree())
- return (treeFactory, (state,))
+ state = lxml.etree.tostring(obj.getroottree())
+ return (
+ deserialize,
+ (state,),
+ )
-copyreg.pickle(lxml.objectify.ObjectifiedElement, reduceObjectifiedElement, treeFactory)
+copyreg.pickle(lxml.etree._Element, serialize)
+
+# BBB
+treeFactory = deserialize
+copyreg.pickle(lxml.objectify.ObjectifiedElement, serialize)
diff --git a/core/src/zeit/cms/content/lxmlpickle.txt b/core/src/zeit/cms/content/lxmlpickle.txt
index 22189ec6c8..62db917b71 100644
--- a/core/src/zeit/cms/content/lxmlpickle.txt
+++ b/core/src/zeit/cms/content/lxmlpickle.txt
@@ -5,12 +5,12 @@ lxml pickle support
Verify the lxml pickle support.
>>> import pickle
->>> import lxml.objectify
+>>> import lxml.etree
>>> import zeit.cms.content.lxmlpickle
->>> xml = lxml.objectify.fromstring('zoot')
+>>> xml = lxml.etree.fromstring('zoot')
>>> p = pickle.dumps(xml)
>>> restored_xml = pickle.loads(p)
->>> print(zeit.cms.testing.xmltotext(restored_xml.getroottree()))
+>>> print(lxml.etree.tostring(restored_xml.getroottree(), pretty_print=True, encoding=str))
zoot
diff --git a/core/src/zeit/cms/content/property.py b/core/src/zeit/cms/content/property.py
index fd835acae8..da0b51b474 100644
--- a/core/src/zeit/cms/content/property.py
+++ b/core/src/zeit/cms/content/property.py
@@ -6,6 +6,7 @@
import zope.component
import zope.schema.interfaces
+from zeit.cms.content.util import create_parent_nodes
import zeit.cms.content.interfaces
import zeit.cms.interfaces
import zeit.connector.resource
@@ -40,50 +41,43 @@ def __get__(self, instance, class_):
if node.text is not None:
return self.field.bind(instance).fromUnicode(str(node.text))
except zope.schema.interfaces.ValidationError:
- # Fall back to not using the field when the validaion fails.
+ # Fall back to not using the field when the validation fails.
pass
- try:
- value = node.pyval
- except AttributeError:
- return None
- if isinstance(value, str):
- # This is safe because lxml only uses str for optimisation
- # reasons and unicode when non us-ascii chars are in the str:
- value = str(value)
- return value
+ return node.text or ''
def __set__(self, instance, value):
if self.path is None:
# We cannot just set the new value because setting detaches the
- # instance.xml from the original tree leaving instance independet
+ # instance.xml from the original tree leaving instance independent
# of the xml-tree.
- node = instance.xml
- parent = node.getparent()
- new_node = lxml.objectify.E.root(getattr(lxml.objectify.E, node.tag)(value))[node.tag]
- lxml.objectify.deannotate(new_node)
- parent.replace(node, new_node)
- instance.xml = new_node
+ root = instance.xml
+ parent = root.getparent()
+ node = lxml.etree.Element(root.tag)
+ node.text = value
+ parent.replace(root, node)
+ instance.xml = node
else:
if value is not None:
- self.path.setattr(instance.xml, value)
+ parent, name = create_parent_nodes(self.path, instance.xml)
+ node = parent.find(name)
+ if node is None:
+ node = lxml.etree.Element(name)
+ parent.append(node)
+ # XXX Previously this used self.path.setattr, relying on
+ # lxml.objectify type conversion (int and bool mostly).
+ # We probably should use self.field instead?
+ if isinstance(value, bool):
+ value = str(value).lower()
+ node.text = str(value)
else:
- node = self.path.find(instance.xml, None)
+ node = self.getNode(instance)
if node is not None:
node.getparent().remove(node)
- node = self.getNode(instance)
- if node is not None:
- lxml.objectify.deannotate(node)
def getNode(self, instance):
if self.path is None:
return instance.xml
- try:
- node = self.path.find(instance.xml)
- except AttributeError:
- return None
- if isinstance(node, lxml.objectify.NoneElement):
- return None
- return node
+ return self.path.find(instance.xml, None)
class Structure(ObjectPathProperty):
@@ -117,10 +111,9 @@ def __get__(self, instance, class_):
node = self.getNode(instance)
if node is None:
return self.field.missing_value if self.field else None
- node = lxml.objectify.fromstring(str(self.remove_namespaces(node)))
- result = [xml.sax.saxutils.escape(str(node))]
+ node = lxml.etree.fromstring(str(self.remove_namespaces(node)))
+ result = [xml.sax.saxutils.escape(node.text)]
for child in node.iterchildren():
- lxml.objectify.deannotate(child)
result.append(lxml.etree.tostring(child, encoding=str))
return ''.join(result)
@@ -128,7 +121,7 @@ def __set__(self, instance, value):
if self.field and value is self.field.missing_value:
value = None
else:
- value = lxml.objectify.fromstring('%s' % value)
+ value = lxml.etree.fromstring('%s' % value)
self.path.setattr(instance.xml, value)
@@ -186,39 +179,37 @@ def __get__(self, instance, class_):
return self
tree = instance.xml
result = []
- try:
- element_set = self.path.find(tree)
- except AttributeError:
- # no keywords
- element_set = []
- for node in element_set:
- result.append(self._element_factory(node, tree))
+ anchor = self.path.find(tree, None)
+ if anchor is not None:
+ for node in anchor.getparent().iterchildren(anchor.tag):
+ result.append(self._element_factory(node))
return self.result_type(elem for elem in result if elem is not None)
def __set__(self, instance, value):
# Remove nodes.
tree = instance.xml
- for entry in self.path.find(tree, []):
- entry.getparent().remove(entry)
+ node = self.path.find(tree, None)
+ if node is not None:
+ for entry in node.getparent().iterchildren(node.tag):
+ entry.getparent().remove(entry)
# Add new nodes:
value = self.sorted(value)
- self.path.setattr(tree, [self._node_factory(entry, tree) for entry in value])
- if value:
- lxml.objectify.deannotate(self.path.find(instance.xml).getparent())
+ self.path.setattr(tree, [self._node_factory(entry) for entry in value])
- def _element_factory(self, node, tree):
+ def _element_factory(self, node):
raise NotImplementedError('Implemented in sub classes.')
- def _node_factory(self, entry, tree):
+ def _node_factory(self, entry):
raise NotImplementedError('Implemented in sub classes.')
class SimpleMultiProperty(MultiPropertyBase):
- def _element_factory(self, node, tree):
- return str(node)
+ def _element_factory(self, node):
+ return node.text
- def _node_factory(self, entry, tree):
- return entry
+ def _node_factory(self, entry):
+ name = str(self.path).split('.')[-1]
+ return getattr(lxml.builder.E, name)(entry)
class SingleResource(ObjectPathProperty):
diff --git a/core/src/zeit/cms/content/property.txt b/core/src/zeit/cms/content/property.txt
index a2cebedbb9..d0e6422426 100644
--- a/core/src/zeit/cms/content/property.txt
+++ b/core/src/zeit/cms/content/property.txt
@@ -4,7 +4,7 @@ XML-Properties
The xml properties map to elements or attributes in an xml document. There are
several of them. All have in common that they are execting the instance to have
-an `xml` attribute being an `lxml.objectify` tree.
+an `xml` attribute being an `lxml.etree` tree.
ObjectPathProperty
@@ -19,12 +19,11 @@ Root as `xml`
Normally the document root is the `xml` attribute. Create a test class:
>>> from zeit.cms.content.property import ObjectPathProperty
->>> import lxml.objectify
+>>> import lxml.etree
>>> from unittest import mock
>>> import persistent
>>> class Content(persistent.Persistent):
-... xml = lxml.objectify.fromstring(
-... '')
+... xml = lxml.etree.fromstring('')
... b = ObjectPathProperty('.b')
>>> content = Content()
>>> content._p_jar = mock.Mock()
@@ -40,7 +39,7 @@ Let's assign some values to b. Integer:
>>> content.b
5
>>> print(zeit.cms.testing.xmltotext(content.xml))
-
+
5
>>> content._p_changed
@@ -53,7 +52,7 @@ Float:
>>> content.b
47.25
>>> print(zeit.cms.testing.xmltotext(content.xml))
-
+
47.25
@@ -61,7 +60,7 @@ Float:
>>> content.b
47.0
>>> print(zeit.cms.testing.xmltotext(content.xml))
-
+
47.0
@@ -71,13 +70,12 @@ String. Note that strings are always unicode:
>>> content.b
'Foo'
>>> print(zeit.cms.testing.xmltotext(content.xml))
-
+
Foo
-It is also possible to declare a field for the property. This helps when
-objectify guesses the data wrong:
+It is also possible to declare a field for the property:
>>> import zope.schema
>>> prop = ObjectPathProperty('.b', zope.schema.TextLine())
@@ -113,7 +111,7 @@ The `xml` attribute can also be a sub node of an xml tree. There is a special
object path of `None` to refer directly to the node specified by `xml`. Let's
create a test-class:
->>> xml_tree = lxml.objectify.fromstring('')
+>>> xml_tree = lxml.etree.fromstring('')
>>> class Content(persistent.Persistent):
... xml = xml_tree.b[1]
... b = ObjectPathProperty(None)
@@ -150,12 +148,11 @@ The ObjectPathAttributeProperty refers to a node via an object path and then to
an attribute of that node. Given the following XML we can access the attributes
via ObjectPathAttributeProperty:
->>> xml_tree = lxml.objectify.fromstring(
+>>> xml_tree = lxml.etree.fromstring(
... 'link')
Let's define a content class using the XML. Word and character count are
-integers. Since lxml.objectify only supports str/unicode attributes we give a
-hint using a zope.schema field. We are also referencing `sencences` which is
+integers. We are also referencing `sencences` which is
not in the document, yet:
>>> from zeit.cms.content.property import ObjectPathAttributeProperty
@@ -285,7 +282,7 @@ referenced:
>>> class Content:
... res = zeit.cms.content.property.SingleResource('.res')
... def __init__(self):
-... self.xml = lxml.objectify.XML('')
+... self.xml = lxml.etree.fromstring('')
Create our content object:
@@ -309,7 +306,7 @@ Let's have a look how the resource has been referenced:
>>> print(zeit.cms.testing.xmltotext(content.xml))
- http://xml.zeit.de/refed
+ http://xml.zeit.de/refed
@@ -348,14 +345,14 @@ which provides IXMLReference for our content object:
... attributes=('foo', 'href', ))
...
... def __init__(self):
-... self.xml = lxml.objectify.XML(
-... '')
+... self.xml = lxml.etree.fromstring('')
...
+>>> import lxml.builder
>>> import zeit.cms.content.interfaces
>>> @zope.component.adapter(Content)
... @zope.interface.implementer(zeit.cms.content.interfaces.IXMLReference)
... def xmlref(context):
-... return lxml.objectify.E.related(type="intern", href=context.uniqueId)
+... return lxml.builder.E.related(type="intern", href=context.uniqueId)
>>> gsm.registerAdapter(xmlref, name='related')
Create a content object and an object which can be referenced:
@@ -372,7 +369,7 @@ True
>>> content.res = referenced_obj
>>> print(zeit.cms.testing.xmltotext(content.xml))
-
+
@@ -431,15 +428,14 @@ The `SimpleMultiProperty` is used for lists of simple values.
... authors = zeit.cms.content.property.SimpleMultiProperty(
... '.authors.author')
... def __init__(self):
-... self.xml = lxml.objectify.XML(
-... '')
+... self.xml = lxml.etree.fromstring('')
>>> content = Content()
Set authors:
>>> content.authors = ('Hans', 'Klaus', 'Siegfried')
>>> print(zeit.cms.testing.xmltotext(content.xml))
-
+
Hans
diff --git a/core/src/zeit/cms/content/reference.py b/core/src/zeit/cms/content/reference.py
index b392a72bca..777940a5c2 100644
--- a/core/src/zeit/cms/content/reference.py
+++ b/core/src/zeit/cms/content/reference.py
@@ -14,7 +14,6 @@
import copy
import urllib.parse
-import gocept.lxml.interfaces
import grokcore.component as grok
import lxml.objectify
import z3c.traverser.interfaces
@@ -23,6 +22,7 @@
import zope.security.proxy
import zope.traversing.browser.absoluteurl
+from zeit.cms.content.util import create_parent_nodes
import zeit.cms.browser.interfaces
import zeit.cms.content.interfaces
import zeit.cms.content.xmlsupport
@@ -122,7 +122,14 @@ def __set__(self, instance, value):
for child in value.iterchildren():
xml.append(copy.copy(child))
else:
- self.path.setattr(xml, value)
+ for node in self._reference_nodes(instance):
+ node.getparent().remove(node)
+ parent, name = create_parent_nodes(self.path, instance.xml)
+ for node in value:
+ node.tag = name
+ parent.append(node)
+ else:
+ parent.text = None
instance._p_changed = True
def _check_for_references(self, values):
@@ -151,10 +158,13 @@ def __name__(self, instance):
return None
def _reference_nodes(self, instance):
- try:
- return self.path.find(zope.security.proxy.getObject(instance.xml))
- except AttributeError:
+ xml = zope.security.proxy.getObject(instance.xml)
+ if str(self.path) == '.':
+ return [xml]
+ node = self.path.find(xml, None)
+ if node is None:
return []
+ return list(node.getparent().iterchildren(node.tag))
def update_metadata(self, instance, suppress_errors=False):
for reference in self.__get__(instance, None):
@@ -254,12 +264,6 @@ def __set__(self, instance, value):
value = (value,)
super().__set__(instance, value)
- def _reference_nodes(self, instance):
- result = super()._reference_nodes(instance)
- if not isinstance(result, list):
- result = [result]
- return result
-
def update_metadata(self, instance, suppress_errors=False):
reference = self.__get__(instance, None)
if reference:
@@ -388,7 +392,7 @@ def __eq__(self, other):
@grok.implementer(zeit.cms.content.interfaces.IReference)
class Reference(grok.MultiAdapter, zeit.cms.content.xmlsupport.Persistent):
- grok.adapts(zeit.cms.content.interfaces.IXMLRepresentation, gocept.lxml.interfaces.IObjectified)
+ grok.adapts(zeit.cms.content.interfaces.IXMLRepresentation, zeit.cms.interfaces.IXMLElement)
grok.baseclass()
# XXX kludgy: These must be set manually after adapter call by clients.
diff --git a/core/src/zeit/cms/content/sources.py b/core/src/zeit/cms/content/sources.py
index 806c7081ef..a2bde0053e 100644
--- a/core/src/zeit/cms/content/sources.py
+++ b/core/src/zeit/cms/content/sources.py
@@ -7,7 +7,7 @@
import xml.sax.saxutils
from zope.app.appsetup.product import getProductConfiguration
-import gocept.lxml.objectify
+import lxml.objectify
import pyramid_dogpile_cache2
import zc.sourcefactory.basic
import zc.sourcefactory.contextual
@@ -66,7 +66,7 @@ def _get_tree(self):
def _get_tree_from_url(self, url):
__traceback_info__ = (url,)
logger.debug('Getting %s' % url)
- return gocept.lxml.objectify.fromfile(load(url))
+ return lxml.objectify.parse(load(url)).getroot()
class ShortCachedXMLBase(CachedXMLBase):
@@ -75,7 +75,7 @@ class ShortCachedXMLBase(CachedXMLBase):
def _get_tree_from_url(self, url):
__traceback_info__ = (url,)
logger.debug('Getting %s' % url)
- return gocept.lxml.objectify.fromfile(load(url))
+ return lxml.objectify.parse(load(url)).getroot()
class SimpleXMLSourceBase(CachedXMLBase):
@@ -134,7 +134,7 @@ def getValues(self, context):
tree = self._get_tree()
if self.attribute is NotImplemented:
# Return text value of nodes
- return [str(node) for node in tree.xpath(self.xpath) if self.isAvailable(node, context)]
+ return [node.text for node in tree.xpath(self.xpath) if self.isAvailable(node, context)]
# Return value of provided attribute for nodes
return [
str(node.get(self.attribute))
diff --git a/core/src/zeit/cms/content/tests/test_lxmlpickle.py b/core/src/zeit/cms/content/tests/test_lxmlpickle.py
index 69af876f15..bdc1b58a64 100644
--- a/core/src/zeit/cms/content/tests/test_lxmlpickle.py
+++ b/core/src/zeit/cms/content/tests/test_lxmlpickle.py
@@ -1,5 +1,5 @@
# coding: utf-8
-import lxml.objectify
+import lxml.etree
import transaction
from zeit.cms.checkout.helper import checked_out
@@ -9,7 +9,7 @@
class XMLPickleException(zeit.cms.testing.ZeitCmsTestCase):
def test_parse_errors_are_deserialized_as_comment(self):
with checked_out(self.repository['testcontent'], temporary=False) as co:
- co.xml.body.append(lxml.objectify.fromstring(''))
+ co.xml.append(lxml.etree.fromstring(''))
transaction.commit()
co._p_invalidate() # evict from ZODB cache and unpickle afresh
co.uniqueId
diff --git a/core/src/zeit/cms/content/tests/test_property.py b/core/src/zeit/cms/content/tests/test_property.py
index 8db79fa27f..49ff7d607b 100644
--- a/core/src/zeit/cms/content/tests/test_property.py
+++ b/core/src/zeit/cms/content/tests/test_property.py
@@ -72,9 +72,11 @@ def test_setting_missing_value_deletes_xml_content(self):
content = ExampleContentType()
prop = Structure('.head.foo', zope.schema.Text(missing_value='missing'))
prop.__set__(content, 'qux')
- self.assertEllipsis('qux', lxml.etree.tostring(content.xml.head.foo))
+ self.assertEllipsis('qux', lxml.etree.tostring(content.xml.find('head/foo')))
prop.__set__(content, 'missing')
- self.assertEllipsis('', lxml.etree.tostring(content.xml.head.foo))
+ self.assertEllipsis(
+ '', lxml.etree.tostring(content.xml.find('head/foo'))
+ )
class TestObjectPathProperty(unittest.TestCase, gocept.testing.assertion.Ellipsis):
@@ -84,9 +86,8 @@ def test_setting_none_value_deletes_xml_content(self):
content = ExampleContentType()
prop = ObjectPathProperty('.raw_query', zope.schema.Text(missing_value='missing'))
prop.__set__(content, 'solr!')
- self.assertEqual(content.xml.findall('raw_query'), ['solr!'])
self.assertEllipsis(
- 'solr!', lxml.etree.tostring(content.xml.raw_query)
+ 'solr!', lxml.etree.tostring(content.xml.find('raw_query'))
)
prop.__set__(content, None)
self.assertEqual(content.xml.findall('raw_query'), [])
diff --git a/core/src/zeit/cms/content/tests/test_reference.py b/core/src/zeit/cms/content/tests/test_reference.py
index ce33836754..273fba95f2 100644
--- a/core/src/zeit/cms/content/tests/test_reference.py
+++ b/core/src/zeit/cms/content/tests/test_reference.py
@@ -114,7 +114,7 @@ def test_properties_of_reference_object_are_stored_in_xml(self):
content._p_changed = False
content.references[0].foo = 'bar'
- self.assertEqual('bar', content.xml.body.references.reference.get('foo'))
+ self.assertEqual('bar', content.xml.find('body/references/reference').get('foo'))
self.assertTrue(content._p_changed)
def test_metadata_of_reference_is_updated_on_checkin(self):
@@ -122,13 +122,17 @@ def test_metadata_of_reference_is_updated_on_checkin(self):
content = self.repository['content']
with checked_out(content) as co:
co.references = (co.references.create(self.repository['target']),)
- self.assertEqual('foo', self.repository['content'].xml.body.references.reference.title)
+ self.assertEqual(
+ 'foo', self.repository['content'].xml.find('body/references/reference/title').text
+ )
with checked_out(self.repository['target']) as co:
co.teaserTitle = 'bar'
with checked_out(self.repository['content']):
pass
- self.assertEqual('bar', self.repository['content'].xml.body.references.reference.title)
+ self.assertEqual(
+ 'bar', self.repository['content'].xml.find('body/references/reference/title').text
+ )
def test_set_accepts_references(self):
content = self.repository['content']
@@ -247,11 +251,11 @@ def test_should_be_updated_on_checkin(self):
with checked_out(self.repository['content']):
pass
- body = self.repository['content'].xml['body']
+ body = self.repository['content'].xml.find('body')
# Since ExampleContentType (our reference target) implements
# ICommonMetadata, its XMLReferenceUpdater will write 'title' (among
# others) into the XML.
- self.assertEqual('bar', body['references']['reference']['title'])
+ self.assertEqual('bar', body.find('references/reference/title').text)
class SingleResourceTest(ReferenceFixture, zeit.cms.testing.ZeitCmsTestCase):
@@ -281,11 +285,8 @@ def test_should_be_updated_on_checkin(self):
with checked_out(self.repository['content']):
pass
- body = self.repository['content'].xml['body']
- # Since ExampleContentType (our reference target) implements
- # ICommonMetadata, its XMLReferenceUpdater will write 'title' (among
- # others) into the XML.
- self.assertEqual('bar', body['references']['reference']['title'])
+ body = self.repository['content'].xml.find('body')
+ self.assertEqual('bar', body.find('references/reference/title').text)
class ReferenceTraversalBase:
diff --git a/core/src/zeit/cms/content/tests/test_sources.py b/core/src/zeit/cms/content/tests/test_sources.py
index 2a7d5951b6..24f768ad5b 100644
--- a/core/src/zeit/cms/content/tests/test_sources.py
+++ b/core/src/zeit/cms/content/tests/test_sources.py
@@ -1,7 +1,7 @@
from unittest.mock import Mock
import importlib.resources
-import gocept.lxml.objectify
+import lxml.etree
import pyramid_dogpile_cache2
import zope.interface
@@ -14,7 +14,7 @@ class ExampleSource(zeit.cms.content.sources.XMLSource):
attribute = 'id'
def _get_tree(self):
- return gocept.lxml.objectify.fromstring(
+ return lxml.etree.fromstring(
"""\
- One
@@ -29,7 +29,7 @@ class UnresolveableSource(zeit.cms.content.sources.XMLSource):
attribute = 'id'
def _get_tree(self):
- return gocept.lxml.objectify.fromstring(
+ return lxml.etree.fromstring(
"""\
- Foo
@@ -42,7 +42,7 @@ class ExampleNestedSource(zeit.cms.content.sources.SearchableXMLSource):
attribute = NotImplemented
def _get_tree(self):
- return gocept.lxml.objectify.fromstring(
+ return lxml.etree.fromstring(
"""\
diff --git a/core/src/zeit/cms/content/tests/test_util.py b/core/src/zeit/cms/content/tests/test_util.py
index dd305f0a26..78e617f100 100644
--- a/core/src/zeit/cms/content/tests/test_util.py
+++ b/core/src/zeit/cms/content/tests/test_util.py
@@ -1,6 +1,8 @@
# coding: utf8
import unittest
+from ..util import etree_soup_fromstring
+
YOUTUBE = """\
@@ -103,11 +103,11 @@ Assigning the same content object again doesn't change the xml:
-
-
-
-
-
+
+
+
+
+
@@ -128,13 +128,13 @@ Let's add another related content:
...
-
+
...
...
-
+
...
@@ -165,17 +165,17 @@ So far nothing has changed:
...
-
-
-
+
+
+
...
...
-
-
-
+
+
+
...
@@ -196,16 +196,16 @@ when we reassign the two objects the metadata will be reflected in the xml:
type="intern" href="http://xml.zeit.de/related"
...year="2006" issue="19"...>
...
- I relate the title
- Dude.
- Dude.
+ I relate the title
+ Dude.
+ Dude.
...
...
-
+
...
-
+
...
@@ -236,16 +236,16 @@ After sending the event the metadata is updated:
type="intern" href="http://xml.zeit.de/related"
...year="2007" issue="19"...>
...
- I relate the title
- Dude.
- Dude.
+ I relate the title
+ Dude.
+ Dude.
...
...
-
-
-
+
+
+
...
diff --git a/core/src/zeit/cms/tagging/tag.py b/core/src/zeit/cms/tagging/tag.py
index bebc0aeeb2..42ab6ae0b8 100644
--- a/core/src/zeit/cms/tagging/tag.py
+++ b/core/src/zeit/cms/tagging/tag.py
@@ -115,14 +115,25 @@ def veto_tagging_properties(event):
def add_ranked_tags_to_head(content):
tagger = zeit.cms.tagging.interfaces.ITagger(content, None)
+ if tagger:
+ tags = zope.security.proxy.removeSecurityProxy(tagger).to_xml()
+ else:
+ tags = None
+
xml = zope.security.proxy.removeSecurityProxy(content.xml)
- if tagger and xml.find('head') is not None:
- xml.head.rankedTags = zope.security.proxy.removeSecurityProxy(tagger).to_xml()
+ head = xml.find('head')
+ if head is None:
+ return
+
+ existing = head.find('rankedTags')
+ if tags is not None:
+ if existing is not None:
+ head.replace(existing, tags)
+ else:
+ head.append(tags)
else:
- try:
- del xml.head.rankedTags
- except AttributeError:
- pass
+ if existing is not None:
+ head.remove(existing)
@grok.subscribe(
diff --git a/core/src/zeit/cms/tagging/testing.py b/core/src/zeit/cms/tagging/testing.py
index c614851875..05ede19370 100644
--- a/core/src/zeit/cms/tagging/testing.py
+++ b/core/src/zeit/cms/tagging/testing.py
@@ -1,7 +1,7 @@
from unittest import mock
import collections
-import lxml.objectify
+import lxml.builder
import zope.component
import zope.interface
@@ -149,7 +149,7 @@ def links(self):
return {x.uniqueId: live_prefix + x.link for x in self.values() if x.link}
def to_xml(self):
- node = lxml.objectify.E.tags(*[lxml.objectify.E.tag(x.label) for x in self.values()])
+ node = lxml.builder.E.tags(*[lxml.builder.E.tag(x.label) for x in self.values()])
return node
diff --git a/core/src/zeit/cms/tagging/tests/test_tag.py b/core/src/zeit/cms/tagging/tests/test_tag.py
index 815a6cf836..b4a9ad1ab3 100644
--- a/core/src/zeit/cms/tagging/tests/test_tag.py
+++ b/core/src/zeit/cms/tagging/tests/test_tag.py
@@ -132,12 +132,12 @@ def test_copies_tags_to_head(self):
pass
self.assertEllipsis(
'...foo...',
- lxml.etree.tostring(self.repository['testcontent'].xml.head, encoding=str),
+ lxml.etree.tostring(self.repository['testcontent'].xml.find('head'), encoding=str),
)
def test_leaves_xml_without_head_alone(self):
content = self.repository['testcontent']
- content.xml.remove(content.xml.head)
+ content.xml.remove(content.xml.find('head'))
self.setup_tags('foo')
with self.assertNothingRaised():
# Need to fake checkin, since other handlers re-create the .
diff --git a/core/src/zeit/cms/testcontenttype/README.txt b/core/src/zeit/cms/testcontenttype/README.txt
index 1c9057bc00..1dddedc11d 100644
--- a/core/src/zeit/cms/testcontenttype/README.txt
+++ b/core/src/zeit/cms/testcontenttype/README.txt
@@ -22,7 +22,7 @@ Instantiate and verify the inital xml:
>>> content.__parent__ = repository
>>> print(zeit.cms.testing.xmltotext(content.xml))
-
+
diff --git a/core/src/zeit/cms/testcontenttype/testcontenttype.py b/core/src/zeit/cms/testcontenttype/testcontenttype.py
index 7f0be87ddd..c9ed3a8db1 100644
--- a/core/src/zeit/cms/testcontenttype/testcontenttype.py
+++ b/core/src/zeit/cms/testcontenttype/testcontenttype.py
@@ -12,10 +12,7 @@
class ExampleContentType(zeit.cms.content.metadata.CommonMetadata):
"""A type for testing."""
- default_template = (
- ''
- ''
- )
+ default_template = ''
class ExampleContentTypeType(zeit.cms.type.XMLContentTypeDeclaration):
diff --git a/core/src/zeit/cms/testing.py b/core/src/zeit/cms/testing.py
index 8d5d81634f..753b59865d 100644
--- a/core/src/zeit/cms/testing.py
+++ b/core/src/zeit/cms/testing.py
@@ -1215,7 +1215,31 @@ def clock(dt=None):
def xmltotext(xml):
- return lxml.etree.tostring(xml, pretty_print=True, encoding=str)
+ xml = copy.deepcopy(xml)
+ indent(xml)
+ return lxml.etree.tostring(xml, encoding=str)
+
+
+INDENT = 2 * ' '
+
+
+def indent(node, level=1):
+ """Backport of lxml.etree.indent, introduced in 4.5.0"""
+ _indent = '\n' + INDENT * level
+
+ children = list(node)
+ if not node.text and children:
+ node.text = _indent
+
+ count = len(children)
+ for i, child in enumerate(children):
+ if list(child):
+ indent(child, level + 1)
+ if not child.tail:
+ if i == count - 1:
+ child.tail = '\n' + INDENT * (level - 1)
+ else:
+ child.tail = _indent
class Trace:
diff --git a/core/src/zeit/connector/connector.py b/core/src/zeit/connector/connector.py
index 62f886203a..a2409f4248 100644
--- a/core/src/zeit/connector/connector.py
+++ b/core/src/zeit/connector/connector.py
@@ -10,7 +10,7 @@
import urllib.parse
import gocept.cache.property
-import gocept.lxml.objectify
+import lxml.etree
import pytz
import zope.cachedescriptors.property
import zope.interface
@@ -616,38 +616,34 @@ def _get_dav_lock(self, id):
if not lockdiscovery:
return {}
- lock_info = gocept.lxml.objectify.fromstring(lockdiscovery)
- davlock = {}
+ lock_info = lxml.etree.fromstring(lockdiscovery)
+ lockinfo_node = lock_info.find('{DAV:}activelock')
+ if lockinfo_node is None:
+ return {}
- try:
- lockinfo_node = lock_info.activelock
- except AttributeError:
- pass
+ davlock = {}
+ owner = lockinfo_node.find('{DAV:}owner')
+ davlock['owner'] = owner.text if owner is not None else None
+ # We get timeout in "Second-1337" format. Extract, add to ref time
+ timeout = getattr(lockinfo_node.find('{DAV:}timeout'), 'text', None)
+ if not timeout:
+ timeout = None
+ elif timeout == 'Infinity':
+ timeout = TIME_ETERNITY
else:
- try:
- davlock['owner'] = str(lockinfo_node['{DAV:}owner'])
- except AttributeError:
- davlock['owner'] = None
- # We get timeout in "Second-1337" format. Extract, add to ref time
- timeout = lockinfo_node.timeout
- if not timeout:
- timeout = None
- elif timeout == 'Infinity':
+ m = re.match(r'second-(\d+)', timeout, re.I)
+ if m is None:
+ # Better too much than not enough
timeout = TIME_ETERNITY
else:
- m = re.match(r'second-(\d+)', str(timeout), re.I)
- if m is None:
- # Better too much than not enough
- timeout = TIME_ETERNITY
- else:
- reftime = self[id].properties.get(('cached-time', 'INTERNAL'))
- if not isinstance(reftime, datetime.datetime):
- # XXX untested
- reftime = datetime.datetime.now(pytz.UTC)
- timeout = reftime + datetime.timedelta(seconds=int(m.group(1)))
- davlock['timeout'] = timeout
-
- davlock['locktoken'] = str(lockinfo_node.locktoken.href)
+ reftime = self[id].properties.get(('cached-time', 'INTERNAL'))
+ if not isinstance(reftime, datetime.datetime):
+ # XXX untested
+ reftime = datetime.datetime.now(pytz.UTC)
+ timeout = reftime + datetime.timedelta(seconds=int(m.group(1)))
+ davlock['timeout'] = timeout
+
+ davlock['locktoken'] = lockinfo_node.find('{DAV:}locktoken/{DAV:}href').text
return davlock
@staticmethod
diff --git a/core/src/zeit/connector/testcontent/online/2022/08/kaenguru-comics-folge-448 b/core/src/zeit/connector/testcontent/online/2022/08/kaenguru-comics-folge-448
index a796da6be7..64dc1f5034 100644
--- a/core/src/zeit/connector/testcontent/online/2022/08/kaenguru-comics-folge-448
+++ b/core/src/zeit/connector/testcontent/online/2022/08/kaenguru-comics-folge-448
@@ -64,7 +64,7 @@
{urn:uuid:0b4182a1-c68a-4c33-baac-6c2c43a06a2d}
1
2022
- <pickle>
+ <pickle>
<initialized_object>
<klass>
<global name="Provides" module="zope.interface.declarations"/>
diff --git a/core/src/zeit/connector/testcontent/online/2022/08/trockenheit b/core/src/zeit/connector/testcontent/online/2022/08/trockenheit
index 9241105191..364d697b65 100644
--- a/core/src/zeit/connector/testcontent/online/2022/08/trockenheit
+++ b/core/src/zeit/connector/testcontent/online/2022/08/trockenheit
@@ -48,7 +48,7 @@
urn:newsml:dpa.com:20090101:220809-99-323096
{urn:uuid:70be866f-1aac-422a-a38e-936722358e1c}
2
- <pickle>
+ <pickle>
<initialized_object>
<klass>
<global name="Provides" module="zope.interface.declarations"/>
diff --git a/core/src/zeit/connector/testcontent/zeit-magazin/wochenmarkt/rezept b/core/src/zeit/connector/testcontent/zeit-magazin/wochenmarkt/rezept
index a938618fde..ca8f5530b4 100644
--- a/core/src/zeit/connector/testcontent/zeit-magazin/wochenmarkt/rezept
+++ b/core/src/zeit/connector/testcontent/zeit-magazin/wochenmarkt/rezept
@@ -1,75 +1,76 @@
-
+
-
- © Privat
+
+ © Privat
- Eva Biringer
+ Eva Biringer
- Linguine con Ricotta, Pistacchi e Mandorle
- © Juri Gottschall
+ Linguine con Ricotta, Pistacchi e Mandorle
+ © Juri Gottschall
- Kochrezept
- Coronavirus
- Quarantäne
- Social Distancing
- Rezept
- Mahlzeit
- kochen
+ Kochrezept
+ Coronavirus
+ Quarantäne
+ Social Distancing
+ Rezept
+ Mahlzeit
+ kochen
- abo
- no
- Eva Biringer
- yes
- yes
- zeit-magazin essen-trinken
- yes
- no
- no
- yes
- 2020-04-14T09:19:59.618155+00:00
- 2020-05-19T13:55:34.458425+00:00
- 2020-05-19T13:55:32.271882+00:00
- 2020-04-14T09:19:59.618155+00:00
- yes
- F
- rezept-vorstellung
- no
- leinwand
- no
- no
- no
- 2020-04-14T09:19:57.550528+00:00
- zope.manager
- yes
- no
- http://xml.zeit.de/zeit-magazin/essen-trinken/2020-04/75c29e83-03eb-4a62-ab6f-00ca33e50926.tmp
- ZEDE
- 20a0364c062e421886b9a1a2e3382bf1
- yes
- no
- zeit-magazin
- yes
- Na, Lust auf was Herzhaftes nach all den Schokoeiern und Hefezöpfen?
- yes
- essen-trinken
- article
- 6437
- no
- article
- no
- {urn:uuid:16e82986-cdc0-492d-84e8-267d09b4ab53}
- 16
- 2020
- <pickle>
+ abo
+ no
+ Eva Biringer
+ yes
+ yes
+ zeit-magazin essen-trinken
+ yes
+ no
+ no
+ yes
+ 2020-05-19T12:45:06.384089+00:00
+ 2020-04-14T09:19:59.618155+00:00
+ 2020-05-19T13:55:34.458425+00:00
+ 2020-05-19T13:55:32.271882+00:00
+ 2020-04-14T09:19:59.618155+00:00
+ yes
+ F
+ rezept-vorstellung
+ no
+ leinwand
+ no
+ no
+ no
+ 2020-04-14T09:19:57.550528+00:00
+ zope.manager
+ yes
+ no
+ http://xml.zeit.de/zeit-magazin/essen-trinken/2020-04/75c29e83-03eb-4a62-ab6f-00ca33e50926.tmp
+ ZEDE
+ 20a0364c062e421886b9a1a2e3382bf1
+ yes
+ no
+ zeit-magazin
+ yes
+ Na, Lust auf was Herzhaftes nach all den Schokoeiern und Hefezöpfen?
+ yes
+ essen-trinken
+ article
+ 6437
+ no
+ article
+ no
+ {urn:uuid:16e82986-cdc0-492d-84e8-267d09b4ab53}
+ 16
+ 2020
+ <pickle>
<initialized_object>
<klass>
<global name="Provides" module="zope.interface.declarations"/>
@@ -90,8 +91,8 @@
Ist genug Brot und Kuchen gebacken, bleibt endlich wieder Zeit, zu kochen. Mit diesen One-Pot-Gerichten können Sie den Zuckerschock vom Osterwochenende kontern.
- Linguine con Ricotta, Pistacchi e Mandorle
- © Juri Gottschall
+ Linguine con Ricotta, Pistacchi e Mandorle
+ © Juri Gottschall
Als Beilage passt gedämpfter Brokkoli.
@@ -104,8 +105,8 @@ Als Beilage passt gedämpfter Brokkoli.
- Powidltascherl mit Haselnussbröseln
- © Sonja Planeta
+ Powidltascherl mit Haselnussbröseln
+ © Sonja Planeta
Zubereitung:
diff --git a/core/src/zeit/connector/tests/test_connector.py b/core/src/zeit/connector/tests/test_connector.py
index c71aace5aa..e37ca51c9c 100755
--- a/core/src/zeit/connector/tests/test_connector.py
+++ b/core/src/zeit/connector/tests/test_connector.py
@@ -4,6 +4,7 @@
from unittest import mock
import unittest
+import lxml.etree
import pytz
import transaction
import zope.component
@@ -338,13 +339,11 @@ def test_xml_strings_should_be_storable(self):
self.assertEqual('', res.properties[('foo', 'bar')])
def test_xml_properties_are_returned_with_surrounding_tag(self):
- import lxml.objectify
-
res = self.get_resource('xmltest', '')
res.properties[('foo', 'bar')] = ''
self.connector.add(res)
res = self.connector[res.id]
- prop = lxml.objectify.fromstring(res.properties[('foo', 'bar')])
+ prop = lxml.etree.fromstring(res.properties[('foo', 'bar')])
self.assertEqual(['a'], [n.tag for n in prop.getchildren()])
diff --git a/core/src/zeit/content/advertisement/advertisement.py b/core/src/zeit/content/advertisement/advertisement.py
index 91d51f0315..7391c9486c 100644
--- a/core/src/zeit/content/advertisement/advertisement.py
+++ b/core/src/zeit/content/advertisement/advertisement.py
@@ -13,10 +13,7 @@
zeit.content.advertisement.interfaces.IAdvertisement, zeit.cms.interfaces.IEditorialContent
)
class Advertisement(zeit.cms.content.xmlsupport.XMLContentBase):
- default_template = (
- ''
- '
'
- )
+ default_template = '