• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

zopefoundation / Products.GenericSetup / 5104175550

pending completion
5104175550

push

github

web-flow
Merge pull request #128 from zopefoundation/serialize_utility_registration_global

Prefer `component` (over  `factory`) in the serialization of utility registrations

1668 of 1988 branches covered (83.9%)

Branch coverage included in aggregate %.

26 of 26 new or added lines in 3 files covered. (100.0%)

9340 of 9819 relevant lines covered (95.12%)

0.95 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

82.19
/src/Products/GenericSetup/utils.py
1
##############################################################################
2
#
3
# Copyright (c) 2004 Zope Foundation and Contributors.
4
#
5
# This software is subject to the provisions of the Zope Public License,
6
# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
7
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
8
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
9
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
10
# FOR A PARTICULAR PURPOSE.
11
#
12
##############################################################################
13
""" GenericSetup product utilities
1✔
14
"""
15

16
import hashlib
1✔
17
import os
1✔
18
from html import escape
1✔
19
from inspect import getdoc
1✔
20
from logging import getLogger
1✔
21
from xml.dom.minidom import Document
1✔
22
from xml.dom.minidom import Element
1✔
23
from xml.dom.minidom import Node
1✔
24
from xml.dom.minidom import _nssplit
1✔
25
from xml.dom.minidom import parseString
1✔
26
from xml.parsers.expat import ExpatError
1✔
27

28
from AccessControl.class_init import InitializeClass
1✔
29
from AccessControl.SecurityInfo import ClassSecurityInfo
1✔
30
from Acquisition import Implicit
1✔
31
from App.Common import package_home
1✔
32
from OFS.interfaces import IOrderedContainer
1✔
33
from Products.Five.utilities.interfaces import IMarkerInterfaces
1✔
34
from zope.component import queryMultiAdapter
1✔
35
from zope.configuration.name import resolve
1✔
36
from zope.interface import directlyProvides
1✔
37
from zope.interface import implementer
1✔
38
from zope.interface import implementer_only
1✔
39
from ZPublisher.Converters import type_converters
1✔
40
from ZPublisher.HTTPRequest import default_encoding
1✔
41

42
from .exceptions import BadRequest
1✔
43
from .interfaces import IBody
1✔
44
from .interfaces import INode
1✔
45
from .interfaces import ISetupTool
1✔
46
from .permissions import ManagePortal
1✔
47

48

49
_pkgdir = package_home(globals())
1✔
50
_wwwdir = os.path.join(_pkgdir, 'www')
1✔
51
_xmldir = os.path.join(_pkgdir, 'xml_templates')
1✔
52

53
# Please note that these values may change. Always import
54
# the values from here instead of using the values directly.
55
CONVERTER, DEFAULT, KEY = 1, 2, 3
1✔
56
I18NURI = 'http://xml.zope.org/namespaces/i18n'
1✔
57

58
# If we have type converters for lines and string, which should be always,
59
# then we may need to call these converters on Zope 5.3 and higher.
60
# This is because since Zope 5.3, the lines converter gives
61
# text instead of bytes.
62
# See https://github.com/zopefoundation/Products.GenericSetup/issues/109
63
if (
1!
64
    "lines" in type_converters
65
    and "string" in type_converters
66
    and isinstance(type_converters["lines"]("blah")[0], str)
67
):
68
    LINES_HAS_TEXT = True
1✔
69
else:
70
    # Older Zope
71
    LINES_HAS_TEXT = False
×
72

73

74
def _getDottedName(named):
1✔
75

76
    if isinstance(named, str):
1✔
77
        return str(named)
1✔
78

79
    try:
1✔
80
        dotted = f'{named.__module__}.{named.__name__}'
1✔
81
    except AttributeError:
1✔
82
        raise ValueError('Cannot compute dotted name: %s' % named)
1✔
83

84
    # remove leading underscore names if possible
85

86
    # Step 1: check if there is a short version
87
    short_dotted = '.'.join([n for n in dotted.split('.')
1✔
88
                             if not n.startswith('_')])
89
    if short_dotted == dotted:
1✔
90
        return dotted
1✔
91

92
    # Step 2: check if short version can be resolved
93
    try:
1✔
94
        short_resolved = _resolveDottedName(short_dotted)
1✔
95
    except (ValueError, ImportError):
×
96
        return dotted
×
97

98
    # Step 3: check if long version resolves to the same object
99
    try:
1✔
100
        resolved = _resolveDottedName(dotted)
1✔
101
    except (ValueError, ImportError):
×
102
        raise ValueError('Cannot compute dotted name: %s' % named)
×
103
    if short_resolved is not resolved:
1!
104
        return dotted
1✔
105

106
    return short_dotted
×
107

108

109
def _resolveDottedName(dotted):
1✔
110
    __traceback_info__ = dotted
1✔
111

112
    try:
1✔
113
        return resolve(dotted)
1✔
114
    except ModuleNotFoundError:
1✔
115
        return
1✔
116

117

118
def _isGlobalObject(obj):
1✔
119
    """Is *obj* identified by a dotted name?"""
