• 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

100.0
/src/zope/configuration/fields.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-specific schema fields
15
"""
16
import os
1✔
17
import sys
1✔
18
import warnings
1✔
19

20
from zope.interface import implementer
1✔
21
from zope.schema import Bool as schema_Bool
1✔
22
from zope.schema import DottedName
1✔
23
from zope.schema import Field
1✔
24
from zope.schema import InterfaceField
1✔
25
from zope.schema import List
1✔
26
from zope.schema import PythonIdentifier as schema_PythonIdentifier
1✔
27
from zope.schema import Text
1✔
28
from zope.schema import ValidationError
1✔
29
from zope.schema.interfaces import IFromUnicode
1✔
30
from zope.schema.interfaces import InvalidValue
1✔
31

32
from zope.configuration._compat import implementer_if_needed
1✔
33
from zope.configuration.exceptions import ConfigurationError
1✔
34
from zope.configuration.interfaces import InvalidToken
1✔
35

36

37
__all__ = [
1✔
38
    'Bool',
39
    'GlobalObject',
40
    'GlobalInterface',
41
    'MessageID',
42
    'Path',
43
    'PythonIdentifier',
44
    'Tokens',
45
]
46

47

48
class PythonIdentifier(schema_PythonIdentifier):
1✔
49
    r"""
50
    This class is like `zope.schema.PythonIdentifier`.
51

52

53
    Let's look at an example:
54

55
      >>> from zope.configuration.fields import PythonIdentifier
56
      >>> class FauxContext(object):
57
      ...     pass
58
      >>> context = FauxContext()
59
      >>> field = PythonIdentifier().bind(context)
60

61
    Let's test the fromUnicode method:
62

63
      >>> field.fromUnicode(u'foo')
64
      'foo'
65
      >>> field.fromUnicode(u'foo3')
66
      'foo3'
67
      >>> field.fromUnicode(u'_foo3')
68
      '_foo3'
69

70
    Now let's see whether validation works alright
71

72
      >>> values = (u'foo', u'foo3', u'foo_', u'_foo3', u'foo_3', u'foo3_')
73
      >>> for value in values:
74
      ...     _ = field.fromUnicode(value)
75
      >>> from zope.schema import ValidationError
76
      >>> for value in (u'3foo', u'foo:', u'\\', u''):
77
      ...     try:
78
      ...         field.fromUnicode(value)
79
      ...     except ValidationError:
80
      ...         print('Validation Error ' + repr(value))
81
      Validation Error '3foo'
82
      Validation Error 'foo:'
83
      Validation Error '\\'
84
      Validation Error ''
85

86
    .. versionchanged:: 4.2.0
87
       Extend `zope.schema.PythonIdentifier`, which implies that `fromUnicode`
88
       validates the strings.
89
    """
90

91
    def _validate(self, value):
1✔
92
        super()._validate(value)
1✔
93
        if not value:
1✔
94
            raise ValidationError(value).with_field_and_value(self, value)
1✔
95

96

97
@implementer_if_needed(IFromUnicode)
1✔
98
class GlobalObject(Field):
1✔
99
    """
100
    An object that can be accessed as a module global.
101

102
    The special value ``*`` indicates a value of `None`; this is
103
    not validated against the *value_type*.
104
    """
105

106
    _DOT_VALIDATOR = DottedName()
1✔
107

108
    def __init__(self, value_type=None, **kw):
1✔
109
        self.value_type = value_type
1✔
110
        super().__init__(**kw)
1✔
111

112
    def _validate(self, value):
1✔
113
        super()._validate(value)
1✔
114
        if self.value_type is not None:
1✔
115
            self.value_type.validate(value)
1✔
116

117
    def fromUnicode(self, value):
1✔
118
        r"""
119
        Find and return the module global at the path *value*.
120

121
          >>> d = {'x': 1, 'y': 42, 'z': 'zope'}
122
          >>> class fakeresolver(dict):
123
          ...     def resolve(self, n):
124
          ...         return self[n]
