tested django-newsletter
This commit is contained in:
@@ -0,0 +1,225 @@
|
||||
##############################################################################
|
||||
#
|
||||
# Copyright (c) 2002, 2003 Zope Foundation and Contributors.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# This software is subject to the provisions of the Zope Public License,
|
||||
# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
|
||||
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
|
||||
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
|
||||
# FOR A PARTICULAR PURPOSE.
|
||||
#
|
||||
##############################################################################
|
||||
"""Structured, schema-driven configuration library.
|
||||
|
||||
ZConfig is a configuration library intended for general use. It
|
||||
supports a hierarchical schema-driven configuration model that allows
|
||||
a schema to specify data conversion routines written in Python.
|
||||
ZConfig's model is very different from the model supported by the
|
||||
ConfigParser module found in Python's standard library, and is more
|
||||
suitable to configuration-intensive applications.
|
||||
|
||||
ZConfig schema are written in an XML-based language and are able to
|
||||
``import`` schema components provided by Python packages. Since
|
||||
components are able to bind to conversion functions provided by Python
|
||||
code in the package (or elsewhere), configuration objects can be
|
||||
arbitrarily complex, with values that have been verified against
|
||||
arbitrary constraints. This makes it easy for applications to
|
||||
separate configuration support from configuration loading even with
|
||||
configuration data being defined and consumed by a wide range of
|
||||
separate packages.
|
||||
|
||||
"""
|
||||
__docformat__ = "reStructuredText"
|
||||
|
||||
version_info = (3, 0)
|
||||
__version__ = ".".join([str(n) for n in version_info])
|
||||
|
||||
from ZConfig.loader import loadConfig, loadConfigFile
|
||||
from ZConfig.loader import loadSchema, loadSchemaFile
|
||||
|
||||
from ZConfig._compat import TextIO
|
||||
|
||||
|
||||
class ConfigurationError(Exception):
|
||||
"""Base class for exceptions specific to the :mod:`ZConfig` package.
|
||||
|
||||
All instances provide a ``message`` attribute that describes
|
||||
the specific error, and a ``url`` attribute that gives the URL
|
||||
of the resource the error was located in, or ``None``.
|
||||
"""
|
||||
|
||||
|
||||
# The 'message' attribute was deprecated for BaseException with
|
||||
# Python 2.6; here we create descriptor properties to continue using it
|
||||
def __set_message(self, v):
|
||||
self.__dict__['message'] = v
|
||||
|
||||
def __get_message(self):
|
||||
return self.__dict__['message']
|
||||
|
||||
def __del_message(self):
|
||||
del self.__dict__['message']
|
||||
|
||||
message = property(__get_message, __set_message, __del_message)
|
||||
|
||||
def __init__(self, msg, url=None):
|
||||
self.message = msg
|
||||
self.url = url
|
||||
Exception.__init__(self, msg)
|
||||
|
||||
def __str__(self):
|
||||
return self.message
|
||||
|
||||
|
||||
class _ParseError(ConfigurationError):
|
||||
def __init__(self, msg, url, lineno, colno=None):
|
||||
self.lineno = lineno
|
||||
self.colno = colno
|
||||
ConfigurationError.__init__(self, msg, url)
|
||||
|
||||
def __str__(self):
|
||||
s = self.message
|
||||
if self.url:
|
||||
s += "\n("
|
||||
elif (self.lineno, self.colno) != (None, None):
|
||||
s += " ("
|
||||
if self.lineno:
|
||||
s += "line %d" % self.lineno
|
||||
if self.colno is not None:
|
||||
s += ", column %d" % self.colno
|
||||
if self.url:
|
||||
s += " in %s)" % self.url
|
||||
else:
|
||||
s += ")"
|
||||
elif self.url:
|
||||
s += self.url + ")"
|
||||
return s
|
||||
|
||||
|
||||
class SchemaError(_ParseError):
|
||||
"""Raised when a schema contains an error.
|
||||
|
||||
This exception type provides the attributes ``url``, ``lineno``,
|
||||
and ``colno``, which provide the source URL, the line number, and
|
||||
the column number at which the error was detected. These attributes
|
||||
may be ``None`` in some cases.
|
||||
"""
|
||||
|
||||
def __init__(self, msg, url=None, lineno=None, colno=None):
|
||||
_ParseError.__init__(self, msg, url, lineno, colno)
|
||||
|
||||
|
||||
class SchemaResourceError(SchemaError):
|
||||
"""Raised when there's an error locating a resource required by the
|
||||
schema.
|
||||
|
||||
Instances of this exception class add the attributes ``filename``,
|
||||
``package``, and ``path``, which hold the filename searched for
|
||||
within the package being loaded, the name of the package, and the
|
||||
``__path__`` attribute of the package itself (or ``None`` if it
|
||||
isn't a package or could not be imported).
|
||||
"""
|
||||
|
||||
def __init__(self, msg, url=None, lineno=None, colno=None,
|
||||
path=None, package=None, filename=None):
|
||||
self.filename = filename
|
||||
self.package = package
|
||||
if path is not None:
|
||||
path = path[:]
|
||||
self.path = path
|
||||
SchemaError.__init__(self, msg, url, lineno, colno)
|
||||
|
||||
def __str__(self):
|
||||
s = SchemaError.__str__(self)
|
||||
if self.package is not None:
|
||||
s += "\n Package name: " + repr(self.package)
|
||||
if self.filename is not None:
|
||||
s += "\n File name: " + repr(self.filename)
|
||||
if self.package is not None:
|
||||
s += "\n Package path: " + repr(self.path)
|
||||
return s
|
||||
|
||||
|
||||
class ConfigurationSyntaxError(_ParseError):
|
||||
"""Exception raised when a configuration source does not conform to
|
||||
the allowed syntax.
|
||||
|
||||
In addition to the ``message`` and ``url`` attributes, exceptions
|
||||
of this type offer the ``lineno`` attribute, which provides the
|
||||
line number at which the error was detected.
|
||||
"""
|
||||
|
||||
|
||||
class DataConversionError(ConfigurationError, ValueError):
|
||||
"""Raised when a data type conversion fails with :exc:`ValueError`.
|
||||
|
||||
This exception is a subclass of both :exc:`ConfigurationError` and
|
||||
:exc:`ValueError`. The :func:`str` of the exception provides the
|
||||
explanation from the original :exc:`ValueError`, and the line
|
||||
number and URL of the value which provoked the error. The
|
||||
following additional attributes are provided:
|
||||
|
||||
``colno``
|
||||
column number at which the value starts, or ``None``
|
||||
``exception``
|
||||
the original :exc:`ValueError` instance
|
||||
``lineno``
|
||||
line number on which the value starts
|
||||
``message``
|
||||
:func:`str` returned by the original :exc:`ValueError`
|
||||
``value``
|
||||
original value passed to the conversion function
|
||||
``url``
|
||||
URL of the resource providing the value text
|
||||
"""
|
||||
|
||||
def __init__(self, exception, value, position):
|
||||
ConfigurationError.__init__(self, str(exception))
|
||||
self.exception = exception
|
||||
self.value = value
|
||||
self.lineno, self.colno, self.url = position
|
||||
|
||||
def __str__(self):
|
||||
s = "%s (line %s" % (self.message, self.lineno)
|
||||
if self.colno is not None:
|
||||
s += ", %s" % self.colno
|
||||
if self.url:
|
||||
s += ", in %s)" % self.url
|
||||
else:
|
||||
s += ")"
|
||||
return s
|
||||
|
||||
|
||||
class SubstitutionSyntaxError(ConfigurationError):
|
||||
"""Raised when interpolation source text contains syntactical errors."""
|
||||
|
||||
|
||||
class SubstitutionReplacementError(ConfigurationSyntaxError, LookupError):
|
||||
"""Raised when the source text contains references to names which are
|
||||
not defined in *mapping*.
|
||||
|
||||
The attributes ``source`` and ``name`` provide the complete source
|
||||
text and the name (converted to lower case) for which no replacement
|
||||
is defined.
|
||||
"""
|
||||
|
||||
def __init__(self, source, name, url=None, lineno=None):
|
||||
self.source = source
|
||||
self.name = name
|
||||
ConfigurationSyntaxError.__init__(
|
||||
self, "no replacement for " + repr(name), url, lineno)
|
||||
|
||||
|
||||
def configureLoggers(text):
|
||||
"""Configure one or more loggers from configuration text."""
|
||||
schema = loadSchemaFile(TextIO("""
|
||||
<schema>
|
||||
<import package='ZConfig.components.logger'/>
|
||||
<multisection type='logger' name='*' attribute='loggers'/>
|
||||
</schema>
|
||||
"""))
|
||||
|
||||
for factory in loadConfigFile(schema, TextIO(text))[0].loggers:
|
||||
factory()
|
||||
@@ -0,0 +1,101 @@
|
||||
##############################################################################
|
||||
#
|
||||
# Copyright (c) 2016 Zope Foundation and Contributors.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# This software is subject to the provisions of the Zope Public License,
|
||||
# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
|
||||
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
|
||||
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
|
||||
# FOR A PARTICULAR PURPOSE.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
import sys
|
||||
|
||||
PY3 = sys.version_info[0] >= 3
|
||||
|
||||
# Native string object IO
|
||||
if str is not bytes:
|
||||
from io import StringIO as NStringIO
|
||||
string_types = str
|
||||
else:
|
||||
# Python 2
|
||||
from io import BytesIO as NStringIO
|
||||
string_types = str, unicode
|
||||
|
||||
NStringIO = NStringIO
|
||||
|
||||
from io import StringIO
|
||||
from io import BytesIO
|
||||
|
||||
def TextIO(text):
|
||||
"Return StringIO or BytesIO as appropriate"
|
||||
return BytesIO(text) if isinstance(text, bytes) else StringIO(text)
|
||||
|
||||
try:
|
||||
import urllib2
|
||||
except ImportError:
|
||||
# Python 3 support.
|
||||
import urllib.request as urllib2
|
||||
|
||||
urllib2 = urllib2
|
||||
|
||||
try:
|
||||
from urllib import pathname2url
|
||||
except ImportError:
|
||||
# Python 3 support.
|
||||
from urllib.request import pathname2url
|
||||
|
||||
pathname2url = pathname2url
|
||||
|
||||
try:
|
||||
import urlparse as urlparse
|
||||
except ImportError:
|
||||
# Python 3 support
|
||||
import urllib.parse as urlparse
|
||||
|
||||
urlparse = urlparse
|
||||
|
||||
if PY3: # pragma: no cover
|
||||
import builtins
|
||||
exec_ = getattr(builtins, "exec")
|
||||
text_type = str
|
||||
binary_type = bytes
|
||||
maxsize = sys.maxsize
|
||||
|
||||
def reraise(tp, value, tb=None): #pragma NO COVER
|
||||
if value.__traceback__ is not tb:
|
||||
raise value.with_traceback(tb)
|
||||
raise value
|
||||
|
||||
else: # pragma: no cover
|
||||
text_type = unicode
|
||||
binary_type = bytes
|
||||
maxsize = sys.maxint
|
||||
|
||||
def exec_(code, globs=None, locs=None): #pragma NO COVER
|
||||
"""Execute code in a namespace."""
|
||||
if globs is None:
|
||||
frame = sys._getframe(1)
|
||||
globs = frame.f_globals
|
||||
if locs is None:
|
||||
locs = frame.f_locals
|
||||
del frame
|
||||
elif locs is None:
|
||||
locs = globs
|
||||
exec("""exec code in globs, locs""")
|
||||
|
||||
exec_("""def reraise(tp, value, tb=None):
|
||||
raise tp, value, tb
|
||||
""")
|
||||
|
||||
|
||||
def raise_with_same_tb(exception):
|
||||
"Raise an exception having the current traceback (if there is one)"
|
||||
reraise(type(exception), exception, sys.exc_info()[2])
|
||||
|
||||
import abc
|
||||
# workaround the metaclass diff in Py2/Py3
|
||||
AbstractBaseClass = abc.ABCMeta('AbstractBaseClass', (object,), {})
|
||||
@@ -0,0 +1,314 @@
|
||||
##############################################################################
|
||||
#
|
||||
# Copyright (c) 2017 Zope Corporation and Contributors.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# This software is subject to the provisions of the Zope Public License,
|
||||
# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
|
||||
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
|
||||
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
|
||||
# FOR A PARTICULAR PURPOSE.
|
||||
#
|
||||
##############################################################################
|
||||
from __future__ import print_function
|
||||
|
||||
from abc import abstractmethod
|
||||
import argparse
|
||||
import itertools
|
||||
import sys
|
||||
import textwrap
|
||||
|
||||
try:
|
||||
from itertools import ifilterfalse
|
||||
from itertools import ifilter
|
||||
except ImportError:
|
||||
# Py3
|
||||
from itertools import filterfalse as ifilterfalse
|
||||
ifilter = filter
|
||||
|
||||
import ZConfig.loader
|
||||
|
||||
from ZConfig._compat import AbstractBaseClass
|
||||
|
||||
from ZConfig.datatypes import null_conversion
|
||||
from ZConfig.info import SectionType
|
||||
from ZConfig.info import SectionInfo
|
||||
from ZConfig.info import ValueInfo
|
||||
from ZConfig.info import MultiKeyInfo
|
||||
from ZConfig.info import AbstractType
|
||||
|
||||
|
||||
class _VisitorBuilder(object):
|
||||
|
||||
def __init__(self):
|
||||
self.visitors = []
|
||||
|
||||
def __call__(self, Type):
|
||||
def dec(func):
|
||||
self.visitors.append((Type, func))
|
||||
return func
|
||||
return dec
|
||||
|
||||
MARKER = object()
|
||||
|
||||
class AbstractSchemaFormatter(AbstractBaseClass):
|
||||
|
||||
def __init__(self, schema, stream=None):
|
||||
self.stream = stream or sys.stdout
|
||||
self._dt = schema.registry.find_name
|
||||
|
||||
def write(self, *args):
|
||||
print(*args, file=self.stream)
|
||||
|
||||
@abstractmethod
|
||||
def esc(self, x):
|
||||
"Escape blocks of text if needed"
|
||||
|
||||
def _dedent(self, text):
|
||||
# dedent the text to avoid producing unwanted
|
||||
# definition lists. The XML parser strips leading whitespace from
|
||||
# the first line, but preserves it for subsequent lines, so for dedent
|
||||
# to work we have to ignore that first line.
|
||||
texts = text.split("\n")
|
||||
if len(texts) > 1:
|
||||
trail = textwrap.dedent('\n'.join(texts[1:]))
|
||||
text = texts[0] + '\n' + trail
|
||||
return text
|
||||
|
||||
@abstractmethod
|
||||
def item_list(self):
|
||||
"Context manager for listing description items"
|
||||
|
||||
def _describing(self, description, after):
|
||||
if description is not MARKER:
|
||||
with self.described_as():
|
||||
self.description(description)
|
||||
if after:
|
||||
after()
|
||||
|
||||
@abstractmethod
|
||||
def describing(self, description=MARKER, after=None):
|
||||
"description term, optional body"
|
||||
|
||||
def describing_name(self, concrete_name,
|
||||
description=MARKER, datatype=None,
|
||||
**kwargs):
|
||||
with self.describing(description):
|
||||
self.concrete_name(concrete_name)
|
||||
self.datatype(datatype)
|
||||
|
||||
for k, v in sorted(kwargs.items()):
|
||||
if v:
|
||||
self.write(self.esc("(%s: %s)" % (k, v)))
|
||||
|
||||
def description(self, description):
|
||||
if description:
|
||||
self.write(self.esc(description))
|
||||
|
||||
example = description
|
||||
|
||||
@abstractmethod
|
||||
def described_as(self):
|
||||
"Description body context manager"
|
||||
|
||||
@abstractmethod
|
||||
def abstract_name(self, name):
|
||||
"Abstract name"
|
||||
|
||||
@abstractmethod
|
||||
def concrete_name(self, *name):
|
||||
"Concrete name"
|
||||
|
||||
@abstractmethod
|
||||
def concrete_section_name(self, *name):
|
||||
"Name of a section a user can type in a config"
|
||||
|
||||
def datatype(self, datatype):
|
||||
self.write("(%s)" % self._dt(datatype))
|
||||
|
||||
@abstractmethod
|
||||
def body(self):
|
||||
"Context manager for the whole document"
|
||||
|
||||
|
||||
class AbstractSchemaPrinter(AbstractBaseClass):
|
||||
|
||||
|
||||
def __init__(self, schema, stream=None, allowed_names=(), excluded_names=()):
|
||||
self.schema = schema
|
||||
stream = stream or sys.stdout
|
||||
self._explained = set()
|
||||
self._seen_typenames = set()
|
||||
self.fmt = self._schema_formatter(schema, stream)
|
||||
|
||||
|
||||
def _make_predicate(names):
|
||||
names = {x.lower() for x in names}
|
||||
def predicate(name_info):
|
||||
name, _ = name_info
|
||||
return name and name.lower() in names
|
||||
return predicate
|
||||
|
||||
def _make_filter(names, filt):
|
||||
iter_all = self._iter_schema_items
|
||||
pred = _make_predicate(names)
|
||||
def it():
|
||||
return filt(pred, iter_all())
|
||||
return it
|
||||
|
||||
if allowed_names:
|
||||
self._iter_schema_items = _make_filter(allowed_names, ifilter)
|
||||
|
||||
if excluded_names:
|
||||
excluded_names = {x.lower() for x in excluded_names}
|
||||
self._iter_schema_items = _make_filter(excluded_names, ifilterfalse)
|
||||
self._included = lambda st: st.name not in excluded_names
|
||||
|
||||
@abstractmethod
|
||||
def _schema_formatter(self, schema, stream):
|
||||
"Return a formatter"
|
||||
|
||||
def _included(self, st):
|
||||
return True
|
||||
|
||||
def _explain(self, st):
|
||||
if st.name in self._explained: # pragma: no cover
|
||||
return
|
||||
|
||||
self._explained.add(st.name)
|
||||
|
||||
self.fmt.description(st.description)
|
||||
if not self._included(st):
|
||||
return
|
||||
|
||||
self.fmt.example(getattr(st, 'example', None))
|
||||
|
||||
for sub in st.getsubtypenames():
|
||||
with self.fmt.item_list():
|
||||
self.visit(None, st.getsubtype(sub))
|
||||
|
||||
def _iter_schema_items(self):
|
||||
def everything():
|
||||
return itertools.chain(self.schema.itertypes(),
|
||||
self.schema)
|
||||
# The abstract types tend to be the most important. Since
|
||||
# we only document a concrete type the first time we find it,
|
||||
# and we can find extensions of abstract types beneath
|
||||
# the abstract type which is itself buried under a concrete section,
|
||||
# all the different permutations would be only documented once under
|
||||
# that section. By exposing these first, they get documented at the top-level,
|
||||
# and each concrete section that uses the abstract type gets a reference
|
||||
# to it.
|
||||
|
||||
def abstract_sections(base):
|
||||
for name, info in base:
|
||||
if isinstance(info, SectionInfo):
|
||||
if info.sectiontype.isabstract():
|
||||
yield name, info
|
||||
|
||||
# XXX: This isn't catching everything. Witness the
|
||||
# relstorage component.
|
||||
elif isinstance(info, SectionType):
|
||||
for x in abstract_sections(info):
|
||||
yield x
|
||||
return itertools.chain(abstract_sections(everything()), everything())
|
||||
|
||||
def printSchema(self):
|
||||
# side-effect of building may be printing
|
||||
self.buildSchema()
|
||||
|
||||
def buildSchema(self):
|
||||
seen = set() # prevent duplicates at the top-level
|
||||
# as we find multiple abstract types
|
||||
with self.fmt.body():
|
||||
with self.fmt.item_list():
|
||||
for name, info in self._iter_schema_items():
|
||||
if info in seen:
|
||||
continue
|
||||
seen.add(info)
|
||||
self.visit(name, info)
|
||||
|
||||
TypeVisitor = _VisitorBuilder()
|
||||
visitors = TypeVisitor.visitors
|
||||
|
||||
def visit(self, name, info):
|
||||
for t, f in self.visitors:
|
||||
if isinstance(info, t):
|
||||
f(self, name, info)
|
||||
break
|
||||
else:
|
||||
self._visit_default(name, info)
|
||||
|
||||
@TypeVisitor(SectionType)
|
||||
def _visit_SectionType(self, name, info):
|
||||
if info.name in self._seen_typenames:
|
||||
return
|
||||
self._seen_typenames.add(info.name)
|
||||
with self.fmt.describing():
|
||||
if info.datatype is not null_conversion:
|
||||
self.fmt.concrete_section_name(info.name)
|
||||
else:
|
||||
self.fmt.abstract_name(info.name)
|
||||
self.fmt.datatype(info.datatype)
|
||||
|
||||
with self.fmt.described_as():
|
||||
self.fmt.description(info.description)
|
||||
self.fmt.example(info.example)
|
||||
|
||||
with self.fmt.item_list():
|
||||
for sub in info:
|
||||
self.visit(*sub) # pragma: no cover
|
||||
|
||||
|
||||
@TypeVisitor(SectionInfo)
|
||||
def _visit_SectionInfo(self, name, info):
|
||||
st = info.sectiontype
|
||||
if st.isabstract():
|
||||
with self.fmt.describing(info.description, lambda: self._explain(st)):
|
||||
self.fmt.abstract_name(st.name)
|
||||
self.fmt.concrete_name(info.name)
|
||||
|
||||
else:
|
||||
with self.fmt.describing():
|
||||
self.fmt.concrete_section_name(info.attribute, info.name)
|
||||
self.fmt.datatype(info.datatype)
|
||||
|
||||
with self.fmt.described_as():
|
||||
with self.fmt.item_list():
|
||||
for sub in info.sectiontype:
|
||||
self.visit(*sub)
|
||||
|
||||
self.fmt.example(info.example)
|
||||
|
||||
@TypeVisitor(AbstractType)
|
||||
def _visit_AbstractType(self, name, info):
|
||||
with self.fmt.describing(info.description, lambda: self._explain(info)):
|
||||
self.fmt.abstract_name(info.name)
|
||||
|
||||
def _visit_default(self, name, info):
|
||||
# KeyInfo or MultiKeyInfo
|
||||
default = info.getdefault()
|
||||
if isinstance(default, ValueInfo):
|
||||
default = default.value
|
||||
|
||||
name = info.name
|
||||
if isinstance(info, MultiKeyInfo):
|
||||
name = name + " (*)"
|
||||
self.fmt.describing_name(name, info.description, info.datatype,
|
||||
default=default, metadefault=info.metadefault)
|
||||
|
||||
del TypeVisitor
|
||||
|
||||
|
||||
def load_schema(schema, package, package_file):
|
||||
if not package:
|
||||
schema_reader = argparse.FileType('r')(schema)
|
||||
else:
|
||||
schema_template = "<schema><import package='%s' file='%s' /></schema>" % (
|
||||
schema, package_file or 'component.xml')
|
||||
from ZConfig._compat import TextIO
|
||||
schema_reader = TextIO(schema_template)
|
||||
|
||||
schema = ZConfig.loader.loadSchemaFile(schema_reader)
|
||||
return schema
|
||||
@@ -0,0 +1,192 @@
|
||||
##############################################################################
|
||||
#
|
||||
# Copyright (c) 2002, 2003 Zope Foundation and Contributors.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# This software is subject to the provisions of the Zope Public License,
|
||||
# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
|
||||
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
|
||||
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
|
||||
# FOR A PARTICULAR PURPOSE.
|
||||
#
|
||||
##############################################################################
|
||||
"""Configuration parser."""
|
||||
|
||||
import ZConfig
|
||||
import ZConfig.url
|
||||
|
||||
from ZConfig.substitution import isname, substitute
|
||||
from ZConfig._compat import raise_with_same_tb
|
||||
|
||||
class ZConfigParser(object):
|
||||
|
||||
__slots__ = ('resource', 'context', 'lineno',
|
||||
'stack', 'defines', 'file', 'url')
|
||||
|
||||
def __init__(self, resource, context, defines=None):
|
||||
self.resource = resource
|
||||
self.context = context
|
||||
self.file = resource.file
|
||||
self.url = resource.url
|
||||
self.lineno = 0
|
||||
self.stack = [] # [(type, name, prevmatcher), ...]
|
||||
if defines is None:
|
||||
defines = {}
|
||||
self.defines = defines
|
||||
|
||||
def nextline(self):
|
||||
line = self.file.readline()
|
||||
if line:
|
||||
self.lineno += 1
|
||||
return False, line.strip()
|
||||
return True, None
|
||||
|
||||
def parse(self, section):
|
||||
done, line = self.nextline()
|
||||
while not done:
|
||||
if line[:1] in ("", "#"):
|
||||
# blank line or comment
|
||||
pass
|
||||
|
||||
elif line[:2] == "</":
|
||||
# section end
|
||||
if line[-1] != ">":
|
||||
self.error("malformed section end")
|
||||
section = self.end_section(section, line[2:-1])
|
||||
|
||||
elif line[0] == "<":
|
||||
# section start
|
||||
if line[-1] != ">":
|
||||
self.error("malformed section start")
|
||||
section = self.start_section(section, line[1:-1])
|
||||
|
||||
elif line[0] == "%":
|
||||
self.handle_directive(section, line[1:])
|
||||
|
||||
else:
|
||||
self.handle_key_value(section, line)
|
||||
|
||||
done, line = self.nextline()
|
||||
|
||||
if self.stack:
|
||||
self.error("unclosed sections not allowed")
|
||||
|
||||
def start_section(self, section, rest):
|
||||
isempty = rest[-1:] == "/"
|
||||
if isempty:
|
||||
rest = rest[:-1]
|
||||
text = rest.rstrip()
|
||||
# parse section start stuff here
|
||||
m = _section_start_rx.match(text)
|
||||
if not m:
|
||||
self.error("malformed section header")
|
||||
type_, name = m.group('type', 'name')
|
||||
type_ = self._normalize_case(type_)
|
||||
if name:
|
||||
name = self._normalize_case(name)
|
||||
try:
|
||||
newsect = self.context.startSection(section, type_, name)
|
||||
except ZConfig.ConfigurationError as e:
|
||||
self.error(e.message)
|
||||
|
||||
if isempty:
|
||||
self.context.endSection(section, type_, name, newsect)
|
||||
return section
|
||||
|
||||
self.stack.append((type_, name, section))
|
||||
return newsect
|
||||
|
||||
def end_section(self, section, rest):
|
||||
if not self.stack:
|
||||
self.error("unexpected section end")
|
||||
type_ = self._normalize_case(rest.rstrip())
|
||||
opentype, name, prevsection = self.stack.pop()
|
||||
if type_ != opentype:
|
||||
self.error("unbalanced section end")
|
||||
try:
|
||||
self.context.endSection(
|
||||
prevsection, type_, name, section)
|
||||
except ZConfig.ConfigurationError as e:
|
||||
self.error(e.args[0])
|
||||
return prevsection
|
||||
|
||||
def handle_key_value(self, section, rest):
|
||||
m = _keyvalue_rx.match(rest)
|
||||
if not m:
|
||||
self.error("malformed configuration data")
|
||||
key, value = m.group('key', 'value')
|
||||
if not value:
|
||||
value = ''
|
||||
else:
|
||||
value = self.replace(value)
|
||||
try:
|
||||
section.addValue(key, value, (self.lineno, None, self.url))
|
||||
except ZConfig.ConfigurationError as e:
|
||||
self.error(e.args[0])
|
||||
|
||||
def handle_directive(self, section, rest):
|
||||
m = _keyvalue_rx.match(rest)
|
||||
if not m:
|
||||
self.error("missing or unrecognized directive")
|
||||
name, arg = m.group('key', 'value')
|
||||
if name not in ("define", "import", "include"):
|
||||
self.error("unknown directive: " + repr(name))
|
||||
if not arg:
|
||||
self.error("missing argument to %%%s directive" % name)
|
||||
|
||||
getattr(self, 'handle_' + name)(section, arg)
|
||||
|
||||
def handle_import(self, section, rest):
|
||||
pkgname = self.replace(rest.strip())
|
||||
self.context.importSchemaComponent(pkgname)
|
||||
|
||||
def handle_include(self, section, rest):
|
||||
rest = self.replace(rest.strip())
|
||||
newurl = ZConfig.url.urljoin(self.url, rest)
|
||||
self.context.includeConfiguration(section, newurl, self.defines)
|
||||
|
||||
def handle_define(self, section, rest):
|
||||
parts = rest.split(None, 1)
|
||||
defname = self._normalize_case(parts[0])
|
||||
defvalue = ''
|
||||
if len(parts) == 2:
|
||||
defvalue = parts[1]
|
||||
if defname in self.defines:
|
||||
if self.defines[defname] != defvalue:
|
||||
self.error("cannot redefine " + repr(defname))
|
||||
if not isname(defname):
|
||||
self.error("not a substitution legal name: " + repr(defname))
|
||||
self.defines[defname] = self.replace(defvalue)
|
||||
|
||||
def replace(self, text):
|
||||
try:
|
||||
return substitute(text, self.defines)
|
||||
except ZConfig.SubstitutionReplacementError as e:
|
||||
e.lineno = self.lineno
|
||||
e.url = self.url
|
||||
raise
|
||||
|
||||
def error(self, message):
|
||||
raise_with_same_tb(
|
||||
ZConfig.ConfigurationSyntaxError(
|
||||
message, self.url, self.lineno))
|
||||
|
||||
|
||||
def _normalize_case(self, string):
|
||||
# This method is factored out solely to allow subclasses to modify
|
||||
# the behavior of the parser.
|
||||
return string.lower()
|
||||
|
||||
|
||||
import re
|
||||
# _name_re does not allow "(" or ")" for historical reasons. Though
|
||||
# the restriction could be lifted, there seems no need to do so.
|
||||
_name_re = r"[^\s()]+"
|
||||
_keyvalue_rx = re.compile(r"(?P<key>%s)\s*(?P<value>[^\s].*)?$"
|
||||
% _name_re)
|
||||
_section_start_rx = re.compile(r"(?P<type>%s)"
|
||||
r"(?:\s+(?P<name>%s))?"
|
||||
r"$"
|
||||
% (_name_re, _name_re))
|
||||
del re
|
||||
@@ -0,0 +1,212 @@
|
||||
##############################################################################
|
||||
#
|
||||
# Copyright (c) 2003 Zope Foundation and Contributors.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# This software is subject to the provisions of the Zope Public License,
|
||||
# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
|
||||
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
|
||||
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
|
||||
# FOR A PARTICULAR PURPOSE.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
"""Support for command-line overrides for configuration settings.
|
||||
|
||||
This module exports an extended version of the :class:`~.ConfigLoader`
|
||||
class from the :mod:`ZConfig.loader` module. This provides support for
|
||||
overriding specific settings from the configuration file from the
|
||||
command line, without requiring the application to provide specific
|
||||
options for everything the configuration file can include.
|
||||
|
||||
Each setting is given by a value specifier string, as described by
|
||||
:meth:`ExtendedConfigLoader.addOption`.
|
||||
"""
|
||||
|
||||
import ZConfig
|
||||
import ZConfig.loader
|
||||
import ZConfig.matcher
|
||||
|
||||
from ZConfig._compat import raise_with_same_tb
|
||||
|
||||
class ExtendedConfigLoader(ZConfig.loader.ConfigLoader):
|
||||
"""A :class:`~.ConfigLoader` subclass that adds support for
|
||||
command-line overrides.
|
||||
"""
|
||||
|
||||
def __init__(self, schema):
|
||||
ZConfig.loader.ConfigLoader.__init__(self, schema)
|
||||
self.clopts = [] # [(optpath, value, source-position), ...]
|
||||
|
||||
def addOption(self, spec, pos=None):
|
||||
"""Add a single value to the list of overridden values.
|
||||
|
||||
The *spec* argument is a value specifier string of the form
|
||||
``optionpath=value``. For example::
|
||||
|
||||
some/path/to/key=value
|
||||
|
||||
The *optionpath* specifies the "full path" to the
|
||||
configuration setting: it can contain a sequence of names,
|
||||
separated by ``/`` characters. Each name before the last names
|
||||
a section from the configuration file, and the last name
|
||||
corresponds to a key within the section identified by the
|
||||
leading section names. If *optionpath* contains only one name,
|
||||
it identifies a key in the top-level schema. *value* is a
|
||||
string that will be treated just like a value in the
|
||||
configuration file.
|
||||
|
||||
A source position for the specifier may be given as *pos*. If
|
||||
*pos* is specified and not ``None``, it must be a sequence of
|
||||
three values. The first is the URL of the source (or some
|
||||
other identifying string). The second and third are the line
|
||||
number and column of the setting. These position information
|
||||
is only used to construct a :exc:`~.DataConversionError` when
|
||||
data conversion fails.
|
||||
"""
|
||||
if pos is None:
|
||||
pos = "<command-line option>", -1, -1
|
||||
if "=" not in spec:
|
||||
e = ZConfig.ConfigurationSyntaxError(
|
||||
"invalid configuration specifier", *pos)
|
||||
e.specifier = spec
|
||||
raise e
|
||||
# For now, just add it to the list; not clear that checking
|
||||
# against the schema at this point buys anything.
|
||||
opt, val = spec.split("=", 1)
|
||||
optpath = opt.split("/")
|
||||
if "" in optpath:
|
||||
# // is not allowed in option path
|
||||
e = ZConfig.ConfigurationSyntaxError(
|
||||
"'//' is not allowed in an option path", *pos)
|
||||
e.specifier = spec
|
||||
raise e
|
||||
self.clopts.append((optpath, val, pos))
|
||||
|
||||
def createSchemaMatcher(self):
|
||||
if self.clopts:
|
||||
sm = ExtendedSchemaMatcher(self.schema)
|
||||
sm.set_optionbag(self.cook())
|
||||
else:
|
||||
sm = ZConfig.loader.ConfigLoader.createSchemaMatcher(self)
|
||||
return sm
|
||||
|
||||
def cook(self):
|
||||
if self.clopts:
|
||||
return OptionBag(self.schema, self.schema, self.clopts)
|
||||
|
||||
|
||||
class OptionBag(object):
|
||||
def __init__(self, schema, sectiontype, options):
|
||||
self.sectiontype = sectiontype
|
||||
self.schema = schema
|
||||
self.keypairs = {}
|
||||
self.sectitems = []
|
||||
self._basic_key = schema.registry.get("basic-key")
|
||||
for item in options:
|
||||
optpath, val, pos = item
|
||||
name = sectiontype.keytype(optpath[0])
|
||||
if len(optpath) == 1:
|
||||
self.add_value(name, val, pos)
|
||||
else:
|
||||
self.sectitems.append(item)
|
||||
|
||||
def basic_key(self, s, pos):
|
||||
try:
|
||||
return self._basic_key(s)
|
||||
except ValueError as e:
|
||||
raise_with_same_tb(ZConfig.ConfigurationSyntaxError(
|
||||
"could not convert basic-key value: " + str(e), *pos))
|
||||
|
||||
def add_value(self, name, val, pos):
|
||||
if name in self.keypairs:
|
||||
L = self.keypairs[name]
|
||||
else:
|
||||
L = []
|
||||
self.keypairs[name] = L
|
||||
L.append((val, pos))
|
||||
|
||||
def __contains__(self, name):
|
||||
return name in self.keypairs
|
||||
|
||||
def get_key(self, name):
|
||||
"""Return a list of (value, pos) items for the key 'name'.
|
||||
|
||||
The returned list may be empty.
|
||||
"""
|
||||
L = self.keypairs.get(name)
|
||||
if L:
|
||||
del self.keypairs[name]
|
||||
return L
|
||||
return []
|
||||
|
||||
def keys(self):
|
||||
return self.keypairs.keys()
|
||||
|
||||
def get_section_info(self, type_, name):
|
||||
L = [] # what pertains to the child section
|
||||
R = [] # what we keep
|
||||
for item in self.sectitems:
|
||||
optpath, val, pos = item
|
||||
s = optpath[0]
|
||||
bk = self.basic_key(s, pos)
|
||||
if name and self._normalize_case(s) == name:
|
||||
L.append((optpath[1:], val, pos))
|
||||
elif bk == type_: # pragma: no cover
|
||||
L.append((optpath[1:], val, pos))
|
||||
else:
|
||||
R.append(item)
|
||||
if L:
|
||||
self.sectitems[:] = R
|
||||
return OptionBag(self.schema, self.schema.gettype(type_), L)
|
||||
|
||||
def finish(self):
|
||||
if self.sectitems or self.keypairs:
|
||||
raise ZConfig.ConfigurationError(
|
||||
"not all command line options were consumed")
|
||||
|
||||
def _normalize_case(self, string):
|
||||
return string.lower()
|
||||
|
||||
|
||||
class MatcherMixin(object):
|
||||
|
||||
def set_optionbag(self, bag):
|
||||
self.optionbag = bag
|
||||
|
||||
def addValue(self, key, value, position):
|
||||
try:
|
||||
realkey = self.type.keytype(key)
|
||||
except ValueError as e:
|
||||
raise_with_same_tb(ZConfig.DataConversionError(e, key, position))
|
||||
|
||||
if realkey in self.optionbag:
|
||||
return
|
||||
ZConfig.matcher.BaseMatcher.addValue(self, key, value, position)
|
||||
|
||||
def createChildMatcher(self, type_, name):
|
||||
sm = ZConfig.matcher.BaseMatcher.createChildMatcher(self, type_, name)
|
||||
bag = self.optionbag.get_section_info(type_.name, name)
|
||||
if bag is not None:
|
||||
sm = ExtendedSectionMatcher(
|
||||
sm.info, sm.type, sm.name, sm.handlers)
|
||||
sm.set_optionbag(bag)
|
||||
return sm
|
||||
|
||||
def finish_optionbag(self):
|
||||
for key in list(self.optionbag.keys()):
|
||||
for val, pos in self.optionbag.get_key(key):
|
||||
ZConfig.matcher.BaseMatcher.addValue(self, key, val, pos)
|
||||
self.optionbag.finish()
|
||||
|
||||
|
||||
class ExtendedSectionMatcher(MatcherMixin, ZConfig.matcher.SectionMatcher):
|
||||
def finish(self):
|
||||
self.finish_optionbag()
|
||||
return ZConfig.matcher.SectionMatcher.finish(self)
|
||||
|
||||
class ExtendedSchemaMatcher(MatcherMixin, ZConfig.matcher.SchemaMatcher):
|
||||
def finish(self):
|
||||
self.finish_optionbag()
|
||||
return ZConfig.matcher.SchemaMatcher.finish(self)
|
||||
@@ -0,0 +1 @@
|
||||
# This is a Python package.
|
||||
@@ -0,0 +1 @@
|
||||
# This is a Python package.
|
||||
@@ -0,0 +1,9 @@
|
||||
<component>
|
||||
<description>
|
||||
Convenient loader which causes all the "basic" components to be
|
||||
loaded.
|
||||
</description>
|
||||
|
||||
<import package="ZConfig.components.basic" file="mapping.xml"/>
|
||||
|
||||
</component>
|
||||
@@ -0,0 +1,18 @@
|
||||
##############################################################################
|
||||
#
|
||||
# Copyright (c) 2003 Zope Foundation and Contributors.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# This software is subject to the provisions of the Zope Public License,
|
||||
# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
|
||||
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
|
||||
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
|
||||
# FOR A PARTICULAR PURPOSE.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
"""Python datatype for the ZConfig.components.basic.mapping section type."""
|
||||
|
||||
def mapping(section):
|
||||
return section.mapping
|
||||
@@ -0,0 +1,34 @@
|
||||
<component>
|
||||
|
||||
<sectiontype name="ZConfig.basic.mapping"
|
||||
datatype="ZConfig.components.basic.mapping.mapping">
|
||||
<description>
|
||||
Section that provides a simple mapping implementation. An
|
||||
application should derive a more specific section type for use
|
||||
in configuration files:
|
||||
|
||||
<import package="ZConfig.components.basic"
|
||||
file="mapping.xml"
|
||||
/>
|
||||
|
||||
<sectiontype name="mapping"
|
||||
extends="ZConfig.basic.mapping"
|
||||
/>
|
||||
|
||||
If a non-standard keytype is needed, it can be overridden as
|
||||
well:
|
||||
|
||||
<sectiontype name="system-map"
|
||||
extends="ZConfig.basic.mapping"
|
||||
keytype="mypkg.datatypes.system_name"
|
||||
/>
|
||||
|
||||
</description>
|
||||
|
||||
<key name="+"
|
||||
attribute="mapping"
|
||||
required="no"
|
||||
/>
|
||||
</sectiontype>
|
||||
|
||||
</component>
|
||||
@@ -0,0 +1 @@
|
||||
# This is a Python package.
|
||||
@@ -0,0 +1,91 @@
|
||||
##############################################################################
|
||||
#
|
||||
# Copyright (c) 2003 Zope Foundation and Contributors.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# This software is subject to the provisions of the Zope Public License,
|
||||
# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
|
||||
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
|
||||
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
|
||||
# FOR A PARTICULAR PURPOSE.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
"""Tests of the 'basic' section types provided as part of
|
||||
ZConfig.components.basic."""
|
||||
|
||||
import ZConfig.tests.support
|
||||
import unittest
|
||||
|
||||
|
||||
SIMPLE_SCHEMA = '''\
|
||||
<schema>
|
||||
<import package="ZConfig.components.basic" file="mapping.xml" />
|
||||
|
||||
<sectiontype name="dict"
|
||||
extends="ZConfig.basic.mapping" />
|
||||
|
||||
<sectiontype name="intkeys"
|
||||
extends="ZConfig.basic.mapping"
|
||||
keytype="integer" />
|
||||
|
||||
<section name="*"
|
||||
type="dict"
|
||||
attribute="simple_dict" />
|
||||
|
||||
<section name="*"
|
||||
type="intkeys"
|
||||
attribute="int_dict" />
|
||||
|
||||
</schema>
|
||||
'''
|
||||
|
||||
|
||||
class BasicSectionTypeTestCase(
|
||||
ZConfig.tests.support.TestHelper, unittest.TestCase):
|
||||
|
||||
schema = None
|
||||
|
||||
def setUp(self):
|
||||
if self.schema is None:
|
||||
self.__class__.schema = self.load_schema_text(SIMPLE_SCHEMA)
|
||||
|
||||
def test_simple_empty_dict(self):
|
||||
conf = self.load_config_text(self.schema, "<dict/>")
|
||||
self.assertEqual(conf.simple_dict, {})
|
||||
conf = self.load_config_text(self.schema, """\
|
||||
<dict foo>
|
||||
# comment
|
||||
</dict>
|
||||
""")
|
||||
self.assertEqual(conf.simple_dict, {})
|
||||
|
||||
def test_simple_dict(self):
|
||||
conf = self.load_config_text(self.schema, """\
|
||||
<dict foo>
|
||||
key-one value-one
|
||||
key-two value-two
|
||||
</dict>
|
||||
""")
|
||||
L = sorted(conf.simple_dict.items())
|
||||
self.assertEqual(L, [("key-one", "value-one"),
|
||||
("key-two", "value-two")])
|
||||
|
||||
def test_derived_dict(self):
|
||||
conf = self.load_config_text(self.schema, """\
|
||||
<intkeys>
|
||||
1 foo
|
||||
2 bar
|
||||
42 question?
|
||||
</intkeys>
|
||||
""")
|
||||
L = sorted(conf.int_dict.items())
|
||||
self.assertEqual(L, [(1, "foo"), (2, "bar"), (42, "question?")])
|
||||
|
||||
|
||||
def test_suite():
|
||||
return unittest.defaultTestLoader.loadTestsFromName(__name__)
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main(defaultTest='test_suite')
|
||||
@@ -0,0 +1,14 @@
|
||||
##############################################################################
|
||||
#
|
||||
# Copyright (c) 2003 Zope Foundation and Contributors.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# This software is subject to the provisions of the Zope Public License,
|
||||
# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
|
||||
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
|
||||
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
|
||||
# FOR A PARTICULAR PURPOSE.
|
||||
#
|
||||
##############################################################################
|
||||
"""ZConfig schema component package for logging configuration."""
|
||||
@@ -0,0 +1,7 @@
|
||||
<component>
|
||||
<description>
|
||||
</description>
|
||||
|
||||
<abstracttype name="ZConfig.logger.handler"/>
|
||||
<abstracttype name="ZConfig.logger.log"/>
|
||||
</component>
|
||||
@@ -0,0 +1,58 @@
|
||||
<component prefix="ZConfig.components.logger.logger">
|
||||
|
||||
<import package="ZConfig.components.logger" file="abstract.xml"/>
|
||||
|
||||
<sectiontype name="ZConfig.logger.base-logger">
|
||||
<description>
|
||||
Base definition for the logger types defined by
|
||||
ZConfig.components.logger. This exists entirely to provide
|
||||
shared key definitions and documentation.
|
||||
</description>
|
||||
<example><![CDATA[
|
||||
<logger>
|
||||
level INFO
|
||||
<logfile>
|
||||
path STDOUT
|
||||
format %(levelname)s %(name)s %(message)s
|
||||
</logfile>
|
||||
</logger>
|
||||
]]>
|
||||
</example>
|
||||
|
||||
<key name="level"
|
||||
datatype="ZConfig.components.logger.datatypes.logging_level"
|
||||
default="info">
|
||||
<description>
|
||||
Verbosity setting for the logger. Values must be a name of
|
||||
a level, or an integer in the range [0..50]. The names of the
|
||||
levels, in order of increasing verbosity (names on the same
|
||||
line are equivalent)::
|
||||
|
||||
critical, fatal
|
||||
error
|
||||
warn, warning
|
||||
info
|
||||
blather
|
||||
debug
|
||||
trace
|
||||
all
|
||||
|
||||
The special name "notset", or the numeric value 0, indicates
|
||||
that the setting for the parent logger should be used.
|
||||
|
||||
It is strongly recommended that names be used rather than
|
||||
numeric values to ensure that configuration files can be
|
||||
deciphered more easily.
|
||||
</description>
|
||||
</key>
|
||||
|
||||
<multisection type="ZConfig.logger.handler"
|
||||
attribute="handlers" name="*">
|
||||
<description>
|
||||
Handlers to install on this logger. Each handler describes
|
||||
how logging events should be presented.
|
||||
</description>
|
||||
</multisection>
|
||||
</sectiontype>
|
||||
|
||||
</component>
|
||||
@@ -0,0 +1,10 @@
|
||||
<component prefix="ZConfig.components.logger.datatypes">
|
||||
<description>
|
||||
</description>
|
||||
|
||||
<import package="ZConfig.components.logger" file="abstract.xml"/>
|
||||
<import package="ZConfig.components.logger" file="handlers.xml"/>
|
||||
<import package="ZConfig.components.logger" file="logger.xml"/>
|
||||
<import package="ZConfig.components.logger" file="eventlog.xml"/>
|
||||
|
||||
</component>
|
||||
@@ -0,0 +1,39 @@
|
||||
##############################################################################
|
||||
#
|
||||
# Copyright (c) 2002 Zope Foundation and Contributors.
|
||||
#
|
||||
# This software is subject to the provisions of the Zope Public License,
|
||||
# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
|
||||
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
|
||||
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
|
||||
# FOR A PARTICULAR PURPOSE.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
"""ZConfig datatypes for logging support."""
|
||||
|
||||
|
||||
_logging_levels = {
|
||||
"critical": 50,
|
||||
"fatal": 50,
|
||||
"error": 40,
|
||||
"warn": 30,
|
||||
"warning": 30,
|
||||
"info": 20,
|
||||
"blather": 15,
|
||||
"debug": 10,
|
||||
"trace": 5,
|
||||
"all": 1,
|
||||
"notset": 0,
|
||||
}
|
||||
|
||||
def logging_level(value):
|
||||
s = str(value).lower()
|
||||
if s in _logging_levels:
|
||||
return _logging_levels[s]
|
||||
else:
|
||||
v = int(s)
|
||||
if v < 0 or v > 50:
|
||||
raise ValueError("log level not in range: " + repr(v))
|
||||
return v
|
||||
@@ -0,0 +1,15 @@
|
||||
<component prefix="ZConfig.components.logger.logger">
|
||||
|
||||
<import package="ZConfig.components.logger" file="abstract.xml"/>
|
||||
<import package="ZConfig.components.logger" file="base-logger.xml"/>
|
||||
|
||||
<sectiontype name="eventlog"
|
||||
datatype=".EventLogFactory"
|
||||
extends="ZConfig.logger.base-logger"
|
||||
implements="ZConfig.logger.log">
|
||||
<description>
|
||||
Configuration for the root logger.
|
||||
</description>
|
||||
</sectiontype>
|
||||
|
||||
</component>
|
||||
@@ -0,0 +1,41 @@
|
||||
##############################################################################
|
||||
#
|
||||
# Copyright (c) 2002 Zope Foundation and Contributors.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# This software is subject to the provisions of the Zope Public License,
|
||||
# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
|
||||
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
|
||||
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
|
||||
# FOR A PARTICULAR PURPOSE.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
_marker = object()
|
||||
|
||||
from abc import abstractmethod
|
||||
|
||||
from ZConfig._compat import AbstractBaseClass
|
||||
|
||||
class Factory(AbstractBaseClass):
|
||||
"""Generic wrapper for instance construction.
|
||||
|
||||
Calling the factory causes the instance to be created if it hasn't
|
||||
already been created, and returns the object. Calling the factory
|
||||
multiple times returns the same object.
|
||||
|
||||
The instance is created using the factory's create() method, which
|
||||
must be overriden by subclasses.
|
||||
"""
|
||||
def __init__(self):
|
||||
self.instance = _marker
|
||||
|
||||
def __call__(self):
|
||||
if self.instance is _marker:
|
||||
self.instance = self.create()
|
||||
return self.instance
|
||||
|
||||
@abstractmethod
|
||||
def create(self):
|
||||
"Subclasses must override create()"
|
||||
@@ -0,0 +1,227 @@
|
||||
##############################################################################
|
||||
#
|
||||
# Copyright (c) 2003 Zope Foundation and Contributors.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# This software is subject to the provisions of the Zope Public License,
|
||||
# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
|
||||
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
|
||||
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
|
||||
# FOR A PARTICULAR PURPOSE.
|
||||
#
|
||||
##############################################################################
|
||||
"""ZConfig factory datatypes for log handlers."""
|
||||
|
||||
from abc import abstractmethod
|
||||
import sys
|
||||
|
||||
from ZConfig._compat import urlparse
|
||||
|
||||
from ZConfig.components.logger.factory import Factory
|
||||
|
||||
_log_format_variables = {
|
||||
'name': '',
|
||||
'levelno': '3',
|
||||
'levelname': 'DEBUG',
|
||||
'pathname': 'apath',
|
||||
'filename': 'afile',
|
||||
'module': 'amodule',
|
||||
'lineno': 1,
|
||||
'created': 1.1,
|
||||
'asctime': 'atime',
|
||||
'msecs': 1,
|
||||
'relativeCreated': 1,
|
||||
'thread': 1,
|
||||
'message': 'amessage',
|
||||
'process': 1,
|
||||
}
|
||||
|
||||
def log_format(value):
|
||||
value = ctrl_char_insert(value)
|
||||
try:
|
||||
# Make sure the format string uses only names that will be
|
||||
# provided, and has reasonable type flags for each, and does
|
||||
# not expect positional args.
|
||||
value % _log_format_variables
|
||||
except (ValueError, KeyError):
|
||||
raise ValueError('Invalid log format string %s' % value)
|
||||
return value
|
||||
|
||||
_control_char_rewrites = {r'\n': '\n', r'\t': '\t', r'\b': '\b',
|
||||
r'\f': '\f', r'\r': '\r'}.items()
|
||||
|
||||
def ctrl_char_insert(value):
|
||||
for pattern, replacement in _control_char_rewrites:
|
||||
value = value.replace(pattern, replacement)
|
||||
return value
|
||||
|
||||
def resolve(name):
|
||||
"""Given a dotted name, returns an object imported from a Python module."""
|
||||
name = name.split('.')
|
||||
used = name.pop(0)
|
||||
found = __import__(used)
|
||||
for n in name:
|
||||
used += '.' + n
|
||||
try:
|
||||
found = getattr(found, n)
|
||||
except AttributeError:
|
||||
__import__(used)
|
||||
found = getattr(found, n)
|
||||
return found
|
||||
|
||||
class HandlerFactory(Factory):
|
||||
def __init__(self, section):
|
||||
Factory.__init__(self)
|
||||
self.section = section
|
||||
|
||||
@abstractmethod
|
||||
def create_loghandler(self):
|
||||
"subclasses must override create_loghandler()"
|
||||
|
||||
def create(self):
|
||||
import logging
|
||||
logger = self.create_loghandler()
|
||||
if self.section.formatter:
|
||||
f = resolve(self.section.formatter)
|
||||
else:
|
||||
f = logging.Formatter
|
||||
logger.setFormatter(f(self.section.format, self.section.dateformat))
|
||||
logger.setLevel(self.section.level)
|
||||
return logger
|
||||
|
||||
def getLevel(self): # pragma: no cover Is this used?
|
||||
return self.section.level
|
||||
|
||||
class FileHandlerFactory(HandlerFactory):
|
||||
def create_loghandler(self):
|
||||
from ZConfig.components.logger import loghandler
|
||||
path = self.section.path
|
||||
max_bytes = self.section.max_size
|
||||
old_files = self.section.old_files
|
||||
when = self.section.when
|
||||
interval = self.section.interval
|
||||
if path == "STDERR":
|
||||
if max_bytes or old_files:
|
||||
raise ValueError("cannot rotate STDERR")
|
||||
handler = loghandler.StreamHandler(sys.stderr)
|
||||
elif path == "STDOUT":
|
||||
if max_bytes or old_files:
|
||||
raise ValueError("cannot rotate STDOUT")
|
||||
handler = loghandler.StreamHandler(sys.stdout)
|
||||
elif when or max_bytes or old_files or interval:
|
||||
if not old_files:
|
||||
raise ValueError("old-files must be set for log rotation")
|
||||
if when:
|
||||
if max_bytes:
|
||||
raise ValueError("can't set *both* max_bytes and when")
|
||||
if not interval:
|
||||
interval = 1
|
||||
handler = loghandler.TimedRotatingFileHandler(
|
||||
path, when=when, interval=interval,
|
||||
backupCount=old_files)
|
||||
elif max_bytes:
|
||||
handler = loghandler.RotatingFileHandler(
|
||||
path, maxBytes=max_bytes, backupCount=old_files)
|
||||
else:
|
||||
raise ValueError(
|
||||
"max-bytes or when must be set for log rotation")
|
||||
else:
|
||||
handler = loghandler.FileHandler(path)
|
||||
return handler
|
||||
|
||||
_syslog_facilities = {
|
||||
"auth": 1,
|
||||
"authpriv": 1,
|
||||
"cron": 1,
|
||||
"daemon": 1,
|
||||
"kern": 1,
|
||||
"lpr": 1,
|
||||
"mail": 1,
|
||||
"news": 1,
|
||||
"security": 1,
|
||||
"syslog": 1,
|
||||
"user": 1,
|
||||
"uucp": 1,
|
||||
"local0": 1,
|
||||
"local1": 1,
|
||||
"local2": 1,
|
||||
"local3": 1,
|
||||
"local4": 1,
|
||||
"local5": 1,
|
||||
"local6": 1,
|
||||
"local7": 1,
|
||||
}
|
||||
|
||||
def syslog_facility(value):
|
||||
value = value.lower()
|
||||
if value not in _syslog_facilities:
|
||||
L = sorted(_syslog_facilities.keys())
|
||||
raise ValueError("Syslog facility must be one of " + ", ".join(L))
|
||||
return value
|
||||
|
||||
class SyslogHandlerFactory(HandlerFactory):
|
||||
def create_loghandler(self):
|
||||
from ZConfig.components.logger import loghandler
|
||||
return loghandler.SysLogHandler(self.section.address.address,
|
||||
self.section.facility)
|
||||
|
||||
class Win32EventLogFactory(HandlerFactory):
|
||||
def create_loghandler(self):
|
||||
from ZConfig.components.logger import loghandler
|
||||
return loghandler.Win32EventLogHandler(self.section.appname)
|
||||
|
||||
def http_handler_url(value):
|
||||
scheme, netloc, path, param, query, fragment = urlparse.urlparse(value)
|
||||
if scheme != 'http':
|
||||
raise ValueError('url must be an http url')
|
||||
if not netloc:
|
||||
raise ValueError('url must specify a location')
|
||||
if not path:
|
||||
raise ValueError('url must specify a path')
|
||||
q = []
|
||||
if param:
|
||||
q.append(';')
|
||||
q.append(param)
|
||||
if query:
|
||||
q.append('?')
|
||||
q.append(query)
|
||||
if fragment:
|
||||
q.append('#')
|
||||
q.append(fragment)
|
||||
return (netloc, path + ''.join(q))
|
||||
|
||||
def get_or_post(value):
|
||||
value = value.upper()
|
||||
if value not in ('GET', 'POST'):
|
||||
raise ValueError('method must be "GET" or "POST", instead received: '
|
||||
+ repr(value))
|
||||
return value
|
||||
|
||||
class HTTPHandlerFactory(HandlerFactory):
|
||||
def create_loghandler(self):
|
||||
from ZConfig.components.logger import loghandler
|
||||
host, selector = self.section.url
|
||||
return loghandler.HTTPHandler(host, selector, self.section.method)
|
||||
|
||||
class SMTPHandlerFactory(HandlerFactory):
|
||||
def create_loghandler(self):
|
||||
from ZConfig.components.logger import loghandler
|
||||
host, port = self.section.smtp_server
|
||||
if not port:
|
||||
mailhost = host
|
||||
else:
|
||||
mailhost = host, port
|
||||
kwargs = {}
|
||||
if self.section.smtp_username and self.section.smtp_password:
|
||||
kwargs['credentials'] = (self.section.smtp_username,
|
||||
self.section.smtp_password)
|
||||
elif (self.section.smtp_username or self.section.smtp_password):
|
||||
raise ValueError(
|
||||
'Either both smtp-username and smtp-password or none must be '
|
||||
'given')
|
||||
return loghandler.SMTPHandler(mailhost,
|
||||
self.section.fromaddr,
|
||||
self.section.toaddrs,
|
||||
self.section.subject,
|
||||
**kwargs)
|
||||
@@ -0,0 +1,107 @@
|
||||
<component prefix="ZConfig.components.logger.handlers">
|
||||
<description>
|
||||
</description>
|
||||
|
||||
<import package="ZConfig.components.logger" file="abstract.xml"/>
|
||||
|
||||
<sectiontype name="ZConfig.logger.base-log-handler">
|
||||
<description>
|
||||
Base type for most log handlers. This is cannot be used as a
|
||||
loghandler directly since it doesn't implement the loghandler
|
||||
abstract section type.
|
||||
</description>
|
||||
<key name="formatter" datatype="dotted-name" required="no">
|
||||
<description>
|
||||
Logging formatter class. The default is 'logging.Formatter'.
|
||||
An alternative is 'zope.exceptions.log.Formatter',
|
||||
which enhances exception tracebacks with information from
|
||||
__traceback_info__ and __traceback_supplement__ variables.
|
||||
</description>
|
||||
</key>
|
||||
<key name="dateformat"
|
||||
default="%Y-%m-%dT%H:%M:%S"/>
|
||||
<key name="level"
|
||||
default="notset"
|
||||
datatype="ZConfig.components.logger.datatypes.logging_level"/>
|
||||
</sectiontype>
|
||||
|
||||
<sectiontype name="logfile"
|
||||
datatype=".FileHandlerFactory"
|
||||
implements="ZConfig.logger.handler"
|
||||
extends="ZConfig.logger.base-log-handler">
|
||||
<example><![CDATA[
|
||||
<logfile>
|
||||
path STDOUT
|
||||
format %(name)s %(message)s
|
||||
</logfile>
|
||||
]]>
|
||||
</example>
|
||||
<key name="path" required="yes"/>
|
||||
<key name="old-files" required="no" default="0" datatype="integer"/>
|
||||
<key name="max-size" required="no" default="0" datatype="byte-size"/>
|
||||
<key name="when" required="no" default="" datatype="string"/>
|
||||
<key name="interval" required="no" default="0" datatype="integer"/>
|
||||
<key name="format"
|
||||
default="------\n%(asctime)s %(levelname)s %(name)s %(message)s"
|
||||
datatype=".log_format"/>
|
||||
</sectiontype>
|
||||
|
||||
<sectiontype name="syslog"
|
||||
datatype=".SyslogHandlerFactory"
|
||||
implements="ZConfig.logger.handler"
|
||||
extends="ZConfig.logger.base-log-handler">
|
||||
<key name="facility" default="user" datatype=".syslog_facility"/>
|
||||
<key name="address" datatype="socket-address" default="localhost:514"/>
|
||||
<key name="format"
|
||||
default="%(name)s %(message)s"
|
||||
datatype=".log_format"/>
|
||||
</sectiontype>
|
||||
|
||||
<sectiontype name="win32-eventlog"
|
||||
datatype=".Win32EventLogFactory"
|
||||
implements="ZConfig.logger.handler"
|
||||
extends="ZConfig.logger.base-log-handler">
|
||||
<key name="appname" default="Zope"/>
|
||||
<key name="format"
|
||||
default="%(levelname)s %(name)s %(message)s"
|
||||
datatype=".log_format"/>
|
||||
</sectiontype>
|
||||
|
||||
<sectiontype name="http-logger"
|
||||
datatype=".HTTPHandlerFactory"
|
||||
implements="ZConfig.logger.handler"
|
||||
extends="ZConfig.logger.base-log-handler">
|
||||
<key name="url" default="http://localhost/" datatype=".http_handler_url"/>
|
||||
<key name="method" default="GET" datatype=".get_or_post"/>
|
||||
<key name="format"
|
||||
default="%(asctime)s %(levelname)s %(name)s %(message)s"
|
||||
datatype=".log_format"/>
|
||||
</sectiontype>
|
||||
|
||||
<sectiontype name="email-notifier"
|
||||
datatype=".SMTPHandlerFactory"
|
||||
implements="ZConfig.logger.handler"
|
||||
extends="ZConfig.logger.base-log-handler">
|
||||
<example><![CDATA[
|
||||
<email-notifier>
|
||||
to sysadmin@example.com
|
||||
to john@example.com
|
||||
from zlog-user@example.com
|
||||
level fatal
|
||||
smtp-username john
|
||||
smtp-password johnpw
|
||||
</email-notifier>
|
||||
]]>
|
||||
</example>
|
||||
<key name="from" required="yes" attribute="fromaddr"/>
|
||||
<multikey name="to" required="yes" attribute="toaddrs"/>
|
||||
<key name="subject" default="Message from Zope"/>
|
||||
<key name="smtp-server" default="localhost" datatype="inet-address"/>
|
||||
<key name="smtp-username" default="" datatype="string"/>
|
||||
<key name="smtp-password" default="" datatype="string"/>
|
||||
<key name="format"
|
||||
default="%(asctime)s %(levelname)s %(name)s %(message)s"
|
||||
datatype=".log_format"/>
|
||||
</sectiontype>
|
||||
|
||||
</component>
|
||||
@@ -0,0 +1,102 @@
|
||||
##############################################################################
|
||||
#
|
||||
# Copyright (c) 2003 Zope Foundation and Contributors.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# This software is subject to the provisions of the Zope Public License,
|
||||
# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
|
||||
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
|
||||
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
|
||||
# FOR A PARTICULAR PURPOSE.
|
||||
#
|
||||
##############################################################################
|
||||
"""ZConfig factory datatypes for loggers."""
|
||||
|
||||
from ZConfig.components.logger.factory import Factory
|
||||
|
||||
|
||||
class LoggerFactoryBase(Factory):
|
||||
"""Base class for logger factories.
|
||||
|
||||
Factory used to create loggers while delaying actual logger
|
||||
instance construction. We need to do this because we may want to
|
||||
reference a logger before actually instantiating it (for example,
|
||||
to allow the app time to set an effective user). An instance of
|
||||
this wrapper is a callable which, when called, returns a logger
|
||||
object.
|
||||
"""
|
||||
|
||||
def __init__(self, section):
|
||||
Factory.__init__(self)
|
||||
self.level = section.level
|
||||
self.handler_factories = section.handlers
|
||||
|
||||
def create(self):
|
||||
# set the logger up
|
||||
import logging
|
||||
logger = logging.getLogger(self.name)
|
||||
logger.setLevel(self.level)
|
||||
if self.handler_factories:
|
||||
for handler_factory in self.handler_factories:
|
||||
handler = handler_factory()
|
||||
logger.addHandler(handler)
|
||||
else:
|
||||
from ZConfig.components.logger import loghandler
|
||||
logger.addHandler(loghandler.NullHandler())
|
||||
return logger
|
||||
|
||||
def startup(self):
|
||||
# make sure we've instantiated the logger
|
||||
self()
|
||||
|
||||
def getLowestHandlerLevel(self):
|
||||
"""Return the lowest log level provided by any configured handler.
|
||||
|
||||
If all handlers and the logger itself have level==NOTSET, this
|
||||
returns NOTSET.
|
||||
"""
|
||||
import logging
|
||||
lowest = self.level
|
||||
for factory in self.handler_factories:
|
||||
level = factory.getLevel()
|
||||
if level != logging.NOTSET:
|
||||
if lowest == logging.NOTSET:
|
||||
lowest = level
|
||||
else:
|
||||
lowest = min(lowest, level)
|
||||
return lowest
|
||||
|
||||
def reopen(self):
|
||||
"""Re-open any handlers for which this is a meaningful operation.
|
||||
|
||||
This only works on handlers on the logger provided by this
|
||||
factory directly; handlers for child loggers are not affected.
|
||||
(This can be considered a bug, but is sufficient at the
|
||||
moment.)
|
||||
"""
|
||||
logger = self()
|
||||
for handler in logger.handlers:
|
||||
reopen = getattr(handler, "reopen", None)
|
||||
if reopen is not None and callable(reopen):
|
||||
reopen()
|
||||
|
||||
|
||||
class EventLogFactory(LoggerFactoryBase):
|
||||
"""Logger factory that returns the root logger."""
|
||||
|
||||
name = None
|
||||
|
||||
|
||||
class LoggerFactory(LoggerFactoryBase):
|
||||
"""Logger factory that returns the named logger."""
|
||||
|
||||
def __init__(self, section):
|
||||
LoggerFactoryBase.__init__(self, section)
|
||||
self.name = section.name
|
||||
self.propagate = section.propagate
|
||||
|
||||
def create(self):
|
||||
logger = LoggerFactoryBase.create(self)
|
||||
logger.propagate = self.propagate
|
||||
return logger
|
||||
@@ -0,0 +1,39 @@
|
||||
<component prefix="ZConfig.components.logger.logger">
|
||||
<description>
|
||||
</description>
|
||||
|
||||
<import package="ZConfig.components.logger" file="abstract.xml"/>
|
||||
<import package="ZConfig.components.logger" file="base-logger.xml"/>
|
||||
<import package="ZConfig.components.logger" file="eventlog.xml"/>
|
||||
|
||||
<sectiontype name="logger"
|
||||
datatype=".LoggerFactory"
|
||||
extends="ZConfig.logger.base-logger"
|
||||
implements="ZConfig.logger.log">
|
||||
<key name="propagate"
|
||||
datatype="boolean"
|
||||
default="true">
|
||||
<description>
|
||||
Indicates whether events that reach this logger should be
|
||||
propogated toward the root of the logger hierarchy. If true
|
||||
(the default), events will be passed to the logger's parent
|
||||
after being handled. If false, events will be handled and the
|
||||
parent will not be informed. There is not a way to control
|
||||
propogation by the severity of the event.
|
||||
</description>
|
||||
</key>
|
||||
|
||||
<key name="name"
|
||||
datatype="dotted-name"
|
||||
>
|
||||
<description>
|
||||
The dotted name of the logger. This give it a location in the
|
||||
logging hierarchy. Most applications provide a specific set
|
||||
of subsystem names for which logging is meaning; consult the
|
||||
application documentation for the set of names that are
|
||||
actually interesting for the application.
|
||||
</description>
|
||||
</key>
|
||||
</sectiontype>
|
||||
|
||||
</component>
|
||||
@@ -0,0 +1,179 @@
|
||||
##############################################################################
|
||||
#
|
||||
# Copyright (c) 2001 Zope Foundation and Contributors.
|
||||
#
|
||||
# This software is subject to the provisions of the Zope Public License,
|
||||
# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
|
||||
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
|
||||
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
|
||||
# FOR A PARTICULAR PURPOSE.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
"""Handlers which can plug into a PEP 282 logger."""
|
||||
|
||||
import os
|
||||
|
||||
import weakref
|
||||
|
||||
from logging import Handler, StreamHandler
|
||||
from logging.handlers import RotatingFileHandler as _RotatingFileHandler
|
||||
from logging.handlers import TimedRotatingFileHandler \
|
||||
as _TimedRotatingFileHandler
|
||||
from logging.handlers import SysLogHandler, BufferingHandler
|
||||
from logging.handlers import HTTPHandler, SMTPHandler
|
||||
from logging.handlers import NTEventLogHandler as Win32EventLogHandler
|
||||
|
||||
# Export these, they're used in handlers.py
|
||||
SysLogHandler = SysLogHandler
|
||||
HTTPHandler = HTTPHandler
|
||||
SMTPHandler = SMTPHandler
|
||||
Win32EventLogHandler = Win32EventLogHandler
|
||||
|
||||
from ZConfig._compat import maxsize
|
||||
|
||||
|
||||
_reopenable_handlers = []
|
||||
|
||||
def closeFiles():
|
||||
"""Reopen all logfiles managed by ZConfig configuration."""
|
||||
while _reopenable_handlers:
|
||||
wr = _reopenable_handlers.pop()
|
||||
h = wr()
|
||||
if h is not None:
|
||||
h.close()
|
||||
|
||||
def reopenFiles():
|
||||
"""Reopen all logfiles managed by ZConfig configuration."""
|
||||
for wr in _reopenable_handlers[:]:
|
||||
h = wr()
|
||||
if h is None:
|
||||
try:
|
||||
_reopenable_handlers.remove(wr)
|
||||
except ValueError:
|
||||
continue
|
||||
else:
|
||||
h.reopen()
|
||||
|
||||
def _remove_from_reopenable(wr):
|
||||
try:
|
||||
_reopenable_handlers.remove(wr)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
|
||||
class FileHandler(StreamHandler):
|
||||
"""File handler which supports reopening of logs.
|
||||
|
||||
Re-opening should be used instead of the 'rollover' feature of
|
||||
the FileHandler from the standard library's logging package.
|
||||
"""
|
||||
|
||||
def __init__(self, filename, mode="a"):
|
||||
filename = os.path.abspath(filename)
|
||||
StreamHandler.__init__(self, open(filename, mode))
|
||||
self.baseFilename = filename
|
||||
self.mode = mode
|
||||
self._wr = weakref.ref(self, _remove_from_reopenable)
|
||||
_reopenable_handlers.append(self._wr)
|
||||
|
||||
def close(self):
|
||||
self.stream.close()
|
||||
# This can raise a KeyError if the handler has already been
|
||||
# removed, but a later error can be raised if
|
||||
# StreamHandler.close() isn't called. This seems the best
|
||||
# compromise. :-(
|
||||
try:
|
||||
StreamHandler.close(self)
|
||||
except KeyError: # pragma: no cover
|
||||
pass
|
||||
_remove_from_reopenable(self._wr)
|
||||
|
||||
def reopen(self):
|
||||
self.acquire()
|
||||
try:
|
||||
self.stream.close()
|
||||
self.stream = open(self.baseFilename, self.mode)
|
||||
finally:
|
||||
self.release()
|
||||
|
||||
|
||||
class Win32FileHandler(FileHandler):
|
||||
"""File-based log handler for Windows that supports an additional 'rotate'
|
||||
method. reopen() is generally useless since Windows cannot do a move on
|
||||
an open file.
|
||||
"""
|
||||
def rotate(self, rotateFilename=None):
|
||||
if not rotateFilename:
|
||||
rotateFilename = self.baseFilename + ".last"
|
||||
error = None
|
||||
self.close()
|
||||
try:
|
||||
os.rename(self.baseFilename, rotateFilename)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
self.stream = open(self.baseFilename, self.mode)
|
||||
|
||||
if os.name == "nt":
|
||||
# Make it the default for Windows - we install a 'reopen' handler that
|
||||
# tries to rotate the logfile.
|
||||
FileHandler = Win32FileHandler
|
||||
|
||||
|
||||
class RotatingFileHandler(_RotatingFileHandler):
|
||||
|
||||
def __init__(self, *args, **kw):
|
||||
_RotatingFileHandler.__init__(self, *args, **kw)
|
||||
self._wr = weakref.ref(self, _remove_from_reopenable)
|
||||
_reopenable_handlers.append(self._wr)
|
||||
|
||||
def close(self):
|
||||
_RotatingFileHandler.close(self)
|
||||
_remove_from_reopenable(self._wr)
|
||||
|
||||
def reopen(self):
|
||||
self.doRollover()
|
||||
|
||||
class TimedRotatingFileHandler(_TimedRotatingFileHandler):
|
||||
|
||||
def __init__(self, *args, **kw):
|
||||
_TimedRotatingFileHandler.__init__(self, *args, **kw)
|
||||
self._wr = weakref.ref(self, _remove_from_reopenable)
|
||||
_reopenable_handlers.append(self._wr)
|
||||
|
||||
def close(self):
|
||||
_TimedRotatingFileHandler.close(self)
|
||||
_remove_from_reopenable(self._wr)
|
||||
|
||||
def reopen(self):
|
||||
self.doRollover()
|
||||
|
||||
|
||||
class NullHandler(Handler):
|
||||
"""Handler that does nothing."""
|
||||
|
||||
def emit(self, record):
|
||||
pass
|
||||
|
||||
def handle(self, record):
|
||||
pass
|
||||
|
||||
|
||||
class StartupHandler(BufferingHandler):
|
||||
"""Handler which stores messages in a buffer until later.
|
||||
|
||||
This is useful at startup before we can know that we can safely
|
||||
write to a configuration-specified handler.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
BufferingHandler.__init__(self, maxsize)
|
||||
|
||||
def shouldFlush(self, record):
|
||||
return False
|
||||
|
||||
def flushBufferTo(self, target):
|
||||
while self.buffer:
|
||||
target.handle(self.buffer.pop(0))
|
||||
@@ -0,0 +1 @@
|
||||
# This is a Python package.
|
||||
@@ -0,0 +1,863 @@
|
||||
##############################################################################
|
||||
#
|
||||
# Copyright (c) 2002 Zope Foundation and Contributors.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# This software is subject to the provisions of the Zope Public License,
|
||||
# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
|
||||
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
|
||||
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
|
||||
# FOR A PARTICULAR PURPOSE.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
"""Tests for logging configuration via ZConfig."""
|
||||
|
||||
import doctest
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
import ZConfig
|
||||
|
||||
from ZConfig.components.logger import datatypes
|
||||
from ZConfig.components.logger import handlers
|
||||
from ZConfig.components.logger import loghandler
|
||||
|
||||
from ZConfig._compat import NStringIO as StringIO
|
||||
from ZConfig._compat import maxsize
|
||||
|
||||
from ZConfig.tests.support import TestHelper
|
||||
|
||||
|
||||
class CustomFormatter(logging.Formatter):
|
||||
def formatException(self, ei):
|
||||
"""Format and return the exception information as a string.
|
||||
|
||||
This adds helpful advice to the end of the traceback.
|
||||
"""
|
||||
import traceback
|
||||
sio = StringIO()
|
||||
traceback.print_exception(ei[0], ei[1], ei[2], file=sio)
|
||||
return sio.getvalue() + "... Don't panic!"
|
||||
|
||||
|
||||
def read_file(filename):
|
||||
with open(filename) as f:
|
||||
return f.read()
|
||||
|
||||
|
||||
class LoggingTestHelper(TestHelper):
|
||||
|
||||
# Not derived from unittest.TestCase; some test runners seem to
|
||||
# think that means this class contains tests.
|
||||
|
||||
# XXX This tries to save and restore the state of logging around
|
||||
# the test. Somewhat surgical; there may be a better way.
|
||||
|
||||
def setUp(self):
|
||||
self._created = []
|
||||
self._old_logger = logging.getLogger()
|
||||
self._old_level = self._old_logger.level
|
||||
self._old_handlers = self._old_logger.handlers[:]
|
||||
self._old_logger.handlers[:] = []
|
||||
self._old_logger.setLevel(logging.WARN)
|
||||
|
||||
self._old_logger_dict = logging.root.manager.loggerDict.copy()
|
||||
logging.root.manager.loggerDict.clear()
|
||||
|
||||
def tearDown(self):
|
||||
logging.root.manager.loggerDict.clear()
|
||||
logging.root.manager.loggerDict.update(self._old_logger_dict)
|
||||
|
||||
for h in self._old_logger.handlers:
|
||||
self._old_logger.removeHandler(h)
|
||||
for h in self._old_handlers:
|
||||
self._old_logger.addHandler(h) # pragma: no cover
|
||||
self._old_logger.setLevel(self._old_level)
|
||||
|
||||
while self._created:
|
||||
os.unlink(self._created.pop())
|
||||
|
||||
self.assertEqual(loghandler._reopenable_handlers, [])
|
||||
loghandler.closeFiles()
|
||||
loghandler._reopenable_handlers == []
|
||||
|
||||
def mktemp(self):
|
||||
fd, fn = tempfile.mkstemp()
|
||||
os.close(fd)
|
||||
self._created.append(fn)
|
||||
return fn
|
||||
|
||||
def move(self, fn):
|
||||
nfn = self.mktemp()
|
||||
os.rename(fn, nfn)
|
||||
return nfn
|
||||
|
||||
_schema = None
|
||||
|
||||
def get_schema(self):
|
||||
if self._schema is None:
|
||||
sio = StringIO(self._schematext)
|
||||
self.__class__._schema = ZConfig.loadSchemaFile(sio)
|
||||
return self._schema
|
||||
|
||||
def get_config(self, text):
|
||||
conf, handler = ZConfig.loadConfigFile(self.get_schema(),
|
||||
StringIO(text))
|
||||
self.assertTrue(not handler)
|
||||
return conf
|
||||
|
||||
|
||||
class TestConfig(LoggingTestHelper, unittest.TestCase):
|
||||
|
||||
_schematext = """
|
||||
<schema>
|
||||
<import package='ZConfig.components.logger'/>
|
||||
<section type='eventlog' name='*' attribute='eventlog'/>
|
||||
</schema>
|
||||
"""
|
||||
|
||||
def test_config_without_logger(self):
|
||||
conf = self.get_config("")
|
||||
self.assertTrue(conf.eventlog is None)
|
||||
|
||||
def test_config_without_handlers(self):
|
||||
logger = self.check_simple_logger("<eventlog/>")
|
||||
# Make sure there's a NullHandler, since a warning gets
|
||||
# printed if there are no handlers:
|
||||
self.assertEqual(len(logger.handlers), 1)
|
||||
self.assertTrue(isinstance(logger.handlers[0], loghandler.NullHandler))
|
||||
|
||||
# And it does nothing
|
||||
logger.handlers[0].emit(None)
|
||||
logger.handlers[0].handle(None)
|
||||
|
||||
def test_factory_without_stream(self):
|
||||
factory = self.check_simple_logger_factory("<eventlog>\n"
|
||||
" <logfile>\n"
|
||||
" path STDERR\n"
|
||||
" </logfile>\n"
|
||||
" <logfile>\n"
|
||||
" path STDERR\n"
|
||||
" level info\n"
|
||||
" </logfile>\n"
|
||||
" <logfile>\n"
|
||||
" path STDERR\n"
|
||||
" level debug\n"
|
||||
" </logfile>\n"
|
||||
"</eventlog>")
|
||||
|
||||
factory.startup()
|
||||
logger = factory.instance
|
||||
|
||||
factory.level = logging.NOTSET
|
||||
self.assertEqual(factory.getLowestHandlerLevel(), logging.DEBUG)
|
||||
logger.handlers[0].reopen = lambda: None
|
||||
factory.reopen()
|
||||
|
||||
def test_with_logfile(self):
|
||||
fn = self.mktemp()
|
||||
logger = self.check_simple_logger("<eventlog>\n"
|
||||
" <logfile>\n"
|
||||
" path %s\n"
|
||||
" level debug\n"
|
||||
" </logfile>\n"
|
||||
"</eventlog>" % fn)
|
||||
logfile = logger.handlers[0]
|
||||
self.assertEqual(logfile.level, logging.DEBUG)
|
||||
self.assertTrue(isinstance(logfile, loghandler.FileHandler))
|
||||
logger.removeHandler(logfile)
|
||||
logfile.close()
|
||||
|
||||
def test_with_stderr(self):
|
||||
self.check_standard_stream("stderr")
|
||||
|
||||
def test_with_stdout(self):
|
||||
self.check_standard_stream("stdout")
|
||||
|
||||
def test_with_rotating_logfile(self):
|
||||
fn = self.mktemp()
|
||||
logger = self.check_simple_logger("<eventlog>\n"
|
||||
" <logfile>\n"
|
||||
" path %s\n"
|
||||
" level debug\n"
|
||||
" max-size 5mb\n"
|
||||
" old-files 10\n"
|
||||
" </logfile>\n"
|
||||
"</eventlog>" % fn)
|
||||
logfile = logger.handlers[0]
|
||||
self.assertEqual(logfile.level, logging.DEBUG)
|
||||
self.assertEqual(logfile.backupCount, 10)
|
||||
self.assertEqual(logfile.maxBytes, 5*1024*1024)
|
||||
self.assertTrue(isinstance(logfile, loghandler.RotatingFileHandler))
|
||||
logger.removeHandler(logfile)
|
||||
logfile.close()
|
||||
|
||||
def test_with_timed_rotating_logfile(self):
|
||||
fn = self.mktemp()
|
||||
logger = self.check_simple_logger("<eventlog>\n"
|
||||
" <logfile>\n"
|
||||
" path %s\n"
|
||||
" level debug\n"
|
||||
" when D\n"
|
||||
" interval 3\n"
|
||||
" old-files 11\n"
|
||||
" </logfile>\n"
|
||||
"</eventlog>" % fn)
|
||||
logfile = logger.handlers[0]
|
||||
self.assertEqual(logfile.level, logging.DEBUG)
|
||||
self.assertEqual(logfile.backupCount, 11)
|
||||
self.assertEqual(logfile.interval, 86400*3)
|
||||
self.assertTrue(isinstance(logfile, loghandler.TimedRotatingFileHandler))
|
||||
logger.removeHandler(logfile)
|
||||
logfile.close()
|
||||
|
||||
def test_with_timed_rotating_logfile_and_size_should_fail(self):
|
||||
fn = self.mktemp()
|
||||
self.assertRaises(
|
||||
ValueError,
|
||||
self.check_simple_logger,
|
||||
"<eventlog>\n"
|
||||
" <logfile>\n"
|
||||
" path %s\n"
|
||||
" level debug\n"
|
||||
" max-size 5mb\n"
|
||||
" when D\n"
|
||||
" old-files 10\n"
|
||||
" </logfile>\n"
|
||||
"</eventlog>" % fn)
|
||||
|
||||
# Mising old-files
|
||||
self.assertRaisesRegex(
|
||||
ValueError,
|
||||
"old-files must be set",
|
||||
self.check_simple_logger,
|
||||
"<eventlog>\n"
|
||||
" <logfile>\n"
|
||||
" path %s\n"
|
||||
" level debug\n"
|
||||
" max-size 5mb\n"
|
||||
" when D\n"
|
||||
" </logfile>\n"
|
||||
"</eventlog>" % fn)
|
||||
|
||||
self.assertRaisesRegex(
|
||||
ValueError,
|
||||
"max-bytes or when must be set",
|
||||
self.check_simple_logger,
|
||||
"<eventlog>\n"
|
||||
" <logfile>\n"
|
||||
" path %s\n"
|
||||
" level debug\n"
|
||||
" interval 1\n"
|
||||
" old-files 10\n"
|
||||
" </logfile>\n"
|
||||
"</eventlog>" % fn)
|
||||
|
||||
|
||||
def test_with_rotating_logfile_and_STD_should_fail(self):
|
||||
for path in ('STDERR', 'STDOUT'):
|
||||
for param in ('old-files 10', 'max-size 5mb'):
|
||||
self.assertRaises(
|
||||
ValueError,
|
||||
self.check_simple_logger,
|
||||
"<eventlog>\n"
|
||||
" <logfile>\n"
|
||||
" path %s\n"
|
||||
" level debug\n"
|
||||
" when D\n"
|
||||
" %s\n"
|
||||
" </logfile>\n"
|
||||
"</eventlog>" % (path, param))
|
||||
|
||||
|
||||
def check_standard_stream(self, name):
|
||||
old_stream = getattr(sys, name)
|
||||
conf = self.get_config("""
|
||||
<eventlog>
|
||||
<logfile>
|
||||
level info
|
||||
path %s
|
||||
</logfile>
|
||||
</eventlog>
|
||||
""" % name.upper())
|
||||
self.assertTrue(conf.eventlog is not None)
|
||||
# The factory has already been created; make sure it picks up
|
||||
# the stderr we set here when we create the logger and
|
||||
# handlers:
|
||||
sio = StringIO()
|
||||
setattr(sys, name, sio)
|
||||
try:
|
||||
logger = conf.eventlog()
|
||||
finally:
|
||||
setattr(sys, name, old_stream)
|
||||
logger.warning("woohoo!")
|
||||
self.assertTrue(sio.getvalue().find("woohoo!") >= 0)
|
||||
|
||||
def test_custom_formatter(self):
|
||||
old_stream = sys.stdout
|
||||
conf = self.get_config("""
|
||||
<eventlog>
|
||||
<logfile>
|
||||
formatter ZConfig.components.logger.tests.test_logger.CustomFormatter
|
||||
level info
|
||||
path STDOUT
|
||||
</logfile>
|
||||
</eventlog>
|
||||
""")
|
||||
sio = StringIO()
|
||||
sys.stdout = sio
|
||||
try:
|
||||
logger = conf.eventlog()
|
||||
finally:
|
||||
sys.stdout = old_stream
|
||||
try:
|
||||
raise KeyError
|
||||
except KeyError:
|
||||
logger.exception("testing a KeyError")
|
||||
self.assertTrue(sio.getvalue().find("KeyError") >= 0)
|
||||
self.assertTrue(sio.getvalue().find("Don't panic") >= 0)
|
||||
|
||||
def test_with_syslog(self):
|
||||
import socket
|
||||
logger = self.check_simple_logger("<eventlog>\n"
|
||||
" <syslog>\n"
|
||||
" level error\n"
|
||||
" facility local3\n"
|
||||
" </syslog>\n"
|
||||
"</eventlog>")
|
||||
syslog = logger.handlers[0]
|
||||
self.assertEqual(syslog.level, logging.ERROR)
|
||||
self.assertTrue(isinstance(syslog, loghandler.SysLogHandler))
|
||||
syslog.close() # avoid ResourceWarning
|
||||
try:
|
||||
syslog.socket.close() # ResourceWarning under 3.2
|
||||
except socket.SocketError: # pragma: no cover
|
||||
pass
|
||||
|
||||
def test_with_http_logger_localhost(self):
|
||||
logger = self.check_simple_logger("<eventlog>\n"
|
||||
" <http-logger>\n"
|
||||
" level error\n"
|
||||
" method post\n"
|
||||
" </http-logger>\n"
|
||||
"</eventlog>")
|
||||
handler = logger.handlers[0]
|
||||
self.assertEqual(handler.host, "localhost")
|
||||
# XXX The "url" attribute of the handler is misnamed; it
|
||||
# really means just the selector portion of the URL.
|
||||
self.assertEqual(handler.url, "/")
|
||||
self.assertEqual(handler.level, logging.ERROR)
|
||||
self.assertEqual(handler.method, "POST")
|
||||
self.assertTrue(isinstance(handler, loghandler.HTTPHandler))
|
||||
|
||||
def test_with_http_logger_remote_host(self):
|
||||
logger = self.check_simple_logger("<eventlog>\n"
|
||||
" <http-logger>\n"
|
||||
" method get\n"
|
||||
" url http://example.com/log/\n"
|
||||
" </http-logger>\n"
|
||||
"</eventlog>")
|
||||
handler = logger.handlers[0]
|
||||
self.assertEqual(handler.host, "example.com")
|
||||
# XXX The "url" attribute of the handler is misnamed; it
|
||||
# really means just the selector portion of the URL.
|
||||
self.assertEqual(handler.url, "/log/")
|
||||
self.assertEqual(handler.level, logging.NOTSET)
|
||||
self.assertEqual(handler.method, "GET")
|
||||
self.assertTrue(isinstance(handler, loghandler.HTTPHandler))
|
||||
|
||||
def test_with_email_notifier(self):
|
||||
logger = self.check_simple_logger("<eventlog>\n"
|
||||
" <email-notifier>\n"
|
||||
" to sysadmin@example.com\n"
|
||||
" to sa-pager@example.com\n"
|
||||
" from zlog-user@example.com\n"
|
||||
" level fatal\n"
|
||||
" </email-notifier>\n"
|
||||
"</eventlog>")
|
||||
handler = logger.handlers[0]
|
||||
self.assertEqual(handler.toaddrs, ["sysadmin@example.com",
|
||||
"sa-pager@example.com"])
|
||||
self.assertEqual(handler.fromaddr, "zlog-user@example.com")
|
||||
self.assertEqual(handler.level, logging.FATAL)
|
||||
|
||||
def test_with_email_notifier_with_credentials(self):
|
||||
logger = self.check_simple_logger("<eventlog>\n"
|
||||
" <email-notifier>\n"
|
||||
" to sysadmin@example.com\n"
|
||||
" from zlog-user@example.com\n"
|
||||
" level fatal\n"
|
||||
" smtp-server foo:487\n"
|
||||
" smtp-username john\n"
|
||||
" smtp-password johnpw\n"
|
||||
" </email-notifier>\n"
|
||||
"</eventlog>")
|
||||
|
||||
self.assertTrue(sys.version_info >= (2, 6))
|
||||
handler = logger.handlers[0]
|
||||
self.assertEqual(handler.toaddrs, ["sysadmin@example.com"])
|
||||
self.assertEqual(handler.fromaddr, "zlog-user@example.com")
|
||||
self.assertEqual(handler.fromaddr, "zlog-user@example.com")
|
||||
self.assertEqual(handler.level, logging.FATAL)
|
||||
self.assertEqual(handler.username, 'john')
|
||||
self.assertEqual(handler.password, 'johnpw')
|
||||
self.assertEqual(handler.mailhost, 'foo')
|
||||
self.assertEqual(handler.mailport, 487)
|
||||
|
||||
def test_with_email_notifier_with_invalid_credentials(self):
|
||||
self.assertRaises(ValueError,
|
||||
self.check_simple_logger,
|
||||
"<eventlog>\n"
|
||||
" <email-notifier>\n"
|
||||
" to sysadmin@example.com\n"
|
||||
" from zlog-user@example.com\n"
|
||||
" level fatal\n"
|
||||
" smtp-username john\n"
|
||||
" </email-notifier>\n"
|
||||
"</eventlog>")
|
||||
self.assertRaises(ValueError,
|
||||
self.check_simple_logger,
|
||||
"<eventlog>\n"
|
||||
" <email-notifier>\n"
|
||||
" to sysadmin@example.com\n"
|
||||
" from zlog-user@example.com\n"
|
||||
" level fatal\n"
|
||||
" smtp-password john\n"
|
||||
" </email-notifier>\n"
|
||||
"</eventlog>")
|
||||
|
||||
def check_simple_logger_factory(self, text, level=logging.INFO):
|
||||
conf = self.get_config(text)
|
||||
self.assertTrue(conf.eventlog is not None)
|
||||
self.assertEqual(conf.eventlog.level, level)
|
||||
return conf.eventlog
|
||||
|
||||
def check_simple_logger(self, text, level=logging.INFO):
|
||||
logger = self.check_simple_logger_factory(text, level)()
|
||||
self.assertTrue(isinstance(logger, logging.Logger))
|
||||
self.assertEqual(len(logger.handlers), 1)
|
||||
return logger
|
||||
|
||||
|
||||
if os.name == 'nt':
|
||||
# Though log files can be closed and re-opened on Windows, these
|
||||
# tests expect to be able to move the underlying files out from
|
||||
# underneath the logger while open. That's not possible on
|
||||
# Windows. So we don't extend TestCase so that they don't get run.
|
||||
#
|
||||
# Different tests are needed that only test that close/re-open
|
||||
# operations are performed by the handler; those can be run on
|
||||
# any platform.
|
||||
_RotateTestBase = object
|
||||
else:
|
||||
_RotateTestBase = unittest.TestCase
|
||||
|
||||
|
||||
class TestReopeningRotatingLogfiles(LoggingTestHelper, _RotateTestBase):
|
||||
|
||||
# These tests should not be run on Windows.
|
||||
|
||||
handler_factory = loghandler.RotatingFileHandler
|
||||
|
||||
_schematext = """
|
||||
<schema>
|
||||
<import package='ZConfig.components.logger'/>
|
||||
<multisection type='logger' name='*' attribute='loggers'/>
|
||||
</schema>
|
||||
"""
|
||||
|
||||
_sampleconfig_template = """
|
||||
<logger>
|
||||
name foo.bar
|
||||
<logfile>
|
||||
path %(path0)s
|
||||
level debug
|
||||
max-size 1mb
|
||||
old-files 10
|
||||
</logfile>
|
||||
<logfile>
|
||||
path %(path1)s
|
||||
level info
|
||||
max-size 1mb
|
||||
old-files 3
|
||||
</logfile>
|
||||
<logfile>
|
||||
path %(path1)s
|
||||
level info
|
||||
when D
|
||||
old-files 3
|
||||
</logfile>
|
||||
</logger>
|
||||
|
||||
<logger>
|
||||
name bar.foo
|
||||
<logfile>
|
||||
path %(path2)s
|
||||
level info
|
||||
max-size 10mb
|
||||
old-files 10
|
||||
</logfile>
|
||||
</logger>
|
||||
"""
|
||||
|
||||
def test_filehandler_reopen(self):
|
||||
|
||||
def mkrecord(msg):
|
||||
args = ["foo.bar", logging.ERROR, __file__, 42, msg, (), ()]
|
||||
return logging.LogRecord(*args)
|
||||
|
||||
# This goes through the reopening operation *twice* to make
|
||||
# sure that we don't lose our handle on the handler the first
|
||||
# time around.
|
||||
|
||||
fn = self.mktemp()
|
||||
h = self.handler_factory(fn)
|
||||
h.handle(mkrecord("message 1"))
|
||||
nfn1 = self.move(fn)
|
||||
h.handle(mkrecord("message 2"))
|
||||
h.reopen()
|
||||
h.handle(mkrecord("message 3"))
|
||||
nfn2 = self.move(fn)
|
||||
h.handle(mkrecord("message 4"))
|
||||
h.reopen()
|
||||
h.handle(mkrecord("message 5"))
|
||||
h.close()
|
||||
|
||||
# Check that the messages are in the right files::
|
||||
text1 = read_file(nfn1)
|
||||
text2 = read_file(nfn2)
|
||||
text3 = read_file(fn)
|
||||
self.assertTrue("message 1" in text1)
|
||||
self.assertTrue("message 2" in text1)
|
||||
self.assertTrue("message 3" in text2)
|
||||
self.assertTrue("message 4" in text2)
|
||||
self.assertTrue("message 5" in text3)
|
||||
|
||||
def test_logfile_reopening(self):
|
||||
#
|
||||
# This test only applies to the simple logfile reopening; it
|
||||
# doesn't work the same way as the rotating logfile handler.
|
||||
#
|
||||
paths = self.mktemp(), self.mktemp(), self.mktemp()
|
||||
d = {
|
||||
"path0": paths[0],
|
||||
"path1": paths[1],
|
||||
"path2": paths[2],
|
||||
}
|
||||
text = self._sampleconfig_template % d
|
||||
conf = self.get_config(text)
|
||||
self.assertEqual(len(conf.loggers), 2)
|
||||
# Build the loggers from the configuration, and write to them:
|
||||
conf.loggers[0]().info("message 1")
|
||||
conf.loggers[1]().info("message 2")
|
||||
#
|
||||
# We expect this to re-open the original filenames, so we'll
|
||||
# have six files instead of three.
|
||||
#
|
||||
loghandler.reopenFiles()
|
||||
#
|
||||
# Write to them again:
|
||||
conf.loggers[0]().info("message 3")
|
||||
conf.loggers[1]().info("message 4")
|
||||
#
|
||||
# We expect this to re-open the original filenames, so we'll
|
||||
# have nine files instead of six.
|
||||
#
|
||||
loghandler.reopenFiles()
|
||||
#
|
||||
# Write to them again:
|
||||
conf.loggers[0]().info("message 5")
|
||||
conf.loggers[1]().info("message 6")
|
||||
#
|
||||
# We should now have all nine files:
|
||||
for fn in paths:
|
||||
fn1 = fn + ".1"
|
||||
fn2 = fn + ".2"
|
||||
self.assertTrue(os.path.isfile(fn), "%r must exist" % fn)
|
||||
self.assertTrue(os.path.isfile(fn1), "%r must exist" % fn1)
|
||||
self.assertTrue(os.path.isfile(fn2), "%r must exist" % fn2)
|
||||
#
|
||||
# Clean up:
|
||||
for logger in conf.loggers:
|
||||
logger = logger()
|
||||
for handler in logger.handlers[:]:
|
||||
logger.removeHandler(handler)
|
||||
handler.close()
|
||||
|
||||
|
||||
class TestReopeningLogfiles(TestReopeningRotatingLogfiles):
|
||||
|
||||
handler_factory = loghandler.FileHandler
|
||||
|
||||
_sampleconfig_template = """
|
||||
<logger>
|
||||
name foo.bar
|
||||
<logfile>
|
||||
path %(path0)s
|
||||
level debug
|
||||
</logfile>
|
||||
<logfile>
|
||||
path %(path1)s
|
||||
level info
|
||||
</logfile>
|
||||
</logger>
|
||||
|
||||
<logger>
|
||||
name bar.foo
|
||||
<logfile>
|
||||
path %(path2)s
|
||||
level info
|
||||
</logfile>
|
||||
</logger>
|
||||
"""
|
||||
|
||||
def test_logfile_reopening(self):
|
||||
#
|
||||
# This test only applies to the simple logfile reopening; it
|
||||
# doesn't work the same way as the rotating logfile handler.
|
||||
#
|
||||
paths = self.mktemp(), self.mktemp(), self.mktemp()
|
||||
d = {
|
||||
"path0": paths[0],
|
||||
"path1": paths[1],
|
||||
"path2": paths[2],
|
||||
}
|
||||
text = self._sampleconfig_template % d
|
||||
conf = self.get_config(text)
|
||||
self.assertEqual(len(conf.loggers), 2)
|
||||
# Build the loggers from the configuration, and write to them:
|
||||
conf.loggers[0]().info("message 1")
|
||||
conf.loggers[1]().info("message 2")
|
||||
npaths1 = [self.move(fn) for fn in paths]
|
||||
#
|
||||
# We expect this to re-open the original filenames, so we'll
|
||||
# have six files instead of three.
|
||||
#
|
||||
loghandler.reopenFiles()
|
||||
#
|
||||
# Write to them again:
|
||||
conf.loggers[0]().info("message 3")
|
||||
conf.loggers[1]().info("message 4")
|
||||
npaths2 = [self.move(fn) for fn in paths]
|
||||
#
|
||||
# We expect this to re-open the original filenames, so we'll
|
||||
# have nine files instead of six.
|
||||
#
|
||||
loghandler.reopenFiles()
|
||||
#
|
||||
# Write to them again:
|
||||
conf.loggers[0]().info("message 5")
|
||||
conf.loggers[1]().info("message 6")
|
||||
#
|
||||
# We should now have all nine files:
|
||||
for fn in paths:
|
||||
self.assertTrue(os.path.isfile(fn), "%r must exist" % fn)
|
||||
for fn in npaths1:
|
||||
self.assertTrue(os.path.isfile(fn), "%r must exist" % fn)
|
||||
for fn in npaths2:
|
||||
self.assertTrue(os.path.isfile(fn), "%r must exist" % fn)
|
||||
#
|
||||
# Clean up:
|
||||
for logger in conf.loggers:
|
||||
logger = logger()
|
||||
for handler in logger.handlers[:]:
|
||||
logger.removeHandler(handler)
|
||||
handler.close()
|
||||
|
||||
def test_filehandler_reopen_thread_safety(self):
|
||||
# The reopen method needs to do locking to avoid a race condition
|
||||
# with emit calls. For simplicity we replace the "acquire" and
|
||||
# "release" methods with dummies that record calls to them.
|
||||
|
||||
fn = self.mktemp()
|
||||
h = self.handler_factory(fn)
|
||||
|
||||
calls = []
|
||||
h.acquire = lambda: calls.append("acquire")
|
||||
h.release = lambda: calls.append("release")
|
||||
|
||||
h.reopen()
|
||||
h.close()
|
||||
|
||||
self.assertEqual(calls, ["acquire", "release"])
|
||||
|
||||
class TestFunctions(TestHelper, unittest.TestCase):
|
||||
|
||||
def test_log_format_bad(self):
|
||||
self.assertRaisesRegex(ValueError,
|
||||
"Invalid log format string",
|
||||
handlers.log_format,
|
||||
"%{no-such-key}s")
|
||||
|
||||
def test_resolve_deep(self):
|
||||
old_mod = None
|
||||
if hasattr(logging, 'handlers'):
|
||||
# This module is nested so it hits our coverage target,
|
||||
# and it doesn't alter any state
|
||||
# on import, so a "reimport" is fine
|
||||
del logging.handlers
|
||||
old_mod = sys.modules['logging.handlers']
|
||||
del sys.modules['logging.handlers']
|
||||
try:
|
||||
handlers.resolve('logging.handlers')
|
||||
finally:
|
||||
if old_mod is not None:
|
||||
logging.handlers = old_mod
|
||||
sys.modules['logging.handlers'] = old_mod
|
||||
|
||||
def test_http_handler_url(self):
|
||||
self.assertRaisesRegex(ValueError,
|
||||
'must be an http',
|
||||
handlers.http_handler_url, 'file://foo/baz')
|
||||
self.assertRaisesRegex(ValueError,
|
||||
'must specify a location',
|
||||
handlers.http_handler_url, 'http://')
|
||||
self.assertRaisesRegex(ValueError,
|
||||
'must specify a path',
|
||||
handlers.http_handler_url, 'http://server')
|
||||
|
||||
v = handlers.http_handler_url("http://server/path;param?q=v#fragment")
|
||||
self.assertEqual(v, ('server', '/path;param?q=v#fragment'))
|
||||
|
||||
def test_close_files(self):
|
||||
class F(object):
|
||||
closed = 0
|
||||
def close(self):
|
||||
self.closed += 1
|
||||
f = F()
|
||||
def wr():
|
||||
return f
|
||||
|
||||
loghandler._reopenable_handlers.append(wr)
|
||||
loghandler.closeFiles()
|
||||
loghandler.closeFiles()
|
||||
|
||||
self.assertEqual(1, f.closed)
|
||||
|
||||
def test_reopen_files_missing_wref(self):
|
||||
# simulate concurrent iteration that pops the ref
|
||||
def wr():
|
||||
loghandler._reopenable_handlers.remove(wr)
|
||||
|
||||
loghandler._reopenable_handlers.append(wr)
|
||||
loghandler.reopenFiles()
|
||||
|
||||
def test_logging_level(self):
|
||||
# Make sure the expected names are supported; it's not clear
|
||||
# how to check the values in a meaningful way.
|
||||
# Just make sure they're case-insensitive.
|
||||
convert = datatypes.logging_level
|
||||
for name in ["notset", "all", "trace", "debug", "blather",
|
||||
"info", "warn", "warning", "error", "fatal",
|
||||
"critical"]:
|
||||
self.assertEqual(convert(name), convert(name.upper()))
|
||||
self.assertRaises(ValueError, convert, "hopefully-not-a-valid-value")
|
||||
self.assertEqual(convert('10'), 10)
|
||||
self.assertRaises(ValueError, convert, '100')
|
||||
|
||||
def test_http_method(self):
|
||||
convert = handlers.get_or_post
|
||||
self.assertEqual(convert("get"), "GET")
|
||||
self.assertEqual(convert("GET"), "GET")
|
||||
self.assertEqual(convert("post"), "POST")
|
||||
self.assertEqual(convert("POST"), "POST")
|
||||
self.assertRaises(ValueError, convert, "")
|
||||
self.assertRaises(ValueError, convert, "foo")
|
||||
|
||||
def test_syslog_facility(self):
|
||||
convert = handlers.syslog_facility
|
||||
for name in ["auth", "authpriv", "cron", "daemon", "kern",
|
||||
"lpr", "mail", "news", "security", "syslog",
|
||||
"user", "uucp", "local0", "local1", "local2",
|
||||
"local3", "local4", "local5", "local6", "local7"]:
|
||||
self.assertEqual(convert(name), name)
|
||||
self.assertEqual(convert(name.upper()), name)
|
||||
self.assertRaises(ValueError, convert, "hopefully-never-a-valid-value")
|
||||
|
||||
|
||||
class TestStartupHandler(unittest.TestCase):
|
||||
|
||||
def test_buffer(self):
|
||||
handler = loghandler.StartupHandler()
|
||||
self.assertFalse(handler.shouldFlush(None))
|
||||
self.assertEqual(maxsize, handler.capacity)
|
||||
|
||||
records = []
|
||||
def handle(record):
|
||||
records.append(record)
|
||||
handle.handle = handle
|
||||
|
||||
handler.flushBufferTo(handle)
|
||||
self.assertEqual([], records)
|
||||
|
||||
handler.buffer.append(1)
|
||||
handler.flushBufferTo(handle)
|
||||
self.assertEqual([1], records)
|
||||
|
||||
del handle.handle
|
||||
|
||||
def test_logger_convenience_function_and_ommiting_name_to_get_root_logger():
|
||||
"""
|
||||
|
||||
The ZConfig.loggers function can be used to configure one or more loggers.
|
||||
We'll configure the rot logger and a non-root logger.
|
||||
|
||||
>>> old_level = logging.getLogger().getEffectiveLevel()
|
||||
>>> old_handler_count = len(logging.getLogger().handlers)
|
||||
|
||||
>>> ZConfig.configureLoggers('''
|
||||
... <logger>
|
||||
... level INFO
|
||||
... <logfile>
|
||||
... PATH STDOUT
|
||||
... format root %(levelname)s %(name)s %(message)s
|
||||
... </logfile>
|
||||
... </logger>
|
||||
...
|
||||
... <logger>
|
||||
... name ZConfig.TEST
|
||||
... level DEBUG
|
||||
... <logfile>
|
||||
... PATH STDOUT
|
||||
... format test %(levelname)s %(name)s %(message)s
|
||||
... </logfile>
|
||||
... </logger>
|
||||
... ''')
|
||||
|
||||
>>> logging.getLogger('ZConfig.TEST').debug('test message')
|
||||
test DEBUG ZConfig.TEST test message
|
||||
root DEBUG ZConfig.TEST test message
|
||||
|
||||
>>> logging.getLogger().getEffectiveLevel() == logging.INFO
|
||||
True
|
||||
>>> len(logging.getLogger().handlers) == old_handler_count + 1
|
||||
True
|
||||
>>> logging.getLogger('ZConfig.TEST').getEffectiveLevel() == logging.DEBUG
|
||||
True
|
||||
>>> len(logging.getLogger('ZConfig.TEST').handlers) == 1
|
||||
True
|
||||
|
||||
.. cleanup
|
||||
|
||||
>>> logging.getLogger('ZConfig.TEST').setLevel(logging.NOTSET)
|
||||
>>> logging.getLogger('ZConfig.TEST').removeHandler(
|
||||
... logging.getLogger('ZConfig.TEST').handlers[-1])
|
||||
>>> logging.getLogger().setLevel(old_level)
|
||||
>>> logging.getLogger().removeHandler(logging.getLogger().handlers[-1])
|
||||
|
||||
|
||||
"""
|
||||
|
||||
def test_suite():
|
||||
return unittest.TestSuite([
|
||||
unittest.defaultTestLoader.loadTestsFromName(__name__),
|
||||
doctest.DocTestSuite()
|
||||
])
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main(defaultTest="test_suite")
|
||||
@@ -0,0 +1,528 @@
|
||||
##############################################################################
|
||||
#
|
||||
# Copyright (c) 2002, 2003 Zope Foundation and Contributors.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# This software is subject to the provisions of the Zope Public License,
|
||||
# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
|
||||
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
|
||||
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
|
||||
# FOR A PARTICULAR PURPOSE.
|
||||
#
|
||||
##############################################################################
|
||||
"""Default implementation of a data type registry
|
||||
|
||||
This module provides the implementation of the default data type
|
||||
registry and all the standard data types supported by :mod:`ZConfig`.
|
||||
A number of convenience classes are also provided to assist in the
|
||||
creation of additional data types.
|
||||
|
||||
A "data type registry" is an object that provides conversion functions
|
||||
for data types. The interface for a :class:`registry <Registry>` is
|
||||
fairly simple.
|
||||
|
||||
A "conversion function" is any callable object that accepts a single
|
||||
argument and returns a suitable value, or raises an exception if the
|
||||
input value is not acceptable. :exc:`ValueError` is the preferred
|
||||
exception for disallowed inputs, but any other exception will be
|
||||
properly propagated.
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import datetime
|
||||
|
||||
try:
|
||||
unicode
|
||||
except NameError:
|
||||
# Python 3
|
||||
have_unicode = False
|
||||
from functools import reduce
|
||||
else:
|
||||
have_unicode = True
|
||||
|
||||
|
||||
class MemoizedConversion(object):
|
||||
"""Simple memoization for potentially expensive conversions.
|
||||
|
||||
This conversion helper caches each successful conversion for re-use
|
||||
at a later time; failed conversions are not cached in any way, since
|
||||
it is difficult to raise a meaningful exception providing
|
||||
information about the specific failure.
|
||||
"""
|
||||
|
||||
def __init__(self, conversion):
|
||||
self._memo = {}
|
||||
self._conversion = conversion
|
||||
|
||||
def __call__(self, value):
|
||||
try:
|
||||
return self._memo[value]
|
||||
except KeyError:
|
||||
v = self._conversion(value)
|
||||
self._memo[value] = v
|
||||
return v
|
||||
|
||||
|
||||
class RangeCheckedConversion(object):
|
||||
"""Conversion helper that performs range checks on the result of
|
||||
another conversion.
|
||||
|
||||
Values passed to instances of this conversion are converted using
|
||||
*conversion* and then range checked. *min* and *max*, if given and
|
||||
not ``None``, are the inclusive endpoints of the allowed range.
|
||||
Values returned by *conversion* which lay outside the range
|
||||
described by *min* and *max* cause :exc:`ValueError` to be raised.
|
||||
"""
|
||||
|
||||
def __init__(self, conversion, min=None, max=None):
|
||||
self._min = min
|
||||
self._max = max
|
||||
self._conversion = conversion
|
||||
|
||||
def __call__(self, value):
|
||||
v = self._conversion(value)
|
||||
if self._min is not None and v < self._min:
|
||||
raise ValueError("%s is below lower bound (%s)"
|
||||
% (repr(v), repr(self._min)))
|
||||
if self._max is not None and v > self._max:
|
||||
raise ValueError("%s is above upper bound (%s)"
|
||||
% (repr(v), repr(self._max)))
|
||||
return v
|
||||
|
||||
|
||||
class RegularExpressionConversion(object):
|
||||
"""Conversion that checks that the input matches the regular
|
||||
expression *regex*.
|
||||
|
||||
If it matches, returns the input, otherwise raises
|
||||
:exc:`ValueError`.
|
||||
"""
|
||||
|
||||
reason = "value did not match regular expression"
|
||||
|
||||
def __init__(self, regex):
|
||||
self._rx = re.compile(regex)
|
||||
|
||||
def __call__(self, value):
|
||||
m = self._rx.match(value)
|
||||
if m and m.group() == value:
|
||||
return value
|
||||
else:
|
||||
raise ValueError("%s: %s" % (self.reason, repr(value)))
|
||||
|
||||
|
||||
def check_locale(value):
|
||||
import locale
|
||||
prev = locale.setlocale(locale.LC_ALL)
|
||||
try:
|
||||
try:
|
||||
locale.setlocale(locale.LC_ALL, value)
|
||||
finally:
|
||||
locale.setlocale(locale.LC_ALL, prev)
|
||||
except locale.Error:
|
||||
raise ValueError(
|
||||
'The specified locale "%s" is not supported by your system.\n'
|
||||
'See your operating system documentation for more\n'
|
||||
'information on locale support.' % value)
|
||||
else:
|
||||
return value
|
||||
|
||||
|
||||
class BasicKeyConversion(RegularExpressionConversion):
|
||||
def __init__(self):
|
||||
RegularExpressionConversion.__init__(self, "[a-zA-Z][-._a-zA-Z0-9]*")
|
||||
|
||||
def __call__(self, value):
|
||||
value = str(value)
|
||||
return RegularExpressionConversion.__call__(self, value).lower()
|
||||
|
||||
|
||||
class ASCIIConversion(RegularExpressionConversion):
|
||||
def __call__(self, value):
|
||||
value = RegularExpressionConversion.__call__(self, value)
|
||||
if have_unicode and isinstance(value, unicode):
|
||||
value = value.encode("ascii")
|
||||
return value
|
||||
|
||||
|
||||
_ident_re = "[_a-zA-Z][_a-zA-Z0-9]*"
|
||||
|
||||
class IdentifierConversion(ASCIIConversion):
|
||||
reason = "not a valid Python identifier"
|
||||
|
||||
def __init__(self):
|
||||
ASCIIConversion.__init__(self, _ident_re)
|
||||
|
||||
|
||||
class DottedNameConversion(ASCIIConversion):
|
||||
reason = "not a valid dotted name"
|
||||
|
||||
def __init__(self):
|
||||
ASCIIConversion.__init__(self,
|
||||
r"%s(?:\.%s)*" % (_ident_re, _ident_re))
|
||||
|
||||
|
||||
class DottedNameSuffixConversion(ASCIIConversion):
|
||||
reason = "not a valid dotted name or suffix"
|
||||
|
||||
def __init__(self):
|
||||
ASCIIConversion.__init__(self,
|
||||
r"(?:%s)(?:\.%s)*|(?:\.%s)+"
|
||||
% (_ident_re, _ident_re, _ident_re))
|
||||
|
||||
|
||||
def integer(value):
|
||||
return int(value)
|
||||
|
||||
|
||||
def null_conversion(value):
|
||||
return value
|
||||
|
||||
|
||||
def asBoolean(s):
|
||||
"""Convert a string value to a boolean value."""
|
||||
ss = str(s).lower()
|
||||
if ss in ('yes', 'true', 'on'):
|
||||
return True
|
||||
elif ss in ('no', 'false', 'off'):
|
||||
return False
|
||||
else:
|
||||
raise ValueError("not a valid boolean value: " + repr(s))
|
||||
|
||||
|
||||
def string_list(s):
|
||||
"""Convert a string to a list of strings using .split()."""
|
||||
return s.split()
|
||||
|
||||
|
||||
port_number = RangeCheckedConversion(integer, min=0, max=0xffff).__call__
|
||||
|
||||
|
||||
class InetAddress(object):
|
||||
|
||||
def __init__(self, default_host):
|
||||
self.DEFAULT_HOST = default_host
|
||||
|
||||
def __call__(self, s):
|
||||
# returns (host, port) tuple
|
||||
host = ''
|
||||
port = None
|
||||
if ":" in s:
|
||||
host, p = s.rsplit(":", 1)
|
||||
if host.startswith('[') and host.endswith(']'):
|
||||
# [IPv6]:port
|
||||
host = host[1:-1]
|
||||
elif ':' in host:
|
||||
# Unbracketed IPv6 address;
|
||||
# last part is not the port number
|
||||
host = s
|
||||
p = None
|
||||
if p: # else leave port at None
|
||||
port = port_number(p)
|
||||
host = host.lower()
|
||||
else:
|
||||
try:
|
||||
port = port_number(s)
|
||||
except ValueError:
|
||||
if len(s.split()) != 1:
|
||||
raise ValueError("not a valid host name: " + repr(s))
|
||||
host = s.lower()
|
||||
if not host:
|
||||
host = self.DEFAULT_HOST
|
||||
return host, port
|
||||
|
||||
|
||||
if sys.platform[:3] == "win":
|
||||
DEFAULT_HOST = "localhost"
|
||||
else:
|
||||
DEFAULT_HOST = ""
|
||||
|
||||
inet_address = InetAddress(DEFAULT_HOST)
|
||||
inet_connection_address = InetAddress("127.0.0.1")
|
||||
inet_binding_address = InetAddress("")
|
||||
|
||||
class SocketAddress(object):
|
||||
# Parsing results in family and address
|
||||
# Family can be AF_UNIX (for addresses that are path names)
|
||||
# or AF_INET6 (for inet addresses with colons in them)
|
||||
# or AF_INET (for all other inet addresses);
|
||||
# An inet address is a (host, port) pair
|
||||
# Notice that no DNS lookup is performed, so if the host
|
||||
# is a DNS name, DNS lookup may end up with either IPv4 or
|
||||
# IPv6 addresses, or both
|
||||
def __init__(self, s):
|
||||
import socket
|
||||
if "/" in s or s.find(os.sep) >= 0:
|
||||
self.family = getattr(socket, "AF_UNIX", None)
|
||||
self.address = s
|
||||
else:
|
||||
self.family = socket.AF_INET
|
||||
self.address = self._parse_address(s)
|
||||
if ':' in self.address[0]:
|
||||
self.family = socket.AF_INET6
|
||||
|
||||
def _parse_address(self, s):
|
||||
return inet_address(s)
|
||||
|
||||
class SocketBindingAddress(SocketAddress):
|
||||
|
||||
def _parse_address(self, s):
|
||||
return inet_binding_address(s)
|
||||
|
||||
class SocketConnectionAddress(SocketAddress):
|
||||
|
||||
def _parse_address(self, s):
|
||||
return inet_connection_address(s)
|
||||
|
||||
|
||||
def float_conversion(v):
|
||||
return float(v)
|
||||
|
||||
|
||||
class IpaddrOrHostname(RegularExpressionConversion):
|
||||
def __init__(self):
|
||||
# IP address regex from the Perl Cookbook, Recipe 6.23 (revised ed.)
|
||||
# We allow underscores in hostnames although this is considered
|
||||
# illegal according to RFC1034.
|
||||
# Addition: IPv6 addresses are now also accepted
|
||||
expr = (r"(^(\d|[01]?\d\d|2[0-4]\d|25[0-5])\." #ipaddr
|
||||
r"(\d|[01]?\d\d|2[0-4]\d|25[0-5])\." #ipaddr cont'd
|
||||
r"(\d|[01]?\d\d|2[0-4]\d|25[0-5])\." #ipaddr cont'd
|
||||
r"(\d|[01]?\d\d|2[0-4]\d|25[0-5])$)" #ipaddr cont'd
|
||||
r"|([A-Za-z_][-A-Za-z0-9_.]*[-A-Za-z0-9_])" # or hostname
|
||||
r"|([0-9A-Fa-f:.]+:[0-9A-Fa-f:.]*)" # or superset of IPv6 addresses
|
||||
# (requiring at least one colon)
|
||||
)
|
||||
RegularExpressionConversion.__init__(self, expr)
|
||||
|
||||
def __call__(self, value):
|
||||
result = RegularExpressionConversion.__call__(self, value).lower()
|
||||
# Use C library to validate IPv6 addresses, in particular wrt.
|
||||
# number of colons and number of digits per group
|
||||
if ':' in result:
|
||||
import socket
|
||||
try:
|
||||
socket.inet_pton(socket.AF_INET6, result)
|
||||
except socket.error:
|
||||
raise ValueError('%r is not a valid IPv6 address' % value)
|
||||
return result
|
||||
|
||||
def existing_directory(v):
|
||||
nv = os.path.expanduser(v)
|
||||
if os.path.isdir(nv):
|
||||
return nv
|
||||
raise ValueError('%s is not an existing directory' % v)
|
||||
|
||||
def existing_path(v):
|
||||
nv = os.path.expanduser(v)
|
||||
if os.path.exists(nv):
|
||||
return nv
|
||||
raise ValueError('%s is not an existing path' % v)
|
||||
|
||||
def existing_file(v):
|
||||
nv = os.path.expanduser(v)
|
||||
if os.path.exists(nv):
|
||||
return nv
|
||||
raise ValueError('%s is not an existing file' % v)
|
||||
|
||||
def existing_dirpath(v):
|
||||
nv = os.path.expanduser(v)
|
||||
dirname = os.path.dirname(nv)
|
||||
if not dirname:
|
||||
# relative pathname with no directory component
|
||||
return nv
|
||||
if os.path.isdir(dirname):
|
||||
return nv
|
||||
raise ValueError('The directory named as part of the path %s '
|
||||
'does not exist.' % v)
|
||||
|
||||
|
||||
class SuffixMultiplier(object):
|
||||
# d is a dictionary of suffixes to integer multipliers. If no suffixes
|
||||
# match, default is the multiplier. Matches are case insensitive. Return
|
||||
# values are in the fundamental unit.
|
||||
def __init__(self, d, default=1):
|
||||
self._d = d
|
||||
self._default = default
|
||||
# all keys must be the same size
|
||||
def check(a, b):
|
||||
if len(a) != len(b):
|
||||
raise ValueError("suffix length mismatch")
|
||||
return a
|
||||
self._keysz = len(reduce(check, d))
|
||||
|
||||
def __call__(self, v):
|
||||
v = v.lower()
|
||||
for s, m in self._d.items():
|
||||
if v[-self._keysz:] == s:
|
||||
return int(v[:-self._keysz]) * m
|
||||
return int(v) * self._default
|
||||
|
||||
|
||||
def timedelta(s):
|
||||
# Unlike the standard time-interval data type, which returns a float
|
||||
# number of seconds, this datatype takes a wider range of syntax and
|
||||
# returns a datetime.timedelta
|
||||
#
|
||||
# Accepts suffixes:
|
||||
# w - weeks
|
||||
# d - days
|
||||
# h - hours
|
||||
# m - minutes
|
||||
# s - seconds
|
||||
#
|
||||
# and all arguments may be integers or floats, positive or negative.
|
||||
# More than one time interval suffix value may appear on the line, but
|
||||
# they should all be separated by spaces, e.g.:
|
||||
#
|
||||
# sleep_time 4w 2d 7h 12m 0.00001s
|
||||
weeks = days = hours = minutes = seconds = 0
|
||||
for part in s.split():
|
||||
val = float(part[:-1])
|
||||
suffix = part[-1]
|
||||
if suffix == 'w':
|
||||
weeks = val
|
||||
elif suffix == 'd':
|
||||
days = val
|
||||
elif suffix == 'h':
|
||||
hours = val
|
||||
elif suffix == 'm':
|
||||
minutes = val
|
||||
elif suffix == 's':
|
||||
seconds = val
|
||||
else:
|
||||
raise TypeError('bad part %s in %s' % (part, s))
|
||||
return datetime.timedelta(weeks=weeks, days=days, hours=hours,
|
||||
minutes=minutes, seconds=seconds)
|
||||
|
||||
|
||||
stock_datatypes = {
|
||||
"boolean": asBoolean,
|
||||
"dotted-name": DottedNameConversion(),
|
||||
"dotted-suffix": DottedNameSuffixConversion(),
|
||||
"identifier": IdentifierConversion(),
|
||||
"integer": integer,
|
||||
"float": float_conversion,
|
||||
"string": str,
|
||||
"string-list": string_list,
|
||||
"null": null_conversion,
|
||||
"locale": MemoizedConversion(check_locale),
|
||||
"port-number": port_number,
|
||||
"basic-key": BasicKeyConversion(),
|
||||
"inet-address": inet_address,
|
||||
"inet-binding-address": inet_binding_address,
|
||||
"inet-connection-address": inet_connection_address,
|
||||
"socket-address": SocketAddress,
|
||||
"socket-binding-address": SocketBindingAddress,
|
||||
"socket-connection-address": SocketConnectionAddress,
|
||||
"ipaddr-or-hostname":IpaddrOrHostname(),
|
||||
"existing-directory":existing_directory,
|
||||
"existing-path": existing_path,
|
||||
"existing-file": existing_file,
|
||||
"existing-dirpath": existing_dirpath,
|
||||
"byte-size": SuffixMultiplier({'kb': 1024,
|
||||
'mb': 1024*1024,
|
||||
'gb': 1024*1024*1024,
|
||||
}),
|
||||
"time-interval": SuffixMultiplier({'s': 1,
|
||||
'm': 60,
|
||||
'h': 60*60,
|
||||
'd': 60*60*24,
|
||||
}),
|
||||
"timedelta": timedelta,
|
||||
}
|
||||
|
||||
|
||||
class Registry(object):
|
||||
"""Implementation of a simple type registry.
|
||||
|
||||
If given, *stock* should be a mapping which defines the "built-in"
|
||||
data types for the registry; if omitted or ``None``, the standard
|
||||
set of data types is used (see :ref:`standard-datatypes`).
|
||||
"""
|
||||
def __init__(self, stock=None):
|
||||
if stock is None:
|
||||
stock = stock_datatypes.copy()
|
||||
self._stock = stock
|
||||
self._other = {}
|
||||
self._basic_key = None
|
||||
|
||||
def find_name(self, conversion):
|
||||
"""Return the best name for *conversion*, which must have been returned
|
||||
from *get* on this object."""
|
||||
for dct in self._other, self._stock:
|
||||
for k, v in dct.items():
|
||||
if v is conversion:
|
||||
return k
|
||||
|
||||
# If they followed the rules, we shouldn't get here.
|
||||
return str(conversion) # pragma: no cover
|
||||
|
||||
def get(self, name):
|
||||
"""Return the type conversion routine for *name*.
|
||||
|
||||
If the conversion function cannot be found, an (unspecified)
|
||||
exception is raised. If the name is not provided in the stock
|
||||
set of data types by this registry and has not otherwise been
|
||||
registered, this method uses the :meth:`search` method to load
|
||||
the conversion function. This is the only method the rest of
|
||||
:mod:`ZConfig` requires.
|
||||
"""
|
||||
if '.' not in name:
|
||||
if self._basic_key is None:
|
||||
self._basic_key = self._other.get("basic-key")
|
||||
if self._basic_key is None:
|
||||
self._basic_key = self._stock.get("basic-key")
|
||||
if self._basic_key is None:
|
||||
self._basic_key = stock_datatypes["basic-key"]
|
||||
name = self._basic_key(name)
|
||||
t = self._stock.get(name)
|
||||
if t is None:
|
||||
t = self._other.get(name)
|
||||
if t is None:
|
||||
t = self.search(name)
|
||||
return t
|
||||
|
||||
def register(self, name, conversion):
|
||||
"""Register the data type name *name* to use the conversion function
|
||||
*conversion*.
|
||||
|
||||
If *name* is already registered or provided as a stock data
|
||||
type, :exc:`ValueError` is raised (this includes the case when
|
||||
*name* was found using the :meth:`search` method).
|
||||
"""
|
||||
if name in self._stock:
|
||||
raise ValueError("datatype name conflicts with built-in type: "
|
||||
+ repr(name))
|
||||
if name in self._other:
|
||||
raise ValueError("datatype name already registered: " + repr(name))
|
||||
self._other[name] = conversion
|
||||
|
||||
def search(self, name):
|
||||
"""This is a helper method for the default implementation of the
|
||||
:meth:`get` method.
|
||||
|
||||
If *name* is a Python dotted-name, this method loads the value
|
||||
for the name by dynamically importing the containing module
|
||||
and extracting the value of the name. The name must refer to a
|
||||
usable conversion function.
|
||||
"""
|
||||
if not "." in name:
|
||||
raise ValueError("unloadable datatype name: " + repr(name))
|
||||
components = name.split('.')
|
||||
start = components[0]
|
||||
g = {}
|
||||
package = __import__(start, g, g)
|
||||
modulenames = [start]
|
||||
for component in components[1:]:
|
||||
modulenames.append(component)
|
||||
try:
|
||||
package = getattr(package, component)
|
||||
except AttributeError:
|
||||
n = '.'.join(modulenames)
|
||||
package = __import__(n, g, g, component)
|
||||
self._other[name] = package
|
||||
return package
|
||||
@@ -0,0 +1,519 @@
|
||||
##############################################################################
|
||||
#
|
||||
# Copyright (c) 2002, 2003 Zope Foundation and Contributors.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# This software is subject to the provisions of the Zope Public License,
|
||||
# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
|
||||
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
|
||||
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
|
||||
# FOR A PARTICULAR PURPOSE.
|
||||
#
|
||||
##############################################################################
|
||||
"""Objects that can describe a ZConfig schema."""
|
||||
|
||||
import copy
|
||||
import ZConfig
|
||||
|
||||
from abc import abstractmethod
|
||||
from collections import OrderedDict
|
||||
from functools import total_ordering
|
||||
|
||||
from ZConfig._compat import AbstractBaseClass
|
||||
|
||||
@total_ordering
|
||||
class UnboundedThing(object):
|
||||
__slots__ = ()
|
||||
|
||||
def __gt__(self, other):
|
||||
if isinstance(other, self.__class__):
|
||||
return False
|
||||
return True
|
||||
|
||||
def __eq__(self, other):
|
||||
return isinstance(other, self.__class__)
|
||||
|
||||
def __repr__(self): # pragma: no cover
|
||||
return "<Unbounded>"
|
||||
|
||||
Unbounded = UnboundedThing()
|
||||
|
||||
|
||||
class ValueInfo(object):
|
||||
__slots__ = 'value', 'position'
|
||||
|
||||
def __init__(self, value, position):
|
||||
self.value = value
|
||||
# position is (lineno, colno, url)
|
||||
self.position = position
|
||||
|
||||
def convert(self, datatype):
|
||||
try:
|
||||
return datatype(self.value)
|
||||
except ValueError as e:
|
||||
raise ZConfig.DataConversionError(e, self.value, self.position)
|
||||
|
||||
|
||||
class BaseInfo(object):
|
||||
"""Information about a single configuration key."""
|
||||
|
||||
description = None
|
||||
example = None
|
||||
metadefault = None
|
||||
|
||||
def __init__(self, name, datatype, minOccurs, maxOccurs, handler,
|
||||
attribute):
|
||||
assert maxOccurs is not None, "Use Unbounded for an upper bound, not None"
|
||||
assert minOccurs is not None, "Use 0 for a lower bound, not None"
|
||||
|
||||
if maxOccurs < 1:
|
||||
raise ZConfig.SchemaError(
|
||||
"maxOccurs must be at least 1")
|
||||
if minOccurs > maxOccurs:
|
||||
raise ZConfig.SchemaError(
|
||||
"minOccurs cannot be more than maxOccurs")
|
||||
self.name = name
|
||||
self.datatype = datatype
|
||||
self.minOccurs = minOccurs
|
||||
self.maxOccurs = maxOccurs
|
||||
self.handler = handler
|
||||
self.attribute = attribute
|
||||
|
||||
def __repr__(self):
|
||||
clsname = self.__class__.__name__
|
||||
return "<%s for %s>" % (clsname, repr(self.name))
|
||||
|
||||
def isabstract(self):
|
||||
return False
|
||||
|
||||
def ismulti(self):
|
||||
return self.maxOccurs > 1
|
||||
|
||||
def issection(self):
|
||||
return False
|
||||
|
||||
|
||||
class BaseKeyInfo(AbstractBaseClass, BaseInfo):
|
||||
|
||||
_rawdefaults = None
|
||||
|
||||
def __init__(self, name, datatype, minOccurs, maxOccurs, handler,
|
||||
attribute):
|
||||
BaseInfo.__init__(self, name, datatype, minOccurs, maxOccurs,
|
||||
handler, attribute)
|
||||
self._finished = False
|
||||
|
||||
def finish(self):
|
||||
if self._finished:
|
||||
raise ZConfig.SchemaError(
|
||||
"cannot finish KeyInfo more than once")
|
||||
self._finished = True
|
||||
|
||||
def adddefault(self, value, position, key=None):
|
||||
if self._finished:
|
||||
raise ZConfig.SchemaError(
|
||||
"cannot add default values to finished KeyInfo")
|
||||
# Check that the name/keyed relationship is right:
|
||||
if self.name == "+" and key is None:
|
||||
raise ZConfig.SchemaError(
|
||||
"default values must be keyed for name='+'")
|
||||
elif self.name != "+" and key is not None:
|
||||
raise ZConfig.SchemaError(
|
||||
"unexpected key for default value")
|
||||
self.add_valueinfo(ValueInfo(value, position), key)
|
||||
|
||||
@abstractmethod
|
||||
def add_valueinfo(self, vi, key):
|
||||
"""Actually add a ValueInfo to this key-info object.
|
||||
|
||||
The appropriate value of None-ness of key has already been
|
||||
checked with regard to the name of the key, and has been found
|
||||
permissible to add.
|
||||
|
||||
This method is a requirement for subclasses, and should not be
|
||||
called by client code.
|
||||
"""
|
||||
|
||||
def prepare_raw_defaults(self):
|
||||
assert self.name == "+"
|
||||
if self._rawdefaults is None:
|
||||
self._rawdefaults = self._default
|
||||
self._default = OrderedDict()
|
||||
|
||||
|
||||
class KeyInfo(BaseKeyInfo):
|
||||
|
||||
_default = None
|
||||
|
||||
def __init__(self, name, datatype, minOccurs, handler, attribute):
|
||||
BaseKeyInfo.__init__(self, name, datatype, minOccurs, 1,
|
||||
handler, attribute)
|
||||
if self.name == "+":
|
||||
self._default = OrderedDict()
|
||||
|
||||
def add_valueinfo(self, vi, key):
|
||||
if self.name == "+":
|
||||
if key in self._default:
|
||||
# not ideal: we're presenting the unconverted
|
||||
# version of the key
|
||||
raise ZConfig.SchemaError(
|
||||
"duplicate default value for key %s" % repr(key))
|
||||
self._default[key] = vi
|
||||
elif self._default is not None:
|
||||
raise ZConfig.SchemaError(
|
||||
"cannot set more than one default to key with maxOccurs == 1")
|
||||
else:
|
||||
self._default = vi
|
||||
|
||||
def computedefault(self, keytype):
|
||||
self.prepare_raw_defaults()
|
||||
for k, vi in self._rawdefaults.items():
|
||||
key = ValueInfo(k, vi.position).convert(keytype)
|
||||
self.add_valueinfo(vi, key)
|
||||
|
||||
def getdefault(self):
|
||||
# Use copy.copy() to make sure we don't allow polution of
|
||||
# our internal data without having to worry about both the
|
||||
# list and dictionary cases:
|
||||
return copy.copy(self._default)
|
||||
|
||||
|
||||
class MultiKeyInfo(BaseKeyInfo):
|
||||
|
||||
def __init__(self, name, datatype, minOccurs, maxOccurs, handler,
|
||||
attribute):
|
||||
BaseKeyInfo.__init__(self, name, datatype, minOccurs, maxOccurs,
|
||||
handler, attribute)
|
||||
if self.name == "+":
|
||||
self._default = OrderedDict()
|
||||
else:
|
||||
self._default = []
|
||||
|
||||
def add_valueinfo(self, vi, key):
|
||||
if self.name == "+":
|
||||
# This is a keyed value, not a simple value:
|
||||
if key in self._default:
|
||||
self._default[key].append(vi)
|
||||
else:
|
||||
self._default[key] = [vi]
|
||||
else:
|
||||
self._default.append(vi)
|
||||
|
||||
def computedefault(self, keytype):
|
||||
self.prepare_raw_defaults()
|
||||
for k, vlist in self._rawdefaults.items():
|
||||
key = ValueInfo(k, vlist[0].position).convert(keytype)
|
||||
for vi in vlist:
|
||||
self.add_valueinfo(vi, key)
|
||||
|
||||
def getdefault(self):
|
||||
return copy.copy(self._default)
|
||||
|
||||
|
||||
class SectionInfo(BaseInfo):
|
||||
def __init__(self, name, sectiontype, minOccurs, maxOccurs, handler,
|
||||
attribute):
|
||||
# name - name of the section; one of '*', '+', or name1
|
||||
# sectiontype - SectionType instance
|
||||
# minOccurs - minimum number of occurances of the section
|
||||
# maxOccurs - maximum number of occurances; if > 1, name
|
||||
# must be '*' or '+'
|
||||
# handler - handler name called when value(s) must take effect,
|
||||
# or None
|
||||
# attribute - name of the attribute on the SectionValue object
|
||||
if maxOccurs > 1:
|
||||
if name not in ('*', '+'):
|
||||
raise ZConfig.SchemaError(
|
||||
"sections which can occur more than once must"
|
||||
" use a name of '*' or '+'")
|
||||
if not attribute:
|
||||
raise ZConfig.SchemaError(
|
||||
"sections which can occur more than once must"
|
||||
" specify a target attribute name")
|
||||
if sectiontype.isabstract():
|
||||
datatype = None
|
||||
else:
|
||||
datatype = sectiontype.datatype
|
||||
BaseInfo.__init__(self, name, datatype,
|
||||
minOccurs, maxOccurs, handler, attribute)
|
||||
self.sectiontype = sectiontype
|
||||
|
||||
def __repr__(self):
|
||||
clsname = self.__class__.__name__
|
||||
return "<%s for %s (%s)>" % (
|
||||
clsname, self.sectiontype.name, repr(self.name))
|
||||
|
||||
def issection(self):
|
||||
return True
|
||||
|
||||
def allowUnnamed(self):
|
||||
return self.name == "*"
|
||||
|
||||
def isAllowedName(self, name):
|
||||
if name == "*" or name == "+":
|
||||
return False
|
||||
elif self.name == "+":
|
||||
return True if name else False
|
||||
elif self.name == "*":
|
||||
return True
|
||||
else:
|
||||
return name == self.name
|
||||
|
||||
def getdefault(self):
|
||||
# sections cannot have defaults
|
||||
if self.maxOccurs > 1:
|
||||
return []
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
class AbstractType(object):
|
||||
# This isn't actually "abstract" in the Python ABC sense,
|
||||
# it's only abstract from the schema sense. This class is
|
||||
# instantiated, and not expected to be subclassed.
|
||||
__slots__ = '_subtypes', 'name', 'description'
|
||||
|
||||
def __init__(self, name):
|
||||
self._subtypes = OrderedDict()
|
||||
self.name = name
|
||||
self.description = None
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self._subtypes.items())
|
||||
|
||||
def addsubtype(self, type_):
|
||||
self._subtypes[type_.name] = type_
|
||||
|
||||
def getsubtype(self, name):
|
||||
try:
|
||||
return self._subtypes[name]
|
||||
except KeyError:
|
||||
raise ZConfig.SchemaError("no sectiontype %s in abstracttype %s"
|
||||
% (repr(name), repr(self.name)))
|
||||
|
||||
def hassubtype(self, name):
|
||||
"""Return true iff this type has 'name' as a concrete manifestation."""
|
||||
return name in self._subtypes.keys()
|
||||
|
||||
def getsubtypenames(self):
|
||||
"""Return the names of all concrete types as a sorted list."""
|
||||
return sorted(self._subtypes.keys())
|
||||
|
||||
def isabstract(self):
|
||||
return True
|
||||
|
||||
|
||||
class SectionType(object):
|
||||
def __init__(self, name, keytype, valuetype, datatype, registry, types):
|
||||
# name - name of the section, or '*' or '+'
|
||||
# datatype - type for the section itself
|
||||
# keytype - type for the keys themselves
|
||||
# valuetype - default type for key values
|
||||
self.name = name
|
||||
self.datatype = datatype
|
||||
self.keytype = keytype
|
||||
self.valuetype = valuetype
|
||||
self.handler = None
|
||||
self.description = None
|
||||
self.example = None
|
||||
self.registry = registry
|
||||
self._children = [] # [(key, info), ...]
|
||||
self._attrmap = OrderedDict() # {attribute: info, ...}
|
||||
self._keymap = OrderedDict() # {key: info, ...}
|
||||
self._types = types
|
||||
|
||||
def gettype(self, name):
|
||||
n = name.lower()
|
||||
try:
|
||||
return self._types[n]
|
||||
except KeyError:
|
||||
raise ZConfig.SchemaError("unknown type name: " + repr(name))
|
||||
|
||||
def gettypenames(self):
|
||||
return list(self._types.keys())
|
||||
|
||||
def __len__(self):
|
||||
return len(self._children)
|
||||
|
||||
def __getitem__(self, index):
|
||||
return self._children[index]
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self._children)
|
||||
|
||||
def itertypes(self):
|
||||
return iter(sorted(self._types.items()))
|
||||
|
||||
def _add_child(self, key, info):
|
||||
# check naming constraints
|
||||
assert key or info.attribute
|
||||
if key and key in self._keymap:
|
||||
raise ZConfig.SchemaError(
|
||||
"child name %s already used" % key)
|
||||
if info.attribute and info.attribute in self._attrmap:
|
||||
raise ZConfig.SchemaError(
|
||||
"child attribute name %s already used" % info.attribute)
|
||||
# a-ok, add the item to the appropriate maps
|
||||
if info.attribute:
|
||||
self._attrmap[info.attribute] = info
|
||||
if key:
|
||||
self._keymap[key] = info
|
||||
self._children.append((key, info))
|
||||
|
||||
def addkey(self, keyinfo):
|
||||
self._add_child(keyinfo.name, keyinfo)
|
||||
|
||||
def addsection(self, name, sectinfo):
|
||||
assert name not in ("*", "+")
|
||||
self._add_child(name, sectinfo)
|
||||
|
||||
def getinfo(self, key):
|
||||
if not key:
|
||||
raise ZConfig.ConfigurationError(
|
||||
"cannot match a key without a name")
|
||||
try:
|
||||
return self._keymap[key]
|
||||
except KeyError:
|
||||
raise ZConfig.ConfigurationError("no key matching " + repr(key))
|
||||
|
||||
def getrequiredtypes(self):
|
||||
d = OrderedDict()
|
||||
if self.name:
|
||||
d[self.name] = 1
|
||||
stack = [self]
|
||||
while stack:
|
||||
info = stack.pop()
|
||||
for key, ci in info._children:
|
||||
if ci.issection():
|
||||
t = ci.sectiontype
|
||||
if t.name not in d:
|
||||
d[t.name] = 1
|
||||
stack.append(t)
|
||||
return list(d.keys())
|
||||
|
||||
def getsectioninfo(self, type_, name):
|
||||
for key, info in self._children:
|
||||
if key:
|
||||
if key == name:
|
||||
if not info.issection():
|
||||
raise ZConfig.ConfigurationError(
|
||||
"section name %s already in use for key" % key)
|
||||
st = info.sectiontype
|
||||
if st.isabstract():
|
||||
try:
|
||||
st = st.getsubtype(type_)
|
||||
except ZConfig.ConfigurationError: # pragma: no cover
|
||||
raise ZConfig.ConfigurationError(
|
||||
"section type %s not allowed for name %s"
|
||||
% (repr(type_), repr(key)))
|
||||
if st.name != type_:
|
||||
raise ZConfig.ConfigurationError(
|
||||
"name %s must be used for a %s section"
|
||||
% (repr(name), repr(st.name)))
|
||||
return info
|
||||
# else must be a sectiontype or an abstracttype:
|
||||
elif info.sectiontype.name == type_:
|
||||
if not (name or info.allowUnnamed()):
|
||||
raise ZConfig.ConfigurationError(
|
||||
repr(type_) + " sections must be named")
|
||||
return info
|
||||
elif info.sectiontype.isabstract():
|
||||
st = info.sectiontype
|
||||
if st.name == type_: # pragma: no cover
|
||||
raise ZConfig.ConfigurationError(
|
||||
"cannot define section with an abstract type")
|
||||
try:
|
||||
st = st.getsubtype(type_)
|
||||
except ZConfig.ConfigurationError: # pragma: no cover
|
||||
# not this one; maybe a different one
|
||||
pass
|
||||
else:
|
||||
return info
|
||||
raise ZConfig.ConfigurationError(
|
||||
"no matching section defined for type='%s', name='%s'" % (
|
||||
type_, name))
|
||||
|
||||
def isabstract(self):
|
||||
return False
|
||||
|
||||
|
||||
class SchemaType(SectionType):
|
||||
def __init__(self, keytype, valuetype, datatype, handler, url,
|
||||
registry):
|
||||
SectionType.__init__(self, None, keytype, valuetype, datatype,
|
||||
registry, {})
|
||||
self._components = OrderedDict()
|
||||
self.handler = handler
|
||||
self.url = url
|
||||
|
||||
def addtype(self, typeinfo):
|
||||
n = typeinfo.name
|
||||
if n in self._types:
|
||||
raise ZConfig.SchemaError("type name cannot be redefined: "
|
||||
+ repr(typeinfo.name))
|
||||
self._types[n] = typeinfo
|
||||
|
||||
def allowUnnamed(self):
|
||||
return True
|
||||
|
||||
def isAllowedName(self, name):
|
||||
return False
|
||||
|
||||
def issection(self):
|
||||
return True
|
||||
|
||||
def getunusedtypes(self):
|
||||
alltypes = self.gettypenames()
|
||||
reqtypes = self.getrequiredtypes()
|
||||
for n in reqtypes:
|
||||
alltypes.remove(n)
|
||||
if self.name and self.name in alltypes:
|
||||
# Not clear we can get here
|
||||
alltypes.remove(self.name) # pragma: no cover.
|
||||
return alltypes
|
||||
|
||||
def createSectionType(self, name, keytype, valuetype, datatype):
|
||||
t = SectionType(name, keytype, valuetype, datatype,
|
||||
self.registry, self._types)
|
||||
self.addtype(t)
|
||||
return t
|
||||
|
||||
def deriveSectionType(self, base, name, keytype, valuetype, datatype):
|
||||
if isinstance(base, SchemaType):
|
||||
raise ZConfig.SchemaError(
|
||||
"cannot derive sectiontype from top-level schema")
|
||||
t = self.createSectionType(name, keytype, valuetype, datatype)
|
||||
t._attrmap.update(base._attrmap)
|
||||
t._keymap.update(base._keymap)
|
||||
t._children.extend(base._children)
|
||||
for i in range(len(t._children)):
|
||||
key, info = t._children[i]
|
||||
if isinstance(info, BaseKeyInfo) and info.name == "+":
|
||||
# need to create a new info object and recompute the
|
||||
# default mapping based on the new keytype
|
||||
info = copy.copy(info)
|
||||
info.computedefault(t.keytype)
|
||||
t._children[i] = (key, info)
|
||||
return t
|
||||
|
||||
def addComponent(self, name):
|
||||
if name in self._components:
|
||||
raise ZConfig.SchemaError("already have component %s" % name)
|
||||
self._components[name] = name
|
||||
|
||||
def hasComponent(self, name):
|
||||
return name in self._components
|
||||
|
||||
|
||||
def createDerivedSchema(base):
|
||||
new = SchemaType(base.keytype, base.valuetype, base.datatype,
|
||||
base.handler, base.url, base.registry)
|
||||
new._components.update(base._components)
|
||||
new.description = base.description
|
||||
new.example = base.example
|
||||
new._children[:] = base._children
|
||||
new._attrmap.update(base._attrmap)
|
||||
new._keymap.update(base._keymap)
|
||||
new._types.update(base._types)
|
||||
return new
|
||||
@@ -0,0 +1,528 @@
|
||||
##############################################################################
|
||||
#
|
||||
# Copyright (c) 2002, 2003 Zope Foundation and Contributors.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# This software is subject to the provisions of the Zope Public License,
|
||||
# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
|
||||
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
|
||||
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
|
||||
# FOR A PARTICULAR PURPOSE.
|
||||
#
|
||||
##############################################################################
|
||||
"""Schema loader utility."""
|
||||
|
||||
import os.path
|
||||
import re
|
||||
import sys
|
||||
|
||||
from abc import abstractmethod
|
||||
from io import StringIO
|
||||
|
||||
import ZConfig
|
||||
import ZConfig.cfgparser
|
||||
import ZConfig.datatypes
|
||||
import ZConfig.info
|
||||
import ZConfig.matcher
|
||||
import ZConfig.schema
|
||||
import ZConfig.url
|
||||
|
||||
from ZConfig._compat import reraise
|
||||
from ZConfig._compat import raise_with_same_tb
|
||||
from ZConfig._compat import urllib2
|
||||
from ZConfig._compat import AbstractBaseClass
|
||||
from ZConfig._compat import pathname2url
|
||||
|
||||
|
||||
def loadSchema(url):
|
||||
"""Load a schema definition from the URL *url*.
|
||||
|
||||
*url* may be a URL, absolute pathname, or relative pathname.
|
||||
Fragment identifiers are not supported.
|
||||
|
||||
The resulting schema object can be passed to :func:`loadConfig` or
|
||||
:func:`loadConfigFile`. The schema object may be used as many
|
||||
times as needed.
|
||||
|
||||
.. seealso:: :class:`~.SchemaLoader`, :meth:`.BaseLoader.loadURL`
|
||||
"""
|
||||
return SchemaLoader().loadURL(url)
|
||||
|
||||
|
||||
def loadSchemaFile(file, url=None):
|
||||
"""Load a schema definition from the open file object *file*.
|
||||
|
||||
If *url* is given and not ``None``, it should be the URL of
|
||||
resource represented by *file*. If *url* is omitted or ``None``, a
|
||||
URL may be computed from the ``name`` attribute of *file*, if
|
||||
present. The resulting schema object can be passed to
|
||||
:func:`loadConfig` or :func:`loadConfigFile`. The schema object
|
||||
may be used as many times as needed.
|
||||
|
||||
.. seealso:: :class:`~.SchemaLoader`, :meth:`.BaseLoader.loadFile`
|
||||
"""
|
||||
return SchemaLoader().loadFile(file, url)
|
||||
|
||||
|
||||
def loadConfig(schema, url, overrides=()):
|
||||
"""Load and return a configuration from a URL or pathname given by
|
||||
*url*.
|
||||
|
||||
*url* may be a URL, absolute pathname, or relative pathname.
|
||||
Fragment identifiers are not supported. *schema* is a reference to a
|
||||
schema loaded by :func:`loadSchema` or :func:`loadSchemaFile`.
|
||||
|
||||
The return value is a tuple containing the configuration object and
|
||||
a composite handler that, when called with a name-to-handler
|
||||
mapping, calls all the handlers for the configuration.
|
||||
|
||||
The optional *overrides* argument represents information derived
|
||||
from command-line arguments. If given, it must be either a
|
||||
sequence of value specifiers, or ``None``. A "value specifier" is
|
||||
a string of the form ``optionpath=value``, for example,
|
||||
``some/path/to/key=value``.
|
||||
|
||||
.. seealso::
|
||||
:meth:`.ExtendedConfigLoader.addOption`
|
||||
For information on the format of value specifiers.
|
||||
:class:`~.ConfigLoader`
|
||||
For information about loading configs.
|
||||
:meth:`.BaseLoader.loadURL`
|
||||
For information about the format of *url*
|
||||
"""
|
||||
return _get_config_loader(schema, overrides).loadURL(url)
|
||||
|
||||
|
||||
def loadConfigFile(schema, file, url=None, overrides=()):
|
||||
"""Load and return a configuration from an opened file object.
|
||||
|
||||
If *url* is omitted, one will be computed based on the ``name``
|
||||
attribute of *file*, if it exists. If no URL can be determined,
|
||||
all ``%include`` statements in the configuration must use absolute
|
||||
URLs. *schema* is a reference to a schema loaded by
|
||||
:func:`loadSchema` or :func:`loadSchemaFile`.
|
||||
|
||||
The return value is a tuple containing the configuration object
|
||||
and a composite handler that, when called with a name-to-handler
|
||||
mapping, calls all the handlers for the configuration. The
|
||||
*overrides* argument is the same as for the :func:`loadConfig`
|
||||
function.
|
||||
|
||||
.. seealso:: :class:`~.ConfigLoader`, :meth:`.BaseLoader.loadFile`,
|
||||
:meth:`.ExtendedConfigLoader.addOption`
|
||||
"""
|
||||
return _get_config_loader(schema, overrides).loadFile(file, url)
|
||||
|
||||
|
||||
def _get_config_loader(schema, overrides):
|
||||
if overrides:
|
||||
from ZConfig import cmdline
|
||||
loader = cmdline.ExtendedConfigLoader(schema)
|
||||
for opt in overrides:
|
||||
loader.addOption(opt)
|
||||
else:
|
||||
loader = ConfigLoader(schema)
|
||||
return loader
|
||||
|
||||
|
||||
class BaseLoader(AbstractBaseClass):
|
||||
"""Base class for loader objects.
|
||||
|
||||
This should not be instantiated
|
||||
directly, as the :meth:`loadResource` method must be overridden
|
||||
for the instance to be used via the public API.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def createResource(self, file, url):
|
||||
"""Returns a resource object for an open file and URL, given as *file*
|
||||
and *url*, respectively.
|
||||
|
||||
This may be overridden by a subclass if an alternate resource
|
||||
implementation is desired.
|
||||
"""
|
||||
return Resource(file, url)
|
||||
|
||||
def loadURL(self, url):
|
||||
"""Open and load a resource specified by the URL *url*.
|
||||
|
||||
This method uses the :meth:`loadResource` method to perform the
|
||||
actual load, and returns whatever that method returns.
|
||||
"""
|
||||
url = self.normalizeURL(url)
|
||||
r = self.openResource(url)
|
||||
try:
|
||||
return self.loadResource(r)
|
||||
finally:
|
||||
r.close()
|
||||
|
||||
def loadFile(self, file, url=None):
|
||||
"""Load from an open file object, *file*.
|
||||
|
||||
If given and not ``None``, *url* should be the URL of the
|
||||
resource represented by *file*. If omitted or *None*, the
|
||||
``name`` attribute of *file* is used to compute a ``file:``
|
||||
URL, if present.
|
||||
|
||||
This method uses the :meth:`loadResource` method to perform the
|
||||
actual load, and returns whatever that method returns.
|
||||
"""
|
||||
if not url:
|
||||
url = _url_from_file(file)
|
||||
r = self.createResource(file, url)
|
||||
try:
|
||||
return self.loadResource(r)
|
||||
finally:
|
||||
r.close()
|
||||
|
||||
# utilities
|
||||
|
||||
@abstractmethod
|
||||
def loadResource(self, resource):
|
||||
"""Abstract method.
|
||||
|
||||
Subclasses of :class:`BaseLoader` must implement this method to
|
||||
actually load the resource and return the appropriate
|
||||
application-level object.
|
||||
"""
|
||||
|
||||
def openResource(self, url):
|
||||
"""Returns a resource object that represents the URL *url*.
|
||||
|
||||
The URL is opened using the :func:`urllib2.urlopen` function,
|
||||
and the returned resource object is created using
|
||||
:meth:`createResource`. If the URL cannot be opened,
|
||||
:exc:`~.ConfigurationError` is raised.
|
||||
"""
|
||||
# ConfigurationError exceptions raised here should be
|
||||
# str()able to generate a message for an end user.
|
||||
#
|
||||
# XXX This should be replaced to use a local cache for remote
|
||||
# resources. The policy needs to support both re-retrieve on
|
||||
# change and provide the cached resource when the remote
|
||||
# resource is not accessible.
|
||||
url = str(url)
|
||||
if url.startswith("package:"):
|
||||
_, package, filename = url.split(":", 2)
|
||||
file = openPackageResource(package, filename)
|
||||
else:
|
||||
try:
|
||||
file = urllib2.urlopen(url)
|
||||
except urllib2.URLError as e:
|
||||
# urllib2.URLError has a particularly hostile str(), so we
|
||||
# generally don't want to pass it along to the user.
|
||||
self._raise_open_error(url, e.reason) # pragma: no cover
|
||||
except (IOError, OSError) as e:
|
||||
# Python 2.1 raises a different error from Python 2.2+,
|
||||
# so we catch both to make sure we detect the situation.
|
||||
self._raise_open_error(url, str(e))
|
||||
|
||||
# Python 3 support: file.read() returns bytes, so we convert it
|
||||
# to an StringIO. (Can't use io.TextIOWrapper because of
|
||||
# http://bugs.python.org/issue16723 and probably other bugs).
|
||||
# Do this even on Python 2 to avoid keeping a network connection
|
||||
# open for an unbounded amount of time and to catch IOErrors here,
|
||||
# where they make sense.
|
||||
try:
|
||||
data = file.read()
|
||||
finally:
|
||||
file.close()
|
||||
if isinstance(data, bytes):
|
||||
# Be sure to specify an (useful) encoding so we don't get
|
||||
# the system default, typically ascii.
|
||||
data = data.decode('utf-8')
|
||||
file = StringIO(data)
|
||||
return self.createResource(file, url)
|
||||
|
||||
def _raise_open_error(self, url, message):
|
||||
if url[:7].lower() == "file://":
|
||||
what = "file"
|
||||
ident = urllib2.url2pathname(url[7:])
|
||||
else:
|
||||
what = "URL"
|
||||
ident = url
|
||||
error = ZConfig.ConfigurationError(
|
||||
"error opening %s %s: %s" % (what, ident, message),
|
||||
url)
|
||||
raise_with_same_tb(error)
|
||||
|
||||
def normalizeURL(self, url):
|
||||
"""Return a URL for *url*
|
||||
|
||||
If *url* refers to an existing file, the corresponding
|
||||
``file:`` URL is returned. Otherwise *url* is checked
|
||||
for sanity: if it does not have a schema, :exc:`ValueError` is
|
||||
raised, and if it does have a fragment identifier,
|
||||
:exc:`~.ConfigurationError` is raised.
|
||||
|
||||
This uses :meth:`isPath` to determine whether *url* is
|
||||
a URL of a filesystem path.
|
||||
"""
|
||||
if self.isPath(url):
|
||||
url = "file://" + pathname2url(os.path.abspath(url))
|
||||
newurl, fragment = ZConfig.url.urldefrag(url)
|
||||
if fragment:
|
||||
raise ZConfig.ConfigurationError(
|
||||
"fragment identifiers are not supported",
|
||||
url)
|
||||
return newurl
|
||||
|
||||
# from RFC 3986:
|
||||
# schema = ALPHA *( ALPHA / DIGIT / "+" / "-" / "." )
|
||||
_pathsep_rx = re.compile(r"[a-zA-Z][-+.a-zA-Z0-9]*:")
|
||||
|
||||
def isPath(self, s):
|
||||
"""Return true if *s* should be considered a filesystem path rather
|
||||
than a URL.
|
||||
"""
|
||||
if ":" in s:
|
||||
# XXX This assumes that one-character scheme identifiers
|
||||
# are always Windows drive letters; I don't know of any
|
||||
# one-character scheme identifiers.
|
||||
m = self._pathsep_rx.match(s)
|
||||
if m is None:
|
||||
return True
|
||||
# Does it look like a drive letter?
|
||||
return len(m.group(0)) == 2
|
||||
else:
|
||||
return True
|
||||
|
||||
|
||||
def openPackageResource(package, path):
|
||||
__import__(package)
|
||||
pkg = sys.modules[package]
|
||||
try:
|
||||
loader = pkg.__loader__
|
||||
except AttributeError:
|
||||
relpath = os.path.join(*path.split("/"))
|
||||
for dirname in pkg.__path__:
|
||||
filename = os.path.join(dirname, relpath)
|
||||
if os.path.exists(filename):
|
||||
break
|
||||
else:
|
||||
raise ZConfig.SchemaResourceError("schema component not found",
|
||||
filename=path,
|
||||
package=package,
|
||||
path=pkg.__path__)
|
||||
url = "file:" + pathname2url(filename)
|
||||
url = ZConfig.url.urlnormalize(url)
|
||||
return urllib2.urlopen(url)
|
||||
else:
|
||||
v, tb = (None, None)
|
||||
for dirname in pkg.__path__:
|
||||
loadpath = os.path.join(dirname, path)
|
||||
try:
|
||||
return StringIO(
|
||||
loader.get_data(loadpath).decode('utf-8'))
|
||||
except Exception as e:
|
||||
v = ZConfig.SchemaResourceError(
|
||||
"error opening schema component: " + repr(e),
|
||||
filename=path,
|
||||
package=package,
|
||||
path=pkg.__path__)
|
||||
tb = sys.exc_info()[2]
|
||||
|
||||
if v is not None:
|
||||
try:
|
||||
reraise(type(v), v, tb)
|
||||
finally:
|
||||
del tb
|
||||
|
||||
raise ZConfig.SchemaResourceError("schema component not found",
|
||||
filename=path,
|
||||
package=package,
|
||||
path=pkg.__path__)
|
||||
|
||||
|
||||
def _url_from_file(file_or_path):
|
||||
name = getattr(file_or_path, "name", None)
|
||||
if name and name[0] != "<" and name[-1] != ">":
|
||||
return "file://" + pathname2url(os.path.abspath(name))
|
||||
|
||||
|
||||
class SchemaLoader(BaseLoader):
|
||||
""" Loader that loads schema instances.
|
||||
|
||||
All schema loaded by a :class:`SchemaLoader` will use the same
|
||||
data type registry. If *registry* is provided and not ``None``, it
|
||||
will be used, otherwise an instance of
|
||||
:class:`ZConfig.datatypes.Registry` will be used.
|
||||
"""
|
||||
|
||||
def __init__(self, registry=None):
|
||||
if registry is None:
|
||||
registry = ZConfig.datatypes.Registry()
|
||||
BaseLoader.__init__(self)
|
||||
self.registry = registry
|
||||
self._cache = {}
|
||||
|
||||
def loadResource(self, resource):
|
||||
if resource.url and resource.url in self._cache:
|
||||
schema = self._cache[resource.url]
|
||||
else:
|
||||
schema = ZConfig.schema.parseResource(resource, self)
|
||||
self._cache[resource.url] = schema
|
||||
return schema
|
||||
|
||||
# schema parser support API
|
||||
|
||||
def schemaComponentSource(self, package, filename):
|
||||
parts = package.split(".")
|
||||
if not parts: # pragma: no cover. can we even get here?
|
||||
raise ZConfig.SchemaError(
|
||||
"illegal schema component name: " + repr(package))
|
||||
if "" in parts:
|
||||
# '' somewhere in the package spec; still illegal
|
||||
raise ZConfig.SchemaError(
|
||||
"illegal schema component name: " + repr(package))
|
||||
filename = filename or "component.xml"
|
||||
try:
|
||||
__import__(package)
|
||||
except ImportError as e:
|
||||
raise ZConfig.SchemaResourceError(
|
||||
"could not load package %s: %s" % (package, str(e)),
|
||||
filename=filename,
|
||||
package=package)
|
||||
pkg = sys.modules[package]
|
||||
if not hasattr(pkg, "__path__"):
|
||||
raise ZConfig.SchemaResourceError(
|
||||
"import name does not refer to a package",
|
||||
filename=filename, package=package)
|
||||
return "package:%s:%s" % (package, filename)
|
||||
|
||||
|
||||
class ConfigLoader(BaseLoader):
|
||||
"""Loader for configuration files.
|
||||
|
||||
Each configuration file must
|
||||
conform to the schema *schema*. The ``load*()`` methods
|
||||
return a tuple consisting of the configuration object and a
|
||||
composite handler.
|
||||
"""
|
||||
|
||||
|
||||
def __init__(self, schema):
|
||||
if schema.isabstract():
|
||||
raise ZConfig.SchemaError(
|
||||
"cannot check a configuration an abstract type")
|
||||
BaseLoader.__init__(self)
|
||||
self.schema = schema
|
||||
self._private_schema = False
|
||||
|
||||
def loadResource(self, resource):
|
||||
sm = self.createSchemaMatcher()
|
||||
self._parse_resource(sm, resource)
|
||||
result = sm.finish(), CompositeHandler(sm.handlers, self.schema)
|
||||
return result
|
||||
|
||||
def createSchemaMatcher(self):
|
||||
return ZConfig.matcher.SchemaMatcher(self.schema)
|
||||
|
||||
# config parser support API
|
||||
|
||||
def startSection(self, parent, type_, name):
|
||||
t = self.schema.gettype(type_)
|
||||
if t.isabstract():
|
||||
raise ZConfig.ConfigurationError(
|
||||
"concrete sections cannot match abstract section types;"
|
||||
" found abstract type " + repr(type_))
|
||||
return parent.createChildMatcher(t, name)
|
||||
|
||||
def endSection(self, parent, type_, name, matcher):
|
||||
sectvalue = matcher.finish()
|
||||
parent.addSection(type_, name, sectvalue)
|
||||
|
||||
def importSchemaComponent(self, pkgname):
|
||||
schema = self.schema
|
||||
if not self._private_schema:
|
||||
# replace the schema with an extended schema on the first %import
|
||||
self._loader = SchemaLoader(self.schema.registry)
|
||||
schema = ZConfig.info.createDerivedSchema(self.schema)
|
||||
self._private_schema = True
|
||||
self.schema = schema
|
||||
url = self._loader.schemaComponentSource(pkgname, '')
|
||||
if schema.hasComponent(url):
|
||||
return
|
||||
resource = self.openResource(url)
|
||||
schema.addComponent(url)
|
||||
try:
|
||||
ZConfig.schema.parseComponent(resource, self._loader, schema)
|
||||
finally:
|
||||
resource.close()
|
||||
|
||||
def includeConfiguration(self, section, url, defines):
|
||||
url = self.normalizeURL(url)
|
||||
r = self.openResource(url)
|
||||
try:
|
||||
self._parse_resource(section, r, defines)
|
||||
finally:
|
||||
r.close()
|
||||
|
||||
# internal helper
|
||||
|
||||
def _parse_resource(self, matcher, resource, defines=None):
|
||||
parser = ZConfig.cfgparser.ZConfigParser(resource, self, defines)
|
||||
parser.parse(matcher)
|
||||
|
||||
|
||||
class CompositeHandler(object):
|
||||
|
||||
def __init__(self, handlers, schema):
|
||||
self._handlers = handlers
|
||||
self._convert = schema.registry.get("basic-key")
|
||||
|
||||
def __call__(self, handlermap):
|
||||
d = {}
|
||||
for name, callback in handlermap.items():
|
||||
n = self._convert(name)
|
||||
if n in d:
|
||||
raise ZConfig.ConfigurationError(
|
||||
"handler name not unique when converted to a basic-key: "
|
||||
+ repr(name))
|
||||
d[n] = callback
|
||||
L = []
|
||||
for handler, value in self._handlers:
|
||||
if handler not in d:
|
||||
L.append(handler)
|
||||
if L:
|
||||
raise ZConfig.ConfigurationError(
|
||||
"undefined handlers: " + ", ".join(L))
|
||||
for handler, value in self._handlers:
|
||||
f = d[handler]
|
||||
if f is not None:
|
||||
f(value)
|
||||
|
||||
def __len__(self):
|
||||
return len(self._handlers)
|
||||
|
||||
|
||||
class Resource(object):
|
||||
"""Object that allows an open file object and a URL to be bound
|
||||
together to ease handling.
|
||||
|
||||
Instances have the attributes :attr:`file` and :attr:`url`, which
|
||||
store the constructor arguments. These objects also have a
|
||||
:meth:`close` method which will call :meth:`~file.close` on
|
||||
*file*, then set the :attr:`file` attribute to ``None`` and the
|
||||
:attr:`closed` attribute to ``True``.
|
||||
|
||||
All other attributes are delegated to *file*.
|
||||
"""
|
||||
|
||||
closed = False
|
||||
|
||||
def __init__(self, file, url):
|
||||
self.file = file
|
||||
self.url = url
|
||||
|
||||
def close(self):
|
||||
if self.file is not None:
|
||||
self.file.close()
|
||||
self.file = None
|
||||
self.closed = True
|
||||
|
||||
def __getattr__(self, name):
|
||||
return getattr(self.file, name)
|
||||
@@ -0,0 +1,305 @@
|
||||
##############################################################################
|
||||
#
|
||||
# Copyright (c) 2002, 2003 Zope Foundation and Contributors.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# This software is subject to the provisions of the Zope Public License,
|
||||
# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
|
||||
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
|
||||
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
|
||||
# FOR A PARTICULAR PURPOSE.
|
||||
#
|
||||
##############################################################################
|
||||
"""Utility that manages the binding of configuration data to a section."""
|
||||
|
||||
import ZConfig
|
||||
|
||||
from ZConfig.info import ValueInfo
|
||||
from ZConfig._compat import raise_with_same_tb
|
||||
|
||||
|
||||
class BaseMatcher(object):
|
||||
def __init__(self, info, type_, handlers):
|
||||
self.info = info
|
||||
self.type = type_
|
||||
self._values = {}
|
||||
for _type_key, type_info in type_:
|
||||
if type_info.name == "+" and not type_info.issection():
|
||||
v = {}
|
||||
elif type_info.ismulti():
|
||||
v = []
|
||||
else:
|
||||
v = None
|
||||
assert type_info.attribute is not None
|
||||
self._values[type_info.attribute] = v
|
||||
self._sectionnames = {}
|
||||
self.handlers = handlers if handlers is not None else []
|
||||
|
||||
def __repr__(self):
|
||||
clsname = self.__class__.__name__
|
||||
extra = "type " + repr(self.type.name)
|
||||
return "<%s for %s>" % (clsname, extra)
|
||||
|
||||
def addSection(self, type_, name, sectvalue):
|
||||
if name:
|
||||
if name in self._sectionnames:
|
||||
raise ZConfig.ConfigurationError(
|
||||
"section names must not be re-used within the"
|
||||
" same container:" + repr(name))
|
||||
self._sectionnames[name] = name
|
||||
ci = self.type.getsectioninfo(type_, name)
|
||||
attr = ci.attribute
|
||||
v = self._values[attr]
|
||||
if ci.ismulti():
|
||||
v.append(sectvalue)
|
||||
elif v is None:
|
||||
self._values[attr] = sectvalue
|
||||
else: # pragma: no cover
|
||||
raise ZConfig.ConfigurationError(
|
||||
"too many instances of %s section" % repr(ci.sectiontype.name))
|
||||
|
||||
def addValue(self, key, value, position):
|
||||
try:
|
||||
realkey = self.type.keytype(key)
|
||||
except ValueError as e:
|
||||
raise ZConfig.DataConversionError(e, key, position)
|
||||
arbkey_info = None
|
||||
for i in range(len(self.type)):
|
||||
k, ci = self.type[i]
|
||||
if k == realkey:
|
||||
break
|
||||
if ci.name == "+" and not ci.issection():
|
||||
arbkey_info = k, ci
|
||||
else:
|
||||
if arbkey_info is None:
|
||||
raise ZConfig.ConfigurationError(
|
||||
repr(key) + " is not a known key name")
|
||||
k, ci = arbkey_info
|
||||
if ci.issection(): # pragma: no cover
|
||||
if ci.name:
|
||||
extra = " in %s sections" % repr(self.type.name)
|
||||
else:
|
||||
extra = ""
|
||||
raise ZConfig.ConfigurationError(
|
||||
"%s is not a valid key name%s" % (repr(key), extra))
|
||||
|
||||
ismulti = ci.ismulti()
|
||||
attr = ci.attribute
|
||||
assert attr is not None
|
||||
v = self._values[attr]
|
||||
if v is None:
|
||||
if k == '+':
|
||||
v = {} # pragma: no cover
|
||||
elif ismulti:
|
||||
v = [] # pragma: no cover
|
||||
self._values[attr] = v
|
||||
elif not ismulti:
|
||||
if k != '+':
|
||||
raise ZConfig.ConfigurationError(
|
||||
repr(key) + " does not support multiple values")
|
||||
elif len(v) == ci.maxOccurs: # pragma: no cover
|
||||
# This code may be impossible to hit. Previously it would
|
||||
# have raised a NameError because it used an unbound
|
||||
# local.
|
||||
raise ZConfig.ConfigurationError(
|
||||
"too many values for " + repr(ci))
|
||||
|
||||
value = ValueInfo(value, position)
|
||||
if k == '+':
|
||||
if ismulti:
|
||||
if realkey in v:
|
||||
v[realkey].append(value)
|
||||
else:
|
||||
v[realkey] = [value]
|
||||
else:
|
||||
if realkey in v: # pragma: no cover
|
||||
raise ZConfig.ConfigurationError(
|
||||
"too many values for " + repr(key))
|
||||
v[realkey] = value
|
||||
elif ismulti:
|
||||
v.append(value)
|
||||
else:
|
||||
self._values[attr] = value
|
||||
|
||||
def createChildMatcher(self, type_, name):
|
||||
ci = self.type.getsectioninfo(type_.name, name)
|
||||
assert not ci.isabstract()
|
||||
if not ci.isAllowedName(name):
|
||||
raise ZConfig.ConfigurationError(
|
||||
"%s is not an allowed name for %s sections"
|
||||
% (repr(name), repr(ci.sectiontype.name)))
|
||||
return SectionMatcher(ci, type_, name, self.handlers)
|
||||
|
||||
def finish(self):
|
||||
"""Check the constraints of the section and convert to an application
|
||||
object."""
|
||||
values = self._values
|
||||
for key, ci in self.type:
|
||||
if key:
|
||||
key = repr(key)
|
||||
else:
|
||||
key = "section type " + repr(ci.sectiontype.name)
|
||||
assert ci.attribute is not None
|
||||
attr = ci.attribute
|
||||
v = values[attr]
|
||||
if ci.name == '+' and not ci.issection():
|
||||
# v is a dict
|
||||
if ci.minOccurs > len(v):
|
||||
raise ZConfig.ConfigurationError(
|
||||
"no keys defined for the %s key/value map; at least %d"
|
||||
" must be specified" % (attr, ci.minOccurs))
|
||||
if v is None and ci.minOccurs:
|
||||
default = ci.getdefault()
|
||||
if default is None:
|
||||
raise ZConfig.ConfigurationError(
|
||||
"no values for %s; %s required" % (key, ci.minOccurs))
|
||||
else:
|
||||
v = values[attr] = default[:] # pragma: no cover
|
||||
if ci.ismulti():
|
||||
if not v:
|
||||
default = ci.getdefault()
|
||||
if isinstance(default, dict):
|
||||
v.update(default)
|
||||
else:
|
||||
v[:] = default
|
||||
if len(v) < ci.minOccurs:
|
||||
raise ZConfig.ConfigurationError(
|
||||
"not enough values for %s; %d found, %d required"
|
||||
% (key, len(v), ci.minOccurs))
|
||||
if v is None and not ci.issection():
|
||||
if ci.ismulti():
|
||||
v = ci.getdefault()[:] # pragma: no cover
|
||||
else:
|
||||
v = ci.getdefault()
|
||||
values[attr] = v
|
||||
return self.constuct()
|
||||
|
||||
def constuct(self):
|
||||
values = self._values
|
||||
for name, ci in self.type:
|
||||
assert ci.attribute is not None
|
||||
attr = ci.attribute
|
||||
if ci.ismulti():
|
||||
if ci.issection():
|
||||
v = []
|
||||
for s in values[attr]:
|
||||
if s is not None:
|
||||
st = s.getSectionDefinition()
|
||||
try:
|
||||
s = st.datatype(s)
|
||||
except ValueError as e:
|
||||
raise_with_same_tb(ZConfig.DataConversionError(
|
||||
e, s, (-1, -1, None)))
|
||||
|
||||
v.append(s)
|
||||
elif ci.name == '+':
|
||||
v = values[attr]
|
||||
for key, val in v.items():
|
||||
v[key] = [vi.convert(ci.datatype) for vi in val]
|
||||
else:
|
||||
v = [vi.convert(ci.datatype) for vi in values[attr]]
|
||||
elif ci.issection():
|
||||
if values[attr] is not None:
|
||||
st = values[attr].getSectionDefinition()
|
||||
try:
|
||||
v = st.datatype(values[attr])
|
||||
except ValueError as e:
|
||||
raise_with_same_tb(ZConfig.DataConversionError(
|
||||
e, values[attr], (-1, -1, None)))
|
||||
|
||||
else:
|
||||
v = None
|
||||
elif name == '+':
|
||||
v = values[attr]
|
||||
if not v:
|
||||
for key, val in ci.getdefault().items():
|
||||
v[key] = val.convert(ci.datatype)
|
||||
else:
|
||||
for key, val in v.items():
|
||||
v[key] = val.convert(ci.datatype)
|
||||
else:
|
||||
v = values[attr]
|
||||
if v is not None:
|
||||
v = v.convert(ci.datatype)
|
||||
values[attr] = v
|
||||
if ci.handler is not None:
|
||||
self.handlers.append((ci.handler, v))
|
||||
return self.createValue()
|
||||
|
||||
def createValue(self):
|
||||
return SectionValue(self._values, None, self)
|
||||
|
||||
|
||||
class SectionMatcher(BaseMatcher):
|
||||
def __init__(self, info, type_, name, handlers):
|
||||
if name or info.allowUnnamed():
|
||||
self.name = name
|
||||
else:
|
||||
raise ZConfig.ConfigurationError(
|
||||
repr(type_.name) + " sections may not be unnamed")
|
||||
BaseMatcher.__init__(self, info, type_, handlers)
|
||||
|
||||
def createValue(self):
|
||||
return SectionValue(self._values, self.name, self)
|
||||
|
||||
|
||||
class SchemaMatcher(BaseMatcher):
|
||||
def __init__(self, schema):
|
||||
BaseMatcher.__init__(self, schema, schema, [])
|
||||
|
||||
def finish(self):
|
||||
# Since there's no outer container to call datatype()
|
||||
# for the schema, we convert on the way out.
|
||||
v = BaseMatcher.finish(self)
|
||||
v = self.type.datatype(v)
|
||||
if self.type.handler is not None:
|
||||
self.handlers.append((self.type.handler, v))
|
||||
return v
|
||||
|
||||
|
||||
class SectionValue(object):
|
||||
"""Generic 'bag-of-values' object for a section.
|
||||
|
||||
Derived classes should always call the SectionValue constructor
|
||||
before attempting to modify self.
|
||||
"""
|
||||
|
||||
def __init__(self, values, name, matcher):
|
||||
self.__dict__.update(values)
|
||||
self._name = name
|
||||
self._matcher = matcher
|
||||
self._attributes = tuple(values.keys())
|
||||
|
||||
def __repr__(self):
|
||||
if self._name:
|
||||
# probably unique for a given config file; more readable than id()
|
||||
name = repr(self._name)
|
||||
else:
|
||||
# identify uniquely
|
||||
name = "at %#x" % id(self)
|
||||
clsname = self.__class__.__name__
|
||||
return "<%s for %s %s>" % (clsname, self._matcher.type.name, name)
|
||||
|
||||
def __str__(self):
|
||||
l = []
|
||||
attrnames = sorted([s for s in self.__dict__ if s[0] != "_"])
|
||||
for k in attrnames:
|
||||
v = getattr(self, k)
|
||||
l.append('%-40s: %s' % (k, v))
|
||||
return '\n'.join(l)
|
||||
|
||||
def getSectionName(self):
|
||||
return self._name
|
||||
|
||||
def getSectionType(self):
|
||||
return self._matcher.type.name
|
||||
|
||||
def getSectionDefinition(self):
|
||||
return self._matcher.type
|
||||
|
||||
def getSectionMatcher(self):
|
||||
return self._matcher
|
||||
|
||||
def getSectionAttributes(self):
|
||||
return self._attributes
|
||||
@@ -0,0 +1,606 @@
|
||||
##############################################################################
|
||||
#
|
||||
# Copyright (c) 2002, 2003 Zope Foundation and Contributors.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# This software is subject to the provisions of the Zope Public License,
|
||||
# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
|
||||
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
|
||||
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
|
||||
# FOR A PARTICULAR PURPOSE.
|
||||
#
|
||||
##############################################################################
|
||||
"""Parser for ZConfig schemas."""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import xml.sax
|
||||
|
||||
import ZConfig
|
||||
|
||||
from ZConfig import info
|
||||
from ZConfig import url
|
||||
|
||||
from ZConfig._compat import raise_with_same_tb
|
||||
|
||||
BLANK = u''
|
||||
|
||||
def parseResource(resource, loader):
|
||||
parser = SchemaParser(loader, resource.url)
|
||||
xml.sax.parse(resource.file, parser)
|
||||
return parser._schema
|
||||
|
||||
|
||||
def parseComponent(resource, loader, schema):
|
||||
parser = ComponentParser(loader, resource.url, schema)
|
||||
xml.sax.parse(resource.file, parser)
|
||||
|
||||
|
||||
def _srepr(ob):
|
||||
if isinstance(ob, type(BLANK)) and sys.version_info[0] < 3:
|
||||
# drop the leading "u" from a unicode repr
|
||||
return repr(ob)[1:]
|
||||
return repr(ob)
|
||||
|
||||
|
||||
class BaseParser(xml.sax.ContentHandler):
|
||||
|
||||
_cdata_tags = "description", "metadefault", "example", "default"
|
||||
_handled_tags = ("import", "abstracttype", "sectiontype",
|
||||
"key", "multikey", "section", "multisection")
|
||||
|
||||
_allowed_parents = {
|
||||
"description": ["key", "section", "multikey", "multisection",
|
||||
"sectiontype", "abstracttype",
|
||||
"schema", "component"],
|
||||
"example": ["schema", "sectiontype", "key", "multikey", "section", "multisection"],
|
||||
"metadefault": ["key", "section", "multikey", "multisection"],
|
||||
"default": ["key", "multikey"],
|
||||
"import": ["schema", "component"],
|
||||
"abstracttype": ["schema", "component"],
|
||||
"sectiontype": ["schema", "component"],
|
||||
"key": ["schema", "sectiontype"],
|
||||
"multikey": ["schema", "sectiontype"],
|
||||
"section": ["schema", "sectiontype"],
|
||||
"multisection": ["schema", "sectiontype"],
|
||||
}
|
||||
|
||||
def __init__(self, loader, url):
|
||||
self._registry = loader.registry
|
||||
self._loader = loader
|
||||
self._basic_key = self._registry.get("basic-key")
|
||||
self._identifier = self._registry.get("identifier")
|
||||
self._cdata = None
|
||||
self._locator = None
|
||||
self._prefixes = []
|
||||
self._schema = None
|
||||
self._stack = []
|
||||
self._url = url
|
||||
self._elem_stack = []
|
||||
|
||||
# SAX 2 ContentHandler methods
|
||||
|
||||
def setDocumentLocator(self, locator):
|
||||
self._locator = locator
|
||||
|
||||
def startElement(self, name, attrs):
|
||||
attrs = dict(attrs)
|
||||
if self._elem_stack:
|
||||
parent = self._elem_stack[-1]
|
||||
if name not in self._allowed_parents:
|
||||
self.error("Unknown tag " + name)
|
||||
if parent not in self._allowed_parents[name]:
|
||||
self.error("%s elements may not be nested in %s elements"
|
||||
% (_srepr(name), _srepr(parent)))
|
||||
elif name != self._top_level:
|
||||
self.error("Unknown document type " + name)
|
||||
|
||||
self._elem_stack.append(name)
|
||||
|
||||
# self._schema is assigned to in self.start_<_top_level>, so
|
||||
# most of the checks for it being None are just extra precaution.
|
||||
if name == self._top_level:
|
||||
if self._schema is not None: # pragma: no cover
|
||||
self.error("schema element improperly nested")
|
||||
getattr(self, "start_" + name)(attrs)
|
||||
elif name in self._handled_tags:
|
||||
if self._schema is None: # pragma: no cover
|
||||
self.error(name + " element outside of schema")
|
||||
getattr(self, "start_" + name)(attrs)
|
||||
elif name in self._cdata_tags:
|
||||
if self._schema is None: # pragma: no cover
|
||||
self.error(name + " element outside of schema")
|
||||
if self._cdata is not None: # pragma: no cover
|
||||
# this should be handled by the earlier nesting check
|
||||
self.error(name + " element improperly nested")
|
||||
self._cdata = []
|
||||
self._position = None
|
||||
self._attrs = attrs
|
||||
|
||||
def characters(self, data):
|
||||
if self._cdata is not None:
|
||||
if self._position is None:
|
||||
self._position = self.get_position()
|
||||
self._cdata.append(data)
|
||||
elif data.strip():
|
||||
self.error("unexpected non-blank character data: "
|
||||
+ repr(data.strip()))
|
||||
|
||||
def endElement(self, name):
|
||||
del self._elem_stack[-1]
|
||||
if name in self._handled_tags:
|
||||
getattr(self, "end_" + name)()
|
||||
else:
|
||||
data = ''.join(self._cdata).strip()
|
||||
self._cdata = None
|
||||
getattr(self, "characters_" + name)(data)
|
||||
|
||||
def endDocument(self):
|
||||
if self._schema is None: # pragma: no cover
|
||||
# this would have to be a broken subclass
|
||||
self.error("no %s found" % self._top_level)
|
||||
|
||||
# helper methods
|
||||
|
||||
def get_position(self):
|
||||
if self._locator:
|
||||
return (self._locator.getLineNumber(),
|
||||
self._locator.getColumnNumber(),
|
||||
(self._locator.getSystemId() or self._url))
|
||||
return None, None, self._url # pragma: no cover
|
||||
|
||||
def get_handler(self, attrs):
|
||||
v = attrs.get("handler")
|
||||
if v is None:
|
||||
return v
|
||||
return self.basic_key(v)
|
||||
|
||||
def push_prefix(self, attrs):
|
||||
name = attrs.get("prefix")
|
||||
if name:
|
||||
if self._prefixes:
|
||||
convert = self._registry.get("dotted-suffix")
|
||||
else:
|
||||
convert = self._registry.get("dotted-name")
|
||||
try:
|
||||
name = convert(name)
|
||||
except ValueError as err:
|
||||
self.error("not a valid prefix: %s (%s)"
|
||||
% (_srepr(name), str(err)))
|
||||
if name[0] == ".":
|
||||
prefix = self._prefixes[-1] + name
|
||||
else:
|
||||
prefix = name
|
||||
elif self._prefixes:
|
||||
prefix = self._prefixes[-1]
|
||||
else:
|
||||
prefix = ''
|
||||
self._prefixes.append(prefix)
|
||||
|
||||
def pop_prefix(self):
|
||||
del self._prefixes[-1]
|
||||
|
||||
def get_classname(self, name):
|
||||
name = str(name)
|
||||
if name.startswith("."):
|
||||
return self._prefixes[-1] + name
|
||||
return name
|
||||
|
||||
def get_datatype(self, attrs, attrkey, default, base=None):
|
||||
if attrkey in attrs:
|
||||
dtname = self.get_classname(attrs[attrkey])
|
||||
else:
|
||||
convert = getattr(base, attrkey, None)
|
||||
if convert is not None:
|
||||
return convert
|
||||
dtname = default
|
||||
|
||||
try:
|
||||
return self._registry.get(dtname)
|
||||
except ValueError as e:
|
||||
self.error(e.args[0])
|
||||
|
||||
def get_sect_typeinfo(self, attrs, base=None):
|
||||
keytype = self.get_datatype(attrs, "keytype", "basic-key", base)
|
||||
valuetype = self.get_datatype(attrs, "valuetype", "string")
|
||||
datatype = self.get_datatype(attrs, "datatype", "null", base)
|
||||
return keytype, valuetype, datatype
|
||||
|
||||
def get_required(self, attrs):
|
||||
if "required" in attrs:
|
||||
v = attrs["required"]
|
||||
if v == "yes":
|
||||
return True
|
||||
elif v == "no":
|
||||
return False
|
||||
self.error("value for 'required' must be 'yes' or 'no'")
|
||||
else:
|
||||
return False
|
||||
|
||||
def get_ordinality(self, attrs):
|
||||
# used by start_multi*()
|
||||
minOccurs, maxOccurs = 0, info.Unbounded
|
||||
if self.get_required(attrs):
|
||||
minOccurs = 1
|
||||
return minOccurs, maxOccurs
|
||||
|
||||
def get_sectiontype(self, attrs):
|
||||
type_name = attrs.get("type")
|
||||
if not type_name:
|
||||
self.error("section must specify type")
|
||||
return self._schema.gettype(type_name)
|
||||
|
||||
def get_key_info(self, attrs, element):
|
||||
any_name, name, attribute = self.get_name_info(attrs, element)
|
||||
if any_name == '*':
|
||||
self.error(element + " may not specify '*' for name")
|
||||
if not name and any_name != '+': # pragma: no cover
|
||||
# Can we even get here?
|
||||
self.error(element + " name may not be omitted or empty")
|
||||
datatype = self.get_datatype(attrs, "datatype", "string")
|
||||
handler = self.get_handler(attrs)
|
||||
return name or any_name, datatype, handler, attribute
|
||||
|
||||
def get_name_info(self, attrs, element, default=None):
|
||||
name = attrs.get("name", default)
|
||||
if not name:
|
||||
self.error(element + " name must be specified and non-empty")
|
||||
aname = attrs.get("attribute")
|
||||
if aname:
|
||||
aname = self.identifier(aname)
|
||||
if aname.startswith("getSection"):
|
||||
# reserved; used for SectionValue methods to get meta-info
|
||||
self.error("attribute names may not start with 'getSection'")
|
||||
if name in ("*", "+"):
|
||||
if not aname:
|
||||
self.error(
|
||||
"container attribute must be specified and non-empty"
|
||||
" when using '*' or '+' for a section name")
|
||||
return name, None, aname
|
||||
else:
|
||||
# run the keytype converter to make sure this is a valid key
|
||||
try:
|
||||
name = self._stack[-1].keytype(name)
|
||||
except ValueError as e:
|
||||
self.error("could not convert key name to keytype: " + str(e))
|
||||
if not aname:
|
||||
aname = self.basic_key(name)
|
||||
aname = self.identifier(aname.replace('-', '_'))
|
||||
return None, name, aname
|
||||
|
||||
# schema loading logic
|
||||
|
||||
def characters_default(self, data):
|
||||
key = self._attrs.get("key")
|
||||
self._stack[-1].adddefault(data, self._position, key)
|
||||
|
||||
def characters_description(self, data):
|
||||
if self._stack[-1].description is not None:
|
||||
self.error(
|
||||
"at most one <description> may be used for each element")
|
||||
self._stack[-1].description = data
|
||||
|
||||
def characters_example(self, data):
|
||||
if self._stack[-1].example is not None:
|
||||
self.error(
|
||||
"at most one <example> may be used for each element")
|
||||
self._stack[-1].example = data
|
||||
|
||||
def characters_metadefault(self, data):
|
||||
self._stack[-1].metadefault = data
|
||||
|
||||
def start_import(self, attrs):
|
||||
src = attrs.get("src", "").strip()
|
||||
pkg = attrs.get("package", "").strip()
|
||||
filename = attrs.get("file", "").strip()
|
||||
if not (src or pkg):
|
||||
self.error("import must specify either src or package")
|
||||
if src and pkg:
|
||||
self.error("import may only specify one of src or package")
|
||||
if src:
|
||||
if filename:
|
||||
self.error("import may not specify file and src")
|
||||
src = url.urljoin(self._url, src)
|
||||
src, fragment = url.urldefrag(src)
|
||||
if fragment:
|
||||
self.error("import src may not include"
|
||||
" a fragment identifier")
|
||||
schema = self._loader.loadURL(src)
|
||||
for n in schema.gettypenames():
|
||||
self._schema.addtype(schema.gettype(n))
|
||||
else:
|
||||
if os.path.dirname(filename):
|
||||
self.error("file may not include a directory part")
|
||||
pkg = self.get_classname(pkg)
|
||||
src = self._loader.schemaComponentSource(pkg, filename)
|
||||
if not self._schema.hasComponent(src):
|
||||
self._schema.addComponent(src)
|
||||
self.loadComponent(src)
|
||||
|
||||
def loadComponent(self, src):
|
||||
r = self._loader.openResource(src)
|
||||
parser = ComponentParser(self._loader, src, self._schema)
|
||||
try:
|
||||
xml.sax.parse(r.file, parser)
|
||||
finally:
|
||||
r.close()
|
||||
|
||||
def end_import(self):
|
||||
pass
|
||||
|
||||
def start_sectiontype(self, attrs):
|
||||
name = attrs.get("name")
|
||||
if not name:
|
||||
self.error("sectiontype name must not be omitted or empty")
|
||||
name = self.basic_key(name)
|
||||
self.push_prefix(attrs)
|
||||
if "extends" in attrs:
|
||||
basename = self.basic_key(attrs["extends"])
|
||||
base = self._schema.gettype(basename)
|
||||
if base.isabstract():
|
||||
self.error("sectiontype cannot extend an abstract type")
|
||||
keytype, valuetype, datatype = self.get_sect_typeinfo(attrs, base)
|
||||
sectinfo = self._schema.deriveSectionType(
|
||||
base, name, keytype, valuetype, datatype)
|
||||
else:
|
||||
keytype, valuetype, datatype = self.get_sect_typeinfo(attrs)
|
||||
sectinfo = self._schema.createSectionType(
|
||||
name, keytype, valuetype, datatype)
|
||||
if "implements" in attrs:
|
||||
ifname = self.basic_key(attrs["implements"])
|
||||
interface = self._schema.gettype(ifname)
|
||||
if not interface.isabstract():
|
||||
self.error(
|
||||
"type specified by implements is not an abstracttype")
|
||||
interface.addsubtype(sectinfo)
|
||||
self._stack.append(sectinfo)
|
||||
|
||||
def end_sectiontype(self):
|
||||
self.pop_prefix()
|
||||
self._stack.pop()
|
||||
|
||||
def start_section(self, attrs):
|
||||
sectiontype = self.get_sectiontype(attrs)
|
||||
handler = self.get_handler(attrs)
|
||||
minOccurs = 1 if self.get_required(attrs) else 0
|
||||
any_name, name, attribute = self.get_name_info(attrs, "section", "*")
|
||||
if any_name and not attribute: # pragma: no cover
|
||||
# It seems like this is handled by get_name_info.
|
||||
self.error(
|
||||
"attribute must be specified if section name is '*' or '+'")
|
||||
section = info.SectionInfo(any_name or name, sectiontype,
|
||||
minOccurs, 1, handler, attribute)
|
||||
self._stack[-1].addsection(name, section)
|
||||
self._stack.append(section)
|
||||
|
||||
def end_section(self):
|
||||
self._stack.pop()
|
||||
|
||||
def start_multisection(self, attrs):
|
||||
sectiontype = self.get_sectiontype(attrs)
|
||||
minOccurs, maxOccurs = self.get_ordinality(attrs)
|
||||
any_name, name, attribute = self.get_name_info(attrs, "multisection", "*")
|
||||
if any_name not in ("*", "+"):
|
||||
self.error("multisection must specify '*' or '+' for the name")
|
||||
handler = self.get_handler(attrs)
|
||||
section = info.SectionInfo(any_name or name, sectiontype,
|
||||
minOccurs, maxOccurs, handler, attribute)
|
||||
self._stack[-1].addsection(name, section)
|
||||
self._stack.append(section)
|
||||
|
||||
def end_multisection(self):
|
||||
self._stack.pop()
|
||||
|
||||
def start_abstracttype(self, attrs):
|
||||
name = attrs.get("name")
|
||||
if not name:
|
||||
self.error("abstracttype name must not be omitted or empty")
|
||||
name = self.basic_key(name)
|
||||
abstype = info.AbstractType(name)
|
||||
self._schema.addtype(abstype)
|
||||
self._stack.append(abstype)
|
||||
|
||||
def end_abstracttype(self):
|
||||
self._stack.pop()
|
||||
|
||||
def start_key(self, attrs):
|
||||
name, datatype, handler, attribute = self.get_key_info(attrs, "key")
|
||||
minOccurs = 1 if self.get_required(attrs) else 0
|
||||
key = info.KeyInfo(name, datatype, minOccurs, handler, attribute)
|
||||
if "default" in attrs:
|
||||
if minOccurs:
|
||||
self.error("required key cannot have a default value")
|
||||
key.adddefault(str(attrs["default"]).strip(),
|
||||
self.get_position())
|
||||
if name != "+":
|
||||
key.finish()
|
||||
self._stack[-1].addkey(key)
|
||||
self._stack.append(key)
|
||||
|
||||
def end_key(self):
|
||||
key = self._stack.pop()
|
||||
if key.name == "+":
|
||||
key.computedefault(self._stack[-1].keytype)
|
||||
key.finish()
|
||||
|
||||
def start_multikey(self, attrs):
|
||||
if "default" in attrs:
|
||||
self.error("default values for multikey must be given using"
|
||||
" 'default' elements")
|
||||
name, datatype, handler, attribute = self.get_key_info(attrs,
|
||||
"multikey")
|
||||
minOccurs, maxOccurs = self.get_ordinality(attrs)
|
||||
key = info.MultiKeyInfo(name, datatype, minOccurs, maxOccurs, handler, attribute)
|
||||
self._stack[-1].addkey(key)
|
||||
self._stack.append(key)
|
||||
|
||||
def end_multikey(self):
|
||||
multikey = self._stack.pop()
|
||||
if multikey.name == "+":
|
||||
multikey.computedefault(self._stack[-1].keytype)
|
||||
multikey.finish()
|
||||
|
||||
# datatype conversion wrappers
|
||||
|
||||
def basic_key(self, s):
|
||||
try:
|
||||
return self._basic_key(s)
|
||||
except ValueError as e:
|
||||
self.error(str(e))
|
||||
|
||||
def identifier(self, s):
|
||||
try:
|
||||
return self._identifier(s)
|
||||
except ValueError as e:
|
||||
self.error(str(e))
|
||||
|
||||
# exception setup helpers
|
||||
|
||||
def initerror(self, e):
|
||||
if self._locator is not None:
|
||||
e.colno = self._locator.getColumnNumber()
|
||||
e.lineno = self._locator.getLineNumber()
|
||||
e.url = self._locator.getSystemId()
|
||||
return e
|
||||
|
||||
def error(self, message):
|
||||
raise_with_same_tb(self.initerror(ZConfig.SchemaError(message)))
|
||||
|
||||
|
||||
class SchemaParser(BaseParser):
|
||||
|
||||
# needed by startElement() and endElement()
|
||||
_handled_tags = BaseParser._handled_tags + ("schema",)
|
||||
_top_level = "schema"
|
||||
|
||||
def __init__(self, loader, url, extending_parser=None):
|
||||
BaseParser.__init__(self, loader, url)
|
||||
self._extending_parser = extending_parser
|
||||
self._base_keytypes = []
|
||||
self._base_datatypes = []
|
||||
self._descriptions = []
|
||||
|
||||
def start_schema(self, attrs):
|
||||
self.push_prefix(attrs)
|
||||
handler = self.get_handler(attrs)
|
||||
keytype, valuetype, datatype = self.get_sect_typeinfo(attrs)
|
||||
|
||||
if self._extending_parser is None:
|
||||
# We're not being inherited, so we need to create the schema
|
||||
self._schema = info.SchemaType(keytype, valuetype, datatype,
|
||||
handler, self._url, self._registry)
|
||||
else:
|
||||
# Parse into the extending ("subclass") parser's schema
|
||||
self._schema = self._extending_parser._schema
|
||||
|
||||
self._stack = [self._schema]
|
||||
|
||||
if "extends" in attrs:
|
||||
sources = attrs["extends"].split()
|
||||
sources.reverse()
|
||||
|
||||
for src in sources:
|
||||
src = url.urljoin(self._url, src)
|
||||
src, fragment = url.urldefrag(src)
|
||||
if fragment:
|
||||
self.error("schema extends many not include"
|
||||
" a fragment identifier")
|
||||
self.extendSchema(src)
|
||||
|
||||
# Inherit keytype from bases, if unspecified and not conflicting
|
||||
if self._base_keytypes and "keytype" not in attrs:
|
||||
keytype = self._base_keytypes[0]
|
||||
for kt in self._base_keytypes[1:]:
|
||||
if kt is not keytype:
|
||||
self.error("base schemas have conflicting keytypes,"
|
||||
" but no keytype was specified in the"
|
||||
" extending schema")
|
||||
|
||||
# Inherit datatype from bases, if unspecified and not conflicting
|
||||
if self._base_datatypes and "datatype" not in attrs:
|
||||
datatype = self._base_datatypes[0]
|
||||
for dt in self._base_datatypes[1:]:
|
||||
if dt is not datatype:
|
||||
self.error("base schemas have conflicting datatypes,"
|
||||
" but no datatype was specified in the"
|
||||
" extending schema")
|
||||
|
||||
# Reset the schema types to our own, while we parse the schema body
|
||||
self._schema.keytype = keytype
|
||||
self._schema.valuetype = valuetype
|
||||
self._schema.datatype = datatype
|
||||
|
||||
# Update base key/datatypes for the "extending" parser
|
||||
if self._extending_parser is not None:
|
||||
self._extending_parser._base_keytypes.append(keytype)
|
||||
self._extending_parser._base_datatypes.append(datatype)
|
||||
|
||||
def extendSchema(self, src):
|
||||
parser = SchemaParser(self._loader, src, self)
|
||||
r = self._loader.openResource(src)
|
||||
try:
|
||||
xml.sax.parse(r.file, parser)
|
||||
finally:
|
||||
r.close()
|
||||
|
||||
def end_schema(self):
|
||||
del self._stack[-1]
|
||||
assert not self._stack
|
||||
self.pop_prefix()
|
||||
assert not self._prefixes
|
||||
schema = self._schema
|
||||
if self._extending_parser is None:
|
||||
# Top-level schema:
|
||||
if self._descriptions and not schema.description:
|
||||
# Use the last one, since the base schemas are processed in
|
||||
# reverse order.
|
||||
schema.description = self._descriptions[-1]
|
||||
elif schema.description:
|
||||
self._extending_parser._descriptions.append(schema.description)
|
||||
schema.description = None
|
||||
|
||||
|
||||
class ComponentParser(BaseParser):
|
||||
|
||||
_handled_tags = BaseParser._handled_tags + ("component",)
|
||||
_top_level = "component"
|
||||
|
||||
def __init__(self, loader, url, schema):
|
||||
BaseParser.__init__(self, loader, url)
|
||||
self._parent = schema
|
||||
|
||||
def characters_description(self, data):
|
||||
if self._stack:
|
||||
self._stack[-1].description = data
|
||||
|
||||
def start_key(self, attrs):
|
||||
self._check_not_toplevel("key")
|
||||
BaseParser.start_key(self, attrs)
|
||||
|
||||
def start_multikey(self, attrs):
|
||||
self._check_not_toplevel("multikey")
|
||||
BaseParser.start_multikey(self, attrs)
|
||||
|
||||
def start_section(self, attrs):
|
||||
self._check_not_toplevel("section")
|
||||
BaseParser.start_section(self, attrs)
|
||||
|
||||
def start_multisection(self, attrs):
|
||||
self._check_not_toplevel("multisection")
|
||||
BaseParser.start_multisection(self, attrs)
|
||||
|
||||
def start_component(self, attrs):
|
||||
self._schema = self._parent
|
||||
self.push_prefix(attrs)
|
||||
|
||||
def end_component(self):
|
||||
self.pop_prefix()
|
||||
|
||||
def _check_not_toplevel(self, what):
|
||||
if not self._stack: # pragma: no cover
|
||||
# we can't get here because the elements that call
|
||||
# this function have specified _allowed_parents that are
|
||||
# checked first
|
||||
self.error("cannot define top-level %s in a schema %s"
|
||||
% (what, self._top_level))
|
||||
@@ -0,0 +1,152 @@
|
||||
##############################################################################
|
||||
#
|
||||
# Copyright (c) 2003 Zope Corporation and Contributors.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# This software is subject to the provisions of the Zope Public License,
|
||||
# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
|
||||
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
|
||||
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
|
||||
# FOR A PARTICULAR PURPOSE.
|
||||
#
|
||||
##############################################################################
|
||||
from __future__ import print_function
|
||||
|
||||
import argparse
|
||||
from contextlib import contextmanager
|
||||
try:
|
||||
import html
|
||||
except ImportError:
|
||||
# Py2
|
||||
import cgi as html
|
||||
import sys
|
||||
|
||||
from ZConfig._schema_utils import AbstractSchemaPrinter
|
||||
from ZConfig._schema_utils import AbstractSchemaFormatter
|
||||
from ZConfig._schema_utils import MARKER
|
||||
from ZConfig._schema_utils import load_schema
|
||||
from ZConfig.sphinx import RstSchemaPrinter
|
||||
|
||||
class HtmlSchemaFormatter(AbstractSchemaFormatter):
|
||||
|
||||
def esc(self, x):
|
||||
return html.escape(str(x))
|
||||
|
||||
@contextmanager
|
||||
def _simple_tag(self, tag):
|
||||
self.write("<%s>" % tag)
|
||||
yield
|
||||
self.write("</%s>" % tag)
|
||||
|
||||
def item_list(self):
|
||||
return self._simple_tag("dl")
|
||||
|
||||
@contextmanager
|
||||
def describing(self, description=MARKER, after=None):
|
||||
with self._simple_tag("dt"):
|
||||
yield
|
||||
self._describing(description, after)
|
||||
|
||||
def described_as(self):
|
||||
return self._simple_tag("dd")
|
||||
|
||||
def abstract_name(self, name):
|
||||
self.write("<b><i>", name, "</b></i>")
|
||||
|
||||
def concrete_name(self, *name):
|
||||
self.write("<b>", *name)
|
||||
self.write("</b>")
|
||||
|
||||
def concrete_section_name(self, *name):
|
||||
name = ' '.join(name)
|
||||
self.write("<b>", self.esc("<%s>" % name), "</b>")
|
||||
|
||||
def datatype(self, datatype):
|
||||
self.write("(%s)" % self._dt(datatype))
|
||||
|
||||
def example(self, text):
|
||||
if not text:
|
||||
return
|
||||
|
||||
with self._simple_tag("p"):
|
||||
with self._simple_tag("i"):
|
||||
self.write("Example:")
|
||||
with self._simple_tag("pre"):
|
||||
self.write(self.esc(self._dedent(text)))
|
||||
|
||||
@contextmanager
|
||||
def body(self):
|
||||
self.write('''<html><body>
|
||||
<style>
|
||||
dl {margin: 0 0 1em 0;}
|
||||
</style>
|
||||
''')
|
||||
yield
|
||||
self.write('</body></html>')
|
||||
|
||||
class HtmlSchemaPrinter(AbstractSchemaPrinter):
|
||||
|
||||
_schema_formatter = HtmlSchemaFormatter
|
||||
|
||||
def main(argv=None):
|
||||
argv = argv if argv is not None else sys.argv[1:]
|
||||
|
||||
argparser = argparse.ArgumentParser(
|
||||
description="Print an HTML version of a schema")
|
||||
argparser.add_argument(
|
||||
"schema",
|
||||
metavar='[SCHEMA-OR-PACKAGE]',
|
||||
help="The schema to print. By default, a file. Optionally, a Python package."
|
||||
" If not given, defaults to reading a schema file from stdin",
|
||||
default="-"
|
||||
)
|
||||
argparser.add_argument(
|
||||
"--out", "-o",
|
||||
help="Write the schema to this file; if not given, write to stdout",
|
||||
type=argparse.FileType('w'))
|
||||
argparser.add_argument(
|
||||
"--package",
|
||||
action='store_true',
|
||||
default=False,
|
||||
help="The SCHEMA-OR-PACKAGE argument indicates a Python package instead of a file."
|
||||
" The component.xml (by default) from the package will be read.")
|
||||
argparser.add_argument(
|
||||
"--package-file",
|
||||
action="store",
|
||||
default="component.xml",
|
||||
help="When PACKAGE is given, this can specify the file inside it to load.")
|
||||
|
||||
argparser.add_argument(
|
||||
"--members",
|
||||
action="store",
|
||||
nargs="*",
|
||||
help="Only output sections and types in this list (and reachable from it)")
|
||||
|
||||
if RstSchemaPrinter:
|
||||
argparser.add_argument(
|
||||
"--format",
|
||||
action="store",
|
||||
choices=('html', 'xml'), # XXX Can we get actual valid RST out?
|
||||
default="HTML",
|
||||
help="What output format to produce"
|
||||
)
|
||||
|
||||
args = argparser.parse_args(argv)
|
||||
|
||||
out = args.out or sys.stdout
|
||||
|
||||
schema = load_schema(args.schema, args.package, args.package_file)
|
||||
|
||||
printer_factory = HtmlSchemaPrinter
|
||||
if hasattr(args, 'format') and args.format == 'xml':
|
||||
printer_factory = RstSchemaPrinter
|
||||
|
||||
|
||||
printer_factory(schema, out, allowed_names=args.members).printSchema()
|
||||
|
||||
|
||||
return 0
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,115 @@
|
||||
##############################################################################
|
||||
#
|
||||
# Copyright (c) 2007 Zope Foundation and Contributors.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# This software is subject to the provisions of the Zope Public License,
|
||||
# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
|
||||
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
|
||||
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
|
||||
# FOR A PARTICULAR PURPOSE.
|
||||
#
|
||||
##############################################################################
|
||||
"""\
|
||||
Support for working with ZConfig data without a schema.
|
||||
|
||||
"""
|
||||
__docformat__ = "reStructuredText"
|
||||
|
||||
import ZConfig.cfgparser
|
||||
|
||||
|
||||
def loadConfigFile(file, url=None):
|
||||
c = Context()
|
||||
Parser(Resource(file, url), c).parse(c.top)
|
||||
return c.top
|
||||
|
||||
|
||||
class Resource(object):
|
||||
|
||||
def __init__(self, file, url=''):
|
||||
self.file, self.url = file, url
|
||||
|
||||
|
||||
class Section(dict):
|
||||
|
||||
imports = ()
|
||||
|
||||
def __init__(self, type='', name='', data=None, sections=None):
|
||||
dict.__init__(self)
|
||||
if data:
|
||||
self.update(data)
|
||||
self.sections = sections or []
|
||||
self.type, self.name = type, name
|
||||
|
||||
def addValue(self, key, value, *args):
|
||||
if key in self:
|
||||
self[key].append(value)
|
||||
else:
|
||||
self[key] = [value]
|
||||
|
||||
def __str__(self, pre=''):
|
||||
result = []
|
||||
|
||||
if self.imports:
|
||||
for pkgname in self.imports:
|
||||
result.append('%import ' + pkgname)
|
||||
result.append('')
|
||||
|
||||
if self.type:
|
||||
if self.name:
|
||||
start = '%s<%s %s>' % (pre, self.type, self.name)
|
||||
else:
|
||||
start = '%s<%s>' % (pre, self.type)
|
||||
result.append(start)
|
||||
pre += ' '
|
||||
|
||||
lst = sorted(self.items())
|
||||
for name, values in lst:
|
||||
for value in values:
|
||||
result.append('%s%s %s' % (pre, name, value))
|
||||
|
||||
if self.sections and self:
|
||||
result.append('')
|
||||
|
||||
for section in self.sections:
|
||||
result.append(section.__str__(pre))
|
||||
|
||||
if self.type:
|
||||
pre = pre[:-2]
|
||||
result.append('%s</%s>' % (pre, self.type))
|
||||
result.append('')
|
||||
|
||||
result = '\n'.join(result).rstrip()
|
||||
if not pre:
|
||||
result += '\n'
|
||||
return result
|
||||
|
||||
|
||||
class Context(object):
|
||||
|
||||
def __init__(self):
|
||||
self.top = Section()
|
||||
self.sections = []
|
||||
|
||||
def startSection(self, container, type_, name):
|
||||
newsec = Section(type_, name)
|
||||
container.sections.append(newsec)
|
||||
return newsec
|
||||
|
||||
def endSection(self, container, type_, name, newsect):
|
||||
pass
|
||||
|
||||
def importSchemaComponent(self, pkgname):
|
||||
if pkgname not in self.top.imports:
|
||||
self.top.imports += (pkgname, )
|
||||
|
||||
def includeConfiguration(self, section, newurl, defines):
|
||||
raise NotImplementedError('includes are not supported')
|
||||
|
||||
|
||||
class Parser(ZConfig.cfgparser.ZConfigParser):
|
||||
|
||||
def handle_define(self, section, rest):
|
||||
raise NotImplementedError('defines are not supported')
|
||||
@@ -0,0 +1,293 @@
|
||||
=================================
|
||||
Using ZConfig data without schema
|
||||
=================================
|
||||
|
||||
Sometimes it's useful to use ZConfig configuration data without a
|
||||
schema. This is most interesting when assembling a configuration from
|
||||
fragments, as some buildout recipes do. This is not recommended for
|
||||
general application use.
|
||||
|
||||
The ``ZConfig.schemaless`` module provides some support for working
|
||||
without schema. Something things are not (currently) supported,
|
||||
including the %define and %include directives. The %import directive
|
||||
is supported.
|
||||
|
||||
This module provides basic support for loading configuration,
|
||||
inspecting and modifying it, and re-serializing the result.
|
||||
|
||||
>>> from ZConfig import schemaless
|
||||
|
||||
There is a single function which loads configuration data from a file
|
||||
open for reading. Let's take a look at this, and what it returns::
|
||||
|
||||
>>> config_text = '''
|
||||
...
|
||||
... some-key some-value
|
||||
...
|
||||
... some-key another-value
|
||||
...
|
||||
... <section>
|
||||
... key1 value1.1
|
||||
... key1 value1.2
|
||||
... key2 value2
|
||||
...
|
||||
... <deeper>
|
||||
... another key
|
||||
... another value
|
||||
... </deeper>
|
||||
... </section>
|
||||
...
|
||||
... another-key whee!
|
||||
...
|
||||
...
|
||||
... <another named>
|
||||
... nothing here
|
||||
... </another>
|
||||
...
|
||||
... '''
|
||||
|
||||
>>> from ZConfig._compat import NStringIO as StringIO
|
||||
>>> config = schemaless.loadConfigFile(StringIO(config_text))
|
||||
|
||||
The `config` object is a mapping from top-level keys to lists of
|
||||
values::
|
||||
|
||||
>>> config["some-key"]
|
||||
['some-value', 'another-value']
|
||||
|
||||
>>> config["another-key"]
|
||||
['whee!']
|
||||
|
||||
>>> config["no-such-key-in-the-config"]
|
||||
Traceback (most recent call last):
|
||||
KeyError: 'no-such-key-in-the-config'
|
||||
|
||||
>>> lst = list(config)
|
||||
>>> lst.sort()
|
||||
>>> lst
|
||||
['another-key', 'some-key']
|
||||
|
||||
There is also a ``sections`` attribute that lists child sections::
|
||||
|
||||
>>> len(config.sections)
|
||||
2
|
||||
|
||||
Let's take a look at one of the sections. Like the top-level
|
||||
configuration, the section maps keys
|
||||
|
||||
>>> section = config.sections[0]
|
||||
|
||||
>>> section["key1"]
|
||||
['value1.1', 'value1.2']
|
||||
|
||||
>>> section["key2"]
|
||||
['value2']
|
||||
|
||||
>>> section["no-such-key-in-the-config"]
|
||||
Traceback (most recent call last):
|
||||
KeyError: 'no-such-key-in-the-config'
|
||||
|
||||
>>> lst = list(section)
|
||||
>>> lst.sort()
|
||||
>>> lst
|
||||
['key1', 'key2']
|
||||
|
||||
Child sections are again available via the ``sections`` attribute::
|
||||
|
||||
>>> len(section.sections)
|
||||
1
|
||||
|
||||
In addition, the section has ``type`` and ``name`` attributes that
|
||||
record the type and name of the section as ZConfig understands them::
|
||||
|
||||
>>> section.type
|
||||
'section'
|
||||
|
||||
>>> print(section.name)
|
||||
None
|
||||
|
||||
Let's look at the named section from our example, so we can see the
|
||||
name::
|
||||
|
||||
>>> section = config.sections[1]
|
||||
>>> section.type
|
||||
'another'
|
||||
>>> section.name
|
||||
'named'
|
||||
|
||||
We can also mutate the configuration, adding new keys and values as
|
||||
desired::
|
||||
|
||||
>>> config["new-key"] = ["new-value-1", "new-value-2"]
|
||||
>>> config["some-key"].append("third-value")
|
||||
|
||||
New sections can also be added::
|
||||
|
||||
>>> section = schemaless.Section("sectiontype", "my-name")
|
||||
>>> section["key"] = ["value"]
|
||||
>>> config.sections.insert(1, section)
|
||||
|
||||
The configuration can be re-serialized using ``str()``::
|
||||
|
||||
>>> print(str(config))
|
||||
another-key whee!
|
||||
new-key new-value-1
|
||||
new-key new-value-2
|
||||
some-key some-value
|
||||
some-key another-value
|
||||
some-key third-value
|
||||
<BLANKLINE>
|
||||
<section>
|
||||
key1 value1.1
|
||||
key1 value1.2
|
||||
key2 value2
|
||||
<BLANKLINE>
|
||||
<deeper>
|
||||
another key
|
||||
another value
|
||||
</deeper>
|
||||
</section>
|
||||
<BLANKLINE>
|
||||
<sectiontype my-name>
|
||||
key value
|
||||
</sectiontype>
|
||||
<BLANKLINE>
|
||||
<another named>
|
||||
nothing here
|
||||
</another>
|
||||
<BLANKLINE>
|
||||
|
||||
Note that some adjustments have been made:
|
||||
|
||||
- key/value pairs come before child sections
|
||||
|
||||
- keys are sorted at each level
|
||||
|
||||
- blank lines are removed, with new blank lines inserted to preserve
|
||||
some semblance of readability
|
||||
|
||||
These are all presentation changes, but not essential changes to the
|
||||
configuration data. The ordering of sections is not modified in
|
||||
rendering, nor are the values for a single key re-ordered within a
|
||||
section or top-level configuration.
|
||||
|
||||
|
||||
Support for %import
|
||||
-------------------
|
||||
|
||||
Imports are supported, and are re-ordered in much the same way that
|
||||
other elements of a configuration are::
|
||||
|
||||
>>> config_text = '''
|
||||
...
|
||||
... %import some.package
|
||||
...
|
||||
... <section>
|
||||
...
|
||||
... %import another.package
|
||||
...
|
||||
... <another>
|
||||
... some value
|
||||
... </another>
|
||||
...
|
||||
... </section>
|
||||
...
|
||||
... some-key some-value
|
||||
...
|
||||
... '''
|
||||
|
||||
>>> config = schemaless.loadConfigFile(StringIO(config_text))
|
||||
|
||||
>>> print(config)
|
||||
%import some.package
|
||||
%import another.package
|
||||
<BLANKLINE>
|
||||
some-key some-value
|
||||
<BLANKLINE>
|
||||
<section>
|
||||
<another>
|
||||
some value
|
||||
</another>
|
||||
</section>
|
||||
<BLANKLINE>
|
||||
|
||||
The imports are also available as the ``imports`` attribute of the
|
||||
configuration object::
|
||||
|
||||
>>> config.imports
|
||||
('some.package', 'another.package')
|
||||
|
||||
Multiple imports of the same name are removed::
|
||||
|
||||
>>> config_text = '''
|
||||
...
|
||||
... %import some.package
|
||||
... %import another.package
|
||||
... %import some.package
|
||||
...
|
||||
... '''
|
||||
|
||||
>>> config = schemaless.loadConfigFile(StringIO(config_text))
|
||||
|
||||
>>> print(config)
|
||||
%import some.package
|
||||
%import another.package
|
||||
<BLANKLINE>
|
||||
|
||||
>>> config.imports
|
||||
('some.package', 'another.package')
|
||||
|
||||
|
||||
Limitations
|
||||
-----------
|
||||
|
||||
There are some limitations of handling ZConfig-based configurations
|
||||
using the ``ZConfig.schemaless`` module. Some of these are
|
||||
implementation issues, and may be corrected in the future:
|
||||
|
||||
- %define is not supported.
|
||||
|
||||
- %include is not supported.
|
||||
|
||||
Others are a function of not processing the schema, and can't easily
|
||||
be avoided:
|
||||
|
||||
- normalization of keys based on keytypes specified in the <schema> or
|
||||
<sectiontype> elements of the schema if not performed.
|
||||
|
||||
If the transformation of a key might affect the behavior controlled
|
||||
by the resulting configuration, the generated configuration may not
|
||||
be equivalent. Examples of this are unusual, but exist.
|
||||
|
||||
Limitations related to the non-processing of the schema cannot be
|
||||
detected by the ``ZConfig.schemaless``, so no errors are reported in
|
||||
these situations.
|
||||
|
||||
For the strictly syntactic limitations, we do get errors when the
|
||||
input data requires they be supported. Let's look at both the %define
|
||||
and %include handling.
|
||||
|
||||
When %define is used in the input configuration, an exception is
|
||||
raised when loading the configuration::
|
||||
|
||||
>>> config_text = '''
|
||||
...
|
||||
... %define somename somevalue
|
||||
...
|
||||
... '''
|
||||
|
||||
>>> schemaless.loadConfigFile(StringIO(config_text))
|
||||
Traceback (most recent call last):
|
||||
NotImplementedError: defines are not supported
|
||||
|
||||
A similar exception is raised for %include::
|
||||
|
||||
>>> config_text = '''
|
||||
...
|
||||
... %include some/other/file.conf
|
||||
...
|
||||
... '''
|
||||
|
||||
>>> schemaless.loadConfigFile(StringIO(config_text))
|
||||
Traceback (most recent call last):
|
||||
NotImplementedError: includes are not supported
|
||||
@@ -0,0 +1,183 @@
|
||||
##############################################################################
|
||||
#
|
||||
# Copyright (c) 2017 Zope Corporation and Contributors.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# This software is subject to the provisions of the Zope Public License,
|
||||
# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
|
||||
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
|
||||
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
|
||||
# FOR A PARTICULAR PURPOSE.
|
||||
#
|
||||
##############################################################################
|
||||
from __future__ import print_function, absolute_import
|
||||
|
||||
|
||||
from contextlib import contextmanager
|
||||
|
||||
try:
|
||||
from docutils import nodes
|
||||
import docutils.utils
|
||||
import docutils.frontend
|
||||
import docutils.parsers.rst
|
||||
from docutils.parsers.rst import Directive
|
||||
except ImportError: # pragma: no cover
|
||||
RstSchemaPrinter = None
|
||||
RstSchemaFormatter = None
|
||||
else:
|
||||
|
||||
from ZConfig._compat import string_types
|
||||
from ZConfig._schema_utils import load_schema
|
||||
from ZConfig._schema_utils import AbstractSchemaFormatter
|
||||
from ZConfig._schema_utils import AbstractSchemaPrinter
|
||||
from ZConfig._schema_utils import MARKER
|
||||
|
||||
class RstSchemaFormatter(AbstractSchemaFormatter):
|
||||
|
||||
settings = None
|
||||
|
||||
def __init__(self, schema, stream=None):
|
||||
super(RstSchemaFormatter, self).__init__(schema, stream)
|
||||
self.document = None
|
||||
self._current_node = None
|
||||
self._nodes = []
|
||||
self.settings = docutils.frontend.OptionParser(
|
||||
components=(docutils.parsers.rst.Parser,)).get_default_values()
|
||||
|
||||
def esc(self, text):
|
||||
return text
|
||||
|
||||
def _parsed(self, text, name='Schema'):
|
||||
document = docutils.utils.new_document(
|
||||
name,
|
||||
settings=self.settings)
|
||||
|
||||
|
||||
parser = docutils.parsers.rst.Parser()
|
||||
parser.parse(text, document)
|
||||
return document.children
|
||||
|
||||
def write(self, *texts):
|
||||
for text in texts:
|
||||
if isinstance(text, string_types):
|
||||
self._current_node += nodes.Text(' ' + text + ' ', text)
|
||||
else:
|
||||
# Already parsed
|
||||
self._current_node += text
|
||||
|
||||
def description(self, text):
|
||||
if not text:
|
||||
return
|
||||
|
||||
self.write(self._parsed(self._dedent(text), "description"))
|
||||
|
||||
def example(self, text):
|
||||
if not text:
|
||||
return
|
||||
|
||||
dedented = self._dedent(text)
|
||||
example = "Example::\n\n\t" + '\n\t'.join(dedented.split('\n'))
|
||||
self.write(self._parsed(example, "example"))
|
||||
|
||||
@contextmanager
|
||||
def item_list(self):
|
||||
old_node = self._current_node
|
||||
self._current_node = nodes.definition_list()
|
||||
old_node += self._current_node
|
||||
yield
|
||||
self._current_node = old_node
|
||||
|
||||
|
||||
@contextmanager
|
||||
def describing(self, description=MARKER, after=None):
|
||||
dl = self._current_node
|
||||
assert isinstance(dl, nodes.definition_list), dl
|
||||
item = nodes.definition_list_item()
|
||||
dl += item
|
||||
term = nodes.term()
|
||||
item += term
|
||||
self._current_node = term
|
||||
|
||||
yield
|
||||
|
||||
# We must now have either a description (so we call
|
||||
# described_as) or they must call described_as
|
||||
# des
|
||||
self._current_node = item
|
||||
|
||||
self._describing(description, after)
|
||||
|
||||
|
||||
@contextmanager
|
||||
def described_as(self):
|
||||
item = self._current_node
|
||||
assert isinstance(item, nodes.definition_list_item), item
|
||||
|
||||
definition = nodes.definition()
|
||||
para = nodes.paragraph()
|
||||
definition += para
|
||||
item += definition
|
||||
self._current_node = para
|
||||
|
||||
yield
|
||||
|
||||
# When this is done, we're back to the list
|
||||
self._current_node = item.parent
|
||||
|
||||
def abstract_name(self, name):
|
||||
self._current_node += nodes.emphasis(text=name, rawsource=name)
|
||||
|
||||
def concrete_name(self, *name):
|
||||
name = ' '.join(name)
|
||||
self._current_node += nodes.strong(text=name, rawsource=name)
|
||||
|
||||
def concrete_section_name(self, *name):
|
||||
name = ' '.join(name)
|
||||
return self.concrete_name("<" + name + ">")
|
||||
|
||||
@contextmanager
|
||||
def body(self):
|
||||
self.document = self._current_node = docutils.utils.new_document(
|
||||
"Schema",
|
||||
settings=self.settings)
|
||||
yield
|
||||
|
||||
class RstSchemaPrinter(AbstractSchemaPrinter):
|
||||
_schema_formatter = RstSchemaFormatter
|
||||
|
||||
def printSchema(self):
|
||||
super(RstSchemaPrinter, self).printSchema()
|
||||
print(self.fmt.document.pformat(), file=self.fmt.stream)
|
||||
|
||||
|
||||
class SchemaToRstDirective(Directive):
|
||||
required_arguments = 1
|
||||
optional_arguments = 2
|
||||
option_spec = {
|
||||
'file': str,
|
||||
'members': str,
|
||||
'excluded-members': str,
|
||||
}
|
||||
def run(self):
|
||||
schema = load_schema(self.arguments[0],
|
||||
True, self.options.get('file'))
|
||||
|
||||
members = ()
|
||||
if 'members' in self.options:
|
||||
members = self.options['members'].split()
|
||||
|
||||
excluded_members = ()
|
||||
if 'excluded-members' in self.options:
|
||||
excluded_members = self.options['excluded-members'].split()
|
||||
|
||||
printer = RstSchemaPrinter(schema, allowed_names=members, excluded_names=excluded_members)
|
||||
printer.fmt.settings = self.state.document.settings
|
||||
|
||||
printer.buildSchema()
|
||||
|
||||
return printer.fmt.document.children
|
||||
|
||||
def setup(app): # pragma: no cover
|
||||
"Sphinx extension entry point to add the zconfig directive."
|
||||
app.add_directive("zconfig", SchemaToRstDirective)
|
||||
@@ -0,0 +1,115 @@
|
||||
##############################################################################
|
||||
#
|
||||
# Copyright (c) 2002, 2003 Zope Foundation and Contributors.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# This software is subject to the provisions of the Zope Public License,
|
||||
# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
|
||||
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
|
||||
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
|
||||
# FOR A PARTICULAR PURPOSE.
|
||||
#
|
||||
##############################################################################
|
||||
"""Shell-style string substitution helper."""
|
||||
|
||||
import os
|
||||
import ZConfig
|
||||
|
||||
|
||||
def substitute(s, mapping):
|
||||
"""Substitute values from *mapping* into *s*.
|
||||
|
||||
*mapping* can be a :class:`dict` or any type that supports the
|
||||
``get()`` method of the mapping protocol. Replacement values are
|
||||
copied into the result without further interpretation. Raises
|
||||
:exc:`~.SubstitutionSyntaxError` if there are malformed constructs
|
||||
in *s*.
|
||||
"""
|
||||
|
||||
if "$" in s:
|
||||
result = ''
|
||||
rest = s
|
||||
while rest:
|
||||
p, name, namecase, rest, vtype = _split(rest)
|
||||
result += p
|
||||
if name:
|
||||
v = None
|
||||
if vtype == 'define':
|
||||
v = mapping.get(name)
|
||||
if vtype == 'env':
|
||||
v = os.getenv(namecase)
|
||||
|
||||
if v is None:
|
||||
raise ZConfig.SubstitutionReplacementError(s, namecase)
|
||||
result += v
|
||||
return result
|
||||
else:
|
||||
return s
|
||||
|
||||
|
||||
def isname(s):
|
||||
"""Returns ``True`` if *s* is a valid name for a substitution
|
||||
text, otherwise returns ``False``.
|
||||
"""
|
||||
|
||||
m = _name_match(s)
|
||||
if m:
|
||||
return m.group() == s
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def _split(s):
|
||||
# Return a four tuple: prefix, name, namecase, suffix
|
||||
# - prefix is text that can be used literally in the result (may be '')
|
||||
# - name is a referenced name, or None
|
||||
# - namecase is the name with case preserved
|
||||
# - suffix is trailling text that may contain additional references
|
||||
# (may be '' or None)
|
||||
if "$" in s:
|
||||
i = s.find("$")
|
||||
c = s[i+1:i+2]
|
||||
if c == "":
|
||||
raise ZConfig.SubstitutionSyntaxError(
|
||||
"illegal lone '$' at end of source")
|
||||
if c == "$":
|
||||
return s[:i+1], None, None, s[i+2:], None
|
||||
prefix = s[:i]
|
||||
vtype = 'define'
|
||||
if c == "{":
|
||||
m = _name_match(s, i + 2)
|
||||
if not m:
|
||||
raise ZConfig.SubstitutionSyntaxError(
|
||||
"'${' not followed by name")
|
||||
name = m.group(0)
|
||||
i = m.end() + 1
|
||||
if not s.startswith("}", i - 1):
|
||||
raise ZConfig.SubstitutionSyntaxError(
|
||||
"'${%s' not followed by '}'" % name)
|
||||
elif c == "(":
|
||||
m = _name_match(s, i + 2)
|
||||
if not m:
|
||||
raise ZConfig.SubstitutionSyntaxError(
|
||||
"'$(' not followed by name")
|
||||
name = m.group(0)
|
||||
i = m.end() + 1
|
||||
if not s.startswith(")", i - 1):
|
||||
raise ZConfig.SubstitutionSyntaxError(
|
||||
"'$(%s' not followed by ')'" % name)
|
||||
vtype = 'env'
|
||||
else:
|
||||
m = _name_match(s, i+1)
|
||||
if not m:
|
||||
raise ZConfig.SubstitutionSyntaxError(
|
||||
"'$' not followed by '$' or name")
|
||||
name = m.group(0)
|
||||
i = m.end()
|
||||
return prefix, name.lower(), name, s[i:], vtype
|
||||
else:
|
||||
return s, None, None, None, None
|
||||
|
||||
|
||||
import re
|
||||
_name_match = re.compile(r"[a-zA-Z_][a-zA-Z0-9_]*").match
|
||||
del re
|
||||
@@ -0,0 +1,17 @@
|
||||
##############################################################################
|
||||
#
|
||||
# Copyright (c) 2002, 2003 Zope Foundation and Contributors.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# This software is subject to the provisions of the Zope Public License,
|
||||
# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
|
||||
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
|
||||
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
|
||||
# FOR A PARTICULAR PURPOSE.
|
||||
#
|
||||
##############################################################################
|
||||
"""Tests for the configuration data structures and loader.
|
||||
|
||||
$Id: __init__.py,v 1.2 2003/01/03 21:05:56 fdrake Exp $
|
||||
"""
|
||||
@@ -0,0 +1,5 @@
|
||||
<component>
|
||||
|
||||
<section />
|
||||
|
||||
</component>
|
||||
@@ -0,0 +1,7 @@
|
||||
<component>
|
||||
|
||||
<sectiontype name='foo'>
|
||||
<section type='foo' attribute='bar' />
|
||||
</sectiontype>
|
||||
|
||||
</component>
|
||||
Binary file not shown.
@@ -0,0 +1,4 @@
|
||||
<schema datatype="ZConfig.tests.test_schema.MySection">
|
||||
<sectiontype name="type-1"/>
|
||||
</schema>
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
<schema datatype="ZConfig.tests.test_schema.appsection">
|
||||
<sectiontype name="type-2"/>
|
||||
</schema>
|
||||
@@ -0,0 +1,3 @@
|
||||
<schema keytype="basic-key">
|
||||
<sectiontype name="type-1"/>
|
||||
</schema>
|
||||
@@ -0,0 +1,3 @@
|
||||
<schema keytype="ZConfig.tests.test_schema.uppercase">
|
||||
<sectiontype name="type-2"/>
|
||||
</schema>
|
||||
@@ -0,0 +1,7 @@
|
||||
<schema>
|
||||
<description>
|
||||
<!-- This description is referenced from a test. -->
|
||||
base description
|
||||
</description>
|
||||
<sectiontype name="type-X"/>
|
||||
</schema>
|
||||
@@ -0,0 +1,4 @@
|
||||
var2 value2
|
||||
%include simple.conf
|
||||
var3 value3
|
||||
var4 $name
|
||||
@@ -0,0 +1,2 @@
|
||||
refouter $outervar
|
||||
%define innervar inner
|
||||
@@ -0,0 +1,7 @@
|
||||
<schema>
|
||||
<description>
|
||||
Sample library of reusable data types.
|
||||
</description>
|
||||
<sectiontype name="type-a"/>
|
||||
<sectiontype name="type-b"/>
|
||||
</schema>
|
||||
@@ -0,0 +1,12 @@
|
||||
<schema>
|
||||
<sectiontype name="loghandler">
|
||||
<key name="constructor" datatype="constructor" required="yes"/>
|
||||
<key name="formatter" datatype="constructor"
|
||||
default="logging.Formatter()"/>
|
||||
</sectiontype>
|
||||
|
||||
<sectiontype name="logger">
|
||||
<key name="level" datatype="integer" default="info"/>
|
||||
<multisection type="loghandler" attribute="handlers" name="*"/>
|
||||
</sectiontype>
|
||||
</schema>
|
||||
@@ -0,0 +1,2 @@
|
||||
# -*-coding: utf-8; mode: conf-*-
|
||||
This file contains a snowman, U+2603: ☃
|
||||
@@ -0,0 +1,3 @@
|
||||
%define outervar outer
|
||||
%include inner.conf
|
||||
refinner $innervar
|
||||
@@ -0,0 +1,32 @@
|
||||
empty
|
||||
|
||||
var1 abc
|
||||
int-var 12
|
||||
float-var 12.02
|
||||
neg-int -2
|
||||
|
||||
true-var-1 true
|
||||
true-var-2 on
|
||||
true-var-3 yes
|
||||
|
||||
false-var-1 false
|
||||
false-var-2 off
|
||||
false-var-3 no
|
||||
|
||||
list-1
|
||||
list-2 abc
|
||||
list-3 abc def ghi
|
||||
list-4 [ what now? ]
|
||||
|
||||
# These test the %define mechanism:
|
||||
|
||||
%define dollars $$$$
|
||||
%define empty
|
||||
%define name value
|
||||
%define twowords two words
|
||||
|
||||
getname $name
|
||||
getnametwice $name${name}
|
||||
getdollars $dollars
|
||||
getempty x${empty}y
|
||||
getwords abc $twowords def
|
||||
@@ -0,0 +1,29 @@
|
||||
<schema>
|
||||
<key name="empty" />
|
||||
<key name="var1" />
|
||||
<key name="getname" />
|
||||
<key name="getnametwice" />
|
||||
<key name="getdollars" />
|
||||
<key name="getempty" />
|
||||
<key name="getwords" />
|
||||
|
||||
<key name="int-var" datatype="integer" />
|
||||
<key name="float-var" datatype="float" />
|
||||
<key name="neg-int" datatype="integer" />
|
||||
<key name="true-var-1" datatype="boolean" />
|
||||
<key name="true-var-2" datatype="boolean" />
|
||||
<key name="true-var-3" datatype="boolean" />
|
||||
<key name="false-var-1" datatype="boolean" />
|
||||
<key name="false-var-2" datatype="boolean" />
|
||||
<key name="false-var-3" datatype="boolean" />
|
||||
|
||||
<key name="list-1" datatype="string-list" />
|
||||
<key name="list-2" datatype="string-list" />
|
||||
<key name="list-3" datatype="string-list" />
|
||||
<key name="list-4" datatype="string-list" />
|
||||
|
||||
<!-- added by include.conf -->
|
||||
<key name="var2" />
|
||||
<key name="var3" />
|
||||
<key name="var4" />
|
||||
</schema>
|
||||
@@ -0,0 +1,40 @@
|
||||
var foo
|
||||
var-0 foo-0
|
||||
|
||||
<section name>
|
||||
var bar
|
||||
var-one splat
|
||||
</section>
|
||||
|
||||
var-1 foo-1
|
||||
|
||||
<section delegate>
|
||||
var spam
|
||||
var-two stuff
|
||||
</section>
|
||||
|
||||
var-2 foo-2
|
||||
|
||||
<section another>
|
||||
var quack!
|
||||
var-three yet
|
||||
</section>
|
||||
|
||||
var-3 foo-3
|
||||
|
||||
# An anonymous empty section:
|
||||
<section />
|
||||
|
||||
var-4 foo-4
|
||||
|
||||
# A fairly trivial section:
|
||||
<trivial>
|
||||
var triv
|
||||
</trivial>
|
||||
|
||||
var-5 foo-5
|
||||
|
||||
# A minimal section:
|
||||
<minimal/>
|
||||
|
||||
var-6 foo-6
|
||||
@@ -0,0 +1,63 @@
|
||||
<schema>
|
||||
<sectiontype name="section">
|
||||
<key name="var" />
|
||||
<key name="var-one" />
|
||||
<key name="var-two" />
|
||||
<key name="var-three" />
|
||||
</sectiontype>
|
||||
<sectiontype name="minimal" />
|
||||
<sectiontype name="trivial">
|
||||
<key name="var" />
|
||||
</sectiontype>
|
||||
|
||||
<sectiontype name="hasmin">
|
||||
<key name="var" required="yes" />
|
||||
</sectiontype>
|
||||
|
||||
<multisection type="section" name="*" attribute="sections">
|
||||
<example>Multisection Example</example>
|
||||
</multisection>
|
||||
<section type="minimal" name="*" attribute="minimal" />
|
||||
<section type="trivial" name="*" attribute="trivial" />
|
||||
<section type="hasmin" name="+" attribute="hasmin" />
|
||||
|
||||
<key name="var" />
|
||||
<key name="var-0" />
|
||||
<key name="var-1" />
|
||||
<key name="var-2" />
|
||||
<key name="var-3" />
|
||||
<key name="var-4" />
|
||||
<key name="var-5" />
|
||||
<key name="var-6" default="1">
|
||||
<description>Description</description>
|
||||
<metadefault>For humans</metadefault>
|
||||
</key>
|
||||
|
||||
<abstracttype name="abstype" abstract="true">
|
||||
<description>Description</description>
|
||||
</abstracttype>
|
||||
<sectiontype name="implabs" implements="abstype">
|
||||
<description>Description</description>
|
||||
</sectiontype>
|
||||
<sectiontype name="extabs" extends="implabs">
|
||||
<description>Description</description>
|
||||
</sectiontype>
|
||||
<sectiontype name="extabs2" extends="implabs">
|
||||
<description>Description</description>
|
||||
</sectiontype>
|
||||
<sectiontype name="extabs3" extends="extabs2">
|
||||
<description>Description</description>
|
||||
</sectiontype>
|
||||
<section type="abstype" name="absinfo">
|
||||
<description>Description</description>
|
||||
<example>Section Example</example>
|
||||
</section>
|
||||
<section type="extabs" name="extabs" />
|
||||
<section type="extabs" name="extabs2" />
|
||||
<section type="implabs" name="implabs" />
|
||||
<section type="extabs3" name="implabs2" />
|
||||
<multikey name="mkey">
|
||||
<default>1</default>
|
||||
<default>2</default>
|
||||
</multikey>
|
||||
</schema>
|
||||
@@ -0,0 +1,2 @@
|
||||
This is a sample library of configuration schema components. This is
|
||||
used for testing.
|
||||
@@ -0,0 +1 @@
|
||||
# Make this a package.
|
||||
@@ -0,0 +1,22 @@
|
||||
##############################################################################
|
||||
#
|
||||
# Copyright (c) 2003 Zope Foundation and Contributors.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# This software is subject to the provisions of the Zope Public License,
|
||||
# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
|
||||
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
|
||||
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
|
||||
# FOR A PARTICULAR PURPOSE.
|
||||
#
|
||||
##############################################################################
|
||||
"""Example of a package that extends its __path__.
|
||||
|
||||
$Id: __init__.py,v 1.2 2003/10/03 17:11:33 fdrake Exp $
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
here = os.path.dirname(__file__)
|
||||
__path__.append(os.path.join(here, "extras"))
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="us-ascii"?>
|
||||
<component>
|
||||
<abstracttype name='thing'/>
|
||||
<sectiontype name='thing-a' implements='thing'>
|
||||
<key name='thing-a-key' default='thing-a-default'/>
|
||||
</sectiontype>
|
||||
<sectiontype name='thing-b' implements='thing'>
|
||||
<key name='thing-b-key' default='thing-b-default'/>
|
||||
</sectiontype>
|
||||
</component>
|
||||
@@ -0,0 +1,5 @@
|
||||
<component>
|
||||
<sectiontype name='extra-thing'>
|
||||
<key name='some-key'/>
|
||||
</sectiontype>
|
||||
</component>
|
||||
@@ -0,0 +1 @@
|
||||
# Make this a package.
|
||||
@@ -0,0 +1,7 @@
|
||||
<component>
|
||||
<abstracttype name='widget'/>
|
||||
<sectiontype name='widget-a' implements='widget'>
|
||||
<key name='widget-a-key' default='widget-a-default'/>
|
||||
</sectiontype>
|
||||
<sectiontype name='widget-b' implements='widget' extends='widget-a'/>
|
||||
</component>
|
||||
@@ -0,0 +1,5 @@
|
||||
<component>
|
||||
<sectiontype name='extra-type'>
|
||||
<key name='some-key'/>
|
||||
</sectiontype>
|
||||
</component>
|
||||
@@ -0,0 +1,97 @@
|
||||
##############################################################################
|
||||
#
|
||||
# Copyright (c) 2003 Zope Foundation and Contributors.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# This software is subject to the provisions of the Zope Public License,
|
||||
# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
|
||||
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
|
||||
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
|
||||
# FOR A PARTICULAR PURPOSE.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
"""Support code shared among the tests."""
|
||||
|
||||
import contextlib
|
||||
import os
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
import ZConfig
|
||||
|
||||
from ZConfig.loader import ConfigLoader
|
||||
from ZConfig.url import urljoin
|
||||
|
||||
from ZConfig._compat import NStringIO as StringIO
|
||||
from ZConfig._compat import pathname2url
|
||||
|
||||
INPUT_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "input"))
|
||||
CONFIG_BASE = "file://%s/" % pathname2url(INPUT_DIR)
|
||||
|
||||
def input_file(fname):
|
||||
return os.path.abspath(os.path.join(INPUT_DIR, fname))
|
||||
|
||||
def with_stdin_from_input_file(fname):
|
||||
input_fname = input_file(fname)
|
||||
@contextlib.contextmanager
|
||||
def stdin_replaced():
|
||||
old_stdin = sys.stdin
|
||||
sys.stdin = open(input_fname)
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
sys.stdin.close()
|
||||
sys.stdin = old_stdin
|
||||
|
||||
def make_wrapper(f):
|
||||
def f2(self):
|
||||
with stdin_replaced():
|
||||
f(self)
|
||||
return f2
|
||||
|
||||
return make_wrapper
|
||||
|
||||
|
||||
class TestHelper(object):
|
||||
"""Utility methods which can be used with the schema support."""
|
||||
|
||||
# Not derived from unittest.TestCase; some test runners seem to
|
||||
# think that means this class contains tests.
|
||||
|
||||
assertRaisesRegex = getattr(unittest.TestCase, 'assertRaisesRegex',
|
||||
unittest.TestCase.assertRaisesRegexp)
|
||||
|
||||
def load_both(self, schema_url, conf_url):
|
||||
schema = self.load_schema(schema_url)
|
||||
conf = self.load_config(schema, conf_url)
|
||||
return schema, conf
|
||||
|
||||
def load_schema(self, relurl):
|
||||
self.url = urljoin(CONFIG_BASE, relurl)
|
||||
self.schema = ZConfig.loadSchema(self.url)
|
||||
self.assertTrue(self.schema.issection())
|
||||
return self.schema
|
||||
|
||||
def load_schema_text(self, text, url=None):
|
||||
sio = StringIO(text)
|
||||
self.schema = ZConfig.loadSchemaFile(sio, url)
|
||||
return self.schema
|
||||
|
||||
def load_config(self, schema, conf_url, num_handlers=0):
|
||||
conf_url = urljoin(CONFIG_BASE, conf_url)
|
||||
loader = self.create_config_loader(schema)
|
||||
self.conf, self.handlers = loader.loadURL(conf_url)
|
||||
self.assertEqual(len(self.handlers), num_handlers)
|
||||
return self.conf
|
||||
|
||||
def load_config_text(self, schema, text, num_handlers=0, url=None):
|
||||
sio = StringIO(text)
|
||||
loader = self.create_config_loader(schema)
|
||||
self.conf, self.handlers = loader.loadFile(sio, url)
|
||||
self.assertEqual(len(self.handlers), num_handlers)
|
||||
return self.conf
|
||||
|
||||
def create_config_loader(self, schema):
|
||||
return ConfigLoader(schema)
|
||||
@@ -0,0 +1,57 @@
|
||||
##############################################################################
|
||||
#
|
||||
# Copyright (c) 2003 Zope Foundation and Contributors.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# This software is subject to the provisions of the Zope Public License,
|
||||
# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
|
||||
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
|
||||
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
|
||||
# FOR A PARTICULAR PURPOSE.
|
||||
#
|
||||
##############################################################################
|
||||
"""Tests of the %import mechanism.
|
||||
"""
|
||||
import unittest
|
||||
|
||||
import ZConfig
|
||||
import ZConfig.tests.support
|
||||
|
||||
from ZConfig._compat import NStringIO as StringIO
|
||||
|
||||
|
||||
class TestImportFromConfiguration(
|
||||
ZConfig.tests.support.TestHelper, unittest.TestCase):
|
||||
|
||||
def test_simple_import(self):
|
||||
schema = self.load_schema_text("<schema/>")
|
||||
loader = self.create_config_loader(schema)
|
||||
config, _ = loader.loadFile(
|
||||
StringIO("%import ZConfig.tests.library.widget\n"))
|
||||
# make sure we now have a "private" schema object; the only
|
||||
# way to get it is from the loader itself
|
||||
self.assertTrue(schema is not loader.schema)
|
||||
# make sure component types are only found on the private schema:
|
||||
loader.schema.gettype("widget-b")
|
||||
self.assertRaises(ZConfig.SchemaError, schema.gettype, "widget-b")
|
||||
|
||||
def test_repeated_import(self):
|
||||
schema = self.load_schema_text("<schema/>")
|
||||
loader = self.create_config_loader(schema)
|
||||
config, _ = loader.loadFile(
|
||||
StringIO("%import ZConfig.tests.library.widget\n"
|
||||
"%import ZConfig.tests.library.widget\n"))
|
||||
|
||||
def test_missing_import(self):
|
||||
schema = self.load_schema_text("<schema/>")
|
||||
loader = self.create_config_loader(schema)
|
||||
self.assertRaises(ZConfig.SchemaError, loader.loadFile,
|
||||
StringIO("%import ZConfig.tests.missing\n"))
|
||||
|
||||
|
||||
def test_suite():
|
||||
return unittest.defaultTestLoader.loadTestsFromName(__name__)
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main(defaultTest='test_suite')
|
||||
@@ -0,0 +1,213 @@
|
||||
##############################################################################
|
||||
#
|
||||
# Copyright (c) 2003 Zope Foundation and Contributors.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# This software is subject to the provisions of the Zope Public License,
|
||||
# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
|
||||
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
|
||||
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
|
||||
# FOR A PARTICULAR PURPOSE.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
"""Tests of the command-line integration."""
|
||||
|
||||
import unittest
|
||||
|
||||
import ZConfig
|
||||
import ZConfig.tests.support
|
||||
|
||||
from ZConfig.cmdline import ExtendedConfigLoader
|
||||
|
||||
|
||||
class CommandLineTest(ZConfig.tests.support.TestHelper, unittest.TestCase):
|
||||
|
||||
clopts = ()
|
||||
|
||||
def create_config_loader(self, schema):
|
||||
loader = ExtendedConfigLoader(schema)
|
||||
for item in self.clopts:
|
||||
loader.addOption(*item)
|
||||
return loader
|
||||
|
||||
def test_loading(self):
|
||||
schema = self.load_schema_text("""\
|
||||
<schema>
|
||||
<sectiontype name='st'>
|
||||
<key name='innerkey'/>
|
||||
</sectiontype>
|
||||
<key name='mykey'/>
|
||||
<section name='*' type='st' attribute='sect'/>
|
||||
</schema>
|
||||
""")
|
||||
self.clopts = [("mykey=splat!", None),
|
||||
("section/innerkey=spoogey", None)]
|
||||
bag = self.create_config_loader(schema).cook()
|
||||
# Test a variety of queries on the OptionBag:
|
||||
self.assertTrue("mykey" in bag)
|
||||
self.assertTrue("another" not in bag)
|
||||
self.assertEqual(bag.get_section_info("st", None), None)
|
||||
self.assertEqual(bag.get_section_info("st", "missing-sect"), None)
|
||||
# Consume everything in the OptionBag:
|
||||
L = bag.get_key("mykey")
|
||||
s, pos = L[0]
|
||||
self.assertEqual(len(L), 1)
|
||||
self.assertEqual(s, "splat!")
|
||||
bag2 = bag.get_section_info("st", "section")
|
||||
self.assertTrue("innerkey" in bag2)
|
||||
self.assertTrue("another" not in bag2)
|
||||
L = bag2.get_key("innerkey")
|
||||
s, pos = L[0]
|
||||
self.assertEqual(len(L), 1)
|
||||
self.assertEqual(s, "spoogey")
|
||||
# "Finish" to make sure everything has been consumed:
|
||||
bag2.finish()
|
||||
bag.finish()
|
||||
|
||||
def test_named_sections(self):
|
||||
schema = self.load_schema_text("""\
|
||||
<schema>
|
||||
<abstracttype name='at'/>
|
||||
<sectiontype name='st1' implements='at'>
|
||||
<key name='k1'/>
|
||||
</sectiontype>
|
||||
<sectiontype name='st2' implements='at'>
|
||||
<key name='k2'/>
|
||||
</sectiontype>
|
||||
<section name='foo' type='at'/>
|
||||
<section name='bar' type='st2'/>
|
||||
</schema>
|
||||
""")
|
||||
self.clopts = [("foo/k1=v1", None), ("bar/k2=v2", ("someurl", 2, 3))]
|
||||
loader = self.create_config_loader(schema)
|
||||
bag = loader.cook()
|
||||
foo = bag.get_section_info("st2", "foo")
|
||||
bar = bag.get_section_info("st2", "bar")
|
||||
bag.finish()
|
||||
self.assertEqual(bar.get_key("k2"), [("v2", ("someurl", 2, 3))])
|
||||
bar.finish()
|
||||
# Ignore foo for now; it's not really important *when* it fails.
|
||||
|
||||
# ValueErrors are converted into ConfigurationSyntaxErrors
|
||||
self.assertRaisesRegex(ZConfig.ConfigurationSyntaxError,
|
||||
"could not convert",
|
||||
foo.basic_key,
|
||||
'invalid name', ('<place>', 1,))
|
||||
|
||||
# missing keys return empty lists
|
||||
self.assertEqual(foo.get_key('no such key'), [])
|
||||
|
||||
# VE for matchers do the same conversion
|
||||
matcher = loader.createSchemaMatcher()
|
||||
self.assertRaisesRegex(ZConfig.DataConversionError,
|
||||
"value did not match",
|
||||
matcher.addValue,
|
||||
'invalid name', 'value', (1, 1, '<place>'))
|
||||
|
||||
|
||||
simple_schema = None
|
||||
|
||||
def get_simple_schema(self):
|
||||
if self.simple_schema is None:
|
||||
self.__class__.simple_schema = self.load_schema_text("""\
|
||||
<schema>
|
||||
<key name='k0'/>
|
||||
<key name='k1'/>
|
||||
<key name='k2' datatype='integer'/>
|
||||
<key name='k3' datatype='integer' default='19'/>
|
||||
</schema>
|
||||
""")
|
||||
return self.simple_schema
|
||||
|
||||
def test_reading_config(self):
|
||||
self.clopts = [("k1=stringvalue", None), ("k2=12", None)]
|
||||
schema = self.get_simple_schema()
|
||||
conf = self.load_config_text(schema, """\
|
||||
k0 stuff
|
||||
k1 replaced-stuff
|
||||
k2 42
|
||||
""")
|
||||
self.assertEqual(conf.k0, "stuff")
|
||||
self.assertEqual(conf.k1, "stringvalue")
|
||||
self.assertEqual(conf.k2, 12)
|
||||
self.assertEqual(conf.k3, 19)
|
||||
|
||||
def test_unknown_key(self):
|
||||
self.clopts = [("foo=bar", None)]
|
||||
schema = self.get_simple_schema()
|
||||
self.assertRaises(ZConfig.ConfigurationError,
|
||||
self.load_config_text, schema, "")
|
||||
|
||||
def test_too_many_keys(self):
|
||||
self.clopts = [("k1=v1", None), ("k1=v2", None)]
|
||||
schema = self.get_simple_schema()
|
||||
self.assertRaises(ZConfig.ConfigurationError,
|
||||
self.load_config_text, schema, "")
|
||||
|
||||
def test_bad_datatype(self):
|
||||
self.clopts = [("k2=42.0", None)]
|
||||
schema = self.get_simple_schema()
|
||||
self.assertRaises(ZConfig.DataConversionError,
|
||||
self.load_config_text, schema, "")
|
||||
|
||||
def test_without_clopts(self):
|
||||
self.clopts = []
|
||||
schema = self.get_simple_schema()
|
||||
conf = self.load_config_text(schema, "k3 42")
|
||||
self.assertEqual(conf.k0, None)
|
||||
self.assertEqual(conf.k1, None)
|
||||
self.assertEqual(conf.k2, None)
|
||||
self.assertEqual(conf.k3, 42)
|
||||
|
||||
def test_section_contents(self):
|
||||
schema = self.load_schema_text("""\
|
||||
<schema>
|
||||
<sectiontype name='st'>
|
||||
<key name='k1'/>
|
||||
<key name='k2' default='3' datatype='integer'/>
|
||||
<multikey name='k3'>
|
||||
<default>k3-v1</default>
|
||||
<default>k3-v2</default>
|
||||
<default>k3-v3</default>
|
||||
</multikey>
|
||||
</sectiontype>
|
||||
<section name='s1' type='st'/>
|
||||
<section name='s2' type='st'/>
|
||||
</schema>
|
||||
""")
|
||||
self.clopts = [("s1/k1=foo", None),
|
||||
("s2/k3=value1", None),
|
||||
("s2/k3=value2", None),
|
||||
("s1/k2=99", None),
|
||||
("s2/k3=value3", None),
|
||||
("s2/k3=value4", None),
|
||||
]
|
||||
conf = self.load_config_text(schema, "<st s1/>\n<st s2/>")
|
||||
self.assertEqual(conf.s1.k1, "foo")
|
||||
self.assertEqual(conf.s1.k2, 99)
|
||||
self.assertEqual(conf.s1.k3, ["k3-v1", "k3-v2", "k3-v3"])
|
||||
self.assertEqual(conf.s2.k1, None)
|
||||
self.assertEqual(conf.s2.k2, 3)
|
||||
self.assertEqual(conf.s2.k3, ["value1", "value2", "value3", "value4"])
|
||||
|
||||
self.clopts = [("path/that/dne=foo",)]
|
||||
self.assertRaisesRegex(ZConfig.ConfigurationError,
|
||||
"not all command line options were consumed",
|
||||
self.load_config_text,
|
||||
schema, "<st s1/>")
|
||||
|
||||
def test_bad_overrides(self):
|
||||
schema = self.get_simple_schema()
|
||||
self.clopts = [('',)]
|
||||
self.assertRaisesRegex(ZConfig.ConfigurationSyntaxError,
|
||||
"invalid configuration specifier",
|
||||
self.create_config_loader,
|
||||
schema)
|
||||
|
||||
self.clopts = [('double//slashes=value',)]
|
||||
self.assertRaisesRegex(ZConfig.ConfigurationSyntaxError,
|
||||
"not allowed in an option path",
|
||||
self.create_config_loader,
|
||||
schema)
|
||||
@@ -0,0 +1,274 @@
|
||||
##############################################################################
|
||||
#
|
||||
# Copyright (c) 2002, 2003 Zope Foundation and Contributors.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# This software is subject to the provisions of the Zope Public License,
|
||||
# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
|
||||
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
|
||||
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
|
||||
# FOR A PARTICULAR PURPOSE.
|
||||
#
|
||||
##############################################################################
|
||||
"""Tests of the configuration data structures and loader."""
|
||||
import os
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
import ZConfig
|
||||
|
||||
from ZConfig.tests.support import CONFIG_BASE
|
||||
from ZConfig.tests.support import TestHelper
|
||||
|
||||
from ZConfig._compat import NStringIO as StringIO
|
||||
|
||||
class ConfigurationTestCase(TestHelper, unittest.TestCase):
|
||||
|
||||
schema = None
|
||||
|
||||
def get_schema(self):
|
||||
if self.schema is None:
|
||||
ConfigurationTestCase.schema = ZConfig.loadSchema(
|
||||
CONFIG_BASE + "simple.xml")
|
||||
return self.schema
|
||||
|
||||
def load(self, relurl, context=None):
|
||||
url = CONFIG_BASE + relurl
|
||||
self.conf, self.handlers = ZConfig.loadConfig(self.get_schema(), url)
|
||||
conf = self.conf
|
||||
#self.assertEqual(conf.url, url)
|
||||
self.assertTrue(conf.getSectionName() is None)
|
||||
self.assertTrue(conf.getSectionType() is None)
|
||||
#self.assertTrue(conf.delegate is None)
|
||||
return conf
|
||||
|
||||
def loadtext(self, text):
|
||||
sio = StringIO(text)
|
||||
return self.loadfile(sio)
|
||||
|
||||
def loadfile(self, file_or_path):
|
||||
schema = self.get_schema()
|
||||
self.conf, self.handlers = ZConfig.loadConfigFile(schema, file_or_path)
|
||||
return self.conf
|
||||
|
||||
def check_simple_gets(self, conf):
|
||||
self.assertEqual(conf.empty, '')
|
||||
self.assertEqual(conf.int_var, 12)
|
||||
self.assertEqual(conf.neg_int, -2)
|
||||
self.assertEqual(conf.float_var, 12.02)
|
||||
self.assertEqual(conf.var1, 'abc')
|
||||
self.assertTrue(conf.true_var_1)
|
||||
self.assertTrue(conf.true_var_2)
|
||||
self.assertTrue(conf.true_var_3)
|
||||
self.assertTrue(not conf.false_var_1)
|
||||
self.assertTrue(not conf.false_var_2)
|
||||
self.assertTrue(not conf.false_var_3)
|
||||
self.assertEqual(conf.list_1, [])
|
||||
self.assertEqual(conf.list_2, ['abc'])
|
||||
self.assertEqual(conf.list_3, ['abc', 'def', 'ghi'])
|
||||
self.assertEqual(conf.list_4, ['[', 'what', 'now?', ']'])
|
||||
|
||||
def test_simple_gets(self):
|
||||
conf = self.load("simple.conf")
|
||||
self.check_simple_gets(conf)
|
||||
|
||||
def test_type_errors(self):
|
||||
Error = ZConfig.DataConversionError
|
||||
raises = self.assertRaises
|
||||
raises(Error, self.loadtext, "int-var true")
|
||||
raises(Error, self.loadtext, "float-var true")
|
||||
raises(Error, self.loadtext, "neg-int false")
|
||||
raises(Error, self.loadtext, "true-var-1 0")
|
||||
raises(Error, self.loadtext, "true-var-1 1")
|
||||
with raises(Error) as e:
|
||||
self.loadtext("true-var-1 -1")
|
||||
|
||||
# str doesn't fail
|
||||
exc = e.exception
|
||||
str(exc)
|
||||
self.assertIsNone(exc.colno)
|
||||
self.assertIsNone(exc.url)
|
||||
|
||||
exc.colno = 1
|
||||
exc.url = 'url'
|
||||
self.assertIn('url', str(exc))
|
||||
|
||||
def test_simple_sections(self):
|
||||
self.schema = ZConfig.loadSchema(CONFIG_BASE + "simplesections.xml")
|
||||
conf = self.load("simplesections.conf")
|
||||
self.assertEqual(conf.var, "foo")
|
||||
# check each interleaved position between sections
|
||||
for c in "0123456":
|
||||
self.assertEqual(getattr(conf, "var_" +c), "foo-" + c)
|
||||
sect = [sect for sect in conf.sections
|
||||
if sect.getSectionName() == "name"][0]
|
||||
self.assertEqual(sect.var, "bar")
|
||||
self.assertEqual(sect.var_one, "splat")
|
||||
self.assertTrue(sect.var_three is None)
|
||||
sect = [sect for sect in conf.sections
|
||||
if sect.getSectionName() == "delegate"][0]
|
||||
self.assertEqual(sect.var, "spam")
|
||||
self.assertEqual(sect.var_two, "stuff")
|
||||
self.assertTrue(sect.var_three is None)
|
||||
|
||||
def test_include(self):
|
||||
conf = self.load("include.conf")
|
||||
self.assertEqual(conf.var1, "abc")
|
||||
self.assertEqual(conf.var2, "value2")
|
||||
self.assertEqual(conf.var3, "value3")
|
||||
self.assertEqual(conf.var4, "value")
|
||||
|
||||
def test_includes_with_defines(self):
|
||||
self.schema = ZConfig.loadSchemaFile(StringIO("""\
|
||||
<schema>
|
||||
<key name='refinner' />
|
||||
<key name='refouter' />
|
||||
</schema>
|
||||
"""))
|
||||
conf = self.load("outer.conf")
|
||||
self.assertEqual(conf.refinner, "inner")
|
||||
self.assertEqual(conf.refouter, "outer")
|
||||
|
||||
def test_define(self):
|
||||
conf = self.load("simple.conf")
|
||||
self.assertEqual(conf.getname, "value")
|
||||
self.assertEqual(conf.getnametwice, "valuevalue")
|
||||
self.assertEqual(conf.getdollars, "$$")
|
||||
self.assertEqual(conf.getempty, "xy")
|
||||
self.assertEqual(conf.getwords, "abc two words def")
|
||||
|
||||
def test_define_errors(self):
|
||||
# doesn't raise if value is equal
|
||||
self.loadtext("%define a value\n%define a value\n")
|
||||
|
||||
self.assertRaises(ZConfig.ConfigurationSyntaxError,
|
||||
self.loadtext, "%define\n")
|
||||
self.assertRaises(ZConfig.ConfigurationSyntaxError,
|
||||
self.loadtext, "%define abc-def\n")
|
||||
|
||||
self.assertRaises(ZConfig.SubstitutionReplacementError,
|
||||
self.loadtext,
|
||||
"foo $name")
|
||||
|
||||
with self.assertRaises(ZConfig.ConfigurationSyntaxError) as e:
|
||||
self.loadtext("%define a value\n%define a other\n")
|
||||
|
||||
# str doesn't throw unexpected exceptions
|
||||
exc = e.exception
|
||||
self.assertIn('line', str(exc))
|
||||
self.assertNotIn('column', str(exc))
|
||||
# doesn't have these properties
|
||||
self.assertIsNone(exc.colno)
|
||||
self.assertIsNone(exc.url)
|
||||
|
||||
# If we fill them in, we get different str output
|
||||
exc.colno = 10
|
||||
exc.url = 'a url'
|
||||
self.assertIn('column', str(exc))
|
||||
|
||||
# There's also a case if we don't have a line number
|
||||
exc.lineno = None
|
||||
self.assertNotIn('line', str(exc))
|
||||
|
||||
def test_bad_directive(self):
|
||||
self.assertRaisesRegex(ZConfig.ConfigurationSyntaxError,
|
||||
'unknown directive',
|
||||
self.loadtext, '%not a directive')
|
||||
|
||||
self.assertRaisesRegex(ZConfig.ConfigurationSyntaxError,
|
||||
'missing or unrecognized',
|
||||
self.loadtext, '%')
|
||||
|
||||
def test_bad_key(self):
|
||||
self.assertRaisesRegex(ZConfig.ConfigurationSyntaxError,
|
||||
'malformed configuration data',
|
||||
self.loadtext, '(int-var')
|
||||
|
||||
def test_bad_section(self):
|
||||
self.schema = ZConfig.loadSchema(CONFIG_BASE + "simplesections.xml")
|
||||
self.assertRaisesRegex(ZConfig.ConfigurationSyntaxError,
|
||||
'unexpected section end',
|
||||
self.loadtext, '</close>')
|
||||
|
||||
self.assertRaisesRegex(ZConfig.ConfigurationSyntaxError,
|
||||
'unbalanced section end',
|
||||
self.loadtext, '<section>\n</close>')
|
||||
|
||||
self.assertRaisesRegex(ZConfig.ConfigurationSyntaxError,
|
||||
'unclosed sections not allowed',
|
||||
self.loadtext, '<section>\n')
|
||||
|
||||
self.assertRaisesRegex(ZConfig.ConfigurationSyntaxError,
|
||||
'malformed section header',
|
||||
self.loadtext, '<section()>\n</close>')
|
||||
|
||||
self.assertRaisesRegex(ZConfig.ConfigurationSyntaxError,
|
||||
'malformed section end',
|
||||
self.loadtext, '<section>\n</section')
|
||||
|
||||
self.assertRaisesRegex(ZConfig.ConfigurationSyntaxError,
|
||||
'malformed section start',
|
||||
self.loadtext, '<section')
|
||||
|
||||
# ConfigLoader.endSection raises this and it is recaught and changed to a
|
||||
# SyntaxError
|
||||
self.assertRaisesRegex(ZConfig.ConfigurationSyntaxError,
|
||||
"no values for",
|
||||
self.loadtext,
|
||||
"<hasmin foo>\n</hasmin>")
|
||||
|
||||
def test_configuration_error_str(self):
|
||||
|
||||
e = ZConfig.ConfigurationError('message')
|
||||
self.assertEqual(e.message, 'message')
|
||||
self.assertEqual('message', str(e))
|
||||
|
||||
# We can delete the message, for some reason
|
||||
del e.message
|
||||
|
||||
def test_fragment_ident_disallowed(self):
|
||||
self.assertRaises(ZConfig.ConfigurationError,
|
||||
self.load, "simplesections.conf#another")
|
||||
|
||||
def test_load_from_fileobj(self):
|
||||
sio = StringIO("%define name value\n"
|
||||
"getname x $name y \n")
|
||||
cf = self.loadfile(sio)
|
||||
self.assertEqual(cf.getname, "x value y")
|
||||
|
||||
def test_load_from_abspath(self):
|
||||
fn = self.write_tempfile()
|
||||
try:
|
||||
self.check_load_from_path(fn)
|
||||
finally:
|
||||
os.unlink(fn)
|
||||
|
||||
def test_load_from_relpath(self):
|
||||
fn = self.write_tempfile()
|
||||
dirname, name = os.path.split(fn)
|
||||
pwd = os.getcwd()
|
||||
try:
|
||||
os.chdir(dirname)
|
||||
self.check_load_from_path(name)
|
||||
finally:
|
||||
os.chdir(pwd)
|
||||
os.unlink(fn)
|
||||
|
||||
def write_tempfile(self):
|
||||
fn = tempfile.mktemp()
|
||||
fp = open(fn, "w")
|
||||
fp.write("var1 value\n")
|
||||
fp.close()
|
||||
return fn
|
||||
|
||||
def check_load_from_path(self, path):
|
||||
schema = self.get_schema()
|
||||
ZConfig.loadConfig(schema, path)
|
||||
|
||||
|
||||
def test_suite():
|
||||
return unittest.defaultTestLoader.loadTestsFromName(__name__)
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main(defaultTest='test_suite')
|
||||
@@ -0,0 +1,70 @@
|
||||
##############################################################################
|
||||
#
|
||||
# Copyright (c) 2004 Zope Foundation and Contributors.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# This software is subject to the provisions of the Zope Public License,
|
||||
# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
|
||||
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
|
||||
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
|
||||
# FOR A PARTICULAR PURPOSE.
|
||||
#
|
||||
##############################################################################
|
||||
"""Tests of examples from the online cookbook, so we don't break them
|
||||
down the road. Unless we really mean to.
|
||||
|
||||
The ZConfig Cookbook is available online at:
|
||||
|
||||
http://dev.zope.org/Zope3/ZConfig
|
||||
|
||||
"""
|
||||
|
||||
import ZConfig.tests.support
|
||||
import unittest
|
||||
|
||||
|
||||
def basic_key_mapping_password_to_passwd(key):
|
||||
# Lower-case the key since that's what basic-key does:
|
||||
key = key.lower()
|
||||
# Now map password to passwd:
|
||||
if key == "password":
|
||||
key = "passwd"
|
||||
return key
|
||||
|
||||
def user_info_conversion(section):
|
||||
return section
|
||||
|
||||
|
||||
class CookbookTestCase(ZConfig.tests.support.TestHelper, unittest.TestCase):
|
||||
|
||||
def test_rewriting_key_names(self):
|
||||
schema = self.load_schema_text("""
|
||||
<schema prefix='%s'>
|
||||
<sectiontype name='userinfo' datatype='.user_info_conversion'
|
||||
keytype='.basic_key_mapping_password_to_passwd'>
|
||||
<key name='userid' datatype='integer'/>
|
||||
<key name='username' datatype='identifier'/>
|
||||
<key name='password'/>
|
||||
</sectiontype>
|
||||
<section type='userinfo' name='*' attribute='userinfo'/>
|
||||
</schema>
|
||||
""" % __name__)
|
||||
config = self.load_config_text(schema, """\
|
||||
<userinfo>
|
||||
USERID 42
|
||||
USERNAME foouser
|
||||
PASSWORD yeah-right
|
||||
</userinfo>
|
||||
""")
|
||||
self.assertEqual(config.userinfo.userid, 42)
|
||||
self.assertEqual(config.userinfo.username, "foouser")
|
||||
self.assertEqual(config.userinfo.passwd, "yeah-right")
|
||||
self.assertTrue(not hasattr(config.userinfo, "password"))
|
||||
|
||||
|
||||
def test_suite():
|
||||
return unittest.defaultTestLoader.loadTestsFromName(__name__)
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main(defaultTest='test_suite')
|
||||
@@ -0,0 +1,435 @@
|
||||
##############################################################################
|
||||
#
|
||||
# Copyright (c) 2002, 2003 Zope Foundation and Contributors.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# This software is subject to the provisions of the Zope Public License,
|
||||
# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
|
||||
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
|
||||
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
|
||||
# FOR A PARTICULAR PURPOSE.
|
||||
#
|
||||
##############################################################################
|
||||
"""Tests of standard ZConfig datatypes."""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import shutil
|
||||
import socket
|
||||
import datetime
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
import ZConfig.datatypes
|
||||
|
||||
from ZConfig.tests.support import TestHelper
|
||||
|
||||
here = os.path.abspath(__file__)
|
||||
|
||||
try:
|
||||
unicode
|
||||
except NameError:
|
||||
have_unicode = False
|
||||
else:
|
||||
have_unicode = True
|
||||
|
||||
|
||||
class DatatypeTestCase(unittest.TestCase):
|
||||
types = ZConfig.datatypes.Registry()
|
||||
|
||||
def test_datatype_basickey(self):
|
||||
convert = self.types.get("basic-key")
|
||||
eq = self.assertEqual
|
||||
raises = self.assertRaises
|
||||
|
||||
eq(convert("abc"), "abc")
|
||||
eq(convert("ABC_DEF.123"), "abc_def.123")
|
||||
eq(convert("Abc-Def-456"), "abc-def-456")
|
||||
eq(convert("Abc.Def"), "abc.def")
|
||||
|
||||
raises(ValueError, convert, "_abc")
|
||||
raises(ValueError, convert, "-abc")
|
||||
raises(ValueError, convert, "123")
|
||||
raises(ValueError, convert, "")
|
||||
|
||||
def test_datatype_boolean(self):
|
||||
convert = self.types.get("boolean")
|
||||
check = self.assertTrue
|
||||
raises = self.assertRaises
|
||||
|
||||
check(convert("on"))
|
||||
check(convert("true"))
|
||||
check(convert("yes"))
|
||||
check(not convert("off"))
|
||||
check(not convert("false"))
|
||||
check(not convert("no"))
|
||||
raises(ValueError, convert, '0')
|
||||
raises(ValueError, convert, '1')
|
||||
raises(ValueError, convert, '')
|
||||
raises(ValueError, convert, 'junk')
|
||||
|
||||
def test_datatype_float(self):
|
||||
convert = self.types.get("float")
|
||||
eq = self.assertEqual
|
||||
raises = self.assertRaises
|
||||
|
||||
eq(convert("1"), 1.0)
|
||||
self.assertTrue(type(convert(1)) is type(1.0))
|
||||
eq(convert("1.1"), 1.1)
|
||||
eq(convert("50.50"), 50.50)
|
||||
eq(convert("-50.50"), -50.50)
|
||||
eq(convert(0), 0.0)
|
||||
eq(convert("0"), 0.0)
|
||||
eq(convert("-0"), 0.0)
|
||||
eq(convert("0.0"), 0.0)
|
||||
|
||||
raises(ValueError, convert, "junk")
|
||||
raises(ValueError, convert, "0x234.1.9")
|
||||
raises(ValueError, convert, "0.9-")
|
||||
|
||||
# float handles inf/nan portably in both bytes and
|
||||
# unicode on both Python 2.6+ and Python 3. Make sure conversion
|
||||
# does too.
|
||||
for literal in ("inf", "-inf", b"inf", b"-inf"):
|
||||
eq(convert(literal), float(literal))
|
||||
|
||||
# notably, nan is not equal to itself
|
||||
self.assertNotEqual(convert("nan"), float("nan"))
|
||||
self.assertNotEqual(convert(b"nan"), float(b"nan"))
|
||||
|
||||
def test_datatype_identifier(self):
|
||||
convert = self.types.get("identifier")
|
||||
raises = self.assertRaises
|
||||
self.check_names(convert)
|
||||
self.check_never_namelike(convert)
|
||||
raises(ValueError, convert, ".abc")
|
||||
|
||||
def check_names(self, convert):
|
||||
eq = self.assert_ascii_equal
|
||||
eq(convert, "AbcDef")
|
||||
eq(convert, "a________")
|
||||
eq(convert, "abc_def")
|
||||
eq(convert, "int123")
|
||||
eq(convert, "_abc")
|
||||
eq(convert, "_123")
|
||||
eq(convert, "__dict__")
|
||||
|
||||
def assert_ascii_equal(self, convert, value):
|
||||
v = convert(value)
|
||||
self.assertEqual(v, value)
|
||||
self.assertTrue(isinstance(v, str))
|
||||
if have_unicode:
|
||||
unicode_value = unicode(value)
|
||||
v = convert(unicode_value)
|
||||
self.assertEqual(v, value)
|
||||
self.assertTrue(isinstance(v, str))
|
||||
|
||||
def check_never_namelike(self, convert):
|
||||
raises = self.assertRaises
|
||||
raises(ValueError, convert, "2345")
|
||||
raises(ValueError, convert, "23.45")
|
||||
raises(ValueError, convert, ".45")
|
||||
raises(ValueError, convert, "23.")
|
||||
raises(ValueError, convert, "abc.")
|
||||
raises(ValueError, convert, "-abc")
|
||||
raises(ValueError, convert, "-123")
|
||||
raises(ValueError, convert, "abc-")
|
||||
raises(ValueError, convert, "123-")
|
||||
raises(ValueError, convert, "-")
|
||||
raises(ValueError, convert, ".")
|
||||
raises(ValueError, convert, "&%$*()")
|
||||
raises(ValueError, convert, "")
|
||||
|
||||
def test_datatype_dotted_name(self):
|
||||
convert = self.types.get("dotted-name")
|
||||
raises = self.assertRaises
|
||||
self.check_names(convert)
|
||||
self.check_dotted_names(convert)
|
||||
self.check_never_namelike(convert)
|
||||
raises(ValueError, convert, "abc.")
|
||||
raises(ValueError, convert, ".abc.")
|
||||
raises(ValueError, convert, "abc.def.")
|
||||
raises(ValueError, convert, ".abc.def.")
|
||||
raises(ValueError, convert, ".abc.def")
|
||||
|
||||
def test_datatype_dotted_suffix(self):
|
||||
convert = self.types.get("dotted-suffix")
|
||||
eq = self.assert_ascii_equal
|
||||
raises = self.assertRaises
|
||||
self.check_names(convert)
|
||||
self.check_dotted_names(convert)
|
||||
self.check_never_namelike(convert)
|
||||
eq(convert, ".a")
|
||||
eq(convert, ".a.b")
|
||||
eq(convert, ".a.b.c.d.e.f.g.h.i.j.k.l.m.n.o")
|
||||
raises(ValueError, convert, "abc.")
|
||||
raises(ValueError, convert, ".abc.")
|
||||
raises(ValueError, convert, "abc.def.")
|
||||
raises(ValueError, convert, ".abc.def.")
|
||||
|
||||
def check_dotted_names(self, convert):
|
||||
eq = self.assert_ascii_equal
|
||||
eq(convert, "abc.def")
|
||||
eq(convert, "abc.def.ghi")
|
||||
eq(convert, "a.d.g.g.g.g.g.g.g")
|
||||
|
||||
def test_datatype_inet_address(self):
|
||||
convert = self.types.get("inet-address")
|
||||
eq = self.assertEqual
|
||||
defhost = ZConfig.datatypes.DEFAULT_HOST
|
||||
eq(convert("Host.Example.Com:80"), ("host.example.com", 80))
|
||||
eq(convert("Host.Example.Com:0"), ("host.example.com", 0))
|
||||
eq(convert(":80"), (defhost, 80))
|
||||
eq(convert("80"), (defhost, 80))
|
||||
eq(convert("[::1]:80"), ("::1", 80))
|
||||
eq(convert("host.EXAMPLE.com"), ("host.example.com", None))
|
||||
eq(convert("2001::ABCD"), ("2001::abcd", None))
|
||||
self.assertRaises(ValueError, convert, "40 # foo")
|
||||
|
||||
def test_datatype_inet_binding_address(self):
|
||||
convert = self.types.get("inet-binding-address")
|
||||
eq = self.assertEqual
|
||||
defhost = ""
|
||||
eq(convert("Host.Example.Com:80"), ("host.example.com", 80))
|
||||
eq(convert(":80"), (defhost, 80))
|
||||
eq(convert("80"), (defhost, 80))
|
||||
eq(convert("host.EXAMPLE.com"), ("host.example.com", None))
|
||||
self.assertRaises(ValueError, convert, "40 # foo")
|
||||
|
||||
def test_datatype_inet_connection_address(self):
|
||||
convert = self.types.get("inet-connection-address")
|
||||
eq = self.assertEqual
|
||||
defhost = "127.0.0.1"
|
||||
eq(convert("Host.Example.Com:80"), ("host.example.com", 80))
|
||||
eq(convert(":80"), (defhost, 80))
|
||||
eq(convert("80"), (defhost, 80))
|
||||
eq(convert("host.EXAMPLE.com"), ("host.example.com", None))
|
||||
self.assertRaises(ValueError, convert, "40 # foo")
|
||||
|
||||
def test_datatype_integer(self):
|
||||
convert = self.types.get("integer")
|
||||
eq = self.assertEqual
|
||||
raises = self.assertRaises
|
||||
|
||||
eq(convert('-100'), -100)
|
||||
eq(convert('-1'), -1)
|
||||
eq(convert('-0'), 0)
|
||||
eq(convert('0'), 0)
|
||||
eq(convert('1'), 1)
|
||||
eq(convert('100'), 100)
|
||||
eq(convert('65535'), 65535)
|
||||
eq(convert('65536'), 65536)
|
||||
|
||||
raises(ValueError, convert, 'abc')
|
||||
raises(ValueError, convert, '-0xabc')
|
||||
raises(ValueError, convert, '')
|
||||
raises(ValueError, convert, '123 456')
|
||||
raises(ValueError, convert, '123-')
|
||||
|
||||
def test_datatype_locale(self):
|
||||
convert = self.types.get("locale")
|
||||
# Python supports "C" even when the _locale module is not available
|
||||
self.assertEqual(convert("C"), "C")
|
||||
self.assertRaises(ValueError, convert, "locale-does-not-exist")
|
||||
|
||||
def test_datatype_port(self):
|
||||
convert = self.types.get("port-number")
|
||||
eq = self.assertEqual
|
||||
raises = self.assertRaises
|
||||
|
||||
raises(ValueError, convert, '-1')
|
||||
eq(convert('0'), 0)
|
||||
eq(convert('1'), 1)
|
||||
eq(convert('80'), 80)
|
||||
eq(convert('1023'), 1023)
|
||||
eq(convert('1024'), 1024)
|
||||
eq(convert('60000'), 60000)
|
||||
eq(convert('65535'), 0xffff)
|
||||
raises(ValueError, convert, '65536')
|
||||
|
||||
def test_datatype_socket_address(self):
|
||||
convert = self.types.get("socket-address")
|
||||
eq = self.assertEqual
|
||||
AF_INET = socket.AF_INET
|
||||
AF_INET6 = socket.AF_INET6
|
||||
defhost = ZConfig.datatypes.DEFAULT_HOST
|
||||
|
||||
def check(value, family, address):
|
||||
a = convert(value)
|
||||
self.assertEqual(a.family, family)
|
||||
self.assertEqual(a.address, address)
|
||||
|
||||
check("Host.Example.Com:80", AF_INET, ("host.example.com", 80))
|
||||
check(":80", AF_INET, (defhost, 80))
|
||||
check("80", AF_INET, (defhost, 80))
|
||||
check("host.EXAMPLE.com", AF_INET, ("host.example.com",None))
|
||||
check("::1", AF_INET6,("::1", None))
|
||||
check("[::]:80", AF_INET6,("::", 80))
|
||||
a1 = convert("/tmp/var/@345.4")
|
||||
a2 = convert("/tmp/var/@345.4:80")
|
||||
self.assertEqual(a1.address, "/tmp/var/@345.4")
|
||||
self.assertEqual(a2.address, "/tmp/var/@345.4:80")
|
||||
if hasattr(socket, "AF_UNIX"):
|
||||
self.assertEqual(a1.family, socket.AF_UNIX)
|
||||
self.assertEqual(a2.family, socket.AF_UNIX)
|
||||
else: # pragma: no cover
|
||||
self.assertTrue(a1.family is None)
|
||||
self.assertTrue(a2.family is None)
|
||||
|
||||
convert = self.types.get('socket-binding-address')
|
||||
check(":80", AF_INET, (defhost, 80))
|
||||
|
||||
convert = self.types.get('socket-connection-address')
|
||||
check(":80", AF_INET, ("127.0.0.1", 80))
|
||||
|
||||
def test_ipaddr_or_hostname(self):
|
||||
convert = self.types.get('ipaddr-or-hostname')
|
||||
eq = self.assertEqual
|
||||
raises = self.assertRaises
|
||||
eq(convert('hostname'), 'hostname')
|
||||
eq(convert('hostname.com'), 'hostname.com')
|
||||
eq(convert('www.hostname.com'), 'www.hostname.com')
|
||||
eq(convert('HOSTNAME'), 'hostname')
|
||||
eq(convert('HOSTNAME.COM'), 'hostname.com')
|
||||
eq(convert('WWW.HOSTNAME.COM'), 'www.hostname.com')
|
||||
eq(convert('127.0.0.1'), '127.0.0.1')
|
||||
eq(convert('::1'), '::1')
|
||||
eq(convert('2001:DB8:1234:4567:89AB:cdef:0:1'), '2001:db8:1234:4567:89ab:cdef:0:1')
|
||||
eq(convert('2001:DB8:1234:4567::10.11.12.13'), '2001:db8:1234:4567::10.11.12.13')
|
||||
raises(ValueError, convert, '1hostnamewithleadingnumeric')
|
||||
raises(ValueError, convert, '255.255')
|
||||
raises(ValueError, convert, '12345678')
|
||||
raises(ValueError, convert, '999.999.999.999')
|
||||
raises(ValueError, convert, 'a!badhostname')
|
||||
raises(ValueError, convert, '2001:DB8:0123:4567:89AB:cdef:0:1:2')
|
||||
raises(ValueError, convert, '2001:DB8:0123:4567::10.11.12.13.14')
|
||||
|
||||
def test_existing_directory(self):
|
||||
convert = self.types.get('existing-directory')
|
||||
eq = self.assertEqual
|
||||
raises = self.assertRaises
|
||||
eq(convert('.'), '.')
|
||||
eq(convert(os.path.dirname(here)), os.path.dirname(here))
|
||||
raises(ValueError, convert, tempfile.mktemp())
|
||||
|
||||
def test_existing_file(self):
|
||||
convert = self.types.get('existing-file')
|
||||
eq = self.assertEqual
|
||||
raises = self.assertRaises
|
||||
eq(convert('.'), '.')
|
||||
eq(convert(here), here)
|
||||
raises(ValueError, convert, tempfile.mktemp())
|
||||
|
||||
def test_existing_path(self):
|
||||
convert = self.types.get('existing-path')
|
||||
eq = self.assertEqual
|
||||
raises = self.assertRaises
|
||||
eq(convert('.'), '.')
|
||||
eq(convert(here), here)
|
||||
eq(convert(os.path.dirname(here)), os.path.dirname(here))
|
||||
raises(ValueError, convert, tempfile.mktemp())
|
||||
|
||||
def test_existing_dirpath(self):
|
||||
convert = self.types.get('existing-dirpath')
|
||||
eq = self.assertEqual
|
||||
raises = self.assertRaises
|
||||
eq(convert('.'), '.')
|
||||
eq(convert(here), here)
|
||||
raises(ValueError, convert, '/a/hopefully/nonexistent/path')
|
||||
raises(ValueError, convert, here + '/bogus')
|
||||
|
||||
def test_byte_size(self):
|
||||
eq = self.assertEqual
|
||||
raises = self.assertRaises
|
||||
convert = self.types.get('byte-size')
|
||||
eq(convert('128'), 128)
|
||||
eq(convert('128KB'), 128*1024)
|
||||
eq(convert('128MB'), 128*1024*1024)
|
||||
eq(convert('128GB'), 128*1024*1024*1024)
|
||||
raises(ValueError, convert, '128TB')
|
||||
eq(convert('128'), 128)
|
||||
eq(convert('128kb'), 128*1024)
|
||||
eq(convert('128mb'), 128*1024*1024)
|
||||
eq(convert('128gb'), 128*1024*1024*1024)
|
||||
raises(ValueError, convert, '128tb')
|
||||
|
||||
def test_time_interval(self):
|
||||
eq = self.assertEqual
|
||||
raises = self.assertRaises
|
||||
convert = self.types.get('time-interval')
|
||||
eq(convert('120'), 120)
|
||||
eq(convert('120S'), 120)
|
||||
eq(convert('120M'), 120*60)
|
||||
eq(convert('120H'), 120*60*60)
|
||||
eq(convert('120D'), 120*60*60*24)
|
||||
raises(ValueError, convert, '120W')
|
||||
eq(convert('120'), 120)
|
||||
eq(convert('120s'), 120)
|
||||
eq(convert('120m'), 120*60)
|
||||
eq(convert('120h'), 120*60*60)
|
||||
eq(convert('120d'), 120*60*60*24)
|
||||
raises(ValueError, convert, '120w')
|
||||
|
||||
def test_timedelta(self):
|
||||
eq = self.assertEqual
|
||||
raises = self.assertRaises
|
||||
convert = self.types.get('timedelta')
|
||||
eq(convert('4w'), datetime.timedelta(weeks=4))
|
||||
eq(convert('2d'), datetime.timedelta(days=2))
|
||||
eq(convert('7h'), datetime.timedelta(hours=7))
|
||||
eq(convert('12m'), datetime.timedelta(minutes=12))
|
||||
eq(convert('14s'), datetime.timedelta(seconds=14))
|
||||
eq(convert('4w 2d 7h 12m 14s'),
|
||||
datetime.timedelta(2, 14, minutes=12, hours=7, weeks=4))
|
||||
|
||||
raises(TypeError, convert, '1y')
|
||||
|
||||
class RegistryTestCase(TestHelper, unittest.TestCase):
|
||||
|
||||
def test_registry_does_not_mask_toplevel_imports(self):
|
||||
old_sys_path = sys.path[:]
|
||||
tmpdir = tempfile.mkdtemp(prefix="test_datatypes_")
|
||||
fn = os.path.join(tmpdir, "datatypes.py")
|
||||
f = open(fn, "w")
|
||||
f.write(TEST_DATATYPE_SOURCE)
|
||||
f.close()
|
||||
registry = ZConfig.datatypes.Registry()
|
||||
|
||||
# we really want the temp area to override everything else:
|
||||
sys.path.insert(0, tmpdir)
|
||||
try:
|
||||
datatype = registry.get("datatypes.my_sample_datatype")
|
||||
finally:
|
||||
shutil.rmtree(tmpdir)
|
||||
sys.path[:] = old_sys_path
|
||||
self.assertEqual(datatype, 42)
|
||||
|
||||
|
||||
def test_register_shadow(self):
|
||||
reg = ZConfig.datatypes.Registry()
|
||||
self.assertRaisesRegex(ValueError,
|
||||
"conflicts with built-in type",
|
||||
reg.register,
|
||||
'integer', None)
|
||||
|
||||
reg.register("foobar", None)
|
||||
self.assertRaisesRegex(ValueError,
|
||||
"already registered",
|
||||
reg.register,
|
||||
'foobar', None)
|
||||
|
||||
def test_get_fallback_basic_key(self):
|
||||
reg = ZConfig.datatypes.Registry({})
|
||||
self.assertIsNone(reg._basic_key)
|
||||
self.assertRaisesRegex(ValueError,
|
||||
"unloadable datatype name",
|
||||
reg.get,
|
||||
'integer')
|
||||
self.assertIsNotNone(reg._basic_key)
|
||||
|
||||
TEST_DATATYPE_SOURCE = """
|
||||
# sample datatypes file
|
||||
|
||||
my_sample_datatype = 42
|
||||
"""
|
||||
@@ -0,0 +1,216 @@
|
||||
##############################################################################
|
||||
#
|
||||
# Copyright (c) 2017 Zope Foundation and Contributors.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# This software is subject to the provisions of the Zope Public License,
|
||||
# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
|
||||
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
|
||||
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
|
||||
# FOR A PARTICULAR PURPOSE.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
import unittest
|
||||
|
||||
from ZConfig import SchemaError
|
||||
from ZConfig import ConfigurationError
|
||||
|
||||
from ZConfig.info import Unbounded
|
||||
from ZConfig.info import BaseInfo
|
||||
from ZConfig.info import BaseKeyInfo
|
||||
from ZConfig.info import KeyInfo
|
||||
from ZConfig.info import SectionInfo
|
||||
from ZConfig.info import AbstractType
|
||||
from ZConfig.info import SectionType
|
||||
from ZConfig.info import SchemaType
|
||||
|
||||
from ZConfig.tests.support import TestHelper
|
||||
|
||||
|
||||
class UnboundTestCase(unittest.TestCase):
|
||||
|
||||
def test_order(self):
|
||||
self.assertGreater(Unbounded, self)
|
||||
self.assertFalse(Unbounded > Unbounded)
|
||||
self.assertEqual(Unbounded, Unbounded)
|
||||
|
||||
class InfoMixin(TestHelper):
|
||||
|
||||
Class = None
|
||||
|
||||
default_kwargs = {'name': '', 'datatype': None, 'handler': None,
|
||||
'minOccurs': 0, 'maxOccurs': Unbounded, 'attribute': None}
|
||||
|
||||
def make_one(self, **kwargs):
|
||||
args = self.default_kwargs.copy()
|
||||
args.update(kwargs)
|
||||
return self.Class(**args)
|
||||
|
||||
|
||||
class BaseInfoTestCase(InfoMixin, unittest.TestCase):
|
||||
|
||||
Class = BaseInfo
|
||||
|
||||
def test_constructor_error(self):
|
||||
self.assertRaisesRegex(SchemaError,
|
||||
'maxOccurs',
|
||||
self.make_one,
|
||||
maxOccurs=0, minOccurs=0)
|
||||
|
||||
# This case doesn't really make sense
|
||||
self.assertRaisesRegex(SchemaError,
|
||||
'minOccurs',
|
||||
self.make_one,
|
||||
maxOccurs=1,
|
||||
minOccurs=2)
|
||||
|
||||
def test_repr(self):
|
||||
# just doesn't raise
|
||||
repr(self.make_one())
|
||||
|
||||
class BaseKeyInfoTestCase(InfoMixin, unittest.TestCase):
|
||||
|
||||
class Class(BaseKeyInfo):
|
||||
def add_valueinfo(self, vi, key):
|
||||
"This wont actually be called"
|
||||
|
||||
def test_cant_instantiate(self):
|
||||
self.Class = BaseKeyInfo
|
||||
with self.assertRaises(TypeError):
|
||||
self.make_one()
|
||||
del self.Class
|
||||
|
||||
def test_finish(self):
|
||||
info = self.make_one(minOccurs=1)
|
||||
info.finish()
|
||||
with self.assertRaises(SchemaError):
|
||||
info.finish()
|
||||
|
||||
def test_adddefaultc(self):
|
||||
info = self.make_one(name='foo', minOccurs=1)
|
||||
self.assertRaisesRegex(SchemaError,
|
||||
'unexpected key for default',
|
||||
info.adddefault,
|
||||
None, None, key='key')
|
||||
|
||||
class KeyInfoTestCase(InfoMixin, unittest.TestCase):
|
||||
|
||||
Class = KeyInfo
|
||||
default_kwargs = InfoMixin.default_kwargs.copy()
|
||||
default_kwargs.pop('maxOccurs')
|
||||
|
||||
def test_add_with_default(self):
|
||||
info = self.make_one(minOccurs=1, name='name')
|
||||
info.adddefault('value', None)
|
||||
self.assertRaisesRegex(SchemaError,
|
||||
'cannot set more than one',
|
||||
info.adddefault,
|
||||
'value', None)
|
||||
|
||||
class SectionInfoTestCase(InfoMixin, unittest.TestCase):
|
||||
|
||||
Class = SectionInfo
|
||||
|
||||
class MockSectionType(object):
|
||||
name = None
|
||||
@classmethod
|
||||
def isabstract(cls):
|
||||
return True
|
||||
|
||||
default_kwargs = InfoMixin.default_kwargs.copy()
|
||||
default_kwargs.pop('datatype')
|
||||
default_kwargs['sectiontype'] = MockSectionType
|
||||
|
||||
def test_constructor_error(self):
|
||||
self.assertRaisesRegex(SchemaError,
|
||||
'must use a name',
|
||||
self.make_one,
|
||||
name='name', maxOccurs=2)
|
||||
self.assertRaisesRegex(SchemaError,
|
||||
'must specify a target attribute',
|
||||
self.make_one,
|
||||
name='*', maxOccurs=2)
|
||||
|
||||
def test_misc(self):
|
||||
info = self.make_one(maxOccurs=1)
|
||||
repr(info)
|
||||
self.assertFalse(info.isAllowedName('*'))
|
||||
self.assertFalse(info.isAllowedName('+'))
|
||||
|
||||
class AbstractTypeTestCase(unittest.TestCase):
|
||||
|
||||
def test_subtypes(self):
|
||||
|
||||
t = AbstractType('name')
|
||||
self.assertFalse(t.hassubtype('foo'))
|
||||
self.assertEqual([], list(t.getsubtypenames()))
|
||||
|
||||
self.name = 'foo'
|
||||
t.addsubtype(self)
|
||||
self.assertTrue(t.hassubtype('foo'))
|
||||
|
||||
class SectionTypeTestCase(TestHelper, unittest.TestCase):
|
||||
|
||||
def make_one(self, name='', keytype=None, valuetype=None,
|
||||
datatype=None, registry={}, types=None):
|
||||
return SectionType(name, keytype, valuetype, datatype, registry, types)
|
||||
|
||||
def test_getinfo_no_key(self):
|
||||
info = self.make_one()
|
||||
self.assertRaisesRegex(ConfigurationError,
|
||||
"cannot match a key without a name",
|
||||
info.getinfo,
|
||||
None)
|
||||
|
||||
def test_required_types_with_name(self):
|
||||
info = self.make_one(name='foo')
|
||||
self.assertEqual(['foo'], info.getrequiredtypes())
|
||||
|
||||
def test_getsectioninfo(self):
|
||||
class MockChild(object):
|
||||
_issection = False
|
||||
def issection(self):
|
||||
return self._issection
|
||||
child = MockChild()
|
||||
|
||||
info = self.make_one()
|
||||
|
||||
info._children.append(('foo', child))
|
||||
|
||||
self.assertRaisesRegex(ConfigurationError,
|
||||
'already in use for key',
|
||||
info.getsectioninfo,
|
||||
None, 'foo')
|
||||
|
||||
self.assertRaisesRegex(ConfigurationError,
|
||||
'no matching section',
|
||||
info.getsectioninfo,
|
||||
None, 'baz')
|
||||
|
||||
class SchemaTypeTestCase(TestHelper, unittest.TestCase):
|
||||
|
||||
def test_various(self):
|
||||
class Mock(object):
|
||||
pass
|
||||
|
||||
mock = Mock()
|
||||
schema = SchemaType(None, None, None, None, 'url', {})
|
||||
|
||||
mock.name = 'name'
|
||||
schema.addtype(mock)
|
||||
with self.assertRaises(SchemaError):
|
||||
schema.addtype(mock)
|
||||
|
||||
self.assertTrue(schema.allowUnnamed())
|
||||
self.assertFalse(schema.isAllowedName(None))
|
||||
|
||||
with self.assertRaises(SchemaError):
|
||||
schema.deriveSectionType(schema, None, None, None, None)
|
||||
|
||||
schema.addComponent('name')
|
||||
self.assertRaisesRegex(SchemaError,
|
||||
'already have component',
|
||||
schema.addComponent,
|
||||
'name')
|
||||
@@ -0,0 +1,420 @@
|
||||
##############################################################################
|
||||
#
|
||||
# Copyright (c) 2002, 2003 Zope Foundation and Contributors.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# This software is subject to the provisions of the Zope Public License,
|
||||
# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
|
||||
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
|
||||
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
|
||||
# FOR A PARTICULAR PURPOSE.
|
||||
#
|
||||
##############################################################################
|
||||
"""Tests of ZConfig.loader classes and helper functions."""
|
||||
|
||||
import os.path
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
import ZConfig
|
||||
import ZConfig.loader
|
||||
import ZConfig.url
|
||||
|
||||
from ZConfig._compat import NStringIO as StringIO
|
||||
from ZConfig._compat import urllib2
|
||||
|
||||
from ZConfig.tests.support import CONFIG_BASE, TestHelper
|
||||
|
||||
|
||||
myfile = os.path.abspath(__file__)
|
||||
LIBRARY_DIR = os.path.join(os.path.dirname(myfile), "library")
|
||||
|
||||
|
||||
class LoaderTestCase(TestHelper, unittest.TestCase):
|
||||
|
||||
def test_open_resource_non_ascii(self):
|
||||
# Files are decoded using utf-8 on open
|
||||
loader = ZConfig.loader.SchemaLoader()
|
||||
url = ZConfig.url.urljoin(CONFIG_BASE, "non-ascii.txt")
|
||||
stream = loader.openResource(url)
|
||||
val = stream.read()
|
||||
self.assertEqual(
|
||||
val,
|
||||
u'# -*-coding: utf-8; mode: conf-*-\n'
|
||||
u'This file contains a snowman, U+2603: \u2603\n'
|
||||
)
|
||||
|
||||
def test_schema_caching(self):
|
||||
loader = ZConfig.loader.SchemaLoader()
|
||||
url = ZConfig.url.urljoin(CONFIG_BASE, "simple.xml")
|
||||
schema1 = loader.loadURL(url)
|
||||
schema2 = loader.loadURL(url)
|
||||
self.assertIs(schema1, schema2)
|
||||
|
||||
def test_simple_import_with_cache(self):
|
||||
loader = ZConfig.loader.SchemaLoader()
|
||||
url1 = ZConfig.url.urljoin(CONFIG_BASE, "library.xml")
|
||||
schema1 = loader.loadURL(url1)
|
||||
sio = StringIO("<schema>"
|
||||
" <import src='library.xml'/>"
|
||||
" <section type='type-a' name='section'/>"
|
||||
"</schema>")
|
||||
url2 = ZConfig.url.urljoin(CONFIG_BASE, "stringio")
|
||||
schema2 = loader.loadFile(sio, url2)
|
||||
self.assertTrue(schema1.gettype("type-a") is schema2.gettype("type-a"))
|
||||
|
||||
def test_schema_loader_source_errors(self):
|
||||
loader = ZConfig.loader.SchemaLoader()
|
||||
self.assertRaisesRegex(ZConfig.SchemaError,
|
||||
"illegal schema component name",
|
||||
loader.schemaComponentSource,
|
||||
'', None)
|
||||
self.assertRaisesRegex(ZConfig.SchemaError,
|
||||
"illegal schema component name",
|
||||
loader.schemaComponentSource,
|
||||
'foo..bar', None)
|
||||
|
||||
def test_config_loader_abstract_schema(self):
|
||||
class MockSchema(object):
|
||||
_abstract = True
|
||||
def isabstract(self):
|
||||
return self._abstract
|
||||
def gettype(self, _t):
|
||||
return self
|
||||
|
||||
self.assertRaisesRegex(ZConfig.SchemaError,
|
||||
"abstract type",
|
||||
ZConfig.loader.ConfigLoader,
|
||||
MockSchema())
|
||||
|
||||
s = MockSchema()
|
||||
s._abstract = False
|
||||
|
||||
loader = ZConfig.loader.ConfigLoader(s)
|
||||
s._abstract = True
|
||||
|
||||
self.assertRaisesRegex(ZConfig.ConfigurationError,
|
||||
"cannot match abstract section",
|
||||
loader.startSection,
|
||||
None, None, None)
|
||||
|
||||
def test_simple_import_using_prefix(self):
|
||||
self.load_schema_text("""\
|
||||
<schema prefix='ZConfig.tests.library'>
|
||||
<import package='.thing'/>
|
||||
</schema>
|
||||
""")
|
||||
|
||||
def test_import_errors(self):
|
||||
# must specify exactly one of package or src
|
||||
self.assertRaises(ZConfig.SchemaError, ZConfig.loadSchemaFile,
|
||||
StringIO("<schema><import/></schema>"))
|
||||
self.assertRaises(ZConfig.SchemaError, ZConfig.loadSchemaFile,
|
||||
StringIO("<schema>"
|
||||
" <import src='library.xml'"
|
||||
" package='ZConfig'/>"
|
||||
"</schema>"))
|
||||
# cannot specify src and file
|
||||
self.assertRaises(ZConfig.SchemaError, ZConfig.loadSchemaFile,
|
||||
StringIO("<schema>"
|
||||
" <import src='library.xml'"
|
||||
" file='other.xml'/>"
|
||||
"</schema>"))
|
||||
# cannot specify module as package
|
||||
sio = StringIO("<schema>"
|
||||
" <import package='ZConfig.tests.test_loader'/>"
|
||||
"</schema>")
|
||||
with self.assertRaises(ZConfig.SchemaResourceError) as ctx:
|
||||
ZConfig.loadSchemaFile(sio)
|
||||
|
||||
e = ctx.exception
|
||||
self.assertEqual(e.filename, "component.xml")
|
||||
self.assertEqual(e.package, "ZConfig.tests.test_loader")
|
||||
self.assertTrue(e.path is None)
|
||||
# make sure the str() doesn't raise an unexpected exception
|
||||
str(e)
|
||||
|
||||
def test_import_from_package(self):
|
||||
loader = ZConfig.loader.SchemaLoader()
|
||||
sio = StringIO("<schema>"
|
||||
" <import package='ZConfig.tests.library.widget'/>"
|
||||
"</schema>")
|
||||
schema = loader.loadFile(sio)
|
||||
self.assertTrue(schema.gettype("widget-a") is not None)
|
||||
|
||||
def test_import_from_package_with_file(self):
|
||||
loader = ZConfig.loader.SchemaLoader()
|
||||
sio = StringIO("<schema>"
|
||||
" <import package='ZConfig.tests.library.widget'"
|
||||
" file='extra.xml' />"
|
||||
"</schema>")
|
||||
schema = loader.loadFile(sio)
|
||||
self.assertTrue(schema.gettype("extra-type") is not None)
|
||||
|
||||
def test_import_from_package_extra_directory(self):
|
||||
loader = ZConfig.loader.SchemaLoader()
|
||||
sio = StringIO("<schema>"
|
||||
" <import package='ZConfig.tests.library.thing'"
|
||||
" file='extras.xml' />"
|
||||
"</schema>")
|
||||
schema = loader.loadFile(sio)
|
||||
self.assertTrue(schema.gettype("extra-thing") is not None)
|
||||
|
||||
def test_import_from_package_with_missing_file(self):
|
||||
loader = ZConfig.loader.SchemaLoader()
|
||||
sio = StringIO("<schema>"
|
||||
" <import package='ZConfig.tests.library.widget'"
|
||||
" file='notthere.xml' />"
|
||||
"</schema>")
|
||||
with self.assertRaises(ZConfig.SchemaResourceError) as ctx:
|
||||
loader.loadFile(sio)
|
||||
e = ctx.exception
|
||||
self.assertEqual(e.filename, "notthere.xml")
|
||||
self.assertEqual(e.package, "ZConfig.tests.library.widget")
|
||||
self.assertTrue(e.path)
|
||||
# make sure the str() doesn't raise an unexpected exception
|
||||
str(e)
|
||||
|
||||
def test_import_from_package_with_directory_file(self):
|
||||
loader = ZConfig.loader.SchemaLoader()
|
||||
sio = StringIO("<schema>"
|
||||
" <import package='ZConfig.tests.library.widget'"
|
||||
" file='really/notthere.xml' />"
|
||||
"</schema>")
|
||||
self.assertRaises(ZConfig.SchemaError, loader.loadFile, sio)
|
||||
|
||||
def test_import_two_components_one_package(self):
|
||||
loader = ZConfig.loader.SchemaLoader()
|
||||
sio = StringIO("<schema>"
|
||||
" <import package='ZConfig.tests.library.widget' />"
|
||||
" <import package='ZConfig.tests.library.widget'"
|
||||
" file='extra.xml' />"
|
||||
"</schema>")
|
||||
schema = loader.loadFile(sio)
|
||||
schema.gettype("widget-a")
|
||||
schema.gettype("extra-type")
|
||||
|
||||
def test_import_component_twice_1(self):
|
||||
# Make sure we can import a component twice from a schema.
|
||||
# This is most likely to occur when the component is imported
|
||||
# from each of two other components, or from the top-level
|
||||
# schema and a component.
|
||||
loader = ZConfig.loader.SchemaLoader()
|
||||
sio = StringIO("<schema>"
|
||||
" <import package='ZConfig.tests.library.widget' />"
|
||||
" <import package='ZConfig.tests.library.widget' />"
|
||||
"</schema>")
|
||||
schema = loader.loadFile(sio)
|
||||
schema.gettype("widget-a")
|
||||
|
||||
def test_import_component_twice_2(self):
|
||||
# Make sure we can import a component from a config file even
|
||||
# if it has already been imported from the schema.
|
||||
loader = ZConfig.loader.SchemaLoader()
|
||||
sio = StringIO("<schema>"
|
||||
" <import package='ZConfig.tests.library.widget' />"
|
||||
"</schema>")
|
||||
schema = loader.loadFile(sio)
|
||||
loader = ZConfig.loader.ConfigLoader(schema)
|
||||
sio = StringIO("%import ZConfig.tests.library.widget")
|
||||
loader.loadFile(sio)
|
||||
|
||||
def test_urlsplit_urlunsplit(self):
|
||||
# Extracted from Python's test.test_urlparse module:
|
||||
for url, parsed, split in [
|
||||
('http://www.python.org',
|
||||
('http', 'www.python.org', '', '', '', ''),
|
||||
('http', 'www.python.org', '', '', '')),
|
||||
('http://www.python.org#abc',
|
||||
('http', 'www.python.org', '', '', '', 'abc'),
|
||||
('http', 'www.python.org', '', '', 'abc')),
|
||||
('http://www.python.org/#abc',
|
||||
('http', 'www.python.org', '/', '', '', 'abc'),
|
||||
('http', 'www.python.org', '/', '', 'abc')),
|
||||
("http://a/b/c/d;p?q#f",
|
||||
('http', 'a', '/b/c/d', 'p', 'q', 'f'),
|
||||
('http', 'a', '/b/c/d;p', 'q', 'f')),
|
||||
('file:///tmp/junk.txt',
|
||||
('file', '', '/tmp/junk.txt', '', '', ''),
|
||||
('file', '', '/tmp/junk.txt', '', '')),
|
||||
]:
|
||||
result = ZConfig.url.urlsplit(url)
|
||||
self.assertEqual(result, split)
|
||||
result2 = ZConfig.url.urlunsplit(result)
|
||||
self.assertEqual(result2, url)
|
||||
|
||||
def test_file_url_normalization(self):
|
||||
self.assertEqual(
|
||||
ZConfig.url.urlnormalize("file:/abc/def"),
|
||||
"file:///abc/def")
|
||||
self.assertEqual(
|
||||
ZConfig.url.urlunsplit(("file", "", "/abc/def", "", "")),
|
||||
"file:///abc/def")
|
||||
self.assertEqual(
|
||||
ZConfig.url.urljoin("file:/abc/", "def"),
|
||||
"file:///abc/def")
|
||||
self.assertEqual(
|
||||
ZConfig.url.urldefrag("file:/abc/def#frag"),
|
||||
("file:///abc/def", "frag"))
|
||||
|
||||
def test_url_from_file(self):
|
||||
class MockFile(object):
|
||||
name = 'path'
|
||||
self.assertEqual('file://',
|
||||
ZConfig.loader._url_from_file(MockFile)[:7])
|
||||
|
||||
def test_isPath(self):
|
||||
assertTrue = self.assertTrue
|
||||
isPath = ZConfig.loader.SchemaLoader().isPath
|
||||
assertTrue(isPath("abc"))
|
||||
assertTrue(isPath("abc/def"))
|
||||
assertTrue(isPath("/abc"))
|
||||
assertTrue(isPath("/abc/def"))
|
||||
assertTrue(isPath(r"\abc"))
|
||||
assertTrue(isPath(r"\abc\def"))
|
||||
assertTrue(isPath(r"c:\abc\def"))
|
||||
assertTrue(isPath("/ab:cd"))
|
||||
assertTrue(isPath(r"\ab:cd"))
|
||||
assertTrue(isPath("long name with spaces"))
|
||||
assertTrue(isPath("long name:with spaces"))
|
||||
assertTrue(not isPath("ab:cd"))
|
||||
assertTrue(not isPath("http://www.example.com/"))
|
||||
assertTrue(not isPath("http://www.example.com/sample.conf"))
|
||||
assertTrue(not isPath("file:///etc/zope/zope.conf"))
|
||||
assertTrue(not isPath("file:///c|/foo/bar.conf"))
|
||||
|
||||
|
||||
class TestNonExistentResources(unittest.TestCase):
|
||||
|
||||
# XXX Not sure if this is the best approach for these. These
|
||||
# tests make sure that the error reported by ZConfig for missing
|
||||
# resources is handled in a consistent way. Since ZConfig uses
|
||||
# urllib2.urlopen() for opening all resources, what we do is
|
||||
# replace that function with one that always raises an exception.
|
||||
# Since urllib2.urlopen() can raise either IOError or OSError
|
||||
# (depending on the version of Python), we run test for each
|
||||
# exception. urllib2.urlopen() is restored after running the
|
||||
# test.
|
||||
|
||||
def setUp(self):
|
||||
self.urllib2_urlopen = urllib2.urlopen
|
||||
urllib2.urlopen = self.fake_urlopen
|
||||
|
||||
def tearDown(self):
|
||||
urllib2.urlopen = self.urllib2_urlopen
|
||||
|
||||
def fake_urlopen(self, url):
|
||||
raise self.error()
|
||||
|
||||
def test_nonexistent_file_ioerror(self):
|
||||
self.error = IOError
|
||||
self.check_nonexistent_file()
|
||||
|
||||
def test_nonexistent_file_oserror(self):
|
||||
self.error = OSError
|
||||
self.check_nonexistent_file()
|
||||
|
||||
def check_nonexistent_file(self):
|
||||
fn = tempfile.mktemp()
|
||||
schema = ZConfig.loadSchemaFile(StringIO("<schema/>"))
|
||||
self.assertRaises(ZConfig.ConfigurationError,
|
||||
ZConfig.loadSchema, fn)
|
||||
self.assertRaises(ZConfig.ConfigurationError,
|
||||
ZConfig.loadConfig, schema, fn)
|
||||
self.assertRaises(ZConfig.ConfigurationError,
|
||||
ZConfig.loadConfigFile, schema,
|
||||
StringIO("%include " + fn))
|
||||
self.assertRaises(ZConfig.ConfigurationError,
|
||||
ZConfig.loadSchema,
|
||||
"http://www.zope.org/no-such-document/")
|
||||
self.assertRaises(ZConfig.ConfigurationError,
|
||||
ZConfig.loadConfig, schema,
|
||||
"http://www.zope.org/no-such-document/")
|
||||
|
||||
|
||||
class TestResourcesInZip(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.old_path = sys.path[:]
|
||||
# now add our sample EGG to sys.path:
|
||||
zipfile = os.path.join(os.path.dirname(myfile), "foosample.zip")
|
||||
sys.path.append(zipfile)
|
||||
|
||||
def tearDown(self):
|
||||
sys.path[:] = self.old_path
|
||||
|
||||
def test_zip_import_component_from_schema(self):
|
||||
sio = StringIO('''
|
||||
<schema>
|
||||
<abstracttype name="something"/>
|
||||
<import package="foo.sample"/>
|
||||
<section name="*"
|
||||
attribute="something"
|
||||
type="something"
|
||||
/>
|
||||
</schema>
|
||||
''')
|
||||
schema = ZConfig.loadSchemaFile(sio)
|
||||
t = schema.gettype("sample")
|
||||
self.assertFalse(t.isabstract())
|
||||
|
||||
def test_zip_import_component_from_config(self):
|
||||
sio = StringIO('''
|
||||
<schema>
|
||||
<abstracttype name="something"/>
|
||||
<section name="*"
|
||||
attribute="something"
|
||||
type="something"
|
||||
/>
|
||||
</schema>
|
||||
''')
|
||||
schema = ZConfig.loadSchemaFile(sio)
|
||||
|
||||
value = '''
|
||||
%import foo.sample
|
||||
<sample>
|
||||
data value
|
||||
</sample>
|
||||
'''
|
||||
sio = StringIO(value)
|
||||
config, _ = ZConfig.loadConfigFile(schema, sio)
|
||||
self.assertEqual(config.something.data, "| value |")
|
||||
|
||||
sio = StringIO(value)
|
||||
with self.assertRaises(ZConfig.ConfigurationSyntaxError):
|
||||
ZConfig.loadConfigFile(schema, sio,
|
||||
overrides=["sample/data=othervalue"])
|
||||
|
||||
class TestOpenPackageResource(TestHelper, unittest.TestCase):
|
||||
|
||||
magic_name = 'not a valid import name'
|
||||
|
||||
def setUp(self):
|
||||
sys.modules[self.magic_name] = self
|
||||
|
||||
def tearDown(self):
|
||||
del sys.modules[self.magic_name]
|
||||
|
||||
def test_package_loader_resource_error(self):
|
||||
class MockLoader(object):
|
||||
pass
|
||||
self.__loader__ = MockLoader()
|
||||
self.__path__ = ['dir']
|
||||
|
||||
self.assertRaisesRegex(ZConfig.SchemaResourceError,
|
||||
"error opening schema component",
|
||||
ZConfig.loader.openPackageResource,
|
||||
self.magic_name, 'a path')
|
||||
|
||||
# Now with an empty path
|
||||
self.__path__ = []
|
||||
self.assertRaisesRegex(ZConfig.SchemaResourceError,
|
||||
"schema component not found",
|
||||
ZConfig.loader.openPackageResource,
|
||||
self.magic_name, 'a path')
|
||||
|
||||
def test_resource(self):
|
||||
r = ZConfig.loader.Resource(self, None)
|
||||
self.assertEqual(self.magic_name, r.magic_name)
|
||||
@@ -0,0 +1,142 @@
|
||||
##############################################################################
|
||||
#
|
||||
# Copyright (c) 2017 Zope Foundation and Contributors.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# This software is subject to the provisions of the Zope Public License,
|
||||
# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
|
||||
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
|
||||
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
|
||||
# FOR A PARTICULAR PURPOSE.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
import unittest
|
||||
|
||||
from ZConfig import ConfigurationError
|
||||
from ZConfig import DataConversionError
|
||||
|
||||
from ZConfig.matcher import SectionValue
|
||||
from ZConfig.matcher import SectionMatcher
|
||||
from ZConfig.matcher import BaseMatcher
|
||||
|
||||
from ZConfig.tests.support import TestHelper
|
||||
|
||||
|
||||
class SectionValueTestCase(unittest.TestCase):
|
||||
|
||||
def test_repr(self):
|
||||
class MockMatcher(object):
|
||||
type = None
|
||||
|
||||
matcher = MockMatcher()
|
||||
matcher.type = MockMatcher()
|
||||
matcher.type.name = 'matcher'
|
||||
|
||||
sv = SectionValue({}, 'name', matcher)
|
||||
self.assertIn('name', repr(sv))
|
||||
|
||||
sv = SectionValue({}, None, matcher)
|
||||
self.assertIn('at', repr(sv))
|
||||
|
||||
self.assertIs(matcher, sv.getSectionMatcher())
|
||||
|
||||
def test_str(self):
|
||||
d = {'k': 'v'}
|
||||
sv = SectionValue(d, None, None)
|
||||
self.assertEqual(
|
||||
'k : v',
|
||||
str(sv))
|
||||
|
||||
class SectionMatcherTestCase(TestHelper, unittest.TestCase):
|
||||
|
||||
def test_constructor_error(self):
|
||||
class Mock(object):
|
||||
name = 'name'
|
||||
def allowUnnamed(self):
|
||||
return False
|
||||
mock = Mock()
|
||||
self.assertRaisesRegex(ConfigurationError,
|
||||
"sections may not be unnamed",
|
||||
SectionMatcher,
|
||||
mock, mock, None, None)
|
||||
|
||||
class BaseMatcherTestCase(TestHelper, unittest.TestCase):
|
||||
|
||||
def test_repr(self):
|
||||
class Mock(dict):
|
||||
name = 'name'
|
||||
|
||||
matcher = BaseMatcher(None, Mock(), None)
|
||||
repr(matcher)
|
||||
|
||||
def test_duplicate_section_names(self):
|
||||
class Mock(dict):
|
||||
name = 'name'
|
||||
|
||||
matcher = BaseMatcher(None, Mock(), None)
|
||||
matcher._sectionnames['foo'] = None
|
||||
|
||||
self.assertRaisesRegex(ConfigurationError,
|
||||
"section names must not be re-used",
|
||||
matcher.addSection,
|
||||
None, 'foo', None)
|
||||
|
||||
def test_construct_errors(self):
|
||||
class MockType(object):
|
||||
attribute = 'attr'
|
||||
|
||||
_multi = True
|
||||
_section = True
|
||||
|
||||
def ismulti(self):
|
||||
return self._multi
|
||||
|
||||
def issection(self):
|
||||
return self._section
|
||||
|
||||
type_ = []
|
||||
matcher = BaseMatcher(None, type_, None)
|
||||
type_.append( ('key', MockType() ) )
|
||||
|
||||
class MockSection(object):
|
||||
def getSectionDefinition(self):
|
||||
return self
|
||||
|
||||
def datatype(self, _s):
|
||||
raise ValueError()
|
||||
|
||||
matcher._values['attr'] = [MockSection()]
|
||||
|
||||
with self.assertRaises(DataConversionError):
|
||||
matcher.constuct()
|
||||
|
||||
type_[0][1]._multi = False
|
||||
matcher._values['attr'] = MockSection()
|
||||
with self.assertRaises(DataConversionError):
|
||||
matcher.constuct()
|
||||
|
||||
|
||||
def test_create_child_bad_name(self):
|
||||
|
||||
class MockType(list):
|
||||
name = 'foo'
|
||||
sectiontype = None
|
||||
|
||||
def getsectioninfo(self, type_name, name):
|
||||
return self
|
||||
|
||||
def isabstract(self):
|
||||
return False
|
||||
|
||||
def isAllowedName(self, name):
|
||||
return False
|
||||
|
||||
t = MockType()
|
||||
t.sectiontype = MockType()
|
||||
matcher = BaseMatcher(None, t, None)
|
||||
self.assertRaisesRegex(ConfigurationError,
|
||||
'is not an allowed name',
|
||||
matcher.createChildMatcher,
|
||||
MockType(), 'ignored')
|
||||
@@ -0,0 +1,87 @@
|
||||
##############################################################################
|
||||
#
|
||||
# Copyright (c) 2009 Zope Foundation and Contributors.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# This software is subject to the provisions of the Zope Public License,
|
||||
# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
|
||||
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
|
||||
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
|
||||
# FOR A PARTICULAR PURPOSE.
|
||||
#
|
||||
##############################################################################
|
||||
import doctest
|
||||
import manuel.capture
|
||||
import manuel.doctest
|
||||
import manuel.testing
|
||||
import os
|
||||
import os.path
|
||||
import unittest
|
||||
import logging
|
||||
|
||||
|
||||
options = doctest.REPORT_NDIFF | doctest.ELLIPSIS
|
||||
|
||||
old = {}
|
||||
|
||||
def setUp(test):
|
||||
logger = logging.getLogger()
|
||||
old['level'] = logger.level
|
||||
old['handlers'] = logger.handlers[:]
|
||||
|
||||
def tearDown(test):
|
||||
logger = logging.getLogger()
|
||||
logger.level = old['level']
|
||||
logger.handlers = old['handlers']
|
||||
|
||||
def findRoot():
|
||||
here = os.path.dirname(os.path.abspath(__file__))
|
||||
while not os.path.exists(os.path.join(here, 'setup.py')):
|
||||
prev, here = here, os.path.dirname(here)
|
||||
if here == prev:
|
||||
# Let's avoid infinite loops at root
|
||||
raise AssertionError('could not find my setup.py')
|
||||
return here
|
||||
|
||||
def docSetUp(test):
|
||||
# Python 2 makes __path__ and __file__ relative in some
|
||||
# cases (such as when we're executing with the 'ZConfig'
|
||||
# directory on sys.path as CWD). This breaks finding
|
||||
# schema components when we change directories.
|
||||
import ZConfig.components.logger as logger
|
||||
logger.__file__ = os.path.abspath(logger.__file__)
|
||||
logger.__path__ = [os.path.abspath(x) for x in logger.__path__]
|
||||
|
||||
old['pwd'] = os.getcwd()
|
||||
doc_path = os.path.join(
|
||||
findRoot(),
|
||||
'doc')
|
||||
os.chdir(doc_path)
|
||||
setUp(test)
|
||||
|
||||
def docTearDown(test):
|
||||
os.chdir(old['pwd'])
|
||||
tearDown(test)
|
||||
old.clear()
|
||||
|
||||
def test_suite():
|
||||
root = findRoot()
|
||||
plugins = manuel.doctest.Manuel(optionflags=options)
|
||||
plugins += manuel.capture.Manuel()
|
||||
return unittest.TestSuite([
|
||||
manuel.testing.TestSuite(
|
||||
plugins,
|
||||
os.path.join(root, 'README.rst'),
|
||||
setUp=setUp, tearDown=tearDown,
|
||||
),
|
||||
manuel.testing.TestSuite(
|
||||
plugins,
|
||||
os.path.join(root, 'doc', 'using-logging.rst'),
|
||||
globs={'resetLoggers': lambda: tearDown(None)},
|
||||
setUp=docSetUp, tearDown=docTearDown,
|
||||
),
|
||||
])
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main(defaultTest='test_suite')
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,227 @@
|
||||
##############################################################################
|
||||
#
|
||||
# Copyright (c) 2017 Zope Foundation and Contributors.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# This software is subject to the provisions of the Zope Public License,
|
||||
# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
|
||||
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
|
||||
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
|
||||
# FOR A PARTICULAR PURPOSE.
|
||||
#
|
||||
##############################################################################
|
||||
from __future__ import absolute_import
|
||||
|
||||
import contextlib
|
||||
import sys
|
||||
import textwrap
|
||||
import unittest
|
||||
|
||||
import docutils
|
||||
import docutils.utils
|
||||
import docutils.frontend
|
||||
import docutils.parsers.rst
|
||||
import docutils.parsers.rst.directives
|
||||
|
||||
|
||||
|
||||
try:
|
||||
# Note that we're purposely using the old
|
||||
# StringIO object on Python 2 because it auto-converts
|
||||
# Unicode to str, which io.BytesIO and io.StringIO don't
|
||||
# but which normal printing to default sys.stdout *does*
|
||||
from cStringIO import StringIO
|
||||
except ImportError:
|
||||
from ZConfig._compat import NStringIO as StringIO
|
||||
|
||||
|
||||
from ZConfig import schema2html
|
||||
|
||||
from ZConfig.sphinx import SchemaToRstDirective
|
||||
docutils.parsers.rst.directives.register_directive("zconfig", SchemaToRstDirective)
|
||||
from ZConfig.sphinx import RstSchemaFormatter
|
||||
|
||||
from .support import input_file
|
||||
from .support import with_stdin_from_input_file
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def stdout_replaced(buf):
|
||||
old_stdout = sys.stdout
|
||||
sys.stdout = buf
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
sys.stdout = old_stdout
|
||||
|
||||
|
||||
def run_transform(*args):
|
||||
if '--out' not in args and '-o' not in args:
|
||||
buf = StringIO()
|
||||
with stdout_replaced(buf):
|
||||
schema2html.main(args)
|
||||
return buf
|
||||
return schema2html.main(args) # pragma: no cover
|
||||
|
||||
if schema2html.RstSchemaPrinter:
|
||||
def run_transform_rst(*args):
|
||||
args += ('--format', 'xml')
|
||||
return run_transform(*args)
|
||||
else:
|
||||
def run_transform_rst(*args):
|
||||
pass
|
||||
|
||||
class TestSchema2HTML(unittest.TestCase):
|
||||
|
||||
def test_schema_only(self):
|
||||
res = run_transform(input_file('simple.xml'))
|
||||
self.assertIn('</html>', res.getvalue())
|
||||
run_transform_rst(input_file('simple.xml'))
|
||||
|
||||
@with_stdin_from_input_file('simple.xml')
|
||||
def test_schema_only_redirect(self):
|
||||
res = run_transform("-")
|
||||
self.assertIn('</html>', res.getvalue())
|
||||
|
||||
def test_cover_all_schemas(self):
|
||||
for name in ('base-datatype1.xml',
|
||||
'base-datatype2.xml',
|
||||
'base-keytype1.xml',
|
||||
'base-keytype2.xml',
|
||||
'base.xml',
|
||||
'library.xml',
|
||||
'simplesections.xml',):
|
||||
res = run_transform(input_file(name))
|
||||
self.assertIn('</html>', res.getvalue())
|
||||
run_transform_rst(input_file(name))
|
||||
|
||||
def test_html_section_example(self):
|
||||
name = 'simplesections.xml'
|
||||
res = run_transform(input_file(name))
|
||||
out = res.getvalue()
|
||||
self.assertIn('Section Example', out)
|
||||
self.assertIn('Multisection Example', out)
|
||||
|
||||
def test_rst_section_example(self):
|
||||
name = 'simplesections.xml'
|
||||
res = run_transform_rst(input_file(name))
|
||||
out = res.getvalue()
|
||||
self.assertIn('Section Example', out)
|
||||
self.assertIn('Multisection Example', out)
|
||||
|
||||
def test_cover_logging_components(self):
|
||||
res = run_transform('--package', 'ZConfig.components.logger')
|
||||
self.assertIn('eventlog', res.getvalue())
|
||||
run_transform_rst('--package', 'ZConfig.components.logger')
|
||||
|
||||
class TestRst(unittest.TestCase):
|
||||
|
||||
def _parse(self, text):
|
||||
document = docutils.utils.new_document(
|
||||
"Schema",
|
||||
settings=docutils.frontend.OptionParser(
|
||||
components=(docutils.parsers.rst.Parser,)
|
||||
).get_default_values())
|
||||
|
||||
parser = docutils.parsers.rst.Parser()
|
||||
text = textwrap.dedent(text)
|
||||
parser.parse(text, document)
|
||||
return document
|
||||
|
||||
def test_parse_package(self):
|
||||
text = """
|
||||
Document
|
||||
========
|
||||
.. zconfig:: ZConfig.components.logger
|
||||
|
||||
"""
|
||||
document = self._parse(text)
|
||||
doc_text = document.astext()
|
||||
# Check that it produced output
|
||||
self.assertIn("SMTPHandler", doc_text)
|
||||
self.assertIn("Example:", doc_text)
|
||||
|
||||
def test_parse_package_file(self):
|
||||
text = """
|
||||
Document
|
||||
========
|
||||
.. zconfig:: ZConfig.components.logger
|
||||
:file: base-logger.xml
|
||||
|
||||
"""
|
||||
document = self._parse(text)
|
||||
doc_text = document.astext()
|
||||
# Check that it produced output, limited to
|
||||
# just that one file.
|
||||
self.assertNotIn("SMTPHandler", doc_text)
|
||||
self.assertIn("base-logger", doc_text)
|
||||
self.assertIn("Base definition", doc_text)
|
||||
self.assertIn("Example:", doc_text)
|
||||
|
||||
def test_parse_package_limited_names(self):
|
||||
text = """
|
||||
Document
|
||||
========
|
||||
.. zconfig:: ZConfig.components.logger
|
||||
:members: syslog logfile
|
||||
"""
|
||||
document = self._parse(text)
|
||||
doc_text = document.astext()
|
||||
|
||||
# Check that it produced output, limited to
|
||||
# just that one part of the tree
|
||||
self.assertNotIn("SMTPHandler", doc_text)
|
||||
self.assertIn("syslog", doc_text)
|
||||
self.assertIn("SyslogHandlerFactory", doc_text)
|
||||
self.assertIn("FileHandlerFactory", doc_text)
|
||||
|
||||
def test_parse_package_excluded_names(self):
|
||||
text = """
|
||||
Document
|
||||
========
|
||||
.. zconfig:: ZConfig.components.logger
|
||||
:members: ZConfig.logger.base-logger
|
||||
:excluded-members: ZConfig.logger.handler
|
||||
"""
|
||||
document = self._parse(text)
|
||||
doc_text = document.astext()
|
||||
|
||||
# Check that it produced output, limited to
|
||||
# just that one part of the tree
|
||||
# In this case, the root base-logger, but the handlers subtree
|
||||
# was excluded.
|
||||
self.assertIn("zconfig.logger.base-logger", doc_text)
|
||||
self.assertNotIn("SMTPHandler", doc_text)
|
||||
self.assertNotIn("syslog", doc_text)
|
||||
self.assertNotIn("SyslogHandlerFactory", doc_text)
|
||||
self.assertNotIn("FileHandlerFactory", doc_text)
|
||||
|
||||
|
||||
def test_description_dedent(self):
|
||||
text = """No leading whitespace on this line.
|
||||
But this line has whitespace.
|
||||
As does this one.
|
||||
"""
|
||||
written = []
|
||||
class FUT(RstSchemaFormatter):
|
||||
def __init__(self):
|
||||
pass
|
||||
def _parsed(self, text, _):
|
||||
return text
|
||||
def write(self, *texts):
|
||||
written.extend(texts)
|
||||
fut = FUT()
|
||||
fut.description(text)
|
||||
|
||||
dedented = ("""No leading whitespace on this line.\n"""
|
||||
"""But this line has whitespace.\n"""
|
||||
"""As does this one.\n""")
|
||||
|
||||
self.assertEqual(written[0], dedented)
|
||||
|
||||
def test_suite():
|
||||
return unittest.defaultTestLoader.loadTestsFromName(__name__)
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main(defaultTest='test_suite')
|
||||
@@ -0,0 +1,42 @@
|
||||
##############################################################################
|
||||
#
|
||||
# Copyright (c) 2007 Zope Foundation and Contributors.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# This software is subject to the provisions of the Zope Public License,
|
||||
# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
|
||||
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
|
||||
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
|
||||
# FOR A PARTICULAR PURPOSE.
|
||||
#
|
||||
##############################################################################
|
||||
"""\
|
||||
Test driver for ZConfig.schemaless.
|
||||
|
||||
"""
|
||||
__docformat__ = "reStructuredText"
|
||||
|
||||
import manuel.doctest
|
||||
import manuel.testing
|
||||
import unittest
|
||||
|
||||
from ZConfig.schemaless import Section
|
||||
|
||||
class TestSection(unittest.TestCase):
|
||||
|
||||
def test_init_with_data(self):
|
||||
s = Section(data={'k': 'v'})
|
||||
self.assertDictEqual(s, {'k': 'v'})
|
||||
|
||||
|
||||
def test_suite():
|
||||
return unittest.TestSuite([
|
||||
unittest.defaultTestLoader.loadTestsFromName(__name__),
|
||||
manuel.testing.TestSuite(
|
||||
manuel.doctest.Manuel(),
|
||||
'../schemaless.txt'),
|
||||
])
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main(defaultTest='test_suite')
|
||||
@@ -0,0 +1,109 @@
|
||||
##############################################################################
|
||||
#
|
||||
# Copyright (c) 2002, 2003 Zope Foundation and Contributors.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# This software is subject to the provisions of the Zope Public License,
|
||||
# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
|
||||
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
|
||||
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
|
||||
# FOR A PARTICULAR PURPOSE.
|
||||
#
|
||||
##############################################################################
|
||||
"""Tests of the string interpolation module."""
|
||||
|
||||
# This is needed to support Python 2.1.
|
||||
from __future__ import nested_scopes
|
||||
import os
|
||||
|
||||
import unittest
|
||||
|
||||
from ZConfig import SubstitutionReplacementError, SubstitutionSyntaxError
|
||||
from ZConfig.substitution import isname, substitute
|
||||
|
||||
|
||||
class SubstitutionTestCase(unittest.TestCase):
|
||||
def test_simple_names(self):
|
||||
d = {"name": "value",
|
||||
"name1": "abc",
|
||||
"name_": "def",
|
||||
"_123": "ghi"}
|
||||
def check(s, v):
|
||||
self.assertEqual(substitute(s, d), v)
|
||||
check("$name", "value")
|
||||
check(" $name ", " value ")
|
||||
check("${name}", "value")
|
||||
check(" ${name} ", " value ")
|
||||
check("$name$name", "valuevalue")
|
||||
check("$name1$name", "abcvalue")
|
||||
check("$name_$name", "defvalue")
|
||||
check("$_123$name", "ghivalue")
|
||||
check("$name $name", "value value")
|
||||
check("$name1 $name", "abc value")
|
||||
check("$name_ $name", "def value")
|
||||
check("$_123 $name", "ghi value")
|
||||
check("splat", "splat")
|
||||
check("$$", "$")
|
||||
check("$$$name$$", "$value$")
|
||||
|
||||
# Check for an ENV var
|
||||
self.assertEqual(substitute("$(PATH)", d), os.getenv("PATH"))
|
||||
|
||||
def test_undefined_names(self):
|
||||
d = {"name": "value"}
|
||||
self.assertRaises(SubstitutionReplacementError,
|
||||
substitute, "$splat", d)
|
||||
self.assertRaises(SubstitutionReplacementError,
|
||||
substitute, "$splat1", d)
|
||||
self.assertRaises(SubstitutionReplacementError,
|
||||
substitute, "$splat_", d)
|
||||
|
||||
# An undefined ENV should also rise
|
||||
self.assertRaises(SubstitutionReplacementError,
|
||||
substitute, "$(MY_SUPER_PATH)", d)
|
||||
|
||||
def test_syntax_errors(self):
|
||||
d = {"name": "${next"}
|
||||
def check(s):
|
||||
self.assertRaises(SubstitutionSyntaxError,
|
||||
substitute, s, d)
|
||||
check("${")
|
||||
check("${name")
|
||||
check("${1name}")
|
||||
check("${ name}")
|
||||
check("$(")
|
||||
check("$(name")
|
||||
check("$(1name)")
|
||||
check("$( name)")
|
||||
|
||||
def test_edge_cases(self):
|
||||
# It's debatable what should happen for these cases, so we'll
|
||||
# follow the lead of the Bourne shell here.
|
||||
def check(s):
|
||||
self.assertRaises(SubstitutionSyntaxError,
|
||||
substitute, s, {})
|
||||
check("$1")
|
||||
check("$")
|
||||
check("$ stuff")
|
||||
|
||||
def test_non_nesting(self):
|
||||
d = {"name": "$value"}
|
||||
self.assertEqual(substitute("$name", d), "$value")
|
||||
|
||||
def test_isname(self):
|
||||
self.assertTrue(isname("abc"))
|
||||
self.assertTrue(isname("abc_def"))
|
||||
self.assertTrue(isname("_abc"))
|
||||
self.assertTrue(isname("abc_"))
|
||||
self.assertTrue(not isname("abc-def"))
|
||||
self.assertTrue(not isname("-def"))
|
||||
self.assertTrue(not isname("abc-"))
|
||||
self.assertTrue(not isname(""))
|
||||
|
||||
|
||||
def test_suite():
|
||||
return unittest.defaultTestLoader.loadTestsFromName(__name__)
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main(defaultTest='test_suite')
|
||||
@@ -0,0 +1,57 @@
|
||||
##############################################################################
|
||||
#
|
||||
# Copyright (c) 2017 Zope Foundation and Contributors.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# This software is subject to the provisions of the Zope Public License,
|
||||
# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
|
||||
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
|
||||
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
|
||||
# FOR A PARTICULAR PURPOSE.
|
||||
#
|
||||
##############################################################################
|
||||
from __future__ import absolute_import
|
||||
|
||||
import unittest
|
||||
|
||||
from ZConfig import validator
|
||||
|
||||
from .support import input_file
|
||||
from .support import with_stdin_from_input_file
|
||||
|
||||
def run_validator(*args):
|
||||
return validator.main(args)
|
||||
|
||||
class TestValidator(unittest.TestCase):
|
||||
|
||||
def test_no_schema(self):
|
||||
self.assertRaises(SystemExit,
|
||||
run_validator)
|
||||
|
||||
def test_schema_only(self):
|
||||
res = run_validator("--schema", input_file('simple.xml'))
|
||||
self.assertEqual(res, 0)
|
||||
|
||||
@with_stdin_from_input_file('simple.conf')
|
||||
def test_schema_only_redirect(self):
|
||||
res = run_validator("--schema", input_file('simple.xml'))
|
||||
self.assertEqual(res, 0)
|
||||
|
||||
def test_good_config(self):
|
||||
res = run_validator("--schema", input_file('simple.xml'),
|
||||
input_file('simple.conf'),
|
||||
input_file('simple.conf'))
|
||||
self.assertEqual(res, 0)
|
||||
|
||||
def test_bad_config(self):
|
||||
res = run_validator("--schema", input_file("simple.xml"),
|
||||
input_file("outer.conf"))
|
||||
self.assertEqual(res, 1)
|
||||
|
||||
|
||||
def test_suite():
|
||||
return unittest.defaultTestLoader.loadTestsFromName(__name__)
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main(defaultTest='test_suite')
|
||||
@@ -0,0 +1,2 @@
|
||||
This directory contains a sample package that is used to create the
|
||||
'foosample.zip' file used in the tests.
|
||||
@@ -0,0 +1 @@
|
||||
# This directory is a Python package.
|
||||
@@ -0,0 +1 @@
|
||||
# This directory is a Python package.
|
||||
@@ -0,0 +1,9 @@
|
||||
<component prefix="foo.sample">
|
||||
|
||||
<!-- `something` is an abstract type that's already defined -->
|
||||
|
||||
<sectiontype name="sample" implements="something">
|
||||
<key name="data" datatype=".datatypes.data"/>
|
||||
</sectiontype>
|
||||
|
||||
</component>
|
||||
@@ -0,0 +1,7 @@
|
||||
"""Sample datatypes used for testing.
|
||||
"""
|
||||
__docformat__ = "reStructuredText"
|
||||
|
||||
|
||||
def data(value):
|
||||
return "| %s |" % value
|
||||
@@ -0,0 +1,55 @@
|
||||
##############################################################################
|
||||
#
|
||||
# Copyright (c) 2002, 2003 Zope Foundation and Contributors.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# This software is subject to the provisions of the Zope Public License,
|
||||
# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
|
||||
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
|
||||
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
|
||||
# FOR A PARTICULAR PURPOSE.
|
||||
#
|
||||
##############################################################################
|
||||
"""urlparse-like helpers that normalize file: URLs.
|
||||
|
||||
ZConfig and urllib2 expect file: URLs to consistently use the '//'
|
||||
hostpart seperator; the functions here enforce this constraint.
|
||||
"""
|
||||
|
||||
from ZConfig._compat import urlparse as _urlparse
|
||||
|
||||
urlsplit = _urlparse.urlsplit
|
||||
|
||||
def urlnormalize(url):
|
||||
lc = url.lower()
|
||||
if lc.startswith("file:/") and not lc.startswith("file:///"):
|
||||
url = "file://" + url[5:]
|
||||
return url
|
||||
|
||||
|
||||
def urlunsplit(parts):
|
||||
parts = list(parts)
|
||||
parts.insert(3, '')
|
||||
url = _urlparse.urlunparse(tuple(parts))
|
||||
if (parts[0] == "file"
|
||||
and url.startswith("file:/")
|
||||
and not url.startswith("file:///")):
|
||||
# It may not be possible to get here anymore with
|
||||
# modern urlparse, at least not on posix?
|
||||
url = "file://" + url[5:] # pragma: no cover
|
||||
return url
|
||||
|
||||
|
||||
def urldefrag(url):
|
||||
url, fragment = _urlparse.urldefrag(url)
|
||||
return urlnormalize(url), fragment
|
||||
|
||||
|
||||
def urljoin(base, relurl):
|
||||
url = _urlparse.urljoin(base, relurl)
|
||||
if url.startswith("file:/") and not url.startswith("file:///"):
|
||||
# It may not be possible to get here anymore with
|
||||
# modern urlparse, at least not on posix?
|
||||
url = "file://" + url[5:] # pragma: no cover
|
||||
return url
|
||||
@@ -0,0 +1,75 @@
|
||||
##############################################################################
|
||||
#
|
||||
# Copyright (c) 2003 Zope Corporation and Contributors.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# This software is subject to the provisions of the Zope Public License,
|
||||
# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
|
||||
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
|
||||
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
|
||||
# FOR A PARTICULAR PURPOSE.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
"""Script to check validity of a configuration file.
|
||||
"""
|
||||
|
||||
from __future__ import print_function
|
||||
import argparse
|
||||
import sys
|
||||
|
||||
|
||||
import ZConfig
|
||||
|
||||
|
||||
def main(args=None):
|
||||
optparser = argparse.ArgumentParser(
|
||||
description="Script to check validity of a configuration file",
|
||||
epilog="""
|
||||
Each file named on the command line is checked for syntactical errors
|
||||
and schema conformance. The schema must be specified. If no files
|
||||
are specified and standard input is not a TTY, standard in is treated
|
||||
as a configuration file. Specifying a schema and no configuration
|
||||
files causes the schema to be checked.""",
|
||||
)
|
||||
|
||||
optparser.add_argument(
|
||||
"-s", "--schema", dest="schema",
|
||||
required=True,
|
||||
help="use the schema in FILE (can be a URL)",
|
||||
metavar="FILE"
|
||||
)
|
||||
|
||||
optparser.add_argument(
|
||||
"file",
|
||||
nargs='*',
|
||||
help="Optional configuration file to check",
|
||||
type=argparse.FileType('r'),
|
||||
)
|
||||
|
||||
options = optparser.parse_args(args=args)
|
||||
|
||||
schema = ZConfig.loadSchema(options.schema)
|
||||
|
||||
if not options.file:
|
||||
if sys.stdin.isatty():
|
||||
# just checking the schema
|
||||
return 0
|
||||
|
||||
# stdin is a pipe
|
||||
options.file = [sys.stdin]
|
||||
|
||||
errors = False
|
||||
for f in options.file:
|
||||
try:
|
||||
ZConfig.loadConfigFile(schema, f)
|
||||
except ZConfig.ConfigurationError as e:
|
||||
print(str(e), file=sys.stderr)
|
||||
errors = True
|
||||
|
||||
return int(errors)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user