# Copyright 2009, Peter A. Bigot
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain a
# copy of the License at:
#
#            http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.

"""Utility functions and classes."""

import re
import os
import errno
import pyxb

def QuotedEscaped (s):
    """Convert a string into a literal value that can be used in Python source.

    This just calls C{repr}.  No point in getting all complex when the language
    already gives us what we need.

    @rtype: C{str}
    """
    return repr(s)

_UnderscoreSubstitute_re = re.compile(r'[- .]')
_NonIdentifier_re = re.compile(r'[^a-zA-Z0-9_]')
_PrefixUnderscore_re = re.compile(r'^_+')
_PrefixDigit_re = re.compile(r'^\d+')

def MakeIdentifier (s):
    """Convert a string into something suitable to be a Python identifier.

    The string is converted to unicode; spaces and periods replaced by
    underscores; non-printables stripped.  Furthermore, any leading
    underscores are removed.  If the result begins with a digit, the
    character 'n' is prepended.  If the result is the empty string,
    the string 'emptyString' is substituted.

    No check is made for L{conflicts with keywords <DeconflictKeyword>}.

    @rtype: C{str}
    """
    s = _PrefixUnderscore_re.sub('', _NonIdentifier_re.sub('',_UnderscoreSubstitute_re.sub('_', str(s))))
    if _PrefixDigit_re.match(s):
        s = 'n' + s
    if 0 == len(s):
        s = 'emptyString'
    return s

# Note that types like int and float are not keywords
_Keywords = frozenset( ( "and", "del", "from", "not", "while", "as", "elif", "global",
              "or", "with", "assert", "else", "if", "pass", "yield",
              "break", "except", "import", "print", "class", "exec",
              "in", "raise", "continue", "finally", "is", "return",
              "def", "for", "lambda", "try",
              "None" ) )
"""The keywords reserved for Python."""

def DeconflictKeyword (s, aux_keywords=frozenset()):
    """If the provided string C{s} matches a Python language keyword,
    append an underscore to distinguish them.

    See also L{MakeUnique}.

    @param s: string to be deconflicted
    
    @keyword aux_keywords: optional iterable of additional strings
    that should be treated as keywords.

    @rtype: C{str}

    """
    if (s in _Keywords) or (s in aux_keywords):
        return '%s_' % (s,)
    return s

def MakeUnique (s, in_use):
    """Return an identifier based on C{s} that is not in the given set.

    The returned identifier is made unique by appending an underscore
    and, if necessary, a serial number.

    The order is : C{x}, C{x_}, C{x_2}, C{x_3}, ...

    @param in_use: The set of identifiers already in use in the
    relevant scope.  C{in_use} is updated to contain the returned
    identifier.

    @rtype: C{str}
    """
    if s in in_use:
        ctr = 2
        s = s.rstrip('_')
        candidate = '%s_' % (s,)
        while candidate in in_use:
            candidate = '%s_%d' % (s, ctr)
            ctr += 1
        s = candidate
    in_use.add(s)
    return s

def PrepareIdentifier (s, in_use, aux_keywords=frozenset(), private=False, protected=False):
    """Combine everything required to create a unique identifier.

    Leading and trailing underscores are stripped from all
    identifiers.

    @param in_use: the set of already used identifiers.  Upon return
    from this function, it is updated to include the returned
    identifier.

    @keyword aux_keywords: an optional set of additional symbols that
    are illegal in the given context; use this to prevent conflicts
    with known method names.

    @keyword private: if C{False} (default), all leading underscores
    are stripped, guaranteeing the identifier will not be private.  If
    C{True}, the returned identifier has two leading underscores,
    making it a private variable within a Python class.

    @keyword protected: as for C{private}, but uses only one
    underscore.

    @rtype: C{str}

    @note: Only module-level identifiers should be treated as
    protected.  The class-level L{_DeconflictSymbols_mixin}
    infrastructure does not include protected symbols.  All class and
    instance members beginning with a single underscore are reserved
    for the PyXB infrastructure."""
    s = DeconflictKeyword(MakeIdentifier(s).strip('_'), aux_keywords)
    if private:
        s = '__' + s
    elif protected:
        s = '_' + s
    return MakeUnique(s, in_use)

