diff --git a/pyproject.toml b/pyproject.toml index 4b86ab10..b815940f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api" [tool.poetry] name = "readmeai" -version = "0.4.0987" +version = "0.4.0988" description = "๐ Generate beautiful README.md files from the terminal using GPT LLM APIs ๐ซ" authors = ["Eli <0x.eli.64s@gmail.com>"] license = "MIT" @@ -16,11 +16,11 @@ keywords = [ "markdown", "readme", "ai", + "generator", "devtools", "documentation", "documentation-generator", "readme-md", - "automated-documentation", "readme-generator", "openai-api", "automated-readme", diff --git a/readmeai/cli/options.py b/readmeai/cli/options.py index 02182012..1b764545 100644 --- a/readmeai/cli/options.py +++ b/readmeai/cli/options.py @@ -8,7 +8,7 @@ import click from click import Context, Parameter -from readmeai.config.settings import BadgeOptions, ImageOptions +from readmeai.config.enums import BadgeOptions, ImageOptions def prompt_for_custom_image( diff --git a/readmeai/config/enums.py b/readmeai/config/enums.py new file mode 100644 index 00000000..b2bd544f --- /dev/null +++ b/readmeai/config/enums.py @@ -0,0 +1,62 @@ +"""Enum classes for the readmeai package.""" + +from enum import Enum + + +class GitService(str, Enum): + """ + Enum class for Git service details. + """ + + LOCAL = "local" + GITHUB = "github.com" + GITLAB = "gitlab.com" + BITBUCKET = "bitbucket.org" + + @property + def api_url(self): + """Gets the API URL for the Git service.""" + return { + GitService.LOCAL: None, + GitService.GITHUB: "https://api.github.com/repos/", + GitService.GITLAB: "https://api.gitlab.com/v4/projects/", + GitService.BITBUCKET: "https://api.bitbucket.org/2.0/repositories/", + }.get(self, None) + + @property + def file_url_template(self): + """Gets the file URL template for the Git service.""" + return { + GitService.LOCAL: "{file_path}", + GitService.GITHUB: "https://github.com/{full_name}/blob/main/{file_path}", + GitService.GITLAB: "https://gitlab.com/{full_name}/-/blob/master/{file_path}", + GitService.BITBUCKET: "https://bitbucket.org/{full_name}/src/master/{file_path}", + }.get(self, None) + + +class BadgeOptions(str, Enum): + """ + Enum for CLI options for README file badge icons. + """ + + DEFAULT = "default" + FLAT = "flat" + FLAT_SQUARE = "flat-square" + FOR_THE_BADGE = "for-the-badge" + PLASTIC = "plastic" + SKILLS = "skills" + SKILLS_LIGHT = "skills-light" + SOCIAL = "social" + + +class ImageOptions(str, Enum): + """ + Enum for CLI options for README file header images. + """ + + CUSTOM = "CUSTOM" + DEFAULT = "https://raw.githubusercontent.com/PKief/vscode-material-icon-theme/ec559a9f6bfd399b82bb44393651661b08aaf7ba/icons/folder-markdown-open.svg" + BLACK = "https://img.icons8.com/external-tal-revivo-regular-tal-revivo/96/external-readme-is-a-easy-to-build-a-developer-hub-that-adapts-to-the-user-logo-regular-tal-revivo.png" + GREY = "https://img.icons8.com/external-tal-revivo-filled-tal-revivo/96/external-markdown-a-lightweight-markup-language-with-plain-text-formatting-syntax-logo-filled-tal-revivo.png" + PURPLE = "https://img.icons8.com/external-tal-revivo-duo-tal-revivo/100/external-markdown-a-lightweight-markup-language-with-plain-text-formatting-syntax-logo-duo-tal-revivo.png" + YELLOW = "https://img.icons8.com/pulsar-color/96/markdown.png" diff --git a/readmeai/config/settings.py b/readmeai/config/settings.py index 584235a5..121af3f4 100644 --- a/readmeai/config/settings.py +++ b/readmeai/config/settings.py @@ -1,7 +1,6 @@ """Data models and functions for configuring the readme-ai CLI tool.""" -import os -from enum import Enum +import re from importlib import resources from pathlib import Path from typing import Dict, List, Optional, Union @@ -10,107 +9,17 @@ import pkg_resources from pydantic import BaseModel, validator +from readmeai.config.enums import GitService from readmeai.core.factory import FileHandler from readmeai.core.logger import Logger logger = Logger(__name__) -class GitService(str, Enum): - """ - Enum class for Git service details. - """ +class GitSettingsValidator: + """Validator class for GitSettings.""" - LOCAL = ("local", None, "{file_path}") - GITHUB = ( - "github.com", - "https://api.github.com/repos/", - "https://github.com/{full_name}/blob/main/{file_path}", - ) - GITLAB = ( - "gitlab.com", - "https://api.gitlab.com/v4/projects/", - "https://gitlab.com/{full_name}/-/blob/master/{file_path}", - ) - BITBUCKET = ( - "bitbucket.org", - "https://api.bitbucket.org/2.0/repositories/", - "https://bitbucket.org/{full_name}/src/master/{file_path}", - ) - - def __new__(cls, host, api_url, file_url) -> object: - """Create a new instance of the GitService enum.""" - obj = str.__new__(cls, host) - obj._value_ = host - obj.host = host - obj.api_url = api_url - obj.file_url = file_url - return obj - - def extract_name_from_host(repo_host: str) -> str: - """Return the hostname without periods.""" - if repo_host == GitService.LOCAL.host: - return GitService.LOCAL.host - else: - return repo_host.split(".")[0] - - -class BadgeOptions(str, Enum): - """ - Enum for CLI options for README file badge icons. - """ - - DEFAULT = "default" - FLAT = "flat" - FLAT_SQUARE = "flat-square" - FOR_THE_BADGE = "for-the-badge" - PLASTIC = "plastic" - SKILLS = "skills" - SKILLS_LIGHT = "skills-light" - SOCIAL = "social" - - -class ImageOptions(str, Enum): - """ - Enum for CLI options for README file header images. - """ - - CUSTOM = "CUSTOM" - DEFAULT = "https://raw.githubusercontent.com/PKief/vscode-material-icon-theme/ec559a9f6bfd399b82bb44393651661b08aaf7ba/icons/folder-markdown-open.svg" - BLACK = "https://img.icons8.com/external-tal-revivo-regular-tal-revivo/96/external-readme-is-a-easy-to-build-a-developer-hub-that-adapts-to-the-user-logo-regular-tal-revivo.png" - GREY = "https://img.icons8.com/external-tal-revivo-filled-tal-revivo/96/external-markdown-a-lightweight-markup-language-with-plain-text-formatting-syntax-logo-filled-tal-revivo.png" - PURPLE = "https://img.icons8.com/external-tal-revivo-duo-tal-revivo/100/external-markdown-a-lightweight-markup-language-with-plain-text-formatting-syntax-logo-duo-tal-revivo.png" - YELLOW = "https://img.icons8.com/pulsar-color/96/markdown.png" - - -class CliSettings(BaseModel): - """CLI options for the readme-ai application.""" - - emojis: bool = True - offline: bool = False - - -class FileSettings(BaseModel): - """Pydantic model for configuration file paths.""" - - dependency_files: str - identifiers: str - ignore_files: str - language_names: str - language_setup: str - output: str - shields_icons: str - skill_icons: str - - -class GitSettings(BaseModel): - """Codebase repository settings and validations.""" - - repository: Union[str, Path] - source: Optional[str] - name: Optional[str] - - @validator("repository", pre=True, always=True) + @classmethod def validate_repository(cls, value: Union[str, Path]) -> Union[str, Path]: """Validate the repository URL or path.""" if isinstance(value, str): @@ -120,7 +29,7 @@ def validate_repository(cls, value: Union[str, Path]) -> Union[str, Path]: try: parsed_url = urlparse(value) if parsed_url.scheme in ["http", "https"] and any( - service.host in parsed_url.netloc for service in GitService + service in parsed_url.netloc for service in GitService ): return value except ValueError: @@ -129,22 +38,23 @@ def validate_repository(cls, value: Union[str, Path]) -> Union[str, Path]: return value raise ValueError(f"Invalid repository URL or path: {value}") - @validator("source", pre=True, always=True) - def set_source(cls, value: Optional[str], values: dict) -> str: - """Sets the Git service source from the repository provided.""" + @classmethod + def set_host(cls, value: Optional[str], values: dict) -> str: + """Sets the Git service host from the repository provided.""" repo = values.get("repository") if isinstance(repo, Path) or ( isinstance(repo, str) and Path(repo).is_dir() ): - return GitService.LOCAL.host + return GitService.LOCAL parsed_url = urlparse(str(repo)) for service in GitService: - if service.host in parsed_url.netloc: - return service.host - return GitService.LOCAL.host + if service in parsed_url.netloc: + return service.split(".")[0] + + return GitService.LOCAL - @validator("name", pre=True, always=True) + @classmethod def set_name(cls, value: Optional[str], values: dict) -> str: """Sets the repository name from the repository provided.""" repo = values.get("repository") @@ -156,6 +66,91 @@ def set_name(cls, value: Optional[str], values: dict) -> str: return name.removesuffix(".git") return "n/a" + @classmethod + def set_source(cls, value: Optional[str], values: dict) -> str: + repo = values.get("repository") + if isinstance(repo, Path) or ( + isinstance(repo, str) and Path(repo).is_dir() + ): + return GitService.LOCAL + + parsed_url = urlparse(str(repo)) + for service in GitService: + if service in parsed_url.netloc: + return service + return GitService.LOCAL + + @classmethod + def validate_full_name(cls, value: Optional[str], values: dict) -> str: + """Validator for getting the full name of the repository.""" + url_or_path = values.get("repository") + + path = ( + url_or_path if isinstance(url_or_path, Path) else Path(url_or_path) + ) + if path.exists(): + return str(path.name) + + patterns = { + GitService.GITHUB: r"https?://github.com/([^/]+)/([^/]+)", + GitService.GITLAB: r"https?://gitlab.com/([^/]+)/([^/]+)", + GitService.BITBUCKET: r"https?://bitbucket.org/([^/]+)/([^/]+)", + } + + for _, pattern in patterns.items(): + match = re.match(pattern, url_or_path) + if match: + user_name, repo_name = match.groups() + return f"{user_name}/{repo_name}" + + raise ValueError("Error: invalid repository URL or path.") + + +class CliSettings(BaseModel): + """CLI options for the readme-ai application.""" + + emojis: bool + offline: bool + + +class FileSettings(BaseModel): + """File paths for the readme-ai application.""" + + dependency_files: str + identifiers: str + ignore_files: str + language_names: str + language_setup: str + output: str + shields_icons: str + skill_icons: str + + +class GitSettings(BaseModel): + """Codebase repository settings and validations.""" + + repository: Union[str, Path] + full_name: Optional[str] + host: Optional[str] + name: Optional[str] + source: Optional[str] + + _validate_repository = validator("repository", pre=True, always=True)( + GitSettingsValidator.validate_repository + ) + _validate_full_name = validator("full_name", pre=True, always=True)( + GitSettingsValidator.validate_full_name + ) + _set_host = validator("host", pre=True, always=True)( + GitSettingsValidator.set_host + ) + _set_name = validator("name", pre=True, always=True)( + GitSettingsValidator.set_name + ) + _set_source = validator("source", pre=True, always=True)( + GitSettingsValidator.set_source + ) + class LlmApiSettings(BaseModel): """Pydantic model for OpenAI LLM API details.""" @@ -270,21 +265,28 @@ def _get_config_dict(handler: FileHandler, file_path: str) -> dict: """Get configuration dictionary from TOML file.""" try: resource_path = resources.files("readmeai.settings") / file_path - logger.info(f"Resource path using importlib: {resource_path}") + logger.info(f"Using importlib.resources to load: {resource_path}") + except TypeError as exc_info: - logger.debug(f"Error with importlib.resources: {exc_info}") + logger.debug(f"Error using importlib.resources: {exc_info}") + try: resource_path = Path( pkg_resources.resource_filename( "readmeai", f"settings/{file_path}" ) ).resolve() - logger.info(f"Resource path using pkg_resources: {resource_path}") + logger.info( + f"Using pkg_resources.resource_filename: {resource_path}" + ) + except FileNotFoundError as exc_info: - logger.debug(f"Error with pkg_resources: {exc_info}") - raise + logger.debug( + f"Error using pkg_resources.resource_filename: {exc_info}" + ) + raise FileNotFoundError(f"Config file not found: {file_path}") - if not os.path.exists(resource_path): + if not resource_path.exists(): raise FileNotFoundError(f"Config file not found: {resource_path}") return handler.read(resource_path) diff --git a/readmeai/core/preprocess.py b/readmeai/core/preprocess.py index 66d515e9..cdac2f24 100644 --- a/readmeai/core/preprocess.py +++ b/readmeai/core/preprocess.py @@ -3,7 +3,7 @@ from pathlib import Path from typing import Dict, Generator, List, Tuple -from readmeai.config import settings +from readmeai.config import enums, settings from readmeai.core.logger import Logger from readmeai.core.tokens import get_token_count from readmeai.core.utils import flatten_list, should_ignore @@ -27,14 +27,13 @@ def __init__( self.language_names = conf_helper.language_names self.language_setup = conf_helper.language_setup self.encoding_name = config.llm.encoding - self.local_source = settings.GitService.LOCAL.value def analyze(self, temp_dir: str) -> List[Dict]: """Analyzes a local or remote git repository.""" contents = self.generate_contents(temp_dir) repo_source = self.config.git.source - if repo_source != self.local_source: + if repo_source != enums.GitService.LOCAL: logger.info(f"Tokenizing content from host: {repo_source}") contents = self.tokenize_content(contents) @@ -42,6 +41,16 @@ def analyze(self, temp_dir: str) -> List[Dict]: return contents + def get_dependencies( + self, temp_dir: str = None + ) -> Tuple[List[str], Dict[str, str]]: + """Extracts the dependencies of the user's repository.""" + contents = self.analyze(temp_dir) + dependencies = self.get_dependency_file_contents(contents) + attributes = ["extension", "language", "name"] + dependencies.extend(self.get_unique_contents(contents, attributes)) + return list(set(dependencies)), self.get_file_contents(contents) + def generate_contents(self, repo_path: str) -> List[Dict]: """Generates a List of Dict of file information.""" repo_path = Path(repo_path) @@ -79,13 +88,6 @@ def get_dependency_file_contents(self, contents: List[Dict]) -> List[str]: ) return flatten_list(parsed_contents) - def parse_content(content: Dict) -> List[str]: - """Helper function to parse the content of a file.""" - parser = PARSERS.get(content["name"]) - if parser: - return parser.parse(content["content"]) - return [] - def generate_file_info( self, repo_path: Path ) -> Generator[Tuple[str, Path, str], None, None]: @@ -120,27 +122,12 @@ def get_unique_contents( unique_contents = {data[key] for key in keys for data in contents} return list(unique_contents) - def get_dependencies( - self, temp_dir: str = None - ) -> Tuple[List[str], Dict[str, str]]: - """Extracts the dependencies of the user's repository.""" - contents = self.analyze(temp_dir) - dependencies = self.get_dependency_file_contents(contents) - attributes = ["extension", "language", "name"] - dependencies.extend(self.get_unique_contents(contents, attributes)) - return list(set(dependencies)), self.get_file_contents(contents) - def process_language_mapping(self, contents: List[Dict]) -> List[Dict]: """Maps file extensions to their programming languages.""" for content in contents: content["language"] = self.language_names.get( content["extension"], "" ).lower() - setup = self.language_setup.get(content["language"], "") - setup = setup if isinstance(setup, list) else [None, None] - while len(setup) < 3: - setup.append(None) - content["install"], content["run"], content["test"] = setup return contents def tokenize_content(self, contents: List[Dict]) -> List[Dict]: diff --git a/readmeai/main.py b/readmeai/main.py index 61c76107..4179ebce 100644 --- a/readmeai/main.py +++ b/readmeai/main.py @@ -11,20 +11,19 @@ import traceback from readmeai.cli.options import prompt_for_custom_image +from readmeai.config.enums import ImageOptions from readmeai.config.settings import ( AppConfig, AppConfigModel, ConfigHelper, GitSettings, - ImageOptions, load_config, load_config_helper, ) from readmeai.core.logger import Logger from readmeai.core.model import ModelHandler from readmeai.core.preprocess import RepoProcessor -from readmeai.markdown.headers import build_readme_md -from readmeai.markdown.tree import TreeGenerator +from readmeai.markdown.builder import ReadmeBuilder, build_readme_md from readmeai.services.git_operations import clone_repo_to_temp_dir logger = Logger(__name__) @@ -37,14 +36,15 @@ async def readme_agent(conf: AppConfig, conf_helper: ConfigHelper) -> None: try: await clone_repo_to_temp_dir(repo_url, temp_dir) - tree_command(conf, conf_helper, temp_dir) - parser = RepoProcessor(conf, conf_helper) dependencies, file_context = parser.get_dependencies(temp_dir) summaries = [(path, content) for path, content in file_context.items()] + repo_tree = ReadmeBuilder( + conf, conf_helper, dependencies, summaries, temp_dir + ).md_tree logger.info(f"Project dependencies: {dependencies}") - logger.info(f"Project structure:\n{conf.md.tree}") + logger.info(f"Project structure:\n{repo_tree}") async with ModelHandler(conf).use_api() as llm: if conf.cli.offline is False: @@ -53,7 +53,7 @@ async def readme_agent(conf: AppConfig, conf_helper: ConfigHelper) -> None: "type": "summaries", "context": { "repo": repo_url, - "tree": conf.md.tree, + "tree": repo_tree, "dependencies": dependencies, "summaries": summaries, }, @@ -62,9 +62,8 @@ async def readme_agent(conf: AppConfig, conf_helper: ConfigHelper) -> None: "type": "features", "context": { "repo": repo_url, - "tree": conf.md.tree, - "dependencies": dependencies, - "summaries": summaries, + "tree": repo_tree, + "summaries": file_context, }, }, { @@ -110,7 +109,7 @@ async def readme_agent(conf: AppConfig, conf_helper: ConfigHelper) -> None: conf.md.default, ) - build_readme_md(conf, conf_helper, dependencies, summaries) + build_readme_md(conf, conf_helper, dependencies, summaries, temp_dir) except Exception as exc_info: logger.error( @@ -153,10 +152,10 @@ def main( repository, temperature, ) - export_to_environment(conf, api_key) - log_settings(conf) + export_to_environment(conf, api_key) + asyncio.run(readme_agent(conf, conf_helper)) except Exception as exc_info: @@ -204,19 +203,6 @@ def export_to_environment(config: AppConfig, api_key: str) -> None: config.cli.offline = True -def tree_command( - conf: AppConfig, conf_helper: ConfigHelper, temp_dir: str -) -> None: - """Updates the markdown tree configuration.""" - tree_generator = TreeGenerator( - conf_helper=conf_helper, - root_dir=temp_dir, - repo_url=conf.git.repository, - repo_name=conf.git.name, - ) - conf.md.tree = conf.md.tree.format(tree_generator.run()) - - def log_settings(conf: AppConfig) -> None: """Log the settings for the CLI application.""" logger.info("Starting README-AI processing...") @@ -228,3 +214,4 @@ def log_settings(conf: AppConfig) -> None: logger.info(f"Header alignment: {conf.md.align}") logger.info(f"Using emojis: {conf.cli.emojis}") logger.info(f"Offline mode: {conf.cli.offline}") + logger.info(f"Repository validations: {conf.git}") diff --git a/readmeai/markdown/badges.py b/readmeai/markdown/badges.py index 87aee7a4..cfe1e1b4 100644 --- a/readmeai/markdown/badges.py +++ b/readmeai/markdown/badges.py @@ -2,7 +2,8 @@ from typing import Dict, Tuple -from readmeai.config.settings import AppConfig, BadgeOptions +from readmeai.config.enums import BadgeOptions +from readmeai.config.settings import AppConfig from readmeai.core.factory import FileHandler from readmeai.core.utils import get_resource_path from readmeai.services.git_utilities import GitService @@ -31,13 +32,11 @@ def build_dependency_badges( return format_badges(badges) -def build_metadata_badges( - config: AppConfig, host: str, repository: str -) -> str: +def build_metadata_badges(config: AppConfig, full_name: str, host: str) -> str: """Build metadata badges using shields.io.""" return config.md.badges_shields.format( host=host, - full_name=repository, + full_name=full_name, badge_style=config.md.badge_style, badge_color=config.md.badge_color, ) @@ -71,16 +70,16 @@ def format_badges(badges: list[str]) -> str: def shields_icons( - conf: AppConfig, deps: list, full_name: str + conf: AppConfig, deps: list, full_name: str, git_host: str ) -> Tuple[str, str]: """ Generates badges for the README using shields.io icons, referencing the repository - https://github.com/Aveek-Saha/GitHub-Profile-Badges. """ badge_set = _read_badge_file(conf.files.shields_icons) - git_host = GitService.extract_name_from_host(conf.git.source) - metadata_badges = build_metadata_badges(conf, git_host, full_name) + metadata_badges = build_metadata_badges(conf, full_name, git_host) + dependency_badges = build_dependency_badges( deps, badge_set, conf.md.badge_style ) @@ -90,14 +89,14 @@ def shields_icons( if ( conf.md.badge_style == BadgeOptions.DEFAULT.value - and git_host != GitService.LOCAL.host + and git_host != GitService.LOCAL ): return ( metadata_badges, "\n", ) - if git_host == GitService.LOCAL.host: + if git_host == GitService.LOCAL: return ( "\n", dependency_badges, diff --git a/readmeai/markdown/builder.py b/readmeai/markdown/builder.py new file mode 100644 index 00000000..994dc112 --- /dev/null +++ b/readmeai/markdown/builder.py @@ -0,0 +1,172 @@ +"""Builds each section of the README Markdown file.""" + +__package__ = "readmeai" + +import re +from pathlib import Path +from typing import List + +from readmeai.config.enums import BadgeOptions +from readmeai.config.settings import AppConfig, ConfigHelper, GitService +from readmeai.core import factory +from readmeai.markdown import badges, tables, tree +from readmeai.markdown.quickstart import getting_started + + +class ReadmeBuilder: + """Builds each section of the README Markdown file.""" + + def __init__( + self, + conf: AppConfig, + helper: ConfigHelper, + dependencies: list, + summaries: tuple, + temp_dir: str, + ): + """Initializes the ReadmeBuilder class.""" + self.conf = conf + self.helper = helper + self.deps = dependencies + self.summaries = summaries + self.temp_dir = temp_dir + self.md = self.conf.md + self.host = self.conf.git.host + self.full_name = self.conf.git.full_name + self.repo_name = self.conf.git.name + self.repo_url = self.conf.git.repository + if self.host == GitService.LOCAL: + self.repo_url = f"../{self.repo_name}" + + @property + def md_header(self) -> str: + """Generates the README header section.""" + if BadgeOptions.SKILLS.value not in self.md.badge_style: + md_shields, md_badges = badges.shields_icons( + self.conf, self.deps, self.full_name, self.host + ) + else: + md_shields = ( + "" + ) + md_badges = badges.skill_icons(self.conf, self.deps) + + return self.md.header.format( + align=self.md.align, + image=self.md.image, + repo_name=self.repo_name.upper(), + slogan=self.md.slogan, + badges_shields=md_shields, + badges=md_badges, + ) + + @property + def md_summaries(self) -> str: + """Generates the README code summaries section.""" + formatted_summaries = tables.format_code_summaries( + self.md.default, + self.summaries, + ) + md_summaries = tables.generate_markdown_tables( + self.md.modules_widget, + formatted_summaries, + self.full_name, + self.repo_url, + ) + return md_summaries + + @property + def md_tree(self) -> str: + """Generates the README directory tree structure.""" + tree_generator = tree.TreeGenerator( + conf_helper=self.helper, + repo_name=self.repo_name, + repo_url=self.repo_url, + root_dir=self.temp_dir, + ) + tree_structure = tree_generator.run() + return self.md.tree.format(tree_structure) + + @property + def md_quickstart(self) -> str: + """Generates the README Getting Started section.""" + project_setup = getting_started(self.conf, self.helper, self.summaries) + md_project_setup = self.md.getting_started.format( + repo_name=self.repo_name, + repo_url=self.repo_url, + language_name=project_setup.top_language_full_name, + install_command=project_setup.install_command, + run_command=project_setup.run_command, + test_command=project_setup.test_command, + ) + return md_project_setup + + def build(self) -> str: + """Builds the README Markdown file.""" + readme_md_sections = [ + self.md_header, + self.md.toc.format(self.repo_name), + self.md.overview, + self.md.features, + self.md_tree, + self.md.modules, + self.md_summaries, + self.md_quickstart, + self.md.contribute.format( + full_name=self.full_name, repo_name=self.repo_name + ), + ] + + if self.conf.cli.emojis is False: + readme_md_sections = self.remove_emojis(readme_md_sections) + + return "\n".join(readme_md_sections) + + @staticmethod + def remove_emojis(content_list: List[str]) -> List[str]: + """Removes emojis from headers and ToC.""" + emoji_pattern = re.compile( + pattern="[" + "\U0001F600-\U0001F64F" # emoticons + "\U0001F300-\U0001F5FF" # symbols & pictographs + "\U0001F680-\U0001F6FF" # transport & map symbols + "\U0001F700-\U0001F77F" # alchemical symbols + "\U0001F780-\U0001F7FF" # Geometric Shapes Extended + "\U0001F800-\U0001F8FF" # Supplemental Arrows-C + "\U0001F900-\U0001F9FF" # Supplemental Symbols and Pictographs + "\U0001FA00-\U0001FA6F" # Chess Symbols + "\U0001FA70-\U0001FAFF" # Symbols and Pictographs Extended-A + "\U00002702-\U000027B0" # Dingbats + "\U000024C2-\U0001F251" # flags (iOS) + "]+", + flags=re.UNICODE, + ) + modified_content = [] + + for section in content_list: + lines = section.split("\n") + for index, line in enumerate(lines): + if ( + line.startswith("#") + or "Table of Contents" in section + or "Quick Links" in section + ): + lines[index] = emoji_pattern.sub("", line) + modified_content.append("\n".join(lines)) + + return modified_content + + +def build_readme_md( + conf: AppConfig, + helper: ConfigHelper, + dependencies: list, + summaries: tuple, + temp_dir: str, +) -> None: + """Builds the README Markdown file.""" + builder = ReadmeBuilder(conf, helper, dependencies, summaries, temp_dir) + readme_md_file = builder.build() + readme_path = Path(conf.files.output) + readme_path.parent.mkdir(parents=True, exist_ok=True) + factory.FileHandler().write(readme_path, readme_md_file) diff --git a/readmeai/markdown/headers.py b/readmeai/markdown/headers.py deleted file mode 100644 index 590ac1bd..00000000 --- a/readmeai/markdown/headers.py +++ /dev/null @@ -1,132 +0,0 @@ -"""Builds each section of the README Markdown file.""" - -__package__ = "readmeai" - -import re -from pathlib import Path -from typing import List - -from readmeai.config.settings import ( - AppConfig, - BadgeOptions, - ConfigHelper, - GitService, -) -from readmeai.core import factory -from readmeai.markdown import badges, tables -from readmeai.markdown.quickstart import getting_started -from readmeai.services.git_utilities import get_remote_full_name - - -def build_readme_md( - conf: AppConfig, - helper: ConfigHelper, - deps: list, - summaries: tuple, -) -> None: - """Constructs each section of the README Markdown file.""" - readme_md_contents = format_readme_md(conf, helper, deps, summaries) - readme_md_file = "\n".join(readme_md_contents) - readme_path = Path(conf.files.output) - readme_path.parent.mkdir(parents=True, exist_ok=True) - factory.FileHandler().write(readme_path, readme_md_file) - - -def format_readme_md( - conf: AppConfig, - helper: ConfigHelper, - deps: list, - summaries: tuple, -) -> List[str]: - """Formats the README markdown contents.""" - repo_url = conf.git.repository - user_name, repo_name = get_remote_full_name(repo_url) - full_name = f"{user_name}/{repo_name}" - if conf.git.source == GitService.LOCAL.value: - repo_url = f"../{repo_name}" - - if BadgeOptions.SKILLS.value not in conf.md.badge_style: - md_shields, md_badges = badges.shields_icons(conf, deps, full_name) - else: - md_shields = "" - md_badges = badges.skill_icons(conf, deps) - - md_header = conf.md.header.format( - align=conf.md.align, - image=conf.md.image, - repo_name=repo_name.upper(), - slogan=conf.md.slogan, - badges_shields=md_shields, - badges=md_badges, - ) - - formatted_code_summaries = tables.format_code_summaries( - conf.md.default, - summaries, - ) - md_code_summaries = tables.generate_markdown_tables( - conf.md.modules_widget, formatted_code_summaries, full_name, repo_url - ) - - project_setup = getting_started(conf, helper, summaries) - md_project_setup = conf.md.getting_started.format( - repo_name=repo_name, - repo_url=repo_url, - language_name=project_setup.top_language_full_name, - install_command=project_setup.install_command, - run_command=project_setup.run_command, - test_command=project_setup.test_command, - ) - - readme_md_sections = [ - md_header, - conf.md.toc.format(repo_name), - conf.md.overview, - conf.md.features, - conf.md.tree, - conf.md.modules, - md_code_summaries, - md_project_setup, - conf.md.contribute.format(full_name=full_name, repo_name=repo_name), - ] - - if conf.cli.emojis is False: - return remove_emojis_from_headers(readme_md_sections) - - return readme_md_sections - - -def remove_emojis_from_headers(content_list: List[str]) -> List[str]: - """Removes emojis from headers and ToC.""" - emoji_pattern = re.compile( - pattern="[" - "\U0001F600-\U0001F64F" # emoticons - "\U0001F300-\U0001F5FF" # symbols & pictographs - "\U0001F680-\U0001F6FF" # transport & map symbols - "\U0001F700-\U0001F77F" # alchemical symbols - "\U0001F780-\U0001F7FF" # Geometric Shapes Extended - "\U0001F800-\U0001F8FF" # Supplemental Arrows-C - "\U0001F900-\U0001F9FF" # Supplemental Symbols and Pictographs - "\U0001FA00-\U0001FA6F" # Chess Symbols - "\U0001FA70-\U0001FAFF" # Symbols and Pictographs Extended-A - "\U00002702-\U000027B0" # Dingbats - "\U000024C2-\U0001F251" # flags (iOS) - "]+", - flags=re.UNICODE, - ) - - # Replace emojis for markdown lines starting with header symbols or ToC - modified_content = [] - - for section in content_list: - lines = section.split("\n") - for index, line in enumerate(lines): - if ( - line.startswith("#") - or "Table of Contents" in section - or "Quick Links" in section - ): - lines[index] = emoji_pattern.sub("", line) - modified_content.append("\n".join(lines)) - - return modified_content diff --git a/readmeai/markdown/tables.py b/readmeai/markdown/tables.py index 6fd9d540..7bc47ada 100644 --- a/readmeai/markdown/tables.py +++ b/readmeai/markdown/tables.py @@ -10,16 +10,14 @@ def construct_markdown_table( - data: List[Tuple[str, str]], repository: str, project_name: str + data: List[Tuple[str, str]], repo_url: str, full_name: str ) -> str: """Builds a Markdown table from the provided data.""" headers = ["File", "Summary"] table_rows = [headers, ["---", "---"]] for module, summary in data: file_name = str(Path(module).name) - hyperlink = create_hyperlink( - file_name, project_name, module, repository - ) + hyperlink = create_hyperlink(file_name, full_name, module, repo_url) table_rows.append([hyperlink, summary]) return format_as_markdown_table(table_rows) @@ -29,13 +27,6 @@ def create_hyperlink( ) -> str: """ Creates a hyperlink for a file, using its Git URL if possible. - logger.debug( - f"Creating git host file hyperlink:\n\ - \tFile: {file_name} \n\ - \tFull name: {full_name} \n\ - \tModule: {module} \n\ - \tRepo URL: {repo_url}" - ) """ if "invalid" in full_name.lower(): return file_name diff --git a/readmeai/markdown/tree.py b/readmeai/markdown/tree.py index d50e2edb..746aacc0 100644 --- a/readmeai/markdown/tree.py +++ b/readmeai/markdown/tree.py @@ -15,70 +15,55 @@ class TreeGenerator: def __init__( self, conf_helper: ConfigHelper, - root_dir: Path, - repo_url: Path, repo_name: str, + repo_url: Path, + root_dir: Path, max_depth: int = 3, ): self.config_helper = conf_helper - self.root_dir = Path(root_dir) self.repo_name = repo_name self.repo_url = repo_url + self.root_dir = Path(root_dir) self.max_depth = max_depth def run(self) -> str: """Generates and formats a tree structure.""" tree_str = self._generate_tree(self.root_dir) - formatted_tree_str = self._format_tree(tree_str) - return formatted_tree_str + return tree_str.replace(self.root_dir.name, f"{self.repo_name}/") def _generate_tree( self, directory: Path, prefix: str = "", is_last: bool = True, - parent_prefix: str = "", depth: int = 0, ) -> str: """Generates a tree structure for a given directory.""" - if depth > self.max_depth: + if depth > self.max_depth or utils.should_ignore( + self.config_helper, directory + ): return "" - if utils.should_ignore(self.config_helper, directory): - return "" + children = sorted(directory.iterdir()) if directory.is_dir() else [] + children = [ + child + for child in children + if not utils.should_ignore(self.config_helper, child) + ] - display_name = "." if directory == self.repo_url else directory.name - box_branch = "โโโ " if is_last else "โโโ " - parts = [parent_prefix + box_branch + display_name] + # If the directory is empty or contains only ignored files, return an empty string + if not children and directory.is_dir(): + return "" - if directory.is_dir(): - parts.append("/\n") - children = sorted( - [ - child - for child in directory.iterdir() - if child.name != ".git" - ] + parts = [f"{prefix}{'โโโ ' if is_last else 'โโโ '}{directory.name}"] + for index, child in enumerate(children): + child_prefix = prefix + (" " if is_last else "โ ") + child_tree = self._generate_tree( + child, child_prefix, index == len(children) - 1, depth + 1 ) - for index, child in enumerate(children): - is_last_child = index == len(children) - 1 - child_prefix = " " if is_last else "โ " - parts.append( - self._generate_tree( - child, - box_branch, - is_last_child, - f"{parent_prefix}{child_prefix}", - depth + 1, - ) - ) - else: - parts.append("\n") - return "".join(parts) + # Append the child tree only if it's not empty + if child_tree: + parts.append(child_tree) - def _format_tree(self, tree_str: str) -> str: - """Formats the directory tree structure.""" - tree_str = tree_str.split("\n", 1) - tree_str[0] = f"โโโ {self.repo_name}/" - return "\n".join(tree_str) + return "\n".join(parts) diff --git a/readmeai/services/git_utilities.py b/readmeai/services/git_utilities.py index 96ae60a5..db477a33 100644 --- a/readmeai/services/git_utilities.py +++ b/readmeai/services/git_utilities.py @@ -1,9 +1,6 @@ """Git service utilities to retrieve repository metadata.""" -import os -import re from pathlib import Path -from typing import Tuple from readmeai.config.settings import GitService from readmeai.core.logger import Logger @@ -14,37 +11,17 @@ def get_remote_file_url(file_path: str, full_name: str, repo_url: str) -> str: """Returns the URL of the file in the remote repository.""" if Path(repo_url).exists(): - return GitService.LOCAL.file_url.format(file_path=file_path) + return GitService.LOCAL.file_url_template.format(file_path=file_path) for service in GitService: - if service.host in repo_url: - return service.file_url.format( + if service in repo_url: + return service.file_url_template.format( full_name=full_name, file_path=file_path ) raise ValueError("Unsupported Git service for URL generation.") -def get_remote_full_name(url_or_path) -> Tuple[str, str]: - """Returns the full name of the repository.""" - if os.path.exists(url_or_path): - return "local", os.path.basename(url_or_path) - - patterns = { - GitService.GITHUB.host: r"https?://github.com/([^/]+)/([^/]+)", - GitService.GITLAB.host: r"https?://gitlab.com/([^/]+)/([^/]+)", - GitService.BITBUCKET.host: r"https?://bitbucket.org/([^/]+)/([^/]+)", - } - - for host, pattern in patterns.items(): - match = re.match(pattern, url_or_path) - if match: - username, repo_name = match.groups() - return username, repo_name - - raise ValueError("Error: invalid repository URL or path.") - - async def parse_repo_url(repo_url: str) -> str: """Parses the repository URL and returns the API URL.""" try: @@ -52,7 +29,7 @@ async def parse_repo_url(repo_url: str) -> str: repo_name = f"{parts[-2]}/{parts[-1]}" for service in GitService: - if service.host in repo_url: + if service in repo_url: api_url = f"{service.api_url}{repo_name}" logger.info(f"{service.name.upper()} API URL: {api_url}") return api_url diff --git a/readmeai/settings/config.toml b/readmeai/settings/config.toml index 2757eeff..70d45459 100644 --- a/readmeai/settings/config.toml +++ b/readmeai/settings/config.toml @@ -54,7 +54,7 @@ header = """\ badge_color = "0080ff" badge_style = "standard" -badges_software = """\tDeveloped with the software and tools below\n
\n\n\t{badges}""" +badges_software = """\tDeveloped with the software and tools below.\n
\n\n\t{badges}"""
badges_shields = """
\t
@@ -228,24 +228,20 @@ Generate your response as a Markdown table with the following columns:
| | Feature | Description |
|----|--------------------|--------------------------------------------------------------------------------------------------------------------|
-| โ๏ธ | **Architecture** | Analyze the structural design of the system here. The description should not exceed 110 characters in total. |
-| ๐ | **Documentation** | Discuss the quality and comprehensiveness of the documentation here. The description should not exceed 110 characters in total.|
-| ๐ | **Dependencies** | Examine the external libraries or other systems that this system relies on here. The description should not exceed 110 characters in total.|
-| ๐งฉ | **Modularity** | Discuss the system's organization into smaller, interchangeable components here. The description should not exceed 110 characters in total.|
-| ๐งช | **Testing** | Evaluate the system's testing strategies and tools here. The description should not exceed 110 characters in total. |
-| โก๏ธ | **Performance** | Analyze how well the system performs, considering speed, efficiency, and resource usage here. The description should not exceed 110 characters in total.|
-| ๐ | **Security** | Assess the measures the system uses to protect data and maintain functionality here. The description should not exceed 110 characters in total.|
-| ๐ | **Version Control**| Discuss the system's version control strategies and tools here. The description should not exceed 110 characters in total.|
-| ๐ | **Integrations** | Evaluate how the system interacts with other systems and services here. The description should not exceed 110 characters in total.|
-| ๐ถ | **Scalability** | Analyze the system's ability to handle growth here. The description should not exceed 110 characters in total. |
+| โ๏ธ | **Architecture** | Analyze the structural design of the system here. The description should not exceed 99 characters in total. |
+| ๐ | **Documentation** | Discuss the quality and comprehensiveness of the documentation here. The description should not exceed 99 characters in total.|
+| ๐ | **Dependencies** | Examine the external libraries or other systems that this system relies on here. The description should not exceed 99 characters in total.|
+| ๐งฉ | **Modularity** | Discuss the system's organization into smaller, interchangeable components here. The description should not exceed 99 characters in total.|
+| ๐งช | **Testing** | Evaluate the system's testing strategies and tools here. The description should not exceed 99 characters in total. |
+| โก๏ธ | **Performance** | Analyze how well the system performs, considering speed, efficiency, and resource usage here. The description should not exceed 99 characters in total.|
+| ๐ | **Security** | Assess the measures the system uses to protect data and maintain functionality here. The description should not exceed 99 characters in total.|
+| ๐ | **Version Control**| Discuss the system's version control strategies and tools here. The description should not exceed 99 characters in total.|
+| ๐ | **Integrations** | Evaluate how the system interacts with other systems and services here. The description should not exceed 99 characters in total.|
+| ๐ถ | **Scalability** | Analyze the system's ability to handle growth here. The description should not exceed 99 characters in total. |
Provided information includes:
-
-- Repository directory structure: {1}
-- Repository dependencies and software: {2}
-- Codebase file summaries: {3}
-
-Thank you for your time and effort!
+Repository directory structure: {1}
+Code files: {2}
"""
overview = """Analyze the codebase named {} ({}) and provide a robust, yet succinct overview of the project.\n
Craft a paragraph, 80 TOKEN MAXIMUM, that encapsulate the core functionalities of the project, its purpose and value proposition.\n
diff --git a/tests/conftest.py b/tests/conftest.py
index 9dbb9af3..316e21ee 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -36,3 +36,9 @@ def mock_summaries():
("/path/to/file2.py", "This is summary for file2.py"),
(".github/workflows/ci.yml", "This is summary for ci.yml"),
]
+
+
+@pytest.fixture(scope="session")
+def mock_temp_dir(tmp_path_factory):
+ """Returns a temporary directory for the test session."""
+ return tmp_path_factory.mktemp("test_readmeai_temp_dir")
diff --git a/tests/test_config/test_enums.py b/tests/test_config/test_enums.py
new file mode 100644
index 00000000..87d9eb5d
--- /dev/null
+++ b/tests/test_config/test_enums.py
@@ -0,0 +1,61 @@
+"""Unit tests for enums and configuration settings."""
+
+import pytest
+
+from readmeai.config.enums import BadgeOptions, GitService, ImageOptions
+
+
+@pytest.mark.parametrize(
+ "service, expected_api_url",
+ [
+ (GitService.LOCAL, None),
+ (GitService.GITHUB, "https://api.github.com/repos/"),
+ (GitService.GITLAB, "https://api.gitlab.com/v4/projects/"),
+ (GitService.BITBUCKET, "https://api.bitbucket.org/2.0/repositories/"),
+ ],
+)
+def test_git_service_api_url(service, expected_api_url):
+ """Test the API URL for the Git service."""
+ assert service.api_url == expected_api_url
+
+
+@pytest.mark.parametrize(
+ "service, expected_file_url_template",
+ [
+ (GitService.LOCAL, "{file_path}"),
+ (
+ GitService.GITHUB,
+ "https://github.com/{full_name}/blob/main/{file_path}",
+ ),
+ (
+ GitService.GITLAB,
+ "https://gitlab.com/{full_name}/-/blob/master/{file_path}",
+ ),
+ (
+ GitService.BITBUCKET,
+ "https://bitbucket.org/{full_name}/src/master/{file_path}",
+ ),
+ ],
+)
+def test_git_service_file_url_template(service, expected_file_url_template):
+ """Test the file URL template for the Git service."""
+ assert service.file_url_template == expected_file_url_template
+
+
+def test_badge_options():
+ """Test the CLI options for badge icons."""
+ assert BadgeOptions.DEFAULT == "default"
+ assert BadgeOptions.FLAT == "flat"
+ assert BadgeOptions.FLAT_SQUARE == "flat-square"
+ assert BadgeOptions.FOR_THE_BADGE == "for-the-badge"
+ assert BadgeOptions.PLASTIC == "plastic"
+ assert BadgeOptions.SKILLS == "skills"
+ assert BadgeOptions.SKILLS_LIGHT == "skills-light"
+ assert BadgeOptions.SOCIAL == "social"
+
+
+def test_image_options():
+ """Test the CLI options for header images."""
+ assert ImageOptions.CUSTOM == "CUSTOM"
+ assert isinstance(ImageOptions.DEFAULT, str)
+ assert isinstance(ImageOptions.BLACK, str)
diff --git a/tests/test_config/test_settings.py b/tests/test_config/test_settings.py
index fcd83ef8..ef111c4f 100644
--- a/tests/test_config/test_settings.py
+++ b/tests/test_config/test_settings.py
@@ -2,19 +2,7 @@
import pytest
-from readmeai.config.settings import BadgeOptions, GitSettings, load_config
-
-
-def test_badge_options():
- """Test the CLI options for badge icons."""
- assert BadgeOptions.DEFAULT == "default"
- assert BadgeOptions.FLAT == "flat"
- assert BadgeOptions.FLAT_SQUARE == "flat-square"
- assert BadgeOptions.FOR_THE_BADGE == "for-the-badge"
- assert BadgeOptions.PLASTIC == "plastic"
- assert BadgeOptions.SKILLS == "skills"
- assert BadgeOptions.SKILLS_LIGHT == "skills-light"
- assert BadgeOptions.SOCIAL == "social"
+from readmeai.config.settings import GitSettings, load_config
def test_git_settings_invalid_url():
diff --git a/tests/test_markdown/test_badges.py b/tests/test_markdown/test_badges.py
index f301b38c..c4dba38b 100644
--- a/tests/test_markdown/test_badges.py
+++ b/tests/test_markdown/test_badges.py
@@ -88,17 +88,9 @@ def test_build_metadata_badges_success(config):
assert "license" in badges
-def test_shields_icons_success(config):
+def test_shields_icons_success(config, mock_dependencies):
"""Tests shields_icons with valid inputs."""
- mock_config = config
- mock_config.git.source = "github.com"
- mock_config.files.shields_icons = config.files.shields_icons
- mock_config.md.badge_style = "flat"
-
- mock_helper = MagicMock()
- mock_helper.language_setup = {"Python": ["install", "run", "test"]}
-
- deps = ["Python", "JavaScript"]
+ config.md.badge_style = "flat"
with patch("readmeai.markdown.badges._read_badge_file") as mock_read:
mock_read.return_value = {
@@ -108,19 +100,16 @@ def test_shields_icons_success(config):
],
}
shields_badges, deps_badges = shields_icons(
- mock_config, deps, "full_name"
+ config, mock_dependencies, "github.com", "github"
)
assert "license" in shields_badges
assert "Python" in deps_badges
- assert "3776AB" in deps_badges
- assert "flat" in deps_badges
+ assert "shields.io" in deps_badges
def test_skill_icons_success(config):
"""Tests skill_icons with valid inputs."""
- mock_config = config
- mock_config.files.skill_icons = config.files.skill_icons
- mock_config.md.badge_style = "skills-light"
+ config.md.badge_style = "skills-light"
mock_icons = {
"icons": {"names": ["fastapi", "py", "redis", "md", "github", "git"]},
"url": {"base_url": "https://skillicons.dev/icons?i="},
@@ -129,7 +118,7 @@ def test_skill_icons_success(config):
with patch("readmeai.markdown.badges._read_badge_file") as mock_read:
mock_read.return_value = mock_icons
- result = skill_icons(mock_config, deps)
+ result = skill_icons(config, deps)
assert "&theme=light" in result
assert """""" in result
assert (
diff --git a/tests/test_markdown/test_builder.py b/tests/test_markdown/test_builder.py
new file mode 100644
index 00000000..a20009a4
--- /dev/null
+++ b/tests/test_markdown/test_builder.py
@@ -0,0 +1,79 @@
+"""Unit tests for the README Markdown file builder."""
+
+from unittest.mock import patch
+
+import pytest
+
+from readmeai.markdown.builder import ReadmeBuilder, build_readme_md
+
+
+@pytest.fixture
+def readme_builder(
+ config, config_helper, mock_dependencies, mock_summaries, mock_temp_dir
+):
+ return ReadmeBuilder(
+ config, config_helper, mock_dependencies, mock_summaries, mock_temp_dir
+ )
+
+
+def test_md_header(readme_builder):
+ """Tests if md_header property returns a string."""
+ header = readme_builder.md_header
+ assert isinstance(header, str)
+
+
+def test_md_summaries(readme_builder):
+ """Tests if md_summaries property returns a string."""
+ summaries = readme_builder.md_summaries
+ assert isinstance(summaries, str)
+
+
+def test_md_tree(readme_builder):
+ """Tests if md_tree property returns a string."""
+ tree = readme_builder.md_tree
+ assert isinstance(tree, str)
+
+
+def test_md_quickstart(readme_builder):
+ """Tests if md_quickstart property returns a string."""
+ quickstart = readme_builder.md_quickstart
+ assert isinstance(quickstart, str)
+
+
+@patch("readmeai.markdown.builder.ReadmeBuilder.remove_emojis")
+def test_build_with_emojis(mock_remove_emojis, config, readme_builder):
+ """Tests if emojis are removed when the config is set."""
+ config.cli.emojis = False
+ readme_builder.build()
+ mock_remove_emojis.assert_called_once()
+
+
+@patch("readmeai.markdown.builder.factory.FileHandler.write")
+def test_build_readme_md(
+ mock_write,
+ config,
+ config_helper,
+ mock_dependencies,
+ mock_summaries,
+ mock_temp_dir,
+):
+ """Tests the build_readme_md function."""
+ build_readme_md(
+ config, config_helper, mock_dependencies, mock_summaries, mock_temp_dir
+ )
+ mock_write.assert_called_once()
+
+
+def test_remove_emojis_from_headers_with_emojis():
+ """Tests the remove_emojis static method with emojis."""
+ content_with_emojis = ["# Header ๐", "## Another Header ๐"]
+ expected_output = ["# Header ", "## Another Header "]
+ result = ReadmeBuilder.remove_emojis(content_with_emojis)
+ assert result == expected_output
+
+
+def test_remove_emojis_from_headers_without_emojis():
+ """Tests the remove_emojis static method without emojis."""
+ content_without_emojis = ["# Header", "## Another Header"]
+ result = ReadmeBuilder.remove_emojis(content_without_emojis)
+ assert result == content_without_emojis
diff --git a/tests/test_markdown/test_headers.py b/tests/test_markdown/test_headers.py
deleted file mode 100644
index 2361d880..00000000
--- a/tests/test_markdown/test_headers.py
+++ /dev/null
@@ -1,62 +0,0 @@
-"""Unit tests for the markdown headers generator."""
-
-from unittest.mock import patch
-
-from readmeai.markdown.headers import (
- build_readme_md,
- format_readme_md,
- remove_emojis_from_headers,
-)
-
-
-def test_build_readme_md(
- config, config_helper, mock_dependencies, mock_summaries
-):
- """Tests the build_readme_md method."""
- with patch("readmeai.core.factory.FileHandler.write") as mock_write:
- build_readme_md(
- config, config_helper, mock_dependencies, mock_summaries
- )
- mock_write.assert_called_once()
- written_content = mock_write.call_args[0][1]
- assert isinstance(written_content, str)
- assert len(written_content) > 0
-
-
-def test_format_readme_md_local_source(
- config, config_helper, mock_dependencies, mock_summaries
-):
- """Tests the format_readme_md method."""
- readme_md = format_readme_md(
- config, config_helper, mock_dependencies, mock_summaries
- )
- assert isinstance(readme_md, list)
- assert len(readme_md) == 9
-
-
-def test_format_readme_md_nonlocal_source(
- config, config_helper, mock_dependencies, mock_summaries
-):
- """Tests the format_readme_md method."""
- mock_config = config
- mock_config.cli.offline = True
- readme_md = format_readme_md(
- mock_config, config_helper, mock_dependencies, mock_summaries
- )
- assert isinstance(readme_md, list)
-
-
-def test_remove_emojis_from_headers_with_emojis():
- """Tests the remove_emojis_from_headers method."""
- content_with_emojis = ["# Header ๐", "## Another Header ๐"]
- expected_output = ["# Header ", "## Another Header "]
- assert remove_emojis_from_headers(content_with_emojis) == expected_output
-
-
-def test_remove_emojis_from_headers_without_emojis():
- """Tests the remove_emojis_from_headers method."""
- content_without_emojis = ["# Header", "## Another Header"]
- assert (
- remove_emojis_from_headers(content_without_emojis)
- == content_without_emojis
- )
diff --git a/tests/test_markdown/test_tables.py b/tests/test_markdown/test_tables.py
index 20176196..bb10f6f9 100644
--- a/tests/test_markdown/test_tables.py
+++ b/tests/test_markdown/test_tables.py
@@ -30,9 +30,8 @@ def test_create_hyperlink(config):
"""Test that the create_hyperlink function creates the hyperlink."""
file_name = "main.py"
module = f"readmeai/{file_name}"
+ full_name = config.git.full_name
repo_url = config.git.repository
- user, project = git_utilities.get_remote_full_name(repo_url)
- full_name = f"{user}/{project}"
repo_file_url = git_utilities.get_remote_file_url(
module, full_name, repo_url
)
diff --git a/tests/test_markdown/test_tree.py b/tests/test_markdown/test_tree.py
index 2a35b6d6..561464b9 100644
--- a/tests/test_markdown/test_tree.py
+++ b/tests/test_markdown/test_tree.py
@@ -1,108 +1,69 @@
"""Unit tests for the markdown tree generator."""
-from pathlib import Path
-from unittest.mock import Mock
-
import pytest
-from readmeai.config.settings import ConfigHelper
from readmeai.markdown.tree import TreeGenerator
@pytest.fixture
-def mock_config_helper():
- """Generates a mock ConfigHelper object."""
- mock_config_helper = Mock(ConfigHelper)
- mock_config_helper.ignore_files = {
- "files": ["*.pyc", "*.pyo"],
- "extensions": ["pyc", "pyo"],
- "directories": ["__pycache__", ".git"],
- }
- return mock_config_helper
-
-
-@pytest.fixture
-def tree_gen(mock_config_helper, tmp_path):
- """Generates a tree structure for a given directory."""
- (tmp_path / "dir1").mkdir()
- (tmp_path / "dir1" / "file1.txt").touch()
+def tree_gen(config_helper, tmp_path):
+ """Fixture for the TreeGenerator class."""
+ dir1 = tmp_path / "dir1"
+ dir1.mkdir()
+ (dir1 / "file1.txt").touch()
(tmp_path / "dir2").mkdir()
- tree_gen = TreeGenerator(
- conf_helper=mock_config_helper,
- root_dir=tmp_path,
- repo_url="http://repo.url",
+ return TreeGenerator(
+ conf_helper=config_helper,
repo_name="TestProject",
+ repo_url=tmp_path,
+ root_dir=tmp_path,
max_depth=3,
)
- return tree_gen
-def test_initialization(mock_config_helper):
- """Tests the initialization of the TreeGenerator class."""
- tree_gen = TreeGenerator(
- conf_helper=mock_config_helper,
- root_dir=Path("/test/path"),
- repo_url="http://repo.url",
- repo_name="TestProject",
- )
- assert tree_gen.root_dir == Path("/test/path")
- assert tree_gen.repo_url == "http://repo.url"
+def test_initialization(tree_gen):
+ """Test initialization of the TreeGenerator class."""
+ assert tree_gen.root_dir.is_dir()
assert tree_gen.repo_name == "TestProject"
assert tree_gen.max_depth == 3
def test_generate_tree(tree_gen, tmp_path):
- """Tests the _generate_tree method."""
+ """Test the _generate_tree method."""
tree = tree_gen._generate_tree(tmp_path)
- expected_tree = """โโโ test_generate_tree0/
- โโโ dir1/
- โ โโโ file1.txt
- โโโ dir2/\n"""
+ expected_tree = (
+ f"โโโ {tmp_path.name}\n" " โโโ dir1\n" " โ โโโ file1.txt"
+ )
assert tree == expected_tree
-def test_format_tree(tree_gen, tmp_path):
- """Tests the _format_tree method."""
- tree = tree_gen._generate_tree(tmp_path)
- formatted_tree = tree_gen._format_tree(tree)
- expected_formatted_tree = """โโโ TestProject/
- โโโ dir1/
- โ โโโ file1.txt
- โโโ dir2/\n"""
- assert formatted_tree == expected_formatted_tree
-
-
@pytest.mark.parametrize(
- "depth, expected",
+ "depth, expected_suffix",
[
- (
- 2,
- """โโโ TestProject/
- โโโ dir1/
- โโโ dir2/\n""",
- ),
- (
- 5,
- """โโโ TestProject/
- โโโ dir1/
- โโโ dir2/
- โโโ dir3/
- โโโ dir4/\n""",
- ),
+ (0, ""),
+ (1, "\n โโโ dir1"),
],
)
-def test_max_depth(depth, expected, mock_config_helper, tmp_path):
- """Tests the max_depth parameter of the TreeGenerator class."""
- (tmp_path / "dir1").mkdir()
- (tmp_path / "dir1" / "dir2").mkdir()
- (tmp_path / "dir1" / "dir2" / "dir3").mkdir()
- (tmp_path / "dir1" / "dir2" / "dir3" / "dir4").mkdir()
- tree_gen = TreeGenerator(
- conf_helper=mock_config_helper,
- root_dir=tmp_path,
- repo_url="http://repo.url",
- repo_name="TestProject",
- max_depth=depth,
- )
- tree = tree_gen.run()
- assert tree == expected
+def test_max_depth_param(tree_gen, depth, expected_suffix, tmp_path):
+ """Test the _generate_tree method."""
+ tree_gen.max_depth = depth
+ tree = tree_gen._generate_tree(tmp_path)
+ expected_tree = f"โโโ {tmp_path.name}{expected_suffix}"
+ assert tree == expected_tree
+
+
+def test_run_method(tree_gen):
+ """Test the run method."""
+ expected_tree = tree_gen.run()
+ assert "TestProject" in expected_tree
+ assert "dir1" in expected_tree
+ assert "file1.txt" in expected_tree
+
+
+def test_ignore_files(tree_gen, tmp_path):
+ """Test that the tree generator ignores files."""
+ (tmp_path / "dir1" / "__pycache__").mkdir()
+ (tmp_path / "dir1" / "file.pyc").touch()
+ tree = tree_gen._generate_tree(tmp_path)
+ assert "__pycache__" not in tree
+ assert "file.pyc" not in tree
diff --git a/tests/test_services/test_git_utilities.py b/tests/test_services/test_git_utilities.py
index 59e6f0df..5ac04f2c 100644
--- a/tests/test_services/test_git_utilities.py
+++ b/tests/test_services/test_git_utilities.py
@@ -1,9 +1,6 @@
"""Unit tests for git utility methods."""
-from readmeai.services.git_utilities import (
- get_remote_file_url,
- get_remote_full_name,
-)
+from readmeai.services.git_utilities import get_remote_file_url
def test_get_remote_file_url():
@@ -16,10 +13,3 @@ def test_get_remote_file_url():
"https://github.com/eli64s/readme-ai/blob/main/readmeai/main.py"
)
assert file_url == expected_url
-
-
-def test_get_remote_full_name():
- """Test method for getting the remote repository name."""
- repo_url = "https://github.com/eli64s/readme-ai"
- expected_name = tuple(["eli64s", "readme-ai"])
- assert get_remote_full_name(repo_url) == expected_name