commit c8e75a42d1380cc9cbb7c08de26407cf61faa44b Author: Leonov Artur (Depish) Date: Thu Nov 27 17:11:34 2025 +0300 init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..505a3b1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +# Python-generated files +__pycache__/ +*.py[oc] +build/ +dist/ +wheels/ +*.egg-info + +# Virtual environments +.venv diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..93a848f --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.15 diff --git a/README.md b/README.md new file mode 100644 index 0000000..84f4462 --- /dev/null +++ b/README.md @@ -0,0 +1,21 @@ +# PPMC - C++ Project Manager/Creator + +A command-line utility for creating modern C++ projects from templates. + +## Features + +- 🎯 Quick project scaffolding +- 📦 Conan package manager integration +- 🔨 CMake build system +- ✅ GTest testing framework +- 🎨 Multiple project templates +- 📚 Documentation ready + +## Installation + +### From Source + +```bash +git clone https://github.com/yourusername/ppmc.git +cd ppmc +pip install -e . \ No newline at end of file diff --git a/ppmc/__init__.py b/ppmc/__init__.py new file mode 100644 index 0000000..8e3ddca --- /dev/null +++ b/ppmc/__init__.py @@ -0,0 +1,2 @@ +__version__ = "0.1.0" +__author__ = "Your Name" \ No newline at end of file diff --git a/ppmc/__main__.py b/ppmc/__main__.py new file mode 100644 index 0000000..7fa98cf --- /dev/null +++ b/ppmc/__main__.py @@ -0,0 +1,5 @@ +"""Entry point for ppmc when run as module.""" +from ppmc.cli import main + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/ppmc/cli.py b/ppmc/cli.py new file mode 100644 index 0000000..a7a7b92 --- /dev/null +++ b/ppmc/cli.py @@ -0,0 +1,138 @@ +import sys +from pathlib import Path +import click +from colorama import init, Fore, Style + +from ppmc.generator import ProjectGenerator + +# Initialize colorama for cross-platform colored output +init(autoreset=True) + + +@click.group() +@click.version_option() +def main(): + """PPMC - C++ Project Manager and Creator. + + A utility for creating modern C++ projects from templates. + """ + pass + + +@main.command() +@click.argument('project_name') +@click.option( + '--template', '-t', + default='default', + help='Template to use for project creation' +) +@click.option( + '--path', '-p', + type=click.Path(), + default='.', + help='Path where to create the project' +) +@click.option( + '--force', '-f', + is_flag=True, + help='Overwrite existing project directory' +) +def create(project_name: str, template: str, path: str, force: bool): + """Create a new C++ project. + + PROJECT_NAME is the name of your new C++ project. + """ + try: + generator = ProjectGenerator() + + # Validate project name + if not _is_valid_project_name(project_name): + click.echo( + f"{Fore.RED}✗ Invalid project name. " + f"Use alphanumeric characters, hyphens, and underscores only.", + err=True + ) + sys.exit(1) + + # Check if template exists + available_templates = generator.list_templates() + if template not in available_templates: + click.echo( + f"{Fore.RED}✗ Template '{template}' not found.", + err=True + ) + click.echo(f"\nAvailable templates:") + for tmpl in available_templates: + click.echo(f" • {tmpl}") + sys.exit(1) + + # Create project + project_path = Path(path) / project_name + + if project_path.exists() and not force: + click.echo( + f"{Fore.RED}✗ Directory '{project_path}' already exists. " + f"Use --force to overwrite.", + err=True + ) + sys.exit(1) + + click.echo(f"{Fore.CYAN}Creating C++ project '{project_name}'...") + click.echo(f"Template: {Fore.YELLOW}{template}") + click.echo(f"Location: {Fore.YELLOW}{project_path.absolute()}\n") + + generator.generate_project(project_name, template, str(project_path)) + + click.echo(f"\n{Fore.GREEN}✓ Project created successfully!\n") + + # Print next steps + _print_next_steps(project_name, project_path) + + except Exception as e: + click.echo(f"{Fore.RED}✗ Error: {str(e)}", err=True) + sys.exit(1) + + +@main.command() +def templates(): + """List available project templates.""" + generator = ProjectGenerator() + available_templates = generator.list_templates() + + click.echo(f"{Fore.CYAN}Available templates:\n") + + for template_name in available_templates: + info = generator.get_template_info(template_name) + click.echo(f"{Fore.GREEN}• {Style.BRIGHT}{template_name}") + if info and 'description' in info: + click.echo(f" {info['description']}") + click.echo() + + +def _is_valid_project_name(name: str) -> bool: + """Check if project name is valid.""" + import re + return bool(re.match(r'^[a-zA-Z0-9_-]+$', name)) + + +def _print_next_steps(project_name: str, project_path: Path): + """Print next steps after project creation.""" + click.echo(f"{Fore.CYAN}Next steps:") + click.echo(f"\n {Fore.YELLOW}1. {Style.RESET_ALL}Navigate to your project:") + click.echo(f" {Fore.WHITE}cd {project_path}") + + click.echo(f"\n {Fore.YELLOW}2. {Style.RESET_ALL}Install Conan dependencies:") + click.echo(f" {Fore.WHITE}conan install . --output-folder=build --build=missing") + + click.echo(f"\n {Fore.YELLOW}3. {Style.RESET_ALL}Configure the project:") + click.echo(f" {Fore.WHITE}cmake --preset=conan-release") + + click.echo(f"\n {Fore.YELLOW}4. {Style.RESET_ALL}Build the project:") + click.echo(f" {Fore.WHITE}cmake --build --preset=conan-release") + + click.echo(f"\n {Fore.YELLOW}5. {Style.RESET_ALL}Run tests:") + click.echo(f" {Fore.WHITE}cd build/Release && ctest\n") + + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/ppmc/generator.py b/ppmc/generator.py new file mode 100644 index 0000000..3249775 --- /dev/null +++ b/ppmc/generator.py @@ -0,0 +1,126 @@ +import json +import shutil +from pathlib import Path +from typing import Dict, List, Optional +from jinja2 import Environment, FileSystemLoader, Template + + +class ProjectGenerator: + """Generates C++ projects from templates.""" + + def __init__(self): + """Initialize the project generator.""" + self.templates_dir = Path(__file__).parent / 'templates' + + def list_templates(self) -> List[str]: + """List all available templates. + + Returns: + List of template names. + """ + templates = [] + if not self.templates_dir.exists(): + return templates + + for item in self.templates_dir.iterdir(): + if item.is_dir(): + templates.append(item.name) + + return sorted(templates) + + def get_template_info(self, template_name: str) -> Optional[Dict]: + """Get information about a template. + + Args: + template_name: Name of the template. + + Returns: + Dictionary with template information or None. + """ + config_file = self.templates_dir / template_name / 'template_config.json' + if not config_file.exists(): + return None + + try: + with open(config_file, 'r', encoding='utf-8') as f: + return json.load(f) + except Exception: + return None + + def generate_project(self, project_name: str, template_name: str, output_path: str): + """Generate a new C++ project. + + Args: + project_name: Name of the project. + template_name: Template to use. + output_path: Where to create the project. + """ + template_path = self.templates_dir / template_name + if not template_path.exists(): + raise ValueError(f"Template '{template_name}' not found") + + output_path = Path(output_path) + output_path.mkdir(parents=True, exist_ok=True) + + # Prepare template context + context = { + 'project_name': project_name, + 'project_name_upper': project_name.upper(), + 'project_name_lower': project_name.lower(), + } + + # Process template directory + self._process_directory(template_path, output_path, context) + + def _process_directory(self, template_dir: Path, output_dir: Path, context: Dict): + """Recursively process template directory. + + Args: + template_dir: Source template directory. + output_dir: Destination directory. + context: Template context variables. + """ + for item in template_dir.iterdir(): + # Skip config file + if item.name == 'template_config.json': + continue + + if item.is_dir(): + # Create subdirectory + new_dir = output_dir / item.name + new_dir.mkdir(exist_ok=True) + # Recursively process + self._process_directory(item, new_dir, context) + else: + # Process file + self._process_file(item, output_dir, context) + + def _process_file(self, template_file: Path, output_dir: Path, context: Dict): + """Process a single template file. + + Args: + template_file: Source template file. + output_dir: Destination directory. + context: Template context variables. + """ + # Determine output filename + output_name = template_file.name + if output_name.endswith('.jinja'): + output_name = output_name[:-6] # Remove .jinja extension + + output_file = output_dir / output_name + + # Check if file should be templated + if template_file.suffix == '.jinja': + # Process as Jinja2 template + with open(template_file, 'r', encoding='utf-8') as f: + template_content = f.read() + + template = Template(template_content) + rendered_content = template.render(**context) + + with open(output_file, 'w', encoding='utf-8') as f: + f.write(rendered_content) + else: + # Copy file as-is + shutil.copy2(template_file, output_file) \ No newline at end of file diff --git a/ppmc/templates/default/.gitignore b/ppmc/templates/default/.gitignore new file mode 100644 index 0000000..7c455bd --- /dev/null +++ b/ppmc/templates/default/.gitignore @@ -0,0 +1,52 @@ +# Build directories +build/ +cmake-build-*/ +out/ + +# Conan +conan.lock +conaninfo.txt +conanbuildinfo.* +graph_info.json +CMakeUserPresets.json + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ +.DS_Store + +# Compiled files +*.o +*.obj +*.exe +*.out +*.app +*.dll +*.so +*.dylib +*.a +*.lib + +# CMake +CMakeCache.txt +CMakeFiles/ +cmake_install.cmake +install_manifest.txt +compile_commands.json +CTestTestfile.cmake + +# Documentation +docs/html/ +docs/latex/ + +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +*.egg-info/ +dist/ \ No newline at end of file diff --git a/ppmc/templates/default/CMakeLists.txt.jinja b/ppmc/templates/default/CMakeLists.txt.jinja new file mode 100644 index 0000000..c8d113d --- /dev/null +++ b/ppmc/templates/default/CMakeLists.txt.jinja @@ -0,0 +1,31 @@ +cmake_minimum_required(VERSION 3.25) + +project({{ project_name }} + VERSION 1.0.0 + DESCRIPTION "{{ project_name }} - A modern C++ project" + LANGUAGES CXX +) + +# Set C++ standard +set(CMAKE_CXX_STANDARD 23) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + +# Export compile commands for IDE support +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) + +# Include custom CMake modules +list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake") + +# Find Conan-generated dependencies +find_package(GTest REQUIRED) + +# Include compile options +include(compile_options) + +# Enable testing +enable_testing() + +# Add subdirectories +add_subdirectory(src) +add_subdirectory(tests) \ No newline at end of file diff --git a/ppmc/templates/default/CMakePresets.json.jinja b/ppmc/templates/default/CMakePresets.json.jinja new file mode 100644 index 0000000..93ae773 --- /dev/null +++ b/ppmc/templates/default/CMakePresets.json.jinja @@ -0,0 +1,69 @@ +{ + "version": 6, + "cmakeMinimumRequired": { + "major": 3, + "minor": 25, + "patch": 0 + }, + "configurePresets": [ + { + "name": "conan-default", + "hidden": true, + "cacheVariables": { + "CMAKE_POLICY_DEFAULT_CMP0091": "NEW" + }, + "binaryDir": "${sourceDir}/build/${presetName}", + "installDir": "${sourceDir}/install/${presetName}" + }, + { + "name": "conan-debug", + "displayName": "Debug", + "description": "Debug build using Conan toolchain", + "inherits": "conan-default", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Debug", + "CMAKE_TOOLCHAIN_FILE": "${sourceDir}/build/conan_toolchain.cmake" + } + }, + { + "name": "conan-release", + "displayName": "Release", + "description": "Release build using Conan toolchain", + "inherits": "conan-default", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Release", + "CMAKE_TOOLCHAIN_FILE": "${sourceDir}/build/conan_toolchain.cmake" + } + } + ], + "buildPresets": [ + { + "name": "conan-debug", + "configurePreset": "conan-debug", + "configuration": "Debug" + }, + { + "name": "conan-release", + "configurePreset": "conan-release", + "configuration": "Release" + } + ], + "testPresets": [ + { + "name": "conan-debug", + "configurePreset": "conan-debug", + "configuration": "Debug", + "output": { + "outputOnFailure": true + } + }, + { + "name": "conan-release", + "configurePreset": "conan-release", + "configuration": "Release", + "output": { + "outputOnFailure": true + } + } + ] +} \ No newline at end of file diff --git a/ppmc/templates/default/README.md.jinja b/ppmc/templates/default/README.md.jinja new file mode 100644 index 0000000..dd76f93 --- /dev/null +++ b/ppmc/templates/default/README.md.jinja @@ -0,0 +1,75 @@ +# {{ project_name }} + +A modern C++23 project built with CMake and Conan. + +## Overview + +{{ project_name }} is a C++ project template that demonstrates best practices for modern C++ development. + +## Features + +- 🚀 **C++23** - Latest C++ standard +- 📦 **Conan** - Dependency management +- 🔨 **CMake** - Modern build system +- ✅ **GTest** - Unit testing framework +- 🛡️ **Strict Compilation** - Enhanced warnings and sanitizers +- 📚 **Documentation Ready** - Doxygen support + +## Requirements + +- CMake 3.25 or later +- C++23 compatible compiler (GCC 11+, Clang 14+, MSVC 2022+) +- Conan 2.0+ +- Python 3.8+ (for Conan) + +## Building the Project + +### 1. Install Dependencies + +```bash +conan install . --output-folder=build --build=missing + +### 2. Configure + +```shell +cmake --preset=conan-release +``` + +Or for debug builds: + +```shell +cmake --preset=conan-debug +``` + +### 3. Build + +```shell +cmake --build --preset=conan-release +``` + +### 4. Run + +```shell +./build/Release/{{ project_name_lower }} +``` + + +## Running Tests +### Build and Run Tests + +```shell +cd build/Release +ctest --output-on-failure +``` + +Or using the test preset: + +```shell +ctest --preset=conan-release +``` + +### Run Specific Tests + +```shell +./build/Release/tests/{{ project_name_lower }}_tests +``` \ No newline at end of file diff --git a/ppmc/templates/default/cmake/compile_options.cmake.jinja b/ppmc/templates/default/cmake/compile_options.cmake.jinja new file mode 100644 index 0000000..1c1ea39 --- /dev/null +++ b/ppmc/templates/default/cmake/compile_options.cmake.jinja @@ -0,0 +1,96 @@ +# compile_options.cmake +# Provides target_compile_checks function for setting strict compile options + +function(target_compile_checks TARGET) + # Get the compiler ID + set(COMPILER_ID ${CMAKE_CXX_COMPILER_ID}) + + if(COMPILER_ID MATCHES "GNU|Clang") + target_compile_options(${TARGET} PRIVATE + # Enable many warnings + -Wall + -Wextra + -Wpedantic + + # Additional warnings + -Wshadow + -Wnon-virtual-dtor + -Wold-style-cast + -Wcast-align + -Wunused + -Woverloaded-virtual + -Wconversion + -Wsign-conversion + -Wmisleading-indentation + -Wduplicated-cond + -Wduplicated-branches + -Wlogical-op + -Wnull-dereference + -Wuseless-cast + -Wdouble-promotion + -Wformat=2 + + # Treat warnings as errors in Release builds + $<$:-Werror> + ) + + # Enable sanitizers in Debug mode + if(CMAKE_BUILD_TYPE STREQUAL "Debug") + target_compile_options(${TARGET} PRIVATE + -fsanitize=address + -fsanitize=undefined + -fno-omit-frame-pointer + ) + target_link_options(${TARGET} PRIVATE + -fsanitize=address + -fsanitize=undefined + ) + endif() + + elseif(COMPILER_ID MATCHES "MSVC") + target_compile_options(${TARGET} PRIVATE + # Warning level 4 + /W4 + + # Treat warnings as errors in Release builds + $<$:/WX> + + # Additional warnings + /w14242 # Implicit conversion + /w14254 # Operator with different sizes + /w14263 # Member function does not override + /w14265 # Class has virtual functions but destructor is not virtual + /w14287 # Unsigned/negative constant mismatch + /we4289 # Loop control variable used outside loop + /w14296 # Expression is always true/false + /w14311 # Pointer truncation + /w14545 # Expression before comma has no effect + /w14546 # Function call before comma missing argument list + /w14547 # Operator before comma has no effect + /w14549 # Operator before comma has no effect + /w14555 # Expression has no effect + /w14619 # Pragma warning number doesn't exist + /w14640 # Thread-unsafe static member initialization + /w14826 # Conversion is sign-extended + /w14905 # Wide string literal cast + /w14906 # String literal cast + /w14928 # Illegal copy-initialization + ) + + # Enable address sanitizer in Debug mode (MSVC 2019+) + if(CMAKE_BUILD_TYPE STREQUAL "Debug" AND MSVC_VERSION GREATER_EQUAL 1920) + target_compile_options(${TARGET} PRIVATE /fsanitize=address) + endif() + endif() + + # Set optimization flags + if(CMAKE_BUILD_TYPE STREQUAL "Release") + if(COMPILER_ID MATCHES "GNU|Clang") + target_compile_options(${TARGET} PRIVATE -O3) + elseif(COMPILER_ID MATCHES "MSVC") + target_compile_options(${TARGET} PRIVATE /O2) + endif() + endif() + + message(STATUS "Applied compile checks to target: ${TARGET}") +endfunction() \ No newline at end of file diff --git a/ppmc/templates/default/conanfile.py.jinja b/ppmc/templates/default/conanfile.py.jinja new file mode 100644 index 0000000..c19242b --- /dev/null +++ b/ppmc/templates/default/conanfile.py.jinja @@ -0,0 +1,29 @@ +from conan import ConanFile +from conan.tools.cmake import cmake_layout + + +class {{ project_name }}Conan(ConanFile): + name = "{{ project_name_lower }}" + version = "1.0.0" + + # Binary configuration + settings = "os", "compiler", "build_type", "arch" + + # Sources + exports_sources = "CMakeLists.txt", "src/*", "tests/*", "cmake/*" + + def requirements(self): + """Define project dependencies.""" + self.requires("gtest/1.14.0") + + def build_requirements(self): + """Define build dependencies.""" + self.tool_requires("cmake/3.27.1") + + def layout(self): + """Define project layout.""" + cmake_layout(self) + + def generate(self): + """Generate necessary files for build.""" + pass \ No newline at end of file diff --git a/ppmc/templates/default/docs/Doxyfile.jinja b/ppmc/templates/default/docs/Doxyfile.jinja new file mode 100644 index 0000000..6fca126 --- /dev/null +++ b/ppmc/templates/default/docs/Doxyfile.jinja @@ -0,0 +1,19 @@ +# Doxyfile for {{ project_name }} + +PROJECT_NAME = "{{ project_name }}" +PROJECT_NUMBER = 1.0.0 +PROJECT_BRIEF = "A modern C++ project" +OUTPUT_DIRECTORY = . +INPUT = ../src +RECURSIVE = YES +EXTRACT_ALL = YES +EXTRACT_PRIVATE = YES +EXTRACT_STATIC = YES +GENERATE_HTML = YES +GENERATE_LATEX = NO +HTML_OUTPUT = html +USE_MDFILE_AS_MAINPAGE = ../README.md +MARKDOWN_SUPPORT = YES +AUTOLINK_SUPPORT = YES +GENERATE_TREEVIEW = YES +HAVE_DOT = NO \ No newline at end of file diff --git a/ppmc/templates/default/src/CMakeLists.txt.jinja b/ppmc/templates/default/src/CMakeLists.txt.jinja new file mode 100644 index 0000000..d8df6fe --- /dev/null +++ b/ppmc/templates/default/src/CMakeLists.txt.jinja @@ -0,0 +1,37 @@ +# Source files +set(SOURCES + calculator.cpp +) + +set(HEADERS + calculator.hpp +) + +# Create library +add_library({{ project_name_lower }}_lib STATIC + ${SOURCES} + ${HEADERS} +) + +# Set include directories +target_include_directories({{ project_name_lower }}_lib + PUBLIC + $ + $ +) + +# Apply compile options +target_compile_checks({{ project_name_lower }}_lib) + +# Create executable +add_executable({{ project_name_lower }} + main.cpp +) + +target_link_libraries({{ project_name_lower }} + PRIVATE + {{ project_name_lower }}_lib +) + +# Apply compile options +target_compile_checks({{ project_name_lower }}) \ No newline at end of file diff --git a/ppmc/templates/default/src/calculator.cpp.jinja b/ppmc/templates/default/src/calculator.cpp.jinja new file mode 100644 index 0000000..65d19cd --- /dev/null +++ b/ppmc/templates/default/src/calculator.cpp.jinja @@ -0,0 +1,4 @@ +#include "calculator.hpp" + +// Implementation is header-only for this simple example +// This file exists to demonstrate the project structure \ No newline at end of file diff --git a/ppmc/templates/default/src/calculator.hpp.jinja b/ppmc/templates/default/src/calculator.hpp.jinja new file mode 100644 index 0000000..fa9bf07 --- /dev/null +++ b/ppmc/templates/default/src/calculator.hpp.jinja @@ -0,0 +1,80 @@ +#pragma once + +#include +#include + +namespace {{ project_name_lower }} { + +/** + * @brief A simple calculator class for demonstration. + * + * This class provides basic arithmetic operations and serves + * as an example of a well-structured C++23 class. + */ +class Calculator { +public: + /** + * @brief Add two integers. + * @param a First operand + * @param b Second operand + * @return Sum of a and b + */ + [[nodiscard]] static constexpr auto add(int32_t a, int32_t b) noexcept -> int32_t { + return a + b; + } + + /** + * @brief Subtract two integers. + * @param a First operand + * @param b Second operand + * @return Difference of a and b + */ + [[nodiscard]] static constexpr auto subtract(int32_t a, int32_t b) noexcept -> int32_t { + return a - b; + } + + /** + * @brief Multiply two integers. + * @param a First operand + * @param b Second operand + * @return Product of a and b + */ + [[nodiscard]] static constexpr auto multiply(int32_t a, int32_t b) noexcept -> int32_t { + return a * b; + } + + /** + * @brief Divide two integers. + * @param a Dividend + * @param b Divisor + * @return Quotient of a and b + * @throws std::invalid_argument if b is zero + */ + [[nodiscard]] static constexpr auto divide(int32_t a, int32_t b) -> int32_t { + if (b == 0) { + throw std::invalid_argument("Division by zero"); + } + return a / b; + } + + /** + * @brief Calculate power (a^b). + * @param base Base value + * @param exponent Exponent value (must be non-negative) + * @return base raised to the power of exponent + * @throws std::invalid_argument if exponent is negative + */ + [[nodiscard]] static constexpr auto power(int32_t base, int32_t exponent) -> int64_t { + if (exponent < 0) { + throw std::invalid_argument("Negative exponent not supported"); + } + + int64_t result = 1; + for (int32_t i = 0; i < exponent; ++i) { + result *= base; + } + return result; + } +}; + +} // namespace {{ project_name_lower }} \ No newline at end of file diff --git a/ppmc/templates/default/src/main.cpp.jinja b/ppmc/templates/default/src/main.cpp.jinja new file mode 100644 index 0000000..8ed8df9 --- /dev/null +++ b/ppmc/templates/default/src/main.cpp.jinja @@ -0,0 +1,39 @@ +#include +#include +#include "calculator.hpp" + +auto main() -> int { + using {{ project_name_lower }}::Calculator; + + try { + std::cout << "=== {{ project_name }} Calculator Demo ===\n\n"; + + // Demonstrate addition + std::cout << "Addition: 10 + 5 = " + << Calculator::add(10, 5) << '\n'; + + // Demonstrate subtraction + std::cout << "Subtraction: 10 - 5 = " + << Calculator::subtract(10, 5) << '\n'; + + // Demonstrate multiplication + std::cout << "Multiplication: 10 * 5 = " + << Calculator::multiply(10, 5) << '\n'; + + // Demonstrate division + std::cout << "Division: 10 / 5 = " + << Calculator::divide(10, 5) << '\n'; + + // Demonstrate power + std::cout << "Power: 2^8 = " + << Calculator::power(2, 8) << '\n'; + + std::cout << "\nAll operations completed successfully!\n"; + + return 0; + } + catch (const std::exception& e) { + std::cerr << "Error: " << e.what() << '\n'; + return 1; + } +} \ No newline at end of file diff --git a/ppmc/templates/default/template_config.json b/ppmc/templates/default/template_config.json new file mode 100644 index 0000000..6b5c65f --- /dev/null +++ b/ppmc/templates/default/template_config.json @@ -0,0 +1,5 @@ +{ + "name": "default", + "description": "Modern C++23 project with CMake, Conan, and GTest", + "version": "1.0.0" +} \ No newline at end of file diff --git a/ppmc/templates/default/tests/CMakeLists.txt.jinja b/ppmc/templates/default/tests/CMakeLists.txt.jinja new file mode 100644 index 0000000..bd8dcfe --- /dev/null +++ b/ppmc/templates/default/tests/CMakeLists.txt.jinja @@ -0,0 +1,19 @@ +# Test executable +add_executable({{ project_name_lower }}_tests + test_calculator.cpp +) + +# Link with library and GTest +target_link_libraries({{ project_name_lower }}_tests + PRIVATE + {{ project_name_lower }}_lib + GTest::gtest + GTest::gtest_main +) + +# Apply compile options +target_compile_checks({{ project_name_lower }}_tests) + +# Discover tests +include(GoogleTest) +gtest_discover_tests({{ project_name_lower }}_tests) \ No newline at end of file diff --git a/ppmc/templates/default/tests/test_calculator.cpp.jinja b/ppmc/templates/default/tests/test_calculator.cpp.jinja new file mode 100644 index 0000000..4926897 --- /dev/null +++ b/ppmc/templates/default/tests/test_calculator.cpp.jinja @@ -0,0 +1,151 @@ +#include +#include "calculator.hpp" + +using {{ project_name_lower }}::Calculator; + +// Test fixture for Calculator tests +class CalculatorTest : public ::testing::Test { +protected: + void SetUp() override { + // Setup code if needed + } + + void TearDown() override { + // Cleanup code if needed + } +}; + +// === Addition Tests === + +TEST_F(CalculatorTest, AddPositiveNumbers) { + EXPECT_EQ(Calculator::add(5, 3), 8); + EXPECT_EQ(Calculator::add(100, 200), 300); +} + +TEST_F(CalculatorTest, AddNegativeNumbers) { + EXPECT_EQ(Calculator::add(-5, -3), -8); + EXPECT_EQ(Calculator::add(-100, -200), -300); +} + +TEST_F(CalculatorTest, AddMixedNumbers) { + EXPECT_EQ(Calculator::add(5, -3), 2); + EXPECT_EQ(Calculator::add(-5, 3), -2); +} + +TEST_F(CalculatorTest, AddZero) { + EXPECT_EQ(Calculator::add(0, 0), 0); + EXPECT_EQ(Calculator::add(5, 0), 5); + EXPECT_EQ(Calculator::add(0, 5), 5); +} + +// === Subtraction Tests === + +TEST_F(CalculatorTest, SubtractPositiveNumbers) { + EXPECT_EQ(Calculator::subtract(10, 5), 5); + EXPECT_EQ(Calculator::subtract(100, 50), 50); +} + +TEST_F(CalculatorTest, SubtractNegativeNumbers) { + EXPECT_EQ(Calculator::subtract(-10, -5), -5); + EXPECT_EQ(Calculator::subtract(-5, -10), 5); +} + +TEST_F(CalculatorTest, SubtractMixedNumbers) { + EXPECT_EQ(Calculator::subtract(10, -5), 15); + EXPECT_EQ(Calculator::subtract(-10, 5), -15); +} + +// === Multiplication Tests === + +TEST_F(CalculatorTest, MultiplyPositiveNumbers) { + EXPECT_EQ(Calculator::multiply(5, 3), 15); + EXPECT_EQ(Calculator::multiply(10, 10), 100); +} + +TEST_F(CalculatorTest, MultiplyNegativeNumbers) { + EXPECT_EQ(Calculator::multiply(-5, -3), 15); + EXPECT_EQ(Calculator::multiply(-10, -10), 100); +} + +TEST_F(CalculatorTest, MultiplyMixedNumbers) { + EXPECT_EQ(Calculator::multiply(5, -3), -15); + EXPECT_EQ(Calculator::multiply(-5, 3), -15); +} + +TEST_F(CalculatorTest, MultiplyByZero) { + EXPECT_EQ(Calculator::multiply(0, 5), 0); + EXPECT_EQ(Calculator::multiply(5, 0), 0); + EXPECT_EQ(Calculator::multiply(0, 0), 0); +} + +// === Division Tests === + +TEST_F(CalculatorTest, DividePositiveNumbers) { + EXPECT_EQ(Calculator::divide(10, 2), 5); + EXPECT_EQ(Calculator::divide(100, 10), 10); +} + +TEST_F(CalculatorTest, DivideNegativeNumbers) { + EXPECT_EQ(Calculator::divide(-10, -2), 5); + EXPECT_EQ(Calculator::divide(-100, -10), 10); +} + +TEST_F(CalculatorTest, DivideMixedNumbers) { + EXPECT_EQ(Calculator::divide(10, -2), -5); + EXPECT_EQ(Calculator::divide(-10, 2), -5); +} + +TEST_F(CalculatorTest, DivideByZeroThrows) { + EXPECT_THROW(Calculator::divide(10, 0), std::invalid_argument); + EXPECT_THROW(Calculator::divide(-10, 0), std::invalid_argument); +} + +// === Power Tests === + +TEST_F(CalculatorTest, PowerPositive) { + EXPECT_EQ(Calculator::power(2, 3), 8); + EXPECT_EQ(Calculator::power(5, 2), 25); + EXPECT_EQ(Calculator::power(10, 3), 1000); +} + +TEST_F(CalculatorTest, PowerZeroExponent) { + EXPECT_EQ(Calculator::power(5, 0), 1); + EXPECT_EQ(Calculator::power(100, 0), 1); +} + +TEST_F(CalculatorTest, PowerOneExponent) { + EXPECT_EQ(Calculator::power(5, 1), 5); + EXPECT_EQ(Calculator::power(100, 1), 100); +} + +TEST_F(CalculatorTest, PowerNegativeBase) { + EXPECT_EQ(Calculator::power(-2, 3), -8); + EXPECT_EQ(Calculator::power(-2, 2), 4); +} + +TEST_F(CalculatorTest, PowerNegativeExponentThrows) { + EXPECT_THROW(Calculator::power(2, -1), std::invalid_argument); + EXPECT_THROW(Calculator::power(5, -3), std::invalid_argument); +} + +// === Parameterized Tests Example === + +class CalculatorAddParameterizedTest : public ::testing::TestWithParam> { +}; + +TEST_P(CalculatorAddParameterizedTest, AddVariousInputs) { + auto [a, b, expected] = GetParam(); + EXPECT_EQ(Calculator::add(a, b), expected); +} + +INSTANTIATE_TEST_SUITE_P( + AdditionTests, + CalculatorAddParameterizedTest, + ::testing::Values( + std::make_tuple(1, 1, 2), + std::make_tuple(0, 0, 0), + std::make_tuple(-1, 1, 0), + std::make_tuple(100, 200, 300), + std::make_tuple(-50, -50, -100) + ) +); \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..2e94e45 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,41 @@ +[project] +name = "ppmc" +version = "0.1.0" +description = "C++ Project Manager and Creator" +readme = "README.md" +requires-python = ">=3.13" + +license = {text = "MIT"} +authors = [ + {name = "Your Name", email = "your.email@example.com"} +] +keywords = ["c++", "project", "generator", "cmake", "conan"] +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.13", + "Topic :: Software Development :: Code Generators", +] + +dependencies = [ + "argparse>=1.4.0", + "click>=8.3.1", + "colorama>=0.4.6", + "jinja2>=3.1.6", +] + +[project.scripts] +ppmc = "ppmc.cli:main" + +[project.urls] +Homepage = "https://github.com/yourusername/ppmc" +Repository = "https://github.com/yourusername/ppmc" + +[tool.setuptools.packages.find] +where = ["."] +include = ["ppmc*"] + +[tool.setuptools.package-data] +ppmc = ["templates/**/*"] \ No newline at end of file diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..d3dfd97 --- /dev/null +++ b/setup.py @@ -0,0 +1,7 @@ +"""Setup script for ppmc.""" +from setuptools import setup, find_packages + +setup( + packages=find_packages(), + include_package_data=True, +) \ No newline at end of file diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..a34bf67 --- /dev/null +++ b/uv.lock @@ -0,0 +1,116 @@ +version = 1 +revision = 3 +requires-python = ">=3.13" + +[[package]] +name = "argparse" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/18/dd/e617cfc3f6210ae183374cd9f6a26b20514bbb5a792af97949c5aacddf0f/argparse-1.4.0.tar.gz", hash = "sha256:62b089a55be1d8949cd2bc7e0df0bddb9e028faefc8c32038cc84862aefdd6e4", size = 70508, upload-time = "2015-09-12T20:22:16.217Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f2/94/3af39d34be01a24a6e65433d19e107099374224905f1e0cc6bbe1fd22a2f/argparse-1.4.0-py2.py3-none-any.whl", hash = "sha256:c31647edb69fd3d465a847ea3157d37bed1f95f19760b11a47aa91c04b666314", size = 23000, upload-time = "2015-09-14T16:03:16.137Z" }, +] + +[[package]] +name = "click" +version = "8.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, +] + +[[package]] +name = "ppmc" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "argparse" }, + { name = "click" }, + { name = "colorama" }, + { name = "jinja2" }, +] + +[package.metadata] +requires-dist = [ + { name = "argparse", specifier = ">=1.4.0" }, + { name = "click", specifier = ">=8.3.1" }, + { name = "colorama", specifier = ">=0.4.6" }, + { name = "jinja2", specifier = ">=3.1.6" }, +]