120
    try:
1✔
121
        dn = _getDottedName(obj)
1✔
122
        return obj is _resolveDottedName(dn)
1✔
123
    except Exception:
×
124
        return False
×
125

126

127
def _extractDocstring(func, default_title, default_description):
1✔
128
    try:
1✔
129
        doc = getdoc(func)
1✔
130
        lines = doc.split('\n')
1✔
131

132
    except AttributeError:
1✔
133

134
        title = default_title
1✔
135
        description = default_description
1✔
136

137
    else:
138
        title = lines[0]
1✔
139

140
        if len(lines) > 1 and lines[1].strip() == '':
1!
141
            del lines[1]
1✔
142

143
        description = '\n'.join(lines[1:])
1✔
144

145
    return title, description
1✔
146

147

148
def _version_for_print(version):
1✔
149
    """Return a version suitable for printing/logging.
150

151
    Versions of profiles and destinations of upgrade steps
152
    are likely tuples.  We join them with dots.
153

154
    Used internally when logging.
155
    """
156
    if isinstance(version, str):
1✔
157
        return version
1✔
158
    if isinstance(version, tuple):
1✔
159
        return ".".join(version)
1✔
160
    return str(version)
1✔
161

162

163
##############################################################################
164
# WARNING: PLEASE DON'T USE THE CONFIGURATOR PATTERN. THE RELATED BASE CLASSES
165
# WILL BECOME DEPRECATED AS SOON AS GENERICSETUP ITSELF NO LONGER USES THEM.
166

167
class ImportConfiguratorBase(Implicit):
1✔
168
    # old code, will become deprecated
169
    """ Synthesize data from XML description.
170
    """
171
    security = ClassSecurityInfo()
1✔
172
    security.setDefaultAccess('allow')
1✔
173

174
    def __init__(self, site, encoding='utf-8'):
1✔
175

176
        self._site = site
1✔
177
        self._encoding = None
1✔
178

179
    @security.protected(ManagePortal)
1✔
180
    def parseXML(self, xml):
1✔
181
        """ Pseudo API.
182
        """
183
        reader = getattr(xml, 'read', None)
1✔
184

185
        if reader is not None:
1!
186
            xml = reader()
×
187

188
        if isinstance(xml, bytes):
1✔
189
            xml = xml.decode('utf-8')
1✔
190

191
        dom = parseString(xml)
1✔
192
        root = dom.documentElement
1✔
193

194
        return self._extractNode(root)
1✔
195

196
    def _extractNode(self, node):
1✔
197
        """ Please see docs/configurator.txt for information about the
198
        import mapping syntax.
199
        """
200
        nodes_map = self._getImportMapping()
1✔
201
        if node.nodeName not in nodes_map:
1!
202
            nodes_map = self._getSharedImportMapping()
×
203
            if node.nodeName not in nodes_map:
×
204
                raise ValueError('Unknown node: %s' % node.nodeName)
×
205
        node_map = nodes_map[node.nodeName]
1✔
206
        info = {}
1✔
207

208
        for name, val in node.attributes.items():
1✔
209
            key = node_map[name].get(KEY, str(name))
1✔
210
            val = self._encoding and val.encode(self._encoding) or val
1✔
211
            info[key] = val
1✔
212

213
        for child in node.childNodes:
1✔
214
            name = child.nodeName
1✔
215

216
            if name == '#comment':
1!
217
                continue
×
218

219
            if not name == '#text':
1✔
220
                key = node_map[name].get(KEY, str(name))
1✔
221
                info[key] = info.setdefault(key, ()) + (
1✔
222
                                                    self._extractNode(child),)
223

224
            elif '#text' in node_map:
1✔
225
                key = node_map['#text'].get(KEY, 'value')
1✔
226
                val = child.nodeValue.lstrip()
1✔
227
                val = self._encoding and val.encode(self._encoding) or val
1✔
228
                info[key] = info.setdefault(key, '') + val
1✔
229

230
        for k, v in node_map.items():
1✔
231
            key = v.get(KEY, k)
1✔
232

233
            if DEFAULT in v and key not in info:
1✔
234
                if isinstance(v[DEFAULT], str):
1!
235
                    info[key] = v[DEFAULT] % info
×
236
                else:
237
                    info[key] = v[DEFAULT]
1✔
238

239
            elif CONVERTER in v and key in info:
1✔
240
                info[key] = v[CONVERTER](info[key])
1✔
241

242
            if key is None:
1✔
243
                info = info[key]
1✔
244

245
        return info
1✔
246

247
    def _getSharedImportMapping(self):
1✔
248

249
        return {
×
250
          'object': {'i18n:domain': {},
251
                     'name': {KEY: 'id'},
252
                     'meta_type': {},
253
                     'insert-before': {},
254
                     'insert-after': {},
255
                     'property': {KEY: 'properties', DEFAULT: ()},
256
                     'object': {KEY: 'objects', DEFAULT: ()},
257
                     'xmlns:i18n': {}},
258
          'property': {'name': {KEY: 'id'},
259
                       '#text': {KEY: 'value', DEFAULT: ''},
260
                       'element': {KEY: 'elements', DEFAULT: ()},
261
                       'type': {},
262
                       'select_variable': {},
263
                       'i18n:translate': {}},
264
          'element': {'value': {KEY: None}},
265
          'description': {'#text': {KEY: None, DEFAULT: ''}}}
