GitHub Access Token became invalid

It seems like the GitHub access token used for retrieving details about this repository from GitHub became invalid. This might prevent certain types of inspections from being run (in particular, everything related to pull requests).
Please ask an admin of your repository to re-new the access token on this website.

Orange.canvas.registry.WidgetDiscovery   F
last analyzed

Complexity

Total Complexity 80

Size/Duplication

Total Lines 401
Duplicated Lines 0 %
Metric Value
dl 0
loc 401
rs 1.5789
wmc 80

17 Methods

Rating   Name   Duplication   Size   Complexity  
D cache_has_valid_entry() 0 27 8
A process_iter() 0 11 4
D widget_description() 0 32 8
B cache_insert() 0 33 6
A process_directory() 0 9 2
A __init__() 0 7 2
D process_category_package() 0 43 8
A cache_get() 0 8 2
B process_file() 0 25 5
D run() 0 56 11
A process_widget_module() 0 13 2
A handle_category() 0 10 2
A process_loader() 0 8 2
A cache_log_error() 0 11 2
D iter_widget_descriptions() 0 57 11
A cache_can_ignore() 0 20 3
A handle_widget() 0 10 2

How to fix   Complexity   

Complex Class

Complex classes like Orange.canvas.registry.WidgetDiscovery often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
"""
2
Widget Discovery
3
================
4
5
Discover which widgets are installed/available.
6
7
This module implements a discovery process
8
9
"""
10
11
import os
12
import sys
0 ignored issues
show
Unused Code introduced by
The import sys seems to be unused.
Loading history...
13
import stat
14
import glob
15
import logging
16
import types
17
import pkgutil
18
from collections import namedtuple
19
import pkg_resources
20
21
from .description import (
22
    WidgetDescription, CategoryDescription,
23
    WidgetSpecificationError, CategorySpecificationError
24
)
25
26
from . import VERSION_HEX
27
from . import cache, WidgetRegistry
28
import collections
0 ignored issues
show
Unused Code introduced by
The import collections seems to be unused.
Loading history...
29
30
log = logging.getLogger(__name__)
31
32
33
_CacheEntry = \
34
    namedtuple(
35
        "_CacheEntry",
36
        ["mod_path",         # Module path (filename)
37
         "name",             # Module qualified import name
38
         "mtime",            # Modified time
39
         "project_name",     # distribution name (if available)
40
         "project_version",  # distribution version (if available)
41
         "exc_type",         # exception type when last trying to import
42
         "exc_val",          # exception value (str of value)
43
         "description"       # WidgetDescription instance
44
         ]
45
    )
46
47
48
def default_category_for_module(module):
49
    """
50
    Return a default constructed :class:`CategoryDescription`
51
    for a `module`.
52
53
    """
54
    if isinstance(module, str):
55
        module = __import__(module, fromlist=[""])
56
    name = module.__name__.rsplit(".", 1)[-1]
57
    qualified_name = module.__name__
58
    return CategoryDescription(name=name, qualified_name=qualified_name)
59
60
61
class WidgetDiscovery(object):
62
    """
63
    Base widget discovery runner.
64
    """
65
66
    def __init__(self, registry=None, cached_descriptions=None):
67
        self.registry = registry
68
        self.cached_descriptions = cached_descriptions or {}
69
        version = (VERSION_HEX, )
70
        if self.cached_descriptions.get("!VERSION") != version:
71
            self.cached_descriptions.clear()
72
            self.cached_descriptions["!VERSION"] = version
73
74
    def run(self, entry_points_iter):
75
        """
76
        Run the widget discovery process from an entry point iterator
77
        (yielding :class:`pkg_resources.EntryPoint` instances).
78
79
        As a convenience, if `entry_points_iter` is a string it will be used
80
        to retrieve the iterator using `pkg_resources.iter_entry_points`.
81
82
        """
83
        if isinstance(entry_points_iter, str):
84
            entry_points_iter = \
85
                pkg_resources.iter_entry_points(entry_points_iter)
86
87
        for entry_point in entry_points_iter:
88
            try:
89
                point = entry_point.load()
90
            except pkg_resources.DistributionNotFound:
91
                log.error("Could not load '%s' (unsatisfied dependencies).",
92
                          entry_point, exc_info=True)
93
                continue
94
            except ImportError:
95
                log.error("An ImportError occurred while loading "
96
                          "entry point '%s'", entry_point, exc_info=True)
97
                continue
98
            except Exception:
0 ignored issues
show
Best Practice introduced by
Catching very general exceptions such as Exception is usually not recommended.

Generally, you would want to handle very specific errors in the exception handler. This ensure that you do not hide other types of errors which should be fixed.

So, unless you specifically plan to handle any error, consider adding a more specific exception.

Loading history...
99
                log.error("An exception occurred while loading "
100
                          "entry point '%s'", entry_point, exc_info=True)
101
                continue
102
103
            try:
104
                if isinstance(point, types.ModuleType):
105
                    if hasattr(point, "__path__"):
106
                        # Entry point is a package (a widget category)
107
                        self.process_category_package(
108
                            point,
109
                            name=entry_point.name,
110
                            distribution=entry_point.dist
111
                        )
112
                    else:
113
                        # Entry point is a module (a single widget)
114
                        self.process_widget_module(
115
                            point,
116
                            name=entry_point.name,
117
                            distribution=entry_point.dist
118
                        )
119
                elif isinstance(point, (types.FunctionType, types.MethodType)):
120
                    # Entry point is a callable loader function
121
                    self.process_loader(point)
122
                elif isinstance(point, (list, tuple)):
123
                    # An iterator yielding Category/WidgetDescriptor instances.
124
                    self.process_iter(point)
125
                else:
126
                    log.error("Cannot handle entry point %r", point)
127
            except Exception:
0 ignored issues
show
Best Practice introduced by
Catching very general exceptions such as Exception is usually not recommended.

Generally, you would want to handle very specific errors in the exception handler. This ensure that you do not hide other types of errors which should be fixed.

So, unless you specifically plan to handle any error, consider adding a more specific exception.

Loading history...
128
                log.error("An exception occurred while processing %r.",
129
                          entry_point, exc_info=True)
130
131
    def process_directory(self, directory):
132
        """
133
        Process and locate any widgets in directory on a file system,
134
        i.e. examines all .py files in the directory, delegating the
135
        to `process_file` (deprecated)
136
137
        """
138
        for filename in glob.glob(os.path.join(directory, "*.py")):
139
            self.process_file(filename)
140
141
    def process_file(self, filename):
142
        """
143
        Process .py file containing the widget code (deprecated).
144
        """
145
        filename = fix_pyext(filename)
146
        # TODO: zipped modules
0 ignored issues
show
Coding Style introduced by
TODO and FIXME comments should generally be avoided.
Loading history...
147
        if self.cache_has_valid_entry(filename):
148
            desc = self.cache_get(filename).description
149
            return
150
151
        if self.cache_can_ignore(filename):
152
            log.info("Ignoring %r.", filename)
153
            return
154
155
        try:
156
            desc = WidgetDescription.from_file(filename)
157
        except WidgetSpecificationError:
158
            self.cache_insert(filename, 0, None, None,
159
                              WidgetSpecificationError)
160
            return
161
        except Exception:
0 ignored issues
show
Best Practice introduced by
Catching very general exceptions such as Exception is usually not recommended.

Generally, you would want to handle very specific errors in the exception handler. This ensure that you do not hide other types of errors which should be fixed.

So, unless you specifically plan to handle any error, consider adding a more specific exception.

Loading history...
162
            log.info("Error processing a file.", exc_info=True)
163
            return
164
165
        self.handle_widget(desc)
166
167
    def process_widget_module(self, module, name=None, category_name=None,
0 ignored issues
show
Unused Code introduced by
The argument category_name seems to be unused.
Loading history...
168
                              distribution=None):
169
        """
170
        Process a widget module.
171
        """
172
        try:
173
            desc = self.widget_description(module, widget_name=name,
174
                                           distribution=distribution)
175
        except (WidgetSpecificationError, Exception) as ex:
0 ignored issues
show
Best Practice introduced by
Catching very general exceptions such as Exception is usually not recommended.