# @todo: descend from pyxb.cscRoot, if we import pyxb
class _DeconflictSymbols_mixin (object):
    """Mix-in used to deconflict public symbols in classes that may be
    inherited by generated binding classes.

    Some classes, like the L{pyxb.binding.basis.element} or
    L{pyxb.binding.basis.simpleTypeDefinition} classes in
    L{pyxb.binding.basis}, have public symbols associated with
    functions and variables.  It is possible that an XML schema might
    include tags and attribute names that match these symbols.  To
    avoid conflict, the reserved symbols marked in this class are
    added to the pre-defined identifier set.

    Subclasses should create a class-level variable
    C{_ReservedSymbols} that contains a set of strings denoting the
    symbols reserved in this class, combined with those from any
    superclasses that also have reserved symbols.  Code like the
    following is suggested::

       # For base classes (direct mix-in):
       _ReservedSymbols = set([ 'one', 'two' ])
       # For subclasses:
       _ReservedSymbols = SuperClass._ReservedSymbols.union(set([ 'three' ]))

    Only public symbols (those with no underscores) are currently
    supported.  (Private symbols can't be deconflicted that easily,
    and no protected symbols that derive from the XML are created by
    the binding generator.)
    """

    _ReservedSymbols = set()
    """There are no reserved symbols in the base class."""

# Regular expression detecting tabs, carriage returns, and line feeds
__TabCRLF_re = re.compile("[\t\n\r]")
# Regular expressoin detecting sequences of two or more spaces
__MultiSpace_re = re.compile(" +")
    
def NormalizeWhitespace (text, preserve=False, replace=False, collapse=False):
    """Normalize the given string.

    Exactly one of the C{preserve}, C{replace}, and C{collapse} keyword
    parameters must be assigned the value C{True} by the caller.

     - C{preserve}: the text is returned unchanged.

     - C{replace}: all tabs, newlines, and carriage returns are
     replaced with ASCII spaces.

     - C{collapse}: the C{replace} normalization is done, then
     sequences of two or more spaces are replaced by a single space.

    See the U{whiteSpace facet<http://www.w3.org/TR/xmlschema-2/#rf-whiteSpace>}.

    @rtype: C{str}
    """
    if preserve:
        return text
    text = __TabCRLF_re.sub(' ', text)
    if replace:
        return text
    if collapse:
        return __MultiSpace_re.sub(' ', text).strip()
    # pyxb not imported here; could be.
    raise Exception('NormalizeWhitespace: No normalization specified')