266

267
    def _convertToBoolean(self, val):
1✔
268

269
        return val.lower() in ('true', 'yes', '1')
1✔
270

271
    def _convertToUnique(self, val):
1✔
272

273
        assert len(val) == 1
1✔
274
        return val[0]
1✔
275

276

277
InitializeClass(ImportConfiguratorBase)
1✔
278

279

280
class ExportConfiguratorBase(Implicit):
1✔
281
    # old code, will become deprecated
282
    """ Synthesize XML description.
283
    """
284
    security = ClassSecurityInfo()
1✔
285
    security.setDefaultAccess('allow')
1✔
286

287
    def __init__(self, site, encoding='utf-8'):
1✔
288

289
        self._site = site
1✔
290
        self._encoding = encoding
1✔
291
        self._template = self._getExportTemplate()
1✔
292

293
    @security.protected(ManagePortal)
1✔
294
    def generateXML(self, **kw):
1✔
295
        """ Pseudo API.
296
        """
297
        return self._template(**kw)
1✔
298

299

300
InitializeClass(ExportConfiguratorBase)
1✔
301

302
#
303
##############################################################################
304

305

306
class _LineWrapper:
1✔
307

308
    def __init__(self, writer, indent, addindent, newl, max):
1✔
309
        self._writer = writer
1✔
310
        self._indent = indent
1✔
311
        self._addindent = addindent
1✔
312
        self._newl = newl
1✔
313
        self._max = max
1✔
314
        self._length = 0
1✔
315
        self._queue = self._indent
1✔
316

317
    def queue(self, text):
1✔
318
        self._queue += text
1✔
319

320
    def write(self, text='', enforce=False):
1✔
321
        self._queue += text
1✔
322

323
        if 0 < self._length > self._max - len(self._queue):
1✔
324
            self._writer.write(self._newl)
1✔
325
            self._length = 0
1✔
326
            self._queue = f'{self._indent}{self._addindent} {self._queue}'
1✔
327

328
        if self._queue != self._indent:
1✔
329
            self._writer.write(self._queue)
1✔
330
            self._length += len(self._queue)
1✔
331
            self._queue = ''
1✔
332

333
        if 0 < self._length and enforce:
1✔
334
            self._writer.write(self._newl)
1✔
335
            self._length = 0
1✔
336
            self._queue = self._indent
1✔
337

338

339
class _Element(Element):
1✔
340

341
    """minidom element with 'pretty' XML output.
342
    """
343

344
    def writexml(self, writer, indent="", addindent="", newl=""):
1✔
345
        # indent = current indentation
346
        # addindent = indentation to add to higher levels
347
        # newl = newline string
348
        wrapper = _LineWrapper(writer, indent, addindent, newl, 78)
1✔
349
        wrapper.write('<%s' % self.tagName)
1✔
350

351
        # move 'name', 'meta_type' and 'title' to the top, sort the rest
352
        attrs = self._get_attributes()
1✔
353
        a_names = sorted(attrs.keys())
1✔
354
        if 'title' in a_names:
1!
355
            a_names.remove('title')
×
356
            a_names.insert(0, 'title')
×
357
        if 'meta_type' in a_names:
1✔
358
            a_names.remove('meta_type')
1✔
359
            a_names.insert(0, 'meta_type')
1✔
360
        if 'name' in a_names:
1✔
361
            a_names.remove('name')
1✔
362
            a_names.insert(0, 'name')
1✔
363

364
        for a_name in a_names:
1✔
365
            wrapper.write()
1✔
366
            a_value = attrs[a_name].value
1✔
367
            if a_value is None:
1✔
368
                a_value = ""
1✔
369
            else:
370
                a_value = escape(a_value, quote=True)
1✔
371

372
            wrapper.queue(f' {a_name}="{a_value}"')
1✔
373

374
        if self.childNodes:
1✔
375
            wrapper.queue('>')
1✔
376
            for node in self.childNodes:
1✔
377
                if node.nodeType == Node.TEXT_NODE:
1✔
378
                    data = escape(node.data)
1✔
379
                    textlines = data.splitlines()
1✔
380
                    if textlines:
1✔
381
                        wrapper.queue(textlines.pop(0))
1✔
382
                    if textlines:
1✔
383
                        for textline in textlines:
1✔
384
                            wrapper.write('', True)
1✔
385
                            wrapper.queue(f'{addindent}{textline}')
1✔
386
                else:
387
                    wrapper.write('', True)
1✔
388
                    node.writexml(writer, indent + addindent, addindent, newl)
1✔
389
            wrapper.write('</%s>' % self.tagName, True)
1✔
390
        else:
391
            wrapper.write('/>', True)