Generally, you would want to handle very specific errors in the exception handler. This ensure that you do not hide other types of errors which should be fixed.

So, unless you specifically plan to handle any error, consider adding a more specific exception.

Loading history...
Unused Code introduced by
The variable ex seems to be unused.
Loading history...
176
            log.info("Invalid widget specification.", exc_info=True)
177
            return
178
179
        self.handle_widget(desc)
180
181
    def process_category_package(self, category, name=None, distribution=None):
182
        """
183
        Process a category package.
184
        """
185
        cat_desc = None
186
        category = asmodule(category)
187
188
        if hasattr(category, "widget_discovery"):
189
            widget_discovery = getattr(category, "widget_discovery")
190
            self.process_loader(widget_discovery)
191
            return  # The widget_discovery function handles all
192
        elif hasattr(category, "category_description"):
193
            category_description = getattr(category, "category_description")
194
            try:
195
                cat_desc = category_description()
196
            except Exception:
0 ignored issues
show
Best Practice introduced by
Catching very general exceptions such as Exception is usually not recommended.

Generally, you would want to handle very specific errors in the exception handler. This ensure that you do not hide other types of errors which should be fixed.

So, unless you specifically plan to handle any error, consider adding a more specific exception.

Loading history...
197
                log.error("Error calling 'category_description' in %r.",
198
                          category, exc_info=True)
199
                cat_desc = default_category_for_module(category)
200
        else:
201
            try:
202
                cat_desc = CategoryDescription.from_package(category)
203
            except (CategorySpecificationError, Exception) as ex:
0 ignored issues
show
Best Practice introduced by
Catching very general exceptions such as Exception is usually not recommended.

Generally, you would want to handle very specific errors in the exception handler. This ensure that you do not hide other types of errors which should be fixed.

So, unless you specifically plan to handle any error, consider adding a more specific exception.

Loading history...
Unused Code introduced by
The variable ex seems to be unused.
Loading history...
204
                log.info("Package %r does not describe a category.", category,
205
                         exc_info=True)
206
                cat_desc = default_category_for_module(category)
207
208
        if name is not None:
209
            cat_desc.name = name
210
211
        if distribution is not None:
212
            cat_desc.project_name = distribution.project_name
213
214
        self.handle_category(cat_desc)
215
216
        desc_iter = self.iter_widget_descriptions(
217
                        category,
218
                        category_name=cat_desc.name,
219
                        distribution=distribution
220
                        )
221
222
        for desc in desc_iter:
223
            self.handle_widget(desc)
224
225
    def process_loader(self, callable):
0 ignored issues
show
Bug Best Practice introduced by
This seems to re-define the built-in callable.

It is generally discouraged to redefine built-ins as this makes code very hard to read.

Loading history...
226
        """
227
        Process a callable loader function.
228
        """
229
        try:
230
            callable(self)
231
        except Exception:
0 ignored issues
show
Best Practice introduced by
Catching very general exceptions such as Exception is usually not recommended.

Generally, you would want to handle very specific errors in the exception handler. This ensure that you do not hide other types of errors which should be fixed.

So, unless you specifically plan to handle any error, consider adding a more specific exception.

Loading history...
232
            log.error("Error calling %r", callable, exc_info=True)
233
234
    def process_iter(self, iter):
0 ignored issues
show
Documentation introduced by
Empty method docstring
Loading history...
Bug Best Practice introduced by
This seems to re-define the built-in iter.

It is generally discouraged to redefine built-ins as this makes code very hard to read.

Loading history...
235
        """
236
        """
237
        for desc in iter:
238
            if isinstance(desc, CategoryDescription):
239
                self.handle_category(desc)
240
            elif isinstance(desc, WidgetDescription):
241
                self.handle_widget(desc)
242
            else:
243
                log.error("Category or Widget Description instance "
244
                          "expected. Got %r.", desc)
245
246
    def handle_widget(self, desc):
247
        """
248
        Handle a found widget description.
249
250
        Base implementation adds it to the registry supplied in the
251
        constructor.
252
253
        """
254
        if self.registry:
255
            self.registry.register_widget(desc)
256
257
    def handle_category(self, desc):
