diff --git a/README.md b/README.md index b512745695..873c814134 100644 --- a/README.md +++ b/README.md @@ -19,9 +19,12 @@ A list of missing exercise can be found here: http://exercism.io/languages/pytho ### Conventions -- We use minimalistic stub files for all exercises (#272). +- We use minimalistic stub files for all exercises ([#272](https://github.com/exercism/python/issues/272)). - We use `unittest` (Python Standard Library) and no 3rd-party-framework. -- We use the parameter order `self.assertEqual(actual, expected)` (#440). +- We use the parameter order `self.assertEqual(actual, expected)` ([#440](https://github.com/exercism/python/issues/440)). +- We use context managers (`with self.assertRaises(\):`) for testing for exceptions ([#477](https://github.com/exercism/python/issues/477)). +- We use `assertIs(actual, True)` and `assertIs(actual, False)` rather than `assertTrue(actual)` or `assertFalse(actual)` ([#419](https://github.com/exercism/python/pull/419)). +- We use a comment string in the test file to reference the version of the exercise's `canonical-data.json` that tests were adapted from (wording can be found in: [#784](https://github.com/exercism/python/issues/784)). ### Testing @@ -50,24 +53,27 @@ This repo uses [flake8](http://flake8.readthedocs.org/en/latest/) with default s This repo uses `travis-ci` in the following configuration: [travis.yml](https://github.com/exercism/python/blob/master/.travis.yml) -It will check automatically the code style, the problem configuration and runns the unittests with all supported Python versions. +It will automatically check the code style, the problem configuration, and run the unittests with all supported Python versions. ## Pull Requests -We :heart: pull requests! +We :heart: pull requests! We even :sparkling_heart: them if they contain well written commit messages! Please write the first line of your commit message in the following style: -```exercise-name: Change some things``` +```exercise-name: Change some things``` Please try to follow the [The seven rules of a great Git commit message](https://chris.beams.io/posts/git-commit/#seven-rules) like to capitalize the subject line and use the imperative mood. If there are more details to add, put those into the body of the commit message. If you're interested, Tim Pope even has an [entire blog post](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html) on good commit messages. -If you're new to Git take a look at [this short guide](https://github.com/exercism/docs/blob/master/contributing-to-language-tracks/README.md#git-basics). +If you're new to Git, take a look at [this short guide](https://github.com/exercism/docs/blob/master/contributing-to-language-tracks/README.md#git-basics). ## Python icon The Python logo is an unregistered trademark. We are using a derived logo with the permission of the Python Software Foundation. + +## License +This repository uses the [MIT License](/LICENSE). diff --git a/config.json b/config.json index cbbdb94ea7..1ef0957810 100644 --- a/config.json +++ b/config.json @@ -9,9 +9,9 @@ "unlocked_by": null, "difficulty": 1, "topics": [ - "control-flow (if-else statements)", - "optional values", - "text formatting" + "conditionals", + "optional_values", + "text_formatting" ] }, { @@ -21,7 +21,7 @@ "unlocked_by": null, "difficulty": 1, "topics": [ - "control-flow (if-else statements)", + "conditionals", "booleans", "logic" ] @@ -46,8 +46,8 @@ "unlocked_by": null, "difficulty": 1, "topics": [ - "control-flow (loops)", - "control-flow (if-else statements)", + "loops", + "conditionals", "strings", "algorithms", "filtering", @@ -61,8 +61,8 @@ "unlocked_by": null, "difficulty": 1, "topics": [ - "control-flow (if-else statements)", - "control-flow (loops)", + "conditionals", + "loops", "maps", "strings", "logic", @@ -76,8 +76,8 @@ "unlocked_by": null, "difficulty": 1, "topics": [ - "control-flow (if-else statements)", - "control-flow (loops)", + "conditionals", + "loops", "sequences", "sets", "strings", @@ -96,8 +96,8 @@ "strings", "algorithms", "logic", - "pattern recognition", - "text formatting" + "pattern_recognition", + "text_formatting" ] }, { @@ -118,7 +118,9 @@ "unlocked_by": null, "difficulty": 1, "topics": [ - + "strings", + "equality", + "conditionals" ] }, { @@ -140,7 +142,11 @@ "unlocked_by": null, "difficulty": 1, "topics": [ - + "strings", + "conditionals", + "dates", + "parsing", + "pattern_recognition" ] }, { @@ -152,7 +158,7 @@ "topics": [ "strings", "logic", - "control-flow (loops)" + "loops" ] }, { @@ -367,7 +373,10 @@ "unlocked_by": null, "difficulty": 1, "topics": [ - + "bitwise_operations", + "integers", + "mathematics", + "type_conversion" ] }, { @@ -470,8 +479,7 @@ "topics": [ "lists", "searching", - "loops", - "iteration" + "loops" ] }, { @@ -503,7 +511,11 @@ "unlocked_by": null, "difficulty": 1, "topics": [ - + "algorithms", + "conditionals", + "classes", + "exception_handling", + "queues" ] }, { @@ -537,7 +549,10 @@ "unlocked_by": null, "difficulty": 1, "topics": [ - + "algorithms", + "cryptography", + "text_formatting", + "lists" ] }, { @@ -560,7 +575,13 @@ "unlocked_by": null, "difficulty": 1, "topics": [ - + "algorithms", + "conditionals", + "lists", + "loops", + "mathematics", + "matrices", + "sets" ] }, { @@ -582,7 +603,10 @@ "unlocked_by": null, "difficulty": 1, "topics": [ - + "algorithms", + "loops", + "mathematics", + "logic" ] }, { @@ -632,7 +656,10 @@ "unlocked_by": null, "difficulty": 1, "topics": [ - + "recursion", + "strings", + "stacks", + "parsing" ] }, { @@ -642,7 +669,11 @@ "unlocked_by": null, "difficulty": 1, "topics": [ - + "lists", + "parsing", + "transforming", + "loops", + "games" ] }, { @@ -696,7 +727,9 @@ "unlocked_by": null, "difficulty": 1, "topics": [ - + "lists", + "parsing", + "pattern_recognition" ] }, { @@ -874,7 +907,7 @@ "time", "mathematics", "logic", - "text formatting" + "text_formatting" ] }, { @@ -885,7 +918,7 @@ "difficulty": 4, "topics": [ "files", - "text formatting", + "text_formatting", "searching" ] }, @@ -933,7 +966,7 @@ "difficulty": 3, "topics": [ "strings", - "pattern matching" + "pattern_matching" ] }, { @@ -944,7 +977,7 @@ "difficulty": 3, "topics": [ "strings", - "pattern matching" + "pattern_matching" ] }, { @@ -954,9 +987,9 @@ "unlocked_by": null, "difficulty": 4, "topics": [ - "Control-flow (loops)", - "Arrays", - "Algorithms" + "loops", + "arrays", + "algorithms" ] }, { @@ -966,9 +999,9 @@ "unlocked_by": null, "difficulty": 1, "topics": [ - "control-flow (if-else statements)", - "optional values", - "text formatting" + "conditionals", + "optional_values", + "text_formatting" ] }, { @@ -978,7 +1011,7 @@ "unlocked_by": null, "difficulty": 1, "topics": [ - "control-flow (loops)" + "loops" ] }, { @@ -1043,6 +1076,28 @@ "conditionals" ] }, + { + "uuid": "4f5f890d-0db4-5480-f79a-21057c37871b15133dc", + "slug": "zipper", + "core": false, + "unlocked_by": null, + "difficulty": 8, + "topics": [ + "recursion", + "searching", + "trees" + ] + }, + { + "uuid": "3a2a947a-01b3-1e80-e32b-de1756fd88365adf12e", + "slug": "error-handling", + "core": false, + "unlocked_by": null, + "difficulty": 3, + "topics": [ + "exception_handling" + ] + }, { "uuid": "e7351e8e-d3ff-4621-b818-cd55cf05bffd", "slug": "accumulate", diff --git a/docs/TESTS.md b/docs/TESTS.md index 53ea9ddf3f..697282a158 100644 --- a/docs/TESTS.md +++ b/docs/TESTS.md @@ -4,14 +4,38 @@ We recommend you install [pytest](http://pytest.org/latest/) and [pytest-cache](http://pythonhosted.org/pytest-cache/). `pytest` is a testing tool that will give you more flexibility over running your unit tests. +If you want to install `pytest` for Python 2, then use `pip`. + ```bash pip install pytest pytest-cache ``` +If you instead want the version of `pytest` for Python 3, then use `pip3`. + +```bash +pip3 install pytest pytest-cache +``` If you get a `command not found` response from your system, you can find a tutorial on how to install `pip` [here](https://pip.pypa.io/en/stable/installing/). +**Note:** Whichever version of `pytest` you install last will be the default one used whenever `pytest` is executed, regardless of whether you have installed both versions. + +If you want to check what the default version of `pytest` being used is, run the following: + +```bash +pytest --version +``` + +If you have either version of `pytest` installed and you want to specifically run one of the versions, you can run that version by using `python` with the `-m` flag. + +For example, you could run the Python 3 version of pytest like so: + +```bash +$ python3 -m pytest --version +This is pytest version 3.2.3, imported from /usr/local/lib/python3.5/dist-packages/pytest.py +``` + If you choose not to install `pytest`, you can still run tests individually and skip the rest of this tutorial: @@ -29,7 +53,7 @@ an example here), place yourself in the directory where that exercise has been fetched and run: ```bash -py.test bob_test.py +pytest bob_test.py ``` **Note:** To run the tests you need to pass the name of the testsuite file to @@ -56,7 +80,7 @@ The above will run all the tests, whether they fail or not. If you'd rather stop the process and exit on the first failure, run: ```bash -py.test -x bob_test.py +pytest -x bob_test.py ``` ### Failed Tests First @@ -64,14 +88,14 @@ py.test -x bob_test.py `pytest-cache` remembers which tests failed, and can run those tests first. ```bash -py.test --ff bob_test.py +pytest --ff bob_test.py ``` ### Running All Tests for All Exercises ```bash cd exercism/python/ -py.test +pytest ``` ## Recommended Workflow @@ -80,7 +104,7 @@ We recommend you run this command while working on exercises. ```bash cd exercism/python/bob -py.test -x --ff bob_test.py +pytest -x --ff bob_test.py ``` ## PDB @@ -89,28 +113,28 @@ Will drop you into the python debugger when a test fails. To learn how to use pdb, check out the [documentation](https://docs.python.org/3/library/pdb.html#debugger-commands). -You may also be interested in watching [Clayton Parker's "So you think you can -pdb?" PyCon 2015 talk](https://www.youtube.com/watch?v=P0pIW5tJrRM) - ```bash -py.test --pdb bob_test.py +pytest --pdb bob_test.py ``` +You may also be interested in watching [Clayton Parker's "So you think you can +pdb?" PyCon 2015 talk](https://www.youtube.com/watch?v=P0pIW5tJrRM). + ## PEP8 PEP8 is the [Style Guide for Python Code](https://www.python.org/dev/peps/pep-0008/). If you would like to test for compliance to the style guide, install -[pytest-pep8](https://pypi.python.org/pypi/pytest-pep8) +[pytest-pep8](https://pypi.python.org/pypi/pytest-pep8). ```bash pip install pytest-pep8 ``` -and add the pep8 flag to your command +Then, just add the `--pep8` flag to your command ```bash -py.test --pep8 bob_test.py +pytest --pep8 bob_test.py ``` Read the [pytest documentation](http://pytest.org/latest/contents.html#toc) and diff --git a/exercises/allergies/allergies_test.py b/exercises/allergies/allergies_test.py index b7d62f5205..fcdd9c2f7c 100644 --- a/exercises/allergies/allergies_test.py +++ b/exercises/allergies/allergies_test.py @@ -12,18 +12,18 @@ class AllergiesTests(unittest.TestCase): def test_no_allergies_means_not_allergic(self): allergies = Allergies(0) - self.assertFalse(allergies.is_allergic_to('peanuts')) - self.assertFalse(allergies.is_allergic_to('cats')) - self.assertFalse(allergies.is_allergic_to('strawberries')) + self.assertIs(allergies.is_allergic_to('peanuts'), False) + self.assertIs(allergies.is_allergic_to('cats'), False) + self.assertIs(allergies.is_allergic_to('strawberries'), False) def test_is_allergic_to_eggs(self): - self.assertTrue(Allergies(1).is_allergic_to('eggs')) + self.assertIs(Allergies(1).is_allergic_to('eggs'), True) def test_allergic_to_eggs_in_addition_to_other_stuff(self): allergies = Allergies(5) - self.assertTrue(allergies.is_allergic_to('eggs')) - self.assertTrue(allergies.is_allergic_to('shellfish')) - self.assertFalse(allergies.is_allergic_to('strawberries')) + self.assertIs(allergies.is_allergic_to('eggs'), True) + self.assertIs(allergies.is_allergic_to('shellfish'), True) + self.assertIs(allergies.is_allergic_to('strawberries'), False) def test_no_allergies_at_all(self): self.assertEqual(Allergies(0).lst, []) diff --git a/exercises/allergies/example.py b/exercises/allergies/example.py index 2e4bb090de..a88674ca97 100644 --- a/exercises/allergies/example.py +++ b/exercises/allergies/example.py @@ -15,7 +15,7 @@ def __init__(self, score): self.score = score def is_allergic_to(self, allergy): - return self.score & 1 << self._allergies.index(allergy) + return bool(self.score & 1 << self._allergies.index(allergy)) @property def lst(self): diff --git a/exercises/binary-search/binary_search_test.py b/exercises/binary-search/binary_search_test.py index 96200e4ceb..c539d15f28 100644 --- a/exercises/binary-search/binary_search_test.py +++ b/exercises/binary-search/binary_search_test.py @@ -27,17 +27,20 @@ def test_finds_value_in_array_of_even_length(self): 5) def test_identifies_value_missing(self): - self.assertRaises(ValueError, binary_search, [1, 3, 4, 6, 8, 9, 11], 7) + with self.assertRaises(ValueError): + binary_search([1, 3, 4, 6, 8, 9, 11], 7) def test_value_smaller_than_arrays_minimum(self): - self.assertRaises(ValueError, binary_search, [1, 3, 4, 6, 8, 9, 11], 0) + with self.assertRaises(ValueError): + binary_search([1, 3, 4, 6, 8, 9, 11], 0) def test_value_larger_than_arrays_maximum(self): - self.assertRaises(ValueError, binary_search, [1, 3, 4, 6, 8, 9, 11], - 13) + with self.assertRaises(ValueError): + binary_search([1, 3, 4, 6, 8, 9, 11], 13) def test_empty_array(self): - self.assertRaises(ValueError, binary_search, [], 1) + with self.assertRaises(ValueError): + binary_search([], 1) if __name__ == '__main__': diff --git a/exercises/binary/binary_test.py b/exercises/binary/binary_test.py index 8254d7b549..28ce8fcdc0 100644 --- a/exercises/binary/binary_test.py +++ b/exercises/binary/binary_test.py @@ -32,16 +32,20 @@ def test_binary_10001101000_is_decimal_1128(self): self.assertEqual(parse_binary("10001101000"), 1128) def test_invalid_binary_text_only(self): - self.assertRaises(ValueError, parse_binary, "carrot") + with self.assertRaises(ValueError): + parse_binary("carrot") def test_invalid_binary_number_not_base2(self): - self.assertRaises(ValueError, parse_binary, "102011") + with self.assertRaises(ValueError): + parse_binary("102011") def test_invalid_binary_numbers_with_text(self): - self.assertRaises(ValueError, parse_binary, "10nope") + with self.assertRaises(ValueError): + parse_binary("10nope") def test_invalid_binary_text_with_numbers(self): - self.assertRaises(ValueError, parse_binary, "nope10") + with self.assertRaises(ValueError): + parse_binary("nope10") if __name__ == '__main__': diff --git a/exercises/book-store/example.py b/exercises/book-store/example.py index 4c7b69ae0d..4e3b598924 100644 --- a/exercises/book-store/example.py +++ b/exercises/book-store/example.py @@ -5,7 +5,7 @@ def _group_price(size): discounts = [0, .05, .1, .2, .25] if not (0 < size <= 5): raise ValueError('size must be in 1..' + len(discounts)) - return 8 * size * (1 - discounts[size - 1]) + return BOOK_PRICE * size * (1 - discounts[size - 1]) def calculate_total(books, price_so_far=0.): diff --git a/exercises/clock/clock.py b/exercises/clock/clock.py index 3044410f43..afd470e62d 100644 --- a/exercises/clock/clock.py +++ b/exercises/clock/clock.py @@ -1,3 +1,6 @@ class Clock(object): def __init__(self, hour, minute): pass + + def __add__(self, other): + pass diff --git a/exercises/clock/clock_test.py b/exercises/clock/clock_test.py index 444e30021d..07b23e75c8 100644 --- a/exercises/clock/clock_test.py +++ b/exercises/clock/clock_test.py @@ -64,52 +64,52 @@ def test_negative_hour_and_minutes_both_roll_over_continuously(self): # Test adding and subtracting minutes. def test_add_minutes(self): - self.assertEqual(str(Clock(10, 0).add(3)), '10:03') + self.assertEqual(str(Clock(10, 0) + 3), '10:03') def test_add_no_minutes(self): - self.assertEqual(str(Clock(6, 41).add(0)), '06:41') + self.assertEqual(str(Clock(6, 41) + 0), '06:41') def test_add_to_next_hour(self): - self.assertEqual(str(Clock(0, 45).add(40)), '01:25') + self.assertEqual(str(Clock(0, 45) + 40), '01:25') def test_add_more_than_one_hour(self): - self.assertEqual(str(Clock(10, 0).add(61)), '11:01') + self.assertEqual(str(Clock(10, 0) + 61), '11:01') def test_add_more_than_two_hours_with_carry(self): - self.assertEqual(str(Clock(0, 45).add(160)), '03:25') + self.assertEqual(str(Clock(0, 45) + 160), '03:25') def test_add_across_midnight(self): - self.assertEqual(str(Clock(23, 59).add(2)), '00:01') + self.assertEqual(str(Clock(23, 59) + 2), '00:01') def test_add_more_than_one_day(self): - self.assertEqual(str(Clock(5, 32).add(1500)), '06:32') + self.assertEqual(str(Clock(5, 32) + 1500), '06:32') def test_add_more_than_two_days(self): - self.assertEqual(str(Clock(1, 1).add(3500)), '11:21') + self.assertEqual(str(Clock(1, 1) + 3500), '11:21') def test_subtract_minutes(self): - self.assertEqual(str(Clock(10, 3).add(-3)), '10:00') + self.assertEqual(str(Clock(10, 3) + -3), '10:00') def test_subtract_to_previous_hour(self): - self.assertEqual(str(Clock(10, 3).add(-3)), '10:00') + self.assertEqual(str(Clock(10, 3) + -3), '10:00') def test_subtract_more_than_an_hour(self): - self.assertEqual(str(Clock(10, 3).add(-30)), '09:33') + self.assertEqual(str(Clock(10, 3) + -30), '09:33') def test_subtract_across_midnight(self): - self.assertEqual(str(Clock(10, 3).add(-70)), '08:53') + self.assertEqual(str(Clock(10, 3) + -70), '08:53') def test_subtract_more_than_two_hours(self): - self.assertEqual(str(Clock(0, 0).add(-160)), '21:20') + self.assertEqual(str(Clock(0, 0) + -160), '21:20') def test_subtract_more_than_two_hours_with_borrow(self): - self.assertEqual(str(Clock(6, 15).add(-160)), '03:35') + self.assertEqual(str(Clock(6, 15) + -160), '03:35') def test_subtract_more_than_one_day(self): - self.assertEqual(str(Clock(5, 32).add(-1500)), '04:32') + self.assertEqual(str(Clock(5, 32) + -1500), '04:32') def test_subtract_more_than_two_days(self): - self.assertEqual(str(Clock(2, 20).add(-3000)), '00:20') + self.assertEqual(str(Clock(2, 20) + -3000), '00:20') # Construct two separate clocks, set times, test if they are equal. def test_clocks_with_same_time(self): diff --git a/exercises/clock/example.py b/exercises/clock/example.py index 4b80c2f6a7..27c7e16b40 100644 --- a/exercises/clock/example.py +++ b/exercises/clock/example.py @@ -13,7 +13,7 @@ def __repr__(self): def __eq__(self, other): return repr(self) == repr(other) - def add(self, minutes): + def __add__(self, minutes): self.minute += minutes return self.cleanup() diff --git a/exercises/complex-numbers/complex_numbers_test.py b/exercises/complex-numbers/complex_numbers_test.py index 08b14238cf..cad5d9670d 100644 --- a/exercises/complex-numbers/complex_numbers_test.py +++ b/exercises/complex-numbers/complex_numbers_test.py @@ -55,7 +55,7 @@ def test_subtract_purely_real_numbers(self): self.assertEqual(first_input.sub(second_input).real, 2) self.assertEqual(first_input.sub(second_input).imaginary, 0) - def test_substract_numbers_with_real_and_imaginary_part(self): + def test_subtract_numbers_with_real_and_imaginary_part(self): first_input = ComplexNumber(1, 2) second_input = ComplexNumber(-2, -2) self.assertEqual(first_input.sub(second_input).real, 3) diff --git a/exercises/diffie-hellman/.meta/hints.md b/exercises/diffie-hellman/.meta/hints.md new file mode 100644 index 0000000000..c3f8575374 --- /dev/null +++ b/exercises/diffie-hellman/.meta/hints.md @@ -0,0 +1,16 @@ +## Should I use random or secrets? + +Python, as of version 3.6, includes two different random modules. + +The module called `random` is pseudo-random, meaning it does not generate +true randomness, but follows an algorithm that simulates randomness. +Since random numbers are generated through a known algorithm, they are not truly random. + +The `random` module is not correctly suited for cryptography and should not be used, +precisely because it is pseudo-random. + +For this reason, in version 3.6, Python introduced the `secrets` module, which generates +cryptographically strong random numbers that provide the greater security required for cryptography. + +Since this is only an exercise, `random` is fine to use, but note that **it would be +very insecure if actually used for cryptography.** diff --git a/exercises/diffie-hellman/README.md b/exercises/diffie-hellman/README.md index 0a793540a7..d4a5f867a0 100644 --- a/exercises/diffie-hellman/README.md +++ b/exercises/diffie-hellman/README.md @@ -37,9 +37,22 @@ Bob calculates The calculations produce the same result! Alice and Bob now share secret s. -## Notes +## Should I use random or secrets? -Python, as of version 3.6, includes two different random modules. The module called `random` is pseudo-random, meaning it does not generate true randomness, but follows and algorithm that simulates randomness. Since random numbers are generated through a known algorithm, they are not truly random. The `random` module is not correctly suited for cryptography and should not be used, because it is pseudo-random. In version 3.6, Python introduced the `secrets` module, which generates cryptographically strong random numbers that provide the greater security required for cryptography. Since this is only an exercise, `random` is fine to use, but note that it would be very insecure if actually used for cryptography. +Python, as of version 3.6, includes two different random modules. + +The module called `random` is pseudo-random, meaning it does not generate +true randomness, but follows an algorithm that simulates randomness. +Since random numbers are generated through a known algorithm, they are not truly random. + +The `random` module is not correctly suited for cryptography and should not be used, +precisely because it is pseudo-random. + +For this reason, in version 3.6, Python introduced the `secrets` module, which generates +cryptographically strong random numbers that provide the greater security required for cryptography. + +Since this is only an exercise, `random` is fine to use, but note that **it would be +very insecure if actually used for cryptography.** ### Submitting Exercises diff --git a/exercises/error-handling/README.md b/exercises/error-handling/README.md new file mode 100644 index 0000000000..94adb62aea --- /dev/null +++ b/exercises/error-handling/README.md @@ -0,0 +1,23 @@ +# Error Handling + +Implement various kinds of error handling and resource management. + +An important point of programming is how to handle errors and close +resources even if errors occur. + +This exercise requires you to handle various errors. Because error handling +is rather programming language specific you'll have to refer to the tests +for your track to see what's exactly required. + +### Submitting Exercises + +Note that, when trying to submit an exercise, make sure the solution is in the `exercism/python/` directory. + +For example, if you're submitting `bob.py` for the Bob exercise, the submit command would be something like `exercism submit /python/bob/bob.py`. + + +For more detailed information about running tests, code style and linting, +please see the [help page](http://exercism.io/languages/python). + +## Submitting Incomplete Solutions +It's possible to submit an incomplete solution so you can see how others have completed the exercise. diff --git a/exercises/error-handling/error_handling.py b/exercises/error-handling/error_handling.py new file mode 100644 index 0000000000..f34ccee1cf --- /dev/null +++ b/exercises/error-handling/error_handling.py @@ -0,0 +1,14 @@ +def handle_error_by_throwing_exception(): + pass + + +def handle_error_by_returning_none(input_data): + pass + + +def handle_error_by_returning_tuple(input_data): + pass + + +def filelike_objects_are_closed_on_exception(filelike_object): + pass diff --git a/exercises/error-handling/error_handling_test.py b/exercises/error-handling/error_handling_test.py new file mode 100644 index 0000000000..b29bbfa050 --- /dev/null +++ b/exercises/error-handling/error_handling_test.py @@ -0,0 +1,66 @@ +import unittest + +import error_handling as er + + +class FileLike(object): + def __init__(self): + self.is_open = False + self.was_open = False + self.did_something = False + + def open(self): + self.was_open = False + self.is_open = True + + def close(self): + self.is_open = False + self.was_open = True + + def __enter__(self): + self.open() + return self + + def __exit__(self, *args): + self.close() + + def do_something(self): + self.did_something = True + raise Exception() + + +class ErrorHandlingTest(unittest.TestCase): + def test_throw_exception(self): + with self.assertRaises(Exception): + er.handle_error_by_throwing_exception() + + def test_return_none(self): + self.assertEqual(er.handle_error_by_returning_none('1'), 1, + 'Result of valid input should not be None') + self.assertIsNone(er.handle_error_by_returning_none('a'), + 'Result of invalid input should be None') + + def test_return_tuple(self): + successful_result, result = er.handle_error_by_returning_tuple('1') + self.assertIs(successful_result, True, + 'Valid input should be successful') + self.assertEqual(result, 1, 'Result of valid input should not be None') + + failure_result, result = er.handle_error_by_returning_tuple('a') + self.assertIs(failure_result, False, + 'Invalid input should not be successful') + + def test_filelike_objects_are_closed_on_exception(self): + filelike_object = FileLike() + with self.assertRaises(Exception): + er.filelike_objects_are_closed_on_exception(filelike_object) + self.assertIs(filelike_object.is_open, False, + 'filelike_object should be closed') + self.assertIs(filelike_object.was_open, True, + 'filelike_object should have been opened') + self.assertIs(filelike_object.did_something, True, + 'filelike_object should call do_something()') + + +if __name__ == '__main__': + unittest.main() diff --git a/exercises/error-handling/example.py b/exercises/error-handling/example.py new file mode 100644 index 0000000000..8544114809 --- /dev/null +++ b/exercises/error-handling/example.py @@ -0,0 +1,21 @@ +def handle_error_by_throwing_exception(): + raise Exception() + + +def handle_error_by_returning_none(input_data): + try: + return int(input_data) + except ValueError: + return None + + +def handle_error_by_returning_tuple(input_data): + try: + return (True, int(input_data)) + except ValueError: + return (False, None) + + +def filelike_objects_are_closed_on_exception(filelike_object): + with filelike_object as fobj: + fobj.do_something() diff --git a/exercises/forth/forth.py b/exercises/forth/forth.py index f8362ec7f4..7cf4ff3b40 100644 --- a/exercises/forth/forth.py +++ b/exercises/forth/forth.py @@ -1,2 +1,6 @@ +class StackUnderflowError(Exception): + pass + + def evaluate(input_data): pass diff --git a/exercises/forth/forth_test.py b/exercises/forth/forth_test.py index 66a4ec819a..53fff3b091 100644 --- a/exercises/forth/forth_test.py +++ b/exercises/forth/forth_test.py @@ -1,9 +1,9 @@ import unittest -from example import evaluate, StackUnderflowError +from forth import evaluate, StackUnderflowError -# test cases adapted from `x-common//canonical-data.json` @ version: 1.2.0 +# Tests adapted from `problem-specifications//canonical-data.json` @ v1.2.0 class ForthAdditionTest(unittest.TestCase): diff --git a/exercises/leap/leap_test.py b/exercises/leap/leap_test.py index 1936a1a136..68cd970057 100644 --- a/exercises/leap/leap_test.py +++ b/exercises/leap/leap_test.py @@ -3,14 +3,14 @@ from leap import is_leap_year -# Tests adapted from `problem-specifications//canonical-data.json` @ v1.0.0 +# Tests adapted from `problem-specifications//canonical-data.json` @ v1.2.0 class YearTest(unittest.TestCase): def test_year_not_divisible_by_4(self): self.assertIs(is_leap_year(2015), False) def test_year_divisible_by_4_not_divisible_by_100(self): - self.assertIs(is_leap_year(2016), True) + self.assertIs(is_leap_year(1996), True) def test_year_divisible_by_100_not_divisible_by_400(self): self.assertIs(is_leap_year(2100), False) diff --git a/exercises/list-ops/example.py b/exercises/list-ops/example.py index e0cba98f39..ec5f2eb2d7 100644 --- a/exercises/list-ops/example.py +++ b/exercises/list-ops/example.py @@ -1,25 +1,21 @@ -def map_clone(function, xs): - return [function(elem) for elem in xs] +def append(xs, ys): + return concat([xs, ys]) -def length(xs): - return sum(1 for _ in xs) +def concat(lists): + return [elem for lst in lists for elem in lst] def filter_clone(function, xs): return [x for x in xs if function(x)] -def reverse(xs): - if not xs: - return [] - else: - return xs[::-1] +def length(xs): + return sum(1 for _ in xs) -def append(xs, y): - xs[len(xs):] = [y] - return xs +def map_clone(function, xs): + return [function(elem) for elem in xs] def foldl(function, xs, acc): @@ -36,20 +32,5 @@ def foldr(function, xs, acc): return function(xs[0], foldr(function, xs[1:], acc)) -def flat(xs): - out = [] - for item in xs: - if isinstance(item, list): - out.extend(flat(item)) - else: - out.append(item) - return out - - -def concat(xs, ys): - if not ys: - return xs - else: - for item in ys: - xs.append(item) - return xs +def reverse(xs): + return xs[::-1] diff --git a/exercises/list-ops/list_ops.py b/exercises/list-ops/list_ops.py index 9c03405352..33a2e6e1e1 100644 --- a/exercises/list-ops/list_ops.py +++ b/exercises/list-ops/list_ops.py @@ -1,34 +1,30 @@ -def map_clone(): +def append(xs, ys): pass -def length(): +def concat(lists): pass -def filter_clone(): +def filter_clone(function, xs): pass -def reverse(): +def length(xs): pass -def append(): +def map_clone(function, xs): pass -def foldl(): +def foldl(function, xs, acc): pass -def foldr(): +def foldr(function, xs, acc): pass -def flat(): - pass - - -def concat(): +def reverse(xs): pass diff --git a/exercises/list-ops/list_ops_test.py b/exercises/list-ops/list_ops_test.py index 14b11a2eeb..c96f6a599c 100644 --- a/exercises/list-ops/list_ops_test.py +++ b/exercises/list-ops/list_ops_test.py @@ -4,122 +4,91 @@ import list_ops +# Tests adapted from problem-specifications//canonical-data.json @ v2.0.0 + class ListOpsTest(unittest.TestCase): - # tests for map - def test_map_square(self): - self.assertEqual( - list_ops.map_clone(lambda x: x**2, - [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]), - [1, 4, 9, 16, 25, 36, 49, 64, 81, 100]) + # test for append + def test_append_empty_lists(self): + self.assertEqual(list_ops.append([], []), []) - def test_map_cube(self): - self.assertEqual( - list_ops.map_clone(lambda x: x**3, - [-1, 2, -3, 4, -5, 6, -7, 8, -9, 10]), - [-1, 8, -27, 64, -125, 216, -343, 512, -729, 1000]) + def test_append_empty_list_to_list(self): + self.assertEqual(list_ops.append([], [1, 2, 3, 4]), [1, 2, 3, 4]) - def test_map_absolute(self): - self.assertEqual( - list_ops.map_clone(lambda x: abs(x), - [-1, 2, -3, 4, -5, 6, -7, 8, -9, 10]), - [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) + def test_append_nonempty_lists(self): + self.assertEqual(list_ops.append([1, 2], [2, 3, 4, 5]), + [1, 2, 2, 3, 4, 5]) - def test_map_empty(self): - self.assertEqual(list_ops.map_clone(operator.index, []), []) + # tests for concat + def test_concat_empty_list(self): + self.assertEqual(list_ops.concat([]), []) - # tests for length - def test_pos_leng(self): - self.assertEqual( - list_ops.length([-1, 2, -3, 4, -5, 6, -7, 8, -9, 10]), 10) + def test_concat_list_of_lists(self): + self.assertEqual(list_ops.concat([[1, 2], [3], [], [4, 5, 6]]), + [1, 2, 3, 4, 5, 6]) - def test_empty_len(self): - self.assertEqual(list_ops.length([]), 0) + # tests for filter_clone + def test_filter_empty_list(self): + self.assertEqual(list_ops.filter_clone(lambda x: x % 2 == 1, []), []) - # tests for filter - def test_filter_odd(self): + def test_filter_nonempty_list(self): self.assertEqual( - list_ops.filter_clone(lambda x: x % 2 != 0, [1, 2, 3, 4, 5, 6]), + list_ops.filter_clone(lambda x: x % 2 == 1, [1, 2, 3, 4, 5]), [1, 3, 5]) - def test_filter_even(self): - self.assertEqual( - list_ops.filter_clone(lambda x: x % 2 == 0, [1, 2, 3, 4, 5, 6]), - [2, 4, 6]) - - # tests for reverse - def test_reverse_small(self): - self.assertEqual(list_ops.reverse([3, 2, 1]), [1, 2, 3]) - - def test_reverse_mixed_types(self): - self.assertEqual( - list_ops.reverse(["xyz", 4.0, "cat", 1]), [1, "cat", 4.0, "xyz"]) - - def test_reverse_empty(self): - self.assertEqual(list_ops.reverse([]), []) + # tests for length + def test_length_empty_list(self): + self.assertEqual(list_ops.length([]), 0) - # tests for append - def test_append_tuple(self): - self.assertEqual( - list_ops.append(["10", "python"], "hello"), - ["10", "python", "hello"]) + def test_length_nonempty_list(self): + self.assertEqual(list_ops.length([1, 2, 3, 4]), 4) - def test_append_range(self): - self.assertEqual( - list_ops.append([100], range(1000)), [100, range(1000)]) + # tests for map_clone + def test_map_empty_list(self): + self.assertEqual(list_ops.map_clone(lambda x: x + 1, []), []) - def test_append_to_empty(self): - self.assertEqual(list_ops.append([], 42), [42]) + def test_map_nonempty_list(self): + self.assertEqual(list_ops.map_clone(lambda x: x + 1, [1, 3, 5, 7]), + [2, 4, 6, 8]) # tests for foldl - def test_foldl_sum(self): - self.assertEqual( - list_ops.foldl(operator.add, [1, 2, 3, 4, 5, 6], 0), 21) + def test_foldl_empty_list(self): + self.assertEqual(list_ops.foldl(operator.mul, [], 2), 2) - def test_foldl_product(self): - self.assertEqual( - list_ops.foldl(operator.mul, [1, 2, 3, 4, 5, 6], 1), 720) + def test_foldl_nonempty_list_addition(self): + self.assertEqual(list_ops.foldl(operator.add, [1, 2, 3, 4], 5), 15) - def test_foldl_div(self): - self.assertEqual( - list_ops.foldl(operator.floordiv, [1, 2, 3, 4, 5, 6], 1), 0) - - def test_foldl_sub(self): - self.assertEqual(list_ops.foldl(operator.sub, [1, 2, 3, 4, 5], 0), -15) + def test_foldl_nonempty_list_floordiv(self): + self.assertEqual(list_ops.foldl(operator.floordiv, [2, 5], 5), 0) # tests for foldr - def test_foldr_sub(self): - self.assertEqual(list_ops.foldr(operator.sub, [1, 2, 3, 4, 5], 0), 3) + def test_foldr_empty_list(self): + self.assertEqual(list_ops.foldr(operator.mul, [], 2), 2) + + def test_foldr_nonempty_list_addition(self): + self.assertEqual(list_ops.foldr(operator.add, [1, 2, 3, 4], 5), 15) + + def test_foldr_nonempty_list_floordiv(self): + self.assertEqual(list_ops.foldr(operator.floordiv, [2, 5], 5), 2) + # additional test for foldr def test_foldr_add_str(self): self.assertEqual( list_ops.foldr(operator.add, ["e", "x", "e", "r", "c", "i", "s", "m"], "!"), "exercism!") - # tests for flatten - def test_flatten_nested(self): - self.assertEqual(list_ops.flat([[[1, 2], [3]], [[4]]]), [1, 2, 3, 4]) - - def test_flatten_once(self): - self.assertEqual(list_ops.flat([["x", "y", "z"]]), ["x", "y", "z"]) + # tests for reverse + def test_reverse_empty_list(self): + self.assertEqual(list_ops.reverse([]), []) - def test_flatten_empty(self): - self.assertEqual(list_ops.flat([]), []) + def test_reverse_nonempty_list(self): + self.assertEqual(list_ops.reverse([1, 3, 5, 7]), [7, 5, 3, 1]) - # tests for concat - def test_concat_two(self): - self.assertEqual( - list_ops.concat([1, 3, 5, 8], [9, 4, 5, 6]), - [1, 3, 5, 8, 9, 4, 5, 6]) - - def test_concat_nothing(self): + # additional test for reverse + def test_reverse_mixed_types(self): self.assertEqual( - list_ops.concat(['orange', 'apple', 'banana'], None), - ["orange", "apple", "banana"]) - - def test_concat_empty(self): - self.assertEqual(list_ops.concat([], []), []) + list_ops.reverse(["xyz", 4.0, "cat", 1]), [1, "cat", 4.0, "xyz"]) if __name__ == '__main__': diff --git a/exercises/markdown/example.py b/exercises/markdown/example.py index 7321ad6f34..dbdb77b06c 100644 --- a/exercises/markdown/example.py +++ b/exercises/markdown/example.py @@ -66,7 +66,7 @@ def parse_line(line, in_list): if not re.match(')?
  • _(.*)', res): + if list_match is None: res = re.sub('(.*)(
  • )(.*)(
  • )(.*)', r'\1\2

    \3

    \4\5', res) while check_bold(res): diff --git a/exercises/markdown/markdown_test.py b/exercises/markdown/markdown_test.py index 6042594ff3..1cea371896 100644 --- a/exercises/markdown/markdown_test.py +++ b/exercises/markdown/markdown_test.py @@ -2,7 +2,7 @@ from markdown import parse_markdown -# Tests adapted from `problem-specifications//canonical-data.json` @ v1.0.0 +# Tests adapted from `problem-specifications//canonical-data.json` @ v1.1.0 class TestMarkdown(unittest.TestCase): @@ -37,8 +37,8 @@ def test_h6(self): def test_unordered_lists(self): self.assertEqual(parse_markdown('* Item 1\n* Item 2'), - '
    • Item 1

    • ' - '
    • Item 2

    ') + '
    • Item 1
    • ' + '
    • Item 2
    ') def test_little_bit_of_everything(self): self.assertEqual(parse_markdown( diff --git a/exercises/meetup/meetup_test.py b/exercises/meetup/meetup_test.py index eb01a4bc32..4455bb8fbd 100644 --- a/exercises/meetup/meetup_test.py +++ b/exercises/meetup/meetup_test.py @@ -399,8 +399,8 @@ def test_fifth_monday_of_march_2015(self): meetup_day(2015, 3, 'Monday', '5th'), date(2015, 3, 30)) def test_nonexistent_fifth_monday_of_february_2015(self): - self.assertRaises(MeetupDayException, meetup_day, 2015, 2, 'Monday', - '5th') + with self.assertRaises(MeetupDayException): + meetup_day(2015, 2, 'Monday', '5th') if __name__ == '__main__': diff --git a/exercises/minesweeper/minesweeper_test.py b/exercises/minesweeper/minesweeper_test.py index f3518f3c67..542944bd04 100644 --- a/exercises/minesweeper/minesweeper_test.py +++ b/exercises/minesweeper/minesweeper_test.py @@ -141,19 +141,22 @@ def test_different_len(self): "|* |", "| |", "+-+"] - self.assertRaises(ValueError, board, inp) + with self.assertRaises(ValueError): + board(inp) def test_faulty_border(self): inp = ["+-----+", "* * |", "+-- --+"] - self.assertRaises(ValueError, board, inp) + with self.assertRaises(ValueError): + board(inp) def test_invalid_char(self): inp = ["+-----+", "|X * |", "+-----+"] - self.assertRaises(ValueError, board, inp) + with self.assertRaises(ValueError): + board(inp) if __name__ == '__main__': diff --git a/exercises/ocr-numbers/ocr_numbers_test.py b/exercises/ocr-numbers/ocr_numbers_test.py index 1fae0d1019..2be8935743 100644 --- a/exercises/ocr-numbers/ocr_numbers_test.py +++ b/exercises/ocr-numbers/ocr_numbers_test.py @@ -43,15 +43,17 @@ def test_unknown_char(self): " "]), '?') def test_too_short_row(self): - self.assertRaises(ValueError, number, [" ", - " _|", - " |", - " "]) + with self.assertRaises(ValueError): + number([" ", + " _|", + " |", + " "]) def test_insufficient_rows(self): - self.assertRaises(ValueError, number, [" ", - " _|", - " X|"]) + with self.assertRaises(ValueError): + number([" ", + " _|", + " X|"]) def test_grid0(self): self.assertEqual(grid('0'), [" _ ", @@ -114,7 +116,8 @@ def test_grid3186547290(self): ]) def test_invalid_grid(self): - self.assertRaises(ValueError, grid, '123a') + with self.assertRaises(ValueError): + grid('123a') if __name__ == '__main__': diff --git a/exercises/pangram/pangram_test.py b/exercises/pangram/pangram_test.py index 09465a0228..72028650a6 100644 --- a/exercises/pangram/pangram_test.py +++ b/exercises/pangram/pangram_test.py @@ -7,39 +7,48 @@ class PangramTests(unittest.TestCase): def test_sentence_empty(self): - self.assertFalse(is_pangram('')) + self.assertIs(is_pangram(''), False) def test_pangram_with_only_lower_case(self): - self.assertTrue( - is_pangram('the quick brown fox jumps over the lazy dog')) + self.assertIs( + is_pangram('the quick brown fox jumps over the lazy dog'), + True) def test_missing_character_x(self): - self.assertFalse( + self.assertIs( is_pangram('a quick movement of the enemy will ' - 'jeopardize five gunboats')) + 'jeopardize five gunboats'), + False) def test_another_missing_character_x(self): - self.assertFalse( - is_pangram('the quick brown fish jumps over the lazy dog')) + self.assertIs( + is_pangram('the quick brown fish jumps over the lazy dog'), + False) def test_pangram_with_underscores(self): - self.assertTrue( - is_pangram('the_quick_brown_fox_jumps_over_the_lazy_dog')) + self.assertIs( + is_pangram('the_quick_brown_fox_jumps_over_the_lazy_dog'), + True) def test_pangram_with_numbers(self): - self.assertTrue( - is_pangram('the 1 quick brown fox jumps over the 2 lazy dogs')) + self.assertIs( + is_pangram('the 1 quick brown fox jumps over the 2 lazy dogs'), + True) def test_missing_letters_replaced_by_numbers(self): - self.assertFalse( - is_pangram('7h3 qu1ck brown fox jumps ov3r 7h3 lazy dog')) + self.assertIs( + is_pangram('7h3 qu1ck brown fox jumps ov3r 7h3 lazy dog'), + False) def test_pangram_with_mixedcase_and_punctuation(self): - self.assertTrue(is_pangram('"Five quacking Zephyrs jolt my wax bed."')) + self.assertIs( + is_pangram('"Five quacking Zephyrs jolt my wax bed."'), + True) def test_upper_and_lower_case_versions_of_the_same_character(self): - self.assertFalse( - is_pangram('the quick brown fox jumped over the lazy FOX')) + self.assertIs( + is_pangram('the quick brown fox jumped over the lazy FOX'), + False) if __name__ == '__main__': diff --git a/exercises/point-mutations/point_mutations.py b/exercises/point-mutations/point_mutations.py index 928f7d890c..5c7af56182 100644 --- a/exercises/point-mutations/point_mutations.py +++ b/exercises/point-mutations/point_mutations.py @@ -1,2 +1,2 @@ -def hamming_distance(): +def hamming_distance(dna_strand_1, dna_strand_2): pass diff --git a/exercises/pythagorean-triplet/pythagorean_triplet_test.py b/exercises/pythagorean-triplet/pythagorean_triplet_test.py index 4d9b29d10d..3c46fa5be3 100644 --- a/exercises/pythagorean-triplet/pythagorean_triplet_test.py +++ b/exercises/pythagorean-triplet/pythagorean_triplet_test.py @@ -80,7 +80,8 @@ def test_is_triplet3(self): self.assertIs(is_triplet((924, 43, 925)), True) def test_odd_number(self): - self.assertRaises(ValueError, primitive_triplets, 5) + with self.assertRaises(ValueError): + primitive_triplets(5) if __name__ == '__main__': diff --git a/exercises/rotational-cipher/rotational_cipher_test.py b/exercises/rotational-cipher/rotational_cipher_test.py index 82e81a5cfd..88b0983958 100644 --- a/exercises/rotational-cipher/rotational_cipher_test.py +++ b/exercises/rotational-cipher/rotational_cipher_test.py @@ -3,18 +3,18 @@ import rotational_cipher -# test cases adapted from `x-common//canonical-data.json` @ version: 1.0.0 +# Tests adapted from `problem-specifications//canonical-data.json` @ v1.1.0 class RotationalCipher(unittest.TestCase): + def test_rotate_a_by_0(self): + self.assertEqual(rotational_cipher.rotate('a', 0), 'a') + def test_rotate_a_by_1(self): self.assertEqual(rotational_cipher.rotate('a', 1), 'b') def test_rotate_a_by_26(self): self.assertEqual(rotational_cipher.rotate('a', 26), 'a') - def test_rotate_a_by_0(self): - self.assertEqual(rotational_cipher.rotate('a', 0), 'a') - def test_rotate_m_by_13(self): self.assertEqual(rotational_cipher.rotate('m', 13), 'z') diff --git a/exercises/saddle-points/saddle_points_test.py b/exercises/saddle-points/saddle_points_test.py index 2579b05474..065b24de74 100644 --- a/exercises/saddle-points/saddle_points_test.py +++ b/exercises/saddle-points/saddle_points_test.py @@ -28,7 +28,8 @@ def test_empty_matrix(self): def test_irregular_matrix(self): inp = [[3, 2, 1], [0, 1], [2, 1, 0]] - self.assertRaises(ValueError, saddle_points, inp) + with self.assertRaises(ValueError): + saddle_points(inp) if __name__ == '__main__': diff --git a/exercises/simple-cipher/.meta/hints.md b/exercises/simple-cipher/.meta/hints.md new file mode 100644 index 0000000000..b2358ef5d3 --- /dev/null +++ b/exercises/simple-cipher/.meta/hints.md @@ -0,0 +1,16 @@ +## Should I use random or secrets? + +Python, as of version 3.6, includes two different random modules. + +The module called `random` is pseudo-random, meaning it does not generate +true randomness, but follows an algorithm that simulates randomness. +Since random numbers are generated through a known algorithm, they are not truly random. + +The `random` module is not correctly suited for cryptography and should not be used, +precisely because it is pseudo-random. + +For this reason, in version 3.6, Python introduced the `secrets` module, which generates +cryptographically strong random numbers that provide the greater security required for cryptography. + +Since this is only an exercise, `random` is fine to use, but note that **it would be +very insecure if actually used for cryptography.** diff --git a/exercises/simple-cipher/README.md b/exercises/simple-cipher/README.md index 83779a968d..d271150fa4 100644 --- a/exercises/simple-cipher/README.md +++ b/exercises/simple-cipher/README.md @@ -83,6 +83,23 @@ on Wikipedia][dh] for one of the first implementations of this scheme. [1]: https://upload.wikimedia.org/wikipedia/commons/thumb/4/4a/Caesar_cipher_left_shift_of_3.svg/320px-Caesar_cipher_left_shift_of_3.svg.png [dh]: http://en.wikipedia.org/wiki/Diffie%E2%80%93Hellman_key_exchange +## Should I use random or secrets? + +Python, as of version 3.6, includes two different random modules. + +The module called `random` is pseudo-random, meaning it does not generate +true randomness, but follows an algorithm that simulates randomness. +Since random numbers are generated through a known algorithm, they are not truly random. + +The `random` module is not correctly suited for cryptography and should not be used, +precisely because it is pseudo-random. + +For this reason, in version 3.6, Python introduced the `secrets` module, which generates +cryptographically strong random numbers that provide the greater security required for cryptography. + +Since this is only an exercise, `random` is fine to use, but note that **it would be +very insecure if actually used for cryptography.** + ### Submitting Exercises Note that, when trying to submit an exercise, make sure the solution is in the `exercism/python/` directory. diff --git a/exercises/simple-cipher/simple_cipher_test.py b/exercises/simple-cipher/simple_cipher_test.py index fb0d51f9a1..d61cbc717d 100644 --- a/exercises/simple-cipher/simple_cipher_test.py +++ b/exercises/simple-cipher/simple_cipher_test.py @@ -68,8 +68,10 @@ def test_cipher_random_key(self): 'All items in the key must be chars and lowercase!') def test_cipher_wrong_key(self): - self.assertRaises(ValueError, Cipher, 'a1cde') - self.assertRaises(ValueError, Cipher, 'aBcde') + with self.assertRaises(ValueError): + Cipher('a1cde') + with self.assertRaises(ValueError): + Cipher('aBcde') if __name__ == '__main__': diff --git a/exercises/sum-of-multiples/sum_of_multiples_test.py b/exercises/sum-of-multiples/sum_of_multiples_test.py index affc13195d..f1e20cfd27 100644 --- a/exercises/sum-of-multiples/sum_of_multiples_test.py +++ b/exercises/sum-of-multiples/sum_of_multiples_test.py @@ -12,7 +12,7 @@ from sum_of_multiples import sum_of_multiples -# test cases adapted from `x-common//canonical-data.json` @ version: 1.0.0 +# Tests adapted from `problem-specifications//canonical-data.json` @ v1.1.0 class SumOfMultiplesTest(unittest.TestCase): def test_multiples_of_3_or_5_up_to_1(self): @@ -21,6 +21,9 @@ def test_multiples_of_3_or_5_up_to_1(self): def test_multiples_of_3_or_5_up_to_4(self): self.assertEqual(sum_of_multiples(4, [3, 5]), 3) + def test_multiples_of_3_up_to_7(self): + self.assertEqual(sum_of_multiples(7, [3]), 9) + def test_multiples_of_3_or_5_up_to_10(self): self.assertEqual(sum_of_multiples(10, [3, 5]), 23) diff --git a/exercises/triangle/triangle_test.py b/exercises/triangle/triangle_test.py index 6566744f5c..38d0ba422f 100644 --- a/exercises/triangle/triangle_test.py +++ b/exercises/triangle/triangle_test.py @@ -34,19 +34,24 @@ def test_very_small_triangles_are_legal(self): self.assertEqual(Triangle(0.4, 0.6, 0.3).kind(), "scalene") def test_triangles_with_no_size_are_illegal(self): - self.assertRaises(TriangleError, Triangle, 0, 0, 0) + with self.assertRaises(TriangleError): + Triangle(0, 0, 0) def test_triangles_with_negative_sides_are_illegal(self): - self.assertRaises(TriangleError, Triangle, 3, 4, -5) + with self.assertRaises(TriangleError): + Triangle(3, 4, -5) def test_triangles_violating_triangle_inequality_are_illegal(self): - self.assertRaises(TriangleError, Triangle, 1, 1, 3) + with self.assertRaises(TriangleError): + Triangle(1, 1, 3) def test_triangles_violating_triangle_inequality_are_illegal_2(self): - self.assertRaises(TriangleError, Triangle, 2, 4, 2) + with self.assertRaises(TriangleError): + Triangle(2, 4, 2) def test_triangles_violating_triangle_inequality_are_illegal_3(self): - self.assertRaises(TriangleError, Triangle, 7, 3, 2) + with self.assertRaises(TriangleError): + Triangle(7, 3, 2) if __name__ == '__main__': diff --git a/exercises/twelve-days/example.py b/exercises/twelve-days/example.py index 029a142750..ccfc0c0c60 100644 --- a/exercises/twelve-days/example.py +++ b/exercises/twelve-days/example.py @@ -15,13 +15,13 @@ 'seventh', 'eighth', 'ninth', 'tenth', 'eleventh', 'twelfth'] -def verse(n): - gifts = GIFTS[-n:] +def verse(day_number): + gifts = GIFTS[-day_number:] if len(gifts) > 1: gifts[:-1] = [', '.join(gifts[:-1])] gifts = ', and '.join(gifts) return 'On the {} day of Christmas my true love gave to me, {}.\n'.format( - ORDINAL[n], gifts) + ORDINAL[day_number], gifts) def verses(start, end): diff --git a/exercises/twelve-days/twelve_days.py b/exercises/twelve-days/twelve_days.py index 0d17418086..90a02988cd 100644 --- a/exercises/twelve-days/twelve_days.py +++ b/exercises/twelve-days/twelve_days.py @@ -1,8 +1,8 @@ -def verse(): +def verse(day_number): pass -def verses(): +def verses(start, end): pass diff --git a/exercises/variable-length-quantity/example.py b/exercises/variable-length-quantity/example.py index a1d4bb6fa8..5a59cb7f24 100644 --- a/exercises/variable-length-quantity/example.py +++ b/exercises/variable-length-quantity/example.py @@ -3,32 +3,32 @@ def encode_single(n): - bytes = [n & SEVENBITSMASK] + bytes_ = [n & SEVENBITSMASK] n >>= 7 while n > 0: - bytes.append(n & SEVENBITSMASK | EIGHTBITMASK) + bytes_.append(n & SEVENBITSMASK | EIGHTBITMASK) n >>= 7 - return bytes[::-1] + return bytes_[::-1] def encode(numbers): return sum((encode_single(n) for n in numbers), []) -def decode(bytes): +def decode(bytes_): values = [] n = 0 - for i, byte in enumerate(bytes): + for i, byte in enumerate(bytes_): n <<= 7 n += (byte & SEVENBITSMASK) if byte & EIGHTBITMASK == 0: values.append(n) n = 0 - elif i == len(bytes) - 1: + elif i == len(bytes_) - 1: raise ValueError('incomplete byte sequence') return values diff --git a/exercises/variable-length-quantity/variable_length_quantity.py b/exercises/variable-length-quantity/variable_length_quantity.py index 2536c2e670..41bdcdc849 100644 --- a/exercises/variable-length-quantity/variable_length_quantity.py +++ b/exercises/variable-length-quantity/variable_length_quantity.py @@ -1,6 +1,6 @@ -def encode(): +def encode(numbers): pass -def decode(): +def decode(bytes_): pass diff --git a/exercises/wordy/wordy_test.py b/exercises/wordy/wordy_test.py index c35fe38529..588f190481 100644 --- a/exercises/wordy/wordy_test.py +++ b/exercises/wordy/wordy_test.py @@ -49,17 +49,20 @@ def test_divide_twice(self): calculate("What is -12000 divided by 25 divided by -30?"), 16) def test_invalid_operation(self): - self.assertRaises(ValueError, calculate, "What is 4 xor 7?") + with self.assertRaises(ValueError): + calculate("What is 4 xor 7?") def test_missing_operation(self): - self.assertRaises(ValueError, calculate, "What is 2 2 minus 3?") + with self.assertRaises(ValueError): + calculate("What is 2 2 minus 3?") def test_missing_number(self): - self.assertRaises(ValueError, calculate, - "What is 7 plus multiplied by -2?") + with self.assertRaises(ValueError): + calculate("What is 7 plus multiplied by -2?") def test_irrelevant_question(self): - self.assertRaises(ValueError, calculate, "Which is greater, 3 or 2?") + with self.assertRaises(ValueError): + calculate("Which is greater, 3 or 2?") if __name__ == '__main__': diff --git a/exercises/zipper/README.md b/exercises/zipper/README.md new file mode 100644 index 0000000000..d1241c229c --- /dev/null +++ b/exercises/zipper/README.md @@ -0,0 +1,43 @@ +# Zipper + +Creating a zipper for a binary tree. + +[Zippers](https://en.wikipedia.org/wiki/Zipper_%28data_structure%29) are +a purely functional way of navigating within a data structure and +manipulating it. They essentially contain a data structure and a +pointer into that data structure (called the focus). + +For example given a rose tree (where each node contains a value and a +list of child nodes) a zipper might support these operations: + +- `from_tree` (get a zipper out of a rose tree, the focus is on the root node) +- `to_tree` (get the rose tree out of the zipper) +- `value` (get the value of the focus node) +- `prev` (move the focus to the previous child of the same parent, + returns a new zipper) +- `next` (move the focus to the next child of the same parent, returns a + new zipper) +- `up` (move the focus to the parent, returns a new zipper) +- `set_value` (set the value of the focus node, returns a new zipper) +- `insert_before` (insert a new subtree before the focus node, it + becomes the `prev` of the focus node, returns a new zipper) +- `insert_after` (insert a new subtree after the focus node, it becomes + the `next` of the focus node, returns a new zipper) +- `delete` (removes the focus node and all subtrees, focus moves to the + `next` node if possible otherwise to the `prev` node if possible, + otherwise to the parent node, returns a new zipper) + +### Submitting Exercises + +Note that, when trying to submit an exercise, make sure the solution is in the `exercism/python/` directory. + +For example, if you're submitting `bob.py` for the Bob exercise, the submit command would be something like `exercism submit /python/bob/bob.py`. + + +For more detailed information about running tests, code style and linting, +please see the [help page](http://exercism.io/languages/python). + + +## Submitting Incomplete Solutions + +It's possible to submit an incomplete solution so you can see how others have completed the exercise. diff --git a/exercises/zipper/example.py b/exercises/zipper/example.py new file mode 100644 index 0000000000..a9003e9e21 --- /dev/null +++ b/exercises/zipper/example.py @@ -0,0 +1,41 @@ +class Zipper(object): + @staticmethod + def from_tree(tree): + return Zipper(dict(tree), []) + + def __init__(self, tree, ancestors): + self.tree = tree + self.ancestors = ancestors + + def value(self): + return self.tree['value'] + + def set_value(self, value): + self.tree['value'] = value + return self + + def left(self): + if self.tree['left'] is None: + return None + return Zipper(self.tree['left'], self.ancestors + [self.tree]) + + def set_left(self, tree): + self.tree['left'] = tree + return self + + def right(self): + if self.tree['right'] is None: + return None + return Zipper(self.tree['right'], self.ancestors + [self.tree]) + + def set_right(self, tree): + self.tree['right'] = tree + return self + + def up(self): + return Zipper(self.ancestors[-1], self.ancestors[:-1]) + + def to_tree(self): + if any(self.ancestors): + return self.ancestors[0] + return self.tree diff --git a/exercises/zipper/zipper.py b/exercises/zipper/zipper.py new file mode 100644 index 0000000000..14903a8a09 --- /dev/null +++ b/exercises/zipper/zipper.py @@ -0,0 +1,28 @@ +class Zipper(object): + @staticmethod + def from_tree(tree): + pass + + def value(self): + pass + + def set_value(self): + pass + + def left(self): + pass + + def set_left(self): + pass + + def right(self): + pass + + def set_right(self): + pass + + def up(self): + pass + + def to_tree(self): + pass diff --git a/exercises/zipper/zipper_test.py b/exercises/zipper/zipper_test.py new file mode 100644 index 0000000000..0e2a0a7ef7 --- /dev/null +++ b/exercises/zipper/zipper_test.py @@ -0,0 +1,82 @@ +import unittest + +from zipper import Zipper + +# Tests adapted from `problem-specifications//canonical-data.json` @ v1.0.0 + + +def bt(value, left, right): + return { + 'value': value, + 'left': left, + 'right': right + } + + +def leaf(value): + return bt(value, None, None) + + +EMPTY_TREE = None + + +def create_trees(): + t1 = bt(1, bt(2, EMPTY_TREE, leaf(3)), leaf(4)) + t2 = bt(1, bt(5, EMPTY_TREE, leaf(3)), leaf(4)) + t3 = bt(1, bt(2, leaf(5), leaf(3)), leaf(4)) + t4 = bt(1, leaf(2), leaf(4)) + return (t1, t2, t3, t4) + + +class ZipperTest(unittest.TestCase): + def test_data_is_retained(self): + t1, _, _, _ = create_trees() + zipper = Zipper.from_tree(t1) + tree = zipper.to_tree() + self.assertEqual(tree, t1) + + def test_left_and_right_value(self): + t1, _, _, _ = create_trees() + zipper = Zipper.from_tree(t1) + self.assertEqual(zipper.left().right().value(), 3) + + def test_dead_end(self): + t1, _, _, _ = create_trees() + zipper = Zipper.from_tree(t1) + self.assertIsNone(zipper.left().left()) + + def test_tree_from_deep_focus(self): + t1, _, _, _ = create_trees() + zipper = Zipper.from_tree(t1) + self.assertEqual(zipper.left().right().to_tree(), t1) + + def test_set_value(self): + t1, t2, _, _ = create_trees() + zipper = Zipper.from_tree(t1) + updatedZipper = zipper.left().set_value(5) + tree = updatedZipper.to_tree() + self.assertEqual(tree, t2) + + def test_set_left_with_value(self): + t1, _, t3, _ = create_trees() + zipper = Zipper.from_tree(t1) + updatedZipper = zipper.left().set_left(leaf(5)) + tree = updatedZipper.to_tree() + self.assertEqual(tree, t3) + + def test_set_right_to_none(self): + t1, _, _, t4 = create_trees() + zipper = Zipper.from_tree(t1) + updatedZipper = zipper.left().set_right(None) + tree = updatedZipper.to_tree() + self.assertEqual(tree, t4) + + def test_different_paths_to_same_zipper(self): + t1, _, _, _ = create_trees() + zipper = Zipper.from_tree(t1) + self.assertEqual(zipper.left().up().right().to_tree(), + zipper.right().to_tree()) + + +if __name__ == '__main__': + unittest.main()