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 = """\