~fearsomedragonfly/activeprocess/trunk

« back to all changes in this revision

Viewing changes to apglib/resman.py

  • Committer: Bradley Harms
  • Date: 2011-01-08 11:22:58 UTC
  • Revision ID: themusicguy123@gmail.com-20110108112258-qkxbmuqg624o616d
- >
  My bad; the last revision actually did not contain the changes stated.
  This one does.
- >
  There has been another sudden inexplicable drop in speed, though not quite as
  bad as last time. Profiling data is again illusive, but I have a feeling the
  problem is somehow related to the fact that Cameras are again a type of
  entity.

Show diffs side-by-side

added added

removed removed

Lines of Context:
718
718
 
719
719
    return jdata
720
720
 
721
 
#def strbool(s):
722
 
#    """ Returns whether or not a string evaluates to a boolean True or False.
723
 
#        Possible string values for False are:
724
 
#        '' (empty string), '0', 'no', 'false', 'off', 'none'
725
 
#        Also, if any non-string object which evaluates to False is passed, False
726
 
#        is returned.
727
 
#        All other values will return True.
728
 
#        The string is case insensitive and ignores whitespace padding.
729
 
#    """
730
 
#    if isinstance(s, str):
731
 
#        s = s.lower().strip()
732
 
#        return s not in ('', '0', 'no', 'false', 'off', 'none')
733
 
#    return bool(s)
734
 
 
735
721
 
736
722
# ~~~~~~~ CLASSES ~~~~~~~~
737
723
 
742
728
    def __init__(cls, name, bases, dict):
743
729
        super(RepresentableMixinM, cls).__init__(name, bases, dict)
744
730
 
745
 
#        # Raise a warning if the class is missing a keyorder.
746
 
#        if not hasattr(cls, 'keyorder'):
747
 
#            message = """
748
 
#    Class "%s" does not have have a `keyorder` attribute. See docs for
749
 
#    `resman.RepresentableMixin` for more details.
750
 
#    """ % name
751
 
#            warnings.warn(message, MissingAttributeWarning, 2)
752
 
#
753
 
#        # Raise a warning if the class is missing a tag.
754
 
#        if not hasattr(cls, 'tag'):
755
 
#            message = """
756
 
#    Class "%s" does not have a `tag` attribute. See docs for
757
 
#    `resman.RepresentableMixin` for more details.
758
 
#    """ % name
759
 
#            warnings.warn(message, MissingAttributeWarning, 2 )
760
 
 
761
731
        # Find all rep and unrep methods
762
732
        reppers = {}
763
733
        unreppers = {}
774
744
        cls.reppers = reppers
775
745
        cls.unreppers = unreppers
776
746
 
777
 
# Prevent unnecessary warning messages within the following class definitions.
778
 
with warnings.catch_warnings():
779
 
    warnings.simplefilter("ignore")
780
 
 
781
 
    class RepresentableMixin(object):
782
 
        """ Mixin class for JSON-representable objects.
783
 
 
784
 
            This class makes special use of methods that have the prefixes
785
 
            "_rep_" or "_unrep_":
786
 
 
787
 
            * Methods named like "_rep_x" will be used to get a
788
 
              JSON-representable version of the attribute `x` in the
789
 
              object.
790
 
 
791
 
            * Methods named like "_unrep_x" will be used to create an
792
 
              object that will be passed to the constructor as the
793
 
              argument `x` when creating an instance from JSON data.
794
 
              All _unrep_ methods MUST be defined as classmethods since they
795
 
              will be called before any instance is actually created.
796
 
 
797
 
            _rep_ and _unrep_ methods must be defined with the following form,
798
 
            where arguments in [brackets] are optional:
799
 
 
800
 
            >>> def _rep_attr(self [, usenorep]):
801
 
            >>>     ''' Returns a JSON representation of attribute `attr`.
802
 
            >>>
803
 
            >>>         usenorep:  A flag that indicates whether or not the
804
 
            >>>                   object or subobjects of the object being
805
 
            >>>                   represented should be represented as None
806
 
            >>>                   (JSON 'null') when they cannot otherwise
807
 
            >>>                   be represented.
808
 
            >>>     '''
809
 
            >>>     return rep_of_attr
810
 
            >>>
811
 
            >>> @classmethod
812
 
            >>> def _unrep_attr(cls, value [, jdata]):
813
 
            >>>     ''' Returns an object based on a rep. of attribute `attr`
814
 
            >>>
815
 
            >>>         value:  The representation of the attribute's value.
816
 
            >>>
817
 
            >>>         jdata:  Other represented data (in case it is needed).
818
 
            >>>
819
 
            >>>     '''
820
 
            >>>     return value_of_attr
821
 
 
822
 
            You do not have to put `usenorep` or `jdata` parameters into a
823
 
            method's definition if the method's code won't be using them; the
824
 
            calling class uses Python's function introspection utilities to
825
 
            determine which way to call them.
826
 
 
827
 
            Attribute representers and unrepresenters can each return the
828
 
            special constant object `resman.NOREP` to prevent their attribute
829
 
            from being represented or unrepresented, respectively. NOREP is a
830
 
            one-of-a-kind object that the resource manager recognizes as a
831
 
            signal that means that means "ignore this representation". It can
832
 
            be used to conditionally represent certain attributes and/or
833
 
            conditionally pass them as arguments to the class constructor when
834
 
            the representation data is loaded back into an object.
835
 
 
836
 
            If the special-prefixed methods approach does not suit your
837
 
            needs or if you find yourself making heavy use of the `usenorep` and
838
 
            `jdata` arguments, you can override the `to_json` and `from_json`
839
 
            methods for more complete control over how the representations
840
 
            are created. Doing this will prevent any attribute rep/unrep methods
841
 
            from  being used automatically.
842
 
 
843
 
            Should you choose to override the `from_json` classmethod but still
844
 
            want to use some or all of the class's unrepresenter functions, you
845
 
            can call the `unrep_jdata` class method instead. This method accepts
846
 
            the same arguments that would be passed to `from_json`, but instead
847
 
            of creating a new instance of the current class, it simply passes
848
 
            the given jdata through the appropriate attribute unrepresenter
849
 
            class methods and returns the results as a dict. (In fact, this is
850
 
            how most of the work for the default `from_json` is performed
851
 
            internally.)
852
 
 
853
 
            It is permissible to create an attribute representer method without
854
 
            a corresponding unrepresenter. In this case, the attribute
855
 
            representation will be passed to the constructor unaltered. It is
856
 
            also permissible to have an unrepresenter without a representer
857
 
            because attributes missing from the given data will simply not get
858
 
            passed to the constructor.
859
 
 
860
 
            This class also makes use of a special class attribute, `tag`.
861
 
            The purpose of `tag` is to help complex un-representer chains
862
 
            to determine how the rep. is to be unrepped. For example, if you
863
 
            have three different possible classes that could be used for
864
 
            a given representation, the value of the representation's tag can
865
 
            be used to determine which class to construct.
866
 
 
867
 
            The `to_json` method only includes the `tag` attribute in a rep
868
 
            of an object if the tag exists on that object or its class. The
869
 
            RepresentableMixin class itself does not actually have the `tag`
870
 
            attribute, so it is up to subclasses to implement it. Also,
871
 
            The default implementation of the `from_json` method will not pass
872
 
            the tag to the class constructor as it is assumed that the tag
873
 
            will mostly be used for the purpose of determining which class to
874
 
            construct rather than as instance data.
875
 
 
876
 
            The RepresentableMixin class has support for "shorthand format"
877
 
            representations to decrease the amount of space and typing required
878
 
            when creating resource files by hand. However, the use of shorthand
879
 
            requires a supporting class to have a class attribute called
880
 
            `keyorder`. Its value should be an ordered sequence of strings that
881
 
            represent the recognized keys in a full format representation. The
882
 
            order of these strings within the keyorder will determine which
883
 
            positions in a shorthand format representation correspond to which
884
 
            keys in full (standard) format representations.
885
 
 
886
 
            Warnings are issued when either the `tag` or `keyorder` attributes
887
 
            are not present in the definition of a class that derives from
888
 
            RepresentableMixin.
889
 
 
890
 
            ~~~ Class Attributes ~~~
891
 
 
892
 
            All subclasses of RepresentableMixin will have the following
893
 
            attributes.
894
 
 
895
 
            reppers:    A dict that maps the name of each representable
896
 
                        attribute to the representer method for that attribute.
897
 
                        This is created automatically for each class using
898
 
                        methods that were defined in the class.
899
 
 
900
 
            unreppers:  A dict that maps the name of each unrepresentable
901
 
                        attribute to the unrepresenter method for that
902
 
                        attribute. This is created automatically for each class
903
 
                        using methods that were defined in the class.
904
 
        """
905
 
 
906
 
        __metaclass__ = RepresentableMixinM
907
 
 
908
 
        def __init__(self, *_vargs_unused, **_kwargs_unused):
909
 
            # Allows RepresentableMixin to be used as a mixin.
910
 
            super(RepresentableMixin, self).__init__()
911
 
 
912
 
        @classmethod
913
 
        def short_to_full(cls, sjdata):
914
 
            """ Converts shorthand-format data to full format.
915
 
 
916
 
                <rep>.short_to_full(sjdata) -> jdata
917
 
 
918
 
                The class attribute `keyorder`, a sequence of strings, of the class
919
 
                `<rep>` is used to determine which positions in the given
920
 
                sequence `sjdata` correspond to which keys in the dicitonary
921
 
                `jdata`. Note that the tag must be present in shorthand format
922
 
                 and always comes first in the key order and is therefore not
923
 
                 included in the `keyorder` class attribute.
924
 
 
925
 
                This method determines whether the given jdata is in shorthand
926
 
                or full format in a primitive way. It is not fool proof, but works
927
 
                most of the time. It basically just tries to access an item in the
928
 
                data using a key/index that should not be present. If a KeyError
929
 
                occurs, it is assumed that the data is in full format. If an
930
 
                IndexError occurs, it is assumed to be in shorthand format. If
931
 
                neither error occurs, a FormatError is excplicitly raised because
932
 
                the full/shortness of the data could not be determined.
933
 
 
934
 
                If `sjdata` is determined to be full format data it is returned
935
 
                unmodified.
936
 
            """