125
          >>> fake = fakeresolver(d)
126

127
          >>> from zope.schema import Int
128
          >>> from zope.configuration.fields import GlobalObject
129
          >>> g = GlobalObject(value_type=Int())
130
          >>> gg = g.bind(fake)
131
          >>> gg.fromUnicode("x")
132
          1
133
          >>> gg.fromUnicode("   x  \n  ")
134
          1
135
          >>> gg.fromUnicode("y")
136
          42
137
          >>> gg.fromUnicode("z")
138
          Traceback (most recent call last):
139
          ...
140
          WrongType: ('zope', (<type 'int'>, <type 'long'>), '')
141

142
          >>> g = GlobalObject(constraint=lambda x: x%2 == 0)
143
          >>> gg = g.bind(fake)
144
          >>> gg.fromUnicode("x")
145
          Traceback (most recent call last):
146
          ...
147
          ConstraintNotSatisfied: 1
148
          >>> gg.fromUnicode("y")
149
          42
150
          >>> g = GlobalObject()
151
          >>> gg = g.bind(fake)
152
          >>> print(gg.fromUnicode('*'))
153
          None
154
        """
155
        name = str(value.strip())
1✔
156

157
        # special case, mostly for interfaces
158
        if name == '*':
1✔
159
            return None
1✔
160

161
        try:
1✔
162
            # Leading dots are allowed here to indicate current
163
            # package, but not accepted by DottedName. Take care,
164
            # though, because a single dot is valid to resolve, but
165
            # not valid to pass to DottedName (as an empty string)
166
            to_validate = name.lstrip('.')
1✔
167
            if to_validate:
1✔
168
                self._DOT_VALIDATOR.validate(to_validate)
1✔
169
        except ValidationError as v:
1✔
170
            v.with_field_and_value(self, name)
1✔
171
            raise
1✔
172

173
        try:
1✔
174
            value = self.context.resolve(name)
1✔
175
        except ConfigurationError as v:
1✔
176
            raise ValidationError(v).with_field_and_value(self, name)
1✔
177

178
        self.validate(value)
1✔
179
        return value
1✔
180

181

182
@implementer_if_needed(IFromUnicode)
1✔
183
class GlobalInterface(GlobalObject):
1✔
184
    """
185
    An interface that can be accessed from a module.
186

187
    Example:
188

189
    First, we need to set up a stub name resolver:
190

191
      >>> from zope.interface import Interface
192
      >>> class IFoo(Interface):
193
      ...     pass
194
      >>> class Foo(object):
195
      ...     pass
196
      >>> d = {'Foo': Foo, 'IFoo': IFoo}
197
      >>> class fakeresolver(dict):
198
      ...     def resolve(self, n):
199
      ...         return self[n]
200
      >>> fake = fakeresolver(d)
201

202
    Now verify constraints are checked correctly:
203

204
      >>> from zope.configuration.fields import GlobalInterface
205
      >>> g = GlobalInterface()
206
      >>> gg = g.bind(fake)
207
      >>> gg.fromUnicode('IFoo') is IFoo
208
      True
209
      >>> gg.fromUnicode('  IFoo  ') is IFoo
210
      True
211
      >>> gg.fromUnicode('Foo')
212
      Traceback (most recent call last):
213
      ...
