source: orange/Orange/OrangeCanvas/registry/description.py @ 11252:e50ca88b9854

Revision 11252:e50ca88b9854, 16.9 KB checked in by Ales Erjavec <ales.erjavec@…>, 16 months ago (diff)

Changes to widget descriptors for better documentation/help.

Line 
1"""
2Widget meta description classes.
3
4"""
5
6import os
7import sys
8import warnings
9
10# Exceptions
11
12
13class DescriptionError(Exception):
14    pass
15
16
17class WidgetSpecificationError(DescriptionError):
18    pass
19
20
21class SignalSpecificationError(DescriptionError):
22    pass
23
24
25class CategorySpecificationError(DescriptionError):
26    pass
27
28
29###############
30# Channel flags
31###############
32
33# A single signal
34Single = 2
35
36# Multiple signal (more then one input on the channel)
37Multiple = 4
38
39# Default signal (default or primary input/output)
40Default = 8
41NonDefault = 16
42
43# Explicit - only connected if specifically requested or the only possibility
44Explicit = 32
45
46# Dynamic type output signal
47Dynamic = 64
48
49
50# Input/output signal (channel) description
51
52
53class InputSignal(object):
54    """Description of an input channel.
55
56    Parameters
57    ----------
58    name : str
59        Name of the channel.
60    type : str or `type`
61        Type of the accepted signals.
62    handler : str
63        Name of the handler method for the signal.
64    flags : int, optional
65        Channel flags.
66    id : str
67        A unique id of the input signal.
68    doc : str, optional
69        A docstring documenting the channel.
70
71    """
72    def __init__(self, name, type, handler, flags=Single + NonDefault,
73                 id=None, doc=None):
74        self.name = name
75        self.type = type
76        self.handler = handler
77        self.id = id
78        self.doc = doc
79
80        if isinstance(flags, basestring):
81            # flags are stored as strings
82            warnings.warn("Passing 'flags' as string is deprecated, use "
83                          "integer constants instead",
84                          PendingDeprecationWarning)
85            flags = eval(flags)
86
87        if not (flags & Single or flags & Multiple):
88            flags += Single
89
90        if not (flags & Default or flags & NonDefault):
91            flags += NonDefault
92
93        self.single = flags & Single
94        self.default = flags & Default
95        self.explicit = flags & Explicit
96        self.flags = flags
97
98
99def input_channel_from_args(args):
100    if isinstance(args, tuple):
101        return InputSignal(*args)
102    elif isinstance(args, dict):
103        return InputSignal(**args)
104    elif isinstance(args, InputSignal):
105        return args
106    else:
107        raise TypeError
108
109
110class OutputSignal(object):
111    """Description of an output channel.
112
113    Parameters
114    ----------
115    name : str
116        Name of the channel.
117    type : str or `type`
118        Type of the output signals.
119    flags : int, optional
120        Channel flags.
121    id : str
122        A unique id of the output signal.
123    doc : str, optional
124        A docstring documenting the channel.
125
126    """
127    def __init__(self, name, type, flags=Single + NonDefault,
128                 id=None, doc=None):
129        self.name = name
130        self.type = type
131        self.id = id
132        self.doc = doc
133
134        if isinstance(flags, basestring):
135            # flags are stored as strings
136            warnings.warn("Passing 'flags' as string is deprecated, use "
137                          "integer constants instead",
138                          PendingDeprecationWarning)
139            flags = eval(flags)
140
141        if not (flags & Single or flags & Multiple):
142            flags += Single
143
144        if not (flags & Default or flags & NonDefault):
145            flags += NonDefault
146
147        self.single = flags & Single
148        self.default = flags & Default
149        self.explicit = flags & Explicit
150        self.dynamic = flags & Dynamic
151        self.flags = flags
152
153        if self.dynamic and not self.single:
154            raise SignalSpecificationError(
155                "Output signal can not be 'Multiple' and 'Dynamic'."
156                )
157
158
159def output_channel_from_args(args):
160    if isinstance(args, tuple):
161        return OutputSignal(*args)
162    elif isinstance(args, dict):
163        return OutputSignal(**args)
164    elif isinstance(args, InputSignal):
165        return args
166    else:
167        raise TypeError
168
169
170class WidgetDescription(object):
171    """Description of a widget.
172
173    Parameters
174    ----------
175    name : str
176        A human readable name of the widget.
177    id : str
178        A unique identifier of the widget (in most situations this should
179        be the full module name).
180    category : str, optional
181        A name of the category in which this widget belongs.
182    version : str, optional
183        Version of the widget. By default the widget inherits the project
184        version.
185    description : str, optional
186        A short description of the widget, suitable for a tool tip.
187    long_description : str, optional
188        A longer description of the widget, suitable for a 'what's this?'
189        role.
190    qualified_name : str
191        A qualified name (import name) of the class implementing the widget.
192    package : str, optional
193        A package name where the widget is implemented.
194    project_name : str, optional
195        The distribution name that provides the widget.
196    inputs : list of `InputSignal`, optional
197        A list of input channels provided by the widget.
198    outputs : list of `OutputSignal`, optional
199        A list of output channels provided by the widget.
200    help : str, optional
201        URL or an Resource template of a detailed widget help page.
202    author : str, optional
203        Author name.
204    author_email : str, optional
205        Author email address.
206    maintainer : str, optional
207        Maintainer name
208    maintainer_email : str, optional
209        Maintainer email address.
210    keywords : list-of-str, optional
211        A list of keyword phrases.
212    priority : int, optional
213        Widget priority (the order of the widgets in a GUI presentation).
214    icon : str, optional
215        A filename of the widget icon (in relation to the package).
216    background : str, optional
217        Widget's background color (in the canvas GUI).
218    replaces: list-of-str, optional
219        A list of `id`s this widget replaces (optional).
220
221    """
222    def __init__(self, name, id, category=None, version=None,
223                 description=None, long_description=None,
224                 qualified_name=None, package=None, project_name=None,
225                 inputs=[], outputs=[],
226                 author=None, author_email=None,
227                 maintainer=None, maintainer_email=None,
228                 help=None, url=None, keywords=None,
229                 priority=sys.maxint,
230                 icon=None, background=None,
231                 replaces=None,
232                 ):
233
234        if not qualified_name:
235            # TODO: Should also check that the name is real.
236            raise ValueError("'qualified_name' must be supplied.")
237
238        self.name = name
239        self.id = id
240        self.category = category
241        self.version = version
242        self.description = description
243        self.long_description = long_description
244        self.qualified_name = qualified_name
245        self.package = package
246        self.project_name = project_name
247        self.inputs = inputs
248        self.outputs = outputs
249        self.help = help
250        self.author = author
251        self.author_email = author_email
252        self.maintainer = maintainer
253        self.maintainer_email = maintainer_email
254        self.url = url
255        self.keywords = keywords
256        self.priority = priority
257        self.icon = icon
258        self.background = background
259        self.replaces = replaces
260
261    def __str__(self):
262        return ("WidgetDescription(name=%(name)r, id=%(id)r), "
263                "category=%(category)r, ...)") % self.__dict__
264
265    def __repr__(self):
266        return self.__str__()
267
268    @classmethod
269    def from_file(cls, filename, import_name=None):
270        """Widget description from old style (2.5 version) widget
271        descriptions.
272
273        """
274        from Orange.orng.widgetParser import WidgetMetaData
275        from ..orngSignalManager import resolveSignal
276
277        rest, ext = os.path.splitext(filename)
278        if ext in [".pyc", ".pyo"]:
279            filename = filename[:-1]
280
281        contents = open(filename, "rb").read()
282
283        dirname, basename = os.path.split(filename)
284        default_cat = os.path.basename(dirname)
285
286        try:
287            meta = WidgetMetaData(contents, default_cat)
288        except Exception, ex:
289            if "Not an Orange widget module." in str(ex):
290                raise WidgetSpecificationError
291            else:
292                raise
293
294        widget_name, ext = os.path.splitext(basename)
295        if import_name is None:
296            import_name = widget_name
297
298        wmod = __import__(import_name, fromlist=[""])
299
300        qualified_name = "%s.%s" % (import_name, widget_name)
301
302        inputs = eval(meta.inputList)
303        outputs = eval(meta.outputList)
304
305        inputs = map(input_channel_from_args, inputs)
306
307        outputs = map(output_channel_from_args, outputs)
308
309        # Resolve signal type names into concrete type instances
310        inputs = [resolveSignal(input, globals=wmod.__dict__)
311                  for input in inputs]
312        outputs = [resolveSignal(output, globals=wmod.__dict__)
313                  for output in outputs]
314
315        # Convert all signal types back into qualified names.
316        # This is to prevent any possible import problems when cached
317        # descriptions are unpickled (the relevant code using this lists
318        # should be able to handle missing types better).
319        for s in inputs + outputs:
320            s.type = "%s.%s" % (s.type.__module__, s.type.__name__)
321
322        desc = WidgetDescription(
323             name=meta.name,
324             id=qualified_name,
325             category=meta.category,
326             description=meta.description,
327             qualified_name=qualified_name,
328             package=wmod.__package__,
329             keywords=meta.tags,
330             inputs=inputs,
331             outputs=outputs,
332             icon=meta.icon,
333             priority=int(meta.priority)
334            )
335        return desc
336
337    @classmethod
338    def from_module(cls, module):
339        """Get the widget description from a module.
340
341        The module is inspected for global variables (upper case versions of
342        `WidgetDescription.__init__` parameters).
343
344        Parameters
345        ----------
346        module : `module` or str
347            A module to inspect for widget description. Can be passed
348            as a string (qualified import name).
349
350        """
351        if isinstance(module, basestring):
352            module = __import__(module, fromlist=[""])
353
354        module_name = module.__name__.rsplit(".", 1)[-1]
355        if module.__package__:
356            package_name = module.__package__.rsplit(".", 1)[-1]
357        else:
358            package_name = None
359
360        # Default widget class name unless otherwise specified is the
361        # module name, and default category the package name
362        default_cls_name = module_name
363        default_cat_name = package_name if package_name else ""
364
365        widget_cls_name = getattr(module, "WIDGET_CLASS", default_cls_name)
366        try:
367            widget_class = getattr(module, widget_cls_name)
368            name = getattr(module, "NAME")
369        except AttributeError:
370            # The module does not have a widget class implementation or the
371            # widget name.
372            raise WidgetSpecificationError
373
374        qualified_name = "%s.%s" % (module.__name__, widget_class.__name__)
375
376        id = getattr(module, "ID", module_name)
377        inputs = getattr(module, "INPUTS", [])
378        outputs = getattr(module, "OUTPUTS", [])
379        category = getattr(module, "CATEGORY", default_cat_name)
380        version = getattr(module, "VERSION", None)
381        description = getattr(module, "DESCRIPTION", name)
382        long_description = getattr(module, "LONG_DESCRIPTION", None)
383        author = getattr(module, "AUTHOR", None)
384        author_email = getattr(module, "AUTHOR_EMAIL", None)
385        maintainer = getattr(module, "MAINTAINER", None)
386        maintainer_email = getattr(module, "MAINTAINER_EMAIL", None)
387        help = getattr(module, "HELP", None)
388        url = getattr(module, "URL", None)
389
390        icon = getattr(module, "ICON", None)
391        priority = getattr(module, "PRIORITY", sys.maxint)
392        keywords = getattr(module, "KEYWORDS", None)
393        background = getattr(module, "BACKGROUND", None)
394        replaces = getattr(module, "REPLACES", None)
395
396        inputs = map(input_channel_from_args, inputs)
397        outputs = map(output_channel_from_args, outputs)
398
399        # Convert all signal types into qualified names.
400        # This is to prevent any possible import problems when cached
401        # descriptions are unpickled (the relevant code using this lists
402        # should be able to handle missing types better).
403        for s in inputs + outputs:
404            s.type = "%s.%s" % (s.type.__module__, s.type.__name__)
405
406        return WidgetDescription(
407            name=name,
408            id=id,
409            category=category,
410            version=version,
411            description=description,
412            long_description=long_description,
413            qualified_name=qualified_name,
414            package=module.__package__,
415            inputs=inputs,
416            outputs=outputs,
417            author=author,
418            author_email=author_email,
419            maintainer=maintainer,
420            maintainer_email=maintainer_email,
421            help=help,
422            url=url,
423            keywords=keywords,
424            priority=priority,
425            icon=icon,
426            background=background,
427            replaces=replaces)
428
429
430class CategoryDescription(object):
431    """Description of a widget category.
432
433    Parameters
434    ----------
435
436    name : str
437        A human readable name.
438    version : str
439        Version string (optional).
440    description : str
441        A short description of the category, suitable for a tool
442        tip (optional).
443    long_description : str
444        A longer description.
445    qualified_name : str
446        Qualified name
447    project_name : str
448        A project name providing the category.
449    priority : int
450        Priority (order in the GUI).
451    icon : str
452        An icon filename
453    background : str
454        An background color for widgets in this category.
455
456    """
457    def __init__(self, name=None, version=None,
458                 description=None, long_description=None,
459                 qualified_name=None, package=None,
460                 project_name=None, author=None, author_email=None,
461                 maintainer=None, maintainer_email=None,
462                 url=None, help=None, keywords=None,
463                 widgets=None, priority=sys.maxint,
464                 icon=None, background=None
465                 ):
466
467        self.name = name
468        self.version = version
469        self.description = description
470        self.long_description = long_description
471        self.qualified_name = qualified_name
472        self.package = package
473        self.project_name = project_name
474        self.author = author
475        self.author_email = author_email
476        self.maintainer = maintainer
477        self.maintainer_email = maintainer_email
478        self.url = url
479        self.help = help
480        self.keywords = keywords
481        self.widgets = widgets or []
482        self.priority = priority
483        self.icon = icon
484        self.background = background
485
486    def __str__(self):
487        return "CategoryDescription(name=%(name)r, ...)" % self.__dict__
488
489    def __repr__(self):
490        return self.__str__()
491
492    @classmethod
493    def from_package(cls, package):
494        """Get the CategoryDescription from a package.
495
496        Parameters
497        ----------
498        package : `module` or `str`
499            A package containing the category.
500
501        """
502        if isinstance(package, basestring):
503            package = __import__(package, fromlist=[""])
504        package_name = package.__name__
505        qualified_name = package_name
506        default_name = package_name.rsplit(".", 1)[-1]
507
508        name = getattr(package, "NAME", default_name)
509        description = getattr(package, "DESCRIPTION", None)
510        long_description = getattr(package, "LONG_DESCRIPTION", None)
511        author = getattr(package, "AUTHOR", None)
512        author_email = getattr(package, "AUTHOR_EMAIL", None)
513        maintainer = getattr(package, "MAINTAINER", None)
514        maintainer_email = getattr(package, "MAINTAINER_MAIL", None)
515        url = getattr(package, "URL", None)
516        help = getattr(package, "HELP", None)
517        keywords = getattr(package, "KEYWORDS", None)
518        widgets = getattr(package, "WIDGETS", None)
519        priority = getattr(package, "PRIORITY", sys.maxint - 1)
520        icon = getattr(package, "ICON", None)
521        background = getattr(package, "BACKGROUND", None)
522
523        if priority == sys.maxint - 1 and name.lower() == "prototypes":
524            priority = sys.maxint
525
526        return CategoryDescription(
527            name=name,
528            qualified_name=qualified_name,
529            description=description,
530            long_description=long_description,
531            help=help,
532            author=author,
533            author_email=author_email,
534            maintainer=maintainer,
535            maintainer_email=maintainer_email,
536            url=url,
537            keywords=keywords,
538            widgets=widgets,
539            priority=priority,
540            icon=icon,
541            background=background)
Note: See TracBrowser for help on using the repository browser.