tested django-newsletter

This commit is contained in:
Esther Kleinhenz
2018-10-26 15:21:17 +02:00
parent 7ed667d043
commit 811b7c5453
2352 changed files with 448169 additions and 1 deletions
@@ -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:
&lt;import package="ZConfig.components.basic"
file="mapping.xml"
/&gt;
&lt;sectiontype name="mapping"
extends="ZConfig.basic.mapping"
/&gt;
If a non-standard keytype is needed, it can be overridden as
well:
&lt;sectiontype name="system-map"
extends="ZConfig.basic.mapping"
keytype="mypkg.datatypes.system_name"
/&gt;
</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>
@@ -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())