• 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.83
/src/zope/configuration/config.py
1
##############################################################################
2
#
3
# Copyright (c) 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
"""Configuration processor
15
"""
16
import builtins
1✔
17
import operator
1✔
18
import os.path
1✔
19
import sys
1✔
20
from keyword import iskeyword
1✔
21

22
from zope.interface import Interface
1✔
23
from zope.interface import implementer
1✔
24
from zope.interface import providedBy
1✔
25
from zope.interface.adapter import AdapterRegistry
1✔
26
from zope.schema import URI
1✔
27
from zope.schema import TextLine
1✔
28
from zope.schema import ValidationError
1✔
29

30
from zope.configuration._compat import implementer_if_needed
1✔
31
from zope.configuration.exceptions import ConfigurationError
1✔
32
from zope.configuration.exceptions import ConfigurationWrapperError
1✔
33
from zope.configuration.fields import GlobalInterface
1✔
34
from zope.configuration.fields import GlobalObject
1✔
35
from zope.configuration.fields import PathProcessor
1✔
36
from zope.configuration.interfaces import IConfigurationContext
1✔
37
from zope.configuration.interfaces import IGroupingContext
1✔
38

39

40
__all__ = [
1✔
41
    'ConfigurationContext',
42
    'ConfigurationAdapterRegistry',
43
    'ConfigurationMachine',
44
    'IStackItem',
45
    'SimpleStackItem',
46
    'RootStackItem',
47
    'GroupingStackItem',
48
    'ComplexStackItem',
49
    'GroupingContextDecorator',
50
    'DirectiveSchema',
51
    'IDirectivesInfo',
52
    'IDirectivesContext',
53
    'DirectivesHandler',
54
    'IDirectiveInfo',
55
    'IFullInfo',
56
    'IStandaloneDirectiveInfo',
57
    'defineSimpleDirective',
58
    'defineGroupingDirective',
59
    'IComplexDirectiveContext',
60
    'ComplexDirectiveDefinition',
61
    'subdirective',
62
    'IProvidesDirectiveInfo',
63
    'provides',
64
    'toargs',
65
    'expand_action',
66
    'resolveConflicts',
67
    'ConfigurationConflictError',
68
]
69

70
zopens = 'http://namespaces.zope.org/zope'
1✔
71
metans = 'http://namespaces.zope.org/meta'
1✔
72
testns = 'http://namespaces.zope.org/test'
1✔
73

74

75
class ConfigurationContext:
1✔
76
    """
77
    Mix-in for implementing.
78
    :class:`zope.configuration.interfaces.IConfigurationContext`.
79

80
    Note that this class itself does not actually declare that it
81
    implements that interface; the subclass must do that. In addition,
82
    subclasses must provide a ``package`` attribute and a ``basepath``
83
    attribute. If the base path is not None, relative paths are
84
    converted to absolute paths using the the base path. If the
85
    package is not none, relative imports are performed relative to
86
    the package.
87

88
    In general, the basepath and package attributes should be
89
    consistent. When a package is provided, the base path should be
90
    set to the path of the package directory.
91

92
    Subclasses also provide an ``actions`` attribute, which is a list
93
    of actions, an ``includepath`` attribute, and an ``info``
94
    attribute.
95

96
    The include path is appended to each action and is used when
97
    resolving conflicts among actions. Normally, only the a
98
    ConfigurationMachine provides the actions attribute. Decorators
99
    simply use the actions of the context they decorate. The
100
    ``includepath`` attribute is a tuple of names. Each name is
101
    typically the name of an included configuration file.
102

103
    The ``info`` attribute contains descriptive information helpful
104
    when reporting errors. If not set, it defaults to an empty string.
105

106
    The actions attribute is a sequence of dictionaries where each
107
    dictionary has the following keys:
108

109
        - ``discriminator``, a value that identifies the action. Two
110
          actions that have the same (non None) discriminator
111
          conflict.
112

113
        - ``callable``, an object that is called to execute the
114
          action,
115

116
        - ``args``, positional arguments for the action
117

118
        - ``kw``, keyword arguments for the action
119

120
        - ``includepath``, a tuple of include file names (defaults to
121
          ())
122

123
        - ``info``, an object that has descriptive information about
124
          the action (defaults to '')
125
    """
126

127
    # pylint:disable=no-member
128

129
    def __init__(self):
1✔
130
        super().__init__()
1✔
131
        self._seen_files = set()
1✔
132
        self._features = set()
1✔
133

134
    def resolve(self, dottedname):
1✔
135
        """
136
        Resolve a dotted name to an object.
137

138
        Examples:
139

140
             >>> from zope.configuration.config import ConfigurationContext
141
             >>> c = ConfigurationContext()
142
             >>> import zope, zope.interface
143
             >>> c.resolve('zope') is zope
144
             True
145
             >>> c.resolve('zope.interface') is zope.interface
146
             True
147
             >>> c.resolve('zope.configuration.eek') #doctest: +NORMALIZE_WHITESPACE
148
             Traceback (most recent call last):
149
             ...
150
             ConfigurationError:
151
             ImportError: Module zope.configuration has no global eek
152

153
             >>> c.resolve('.config.ConfigurationContext')
154
             Traceback (most recent call last):
155
             ...
156
             AttributeError: 'ConfigurationContext' object has no attribute 'package'
157
             >>> import zope.configuration
158
             >>> c.package = zope.configuration
159
             >>> c.resolve('.') is zope.configuration
160
             True
161
             >>> c.resolve('.config.ConfigurationContext') is ConfigurationContext
162
             True
163
             >>> c.resolve('..interface') is zope.interface
164
             True
165
             >>> c.resolve('str') is str
166
             True
167
        """  # noqa: E501 line too long
168
        name = dottedname.strip()
1✔
169

170
        if not name:
1✔
171
            raise ValueError("The given name is blank")
1✔
172

173
        if name == '.':
1✔
174
            return self.package
1✔
175

176
        names = name.split('.')
1✔
177

178
        if not names[-1]:
1✔
179
            raise ValueError(
1✔
180
                "Trailing dots are no longer supported in dotted names")
181

182
        if len(names) == 1:
1✔
183
            # Check for built-in objects
184
            marker = object()
1✔
185
            obj = getattr(builtins, names[0], marker)
1✔
186
            if obj is not marker:
1✔
187
                return obj
1✔
188

189
        if not names[0]:
1✔
190
            # Got a relative name. Convert it to abs using package info
191
            if self.package is None:
1✔
192
                raise ConfigurationError(
1✔
193
                    "Can't use leading dots in dotted names, "
194
                    "no package has been set.")
195
            pnames = self.package.__name__.split(".")
1✔
196
            pnames.append('')
1✔
197
            while names and not names[0]:
1✔
198
                names.pop(0)
1✔
199
                try:
1✔
200
                    pnames.pop()
1✔
201
                except IndexError:
1✔
202
                    raise ConfigurationError("Invalid global name", name)
1✔
203
            names[0:0] = pnames
1✔
204

205
        # Now we should have an absolute dotted name
206

207
        # Split off object name:
208
        oname, mname = names[-1], '.'.join(names[:-1])
1✔
209

210
        # Import the module
211
        if not mname:
1✔
212
            # Just got a single name. Must me a module
213
            mname = oname
1✔
214
            oname = ''
1✔
215

216
        try:
1✔
217
            # Without a fromlist, this returns the package rather than the
218
            # module if the name contains a dot. Getting the module from
219
            # sys.modules instead avoids this problem.
220
            __import__(mname)
1✔
221
            mod = sys.modules[mname]
1✔
222
        except ImportError as v:
1✔
223
            if sys.exc_info()[2].tb_next is not None:
1✔
224
                # ImportError was caused deeper
225
                raise
1✔
226
            raise ConfigurationError(
1✔
227
                f"ImportError: Couldn't import {mname}, {v}")
228

229
        if not oname:
1✔
230
            # see not mname case above
231
            return mod
1✔
232

233
        try:
1✔
234
            obj = getattr(mod, oname)
1✔
235
            return obj
1✔
236
        except AttributeError:
1✔
237
            # No such name, maybe it's a module that we still need to import
238
            try:
1✔
239
                moname = mname + '.' + oname
1✔
240
                __import__(moname)
1✔
241
                return sys.modules[moname]
1✔
242
            except ImportError:
243
                if sys.exc_info()[2].tb_next is not None:
244
                    # ImportError was caused deeper
245
                    raise
246
                raise ConfigurationError(
247
                    f"ImportError: Module {mname} has no global {oname}")
248

249
    def path(self, filename):
