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

zopefoundation / zope.configuration / 16248876899

06 Dec 2024 07:34AM UTC coverage: 99.857%. Remained the same
16248876899

push

github

icemac
Back to development: 6.1

350 of 356 branches covered (98.31%)

Branch coverage included in aggregate %.

3850 of 3850 relevant lines covered (100.0%)

1.0 hits per line

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

99.17
/src/zope/configuration/xmlconfig.py
1
##############################################################################
2
#
3
# Copyright (c) 2001, 2002, 2003 Zope Foundation and Contributors.
4
# All Rights Reserved.
5
#
6
# This software is subject to the provisions of the Zope Public License,
7
# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
8
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
9
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
10
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
11
# FOR A PARTICULAR PURPOSE.
12
#
13
##############################################################################
14
"""Support for the XML configuration file format
15

16
Note, for a detailed description of the way that conflicting
17
configuration actions are resolved, see the detailed example in
18
test_includeOverrides in tests/test_xmlconfig.py
19
"""
20
__docformat__ = 'restructuredtext'
1✔
21

22
import errno
1✔
23
import io
1✔
24
import logging
1✔
25
import os
1✔
26
import sys
1✔
27
from glob import glob
1✔
28
from xml.sax import SAXParseException
1✔
29
from xml.sax import make_parser
1✔
30
from xml.sax.handler import ContentHandler
1✔
31
from xml.sax.handler import feature_namespaces
1✔
32
from xml.sax.xmlreader import InputSource
1✔
33

34
from zope.interface import Interface
1✔
35
from zope.schema import NativeStringLine
1✔
36

37
from zope.configuration.config import ConfigurationMachine
1✔
38
from zope.configuration.config import GroupingContextDecorator
1✔
39
from zope.configuration.config import GroupingStackItem
1✔
40
from zope.configuration.config import defineGroupingDirective
1✔
41
from zope.configuration.config import defineSimpleDirective
1✔
42
from zope.configuration.config import resolveConflicts
1✔
43
from zope.configuration.exceptions import ConfigurationError
1✔
44
from zope.configuration.exceptions import ConfigurationWrapperError
1✔
45
from zope.configuration.fields import GlobalObject
1✔
46
from zope.configuration.zopeconfigure import IZopeConfigure
1✔
47
from zope.configuration.zopeconfigure import ZopeConfigure
1✔
48

49

50
__all__ = [
1✔
51
    'ParserInfo',
52
    'ConfigurationHandler',
53
    'processxmlfile',
54
    'openInOrPlain',
55
    'IInclude',
56
    'include',
57
    'exclude',
58
    'includeOverrides',
59
    'registerCommonDirectives',
60
    'file',
61
    'string',
62
    'XMLConfig',
63
    'xmlconfig',
64
    'testxmlconfig',
65
]
66

67
logger = logging.getLogger("config")
1✔
68

69
ZCML_NAMESPACE = "http://namespaces.zope.org/zcml"
1✔
70
ZCML_CONDITION = (ZCML_NAMESPACE, "condition")
1✔
71

72

73
class ZopeXMLConfigurationError(ConfigurationWrapperError):
1✔
74
    """
75
    Zope XML Configuration error
76

77
    These errors are wrappers for other errors. They include
78
    configuration info and the wrapped error type and value.
79

80
    Example
81

82
        >>> from zope.configuration.xmlconfig import ZopeXMLConfigurationError
83
        >>> v = ZopeXMLConfigurationError("blah", AttributeError("xxx"))
84
        >>> print(v)
85
        'blah'
86
            AttributeError: xxx
87
    """
88

89
    USE_INFO_REPR = True
1✔
90

91

92
class ZopeSAXParseException(ConfigurationWrapperError):
1✔
93
    """
94
    Sax Parser errors as a ConfigurationError.
95

96
    Example
97

98
        >>> from zope.configuration.xmlconfig import ZopeSAXParseException
99
        >>> v = ZopeSAXParseException(
100
        ...     "info", Exception("foo.xml:12:3:Not well formed"))
101
        >>> print(v)
102
        info
103
            Exception: foo.xml:12:3:Not well formed
104
    """