class Graph:
    """Represent a directed graph with arbitrary objects as nodes.

    This is used in the L{code
    generator<pyxb.binding.generate.Generator>} to determine order
    dependencies among components within a namespace, and schema that
    comprise various namespaces.  An edge from C{source} to C{target}
    indicates that some aspect of C{source} requires that some aspect
    of C{target} already be available.
    """
    
    def __init__ (self, root=None):
        self.__roots = None
        if root is not None:
            self.__roots = set([root])
        self.__edges = set()
        self.__edgeMap = { }
        self.__reverseMap = { }
        self.__nodes = set()

    __scc = None
    __sccMap = None
    __dfsOrder = None

    def addEdge (self, source, target):
        """Add a directed edge from the C{source} to the C{target}.

        The nodes are added to the graph if necessary.
        """
        self.__edges.add( (source, target) )
        self.__edgeMap.setdefault(source, set()).add(target)
        if source != target:
            self.__reverseMap.setdefault(target, set()).add(source)
        self.__nodes.add(source)
        self.__nodes.add(target)

    def addNode (self, node):
        """Add  the given node to the graph."""
        self.__nodes.add(node)

    __roots = None
    def roots (self, reset=False):
        """Return the set of nodes calculated to be roots (i.e., those that have no incoming edges).

        This caches the roots calculated in a previous invocation
        unless the C{reset} keyword is given the value C{True}.

        @note: Upon reset, any notes that had been manually added
        using L{addNode} will no longer be in the set.

        @keyword reset: If C{True}, any cached value is discarded and
        recomputed.  No effect if C{False} (defalut).

        @rtype: C{set}
        """
        if reset or (self.__roots is None):
            self.__roots = set()
            for n in self.__nodes:
                if not (n in self.__reverseMap):
                    self.__roots.add(n)
        return self.__roots
    def addRoot (self, root):
        """Add the provided node as a root node, even if it has incoming edges.

        The node need not be present in the graph (if necessary, it is added).

        Note that roots added in this way do not survive a reset using
        L{roots}.

        @return: C{self}
        """
        if self.__roots is None:
            self.__roots = set()
        self.__nodes.add(root)
        self.__roots.add(root)
        return self

    def edgeMap (self):
        """Return the edges in the graph.

        The edge data structure is a map from the source node to the
        set of nodes that can be reached in a single step from the
        source.
        """
        return self.__edgeMap
    __edgeMap = None

    def edges (self):
        """Return the edges in the graph.

        The edge data structure is a set of node pairs represented as C{( source, target )}.
        """
        return self.__edges

    def nodes (self):
        """Return the set of nodes in the graph.

        The node collection data structure is a set containing node
        objects, whatever they may be."""
        return self.__nodes

    def tarjan (self, reset=False):
        """Execute Tarjan's algorithm on the graph.

        U{Tarjan's
        algorithm<http://en.wikipedia.org/wiki/Tarjan%27s_strongly_connected_components_algorithm>}
        computes the U{strongly-connected
        components<http://en.wikipedia.org/wiki/Strongly_connected_component>}
        of the graph: i.e., the sets of nodes that form a minimal
        closed set under edge transition.  In essence, the loops.  We
        use this to detect groups of components that have a dependency
        cycle.

        @keyword reset: If C{True}, any cached component set is erased
        and recomputed.  If C{True}, an existing previous result is
        left unchanged."""

        if (self.__scc is not None) and (not reset):
            return
        self.__sccMap = { }
        self.__stack = []
        self.__sccOrder = []
        self.__scc = []
        self.__index = 0
        self.__tarjanIndex = { }
        self.__tarjanLowLink = { }
        for v in self.__nodes:
            self.__tarjanIndex[v] = None
        roots = self.roots()
        if (0 == len(roots)) and (0 < len(self.__nodes)):
            raise Exception('TARJAN: No roots found in graph with %d nodes' % (len(self.__nodes),))
        for r in roots:
            self._tarjan(r)
        self.__didTarjan = True

    def _tarjan (self, v):
        """Do the work of Tarjan's algorithm for a given root node."""
        if self.__tarjanIndex.get(v) is not None:
            # "Root" was already reached.
            return
        self.__tarjanIndex[v] = self.__tarjanLowLink[v] = self.__index
        self.__index += 1
        self.__stack.append(v)
        source = v
        for target in self.__edgeMap.get(source, []):
            if self.__tarjanIndex[target] is None:
                #print "Target %s not found in processed" % (target,)
                self._tarjan(target)
                self.__tarjanLowLink[v] = min(self.__tarjanLowLink[v], self.__tarjanLowLink[target])
            elif target in self.__stack:
                #print "Found %s in stack" % (target,)
                self.__tarjanLowLink[v] = min(self.__tarjanLowLink[v], self.__tarjanLowLink[target])
            else:
                #print "No %s in stack" % (target,)
                pass

        if self.__tarjanLowLink[v] == self.__tarjanIndex[v]:
            scc = []
            while True:
                scc.append(self.__stack.pop())
                if v == scc[-1]:
                    break;
            self.__sccOrder.append(scc)
            if 1 < len(scc):
                self.__scc.append(scc)
                [ self.__sccMap.setdefault(_v, scc) for _v in scc ]
                #print 'SCC at %s' % (' '.join( [str(_s) for _s in scc ]),)

    def scc (self, reset=False):
        """Return the strongly-connected components of the graph.

        The data structure is a set, each element of which is itself a
        set containing one or more nodes from the graph.

        @see: L{tarjan}.
        """
        if reset or (self.__scc is None):
            self.tarjan(reset)
        return self.__scc
    __scc = None

    def sccMap (self, reset=False):
        """Return a map from nodes to the strongly-connected component
        to which the node belongs.

        @keyword reset: If C{True}, the L{tarjan} method will be
        re-invoked, propagating the C{reset} value.  If C{False}
        (default), a cached value will be returned if available.

        @see: L{tarjan}.
        """
        if reset or (self.__sccMap is None):
            self.tarjan(reset)
        return self.__sccMap
    __sccMap = None

    def sccOrder (self, reset=False):
        """Return the strongly-connected components in order.

        The data structure is a list, in dependency order, of strongly
        connected components (which can be single nodes).  Appearance
        of a node in a set earlier in the list indicates that it has
        no dependencies on any node that appears in a subsequent set.
        This order is preferred over L{dfsOrder} for code generation,
        since it detects loops.

        @see: L{tarjan}.
        """
        if reset or (self.__sccOrder is None):
            self.tarjan(reset)
        return self.__sccOrder
    __sccOrder = None

    def sccForNode (self, node, **kw):
        """Return the strongly-connected component to which the given
        node belongs.

        Any keywords suppliend when invoking this method are passed to
        the L{sccMap} method.

        @return: The SCC set, or C{None} if the node is not present in
        the results of Tarjan's algorithm."""
        
        return self.sccMap(**kw).get(node, None)

    def cyclomaticComplexity (self):
        """Return the cyclomatic complexity of the graph."""
        self.tarjan()
        return len(self.__edges) - len(self.__nodes) + 2 * len(self.__scc)

    def __dfsWalk (self, source):
        assert not (source in self.__dfsWalked)
        self.__dfsWalked.add(source)
        for target in self.__edgeMap.get(source, []):
            if not (target in self.__dfsWalked): 
                self.__dfsWalk(target)
        self.__dfsOrder.append(source)

    def _generateDOT (self, title='UNKNOWN', labeller=None):
        node_map = { }
        idx = 1
        for n in self.__nodes:
            node_map[n] = idx
            idx += 1
        text = []
        text.append('digraph "%s" {' % (title,))
        for n in self.__nodes:
            if labeller is not None:
                nn = labeller(n)
            else:
                nn = str(n)
            text.append('%s [shape=box,label="%s"];' % (node_map[n], nn))
        for s in self.__nodes:
            for d in self.__edgeMap.get(s, []):
                if s != d:
                    text.append('%s -> %s;' % (node_map[s], node_map[d]))
        text.append("};")
        return "\n".join(text)

    def dfsOrder (self, reset=False):
        """Return the nodes of the graph in U{depth-first-search
        order<http://en.wikipedia.org/wiki/Depth-first_search>}.

        The data structure is a list.  Calculated lists are retained
        and returned on future invocations, subject to the C{reset}
        keyword.

        @keyword reset: If C{True}, discard cached results and recompute the order."""
        if reset or (self.__dfsOrder is None):
            self.__dfsWalked = set()
            self.__dfsOrder = []
            for root in self.roots(reset=reset):
                self.__dfsWalk(root)
            self.__dfsWalked = None
            if len(self.__dfsOrder) != len(self.__nodes):
                raise Exception('DFS walk did not cover all nodes (walk %d versus nodes %d)' % (len(self.__dfsOrder), len(self.__nodes)))
        return self.__dfsOrder
        