1✔
250
        """
251
        Compute package-relative paths.
252

253
        Examples:
254

255
             >>> import os
256
             >>> from zope.configuration.config import ConfigurationContext
257
             >>> c = ConfigurationContext()
258
             >>> c.path("/x/y/z") == os.path.normpath("/x/y/z")
259
             True
260
             >>> c.path("y/z")
261
             Traceback (most recent call last):
262
             ...
263
             AttributeError: 'ConfigurationContext' object has no attribute 'package'
264
             >>> import zope.configuration
265
             >>> c.package = zope.configuration
266
             >>> import os
267
             >>> d = os.path.dirname(zope.configuration.__file__)
268
             >>> c.path("y/z") == d + os.path.normpath("/y/z")
269
             True
270
             >>> c.path("y/./z") == d + os.path.normpath("/y/z")
271
             True
272
             >>> c.path("y/../z") == d + os.path.normpath("/z")
273
             True
274
        """  # noqa: E501 line too long
275
        filename, needs_processing = PathProcessor.expand(filename)
1✔
276

277
        if not needs_processing:
1✔
278
            return filename
1✔
279

280
        # Got a relative path, combine with base path.
281
        # If we have no basepath, compute the base path from the package
282
        # path.
283
        basepath = getattr(self, 'basepath', '')
1✔
284

285
        if not basepath:
1✔
286
            if self.package is None:
1✔
287
                basepath = os.getcwd()
1✔
288
            else:
289
                if hasattr(self.package, '__path__'):
1✔
290
                    basepath = self.package.__path__[0]
1✔
291
                else:
292
                    basepath = os.path.dirname(self.package.__file__)
1✔
293
                basepath = os.path.abspath(os.path.normpath(basepath))
1✔
294
            self.basepath = basepath
1✔
295

296
        return os.path.normpath(os.path.join(basepath, filename))
1✔
297

298
    def checkDuplicate(self, filename):
1✔
299
        """
300
        Check for duplicate imports of the same file.
301

302
        Raises an exception if this file had been processed before.
303
        This is better than an unlimited number of conflict errors.
304

305
        Examples:
306

307
             >>> from zope.configuration.config import ConfigurationContext
308
             >>> from zope.configuration.config import ConfigurationError
309
             >>> c = ConfigurationContext()
310
             >>> c.checkDuplicate('/foo.zcml')
311
             >>> try:
312
             ...     c.checkDuplicate('/foo.zcml')
313
             ... except ConfigurationError as e:
314
             ...     # On Linux the exact msg has /foo, on Windows \\foo.
315
             ...     str(e).endswith("foo.zcml' included more than once")
316
             True
317

318
        You may use different ways to refer to the same file:
319

320
             >>> import zope.configuration
321
             >>> c.package = zope.configuration
322
             >>> import os
323
             >>> d = os.path.dirname(zope.configuration.__file__)
324
             >>> c.checkDuplicate('bar.zcml')
325
             >>> try:
326
             ...   c.checkDuplicate(d + os.path.normpath('/bar.zcml'))
327
             ... except ConfigurationError as e:
328
             ...   str(e).endswith("bar.zcml' included more than once")
329
             ...
330
             True
331

332
        """
333
        path = self.path(filename)
1✔
334
        if path in self._seen_files:
1✔
335
            raise ConfigurationError('%r included more than once' % path)
1✔
336
        self._seen_files.add(path)
1✔
337

338
    def processFile(self, filename):
1✔
339
        """
340
        Check whether a file needs to be processed.
341

342
        Return True if processing is needed and False otherwise. If
343
        the file needs to be processed, it will be marked as
344
        processed, assuming that the caller will procces the file if
345
        it needs to be procssed.
346

347
        Examples:
348

349
             >>> from zope.configuration.config import ConfigurationContext
350
             >>> c = ConfigurationContext()
351
             >>> c.processFile('/foo.zcml')
352
             True
353
             >>> c.processFile('/foo.zcml')
354
             False
355

356
        You may use different ways to refer to the same file:
357

358
             >>> import zope.configuration
359
             >>> c.package = zope.configuration
360
             >>> import os
361
             >>> d = os.path.dirname(zope.configuration.__file__)
362
             >>> c.processFile('bar.zcml')
363
             True
364
             >>> c.processFile(os.path.join(d, 'bar.zcml'))
365
             False
366
        """
367
        path = self.path(filename)
1✔
368
        if path in self._seen_files:
1✔
369
            return False
1✔
370
        self._seen_files.add(path)
1✔
371
        return True
1✔
372

373
    def action(self,
1✔
374
               discriminator,
375
               callable=None,
376
               args=(),
377
               kw=None,
378
               order=0,
379
               includepath=None,
380
               info=None,
381
               **extra):
382
        """
383
        Add an action with the given discriminator, callable and
384
        arguments.
385

386
        For testing purposes, the callable and arguments may be
387
        omitted. In that case, a default noop callable is used.
388

389
        The discriminator must be given, but it can be None, to
390
        indicate that the action never conflicts.
391

392

393
        Examples:
394

395
             >>> from zope.configuration.config import ConfigurationContext
396
             >>> c = ConfigurationContext()
397

398
        Normally, the context gets actions from subclasses. We'll provide
399
        an actions attribute ourselves:
400

401
             >>> c.actions = []
402

403
        We'll use a test callable that has a convenient string representation
404

405
             >>> from zope.configuration.tests.directives import f
406
             >>> c.action(1, f, (1, ), {'x': 1})
407
             >>> from pprint import PrettyPrinter
408
             >>> pprint = PrettyPrinter(width=60).pprint
409
             >>> pprint(c.actions)
410
             [{'args': (1,),
411
               'callable': f,
412
               'discriminator': 1,
413
               'includepath': (),
414
               'info': '',
415
               'kw': {'x': 1},
416
               'order': 0}]
417

418
             >>> c.action(None)
419
             >>> pprint(c.actions)
420
             [{'args': (1,),
421
               'callable': f,
422
               'discriminator': 1,
423
               'includepath': (),
424
               'info': '',
425
               'kw': {'x': 1},
426
               'order': 0},
427
              {'args': (),
428
               'callable': None,
429
               'discriminator': None,
430
               'includepath': (),
431
               'info': '',
432
               'kw': {},
433
               'order': 0}]
434

435
        Now set the include path and info:
436

437
             >>> c.includepath = ('foo.zcml',)
438
             >>> c.info = "?"
439
             >>> c.action(None)
440
             >>> pprint(c.actions[-1])
441
             {'args': (),
442
              'callable': None,
443
              'discriminator': None,
444
              'includepath': ('foo.zcml',),
445
              'info': '?',
446
              'kw': {},
447
              'order': 0}
448

449
        We can add an order argument to crudely control the order
450
        of execution:
451

452
             >>> c.action(None, order=99999)
453
             >>> pprint(c.actions[-1])
454
             {'args': (),
455
              'callable': None,
456
              'discriminator': None,
457
              'includepath': ('foo.zcml',),
458
              'info': '?',
459
              'kw': {},
460
              'order': 99999}
461

462
        We can also pass an includepath argument, which will be used as the the
463
        includepath for the action.  (if includepath is None, self.includepath
464
        will be used):
465

466
             >>> c.action(None, includepath=('abc',))
467
             >>> pprint(c.actions[-1])
468
             {'args': (),
469
              'callable': None,
470
              'discriminator': None,
471
              'includepath': ('abc',),
472
              'info': '?',
473
              'kw': {},
474
              'order': 0}
475

476
        We can also pass an info argument, which will be used as the the
477
        source line info for the action.  (if info is None, self.info will be
478
        used):
479

480
             >>> c.action(None, info='abc')
481
             >>> pprint(c.actions[-1])
482
             {'args': (),
483
              'callable': None,
484
              'discriminator': None,
485
              'includepath': ('foo.zcml',),
486
              'info': 'abc',
487
              'kw': {},
488
              'order': 0}
489

490
        """
491
        if kw is None:
1✔
492
            kw = {}
1✔
493

494
        action = extra
1✔
495

496
        if info is None:
1✔
497
            info = getattr(self, 'info', '')
1✔
498

499
        if includepath is None:
1✔
500
            includepath = getattr(self, 'includepath', ())
1✔
501

502
        action.update(
1✔
503
            dict(
504
                discriminator=discriminator,
505
                callable=callable,
506
                args=args,
507
                kw=kw,
508
                includepath=includepath,
509
                info=info,
510
                order=order,
511
            ))
512

513
        self.actions.append(action)
1✔
514

515
    def hasFeature(self, feature):
1✔
516
        """
517
        Check whether a named feature has been provided.
518

519
        Initially no features are provided.
520

521
        Examples:
522

523
            >>> from zope.configuration.config import ConfigurationContext
524
            >>> c = ConfigurationContext()
525
            >>> c.hasFeature('onlinehelp')
526
            False
527

528
        You can declare that a feature is provided
529

530
            >>> c.provideFeature('onlinehelp')
531

532
        and it becomes available
533

534
            >>> c.hasFeature('onlinehelp')
535
            True
536
        """
537
        return feature in self._features
1✔
538

539
    def provideFeature(self, feature):
1✔
540
        """
541
        Declare that a named feature has been provided.
542

543
        See :meth:`hasFeature` for examples.
544
        """
545
        self._features.add(feature)