105

106

107
class ParserInfo:
1✔
108
    r"""
109
    Information about a directive based on parser data
110

111
    This includes the directive location, as well as text data
112
    contained in the directive.
113

114
    Example
115

116
        >>> from zope.configuration.xmlconfig import ParserInfo
117
        >>> info = ParserInfo('tests//sample.zcml', 1, 0)
118
        >>> info
119
        File "tests//sample.zcml", line 1.0
120

121
        >>> print(info)
122
        File "tests//sample.zcml", line 1.0
123

124
        >>> info.characters("blah\n")
125
        >>> info.characters("blah")
126
        >>> info.text
127
        'blah\nblah'
128

129
        >>> info.end(7, 0)
130
        >>> info
131
        File "tests//sample.zcml", line 1.0-7.0
132

133
        >>> print(info)
134
        File "tests//sample.zcml", line 1.0-7.0
135
          <configure xmlns='http://namespaces.zope.org/zope'>
136
            <!-- zope.configure -->
137
            <directives namespace="http://namespaces.zope.org/zope">
138
              <directive name="hook" attributes="name implementation module"
139
                 handler="zope.configuration.metaconfigure.hook" />
140
            </directives>
141
          </configure>
142
    """
143
    text = ''
1✔
144

145
    def __init__(self, file, line, column):
1✔
146
        self.file, self.line, self.column = file, line, column
1✔
147
        self.eline, self.ecolumn = line, column
1✔
148

149
    def end(self, line, column):
1✔
150
        self.eline, self.ecolumn = line, column
1✔
151

152
    def __repr__(self):
1✔
153
        if (self.line, self.column) == (self.eline, self.ecolumn):
1✔
154
            return 'File "{}", line {}.{}'.format(
1✔
155
                self.file, self.line, self.column)
156

157
        return 'File "{}", line {}.{}-{}.{}'.format(
1✔
158
            self.file, self.line, self.column, self.eline, self.ecolumn)
159

160
    def __str__(self):
1✔
161
        if (self.line, self.column) == (self.eline, self.ecolumn):
1✔
162
            return 'File "{}", line {}.{}'.format(
1✔
163
                self.file, self.line, self.column)
164

165
        file = self.file
1✔
166
        if file == 'tests//sample.zcml':
1✔
167
            # special case for testing
168
            file = os.path.join(os.path.dirname(__file__),
1✔
169
                                'tests', 'sample.zcml')
170

171
        try:
1✔
172
            with open(file) as f:
1✔
173
                lines = f.readlines()[self.line - 1:self.eline]
1✔
174
        except OSError:
1✔
175
            src = "  Could not read source."
1✔
176
        else:
177
            ecolumn = self.ecolumn
1✔
178
            if lines[-1][ecolumn:ecolumn + 2] == '</':  # pragma: no cover
179
                # We're pointing to the start of an end tag. Try to find
180
                # the end
181
                l_ = lines[-1].find('>', ecolumn)
182
                if l_ >= 0:
183
                    lines[-1] = lines[-1][:l_ + 1]
184
            else:  # pragma: no cover
185
                lines[-1] = lines[-1][:ecolumn + 1]
186

187
            column = self.column
1✔
188
            if lines[0][:column].strip():  # pragma: no cover
189
                # Remove text before start if it's noy whitespace
190
                lines[0] = lines[0][self.column:]
191

192
            pad = '  '
1✔
193
            blank = ''
1✔
194
            try:
1✔
195
                src = blank.join([pad + line for line in lines])
1✔
196
            except UnicodeDecodeError:  # pragma: no cover
197
                # XXX:
198
                # I hope so most internation zcml will use UTF-8 as encoding
199
                # otherwise this code must be made more clever
200
                src = blank.join([pad + line.decode('utf-8')
201
                                  for line in lines])
202
                # unicode won't be printable, at least on my console
203
                src = src.encode('ascii', 'replace')
204

205
        return f"{repr(self)}\n{src}"
1✔
206

207
    def characters(self, characters):
1✔
208
        self.text += characters
1✔
209

210