937
 
 
938
 
            # Try to check for shorthand format data.
 
747
 
 
748
 
 
749
class RepresentableMixin(object):
 
750
    """ Mixin class for JSON-representable objects.
 
751
 
 
752
        This class makes special use of methods that have the prefixes
 
753
        "_rep_" or "_unrep_":
 
754
 
 
755
        * Methods named like "_rep_x" will be used to get a
 
756
          JSON-representable version of the attribute `x` in the
 
757
          object.
 
758
 
 
759
        * Methods named like "_unrep_x" will be used to create an
 
760
          object that will be passed to the constructor as the
 
761
          argument `x` when creating an instance from JSON data.
 
762
          All _unrep_ methods MUST be defined as classmethods since they
 
763
          will be called before any instance is actually created.
 
764
 
 
765
        _rep_ and _unrep_ methods must be defined with the following form,
 
766
        where arguments in [brackets] are optional:
 
767
 
 
768
        >>> def _rep_attr(self [, usenorep]):
 
769
        >>>     ''' Returns a JSON representation of attribute `attr`.
 
770
        >>>
 
771
        >>>         usenorep:  A flag that indicates whether or not the
 
772
        >>>                   object or subobjects of the object being
 
773
        >>>                   represented should be represented as None
 
774
        >>>                   (JSON 'null') when they cannot otherwise
 
775
        >>>                   be represented.
 
776
        >>>     '''
 
777
        >>>     return rep_of_attr
 
778
        >>>
 
779
        >>> @classmethod
 
780
        >>> def _unrep_attr(cls, value [, jdata]):
 
781
        >>>     ''' Returns an object based on a rep. of attribute `attr`
 
782
        >>>
 
783
        >>>         value:  The representation of the attribute's value.
 
784
        >>>
 
785
        >>>         jdata:  Other represented data (in case it is needed).
 
786
        >>>
 
787
        >>>     '''
 
788
        >>>     return value_of_attr
 
789
 
 
790
        You do not have to put `usenorep` or `jdata` parameters into a
 
791
        method's definition if the method's code won't be using them; the
 
792
        calling class uses Python's function introspection utilities to
 
793
        determine which way to call them.
 
794
 
 
795
        Attribute representers and unrepresenters can each return the
 
796
        special constant object `resman.NOREP` to prevent their attribute
 
797
        from being represented or unrepresented, respectively. NOREP is a
 
798
        one-of-a-kind object that the resource manager recognizes as a
 
799
        signal that means that means "ignore this representation". It can
 
800
        be used to conditionally represent certain attributes and/or
 
801
        conditionally pass them as arguments to the class constructor when
 
802
        the representation data is loaded back into an object.
 
803
 
 
804
        If the special-prefixed methods approach does not suit your
 
805
        needs or if you find yourself making heavy use of the `usenorep` and
 
806
        `jdata` arguments, you can override the `to_json` and `from_json`
 
807
        methods for more complete control over how the representations
 
808
        are created. Doing this will prevent any attribute rep/unrep methods
 
809
        from  being used automatically.
 
810
 
 
811
        Should you choose to override the `from_json` classmethod but still
 
812
        want to use some or all of the class's unrepresenter functions, you
 
813
        can call the `unrep_jdata` class method instead. This method accepts
 
814
        the same arguments that would be passed to `from_json`, but instead
 
815
        of creating a new instance of the current class, it simply passes
 
816
        the given jdata through the appropriate attribute unrepresenter
 
817
        class methods and returns the results as a dict. (In fact, this is
 
818
        how most of the work for the default `from_json` is performed
 
819
        internally.)
 
820
 
 
821
        It is permissible to create an attribute representer method without
 
822
        a corresponding unrepresenter. In this case, the attribute
 
823
        representation will be passed to the constructor unaltered. It is
 
824
        also permissible to have an unrepresenter without a representer
 
825
        because attributes missing from the given data will simply not get
 
826
        passed to the constructor.
 
827
 
 
828
        This class also makes use of a special class attribute, `tag`.
 
829
        The purpose of `tag` is to help complex un-representer chains
 
830
        to determine how the rep. is to be unrepped. For example, if you
 
831
        have three different possible classes that could be used for
 
832
        a given representation, the value of the representation's tag can
 
833
        be used to determine which class to construct.
 
834
 
 
835
        The `to_json` method only includes the `tag` attribute in a rep
 
836
        of an object if the tag exists on that object or its class. The
 
837
        RepresentableMixin class itself does not actually have the `tag`
 
838
        attribute, so it is up to subclasses to implement it. Also,
 
839
        The default implementation of the `from_json` method will not pass
 
840
        the tag to the class constructor as it is assumed that the tag
 
841
        will mostly be used for the purpose of determining which class to
 
842
        construct rather than as instance data.
 
843
 
 
844
        The RepresentableMixin class has support for "shorthand format"
 
845
        representations to decrease the amount of space and typing required
 
846
        when creating resource files by hand. However, the use of shorthand
 
847
        requires a supporting class to have a class attribute called
 
848
        `keyorder`. Its value should be an ordered sequence of strings that
 
849
        represent the recognized keys in a full format representation. The
 
850
        order of these strings within the keyorder will determine which
 
851
        positions in a shorthand format representation correspond to which
 
852
        keys in full (standard) format representations.
 
853
 
 
854
        Warnings are issued when either the `tag` or `keyorder` attributes
 
855
        are not present in the definition of a class that derives from
 
856
        RepresentableMixin.
 
857
 
 
858
        ~~~ Class Attributes ~~~
 
859
 
 
860
        All subclasses of RepresentableMixin will have the following
 
861
        attributes.
 
862
 
 
863
        reppers:    A dict that maps the name of each representable
 
864
                    attribute to the representer method for that attribute.
 
865
                    This is created automatically for each class using
 
866
                    methods that were defined in the class.
 
867
 
 
868
        unreppers:  A dict that maps the name of each unrepresentable
 
869
                    attribute to the unrepresenter method for that
 
870
                    attribute. This is created automatically for each class
 
871
                    using methods that were defined in the class.
 
872
    """
 
873
 
 
874
    __metaclass__ = RepresentableMixinM
 
875
 
 
876
    def __init__(self, *vargs, **kwargs):
 
877
        # Allows RepresentableMixin to be used as a mixin.
 
878
        super(RepresentableMixin, self).__init__(*vargs, **kwargs)
 
879
 
 
880
    @classmethod
 
881
    def short_to_full(cls, sjdata):
 
882
        """ Converts shorthand-format data to full format.
 
883
 
 
884
            <rep>.short_to_full(sjdata) -> jdata
 
885
 
 
886
            The class attribute `keyorder`, a sequence of strings, of the class
 
887
            `<rep>` is used to determine which positions in the given
 
888
            sequence `sjdata` correspond to which keys in the dicitonary
 
889
            `jdata`. Note that the tag must be present in shorthand format
 
890
             and always comes first in the key order and is therefore not
 
891
             included in the `keyorder` class attribute.
 
892
 
 
893
            This method determines whether the given jdata is in shorthand
 
894
            or full format in a primitive way. It is not fool proof, but works
 
895
            most of the time. It basically just tries to access an item in the
 
896
            data using a key/index that should not be present. If a KeyError
 
897
            occurs, it is assumed that the data is in full format. If an
 
898
            IndexError occurs, it is assumed to be in shorthand format. If
 
899
            neither error occurs, a FormatError is excplicitly raised because
 
900
            the full/shortness of the data could not be determined.
 
901
 
 
902
            If `sjdata` is determined to be full format data it is returned
 
903
            unmodified.
 
904
        """
 
905
 
 
906
        # Try to check for shorthand format data.
 
907
        try:
 
908
            # Deliberately cause an invalid index/key error. This will determine
 
909
            # whether the format is full (a mapping) or shorthand (a sqeuence).
 
910
            L = len(sjdata)
 
911
            sjdata[L]
 
912
 
 
913
        # The data appears to be in full format, not shorthand.
 
914
        except KeyError:
 
915
            jdata = sjdata
 
916
 
 
917
        # The data appears to be shorthand format.
 
918
        except IndexError:
 
919
            # Add the class tag to the keyorder. (The tag is always first.)
 
920
            keyorder = (cls.tag,) + tuple(cls.keyorder)
 
921
 
 
922
            # Convert shorthand format to full format.
 
923
            jdata = dict((key, sjdata[n]) for n, key in enumerate(keyorder))
 
924
 
 
925
        # Since no exception occured, there is no way to determine the format.
 
926
        else:
 
927
            raise _format_err(
 
928
                cls.tag,
 
929
                "Unable to determine whether data format is full or shorthand! "
 
930
                "Either the data object given is a mapping/sequence that has a "
 
931
                "key/index of %r in it or else the data object does not raise "
 
932
                "errors in the standard way.\nObject:\n%r" % (L, sjdata)
 
933
             )
 
934
 
 
935
        return jdata
 
936
 
 
937
    @classmethod
 
938
    def unrep_jdata(cls, jdata):
 
939
        """ Unrepresents given JSON data for use with the constructor.
 
940
 
 
941
            This method is called to turn data in the given JSON data dict
 
942
            into data that is compatible with the representable class's
 
943
            constructor. It does not actually create an instance of the
 
944
            class; it merely returns a dict containing data that was created
 
945
            by procesing the given jdata using the class's unreppers.
 
946
        """
 
947
 
 
948
        # Compile a mapping of all unreppers from the MRO
 
949
        unreppers = {}
 
950
        for base in reversed(cls.__mro__):
 
951
            if hasattr(base, "unreppers"):
 
952
                unreppers.update(base.unreppers)
 
953
 
 
954
        # The keys in jdata will most likely be unicode, so they must be
 
955
        # converted to string.
 
956
        data = dict((str(k), v) for k, v in jdata.iteritems())
 
957
 
 
958
        # Sort the unreppers by the class' key order.
 
959
        # (Keys not in the keyorder are placed at the end.)
 