def NormalizeLocation (uri, parent_uri=None):
    """Normalize a URI against an optional parent_uri in the way that is
    done for C{schemaLocation} attribute values.

    If no URI schema is present, this will normalize a file system
    path."""
    import urlparse
    import os
    
    if uri is None:
        return uri
    if parent_uri is None:
        abs_uri = uri
    else:
        #if (0 > parent_uri.find(':')) and (not parent_uri.endswith(os.sep)):
        #    parent_uri = parent_uri + os.sep
        abs_uri = urlparse.urljoin(parent_uri, uri)
    if 0 > abs_uri.find(':'):
        abs_uri = os.path.realpath(abs_uri)
    return abs_uri

import urlparse

def TextFromURI (uri, archive_directory=None):
    """Retrieve the contents of the uri as a text string.

    If the uri does not include a scheme (e.g., C{http:}), it is
    assumed to be a file path on the local system."""
    import urllib2
    xmls = None
    try:
        if 0 <= uri.find(':'):
            xmls = urllib2.urlopen(uri).read()
        else:
            xmls = file(uri).read()
            archive_directory = None
    except Exception, e:
        print 'TextFromURI: open %s caught: %s' % (uri, e)
        raise
    if archive_directory:
        base_name = os.path.basename(os.path.normpath(urlparse.urlparse(uri)[2]))
        counter = 1
        dest_file = os.path.join(archive_directory, base_name)
        while os.path.isfile(dest_file):
            dest_file = os.path.join(archive_directory, '%s.%d' % (base_name, counter))
            counter += 1
        try:
            OpenOrCreate(dest_file).write(xmls)
        except OSError, e:
            print 'WARNING: Unable to save %s in %s: %s' % (uri, dest_file, e)
    return xmls