211
class ConfigurationHandler(ContentHandler):
1✔
212
    """
213
    Interface to the XML parser
214

215
    Translate parser events into calls into the configuration system.
216
    """
217
    locator = None
1✔
218

219
    def __init__(self, context, testing=False):
1✔
220
        self.context = context
1✔
221
        self.testing = testing
1✔
222
        self.ignore_depth = 0
1✔
223

224
    def setDocumentLocator(self, locator):
1✔
225
        self.locator = locator
1✔
226

227
    def characters(self, text):
1✔
228
        self.context.getInfo().characters(text)
1✔
229

230
    def _handle_exception(self, ex, info):
1✔
231
        if self.testing:
1✔
232
            raise
1✔
233
        if isinstance(ex, ConfigurationError):
1✔
234
            ex.add_details(repr(info))
1✔
235
            raise
1✔
236

237
        raise ZopeXMLConfigurationError(info, ex)
1✔
238

239
    def startElementNS(self, name, qname, attrs):
1✔
240
        if self.ignore_depth:
1✔
241
            self.ignore_depth += 1
1✔
242
            return
1✔
243

244
        data = {}
1✔
245
        for (ns, aname), value in attrs.items():
1✔
246
            # NB: even though on CPython, 'ns' will be ``None`` always,
247
            # do not change the below to "if ns is None" because Jython's
248
            # sax parser generates attrs that have empty strings for
249
            # the namepace instead of ``None``.
250
            if not ns:
1✔
251
                aname = str(aname)
1✔
252
                data[aname] = value
1✔
253
            if (ns, aname) == ZCML_CONDITION:
1✔
254
                # need to process the expression to determine if we
255
                # use this element and it's descendents
256
                use = self.evaluateCondition(value)
1✔
257
                if not use:
1✔
258
                    self.ignore_depth = 1
1✔
259
                    return
1✔
260

261
        info = ParserInfo(
1✔
262
            self.locator.getSystemId(),
263
            self.locator.getLineNumber(),
264
            self.locator.getColumnNumber(),
265
        )
266

267
        try:
1✔
268
            self.context.begin(name, data, info)
1✔
269
        except Exception as ex:
1✔
270
            self._handle_exception(ex, info)
1✔
271

272
        self.context.setInfo(info)
1✔
273

274
    def evaluateCondition(self, expression):