960
        keyorder = list(cls.keyorder)
 
961
        keyorder.extend(k for k in unreppers.keys() if k not in keyorder)
 
962
        unreppers = [(k, unreppers[k]) for k in keyorder if k in unreppers]
 
963
 
 
964
        # Convert jdata representations to actual data.
 
965
        for name, unrep in unreppers:
 
966
 
 
967
            # Skip values that are not present in both the data and the
 
968
            # unrepper table
 
969
            if name in jdata:
 
970
 
 
971
                # Choose the calling convention appropriate for the unrepper
 
972
                argc = len(inspect.getargspec(unrep)[0])
 
973
                if argc == 3:
 
974
                    v = unrep(jdata[name], jdata)
 
975
                else:
 
976
                    v = unrep(jdata[name])
 
977
 
 
978
                # Don't unrep NOREP
 
979
                if v is NOREP:
 
980
                    del data[name]
 
981
                else:
 
982
                    data[name] = v
 
983
 
 
984
        return data
 
985
 
 
986
    @classmethod
 
987
    def from_json(cls, jdata):
 
988
        """ Create an object from a JSON representation.
 
989
 
 
990
            jdata:  JSON representation of the object. A copy of this
 
991
                    mapping will be expanded into the arguments for
 
992
                    the constructor with values for which a _unrep_
 
993
                    method exists replaced by the values returned from
 
994
                    those methods.
 
995
 
 
996
            Returns a representable object based on the calling class.
 
997
        """
 
998
 
 
999
        args = cls.unrep_jdata(jdata)
 
1000
 
 
1001
        # Remove the tag from the arguments.
 
1002
        if "tag" in args:
 
1003
            del args["tag"]
 
1004
 
 
1005
        # Remove any temporary data
 
1006
        for name in args.keys():
 
1007
            if name.startswith("<") and name.endswith(">"):
 
1008
                del args[name]
 
1009
 
 
1010
        # Create a new object from the arguments.
 
1011
        obj = cls(** args)
 
1012
 
 
1013
        return obj
 
1014
 
 
1015
    def to_json(self, usenorep=False):
 
1016
        """ Create a JSON representation from the object.
 
1017
 
 
1018
            usenorep:    A flag indicating whether or not objects that
 
1019
                        cannot be represented should be represented as
 
1020
                        None. If True, None will be used for such
 
1021
                        objects. If False, sbak.error.RepresentError
 
1022
                        will be raised. Default is False.
 
1023
        """
 
1024
 
 
1025
        # Empty representation.
 
1026
        jdata = {}
 
1027
 
 
1028
        # Add tag to the data if tag is available.
 
1029
        if hasattr(self, "tag"):
 
1030
            jdata["tag"] = self.tag
 
1031
 
 
1032
        # Compile a mapping of all unreppers from the MRO
 
1033
        reppers = {}
 
1034
        for base in reversed(self.__class__.__mro__):
 
1035
            if hasattr(base, "reppers"):
 
1036
                reppers.update(base.reppers)
 
1037
 
 
1038
        # Represent attributes that are marked as representable.
 
1039
        for name, rep in reppers.iteritems():
 
1040
            # Try different calling conventions until one works, or until
 
1041
            # there are no other options.
939
1042
            try:
940
 
                # Deliberately cause an invalid index/key error. This will determine
941
 
                # whether the format is full (a mapping) or shorthand (a sqeuence).
942
 
                L = len(sjdata)
943
 
                sjdata[L]
944
 
 
945
 
            # The data appears to be in full format, not shorthand.
946
 
            except KeyError:
947
 
                jdata = sjdata
948
 
 
949
 
            # The data appears to be shorthand format.
950
 
            except IndexError:
951
 
                # Add the class tag to the keyorder. (The tag is always first.)
952
 
                keyorder = (cls.tag,) + tuple(cls.keyorder)
953
 
 
954
 
                # Convert shorthand format to full format.
955
 
                jdata = dict((key, sjdata[n]) for n, key in enumerate(keyorder))
956
 
 
957
 
            # Since no exception occured, there is no way to determine the format.
 
1043
                v = rep(self, usenorep)
 
1044
            except TypeError:
 
1045
                v = rep(self)
 
1046
            # Don't rep NOREP
 
1047
            if v is not NOREP:
 
1048
                jdata[name] = v
 
1049
 
 
1050
        return jdata
 
1051
 
 
1052
# These are needed to make sure that the missing class attribute warnings are
 
1053
# triggered when necessary.
 
1054
 
 
1055
class ResourceMixin(RepresentableMixin):
 
1056
    """ Mixin class to add resource functionality to a class.
 
1057
 
 
1058
        In addition to being representable, resource classes also have
 
1059
        save() and load() methods for saving and loading resource data
 
1060
        from files.
 
1061
    """
 
1062
 
 
1063
    @classmethod
 
1064
    def load(cls, src):
 
1065
        """ Loads JSON data from a file and decodes it into a resource.
 
1066
 
 
1067
            src:        A file name or file-like object from which to
 
1068
                        load the JSON data.
 
1069
 
 
1070
            This method does not normally need to be overridden since
 
1071
            the real work is in the JSON decoder method for subclasses
 
1072
            and not in the raw data loading. However, should a subclass
 
1073
            need to load data from some other format besides ordinary
 
1074
            JSON, this may need to be overridden.
 
1075
 
 
1076
            Returns a resource.
 
1077
        """
 
1078
        # Determine the correct way to use `src`.
 
1079
 
 
1080
        # If `src` is a string, open a file using `src` as a filename.
 
1081
        if isinstance(src, basestring):
 
1082
            fsrc = open(src)
 
1083
        # If `src` is not a string, assume it is an open file.
 
1084
        else:
 
1085
            fsrc = src
 
1086
 
 
1087
        try:
 
1088
            # Load the JSON data from the file.
 
1089
            obj = json.load(fsrc)
 
1090
        except ValueError, e:
 
1091
            # Raise a standardized FormatError if there were any value errors
 
1092
            # while loading the JSON data.
 
1093
            raise _file_format_err(cls.tag, e, src)
 
1094
 
 
1095
        # Close the file if necessary.
 
1096
        if src is not fsrc:
 
1097
            fsrc.close()
 
1098
 
 
1099
        # Turn the JSON object into a resource object.
 
1100
        obj = cls.from_json(obj)
 
1101
 
 
1102
        return obj
 
1103
 
 
1104
    def save(self, dest=None, usenorep=False):
 
1105
        """ Save a JSON representation of the resource to a file.
 
1106
 
 
1107
            dest:       Destination file. May be an open file-like
 
1108
                        object or a file name. If None (default), the
 
1109
                        file name will be determine using the `src`
 
1110
                        attribute.
 
1111
 
 
1112
            usenorep:    If true, referenced resources that cannot be
 
1113
                        represented as JSON or as filenames will be
 
1114
                        represented as None (JSON 'null'). If false
 
1115
                        (default), sbak.error.RepresentError will be
 
1116
                        raised when such an object is encountered.
 
1117
 
 
1118
            This method does not normally need to be overridden because
 
1119
            most of the work is performed by ResourceMixin.to_json.
 
1120
            However, should a subclass need to save its data using some
 
1121
            format other than JSON, overriding may be necessary.
 
1122
 
 
1123
            Raises ValueError if no file name is provided and no file name
 
1124
            is associated with the resource.
 
1125
        """
 
1126
        if dest is None:
 
1127
            dest = self.src
 
1128
        if dest is None:
 
1129
            raise ValueError(
 
1130
                "Resource %r has no source file name associated, and none "
 
1131
                "was provided." % self
 
1132
            )
 
1133
 
 
1134
        # Encode myself
 
1135
        obj = self.to_json(usenorep)
 
1136
 
 
1137
        # Save encoded self to file.
 
1138
        try:
 
1139
            # Open dest for writing,
 
1140
            close = False
 
1141
            if isinstance(dest, basestring):
 
1142
                dest = open(dest, 'w')
 
1143
                close = True
 
1144
 
 
1145
            # and save encoded self to it.
 
1146
            json.dump(obj, dest, sort_keys=True, indent=4)
 
1147
 
 
1148
        # Make sure the file gets closed.
 
1149
        finally:
 
1150
            if close:
 
1151
                dest.close()
 
1152
 
 
1153
    def save_as(self, dest, usenorep=False):
 
1154
        """ Saves the resource to the given location and changes its src.
 
1155
 
 
1156
            This will save the resource to the given destination and also
 
1157
            change the source file name associated with the resource
 
1158
            within the resource manager. (If the resource manager does not
 
1159
            have a filename associated with the resource, the destination
 
1160
            will be associated with the resource.)
 
1161
 
 
1162
            A ValueError will occur if you try to save a resource to a
 
1163
            location that is known to belong to a loaded resource object
 
1164
            other than the current one.
 
1165
 
 
1166
            A TypeError will be raised if dest is None.
 
1167
        """
 
1168
        if dest is None:
 
1169
            raise TypeError(
 
1170
                "A destination must be specified when using `save_as`."
 
1171
            )
 
1172
 
 
1173
        self.save(dest, usenorep)
 
1174
        try:
 
1175
            clear_res(self)
 
1176
        except ValueError:
 
1177
            pass
 
1178
        set_src(self, dest)
 
1179
 
 
1180
    @property
 
1181
    def src(self):
 
1182
        """ The source file path that was used to load the resource.
 
1183
 
 
1184
            This will be None if the resource was not loaded from a file or
 
1185
            if the resource was cleared from the resource manager.
 
1186
 
 
1187
            The value of this attribute is determined dynamically by
 
1188
            calling `resman.get_src` on the resource in question. If that
 
1189
            raises a ValueError, the src is determined to be None.
 
1190
 
 
1191
            Read-only. You must use `resman.clear_src` and `resman.set_src`
 
1192
            in order to change this value, or you must save the resource to
 
1193
            a new location using the resource's `save_as` method.
 
1194
        """
 
1195
        try:
 
1196
            return get_src(self)
 
1197
        except ValueError:
 
1198
            return None
 