1✔
546

547
    def hasEnvironmentVariable(self, envvar):
1✔
548
        """
549
        Check whether an environment variable is set to a "truthy" value.
550

551
        Examples:
552

553
            >>> from zope.configuration.config import ConfigurationContext
554
            >>> c = ConfigurationContext()
555
            >>> c.hasEnvironmentVariable('SAMPLE_ZOPE_ENV_VAR')
556
            False
557

558
        An empty environment variable is false.
559

560
            >>> try:
561
            ...     os.environ['SAMPLE_ZOPE_ENV_VAR'] = ''
562
            ...     c.hasEnvironmentVariable('SAMPLE_ZOPE_ENV_VAR')
563
            ... finally:
564
            ...     del os.environ['SAMPLE_ZOPE_ENV_VAR']
565
            False
566

567
        A "falsy" environment variable is false.
568

569
            >>> try:
570
            ...     os.environ['SAMPLE_ZOPE_ENV_VAR'] = '0'
571
            ...     c.hasEnvironmentVariable('SAMPLE_ZOPE_ENV_VAR')
572
            ... finally:
573
            ...     del os.environ['SAMPLE_ZOPE_ENV_VAR']
574
            False
575

576
        Other values of the environment variable are true.
577

578
            >>> try:
579
            ...     os.environ['SAMPLE_ZOPE_ENV_VAR'] = '1'
580
            ...     c.hasEnvironmentVariable('SAMPLE_ZOPE_ENV_VAR')
581
            ... finally:
582
            ...     del os.environ['SAMPLE_ZOPE_ENV_VAR']
583
            True
584

585
        """
586
        value = os.getenv(envvar)
1✔
587
        if not value:
1✔
588
            return False
1✔
589
        return value.lower() not in ('0', 'false', 'no', 'f', 'n')
1✔
590

591

592
class ConfigurationAdapterRegistry:
1✔
593
    """
594
    Simple adapter registry that manages directives as adapters.
595

596
    Examples:
597

598
        >>> from zope.configuration.interfaces import IConfigurationContext
599
        >>> from zope.configuration.config import ConfigurationAdapterRegistry
600
        >>> from zope.configuration.config import ConfigurationMachine
601
        >>> r = ConfigurationAdapterRegistry()
602
        >>> c = ConfigurationMachine()
603
        >>> r.factory(c, ('http://www.zope.com', 'xxx'))
604
        Traceback (most recent call last):
605
        ...
606
        ConfigurationError: ('Unknown directive', 'http://www.zope.com', 'xxx')
607
        >>> def f():
608
        ...     pass
609

610
        >>> r.register(
611
        ...     IConfigurationContext, ('http://www.zope.com', 'xxx'), f)
612
        >>> r.factory(c, ('http://www.zope.com', 'xxx')) is f
613
        True
614
        >>> r.factory(c, ('http://www.zope.com', 'yyy')) is f
615
        Traceback (most recent call last):
616
        ...
617
        ConfigurationError: ('Unknown directive', 'http://www.zope.com', 'yyy')
618
        >>> r.register(IConfigurationContext, 'yyy', f)
619
        >>> r.factory(c, ('http://www.zope.com', 'yyy')) is f
620
        True
621

622
    Test the documentation feature:
623

624
        >>> from zope.configuration.config import IFullInfo
625
        >>> r._docRegistry
626
        []
627
        >>> r.document(('ns', 'dir'), IFullInfo, IConfigurationContext, None,
628
        ...            'inf', None)
629
        >>> r._docRegistry[0][0] == ('ns', 'dir')
630
        True
631
        >>> r._docRegistry[0][1] is IFullInfo
632
        True
633
        >>> r._docRegistry[0][2] is IConfigurationContext
634
        True
635
        >>> r._docRegistry[0][3] is None
636
        True
637
        >>> r._docRegistry[0][4] == 'inf'
638
        True
639
        >>> r._docRegistry[0][5] is None
640
        True
641
        >>> r.document('all-dir', None, None, None, None)
642
        >>> r._docRegistry[1][0]
643
        ('', 'all-dir')
644
    """
645

646
    def __init__(self):
1✔
647
        super().__init__()
1✔
648
        self._registry = {}
1✔
649
        # Stores tuples of form:
650
        #   (namespace, name), schema, usedIn, info, parent
651
        self._docRegistry = []
1✔
652

653
    def register(self, interface, name, factory):
1✔
654
        r = self._registry.get(name)
1✔
655
        if r is None:
1✔
656
            r = AdapterRegistry()
1✔
657
            self._registry[name] = r
1✔
658

659
        r.register([interface], Interface, '', factory)
1✔
660

661
    def document(self, name, schema, usedIn, handler, info, parent=None):
1✔
662
        if isinstance(name, str):
1✔
663
            name = ('', name)
1✔
664
        self._docRegistry.append((name, schema, usedIn, handler, info, parent))
1✔
665

666
    def factory(self, context, name):
1✔
667
        r = self._registry.get(name)
1✔
668
        if r is None:
1✔
669
            # Try namespace-independent name
670
            ns, n = name
1✔
671
            r = self._registry.get(n)
1✔
672
            if r is None:
1✔
673
                raise ConfigurationError("Unknown directive", ns, n)
1✔
674

675
        f = r.lookup1(providedBy(context), Interface)
1✔
676
        if f is None:
1✔
677
            raise ConfigurationError(
1✔
678
                f"The directive {name} cannot be used in this context")
679
        return f
1✔
680

681

682
@implementer(IConfigurationContext)
1✔
683
class ConfigurationMachine(ConfigurationAdapterRegistry, ConfigurationContext):
1✔
684
    """
685
    Configuration machine.
686

687
    Example:
688

689
      >>> from zope.configuration.config import ConfigurationMachine
690
      >>> machine = ConfigurationMachine()
691
      >>> ns = "http://www.zope.org/testing"
692

693
    Register a directive:
694

695
      >>> from zope.configuration.config import metans
696
      >>> machine((metans, "directive"),
697
      ...         namespace=ns, name="simple",
698
      ...         schema="zope.configuration.tests.directives.ISimple",
699
      ...         handler="zope.configuration.tests.directives.simple")
700

701
    and try it out:
702

703
      >>> machine((ns, "simple"), a=u"aa", c=u"cc")
704
      >>> from pprint import PrettyPrinter
705
      >>> pprint = PrettyPrinter(width=60).pprint
706
      >>> pprint(machine.actions)
707
      [{'args': ('aa', 'xxx', 'cc'),
708
        'callable': f,
709
        'discriminator': ('simple', 'aa', 'xxx', 'cc'),
710
        'includepath': (),
711
        'info': None,
712
        'kw': {},
713
        'order': 0}]
714
    """
715
    package = None
1✔
716
    basepath = None
1✔
717
    includepath = ()
1✔
718
    info = ''
1✔
719

720
    #: These `Exception` subclasses are allowed to be raised from
721
    #: `execute_actions` without being re-wrapped into a
722
    #: `~.ConfigurationError`. (`BaseException` and other
723
    #: `~.ConfigurationError` instances are never wrapped.)
724
    #:
725
    #: Users of instances of this class may modify this before calling
726
    #: `execute_actions` if they need to propagate specific exceptions.
727
    #:
728
    #: .. versionadded:: 4.2.0
729
    pass_through_exceptions = ()
1✔
730

731
    def __init__(self):
1✔
732
        super().__init__()
1✔
733
        self.actions = []
1✔
734
        self.stack = [RootStackItem(self)]
1✔
735
        self.i18n_strings = {}
1✔
736
        _bootstrap(self)
1✔
737

738
    def begin(self, __name, __data=None, __info=None, **kw):
1✔
739
        if __data:
1✔
740
            if kw:
1✔
741
                raise TypeError("Can't provide a mapping object and keyword "
1✔
742
                                "arguments")
743
        else:
744
            __data = kw
1✔
745
        self.stack.append(self.stack[-1].contained(__name, __data, __info))
1✔
746

747
    def end(self):
1✔
748
        self.stack.pop().finish()
1✔
749

750
    def __call__(self, __name, __info=None, **__kw):
1✔
751
        self.begin(__name, __kw, __info)
1✔
752
        self.end()
1✔
753

754
    def getInfo(self):
1✔
755
        return self.stack[-1].context.info
1✔
756

757
    def setInfo(self, info):
1✔
758
        self.stack[-1].context.info = info
1✔
759

760
    def execute_actions(self, clear=True, testing=False):
