Repository: oppia/oppia

Issue:
Replace JSRepl dependency for running Python code
The JSRepl project, which we depend on for client-side Python code execution, has been [deprecated](https://github.com/replit/jsrepl/blob/master/README.md). In addition, the Python interaction is currently failing on Chrome (but not Firefox) with the error

```
   sandbox.js:39 Uncaught RangeError: Maximum call stack size exceeded
```

We should probably move to a different client-side code execution library for Python. Possibilities include:
- http://www.skulpt.org/
- https://github.com/brython-dev/brython



Discussion:
Note to contributors: this involves creating a new interaction. Please see this [wiki page](https://github.com/oppia/oppia/wiki/Creating-Interactions) for instructions on how to do this. You can use the code in `extensions/interactions/CodeRepl` as an example.

I'd like to contribute. I've set up Oppia and signed the CLA. However, I'm new to open source and so would appreciate some guidance.

Hi @virajprabhu, nice to meet you! I've sent you an invite to the Oppia team; thanks for helping out!

I'm cc-ing @jacobdavis11, who generally takes care of extensions within Oppia -- he should be able to help if you have questions. (Speaking of which, do you have questions? If so, please let us know, and we can try to answer them!)

Thanks! I'm trying to understand the code structure and was exploring some of the Oppia workflows. I've been browsing the issues tracker for minor bugs or a suitable starter project, but haven't found any that are still open/not already assigned. Is there one you could suggest?

(Just to close the loop on this thread, I've suggested @virajprabhu take on #801 instead.)


Resolving patch:
diff --git a/extensions/dependencies/dependencies_config.py b/extensions/dependencies/dependencies_config.py
--- a/extensions/dependencies/dependencies_config.py
+++ b/extensions/dependencies/dependencies_config.py
@@ -24,7 +24,7 @@
 DEPENDENCIES_TO_ANGULAR_MODULES_DICT = {
     'codemirror': ['ui.codemirror'],
     'google_maps': ['ui.map'],
-    'jsrepl': [],
     'logic_proof': [],
     'midijs': [],
+    'skulpt': [],
 }
diff --git a/extensions/interactions/CodeRepl/CodeRepl.py b/extensions/interactions/CodeRepl/CodeRepl.py
--- a/extensions/interactions/CodeRepl/CodeRepl.py
+++ b/extensions/interactions/CodeRepl/CodeRepl.py
@@ -24,20 +24,20 @@ class CodeRepl(base.BaseInteraction):
     description = 'Allows learners to enter code and get it evaluated.'
     display_mode = base.DISPLAY_MODE_SUPPLEMENTAL
     is_trainable = True
-    _dependency_ids = ['jsrepl', 'codemirror']
+    _dependency_ids = ['skulpt', 'codemirror']
     answer_type = 'CodeEvaluation'
     instructions = 'Type code in the editor'
     needs_summary = True
 
-    # Language options 'lua' and 'scheme' have been removed for possible
-    # later re-release.
+    # Language options 'lua', 'scheme', 'coffeescript', 'javascript', and
+    # 'ruby' have been removed for possible later re-release.
     _customization_arg_specs = [{
         'name': 'language',
         'description': 'Programming language',
         'schema': {
             'type': 'unicode',
             'choices': [
-                'coffeescript', 'javascript', 'python', 'ruby',
+                'python',
             ]
         },
         'default_value': 'python'


Test patch:
diff --git a/core/domain/dependency_registry_test.py b/core/domain/dependency_registry_test.py
--- a/core/domain/dependency_registry_test.py
+++ b/core/domain/dependency_registry_test.py
@@ -30,8 +30,8 @@ class DependencyRegistryTests(test_utils.GenericTestBase):
 
     def test_get_dependency_html(self):
         self.assertIn(
-            'jsrepl',
-            dependency_registry.Registry.get_dependency_html('jsrepl'))
+            'skulpt',
+            dependency_registry.Registry.get_dependency_html('skulpt'))
 
         with self.assertRaises(IOError):
             dependency_registry.Registry.get_dependency_html('a')
@@ -43,11 +43,11 @@ class DependencyControllerTests(test_utils.GenericTestBase):
     def test_no_dependencies_in_non_exploration_pages(self):
         response = self.testapp.get(feconf.GALLERY_URL)
         self.assertEqual(response.status_int, 200)
-        response.mustcontain(no=['jsrepl'])
+        response.mustcontain(no=['skulpt'])
 
         response = self.testapp.get('/about')
         self.assertEqual(response.status_int, 200)
-        response.mustcontain(no=['jsrepl'])
+        response.mustcontain(no=['skulpt'])
 
     def test_dependencies_loaded_in_exploration_editor(self):
         exp_services.load_demo('0')
@@ -56,20 +56,20 @@ def test_dependencies_loaded_in_exploration_editor(self):
         self.signup(self.EDITOR_EMAIL, self.EDITOR_USERNAME)
         self.login(self.EDITOR_EMAIL)
 
-        # Verify that the exploration does not have a jsrepl dependency.
+        # Verify that the exploration does not have a Skulpt dependency.
         exploration = exp_services.get_exploration_by_id('0')
         interaction_ids = exploration.get_interaction_ids()
         all_dependency_ids = (
             interaction_registry.Registry.get_deduplicated_dependency_ids(
                 interaction_ids))
 
-        self.assertNotIn('jsrepl', all_dependency_ids)
+        self.assertNotIn('skulpt', all_dependency_ids)
 
-        # However, jsrepl is loaded in the exploration editor anyway, since
+        # However, Skulpt is loaded in the exploration editor anyway, since
         # all dependencies are loaded in the exploration editor.
         response = self.testapp.get('/create/0')
         self.assertEqual(response.status_int, 200)
-        response.mustcontain('jsrepl')
+        response.mustcontain('skulpt')
 
         self.logout()
 
@@ -78,33 +78,33 @@ def test_dependency_does_not_load_in_exploration_not_containing_it(self):
 
         exp_services.load_demo(EXP_ID)
 
-        # Verify that exploration 0 does not have a jsrepl dependency.
+        # Verify that exploration 0 does not have a Skulpt dependency.
         exploration = exp_services.get_exploration_by_id(EXP_ID)
         interaction_ids = exploration.get_interaction_ids()
         all_dependency_ids = (
             interaction_registry.Registry.get_deduplicated_dependency_ids(
                 interaction_ids))
-        self.assertNotIn('jsrepl', all_dependency_ids)
+        self.assertNotIn('skulpt', all_dependency_ids)
 
-        # Thus, jsrepl is not loaded in the exploration reader.
+        # Thus, Skulpt is not loaded in the exploration reader.
         response = self.testapp.get('/explore/%s' % EXP_ID)
         self.assertEqual(response.status_int, 200)
-        response.mustcontain(no=['jsrepl'])
+        response.mustcontain(no=['skulpt'])
 
     def test_dependency_loads_in_exploration_containing_it(self):
         EXP_ID = '1'
 
         exp_services.load_demo(EXP_ID)
 
-        # Verify that exploration 1 has a jsrepl dependency.
+        # Verify that exploration 1 has a Skulpt dependency.
         exploration = exp_services.get_exploration_by_id(EXP_ID)
         interaction_ids = exploration.get_interaction_ids()
         all_dependency_ids = (
             interaction_registry.Registry.get_deduplicated_dependency_ids(
                 interaction_ids))
-        self.assertIn('jsrepl', all_dependency_ids)
+        self.assertIn('skulpt', all_dependency_ids)
 
-        # Thus, jsrepl is loaded in the exploration reader.
+        # Thus, Skulpt is loaded in the exploration reader.
         response = self.testapp.get('/explore/%s' % EXP_ID)
         self.assertEqual(response.status_int, 200)
-        response.mustcontain('jsrepl')
+        response.mustcontain('skulpt')
diff --git a/core/domain/interaction_registry_test.py b/core/domain/interaction_registry_test.py
--- a/core/domain/interaction_registry_test.py
+++ b/core/domain/interaction_registry_test.py
@@ -30,17 +30,17 @@ def test_deduplication_of_dependency_ids(self):
         self.assertItemsEqual(
             interaction_registry.Registry.get_deduplicated_dependency_ids(
                 ['CodeRepl']),
-            ['jsrepl', 'codemirror'])
+            ['skulpt', 'codemirror'])
 
         self.assertItemsEqual(
             interaction_registry.Registry.get_deduplicated_dependency_ids(
                 ['CodeRepl', 'CodeRepl', 'CodeRepl']),
-            ['jsrepl', 'codemirror'])
+            ['skulpt', 'codemirror'])
 
         self.assertItemsEqual(
             interaction_registry.Registry.get_deduplicated_dependency_ids(
                 ['CodeRepl', 'LogicProof']),
-            ['jsrepl', 'codemirror', 'logic_proof'])
+            ['skulpt', 'codemirror', 'logic_proof'])
 
 
 class InteractionRegistryUnitTests(test_utils.GenericTestBase):