1199
 
 
1200
class ResourceGroupMixin(ResourceMixin):
 
1201
    """ Allows a bunch of resources to be collected together in a mapping.
 
1202
 
 
1203
        ResourceMixinGroup() -> resgroup
 
1204
        ResourceMixinGroup(resources) -> resgroup
 
1205
        ResourceMixinGroup(name=resource [, ...]) -> resgroup
 
1206
        ResourceMixinGroup.load(filename) -> resgroup
 
1207
        ResourceMixinGroup.from_json(jdata) -> resgroup
 
1208
 
 
1209
        Examples:
 
1210
 
 
1211
            # ResourceMixinGroup must be subclassed to be used. The subclass
 
1212
            # must define a classmethod called `resource_from_json` as follows:
 
1213
 
 
1214
            class MyResourceMap(ResourceMixinGroup):
 
1215
                @classmethod
 
1216
                def resource_from_json(cls, jdata):
 
1217
                    # ... Code to parse jdata into a resource ...
 
1218
                    return new_resource
 
1219
 
 
1220
            resgroup = MyResourceMap()
 
1221
 
 
1222
            resgroup = MyResourceMap({
 
1223
                "nameA" : resourceA,
 
1224
                "nameB" : resourceB,
 
1225
            })
 
1226
 
 
1227
            resgroup = MyResourceMap(
 
1228
                nameA = resourceA,
 
1229
                nameB = resourceB
 
1230
            )
 
1231
 
 
1232
            resgroup = MyResourceMap.load("my_resource.resgroup")
 
1233
 
 
1234
            resgroup = MyResourceMap.from_json( jdata )
 
1235
 
 
1236
        ~~~ Description ~~~
 
1237
 
 
1238
        The ResourceMixinGroup class is used as a base for creating resource
 
1239
        map classes. A "resource group" is a collection of resources which are
 
1240
        related, though not necessarily connected via references, and which must
 
1241
        all be present in memory at once.
 
1242
 
 
1243
        A resource group functions like a mapping, such as a Python dict
 
1244
        object. Each key in the mapping is the name of the resource within the
 
1245
        mapping and may be used to reference resources within the resource group.
 
1246
 
 
1247
        *** All keys MUST be strings! ***
 
1248
        This includes regular strings as well as unicode strings. A TypeError
 
1249
        will be raised if ever an attempt is made to assign a value using a key
 
1250
        that is not a string. This limitation is put in place to prevent errors
 
1251
        when storing the resource group as a representation.
 
1252
 
 
1253
        Items are automatically sorted whenever an item is added to the
 
1254
        resource group. This means that when iterating over the resource's data
 
1255
        using the `keys`, `values`, or `iteritems` methods, items will appear in
 
1256
        alphanumeric order based on key.
 
1257
 
 
1258
        ~~~ Mapping Operations ~~~
 
1259
 
 
1260
        Mapping operations supported include the following:
 
1261
 
 
1262
        resgroup["name"] = resource       # Set a resource to a key
 
1263
        resource = resgroup["name"]       # Get a resource from a key
 
1264
        del resource["name"]            # Remove a resource from a key
 
1265
        "name" in resgroup                # Test for existence of a key
 
1266
        len(resgroup)                     # Get the number of items
 
1267
        iter(resgroup)                    # Iterate over keys (for loops, etc.)
 
1268
        resgroup.keys()                   # Get a list of keys (sorted)
 
1269
        resgroup.values()                 # Get a list of values (sorted by key)
 
1270
        resgroup.iteritems()              # Iterate over key,resource pairs
 
1271
                                        # (sorted by key)
 
1272
 
 
1273
        ~~~ Resource Key-Paths ~~~
 
1274
 
 
1275
        The ResourceMixinGroup object supports a special kind of key. It is
 
1276
        called a "key-path" because it is a key that functions like a path, as
 
1277
        in a file system.
 
1278
 
 
1279
        To use this feature, the resource group must have resources in it that are
 
1280
        other resource paths, and those resource groups must have resources in
 
1281
        them. Once this is asserted, the resources of one of the inner resource
 
1282
        maps may be accessed by indexing the outermost resource group with a
 
1283
        sequence of names. These names indicate the "path" taken to get from the
 
1284
        outermost level of the resource group hierarchy to the inner resource
 
1285
        being targeted.
 
1286
 
 
1287
        Example:
 
1288
 
 
1289
            resgroup = ResourceMixinGroup(
 
1290
                foo = ResourceMixinGroup(
 
1291
                    innerFoo = resourceA,
 
1292
                    innerBar = resourceB
 
1293
                ),
 
1294
                bar = resourceC
 
1295
            )
 
1296
 
 
1297
            myres = resgroup["foo", "innerFoo"]   # 1. myres gets resourceA
 
1298
            myres = resgroup["foo", "innerBar"]   # 2. myres gets resourceB
 
1299
            myres = resgroup["bar"]               # 3. myres gets resourceC
 
1300
            myres = resgroup["foo"]["innerFoo"]   # 4. Same as 1
 
1301
            myres = resgroup["foo"]["innerBar"]   # 5. Same as 2
 
1302
 
 
1303
            location = ("foo", "innerFoo")
 
1304
            myres = resgroup[location]            # 6. Same as 1 and 4
 
1305
 
 
1306
        This feature is implemented with the intent of allowing locations within
 
1307
        a complex resource tree to be stored as a single unit and used in a
 
1308
        convenient way (such as example #6 above).
 
1309
 
 
1310
        TODO: Add support for custom reference types. (A reference is any
 
1311
        potential resource representation that contains a key that begins with
 
1312
        the reference symbol.)
 
1313
    """
 
1314
 
 
1315
    keyorder = "resources",
 
1316
 
 
1317
    def __init__(self, resources = (), **kwargs):
 
1318
        """ Initializes an ResourceMixinGroupt with optional initial items
 
1319
 
 
1320
            ResourceMixinGroup.__init__(self) -> None
 
1321
            ResourceMixinGroup.__init__(self, resources) -> None
 
1322
            ResourceMixinGroup.__init__(self, name = resource [, ...]) ->
 
1323
 
 
1324
            resources:  Must be a mapping or sequence of (name, resource) pairs.
 
1325
                        Defaults to an empty tuple.
 
1326
 
 
1327
            name = resource [, ...]:
 
1328
                        You may also provide resources as a series of keyword
 
1329
                        arguments, where the argument name is the key that will
 
1330
                        be used to access the resource and the value of the
 
1331
                        argument is the resource being added.
 
1332
 
 
1333
            Note that if you use both the `resources` argument as well as
 
1334
            additional keyword arguments then both sets of data will be used,
 
1335
            with the keyword arguments taking precedence if there is a naming
 
1336
            conflict.
 
1337
        """
 
1338
        resources = self._items = dict(resources)
 
1339
        resources.update(kwargs)
 
1340
        self._keyorder = sorted(resources.keys())
 
1341
 
 
1342
 
 
1343
    @classmethod
 
1344
    def _unrep_resources(cls, value, jdata):
 
1345
        """ Unrepper for `resources`.
 
1346
 
 
1347
            value:
 
1348
 
 
1349
                A mapping where keys indicate names of resources and the
 
1350
                values are the representations of those resources, except
 
1351
                for when the value is another mapping that contains a key
 
1352
                that is one of the strings "&ref" or "&copy". In such a
 
1353
                case, the value of the "&ref" or "&copy" key refers to
 
1354
                a resource with the given name within the current
 
1355
                ResourceMixinGroup.
 
1356
 
 
1357
                Ex:  {"&ref": "foo"}    Indicates a reference to a resource
 
1358
                                        named "foo" within the current
 
1359
                ResourceMixinGroup. If the resource named "foo" has been
 
1360
                unrepresented by the time {"&ref" : "foo"} is processed,
 
1361
                {"&ref": "foo"} will become another occurence of "foo"
 
1362
                within the curent ResourceMixinGroup. If "foo" has not yet been
 
1363
                unrepresented, the name of {"&ref": "foo"} and the string
 
1364
                "foo" itself will both be remembered, and as soon as "foo"
 
1365
                is unrepresented, {"&ref": "foo"} will become an alternate
 
1366
                reference to "foo".
 
1367
 
 
1368
            Example value for "resources" key:
 
1369
 
 
1370
                ...
 
1371
                "resources" : {
 
1372
                    "resource_foo": foo_representation_or_filename,
 
1373
                    "resource_bar": bar_representation_or_filename,
 
1374
                    "resource_spam": {"&ref": "resource_foo"}
 
1375
                },
 
1376
                ...
 
1377
 
 
1378
            This method expects a classmethod called `resource_from_json`
 
1379
            to be defined within the current ResourceMixinGroup
 
1380
            subclass. It must be defined with a signature and return value
 
1381
            as indicated by the following example method:
 
1382
 
 
1383
                @classmethod
 
1384
                def resource_from_json(cls, rep, jdata):
 
1385
                    ''' Return an object based on a given rep.
 
1386
 
 
1387
                        rep:    Representation for an object of the type
 
1388
                                that is handled by the current
 
1389
                                ResourceMixinGroup subclass.
 
1390
 
 
1391
                        jdata:  The full JSON representation of the
 
1392
                                resource group that is currently being
 
1393
                                unrepresented. Provided in case there is
 
1394
                                some information in the resource group that
 
1395
                                affects all resources contained within.
 
1396
                    '''
 
1397
                    # ...processing using rep and/or jdata...
 
1398
                    return obj_from_rep
 
1399
 
 
1400
            `_unrep_resources` returns a mapping of loaded resources
 
1401
            suitable for use as the parameter to the ResourceMixinGroup
 
1402
            constructor.
 
1403
 
 
1404
            TODO: Make the use of "&" more generalized so that it can be used to create custom reference types
 
1405
        """
 
1406
 
 
1407
        # This will be used to store resources as they are loaded.
 
1408
        resources = {}
 
1409
 
 
1410
        # This will be used to store references that need to be defreferenced.
 
1411
        refs_waiting = {}
 
1412
 
 
1413
        # This will be used to store copy references that need to be copied.
 
1414
        copies_waiting = {}
 