1✔
761
        """
762
        Execute the configuration actions.
763

764
        This calls the action callables after resolving conflicts.
765

766
        For example:
767

768
            >>> from zope.configuration.config import ConfigurationMachine
769
            >>> output = []
770
            >>> def f(*a, **k):
771
            ...    output.append(('f', a, k))
772
            >>> context = ConfigurationMachine()
773
            >>> context.actions = [
774
            ...   (1, f, (1,)),
775
            ...   (1, f, (11,), {}, ('x', )),
776
            ...   (2, f, (2,)),
777
            ...   ]
778
            >>> context.execute_actions()
779
            >>> output
780
            [('f', (1,), {}), ('f', (2,), {})]
781

782
        If the action raises an error, we convert it to a
783
        `~.ConfigurationError`.
784

785
            >>> output = []
786
            >>> def bad():
787
            ...    bad.xxx
788
            >>> context.actions = [
789
            ...   (1, f, (1,)),
790
            ...   (1, f, (11,), {}, ('x', )),
791
            ...   (2, f, (2,)),
792
            ...   (3, bad, (), {}, (), 'oops')
793
            ...   ]
794

795
            >>> context.execute_actions()
796
            Traceback (most recent call last):
797
            ...
798
            zope.configuration.config.ConfigurationExecutionError: oops
799
                AttributeError: 'function' object has no attribute 'xxx'
800

801
        Note that actions executed before the error still have an effect:
802

803
            >>> output
804
            [('f', (1,), {}), ('f', (2,), {})]
805

806
        If the exception was already a `~.ConfigurationError`, it is raised
807
        as-is with the action's ``info`` added.
808

809
            >>> def bad():
810
            ...     raise ConfigurationError("I'm bad")
811
            >>> context.actions = [
812
            ...   (1, f, (1,)),
813
            ...   (1, f, (11,), {}, ('x', )),
814
            ...   (2, f, (2,)),
815
            ...   (3, bad, (), {}, (), 'oops')
816
            ...   ]
817
            >>> context.execute_actions()
818
            Traceback (most recent call last):
819
            ...
820
            zope.configuration.exceptions.ConfigurationError: I'm bad
821
                oops
822

823
        """
824
        pass_through_exceptions = self.pass_through_exceptions
1✔
825
        if testing:
1✔
826
            pass_through_exceptions = BaseException
1✔
827
        try:
1✔
828
            for action in resolveConflicts(self.actions):
1✔
829
                callable = action['callable']
1✔
830
                if callable is None:
1✔
831
                    continue
1✔
832
                args = action['args']
1✔
833
                kw = action['kw']
1✔
834
                info = action['info']
1✔
835
                try:
1✔
836
                    callable(*args, **kw)
1✔
837
                except ConfigurationError as ex:
1✔
838
                    ex.add_details(info)
1✔
839
                    raise
1✔
840
                except pass_through_exceptions:
1✔
841
                    raise
1✔
842
                except Exception:
1✔
843
                    # Wrap it up and raise.
844
                    raise ConfigurationExecutionError(info, sys.exc_info()[1])
1✔
845
        finally:
846
            if clear:
1!
847
                del self.actions[:]
1✔
848

849

850
class ConfigurationExecutionError(ConfigurationWrapperError):
1✔
851
    """
852
    An error occurred during execution of a configuration action
853
    """
854

855

856
##############################################################################
857
# Stack items
858

859

860
class IStackItem(Interface):
1✔
861
    """
862
    Configuration machine stack items
863

864
    Stack items are created when a directive is being processed.
865

866
    A stack item is created for each directive use.
867
    """
868

869
    def contained(name, data, info):
1✔
870
        """Begin processing a contained directive
871

872
        The data are a dictionary of attribute names mapped to unicode
873
        strings.
874

875
        The info argument is an object that can be converted to a
876
        string and that contains information about the directive.
877

878
        The begin method returns the next item to be placed on the stack.
879
        """
880

881
    def finish():
1✔
882
        """Finish processing a directive
883
        """
884

885

886
@implementer(IStackItem)
1✔
887
class SimpleStackItem:
1✔
888
    """
889
    Simple stack item
890

891
    A simple stack item can't have anything added after it.  It can
892
    only be removed.  It is used for simple directives and
893
    subdirectives, which can't contain other directives.
894

895
    It also defers any computation until the end of the directive
896
    has been reached.
897
    """
898

899
    # XXX why this *argdata hack instead of schema, data?
900

901
    def __init__(self, context, handler, info, *argdata):
1✔
902
        newcontext = GroupingContextDecorator(context)
1✔
903
        newcontext.info = info
1✔
904
        self.context = newcontext
1✔
905
        self.handler = handler
1✔
906
        self.argdata = argdata
1✔
907

908
    def contained(self, name, data, info):
1✔
909
        raise ConfigurationError("Invalid directive %s" % str(name))
1✔
910

911
    def finish(self):
1✔
912
        # We're going to use the context that was passed to us, which wasn't
913
        # created for the directive.  We want to set it's info to the one
914
        # passed to us while we make the call, so we'll save the old one
915
        # and restore it.
916
        context = self.context
1✔
917
        args = toargs(context, *self.argdata)
1✔
918
        actions = self.handler(context, **args)
1✔
919
        if actions:
1✔
920
            # we allow the handler to return nothing
921
            for action in actions:
1✔
922
                if not isinstance(action, dict):
1✔
923
                    action = expand_action(*action)  # b/c
1✔
924
                context.action(**action)
1✔
925

926

927
@implementer(IStackItem)
1✔
928
class RootStackItem:
1✔
929
    """
930
    A root stack item.
931
    """
932

933
    def __init__(self, context):
1✔
934
        self.context = context
1✔
935

936
    def contained(self, name, data, info):
1✔
937
        """Handle a contained directive
938

939
        We have to compute a new stack item by getting a named adapter
940
        for the current context object.
941
        """
942
        factory = self.context.factory(self.context, name)
1✔
943
        if factory is None:
1✔
944
            raise ConfigurationError("Invalid directive", name)
1✔
945
        adapter = factory(self.context, data, info)
1✔
946
        return adapter
1✔
947

948
    def finish(self):
1✔
949
        pass
1✔
950

951

952
@implementer_if_needed(IStackItem)
1✔
953
class GroupingStackItem(RootStackItem):
1✔
954
    """
955
    Stack item for a grouping directive
956

957
    A grouping stack item is in the stack when a grouping directive is
958
    being processed. Grouping directives group other directives.
959
    Often, they just manage common data, but they may also take
960
    actions, either before or after contained directives are executed.
961

962
    A grouping stack item is created with a grouping directive
963
    definition, a configuration context, and directive data.
964

965
    To see how this works, let's look at an example:
966

967
    We need a context. We'll just use a configuration machine
968

969
        >>> from zope.configuration.config import GroupingStackItem
970
        >>> from zope.configuration.config import ConfigurationMachine
971
        >>> context = ConfigurationMachine()
972

973
    We need a callable to use in configuration actions. We'll use a
974
    convenient one from the tests:
975

976
        >>> from zope.configuration.tests.directives import f
977

978
    We need a handler for the grouping directive. This is a class that
979
    implements a context decorator. The decorator must also provide
980
    ``before`` and ``after`` methods that are called before and after
981
    any contained directives are processed. We'll typically subclass
982
    ``GroupingContextDecorator``, which provides context decoration,
983
    and default ``before`` and ``after`` methods.
984

985
        >>> from zope.configuration.config import GroupingContextDecorator
986
        >>> class SampleGrouping(GroupingContextDecorator):
987
        ...    def before(self):
988
        ...       self.action(('before', self.x, self.y), f)
989
        ...    def after(self):
990
        ...       self.action(('after'), f)
991

992
    We'll use our decorator to decorate our initial context, providing
993
    keyword arguments x and y:
994

995
        >>> dec = SampleGrouping(context, x=1, y=2)
996

997
    Note that the keyword arguments are made attributes of the
998
    decorator.
999

1000
    Now we'll create the stack item.
1001

1002
        >>> item = GroupingStackItem(dec)
1003

1004
    We still haven't called the before action yet, which we can verify
1005
    by looking at the context actions:
1006

1007
        >>> context.actions
1008
        []
1009

1010
    Subdirectives will get looked up as adapters of the context.
1011

1012
    We'll create a simple handler:
1013

1014
        >>> def simple(context, data, info):
1015
        ...     context.action(("simple", context.x, context.y, data), f)
1016
        ...     return info
1017

1018
    and register it with the context:
1019

1020
        >>> from zope.configuration.interfaces import IConfigurationContext
1021
        >>> from zope.configuration.config import testns
1022
        >>> context.register(IConfigurationContext, (testns, 'simple'), simple)
1023

1024
    This handler isn't really a propert handler, because it doesn't
1025
    return a new context. It will do for this example.
1026

1027
    Now we'll call the contained method on the stack item:
1028

1029
        >>> item.contained((testns, 'simple'), {'z': 'zope'}, "someinfo")
1030
        'someinfo'
1031

1032
    We can verify thet the simple method was called by looking at the
1033
    context actions. Note that the before method was called before
1034
    handling the contained directive.
1035

1036
        >>> from pprint import PrettyPrinter
1037
        >>> pprint = PrettyPrinter(width=60).pprint
1038

1039
        >>> pprint(context.actions)
1040
        [{'args': (),
1041
          'callable': f,
1042
          'discriminator': ('before', 1, 2),
1043
          'includepath': (),
1044
          'info': '',
1045
          'kw': {},
1046
          'order': 0},
1047
         {'args': (),
1048
          'callable': f,
1049
          'discriminator': ('simple', 1, 2, {'z': 'zope'}),
1050
          'includepath': (),
1051
          'info': '',
1052
          'kw': {},
1053
          'order': 0}]
1054

1055
    Finally, we call finish, which calls the decorator after method:
1056

1057
        >>> item.finish()
1058

1059
        >>> pprint(context.actions)
1060
        [{'args': (),
1061
          'callable': f,
1062
          'discriminator': ('before', 1, 2),
1063
          'includepath': (),
1064
          'info': '',
1065
          'kw': {},
1066
          'order': 0},
1067
         {'args': (),
1068
          'callable': f,
1069
          'discriminator': ('simple', 1, 2, {'z': 'zope'}),
1070
          'includepath': (),
1071
          'info': '',
1072
          'kw': {},
1073
          'order': 0},
1074
         {'args': (),
1075
          'callable': f,
1076
          'discriminator': 'after',
1077
          'includepath': (),
1078
          'info': '',
1079
          'kw': {},
1080
          'order': 0}]
1081

1082
    If there were no nested directives:
1083

1084
        >>> context = ConfigurationMachine()
1085
        >>> dec = SampleGrouping(context, x=1, y=2)
1086
        >>> item = GroupingStackItem(dec)
1087
        >>> item.finish()
1088

1089
    Then before will be when we call finish:
1090

1091
        >>> pprint(context.actions)
1092
        [{'args': (),
1093
          'callable': f,
1094
          'discriminator': ('before', 1, 2),
1095
          'includepath': (),
1096
          'info': '',
1097
          'kw': {},
1098
          'order': 0},
1099
         {'args': (),
1100
          'callable': f,
1101
          'discriminator': 'after',
1102
          'includepath': (),
1103
          'info': '',
1104
          'kw': {},
1105
          'order': 0}]
1106
    """