1✔
275
        """
276
        Evaluate a ZCML condition.
277

278
        ``expression`` is a string of the form "verb arguments".
279

280
        Currently the supported verbs are ``have``, ``not-have``,
281
        ``installed``, ``not-installed``, ``envvar`` and ``not-envvar``.
282

283
        The ``have`` and ``not-have`` verbs each take one argument:
284
        the name of a feature:
285

286
            >>> from zope.configuration.config import ConfigurationContext
287
            >>> from zope.configuration.xmlconfig import ConfigurationHandler
288
            >>> context = ConfigurationContext()
289
            >>> context.provideFeature('apidoc')
290
            >>> c = ConfigurationHandler(context, testing=True)
291
            >>> c.evaluateCondition("have apidoc")
292
            True
293
            >>> c.evaluateCondition("not-have apidoc")
294
            False
295
            >>> c.evaluateCondition("have onlinehelp")
296
            False
297
            >>> c.evaluateCondition("not-have onlinehelp")
298
            True
299

300
        Ill-formed expressions raise an error:
301

302
            >>> c.evaluateCondition("want apidoc")
303
            Traceback (most recent call last):
304
              ...
305
            ValueError: Invalid ZCML condition: 'want apidoc'
306

307
            >>> c.evaluateCondition("have x y")
308
            Traceback (most recent call last):
309
              ...
310
            ValueError: Only one feature allowed: 'have x y'
311

312
            >>> c.evaluateCondition("have")
313
            Traceback (most recent call last):
314
              ...
315
            ValueError: Feature name missing: 'have'
316

317
        The ``installed`` and ``not-installed`` verbs each take one
318
        argument: the dotted name of a pacakge.
319

320
        If the pacakge is found, in other words, can be imported, then
321
        the condition will return true / false:
322

323
            >>> context = ConfigurationContext()
324
            >>> c = ConfigurationHandler(context, testing=True)
325
            >>> c.evaluateCondition('installed zope.interface')
326
            True
327
            >>> c.evaluateCondition('not-installed zope.interface')
328
            False
329
            >>> c.evaluateCondition('installed zope.foo')
330
            False
331
            >>> c.evaluateCondition('not-installed zope.foo')
332
            True
333

334
        Ill-formed expressions raise an error:
335

336
            >>> c.evaluateCondition("installed foo bar")
337
            Traceback (most recent call last):
338
              ...
339
            ValueError: Only one package allowed: 'installed foo bar'
340

341
            >>> c.evaluateCondition("installed")
342
            Traceback (most recent call last):
343
              ...
344
            ValueError: Package name missing: 'installed'
345

346
        The ``envvar`` and ``not-envvar`` verbs each take one argument:
347
        the name of an environment variable:
348

349
            >>> from zope.configuration.config import ConfigurationContext
350
            >>> from zope.configuration.xmlconfig import ConfigurationHandler
351
            >>> context = ConfigurationContext()
352
            >>> c = ConfigurationHandler(context, testing=True)
353
            >>> c.evaluateCondition("envvar SAMPLE_ZOPE_ENV_VAR")
354
            False
355
            >>> c.evaluateCondition("not-envvar SAMPLE_ZOPE_ENV_VAR")
356
            True
357
            >>> try:
358
            ...     os.environ['SAMPLE_ZOPE_ENV_VAR'] = '1'
359
            ...     c.evaluateCondition("envvar SAMPLE_ZOPE_ENV_VAR")
360
            ... finally:
361
            ...     del os.environ['SAMPLE_ZOPE_ENV_VAR']
362
            True
363
            >>> try:
364
            ...     os.environ['SAMPLE_ZOPE_ENV_VAR'] = '1'
365
            ...     c.evaluateCondition("not-envvar SAMPLE_ZOPE_ENV_VAR")
366
            ... finally:
367
            ...     del os.environ['SAMPLE_ZOPE_ENV_VAR']
368
            False
369

370
        Ill-formed expressions raise an error:
371

372
            >>> c.evaluateCondition("envvar x y")
373
            Traceback (most recent call last):
374
              ...
375
            ValueError: Only one environment variable name allowed: \
376
            'envvar x y'
377

378
            >>> c.evaluateCondition("envvar")
379
            Traceback (most recent call last):
380
              ...
381
            ValueError: Environment variable name missing: 'envvar'
382

383
        """
384
        arguments = expression.split(None)
1✔
385
        verb = arguments.pop(0)
1✔
386

387
        if verb in ('have', 'not-have'):
1✔
388
            if not arguments:
1✔
389
                raise ValueError("Feature name missing: %r" % expression)
1✔
390
            if len(arguments) > 1:
1✔
391
                raise ValueError("Only one feature allowed: %r" % expression)
1✔
392

393
            if verb == 'have':
1✔
394
                return self.context.hasFeature(arguments[0])
1✔
395
            elif verb == 'not-have':
1!
396
                return not self.context.hasFeature(arguments[0])
1✔
397

398
        elif verb in ('installed', 'not-installed'):
1✔
399
            if not arguments:
1✔
400
                raise ValueError("Package name missing: %r" % expression)
1✔
401
            if len(arguments) > 1:
1✔
402
                raise ValueError("Only one package allowed: %r" % expression)
1✔
403

404
            try:
1✔
405
                __import__(arguments[0])
1✔
406
                installed = True
1✔
407
            except ImportError:
408
                installed = False
409

410
            if verb == 'installed':
1✔
411
                return installed
1✔
412
            elif verb == 'not-installed':
1!
413
                return not installed
1✔
414

415
        elif verb in ('envvar', 'not-envvar'):
1✔
416
            if not arguments:
1✔
417
                raise ValueError(
1✔
418
                    "Environment variable name missing: %r" % expression
419
                )
420
            if len(arguments) > 1:
1✔
421
                raise ValueError(
1✔
422
                    "Only one environment variable name allowed: %r"
423
                    % expression
424
                )