1415
 
 
1416
        # Iterate over the resoruce representations
 
1417
        for name, rep in value.iteritems():
 
1418
            # See if its a string (ie. filename)
 
1419
            if isinstance(rep,basestring):
 
1420
                # It's a filename. Attempt to get the resource.
 
1421
                resources[name] = get(rep)
958
1422
            else:
959
 
                raise _format_err(
960
 
                    cls.tag,
961
 
                    "Unable to determine whether data format is full or shorthand! "
962
 
                    "Either the data object given is a mapping/sequence that has a "
963
 
                    "key/index of %r in it or else the data object does not raise "
964
 
                    "errors in the standard way.\nObject:\n%r" % (L, sjdata)
965
 
                 )
966
 
 
967
 
            return jdata
968
 
 
969
 
        @classmethod
970
 
        def unrep_jdata(cls, jdata):
971
 
            """ Unrepresents given JSON data for use with the constructor.
972
 
 
973
 
                This method is called to turn data in the given JSON data dict
974
 
                into data that is compatible with the representable class's
975
 
                constructor. It does not actually create an instance of the
976
 
                class; it merely returns a dict containing data that was created
977
 
                by procesing the given jdata using the class's unreppers.
978
 
            """
979
 
 
980
 
            # Compile a mapping of all unreppers from the MRO
981
 
            unreppers = {}
982
 
            for base in reversed(cls.__mro__):
983
 
                if hasattr(base, "unreppers"):
984
 
                    unreppers.update(base.unreppers)
985
 
 
986
 
            # The keys in jdata will most likely be unicode, so they must be
987
 
            # converted to string.
988
 
            data = dict((str(k), v) for k, v in jdata.iteritems())
989
 
 
990
 
            # Sort the unreppers by the class' key order.
991
 
            # (Keys not in the keyorder are placed at the end.)
992
 
            keyorder = list(cls.keyorder)
993
 
            keyorder.extend(k for k in unreppers.keys() if k not in keyorder)
994
 
            unreppers = [(k, unreppers[k]) for k in keyorder if k in unreppers]
995
 
 
996
 
            # Convert jdata representations to actual data.
997
 
            for name, unrep in unreppers:
998
 
 
999
 
                # Skip values that are not present in both the data and the
1000
 
                # unrepper table
1001
 
                if name in jdata:
1002
 
 
1003
 
                    # Choose the calling convention appropriate for the unrepper
1004
 
                    argc = len(inspect.getargspec(unrep)[0])
1005
 
                    if argc == 3:
1006
 
                        v = unrep(jdata[name], jdata)
1007
 
                    else:
1008
 
                        v = unrep(jdata[name])
1009
 
                    
1010
 
                    # Don't unrep NOREP
1011
 
                    if v is NOREP:
1012
 
                        del data[name]
1013
 
                    else:
1014
 
                        data[name] = v
1015
 
 
1016
 
            return data
1017
 
 
1018
 
        @classmethod
1019
 
        def from_json(cls, jdata):
1020
 
            """ Create an object from a JSON representation.
1021
 
 
1022
 
                jdata:  JSON representation of the object. A copy of this
1023
 
                        mapping will be expanded into the arguments for
1024
 
                        the constructor with values for which a _unrep_
1025
 
                        method exists replaced by the values returned from
1026
 
                        those methods.
1027
 
 
1028
 
                Returns a representable object based on the calling class.
1029
 
            """
1030
 
 
1031
 
            args = cls.unrep_jdata(jdata)
1032
 
 
1033
 
            # Remove the tag from the arguments.
1034
 
            if "tag" in args:
1035
 
                del args["tag"]
1036
 
 
1037
 
            # Remove any temporary data
1038
 
            for name in args.keys():
1039
 
                if name.startswith("<") and name.endswith(">"):
1040
 
                    del args[name]
1041
 
 
1042
 
            # Create a new object from the arguments.
1043
 
            obj = cls(** args)
1044
 
 
1045
 
            return obj
1046
 
 
1047
 
        def to_json(self, usenorep=False):
1048
 
            """ Create a JSON representation from the object.
1049
 
 
1050
 
                usenorep:    A flag indicating whether or not objects that
1051
 
                            cannot be represented should be represented as
1052
 
                            None. If True, None will be used for such
1053
 
                            objects. If False, sbak.error.RepresentError
1054
 
                            will be raised. Default is False.
1055
 
            """
1056
 
 
1057
 
            # Empty representation.
1058
 
            jdata = {}
1059
 
 
1060
 
            # Add tag to the data if tag is available.
1061
 
            if hasattr(self, "tag"):
1062
 
                jdata["tag"] = self.tag
1063
 
 
1064
 
            # Compile a mapping of all unreppers from the MRO
1065
 
            reppers = {}
1066
 
            for base in reversed(self.__class__.__mro__):
1067
 
                if hasattr(base, "reppers"):
1068
 
                    reppers.update(base.reppers)
1069
 
 
1070
 
            # Represent attributes that are marked as representable.
1071
 
            for name, rep in reppers.iteritems():
1072
 
                # Try different calling conventions until one works, or until
1073
 
                # there are no other options.
 
1423
                # Not a filename. See if its a regular reference.
1074
1424
                try:
1075
 
                    v = rep(self, usenorep)
1076
 
                except TypeError:
1077
 
                    v = rep(self)
1078
 
                # Don't rep NOREP
1079
 
                if v is not NOREP:
1080
 
                    jdata[name] = v
1081
 
 
1082
 
            return jdata
1083
 
 
1084
 
    # These are needed to make sure that the missing class attribute warnings are
1085
 
    # triggered when necessary.
1086
 
 
1087
 
    class ResourceMixin(RepresentableMixin):
1088
 
        """ Mixin class to add resource functionality to a class.
1089
 
 
1090
 
            In addition to being representable, resource classes also have
1091
 
            save() and load() methods for saving and loading resource data
1092
 
            from files.
1093
 
        """
1094
 
 
1095
 
        @classmethod
1096
 
        def load(cls, src):
1097
 
            """ Loads JSON data from a file and decodes it into a resource.
1098
 
 
1099
 
                src:        A file name or file-like object from which to
1100
 
                            load the JSON data.
1101
 
 
1102
 
                This method does not normally need to be overridden since
1103
 
                the real work is in the JSON decoder method for subclasses
1104
 
                and not in the raw data loading. However, should a subclass
1105
 
                need to load data from some other format besides ordinary
1106
 
                JSON, this may need to be overridden.
1107
 
 
1108
 
                Returns a resource.
1109
 
            """
1110
 
            # Determine the correct way to use `src`.
1111
 
 
1112
 
            # If `src` is a string, open a file using `src` as a filename.
1113
 
            if isinstance(src, basestring):
1114
 
                fsrc = open(src)
1115
 
            # If `src` is not a string, assume it is an open file.
1116
 
            else:
1117
 
                fsrc = src
1118
 
 
1119
 
            try:
1120
 
                # Load the JSON data from the file.
1121
 
                obj = json.load(fsrc)
1122
 
            except ValueError, e:
1123
 
                # Raise a standardized FormatError if there were any value errors
1124
 
                # while loading the JSON data.
1125
 
                raise _file_format_err(cls.tag, e, src)
1126
 
 
1127
 
            # Close the file if necessary.
1128
 
            if src is not fsrc:
1129
 
                fsrc.close()
1130
 
 
1131
 
            # Turn the JSON object into a resource object.
1132
 
            obj = cls.from_json(obj)
1133
 
 
1134
 
            return obj
1135
 
 
1136
 
        def save(self, dest=None, usenorep=False):
1137
 
            """ Save a JSON representation of the resource to a file.
1138
 
 
1139
 
                dest:       Destination file. May be an open file-like
1140
 
                            object or a file name. If None (default), the
1141
 
                            file name will be determine using the `src`
1142
 
                            attribute.
1143
 
 
1144
 
                usenorep:    If true, referenced resources that cannot be
1145
 
                            represented as JSON or as filenames will be
1146
 
                            represented as None (JSON 'null'). If false
1147
 
                            (default), sbak.error.RepresentError will be
1148
 
                            raised when such an object is encountered.
1149
 
 
1150
 
                This method does not normally need to be overridden because
1151
 
                most of the work is performed by ResourceMixin.to_json.
1152
 
                However, should a subclass need to save its data using some
1153
 
                format other than JSON, overriding may be necessary.
1154
 
 
1155
 
                Raises ValueError if no file name is provided and no file name
1156
 
                is associated with the resource.
1157
 
            """
1158
 
            if dest is None:
1159
 
                dest = self.src
1160
 
            if dest is None:
1161
 
                raise ValueError(
1162
 
                    "Resource %r has no source file name associated, and none "
1163
 
                    "was provided." % self
1164
 
                )
1165
 
 
1166
 
            # Encode myself
1167
 
            obj = self.to_json(usenorep)
1168
 
 
1169
 
            # Save encoded self to file.
1170
 
            try:
1171
 
                # Open dest for writing,
1172
 
                close = False
1173
 
                if isinstance(dest, basestring):
1174
 
                    dest = open(dest, 'w')
1175
 
                    close = True
1176
 
 
1177
 
                # and save encoded self to it.
1178
 
                json.dump(obj, dest, sort_keys=True, indent=4)
1179
 
 
1180
 
            # Make sure the file gets closed.
1181
 
            finally:
1182
 
                if close:
1183
 
                    dest.close()
1184
 
        
1185
 
        def save_as(self, dest, usenorep=False):
1186
 
            """ Saves the resource to the given location and changes its src.
1187
 
            
1188
 
                This will save the resource to the given destination and also
1189
 
                change the source file name associated with the resource
1190
 
                within the resource manager. (If the resource manager does not
1191
 
                have a filename associated with the resource, the destination
1192
 
                will be associated with the resource.)
1193
 
                
1194
 
                A ValueError will occur if you try to save a resource to a
1195
 
                location that is known to belong to a loaded resource object
1196
 
                other than the current one.
1197
 
 
1198
 
                A TypeError will be raised if dest is None.
1199
 
            """