1107

1108
    def __init__(self, context):
1✔
1109
        super().__init__(context)
1✔
1110

1111
    def __callBefore(self):
1✔
1112
        actions = self.context.before()
1✔
1113
        if actions:
1✔
1114
            for action in actions:
1✔
1115
                if not isinstance(action, dict):
1✔
1116
                    action = expand_action(*action)
1✔
1117
                self.context.action(**action)
1✔
1118
        self.__callBefore = noop
1✔
1119

1120
    def contained(self, name, data, info):
1✔
1121
        self.__callBefore()
1✔
1122
        return RootStackItem.contained(self, name, data, info)
1✔
1123

1124
    def finish(self):
1✔
1125
        self.__callBefore()
1✔
1126
        actions = self.context.after()
1✔
1127
        if actions:
1✔
1128
            for action in actions:
1✔
1129
                if not isinstance(action, dict):
1✔
1130
                    action = expand_action(*action)
1✔
1131
                self.context.action(**action)
1✔
1132

1133

1134
def noop():
1✔
1135
    pass
1✔
1136

1137

1138
@implementer(IStackItem)
1✔
1139
class ComplexStackItem:
1✔
1140
    """
1141
    Complex stack item
1142

1143
    A complex stack item is in the stack when a complex directive is
1144
    being processed. It only allows subdirectives to be used.
1145

1146
    A complex stack item is created with a complex directive
1147
    definition (IComplexDirectiveContext), a configuration context,
1148
    and directive data.
1149

1150
    To see how this works, let's look at an example:
1151

1152
    We need a context. We'll just use a configuration machine
1153

1154
        >>> from zope.configuration.config import ConfigurationMachine
1155
        >>> context = ConfigurationMachine()
1156

1157
    We need a callable to use in configuration actions. We'll use a
1158
    convenient one from the tests:
1159

1160
        >>> from zope.configuration.tests.directives import f
1161

1162
    We need a handler for the complex directive. This is a class with
1163
    a method for each subdirective:
1164

1165
        >>> class Handler(object):
1166
        ...   def __init__(self, context, x, y):
1167
        ...      self.context, self.x, self.y = context, x, y
1168
        ...      context.action('init', f)
1169
        ...   def sub(self, context, a, b):
1170
        ...      context.action(('sub', a, b), f)
1171
        ...   def __call__(self):
1172
        ...      self.context.action(('call', self.x, self.y), f)
1173

1174
    We need a complex directive definition:
1175

1176
        >>> from zope.interface import Interface
1177
        >>> from zope.schema import TextLine
1178
        >>> from zope.configuration.config import ComplexDirectiveDefinition
1179
        >>> class Ixy(Interface):
1180
        ...    x = TextLine()
1181
        ...    y = TextLine()
1182
        >>> definition = ComplexDirectiveDefinition(
1183
        ...        context, name="test", schema=Ixy,
1184
        ...        handler=Handler)
1185
        >>> class Iab(Interface):
1186
        ...    a = TextLine()
1187
        ...    b = TextLine()
1188
        >>> definition['sub'] = Iab, ''
1189

1190
    OK, now that we have the context, handler and definition, we're
1191
    ready to use a stack item.
1192

1193
        >>> from zope.configuration.config import ComplexStackItem
1194
        >>> item = ComplexStackItem(
1195
        ...     definition, context, {'x': u'xv', 'y': u'yv'}, 'foo')
1196

1197
    When we created the definition, the handler (factory) was called.
1198

1199
        >>> from pprint import PrettyPrinter
1200
        >>> pprint = PrettyPrinter(width=60).pprint
1201
        >>> pprint(context.actions)
1202
        [{'args': (),
1203
          'callable': f,
1204
          'discriminator': 'init',
1205
          'includepath': (),
1206
          'info': 'foo',
1207
          'kw': {},
1208
          'order': 0}]
1209

1210
    If a subdirective is provided, the ``contained`` method of the
1211
    stack item is called. It will lookup the subdirective schema and
1212
    call the corresponding method on the handler instance:
1213

1214
        >>> simple = item.contained(('somenamespace', 'sub'),
1215
        ...                         {'a': u'av', 'b': u'bv'}, 'baz')
1216
        >>> simple.finish()
1217

1218
    Note that the name passed to ``contained`` is a 2-part name,
1219
    consisting of a namespace and a name within the namespace.
1220

1221
        >>> pprint(context.actions)
1222
        [{'args': (),
1223
          'callable': f,
1224
          'discriminator': 'init',
1225
          'includepath': (),
1226
          'info': 'foo',
1227
          'kw': {},
1228
          'order': 0},
1229
         {'args': (),
1230
          'callable': f,
1231
          'discriminator': ('sub', 'av', 'bv'),
1232
          'includepath': (),
1233
          'info': 'baz',
1234
          'kw': {},
1235
          'order': 0}]
1236

1237
    The new stack item returned by contained is one that doesn't allow
1238
    any more subdirectives,
1239

1240
    When all of the subdirectives have been provided, the ``finish``
1241
    method is called:
1242

1243
        >>> item.finish()
1244

1245
    The stack item will call the handler if it is callable.
1246

1247
        >>> pprint(context.actions)
1248
        [{'args': (),
1249
          'callable': f,
1250
          'discriminator': 'init',
1251
          'includepath': (),
1252
          'info': 'foo',
1253
          'kw': {},
1254
          'order': 0},
1255
         {'args': (),
1256
          'callable': f,
1257
          'discriminator': ('sub', 'av', 'bv'),
1258
          'includepath': (),
1259
          'info': 'baz',
1260
          'kw': {},
1261
          'order': 0},
1262
         {'args': (),
1263
          'callable': f,
1264
          'discriminator': ('call', 'xv', 'yv'),
1265
          'includepath': (),
1266
          'info': 'foo',
1267
          'kw': {},
1268
          'order': 0}]
1269
    """
1270

1271
    def __init__(self, meta, context, data, info):
1✔
1272
        newcontext = GroupingContextDecorator(context)
1✔
1273
        newcontext.info = info
1✔
1274
        self.context = newcontext
1✔
1275
        self.meta = meta
1✔
1276

1277
        # Call the handler contructor
1278
        args = toargs(newcontext, meta.schema, data)
1✔
1279
        self.handler = self.meta.handler(newcontext, **args)
1✔
1280

1281
    def contained(self, name, data, info):
1✔
1282
        """Handle a subdirective
1283
        """
1284
        # Look up the subdirective meta data on our meta object
1285
        ns, name = name
1✔
1286
        schema = self.meta.get(name)
1✔
1287
        if schema is None:
1✔
1288
            raise ConfigurationError("Invalid directive", name)
1✔
1289
        schema = schema[0]  # strip off info
1✔
1290
        handler = getattr(self.handler, name)
1✔
1291
        return SimpleStackItem(self.context, handler, info, schema, data)
1✔
1292

1293
    def finish(self):
1✔
1294
        # when we're done, we call the handler, which might return more actions
