diff --git a/python/README.md b/python/README.md index 691f7eb4..01486a22 100644 --- a/python/README.md +++ b/python/README.md @@ -38,3 +38,4 @@ - [saddle-points](./saddle-points/README.md) - [leap](./leap/README.md) - [mecha-munch-management](./mecha-munch-management/README.md) +- [plane-tickets](./plane-tickets/README.md) diff --git a/python/plane-tickets/.coverage b/python/plane-tickets/.coverage new file mode 100644 index 00000000..824ef023 Binary files /dev/null and b/python/plane-tickets/.coverage differ diff --git a/python/plane-tickets/.coverage.xml b/python/plane-tickets/.coverage.xml new file mode 100644 index 00000000..b4c28f03 --- /dev/null +++ b/python/plane-tickets/.coverage.xml @@ -0,0 +1,55 @@ + + + + + + /home/vpayno/git_vpayno/exercism-workspace/python/plane-tickets + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/python/plane-tickets/.coveragerc b/python/plane-tickets/.coveragerc new file mode 100644 index 00000000..883531db --- /dev/null +++ b/python/plane-tickets/.coveragerc @@ -0,0 +1,2 @@ +[run] +omit = __init__.py, *_test.py diff --git a/python/plane-tickets/.exercism/config.json b/python/plane-tickets/.exercism/config.json new file mode 100644 index 00000000..12228348 --- /dev/null +++ b/python/plane-tickets/.exercism/config.json @@ -0,0 +1,23 @@ +{ + "authors": [ + "J08K" + ], + "contributors": [ + "BethanyG", + "kytrinyx", + "meatball133" + ], + "files": { + "solution": [ + "generators.py" + ], + "test": [ + "generators_test.py" + ], + "exemplar": [ + ".meta/exemplar.py" + ] + }, + "icon": "new-passport", + "blurb": "Learn about generators by assigning seats to passengers on Anaconda Airlines." +} diff --git a/python/plane-tickets/.exercism/metadata.json b/python/plane-tickets/.exercism/metadata.json new file mode 100644 index 00000000..9745fcd4 --- /dev/null +++ b/python/plane-tickets/.exercism/metadata.json @@ -0,0 +1 @@ +{"track":"python","exercise":"plane-tickets","id":"64bebba2c2354807b821de9b93c9cfab","url":"https://exercism.org/tracks/python/exercises/plane-tickets","handle":"vpayno","is_requester":true,"auto_approve":false} \ No newline at end of file diff --git a/python/plane-tickets/.pylintrc b/python/plane-tickets/.pylintrc new file mode 120000 index 00000000..30b33b52 --- /dev/null +++ b/python/plane-tickets/.pylintrc @@ -0,0 +1 @@ +../.pylintrc \ No newline at end of file diff --git a/python/plane-tickets/HELP.md b/python/plane-tickets/HELP.md new file mode 100644 index 00000000..3ed49f32 --- /dev/null +++ b/python/plane-tickets/HELP.md @@ -0,0 +1,130 @@ +# Help + +## Running the tests + +We use [pytest][pytest: Getting Started Guide] as our website test runner. +You will need to install `pytest` on your development machine if you want to run tests for the Python track locally. +You should also install the following `pytest` plugins: + +- [pytest-cache][pytest-cache] +- [pytest-subtests][pytest-subtests] + +Extended information can be found in our website [Python testing guide][Python track tests page]. + + +### Running Tests + +To run the included tests, navigate to the folder where the exercise is stored using `cd` in your terminal (_replace `{exercise-folder-location}` below with your path_). +Test files usually end in `_test.py`, and are the same tests that run on the website when a solution is uploaded. + +Linux/MacOS +```bash +$ cd {path/to/exercise-folder-location} +``` + +Windows +```powershell +PS C:\Users\foobar> cd {path\to\exercise-folder-location} +``` + +
+ +Next, run the `pytest` command in your terminal, replacing `{exercise_test.py}` with the name of the test file: + +Linux/MacOS +```bash +$ python3 -m pytest -o markers=task {exercise_test.py} +==================== 7 passed in 0.08s ==================== +``` + +Windows +```powershell +PS C:\Users\foobar> py -m pytest -o markers=task {exercise_test.py} +==================== 7 passed in 0.08s ==================== +``` + + +### Common options +- `-o` : override default `pytest.ini` (_you can use this to avoid marker warnings_) +- `-v` : enable verbose output. +- `-x` : stop running tests on first failure. +- `--ff` : run failures from previous test before running other test cases. + +For additional options, use `python3 -m pytest -h` or `py -m pytest -h`. + + +### Fixing warnings + +If you do not use `pytest -o markers=task` when invoking `pytest`, you might receive a `PytestUnknownMarkWarning` for tests that use our new syntax: + +```bash +PytestUnknownMarkWarning: Unknown pytest.mark.task - is this a typo? You can register custom marks to avoid this warning - for details, see https://docs.pytest.org/en/stable/mark.html +``` + +To avoid typing `pytest -o markers=task` for every test you run, you can use a `pytest.ini` configuration file. +We have made one that can be downloaded from the top level of the Python track directory: [pytest.ini][pytest.ini]. + +You can also create your own `pytest.ini` file with the following content: + +```ini +[pytest] +markers = + task: A concept exercise task. +``` + +Placing the `pytest.ini` file in the _root_ or _working_ directory for your Python track exercises will register the marks and stop the warnings. +More information on pytest marks can be found in the `pytest` documentation on [marking test functions][pytest: marking test functions with attributes] and the `pytest` documentation on [working with custom markers][pytest: working with custom markers]. + +Information on customizing pytest configurations can be found in the `pytest` documentation on [configuration file formats][pytest: configuration file formats]. + + +### Extending your IDE or Code Editor + +Many IDEs and code editors have built-in support for using `pytest` and other code quality tools. +Some community-sourced options can be found on our [Python track tools page][Python track tools page]. + +[Pytest: Getting Started Guide]: https://docs.pytest.org/en/latest/getting-started.html +[Python track tools page]: https://exercism.org/docs/tracks/python/tools +[Python track tests page]: https://exercism.org/docs/tracks/python/tests +[pytest-cache]:http://pythonhosted.org/pytest-cache/ +[pytest-subtests]:https://github.com/pytest-dev/pytest-subtests +[pytest.ini]: https://github.com/exercism/python/blob/main/pytest.ini +[pytest: configuration file formats]: https://docs.pytest.org/en/6.2.x/customize.html#configuration-file-formats +[pytest: marking test functions with attributes]: https://docs.pytest.org/en/6.2.x/mark.html#raising-errors-on-unknown-marks +[pytest: working with custom markers]: https://docs.pytest.org/en/6.2.x/example/markers.html#working-with-custom-markers + +## Submitting your solution + +You can submit your solution using the `exercism submit generators.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) +- The [Python track's programming category on the forum](https://forum.exercism.org/c/programming/python) +- [Exercism's programming category on the forum](https://forum.exercism.org/c/programming/5) +- 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. +- [The Exercism Community on Discord](https://exercism.org/r/discord) +- [Python Community on Discord](https://pythondiscord.com/) is a very helpful and active community. +- [/r/learnpython/](https://www.reddit.com/r/learnpython/) is a subreddit designed for Python learners. +- [#python on Libera.chat](https://www.python.org/community/irc/) this is where the core developers for the language hang out and get work done. +- [Python Community Forums](https://discuss.python.org/) +- [Free Code Camp Community Forums](https://forum.freecodecamp.org/) +- [CodeNewbie Community Help Tag](https://community.codenewbie.org/t/help) +- [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/plane-tickets/HINTS.md b/python/plane-tickets/HINTS.md new file mode 100644 index 00000000..e00e7f76 --- /dev/null +++ b/python/plane-tickets/HINTS.md @@ -0,0 +1,24 @@ +# Hints + +## 1. Generate seat letters + +- The returned value should be of _type_ `generator`. +- You can have a sequence of letters from `A` to `D` and cycle through them. +- Use `yield` to return the next letter. + +## 2. Generate seats + +- The returned value should be of _type_ `generator`. +- Row `13` should be skipped, so go from `12` to `14`. +- Keep in mind that the returned values should be ordered from low to high. `1A, 1B, 2A, ...` +- Here it might be good to reuse or call functions you have already defined. + +## 3. Assign seats to passengers + +- Make sure your seat numbers do not have any spaces in them. +- Here it might be good to reuse or call functions you have already defined. + +## 4. Ticket codes + +- You can use `len()` to get the length of a string. +- You can use `"" * ` to repeat a string. \ No newline at end of file diff --git a/python/plane-tickets/README.md b/python/plane-tickets/README.md new file mode 100644 index 00000000..7070fade --- /dev/null +++ b/python/plane-tickets/README.md @@ -0,0 +1,270 @@ +# Plane Tickets + +Welcome to Plane Tickets 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 + +A `generator` is a function or expression that returns a special type of [iterator][iterator] called [generator iterator][generator-iterator]. +`Generator-iterators` are [lazy][lazy iterator]: they do not store their `values` in memory, but _generate_ their values when needed. + +A generator function looks like any other function, but contains one or more [yield expressions][yield expression]. +Each `yield` will suspend code execution, saving the current execution state (_including all local variables and try-statements_). +When the generator resumes, it picks up state from the suspension - unlike regular functions which reset with every call. + + +## Constructing a generator + +Generators are constructed much like other looping or recursive functions, but require a [`yield` expression](#the-yield-expression), which we will explore in depth a bit later. + +An example is a function that returns the _squares_ from a given list of numbers. +As currently written, all input must be processed before any values can be returned: + +```python +>>> def squares(list_of_numbers): +... squares = [] +... for number in list_of_numbers: +... squares.append(number ** 2) +... return squares +``` + +You can convert that function into a generator like this: + +```python +>>> def squares_generator(list_of_numbers): +... for number in list_of_numbers: +... yield number ** 2 +``` + +The rationale behind this is that you use a generator when you do not need to produce all the values _at once_. +This saves memory and processing power, since only the value you are _currently working on_ is calculated. + + +## Using a generator + +Generators may be used in place of most `iterables` in Python. +This includes _functions_ or _objects_ that require an `iterable`/`iterator` as an argument. + +To use the `squares_generator()` generator: + +```python +>>> squared_numbers = squares_generator([1, 2, 3, 4]) + +>>> for square in squared_numbers: +... print(square) +... +1 +4 +9 +16 +``` + +Values within a `generator` can also be produced/accessed via the `next()` function. +`next()` calls the `__next__()` method of a generator-iterator object, "advancing" or evaluating the code up to its `yield` expression, which then "yields" or returns a value: + +```python +>>> squared_numbers = squares_generator([1, 2]) + +>>> next(squared_numbers) +1 +>>> next(squared_numbers) +4 +``` + +When a `generator-iterator` is fully consumed and has no more values to return, it throws a `StopIteration` error. + +```python +>>> next(squared_numbers) +Traceback (most recent call last): + File "", line 1, in +StopIteration +``` + + +~~~~exercism/note + +Generator-iterators are a special sub-set of [iterators][iterator]. +`Iterators` are the mechanism/protocol that enables looping over _iterables_. +Generator-iterators and the iterators returned by common Python [`iterables`][iterables] act very similarly, but there are some important differences to note: + +- They are _[lazily evaluated][lazy evaluation]_; iteration is _one-way_ and there is no "backing up" to a previous value. +- They are _consumed_ by iterating over the returned values; there is no resetting or saving in memory. +- They are not sortable and cannot be reversed. +- They are not sequence types, and _do not_ have `indexes`. + You cannot reference a previous or future value using addition or subtraction and you cannot use bracket (`[]`) notation or slicing. +- They cannot be used with the `len()` function, as they have no length. +- They can be _finite_ or _infinite_ - be careful when collecting all values from an _infinite_ `generator-iterator`! + +[iterator]: https://docs.python.org/3.11/glossary.html#term-iterator +[iterables]: https://wiki.python.org/moin/Iterator +[lazy evaluation]: https://en.wikipedia.org/wiki/Lazy_evaluation +~~~~ + + +## The yield expression + +The [yield expression][yield expression] is very similar to the `return` expression. +_Unlike_ the `return` expression, `yield` gives up values to the caller at a _specific point_, suspending evaluation/return of any additional values until they are requested. +When `yield` is evaluated, it pauses the execution of the enclosing function and returns any values of the function _at that point in time_. +The function then _stays in scope_, and when `__next__()` is called, execution resumes until `yield` is encountered again. + + +~~~~exercism/note +Using `yield` expressions is prohibited outside of functions. +~~~~ + +```python +>>> def infinite_sequence(): +... current_number = 0 +... while True: +... yield current_number +... current_number += 1 + +>>> lets_try = infinite_sequence() +>>> lets_try.__next__() +0 +>>> lets_try.__next__() +1 +``` + + +## Why Create a Generator? + +Generators are useful in a lot of applications. + +When working with a potentially large collection of values, you might not want to put all of them into memory. +A generator can be used to work on larger data piece-by-piece, saving memory and improving performance. + +Generators are also very helpful when a process or calculation is _complex_, _expensive_, or _infinite_: + +```python +>>> def infinite_sequence(): +... current_number = 0 +... while True: +... yield current_number +... current_number += 1 +``` + +Now whenever `__next__()` is called on the `infinite_sequence` object, it will return the _previous number_ + 1. + + +[generator-iterator]: https://docs.python.org/3.11/glossary.html#term-generator-iterator +[iterables]: https://wiki.python.org/moin/Iterator +[iterator]: https://docs.python.org/3.11/glossary.html#term-iterator +[lazy evaluation]: https://en.wikipedia.org/wiki/Lazy_evaluation +[lazy iterator]: https://en.wikipedia.org/wiki/Lazy_evaluation +[yield expression]: https://docs.python.org/3.11/reference/expressions.html#yield-expressions + +## Instructions + +Conda Airlines is the programming-world's biggest airline, with over 10,000 flights a day! + +They are currently assigning all seats to passengers by hand; this will need to be automated. + +They have asked _you_ to create software to automate passenger seat assignments. +They require your software to be memory efficient and performant. + +## 1. Generate seat letters + +Conda wants to generate seat letters for their airplanes. +An airplane is made of rows of seats. +Each row has _4 seats_. +The seats in each row are always named `A`, `B`, `C`, and `D`. +The first seat in the row is `A`, the second seat in the row is `B`, and so on. +After reaching `D`, it should start again with `A`. + +Implement a function `generate_seat_letters()` that accepts an `int` that holds how many seat letters to be generated. +The function should then return an _iterable_ of seat letters. + +```python +>>> letters = generate_seat_letters(4) +>>> next(letters) +"A" +>>> next(letters) +"B" +``` + +## 2. Generate seats + +Conda wants a system that can generate a given number of seats for their airplanes. +Each airplane has _4 seats_ in each row. +The rows are defined using numbers, starting from `1` and going up. +The seats should be ordered, like: `1A`, `1B`, `1C`, `1D`, `2A`, `2B`, `2C`, `2D`, `3A`, `3B`, `3C`, `3D`, ... + +Here is an example: + +| x | 1 | 2 | +| :---------: | :-: | :-: | +| Row | 5 | 21 | +| Seat letter | A | D | +| Result | 5A | 21D | + +Many airlines do not have _row_ number 13 on their flights, due to superstition amongst passengers. +Conda Airlines also follows this convention, so make sure you _don't_ generate seats for _row_ number 13. + +Implement a function `generate_seats()` that accepts an `int` that holds how many seats to be generated. +The function should then return an _iterable_ of seats given. + +```python +>>> seats = generate_seats(10) +>>> next(seats) +"1A" +>>> next(seats) +"1B" +``` + +## 3. Assign seats to passengers + +Now that you have a function that generates seats, you can use it to assign seats to passengers. + +Implement a function `assign_seats()` that accepts a `list` of passenger names. +The function should then return a _dictionary_ of `passenger` as _key_, and `seat_number` as _value_. + +```python +>>> passengers = ['Jerimiah', 'Eric', 'Bethany', 'Byte', 'SqueekyBoots', 'Bob'] + +>>> assign_seats(passengers) +{'Jerimiah': '1A', 'Eric': '1B', 'Bethany': '1C', 'Byte': '1D', 'SqueekyBoots': '2A', 'Bob': '2B'} +``` + +## 4. Ticket codes + +Conda Airlines would like to have a unique code for each ticket. +Since they are a big airline, they have a lot of flights. +This means that there are multiple flights with the same seat number. +They want you to create a system that creates a unique ticket that is _12_ characters long string code for identification. + +This code begins with the `assigned_seat` followed by the `flight_id`. +The rest of the code is appended by `0s`. + +Implement a function `generate_codes(, )` that accepts a `list` of `seat_numbers` and a `string` with the flight number. +The function should then return a `generator` that yields a `ticket_number`. + +```python +>>> seat_numbers = ['1A', '17D'] +>>> flight_id = 'CO1234' +>>> ticket_ids = generate_codes(seat_numbers, flight_id) + +>>> next(ticket_ids) +'1ACO12340000' +>>> next(ticket_ids) +'17DCO1234000' +``` + +## Source + +### Created by + +- @J08K + +### Contributed to by + +- @BethanyG +- @kytrinyx +- @meatball133 + +### My Solution + +- [my solution](./generators.py) +- [run-tests](./run-tests-python.txt) diff --git a/python/plane-tickets/generators.py b/python/plane-tickets/generators.py new file mode 100644 index 00000000..aea5e48e --- /dev/null +++ b/python/plane-tickets/generators.py @@ -0,0 +1,102 @@ +"""Functions to automate Conda airlines ticketing system.""" + +from typing import Any, Generator + +IndexT = int +QuantityT = int + +LetterT = str +LettersT = list[LetterT] + +RowT = int +SeatT = str +SeatsT = list[SeatT] + +NameT = str +PassengersT = list[NameT] +SeatAssignmentsT = dict[NameT, SeatT] + +CodeT = str +FlightIdT = str + + +def generate_seat_letters(quantity: QuantityT) -> Generator[LetterT, Any, Any]: + """Generate a series of letters for airline seats. + + :param quantity: int - total number of seat letters to be generated. + :return: generator - generator that yields seat letters. + + Seat letters are generated from A to D. + After D it should start again with A. + + Example: A, B, C, D + """ + + letters: LettersT = ["A", "B", "C", "D"] + + for pos in range(0, quantity): + index: IndexT = pos % len(letters) + + yield letters[index] + + +def generate_seats(quantity: QuantityT) -> Generator[SeatT, Any, Any]: + """Generate a series of identifiers for airline seats. + + :param quantity: int - total number of seats to be generated. + :return: generator - generator that yields seat numbers. + + A seat number consists of the row number and the seat letter. + + There is no row 13. + Each row has 4 seats. + + Seats should be sorted from low to high. + + Example: 3C, 3D, 4A, 4B + """ + + row: RowT = 0 + + for seat in generate_seat_letters(quantity): + if seat == "A": + row += 1 + + if row == 13: + row += 1 + + yield f"{row}{seat}" + + +def assign_seats(passengers: PassengersT) -> SeatAssignmentsT: + """Assign seats to passengers. + + :param passengers: list[str] - a list of strings containing names of passengers. + :return: dict - with the names of the passengers as keys and seat numbers as values. + + Example output: {"Adele": "1A", "Björk": "1B"} + """ + + seat_assignments: SeatAssignmentsT = {} + seat: Generator[SeatT, Any, Any] = generate_seats(len(passengers)) + + for passenger in passengers: + seat_assignments[passenger] = next(seat) + + return seat_assignments + + +def generate_codes( + seat_numbers: SeatsT, flight_id: FlightIdT +) -> Generator[CodeT, Any, Any]: + """Generate codes for a ticket. + + :param seat_numbers: list[str] - list of seat numbers. + :param flight_id: str - string containing the flight identifier. + :return: generator - generator that yields 12 character long ticket codes. + """ + + for seat in seat_numbers: + code: CodeT = f"{seat}{flight_id}" + + yield f"{code:012}" diff --git a/python/plane-tickets/generators.py,cover b/python/plane-tickets/generators.py,cover new file mode 100644 index 00000000..3cc7f56c --- /dev/null +++ b/python/plane-tickets/generators.py,cover @@ -0,0 +1,102 @@ +> """Functions to automate Conda airlines ticketing system.""" + +> from typing import Any, Generator + +> IndexT = int +> QuantityT = int + +> LetterT = str +> LettersT = list[LetterT] + +> RowT = int +> SeatT = str +> SeatsT = list[SeatT] + +> NameT = str +> PassengersT = list[NameT] +> SeatAssignmentsT = dict[NameT, SeatT] + +> CodeT = str +> FlightIdT = str + + +> def generate_seat_letters(quantity: QuantityT) -> Generator[LetterT, Any, Any]: +> """Generate a series of letters for airline seats. + +> :param quantity: int - total number of seat letters to be generated. +> :return: generator - generator that yields seat letters. + +> Seat letters are generated from A to D. +> After D it should start again with A. + +> Example: A, B, C, D +> """ + +> letters: LettersT = ["A", "B", "C", "D"] + +> for pos in range(0, quantity): +> index: IndexT = pos % len(letters) + +> yield letters[index] + + +> def generate_seats(quantity: QuantityT) -> Generator[SeatT, Any, Any]: +> """Generate a series of identifiers for airline seats. + +> :param quantity: int - total number of seats to be generated. +> :return: generator - generator that yields seat numbers. + +> A seat number consists of the row number and the seat letter. + +> There is no row 13. +> Each row has 4 seats. + +> Seats should be sorted from low to high. + +> Example: 3C, 3D, 4A, 4B +> """ + +> row: RowT = 0 + +> for seat in generate_seat_letters(quantity): +> if seat == "A": +> row += 1 + +> if row == 13: +> row += 1 + +> yield f"{row}{seat}" + + +> def assign_seats(passengers: PassengersT) -> SeatAssignmentsT: +> """Assign seats to passengers. + +> :param passengers: list[str] - a list of strings containing names of passengers. +> :return: dict - with the names of the passengers as keys and seat numbers as values. + +> Example output: {"Adele": "1A", "Björk": "1B"} +> """ + +> seat_assignments: SeatAssignmentsT = {} +> seat: Generator[SeatT, Any, Any] = generate_seats(len(passengers)) + +> for passenger in passengers: +> seat_assignments[passenger] = next(seat) + +> return seat_assignments + + +> def generate_codes( +> seat_numbers: SeatsT, flight_id: FlightIdT +> ) -> Generator[CodeT, Any, Any]: +> """Generate codes for a ticket. + +> :param seat_numbers: list[str] - list of seat numbers. +> :param flight_id: str - string containing the flight identifier. +> :return: generator - generator that yields 12 character long ticket codes. +> """ + +> for seat in seat_numbers: +> code: CodeT = f"{seat}{flight_id}" + +> yield f"{code:012}" diff --git a/python/plane-tickets/generators_test.py b/python/plane-tickets/generators_test.py new file mode 100644 index 00000000..12a3532a --- /dev/null +++ b/python/plane-tickets/generators_test.py @@ -0,0 +1,137 @@ +import inspect +import unittest +import pytest + +from generators import ( + generate_seat_letters, + generate_seats, + assign_seats, + generate_codes +) + +class PlaneTicketsTest(unittest.TestCase): + @pytest.mark.task(taskno=1) + def test_task1_returns_generator(self): + """Test if generate_seat_letters() returns a generator type.""" + + number = 5 + error_message = (f'Called generate_seat_letters({number}). ' + f'The function returned a {type(generate_seat_letters(number))} type, ' + f"but the tests expected the function to return a type.") + + self.assertTrue(inspect.isgenerator(generate_seat_letters(number)), msg=error_message) + + @pytest.mark.task(taskno=1) + def test_generate_seat_letters(self): + test_data = [1, 2, 3, 4, 5] + result_data = [["A"], + ["A", "B"], + ["A", "B", "C"], + ["A", "B", "C", "D"], + ["A", "B", "C", "D", "A"]] + + for variant, (number, expected) in enumerate(zip(test_data, result_data), start=1): + with self.subTest(f"variation #{variant}", number=number, expected=expected): + actual_result = list(generate_seat_letters(number)) + error_message = (f'Called generate_seat_letters({number}). ' + f'The function returned {actual_result}, but the tests ' + f'expected {expected} when generating {number} seat(s).') + + self.assertEqual(actual_result, expected, msg=error_message) + + @pytest.mark.task(taskno=2) + def test_task2_returns_generator(self): + """Test if generate_seats() returns a generator type.""" + + number = 7 + error_message = (f'Called generate_seats({number}). ' + f'The function returned a {type(generate_seats(number))} type, ' + f"but the tests expected the function to return a type.") + + self.assertTrue(inspect.isgenerator(generate_seats(number)), msg=error_message) + + @pytest.mark.task(taskno=2) + def test_generate_seats(self): + test_data = [1, 2, 3, 4, 5] + result_data = [["1A"], + ["1A", "1B"], + ["1A", "1B", "1C"], + ["1A", "1B", "1C", "1D"], + ["1A", "1B", "1C", "1D", "2A"]] + + for variant, (number, expected) in enumerate(zip(test_data, result_data), start=1): + with self.subTest(f"variation #{variant}", number=number, expected=expected): + actual_result = list(generate_seats(number)) + error_message = (f'Called generate_seats({number}). ' + f'The function returned {actual_result}, but the tests ' + f'expected {expected} when generating {number} seat(s).') + + self.assertEqual(actual_result, expected, msg=error_message) + + @pytest.mark.task(taskno=2) + def test_generate_seats_skips_row_13(self): + test_data = [14 * 4] + result_data = [["1A", "1B", "1C", "1D", "2A", "2B", "2C", "2D", + "3A", "3B", "3C", "3D", "4A", "4B", "4C", "4D", + "5A", "5B", "5C", "5D", "6A", "6B", "6C", "6D", + "7A", "7B", "7C", "7D", "8A", "8B", "8C", "8D", + "9A", "9B", "9C", "9D", "10A", "10B", "10C", "10D", + "11A", "11B", "11C", "11D", "12A", "12B", "12C", "12D", + "14A", "14B", "14C", "14D", "15A", "15B", "15C", "15D"]] + + for variant, (number, expected) in enumerate(zip(test_data, result_data), start=1): + with self.subTest(f"variation #{variant}", number=number, expected=expected): + actual_result = list(generate_seats(number)) + error_message = (f'Called generate_seats({number}). ' + f'The function returned {actual_result}, but the tests ' + f'expected: {expected}, when generating {number} seat(s).') + + self.assertEqual(actual_result, expected, msg=error_message) + + @pytest.mark.task(taskno=3) + def test_assign_seats(self): + test_data = [["Passenger1", "Passenger2", "Passenger3", "Passenger4", "Passenger5"], + ["TicketNo=5644", "TicketNo=2273", "TicketNo=493", "TicketNo=5411", "TicketNo=824"]] + result_data = [{"Passenger1": "1A", "Passenger2": "1B", + "Passenger3": "1C", "Passenger4": "1D", "Passenger5": "2A"}, + {"TicketNo=5644": "1A", "TicketNo=2273": "1B", + "TicketNo=493": "1C", "TicketNo=5411": "1D", "TicketNo=824": "2A"}] + + for variant, (passengers, expected) in enumerate(zip(test_data, result_data), start=1): + with self.subTest(f"variation #{variant}", passengers=passengers, expected=expected): + actual_result = assign_seats(passengers) + error_message = (f'Called assign_seats({passengers}). ' + f'The function returned {actual_result}, but the tests ' + f'expected {expected}, when assigning seats.') + + self.assertEqual(actual_result, expected, msg=error_message) + + @pytest.mark.task(taskno=4) + def test_task4_returns_generator(self): + """Test if generate_codes() returns a generator type.""" + + seat_numbers, flight_id = "11B", "HA80085" + error_message = (f'Called generate_codes({seat_numbers}, {flight_id}). ' + f'The function returned a {type(generate_codes(seat_numbers, flight_id))} type, ' + f"but the tests expected the function to return a type.") + + self.assertTrue(inspect.isgenerator(generate_codes(seat_numbers, flight_id)), msg=error_message) + + + @pytest.mark.task(taskno=4) + def test_generate_codes(self): + test_data = [(["12A", "38B", "69C", "102B"],"KL1022"), + (["22C", "88B", "33A", "44B"], "DL1002")] + result_data = [['12AKL1022000', '38BKL1022000', '69CKL1022000', '102BKL102200'], + ['22CDL1002000', '88BDL1002000', '33ADL1002000', '44BDL1002000']] + + for variant, ((seat_numbers, flight_id), expected) in enumerate(zip(test_data, result_data), start=1): + with self.subTest(f"variation #{variant}", seat_numbbers=seat_numbers, + flight_id=flight_id, expected=expected): + + actual_result = list(generate_codes(seat_numbers, flight_id)) + error_message = (f'Called generate_codes({seat_numbers}, {flight_id}). ' + f'The function returned {actual_result}, but the tests ' + f'expected {expected} when generating ticket numbers.') + + self.assertEqual(list(generate_codes(seat_numbers, flight_id)), expected, msg=error_message) diff --git a/python/plane-tickets/pytest.ini b/python/plane-tickets/pytest.ini new file mode 100644 index 00000000..8dc12a49 --- /dev/null +++ b/python/plane-tickets/pytest.ini @@ -0,0 +1,5 @@ +[pytest] +pythonpath = . +addopts = --doctest-modules +markers = + task: exercise task/step diff --git a/python/plane-tickets/run-tests-python.txt b/python/plane-tickets/run-tests-python.txt new file mode 100644 index 00000000..96f7eb91 --- /dev/null +++ b/python/plane-tickets/run-tests-python.txt @@ -0,0 +1,500 @@ +Running automated test file(s): + + +=============================================================================== + +Running: ../../.github/citools/python/python-lint-pylint + +Running Python Lint - PyLint + +Python versions: + + Python 3.12.1 + pyenv 2.3.36-21-g7e550e31 + pip 24.0 from /home/vpayno/.pyenv/versions/3.12.1/lib/python3.12/site-packages/pip (python 3.12) + PDM, version 2.13.2 + + + ============================================================================== + +Running: pylint --version + +pylint 3.0.3 +astroid 3.0.2 +Python 3.12.1 (main, Dec 28 2023, 08:22:05) [GCC 10.2.1 20210110] + +real 0m0.238s +user 0m0.158s +sys 0m0.082s + + + ============================================================================== + +Running: pylint ./src + + +------------------------------------------------------------------- +Your code has been rated at 10.00/10 (previous run: 9.74/10, +0.26) + + +real 0m0.768s +user 0m0.685s +sys 0m0.084s + + + ============================================================================== + +Exit code: 0 + +real 0m1.992s +user 0m1.511s +sys 0m0.501s + +real 0m1.997s +user 0m1.512s +sys 0m0.505s + +=============================================================================== + +Running: ../../.github/citools/python/python-lint-ruff + +Running Python Lint - Ruff + +Python versions: + + Python 3.12.1 + pyenv 2.3.36-21-g7e550e31 + pip 24.0 from /home/vpayno/.pyenv/versions/3.12.1/lib/python3.12/site-packages/pip (python 3.12) + PDM, version 2.13.2 + + + ============================================================================== + +Running: ruff --version + +ruff 0.3.5 + +real 0m0.070s +user 0m0.032s +sys 0m0.039s + + + ============================================================================== + +Running: ruff check --ignore E501 ./src + +All checks passed! + +real 0m0.126s +user 0m0.055s +sys 0m0.074s + + + ============================================================================== + +Exit code: 0 + +real 0m1.149s +user 0m0.754s +sys 0m0.415s + +real 0m1.151s +user 0m0.756s +sys 0m0.415s + +=============================================================================== + +Running: ../../.github/citools/python/python-lint-pyright + +Running Python Lint - PyRight + +Python versions: + + Python 3.12.1 + pyenv 2.3.36-21-g7e550e31 + pip 24.0 from /home/vpayno/.pyenv/versions/3.12.1/lib/python3.12/site-packages/pip (python 3.12) + PDM, version 2.13.2 + + + ============================================================================== + +Running: pyright --version + +pyright 1.1.357 + +real 0m0.676s +user 0m0.500s +sys 0m0.128s + + + ============================================================================== + +Running: pyright --stats ./src + +Found 2 source files +pyright 1.1.357 +0 errors, 0 warnings, 0 informations +Completed in 0.601sec + +Analysis stats +Total files parsed and bound: 20 +Total files checked: 2 + +Timing stats +Find Source Files: 0sec +Read Source Files: 0sec +Tokenize: 0.06sec +Parse: 0.05sec +Resolve Imports: 0.05sec +Bind: 0.06sec +Check: 0.06sec +Detect Cycles: 0sec + +real 0m1.269s +user 0m1.421s +sys 0m0.188s + + + ============================================================================== + +Exit code: 0 + +real 0m2.930s +user 0m2.579s +sys 0m0.658s + +real 0m2.933s +user 0m2.581s +sys 0m0.659s + +=============================================================================== + +Running: ../../.github/citools/python/python-lint-bandit + +Running Python Lint - Bandit + +Python versions: + + Python 3.12.1 + pyenv 2.3.36-21-g7e550e31 + pip 24.0 from /home/vpayno/.pyenv/versions/3.12.1/lib/python3.12/site-packages/pip (python 3.12) + PDM, version 2.13.2 + + + ============================================================================== + +Running: bandit --version + +bandit 1.7.7 + python version = 3.12.1 (main, Dec 28 2023, 08:22:05) [GCC 10.2.1 20210110] + +real 0m0.285s +user 0m0.220s +sys 0m0.067s + + + ============================================================================== + +Running: bandit --verbose --recursive ./src + +[main] INFO profile include tests: None +[main] INFO profile exclude tests: None +[main] INFO cli include tests: None +[main] INFO cli exclude tests: None +[main] INFO running on Python 3.12.1 +Run started:2024-04-05 03:39:46.246487 +Files in scope (2): + ./src/plane_tickets/__init__.py (score: {SEVERITY: 0, CONFIDENCE: 0}) + ./src/plane_tickets/generators.py (score: {SEVERITY: 0, CONFIDENCE: 0}) +Files excluded (2): + ./src/plane_tickets/__pycache__/__init__.cpython-312.pyc + ./src/plane_tickets/__pycache__/generators.cpython-312.pyc + +Test results: + No issues identified. + +Code scanned: + Total lines of code: 65 + Total lines skipped (#nosec): 0 + Total potential issues skipped due to specifically being disabled (e.g., #nosec BXXX): 0 + +Run metrics: + Total issues (by severity): + Undefined: 0 + Low: 0 + Medium: 0 + High: 0 + Total issues (by confidence): + Undefined: 0 + Low: 0 + Medium: 0 + High: 0 +Files skipped (0): + +real 0m0.237s +user 0m0.166s +sys 0m0.071s + + + ============================================================================== + +Exit code: 0 + +real 0m1.526s +user 0m1.063s +sys 0m0.482s + +real 0m1.529s +user 0m1.064s +sys 0m0.484s + +=============================================================================== + +Running: ../../.github/citools/python/python-lint-refurb + +Running Python Lint - Refurb + +Python versions: + + Python 3.12.1 + pyenv 2.3.36-21-g7e550e31 + pip 24.0 from /home/vpayno/.pyenv/versions/3.12.1/lib/python3.12/site-packages/pip (python 3.12) + PDM, version 2.13.2 + + + ============================================================================== + +Running: refurb --version + +Refurb: v1.26.0 +Mypy: v1.9.0 + +real 0m0.200s +user 0m0.143s +sys 0m0.058s + + + ============================================================================== + +Running: refurb ./src + + +real 0m1.110s +user 0m1.029s +sys 0m0.082s + + + ============================================================================== + +Exit code: 0 + +real 0m2.249s +user 0m1.830s +sys 0m0.434s + +real 0m2.252s +user 0m1.830s +sys 0m0.436s + +=============================================================================== + +Running: ../../.github/citools/python/python-test-with-doctests + +Running Python DocTests + +Python versions: + + Python 3.12.1 + pyenv 2.3.36-21-g7e550e31 + pip 24.0 from /home/vpayno/.pyenv/versions/3.12.1/lib/python3.12/site-packages/pip (python 3.12) + PDM, version 2.13.2 + + + ============================================================================== + +PYTHONPATH="./src" + + ============================================================================== + +Running: python -m doctest -v ./src/plane_tickets/generators.py ./src/plane_tickets/__init__.py + +5 items had no tests: + generators + generators.assign_seats + generators.generate_codes + generators.generate_seat_letters + generators.generate_seats +0 tests in 5 items. +0 passed and 0 failed. +Test passed. +1 items had no tests: + __init__ +0 tests in 1 items. +0 passed and 0 failed. +Test passed. + +real 0m0.146s +user 0m0.087s +sys 0m0.061s + + + ============================================================================== + +Exit code: 0 + +real 0m1.088s +user 0m0.759s +sys 0m0.344s + +real 0m1.090s +user 0m0.759s +sys 0m0.347s + +=============================================================================== + +Running: ../../.github/citools/python/python-test-with-coverage + +Running Python Tests With Coverage + +Python versions: + + Python 3.12.1 + pyenv 2.3.36-21-g7e550e31 + pip 24.0 from /home/vpayno/.pyenv/versions/3.12.1/lib/python3.12/site-packages/pip (python 3.12) + PDM, version 2.13.2 + + + ============================================================================== + +Running: rm -rf ./coverage + + +real 0m0.001s +user 0m0.000s +sys 0m0.001s + + + ============================================================================== + +Running: pytest --version + +pytest 7.4.3 + +real 0m0.862s +user 0m0.914s +sys 0m0.836s + + + ============================================================================== + +PYTHONPATH="./src" + + ============================================================================== + +Running: pytest --verbose --cov=. --cov-branch --cov-report=term-missing --cov-report=xml:.coverage.xml -p no:randomly ./test + +============================= test session starts ============================== +platform linux -- Python 3.12.1, pytest-7.4.3, pluggy-1.3.0 -- /home/vpayno/.pyenv/versions/3.12.1/bin/python +cachedir: .pytest_cache +hypothesis profile 'default' -> database=DirectoryBasedExampleDatabase(PosixPath('/home/vpayno/git_vpayno/exercism-workspace/python/plane-tickets/.hypothesis/examples')) +rootdir: /home/vpayno/git_vpayno/exercism-workspace/python/plane-tickets +configfile: pytest.ini +plugins: anyio-4.2.0, libtmux-0.25.0, pylama-8.4.1, cov-4.1.0, datafiles-3.0.0, docker-2.0.1, subprocess-1.5.0, typeguard-4.1.5, hypothesis-6.98.17, snapshot-0.9.0 +collecting ... collected 8 items + +test/generators_test.py::PlaneTicketsTest::test_assign_seats PASSED [ 12%] +test/generators_test.py::PlaneTicketsTest::test_generate_codes PASSED [ 25%] +test/generators_test.py::PlaneTicketsTest::test_generate_seat_letters PASSED [ 37%] +test/generators_test.py::PlaneTicketsTest::test_generate_seats PASSED [ 50%] +test/generators_test.py::PlaneTicketsTest::test_generate_seats_skips_row_13 PASSED [ 62%] +test/generators_test.py::PlaneTicketsTest::test_task1_returns_generator PASSED [ 75%] +test/generators_test.py::PlaneTicketsTest::test_task2_returns_generator PASSED [ 87%] +test/generators_test.py::PlaneTicketsTest::test_task4_returns_generator PASSED [100%] + +---------- coverage: platform linux, python 3.12.1-final-0 ----------- +Name Stmts Miss Branch BrPart Cover Missing +----------------------------------------------------------- +generators.py 36 0 12 0 100% +----------------------------------------------------------- +TOTAL 36 0 12 0 100% +Coverage XML written to file .coverage.xml + + +============================== 8 passed in 0.37s =============================== + +real 0m1.209s +user 0m1.049s +sys 0m0.161s + + + ============================================================================== + +Running: coverage report --show-missing + +Name Stmts Miss Branch BrPart Cover Missing +----------------------------------------------------------- +generators.py 36 0 12 0 100% +----------------------------------------------------------- +TOTAL 36 0 12 0 100% + +real 0m0.172s +user 0m0.105s +sys 0m0.069s + + + ============================================================================== + +Running: coverage annotate + + +real 0m0.162s +user 0m0.102s +sys 0m0.061s + + + ============================================================================== + +Line Coverage: 100.0% +Branch Coverage: 100.0% + + ============================================================================== + +Exit code: 0 + +real 0m3.377s +user 0m2.849s +sys 0m1.437s + +real 0m3.379s +user 0m2.850s +sys 0m1.437s + +=============================================================================== + +tail -n 10000 ./*,cover | grep -E -C 3 '^> def |^! ' + +> def generate_seat_letters(quantity: QuantityT) -> Generator[LetterT, Any, Any]: +> """Generate a series of letters for airline seats. +-- + +> def generate_seats(quantity: QuantityT) -> Generator[SeatT, Any, Any]: +> """Generate a series of identifiers for airline seats. +-- + +> def assign_seats(passengers: PassengersT) -> SeatAssignmentsT: +> """Assign seats to passengers. +-- + +> def generate_codes( +> seat_numbers: SeatsT, flight_id: FlightIdT + +=============================================================================== + +Running: misspell ./src/plane_tickets/generators.py ./src/plane_tickets/__init__.py + +real 0m0.020s +user 0m0.021s +sys 0m0.008s + +=============================================================================== + diff --git a/python/plane-tickets/src/plane_tickets/__init__.py b/python/plane-tickets/src/plane_tickets/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/python/plane-tickets/src/plane_tickets/generators.py b/python/plane-tickets/src/plane_tickets/generators.py new file mode 120000 index 00000000..7bd19289 --- /dev/null +++ b/python/plane-tickets/src/plane_tickets/generators.py @@ -0,0 +1 @@ +../../generators.py \ No newline at end of file diff --git a/python/plane-tickets/test/__init__.py b/python/plane-tickets/test/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/python/plane-tickets/test/generators_test.py b/python/plane-tickets/test/generators_test.py new file mode 120000 index 00000000..bf11ad55 --- /dev/null +++ b/python/plane-tickets/test/generators_test.py @@ -0,0 +1 @@ +../generators_test.py \ No newline at end of file