1✔
392

393

394
class PrettyDocument(Document):
1✔
395

396
    """minidom document with 'pretty' XML output.
397
    """
398

399
    def createElement(self, tagName):
1✔
400
        e = _Element(tagName)
1✔
401
        e.ownerDocument = self
1✔
402
        return e
1✔
403

404
    def createElementNS(self, namespaceURI, qualifiedName):
1✔
405
        prefix, _localName = _nssplit(qualifiedName)
×
406
        e = _Element(qualifiedName, namespaceURI, prefix)
×
407
        e.ownerDocument = self
×
408
        return e
×
409

410
    def writexml(self, writer, indent="", addindent="", newl="",
1✔
411
                 encoding='utf-8', standalone=None):
412
        # `standalone` was added in Python 3.9 but is ignored here
413
        if encoding is None:
1!
414
            writer.write('<?xml version="1.0"?>\n')
×
415
        else:
416
            writer.write('<?xml version="1.0" encoding="%s"?>\n' % encoding)
1✔
417
        for node in self.childNodes:
1✔
418
            node.writexml(writer, indent, addindent, newl)
1✔
419

420
    def toprettyxml(self, indent='\t', newl='\n', encoding='utf-8'):
1✔
421
        return super().toprettyxml(indent, newl, encoding)
1✔
422

423

424
@implementer(INode)
1✔
425
class NodeAdapterBase:
1✔
426

427
    """Node im- and exporter base.
428
    """
429

430
    _encoding = 'utf-8'
1✔
431
    _LOGGER_ID = ''
1✔
432

433
    def __init__(self, context, environ):
1✔
434
        self.context = context
1✔
435
        self.environ = environ
1✔
436
        self._logger = environ.getLogger(self._LOGGER_ID)
1✔
437
        self._doc = PrettyDocument()
1✔
438

439
    def _getObjectNode(self, name, i18n=True):
1✔
440
        node = self._doc.createElement(name)
1✔
441
        node.setAttribute('name', self.context.getId())
1✔
442
        node.setAttribute('meta_type', self.context.meta_type)
1✔
443
        i18n_domain = getattr(self.context, 'i18n_domain', None)
1✔
444
        if i18n and i18n_domain:
1!
445
            node.setAttributeNS(I18NURI, 'i18n:domain', i18n_domain)
×
446
            self._i18n_props = ('title', 'description')
×
447
        return node
1✔
448

449
    def _getNodeText(self, node):
1✔
450
        text = ''
1✔
451
        for child in node.childNodes:
1✔
452
            if child.nodeName != '#text':
1!
453
                continue
×
454
            lines = [line.lstrip() for line in child.nodeValue.splitlines()]
1✔
455
            text += '\n'.join(lines)
1✔
456
        return text
1✔
457

458
    def _convertToBoolean(self, val):
1✔
459
        return val.lower() in ('true', 'yes', '1')
1✔
460

461

462
@implementer_only(IBody)
1✔
463
class BodyAdapterBase(NodeAdapterBase):
1✔
464

465
    """Body im- and exporter base.
466
    """
467

468
    def _exportSimpleNode(self):
1✔
469
        """Export the object as a DOM node.
470
        """
471
        if ISetupTool.providedBy(self.context):
×
472
            return None
×
473
        return self._getObjectNode('object', False)
×
474

475
    def _importSimpleNode(self, node):
1✔
476
        """Import the object from the DOM node.
477
        """
478

479
    node = property(_exportSimpleNode, _importSimpleNode)
1✔
480

481
    def _exportBody(self):
1✔
482
        """Export the object as a file body.
483
        """
484
        return b''
×
485

486
    def _importBody(self, body):
1✔
487
        """Import the object from the file body.
488
        """
489

490
    body = property(_exportBody, _importBody)
1✔
491

492
    mime_type = 'text/plain'
1✔
493

494
    name = ''
1✔
495

496
    suffix = ''
1✔
497

498

499
@implementer_only(IBody)
1✔
500
class XMLAdapterBase(BodyAdapterBase):
1✔
501

502
    """XML im- and exporter base.
503
    """
504

505
    def _exportBody(self):
1✔
506
        """Export the object as a file body.
507
        """
508
        self._doc.appendChild(self._exportNode())
1✔
509
        xml = self._doc.toprettyxml(' ', encoding=self._encoding)
1✔
510
        self._doc.unlink()
1✔
511
        return xml
1✔
512

513
    def _importBody(self, body):
1✔
514
        """Import the object from the file body.
515
        """
516
        try:
1✔
517
            dom = parseString(body)
1✔
518
        except ExpatError as e:
×
519
            filename = (self.filename or
×
520
                        '/'.join(self.context.getPhysicalPath()))
521
            raise ExpatError(f'{filename}: {e}')
×
522

523
        # Replace the encoding with the one from the XML
524
        self._encoding = dom.encoding or self._encoding
1✔
525
        self._importNode(dom.documentElement)
1✔
526

527
    body = property(_exportBody, _importBody)
1✔
528

529
    mime_type = 'text/xml'
