source: pyromaths/trunk/fuentes/src/pyromaths/ex/__init__.py @ 423

Last change on this file since 423 was 423, checked in by mabarracus, 3 years ago

add sources from pyromaths 15.10

File size: 5.9 KB
Line 
1#!/usr/bin/python
2# -*- coding: utf-8 -*-
3#
4import inspect
5import os
6import pkgutil
7import types
8import sys
9
10class Exercise(object):
11    ''' Base class for all exercise types. '''
12
13    def __str__(self):
14        return self.description
15
16
17class TexExercise(Exercise):
18    ''' Exercise with TeX support. '''
19
20    @classmethod
21    def name(cls):
22        return cls.__name__
23
24    @classmethod
25    def thumb(cls):
26        from pyromaths.Values import data_dir
27        return os.path.join(data_dir(), 'ex', cls.dirlevel, 'img', "%s.png" % cls.name())
28
29    @classmethod
30    def id(cls):
31        return "{}.{}".format(
32            cls.dirlevel,
33            cls.name(),
34            )
35
36    def tex_statement(self):
37        ''' Return problem statement in TeX format. '''
38        raise NotImplementedError()
39        return ["\\exercice TODO"]
40
41    def tex_answer(self):
42        ''' Return full answer in TeX format. '''
43        return ["\\exercice* TODO"]
44
45
46class LegacyExercise(TexExercise):
47    ''' Base class for legacy format exercise proxies. '''
48
49    function = []
50
51    def __init__(self):
52        self.stat, self.ans = self.function[0]()
53
54    @classmethod
55    def name(cls):
56        return cls.function[0].__name__
57
58    def tex_statement(self):
59        return self.stat
60
61    def tex_answer(self):
62        return self.ans
63
64def __module(filename):
65    """Expect an absolute path, subpath of this module's path. Return a relative path."""
66    # Get root of this application
67    root = '/'.join(__file__.split('/')[:-len(__name__.split('.')) - 1])
68    # Get filename, relative to said root
69    relative = filename[len(root)+1:]
70    # Remove extension
71    relative = relative[:-len('.py')]
72    # Turn file system path into python package
73    relative = relative.replace('/', '.')
74
75    return relative
76
77def __legacy(function, dirlevel):
78    ''' Create a new class proxying for a legacy exercise 'function'. '''
79    # Create a proxy class inheriting from LegacyExercise for this function
80    module = __module(function.func_code.co_filename)
81    name = function.func_name.title().replace('_', '')
82    return type("{}.{}".format(module, name),
83                (LegacyExercise,),
84                dict(description=function.description,
85                     level=function.level,
86                     module=module,
87                     function=(function,),
88                     dirlevel=dirlevel,
89                     )
90                )
91
92def __hasdescription(obj):
93    ''' Has 'obj' a legit description? '''
94    if 'description' not in dir(obj): return False
95    description = obj.__dict__['description']
96    # description must be some kind of string (preferably unicode)
97    if not isinstance(description, basestring): return False
98    return True
99
100def __islegacy(obj):
101    ''' Is target object an exercise in legacy format? '''
102    return inspect.isfunction(obj) and __hasdescription(obj)
103
104def __isexercise(obj):
105    ''' Is target object an exercise (in new format)? '''
106    return inspect.isclass(obj) and issubclass(obj, Exercise) and __hasdescription(obj)
107
108def __level(level):
109    ''' Format academic level(s). '''
110    # level may be a string or a list (default)
111    if not isinstance(level, list): level = [level]
112    level.sort()
113    return level
114
115def __import(name=__name__, parent=None):
116    ''' Import 'name' from 'parent' package. '''
117    if not isinstance(name, basestring):
118        name = name.__name__
119    # parent is None: assume 'name' is a package name
120    # hack tout moche pour l'import des exercices dans la version Windows de Pyromaths :
121    # Les modules sixiemes, quatriemes doivent être appelés avec le chemin complet,
122    # alors que les exercices cinquiemes.aires ne doivent être appelés qu'ainsi.
123    if "." not in name and hasattr(sys, "frozen"): name = "pyromaths.ex." + name
124    if parent is None: parent = name
125    elif not isinstance(parent, basestring):
126        # assume 'parent' is a package instance
127        parent = parent.__name__
128    return __import__(name, fromlist=parent)
129
130def _exercises(pkg):
131    ''' List exercises in 'pkg' modules. '''
132    # level defaults to description, then unknown
133    if 'level' not in dir(pkg): pkg.level = u"Inconnu"
134    for _, name, ispkg in pkgutil.iter_modules(pkg.__path__, pkg.__name__ + '.'):
135        # skip packages
136        if ispkg: continue;
137        # import module
138        mod = __import(name, pkg)
139        if 'level' not in dir(mod): mod.level = pkg.level
140        # search exercises in module
141        for element in dir(mod):
142            element = mod.__dict__[element]
143            level = __level(element.level if 'level' in dir(element)
144                              else mod.level)
145
146            if __isexercise(element) or __islegacy(element):
147                dirlevel = os.path.split(pkg.__path__[0])[1]
148                element.level = level
149            if __isexercise(element):
150                element.dirlevel = dirlevel
151                yield element
152            elif __islegacy(element):
153                yield __legacy(element, dirlevel)
154
155def _subpackages(pkg):
156    ''' List 'pkg' sub-packages. '''
157    for _, name, ispkg in pkgutil.iter_modules(pkg.__path__, pkg.__name__ + '.'):
158        # skip modules
159        if not ispkg: continue;
160        yield __import(name, pkg)
161
162def load_levels(pkg=None, recursive=True):
163    ''' Discover exercises. '''
164    levels = {}
165    # target package defaults to this package (pyromaths.ex)
166    if pkg is None: pkg = __import()
167    # load package exercises
168    for ex in _exercises(pkg):
169        for lvl in ex.level:
170            # new level? create its exercise list
171            if lvl not in levels.keys():
172                levels[lvl] = []
173            levels[lvl].append(ex)
174
175    if recursive:
176        # load sub-packages
177        for pk in _subpackages(pkg):
178            sublevels = load_levels(pk)
179            for lvl in sublevels:
180                if lvl in levels:
181                    levels[lvl].extend(sublevels[lvl])
182                else:
183                    levels[lvl] = sublevels[lvl]
184
185    return levels
Note: See TracBrowser for help on using the repository browser.