1200
 
            if dest is None:
1201
 
                raise TypeError(
1202
 
                    "A destination must be specified when using `save_as`."
1203
 
                )
1204
 
 
1205
 
            self.save(dest, usenorep)
1206
 
            try:
1207
 
                clear_res(self)
1208
 
            except ValueError:
1209
 
                pass
1210
 
            set_src(self, dest)
1211
 
 
1212
 
        @property
1213
 
        def src(self):
1214
 
            """ The source file path that was used to load the resource.
1215
 
 
1216
 
                This will be None if the resource was not loaded from a file or
1217
 
                if the resource was cleared from the resource manager.
1218
 
 
1219
 
                The value of this attribute is determined dynamically by
1220
 
                calling `resman.get_src` on the resource in question. If that
1221
 
                raises a ValueError, the src is determined to be None.
1222
 
 
1223
 
                Read-only. You must use `resman.clear_src` and `resman.set_src`
1224
 
                in order to change this value, or you must save the resource to
1225
 
                a new location using the resource's `save_as` method.
1226
 
            """
1227
 
            try:
1228
 
                return get_src(self)
1229
 
            except ValueError:
1230
 
                return None
1231
 
 
1232
 
    class ResourceMixinGroup(ResourceMixin):
1233
 
        """ Allows a bunch of resources to be collected together in a mapping.
1234
 
 
1235
 
            ResourceMixinGroup() -> resgroup
1236
 
            ResourceMixinGroup(resources) -> resgroup
1237
 
            ResourceMixinGroup(name=resource [, ...]) -> resgroup
1238
 
            ResourceMixinGroup.load(filename) -> resgroup
1239
 
            ResourceMixinGroup.from_json(jdata) -> resgroup
1240
 
 
1241
 
            Examples:
1242
 
 
1243
 
                # ResourceMixinGroup must be subclassed to be used. The subclass
1244
 
                # must define a classmethod called `resource_from_json` as follows:
1245
 
 
1246
 
                class MyResourceMap(ResourceMixinGroup):
1247
 
                    @classmethod
1248
 
                    def resource_from_json(cls, jdata):
1249
 
                        # ... Code to parse jdata into a resource ...
1250
 
                        return new_resource
1251
 
 
1252
 
                resgroup = MyResourceMap()
1253
 
 
1254
 
                resgroup = MyResourceMap({
1255
 
                    "nameA" : resourceA,
1256
 
                    "nameB" : resourceB,
1257
 
                })
1258
 
 
1259
 
                resgroup = MyResourceMap(
1260
 
                    nameA = resourceA,
1261
 
                    nameB = resourceB
1262
 
                )
1263
 
 
1264
 
                resgroup = MyResourceMap.load("my_resource.resgroup")
1265
 
 
1266
 
                resgroup = MyResourceMap.from_json( jdata )
1267
 
 
1268
 
            ~~~ Description ~~~
1269
 
 
1270
 
            The ResourceMixinGroup class is used as a base for creating resource
1271
 
            map classes. A "resource group" is a collection of resources which are
1272
 
            related, though not necessarily connected via references, and which must
1273
 
            all be present in memory at once.
1274
 
 
1275
 
            A resource group functions like a mapping, such as a Python dict
1276
 
            object. Each key in the mapping is the name of the resource within the
1277
 
            mapping and may be used to reference resources within the resource group.
1278
 
 
1279
 
            *** All keys MUST be strings! ***
1280
 
            This includes regular strings as well as unicode strings. A TypeError
1281
 
            will be raised if ever an attempt is made to assign a value using a key
1282
 
            that is not a string. This limitation is put in place to prevent errors
1283
 
            when storing the resource group as a representation.
1284
 
 
1285
 
            Items are automatically sorted whenever an item is added to the
1286
 
            resource group. This means that when iterating over the resource's data
1287
 
            using the `keys`, `values`, or `iteritems` methods, items will appear in
1288
 
            alphanumeric order based on key.
1289
 
 
1290
 
            ~~~ Mapping Operations ~~~
1291
 
 
1292
 
            Mapping operations supported include the following:
1293
 
 
1294
 
            resgroup["name"] = resource       # Set a resource to a key
1295
 
            resource = resgroup["name"]       # Get a resource from a key
1296
 
            del resource["name"]            # Remove a resource from a key
1297
 
            "name" in resgroup                # Test for existence of a key
1298
 
            len(resgroup)                     # Get the number of items
1299
 
            iter(resgroup)                    # Iterate over keys (for loops, etc.)
1300
 
            resgroup.keys()                   # Get a list of keys (sorted)
1301
 
            resgroup.values()                 # Get a list of values (sorted by key)
1302
 
            resgroup.iteritems()              # Iterate over key,resource pairs
1303
 
                                            # (sorted by key)
1304
 
 
1305
 
            ~~~ Resource Key-Paths ~~~
1306
 
 
1307
 
            The ResourceMixinGroup object supports a special kind of key. It is
1308
 
            called a "key-path" because it is a key that functions like a path, as
1309
 
            in a file system.
1310
 
 
1311
 
            To use this feature, the resource group must have resources in it that are
1312
 
            other resource paths, and those resource groups must have resources in
1313
 
            them. Once this is asserted, the resources of one of the inner resource
1314
 
            maps may be accessed by indexing the outermost resource group with a
1315
 
            sequence of names. These names indicate the "path" taken to get from the
1316
 
            outermost level of the resource group hierarchy to the inner resource
1317
 
            being targeted.
1318
 
 
1319
 
            Example:
1320
 
 
1321
 
                resgroup = ResourceMixinGroup(
1322
 
                    foo = ResourceMixinGroup(
1323
 
                        innerFoo = resourceA,
1324
 
                        innerBar = resourceB
1325
 
                    ),
1326
 
                    bar = resourceC
1327
 
                )
1328
 
 
1329
 
                myres = resgroup["foo", "innerFoo"]   # 1. myres gets resourceA
1330
 
                myres = resgroup["foo", "innerBar"]   # 2. myres gets resourceB
1331
 
                myres = resgroup["bar"]               # 3. myres gets resourceC
1332
 
                myres = resgroup["foo"]["innerFoo"]   # 4. Same as 1
1333
 
                myres = resgroup["foo"]["innerBar"]   # 5. Same as 2
1334
 
 
1335
 
                location = ("foo", "innerFoo")
1336
 
                myres = resgroup[location]            # 6. Same as 1 and 4
1337
 
 
1338
 
            This feature is implemented with the intent of allowing locations within
1339
 
            a complex resource tree to be stored as a single unit and used in a
1340
 
            convenient way (such as example #6 above).
1341
 
 
1342
 
            TODO: Add support for custom reference types. (A reference is any
1343
 
            potential resource representation that contains a key that begins with
1344
 
            the reference symbol.)
1345
 
        """
1346
 
 
1347
 
        keyorder = "resources",
1348
 
 
1349
 
        def __init__(self, resources = (), **kwargs):
1350
 
            """ Initializes an ResourceMixinGroupt with optional initial items
1351
 
 
1352
 
                ResourceMixinGroup.__init__(self) -> None
1353
 
                ResourceMixinGroup.__init__(self, resources) -> None
1354
 
                ResourceMixinGroup.__init__(self, name = resource [, ...]) ->
1355
 
 
1356
 
                resources:  Must be a mapping or sequence of (name, resource) pairs.
1357
 
                            Defaults to an empty tuple.
1358
 
 
1359
 
                name = resource [, ...]:
1360
 
                            You may also provide resources as a series of keyword
1361
 
                            arguments, where the argument name is the key that will
1362
 
                            be used to access the resource and the value of the
1363
 
                            argument is the resource being added.
1364
 
 
1365
 
                Note that if you use both the `resources` argument as well as
1366
 
                additional keyword arguments then both sets of data will be used,
1367
 
                with the keyword arguments taking precedence if there is a naming
1368
 
                conflict.
1369
 
            """
1370
 
            resources = self._items = dict(resources)
1371
 
            resources.update(kwargs)
1372
 
            self._keyorder = sorted(resources.keys())
1373
 
 
1374
 
 
1375
 
        @classmethod
1376
 
        def _unrep_resources(cls, value, jdata):
1377
 
            """ Unrepper for `resources`.
1378
 
 
1379
 
                value:
1380
 
 
1381
 
                    A mapping where keys indicate names of resources and the
1382
 
                    values are the representations of those resources, except
1383
 
                    for when the value is another mapping that contains a key
1384
 
                    that is one of the strings "&ref" or "&copy". In such a
1385
 
                    case, the value of the "&ref" or "&copy" key refers to
1386
 
                    a resource with the given name within the current
1387
 
                    ResourceMixinGroup.
1388
 
 
1389
 
                    Ex:  {"&ref": "foo"}    Indicates a reference to a resource
1390
 
                                            named "foo" within the current
1391
 
                    ResourceMixinGroup. If the resource named "foo" has been
1392
 
                    unrepresented by the time {"&ref" : "foo"} is processed,
1393
 
                    {"&ref": "foo"} will become another occurence of "foo"
1394
 
                    within the curent ResourceMixinGroup. If "foo" has not yet been
1395
 
                    unrepresented, the name of {"&ref": "foo"} and the string
1396
 
                    "foo" itself will both be remembered, and as soon as "foo"
1397
 
                    is unrepresented, {"&ref": "foo"} will become an alternate
1398
 
                    reference to "foo".
1399
 
 
1400
 
                Example value for "resources" key:
1401
 
 
1402
 
                    ...
1403
 
                    "resources" : {
1404
 
                        "resource_foo": foo_representation_or_filename,
1405
 
                        "resource_bar": bar_representation_or_filename,
1406
 
                        "resource_spam": {"&ref": "resource_foo"}
1407
 
                    },
1408
 
                    ...
1409
 
 
1410
 
                This method expects a classmethod called `resource_from_json`
1411
 
                to be defined within the current ResourceMixinGroup
1412
 
                subclass. It must be defined with a signature and return value
1413
 
                as indicated by the following example method:
1414
 
 
1415
 
                    @classmethod
1416
 
                    def resource_from_json(cls, rep, jdata):
1417
 
                        ''' Return an object based on a given rep.
1418
 
 
1419
 
                            rep:    Representation for an object of the type
1420
 
                                    that is handled by the current
1421
 
                                    ResourceMixinGroup subclass.
1422
 
 
1423
 
                            jdata:  The full JSON representation of the
1424
 
                                    resource group that is currently being
1425
 
                                    unrepresented. Provided in case there is
1426
 
                                    some information in the resource group that
1427
 
                                    affects all resources contained within.
1428
 
                        '''
1429
 
                        # ...processing using rep and/or jdata...
1430
 
                        return obj_from_rep
1431
 
 
1432
 
                `_unrep_resources` returns a mapping of loaded resources
1433
 
                suitable for use as the parameter to the ResourceMixinGroup
1434
 
                constructor.
1435
 
 
1436
 
                TODO: Make the use of "&" more generalized so that it can be used to create custom reference types
1437
 
            """