214
      NotAnInterface: (<class 'Foo'>, ...
215

216
    """
217

218
    def __init__(self, **kw):
1✔
219
        super().__init__(InterfaceField(), **kw)
1✔
220

221

222
@implementer(IFromUnicode)
1✔
223
class Tokens(List):
1✔
224
    """
225
    A list that can be read from a space-separated string.
226
    """
227

228
    def fromUnicode(self, value):
1✔
229
        r"""
230
        Split the input string and convert it to *value_type*.
231

232
        Consider GlobalObject tokens:
233

234
        First, we need to set up a stub name resolver:
235

236
          >>> d = {'x': 1, 'y': 42, 'z': 'zope', 'x.y.x': 'foo'}
237
          >>> class fakeresolver(dict):
238
          ...     def resolve(self, n):
239
          ...         return self[n]
240
          >>> fake = fakeresolver(d)
241

242
          >>> from zope.configuration.fields import Tokens
243
          >>> from zope.configuration.fields import GlobalObject
244
          >>> g = Tokens(value_type=GlobalObject())
245
          >>> gg = g.bind(fake)
246
          >>> gg.fromUnicode("  \n  x y z  \n")
247
          [1, 42, 'zope']
248

249
          >>> from zope.schema import Int
250
          >>> g = Tokens(value_type=
251
          ...            GlobalObject(value_type=
252
          ...                         Int(constraint=lambda x: x%2 == 0)))
253
          >>> gg = g.bind(fake)
254
          >>> gg.fromUnicode("x y")
255
          Traceback (most recent call last):
256
          ...
257
          InvalidToken: 1 in x y
258

259
          >>> gg.fromUnicode("z y")
260
          Traceback (most recent call last):
261
          ...
262
          InvalidToken: ('zope', (<type 'int'>, <type 'long'>), '') in z y
263
          >>> gg.fromUnicode("y y")
264
          [42, 42]
265
        """
266
        value = value.strip()
1✔
267
        if value:
1✔
268
            vt = self.value_type.bind(self.context)
1✔
269
            values = []
1✔
270
            for s in value.split():
1✔
271
                try:
1✔
272
                    v = vt.fromUnicode(s)
1✔
273
                except ValidationError as ex:
1✔
274
                    raise InvalidToken(
1✔
275
                        f"{ex} in {value!r}").with_field_and_value(
276
                            self, s)
277
                else:
278
                    values.append(v)
1✔
279
        else:
280
            values = []
1✔
281

282
        self.validate(values)
1✔
283

284
        return values
1✔
285

286

287
class PathProcessor:
1✔
288
    # Internal helper for manipulations on paths
289

290
    @classmethod
1✔
291
    def expand(cls, filename):
1✔
292
        # Perform the expansions we want to have done. Returns a
293
        # tuple: (path, needs_processing) If the second value is true,
294
        # further processing should be done (the path isn't fully
295
        # resolved); if false, the path should be used as is
296

297
        filename = filename.strip()
1✔
298
        # expanding a ~ at the front should generally result
299
        # in an absolute path.
300
        filename = os.path.expanduser(filename)
1✔
301
        filename = os.path.expandvars(filename)
1✔
302
        if os.path.isabs(filename):
1✔
303
            return os.path.normpath(filename), False
1✔
304
        return filename, True
1✔
305

306

307
@implementer_if_needed(IFromUnicode)
1✔
308
class Path(Text):
1✔
309
    """
310
    A file path name, which may be input as a relative path
311

312
    Input paths are converted to absolute paths and normalized.
313
    """
314

315
    def fromUnicode(self, value):
1✔
316
        r"""
317
        Convert the input path to a normalized, absolute path.
318

319
        Let's look at an example:
320

321
        First, we need a "context" for the field that has a path
322
        function for converting relative path to an absolute path.
323

324
        We'll be careful to do this in an operating system independent fashion.
325

326
          >>> from zope.configuration.fields import Path
327
          >>> class FauxContext(object):
328
          ...    def path(self, p):
329
          ...       return os.path.join(os.sep, 'faux', 'context', p)
330
          >>> context = FauxContext()
331
          >>> field = Path().bind(context)
332

333
        Lets try an absolute path first:
334

335
          >>> import os
336
          >>> p = os.path.join(os.sep, u'a', u'b')
337
          >>> n = field.fromUnicode(p)
338
          >>> n.split(os.sep)
339
          ['', 'a', 'b']
340

341
        This should also work with extra spaces around the path:
342

343
          >>> p = "   \n   %s   \n\n   " % p
344
          >>> n = field.fromUnicode(p)
345
          >>> n.split(os.sep)
346
          ['', 'a', 'b']
347

348
        Environment variables are expanded:
349

350
          >>> os.environ['path-test'] = '42'
351
          >>> with_env = os.path.join(os.sep, u'a', u'${path-test}')
352
          >>> n = field.fromUnicode(with_env)
353
          >>> n.split(os.sep)
354
          ['', 'a', '42']
355

356
        Now try a relative path:
357

358
          >>> p = os.path.join(u'a', u'b')
359
          >>> n = field.fromUnicode(p)
360
          >>> n.split(os.sep)
361
          ['', 'faux', 'context', 'a', 'b']
362

363
        The current user is expanded (these are implicitly relative paths):
364

365
          >>> old_home = os.environ.get('HOME')
366
          >>> os.environ['HOME'] = os.path.join(os.sep, 'HOME')
367
          >>> n = field.fromUnicode('~')
368
          >>> n.split(os.sep)
369
          ['', 'HOME']
370
          >>> if old_home:
371
          ...    os.environ['HOME'] = old_home
372
          ... else:
373
          ...    del os.environ['HOME']
374

375

376
        .. versionchanged:: 4.2.0
377
            Start expanding home directories and environment variables.
378
        """
379
        filename, needs_processing = PathProcessor.expand(value)
1✔
380
        if needs_processing:
1✔
381
            filename = self.context.path(filename)
1✔
382

383
        return filename
1✔
384

385

386
@implementer_if_needed(IFromUnicode)
1✔
387
class Bool(schema_Bool):
1✔
388
    """
389
    A boolean value.
390

391
    Values may be input (in upper or lower case) as any of:
392

393
    - yes / no
394
    - y / n
395
    - true / false
396
    - t / f
397

398
    .. caution::
399

400
       Do not confuse this with :class:`zope.schema.Bool`.
401
       That class will only parse ``"True"`` and ``"true"`` as
402
       `True` values. Any other value will silently be accepted as
403
       `False`. This class raises a validation error for unrecognized
404
       input.
405

406
    """
407

408
    def fromUnicode(self, value):
1✔
409
        """
410
        Convert the input string to a boolean.
411

412
        Example:
413

414
            >>> from zope.configuration.fields import Bool
415
            >>> Bool().fromUnicode(u"yes")
416
            True
417
            >>> Bool().fromUnicode(u"y")
418
            True
419
            >>> Bool().fromUnicode(u"true")
420
            True
421
            >>> Bool().fromUnicode(u"no")
422
            False
423
            >>> Bool().fromUnicode(u"surprise")
424
            Traceback (most recent call last):
425
            ...
426
            zope.schema._bootstrapinterfaces.InvalidValue
427
        """
428
        value = value.lower()
1✔
429
        if value in ('1', 'true', 'yes', 't', 'y'):
1✔
430
            return True
1✔
431
        if value in ('0', 'false', 'no', 'f', 'n'):
1✔
432
            return False
1✔
433
        # Unlike the superclass, anything else is invalid.
434
        raise InvalidValue().with_field_and_value(self, value)
1✔
435

436

437
@implementer_if_needed(IFromUnicode)
1✔
438
class MessageID(Text):
1✔
439
    """
440
    Text string that should be translated.
441

442
    When a string is converted to a message ID, it is also recorded in
443
    the context.
444
    """
445

446
    __factories = {}
1✔
447

448
    def fromUnicode(self, value):
1✔
449
        """
450
        Translate a string to a MessageID.
451

452
          >>> from zope.configuration.fields import MessageID
453
          >>> class Info(object):
454
          ...     file = 'file location'
455
          ...     line = 8
456
          >>> class FauxContext(object):
457
          ...     i18n_strings = {}
458
          ...     info = Info()
459
          >>> context = FauxContext()
460
          >>> field = MessageID().bind(context)
461

462
        There is a fallback domain when no domain has been specified.
463

464
        Exchange the warn function so we can make test whether the warning
465
        has been issued
466

467
          >>> warned = None
468
          >>> def fakewarn(*args, **kw):
469
          ...     global warned
470
          ...     warned = args
471

472
          >>> import warnings
473
          >>> realwarn = warnings.warn
474
          >>> warnings.warn = fakewarn
475

476
          >>> i = field.fromUnicode(u"Hello world!")
477
          >>> i
478
          'Hello world!'
479
          >>> i.domain
480
          'untranslated'
481
          >>> warned
482
          ("You did not specify an i18n translation domain for the '' field in file location",)
483

484
          >>> warnings.warn = realwarn
485

486
        With the domain specified:
487

488
          >>> context.i18n_strings = {}
489
          >>> context.i18n_domain = 'testing'
490

491
        We can get a message id:
492

493
          >>> i = field.fromUnicode(u"Hello world!")
494
          >>> i
495
          'Hello world!'
496
          >>> i.domain
497
          'testing'
498

499
        In addition, the string has been registered with the context:
500

501
          >>> context.i18n_strings
502
          {'testing': {'Hello world!': [('file location', 8)]}}
503

504
          >>> i = field.fromUnicode(u"Foo Bar")
505
          >>> i = field.fromUnicode(u"Hello world!")
506
          >>> from pprint import PrettyPrinter
507
          >>> pprint=PrettyPrinter(width=70).pprint
508
          >>> pprint(context.i18n_strings)
509
          {'testing': {'Foo Bar': [('file location', 8)],
510
                       'Hello world!': [('file location', 8),
511
                                        ('file location', 8)]}}
512

513
          >>> from zope.i18nmessageid import Message
514
          >>> isinstance(list(context.i18n_strings['testing'].keys())[0],
515
          ...            Message)
516
          True
517

518
        Explicit Message IDs
519

520
          >>> i = field.fromUnicode(u'[View-Permission] View')
521
          >>> i
522
          'View-Permission'
523
          >>> i.default
524
          'View'
525

526
          >>> i = field.fromUnicode(u'[] [Some] text')
527
          >>> i
528
          '[Some] text'
529
          >>> i.default is None
530
          True
531

532
        """  # noqa: E501 line too long
533
        context = self.context
1✔
534
        domain = getattr(context, 'i18n_domain', '')
1✔
535
        if not domain:
1✔
536
            domain = 'untranslated'
1✔
537
            warnings.warn(
1✔
538
                "You did not specify an i18n translation domain for the "
539
                "'%s' field in %s" % (self.getName(), context.info.file)
540
            )
541
        if not isinstance(domain, str):
1✔
542
            # IZopeConfigure specifies i18n_domain as a BytesLine, but that's
543
            # wrong as the filesystem uses str, and hence
544
            # zope.i18n registers ITranslationDomain utilities with str names.
545
            # If we keep bytes, we can't find those utilities.
546
            enc = sys.getfilesystemencoding() or sys.getdefaultencoding()
1✔
547
            domain = domain.decode(enc)
1✔
548

549
        v = super().fromUnicode(value)
1✔
550

551
        # Check whether there is an explicit message is specified
552
        default = None
1✔
553
        if v.startswith('[]'):
1✔
554
            v = v[2:].lstrip()
1✔
555
        elif v.startswith('['):
1✔
556
            end = v.find(']')
1✔
557
            default = v[end + 2:]
1✔
558
            v = v[1:end]
1✔
559

560
        # Convert to a message id, importing the factory, if necessary
561
        factory = self.__factories.get(domain)
1✔
562
        if factory is None:
1✔
563
            import zope.i18nmessageid
1✔
564
            factory = zope.i18nmessageid.MessageFactory(domain)
1✔
565
            self.__factories[domain] = factory
1✔
566

567
        msgid = factory(v, default)
1✔
568

569
        # Record the string we got for the domain
570
        i18n_strings = context.i18n_strings
1✔
571
        strings = i18n_strings.get(domain)
1✔
572
        if strings is None:
1✔
573
            strings = i18n_strings[domain] = {}
1✔
574
        locations = strings.setdefault(msgid, [])
1✔
575
        locations.append((context.info.file, context.info.line))
1✔
576

577
        return msgid
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