425

426
            if verb == 'envvar':
1✔
427
                return self.context.hasEnvironmentVariable(arguments[0])
1✔
428
            elif verb == 'not-envvar':
1!
429
                return not self.context.hasEnvironmentVariable(arguments[0])
1✔
430

431
        else:
432
            raise ValueError("Invalid ZCML condition: %r" % expression)
1✔
433

434
    def endElementNS(self, name, qname):
1✔
435
        # If ignore_depth is set, this element will be ignored, even
436
        # if this this decrements ignore_depth to 0.
437
        if self.ignore_depth:
1✔
438
            self.ignore_depth -= 1
1✔
439
            return
1✔
440

441
        info = self.context.getInfo()
1✔
442
        info.end(
1✔
443
            self.locator.getLineNumber(),
444
            self.locator.getColumnNumber(),
445
        )
446

447
        try:
1✔
448
            self.context.end()
1✔
449
        except Exception as ex:
1✔
450
            self._handle_exception(ex, info)
1✔
451

452

453
def processxmlfile(file, context, testing=False):
1✔
454
    """Process a configuration file
455

456
    See examples in tests/test_xmlconfig.py
457
    """
458
    src = InputSource(getattr(file, 'name', '<string>'))
1✔
459
    src.setByteStream(file)
1✔
460
    parser = make_parser()
1✔
461
    parser.setContentHandler(ConfigurationHandler(context, testing=testing))
1✔
462
    parser.setFeature(feature_namespaces, True)
1✔
463
    try:
1✔
464
        parser.parse(src)
1✔
465
    except SAXParseException:
1✔
466
        raise ZopeSAXParseException(file, sys.exc_info()[1])
1✔
467

468

469
def openInOrPlain(filename):
1✔
470
    """
471
    Open a file, falling back to filename.in.
472

473
    If the requested file does not exist and filename.in does, fall
474
    back to filename.in. If opening the original filename fails for
475
    any other reason, allow the failure to propagate.
476

477
    For example, the tests/samplepackage directory has files:
478

479
        - configure.zcml
480

481
        - configure.zcml.in
482

483
        - foo.zcml.in
484

485
    If we open configure.zcml, we'll get that file:
486

487
        >>> import os
488
        >>> from zope.configuration.xmlconfig import __file__
489
        >>> from zope.configuration.xmlconfig import openInOrPlain
490
        >>> here = os.path.dirname(__file__)
491
        >>> path = os.path.join(
492
        ...     here, 'tests', 'samplepackage', 'configure.zcml')
493
        >>> f = openInOrPlain(path)
494
        >>> f.name[-14:]
495
        'configure.zcml'
496
        >>> f.close()
497

498
    But if we open foo.zcml, we'll get foo.zcml.in, since there isn't
499
    a foo.zcml:
500

501
        >>> path = os.path.join(here, 'tests', 'samplepackage', 'foo.zcml')
502
        >>> f = openInOrPlain(path)
503
        >>> f.name[-11:]
504
        'foo.zcml.in'
505
        >>> f.close()
506

507
    Make sure other IOErrors are re-raised. We need to do this in a
508
    try-except block because different errors are raised on Windows
509
    and on Linux.
510

511
        >>> try:
512
        ...     f = openInOrPlain('.')
513
        ... except IOError:
514
        ...     print("passed")
515
        ... else:
516
        ...     print("failed")
517
        passed
518
    """
519
    try:
1✔
520
        return open(filename)
1✔
521
    except OSError as e:
1✔
522
        code, msg = e.args
1✔
523
        if code == errno.ENOENT:
1✔
524
            fn = filename + ".in"
1✔
525
            if os.path.exists(fn):
1✔
526
                return open(fn)
1✔
527
        raise
1✔
528

529

530
class IInclude(Interface):
1✔
531
    """The `include`, `includeOverrides` and `exclude`
532
    directives.
533

534
    These directives allows you to include or preserve including of
535
    another ZCML file in the configuration. This enables you to write
536
    configuration files in each package and then link them together.
537
    """
538

