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

zopefoundation / Products.GenericSetup / 9927778353

12 Jun 2024 06:30AM UTC coverage: 93.296% (+0.06%) from 93.233%
9927778353

push

github

web-flow
- Add support for Python 3.12.  - Drop support for Python 3.7. (#130)

1806 of 2123 branches covered (85.07%)

Branch coverage included in aggregate %.

127 of 134 new or added lines in 4 files covered. (94.78%)

2 existing lines in 2 files now uncovered.

9285 of 9765 relevant lines covered (95.08%)

0.95 hits per line

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

82.45
/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
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 ("lines" in type_converters and "string" in type_converters
1!
64
        and isinstance(type_converters["lines"]("blah")[0], str)):
65
    LINES_HAS_TEXT = True
1✔
66
else:
67
    # Older Zope
68
    LINES_HAS_TEXT = False
×
69

70

71
def _getDottedName(named):
1✔
72

73
    if isinstance(named, str):
1✔
74
        return str(named)
1✔
75

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

81
    # remove leading underscore names if possible
82

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

89
    # Step 2: check if short version can be resolved
90
    try:
1✔
91
        short_resolved = _resolveDottedName(short_dotted)
1✔
NEW
92
    except (ValueError, ModuleNotFoundError):
×
93
        return dotted
×
94

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

103
    return short_dotted
×
104

105

106
def _resolveDottedName(dotted):
1✔
107
    __traceback_info__ = dotted
1✔
108

109
    try:
1✔
110
        return resolve(dotted)
1✔
111
    except ModuleNotFoundError:
1✔
112
        return
1✔
113

114

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

123

124
def _extractDocstring(func, default_title, default_description):
1✔
125
    try:
1✔
126
        doc = getdoc(func)
1✔
127
        lines = doc.split('\n')
1✔
128

129
    except AttributeError:
1✔
130

131
        title = default_title
1✔
132
        description = default_description
1✔
133

134
    else:
135
        title = lines[0]
1✔
136

137
        if len(lines) > 1 and lines[1].strip() == '':
1!
138
            del lines[1]
1✔
139

140
        description = '\n'.join(lines[1:])
1✔
141

142
    return title, description
1✔
143

144

145
def _version_for_print(version):
1✔
146
    """Return a version suitable for printing/logging.
147

148
    Versions of profiles and destinations of upgrade steps
149
    are likely tuples.  We join them with dots.
150

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

159

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

164

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

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

174
        self._site = site
1✔
175
        self._encoding = None
1✔
176

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

183
        if reader is not None:
1!
184
            xml = reader()
×
185

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

189
        dom = parseString(xml)
1✔
190
        root = dom.documentElement
1✔
191

192
        return self._extractNode(root)
1✔
193

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

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

211
        for child in node.childNodes:
1✔
212
            name = child.nodeName
1✔
213

214
            if name == '#comment':
1!
215
                continue
×
216

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

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

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

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

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

240
            if key is None:
1✔
241
                info = info[key]
1✔
242

243
        return info
1✔
244

245
    def _getSharedImportMapping(self):
1✔
246

247
        return {
×
248
            'object': {
249
                'i18n:domain': {},
250
                'name': {
251
                    KEY: 'id'
252
                },
253
                'meta_type': {},
254
                'insert-before': {},
255
                'insert-after': {},
256
                'property': {
257
                    KEY: 'properties',
258
                    DEFAULT: ()
259
                },
260
                'object': {
261
                    KEY: 'objects',
262
                    DEFAULT: ()
263
                },
264
                'xmlns:i18n': {}
265
            },
266
            'property': {
267
                'name': {
268
                    KEY: 'id'
269
                },
270
                '#text': {
271
                    KEY: 'value',
272
                    DEFAULT: ''
273
                },
274
                'element': {
275
                    KEY: 'elements',
276
                    DEFAULT: ()
277
                },
278
                'type': {},
279
                'select_variable': {},
280
                'i18n:translate': {}
281
            },
282
            'element': {
283
                'value': {
284
                    KEY: None
285
                }
286
            },
287
            'description': {
288
                '#text': {
289
                    KEY: None,
290
                    DEFAULT: ''
291
                }
292
            }
293
        }
294

295
    def _convertToBoolean(self, val):
1✔
296

297
        return val.lower() in ('true', 'yes', '1')
1✔
298

299
    def _convertToUnique(self, val):
1✔
300

301
        assert len(val) == 1
1✔
302
        return val[0]
1✔
303

304

305
InitializeClass(ImportConfiguratorBase)
1✔
306

307

308
class ExportConfiguratorBase(Implicit):
1✔
309
    # old code, will become deprecated
310
    """ Synthesize XML description.
311
    """
312
    security = ClassSecurityInfo()
1✔
313
    security.setDefaultAccess('allow')
1✔
314

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

317
        self._site = site
1✔
318
        self._encoding = encoding
1✔
319
        self._template = self._getExportTemplate()
1✔
320

321
    @security.protected(ManagePortal)
1✔
322
    def generateXML(self, **kw):
1✔
323
        """ Pseudo API.
324
        """
325
        return self._template(**kw)
1✔
326

327

328
InitializeClass(ExportConfiguratorBase)
1✔
329

330
#
331
##############################################################################
332

333

334
class _LineWrapper:
1✔
335

336
    def __init__(self, writer, indent, addindent, newl, max):
1✔
337
        self._writer = writer
1✔
338
        self._indent = indent
1✔
339
        self._addindent = addindent
1✔
340
        self._newl = newl
1✔
341
        self._max = max
1✔
342
        self._length = 0
1✔
343
        self._queue = self._indent
1✔
344

345
    def queue(self, text):
1✔
346
        self._queue += text
1✔
347

348
    def write(self, text='', enforce=False):
1✔
349
        self._queue += text
1✔
350

351
        if 0 < self._length > self._max - len(self._queue):
1✔
352
            self._writer.write(self._newl)
1✔
353
            self._length = 0
1✔
354
            self._queue = f'{self._indent}{self._addindent} {self._queue}'
1✔
355

356
        if self._queue != self._indent:
1✔
357
            self._writer.write(self._queue)
1✔
358
            self._length += len(self._queue)
1✔
359
            self._queue = ''
1✔
360

361
        if 0 < self._length and enforce:
1✔
362
            self._writer.write(self._newl)
1✔
363
            self._length = 0
1✔
364
            self._queue = self._indent
1✔
365

366

367
class _Element(Element):
1✔
368
    """minidom element with 'pretty' XML output.
369
    """
370

371
    def writexml(self, writer, indent="", addindent="", newl=""):
1✔
372
        # indent = current indentation
373
        # addindent = indentation to add to higher levels
374
        # newl = newline string
375
        wrapper = _LineWrapper(writer, indent, addindent, newl, 78)
1✔
376
        wrapper.write('<%s' % self.tagName)
1✔
377

378
        # move 'name', 'meta_type' and 'title' to the top, sort the rest
379
        attrs = self._get_attributes()
1✔
380
        a_names = sorted(attrs.keys())
1✔
381
        if 'title' in a_names:
1!
382
            a_names.remove('title')
×
383
            a_names.insert(0, 'title')
×
384
        if 'meta_type' in a_names:
1✔
385
            a_names.remove('meta_type')
1✔
386
            a_names.insert(0, 'meta_type')
1✔
387
        if 'name' in a_names:
1✔
388
            a_names.remove('name')
1✔
389
            a_names.insert(0, 'name')
1✔
390

391
        for a_name in a_names:
1✔
392
            wrapper.write()
1✔
393
            a_value = attrs[a_name].value
1✔
394
            if a_value is None:
1✔
395
                a_value = ""
1✔
396
            else:
397
                a_value = escape(a_value, quote=True)
1✔
398

399
            wrapper.queue(f' {a_name}="{a_value}"')
1✔
400

401
        if self.childNodes:
1✔
402
            wrapper.queue('>')
1✔
403
            for node in self.childNodes:
1✔
404
                if node.nodeType == Node.TEXT_NODE:
1✔
405
                    data = escape(node.data)
1✔
406
                    textlines = data.splitlines()
1✔
407
                    if textlines:
1✔
408
                        wrapper.queue(textlines.pop(0))
1✔
409
                    if textlines:
1✔
410
                        for textline in textlines:
1✔
411
                            wrapper.write('', True)
1✔
412
                            wrapper.queue(f'{addindent}{textline}')
1✔
413
                else:
414
                    wrapper.write('', True)
1✔
415
                    node.writexml(writer, indent + addindent, addindent, newl)
1✔
416
            wrapper.write('</%s>' % self.tagName, True)
1✔
417
        else:
418
            wrapper.write('/>', True)
1✔
419

420

421
class PrettyDocument(Document):
1✔
422
    """minidom document with 'pretty' XML output.
423
    """
424

425
    def createElement(self, tagName):
1✔
426
        e = _Element(tagName)
1✔
427
        e.ownerDocument = self
1✔
428
        return e
1✔
429

430
    def createElementNS(self, namespaceURI, qualifiedName):
1✔
431
        prefix, _localName = _nssplit(qualifiedName)
×
432
        e = _Element(qualifiedName, namespaceURI, prefix)
×
433
        e.ownerDocument = self
×
434
        return e
×
435

436
    def writexml(self,
1✔
437
                 writer,
438
                 indent="",
439
                 addindent="",
440
                 newl="",
441
                 encoding='utf-8',
442
                 standalone=None):
443
        # `standalone` was added in Python 3.9 but is ignored here
444
        if encoding is None:
1!
445
            writer.write('<?xml version="1.0"?>\n')
×
446
        else:
447
            writer.write('<?xml version="1.0" encoding="%s"?>\n' % encoding)
1✔
448
        for node in self.childNodes:
1✔
449
            node.writexml(writer, indent, addindent, newl)
1✔
450

451
    def toprettyxml(self, indent='\t', newl='\n', encoding='utf-8'):
1✔
452
        return super().toprettyxml(indent, newl, encoding)
1✔
453

454

455
@implementer(INode)
1✔
456
class NodeAdapterBase:
1✔
457
    """Node im- and exporter base.
458
    """
459

460
    _encoding = 'utf-8'
1✔
461
    _LOGGER_ID = ''
1✔
462

463
    def __init__(self, context, environ):
1✔
464
        self.context = context
1✔
465
        self.environ = environ
1✔
466
        self._logger = environ.getLogger(self._LOGGER_ID)
1✔
467
        self._doc = PrettyDocument()
1✔
468

469
    def _getObjectNode(self, name, i18n=True):
1✔
470
        node = self._doc.createElement(name)
1✔
471
        node.setAttribute('name', self.context.getId())
1✔
472
        node.setAttribute('meta_type', self.context.meta_type)
1✔
473
        i18n_domain = getattr(self.context, 'i18n_domain', None)
1✔
474
        if i18n and i18n_domain:
1!
475
            node.setAttributeNS(I18NURI, 'i18n:domain', i18n_domain)
×
476
            self._i18n_props = ('title', 'description')
×
477
        return node
1✔
478

479
    def _getNodeText(self, node):
1✔
480
        text = ''
1✔
481
        for child in node.childNodes:
1✔
482
            if child.nodeName != '#text':
1!
483
                continue
×
484
            lines = [line.lstrip() for line in child.nodeValue.splitlines()]
1✔
485
            text += '\n'.join(lines)
1✔
486
        return text
1✔
487

488
    def _convertToBoolean(self, val):
1✔
489
        return val.lower() in ('true', 'yes', '1')
1✔
490

491

492
@implementer_only(IBody)
1✔
493
class BodyAdapterBase(NodeAdapterBase):
1✔
494
    """Body im- and exporter base.
495
    """
496

497
    def _exportSimpleNode(self):
1✔
498
        """Export the object as a DOM node.
499
        """
500
        if ISetupTool.providedBy(self.context):
×
501
            return None
×
502
        return self._getObjectNode('object', False)
×
503

504
    def _importSimpleNode(self, node):
1✔
505
        """Import the object from the DOM node.
506
        """
507

508
    node = property(_exportSimpleNode, _importSimpleNode)
1✔
509

510
    def _exportBody(self):
1✔
511
        """Export the object as a file body.
512
        """
513
        return b''
×
514

515
    def _importBody(self, body):
1✔
516
        """Import the object from the file body.
517
        """
518

519
    body = property(_exportBody, _importBody)
1✔
520

521
    mime_type = 'text/plain'
1✔
522

523
    name = ''
1✔
524

525
    suffix = ''
1✔
526

527

528
@implementer_only(IBody)
1✔
529
class XMLAdapterBase(BodyAdapterBase):
1✔
530
    """XML im- and exporter base.
531
    """
532

533
    def _exportBody(self):
1✔
534
        """Export the object as a file body.
535
        """
536
        self._doc.appendChild(self._exportNode())
1✔
537
        xml = self._doc.toprettyxml(' ', encoding=self._encoding)
1✔
538
        self._doc.unlink()
1✔
539
        return xml
1✔
540

541
    def _importBody(self, body):
1✔
542
        """Import the object from the file body.
543
        """
544
        try:
1✔
545
            dom = parseString(body)
1✔
546
        except ExpatError as e:
×
NEW
547
            filename = (self.filename
×
548
                        or '/'.join(self.context.getPhysicalPath()))
UNCOV
549
            raise ExpatError(f'{filename}: {e}')
×
550

551
        # Replace the encoding with the one from the XML
552
        self._encoding = dom.encoding or self._encoding
1✔
553
        self._importNode(dom.documentElement)
1✔
554

555
    body = property(_exportBody, _importBody)
1✔
556

557
    mime_type = 'text/xml'
1✔
558

559
    name = ''
1✔
560

561
    suffix = '.xml'
1✔
562

563
    filename = ''  # for error reporting during import
1✔
564

565

566
class ObjectManagerHelpers:
1✔
567
    """ObjectManager in- and export helpers.
568
    """
569

570
    def _extractObjects(self):
1✔
571
        fragment = self._doc.createDocumentFragment()
1✔
572
        objects = self.context.objectValues()
1✔
573
        if not IOrderedContainer.providedBy(self.context):
1!
574
            objects = list(objects)
1✔
575
            objects.sort(key=lambda x: x.getId())
1✔
576
        for obj in objects:
1✔
577
            exporter = queryMultiAdapter((obj, self.environ), INode)
1✔
578
            if exporter:
1!
579
                node = exporter.node
1✔
580
                if node is not None:
1!
581
                    fragment.appendChild(exporter.node)
1✔
582
        return fragment
1✔
583

584
    def _purgeObjects(self):
1✔
585
        for obj_id, obj in self.context.objectItems():
1!
586
            if ISetupTool.providedBy(obj):
×
587
                continue
×
588
            self.context._delObject(obj_id)
×
589

590
    def _initObjects(self, node):
1✔
591
        import Products
1✔
592
        for child in node.childNodes:
1✔
593
            if child.nodeName != 'object':
1✔
594
                continue
1✔
595
            if child.hasAttribute('deprecated'):
1!
596
                continue
×
597
            parent = self.context
1✔
598

599
            obj_id = str(child.getAttribute('name'))
1✔
600
            if self._convertToBoolean(child.getAttribute('remove') or 'False'):
1✔
601
                if obj_id in parent.objectIds():
1✔
602
                    parent._delObject(obj_id)
1✔
603
                continue
1✔
604

605
            if obj_id not in parent.objectIds():
1✔
606
                meta_type = str(child.getAttribute('meta_type'))
1✔
607
                __traceback_info__ = obj_id, meta_type
1✔
608
                for mt_info in Products.meta_types:
1!
609
                    if mt_info['name'] == meta_type:
1✔
610
                        parent._setObject(obj_id, mt_info['instance'](obj_id))
1✔
611
                        break
1✔
612
                else:
613
                    raise ValueError("unknown meta_type '%s'" % meta_type)
×
614

615
            if child.hasAttribute('insert-before'):
1!
616
                insert_before = child.getAttribute('insert-before')
×
617
                if insert_before == '*':
×
618
                    parent.moveObjectsToTop(obj_id)
×
619
                else:
620
                    try:
×
621
                        position = parent.getObjectPosition(insert_before)
×
622
                        if parent.getObjectPosition(obj_id) < position:
×
623
                            position -= 1
×
624
                        parent.moveObjectToPosition(obj_id, position)
×
625
                    except ValueError:
×
626
                        pass
×
627
            elif child.hasAttribute('insert-after'):
1!
628
                insert_after = child.getAttribute('insert-after')
×
629
                if insert_after == '*':
×
630
                    parent.moveObjectsToBottom(obj_id)
×
631
                else:
632
                    try:
×
633
                        position = parent.getObjectPosition(insert_after)
×
634
                        if parent.getObjectPosition(obj_id) < position:
×
635
                            position -= 1
×
636
                        parent.moveObjectToPosition(obj_id, position + 1)
×
637
                    except ValueError:
×
638
                        pass
×
639

640
            obj = getattr(self.context, obj_id)
1✔
641
            importer = queryMultiAdapter((obj, self.environ), INode)
1✔
642
            if importer:
1✔
643
                importer.node = child
1✔
644

645

646
class PropertyManagerHelpers:
1✔
647
    """PropertyManager im- and export helpers.
648

649
      o Derived classes can supply a '_PROPERTIES' scehma, which is then used
650
        to mock up a temporary propertysheet for the object.  The adapter's
651
        methods ('_extractProperties', '_purgeProperties', '_initProperties')
652
        then run against that propertysheet.
653
    """
654
    _PROPERTIES = ()
1✔
655

656
    _encoding = default_encoding
1✔
657

658
    def __init__(self, context, environ):
1✔
659
        from OFS.PropertyManager import PropertyManager
1✔
660
        if not isinstance(context, PropertyManager):
1✔
661
            context = self._fauxAdapt(context)
1✔
662

663
        super().__init__(context, environ)
1✔
664

665
    def _fauxAdapt(self, context):
1✔
666
        from OFS.PropertySheets import PropertySheet
1✔
667

668
        class Adapted(PropertySheet):
1✔
669

670
            def __init__(self, real, properties):
1✔
671
                self._real = real
1✔
672
                self._properties = properties
1✔
673

674
            def p_self(self):
1✔
675
                return self
1✔
676

677
            def v_self(self):
1✔
678
                return self._real
1✔
679

680
            def propdict(self):
1✔
681
                # PropertyManager method used by _initProperties
682
                return {p['id']: p for p in self._properties}
1✔
683

684
        return Adapted(context, self._PROPERTIES)
1✔
685

686
    def _extractProperties(self):
1✔
687
        fragment = self._doc.createDocumentFragment()
1✔
688

689
        for prop_map in self.context._propertyMap():
1✔
690
            prop_id = prop_map['id']
1✔
691
            if prop_id == 'i18n_domain':
1!
692
                continue
×
693

694
            # Don't export read-only nodes
695
            if 'w' not in prop_map.get('mode', 'wd'):
1✔
696
                continue
1✔
697

698
            node = self._doc.createElement('property')
1✔
699
            node.setAttribute('name', prop_id)
1✔
700

701
            prop = self.context.getProperty(prop_id)
1✔
702
            if prop is None:
1✔
703
                continue
1✔
704
            if isinstance(prop, (tuple, list)):
1✔
705
                for value in prop:
1✔
706
                    if isinstance(value, bytes):
1✔
707
                        value = value.decode(self._encoding)
1✔
708
                    child = self._doc.createElement('element')
1✔
709
                    child.setAttribute('value', value)
1✔
710
                    node.appendChild(child)
1✔
711
            else:
712
                if prop_map.get('type') == 'boolean':
1✔
713
                    prop = str(bool(prop))
1✔
714
                elif prop_map.get('type') == 'date':
1✔
715
                    if prop.timezoneNaive():
1✔
716
                        prop = str(prop).rsplit(None, 1)[0]
1✔
717
                    else:
718
                        prop = str(prop)
1✔
719
                elif isinstance(prop, bytes):
1!
720
                    prop = prop.decode(self._encoding)
×
721
                elif isinstance(prop, ((int, ), float)):
1✔
722
                    prop = str(prop)
1✔
723
                elif not isinstance(prop, str):
1!
724
                    prop = prop.decode(self._encoding)
×
725
                child = self._doc.createTextNode(prop)
1✔
726
                node.appendChild(child)
1✔
727

728
            if 'd' in prop_map.get('mode', 'wd') and not prop_id == 'title':
1✔
729
                prop_type = prop_map.get('type', 'string')
1✔
730
                node.setAttribute('type', prop_type)
1✔
731
                select_variable = prop_map.get('select_variable', None)
1✔
732
                if select_variable is not None:
1✔
733
                    node.setAttribute('select_variable', select_variable)
1✔
734

735
            if hasattr(self, '_i18n_props') and prop_id in self._i18n_props:
1!
736
                node.setAttribute('i18n:translate', '')
×
737

738
            fragment.appendChild(node)
1✔
739

740
        return fragment
1✔
741

742
    def _purgeProperties(self):
1✔
743
        for prop_map in self.context._propertyMap():
1✔
744
            mode = prop_map.get('mode', 'wd')
1✔
745
            if 'w' not in mode:
1✔
746
                continue
1✔
747
            prop_id = prop_map['id']
1✔
748
            if 'd' in mode and not prop_id == 'title':
1✔
749
                self.context._delProperty(prop_id)
1✔
750
            else:
751
                prop_type = prop_map.get('type')
1✔
752
                if prop_type == 'multiple selection':
1!
753
                    prop_value = ()
×
754
                elif prop_type in ('int', 'float'):
1✔
755
                    prop_value = 0
1✔
756
                elif prop_type == 'date':
1✔
757
                    prop_value = '1970/01/01 00:00:00 UTC'  # DateTime(0) UTC
1✔
758
                else:
759
                    prop_value = ''
1✔
760
                self.context._updateProperty(prop_id, prop_value)
1✔
761

762
    def _initProperties(self, node):
1✔
763
        obj = self.context
1✔
764
        if node.hasAttribute('i18n:domain'):
1✔
765
            i18n_domain = str(node.getAttribute('i18n:domain'))
1✔
766
            obj._updateProperty('i18n_domain', i18n_domain)
1✔
767
        for child in node.childNodes:
1✔
768
            if child.nodeName != 'property':
1✔
769
                continue
1✔
770
            remove = self._convertToBoolean(
1✔
771
                child.getAttribute('remove') or 'False')
772
            prop_id = str(child.getAttribute('name'))
1✔
773
            prop_map = obj.propdict().get(prop_id, None)
1✔
774

775
            if prop_map is None:
1✔
776
                if remove:
1✔
777
                    continue
1✔
778
                if child.hasAttribute('type'):
1!
779
                    val = str(child.getAttribute('select_variable'))
1✔
780
                    prop_type = str(child.getAttribute('type'))
1✔
781
                    obj._setProperty(prop_id, val, prop_type)
1✔
782
                    prop_map = obj.propdict().get(prop_id, None)
1✔
783
                else:
784
                    raise ValueError("undefined property '%s'" % prop_id)
×
785

786
            if remove:
1✔
787
                if 'd' not in prop_map.get('mode', 'wd'):
1!
788
                    raise BadRequest('%s cannot be deleted' % prop_id)
×
789
                obj._delProperty(prop_id)
1✔
790
                continue
1✔
791

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

795
            new_elements = []
1✔
796
            remove_elements = []
1✔
797
            for sub in child.childNodes:
1✔
798
                if sub.nodeName == 'element':
1✔
799
                    value = sub.getAttribute('value')
1✔
800
                    if prop_map.get('type') not in ('ulines',
1✔
801
                                                    'multiple selection'):
802
                        value = value.encode(self._encoding)
1✔
803
                    if self._convertToBoolean(
1✔
804
                            sub.getAttribute('remove') or 'False'):
805
                        remove_elements.append(value)
1✔
806
                        if value in new_elements:
1!
807
                            new_elements.remove(value)
×
808
                    else:
809
                        new_elements.append(value)
1✔
810
                        if value in remove_elements:
1!
811
                            remove_elements.remove(value)
×
812

813
            if LINES_HAS_TEXT and obj.getPropertyType(prop_id) == 'lines':
1✔
814
                # Since Zope 5.3, lines should contain text, not bytes.
815
                # https://github.com/zopefoundation/Products.GenericSetup/issues/109
816
                new_elements = _convert_lines(new_elements, self._encoding)
1✔
817
                remove_elements = _convert_lines(remove_elements,
1✔
818
                                                 self._encoding)
819

820
            if prop_map.get('type') in ('lines', 'tokens', 'ulines',
1✔
821
                                        'multiple selection'):
822
                prop_value = tuple(new_elements) or ()
1✔
823
            elif prop_map.get('type') == 'boolean':
1✔
824
                prop_value = self._convertToBoolean(self._getNodeText(child))
1✔
825
            else:
826
                # if we pass a *string* to _updateProperty, all other values
827
                # are converted to the right type
828
                prop_value = self._getNodeText(child)
1✔
829

830
            if not self._convertToBoolean(
1✔
831
                    child.getAttribute('purge') or 'True'):
832
                # If the purge attribute is False, merge sequences
833
                prop = obj.getProperty(prop_id)
1✔
834
                # Before Zope 5.3, lines contained bytes.
835
                # After, they contain text.
836
                # We may need to convert the existing property value first,
837
                # otherwise we may be combining bytes and text.
838
                # See zopefoundation/Products.GenericSetup/issues/109
839
                if LINES_HAS_TEXT and obj.getPropertyType(prop_id) == 'lines':
1!
840
                    prop = _convert_lines(prop, self._encoding)
1✔
841
                if isinstance(prop, (tuple, list)):
1!
842
                    prop_value = (tuple([
1✔
843
                        p for p in prop
844
                        if p not in prop_value and p not in remove_elements
845
                    ]) + tuple(prop_value))
846

847
            if isinstance(prop_value, (bytes, str)):
1✔
848
                prop_type = obj.getPropertyType(prop_id) or 'string'
1✔
849
                if prop_type in type_converters:
1✔
850
                    prop_converter = type_converters[prop_type]
1✔
851
                    # The type_converters use the ZPublisher default_encoding
852
                    # for decoding bytes!
853
                    if self._encoding.lower() != default_encoding:
1✔
854
                        prop_value = _de_encode_value(prop_value,
1✔
855
                                                      self._encoding,
856
                                                      prop_converter)
857
                    else:
858
                        prop_value = prop_converter(prop_value)
1✔
859
            obj._updateProperty(prop_id, prop_value)
1✔
860

861

862
def _de_encode_value(prop_value, encoding, converter):
1✔
863
    if isinstance(prop_value, bytes):
1✔
864
        u_prop_value = prop_value.decode(encoding)
1✔
865
        prop_value = u_prop_value.encode(default_encoding)
1✔
866
    prop_value = converter(prop_value)
1✔
867
    if isinstance(prop_value, bytes):
1!
868
        u_prop_value = prop_value.decode(default_encoding)
×
869
        prop_value = u_prop_value.encode(encoding)
×
870
    return prop_value
1✔
871

872

873
def _convert_lines(values, encoding):
1✔
874
    # Only called when LINES_HAS_TEXT is True.
875
    if not isinstance(values, (list, tuple)):
1!
876
        values = values.splitlines()
×
877
    if encoding.lower() == default_encoding:
1✔
878
        converter = type_converters['lines']
1✔
879
        return converter(values)
1✔
880
    # According to the tests, we support non utf-8 encodings like iso-8859-1.
881
    converter = type_converters['string']
1✔
882
    return [
1✔
883
        _de_encode_value(prop_value, encoding, converter)
884
        for prop_value in values
885
    ]
886

887

888
class MarkerInterfaceHelpers:
1✔
889
    """Marker interface im- and export helpers.
890
    """
891

892
    def _extractMarkers(self):
1✔
893
        fragment = self._doc.createDocumentFragment()
1✔
894
        adapted = IMarkerInterfaces(self.context)
1✔
895

896
        for marker_id in adapted.getDirectlyProvidedNames():
1✔
897
            node = self._doc.createElement('marker')
1✔
898
            node.setAttribute('name', marker_id)
1✔
899
            fragment.appendChild(node)
1✔
900

901
        return fragment
1✔
902

903
    def _purgeMarkers(self):
1✔
904
        directlyProvides(self.context)
1✔
905

906
    def _initMarkers(self, node):
1✔
907
        markers = []
1✔
908
        adapted = IMarkerInterfaces(self.context)
1✔
909

910
        for child in node.childNodes:
1✔
911
            if child.nodeName != 'marker':
1✔
912
                continue
1✔
913
            markers.append(str(child.getAttribute('name')))
1✔
914

915
        adapted.update(adapted.dottedToInterfaces(markers))
1✔
916

917

918
def exportObjects(obj, parent_path, context):
1✔
919
    """ Export subobjects recursively.
920
    """
921
    exporter = queryMultiAdapter((obj, context), IBody)
×
922
    path = '{}{}'.format(parent_path, obj.getId().replace(' ', '_'))
×
923
    if exporter:
×
924
        if exporter.name:
×
925
            path = f'{parent_path}{exporter.name}'
×
926
        filename = f'{path}{exporter.suffix}'
×
927
        body = exporter.body
×
928
        if body is not None:
×
929
            context.writeDataFile(filename, body, exporter.mime_type)
×
930

931
    if getattr(obj, 'objectValues', False):
×
932
        for sub in obj.objectValues():
×
933
            exportObjects(sub, path + '/', context)
×
934

935

936
def importObjects(obj, parent_path, context):
1✔
937
    """ Import subobjects recursively.
938
    """
939
    importer = queryMultiAdapter((obj, context), IBody)
×
940
    path = '{}{}'.format(parent_path, obj.getId().replace(' ', '_'))
×
941
    __traceback_info__ = path
×
942
    if importer:
×
943
        if importer.name:
×
944
            path = f'{parent_path}{importer.name}'
×
945
        filename = f'{path}{importer.suffix}'
×
946
        body = context.readDataFile(filename)
×
947
        if body is not None:
×
948
            importer.filename = filename  # for error reporting
×
949
            importer.body = body
×
950

951
    if getattr(obj, 'objectValues', False):
×
952
        for sub in obj.objectValues():
×
953
            importObjects(sub, path + '/', context)
×
954

955

956
def _computeTopologicalSort(steps):
1✔
957
    result = []
1✔
958
    graph = [(x['id'], x['dependencies']) for x in steps]
1✔
959

960
    unresolved = []
1✔
961

962
    while 1:
1✔
963
        for node, edges in graph:
1✔
964

965
            after = -1
1✔
966
            resolved = 0
1✔
967

968
            for edge in edges:
1✔
969

970
                if edge in result:
1✔
971
                    resolved += 1
1✔
972
                    after = max(after, result.index(edge))
1✔
973

974
            if len(edges) > resolved:
1✔
975
                unresolved.append((node, edges))
1✔
976
            else:
977
                result.insert(after + 1, node)
1✔
978

979
        if not unresolved:
1✔
980
            break
1✔
981
        if len(unresolved) == len(graph):
1✔
982
            # Nothing was resolved in this loop. There must be circular or
983
            # missing dependencies. Just add them to the end. We can't
984
            # raise an error, because checkComplete relies on this method.
985
            logger = getLogger('GenericSetup')
1✔
986
            log_msg = 'There are unresolved or circular dependencies. '\
1✔
987
                      'Graphviz diagram:: digraph dependencies {'
988
            for step in steps:
1✔
989
                step_id = step['id']
1✔
990
                for dependency in step['dependencies']:
1✔
991
                    log_msg += f'"{step_id}" -> "{dependency}"; '
1✔
992
                if not step['dependencies']:
1✔
993
                    log_msg += '"%s";' % step_id
1✔
994
            for unresolved_key, _ignore in unresolved:
1✔
995
                log_msg += '"%s" [color=red,style=filled]; ' % unresolved_key
1✔
996
            log_msg += '}'
1✔
997
            logger.warning(log_msg)
1✔
998

999
            for node, edges in unresolved:
1✔
1000
                result.append(node)
1✔
1001
            break
1✔
1002
        graph = unresolved
1✔
1003
        unresolved = []
1✔
1004

1005
    return result
1✔
1006

1007

1008
def _getProductPath(product_name):
1✔
1009
    """ Return the absolute path of the product's directory.
1010
    """
1011
    try:
1✔
1012
        # BBB: for GenericSetup 1.1 style product names
1013
        product = __import__(f'Products.{product_name}', globals(), {},
1✔
1014
                             ['initialize'])
1015
    except ModuleNotFoundError:
1✔
1016
        try:
1✔
1017
            product = __import__(product_name, globals(), {}, ['initialize'])
1✔
1018
        except ModuleNotFoundError:
1✔
1019
            raise ValueError(f'Not a valid product name: {product_name}')
1✔
1020

1021
    return product.__path__[0]
1✔
1022

1023

1024
def _getHash(*args):
1✔
1025
    """return a stable md hash of given string arguments"""
1026
    base = "".join([str(x) for x in args])
1✔
1027
    hashmd5 = hashlib.md5(base.encode('utf8'))
1✔
1028
    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