def OpenOrCreate (file_name, tag=None, preserve_contents=False):
    """Return a file object used to write the given file.

    Use the C{tag} keyword to preserve the contents of existing files
    that are not supposed to be overwritten.

    To get a writable file but leaving any existing contents in place,
    set the C{preserve_contents} keyword to C{True}.  Normally, existing file
    contents are erased.

    The returned file pointer is positioned at the end of the file.

    @keyword tag: If not C{None} and the file already exists, absence
    of the given value in the first 4096 bytes of the file causes an
    C{IOError} to be raised with C{errno} set to C{EEXIST}.  I.e.,
    only files with this value in the first 4KB will be returned for
    writing.

    @keyword preserve_contents: This value controls whether existing
    contents of the file will be erased (C{False}, default) or left in
    place (C{True}).
    """
    (path, leaf) = os.path.split(file_name)
    if path:
        try:
            os.makedirs(path)
        except Exception, e:
            if not (isinstance(e, (OSError, IOError)) and (errno.EEXIST == e.errno)):
                raise
    fp = file(file_name, 'a+')
    if (tag is not None) and (0 < os.fstat(fp.fileno()).st_size):
        text = fp.read(4096)
        if 0 > text.find(tag):
            raise OSError(errno.EEXIST, os.strerror(errno.EEXIST))
    if not preserve_contents:
        fp.seek(0) # os.SEEK_SET
        fp.truncate()
    else:
        fp.seek(2) # os.SEEK_END
    return fp
            
# hashlib didn't show up until 2.5, and sha is deprecated in 2.6.
__Hasher = None
try:
    import hashlib
    __Hasher = hashlib.sha1
except ImportError:
    import sha
    __Hasher = sha.new
    
def HashForText (text):
    """Calculate a cryptographic hash of the given string.

    For example, this is used to verify that a given module file
    contains bindings from a previous generation run for the same
    namespace.  See L{OpenOrCreate}.

    @return: A C{str}, generally a sequence of hexadecimal "digit"s.
    """
    return __Hasher(text).hexdigest()

# uuid didn't show up until 2.5
__HaveUUID = False
try:
    import uuid
    __HaveUUID = True
except ImportError:
    import time
    import random
def _NewUUIDString ():
    """Obtain a UUID using the best available method.  On a version of
    python that does not incorporate the C{uuid} class, this creates a
    string combining the current date and time (to the second) with a
    random number.

    @rtype: C{str}
    """
    if __HaveUUID:
        return uuid.uuid1().urn
    return '%s:%08.8x' % (time.strftime('%Y%m%d%H%M%S'), random.randint(0, 0xFFFFFFFFL))