539
    file = NativeStringLine(
1✔
540
        title="Configuration file name",
541
        description=(
542
            "The name of a configuration file to be included/"
543
            "excluded, relative to the directive containing the "
544
            "including configuration file."
545
        ),
546
        required=False,
547
    )
548

549
    files = NativeStringLine(
1✔
550
        title="Configuration file name pattern",
551
        description="""
552
        The names of multiple configuration files to be included/excluded,
553
        expressed as a file-name pattern, relative to the directive
554
        containing the including or excluding configuration file.
555
        The pattern can include:
556

557
        - ``*`` matches 0 or more characters
558

559
        - ``?`` matches a single character
560

561
        - ``[<seq>]`` matches any character in seq
562

563
        - ``[!<seq>]`` matches any character not in seq
564

565
        The file names are included in sorted order, where sorting is
566
        without regard to case.
567
        """,
568
        required=False,
569
    )
570

571
    package = GlobalObject(
1✔
572
        title="Include or exclude package",
573
        description="""
574
        Include or exclude the named file (or configure.zcml) from the
575
        directory of this package.
576
        """,
577
        required=False,
578
    )
579

580

581
def include(_context, file=None, package=None, files=None):
1✔
582
    """Include a zcml file
583
    """
584

585
    if files:
1✔
586
        if file:
1✔
587
            raise ValueError("Must specify only one of file or files")
1✔
588
    elif not file:
1✔
589
        file = 'configure.zcml'
1✔
590

591
    # This is a tad tricky. We want to behave as a grouping directive.
592

593
    context = GroupingContextDecorator(_context)
1✔
594
    if package is not None:
1✔
595
        context.package = package
1✔
596
        context.basepath = None
1✔
597

598
    if files:
1✔
599
        paths = glob(context.path(files))
1✔
600
        paths = sorted(zip([path.lower() for path in paths], paths))
1✔
601
        paths = [path for (l, path) in paths]
1✔
602
    else:
603
        paths = [context.path(file)]
1✔
604

605
    for path in paths:
1✔
606
        if context.processFile(path):
1✔
607
            with openInOrPlain(path) as f:
1✔
608
                logger.debug("include %s", f.name)
1✔
609

610
                context.basepath = os.path.dirname(path)
1✔
611
                context.includepath = _context.includepath + (f.name, )
1✔
612
                _context.stack.append(GroupingStackItem(context))
1✔
613

614
                processxmlfile(f, context)
1✔
615
            assert _context.stack[-1].context is context
1✔
616
            _context.stack.pop()
1✔
617

618

619
def exclude(_context, file=None, package=None, files=None):
1✔
620
    """Exclude a zcml file
621

622
    This directive should be used before any ZML that includes
623
    configuration you want to exclude.
624
    """
625

626
    if files:
1✔
627
        if file:
1✔
628
            raise ValueError("Must specify only one of file or files")
1✔
629
    elif not file:
1✔
630
        file = 'configure.zcml'
1✔
631

632
    context = GroupingContextDecorator(_context)
1✔
633
    if package is not None:
1✔
634
        context.package = package
1✔
635
        context.basepath = None
1✔
636

637
    if files:
1✔
638
        paths = glob(context.path(files))
1✔
639
        paths = sorted(zip([path.lower() for path in paths], paths))
1✔
640
        paths = [path for (l, path) in paths]
1✔
641
    else:
642
        paths = [context.path(file)]
1✔
643

644
    for path in paths:
1✔
645
        # processFile returns a boolean indicating if the file has been
646
        # processed or not, it *also* marks the file as having been processed,
647
        # here the side effect is used to keep the given file from being
648
        # processed in the future
649
        context.processFile(path)
1✔
650

651

652
def includeOverrides(_context, file=None, package=None, files=None):
1✔
653
    """Include zcml file containing overrides.
654

655
    The actions in the included file are added to the context as if
656
    they were in the including file directly. Conflicting actions
657
    added by the named *file* or *files* are resolved before this
658
    directive completes.
659

660
    .. caution::
661
        If you do not specify a *file*, then the default file
662
        of ``configure.zcml`` will be used. A common use is to set *file*
663
        to ``overrides.zcml``.
664
    """
