This commit is contained in:
2025-11-27 17:11:34 +03:00
commit c8e75a42d1
24 changed files with 1173 additions and 0 deletions

10
.gitignore vendored Normal file
View File

@@ -0,0 +1,10 @@
# Python-generated files
__pycache__/
*.py[oc]
build/
dist/
wheels/
*.egg-info
# Virtual environments
.venv

1
.python-version Normal file
View File

@@ -0,0 +1 @@
3.15

21
README.md Normal file
View File

@@ -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 .

2
ppmc/__init__.py Normal file
View File

@@ -0,0 +1,2 @@
__version__ = "0.1.0"
__author__ = "Your Name"

5
ppmc/__main__.py Normal file
View File

@@ -0,0 +1,5 @@
"""Entry point for ppmc when run as module."""
from ppmc.cli import main
if __name__ == "__main__":
main()

138
ppmc/cli.py Normal file
View File

@@ -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()

126
ppmc/generator.py Normal file
View File

@@ -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)

52
ppmc/templates/default/.gitignore vendored Normal file
View File

@@ -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/

View File

@@ -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)

View File

@@ -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
}
}
]
}

View File

@@ -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
```

View File

@@ -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
$<$<CONFIG:Release>:-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
$<$<CONFIG:Release>:/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()

View File

@@ -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

View File

@@ -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

View File

@@ -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
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>
$<INSTALL_INTERFACE:include>
)
# 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 }})

View File

@@ -0,0 +1,4 @@
#include "calculator.hpp"
// Implementation is header-only for this simple example
// This file exists to demonstrate the project structure

View File

@@ -0,0 +1,80 @@
#pragma once
#include <cstdint>
#include <stdexcept>
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 }}

View File

@@ -0,0 +1,39 @@
#include <iostream>
#include <exception>
#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;
}
}

View File

@@ -0,0 +1,5 @@
{
"name": "default",
"description": "Modern C++23 project with CMake, Conan, and GTest",
"version": "1.0.0"
}

View File

@@ -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)

View File

@@ -0,0 +1,151 @@
#include <gtest/gtest.h>
#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<std::tuple<int, int, int>> {
};
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)
)
);

41
pyproject.toml Normal file
View File

@@ -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/**/*"]

7
setup.py Normal file
View File

@@ -0,0 +1,7 @@
"""Setup script for ppmc."""
from setuptools import setup, find_packages
setup(
packages=find_packages(),
include_package_data=True,
)

116
uv.lock generated Normal file
View File

@@ -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" },
]