class UniqueIdentifier (object):
    """Records a unique identifier, generally associated with a
    binding generation action.

    The identifier is a string, but gets wrapped in an instance of
    this class to optimize comparisons and reduce memory footprint.

    Invoking the constructor for this class on the same string
    multiple times will return the same Python object.

    An instance of this class compares equal to, and hashes equivalent
    to, the uid string.  When C{str}'d, the result is the uid; when
    C{repr}'d, the result is a constructor call to
    C{pyxb.utils.utility.UniqueIdentifier}.
    """

    # A map from UID string to the instance that represents it
    __ExistingUIDs = {}

    def uid (self):
        """The string unique identifier"""
        return self.__uid
    __uid = None
    
    # Support pickling, which is done using only the UID.
    def __getnewargs__ (self):
        return (self.__uid,)

    def __getstate__ (self):
        return self.__uid

    def __setstate__ (self, state):
        assert self.__uid == state

    # Singleton-like
    def __new__ (cls, *args):
        if 0 == len(args):
            uid = _NewUUIDString()
        else:
            uid = args[0]
        if isinstance(uid, UniqueIdentifier):
            uid = uid.uid()
        if not isinstance(uid, basestring):
            raise TypeError('UniqueIdentifier uid must be a string')
        rv = cls.__ExistingUIDs.get(uid)
        if rv is None:
            rv = super(UniqueIdentifier, cls).__new__(cls)
            rv.__uid = uid
            cls.__ExistingUIDs[uid] = rv
        return rv

    def associateObject (self, obj):
        """Associate the given object witth this identifier.

        This is a one-way associatoin: the object is not provided with
        a return path to this identifier instance."""
        self.__associatedObjects.add(obj)
    def associatedObjects (self):
        """The set of objects that have been associated with this
        identifier instance."""
        return self.__associatedObjects
    __associatedObjects = None

    def __init__ (self, uid=None):
        """Create a new UniqueIdentifier instance.

        @param uid: The unique identifier string.  If present, it is
        the callers responsibility to ensure the value is universally
        unique.  If C{None}, one will be provided.
        @type uid: C{str} or C{unicode}
        """
        assert (uid is None) or (self.uid() == uid), 'UniqueIdentifier: ctor %s, actual %s' % (uid, self.uid())
        self.__associatedObjects = set()

    def __eq__ (self, other):
        if other is None:
            return False
        elif isinstance(other, UniqueIdentifier):
            other_uid = other.uid()
        elif isinstance(other, basestring):
            other_uid = other
        else:
            raise TypeError('UniqueIdentifier: Cannot compare with type %s' % (type(other),))
        return self.uid() == other_uid

    def __hash__ (self):
        return hash(self.uid())

    def __str__ (self):
        return self.uid()

    def __repr__ (self):
        return 'pyxb.utils.utility.UniqueIdentifier(%s)' % (repr(self.uid()),)

import datetime
import calendar
import time
class UTCOffsetTimeZone (datetime.tzinfo):
    """A C{datetime.tzinfo} subclass that helps deal with UTC
    conversions in an ISO8601 world.

    This class only supports fixed offsets from UTC.
    """

    # Regular expression that matches valid ISO8601 time zone suffixes
    __Lexical_re = re.compile('^([-+])(\d\d):(\d\d)$')

    # The offset in minutes east of UTC.
    __utcOffset_min = 0

    # Same as __utcOffset_min, but as a datetime.timedelta
    __utcOffset_td = None

    # A zero-length duration
    __ZeroDuration = datetime.timedelta(0)

    def __init__ (self, spec=None, flip=False):
        """Create a time zone instance with a fixed offset from UTC.

        @param spec: Specifies the offset.  Can be an integer counting
        minutes east of UTC, the value C{None} (equal to 0 minutes
        east), or a string that conform to the ISO8601 time zone
        sequence (B{Z}, or B{[+-]HH:MM}).

        @param flip: If C{False} (default), no adaptation is done.  If
        C{True}, the time zone offset is negated, resulting in the
        conversion from localtime to UTC rather than the default of
        UTC to localtime.
        """

        if spec is not None:
            if isinstance(spec, basestring):
                if 'Z' == spec:
                    self.__utcOffset_min = 0
                else:
                    match = self.__Lexical_re.match(spec)
                    if match is None:
                        raise ValueError('Bad time zone: %s' % (spec,))
                    self.__utcOffset_min = int(match.group(2)) * 60 + int(match.group(3))
                    if '-' == match.group(1):
                        self.__utcOffset_min = - self.__utcOffset_min
            elif isinstance(spec, int):
                self.__utcOffset_min = spec
            elif isinstance(spec, datetime.timedelta):
                self.__utcOffset_min = spec.seconds / 60
            else:
                raise TypeError('%s: unexpected type %s' % (type(self), type(spec)))
            if flip:
                self.__utcOffset_min = - self.__utcOffset_min
        self.__utcOffset_td = datetime.timedelta(minutes=self.__utcOffset_min)
        if 0 == self.__utcOffset_min:
            self.__tzName = 'Z'
        elif 0 > self.__utcOffset_min:
            self.__tzName = '-%02d%02d' % divmod(-self.__utcOffset_min, 60)
        else:
            self.__tzName = '+%02d%02d' % divmod(self.__utcOffset_min, 60)

    def utcoffset (self, dt):
        """Returns the constant offset for this zone."""
        return self.__utcOffset_td

    def tzname (self, dt):
        """Return the name of the timezone in ISO8601 format."""
        return self.__tzName
    
    def dst (self, dt):
        """Returns a constant zero duration."""
        return self.__ZeroDuration

