From d36c95f00b0ed8216d07b0c55f262c6d2cf154e1 Mon Sep 17 00:00:00 2001 From: Carlos Meza Date: Tue, 1 Mar 2022 12:37:16 -0800 Subject: [PATCH 01/20] chore: create initial commit From 81de98a2b5d5c0e86e83ef198b7f5217b66240cf Mon Sep 17 00:00:00 2001 From: Carlos Meza Date: Tue, 1 Mar 2022 13:15:00 -0800 Subject: [PATCH 02/20] ci(pre-commit): add --- .commitlintrc.yaml | 2 ++ .pre-commit-config.yaml | 42 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+) create mode 100644 .commitlintrc.yaml create mode 100644 .pre-commit-config.yaml diff --git a/.commitlintrc.yaml b/.commitlintrc.yaml new file mode 100644 index 000000000..9cb74a70c --- /dev/null +++ b/.commitlintrc.yaml @@ -0,0 +1,2 @@ +extends: + - "@commitlint/config-conventional" diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 000000000..19040b547 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,42 @@ +exclude: >- + (Pipfile\.lock|CHANGELOG\.md|Makefile|solution|test|appendix|bin|docker|extra|input|template|01_hello/hello0|07_gashlycrumb/gashlycrumb|18_gematria/asciitbl|20_password/harvest|using|typehints|unit|manual|README) +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: "v4.1.0" + hooks: + - id: check-executables-have-shebangs + - id: check-shebang-scripts-are-executable + - id: check-toml + - id: check-yaml + - id: end-of-file-fixer + - id: fix-byte-order-marker + - id: mixed-line-ending + - id: trailing-whitespace + args: [--markdown-linebreak-ext=md] + - id: no-commit-to-branch + args: ["-b", "master"] + - repo: https://github.com/pycqa/isort + rev: "5.10.1" + hooks: + - id: isort + - repo: https://github.com/psf/black + rev: "22.1.0" + hooks: + - id: black + - repo: https://github.com/flakeheaven/flakeheaven + rev: "0.11.0" + hooks: + - id: flakeheaven + - repo: https://github.com/executablebooks/mdformat + rev: "0.7.13" + hooks: + - id: mdformat + additional_dependencies: + - mdformat-gfm + - mdformat-beautysh + - mdformat-tables + exclude: ^docs\/(index|changelog)\.md$ + - repo: https://github.com/rhysd/actionlint + rev: "v1.6.9" + hooks: + - id: actionlint-docker From 2cfa29cf183eacb77afed025e2da8e7320ea8443 Mon Sep 17 00:00:00 2001 From: Carlos Meza Date: Tue, 1 Mar 2022 14:20:41 -0800 Subject: [PATCH 03/20] ci(ci): add workflow --- .github/workflows/ci.yaml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 .github/workflows/ci.yaml diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 000000000..16d4cd7ed --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,19 @@ +name: CI + +on: + push: + branches: ['**'] + paths-ignore: [CHANGELOG.md] + tags-ignore: ['**'] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref_name }} + cancel-in-progress: true + +jobs: + + pre-commit: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: digitalr00ts/action-pre-commit@v0 From 2b3850cccce1d44e5dd5ccd1f06c11141d827b33 Mon Sep 17 00:00:00 2001 From: Carlos Meza Date: Tue, 1 Mar 2022 17:29:32 -0800 Subject: [PATCH 04/20] chore(pyproject): add configs for tools --- pyproject.toml | 184 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 184 insertions(+) create mode 100644 pyproject.toml diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 000000000..3f84a5cd1 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,184 @@ +[tool.bandit] +exclude = ['tests'] + +[tool.black] +line-length = 98 +target-version = ['py310'] + +[tool.coverage.run] +branch = true + +[tool.coverage.report] +precision = 2 +show_missing = true + +[tool.flakeheaven] +exclude=[ + '.venv', + '01_hello/hello01_print.py', + '01_hello/hello02_comment.py', + '01_hello/hello03_shebang.py', + '01_hello/hello04_argparse_positional.py', + '01_hello/hello05_argparse_option.py', + '01_hello/hello06_main_function.py', + '01_hello/hello07_get_args.py', + '01_hello/hello08_formatted.py', + '01_hello/test.py', + '02_crowsnest/solution.py', + '02_crowsnest/test.py', + '03_picnic/solution.py', + '03_picnic/test.py', + '04_jump_the_five/solution1.py', + '04_jump_the_five/solution2.py', + '04_jump_the_five/solution3.py', + '04_jump_the_five/solution4.py', + '04_jump_the_five/solution5.py', + '04_jump_the_five/test.py', + '05_howler/solution1.py', + '05_howler/solution2.py', + '05_howler/test.py', + '06_wc/solution.py', + '06_wc/test.py', + '07_gashlycrumb/gashlycrumb_interactive.py', + '07_gashlycrumb/solution1.py', + '07_gashlycrumb/solution2_dict_comp.py', + '07_gashlycrumb/solution3_dict_get.py', + '07_gashlycrumb/test.py', + '08_apples_and_bananas/solution1_iterate_chars.py', + '08_apples_and_bananas/solution2_str_replace.py', + '08_apples_and_bananas/solution3_str_translate.py', + '08_apples_and_bananas/solution4_list_comprehension.py', + '08_apples_and_bananas/solution5.1_no_closure.py', + '08_apples_and_bananas/solution5_list_comp_function.py', + '08_apples_and_bananas/solution6_map_lambda.py', + '08_apples_and_bananas/solution7_map_function.py', + '08_apples_and_bananas/solution8_regex.py', + '08_apples_and_bananas/test.py', + '09_abuse/solution.py', + '09_abuse/test.py', + '10_telephone/solution1.py', + '10_telephone/solution2_list.py', + '10_telephone/test.py', + '11_bottles_of_beer/solution.py', + '11_bottles_of_beer/test.py', + '12_ransom/solution1_for_loop.py', + '12_ransom/solution2_for_append_list.py', + '12_ransom/solution3_for_append_string.py', + '12_ransom/solution4_list_comprehension.py', + '12_ransom/solution5_shorter_list_comp.py', + '12_ransom/solution6_map.py', + '12_ransom/solution7_shorter_map.py', + '12_ransom/test.py', + '13_twelve_days/solution.py', + '13_twelve_days/solution_emoji.py', + '13_twelve_days/test.py', + '14_rhymer/solution1_regex.py', + '14_rhymer/solution2_no_regex.py', + '14_rhymer/solution3_dict_words.py', + '14_rhymer/test.py', + '15_kentucky_friar/solution1_regex.py', + '15_kentucky_friar/solution2_re_compile.py', + '15_kentucky_friar/solution3_no_regex.py', + '15_kentucky_friar/test.py', + '16_scrambler/solution.py', + '16_scrambler/test.py', + '17_mad_libs/solution1_regex.py', + '17_mad_libs/solution2_no_regex.py', + '17_mad_libs/test.py', + '18_gematria/asciitbl.py', + '18_gematria/solution.py', + '18_gematria/test.py', + '19_wod/manual1.py', + '19_wod/manual2_list_comprehension.py', + '19_wod/manual3_map.py', + '19_wod/solution1.py', + '19_wod/solution2.py', + '19_wod/test.py', + '19_wod/unit.py', + '19_wod/using_csv1.py', + '19_wod/using_csv2.py', + '19_wod/using_csv3.py', + '19_wod/using_pandas.py', + '20_password/harvest.py', + '20_password/solution.py', + '20_password/test.py', + '20_password/unit.py', + '21_tictactoe/solution1.py', + '21_tictactoe/solution2.py', + '21_tictactoe/test.py', + '21_tictactoe/unit.py', + '22_itictactoe/solution1.py', + '22_itictactoe/solution2_typed_dict.py', + '22_itictactoe/typehints.py', + '22_itictactoe/typehints2.py', + '22_itictactoe/unit.py', + 'appendix_argparse/cat_n.py', + 'appendix_argparse/cat_n_manual.py', + 'appendix_argparse/choices.py', + 'appendix_argparse/manual.py', + 'appendix_argparse/nargs+.py', + 'appendix_argparse/nargs2.py', + 'appendix_argparse/one_arg.py', + 'appendix_argparse/two_args.py', + 'bin/new.py', + 'extra/02_dna/test.py', + 'extra/02_spanish/test.py', + 'extra/02_strings/test.py', + 'extra/03_lister/test.py', + 'extra/04_days/test.py', + 'extra/06_head/test.py', + 'extra/07_proteins/test.py', + 'extra/08_rna/test.py', + 'extra/09_moog/test.py', + 'extra/10_whitmans/solution.py', + 'extra/10_whitmans/test.py', + 'template/template.py', +] + +[tool.flakeheaven.plugins] +dlint = ['+*'] +# flake8-bandit = ['+*'] +flake8-bugbear = ['+*'] +flake8-comprehensions = ['+*'] +# flake8-darglint = ['+*',] +flake8-debugger = ['+*'] +flake8-docstrings = ['+*'] +flake8-eradicate = ['+*'] +flake8-pytest-style = ['+*'] +flake8-rst-docstrings = ['+*'] +mccabe = ['+*'] +pep8-naming = ['+*'] +pyflakes = ['+*'] + +[tool.isort] +profile = 'black' +line_length = 98 + +[tool.pydocstyle] +convention = 'google' + +[tool.pylint.config] +max-line-length = 98 +persistent = 'no' +enable = [ + 'F', # Fatal + 'E', # Error + 'W', # Warning + 'C', # Convention + 'I', # Informational +] +disable = [ + # 'R', # Refactor + 'too-many-instance-attributes', + 'fixme', + 'locally-disabled', + 'no-absolute-import', + 'suppressed-message', +] + +[tool.pytest.ini_options] +addopts = '--verbosity=2 --doctest-modules --ignore=docs --showlocals -rfp --strict-markers --cov-report=term' +log_cli = true +log_level = 'DEBUG' +log_cli_level = 'INFO' +log_file_level = 'DEBUG' From 443b4a8004dc042ac52b9a12f1ccc63476533c81 Mon Sep 17 00:00:00 2001 From: Carlos Meza Date: Tue, 1 Mar 2022 17:30:12 -0800 Subject: [PATCH 05/20] chore(pipfile): add --- Pipfile | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 Pipfile diff --git a/Pipfile b/Pipfile new file mode 100644 index 000000000..76300d5c5 --- /dev/null +++ b/Pipfile @@ -0,0 +1,25 @@ +[[source]] +url = "https://pypi.org/simple" +verify_ssl = true +name = "pypi" + +[packages] + +[dev-packages] +black = "*" +pylint = "*" +isort = "*" +pytest-cov = "*" +flakeheaven = "*" +dlint = "*" +flake8-bugbear = "*" +flake8-comprehensions = "*" +flake8-debugger = "*" +flake8-docstrings = "*" +flake8-eradicate = "*" +flake8-pytest-style = "*" +flake8-rst-docstrings = "*" +pep8-naming = "*" + +[requires] +python_version = "3.11" From 6eedc2a520b3245af26b2eb7bb5c88179369b27d Mon Sep 17 00:00:00 2001 From: Carlos Meza Date: Tue, 1 Mar 2022 23:50:07 -0800 Subject: [PATCH 06/20] feat(01_hello): add answer --- 01_hello/hello.py | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100755 01_hello/hello.py diff --git a/01_hello/hello.py b/01_hello/hello.py new file mode 100755 index 000000000..99c5ee5da --- /dev/null +++ b/01_hello/hello.py @@ -0,0 +1,32 @@ +#!/usr/bin/env python3 +"""hello module.""" + +import argparse +from typing import Any + + +def get_args(args: list[Any] | None = None) -> argparse.Namespace: + """Get parsed argparse agruments.""" + parser = argparse.ArgumentParser( + description="Say hello", + allow_abbrev=False, + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + ) + parser.add_argument( + "-n", + "--name", + default="World", + help="The name to greet", + ) + + return parser.parse_args(args) + + +def main(): + """Run main logic.""" + args = get_args() + print(f"Hello, {args.name}!") + + +if __name__ == "__main__": + main() From 551ab02389edad0d6655aaee26acad3934a52bf1 Mon Sep 17 00:00:00 2001 From: Carlos Meza Date: Tue, 1 Mar 2022 23:51:44 -0800 Subject: [PATCH 07/20] feat(02_crowsnet): add answer --- 02_crowsnest/crowsnest.py | 43 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100755 02_crowsnest/crowsnest.py diff --git a/02_crowsnest/crowsnest.py b/02_crowsnest/crowsnest.py new file mode 100755 index 000000000..5fbc48bac --- /dev/null +++ b/02_crowsnest/crowsnest.py @@ -0,0 +1,43 @@ +#!/usr/bin/env python3 +"""crowsnet module.""" + +import argparse +from typing import Any, Literal + + +def get_args(args: list[Any] | None = None) -> argparse.Namespace: + """Get parsed argparse agruments.""" + parser = argparse.ArgumentParser( + description="Crow's Nest -- choose the correct article", + allow_abbrev=False, + ) + parser.add_argument( + "str", + help="A word", + ) + + return parser.parse_args(args) + + +def get_article(word: str) -> Literal["a", "an"]: + """Return article 'an' if word begins with a vowel, otherwise return 'a'. + + >>> get_article("consonants") + 'a' + >>> get_article("apple") + 'an' + >>> word="example" + >>> f"This is {get_article(word)} {word}." + 'This is an example.' + """ + return "an" if word.casefold().startswith(("a", "e", "i", "o", "u")) else "a" + + +def main(): + """Run main logic.""" + args = get_args() + print(f"Ahoy, Captain, {get_article(args.str)} {args.str} off the larboard bow!") + + +if __name__ == "__main__": + main() From 88ae49b4f0f915ce0850e5cd466460ff163d6adf Mon Sep 17 00:00:00 2001 From: Carlos Meza Date: Wed, 2 Mar 2022 01:04:04 -0800 Subject: [PATCH 08/20] feat(02_crowsnet): add "going further" --- 02_crowsnest/crowsnest.py | 49 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 46 insertions(+), 3 deletions(-) diff --git a/02_crowsnest/crowsnest.py b/02_crowsnest/crowsnest.py index 5fbc48bac..03ff2ded2 100755 --- a/02_crowsnest/crowsnest.py +++ b/02_crowsnest/crowsnest.py @@ -2,41 +2,84 @@ """crowsnet module.""" import argparse +import sys from typing import Any, Literal +SIDES = ("larboard", "starboard") + def get_args(args: list[Any] | None = None) -> argparse.Namespace: """Get parsed argparse agruments.""" parser = argparse.ArgumentParser( description="Crow's Nest -- choose the correct article", allow_abbrev=False, + formatter_class=argparse.ArgumentDefaultsHelpFormatter, ) parser.add_argument( "str", help="A word", ) + parser.add_argument( + "-s", + "--side", + choices=SIDES, + default="larboard", + help="Select side.", + ) return parser.parse_args(args) -def get_article(word: str) -> Literal["a", "an"]: +def get_article(word: str) -> Literal["a", "an", "A", "An"]: """Return article 'an' if word begins with a vowel, otherwise return 'a'. >>> get_article("consonants") 'a' >>> get_article("apple") 'an' + >>> get_article("Consonants") + 'A' + >>> get_article("Apple") + 'An' >>> word="example" >>> f"This is {get_article(word)} {word}." 'This is an example.' """ - return "an" if word.casefold().startswith(("a", "e", "i", "o", "u")) else "a" + article = "an" if word.casefold().startswith(("a", "e", "i", "o", "u")) else "a" + return article if word.islower() else article.title() + + +def is_word_ok(word: str) -> bool: + """Raise exception if word is invalid. + + >>> is_word_ok("Yes") + True + >>> is_word_ok("!nope") + Traceback (most recent call last): + ValueError: '!nope' does not start with an alphabetic character. + """ + if not word[0].isalpha(): + sys.tracebacklimit = 0 + raise ValueError(f"'{word}' does not start with an alphabetic character.") + return True + + +def get_message(word: str, side: str) -> str: + """Get message. + + >>> get_message("unicorn", "larboard") + 'Ahoy, Captain, an unicorn off the larboard bow!' + >>> get_message("Shamu", "starboard") + 'Ahoy, Captain, A Shamu off the starboard bow!' + """ + return f"Ahoy, Captain, {get_article(word)} {word} off the {side} bow!" def main(): """Run main logic.""" args = get_args() - print(f"Ahoy, Captain, {get_article(args.str)} {args.str} off the larboard bow!") + is_word_ok(args.str) + print(get_message(args.str, args.side)) if __name__ == "__main__": From 6bb3b44a41a5108d25f8279d34b56c0349a3b901 Mon Sep 17 00:00:00 2001 From: Carlos Meza Date: Wed, 2 Mar 2022 01:10:11 -0800 Subject: [PATCH 09/20] feat(02_crowsnet): merge get_article into get_message --- 02_crowsnest/crowsnest.py | 25 ++++--------------------- 1 file changed, 4 insertions(+), 21 deletions(-) diff --git a/02_crowsnest/crowsnest.py b/02_crowsnest/crowsnest.py index 03ff2ded2..baed12ec0 100755 --- a/02_crowsnest/crowsnest.py +++ b/02_crowsnest/crowsnest.py @@ -3,7 +3,7 @@ import argparse import sys -from typing import Any, Literal +from typing import Any SIDES = ("larboard", "starboard") @@ -30,25 +30,6 @@ def get_args(args: list[Any] | None = None) -> argparse.Namespace: return parser.parse_args(args) -def get_article(word: str) -> Literal["a", "an", "A", "An"]: - """Return article 'an' if word begins with a vowel, otherwise return 'a'. - - >>> get_article("consonants") - 'a' - >>> get_article("apple") - 'an' - >>> get_article("Consonants") - 'A' - >>> get_article("Apple") - 'An' - >>> word="example" - >>> f"This is {get_article(word)} {word}." - 'This is an example.' - """ - article = "an" if word.casefold().startswith(("a", "e", "i", "o", "u")) else "a" - return article if word.islower() else article.title() - - def is_word_ok(word: str) -> bool: """Raise exception if word is invalid. @@ -72,7 +53,9 @@ def get_message(word: str, side: str) -> str: >>> get_message("Shamu", "starboard") 'Ahoy, Captain, A Shamu off the starboard bow!' """ - return f"Ahoy, Captain, {get_article(word)} {word} off the {side} bow!" + article = "an" if word.casefold().startswith(("a", "e", "i", "o", "u")) else "a" + article = article if word.islower() else article.title() + return f"Ahoy, Captain, {article} {word} off the {side} bow!" def main(): From 422dc87bd57749858584b118434128b4965f4d6a Mon Sep 17 00:00:00 2001 From: Carlos Meza Date: Wed, 2 Mar 2022 09:50:09 -0800 Subject: [PATCH 10/20] refactor(02_crowsnest): extract vowels into constant --- 02_crowsnest/crowsnest.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/02_crowsnest/crowsnest.py b/02_crowsnest/crowsnest.py index baed12ec0..6d2beb0c0 100755 --- a/02_crowsnest/crowsnest.py +++ b/02_crowsnest/crowsnest.py @@ -6,6 +6,7 @@ from typing import Any SIDES = ("larboard", "starboard") +VOWELS = ("a", "e", "i", "o", "u") def get_args(args: list[Any] | None = None) -> argparse.Namespace: @@ -53,7 +54,7 @@ def get_message(word: str, side: str) -> str: >>> get_message("Shamu", "starboard") 'Ahoy, Captain, A Shamu off the starboard bow!' """ - article = "an" if word.casefold().startswith(("a", "e", "i", "o", "u")) else "a" + article = "an" if word.casefold().startswith(VOWELS) else "a" article = article if word.islower() else article.title() return f"Ahoy, Captain, {article} {word} off the {side} bow!" From 84e6ccd8d9963fb24c203e964f6155f5dd24ea59 Mon Sep 17 00:00:00 2001 From: Carlos Meza Date: Wed, 9 Mar 2022 01:18:55 -0800 Subject: [PATCH 11/20] feat(03_picnic): add picnic --- 03_picnic/picnic.py | 75 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100755 03_picnic/picnic.py diff --git a/03_picnic/picnic.py b/03_picnic/picnic.py new file mode 100755 index 000000000..b033b3e89 --- /dev/null +++ b/03_picnic/picnic.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python3 +"""picnic module.""" + +import argparse +from typing import Any + + +def get_args(args: list[Any] | None = None) -> argparse.Namespace: + """Get parsed argparse agruments.""" + parser = argparse.ArgumentParser( + description="Picnic game.", + allow_abbrev=False, + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + ) + parser.add_argument( + "str", + nargs="+", + type=str, + help="Item(s) to bring", + ) + parser.add_argument( + "-s", + "--sorted", + action="store_true", + help="Sort the items", + ) + + parser.add_argument( + "-x", + "--no-oxford", + action="store_false", + help="Do not use Oxford comma", + ) + + parser.add_argument( + "-c", + "--separator", + default=",", + type=str, + help="Separator", + ) + + return parser.parse_args(args) + + +def get_message( + items: list[str], sort: bool = False, oxford: bool = True, separator: str = "," +) -> str: + """Get message. + + >>> get_message(["chips", "pie", "soda"], separator=";") + 'You are bringing chips; pie; and soda.' + >>> get_message(["chips", "pie", "soda"], oxford=False) + 'You are bringing chips, pie and soda.' + >>> get_message([]) + Traceback (most recent call last): + ValueError: Empty list. + """ + if not items: + raise ValueError("Empty list.") + items.sort() if sort else items + connect = f"{separator if oxford and len(items) > 2 else ''} and " if len(items) > 1 else "" + return f"You are bringing {f'{separator} '.join(items[:-1])}{connect}{items[-1]}." + + +def main(): + """Run main logic.""" + args = get_args() + print( + get_message(args.str, sort=args.sorted, oxford=args.no_oxford, separator=args.separator) + ) + + +if __name__ == "__main__": + main() From a7e549208d6c0239faa069ea6ed0342095918fc0 Mon Sep 17 00:00:00 2001 From: Carlos Meza Date: Mon, 14 Mar 2022 23:24:53 -0700 Subject: [PATCH 12/20] feat(04_jump_the_five): add --- 04_jump_the_five/jump.py | 83 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100755 04_jump_the_five/jump.py diff --git a/04_jump_the_five/jump.py b/04_jump_the_five/jump.py new file mode 100755 index 000000000..a4d6db272 --- /dev/null +++ b/04_jump_the_five/jump.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python3 +"""jump module.""" + +import argparse +from typing import Any + +J0 = { + "1": "9", + "2": "8", + "3": "7", + "4": "6", + "5": "0", + "6": "4", + "7": "3", + "8": "2", + "9": "1", + "0": "5", +} +J1 = { + "1": "one", + "2": "two", + "3": "three", + "4": "four", + "5": "five", + "6": "six", + "7": "seven", + "8": "eight", + "9": "nine", + "0": "zero", +} +JUMPER = [J0, J1] + + +def get_args(args: list[Any] | None = None) -> argparse.Namespace: + """Get parsed argparse agruments.""" + parser = argparse.ArgumentParser( + description="Jump the Five", + allow_abbrev=False, + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + ) + parser.add_argument( + "str", + nargs="+", + type=str, + help="Input text", + ) + + return parser.parse_args(args) + + +def get_encoded(string: str, jumper: dict[str, Any] = JUMPER[0]) -> str: + return "".join(jumper.get(c, "") if c.isnumeric() else c for c in string) + + +def main(): + """Run main logic.""" + args = get_args() + print(get_encoded(" ".join(args.str))) + + +if __name__ == "__main__": + main() + +import random + +import pytest + + +def test_round_trip(): + string = str(random.randint(10**9, 10**10)) + assert string == get_encoded(get_encoded(string)) + + +@pytest.mark.parametrize( + "string, expected", + [ + ("509", "fivezeronine"), + ("213-01", "twoonethree-zeroone"), + ("this is 7.", "this is seven."), + ], +) +def test_j1(string: str, expected: str): + assert expected == get_encoded(string, JUMPER[1]) From 4b4df468e37aaaad3600ecc3c5a7c993b5d0f98e Mon Sep 17 00:00:00 2001 From: Carlos Meza Date: Mon, 14 Mar 2022 23:25:34 -0700 Subject: [PATCH 13/20] ci(pre-commit): update hooks --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 19040b547..a8c0923b6 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -28,7 +28,7 @@ repos: hooks: - id: flakeheaven - repo: https://github.com/executablebooks/mdformat - rev: "0.7.13" + rev: "0.7.14" hooks: - id: mdformat additional_dependencies: @@ -37,6 +37,6 @@ repos: - mdformat-tables exclude: ^docs\/(index|changelog)\.md$ - repo: https://github.com/rhysd/actionlint - rev: "v1.6.9" + rev: "v1.6.10" hooks: - id: actionlint-docker From f1244dfe9ff287cc252cc1259da5a74200eeeb9d Mon Sep 17 00:00:00 2001 From: Carlos Meza Date: Wed, 16 Mar 2022 13:00:02 -0700 Subject: [PATCH 14/20] refactor(04_jump_the_five): remove unnecessary conditional --- 04_jump_the_five/jump.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/04_jump_the_five/jump.py b/04_jump_the_five/jump.py index a4d6db272..c531cf870 100755 --- a/04_jump_the_five/jump.py +++ b/04_jump_the_five/jump.py @@ -49,7 +49,7 @@ def get_args(args: list[Any] | None = None) -> argparse.Namespace: def get_encoded(string: str, jumper: dict[str, Any] = JUMPER[0]) -> str: - return "".join(jumper.get(c, "") if c.isnumeric() else c for c in string) + return "".join(jumper.get(c, c) for c in string) def main(): From 766ab491733c85aa317d6dfa612c99ed1ef95c7b Mon Sep 17 00:00:00 2001 From: Carlos Meza Date: Mon, 28 Mar 2022 16:18:14 -0700 Subject: [PATCH 15/20] feat(05_howler): add howler.py --- 05_howler/howler.py | 60 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100755 05_howler/howler.py diff --git a/05_howler/howler.py b/05_howler/howler.py new file mode 100755 index 000000000..7152df375 --- /dev/null +++ b/05_howler/howler.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python3 +"""howler module.""" + +import argparse +import sys +from pathlib import Path +from typing import Any + + +def get_args(args: list[Any] | None = None) -> argparse.Namespace: + """Get parsed argparse agruments.""" + parser = argparse.ArgumentParser( + description="Howler (upper-cases input)", + allow_abbrev=False, + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + ) + parser.add_argument( + "-e", + "--ee", + action="store_true", + help="lower-case instead", + ) + parser.add_argument( + "-o", + "--outfile", + metavar="str", + default="", + type=str, + help="Output filename", + ) + parser.add_argument( + "text", + type=str, + help="Input string or file", + ) + + return parser.parse_args(args) + + +def get_text(text: str) -> str: + """Get text from file or pass thru.""" + path = Path(text) + return path.read_text() if path.is_file() else text + + +def change_case(text: str, lower: bool = False) -> str: + """Change text to upper or lower case.""" + return getattr(get_text(text), "lower" if lower else "upper")() + + +def main(): + """Run main logic.""" + args = get_args() + + with Path(args.outfile).open(mode="w") if args.outfile else sys.stdout as fid: + print(change_case(args.text, args.ee), file=fid) + + +if __name__ == "__main__": + main() From 4ab86011713c7fb6dccafe0c237de8e2d9403812 Mon Sep 17 00:00:00 2001 From: Carlos Meza Date: Tue, 29 Mar 2022 21:12:46 -0700 Subject: [PATCH 16/20] fix(05_howler): add tests --- 05_howler/howler.py | 52 +++++++++++++++++++++++++++++++++++++-------- 1 file changed, 43 insertions(+), 9 deletions(-) diff --git a/05_howler/howler.py b/05_howler/howler.py index 7152df375..a6fd64e3d 100755 --- a/05_howler/howler.py +++ b/05_howler/howler.py @@ -4,10 +4,10 @@ import argparse import sys from pathlib import Path -from typing import Any +from typing import Sequence -def get_args(args: list[Any] | None = None) -> argparse.Namespace: +def get_args(args: Sequence[str] | None = None) -> argparse.Namespace: """Get parsed argparse agruments.""" parser = argparse.ArgumentParser( description="Howler (upper-cases input)", @@ -38,23 +38,57 @@ def get_args(args: list[Any] | None = None) -> argparse.Namespace: def get_text(text: str) -> str: - """Get text from file or pass thru.""" + """Get text from file or pass thru. + + >>> get_text("not a file") + 'not a file' + """ path = Path(text) return path.read_text() if path.is_file() else text def change_case(text: str, lower: bool = False) -> str: - """Change text to upper or lower case.""" + """Change text to upper or lower case. + + >>> change_case("sOmE tExT") + 'SOME TEXT' + >>> change_case("sOmE tExT", lower = True) + 'some text' + """ return getattr(get_text(text), "lower" if lower else "upper")() -def main(): +def main(args: Sequence[str] | None = None): """Run main logic.""" - args = get_args() - - with Path(args.outfile).open(mode="w") if args.outfile else sys.stdout as fid: - print(change_case(args.text, args.ee), file=fid) + parsed_args = get_args(args) + # print('some text', end='') + with Path(parsed_args.outfile).open(mode="w") if parsed_args.outfile else sys.stdout as fid: + print(change_case(parsed_args.text, parsed_args.ee), file=fid) if __name__ == "__main__": main() + + +def test_get_text_file(): + """Verify when arg is a file, then the text of the file is returned.""" + ### Arrange + arg = __file__ + expected = Path(arg).read_text() + ### Act + result = get_text(arg) + ### Assert + assert result == expected + + +def test_main_lower(): + ### Arrange + results_file = Path("test_output.txt") + args = ["-o", str(results_file), "--ee", "sOmE tExT"] + expected = "some text\n" + ### Act + main(args) + ### Assert + assert results_file.read_text() == expected + ### Cleanup + results_file.unlink() From a951b849aaaa51704476131e37a26d0917d91080 Mon Sep 17 00:00:00 2001 From: Carlos Meza Date: Sat, 2 Apr 2022 21:59:06 -0700 Subject: [PATCH 17/20] ci(pre-commit): update black --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a8c0923b6..b85806424 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -20,7 +20,7 @@ repos: hooks: - id: isort - repo: https://github.com/psf/black - rev: "22.1.0" + rev: "22.3.0" hooks: - id: black - repo: https://github.com/flakeheaven/flakeheaven From c324a516f13a0f93894c0c0bdae8b8df2ceb2896 Mon Sep 17 00:00:00 2001 From: Carlos Meza Date: Tue, 5 Apr 2022 00:55:02 -0700 Subject: [PATCH 18/20] feat(06_wc): add wc.py --- 06_wc/wc.py | 54 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100755 06_wc/wc.py diff --git a/06_wc/wc.py b/06_wc/wc.py new file mode 100755 index 000000000..4b5853b11 --- /dev/null +++ b/06_wc/wc.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python3 +"""wc module.""" + +import argparse +import sys +from io import TextIOWrapper +from typing import Sequence + +TEMPLATE = "{:8} {:7} {:7} {}" + + +def get_args(args: Sequence[str] | None = None) -> argparse.Namespace: + """Get parsed argparse agruments.""" + parser = argparse.ArgumentParser( + description="Emulate wc (word count)", + allow_abbrev=False, + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + ) + parser.add_argument( + "files", + nargs="*", + metavar="FILE", + type=argparse.FileType("rt"), + default=[sys.stdin], + help="Input file(s)", + ) + + return parser.parse_args(args) + + +def count(fid: TextIOWrapper) -> tuple[int, int, int]: + """Count the charaters, letters, and lines in a file.""" + chars, words, lines = 0, 0, 0 + for line in fid: + lines += 1 + words += len(line.split()) + chars += len(line) + return lines, words, chars + + +def main(args: Sequence[str] | None = None): + """Run main logic.""" + parsed_args = get_args(args) + + sums = {fid.name: count(fid) for fid in parsed_args.files} + + for file, values in sums.items(): + print(TEMPLATE.format(*values, file)) + if len(parsed_args.files) > 1: + print(TEMPLATE.format(*list(map(sum, zip(*sums.values()))), "total")) + + +if __name__ == "__main__": + main() From ef09625eb89efad73e50741306af38dd14337116 Mon Sep 17 00:00:00 2001 From: Carlos Meza Date: Sun, 10 Apr 2022 19:08:38 -0700 Subject: [PATCH 19/20] feat(07_gashlycrumb): add gashlycrumb.py --- 07_gashlycrumb/gashlycrumb.py | 45 +++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100755 07_gashlycrumb/gashlycrumb.py diff --git a/07_gashlycrumb/gashlycrumb.py b/07_gashlycrumb/gashlycrumb.py new file mode 100755 index 000000000..c54270731 --- /dev/null +++ b/07_gashlycrumb/gashlycrumb.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 +"""gashlycrumb module.""" + +import argparse +from typing import Sequence + +DEFAULT_FILE = "gashlycrumb.txt" + + +def get_args(args: Sequence[str] | None = None) -> argparse.Namespace: + """Get parsed argparse agruments.""" + parser = argparse.ArgumentParser( + description="Gashlycrumb", + allow_abbrev=False, + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + ) + parser.add_argument( + "letter", + nargs="+", + type=str, + help="Letter(s)", + ) + parser.add_argument( + "-f", + "--file", + type=argparse.FileType("rt"), + default=DEFAULT_FILE, + help="Input file", + ) + + return parser.parse_args(args) + + +def main(args: Sequence[str] | None = None): + """Run main logic.""" + parsed_args = get_args(args) + key_line = {line.split(" ", 1)[0].casefold(): line.strip() for line in parsed_args.file} + output = [ + key_line.get(key.casefold(), f'I do not know "{key}".') for key in parsed_args.letter + ] + print("\n".join(output)) + + +if __name__ == "__main__": + main() From 1681b8194b079d9a5024f7b99fed465b4e467ca8 Mon Sep 17 00:00:00 2001 From: Carlos Meza Date: Mon, 25 Apr 2022 23:05:42 -0600 Subject: [PATCH 20/20] feat(08_apples_and_bananas): add apples.py --- 08_apples_and_bananas/apples.py | 152 ++++++++++++++++++++++++++++++++ 1 file changed, 152 insertions(+) create mode 100755 08_apples_and_bananas/apples.py diff --git a/08_apples_and_bananas/apples.py b/08_apples_and_bananas/apples.py new file mode 100755 index 000000000..8195fcfbe --- /dev/null +++ b/08_apples_and_bananas/apples.py @@ -0,0 +1,152 @@ +#!/usr/bin/env python3 +"""Apples and Bananas""" + +import argparse + +# from concurrent.futures import ProcessPoolExecutor +from ctypes.wintypes import MAX_PATH +from pathlib import Path +from typing import Sequence + +VOWELS = "aeiou" + + +def get_args(args: Sequence[str] | None = None) -> argparse.Namespace: + """Get parsed argparse agruments.""" + + parser = argparse.ArgumentParser( + description="Apples and bananas", + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + ) + + parser.add_argument("text", help="Input text or file") + + parser.add_argument( + "-v", + "--vowel", + help="The vowel to substitute", + type=str, + default="a", + choices=list(VOWELS), + ) + + parsed_args = parser.parse_args(args) + + if len(parsed_args.text) <= MAX_PATH: + path = Path(parsed_args.text) + if path.is_file(): + parsed_args.text = path.read_text() + + return parsed_args + + +def solution_loop(args: Sequence[str] | None = None) -> str: + """Iterate over text.""" + + VOWELS_UPPER = VOWELS.upper() + + def swap_vowel(char: str) -> str: + if char in VOWELS: + return vowel + if char in VOWELS_UPPER: + return vowel.upper() + return char + + ns_args = get_args(args) + vowel = ns_args.vowel + + text = (swap_vowel(char) for char in ns_args.text) + return "".join(text) + + +def solution_translate(args: Sequence[str] | None = None) -> str: + """Use translate.""" + ns_args = get_args(args) + vowel = ns_args.vowel + trans = str.maketrans(VOWELS + VOWELS.upper(), vowel * 5 + vowel.upper() * 5) + return ns_args.text.translate(trans) + + +def solution_replace(args: Sequence[str] | None = None) -> str: + """Use replace.""" + ns_args = get_args(args) + text = ns_args.text + vowel = ns_args.vowel + + for v in VOWELS: + text = text.replace(v, vowel).replace(v.upper(), vowel.upper()) + + return text + + +def solution_map(args: Sequence[str] | None = None) -> str: + """Iterate over text.""" + + VOWELS_UPPER = VOWELS.upper() + + def swap_vowel(char: str) -> str: + if char in VOWELS: + return vowel + if char in VOWELS_UPPER: + return vowel.upper() + return char + + ns_args = get_args(args) + vowel = ns_args.vowel + + # with ProcessPoolExecutor() as executor: + # text = executor.map( + # swap_vowel, ns_args.text, + # chunksize= 10000, + # ) + text = map(swap_vowel, ns_args.text) + + return "".join(text) + + +IMPLEMENTATION = ( + solution_translate, + solution_replace, + solution_map, + solution_loop, +) + + +def main(args: Sequence[str] | None = None, implementation: int = 0): + """Run main logic.""" + + print(IMPLEMENTATION[implementation](args)) + + +if __name__ == "__main__": + main() + +# import cProfile +import gc +from time import time + +import pytest + + +@pytest.mark.parametrize("i", range(4)) +def test_profile(i: int): + gc.disable() + trys = 10 + size = 4 * 10**5 + text = Path("apples.py").read_text() * size + func = IMPLEMENTATION[i] + + def run(): + gc.collect() + start_time = time() + func(args=[text]) + return time() - start_time + + print( + f"{func.__name__} avg {sum(run() for _ in range(trys)) / float(trys)} secs " + f"for {trys} executions on {text.__sizeof__()/float(1<<10):,.0f} KB." + ) + + # with cProfile.Profile() as pr: + # IMPLEMENTATION[i](args=[text]) + # pr.print_stats()