Index: trac/env.py =================================================================== --- trac/env.py (revision 4591) +++ trac/env.py (working copy) @@ -30,6 +30,10 @@ __all__ = ['Environment', 'IEnvironmentSetupParticipant', 'open_environment'] +class InterfaceError(TracError): + """Raised when a component does not match its declared interfaces.""" + + class IEnvironmentSetupParticipant(Interface): """Extension point interface for components that need to participate in the creation and upgrading of Trac environments, for example to create @@ -74,6 +78,9 @@ setups, usually involving running Trac behind a HTTP proxy, you may need to use this option to force Trac to use the correct URL.""") + check_interfaces = BoolOption('trac', 'check_interfaces', True, + """Whether to check plugin interface conformance.""") + project_name = Option('project', 'name', 'My Project', """Name of the project.""") @@ -171,6 +178,8 @@ Every component activated through the `Environment` object gets three member variables: `env` (the environment object), `config` (the environment configuration) and `log` (a logger object).""" + if self.check_interfaces: + self._check_interfaces(component) component.env = self component.config = self.config component.log = self.log @@ -408,6 +417,82 @@ return self._abs_href abs_href = property(_get_abs_href, 'The application URL') + def _check_interfaces(self, component): + """Check that component conforms with its declared interfaces.""" + import inspect + for interface in component._implements: + for method_name, imethod in \ + inspect.getmembers(interface, inspect.ismethod): + try: + cmethod = getattr(component, method_name) + except AttributeError: + method_signature = method_name + \ + inspect.formatargspec(*inspect.getargspec( + imethod)) + raise InterfaceError('Component %s does not have Interface ' + 'method %s.%s' + (self._format_class(component.__class__), + self._format_class(interface)), + method_signature) + self._compare_method(component, interface, imethod, cmethod) + + def _compare_method(self, component, interface, imethod, cmethod): + import inspect + imethod_spec = inspect.getargspec(imethod) + if not imethod_spec[0] or imethod_spec[0][0] != 'self': + imethod_spec[0].insert(0, 'self') + cmethod_spec = inspect.getargspec(cmethod) + + imethod_sig = imethod.__name__ + \ + inspect.formatargspec(*imethod_spec) + cmethod_sig = cmethod.__name__ + \ + inspect.formatargspec(*cmethod_spec) + + component_fmt = self._format_class(component.__class__) + interface_fmt = self._format_class(interface) + + # Can't check varargs, but log them + if cmethod_spec[2] or imethod_spec[2]: + if cmethod_spec[2]: + self.log.debug("Component method %s.%s contains varargs, can't " + "check interface conformance." + % (component_fmt, cmethod_sig)) + if imethod_spec[2]: + self.log.debug("Interface method %s.%s contains varargs, can't " + "check interface conformance for component %s." + % (interface_fmt, imethod_sig, component_fmt)) + return + + if getattr(interface, 'deprecated', False): + self.log.warning('Interface %s used by component %s is deprecated ' + 'and will be removed in the next version of Trac.' + % (interface_fmt, component_fmt)) + + if getattr(imethod, 'deprecated', False): + self.log.warning('Interface method %s.%s used by component %s is ' + 'deprecated and will be removed in the next ' + 'version of Trac.' % (interface_fmt, imethod_sig, + component_fmt)) + + # TODO This could be smarter. Possibly checking to see if there are + # keyword args in the interface indicating that some of the arguments + # are optional (this has occurred over time due to backwards + # compatibility). + if len(imethod_spec[0]) != len(cmethod_spec[0]): + self.log.error('Component method %s.%s does not match ' + 'Interface method %s.%s' + % (component_fmt, cmethod_sig, interface_fmt, + imethod_sig)) + if imethod_spec != cmethod_spec: + self.log.warning('Component method %s.%s arguments differ in ' + 'name from the Interface method %s.%s' + % (component_fmt, cmethod_sig, interface_fmt, + imethod_sig)) + + def _format_class(self, cls): + return '%s.%s' % (cls.__module__, cls.__name__) + + class EnvironmentSetup(Component): implements(IEnvironmentSetupParticipant)