class LocalTimeZone (datetime.tzinfo):
    """A C{datetime.tzinfo} subclass for the local time zone.

    Mostly pinched from the C{datetime.tzinfo} documentation in Python 2.5.1.
    """

    __STDOffset = datetime.timedelta(seconds=-time.timezone)
    __DSTOffset = __STDOffset
    if time.daylight:
        __DSTOffset = datetime.timedelta(seconds=-time.altzone)
    __ZeroDelta = datetime.timedelta(0)
    __DSTDelta = __DSTOffset - __STDOffset

    def utcoffset (self, dt):
        if self.__isDST(dt):
            return self.__DSTOffset
        return self.__STDOffset

    def dst (self, dt):
        if self.__isDST(dt):
            return self.__DSTDelta
        return self.__ZeroDelta

    def tzname (self, dt):
        return time.tzname[self.__isDST(dt)]

    def __isDST (self, dt):
        tt = (dt.year, dt.month, dt.day,
              dt.hour, dt.minute, dt.second,
              0, 0, -1)
        tt = time.localtime(time.mktime(tt))
        return tt.tm_isdst > 0

class PrivateTransient_mixin (pyxb.cscRoot):
    """Emulate the B{transient} keyword from Java for private member
    variables.

    This class defines a C{__getstate__} method which returns a copy
    of C{self.__dict__} with certain members removed.  Specifically,
    if a string "s" appears in a class member variable named
    C{__PrivateTransient} defined in the "Class" class, then the
    corresponding private variable "_Class__s" will be removed from
    the state dictionary.  This is used to eliminate unnecessary
    fields from instances placed in L{namespace
    archives<pyxb.namespace.archive.NamespaceArchive>} without having
    to implement a C{__getstate__} method in every class in the
    instance hierarchy.

    For an example, see
    L{pyxb.xmlschema.structures._SchemaComponent_mixin}

    If you use this, it is your responsibility to define the
    C{__PrivateTransient} class variable and add to it the required
    variable names.

    Classes that inherit from this are free to define their own
    C{__getstate__} method, which may or may not invoke the superclass
    one.  If you do this, be sure that the class defining
    C{__getstate__} lists L{PrivateTransient_mixin} as one of its
    direct superclasses, lest the latter end up earlier in the mro and
    consequently bypass the local override.
    """

    # Suffix used when creating the class member variable in which the
    # transient members are cached.
    __Attribute = '__PrivateTransient'
    
    def __getstate__ (self):
        state = self.__dict__.copy()
        # Note that the aggregate set is stored in a class variable
        # with a slightly different name than the class-level set.
        attr = '_%s%s_' % (self.__class__.__name__, self.__Attribute)
        skipped = getattr(self.__class__, attr, None)
        if skipped is None:
            skipped = set()
            for cl in self.__class__.mro():
                for (k, v) in cl.__dict__.items():
                    if k.endswith(self.__Attribute):
                        cl2 = k[:-len(self.__Attribute)]
                        skipped.update([ '%s__%s' % (cl2, _n) for _n in v ])
            setattr(self.__class__, attr, skipped)
            #print 'Defined skipped for %s: %s' % (self.__class__, skipped)
        for k in skipped:
            if state.get(k) is not None:
                #print 'Stripping %s from instance %x of %s' % (k, id(self), type(self))
                del state[k]
        # Uncomment the following to test whether undesirable types
        # are being pickled, generally by accidently leaving a
        # reference to one in an instance private member.
        #for (k, v) in state.items():
        #    import pyxb.namespace
        #    import xml.dom
        #    import pyxb.xmlschema.structures
        #    if isinstance(v, (pyxb.namespace.resolution.NamespaceContext, xml.dom.Node, pyxb.xmlschema.structures.Schema)):
        #        raise pyxb.LogicError('Unexpected instance of %s key %s in %s' % (type(v), k, self))

        return state

