diff --git a/.github/workflows/event-logger.yml b/.github/workflows/event-logger.yml index 31f231e..01c9a2b 100644 --- a/.github/workflows/event-logger.yml +++ b/.github/workflows/event-logger.yml @@ -35,20 +35,32 @@ jobs: - name: Log pull request opened if: github.event_name == 'pull_request' && github.event.action == 'opened' run: | + if [ -z "$COMMIT_LOG_API" ]; then + echo "COMMIT_LOG_API is not set; skipping logging step."; exit 0 + fi pipenv run gitcommitlogger -r $(echo $REPOSITORY_URL) -t pull_request_opened -d $(echo $PR_CREATED_AT) -un $(echo $GITHUB_LOGIN) -o commit_stats.csv -u $(echo $COMMIT_LOG_API) -v - name: Log pull request closed and merged if: github.event_name == 'pull_request' && github.event.action == 'closed' && github.event.pull_request.merged == true run: | + if [ -z "$COMMIT_LOG_API" ]; then + echo "COMMIT_LOG_API is not set; skipping logging step."; exit 0 + fi echo $COMMITS > commits.json cat commits.json # debugging pipenv run gitcommitlogger -r $(echo $REPOSITORY_URL) -t pull_request_merged -d $(echo $PR_CLOSED_AT) -un $(echo $GITHUB_LOGIN) -i commits.json -o commit_stats.csv -u $(echo $COMMIT_LOG_API) -v - name: Log pull request closed without merge if: github.event_name == 'pull_request' && github.event.action == 'closed' && github.event.pull_request.merged == false run: | + if [ -z "$COMMIT_LOG_API" ]; then + echo "COMMIT_LOG_API is not set; skipping logging step."; exit 0 + fi pipenv run gitcommitlogger -r $(echo $REPOSITORY_URL) -t pull_request_closed -d $(echo $PR_CLOSED_AT) -un $(echo $GITHUB_LOGIN) -o commit_stats.csv -u $(echo $COMMIT_LOG_API) -v - name: Log push if: github.event_name == 'push' run: | + if [ -z "$COMMIT_LOG_API" ]; then + echo "COMMIT_LOG_API is not set; skipping logging step."; exit 0 + fi echo $COMMITS > commits.json cat commits.json # debugging pipenv run gitcommitlogger -r $(echo $REPOSITORY_URL) -t $(echo $EVENT_TYPE) -i commits.json -o commit_stats.csv -u $(echo $COMMIT_LOG_API) -v diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000..e1b5690 --- /dev/null +++ b/Pipfile @@ -0,0 +1,17 @@ +[[source]] +url = "https://pypi.org/simple" +verify_ssl = true +name = "pypi" + +[packages] +bloomsays = {file = ".", editable = true} + +[dev-packages] +build = "*" +pytest = "*" +coverage = "*" +pytest-cov = "*" +twine = "*" + +[requires] +python_version = "3" \ No newline at end of file diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 0000000..dd6eb58 --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,66 @@ +{ + "_meta": { + "hash": { + "sha256": "3beed5b7bae4170fab84abbe8e8d59fbbbceafcf89a35fb2def3b9ffd9be4e83" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "bloomsays": { + "editable": true, + "file": "." + }, + "iniconfig": { + "hashes": [ + "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", + "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12" + ], + "markers": "python_version >= '3.10'", + "version": "==2.3.0" + }, + "packaging": { + "hashes": [ + "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", + "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f" + ], + "markers": "python_version >= '3.8'", + "version": "==25.0" + }, + "pluggy": { + "hashes": [ + "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", + "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746" + ], + "markers": "python_version >= '3.9'", + "version": "==1.6.0" + }, + "pygments": { + "hashes": [ + "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", + "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b" + ], + "markers": "python_version >= '3.8'", + "version": "==2.19.2" + }, + "pytest": { + "hashes": [ + "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", + "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79" + ], + "index": "pypi", + "markers": "python_version >= '3.9'", + "version": "==8.4.2" + } + }, + "develop": {} +} diff --git a/README.md b/README.md index 6022e0e..0ce73ad 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,78 @@ -# Python Package Exercise +# Bloomsays + +## What is Bloomsays? +Bloomsays is a fun python package with some of our favorite lines from Professor Bloomberg. + +## Instructions +pip install -e . + +run python: python + +## Usage +Ater installation, you can import and call the functions from the package +```python +from bloomsays.wisdom import avg, random_quote, coding_wisdom + +#Get your average grade +avg(90, 80, 100) + +#Get a random quote from Professor Bloomberg! +random_quote() + +#Get some coding wisdom with your specified language +coding_wisdom("Python") + +``` + +## Functions + +## Example Output + ______________ + | ask Bloombot | + ============== + \ + \ + + @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ + @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ + @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@&%%%##(##&@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ + @@@@@@@@@@@@@@@@@@@@@@@@@@@@@%%%#%%%%%%%%%#######%@&@@@@@@@@@@@@@@@@@@@@@@@@@@@@ + @@@@@@@@@@@@@@@@@@@@@@@@@@%%#%%&%%%###%%####(#%&&&%%%%&&@@@@@@@@@@@@@@@@@@@@@@@@ + @@@@@@@@@@@@@@@@@@@@@@@%&&&&&#((///((((////**////(#&&&&&&%@@@@@@@@@@@@@@@@@@@@@@ + @@@@@@@@@@@@@@@@@@@@&&&&&&%#(////***************////(#@@&&&@@@@@@@@@@@@@@@@@@@@@ + @@@@@@@@@@@@@@@@@@@@&&&@&#(///***************,*****///(&@&&&@@@@@@@@@@@@@@@@@@@@ + @@@@@@@@@@@@@@@@@@@&&&&&#(///********,,,,,,,,,,,****///(&&&&&@@@@@@@@@@@@@@@@@@@ + @@@@@@@@@@@@@@@@@@@&&&&%(////*****,*,*,,,,,,,,,,,,****//#&&@&@@@@@@@@@@@@@@@@@@@ + @@@@@@@@@@@@@@@@@@&&&&&%(////*******,,,,,,,,,,********//(&&&&&@@@@@@@@@@@@@@@@@@ + @@@@@@@@@@@@@@@@@@&@&&&%////**********,,,,,,,,,,******//(%&&&&@@@@@@@@@@@@@@@@@@ + @@@@@@@@@@@@@@@@@@&@@&&%(///*******,,,,,,,,,,,,,,******//#&@&&@@@@@@@@@@@@@@@@@@ + @@@@@@@@@@@@@@@@@@&&@@&%(///(#(####(/****,**//(%%%%##(///(&&&@@@@@@@@@@@@@@@@@@@ + @@@@@@@@@@@@@@@@@@@&&&&(//%###%##%%###(/***/(((%&&&&%###((&@&//@@@@@@@@@@@@@@@@@ + @@@@@@@@@@@@@@@@@(((#&&(//(#%#(*##,/(/(/***///(**#*/(((///&%#((/@@@@@@@@@@@@@@@@ + @@@@@@@@@@@@@@@@@/(//&&(//***//*////////****/*****/******/#(**/*@@@@@@@@@@@@@@@@ + @@@@@@@@@@@@@@@@@/(/(%&(//***/*******//**,,*/*******,,**//##(*/@@@@@@@@@@@@@@@@@ + @@@@@@@@@@@@@@@@@@/(#(&(///*********///*,,,,*//*********//%%(*#@@@@@@@@@@@@@@@@@ + @@@@@@@@@@@@@@@@@@@///&%(//********/////****///*******///(&%//&@@@@@@@@@@@@@@@@@ + @@@@@@@@@@@@@@@@@@@@(/&&##((///**//((#&##%####(//***//(###&#/&@@@@@@@@@@@@@@@@@@ + @@@@@@@@@@@@@@@@@@@@@(&&&&%#(/(%%###%%%##%##%%%((##(((##%&&&@@@@@@@@@@@@@@@@@@@@ + @@@@@@@@@@@@@@@@@@@@@@&&&&%##%&%&&%###%##((###%%%%#%%%%%&&&@@@@@@@@@@@@@@@@@@@@@ + @@@@@@@@@@@@@@@@@@@@@@@&&&&&&%&%%#//(////////////%&&&#&&&&@@@@@@@@@@@@@@@@@@@@@@ + @@@@@@@@@@@@@@@@@@@@@@@@(%&@&&&&%#((((###%##((((((%&&&&@&%@@@@@@@@@@@@@@@@@@@@@@ + @@@@@@@@@@@@@@@@@@@@@@,#%/#&&&&&&&&%##%%#%%%##(#%&&%&&@&@@@@@@@@@@@@@@@@@@@@@@@@ + @@@@@@@@@@@@@@@@@@@@@..(&//(&&&&&&&&&%%%%#%%(%%&&@&&@%(@@@@@@@@@@@@@@@@@@@@@@@@@ + @@@@@@@@@@@@@@@@@@@@....,///(%&&&&@&&&&%%%%%&&&&@@@&(/..@@@@@@@@@@@@@@@@@@@@@@@@ + @@@@@@@@@@@@@@@@@@@.......////((#%&&&&&&@&&&@&@&&@%(//*,../@@@@@@@@@@@@@@@@@@@@@ + @@@@@@@@@@@@@@@@............//(((((((##&&@@@@@&&#/////#,,......(@@@@@@@@@@@@@@@@ + @@@@@@@@@@@@%.......... ...*///////(((((((//////*//&(.......... *@@@@@@@@@@@@ + @@@@@@@@@%. ..,..//**///////////****//%/*. ............ .&@@@@@@ + +['ask Bloombot'] + +## Contributors +- Luna Suzuki - [github](https://github.com/lunasuzuki) +- Kazi Hossain - [github](https://github.com/kazisean) +- Tawhid Zaman [github](https://github.com/TawhidZGit) +- Jack Chen - [github]() +- Howard Appel - [github](https://github.com/hna2019) + -An exercise to create a Python package, build it, test it, distribute it, and use it. See [instructions](./instructions.md) for details. diff --git a/TODO b/TODO new file mode 100644 index 0000000..77bc9fb --- /dev/null +++ b/TODO @@ -0,0 +1,17 @@ +[] At least four functions that accept arguments which influence their behavior +[] The package must be distributed in the PyPI repository and installable via pip. +[] Use pipenv to manage the package dependencies and virtual environments with a Pipfile. +[] Use pytest to test this should be no fewer than three tests per package function. +[] Use build to create the package artifacts. +[] Use twine to upload the package to PyPI. +[] Use GitHub Actions to build your package and run your tests on two different recent versions of Python with every pull request to the main branch of your GitHub repository. +[] delete the feature branch +[] beautiful read-me file on + [] description + [] clear instructions + [] code examples + [] include doc for all the functions and how to use + import + [] how to contribute + build + test + run + [] Include a badge at the top of the README.md file showing the result of the latest build/test workflow run. + [] Include the names of all teammates as links to their GitHub profiles in the README.md file. + [] Include a link to your package's page on the PyPI website. \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..32422c0 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,36 @@ +[build-system] +requires = ["setuptools>=61.0", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "bloomsays" +description = "A package that gives quotes from the professor" +version = "0.1.0" +authors = [ + { name="Howard Appel", email="hna2019@nyu.edu" }, + { name="Kazi Hossain", email="keh8423@nyu.edu" }, + { name="Luna Suzuki", email="las9963@nyu.edu" }, + { name="Tawhid Zaman", email="tnz8738@nyu.edu" }, + { name="Jack Chen", email="jc11462@nyu.edu"} +] +license = { file = "LICENSE" } +readme = "README.md" +keywords = ["python", "package", "build", "lighthearted"] +requires-python = ">=3.7" +classifiers = [ + "Programming Language :: Python :: 3", + "Intended Audience :: Education", + "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", + "Operating System :: OS Independent", +] + +[project.optional-dependencies] +dev = ["pytest"] + +[project.urls] +Homepage = "https://github.com/swe-students-fall2025/3-python-package-team_orchid" +Repository = "https://github.com/swe-students-fall2025/3-python-package-team_orchid.git" + + +[project.scripts] +bloomsays = "bloomsays.__main__:main" diff --git a/src/bloomsays/__init__.py b/src/bloomsays/__init__.py new file mode 100644 index 0000000..1c2be9c --- /dev/null +++ b/src/bloomsays/__init__.py @@ -0,0 +1,8 @@ +from .bubble import wrap_text, make_bubble + +from . import wisdom +__all__ = [ + "wrap_text", + "make_bubble", + "wisdom", +] diff --git a/src/bloomsays/__main__.py b/src/bloomsays/__main__.py new file mode 100644 index 0000000..45d6e20 --- /dev/null +++ b/src/bloomsays/__main__.py @@ -0,0 +1,47 @@ +import sys +from bloomsays import wisdom + +def main(): + args = sys.argv[1:] + if not args: + print("Usage: bloomsays randomQuote [n] OR bloomsays joke [n] OR bloomsays avg num1 num2 ...") + return + + command = args[0] + + if command == "randomQuote": + n = int(args[1]) if len(args) > 1 else 1 + wisdom.random_quote(n) + elif command == 'joke': + n = int(args[1]) if len(args) > 1 else 1 + wisdom.jokes(n) + elif command == "avg": + if len(args) < 2: + print("Usage: bloomsays avg num1 num2 ...") + return + try: + numbers = [float(x) for x in args[1:]] + except ValueError: + print("All arguments for avg must be numbers.") + return + wisdom.avg(*numbers) + elif command == "codingWisdom": + language = args[1] if len(args) > 1 else "Python" + wisdom.coding_wisdom(language) + elif command == "studyTip": + if len(args) < 3: + print("Usage: bloomsays studyTip numQuestions difficulty") + return + try: + num_questions = int(args[1]) + difficulty = args[2] + except ValueError: + print("numQuestions must be an integer.") + return + wisdom.study_tip(num_questions, difficulty) + else: + print(f"Unknown command: {command}") + print("Usage: bloomsays randomQuote [n] | bloomsays avg num1 num2 ...") + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/src/bloomsays/bubble.py b/src/bloomsays/bubble.py new file mode 100644 index 0000000..0826c7d --- /dev/null +++ b/src/bloomsays/bubble.py @@ -0,0 +1,66 @@ +from __future__ import annotations +from typing import List + + +def wrap_text(text: str, width: int) -> List[str]: + """ + Pure text wrapper (no printing). + - Splits on whitespace. + - If a single word is longer than width, it is put on its own line (no hyphenation). + """ + if width is None or width < 1: + raise ValueError("width must be >= 1") + + words = text.split() + if not words: + return [""] + + lines: List[str] = [] + cur = words[0] + for w in words[1:]: + if len(cur) + 1 + len(w) <= width: + cur += " " + w + else: + lines.append(cur) + cur = w + lines.append(cur) + return lines + + +def make_bubble(text: str, width: int | None = None) -> str: + """ + Build a speech bubble as a single string. + - If width is provided, wrap the text to that width. + - Supports multi-line input already containing '\n' (each line is treated as a paragraph). + """ + if text is None: + raise ValueError("text must be a string") + + # split paragraphs first + paragraphs = text.split("\n") + if width is not None: + if width < 1: + raise ValueError("width must be >= 1") + lines = [] + for p in paragraphs: + if p.strip() == "": + lines.append("") # preserve empty line + else: + lines.extend(wrap_text(p, width)) + else: + lines = paragraphs + + # compute max visible width + maxw = max((len(line) for line in lines), default=0) + + top = " " + "_" * (maxw + 2) + body = "\n".join(f"| {line.ljust(maxw)} |" for line in lines) + bottom = " " + "=" * (maxw + 2) + tail = " \\\n \\" + + return f"{top}\n {body}\n{bottom}\n{tail}" + +#test - run: python3 -m bloomsays.bubble +if __name__ == "__main__": + print(make_bubble("Ask Bloombot!")) + diff --git a/src/bloomsays/wisdom.py b/src/bloomsays/wisdom.py new file mode 100644 index 0000000..59b86eb --- /dev/null +++ b/src/bloomsays/wisdom.py @@ -0,0 +1,179 @@ +import random +import textwrap +from pathlib import Path +from .bubble import make_bubble + +allJokes = [ + "I was about to crack a joke on Ubuntu’s text editor, but you might not gedit.", + "I’d tell them a UDP joke but there’s no guarantee that they would get it.", + "When I wrote this, only God and I understood what I was doing. Now, God only knows.", + "#define TRUE FALSE //Happy debugging suckers", + "Which body part does a programmer know best? -> ARM", + "What do you call a busy waiter? -> A server.", + "What do you call an idle server? -> A waiter", + "!false -> It's funny 'cause it's true." + ] +# source : https://zriyansh.medium.com/top-programming-jokes-that-will-make-your-day-or-night-6d986b338f2d +# https://github.com/wesbos/dad-jokes + + +ascii_art = r""" + @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ + @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ + @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@&%%%##(##&@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ + @@@@@@@@@@@@@@@@@@@@@@@@@@@@@%%%#%%%%%%%%%#######%@&@@@@@@@@@@@@@@@@@@@@@@@@@@@@ + @@@@@@@@@@@@@@@@@@@@@@@@@@%%#%%&%%%###%%####(#%&&&%%%%&&@@@@@@@@@@@@@@@@@@@@@@@@ + @@@@@@@@@@@@@@@@@@@@@@@%&&&&&#((///((((////**////(#&&&&&&%@@@@@@@@@@@@@@@@@@@@@@ + @@@@@@@@@@@@@@@@@@@@&&&&&&%#(////***************////(#@@&&&@@@@@@@@@@@@@@@@@@@@@ + @@@@@@@@@@@@@@@@@@@@&&&@&#(///***************,*****///(&@&&&@@@@@@@@@@@@@@@@@@@@ + @@@@@@@@@@@@@@@@@@@&&&&&#(///********,,,,,,,,,,,****///(&&&&&@@@@@@@@@@@@@@@@@@@ + @@@@@@@@@@@@@@@@@@@&&&&%(////*****,*,*,,,,,,,,,,,,****//#&&@&@@@@@@@@@@@@@@@@@@@ + @@@@@@@@@@@@@@@@@@&&&&&%(////*******,,,,,,,,,,********//(&&&&&@@@@@@@@@@@@@@@@@@ + @@@@@@@@@@@@@@@@@@&@&&&%////**********,,,,,,,,,,******//(%&&&&@@@@@@@@@@@@@@@@@@ + @@@@@@@@@@@@@@@@@@&@@&&%(///*******,,,,,,,,,,,,,,******//#&@&&@@@@@@@@@@@@@@@@@@ + @@@@@@@@@@@@@@@@@@&&@@&%(///(#(####(/****,**//(%%%%##(///(&&&@@@@@@@@@@@@@@@@@@@ + @@@@@@@@@@@@@@@@@@@&&&&(//%###%##%%###(/***/(((%&&&&%###((&@&//@@@@@@@@@@@@@@@@@ + @@@@@@@@@@@@@@@@@(((#&&(//(#%#(*##,/(/(/***///(**#*/(((///&%#((/@@@@@@@@@@@@@@@@ + @@@@@@@@@@@@@@@@@/(//&&(//***//*////////****/*****/******/#(**/*@@@@@@@@@@@@@@@@ + @@@@@@@@@@@@@@@@@/(/(%&(//***/*******//**,,*/*******,,**//##(*/@@@@@@@@@@@@@@@@@ + @@@@@@@@@@@@@@@@@@/(#(&(///*********///*,,,,*//*********//%%(*#@@@@@@@@@@@@@@@@@ + @@@@@@@@@@@@@@@@@@@///&%(//********/////****///*******///(&%//&@@@@@@@@@@@@@@@@@ + @@@@@@@@@@@@@@@@@@@@(/&&##((///**//((#&##%####(//***//(###&#/&@@@@@@@@@@@@@@@@@@ + @@@@@@@@@@@@@@@@@@@@@(&&&&%#(/(%%###%%%##%##%%%((##(((##%&&&@@@@@@@@@@@@@@@@@@@@ + @@@@@@@@@@@@@@@@@@@@@@&&&&%##%&%&&%###%##((###%%%%#%%%%%&&&@@@@@@@@@@@@@@@@@@@@@ + @@@@@@@@@@@@@@@@@@@@@@@&&&&&&%&%%#//(////////////%&&&#&&&&@@@@@@@@@@@@@@@@@@@@@@ + @@@@@@@@@@@@@@@@@@@@@@@@(%&@&&&&%#((((###%##((((((%&&&&@&%@@@@@@@@@@@@@@@@@@@@@@ + @@@@@@@@@@@@@@@@@@@@@@,#%/#&&&&&&&&%##%%#%%%##(#%&&%&&@&@@@@@@@@@@@@@@@@@@@@@@@@ + @@@@@@@@@@@@@@@@@@@@@..(&//(&&&&&&&&&%%%%#%%(%%&&@&&@%(@@@@@@@@@@@@@@@@@@@@@@@@@ + @@@@@@@@@@@@@@@@@@@@....,///(%&&&&@&&&&%%%%%&&&&@@@&(/..@@@@@@@@@@@@@@@@@@@@@@@@ + @@@@@@@@@@@@@@@@@@@.......////((#%&&&&&&@&&&@&@&&@%(//*,../@@@@@@@@@@@@@@@@@@@@@ + @@@@@@@@@@@@@@@@............//(((((((##&&@@@@@&&#/////#,,......(@@@@@@@@@@@@@@@@ + @@@@@@@@@@@@%.......... ...*///////(((((((//////*//&(.......... *@@@@@@@@@@@@ + @@@@@@@@@%. ..,..//**///////////****//%/*. ............ .&@@@@@@ + """ + + +def avg(*grades): + + average = sum(grades)/ len(grades) + message = f"Your average grade is {average:.2f}" + + bubble = make_bubble(message) + print(f"{bubble}\n{ascii_art}") + + return average + +def random_quote(n=1): + profLines = ["everything is due at class time", "ask Bloombot", "Quizzes: 25%", "Exercises & Projects: 75%", "Discord is our main source of communitcation"] + selected_quotes = random.choices(profLines, k=n) + + bubble_text = "\n".join(selected_quotes) + + bubble = make_bubble(bubble_text) + print(f"{bubble}\n{ascii_art}") + + return selected_quotes + +def coding_wisdom(language="Python"): + wisdom_dict = { + "Python": [ + "Remember: readability counts!", + "Use list comprehensions wisely", + "Virtual environments are your friend", + "PEP 8 is the style guide to follow", + "Test your code with pytest" + ], + "JavaScript": [ + "Async/await makes life easier", + "Always use const and let, never var", + "Arrow functions are your friend", + "npm install is just the beginning", + "Console.log is for debugging only" + ], + "Java": [ + "Object-oriented design matters", + "Exceptions should be exceptional", + "Use interfaces wisely", + "The JVM is powerful but watch memory", + "Unit tests save production bugs" + ], + "C++": [ + "Manage your memory carefully", + "RAII is your best friend", + "Smart pointers over raw pointers", + "The STL is incredibly powerful", + "Compile warnings are errors in disguise" + ], + "default": [ + "Write clean, readable code", + "Test early, test often", + "Documentation is never optional", + "Version control is essential", + "Code reviews make better developers" + ] + } + + wisdom_list = wisdom_dict.get(language, wisdom_dict["default"]) + message = random.choice(wisdom_list) + full_message = f"{language} wisdom: {message}" + + bubble = make_bubble(full_message) + print(f"{bubble}\n{ascii_art}") + + return message + +def jokes (n=1): + randomSelect = random.choices(allJokes, k=n) + + bubble_text = "\n".join(randomSelect) + + bubble = make_bubble(bubble_text) + print(f"{bubble}\n{ascii_art}") + + return randomSelect + +def study_tip(hours_available=2, difficulty="medium"): + if hours_available < 0: + raise ValueError("Hours must be non-negative") + + difficulty = difficulty.lower() + if difficulty not in ["easy", "medium", "hard"]: + difficulty = "medium" + + tips = { + "easy": [ + "Quick review session should do it!", + "Focus on the key concepts", + "Practice a few examples", + "Make sure you understand the basics" + ], + "medium": [ + "Break it into manageable chunks", + "Practice problems are essential", + "Review your notes thoroughly", + "Try explaining it to someone else" + ], + "hard": [ + "Start early, don't cram!", + "Work through multiple examples", + "Seek help during office hours", + "Form a study group if possible", + "Break down complex problems step by step" + ] + } + + base_tip = random.choice(tips[difficulty]) + + if hours_available < 1: + time_advice = "Time is tight! Focus on the most important concepts." + elif hours_available < 3: + time_advice = "You have decent time. Use it wisely!" + else: + time_advice = "Great! You have plenty of time to master this." + + message = f"{time_advice}\n{base_tip}" + + bubble = make_bubble(message) + print(f"{bubble}\n{ascii_art}") + + return base_tip diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_bubble.py b/tests/test_bubble.py new file mode 100644 index 0000000..e5a98a8 --- /dev/null +++ b/tests/test_bubble.py @@ -0,0 +1,103 @@ +import pytest + +from bloomsays.bubble import make_bubble, wrap_text + + +def _assert_bubble_matches(out: str, expected_lines: list[str]): + parts = out.splitlines() + # top + maxw = max((len(l) for l in expected_lines), default=0) + assert parts[0] == " " + "_" * (maxw + 2) + + # body + body = parts[1 : 1 + len(expected_lines)] + assert len(body) == len(expected_lines) + for idx, (got, expect) in enumerate(zip(body, expected_lines)): + # the first body line is prefixed with a single space in the output + if idx == 0: + assert got.startswith(" | ") + inner = got[3:-2] + else: + # subsequent lines start with '|' directly + assert got.startswith("| ") + inner = got[2:-2] + assert got.endswith(" |") + assert inner.strip() == expect + + # bottom + bottom = parts[1 + len(expected_lines)] + assert bottom == " " + "=" * (maxw + 2) + + # tail should be two lines that each end with a backslash + assert parts[-2].endswith("\\") + assert parts[-1].endswith("\\") + + +def test_simple_bubble_no_width(): + out = make_bubble("Hi") + _assert_bubble_matches(out, ["Hi"]) + + +def test_wrap_with_width(): + out = make_bubble("one two three", width=6) + # wrapped into three lines + _assert_bubble_matches(out, ["one", "two", "three"]) + + +def test_preserve_empty_line(): + out = make_bubble("a\n\nb") + # empty paragraph should be preserved as an empty line + _assert_bubble_matches(out, ["a", "", "b"]) + + +def test_text_none_raises(): + with pytest.raises(ValueError): + make_bubble(None) + + +# Tests for wrap_text function +def test_wrap_text_simple(): + result = wrap_text("hello world", 10) + assert result == ["hello", "world"] + + +def test_wrap_text_fits_on_one_line(): + result = wrap_text("hello", 10) + assert result == ["hello"] + + +def test_wrap_text_multiple_words_fit(): + result = wrap_text("one two three four", 15) + assert result == ["one two three", "four"] + + +def test_wrap_text_long_word_exceeds_width(): + # A single word longer than width should be on its own line + result = wrap_text("short verylongword short", 10) + assert result == ["short", "verylongword", "short"] + + +def test_wrap_text_empty_string(): + result = wrap_text("", 10) + assert result == [""] + + +def test_wrap_text_width_one(): + result = wrap_text("a b c", 1) + assert result == ["a", "b", "c"] + + +def test_wrap_text_invalid_width_zero(): + with pytest.raises(ValueError, match="width must be >= 1"): + wrap_text("text", 0) + + +def test_wrap_text_invalid_width_negative(): + with pytest.raises(ValueError, match="width must be >= 1"): + wrap_text("text", -5) + + +def test_wrap_text_invalid_width_none(): + with pytest.raises(ValueError, match="width must be >= 1"): + wrap_text("text", None) + diff --git a/tests/test_wisdom.py b/tests/test_wisdom.py new file mode 100644 index 0000000..80caa11 --- /dev/null +++ b/tests/test_wisdom.py @@ -0,0 +1,173 @@ +import pytest +from bloomsays import wisdom + +class Tests: + + def test_avg_simple(self, capsys): + wisdom.avg(97, 76, 67) + captured = capsys.readouterr() + assert "Your average grade is 80.00" in captured.out + assert "____" in captured.out + assert "@@@@" in captured.out + + def test_avg_identical_numbers(self, capsys): + wisdom.avg(67, 67, 67) + captured = capsys.readouterr() + assert "Your average grade is 67.00" in captured.out + assert "____" in captured.out + assert "@@@@" in captured.out + + def test_avg_random_floats(self, capsys): + wisdom.avg(5.5, 7.3, 8.2) + captured = capsys.readouterr() + assert "Your average grade is 7.00" in captured.out + assert "____" in captured.out + assert "@@@@" in captured.out + + def test_random_quote_runs(self, capsys): + wisdom.random_quote(3) + captured = capsys.readouterr() + assert "____" in captured.out + assert "@@@@" in captured.out + + def test_random_quote_default(self, capsys): + wisdom.random_quote() + captured = capsys.readouterr() + assert any("|" in line for line in captured.out.splitlines()) + assert "@@@@" in captured.out + + def test_random_quote_multiple_quotes_in_bubble(self, capsys): + wisdom.random_quote(2) + captured = capsys.readouterr() + lines = captured.out.splitlines() + bubble_lines = [line for line in lines if "|" in line and "Your" not in line and "wisdom" not in line and "bloomsays" not in line] + assert len(bubble_lines) >= 2 + assert "@@@@" in captured.out + + def test_coding_wisdom_default(self, capsys): + message = wisdom.coding_wisdom() + captured = capsys.readouterr() + assert isinstance(message, str) + assert "Python wisdom:" in captured.out + assert "@@@" in captured.out + + def test_coding_wisdom_javascript(self, capsys): + message = wisdom.coding_wisdom("JavaScript") + captured = capsys.readouterr() + assert isinstance(message, str) + assert "JavaScript wisdom:" in captured.out + assert "@@@" in captured.out + + def test_coding_wisdom_java(self, capsys): + message = wisdom.coding_wisdom("Java") + captured = capsys.readouterr() + assert isinstance(message, str) + assert "Java wisdom:" in captured.out + assert "@@@" in captured.out + + def test_coding_wisdom_cpp(self, capsys): + message = wisdom.coding_wisdom("C++") + captured = capsys.readouterr() + assert isinstance(message, str) + assert "C++ wisdom:" in captured.out + assert "@@@" in captured.out + + def test_coding_wisdom_unknown_language(self, capsys): + message = wisdom.coding_wisdom("COBOL") + captured = capsys.readouterr() + assert isinstance(message, str) + assert "COBOL wisdom:" in captured.out + assert "@@@" in captured.out + + def test_coding_wisdom_returns_string(self): + message = wisdom.coding_wisdom("Python") + assert isinstance(message, str) + assert len(message) > 0 + + def test_jokes_default(self, capsys): + getJoke = wisdom.jokes() + getReturn = capsys.readouterr() + + assert isinstance(getJoke, list) + assert len(getJoke) == 1 + assert getJoke[0] in wisdom.allJokes + assert "____" in getReturn.out + assert "@@@@" in getReturn.out + + def test_joke_true_value(self): + numJokes = 2 + output = wisdom.jokes(n=2) + + assert isinstance(output, list) + assert len(output) == numJokes + assert all(isinstance(i, str) for i in output) + + def test_jokes_multiple (self, capsys): + numJokes = 3 + getJokes = wisdom.jokes(n=numJokes) + getReturn = capsys.readouterr() + + assert isinstance(getJokes, list) + assert len(getJokes) == numJokes + for joke in getJokes: + assert joke in wisdom.allJokes + + assert "____" in getReturn.out + assert "@@@@" in getReturn.out + + numJokesLine = [line for line in getReturn.out.splitlines() if "|" in line] + assert len(numJokesLine) >= numJokes + + def test_study_tip_default(self, capsys): + tip = wisdom.study_tip() + captured = capsys.readouterr() + assert isinstance(tip, str) + assert "@@@" in captured.out + assert "|" in captured.out + + def test_study_tip_easy(self, capsys): + tip = wisdom.study_tip(2, "easy") + captured = capsys.readouterr() + assert isinstance(tip, str) + assert "@@@" in captured.out + + def test_study_tip_medium(self, capsys): + tip = wisdom.study_tip(3, "medium") + captured = capsys.readouterr() + assert isinstance(tip, str) + assert "@@@" in captured.out + + def test_study_tip_hard(self, capsys): + tip = wisdom.study_tip(5, "hard") + captured = capsys.readouterr() + assert isinstance(tip, str) + assert "@@@" in captured.out + + def test_study_tip_short_time(self, capsys): + tip = wisdom.study_tip(0.5, "hard") + captured = capsys.readouterr() + assert "Time is tight" in captured.out + + def test_study_tip_long_time(self, capsys): + tip = wisdom.study_tip(10, "easy") + captured = capsys.readouterr() + assert "plenty of time" in captured.out + + def test_study_tip_negative_hours(self): + with pytest.raises(ValueError, match="Hours must be non-negative"): + wisdom.study_tip(-1, "medium") + + def test_study_tip_invalid_difficulty(self, capsys): + tip = wisdom.study_tip(2, "impossible") + captured = capsys.readouterr() + assert isinstance(tip, str) + assert "@@@" in captured.out + + def test_study_tip_case_insensitive(self, capsys): + tip = wisdom.study_tip(2, "HARD") + captured = capsys.readouterr() + assert isinstance(tip, str) + assert "@@@" in captured.out + + +