665

666
    # We need to remember how many actions we had before
667
    nactions = len(_context.actions)
1✔
668

669
    # We'll give the new actions this include path
670
    includepath = _context.includepath
1✔
671

672
    # Now we'll include the file. We'll munge the actions after
673
    include(_context, file, package, files)
1✔
674

675
    # Now we'll grab the new actions, resolve conflicts,
676
    # and munge the includepath:
677
    newactions = []
1✔
678

679
    for action in resolveConflicts(_context.actions[nactions:]):
1✔
680
        action['includepath'] = includepath
1✔
681
        newactions.append(action)
1✔
682

683
    _context.actions[nactions:] = newactions
1✔
684

685

686
def registerCommonDirectives(context):
1✔
687
    # We have to use the direct definition functions to define
688
    # a directive for all namespaces.
689

690
    defineSimpleDirective(
1✔
691
        context, "include", IInclude, include, namespace="*")
692

693
    defineSimpleDirective(
1✔
694
        context, "exclude", IInclude, exclude, namespace="*")
695

696
    defineSimpleDirective(
1✔
697
        context, "includeOverrides", IInclude, includeOverrides, namespace="*")
698

699
    defineGroupingDirective(
1✔
700
        context,
701
        name="configure",
702
        namespace="*",
703
        schema=IZopeConfigure,
704
        handler=ZopeConfigure,
705
    )
706

707

708
def file(name, package=None, context=None, execute=True):
1✔
709
    """Execute a zcml file
710
    """
711

712
    if context is None:
1✔
713
        context = ConfigurationMachine()
1✔
714
        registerCommonDirectives(context)
1✔
715
        context.package = package
1✔
716

717
    include(context, name, package)
1✔
718
    if execute:
1✔
719
        context.execute_actions()
1✔
720

721
    return context
1✔
722

723

724
def string(s, context=None, name="<string>", execute=True):
1✔
725
    """Execute a zcml string
726
    """
727
    if context is None:
1✔
728
        context = ConfigurationMachine()
1✔
729
        registerCommonDirectives(context)
1✔
730

731
    f = io.BytesIO(s) if isinstance(s, bytes) else io.StringIO(s)
1✔
732
    f.name = name
1✔
733
    processxmlfile(f, context)
1✔
734

735
    if execute:
1✔
736
        context.execute_actions()
1✔
737

738
    return context
1✔
739

740

741
##############################################################################
742
# Backward compatability, mainly for tests
743

744

745
_context = None
1✔
746

747

748
def _clearContext():
1✔
749
    global _context
750
    _context = ConfigurationMachine()
1✔
751
    registerCommonDirectives(_context)
1✔
752

753

754
def _getContext():
1✔
755
    global _context
756
    if _context is None:
1✔
757
        _clearContext()
1✔
758
        try:
1✔
759
            from zope.testing.cleanup import addCleanUp
1✔
760
        except ImportError:  # pragma: no cover
761
            pass
762
        else:  # pragma: no cover
763
            addCleanUp(_clearContext)
764
            del addCleanUp
765
    return _context
1✔
766

767

768
class XMLConfig:
1✔
769
    """Provide high-level handling of configuration files.
770

771
    See examples in tests/text_xmlconfig.py
772
    """
773

774
    def __init__(self, file_name, module=None):
1✔
775
        context = _getContext()
1✔
776
        include(context, file_name, module)
1✔
777
        self.context = context
1✔
778

779
    def __call__(self):
1✔
780
        self.context.execute_actions()
1✔
781

782

783
def xmlconfig(file, testing=False):
1✔
784
    context = _getContext()
1✔
785
    processxmlfile(file, context, testing=testing)
1✔
786
    context.execute_actions(testing=testing)
1✔
787

788

789
def testxmlconfig(file):
1✔
790
    """xmlconfig that doesn't raise configuration errors
791

792
    This is useful for testing, as it doesn't mask exception types.
793
    """
794
    context = _getContext()
1✔
795
    processxmlfile(file, context, testing=True)
1✔
796
    context.execute_actions(testing=True)
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