def GetMatchingFiles (path, pattern=None, default_path_wildcard=None, default_path=None, prefix_pattern=None, prefix_substituend=None):
    """Provide a list of absolute paths to files present in any of a
    set of directories and meeting certain criteria.

    This is used, for example, to locate namespace archive files
    within the archive path specified by the user.  One could use::

      files = GetMatchingFiles('&bundles//:+',
                               pattern=re.compile('.*\.wxs$'),
                               default_path_wildcard='+',
                               default_path='/usr/local/pyxb/nsarchives',
                               prefix_pattern='&',
                               prefix_substituend='/opt/pyxb')

    to obtain all files that can be recursively found within
    C{/opt/pyxb/bundles}, or non-recursively within
    C{/usr/local/pyxb/nsarchives}.

    @param path: A colon separated list of directories in which the
    search should be performed.  If a path entry ends with C{//}, any
    directory beneath it is scanned as well, recursively.

    @keyword pattern: Optional regular expression object used to
    determine whether a given directory entry should be returned.  If
    left as C{None}, all directory entries will be returned.

    @keyword default_path_wildcard: An optional string which, if
    present as a single directory in the path, is replaced by the
    value of C{default-path}.

    @keyword default_path: A system-defined directory which can be
    restored to the path by placing the C{default_path_wildcard} in
    the C{path}.

    @keyword prefix_pattern: An optional string which, if present at
    the start of a path element, is replaced by the value of
    C{prefix_substituend}.

    @keyword prefix_substituend: A system-defined string (path prefix)
    which can be combined with the user-provided path information to
    identify a file or subdirectory within an installation-specific
    area.
    """
    matching_files = []
    path_set = path.split(':')
    while 0 < len(path_set):
        path = path_set.pop(0)
        if default_path_wildcard == path:
            if default_path is not None:
                path_set[0:0] = default_path.split(':')
            continue
        recursive = False
        if (prefix_pattern is not None) and path.startswith(prefix_pattern):
            path = os.path.join(prefix_substituend, path[len(prefix_pattern):])
        if path.endswith('//'):
            recursive = True
            path = path[:-2]
        if os.path.isfile(path):
            if (pattern is None) or (pattern.search(path) is not None):
                matching_files.append(path)
        else:
            for (root, dirs, files) in os.walk(path):
                for f in files:
                    if (pattern is None) or (pattern.search(f) is not None):
                        matching_files.append(os.path.join(root, f))
                if not recursive:
                    break
    return matching_files
    
class _LocationBase (object):
    """Wrap a location.

    This is probably a string, but might be a uri object or the like.
    Really we only have this as a separate object so as to avoid
    creating hundreds of copies of the same string."""

    __locationBase = None
    def locationBase (self):
        return self.__locationBase

    def __init__ (self, location_base):
        if isinstance(location_base, _LocationBase):
            location_base = location_base.locationBase()
        self.__locationBase = location_base

    def __str__ (self):
        return str(self.__locationBase)

class Location (object):
    __locationBase = None
    __lineNumber = None
    __columnNumber = None

    def __init__ (self, location_base=None, line_number=None, column_number=None):
        if not isinstance(location_base, _LocationBase):
            location_base = _LocationBase(location_base)
        self.__locationBase = location_base
        self.__lineNumber = line_number
        self.__columnNumber = column_number

    def newLocation (self, locator=None, line_number=None, column_number=None):
        if locator is not None:
            try:
                line_number = locator.getLineNumber()
                column_number = locator.getColumnNumber()
            except:
                pass
        return Location(self.__locationBase, line_number, column_number)

    locationBase = property(lambda _s: _s.__locationBase)
    lineNumber = property(lambda _s: _s.__lineNumber)
    columnNumber = property(lambda _s: _s.__columnNumber)

    def __str__ (self):
        if self.locationBase is None:
            return '<unknownLocation>'
        return '%s[%s:%s]' % (self.locationBase, self.lineNumber, self.columnNumber)

class Locatable_mixin (pyxb.cscRoot):
    __location = None

    def __init__ (self, *args, **kw):
        self.__location = kw.pop('location', None)
        super(Locatable_mixin, self).__init__(*args, **kw)

    def _setLocation (self, location):
        self.__location = location

    def _location (self):
        return self.__location