258
        """
259
        Handle a found category description.
260
261
        Base implementation adds it to the registry supplied in the
262
        constructor.
263
264
        """
265
        if self.registry:
266
            self.registry.register_category(desc)
267
268
    def iter_widget_descriptions(self, package, category_name=None,
269
                                 distribution=None):
270
        """
271
        Return an iterator over widget descriptions accessible from
272
        `package`.
273
274
        """
275
        package = asmodule(package)
276
277
        for path in package.__path__:
278
            for _, mod_name, ispkg in pkgutil.iter_modules([path]):
279
                if ispkg:
280
                    continue
281
                name = package.__name__ + "." + mod_name
282
                source_path = os.path.join(path, mod_name + ".py")
283
                desc = None
284
285
                # Check if the path can be ignored.
286
                if self.cache_can_ignore(source_path, distribution):
287
                    log.info("Ignoring %r.", source_path)
288
                    continue
289
290
                # Check if a source file for the module is available
291
                # and is already cached.
292
                if self.cache_has_valid_entry(source_path, distribution):
293
                    desc = self.cache_get(source_path).description
294
295
                if desc is None:
296
                    try:
297
                        module = asmodule(name)
298
                    except ImportError:
299
                        log.info("Could not import %r.", name, exc_info=True)
300
                        continue
301
                    except Exception:
0 ignored issues
show
Best Practice introduced by
Catching very general exceptions such as Exception is usually not recommended.

Generally, you would want to handle very specific errors in the exception handler. This ensure that you do not hide other types of errors which should be fixed.

So, unless you specifically plan to handle any error, consider adding a more specific exception.

Loading history...
302
                        log.warning("Error while importing %r.", name,
303
                                    exc_info=True)
304
                        continue
305
306
                    try:
307
                        desc = self.widget_description(
308
                                 module,
309
                                 category_name=category_name,
310
                                 distribution=distribution
311
                                 )
312
                    except WidgetSpecificationError:
313
                        self.cache_log_error(
314
                                 source_path, WidgetSpecificationError,
315
                                 distribution
316
                                 )
317
318
                        continue
319
                    except Exception:
0 ignored issues
show
Best Practice introduced by
Catching very general exceptions such as Exception is usually not recommended.

Generally, you would want to handle very specific errors in the exception handler. This ensure that you do not hide other types of errors which should be fixed.

So, unless you specifically plan to handle any error, consider adding a more specific exception.

Loading history...
320
                        log.warning("Problem parsing %r", name, exc_info=True)
321
                        continue
322
                yield desc
323
                self.cache_insert(source_path, os.stat(source_path).st_mtime,
324
                                  desc, distribution)
325
326
    def widget_description(self, module, widget_name=None,
0 ignored issues
show
Coding Style introduced by
This method could be written as a function/class method.

If a method does not access any attributes of the class, it could also be implemented as a function or static method. This can help improve readability. For example

class Foo:
    def some_method(self, x, y):
        return x + y;

could be written as

class Foo:
    @classmethod
    def some_method(cls, x, y):
        return x + y;
Loading history...
327
                           category_name=None, distribution=None):
328
        """
329
        Return a widget description from a module.
330
        """
331
        if isinstance(module, str):
332
            module = __import__(module, fromlist=[""])
333
334
        desc = None
335
        try:
336
            desc = WidgetDescription.from_module(module)
337
        except WidgetSpecificationError:
338
            try:
339
                desc = WidgetDescription.from_file(
340
                    module.__file__, import_name=module.__name__
341
                )
342
            except Exception:
0 ignored issues
show
Unused Code introduced by
This except handler seems to be unused and could be removed.

Except handlers which only contain pass and do not have an else clause can usually simply be removed:

try:
    raises_exception()
except:  # Could be removed
    pass
Loading history...
Best Practice introduced by
Catching very general exceptions such as Exception is usually not recommended.

Generally, you would want to handle very specific errors in the exception handler. This ensure that you do not hide other types of errors which should be fixed.

So, unless you specifically plan to handle any error, consider adding a more specific exception.

Loading history...
343
                pass
344
345
            if desc is None:
346
                raise
347
348
        if widget_name is not None:
349
            desc.name = widget_name
350
351
        if category_name is not None:
352
            desc.category = category_name
353
354
        if distribution is not None:
355
            desc.project_name = distribution.project_name
356
357
        return desc
358
359
    def cache_insert(self, module, mtime, description, distribution=None,
360
                     error=None):
361
        """
362
        Insert the description into the cache.
363
        """
364
        if isinstance(module, types.ModuleType):
365
            mod_path = module.__file__
366
            mod_name = module.__name__
367
        else:
368
            mod_path = module
369
            mod_name = None
370
        mod_path = fix_pyext(mod_path)
371
372
        project_name = project_version = None
373
374
        if distribution is not None:
375
            project_name = distribution.project_name
376
            project_version = distribution.version
377
378
        exc_type = exc_val = None
379
380
        if error is not None:
381
            if isinstance(error, type):
382
                exc_type = error
383
                exc_val = None
384
            elif isinstance(error, Exception):
385
                exc_type = type(error)
386
                exc_val = repr(error.args)
387
388
        self.cached_descriptions[mod_path] = \
389
                _CacheEntry(mod_path, mod_name, mtime, project_name,
390
                            project_version, exc_type, exc_val,
391
                            description)
392
393
    def cache_get(self, mod_path, distribution=None):
0 ignored issues
show
Unused Code introduced by
The argument distribution seems to be unused.
Loading history...
394
        """
395
        Get the cache entry for `mod_path`.
396
        """
397
        if isinstance(mod_path, types.ModuleType):
398
            mod_path = mod_path.__file__
399
        mod_path = fix_pyext(mod_path)
400
        return self.cached_descriptions.get(mod_path)
401
402
    def cache_has_valid_entry(self, mod_path, distribution=None):
403
        """
404
        Does the cache have a valid entry for `mod_path`.
405
        """
406
        mod_path = fix_pyext(mod_path)
407
408
        if not os.path.exists(mod_path):
409
            return False
410
411
        if mod_path in self.cached_descriptions:
412
            entry = self.cache_get(mod_path)
413
            mtime = os.stat(mod_path).st_mtime
414
            if entry.mtime != mtime:
415
                return False
416
417
            if distribution is not None:
418
                if entry.project_name != distribution.project_name or \
419
                        entry.project_version != distribution.version:
420
                    return False
421
422
            if entry.exc_type == WidgetSpecificationError:
423
                return False
424
425
            # All checks pass
426
            return True
427
428
        return False
429
430
    def cache_can_ignore(self, mod_path, distribution=None):
0 ignored issues
show
Unused Code introduced by
The argument distribution seems to be unused.
Loading history...
431
        """
432
        Can the `mod_path` be ignored (i.e. it was determined that it
433
        could not contain a valid widget description, for instance the
434
        module does not have a valid description and was not changed from
435
        the last discovery run).
436
437
        """
438
        mod_path = fix_pyext(mod_path)
439
        if not os.path.exists(mod_path):
440
            # Possible orphaned .py[co] file
441
            return True
442
443
        mtime = os.stat(mod_path).st_mtime
444
        if mod_path in self.cached_descriptions:
445
            entry = self.cached_descriptions[mod_path]
446
            return entry.mtime == mtime and \
447
                    entry.exc_type == WidgetSpecificationError
448
        else:
449
            return False
450
451
    def cache_log_error(self, mod_path, error, distribution=None):
452
        """
453
        Cache that the `error` occurred while processing `mod_path`.
454
        """
455
        mod_path = fix_pyext(mod_path)
456
        if not os.path.exists(mod_path):
457
            # Possible orphaned .py[co] file
458
            return
459
        mtime = os.stat(mod_path).st_mtime
460
461
        self.cache_insert(mod_path, mtime, None, distribution, error)
462
463
464
def fix_pyext(mod_path):
465
    """
466
    Fix a module filename path extension to always end with the
467
    modules source file (i.e. strip compiled/optimized .pyc, .pyo
468
    extension and replace it with .py).
469
470
    """
471
    if mod_path[-4:] in [".pyo", "pyc"]:
472
        mod_path = mod_path[:-1]
473
    return mod_path
474
475
476
def widget_descriptions_from_package(package):
477
    package = asmodule(package)