1✔
530

531
    name = ''
1✔
532

533
    suffix = '.xml'
1✔
534

535
    filename = ''  # for error reporting during import
1✔
536

537

538
class ObjectManagerHelpers:
1✔
539

540
    """ObjectManager in- and export helpers.
541
    """
542

543
    def _extractObjects(self):
1✔
544
        fragment = self._doc.createDocumentFragment()
1✔
545
        objects = self.context.objectValues()
1✔
546
        if not IOrderedContainer.providedBy(self.context):
1!
547
            objects = list(objects)
1✔
548
            objects.sort(key=lambda x: x.getId())
1✔
549
        for obj in objects:
1✔
550
            exporter = queryMultiAdapter((obj, self.environ), INode)
1✔
551
            if exporter:
1!
552
                node = exporter.node
1✔
553
                if node is not None:
1!
554
                    fragment.appendChild(exporter.node)
1✔
555
        return fragment
1✔
556

557
    def _purgeObjects(self):
1✔
558
        for obj_id, obj in self.context.objectItems():
1!
559
            if ISetupTool.providedBy(obj):
×
560
                continue
×
561
            self.context._delObject(obj_id)
×
562

563
    def _initObjects(self, node):
1✔
564
        import Products
1✔
565
        for child in node.childNodes:
1✔
566
            if child.nodeName != 'object':
1✔
567
                continue
1✔
568
            if child.hasAttribute('deprecated'):
1!
569
                continue
×
570
            parent = self.context
1✔
571

572
            obj_id = str(child.getAttribute('name'))
1✔
573
            if self._convertToBoolean(child.getAttribute('remove') or 'False'):
1✔
574
                if obj_id in parent.objectIds():
1!
575
                    parent._delObject(obj_id)
1✔
576
                continue
1✔
577

578
            if obj_id not in parent.objectIds():
1✔
579
                meta_type = str(child.getAttribute('meta_type'))
1✔
580
                __traceback_info__ = obj_id, meta_type
1✔
581
                for mt_info in Products.meta_types:
1!
582
                    if mt_info['name'] == meta_type:
1✔
583
                        parent._setObject(obj_id, mt_info['instance'](obj_id))
1✔
584
                        break
1✔
585
                else:
586
                    raise ValueError("unknown meta_type '%s'" % meta_type)
×
587

588
            if child.hasAttribute('insert-before'):
1!
589
                insert_before = child.getAttribute('insert-before')
×
590
                if insert_before == '*':
×
591
                    parent.moveObjectsToTop(obj_id)
×
592
                else:
593
                    try:
×
594
                        position = parent.getObjectPosition(insert_before)
×
595
                        if parent.getObjectPosition(obj_id) < position:
×
596
                            position -= 1
×
597
                        parent.moveObjectToPosition(obj_id, position)
×
598
                    except ValueError:
×
599
                        pass
×
600
            elif child.hasAttribute('insert-after'):
1!
601
                insert_after = child.getAttribute('insert-after')
×
602
                if insert_after == '*':
×
603
                    parent.moveObjectsToBottom(obj_id)
×
604
                else:
605
                    try:
×
606
                        position = parent.getObjectPosition(insert_after)
×
607
                        if parent.getObjectPosition(obj_id) < position:
×
608
                            position -= 1
×
609
                        parent.moveObjectToPosition(obj_id, position + 1)
×
610
                    except ValueError:
×
611
                        pass
×
612

613
            obj = getattr(self.context, obj_id)
1✔
614
            importer = queryMultiAdapter((obj, self.environ), INode)
1✔
615
            if importer:
1✔
616
                importer.node = child
1✔
617

618

619
class PropertyManagerHelpers:
1✔
620

621
    """PropertyManager im- and export helpers.
622

623
      o Derived classes can supply a '_PROPERTIES' scehma, which is then used
624
        to mock up a temporary propertysheet for the object.  The adapter's
625
        methods ('_extractProperties', '_purgeProperties', '_initProperties')
626
        then run against that propertysheet.
627
    """
628
    _PROPERTIES = ()
1✔
629

630
    _encoding = default_encoding
1✔
631

632
    def __init__(self, context, environ):
1✔
633
        from OFS.PropertyManager import PropertyManager
1✔
634
        if not isinstance(context, PropertyManager):
1✔
635
            context = self._fauxAdapt(context)
1✔
636

637
        super().__init__(context, environ)
1✔
638

639
    def _fauxAdapt(self, context):
1✔
640
        from OFS.PropertySheets import PropertySheet
1✔
641

642
        class Adapted(PropertySheet):
1✔
643
            def __init__(self, real, properties):
1✔
644
                self._real = real
1✔
645
                self._properties = properties
1✔
646

647
            def p_self(self):
1✔
648
                return self
1✔
649

650
            def v_self(self):
1✔
651
                return self._real
1✔
652

653
            def propdict(self):
1✔
654
                # PropertyManager method used by _initProperties
655
                return {p['id']: p for p in self._properties}
1✔
656

657
        return Adapted(context, self._PROPERTIES)