1295
        # Need to save and restore old info
1296
        # XXX why not just use callable()?
1297
        try:
1✔
1298
            actions = self.handler()
1✔
1299
        except AttributeError as v:
1✔
1300
            if v.args[0] == '__call__':
1✔
1301
                return  # noncallable
1✔
1302
            raise
1✔
1303
        except TypeError:
1✔
1304
            return  # non callable
1✔
1305

1306
        if actions:
1✔
1307
            # we allow the handler to return nothing
1308
            for action in actions:
1✔
1309
                if not isinstance(action, dict):
1✔
1310
                    action = expand_action(*action)
1✔
1311
                self.context.action(**action)
1✔
1312

1313

1314
##############################################################################
1315
# Helper classes
1316

1317

1318
@implementer(IConfigurationContext, IGroupingContext)
1✔
1319
class GroupingContextDecorator(ConfigurationContext):
1✔
1320
    """Helper mix-in class for building grouping directives
1321

1322
    See the discussion (and test) in GroupingStackItem.
1323
    """
1324

1325
    def __init__(self, context, **kw):
1✔
1326
        self.context = context
1✔
1327
        for name, v in kw.items():
1✔
1328
            setattr(self, name, v)
1✔
1329

1330
    def __getattr__(self, name):
1✔
1331
        v = getattr(self.context, name)
1✔
1332
        # cache result in self
1333
        setattr(self, name, v)
1✔
1334
        return v
1✔
1335

1336
    def before(self):
1✔
1337
        pass
1✔
1338

1339
    def after(self):
1✔
1340
        pass
1✔
1341

1342

1343
##############################################################################
1344
# Directive-definition
1345

1346

1347
class DirectiveSchema(GlobalInterface):
1✔
1348
    """A field that contains a global variable value that must be a schema
1349
    """
1350

1351

1352
class IDirectivesInfo(Interface):
1✔
1353
    """Schema for the ``directives`` directive
1354
    """
1355

1356
    namespace = URI(
1✔
1357
        title="Namespace",
1358
        description=("The namespace in which directives' names "
1359
                     "will be defined"),
1360
    )
1361

1362

1363
class IDirectivesContext(IDirectivesInfo, IConfigurationContext):
1✔
1364
    pass
1✔
1365

1366

1367
@implementer(IDirectivesContext)
1✔
1368
class DirectivesHandler(GroupingContextDecorator):
1✔
1369
    """Handler for the directives directive
1370

1371
    This is just a grouping directive that adds a namespace attribute
1372
    to the normal directive context.
1373

1374
    """
1375

1376

1377
class IDirectiveInfo(Interface):
1✔
1378
    """Information common to all directive definitions.
1379
    """
1380

1381
    name = TextLine(
1✔
1382
        title="Directive name",
1383
        description="The name of the directive being defined",
1384
    )
1385

1386
    schema = DirectiveSchema(
1✔
1387
        title="Directive handler",
1388
        description="The dotted name of the directive handler",
1389
    )
1390

1391

1392
class IFullInfo(IDirectiveInfo):
1✔
1393
    """Information that all top-level directives (not subdirectives)
1394
    have.
1395
    """
1396

1397
    handler = GlobalObject(
1✔
1398
        title="Directive handler",
1399
        description="The dotted name of the directive handler",
1400
    )
1401

1402
    usedIn = GlobalInterface(
1✔
1403
        title="The directive types the directive can be used in",
1404
        description=("The interface of the directives that can contain "
1405
                     "the directive"),
1406
        default=IConfigurationContext,
1407
    )
1408

1409

1410
class IStandaloneDirectiveInfo(IDirectivesInfo, IFullInfo):
1✔
1411
    """Info for full directives defined outside a directives directives
1412
    """
1413

1414

1415
def defineSimpleDirective(context,
1✔
1416
                          name,
1417
                          schema,
1418
                          handler,
1419
                          namespace='',
1420
                          usedIn=IConfigurationContext):
1421
    """
1422
    Define a simple directive
1423

1424
    Define and register a factory that invokes the simple directive
1425
    and returns a new stack item, which is always the same simple
1426
    stack item.
1427

1428
    If the namespace is '*', the directive is registered for all
1429
    namespaces.
1430

1431
    Example:
1432

1433
        >>> from zope.configuration.config import ConfigurationMachine
1434
        >>> context = ConfigurationMachine()
1435
        >>> from zope.interface import Interface
1436
        >>> from zope.schema import TextLine
1437
        >>> from zope.configuration.tests.directives import f
1438
        >>> class Ixy(Interface):
1439
        ...    x = TextLine()
1440
        ...    y = TextLine()
1441
        >>> def s(context, x, y):
1442
        ...    context.action(('s', x, y), f)
1443

1444
        >>> from zope.configuration.config import defineSimpleDirective
1445
        >>> defineSimpleDirective(context, 's', Ixy, s, testns)
1446

1447
        >>> context((testns, "s"), x=u"vx", y=u"vy")
1448
        >>> from pprint import PrettyPrinter
1449
        >>> pprint = PrettyPrinter(width=60).pprint
1450
        >>> pprint(context.actions)
1451
        [{'args': (),
1452
          'callable': f,
1453
          'discriminator': ('s', 'vx', 'vy'),
1454
          'includepath': (),
1455
          'info': None,
1456
          'kw': {},
1457
          'order': 0}]
1458

1459
        >>> context(('http://www.zope.com/t1', "s"), x=u"vx", y=u"vy")
1460
        Traceback (most recent call last):
1461
        ...
1462
        ConfigurationError: ('Unknown directive', 'http://www.zope.com/t1', 's')
1463

1464
        >>> context = ConfigurationMachine()
1465
        >>> defineSimpleDirective(context, 's', Ixy, s, "*")
1466

1467
        >>> context(('http://www.zope.com/t1', "s"), x=u"vx", y=u"vy")
1468
        >>> pprint(context.actions)
1469
        [{'args': (),
1470
          'callable': f,
1471
          'discriminator': ('s', 'vx', 'vy'),
1472
          'includepath': (),
1473
          'info': None,
1474
          'kw': {},
1475
          'order': 0}]
1476
    """  # noqa: E501 line too long
1477
    namespace = namespace or context.namespace
1✔
1478
    if namespace != '*':
1✔
1479
        name = namespace, name
1✔
1480

1481
    def factory(context, data, info):
1✔
1482
        return SimpleStackItem(context, handler, info, schema, data)
1✔
1483

1484
    factory.schema = schema
1✔
1485

1486
    context.register(usedIn, name, factory)
1✔
1487
    context.document(name, schema, usedIn, handler, context.info)
1✔
1488

1489

1490
def defineGroupingDirective(context,
1✔
1491
                            name,
1492
                            schema,
1493
                            handler,
1494
                            namespace='',
1495
                            usedIn=IConfigurationContext):
1496
    """
1497
    Define a grouping directive
1498

1499
    Define and register a factory that sets up a grouping directive.
1500

1501
    If the namespace is '*', the directive is registered for all
1502
    namespaces.
1503

1504
    Example:
1505

1506
        >>> from zope.configuration.config import ConfigurationMachine
1507
        >>> context = ConfigurationMachine()
1508
        >>> from zope.interface import Interface
1509
        >>> from zope.schema import TextLine
1510
        >>> class Ixy(Interface):
1511
        ...    x = TextLine()
1512
        ...    y = TextLine()
1513

1514
    We won't bother creating a special grouping directive class. We'll
1515
    just use :class:`GroupingContextDecorator`, which simply sets up a
1516
    grouping context that has extra attributes defined by a schema:
1517

1518
        >>> from zope.configuration.config import defineGroupingDirective
1519
        >>> from zope.configuration.config import GroupingContextDecorator
1520
        >>> defineGroupingDirective(context, 'g', Ixy,
1521
        ...                         GroupingContextDecorator, testns)
1522

1523
        >>> context.begin((testns, "g"), x=u"vx", y=u"vy")
1524
        >>> context.stack[-1].context.x
1525
        'vx'
1526
        >>> context.stack[-1].context.y
1527
        'vy'
1528

1529
        >>> context(('http://www.zope.com/t1', "g"), x=u"vx", y=u"vy")
1530
        Traceback (most recent call last):
1531
        ...
1532
        ConfigurationError: ('Unknown directive', 'http://www.zope.com/t1', 'g')
1533

1534
        >>> context = ConfigurationMachine()
1535
        >>> defineGroupingDirective(context, 'g', Ixy,
1536
        ...                         GroupingContextDecorator, "*")
1537

1538
        >>> context.begin(('http://www.zope.com/t1', "g"), x=u"vx", y=u"vy")
1539
        >>> context.stack[-1].context.x
1540
        'vx'
1541
        >>> context.stack[-1].context.y
1542
        'vy'
1543
    """  # noqa: E501 line too long
1544
    namespace = namespace or context.namespace
1✔
1545
    if namespace != '*':
1✔
1546
        name = namespace, name
1✔
1547

