This repository was archived by the owner on May 31, 2020. It is now read-only.
-
-
Notifications
You must be signed in to change notification settings - Fork 418
How-To Update for docs. #819
Open
alexjdw
wants to merge
17
commits into
beeware:master
Choose a base branch
from
alexjdw:docs
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
17 commits
Select commit
Hold shift + click to select a range
fa9452f
Updated the how-tos with a bunch of stuff. Split out tests to its own…
alexjdw 9705679
Updated builtins.rst to include a better breakdown and more information.
alexjdw 1af71c9
Added introduction. Bit of cleanup as well.
alexjdw dfa868b
Added types.rst, a similar document to builtins.rst for working with …
alexjdw 38a2dcd
Flavor edits.
alexjdw 2d11319
Flavor edits v2.
alexjdw 253094f
Grammar.
alexjdw f0ec580
Finish my sentence
alexjdw 25ff0f2
Explain a bit more.
alexjdw f73628c
Added credit section.
alexjdw 708005e
Cleanup for contribute-tests
alexjdw 9a076c2
Cleanup/suggested changes in builtins.rst.
alexjdw 8eaa1e8
A bit more cleanup.
alexjdw dc4d547
Implement suggested changes to tour of the code.
alexjdw 1e8e57a
Added a bit about edge cases.
alexjdw 3a348e4
Added a bit about edge cases.
alexjdw 53053fd
Indentation
alexjdw File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,104 +1,136 @@ | ||
| Implementing Python Built-ins in JavaScript | ||
| =========================================== | ||
|
|
||
| Python's builtins give Python its trademark magical feel. If you're new to Python, read up on `<them here . | ||
| https://docs.python.org/3/library/functions.html>`_ | ||
|
|
||
| Most builtins have already been added to the project, but many are do not quite match the original | ||
| implementation exactly. Some may not handle certain types of inputs correctly. In addition, new builtins | ||
| may arrive with the latest and greatest Python version. This guide should serve as your field manual for | ||
| adding, updating, and navigating our implementations. | ||
|
|
||
| Process | ||
| ------- | ||
|
|
||
| The first thing to do when adding anything to Batavia is to play around a bit with it in the Python REPL. | ||
| Here's an example using ``list()``:: | ||
|
|
||
| >> list() | ||
| [] | ||
| >> list((1, 2, 3, 4)) | ||
| [1, 2, 3, 4] | ||
| >> list(4) | ||
| Traceback (most recent call last): | ||
| File "<stdin>", line 1, in <module> | ||
| TypeError: 'int' object is not iterable | ||
|
|
||
| Your goal is to find out how the function responds to various inputs and outputs. You may also | ||
| want to consult the offical documentation. Once you're a little familiar, you can start to add your | ||
| implementation to Batavia. | ||
|
|
||
| General Structure | ||
| ----------------- | ||
| ***************** | ||
|
|
||
| JavaScript versions of Python built-in functions can be found inside the ``batavia/builtins`` | ||
| directory in the Batavia code. Each built-in is placed inside its own file. | ||
| directory in the Batavia code. Each built-in is placed inside its own file. These builtins are | ||
| designed to be used only inside Batavia, as such they need to ensure they are being used in | ||
| a compatible manner. | ||
|
|
||
| .. code-block:: javascript | ||
| Each builtin function will receive arguments and keyword arguments and needs to handle them, | ||
| even if the result is throwing an error. Args should be an array, and kwargs should be a | ||
| JavaScript object. The first thing to do is check that both were passed in. | ||
|
|
||
| // Example: a function that accepts exactly one argument, and no keyword arguments | ||
| Let's take a look at an example using the ``list()`` builtin | ||
|
|
||
| .. code-block:: javascript | ||
|
|
||
| var <fn> = function(<args>, <kwargs>) { | ||
| // These builtins are designed to be used only inside Batavia, as such they need to ensure | ||
| // they are being used in a compatible manner. | ||
| // List accepts exactly one argument and no keyword arguments | ||
|
|
||
| // Batavia will only ever pass two arguments, args and kwargs. If more or fewer arguments | ||
| // are passed, then Batavia is being used in an incompatible way. | ||
| // See: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/arguments | ||
| var list = function(args, kwargs) { | ||
| // Always add this code. | ||
| if (arguments.length !== 2) { | ||
| throw new builtins.BataviaError.$pyclass("Batavia calling convention not used."); | ||
| } | ||
|
|
||
| // We are now checking if a kwargs object is passed. If it isn't kwargs will be null. Like | ||
| // obj.keys() in Python we can use Object.keys(obj) to get the keys of an object. If the | ||
| // function doesn't need support any kwargs we throw an error. | ||
| if (kwargs && Object.keys(kwargs).length > 0) { | ||
| throw new builtins.TypeError.$pyclass("<fn>() doesn't accept keyword arguments."); | ||
| } | ||
| This code ensures that the function can handle keyword arguments. Next, we need to validate the arguments are | ||
| correct. We can use JavaScript's ``Object.keys()`` to get the keys of an object. If we can't accept certain | ||
| args or kwargs, we will check the Python REPL to see what kind of error should be thrown and throw it. | ||
|
|
||
| // Now we can check if the function has the supported number of arguments. In this case a | ||
| // single required argument. | ||
| if (!args || args.length !== 1) { | ||
| throw new builtins.TypeError.$pyclass("<fn>() expected exactly 1 argument (" + args.length + " given)"); | ||
| } | ||
| .. tabs:: | ||
|
|
||
| // If the function only works with a specific object type, add a test | ||
| var obj = args[0]; | ||
| if (!types.isinstance(obj, types.<type>)) { | ||
| throw new builtins.TypeError.$pyclass( | ||
| "<fn>() expects a <type> (" + type_name(obj) + " given)"); | ||
| } | ||
| .. group-tab:: Python REPL | ||
|
|
||
| // actual code goes here | ||
| Javascript.Function.Stuff(); | ||
| } | ||
| .. code-block:: | ||
|
|
||
| >> list(a=1) | ||
| TypeError: list() doesn't accept keyword arguments. | ||
| >> list(1, 2, 3) | ||
| TypeError: list() expected exactly 1 argument (3 given) | ||
|
|
||
| <fn>.__doc__ = 'docstring from Python 3.4 goes here, for documentation' | ||
| .. group-tab:: Batavia Code | ||
|
|
||
| modules.export = <fn> | ||
| .. code-block:: javascript | ||
|
|
||
| if (kwargs && Object.keys(kwargs).length > 0) { | ||
| throw new exceptions.TypeError.$pyclass("<fn>() doesn't accept keyword arguments."); | ||
| } | ||
|
|
||
| Adding Tests | ||
| ------------ | ||
| if (!args || args.length !== 1) { | ||
| throw new exceptions.TypeError.$pyclass("<fn>() expected exactly 1 argument (" + args.length + " given)"); | ||
| } | ||
|
|
||
| The tests corresponding to Batavia implementations of built-ins are available inside | ||
| ``tests/builtins``. The Batavia test infrastructure includes a system to check the compatibility of | ||
| JavaScript implementation of Python with the reference CPython implementation. | ||
| // If the function only works with a specific object type, add a test | ||
| var obj = args[0]; | ||
| if (!types.isinstance(obj, types.<type>)) { | ||
| throw new exceptions.TypeError.$pyclass( | ||
| "<fn>() expects a <type> (" + type_name(obj) + " given)"); | ||
| } | ||
|
|
||
| It does this by running a test in the Python interpreter, and then running the same code using | ||
| Batavia in the Node.js JavaScript interpreter. It will compare the output in both cases to see if | ||
| they match. Furthermore the test suite will automatically test the builtin against values of all | ||
| data types to check if it gets the same response across both implementations. | ||
| Useful functions are ``types.isinstance``, which checks for a match against a Batavia type or list, | ||
| of Batavia types, ``types.isbataviainstance``, which checks for a match against any Batavia instance, | ||
| ``Object.keys(kwargs)`` for dealing with kwargs, and JavaScript's ``for in``, ``for of``, and | ||
| ``Array.forEach`` loops for iterating over the JavaScript arrays and objects. | ||
|
|
||
| In many cases these tests will not cover everything, so you can add your own. For an example look at | ||
| the ``test_bool.py`` file in ``tests/builtins``. You will see two classes with test cases, | ||
| ``BoolTests`` and ``BuiltinBoolFunctionTests``. Both derive from ``TranspileTestCase`` which | ||
| handles running your code in both interpreters and comparing outputs. | ||
| Note also the format for errors: ``throw new exceptions.<Error>.$pyclass``. | ||
|
|
||
| Let's look at some test code that checks if a the Batavia implementation of ``bool`` can handle a | ||
| bool-like class that implements ``__bool__``. | ||
| Returning a value | ||
| ***************** | ||
|
|
||
| .. code-block:: Python | ||
| Builtins implement Python functions and should return a Python object. | ||
| Batavia implementations of all Python types are located in ``/batavia/types.js``. | ||
| JavaScript imports use the ``require`` keyword and can be imported inline or at | ||
| the top of the file. Inline imports can be preferable in some cases. | ||
|
|
||
| def test_bool_like(self): | ||
| self.assertCodeExecution(""" | ||
| class BoolLike: | ||
| def __init__(self, val): | ||
| self.val = val | ||
| .. code-block:: javascript | ||
|
|
||
| def __bool__(self): | ||
| return self.val == 1 | ||
| print(bool(BoolLike(0))) | ||
| print(bool(BoolLike(1))) | ||
| """) | ||
| ... | ||
|
|
||
| The ``assertCodeExecution`` method will run the code provided to it in both implementations. This | ||
| code needs to generate some output so that the output can be compared, hence the need to print the | ||
| values. | ||
| Tuple = require('../types.js').Tuple | ||
| return new Tuple(my, results, here) | ||
| } | ||
|
|
||
| Documentation | ||
| ************* | ||
|
|
||
| Process | ||
| ---------- | ||
| Finally, add the docstring to the function object. In JavaScript, like in Python, functions | ||
| are first-class objects and can have additional properties. | ||
|
|
||
| .. code-block:: javascript | ||
|
|
||
| For a given function, run `functionname.__doc__` in the Python 3.4 repl | ||
| list.__doc__ = 'docstring from Python 3.x goes here, for documentation' | ||
|
|
||
| Copy the docstring into the doc | ||
| module.exports = list | ||
|
|
||
| Run the function in Python 3.4 | ||
| Tests | ||
| ***** | ||
|
|
||
| Take a guess at the implementation structure based on the other functions. | ||
| No implemenation for a project like this is complete without tests. Check out the other sections for | ||
| more details on test structure. Tests are located in ``/tests`` in a similar folder structure to the | ||
| core code, and most test files have already been created. Some things that should almost always be | ||
| tested: | ||
|
|
||
| Copy the style of the other implemented functions | ||
| * Write a test or three to ensure your function returns the correct output with some normal inputs. | ||
| * Think of a few weird inputs that could throw off your code (or future code). Test them. | ||
| * If you are throwing an error (excluding ``BataviaError``) anywhere, write a test that tries to throw it. | ||
| * If you accounted for an edge case (look for an ``if`` statement), test it. | ||
| * Check out the `official documentation <https://docs.python.org/3/>`_ for more edge cases. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,123 @@ | ||
| Implementing Tests in Batavia | ||
| ============================= | ||
|
|
||
| Basic Test Structure | ||
| -------------------- | ||
|
|
||
| Batavia's job is to run a browser-compatible Python compiler, which takes valid Python as input and runs it. | ||
| Therefore, tests should test that the output of the Batavia compiler matches the output of CPython:: | ||
|
|
||
| print('Hello') # Code to test | ||
| Hello # CPython output | ||
| Hello # Batavia output | ||
| # Outputs match. Test passes! | ||
|
|
||
| This test structure is simple and effective. It's used in almost every test we've written. | ||
|
|
||
| Adding Tests | ||
| ------------ | ||
|
|
||
| In many cases, existing tests will not cover everything. Feel free to add your own! | ||
|
|
||
| The tests corresponding to Batavia implementations of built-ins are available inside | ||
| ``tests/builtins``. The Batavia test infrastructure includes a system to check the compatibility of | ||
| JavaScript implementation of Python with the reference CPython implementation. | ||
|
|
||
| These tests all derive from ``TranspileTestCase``, which handles running your code in both interpreters | ||
| and comparing outputs. For an example, look at the ``test_bool.py`` file in ``tests/builtins``. You | ||
| will see two classes with test cases, ``BoolTests`` and ``BuiltinBoolFunctionTests``. Both derive | ||
| from ``TranspileTestCase``. | ||
|
|
||
| Let's look at some test code that checks if a the Batavia implementation of ``bool`` can handle a | ||
| bool-like class that implements ``__bool__``. | ||
|
|
||
| .. code-block:: Python | ||
|
|
||
| def test_bool_like(self): | ||
| self.assertCodeExecution(""" | ||
| class BoolLike: | ||
| def __init__(self, val): | ||
| self.val = val | ||
|
|
||
| def __bool__(self): | ||
| return self.val == 1 | ||
| print(bool(BoolLike(0))) | ||
| print(bool(BoolLike(1))) | ||
| """) | ||
|
|
||
| The ``assertCodeExecution`` method will run the code provided to it in both implementations. This | ||
| code needs to generate some output so that the output can be compared, hence the need to print the | ||
| values. **Code that is not being printed is not being tested.** | ||
|
|
||
| Finally, ``print()`` is an imperfect creature for tests. Some things in Python aren't guaranteed to | ||
| print out in the same order every time, like sets dictionaries. Tests should be structured to compensate, | ||
| for instance by converting to a sorted list. See also the output cleaners section below. | ||
|
|
||
| Template | ||
| -------- | ||
|
|
||
| .. code-block:: python | ||
|
|
||
| def test_<builtin>_<feature/case>(self): | ||
| # Valid Python code to be tested. | ||
| code = """ | ||
| print('>>> print(<code>)') | ||
| print(<code>) | ||
| """ | ||
| self.assertCodeExecution(code) | ||
|
|
||
| This code block provides a printout of the code being run as well as the output of the code, | ||
| which can be very useful for debugging in test cases where more than a few lines of code are being run. | ||
|
|
||
| Testing for Errors | ||
| ------------------ | ||
|
|
||
| Since we're testing the compiler, we need to ensure that errors for all of the builtins are thrown correctly. | ||
| We also want to ensure that we're not getting the wrong errors in our tests. Simply include a try/except | ||
| block in your test. | ||
|
|
||
| .. code-block:: python | ||
|
|
||
| def test_some_error(self): | ||
| code = """ | ||
| try: | ||
| code_that_raises_a_ValueError() | ||
| except ValueError as err: | ||
| print(err) | ||
| print("Test complete!") | ||
| """ | ||
| self.assertCodeExecution(code) | ||
|
|
||
| Remember to catch the specific error you want, and then print the error. Then, print a success message to | ||
| validate that your except block didn't crash as well. **Code that is not being printed is not being tested.** | ||
|
|
||
| Output Cleaners | ||
| --------------- | ||
|
|
||
| In some cases, the test output will vary. ``TranspileTestCase`` will automatically apply some common output | ||
| cleanup for you. Some cases will need more or less cleanup. If you run your Python code directly in the REPL, | ||
| and the output differs from the test case output, you may need to modify what cleanup steps are being run. | ||
|
|
||
| As such, ``assertCodeExecution`` accepts optional ``js_cleaner`` and ``py_cleaner`` objects. These can be provided by | ||
| the ``@transform`` decorator, located in ``tests/utils/output_cleaners.py``. Here's an example: | ||
|
|
||
| .. code-block:: python | ||
|
|
||
| @transform(float_exp=False) | ||
| def test_some_floats(self, js_cleaner, py_cleaner): # + Cleaner objects as arguments | ||
| code = ... | ||
| self.assertCodeExecution(code, js_cleaner=js_cleaner, py_cleaner=py_cleaner) # + Cleaner objects again | ||
|
|
||
| This code means that the output of floating-point numbers will not be normalized using a regex. Refer to other | ||
| test cases and the docstring for ``@transform`` for more examples. | ||
|
|
||
| Node/Python Crashes | ||
| ------------------- | ||
|
|
||
| If the CPython or JavaScript code crashes outright, UnitTest struggles. For instance, | ||
| ``confused END_FINALLY`` in the middle of your test output tends to mean that the JavaScript code threw an | ||
| uncaught exception, causing Node to stop. It's hard for UnitTest to pull the details out of this type of thing | ||
| since that error occurred in Node, not Python. | ||
|
|
||
| These types of errors will often appear above the test case as a crash report instead of in the usual section for the | ||
| output of your test's print() statements. Look there for clues. | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.