1✔
658

659
    def _extractProperties(self):
1✔
660
        fragment = self._doc.createDocumentFragment()
1✔
661

662
        for prop_map in self.context._propertyMap():
1✔
663
            prop_id = prop_map['id']
1✔
664
            if prop_id == 'i18n_domain':
1!
665
                continue
×
666

667
            # Don't export read-only nodes
668
            if 'w' not in prop_map.get('mode', 'wd'):
1✔
669
                continue
1✔
670

671
            node = self._doc.createElement('property')
1✔
672
            node.setAttribute('name', prop_id)
1✔
673

674
            prop = self.context.getProperty(prop_id)
1✔
675
            if prop is None:
1✔
676
                continue
1✔
677
            if isinstance(prop, (tuple, list)):
1✔
678
                for value in prop:
1✔
679
                    if isinstance(value, bytes):
1✔
680
                        value = value.decode(self._encoding)
1✔
681
                    child = self._doc.createElement('element')
1✔
682
                    child.setAttribute('value', value)
1✔
683
                    node.appendChild(child)
1✔
684
            else:
685
                if prop_map.get('type') == 'boolean':
1✔
686
                    prop = str(bool(prop))
1✔
687
                elif prop_map.get('type') == 'date':
1✔
688
                    if prop.timezoneNaive():
1✔
689
                        prop = str(prop).rsplit(None, 1)[0]
1✔
690
                    else:
691
                        prop = str(prop)
1✔
692
                elif isinstance(prop, bytes):
1!
693
                    prop = prop.decode(self._encoding)
×
694
                elif isinstance(prop, ((int,), float)):
1✔
695
                    prop = str(prop)
1✔
696
                elif not isinstance(prop, str):
1!
697
                    prop = prop.decode(self._encoding)
×
698
                child = self._doc.createTextNode(prop)
1✔
699
                node.appendChild(child)
1✔
700

701
            if 'd' in prop_map.get('mode', 'wd') and not prop_id == 'title':
1✔
702
                prop_type = prop_map.get('type', 'string')
1✔
703
                node.setAttribute('type', prop_type)
1✔
704
                select_variable = prop_map.get('select_variable', None)
1✔
705
                if select_variable is not None:
1✔
706
                    node.setAttribute('select_variable', select_variable)
1✔
707

708
            if hasattr(self, '_i18n_props') and prop_id in self._i18n_props:
1!
709
                node.setAttribute('i18n:translate', '')
×
710

711
            fragment.appendChild(node)
1✔
712

713
        return fragment
1✔
714

715
    def _purgeProperties(self):
1✔
716
        for prop_map in self.context._propertyMap():
1✔
717
            mode = prop_map.get('mode', 'wd')
1✔
718
            if 'w' not in mode:
1✔
719
                continue
1✔
720
            prop_id = prop_map['id']
1✔
721
            if 'd' in mode and not prop_id == 'title':
1✔
722
                self.context._delProperty(prop_id)
1✔
723
            else:
724
                prop_type = prop_map.get('type')
1✔
725
                if prop_type == 'multiple selection':
1!
726
                    prop_value = ()
×
727
                elif prop_type in ('int', 'float'):
1✔
728
                    prop_value = 0
1✔
729
                elif prop_type == 'date':
1✔
730
                    prop_value = '1970/01/01 00:00:00 UTC'  # DateTime(0) UTC
1✔
731
                else:
732
                    prop_value = ''
1✔
733
                self.context._updateProperty(prop_id, prop_value)
1✔
734

735
    def _initProperties(self, node):
1✔
736
        obj = self.context
1✔
737
        if node.hasAttribute('i18n:domain'):
1✔
738
            i18n_domain = str(node.getAttribute('i18n:domain'))
1✔
739
            obj._updateProperty('i18n_domain', i18n_domain)
1✔
740
        for child in node.childNodes:
1✔
741
            if child.nodeName != 'property':
1✔
742
                continue
1✔
743
            remove = self._convertToBoolean(
1✔
744
                child.getAttribute('remove') or 'False')
745
            prop_id = str(child.getAttribute('name'))
1✔
746
            prop_map = obj.propdict().get(prop_id, None)
1✔
747

748
            if prop_map is None:
1✔
749
                if remove:
1✔
750
                    continue
1✔
751
                if child.hasAttribute('type'):
1!
752
                    val = str(child.getAttribute('select_variable'))
1✔
753
                    prop_type = str(child.getAttribute('type'))
1✔
754
                    obj._setProperty(prop_id, val, prop_type)
1✔
755
                    prop_map = obj.propdict().get(prop_id, None)
1✔
756
                else:
757
                    raise ValueError("undefined property '%s'" % prop_id)
×
758

759
            if remove:
1✔
760
                if 'd' not in prop_map.get('mode', 'wd'):
1!
761
                    raise BadRequest('%s cannot be deleted' % prop_id)
×
762
                obj._delProperty(prop_id)
1✔
763
                continue
1✔
764

765
            if 'w' not in prop_map.get('mode', 'wd'):