478
479
    desciptions = []
480
    for _, name, ispkg in pkgutil.iter_modules(
481
            package.__path__, package.__name__ + "."):
482
        if ispkg:
483
            continue
484
        try:
485
            module = asmodule(name)
486
        except Exception:
0 ignored issues
show
Best Practice introduced by
Catching very general exceptions such as Exception is usually not recommended.

Generally, you would want to handle very specific errors in the exception handler. This ensure that you do not hide other types of errors which should be fixed.

So, unless you specifically plan to handle any error, consider adding a more specific exception.

Loading history...
487
            log.error("Error importing %r.", name, exc_info=True)
488
            continue
489
490
        desc = None
491
        try:
492
            desc = WidgetDescription.from_module(module)
493
        except Exception:
0 ignored issues
show
Unused Code introduced by
This except handler seems to be unused and could be removed.

Except handlers which only contain pass and do not have an else clause can usually simply be removed:

try:
    raises_exception()
except:  # Could be removed
    pass
Loading history...
Best Practice introduced by
Catching very general exceptions such as Exception is usually not recommended.

Generally, you would want to handle very specific errors in the exception handler. This ensure that you do not hide other types of errors which should be fixed.

So, unless you specifically plan to handle any error, consider adding a more specific exception.

Loading history...
494
            pass
495
        if not desc:
496
            try:
497
                desc = WidgetDescription.from_file(
498
                    module.__file__, import_name=name
499
                    )
500
            except Exception:
0 ignored issues
show
Unused Code introduced by
This except handler seems to be unused and could be removed.

Except handlers which only contain pass and do not have an else clause can usually simply be removed:

try:
    raises_exception()
except:  # Could be removed
    pass
Loading history...
Best Practice introduced by
Catching very general exceptions such as Exception is usually not recommended.

Generally, you would want to handle very specific errors in the exception handler. This ensure that you do not hide other types of errors which should be fixed.

So, unless you specifically plan to handle any error, consider adding a more specific exception.

Loading history...
501
                pass
502
503
        if not desc:
504
            log.info("Error in %r", name, exc_info=True)
505
        else:
506
            desciptions.append(desc)
507
    return desciptions
508
509
510
def module_name_split(name):
511
    """
512
    Split the module name into package name and module name.
513
    """
514
    if "." in name:
515
        package_name, module = name.rsplit(".", 1)
516
    else:
517
        package_name, module = "", name
518
    return package_name, module
519
520
521
def module_modified_time(module):
522
    """
523
    Return the `module`s source filename and modified time as a tuple
524
    (source_filename, modified_time). In case the module is from a zipped
525
    package the modified time is that of of the archive.
526
527
    """
528
    module = asmodule(module)
529
    name = module.__name__
530
    module_filename = module.__file__
531
532
    provider = pkg_resources.get_provider(name)
533
    if provider.loader:
534
        m_time = os.stat(provider.loader.archive)[stat.ST_MTIME]
535
    else:
536
        basename = os.path.basename(module_filename)
537
        path = pkg_resources.resource_filename(name, basename)
538
        m_time = os.stat(path)[stat.ST_MTIME]
539
    return (module_filename, m_time)
540
541
542
def asmodule(module):
543
    """
544
    Return the module references by `module` name. If `module` is
545
    already an imported module instance, return it as is.
546
547
    """
548
    if isinstance(module, types.ModuleType):
549
        return module
550
    elif isinstance(module, str):
551
        return __import__(module, fromlist=[""])
552
    else:
553
        raise TypeError(type(module))
554
555
556
def run_discovery(entry_point, cached=False):
0 ignored issues
show
Unused Code introduced by
The argument entry_point seems to be unused.
Loading history...
557
    """
558
    Run the default widget discovery and return a :class:`WidgetRegistry`
559
    instance.
560
561
    """
562
    reg_cache = {}
563
    if cached:
564
        reg_cache = cache.registry_cache()
565
566
    registry = WidgetRegistry()
567
    discovery = WidgetDiscovery(registry, cached_descriptions=reg_cache)
568
    discovery.run()
569
    if cached:
570
        cache.save_registry_cache(reg_cache)
571
    return registry
572