1548
    def factory(context, data, info):
1✔
1549
        args = toargs(context, schema, data)
1✔
1550
        newcontext = handler(context, **args)
1✔
1551
        newcontext.info = info
1✔
1552
        return GroupingStackItem(newcontext)
1✔
1553

1554
    factory.schema = schema
1✔
1555

1556
    context.register(usedIn, name, factory)
1✔
1557
    context.document(name, schema, usedIn, handler, context.info)
1✔
1558

1559

1560
class IComplexDirectiveContext(IFullInfo, IConfigurationContext):
1✔
1561
    pass
1✔
1562

1563

1564
@implementer(IComplexDirectiveContext)
1✔
1565
class ComplexDirectiveDefinition(GroupingContextDecorator, dict):
1✔
1566
    """Handler for defining complex directives
1567

1568
    See the description and tests for ComplexStackItem.
1569
    """
1570

1571
    def before(self):
1✔
1572

1573
        def factory(context, data, info):
1✔
1574
            return ComplexStackItem(self, context, data, info)
1✔
1575

1576
        factory.schema = self.schema
1✔
1577

1578
        self.register(self.usedIn, (self.namespace, self.name), factory)
1✔
1579
        self.document((self.namespace, self.name), self.schema, self.usedIn,
1✔
1580
                      self.handler, self.info)
1581

1582

1583
def subdirective(context, name, schema):
1✔
1584
    context.document((context.namespace, name), schema, context.usedIn,
1✔
1585
                     getattr(context.handler, name, context.handler),
1586
                     context.info, context.context)
1587
    context.context[name] = schema, context.info
1✔
1588

1589

1590
##############################################################################
1591
# Features
1592

1593

1594
class IProvidesDirectiveInfo(Interface):
1✔
1595
    """Information for a <meta:provides> directive"""
1596

1597
    feature = TextLine(
1✔
1598
        title="Feature name",
1599
        description="""The name of the feature being provided
1600

1601
        You can test available features with zcml:condition="have featurename".
1602
        """,
1603
    )
1604

1605

1606
def provides(context, feature):
1✔
1607
    """
1608
    Declare that a feature is provided in context.
1609

1610
    Example:
1611

1612
        >>> from zope.configuration.config import ConfigurationContext
1613
        >>> from zope.configuration.config import provides
1614
        >>> c = ConfigurationContext()
1615
        >>> provides(c, 'apidoc')
1616
        >>> c.hasFeature('apidoc')
1617
        True
1618

1619
    Spaces are not allowed in feature names (this is reserved for
1620
    providing many features with a single directive in the future).
1621

1622
        >>> provides(c, 'apidoc onlinehelp')
1623
        Traceback (most recent call last):
1624
          ...
1625
        ValueError: Only one feature name allowed
1626

1627
        >>> c.hasFeature('apidoc onlinehelp')
1628
        False
1629
    """
1630
    if len(feature.split()) > 1:
1✔
1631
        raise ValueError("Only one feature name allowed")
1✔
1632
    context.provideFeature(feature)
1✔
1633

1634

1635
##############################################################################
1636
# Argument conversion
1637

1638

1639
def toargs(context, schema, data):
1✔
1640
    """
1641
    Marshal data to an argument dictionary using a schema
1642

1643
    Names that are python keywords have an underscore added as a
1644
    suffix in the schema and in the argument list, but are used
1645
    without the underscore in the data.
1646

1647
    The fields in the schema must all implement IFromUnicode.
1648

1649
    All of the items in the data must have corresponding fields in the
1650
    schema unless the schema has a true tagged value named
1651
    'keyword_arguments'.
1652

1653
    Example:
1654

1655
        >>> from zope.configuration.config import toargs
1656
        >>> from zope.schema import BytesLine
1657
        >>> from zope.schema import Float
1658
        >>> from zope.schema import Int
1659
        >>> from zope.schema import TextLine
1660
        >>> from zope.schema import URI
1661
        >>> class schema(Interface):
1662
        ...     in_ = Int(constraint=lambda v: v > 0)
1663
        ...     f = Float()
1664
        ...     n = TextLine(min_length=1, default=u"rob")
1665
        ...     x = BytesLine(required=False)
1666
        ...     u = URI()
1667

1668
        >>> context = ConfigurationMachine()
1669
        >>> from pprint import PrettyPrinter
1670
        >>> pprint = PrettyPrinter(width=50).pprint
1671

1672
        >>> pprint(toargs(context, schema,
1673
        ...        {'in': u'1', 'f': u'1.2', 'n': u'bob', 'x': u'x.y.z',
1674
        ...          'u': u'http://www.zope.org' }))
1675
        {'f': 1.2,
1676
         'in_': 1,
1677
         'n': 'bob',
1678
         'u': 'http://www.zope.org',
1679
         'x': b'x.y.z'}
1680

1681
    If we have extra data, we'll get an error:
1682

1683
        >>> toargs(context, schema,
1684
        ...        {'in': u'1', 'f': u'1.2', 'n': u'bob', 'x': u'x.y.z',
1685
        ...          'u': u'http://www.zope.org', 'a': u'1'})
1686
        Traceback (most recent call last):
1687
        ...
1688
        ConfigurationError: ('Unrecognized parameters:', 'a')
1689

1690
    Unless we set a tagged value to say that extra arguments are ok:
1691

1692
        >>> schema.setTaggedValue('keyword_arguments', True)
1693

1694
        >>> pprint(toargs(context, schema,
1695
        ...        {'in': u'1', 'f': u'1.2', 'n': u'bob', 'x': u'x.y.z',
1696
        ...          'u': u'http://www.zope.org', 'a': u'1'}))
1697
        {'a': '1',
1698
         'f': 1.2,
1699
         'in_': 1,
1700
         'n': 'bob',
1701
         'u': 'http://www.zope.org',
1702
         'x': b'x.y.z'}
1703

1704
    If we omit required data we get an error telling us what was
1705
    omitted:
1706

1707
        >>> pprint(toargs(context, schema,
1708
        ...        {'in': u'1', 'f': u'1.2', 'n': u'bob', 'x': u'x.y.z'}))
1709
        Traceback (most recent call last):
1710
        ...
1711
        ConfigurationError: ('Missing parameter:', 'u')
1712

1713
    Although we can omit not-required data:
1714

1715
        >>> pprint(toargs(context, schema,
1716
        ...        {'in': u'1', 'f': u'1.2', 'n': u'bob',
1717
        ...          'u': u'http://www.zope.org', 'a': u'1'}))
1718
        {'a': '1',
1719
         'f': 1.2,
1720
         'in_': 1,
1721
         'n': 'bob',
1722
         'u': 'http://www.zope.org'}
1723

1724
    And we can omit required fields if they have valid defaults
1725
    (defaults that are valid values):
1726

1727
        >>> pprint(toargs(context, schema,
1728
        ...        {'in': u'1', 'f': u'1.2',
1729
        ...          'u': u'http://www.zope.org', 'a': u'1'}))
1730
        {'a': '1',
1731
         'f': 1.2,
1732
         'in_': 1,
1733
         'n': 'rob',
1734
         'u': 'http://www.zope.org'}
1735

1736
    We also get an error if any data was invalid:
1737

1738
        >>> pprint(toargs(context, schema,
1739
        ...        {'in': u'0', 'f': u'1.2', 'n': u'bob', 'x': u'x.y.z',
1740
        ...          'u': u'http://www.zope.org', 'a': u'1'}))
1741
        Traceback (most recent call last):
1742
        ...
1743
        ConfigurationError: ('Invalid value for', 'in', '0')
1744
    """
1745
    data = dict(data)
1✔
1746
    args = {}
1✔
1747
    for name, field in schema.namesAndDescriptions(True):
1✔
1748
        field = field.bind(context)
1✔
1749
        n = name
1✔
1750
        if n.endswith('_') and iskeyword(n[:-1]):
1✔
1751
            n = n[:-1]
1✔
1752

1753
        s = data.get(n, data)
1✔
1754
        if s is not data:
1✔
1755
            s = str(s)
1✔
1756
            del data[n]
1✔
1757

1758
            try:
1✔
1759
                args[str(name)] = field.fromUnicode(s)
1✔
1760
            except ValidationError as v:
1✔
1761
                raise ConfigurationError("Invalid value for %r" %
1✔
1762
                                         (n)).add_details(v)
1763
        elif field.required:
1✔
1764
            # if the default is valid, we can use that:
1765
            default = field.default
1✔
1766
            try:
1✔
1767
                field.validate(default)
1✔
1768
            except ValidationError as v:
1✔
1769
                raise ConfigurationError(
1✔
1770
                    f"Missing parameter: {n!r}").add_details(v)
1771
            args[str(name)] = default
1✔
1772

1773
    if data:
1✔
1774
        # we had data left over
1775
        try:
1✔
1776
            keyword_arguments = schema.getTaggedValue('keyword_arguments')
1✔
1777
        except KeyError:
