From 2a092a86865096303913f20bfe9d7eb839bce646 Mon Sep 17 00:00:00 2001 From: Julio Biason Date: Wed, 13 Oct 2021 13:18:13 -0300 Subject: [PATCH] Guidos Gorgeous Lasagna --- .gitignore | 3 + .../.exercism/config.json | 11 + .../.exercism/metadata.json | 1 + python/guidos-gorgeous-lasagna/HELP.md | 63 ++++ python/guidos-gorgeous-lasagna/HINTS.md | 45 +++ python/guidos-gorgeous-lasagna/README.md | 305 ++++++++++++++++++ python/guidos-gorgeous-lasagna/lasagna.py | 35 ++ .../guidos-gorgeous-lasagna/lasagna_test.py | 72 +++++ 8 files changed, 535 insertions(+) create mode 100644 python/guidos-gorgeous-lasagna/.exercism/config.json create mode 100644 python/guidos-gorgeous-lasagna/.exercism/metadata.json create mode 100644 python/guidos-gorgeous-lasagna/HELP.md create mode 100644 python/guidos-gorgeous-lasagna/HINTS.md create mode 100644 python/guidos-gorgeous-lasagna/README.md create mode 100644 python/guidos-gorgeous-lasagna/lasagna.py create mode 100644 python/guidos-gorgeous-lasagna/lasagna_test.py diff --git a/.gitignore b/.gitignore index e275917..82a9022 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,6 @@ _build # Haskell **/.stack-work/** + +# Python +**/__pycache__/** diff --git a/python/guidos-gorgeous-lasagna/.exercism/config.json b/python/guidos-gorgeous-lasagna/.exercism/config.json new file mode 100644 index 0000000..457426c --- /dev/null +++ b/python/guidos-gorgeous-lasagna/.exercism/config.json @@ -0,0 +1,11 @@ +{ + "blurb": "Learn the basics of Python by cooking Guido's Gorgeous Lasagna.", + "icon": "lasagna", + "authors": ["BethanyG"], + "files": { + "solution": ["lasagna.py"], + "test": ["lasagna_test.py"], + "exemplar": [".meta/exemplar.py"] + }, + "forked_from": ["csharp/lucians-luscious-lasagna", "ruby/lasagna"] +} diff --git a/python/guidos-gorgeous-lasagna/.exercism/metadata.json b/python/guidos-gorgeous-lasagna/.exercism/metadata.json new file mode 100644 index 0000000..a6e3725 --- /dev/null +++ b/python/guidos-gorgeous-lasagna/.exercism/metadata.json @@ -0,0 +1 @@ +{"track":"python","exercise":"guidos-gorgeous-lasagna","id":"1f7ccd608f874beca897f6d07fd6b53b","url":"https://exercism.org/tracks/python/exercises/guidos-gorgeous-lasagna","handle":"JBiason","is_requester":true,"auto_approve":false} \ No newline at end of file diff --git a/python/guidos-gorgeous-lasagna/HELP.md b/python/guidos-gorgeous-lasagna/HELP.md new file mode 100644 index 0000000..6dedba1 --- /dev/null +++ b/python/guidos-gorgeous-lasagna/HELP.md @@ -0,0 +1,63 @@ +# Help + +## Running the tests + +You can run the included tests by typing `pytest _test.py` on the command line from within the exercise's directory. + +You can also tell Python to run the pytest module on the command line from either within the exercise directory or with a path to the exercise directory. +`python -m pytest _test.py` from within the exercise directory. + +`python -m pytest /fully/qualified/path/to//` OR `python -m pytest realtive/path/to/` from a non-exercise directory. + +Many IDE's and code editors also have built-in support for using PyTest to run tests. + +- [Visual Studio Code](https://code.visualstudio.com/docs/python/testing) +- [PyCharm Professional & Community Editions](https://www.jetbrains.com/help/pycharm/pytest.html#create-pytest-test) +- [Atom](https://atom.io/packages/atom-python-test) +- [Spyder](https://www.spyder-ide.org/blog/introducing-unittest-plugin/) +- [Sublime](https://github.com/kaste/PyTest) +- [vim-test](https://github.com/vim-test/vim-test) + +See the [Python tests page](https://github.com/exercism/python/blob/main/docs/TESTS.md) for more information. + +### Common `pytest` options + +- `-v` : enable verbose output. +- `-x` : stop running tests on first failure. +- `--ff` : run failures from previous test before running other test cases. + +For other options, see `python -m pytest -h`. PyTest documentation can be found [here](https://docs.pytest.org/en/latest/getting-started.html). + +## Submitting your solution + +You can submit your solution using the `exercism submit lasagna.py` command. +This command will upload your solution to the Exercism website and print the solution page's URL. + +It's possible to submit an incomplete solution which allows you to: + +- See how others have completed the exercise +- Request help from a mentor + +## Need to get help? + +If you'd like help solving the exercise, check the following pages: + +- The [Python track's documentation](https://exercism.org/docs/tracks/python) +- [Exercism's support channel on gitter](https://gitter.im/exercism/support) +- The [Frequently Asked Questions](https://exercism.org/docs/using/faqs) + +Should those resources not suffice, you could submit your (incomplete) solution to request mentoring. + +Below are some resources for getting help if you run into trouble: + +- [The PSF](https://www.python.org) hosts Python downloads, documentation, and community resources. +- [Python Community on Discord](https://pythondiscord.com/) is a very helpful and active community. +- [#python on Libera.chat](https://www.python.org/community/irc/) this is where the cored developers for the language hang out and get work done. +- [Exercism on Gitter](https://gitter.im/exercism/home) join the Python room for Python-related questions or problems. +- [/r/learnpython/](https://www.reddit.com/r/learnpython/) is a subreddit designed for Python learners. +- [Python Community Forums](https://discuss.python.org/) +- [Pythontutor](http://pythontutor.com/) for stepping through small code snippets visually. + + +Additionally, [StackOverflow](http://stackoverflow.com/questions/tagged/python) is a good spot to search for your problem/question to see if it has been answered already. + If not - you can always [ask](https://stackoverflow.com/help/how-to-ask) or [answer](https://stackoverflow.com/help/how-to-answer) someone else's question. \ No newline at end of file diff --git a/python/guidos-gorgeous-lasagna/HINTS.md b/python/guidos-gorgeous-lasagna/HINTS.md new file mode 100644 index 0000000..41e3c66 --- /dev/null +++ b/python/guidos-gorgeous-lasagna/HINTS.md @@ -0,0 +1,45 @@ +# Hints + +## General + +- [The Python Tutorial][the python tutorial] can be a great introduction. +- [Numbers][numbers] in Python can be integers, floats, or complex. + +## 1. Define expected bake time in minutes + +- You need to [name][naming] a constant, and [assign][assignment] it an integer value. + +## 2. Calculate remaining bake time in minutes + +- You need to define a [function][defining functions] with a single parameter representing the time elapsed so far. +- Use the [mathematical operator for subtraction][numbers] to subtract values. +- This function should [return a value][return]. + +## 3. Calculate preparation time in minutes + +- You need to define a [function][defining functions] with a single parameter representing the number of layers. +- Use the [mathematical operator for multiplication][numbers] to multiply values. +- You could define an extra _constant_ for the time in minutes per layer rather than using a "magic number" in your code. +- This function should [return a value][return]. + +## 4. Calculate total elapsed cooking time (prep + bake) in minutes + +- You need to define a [function][defining-functions] with two parameters. +- Remember: you can always _call_ a function you've defined previously. +- You can use the [mathematical operator for addition][python as a calculator] to sum values. +- This function should [return a value][return]. + +## 5. Update the recipe with notes + +- Clearly [commenting][comments] and [documenting][docstrings] your code according to [PEP257][PEP257] is always recommended. + +[the python tutorial]: https://docs.python.org/3/tutorial/introduction.html +[numbers]: https://docs.python.org/3/tutorial/introduction.html#numbers +[naming]: https://realpython.com/python-variables/ +[assignment]: https://docs.python.org/3/reference/simple_stmts.html#grammar-token-assignment-stmt +[defining functions]: https://docs.python.org/3/tutorial/controlflow.html#defining-functions +[return]: https://docs.python.org/3/reference/simple_stmts.html#return +[python as a calculator]: https://docs.python.org/3/tutorial/introduction.html#using-python-as-a-calculator +[comments]: https://realpython.com/python-comments-guide/ +[docstrings]: https://docs.python.org/3/tutorial/controlflow.html#tut-docstrings +[PEP257]: https://www.python.org/dev/peps/pep-0257/ \ No newline at end of file diff --git a/python/guidos-gorgeous-lasagna/README.md b/python/guidos-gorgeous-lasagna/README.md new file mode 100644 index 0000000..19130ac --- /dev/null +++ b/python/guidos-gorgeous-lasagna/README.md @@ -0,0 +1,305 @@ +# Guido's Gorgeous Lasagna + +Welcome to Guido's Gorgeous Lasagna on Exercism's Python Track. +If you need help running the tests or submitting your code, check out `HELP.md`. +If you get stuck on the exercise, check out `HINTS.md`, but try and solve it without using those first :) + +## Introduction + +[Python][python docs] is a [dynamic and strongly][dynamic typing in python] typed [object-oriented][object oriented programming] programming language. +It employs both [duck typing][duck typing] and [gradual typing][gradual typing], via [type hints][type hints]. +Programming across paradigms is fully _supported_ -- but internally, [everything in Python is an object][everythings an object]. + +Python puts a strong emphasis on code readability and (_similar to Haskell_) uses [significant indentation][significant indentation] for function, method, and class definitions. +[The Zen of Python (PEP 20)][the zen of python] and [_What is Pythonic?_][what is pythonic] lay out additional philosophies. + +Objects are [assigned][assignment statements] to [names][naming and binding] via the _assignment operator_, `=`. +[Variables][variables] are written in [`snake_case`][snake case], and _constants_ usually in `SCREAMING_SNAKE_CASE`. + +A `name` (_variable or constant_) is not itself typed, and can be attached or re-attached to different objects over its lifetime. +For extended naming conventions and advice, see [PEP 8][pep8]. + +```python +>>> my_first_variable = 1 +>>> my_first_variable = "Last one, I promise" +>>> print(my_first_variable) +... +"Last one, I promise" +``` + +Constants are typically defined on a [module][module] or _global_ level, and although they _can_ be changed, they are _intended_ to be named only once. + +Their `SCREAMING_SNAKE_CASE` is a message to other developers that the assignment should not be altered: + +```python +# All caps signal that this is intended as a constant. +MY_FIRST_CONSTANT = 16 + +# Re-assignment will be allowed by the compiler & interpreter, +# but this is VERY strongly discouraged. +# Please don't do: MY_FIRST_CONSTANT = "Some other value" +``` + +The keyword `def` begins a [function definition][function definition]. +It must be followed by the function name and a parenthesized list of zero or more formal [parameters][parameters]. + Parameters can be of several different varieties, and can even [vary][more on functions] in length. +The `def` line is terminated with a colon. + +Statements for the _body_ of the function begin on the line following `def` and must be _indented in a block_. +There is no strict indentation amount (_either space **OR** [tab] characters are acceptable_), but [indentation][indentation] must be _consistent for all indented statements_. + +Functions explicitly return a value or object via the [`return`][return] keyword. + +```python +# Function definition on first line. +def add_two_numbers(number_one, number_two): + return number_one + number_two # Returns the sum of the numbers, and is indented by 2 spaces. + +>>> add_two_numbers(3, 4) +7 +``` + +Functions that do not have an explicit `return` expression will return [`None`][none]. + +```python +# This function will return None. +def add_two_numbers(number_one, number_two): + result = number_one + number_two + +>>> print(add_two_numbers(5, 7)) +None +``` + +Inconsistent indentation will raise an error: + +```python +# The return statement line does not match the first line indent. +>>> def add_three_numbers_misformatted(number_one, number_two, number_three): +... result = number_one + number_two + number_three # Indented by 4 spaces. +... return result #this was only indented by 3 spaces + File "", line 3 + return result + ^ +IndentationError: unindent does not match any outer indentation level +``` + +Functions are [_called_][calls] using their name followed by `()`. +The number of arguments passed in the parentheses must match the number of parameters in the original function definition unless [default arguments][default arguments] have been used: + +```python +>>> def number_to_the_power_of(number_one, number_two): + """Raise a number to an arbitrary power. + + :param number_one: int the base number. + :param number_two: int the power to raise the base number to. + :return: int - number raised to power of second number + + Takes number_one and raises it to the power of number_two, returning the result. + """ + + return number_one ** number_two +... + +>>> number_to_the_power_of(3,3) +27 +``` + +A mis-match between parameters and arguments will raise an error: + +```python +>>> number_to_the_power_of(4,) +... +Traceback (most recent call last): + File "", line 1, in +TypeError: number_to_the_power_of() missing 1 required positional argument: 'number_two' + +``` + +Adding a [default value][default arguments] for a parameter can defend against such errors: + +```python +>>> def number_to_the_power_of_default(number_one, number_two=2): + """Raise a number to an arbitrary power. + + :param number_one: int the base number. + :param number_two: int the power to raise the base number to. + :return: int - number raised to power of second number + + Takes number_one and raises it to the power of number_two, returning the result. + """ + + return number_one ** number_two + +... +>>> number_to_the_power_of_default(4) +16 +``` + +Methods bound to class names are invoked via dot notation (.), as are functions, constants, or global names imported as part of a module.: + +```python + +import string + +# This is a constant provided by the *string* module. +>>> print(string.ascii_lowercase) +"abcdefghijklmnopqrstuvwxyz" + +# This is a method call of the str *class*. +>>> start_text = "my silly sentence for examples." +>>> str.upper(start_text) +"MY SILLY SENTENCE FOR EXAMPLES." + +# This is a method call of an *instance* of the str *class*. +>>> start_text.upper() +"MY SILLY SENTENCE FOR EXAMPLES." +``` + +[Comments][comments] in Python start with a `#` that is not part of a string, and end at line termination. +Unlike many other programming languages, Python does not support multi-line comment marks. +Each line of a comment block must start with the `#` character. + +Comments are ignored by the interpreter: + +```python +# This is a single line comment. + +x = "foo" # This is an in-line comment. + +# This is a multi-line +# comment block over multiple lines -- +# these should be used sparingly. +``` + +The first statement of a function body can optionally be a [_docstring_][docstring], which concisely summarizes the function or object's purpose. +Docstrings are read by automated documentation tools and are returned by calling `.__doc__` on the function, method, or class name. +They can also function as [lightweight unit tests][doctests], which will be covered in a later exercise. +They are recommended for programs of any size where documentation is needed, and their conventions are laid out in [PEP257][PEP257]: + +```python +# An example on a user-defined function. +>>> def number_to_the_power_of(number_one, number_two): + """Raise a number to an arbitrary power. + + :param number_one: int the base number. + :param number_two: int the power to raise the base number to. + :return: int - number raised to power of second number + + Takes number_one and raises it to the power of number_two, returning the result. + """ + + return number_one ** number_two +... + +>>> print(number_to_the_power_of.__doc__) +Returns float or int. + + Takes number_one and raises it to the power of number_two, returning the result. + +# __doc__() for the built-in type: str. +>>> print(str.__doc__) +str(object='') -> str +str(bytes_or_buffer[, encoding[, errors]]) -> str + +Create a new string object from the given object. If encoding or +errors is specified, then the object must expose a data buffer +that will be decoded using the given encoding and error handler. +Otherwise, returns the result of object.__str__() (if defined) +or repr(object). +encoding defaults to sys.getdefaultencoding(). +errors defaults to 'strict'. +``` + +[PEP257]: https://www.python.org/dev/peps/pep-0257/ +[assignment statements]: https://docs.python.org/3/reference/simple_stmts.html#assignment-statements +[calls]: https://docs.python.org/3/reference/expressions.html#calls +[comments]: https://realpython.com/python-comments-guide/#python-commenting-basics +[default arguments]: https://docs.python.org/3/tutorial/controlflow.html#default-argument-values +[docstring]: https://docs.python.org/3/tutorial/controlflow.html#tut-docstrings +[doctests]: https://docs.python.org/3/library/doctest.html +[duck typing]: https://en.wikipedia.org/wiki/Duck_typing +[dynamic typing in python]: https://stackoverflow.com/questions/11328920/is-python-strongly-typed +[everythings an object]: https://docs.python.org/3/reference/datamodel.html +[function definition]: https://docs.python.org/3/tutorial/controlflow.html#defining-functions +[gradual typing]: https://en.wikipedia.org/wiki/Gradual_typing +[indentation]: https://docs.python.org/3/reference/lexical_analysis.html#indentation +[module]: https://docs.python.org/3/tutorial/modules.html +[more on functions]: https://docs.python.org/3/tutorial/controlflow.html#more-on-defining-functions +[naming and binding]: https://docs.python.org/3/reference/executionmodel.html#naming-and-binding +[none]: https://docs.python.org/3/library/constants.html +[object oriented programming]: https://en.wikipedia.org/wiki/Object-oriented_programming +[parameters]: https://docs.python.org/3/glossary.html#term-parameter +[pep8]: https://www.python.org/dev/peps/pep-0008/ +[python docs]: https://docs.python.org/3/ +[return]: https://docs.python.org/3/reference/simple_stmts.html#return +[significant indentation]: https://docs.python.org/3/reference/lexical_analysis.html#indentation +[snake case]: https://en.wikipedia.org/wiki/Snake_case +[the zen of python]: https://www.python.org/dev/peps/pep-0020/ +[type hints]: https://docs.python.org/3/library/typing.html +[variables]: https://realpython.com/python-variables/ +[what is pythonic]: https://blog.startifact.com/posts/older/what-is-pythonic.html + +## Instructions + +You're going to write some code to help you cook a gorgeous lasagna from your favorite cookbook. + +You have five tasks, all related to cooking your recipe. + +## 1. Define expected bake time in minutes + +Define an `EXPECTED_BAKE_TIME` constant that returns how many minutes the lasagna should bake in the oven. +According to your cookbook, the Lasagna should be in the oven for 40 minutes: + +```python +>>> lasagna.EXPECTED_BAKE_TIME +40 +``` + +## 2. Calculate remaining bake time in minutes + +Implement the `bake_time_remaining()` function that takes the actual minutes the lasagna has been in the oven as an argument and returns how many minutes the lasagna still needs to bake based on the `EXPECTED_BAKE_TIME`. + +```python +>>> bake_time_remaining(30) +10 +``` + +## 3. Calculate preparation time in minutes + +Implement the `preparation_time_in_minutes()` function that takes the number of layers you want to add to the lasagna as an argument and returns how many minutes you would spend making them. +Assume each layer takes 2 minutes to prepare. + +```python +>>> preparation_time_in_minutes(2) +4 +``` + +## 4. Calculate total elapsed cooking time (prep + bake) in minutes + +Implement the `elapsed_time_in_minutes()` function that has two parameters: `number_of_layers` (_the number of layers added to the lasagna_) and `elapsed_bake_time` (_the number of minutes the lasagna has been baking in the oven_). +This function should return the total number of minutes you've been cooking, or the sum of your preparation time and the time the lasagna has already spent baking in the oven. + +```python +>>> elapsed_time_in_minutes(3, 20) +26 +``` + +## 5. Update the recipe with notes + +Go back through the recipe, adding notes and documentation. + +```python +def elapsed_time_in_minutes(number_of_layers, elapsed_bake_time): + """ + Return elapsed cooking time. + + This function takes two numbers representing the number of layers & the time already spent + baking and calculates the total elapsed minutes spent cooking the lasagna. + """ +``` + +## Source + +### Created by + +- @BethanyG \ No newline at end of file diff --git a/python/guidos-gorgeous-lasagna/lasagna.py b/python/guidos-gorgeous-lasagna/lasagna.py new file mode 100644 index 0000000..5bd3d75 --- /dev/null +++ b/python/guidos-gorgeous-lasagna/lasagna.py @@ -0,0 +1,35 @@ +EXPECTED_BAKE_TIME = 40 +PREPARATION_TIME = 2 + + +def bake_time_remaining(elapsed: int) -> int: + """Calculate the bake time remaining. + + :param elapsed_bake_time: int baking time already elapsed. + :return: int remaining bake time derived from 'EXPECTED_BAKE_TIME'. + + Function that takes the actual minutes the lasagna has been in the oven as + an argument and returns how many minutes the lasagna still needs to bake + based on the `EXPECTED_BAKE_TIME`. + """ + return EXPECTED_BAKE_TIME - elapsed + + +def preparation_time_in_minutes(layers: int) -> int: + """Calculate the preparation time for the number of layers. + + :param layers: Number of layers. + :return: Preparation time in minutes. + """ + return PREPARATION_TIME * layers + + +def elapsed_time_in_minutes(layers: int, elapsed: int) -> int: + """Calculate total elapsed cooking time. + + :param layers: Number of layers. + :param elapsed: Elapsed time in minutes. + + :return: Total elapsed time. + """ + return preparation_time_in_minutes(layers) + elapsed diff --git a/python/guidos-gorgeous-lasagna/lasagna_test.py b/python/guidos-gorgeous-lasagna/lasagna_test.py new file mode 100644 index 0000000..3a10c9d --- /dev/null +++ b/python/guidos-gorgeous-lasagna/lasagna_test.py @@ -0,0 +1,72 @@ +import unittest +import pytest + + +try: + from lasagna import (EXPECTED_BAKE_TIME, + bake_time_remaining, + preparation_time_in_minutes, + elapsed_time_in_minutes) + + +except ImportError as import_fail: + message = import_fail.args[0].split('(', maxsplit=1) + item_name = import_fail.args[0].split()[3] + + if 'EXPECTED_BAKE_TIME' in message: + # pylint: disable=raise-missing-from + raise ImportError(f'We can not find or import the constant {item_name} in your' + " 'lasagna.py' file. Did you mis-name or forget to define it?") + else: + item_name = item_name[:-1] + "()'" + # pylint: disable=raise-missing-from + raise ImportError("In your 'lasagna.py' file, we can not find or import the" + f' function named {item_name}. Did you mis-name or forget to define it?') + + +class LasagnaTest(unittest.TestCase): + + @pytest.mark.task(taskno=1) + def test_EXPECTED_BAKE_TIME(self): + failure_msg = 'Expected a constant of EXPECTED_BAKE_TIME with a value of 40.' + self.assertEqual(EXPECTED_BAKE_TIME, 40, msg=failure_msg) + + @pytest.mark.task(taskno=2) + def test_bake_time_remaining(self): + input_data = [1, 2, 5, 10, 15, 23, 33, 39] + result_data = [40 - item for item in input_data] + + for variant, (time, result) in enumerate(zip(input_data, result_data), start=1): + with self.subTest(f'variation #{variant}', time=time, result=result): + failure_msg = f'Expected: {result} but the bake time remaining was calculated incorrectly.' + self.assertEqual(bake_time_remaining(time), result, msg=failure_msg) + + @pytest.mark.task(taskno=3) + def test_preparation_time_in_minutes(self): + input_data = [1, 2, 5, 8, 11, 15] + result_data = [item * 2 for item in input_data] + + for variant, (layers, time) in enumerate(zip(input_data, result_data), start=1): + with self.subTest(f'variation #{variant}', layers=layers, time=time): + failure_msg = f'Expected: {time} minutes, but preparation time was calculated incorrectly.' + self.assertEqual(preparation_time_in_minutes(layers), time, msg=failure_msg) + + @pytest.mark.task(taskno=4) + def test_elapsed_time_in_minutes(self): + layer_data = (1, 2, 5, 8, 11, 15) + time_data = (3, 7, 8, 4, 15, 20) + result_data = [prep * 2 + elapsed for prep, elapsed in zip(layer_data, time_data)] + + for variant, (layers, time, total_time) in enumerate(zip(layer_data, time_data, result_data), start=1): + with self.subTest(f'variation #{variant}', layers=layers, time=time, total_time=total_time): + failure_msg = f'Expected {time} minutes elapsed, but the timing was calculated incorrectly.' + self.assertEqual(elapsed_time_in_minutes(layers, time), total_time, msg=failure_msg) + + @pytest.mark.task(taskno=5) + def test_docstrings_were_written(self): + functions = [bake_time_remaining, preparation_time_in_minutes, elapsed_time_in_minutes] + + for variant, function in enumerate(functions, start=1): + with self.subTest(f'variation #{variant}', function=function): + failure_msg = f'Expected a docstring for `{function.__name__}`, but received `None` instead.' + self.assertIsNotNone(function.__doc__, msg=failure_msg)