1!
766
                raise BadRequest('%s cannot be changed' % prop_id)
×
767

768
            new_elements = []
1✔
769
            remove_elements = []
1✔
770
            for sub in child.childNodes:
1✔
771
                if sub.nodeName == 'element':
1✔
772
                    value = sub.getAttribute('value')
1✔
773
                    if prop_map.get('type') not in (
1✔
774
                            'ulines', 'multiple selection'):
775
                        value = value.encode(self._encoding)
1✔
776
                    if self._convertToBoolean(sub.getAttribute('remove')
1✔
777
                                              or 'False'):
778
                        remove_elements.append(value)
1✔
779
                        if value in new_elements:
1!
780
                            new_elements.remove(value)
×
781
                    else:
782
                        new_elements.append(value)
1✔
783
                        if value in remove_elements:
1!
784
                            remove_elements.remove(value)
×
785

786
            if LINES_HAS_TEXT and obj.getPropertyType(prop_id) == 'lines':
1✔
787
                # Since Zope 5.3, lines should contain text, not bytes.
788
                # https://github.com/zopefoundation/Products.GenericSetup/issues/109
789
                new_elements = _convert_lines(new_elements, self._encoding)
1✔
790
                remove_elements = _convert_lines(
1✔
791
                    remove_elements, self._encoding)
792

793
            if prop_map.get('type') in ('lines', 'tokens', 'ulines',
1✔
794
                                        'multiple selection'):
795
                prop_value = tuple(new_elements) or ()
1✔
796
            elif prop_map.get('type') == 'boolean':
1✔
797
                prop_value = self._convertToBoolean(self._getNodeText(child))
1✔
798
            else:
799
                # if we pass a *string* to _updateProperty, all other values
800
                # are converted to the right type
801
                prop_value = self._getNodeText(child)
1✔
802

803
            if not self._convertToBoolean(child.getAttribute('purge')
1✔
804
                                          or 'True'):
805
                # If the purge attribute is False, merge sequences
806
                prop = obj.getProperty(prop_id)
1✔
807
                # Before Zope 5.3, lines contained bytes.
808
                # After, they contain text.
809
                # We may need to convert the existing property value first,
810
                # otherwise we may be combining bytes and text.
811
                # See zopefoundation/Products.GenericSetup/issues/109
812
                if LINES_HAS_TEXT and obj.getPropertyType(prop_id) == 'lines':
1!
813
                    prop = _convert_lines(prop, self._encoding)
1✔
814
                if isinstance(prop, (tuple, list)):
1!
815
                    prop_value = (tuple([p for p in prop
1✔
816
                                         if p not in prop_value and
817
                                         p not in remove_elements]) +
818
                                  tuple(prop_value))
819

820
            if isinstance(prop_value, (bytes, str)):
1✔
821
                prop_type = obj.getPropertyType(prop_id) or 'string'
1✔
822
                if prop_type in type_converters:
1✔
823
                    prop_converter = type_converters[prop_type]
1✔
824
                    # The type_converters use the ZPublisher default_encoding
825
                    # for decoding bytes!
826
                    if self._encoding.lower() != default_encoding:
1✔
827
                        prop_value = _de_encode_value(
1✔
828
                            prop_value, self._encoding, prop_converter)
829
                    else:
830
                        prop_value = prop_converter(prop_value)
1✔
831
            obj._updateProperty(prop_id, prop_value)
1✔
832

833

834
def _de_encode_value(prop_value, encoding, converter):
1✔
835
    if isinstance(prop_value, bytes):
1✔
836
        u_prop_value = prop_value.decode(encoding)
1✔
837
        prop_value = u_prop_value.encode(default_encoding)
1✔
838
    prop_value = converter(prop_value)
1✔
839
    if isinstance(prop_value, bytes):
1!
840
        u_prop_value = prop_value.decode(default_encoding)
×
841
        prop_value = u_prop_value.encode(encoding)
×
842
    return prop_value
1✔
843

844

845
def _convert_lines(values, encoding):
1✔
846
    # Only called when LINES_HAS_TEXT is True.
847
    if not isinstance(values, (list, tuple)):
1!
848
        values = values.splitlines()
×
849
    if encoding.lower() == default_encoding:
1✔
850
        converter = type_converters['lines']
1✔
851
        return converter(values)
1✔
852
    # According to the tests, we support non utf-8 encodings like iso-8859-1.
853
    converter = type_converters['string']
1✔
854
    return [
1✔
855
        _de_encode_value(prop_value, encoding, converter)
856
        for prop_value in values
857
    ]
858

859

860
class MarkerInterfaceHelpers:
1✔
861

862
    """Marker interface im- and export helpers.
863
    """
864

865
    def _extractMarkers(self):
1✔
866
        fragment = self._doc.createDocumentFragment()
1✔
867
        adapted = IMarkerInterfaces(self.context)
1✔
868

869
        for marker_id in adapted.getDirectlyProvidedNames():
1✔
870
            node = self._doc.createElement('marker')
1✔
871
            node.setAttribute('name', marker_id)
