├── .gitignore
├── LICENSE
├── README.md
├── bin
└── create_sample_src.sh
├── dev_achievements
├── __init__.py
├── achievements.py
├── processing
│ ├── __init__.py
│ ├── tree.py
│ └── visitor.py
└── utilities
│ ├── __init__.py
│ ├── constants.py
│ └── utils.py
├── setup.py
├── test_src.py
└── tests
├── __init__.py
├── samples
├── invalid
│ ├── AssignAchievement.py
│ ├── BitwiseOperatorsAchievement.py
│ ├── ClassAchievement.py
│ ├── ComprehensionsAchievement.py
│ ├── ConditionalAchievement.py
│ ├── DictAchievement.py
│ ├── FunctionAchievement.py
│ ├── HelloWorldAchievement.py
│ ├── LambdaAchievement.py
│ ├── ListAchievement.py
│ ├── LoopsAchievement.py
│ ├── MathOperatorsAchievement.py
│ └── PassAchievement.py
└── valid
│ ├── AssignAchievement.py
│ ├── BitwiseOperatorsAchievement.py
│ ├── ClassAchievement.py
│ ├── ComprehensionsAchievement.py
│ ├── ConditionalAchievement.py
│ ├── DictAchievement.py
│ ├── FunctionAchievement.py
│ ├── HelloWorldAchievement.py
│ ├── LambdaAchievement.py
│ ├── ListAchievement.py
│ ├── LoopsAchievement.py
│ ├── MathOperatorsAchievement.py
│ └── PassAchievement.py
└── test.py
/.gitignore:
--------------------------------------------------------------------------------
1 | dev_achievements/store.json
2 |
3 | # TODO: temp files - remove later
4 | ast_list.txt
5 |
6 | # MacOS files
7 | .DS_STORE
8 |
9 | # Byte-compiled / optimized / DLL files
10 | __pycache__/
11 | *.py[cod]
12 | *$py.class
13 |
14 | # C extensions
15 | *.so
16 |
17 | # Distribution / packaging
18 | .Python
19 | build/
20 | develop-eggs/
21 | dist/
22 | downloads/
23 | eggs/
24 | .eggs/
25 | lib/
26 | lib64/
27 | parts/
28 | sdist/
29 | var/
30 | wheels/
31 | pip-wheel-metadata/
32 | share/python-wheels/
33 | *.egg-info/
34 | .installed.cfg
35 | *.egg
36 | MANIFEST
37 |
38 | # PyInstaller
39 | # Usually these files are written by a python script from a template
40 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
41 | *.manifest
42 | *.spec
43 |
44 | # Installer logs
45 | pip-log.txt
46 | pip-delete-this-directory.txt
47 |
48 | # Unit test / coverage reports
49 | htmlcov/
50 | .tox/
51 | .nox/
52 | .coverage
53 | .coverage.*
54 | .cache
55 | nosetests.xml
56 | coverage.xml
57 | *.cover
58 | *.py,cover
59 | .hypothesis/
60 | .pytest_cache/
61 |
62 | # Translations
63 | *.mo
64 | *.pot
65 |
66 | # Django stuff:
67 | *.log
68 | local_settings.py
69 | db.sqlite3
70 | db.sqlite3-journal
71 |
72 | # Flask stuff:
73 | instance/
74 | .webassets-cache
75 |
76 | # Scrapy stuff:
77 | .scrapy
78 |
79 | # Sphinx documentation
80 | docs/_build/
81 |
82 | # PyBuilder
83 | target/
84 |
85 | # Jupyter Notebook
86 | .ipynb_checkpoints
87 |
88 | # IPython
89 | profile_default/
90 | ipython_config.py
91 |
92 | # pyenv
93 | .python-version
94 |
95 | # pipenv
96 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
97 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
98 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
99 | # install all needed dependencies.
100 | #Pipfile.lock
101 |
102 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow
103 | __pypackages__/
104 |
105 | # Celery stuff
106 | celerybeat-schedule
107 | celerybeat.pid
108 |
109 | # SageMath parsed files
110 | *.sage.py
111 |
112 | # Environments
113 | .env
114 | .venv
115 | env/
116 | venv/
117 | ENV/
118 | env.bak/
119 | venv.bak/
120 |
121 | # Spyder project settings
122 | .spyderproject
123 | .spyproject
124 |
125 | # Rope project settings
126 | .ropeproject
127 |
128 | # mkdocs documentation
129 | /site
130 |
131 | # mypy
132 | .mypy_cache/
133 | .dmypy.json
134 | dmypy.json
135 |
136 | # Pyre type checker
137 | .pyre/
138 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Ravi Patel
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # dev-achievements
2 | Earn Achievements while learning how to code
3 |
4 |
5 | Game-ify your experience while learning to code and get achievements for different concepts as you use them. Start off at a simple `print` statement and work your way up to `functions` and `classes` (and more to come).
6 |
7 |
8 |
9 |
10 |
11 | ### Example
12 |
13 | Just `import` the package at the top of your script (example):
14 |
15 | ```python
16 | # my_script.py
17 |
18 | import dev_achievements
19 |
20 |
21 | def cumulative_sum(x):
22 | # calculates sum of numbers from 0 to x
23 | total = 0
24 | for i in range(x):
25 | total += i
26 | return total
27 |
28 |
29 | result = cumulative_sum(4)
30 | print('value: ', result)
31 | ```
32 |
33 | And run it in terminal as you normally would:
34 | ```shell
35 | $ python3 my_script.py
36 |
37 | ┌──────────────────────────────────┐
38 | │ Achievement Unlocked: Loops! │
39 | │ Achievement Unlocked: Functions! │
40 | └──────────────────────────────────┘
41 |
42 | value: 6
43 | ```
44 |
45 |
46 |
47 |
48 |
49 | ### Usage
50 |
51 | To install and use:
52 | 1. Install the package with `pip install dev-achievements`
53 | 1. Use `import dev_achievements` at the top of your script
54 | 1. Run your `python` script as normal
55 |
56 | To uninstall:
57 | 1. Uninstall the package with `pip uninstall dev-achievements`
58 | 1. Optionally, delete the directory `~/.dev_achievements` to remove any achievement progress. Keep this directory to save progress through installs.
59 |
60 |
61 |
62 |
63 |
64 | ### Some things to note
65 |
66 | 1. Currently this _only_ works on single file scripts - if you import your own module (e.g. for utility functions) that module will _not_ be parsed (planning on fixing this though)
67 | 1. Some achievements have _dependencies_, and will only be unlocked once previous ones have been unlocked
68 | 1. Unlocked achievements will remain unlocked, so those "Achievement Unlocked" messages will should only show once per achievement
69 |
70 |
71 |
72 |
73 |
74 | ### Future plans and contributing
75 | 1. The bare bones achievements are currently implemented (using `"hello world"`, `for` loops, `lists`, `functions`, etc.) - more achievements are in development (see issue [#1](https://github.com/raviolliii/dev-achievements/issues/1))
76 | 1. More details on how to contribute (along with code docs to help with the development process) are coming soon
77 |
--------------------------------------------------------------------------------
/bin/create_sample_src.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 |
4 | # quick way to make sample src files for an Achievement
5 | # usage: create_sample_src.sh achievement_name
6 | # it'll create the files under valid and invalid subdirs
7 | # using the achievement_name
8 |
9 |
10 | VALID_PATH="./tests/samples/valid/$1.py"
11 | INVALID_PATH="./tests/samples/invalid/$1.py"
12 |
13 | touch $VALID_PATH
14 | touch $INVALID_PATH
15 |
16 | printf "# cases where $1 should unlock\n\n# >> CASE\n" > $VALID_PATH
17 | printf "# cases where $1 should not unlock\n\n# >> CASE\n" > $INVALID_PATH
18 |
--------------------------------------------------------------------------------
/dev_achievements/__init__.py:
--------------------------------------------------------------------------------
1 | # __init__.py
2 | # -----------
3 | # Primary executor of dev_achievements. Runs syntax processing
4 | # and Achievement checking on the file that imported the package.
5 |
6 | import ast
7 | import os
8 | import sys
9 |
10 | from dev_achievements.processing.visitor import Visitor
11 | from dev_achievements.utilities.utils import bordered
12 |
13 |
14 | # package version
15 | __version__ = '1.0.3'
16 |
17 |
18 | def process_tree(tree):
19 | """ Creates an AST Node Visitor to process the built
20 | syntax tree.
21 |
22 | Args:
23 | tree (ast.AST): AST syntax tree
24 | """
25 | v = Visitor()
26 | v.visit(tree)
27 | unlocked = v.check_achievements()
28 | if unlocked:
29 | text = '\n'.join(a.unlock_message for a in unlocked)
30 | print('\n' + bordered(text) + '\n')
31 | return
32 |
33 |
34 | def build_tree(file_path):
35 | """ Creates an AST syntax tree from the source file.
36 |
37 | Args:
38 | file_path (str): path of source file
39 |
40 | Returns:
41 | ast.AST: Root of syntax tree
42 | """
43 | tree = None
44 | with open(file_path, 'r') as file:
45 | tree = ast.parse(file.read())
46 | return tree
47 |
48 |
49 | def process_file(file_path):
50 | """ Builds an AST syntax tree and visits each node to
51 | process for Achievements.
52 |
53 | Args:
54 | file_path (str): path of file
55 | """
56 | tree = build_tree(file_path)
57 | process_tree(tree)
58 | return
59 |
60 |
61 | # run the whole Achievement process on package import
62 | # if the passed script path exists
63 | if __name__ != '__main__':
64 | if len(sys.argv) > 0 and os.path.isfile(sys.argv[0]):
65 | process_file(sys.argv[0])
66 |
--------------------------------------------------------------------------------
/dev_achievements/achievements.py:
--------------------------------------------------------------------------------
1 | # achievements.py
2 | # ---------------
3 | # Contains all implemented Achievements.
4 |
5 | import ast
6 | from abc import abstractmethod
7 |
8 |
9 | # Base classes
10 | # ------------
11 |
12 |
13 | class Achievement:
14 | """ Base Achievement with common unlock and dependency fields.
15 |
16 | New Achievements should sublass this base, implementing the
17 | _check_condition method to specify when it should be unlocked.
18 |
19 | Attributes:
20 | unlocked (bool): unlock state
21 | dependencies (list[Achievements]): list of Achievements to unlock first
22 | on_unlock (function): callback on Achievement unlock
23 | unlock_message (str): pretty formatted message after being unlocked
24 |
25 | Args:
26 | unlocked (bool): unlock state
27 | on_unlock (function): Achievement unlock handler
28 | """
29 | def __init__(self, unlocked=False, on_unlock=None):
30 | # unlocked state and dependencies
31 | self._unlocked = unlocked
32 | self.dependencies = []
33 | # state change handlers
34 | self.on_unlock = on_unlock
35 |
36 | @property
37 | def unlocked(self):
38 | """ Get unlocked state """
39 | return self._unlocked
40 |
41 | @unlocked.setter
42 | def unlocked(self, status):
43 | """ Sets the unlocked state, and calls the event handler
44 | accordingly.
45 |
46 | Args:
47 | status (bool): new unlocked state
48 | """
49 | self._unlocked = status
50 | if self._unlocked and callable(self.on_unlock):
51 | self.on_unlock()
52 | return
53 |
54 | @property
55 | def unlock_message(self):
56 | """ Gives the message to show when unlocked based on the
57 | Achievement title.
58 |
59 | Returns:
60 | str: Pretty formatted unlock message
61 | """
62 | if not hasattr(self, 'title'):
63 | return 'Achievement Unlocked!'
64 | return f'Achievement Unlocked: {self.title}'
65 |
66 | @abstractmethod
67 | def _check_condition(self, nodes):
68 | """ When implemented, specifies whether or not this Achievement
69 | should be unlocked based on the given AST node table. Must be
70 | implemented for every subclass of Achievement.
71 |
72 | Args:
73 | nodes (dict): table of ast.AST nodes in tree
74 |
75 | Returns:
76 | bool: True if this Achievement should be unlocked, False otherwise
77 |
78 | Raises:
79 | NotImplementedError: If not implemented in subclasses
80 | """
81 | msg = 'Must implement _check_condition method for' \
82 | + f' class {self.__class__.__name__}'
83 | raise NotImplementedError(msg)
84 |
85 | def check(self, nodes):
86 | """ Calls the _check_condition method with the given AST nodes
87 | and updates the unlocked state accordingly.
88 |
89 | Args:
90 | nodes (dict): table of ast.AST nodes in tree
91 |
92 | Returns:
93 | bool: Unlocked state after checking condition
94 | """
95 | if not self.unlocked:
96 | self.unlocked = self._check_condition(nodes)
97 | return self.unlocked
98 |
99 | @classmethod
100 | def subclasses(cls):
101 | """ Returns a list of subclasses to Achievement """
102 | return cls.__subclasses__()
103 |
104 | def __init_subclass__(cls, /, **kwargs):
105 | """ Each subclass to this Achievement will get a UID as a class
106 | variable. The UID is just sequentially generated on every subclass.
107 |
108 | Reference:
109 | https://docs.python.org/3/reference/datamodel.html#customizing-class-creation
110 | """
111 | super().__init_subclass__(**kwargs)
112 | cls.uid = len(Achievement.__subclasses__()) - 1
113 |
114 | def __repr__(self):
115 | """ Representation version of the Achievement """
116 | res = f'{self.__class__.__name__}(dependencies={self.dependencies})'
117 | return res
118 |
119 | def __str__(self):
120 | """ String version of the Achievement """
121 | ulock_str = 'unlocked' if self.unlocked else 'locked'
122 | res = f'Achievement {self.uid}: {self.__class__.__name__}' \
123 | + f' ({ulock_str})'
124 | return res
125 |
126 |
127 | # All unlockable achievements
128 | # ---------------------------
129 |
130 |
131 | class SampleAchievement(Achievement):
132 | """ Sample Achievement for reference """
133 | def __init__(self, **kwargs):
134 | super().__init__(**kwargs)
135 | self.title = 'SAMPLE'
136 |
137 | def _check_condition(self, nodes):
138 | """ Check something """
139 | return False
140 |
141 |
142 | class HelloWorldAchievement(Achievement):
143 | """ Unlocks on printing Hello World """
144 | def __init__(self, **kwargs):
145 | super().__init__(**kwargs)
146 | self.title = 'Hello Hello!'
147 | self.dependencies = []
148 |
149 | def _check_condition(self, nodes):
150 | """ Checks for print call with "hello world" """
151 | for call in nodes.get(ast.Call, []):
152 | # has to call "print" function
153 | if call.func.id != print.__name__:
154 | continue
155 | # has to have at least 1 str literal (constant) argument
156 | if not len(call.args):
157 | continue
158 | arg = call.args[0]
159 | is_str = isinstance(arg, ast.Constant) and type(arg.value) == str
160 | # check "hello world" in first arg
161 | if is_str and 'hello world' in arg.value.lower():
162 | return True
163 | return False
164 |
165 |
166 | class AssignAchievement(Achievement):
167 | """ Unlocks on variable assignment """
168 | def __init__(self, **kwargs):
169 | super().__init__(**kwargs)
170 | self.title = 'Variables!'
171 |
172 | def _check_condition(self, nodes):
173 | """ Checks for assignment operator """
174 | return bool(ast.Assign in nodes)
175 |
176 |
177 | class MathOperatorsAchievement(Achievement):
178 | """ Unlocks on using any math (non binary) operator """
179 | def __init__(self, **kwargs):
180 | super().__init__(**kwargs)
181 | self.title = 'Operators!'
182 |
183 | def _check_condition(self, nodes):
184 | """ Checks for any non-binary operator """
185 | ops = [ast.Add, ast.Sub, ast.Mult, ast.Div,
186 | ast.FloorDiv, ast.Mod, ast.Pow, ast.MatMult]
187 | return any([o in nodes for o in ops])
188 |
189 |
190 | class BitwiseOperatorsAchievement(Achievement):
191 | """ Unlocks on using any bitwise operators """
192 | def __init__(self, **kwargs):
193 | super().__init__(**kwargs)
194 | self.title = 'Bitwise!'
195 | self.dependencies = [MathOperatorsAchievement]
196 |
197 | def _check_condition(self, nodes):
198 | """ Checks for any bitwise operators """
199 | ops = [ast.LShift, ast.RShift, ast.BitOr,
200 | ast.BitAnd, ast.BitXor, ast.Invert]
201 | return any([o in nodes for o in ops])
202 |
203 |
204 | class ConditionalAchievement(Achievement):
205 | """ Unlocks on using if statements """
206 | def __init__(self, **kwargs):
207 | super().__init__(**kwargs)
208 | self.title = 'If statements!'
209 |
210 | def _check_condition(self, nodes):
211 | """ Checks for if statements (regular and ternary) """
212 | reg = bool(ast.If in nodes)
213 | tern = bool(ast.IfExp in nodes)
214 | return reg or tern
215 |
216 |
217 | class LoopsAchievement(Achievement):
218 | """ Unlocks on loops (for and while) """
219 | def __init__(self, **kwargs):
220 | super().__init__(**kwargs)
221 | self.title = 'Loops!'
222 | self.dependencies = [AssignAchievement, ConditionalAchievement]
223 |
224 | def _check_condition(self, nodes):
225 | """ Checks for loop keywords """
226 | has_for = bool(ast.For in nodes)
227 | has_while = bool(ast.While in nodes)
228 | return has_for or has_while
229 |
230 |
231 | class ComprehensionsAchievement(Achievement):
232 | """ Unlocks on any form of comprehension (list, set, etc.) """
233 | def __init__(self, **kwargs):
234 | super().__init__(**kwargs)
235 | self.title = 'Comprehensions!'
236 | self.dependencies = [LoopsAchievement]
237 |
238 | def _check_condition(self, nodes):
239 | """ Checks for any form of comprehension """
240 | comps = [ast.ListComp, ast.SetComp, ast.GeneratorExp, ast.DictComp]
241 | return any([c in nodes for c in comps])
242 |
243 |
244 | class PassAchievement(Achievement):
245 | """ Unlocks on using pass """
246 | def __init__(self, **kwargs):
247 | super().__init__(**kwargs)
248 | self.title = 'Pass!'
249 | self.dependencies = [LoopsAchievement]
250 |
251 | def _check_condition(self, nodes):
252 | """ Checks for pass keyword """
253 | return bool(ast.Pass in nodes)
254 |
255 |
256 | class FunctionAchievement(Achievement):
257 | """ Unlocks on defining and calling a function """
258 | def __init__(self, **kwargs):
259 | super().__init__(**kwargs)
260 | self.title = 'Functions!'
261 | self.dependencies = [ConditionalAchievement, LoopsAchievement]
262 |
263 | def _check_condition(self, nodes):
264 | """ Checks for user defined function calls """
265 | fn_defs = nodes.get(ast.FunctionDef, [])
266 | fn_calls = nodes.get(ast.Call, [])
267 | # unique names of functions defined/called
268 | def_names = set([fn.name for fn in fn_defs])
269 | call_names = set([c.func.id for c in fn_calls])
270 | # at least one function must be defined and called
271 | return len(def_names & call_names) > 0
272 |
273 |
274 | class LambdaAchievement(Achievement):
275 | """ Unlocks on using lambda functions """
276 | def __init__(self, **kwargs):
277 | super().__init__(**kwargs)
278 | self.title = 'Lambdas!'
279 | self.dependencies = [FunctionAchievement]
280 |
281 | def _check_condition(self, nodes):
282 | """ Checks for lambda functions """
283 | return bool(ast.Lambda in nodes)
284 |
285 |
286 | class ListAchievement(Achievement):
287 | """ Unlocks on using lists """
288 | def __init__(self, **kwargs):
289 | super().__init__(**kwargs)
290 | self.title = 'Lists!'
291 |
292 | def _check_condition(self, nodes):
293 | """ Checks for list data type """
294 | return bool(ast.List in nodes)
295 |
296 |
297 | class DictAchievement(Achievement):
298 | """ Unlocks on using a dict """
299 | def __init__(self, **kwargs):
300 | super().__init__(**kwargs)
301 | self.title = 'Dictionaries!'
302 |
303 | def _check_condition(self, nodes):
304 | """ Checks for dict data type """
305 | return bool(ast.Dict in nodes)
306 |
307 |
308 | class ClassAchievement(Achievement):
309 | """ Unlocks on declaring and creating an instance of a class """
310 | def __init__(self, **kwargs):
311 | super().__init__(**kwargs)
312 | self.title = 'Classes!'
313 | self.dependencies = [FunctionAchievement]
314 |
315 | def _check_condition(self, nodes):
316 | """ Checks for creating a class and creating an instance """
317 | cls_defs = nodes.get(ast.ClassDef, [])
318 | cls_calls = nodes.get(ast.Call, [])
319 | # unique names of functions defined/called
320 | cls_names = set([fn.name for fn in cls_defs])
321 | call_names = set([c.func.id for c in cls_calls])
322 | # at least one function must be defined and called
323 | return len(cls_names & call_names) > 0
324 |
325 |
--------------------------------------------------------------------------------
/dev_achievements/processing/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/raviolliii/dev-achievements/da0a92f0d53b20192ef68ee1222cbf01fb0c0ed0/dev_achievements/processing/__init__.py
--------------------------------------------------------------------------------
/dev_achievements/processing/tree.py:
--------------------------------------------------------------------------------
1 | # tree.py
2 | # -------
3 | # Contains utilities for building and traversing an Achievement
4 | # tree (for finding Achievements to unlock next, etc.).
5 |
6 | from dev_achievements.achievements import *
7 | from dev_achievements.utilities.utils import load_store, save_completed
8 |
9 |
10 | class AchievementTree:
11 | """ Tree of Achievements with dependencies as edges.
12 |
13 | Attributes:
14 | nodes (list[Achievement]): all Achievements
15 | queue (list[Achievement]): list of unlockable Achievements
16 | """
17 | def __init__(self):
18 | self._init_nodes()
19 |
20 | def _init_nodes(self):
21 | """ Creates list of all Achievements according to their unlocked
22 | state. Each one prints its unlock message and saves to the store
23 | upon being unlocked.
24 | """
25 | unlocked_ach = load_store(field='unlocked')
26 |
27 | def _create_node(ach):
28 | # helper to create achievement with unlocked state and handler
29 | unlocked = ach.__name__ in unlocked_ach
30 | save_ach = lambda: save_completed(ach.__name__)
31 | return ach(unlocked=unlocked, on_unlock=save_ach)
32 |
33 | self.nodes = [_create_node(a) for a in Achievement.subclasses()]
34 | # set node dependencies as references to initialized nodes
35 | for node in self.nodes:
36 | node.dependencies = [self.nodes[d.uid] for d in node.dependencies]
37 | return
38 |
39 | @property
40 | def queue(self):
41 | """ Returns a list of unlockable Achievements.
42 | An Achievement is unlockable if it's still locked, and all its
43 | dependencies are unlocked.
44 |
45 | Returns:
46 | list: All unlockable Achievements
47 | """
48 | def _is_unlockable(node):
49 | # helper to determine if given node is unlockable
50 | par_unlocked = all([p.unlocked for p in node.dependencies])
51 | is_locked = not node.unlocked
52 | return par_unlocked and is_locked
53 | # filter nodes to unlockable ones
54 | return [n for n in self.nodes if _is_unlockable(n)]
55 |
--------------------------------------------------------------------------------
/dev_achievements/processing/visitor.py:
--------------------------------------------------------------------------------
1 | # visitor.py
2 | # ----------
3 | # Contains utilities for traversing and processing a built ast.AST
4 | # syntax tree (for building custom ast.AST table, etc.).
5 |
6 | import ast
7 |
8 | from dev_achievements.processing.tree import AchievementTree
9 |
10 |
11 | class Visitor(ast.NodeVisitor):
12 | """ Traverses the AST syntax tree and processes each node
13 | accordingly.
14 |
15 | Attributes:
16 | ach_tree (AchievementTree): Achievements in tree structure
17 | table (dict): table of ast.AST nodes in tree
18 | """
19 | def __init__(self):
20 | super().__init__()
21 | self.ach_tree = AchievementTree()
22 | self.table = {}
23 |
24 | def generic_visit(self, node):
25 | """ Processes any general AST node type, checking all
26 | Achievements in queue for any unlock state changes.
27 |
28 | Args:
29 | node (ast.AST): AST syntax tree node
30 | """
31 | node_class = node.__class__
32 | curr = self.table.get(node_class, [])
33 | self.table[node_class] = curr + [node]
34 | # call default visit traversal on node
35 | super().generic_visit(node)
36 | return
37 |
38 | def check_achievements(self):
39 | """ Checks and unlocks all possible Achievements. """
40 | unlocked = []
41 | changed = True
42 | check_ach = lambda ach: ach.check(self.table)
43 | while changed:
44 | # check if any Achievements have been unlocked
45 | checks = [(a, check_ach(a)) for a in self.ach_tree.queue]
46 | changed = any(c[1] for c in checks)
47 | # accumulate and return unlocked Achievements
48 | unlocked += [a for a, ch in checks if ch]
49 | return unlocked
50 |
--------------------------------------------------------------------------------
/dev_achievements/utilities/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/raviolliii/dev-achievements/da0a92f0d53b20192ef68ee1222cbf01fb0c0ed0/dev_achievements/utilities/__init__.py
--------------------------------------------------------------------------------
/dev_achievements/utilities/constants.py:
--------------------------------------------------------------------------------
1 | # constants.py
2 | # ------------
3 | # Just a bunch of constants values for use throughout the codebase.
4 |
5 | import os
6 |
7 |
8 | # Achievement store path
9 | _ROOT_PATH = os.path.expanduser('~')
10 | STORE_PATH = os.path.join(_ROOT_PATH, '.dev_achievements/store.json')
11 |
12 |
13 | # default Achievement store data
14 | DEFAULT_STORE = {
15 | 'unlocked': [],
16 | }
17 |
--------------------------------------------------------------------------------
/dev_achievements/utilities/utils.py:
--------------------------------------------------------------------------------
1 | # utils.py
2 | # --------
3 | # Contains a bunch of utility functions to make life easier
4 | # throughout the rest of the codebase.
5 |
6 | import json
7 | import os
8 | import pathlib
9 |
10 | from dev_achievements.utilities.constants import STORE_PATH, DEFAULT_STORE
11 |
12 |
13 | def load_json(file_path):
14 | """ Loads in a JSON file as a dict
15 |
16 | Args:
17 | file_path (str): path to JSON file
18 |
19 | Returns:
20 | dict: Data in file
21 | """
22 | data = {}
23 | with open(file_path, 'r') as file:
24 | data = json.load(file)
25 | return data
26 |
27 |
28 | def write_json(file_path, data):
29 | """ Writes given dict to a JSON file
30 |
31 | Args:
32 | file_path (str): path of JSON file
33 | data (dict): data to write
34 | """
35 | with open(file_path, 'w+') as file:
36 | json.dump(data, file, indent=4)
37 | return
38 |
39 |
40 | def load_store(field=None):
41 | """ Loads in Achievement store as a dict. If a field value
42 | is specified, the data in the field is returned. If there
43 | is no file in the configured STORE_PATH, the configured
44 | DEFAULT_STORE is used.
45 |
46 | Args:
47 | field (str, optional): dictionary field
48 |
49 | Returns:
50 | The whole data store, or the data in the field if one is given.
51 | """
52 | store = DEFAULT_STORE
53 | # load in store if saved
54 | if os.path.isfile(STORE_PATH):
55 | store = load_json(STORE_PATH)
56 | # get field if specified
57 | if field is not None:
58 | return store.get(field, None)
59 | return store
60 |
61 |
62 | def write_store(data):
63 | """ Writes given data to the Achievement store.
64 |
65 | Creates the full nested directory path of STORE_DIR
66 | if it doesn't exist, before writing/creating the store.
67 |
68 | Args:
69 | data (dict): updated Achievement store to write
70 | """
71 | store_dir = os.path.dirname(STORE_PATH)
72 | pathlib.Path(store_dir).mkdir(parents=True, exist_ok=True)
73 | return write_json(STORE_PATH, data)
74 |
75 |
76 | def save_completed(ach_name):
77 | """ Marks the given Achievement as unlocked in the store.
78 |
79 | Args:
80 | ach_name (Achievement): class of Achievement
81 | """
82 | store = load_store()
83 | store['unlocked'].append(ach_name)
84 | write_store(store)
85 | return
86 |
87 |
88 | def bordered(text):
89 | """ Pretty formats the given text in a solid box outline.
90 |
91 | Args:
92 | text (str): text within the box
93 |
94 | Returns:
95 | str: Boxed text
96 | """
97 | lines = text.splitlines()
98 | width = max([len(s) for s in lines])
99 | res = ['┌' + ('─' * (width + 2)) + '┐']
100 | for s in lines:
101 | sub = (s + (' ' * width))[:width]
102 | res.append('│ ' + sub + ' │')
103 | res.append('└' + ('─' * (width + 2)) + '┘')
104 | return '\n'.join(res)
105 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | # setup.py
2 | # --------
3 | # Specifies installation config for building/pypi.
4 |
5 | from setuptools import find_packages, setup
6 |
7 |
8 | # ignore unit tests
9 | excluded_packages = ['tests', 'tests.*']
10 |
11 | # load in README as long description
12 | with open('README.md', 'r') as file:
13 | long_description = file.read()
14 |
15 |
16 | setup(
17 | name='dev_achievements',
18 | version='1.0.3',
19 | author='Ravi Patel',
20 | author_email='ravi.patel1245@gmail.com',
21 | description='Earn Achievements while learning how to code',
22 | long_description=long_description,
23 | long_description_content_type='text/markdown',
24 | url='https://github.com/raviolliii/dev-achievements',
25 | packages=find_packages(exclude=excluded_packages),
26 | python_requires='>=3.5',
27 | install_requires=[],
28 | classifiers=[
29 | 'Programming Language :: Python :: 3',
30 | 'License :: OSI Approved :: MIT License',
31 | 'Operating System :: OS Independent',
32 | ],
33 | )
34 |
--------------------------------------------------------------------------------
/test_src.py:
--------------------------------------------------------------------------------
1 | # test_src.py
2 | # -----------
3 | # Basically a sample script that can be run to unlock any
4 | # Achievements specified below
5 |
6 | import dev_achievements
7 |
8 |
9 | # HelloWorldAchievement
10 | print('Helo world', end='')
11 | print('\nHello World!')
12 |
13 |
14 | # AssignAchievement
15 | x = 4
16 |
17 |
18 | # MathOperatorsAchievement
19 | x += 1
20 |
21 |
22 | # BitwiseOperatorsAchievement
23 | x << 2
24 | x ^ 2
25 |
26 |
27 | # ConditionalAchievement
28 | if x < 4:
29 | x = 4
30 | else:
31 | x = 5
32 |
33 |
34 | # LoopsAchievement
35 | for i in range(2):
36 | x + i
37 |
38 | x = 0
39 | while x < 5:
40 | x += 1
41 |
42 |
43 | # ComprehensionAchievement
44 | x = [i for i in range(10)]
45 |
46 |
47 | # PassAchievement
48 | for _ in range(2):
49 | pass
50 |
51 |
52 | # FunctionAchievement
53 | def some_func():
54 | pass
55 |
56 | def some_other_func():
57 | pass
58 |
59 | some_func()
60 |
61 |
62 | # LambdaAchievement
63 | lf = lambda x: x + 1
64 |
65 |
66 | # ListAchievement
67 | x = [4, 5, 6]
68 |
69 |
70 | # DictAchievement
71 | x = {'name': 'John Doe'}
72 |
73 |
74 | # ClassAchievement
75 | class Test:
76 | pass
77 |
78 | class OtherTest:
79 | pass
80 |
81 | t = Test()
82 |
83 |
--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/raviolliii/dev-achievements/da0a92f0d53b20192ef68ee1222cbf01fb0c0ed0/tests/__init__.py
--------------------------------------------------------------------------------
/tests/samples/invalid/AssignAchievement.py:
--------------------------------------------------------------------------------
1 | # cases where AssignAchievement should not unlock
2 |
3 | # >> CASE
4 |
5 | # technically this isn't allowed, but we're only really
6 | # checking syntax here, nothing else
7 | x
8 |
9 | # >> CASE
10 | print(x)
11 |
--------------------------------------------------------------------------------
/tests/samples/invalid/BitwiseOperatorsAchievement.py:
--------------------------------------------------------------------------------
1 | # cases where BitwiseOperatorsAchievement should not unlock
2 |
3 | # >> CASE
4 | 4 + 4
5 |
6 | # >> CASE
7 | 4 * 5
8 |
9 | # >> CASE
10 | 4 - 5
11 |
12 | # >> CASE
13 | 4 / 5
14 |
15 | # >> CASE
16 | 4 // 5
17 |
18 | # >> CASE
19 | 4 % 5
20 |
21 | # >> CASE
22 | 2 ** 3
23 |
24 | # >> CASE
25 | [4, 5] @ [4, 5]
26 |
27 | # >> CASE
28 | [4 + 5 - 4 * 5 / 4 // 5 % 5 ** 1] @ [4]
29 |
--------------------------------------------------------------------------------
/tests/samples/invalid/ClassAchievement.py:
--------------------------------------------------------------------------------
1 | # cases where ClassAchievement should not unlock
2 |
3 | # >> CASE
4 | class Foo:
5 | pass
6 |
7 | # >> CASE
8 | class Foo:
9 | pass
10 |
11 | Foo
12 |
13 | # >> CASE
14 | class Foo:
15 | pass
16 |
17 | f = Foo
18 | f()
19 |
20 | # >> CASE
21 | Foo()
22 |
23 | # >> CASE
24 | Foo
25 |
--------------------------------------------------------------------------------
/tests/samples/invalid/ComprehensionsAchievement.py:
--------------------------------------------------------------------------------
1 | # cases where ComprehensionsAchievement should not unlock
2 |
3 | # >> CASE
4 | for _ in range(10):
5 | pass
6 |
7 | # >> CASE
8 | set([4, 5, 6])
9 |
10 | # >> CASE
11 | {4, 5, 6}
12 |
--------------------------------------------------------------------------------
/tests/samples/invalid/ConditionalAchievement.py:
--------------------------------------------------------------------------------
1 | # cases where ConditionalAchievement should not unlock
2 |
3 | # >> CASE
4 | [i for i in range(10) if i % 2 == 0]
5 |
6 | # >> CASE
7 | pass
8 |
--------------------------------------------------------------------------------
/tests/samples/invalid/DictAchievement.py:
--------------------------------------------------------------------------------
1 | # cases where DictAchievement should not unlock
2 |
3 | # >> CASE
4 | {i: i for i in range(10)}
5 |
6 | # >> CASE
7 | {4, 5, 6}
8 |
--------------------------------------------------------------------------------
/tests/samples/invalid/FunctionAchievement.py:
--------------------------------------------------------------------------------
1 | # cases where FunctionAchievement should not unlock
2 |
3 | # >> CASE
4 | def test():
5 | pass
6 |
7 | # >> CASE
8 | def func():
9 | pass
10 |
11 | func
12 |
13 | # >> CASE
14 | def func():
15 | pass
16 |
17 | f = func
18 | f()
19 |
20 | # >> CASE
21 | func()
22 |
23 | # >> CASE
24 | func
25 |
--------------------------------------------------------------------------------
/tests/samples/invalid/HelloWorldAchievement.py:
--------------------------------------------------------------------------------
1 | # cases where HelloWorldAchievement should not unlock
2 |
3 | # >> CASE
4 | print(123)
5 |
6 | # >> CASE
7 | print('hello wrold')
8 |
9 | # >> CASE
10 | print('helol world')
11 |
12 | # >> CASE
13 | print('helloworld', end='')
14 |
--------------------------------------------------------------------------------
/tests/samples/invalid/LambdaAchievement.py:
--------------------------------------------------------------------------------
1 | # cases where LambdaAchievement should not unlock
2 |
3 | # >> CASE
4 | def test():
5 | return
6 |
7 | test()
8 |
--------------------------------------------------------------------------------
/tests/samples/invalid/ListAchievement.py:
--------------------------------------------------------------------------------
1 | # cases where ListAchievement should not unlock
2 |
3 | # >> CASE
4 | [i for i in range(10)]
5 |
6 | # >> CASE
7 | [i for i in range(10) if i % 2 == 0]
8 |
9 | # >> CASE
10 | x = 4, 5, 6
11 |
12 | # >> CASE
13 | x = (4, 5, 6)
14 |
15 | # >> CASE
16 | x = (i for i in range(10))
17 |
--------------------------------------------------------------------------------
/tests/samples/invalid/LoopsAchievement.py:
--------------------------------------------------------------------------------
1 | # cases where LoopsAchievement should not unlock
2 |
3 | # >> CASE
4 | [None for _ in range(4)]
5 |
6 | # >> CASE
7 | pass
8 |
--------------------------------------------------------------------------------
/tests/samples/invalid/MathOperatorsAchievement.py:
--------------------------------------------------------------------------------
1 | # cases where MathOperatorsAchievement should not unlock
2 |
3 | # >> CASE
4 | True and False
5 |
6 | # >> CASE
7 | True or False
8 |
9 | # >> CASE
10 | not True
11 |
12 | # >> CASE
13 | 4 << 1
14 |
15 | # >> CASE
16 | 4 >> 1
17 |
18 | # >> CASE
19 | 4 >> 1
20 |
21 | # >> CASE
22 | 4 & 5
23 |
24 | # >> CASE
25 | 4 | 5
26 |
27 | # >> CASE
28 | 4 ^ 5
29 |
30 | # >> CASE
31 | ~4
32 |
--------------------------------------------------------------------------------
/tests/samples/invalid/PassAchievement.py:
--------------------------------------------------------------------------------
1 | # cases where PassAchievement should not unlock
2 |
3 | # >> CASE
4 | for _ in range(4):
5 | x = 4
6 |
7 | # >> CASE
8 | def test():
9 | return
10 |
11 | # >> CASE
12 | class Test:
13 | value = 4
14 |
--------------------------------------------------------------------------------
/tests/samples/valid/AssignAchievement.py:
--------------------------------------------------------------------------------
1 | # cases where AssignAchievement should unlock
2 |
3 | # >> CASE
4 | x = 4
5 |
6 | # >> CASE
7 | x = [4, 5, 6]
8 |
--------------------------------------------------------------------------------
/tests/samples/valid/BitwiseOperatorsAchievement.py:
--------------------------------------------------------------------------------
1 | # cases where BitwiseOperatorsAchievement should unlock
2 |
3 | # >> CASE
4 | 4 << 1
5 |
6 | # >> CASE
7 | 4 >> 1
8 |
9 | # >> CASE
10 | 4 & 5
11 |
12 | # >> CASE
13 | 4 | 5
14 |
15 | # >> CASE
16 | 4 ^ 5
17 |
18 | # >> CASE
19 | ~4
20 |
21 | # >> CASE
22 | ~(1 << 1 >> 1 & 1 | 0 ^ 1)
23 |
--------------------------------------------------------------------------------
/tests/samples/valid/ClassAchievement.py:
--------------------------------------------------------------------------------
1 | # cases where ClassAchievement should unlock
2 |
3 | # >> CASE
4 | class Foo:
5 | pass
6 |
7 | t = Foo()
8 |
9 | # >> CASE
10 | class Bar():
11 | pass
12 |
13 | Bar()
14 |
--------------------------------------------------------------------------------
/tests/samples/valid/ComprehensionsAchievement.py:
--------------------------------------------------------------------------------
1 | # cases where ComprehensionsAchievement should unlock
2 |
3 | # >> CASE
4 | [None for _ in range(10)]
5 |
6 | # >> CASE
7 | [None for i in range(10) if i % 2 == 0]
8 |
9 | # >> CASE
10 | {i: i for i in range(10)}
11 |
12 | # >> CASE
13 | {i: i for i in range(10) if i % 2 == 0}
14 |
15 | # >> CASE
16 | {i for i in range(10)}
17 |
18 | # >> CASE
19 | {i for i in range(10) if i % 2 == 0}
20 |
21 | # >> CASE
22 | (True for _ in range(10))
23 |
24 | # >> CASE
25 | (True for i in range(10) if i % 2 == 0)
26 |
--------------------------------------------------------------------------------
/tests/samples/valid/ConditionalAchievement.py:
--------------------------------------------------------------------------------
1 | # cases where ConditionalAchievement should unlock
2 |
3 | # >> CASE
4 | if True:
5 | pass
6 |
7 | # >> CASE
8 | if False:
9 | pass
10 | else:
11 | pass
12 |
13 | # >> CASE
14 | if False:
15 | pass
16 | elif True:
17 | pass
18 | else:
19 | pass
20 |
21 | # >> CASE
22 | 4 if True else 5
23 |
--------------------------------------------------------------------------------
/tests/samples/valid/DictAchievement.py:
--------------------------------------------------------------------------------
1 | # cases where DictAchievement should unlock
2 |
3 | # >> CASE
4 | {'name': 'John Doe', 'age': 24}
5 |
6 | # >> CASE
7 | {
8 | 'name': 'John Doe',
9 | 'age': 24
10 | }
11 |
12 | # >> CASE
13 | func({'name': 'John Doe', 'age': 24})
14 |
--------------------------------------------------------------------------------
/tests/samples/valid/FunctionAchievement.py:
--------------------------------------------------------------------------------
1 | # cases where FunctionAchievement should unlock
2 |
3 | # >> CASE
4 | def test():
5 | pass
6 |
7 | test()
8 |
9 | # >> CASE
10 | def func():
11 | pass
12 |
13 | func()
14 |
--------------------------------------------------------------------------------
/tests/samples/valid/HelloWorldAchievement.py:
--------------------------------------------------------------------------------
1 | # cases where HelloWorldAchievement should unlock
2 |
3 | # >> CASE
4 | print('hello world!')
5 |
6 | # >> CASE
7 | print('Hello world', end='')
8 |
9 | # >> CASE
10 | print('Hello World')
11 |
--------------------------------------------------------------------------------
/tests/samples/valid/LambdaAchievement.py:
--------------------------------------------------------------------------------
1 | # cases where LambdaAchievement should unlock
2 |
3 | # >> CASE
4 | lambda x: x
5 |
6 | # >> CASE
7 | f = lambda x: x
8 |
9 | # >> CASE
10 | [lambda x: x for x in range(4)]
11 |
12 | # >> CASE
13 | def test(f):
14 | return f()
15 |
16 | test(lambda _: None)
17 |
--------------------------------------------------------------------------------
/tests/samples/valid/ListAchievement.py:
--------------------------------------------------------------------------------
1 | # cases where ListAchievement should unlock
2 |
3 | # >> CASE
4 | [4, 5, 6]
5 |
6 | # >> CASE
7 | x = [4, 5, 6]
8 |
9 | # >> CASE
10 | x = [4, '5', None, False, 1 + 2]
11 |
12 | # >> CASE
13 | [x, y] = [4, 5]
14 |
--------------------------------------------------------------------------------
/tests/samples/valid/LoopsAchievement.py:
--------------------------------------------------------------------------------
1 | # cases where LoopsAchievement should unlock
2 |
3 | # >> CASE
4 | for _ in range(4):
5 | pass
6 |
7 | # >> CASE
8 | while False:
9 | pass
10 |
11 | # >> CASE
12 | for _ in range(4):
13 | pass
14 | else:
15 | pass
16 |
17 | # >> CASE
18 | while False:
19 | pass
20 | else:
21 | pass
22 |
--------------------------------------------------------------------------------
/tests/samples/valid/MathOperatorsAchievement.py:
--------------------------------------------------------------------------------
1 | # cases where MathOperatorsAchievement should unlock
2 |
3 | # >> CASE
4 | 4 + 4
5 |
6 | # >> CASE
7 | 4 * 5
8 |
9 | # >> CASE
10 | 4 - 5
11 |
12 | # >> CASE
13 | 4 / 5
14 |
15 | # >> CASE
16 | 4 // 5
17 |
18 | # >> CASE
19 | 4 % 5
20 |
21 | # >> CASE
22 | 2 ** 3
23 |
24 | # >> CASE
25 | [4, 5] @ [4, 5]
26 |
27 | # >> CASE
28 | [4 + 5 - 4 * 5 / 4 // 5 % 5 ** 1] @ [4]
29 |
--------------------------------------------------------------------------------
/tests/samples/valid/PassAchievement.py:
--------------------------------------------------------------------------------
1 | # cases where PassAchievement should unlock
2 |
3 | # >> CASE
4 | pass
5 |
6 | # >> CASE
7 | for _ in range(4):
8 | pass
9 |
10 | # >> CASE
11 | while False:
12 | pass
13 |
14 | # >> CASE
15 | def test():
16 | pass
17 |
18 | # >> CASE
19 | class Test:
20 | pass
21 |
--------------------------------------------------------------------------------
/tests/test.py:
--------------------------------------------------------------------------------
1 | import ast
2 | import itertools
3 | import os
4 | import unittest
5 |
6 | from dev_achievements.achievements import *
7 |
8 |
9 | # some constants
10 | SAMPLE_SRCS_DIR = 'tests/samples/{valid}/{ach}.py'
11 | VALIDITY_DIR_NAMES = ['valid', 'invalid']
12 | CASE_DELIM = '>> CASE'
13 |
14 | ALL_ACHIEVEMENTS = Achievement.subclasses()
15 |
16 |
17 | class TestAchievements(unittest.TestCase):
18 | """ Checks all Achievements using sample source code
19 |
20 | Unit tests are created and set dynamically, based on Achievements
21 | with written sample source code (in tests/samples directory).
22 | """
23 | pass
24 |
25 |
26 | def _build_table(src):
27 | """ Builds AST tree table from given source.
28 |
29 | Args:
30 | src (str): source code
31 |
32 | Returns:
33 | dict: table of ast.AST nodes in tree
34 | """
35 | table = {}
36 | tree = ast.parse(src)
37 | for node in ast.walk(tree):
38 | curr = table.get(node.__class__, [])
39 | table[node.__class__] = curr + [node]
40 | return table
41 |
42 |
43 | def _read_file(file_path):
44 | """ Reads returns file contents.
45 |
46 | Args:
47 | file_path (str): path of file
48 |
49 | Returns:
50 | str: file contents
51 | """
52 | lines = []
53 | with open(file_path, 'r') as file:
54 | lines = file.readlines()
55 | return ''.join(lines)
56 |
57 |
58 | def _parse_sample_src(src):
59 | """ Splits sample source string based on CASE_DELIM.
60 |
61 | Args:
62 | src (str): sample source code
63 |
64 | Returns:
65 | list: sample source split by test case
66 | """
67 | cases = src.split(CASE_DELIM)
68 | if len(cases) > 1:
69 | return cases[1:]
70 | return cases
71 |
72 |
73 | def create_achievement_test(ach, cases, exp_result):
74 | """ Creates a unit test out of the source cases.
75 |
76 | The Achievement's _check_condition method is called on each case
77 | given (as a subtest), and compared against the expected result.
78 |
79 | Args:
80 | ach (Achievement): Achievement for test
81 | cases (list): sample source cases
82 | exp_result (bool): expected _check_condition result
83 |
84 | Returns:
85 | function: created unit test
86 | """
87 | def _test(self):
88 | for i, case in enumerate(cases):
89 | nodes = _build_table(case)
90 | res = ach()._check_condition(nodes)
91 | with self.subTest(case=i):
92 | self.assertEqual(res, exp_result)
93 | return _test
94 |
95 |
96 | # dynamically create and set unit tests for TestAchievements
97 |
98 | # combinations of Achievement names and valid/invalid sources
99 | v_ach_combos = itertools.product(VALIDITY_DIR_NAMES, ALL_ACHIEVEMENTS)
100 | for valid, ach in v_ach_combos:
101 | file_path = SAMPLE_SRCS_DIR.format(valid=valid, ach=ach.__name__)
102 | if not os.path.isfile(file_path):
103 | continue
104 | # get all cases in source file
105 | sample_src = _read_file(file_path)
106 | cases = _parse_sample_src(sample_src)
107 | exp_result = valid == 'valid'
108 | # create and set unit test method
109 | test_method = create_achievement_test(ach, cases, exp_result)
110 | test_method.__name__ = f'test_{ach.__name__}_{valid}'
111 | setattr(TestAchievements, test_method.__name__, test_method)
112 |
--------------------------------------------------------------------------------