source: TI12-security/trunk/NDGSecurity/python/ndg_security_common/ndg/security/common/authz/xacml/cond/factory.py @ 6586

Subversion URL: http://proj.badc.rl.ac.uk/svn/ndg/TI12-security/trunk/NDGSecurity/python/ndg_security_common/ndg/security/common/authz/xacml/cond/factory.py@6586
Revision 6586, 27.8 KB checked in by pjkersha, 12 years ago (diff)

Started ESG Authorisation Service implementation ndg.security.server.wsgi.authorizationservice - SAML SOAP based interface to a Policy Decision Point enabling centralised policy for a range of services.

Line 
1"""XACML function factory module
2
3NERC DataGrid Project
4
5This code is adapted from the Sun Java XACML implementation ...
6
7Copyright 2004 Sun Microsystems, Inc. All Rights Reserved.
8
9Redistribution and use in source and binary forms, with or without
10modification, are permitted provided that the following conditions are met:
11
12  1. Redistribution of source code must retain the above copyright notice,
13     this list of conditions and the following disclaimer.
14
15  2. Redistribution in binary form must reproduce the above copyright
16     notice, this list of conditions and the following disclaimer in the
17     documentation and/or other materials provided with the distribution.
18
19Neither the name of Sun Microsystems, Inc. or the names of contributors may
20be used to endorse or promote products derived from this software without
21specific prior written permission.
22
23This software is provided "AS IS," without a warranty of any kind. ALL
24EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND WARRANTIES, INCLUDING
25ANY IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE
26OR NON-INFRINGEMENT, ARE HEREBY EXCLUDED. SUN MICROSYSTEMS, INC. ("SUN")
27AND ITS LICENSORS SHALL NOT BE LIABLE FOR ANY DAMAGES SUFFERED BY LICENSEE
28AS A RESULT OF USING, MODIFYING OR DISTRIBUTING THIS SOFTWARE OR ITS
29DERIVATIVES. IN NO EVENT WILL SUN OR ITS LICENSORS BE LIABLE FOR ANY LOST
30REVENUE, PROFIT OR DATA, OR FOR DIRECT, INDIRECT, SPECIAL, CONSEQUENTIAL,
31INCIDENTAL OR PUNITIVE DAMAGES, HOWEVER CAUSED AND REGARDLESS OF THE THEORY
32OF LIABILITY, ARISING OUT OF THE USE OF OR INABILITY TO USE THIS SOFTWARE,
33EVEN IF SUN HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
34
35You acknowledge that this software is not designed or intended for use in
36the design, construction, operation or maintenance of any nuclear facility.
37"""
38import logging
39log = logging.getLogger(__name__)
40
41from ndg.security.common.utils import UniqList
42from ndg.security.common.authz.xacml.cond import Function, EqualFunction, \
43    LogicalFunction, NOfFunction, NotFunction, ComparisonFunction, \
44    MatchFunction, ConditionBagFunction, ConditionSetFunction, \
45    HigherOrderFunction, AddFunction, SubtractFunction, MultiplyFunction, \
46    DivideFunction, ModFunction, AbsFunction, RoundFunction, FloorFunction, \
47    DateMathFunction, GeneralBagFunction, NumericConvertFunction, \
48    StringNormalizeFunction, GeneralSetFunction, MapFunction, \
49    MapFunctionProxy, FunctionProxy
50
51from ndg.security.common.authz.xacml.exceptions import ParsingException, \
52    UnknownIdentifierException, FunctionTypeException
53     
54
55class FunctionFactoryProxy(object):
56    '''A simple proxy interface used to install new FunctionFactory's.
57    The three kinds of factory (Target, Condition, and General) are tied
58    together in this interface because implementors writing new factories
59    should always implement all three types and provide them together'''
60   
61    @classmethod
62    def getTargetFactory(cls):
63        """Return a Target instance
64        @type cls: FunctionFactoryProxy
65        @param cls: class instance
66        """
67        raise NotImplementedError()
68
69    @classmethod
70    def getConditionFactory(cls):
71        """Return a Condition instance
72        @type cls: FunctionFactoryProxy
73        @param cls: class instance
74        """
75        raise NotImplementedError()
76
77    @classmethod
78    def getGeneralFactory(cls):
79        """General Factory method
80        @type cls: FunctionFactoryProxy
81        @param cls: class instance
82        """
83        raise NotImplementedError()
84
85
86class FunctionFactory(object):
87    '''Factory used to create all functions. There are three kinds of factories:
88    general, condition, and target. These provide functions that can be used
89    anywhere, only in a condition's root and only in a target (respectively).
90   
91    Note that all functions, except for abstract functions, are singletons, so
92    any instance that is added to a factory will be the same one returned
93    from the create methods. This is done because most functions don't have
94    state, so there is no need to have more than one, or to spend the time
95    creating multiple instances that all do the same thing.'''
96
97    class defaultFactoryProxy(FunctionFactoryProxy):
98        @classmethod
99        def getTargetFactory(cls):
100            return StandardFunctionFactory.getTargetFactory()
101   
102        @classmethod
103        def getConditionFactory(cls):
104            return StandardFunctionFactory.getConditionFactory()
105   
106        @classmethod
107        def getGeneralFactory(cls):
108            return StandardFunctionFactory.getGeneralFactory()                 
109           
110    @classmethod
111    def getTargetInstance(cls):
112        '''Returns the default FunctionFactory that will only provide those
113        functions that are usable in Target matching.
114       
115        @return a FunctionFactory for target functions'''
116        return cls.defaultFactoryProxy.getTargetFactory()
117       
118    @classmethod 
119    def getConditionInstance(cls): 
120        '''Returns the default FunctionFactory that provides access to all the
121        functions. These Functions are a superset of the Condition functions.
122       
123        @return a FunctionFactory for all functions
124        '''
125        return cls.defaultFactoryProxy.getConditionFactory()
126   
127    @classmethod
128    def getGeneralInstance(cls): 
129        '''Sets the default factory. Note that this is just a place holder for
130        now, and will be replaced with a more useful mechanism soon.'''
131        return cls.defaultFactoryProxy.getGeneralFactory()
132   
133   
134    def addFunction(self, function):
135        '''Adds the function to the factory. Most functions have no state, so
136        the singleton model used here is typically desirable. The factory will
137        not enforce the requirement that a Target or Condition matching
138        function must be boolean.
139       
140        @param function the Function to add to the factory
141        '''
142        raise NotImplementedError()
143       
144    def addAbstractFunction(self, functionProxy, identity):
145        '''Adds the abstract function proxy to the factory. This is used for
146        those functions which have state, or change behaviour (for instance
147        the standard map function, which changes its return type based on
148        how it is used).
149       
150        @param proxy the FunctionProxy to add to the factory
151        @param identity the function's identifier
152        '''
153        raise NotImplementedError()       
154   
155    def getSupportedFunctions(self):
156        '''Returns the function identifiers supported by this factory.
157       
158        @return a Set of Strings'''
159        raise NotImplementedError()
160
161    def createFunction(self, identity):
162        '''Tries to get an instance of the specified function.
163       
164        @param identity the name of the function
165        '''       
166        raise NotImplementedError()
167   
168    def createAbstractFunction(self, identity, root):
169        '''Tries to get an instance of the specified abstract function.
170       
171        @param identity the name of the function
172        @param root the DOM root containing info used to create the function
173        '''
174        raise NotImplementedError()
175
176
177class BasicFunctionFactoryProxy(FunctionFactoryProxy):
178    '''A simple utility class that manages triples of function factories.'''
179   
180    # the triple of factories
181    targetFactory = None
182    conditionFactory = None
183    generalFactory = None
184
185    def __init__(self, targetFactory, conditionFactory, generalFactory): 
186        '''Creates a new proxy.
187       
188        @param targetFactory the target factory provided by this proxy
189        @param conditionFactory the target condition provided by this proxy
190        @param generalFactory the general factory provided by this proxy
191        '''
192        BasicFunctionFactoryProxy.targetFactory = targetFactory
193        BasicFunctionFactoryProxy.conditionFactory = conditionFactory
194        BasicFunctionFactoryProxy.generalFactory = generalFactory
195   
196    @classmethod
197    def getTargetFactory(cls):
198        return cls.targetFactory
199
200    @classmethod
201    def getConditionFactory(cls):
202        return cls.conditionFactory
203   
204    @classmethod
205    def getGeneralFactory(cls):
206        return cls.generalFactory
207   
208
209class BaseFunctionFactory(FunctionFactory):
210    '''This is a basic implementation of <code>FunctionFactory</code>. It
211    implements the insertion and retrieval methods, but it doesn't actually
212    setup the factory with any functions. It also assumes a certain model
213    with regard to the different kinds of functions (Target, Condition, and
214    General). For this reason, you may want to re-use this class, or you
215    may want to extend FunctionFactory directly, if you're writing a new
216    factory implementation.
217   
218    Note that while this class is thread-safe on all creation methods, it
219    is not safe to add support for a new function while creating an instance
220    of a function. This follows from the assumption that most people will
221    initialize these factories up-front, and then start processing without
222    ever modifying the factories. If you need these mutual operations to
223    be thread-safe, then you should write a wrapper class that implements
224    the right synchronization.
225    '''
226   
227    def __init__(self, 
228                 superset=None, 
229                 supportedFunctions=[],
230                 supportedAbstractFunctions={}):
231        '''Sets a "superset factory". This is useful since
232        the different function factories (Target, Condition, and General)
233        have a superset relationship (Condition functions are a superset
234        of Target functions, etc.). Adding a function to this factory will
235        automatically add the same function to the superset factory.
236
237        Constructor that defines the initial functions supported by this
238        factory but doesn't use a superset factory.
239
240        Constructor that defines the initial functions supported by this
241        factory but doesn't use a superset factory.
242
243        Constructor that defines the initial functions supported by this
244        factory and uses a superset factory. Note that the functions
245        supplied here are not propagated up to the superset factory, so
246        you must either make sure the superset factory is correctly
247        initialized or use BaseFunctionFactory(FunctionFactory)
248        and then manually add each function.
249       
250        @param supportedFunctions a Set of Functions
251        @param supportedAbstractFunctions a mapping from URI to
252                                          FunctionProxy
253       
254        @param supportedFunctions a Set of Functions
255        @param supportedAbstractFunctions a mapping from URI to FunctionProxy
256       
257        @param superset the superset factory or None'''
258       
259        # the backing maps for the Function objects
260        self.functionMap = {}
261   
262        # the superset factory chained to this factory
263        self.superset = superset
264     
265        for function in supportedFunctions:
266            if function.functionName not in self.functionMap:
267                self.functionMap[function.functionName] = function
268       
269        for functionId in supportedAbstractFunctions.keys():
270            proxy = supportedAbstractFunctions.get(functionId)
271            self.functionMap[functionId] = proxy
272 
273    def addFunction(self, function):
274        '''Adds the function to the factory. Most functions have no state, so
275        the singleton model used here is typically desirable. The factory will
276        not enforce the requirement that a Target or Condition matching
277        function must be boolean.
278       
279        @param function the Function to add to the factory
280        @raise TypeError if the function's identifier is already used or if the
281        function is non-boolean (when this is a Target or Condition factory)
282        '''
283        functionId = function.functionId
284
285        # make sure this doesn't already exist
286        if functionId in self.functionMap:
287            raise TypeError("function %s already exists" % functionId)
288
289        # add to the superset factory
290        if self.superset != None:
291            self.superset.addFunction(function)
292
293        # Add to this factory
294        self.functionMap[functionId] = function
295   
296    def addAbstractFunction(self, proxy, functionId):
297        '''Adds the abstract function proxy to the factory. This is used for
298        those functions which have state, or change behaviour (for instance
299        the standard map function, which changes its return type based on
300        how it is used).
301       
302        @param proxy: the FunctionProxy to add to the factory
303        @param functionId: the function's identifier
304       
305        @raise TypeError if the function's identifier is already used'''
306
307        # make sure this doesn't already exist
308        if functionId in self.functionMap:
309            raise TypeError("function already exists")
310
311        # add to the superset factory
312        if self.superset != None:
313            self.superset.addAbstractFunction(proxy, functionId)
314
315        # finally, add to this factory
316        self.functionMap[functionId] = proxy
317   
318
319    def getSupportedFunctions(self): 
320        '''Returns the function identifiers supported by this factory.
321       
322        @return a list of strings'''
323   
324        functions = self.functionMap.keys()
325
326        if self.superset is not None:
327            functions += self.superset.getSupportedFunctions()
328
329        return functions
330   
331    def createFunction(self, identity):
332        '''Tries to get an instance of the specified function.
333       
334        @param identity the name of the function
335       
336        @raise UnknownIdentifierException if the name isn't known
337        @raise FunctionTypeException if the name is known to map to an
338                                      abstract function, and should therefore
339                                      be created through createAbstractFunction
340        '''
341        entry = self.functionMap.get(identity)
342        if entry is not None:
343            if isinstance(entry, Function):
344                return entry
345            else:
346                # this is actually a proxy, which means the other create
347                # method should have been called
348                raise FunctionTypeException("function [%s] is abstract" %
349                                            identity)   
350        else:
351            # we couldn't find a match
352            raise UnknownIdentifierException("functions of type [%s] are not "
353                                             "supported by this factory" % 
354                                             identity)
355   
356    def createAbstractFunction(self, identity, root):
357        '''Tries to get an instance of the specified abstract function.
358       
359        @param identity the name of the function
360        @param root the DOM root containing info used to create the function
361        @param xpathVersion the version specified in the containing policy, or
362                            None if no version was specified
363       
364        @throws UnknownIdentifierException if the name isn't known
365        @throws FunctionTypeException if the name is known to map to a
366                                      concrete function, and should therefore
367                                      be created through createFunction
368        @throws ParsingException if the function can't be created with the
369                                 given inputs'''
370   
371        entry = self.functionMap.get(identity)
372        if entry is not None:
373            if isinstance(entry, FunctionProxy): 
374                try: 
375                    return entry.getInstance(root)
376               
377                except Exception, e:
378                    raise ParsingException("Couldn't create abstract function "
379                                           "%s: %s" % identity, e)     
380            else:
381                # this is actually a concrete function, which means that
382                # the other create method should have been called
383                raise FunctionTypeException("function is concrete")
384           
385        else:
386            raise UnknownIdentifierException("Abstract functions of type %s "
387                                             "are not supported by this "
388                                             "factory" % identity)
389
390getSupportedFunctions = lambda cls: [cls(i) for i in cls.supportedIdentifiers]
391
392class StandardFunctionFactory(BaseFunctionFactory):
393    '''This factory supports the standard set of functions specified in XACML
394    1.0 and 1.1. It is the default factory used by the system, and imposes
395    a singleton pattern insuring that there is only ever one instance of
396    this class.
397    <p>
398    Note that because this supports only the standard functions, this
399    factory does not allow the addition of any other functions. If you call
400    addFunction on an instance of this class, an exception
401    will be thrown. If you need a standard factory that is modifiable,
402    you can either create a new BaseFunctionFactory (or some
403    other implementation of FunctionFactory) populated with
404    the standard functions from getStandardFunctions or
405    you can use getNewFactoryProxy to get a proxy containing
406    a new, modifiable set of factories.'''
407
408
409    # the three singleton instances
410    targetFactory = None
411    conditionFactory = None
412    generalFactory = None
413
414    # the three function sets/maps that we use internally
415    targetFunctions = None
416    conditionFunctions = None
417    generalFunctions = None
418
419    targetAbstractFunctions = None
420    conditionAbstractFunctions = None
421    generalAbstractFunctions = None
422
423    # the set/map used by each singleton factory instance
424    supportedFunctions = None
425    supportedAbstractFunctions = None
426
427   
428    def __init__(self, supportedFunctions, supportedAbstractFunctions): 
429        '''Creates a new StandardFunctionFactory, making sure that the default
430        maps are initialized correctly. Standard factories can't be modified,
431        so there is no notion of supersetting since that's only used for
432        correctly propagating new functions.'''
433        super(StandardFunctionFactory, self).__init__(
434                        supportedFunctions=supportedFunctions, 
435                        supportedAbstractFunctions=supportedAbstractFunctions)
436
437        self.supportedFunctions = supportedFunctions
438        self.supportedAbstractFunctions = supportedAbstractFunctions
439   
440    @classmethod
441    def _initTargetFunctions(cls): 
442        '''Private initializer for the target functions. This is only ever
443        called once.'''
444        log.info("Initializing standard Target functions")
445
446        cls.targetFunctions = UniqList()
447
448        # add EqualFunction
449        cls.targetFunctions.extend(getSupportedFunctions(EqualFunction))
450
451        # add LogicalFunction
452        cls.targetFunctions.extend(getSupportedFunctions(LogicalFunction))
453       
454        # add NOfFunction
455        cls.targetFunctions.extend(getSupportedFunctions(NOfFunction))
456       
457        # add NotFunction
458        cls.targetFunctions.extend(getSupportedFunctions(NotFunction))
459       
460        # add ComparisonFunction
461        cls.targetFunctions.extend(getSupportedFunctions(ComparisonFunction))
462
463        # add MatchFunction
464        cls.targetFunctions.extend(getSupportedFunctions(MatchFunction))
465
466        cls.targetAbstractFunctions = {}
467   
468    @classmethod
469    def _initConditionFunctions(cls): 
470        '''Private initializer for the condition functions. This is only ever
471        called once.'''
472        log.info("Initializing standard Condition functions")
473
474        if cls.targetFunctions is None:
475            cls._initTargetFunctions()
476
477        cls.conditionFunctions = cls.targetFunctions[:]
478
479        # add condition functions from BagFunction
480        try:
481            cls.conditionFunctions.extend(
482                                getSupportedFunctions(ConditionBagFunction))
483        except NotImplementedError:
484            log.warning("ConditionBagFunction is not implemented")
485       
486        try:
487            # add condition functions from SetFunction
488            cls.conditionFunctions.extend(
489                                getSupportedFunctions(ConditionSetFunction))
490        except NotImplementedError:
491            log.warning("ConditionSetFunction is not implemented")
492       
493        try:
494            # add condition functions from HigherOrderFunction
495            cls.conditionFunctions.extend(
496                                getSupportedFunctions(HigherOrderFunction))
497        except NotImplementedError:
498            log.warning("HigherOrderFunction is not implemented")
499           
500        cls.conditionAbstractFunctions = cls.targetAbstractFunctions.copy()
501   
502    @classmethod
503    def _initGeneralFunctions(cls):     
504        '''Private initializer for the general functions. This is only ever
505        called once.'''
506   
507        log.info("Initializing standard General functions")
508
509        if cls.conditionFunctions is None:
510            cls._initConditionFunctions()
511
512        cls.generalFunctions = cls.conditionFunctions[:]
513
514        # add AddFunction
515        cls.generalFunctions.extend(getSupportedFunctions(AddFunction))
516           
517        # add SubtractFunction
518        cls.generalFunctions.extend(getSupportedFunctions(SubtractFunction))
519           
520        # add MultiplyFunction
521        cls.generalFunctions.extend(getSupportedFunctions(MultiplyFunction))
522           
523        # add DivideFunction
524        cls.generalFunctions.extend(getSupportedFunctions(DivideFunction))
525           
526        # add ModFunction
527        cls.generalFunctions.extend(getSupportedFunctions(ModFunction))
528       
529        # add AbsFunction
530        cls.generalFunctions.extend(getSupportedFunctions(AbsFunction))
531           
532        # add RoundFunction
533        cls.generalFunctions.extend(getSupportedFunctions(RoundFunction))
534           
535        # add FloorFunction
536        cls.generalFunctions.extend(getSupportedFunctions(FloorFunction))
537       
538        # add DateMathFunction
539        cls.generalFunctions.extend(getSupportedFunctions(DateMathFunction))
540           
541        # add general functions from BagFunction
542        cls.generalFunctions.extend(getSupportedFunctions(GeneralBagFunction))
543           
544        # add NumericConvertFunction
545        cls.generalFunctions.extend(getSupportedFunctions(
546                                                    NumericConvertFunction))
547           
548        # add StringNormalizeFunction
549        cls.generalFunctions.extend(getSupportedFunctions(
550                                                    StringNormalizeFunction))
551       
552        # add general functions from SetFunction
553        cls.generalFunctions.extend(getSupportedFunctions(GeneralSetFunction))
554           
555        cls.generalAbstractFunctions = cls.conditionAbstractFunctions.copy()
556
557        # Add the map function's proxy
558        cls.generalAbstractFunctions[MapFunction.NAME_MAP] = MapFunctionProxy()
559   
560    @classmethod 
561    def getTargetFactory(cls): 
562        '''Returns a FunctionFactory that will only provide those functions
563        that are usable in Target matching. This method enforces a singleton
564        model, meaning that this always returns the same instance, creating
565        the factory if it hasn't been requested before. This is the default
566        model used by the FunctionFactory, ensuring quick
567        access to this factory.
568       
569        @return a FunctionFactory for target functions'''
570        if cls.targetFactory is None: 
571            if cls.targetFunctions is None:
572                cls._initTargetFunctions()
573               
574            if cls.targetFactory is None:
575                cls.targetFactory=cls(cls.targetFunctions,
576                                      cls.targetAbstractFunctions)
577       
578        return cls.targetFactory
579
580   
581    @classmethod
582    def getConditionFactory(cls): 
583        '''Returns a FuntionFactory that will only provide those functions that
584        are usable in the root of the Condition. These Functions are a
585        superset of the Target functions. This method enforces a singleton
586        model, meaning that this always returns the same instance, creating
587        the factory if it hasn't been requested before. This is the default
588        model used by the FunctionFactory, ensuring quick
589        access to this factory.
590   
591        @return a FunctionFactory for condition functions
592        '''
593        if cls.conditionFactory is None:
594            if cls.conditionFunctions is None:
595                cls._initConditionFunctions()
596               
597            if cls.conditionFactory is None:
598                cls.conditionFactory = cls(cls.conditionFunctions,
599                                           cls.conditionAbstractFunctions)       
600
601        return cls.conditionFactory
602   
603
604    @classmethod
605    def getGeneralFactory(cls): 
606        '''Returns a FunctionFactory that provides access to all the functions.
607        These Functions are a superset of the Condition functions. This method
608        enforces a singleton model, meaning that this always returns the same
609        instance, creating the factory if it hasn't been requested before.
610        This is the default model used by the FunctionFactory,
611        ensuring quick access to this factory.
612       
613        @return a FunctionFactory for all functions'''
614   
615        if cls.generalFactory is None:
616            if cls.generalFunctions is None:
617                cls._initGeneralFunctions()
618               
619                cls.generalFactory = cls(cls.generalFunctions,
620                                         cls.generalAbstractFunctions)
621               
622        return cls.generalFactory
623
624
625    def getStandardFunctions(self):
626        '''Returns the set of functions that this standard factory supports.
627       
628        @return a Set of Functions'''
629        return tuple(self.supportedFunctions.keys())
630       
631    def getStandardAbstractFunctions(self):
632        '''Returns the set of abstract functions that this standard factory
633        supports as a mapping of identifier to proxy.
634       
635        @return a Map mapping URIs to FunctionProxys'''
636        return tuple(self.supportedAbstractFunctions.keys())
637   
638   
639    @classmethod
640    def getNewFactoryProxy(cls): 
641        '''A convenience method that returns a proxy containing newly created
642        instances of BaseFunctionFactorys that are correctly
643        supersetted and contain the standard functions and abstract functions.
644        These factories allow adding support for new functions.
645       
646        @return a new proxy containing new factories supporting the standard
647        functions'''
648       
649        general = cls.getGeneralFactory()
650           
651        newGeneral=BaseFunctionFactory(general.getStandardFunctions(),
652                                       general.getStandardAbstractFunctions())
653
654        condition = cls.getConditionFactory()
655       
656        newCondition = BaseFunctionFactory(newGeneral,
657                                    condition.getStandardFunctions(),
658                                    condition.getStandardAbstractFunctions())
659
660        target = cls.getTargetFactory()
661        newTarget = BaseFunctionFactory(newCondition,
662                                    target.getStandardFunctions(),
663                                    target.getStandardAbstractFunctions())
664
665        return BasicFunctionFactoryProxy(newTarget, newCondition, newGeneral)
666   
667    def addFunction(self, function):
668        '''Always throws an exception, since support for new functions may not
669        be added to a standard factory.
670       
671        @param function the Function to add to the factory       
672        @raise NotImplementedError'''
673   
674        raise NotImplementedError("a standard factory cannot support new "
675                                  "functions")
676   
677   
678    def addAbstractFunction(self, proxy, identity):
679        '''Always throws an exception, since support for new functions may not
680        be added to a standard factory.
681       
682        @param proxy the FunctionProxy to add to the factory
683        @param identity the function's identifier
684       
685        @raise NotImplementedError always'''
686        raise NotImplementedError("a standard factory cannot support new "
687                                  "functions")
Note: See TracBrowser for help on using the repository browser.