1✔
1778
            keyword_arguments = False
1✔
1779
        if not keyword_arguments:
1✔
1780
            raise ConfigurationError("Unrecognized parameters:", *data)
1✔
1781

1782
        for name in data:
1✔
1783
            args[str(name)] = data[name]
1✔
1784

1785
    return args
1✔
1786

1787

1788
##############################################################################
1789
# Conflict resolution
1790

1791

1792
def expand_action(discriminator,
1✔
1793
                  callable=None,
1794
                  args=(),
1795
                  kw=None,
1796
                  includepath=(),
1797
                  info=None,
1798
                  order=0,
1799
                  **extra):
1800
    if kw is None:
1✔
1801
        kw = {}
1✔
1802
    action = extra
1✔
1803
    action.update(
1✔
1804
        dict(
1805
            discriminator=discriminator,
1806
            callable=callable,
1807
            args=args,
1808
            kw=kw,
1809
            includepath=includepath,
1810
            info=info,
1811
            order=order,
1812
        ))
1813
    return action
1✔
1814

1815

1816
def resolveConflicts(actions):
1✔
1817
    """
1818
    Resolve conflicting actions.
1819

1820
    Given an actions list, identify and try to resolve conflicting actions.
1821
    Actions conflict if they have the same non-None discriminator.
1822
    Conflicting actions can be resolved if the include path of one of
1823
    the actions is a prefix of the includepaths of the other
1824
    conflicting actions and is unequal to the include paths in the
1825
    other conflicting actions.
1826
    """
1827

1828
    # organize actions by discriminators
1829
    unique = {}
1✔
1830
    output = []
1✔
1831
    for i, action in enumerate(actions):
1✔
1832
        if not isinstance(action, dict):
1✔
1833
            # old-style tuple action
1834
            action = expand_action(*action)
1✔
1835

1836
        # "order" is an integer grouping. Actions in a lower order will be
1837
        # executed before actions in a higher order.  Within an order,
1838
        # actions are executed sequentially based on original action ordering
1839
        # ("i").
1840
        order = action['order'] or 0
1✔
1841
        discriminator = action['discriminator']
1✔
1842

1843
        # "ainfo" is a tuple of (order, i, action) where "order" is a
1844
        # user-supplied grouping, "i" is an integer expressing the relative
1845
        # position of this action in the action list being resolved, and
1846
        # "action" is an action dictionary.  The purpose of an ainfo is to
1847
        # associate an "order" and an "i" with a particular action; "order"
1848
        # and "i" exist for sorting purposes after conflict resolution.
1849
        ainfo = (order, i, action)
1✔
1850

1851
        if discriminator is None:
1✔
1852
            # The discriminator is None, so this action can never conflict.
1853
            # We can add it directly to the result.
1854
            output.append(ainfo)
1✔
1855
            continue
1✔
1856

1857
        L = unique.setdefault(discriminator, [])
1✔
1858
        L.append(ainfo)
1✔
1859

1860
    # Check for conflicts
1861
    conflicts = {}
1✔
1862

1863
    for discriminator, ainfos in unique.items():
1✔
1864

1865
        # We use (includepath, order, i) as a sort key because we need to
1866
        # sort the actions by the paths so that the shortest path with a
1867
        # given prefix comes first.  The "first" action is the one with the
1868
        # shortest include path.  We break sorting ties using "order", then
1869
        # "i".
1870
        def bypath(ainfo):
1✔
1871
            path, order, i = ainfo[2]['includepath'], ainfo[0], ainfo[1]
1✔
1872
            return path, order, i
1✔
1873

1874
        ainfos.sort(key=bypath)
1✔
1875
        ainfo, rest = ainfos[0], ainfos[1:]
1✔
1876
        output.append(ainfo)
1✔
1877
        _, _, action = ainfo
1✔
1878
        basepath, baseinfo, discriminator = (action['includepath'],
1✔
1879
                                             action['info'],
1880
                                             action['discriminator'])
1881

1882
        for _, _, action in rest:
1✔
1883
            includepath = action['includepath']
1✔
1884
            # Test whether path is a prefix of opath
1885
            if (includepath[:len(basepath)] != basepath  # not a prefix
1✔
1886
                    or includepath == basepath):
1887
                L = conflicts.setdefault(discriminator, [baseinfo])
1✔
1888
                L.append(action['info'])
1✔
1889

1890
    if conflicts:
1✔
1891
        raise ConfigurationConflictError(conflicts)
1✔
1892

1893
    # Sort conflict-resolved actions by (order, i) and return them.
1894
    return [x[2] for x in sorted(output, key=operator.itemgetter(0, 1))]
1✔
1895

1896

1897
class ConfigurationConflictError(ConfigurationError):
1✔
1898

1899
    def __init__(self, conflicts):
1✔
1900
        super().__init__()
1✔
1901
        self._conflicts = conflicts
1✔
1902

1903
    def _with_details(self, opening, detail_formatter):
1✔
1904
        r = ["Conflicting configuration actions"]
1✔
1905
        for discriminator, infos in sorted(self._conflicts.items()):
1✔
1906
            r.append(f"  For: {discriminator}")
1✔
1907
            for info in infos:
1✔
1908
                for line in str(info).rstrip().split('\n'):
1✔
1909
                    r.append("    " + line)
1✔
1910

1911
        opening = "\n".join(r)
1✔
1912
        return super()._with_details(opening, detail_formatter)
1✔
1913

1914

1915
##############################################################################
1916
# Bootstap code
1917

1918

1919
def _bootstrap(context):
1✔
1920

1921
    # Set enough machinery to register other directives
1922

1923
    # Define the directive (simple directive) directive by calling it's
1924
    # handler directly
1925

1926
    info = 'Manually registered in zope/configuration/config.py'
1✔
1927

1928
    context.info = info
1✔
1929
    defineSimpleDirective(context,
1✔
1930
                          namespace=metans,
1931
                          name='directive',
1932
                          schema=IStandaloneDirectiveInfo,
1933
                          handler=defineSimpleDirective)
1934
    context.info = ''
1✔
1935

1936
    # OK, now that we have that, we can use the machine to define the
1937
    # other directives. This isn't the easiest way to proceed, but it lets
1938
    # us eat our own dogfood. :)
1939

1940
    # Standalone groupingDirective
1941
    context((metans, 'directive'),
1✔
1942
            info,
1943
            name='groupingDirective',
1944
            namespace=metans,
1945
            handler="zope.configuration.config.defineGroupingDirective",
1946
            schema="zope.configuration.config.IStandaloneDirectiveInfo")
1947

1948
    # Now we can use the grouping directive to define the directives directive
1949
    context((metans, 'groupingDirective'),
1✔
1950
            info,
1951
            name='directives',
1952
            namespace=metans,
1953
            handler="zope.configuration.config.DirectivesHandler",
1954
            schema="zope.configuration.config.IDirectivesInfo")
1955

1956
    # directive and groupingDirective inside directives
1957
    context((metans, 'directive'),
1✔
1958
            info,
1959
            name='directive',
1960
            namespace=metans,
1961
            usedIn="zope.configuration.config.IDirectivesContext",
1962
            handler="zope.configuration.config.defineSimpleDirective",
1963
            schema="zope.configuration.config.IFullInfo")
1964
    context((metans, 'directive'),
1✔
1965
            info,
1966
            name='groupingDirective',
1967
            namespace=metans,
1968
            usedIn="zope.configuration.config.IDirectivesContext",
1969
            handler="zope.configuration.config.defineGroupingDirective",
1970
            schema="zope.configuration.config.IFullInfo")
1971

1972
    # Setup complex directive directive, both standalone, and in
1973
    # directives directive
1974
    context((metans, 'groupingDirective'),
1✔
1975
            info,
1976
            name='complexDirective',
1977
            namespace=metans,
1978
            handler="zope.configuration.config.ComplexDirectiveDefinition",
1979
            schema="zope.configuration.config.IStandaloneDirectiveInfo")
1980
    context((metans, 'groupingDirective'),
1✔
1981
            info,
1982
            name='complexDirective',
1983
            namespace=metans,
1984
            usedIn="zope.configuration.config.IDirectivesContext",
1985
            handler="zope.configuration.config.ComplexDirectiveDefinition",
1986
            schema="zope.configuration.config.IFullInfo")
1987

1988
    # Finally, setup subdirective directive
1989
    context((metans, 'directive'),
1✔
1990
            info,
1991
            name='subdirective',
1992
            namespace=metans,
1993
            usedIn="zope.configuration.config.IComplexDirectiveContext",
1994
            handler="zope.configuration.config.subdirective",
1995
            schema="zope.configuration.config.IDirectiveInfo")
1996

1997
    # meta:provides
1998
    context((metans, 'directive'),
1✔
1999
            info,
2000
            name='provides',
2001
            namespace=metans,
2002
            handler="zope.configuration.config.provides",
2003
            schema="zope.configuration.config.IProvidesDirectiveInfo")
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