1438
 
 
1439
 
            # This will be used to store resources as they are loaded.
1440
 
            resources = {}
1441
 
 
1442
 
            # This will be used to store references that need to be defreferenced.
1443
 
            refs_waiting = {}
1444
 
 
1445
 
            # This will be used to store copy references that need to be copied.
1446
 
            copies_waiting = {}
1447
 
 
1448
 
            # Iterate over the resoruce representations
1449
 
            for name, rep in value.iteritems():
1450
 
                # See if its a string (ie. filename)
1451
 
                if isinstance(rep,basestring):
1452
 
                    # It's a filename. Attempt to get the resource.
1453
 
                    resources[name] = get(rep)
1454
 
                else:
1455
 
                    # Not a filename. See if its a regular reference.
 
1425
                    nameref = rep["&ref"]
 
1426
                except (TypeError,KeyError):
 
1427
                    # It's not a regular reference.
 
1428
                    # See if its a copy reference.
1456
1429
                    try:
1457
 
                        nameref = rep["&ref"]
 
1430
                        namecopy = rep["&copy"]
1458
1431
                    except (TypeError,KeyError):
1459
 
                        # It's not a regular reference.
1460
 
                        # See if its a copy reference.
1461
 
                        try:
1462
 
                            namecopy = rep["&copy"]
1463
 
                        except (TypeError,KeyError):
1464
 
                            # It's not a copy reference, either.
1465
 
                            # Assume it is standard jdata. Try to unrep it.
1466
 
                            resources[name] = cls.resource_from_json(rep, jdata)
1467
 
 
1468
 
                            # Look for references waiting for this resource and
1469
 
                            # dereference any that exist.
1470
 
                            for nameref, refname in dict(refs_waiting).iteritems():
1471
 
                                if refname == name:
1472
 
                                    resources[nameref] = resources[name]
1473
 
                                    del refs_waiting[nameref]
1474
 
 
1475
 
                            # Look for copy references waiting for this resource and
1476
 
                            # make copies for any that exist.
1477
 
                            for copyref, refname in dict(copies_waiting).iteritems():
1478
 
                                if refname == name:
1479
 
                                    resources[copyref] =\
1480
 
                                        cls.resource_from_json(rep, jdata)
1481
 
                                    del copies_waiting[copyref]
1482
 
                        else:
1483
 
                            # It appears to be a copy reference. Try to make another
1484
 
                            # copy of the  referenced resource.
1485
 
                            try:
1486
 
                                resources[name] =\
1487
 
                                    cls.resource_from_json(value[namecopy], jdata)
1488
 
                            except KeyError:
1489
 
                                # The resource being referenced does not yet exist.
1490
 
                                # Put this copyref on the mapping of references waiting
1491
 
                                # to be copied.
1492
 
                                copies_waiting[copyref] = name
 
1432
                        # It's not a copy reference, either.
 
1433
                        # Assume it is standard jdata. Try to unrep it.
 
1434
                        resources[name] = cls.resource_from_json(rep, jdata)
 
1435
 
 
1436
                        # Look for references waiting for this resource and
 
1437
                        # dereference any that exist.
 
1438
                        for nameref, refname in dict(refs_waiting).iteritems():
 
1439
                            if refname == name:
 
1440
                                resources[nameref] = resources[name]
 
1441
                                del refs_waiting[nameref]
 
1442
 
 
1443
                        # Look for copy references waiting for this resource and
 
1444
                        # make copies for any that exist.
 
1445
                        for copyref, refname in dict(copies_waiting).iteritems():
 
1446
                            if refname == name:
 
1447
                                resources[copyref] =\
 
1448
                                    cls.resource_from_json(rep, jdata)
 
1449
                                del copies_waiting[copyref]
1493
1450
                    else:
1494
 
                        # It appears to be a reference. Try to get the resource
1495
 
                        # that is being referenced.
 
1451
                        # It appears to be a copy reference. Try to make another
 
1452
                        # copy of the  referenced resource.
1496
1453
                        try:
1497
 
                            resources[name] = resources[nameref]
 
1454
                            resources[name] =\
 
1455
                                cls.resource_from_json(value[namecopy], jdata)
1498
1456
                        except KeyError:
1499
1457
                            # The resource being referenced does not yet exist.
1500
 
                            # Put this nameref on the mapping of references waiting to
1501
 
                            # be dereferenced.
1502
 
                            refs_waiting[nameref] = name
1503
 
 
1504
 
            return resources
1505
 
 
1506
 
        def __getitem__(self,key):
1507
 
            """ Allows references to values (resources) in resgroups to be acquired.
1508
 
 
1509
 
                resgroup.__getitem__(key) <==> resgroup[key]
1510
 
 
1511
 
                This method supports key-paths.
1512
 
            """
1513
 
            try:
1514
 
                if isinstance(key,basestring):
1515
 
                    raise TypeError
1516
 
                ikey = iter(key)
1517
 
            except TypeError:
1518
 
                # This will only run (and succeed) if the key is a string and the
1519
 
                # string is currently used as a key in the mapping.
1520
 
                return self._items[key]
1521
 
            else:
1522
 
                location = self
1523
 
                for k in ikey:
1524
 
                    location = location[k]
1525
 
                return location
1526
 
 
1527
 
        def __setitem__(self,key,value):
1528
 
            """ Allows resources in an ResourceMixinGroup to be added or replaced
1529
 
 
1530
 
                resgroup.__setitem__(key, value) <==> resgroup[key] = value
1531
 
 
1532
 
                This method supports key-paths.
1533
 
            """
1534
 
            try:
1535
 
                if isinstance(key,basestring):
1536
 
                    raise TypeError
1537
 
                ikey = iter(key[:-1])
1538
 
            except TypeError:
1539
 
                if not isinstance(key, basestring):
1540
 
                    # This error is only raised if the key is NEITHER a string NOR
1541
 
                    # any other kind of iterable.
1542
 
                    raise TypeError(
1543
 
                        "Only strings are allowed as keys. (Got %r)" % key
1544
 
                    )
1545
 
 
1546
 
                # This will only run (and succeed) if the key is a string and the
1547
 
                # string is currently used as a key in the mapping.
1548
 
                self._items[key]= value
1549
 
 
1550
 
 
1551
 
            # This block will run if the key is NOT a string, but is still iterable.
1552
 
            else:
1553
 
                location = self
1554
 
                for k in ikey:
1555
 
                    location = location[k]
1556
 
                location[key[-1]] = value
1557
 
 
1558
 
            # Re-sort the keys
1559
 
            self._keyorder = sorted(self._items.keys())
1560
 
 
1561
 
        def __delitem__(self, key):
1562
 
            """ Allows an item with a given to be removed from ResourceMixinGroup objects.
1563
 
 
1564
 
                resgroup.__delitem__[key] <==> del resgroup[key]
1565
 
 
1566
 
                This method supports key-paths.
1567
 
            """
1568
 
            try:
1569
 
                if isinstance(key,basestring):
1570
 
                    raise TypeError
1571
 
                ikey = iter(key[:-1])
1572
 
            except TypeError:
1573
 
                # This will only run (and succeed) if the key is a string and the
1574
 
                # string is currently used as a key in the mapping.
1575
 
                del self._items[key]
1576
 
            # This block will run if the key is NOT a string, but is still iterable.
1577
 
            else:
1578
 
                location = self
1579
 
                for k in ikey:
1580
 
                    location = location[k]
1581
 
                del location[key[-1]]
1582
 
 
1583
 
            # Re-sort the keys
1584
 
            self._keyorder = sorted(self._items.keys())
1585
 
 
1586
 
        def __len__(self):
1587
 
            """ Allows the number of items in a ResourceMixinGroup object to be determined.
1588
 
 
1589
 
                resgroup.__len__() <==> len(resgroup)
1590
 
 
1591
 
                This method does NOT support key-paths.
1592
 
            """
1593
 
            return len(self._items)
1594
 
 
1595
 
        def __contains__(self, key):
1596
 
            """ Allows for checks to see if a particular key exists in the mapping.
1597
 
 
1598
 
                resgroup.__contains__(key) <==> key in resgroup
1599
 
 
1600
 
                This method supports key-paths.
1601
 
            """
1602
 
            try:
1603
 
                if isinstance(key,basestring):
1604
 
                    raise TypeError
1605
 
                ikey = iter(key)
1606
 
            except TypeError:
1607
 
                return key in self._items
1608
 
            else:
1609
 
                location = self
1610
 
                try:
1611
 
                    for k in ikey:
1612
 
                        location = location[k]
1613
 
                    return True
1614
 
                except KeyError:
1615
 
                    return False
1616
 
 
1617
 
        def __iter__(self):
1618
 
            """ Allows iteration over the keys of ResourceMixinGroup objects.
1619
 
 
1620
 
                resgroup.__iter__() <==> iter(resgroup)
1621
 
 
1622
 
                Keys are yielded in sorted order.
1623
 
 
1624
 
                This method does NOT support key-paths.
1625
 
            """
1626
 
            items = self._items
1627
 
            for k in self._keyorder:
1628
 
                yield items[k]
1629
 
 
1630
 
        def iteritems(self):