1✔
872
            fragment.appendChild(node)
1✔
873

874
        return fragment
1✔
875

876
    def _purgeMarkers(self):
1✔
877
        directlyProvides(self.context)
1✔
878

879
    def _initMarkers(self, node):
1✔
880
        markers = []
1✔
881
        adapted = IMarkerInterfaces(self.context)
1✔
882

883
        for child in node.childNodes:
1✔
884
            if child.nodeName != 'marker':
1✔
885
                continue
1✔
886
            markers.append(str(child.getAttribute('name')))
1✔
887

888
        adapted.update(adapted.dottedToInterfaces(markers))
1✔
889

890

891
def exportObjects(obj, parent_path, context):
1✔
892
    """ Export subobjects recursively.
893
    """
894
    exporter = queryMultiAdapter((obj, context), IBody)
×
895
    path = '{}{}'.format(parent_path, obj.getId().replace(' ', '_'))
×
896
    if exporter:
×
897
        if exporter.name:
×
898
            path = f'{parent_path}{exporter.name}'
×
899
        filename = f'{path}{exporter.suffix}'
×
900
        body = exporter.body
×
901
        if body is not None:
×
902
            context.writeDataFile(filename, body, exporter.mime_type)
×
903

904
    if getattr(obj, 'objectValues', False):
×
905
        for sub in obj.objectValues():
×
906
            exportObjects(sub, path + '/', context)
×
907

908

909
def importObjects(obj, parent_path, context):
1✔
910
    """ Import subobjects recursively.
911
    """
912
    importer = queryMultiAdapter((obj, context), IBody)
×
913
    path = '{}{}'.format(parent_path, obj.getId().replace(' ', '_'))
×
914
    __traceback_info__ = path
×
915
    if importer:
×
916
        if importer.name:
×
917
            path = f'{parent_path}{importer.name}'
×
918
        filename = f'{path}{importer.suffix}'
×
919
        body = context.readDataFile(filename)
×
920
        if body is not None:
×
921
            importer.filename = filename  # for error reporting
×
922
            importer.body = body
×
923

924
    if getattr(obj, 'objectValues', False):
×
925
        for sub in obj.objectValues():
×
926
            importObjects(sub, path + '/', context)
×
927

928

929
def _computeTopologicalSort(steps):
1✔
930
    result = []
1✔
931
    graph = [(x['id'], x['dependencies']) for x in steps]
1✔
932

933
    unresolved = []
1✔
934

935
    while 1:
936
        for node, edges in graph:
1✔
937

938
            after = -1
1✔
939
            resolved = 0
1✔
940

941
            for edge in edges:
1✔
942

943
                if edge in result:
1✔
944
                    resolved += 1
1✔
945
                    after = max(after, result.index(edge))
1✔
946

947
            if len(edges) > resolved:
1✔
948
                unresolved.append((node, edges))
1✔
949
            else:
950
                result.insert(after + 1, node)
1✔
951

952
        if not unresolved:
1✔
953
            break
1✔
954
        if len(unresolved) == len(graph):
1✔
955
            # Nothing was resolved in this loop. There must be circular or
956
            # missing dependencies. Just add them to the end. We can't
957
            # raise an error, because checkComplete relies on this method.
958
            logger = getLogger('GenericSetup')
1✔
959
            log_msg = 'There are unresolved or circular dependencies. '\
1✔
960
                      'Graphviz diagram:: digraph dependencies {'
961
            for step in steps:
1✔
962
                step_id = step['id']
1✔
963
                for dependency in step['dependencies']:
1✔
964
                    log_msg += f'"{step_id}" -> "{dependency}"; '
1✔
965
                if not step['dependencies']:
1✔
966
                    log_msg += '"%s";' % step_id
1✔
967
            for unresolved_key, _ignore in unresolved:
1✔
968
                log_msg += '"%s" [color=red,style=filled]; ' % unresolved_key
1✔
969
            log_msg += '}'
1✔
970
            logger.warning(log_msg)
1✔
971

972
            for node, edges in unresolved:
1✔
973
                result.append(node)
1✔
974
            break
1✔
975
        graph = unresolved
1✔
976
        unresolved = []
1✔
977

978
    return result
1✔
979

980

981
def _getProductPath(product_name):
1✔
982
    """ Return the absolute path of the product's directory.
983
    """
984
    try:
1✔
985
        # BBB: for GenericSetup 1.1 style product names
986
        product = __import__(f'Products.{product_name}', globals(), {},
1✔
987
                             ['initialize'])
988
    except ImportError:
989
        try:
990
            product = __import__(product_name, globals(), {}, ['initialize'])
991
        except ImportError:
992
            raise ValueError(f'Not a valid product name: {product_name}')
993

994
    return product.__path__[0]
1✔
995

996

997
def _getHash(*args):
1✔
998
    """return a stable md hash of given string arguments"""
999
    base = "".join([str(x) for x in args])
1✔
1000
    hashmd5 = hashlib.md5(base.encode('utf8'))
1✔
1001
    return hashmd5.hexdigest()
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc