From 87135f5a14bc825037238a58f069965677cd058f Mon Sep 17 00:00:00 2001 From: jimmy-seraph1080 Date: Mon, 20 Oct 2025 03:08:49 -0400 Subject: [PATCH 1/4] add set up --- pyproject.toml | 18 ++++++++++++++++++ src/githelp/_init_.py | 2 ++ src/githelp/cli.py | 19 +++++++++++++++++++ src/githelp/greeter.py | 6 ++++++ 4 files changed, 45 insertions(+) create mode 100644 pyproject.toml create mode 100644 src/githelp/_init_.py create mode 100644 src/githelp/cli.py create mode 100644 src/githelp/greeter.py diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..522f6c4 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,18 @@ +#how the project is built +[build-system] +requires = ["setuptools>=61.0"] +build-backend = "setuptools.build_meta" + +#project's metadata +[project] +name = "githelp" +version = "0.1.0" +description = "example CLI using Click." +authors = [{ name = "Jimmy Zhang", email = "jimmy_zhang@uri.edu" }] +readme = "README.md" +requires-python = ">=3.9" +dependencies = ["click>=8.1"] + +# This creates the 'githelp' command +[project.scripts] +githelp = "githelp.cli:main" \ No newline at end of file diff --git a/src/githelp/_init_.py b/src/githelp/_init_.py new file mode 100644 index 0000000..469e977 --- /dev/null +++ b/src/githelp/_init_.py @@ -0,0 +1,2 @@ +__all__ = ["__version__"] +__version__ = "0.1.0" \ No newline at end of file diff --git a/src/githelp/cli.py b/src/githelp/cli.py new file mode 100644 index 0000000..7fb55dc --- /dev/null +++ b/src/githelp/cli.py @@ -0,0 +1,19 @@ +#import the click library for the building cli interfaces +import click +#import the inner function that actually builds the greeting text +#keynote ".greater" is a relative import not an absolute. +from .greeter import make_greeting + +#declare the click command +#the text in help show up in the githelp --help +@click.command(help="Say hello from githelp.") +#option "--name is define" and "-n" for short +#default= World if user didnt provide a name +#show default flag is set to false so it doesnt display in option +@click.option("--name", "-n", default="World", show_default=False, help="Who to greet.") +#here there is a boolean flag pair --shout/--no-shout" +@click.option("--shout/--no-shout", default=False, show_default=False, help="End with an exclamation mark.") +#click auto parse the value +def main(name: str, shout: bool) -> None: + # Call the inner function to build the message, then print it to stdout + click.echo(make_greeting(name, shout)) \ No newline at end of file diff --git a/src/githelp/greeter.py b/src/githelp/greeter.py new file mode 100644 index 0000000..d4a4321 --- /dev/null +++ b/src/githelp/greeter.py @@ -0,0 +1,6 @@ +def make_greeting(name, shout = False): + if(shout): + suffix = "!" + else: + suffix = "." + return f"Hello, {name}{suffix}" \ No newline at end of file From 4df0403502997c77e172b17568d6e56ee3a17609 Mon Sep 17 00:00:00 2001 From: Jimmy Date: Mon, 20 Oct 2025 15:54:03 -0400 Subject: [PATCH 2/4] add setup --- pyproject.toml | 2 +- src/githelp/cli.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 522f6c4..70d6a07 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,6 +13,6 @@ readme = "README.md" requires-python = ">=3.9" dependencies = ["click>=8.1"] -# This creates the 'githelp' command +#this creates the 'githelp' command [project.scripts] githelp = "githelp.cli:main" \ No newline at end of file diff --git a/src/githelp/cli.py b/src/githelp/cli.py index 7fb55dc..11c6c9c 100644 --- a/src/githelp/cli.py +++ b/src/githelp/cli.py @@ -15,5 +15,5 @@ @click.option("--shout/--no-shout", default=False, show_default=False, help="End with an exclamation mark.") #click auto parse the value def main(name: str, shout: bool) -> None: - # Call the inner function to build the message, then print it to stdout + #call the inner function to build the message, then print it to stdout click.echo(make_greeting(name, shout)) \ No newline at end of file From 8e4b03f4262654f79565c0c3cf44d9cb0fafde94 Mon Sep 17 00:00:00 2001 From: Jimmy Date: Sun, 26 Oct 2025 15:41:02 -0400 Subject: [PATCH 3/4] week2 version 0.1 --- MANIFEST.in | 1 + pyproject.toml | 15 ++++- src/githelp/cli.py | 45 ++++++++----- src/githelp/data/overlays/pull.yml | 6 ++ src/githelp/greeter.py | 6 -- src/githelp/overlays.py | 102 +++++++++++++++++++++++++++++ 6 files changed, 149 insertions(+), 26 deletions(-) create mode 100644 MANIFEST.in create mode 100644 src/githelp/data/overlays/pull.yml delete mode 100644 src/githelp/greeter.py create mode 100644 src/githelp/overlays.py diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..23c5580 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1 @@ +recursive-include src/githelp/data/overlays *.yml \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 70d6a07..b8c2cd3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,8 +11,17 @@ description = "example CLI using Click." authors = [{ name = "Jimmy Zhang", email = "jimmy_zhang@uri.edu" }] readme = "README.md" requires-python = ">=3.9" -dependencies = ["click>=8.1"] +dependencies = ["click>=8.1", "pyyaml>=6.0"] -#this creates the 'githelp' command +#this creates the 'githelp' command that point to the click group [project.scripts] -githelp = "githelp.cli:main" \ No newline at end of file +githelp = "githelp.cli:main" + +#tell the setuptool to include non py file +[tool.setuptools] +include-package-data = true + +#include the yml file inside the install so that it can be found in the runtime +[tool.setuptools.package-data] +githelp = ["data/overlays/*.yml"] + diff --git a/src/githelp/cli.py b/src/githelp/cli.py index 11c6c9c..d502e7b 100644 --- a/src/githelp/cli.py +++ b/src/githelp/cli.py @@ -1,19 +1,30 @@ -#import the click library for the building cli interfaces import click -#import the inner function that actually builds the greeting text -#keynote ".greater" is a relative import not an absolute. -from .greeter import make_greeting +from .overlays import load_overlay, render_overlay, render_menu -#declare the click command -#the text in help show up in the githelp --help -@click.command(help="Say hello from githelp.") -#option "--name is define" and "-n" for short -#default= World if user didnt provide a name -#show default flag is set to false so it doesnt display in option -@click.option("--name", "-n", default="World", show_default=False, help="Who to greet.") -#here there is a boolean flag pair --shout/--no-shout" -@click.option("--shout/--no-shout", default=False, show_default=False, help="End with an exclamation mark.") -#click auto parse the value -def main(name: str, shout: bool) -> None: - #call the inner function to build the message, then print it to stdout - click.echo(make_greeting(name, shout)) \ No newline at end of file +@click.group( + help="githelp — terminal-first Git helper.", + invoke_without_command=True, + add_help_option=False, +) +@click.option("-h", "--help", "show_help", is_flag=True, help="Provides option menu") +@click.pass_context +def main(context, show_help): + if show_help or context.invoked_subcommand is None: + click.echo(context.get_help()) + click.echo() + click.echo(render_menu()) + if context.invoked_subcommand is None: + context.exit(0) + +@main.command(name="list", help="List of available githelp tip pages.") +def list_cmd(): + click.echo(render_menu()) + +@main.command(name="run", help="Show githelp overlay tips for a git subcommand.") +@click.argument("cmd") +def run_cmd(cmd): + tips = load_overlay(cmd) + if tips: + click.echo(render_overlay(tips)) + else: + click.echo(f"githelp tips\n\nNo tips found for '{cmd}'.") \ No newline at end of file diff --git a/src/githelp/data/overlays/pull.yml b/src/githelp/data/overlays/pull.yml new file mode 100644 index 0000000..4aaebdd --- /dev/null +++ b/src/githelp/data/overlays/pull.yml @@ -0,0 +1,6 @@ +command: pull +summary: "A git command that used to fetch and download from a remote Repo and immediately update the local Repo. It is basically a combination of two commands git fetch and git merge." +when_to_use: + - "You want to use to get the latest update on your local repo." +examples: + - { cmd: "git pull", say: "Fetches from your branch’s tracked remote (usually 'origin') and merges into your current branch." } \ No newline at end of file diff --git a/src/githelp/greeter.py b/src/githelp/greeter.py deleted file mode 100644 index d4a4321..0000000 --- a/src/githelp/greeter.py +++ /dev/null @@ -1,6 +0,0 @@ -def make_greeting(name, shout = False): - if(shout): - suffix = "!" - else: - suffix = "." - return f"Hello, {name}{suffix}" \ No newline at end of file diff --git a/src/githelp/overlays.py b/src/githelp/overlays.py new file mode 100644 index 0000000..aefb052 --- /dev/null +++ b/src/githelp/overlays.py @@ -0,0 +1,102 @@ +import os +import yaml + +# build absolute path to githelp/data/overlays +THIS_DIR = os.path.dirname(os.path.abspath(__file__)) +DATA_DIR = os.path.join(THIS_DIR, "data", "overlays") + +def load_overlay(command: str): + fname = f"{command}.yml" + path = os.path.join(DATA_DIR, fname) + try: + #try opening the file and parse the yaml to python object + #default to {} if empty + with open(path, "rb") as f: + data = yaml.safe_load(f) or {} + #expect dict at the top level + if not isinstance(data, dict): + return None + #a check: if yaml includes the command key and if command doesnt match command then return none + if data.get("command") and data["command"] != command: + return None + #return the parsed, validated dictionary + return data + #if the yaml file doesnt exist then return none + except FileNotFoundError: + return None + #if any other error then return none + except Exception: + return None +#basically a list of command for tips +def list_overlay_names(): + #if the overlays folder doesnt exist then return empty list + if not os.path.isdir(DATA_DIR): + return [] + #names variable stores an empty list + names = [] + #a for loop that iterate over single file in the overlays directory + for entry in os.listdir(DATA_DIR): + #build a full path for this entry + full_path = os.path.join(DATA_DIR, entry) + #if the path ends with .yml then strip it and then append it + if os.path.isfile(full_path) and entry.endswith(".yml"): + # strip .yml + names.append(entry[:-4]) + #sort the names to make it easier to find + names.sort() + #return the names + return names + +def render_overlay(d: dict) -> str: + #line variable that stores a list of outputs + lines = [] + #append header + lines.append("\ngithelp tips") + + #get the summary, when to use, and example in the dictionary + summary = d.get("summary") + when_to_use = d.get("when_to_use") + examples = d.get("examples") + + #summary = true then append the summary text to line list + if summary: + lines.append("") + lines.append(summary) + #when to use = true then append the item in the when to use to line list + if when_to_use: + lines.append("\nWhen to use") + for item in d["when_to_use"]: + lines.append(f" - {item}") + #example = true then for each example(ex) in examples append to line list + if examples: + lines.append("\nExamples") + for ex in d["examples"]: + #get the cmd and say + cmd = ex.get("cmd", "") + say = ex.get("say", "") + lines.append(f" $ {cmd}") + # If say exists, indent it by 3 spaces + if say: + lines.append(f" {say}") + #append a new line at the end + lines.append("") + #join all lines into a single string and return it + return "\n".join(lines) + +def render_menu(): + #declare an empty list called lines + lines = [] + #display commands + lines.append("Commands:") + lines.append(" - run Show tips for a git subcommand") + lines.append(" - list List available tip pages\n") + + #list all overlays found in data/overlays + names = list_overlay_names() + if names: + lines.append("Available tip pages:") + for n in names: + lines.append(f" - {n}") + else: + lines.append("No tip pages found.") + return "\n".join(lines) \ No newline at end of file From 0c3dadc094e9001d42dd166baaec6edeea64ce75 Mon Sep 17 00:00:00 2001 From: Jimmy Date: Mon, 17 Nov 2025 00:04:41 -0500 Subject: [PATCH 4/4] fix tips.yml, adjust code, and added numpydoc --- pyproject.toml | 2 +- src/githelp/cli.py | 12 ++- src/githelp/data/overlays/pull.yml | 6 -- src/githelp/data/overlays/tips.yml | 135 +++++++++++++++++++++++++ src/githelp/overlays.py | 155 +++++++++++++++++++++-------- 5 files changed, 258 insertions(+), 52 deletions(-) delete mode 100644 src/githelp/data/overlays/pull.yml create mode 100644 src/githelp/data/overlays/tips.yml diff --git a/pyproject.toml b/pyproject.toml index b8c2cd3..cca40b6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,7 +15,7 @@ dependencies = ["click>=8.1", "pyyaml>=6.0"] #this creates the 'githelp' command that point to the click group [project.scripts] -githelp = "githelp.cli:main" +githelp = "githelp.cli:githelp_cli" #tell the setuptool to include non py file [tool.setuptools] diff --git a/src/githelp/cli.py b/src/githelp/cli.py index d502e7b..d3d3f54 100644 --- a/src/githelp/cli.py +++ b/src/githelp/cli.py @@ -2,13 +2,13 @@ from .overlays import load_overlay, render_overlay, render_menu @click.group( - help="githelp — terminal-first Git helper.", + help="githelp terminal first Git helper.", invoke_without_command=True, add_help_option=False, ) @click.option("-h", "--help", "show_help", is_flag=True, help="Provides option menu") @click.pass_context -def main(context, show_help): +def githelp_cli(context, show_help): if show_help or context.invoked_subcommand is None: click.echo(context.get_help()) click.echo() @@ -16,13 +16,15 @@ def main(context, show_help): if context.invoked_subcommand is None: context.exit(0) -@main.command(name="list", help="List of available githelp tip pages.") +@githelp_cli.command(name="list", help="List of available githelp tip pages.") def list_cmd(): + '''List all available githelp tip.''' click.echo(render_menu()) -@main.command(name="run", help="Show githelp overlay tips for a git subcommand.") +@githelp_cli.command(name="explain", help="Show githelp overlay tips for a git subcommand.") @click.argument("cmd") -def run_cmd(cmd): +def explain_cmd(cmd): + '''Show tips for a specific git subcommand''' tips = load_overlay(cmd) if tips: click.echo(render_overlay(tips)) diff --git a/src/githelp/data/overlays/pull.yml b/src/githelp/data/overlays/pull.yml deleted file mode 100644 index 4aaebdd..0000000 --- a/src/githelp/data/overlays/pull.yml +++ /dev/null @@ -1,6 +0,0 @@ -command: pull -summary: "A git command that used to fetch and download from a remote Repo and immediately update the local Repo. It is basically a combination of two commands git fetch and git merge." -when_to_use: - - "You want to use to get the latest update on your local repo." -examples: - - { cmd: "git pull", say: "Fetches from your branch’s tracked remote (usually 'origin') and merges into your current branch." } \ No newline at end of file diff --git a/src/githelp/data/overlays/tips.yml b/src/githelp/data/overlays/tips.yml new file mode 100644 index 0000000..cbc5266 --- /dev/null +++ b/src/githelp/data/overlays/tips.yml @@ -0,0 +1,135 @@ +pull: + command: pull + summary: "A git command that fetches from a remote repository and merges into the current branch (fetch + merge)." + when_to_use: + - "You want to get the latest updates into your local branch." + examples: + - cmd: "git pull" + say: "Fetches from your branch's tracked remote (usually 'origin') and merges into your current branch." + +commit: + command: commit + summary: "Create a snapshot of staged changes; keep messages short and imperative." + when_to_use: + - "You have staged changes and want to record them." + examples: + - cmd: "git commit -m \"messages\"" + say: "Short, informative summary." + +add: + command: add + summary: "Stages changes like new, modified, or deleted files to be included in the next commit. This also moves files from working directory to the staging area." + when_to_use: + - "When you want to stage changes this could include new, modified, or deleted files to the repo." + examples: + - cmd: "git add ." + say: "add all file" + - cmd: "git add " + say: "add a file" + +status: + command: status + summary: "command provides an overview of the current state of your Git repository. this is done by checking pointer and compare them to the current directory" + when_to_use: + - "when you want to check the differences/changes of current vs before" + examples: + - cmd: "git status" + say: "overview changes" + +clone: + command: clone + summary: "Creates a copy of a remote repository on your local machine." + when_to_use: + - "you want to use this to create a local copy of an existing Git repository" + examples: + - cmd: "git clone " + - say: "Downloads the remote repo into a new local folder." + +checkout: + command: checkout + summary: "switch between branch" + when_to_use: + - "you want to use this to navigate between difference branches" + examples: + - cmd: "git checkout " + say: "Switches your working directory to the specified branch." + +branch: + command: branch + summary: "Lists all branches in your repository" + when_to_use: + - "you use this to check what branch you're on" + examples: + - cmd: "git branch" + say: "Shows all local branches and highlights the one you're on." + - cmd: "git branch -m " + say: "Renames the current branch to the new name." + +logs: + command: logs + summary: "Displays the commit history of the repository. " + when_to_use: + - "you want to use this to see commit hashes, authors, dates, and messages." + examples: + - cmd: git logs + say: "information of commit history" + +tag: + command: tag + summary: "command in Git is used to mark specific points in a repository's history as important." + when_to_use: + - "use this to mark a particular point in the commit ancestry chain." + examples: + - cmd: git tag + say: "Lists all tags that mark important commits in the repo." + +rebase: + command: rebase + summary: "Moves or reapplies commits from one branch on top of another to keep history linear." + when_to_use: + - "You want to update your feature branch on top of the latest main or another branch." + - "You want a cleaner, straight line commit history instead of a lot of merge commits." + examples: + - cmd: "git rebase main" + say: "Replay your current branch’s commits on top of the latest 'main' branch." + - cmd: "git rebase origin/main" + say: "Rebase your work on top of the 'origin/main' branch from the remote." + +fetch: + command: fetch + summary: "Downloads new commits, branches, and tags from the remote without changing local branches or files." + when_to_use: + - "You want to see what changed on the remote before merging or rebasing." + - "You want to update remote-tracking branches like origin/main without touching your current branch." + examples: + - cmd: "git fetch" + say: "Gets all new data from the default remote (usually 'origin') without modifying your current branch." + - cmd: "git fetch origin main" + say: "Fetches only updates for the 'main' branch from 'origin'." + +push: + command: push + summary: "Sends your local commits to a remote repository branch." + when_to_use: + - "You want to upload your local commits so they appear on GitHub or another remote." + - "You want to share your changes with others or back them up remotely." + examples: + - cmd: "git push" + say: "Pushes your current branch to its tracked remote branch." + - cmd: "git push origin " + say: "Pushes the specified local branch to the 'origin' remote." + +merge: + command: merge + summary: "Combines another branch into the current branch by creating a merge commit." + when_to_use: + - "You want to bring changes from one branch into another and keep full history from both." + - "You want to integrate a completed feature branch into main (or another base branch)." + examples: + - cmd: "git merge " + say: "Merges the named branch into your current branch, creating a merge commit." + - cmd: "git merge main" + say: "Brings the latest changes from 'main' into your current branch." + + + diff --git a/src/githelp/overlays.py b/src/githelp/overlays.py index aefb052..897b20c 100644 --- a/src/githelp/overlays.py +++ b/src/githelp/overlays.py @@ -1,53 +1,115 @@ import os import yaml -# build absolute path to githelp/data/overlays +#build absolute path to githelp/data/overlays THIS_DIR = os.path.dirname(os.path.abspath(__file__)) DATA_DIR = os.path.join(THIS_DIR, "data", "overlays") +#master dictionary file: to /src/githelp/data/overlays/tips.yml +MASTER_FILE = os.path.join(DATA_DIR, "tips.yml") -def load_overlay(command: str): - fname = f"{command}.yml" - path = os.path.join(DATA_DIR, fname) + +def read_yaml(path: str): + ''' + Read a YAML file into a Python object. + + Parameters + ---------- + path : str + Path to the YAML file to read. + + Returns + ------- + dict + Parsed YAML contents. Returns an empty dict if the file is empty + or if any error occurs while reading/parsing. + ''' try: - #try opening the file and parse the yaml to python object - #default to {} if empty - with open(path, "rb") as f: + #r opens the file for reading in text mode, and encoding="utf-8" tells Python to decode the file's bytes + #as UTF-8 text into normal Python strings + with open(path, "r", encoding="utf-8") as f: + #convert YAML content to Python object or empty dict if file is empty data = yaml.safe_load(f) or {} - #expect dict at the top level - if not isinstance(data, dict): - return None - #a check: if yaml includes the command key and if command doesnt match command then return none - if data.get("command") and data["command"] != command: - return None - #return the parsed, validated dictionary + #return the data of whatever was loaded return data - #if the yaml file doesnt exist then return none - except FileNotFoundError: - return None - #if any other error then return none except Exception: - return None -#basically a list of command for tips + #if error occurs return empty dict + return {} + +def helper(): + + data = read_yaml(MASTER_FILE) + return data + +def load_overlay(command: str): + ''' + Get the tips for one git subcommand. + + This first looks in the master file tips.yml under the given + command name (ex "pull"). If found, it returns the corresponding + dictionary of tips. + + Parameters + ---------- + command : str + Name of the git subcommand whose overlay should be loaded. + + Returns + ------- + dict or None + The overlay dictionary for the given command, with a ``"command"`` key + ensured, or ``None`` if no overlay is defined. + ''' + #set data to the master dictionary file + data = helper() + #check to see if the master is a dict and not empty + if isinstance(data, dict): + #get the section for this specific command and store it in section + section = data.get(command) + #check if section is a dict + if isinstance(section, dict): + # make sure the section has a command key + # does nothing if "command" already exists + section.setdefault("command", command) + #return the section + return section + + def list_overlay_names(): - #if the overlays folder doesnt exist then return empty list - if not os.path.isdir(DATA_DIR): - return [] - #names variable stores an empty list + ''' + List available subcommand names from tips.yml. + + Returns + ------- + list of str + Sorted list of subcommand names found in the master tips mapping. + ''' names = [] - #a for loop that iterate over single file in the overlays directory - for entry in os.listdir(DATA_DIR): - #build a full path for this entry - full_path = os.path.join(DATA_DIR, entry) - #if the path ends with .yml then strip it and then append it - if os.path.isfile(full_path) and entry.endswith(".yml"): - # strip .yml - names.append(entry[:-4]) - #sort the names to make it easier to find + data = helper() + #check if data is a dict and not empty + if not isinstance(data, dict): + return names + + for key, value in data.items(): + if isinstance(value, dict): + names.append(key) + names.sort() - #return the names return names - def render_overlay(d: dict) -> str: + ''' + Render a single overlay dictionary into a human-readable text block. + + Parameters + ---------- + d : dict + Overlay dictionary containing keys such as ``"summary"``, + ``"when_to_use"``, and ``"examples"`` (any of which may be absent). + + Returns + ------- + str + A formatted multi-line string + ''' #line variable that stores a list of outputs lines = [] #append header @@ -67,15 +129,16 @@ def render_overlay(d: dict) -> str: lines.append("\nWhen to use") for item in d["when_to_use"]: lines.append(f" - {item}") - #example = true then for each example(ex) in examples append to line list + #example = true then append new line and example into line if examples: lines.append("\nExamples") + #for each example in d["examples"] for ex in d["examples"]: #get the cmd and say cmd = ex.get("cmd", "") say = ex.get("say", "") lines.append(f" $ {cmd}") - # If say exists, indent it by 3 spaces + # If say exists, indent it by 3 spaces for formatting if say: lines.append(f" {say}") #append a new line at the end @@ -84,19 +147,31 @@ def render_overlay(d: dict) -> str: return "\n".join(lines) def render_menu(): + ''' + Render the main menu text for githelp. + + Returns + ------- + str + A formatted multi-line string describing available commands. + ''' #declare an empty list called lines lines = [] #display commands lines.append("Commands:") - lines.append(" - run Show tips for a git subcommand") - lines.append(" - list List available tip pages\n") + lines.append(" - explain Show an explaination for a git subcommand") + lines.append(" - list List available tip pages\n") - #list all overlays found in data/overlays + #name variable that stores the list of overlay names names = list_overlay_names() + #if name is true then append the available tip pages to line list if names: lines.append("Available tip pages:") + #for each name in names for n in names: + #append each name with a dash and space before it for formatting lines.append(f" - {n}") else: + #if no names found append no tip pages found to line list lines.append("No tip pages found.") return "\n".join(lines) \ No newline at end of file