1631
 
            """ Returns an iterator over the key,value pairs in the mapping.
1632
 
 
1633
 
                resgroup.iteritems() -> pair_iterator
1634
 
 
1635
 
                Ex:
1636
 
 
1637
 
                    for name, resource in resgroup.iteritems():
1638
 
                        print name, "=", resource
1639
 
 
1640
 
                Pairs are yielded in sorted order based on the key of each item.
1641
 
 
1642
 
                Note that the value returned by `ResourceMixinGroup.iteritems()` is only
1643
 
                an iterator and not a collection. This  means that it CANNOT be
1644
 
                indexed, though it can be used in any place that respects the Python
1645
 
                iteration protocol, such as a for loop.
1646
 
 
1647
 
                This method does NOT support key-paths.
1648
 
            """
1649
 
            items = self._items
1650
 
            for k in self._keyorder:
1651
 
                yield (k, items[k])
1652
 
 
1653
 
        def keys(self):
1654
 
            """ Returns all keys in the mapping as a sorted list.
1655
 
 
1656
 
                resgroup.keys() -> sorted_list
1657
 
 
1658
 
                This method does NOT support key-paths.
1659
 
            """
1660
 
            return list(self._keyorder)
1661
 
 
1662
 
        def values(self):
1663
 
            """ Returns all values (resources) in the mapping as a list.
1664
 
 
1665
 
                resgroup.values() -> sorted_list
1666
 
 
1667
 
                The list items are ordered based on the sort order of the key that
1668
 
                corresponds to each value for each key,value pair in the resgroup.
1669
 
                This means, for example, that if you have a resgroup such as this
1670
 
                one:
1671
 
 
1672
 
                    resgroup = ResourceMixinGroup({
1673
 
                        "a" : resourceA,
1674
 
                        "s" : resourceB,
1675
 
                        "d" : resourceC
1676
 
                    })
1677
 
 
1678
 
                then the list returned by `resgroup.values()` will look like this:
1679
 
 
1680
 
                    [resourceA, resourceC, resourceB]
1681
 
 
1682
 
                This is because the alphanumeric order for the keys of the resgroup is
1683
 
                ["a", "d", "s"], not ["a", "s", "d"] as it was entered.
1684
 
 
1685
 
                This method does NOT support key-paths.
1686
 
            """
1687
 
            items = self._items
1688
 
            return [items[k] for k in self._keyorder]
 
 
b'\\ No newline at end of file'
 
1458
                            # Put this copyref on the mapping of references waiting
 
1459
                            # to be copied.
 
1460
                            copies_waiting[copyref] = name
 
1461
                else:
 
1462
                    # It appears to be a reference. Try to get the resource
 
1463
                    # that is being referenced.
 
1464
                    try:
 
1465
                        resources[name] = resources[nameref]
 
1466
                    except KeyError:
 
1467
                        # The resource being referenced does not yet exist.
 
1468
                        # Put this nameref on the mapping of references waiting to
 
1469
                        # be dereferenced.
 
1470
                        refs_waiting[nameref] = name
 
1471
 
 
1472
        return resources
 
1473
 
 
1474
    def __getitem__(self,key):
 
1475
        """ Allows references to values (resources) in resgroups to be acquired.
 
1476
 
 
1477
            resgroup.__getitem__(key) <==> resgroup[key]
 
1478
 
 
1479
            This method supports key-paths.
 
1480
        """
 
1481
        try:
 
1482
            if isinstance(key,basestring):
 
1483
                raise TypeError
 
1484
            ikey = iter(key)
 
1485
        except TypeError:
 
1486
            # This will only run (and succeed) if the key is a string and the
 
1487
            # string is currently used as a key in the mapping.
 
1488
            return self._items[key]
 
1489
        else:
 
1490
            location = self
 
1491
            for k in ikey:
 
1492
                location = location[k]
 
1493
            return location
 
1494
 
 
1495
    def __setitem__(self,key,value):
 
1496
        """ Allows resources in an ResourceMixinGroup to be added or replaced
 
1497
 
 
1498
            resgroup.__setitem__(key, value) <==> resgroup[key] = value
 
1499
 
 
1500
            This method supports key-paths.
 
1501
        """
 
1502
        try:
 
1503
            if isinstance(key,basestring):
 
1504
                raise TypeError
 
1505
            ikey = iter(key[:-1])
 
1506
        except TypeError:
 
1507
            if not isinstance(key, basestring):
 
1508
                # This error is only raised if the key is NEITHER a string NOR
 
1509
                # any other kind of iterable.
 
1510
                raise TypeError(
 
1511
                    "Only strings are allowed as keys. (Got %r)" % key
 
1512
                )
 
1513
 
 
1514
            # This will only run (and succeed) if the key is a string and the
 
1515
            # string is currently used as a key in the mapping.
 
1516
            self._items[key]= value
 
1517
 
 
1518
 
 
1519
        # This block will run if the key is NOT a string, but is still iterable.
 
1520
        else:
 
1521
            location = self
 
1522
            for k in ikey:
 
1523
                location = location[k]
 
1524
            location[key[-1]] = value
 
1525
 
 
1526
        # Re-sort the keys
 
1527
        self._keyorder = sorted(self._items.keys())
 
1528
 
 
1529
    def __delitem__(self, key):
 
1530
        """ Allows an item with a given to be removed from ResourceMixinGroup objects.
 
1531
 
 
1532
            resgroup.__delitem__[key] <==> del resgroup[key]
 
1533
 
 
1534
            This method supports key-paths.
 
1535
        """
 
1536
        try:
 
1537
            if isinstance(key,basestring):
 
1538
                raise TypeError
 
1539
            ikey = iter(key[:-1])
 
1540
        except TypeError:
 
1541
            # This will only run (and succeed) if the key is a string and the
 
1542
            # string is currently used as a key in the mapping.
 
1543
            del self._items[key]
 
1544
        # This block will run if the key is NOT a string, but is still iterable.
 
1545
        else:
 
1546
            location = self
 
1547
            for k in ikey:
 
1548
                location = location[k]
 
1549
            del location[key[-1]]
 
1550
 
 
1551
        # Re-sort the keys
 
1552
        self._keyorder = sorted(self._items.keys())
 
1553
 
 
1554
    def __len__(self):
 
1555
        """ Allows the number of items in a ResourceMixinGroup object to be determined.
 
1556
 
 
1557
            resgroup.__len__() <==> len(resgroup)
 
1558
 
 
1559
            This method does NOT support key-paths.
 
1560
        """
 
1561
        return len(self._items)
 
1562
 
 
1563
    def __contains__(self, key):
 
1564
        """ Allows for checks to see if a particular key exists in the mapping.
 
1565
 
 
1566
            resgroup.__contains__(key) <==> key in resgroup
 
1567
 
 
1568
            This method supports key-paths.
 
1569
        """
 
1570
        try:
 
1571
            if isinstance(key,basestring):
 
1572
                raise TypeError
 
1573
            ikey = iter(key)
 
1574
        except TypeError:
 
1575
            return key in self._items
 
1576
        else:
 
1577
            location = self
 
1578
            try:
 
1579
                for k in ikey:
 
1580
                    location = location[k]
 
1581
                return True
 
1582
            except KeyError:
 
1583
                return False
 
1584
 
 
1585
    def __iter__(self):
 
1586
        """ Allows iteration over the keys of ResourceMixinGroup objects.
 
1587
 
 
1588
            resgroup.__iter__() <==> iter(resgroup)
 
1589
 
 
1590
            Keys are yielded in sorted order.
 
1591
 
 
1592
            This method does NOT support key-paths.
 
1593
        """
 
1594
        items = self._items
 
1595
        for k in self._keyorder:
 
1596
            yield items[k]
 
1597
 
 
1598
    def iteritems(self):
 
1599
        """ Returns an iterator over the key,value pairs in the mapping.
 
1600
 
 
1601
            resgroup.iteritems() -> pair_iterator
 
1602
 
 
1603
            Ex:
 
1604
 
 
1605
                for name, resource in resgroup.iteritems():
 
1606
                    print name, "=", resource
 
1607
 
 
1608
            Pairs are yielded in sorted order based on the key of each item.
 
1609
 
 
1610
            Note that the value returned by `ResourceMixinGroup.iteritems()` is only
 
1611
            an iterator and not a collection. This  means that it CANNOT be
 
1612
            indexed, though it can be used in any place that respects the Python
 
1613
            iteration protocol, such as a for loop.
 
1614
 
 
1615
            This method does NOT support key-paths.
 
1616
        """
 
1617
        items = self._items
 
1618
        for k in self._keyorder:
 
1619
            yield (k, items[k])
 
1620
 
 
1621
    def keys(self):
 
1622
        """ Returns all keys in the mapping as a sorted list.
 
1623
 
 
1624
            resgroup.keys() -> sorted_list
 
1625
 
 
1626
            This method does NOT support key-paths.
 
1627
        """
 
1628
        return list(self._keyorder)
 
1629
 
 
1630
    def values(self):
 
1631
        """ Returns all values (resources) in the mapping as a list.
 
1632
 
 
1633
            resgroup.values() -> sorted_list
 
1634
 
 
1635
            The list items are ordered based on the sort order of the key that
 
1636
            corresponds to each value for each key,value pair in the resgroup.
 
1637
            This means, for example, that if you have a resgroup such as this
 
1638
            one:
 
1639
 
 
1640
                resgroup = ResourceMixinGroup({
 
1641
                    "a" : resourceA,
 
1642
                    "s" : resourceB,
 
1643
                    "d" : resourceC
 
1644
                })
 
1645
 
 
1646
            then the list returned by `resgroup.values()` will look like this:
 
1647
 
 
1648
                [resourceA, resourceC, resourceB]
 
1649
 
 
1650
            This is because the alphanumeric order for the keys of the resgroup is
 
1651
            ["a", "d", "s"], not ["a", "s", "d"] as it was entered.
 
1652
 
 
1653
            This method does NOT support key-paths.
 
1654
        """
 
1655
        items = self._items
 
1656
        return [items[k] for k in self._keyorder]
 
 
b'\\ No newline at end of file'